using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using System;
using System.Collections.Generic;
using Terraria;
using Terraria.ModLoader;
namespace SoulHarvest.Common;
/// <summary>Client-only one-second scene tears left by the Void blade tip.</summary>
[Autoload(Side = ModSide.Client)]
internal sealed class ReaperVoidTrailVisualSystem : ModSystem
{
private const int PointLifetime = 60;
private const int MaximumTrails = 24;
private const int MaximumPointsPerTrail = 72;
private static readonly Dictionary<TrailKey, ReaperVoidTrail> Trails = [];
private readonly record struct TrailKey(int Owner, int Identity);
internal static void RecordTip(int owner, int identity, ReaperStage stage,
Vector2 position)
{
if (Main.dedServ || Main.gameMenu || !float.IsFinite(position.X) || !float.IsFinite(position.Y))
return;
TrailKey key = new(owner, identity);
if (!Trails.TryGetValue(key, out ReaperVoidTrail? trail))
{
if (Trails.Count >= MaximumTrails)
{
TrailKey keyToRemove = default;
foreach (TrailKey candidate in Trails.Keys)
{
keyToRemove = candidate;
break;
}
Trails.Remove(keyToRemove);
}
trail = new ReaperVoidTrail
{
Stage = stage,
Seed = unchecked(owner * 73856093 ^ identity * 19349663)
};
Trails[key] = trail;
}
trail.Stage = stage;
if (trail.Points.Count > 0)
{
float distanceSquared = Vector2.DistanceSquared(trail.Points[^1].Position, position);
if (distanceSquared < 2.5f * 2.5f)
{
trail.Points[^1].Position = position;
trail.Points[^1].Age = 0;
return;
}
if (distanceSquared > 180f * 180f)
trail.Points.Clear();
}
trail.Points.Add(new ReaperVoidTrailPoint { Position = position });
if (trail.Points.Count > MaximumPointsPerTrail)
trail.Points.RemoveAt(0);
}
public override void PostUpdateEverything()
{
if (Main.gameMenu)
{
Trails.Clear();
return;
}
List<TrailKey>? empty = null;
foreach ((TrailKey key, ReaperVoidTrail trail) in Trails)
{
for (int index = trail.Points.Count - 1; index >= 0; index--)
{
if (++trail.Points[index].Age > PointLifetime)
trail.Points.RemoveAt(index);
}
if (trail.Points.Count == 0)
(empty ??= []).Add(key);
}
if (empty is not null)
foreach (TrailKey key in empty)
Trails.Remove(key);
}
public override void PostDrawTiles()
{
if (Main.dedServ || Main.gameMenu || Trails.Count == 0)
return;
SpriteBatch batch = Main.spriteBatch;
batch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp,
DepthStencilState.None, RasterizerState.CullNone, null,
Main.GameViewMatrix.TransformationMatrix);
foreach (ReaperVoidTrail trail in Trails.Values)
DrawTrail(batch, trail);
batch.End();
}
private static void DrawTrail(SpriteBatch batch, ReaperVoidTrail trail)
{
float width = trail.Stage switch
{
ReaperStage.StageI => 7f,
ReaperStage.StageII => 10f,
// Let the fully evolved Void Reaper leave a tear twice as wide as
// its previous Stage III presentation.
_ => 26f
};
for (int index = 1; index < trail.Points.Count; index++)
{
ReaperVoidTrailPoint older = trail.Points[index - 1];
ReaperVoidTrailPoint newer = trail.Points[index];
float age = Math.Max(older.Age, newer.Age);
float opacity = 1f - Smooth01((age - 34f) / 26f);
if (opacity <= 0.001f || Vector2.DistanceSquared(older.Position, newer.Position) > 90f * 90f)
continue;
float progress = (index - 0.5f) / Math.Max(1f, trail.Points.Count - 1f);
// Both ends collapse continuously to a blade point. This removes the
// blunt caps while preserving the same curved, tip-following path.
float taper = (float)Math.Pow(Math.Max(0f,
Math.Sin(progress * MathHelper.Pi)), 0.46f);
ReaperVoidBackdropRenderer.DrawTrailStrip(batch, older.Position, newer.Position,
Math.Max(0.55f, width * taper), opacity * 0.92f, drawStars: true,
branchSeed: trail.Seed, branchIndex: index);
}
}
public override void OnWorldUnload() => Trails.Clear();
public override void Unload() => Trails.Clear();
private static float Smooth01(float value)
{
value = MathHelper.Clamp(value, 0f, 1f);
return value * value * (3f - 2f * value);
}
}
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using System;
using System.Collections.Generic;
using Terraria;
using Terraria.ModLoader;
namespace SoulHarvest.Common;
/// <summary>Client-only one-second scene tears left by the Void blade tip.</summary>
[Autoload(Side = ModSide.Client)]
internal sealed class ReaperVoidTrailVisualSystem : ModSystem
{
private const int PointLifetime = 60;
private const int MaximumTrails = 24;
private const int MaximumPointsPerTrail = 72;
private static readonly Dictionary<TrailKey, ReaperVoidTrail> Trails = [];
private readonly record struct TrailKey(int Owner, int Identity);
internal static void RecordTip(int owner, int identity, ReaperStage stage,
Vector2 position)
{
if (Main.dedServ || Main.gameMenu || !float.IsFinite(position.X) || !float.IsFinite(position.Y))
return;
TrailKey key = new(owner, identity);
if (!Trails.TryGetValue(key, out ReaperVoidTrail? trail))
{
if (Trails.Count >= MaximumTrails)
{
TrailKey keyToRemove = default;
foreach (TrailKey candidate in Trails.Keys)
{
keyToRemove = candidate;
break;
}
Trails.Remove(keyToRemove);
}
trail = new ReaperVoidTrail
{
Stage = stage,
Seed = unchecked(owner * 73856093 ^ identity * 19349663)
};
Trails[key] = trail;
}
trail.Stage = stage;
if (trail.Points.Count > 0)
{
float distanceSquared = Vector2.DistanceSquared(trail.Points[^1].Position, position);
if (distanceSquared < 2.5f * 2.5f)
{
trail.Points[^1].Position = position;
trail.Points[^1].Age = 0;
return;
}
if (distanceSquared > 180f * 180f)
trail.Points.Clear();
}
trail.Points.Add(new ReaperVoidTrailPoint { Position = position });
if (trail.Points.Count > MaximumPointsPerTrail)
trail.Points.RemoveAt(0);
}
public override void PostUpdateEverything()
{
if (Main.gameMenu)
{
Trails.Clear();
return;
}
List<TrailKey>? empty = null;
foreach ((TrailKey key, ReaperVoidTrail trail) in Trails)
{
for (int index = trail.Points.Count - 1; index >= 0; index--)
{
if (++trail.Points[index].Age > PointLifetime)
trail.Points.RemoveAt(index);
}
if (trail.Points.Count == 0)
(empty ??= []).Add(key);
}
if (empty is not null)
foreach (TrailKey key in empty)
Trails.Remove(key);
}
public override void PostDrawTiles()
{
if (Main.dedServ || Main.gameMenu || Trails.Count == 0)
return;
SpriteBatch batch = Main.spriteBatch;
batch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp,
DepthStencilState.None, RasterizerState.CullNone, null,
Main.GameViewMatrix.TransformationMatrix);
foreach (ReaperVoidTrail trail in Trails.Values)
DrawTrail(batch, trail);
batch.End();
}
private static void DrawTrail(SpriteBatch batch, ReaperVoidTrail trail)
{
float width = trail.Stage switch
{
ReaperStage.StageI => 7f,
ReaperStage.StageII => 10f,
// Let the fully evolved Void Reaper leave a tear twice as wide as
// its previous Stage III presentation.
_ => 26f
};
for (int index = 1; index < trail.Points.Count; index++)
{
ReaperVoidTrailPoint older = trail.Points[index - 1];
ReaperVoidTrailPoint newer = trail.Points[index];
float age = Math.Max(older.Age, newer.Age);
float opacity = 1f - Smooth01((age - 34f) / 26f);
if (opacity <= 0.001f || Vector2.DistanceSquared(older.Position, newer.Position) > 90f * 90f)
continue;
float progress = (index - 0.5f) / Math.Max(1f, trail.Points.Count - 1f);
// Both ends collapse continuously to a blade point. This removes the
// blunt caps while preserving the same curved, tip-following path.
float taper = (float)Math.Pow(Math.Max(0f,
Math.Sin(progress * MathHelper.Pi)), 0.46f);
ReaperVoidBackdropRenderer.DrawTrailStrip(batch, older.Position, newer.Position,
Math.Max(0.55f, width * taper), opacity * 0.92f, drawStars: true,
branchSeed: trail.Seed, branchIndex: index);
}
}
public override void OnWorldUnload() => Trails.Clear();
public override void Unload() => Trails.Clear();
private static float Smooth01(float value)
{
value = MathHelper.Clamp(value, 0f, 1f);
return value * value * (3f - 2f * value);
}
}