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

DeathMod

【Terraria】DeathMod (now aka Soul Harvest)

公开
关注 0 Fork 0 Star 0
UTF-8
using SoulHarvest.Buffs;
using SoulHarvest.Items;
using SoulHarvest.Projectiles;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using System;
using System.IO;
using Terraria;
using Terraria.GameContent;
using Terraria.ID;
using Terraria.ModLoader;
using Terraria.ModLoader.IO;

namespace SoulHarvest.Common;

public class MyGlobalNPC : GlobalNPC
{
    internal const int DeathMarkDuration = 60 * 60 * 10;
    internal const int AbsoluteMaximumFatedStacks = NormalSickle.MaxFatedStackCapacity;

    public override bool InstancePerEntity => true;

    private readonly bool[] sickleParticipants = new bool[Main.maxPlayers];
    private readonly int[] fatedStackTimers = new int[AbsoluteMaximumFatedStacks];
    private readonly int[] fatedStackOwners = new int[AbsoluteMaximumFatedStacks];
    private readonly int[] fatedStackLifeStealLevels = new int[AbsoluteMaximumFatedStacks];
    private int lastSicklePlayer = -1;
    private int fatedSecondTimer;
    private bool deathMarked;
    private int deathDomainHitStopFrames;
    private Vector2 deathDomainStoredVelocity;

    public int FatedStackCount { get; private set; }

    public override void SetDefaults(NPC npc)
    {
        Array.Clear(sickleParticipants);
        Array.Clear(fatedStackTimers);
        Array.Fill(fatedStackOwners, -1);
        Array.Clear(fatedStackLifeStealLevels);
        lastSicklePlayer = -1;
        fatedSecondTimer = 0;
        deathMarked = false;
        deathDomainHitStopFrames = 0;
        deathDomainStoredVelocity = Vector2.Zero;
        FatedStackCount = 0;
    }

    public override bool PreAI(NPC npc)
    {
        if (deathDomainHitStopFrames <= 0)
            return true;

        deathDomainHitStopFrames--;
        npc.velocity = Vector2.Zero;
        if (deathDomainHitStopFrames <= 0)
        {
            // Restore only part of the previous movement so the pause reads as an
            // impact without launching fast enemies immediately out of the blade.
            npc.velocity = deathDomainStoredVelocity * 0.35f;
            deathDomainStoredVelocity = Vector2.Zero;
            if (Main.netMode != NetmodeID.MultiplayerClient)
                npc.netUpdate = true;
        }
        return false;
    }

    public override void ModifyHitByItem(NPC npc, Player player, Item item, ref NPC.HitModifiers modifiers)
    {
        if (item.ModItem is not (NormalSickle or LegacyDeath))
            return;

        if (item.ModItem is NormalSickle sickle)
            modifiers.ScalingArmorPenetration += sickle.ArmorPenetrationPercent;

        if (Main.netMode == NetmodeID.MultiplayerClient)
        {
            return;
        }

        // The server records the reaper before damage is applied. This avoids a one-shot kill
        // racing ahead of the client's early registration packet.
        RegisterSickleHit(npc, player.whoAmI);
    }

    public override void OnHitByItem(NPC npc, Player player, Item item, NPC.HitInfo hit, int damageDone)
    {
        if (item.ModItem is NormalSickle or LegacyDeath)
            RegisterSickleHit(npc, player.whoAmI);
    }

    public override void OnHitByProjectile(NPC npc, Projectile projectile, NPC.HitInfo hit, int damageDone)
    {
        if (projectile.GetGlobalProjectile<MyGlobalProjectile>().IsSickleProjectile)
            RegisterSickleHit(npc, projectile.owner);
    }

    public override void PostAI(NPC npc)
    {
        if (Main.netMode != NetmodeID.MultiplayerClient && lastSicklePlayer >= 0)
            npc.AddBuff(ModContent.BuffType<DeathMarkBuff>(), 2);
        UpdateFatedState(npc);
        SpawnStatusParticles(npc);
    }

    public override void PostDraw(NPC npc, SpriteBatch spriteBatch, Vector2 screenPos, Color drawColor)
    {
        if (!deathMarked)
            return;

        Texture2D markTexture = TextureAssets.Buff[ModContent.BuffType<DeathMarkBuff>()].Value;
        float pulse = 0.52f + (float)Math.Sin(Main.GlobalTimeWrappedHourly * 5f + npc.whoAmI) * 0.035f;
        Vector2 position = new Vector2(npc.Center.X, npc.Top.Y - 13f) - screenPos;
        Color auraColor = FatedStackCount > 0 ? new Color(235, 55, 150, 0) : new Color(65, 235, 255, 0);

        for (int index = 0; index < 4; index++)
        {
            Vector2 offset = (Main.GlobalTimeWrappedHourly * 2f + index * MathHelper.PiOver2).ToRotationVector2() * 2f;
            spriteBatch.Draw(markTexture, position + offset, null, auraColor * 0.32f, 0f, markTexture.Size() / 2f, pulse * 1.12f, SpriteEffects.None, 0f);
        }

        spriteBatch.Draw(markTexture, position, null, Color.White, 0f, markTexture.Size() / 2f, pulse, SpriteEffects.None, 0f);
        if (FatedStackCount <= 0)
            return;

        Texture2D fatedTexture = TextureAssets.Buff[ModContent.BuffType<FatedBuff>()].Value;
        Vector2 fatedPosition = position + new Vector2(18f, 1f);
        spriteBatch.Draw(fatedTexture, fatedPosition, null, Color.White, 0f, fatedTexture.Size() / 2f, 0.44f, SpriteEffects.None, 0f);
        Utils.DrawBorderStringFourWay(spriteBatch, FontAssets.ItemStack.Value, FatedStackCount.ToString(), fatedPosition.X + 5f, fatedPosition.Y + 3f, new Color(255, 95, 180), Color.Black, Vector2.Zero, 0.58f);
    }

    public override void SendExtraAI(NPC npc, BitWriter bitWriter, BinaryWriter binaryWriter)
    {
        bitWriter.WriteBit(deathMarked);
        binaryWriter.Write((byte)FatedStackCount);
    }

    public override void ReceiveExtraAI(NPC npc, BitReader bitReader, BinaryReader binaryReader)
    {
        deathMarked = bitReader.ReadBit();
        FatedStackCount = Math.Clamp((int)binaryReader.ReadByte(), 0, AbsoluteMaximumFatedStacks);
    }

    public override void OnKill(NPC npc)
    {
        if (Main.netMode == NetmodeID.MultiplayerClient || npc.friendly || npc.lifeMax <= 5)
            return;

        // Dungeon Guardians and Paladins are exceptional normal enemies: their unusual
        // health values should not distort the regular curve, and they always cap it.
        if (IsFixedMaximumSoulEnemy(npc.type))
        {
            if (lastSicklePlayer >= 0 && lastSicklePlayer < Main.maxPlayers && Main.player[lastSicklePlayer].active)
                Main.player[lastSicklePlayer].GetModPlayer<MyPlayer>().AddSouls(50);
            return;
        }

        if (TryGetBossParticipants(npc, out bool[] bossParticipants, out int encounterLifeMax))
        {
            int reward = CalculateSoulReward(encounterLifeMax, boss: true);
            for (int playerIndex = 0; playerIndex < Main.maxPlayers; playerIndex++)
            {
                if (bossParticipants[playerIndex] && Main.player[playerIndex].active)
                    Main.player[playerIndex].GetModPlayer<MyPlayer>().AddSouls(reward, announce: true);
            }
            return;
        }

        if (IsCompositeBossPart(npc.type))
            return;

        if (npc.realLife >= 0 && npc.realLife < Main.maxNPCs && Main.npc[npc.realLife].boss)
            return;

        if (!npc.boss && lastSicklePlayer >= 0 && lastSicklePlayer < Main.maxPlayers && Main.player[lastSicklePlayer].active)
            Main.player[lastSicklePlayer].GetModPlayer<MyPlayer>().AddSouls(CalculateSoulReward(npc.lifeMax, boss: false));
    }

    internal void RegisterSickleHit(NPC npc, int playerIndex, bool applyDeathMark = true)
    {
        if (Main.netMode == NetmodeID.MultiplayerClient
            || playerIndex < 0
            || playerIndex >= Main.maxPlayers
            || npc.friendly)
            return;

        if (applyDeathMark)
            MarkForReaping(npc);

        sickleParticipants[playerIndex] = true;
        lastSicklePlayer = playerIndex;
        BossSoulParticipationSystem.RecordCompositeBossHit(npc, playerIndex);

        if (npc.realLife >= 0 && npc.realLife < Main.maxNPCs && npc.realLife != npc.whoAmI)
        {
            MyGlobalNPC root = Main.npc[npc.realLife].GetGlobalNPC<MyGlobalNPC>();
            root.sickleParticipants[playerIndex] = true;
            root.lastSicklePlayer = playerIndex;
        }
    }

#if DEBUG
    internal bool HasSickleHarvester(int playerIndex)
        => playerIndex >= 0
            && playerIndex < Main.maxPlayers
            && sickleParticipants[playerIndex]
            && lastSicklePlayer == playerIndex;
#endif

    internal void AddFatedStacks(NPC npc, int amount, int maximumStacks, int durationFrames, int playerIndex, int lifeStealLevel)
    {
        if (amount <= 0 || Main.netMode == NetmodeID.MultiplayerClient || npc.friendly || npc.lifeMax <= 5)
            return;

        maximumStacks = Math.Clamp(maximumStacks, 1, AbsoluteMaximumFatedStacks);
        durationFrames = Math.Clamp(durationFrames, 60, ushort.MaxValue);
        int stacksToAdd = Math.Min(amount, Math.Max(0, maximumStacks - FatedStackCount));
        for (int index = 0; index < stacksToAdd; index++)
        {
            fatedStackTimers[FatedStackCount] = durationFrames;
            fatedStackOwners[FatedStackCount] = playerIndex is >= 0 and < Main.maxPlayers ? playerIndex : -1;
            fatedStackLifeStealLevels[FatedStackCount] = Math.Max(0, lifeStealLevel);
            FatedStackCount++;
        }

        for (int index = 0; index < FatedStackCount; index++)
            fatedStackTimers[index] = Math.Max(fatedStackTimers[index], durationFrames);

        npc.AddBuff(ModContent.BuffType<FatedBuff>(), durationFrames);
        npc.netUpdate = true;
    }

    internal void MarkForReaping(NPC npc)
    {
        bool newlyMarked = !deathMarked;
        deathMarked = true;
        npc.AddBuff(ModContent.BuffType<DeathMarkBuff>(), DeathMarkDuration);
        SyncBuffState(npc);
        if (newlyMarked)
            SoulHarvest.BroadcastDeathMark(npc, marked: true);
    }

    internal void ApplyDeathDomainHitStop(NPC npc, int frames)
    {
        frames = Math.Clamp(frames, 1, 4);
        if (deathDomainHitStopFrames <= 0)
            deathDomainStoredVelocity = npc.velocity;

        deathDomainHitStopFrames = Math.Max(deathDomainHitStopFrames, frames);
        npc.velocity = Vector2.Zero;
        if (Main.netMode != NetmodeID.MultiplayerClient)
            npc.netUpdate = true;
    }

    internal void ReceiveDeathMarkState(bool marked)
    {
        deathMarked = marked;
    }

    internal static void SyncBuffState(NPC npc)
    {
        npc.netUpdate = true;
        if (Main.netMode == NetmodeID.Server)
            NetMessage.SendData(MessageID.NPCBuffs, -1, -1, null, npc.whoAmI);
    }

    private void UpdateFatedState(NPC npc)
    {
        if (Main.netMode == NetmodeID.MultiplayerClient || FatedStackCount <= 0)
            return;

        int writeIndex = 0;
        for (int index = 0; index < FatedStackCount; index++)
        {
            if (--fatedStackTimers[index] > 0)
            {
                fatedStackTimers[writeIndex] = fatedStackTimers[index];
                fatedStackOwners[writeIndex] = fatedStackOwners[index];
                fatedStackLifeStealLevels[writeIndex] = fatedStackLifeStealLevels[index];
                writeIndex++;
            }
        }

        if (writeIndex != FatedStackCount)
        {
            FatedStackCount = writeIndex;
            npc.netUpdate = true;
        }

        if (FatedStackCount <= 0)
        {
            fatedSecondTimer = 0;
            return;
        }

        npc.AddBuff(ModContent.BuffType<FatedBuff>(), 2);
        if (++fatedSecondTimer < 60)
            return;

        fatedSecondTimer = 0;
        bool bossOrBossPart = npc.boss || npc.realLife >= 0 && npc.realLife < Main.maxNPCs && Main.npc[npc.realLife].boss;
        float ratePerStack = bossOrBossPart ? 0.01f : 1f / 3f;
        int damage = Math.Max(1, (int)Math.Ceiling(npc.lifeMax * ratePerStack * FatedStackCount));
        int lifeBeforeHit = npc.life;
        Vector2 lifeStealSource = npc.Center;
        int[] stackCountsByPlayer = new int[Main.maxPlayers];
        int[] lifeStealLevelByPlayer = new int[Main.maxPlayers];
        for (int index = 0; index < FatedStackCount; index++)
        {
            int owner = fatedStackOwners[index];
            if (owner < 0 || owner >= Main.maxPlayers)
                continue;

            stackCountsByPlayer[owner]++;
            lifeStealLevelByPlayer[owner] = Math.Max(lifeStealLevelByPlayer[owner], fatedStackLifeStealLevels[index]);
        }

        NPC.HitInfo fateHit = new()
        {
            Damage = damage,
            SourceDamage = damage,
            HitDirection = 0,
            Knockback = 0f,
            DamageType = DamageClass.Generic,
            Crit = false
        };
        npc.StrikeNPC(fateHit, fromNet: false, noPlayerInteraction: false);
        if (Main.netMode == NetmodeID.Server)
            NetMessage.SendStrikeNPC(npc, in fateHit);

        int actualDamage = Math.Min(Math.Max(0, lifeBeforeHit), damage);
        for (int playerIndex = 0; playerIndex < Main.maxPlayers; playerIndex++)
        {
            int stackCount = stackCountsByPlayer[playerIndex];
            int lifeStealLevel = lifeStealLevelByPlayer[playerIndex];
            if (stackCount <= 0 || lifeStealLevel <= 0 || !Main.player[playerIndex].active || Main.player[playerIndex].dead)
                continue;

            int attributedDamage = Math.Max(1, (int)Math.Round(actualDamage * stackCount / (double)FatedStackCount));
            int healedLife = Main.player[playerIndex].GetModPlayer<MyPlayer>().TryLifeSteal(attributedDamage, lifeStealLevel);
            LifeStealVisuals.Spawn(lifeStealSource, playerIndex, lifeStealLevel, healedLife);
        }
    }

    private void SpawnStatusParticles(NPC npc)
    {
        if (Main.netMode == NetmodeID.Server || !npc.active)
            return;

        if (deathMarked && Main.rand.NextBool(4))
        {
            float angle = Main.GlobalTimeWrappedHourly * 3.1f + npc.whoAmI * 0.73f + Main.rand.NextFloat(-0.35f, 0.35f);
            Vector2 orbit = new Vector2(Math.Max(12f, npc.width * 0.58f), Math.Max(9f, npc.height * 0.36f));
            Vector2 position = npc.Center + new Vector2((float)Math.Cos(angle) * orbit.X, (float)Math.Sin(angle) * orbit.Y);
            Dust markDust = Dust.NewDustPerfect(position, DustID.DungeonSpirit, -npc.velocity * 0.08f + Vector2.UnitY * -0.25f, 80, new Color(70, 230, 255), 0.72f);
            markDust.noGravity = true;
        }

        if (FatedStackCount <= 0)
            return;

        int particleCount = Main.rand.NextBool(2) ? Math.Min(2, FatedStackCount) : 1;
        for (int index = 0; index < particleCount; index++)
        {
            Vector2 position = npc.Center + Main.rand.NextVector2CircularEdge(npc.width * 0.62f + 8f, npc.height * 0.5f + 8f);
            Vector2 velocity = (npc.Center - position).SafeNormalize(Vector2.Zero) * Main.rand.NextFloat(0.45f, 1.2f);
            Dust fateDust = Dust.NewDustPerfect(position, DustID.Shadowflame, velocity, 55, new Color(245, 45, 145), 0.9f + FatedStackCount * 0.08f);
            fateDust.noGravity = true;
        }
    }

    internal static int CalculateSoulReward(int lifeMax, bool boss)
    {
        lifeMax = Math.Max(1, lifeMax);
        if (boss)
            return Math.Clamp((int)Math.Ceiling(1.3d * Math.Sqrt(lifeMax)), 1, 500);

        double normalizedLife = lifeMax / 25d;
        return Math.Clamp((int)Math.Ceiling(Math.Pow(normalizedLife, 0.55d)), 1, 50);
    }

    private bool TryGetBossParticipants(NPC npc, out bool[] participants, out int encounterLifeMax)
    {
        if (BossSoulParticipationSystem.TryTakeCompositeParticipants(npc, out participants, out encounterLifeMax))
            return true;

        if (IsCompositeBossPart(npc.type))
        {
            encounterLifeMax = 0;
            return false;
        }

        if (!npc.boss)
        {
            encounterLifeMax = 0;
            return false;
        }

        if (npc.realLife >= 0 && npc.realLife < Main.maxNPCs && npc.realLife != npc.whoAmI && Main.npc[npc.realLife].active)
        {
            encounterLifeMax = 0;
            return false;
        }

        if (npc.realLife >= 0 && npc.realLife < Main.maxNPCs)
        {
            participants = (bool[])Main.npc[npc.realLife].GetGlobalNPC<MyGlobalNPC>().sickleParticipants.Clone();
            encounterLifeMax = BossSoulParticipationSystem.GetEncounterLifeMax(npc);
            return true;
        }

        participants = (bool[])sickleParticipants.Clone();
        encounterLifeMax = BossSoulParticipationSystem.GetEncounterLifeMax(npc);
        return true;
    }

    private static bool IsCompositeBossPart(int npcType)
    {
        return npcType is NPCID.Retinazer
            or NPCID.Spazmatism
            or NPCID.EaterofWorldsBody
            or NPCID.EaterofWorldsHead
            or NPCID.EaterofWorldsTail
            or NPCID.Creeper
            or NPCID.GolemHead
            or NPCID.GolemHeadFree
            or NPCID.MoonLordHead
            or NPCID.MoonLordHand;
    }

    private static bool IsFixedMaximumSoulEnemy(int npcType)
    {
        return npcType is NPCID.DungeonGuardian or NPCID.Paladin;
    }
}