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

namespace SoulHarvest.Common;


/// <summary>
/// Client-only compositor for short, screen-space Reaper impacts. Gameplay remains
/// server authoritative; this class only combines camera impulses and capped flashes.
/// </summary>
[Autoload(Side = ModSide.Client)]
public sealed class ReaperVfxDirector : ModSystem
{
    private const float MaximumShakeStrength = 12f;
    private const float MaximumFlashOpacity = 0.78f;
    private const float MaximumCinematicZoom = 1.11f;
    private const float MinimumCinematicZoom = 0.985f;
    private const float MaximumCinematicBias = 22f;

    private static int shakeFrames;
    private static int shakeDuration;
    private static float shakeStrength;
    private static Vector2 shakeDirection = Vector2.UnitY;
    private static float shakePhase;

    private static int flashFrames;
    private static int flashDuration;
    private static float flashOpacity;
    private static Color flashColor = Color.White;
    private static float edgeDarkness;

    private static CinematicCameraState[] cinematicStates = [];

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

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

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

        if (shakeFrames > 0)
            shakeFrames--;
        else
            shakeStrength = 0f;

        if (flashFrames > 0)
            flashFrames--;
        else
        {
            flashOpacity = 0f;
            edgeDarkness = 0f;
        }

        EnsureCinematicStorage();
        for (int index = 0; index < cinematicStates.Length; index++)
        {
            ref CinematicCameraState state = ref cinematicStates[index];
            if (state.Active && Main.GameUpdateCount > state.LastReportTick + 2UL)
                state = default;
        }
    }

    public override void ModifyTransformMatrix(ref SpriteViewMatrix transform)
    {
        if (Main.dedServ || Main.gameMenu)
            return;

        ComposeCinematicCamera(out float zoomScale, out _);
        if (Math.Abs(zoomScale - 1f) <= 0.0001f)
            return;

        transform.Zoom *= zoomScale;
    }

    public override void ModifyInterfaceLayers(List<GameInterfaceLayer> layers)
    {
        int index = layers.FindIndex(layer => layer.Name == "Vanilla: Resource Bars");
        if (index < 0)
            index = layers.FindIndex(layer => layer.Name == "Vanilla: Mouse Text");
        if (index < 0)
            index = layers.Count;

        layers.Insert(index, new LegacyGameInterfaceLayer(
            "SoulHarvest: Reaper Impact Composite",
            DrawImpactOverlay,
            InterfaceScaleType.None));
    }

    /// <summary>
    /// Adds a global impact without distance falloff. Ultimate timelines use this so
    /// every client which receives the action sees the same full-strength punctuation.
    /// Concurrent calls use maxima instead of unbounded addition.
    /// </summary>
    public static void TriggerGlobalImpact(
        Vector2 direction,
        float strength,
        int cameraFrames,
        Color color,
        float opacity,
        int colorFrames,
        float vignette = 0.28f)
    {
        if (Main.dedServ || Main.gameMenu)
            return;

        direction = direction.SafeNormalize(Vector2.UnitY);
        strength = MathHelper.Clamp(strength, 0f, MaximumShakeStrength);
        cameraFrames = Math.Clamp(cameraFrames, 0, 30);
        opacity = MathHelper.Clamp(opacity, 0f, MaximumFlashOpacity);
        colorFrames = Math.Clamp(colorFrames, 0, 30);

        // Bright full-viewport overlays read as a white-screen defect when rapid
        // melee or passive events overlap. Keep their camera punch and vignette,
        // but suppress the bright color plane entirely.
        Vector3 rgb = color.ToVector3();
        float luminance = rgb.X * 0.2126f + rgb.Y * 0.7152f + rgb.Z * 0.0722f;
        if (luminance >= 0.64f)
        {
            opacity = 0f;
            colorFrames = 0;
        }

        if (strength >= shakeStrength || shakeFrames <= 1)
        {
            shakeStrength = strength;
            shakeDirection = direction;
            shakeDuration = Math.Max(1, cameraFrames);
            shakeFrames = cameraFrames;
            shakePhase += 1.731f;
        }
        else
        {
            shakeFrames = Math.Max(shakeFrames, cameraFrames);
            shakeDuration = Math.Max(shakeDuration, shakeFrames);
            shakeDirection = (shakeDirection + direction * 0.45f).SafeNormalize(shakeDirection);
        }

        // A camera-only impact must not create, extend or recolor an unrelated
        // full-screen flash that is already active (for example an overlapping
        // ultimate). Unique-skill cues intentionally pass zero here.
        if (opacity > 0.001f && colorFrames > 0)
        {
            if (opacity >= flashOpacity || flashFrames <= 1)
            {
                flashColor = color;
                flashOpacity = opacity;
                flashDuration = Math.Max(1, colorFrames);
                flashFrames = colorFrames;
            }
            else
            {
                flashFrames = Math.Max(flashFrames, colorFrames);
                flashDuration = Math.Max(flashDuration, flashFrames);
                flashColor = Color.Lerp(flashColor, color, 0.25f);
            }
        }

        edgeDarkness = Math.Max(edgeDarkness, MathHelper.Clamp(vignette, 0f, 0.62f));
    }

    /// <summary>
    /// Reports an ultimate's normalized visual clock. The resulting camera move is
    /// a push-in, held composition and capped rebound; it never pauses or changes
    /// gameplay time. Concurrent ultimates use the strongest safe zoom instead of
    /// multiplying their transforms together.
    /// </summary>
    public static void ReportCinematicCamera(
        int owner,
        int actionId,
        float normalizedTime,
        Vector2 direction,
        float intensity = 1f)
    {
        if (Main.dedServ || Main.gameMenu || owner < 0 || owner >= Main.maxPlayers)
            return;

        EnsureCinematicStorage();
        ref CinematicCameraState state = ref cinematicStates[owner];
        if (state.Active && state.ActionId == actionId && state.LastReportTick == Main.GameUpdateCount
            && normalizedTime < state.NormalizedTime)
        {
            return;
        }

        state.Active = true;
        state.ActionId = actionId;
        state.NormalizedTime = MathHelper.Clamp(float.IsFinite(normalizedTime) ? normalizedTime : 0f, 0f, 1f);
        state.Direction = direction.SafeNormalize(Vector2.UnitX);
        state.Intensity = MathHelper.Clamp(float.IsFinite(intensity) ? intensity : 1f, 0f, 1f);
        state.LastReportTick = Main.GameUpdateCount;
    }

    /// <summary>
    /// Dispatches a synchronized visual description without granting it authority
    /// to create damage. Skill cues and Void direct-hit cues are supported now; the
    /// same event shape is kept for other normal, special and ultimate storyboards.
    /// </summary>
    internal static bool Submit(in ReaperVisualEvent visualEvent)
    {
        if (Main.dedServ || Main.gameMenu
            || visualEvent.Owner < 0 || visualEvent.Owner >= Main.maxPlayers)
        {
            return false;
        }

        if (visualEvent.ActionKind == ReaperVisualActionKind.NormalAttack
            && visualEvent.Form == ReaperFormId.Void)
        {
            ReaperVoidHitVisualSystem.ReportHit(
                visualEvent.Owner,
                visualEvent.Stage,
                visualEvent.ActionId,
                visualEvent.Target,
                visualEvent.Origin,
                visualEvent.Direction,
                visualEvent.Seed);
            return true;
        }

        if (visualEvent.ActionKind != ReaperVisualActionKind.Skill)
            return false;

        Vector2 center = visualEvent.Origin;
        if (visualEvent.Target >= 0 && visualEvent.Target < Main.maxNPCs)
        {
            NPC target = Main.npc[visualEvent.Target];
            if (target.active)
                center = target.Center;
        }

        ReaperSkillVisualSystem.ReportSkill(
            visualEvent.Owner,
            visualEvent.Form,
            visualEvent.Stage,
            visualEvent.Skill,
            visualEvent.SkillLevel,
            visualEvent.TimelinePhase,
            center,
            visualEvent.Direction,
            visualEvent.ActionId,
            visualEvent.Seed);
        return true;
    }

    /// <summary>Applies the current composite shake to the local camera.</summary>
    public static void ApplyCameraImpulse(ref Vector2 screenPosition)
    {
        if (Main.dedServ)
            return;

        ComposeCinematicCamera(out _, out Vector2 cinematicBias);
        screenPosition += cinematicBias;

        if (shakeFrames <= 0 || shakeStrength <= 0.01f)
            return;

        float remaining = shakeFrames / (float)Math.Max(1, shakeDuration);
        float envelope = remaining * remaining;
        float tick = (float)Main.GameUpdateCount + shakePhase;
        Vector2 tangent = shakeDirection.RotatedBy(MathHelper.PiOver2);
        float along = (float)Math.Sin(tick * 2.31f) * shakeStrength * envelope;
        float across = (float)Math.Sin(tick * 3.77f + 0.9f) * shakeStrength * envelope * 0.52f;
        screenPosition += shakeDirection * along + tangent * across;
    }

    private static bool DrawImpactOverlay()
    {
        if (Main.gameMenu || flashFrames <= 0 || flashOpacity <= 0.001f)
            return true;

        float remaining = flashFrames / (float)Math.Max(1, flashDuration);
        float opacity = flashOpacity * remaining * remaining;
        Rectangle viewport = new(
            0,
            0,
            Math.Max(1, Main.screenWidth),
            Math.Max(1, Main.screenHeight));
        Texture2D pixel = TextureAssets.MagicPixel.Value;
        SpriteBatch batch = Main.spriteBatch;

        batch.Draw(pixel, viewport, flashColor * opacity);

        float edge = edgeDarkness * MathHelper.Clamp(remaining * 1.45f, 0f, 1f);
        if (edge > 0.001f)
            batch.Draw(pixel, viewport, Color.Black * (edge * 0.12f));

        return true;
    }

    private static void Clear()
    {
        shakeFrames = 0;
        shakeDuration = 0;
        shakeStrength = 0f;
        shakeDirection = Vector2.UnitY;
        shakePhase = 0f;
        flashFrames = 0;
        flashDuration = 0;
        flashOpacity = 0f;
        flashColor = Color.White;
        edgeDarkness = 0f;
        cinematicStates = [];
    }

    private static void ComposeCinematicCamera(out float zoomScale, out Vector2 cameraBias)
    {
        zoomScale = 1f;
        cameraBias = Vector2.Zero;
        if (cinematicStates.Length == 0)
            return;

        float strongestPositiveZoom = 0f;
        float strongestRebound = 0f;
        float strongestBias = 0f;
        for (int index = 0; index < cinematicStates.Length; index++)
        {
            ref CinematicCameraState state = ref cinematicStates[index];
            if (!state.Active || Main.GameUpdateCount > state.LastReportTick + 2UL)
                continue;

            EvaluateCinematicCurve(state.NormalizedTime, out float zoomDelta, out float bias);
            zoomDelta *= state.Intensity;
            bias *= state.Intensity;
            if (zoomDelta >= 0f)
                strongestPositiveZoom = Math.Max(strongestPositiveZoom, zoomDelta);
            else
                strongestRebound = Math.Min(strongestRebound, zoomDelta);

            float biasMagnitude = Math.Abs(bias);
            if (biasMagnitude > strongestBias)
            {
                strongestBias = biasMagnitude;
                cameraBias = state.Direction * bias;
            }
        }

        float selectedZoom = strongestPositiveZoom > 0.0001f
            ? strongestPositiveZoom
            : strongestRebound;
        zoomScale = MathHelper.Clamp(1f + selectedZoom, MinimumCinematicZoom, MaximumCinematicZoom);
        if (cameraBias.LengthSquared() > MaximumCinematicBias * MaximumCinematicBias)
            cameraBias = cameraBias.SafeNormalize(Vector2.Zero) * MaximumCinematicBias;
    }

    private static void EvaluateCinematicCurve(float progress, out float zoomDelta, out float bias)
    {
        progress = MathHelper.Clamp(progress, 0f, 1f);
        if (progress < 0.12f)
        {
            float amount = Smooth(progress / 0.12f);
            zoomDelta = MathHelper.Lerp(0f, 0.060f, amount);
            bias = MathHelper.Lerp(0f, 9f, amount);
            return;
        }

        if (progress < 0.76f)
        {
            zoomDelta = 0.060f;
            bias = 9f;
            return;
        }

        if (progress < 0.90f)
        {
            float amount = Smooth((progress - 0.76f) / 0.14f);
            zoomDelta = MathHelper.Lerp(0.060f, 0.105f, amount);
            bias = MathHelper.Lerp(9f, 19f, amount);
            return;
        }

        if (progress < 0.965f)
        {
            float amount = Smooth((progress - 0.90f) / 0.065f);
            zoomDelta = MathHelper.Lerp(0.105f, -0.015f, amount);
            bias = MathHelper.Lerp(19f, -7f, amount);
            return;
        }

        float rebound = Smooth((progress - 0.965f) / 0.035f);
        zoomDelta = MathHelper.Lerp(-0.015f, 0f, rebound);
        bias = MathHelper.Lerp(-7f, 0f, rebound);
    }

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

    private static void EnsureCinematicStorage()
    {
        if (cinematicStates.Length != Main.maxPlayers)
            cinematicStates = new CinematicCameraState[Main.maxPlayers];
    }

    private struct CinematicCameraState
    {
        public bool Active;
        public int ActionId;
        public float NormalizedTime;
        public Vector2 Direction;
        public float Intensity;
        public ulong LastReportTick;
    }
}