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

namespace SoulHarvest.Common;

/// <summary>
/// Builds a high-resolution, antialiased double-pointed mask for death-domain
/// blades. Drawing a single stretched mask avoids the seams and blocky corners
/// produced by assembling a slash out of dozens of MagicPixel rectangles.
/// </summary>
[Autoload(Side = ModSide.Client)]
internal sealed class DeathDomainPrimitiveTextureSystem : ModSystem
{
    internal static Texture2D? BladeMask { get; private set; }
    internal static Texture2D? RadialGradientMask { get; private set; }
    internal static Texture2D? SmoothLineMask { get; private set; }
    internal static Texture2D? SmoothStripMask { get; private set; }
    internal static Texture2D? SmoothDiscMask { get; private set; }
    internal static Texture2D? BloodScarMask { get; private set; }
    internal static Texture2D? VoidRiftMask { get; private set; }

    private static Texture2D CreateBladeMask()
    {
        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);
            for (int x = 0; x < width; x++)
            {
                float horizontal = (x + 0.5f) / width;
                float envelope = (float)Math.Pow(Math.Max(0f, Math.Sin(horizontal * MathHelper.Pi)), 0.62f);
                float signedDistance = envelope <= 0.0001f ? 4f : vertical / envelope;
                float alpha = 1f - SmoothStep(0.955f, 1.035f, signedDistance);

                // Runtime textures are not processed by the content pipeline, so
                // store premultiplied white explicitly for clean tinted edges.
                byte value = (byte)Math.Clamp((int)Math.Round(alpha * 255f), 0, 255);
                pixels[y * width + x] = new Color(value, value, value, value);
            }
        }

        texture.SetData(pixels);
        return texture;
    }

    private static Texture2D CreateRadialGradientMask()
    {
        const int size = 512;
        Texture2D texture = new(Main.instance.GraphicsDevice, size, size, false, SurfaceFormat.Color);
        Color[] pixels = new Color[size * size];
        for (int y = 0; y < size; y++)
        {
            float normalizedY = (y + 0.5f) / size * 2f - 1f;
            for (int x = 0; x < size; x++)
            {
                float normalizedX = (x + 0.5f) / size * 2f - 1f;
                float distance = (float)Math.Sqrt(normalizedX * normalizedX + normalizedY * normalizedY);
                float falloff = MathHelper.Clamp(1f - distance, 0f, 1f);
                float alpha = falloff * falloff * (3f - 2f * falloff);
                alpha = (float)Math.Pow(alpha, 1.18f);
                byte value = (byte)Math.Clamp((int)Math.Round(alpha * 255f), 0, 255);
                pixels[y * size + x] = new Color(value, value, value, value);
            }
        }

        texture.SetData(pixels);
        return texture;
    }

    private static Texture2D CreateSmoothDiscMask()
    {
        const int size = 512;
        Texture2D texture = new(Main.instance.GraphicsDevice, size, size, false, SurfaceFormat.Color);
        Color[] pixels = new Color[size * size];
        for (int y = 0; y < size; y++)
        {
            float normalizedY = (y + 0.5f) / size * 2f - 1f;
            for (int x = 0; x < size; x++)
            {
                float normalizedX = (x + 0.5f) / size * 2f - 1f;
                float distance = (float)Math.Sqrt(normalizedX * normalizedX + normalizedY * normalizedY);
                float alpha = 1f - SmoothStep(0.982f, 1.006f, distance);
                byte value = (byte)Math.Clamp((int)Math.Round(alpha * 255f), 0, 255);
                pixels[y * size + x] = new Color(value, value, value, value);
            }
        }

        texture.SetData(pixels);
        return texture;
    }

    private static Texture2D CreateSmoothLineMask()
    {
        // The very wide aspect ratio keeps the antialiased end caps compact even
        // when a long trail segment is stretched across the screen. Unlike a
        // MagicPixel rectangle, both the sides and tips dissolve over several
        // source texels, so connected curves do not expose hard stair-steps.
        const int width = 1024;
        const int height = 96;
        Texture2D texture = new(Main.instance.GraphicsDevice, width, height, false, SurfaceFormat.Color);
        Color[] pixels = new Color[width * height];
        float capRadius = height * 0.5f - 2f;
        float leftCenter = capRadius + 1f;
        float rightCenter = width - capRadius - 1f;

        for (int y = 0; y < height; y++)
        {
            float dy = Math.Abs(y + 0.5f - height * 0.5f);
            for (int x = 0; x < width; x++)
            {
                float dx = x + 0.5f < leftCenter
                    ? leftCenter - (x + 0.5f)
                    : x + 0.5f > rightCenter
                        ? x + 0.5f - rightCenter
                        : 0f;
                float distance = (float)Math.Sqrt(dx * dx + dy * dy);
                float alpha = 1f - SmoothStep(capRadius - 2.25f, capRadius + 0.75f, distance);
                byte value = (byte)Math.Clamp((int)Math.Round(alpha * 255f), 0, 255);
                pixels[y * width + x] = new Color(value, value, value, value);
            }
        }

        texture.SetData(pixels);
        return texture;
    }

    private static Texture2D CreateSmoothStripMask()
    {
        // Connected curves must not use the rounded end caps from SmoothLineMask:
        // dozens of overlapping caps make every sampling point visibly brighter.
        // This mask is fully continuous along X and only antialiases its two long
        // edges, allowing short rotated spans to read as one uninterrupted ribbon.
        const int width = 256;
        const int height = 96;
        Texture2D texture = new(Main.instance.GraphicsDevice, width, height, false, SurfaceFormat.Color);
        Color[] pixels = new Color[width * height];
        float halfHeight = height * 0.5f - 1f;
        for (int y = 0; y < height; y++)
        {
            float distance = Math.Abs(y + 0.5f - height * 0.5f);
            float alpha = 1f - SmoothStep(halfHeight - 2.25f, halfHeight + 0.75f, distance);
            byte value = (byte)Math.Clamp((int)Math.Round(alpha * 255f), 0, 255);
            Color pixel = new(value, value, value, value);
            for (int x = 0; x < width; x++)
                pixels[y * width + x] = pixel;
        }

        texture.SetData(pixels);
        return texture;
    }

    private static Texture2D CreateBloodScarMask()
    {
        // One high-resolution mask owns the complete wound silhouette.  Its two
        // edges use continuous low-frequency waves, so stretching it across the
        // world cannot expose joins or the saw teeth caused by per-span quads.
        const int width = 2048;
        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 = (y + 0.5f) / height * 2f - 1f;
            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.46f);
                float wave = (float)Math.Sin(progress * MathHelper.TwoPi * 3.1f + 0.42f)
                    * 0.045f
                    + (float)Math.Sin(progress * MathHelper.TwoPi * 7.3f - 1.17f)
                    * 0.021f;
                float asymmetric = vertical >= 0f ? 1f + wave : 1f - wave * 0.72f;
                float boundary = Math.Max(0.0001f, taper * asymmetric);
                float distance = Math.Abs(vertical) / boundary;
                float alpha = 1f - SmoothStep(0.968f, 1.018f, distance);
                byte value = (byte)Math.Clamp((int)Math.Round(alpha * 255f), 0, 255);
                pixels[y * width + x] = new Color(value, value, value, value);
            }
        }
        texture.SetData(pixels);
        return texture;
    }

    private static Texture2D CreateVoidRiftMask()
    {
        // The complete giant rift is one mask. Continuous waves alter its center
        // and width across X, giving it a torn silhouette without assembling the
        // opening from independently rotated strips.
        const int width = 2048;
        const int height = 384;
        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 = (y + 0.5f) / height * 2f - 1f;
            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.42f);
                float centerWave = (float)Math.Sin(progress * MathHelper.TwoPi * 2.3f + 0.61f)
                    * 0.050f
                    + (float)Math.Sin(progress * MathHelper.TwoPi * 5.7f - 0.84f)
                    * 0.018f;
                float widthWave = 1f
                    + (float)Math.Sin(progress * MathHelper.TwoPi * 4.1f - 0.32f) * 0.085f
                    + (float)Math.Sin(progress * MathHelper.TwoPi * 10.7f + 1.11f) * 0.032f;
                float boundary = Math.Max(0.0001f, taper * widthWave);
                float distance = Math.Abs(vertical - centerWave * taper) / boundary;
                float alpha = 1f - SmoothStep(0.966f, 1.018f, distance);
                byte value = (byte)Math.Clamp((int)Math.Round(alpha * 255f), 0, 255);
                pixels[y * width + x] = new Color(value, value, value, value);
            }
        }
        texture.SetData(pixels);
        return texture;
    }

    public override void Unload()
    {
        Texture2D? bladeTexture = BladeMask;
        Texture2D? gradientTexture = RadialGradientMask;
        Texture2D? smoothLineTexture = SmoothLineMask;
        Texture2D? smoothStripTexture = SmoothStripMask;
        Texture2D? smoothDiscTexture = SmoothDiscMask;
        Texture2D? bloodScarTexture = BloodScarMask;
        Texture2D? voidRiftTexture = VoidRiftMask;
        BladeMask = null;
        RadialGradientMask = null;
        SmoothLineMask = null;
        SmoothStripMask = null;
        SmoothDiscMask = null;
        BloodScarMask = null;
        VoidRiftMask = null;
        if (!Main.dedServ && (bladeTexture is not null || gradientTexture is not null
            || smoothLineTexture is not null || smoothStripTexture is not null
            || smoothDiscTexture is not null || bloodScarTexture is not null
            || voidRiftTexture is not null))
        {
            Main.QueueMainThreadAction(() =>
            {
                bladeTexture?.Dispose();
                gradientTexture?.Dispose();
                smoothLineTexture?.Dispose();
                smoothStripTexture?.Dispose();
                smoothDiscTexture?.Dispose();
                bloodScarTexture?.Dispose();
                voidRiftTexture?.Dispose();
            });
        }
    }

    internal static void DrawBlade(
        Vector2 start,
        float rotation,
        float length,
        float width,
        Color color,
        float completion = 1f)
    {
        // Mod content hooks may run on a loader worker thread. PreDraw always runs
        // on the main thread, so lazy creation here satisfies FNA's graphics-thread
        // requirement and keeps dedicated servers entirely texture-free.
        Texture2D? texture = BladeMask;
        if (texture is null && !Main.dedServ)
        {
            texture = CreateBladeMask();
            BladeMask = texture;
        }
        completion = MathHelper.Clamp(completion, 0f, 1f);
        if (texture is null || length <= 0.05f || width <= 0.05f || completion <= 0.001f)
            return;

        float visibleLength = Math.Max(0.5f, length * completion);
        float widthGrowth = MathHelper.Lerp(0.22f, 1f, SmoothStep(0f, 0.55f, completion));
        Main.EntitySpriteDraw(
            texture,
            start,
            null,
            color,
            rotation,
            new Vector2(0f, texture.Height * 0.5f),
            new Vector2(visibleLength / texture.Width, width * widthGrowth / texture.Height),
            SpriteEffects.None,
            0f);
    }

    internal static void DrawBladeCentered(
        Vector2 center,
        float rotation,
        float length,
        float width,
        Color color,
        float completion = 1f)
    {
        completion = MathHelper.Clamp(completion, 0f, 1f);
        float visibleLength = Math.Max(0.5f, length * completion);
        Vector2 start = center - rotation.ToRotationVector2() * visibleLength * 0.5f;
        DrawBlade(start, rotation, length, width, color, completion);
    }

    internal static void DrawRadialGradient(SpriteBatch batch, Vector2 center, float radius, Color color)
    {
        if (Main.dedServ || radius <= 0.5f)
            return;

        Texture2D texture = RadialGradientMask ??= CreateRadialGradientMask();
        batch.Draw(
            texture,
            center,
            null,
            color,
            0f,
            texture.Size() * 0.5f,
            radius * 2f / texture.Width,
            SpriteEffects.None,
            0f);
    }

    internal static void DrawSmoothDisc(SpriteBatch batch, Vector2 center, float radius, Color color)
    {
        if (Main.dedServ || radius <= 0.5f)
            return;

        Texture2D texture = SmoothDiscMask ??= CreateSmoothDiscMask();
        batch.Draw(texture, center, null, color, 0f, texture.Size() * 0.5f,
            radius * 2f / texture.Width, SpriteEffects.None, 0f);
    }

    internal static void DrawSmoothLine(
        SpriteBatch batch,
        Vector2 start,
        Vector2 end,
        Color color,
        float width)
    {
        if (Main.dedServ || width <= 0.05f)
            return;

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

        Texture2D texture = SmoothLineMask ??= CreateSmoothLineMask();
        batch.Draw(
            texture,
            (start + end) * 0.5f,
            null,
            color,
            delta.ToRotation(),
            texture.Size() * 0.5f,
            new Vector2((length + Math.Min(width, 3f)) / texture.Width, width / texture.Height),
            SpriteEffects.None,
            0f);
    }

    internal static void DrawSmoothWorldLine(
        Vector2 start,
        Vector2 end,
        Color color,
        float width)
    {
        DrawSmoothLine(Main.spriteBatch, start - Main.screenPosition,
            end - Main.screenPosition, color, width);
    }

    internal static void DrawSmoothStrip(
        SpriteBatch batch,
        Vector2 start,
        Vector2 end,
        Color color,
        float width)
    {
        if (Main.dedServ || width <= 0.05f)
            return;

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

        Texture2D texture = SmoothStripMask ??= CreateSmoothStripMask();
        Vector2 direction = delta / length;
        // A sub-pixel overlap closes rotated joins without reintroducing the
        // conspicuous circular stamps produced by the capped line mask.
        float overlap = Math.Min(0.7f, width * 0.08f);
        start -= direction * overlap;
        end += direction * overlap;
        delta = end - start;
        length = delta.Length();
        batch.Draw(
            texture,
            (start + end) * 0.5f,
            null,
            color,
            delta.ToRotation(),
            texture.Size() * 0.5f,
            new Vector2(length / texture.Width, width / texture.Height),
            SpriteEffects.None,
            0f);
    }

    internal static void DrawSmoothWorldStrip(
        Vector2 start,
        Vector2 end,
        Color color,
        float width)
    {
        DrawSmoothStrip(Main.spriteBatch, start - Main.screenPosition,
            end - Main.screenPosition, color, width);
    }

    internal static void DrawBloodScar(
        Vector2 center,
        float rotation,
        float length,
        float width,
        float opacity,
        float reveal = 1f)
    {
        if (Main.dedServ || length <= 0.5f || width <= 0.5f || opacity <= 0.001f)
            return;
        Texture2D texture = BloodScarMask ??= CreateBloodScarMask();
        reveal = MathHelper.Clamp(reveal, 0f, 1f);
        int visibleWidth = Math.Max(1, (int)Math.Round(texture.Width * reveal));
        Rectangle source = new(0, 0, visibleWidth, texture.Height);
        Vector2 position = center - rotation.ToRotationVector2() * length * 0.5f
            - Main.screenPosition;
        Vector2 origin = new(0f, texture.Height * 0.5f);
        Vector2 scale = new(length / texture.Width, width / texture.Height);

        Main.EntitySpriteDraw(texture, position, source,
            new Color(118, 0, 20, 0) * (opacity * 0.46f), rotation, origin,
            new Vector2(scale.X, scale.Y * 1.42f), SpriteEffects.None);
        Main.EntitySpriteDraw(texture, position, source,
            new Color(250, 20, 54, 235) * (opacity * 0.96f), rotation, origin,
            new Vector2(scale.X, scale.Y * 1.13f), SpriteEffects.None);
        Main.EntitySpriteDraw(texture, position, source,
            new Color(7, 0, 3, 252) * opacity, rotation, origin, scale,
            SpriteEffects.None);
        Main.EntitySpriteDraw(texture, position, source,
            new Color(31, 0, 9, 240) * (opacity * 0.82f), rotation, origin,
            new Vector2(scale.X, scale.Y * 0.56f), SpriteEffects.None);
    }

    internal static void DrawVoidRift(
        Vector2 center,
        float rotation,
        float length,
        float width,
        float opacity,
        float reveal = 1f)
    {
        if (Main.dedServ || length <= 0.5f || width <= 0.5f || opacity <= 0.001f)
            return;
        Texture2D texture = VoidRiftMask ??= CreateVoidRiftMask();
        reveal = MathHelper.Clamp(reveal, 0f, 1f);
        int visibleWidth = Math.Max(1, (int)Math.Round(texture.Width * reveal));
        Rectangle source = new(0, 0, visibleWidth, texture.Height);
        Vector2 position = center - rotation.ToRotationVector2() * length * 0.5f
            - Main.screenPosition;
        Vector2 origin = new(0f, texture.Height * 0.5f);
        Vector2 scale = new(length / texture.Width, width / texture.Height);

        Main.EntitySpriteDraw(texture, position, source,
            new Color(54, 3, 88, 0) * (opacity * 0.42f), rotation, origin,
            new Vector2(scale.X, scale.Y * 1.25f), SpriteEffects.None);
        Main.EntitySpriteDraw(texture, position, source,
            new Color(151, 42, 226, 235) * (opacity * 0.88f), rotation, origin,
            new Vector2(scale.X, scale.Y * 1.08f), SpriteEffects.None);
        Main.EntitySpriteDraw(texture, position, source,
            new Color(2, 0, 6, 252) * opacity, rotation, origin, scale,
            SpriteEffects.None);
        Main.EntitySpriteDraw(texture, position, source,
            new Color(10, 2, 18, 232) * (opacity * 0.78f), rotation, origin,
            new Vector2(scale.X, scale.Y * 0.52f), SpriteEffects.None);
    }

    internal static void DrawDeathPortalRift(
        Vector2 center,
        float rotation,
        float length,
        float width,
        float opacity)
    {
        if (Main.dedServ || length <= 0.5f || width <= 0.5f
            || opacity <= 0.001f)
        {
            return;
        }

        Texture2D texture = VoidRiftMask ??= CreateVoidRiftMask();
        Vector2 position = center - rotation.ToRotationVector2()
            * length * 0.5f - Main.screenPosition;
        Vector2 origin = new(0f, texture.Height * 0.5f);
        Vector2 scale = new(length / texture.Width, width / texture.Height);

        Main.EntitySpriteDraw(texture, position, null,
            new Color(111, 0, 22, 0) * (opacity * 0.42f), rotation, origin,
            new Vector2(scale.X, scale.Y * 1.62f), SpriteEffects.None);
        Main.EntitySpriteDraw(texture, position, null,
            new Color(231, 13, 47, 0) * (opacity * 0.66f), rotation, origin,
            new Vector2(scale.X, scale.Y * 1.23f), SpriteEffects.None);
        Main.EntitySpriteDraw(texture, position, null,
            new Color(2, 0, 3, 252) * opacity, rotation, origin, scale,
            SpriteEffects.None);
    }

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

        value = MathHelper.Clamp((value - start) / (end - start), 0f, 1f);
        return value * value * (3f - 2f * value);
    }
}