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 System;
using System.Collections.Generic;
using Terraria;
using Terraria.ModLoader;

namespace SoulHarvest.Common;

/// <summary>
/// High-resolution, seamless crescent surfaces shared by every non-Void reaper.
/// Each form keeps the Blood crescent's continuous silhouette while baking its
/// own material into the face. Textures are created lazily on the draw thread:
/// dedicated servers never own graphics resources.
/// </summary>
[Autoload(Side = ModSide.Client)]
internal sealed class ReaperCrescentPrimitiveTextureSystem : ModSystem
{
    private const int TextureSize = 768;
    private const int MaterialTextureSize = 512;
    private const float OuterRadius = 0.91f;
    private const int BloodLifecycleFrames = 16;
    private const int MaterialLifecycleFrames = 10;

    private static readonly Texture2D?[] bloodCrescents = new Texture2D?[BloodLifecycleFrames];
    private static readonly Texture2D?[] deathDomainCrescents = new Texture2D?[BloodLifecycleFrames];
    private static readonly Texture2D?[] deathDomainRimCrescents = new Texture2D?[BloodLifecycleFrames];
    private static readonly Dictionary<ReaperFormId, Texture2D?[]> formCrescents = [];
    private static Color[]? deathDomainSurface;
    private static Texture2D? deathDomainBlade;
    private static Texture2D? deathDomainRiftCrescent;

    public override void PostSetupContent()
    {
        if (Main.dedServ)
            return;

        // These procedural atlases used to be built synchronously by the first
        // Blood/Death swing. Prewarm them on the graphics thread after content
        // setup so combat never pays several million samples and GPU uploads in
        // a single frame. Dedicated servers remain completely texture-free.
        Main.QueueMainThreadAction(() =>
        {
            DeathDomainBackdropTextureSystem.Prewarm();
            for (int frame = 0; frame < BloodLifecycleFrames; frame++)
            {
                _ = GetBloodCrescent(frame);
                _ = GetDeathDomainRimCrescent(frame);
            }
        });
    }

    internal static void DrawFormCrescent(
        ReaperFormId form,
        Vector2 center,
        float rotation,
        float radius,
        float opacity,
        int motionLayers,
        int swingDirection,
        float time,
        float lifeProgress)
    {
        if (form == ReaperFormId.Blood)
        {
            DrawBloodCrescent(center, rotation, radius, opacity, motionLayers,
                swingDirection, time, lifeProgress);
            return;
        }
        if (form == ReaperFormId.Death)
        {
            DrawDeathDomainCrescent(center, rotation, radius, opacity, motionLayers,
                swingDirection, time, lifeProgress);
            return;
        }
        if (Main.dedServ || radius <= 1f || opacity <= 0.001f
            || form == ReaperFormId.Void)
        {
            return;
        }

        lifeProgress = MathHelper.Clamp(lifeProgress, 0f, 1f);
        float lifecycleFrame = lifeProgress * (MaterialLifecycleFrames - 1);
        int firstFrame = Math.Clamp((int)Math.Floor(lifecycleFrame), 0,
            MaterialLifecycleFrames - 1);
        int secondFrame = Math.Min(firstFrame + 1, MaterialLifecycleFrames - 1);
        float frameBlend = lifecycleFrame - firstFrame;
        Texture2D firstTexture = GetFormCrescent(form, firstFrame);
        Texture2D secondTexture = secondFrame == firstFrame
            ? firstTexture : GetFormCrescent(form, secondFrame);
        Vector2 screenCenter = center - Main.screenPosition;
        float scale = GetMaterialScale(radius);
        motionLayers = Math.Clamp(motionLayers, 0, 4);
        SpriteEffects effects = swingDirection < 0
            ? SpriteEffects.FlipVertically : SpriteEffects.None;
        Color aura = ReaperCombatRegistry.GetPrimaryColor(form) with { A = 0 };

        for (int layer = motionLayers; layer >= 1; layer--)
        {
            float lag = swingDirection * (0.015f + layer * 0.017f);
            DrawBloodFramePair(firstTexture, secondTexture, frameBlend,
                screenCenter, rotation - lag, scale * (1f - layer * 0.014f),
                aura * (opacity * (0.030f + layer * 0.012f)), effects);
        }
        float pulse = 0.982f + (float)Math.Sin(time * 6.4f + (int)form) * 0.012f;
        DrawBloodFramePair(firstTexture, secondTexture, frameBlend,
            screenCenter, rotation, scale * 1.018f,
            aura * (opacity * 0.16f), effects);
        DrawBloodFramePair(firstTexture, secondTexture, frameBlend,
            screenCenter, rotation, scale * pulse, Color.White * opacity, effects);
    }

    internal static void DrawBloodCrescent(
        Vector2 center,
        float rotation,
        float radius,
        float opacity,
        int motionLayers,
        int swingDirection,
        float time,
        float lifeProgress)
    {
        if (Main.dedServ || radius <= 1f || opacity <= 0.001f)
            return;

        lifeProgress = MathHelper.Clamp(lifeProgress, 0f, 1f);
        float lifecycleFrame = lifeProgress * (BloodLifecycleFrames - 1);
        int firstFrame = Math.Clamp((int)Math.Floor(lifecycleFrame), 0, BloodLifecycleFrames - 1);
        int secondFrame = Math.Min(firstFrame + 1, BloodLifecycleFrames - 1);
        float frameBlend = lifecycleFrame - firstFrame;
        Texture2D firstTexture = GetBloodCrescent(firstFrame);
        Texture2D secondTexture = secondFrame == firstFrame
            ? firstTexture
            : GetBloodCrescent(secondFrame);
        float scale = GetScale(radius);
        Vector2 screenCenter = center - Main.screenPosition;
        motionLayers = Math.Clamp(motionLayers, 0, 4);
        SpriteEffects effects = swingDirection < 0
            ? SpriteEffects.FlipVertically
            : SpriteEffects.None;

        // Reference-style motion exposure: translucent copies sit behind one
        // continuous surface rather than breaking the blade into radial blocks.
        for (int layer = motionLayers; layer >= 1; layer--)
        {
            float layerOpacity = opacity * (0.055f + layer * 0.018f);
            float rotationLag = swingDirection * (0.018f + layer * 0.018f);
            float breathing = 1f - layer * 0.018f;
            DrawBloodFramePair(firstTexture, secondTexture, frameBlend,
                screenCenter, rotation - rotationLag, scale * breathing,
                Color.White * layerOpacity, effects);
        }

        float pulse = 0.96f + (float)Math.Sin(time * 8.2f) * 0.015f;
        DrawBloodFramePair(firstTexture, secondTexture, frameBlend,
            screenCenter, rotation, scale * 1.018f,
            new Color(255, 34, 42, 0) * (opacity * 0.18f), effects);
        DrawBloodFramePair(firstTexture, secondTexture, frameBlend,
            screenCenter, rotation, scale * pulse, Color.White * opacity, effects);
    }

    internal static void DrawDeathDomainCrescent(
        Vector2 center,
        float rotation,
        float radius,
        float opacity,
        int motionLayers,
        int swingDirection,
        float time,
        float lifeProgress)
    {
        if (Main.dedServ || radius <= 1f || opacity <= 0.001f)
            return;

        lifeProgress = MathHelper.Clamp(lifeProgress, 0f, 1f);
        float lifecycleFrame = lifeProgress * (BloodLifecycleFrames - 1);
        int firstFrame = Math.Clamp((int)Math.Floor(lifecycleFrame), 0,
            BloodLifecycleFrames - 1);
        int secondFrame = Math.Min(firstFrame + 1, BloodLifecycleFrames - 1);
        float frameBlend = lifecycleFrame - firstFrame;
        Texture2D firstTexture = GetDeathDomainCrescent(firstFrame);
        Texture2D secondTexture = secondFrame == firstFrame
            ? firstTexture
            : GetDeathDomainCrescent(secondFrame);
        float scale = GetScale(radius);
        Vector2 screenCenter = center - Main.screenPosition;
        motionLayers = Math.Clamp(motionLayers, 0, 4);
        SpriteEffects effects = swingDirection < 0
            ? SpriteEffects.FlipVertically
            : SpriteEffects.None;

        for (int layer = motionLayers; layer >= 1; layer--)
        {
            float layerOpacity = opacity * (0.042f + layer * 0.014f);
            float lag = swingDirection * (0.016f + layer * 0.017f);
            DrawBloodFramePair(firstTexture, secondTexture, frameBlend,
                screenCenter, rotation - lag, scale * (1f - layer * 0.014f),
                new Color(194, 16, 61, 0) * layerOpacity, effects);
        }

        DrawBloodFramePair(firstTexture, secondTexture, frameBlend,
            screenCenter, rotation, scale,
            new Color(235, 20, 68, 0) * (opacity * 0.16f), effects);
        DrawBloodFramePair(firstTexture, secondTexture, frameBlend,
            screenCenter, rotation, scale, Color.White * opacity, effects);
    }

    internal static void DrawDeathDomainCrescentRim(
        Vector2 center,
        float rotation,
        float radius,
        float opacity,
        int motionLayers,
        int swingDirection,
        float time,
        float lifeProgress)
    {
        if (Main.dedServ || radius <= 1f || opacity <= 0.001f)
            return;

        lifeProgress = MathHelper.Clamp(lifeProgress, 0f, 1f);
        float lifecycleFrame = lifeProgress * (BloodLifecycleFrames - 1);
        int firstFrame = Math.Clamp((int)Math.Floor(lifecycleFrame), 0,
            BloodLifecycleFrames - 1);
        int secondFrame = Math.Min(firstFrame + 1, BloodLifecycleFrames - 1);
        float frameBlend = lifecycleFrame - firstFrame;
        Texture2D firstTexture = GetDeathDomainRimCrescent(firstFrame);
        Texture2D secondTexture = secondFrame == firstFrame
            ? firstTexture
            : GetDeathDomainRimCrescent(secondFrame);
        float scale = GetScale(radius);
        Vector2 screenCenter = center - Main.screenPosition;
        motionLayers = Math.Clamp(motionLayers, 0, 4);
        SpriteEffects effects = swingDirection < 0
            ? SpriteEffects.FlipVertically
            : SpriteEffects.None;

        for (int layer = motionLayers; layer >= 1; layer--)
        {
            float lag = swingDirection * (0.016f + layer * 0.017f);
            DrawBloodFramePair(firstTexture, secondTexture, frameBlend,
                screenCenter, rotation - lag, scale,
                new Color(208, 12, 54, 0)
                    * (opacity * (0.034f + layer * 0.012f)), effects);
        }

        DrawBloodFramePair(firstTexture, secondTexture, frameBlend,
            screenCenter, rotation, scale,
            new Color(235, 20, 68, 0) * (opacity * 0.18f), effects);
        DrawBloodFramePair(firstTexture, secondTexture, frameBlend,
            screenCenter, rotation, scale, Color.White * opacity,
            effects);
    }

    internal static void DrawDeathDomainBlade(Vector2 screenCenter, float rotation,
        float length, float width, float opacity, float completion)
    {
        if (Main.dedServ || length <= 0.5f || width <= 0.5f
            || opacity <= 0.001f || completion <= 0.001f)
        {
            return;
        }
        Texture2D texture = deathDomainBlade ??= CreateDeathDomainBlade();
        completion = MathHelper.Clamp(completion, 0f, 1f);
        float visibleLength = Math.Max(0.5f, length * completion);
        float widthGrowth = MathHelper.Lerp(0.22f, 1f,
            SmoothStep(0f, 0.55f, completion));
        Vector2 start = screenCenter
            - rotation.ToRotationVector2() * visibleLength * 0.5f;
        Main.EntitySpriteDraw(texture, start, null, Color.White * opacity,
            rotation, new Vector2(0f, texture.Height * 0.5f),
            new Vector2(visibleLength / texture.Width,
                width * widthGrowth / texture.Height),
            SpriteEffects.None, 0f);
    }

    internal static void DrawDeathDomainRiftCrescent(Vector2 center, float rotation,
        float radius, float opacity, int swingDirection)
    {
        if (Main.dedServ || radius <= 1f || opacity <= 0.001f)
            return;
        Texture2D texture = deathDomainRiftCrescent ??= CreateDeathDomainRiftCrescent();
        SpriteEffects effects = swingDirection < 0
            ? SpriteEffects.FlipVertically : SpriteEffects.None;
        Vector2 screenCenter = center - Main.screenPosition;
        float scale = GetScale(radius);
        DrawTexture(texture, screenCenter, rotation, scale * 1.035f,
            new Color(188, 9, 54, 0) * (opacity * 0.28f), effects);
        DrawTexture(texture, screenCenter, rotation, scale, Color.White * opacity,
            effects);
    }

    public override void Unload()
    {
        Texture2D?[] oldBlood = new Texture2D?[BloodLifecycleFrames];
        Array.Copy(bloodCrescents, oldBlood, BloodLifecycleFrames);
        Texture2D?[] oldDeath = new Texture2D?[BloodLifecycleFrames];
        Array.Copy(deathDomainCrescents, oldDeath, BloodLifecycleFrames);
        Texture2D?[] oldDeathRims = new Texture2D?[BloodLifecycleFrames];
        Array.Copy(deathDomainRimCrescents, oldDeathRims, BloodLifecycleFrames);
        Texture2D? oldDeathBlade = deathDomainBlade;
        Texture2D? oldDeathRift = deathDomainRiftCrescent;
        List<Texture2D?> oldForms = [];
        foreach (Texture2D?[] textures in formCrescents.Values)
            oldForms.AddRange(textures);
        Array.Clear(bloodCrescents, 0, bloodCrescents.Length);
        Array.Clear(deathDomainCrescents, 0, deathDomainCrescents.Length);
        Array.Clear(deathDomainRimCrescents, 0, deathDomainRimCrescents.Length);
        formCrescents.Clear();
        deathDomainSurface = null;
        deathDomainBlade = null;
        deathDomainRiftCrescent = null;
        if (Main.dedServ)
            return;

        Main.QueueMainThreadAction(() =>
        {
            foreach (Texture2D? texture in oldBlood)
                texture?.Dispose();
            foreach (Texture2D? texture in oldDeath)
                texture?.Dispose();
            foreach (Texture2D? texture in oldDeathRims)
                texture?.Dispose();
            foreach (Texture2D? texture in oldForms)
                texture?.Dispose();
            oldDeathBlade?.Dispose();
            oldDeathRift?.Dispose();
        });
    }

    private static Texture2D GetBloodCrescent(int frame)
    {
        return bloodCrescents[frame] ??= CreateBloodCrescent(
            frame / (float)(BloodLifecycleFrames - 1));
    }

    private static Texture2D GetDeathDomainCrescent(int frame)
    {
        return deathDomainCrescents[frame] ??= CreateDeathDomainCrescent(
            frame / (float)(BloodLifecycleFrames - 1));
    }

    private static Texture2D GetDeathDomainRimCrescent(int frame)
    {
        return deathDomainRimCrescents[frame] ??= CreateDeathDomainRimCrescent(
            frame / (float)(BloodLifecycleFrames - 1));
    }

    private static Texture2D GetFormCrescent(ReaperFormId form, int frame)
    {
        if (!formCrescents.TryGetValue(form, out Texture2D?[]? textures))
        {
            textures = new Texture2D?[MaterialLifecycleFrames];
            formCrescents[form] = textures;
        }
        return textures[frame] ??= CreateFormCrescent(form,
            frame / (float)(MaterialLifecycleFrames - 1));
    }

    private static Texture2D CreateFormCrescent(ReaperFormId form, float lifeProgress)
    {
        Texture2D texture = new(Main.instance.GraphicsDevice, MaterialTextureSize,
            MaterialTextureSize,
            false, SurfaceFormat.Color);
        Color[] pixels = new Color[MaterialTextureSize * MaterialTextureSize];
        float antialias = 3f / MaterialTextureSize;
        float reveal = Smooth01(lifeProgress);

        for (int y = 0; y < MaterialTextureSize; y++)
        {
            float normalizedY = (y + 0.5f) / MaterialTextureSize * 2f - 1f;
            for (int x = 0; x < MaterialTextureSize; x++)
            {
                float normalizedX = (x + 0.5f) / MaterialTextureSize * 2f - 1f;
                CrescentSample sample = SampleCrescent(normalizedX, normalizedY,
                    halfSweep: 2.43f, maximumThickness:
                        ReaperCombatRegistry.StandardCrescentThicknessRatio,
                    innerWarp: 0.015f, asymmetry: 0.22f, antialias);
                if (sample.Body <= 0.001f)
                    continue;
                float revealMask = 1f - SmoothStep(reveal - 0.018f,
                    reveal + 0.012f, sample.Progress);
                float body = sample.Body * revealMask;
                if (body <= 0.001f)
                    continue;

                Vector3 color;
                float materialAlpha;
                SampleFormMaterial(form, sample.Progress, sample.Depth,
                    out color, out materialAlpha);
                float outerGlow = sample.OuterGlow * revealMask;
                float outerHot = sample.OuterHot * revealMask;
                Vector3 rim = form switch
                {
                    ReaperFormId.Bone => new Vector3(0.94f, 1f, 0.89f),
                    ReaperFormId.Infernal => new Vector3(1f, 0.94f, 0.55f),
                    ReaperFormId.Frost => new Vector3(0.91f, 1f, 1f),
                    ReaperFormId.Soul => new Vector3(0.75f, 1f, 1f),
                    _ => new Vector3(0.84f, 1f, 1f)
                };
                Vector3 glow = ReaperCombatRegistry.GetPrimaryColor(form).ToVector3();
                color = Vector3.Lerp(color, glow, outerGlow * 0.68f);
                color = Vector3.Lerp(color, rim, outerHot * 0.94f);
                float alpha = Math.Max(body * materialAlpha,
                    Math.Max(outerGlow * 0.88f, outerHot));
                pixels[y * MaterialTextureSize + x] = Premultiplied(color, alpha);
            }
        }

        texture.SetData(pixels);
        return texture;
    }

    private static void SampleFormMaterial(ReaperFormId form, float progress,
        float depth, out Vector3 color, out float alpha)
    {
        switch (form)
        {
            case ReaperFormId.Bone:
            {
                float joint = Ridge((float)Math.Sin(progress * 45f + 0.4f), 13f);
                float marrow = Ridge((float)Math.Sin(progress * 22f
                    - depth * 13f + 1.1f), 8f);
                color = Vector3.Lerp(new Vector3(0.025f, 0.20f, 0.25f),
                    new Vector3(0.91f, 0.91f, 0.72f),
                    MathHelper.Clamp(0.20f + joint * 0.62f
                        + marrow * (1f - depth) * 0.30f, 0f, 1f));
                alpha = MathHelper.Lerp(0.82f, 0.58f, depth);
                break;
            }
            case ReaperFormId.Infernal:
            {
                float lava = Ridge((float)Math.Sin(progress * 30f
                    + depth * 19f + (float)Math.Sin(progress * 9f)), 8f);
                float heat = MathHelper.Clamp(lava * 0.82f
                    + (1f - depth) * 0.25f, 0f, 1f);
                color = Vector3.Lerp(new Vector3(0.035f, 0.006f, 0.002f),
                    new Vector3(1f, 0.42f, 0.025f), heat);
                color = Vector3.Lerp(color, new Vector3(1f, 0.96f, 0.56f),
                    lava * lava * 0.58f);
                alpha = MathHelper.Lerp(0.92f, 0.68f, depth);
                break;
            }
            case ReaperFormId.Frost:
            {
                float facetA = Math.Abs((float)Math.Sin(progress * 27f + depth * 8f));
                float facetB = Math.Abs((float)Math.Sin(progress * 13f - depth * 17f));
                float facet = MathHelper.Clamp(facetA * 0.52f + facetB * 0.38f, 0f, 1f);
                color = Vector3.Lerp(new Vector3(0.025f, 0.17f, 0.38f),
                    new Vector3(0.62f, 0.94f, 1f), facet);
                color = Vector3.Lerp(color, Vector3.One,
                    Ridge((float)Math.Sin(progress * 34f - depth * 24f), 14f) * 0.62f);
                alpha = MathHelper.Lerp(0.72f, 0.43f, depth);
                break;
            }
            case ReaperFormId.Soul:
            {
                float stream = Ridge((float)Math.Sin(progress * 21f
                    - depth * 26f + (float)Math.Sin(progress * 8f) * 1.4f), 6f);
                float echo = Ridge((float)Math.Sin(progress * 38f
                    + depth * 15f + 2.2f), 12f);
                color = Vector3.Lerp(new Vector3(0.10f, 0.018f, 0.28f),
                    new Vector3(0.34f, 0.28f, 1f), stream * 0.72f);
                color = Vector3.Lerp(color, new Vector3(0.22f, 1f, 1f),
                    echo * 0.62f);
                alpha = MathHelper.Lerp(0.76f, 0.40f, depth);
                break;
            }
            default:
            {
                float spirit = Ridge((float)Math.Sin(progress * 25f
                    - depth * 18f), 8f);
                color = Vector3.Lerp(new Vector3(0.025f, 0.18f, 0.21f),
                    new Vector3(0.50f, 0.96f, 1f), spirit * 0.68f);
                alpha = MathHelper.Lerp(0.68f, 0.38f, depth);
                break;
            }
        }
    }

    private static Texture2D CreateDeathDomainCrescent(float lifeProgress)
    {
        Texture2D texture = new(Main.instance.GraphicsDevice, TextureSize, TextureSize,
            false, SurfaceFormat.Color);
        Color[] pixels = new Color[TextureSize * TextureSize];
        Color[] surface = deathDomainSurface ??= CreateDeathDomainSurface();
        float antialias = 3f / TextureSize;
        float reveal = Smooth01(lifeProgress);
        float erosion = Smooth01((lifeProgress - 0.30f) / 0.66f);
        float edgeFade = 1f - Smooth01((lifeProgress - 0.96f) / 0.04f);

        for (int y = 0; y < TextureSize; y++)
        {
            float normalizedY = (y + 0.5f) / TextureSize * 2f - 1f;
            for (int x = 0; x < TextureSize; x++)
            {
                float normalizedX = (x + 0.5f) / TextureSize * 2f - 1f;
                CrescentSample sample = SampleCrescent(normalizedX, normalizedY,
                    halfSweep: 2.43f, maximumThickness:
                        ReaperCombatRegistry.StandardCrescentThicknessRatio,
                    innerWarp: 0.018f, asymmetry: 0.22f, antialias);
                if (sample.Body <= 0.001f)
                    continue;
                float revealMask = 1f - SmoothStep(reveal - 0.018f,
                    reveal + 0.012f, sample.Progress);
                float integrity = SampleDeathInteriorIntegrity(sample.Progress,
                    sample.Depth, erosion);
                float body = sample.Body * revealMask * integrity;

                Vector4 scene = surface[y * TextureSize + x].ToVector4();
                Vector3 color = new(scene.X, scene.Y, scene.Z);
                float alpha = body;
                float crimsonVein = Ridge((float)Math.Sin(sample.Progress * 21f
                    - sample.Depth * 18f + 0.9f), 9f);
                color = Vector3.Lerp(color, new Vector3(0.66f, 0.008f, 0.09f),
                    crimsonVein * 0.18f * body);

                // The outer cutting edge is never eroded. Only the domain-filled
                // face tears away, keeping one continuous hot blade silhouette.
                float redRim = sample.OuterGlow * revealMask * edgeFade;
                float whiteRim = sample.OuterHot * revealMask * edgeFade;
                color = Vector3.Lerp(color, new Vector3(0.92f, 0.025f, 0.12f),
                    redRim * 0.78f);
                color = Vector3.Lerp(color, new Vector3(1f, 0.88f, 0.84f),
                    whiteRim * 0.90f);
                alpha = Math.Max(alpha, Math.Max(redRim * 0.90f,
                    whiteRim * 0.98f));
                if (alpha <= 0.001f)
                    continue;
                pixels[y * TextureSize + x] = Premultiplied(color, alpha);
            }
        }

        texture.SetData(pixels);
        return texture;
    }

    private static Texture2D CreateDeathDomainRimCrescent(float lifeProgress)
    {
        Texture2D texture = new(Main.instance.GraphicsDevice, TextureSize,
            TextureSize, false, SurfaceFormat.Color);
        Color[] pixels = new Color[TextureSize * TextureSize];
        float antialias = 3f / TextureSize;
        float reveal = Smooth01(lifeProgress);
        float edgeFade = 1f - Smooth01((lifeProgress - 0.96f) / 0.04f);

        for (int y = 0; y < TextureSize; y++)
        {
            float normalizedY = (y + 0.5f) / TextureSize * 2f - 1f;
            for (int x = 0; x < TextureSize; x++)
            {
                float normalizedX = (x + 0.5f) / TextureSize * 2f - 1f;
                CrescentSample sample = SampleCrescent(normalizedX, normalizedY,
                    halfSweep: 2.43f, maximumThickness:
                        ReaperCombatRegistry.StandardCrescentThicknessRatio,
                    innerWarp: 0.018f, asymmetry: 0.22f, antialias);
                if (sample.Body <= 0.001f)
                    continue;

                float revealMask = 1f - SmoothStep(reveal - 0.018f,
                    reveal + 0.012f, sample.Progress);
                float redRim = sample.OuterGlow * revealMask * edgeFade;
                float whiteRim = sample.OuterHot * revealMask * edgeFade;
                float alpha = Math.Max(redRim * 0.90f, whiteRim * 0.98f);
                if (alpha <= 0.001f)
                    continue;

                Vector3 color = Vector3.Lerp(new Vector3(0.92f, 0.025f, 0.12f),
                    new Vector3(1f, 0.88f, 0.84f), whiteRim * 0.94f);
                pixels[y * TextureSize + x] = Premultiplied(color, alpha);
            }
        }

        texture.SetData(pixels);
        return texture;
    }

    private static Color[] CreateDeathDomainSurface()
    {
        Color[] surface = new Color[TextureSize * TextureSize];
        for (int y = 0; y < TextureSize; y++)
        {
            float v = (y + 0.5f) / TextureSize;
            for (int x = 0; x < TextureSize; x++)
            {
                float u = (x + 0.5f) / TextureSize;
                surface[y * TextureSize + x] =
                    DeathDomainBackdropTextureSystem.SampleCompositePixel(
                        x * 2, y * 2, u, v);
            }
        }
        return surface;
    }

    private static Texture2D CreateDeathDomainBlade()
    {
        const int width = 1024;
        const int height = 256;
        Texture2D texture = new(Main.instance.GraphicsDevice, width, height,
            false, SurfaceFormat.Color);
        Color[] pixels = new Color[width * height];
        for (int y = 0; y < height; y++)
        {
            float vertical = Math.Abs((y + 0.5f) / height * 2f - 1f);
            float v = (y + 0.5f) / height;
            for (int x = 0; x < width; x++)
            {
                float progress = (x + 0.5f) / width;
                float taper = (float)Math.Pow(Math.Max(0f,
                    Math.Sin(progress * MathHelper.Pi)), 0.56f);
                taper *= 1f
                    + (float)Math.Sin(progress * MathHelper.TwoPi * 5.1f) * 0.035f
                    + (float)Math.Sin(progress * MathHelper.TwoPi * 13.7f) * 0.014f;
                float normalizedDistance = vertical / Math.Max(0.0001f, taper);
                float body = 1f - SmoothStep(0.965f, 1.018f,
                    normalizedDistance);
                if (body <= 0.001f)
                    continue;

                float u = progress;
                Vector4 sampled = DeathDomainBackdropTextureSystem
                    .SampleCompositePixel(x * 2, y * 3, u, v).ToVector4();
                Vector3 color = new(sampled.X, sampled.Y, sampled.Z);
                float redRim = SmoothStep(0.70f, 0.92f, normalizedDistance)
                    * (1f - SmoothStep(0.97f, 1.02f, normalizedDistance));
                float whiteRim = SmoothStep(0.86f, 0.965f, normalizedDistance)
                    * (1f - SmoothStep(0.985f, 1.02f, normalizedDistance));
                color = Vector3.Lerp(color, new Vector3(0.94f, 0.018f, 0.12f),
                    redRim * 0.86f);
                color = Vector3.Lerp(color, new Vector3(1f, 0.90f, 0.86f),
                    whiteRim * 0.88f);
                float alpha = Math.Max(body * 0.96f,
                    Math.Max(redRim * 0.92f, whiteRim));
                pixels[y * width + x] = Premultiplied(color, alpha);
            }
        }
        texture.SetData(pixels);
        return texture;
    }

    private static Texture2D CreateDeathDomainRiftCrescent()
    {
        Texture2D texture = new(Main.instance.GraphicsDevice, TextureSize, TextureSize,
            false, SurfaceFormat.Color);
        Color[] pixels = new Color[TextureSize * TextureSize];
        float antialias = 3f / TextureSize;
        for (int y = 0; y < TextureSize; y++)
        {
            float normalizedY = (y + 0.5f) / TextureSize * 2f - 1f;
            for (int x = 0; x < TextureSize; x++)
            {
                float normalizedX = (x + 0.5f) / TextureSize * 2f - 1f;
                CrescentSample sample = SampleCrescent(normalizedX, normalizedY,
                    halfSweep: 2.43f, maximumThickness: 0.15f,
                    innerWarp: 0.026f, asymmetry: 0.20f, antialias);
                if (sample.Body <= 0.001f)
                    continue;
                float integrity = SampleInteriorIntegrity(sample.Progress,
                    sample.Depth, 1f, 1.28f);
                Vector4 sampled = DeathDomainBackdropTextureSystem
                    .SampleCompositePixel(x * 2, y * 2,
                        (x + 0.5f) / TextureSize, (y + 0.5f) / TextureSize)
                    .ToVector4();
                Vector3 color = new(sampled.X, sampled.Y, sampled.Z);
                float redRim = sample.OuterGlow;
                float whiteRim = sample.OuterHot;
                color = Vector3.Lerp(color, new Vector3(0.94f, 0.012f, 0.12f),
                    redRim * 0.88f);
                color = Vector3.Lerp(color, new Vector3(1f, 0.88f, 0.84f),
                    whiteRim * 0.92f);
                float alpha = Math.Max(sample.Body * integrity * 0.94f,
                    Math.Max(redRim * 0.94f, whiteRim));
                pixels[y * TextureSize + x] = Premultiplied(color, alpha);
            }
        }
        texture.SetData(pixels);
        return texture;
    }

    private static Texture2D CreateBloodCrescent(float lifeProgress)
    {
        Texture2D texture = new(Main.instance.GraphicsDevice, TextureSize, TextureSize,
            false, SurfaceFormat.Color);
        Color[] pixels = new Color[TextureSize * TextureSize];
        float antialias = 3f / TextureSize;
        // The weapon pose uses the same smooth-step clock. Keeping the mask on
        // that clock makes the bright leading edge sit at the live blade tip
        // instead of revealing most of the crescent before the hand gets there.
        float reveal = Smooth01(lifeProgress);
        float erosion = Smooth01((lifeProgress - 0.26f) / 0.68f);

        for (int y = 0; y < TextureSize; y++)
        {
            float normalizedY = ((y + 0.5f) / TextureSize * 2f - 1f);
            for (int x = 0; x < TextureSize; x++)
            {
                float normalizedX = (x + 0.5f) / TextureSize * 2f - 1f;
                CrescentSample sample = SampleCrescent(normalizedX, normalizedY,
                    halfSweep: 2.43f, maximumThickness:
                        ReaperCombatRegistry.StandardCrescentThicknessRatio,
                    innerWarp: 0.018f, asymmetry: 0.22f, antialias);
                if (sample.Body <= 0.001f)
                    continue;

                // Only the arc already crossed by the moving blade is visible.
                // FlipVertically reverses this reveal for the opposite swing.
                float revealMask = 1f - SmoothStep(reveal - 0.018f, reveal + 0.012f, sample.Progress);
                float integrity = SampleInteriorIntegrity(sample.Progress,
                    sample.Depth, erosion, 1.12f) * revealMask;
                if (integrity <= 0.001f)
                    continue;

                float liquidA = Ridge((float)Math.Sin(
                    sample.Progress * 37f + sample.Depth * 15f
                    + (float)Math.Sin(sample.Progress * 12f) * 1.8f), 7f);
                float liquidB = Ridge((float)Math.Sin(
                    sample.Progress * 19f - sample.Depth * 24f + 1.7f), 11f);
                float flow = MathHelper.Clamp(liquidA * 0.52f + liquidB * 0.30f, 0f, 1f);

                Vector3 deepRed = new(0.19f, 0.002f, 0.018f);
                Vector3 outerRed = new(1f, 0.025f, 0.055f);
                Vector3 warmFlow = new(1f, 0.22f, 0.105f);
                Vector3 color = Vector3.Lerp(outerRed, deepRed,
                    (float)Math.Pow(sample.Depth, 0.70f));
                color = Vector3.Lerp(color, warmFlow, flow * 0.46f);

                float alpha = sample.Body * integrity * MathHelper.Lerp(0.78f, 0.52f, sample.Depth);
                float innerHeat = sample.InnerEdge * integrity * (0.18f + flow * 0.24f);
                color = Vector3.Lerp(color, new Vector3(1f, 0.09f, 0.08f), innerHeat);
                alpha = Math.Max(alpha, innerHeat * 0.72f);

                // Fractures consume only the liquid energy face. The hot cutting
                // edge remains continuous from tip to tip at every lifetime frame.
                float whiteRim = sample.OuterHot * revealMask;
                float redRim = sample.OuterGlow * revealMask;
                color = Vector3.Lerp(color, new Vector3(1f, 0.18f, 0.10f), redRim * 0.62f);
                color = Vector3.Lerp(color, new Vector3(1f, 0.94f, 0.79f), whiteRim);
                alpha = Math.Max(alpha, Math.Max(redRim * 0.82f, whiteRim * 0.98f));
                pixels[y * TextureSize + x] = Premultiplied(color, alpha);
            }
        }

        texture.SetData(pixels);
        return texture;
    }

    private static float SampleInteriorIntegrity(float progress, float depth,
        float erosion, float intensity)
    {
        if (erosion <= 0.015f)
            return 1f;

        float integrity = 1f;

        // Persistent bites grow from the inner edge. Their deterministic layout
        // keeps the crescent coherent from frame to frame instead of shimmering.
        for (int index = 0; index < 18; index++)
        {
            float hashA = Hash01((uint)index, 0xB10D51C1u);
            float hashB = Hash01((uint)index, 0xA63E218Fu);
            float center = 0.055f + hashA * 0.89f;
            float activation = 0.035f + index / 17f * 0.62f;
            float growth = Smooth01((erosion - activation) / 0.28f) * intensity;
            if (growth <= 0f)
                continue;

            float halfWidth = MathHelper.Lerp(0.018f, 0.052f, hashB) * (0.35f + growth * 0.65f);
            float horizontal = Math.Abs(progress - center) / Math.Max(0.001f, halfWidth);
            if (horizontal >= 1f)
                continue;

            float arch = (float)Math.Sqrt(Math.Max(0f, 1f - horizontal * horizontal));
            float biteDepth = MathHelper.Lerp(0.16f, 0.52f, hashA)
                * Math.Min(1f, growth) * arch;
            float boundary = 1f - biteDepth;
            integrity = Math.Min(integrity, 1f - SmoothStep(boundary - 0.018f, boundary + 0.018f, depth));
        }

        // Late frames tear several isolated wounds through the energy face. This
        // is what creates the irregular missing islands visible in the reference.
        for (int index = 0; index < 13; index++)
        {
            float hashA = Hash01((uint)index, 0xC04A711Du);
            float hashB = Hash01((uint)index, 0xF731A2E9u);
            float activation = 0.18f + index / 12f * 0.50f;
            float growth = Smooth01((erosion - activation) / 0.27f) * intensity;
            if (growth <= 0f)
                continue;

            float centerX = 0.08f + hashA * 0.84f;
            // Keep every isolated wound away from depth zero: the outer blade
            // rim may glow over a missing interior but is never itself severed.
            float centerY = 0.30f + hashB * 0.54f;
            float radiusX = MathHelper.Lerp(0.018f, 0.058f, hashB)
                * Math.Min(1f, growth);
            float radiusY = MathHelper.Lerp(0.055f, 0.15f, hashA)
                * Math.Min(1f, growth);
            float nx = (progress - centerX) / Math.Max(0.001f, radiusX);
            float ny = (depth - centerY) / Math.Max(0.001f, radiusY);
            float distance = (float)Math.Sqrt(nx * nx + ny * ny);
            integrity = Math.Min(integrity, SmoothStep(0.82f, 1.08f, distance));
        }

        return MathHelper.Clamp(integrity, 0f, 1f);
    }

    internal static float SampleDeathInteriorIntegrity(float progress,
        float depth, float erosion)
    {
        float bites = SampleInteriorIntegrity(progress, depth, erosion, 1.18f);
        if (erosion <= 0.24f)
            return bites;

        // A continuous low-frequency field turns the early isolated wounds into
        // one spreading tear.  At the end every point of the energy face has
        // crossed the threshold; the separately drawn hot cutting edge is not
        // sampled here and therefore never breaks into teeth or blocks.
        float waveA = (float)Math.Sin(progress * 19.7f + depth * 11.3f + 0.6f);
        float waveB = (float)Math.Sin(progress * 37.1f - depth * 16.9f + 2.2f);
        float waveC = (float)Math.Sin(progress * 8.3f + depth * 31.7f - 1.1f);
        float field = MathHelper.Clamp(0.50f + waveA * 0.19f
            + waveB * 0.11f + waveC * 0.07f, 0.12f, 0.88f);
        float dissolve = Smooth01((erosion - 0.24f) / 0.76f);
        float continuousIntegrity = 1f - SmoothStep(field - 0.09f,
            field + 0.09f, dissolve);
        return MathHelper.Clamp(bites * continuousIntegrity, 0f, 1f);
    }

    private static void DrawBloodFramePair(
        Texture2D first,
        Texture2D second,
        float blend,
        Vector2 center,
        float rotation,
        float scale,
        Color color,
        SpriteEffects effects)
    {
        if (blend < 0.999f)
            DrawTexture(first, center, rotation, scale, color * (1f - blend), effects);
        if (blend > 0.001f)
            DrawTexture(second, center, rotation, scale, color * blend, effects);
    }

    private static CrescentSample SampleCrescent(
        float x,
        float y,
        float halfSweep,
        float maximumThickness,
        float innerWarp,
        float asymmetry,
        float antialias)
    {
        float angle = (float)Math.Atan2(y, x);
        if (angle <= -halfSweep - 0.02f || angle >= halfSweep + 0.02f)
            return default;

        float progress = MathHelper.Clamp((angle + halfSweep) / (halfSweep * 2f), 0f, 1f);
        float taper = (float)Math.Pow(Math.Max(0f, Math.Sin(progress * MathHelper.Pi)), 0.67f);
        taper *= 1f + asymmetry * (progress * 2f - 1f);
        float thickness = maximumThickness * taper;
        if (thickness <= antialias * 0.35f)
            return default;

        float radius = (float)Math.Sqrt(x * x + y * y);
        float outerRadius = OuterRadius
            + (float)Math.Sin(progress * 29f + (float)Math.Sin(progress * 8f) * 1.3f)
            * innerWarp * 0.18f * taper;
        float innerDistortion = ((float)Math.Sin(progress * 23f + 0.7f)
            + (float)Math.Sin(progress * 47f - 1.4f) * 0.42f)
            * innerWarp * taper;
        float innerRadius = outerRadius - thickness + innerDistortion;
        float outerMask = 1f - SmoothStep(outerRadius - antialias, outerRadius + antialias, radius);
        float innerMask = SmoothStep(innerRadius - antialias, innerRadius + antialias, radius);
        float capMask = SmoothStep(0f, 0.018f, progress)
            * SmoothStep(0f, 0.018f, 1f - progress);
        float body = outerMask * innerMask * capMask;
        float depth = MathHelper.Clamp((outerRadius - radius) / Math.Max(antialias, thickness), 0f, 1f);

        float outerDistance = Math.Abs(radius - outerRadius);
        float innerDistance = Math.Abs(radius - innerRadius);
        float outerGlow = (1f - SmoothStep(0.020f, 0.052f, outerDistance)) * capMask;
        float outerHot = (1f - SmoothStep(0.004f, 0.014f, outerDistance)) * capMask;
        float innerEdge = (1f - SmoothStep(0.006f, 0.020f, innerDistance)) * capMask;
        return new CrescentSample(body, progress, depth, outerGlow, outerHot, innerEdge);
    }

    private static void DrawTexture(Texture2D texture, Vector2 center, float rotation,
        float scale, Color color, SpriteEffects effects = SpriteEffects.None)
    {
        Main.EntitySpriteDraw(texture, center, null, color, rotation,
            texture.Size() * 0.5f, scale, effects);
    }

    private static float GetScale(float radius)
        => radius / (TextureSize * 0.5f * OuterRadius);

    private static float GetMaterialScale(float radius)
        => radius / (MaterialTextureSize * 0.5f * OuterRadius);

    private static float Ridge(float sine, float sharpness)
        => (float)Math.Pow(MathHelper.Clamp((sine + 1f) * 0.5f, 0f, 1f), sharpness);

    private static Color Premultiplied(Vector3 color, float alpha)
    {
        alpha = MathHelper.Clamp(alpha, 0f, 1f);
        color *= alpha;
        return new Color(
            MathHelper.Clamp(color.X, 0f, 1f),
            MathHelper.Clamp(color.Y, 0f, 1f),
            MathHelper.Clamp(color.Z, 0f, 1f),
            alpha);
    }

    private static float SmoothStep(float start, float end, float value)
    {
        if (end <= start)
            return value >= end ? 1f : 0f;
        return Smooth01((value - start) / (end - start));
    }

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

    private static uint Hash(uint x, uint y, uint seed)
    {
        uint value = x * 374761393u + y * 668265263u + seed;
        value = (value ^ (value >> 13)) * 1274126177u;
        return value ^ (value >> 16);
    }

    private static float Hash01(uint index, uint seed)
        => (Hash(index, index * 17u + 5u, seed) & 0x00FFFFFFu) / 16777215f;

    private readonly record struct CrescentSample(
        float Body,
        float Progress,
        float Depth,
        float OuterGlow,
        float OuterHot,
        float InnerEdge);
}