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 Death-scythe afterimage anchored at a position cut by the primary attack.
/// It repeatedly acquires enemies from that exact position and performs real,
/// server-authoritative follow-up cuts without summoning any branch weapon.
/// </summary>
public sealed class ReaperDeathPhantomProjectile : ModProjectile
{
    private const int MaximumPhantomsPerOwner = 20;
    private SickleCombatSnapshot snapshot;
    private Vector2 anchor;
    private Vector2 attackDirection = Vector2.UnitX;
    private float tempo = 1f;
    private float weaponAngle;
    private int age;
    private int attackClock;
    private int seed;
    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 snapshot, Vector2 position, Vector2 initialDirection,
        float tempo, int stableSeed)
    {
        if (Main.netMode == NetmodeID.MultiplayerClient)
            return -1;

        TrimOldest(owner);
        int index = Projectile.NewProjectile(source, position, Vector2.Zero,
            ModContent.ProjectileType<ReaperDeathPhantomProjectile>(), 0, 0f, owner);
        if (index < 0 || index >= Main.maxProjectiles)
            return -1;
        Projectile projectile = Main.projectile[index];
        if (projectile.ModProjectile is ReaperDeathPhantomProjectile phantom)
            phantom.Configure(snapshot, position, initialDirection, tempo, stableSeed);
        ReaperProjectileHelper.SyncNewProjectile(projectile);
        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 = 240;
        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++;
        attackClock++;
        Projectile.Center = anchor;
        NPC? target = FindTarget(720f);
        if (target is not null)
        {
            Vector2 desired = (target.Center - anchor).SafeNormalize(attackDirection);
            attackDirection = Vector2.Lerp(attackDirection, desired, 0.18f)
                .SafeNormalize(desired);
        }

        int interval = Math.Clamp((int)Math.Round(MathHelper.Lerp(30f, 12f,
            MathHelper.Clamp((tempo - 1f) / 1.6f, 0f, 1f))), 12, 30);
        float cycle = (attackClock % interval) / (float)interval;
        int facing = attackDirection.X >= 0f ? 1 : -1;
        weaponAngle = attackDirection.ToRotation()
            + MathHelper.Lerp(-1.65f * facing, 1.35f * facing,
                cycle * cycle * (3f - 2f * cycle));

        if (Main.netMode != NetmodeID.MultiplayerClient && target is not null
            && age >= 10 && attackClock >= interval)
        {
            attackClock = 0;
            Vector2 direction = (target.Center - anchor).SafeNormalize(attackDirection);
            float length = MathHelper.Clamp(Vector2.Distance(anchor, target.Center) + 34f,
                72f, 720f);
            ReaperStrikeProjectile.Spawn(Projectile.GetSource_FromThis(), Projectile.owner,
                snapshot, ReaperHitKind.PrimaryDerived, seed % 6, anchor, direction,
                ReaperStrikeShape.Line, length, 22f + tempo * 3f,
                MathHelper.Lerp(0.30f, 0.48f, (tempo - 1f) / 1.6f),
                actionId: seed * 509 + age);
            Projectile.netUpdate = true;
        }
    }

    public override bool PreDraw(ref Color lightColor)
    {
        if (!configured || Main.dedServ)
            return false;
        float fadeIn = MathHelper.Clamp(age / 10f, 0f, 1f);
        float fadeOut = MathHelper.Clamp(Projectile.timeLeft / 35f, 0f, 1f);
        float opacity = fadeIn * fadeOut;
        Asset<Texture2D> asset = ModContent.Request<Texture2D>(
            ReaperCombatRegistry.GetTexturePath(ReaperFormId.Death, ReaperStage.StageIII));
        Texture2D texture = asset.Value;
        Vector2 anchorRatio = ReaperCombatRegistry.GetHandleAnchor(
            ReaperFormId.Death, ReaperStage.StageIII);
        Vector2 origin = new(texture.Width * anchorRatio.X, texture.Height * anchorRatio.Y);
        SpriteEffects effects = attackDirection.X < 0f
            ? SpriteEffects.FlipVertically : SpriteEffects.None;
        if ((effects & SpriteEffects.FlipVertically) != 0)
            origin.Y = texture.Height - origin.Y;
        float correction = ReaperCombatRegistry.GetTextureRotationCorrection(
            ReaperFormId.Death, ReaperStage.StageIII);
        Vector2 drawPosition = anchor - Main.screenPosition;
        Color shadow = new Color(32, 0, 18, 0) * (opacity * 0.58f);
        Color edge = new Color(245, 24, 80, 0) * (opacity * 0.72f);
        Main.EntitySpriteDraw(texture, drawPosition, null, shadow,
            weaponAngle + correction - 0.12f, origin, 1.08f, effects);
        Main.EntitySpriteDraw(texture, drawPosition, null, edge,
            weaponAngle + correction, origin, 0.93f, effects);
        Main.EntitySpriteDraw(texture, drawPosition, null, Color.White * (opacity * 0.34f),
            weaponAngle + correction, origin, 0.855f, effects);
        return false;
    }

    public override void SendExtraAI(BinaryWriter writer)
    {
        writer.Write(configured);
        if (!configured)
            return;
        snapshot.Write(writer);
        writer.WriteVector2(anchor);
        writer.WriteVector2(attackDirection);
        writer.Write(tempo);
        writer.Write(seed);
        writer.Write((short)Math.Clamp(age, 0, short.MaxValue));
        writer.Write((short)Math.Clamp(attackClock, 0, short.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);
        Vector2 incomingAnchor = reader.ReadVector2();
        Vector2 incomingDirection = reader.ReadVector2();
        float incomingTempo = reader.ReadSingle();
        int incomingSeed = reader.ReadInt32();
        int incomingAge = reader.ReadInt16();
        int incomingClock = reader.ReadInt16();
        if (Main.netMode == NetmodeID.Server)
            return;
        snapshot = incomingSnapshot;
        anchor = incomingAnchor;
        attackDirection = incomingDirection.SafeNormalize(Vector2.UnitX);
        tempo = MathHelper.Clamp(incomingTempo, 1f, 2.6f);
        seed = incomingSeed;
        age = incomingAge;
        attackClock = incomingClock;
        configured = true;
    }

    private void Configure(in SickleCombatSnapshot value, Vector2 position,
        Vector2 initialDirection, float attackTempo, int stableSeed)
    {
        snapshot = value;
        anchor = position;
        attackDirection = initialDirection.SafeNormalize(Vector2.UnitX);
        tempo = MathHelper.Clamp(attackTempo, 1f, 2.6f);
        seed = stableSeed;
        age = 0;
        attackClock = 0;
        configured = true;
        serverAuthorized = Main.netMode != NetmodeID.MultiplayerClient;
        Projectile.Center = anchor;
        Projectile.timeLeft = 240;
    }

    private NPC? FindTarget(float maximumDistance)
    {
        NPC? nearest = null;
        float best = maximumDistance * maximumDistance;
        foreach (NPC npc in Main.ActiveNPCs)
        {
            if (!ReaperTargeting.IsValidWeaponTarget(npc)
                || !ReaperTargeting.IsTrainingDummy(npc)
                    && !npc.CanBeChasedBy(Projectile))
                continue;
            float distance = Vector2.DistanceSquared(npc.Center, anchor);
            if (distance >= best)
                continue;
            best = distance;
            nearest = npc;
        }
        return nearest;
    }

    private static void TrimOldest(int owner)
    {
        int type = ModContent.ProjectileType<ReaperDeathPhantomProjectile>();
        Projectile? oldest = null;
        int count = 0;
        foreach (Projectile projectile in Main.ActiveProjectiles)
        {
            if (projectile.owner != owner || projectile.type != type)
                continue;
            count++;
            if (oldest is null || projectile.timeLeft < oldest.timeLeft)
                oldest = projectile;
        }
        if (count >= MaximumPhantomsPerOwner)
            oldest?.Kill();
    }
}