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

DeathMod

【Terraria】DeathMod (now aka Soul Harvest)

公开
关注 0 Fork 0 Star 0
UTF-8
using SoulHarvest.Items;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using System;
using Terraria;
using Terraria.GameContent;
using Terraria.ModLoader;

namespace SoulHarvest.Common;

[Autoload(Side = ModSide.Client)]
internal class DeathDomainVisualSystem : ModSystem
{
    private const float OpenAnimationSpeed = 1f / 42f;
    private const float CloseAnimationSpeed = 1f / 42f;
    private const float DescentAnimationSpeed = 1f / 52f;
    private const float DepartureAnimationSpeed = 1f / 42f;

    private readonly float[] domainAnimation = new float[Main.maxPlayers];
    private readonly float[] descentAnimation = new float[Main.maxPlayers];
    private readonly float[] cachedRadius = new float[Main.maxPlayers];
    private readonly int[] cachedMastery = new int[Main.maxPlayers];
    private readonly bool[] cachedFullScreen = new bool[Main.maxPlayers];

    public override void Load()
    {
        On_Main.DoDraw_WallsTilesNPCs += DrawDomainBackdropsBeforeWallsAndTiles;
    }

    public override void Unload()
    {
        On_Main.DoDraw_WallsTilesNPCs -= DrawDomainBackdropsBeforeWallsAndTiles;
    }

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

        for (int index = 0; index < Main.maxPlayers; index++)
        {
            Player player = Main.player[index];
            if (!player.active)
            {
                domainAnimation[index] = 0f;
                descentAnimation[index] = 0f;
                cachedRadius[index] = 0f;
                cachedMastery[index] = 0;
                cachedFullScreen[index] = false;
                continue;
            }

            MyPlayer data = player.GetModPlayer<MyPlayer>();
            DeathNecklace? necklace = data.ActiveDeathNecklace;
            if (necklace is not null)
            {
                cachedRadius[index] = necklace.DomainRadius;
                cachedMastery[index] = 5 + (int)MathF.Round(data.DeathDomainProgression.VisualMastery * 70f);
            }

            bool enabled = !player.dead && necklace is not null && data.DeathDomainEnabled;
            bool descended = enabled && data.IsDeathDomainDescended(necklace);
            cachedFullScreen[index] = descended;
            float speed = enabled ? OpenAnimationSpeed : CloseAnimationSpeed;
            domainAnimation[index] = MathHelper.Clamp(
                domainAnimation[index] + (enabled ? speed : -speed),
                0f,
                1f);
            float descentSpeed = descended
                ? DescentAnimationSpeed
                : DepartureAnimationSpeed;
            descentAnimation[index] = MathHelper.Clamp(
                descentAnimation[index] + (descended ? descentSpeed : -descentSpeed),
                0f,
                1f);
        }
    }

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

    public override void PostDrawTiles()
    {
        Player localPlayer = Main.LocalPlayer;
        if (Main.gameMenu || !localPlayer.active)
            return;

        bool anyVisibleDomain = false;
        for (int index = 0; index < Main.maxPlayers; index++)
        {
            if (domainAnimation[index] > 0.001f)
            {
                anyVisibleDomain = true;
                break;
            }
        }
        if (!anyVisibleDomain)
            return;

        SpriteBatch batch = Main.spriteBatch;
        batch.Begin(
            SpriteSortMode.Deferred,
            BlendState.AlphaBlend,
            SamplerState.LinearClamp,
            DepthStencilState.None,
            RasterizerState.CullNone,
            null,
            Main.GameViewMatrix.TransformationMatrix);

        for (int index = 0; index < Main.maxPlayers; index++)
        {
            Player player = Main.player[index];
            float animation = domainAnimation[index];
            if (!player.active || animation <= 0.001f)
                continue;

            float radius = cachedRadius[index];
            int mastery = cachedMastery[index];
            bool fullScreen = cachedFullScreen[index];
            if (radius <= 0f)
                radius = 160f;
            Vector2 screenCenter = player.Center - Main.screenPosition;
            if (!fullScreen
                && (screenCenter.X < -radius
                    || screenCenter.X > Main.screenWidth + radius
                    || screenCenter.Y < -radius
                    || screenCenter.Y > Main.screenHeight + radius))
            {
                continue;
            }
            if (!fullScreen)
                DrawDomainRing(batch, screenCenter, radius, mastery, animation, drawBackdrop: false);
            else
                DrawFullScreenDomain(batch, screenCenter, radius, mastery, animation);

            float descent = descentAnimation[index];
            if (descent > 0.001f && descent < 0.999f)
            {
                DrawManifestationWave(
                    batch,
                    screenCenter,
                    radius,
                    GetMasteryProgress(mastery),
                    GetDomainApertureReveal(animation),
                    descent,
                    drawBackdrop: false,
                    drawBoundary: true);
            }
        }
        batch.End();
    }

    private static void DrawDomainRing(
        SpriteBatch batch,
        Vector2 center,
        float targetRadius,
        int mastery,
        float animation,
        bool drawBackdrop = true)
    {
        float time = Main.GlobalTimeWrappedHourly;
        float progress = MathHelper.SmoothStep(0f, 1f, animation);
        float reveal = GetDomainApertureReveal(animation);
        float rangeProgress = MathHelper.Clamp(
            (targetRadius - 160f) / (DeathNecklace.RadiusGrowthPerLevel * (DeathNecklace.MaxCoreLevel - 1)),
            0f,
            1f);
        float manifestation = MathHelper.Lerp(0.38f, 1f, rangeProgress);
        float manifestedReveal = reveal * manifestation;
        float transition = (float)Math.Sqrt(Math.Max(0f,
            (float)Math.Sin(animation * MathHelper.Pi)));
        float overshoot = 1f + (float)Math.Sin(progress * MathHelper.Pi) * 0.028f;
        float radius = MathHelper.Lerp(8f, targetRadius, progress) * overshoot;
        float pulse = (0.82f + (float)Math.Sin(time * 3.2f) * 0.12f) * manifestedReveal;
        float masteryProgress = GetMasteryProgress(mastery);
        DrawBlackHoleField(batch, center, radius, masteryProgress, pulse, time, manifestedReveal, drawBackdrop);
        DrawNoiseBoundary(batch, center, radius + 3f, time, 0.4f,
            12f + transition * 28f, 9f + transition * 3f,
            (0.18f + transition * 0.16f) * pulse, masteryProgress, gaps: true);
        DrawNoiseBoundary(batch, center, radius, time, 1.7f,
            9f + transition * 20f, 5f + transition * 1.8f,
            (0.42f + transition * 0.34f) * pulse, masteryProgress, gaps: true);
        DrawNoiseBoundary(batch, center, radius - 2f, time, 3.3f,
            7f + transition * 14f, 2.7f + masteryProgress + transition * 1.6f,
            (0.94f + transition * 0.22f) * pulse, masteryProgress, gaps: false);
        DrawNoiseBoundary(batch, center, radius - 7f, -time, 5.1f, 5f, 1.4f, 0.58f * pulse, masteryProgress, gaps: true);
        DrawDomainRippleWaves(batch, center, radius, time, masteryProgress, pulse);
    }

    private static void DrawBlackHoleField(
        SpriteBatch batch,
        Vector2 center,
        float radius,
        float mastery,
        float pulse,
        float time,
        float reveal,
        bool drawBackdrop)
    {
        float coreGap = MathHelper.Lerp(22f, 14f, mastery);
        float coreRadius = radius > coreGap + 6f ? radius - coreGap : radius * 0.58f;

        for (int arm = 0; arm < 5; arm++)
            DrawAccretionSpiral(batch, center, coreRadius, Math.Max(coreRadius + 2f, radius - 3f), time, arm, mastery, pulse);

        float gap = Math.Max(2f, radius - coreRadius);
        DrawNoiseBoundary(batch, center, coreRadius + gap * 0.72f, -time * 1.35f, 9.4f, 4.5f, 7f, 0.18f * pulse, mastery, gaps: true);
        DrawNoiseBoundary(batch, center, coreRadius + gap * 0.34f, time * 1.8f, 12.8f, 2.8f, 3.8f, 0.78f * pulse, mastery, gaps: true);

        float coreNoiseTime = time * 0.8f;
        const float coreNoiseSeed = 16.2f;
        float coreNoiseAmplitude = Math.Min(coreRadius * 0.18f, MathHelper.Lerp(5f, 10f, mastery) * MathHelper.Lerp(0.35f, 1f, reveal));
        if (drawBackdrop)
        {
            DrawClippedDeathDomainBackdrop(
                batch,
                center,
                coreRadius,
                coreNoiseTime,
                coreNoiseSeed,
                coreNoiseAmplitude,
                mastery,
                reveal);
        }
        // The old scan-band fill stamped hundreds of horizontal rounded strips
        // over the centre and produced the visible television-line pattern. A
        // single antialiased disc supplies the same restrained shadow without any
        // row seams; the animated noisy boundary still defines its organic edge.
        DeathDomainPrimitiveTextureSystem.DrawSmoothDisc(
            batch,
            center,
            coreRadius,
            new Color(3, 0, 10) * (MathHelper.Lerp(0.08f, 0.26f, mastery) * reveal));
        DeathDomainPrimitiveTextureSystem.DrawRadialGradient(
            batch,
            center,
            coreRadius * MathHelper.Lerp(0.48f, 0.58f, mastery),
            new Color(255, 58, 84) * (MathHelper.Lerp(0.16f, 0.24f, mastery) * pulse));
        DrawOutwardPressureWaves(batch, center, coreRadius, time, mastery, pulse);
        DrawNoiseBoundary(batch, center, coreRadius, coreNoiseTime, coreNoiseSeed, coreNoiseAmplitude, 2.2f, 0.76f * pulse, mastery, gaps: false);
        DrawInwardSuctionWaves(batch, center, radius, coreRadius, time, mastery, pulse);
    }

    private static void DrawClippedDeathDomainBackdrop(
        SpriteBatch batch,
        Vector2 center,
        float baseRadius,
        float noiseTime,
        float noiseSeed,
        float noiseAmplitude,
        float mastery,
        float reveal)
    {
        const int boundarySegments = 192;
        float preferredBandHeight = MathHelper.Clamp(baseRadius / 280f, 2f, 8f);
        Span<Vector2> boundary = stackalloc Vector2[boundarySegments];
        float extent = 0f;
        for (int index = 0; index < boundarySegments; index++)
        {
            float angle = MathHelper.TwoPi * index / boundarySegments;
            float radius = Math.Max(1f, baseRadius + BoundaryNoise(angle, noiseTime, noiseSeed) * noiseAmplitude);
            boundary[index] = angle.ToRotationVector2() * radius;
            extent = Math.Max(extent, Math.Abs(boundary[index].Y));
        }

        int bands = Math.Max(1, (int)Math.Ceiling(extent * 2f / preferredBandHeight));
        float bandHeight = extent * 2f / bands;
        DeathDomainBackdropTextureSystem.GetExpandedDomainDrawBounds(
            out Vector2 expandedDestination,
            out float expandedDestinationWidth,
            out float expandedDestinationHeight);
        float expandedDestinationRight = expandedDestination.X
            + expandedDestinationWidth;
        float expandedDestinationBottom = expandedDestination.Y
            + expandedDestinationHeight;
        // Draw one complete layer at a time. This retains proper alpha compositing
        // while allowing SpriteBatch to group hundreds of clipped scan bands into
        // only three texture batches instead of switching textures for every row.
        for (int layer = 0; layer < 3; layer++)
        {
            float opacity = layer switch
            {
                0 => MathHelper.Lerp(0.88f, 1f, mastery) * reveal,
                1 => MathHelper.Lerp(0.72f, 0.9f, mastery) * reveal,
                _ => MathHelper.Lerp(0.78f, 0.96f, mastery) * reveal
            };
            Texture2D texture = DeathDomainBackdropTextureSystem.GetLayer(layer);
            DeathDomainBackdropTextureSystem.GetExpandedDomainSourceFrame(layer,
                out float sourceOffsetX, out float sourceOffsetY,
                out float viewWidth, out float viewHeight);

            for (int index = 0; index < bands; index++)
            {
                float scanY = -extent + (index + 0.5f) * bandHeight;
                float left = float.MaxValue;
                float right = float.MinValue;
                for (int edge = 0; edge < boundarySegments; edge++)
                {
                    Vector2 start = boundary[edge];
                    Vector2 end = boundary[(edge + 1) % boundarySegments];
                    if ((start.Y <= scanY && end.Y > scanY) || (end.Y <= scanY && start.Y > scanY))
                    {
                        float amount = (scanY - start.Y) / (end.Y - start.Y);
                        float intersectionX = MathHelper.Lerp(start.X, end.X, amount);
                        left = Math.Min(left, intersectionX);
                        right = Math.Max(right, intersectionX);
                    }
                }

                if (left >= right)
                    continue;

                float drawLeft = Math.Max(expandedDestination.X,
                    center.X + left);
                float drawRight = Math.Min(expandedDestinationRight,
                    center.X + right);
                float drawTop = Math.Max(expandedDestination.Y,
                    center.Y + scanY - bandHeight * 0.5f);
                float drawBottom = Math.Min(expandedDestinationBottom,
                    center.Y + scanY + bandHeight * 0.5f);
                if (drawLeft >= drawRight || drawTop >= drawBottom)
                    continue;

                float normalizedLeft = (drawLeft - expandedDestination.X)
                    / expandedDestinationWidth;
                float normalizedRight = (drawRight - expandedDestination.X)
                    / expandedDestinationWidth;
                float normalizedTop = (drawTop - expandedDestination.Y)
                    / expandedDestinationHeight;
                float normalizedBottom = (drawBottom - expandedDestination.Y)
                    / expandedDestinationHeight;
                DrawBackdropBand(
                    batch,
                    texture,
                    new Vector2(drawLeft, drawTop),
                    drawRight - drawLeft,
                    drawBottom - drawTop,
                    sourceOffsetX + normalizedLeft * viewWidth,
                    sourceOffsetY + normalizedTop * viewHeight,
                    Math.Max(0.5f, (normalizedRight - normalizedLeft) * viewWidth),
                    Math.Max(0.5f, (normalizedBottom - normalizedTop) * viewHeight),
                    opacity);
            }
        }
    }

    private static void DrawBackdropBand(
        SpriteBatch batch,
        Texture2D texture,
        Vector2 destination,
        float destinationWidth,
        float destinationHeight,
        float sourceX,
        float sourceY,
        float sourceWidth,
        float sourceHeight,
        float opacity)
    {
        if (destinationWidth <= 0.05f || destinationHeight <= 0.05f || opacity <= 0.001f)
            return;

        float clampedSourceY = MathHelper.Clamp(sourceY, 0f, texture.Height - 0.001f);
        float clampedSourceHeight = Math.Min(sourceHeight, texture.Height - clampedSourceY);
        if (clampedSourceHeight <= 0.001f)
            return;

        float remainingSourceWidth = sourceWidth;
        float consumedSourceWidth = 0f;
        float wrappedSourceX = PositiveModulo(sourceX, texture.Width);
        while (remainingSourceWidth > 0.001f)
        {
            float sourceWidthPart = Math.Min(remainingSourceWidth, texture.Width - wrappedSourceX);
            float destinationWidthPart = destinationWidth * sourceWidthPart / sourceWidth;
            float destinationX = destination.X + destinationWidth * consumedSourceWidth / sourceWidth;
            DrawBackdropPart(
                batch,
                texture,
                new Vector2(destinationX, destination.Y),
                destinationWidthPart,
                destinationHeight,
                wrappedSourceX,
                clampedSourceY,
                sourceWidthPart,
                clampedSourceHeight,
                opacity);

            remainingSourceWidth -= sourceWidthPart;
            consumedSourceWidth += sourceWidthPart;
            wrappedSourceX = 0f;
        }
    }

    private static void DrawBackdropPart(
        SpriteBatch batch,
        Texture2D texture,
        Vector2 destination,
        float destinationWidth,
        float destinationHeight,
        float sourceX,
        float sourceY,
        float sourceWidth,
        float sourceHeight,
        float opacity)
    {
        int sourceLeft = Math.Clamp((int)Math.Floor(sourceX), 0, texture.Width - 1);
        int sourceTop = Math.Clamp((int)Math.Floor(sourceY), 0, texture.Height - 1);
        int sourceRight = Math.Clamp((int)Math.Ceiling(sourceX + sourceWidth), sourceLeft + 1, texture.Width);
        int sourceBottom = Math.Clamp((int)Math.Ceiling(sourceY + sourceHeight), sourceTop + 1, texture.Height);
        Rectangle source = new(sourceLeft, sourceTop, sourceRight - sourceLeft, sourceBottom - sourceTop);
        batch.Draw(
            texture,
            destination,
            source,
            Color.White * opacity,
            0f,
            Vector2.Zero,
            new Vector2(destinationWidth / source.Width, destinationHeight / source.Height),
            SpriteEffects.None,
            0f);
    }

    private static void DrawFullScreenDomain(
        SpriteBatch batch,
        Vector2 center,
        float playerDomainRadius,
        int mastery,
        float animation)
    {
        // Full-screen mastery removes the circular crop from the domain scene. The
        // player's event horizon is still the same max-level field, rather than a
        // circle enlarged until it reaches the viewport corners.
        DrawDomainRing(batch, center, playerDomainRadius, mastery, animation, drawBackdrop: false);
    }

    private void DrawDomainBackdropsBeforeWallsAndTiles(On_Main.orig_DoDraw_WallsTilesNPCs orig, Main self)
    {
        DrawDomainBackdrops();
        orig(self);
    }

    private void DrawDomainBackdrops()
    {
        if (Main.gameMenu || !Main.LocalPlayer.active)
            return;

        bool anyCompletedFullScreen = false;
        bool anyClipped = false;
        float fullScreenMastery = 0f;
        float fullScreenReveal = 0f;
        for (int index = 0; index < Main.maxPlayers; index++)
        {
            if (domainAnimation[index] <= 0.001f || !Main.player[index].active)
                continue;
            float descent = descentAnimation[index];
            if (descent >= 0.999f)
            {
                anyCompletedFullScreen = true;
                fullScreenMastery = Math.Max(fullScreenMastery, GetMasteryProgress(cachedMastery[index]));
                fullScreenReveal = Math.Max(
                    fullScreenReveal,
                    MathHelper.SmoothStep(0f, 1f, domainAnimation[index]));
            }
            if (descent < 0.999f)
            {
                anyClipped = true;
            }
        }
        if (!anyCompletedFullScreen && !anyClipped)
            return;

        // This hook runs during final world composition, after Terraria has drawn
        // (or composited) its background and before it starts walls and tiles.
        // Reuse the active world SpriteBatch so the domain is never captured in
        // the scrolling background render target.
        SpriteBatch batch = Main.spriteBatch;
        if (anyClipped)
        {
            for (int index = 0; index < Main.maxPlayers; index++)
            {
                Player player = Main.player[index];
                float animation = domainAnimation[index];
                if (descentAnimation[index] >= 0.999f
                    || animation <= 0.001f
                    || !player.active)
                    continue;

                float targetRadius = Math.Max(160f, cachedRadius[index]);
                Vector2 center = player.Center - Main.screenPosition;
                if (center.X < -targetRadius || center.X > Main.screenWidth + targetRadius
                    || center.Y < -targetRadius || center.Y > Main.screenHeight + targetRadius)
                {
                    continue;
                }

                GetAnimatedBackdropGeometry(targetRadius, cachedMastery[index], animation,
                    out float coreRadius, out float noiseTime, out float noiseAmplitude,
                    out float masteryProgress, out float reveal);
                DrawClippedDeathDomainBackdrop(
                    batch,
                    center,
                    coreRadius,
                    noiseTime,
                    16.2f,
                    noiseAmplitude,
                    masteryProgress,
                    reveal);
                DrawTransientBackdropBlackHoles(
                    batch,
                    center,
                    new Vector2(coreRadius),
                    masteryProgress,
                    reveal,
                    coreRadius,
                    Main.worldID + index * 101);
            }
        }

        if (anyCompletedFullScreen)
        {
            // The backdrop is camera-relative, so multiple manifested domains share
            // one strongest presentation instead of darkening the scene repeatedly.
            DrawFullScreenBackdrop(batch, fullScreenMastery, fullScreenReveal);
        }

        for (int index = 0; index < Main.maxPlayers; index++)
        {
            Player player = Main.player[index];
            float descent = descentAnimation[index];
            if (!player.active || domainAnimation[index] <= 0.001f
                || descent <= 0.001f || descent >= 0.999f)
            {
                continue;
            }

            if (!anyCompletedFullScreen)
            {
                DrawManifestationWave(
                    batch,
                    player.Center - Main.screenPosition,
                    Math.Max(160f, cachedRadius[index]),
                    GetMasteryProgress(cachedMastery[index]),
                    GetDomainApertureReveal(domainAnimation[index]),
                    descent,
                    drawBackdrop: true,
                    drawBoundary: false);
            }
        }
    }

    private static void GetAnimatedBackdropGeometry(
        float targetRadius,
        int mastery,
        float animation,
        out float coreRadius,
        out float noiseTime,
        out float noiseAmplitude,
        out float masteryProgress,
        out float reveal)
    {
        float progress = MathHelper.SmoothStep(0f, 1f, animation);
        reveal = GetDomainApertureReveal(animation);
        float rangeProgress = MathHelper.Clamp(
            (targetRadius - 160f) / (DeathNecklace.RadiusGrowthPerLevel * (DeathNecklace.MaxCoreLevel - 1)),
            0f,
            1f);
        float manifestedReveal = reveal * MathHelper.Lerp(0.38f, 1f, rangeProgress);
        float overshoot = 1f + (float)Math.Sin(progress * MathHelper.Pi) * 0.028f;
        float radius = MathHelper.Lerp(8f, targetRadius, progress) * overshoot;
        masteryProgress = GetMasteryProgress(mastery);
        float coreGap = MathHelper.Lerp(22f, 14f, masteryProgress);
        coreRadius = radius > coreGap + 6f ? radius - coreGap : radius * 0.58f;
        noiseTime = Main.GlobalTimeWrappedHourly * 0.8f;
        noiseAmplitude = Math.Min(
            coreRadius * 0.18f,
            MathHelper.Lerp(5f, 10f, masteryProgress) * MathHelper.Lerp(0.35f, 1f, manifestedReveal));
        reveal = manifestedReveal;
    }

    private static float GetDomainApertureReveal(float animation)
    {
        // The domain material becomes opaque almost immediately behind the moving
        // noisy edge. Opacity only falls once the contracting aperture is nearly
        // closed, so opening and closing read as spatial sweeps instead of fades.
        return MathHelper.SmoothStep(0f, 1f,
            MathHelper.Clamp(animation / 0.1f, 0f, 1f));
    }

    private static void DrawManifestationWave(
        SpriteBatch batch,
        Vector2 center,
        float finiteRadius,
        float mastery,
        float domainReveal,
        float progress,
        bool drawBackdrop,
        bool drawBoundary)
    {
        DeathDomainBackdropTextureSystem.GetExpandedDomainDrawBounds(
            out Vector2 destination,
            out float destinationWidth,
            out float destinationHeight);
        Vector2 topRight = destination + Vector2.UnitX * destinationWidth;
        Vector2 bottomLeft = destination + Vector2.UnitY * destinationHeight;
        Vector2 bottomRight = destination + new Vector2(destinationWidth, destinationHeight);
        float screenRadius = Math.Max(
            Math.Max(Vector2.Distance(center, destination), Vector2.Distance(center, topRight)),
            Math.Max(Vector2.Distance(center, bottomLeft), Vector2.Distance(center, bottomRight)));

        float expansion = MathHelper.SmoothStep(0f, 1f, progress);
        float radius = MathHelper.Lerp(Math.Max(24f, finiteRadius), screenRadius + 72f, expansion);
        float time = Main.GlobalTimeWrappedHourly;
        float noiseAmplitude = MathHelper.Lerp(46f, 24f, expansion)
            * (0.88f + (float)Math.Sin(time * 4.1f) * 0.12f);
        float backdropReveal = MathHelper.SmoothStep(0f, 1f,
            MathHelper.Clamp(progress / 0.14f, 0f, 1f)) * domainReveal;

        if (drawBackdrop)
        {
            DrawClippedFullScreenBackdrop(
                batch,
                center,
                radius,
                time * 1.15f,
                31.7f,
                noiseAmplitude,
                mastery,
                backdropReveal,
                destination,
                destinationWidth,
                destinationHeight);
        }

        if (drawBoundary)
        {
            float edgeFade = (float)Math.Sin(progress * MathHelper.Pi);
            edgeFade = (float)Math.Sqrt(Math.Max(0f, edgeFade)) * domainReveal;
            DrawNoiseBoundary(batch, center, radius + 8f, -time * 1.38f,
                47.3f, noiseAmplitude * 1.16f, 10f, edgeFade * 0.22f,
                mastery, gaps: true);
            DrawNoiseBoundary(batch, center, radius, time * 1.15f,
                31.7f, noiseAmplitude, 5.5f, edgeFade * 0.92f,
                mastery, gaps: false);
            DrawNoiseBoundary(batch, center, radius - 10f, time * 1.72f,
                63.1f, noiseAmplitude * 0.58f, 2.4f, edgeFade * 0.48f,
                mastery, gaps: true);
        }
    }

    private static void DrawClippedFullScreenBackdrop(
        SpriteBatch batch,
        Vector2 center,
        float baseRadius,
        float noiseTime,
        float noiseSeed,
        float noiseAmplitude,
        float mastery,
        float reveal,
        Vector2 destination,
        float destinationWidth,
        float destinationHeight)
    {
        const int boundarySegments = 192;
        const int maximumBands = 1024;
        Span<Vector2> boundary = stackalloc Vector2[boundarySegments];
        float extent = 0f;
        for (int index = 0; index < boundarySegments; index++)
        {
            float angle = MathHelper.TwoPi * index / boundarySegments;
            float radius = Math.Max(1f,
                baseRadius + BoundaryNoise(angle, noiseTime, noiseSeed) * noiseAmplitude);
            boundary[index] = angle.ToRotationVector2() * radius;
            extent = Math.Max(extent, Math.Abs(boundary[index].Y));
        }

        int bands = Math.Clamp((int)Math.Ceiling(extent * 2f / 8f), 1, maximumBands);
        float bandHeight = extent * 2f / bands;
        Span<float> leftEdges = stackalloc float[bands];
        Span<float> rightEdges = stackalloc float[bands];
        for (int band = 0; band < bands; band++)
        {
            float scanY = -extent + (band + 0.5f) * bandHeight;
            float left = float.MaxValue;
            float right = float.MinValue;
            for (int edge = 0; edge < boundarySegments; edge++)
            {
                Vector2 start = boundary[edge];
                Vector2 end = boundary[(edge + 1) % boundarySegments];
                if ((start.Y <= scanY && end.Y > scanY)
                    || (end.Y <= scanY && start.Y > scanY))
                {
                    float amount = (scanY - start.Y) / (end.Y - start.Y);
                    float intersectionX = MathHelper.Lerp(start.X, end.X, amount);
                    left = Math.Min(left, intersectionX);
                    right = Math.Max(right, intersectionX);
                }
            }
            leftEdges[band] = left;
            rightEdges[band] = right;
        }

        float destinationRight = destination.X + destinationWidth;
        float destinationBottom = destination.Y + destinationHeight;
        for (int layer = 0; layer < 3; layer++)
        {
            float opacity = layer switch
            {
                0 => MathHelper.Lerp(0.88f, 1f, mastery) * reveal,
                1 => MathHelper.Lerp(0.72f, 0.9f, mastery) * reveal,
                _ => MathHelper.Lerp(0.78f, 0.96f, mastery) * reveal
            };
            Texture2D texture = DeathDomainBackdropTextureSystem.GetLayer(layer);
            DeathDomainBackdropTextureSystem.GetExpandedDomainSourceFrame(layer,
                out float sourceOffsetX, out float sourceOffsetY,
                out float viewWidth, out float viewHeight);

            for (int band = 0; band < bands; band++)
            {
                float left = leftEdges[band];
                float right = rightEdges[band];
                if (left >= right)
                    continue;

                float scanY = -extent + (band + 0.5f) * bandHeight;
                float drawLeft = Math.Max(destination.X, center.X + left);
                float drawRight = Math.Min(destinationRight, center.X + right);
                float drawTop = Math.Max(destination.Y,
                    center.Y + scanY - bandHeight * 0.5f);
                float drawBottom = Math.Min(destinationBottom,
                    center.Y + scanY + bandHeight * 0.5f);
                if (drawLeft >= drawRight || drawTop >= drawBottom)
                    continue;

                float normalizedLeft = (drawLeft - destination.X) / destinationWidth;
                float normalizedRight = (drawRight - destination.X) / destinationWidth;
                float normalizedTop = (drawTop - destination.Y) / destinationHeight;
                float normalizedBottom = (drawBottom - destination.Y) / destinationHeight;
                DrawBackdropBand(
                    batch,
                    texture,
                    new Vector2(drawLeft, drawTop),
                    drawRight - drawLeft,
                    drawBottom - drawTop,
                    sourceOffsetX + normalizedLeft * viewWidth,
                    sourceOffsetY + normalizedTop * viewHeight,
                    Math.Max(0.5f, (normalizedRight - normalizedLeft) * viewWidth),
                    Math.Max(0.5f, (normalizedBottom - normalizedTop) * viewHeight),
                    opacity);
            }
        }
    }

    private static void DrawFullScreenBackdrop(SpriteBatch batch, float mastery, float reveal)
    {
        DeathDomainBackdropTextureSystem.GetExpandedDomainDrawBounds(
            out Vector2 destination, out float destinationWidth,
            out float destinationHeight);
        for (int layer = 0; layer < 3; layer++)
        {
            float opacity = layer switch
            {
                0 => MathHelper.Lerp(0.88f, 1f, mastery) * reveal,
                1 => MathHelper.Lerp(0.72f, 0.9f, mastery) * reveal,
                _ => MathHelper.Lerp(0.78f, 0.96f, mastery) * reveal
            };
            Texture2D texture = DeathDomainBackdropTextureSystem.GetLayer(layer);
            DeathDomainBackdropTextureSystem.GetExpandedDomainSourceFrame(layer,
                out float sourceOffsetX, out float sourceOffsetY, out float viewWidth, out float viewHeight);

            DrawBackdropBand(
                batch,
                texture,
                destination,
                destinationWidth,
                destinationHeight,
                sourceOffsetX,
                sourceOffsetY,
                viewWidth,
                viewHeight,
                opacity);
        }

        DrawTransientBackdropBlackHoles(
            batch,
            destination + new Vector2(destinationWidth, destinationHeight) * 0.5f,
            new Vector2(destinationWidth, destinationHeight) * 0.5f,
            mastery,
            reveal,
            0f,
            Main.worldID);
    }

    private static void DrawTransientBackdropBlackHoles(
        SpriteBatch batch,
        Vector2 center,
        Vector2 halfExtent,
        float mastery,
        float domainReveal,
        float clipRadius,
        int seedGroup)
    {
        if (domainReveal <= 0.01f || halfExtent.X <= 8f || halfExtent.Y <= 8f)
            return;

        bool clipped = clipRadius > 0f;
        int largeCount = clipped ? 1 : 2;
        int smallCount = clipped ? 3 : 6;
        float minimumExtent = Math.Min(halfExtent.X, halfExtent.Y);
        float largeRadius = clipped
            ? MathHelper.Clamp(clipRadius * 0.13f, 13f, 52f)
            : MathHelper.Clamp(minimumExtent * 0.115f, 58f, 138f);
        float smallRadius = clipped
            ? MathHelper.Clamp(clipRadius * 0.058f, 7f, 25f)
            : MathHelper.Clamp(minimumExtent * 0.043f, 20f, 52f);

        for (int slot = 0; slot < largeCount + smallCount; slot++)
        {
            bool large = slot < largeCount;
            int seed = seedGroup * 37 + slot * 193 + (large ? 701 : 1301);
            if (!TryGetBackdropHoleAnimation(seed, large,
                    out float opacity, out float scale, out float collapse))
            {
                continue;
            }

            Vector2 position;
            if (clipped)
            {
                float angle = Hash01(seed + 11) * MathHelper.TwoPi;
                float distance = clipRadius * MathHelper.Lerp(
                    large ? 0.19f : 0.27f,
                    large ? 0.42f : 0.62f,
                    Hash01(seed + 23));
                position = center + angle.ToRotationVector2() * distance;
            }
            else
            {
                position = center + new Vector2(
                    MathHelper.Lerp(-0.76f, 0.76f, Hash01(seed + 11)) * halfExtent.X,
                    MathHelper.Lerp(-0.68f, 0.68f, Hash01(seed + 23)) * halfExtent.Y);
            }

            float finalOpacity = opacity * domainReveal * MathHelper.Lerp(0.72f, 1f, mastery);
            float rotation = Hash01(seed + 41) * MathHelper.TwoPi
                + Main.GlobalTimeWrappedHourly * (large ? 0.055f : -0.16f);
            if (large)
            {
                DrawLargeBackdropBlackHole(batch, position,
                    largeRadius * scale, rotation, finalOpacity, collapse, mastery, seed);
            }
            else
            {
                DrawSmallBackdropBlackHole(batch, position,
                    smallRadius * scale, rotation, finalOpacity, collapse, seed);
            }
        }
    }

    private static bool TryGetBackdropHoleAnimation(
        int seed,
        bool large,
        out float opacity,
        out float scale,
        out float collapse)
    {
        float cycle = large ? 780f : 570f;
        float visibleDuration = large ? 470f : 330f;
        float introDuration = large ? 58f : 40f;
        float outroDuration = large ? 152f : 112f;
        float offset = Hash01(seed + 67) * cycle;
        float age = PositiveModulo((float)Main.GameUpdateCount + offset, cycle);
        if (age >= visibleDuration)
        {
            opacity = 0f;
            scale = 0f;
            collapse = 0f;
            return false;
        }

        float intro = MathHelper.SmoothStep(0f, 1f, MathHelper.Clamp(age / introDuration, 0f, 1f));
        float outro = MathHelper.SmoothStep(0f, 1f,
            MathHelper.Clamp((visibleDuration - age) / outroDuration, 0f, 1f));
        opacity = Math.Min(intro, outro);
        collapse = 1f - outro;
        float openingScale = MathHelper.Lerp(0.08f, 1f, 1f - (float)Math.Pow(1f - intro, 3f));
        openingScale *= 1f + (float)Math.Sin(intro * MathHelper.Pi) * 0.12f;
        scale = openingScale * MathHelper.Lerp(0.18f, 1f, outro);
        return opacity > 0.005f && scale > 0.02f;
    }

    private static void DrawSmallBackdropBlackHole(
        SpriteBatch batch,
        Vector2 center,
        float radius,
        float rotation,
        float opacity,
        float collapse,
        int seed)
    {
        if (radius <= 1f || opacity <= 0.001f)
            return;

        DrawBackdropDomainBlackHole(batch, center, radius, rotation, opacity,
            collapse, 0.34f, seed, large: false);
    }

    private static void DrawLargeBackdropBlackHole(
        SpriteBatch batch,
        Vector2 center,
        float radius,
        float rotation,
        float opacity,
        float collapse,
        float mastery,
        int seed)
    {
        if (radius <= 2f || opacity <= 0.001f)
            return;

        DrawBackdropDomainBlackHole(batch, center, radius, rotation, opacity,
            collapse, mastery, seed, large: true);
    }

    private static void DrawBackdropDomainBlackHole(
        SpriteBatch batch,
        Vector2 center,
        float radius,
        float rotation,
        float opacity,
        float collapse,
        float mastery,
        int seed,
        bool large)
    {
        float time = Main.GlobalTimeWrappedHourly;
        float coreRadius = radius * (large ? 0.68f : 0.62f);
        float pulse = opacity * (0.88f + (float)Math.Sin(time * 2.7f + seed) * 0.08f);
        DeathDomainPrimitiveTextureSystem.DrawRadialGradient(batch, center,
            radius * (large ? 1.95f : 1.65f), new Color(42, 0, 16) * (opacity * 0.42f));
        DrawBlackHoleSuctionStreams(batch, center, radius, 1f, rotation,
            opacity * (1f - collapse * 0.38f), seed, large);

        // Five inward-curving arms deliberately mirror the player's event
        // horizon instead of the old unrelated crossed-orbit atom symbol.
        for (int arm = 0; arm < 5; arm++)
            DrawAccretionSpiral(batch, center, coreRadius * 0.96f, radius * 1.06f,
                time + rotation * 0.72f, arm, mastery, pulse);

        DeathDomainPrimitiveTextureSystem.DrawSmoothDisc(batch, center, coreRadius,
            new Color(0, 0, 2) * (opacity * 0.98f));
        DrawNoiseBoundary(batch, center, coreRadius + radius * 0.07f,
            time * 0.76f + rotation, seed * 0.013f, radius * 0.07f,
            large ? 3.2f : 2.1f, opacity * 0.34f, mastery, gaps: true);
        DrawNoiseBoundary(batch, center, coreRadius,
            -time * 0.94f + rotation, seed * 0.021f, radius * 0.045f,
            large ? 2.4f : 1.55f, opacity * 0.92f, mastery, gaps: false);
    }

    private static void DrawFilledEllipse(
        SpriteBatch batch,
        Vector2 center,
        float radiusX,
        float radiusY,
        Color color)
    {
        int bands = Math.Clamp((int)Math.Ceiling(radiusY), 6, 72);
        for (int band = 0; band < bands; band++)
        {
            float normalizedY = (band + 0.5f) / bands * 2f - 1f;
            float halfWidth = radiusX * (float)Math.Sqrt(Math.Max(0f, 1f - normalizedY * normalizedY));
            float y = center.Y + normalizedY * radiusY;
            DrawSegment(batch, new Vector2(center.X - halfWidth, y),
                new Vector2(halfWidth * 2f, 0f), color, Math.Max(1f, radiusY * 2f / bands + 0.6f));
        }
    }

    private static void DrawEllipseArc(
        SpriteBatch batch,
        Vector2 center,
        float radiusX,
        float radiusY,
        float rotation,
        float startAngle,
        float endAngle,
        int segments,
        Color color,
        float width)
    {
        Vector2 AxisPoint(float angle)
        {
            Vector2 local = new((float)Math.Cos(angle) * radiusX, (float)Math.Sin(angle) * radiusY);
            return center + local.RotatedBy(rotation);
        }

        Vector2 previous = AxisPoint(startAngle);
        for (int index = 1; index <= segments; index++)
        {
            float angle = MathHelper.Lerp(startAngle, endAngle, index / (float)segments);
            Vector2 current = AxisPoint(angle);
            DrawSegment(batch, previous, current - previous, color, width);
            previous = current;
        }
    }

    private static float Hash01(int seed)
    {
        double value = Math.Sin(seed * 12.9898 + 78.233) * 43758.5453;
        return (float)(value - Math.Floor(value));
    }

    private static void DrawDomainRippleWaves(SpriteBatch batch, Vector2 center,
        float radius, float time, float mastery, float pulse)
    {
        for (int wave = 0; wave < 3; wave++)
        {
            float travel = PositiveModulo(time * (0.22f + mastery * 0.08f)
                + wave / 3f, 1f);
            float waveRadius = radius + MathHelper.Lerp(8f, 64f, travel);
            float opacity = (1f - travel) * pulse * (0.14f + mastery * 0.08f);
            const int segments = 128;
            float firstRadius = waveRadius + (float)Math.Sin(-time * 2.1f + wave) * 2.6f;
            Vector2 previous = center + Vector2.UnitX * firstRadius;
            for (int segment = 1; segment <= segments; segment++)
            {
                float angle = MathHelper.TwoPi * segment / segments;
                float ripple = (float)Math.Sin(angle * 9f - time * 2.1f + wave * 1.7f) * 2.6f
                    + (float)Math.Sin(angle * 17f + time * 1.3f) * 0.9f;
                Vector2 current = center + angle.ToRotationVector2() * (waveRadius + ripple);
                DeathDomainPrimitiveTextureSystem.DrawSmoothStrip(batch, previous, current,
                    Color.Lerp(new Color(122, 4, 34), new Color(255, 54, 92), mastery)
                        * opacity,
                    MathHelper.Lerp(1.1f, 2.4f, 1f - travel));
                previous = current;
            }
        }
    }

    private static void DrawBlackHoleSuctionStreams(SpriteBatch batch, Vector2 center,
        float radius, float verticalScale, float rotation, float opacity,
        int seed, bool large)
    {
        int streams = large ? 10 : 6;
        int points = large ? 30 : 22;
        float time = Main.GlobalTimeWrappedHourly;
        for (int stream = 0; stream < streams; stream++)
        {
            float streamOffset = stream * MathHelper.TwoPi / streams + seed * 0.013f;
            float movingHead = 1f - PositiveModulo(time * (large ? 0.42f : 0.58f)
                + stream / (float)streams, 1f);
            Vector2 previous = Vector2.Zero;
            for (int point = 0; point < points; point++)
            {
                float inward = point / (points - 1f);
                float curved = inward * inward * (3f - 2f * inward);
                float localAngle = streamOffset - time * (large ? 0.18f : 0.27f)
                    + inward * (large ? 1.72f : 1.38f);
                float localRadius = MathHelper.Lerp(radius * (large ? 2.75f : 2.05f),
                    radius * 0.56f, curved);
                localRadius += (float)Math.Sin(localAngle * 5f + seed * 0.07f)
                    * radius * 0.025f * (1f - inward);
                Vector2 local = new((float)Math.Cos(localAngle) * localRadius,
                    (float)Math.Sin(localAngle) * localRadius * verticalScale);
                Vector2 current = center + local.RotatedBy(rotation);
                if (point > 0)
                {
                    float body = (float)Math.Sin(inward * MathHelper.Pi);
                    float glow = 1f - MathHelper.SmoothStep(0f, 1f,
                        MathHelper.Clamp(Math.Abs(inward - movingHead) / 0.20f, 0f, 1f));
                    // The near half of the tilted disc is brighter and thicker;
                    // the far half remains visible behind the black core, giving
                    // the accretion flow genuine front/back depth.
                    float nearSide = 0.55f + 0.45f * Math.Max(0f, (float)Math.Sin(localAngle));
                    Color color = Color.Lerp(new Color(58, 0, 12),
                        new Color(255, 42, 78), inward * 0.82f + glow * 0.18f);
                    float streamOpacity = opacity * body * nearSide * (0.18f + glow * 0.62f);
                    float width = MathHelper.Lerp(0.65f, large ? 3.6f : 2.2f, inward)
                        * (0.72f + glow * 0.38f);
                    DeathDomainPrimitiveTextureSystem.DrawSmoothStrip(batch, previous,
                        current, color * streamOpacity, width);
                }
                previous = current;
            }
        }
    }

    private static void DrawFilledNoiseDisc(
        SpriteBatch batch,
        Vector2 center,
        float baseRadius,
        float time,
        float seed,
        float noiseAmplitude,
        Color color)
    {
        const int boundarySegments = 192;
        float preferredBandHeight = MathHelper.Clamp(baseRadius / 240f, 3f, 9f);
        Span<Vector2> boundary = stackalloc Vector2[boundarySegments];
        float extent = 0f;
        for (int index = 0; index < boundarySegments; index++)
        {
            float angle = MathHelper.TwoPi * index / boundarySegments;
            float radius = Math.Max(1f, baseRadius + BoundaryNoise(angle, time, seed) * noiseAmplitude);
            boundary[index] = angle.ToRotationVector2() * radius;
            extent = Math.Max(extent, Math.Abs(boundary[index].Y));
        }

        int bands = Math.Max(1, (int)Math.Ceiling(extent * 2f / preferredBandHeight));
        float bandHeight = extent * 2f / bands;
        for (int index = 0; index < bands; index++)
        {
            float scanY = -extent + (index + 0.5f) * bandHeight;
            float left = float.MaxValue;
            float right = float.MinValue;
            for (int edge = 0; edge < boundarySegments; edge++)
            {
                Vector2 start = boundary[edge];
                Vector2 end = boundary[(edge + 1) % boundarySegments];
                if ((start.Y <= scanY && end.Y > scanY) || (end.Y <= scanY && start.Y > scanY))
                {
                    float amount = (scanY - start.Y) / (end.Y - start.Y);
                    float intersectionX = MathHelper.Lerp(start.X, end.X, amount);
                    left = Math.Min(left, intersectionX);
                    right = Math.Max(right, intersectionX);
                }
            }

            if (left >= right)
                continue;

            Vector2 fillStart = new(center.X + left, center.Y + scanY);
            Vector2 fillEnd = new(center.X + right, center.Y + scanY);
            DeathDomainPrimitiveTextureSystem.DrawSmoothLine(batch, fillStart, fillEnd,
                color, bandHeight + 0.8f);
        }
    }

    private static void DrawNoiseBoundary(
        SpriteBatch batch,
        Vector2 center,
        float baseRadius,
        float time,
        float seed,
        float noiseAmplitude,
        float width,
        float opacity,
        float mastery,
        bool gaps)
    {
        const int segments = 256;
        float previousAngle = 0f;
        float previousRadius = baseRadius + BoundaryNoise(previousAngle, time, seed) * noiseAmplitude;
        Vector2 previous = center + Vector2.UnitX * previousRadius;
        Color darkRed = Color.Lerp(new Color(58, 0, 12), new Color(118, 0, 25), mastery);
        Color brightRed = Color.Lerp(new Color(225, 12, 42), new Color(255, 72, 105), mastery);

        for (int index = 1; index <= segments; index++)
        {
            float angle = MathHelper.TwoPi * index / segments;
            float noisyRadius = baseRadius + BoundaryNoise(angle, time, seed) * noiseAmplitude;
            Vector2 current = center + angle.ToRotationVector2() * noisyRadius;
            Vector2 segment = current - previous;
            float middleAngle = (previousAngle + angle) * 0.5f;
            float flow = 0.5f + 0.5f * (float)Math.Sin(middleAngle * 3f - time * 1.9f + seed);
            float arcMask = 0.58f + 0.42f * (float)Math.Sin(middleAngle * 2f + time * 0.47f + seed * 1.31f);
            arcMask *= 0.72f + 0.28f * (float)Math.Sin(middleAngle * 7f - time * 0.83f + seed * 0.73f);
            float visibility = gaps ? MathHelper.SmoothStep(0.08f, 0.62f, arcMask) : 1f;
            if (visibility > 0.025f)
            {
                Color color = Color.Lerp(darkRed, brightRed, flow);
                DrawSegment(batch, previous, segment, color * (opacity * visibility), width * (0.72f + flow * 0.42f));
            }

            previousAngle = angle;
            previous = current;
        }
    }

    private static float BoundaryNoise(float angle, float time, float seed)
    {
        float low = (float)Math.Sin(angle * 3f + time * 0.72f + seed) * 0.48f;
        float middle = (float)Math.Sin(angle * 7f - time * 1.11f + seed * 1.73f) * 0.29f;
        float high = (float)Math.Sin(angle * 13f + time * 1.67f + seed * 0.37f) * 0.16f;
        float grain = (float)Math.Sin(angle * 29f - time * 2.23f + seed * 2.17f) * 0.07f;
        return low + middle + high + grain;
    }

    private static void DrawInwardSuctionWaves(
        SpriteBatch batch,
        Vector2 center,
        float edgeRadius,
        float coreRadius,
        float time,
        float mastery,
        float pulse)
    {
        // Preserve the original long black-hole accretion lines. Only the animated
        // highlight direction is reversed so they now read as being swallowed by
        // the event horizon instead of being emitted from it.
        int streamCount = 7 + (int)Math.Round(mastery * 5f);
        float outerRadius = edgeRadius + MathHelper.Lerp(72f, 126f, mastery);
        for (int stream = 0; stream < streamCount; stream++)
        {
            float seed = stream * 4.731f + 2.4f;
            float movingHead = 1f - PositiveModulo(time * MathHelper.Lerp(0.48f, 0.78f, mastery)
                + stream / (float)streamCount, 1f);
            float baseAngle = stream * MathHelper.TwoPi / streamCount
                - time * (0.13f + mastery * 0.08f)
                + BoundaryNoise(stream, time * 0.18f, seed) * 0.06f;

            const int points = 46;
            Vector2 previous = Vector2.Zero;
            for (int point = 0; point < points; point++)
            {
                float inward = point / (points - 1f);
                float curved = inward * inward * (3f - 2f * inward);
                float angle = baseAngle + inward * MathHelper.Lerp(1.08f, 1.54f, mastery);
                float radius = MathHelper.Lerp(outerRadius, edgeRadius + 2f, curved)
                    + BoundaryNoise(angle, -time * 0.82f, seed) * MathHelper.Lerp(5.2f, 1.2f, inward);
                Vector2 current = center + angle.ToRotationVector2() * radius;
                if (point > 0)
                {
                    float bodyFade = (float)Math.Sin(inward * MathHelper.Pi);
                    float distanceFromHead = Math.Abs(inward - movingHead);
                    float movingGlow = 1f - MathHelper.SmoothStep(0.02f, 0.25f, distanceFromHead);
                    float opacity = bodyFade * pulse * (0.16f + movingGlow * 0.64f);
                    Color color = Color.Lerp(
                        new Color(18, 0, 8),
                        Color.Lerp(new Color(174, 3, 40), new Color(255, 34, 76), mastery),
                        inward * (0.45f + movingGlow * 0.55f));
                    float width = MathHelper.Lerp(0.55f, 3.4f + mastery * 1.7f, inward)
                        * (0.68f + movingGlow * 0.48f);
                    DrawSegment(batch, previous, current - previous, color * opacity, width);
                }
                previous = current;
            }
        }
    }

    private static void DrawOutwardPressureWaves(
        SpriteBatch batch,
        Vector2 center,
        float coreRadius,
        float time,
        float mastery,
        float pulse)
    {
        // This is the same continuous accretion-line construction used outside,
        // with radius progression, spiral turn and highlight travel all inverted.
        int streamCount = 7 + (int)Math.Round(mastery * 5f);
        float innerRadius = Math.Max(5f, coreRadius * 0.06f);
        float outerRadius = coreRadius * 0.90f;
        for (int stream = 0; stream < streamCount; stream++)
        {
            float seed = 42.7f + stream * 3.117f;
            float movingHead = PositiveModulo(time * MathHelper.Lerp(0.48f, 0.78f, mastery)
                + stream / (float)streamCount, 1f);
            float baseAngle = stream * MathHelper.TwoPi / streamCount
                + time * (0.13f + mastery * 0.08f)
                - BoundaryNoise(stream, time * 0.18f, seed) * 0.06f;

            const int points = 40;
            Vector2 previous = Vector2.Zero;
            for (int point = 0; point < points; point++)
            {
                float outward = point / (points - 1f);
                float curved = outward * outward * (3f - 2f * outward);
                float angle = baseAngle - outward * MathHelper.Lerp(1.08f, 1.54f, mastery);
                float radius = MathHelper.Lerp(innerRadius, outerRadius, curved)
                    + BoundaryNoise(angle, time * 0.82f, seed) * MathHelper.Lerp(1.2f, 4.6f, outward);
                Vector2 current = center + angle.ToRotationVector2() * radius;
                if (point > 0)
                {
                    float bodyFade = (float)Math.Sin(outward * MathHelper.Pi);
                    float distanceFromHead = Math.Abs(outward - movingHead);
                    float movingGlow = 1f - MathHelper.SmoothStep(0.02f, 0.25f, distanceFromHead);
                    float opacity = bodyFade * pulse * (0.13f + movingGlow * 0.58f);
                    Color color = Color.Lerp(
                        new Color(255, 116, 136),
                        Color.Lerp(new Color(112, 2, 28), new Color(220, 18, 58), mastery),
                        outward * 0.78f);
                    float width = MathHelper.Lerp(3.5f + mastery * 1.4f, 0.65f, outward)
                        * (0.66f + movingGlow * 0.46f);
                    DrawSegment(batch, previous, current - previous, color * opacity, width);
                }
                previous = current;
            }
        }
    }

    private static void DrawAccretionSpiral(SpriteBatch batch, Vector2 center, float coreRadius, float outerRadius, float time, int arm, float mastery, float pulse)
    {
        const int points = 34;
        float armOffset = arm * MathHelper.TwoPi / 5f;
        Vector2 previous = Vector2.Zero;
        for (int index = 0; index < points; index++)
        {
            float progress = index / (points - 1f);
            float angle = armOffset - time * (0.72f + arm * 0.035f) + progress * MathHelper.Pi * 1.12f;
            float radius = MathHelper.Lerp(coreRadius * 0.88f, outerRadius, progress);
            radius += BoundaryNoise(angle, time, arm * 2.7f) * 2.4f;
            Vector2 current = center + angle.ToRotationVector2() * radius;
            if (index > 0)
            {
                Vector2 segment = current - previous;
                Color color = Color.Lerp(new Color(255, 70, 92), new Color(82, 0, 22), progress);
                float fade = (float)Math.Sin(progress * MathHelper.Pi);
                DrawSegment(batch, previous, segment, color * (fade * pulse * 0.54f), MathHelper.Lerp(2.6f + mastery, 0.7f, progress));
            }
            previous = current;
        }
    }

    private static void DrawCircle(SpriteBatch batch, Vector2 center, float radius, int segments, Color color, float width, float rotation)
    {
        segments = Math.Max(segments,
            Math.Min(192, (int)Math.Ceiling(MathHelper.TwoPi * radius / 18f)));
        Vector2 previous = center + rotation.ToRotationVector2() * radius;
        for (int index = 1; index <= segments; index++)
        {
            float angle = rotation + MathHelper.TwoPi * index / segments;
            Vector2 current = center + angle.ToRotationVector2() * radius;
            DeathDomainPrimitiveTextureSystem.DrawSmoothStrip(batch, previous, current, color, width);
            previous = current;
        }
    }

    private static void DrawSegment(SpriteBatch batch, Vector2 start, Vector2 segment, Color color, float width)
    {
        DeathDomainPrimitiveTextureSystem.DrawSmoothStrip(batch,
            start, start + segment, color, width);
    }

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

    private static float GetMasteryProgress(int mastery)
    {
        // The character-owned Death Domain tree is packed into the legacy 5..75
        // visual range so existing shaders keep the same progression curve.
        return MathHelper.Clamp((mastery - 5f) / 70f, 0f, 1f);
    }

    private void ClearAnimationState()
    {
        Array.Clear(domainAnimation, 0, domainAnimation.Length);
        Array.Clear(descentAnimation, 0, descentAnimation.Length);
        Array.Clear(cachedRadius, 0, cachedRadius.Length);
        Array.Clear(cachedMastery, 0, cachedMastery.Length);
        Array.Clear(cachedFullScreen, 0, cachedFullScreen.Length);
    }
}