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

namespace SoulHarvest.Common;

/// <summary>
/// Procedurally builds the three seamless plates used by the Death Domain's
/// clipped parallax window. Textures are created lazily from the draw thread so
/// no FNA graphics resource is ever allocated by a content-loader worker.
/// </summary>
[Autoload(Side = ModSide.Client)]
internal sealed class DeathDomainBackdropTextureSystem : ModSystem
{
    internal const int LayerWidth = 4096;
    internal const int LayerHeight = 1024;
    internal const float ReferenceDomainDiameter =
        (160f + DeathNecklace.RadiusGrowthPerLevel
            * (DeathNecklace.MaxCoreLevel - 1)) * 2f;

    private static readonly Texture2D?[] layers = new Texture2D?[3];
    private static readonly float[] expandedSourceX = new float[3];
    private static readonly float[] expandedSourceY = new float[3];
    private static readonly float[] expandedSourceWidth = new float[3];
    private static readonly float[] expandedSourceHeight = new float[3];
    private static ulong expandedFrameTick = ulong.MaxValue;
    private static Vector2 expandedDestination;
    private static float expandedDestinationWidth = 1f;
    private static float expandedDestinationHeight = 1f;

    internal static Texture2D GetLayer(int index)
    {
        index = Math.Clamp(index, 0, layers.Length - 1);
        return layers[index] ??= CreateLayer(index);
    }

    internal static void Prewarm()
    {
        if (Main.dedServ)
            return;

        for (int layer = 0; layer < layers.Length; layer++)
            _ = GetLayer(layer);
    }

    /// <summary>
    /// Returns the single world-space frame used by a fully manifested Death
    /// Domain. Every smaller mask (necklace aperture, weapon wound and wing
    /// trail) samples this frame and merely clips it, preventing the scenery
    /// inside a cut from being enlarged to fit that cut.
    /// </summary>
    internal static void GetExpandedDomainSourceFrame(int layer,
        out float sourceOffsetX, out float sourceOffsetY,
        out float viewWidth, out float viewHeight)
    {
        EnsureExpandedDomainFrame();
        layer = Math.Clamp(layer, 0, layers.Length - 1);
        sourceOffsetX = expandedSourceX[layer];
        sourceOffsetY = expandedSourceY[layer];
        viewWidth = expandedSourceWidth[layer];
        viewHeight = expandedSourceHeight[layer];
    }

    /// <summary>
    /// Returns the rectangle occupied by the fully manifested domain in the
    /// active world SpriteBatch's pre-transform coordinate space. This is also
    /// the normalization frame for every world-space domain mask, so game zoom
    /// cannot make weapon or wing cuts drift away from the necklace backdrop.
    /// </summary>
    internal static void GetExpandedDomainDrawBounds(
        out Vector2 destination,
        out float destinationWidth,
        out float destinationHeight)
    {
        EnsureExpandedDomainFrame();
        destination = expandedDestination;
        destinationWidth = expandedDestinationWidth;
        destinationHeight = expandedDestinationHeight;
    }

    private static void EnsureExpandedDomainFrame()
    {
        // A crescent or merged trail can contain thousands of vertices. Cache
        // the inverse view transform and all three source plates once per game
        // update instead of repeating a matrix inversion for every vertex.
        if (expandedFrameTick == Main.GameUpdateCount)
            return;
        expandedFrameTick = Main.GameUpdateCount;

        Matrix inverse = Matrix.Invert(Main.GameViewMatrix.TransformationMatrix);
        Vector2 topLeft = Vector2.Transform(Vector2.Zero, inverse);
        Vector2 topRight = Vector2.Transform(
            new Vector2(Main.screenWidth, 0f), inverse);
        Vector2 bottomLeft = Vector2.Transform(
            new Vector2(0f, Main.screenHeight), inverse);
        Vector2 bottomRight = Vector2.Transform(
            new Vector2(Main.screenWidth, Main.screenHeight), inverse);
        float left = Math.Min(Math.Min(topLeft.X, topRight.X),
            Math.Min(bottomLeft.X, bottomRight.X));
        float right = Math.Max(Math.Max(topLeft.X, topRight.X),
            Math.Max(bottomLeft.X, bottomRight.X));
        float top = Math.Min(Math.Min(topLeft.Y, topRight.Y),
            Math.Min(bottomLeft.Y, bottomRight.Y));
        float bottom = Math.Max(Math.Max(topLeft.Y, topRight.Y),
            Math.Max(bottomLeft.Y, bottomRight.Y));
        expandedDestination = new Vector2(left, top);
        expandedDestinationWidth = Math.Max(1f, right - left);
        expandedDestinationHeight = Math.Max(1f, bottom - top);

        Vector2 sourceAperture = new(
            Math.Max(1, Main.screenWidth),
            Math.Max(1, Main.screenHeight));
        Vector2 sourceAnchor = Main.screenPosition + sourceAperture * 0.5f;
        for (int layer = 0; layer < layers.Length; layer++)
        {
            GetSourceFrame(layer, sourceAnchor, sourceAperture,
                out expandedSourceX[layer], out expandedSourceY[layer],
                out expandedSourceWidth[layer],
                out expandedSourceHeight[layer]);
        }
    }

    /// <summary>
    /// Maps a world point into the exact parallax plate framing used by the Death
    /// Necklace. Weapon crescents and persistent wounds use this instead of raw
    /// texture-size UVs, so all three layers have the same scale, anchor and
    /// parallax as the domain rather than forming a differently scaled collage.
    /// </summary>
    internal static Vector2 GetWorldUv(int layer, Vector2 world,
        Vector2 anchorWorld, Vector2 apertureSize)
    {
        apertureSize.X = Math.Max(1f, apertureSize.X);
        apertureSize.Y = Math.Max(1f, apertureSize.Y);
        GetSourceFrame(layer, anchorWorld, apertureSize,
            out float sourceOffsetX, out float sourceOffsetY,
            out float viewWidth, out float viewHeight);
        Vector2 normalized = (world - anchorWorld) / apertureSize
            + new Vector2(0.5f);
        return new Vector2(
            (sourceOffsetX + normalized.X * viewWidth) / LayerWidth,
            (sourceOffsetY + normalized.Y * viewHeight) / LayerHeight);
    }

    internal static Vector2 GetWorldUv(int layer, Vector2 world,
        Vector2 anchorWorld)
        => GetWorldUv(layer, world, anchorWorld,
            new Vector2(ReferenceDomainDiameter));

    /// <summary>
    /// Uses the fully manifested Death Necklace framing at all times. The owner
    /// and fallback anchor remain part of the call contract because masks are
    /// owner-scoped, but neither may rescale or recenter the shared backdrop.
    /// Every mask therefore reveals the same parallax pixel at the same visible
    /// world position before, during and after manifestation.
    /// </summary>
    internal static Vector2 GetMatchingDomainUv(int layer, Vector2 world,
        int owner, Vector2 fallbackAnchor)
    {
        _ = owner;
        _ = fallbackAnchor;
        GetExpandedDomainDrawBounds(out Vector2 destination,
            out float destinationWidth, out float destinationHeight);
        GetExpandedDomainSourceFrame(layer,
            out float sourceOffsetX, out float sourceOffsetY,
            out float viewWidth, out float viewHeight);
        Vector2 worldTopLeft = Main.screenPosition + destination;
        Vector2 normalized = new(
            (world.X - worldTopLeft.X) / destinationWidth,
            (world.Y - worldTopLeft.Y) / destinationHeight);
        return new Vector2(
            (sourceOffsetX + normalized.X * viewWidth) / LayerWidth,
            (sourceOffsetY + normalized.Y * viewHeight) / LayerHeight);
    }

    internal static float GetLayerOpacity(int layer)
        => layer switch { 0 => 1f, 1 => 0.9f, _ => 0.96f };

    /// <summary>
    /// Draws the actual three in-game Death Domain plates as a compact UI icon.
    /// The deeper plates move less than the near mist, so cursor motion and the
    /// ambient drift preserve the same parallax language as the world aperture.
    /// </summary>
    internal static void DrawUiIcon(
        SpriteBatch batch,
        Rectangle destination,
        Vector2 focusPoint,
        float opacity = 1f)
    {
        if (Main.dedServ || destination.Width <= 0 || destination.Height <= 0)
            return;

        Vector2 center = destination.Center.ToVector2();
        Vector2 halfSize = new(
            Math.Max(1f, destination.Width * 0.5f),
            Math.Max(1f, destination.Height * 0.5f));
        Vector2 cursor = (focusPoint - center) / halfSize;
        cursor.X = MathHelper.Clamp(cursor.X, -1f, 1f);
        cursor.Y = MathHelper.Clamp(cursor.Y, -1f, 1f);

        Vector2 worldCenter = Main.LocalPlayer.active
            ? Main.LocalPlayer.Center
            : Main.screenPosition + new Vector2(Main.screenWidth, Main.screenHeight) * 0.5f;
        Vector2 apertureSize = new(ReferenceDomainDiameter * 0.72f);
        float time = Main.GlobalTimeWrappedHourly;
        for (int layer = 0; layer < 3; layer++)
        {
            Texture2D texture = GetLayer(layer);
            GetSourceFrame(layer, worldCenter, apertureSize,
                out float sourceX, out float sourceY,
                out float sourceWidth, out float sourceHeight);

            float depth = layer switch { 0 => 0.34f, 1 => 0.68f, _ => 1f };
            sourceX += cursor.X * 54f * depth
                + (float)Math.Sin(time * (0.22f + layer * 0.07f) + layer) * 22f * depth;
            sourceY += cursor.Y * 32f * depth
                + (float)Math.Cos(time * (0.18f + layer * 0.05f) + layer * 1.7f) * 13f * depth;
            sourceX = MathHelper.Clamp(sourceX, 0f,
                Math.Max(0f, texture.Width - sourceWidth));
            sourceY = MathHelper.Clamp(sourceY, 0f,
                Math.Max(0f, texture.Height - sourceHeight));

            int sourceWidthPixels = Math.Clamp(
                (int)MathF.Round(sourceWidth), 1, texture.Width);
            int sourceHeightPixels = Math.Clamp(
                (int)MathF.Round(sourceHeight), 1, texture.Height);
            Rectangle source = new(
                Math.Clamp((int)MathF.Round(sourceX), 0,
                    texture.Width - sourceWidthPixels),
                Math.Clamp((int)MathF.Round(sourceY), 0,
                    texture.Height - sourceHeightPixels),
                sourceWidthPixels,
                sourceHeightPixels);
            batch.Draw(texture, destination, source,
                Color.White * (MathHelper.Clamp(opacity, 0f, 1f)
                    * GetLayerOpacity(layer)));
        }
    }

    /// <summary>
    /// Draws the same world-anchored plates used by the descended Death Domain
    /// into a physical-screen rectangle. The ultimate uses this behind displaced
    /// mirror shards so every opening reveals the real domain at its normal scale.
    /// </summary>
    internal static void DrawScreenAlignedBackdrop(SpriteBatch batch,
        Rectangle destination, float opacity)
    {
        if (Main.dedServ || destination.Width <= 0 || destination.Height <= 0
            || opacity <= 0.001f)
        {
            return;
        }

        for (int layer = 0; layer < 3; layer++)
        {
            Texture2D texture = GetLayer(layer);
            GetExpandedDomainSourceFrame(layer,
                out float sourceX, out float sourceY,
                out float sourceWidth, out float sourceHeight);
            int width = Math.Clamp((int)MathF.Round(sourceWidth), 1,
                texture.Width);
            int height = Math.Clamp((int)MathF.Round(sourceHeight), 1,
                texture.Height);
            Rectangle source = new(
                Math.Clamp((int)MathF.Round(sourceX), 0,
                    texture.Width - width),
                Math.Clamp((int)MathF.Round(sourceY), 0,
                    texture.Height - height),
                width,
                height);
            batch.Draw(texture, destination, source,
                Color.White * (MathHelper.Clamp(opacity, 0f, 1f)
                    * GetLayerOpacity(layer)));
        }
    }

    /// <summary>Returns the necklace's source rectangle for a world aperture.</summary>
    internal static void GetSourceFrame(int layer, Vector2 worldCenter,
        Vector2 apertureSize, out float sourceOffsetX,
        out float sourceOffsetY, out float viewWidth, out float viewHeight)
    {
        layer = Math.Clamp(layer, 0, layers.Length - 1);
        float referenceViewWidth = 1024f
            * (layer switch { 0 => 0.68f, 1 => 0.9f, _ => 0.84f });
        float referenceViewHeight = 512f
            * (layer switch { 0 => 0.78f, 1 => 0.9f, _ => 0.86f });
        float horizontalScale = Math.Max(0.001f,
            apertureSize.X / ReferenceDomainDiameter);
        float verticalScale = Math.Max(0.001f,
            apertureSize.Y / ReferenceDomainDiameter);
        viewWidth = Math.Min(LayerWidth,
            referenceViewWidth * horizontalScale);
        viewHeight = Math.Min(LayerHeight,
            referenceViewHeight * verticalScale);

        float worldWidth = Math.Max(1f, Main.maxTilesX * 16f);
        float worldHeight = Math.Max(1f, Main.maxTilesY * 16f);
        float worldX = MathHelper.Clamp(worldCenter.X / worldWidth, 0f, 1f);
        float worldY = MathHelper.Clamp(worldCenter.Y / worldHeight, 0f, 1f);
        float travelFactor = layer switch { 0 => 0.34f, 1 => 0.66f, _ => 1f };
        float horizontalTravel = Math.Max(0f, LayerWidth - viewWidth)
            * travelFactor;
        sourceOffsetX = (LayerWidth - viewWidth - horizontalTravel) * 0.5f
            + horizontalTravel * worldX;
        sourceOffsetY = worldY * Math.Max(0f, LayerHeight - viewHeight);
    }

    /// <summary>
    /// Samples the exact three procedural plates used by the necklace domain and
    /// composites them in premultiplied-alpha order. Death weapon masks use this
    /// method so their interior is the domain itself, not a similarly colored
    /// substitute texture.
    /// </summary>
    internal static Color SampleCompositePixel(int x, int y, float u, float v)
    {
        Color result = BuildFarPixel(x, y, u, v);
        result = CompositeOver(result, BuildMiddlePixel(u, v));
        result = CompositeOver(result, BuildNearPixel(u, v));
        return result;
    }

    public override void Unload()
    {
        expandedFrameTick = ulong.MaxValue;
        Texture2D?[] oldLayers = (Texture2D?[])layers.Clone();
        Array.Clear(layers);
        if (Main.dedServ)
            return;

        Main.QueueMainThreadAction(() =>
        {
            foreach (Texture2D? texture in oldLayers)
                texture?.Dispose();
        });
    }

    private static Texture2D CreateLayer(int layer)
    {
        Texture2D texture = new(Main.instance.GraphicsDevice, LayerWidth, LayerHeight, false, SurfaceFormat.Color);
        Color[] pixels = new Color[LayerWidth * LayerHeight];
        for (int y = 0; y < LayerHeight; y++)
        {
            float v = (y + 0.5f) / LayerHeight;
            for (int x = 0; x < LayerWidth; x++)
            {
                // Keep roughly the same feature scale as the original 1024-wide
                // plate while supplying enough unique scenery for a 4K full-screen
                // view at exactly the same scale as the player's circular window.
                float u = (x + 0.5f) / 1024f;
                pixels[y * LayerWidth + x] = layer switch
                {
                    0 => BuildFarPixel(x, y, u, v),
                    1 => BuildMiddlePixel(u, v),
                    _ => BuildNearPixel(u, v)
                };
            }
        }

        texture.SetData(pixels);
        return texture;
    }

    private static Color BuildFarPixel(int x, int y, float u, float v)
    {
        Vector3 top = new(0.018f, 0.006f, 0.055f);
        Vector3 bottom = new(0.105f, 0.004f, 0.072f);
        Vector3 color = Vector3.Lerp(top, bottom, Smooth01(v));

        float crimsonCloud = Wave(u, v, 2f, 1f, 0.13f)
            + Wave(u, v, 5f, -2f, 1.7f) * 0.45f
            + Wave(u, v, 9f, 3f, 4.2f) * 0.2f
            + Wave(u, v, 0.37f, 0.45f, 2.3f) * 0.24f;
        crimsonCloud = Smooth01((crimsonCloud + 1.1f) / 2.45f);
        float violetCloud = Smooth01((Wave(u, v, 3f, -2f, 3.4f) + 0.78f) / 1.78f);
        color += new Vector3(0.16f, 0.006f, 0.055f) * crimsonCloud * (0.32f + v * 0.34f);
        color += new Vector3(0.035f, 0.018f, 0.13f) * violetCloud * 0.42f;

        float hash = Hash01(x, y, 9137);
        if (hash > 0.9962f)
        {
            float star = MathHelper.Clamp((hash - 0.9962f) / 0.0038f, 0f, 1f);
            Vector3 starColor = (x + y) % 5 == 0
                ? new Vector3(1f, 0.22f, 0.34f)
                : new Vector3(0.62f, 0.38f, 0.92f);
            color = Vector3.Lerp(color, starColor, 0.42f + star * 0.58f);
        }

        // Background singularities are rendered as independent transient actors
        // by DeathDomainVisualSystem. Keeping them out of this baked plate allows
        // each one to form, live briefly, collapse, and leave a true empty interval.

        return Opaque(color);
    }

    private static Color BuildMiddlePixel(float u, float v)
    {
        Vector3 color = Vector3.Zero;
        float alpha = 0f;

        // A distant eclipsed blood moon with a luminous, broken corona.
        Vector2 moonOffset = new((u - 0.28f) * 2f, (v - 0.28f) * 4f);
        float moonDistance = moonOffset.Length();
        float corona = 1f - SmoothRange(0.19f, 0.245f, moonDistance);
        float hollow = SmoothRange(0.115f, 0.17f, moonDistance);
        float moonRing = MathHelper.Clamp(corona * hollow, 0f, 1f);
        if (moonRing > 0f)
        {
            float fracture = 0.72f + 0.28f * (float)Math.Sin(Math.Atan2(moonOffset.Y, moonOffset.X) * 11f);
            alpha = moonRing * fracture * 0.88f;
            color = new Vector3(0.72f, 0.025f, 0.15f);
        }

        float cloud = Smooth01((Wave(u, v, 4f, 1f, 0.7f) + Wave(u, v, 7f, -2f, 2.9f) * 0.55f + 0.55f) / 2.1f);
        float cloudAlpha = cloud * (1f - Math.Abs(v - 0.48f) * 1.55f) * 0.2f;
        BlendPremultiplied(ref color, ref alpha, new Vector3(0.42f, 0.02f, 0.13f), Math.Max(0f, cloudAlpha));

        // Two ranges of dead mountains create the mid-distance depth plane.
        float rearHorizon = 0.59f
            + (float)Math.Sin(u * MathHelper.TwoPi * 3f + 0.4f) * 0.06f
            + (float)Math.Sin(u * MathHelper.TwoPi * 7f) * 0.025f
            + (float)Math.Sin(u * MathHelper.TwoPi * 0.31f + 1.4f) * 0.024f;
        if (v > rearHorizon)
        {
            float depth = Smooth01((v - rearHorizon) / 0.18f);
            BlendPremultiplied(ref color, ref alpha, new Vector3(0.085f, 0.012f, 0.12f), 0.62f + depth * 0.14f);
        }

        float frontHorizon = 0.71f
            + (float)Math.Sin(u * MathHelper.TwoPi * 5f + 2.1f) * 0.055f
            + (float)Math.Sin(u * MathHelper.TwoPi * 11f) * 0.018f;
        if (v > frontHorizon)
            BlendPremultiplied(ref color, ref alpha, new Vector3(0.035f, 0.004f, 0.055f), 0.82f);

        return Premultiplied(color, alpha);
    }

    private static Color BuildNearPixel(float u, float v)
    {
        float broadMist = Wave(u, v, 2f, 1f, 0.35f)
            + Wave(u, v, 4f, -2f, 2.2f) * 0.52f
            + Wave(u, v, 7f, 3f, 4.6f) * 0.24f
            + Wave(u, v, 0.43f, -0.38f, 5.1f) * 0.26f;
        float curledMist = Wave(u, v, 3f, -3f, 1.1f)
            + Wave(u, v, 9f, 4f, 3.7f) * 0.34f;
        float body = Smooth01((broadMist + 0.72f) / 2.2f);
        float curls = Smooth01((curledMist + 0.82f) / 1.85f);
        float alpha = MathHelper.Clamp(body * 0.34f + body * curls * 0.32f, 0f, 0.62f);
        if (alpha <= 0.002f)
            return Color.Transparent;

        Vector3 color = Vector3.Lerp(
            new Vector3(0.18f, 0.002f, 0.035f),
            new Vector3(0.72f, 0.018f, 0.12f),
            curls * 0.72f);
        return Premultiplied(color, alpha);
    }

    private static void BlendPremultiplied(ref Vector3 color, ref float alpha, Vector3 sourceColor, float sourceAlpha)
    {
        sourceAlpha = MathHelper.Clamp(sourceAlpha, 0f, 1f);
        float combinedAlpha = sourceAlpha + alpha * (1f - sourceAlpha);
        if (combinedAlpha <= 0.0001f)
            return;

        color = (sourceColor * sourceAlpha + color * alpha * (1f - sourceAlpha)) / combinedAlpha;
        alpha = combinedAlpha;
    }

    private static Color Opaque(Vector3 color) => new(
        MathHelper.Clamp(color.X, 0f, 1f),
        MathHelper.Clamp(color.Y, 0f, 1f),
        MathHelper.Clamp(color.Z, 0f, 1f),
        1f);

    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 Color CompositeOver(Color destination, Color source)
    {
        Vector4 under = destination.ToVector4();
        Vector4 over = source.ToVector4();
        float remaining = 1f - over.W;
        return new Color(
            MathHelper.Clamp(over.X + under.X * remaining, 0f, 1f),
            MathHelper.Clamp(over.Y + under.Y * remaining, 0f, 1f),
            MathHelper.Clamp(over.Z + under.Z * remaining, 0f, 1f),
            MathHelper.Clamp(over.W + under.W * remaining, 0f, 1f));
    }

    private static float Wave(float u, float v, float horizontalCycles, float verticalCycles, float phase) =>
        (float)Math.Sin(MathHelper.TwoPi * (u * horizontalCycles + v * verticalCycles) + phase);

    private static float Hash01(int x, int y, int seed)
    {
        uint value = (uint)(x * 374761393 + y * 668265263 + seed * 1442695041);
        value = (value ^ (value >> 13)) * 1274126177u;
        value ^= value >> 16;
        return (value & 0x00FFFFFFu) / 16777215f;
    }

    private static float SmoothRange(float start, float end, float value) =>
        Smooth01((value - start) / Math.Max(0.0001f, end - start));

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

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