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 System;
using System.Collections.Generic;
using System.IO;
using Terraria;
using Terraria.ID;
using Terraria.ModLoader;

namespace SoulHarvest.Projectiles;

public sealed class SoulSickleProjectile : ModProjectile
{
    private const int HomingDelayFrames = 8;

    private SickleCombatSnapshot snapshot;
    private ReaperHitKind hitKind;
    private int phase;
    private int actionId;
    private int variant;
    private int timer;
    private int targetIndex = -1;
    private bool configured;
    private bool serverAuthorized;
    private int authoritativeDamage;
    private Vector2 authoritativeCenter;
    private Vector2 authoritativeVelocity;
    private readonly HashSet<int> hitRoots = [];

    internal static int Spawn(Terraria.DataStructures.IEntitySource source, int owner,
        in SickleCombatSnapshot snapshot, Vector2 position, Vector2 velocity,
        int phase, int actionId, ReaperHitKind hitKind, float damageMultiplier,
        int variant = 0)
    {
        if (Main.netMode == NetmodeID.MultiplayerClient)
            return -1;

        int projectileIndex = Projectile.NewProjectile(source, position, velocity,
            ModContent.ProjectileType<SoulSickleProjectile>(),
            Math.Max(1, (int)Math.Round(snapshot.Damage * damageMultiplier)),
            snapshot.Knockback * 0.45f, owner);
        if (projectileIndex < 0 || projectileIndex >= Main.maxProjectiles)
            return -1;

        Projectile projectile = Main.projectile[projectileIndex];
        projectile.originalDamage = snapshot.Damage;
        projectile.CritChance = snapshot.CritChance;
        if (projectile.ModProjectile is SoulSickleProjectile soulBlade)
            soulBlade.Configure(snapshot, hitKind, phase, actionId, variant);
        projectile.GetGlobalProjectile<MyGlobalProjectile>().ConfigureReaperProjectile(
            projectile, snapshot, hitKind, phase, velocity.ToRotation(), actionId);
        ReaperProjectileHelper.SyncNewProjectile(projectile);
        return projectileIndex;
    }

    public override void SetStaticDefaults()
    {
        ProjectileID.Sets.TrailCacheLength[Type] = 24;
        ProjectileID.Sets.TrailingMode[Type] = 2;
    }

    public override void SetDefaults()
    {
        Projectile.width = 42;
        Projectile.height = 42;
        Projectile.friendly = true;
        Projectile.hostile = false;
        Projectile.tileCollide = false;
        Projectile.penetrate = 4;
        Projectile.ignoreWater = true;
        Projectile.timeLeft = 150;
        Projectile.DamageType = DamageClass.Melee;
        Projectile.netImportant = true;
        Projectile.usesLocalNPCImmunity = true;
        Projectile.localNPCHitCooldown = 14;
    }

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

    public override void AI()
    {
        if (Main.netMode == NetmodeID.Server)
        {
            if (!serverAuthorized)
            {
                Projectile.Kill();
                return;
            }
            Projectile.damage = authoritativeDamage;
            Projectile.Center = authoritativeCenter;
            Projectile.velocity = authoritativeVelocity;
        }
        if (!configured)
            return;

        timer++;
        int curveDirection = (variant & 1) == 0 ? 1 : -1;
        if (timer <= HomingDelayFrames)
        {
            Projectile.velocity = Projectile.velocity.RotatedBy(curveDirection
                * (0.024f + snapshot.StageNumber * 0.004f));
        }
        else
        {
            NPC? target = GetOrFindTarget();
            if (target is not null)
            {
                float speed = Math.Max(14f + snapshot.StageNumber,
                    Projectile.velocity.Length());
                Vector2 desired = (target.Center - Projectile.Center)
                    .SafeNormalize(Projectile.velocity.SafeNormalize(Vector2.UnitX))
                    * speed;
                Projectile.velocity = Vector2.Lerp(Projectile.velocity, desired,
                    0.085f + snapshot.StageNumber * 0.018f);
            }
            else
            {
                Projectile.velocity = Projectile.velocity.RotatedBy(
                    curveDirection * 0.006f);
            }
        }

        Projectile.rotation = Projectile.velocity.ToRotation();
        Projectile.GetGlobalProjectile<MyGlobalProjectile>()
            .UpdateReaperAttackAngle(Projectile.rotation);
        Projectile.position += Projectile.velocity;
        if (Main.netMode == NetmodeID.Server)
        {
            authoritativeCenter = Projectile.Center;
            authoritativeVelocity = Projectile.velocity;
        }

        if (!Main.dedServ)
        {
            Lighting.AddLight(Projectile.Center, new Vector3(0.22f, 0.34f, 0.82f));
            if (Main.rand.NextBool(2))
            {
                Color color = Main.rand.NextBool()
                    ? new Color(85, 225, 255)
                    : new Color(175, 85, 255);
                Dust dust = Dust.NewDustPerfect(Projectile.Center
                    + Main.rand.NextVector2Circular(11f, 11f), DustID.AncientLight,
                    -Projectile.velocity * 0.08f, 55, color, 0.95f);
                dust.noGravity = true;
            }
        }
    }

    public override bool PreDraw(ref Color lightColor)
        => ReaperProjectileHelper.DrawTrail(Projectile, new Color(120, 115, 255),
            scaleMultiplier: 1.04f + snapshot.StageNumber * 0.05f, copies: 22);

    public override void OnHitNPC(NPC target, NPC.HitInfo hit, int damageDone)
    {
        int root = target.realLife >= 0 ? target.realLife : target.whoAmI;
        hitRoots.Add(root);
        if (targetIndex == target.whoAmI)
            targetIndex = -1;
        if (Main.netMode != NetmodeID.MultiplayerClient)
            Projectile.netUpdate = true;
    }

    public override void SendExtraAI(BinaryWriter writer)
    {
        writer.Write(configured);
        if (!configured)
            return;
        snapshot.Write(writer);
        writer.Write((byte)hitKind);
        writer.Write((byte)Math.Clamp(phase, 0, byte.MaxValue));
        writer.Write(actionId);
        writer.Write((byte)Math.Clamp(variant, 0, byte.MaxValue));
        writer.Write((short)Math.Clamp(timer, 0, short.MaxValue));
        writer.Write((short)targetIndex);
    }

    public override void ReceiveExtraAI(BinaryReader reader)
    {
        bool incomingConfigured = reader.ReadBoolean();
        if (!incomingConfigured)
        {
            if (Main.netMode != NetmodeID.Server)
                configured = false;
            return;
        }

        SickleCombatSnapshot incomingSnapshot = SickleCombatSnapshot.Read(reader);
        ReaperHitKind incomingHitKind = (ReaperHitKind)reader.ReadByte();
        int incomingPhase = reader.ReadByte();
        int incomingActionId = reader.ReadInt32();
        int incomingVariant = reader.ReadByte();
        int incomingTimer = reader.ReadInt16();
        int incomingTarget = reader.ReadInt16();
        if (Main.netMode == NetmodeID.Server)
            return;

        snapshot = incomingSnapshot;
        hitKind = incomingHitKind;
        phase = incomingPhase;
        actionId = incomingActionId;
        variant = incomingVariant;
        timer = incomingTimer;
        targetIndex = incomingTarget;
        configured = true;
    }

    private void Configure(in SickleCombatSnapshot value, ReaperHitKind kind,
        int attackPhase, int parentActionId, int projectileVariant)
    {
        snapshot = value;
        hitKind = kind;
        phase = Math.Max(0, attackPhase);
        actionId = parentActionId;
        variant = Math.Max(0, projectileVariant);
        timer = 0;
        targetIndex = -1;
        hitRoots.Clear();
        configured = true;
        serverAuthorized = Main.netMode != NetmodeID.MultiplayerClient;
        authoritativeDamage = Math.Max(1, Projectile.damage);
        authoritativeCenter = Projectile.Center;
        authoritativeVelocity = Projectile.velocity;
    }

    private NPC? GetOrFindTarget()
    {
        float retentionRadius = 760f + snapshot.StageNumber * 80f;
        int previousTarget = targetIndex;
        if (targetIndex >= 0 && targetIndex < Main.maxNPCs)
        {
            NPC current = Main.npc[targetIndex];
            int currentRoot = current.realLife >= 0
                ? current.realLife
                : current.whoAmI;
            if (!hitRoots.Contains(currentRoot)
                && ReaperTargeting.IsValidWeaponTarget(current)
                && (ReaperTargeting.IsTrainingDummy(current)
                    || current.CanBeChasedBy(Projectile))
                && Vector2.DistanceSquared(Projectile.Center, current.Center)
                    <= retentionRadius * retentionRadius)
            {
                return current;
            }
            targetIndex = -1;
        }

        NPC? target = FindUnhitTarget(520f + snapshot.StageNumber * 90f);
        targetIndex = target?.whoAmI ?? -1;
        if (targetIndex != previousTarget
            && Main.netMode != NetmodeID.MultiplayerClient)
            Projectile.netUpdate = true;
        return target;
    }

    private NPC? FindUnhitTarget(float maximumDistance)
    {
        NPC? target = null;
        foreach (NPC npc in Main.ActiveNPCs)
        {
            int root = npc.realLife >= 0 ? npc.realLife : npc.whoAmI;
            if (hitRoots.Contains(root)
                || !ReaperTargeting.IsValidWeaponTarget(npc)
                || !ReaperTargeting.IsTrainingDummy(npc)
                    && !npc.CanBeChasedBy(Projectile))
            {
                continue;
            }

            float distance = Vector2.Distance(npc.Center, Projectile.Center);
            if (distance >= maximumDistance)
                continue;
            maximumDistance = distance;
            target = npc;
        }
        return target;
    }
}