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

DeathMod

【Terraria】DeathMod (now aka Soul Harvest)

公开
关注 0 Fork 0 Star 0
UTF-8
using SoulHarvest.Projectiles;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.IO;
using Terraria;
using Terraria.ID;
using Terraria.ModLoader;

namespace SoulHarvest.Common;

public sealed class ReaperCombatGlobalNPC : GlobalNPC
{
    public override bool InstancePerEntity => true;

    private readonly byte[] boneMarks = new byte[Main.maxPlayers];
    private readonly short[] boneTimers = new short[Main.maxPlayers];
    private readonly short[] boneShatterCooldown = new short[Main.maxPlayers];
    private readonly byte[] frostMarks = new byte[Main.maxPlayers];
    private readonly short[] frostTimers = new short[Main.maxPlayers];
    private readonly float[] frostSlowPerStack = new float[Main.maxPlayers];
    private readonly byte[] soulStacks = new byte[Main.maxPlayers];
    private readonly short[,] soulLayerTimers = new short[Main.maxPlayers, 7];
    private readonly int[,] soulLayerDamage = new int[Main.maxPlayers, 7];
    private readonly SickleCombatSnapshot[] soulSnapshots = new SickleCombatSnapshot[Main.maxPlayers];
    private readonly bool[] hasSoulSnapshot = new bool[Main.maxPlayers];
    private readonly int[] lastSoulFatedActionId = new int[Main.maxPlayers];
    private readonly bool[] hasSoulFatedAction = new bool[Main.maxPlayers];
    private readonly short[] soulShardCooldown = new short[Main.maxPlayers];
    private readonly float[] lastVoidAngle = new float[Main.maxPlayers];
    private readonly bool[] hasVoidAngle = new bool[Main.maxPlayers];
    private readonly short[] voidAngleTimer = new short[Main.maxPlayers];
    private readonly short[] voidIntersectionCooldown = new short[Main.maxPlayers];
    private readonly short[] voidCoordinateTimer = new short[Main.maxPlayers];
    private readonly int[] voidCoordinateSourceActionId = new int[Main.maxPlayers];
    private readonly int[] voidEmpoweredActionId = new int[Main.maxPlayers];
    private readonly short[] voidEmpoweredActionTimer = new short[Main.maxPlayers];
    private readonly short[] infernalScorchVisualCooldown = new short[Main.maxPlayers];
    private readonly int[] deathUltimateActionId = new int[Main.maxPlayers];
    private readonly int[] deathUltimateAccumulatedDamage = new int[Main.maxPlayers];
    private int boneSlowTimer;
    private float boneSlowFactor = 1f;
    private int boneDefenseTimer;
    private int boneDefenseReduction;
    private int soulSecondTimer;
    private int frostFreezeTimer;
    private Vector2 frostStoredVelocity;
    private short deathUltimateStopTimer;
    private Vector2 deathUltimateStoredVelocity;
    private bool deathWorldFreezeCaptured;
    private Vector2 deathWorldFreezeVelocity;
    private float deathWorldFreezeRotation;
    private Rectangle deathWorldFreezeFrame;
    private double deathWorldFreezeFrameCounter;

    public override void SetDefaults(NPC entity)
    {
        Array.Clear(boneMarks);
        Array.Clear(boneTimers);
        Array.Clear(boneShatterCooldown);
        Array.Clear(frostMarks);
        Array.Clear(frostTimers);
        Array.Clear(frostSlowPerStack);
        Array.Clear(soulStacks);
        Array.Clear(soulLayerTimers);
        Array.Clear(soulLayerDamage);
        Array.Clear(soulSnapshots);
        Array.Clear(hasSoulSnapshot);
        Array.Clear(lastSoulFatedActionId);
        Array.Clear(hasSoulFatedAction);
        Array.Clear(soulShardCooldown);
        Array.Clear(lastVoidAngle);
        Array.Clear(hasVoidAngle);
        Array.Clear(voidAngleTimer);
        Array.Clear(voidIntersectionCooldown);
        Array.Clear(voidCoordinateTimer);
        Array.Clear(voidCoordinateSourceActionId);
        Array.Clear(voidEmpoweredActionId);
        Array.Clear(voidEmpoweredActionTimer);
        Array.Clear(infernalScorchVisualCooldown);
        Array.Clear(deathUltimateActionId);
        Array.Clear(deathUltimateAccumulatedDamage);
        boneSlowTimer = 0;
        boneSlowFactor = 1f;
        boneDefenseTimer = 0;
        boneDefenseReduction = 0;
        soulSecondTimer = 0;
        frostFreezeTimer = 0;
        frostStoredVelocity = Vector2.Zero;
        deathUltimateStopTimer = 0;
        deathUltimateStoredVelocity = Vector2.Zero;
        deathWorldFreezeCaptured = false;
        deathWorldFreezeVelocity = Vector2.Zero;
        deathWorldFreezeRotation = 0f;
        deathWorldFreezeFrame = Rectangle.Empty;
        deathWorldFreezeFrameCounter = 0d;
    }

    public override bool PreAI(NPC npc)
    {
        if (ReaperDeathUltimateFreezeSystem.IsWorldFrozen)
        {
            if (!deathWorldFreezeCaptured)
            {
                deathWorldFreezeCaptured = true;
                deathWorldFreezeVelocity = npc.velocity;
                deathWorldFreezeRotation = npc.rotation;
                deathWorldFreezeFrame = npc.frame;
                deathWorldFreezeFrameCounter = npc.frameCounter;
                if (Main.netMode != NetmodeID.MultiplayerClient)
                    npc.netUpdate = true;
            }
            deathUltimateStopTimer = 0;
            deathUltimateStoredVelocity = Vector2.Zero;
            npc.velocity = Vector2.Zero;
            npc.rotation = deathWorldFreezeRotation;
            npc.frame = deathWorldFreezeFrame;
            npc.frameCounter = deathWorldFreezeFrameCounter;
            return false;
        }
        if (deathWorldFreezeCaptured)
        {
            npc.velocity = deathWorldFreezeVelocity;
            deathWorldFreezeCaptured = false;
            deathWorldFreezeVelocity = Vector2.Zero;
            if (Main.netMode != NetmodeID.MultiplayerClient)
                npc.netUpdate = true;
        }

        bool bossLike = IsBossLike(npc);
        if (deathUltimateStopTimer > 0)
        {
            deathUltimateStopTimer--;
            if (bossLike)
            {
                npc.velocity *= 0.08f;
            }
            else
            {
                npc.velocity = Vector2.Zero;
                if (deathUltimateStopTimer == 0)
                    npc.velocity = deathUltimateStoredVelocity * 0.18f;
                return false;
            }
        }
        if (frostFreezeTimer > 0 && !bossLike)
        {
            frostFreezeTimer--;
            npc.velocity = Vector2.Zero;
            if (frostFreezeTimer == 0)
                npc.velocity = frostStoredVelocity * 0.25f;
            return false;
        }
        if (boneSlowTimer > 0 && !bossLike)
        {
            boneSlowTimer--;
            npc.velocity *= boneSlowFactor;
        }
        if (!bossLike)
        {
            float strongestSlow = 0f;
            for (int index = 0; index < frostMarks.Length; index++)
                strongestSlow = Math.Max(strongestSlow, frostMarks[index] * frostSlowPerStack[index]);
            if (strongestSlow > 0f)
                npc.velocity *= Math.Max(0.55f, 1f - strongestSlow);
        }
        return true;
    }

    public override void FindFrame(NPC npc, int frameHeight)
    {
        if (!deathWorldFreezeCaptured
            || !ReaperDeathUltimateFreezeSystem.IsWorldFrozen)
        {
            return;
        }
        npc.frame = deathWorldFreezeFrame;
        npc.frameCounter = deathWorldFreezeFrameCounter;
    }

    internal void RecordDeathUltimateCut(NPC npc, int owner, int actionId, int damageDone)
    {
        if (owner < 0 || owner >= Main.maxPlayers || damageDone <= 0)
            return;
        if (deathUltimateActionId[owner] != actionId)
        {
            deathUltimateActionId[owner] = actionId;
            deathUltimateAccumulatedDamage[owner] = 0;
        }
        deathUltimateAccumulatedDamage[owner] = (int)Math.Min(int.MaxValue / 4L,
            deathUltimateAccumulatedDamage[owner] + (long)damageDone);
        if (ReaperDeathUltimateFreezeSystem.IsWorldFrozen)
        {
            npc.velocity = Vector2.Zero;
            npc.netUpdate = true;
            return;
        }
        if (deathUltimateStopTimer <= 0)
            deathUltimateStoredVelocity = npc.velocity;
        deathUltimateStopTimer = Math.Max(deathUltimateStopTimer, (short)180);
        npc.velocity *= IsBossLike(npc) ? 0.08f : 0f;
        npc.netUpdate = true;
    }

    internal bool IsTrackedByDeathUltimate(int owner, int actionId)
        => owner >= 0 && owner < Main.maxPlayers
            && actionId >= 0
            && deathUltimateActionId[owner] == actionId
            && deathUltimateAccumulatedDamage[owner] > 0;

    internal int ConsumeDeathUltimateDamage(NPC npc, int owner, int actionId)
    {
        if (owner < 0 || owner >= Main.maxPlayers
            || deathUltimateActionId[owner] != actionId)
        {
            return 0;
        }
        int damage = deathUltimateAccumulatedDamage[owner];
        deathUltimateActionId[owner] = 0;
        deathUltimateAccumulatedDamage[owner] = 0;
        deathUltimateStopTimer = 0;
        if (!IsBossLike(npc))
            npc.velocity = deathUltimateStoredVelocity * 0.18f;
        return damage;
    }

    public override void PostAI(NPC npc)
    {
        if (Main.netMode == NetmodeID.MultiplayerClient)
            return;
        if (boneDefenseTimer > 0)
            boneDefenseTimer--;
        for (int playerIndex = 0; playerIndex < Main.maxPlayers; playerIndex++)
        {
            Tick(ref boneTimers[playerIndex], ref boneMarks[playerIndex]);
            if (boneShatterCooldown[playerIndex] > 0)
                boneShatterCooldown[playerIndex]--;
            Tick(ref frostTimers[playerIndex], ref frostMarks[playerIndex]);
            TickSoulLayers(npc, playerIndex);
            if (soulShardCooldown[playerIndex] > 0)
                soulShardCooldown[playerIndex]--;
            if (voidIntersectionCooldown[playerIndex] > 0)
                voidIntersectionCooldown[playerIndex]--;
            if (voidAngleTimer[playerIndex] > 0)
                voidAngleTimer[playerIndex]--;
            else
                hasVoidAngle[playerIndex] = false;
            if (voidCoordinateTimer[playerIndex] > 0)
                voidCoordinateTimer[playerIndex]--;
            if (voidEmpoweredActionTimer[playerIndex] > 0)
                voidEmpoweredActionTimer[playerIndex]--;
            else
                voidEmpoweredActionId[playerIndex] = 0;
            if (infernalScorchVisualCooldown[playerIndex] > 0)
                infernalScorchVisualCooldown[playerIndex]--;
        }

        if (++soulSecondTimer < 60)
            return;
        soulSecondTimer = 0;
        for (int playerIndex = 0; playerIndex < Main.maxPlayers && npc.active; playerIndex++)
        {
            int stacks = soulStacks[playerIndex];
            int damage = 0;
            for (int layer = 0; layer < stacks; layer++)
                damage += soulLayerDamage[playerIndex, layer];
            if (stacks <= 0 || damage <= 0 || !Main.player[playerIndex].active)
                continue;
            NPC.HitInfo hit = new()
            {
                Damage = damage,
                SourceDamage = damage,
                Knockback = 0f,
                HitDirection = 0,
                Crit = false,
                DamageType = DamageClass.Melee
            };
            npc.GetGlobalNPC<MyGlobalNPC>().RegisterSickleHit(npc, playerIndex);
            npc.StrikeNPC(hit, fromNet: false, noPlayerInteraction: false);
            if (Main.netMode == NetmodeID.Server)
                NetMessage.SendStrikeNPC(npc, in hit);
        }
    }

    public override void ModifyIncomingHit(NPC npc, ref NPC.HitModifiers modifiers)
    {
        ReaperCombatGlobalNPC defenseSource = this;
        if (npc.realLife >= 0 && npc.realLife < Main.maxNPCs && Main.npc[npc.realLife].active)
            defenseSource = Main.npc[npc.realLife].GetGlobalNPC<ReaperCombatGlobalNPC>();
        if (defenseSource.boneDefenseTimer > 0)
            modifiers.Defense.Flat -= defenseSource.boneDefenseReduction;
    }

    internal void AddBoneMarks(NPC npc, Player player, in SickleCombatSnapshot snapshot, int amount, int actionId)
    {
        int owner = player.whoAmI;
        int duration = (snapshot.Skill1 switch { 2 => 6, >= 3 => 7, _ => 5 }) * 60;
        boneTimers[owner] = (short)duration;
        boneMarks[owner] = (byte)Math.Min(3, boneMarks[owner] + Math.Max(1, amount));
        if (boneMarks[owner] < 3 || boneShatterCooldown[owner] > 0)
            return;
        boneMarks[owner] = 0;
        boneShatterCooldown[owner] = 45;
        float factor = snapshot.Skill1 switch { 2 => 0.55f, >= 3 => 0.7f, _ => 0.4f };
        ReaperStrikeProjectile.Spawn(npc.GetSource_FromAI(), owner, snapshot, ReaperHitKind.PrimaryDerived, 0,
            npc.Center, Vector2.UnitX, ReaperStrikeShape.Circle, Math.Max(46f, npc.width), 20f, factor, 1);
        ReaperCombatPlayer combatPlayer = player.GetModPlayer<ReaperCombatPlayer>();
        if (combatPlayer.TryReserveSkillCue(ReaperFormId.Bone, 0, actionId))
        {
            ReaperSkillCueProjectile.Spawn(npc.GetSource_FromAI(), owner, snapshot, 0,
                npc.Center, Vector2.UnitY * -1f, actionId: actionId);
        }

        int controlLevel = snapshot.Skill2;
        if (controlLevel > 0)
        {
            if (IsBossLike(npc))
            {
                boneDefenseReduction = Math.Max(boneDefenseReduction, controlLevel switch { 2 => 10, >= 3 => 14, _ => 6 });
                boneDefenseTimer = Math.Max(boneDefenseTimer, 180);
            }
            else
            {
                boneSlowFactor = 1f - (controlLevel switch { 2 => 0.35f, >= 3 => 0.45f, _ => 0.25f });
                boneSlowTimer = Math.Max(boneSlowTimer, 180);
            }
            if (combatPlayer.TryReserveSkillCue(ReaperFormId.Bone, 1, actionId))
            {
                ReaperSkillCueProjectile.Spawn(npc.GetSource_FromAI(), owner, snapshot, 1,
                    npc.Center, Vector2.UnitY * -1f, actionId: actionId);
            }
        }
        if (snapshot.Skill3 > 0)
        {
            float echo = snapshot.Skill3 switch { 2 => 0.3f, >= 3 => 0.4f, _ => 0.2f };
            ReaperStrikeProjectile.Spawn(npc.GetSource_FromAI(), owner, snapshot, ReaperHitKind.PrimaryDerived, 0,
                npc.Center, Vector2.UnitY, ReaperStrikeShape.Cross, 82f, 18f, echo, 12);
            if (combatPlayer.TryReserveSkillCue(ReaperFormId.Bone, 2, actionId))
            {
                ReaperSkillCueProjectile.Spawn(npc.GetSource_FromAI(), owner, snapshot, 2,
                    npc.Center, Vector2.UnitY, actionId: actionId);
            }
        }
    }

    internal void AddFrostMark(NPC npc, Player player, in SickleCombatSnapshot snapshot, bool shatter, int actionId)
    {
        int owner = player.whoAmI;
        frostMarks[owner] = (byte)Math.Min(4, frostMarks[owner] + 1);
        frostTimers[owner] = (short)((snapshot.Skill1 switch { 2 => 5, >= 3 => 6, _ => 4 }) * 60);
        float slowPerStack = snapshot.Skill1 switch { 2 => 0.06f, >= 3 => 0.08f, _ => 0.04f };
        frostSlowPerStack[owner] = slowPerStack;
        if (!shatter || frostMarks[owner] < 4)
            return;
        ShatterFrost(npc, player, snapshot, actionId);
    }

    internal bool HasFullFrostMarks(int owner) => owner >= 0 && owner < Main.maxPlayers && frostMarks[owner] >= 4;

    internal void ShatterFrost(NPC npc, Player player, in SickleCombatSnapshot snapshot, int actionId,
        bool canChargeUltimate = true)
    {
        int owner = player.whoAmI;
        if (frostMarks[owner] < 4)
            return;
        frostMarks[owner] = 0;
        float factor = snapshot.Skill1 switch { 2 => 0.7f, >= 3 => 0.9f, _ => 0.5f };
        ReaperHitKind hitKind = canChargeUltimate
            ? ReaperHitKind.PrimaryDerived
            : ReaperHitKind.UltimateDerived;
        ReaperStrikeProjectile.Spawn(npc.GetSource_FromAI(), owner, snapshot, hitKind, 0,
            npc.Center, Vector2.UnitX, ReaperStrikeShape.Cross, 94f, 20f, factor, 1);
        if (player.GetModPlayer<ReaperCombatPlayer>()
            .TryReserveSkillCue(ReaperFormId.Frost, 0, actionId))
        {
            ReaperSkillCueProjectile.Spawn(npc.GetSource_FromAI(), owner, snapshot, 0,
                npc.Center, Vector2.UnitX, actionId: actionId);
        }
    }

    internal void AddFatedSoulStack(NPC npc, Player player, in SickleCombatSnapshot snapshot, int actionId, int amount)
    {
        int owner = player.whoAmI;
        if (hasSoulFatedAction[owner] && lastSoulFatedActionId[owner] == actionId)
            return;
        hasSoulFatedAction[owner] = true;
        lastSoulFatedActionId[owner] = actionId;
        int maximum = snapshot.Skill3 switch { 2 => 5, >= 3 => 7, _ => 3 };
        short duration = (short)((snapshot.Skill3 switch { 2 => 5, >= 3 => 6, _ => 4 }) * 60);
        float factor = snapshot.Skill3 switch { 2 => 0.10f, >= 3 => 0.12f, _ => 0.08f };
        int contribution = Math.Max(1, (int)Math.Round(snapshot.Damage * factor));
        soulSnapshots[owner] = snapshot;
        hasSoulSnapshot[owner] = true;
        int current = soulStacks[owner];
        for (int layer = 0; layer < current; layer++)
            soulLayerTimers[owner, layer] = Math.Max(soulLayerTimers[owner, layer], duration);
        int toAdd = Math.Min(Math.Max(0, amount), maximum - current);
        for (int layer = 0; layer < toAdd; layer++)
        {
            soulLayerDamage[owner, current + layer] = contribution;
            soulLayerTimers[owner, current + layer] = duration;
        }
        soulStacks[owner] = (byte)(current + toAdd);
        bool reachedMaximum = toAdd > 0 && soulStacks[owner] >= maximum;
        if (toAdd > 0 && !reachedMaximum && snapshot.Skill3 > 0
            && player.GetModPlayer<ReaperCombatPlayer>()
                .TryReserveSkillCue(ReaperFormId.Soul, 2, actionId))
        {
            ReaperSkillCueProjectile.Spawn(npc.GetSource_FromAI(), owner, snapshot, 2,
                npc.Center, Vector2.UnitY * -1f, actionId: actionId);
        }
        if (reachedMaximum)
            ExecuteSoulFate(npc, owner, snapshot, actionId, expired: false);
    }

    internal bool TryTakeSoulShard(int owner)
    {
        if (owner < 0 || owner >= Main.maxPlayers || soulShardCooldown[owner] > 0)
            return false;
        soulShardCooldown[owner] = 60;
        return true;
    }

    internal void RefundSoulShardCooldown(int owner)
    {
        if (owner >= 0 && owner < Main.maxPlayers)
            soulShardCooldown[owner] = 0;
    }

    internal void ApplyFrostFreeze(NPC npc, int frames)
    {
        if (frames <= 0)
            return;
        if (IsBossLike(npc))
        {
            npc.GetGlobalNPC<MyGlobalNPC>().ApplyDeathDomainHitStop(npc, 2);
            return;
        }
        if (frostFreezeTimer <= 0)
            frostStoredVelocity = npc.velocity;
        frostFreezeTimer = Math.Max(frostFreezeTimer, frames);
        npc.velocity = Vector2.Zero;
        npc.netUpdate = true;
    }

    internal void RecordVoidCut(NPC npc, Player player, in SickleCombatSnapshot snapshot, float angle, int actionId)
    {
        int owner = player.whoAmI;
        if (snapshot.Skill1 > 0
            && hasVoidAngle[owner]
            && voidIntersectionCooldown[owner] <= 0
            && AngleDifference(lastVoidAngle[owner], angle) >= MathHelper.Pi / 3f)
        {
            int level = Math.Clamp(snapshot.Skill1, (byte)1, (byte)3);
            int rifts = level;
            float totalFactor = level switch { 2 => 0.24f, >= 3 => 0.34f, _ => 0.16f };
            Vector2 baseAxis = angle.ToRotationVector2();
            for (int index = 0; index < rifts; index++)
            {
                float centered = index - (rifts - 1) * 0.5f;
                Vector2 axis = baseAxis.RotatedBy(centered * 0.48f);
                ReaperVoidRiftProjectile.Spawn(npc.GetSource_FromAI(), owner, snapshot,
                    npc.Center, axis, 122f + level * 30f, 8f + level * 2.5f,
                    ReaperVoidArcRiftProjectile.GetLifetime(level),
                    145f + level * 38f,
                    totalFactor / rifts, index, actionId);
            }
            ReaperCombatPlayer combatPlayer = player.GetModPlayer<ReaperCombatPlayer>();
            if (combatPlayer.TryReserveSkillCue(ReaperFormId.Void, 0, actionId))
            {
                ReaperSkillCueProjectile.Spawn(npc.GetSource_FromAI(), owner, snapshot, 0,
                    npc.Center, angle.ToRotationVector2(), actionId: actionId);
            }
            voidIntersectionCooldown[owner] = 75;
            if (snapshot.Skill3 > 0)
            {
                voidCoordinateTimer[owner] = (short)((snapshot.Skill3 switch { 2 => 5, >= 3 => 6, _ => 4 }) * 60);
                voidCoordinateSourceActionId[owner] = actionId;
                if (combatPlayer.TryReserveSkillCue(ReaperFormId.Void, 2, actionId))
                {
                    ReaperSkillCueProjectile.Spawn(npc.GetSource_FromAI(), owner, snapshot, 2,
                        npc.Center, angle.ToRotationVector2(), actionId: actionId);
                }
            }
        }
        lastVoidAngle[owner] = angle;
        hasVoidAngle[owner] = true;
        voidAngleTimer[owner] = 120;
    }

    internal void ModifyReaperHit(int owner, in SickleCombatSnapshot snapshot, ReaperHitKind kind, int phase, int actionId,
        NPC npc, ref NPC.HitModifiers modifiers)
    {
        if (snapshot.Form == ReaperFormId.Infernal && snapshot.Skill2 > 0 && npc.HasBuff(BuffID.OnFire3))
        {
            modifiers.FinalDamage *= 1f + (snapshot.Skill2 switch { 2 => 0.07f, >= 3 => 0.10f, _ => 0.04f });
            ReaperCombatPlayer combatPlayer = Main.player[owner].GetModPlayer<ReaperCombatPlayer>();
            bool executed = Main.netMode != NetmodeID.MultiplayerClient
                && (kind is ReaperHitKind.Primary or ReaperHitKind.Special)
                && combatPlayer.TryTriggerInfernalScorchExecution(snapshot, npc, actionId);
            if (!executed
                && Main.netMode != NetmodeID.MultiplayerClient
                && infernalScorchVisualCooldown[owner] <= 0
                && combatPlayer.TryReserveSkillCue(ReaperFormId.Infernal, 1, actionId))
            {
                ReaperSkillCueProjectile.Spawn(npc.GetSource_FromAI(), owner, snapshot, 1,
                    npc.Center, Vector2.UnitY * -1f, actionId: actionId);
                infernalScorchVisualCooldown[owner] = (short)(snapshot.Skill2 switch
                {
                    2 => 34,
                    >= 3 => 26,
                    _ => 42
                });
            }
        }
        if (snapshot.Form == ReaperFormId.Frost
            && snapshot.Skill3 > 0
            && kind == ReaperHitKind.Special
            && phase == 2
            && HasFullFrostMarks(owner))
            modifiers.FinalDamage *= 1f + (snapshot.Skill3 switch { 2 => 0.12f, >= 3 => 0.16f, _ => 0.08f });
        if (snapshot.Form == ReaperFormId.Void
            && snapshot.Skill3 > 0
            && kind is ReaperHitKind.Special or ReaperHitKind.Ultimate)
        {
            if (voidEmpoweredActionId[owner] != actionId
                && voidCoordinateTimer[owner] > 0
                && voidCoordinateSourceActionId[owner] != actionId)
            {
                voidCoordinateTimer[owner] = 0;
                voidEmpoweredActionId[owner] = actionId;
                voidEmpoweredActionTimer[owner] = 240;
                if (Main.netMode != NetmodeID.MultiplayerClient
                    && Main.player[owner].GetModPlayer<ReaperCombatPlayer>()
                        .TryReserveSkillCue(ReaperFormId.Void, 2, actionId))
                {
                    ReaperSkillCueProjectile.Spawn(npc.GetSource_FromAI(), owner, snapshot, 2,
                        npc.Center, Vector2.UnitX, variant: 1, actionId: actionId);
                }
            }
            if (voidEmpoweredActionTimer[owner] > 0 && voidEmpoweredActionId[owner] == actionId)
            {
                modifiers.FinalDamage *= 1f + (snapshot.Skill3 switch { 2 => 0.12f, >= 3 => 0.16f, _ => 0.08f });
                modifiers.ScalingArmorPenetration += snapshot.Skill3 switch { 2 => 0.10f, >= 3 => 0.15f, _ => 0.05f };
            }
        }
    }

    private static void Tick(ref short timer, ref byte stacks)
    {
        if (timer > 0)
            timer--;
        if (timer <= 0)
            stacks = 0;
    }

    private void TickSoulLayers(NPC npc, int owner)
    {
        int count = soulStacks[owner];
        int storedDamage = 0;
        int write = 0;
        for (int layer = 0; layer < count; layer++)
        {
            storedDamage += soulLayerDamage[owner, layer];
            short timer = soulLayerTimers[owner, layer];
            if (timer > 0)
                timer--;
            if (timer <= 0)
                continue;
            soulLayerTimers[owner, write] = timer;
            soulLayerDamage[owner, write] = soulLayerDamage[owner, layer];
            write++;
        }
        for (int layer = write; layer < count; layer++)
        {
            soulLayerTimers[owner, layer] = 0;
            soulLayerDamage[owner, layer] = 0;
        }
        soulStacks[owner] = (byte)write;
        if (count > 0 && write == 0 && hasSoulSnapshot[owner])
            ExecuteSoulFate(npc, owner, soulSnapshots[owner], lastSoulFatedActionId[owner],
                expired: true, storedDamage);
    }

    private void ExecuteSoulFate(NPC npc, int owner, in SickleCombatSnapshot snapshot,
        int actionId, bool expired, int storedDamage = -1)
    {
        if (owner < 0 || owner >= Main.maxPlayers || !Main.player[owner].active)
        {
            ClearSoulFate(owner);
            return;
        }

        if (storedDamage < 0)
        {
            storedDamage = 0;
            int stacks = soulStacks[owner];
            for (int layer = 0; layer < stacks; layer++)
                storedDamage += soulLayerDamage[owner, layer];
        }

        // Expiry cashes out only what actually remained.  A full inscription gets
        // the stronger execution beat, while both paths remain one area strike.
        float baseFactor = snapshot.Skill3 switch { 2 => 0.38f, >= 3 => 0.54f, _ => 0.26f };
        float storedFactor = snapshot.Damage > 0 ? storedDamage / (float)snapshot.Damage * 0.45f : 0f;
        float factor = Math.Max(baseFactor * (expired ? 0.72f : 1f), storedFactor);
        factor = Math.Min(factor, snapshot.Skill3 >= 3 ? 0.72f : 0.52f);

        ClearSoulFate(owner);
        int executionActionId = (actionId & 0x3FFFFFFF) ^ 0x40000000;
        Vector2 direction = (npc.Center - Main.player[owner].MountedCenter)
            .SafeNormalize(new Vector2(Main.player[owner].direction, 0f));
        ReaperStrikeProjectile.Spawn(npc.GetSource_FromAI(), owner, snapshot,
            ReaperHitKind.PrimaryDerived, 0, npc.Center, direction,
            snapshot.Skill3 >= 2 ? ReaperStrikeShape.Cross : ReaperStrikeShape.Line,
            92f + snapshot.Skill3 * 18f, 22f + snapshot.Skill3 * 3f,
            factor, expired ? 2 : 8, executionActionId);
        if (Main.player[owner].GetModPlayer<ReaperCombatPlayer>()
            .TryReserveSkillCue(ReaperFormId.Soul, 2, executionActionId))
        {
            ReaperSkillCueProjectile.Spawn(npc.GetSource_FromAI(), owner, snapshot,
                2, npc.Center, direction, variant: 1, actionId: executionActionId);
        }
    }

    private void ClearSoulFate(int owner)
    {
        if (owner < 0 || owner >= Main.maxPlayers)
            return;
        int count = soulStacks[owner];
        for (int layer = 0; layer < count; layer++)
        {
            soulLayerTimers[owner, layer] = 0;
            soulLayerDamage[owner, layer] = 0;
        }
        soulStacks[owner] = 0;
        soulSnapshots[owner] = default;
        hasSoulSnapshot[owner] = false;
    }

    private static float AngleDifference(float first, float second)
    {
        float difference = Math.Abs(MathHelper.WrapAngle(first - second));
        return Math.Min(difference, MathHelper.TwoPi - difference);
    }

    private static bool IsBossLike(NPC npc)
    {
        return npc.boss
            || NPCID.Sets.ShouldBeCountedAsBoss[npc.type]
            || npc.realLife >= 0
                && npc.realLife < Main.maxNPCs
                && Main.npc[npc.realLife].active
                && (Main.npc[npc.realLife].boss
                    || NPCID.Sets.ShouldBeCountedAsBoss[Main.npc[npc.realLife].type]);
    }
}