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

DeathMod

【Terraria】DeathMod (now aka Soul Harvest)

公开
关注 0 Fork 0 Star 0
UTF-8
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using ReLogic.Content;
using System;
using System.Collections.Generic;
using Terraria;
using Terraria.GameContent;
using Terraria.ModLoader;
using Terraria.UI;

namespace SoulHarvest.Common;

/// <summary>
/// Client-only presentation for Reaper ultimates. Combat code reports the authoritative
/// visual clock each tick; this system owns only short-lived draw state and asks the
/// shared VFX director for capped camera/flash punctuation. It never creates attacks.
/// </summary>
[Autoload(Side = ModSide.Client)]
public sealed class ReaperUltimateVisualSystem : ModSystem
{
    private const int TailFrames = 18;
    private const int CrescentMaskSize = 512;
    private const float CrescentReferenceRadius = 180f;
    private static UltimateVisualState[] states = [];
    private static Asset<Texture2D>? soulTexture;
    private static ReaperUltimateCrescentMaskSet[] crescentMasks = [];
    private static readonly float[] deathMirrorAngles =
        new float[ReaperDeathUltimateGeometry.CutCount * 2];
    private static readonly VertexPositionColorTexture[] deathMirrorVertices =
        new VertexPositionColorTexture[ReaperDeathUltimateGeometry.CutCount * 6];
    private static BasicEffect? deathMirrorEffect;

    /// <summary>
    /// Reports one frame of an active ultimate. This method is safe to call from
    /// projectile AI on every net mode: dedicated servers immediately return.
    /// </summary>
    public static void ReportUltimate(Player owner, ReaperFormId form, Vector2 aim,
        Vector2 focusWorld, int actionId, int timer, int duration)
    {
        if (Main.dedServ
            || Main.gameMenu
            || owner is null
            || !owner.active
            || owner.dead
            || owner.whoAmI < 0
            || owner.whoAmI >= Main.maxPlayers
            || form == ReaperFormId.Base
            || (byte)form > (byte)ReaperFormId.Death)
        {
            return;
        }

        EnsureStateStorage();
        Vector2 fallback = Vector2.UnitX * (owner.direction == 0 ? 1 : owner.direction);
        if (!float.IsFinite(aim.X) || !float.IsFinite(aim.Y) || aim.LengthSquared() < 0.0001f)
            aim = fallback;
        else
            aim.Normalize();
        if (!float.IsFinite(focusWorld.X) || !float.IsFinite(focusWorld.Y))
            focusWorld = owner.MountedCenter + aim * 180f;

        duration = Math.Clamp(duration, 1, 3600);
        timer = Math.Clamp(timer, 0, duration);
        ref UltimateVisualState state = ref states[owner.whoAmI];
        bool restarted = !state.Active || state.Form != form || state.ActionId != actionId;
        // A remote controller can first replicate halfway through its action.
        // Floor a newly observed storyboard at that authoritative frame so all
        // earlier impacts are not replayed at once. Tick-one openings still cross
        // normally for actions observed from their actual start.
        int previousTimer = restarted
            ? (timer <= 1 ? timer - 1 : timer)
            : state.Timer;
        if (!restarted)
        {
            // Extra-AI packets can arrive after the local controller has already
            // advanced. Never rewind a storyboard for the same authoritative
            // action, otherwise old packets replay white flashes, sounds and shake.
            int monotonicFloor = state.LastReportTick < Main.GameUpdateCount
                ? Math.Min(duration, state.Timer + 1)
                : state.Timer;
            timer = Math.Max(timer, monotonicFloor);
        }
        state.Active = true;
        state.Form = form;
        state.ActionId = actionId;
        state.OriginWorld = owner.MountedCenter;
        state.FocusWorld = focusWorld;
        state.Aim = aim;
        state.Timer = timer;
        state.Duration = duration;
        state.FramesSinceReport = 0;
        state.LastReportTick = Main.GameUpdateCount;
        float normalizedTime = state.Timer / (float)Math.Max(1, state.Duration);
        ReaperVfxDirector.ReportCinematicCamera(owner.whoAmI, state.ActionId,
            normalizedTime, aim, form == ReaperFormId.Death ? 1f : 0.84f);
        TriggerTimelineImpacts(form, aim, previousTimer, state.Timer);
    }

    public override void Load()
    {
        EnsureStateStorage();
        soulTexture = ModContent.Request<Texture2D>("SoulHarvest/Items/Soul", AssetRequestMode.ImmediateLoad);
    }

    public override void Unload()
    {
        states = [];
        soulTexture = null;

        // Content hooks may run on a loader worker. Detach immediately so no
        // future draw can observe these resources, then dispose them on FNA's
        // graphics thread just like the other procedural texture systems.
        ReaperUltimateCrescentMaskSet[] oldMasks = crescentMasks;
        crescentMasks = [];
        BasicEffect? oldMirrorEffect = deathMirrorEffect;
        deathMirrorEffect = null;
        if (!Main.dedServ && oldMasks.Length > 0)
            Main.QueueMainThreadAction(() => DisposeCrescentMasks(oldMasks));
        if (!Main.dedServ && oldMirrorEffect is not null)
            Main.QueueMainThreadAction(oldMirrorEffect.Dispose);
    }

    public override void OnWorldUnload()
    {
        ClearStates();
    }

    public override void PostUpdateEverything()
    {
        if (Main.gameMenu)
        {
            ClearStates();
            return;
        }

        EnsureStateStorage();
        for (int index = 0; index < states.Length; index++)
        {
            ref UltimateVisualState state = ref states[index];
            if (!state.Active)
                continue;

            Player owner = Main.player[index];
            if (!owner.active || owner.dead || state.LastReportTick > Main.GameUpdateCount)
            {
                state = default;
                continue;
            }

            if (state.LastReportTick == Main.GameUpdateCount)
            {
                state.FramesSinceReport = 0;
                continue;
            }

            state.FramesSinceReport++;
            if (state.FramesSinceReport > TailFrames)
                state = default;
        }
    }

    public override void ModifyInterfaceLayers(List<GameInterfaceLayer> layers)
    {
        int index = layers.FindIndex(layer => layer.Name == "Vanilla: Resource Bars");
        if (index < 0)
            index = layers.FindIndex(layer => layer.Name == "Vanilla: Mouse Text");
        if (index < 0)
            index = layers.Count;

        layers.Insert(index, new LegacyGameInterfaceLayer(
            "SoulHarvest: Reaper Ultimate Spectacle",
            DrawUltimateLayer,
            InterfaceScaleType.None));
    }

    private static bool DrawUltimateLayer()
    {
        if (Main.gameMenu || states.Length == 0)
            return true;

        Player localPlayer = Main.LocalPlayer;
        if (!localPlayer.active)
            return true;

        // This layer renders in physical screen space. Applying UI scale here
        // confined the composition to the upper-left on high-DPI displays.
        const float uiScale = 1f;
        Vector2 viewport = new(Main.screenWidth, Main.screenHeight);
        Rectangle viewportBounds = new(0, 0,
            Math.Max(1, (int)Math.Ceiling(viewport.X)),
            Math.Max(1, (int)Math.Ceiling(viewport.Y)));
        Texture2D pixel = TextureAssets.MagicPixel.Value;
        SpriteBatch batch = Main.spriteBatch;

        float strongestDarkness = 0f;
        for (int index = 0; index < states.Length; index++)
        {
            ref UltimateVisualState state = ref states[index];
            if (!IsVisible(index, in state, viewport, uiScale))
                continue;

            strongestDarkness = Math.Max(strongestDarkness,
                GetBackdropDarkness(in state) * GetStateOpacity(in state));
        }

        if (strongestDarkness <= 0.001f)
            return true;

        // Interface drawing always runs on the graphics thread. Creating the
        // procedural masks lazily here avoids allocating Texture2D instances
        // from a content-loader worker and keeps dedicated servers texture-free.
        EnsureCrescentMasks();

        batch.Draw(pixel, viewportBounds, Color.Black * strongestDarkness);

        for (int index = 0; index < states.Length; index++)
        {
            ref UltimateVisualState state = ref states[index];
            if (!IsVisible(index, in state, viewport, uiScale))
                continue;

            DrawState(batch, pixel, index, in state, viewport, viewportBounds, uiScale);
        }

        return true;
    }

    private static bool IsVisible(int playerIndex, in UltimateVisualState state, Vector2 viewport, float uiScale)
    {
        if (!state.Active || playerIndex < 0 || playerIndex >= Main.maxPlayers)
            return false;

        Player owner = Main.player[playerIndex];
        if (!owner.active || owner.dead)
            return false;
        // Every client which has the authoritative action projectile renders the full
        // storyboard. Off-screen owners are re-framed in DrawState instead of receiving
        // the old reduced/culled presentation.
        return true;
    }

    private static void DrawState(
        SpriteBatch batch,
        Texture2D pixel,
        int ownerIndex,
        in UltimateVisualState state,
        Vector2 viewport,
        Rectangle viewportBounds,
        float uiScale)
    {
        float progress = MathHelper.Clamp(state.Timer / (float)Math.Max(1, state.Duration), 0f, 1f);
        float opacity = GetStateOpacity(in state);
        if (opacity <= 0.001f)
            return;

        Vector2 origin = state.OriginWorld - Main.screenPosition;
        const float presentationMargin = 120f;
        if (origin.X < -presentationMargin || origin.X > viewport.X + presentationMargin
            || origin.Y < -presentationMargin || origin.Y > viewport.Y + presentationMargin)
        {
            origin = viewport * 0.5f - state.Aim * 180f;
        }
        Vector2 focus = state.FocusWorld - Main.screenPosition;
        if (state.Form != ReaperFormId.Death
            && (focus.X < -presentationMargin
                || focus.X > viewport.X + presentationMargin
                || focus.Y < -presentationMargin
                || focus.Y > viewport.Y + presentationMargin))
        {
            focus = viewport * 0.5f;
        }
        Color primary = ReaperCombatRegistry.GetPrimaryColor(state.Form);
        Color secondary = ReaperCombatRegistry.GetSecondaryColor(state.Form);
        float time = Main.GlobalTimeWrappedHourly;

        batch.Draw(pixel, viewportBounds, primary * (0.028f * opacity));
        DrawGlow(batch, focus, Math.Min(viewport.X, viewport.Y) * 0.33f,
            primary * (0.105f * opacity));
        DrawCinematicFrame(batch, pixel, viewport, progress, opacity, primary, secondary);

        switch (state.Form)
        {
            case ReaperFormId.Bone:
                DrawBone(batch, pixel, focus, viewport, progress, opacity, time, primary, secondary);
                break;
            case ReaperFormId.Blood:
                DrawBlood(batch, pixel, focus, viewport, progress, opacity, time, primary, secondary);
                break;
            case ReaperFormId.Infernal:
                DrawInfernal(batch, pixel, focus, viewport, progress, opacity, time, primary, secondary);
                break;
            case ReaperFormId.Frost:
                DrawFrost(batch, pixel, focus, viewport, viewportBounds, progress, opacity, time, primary, secondary);
                break;
            case ReaperFormId.Soul:
                DrawSoul(batch, pixel, focus, viewport, progress, opacity, time, primary, secondary);
                break;
            case ReaperFormId.Void:
                DrawVoid(batch, pixel, focus, viewport, viewportBounds, progress, opacity,
                    time, primary, secondary, state.Aim, state.ActionId);
                break;
            case ReaperFormId.Death:
                DrawDeath(batch, pixel, ownerIndex, in state, focus, viewport,
                    viewportBounds, progress, opacity, time);
                break;
        }
    }

    private static void TriggerTimelineImpacts(
        ReaperFormId form,
        Vector2 direction,
        int previousTimer,
        int currentTimer)
    {
        if (currentTimer <= previousTimer)
            return;

        Color primary = ReaperCombatRegistry.GetPrimaryColor(form);
        Color secondary = ReaperCombatRegistry.GetSecondaryColor(form);
        switch (form)
        {
            case ReaperFormId.Bone:
                TriggerImpactAt(30, previousTimer, currentTimer, direction, 3.4f, primary, 0.10f);
                TriggerImpactAt(66, previousTimer, currentTimer, -direction, 4.0f, primary, 0.12f);
                TriggerImpactAt(102, previousTimer, currentTimer, direction.RotatedBy(0.8f), 4.8f, secondary, 0.15f);
                TriggerImpactAt(138, previousTimer, currentTimer, direction.RotatedBy(-0.8f), 5.6f, secondary, 0.18f);
                TriggerImpactAt(168, previousTimer, currentTimer, Vector2.UnitY, 7.8f, secondary, 0.34f);
                TriggerImpactAt(207, previousTimer, currentTimer, direction, 11.5f, Color.White, 0.68f);
                break;
            case ReaperFormId.Blood:
                TriggerImpactAt(18, previousTimer, currentTimer, Vector2.UnitY,
                    4.2f, new Color(170, 0, 34), 0.16f);
                foreach (int tick in new[] { 48, 78, 108, 138 })
                    TriggerImpactAt(tick, previousTimer, currentTimer,
                        direction.RotatedBy(tick * 0.017f), 4.5f,
                        tick % 2 == 0 ? primary : secondary, 0.13f);
                TriggerImpactAt(166, previousTimer, currentTimer, direction,
                    12f, new Color(255, 238, 228), 0.75f);
                break;
            case ReaperFormId.Infernal:
                for (int index = 0; index < 5; index++)
                    TriggerImpactAt(20 + index * 20, previousTimer, currentTimer,
                        direction.RotatedBy(index * MathHelper.TwoPi / 5f), 4.8f + index * 0.3f,
                        index % 2 == 0 ? primary : secondary, 0.16f);
                TriggerImpactAt(130, previousTimer, currentTimer, direction, 8f, secondary, 0.36f);
                TriggerImpactAt(190, previousTimer, currentTimer, Vector2.UnitY, 12f, Color.White, 0.72f);
                break;
            case ReaperFormId.Frost:
                TriggerImpactAt(1, previousTimer, currentTimer, Vector2.UnitY, 2.6f, new Color(180, 230, 255), 0.12f);
                for (int index = 0; index < 4; index++)
                    TriggerImpactAt(30 + index * 18, previousTimer, currentTimer,
                        direction.RotatedBy(index * MathHelper.PiOver2), 4.2f + index * 0.35f,
                        index % 2 == 0 ? primary : secondary, 0.17f);
                TriggerImpactAt(100, previousTimer, currentTimer, direction.RotatedBy(-0.5f), 8.4f, secondary, 0.43f);
                TriggerImpactAt(110, previousTimer, currentTimer, direction.RotatedBy(0.5f), 11.2f, Color.White, 0.76f);
                break;
            case ReaperFormId.Soul:
                TriggerImpactAt(20, previousTimer, currentTimer, Vector2.UnitY, 3.3f, primary, 0.13f);
                for (int index = 0; index < 8; index++)
                    TriggerImpactAt(32 + index * 8, previousTimer, currentTimer,
                        direction.RotatedBy(index * MathHelper.PiOver4), 3.4f + index * 0.22f,
                        index % 2 == 0 ? primary : secondary, 0.11f);
                TriggerImpactAt(104, previousTimer, currentTimer, direction, 11f, Color.White, 0.66f);
                break;
            case ReaperFormId.Void:
                TriggerImpactAt(32, previousTimer, currentTimer, direction, 11f, Color.White, 0.66f);
                break;
            case ReaperFormId.Death:
                for (int index = 0; index < ReaperDeathUltimateGeometry.CutCount; index++)
                    TriggerImpactAt(ReaperDeathUltimateGeometry.GetCutTick(index),
                        previousTimer, currentTimer,
                        direction.RotatedBy(index * 2.39996f), 4.6f + index * 0.08f,
                        new Color(225, 25, 66), 0.14f);
                TriggerImpactAt(ReaperDeathUltimateGeometry.ShatterTick,
                    previousTimer, currentTimer, direction,
                    12f, new Color(190, 14, 48), 0.14f);
                break;
        }
    }

    private static void TriggerImpactAt(
        int eventTick,
        int previousTimer,
        int currentTimer,
        Vector2 direction,
        float strength,
        Color color,
        float opacity)
    {
        if (previousTimer >= eventTick || currentTimer < eventTick)
            return;
        int frames = strength >= 10f ? 14 : strength >= 7f ? 10 : 7;
        ReaperVfxDirector.TriggerGlobalImpact(direction, strength, frames, color, opacity,
            strength >= 10f ? 7 : 4, strength >= 10f ? 0.50f : 0.24f);
    }

    private static void DrawCinematicFrame(
        SpriteBatch batch,
        Texture2D pixel,
        Vector2 viewport,
        float progress,
        float opacity,
        Color primary,
        Color secondary)
    {
        float ritual = Envelope(progress, 0f, 0.07f, 0.94f, 1f) * opacity;
        int barHeight = Math.Max(8, (int)Math.Round(viewport.Y * 0.035f));
        batch.Draw(pixel, new Rectangle(0, 0, (int)Math.Ceiling(viewport.X), barHeight),
            Color.Black * (ritual * 0.76f));
        batch.Draw(pixel, new Rectangle(0, Math.Max(0, (int)Math.Floor(viewport.Y) - barHeight),
            (int)Math.Ceiling(viewport.X), barHeight), Color.Black * (ritual * 0.76f));

        // Deliberately omit ornamental corner brackets. The restrained full-width
        // letterbox reads as one cinematic composition and cannot resemble a
        // misplaced child viewport.

        // Final impacts are expressed by world geometry, sound and camera
        // response. A full-viewport white heartbeat obscured those details and
        // became a one-frame white screen when several attacks overlapped.
    }

    private static void DrawBone(
        SpriteBatch batch,
        Texture2D pixel,
        Vector2 focus,
        Vector2 viewport,
        float progress,
        float opacity,
        float time,
        Color primary,
        Color secondary)
    {
        float graveWidth = MathHelper.Clamp(viewport.X / 11f, 48f, 76f);
        for (int index = 0; index < 8; index++)
        {
            float reveal = Reveal(progress, 0.05f + index * 0.052f, 0.18f + index * 0.052f) * opacity;
            if (reveal <= 0.001f)
                continue;

            float horizontal = (index - 3.5f) * graveWidth;
            float restingY = focus.Y + 112f + Math.Abs(index - 3.5f) * 8f;
            Vector2 position = new(focus.X + horizontal, restingY + (1f - Ease(reveal / Math.Max(opacity, 0.001f))) * 54f);
            float pulse = 0.82f + (float)Math.Sin(time * 2.7f + index * 1.3f) * 0.12f;
            DrawTombstone(batch, pixel, position, 36f, 56f,
                new Color(7, 20, 27) * (0.90f * reveal),
                Color.Lerp(primary, secondary, 0.38f) * (pulse * reveal));
        }

        float frameReveal = Reveal(progress, 0.58f, 0.78f) * opacity;
        if (frameReveal > 0.001f)
        {
            float halfWidth = MathHelper.Clamp(viewport.X * 0.16f, 150f, 260f);
            float height = MathHelper.Clamp(viewport.Y * 0.46f, 260f, 430f);
            Vector2 topLeft = focus + new Vector2(-halfWidth, -height * 0.63f);
            Vector2 topRight = focus + new Vector2(halfWidth, -height * 0.63f);
            Vector2 bottomLeft = topLeft + Vector2.UnitY * height;
            Vector2 bottomRight = topRight + Vector2.UnitY * height;
            DrawLayeredLine(batch, pixel, topLeft, bottomLeft, primary, secondary, 10f, 2.4f, frameReveal * 0.62f);
            DrawLayeredLine(batch, pixel, topRight, bottomRight, primary, secondary, 10f, 2.4f, frameReveal * 0.62f);
            DrawLayeredLine(batch, pixel, topLeft, topRight, primary, secondary, 12f, 2.8f, frameReveal * 0.78f);

            float bladeReveal = Reveal(progress, 0.75f, 0.94f);
            Vector2 bladeStart = topRight + new Vector2(-20f, 30f);
            Vector2 bladeEnd = bottomLeft + new Vector2(38f, -24f);
            DrawJaggedLine(batch, pixel, bladeStart, bladeEnd, bladeReveal,
                primary, Color.White, 22f, 5.2f, 3.5f, 7.8f, opacity);
            Vector2 parallel = (bladeEnd - bladeStart).SafeNormalize(Vector2.UnitY).RotatedBy(MathHelper.PiOver2) * 22f;
            DrawJaggedLine(batch, pixel, bladeStart + parallel, bladeEnd + parallel, bladeReveal,
                new Color(25, 100, 120), secondary, 12f, 2.5f, 2.4f, 11.2f, opacity * 0.78f);

            float impact = Pulse(progress, 0.965f, 0.07f) * opacity;
            DrawGlow(batch, bladeEnd, 125f, secondary * (0.30f * impact));
            DrawRing(batch, pixel, bladeEnd, MathHelper.Lerp(18f, 128f, Ease(impact)), 36,
                secondary * (impact * 0.9f), 3f, 0f, 1f);
        }

        float burial = Envelope(progress, 0.18f, 0.30f, 0.72f, 0.88f) * opacity;
        if (burial > 0.001f)
        {
            float cageRadius = MathHelper.Clamp(Math.Min(viewport.X, viewport.Y) * 0.21f, 130f, 235f);
            for (int side = -1; side <= 1; side += 2)
            {
                for (int rib = 0; rib < 6; rib++)
                {
                    float y = MathHelper.Lerp(-cageRadius * 0.68f, cageRadius * 0.65f, rib / 5f);
                    float width = cageRadius * (0.92f - Math.Abs(rib - 2.5f) * 0.09f);
                    Vector2 spine = focus + new Vector2(side * cageRadius * 0.88f, y);
                    Vector2 sternum = focus + new Vector2(side * width * 0.12f, y + 13f);
                    Vector2 elbow = Vector2.Lerp(spine, sternum, 0.52f) + new Vector2(side * 24f, -20f);
                    DrawLayeredLine(batch, pixel, spine, elbow, new Color(20, 95, 112), secondary,
                        9f, 2.3f, burial * 0.48f);
                    DrawLayeredLine(batch, pixel, elbow, sternum, primary, Color.White,
                        7f, 1.8f, burial * 0.58f);
                }
            }
        }

        float judge = Envelope(progress, 0.70f, 0.80f, 0.975f, 1f) * opacity;
        if (judge > 0.001f)
        {
            Vector2 judgeCenter = focus - Vector2.UnitY * MathHelper.Clamp(viewport.Y * 0.21f, 120f, 210f);
            float skullRadius = MathHelper.Clamp(viewport.Y * 0.095f, 52f, 94f);
            DrawGlow(batch, judgeCenter, skullRadius * 2.4f, primary * (judge * 0.16f));
            DrawPolygon(batch, pixel, judgeCenter, skullRadius, 8, new Color(8, 25, 30) * (judge * 0.94f),
                skullRadius * 0.75f, -MathHelper.PiOver2, 1f);
            DrawRing(batch, pixel, judgeCenter, skullRadius, 30, secondary * (judge * 0.84f), 4f, 0f, 1f);
            Vector2 eyeOffset = new(skullRadius * 0.33f, -skullRadius * 0.08f);
            DrawGlow(batch, judgeCenter - eyeOffset, skullRadius * 0.34f, primary * (judge * 0.72f));
            DrawGlow(batch, judgeCenter + new Vector2(eyeOffset.X, eyeOffset.Y), skullRadius * 0.34f,
                primary * (judge * 0.72f));
            for (int tooth = -3; tooth <= 3; tooth++)
            {
                Vector2 root = judgeCenter + new Vector2(tooth * skullRadius * 0.16f, skullRadius * 0.62f);
                DrawLayeredLine(batch, pixel, root, root + Vector2.UnitY * skullRadius * 0.34f,
                    primary, Color.White, 5f, 1.3f, judge * 0.72f);
            }
        }
    }

    private static void DrawBlood(
        SpriteBatch batch,
        Texture2D pixel,
        Vector2 focus,
        Vector2 viewport,
        float progress,
        float opacity,
        float time,
        Color primary,
        Color secondary)
    {
        // The shared Blood blade projectiles draw their own exact flight paths and
        // persistent wounds. This layer supplies only restrained atmosphere so it
        // cannot invent a second set of fake, screen-space projectiles or scars.
        float opening = Reveal(progress, 0.01f, 0.10f) * opacity;
        float pulse = 0.82f + (float)Math.Sin(time * 3.4f) * 0.10f;
        DrawGlow(batch, focus, MathHelper.Clamp(Math.Min(viewport.X, viewport.Y) * 0.31f,
            170f, 340f), new Color(122, 0, 24) * (opening * pulse * 0.28f));
        float closing = Envelope(progress, 0.76f, 0.84f, 0.97f, 1f) * opacity;
        DrawGlow(batch, focus, 240f,
            new Color(238, 22, 52) * (closing * 0.24f));
    }

    private static void DrawBloodLegacy(
        SpriteBatch batch,
        Texture2D pixel,
        Vector2 focus,
        Vector2 viewport,
        float progress,
        float opacity,
        float time,
        Color primary,
        Color secondary)
    {
        float opening = Reveal(progress, 0.01f, 0.10f) * opacity;
        DrawGlow(batch, focus, MathHelper.Clamp(Math.Min(viewport.X, viewport.Y) * 0.28f,
            150f, 310f), new Color(122, 0, 24) * (opening * 0.26f));

        // Six projectiles are rendered in the world. The overlay only reinforces
        // their curved approach paths and preserves the wounds they cut, keeping
        // the composition readable instead of replacing them with six crescents.
        float flightFade = 1f - MathHelper.SmoothStep(0f, 1f,
            MathHelper.Clamp((progress - 0.72f) / 0.12f, 0f, 1f));
        float orbit = MathHelper.Clamp(Math.Min(viewport.X, viewport.Y) * 0.31f, 190f, 360f);
        for (int index = 0; index < 6; index++)
        {
            float phase = progress * 5.8f + index / 6f;
            float angle = index * MathHelper.TwoPi / 6f + phase * MathHelper.TwoPi;
            Vector2 tangent = angle.ToRotationVector2();
            Vector2 head = focus + tangent * orbit * (0.72f + (float)Math.Sin(phase * 4f) * 0.12f);
            Vector2 tail = focus + (angle - 0.54f).ToRotationVector2() * orbit * 0.92f;
            DrawLayeredLine(batch, pixel, tail, head, new Color(238, 20, 54),
                new Color(8, 0, 3), 20f, 8f, opening * flightFade * 0.68f);
            DrawTaperedCrescent(batch, pixel, head, 34f, angle - 2.5f, 4.0f,
                opening * flightFade * 0.74f, new Color(58, 0, 14),
                new Color(225, 18, 50), new Color(255, 214, 205), 9f);

            float scarReveal = Reveal(progress, 0.16f + index * 0.035f,
                0.28f + index * 0.035f) * opacity;
            float scarAngle = -0.78f + index * 0.31f;
            Vector2 scarAxis = scarAngle.ToRotationVector2();
            Vector2 scarCenter = focus + (index - 2.5f) * 20f * Vector2.UnitY;
            DrawLayeredLine(batch, pixel, scarCenter - scarAxis * orbit * 0.58f,
                scarCenter + scarAxis * orbit * 0.58f,
                new Color(245, 18, 52), new Color(5, 0, 2),
                15f, 8f, scarReveal * flightFade * 0.56f);
        }

        float finalSlash = Envelope(progress, 0.76f, 0.82f, 0.96f, 1f) * opacity;
        if (finalSlash > 0.001f)
        {
            Vector2 direction = new Vector2(1f, -0.20f).SafeNormalize(Vector2.UnitX);
            float reach = (float)Math.Sqrt(viewport.X * viewport.X + viewport.Y * viewport.Y) * 0.72f;
            Vector2 start = focus - direction * reach;
            Vector2 end = focus + direction * reach;
            DrawLayeredLine(batch, pixel, start, end,
                new Color(255, 24, 58), new Color(4, 0, 2),
                MathHelper.Lerp(18f, 82f, finalSlash),
                MathHelper.Lerp(8f, 49f, finalSlash), finalSlash);
            DrawGlow(batch, focus, 220f,
                new Color(238, 22, 52) * (finalSlash * 0.22f));
        }
    }

    private static void DrawInfernal(
        SpriteBatch batch,
        Texture2D pixel,
        Vector2 focus,
        Vector2 viewport,
        float progress,
        float opacity,
        float time,
        Color primary,
        Color secondary)
    {
        float radius = MathHelper.Clamp(Math.Min(viewport.X, viewport.Y) * 0.23f, 145f, 265f);
        float rotation = -MathHelper.PiOver2 + time * 0.055f;
        float circleReveal = Reveal(progress, 0.015f, 0.22f);
        DrawRing(batch, pixel, focus, radius * 1.08f, 64, primary * (0.82f * opacity), 4f,
            rotation, circleReveal);
        DrawRing(batch, pixel, focus, radius * 0.94f, 54, secondary * (0.48f * opacity), 1.8f,
            -rotation * 0.7f, circleReveal);

        Span<Vector2> vertices = stackalloc Vector2[5];
        for (int index = 0; index < vertices.Length; index++)
            vertices[index] = focus + (rotation + index * MathHelper.TwoPi / 5f).ToRotationVector2() * radius;

        for (int edge = 0; edge < 5; edge++)
        {
            int startIndex = edge * 2 % 5;
            int endIndex = (edge + 1) * 2 % 5;
            float reveal = Reveal(progress, 0.08f + edge * 0.085f, 0.24f + edge * 0.085f);
            DrawLayeredLine(batch, pixel, vertices[startIndex],
                Vector2.Lerp(vertices[startIndex], vertices[endIndex], reveal),
                new Color(185, 38, 5), secondary, 13f, 3f, opacity * reveal);
            if (reveal > 0.02f)
            {
                Vector2 flame = vertices[startIndex] + Vector2.UnitY * (10f + (float)Math.Sin(time * 7f + edge) * 5f);
                DrawGlow(batch, flame, 38f, primary * (0.25f * opacity * reveal));
                DrawRuneTriangle(batch, pixel, vertices[startIndex], 20f, rotation + edge, secondary,
                    opacity * reveal);
            }
        }

        float ignition = Reveal(progress, 0.62f, 0.82f) * opacity;
        for (int index = 0; index < 5; index++)
        {
            Vector2 direction = (vertices[index] - focus).SafeNormalize(Vector2.UnitY);
            Vector2 flameTip = vertices[index] + direction * MathHelper.Lerp(10f, 86f, ignition);
            DrawJaggedLine(batch, pixel, vertices[index], flameTip, ignition,
                primary, secondary, 9f, 2.2f, 5f, 130f + index * 13f, opacity);
        }

        float detonation = Pulse(progress, 0.94f, 0.10f) * opacity;
        DrawGlow(batch, focus, radius * 1.2f, secondary * (0.25f * detonation));
        DrawRing(batch, pixel, focus, MathHelper.Lerp(radius * 0.18f, radius * 1.35f, Ease(detonation)), 72,
            Color.White * (0.78f * detonation), 4.5f, rotation, 1f);

        float gate = Envelope(progress, 0.58f, 0.72f, 0.98f, 1f) * opacity;
        if (gate > 0.001f)
        {
            float gateWidth = MathHelper.Clamp(viewport.X * 0.24f, 210f, 420f);
            float gateHeight = MathHelper.Clamp(viewport.Y * 0.56f, 310f, 590f);
            Vector2 topLeft = focus + new Vector2(-gateWidth * 0.5f, -gateHeight * 0.62f);
            Vector2 topRight = focus + new Vector2(gateWidth * 0.5f, -gateHeight * 0.62f);
            Vector2 bottomLeft = topLeft + Vector2.UnitY * gateHeight;
            Vector2 bottomRight = topRight + Vector2.UnitY * gateHeight;
            DrawLayeredLine(batch, pixel, topLeft, bottomLeft, new Color(42, 8, 3), primary, 24f, 4f, gate * 0.74f);
            DrawLayeredLine(batch, pixel, topRight, bottomRight, new Color(42, 8, 3), primary, 24f, 4f, gate * 0.74f);
            DrawTaperedCrescent(batch, pixel, focus - Vector2.UnitY * gateHeight * 0.50f, gateWidth * 0.52f,
                MathHelper.Pi, MathHelper.Pi, gate * 0.72f, new Color(30, 4, 0),
                new Color(220, 52, 4), secondary, gateWidth * 0.13f);
            for (int ember = 0; ember < 9; ember++)
            {
                float along = ember / 8f;
                Vector2 emberPosition = Vector2.Lerp(bottomLeft, bottomRight, along)
                    - Vector2.UnitY * (22f + (float)Math.Sin(time * 7f + ember) * 18f);
                DrawGlow(batch, emberPosition, 28f + ember % 3 * 7f, primary * (gate * 0.24f));
            }
        }

        float execution = Envelope(progress, 0.84f, 0.91f, 0.99f, 1f) * opacity;
        if (execution > 0.001f)
        {
            DrawReaperSilhouette(batch, pixel, focus - Vector2.UnitY * radius * 0.50f,
                radius * 1.45f, new Color(18, 3, 0), primary, execution * 0.78f);
            DrawTaperedCrescent(batch, pixel, focus, radius * 1.78f, -2.7f, 4.55f, execution,
                new Color(58, 7, 0), new Color(255, 75, 5), Color.White, radius * 0.28f);
        }
    }

    private static void DrawFrost(
        SpriteBatch batch,
        Texture2D pixel,
        Vector2 focus,
        Vector2 viewport,
        Rectangle viewportBounds,
        float progress,
        float opacity,
        float time,
        Color primary,
        Color secondary)
    {
        // A cool gray veil mutes the world without changing Terraria's shared shader state.
        batch.Draw(pixel, viewportBounds, new Color(102, 128, 148) * (0.12f * opacity));

        float reach = Math.Max(viewport.X, viewport.Y) * 0.68f;
        for (int index = 0; index < 4; index++)
        {
            float reveal = Reveal(progress, 0.16f + index * 0.085f, 0.34f + index * 0.085f);
            Vector2 direction = (-MathHelper.PiOver2 + index * MathHelper.PiOver2).ToRotationVector2();
            Vector2 end = focus + direction * reach;
            DrawJaggedLine(batch, pixel, focus, end, reveal,
                new Color(38, 105, 155), secondary, 15f, 3.1f, 17f, 208f + index * 19f, opacity);

            for (int branch = 0; branch < 3; branch++)
            {
                float branchProgress = 0.29f + branch * 0.19f;
                if (reveal <= branchProgress)
                    continue;
                Vector2 branchOrigin = Vector2.Lerp(focus, end, branchProgress);
                float side = (branch + index) % 2 == 0 ? -1f : 1f;
                Vector2 branchDirection = direction.RotatedBy(side * (0.42f + branch * 0.12f));
                float branchReveal = MathHelper.Clamp((reveal - branchProgress) / 0.25f, 0f, 1f);
                DrawJaggedLine(batch, pixel, branchOrigin,
                    branchOrigin + branchDirection * (58f + branch * 24f), branchReveal,
                    primary, Color.White, 7f, 1.6f, 7f, 260f + index * 31f + branch * 7f, opacity * 0.82f);
            }
        }

        float crossReveal = Reveal(progress, 0.76f, 0.93f);
        float diagonalReach = Math.Max(viewport.X, viewport.Y) * 0.72f;
        for (int index = 0; index < 4; index++)
        {
            Vector2 direction = (MathHelper.PiOver4 + index * MathHelper.PiOver2).ToRotationVector2();
            DrawJaggedLine(batch, pixel, focus, focus + direction * diagonalReach, crossReveal,
                primary, Color.White, 24f, 5f, 8f, 330f + index * 17f, opacity);
        }

        DrawGlow(batch, focus, 180f + (float)Math.Sin(time * 3f) * 12f,
            secondary * (Pulse(progress, 0.90f, 0.13f) * 0.26f * opacity));

        float mirrors = Envelope(progress, 0.10f, 0.24f, 0.84f, 0.96f) * opacity;
        float mirrorOrbit = MathHelper.Clamp(Math.Min(viewport.X, viewport.Y) * 0.27f, 170f, 300f);
        for (int index = 0; index < 4; index++)
        {
            float angle = MathHelper.PiOver4 + index * MathHelper.PiOver2;
            Vector2 center = focus + angle.ToRotationVector2() * mirrorOrbit;
            DrawPolygon(batch, pixel, center, 54f, 6, new Color(35, 100, 150) * (mirrors * 0.52f),
                13f, angle, 1f);
            DrawPolygon(batch, pixel, center, 47f, 6, Color.White * (mirrors * 0.72f),
                2.2f, -angle, 1f);
            Vector2 reflection = (focus - center).SafeNormalize(Vector2.UnitY);
            DrawLayeredLine(batch, pixel, center - reflection.RotatedBy(MathHelper.PiOver2) * 25f,
                center + reflection.RotatedBy(MathHelper.PiOver2) * 25f,
                primary, Color.White, 8f, 1.5f, mirrors * 0.52f);
        }

        float shatteredScreen = Envelope(progress, 0.86f, 0.92f, 0.995f, 1f) * opacity;
        if (shatteredScreen > 0.001f)
        {
            Vector2[] corners =
            [
                new Vector2(0f, 0f),
                new Vector2(viewport.X, 0f),
                new Vector2(viewport.X, viewport.Y),
                new Vector2(0f, viewport.Y),
                new Vector2(viewport.X * 0.5f, 0f),
                new Vector2(viewport.X, viewport.Y * 0.5f),
                new Vector2(viewport.X * 0.5f, viewport.Y),
                new Vector2(0f, viewport.Y * 0.5f)
            ];
            for (int index = 0; index < corners.Length; index++)
            {
                DrawJaggedLine(batch, pixel, focus, corners[index], shatteredScreen,
                    new Color(84, 170, 220), Color.White, 10f, 2.1f, 16f,
                    710f + index * 23f, opacity);
            }
        }
    }

    private static void DrawSoul(
        SpriteBatch batch,
        Texture2D pixel,
        Vector2 focus,
        Vector2 viewport,
        float progress,
        float opacity,
        float time,
        Color primary,
        Color secondary)
    {
        float riverY = MathHelper.Clamp(focus.Y, viewport.Y * 0.26f, viewport.Y * 0.74f);
        float riverReveal = Reveal(progress, 0.02f, 0.24f) * opacity;
        float convergence = Reveal(progress, 0.66f, 0.93f);

        for (int lane = 0; lane < 3; lane++)
        {
            const int segments = 34;
            Vector2 previous = default;
            for (int index = 0; index <= segments; index++)
            {
                float along = index / (float)segments;
                float x = MathHelper.Lerp(-70f, viewport.X + 70f, along);
                float wave = (float)Math.Sin(along * MathHelper.TwoPi * 2.2f - time * (1.15f + lane * 0.12f) + lane * 1.8f);
                float y = riverY + (lane - 1) * 45f + wave * (22f + lane * 5f);
                float centerWeight = 1f - MathHelper.Clamp(Math.Abs(along - 0.5f) * 2f, 0f, 1f);
                y = MathHelper.Lerp(y, focus.Y, convergence * centerWeight * 0.88f);
                Vector2 current = new(x, y);
                if (index > 0)
                {
                    Color laneColor = lane == 1 ? secondary : Color.Lerp(primary, secondary, lane * 0.34f);
                    DrawLayeredLine(batch, pixel, previous, current, primary, laneColor,
                        10f - lane * 1.5f, 2.1f, riverReveal * (0.56f + lane * 0.12f));
                }
                previous = current;
            }
        }

        for (int index = 0; index < 12; index++)
        {
            float reveal = Reveal(progress, 0.07f + index * 0.022f, 0.19f + index * 0.022f) * opacity;
            if (reveal <= 0.001f)
                continue;

            float along = PositiveModulo(time * (0.065f + index % 3 * 0.006f) + index / 12f, 1f);
            int lane = index % 3;
            float x = MathHelper.Lerp(-45f, viewport.X + 45f, along);
            float y = riverY + (lane - 1) * 45f
                + (float)Math.Sin(along * MathHelper.TwoPi * 2.2f - time * 1.25f + lane * 1.8f) * 24f;
            Vector2 riverPosition = new(x, y);
            float orbitAngle = index * MathHelper.TwoPi / 12f + time * 0.75f;
            Vector2 gatheredPosition = focus + orbitAngle.ToRotationVector2()
                * MathHelper.Lerp(72f, 20f, Reveal(progress, 0.82f, 0.98f));
            Vector2 position = Vector2.Lerp(riverPosition, gatheredPosition, convergence);
            Color shadeColor = Color.Lerp(secondary, Color.White, index % 4 == 0 ? 0.34f : 0.08f);
            DrawSoulShade(batch, pixel, position, shadeColor, reveal,
                0.78f + (float)Math.Sin(time * 3f + index) * 0.10f, orbitAngle);
        }

        float burial = Reveal(progress, 0.82f, 0.98f) * opacity;
        DrawRing(batch, pixel, focus, MathHelper.Lerp(210f, 26f, Ease(burial)), 56,
            secondary * (0.92f * burial), 4f, -time * 0.22f, 1f);
        DrawGlow(batch, focus, 150f, primary * (0.30f * burial));

        float ferryman = Envelope(progress, 0.63f, 0.76f, 0.99f, 1f) * opacity;
        if (ferryman > 0.001f)
        {
            float scale = MathHelper.Clamp(Math.Min(viewport.X, viewport.Y) * 0.42f, 260f, 480f);
            DrawReaperSilhouette(batch, pixel, focus - Vector2.UnitY * scale * 0.18f,
                scale, new Color(12, 5, 35), primary, ferryman * 0.72f);
            DrawTaperedCrescent(batch, pixel, focus, scale * 0.92f, -2.75f, 4.72f, ferryman,
                new Color(25, 10, 72), primary, new Color(170, 255, 255), scale * 0.16f);
        }

        float soulBloom = Pulse(progress, 0.94f, 0.08f) * opacity;
        for (int petal = 0; petal < 8; petal++)
        {
            float angle = petal * MathHelper.PiOver4 + time * 0.08f;
            Vector2 direction = angle.ToRotationVector2();
            DrawLayeredLine(batch, pixel, focus + direction * 20f,
                focus + direction * MathHelper.Lerp(28f, 190f, soulBloom),
                primary, secondary, 11f, 2.2f, soulBloom * 0.58f);
        }
    }

    private static void DrawVoid(
        SpriteBatch batch,
        Texture2D pixel,
        Vector2 focus,
        Vector2 viewport,
        Rectangle viewportBounds,
        float progress,
        float opacity,
        float time,
        Color primary,
        Color secondary,
        Vector2 aim,
        int actionId)
    {
        // The combat projectile owns the one and only world-space rift. Keeping
        // this UI layer to atmosphere prevents a second crack from following the
        // camera while the authoritative tear stays in the world.
        batch.Draw(pixel, viewportBounds, new Color(1, 0, 5) * (0.31f * opacity));
        DrawGlow(batch, focus, 210f,
            secondary * (Reveal(progress, 0.48f, 1f) * 0.24f * opacity));
    }

    private static void DrawDeath(
        SpriteBatch batch,
        Texture2D pixel,
        int ownerIndex,
        in UltimateVisualState state,
        Vector2 focus,
        Vector2 viewport,
        Rectangle viewportBounds,
        float progress,
        float opacity,
        float time)
    {
        float shatterStart = ReaperDeathUltimateGeometry.ShatterTick
            / (float)ReaperDeathUltimateGeometry.Duration;
        float isolation = Envelope(progress, 0.58f, 0.79f,
            shatterStart - 0.015f, shatterStart + 0.012f) * opacity;
        Texture2D? capturedWorld = Main.screenTarget;

        // Each successful cut removes more of the ordinary scene. Immediately
        // before the shatter, only small windows around the caster and recorded
        // victims are restored from Terraria's world render target.
        if (isolation > 0.001f)
        {
            batch.Draw(pixel, viewportBounds,
                new Color(0, 0, 1) * (isolation * 0.965f));
            if (capturedWorld is not null && !capturedWorld.IsDisposed)
                DrawDeathUltimateActors(batch, capturedWorld, ownerIndex,
                    state.ActionId, viewport, isolation);
        }

        float shatter = Reveal(progress, shatterStart, 0.985f) * opacity;
        if (shatter > 0.001f)
        {
            // The actual Death Necklace plates fill the void behind the broken
            // game image; no substitute gradient or black fill is used here.
            DeathDomainBackdropTextureSystem.DrawScreenAlignedBackdrop(batch,
                viewportBounds, shatter);
            if (capturedWorld is not null && !capturedWorld.IsDisposed)
                DrawDeathMirrorShards(batch, capturedWorld, focus, viewport,
                    state.ActionId, shatter, opacity);
        }

        DrawDeathUltimateCutCracks(batch, pixel, in state, focus, viewport,
            shatter, opacity);
        DrawGlow(batch, focus, 230f,
            new Color(125, 0, 34) * ((0.12f + shatter * 0.18f) * opacity));
        _ = time;
    }

    private static void DrawDeathUltimateActors(SpriteBatch batch,
        Texture2D capturedWorld, int ownerIndex, int actionId,
        Vector2 viewport, float opacity)
    {
        if (ownerIndex < 0 || ownerIndex >= Main.maxPlayers)
            return;
        Player owner = Main.player[ownerIndex];
        if (owner.active && !owner.dead)
            DrawCapturedActorWindow(batch, capturedWorld, owner.getRect(),
                viewport, opacity, 34f);

        HashSet<int> drawnRoots = [];
        foreach (NPC npc in Main.ActiveNPCs)
        {
            int root = npc.realLife >= 0 ? npc.realLife : npc.whoAmI;
            if (root < 0 || root >= Main.maxNPCs || !drawnRoots.Add(root))
                continue;
            NPC target = Main.npc[root];
            if (!target.active
                || !target.GetGlobalNPC<ReaperCombatGlobalNPC>()
                    .IsTrackedByDeathUltimate(ownerIndex, actionId))
            {
                continue;
            }
            DrawCapturedActorWindow(batch, capturedWorld, target.Hitbox,
                viewport, opacity, 28f);
        }
    }

    private static void DrawCapturedActorWindow(SpriteBatch batch,
        Texture2D capturedWorld, Rectangle worldBounds, Vector2 viewport,
        float opacity, float padding)
    {
        Rectangle screenBounds = new(
            (int)Math.Floor(worldBounds.X - Main.screenPosition.X - padding),
            (int)Math.Floor(worldBounds.Y - Main.screenPosition.Y - padding),
            Math.Max(1, (int)Math.Ceiling(worldBounds.Width + padding * 2f)),
            Math.Max(1, (int)Math.Ceiling(worldBounds.Height + padding * 2f)));
        Rectangle clipped = Rectangle.Intersect(screenBounds,
            new Rectangle(0, 0, (int)viewport.X, (int)viewport.Y));
        if (clipped.Width <= 0 || clipped.Height <= 0)
            return;

        const int bands = 12;
        for (int band = 0; band < bands; band++)
        {
            float y0 = band / (float)bands;
            float y1 = (band + 1f) / bands;
            float normalizedY = (y0 + y1) - 1f;
            float halfWidth = (float)Math.Sqrt(Math.Max(0f,
                1f - normalizedY * normalizedY));
            int bandTop = screenBounds.Top
                + (int)Math.Floor(screenBounds.Height * y0);
            int bandBottom = screenBounds.Top
                + (int)Math.Ceiling(screenBounds.Height * y1);
            int bandHalfWidth = Math.Max(1,
                (int)Math.Ceiling(screenBounds.Width * 0.5f * halfWidth));
            Rectangle destination = new(
                screenBounds.Center.X - bandHalfWidth,
                bandTop,
                bandHalfWidth * 2,
                Math.Max(1, bandBottom - bandTop));
            destination = Rectangle.Intersect(destination, clipped);
            if (destination.Width <= 0 || destination.Height <= 0)
                continue;

            Rectangle source = new(
                (int)Math.Floor(destination.X / viewport.X * capturedWorld.Width),
                (int)Math.Floor(destination.Y / viewport.Y * capturedWorld.Height),
                Math.Max(1, (int)Math.Ceiling(destination.Width / viewport.X
                    * capturedWorld.Width)),
                Math.Max(1, (int)Math.Ceiling(destination.Height / viewport.Y
                    * capturedWorld.Height)));
            source = Rectangle.Intersect(source, capturedWorld.Bounds);
            if (source.Width > 0 && source.Height > 0)
                batch.Draw(capturedWorld, destination, source,
                    Color.White * MathHelper.Clamp(opacity, 0f, 1f));
        }
    }

    private static void DrawDeathMirrorShards(SpriteBatch batch,
        Texture2D capturedWorld, Vector2 focus, Vector2 viewport,
        int actionId, float shatter, float opacity)
    {
        if (Main.graphics?.GraphicsDevice is not GraphicsDevice graphicsDevice)
            return;

        int rayCount = deathMirrorAngles.Length;
        for (int cut = 0; cut < ReaperDeathUltimateGeometry.CutCount; cut++)
        {
            float angle = ReaperDeathUltimateGeometry.GetCutAxis(actionId, cut)
                .ToRotation();
            if (angle < 0f)
                angle += MathHelper.TwoPi;
            deathMirrorAngles[cut * 2] = angle;
            deathMirrorAngles[cut * 2 + 1] = PositiveModulo(
                angle + MathHelper.Pi, MathHelper.TwoPi);
        }
        Array.Sort(deathMirrorAngles);

        float eased = Ease(shatter);
        float radius = GetViewportCoverRadius(focus, viewport) * 1.08f;
        Color shardColor = Color.White
            * (opacity * MathHelper.Lerp(1f, 0.48f, eased));
        int vertex = 0;
        for (int shard = 0; shard < rayCount; shard++)
        {
            float startAngle = deathMirrorAngles[shard];
            float endAngle = shard == rayCount - 1
                ? deathMirrorAngles[0] + MathHelper.TwoPi
                : deathMirrorAngles[shard + 1];
            float middleAngle = (startAngle + endAngle) * 0.5f;
            uint hash = unchecked((uint)(actionId * 16777619
                + shard * 2246822519));
            hash ^= hash >> 15;
            float random = (hash & 0xFFFFu) / 65535f;
            float signed = ((hash >> 16) & 1u) == 0u ? -1f : 1f;
            Vector2 displacement = middleAngle.ToRotationVector2()
                * ((18f + random * 48f) * eased)
                + Vector2.UnitY * (eased * eased * (8f + random * 34f));
            float rotation = signed * (0.012f + random * 0.032f) * eased;

            Vector2 originalCenter = focus;
            Vector2 originalStart = focus
                + startAngle.ToRotationVector2() * radius;
            Vector2 originalEnd = focus
                + endAngle.ToRotationVector2() * radius;
            WriteMirrorVertex(ref vertex,
                RotatePoint(originalCenter, focus, rotation) + displacement,
                originalCenter, viewport, shardColor);
            WriteMirrorVertex(ref vertex,
                RotatePoint(originalStart, focus, rotation) + displacement,
                originalStart, viewport, shardColor);
            WriteMirrorVertex(ref vertex,
                RotatePoint(originalEnd, focus, rotation) + displacement,
                originalEnd, viewport, shardColor);
        }

        batch.End();
        try
        {
            deathMirrorEffect ??= new BasicEffect(graphicsDevice)
            {
                TextureEnabled = true,
                VertexColorEnabled = true,
                LightingEnabled = false,
                FogEnabled = false
            };
            deathMirrorEffect.World = Matrix.Identity;
            deathMirrorEffect.View = Matrix.Identity;
            deathMirrorEffect.Projection = Matrix.CreateOrthographicOffCenter(
                0f, viewport.X, viewport.Y, 0f, 0f, 1f);
            deathMirrorEffect.Texture = capturedWorld;
            graphicsDevice.BlendState = BlendState.AlphaBlend;
            graphicsDevice.DepthStencilState = DepthStencilState.None;
            graphicsDevice.RasterizerState = RasterizerState.CullNone;
            graphicsDevice.SamplerStates[0] = SamplerState.LinearClamp;
            foreach (EffectPass pass in deathMirrorEffect.CurrentTechnique.Passes)
            {
                pass.Apply();
                graphicsDevice.DrawUserPrimitives(PrimitiveType.TriangleList,
                    deathMirrorVertices, 0, vertex / 3);
            }
        }
        finally
        {
            batch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend,
                SamplerState.LinearClamp, DepthStencilState.None,
                RasterizerState.CullNone, null, Matrix.Identity);
        }
    }

    private static void WriteMirrorVertex(ref int index, Vector2 position,
        Vector2 samplePosition, Vector2 viewport, Color color)
    {
        Vector2 uv = new(
            samplePosition.X / Math.Max(1f, viewport.X),
            samplePosition.Y / Math.Max(1f, viewport.Y));
        deathMirrorVertices[index++] = new VertexPositionColorTexture(
            new Vector3(position, 0f), color, uv);
    }

    private static Vector2 RotatePoint(Vector2 point, Vector2 origin,
        float rotation)
        => origin + (point - origin).RotatedBy(rotation);

    private static float GetViewportCoverRadius(Vector2 focus, Vector2 viewport)
    {
        float radius = focus.Length();
        radius = Math.Max(radius, Vector2.Distance(focus,
            new Vector2(viewport.X, 0f)));
        radius = Math.Max(radius, Vector2.Distance(focus,
            new Vector2(0f, viewport.Y)));
        radius = Math.Max(radius, Vector2.Distance(focus, viewport));
        return Math.Max(1f, radius);
    }

    private static void DrawDeathUltimateCutCracks(SpriteBatch batch,
        Texture2D pixel, in UltimateVisualState state, Vector2 focus,
        Vector2 viewport, float shatter, float opacity)
    {
        float radius = GetViewportCoverRadius(focus, viewport) * 1.04f;
        for (int cut = 0; cut < ReaperDeathUltimateGeometry.CutCount; cut++)
        {
            int cutTick = ReaperDeathUltimateGeometry.GetCutTick(cut);
            if (state.Timer < cutTick)
                continue;
            float age = state.Timer - cutTick;
            float fresh = (float)Math.Exp(-age / 13f);
            float strength = (0.12f + fresh * 0.38f + shatter * 0.74f)
                * opacity;
            Vector2 axis = ReaperDeathUltimateGeometry.GetCutAxis(
                state.ActionId, cut);
            Vector2 start = focus - axis * radius;
            Vector2 end = focus + axis * radius;
            DrawLayeredLine(batch, pixel, start, end,
                new Color(32, 0, 15),
                Color.Lerp(new Color(235, 12, 62), Color.White,
                    shatter * 0.48f),
                4f + shatter * 3f, 0.9f + shatter * 0.8f,
                strength);
        }
    }

    private static void DrawDeathLegacy(
        SpriteBatch batch,
        Texture2D pixel,
        Vector2 focus,
        Vector2 viewport,
        float progress,
        float opacity,
        float time)
    {
        ReadOnlySpan<ReaperFormId> forms =
        [
            ReaperFormId.Bone,
            ReaperFormId.Blood,
            ReaperFormId.Infernal,
            ReaperFormId.Frost,
            ReaperFormId.Soul,
            ReaperFormId.Void
        ];

        float converge = Reveal(progress, 0.69f, 0.92f);
        float initialOrbit = MathHelper.Clamp(Math.Min(viewport.X, viewport.Y) * 0.31f, 205f, 340f);
        float orbitRadius = MathHelper.Lerp(initialOrbit, 34f, Ease(converge));
        float rotation = -MathHelper.PiOver2 + time * MathHelper.Lerp(0.08f, 0.72f, converge);

        for (int index = 0; index < forms.Length; index++)
        {
            float reveal = Reveal(progress, 0.015f + index * 0.038f, 0.105f + index * 0.038f) * opacity;
            if (reveal <= 0.001f)
                continue;

            Color color = ReaperCombatRegistry.GetPrimaryColor(forms[index]);
            Color core = ReaperCombatRegistry.GetSecondaryColor(forms[index]);
            float angle = rotation + index * MathHelper.TwoPi / forms.Length;
            Vector2 originalPosition = focus + angle.ToRotationVector2() * initialOrbit;
            Vector2 position = focus + angle.ToRotationVector2() * orbitRadius;

            if (converge > 0.001f)
                DrawLayeredLine(batch, pixel, originalPosition, position, color, core, 9f, 2f,
                    converge * reveal * 0.58f);
            DrawLayeredLine(batch, pixel, position, focus, color, core, 5f, 1.2f, reveal * 0.30f);
            DrawGlow(batch, position, 54f, color * (0.24f * reveal));
            float phaseCenter = 36f / 170f + index * 18f / 170f;
            float activePulse = 0.74f + Pulse(progress, phaseCenter, 0.055f) * 0.36f;
            DrawDeathGate(batch, pixel, position, index, angle, reveal * activePulse,
                converge, time, color, core);
        }

        // Six executions use their branch's actual silhouette, not six recolored
        // copies of one crescent. Their centers match the authoritative hit ticks.
        for (int index = 0; index < forms.Length; index++)
        {
            float center = 36f / 170f + index * 18f / 170f;
            float phase = Envelope(progress, center - 0.055f, center - 0.012f,
                center + 0.052f, center + 0.105f) * opacity;
            if (phase > 0.001f)
                DrawDeathPhaseMotif(batch, pixel, focus, viewport, index, phase, time);
        }

        float unity = Reveal(progress, 0.72f, 0.96f) * opacity;
        if (unity > 0.001f)
        {
            for (int index = 0; index < forms.Length; index++)
            {
                Color color = ReaperCombatRegistry.GetPrimaryColor(forms[index]);
                Vector2 direction = (rotation + index * MathHelper.TwoPi / forms.Length).ToRotationVector2();
                DrawLayeredLine(batch, pixel, focus, focus + direction * MathHelper.Lerp(18f, 115f, 1f - converge),
                    color, Color.White, 12f, 2.1f, unity * 0.72f);
            }
            DrawRing(batch, pixel, focus, MathHelper.Lerp(152f, 24f, Ease(unity)), 64,
                Color.White * (0.88f * unity), 5f, rotation, 1f);
            DrawRing(batch, pixel, focus, MathHelper.Lerp(104f, 15f, Ease(unity)), 54,
                new Color(255, 58, 105) * (0.72f * unity), 2.5f, -rotation, 1f);
            DrawGlow(batch, focus, 210f, Color.White * (0.20f * unity));
        }

        float finalCut = Reveal(progress, 0.86f, 0.985f);
        float reach = Math.Max(viewport.X, viewport.Y) * 0.80f;
        Vector2 cutDirection = new Vector2(1f, -0.16f).SafeNormalize(Vector2.UnitX);
        DrawJaggedLine(batch, pixel, focus - cutDirection * reach, focus + cutDirection * reach,
            finalCut, new Color(225, 30, 75), Color.White, 34f, 6f, 5f, 667f, opacity);
        float requiem = Envelope(progress, 0.82f, 0.90f, 0.99f, 1f) * opacity;
        if (requiem > 0.001f)
        {
            float scale = MathHelper.Clamp(Math.Min(viewport.X, viewport.Y) * 0.58f, 370f, 690f);
            DrawReaperSilhouette(batch, pixel, focus - Vector2.UnitY * scale * 0.23f,
                scale, new Color(3, 0, 4), new Color(210, 18, 54), requiem * 0.88f);
            for (int band = 0; band < forms.Length; band++)
            {
                Color color = ReaperCombatRegistry.GetPrimaryColor(forms[band]);
                DrawTaperedCrescent(batch, pixel, focus, scale * (0.94f - band * 0.032f),
                    -2.74f + band * 0.018f, 4.62f, requiem * (0.20f + band * 0.035f),
                    new Color(8, 0, 10), Color.Lerp(new Color(120, 0, 28), color, 0.42f),
                    color, scale * (0.16f - band * 0.012f));
            }
            DrawTaperedCrescent(batch, pixel, focus, scale, -2.76f, 4.64f, requiem,
                new Color(8, 0, 10), new Color(224, 18, 56), Color.White, scale * 0.10f);
        }
    }

    private static void DrawDeathGate(
        SpriteBatch batch,
        Texture2D pixel,
        Vector2 center,
        int gate,
        float rotation,
        float opacity,
        float converge,
        float time,
        Color color,
        Color core)
    {
        float size = MathHelper.Lerp(42f, 23f, converge);
        switch (gate)
        {
            case 0: // Ribbed bone guillotine gate.
            {
                Vector2 topLeft = center + new Vector2(-size * 0.68f, -size);
                Vector2 topRight = center + new Vector2(size * 0.68f, -size);
                Vector2 bottomLeft = center + new Vector2(-size * 0.68f, size);
                Vector2 bottomRight = center + new Vector2(size * 0.68f, size);
                DrawLayeredLine(batch, pixel, topLeft, bottomLeft, new Color(6, 28, 35), color,
                    8f, 2f, opacity);
                DrawLayeredLine(batch, pixel, topRight, bottomRight, new Color(6, 28, 35), color,
                    8f, 2f, opacity);
                DrawJaggedLine(batch, pixel, topLeft, topRight, 1f, color, core,
                    9f, 2.2f, 2f, 90f, opacity);
                for (int rib = 0; rib < 3; rib++)
                {
                    float y = MathHelper.Lerp(-size * 0.52f, size * 0.58f, rib / 2f);
                    DrawLayeredLine(batch, pixel, center + new Vector2(-size * 0.66f, y),
                        center + new Vector2(-size * 0.16f, y + size * 0.13f), color, core,
                        4f, 1f, opacity * 0.78f);
                    DrawLayeredLine(batch, pixel, center + new Vector2(size * 0.66f, y),
                        center + new Vector2(size * 0.16f, y + size * 0.13f), color, core,
                        4f, 1f, opacity * 0.78f);
                }
                break;
            }
            case 1: // Blood moon gate and white-rimmed crescent.
                DrawGlow(batch, center, size * 1.85f, new Color(126, 0, 24) * (opacity * 0.42f));
                DrawRing(batch, pixel, center, size * 0.86f, 32, color * (opacity * 0.82f),
                    size * 0.18f, rotation, 1f);
                DrawTaperedCrescent(batch, pixel, center, size * 0.92f, -2.55f, 4.24f,
                    opacity, new Color(55, 0, 16), color, Color.White, size * 0.18f);
                break;
            case 2: // Infernal five-point gate.
                DrawRing(batch, pixel, center, size, 36, new Color(52, 8, 0) * opacity,
                    7f, rotation, 1f);
                DrawPentagram(batch, pixel, center, size * 0.88f, rotation + time * 0.05f,
                    new Color(70, 7, 0), core, opacity);
                break;
            case 3: // Hexagonal frost mirror and X blades.
                DrawPolygon(batch, pixel, center, size, 6, new Color(54, 120, 170) * (opacity * 0.72f),
                    10f, rotation, 1f);
                DrawPolygon(batch, pixel, center, size * 0.86f, 6, Color.White * opacity,
                    2f, -rotation, 1f);
                DrawLayeredLine(batch, pixel, center + new Vector2(-size * 0.66f, -size * 0.66f),
                    center + new Vector2(size * 0.66f, size * 0.66f), color, core, 5f, 1.2f, opacity);
                DrawLayeredLine(batch, pixel, center + new Vector2(-size * 0.66f, size * 0.66f),
                    center + new Vector2(size * 0.66f, -size * 0.66f), color, Color.White, 5f, 1.2f, opacity);
                break;
            case 4: // Soft soul portal with a single ferryman shade.
                DrawRing(batch, pixel, center, size, 36, color * (opacity * 0.72f), 5f,
                    rotation + time * 0.1f, 1f);
                DrawRing(batch, pixel, center, size * 0.72f, 30, core * (opacity * 0.54f), 2f,
                    -rotation, 1f);
                DrawSoulShade(batch, pixel, center, core, opacity, size / 44f, time);
                break;
            default: // Offset coordinate door cut out of the scene.
            {
                Rectangle voidCore = new(
                    (int)Math.Round(center.X - size * 0.72f),
                    (int)Math.Round(center.Y - size),
                    Math.Max(1, (int)Math.Round(size * 1.44f)),
                    Math.Max(1, (int)Math.Round(size * 2f)));
                batch.Draw(pixel, voidCore, new Color(0, 0, 2) * (opacity * 0.94f));
                Vector2 topLeft = new(voidCore.Left, voidCore.Top);
                Vector2 topRight = new(voidCore.Right, voidCore.Top + size * 0.12f);
                Vector2 bottomLeft = new(voidCore.Left, voidCore.Bottom - size * 0.12f);
                Vector2 bottomRight = new(voidCore.Right, voidCore.Bottom);
                DrawLayeredLine(batch, pixel, topLeft, topRight, color, Color.White, 6f, 1.3f, opacity);
                DrawLayeredLine(batch, pixel, bottomLeft, bottomRight, color, core, 6f, 1.3f, opacity);
                DrawLayeredLine(batch, pixel, center - Vector2.UnitX * size * 0.90f,
                    center + Vector2.UnitX * size * 0.90f, Color.Black, core, 10f, 2f, opacity);
                break;
            }
        }

        DrawDeathRuneGlyph(batch, pixel, center, gate, size * 0.28f, color, core, opacity * 0.74f);
    }

    private static void DrawDeathPhaseMotif(
        SpriteBatch batch,
        Texture2D pixel,
        Vector2 focus,
        Vector2 viewport,
        int phase,
        float opacity,
        float time)
    {
        float shortSide = Math.Min(viewport.X, viewport.Y);
        float longSide = Math.Max(viewport.X, viewport.Y);
        switch (phase)
        {
            case 0: // Bone guillotine and rib frame.
            {
                Color bone = new(210, 244, 232);
                Color corpseFire = new(58, 214, 228);
                float halfWidth = MathHelper.Clamp(viewport.X * 0.17f, 145f, 270f);
                float height = MathHelper.Clamp(viewport.Y * 0.52f, 300f, 520f);
                Vector2 topLeft = focus + new Vector2(-halfWidth, -height * 0.56f);
                Vector2 topRight = focus + new Vector2(halfWidth, -height * 0.56f);
                Vector2 bottomLeft = topLeft + Vector2.UnitY * height;
                Vector2 bottomRight = topRight + Vector2.UnitY * height;
                DrawLayeredLine(batch, pixel, topLeft, bottomLeft, new Color(8, 28, 34), corpseFire,
                    18f, 3f, opacity);
                DrawLayeredLine(batch, pixel, topRight, bottomRight, new Color(8, 28, 34), corpseFire,
                    18f, 3f, opacity);
                DrawJaggedLine(batch, pixel, topLeft, topRight, 1f, corpseFire, bone,
                    20f, 4f, 5f, 180f, opacity);
                for (int rib = 0; rib < 5; rib++)
                {
                    float amount = (rib + 1f) / 6f;
                    Vector2 left = Vector2.Lerp(topLeft, bottomLeft, amount);
                    Vector2 right = Vector2.Lerp(topRight, bottomRight, amount);
                    DrawLayeredLine(batch, pixel, left, Vector2.Lerp(left, focus, 0.56f),
                        corpseFire, bone, 8f, 2f, opacity * 0.68f);
                    DrawLayeredLine(batch, pixel, right, Vector2.Lerp(right, focus, 0.56f),
                        corpseFire, bone, 8f, 2f, opacity * 0.68f);
                }
                DrawJaggedLine(batch, pixel, topRight + new Vector2(-20f, 30f),
                    bottomLeft + new Vector2(34f, -20f), 1f, new Color(22, 92, 110), bone,
                    28f, 5f, 4f, 219f, opacity);
                break;
            }
            case 1: // Thick reference-style blood crescent.
            {
                float radius = MathHelper.Clamp(shortSide * 0.54f, 330f, 610f);
                Vector2 center = focus + new Vector2(-radius * 0.13f, radius * 0.05f);
                DrawGlow(batch, focus - Vector2.UnitY * radius * 0.25f, radius * 0.62f,
                    new Color(115, 0, 22) * (opacity * 0.42f));
                DrawTaperedCrescent(batch, pixel, center, radius * 1.025f, -2.50f, 4.36f,
                    opacity * 0.24f, new Color(35, 0, 10), new Color(155, 5, 34),
                    new Color(255, 102, 112), radius * 0.22f);
                DrawTaperedCrescent(batch, pixel, center, radius, -2.46f, 4.36f,
                    opacity, new Color(70, 0, 18), new Color(238, 24, 58),
                    new Color(255, 244, 226), radius * 0.18f);
                DrawImpactStar(batch, pixel, focus + new Vector2(radius * 0.20f, radius * 0.43f),
                    radius * 0.09f, new Color(255, 145, 65), Color.White, opacity);
                break;
            }
            case 2: // Pentagram and black-shell molten tracks.
            {
                float radius = MathHelper.Clamp(shortSide * 0.30f, 190f, 350f);
                float rotation = -MathHelper.PiOver2 + time * 0.06f;
                DrawPentagram(batch, pixel, focus, radius, rotation, new Color(43, 4, 0),
                    new Color(255, 192, 46), opacity);
                for (int index = 0; index < 5; index++)
                {
                    Vector2 direction = (rotation + index * MathHelper.TwoPi / 5f).ToRotationVector2();
                    Vector2 vertex = focus + direction * radius;
                    DrawLayeredLine(batch, pixel, focus, vertex + direction * radius * 0.55f,
                        new Color(12, 3, 0), new Color(255, 92, 8), 24f, 5f, opacity);
                    DrawGlow(batch, vertex, 52f, new Color(255, 150, 18) * (opacity * 0.30f));
                }
                break;
            }
            case 3: // Four frost mirrors and a crystalline X.
            {
                float orbit = MathHelper.Clamp(shortSide * 0.24f, 155f, 285f);
                for (int index = 0; index < 4; index++)
                {
                    float angle = MathHelper.PiOver4 + index * MathHelper.PiOver2;
                    Vector2 mirror = focus + angle.ToRotationVector2() * orbit;
                    DrawPolygon(batch, pixel, mirror, 58f, 6,
                        new Color(42, 126, 184) * (opacity * 0.66f), 15f, angle, 1f);
                    DrawPolygon(batch, pixel, mirror, 50f, 6, Color.White * opacity,
                        2.4f, -angle, 1f);
                }
                Vector2 diagonalA = new Vector2(1f, 1f).SafeNormalize(Vector2.One);
                Vector2 diagonalB = new Vector2(1f, -1f).SafeNormalize(Vector2.UnitX);
                DrawLayeredLine(batch, pixel, focus - diagonalA * longSide * 0.52f,
                    focus + diagonalA * longSide * 0.52f, new Color(45, 130, 190),
                    Color.White, 26f, 4f, opacity);
                DrawLayeredLine(batch, pixel, focus - diagonalB * longSide * 0.52f,
                    focus + diagonalB * longSide * 0.52f, new Color(28, 91, 160),
                    new Color(190, 250, 255), 26f, 4f, opacity);
                break;
            }
            case 4: // Winding soul river and ferryman shades.
            {
                for (int lane = 0; lane < 3; lane++)
                {
                    Vector2 previous = default;
                    for (int segment = 0; segment <= 20; segment++)
                    {
                        float along = segment / 20f;
                        Vector2 current = new(
                            MathHelper.Lerp(-50f, viewport.X + 50f, along),
                            focus.Y + (lane - 1) * 46f
                                + (float)Math.Sin(along * MathHelper.TwoPi * 2f + lane * 1.7f - time) * 24f);
                        if (segment > 0)
                        {
                            DrawLayeredLine(batch, pixel, previous, current, new Color(31, 8, 72),
                                lane == 1 ? new Color(118, 240, 242) : new Color(120, 75, 225),
                                13f, 2.4f, opacity * 0.78f);
                        }
                        previous = current;
                    }
                }
                for (int index = 0; index < 7; index++)
                {
                    float angle = index * MathHelper.TwoPi / 7f + time * 0.25f;
                    Vector2 shade = focus + angle.ToRotationVector2() * (70f + index % 2 * 38f);
                    DrawSoulShade(batch, pixel, shade, new Color(154, 246, 239), opacity,
                        0.72f + index % 3 * 0.10f, angle);
                }
                break;
            }
            default: // Coordinate grid collapses into a negative-space world split.
            {
                float splitWidth = MathHelper.Clamp(shortSide * 0.10f, 68f, 128f);
                batch.Draw(pixel, new Rectangle(0, (int)Math.Round(focus.Y - splitWidth * 0.5f),
                    (int)Math.Ceiling(viewport.X), (int)Math.Round(splitWidth)),
                    new Color(0, 0, 2) * (opacity * 0.96f));
                batch.Draw(pixel, new Rectangle((int)Math.Round(focus.X - splitWidth * 0.38f), 0,
                    (int)Math.Round(splitWidth * 0.76f), (int)Math.Ceiling(viewport.Y)),
                    new Color(0, 0, 2) * (opacity * 0.96f));
                DrawLayeredLine(batch, pixel, new Vector2(0f, focus.Y - splitWidth * 0.5f),
                    new Vector2(viewport.X, focus.Y - splitWidth * 0.5f), new Color(82, 9, 130),
                    Color.White, 9f, 2f, opacity);
                DrawLayeredLine(batch, pixel, new Vector2(focus.X + splitWidth * 0.38f, 0f),
                    new Vector2(focus.X + splitWidth * 0.38f, viewport.Y), new Color(55, 4, 100),
                    new Color(220, 160, 255), 9f, 2f, opacity);
                for (int panel = 0; panel < 5; panel++)
                {
                    float y = viewport.Y * (panel + 1f) / 6f;
                    float offset = (panel % 2 == 0 ? 1f : -1f) * 22f * opacity;
                    DrawLayeredLine(batch, pixel, new Vector2(offset, y),
                        new Vector2(viewport.X + offset, y), Color.Black,
                        new Color(122, 30, 180), 7f, 1.5f, opacity * 0.72f);
                }
                break;
            }
        }
    }

    private static void DrawPentagram(
        SpriteBatch batch,
        Texture2D pixel,
        Vector2 center,
        float radius,
        float rotation,
        Color shell,
        Color core,
        float opacity)
    {
        Span<Vector2> vertices = stackalloc Vector2[5];
        for (int index = 0; index < vertices.Length; index++)
            vertices[index] = center + (rotation + index * MathHelper.TwoPi / 5f).ToRotationVector2() * radius;
        for (int edge = 0; edge < vertices.Length; edge++)
        {
            int start = edge * 2 % 5;
            int end = (edge + 1) * 2 % 5;
            DrawLayeredLine(batch, pixel, vertices[start], vertices[end], shell, core,
                15f, 3f, opacity);
        }
        DrawRing(batch, pixel, center, radius * 1.04f, 48, shell * (opacity * 0.80f),
            7f, rotation, 1f);
    }

    private static void DrawTaperedCrescent(
        SpriteBatch batch,
        Texture2D pixel,
        Vector2 center,
        float radius,
        float rotation,
        float sweep,
        float opacity,
        Color shadow,
        Color body,
        Color edge,
        float thickness)
    {
        opacity = MathHelper.Clamp(opacity, 0f, 1f);
        if (opacity <= 0.001f || radius <= 2f || thickness <= 0.5f || Math.Abs(sweep) <= 0.01f)
            return;

        ReaperUltimateCrescentMaskSet? mask = SelectCrescentMask(Math.Abs(sweep), thickness / radius);
        if (mask is null || mask.Body.IsDisposed || mask.Flow.IsDisposed || mask.Edge.IsDisposed)
            return;

        float scale = radius / CrescentReferenceRadius;
        Vector2 textureOrigin = new(CrescentMaskSize * 0.5f);
        SpriteEffects effects = sweep < 0f ? SpriteEffects.FlipVertically : SpriteEffects.None;

        // Four cached-mask draws replace hundreds of MagicPixel segments. Body,
        // longitudinal flow and white-hot rim remain independently tintable.
        batch.Draw(mask.Body, center, null, shadow * (opacity * 0.48f), rotation,
            textureOrigin, scale * 1.025f, effects, 0f);
        batch.Draw(mask.Body, center, null, body * (opacity * 0.84f), rotation,
            textureOrigin, scale, effects, 0f);
        batch.Draw(mask.Flow, center, null, Color.Lerp(body, edge, 0.34f) * (opacity * 0.48f),
            rotation + (float)Math.Sin(Main.GlobalTimeWrappedHourly * 2.4f) * 0.004f,
            textureOrigin, scale, effects, 0f);
        batch.Draw(mask.Edge, center, null, edge * opacity, rotation,
            textureOrigin, scale, effects, 0f);
    }

    private static ReaperUltimateCrescentMaskSet? SelectCrescentMask(float sweep, float thicknessRatio)
    {
        ReaperUltimateCrescentMaskSet? best = null;
        float bestScore = float.MaxValue;
        for (int index = 0; index < crescentMasks.Length; index++)
        {
            ReaperUltimateCrescentMaskSet candidate = crescentMasks[index];
            // Preserve the requested arc silhouette first; thickness selects the
            // closer preset when two sweep families are visually comparable.
            float score = Math.Abs(candidate.Sweep - sweep) * 2.4f
                + Math.Abs(candidate.ThicknessRatio - thicknessRatio) * 4f;
            if (score >= bestScore)
                continue;
            best = candidate;
            bestScore = score;
        }
        return best;
    }

    private static void EnsureCrescentMasks()
    {
        if (crescentMasks.Length > 0)
            return;
        if (Main.dedServ || Main.graphics?.GraphicsDevice is not GraphicsDevice graphicsDevice)
            return;

        crescentMasks =
        [
            CreateCrescentMask(graphicsDevice, 3.58f, 0.115f),
            CreateCrescentMask(graphicsDevice, 4.24f, 0.175f),
            CreateCrescentMask(graphicsDevice, 4.66f, 0.245f)
        ];
    }

    private static ReaperUltimateCrescentMaskSet CreateCrescentMask(
        GraphicsDevice graphicsDevice,
        float sweep,
        float thicknessRatio)
    {
        Color[] bodyData = new Color[CrescentMaskSize * CrescentMaskSize];
        Color[] flowData = new Color[bodyData.Length];
        Color[] edgeData = new Color[bodyData.Length];
        float center = CrescentMaskSize * 0.5f;
        float maximumWidth = CrescentReferenceRadius * thicknessRatio;

        for (int y = 0; y < CrescentMaskSize; y++)
        {
            float dy = y + 0.5f - center;
            for (int x = 0; x < CrescentMaskSize; x++)
            {
                float dx = x + 0.5f - center;
                float radial = (float)Math.Sqrt(dx * dx + dy * dy);
                if (radial < CrescentReferenceRadius - maximumWidth
                    || radial > CrescentReferenceRadius + maximumWidth)
                {
                    continue;
                }

                float angle = (float)Math.Atan2(dy, dx);
                if (angle < 0f)
                    angle += MathHelper.TwoPi;
                if (angle > sweep)
                    continue;

                float along = angle / sweep;
                float taper = (float)Math.Pow(Math.Max(0f, Math.Sin(along * MathHelper.Pi)), 0.58f);
                taper *= MathHelper.Lerp(0.72f, 1f, 1f - along * 0.42f);
                float halfWidth = Math.Max(0.75f, maximumWidth * taper * 0.5f);
                float distance = Math.Abs(radial - CrescentReferenceRadius);
                float bodyAlpha = MathHelper.Clamp((halfWidth + 1.6f - distance) / 2.8f, 0f, 1f);
                if (bodyAlpha <= 0f)
                    continue;

                int dataIndex = y * CrescentMaskSize + x;
                bodyData[dataIndex] = PremultipliedMask(bodyAlpha);

                float normalizedRadial = (radial - CrescentReferenceRadius) / Math.Max(1f, halfWidth);
                float firstFlow = -0.34f + (float)Math.Sin(along * MathHelper.TwoPi * 2.1f) * 0.10f;
                float secondFlow = 0.18f + (float)Math.Sin(along * MathHelper.TwoPi * 3.2f + 1.3f) * 0.11f;
                float flowAlpha = Math.Max(
                    1f - Math.Abs(normalizedRadial - firstFlow) / 0.12f,
                    1f - Math.Abs(normalizedRadial - secondFlow) / 0.09f);
                flowAlpha = MathHelper.Clamp(flowAlpha, 0f, 1f) * bodyAlpha * (0.62f + taper * 0.38f);
                flowData[dataIndex] = PremultipliedMask(flowAlpha);

                float rimRadius = CrescentReferenceRadius + halfWidth * 0.88f;
                float rimWidth = Math.Max(1.35f, maximumWidth * 0.055f);
                float rimAlpha = MathHelper.Clamp((rimWidth + 1.2f - Math.Abs(radial - rimRadius)) / 2.2f, 0f, 1f);
                edgeData[dataIndex] = PremultipliedMask(rimAlpha * bodyAlpha);
            }
        }

        Texture2D bodyTexture = new(graphicsDevice, CrescentMaskSize, CrescentMaskSize);
        Texture2D flowTexture = new(graphicsDevice, CrescentMaskSize, CrescentMaskSize);
        Texture2D edgeTexture = new(graphicsDevice, CrescentMaskSize, CrescentMaskSize);
        bodyTexture.SetData(bodyData);
        flowTexture.SetData(flowData);
        edgeTexture.SetData(edgeData);
        return new ReaperUltimateCrescentMaskSet(sweep, thicknessRatio, bodyTexture, flowTexture, edgeTexture);
    }

    private static Color PremultipliedMask(float opacity)
    {
        byte value = (byte)Math.Round(MathHelper.Clamp(opacity, 0f, 1f) * byte.MaxValue);
        return new Color(value, value, value, value);
    }

    private static void DisposeCrescentMasks(ReaperUltimateCrescentMaskSet[] masks)
    {
        for (int index = 0; index < masks.Length; index++)
            masks[index].Dispose();
    }

    private static void DrawImpactStar(
        SpriteBatch batch,
        Texture2D pixel,
        Vector2 center,
        float radius,
        Color outer,
        Color core,
        float opacity)
    {
        opacity = MathHelper.Clamp(opacity, 0f, 1f);
        if (opacity <= 0.001f || radius <= 1f)
            return;

        DrawGlow(batch, center, radius * 2.4f, outer * (opacity * 0.30f));
        for (int ray = 0; ray < 10; ray++)
        {
            float angle = ray * MathHelper.TwoPi / 10f + (ray % 2) * 0.11f;
            float rayLength = radius * (ray % 2 == 0 ? 1f : 0.55f);
            Vector2 direction = angle.ToRotationVector2();
            DrawLayeredLine(batch, pixel, center - direction * rayLength * 0.12f,
                center + direction * rayLength, outer, core, 5f, 1.4f, opacity);
        }
    }

    private static void DrawReaperSilhouette(
        SpriteBatch batch,
        Texture2D pixel,
        Vector2 center,
        float scale,
        Color shadow,
        Color rim,
        float opacity)
    {
        opacity = MathHelper.Clamp(opacity, 0f, 1f);
        if (opacity <= 0.001f || scale <= 10f)
            return;

        float headRadius = scale * 0.11f;
        Vector2 head = center - Vector2.UnitY * scale * 0.30f;
        DrawGlow(batch, head, headRadius * 2.5f, rim * (opacity * 0.13f));
        DrawPolygon(batch, pixel, head, headRadius, 9, shadow * (opacity * 0.96f),
            headRadius * 0.82f, -MathHelper.PiOver2, 1f);
        DrawRing(batch, pixel, head, headRadius * 1.04f, 28, rim * (opacity * 0.56f), 3f, 0f, 1f);

        Vector2 leftShoulder = center + new Vector2(-scale * 0.25f, -scale * 0.17f);
        Vector2 rightShoulder = center + new Vector2(scale * 0.25f, -scale * 0.17f);
        Vector2 hem = center + Vector2.UnitY * scale * 0.42f;
        DrawLayeredLine(batch, pixel, leftShoulder, hem, shadow, rim, scale * 0.22f, 3f, opacity * 0.62f);
        DrawLayeredLine(batch, pixel, rightShoulder, hem, shadow, rim, scale * 0.22f, 3f, opacity * 0.62f);
        DrawLayeredLine(batch, pixel, leftShoulder, rightShoulder, shadow, rim, scale * 0.18f, 3f, opacity * 0.58f);

        Vector2 staffStart = center + new Vector2(-scale * 0.30f, scale * 0.36f);
        Vector2 staffEnd = center + new Vector2(scale * 0.26f, -scale * 0.48f);
        DrawLayeredLine(batch, pixel, staffStart, staffEnd, shadow, rim, 15f, 3.2f, opacity * 0.82f);
        DrawTaperedCrescent(batch, pixel, staffEnd + new Vector2(scale * 0.10f, scale * 0.03f),
            scale * 0.25f, -2.7f, 3.5f, opacity * 0.88f, shadow, rim, Color.White, scale * 0.045f);
    }

    private static void DrawTombstone(
        SpriteBatch batch,
        Texture2D pixel,
        Vector2 bottomCenter,
        float width,
        float height,
        Color fill,
        Color outline)
    {
        Vector2 top = bottomCenter - Vector2.UnitY * height;
        Rectangle body = new(
            (int)Math.Round(bottomCenter.X - width * 0.5f),
            (int)Math.Round(top.Y + width * 0.26f),
            Math.Max(1, (int)Math.Round(width)),
            Math.Max(1, (int)Math.Round(height - width * 0.26f)));
        batch.Draw(pixel, body, fill);
        DrawRing(batch, pixel, new Vector2(bottomCenter.X, body.Top), width * 0.5f, 16,
            fill, width * 0.48f, MathHelper.Pi, 0.5f);
        DrawLayeredLine(batch, pixel, new Vector2(body.Left, body.Bottom), new Vector2(body.Right, body.Bottom),
            outline, Color.White, 4f, 1f, 0.78f);
        DrawLayeredLine(batch, pixel, new Vector2(body.Left, body.Top), new Vector2(body.Left, body.Bottom),
            outline, Color.White, 3f, 0.8f, 0.64f);
        DrawLayeredLine(batch, pixel, new Vector2(body.Right, body.Top), new Vector2(body.Right, body.Bottom),
            outline, Color.White, 3f, 0.8f, 0.64f);
        Vector2 crossCenter = bottomCenter - Vector2.UnitY * height * 0.48f;
        DrawSegment(batch, pixel, crossCenter - Vector2.UnitY * 12f, crossCenter + Vector2.UnitY * 13f,
            outline, 2.2f);
        DrawSegment(batch, pixel, crossCenter - Vector2.UnitX * 8f, crossCenter + Vector2.UnitX * 8f,
            outline, 2.2f);
    }

    private static void DrawSoulShade(
        SpriteBatch batch,
        Texture2D pixel,
        Vector2 position,
        Color color,
        float opacity,
        float scale,
        float rotation)
    {
        DrawGlow(batch, position, 38f * scale, color * (0.20f * opacity));
        Texture2D? texture = soulTexture?.IsLoaded == true ? soulTexture.Value : null;
        if (texture is not null)
        {
            float textureScale = 46f / Math.Max(texture.Width, texture.Height) * scale;
            batch.Draw(texture, position, null, color * (0.74f * opacity), rotation * 0.04f,
                new Vector2(texture.Width, texture.Height) * 0.5f, textureScale, SpriteEffects.None, 0f);
        }
        else
        {
            DrawPolygon(batch, pixel, position, 14f * scale, 4, color * opacity, 2f,
                MathHelper.PiOver4, 1f);
        }

        Vector2 tail = position + new Vector2((float)Math.Sin(rotation) * 7f, 20f * scale);
        DrawLayeredLine(batch, pixel, position, tail, color, Color.White, 5f, 1.1f, opacity * 0.56f);
    }

    private static void DrawDeathRuneGlyph(
        SpriteBatch batch,
        Texture2D pixel,
        Vector2 center,
        int glyph,
        float size,
        Color color,
        Color core,
        float opacity)
    {
        switch (glyph)
        {
            case 0: // Bone cross.
                DrawLayeredLine(batch, pixel, center - Vector2.UnitY * size, center + Vector2.UnitY * size,
                    color, core, 4f, 1.2f, opacity);
                DrawLayeredLine(batch, pixel, center - Vector2.UnitX * size * 0.65f, center + Vector2.UnitX * size * 0.65f,
                    color, core, 4f, 1.2f, opacity);
                break;
            case 1: // Blood fang.
                DrawLayeredLine(batch, pixel, center + new Vector2(-size, -size * 0.7f), center + new Vector2(0f, size),
                    color, core, 4f, 1.2f, opacity);
                DrawLayeredLine(batch, pixel, center + new Vector2(size, -size * 0.7f), center + new Vector2(0f, size),
                    color, core, 4f, 1.2f, opacity);
                break;
            case 2: // Infernal triangle.
                DrawRuneTriangle(batch, pixel, center, size, -MathHelper.PiOver2, core, opacity);
                break;
            case 3: // Frost crystal.
                for (int index = 0; index < 3; index++)
                {
                    Vector2 direction = (index * MathHelper.Pi / 3f).ToRotationVector2() * size;
                    DrawLayeredLine(batch, pixel, center - direction, center + direction,
                        color, core, 3.5f, 1f, opacity);
                }
                break;
            case 4: // Soul diamond.
                DrawPolygon(batch, pixel, center, size, 4, core * opacity, 3f, MathHelper.PiOver4, 1f);
                DrawSegment(batch, pixel, center - Vector2.UnitY * size * 0.55f,
                    center + Vector2.UnitY * size * 0.55f, color * opacity, 2f);
                break;
            default: // Void severance.
                DrawLayeredLine(batch, pixel, center + new Vector2(-size, size), center + new Vector2(size, -size),
                    color, Color.White, 5f, 1.3f, opacity);
                DrawLayeredLine(batch, pixel, center + new Vector2(-size * 0.55f, -size), center + new Vector2(size * 0.55f, size),
                    color, core, 3f, 0.9f, opacity * 0.75f);
                break;
        }
    }

    private static void DrawRuneTriangle(
        SpriteBatch batch,
        Texture2D pixel,
        Vector2 center,
        float radius,
        float rotation,
        Color color,
        float opacity)
    {
        DrawPolygon(batch, pixel, center, radius, 3, color * opacity, 2.2f, rotation, 1f);
    }

    private static void DrawGlow(SpriteBatch batch, Vector2 center, float radius, Color color)
    {
        if (radius <= 0.5f || color.A == 0)
            return;
        DeathDomainPrimitiveTextureSystem.DrawRadialGradient(batch, center, radius, color);
    }

    private static void DrawRing(
        SpriteBatch batch,
        Texture2D pixel,
        Vector2 center,
        float radius,
        int segments,
        Color color,
        float width,
        float rotation,
        float reveal)
    {
        if (radius <= 0.5f || segments < 3 || width <= 0.05f || reveal <= 0.001f || color.A == 0)
            return;

        reveal = MathHelper.Clamp(reveal, 0f, 1f);
        segments = Math.Max(segments,
            Math.Min(192, (int)Math.Ceiling(MathHelper.TwoPi * radius / 18f)));
        Vector2 previous = center + rotation.ToRotationVector2() * radius;
        int visibleSegments = Math.Max(1, (int)Math.Ceiling(segments * reveal));
        for (int index = 1; index <= visibleSegments; index++)
        {
            float amount = Math.Min(reveal, index / (float)segments);
            Vector2 current = center + (rotation + MathHelper.TwoPi * amount).ToRotationVector2() * radius;
            DrawSegment(batch, pixel, previous, current, color, width);
            previous = current;
        }
    }

    private static void DrawPolygon(
        SpriteBatch batch,
        Texture2D pixel,
        Vector2 center,
        float radius,
        int sides,
        Color color,
        float width,
        float rotation,
        float reveal)
    {
        if (sides < 3 || radius <= 0.5f || reveal <= 0.001f)
            return;

        reveal = MathHelper.Clamp(reveal, 0f, 1f);
        Vector2 previous = center + rotation.ToRotationVector2() * radius;
        int visibleEdges = Math.Max(1, (int)Math.Ceiling(sides * reveal));
        for (int edge = 1; edge <= visibleEdges; edge++)
        {
            float edgeCompletion = Math.Min(1f, sides * reveal - (edge - 1));
            Vector2 target = center + (rotation + MathHelper.TwoPi * edge / sides).ToRotationVector2() * radius;
            Vector2 current = Vector2.Lerp(previous, target, edgeCompletion);
            DrawSegment(batch, pixel, previous, current, color, width);
            previous = target;
        }
    }

    private static void DrawJaggedLine(
        SpriteBatch batch,
        Texture2D pixel,
        Vector2 start,
        Vector2 end,
        float reveal,
        Color outerColor,
        Color coreColor,
        float outerWidth,
        float coreWidth,
        float amplitude,
        float seed,
        float opacity)
    {
        reveal = MathHelper.Clamp(reveal, 0f, 1f);
        if (reveal <= 0.001f || opacity <= 0.001f)
            return;

        const int segments = 18;
        float totalLength = Vector2.Distance(start, end);
        int segmentCount = Math.Max(3, Math.Min(segments, (int)(totalLength / 38f) + 3));
        Vector2 direction = (end - start).SafeNormalize(Vector2.UnitX);
        Vector2 perpendicular = new(-direction.Y, direction.X);
        Vector2 previous = JaggedPoint(start, end, perpendicular, 0f, segmentCount, amplitude, seed);
        int visibleSegments = Math.Max(1, (int)Math.Ceiling(segmentCount * reveal));
        for (int index = 1; index <= visibleSegments; index++)
        {
            float amount = Math.Min(reveal, index / (float)segmentCount);
            Vector2 current = JaggedPoint(start, end, perpendicular, amount, segmentCount, amplitude, seed);
            DrawLayeredLine(batch, pixel, previous, current, outerColor, coreColor,
                outerWidth, coreWidth, opacity);
            previous = current;
        }
    }

    private static Vector2 JaggedPoint(
        Vector2 start,
        Vector2 end,
        Vector2 perpendicular,
        float amount,
        int segments,
        float amplitude,
        float seed)
    {
        float envelope = (float)Math.Sin(amount * MathHelper.Pi);
        float grain = (float)Math.Sin((amount * segments + seed) * 5.137f) * 0.67f
            + (float)Math.Sin((amount * segments + seed * 1.91f) * 11.73f) * 0.33f;
        return Vector2.Lerp(start, end, amount) + perpendicular * (grain * amplitude * envelope);
    }

    private static void DrawLayeredLine(
        SpriteBatch batch,
        Texture2D pixel,
        Vector2 start,
        Vector2 end,
        Color outerColor,
        Color coreColor,
        float outerWidth,
        float coreWidth,
        float opacity)
    {
        if (opacity <= 0.001f)
            return;
        DrawSegment(batch, pixel, start, end, outerColor * (opacity * 0.72f), outerWidth);
        DrawSegment(batch, pixel, start, end, coreColor * opacity, coreWidth);
    }

    private static void DrawSegment(
        SpriteBatch batch,
        Texture2D pixel,
        Vector2 start,
        Vector2 end,
        Color color,
        float width)
    {
        DeathDomainPrimitiveTextureSystem.DrawSmoothLine(batch, start, end, color, width);
    }

    private static float GetStateOpacity(in UltimateVisualState state)
    {
        float progress = MathHelper.Clamp(state.Timer / (float)Math.Max(1, state.Duration), 0f, 1f);
        float introduction = MathHelper.Lerp(0.35f, 1f, Ease(progress / 0.06f));
        float tail = 1f - MathHelper.Clamp(state.FramesSinceReport / (float)TailFrames, 0f, 1f);
        return introduction * Ease(tail);
    }

    private static float GetBackdropDarkness(in UltimateVisualState state)
    {
        if (state.Form == ReaperFormId.Death)
        {
            float cutProgress = MathHelper.Clamp(
                (state.Timer - ReaperDeathUltimateGeometry.FirstCutTick)
                / (float)(ReaperDeathUltimateGeometry.ShatterTick
                    - ReaperDeathUltimateGeometry.FirstCutTick), 0f, 1f);
            return MathHelper.Lerp(0.30f, 0.985f, Ease(cutProgress));
        }
        return state.Form switch
        {
            ReaperFormId.Bone => 0.36f,
            ReaperFormId.Blood => 0.42f,
            ReaperFormId.Infernal => 0.38f,
            ReaperFormId.Frost => 0.31f,
            ReaperFormId.Soul => 0.37f,
            ReaperFormId.Void => 0.52f,
            _ => 0f
        };
    }

    private static float Reveal(float progress, float start, float end)
    {
        if (end <= start)
            return progress >= end ? 1f : 0f;
        return Ease((progress - start) / (end - start));
    }

    private static float Envelope(float progress, float start, float riseEnd, float fallStart, float end)
    {
        if (progress < start || progress > end)
            return 0f;
        if (progress < riseEnd)
            return Reveal(progress, start, riseEnd);
        if (progress <= fallStart)
            return 1f;
        return 1f - Reveal(progress, fallStart, end);
    }

    private static float Pulse(float progress, float center, float halfWidth)
    {
        if (halfWidth <= 0f)
            return 0f;
        return Ease(1f - Math.Abs(progress - center) / halfWidth);
    }

    private static float Ease(float value)
    {
        value = MathHelper.Clamp(value, 0f, 1f);
        return value * value * (3f - 2f * value);
    }

    private static float PositiveModulo(float value, float modulus)
    {
        float result = value % modulus;
        return result < 0f ? result + modulus : result;
    }

    private static void EnsureStateStorage()
    {
        if (states.Length != Main.maxPlayers)
            states = new UltimateVisualState[Main.maxPlayers];
    }

    private static void ClearStates()
    {
        if (states.Length > 0)
            Array.Clear(states, 0, states.Length);
    }

    private struct UltimateVisualState
    {
        public bool Active;
        public ReaperFormId Form;
        public int ActionId;
        public Vector2 OriginWorld;
        public Vector2 FocusWorld;
        public Vector2 Aim;
        public int Timer;
        public int Duration;
        public int FramesSinceReport;
        public ulong LastReportTick;
    }
}