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>
/// Clips the stationary Death Domain backdrop into an arbitrary blade-tip ribbon.
/// World-space texture coordinates keep the scenery fixed while the red rim is
/// drawn later by the owning projectile.
/// </summary>
[Autoload(Side = ModSide.Client)]
internal sealed class DeathDomainTrailVisualSystem : ModSystem
{
    private const int MaximumPoints = 72;
    private static readonly Dictionary<long, TrailDrawState> drawStates = [];
    private static readonly VertexPositionColorTexture[] vertices =
        new VertexPositionColorTexture[MaximumPoints * 2];
    private static VertexPositionColorTexture[] mergedOutlineVertices =
        new VertexPositionColorTexture[4096];
    private static VertexPositionColorTexture[][] mergedBackdropVertices =
    [
        new VertexPositionColorTexture[4096],
        new VertexPositionColorTexture[4096],
        new VertexPositionColorTexture[4096]
    ];
    private static readonly short[] indices = CreateIndices();
    private static BasicEffect? effect;

    private readonly record struct TrailDrawState(
        int Owner,
        IReadOnlyList<Vector2> Points,
        float Width,
        float Opacity,
        bool MergeOverlappingRims,
        float Fracture,
        bool TaperEnds,
        bool NoiseFadeEnds,
        int Seed,
        Vector4 Bounds,
        ulong UpdateTick);

    internal static void Record(int owner, int identity,
        IReadOnlyList<Vector2> points, float width, float opacity,
        bool mergeOverlappingRims = false, float fracture = 0f,
        bool taperEnds = true, bool noiseFadeEnds = false)
    {
        if (Main.dedServ || points.Count < 2 || width <= 0.5f
            || opacity <= 0.001f)
        {
            return;
        }
        long key = ((long)owner << 32) | (uint)identity;
        if (drawStates.TryGetValue(key, out TrailDrawState existing)
            && existing.MergeOverlappingRims == mergeOverlappingRims
            && existing.TaperEnds == taperEnds
            && existing.NoiseFadeEnds == noiseFadeEnds
            && ReferenceEquals(existing.Points, points))
        {
            // Persistent rifts submit the same immutable path every update. Only
            // opacity and freshness change, so keep their cached world bounds.
            drawStates[key] = existing with
            {
                Opacity = opacity,
                Fracture = MathHelper.Clamp(fracture, 0f, 1f),
                UpdateTick = Main.GameUpdateCount
            };
            return;
        }

        drawStates[key] = new TrailDrawState(owner, points, width, opacity,
            mergeOverlappingRims, MathHelper.Clamp(fracture, 0f, 1f),
            taperEnds, noiseFadeEnds,
            unchecked(identity * 397 ^ owner * 7919),
            CalculateTrailBounds(points, width * 1.72f),
            Main.GameUpdateCount);
    }

    public override void PostUpdateEverything()
    {
        if (drawStates.Count == 0)
            return;
        List<long>? stale = null;
        foreach ((long key, TrailDrawState state) in drawStates)
        {
            if (Main.GameUpdateCount <= state.UpdateTick + 1)
                continue;
            stale ??= [];
            stale.Add(key);
        }
        if (stale is null)
            return;
        foreach (long key in stale)
            drawStates.Remove(key);
    }

    public override void OnWorldUnload() => ClearVisualState();

    public override void Unload()
    {
        ClearVisualState();
        BasicEffect? oldEffect = effect;
        effect = null;
        if (oldEffect is not null && !Main.dedServ)
            Main.QueueMainThreadAction(oldEffect.Dispose);
    }

    private static void ClearVisualState()
    {
        drawStates.Clear();
    }

    public override void PostDrawTiles()
    {
        if (Main.gameMenu || drawStates.Count == 0
            || Main.graphics?.GraphicsDevice is not GraphicsDevice graphicsDevice)
        {
            return;
        }

        effect ??= new BasicEffect(graphicsDevice)
        {
            TextureEnabled = true,
            VertexColorEnabled = true,
            LightingEnabled = false,
            FogEnabled = false
        };
        effect.World = Main.GameViewMatrix.TransformationMatrix;
        effect.View = Matrix.Identity;
        effect.Projection = Matrix.CreateOrthographicOffCenter(
            0f, Main.screenWidth, Main.screenHeight, 0f, 0f, 1f);
        graphicsDevice.BlendState = BlendState.AlphaBlend;
        graphicsDevice.DepthStencilState = DepthStencilState.None;
        graphicsDevice.RasterizerState = RasterizerState.CullNone;
        graphicsDevice.SamplerStates[0] = SamplerState.LinearWrap;
        DrawMergedTrails(graphicsDevice);

        foreach (TrailDrawState state in drawStates.Values)
        {
            if (Main.GameUpdateCount > state.UpdateTick + 1)
                continue;
            if (state.MergeOverlappingRims)
                continue;
            if (!IsOnScreen(state.Bounds))
                continue;
            int pointCount = Math.Min(MaximumPoints, state.Points.Count);
            if (pointCount < 2)
                continue;
            DrawSolidRibbon(graphicsDevice, state, state.Width * 1.72f,
                new Color(174, 0, 44, 0) * (state.Opacity * 0.24f));
            DrawSolidRibbon(graphicsDevice, state, state.Width * 1.40f,
                new Color(244, 12, 66, 235) * state.Opacity);
            DrawSolidRibbon(graphicsDevice, state, state.Width * 1.17f,
                new Color(255, 187, 178, 235)
                    * (state.Opacity * 0.88f));
            effect.TextureEnabled = true;
            for (int layer = 0; layer < 3; layer++)
            {
                BuildVertices(state, state.Width,
                    Color.White * (state.Opacity
                        * DeathDomainBackdropTextureSystem
                            .GetLayerOpacity(layer)), layer);
                effect.Texture = DeathDomainBackdropTextureSystem.GetLayer(layer);
                foreach (EffectPass pass in effect.CurrentTechnique.Passes)
                {
                    pass.Apply();
                    graphicsDevice.DrawUserIndexedPrimitives(
                        PrimitiveType.TriangleList, vertices, 0, pointCount * 2,
                        indices, 0, (pointCount - 1) * 2);
                }
            }
        }
    }

    private static void DrawMergedTrails(GraphicsDevice graphicsDevice)
    {
        if (effect is null)
            return;

        // Render every rim first and the opaque, world-aligned domain ribbons
        // afterwards. The common backdrop naturally covers rims inside an
        // overlap, producing one continuous surface without any pairwise trail
        // intersection tests. Work therefore stays linear as rifts accumulate.
        DrawMergedOutlineBatch(graphicsDevice, 0.70f, 0.86f,
            new Color(174, 0, 44, 0), 0.24f);
        DrawMergedOutlineBatch(graphicsDevice, 0.585f, 0.70f,
            new Color(244, 12, 66, 235), 1f);
        DrawMergedOutlineBatch(graphicsDevice, 0.50f, 0.585f,
            new Color(255, 187, 178, 235), 0.88f);

        int backdropVertexCount = BuildMergedBackdropBatch();
        if (backdropVertexCount < 3)
            return;
        effect.TextureEnabled = true;
        for (int layer = 0; layer < 3; layer++)
        {
            effect.Texture = DeathDomainBackdropTextureSystem.GetLayer(layer);
            foreach (EffectPass pass in effect.CurrentTechnique.Passes)
            {
                pass.Apply();
                graphicsDevice.DrawUserPrimitives(PrimitiveType.TriangleList,
                    mergedBackdropVertices[layer], 0,
                    backdropVertexCount / 3);
            }
        }
    }

    private static void DrawMergedOutlineBatch(GraphicsDevice graphicsDevice,
        float innerWidthRatio, float outerWidthRatio, Color baseColor,
        float opacityMultiplier)
    {
        if (effect is null)
            return;

        int vertexIndex = 0;
        foreach (TrailDrawState state in drawStates.Values)
        {
            if (!state.MergeOverlappingRims
                || Main.GameUpdateCount > state.UpdateTick + 1
                || !IsOnScreen(state.Bounds))
            {
                continue;
            }
            int pointCount = Math.Min(MaximumPoints, state.Points.Count);
            float innerHalfWidth = state.Width * innerWidthRatio;
            float outerHalfWidth = state.Width * outerWidthRatio;
            Color color = baseColor * (state.Opacity * opacityMultiplier);
            for (int segment = 0; segment < pointCount - 1; segment++)
            {
                if (!IsSegmentVisible(state, segment, pointCount))
                    continue;
                Vector2 start = state.Points[segment];
                Vector2 end = state.Points[segment + 1];
                Vector2 startNormal = GetTrailNormal(state.Points, segment,
                    pointCount);
                Vector2 endNormal = GetTrailNormal(state.Points, segment + 1,
                    pointCount);
                float startTaper = GetTrailTaper(state, segment
                    / Math.Max(1f, pointCount - 1f));
                float endTaper = GetTrailTaper(state, (segment + 1f)
                    / Math.Max(1f, pointCount - 1f));
                float startProgress = segment
                    / Math.Max(1f, pointCount - 1f);
                float endProgress = (segment + 1f)
                    / Math.Max(1f, pointCount - 1f);
                for (int sideIndex = 0; sideIndex < 2; sideIndex++)
                {
                    float side = sideIndex == 0 ? -1f : 1f;
                    Color startColor = color * GetEndNoiseOpacity(state,
                        startProgress, side);
                    Color endColor = color * GetEndNoiseOpacity(state,
                        endProgress, side);
                    Vector2 innerStart = start
                        + startNormal * side * innerHalfWidth * startTaper;
                    Vector2 outerStart = start
                        + startNormal * side * outerHalfWidth * startTaper;
                    Vector2 innerEnd = end
                        + endNormal * side * innerHalfWidth * endTaper;
                    Vector2 outerEnd = end
                        + endNormal * side * outerHalfWidth * endTaper;
                    EnsureBatchCapacity(ref mergedOutlineVertices,
                        vertexIndex + 6);
                    WriteBatchVertex(mergedOutlineVertices, ref vertexIndex,
                        outerStart, startColor, Vector2.Zero);
                    WriteBatchVertex(mergedOutlineVertices, ref vertexIndex,
                        innerStart, startColor, Vector2.Zero);
                    WriteBatchVertex(mergedOutlineVertices, ref vertexIndex,
                        outerEnd, endColor, Vector2.Zero);
                    WriteBatchVertex(mergedOutlineVertices, ref vertexIndex,
                        outerEnd, endColor, Vector2.Zero);
                    WriteBatchVertex(mergedOutlineVertices, ref vertexIndex,
                        innerStart, startColor, Vector2.Zero);
                    WriteBatchVertex(mergedOutlineVertices, ref vertexIndex,
                        innerEnd, endColor, Vector2.Zero);
                }
            }
        }
        if (vertexIndex < 3)
            return;

        effect.TextureEnabled = false;
        foreach (EffectPass pass in effect.CurrentTechnique.Passes)
        {
            pass.Apply();
            graphicsDevice.DrawUserPrimitives(PrimitiveType.TriangleList,
                mergedOutlineVertices, 0, vertexIndex / 3);
        }
    }

    private static int BuildMergedBackdropBatch()
    {
        int vertexIndex = 0;
        foreach (TrailDrawState state in drawStates.Values)
        {
            if (!state.MergeOverlappingRims
                || Main.GameUpdateCount > state.UpdateTick + 1
                || !IsOnScreen(state.Bounds))
            {
                continue;
            }
            int pointCount = Math.Min(MaximumPoints, state.Points.Count);
            for (int segment = 0; segment < pointCount - 1; segment++)
            {
                if (!IsSegmentVisible(state, segment, pointCount))
                    continue;
                Vector2 start = state.Points[segment];
                Vector2 end = state.Points[segment + 1];
                Vector2 startNormal = GetTrailNormal(state.Points, segment,
                    pointCount);
                Vector2 endNormal = GetTrailNormal(state.Points, segment + 1,
                    pointCount);
                float startHalfWidth = state.Width
                    * GetTrailTaper(state,
                        segment / Math.Max(1f, pointCount - 1f))
                    * 0.5f;
                float endHalfWidth = state.Width
                    * GetTrailTaper(state, (segment + 1f)
                        / Math.Max(1f, pointCount - 1f)) * 0.5f;
                Vector2 leftStart = start - startNormal * startHalfWidth;
                Vector2 rightStart = start + startNormal * startHalfWidth;
                Vector2 leftEnd = end - endNormal * endHalfWidth;
                Vector2 rightEnd = end + endNormal * endHalfWidth;
                int bandCount = state.NoiseFadeEnds ? 6 : 1;
                int segmentVertexCount = bandCount * 6;
                for (int layer = 0; layer < 3; layer++)
                    EnsureBatchCapacity(ref mergedBackdropVertices[layer],
                        vertexIndex + segmentVertexCount);
                Vector2 anchor = GetBackgroundAnchor(state);
                float startProgress = segment
                    / Math.Max(1f, pointCount - 1f);
                float endProgress = (segment + 1f)
                    / Math.Max(1f, pointCount - 1f);
                for (int layer = 0; layer < 3; layer++)
                {
                    int layerVertexIndex = vertexIndex;
                    Color baseColor = Color.White * (state.Opacity
                        * DeathDomainBackdropTextureSystem
                            .GetLayerOpacity(layer));
                    for (int band = 0; band < bandCount; band++)
                    {
                        float acrossStart = band / (float)bandCount;
                        float acrossEnd = (band + 1f) / bandCount;
                        float lateralStart = MathHelper.Lerp(-1f, 1f,
                            acrossStart);
                        float lateralEnd = MathHelper.Lerp(-1f, 1f,
                            acrossEnd);
                        Vector2 bandStartLeft = Vector2.Lerp(leftStart,
                            rightStart, acrossStart);
                        Vector2 bandStartRight = Vector2.Lerp(leftStart,
                            rightStart, acrossEnd);
                        Vector2 bandEndLeft = Vector2.Lerp(leftEnd, rightEnd,
                            acrossStart);
                        Vector2 bandEndRight = Vector2.Lerp(leftEnd, rightEnd,
                            acrossEnd);
                        Color startLeftColor = baseColor
                            * GetEndNoiseOpacity(state, startProgress,
                                lateralStart);
                        Color startRightColor = baseColor
                            * GetEndNoiseOpacity(state, startProgress,
                                lateralEnd);
                        Color endLeftColor = baseColor
                            * GetEndNoiseOpacity(state, endProgress,
                                lateralStart);
                        Color endRightColor = baseColor
                            * GetEndNoiseOpacity(state, endProgress,
                                lateralEnd);

                        WriteBackdropBatchVertex(layer, state.Owner,
                            ref layerVertexIndex, bandStartLeft, anchor,
                            startLeftColor);
                        WriteBackdropBatchVertex(layer, state.Owner,
                            ref layerVertexIndex, bandStartRight, anchor,
                            startRightColor);
                        WriteBackdropBatchVertex(layer, state.Owner,
                            ref layerVertexIndex, bandEndLeft, anchor,
                            endLeftColor);
                        WriteBackdropBatchVertex(layer, state.Owner,
                            ref layerVertexIndex, bandEndLeft, anchor,
                            endLeftColor);
                        WriteBackdropBatchVertex(layer, state.Owner,
                            ref layerVertexIndex, bandStartRight, anchor,
                            startRightColor);
                        WriteBackdropBatchVertex(layer, state.Owner,
                            ref layerVertexIndex, bandEndRight, anchor,
                            endRightColor);
                    }
                }
                vertexIndex += segmentVertexCount;
            }
        }
        return vertexIndex;
    }

    private static void WriteBackdropBatchVertex(int layer, int owner,
        ref int vertexIndex, Vector2 world, Vector2 anchor, Color color)
    {
        Vector2 uv = DeathDomainBackdropTextureSystem.GetMatchingDomainUv(
            layer, world, owner, anchor);
        WriteBatchVertex(mergedBackdropVertices[layer], ref vertexIndex, world,
            color, uv);
    }

    private static void WriteBatchVertex(VertexPositionColorTexture[] target,
        ref int vertexIndex, Vector2 world, Color color, Vector2 uv)
    {
        target[vertexIndex++] = new VertexPositionColorTexture(
            new Vector3(world - Main.screenPosition, 0f), color, uv);
    }

    private static void EnsureBatchCapacity(
        ref VertexPositionColorTexture[] target, int required)
    {
        if (target.Length >= required)
            return;
        int capacity = Math.Max(required, Math.Max(4096, target.Length * 2));
        Array.Resize(ref target, capacity);
    }

    private static Vector2 GetTrailNormal(IReadOnlyList<Vector2> points,
        int index, int pointCount)
    {
        Vector2 previous = points[Math.Max(0, index - 1)];
        Vector2 next = points[Math.Min(pointCount - 1, index + 1)];
        return (next - previous).SafeNormalize(Vector2.UnitX)
            .RotatedBy(MathHelper.PiOver2);
    }

    private static float GetTrailTaper(TrailDrawState state, float progress)
        => state.TaperEnds
            ? (float)Math.Pow(Math.Max(0f,
                Math.Sin(MathHelper.Clamp(progress, 0f, 1f)
                    * MathHelper.Pi)), 0.34f)
            : 1f;

    private static float GetEndNoiseOpacity(TrailDrawState state,
        float progress, float lateral)
    {
        if (!state.NoiseFadeEnds)
            return 1f;

        progress = MathHelper.Clamp(progress, 0f, 1f);
        const float fadeSpan = 0.28f;
        float edgeDistance = Math.Min(progress, 1f - progress);
        if (edgeDistance >= fadeSpan)
            return 1f;

        float fade = edgeDistance / fadeSpan;
        int alongCell = (int)Math.Floor(progress * 53f);
        int lateralCell = (int)Math.Floor((lateral + 1f) * 17f);
        uint hash = unchecked((uint)(state.Seed * 747796405
            + alongCell * 2891336453 + lateralCell * 1181783497));
        hash ^= hash >> 16;
        hash *= 0x7FEB352Du;
        hash ^= hash >> 15;
        float noise = (hash & 0x00FFFFFFu) / 16777215f;
        float dissolve = (fade - 0.06f) * 1.18f
            + (noise - 0.5f) * 0.4f;
        return Smooth01(MathHelper.Clamp(dissolve, 0f, 1f));
    }

    private static bool IsSegmentVisible(TrailDrawState state, int segment,
        int pointCount)
    {
        if (state.Fracture <= 0.001f)
            return true;
        uint hash = unchecked((uint)(state.Seed * 747796405
            + segment * 2891336453));
        hash ^= hash >> 16;
        hash *= 0x7FEB352Du;
        hash ^= hash >> 15;
        float random = (hash & 0x00FFFFFFu) / 16777215f;
        float along = (segment + 0.5f) / Math.Max(1f, pointCount - 1f);
        float ripple = 0.5f + 0.5f * (float)Math.Sin(
            along * MathHelper.TwoPi * 5f + state.Seed * 0.017f);
        float breakThreshold = 0.10f + random * 0.68f + ripple * 0.16f;
        return Smooth01(state.Fracture) < breakThreshold;
    }

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

    private static Vector4 CalculateTrailBounds(
        IReadOnlyList<Vector2> points, float width)
    {
        Vector2 minimum = points[0];
        Vector2 maximum = points[0];
        int count = Math.Min(MaximumPoints, points.Count);
        for (int index = 1; index < count; index++)
        {
            minimum = Vector2.Min(minimum, points[index]);
            maximum = Vector2.Max(maximum, points[index]);
        }
        float padding = width * 0.5f + 4f;
        return new Vector4(minimum.X - padding, minimum.Y - padding,
            maximum.X + padding, maximum.Y + padding);
    }

    private static bool IsOnScreen(Vector4 bounds)
    {
        const float padding = 96f;
        float left = Main.screenPosition.X - padding;
        float top = Main.screenPosition.Y - padding;
        float right = Main.screenPosition.X + Main.screenWidth + padding;
        float bottom = Main.screenPosition.Y + Main.screenHeight + padding;
        return bounds.Z >= left && bounds.W >= top
            && bounds.X <= right && bounds.Y <= bottom;
    }

    private static Vector2 GetBackgroundAnchor(TrailDrawState state)
        => new((state.Bounds.X + state.Bounds.Z) * 0.5f,
            (state.Bounds.Y + state.Bounds.W) * 0.5f);

    private static void DrawSolidRibbon(GraphicsDevice graphicsDevice,
        TrailDrawState state, float width, Color color)
    {
        if (effect is null)
            return;
        int pointCount = BuildVertices(state, width, color);
        effect.TextureEnabled = false;
        foreach (EffectPass pass in effect.CurrentTechnique.Passes)
        {
            pass.Apply();
            graphicsDevice.DrawUserIndexedPrimitives(
                PrimitiveType.TriangleList, vertices, 0, pointCount * 2,
                indices, 0, (pointCount - 1) * 2);
        }
    }

    private static int BuildVertices(TrailDrawState state, float width,
        Color color, int backdropLayer = -1)
    {
        int count = Math.Min(MaximumPoints, state.Points.Count);
        for (int index = 0; index < count; index++)
        {
            Vector2 point = state.Points[index];
            Vector2 previous = state.Points[Math.Max(0, index - 1)];
            Vector2 next = state.Points[Math.Min(count - 1, index + 1)];
            Vector2 tangent = (next - previous).SafeNormalize(Vector2.UnitX);
            Vector2 normal = tangent.RotatedBy(MathHelper.PiOver2);
            float progress = index / Math.Max(1f, count - 1f);
            float taper = GetTrailTaper(state, progress);
            float halfWidth = width * taper * 0.5f;

            for (int side = 0; side < 2; side++)
            {
                Vector2 world = point + normal * (side == 0 ? -halfWidth : halfWidth);
                Vector2 screen = world - Main.screenPosition;
                Vector2 uv = backdropLayer >= 0
                    ? DeathDomainBackdropTextureSystem.GetMatchingDomainUv(
                        backdropLayer, world, state.Owner,
                        GetBackgroundAnchor(state))
                    : Vector2.Zero;
                vertices[index * 2 + side] = new VertexPositionColorTexture(
                    new Vector3(screen, 0f), color * GetEndNoiseOpacity(state,
                        progress, side == 0 ? -1f : 1f), uv);
            }
        }
        return count;
    }

    private static short[] CreateIndices()
    {
        short[] result = new short[(MaximumPoints - 1) * 6];
        int cursor = 0;
        for (short index = 0; index < MaximumPoints - 1; index++)
        {
            short leftTop = (short)(index * 2);
            short leftBottom = (short)(leftTop + 1);
            short rightTop = (short)(leftTop + 2);
            short rightBottom = (short)(leftTop + 3);
            result[cursor++] = leftTop;
            result[cursor++] = leftBottom;
            result[cursor++] = rightTop;
            result[cursor++] = rightTop;
            result[cursor++] = leftBottom;
            result[cursor++] = rightBottom;
        }
        return result;
    }
}