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 ReaperCombatPlayer : ModPlayer
{
    private int bloodShield;
    private int bloodShieldIdle;
    private uint bloodShieldRevision;
    private int pendingMirroredBloodShieldAbsorption;
    private int pendingMirroredBloodShieldTimer;
    private int bloodHealingThisSecond;
    private int bloodHealingWindow;
    private int infernalMomentumTimer;
    private int ultimateTimer;
    private ReaperFormId ultimateForm;
    private bool bloodFrenzyLatched;
    private int lastBloodFinisherIdentity = -1;
    private int bloodFinisherTargets;
    private int deathUltimateHealing;
    private byte infernalSpecialMask;
    private int infernalSpecialMaskTimer;
    private int lastSoulActionId;
    private readonly HashSet<int> soulActionTargets = [];
    private int bloodDrinkVisualCooldown;
    private int bloodShieldVisualCooldown;
    private int bloodFrenzyVisualRefresh;
    private int bloodShieldCounterCooldown;
    private int infernalScorchExecutionCooldown;
    private int infernalMomentumVisualCooldown;
    private SickleCombatSnapshot bloodShieldSnapshot;
    private bool hasBloodShieldSnapshot;
    private readonly int[,] lastSkillCueAction = new int[8, ReaperDefinitions.SkillsPerBranch];
    private readonly ulong[,] lastSkillCueTick = new ulong[8, ReaperDefinitions.SkillsPerBranch];
    private readonly bool[,] hasSkillCueAction = new bool[8, ReaperDefinitions.SkillsPerBranch];
    private readonly byte[,] skillCueCenterSamples = new byte[8, ReaperDefinitions.SkillsPerBranch];

    public bool BloodFrenzyActive => bloodFrenzyLatched;
    internal int BloodShieldAmount => bloodShield;
    internal bool FreezesWorldForDeathUltimate
    {
        get
        {
            if (ultimateForm != ReaperFormId.Death || ultimateTimer <= 0)
                return false;
            int elapsed = ReaperDeathUltimateGeometry.Duration - ultimateTimer;
            return elapsed >= ReaperDeathUltimateGeometry.FirstCutTick
                && elapsed <= ReaperDeathUltimateGeometry.ShatterTick;
        }
    }

    public override void Initialize()
    {
        ClearRuntime();
    }

    internal void ClearRuntime()
    {
        bloodShield = 0;
        bloodShieldIdle = 0;
        if (Main.netMode == NetmodeID.Server)
            AdvanceBloodShieldRevision();
        else
            bloodShieldRevision = 0;
        pendingMirroredBloodShieldAbsorption = 0;
        pendingMirroredBloodShieldTimer = 0;
        bloodHealingThisSecond = 0;
        bloodHealingWindow = 0;
        infernalMomentumTimer = 0;
        ultimateTimer = 0;
        ultimateForm = ReaperFormId.Base;
        bloodFrenzyLatched = false;
        lastBloodFinisherIdentity = -1;
        bloodFinisherTargets = 0;
        deathUltimateHealing = 0;
        infernalSpecialMask = 0;
        infernalSpecialMaskTimer = 0;
        lastSoulActionId = int.MinValue;
        soulActionTargets.Clear();
        bloodDrinkVisualCooldown = 0;
        bloodShieldVisualCooldown = 0;
        bloodFrenzyVisualRefresh = 0;
        bloodShieldCounterCooldown = 0;
        infernalScorchExecutionCooldown = 0;
        infernalMomentumVisualCooldown = 0;
        bloodShieldSnapshot = default;
        hasBloodShieldSnapshot = false;
        Array.Clear(lastSkillCueAction);
        Array.Clear(lastSkillCueTick);
        Array.Clear(hasSkillCueAction);
        Array.Clear(skillCueCenterSamples);
    }

    internal void WriteOwnerRuntime(BinaryWriter writer)
    {
        writer.Write((ushort)Math.Clamp(bloodShield, 0, ushort.MaxValue));
        writer.Write((ushort)Math.Clamp(bloodShieldIdle, 0, ushort.MaxValue));
        writer.Write(bloodShieldRevision);
        writer.Write((ushort)Math.Clamp(infernalMomentumTimer, 0, ushort.MaxValue));
        writer.Write((ushort)Math.Clamp(ultimateTimer, 0, ushort.MaxValue));
        writer.Write((byte)ultimateForm);
    }

    internal void ReceiveOwnerRuntime(BinaryReader reader)
    {
        bloodShield = reader.ReadUInt16();
        bloodShieldIdle = reader.ReadUInt16();
        bloodShieldRevision = reader.ReadUInt32();
        infernalMomentumTimer = reader.ReadUInt16();
        ultimateTimer = reader.ReadUInt16();
        ReaperFormId receivedForm = (ReaperFormId)reader.ReadByte();
        ultimateForm = Enum.IsDefined(receivedForm) ? receivedForm : ReaperFormId.Base;
    }

    public override void PostUpdate()
    {
        if (++bloodHealingWindow >= 60)
        {
            bloodHealingWindow = 0;
            bloodHealingThisSecond = 0;
        }
        if (bloodShield > 0 && ++bloodShieldIdle >= 300)
        {
            ReleaseBloodShield(Vector2.UnitY * -1f);
            if (Main.netMode == NetmodeID.Server)
            {
                AdvanceBloodShieldRevision();
                Player.GetModPlayer<MyPlayer>().SyncReaperCombatRuntime();
            }
        }
        if (pendingMirroredBloodShieldTimer > 0)
            pendingMirroredBloodShieldTimer--;
        else
            pendingMirroredBloodShieldAbsorption = 0;
        if (infernalMomentumTimer > 0)
            infernalMomentumTimer--;
        if (ultimateTimer > 0)
            ultimateTimer--;
        if (bloodDrinkVisualCooldown > 0)
            bloodDrinkVisualCooldown--;
        if (bloodShieldVisualCooldown > 0)
            bloodShieldVisualCooldown--;
        if (bloodFrenzyVisualRefresh > 0)
            bloodFrenzyVisualRefresh--;
        if (bloodShieldCounterCooldown > 0)
            bloodShieldCounterCooldown--;
        if (infernalScorchExecutionCooldown > 0)
            infernalScorchExecutionCooldown--;
        if (infernalMomentumVisualCooldown > 0)
            infernalMomentumVisualCooldown--;
        ReaperProgressionState state = Player.GetModPlayer<MyPlayer>().ReaperProgression;
        bool frenzyWasActive = bloodFrenzyLatched;
        if (state.CurrentForm != ReaperFormId.Blood || Player.statLife <= 0)
            bloodFrenzyLatched = false;
        else if (!bloodFrenzyLatched && Player.statLife * 2 < Player.statLifeMax2)
            bloodFrenzyLatched = true;
        else if (bloodFrenzyLatched && Player.statLife * 100 >= Player.statLifeMax2 * 55)
            bloodFrenzyLatched = false;
        if (bloodFrenzyLatched && Main.netMode != NetmodeID.MultiplayerClient
            && (!frenzyWasActive || bloodFrenzyVisualRefresh <= 0))
        {
            int level = state.GetSkillLevel(ReaperFormId.Blood, 1);
            if (level > 0)
            {
                ReaperSkillCueProjectile.Spawn(Player.GetSource_Misc("SoulHarvest:BloodFrenzyVisual"),
                    Player.whoAmI, ReaperFormId.Blood, state.GetStage(ReaperFormId.Blood),
                    1, level, Player.MountedCenter, Vector2.UnitY * -1f);
                bloodFrenzyVisualRefresh = 90;
            }
        }
        else if (frenzyWasActive && !bloodFrenzyLatched
            && Main.netMode != NetmodeID.MultiplayerClient)
        {
            EmitCurrentBloodVisual(1, Player.MountedCenter, Vector2.UnitY * -1f, variant: 1);
            bloodFrenzyVisualRefresh = 0;
        }
        if (infernalSpecialMaskTimer > 0)
            infernalSpecialMaskTimer--;
        else
            infernalSpecialMask = 0;
    }

    public override void PostUpdateRunSpeeds()
    {
        if (ultimateTimer > 0 && ultimateForm == ReaperFormId.Blood)
        {
            Player.moveSpeed *= 0.6f;
            Player.maxRunSpeed *= 0.6f;
            Player.accRunSpeed *= 0.6f;
            Player.runAcceleration *= 0.6f;
        }

        if (infernalMomentumTimer <= 0)
            return;
        ReaperProgressionState state = Player.GetModPlayer<MyPlayer>().ReaperProgression;
        if (state.CurrentForm != ReaperFormId.Infernal)
            return;
        int level = state.GetSkillLevel(ReaperFormId.Infernal, 0);
        float bonus = level switch { 2 => 0.12f, >= 3 => 0.16f, _ => 0.08f };
        Player.moveSpeed += bonus;
        Player.maxRunSpeed *= 1f + bonus;
    }

    public override void ModifyHurt(ref Player.HurtModifiers modifiers)
    {
        if (ultimateTimer > 0)
        {
            modifiers.FinalDamage *= ultimateForm == ReaperFormId.Death ? 0.20f : 0.30f;
            modifiers.Knockback *= 0f;
        }

        if (bloodShield > 0
            || Main.netMode == NetmodeID.Server && pendingMirroredBloodShieldAbsorption > 0)
            modifiers.ModifyHurtInfo += ConsumeBloodShield;
    }

    private void ConsumeBloodShield(ref Player.HurtInfo info)
    {
        if (info.Damage <= 0)
            return;
        if (Main.netMode == NetmodeID.Server && pendingMirroredBloodShieldAbsorption > 0)
        {
            // If the owner's revisioned report arrived before this server-side Hurt
            // hook, its delta already changed the authoritative shield. Mirror only
            // the damage reduction here instead of charging that shield a second time.
            int mirrored = Math.Min(pendingMirroredBloodShieldAbsorption, info.Damage);
            pendingMirroredBloodShieldAbsorption -= mirrored;
            if (pendingMirroredBloodShieldAbsorption <= 0)
                pendingMirroredBloodShieldTimer = 0;
            info.Damage -= mirrored;
        }
        if (bloodShield <= 0 || info.Damage <= 0)
            return;
        int absorbed = Math.Min(bloodShield, info.Damage);
        uint consumedRevision = bloodShieldRevision;
        bloodShield -= absorbed;
        info.Damage -= absorbed;
        if (Main.netMode != NetmodeID.Server)
            CombatText.NewText(Player.Hitbox, new Color(245, 70, 125), absorbed);
        if (Main.netMode == NetmodeID.MultiplayerClient)
        {
            SoulHarvest.SendBloodShieldConsumption(Player, consumedRevision, absorbed);
            AdvanceBloodShieldRevision();
        }
        else if (Main.netMode == NetmodeID.Server)
        {
            AdvanceBloodShieldRevision();
            Player.GetModPlayer<MyPlayer>().SyncReaperCombatRuntime();
        }
        if (Main.netMode != NetmodeID.MultiplayerClient)
            OnBloodShieldAbsorbed(absorbed, ResolveCounterDirection(info.HitDirection));
    }

    internal void AcceptOwnerBloodShieldConsumption(uint revision, int absorbed)
    {
        if (Main.netMode != NetmodeID.Server)
            return;
        if (revision == bloodShieldRevision && absorbed > 0)
        {
            int previousShield = bloodShield;
            bloodShield = Math.Max(0, bloodShield - absorbed);
            if (bloodShield != previousShield)
            {
                int acceptedAbsorption = previousShield - bloodShield;
                pendingMirroredBloodShieldAbsorption = Math.Min(
                    Math.Max(1, Player.statLifeMax2),
                    pendingMirroredBloodShieldAbsorption + acceptedAbsorption);
                pendingMirroredBloodShieldTimer = 15;
                AdvanceBloodShieldRevision();
                OnBloodShieldAbsorbed(acceptedAbsorption, new Vector2(Player.direction, 0f));
            }
        }

        // Always answer, including stale/malformed revisions, so the owner cannot
        // retain a predicted shield state that the server rejected.
        Player.GetModPlayer<MyPlayer>().SyncReaperCombatRuntime();
    }

    internal void ApplyBloodHealing(in SickleCombatSnapshot snapshot, int damageDone, Vector2 visualSource)
    {
        int level = snapshot.Skill1;
        if (level <= 0 || damageDone <= 0 || Player.dead)
            return;
        float rate = level switch { 2 => 0.03f, >= 3 => 0.04f, _ => 0.02f };
        int secondCap = Math.Max(1, (int)(Player.statLifeMax2 * (level switch { 2 => 0.06f, >= 3 => 0.08f, _ => 0.04f })));
        int potential = Math.Max(1, (int)Math.Ceiling(damageDone * rate));
        int permitted = Math.Min(potential, Math.Max(0, secondCap - bloodHealingThisSecond));
        int missing = Math.Max(0, Player.statLifeMax2 - Player.statLife);
        int healed = Math.Min(missing, permitted);
        if (healed > 0)
        {
            Player.Heal(healed);
            bloodHealingThisSecond += healed;
            // Character-owned Blood Covenant healing bypasses the legacy item
            // lifesteal path, so it must explicitly create the returning blood-soul
            // feedback on the authoritative side. The projectile is then synced to
            // clients like every other combat visual.
            LifeStealVisuals.Spawn(visualSource, Player.whoAmI, level, healed);
            if (bloodDrinkVisualCooldown <= 0 && Main.netMode != NetmodeID.MultiplayerClient)
            {
                ReaperSkillCueProjectile.Spawn(Player.GetSource_Misc("SoulHarvest:BloodDrinkVisual"),
                    Player.whoAmI, snapshot, 0, visualSource,
                    (Player.MountedCenter - visualSource).SafeNormalize(Vector2.UnitY * -1f));
                bloodDrinkVisualCooldown = 10;
            }
        }

        if (snapshot.Skill3 <= 0 || permitted <= healed)
            return;
        float shieldRatio = snapshot.Skill3 switch { 2 => 0.06f, >= 3 => 0.08f, _ => 0.04f };
        int shieldCap = Math.Max(1, (int)(Player.statLifeMax2 * shieldRatio));
        int previousShield = bloodShield;
        bloodShield = Math.Min(shieldCap, bloodShield + permitted - healed);
        if (bloodShield != previousShield)
        {
            bloodShieldSnapshot = snapshot;
            hasBloodShieldSnapshot = true;
            bloodShieldIdle = 0;
            LifeStealVisuals.Spawn(visualSource, Player.whoAmI, level, bloodShield - previousShield);
            if (Main.netMode == NetmodeID.Server)
            {
                AdvanceBloodShieldRevision();
                Player.GetModPlayer<MyPlayer>().SyncReaperCombatRuntime();
            }
            if (bloodShieldVisualCooldown <= 0 && Main.netMode != NetmodeID.MultiplayerClient)
            {
                ReaperSkillCueProjectile.Spawn(Player.GetSource_Misc("SoulHarvest:BloodShieldVisual"),
                    Player.whoAmI, snapshot, 2, Player.MountedCenter, Vector2.UnitY * -1f);
                bloodShieldVisualCooldown = 18;
            }
        }
    }

    private void OnBloodShieldAbsorbed(int absorbed, Vector2 direction)
    {
        if (absorbed <= 0 || !hasBloodShieldSnapshot)
            return;

        direction = direction.SafeNormalize(new Vector2(Player.direction, 0f));
        int level = bloodShieldSnapshot.Skill3;
        if (level >= 2 && bloodShieldCounterCooldown <= 0
            && (bloodShield > 0 || level == 2))
        {
            bloodShieldCounterCooldown = 42;
            float counterFactor = level >= 3 ? 0.20f : 0.16f;
            ReaperStrikeProjectile.Spawn(Player.GetSource_Misc("SoulHarvest:BloodShieldCounter"),
                Player.whoAmI, bloodShieldSnapshot, ReaperHitKind.PrimaryDerived, 0,
                Player.MountedCenter + direction * 12f, direction, ReaperStrikeShape.Line,
                level >= 3 ? 142f : 118f, level >= 3 ? 31f : 25f, counterFactor, 3);
            ReaperSkillCueProjectile.Spawn(Player.GetSource_Misc("SoulHarvest:BloodShieldCounterVisual"),
                Player.whoAmI, bloodShieldSnapshot, 2, Player.MountedCenter, direction, variant: 2);
        }

        if (bloodShield <= 0)
            ReleaseBloodShield(direction);
    }

    private void ReleaseBloodShield(Vector2 direction)
    {
        SickleCombatSnapshot releasedSnapshot = bloodShieldSnapshot;
        bool canExecute = Main.netMode != NetmodeID.MultiplayerClient
            && hasBloodShieldSnapshot
            && releasedSnapshot.Skill3 >= 3;
        bloodShield = 0;
        bloodShieldIdle = 0;
        hasBloodShieldSnapshot = false;
        bloodShieldSnapshot = default;

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

        direction = direction.SafeNormalize(Vector2.UnitY * -1f);
        if (canExecute)
        {
            // The cocoon's final ring is one bounded area hit, not twelve radial
            // projectiles.  This keeps the shield's defensive budget predictable.
            ReaperStrikeProjectile.Spawn(Player.GetSource_Misc("SoulHarvest:BloodShieldRelease"),
                Player.whoAmI, releasedSnapshot, ReaperHitKind.PrimaryDerived, 0,
                Player.MountedCenter, direction, ReaperStrikeShape.Circle,
                112f, 30f, 0.32f, 2);
            ReaperSkillCueProjectile.Spawn(Player.GetSource_Misc("SoulHarvest:BloodShieldReleaseVisual"),
                Player.whoAmI, releasedSnapshot, 2, Player.MountedCenter, direction, variant: 1);
        }
        else
        {
            EmitCurrentBloodVisual(2, Player.MountedCenter, direction, variant: 1);
        }
    }

    private Vector2 ResolveCounterDirection(int hitDirection)
    {
        int direction = hitDirection == 0 ? Player.direction : hitDirection;
        return new Vector2(direction == 0 ? 1 : direction, 0f);
    }

    internal bool TryTriggerInfernalScorchExecution(in SickleCombatSnapshot snapshot, NPC target, int actionId)
    {
        if (Main.netMode == NetmodeID.MultiplayerClient
            || snapshot.Skill2 < 3
            || infernalScorchExecutionCooldown > 0
            || !target.active)
        {
            return false;
        }

        infernalScorchExecutionCooldown = 75;
        Vector2 direction = (target.Center - Player.MountedCenter)
            .SafeNormalize(new Vector2(Player.direction, 0f));
        ReaperStrikeProjectile.Spawn(target.GetSource_FromAI(), Player.whoAmI, snapshot,
            ReaperHitKind.PrimaryDerived, 0, target.Center, direction,
            ReaperStrikeShape.Circle, Math.Max(88f, target.width * 0.75f), 28f,
            0.42f, 9, actionId);
        if (TryReserveSkillCue(ReaperFormId.Infernal, 1, actionId))
        {
            ReaperSkillCueProjectile.Spawn(target.GetSource_FromAI(), Player.whoAmI, snapshot,
                1, target.Center, direction, variant: 1, actionId: actionId);
        }
        return true;
    }

    internal bool TryReserveSkillCue(ReaperFormId form, int skillIndex, int actionId)
    {
        int formIndex = (int)form;
        if (formIndex < 0 || formIndex >= lastSkillCueAction.GetLength(0)
            || skillIndex < 0 || skillIndex >= ReaperDefinitions.SkillsPerBranch)
        {
            return false;
        }

        ulong now = Main.GameUpdateCount;
        if (hasSkillCueAction[formIndex, skillIndex]
            && lastSkillCueAction[formIndex, skillIndex] == actionId
            && now - lastSkillCueTick[formIndex, skillIndex] <= 300UL)
        {
            // Same-tick multi-target hits may send a handful of lightweight
            // center samples. Clients merge them into the one existing visual,
            // producing an actual group focus instead of using whichever NPC the
            // server happened to enumerate first. Later ticks remain suppressed.
            if (now != lastSkillCueTick[formIndex, skillIndex]
                || skillCueCenterSamples[formIndex, skillIndex] >= 12)
            {
                return false;
            }
            skillCueCenterSamples[formIndex, skillIndex]++;
            return true;
        }

        hasSkillCueAction[formIndex, skillIndex] = true;
        lastSkillCueAction[formIndex, skillIndex] = actionId;
        lastSkillCueTick[formIndex, skillIndex] = now;
        skillCueCenterSamples[formIndex, skillIndex] = 1;
        return true;
    }

    private void AdvanceBloodShieldRevision()
    {
        bloodShieldRevision++;
        if (bloodShieldRevision == 0)
            bloodShieldRevision++;
    }

    internal void ApplyDeathBloodHealing(int damageDone)
    {
        if (damageDone <= 0 || Player.dead)
            return;
        int amount = Math.Min(Math.Max(0, Player.statLifeMax2 - Player.statLife),
            Math.Min(Math.Max(1, damageDone / 100), Math.Max(1, Player.statLifeMax2 / 100)));
        if (amount > 0)
            Player.Heal(amount);
    }

    internal void ApplyBloodFinisherHealing(int projectileIdentity, Vector2 visualSource, int visualLevel)
    {
        if (projectileIdentity != lastBloodFinisherIdentity)
        {
            lastBloodFinisherIdentity = projectileIdentity;
            bloodFinisherTargets = 0;
        }
        if (bloodFinisherTargets >= 3 || Player.dead)
            return;
        bloodFinisherTargets++;
        int amount = Math.Max(1, (int)Math.Ceiling(Player.statLifeMax2 * 0.0075f));
        amount = Math.Min(amount, Math.Max(0, Player.statLifeMax2 - Player.statLife));
        if (amount <= 0)
            return;
        Player.Heal(amount);
        LifeStealVisuals.Spawn(visualSource, Player.whoAmI, Math.Max(1, visualLevel), amount);
    }

    internal void ApplyDeathUltimateHealing(int damageDone)
    {
        if (ultimateForm != ReaperFormId.Death || ultimateTimer <= 0 || damageDone <= 0 || Player.dead)
            return;
        int cap = Math.Max(1, (int)Math.Ceiling(Player.statLifeMax2 * 0.08f));
        int amount = Math.Min(Math.Max(0, cap - deathUltimateHealing), Math.Max(1, damageDone / 100));
        amount = Math.Min(amount, Math.Max(0, Player.statLifeMax2 - Player.statLife));
        if (amount <= 0)
            return;
        Player.Heal(amount);
        deathUltimateHealing += amount;
    }

    internal void RecordInfernalSpecialHit(in SickleCombatSnapshot snapshot, int phase, Projectile source)
    {
        if (snapshot.Stage < ReaperStage.StageIII || snapshot.Skill3 <= 0 || phase < 0 || phase > 2)
            return;
        infernalSpecialMask |= (byte)(1 << phase);
        infernalSpecialMaskTimer = 90;
        if (infernalSpecialMask != 0x07)
            return;
        infernalSpecialMask = 0;
        float factor = snapshot.Skill3 switch { 2 => 0.75f, >= 3 => 1f, _ => 0.5f };
        int refund = snapshot.Skill3 switch { 2 => 75, >= 3 => 105, _ => 45 };
        ReaperStrikeProjectile.Spawn(source.GetSource_FromThis(), Player.whoAmI, snapshot,
            ReaperHitKind.PrimaryDerived, phase, Player.Center, Vector2.UnitX,
            ReaperStrikeShape.Circle, 125f, 32f, factor, 1);
        ReaperSkillCueProjectile.Spawn(source.GetSource_FromThis(), Player.whoAmI, snapshot,
            2, Player.MountedCenter, source.velocity.SafeNormalize(Vector2.UnitX), actionId: source.identity);
        Player.GetModPlayer<MyPlayer>().ReduceReaperSpecialCooldown(refund);
    }

    internal bool TryRecordSoulShard(int actionId, int targetRoot)
    {
        if (actionId != lastSoulActionId)
        {
            lastSoulActionId = actionId;
            soulActionTargets.Clear();
        }
        if (soulActionTargets.Count >= 2 || !soulActionTargets.Add(targetRoot))
            return false;
        return true;
    }

    internal void RollbackSoulShardReservation(int actionId, int targetRoot)
    {
        if (actionId == lastSoulActionId)
            soulActionTargets.Remove(targetRoot);
    }

    internal void GrantInfernalMomentum()
    {
        bool wasInactive = infernalMomentumTimer <= 0;
        infernalMomentumTimer = 120;
        if ((wasInactive || infernalMomentumVisualCooldown <= 0) && Main.netMode != NetmodeID.MultiplayerClient)
        {
            ReaperProgressionState state = Player.GetModPlayer<MyPlayer>().ReaperProgression;
            int level = state.GetSkillLevel(ReaperFormId.Infernal, 0);
            if (level > 0)
            {
                ReaperSkillCueProjectile.Spawn(Player.GetSource_Misc("SoulHarvest:InfernalMomentumVisual"),
                    Player.whoAmI, ReaperFormId.Infernal, state.GetStage(ReaperFormId.Infernal),
                    0, level, Player.MountedCenter, new Vector2(Player.direction, 0f));
                infernalMomentumVisualCooldown = 18;
            }
        }
        if (Main.netMode == NetmodeID.Server)
            Player.GetModPlayer<MyPlayer>().SyncReaperCombatRuntime();
    }

    internal void BeginUltimate(ReaperFormId form)
    {
        ultimateForm = form;
        deathUltimateHealing = 0;
        ultimateTimer = form switch
        {
            ReaperFormId.Bone => 216,
            ReaperFormId.Blood => 210,
            ReaperFormId.Infernal => 204,
            ReaperFormId.Frost => 120,
            ReaperFormId.Soul => 116,
            ReaperFormId.Void => 220,
            ReaperFormId.Death => ReaperDeathUltimateGeometry.Duration,
            _ => 0
        };
    }

    private void EmitCurrentBloodVisual(int skillIndex, Vector2 center, Vector2 direction, int variant = 0)
    {
        ReaperProgressionState state = Player.GetModPlayer<MyPlayer>().ReaperProgression;
        int level = state.GetSkillLevel(ReaperFormId.Blood, skillIndex);
        if (level <= 0)
            return;
        ReaperSkillCueProjectile.Spawn(Player.GetSource_Misc("SoulHarvest:BloodSkillVisual"),
            Player.whoAmI, ReaperFormId.Blood, state.GetStage(ReaperFormId.Blood),
            skillIndex, level, center, direction, variant);
    }
}