XFE Git
XFE Studio Git
Git 首页 全局搜索
XFE 主站 文档 NuGet

DeathMod

【Terraria】DeathMod (now aka Soul Harvest)

公开
关注 0 Fork 0 Star 0
UTF-8
using SoulHarvest.Common;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using ReLogic.Content;
using System;
using System.IO;
using Terraria;
using Terraria.ID;
using Terraria.ModLoader;

namespace SoulHarvest.Projectiles;

/// <summary>A Stage-III branch scythe summoned by Death's held special.</summary>
public sealed class ReaperDeathEchoScytheProjectile : ModProjectile
{
    private const int HistoryLength = 16;
    private const int SlashCycleFrames = 20;
    private const int StrikeFrame = 8;
    private const int ResidualFrame = 15;
    private static readonly ReaperFormId[] BranchForms =
    [
        ReaperFormId.Bone, ReaperFormId.Blood, ReaperFormId.Infernal,
        ReaperFormId.Frost, ReaperFormId.Soul, ReaperFormId.Void
    ];

    private readonly Vector2[] tipHistory = new Vector2[HistoryLength];
    private SickleCombatSnapshot snapshot;
    private ReaperFormId branchForm;
    private Vector2 summonOrigin;
    private Vector2 strikeFocus;
    private Vector2 focus;
    private Vector2 aim;
    private float weaponAngle;
    private int phase;
    private int actionId;
    private int age;
    private int validHistory;
    private int lastStrikeCycle = -1;
    private int lastResidualCycle = -1;
    private bool configured;
    private bool serverAuthorized;

    public override string Texture => "Terraria/Images/Projectile_0";

    internal static int Spawn(Terraria.DataStructures.IEntitySource source, int owner,
        in SickleCombatSnapshot deathSnapshot, int phase, Vector2 origin,
        Vector2 targetFocus, int actionId)
    {
        if (Main.netMode == NetmodeID.MultiplayerClient)
            return -1;
        phase = Math.Clamp(phase, 0, BranchForms.Length - 1);
        Vector2 direction = (targetFocus - origin).SafeNormalize(Vector2.UnitX);
        int index = Projectile.NewProjectile(source, origin, direction,
            ModContent.ProjectileType<ReaperDeathEchoScytheProjectile>(), 0, 0f, owner);
        if (index < 0 || index >= Main.maxProjectiles)
            return -1;
        Projectile projectile = Main.projectile[index];
        if (projectile.ModProjectile is ReaperDeathEchoScytheProjectile echo)
            echo.Configure(deathSnapshot, phase, origin, targetFocus, actionId);
        projectile.netUpdate = true;
        if (Main.netMode == NetmodeID.Server)
        {
            // The echo is configured after NewProjectile. Push that completed
            // snapshot immediately so multiplayer clients see and animate every
            // summoned right-click scythe from its first cycle.
            NetMessage.SendData(MessageID.SyncProjectile, -1, -1, null,
                index);
        }
        return index;
    }

    public override void SetDefaults()
    {
        Projectile.width = 4;
        Projectile.height = 4;
        Projectile.friendly = false;
        Projectile.hostile = false;
        Projectile.tileCollide = false;
        Projectile.ignoreWater = true;
        Projectile.penetrate = -1;
        Projectile.timeLeft = 190;
        Projectile.netImportant = true;
    }

    public override bool ShouldUpdatePosition() => false;
    public override bool? CanDamage() => false;

    public override void AI()
    {
        if (Main.netMode == NetmodeID.Server && !serverAuthorized)
        {
            Projectile.Kill();
            return;
        }
        if (!configured)
            return;
        age++;
        int slashCount = snapshot.DeathInvocationSlashCount;
        int cycle = Math.Min(slashCount - 1, Math.Max(0, (age - 1) / SlashCycleFrames));
        int cycleAge = Math.Max(1, (age - 1) % SlashCycleFrames + 1);
        float travel = Smooth01(MathHelper.Clamp(age / 11f, 0f, 1f));
        Vector2 arrival = strikeFocus - aim * 42f;
        focus = Vector2.Lerp(summonOrigin, arrival, travel);
        Projectile.Center = focus;
        if (cycleAge == 1)
            validHistory = 0;
        float progress = MathHelper.Clamp((cycleAge - 2f) / 14f, 0f, 1f);
        float eased = progress * progress * (3f - 2f * progress);
        int facing = aim.X >= 0f ? 1 : -1;
        int swingDirection = (cycle & 1) == 0 ? facing : -facing;
        weaponAngle = MathHelper.Lerp(aim.ToRotation() - 2.35f * swingDirection,
            aim.ToRotation() + 2.15f * swingDirection, eased);
        RecordTip(focus + weaponAngle.ToRotationVector2()
            * ReaperCombatRegistry.GetBladeTipLength(branchForm, ReaperStage.StageIII)
            * snapshot.DeathRangeMultiplier);

        if (lastStrikeCycle < cycle && cycleAge >= StrikeFrame
            && Main.netMode != NetmodeID.MultiplayerClient)
        {
            lastStrikeCycle = cycle;
            SpawnBranchExecution(cycle);
        }
        if (lastResidualCycle < cycle && cycleAge >= ResidualFrame
            && validHistory >= 2
            && Main.netMode != NetmodeID.MultiplayerClient
            && branchForm is ReaperFormId.Blood or ReaperFormId.Void)
        {
            lastResidualCycle = cycle;
            if (branchForm == ReaperFormId.Blood)
            {
                ReaperBloodArcScarProjectile.Spawn(Projectile.GetSource_FromThis(),
                    Projectile.owner, snapshot, tipHistory, validHistory, phase,
                    actionId + cycle, 0.25f);
            }
            else
            {
                ReaperVoidArcRiftProjectile.Spawn(Projectile.GetSource_FromThis(),
                    Projectile.owner, snapshot, tipHistory, validHistory, phase,
                    actionId + cycle, 0.25f);
            }
        }
        if (age >= slashCount * SlashCycleFrames + 2)
            Projectile.Kill();
    }

    public override bool PreDraw(ref Color lightColor)
    {
        if (!configured || Main.dedServ)
            return false;
        int cycleAge = Math.Max(1, (age - 1) % SlashCycleFrames + 1);
        float opacity = Smooth01(MathHelper.Clamp(cycleAge / 3f, 0f, 1f))
            * Smooth01(MathHelper.Clamp((SlashCycleFrames + 1f - cycleAge) / 5f,
                0f, 1f));
        if (age <= 35)
        {
            float portalReveal = Smooth01(age / 15f);
            float portalClose = 1f - Smooth01((age - 20f) / 15f);
            DeathDomainPortalRenderer.Draw(summonOrigin, aim,
                1.16f * snapshot.DeathRangeMultiplier,
                Math.Min(portalReveal, portalClose), actionId);
        }
        DrawBranchTrail(opacity);

        Asset<Texture2D> asset = ModContent.Request<Texture2D>(
            ReaperCombatRegistry.GetTexturePath(branchForm, ReaperStage.StageIII));
        Texture2D texture = asset.Value;
        Vector2 anchor = ReaperCombatRegistry.GetHandleAnchor(branchForm, ReaperStage.StageIII);
        Vector2 origin = new(texture.Width * anchor.X, texture.Height * anchor.Y);
        int swingDirection = aim.X >= 0f ? 1 : -1;
        SpriteEffects effects = swingDirection < 0
            ? SpriteEffects.FlipVertically : SpriteEffects.None;
        if ((effects & SpriteEffects.FlipVertically) != 0)
            origin.Y = texture.Height - origin.Y;
        float correction = ReaperCombatRegistry.GetTextureRotationCorrection(
            branchForm, ReaperStage.StageIII);
        Main.EntitySpriteDraw(texture, focus - Main.screenPosition, null,
            ReaperCombatRegistry.GetPrimaryColor(branchForm) with { A = 0 },
            weaponAngle + correction, origin,
            0.74f * snapshot.DeathRangeMultiplier, effects);
        Main.EntitySpriteDraw(texture, focus - Main.screenPosition, null,
            Color.White * opacity, weaponAngle + correction, origin,
            0.62f * snapshot.DeathRangeMultiplier,
            effects);
        return false;
    }

    public override void SendExtraAI(BinaryWriter writer)
    {
        writer.Write(configured);
        if (!configured)
            return;
        snapshot.Write(writer);
        writer.Write((byte)branchForm);
        writer.Write((byte)phase);
        writer.Write(actionId);
        writer.WriteVector2(summonOrigin);
        writer.WriteVector2(strikeFocus);
        writer.WriteVector2(aim);
        writer.Write((byte)Math.Clamp(age, 0, byte.MaxValue));
        writer.Write((sbyte)Math.Clamp(lastStrikeCycle, -1, sbyte.MaxValue));
        writer.Write((sbyte)Math.Clamp(lastResidualCycle, -1, sbyte.MaxValue));
    }

    public override void ReceiveExtraAI(BinaryReader reader)
    {
        bool incoming = reader.ReadBoolean();
        if (!incoming)
        {
            if (Main.netMode != NetmodeID.Server)
                configured = false;
            return;
        }
        SickleCombatSnapshot incomingSnapshot = SickleCombatSnapshot.Read(reader);
        ReaperFormId incomingForm = (ReaperFormId)reader.ReadByte();
        int incomingPhase = reader.ReadByte();
        int incomingAction = reader.ReadInt32();
        Vector2 incomingOrigin = reader.ReadVector2();
        Vector2 incomingStrikeFocus = reader.ReadVector2();
        Vector2 incomingAim = reader.ReadVector2();
        int incomingAge = reader.ReadByte();
        int incomingStrikeCycle = reader.ReadSByte();
        int incomingResidualCycle = reader.ReadSByte();
        if (Main.netMode == NetmodeID.Server)
            return;
        snapshot = incomingSnapshot;
        branchForm = incomingForm;
        phase = incomingPhase;
        actionId = incomingAction;
        summonOrigin = incomingOrigin;
        strikeFocus = incomingStrikeFocus;
        focus = Vector2.Lerp(summonOrigin, strikeFocus - incomingAim * 42f,
            Smooth01(MathHelper.Clamp(incomingAge / 11f, 0f, 1f)));
        aim = incomingAim;
        age = incomingAge;
        lastStrikeCycle = incomingStrikeCycle;
        lastResidualCycle = incomingResidualCycle;
        configured = true;
    }

    private void Configure(in SickleCombatSnapshot deathSnapshot, int attackPhase,
        Vector2 origin, Vector2 targetFocus, int parentActionId)
    {
        phase = Math.Clamp(attackPhase, 0, BranchForms.Length - 1);
        branchForm = BranchForms[phase];
        snapshot = deathSnapshot with
        {
            Form = branchForm,
            Stage = ReaperStage.StageIII,
            Skill1 = 3,
            Skill2 = 3,
            Skill3 = 3
        };
        summonOrigin = origin;
        strikeFocus = targetFocus;
        focus = origin;
        aim = (targetFocus - origin).SafeNormalize(Vector2.UnitX);
        actionId = parentActionId;
        age = 0;
        validHistory = 0;
        lastStrikeCycle = -1;
        lastResidualCycle = -1;
        Projectile.timeLeft = snapshot.DeathInvocationSlashCount
            * SlashCycleFrames + 6;
        configured = true;
        serverAuthorized = Main.netMode != NetmodeID.MultiplayerClient;
    }

    private void SpawnBranchExecution(int slashCycle)
    {
        Vector2 direction = weaponAngle.ToRotationVector2();
        Vector2 executionFocus = strikeFocus;
        ReaperStrikeShape shape = branchForm switch
        {
            ReaperFormId.Bone or ReaperFormId.Frost => ReaperStrikeShape.Cross,
            ReaperFormId.Soul => ReaperStrikeShape.Circle,
            _ => ReaperStrikeShape.Line
        };
        float length = (shape == ReaperStrikeShape.Circle ? 94f : 205f)
            * snapshot.DeathRangeMultiplier;
        Vector2 start = shape == ReaperStrikeShape.Line
            ? executionFocus - direction * length * 0.5f : executionFocus;
        ReaperStrikeProjectile.Spawn(Projectile.GetSource_FromThis(), Projectile.owner,
            snapshot, ReaperHitKind.Primary, 0, start, direction, shape,
            length, 25f * snapshot.DeathRangeMultiplier, 0.75f,
            actionId: actionId + slashCycle);
        ReaperSkillCueProjectile.Spawn(Projectile.GetSource_FromThis(), Projectile.owner,
            snapshot, phase % 3, executionFocus, direction, variant: phase,
            actionId: unchecked(actionId * 8 + phase + slashCycle * 47));
    }

    private void RecordTip(Vector2 point)
    {
        for (int index = HistoryLength - 1; index > 0; index--)
            tipHistory[index] = tipHistory[index - 1];
        tipHistory[0] = point;
        validHistory = Math.Min(HistoryLength, validHistory + 1);
    }

    private void DrawBranchTrail(float opacity)
    {
        for (int index = 1; index < validHistory; index++)
        {
            Vector2 start = tipHistory[index];
            Vector2 end = tipHistory[index - 1];
            float progress = 1f - index / (float)validHistory;
            float width = 4f + 13f * progress;
            float alpha = opacity * progress;
            switch (branchForm)
            {
                case ReaperFormId.Bone:
                    DeathDomainPrimitiveTextureSystem.DrawSmoothWorldStrip(start, end,
                        new Color(18, 96, 128, 0) * (alpha * 0.72f), width * 1.18f);
                    DeathDomainPrimitiveTextureSystem.DrawSmoothWorldStrip(start, end,
                        new Color(226, 239, 211, 235) * alpha, width * 0.32f);
                    if (index % 3 == 0)
                        DrawTrailNode(end, width * 0.34f,
                            new Color(231, 241, 216, 230) * alpha);
                    break;
                case ReaperFormId.Blood:
                    ReaperBloodTrailRenderer.DrawSegment(start, end, width * 0.72f,
                        alpha);
                    break;
                case ReaperFormId.Infernal:
                    DeathDomainPrimitiveTextureSystem.DrawSmoothWorldStrip(start, end,
                        new Color(18, 4, 0, 245) * alpha, width * 1.48f);
                    DeathDomainPrimitiveTextureSystem.DrawSmoothWorldStrip(start, end,
                        new Color(255, 77, 5, 0) * alpha, width * 0.84f);
                    DeathDomainPrimitiveTextureSystem.DrawSmoothWorldStrip(start, end,
                        new Color(255, 238, 150, 235) * alpha, width * 0.17f);
                    break;
                case ReaperFormId.Frost:
                    DeathDomainPrimitiveTextureSystem.DrawSmoothWorldStrip(start, end,
                        new Color(33, 123, 205, 0) * (alpha * 0.64f), width * 1.22f);
                    DeathDomainPrimitiveTextureSystem.DrawSmoothWorldStrip(start, end,
                        new Color(197, 244, 255, 235) * alpha, width * 0.24f);
                    if ((index & 3) == 0)
                        DrawTrailNode(end, width * 0.27f,
                            new Color(218, 251, 255, 220) * alpha);
                    break;
                case ReaperFormId.Soul:
                {
                    Vector2 normal = (end - start).SafeNormalize(Vector2.UnitX)
                        .RotatedBy(MathHelper.PiOver2) * (float)Math.Sin(index * 1.7f) * 3f;
                    DeathDomainPrimitiveTextureSystem.DrawSmoothWorldStrip(start + normal,
                        end + normal, new Color(80, 24, 154, 0) * (alpha * 0.52f),
                        width * 1.5f);
                    DeathDomainPrimitiveTextureSystem.DrawSmoothWorldStrip(start - normal,
                        end - normal, new Color(126, 235, 255, 0) * alpha,
                        width * 0.23f);
                    break;
                }
                case ReaperFormId.Void:
                    ReaperVoidBackdropRenderer.DrawTrailStrip(Main.spriteBatch,
                        start, end, width * 0.72f, alpha, true,
                        actionId, index);
                    break;
            }
        }
    }

    private static void DrawTrailNode(Vector2 worldPosition, float radius, Color color)
    {
        DeathDomainPrimitiveTextureSystem.DrawSmoothDisc(Main.spriteBatch,
            worldPosition - Main.screenPosition, radius, color);
    }

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