using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using System; using System.Collections.Generic; using Terraria; using Terraria.ModLoader; namespace SoulHarvest.Common; /// Client-only one-second scene tears left by the Void blade tip. [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 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? 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); } }