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

DeathMod

【Terraria】DeathMod (now aka Soul Harvest)

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

namespace SoulHarvest.Common;

public class MyPlayer : ModPlayer
{
    public const int StartingSouls = 150;
    public const int SoulsPerEssence = 100;
    internal const int DeathWingTrailCapacity = 28;
    private const int DeathWingBodyRiftIdentity = -173870001;
    private const int DeathWingDomainChunkCapacity = 14;
    private const int DeathWingDomainChunkOverlapPoints = 4;

    public int Souls { get; private set; } = StartingSouls;
    public int SickleAlternateCooldown { get; private set; }
    public int SickleAlternateCooldownMax { get; private set; }
    public bool DeathDomainEnabled { get; private set; }
    public bool DeathDomainDescended { get; private set; }
    public DeathDomainProgression DeathDomainProgression { get; } = new();
    public ReaperProgressionState ReaperProgression { get; } = new();
    public int ReaperSpecialCooldown { get; private set; }
    public int ReaperSpecialCooldownMax { get; private set; }
    public int ReaperExhaustionCooldown { get; private set; }
    public ReaperFormId VisibleReaperForm { get; private set; } = ReaperFormId.Base;
    public ReaperStage VisibleReaperStage { get; private set; } = ReaperStage.StageI;

    internal DeathNecklace? ActiveDeathNecklace { get; private set; }

    internal bool DeathWingsActiveThisTick { get; set; }
    internal bool DeathWingsActiveLastTick { get; private set; }
    internal Vector2[] DeathWingUpperTrail { get; } = new Vector2[DeathWingTrailCapacity];
    internal Vector2[] DeathWingLowerTrail { get; } = new Vector2[DeathWingTrailCapacity];
    internal Vector2[] DeathWingBodyTrail { get; } = new Vector2[DeathWingTrailCapacity];
    internal int DeathWingTrailCount { get; private set; }
    internal float DeathWingTrailMastery { get; private set; }
    internal float DeathWingTrailOpacity { get; private set; }
    internal float DeathWingRiftStrength { get; private set; }

    private int lifeStealCooldown;
    private int swingComboDirection = 1;
    private bool reaperPrimaryInputWasHeld;
    private bool rawSpecialInputWasHeld;
    private bool serverSoulBalanceInitialized;
    private bool serverReaperProgressionInitialized;
    private int clientCharacterStateUploadDelay;
    private int clientCharacterStateUploadAttempts;
    private bool clientCharacterStateAcknowledged;
    private bool reaperDeathStateCleared;
    private bool reaperLegacyMigrationChecked;
    private int reaperLegacyMigrationVersion;
    private int pendingLegacyMigrationEssence;
    private TagCompound? reaperLegacyMigrationSnapshot;
    private int reaperLegacyMigrationNoticeWeapons;
    private int reaperLegacyMigrationNoticeRefund;
    private readonly float[] reaperEnergy = new float[8];
    private readonly int[] reaperUltimateCooldowns = new int[8];
    private byte deathCycleMask;
    private ReaperFormId? queuedReaperForm;
    private ulong lastReaperFormSwitchRequestTick;
    private ulong deathWingTrailUpdateTick = ulong.MaxValue;
    private ulong deathWingDomainCombatUpdateTick = ulong.MaxValue;
    private readonly List<Vector2> deathWingDomainChunkPoints = [];
    private readonly List<float> deathWingDomainChunkOpacities = [];
    private readonly ulong[] deathWingDomainNextHarvestTicks =
        new ulong[Main.maxNPCs];
    private float deathWingDomainChunkMastery;
    private bool deathWingDomainChunkHasNewGeometry;
    private int deathWingDomainChunkSequence;
    private int deathWingDomainVolleyCounter;
    private int deathDomainTimer;
    private int deathDomainVolleyCounter;
    private int deathDomainKeyHoldFrames;
    private bool deathDomainLongPressHandled;
    private int deathDomainScreenShakeFrames;
    private int deathDomainSlashSoundCooldown;
    private float deathDomainScreenShakeStrength;
    private Vector2 deathDomainScreenShakeDirection = Vector2.UnitY;
    private readonly Dictionary<int, PendingDeathDomainCut> deathDomainPendingCuts = [];
    private readonly List<PendingDeathDomainStrike> pendingDeathDomainStrikes = [];
    private readonly List<PendingReaperDomainHarvest> pendingReaperDomainHarvests = [];

    private const int DeathDomainLongPressFrames = 45;

    private readonly record struct PendingDeathDomainCut(float Rotation, int ProjectileIndex);
    private readonly record struct PendingDeathDomainStrike(
        int NpcIndex,
        int RemainingFrames,
        int Damage,
        int LifeStealLevel,
        int DeathUltimateActionId);
    private readonly record struct PendingReaperDomainHarvest(
        int NpcIndex,
        int RemainingFrames,
        float Rotation,
        float VisualMastery,
        bool Requiem,
        int DeathUltimateActionId);

    public override void Initialize()
    {
        Souls = StartingSouls;
        lifeStealCooldown = 0;
        swingComboDirection = 1;
        reaperPrimaryInputWasHeld = false;
        rawSpecialInputWasHeld = false;
        SickleAlternateCooldown = 0;
        SickleAlternateCooldownMax = 0;
        DeathWingsActiveThisTick = false;
        DeathWingsActiveLastTick = false;
        ClearDeathWingTrail();
        DiscardDeathWingDomainChunk();
        deathWingDomainCombatUpdateTick = ulong.MaxValue;
        Array.Clear(deathWingDomainNextHarvestTicks);
        deathWingDomainChunkSequence = 0;
        deathWingDomainVolleyCounter = 0;
        serverSoulBalanceInitialized = false;
        serverReaperProgressionInitialized = false;
        clientCharacterStateUploadDelay = 0;
        clientCharacterStateUploadAttempts = 0;
        clientCharacterStateAcknowledged = false;
        DeathDomainProgression.Reset();
        ReaperProgression.Reset();
        VisibleReaperForm = ReaperFormId.Base;
        VisibleReaperStage = ReaperStage.StageI;
        ClearReaperCombatRuntime(sync: false);
        reaperDeathStateCleared = false;
        queuedReaperForm = null;
        lastReaperFormSwitchRequestTick = 0;
        reaperLegacyMigrationChecked = false;
        reaperLegacyMigrationVersion = 0;
        pendingLegacyMigrationEssence = 0;
        reaperLegacyMigrationSnapshot = null;
        reaperLegacyMigrationNoticeWeapons = 0;
        reaperLegacyMigrationNoticeRefund = 0;
        DeathDomainEnabled = false;
        DeathDomainDescended = false;
        ActiveDeathNecklace = null;
        deathDomainTimer = 0;
        deathDomainVolleyCounter = 0;
        deathDomainKeyHoldFrames = 0;
        deathDomainLongPressHandled = false;
        deathDomainScreenShakeFrames = 0;
        deathDomainSlashSoundCooldown = 0;
        deathDomainScreenShakeStrength = 0f;
        deathDomainScreenShakeDirection = Vector2.UnitY;
        deathDomainPendingCuts.Clear();
        pendingDeathDomainStrikes.Clear();
        pendingReaperDomainHarvests.Clear();
    }

    public override void ResetEffects()
    {
        DeathWingsActiveThisTick = false;
        ActiveDeathNecklace = null;
    }

    public override void SaveData(TagCompound tag)
    {
        tag[nameof(Souls)] = Souls;
        tag[nameof(DeathDomainEnabled)] = DeathDomainEnabled;
        tag[nameof(DeathDomainDescended)] = DeathDomainDescended;
        DeathDomainProgression.SaveData(tag);
        ReaperProgression.SaveData(tag);
        tag["ReaperLegacyMigrationVersion"] = reaperLegacyMigrationVersion;
        tag["PendingLegacyMigrationEssence"] = pendingLegacyMigrationEssence;
        if (reaperLegacyMigrationSnapshot is not null)
            tag["ReaperLegacyMigrationSnapshot"] = reaperLegacyMigrationSnapshot;
    }

    public override void LoadData(TagCompound tag)
    {
        Souls = tag.ContainsKey(nameof(Souls)) ? Math.Max(0, tag.GetInt(nameof(Souls))) : StartingSouls;
        DeathDomainEnabled = tag.GetBool(nameof(DeathDomainEnabled));
        DeathDomainDescended = DeathDomainEnabled && tag.GetBool(nameof(DeathDomainDescended));
        DeathDomainProgression.LoadData(tag);
        ReaperProgression.LoadData(tag);
        reaperLegacyMigrationVersion = tag.ContainsKey("ReaperLegacyMigrationVersion")
            ? Math.Max(0, tag.GetInt("ReaperLegacyMigrationVersion"))
            : 0;
        pendingLegacyMigrationEssence = Math.Max(0, tag.GetInt("PendingLegacyMigrationEssence"));
        reaperLegacyMigrationSnapshot = tag.ContainsKey("ReaperLegacyMigrationSnapshot")
            ? tag.GetCompound("ReaperLegacyMigrationSnapshot")
            : null;

        // Player inventory and personal banks have already been deserialized when
        // ModPlayer data is loaded. Migrating here guarantees the authoritative
        // tree is ready before the first multiplayer SyncPlayer upload.
        if (reaperLegacyMigrationVersion < ReaperLegacyMigration.MigrationVersion)
        {
            ReaperLegacyMigrationResult result = ReaperLegacyMigration.Apply(Player, ReaperProgression);
            reaperLegacyMigrationVersion = ReaperLegacyMigration.MigrationVersion;
            pendingLegacyMigrationEssence = Math.Max(0, pendingLegacyMigrationEssence + result.RefundEssence);
            reaperLegacyMigrationSnapshot = result.Snapshot;
            reaperLegacyMigrationNoticeWeapons = result.ConvertedWeaponCount;
            reaperLegacyMigrationNoticeRefund = result.RefundEssence;
            TryDeliverPendingLegacyMigrationRefund();
        }
    }

    public override void OnEnterWorld()
    {
        ReaperCombatService.ResetPlayerState(Player.whoAmI);
        ClearReaperCombatRuntime(sync: false);
        if (Main.netMode == NetmodeID.MultiplayerClient
            && Player.whoAmI == Main.myPlayer)
        {
            // ModPlayer.SyncPlayer is not guaranteed to be invoked client-side
            // after every join/reload. Explicitly retry the character-owned
            // progression upload so the server cannot remain permanently in
            // its uninitialized state and reject combat, forms and the domain.
            clientCharacterStateUploadDelay = 1;
            clientCharacterStateUploadAttempts = 0;
            clientCharacterStateAcknowledged = false;
        }
        if (Main.netMode == NetmodeID.Server || Player.whoAmI != Main.myPlayer || reaperLegacyMigrationChecked)
            return;

        reaperLegacyMigrationChecked = true;
        TryDeliverPendingLegacyMigrationRefund();

        if (reaperLegacyMigrationNoticeWeapons > 0)
        {
            Main.NewText(
                Language.GetTextValue(
                    "Mods.SoulHarvest.Messages.ReaperMigrationComplete",
                    reaperLegacyMigrationNoticeWeapons,
                    reaperLegacyMigrationNoticeRefund),
                new Color(205, 90, 235));
            reaperLegacyMigrationNoticeWeapons = 0;
            reaperLegacyMigrationNoticeRefund = 0;
        }
    }

    public override void PostUpdate()
    {
        UpdateClientCharacterStateUpload();

        if (Main.netMode != NetmodeID.Server && Player.whoAmI == Main.myPlayer && pendingLegacyMigrationEssence > 0)
            TryDeliverPendingLegacyMigrationRefund();

        if (lifeStealCooldown > 0)
            lifeStealCooldown--;
        if (SickleAlternateCooldown > 0)
            SickleAlternateCooldown--;
        if (ReaperSpecialCooldown > 0)
            ReaperSpecialCooldown--;
        if (ReaperExhaustionCooldown > 0)
            ReaperExhaustionCooldown--;
        for (int formIndex = 0; formIndex < reaperUltimateCooldowns.Length; formIndex++)
        {
            if (reaperUltimateCooldowns[formIndex] > 0)
                reaperUltimateCooldowns[formIndex]--;
        }
        TryApplyQueuedReaperForm();
        if (deathDomainSlashSoundCooldown > 0)
            deathDomainSlashSoundCooldown--;

        UpdatePendingReaperDomainHarvests();
        UpdatePendingDeathDomainStrikes();
        UpdateDeathDomain();

        if (Player.dead)
        {
            if (!reaperDeathStateCleared)
            {
                ClearReaperCombatRuntime(sync: true);
                reaperDeathStateCleared = true;
            }
            ClearDeathWingTrail();
            DiscardDeathWingDomainChunk();
        }
        else if (!DeathWingsActiveThisTick && DeathWingTrailOpacity > 0f)
        {
            DeathWingTrailOpacity *= 0.78f;
            if (DeathWingTrailOpacity < 0.025f)
                ClearDeathWingTrail();
        }

        if (!Player.dead && DeathWingTrailOpacity > 0f)
            SubmitDeathWingDomainRifts();

        if (!Player.dead && !DeathWingsActiveThisTick
            && Main.netMode != NetmodeID.MultiplayerClient)
        {
            SpawnDeathWingDomainChunk(keepTail: false);
        }

        if (!Player.dead)
            reaperDeathStateCleared = false;

        DeathWingsActiveLastTick = DeathWingsActiveThisTick;
    }

    private void UpdateClientCharacterStateUpload()
    {
        if (Main.netMode != NetmodeID.MultiplayerClient
            || Player.whoAmI != Main.myPlayer
            || clientCharacterStateAcknowledged)
        {
            return;
        }

        if (clientCharacterStateUploadDelay > 0)
        {
            clientCharacterStateUploadDelay--;
            if (clientCharacterStateUploadDelay > 0)
                return;
        }

        SoulHarvest.SendReaperProgression(Player);
        clientCharacterStateUploadAttempts++;
        clientCharacterStateUploadDelay = clientCharacterStateUploadAttempts switch
        {
            1 => 30,
            2 => 90,
            3 => 180,
            _ => 300
        };
    }

    internal void AcknowledgeServerCharacterState()
    {
        if (Main.netMode == NetmodeID.MultiplayerClient
            && Player.whoAmI == Main.myPlayer)
        {
            clientCharacterStateAcknowledged = true;
            clientCharacterStateUploadDelay = 0;
        }
    }

#if DEBUG
    internal bool ClientCharacterStateAcknowledged
        => clientCharacterStateAcknowledged;
#endif

    private void TryDeliverPendingLegacyMigrationRefund()
    {
        if (pendingLegacyMigrationEssence <= 0)
            return;

        int essenceType = ModContent.ItemType<Soul>();
        int slotCount = Math.Min(58, Player.inventory.Length);
        for (int slot = 0; slot < slotCount && pendingLegacyMigrationEssence > 0; slot++)
        {
            Item item = Player.inventory[slot];
            if (item.type != essenceType || item.stack <= 0 || item.stack >= item.maxStack)
                continue;

            int inserted = Math.Min(pendingLegacyMigrationEssence, item.maxStack - item.stack);
            item.stack += inserted;
            pendingLegacyMigrationEssence -= inserted;
            SyncMigrationInventorySlot(slot);
        }

        for (int slot = 0; slot < slotCount && pendingLegacyMigrationEssence > 0; slot++)
        {
            Item item = Player.inventory[slot];
            if (!item.IsAir)
                continue;

            item.SetDefaults(essenceType);
            int inserted = Math.Min(pendingLegacyMigrationEssence, item.maxStack);
            item.stack = inserted;
            pendingLegacyMigrationEssence -= inserted;
            SyncMigrationInventorySlot(slot);
        }
    }

    private void SyncMigrationInventorySlot(int slot)
    {
        if (Main.netMode != NetmodeID.MultiplayerClient)
            return;

        NetMessage.SendData(
            MessageID.SyncEquipment,
            -1,
            -1,
            null,
            Player.whoAmI,
            PlayerItemSlotID.Inventory0 + slot,
            Player.inventory[slot].prefix);
    }

    public override void ModifyScreenPosition()
    {
        if (deathDomainScreenShakeFrames > 0 && deathDomainScreenShakeStrength > 0.01f)
        {
            float remaining = deathDomainScreenShakeFrames / 6f;
            float phase = (6 - deathDomainScreenShakeFrames) * 2.45f;
            Vector2 tangent = deathDomainScreenShakeDirection.RotatedBy(MathHelper.PiOver2);
            Main.screenPosition += deathDomainScreenShakeDirection * ((float)Math.Sin(phase) * deathDomainScreenShakeStrength * remaining)
                + tangent * ((float)Math.Sin(phase * 0.63f + 1.1f) * deathDomainScreenShakeStrength * remaining * 0.32f);
            deathDomainScreenShakeFrames--;
            if (deathDomainScreenShakeFrames <= 0)
                deathDomainScreenShakeStrength = 0f;
        }

        ReaperVfxDirector.ApplyCameraImpulse(ref Main.screenPosition);
    }

    internal void TriggerDeathDomainImpact(Vector2 worldPosition, Vector2 direction, float strength)
    {
        if (Main.dedServ || Player.whoAmI != Main.myPlayer)
            return;

        float distance = Vector2.Distance(Player.Center, worldPosition);
        float falloff = 1f - MathHelper.Clamp(distance / 1200f, 0f, 1f);
        strength *= falloff * falloff;
        if (strength <= deathDomainScreenShakeStrength)
            return;

        deathDomainScreenShakeStrength = Math.Min(5.2f, strength);
        deathDomainScreenShakeDirection = direction.SafeNormalize(Vector2.UnitY);
        deathDomainScreenShakeFrames = 6;
    }

    internal void PlayDeathDomainSlashSound(Vector2 worldPosition, float mastery, bool requiem)
    {
        if (Main.dedServ
            || Player.whoAmI != Main.myPlayer
            || deathDomainSlashSoundCooldown > 0
            || Vector2.DistanceSquared(Player.Center, worldPosition) > 1600f * 1600f)
        {
            return;
        }

        deathDomainSlashSoundCooldown = 4;
        SoundEngine.PlaySound(SoundID.Item71, worldPosition);
    }

    internal void RecordDeathWingTrail(Vector2 upperTip, Vector2 lowerTip,
        float mastery)
    {
        if (Main.dedServ || deathWingTrailUpdateTick == Main.GameUpdateCount)
            return;

        deathWingTrailUpdateTick = Main.GameUpdateCount;
        if (DeathWingTrailCount > 0
            && (Vector2.DistanceSquared(upperTip, DeathWingUpperTrail[0]) > 120f * 120f
                || Vector2.DistanceSquared(lowerTip, DeathWingLowerTrail[0]) > 120f * 120f
                || Vector2.DistanceSquared(Player.Center, DeathWingBodyTrail[0]) > 120f * 120f))
        {
            ClearDeathWingTrail();
        }

        int copyCount = Math.Min(DeathWingTrailCount, DeathWingTrailCapacity - 1);
        if (copyCount > 0)
        {
            Array.Copy(DeathWingUpperTrail, 0, DeathWingUpperTrail, 1, copyCount);
            Array.Copy(DeathWingLowerTrail, 0, DeathWingLowerTrail, 1, copyCount);
            Array.Copy(DeathWingBodyTrail, 0, DeathWingBodyTrail, 1, copyCount);
        }

        DeathWingUpperTrail[0] = upperTip;
        DeathWingLowerTrail[0] = lowerTip;
        DeathWingBodyTrail[0] = Player.Center;
        DeathWingTrailCount = Math.Min(DeathWingTrailCount + 1, DeathWingTrailCapacity);
        DeathWingTrailMastery = MathHelper.Clamp(mastery, 0f, 1f);
        DeathWingTrailOpacity = 1f;

        float speedProgress = MathHelper.Clamp(
            (Player.velocity.Length() - 2.5f) / 15.5f, 0f, 1f);
        DeathWingRiftStrength = speedProgress;
    }

    internal void RecordDeathWingDomainTrail(float mastery)
    {
        if (Main.netMode == NetmodeID.MultiplayerClient
            || deathWingDomainCombatUpdateTick == Main.GameUpdateCount)
        {
            return;
        }

        deathWingDomainCombatUpdateTick = Main.GameUpdateCount;
        float speedProgress = MathHelper.Clamp(
            (Player.velocity.Length() - 2.5f) / 15.5f, 0f, 1f);
        if (speedProgress <= 0.035f)
        {
            SpawnDeathWingDomainChunk(keepTail: false);
            return;
        }

        float opacity = GetDeathWingRiftOpacity(speedProgress);
        Vector2 point = Player.Center;
        if (deathWingDomainChunkPoints.Count > 0
            && Vector2.DistanceSquared(point,
                deathWingDomainChunkPoints[^1]) > 120f * 120f)
        {
            SpawnDeathWingDomainChunk(keepTail: false);
        }

        if (deathWingDomainChunkPoints.Count >= 6)
        {
            float averageOpacity = 0f;
            foreach (float sample in deathWingDomainChunkOpacities)
                averageOpacity += sample;
            averageOpacity /= deathWingDomainChunkOpacities.Count;
            if (Math.Abs(opacity - averageOpacity) >= 0.16f)
                SpawnDeathWingDomainChunk(keepTail: true);
        }

        if (deathWingDomainChunkPoints.Count > 0
            && Vector2.DistanceSquared(point,
                deathWingDomainChunkPoints[^1]) < 2.25f * 2.25f)
        {
            deathWingDomainChunkPoints[^1] = point;
            deathWingDomainChunkOpacities[^1] = opacity;
            deathWingDomainChunkMastery = Math.Max(
                deathWingDomainChunkMastery,
                MathHelper.Clamp(mastery, 0f, 1f));
            return;
        }

        deathWingDomainChunkPoints.Add(point);
        deathWingDomainChunkOpacities.Add(opacity);
        deathWingDomainChunkHasNewGeometry = true;
        deathWingDomainChunkMastery = Math.Max(deathWingDomainChunkMastery,
            MathHelper.Clamp(mastery, 0f, 1f));
        if (deathWingDomainChunkPoints.Count >= DeathWingDomainChunkCapacity)
            SpawnDeathWingDomainChunk(keepTail: true);
    }

    internal bool TrySpawnDeathWingDomainTrailHarvest(NPC target, int seed)
    {
        if (Main.netMode == NetmodeID.MultiplayerClient
            || !ReaperTargeting.IsValidWeaponTarget(target))
        {
            return false;
        }

        int npcIndex = target.whoAmI;
        ulong currentTick = Main.GameUpdateCount;
        if (deathWingDomainNextHarvestTicks[npcIndex] > currentTick)
            return false;

        deathWingDomainNextHarvestTicks[npcIndex] = currentTick
            + (ulong)Math.Max(1, DeathDomainProgression.SpawnInterval);
        deathWingDomainVolleyCounter++;
        uint hash = unchecked((uint)(seed * 16777619
            + npcIndex * 486187739 + deathWingDomainVolleyCounter * 97));
        hash ^= hash >> 16;
        float rotation = (hash & 0x00FFFFFFu) / 16777215f
            * MathHelper.TwoPi;
        bool requiem = IsDeathDomainDescended(GetEquippedDeathNecklace())
            && deathWingDomainVolleyCounter % 4 == 0;
        SpawnDeathDomainHarvestFromReaperRift(target, requiem, rotation);
        return true;
    }

    private void SpawnDeathWingDomainChunk(bool keepTail)
    {
        int count = deathWingDomainChunkPoints.Count;
        if (count < 2 || !deathWingDomainChunkHasNewGeometry)
        {
            if (!keepTail)
                DiscardDeathWingDomainChunk();
            return;
        }

        int retainedCount = keepTail
            ? Math.Min(DeathWingDomainChunkOverlapPoints, count)
            : 0;
        Span<Vector2> retainedPoints = stackalloc Vector2[
            DeathWingDomainChunkOverlapPoints];
        Span<float> retainedOpacities = stackalloc float[
            DeathWingDomainChunkOverlapPoints];
        for (int index = 0; index < retainedCount; index++)
        {
            int sourceIndex = count - retainedCount + index;
            retainedPoints[index] = deathWingDomainChunkPoints[sourceIndex];
            retainedOpacities[index] =
                deathWingDomainChunkOpacities[sourceIndex];
        }
        float retainedMastery = deathWingDomainChunkMastery;
        float opacity = 0f;
        foreach (float sample in deathWingDomainChunkOpacities)
            opacity += sample;
        opacity /= deathWingDomainChunkOpacities.Count;
        float width = 32f + deathWingDomainChunkMastery * 8f;

        deathWingDomainChunkSequence = unchecked(
            deathWingDomainChunkSequence + 1);
        DeathWingDomainTrailProjectile.Spawn(
            Player.GetSource_Misc("SoulHarvest:DeathWingDomainTrail"),
            Player.whoAmI, deathWingDomainChunkPoints, width, opacity,
            deathWingDomainChunkSequence);

        DiscardDeathWingDomainChunk();
        if (!keepTail)
            return;
        for (int index = 0; index < retainedCount; index++)
        {
            deathWingDomainChunkPoints.Add(retainedPoints[index]);
            deathWingDomainChunkOpacities.Add(retainedOpacities[index]);
        }
        deathWingDomainChunkMastery = retainedMastery;
        deathWingDomainChunkHasNewGeometry = false;
    }

    private void DiscardDeathWingDomainChunk()
    {
        deathWingDomainChunkPoints.Clear();
        deathWingDomainChunkOpacities.Clear();
        deathWingDomainChunkMastery = 0f;
        deathWingDomainChunkHasNewGeometry = false;
    }

    private static float GetDeathWingRiftOpacity(float speedProgress)
    {
        speedProgress = MathHelper.Clamp(speedProgress, 0f, 1f);
        float smoothedSpeed = speedProgress * speedProgress
            * (3f - 2f * speedProgress);
        return MathHelper.Lerp(0.08f, 0.92f, smoothedSpeed);
    }

    private void SubmitDeathWingDomainRifts()
    {
        if (Main.dedServ || DeathWingTrailCount < 4
            || DeathWingRiftStrength <= 0.035f)
        {
            return;
        }

        int pointCount = Math.Min(DeathWingTrailCount,
            8 + (int)(DeathWingTrailMastery * 10f));
        float width = 32f + DeathWingTrailMastery * 8f;
        float opacity = DeathWingTrailOpacity
            * GetDeathWingRiftOpacity(DeathWingRiftStrength);

        DeathDomainTrailVisualSystem.Record(Player.whoAmI,
            DeathWingBodyRiftIdentity,
            new ArraySegment<Vector2>(DeathWingBodyTrail, 0, pointCount),
            width, opacity, mergeOverlappingRims: true,
            taperEnds: false, noiseFadeEnds: true);
    }

    private void ClearDeathWingTrail()
    {
        DeathWingTrailCount = 0;
        DeathWingTrailMastery = 0f;
        DeathWingTrailOpacity = 0f;
        DeathWingRiftStrength = 0f;
        deathWingTrailUpdateTick = ulong.MaxValue;
    }

    public override void SyncPlayer(int toWho, int fromWho, bool newPlayer)
    {
        if (Main.netMode == NetmodeID.SinglePlayer
            || Main.netMode == NetmodeID.MultiplayerClient && Player.whoAmI != Main.myPlayer)
        {
            return;
        }

        // A joining client uploads souls, domain state and the full Reaper tree in
        // one packet. Splitting those character-owned values allowed mutations to
        // occur between two independently accepted initialization packets.
        if (Main.netMode == NetmodeID.MultiplayerClient)
        {
            SoulHarvest.SendReaperProgression(Player, toWho, fromWho);
            return;
        }
        if (!ServerCharacterStateReady)
            return;

        ModPacket packet = Mod.GetPacket();
        packet.Write((byte)SoulHarvest.MessageType.SyncSoulPlayer);
        packet.Write((byte)Player.whoAmI);
        packet.Write(Souls);
        packet.Write(DeathDomainEnabled);
        packet.Write(DeathDomainDescended);
        DeathDomainProgression.NetSend(packet);
        packet.Send(toWho, fromWho);
        SoulHarvest.SendReaperProgression(Player, toWho, fromWho);
        SoulHarvest.SendReaperCombat(Player, toWho, fromWho);
    }

    internal void ReceiveSouls(int souls)
    {
        Souls = Math.Max(0, souls);
    }

    internal void ReceiveDeathDomainState(bool enabled, bool descended)
    {
        bool opening = enabled && !DeathDomainEnabled;
        bool descending = enabled && descended && !DeathDomainDescended;
        DeathDomainEnabled = enabled;
        DeathDomainDescended = enabled && descended;
        if (!enabled)
            deathDomainTimer = 0;
        if ((opening || descending) && Main.netMode != NetmodeID.Server && !Main.dedServ)
        {
            // Two restrained layers give the expansion both a low spatial rupture
            // and a sharp soul-metal opening without requiring a custom asset.
            SoundEngine.PlaySound(SoundID.Roar with
            {
                Volume = 0.42f,
                Pitch = -0.48f,
                PitchVariance = 0.06f
            }, Player.Center);
            SoundEngine.PlaySound(SoundID.Item122 with
            {
                Volume = 1.05f,
                Pitch = -0.28f,
                PitchVariance = 0.08f
            }, Player.Center);
        }
    }

    internal bool ServerCharacterStateReady => Main.netMode != NetmodeID.Server
        || serverSoulBalanceInitialized && serverReaperProgressionInitialized;

    internal bool TryInitializeServerCharacterState(
        int souls,
        bool deathDomainEnabled,
        bool deathDomainDescended,
        BinaryReader reader)
    {
        if (Main.netMode != NetmodeID.Server)
            return false;

        if (serverSoulBalanceInitialized || serverReaperProgressionInitialized)
        {
            // The client retries until it receives an acknowledgement. Retried
            // uploads are not authority, but their full payload must still be
            // consumed or tModLoader disconnects the sender for an under-read.
            DeathDomainProgression discardedDomain = new();
            ReaperProgressionState discardedProgression = new();
            discardedDomain.NetReceive(reader);
            discardedProgression.NetReceive(reader);
            return false;
        }

        Souls = Math.Max(0, souls);
        DeathNecklace? necklace = FindEquippedDeathNecklace();
        DeathDomainEnabled = deathDomainEnabled && necklace is not null;
        DeathDomainDescended = DeathDomainEnabled
            && deathDomainDescended
            && necklace?.DeathDescentUnlocked == true;
        DeathDomainProgression.NetReceive(reader);
        ReaperProgression.NetReceive(reader);
        VisibleReaperForm = ReaperCombatRegistry.ResolveUsableForm(ReaperProgression);
        VisibleReaperStage = ReaperProgression.GetStage(VisibleReaperForm);
        serverSoulBalanceInitialized = true;
        serverReaperProgressionInitialized = true;
        return true;
    }

    internal bool TrySpendSouls(int amount, bool sync = true)
    {
        if (amount < 0
            || Souls < amount
            || Main.netMode == NetmodeID.MultiplayerClient
            || !ServerCharacterStateReady)
            return false;

        Souls -= amount;
        if (sync)
            SyncSoulBalance();
        return true;
    }

    public void AddSouls(int amount, bool announce = false)
    {
        if (amount <= 0 || Main.netMode == NetmodeID.MultiplayerClient || !ServerCharacterStateReady)
            return;

        amount = DeathDomainProgression.ApplySoulRefinement(amount);
        Souls = (int)Math.Min(int.MaxValue, (long)Souls + amount);
        SyncSoulBalance();

        if (!announce)
            return;

        if (Main.netMode == NetmodeID.Server)
        {
            ChatHelper.SendChatMessageToClient(
                NetworkText.FromKey("Mods.SoulHarvest.Messages.BossSoulReward", amount),
                new Color(90, 235, 255),
                Player.whoAmI);
        }
        else if (Player.whoAmI == Main.myPlayer)
        {
            Main.NewText(Language.GetTextValue("Mods.SoulHarvest.Messages.BossSoulReward", amount), new Color(90, 235, 255));
        }
    }

    public bool TryExtractEssence()
    {
        if (Main.netMode == NetmodeID.MultiplayerClient || !ServerCharacterStateReady)
            return false;

        if (Souls < SoulsPerEssence)
        {
            SoulHarvest.SendOperationResult(Player, SoulHarvest.OperationResult.NotEnoughSouls);
            return false;
        }

        int essenceType = ModContent.ItemType<Soul>();
        if (!TryFindInventoryInsertionSlot(essenceType, out int changedSlot))
        {
            SoulHarvest.SendOperationResult(Player, SoulHarvest.OperationResult.InventoryFull);
            return false;
        }

        Souls -= SoulsPerEssence;
        Item destination = Player.inventory[changedSlot];
        if (destination.type == essenceType && destination.stack > 0)
            destination.stack++;
        else
        {
            destination.SetDefaults(essenceType);
            destination.stack = 1;
        }
        SoulHarvest.SendReaperProgressionTransaction(Player, new[] { changedSlot });
        SoulHarvest.SendOperationResult(Player, SoulHarvest.OperationResult.EssenceExtracted);
        return true;
    }

    public bool TryAbsorbEssence()
    {
        if (Main.netMode == NetmodeID.MultiplayerClient || !ServerCharacterStateReady)
            return false;

        int essenceType = ModContent.ItemType<Soul>();
        int changedSlot = -1;
        for (int slot = 0; slot < 58; slot++)
        {
            Item item = Player.inventory[slot];
            if (item.type != essenceType || item.stack <= 0)
                continue;

            item.stack--;
            if (item.stack <= 0)
                item.TurnToAir();
            changedSlot = slot;
            break;
        }

        if (changedSlot < 0)
        {
            SoulHarvest.SendOperationResult(Player, SoulHarvest.OperationResult.NotEnoughEssence);
            return false;
        }

        Souls = (int)Math.Min(int.MaxValue, (long)Souls + SoulsPerEssence);
        SoulHarvest.SendReaperProgressionTransaction(Player, new[] { changedSlot });
        SoulHarvest.SendOperationResult(Player, SoulHarvest.OperationResult.EssenceAbsorbed);
        return true;
    }

    private bool TryFindInventoryInsertionSlot(int itemType, out int slot)
    {
        slot = -1;
        int emptySlot = -1;
        int slotCount = Math.Min(58, Player.inventory.Length);
        for (int index = 0; index < slotCount; index++)
        {
            Item item = Player.inventory[index];
            if (item.type == itemType && item.stack > 0 && item.stack < item.maxStack)
            {
                slot = index;
                return true;
            }
            if (emptySlot < 0 && item.IsAir)
                emptySlot = index;
        }

        slot = emptySlot;
        return slot >= 0;
    }

    public int TryLifeSteal(int damageDone, int lifeStealLevel)
    {
        if (lifeStealLevel <= 0
            || damageDone <= 0
            || lifeStealCooldown > 0
            || Player.dead
            || Main.netMode == NetmodeID.MultiplayerClient && Player.whoAmI != Main.myPlayer)
        {
            return 0;
        }

        int missingLife = Player.statLifeMax2 - Player.statLife;
        if (missingLife <= 0)
            return 0;

        int amount = Math.Min(missingLife, Math.Max(1, (int)Math.Ceiling(damageDone * lifeStealLevel * 0.01f)));
        Player.Heal(amount);
        lifeStealCooldown = 12;
        if (Main.netMode != NetmodeID.SinglePlayer)
            NetMessage.SendData(MessageID.PlayerLifeMana, -1, -1, null, Player.whoAmI);
        return amount;
    }

    internal int ApplyDeathReaperLifeSteal(int damageDone, Vector2 visualSource,
        int requestedLevel = 0)
    {
        if (damageDone <= 0 || Player.dead || Main.netMode == NetmodeID.MultiplayerClient)
            return 0;

        int lifeStealLevel = Math.Clamp(Math.Max(requestedLevel,
            DeathDomainProgression.LifeStealLevel), 0, DeathDomainProgression.MaxLifeStealLevel);
        if (lifeStealLevel <= 0)
            return 0;
        int missingLife = Math.Max(0, Player.statLifeMax2 - Player.statLife);
        int perHitCap = Math.Max(1, (int)Math.Ceiling(Player.statLifeMax2 * 0.025f));
        int amount = Math.Min(missingLife, Math.Min(perHitCap,
            Math.Max(1, (int)Math.Ceiling(damageDone * lifeStealLevel * 0.01f))));

        if (amount > 0)
        {
            Player.Heal(amount);
            if (Main.netMode == NetmodeID.Server)
                NetMessage.SendData(MessageID.PlayerLifeMana, -1, -1, null, Player.whoAmI);
        }

        // Death Reaper attacks always show the return stream, including at full life.
        // This makes every successful slash readable without granting phantom healing.
        LifeStealVisuals.Spawn(visualSource, Player.whoAmI, lifeStealLevel, amount,
            forceFeedback: true);
        return amount;
    }

    internal int TakeNextSwingDirection(int facingDirection)
    {
        int direction = swingComboDirection * facingDirection;
        swingComboDirection *= -1;
        return direction;
    }

    internal void StartSickleAlternateCooldown(int frames)
    {
        if (frames <= 0 || SickleAlternateCooldown > 0)
            return;

        SickleAlternateCooldown = frames;
        SickleAlternateCooldownMax = frames;
    }

    public float GetReaperEnergy(ReaperFormId form)
    {
        int index = (int)form;
        return index > (int)ReaperFormId.Base && index < reaperEnergy.Length
            ? MathHelper.Clamp(reaperEnergy[index], 0f, 100f)
            : 0f;
    }

    public int GetReaperUltimateCooldown(ReaperFormId form)
    {
        int index = (int)form;
        return index >= 0 && index < reaperUltimateCooldowns.Length
            ? Math.Max(0, reaperUltimateCooldowns[index])
            : 0;
    }

    internal bool TryGainReaperEnergy(ReaperFormId form, float amount)
    {
        if (amount <= 0f || Main.netMode == NetmodeID.MultiplayerClient)
            return false;

        bool unlocked = form == ReaperFormId.Death
            ? ReaperProgression.DeathFormUnlocked
            : ReaperDefinitions.IsBranchForm(form) && ReaperProgression.GetStage(form) == ReaperStage.StageIII;
        int index = (int)form;
        if (!unlocked || index <= (int)ReaperFormId.Base || index >= reaperEnergy.Length)
            return false;

        float gainMultiplier = 1f + ReaperProgression.GetCommonNodeLevel(ReaperCommonNode.EnergyGain) * 0.2f;
        float previous = reaperEnergy[index];
        reaperEnergy[index] = MathHelper.Clamp(previous + amount * gainMultiplier, 0f, 100f);
        if ((int)previous != (int)reaperEnergy[index] || reaperEnergy[index] >= 100f)
            SyncReaperCombat();
        return reaperEnergy[index] > previous;
    }

#if DEBUG
    internal void DebugSetReaperEnergy(ReaperFormId form, float amount)
    {
        if (Main.netMode == NetmodeID.MultiplayerClient)
            return;
        int index = (int)form;
        if (index <= (int)ReaperFormId.Base || index >= reaperEnergy.Length)
            return;
        reaperEnergy[index] = MathHelper.Clamp(amount, 0f, 100f);
        reaperUltimateCooldowns[index] = 0;
        ReaperExhaustionCooldown = 0;
        SyncReaperCombat();
    }
#endif

    internal void RecordReaperAttackHit(ReaperFormId form, ReaperHitKind kind)
    {
        // Ultimate charge has one universal source: an attack actually connecting
        // with an enemy. Damage-over-time ticks and the ultimate itself never feed
        // the meter, while primary, derived, returning and special hits all count.
        if (kind is ReaperHitKind.Ultimate or ReaperHitKind.UltimateDerived or ReaperHitKind.DamageOverTime)
            return;
        TryGainReaperEnergy(form, 4f);
    }

    internal bool TryStartReaperSpecialCooldown(int frames)
    {
        if (frames <= 0 || ReaperSpecialCooldown > 0)
            return false;

        float reduction = Math.Clamp(
            ReaperProgression.GetCommonNodeLevel(ReaperCommonNode.SpecialCooldown) * 0.1f, 0f, 0.5f);
        int adjustedFrames = Math.Max(30, (int)Math.Ceiling(frames * (1f - reduction)));
        ReaperSpecialCooldown = adjustedFrames;
        ReaperSpecialCooldownMax = adjustedFrames;
        SyncReaperCombat();
        return true;
    }

    internal void ReduceReaperSpecialCooldown(int frames)
    {
        if (frames <= 0 || Main.netMode == NetmodeID.MultiplayerClient || ReaperSpecialCooldown <= 0)
            return;

        int previous = ReaperSpecialCooldown;
        ReaperSpecialCooldown = Math.Max(0, ReaperSpecialCooldown - frames);
        if (ReaperSpecialCooldown != previous)
            SyncReaperCombat();
    }

    internal void RefundFailedReaperSpecial()
    {
        if (Main.netMode == NetmodeID.MultiplayerClient)
            return;
        ReaperSpecialCooldown = 0;
        ReaperSpecialCooldownMax = 0;
        SyncReaperCombat();
    }

    internal bool TrySwitchOrQueueReaperForm(ReaperFormId form)
    {
        if (Main.netMode == NetmodeID.MultiplayerClient)
            return false;

        ulong now = Main.GameUpdateCount;
        bool assemblyActive = ReaperFormAssemblyProjectile.TryGetActive(
            Player, out ReaperFormAssemblyProjectile? activeAssembly);
        if (lastReaperFormSwitchRequestTick != 0 && now - lastReaperFormSwitchRequestTick < 10UL)
        {
            // Wheel input can commit twice while closing. Treat an identical
            // request as an idempotent acknowledgement instead of reporting a
            // misleading progression/prerequisite failure to the client.
            if (queuedReaperForm == form
                || activeAssembly?.TargetForm == form
                || ReaperProgression.CurrentForm == form)
                return true;
        }
        lastReaperFormSwitchRequestTick = now;

        if (!assemblyActive && ReaperProgression.CurrentForm == form)
            return true;

        if (IsReaperCombatActionBusy() || assemblyActive)
        {
            queuedReaperForm = form;
            return true;
        }

        queuedReaperForm = null;
        return TryStartReaperFormAssembly(form);
    }

    private void TryApplyQueuedReaperForm()
    {
        if (Main.netMode == NetmodeID.MultiplayerClient
            || queuedReaperForm is not ReaperFormId form
            || IsReaperCombatActionBusy()
            || ReaperFormAssemblyProjectile.TryGetActive(Player, out _))
            return;

        queuedReaperForm = null;
        if (ReaperProgression.CurrentForm != form)
            TryStartReaperFormAssembly(form);
    }

    private bool IsReaperCombatActionBusy()
    {
        return Player.itemAnimation > 0
            || Player.itemTime > 0
            || Player.ownedProjectileCounts[ModContent.ProjectileType<SickleSwingProjectile>()] > 0
            || Player.ownedProjectileCounts[ModContent.ProjectileType<ReaperActionControllerProjectile>()] > 0;
    }

    internal bool IsReaperFormAssemblyActive()
        => ReaperFormAssemblyProjectile.TryGetActive(Player, out _);

    internal bool TryCompleteReaperFormAssembly(int projectileIndex,
        ReaperFormId form)
    {
        if (Main.netMode == NetmodeID.MultiplayerClient
            || projectileIndex < 0 || projectileIndex >= Main.maxProjectiles)
        {
            return false;
        }

        Projectile projectile = Main.projectile[projectileIndex];
        if (!projectile.active || projectile.owner != Player.whoAmI
            || projectile.ModProjectile
                is not ReaperFormAssemblyProjectile assembly
            || assembly.TargetForm != form
            || !assembly.AssemblyComplete
            || !ReaperProgression.TrySetCurrentForm(form))
        {
            return false;
        }

        VisibleReaperForm = ReaperCombatRegistry.ResolveUsableForm(
            ReaperProgression);
        VisibleReaperStage = ReaperProgression.GetStage(VisibleReaperForm);
        SyncReaperProgression();
        return true;
    }

    private bool TryStartReaperFormAssembly(ReaperFormId form)
    {
        ReaperStage stage = form switch
        {
            ReaperFormId.Base => ReaperStage.StageI,
            ReaperFormId.Death => ReaperStage.StageIII,
            _ => ReaperProgression.GetStage(form)
        };
        int level = ReaperProgression.GetCommonNodeLevel(
            ReaperCommonNode.AssemblySpeed);
        int duration = ReaperDefinitions.GetAssemblyFrames(level);
        int rangeLevel = ReaperProgression.GetCommonNodeLevel(
            ReaperCommonNode.DeathRange);
        bool started = ReaperFormAssemblyProjectile.Spawn(Player, form, stage,
            duration, rangeLevel) >= 0;
#if DEBUG
        Mod.Logger.Info(
            $"SOULHARVEST_FORM_ASSEMBLY: start player={Player.whoAmI} from={ReaperProgression.CurrentForm} target={form} duration={duration} started={started}");
#endif
        return started;
    }

    internal bool TryConsumeReaperUltimate(ReaperFormId form, int cooldownFrames)
    {
        if (Main.netMode == NetmodeID.MultiplayerClient
            || form != ReaperProgression.CurrentForm
            || ReaperExhaustionCooldown > 0
            || GetReaperUltimateCooldown(form) > 0
            || GetReaperEnergy(form) < 100f)
        {
            return false;
        }

        bool unlocked = form == ReaperFormId.Death
            ? ReaperProgression.DeathFormUnlocked
            : ReaperDefinitions.IsBranchForm(form) && ReaperProgression.GetStage(form) == ReaperStage.StageIII;
        if (!unlocked)
            return false;

        int index = (int)form;
        reaperEnergy[index] = 0f;
        reaperUltimateCooldowns[index] = Math.Max(1, cooldownFrames);
        ReaperExhaustionCooldown = 60 * 8;
        SyncReaperCombat();
        return true;
    }

    internal void RefundFailedReaperUltimate(ReaperFormId form)
    {
        if (Main.netMode == NetmodeID.MultiplayerClient)
            return;
        int index = (int)form;
        if (index <= (int)ReaperFormId.Base || index >= reaperEnergy.Length)
            return;
        reaperEnergy[index] = 100f;
        reaperUltimateCooldowns[index] = 0;
        ReaperExhaustionCooldown = 0;
        SyncReaperCombat();
    }

    internal void RecordDeathCycleHit(int phase)
    {
        if (phase < 0 || phase >= 6 || ReaperProgression.CurrentForm != ReaperFormId.Death)
            return;

        byte bit = (byte)(1 << phase);
        if ((deathCycleMask & bit) != 0)
            return;

        deathCycleMask |= bit;
        if (deathCycleMask == 0x3F)
            deathCycleMask = 0;
    }

    internal void ClearReaperCombatRuntime(bool sync)
    {
        Array.Clear(reaperEnergy, 0, reaperEnergy.Length);
        Array.Clear(reaperUltimateCooldowns, 0, reaperUltimateCooldowns.Length);
        ReaperSpecialCooldown = 0;
        ReaperSpecialCooldownMax = 0;
        ReaperExhaustionCooldown = 0;
        deathCycleMask = 0;
        queuedReaperForm = null;
        pendingReaperDomainHarvests.Clear();
        Player.GetModPlayer<ReaperCombatPlayer>().ClearRuntime();
        if (sync)
            SyncReaperCombat();
    }

    internal void ResetReaperTreeForDebug()
    {
        ReaperProgression.ResetTreeForDebug();
        VisibleReaperForm = ReaperFormId.Base;
        VisibleReaperStage = ReaperStage.StageI;
        ClearReaperCombatRuntime(sync: true);
        if (Main.netMode == NetmodeID.Server)
            SoulHarvest.SendReaperProgressionTransaction(Player, Array.Empty<int>());
    }

    internal void WriteReaperProgression(BinaryWriter writer) => ReaperProgression.NetSend(writer);

    internal void WriteDeathDomainProgression(BinaryWriter writer) => DeathDomainProgression.NetSend(writer);

    internal void ReceiveDeathDomainProgression(BinaryReader reader) => DeathDomainProgression.NetReceive(reader);

    internal void ReceiveReaperProgression(BinaryReader reader)
    {
        ReaperProgression.NetReceive(reader);
        VisibleReaperForm = ReaperCombatRegistry.ResolveUsableForm(ReaperProgression);
        VisibleReaperStage = ReaperProgression.GetStage(VisibleReaperForm);
    }

    internal void ReceiveReaperPublicState(ReaperFormId form, ReaperStage stage)
    {
        VisibleReaperForm = Enum.IsDefined(form) ? form : ReaperFormId.Base;
        VisibleReaperStage = stage is >= ReaperStage.Locked and <= ReaperStage.StageIII ? stage : ReaperStage.Locked;
    }

    internal void WriteReaperCombat(BinaryWriter writer)
    {
        writer.Write((byte)ReaperProgression.CurrentForm);
        for (int index = 0; index < reaperEnergy.Length; index++)
            writer.Write((ushort)Math.Clamp((int)Math.Round(reaperEnergy[index] * 10f), 0, 1000));
        writer.Write((ushort)Math.Clamp(ReaperSpecialCooldown, 0, ushort.MaxValue));
        writer.Write((ushort)Math.Clamp(ReaperSpecialCooldownMax, 0, ushort.MaxValue));
        writer.Write((ushort)Math.Clamp(ReaperExhaustionCooldown, 0, ushort.MaxValue));
        for (int index = 0; index < reaperUltimateCooldowns.Length; index++)
            writer.Write((ushort)Math.Clamp(reaperUltimateCooldowns[index], 0, ushort.MaxValue));
        writer.Write(deathCycleMask);
        Player.GetModPlayer<ReaperCombatPlayer>().WriteOwnerRuntime(writer);
    }

    internal void ReceiveReaperCombat(BinaryReader reader)
    {
        ReaperProgression.TrySetCurrentForm((ReaperFormId)reader.ReadByte());
        VisibleReaperForm = ReaperCombatRegistry.ResolveUsableForm(ReaperProgression);
        VisibleReaperStage = ReaperProgression.GetStage(VisibleReaperForm);
        for (int index = 0; index < reaperEnergy.Length; index++)
            reaperEnergy[index] = MathHelper.Clamp(reader.ReadUInt16() / 10f, 0f, 100f);
        ReaperSpecialCooldown = reader.ReadUInt16();
        ReaperSpecialCooldownMax = reader.ReadUInt16();
        ReaperExhaustionCooldown = reader.ReadUInt16();
        for (int index = 0; index < reaperUltimateCooldowns.Length; index++)
            reaperUltimateCooldowns[index] = reader.ReadUInt16();
        deathCycleMask = reader.ReadByte();
        Player.GetModPlayer<ReaperCombatPlayer>().ReceiveOwnerRuntime(reader);
    }

    public void SyncReaperProgression() => SoulHarvest.SendReaperProgression(Player);

    private void SyncReaperCombat() => SoulHarvest.SendReaperCombat(Player);

    internal void SyncReaperCombatRuntime() => SyncReaperCombat();

    public override void ProcessTriggers(TriggersSet triggersSet)
    {
        bool reaperInputBlocked = Main.playerInventory
            || Main.drawingPlayerChat
            || Main.editSign
            || Main.editChest
            || Main.mapFullscreen
            || Player.talkNPC >= 0
            || Player.mouseInterface
            || Main.InGameUI.CurrentState is not null
            || ModContent.GetInstance<DeathAltarUISystem>().IsVisible
            || ReaperFormWheelUISystem.IsWheelOpen;
        bool holdingReaperSickle = Player.HeldItem.ModItem is NormalSickle;
        bool deathFormSelected = holdingReaperSickle
            && ReaperCombatRegistry.ResolveUsableForm(ReaperProgression)
                == ReaperFormId.Death;
        bool primaryInputHeld = deathFormSelected && Player.controlUseItem;
        if (reaperPrimaryInputWasHeld && !primaryInputHeld)
        {
            ReaperCombatService.ReleaseDeathPrimaryHold(Player);
            if (Main.netMode == NetmodeID.MultiplayerClient)
                SoulHarvest.SendReaperPrimaryRelease(Player);
        }
        reaperPrimaryInputWasHeld = primaryInputHeld;

        // Mouse2 is Death's documented input. Keep the configurable keybind, but
        // also read the physical button so a stale/missing keybind profile cannot
        // silently disable the whole right-click request chain.
        bool rawSpecialHeld = Main.mouseRight;
        bool specialHeld = SoulHarvest.SpecialAttackKeybind?.Current == true
            || rawSpecialHeld;
        bool specialPressed = SoulHarvest.SpecialAttackKeybind?.JustPressed == true
            || rawSpecialHeld && !rawSpecialInputWasHeld;
        bool specialReleased = SoulHarvest.SpecialAttackKeybind?.JustReleased == true
            || !specialHeld && rawSpecialInputWasHeld;
        // A release must still reach the server if an inventory or another UI was
        // opened during the charge. The controller also auto-releases at its cap.
        if (holdingReaperSickle && specialReleased)
        {
            if (Main.netMode == NetmodeID.MultiplayerClient)
            {
                // Release the replicated owner controller immediately as well as
                // notifying the server. A quick tap can end before the server's
                // spawn packet arrives; the controller also has an owner-input
                // fallback for that late-arrival case.
                ReaperActionControllerProjectile.RequestLocalOwnerRelease(Player);
                SoulHarvest.SendReaperSpecialRelease(Player);
            }
            else
                ReaperCombatService.TryReleaseSpecial(Player);
        }

        if (holdingReaperSickle && !reaperInputBlocked && !Player.dead && !Player.CCed && !Player.noItems)
        {
            if (specialHeld)
                Player.controlUseTile = false;

            Vector2 cursorOffset = Main.MouseWorld - Player.MountedCenter;
            Vector2 aim = cursorOffset.SafeNormalize(Vector2.UnitX * Player.direction);
            bool deathInvocationHeld = specialHeld
                && ReaperCombatRegistry.ResolveUsableForm(ReaperProgression) == ReaperFormId.Death;
            if (specialPressed)
            {
                if (Main.netMode == NetmodeID.MultiplayerClient)
                    SoulHarvest.SendReaperAction(Player, ultimate: false, cursorOffset);
                else
                    ReaperCombatService.TryStartSpecial(Player, cursorOffset);
            }
            else if (deathInvocationHeld && Main.GameUpdateCount % 5UL == (ulong)(Player.whoAmI % 5))
            {
                int controllerType = ModContent.ProjectileType<ReaperActionControllerProjectile>();
                bool invocationExists = Player.ownedProjectileCounts[controllerType] > 0;
                if (Main.netMode == NetmodeID.MultiplayerClient)
                {
                    // A press can reach the server during the last primary-swing
                    // frame and be rejected legitimately. While Death's invocation
                    // key remains held, retry the start at a bounded cadence until
                    // its controller is replicated; afterwards packets only update
                    // the cursor target.
                    if (invocationExists)
                        SoulHarvest.SendReaperSpecialAim(Player, cursorOffset);
                    else
                        SoulHarvest.SendReaperAction(Player, ultimate: false, cursorOffset);
                }
                else if (invocationExists)
                {
                    ReaperCombatService.TryUpdateSpecialTarget(Player, cursorOffset);
                }
                else
                {
                    ReaperCombatService.TryStartSpecial(Player, cursorOffset);
                }
            }

            if (SoulHarvest.UltimateKeybind?.JustPressed == true)
            {
                if (Main.netMode == NetmodeID.MultiplayerClient)
                    SoulHarvest.SendReaperAction(Player, ultimate: true, cursorOffset);
                else
                    ReaperCombatService.TryStartUltimate(Player, cursorOffset);
            }
        }

        rawSpecialInputWasHeld = rawSpecialHeld;

        ProcessDeathDomainKeybind();
    }

    private void ProcessDeathDomainKeybind()
    {
        ModKeybind? keybind = SoulHarvest.ToggleDeathDomainKeybind;
        if (keybind is null)
            return;

        if (keybind.JustPressed)
        {
            deathDomainKeyHoldFrames = 0;
            deathDomainLongPressHandled = false;
        }

        if (keybind.Current)
        {
            deathDomainKeyHoldFrames++;
            if (!deathDomainLongPressHandled
                && deathDomainKeyHoldFrames >= DeathDomainLongPressFrames)
            {
                deathDomainLongPressHandled = true;
                TryToggleDeathDomainDescent();
            }
            return;
        }

        if (keybind.JustReleased && !deathDomainLongPressHandled)
            TryToggleFiniteDeathDomain();

        if (keybind.JustReleased || deathDomainKeyHoldFrames > 0)
        {
            deathDomainKeyHoldFrames = 0;
            deathDomainLongPressHandled = false;
        }
    }

    private void TryToggleFiniteDeathDomain()
    {
        DeathNecklace? necklace = ActiveDeathNecklace ?? FindEquippedDeathNecklace();
        if (necklace is null)
            return;

        bool enabled = !DeathDomainEnabled;
        ReceiveDeathDomainState(enabled, descended: false);
        SoulHarvest.SendDeathDomainState(Player, DeathDomainEnabled, DeathDomainDescended);
    }

    private void TryToggleDeathDomainDescent()
    {
        DeathNecklace? necklace = ActiveDeathNecklace ?? FindEquippedDeathNecklace();
        if (necklace is null)
            return;
        if (!necklace.DeathDescentUnlocked)
            return;

        bool descended = !DeathDomainDescended;
        ReceiveDeathDomainState(enabled: true, descended: descended);
        SoulHarvest.SendDeathDomainState(Player, DeathDomainEnabled, DeathDomainDescended);
    }

    internal void SetDeathNecklace(DeathNecklace necklace)
    {
        if (ActiveDeathNecklace is null || necklace.MasteryScore > ActiveDeathNecklace.MasteryScore)
            ActiveDeathNecklace = necklace;
    }

    private DeathNecklace? FindEquippedDeathNecklace()
    {
        DeathNecklace? best = null;
        foreach (Item item in Player.armor)
        {
            if (item.ModItem is DeathNecklace candidate && (best is null || candidate.MasteryScore > best.MasteryScore))
                best = candidate;
        }
        return best;
    }

    internal void MergeLegacyDeathNecklaceProgression(DeathNecklace necklace)
    {
        bool changed = DeathDomainProgression.MergeLegacyNecklaceLevels(
            necklace.LegacyFrequencyLevel,
            necklace.LegacyDamageLevel,
            necklace.LegacyVolleyLevel,
            necklace.LegacyLifeStealLevel);
        if (!changed)
            return;

        if (Main.netMode == NetmodeID.Server)
            SyncPlayer(Player.whoAmI, -1, false);
        else if (Main.netMode == NetmodeID.MultiplayerClient && Player.whoAmI == Main.myPlayer)
            SoulHarvest.SendReaperProgression(Player);
    }

    internal DeathNecklace? GetEquippedDeathNecklace()
        => ActiveDeathNecklace ?? FindEquippedDeathNecklace();

    internal bool IsDeathDomainDescended(DeathNecklace? necklace)
        => DeathDomainEnabled
            && DeathDomainDescended
            && necklace?.DeathDescentUnlocked == true;

    internal void SpawnDeathDomainHarvestFromReaperRift(NPC target,
        bool requiem, float rotation)
    {
        if (Main.netMode == NetmodeID.MultiplayerClient || !target.active)
            return;
        SpawnDeathDomainHarvestSlash(target, DeathDomainProgression.VisualMastery,
            requiem, rotation);
    }

    internal void QueueDeathDomainHarvestFromReaperHit(NPC target,
        float rotation, int deathUltimateActionId = -1)
    {
        if (Main.netMode == NetmodeID.MultiplayerClient || !target.active)
            return;

        const int telegraphFrames = 12;
        float visualMastery = DeathDomainProgression.VisualMastery;
        float scale = MathHelper.Clamp(Math.Max(target.width, target.height) / 52f,
            0.85f, 2.4f);
        deathDomainVolleyCounter++;
        bool requiem = IsDeathDomainDescended(GetEquippedDeathNecklace())
            && deathDomainVolleyCounter % 4 == 0;

        if (Main.netMode == NetmodeID.Server)
        {
            SoulHarvest.BroadcastDeathDomainTelegraph(Player, target,
                telegraphFrames, DeathDomainProgression.SlashCount, visualMastery, rotation,
                scale);
        }
        else
        {
            int projectileIndex = Projectile.NewProjectile(
                Player.GetSource_Misc("SoulHarvest:ReaperNecklaceTelegraph"),
                target.Center,
                Vector2.Zero,
                ModContent.ProjectileType<DeathDomainHarvestTelegraphProjectile>(),
                0,
                0f,
                Player.whoAmI,
                target.whoAmI,
                telegraphFrames,
                DeathDomainProgression.SlashCount + visualMastery * 0.1f);
            if (projectileIndex >= 0 && projectileIndex < Main.maxProjectiles)
            {
                Projectile telegraph = Main.projectile[projectileIndex];
                telegraph.rotation = rotation;
                telegraph.scale = scale;
            }
        }

        pendingReaperDomainHarvests.Add(new PendingReaperDomainHarvest(
            target.whoAmI,
            telegraphFrames,
            rotation,
            visualMastery,
            requiem,
            deathUltimateActionId));
    }

#if DEBUG
    internal bool HasPendingReaperDomainHarvest(int npcIndex)
        => pendingReaperDomainHarvests.Exists(pending
            => pending.NpcIndex == npcIndex);

    internal bool HasPendingDeathDomainStrike(int npcIndex)
        => pendingDeathDomainStrikes.Exists(pending
            => pending.NpcIndex == npcIndex);
#endif

    private void UpdatePendingReaperDomainHarvests()
    {
        if (Main.netMode == NetmodeID.MultiplayerClient
            || pendingReaperDomainHarvests.Count == 0)
        {
            return;
        }

        for (int index = pendingReaperDomainHarvests.Count - 1;
            index >= 0; index--)
        {
            PendingReaperDomainHarvest pending =
                pendingReaperDomainHarvests[index];
            int remainingFrames = pending.RemainingFrames - 1;
            if (remainingFrames > 0)
            {
                pendingReaperDomainHarvests[index] = pending with
                {
                    RemainingFrames = remainingFrames
                };
                continue;
            }

            pendingReaperDomainHarvests.RemoveAt(index);
            if (pending.NpcIndex < 0 || pending.NpcIndex >= Main.maxNPCs)
                continue;
            NPC target = Main.npc[pending.NpcIndex];
            if (!ReaperTargeting.IsValidWeaponTarget(target))
            {
                continue;
            }
            SpawnDeathDomainHarvestSlash(target, pending.VisualMastery,
                pending.Requiem, pending.Rotation,
                pending.DeathUltimateActionId);
        }
    }

    private void UpdateDeathDomain()
    {
        // UpdateAccessory normally supplies this reference. The direct equipment scan
        // keeps dedicated-server attacks stable on ticks where accessory recalculation
        // did not expose the ModItem instance before PostUpdate.
        DeathNecklace? necklace = ActiveDeathNecklace ?? FindEquippedDeathNecklace();
        if (DeathDomainEnabled && necklace is null)
        {
            ReceiveDeathDomainState(enabled: false, descended: false);
            if (Main.netMode == NetmodeID.Server)
                SoulHarvest.BroadcastDeathDomainState(Player.whoAmI, enabled: false, descended: false);
            else
                SoulHarvest.SendDeathDomainState(Player, enabled: false, descended: false);
        }
        if (!DeathDomainEnabled || necklace is null || Player.dead)
        {
            deathDomainTimer = 0;
            ClearDeathDomainTelegraphs();
            return;
        }

        // Percentage damage is authoritative. Clients receive synchronized NPC strikes
        // and the dedicated visual projectile spawned by the server.
        if (Main.netMode == NetmodeID.MultiplayerClient)
            return;

        bool descended = IsDeathDomainDescended(necklace);
        ApplyDeathDomainForces(necklace, descended);
        ApplyDeathDomainItemAttraction();
        List<NPC> targets = FindDeathDomainTargets(necklace, descended);
        float visualMastery = DeathDomainProgression.VisualMastery;
        UpdateDeathDomainTelegraphs(targets, visualMastery);
        deathDomainTimer++;
        if (deathDomainTimer < DeathDomainProgression.SpawnInterval)
            return;
        deathDomainTimer = 0;

        if (targets.Count == 0)
        {
            ClearDeathDomainTelegraphs();
            return;
        }

        deathDomainVolleyCounter++;
        bool requiem = descended && deathDomainVolleyCounter % 4 == 0;
        foreach (NPC target in targets)
        {
            float rotation = deathDomainPendingCuts.TryGetValue(target.whoAmI, out PendingDeathDomainCut pending)
                ? pending.Rotation
                : Main.rand.NextFloat(MathHelper.TwoPi);
            SpawnDeathDomainHarvestSlash(target, visualMastery, requiem, rotation);
        }
        ClearDeathDomainTelegraphs();
    }

    private void UpdateDeathDomainTelegraphs(List<NPC> targets, float visualMastery)
    {
        HashSet<int> activeTargets = [];
        foreach (NPC target in targets)
            activeTargets.Add(target.whoAmI);

        List<int> staleTargets = [];
        foreach (int npcIndex in deathDomainPendingCuts.Keys)
            if (!activeTargets.Contains(npcIndex))
                staleTargets.Add(npcIndex);
        foreach (int npcIndex in staleTargets)
            RemoveDeathDomainTelegraph(npcIndex);

        int remainingFrames = Math.Max(1, DeathDomainProgression.SpawnInterval - deathDomainTimer);
        foreach (NPC target in targets)
        {
            if (deathDomainPendingCuts.ContainsKey(target.whoAmI))
                continue;

            float rotation = Main.rand.NextFloat(MathHelper.TwoPi);
            float scale = MathHelper.Clamp(Math.Max(target.width, target.height) / 52f, 0.85f, 2.4f);
            int projectileIndex = -1;
            if (Main.netMode == NetmodeID.Server)
            {
                SoulHarvest.BroadcastDeathDomainTelegraph(
                    Player,
                    target,
                    remainingFrames,
                    DeathDomainProgression.SlashCount,
                    visualMastery,
                    rotation,
                    scale);
            }
            else
            {
                projectileIndex = Projectile.NewProjectile(
                    Player.GetSource_Misc("SoulHarvest:DeathDomainTelegraph"),
                    target.Center,
                    Vector2.Zero,
                    ModContent.ProjectileType<DeathDomainHarvestTelegraphProjectile>(),
                    0,
                    0f,
                    Player.whoAmI,
                    target.whoAmI,
                    remainingFrames,
                    DeathDomainProgression.SlashCount + visualMastery * 0.1f);
                if (projectileIndex >= 0 && projectileIndex < Main.maxProjectiles)
                {
                    Projectile telegraph = Main.projectile[projectileIndex];
                    telegraph.rotation = rotation;
                    telegraph.scale = scale;
                }
            }
            deathDomainPendingCuts[target.whoAmI] = new PendingDeathDomainCut(rotation, projectileIndex);
        }
    }

    private void SpawnDeathDomainHarvestSlash(NPC target, float visualMastery,
        bool requiem, float rotation, int deathUltimateActionId = -1)
    {
        int slashDamage = CalculateDeathDomainSlashDamage(target, DeathDomainProgression);
        float scale = MathHelper.Clamp(Math.Max(target.width, target.height) / 52f, 0.85f, 2.4f);
        float visualState = visualMastery + (requiem ? 2f : 0f);

        if (Main.netMode == NetmodeID.Server)
        {
            for (int slash = 0; slash < DeathDomainProgression.SlashCount; slash++)
            {
                pendingDeathDomainStrikes.Add(new PendingDeathDomainStrike(
                    target.whoAmI,
                    DeathDomainHarvestSlashProjectile.SlashImpactFrame
                        + slash * DeathDomainHarvestSlashProjectile.SlashDelayFrames,
                    slashDamage,
                    DeathDomainProgression.LifeStealLevel,
                    deathUltimateActionId));
            }

            SoulHarvest.BroadcastDeathDomainHarvestSlash(
                Player,
                target,
                DeathDomainProgression.SlashCount,
                visualMastery,
                requiem,
                rotation,
                scale);
            return;
        }

        int projectileIndex = Projectile.NewProjectile(
            Player.GetSource_Misc("SoulHarvest:DeathDomain"),
            target.Center,
            Vector2.Zero,
            ModContent.ProjectileType<DeathDomainHarvestSlashProjectile>(),
            slashDamage,
            0f,
            Player.whoAmI,
            target.whoAmI,
            DeathDomainProgression.SlashCount,
            visualState);
        if (projectileIndex < 0 || projectileIndex >= Main.maxProjectiles)
            return;

        Projectile visual = Main.projectile[projectileIndex];
        visual.rotation = rotation;
        visual.scale = scale;
        visual.localAI[1] = DeathDomainProgression.LifeStealLevel;
        if (visual.ModProjectile is DeathDomainHarvestSlashProjectile harvest)
            harvest.SetDeathUltimateActionId(deathUltimateActionId);
    }

    private void UpdatePendingDeathDomainStrikes()
    {
        if (Main.netMode == NetmodeID.MultiplayerClient || pendingDeathDomainStrikes.Count == 0)
            return;

        for (int index = pendingDeathDomainStrikes.Count - 1; index >= 0; index--)
        {
            PendingDeathDomainStrike pending = pendingDeathDomainStrikes[index];
            int remainingFrames = pending.RemainingFrames - 1;
            if (remainingFrames > 0)
            {
                pendingDeathDomainStrikes[index] = pending with { RemainingFrames = remainingFrames };
                continue;
            }

            pendingDeathDomainStrikes.RemoveAt(index);
            ApplyDeathDomainStrike(pending.NpcIndex, pending.Damage,
                pending.LifeStealLevel, pending.DeathUltimateActionId);
        }
    }

    internal void ApplyDeathDomainStrike(int npcIndex, int damage,
        int lifeStealLevel, int deathUltimateActionId = -1)
    {
        if (npcIndex < 0 || npcIndex >= Main.maxNPCs)
            return;

        NPC target = Main.npc[npcIndex];
        if (!ReaperTargeting.IsValidWeaponTarget(target))
            return;

        bool bossOrBossPart = target.boss
            || NPCID.Sets.ShouldBeCountedAsBoss[target.type]
            || target.realLife >= 0 && target.realLife < Main.maxNPCs && Main.npc[target.realLife].boss;
        target.GetGlobalNPC<MyGlobalNPC>().ApplyDeathDomainHitStop(target, bossOrBossPart ? 1 : 3);

        if (Main.netMode == NetmodeID.MultiplayerClient || damage <= 0)
            return;

        target.GetGlobalNPC<MyGlobalNPC>().RegisterSickleHit(target, Player.whoAmI);
        target.lastInteraction = Player.whoAmI;
        NPC.HitInfo harvestHit = new()
        {
            Damage = damage,
            SourceDamage = damage,
            HitDirection = 0,
            Knockback = 0f,
            DamageType = DamageClass.Generic,
            Crit = false
        };
        int lifeBeforeHit = target.life;
        Vector2 lifeStealSource = target.Center;
        NPC accumulationTarget = target.realLife >= 0
            && target.realLife < Main.maxNPCs
            && Main.npc[target.realLife].active
                ? Main.npc[target.realLife]
                : target;
        target.StrikeNPC(harvestHit, fromNet: false, noPlayerInteraction: false);
        int actualDamage = Math.Max(0, lifeBeforeHit - Math.Max(0, target.life));
        if (deathUltimateActionId >= 0 && actualDamage > 0)
        {
            accumulationTarget.GetGlobalNPC<ReaperCombatGlobalNPC>()
                .RecordDeathUltimateCut(accumulationTarget, Player.whoAmI,
                    deathUltimateActionId, actualDamage);
        }
        ApplyDeathReaperLifeSteal(actualDamage, lifeStealSource, lifeStealLevel);
        if (Main.netMode == NetmodeID.Server)
        {
            NetMessage.SendStrikeNPC(target, in harvestHit);
            target.netUpdate = true;
        }
    }

    private void ClearDeathDomainTelegraphs()
    {
        if (deathDomainPendingCuts.Count == 0)
            return;

        foreach ((int npcIndex, PendingDeathDomainCut pending) in deathDomainPendingCuts)
        {
            KillDeathDomainTelegraph(pending.ProjectileIndex);
            SoulHarvest.BroadcastDeathDomainTelegraphClear(Player, npcIndex);
        }
        deathDomainPendingCuts.Clear();
    }

    private void RemoveDeathDomainTelegraph(int npcIndex)
    {
        if (!deathDomainPendingCuts.Remove(npcIndex, out PendingDeathDomainCut pending))
            return;
        KillDeathDomainTelegraph(pending.ProjectileIndex);
        SoulHarvest.BroadcastDeathDomainTelegraphClear(Player, npcIndex);
    }

    private void KillDeathDomainTelegraph(int projectileIndex)
    {
        if (projectileIndex < 0 || projectileIndex >= Main.maxProjectiles)
            return;

        Projectile projectile = Main.projectile[projectileIndex];
        if (projectile.active
            && projectile.owner == Player.whoAmI
            && projectile.ModProjectile is DeathDomainHarvestTelegraphProjectile)
        {
            projectile.Kill();
        }
    }

    private List<NPC> FindDeathDomainTargets(DeathNecklace necklace, bool descended)
    {
        List<NPC> targets = [];
        HashSet<int> selectedTargets = [];
        bool harvestEntireWorld = descended;
        float radiusSquared = necklace.DomainRadius * necklace.DomainRadius;
        foreach (NPC npc in Main.ActiveNPCs)
        {
            // chaseable is intentionally not used: vanilla NPCs toggle it during
            // movement and transition states, which made dedicated-server harvests
            // appear to skip otherwise valid enemies from one cycle to the next.
            // Training dummies are excluded from the passive necklace domain even
            // though weapon-created Death cuts can explicitly strike them.
            if (ReaperTargeting.IsTrainingDummy(npc) || npc.friendly || npc.immortal
                || npc.dontTakeDamage || npc.lifeMax <= 5 || npc.life <= 0)
                continue;

            NPC target = npc.realLife >= 0 && npc.realLife < Main.maxNPCs && Main.npc[npc.realLife].active
                ? Main.npc[npc.realLife]
                : npc;
            if (ReaperTargeting.IsTrainingDummy(target) || target.friendly
                || target.immortal || target.dontTakeDamage || target.lifeMax <= 5 || target.life <= 0
                || selectedTargets.Contains(target.whoAmI)
                || !harvestEntireWorld && Vector2.DistanceSquared(Player.Center, target.Center) > radiusSquared)
            {
                continue;
            }

            selectedTargets.Add(target.whoAmI);
            targets.Add(target);
        }
        return targets;
    }

    private void ApplyDeathDomainForces(DeathNecklace necklace, bool descended)
    {
        float innerRadius = descended ? 3200f : necklace.DomainRadius;
        float outerRadius = descended ? innerRadius : necklace.DomainAttractionRadius;
        float innerRadiusSquared = innerRadius * innerRadius;
        float outerRadiusSquared = outerRadius * outerRadius;
        float attractionWidth = Math.Max(1f, outerRadius - innerRadius);
        HashSet<int> affectedTargets = [];

        foreach (NPC npc in Main.ActiveNPCs)
        {
            if (ReaperTargeting.IsTrainingDummy(npc) || npc.friendly || npc.immortal
                || npc.dontTakeDamage || npc.lifeMax <= 5 || npc.life <= 0)
                continue;

            NPC target = npc.realLife >= 0 && npc.realLife < Main.maxNPCs && Main.npc[npc.realLife].active
                ? Main.npc[npc.realLife]
                : npc;
            if (ReaperTargeting.IsTrainingDummy(target) || target.friendly
                || target.immortal || target.dontTakeDamage || target.lifeMax <= 5 || target.life <= 0
                || !affectedTargets.Add(target.whoAmI))
            {
                continue;
            }

            Vector2 toDomain = Player.Center - target.Center;
            float distanceSquared = toDomain.LengthSquared();
            if (distanceSquared > outerRadiusSquared)
                continue;

            bool bossOrBossPart = target.boss
                || NPCID.Sets.ShouldBeCountedAsBoss[target.type]
                || target.realLife >= 0 && target.realLife < Main.maxNPCs && Main.npc[target.realLife].boss;
            float bossScale = bossOrBossPart ? 0.18f : 1f;
            float distance = (float)Math.Sqrt(distanceSquared);
            bool forceApplied;

            if (distanceSquared <= innerRadiusSquared)
            {
                // Inside the event horizon the flow reverses: enemies are expelled
                // from the pale-crimson core. The force reaches zero at the border
                // and rises quadratically toward the centre.
                Vector2 outward = distance > 0.001f
                    ? -toDomain / distance
                    : (target.whoAmI * 2.39996323f).ToRotationVector2();
                float centralInfluence = 1f - MathHelper.Clamp(distance / innerRadius, 0f, 1f);
                centralInfluence *= centralInfluence;
                float force = necklace.DomainRepulsionStrength * centralInfluence * bossScale;
                float maximumOutwardSpeed = necklace.DomainRepulsionMaximumSpeed * (bossOrBossPart ? 0.35f : 1f);
                float currentOutwardSpeed = Vector2.Dot(target.velocity, outward);
                float allowedForce = Math.Max(0f, maximumOutwardSpeed - currentOutwardSpeed);
                float appliedForce = Math.Min(force, allowedForce);
                target.velocity += outward * appliedForce;
                forceApplied = appliedForce > 0f;
            }
            else
            {
                // Outside the border the black-hole streams draw enemies inward.
                float normalizedDistance = MathHelper.Clamp((distance - innerRadius) / attractionWidth, 0f, 1f);
                float edgeInfluence = 1f - normalizedDistance;
                edgeInfluence = edgeInfluence * edgeInfluence * (3f - 2f * edgeInfluence);
                float force = necklace.DomainAttractionStrength * MathHelper.Lerp(0.22f, 1f, edgeInfluence) * bossScale;
                float maximumInwardSpeed = necklace.DomainAttractionMaximumSpeed * (bossOrBossPart ? 0.35f : 1f);
                Vector2 inward = toDomain / distance;
                float currentInwardSpeed = Vector2.Dot(target.velocity, inward);
                float allowedForce = Math.Max(0f, maximumInwardSpeed - currentInwardSpeed);
                float appliedForce = Math.Min(force, allowedForce);
                target.velocity += inward * appliedForce;
                forceApplied = appliedForce > 0f;
            }

            // Periodic authoritative velocity updates are enough for this gentle
            // force while avoiding an NPC sync packet on every tick.
            if (forceApplied && (Main.GameUpdateCount + (ulong)target.whoAmI) % 10UL == 0UL)
                target.netUpdate = true;
        }
    }

    private void ApplyDeathDomainItemAttraction()
    {
        // The domain owns the whole world while active. Move eligible drops in
        // world space on the authority instead of relying on vanilla pickup range;
        // this also lets coins and loot travel through terrain rather than becoming
        // trapped against a distant wall. Eligible drops are reserved to the
        // domain owner while the attraction is active.
        Vector2 destination = Player.MountedCenter;
        for (int itemIndex = 0; itemIndex < Main.maxItems; itemIndex++)
        {
            Item item = Main.item[itemIndex];
            if (!item.active || item.IsAir || item.stack <= 0
                || !ItemLoader.CanPickup(item, Player)
                || !Player.CanPullItem(item, Player.ItemSpace(item)))
            {
                continue;
            }

            // Item.Update continuously chooses a reservation owner. Without
            // claiming the drop here, that vanilla pass can immediately hand a
            // full-world attraction target to another nearby player and undo the
            // domain movement before its owner can collect it.
            item.playerIndexTheItemIsReservedFor = Player.whoAmI;

            Vector2 toPlayer = destination - item.Center;
            float distance = toPlayer.Length();
            if (distance <= 1f)
                continue;

            Vector2 direction = toPlayer / distance;
            float step = Math.Min(distance,
                MathHelper.Clamp(distance * 0.085f, 18f, 180f));
            item.position += direction * step;
            item.velocity = direction * Math.Min(24f, 8f + distance * 0.004f);
            item.noGrabDelay = 0;

            if (distance < 54f)
            {
                item.Center = destination;
                item.velocity = Vector2.Zero;
            }

            if (Main.netMode == NetmodeID.Server
                && (Main.GameUpdateCount + (ulong)itemIndex) % 6UL == 0UL)
            {
                NetMessage.SendData(MessageID.SyncItem, -1, -1, null, itemIndex);
            }
        }
    }

    private static int CalculateDeathDomainSlashDamage(NPC target, DeathDomainProgression progression)
    {
        bool bossOrBossPart = target.boss
            || NPCID.Sets.ShouldBeCountedAsBoss[target.type]
            || target.realLife >= 0 && target.realLife < Main.maxNPCs && Main.npc[target.realLife].boss;
        float lifeRatio = bossOrBossPart ? progression.BossSlashLifeRatio : progression.NormalSlashLifeRatio;
        return Math.Max(1, (int)Math.Ceiling(target.lifeMax * lifeRatio));
    }

    private void SyncSoulBalance()
    {
        if (Main.netMode != NetmodeID.Server
            || Player.whoAmI < 0
            || Player.whoAmI >= Netplay.Clients.Length
            || !Netplay.Clients[Player.whoAmI].IsActive)
        {
            return;
        }
        SyncPlayer(Player.whoAmI, -1, false);
    }
}