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>
/// Client-only world-space presentation for synchronized Void Reaper direct hits.
/// The server supplies immutable event data; this system only draws short-lived
/// negative-space rifts and cannot create damage or gameplay state.
/// </summary>
[Autoload(Side = ModSide.Client)]
internal sealed class ReaperVoidHitVisualSystem : ModSystem
{
    private const int MaximumVisuals = 40;
    private const int MaximumRecentKeys = 2048;
    private const ulong DeduplicationFrames = 300UL;
    private static readonly List<ReaperVoidHitVisual> Visuals = [];
    private static readonly Dictionary<HitKey, ulong> RecentKeys = [];
    private static readonly Queue<RecentHit> RecentOrder = [];

    internal static void ReportHit(
        int owner,
        ReaperStage stage,
        int actionId,
        int target,
        Vector2 origin,
        Vector2 direction,
        int seed)
    {
        if (Main.dedServ || Main.gameMenu
            || owner < 0 || owner >= Main.maxPlayers
            || stage is < ReaperStage.StageI or > ReaperStage.StageIII
            || target is < -1 || target >= Main.maxNPCs
            || !IsFinite(origin)
            || !IsFinite(direction))
        {
            return;
        }

        direction = direction.SafeNormalize(Vector2.UnitX);
        if (!TryReservePresentation(owner, actionId, target))
            return;

        if (Visuals.Count >= MaximumVisuals)
            Visuals.RemoveAt(0);
        Visuals.Add(new ReaperVoidHitVisual
        {
            Owner = owner,
            Stage = stage,
            ActionId = actionId,
            Target = target,
            Seed = seed,
            Origin = origin,
            Direction = direction,
            Duration = GetDuration(stage)
        });
    }

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

    public override void Unload() => Clear();

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

        PruneRecentKeys(Main.GameUpdateCount);

        for (int index = Visuals.Count - 1; index >= 0; index--)
        {
            if (++Visuals[index].Age >= Visuals[index].Duration)
                Visuals.RemoveAt(index);
        }
    }

    public override void PostDrawTiles()
    {
        if (Main.dedServ || Main.gameMenu || Visuals.Count == 0)
            return;

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

        foreach (ReaperVoidHitVisual visual in Visuals)
        {
            // Keep the tear at its authoritative impact coordinate. Following only
            // an NPC slot number can reattach it to a newly spawned NPC after the
            // original target dies, and also makes fast targets cross a snap limit.
            Vector2 center = visual.Origin;
            if (IsNearScreen(center, 180f))
                DrawRift(batch, visual, center);
        }

        batch.End();
    }

    private static void DrawRift(SpriteBatch batch, ReaperVoidHitVisual visual, Vector2 center)
    {
        float progress = visual.Age / (float)Math.Max(1, visual.Duration);
        float reveal = Smooth01(progress / 0.18f);
        float fade = 1f - Smooth01((progress - 0.62f) / 0.38f);
        float opacity = reveal * fade;
        if (opacity <= 0.001f)
            return;

        Vector2 size = GetBladeSize(visual.Stage);
        float impactPulse = 1f + (float)Math.Exp(-Math.Pow((progress - 0.13f) / 0.055f, 2f)) * 0.10f;
        float length = size.X;
        float width = size.Y * impactPulse;
        float side = (visual.Seed & 1) == 0 ? 1f : -1f;
        Vector2 attackDirection = visual.Direction.SafeNormalize(Vector2.UnitX);
        Vector2 axis = attackDirection.RotatedBy(side * MathHelper.PiOver2 * 0.72f)
            .SafeNormalize(Vector2.UnitX);
        ReaperVoidBackdropRenderer.DrawHitRift(batch, center, axis,
            length, width, visual.Seed, reveal, opacity);
    }

    private static int GetDuration(ReaperStage stage) => stage switch
    {
        ReaperStage.StageI => 30,
        ReaperStage.StageII => 37,
        _ => 45
    };

    private static Vector2 GetBladeSize(ReaperStage stage) => stage switch
    {
        ReaperStage.StageI => new Vector2(126f, 12f),
        ReaperStage.StageII => new Vector2(158f, 16f),
        _ => new Vector2(194f, 20f)
    };

    private static bool IsNearScreen(Vector2 center, float margin)
    {
        Rectangle area = new(
            (int)(Main.screenPosition.X - margin),
            (int)(Main.screenPosition.Y - margin),
            Main.screenWidth + (int)(margin * 2f),
            Main.screenHeight + (int)(margin * 2f));
        return area.Contains(center.ToPoint());
    }

    private static bool TryReservePresentation(int owner, int actionId, int target)
    {
        ulong now = Main.GameUpdateCount;
        PruneRecentKeys(now);
        HitKey key = new(owner, actionId, target);
        if (RecentKeys.TryGetValue(key, out ulong tick)
            && now >= tick && now - tick <= DeduplicationFrames)
        {
            return false;
        }

        while (RecentKeys.Count >= MaximumRecentKeys && RecentOrder.Count > 0)
            RemoveOldestRecentKey();

        RecentKeys[key] = now;
        RecentOrder.Enqueue(new RecentHit(key, now));
        return true;
    }

    private static void PruneRecentKeys(ulong now)
    {
        while (RecentOrder.Count > 0)
        {
            RecentHit oldest = RecentOrder.Peek();
            if (now >= oldest.Tick && now - oldest.Tick <= DeduplicationFrames)
                break;
            RemoveOldestRecentKey();
        }
    }

    private static void RemoveOldestRecentKey()
    {
        RecentHit oldest = RecentOrder.Dequeue();
        if (RecentKeys.TryGetValue(oldest.Key, out ulong tick) && tick == oldest.Tick)
            RecentKeys.Remove(oldest.Key);
    }

    private static void Clear()
    {
        Visuals.Clear();
        RecentKeys.Clear();
        RecentOrder.Clear();
    }

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

    private static bool IsFinite(Vector2 value)
        => float.IsFinite(value.X) && float.IsFinite(value.Y);

    private readonly record struct HitKey(int Owner, int ActionId, int Target);

    private readonly record struct RecentHit(HitKey Key, ulong Tick);
}