XFE Git
XFE Studio Git
Git 首页 全局搜索
XFE 主站 文档 NuGet

DeathMod

【Terraria】DeathMod (now aka Soul Harvest)

公开
关注 0 Fork 0 Star 0
UTF-8
using SoulHarvest.Common;
using SoulHarvest.Items;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using System;
using System.Collections.Generic;
using System.IO;
using Terraria;
using Terraria.Audio;
using Terraria.GameContent;
using Terraria.ID;
using Terraria.ModLoader;

namespace SoulHarvest.Projectiles;

public sealed class ReaperActionControllerProjectile : ModProjectile
{
    private const int BaseDashStartTick = 3;
    private const int BaseDashTravelFrames = 7;
    private const int VoidSpecialTeleportTick = 2;
    private const int VoidSpecialVisualEndTick = 14;
    private const int VoidUltimateBlackHoleTick = 32;
    private SickleCombatSnapshot snapshot;
    private Vector2 aim;
    private Vector2 specialTargetOffset;
    private Vector2 ultimateFocusWorld;
    private bool ultimate;
    private bool configured;
    private int timer;
    private int chargeFrames;
    private int releaseTimer;
    private bool releaseRequested;
    private bool specialReleased;
    private bool bloodOvercharged;
    private long bloodSiphonDamage;
    private int deathInvocationCooldown;
    private int deathInvocationCount;
    private int deathAssemblyTimer;
    private bool deathAssemblyCancelling;
    private int deathAssemblyCancelStartTimer;
    private int deathAssemblyCancelTimer;
    private bool serverAuthorized;
    private Vector2 baseDashStart;
    private Vector2 baseDashEnd;
    private bool baseDashReady;
    // First three entries serve the staged special; all five serve the pentagram
    // route of the Infernal ultimate. The authoritative endpoints are serialized
    // so every client draws the exact terrain-clipped paths used for collision.
    private readonly Vector2[] infernalDashStarts = new Vector2[5];
    private readonly Vector2[] infernalDashEnds = new Vector2[5];
    private readonly bool[] infernalDashReady = new bool[5];
    private float visualWeaponAngle;
    private float previousVisualWeaponAngle;
    private int visualSwingDirection = 1;
    private bool visualDirectionInitialized;
    private float visualCharge;
    private ulong playedVisualEvents;
    private int previousVisualTimer;
    private int previousVisualReleaseTimer;
    private bool visualClockInitialized;

    private bool IsChargedSpecial => !ultimate
        && snapshot.Form is ReaperFormId.Blood or ReaperFormId.Frost
            or ReaperFormId.Soul or ReaperFormId.Death;

    // Death Mouse2 invokes remote echo scythes; unlike the primary swing it never
    // materializes the large held weapon, so it must enter its attack clock on
    // the first authoritative tick without sharing the primary assembly prelude.
    private bool HasDeathAssemblyPrelude => false;
    private bool IsDeathAssemblyActive => HasDeathAssemblyPrelude
        && !deathAssemblyCancelling
        && timer == 0 && deathAssemblyTimer <= snapshot.AssemblyFrames;
    internal bool DeathAssemblyComplete => !HasDeathAssemblyPrelude
        || !deathAssemblyCancelling
            && deathAssemblyTimer >= snapshot.AssemblyFrames;
    internal int DeathAssemblyElapsed => deathAssemblyTimer;
    internal bool DeathAssemblyCancelling => deathAssemblyCancelling;
    internal ReaperFormId ConfiguredForm => snapshot.Form;

#if DEBUG
    internal static bool SawDeathRightHeldWeaponSuppressed { get; private set; }

    internal static void ResetDebugMetrics()
        => SawDeathRightHeldWeaponSuppressed = false;
#endif

    internal bool IsBloodSiphonActive => configured && !ultimate
        && snapshot.Form == ReaperFormId.Blood && !specialReleased;

    public override string Texture => "Terraria/Images/Projectile_0";

    public static int Spawn(Terraria.DataStructures.IEntitySource source, Player player,
        in SickleCombatSnapshot snapshot, Vector2 aim, bool ultimate)
    {
        if (Main.netMode == NetmodeID.MultiplayerClient)
            return -1;
        Vector2 requestedTarget = aim;
        Vector2 normalizedAim = requestedTarget.SafeNormalize(Vector2.UnitX * player.direction);
        int index = Projectile.NewProjectile(source, player.MountedCenter, normalizedAim,
            ModContent.ProjectileType<ReaperActionControllerProjectile>(), 0, 0f, player.whoAmI);
        if (index < 0 || index >= Main.maxProjectiles)
            return -1;
        Projectile projectile = Main.projectile[index];
        if (projectile.ModProjectile is ReaperActionControllerProjectile controller)
            controller.Configure(snapshot, requestedTarget, ultimate, player.Center);
        projectile.netUpdate = true;
        if (Main.netMode == NetmodeID.Server)
        {
            // This projectile is born inside a client request handler. Send its
            // first authoritative snapshot immediately after Configure so the
            // owner cannot miss the short-lived controller or keep retrying the
            // right-click while waiting for the regular projectile update pass.
            NetMessage.SendData(MessageID.SyncProjectile, -1, -1, null,
                index);
        }
        return index;
    }

    public override void SetDefaults()
    {
        Projectile.width = 2;
        Projectile.height = 2;
        Projectile.friendly = false;
        Projectile.hostile = false;
        Projectile.tileCollide = false;
        Projectile.ignoreWater = true;
        Projectile.penetrate = -1;
        Projectile.timeLeft = 360;
        // The controller now owns the held-weapon presentation for specials and
        // assigns itself to Player.heldProj every active tick. Keep it in the held
        // projectile draw layer, matching SickleSwingProjectile, instead of drawing
        // the weapon as an ordinary world projectile over unrelated entities.
        Projectile.hide = true;
        Projectile.netImportant = true;
    }

    public override bool ShouldUpdatePosition() => false;
    public override bool? CanDamage() => false;

    public override void AI()
    {
        if (Main.netMode == NetmodeID.Server && !serverAuthorized)
        {
            Projectile.Kill();
            return;
        }
        if (!configured || Projectile.owner < 0 || Projectile.owner >= Main.maxPlayers)
            return;
        Player player = Main.player[Projectile.owner];
        if (!player.active || player.dead
            || !ultimate && player.HeldItem.ModItem is not NormalSickle)
        {
            Projectile.Kill();
            return;
        }

        // The owner can release Mouse2 before this server-owned projectile has
        // replicated to the client. In that ordering there was no local
        // controller for ProcessTriggers to release, so the late copy refreshed
        // timeLeft forever and blocked every later primary/special action. The
        // server remains authoritative for damage; this only guarantees that the
        // owning client's held presentation follows its real input lifecycle.
        bool debugSimulatedSpecialHeld = false;
#if DEBUG
        debugSimulatedSpecialHeld = ReaperMultiplayerClientSelfTest
            .SimulatedSpecialHeld;
#endif
        if (Main.netMode == NetmodeID.MultiplayerClient
            && Projectile.owner == Main.myPlayer
            && IsChargedSpecial
            && SoulHarvest.SpecialAttackKeybind?.Current != true
            && !Main.mouseRight
            && !debugSimulatedSpecialHeld)
        {
            RequestRelease();
        }

        Projectile.Center = player.MountedCenter;
        if (UpdateDeathAssemblyPrelude(player))
            return;
        timer++;
        if (ultimate)
            UpdateUltimate(player);
        else if (IsChargedSpecial)
            UpdateChargedSpecial(player);
        else
            UpdateSpecial(player);
        ApplyBaseDisplacement(player);
        ApplyInfernalDisplacement(player);
        if (Projectile.active)
        {
            UpdatePlayerPresentation(player);
        }
        SpawnAmbientVisuals(player);
    }

    public override bool PreDraw(ref Color lightColor)
    {
        if (Main.dedServ || !configured || Projectile.owner < 0 || Projectile.owner >= Main.maxPlayers)
            return false;

        Player player = Main.player[Projectile.owner];
        if (!player.active)
            return false;

        Vector2 grip = player.MountedCenter;
        if (deathAssemblyCancelling || IsDeathAssemblyActive)
        {
            Texture2D assemblyTexture = ModContent.Request<Texture2D>(
                ReaperCombatRegistry.GetTexturePath(snapshot.Form,
                    snapshot.Stage)).Value;
            Vector2 normalizedAssemblyAnchor = ReaperCombatRegistry
                .GetHandleAnchor(snapshot.Form, snapshot.Stage);
            Vector2 assemblyAnchor = new(
                assemblyTexture.Width * normalizedAssemblyAnchor.X,
                assemblyTexture.Height * normalizedAssemblyAnchor.Y);
            SpriteEffects assemblyEffects = visualSwingDirection < 0
                ? SpriteEffects.FlipVertically : SpriteEffects.None;
            if ((assemblyEffects & SpriteEffects.FlipVertically) != 0)
                assemblyAnchor.Y = assemblyTexture.Height - assemblyAnchor.Y;
            float assemblyRotation = visualWeaponAngle
                + ReaperCombatRegistry.GetTextureRotationCorrection(
                    snapshot.Form, snapshot.Stage);
            int actionSeed = Projectile.identity * 43 + 1709;
            if (deathAssemblyCancelling)
            {
                DeathSickleAssemblyTextureSystem.DrawCancellation(
                    assemblyTexture, grip, assemblyRotation, assemblyAnchor,
                    ReaperCombatRegistry.DeathWeaponDrawScale,
                    assemblyEffects, lightColor,
                    deathAssemblyCancelStartTimer,
                    deathAssemblyCancelTimer, snapshot.AssemblyFrames,
                    actionSeed, largeDeathLayout: true);
            }
            else
            {
                DeathSickleAssemblyTextureSystem.Draw(assemblyTexture, grip,
                    assemblyRotation, assemblyAnchor,
                    ReaperCombatRegistry.DeathWeaponDrawScale,
                    assemblyEffects, lightColor, deathAssemblyTimer,
                    snapshot.AssemblyFrames, actionSeed,
                    largeDeathLayout: true);
            }
            return false;
        }
        Color primary = ReaperCombatRegistry.GetPrimaryColor(snapshot.Form) with { A = 0 };
        Color secondary = ReaperCombatRegistry.GetSecondaryColor(snapshot.Form) with { A = 0 };
        if (!ultimate && IsChargedSpecial && !specialReleased)
            DrawChargeSeal(grip, primary, secondary);
        if (!ultimate)
            DrawSpecialStoryboard(player, grip);
        else if (snapshot.Form == ReaperFormId.Infernal)
            DrawInfernalUltimateRoute(TextureAssets.MagicPixel.Value);

        if (!ultimate && snapshot.Form == ReaperFormId.Death)
        {
#if DEBUG
            SawDeathRightHeldWeaponSuppressed = true;
#endif
            return false;
        }

        Texture2D texture = ModContent.Request<Texture2D>(
            ReaperCombatRegistry.GetTexturePath(snapshot.Form, snapshot.Stage)).Value;
        Vector2 normalizedAnchor = ReaperCombatRegistry.GetHandleAnchor(snapshot.Form, snapshot.Stage);
        Vector2 anchor = new(texture.Width * normalizedAnchor.X, texture.Height * normalizedAnchor.Y);
        float scale = snapshot.Form == ReaperFormId.Death
            ? ReaperCombatRegistry.DeathWeaponDrawScale
            : 0.9f + snapshot.StageNumber * 0.045f;
        float correctedAngle = visualWeaponAngle
            + ReaperCombatRegistry.GetTextureRotationCorrection(snapshot.Form, snapshot.Stage);
        SpriteEffects weaponEffects = visualSwingDirection < 0
            ? SpriteEffects.FlipVertically : SpriteEffects.None;
        if ((weaponEffects & SpriteEffects.FlipVertically) != 0)
            anchor.Y = texture.Height - anchor.Y;

        // Motionless casts do not receive decorative weapon echoes: those would
        // read as a second swing even when the held weapon itself stays still.
        int heldEchoCount = (ultimate && snapshot.Form == ReaperFormId.Void)
            || (!ultimate && snapshot.Form == ReaperFormId.Death)
            ? 0
            : snapshot.Form == ReaperFormId.Soul
                ? 3 + snapshot.StageNumber
                : 2;
        for (int echo = heldEchoCount; echo >= 1; echo--)
        {
            float offset = (0.018f + visualCharge * 0.014f) * echo;
            Color echoColor = snapshot.Form == ReaperFormId.Soul
                ? Color.Lerp(primary, secondary,
                    echo / (float)(heldEchoCount + 1))
                : primary;
            Main.EntitySpriteDraw(texture, grip - Main.screenPosition, null,
                echoColor * (snapshot.Form == ReaperFormId.Soul
                    ? 0.18f / Math.Max(1f, echo * 0.72f)
                    : 0.12f / echo), correctedAngle - offset, anchor,
                scale * (1f + echo * 0.018f), weaponEffects);
        }
        Main.EntitySpriteDraw(texture, grip - Main.screenPosition, null, lightColor,
            correctedAngle, anchor, scale, weaponEffects);
        return false;
    }

    public override void SendExtraAI(BinaryWriter writer)
    {
        writer.Write(configured);
        if (!configured)
            return;
        snapshot.Write(writer);
        writer.WriteVector2(aim);
        writer.WriteVector2(specialTargetOffset);
        writer.WriteVector2(ultimateFocusWorld);
        writer.Write(ultimate);
        writer.Write(Math.Max(0, timer));
        writer.Write(Math.Max(0, chargeFrames));
        writer.Write(Math.Max(0, releaseTimer));
        writer.Write(releaseRequested);
        writer.Write(specialReleased);
        writer.Write(bloodOvercharged);
        writer.Write(Math.Max(0, deathInvocationCooldown));
        writer.Write(Math.Max(0, deathInvocationCount));
        writer.Write(Math.Max(0, deathAssemblyTimer));
        writer.Write(deathAssemblyCancelling);
        writer.Write(Math.Max(0, deathAssemblyCancelStartTimer));
        writer.Write(Math.Max(0, deathAssemblyCancelTimer));
        WritePath(writer, baseDashReady, baseDashStart, baseDashEnd);
        for (int index = 0; index < infernalDashReady.Length; index++)
            WritePath(writer, infernalDashReady[index], infernalDashStarts[index], infernalDashEnds[index]);
    }

    public override void ReceiveExtraAI(BinaryReader reader)
    {
        bool wasConfigured = configured;
        bool incomingConfigured = reader.ReadBoolean();
        if (!incomingConfigured)
        {
            if (Main.netMode != NetmodeID.Server)
                configured = false;
            return;
        }
        SickleCombatSnapshot incomingSnapshot = SickleCombatSnapshot.Read(reader);
        Vector2 incomingAim = reader.ReadVector2().SafeNormalize(Vector2.UnitX);
        Vector2 incomingTargetOffset = reader.ReadVector2();
        Vector2 incomingUltimateFocusWorld = reader.ReadVector2();
        bool incomingUltimate = reader.ReadBoolean();
        int incomingTimer = Math.Max(0, reader.ReadInt32());
        int incomingChargeFrames = Math.Max(0, reader.ReadInt32());
        int incomingReleaseTimer = Math.Max(0, reader.ReadInt32());
        bool incomingReleaseRequested = reader.ReadBoolean();
        bool incomingSpecialReleased = reader.ReadBoolean();
        bool incomingBloodOvercharged = reader.ReadBoolean();
        int incomingDeathInvocationCooldown = Math.Max(0, reader.ReadInt32());
        int incomingDeathInvocationCount = Math.Max(0, reader.ReadInt32());
        int incomingDeathAssemblyTimer = Math.Max(0, reader.ReadInt32());
        bool incomingDeathAssemblyCancelling = reader.ReadBoolean();
        int incomingDeathAssemblyCancelStartTimer = Math.Max(0,
            reader.ReadInt32());
        int incomingDeathAssemblyCancelTimer = Math.Max(0,
            reader.ReadInt32());
        ReadPath(reader, out bool incomingBaseReady, out Vector2 incomingBaseStart, out Vector2 incomingBaseEnd);
        bool[] incomingInfernalReady = new bool[infernalDashReady.Length];
        Vector2[] incomingInfernalStarts = new Vector2[infernalDashStarts.Length];
        Vector2[] incomingInfernalEnds = new Vector2[infernalDashEnds.Length];
        for (int index = 0; index < incomingInfernalReady.Length; index++)
        {
            ReadPath(reader, out incomingInfernalReady[index],
                out incomingInfernalStarts[index], out incomingInfernalEnds[index]);
        }
        if (Main.netMode == NetmodeID.Server)
            return;
        configured = true;
        snapshot = incomingSnapshot;
        aim = incomingAim;
        specialTargetOffset = incomingTargetOffset;
        ultimateFocusWorld = incomingUltimateFocusWorld;
        ultimate = incomingUltimate;
        timer = incomingTimer;
        chargeFrames = incomingChargeFrames;
        releaseTimer = incomingReleaseTimer;
        releaseRequested = incomingReleaseRequested;
        specialReleased = incomingSpecialReleased;
        bloodOvercharged = incomingBloodOvercharged;
        deathInvocationCooldown = incomingDeathInvocationCooldown;
        deathInvocationCount = incomingDeathInvocationCount;
        deathAssemblyTimer = incomingDeathAssemblyTimer;
        deathAssemblyCancelling = incomingDeathAssemblyCancelling;
        deathAssemblyCancelStartTimer = incomingDeathAssemblyCancelStartTimer;
        deathAssemblyCancelTimer = incomingDeathAssemblyCancelTimer;
        baseDashReady = incomingBaseReady;
        baseDashStart = incomingBaseStart;
        baseDashEnd = incomingBaseEnd;
        for (int index = 0; index < incomingInfernalReady.Length; index++)
        {
            infernalDashReady[index] = incomingInfernalReady[index];
            infernalDashStarts[index] = incomingInfernalStarts[index];
            infernalDashEnds[index] = incomingInfernalEnds[index];
        }
        if (!wasConfigured)
        {
            // Initial replication establishes a floor: a late-joining client does
            // not burst-play every event which happened before it saw the action.
            previousVisualTimer = incomingTimer;
            previousVisualReleaseTimer = incomingReleaseTimer;
            visualClockInitialized = true;
        }
    }

    private static void WritePath(BinaryWriter writer, bool ready, Vector2 start, Vector2 end)
    {
        writer.Write(ready);
        if (!ready)
            return;
        writer.WriteVector2(start);
        writer.WriteVector2(end);
    }

    private static void ReadPath(BinaryReader reader, out bool ready, out Vector2 start, out Vector2 end)
    {
        ready = reader.ReadBoolean();
        if (!ready)
        {
            start = Vector2.Zero;
            end = Vector2.Zero;
            return;
        }
        start = reader.ReadVector2();
        end = reader.ReadVector2();
    }

    private void Configure(in SickleCombatSnapshot value, Vector2 direction,
        bool isUltimate, Vector2 playerCenter)
    {
        snapshot = value;
        aim = direction.SafeNormalize(Vector2.UnitX);
        specialTargetOffset = direction;
        if (!float.IsFinite(specialTargetOffset.X) || !float.IsFinite(specialTargetOffset.Y))
            specialTargetOffset = aim * 240f;
        float targetDistance = specialTargetOffset.Length();
        if (targetDistance > 2000f)
            specialTargetOffset *= 2000f / targetDistance;
        else if (targetDistance < 2f)
            specialTargetOffset = aim * 240f;
        ultimateFocusWorld = playerCenter + specialTargetOffset;
        if (!float.IsFinite(ultimateFocusWorld.X)
            || !float.IsFinite(ultimateFocusWorld.Y))
        {
            ultimateFocusWorld = playerCenter + aim * 240f;
        }
        ultimate = isUltimate;
        configured = true;
        timer = 0;
        chargeFrames = 0;
        releaseTimer = 0;
        releaseRequested = false;
        specialReleased = false;
        bloodOvercharged = false;
        bloodSiphonDamage = 0L;
        deathInvocationCooldown = 0;
        deathInvocationCount = 0;
        deathAssemblyTimer = 0;
        deathAssemblyCancelling = false;
        deathAssemblyCancelStartTimer = 0;
        deathAssemblyCancelTimer = 0;
        baseDashStart = Vector2.Zero;
        baseDashEnd = Vector2.Zero;
        baseDashReady = false;
        Array.Clear(infernalDashStarts);
        Array.Clear(infernalDashEnds);
        Array.Clear(infernalDashReady);
        playedVisualEvents = 0UL;
        previousVisualTimer = -1;
        previousVisualReleaseTimer = -1;
        visualClockInitialized = true;
        previousVisualWeaponAngle = 0f;
        visualSwingDirection = 1;
        visualDirectionInitialized = false;
        serverAuthorized = Main.netMode != NetmodeID.MultiplayerClient;
        Projectile.GetGlobalProjectile<MyGlobalProjectile>().AuthorizeServerReaperProjectile(Projectile);
        Projectile.timeLeft = isUltimate ? 300
            : value.Form == ReaperFormId.Blood ? 760 : 160;
    }

    internal void RecordBloodSiphonDamage(int damageDone)
    {
        if (!configured || ultimate || snapshot.Form != ReaperFormId.Blood
            || specialReleased || damageDone <= 0)
        {
            return;
        }
        bloodSiphonDamage = Math.Min(int.MaxValue / 2L, bloodSiphonDamage + damageDone);
    }

    internal bool UpdateSpecialTarget(Vector2 cursorOffset)
    {
        if (!configured || ultimate || snapshot.Form != ReaperFormId.Death
            || specialReleased || !float.IsFinite(cursorOffset.X)
            || !float.IsFinite(cursorOffset.Y))
        {
            return false;
        }
        float distance = cursorOffset.Length();
        if (distance > 2000f)
            cursorOffset *= 2000f / distance;
        else if (distance < 2f)
            cursorOffset = aim * 240f;
        specialTargetOffset = cursorOffset;
        aim = cursorOffset.SafeNormalize(aim);
        Projectile.velocity = aim;
        Projectile.netUpdate = true;
        return true;
    }

    internal bool RequestRelease()
    {
        if (!configured || !IsChargedSpecial || specialReleased || releaseRequested)
            return false;
        releaseRequested = true;
        Projectile.netUpdate = true;
        if (Main.netMode == NetmodeID.Server)
        {
            NetMessage.SendData(MessageID.SyncProjectile, -1, -1, null,
                Projectile.whoAmI);
        }
        return true;
    }

    internal static bool RequestLocalOwnerRelease(Player player)
    {
        if (Main.netMode != NetmodeID.MultiplayerClient
            || player.whoAmI != Main.myPlayer)
        {
            return false;
        }

        int controllerType = ModContent.ProjectileType<
            ReaperActionControllerProjectile>();
        bool releasedAny = false;
        foreach (Projectile projectile in Main.ActiveProjectiles)
        {
            if (projectile.owner != player.whoAmI
                || projectile.type != controllerType
                || projectile.ModProjectile
                    is not ReaperActionControllerProjectile controller)
            {
                continue;
            }
            releasedAny |= controller.RequestRelease();
        }
        return releasedAny;
    }

    public override void OnKill(int timeLeft)
    {
        if (Projectile.owner < 0 || Projectile.owner >= Main.maxPlayers)
            return;
        Player player = Main.player[Projectile.owner];
        if (player.heldProj == Projectile.whoAmI)
            player.heldProj = -1;
    }

    private void UpdateChargedSpecial(Player player)
    {
        if (snapshot.Form == ReaperFormId.Death)
        {
            UpdateDeathInvocation(player);
            return;
        }

        if (!specialReleased)
        {
            if (snapshot.Form == ReaperFormId.Blood && timer == 1
                && Main.netMode != NetmodeID.MultiplayerClient)
            {
                float siphonRadius = snapshot.Stage switch
                {
                    ReaperStage.StageI => 270f,
                    ReaperStage.StageII => 360f,
                    _ => 460f
                };
                ReaperBloodDrainFieldProjectile.Spawn(Projectile.GetSource_FromThis(),
                    player.whoAmI, snapshot, siphonRadius, Projectile.identity);
            }
            int minimumCharge = snapshot.Form == ReaperFormId.Blood ? 8 : 0;
            int maximumCharge = snapshot.Form == ReaperFormId.Blood
                ? GetBloodMaximumChargeFrames() : 45;
            if (releaseRequested && chargeFrames >= minimumCharge)
            {
                BeginChargedRelease(player);
                return;
            }

            chargeFrames = Math.Min(maximumCharge, chargeFrames + 1);
            if (chargeFrames >= maximumCharge || releaseRequested && chargeFrames >= minimumCharge)
                BeginChargedRelease(player);
            return;
        }

        releaseTimer++;
        Vector2 origin = player.MountedCenter;
        switch (snapshot.Form)
        {
            case ReaperFormId.Blood:
            {
                if (releaseTimer == 6 && Main.netMode != NetmodeID.MultiplayerClient)
                    ExecuteBloodRangeExecution(player);
                FinishChargedAt(34);
                break;
            }
            case ReaperFormId.Frost:
            {
                float total = MathHelper.Lerp(1.7f, 2.4f, MathHelper.Clamp(chargeFrames / 45f, 0f, 1f));
                bool fullCharge = chargeFrames >= 45;
                float length = snapshot.Stage switch
                {
                    ReaperStage.StageI => 220f,
                    ReaperStage.StageII => 285f,
                    _ => 370f
                };
                float width = snapshot.Stage switch
                {
                    ReaperStage.StageI => 31f,
                    ReaperStage.StageII => 38f,
                    _ => 48f
                };
                Vector2 focus = origin + aim * (snapshot.Stage >= ReaperStage.StageIII ? 70f : 35f);
                ChargedStrikeAt(7, player, focus - aim.RotatedBy(-0.55f) * length * 0.5f,
                    aim.RotatedBy(-0.55f), ReaperStrikeShape.Line, length, width, total * 0.5f, 0);
                ChargedStrikeAt(14, player, focus - aim.RotatedBy(0.55f) * length * 0.5f,
                    aim.RotatedBy(0.55f), ReaperStrikeShape.Line, length, width,
                    total * 0.5f, fullCharge ? 2 : 1);
                FinishChargedAt(snapshot.Stage >= ReaperStage.StageIII ? 30 : 26);
                break;
            }
            case ReaperFormId.Soul:
            {
                int circles = snapshot.Stage switch { ReaperStage.StageI => 2, ReaperStage.StageII => 3, _ => 4 };
                float minimum = snapshot.Stage switch { ReaperStage.StageI => 1.5f, ReaperStage.StageII => 2.25f, _ => 3f };
                float maximum = snapshot.Stage switch { ReaperStage.StageI => 2.3f, ReaperStage.StageII => 3.15f, _ => 4.2f };
                float total = MathHelper.Lerp(minimum, maximum, MathHelper.Clamp(chargeFrames / 45f, 0f, 1f));
                int fullChargePhaseOffset = chargeFrames >= 45 ? 8 : 0;
                int bladesPerBeat = chargeFrames >= 45 ? 2 : 1;
                for (int index = 0; index < circles; index++)
                {
                    Vector2 launchDirection = aim.RotatedBy(index
                        * MathHelper.TwoPi / circles);
                    SoulVolleyAt(6 + index * 8, player, origin,
                        launchDirection, total / circles, fullChargePhaseOffset + index,
                        bladesPerBeat);
                }
                FinishChargedAt(14 + circles * 8);
                break;
            }
        }
    }

    private void BeginChargedRelease(Player player)
    {
        if (specialReleased)
            return;
        specialReleased = true;
        releaseTimer = 0;
        bloodOvercharged = snapshot.Form == ReaperFormId.Blood
            && snapshot.Stage >= ReaperStage.StageII
            && chargeFrames >= GetBloodMaximumChargeFrames();
        Projectile.netUpdate = true;
    }

    private void ExecuteBloodRangeExecution(Player player)
    {
        float radius = snapshot.Stage switch
        {
            ReaperStage.StageI => 270f,
            ReaperStage.StageII => 360f,
            _ => 460f
        };
        int maximumCharge = GetBloodMaximumChargeFrames();
        float charge = MathHelper.Clamp((chargeFrames - 8f)
            / Math.Max(1f, maximumCharge - 8f), 0f, 1f);
        float length = MathHelper.Lerp(118f, 182f, snapshot.StageNumber / 3f)
            * MathHelper.Lerp(0.88f, 1.14f, charge);
        float width = 15f + snapshot.StageNumber * 4f + (bloodOvercharged ? 6f : 0f);
        int scarLifetime = ReaperBloodBladeProjectile.GetScarLifetime(
            Math.Max(1, (int)snapshot.Skill1));
        int executionDamage = (int)Math.Clamp(bloodSiphonDamage, 0L, int.MaxValue / 2L);
        if (executionDamage <= 0)
            return;
        HashSet<int> roots = [];
        int phase = 0;
        foreach (NPC npc in Main.ActiveNPCs)
        {
            if (!npc.CanBeChasedBy(Projectile)
                || Vector2.DistanceSquared(npc.Center, player.Center) > radius * radius)
            {
                continue;
            }
            int root = npc.realLife >= 0 ? npc.realLife : npc.whoAmI;
            if (!roots.Add(root))
                continue;
            NPC target = Main.npc[root];
            uint hash = unchecked((uint)(Projectile.identity * 16777619 + root * 486187739));
            hash ^= hash >> 15;
            float offset = ((hash & 0xFFFFu) / 65535f - 0.5f) * 1.15f;
            Vector2 direction = aim.RotatedBy(offset).SafeNormalize(aim);
            Vector2 center = target.Center;
            ReaperBloodExecutionProjectile.Spawn(Projectile.GetSource_FromThis(),
                player.whoAmI, snapshot, target, direction, length, width,
                executionDamage, phase, Projectile.identity);
            ReaperBloodScarProjectile.Spawn(Projectile.GetSource_FromThis(), player.whoAmI,
                snapshot, center, direction, length * 1.12f, width * 0.78f,
                scarLifetime, 0f, phase, Projectile.identity,
                exactDamage: Math.Max(1, executionDamage / 2));
            phase++;
        }
    }

    private int GetBloodMaximumChargeFrames()
    {
        int level = Math.Clamp(Math.Max(1, (int)snapshot.Skill1), 1, 3);
        return level switch
        {
            1 => 240,
            2 => 420,
            _ => 600
        };
    }

    private void UpdateDeathInvocation(Player player)
    {
        Projectile.timeLeft = 2;
        if (specialReleased)
        {
            releaseTimer++;
            if (releaseTimer >= 8)
                Projectile.Kill();
            return;
        }
        if (releaseRequested && deathInvocationCount > 0)
        {
            specialReleased = true;
            releaseTimer = 0;
            Projectile.netUpdate = true;
            return;
        }

        chargeFrames = Math.Min(int.MaxValue / 4, chargeFrames + 1);
        if (Main.netMode == NetmodeID.MultiplayerClient)
            return;

        deathInvocationCooldown--;
        if (deathInvocationCooldown > 0)
            return;

        // Reach the configured held-invocation cap in roughly 2.5 seconds. The
        // primary chain owns a separate, higher cap from the same upgrade node.
        float acceleration = MathHelper.Clamp(chargeFrames / 150f, 0f, 1f);
        acceleration = acceleration * acceleration * (3f - 2f * acceleration);
        float invocationTempo = MathHelper.Lerp(1f,
            snapshot.DeathMaximumTempo, acceleration);
        deathInvocationCooldown = Math.Clamp((int)Math.Round(
            12f / Math.Max(1f, invocationTempo)), 4, 12);
        int invocation = deathInvocationCount;
        float orbitAngle = invocation * 2.3999632f + Projectile.identity * 0.071f;
        Vector2 summonPosition = player.Center + orbitAngle.ToRotationVector2()
            * ((120f + invocation % 3 * 34f) * snapshot.DeathRangeMultiplier);
        Vector2 cursorFocus = player.Center + specialTargetOffset;
        NPC? target = FindDeathInvocationTarget(cursorFocus,
            snapshot.DeathRangeMultiplier);
        Vector2 strikeFocus = target?.Center ?? cursorFocus;
        int echoIndex = ReaperDeathEchoScytheProjectile.Spawn(
            Projectile.GetSource_FromThis(),
            player.whoAmI, snapshot, invocation % 6, summonPosition, strikeFocus,
            Projectile.identity * 4096 + invocation);
        if (echoIndex < 0 || echoIndex >= Main.maxProjectiles)
        {
            // A released click still owes one actual attack. If the projectile
            // pool is momentarily full, keep the invocation uncommitted and
            // retry next tick instead of treating a missing echo as success.
            deathInvocationCooldown = 1;
            return;
        }

        deathInvocationCount++;
        Projectile.netUpdate = true;
    }

    private bool UpdateDeathAssemblyPrelude(Player player)
    {
        if (!HasDeathAssemblyPrelude)
            return false;

        if (deathAssemblyCancelling)
        {
            UpdateDeathAssemblyCancellation(player);
            return true;
        }
        if (releaseRequested
            && deathAssemblyTimer < snapshot.AssemblyFrames)
        {
            deathAssemblyCancelling = true;
            deathAssemblyCancelStartTimer = deathAssemblyTimer;
            deathAssemblyCancelTimer = 0;
            Projectile.netUpdate = true;
            UpdateDeathAssemblyCancellation(player);
            return true;
        }
        if (deathAssemblyTimer >= snapshot.AssemblyFrames)
            return false;

        deathAssemblyTimer++;
        Projectile.timeLeft = 2;
        float aimAngle = aim.ToRotation();
        int facing = Math.Abs(aim.X) > 0.05f ? Math.Sign(aim.X)
            : player.direction;
        if (facing == 0)
            facing = 1;
        player.ChangeDir(facing);
        visualWeaponAngle = aimAngle - 1.45f * facing;
        visualSwingDirection = facing;
        visualDirectionInitialized = true;
        previousVisualWeaponAngle = visualWeaponAngle;
        visualCharge = 0f;
        Projectile.Center = player.MountedCenter;
        player.heldProj = Projectile.whoAmI;
        player.itemTime = Math.Max(player.itemTime, 2);
        player.itemAnimation = Math.Max(player.itemAnimation, 2);
        player.SetCompositeArmFront(true, Player.CompositeArmStretchAmount.Full,
            visualWeaponAngle - MathHelper.PiOver2);
        player.itemRotation = MathHelper.WrapAngle(visualWeaponAngle);
        if (deathAssemblyTimer == snapshot.AssemblyFrames)
        {
            Projectile.netUpdate = true;
        }
        return true;
    }

    private void UpdateDeathAssemblyCancellation(Player player)
    {
        deathAssemblyCancelTimer++;
        Projectile.timeLeft = 2;
        float aimAngle = aim.ToRotation();
        int facing = Math.Abs(aim.X) > 0.05f ? Math.Sign(aim.X)
            : player.direction;
        if (facing == 0)
            facing = 1;
        player.ChangeDir(facing);
        visualWeaponAngle = aimAngle - 1.45f * facing;
        visualSwingDirection = facing;
        visualDirectionInitialized = true;
        previousVisualWeaponAngle = visualWeaponAngle;
        visualCharge = 0f;
        Projectile.Center = player.MountedCenter;
        player.heldProj = Projectile.whoAmI;
        player.itemTime = Math.Max(player.itemTime, 2);
        player.itemAnimation = Math.Max(player.itemAnimation, 2);
        player.SetCompositeArmFront(true,
            Player.CompositeArmStretchAmount.Full,
            visualWeaponAngle - MathHelper.PiOver2);
        player.itemRotation = MathHelper.WrapAngle(visualWeaponAngle);
        if (deathAssemblyCancelTimer >= ReaperDefinitions.AssemblyCancelFrames)
            Projectile.Kill();
    }

    private static NPC? FindDeathInvocationTarget(Vector2 cursorFocus,
        float rangeMultiplier)
    {
        NPC? nearest = null;
        // Only snap to an enemy genuinely close to the cursor. The right-click
        // destination remains the cursor itself rather than silently selecting a
        // distant target and making the summoned scythes appear stationary.
        float targetingRadius = 320f * MathHelper.Clamp(rangeMultiplier,
            0.5f, 1f);
        float best = targetingRadius * targetingRadius;
        foreach (NPC npc in Main.ActiveNPCs)
        {
            if (!npc.CanBeChasedBy())
                continue;
            float distance = Vector2.DistanceSquared(npc.Center, cursorFocus);
            if (distance >= best)
                continue;
            best = distance;
            nearest = npc;
        }
        return nearest;
    }

    private static bool TryPayBloodOvercharge(Player player)
    {
        if (Main.netMode == NetmodeID.MultiplayerClient || player.dead)
            return false;
        int sacrifice = Math.Max(1, (int)Math.Ceiling(player.statLifeMax2 * 0.08f));
        if (player.statLife <= sacrifice)
            return false;
        player.statLife -= sacrifice;
        CombatText.NewText(player.Hitbox, new Color(220, 25, 65), sacrifice, dramatic: true);
        if (Main.netMode == NetmodeID.Server)
            NetMessage.SendData(MessageID.PlayerLifeMana, -1, -1, null, player.whoAmI);
        return true;
    }

    private void ChargedStrikeAt(int eventTick, Player player, Vector2 origin, Vector2 direction,
        ReaperStrikeShape shape, float length, float width, float multiplier, int phase)
    {
        if (releaseTimer != eventTick || Main.netMode == NetmodeID.MultiplayerClient)
            return;
        ReaperStrikeProjectile.Spawn(Projectile.GetSource_FromThis(), player.whoAmI, snapshot,
            ReaperHitKind.Special, phase, origin, direction, shape, length, width, multiplier,
            actionId: Projectile.identity);
    }

    private void FinishChargedAt(int finishTick)
    {
        if (releaseTimer >= finishTick)
            Projectile.Kill();
    }

    private void UpdateSpecial(Player player)
    {
        Vector2 origin = player.MountedCenter;
        switch (snapshot.Form)
        {
            case ReaperFormId.Base:
                if (timer == BaseDashStartTick)
                    StartBaseDash(player);
                Vector2 baseOrigin = baseDashReady ? baseDashEnd : origin;
                StrikeAt(11, player, baseOrigin - aim * 18f, aim.RotatedBy(-0.48f),
                    ReaperStrikeShape.Line, 175f, 29f, 1.65f, 0);
                FinishAt(28);
                break;
            case ReaperFormId.Bone:
            {
                Vector2 focus = origin + aim * (snapshot.Stage >= ReaperStage.StageIII ? 155f : 125f);
                Vector2 guillotineDirection = Vector2.UnitY;
                StrikeAt(12, player, focus - guillotineDirection * 120f, guillotineDirection,
                    ReaperStrikeShape.Line, 240f, 31f, 1.8f, 0);
                if (snapshot.Stage >= ReaperStage.StageII)
                    StrikeAt(22, player, focus, aim, ReaperStrikeShape.Circle, 105f, 25f, 0.9f, 1);
                if (snapshot.Stage >= ReaperStage.StageIII)
                    StrikeAt(32, player, focus, aim, ReaperStrikeShape.Cross, 150f, 33f, 0.9f, 2);
                FinishAt(snapshot.Stage >= ReaperStage.StageIII ? 44 : snapshot.Stage >= ReaperStage.StageII ? 34 : 25);
                break;
            }
            case ReaperFormId.Infernal:
                if (snapshot.Stage >= ReaperStage.StageIII)
                {
                    if (timer == 2)
                        StartInfernalDash(player, 0, aim, 280f);
                    if (timer == 15)
                    {
                        float side = player.direction == 0 ? 1f : player.direction;
                        StartInfernalDash(player, 1, aim.RotatedBy(2.25f * side), 250f);
                    }
                    if (timer == 29)
                    {
                        float side = player.direction == 0 ? 1f : player.direction;
                        StartInfernalDash(player, 2, aim.RotatedBy(-2.25f * side), 290f);
                    }
                    StrikeInfernalPathAt(3, player, 0, 31f, 2f);
                    StrikeInfernalPathAt(16, player, 1, 28f, 1.3f);
                    StrikeInfernalPathAt(30, player, 2, 34f, 1.8f);
                    FinishAt(42);
                }
                else
                {
                    if (timer == 2)
                        StartInfernalDash(player, 0, aim, 300f);
                    StrikeInfernalPathAt(3, player, 0, 30f, 2.2f);
                    if (snapshot.Stage >= ReaperStage.StageII && timer == 19)
                    {
                        float side = player.direction == 0 ? 1f : player.direction;
                        StartInfernalDash(player, 1, (-aim).RotatedBy(0.52f * side), 245f);
                    }
                    if (snapshot.Stage >= ReaperStage.StageII)
                        StrikeInfernalPathAt(20, player, 1, 28f, 1.4f);
                    int finish = snapshot.Stage >= ReaperStage.StageII ? 32 : 18;
                    FinishAt(finish);
                }
                break;
            case ReaperFormId.Void:
                if (timer == VoidSpecialTeleportTick)
                    ExecuteVoidTeleportSpecial(player);
                // Keep the replicated controller alive long enough for clients to
                // receive and render the cast. Killing it on the same tick as the
                // server-side teleport made Void the only special with no held
                // weapon, sound or opening presentation in multiplayer.
                FinishAt(VoidSpecialVisualEndTick);
                break;
        }
    }

    private void UpdateUltimate(Player player)
    {
        int visualDuration = snapshot.Form switch
        {
            ReaperFormId.Bone => 216,
            ReaperFormId.Blood => 210,
            ReaperFormId.Infernal => 204,
            ReaperFormId.Frost => 120,
            ReaperFormId.Soul => 116,
            ReaperFormId.Void => VoidUltimateBlackHoleTick,
            ReaperFormId.Death => ReaperDeathUltimateGeometry.Duration,
            _ => 1
        };
        Vector2 origin = player.MountedCenter;
        Vector2 focus = snapshot.Form switch
        {
            // Death locks the selected point in world space at cast start. Player
            // movement and camera movement must not drag the cut intersection.
            ReaperFormId.Death => ultimateFocusWorld,
            ReaperFormId.Void => player.Center + specialTargetOffset,
            _ => origin + aim * 180f
        };
        ReaperUltimateVisualSystem.ReportUltimate(player, snapshot.Form, aim,
            focus, Projectile.identity, timer, visualDuration);
        switch (snapshot.Form)
        {
            case ReaperFormId.Bone:
                foreach (int tick in new[] { 30, 66, 102, 138 })
                    StrikeAt(tick, player, focus, aim, ReaperStrikeShape.Circle, 82f, 24f, 1.25f, tick);
                StrikeAt(168, player, focus, aim, ReaperStrikeShape.Circle, 150f, 34f, 2.5f, 4);
                StrikeAt(207, player, origin, aim, ReaperStrikeShape.Line, 520f, 52f, 4f, 5);
                FinishAt(216);
                break;
            case ReaperFormId.Blood:
                if (timer == 18 && Main.netMode != NetmodeID.MultiplayerClient)
                {
                    for (int index = 0; index < 6; index++)
                    {
                        Vector2 launch = aim.RotatedBy(index * MathHelper.TwoPi / 6f);
                        ReaperBloodBladeProjectile.Spawn(Projectile.GetSource_FromThis(),
                            player.whoAmI, snapshot, origin + launch * 54f,
                            launch * 14f, index, Projectile.identity,
                            ReaperHitKind.Ultimate, damageMultiplier: 0.75f,
                            maximumPasses: 4,
                            scarLifetime: ReaperBloodBladeProjectile.GetScarLifetime(
                                Math.Max(1, (int)snapshot.Skill1)));
                    }
                }
                if (timer == 166 && Main.netMode != NetmodeID.MultiplayerClient)
                    ExecuteBloodUltimateFinal(player);
                FinishAt(210);
                break;
            case ReaperFormId.Infernal:
                for (int index = 0; index < 5; index++)
                {
                    int eventTick = 20 + index * 20;
                    if (timer == eventTick)
                    {
                        float side = aim.X < -0.01f ? -1f : 1f;
                        Vector2 route = aim.RotatedBy(index * MathHelper.Pi * 0.8f * side);
                        StartInfernalDash(player, index, route, 250f);
                    }
                    StrikeInfernalPathAt(eventTick, player, index, 29f, 1.25f);
                }
                StrikeAt(130, player, focus, aim, ReaperStrikeShape.Cross, 230f, 40f, 2.25f, 5);
                StrikeAt(190, player, focus, aim, ReaperStrikeShape.Circle, 225f, 54f, 3.5f, 6);
                FinishAt(204);
                break;
            case ReaperFormId.Frost:
                StrikeAt(1, player, focus, aim, ReaperStrikeShape.Circle, 230f, 25f, 1.2f, 0);
                for (int index = 0; index < 4; index++)
                    StrikeAt(30 + index * 18, player, focus, aim.RotatedBy(index * MathHelper.PiOver2), ReaperStrikeShape.Line, 230f, 24f, 0.8f, index + 1);
                StrikeAt(100, player, focus, aim.RotatedBy(-0.5f), ReaperStrikeShape.Line, 330f, 45f, 3f, 5);
                StrikeAt(110, player, focus, aim.RotatedBy(0.5f), ReaperStrikeShape.Line, 330f, 45f, 3f, 6);
                FinishAt(120);
                break;
            case ReaperFormId.Soul:
                StrikeAt(20, player, focus, aim, ReaperStrikeShape.Circle, 180f, 28f, 1.2f, 0);
                for (int index = 0; index < 8; index++)
                    StrikeAt(32 + index * 8, player, focus + aim.RotatedBy(index * MathHelper.PiOver4) * 45f,
                        aim.RotatedBy(index * MathHelper.PiOver4), ReaperStrikeShape.Line, 190f, 22f, 0.8f, index + 1);
                StrikeAt(104, player, focus, aim, ReaperStrikeShape.Circle, 245f, 48f, 4f, 9);
                FinishAt(116);
                break;
            case ReaperFormId.Void:
                if (timer == VoidUltimateBlackHoleTick
                    && Main.netMode != NetmodeID.MultiplayerClient)
                {
                    int level = Math.Clamp(Math.Max(1, (int)snapshot.Skill3), 1, 3);
                    Vector2 target = player.Center + specialTargetOffset;
                    ReaperVoidBlackHoleProjectile.Spawn(Projectile.GetSource_FromThis(),
                        player.whoAmI, snapshot, target,
                        160f + level * 24f,
                        ReaperVoidArcRiftProjectile.GetLifetime(level),
                        520f + level * 45f, 0.34f,
                        Projectile.identity);
                }
                // Creating the black-hole tear is the end of the cast. The
                // persistent projectile owns everything that follows.
                FinishAt(VoidUltimateBlackHoleTick);
                break;
            case ReaperFormId.Death:
                for (int index = 0; index < ReaperDeathUltimateGeometry.CutCount; index++)
                    StrikeDeathWorldCutAt(ReaperDeathUltimateGeometry.GetCutTick(index),
                        player, focus, index);
                if (timer == ReaperDeathUltimateGeometry.ShatterTick
                    && Main.netMode != NetmodeID.MultiplayerClient)
                    ExecuteDeathUltimateShatter(player);
                FinishAt(ReaperDeathUltimateGeometry.Duration);
                break;
            default:
                Projectile.Kill();
                break;
        }
    }

    private void StrikeAt(int eventTick, Player player, Vector2 origin, Vector2 direction,
        ReaperStrikeShape shape, float length, float width, float multiplier, int phase)
    {
        if (timer != eventTick || Main.netMode == NetmodeID.MultiplayerClient)
            return;
        ReaperStrikeProjectile.Spawn(Projectile.GetSource_FromThis(), player.whoAmI, snapshot,
            ultimate ? ReaperHitKind.Ultimate : ReaperHitKind.Special, phase,
            origin, direction, shape, length, width, multiplier, actionId: Projectile.identity);
    }

    private void StrikeDeathWorldCutAt(int eventTick, Player player, Vector2 focus,
        int cutIndex)
    {
        if (timer != eventTick || Main.netMode == NetmodeID.MultiplayerClient)
            return;
        Vector2 axis = ReaperDeathUltimateGeometry.GetCutAxis(
            Projectile.identity, cutIndex);
        Vector2 start = focus - axis * (ReaperDeathUltimateGeometry.CutLength * 0.5f);
        ReaperStrikeProjectile.Spawn(Projectile.GetSource_FromThis(), player.whoAmI,
            snapshot, ReaperHitKind.Ultimate, cutIndex, start, axis,
            ReaperStrikeShape.Line, ReaperDeathUltimateGeometry.CutLength,
            ReaperDeathUltimateGeometry.CutCollisionWidth, 0.28f,
            actionId: Projectile.identity);
    }

    private void SoulVolleyAt(int eventTick, Player player, Vector2 origin,
        Vector2 direction, float multiplier, int phase, int bladeCount)
    {
        if (releaseTimer != eventTick || Main.netMode == NetmodeID.MultiplayerClient)
            return;

        bladeCount = Math.Clamp(bladeCount, 1, 2);
        float perBladeMultiplier = multiplier / bladeCount;
        Vector2 normal = direction.RotatedBy(MathHelper.PiOver2);
        for (int blade = 0; blade < bladeCount; blade++)
        {
            float centered = blade - (bladeCount - 1) * 0.5f;
            Vector2 launch = direction.RotatedBy(centered * 0.16f);
            SoulSickleProjectile.Spawn(Projectile.GetSource_FromThis(),
                player.whoAmI, snapshot,
                origin + normal * centered * 24f + launch * 32f,
                launch * (15f + snapshot.StageNumber), phase,
                Projectile.identity, ReaperHitKind.Special, perBladeMultiplier,
                variant: phase * 2 + blade);
        }
    }

    private void ExecuteDeathUltimateShatter(Player player)
    {
        HashSet<int> processedRoots = [];
        bool centerVisualSpawned = false;
        foreach (NPC npc in Main.ActiveNPCs)
        {
            int root = npc.realLife >= 0 ? npc.realLife : npc.whoAmI;
            if (!processedRoots.Add(root) || root < 0 || root >= Main.maxNPCs)
                continue;
            NPC target = Main.npc[root];
            if (!target.active)
                continue;
            ReaperCombatGlobalNPC status = target.GetGlobalNPC<ReaperCombatGlobalNPC>();
            int storedDamage = status.ConsumeDeathUltimateDamage(target,
                player.whoAmI, Projectile.identity);
            if (storedDamage <= 0)
                continue;
            int shatterIndex = ReaperDeathShatterProjectile.Spawn(
                Projectile.GetSource_FromThis(), player.whoAmI, snapshot,
                target, storedDamage, Projectile.identity, ultimateFocusWorld,
                drawCenterVisual: !centerVisualSpawned);
            centerVisualSpawned |= shatterIndex >= 0;
        }
    }

    private void ExecuteBloodUltimateFinal(Player player)
    {
        NPC? target = ReaperProjectileHelper.FindTarget(Projectile, 820f);
        Vector2 focus = target?.Center ?? player.Center + aim * 260f;
        Vector2 approach = (focus - player.Center).SafeNormalize(aim);
        if (target is not null)
        {
            float distance = Vector2.Distance(player.Center, target.Center) + 92f;
            ReaperMovementHelper.MoveAlong(player, approach, distance,
                out _, out _);
        }

        Vector2 scarDirection = approach.RotatedBy(-0.18f *
            (player.direction == 0 ? 1 : player.direction));
        ReaperBloodScarProjectile.Spawn(Projectile.GetSource_FromThis(), player.whoAmI,
            snapshot, focus, scarDirection, 1180f, 74f,
            ReaperBloodBladeProjectile.GetScarLifetime(Math.Max(1, (int)snapshot.Skill1)),
            0.26f, 63, Projectile.identity);
    }

    private void StartInfernalDash(Player player, int phase, Vector2 direction, float distance)
    {
        if (phase < 0 || phase >= infernalDashReady.Length)
            return;
        if (Main.netMode == NetmodeID.MultiplayerClient)
            return;

        float acceptedDistance = ReaperMovementHelper.TraceAlong(
            player, direction, distance, out Vector2 start, out Vector2 end);
        infernalDashStarts[phase] = start;
        infernalDashEnds[phase] = end;
        infernalDashReady[phase] = acceptedDistance > 0f;
        Projectile.netUpdate = true;
    }

    private void ApplyInfernalDisplacement(Player player)
    {
        if (snapshot.Form != ReaperFormId.Infernal
            || Main.netMode == NetmodeID.MultiplayerClient && Projectile.owner != Main.myPlayer)
        {
            return;
        }

        int phase = -1;
        int travelFrames = ultimate ? 10 : snapshot.Stage >= ReaperStage.StageIII ? 9 : 10;
        for (int index = infernalDashReady.Length - 1; index >= 0; index--)
        {
            int candidateStart = ultimate
                ? 20 + index * 20
                : snapshot.Stage >= ReaperStage.StageIII
                    ? index switch { 0 => 2, 1 => 15, 2 => 29, _ => int.MaxValue }
                    : index switch { 0 => 2, 1 => 19, _ => int.MaxValue };
            if (candidateStart == int.MaxValue || timer < candidateStart
                || timer >= candidateStart + travelFrames || !infernalDashReady[index])
            {
                continue;
            }
            phase = index;
            break;
        }

        if (phase < 0)
            return;

        Vector2 path = infernalDashEnds[phase] - infernalDashStarts[phase];
        Vector2 velocity = path / travelFrames;
        if (!float.IsFinite(velocity.X) || !float.IsFinite(velocity.Y))
            return;
        Vector2 candidate = player.position + velocity;
        if (!Collision.SolidCollision(candidate, player.width, player.height))
            player.velocity = velocity;
    }

    private void ExecuteVoidTeleportSpecial(Player player)
    {
        if (Main.netMode == NetmodeID.MultiplayerClient)
            return;

        Vector2 start = player.Center;
        Vector2 requestedTarget = start + specialTargetOffset;
        bool teleported = ReaperMovementHelper.TeleportToTarget(player,
            requestedTarget, 2000f, out start, out Vector2 end);

        // A blocked or very close cursor destination must not turn the whole
        // special into a silent no-op. Keep the server-authoritative movement
        // safety rule, but still tear the selected world-space path when there is
        // nowhere safe to place the player.
        if (!teleported)
        {
            Vector2 fallback = requestedTarget - start;
            float fallbackLength = Math.Min(2000f, fallback.Length());
            Vector2 fallbackAxis = fallback.SafeNormalize(aim);
            if (fallbackLength <= 4f)
                fallbackLength = 160f;
            end = start + fallbackAxis * fallbackLength;
        }

        Vector2 path = end - start;
        float length = path.Length();
        if (length <= 4f)
            return;

        Vector2 axis = path / length;
        float strikeWidth = 24f + snapshot.StageNumber * 6f;
        float strikeMultiplier = 1.45f + snapshot.StageNumber * 0.20f;
        ReaperStrikeProjectile.Spawn(Projectile.GetSource_FromThis(), player.whoAmI,
            snapshot, ReaperHitKind.Special, 0, start, axis,
            ReaperStrikeShape.Line, length, strikeWidth, strikeMultiplier,
            actionId: Projectile.identity);

        int level = Math.Clamp(Math.Max(1, (int)snapshot.Skill2), 1, 3);
        float riftWidth = 20f + snapshot.StageNumber * 5f + level * 4f;
        ReaperVoidRiftProjectile.Spawn(Projectile.GetSource_FromThis(), player.whoAmI,
            snapshot, (start + end) * 0.5f, axis, length, riftWidth,
            ReaperVoidArcRiftProjectile.GetLifetime(level), 0f,
            0.20f, 0, Projectile.identity);
    }

    private void StartBaseDash(Player player)
    {
        if (Main.netMode == NetmodeID.MultiplayerClient)
            return;
        Vector2 direction = new Vector2(aim.X, Math.Min(-0.28f, aim.Y)).SafeNormalize(aim);
        float acceptedDistance = ReaperMovementHelper.TraceAlong(
            player, direction, 92f, out baseDashStart, out baseDashEnd);
        baseDashReady = acceptedDistance > 0f;
        Projectile.netUpdate = true;
    }

    private void ApplyBaseDisplacement(Player player)
    {
        if (ultimate || snapshot.Form != ReaperFormId.Base || !baseDashReady
            || Main.netMode == NetmodeID.MultiplayerClient
                && Projectile.owner != Main.myPlayer)
        {
            return;
        }

        int elapsed = timer - BaseDashStartTick;
        if (elapsed < 0 || elapsed > BaseDashTravelFrames)
            return;
        if (elapsed == BaseDashTravelFrames)
        {
            player.velocity = Vector2.Zero;
            return;
        }

        Vector2 velocity = (baseDashEnd - baseDashStart) / BaseDashTravelFrames;
        if (!float.IsFinite(velocity.X) || !float.IsFinite(velocity.Y))
            return;
        Vector2 candidate = player.position + velocity;
        if (!Collision.SolidCollision(candidate, player.width, player.height))
            player.velocity = velocity;
        else
            player.velocity = Vector2.Zero;
    }

    private void StrikeInfernalPathAt(int eventTick, Player player, int phase, float width, float multiplier)
    {
        if (timer != eventTick || Main.netMode == NetmodeID.MultiplayerClient
            || phase < 0 || phase >= infernalDashReady.Length)
            return;
        Vector2 start = infernalDashReady[phase] ? infernalDashStarts[phase] : player.MountedCenter;
        Vector2 path = infernalDashReady[phase] ? infernalDashEnds[phase] - start : aim;
        float length = Math.Max(8f, path.Length());
        ReaperStrikeProjectile.Spawn(Projectile.GetSource_FromThis(), player.whoAmI, snapshot,
            ultimate ? ReaperHitKind.Ultimate : ReaperHitKind.Special, phase, start, path, ReaperStrikeShape.Line,
            length, width, multiplier, actionId: Projectile.identity);
        ReaperInfernalTrailProjectile.Spawn(Projectile.GetSource_FromThis(), player.whoAmI,
            snapshot, start, start + path.SafeNormalize(aim) * length,
            Math.Max(9f, width * 0.72f), phase, Projectile.identity);
    }

    private void FinishAt(int finishTick)
    {
        if (timer >= finishTick)
            Projectile.Kill();
    }

    private void UpdatePlayerPresentation(Player player)
    {
        float aimAngle = aim.ToRotation();
        int facing = Math.Abs(aim.X) > 0.05f ? Math.Sign(aim.X) : player.direction;
        if (facing == 0)
            facing = 1;
        player.ChangeDir(facing);

        visualCharge = 0f;
        if (ultimate)
        {
            if (snapshot.Form == ReaperFormId.Death)
            {
                if (timer < 20)
                {
                    visualWeaponAngle = aimAngle - 2.25f * facing;
                }
                else if (timer < 128)
                {
                    int cut = Math.Clamp((timer - 20) / 6, 0, 17);
                    float local = MathHelper.Clamp(((timer - 20) % 6) / 5f,
                        0f, 1f);
                    int direction = (cut & 1) == 0 ? facing : -facing;
                    visualWeaponAngle = MathHelper.Lerp(
                        aimAngle - 2.35f * direction,
                        aimAngle + 2.15f * direction,
                        SmoothStep(local));
                }
                else
                {
                    visualWeaponAngle = aimAngle - 1.35f * facing;
                }
            }
            else if (snapshot.Form == ReaperFormId.Void)
            {
                // The black hole supplies the motion; the held scythe remains
                // poised instead of using the shared ultimate spin animation.
                visualWeaponAngle = aimAngle - 1.65f * facing;
            }
            else
            {
                float sweep = timer * 0.085f * facing;
                visualWeaponAngle = aimAngle - 1.65f * facing + sweep;
            }
        }
        else if (IsChargedSpecial)
        {
            int maximumCharge = snapshot.Form == ReaperFormId.Blood
                ? GetBloodMaximumChargeFrames()
                : snapshot.Form == ReaperFormId.Death ? 600 : 45;
            visualCharge = MathHelper.Clamp(chargeFrames / (float)maximumCharge, 0f, 1f);
            if (!specialReleased)
            {
                float drawBack = snapshot.Form switch
                {
                    ReaperFormId.Blood => -2.25f,
                    ReaperFormId.Frost => -1.82f,
                    ReaperFormId.Soul => -2.7f,
                    ReaperFormId.Death => -1.45f,
                    _ => -2f
                };
                float tremor = snapshot.Form != ReaperFormId.Death
                    && visualCharge >= 0.98f
                    ? (float)Math.Sin(Main.GlobalTimeWrappedHourly * 48f) * 0.025f
                    : 0f;
                visualWeaponAngle = aimAngle + (drawBack + tremor) * facing;
            }
            else
            {
                int releaseDuration = snapshot.Form switch
                {
                    ReaperFormId.Blood => snapshot.Stage >= ReaperStage.StageIII ? 32 : 26,
                    ReaperFormId.Frost => snapshot.Stage >= ReaperStage.StageIII ? 30 : 26,
                    ReaperFormId.Soul => 14 + (snapshot.Stage switch
                    {
                        ReaperStage.StageI => 2,
                        ReaperStage.StageII => 3,
                        _ => 4
                    }) * 8,
                    ReaperFormId.Death => 8,
                    _ => 30
                };
                float progress = SmoothStep(MathHelper.Clamp(releaseTimer / (float)releaseDuration, 0f, 1f));
                if (snapshot.Form == ReaperFormId.Soul)
                    visualWeaponAngle = aimAngle - 2.7f * facing + progress * MathHelper.TwoPi * facing;
                else if (snapshot.Form == ReaperFormId.Death)
                    visualWeaponAngle = aimAngle - 1.45f * facing;
                else
                    visualWeaponAngle = MathHelper.Lerp(aimAngle - 2.25f * facing, aimAngle + 1.45f * facing, progress);
            }
        }
        else
        {
            visualWeaponAngle = GetInstantSpecialWeaponAngle(aimAngle, facing);
        }

        if (visualDirectionInitialized)
        {
            float delta = MathHelper.WrapAngle(visualWeaponAngle - previousVisualWeaponAngle);
            if (Math.Abs(delta) > 0.002f)
                visualSwingDirection = delta >= 0f ? 1 : -1;
        }
        else
        {
            visualDirectionInitialized = true;
        }
        previousVisualWeaponAngle = visualWeaponAngle;

        Projectile.Center = player.MountedCenter;
        player.heldProj = Projectile.whoAmI;
        player.itemTime = Math.Max(player.itemTime, 2);
        player.itemAnimation = Math.Max(player.itemAnimation, 2);
        player.SetCompositeArmFront(true, Player.CompositeArmStretchAmount.Full,
            visualWeaponAngle - MathHelper.PiOver2);
        player.itemRotation = MathHelper.WrapAngle(visualWeaponAngle);
    }

    private float GetInstantSpecialWeaponAngle(float aimAngle, int facing)
    {
        int duration = snapshot.Form switch
        {
            ReaperFormId.Base => 28,
            ReaperFormId.Bone => snapshot.Stage >= ReaperStage.StageIII ? 44 : snapshot.Stage >= ReaperStage.StageII ? 34 : 25,
            ReaperFormId.Infernal => snapshot.Stage >= ReaperStage.StageIII ? 42 : snapshot.Stage >= ReaperStage.StageII ? 32 : 18,
            ReaperFormId.Void => VoidSpecialVisualEndTick,
            ReaperFormId.Death => 50,
            _ => 30
        };
        float progress = MathHelper.Clamp(timer / (float)Math.Max(1, duration), 0f, 1f);
        switch (snapshot.Form)
        {
            case ReaperFormId.Infernal:
            {
                int segmentLength = snapshot.Stage >= ReaperStage.StageIII ? 14 : 16;
                int segment = Math.Max(0, (timer - 1) / segmentLength);
                float local = SmoothStep(((timer - 1) % segmentLength) / (float)segmentLength);
                int direction = segment % 2 == 0 ? facing : -facing;
                return MathHelper.Lerp(aimAngle - 1.55f * direction, aimAngle + 1.25f * direction, local);
            }
            case ReaperFormId.Void:
                return aimAngle - 1.65f * facing;
            case ReaperFormId.Death:
            {
                if (timer < 36)
                {
                    int segment = Math.Max(0, (timer - 1) / 5);
                    float local = SmoothStep(((timer - 1) % 5) / 5f);
                    int direction = segment % 2 == 0 ? facing : -facing;
                    return MathHelper.Lerp(aimAngle - 1.3f * direction, aimAngle + 1.3f * direction, local);
                }
                float finish = SmoothStep(MathHelper.Clamp((timer - 36f) / 14f, 0f, 1f));
                return MathHelper.Lerp(aimAngle + 2.4f * facing, aimAngle - 1.2f * facing, finish);
            }
            default:
                return MathHelper.Lerp(aimAngle - 2.35f * facing, aimAngle + 1.25f * facing, SmoothStep(progress));
        }
    }

    private void DrawSpecialStoryboard(Player player, Vector2 grip)
    {
        Texture2D pixel = TextureAssets.MagicPixel.Value;
        switch (snapshot.Form)
        {
            case ReaperFormId.Base:
                DrawBaseSpecial(pixel, grip);
                break;
            case ReaperFormId.Bone:
                DrawBoneSpecial(pixel, grip);
                break;
            case ReaperFormId.Blood:
                DrawBloodSpecial(pixel, grip);
                break;
            case ReaperFormId.Infernal:
                DrawInfernalSpecial(pixel, grip);
                break;
            case ReaperFormId.Frost:
                DrawFrostSpecial(pixel, grip);
                break;
            case ReaperFormId.Soul:
                DrawSoulSpecial(pixel, grip);
                break;
            case ReaperFormId.Void:
                DrawVoidSpecial(pixel, grip);
                break;
            case ReaperFormId.Death:
                DrawDeathSpecial(pixel, grip);
                break;
        }
    }

    private void DrawBaseSpecial(Texture2D pixel, Vector2 grip)
    {
        float opacity = WindowOpacity(timer, 2, 13, 11);
        if (opacity <= 0f)
            return;

        if (baseDashReady)
        {
            float dashProgress = SmoothStep(MathHelper.Clamp(
                (timer - BaseDashStartTick + 1f) / BaseDashTravelFrames,
                0f, 1f));
            Vector2 dashHead = Vector2.Lerp(baseDashStart, baseDashEnd,
                dashProgress);
            Vector2 axis = (baseDashEnd - baseDashStart)
                .SafeNormalize(aim);
            Vector2 normal = axis.RotatedBy(MathHelper.PiOver2);
            DrawVisualLine(pixel, baseDashStart, dashHead,
                new Color(7, 38, 50, 155) * opacity, 18f);
            DrawVisualLine(pixel, baseDashStart, dashHead,
                new Color(40, 185, 205, 0) * opacity * 0.62f, 7.5f);
            DrawVisualLine(pixel, baseDashStart, dashHead,
                new Color(205, 255, 250, 0) * opacity * 0.86f, 1.7f);
            for (int lane = -1; lane <= 1; lane += 2)
            {
                Vector2 offset = normal * lane * 7f;
                DrawVisualLine(pixel, baseDashStart + offset,
                    dashHead + offset,
                    new Color(75, 220, 230, 0) * opacity * 0.28f,
                    2.2f);
            }

            float ringReveal = SmoothStep(MathHelper.Clamp(
                (timer - BaseDashStartTick) / 5f, 0f, 1f));
            DrawArc(pixel, baseDashStart, 15f + ringReveal * 18f,
                aim.ToRotation() + MathHelper.Pi * 0.62f,
                MathHelper.Pi * 0.76f,
                new Color(105, 235, 240, 0) * opacity
                    * (1f - ringReveal) * 0.55f,
                2.4f, 10);
        }

        float reveal = SmoothStep(MathHelper.Clamp((timer - 5f) / 8f, 0f, 1f));
        float fade = 1f - MathHelper.Clamp((timer - 15f) / 10f, 0f, 1f);
        float startAngle = aim.ToRotation() - 1.42f;
        Vector2 center = grip - aim * 24f;
        DrawArc(pixel, center, 142f, startAngle, 2.45f * reveal,
            new Color(8, 45, 56, 155) * fade * 0.82f, 22f, 22);
        DrawArc(pixel, center, 143.5f, startAngle, 2.45f * reveal,
            new Color(35, 185, 205, 0) * fade * 0.58f, 9f, 22);
        DrawArc(pixel, center, 145f, startAngle, 2.45f * reveal,
            new Color(215, 255, 250, 0) * fade * 0.88f, 2.1f, 22);
        DrawArc(pixel, center, 119f, startAngle + 0.08f,
            2.25f * reveal,
            new Color(70, 220, 230, 0) * fade * 0.24f,
            3.4f, 20);
    }

    private void DrawBoneSpecial(Texture2D pixel, Vector2 grip)
    {
        int stage = Math.Max(1, snapshot.StageNumber);
        int finish = stage >= 3 ? 44 : stage == 2 ? 34 : 25;
        float opacity = 1f - MathHelper.Clamp((timer - (finish - 10f)) / 10f, 0f, 1f);
        float unfold = SmoothStep(MathHelper.Clamp(timer / 7f, 0f, 1f));
        if (opacity <= 0f || unfold <= 0f)
            return;

        Vector2 focus = grip + aim * (stage >= 3 ? 155f : 125f);
        float halfWidth = (58f + stage * 22f) * unfold;
        float halfHeight = (105f + stage * 20f) * unfold;
        Color marrow = new Color(55, 225, 245, 0) * opacity;
        Color ivory = new Color(235, 245, 220, 0) * opacity;
        Vector2 topLeft = focus + new Vector2(-halfWidth, -halfHeight);
        Vector2 topRight = focus + new Vector2(halfWidth, -halfHeight);
        Vector2 bottomLeft = focus + new Vector2(-halfWidth, halfHeight);
        Vector2 bottomRight = focus + new Vector2(halfWidth, halfHeight);
        DrawBoneBeam(pixel, topLeft, bottomLeft, marrow, ivory, 8f, 5);
        DrawBoneBeam(pixel, topRight, bottomRight, marrow, ivory, 8f, 5);
        DrawBoneBeam(pixel, topLeft, topRight, marrow, ivory, 9f, 4);

        // Inward curving ribs keep all three stages recognisably part of the same
        // gallows; later stages close more of the cage instead of changing motif.
        for (int rib = 0; rib < 2 + stage; rib++)
        {
            float y = MathHelper.Lerp(-halfHeight * 0.65f, halfHeight * 0.62f,
                (rib + 1f) / (3f + stage));
            float ribWidth = halfWidth * (0.72f + rib * 0.035f);
            Vector2 left = focus + new Vector2(-halfWidth, y);
            Vector2 inward = focus + new Vector2(-ribWidth * 0.12f, y + 12f);
            Vector2 right = focus + new Vector2(halfWidth, y);
            DrawQuadraticCurve(pixel, left, focus + new Vector2(-ribWidth * 0.52f, y + 15f), inward,
                marrow * 0.48f, 7f, 7);
            DrawQuadraticCurve(pixel, right, focus + new Vector2(ribWidth * 0.52f, y + 15f),
                focus + new Vector2(ribWidth * 0.12f, y + 12f), marrow * 0.48f, 7f, 7);
            DrawQuadraticCurve(pixel, left, focus + new Vector2(-ribWidth * 0.52f, y + 15f), inward,
                ivory * 0.64f, 2f, 7);
            DrawQuadraticCurve(pixel, right, focus + new Vector2(ribWidth * 0.52f, y + 15f),
                focus + new Vector2(ribWidth * 0.12f, y + 12f), ivory * 0.64f, 2f, 7);
        }

        float bladeFall = SmoothStep(MathHelper.Clamp((timer - 7f) / 6f, 0f, 1f));
        float bladeY = MathHelper.Lerp(-halfHeight - 34f, halfHeight + 16f, bladeFall);
        Vector2 bladeStart = focus + new Vector2(0f, -halfHeight - 46f);
        Vector2 bladeEnd = focus + new Vector2(0f, bladeY);
        DrawVisualLine(pixel, bladeStart, bladeEnd, marrow * 0.42f, 24f);
        DrawVisualLine(pixel, bladeStart, bladeEnd, ivory * 0.92f, 5f);
        Vector2 edgeLeft = bladeEnd + new Vector2(-34f - stage * 5f, -16f);
        Vector2 edgeRight = bladeEnd + new Vector2(34f + stage * 5f, -16f);
        DrawVisualLine(pixel, edgeLeft, bladeEnd, ivory, 5f);
        DrawVisualLine(pixel, bladeEnd, edgeRight, ivory, 5f);

        if (stage >= 2)
        {
            float jaw = SmoothStep(MathHelper.Clamp((timer - 17f) / 7f, 0f, 1f));
            float jawOffset = MathHelper.Lerp(halfWidth, 15f, jaw);
            DrawScytheGlyph(pixel, focus + new Vector2(-jawOffset, 7f), Vector2.UnitX, 52f,
                marrow * 0.5f);
            DrawScytheGlyph(pixel, focus + new Vector2(jawOffset, 7f), -Vector2.UnitX, 52f,
                marrow * 0.5f);
            DrawRing(pixel, focus, halfWidth * 0.88f, marrow * 0.28f, 5f, 24);
        }

        if (stage >= 3)
        {
            float close = SmoothStep(MathHelper.Clamp((timer - 27f) / 7f, 0f, 1f));
            DrawVisualLine(pixel, focus - Vector2.UnitX * halfWidth * close,
                focus + Vector2.UnitX * halfWidth * close, marrow * 0.54f, 19f);
            DrawVisualLine(pixel, focus - Vector2.UnitX * halfWidth * close,
                focus + Vector2.UnitX * halfWidth * close, ivory * 0.86f, 4f);
            Vector2 palm = focus + new Vector2(0f, -halfHeight - 70f);
            for (int finger = -2; finger <= 2; finger++)
            {
                Vector2 knuckle = focus + new Vector2(finger * halfWidth * 0.25f, -halfHeight - 22f);
                Vector2 tip = focus + new Vector2(finger * halfWidth * 0.34f, -halfHeight + 18f);
                DrawVisualLine(pixel, palm, knuckle, marrow * 0.3f, 9f);
                DrawVisualLine(pixel, knuckle, tip, ivory * 0.45f, 3f);
            }
        }
    }

    private void DrawBloodSpecial(Texture2D pixel, Vector2 grip)
    {
        if (!specialReleased)
        {
            float pulse = 0.82f + (float)Math.Sin(Main.GlobalTimeWrappedHourly * 9f) * 0.13f;
            Vector2 blade = grip + visualWeaponAngle.ToRotationVector2() * 120f;
            int threads = 4 + Math.Max(1, snapshot.StageNumber);
            for (int index = 0; index < threads; index++)
            {
                float angle = MathHelper.TwoPi * index / threads + Main.GlobalTimeWrappedHourly * 0.55f;
                Vector2 start = grip + angle.ToRotationVector2() * (72f + visualCharge * 32f);
                Vector2 bend = Vector2.Lerp(start, blade, 0.48f)
                    + (angle + MathHelper.PiOver2).ToRotationVector2() * 18f;
                DrawQuadraticCurve(pixel, start, bend, blade,
                    new Color(245, 25, 55, 0) * (0.24f + visualCharge * 0.5f) * pulse,
                    1.5f + visualCharge * 1.8f, 8);
            }
            DrawRing(pixel, grip, 24f + visualCharge * 18f,
                new Color(255, 85, 105, 0) * (0.28f + visualCharge * 0.36f),
                2f + visualCharge * 2f, 24);
            return;
        }

        // Target-local strike and scar projectiles own the release presentation.
        // Keep only a restrained contraction at the caster so the range siphon
        // visibly collapses before those per-target wounds open.
        float contraction = 1f - SmoothStep(MathHelper.Clamp(releaseTimer / 9f, 0f, 1f));
        if (contraction > 0.001f)
            DrawRing(pixel, grip, MathHelper.Lerp(34f, 118f, contraction),
                new Color(248, 38, 72, 0) * (contraction * 0.58f),
                3.2f + contraction * 3f, 48);
    }

    private void DrawTimedBloodCrescent(Texture2D pixel, Vector2 grip, Vector2 direction,
        int startTick, float radius, float thickness, float strength)
    {
        int age = releaseTimer - startTick;
        if (age < 0 || age > 21)
            return;
        float reveal = SmoothStep(MathHelper.Clamp(age / 6f, 0f, 1f));
        float fade = 1f - MathHelper.Clamp((age - 10f) / 11f, 0f, 1f);
        float opacity = fade * strength;
        Vector2 center = grip - direction * (42f + thickness * 0.08f);
        float startAngle = direction.ToRotation() - MathHelper.Pi * 0.59f;
        float sweep = MathHelper.Pi * 1.18f * reveal;
        Color shadow = new Color(68, 0, 16, 0);
        Color blood = new Color(210, 10, 38, 0);
        Color hot = new Color(255, 52, 62, 0);
        Color edge = new Color(255, 239, 218, 0);

        DrawArc(pixel, center, radius, startAngle, sweep, shadow * opacity * 0.42f,
            thickness * 1.32f, 18);
        for (int echo = 2; echo >= 1; echo--)
        {
            DrawArc(pixel, center - direction * (echo * 9f), radius - echo * 8f,
                startAngle - echo * 0.035f, sweep, blood * opacity * (0.13f / echo),
                thickness * (1f - echo * 0.12f), 18);
        }
        DrawArc(pixel, center, radius, startAngle, sweep, blood * opacity * 0.68f,
            thickness, 18);
        DrawArc(pixel, center, radius + thickness * 0.18f, startAngle, sweep,
            hot * opacity * 0.5f, thickness * 0.42f, 18);
        DrawArc(pixel, center, radius + thickness * 0.48f, startAngle, sweep,
            edge * opacity * 0.96f, Math.Max(3f, thickness * 0.075f), 18);
        DrawArc(pixel, center, radius - thickness * 0.19f, startAngle + 0.08f,
            Math.Max(0f, sweep - 0.16f), hot * opacity * 0.42f, 3.4f, 16);

        if (age >= 5 && age <= 12)
        {
            Vector2 impact = center + direction * (radius + thickness * 0.35f);
            DrawStarBurst(pixel, impact, 18f + (age - 5f) * 5f,
                new Color(255, 205, 145, 0) * (1f - (age - 5f) / 8f), 9);
        }
    }

    private void DrawInfernalSpecial(Texture2D pixel, Vector2 grip)
    {
        int pathCount = snapshot.Stage >= ReaperStage.StageIII ? 3
            : snapshot.Stage >= ReaperStage.StageII ? 2 : 1;
        int[] starts = { 2, 15, 29 };
        for (int index = 0; index < pathCount; index++)
        {
            if (!infernalDashReady[index])
                continue;
            int age = timer - starts[index];
            if (age < 0)
                continue;
            float opacity = 1f - MathHelper.Clamp((age - 13f) / 26f, 0f, 1f);
            Vector2 start = infernalDashStarts[index];
            Vector2 end = infernalDashEnds[index];
            DrawVisualLine(pixel, start, end, Color.Black * opacity * 0.78f, 24f + index * 2f);
            DrawVisualLine(pixel, start, end, new Color(235, 55, 8, 0) * opacity * 0.68f,
                15f + index * 2f);
            DrawVisualLine(pixel, start, end, new Color(255, 205, 45, 0) * opacity * 0.78f, 5f);
            DrawVisualLine(pixel, start, end, new Color(255, 250, 210, 0) * opacity * 0.72f, 1.7f);

            Vector2 direction = (end - start).SafeNormalize(aim);
            Vector2 normal = direction.RotatedBy(MathHelper.PiOver2);
            for (int ember = 1; ember <= 7; ember++)
            {
                float amount = ember / 8f;
                float side = ((ember + index) & 1) == 0 ? 1f : -1f;
                Vector2 point = Vector2.Lerp(start, end, amount);
                float height = 9f + (float)Math.Sin((timer + ember * 5f) * 0.31f) * 5f;
                DrawVisualLine(pixel, point, point + normal * side * height,
                    new Color(255, 105, 15, 0) * opacity * 0.52f, 2.4f);
            }
            DrawScytheGlyph(pixel, start, direction, 78f,
                new Color(255, 100, 18, 0) * opacity * 0.22f);
            float fan = SmoothStep(MathHelper.Clamp(age / 6f, 0f, 1f));
            DrawArc(pixel, end, 54f + index * 5f, direction.ToRotation() - 1.35f,
                2.7f * fan, new Color(255, 135, 22, 0) * opacity * 0.55f, 13f, 14);
        }

        if (snapshot.Stage >= ReaperStage.StageIII && timer >= 31
            && infernalDashReady[0] && infernalDashReady[1] && infernalDashReady[2])
        {
            float flare = WindowOpacity(timer, 31, 5, 8);
            DrawVisualLine(pixel, infernalDashEnds[2], infernalDashStarts[0],
                new Color(255, 75, 8, 0) * flare * 0.48f, 10f);
            Vector2 center = (infernalDashStarts[0] + infernalDashEnds[0]
                + infernalDashEnds[1] + infernalDashEnds[2]) * 0.25f;
            DrawRing(pixel, center, 72f, new Color(255, 220, 60, 0) * flare * 0.45f, 7f, 20);
        }
    }

    private void DrawInfernalUltimateRoute(Texture2D pixel)
    {
        for (int index = 0; index < infernalDashReady.Length; index++)
        {
            if (!infernalDashReady[index])
                continue;
            int eventTick = 20 + index * 20;
            float opacity = WindowOpacity(timer, eventTick, 92 - index * 10, 26);
            if (opacity <= 0f)
                continue;
            Vector2 start = infernalDashStarts[index];
            Vector2 end = infernalDashEnds[index];
            DrawVisualLine(pixel, start, end, Color.Black * opacity * 0.9f, 34f);
            DrawVisualLine(pixel, start, end,
                new Color(220, 45, 5, 0) * opacity * 0.78f, 22f);
            DrawVisualLine(pixel, start, end,
                new Color(255, 180, 30, 0) * opacity * 0.88f, 9f);
            DrawVisualLine(pixel, start, end,
                new Color(255, 250, 205, 0) * opacity, 2.5f);
            DrawRing(pixel, end, 26f + index * 3f,
                new Color(255, 210, 55, 0) * opacity * 0.62f, 6f, 16);
        }

        if (timer >= 102 && infernalDashReady[0] && infernalDashReady[4])
        {
            float seal = WindowOpacity(timer, 102, 53, 28);
            Vector2 center = Vector2.Zero;
            int count = 0;
            for (int index = 0; index < infernalDashReady.Length; index++)
            {
                if (!infernalDashReady[index])
                    continue;
                center += infernalDashEnds[index];
                count++;
            }
            if (count > 0)
                center /= count;
            DrawRing(pixel, center, 118f, new Color(255, 65, 8, 0) * seal * 0.42f, 18f, 24);
            DrawRing(pixel, center, 118f, new Color(255, 235, 130, 0) * seal * 0.7f, 2.3f, 24);
        }
    }

    private void DrawFrostSpecial(Texture2D pixel, Vector2 grip)
    {
        int stage = Math.Max(1, snapshot.StageNumber);
        if (!specialReleased)
        {
            float radius = 46f + visualCharge * (22f + stage * 7f);
            Vector2 mirror = grip - aim * (42f + stage * 9f);
            DrawRegularPolygon(pixel, mirror, radius, 6,
                new Color(125, 220, 255, 0) * (0.32f + visualCharge * 0.38f),
                4f + visualCharge * 3f, MathHelper.PiOver2);
            DrawRegularPolygon(pixel, mirror, radius * 0.76f, 6,
                new Color(235, 255, 255, 0) * (0.28f + visualCharge * 0.42f),
                1.8f, MathHelper.PiOver2);
            DrawScytheGlyph(pixel, mirror, aim, 58f,
                new Color(205, 250, 255, 0) * visualCharge * 0.2f);
            return;
        }

        float length = stage switch { 1 => 220f, 2 => 285f, _ => 370f };
        Vector2 focus = grip + aim * (stage >= 3 ? 70f : 35f);
        int mirrorCount = stage switch { 1 => 1, 2 => 2, _ => 4 };
        Vector2 normal = aim.RotatedBy(MathHelper.PiOver2);
        for (int index = 0; index < mirrorCount; index++)
        {
            float side = index % 2 == 0 ? -1f : 1f;
            float row = index / 2f;
            Vector2 center = focus + normal * side * (74f + row * 52f) - aim * row * 34f;
            float appear = WindowOpacity(releaseTimer, index * 2, 15, 8);
            DrawRegularPolygon(pixel, center, 43f + stage * 5f, 6,
                new Color(95, 195, 255, 0) * appear * 0.48f, 6f, MathHelper.PiOver2);
            DrawRegularPolygon(pixel, center, 34f + stage * 4f, 6,
                new Color(240, 255, 255, 0) * appear * 0.62f, 2f, MathHelper.PiOver2);
        }

        DrawTimedCrystalBlade(pixel, focus, aim.RotatedBy(-0.55f), length, 3);
        DrawTimedCrystalBlade(pixel, focus, aim.RotatedBy(0.55f), length, 10);
        if (stage >= 3)
        {
            float coffin = WindowOpacity(releaseTimer, 8, 10, 10);
            DrawRegularPolygon(pixel, focus, 108f, 8,
                new Color(90, 185, 245, 0) * coffin * 0.42f, 18f, MathHelper.PiOver4);
            DrawRegularPolygon(pixel, focus, 108f, 8,
                new Color(245, 255, 255, 0) * coffin * 0.78f, 3f, MathHelper.PiOver4);
        }
    }

    private void DrawTimedCrystalBlade(Texture2D pixel, Vector2 focus, Vector2 direction,
        float length, int startTick)
    {
        float opacity = WindowOpacity(releaseTimer, startTick, 5, 10);
        if (opacity <= 0f)
            return;
        Vector2 start = focus - direction * length * 0.5f;
        Vector2 end = focus + direction * length * 0.5f;
        DrawVisualLine(pixel, start, end, new Color(25, 75, 130, 0) * opacity * 0.5f, 34f);
        DrawVisualLine(pixel, start, end, new Color(85, 205, 255, 0) * opacity * 0.72f, 19f);
        DrawVisualLine(pixel, start, end, new Color(240, 255, 255, 0) * opacity, 3.8f);
        Vector2 normal = direction.RotatedBy(MathHelper.PiOver2);
        for (int shard = 1; shard < 6; shard++)
        {
            Vector2 point = Vector2.Lerp(start, end, shard / 6f);
            float side = (shard & 1) == 0 ? 1f : -1f;
            DrawVisualLine(pixel, point, point + normal * side * (13f + shard * 2f),
                new Color(175, 240, 255, 0) * opacity * 0.62f, 2.2f);
        }
    }

    private void DrawSoulSpecial(Texture2D pixel, Vector2 grip)
    {
        int stage = Math.Max(1, snapshot.StageNumber);
        int circles = stage switch { 1 => 2, 2 => 3, _ => 4 };
        if (!specialReleased)
        {
            for (int ring = 0; ring < circles; ring++)
            {
                float radius = 42f + ring * 16f + visualCharge * (16f + ring * 7f);
                float rotation = Main.GlobalTimeWrappedHourly * (ring % 2 == 0 ? 0.55f : -0.42f);
                DrawArc(pixel, grip, radius, rotation, MathHelper.TwoPi * (0.72f + visualCharge * 0.24f),
                    Color.Lerp(new Color(105, 75, 245, 0), new Color(80, 245, 255, 0),
                        ring / (float)Math.Max(1, circles - 1)) * (0.25f + visualCharge * 0.38f),
                    3f + ring, 20);
            }
            return;
        }

        for (int index = 0; index < circles; index++)
        {
            int eventTick = 6 + index * 8;
            float opacity = WindowOpacity(releaseTimer, eventTick - 3, 7, 12);
            if (opacity <= 0f)
                continue;
            float radius = stage switch
            {
                1 => 92f + index * 48f,
                2 => 92f + index * 58f,
                _ => 96f + index * 70f
            };
            Color soul = Color.Lerp(new Color(110, 75, 245, 0), new Color(70, 245, 255, 0),
                index / (float)Math.Max(1, circles - 1));
            DrawRing(pixel, grip, radius, soul * opacity * 0.3f, 17f + index * 2f, 28);
            DrawRing(pixel, grip, radius, new Color(215, 255, 255, 0) * opacity * 0.72f,
                2.5f, 28);
            int heralds = 2 + stage;
            for (int herald = 0; herald < heralds; herald++)
            {
                float angle = MathHelper.TwoPi * herald / heralds + index * 0.53f;
                Vector2 position = grip + angle.ToRotationVector2() * radius;
                DrawScytheGlyph(pixel, position, (-angle.ToRotationVector2()).RotatedBy(0.25f),
                    42f + stage * 5f, soul * opacity * 0.28f);
            }
        }

        if (stage >= 3)
        {
            float reaper = WindowOpacity(releaseTimer, 28, 5, 12);
            Vector2 hood = grip - Vector2.UnitY * 145f;
            DrawArc(pixel, hood, 72f, MathHelper.Pi, MathHelper.Pi,
                new Color(45, 20, 110, 0) * reaper * 0.5f, 34f, 16);
            DrawScytheGlyph(pixel, hood + aim * 18f, aim, 180f,
                new Color(95, 235, 255, 0) * reaper * 0.42f);
        }
    }

    private void DrawVoidSpecial(Texture2D pixel, Vector2 grip)
    {
        float reveal = SmoothStep(MathHelper.Clamp(timer / 5f, 0f, 1f));
        float fade = 1f - SmoothStep(MathHelper.Clamp(
            (timer - 8f) / Math.Max(1f, VoidSpecialVisualEndTick - 8f), 0f, 1f));
        float opacity = reveal * fade;
        if (opacity <= 0.001f)
            return;

        // A compact world-sampled opening surrounds the held blade while the
        // authoritative persistent path arrives. It uses the same Void backdrop
        // renderer as ordinary and ultimate rifts, so there is no unrelated box
        // or flat purple placeholder.
        Vector2 bladeAxis = visualWeaponAngle.ToRotationVector2();
        Vector2 openingCenter = grip + bladeAxis * (92f + snapshot.StageNumber * 7f);
        ReaperVoidBackdropRenderer.DrawPersistentRift(Main.spriteBatch,
            openingCenter, bladeAxis, 118f + snapshot.StageNumber * 18f,
            20f + snapshot.StageNumber * 5f,
            Projectile.identity * 397 ^ Projectile.owner * 31,
            reveal, opacity * 0.88f);
    }

    private void DrawCoordinateGate(Texture2D pixel, Vector2 center, Vector2 forward,
        float radius, float opacity)
    {
        if (opacity <= 0f || radius <= 1f)
            return;
        Vector2 right = forward.SafeNormalize(Vector2.UnitX);
        Vector2 up = right.RotatedBy(MathHelper.PiOver2);
        Color purple = new Color(185, 45, 255, 0) * opacity;
        Color white = new Color(245, 225, 255, 0) * opacity;
        Vector2 a = center - right * radius - up * radius;
        Vector2 b = center + right * radius - up * radius;
        Vector2 c = center + right * radius + up * radius;
        Vector2 d = center - right * radius + up * radius;
        DrawVisualLine(pixel, a, b, purple * 0.62f, 8f);
        DrawVisualLine(pixel, b, c, purple * 0.62f, 8f);
        DrawVisualLine(pixel, c, d, purple * 0.62f, 8f);
        DrawVisualLine(pixel, d, a, purple * 0.62f, 8f);
        DrawVisualLine(pixel, a, b, white * 0.82f, 1.7f);
        DrawVisualLine(pixel, b, c, white * 0.82f, 1.7f);
        DrawVisualLine(pixel, c, d, white * 0.82f, 1.7f);
        DrawVisualLine(pixel, d, a, white * 0.82f, 1.7f);
        DrawVisualLine(pixel, center - right * radius * 0.78f, center + right * radius * 0.78f,
            purple * 0.4f, 2f);
        DrawVisualLine(pixel, center - up * radius * 0.78f, center + up * radius * 0.78f,
            purple * 0.4f, 2f);
        for (int tick = -2; tick <= 2; tick++)
        {
            Vector2 mark = center + right * radius * tick / 3f;
            DrawVisualLine(pixel, mark - up * 5f, mark + up * 5f, white * 0.6f, 1.5f);
        }
    }

    private void DrawTimedVoidCut(Texture2D pixel, Vector2 start, Vector2 direction,
        float length, int startTick)
    {
        float opacity = WindowOpacity(timer, startTick, 4, 10);
        if (opacity <= 0f)
            return;
        direction = direction.SafeNormalize(aim);
        Vector2 end = start + direction * length;
        Vector2 normal = direction.RotatedBy(MathHelper.PiOver2);
        DrawVisualLine(pixel, start, end, Color.Black * opacity * 0.92f, 31f);
        DrawVisualLine(pixel, start + normal * 13f, end + normal * 13f,
            new Color(160, 30, 240, 0) * opacity * 0.72f, 5f);
        DrawVisualLine(pixel, start - normal * 13f, end - normal * 13f,
            new Color(160, 30, 240, 0) * opacity * 0.72f, 5f);
        DrawVisualLine(pixel, start + normal * 16f, end + normal * 16f,
            new Color(250, 235, 255, 0) * opacity, 1.8f);
        DrawVisualLine(pixel, start - normal * 16f, end - normal * 16f,
            new Color(250, 235, 255, 0) * opacity, 1.8f);
        int fragments = 5;
        for (int index = 1; index <= fragments; index++)
        {
            Vector2 point = Vector2.Lerp(start, end, index / (fragments + 1f));
            float side = (index & 1) == 0 ? 1f : -1f;
            DrawRegularPolygon(pixel, point + normal * side * 25f, 6f + index,
                index % 2 == 0 ? 4 : 3, new Color(205, 65, 255, 0) * opacity * 0.5f,
                2f, direction.ToRotation());
        }
    }

    private void DrawDeathSpecial(Texture2D pixel, Vector2 grip)
    {
        float finisher = WindowOpacity(timer, 35, 5, 11);
        if (finisher > 0f)
        {
            Vector2 center = grip;
            DrawArc(pixel, center - aim * 65f, 235f, aim.ToRotation() - 1.55f, 2.95f,
                Color.Black * finisher * 0.9f, 86f, 20);
            DrawArc(pixel, center - aim * 65f, 238f, aim.ToRotation() - 1.55f, 2.95f,
                new Color(210, 12, 42, 0) * finisher * 0.68f, 48f, 20);
            DrawArc(pixel, center - aim * 65f, 261f, aim.ToRotation() - 1.55f, 2.95f,
                new Color(255, 225, 210, 0) * finisher * 0.9f, 4.5f, 20);
            for (int phase = 0; phase < 6; phase++)
            {
                float angle = MathHelper.TwoPi * phase / 6f + timer * 0.035f;
                Vector2 rune = center + angle.ToRotationVector2() * 82f;
                DrawRegularPolygon(pixel, rune, 11f, 6,
                    (GetDeathPhaseColor(phase) with { A = 0 }) * finisher * 0.65f,
                    2f, angle);
            }
        }
    }

    private static Color GetDeathPhaseColor(int phase) => phase switch
    {
        0 => new Color(80, 235, 255),
        1 => new Color(245, 35, 70),
        2 => new Color(255, 125, 20),
        3 => new Color(135, 220, 255),
        4 => new Color(120, 100, 255),
        _ => new Color(205, 50, 255)
    };

    private static void DrawBoneBeam(Texture2D pixel, Vector2 start, Vector2 end,
        Color glow, Color core, float width, int joints)
    {
        DrawVisualLine(pixel, start, end, glow * 0.42f, width * 1.7f);
        DrawVisualLine(pixel, start, end, core * 0.78f, width * 0.56f);
        Vector2 direction = (end - start).SafeNormalize(Vector2.UnitY);
        Vector2 normal = direction.RotatedBy(MathHelper.PiOver2);
        for (int index = 0; index <= joints; index++)
        {
            Vector2 joint = Vector2.Lerp(start, end, index / (float)Math.Max(1, joints));
            DrawVisualLine(pixel, joint - normal * width * 0.7f, joint + normal * width * 0.7f,
                core * 0.72f, Math.Max(1.4f, width * 0.32f));
        }
    }

    private static void DrawScytheGlyph(Texture2D pixel, Vector2 grip, Vector2 direction,
        float size, Color color)
    {
        direction = direction.SafeNormalize(Vector2.UnitX);
        Vector2 normal = direction.RotatedBy(-MathHelper.PiOver2);
        Vector2 handleEnd = grip + direction * size * 0.62f;
        DrawVisualLine(pixel, grip - direction * size * 0.25f, handleEnd, color, Math.Max(2f, size * 0.045f));
        DrawArc(pixel, handleEnd - normal * size * 0.08f, size * 0.38f,
            direction.ToRotation() - 1.78f, 2.35f, color, Math.Max(2f, size * 0.07f), 10);
    }

    private static void DrawStarBurst(Texture2D pixel, Vector2 center, float radius,
        Color color, int rays)
    {
        for (int index = 0; index < rays; index++)
        {
            float angle = MathHelper.TwoPi * index / rays;
            float length = radius * (index % 3 == 0 ? 1f : 0.58f);
            DrawVisualLine(pixel, center + angle.ToRotationVector2() * 3f,
                center + angle.ToRotationVector2() * length, color,
                index % 3 == 0 ? 3f : 1.6f);
        }
    }

    private static void DrawRing(Texture2D pixel, Vector2 center, float radius,
        Color color, float width, int segments)
    {
        DrawArc(pixel, center, radius, 0f, MathHelper.TwoPi, color, width, segments);
    }

    private static void DrawArc(Texture2D pixel, Vector2 center, float radius,
        float startAngle, float sweep, Color color, float width, int segments)
    {
        if (radius <= 0.5f || width <= 0.05f || segments < 2 || Math.Abs(sweep) <= 0.001f)
            return;
        segments = Math.Max(segments,
            Math.Min(160, (int)Math.Ceiling(Math.Abs(sweep) * radius / 18f)));
        Vector2 previous = center + startAngle.ToRotationVector2() * radius;
        for (int index = 1; index <= segments; index++)
        {
            float amount = index / (float)segments;
            Vector2 next = center + (startAngle + sweep * amount).ToRotationVector2() * radius;
            DrawVisualLine(pixel, previous, next, color, width);
            previous = next;
        }
    }

    private static void DrawRegularPolygon(Texture2D pixel, Vector2 center, float radius,
        int sides, Color color, float width, float rotation)
    {
        if (sides < 3 || radius <= 0.5f)
            return;
        Vector2 previous = center + rotation.ToRotationVector2() * radius;
        for (int index = 1; index <= sides; index++)
        {
            Vector2 next = center + (rotation + MathHelper.TwoPi * index / sides).ToRotationVector2() * radius;
            DrawVisualLine(pixel, previous, next, color, width);
            previous = next;
        }
    }

    private static void DrawQuadraticCurve(Texture2D pixel, Vector2 start, Vector2 control,
        Vector2 end, Color color, float width, int segments)
    {
        Vector2 previous = start;
        for (int index = 1; index <= segments; index++)
        {
            float amount = index / (float)segments;
            float inverse = 1f - amount;
            Vector2 next = start * (inverse * inverse)
                + control * (2f * inverse * amount)
                + end * (amount * amount);
            DrawVisualLine(pixel, previous, next, color, width);
            previous = next;
        }
    }

    private static float WindowOpacity(int current, int start, int sustain, int fade)
    {
        int age = current - start;
        if (age < 0 || age >= sustain + fade)
            return 0f;
        float enter = MathHelper.Clamp(age / 3f, 0f, 1f);
        float exit = age <= sustain ? 1f : 1f - (age - sustain) / (float)Math.Max(1, fade);
        return SmoothStep(enter) * SmoothStep(MathHelper.Clamp(exit, 0f, 1f));
    }

    private void DrawChargeSeal(Vector2 center, Color outer, Color core)
    {
        Texture2D pixel = TextureAssets.MagicPixel.Value;
        float pulse = 0.82f + (float)Math.Sin(Main.GlobalTimeWrappedHourly * 7f) * 0.12f;
        float radius = 38f + visualCharge * 24f;
        int segments = 28;
        Vector2 previous = center + Vector2.UnitX * radius;
        for (int index = 1; index <= segments; index++)
        {
            Vector2 next = center + (MathHelper.TwoPi * index / segments).ToRotationVector2() * radius;
            DrawVisualLine(pixel, previous, next, outer * (0.25f + visualCharge * 0.42f) * pulse,
                2.6f + visualCharge * 1.8f);
            DrawVisualLine(pixel, previous, next, core * (0.32f + visualCharge * 0.5f) * pulse, 0.9f);
            previous = next;
        }

        int runeCount = 3 + (int)snapshot.Stage;
        for (int index = 0; index < runeCount; index++)
        {
            float angle = MathHelper.TwoPi * index / runeCount
                + Main.GlobalTimeWrappedHourly * (snapshot.Form == ReaperFormId.Soul ? 0.7f : 0.25f);
            Vector2 runeCenter = center + angle.ToRotationVector2() * radius;
            Vector2 tangent = (angle + MathHelper.PiOver2).ToRotationVector2() * (3f + visualCharge * 3f);
            DrawVisualLine(pixel, runeCenter - tangent, runeCenter + tangent,
                core * (0.45f + visualCharge * 0.45f), 1.2f);
        }
    }

    private static void DrawVisualLine(Texture2D pixel, Vector2 start, Vector2 end, Color color, float width)
    {
        DeathDomainPrimitiveTextureSystem.DrawSmoothWorldLine(start, end, color, width);
    }

    private static float SmoothStep(float value) => value * value * (3f - 2f * value);

    private void SpawnAmbientVisuals(Player player)
    {
        if (Main.netMode == NetmodeID.Server)
            return;

        Color color = ReaperCombatRegistry.GetPrimaryColor(snapshot.Form);
        Lighting.AddLight(player.Center, color.ToVector3() * (ultimate ? 0.75f : 0.25f));

        if (ultimate)
        {
            SpawnUltimateSoundVisuals(player);
            if (Main.rand.NextBool(2))
            {
                Vector2 radius = Main.rand.NextVector2CircularEdge(180f, 120f);
                SpawnVisualDust(player.MountedCenter + radius,
                    -radius.SafeNormalize(Vector2.Zero) * Main.rand.NextFloat(1f, 4f),
                    ReaperCombatRegistry.GetDust(snapshot.Form), color, 1.25f);
            }
            CommitVisualClock();
            return;
        }

        SpawnSpecialSoundVisuals(player);
        SpawnSpecialAmbientDust(player);
        CommitVisualClock();
    }

    private void SpawnSpecialSoundVisuals(Player player)
    {
        Vector2 position = player.Center;
        if (ReachedVisualTick(1) && ConsumeVisualEvent(0))
        {
            SoundStyle start = snapshot.Form switch
            {
                ReaperFormId.Bone => SoundID.Dig,
                ReaperFormId.Blood => SoundID.NPCDeath6,
                ReaperFormId.Infernal => SoundID.Item74,
                ReaperFormId.Frost => SoundID.Shatter,
                ReaperFormId.Soul => SoundID.NPCDeath6,
                ReaperFormId.Void => SoundID.Item8,
                ReaperFormId.Death => SoundID.Item122,
                _ => SoundID.Item71
            };
            PlayTuned(start, position, 0.54f, -0.32f, 0.08f);
            PlayTuned(SoundID.Item8, position, 0.34f, snapshot.Form == ReaperFormId.Void ? -0.45f : -0.15f, 0.05f);
        }

        if (IsChargedSpecial && !specialReleased)
        {
            int maximumCharge = snapshot.Form == ReaperFormId.Blood ? 600 : 45;
            if (chargeFrames >= maximumCharge && ConsumeVisualEvent(1))
            {
                PlayTuned(SoundID.Item29, position, 0.58f,
                    snapshot.Form == ReaperFormId.Frost ? 0.25f : -0.05f, 0.02f);
                TriggerSpecialImpact(position, aim, 2.2f,
                    ReaperCombatRegistry.GetSecondaryColor(snapshot.Form), 0.12f, 5);
            }
            return;
        }

        switch (snapshot.Form)
        {
            case ReaperFormId.Base:
                if (ReachedVisualTick(3) && ConsumeVisualEvent(4))
                    PlayTuned(SoundID.Item8, position, 0.5f, 0.12f, 0.08f);
                if (ReachedVisualTick(11) && ConsumeVisualEvent(5))
                {
                    Vector2 impactPosition = baseDashReady
                        ? baseDashEnd + aim * 72f
                        : position + aim * 95f;
                    PlayTuned(SoundID.Item71, impactPosition, 0.82f,
                        0.12f, 0.08f);
                    PlayTuned(SoundID.NPCDeath6, impactPosition, 0.28f,
                        0.3f, 0.06f);
                    TriggerSpecialImpact(impactPosition, aim, 3.6f,
                        new Color(125, 235, 240), 0f, 0);
                    SpawnVisualBurst(impactPosition, DustID.AncientLight,
                        new Color(125, 235, 240), 13, 4.3f, 0.95f);
                }
                break;
            case ReaperFormId.Bone:
                PlayBoneSpecialEvents(position);
                break;
            case ReaperFormId.Blood:
                PlayBloodSpecialEvents(position);
                break;
            case ReaperFormId.Infernal:
                PlayInfernalSpecialEvents(position);
                break;
            case ReaperFormId.Frost:
                PlayFrostSpecialEvents(position);
                break;
            case ReaperFormId.Soul:
                PlaySoulSpecialEvents(position);
                break;
            case ReaperFormId.Void:
                PlayVoidSpecialEvents(position);
                break;
            case ReaperFormId.Death:
                PlayDeathSpecialEvents(position);
                break;
        }
    }

    private void PlayBoneSpecialEvents(Vector2 position)
    {
        if (ReachedVisualTick(12) && ConsumeVisualEvent(4))
        {
            PlayTuned(SoundID.Item71, position, 0.82f, -0.28f, 0.07f);
            PlayTuned(SoundID.Dig, position, 0.64f, -0.38f, 0.05f);
            TriggerSpecialImpact(position, Vector2.UnitY, 4.2f, new Color(235, 245, 220), 0.2f, 6);
            SpawnVisualBurst(position + aim * 125f, DustID.DungeonSpirit,
                new Color(85, 230, 245), 14, 4f, 1f);
        }
        if (snapshot.Stage >= ReaperStage.StageII && ReachedVisualTick(22) && ConsumeVisualEvent(5))
        {
            PlayTuned(SoundID.Shatter, position, 0.72f, -0.32f, 0.05f);
            PlayTuned(SoundID.Dig, position, 0.46f, 0.05f, 0.05f);
            TriggerSpecialImpact(position, aim, 4.8f, new Color(90, 235, 255), 0.18f, 6);
        }
        if (snapshot.Stage >= ReaperStage.StageIII && ReachedVisualTick(32) && ConsumeVisualEvent(6))
        {
            PlayTuned(SoundID.Item122, position, 0.8f, -0.28f, 0.04f);
            PlayTuned(SoundID.NPCDeath6, position, 0.55f, -0.42f, 0.05f);
            TriggerSpecialImpact(position, aim, 6.4f, new Color(235, 245, 220), 0.3f, 8);
        }
    }

    private void PlayBloodSpecialEvents(Vector2 position)
    {
        if (ReachedVisualTick(1, releaseClock: true) && ConsumeVisualEvent(4))
        {
            PlayTuned(SoundID.NPCDeath6, position, 0.68f, -0.42f, 0.05f);
            PlayTuned(SoundID.Item8, position, 0.5f, -0.25f, 0.04f);
        }
        if (ReachedVisualTick(7, releaseClock: true) && ConsumeVisualEvent(5))
        {
            PlayTuned(SoundID.Item71, position, 0.96f, -0.26f, 0.06f);
            PlayTuned(SoundID.Item14, position, 0.55f, -0.15f, 0.04f);
            TriggerSpecialImpact(position, aim, snapshot.Stage >= ReaperStage.StageIII ? 8f : 5.5f,
                new Color(255, 222, 200), snapshot.Stage >= ReaperStage.StageIII ? 0.4f : 0.27f, 8);
            SpawnVisualBurst(position + aim * 180f, DustID.Blood,
                new Color(245, 35, 60), 18 + snapshot.StageNumber * 4, 5.2f, 1.1f);
        }
        if (snapshot.Stage >= ReaperStage.StageIII
            && ReachedVisualTick(17, releaseClock: true) && ConsumeVisualEvent(6))
        {
            PlayTuned(SoundID.Item71, position, 0.88f, 0.06f, 0.05f);
            PlayTuned(SoundID.Shatter, position, 0.64f, -0.18f, 0.04f);
            TriggerSpecialImpact(position, aim.RotatedBy(0.38f), 7f,
                new Color(255, 235, 215), 0.34f, 7);
        }
    }

    private void PlayInfernalSpecialEvents(Vector2 position)
    {
        int count = snapshot.Stage >= ReaperStage.StageIII ? 3
            : snapshot.Stage >= ReaperStage.StageII ? 2 : 1;
        int[] ticks = { 3, 16, 30 };
        for (int index = 0; index < count; index++)
        {
            if (!ReachedVisualTick(ticks[index]) || !ConsumeVisualEvent(4 + index))
                continue;
            Vector2 soundPosition = infernalDashReady[index] ? infernalDashEnds[index] : position;
            PlayTuned(SoundID.Item74, soundPosition, 0.78f, -0.18f + index * 0.12f, 0.07f);
            PlayTuned(SoundID.Item71, soundPosition, 0.68f, 0.08f + index * 0.08f, 0.06f);
            if (index == count - 1)
                PlayTuned(SoundID.Item14, soundPosition, 0.54f, -0.05f, 0.05f);
            TriggerSpecialImpact(soundPosition, aim, index == count - 1 ? 6.8f : 4.2f,
                new Color(255, 215, 90), index == count - 1 ? 0.31f : 0.16f, 6);
            SpawnVisualBurst(soundPosition, DustID.Torch, new Color(255, 110, 18),
                13 + index * 4, 5.5f, 1.15f);
        }
    }

    private void PlayFrostSpecialEvents(Vector2 position)
    {
        if (ReachedVisualTick(1, releaseClock: true) && ConsumeVisualEvent(4))
            PlayTuned(SoundID.Item29, position, 0.5f, 0.22f, 0.03f);
        if (ReachedVisualTick(7, releaseClock: true) && ConsumeVisualEvent(5))
        {
            PlayTuned(SoundID.Item71, position, 0.78f, 0.28f, 0.05f);
            PlayTuned(SoundID.Shatter, position, 0.68f, 0.08f, 0.04f);
            TriggerSpecialImpact(position, aim.RotatedBy(-0.55f), 4.6f,
                new Color(210, 250, 255), 0.2f, 6);
        }
        if (ReachedVisualTick(14, releaseClock: true) && ConsumeVisualEvent(6))
        {
            PlayTuned(SoundID.Item71, position, 0.82f, 0.42f, 0.04f);
            PlayTuned(SoundID.Shatter, position, 0.85f, -0.12f, 0.04f);
            TriggerSpecialImpact(position, aim.RotatedBy(0.55f),
                snapshot.Stage >= ReaperStage.StageIII ? 7.4f : 5.2f,
                Color.White, snapshot.Stage >= ReaperStage.StageIII ? 0.38f : 0.24f, 8);
            SpawnVisualBurst(position + aim * 65f, DustID.IceTorch,
                new Color(150, 225, 255), 16 + snapshot.StageNumber * 4, 4.5f, 1.05f);
        }
    }

    private void PlaySoulSpecialEvents(Vector2 position)
    {
        if (ReachedVisualTick(1, releaseClock: true) && ConsumeVisualEvent(4))
            PlayTuned(SoundID.NPCDeath6, position, 0.52f, -0.18f, 0.05f);
        int circles = snapshot.Stage switch { ReaperStage.StageI => 2, ReaperStage.StageII => 3, _ => 4 };
        for (int index = 0; index < circles; index++)
        {
            int tick = 6 + index * 8;
            if (!ReachedVisualTick(tick, releaseClock: true) || !ConsumeVisualEvent(5 + index))
                continue;
            PlayTuned(SoundID.Item71, position, 0.64f + index * 0.06f,
                -0.18f + index * 0.12f, 0.06f);
            if (index == circles - 1)
                PlayTuned(SoundID.NPCDeath6, position, 0.52f, -0.35f, 0.04f);
            TriggerSpecialImpact(position, aim.RotatedBy(index * MathHelper.TwoPi / circles),
                index == circles - 1 ? 6f : 3.2f,
                index == circles - 1 ? new Color(105, 245, 255) : new Color(130, 100, 255),
                index == circles - 1 ? 0.28f : 0.12f, 6);
        }
    }

    private void PlayVoidSpecialEvents(Vector2 position)
    {
        if (!ReachedVisualTick(VoidSpecialTeleportTick) || !ConsumeVisualEvent(4))
            return;
        PlayTuned(SoundID.Item71, position, 0.88f, 0.22f, 0.04f);
        PlayTuned(SoundID.Item122, position, 0.62f, -0.42f, 0.03f);
        TriggerSpecialImpact(position, aim, 7.2f,
            new Color(235, 215, 255), 0.31f, 6);
        SpawnVisualBurst(position, DustID.Shadowflame,
            new Color(190, 55, 255), 18, 5.4f, 1.05f);
    }

    private void PlayDeathSpecialEvents(Vector2 position)
    {
        for (int phase = 0; phase < 6; phase++)
        {
            int eventTick = 4 + phase * 5;
            if (!ReachedVisualTick(eventTick) || !ConsumeVisualEvent(4 + phase))
                continue;
            Vector2 eventPosition = position;
            PlayTuned(SoundID.Item71, eventPosition, 0.58f + phase * 0.045f,
                -0.16f + phase * 0.09f, 0.04f);
            if (phase is 0 or 2 or 4)
                PlayTuned(phase == 2 ? SoundID.Item74 : SoundID.NPCDeath6,
                    eventPosition, 0.3f, -0.3f + phase * 0.06f, 0.04f);
            TriggerSpecialImpact(eventPosition, aim.RotatedBy(MathHelper.PiOver2),
                2.8f + phase * 0.3f, GetDeathPhaseColor(phase), 0.1f, 4);
        }
        if (ReachedVisualTick(38) && ConsumeVisualEvent(12))
        {
            PlayTuned(SoundID.Item122, position, 0.95f, -0.34f, 0.03f);
            PlayTuned(SoundID.Item14, position, 0.7f, -0.16f, 0.04f);
            TriggerSpecialImpact(position, aim, 9f, new Color(255, 220, 205), 0.44f, 9);
            SpawnVisualBurst(position, DustID.Shadowflame, new Color(235, 25, 65), 28, 6f, 1.2f);
        }
    }

    private void SpawnUltimateSoundVisuals(Player player)
    {
        // Ultimate audio is placed on the local listener. Unlike ordinary attacks,
        // the complete cinematic mix must not disappear merely because its owner is
        // at the edge of the sync range.
        Vector2 listener = Main.LocalPlayer.active ? Main.LocalPlayer.Center : player.Center;
        if (ReachedVisualTick(1) && ConsumeVisualEvent(0))
        {
            SoundStyle ritual = snapshot.Form switch
            {
                ReaperFormId.Bone => SoundID.NPCDeath6,
                ReaperFormId.Blood => SoundID.NPCDeath6,
                ReaperFormId.Infernal => SoundID.Item74,
                ReaperFormId.Frost => SoundID.Item29,
                ReaperFormId.Soul => SoundID.NPCDeath6,
                ReaperFormId.Void => SoundID.Item8,
                _ => SoundID.Item122
            };
            PlayTuned(ritual, listener, 0.7f, -0.42f, 0.04f);
            PlayTuned(SoundID.Item8, listener, 0.45f, -0.35f, 0.03f);
        }

        switch (snapshot.Form)
        {
            case ReaperFormId.Bone:
                for (int index = 0; index < 4; index++)
                    PlayUltimateCut(listener, 30 + index * 36, 4 + index, -0.32f + index * 0.09f,
                        SoundID.Dig, new Color(210, 250, 245), 4.8f);
                PlayUltimateCut(listener, 168, 8, -0.24f, SoundID.NPCDeath6,
                    new Color(225, 250, 235), 7.5f);
                PlayUltimateFinal(listener, 207, 9, new Color(235, 245, 220), -0.36f);
                break;
            case ReaperFormId.Blood:
                for (int index = 0; index < 6; index++)
                    PlayUltimateCut(listener, 30 + index * 15, 4 + index, -0.28f + index * 0.08f,
                        index % 2 == 0 ? SoundID.NPCDeath6 : SoundID.Item14,
                        new Color(255, 80, 95), 4.5f + index * 0.25f);
                PlayUltimateFinal(listener, 180, 12, new Color(255, 225, 205), -0.28f);
                break;
            case ReaperFormId.Infernal:
                for (int index = 0; index < 5; index++)
                    PlayUltimateCut(listener, 20 + index * 20, 4 + index, -0.18f + index * 0.1f,
                        SoundID.Item74, new Color(255, 190, 55), 5.2f);
                PlayUltimateCut(listener, 130, 10, -0.12f, SoundID.Item14,
                    new Color(255, 225, 105), 8f);
                PlayUltimateFinal(listener, 190, 11, new Color(255, 242, 195), -0.18f);
                break;
            case ReaperFormId.Frost:
                for (int index = 0; index < 4; index++)
                    PlayUltimateCut(listener, 30 + index * 18, 4 + index, 0.12f + index * 0.08f,
                        SoundID.Shatter, new Color(195, 245, 255), 4.6f);
                PlayUltimateCut(listener, 100, 9, 0.18f, SoundID.Shatter,
                    new Color(225, 255, 255), 7f);
                PlayUltimateFinal(listener, 110, 10, Color.White, 0.02f);
                break;
            case ReaperFormId.Soul:
                PlayUltimateCut(listener, 20, 4, -0.25f, SoundID.NPCDeath6,
                    new Color(130, 105, 255), 4.5f);
                for (int index = 0; index < 8; index++)
                    PlayUltimateCut(listener, 32 + index * 8, 5 + index, -0.15f + index * 0.055f,
                        index % 2 == 0 ? SoundID.NPCDeath6 : SoundID.Item8,
                        new Color(90, 235, 255), 3.8f);
                PlayUltimateFinal(listener, 104, 14, new Color(180, 250, 255), -0.24f);
                break;
            case ReaperFormId.Void:
                PlayUltimateFinal(listener, VoidUltimateBlackHoleTick, 4,
                    new Color(250, 235, 255), -0.45f);
                break;
            case ReaperFormId.Death:
                for (int index = 0; index < 6; index++)
                    PlayUltimateCut(listener, 36 + index * 18, 4 + index, -0.22f + index * 0.08f,
                        index switch
                        {
                            0 => SoundID.Dig,
                            1 => SoundID.NPCDeath6,
                            2 => SoundID.Item74,
                            3 => SoundID.Shatter,
                            4 => SoundID.Item8,
                            _ => SoundID.Item122
                        }, GetDeathPhaseColor(index), 5.8f);
                PlayUltimateFinal(listener, ReaperDeathUltimateGeometry.ShatterTick,
                    12, new Color(190, 14, 48), -0.38f, flashStrength: 0.12f);
                break;
        }
    }

    private void PlayUltimateCut(Vector2 listener, int eventTick, int eventIndex,
        float pitch, SoundStyle accent, Color flash, float shake)
    {
        if (!ReachedVisualTick(eventTick) || !ConsumeVisualEvent(eventIndex))
            return;
        PlayTuned(SoundID.Item71, listener, 0.78f, pitch, 0.035f);
        PlayTuned(accent, listener, 0.42f, pitch - 0.12f, 0.025f);
        ReaperVfxDirector.TriggerGlobalImpact(aim, shake, 7, flash, 0.18f, 6, 0.24f);
    }

    private void PlayUltimateFinal(Vector2 listener, int eventTick, int eventIndex,
        Color flash, float pitch, float flashStrength = 0.68f)
    {
        if (!ReachedVisualTick(eventTick) || !ConsumeVisualEvent(eventIndex))
            return;
        PlayTuned(SoundID.Item122, listener, 1f, pitch, 0.02f);
        PlayTuned(SoundID.Item14, listener, 0.76f, pitch + 0.14f, 0.025f);
        PlayTuned(SoundID.NPCDeath6, listener, 0.45f, pitch - 0.12f, 0.02f);
        ReaperVfxDirector.TriggerGlobalImpact(aim, 12f, 14, flash,
            MathHelper.Clamp(flashStrength, 0f, 0.68f), 12, 0.58f);
    }

    private void SpawnSpecialAmbientDust(Player player)
    {
        if (IsChargedSpecial && !specialReleased)
        {
            if (!Main.rand.NextBool(2))
                return;
            Vector2 radius = Main.rand.NextVector2CircularEdge(58f + visualCharge * 42f,
                42f + visualCharge * 28f);
            Vector2 velocity = -radius.SafeNormalize(Vector2.Zero) * Main.rand.NextFloat(1.2f, 3.4f);
            int dustType = snapshot.Form switch
            {
                ReaperFormId.Blood => DustID.Blood,
                ReaperFormId.Frost => DustID.IceTorch,
                _ => DustID.AncientLight
            };
            SpawnVisualDust(player.MountedCenter + radius, velocity, dustType,
                ReaperCombatRegistry.GetPrimaryColor(snapshot.Form), 0.8f + visualCharge * 0.45f);
            return;
        }

        switch (snapshot.Form)
        {
            case ReaperFormId.Infernal:
            {
                int count = snapshot.Stage >= ReaperStage.StageIII ? 3
                    : snapshot.Stage >= ReaperStage.StageII ? 2 : 1;
                for (int index = 0; index < count; index++)
                {
                    if (!infernalDashReady[index] || !Main.rand.NextBool(3))
                        continue;
                    Vector2 point = Vector2.Lerp(infernalDashStarts[index], infernalDashEnds[index], Main.rand.NextFloat());
                    SpawnVisualDust(point, Main.rand.NextVector2Circular(1.5f, 2.8f),
                        DustID.Torch, new Color(255, 105, 15), 0.95f);
                }
                break;
            }
            case ReaperFormId.Blood:
                if (specialReleased && Main.rand.NextBool(2))
                {
                    Vector2 offset = Main.rand.NextVector2CircularEdge(130f, 95f);
                    SpawnVisualDust(player.Center + offset, -offset.SafeNormalize(Vector2.Zero) * 2.4f,
                        DustID.Blood, new Color(235, 30, 55), 1.05f);
                }
                break;
            case ReaperFormId.Frost:
                if (specialReleased && Main.rand.NextBool(2))
                    SpawnVisualDust(player.Center + Main.rand.NextVector2Circular(130f, 100f),
                        Main.rand.NextVector2Circular(1.5f, 1.5f), DustID.IceTorch,
                        new Color(155, 225, 255), 0.9f);
                break;
            case ReaperFormId.Soul:
                if (specialReleased && Main.rand.NextBool(2))
                {
                    Vector2 offset = Main.rand.NextVector2CircularEdge(170f, 170f);
                    SpawnVisualDust(player.Center + offset, -offset.SafeNormalize(Vector2.Zero) * 1.8f,
                        DustID.AncientLight, new Color(105, 210, 255), 0.9f);
                }
                break;
            default:
                if (Main.rand.NextBool(4))
                {
                    Vector2 radius = Main.rand.NextVector2CircularEdge(70f, 50f);
                    SpawnVisualDust(player.MountedCenter + radius,
                        -radius.SafeNormalize(Vector2.Zero) * Main.rand.NextFloat(1f, 2.6f),
                        ReaperCombatRegistry.GetDust(snapshot.Form),
                        ReaperCombatRegistry.GetPrimaryColor(snapshot.Form), 0.85f);
                }
                break;
        }
    }

    private bool ConsumeVisualEvent(int eventIndex)
    {
        if (eventIndex < 0 || eventIndex >= 64)
            return false;
        ulong flag = 1UL << eventIndex;
        if ((playedVisualEvents & flag) != 0UL)
            return false;
        playedVisualEvents |= flag;
        return true;
    }

    private bool ReachedVisualTick(int eventTick, bool releaseClock = false)
    {
        if (!visualClockInitialized)
            return false;
        int current = releaseClock ? releaseTimer : timer;
        int previous = releaseClock ? previousVisualReleaseTimer : previousVisualTimer;
        return previous < eventTick && current >= eventTick;
    }

    private void CommitVisualClock()
    {
        if (!visualClockInitialized)
            return;
        // Never rewind this presentation clock when an older ExtraAI packet
        // arrives. Future event crossings remain catchable after local time heals.
        previousVisualTimer = Math.Max(previousVisualTimer, timer);
        previousVisualReleaseTimer = Math.Max(previousVisualReleaseTimer, releaseTimer);
    }

    private static void PlayTuned(SoundStyle style, Vector2 position,
        float volume, float pitch, float variance)
    {
        SoundEngine.PlaySound(style with
        {
            Volume = MathHelper.Clamp(volume, 0f, 1f),
            Pitch = MathHelper.Clamp(pitch, -1f, 1f),
            PitchVariance = MathHelper.Clamp(variance, 0f, 1f),
            MaxInstances = 2,
            SoundLimitBehavior = SoundLimitBehavior.IgnoreNew
        }, position);
    }

    private static void TriggerSpecialImpact(Vector2 position, Vector2 direction,
        float strength, Color color, float opacity, int frames)
    {
        if (!Main.LocalPlayer.active
            || Vector2.DistanceSquared(Main.LocalPlayer.Center, position) > 1500f * 1500f)
        {
            return;
        }
        ReaperVfxDirector.TriggerGlobalImpact(direction, strength, frames,
            color, opacity, frames, Math.Min(0.42f, opacity + 0.08f));
    }

    private static void SpawnVisualBurst(Vector2 center, int dustType, Color color,
        int count, float speed, float scale)
    {
        for (int index = 0; index < count; index++)
        {
            float angle = MathHelper.TwoPi * index / Math.Max(1, count);
            Vector2 velocity = angle.ToRotationVector2()
                * Main.rand.NextFloat(speed * 0.45f, speed);
            SpawnVisualDust(center, velocity, dustType, color,
                scale * Main.rand.NextFloat(0.8f, 1.18f));
        }
    }

    private static void SpawnVisualDust(Vector2 position, Vector2 velocity,
        int dustType, Color color, float scale)
    {
        Dust dust = Dust.NewDustPerfect(position, dustType, velocity, 60, color, scale);
        dust.noGravity = true;
    }
}