using SoulHarvest.Projectiles; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; using System.IO; using Terraria; using Terraria.ID; using Terraria.ModLoader; namespace SoulHarvest.Common; public sealed class ReaperCombatGlobalNPC : GlobalNPC { public override bool InstancePerEntity => true; private readonly byte[] boneMarks = new byte[Main.maxPlayers]; private readonly short[] boneTimers = new short[Main.maxPlayers]; private readonly short[] boneShatterCooldown = new short[Main.maxPlayers]; private readonly byte[] frostMarks = new byte[Main.maxPlayers]; private readonly short[] frostTimers = new short[Main.maxPlayers]; private readonly float[] frostSlowPerStack = new float[Main.maxPlayers]; private readonly byte[] soulStacks = new byte[Main.maxPlayers]; private readonly short[,] soulLayerTimers = new short[Main.maxPlayers, 7]; private readonly int[,] soulLayerDamage = new int[Main.maxPlayers, 7]; private readonly SickleCombatSnapshot[] soulSnapshots = new SickleCombatSnapshot[Main.maxPlayers]; private readonly bool[] hasSoulSnapshot = new bool[Main.maxPlayers]; private readonly int[] lastSoulFatedActionId = new int[Main.maxPlayers]; private readonly bool[] hasSoulFatedAction = new bool[Main.maxPlayers]; private readonly short[] soulShardCooldown = new short[Main.maxPlayers]; private readonly float[] lastVoidAngle = new float[Main.maxPlayers]; private readonly bool[] hasVoidAngle = new bool[Main.maxPlayers]; private readonly short[] voidAngleTimer = new short[Main.maxPlayers]; private readonly short[] voidIntersectionCooldown = new short[Main.maxPlayers]; private readonly short[] voidCoordinateTimer = new short[Main.maxPlayers]; private readonly int[] voidCoordinateSourceActionId = new int[Main.maxPlayers]; private readonly int[] voidEmpoweredActionId = new int[Main.maxPlayers]; private readonly short[] voidEmpoweredActionTimer = new short[Main.maxPlayers]; private readonly short[] infernalScorchVisualCooldown = new short[Main.maxPlayers]; private readonly int[] deathUltimateActionId = new int[Main.maxPlayers]; private readonly int[] deathUltimateAccumulatedDamage = new int[Main.maxPlayers]; private int boneSlowTimer; private float boneSlowFactor = 1f; private int boneDefenseTimer; private int boneDefenseReduction; private int soulSecondTimer; private int frostFreezeTimer; private Vector2 frostStoredVelocity; private short deathUltimateStopTimer; private Vector2 deathUltimateStoredVelocity; private bool deathWorldFreezeCaptured; private Vector2 deathWorldFreezeVelocity; private float deathWorldFreezeRotation; private Rectangle deathWorldFreezeFrame; private double deathWorldFreezeFrameCounter; public override void SetDefaults(NPC entity) { Array.Clear(boneMarks); Array.Clear(boneTimers); Array.Clear(boneShatterCooldown); Array.Clear(frostMarks); Array.Clear(frostTimers); Array.Clear(frostSlowPerStack); Array.Clear(soulStacks); Array.Clear(soulLayerTimers); Array.Clear(soulLayerDamage); Array.Clear(soulSnapshots); Array.Clear(hasSoulSnapshot); Array.Clear(lastSoulFatedActionId); Array.Clear(hasSoulFatedAction); Array.Clear(soulShardCooldown); Array.Clear(lastVoidAngle); Array.Clear(hasVoidAngle); Array.Clear(voidAngleTimer); Array.Clear(voidIntersectionCooldown); Array.Clear(voidCoordinateTimer); Array.Clear(voidCoordinateSourceActionId); Array.Clear(voidEmpoweredActionId); Array.Clear(voidEmpoweredActionTimer); Array.Clear(infernalScorchVisualCooldown); Array.Clear(deathUltimateActionId); Array.Clear(deathUltimateAccumulatedDamage); boneSlowTimer = 0; boneSlowFactor = 1f; boneDefenseTimer = 0; boneDefenseReduction = 0; soulSecondTimer = 0; frostFreezeTimer = 0; frostStoredVelocity = Vector2.Zero; deathUltimateStopTimer = 0; deathUltimateStoredVelocity = Vector2.Zero; deathWorldFreezeCaptured = false; deathWorldFreezeVelocity = Vector2.Zero; deathWorldFreezeRotation = 0f; deathWorldFreezeFrame = Rectangle.Empty; deathWorldFreezeFrameCounter = 0d; } public override bool PreAI(NPC npc) { if (ReaperDeathUltimateFreezeSystem.IsWorldFrozen) { if (!deathWorldFreezeCaptured) { deathWorldFreezeCaptured = true; deathWorldFreezeVelocity = npc.velocity; deathWorldFreezeRotation = npc.rotation; deathWorldFreezeFrame = npc.frame; deathWorldFreezeFrameCounter = npc.frameCounter; if (Main.netMode != NetmodeID.MultiplayerClient) npc.netUpdate = true; } deathUltimateStopTimer = 0; deathUltimateStoredVelocity = Vector2.Zero; npc.velocity = Vector2.Zero; npc.rotation = deathWorldFreezeRotation; npc.frame = deathWorldFreezeFrame; npc.frameCounter = deathWorldFreezeFrameCounter; return false; } if (deathWorldFreezeCaptured) { npc.velocity = deathWorldFreezeVelocity; deathWorldFreezeCaptured = false; deathWorldFreezeVelocity = Vector2.Zero; if (Main.netMode != NetmodeID.MultiplayerClient) npc.netUpdate = true; } bool bossLike = IsBossLike(npc); if (deathUltimateStopTimer > 0) { deathUltimateStopTimer--; if (bossLike) { npc.velocity *= 0.08f; } else { npc.velocity = Vector2.Zero; if (deathUltimateStopTimer == 0) npc.velocity = deathUltimateStoredVelocity * 0.18f; return false; } } if (frostFreezeTimer > 0 && !bossLike) { frostFreezeTimer--; npc.velocity = Vector2.Zero; if (frostFreezeTimer == 0) npc.velocity = frostStoredVelocity * 0.25f; return false; } if (boneSlowTimer > 0 && !bossLike) { boneSlowTimer--; npc.velocity *= boneSlowFactor; } if (!bossLike) { float strongestSlow = 0f; for (int index = 0; index < frostMarks.Length; index++) strongestSlow = Math.Max(strongestSlow, frostMarks[index] * frostSlowPerStack[index]); if (strongestSlow > 0f) npc.velocity *= Math.Max(0.55f, 1f - strongestSlow); } return true; } public override void FindFrame(NPC npc, int frameHeight) { if (!deathWorldFreezeCaptured || !ReaperDeathUltimateFreezeSystem.IsWorldFrozen) { return; } npc.frame = deathWorldFreezeFrame; npc.frameCounter = deathWorldFreezeFrameCounter; } internal void RecordDeathUltimateCut(NPC npc, int owner, int actionId, int damageDone) { if (owner < 0 || owner >= Main.maxPlayers || damageDone <= 0) return; if (deathUltimateActionId[owner] != actionId) { deathUltimateActionId[owner] = actionId; deathUltimateAccumulatedDamage[owner] = 0; } deathUltimateAccumulatedDamage[owner] = (int)Math.Min(int.MaxValue / 4L, deathUltimateAccumulatedDamage[owner] + (long)damageDone); if (ReaperDeathUltimateFreezeSystem.IsWorldFrozen) { npc.velocity = Vector2.Zero; npc.netUpdate = true; return; } if (deathUltimateStopTimer <= 0) deathUltimateStoredVelocity = npc.velocity; deathUltimateStopTimer = Math.Max(deathUltimateStopTimer, (short)180); npc.velocity *= IsBossLike(npc) ? 0.08f : 0f; npc.netUpdate = true; } internal bool IsTrackedByDeathUltimate(int owner, int actionId) => owner >= 0 && owner < Main.maxPlayers && actionId >= 0 && deathUltimateActionId[owner] == actionId && deathUltimateAccumulatedDamage[owner] > 0; internal int ConsumeDeathUltimateDamage(NPC npc, int owner, int actionId) { if (owner < 0 || owner >= Main.maxPlayers || deathUltimateActionId[owner] != actionId) { return 0; } int damage = deathUltimateAccumulatedDamage[owner]; deathUltimateActionId[owner] = 0; deathUltimateAccumulatedDamage[owner] = 0; deathUltimateStopTimer = 0; if (!IsBossLike(npc)) npc.velocity = deathUltimateStoredVelocity * 0.18f; return damage; } public override void PostAI(NPC npc) { if (Main.netMode == NetmodeID.MultiplayerClient) return; if (boneDefenseTimer > 0) boneDefenseTimer--; for (int playerIndex = 0; playerIndex < Main.maxPlayers; playerIndex++) { Tick(ref boneTimers[playerIndex], ref boneMarks[playerIndex]); if (boneShatterCooldown[playerIndex] > 0) boneShatterCooldown[playerIndex]--; Tick(ref frostTimers[playerIndex], ref frostMarks[playerIndex]); TickSoulLayers(npc, playerIndex); if (soulShardCooldown[playerIndex] > 0) soulShardCooldown[playerIndex]--; if (voidIntersectionCooldown[playerIndex] > 0) voidIntersectionCooldown[playerIndex]--; if (voidAngleTimer[playerIndex] > 0) voidAngleTimer[playerIndex]--; else hasVoidAngle[playerIndex] = false; if (voidCoordinateTimer[playerIndex] > 0) voidCoordinateTimer[playerIndex]--; if (voidEmpoweredActionTimer[playerIndex] > 0) voidEmpoweredActionTimer[playerIndex]--; else voidEmpoweredActionId[playerIndex] = 0; if (infernalScorchVisualCooldown[playerIndex] > 0) infernalScorchVisualCooldown[playerIndex]--; } if (++soulSecondTimer < 60) return; soulSecondTimer = 0; for (int playerIndex = 0; playerIndex < Main.maxPlayers && npc.active; playerIndex++) { int stacks = soulStacks[playerIndex]; int damage = 0; for (int layer = 0; layer < stacks; layer++) damage += soulLayerDamage[playerIndex, layer]; if (stacks <= 0 || damage <= 0 || !Main.player[playerIndex].active) continue; NPC.HitInfo hit = new() { Damage = damage, SourceDamage = damage, Knockback = 0f, HitDirection = 0, Crit = false, DamageType = DamageClass.Melee }; npc.GetGlobalNPC().RegisterSickleHit(npc, playerIndex); npc.StrikeNPC(hit, fromNet: false, noPlayerInteraction: false); if (Main.netMode == NetmodeID.Server) NetMessage.SendStrikeNPC(npc, in hit); } } public override void ModifyIncomingHit(NPC npc, ref NPC.HitModifiers modifiers) { ReaperCombatGlobalNPC defenseSource = this; if (npc.realLife >= 0 && npc.realLife < Main.maxNPCs && Main.npc[npc.realLife].active) defenseSource = Main.npc[npc.realLife].GetGlobalNPC(); if (defenseSource.boneDefenseTimer > 0) modifiers.Defense.Flat -= defenseSource.boneDefenseReduction; } internal void AddBoneMarks(NPC npc, Player player, in SickleCombatSnapshot snapshot, int amount, int actionId) { int owner = player.whoAmI; int duration = (snapshot.Skill1 switch { 2 => 6, >= 3 => 7, _ => 5 }) * 60; boneTimers[owner] = (short)duration; boneMarks[owner] = (byte)Math.Min(3, boneMarks[owner] + Math.Max(1, amount)); if (boneMarks[owner] < 3 || boneShatterCooldown[owner] > 0) return; boneMarks[owner] = 0; boneShatterCooldown[owner] = 45; float factor = snapshot.Skill1 switch { 2 => 0.55f, >= 3 => 0.7f, _ => 0.4f }; ReaperStrikeProjectile.Spawn(npc.GetSource_FromAI(), owner, snapshot, ReaperHitKind.PrimaryDerived, 0, npc.Center, Vector2.UnitX, ReaperStrikeShape.Circle, Math.Max(46f, npc.width), 20f, factor, 1); ReaperCombatPlayer combatPlayer = player.GetModPlayer(); if (combatPlayer.TryReserveSkillCue(ReaperFormId.Bone, 0, actionId)) { ReaperSkillCueProjectile.Spawn(npc.GetSource_FromAI(), owner, snapshot, 0, npc.Center, Vector2.UnitY * -1f, actionId: actionId); } int controlLevel = snapshot.Skill2; if (controlLevel > 0) { if (IsBossLike(npc)) { boneDefenseReduction = Math.Max(boneDefenseReduction, controlLevel switch { 2 => 10, >= 3 => 14, _ => 6 }); boneDefenseTimer = Math.Max(boneDefenseTimer, 180); } else { boneSlowFactor = 1f - (controlLevel switch { 2 => 0.35f, >= 3 => 0.45f, _ => 0.25f }); boneSlowTimer = Math.Max(boneSlowTimer, 180); } if (combatPlayer.TryReserveSkillCue(ReaperFormId.Bone, 1, actionId)) { ReaperSkillCueProjectile.Spawn(npc.GetSource_FromAI(), owner, snapshot, 1, npc.Center, Vector2.UnitY * -1f, actionId: actionId); } } if (snapshot.Skill3 > 0) { float echo = snapshot.Skill3 switch { 2 => 0.3f, >= 3 => 0.4f, _ => 0.2f }; ReaperStrikeProjectile.Spawn(npc.GetSource_FromAI(), owner, snapshot, ReaperHitKind.PrimaryDerived, 0, npc.Center, Vector2.UnitY, ReaperStrikeShape.Cross, 82f, 18f, echo, 12); if (combatPlayer.TryReserveSkillCue(ReaperFormId.Bone, 2, actionId)) { ReaperSkillCueProjectile.Spawn(npc.GetSource_FromAI(), owner, snapshot, 2, npc.Center, Vector2.UnitY, actionId: actionId); } } } internal void AddFrostMark(NPC npc, Player player, in SickleCombatSnapshot snapshot, bool shatter, int actionId) { int owner = player.whoAmI; frostMarks[owner] = (byte)Math.Min(4, frostMarks[owner] + 1); frostTimers[owner] = (short)((snapshot.Skill1 switch { 2 => 5, >= 3 => 6, _ => 4 }) * 60); float slowPerStack = snapshot.Skill1 switch { 2 => 0.06f, >= 3 => 0.08f, _ => 0.04f }; frostSlowPerStack[owner] = slowPerStack; if (!shatter || frostMarks[owner] < 4) return; ShatterFrost(npc, player, snapshot, actionId); } internal bool HasFullFrostMarks(int owner) => owner >= 0 && owner < Main.maxPlayers && frostMarks[owner] >= 4; internal void ShatterFrost(NPC npc, Player player, in SickleCombatSnapshot snapshot, int actionId, bool canChargeUltimate = true) { int owner = player.whoAmI; if (frostMarks[owner] < 4) return; frostMarks[owner] = 0; float factor = snapshot.Skill1 switch { 2 => 0.7f, >= 3 => 0.9f, _ => 0.5f }; ReaperHitKind hitKind = canChargeUltimate ? ReaperHitKind.PrimaryDerived : ReaperHitKind.UltimateDerived; ReaperStrikeProjectile.Spawn(npc.GetSource_FromAI(), owner, snapshot, hitKind, 0, npc.Center, Vector2.UnitX, ReaperStrikeShape.Cross, 94f, 20f, factor, 1); if (player.GetModPlayer() .TryReserveSkillCue(ReaperFormId.Frost, 0, actionId)) { ReaperSkillCueProjectile.Spawn(npc.GetSource_FromAI(), owner, snapshot, 0, npc.Center, Vector2.UnitX, actionId: actionId); } } internal void AddFatedSoulStack(NPC npc, Player player, in SickleCombatSnapshot snapshot, int actionId, int amount) { int owner = player.whoAmI; if (hasSoulFatedAction[owner] && lastSoulFatedActionId[owner] == actionId) return; hasSoulFatedAction[owner] = true; lastSoulFatedActionId[owner] = actionId; int maximum = snapshot.Skill3 switch { 2 => 5, >= 3 => 7, _ => 3 }; short duration = (short)((snapshot.Skill3 switch { 2 => 5, >= 3 => 6, _ => 4 }) * 60); float factor = snapshot.Skill3 switch { 2 => 0.10f, >= 3 => 0.12f, _ => 0.08f }; int contribution = Math.Max(1, (int)Math.Round(snapshot.Damage * factor)); soulSnapshots[owner] = snapshot; hasSoulSnapshot[owner] = true; int current = soulStacks[owner]; for (int layer = 0; layer < current; layer++) soulLayerTimers[owner, layer] = Math.Max(soulLayerTimers[owner, layer], duration); int toAdd = Math.Min(Math.Max(0, amount), maximum - current); for (int layer = 0; layer < toAdd; layer++) { soulLayerDamage[owner, current + layer] = contribution; soulLayerTimers[owner, current + layer] = duration; } soulStacks[owner] = (byte)(current + toAdd); bool reachedMaximum = toAdd > 0 && soulStacks[owner] >= maximum; if (toAdd > 0 && !reachedMaximum && snapshot.Skill3 > 0 && player.GetModPlayer() .TryReserveSkillCue(ReaperFormId.Soul, 2, actionId)) { ReaperSkillCueProjectile.Spawn(npc.GetSource_FromAI(), owner, snapshot, 2, npc.Center, Vector2.UnitY * -1f, actionId: actionId); } if (reachedMaximum) ExecuteSoulFate(npc, owner, snapshot, actionId, expired: false); } internal bool TryTakeSoulShard(int owner) { if (owner < 0 || owner >= Main.maxPlayers || soulShardCooldown[owner] > 0) return false; soulShardCooldown[owner] = 60; return true; } internal void RefundSoulShardCooldown(int owner) { if (owner >= 0 && owner < Main.maxPlayers) soulShardCooldown[owner] = 0; } internal void ApplyFrostFreeze(NPC npc, int frames) { if (frames <= 0) return; if (IsBossLike(npc)) { npc.GetGlobalNPC().ApplyDeathDomainHitStop(npc, 2); return; } if (frostFreezeTimer <= 0) frostStoredVelocity = npc.velocity; frostFreezeTimer = Math.Max(frostFreezeTimer, frames); npc.velocity = Vector2.Zero; npc.netUpdate = true; } internal void RecordVoidCut(NPC npc, Player player, in SickleCombatSnapshot snapshot, float angle, int actionId) { int owner = player.whoAmI; if (snapshot.Skill1 > 0 && hasVoidAngle[owner] && voidIntersectionCooldown[owner] <= 0 && AngleDifference(lastVoidAngle[owner], angle) >= MathHelper.Pi / 3f) { int level = Math.Clamp(snapshot.Skill1, (byte)1, (byte)3); int rifts = level; float totalFactor = level switch { 2 => 0.24f, >= 3 => 0.34f, _ => 0.16f }; Vector2 baseAxis = angle.ToRotationVector2(); for (int index = 0; index < rifts; index++) { float centered = index - (rifts - 1) * 0.5f; Vector2 axis = baseAxis.RotatedBy(centered * 0.48f); ReaperVoidRiftProjectile.Spawn(npc.GetSource_FromAI(), owner, snapshot, npc.Center, axis, 122f + level * 30f, 8f + level * 2.5f, ReaperVoidArcRiftProjectile.GetLifetime(level), 145f + level * 38f, totalFactor / rifts, index, actionId); } ReaperCombatPlayer combatPlayer = player.GetModPlayer(); if (combatPlayer.TryReserveSkillCue(ReaperFormId.Void, 0, actionId)) { ReaperSkillCueProjectile.Spawn(npc.GetSource_FromAI(), owner, snapshot, 0, npc.Center, angle.ToRotationVector2(), actionId: actionId); } voidIntersectionCooldown[owner] = 75; if (snapshot.Skill3 > 0) { voidCoordinateTimer[owner] = (short)((snapshot.Skill3 switch { 2 => 5, >= 3 => 6, _ => 4 }) * 60); voidCoordinateSourceActionId[owner] = actionId; if (combatPlayer.TryReserveSkillCue(ReaperFormId.Void, 2, actionId)) { ReaperSkillCueProjectile.Spawn(npc.GetSource_FromAI(), owner, snapshot, 2, npc.Center, angle.ToRotationVector2(), actionId: actionId); } } } lastVoidAngle[owner] = angle; hasVoidAngle[owner] = true; voidAngleTimer[owner] = 120; } internal void ModifyReaperHit(int owner, in SickleCombatSnapshot snapshot, ReaperHitKind kind, int phase, int actionId, NPC npc, ref NPC.HitModifiers modifiers) { if (snapshot.Form == ReaperFormId.Infernal && snapshot.Skill2 > 0 && npc.HasBuff(BuffID.OnFire3)) { modifiers.FinalDamage *= 1f + (snapshot.Skill2 switch { 2 => 0.07f, >= 3 => 0.10f, _ => 0.04f }); ReaperCombatPlayer combatPlayer = Main.player[owner].GetModPlayer(); bool executed = Main.netMode != NetmodeID.MultiplayerClient && (kind is ReaperHitKind.Primary or ReaperHitKind.Special) && combatPlayer.TryTriggerInfernalScorchExecution(snapshot, npc, actionId); if (!executed && Main.netMode != NetmodeID.MultiplayerClient && infernalScorchVisualCooldown[owner] <= 0 && combatPlayer.TryReserveSkillCue(ReaperFormId.Infernal, 1, actionId)) { ReaperSkillCueProjectile.Spawn(npc.GetSource_FromAI(), owner, snapshot, 1, npc.Center, Vector2.UnitY * -1f, actionId: actionId); infernalScorchVisualCooldown[owner] = (short)(snapshot.Skill2 switch { 2 => 34, >= 3 => 26, _ => 42 }); } } if (snapshot.Form == ReaperFormId.Frost && snapshot.Skill3 > 0 && kind == ReaperHitKind.Special && phase == 2 && HasFullFrostMarks(owner)) modifiers.FinalDamage *= 1f + (snapshot.Skill3 switch { 2 => 0.12f, >= 3 => 0.16f, _ => 0.08f }); if (snapshot.Form == ReaperFormId.Void && snapshot.Skill3 > 0 && kind is ReaperHitKind.Special or ReaperHitKind.Ultimate) { if (voidEmpoweredActionId[owner] != actionId && voidCoordinateTimer[owner] > 0 && voidCoordinateSourceActionId[owner] != actionId) { voidCoordinateTimer[owner] = 0; voidEmpoweredActionId[owner] = actionId; voidEmpoweredActionTimer[owner] = 240; if (Main.netMode != NetmodeID.MultiplayerClient && Main.player[owner].GetModPlayer() .TryReserveSkillCue(ReaperFormId.Void, 2, actionId)) { ReaperSkillCueProjectile.Spawn(npc.GetSource_FromAI(), owner, snapshot, 2, npc.Center, Vector2.UnitX, variant: 1, actionId: actionId); } } if (voidEmpoweredActionTimer[owner] > 0 && voidEmpoweredActionId[owner] == actionId) { modifiers.FinalDamage *= 1f + (snapshot.Skill3 switch { 2 => 0.12f, >= 3 => 0.16f, _ => 0.08f }); modifiers.ScalingArmorPenetration += snapshot.Skill3 switch { 2 => 0.10f, >= 3 => 0.15f, _ => 0.05f }; } } } private static void Tick(ref short timer, ref byte stacks) { if (timer > 0) timer--; if (timer <= 0) stacks = 0; } private void TickSoulLayers(NPC npc, int owner) { int count = soulStacks[owner]; int storedDamage = 0; int write = 0; for (int layer = 0; layer < count; layer++) { storedDamage += soulLayerDamage[owner, layer]; short timer = soulLayerTimers[owner, layer]; if (timer > 0) timer--; if (timer <= 0) continue; soulLayerTimers[owner, write] = timer; soulLayerDamage[owner, write] = soulLayerDamage[owner, layer]; write++; } for (int layer = write; layer < count; layer++) { soulLayerTimers[owner, layer] = 0; soulLayerDamage[owner, layer] = 0; } soulStacks[owner] = (byte)write; if (count > 0 && write == 0 && hasSoulSnapshot[owner]) ExecuteSoulFate(npc, owner, soulSnapshots[owner], lastSoulFatedActionId[owner], expired: true, storedDamage); } private void ExecuteSoulFate(NPC npc, int owner, in SickleCombatSnapshot snapshot, int actionId, bool expired, int storedDamage = -1) { if (owner < 0 || owner >= Main.maxPlayers || !Main.player[owner].active) { ClearSoulFate(owner); return; } if (storedDamage < 0) { storedDamage = 0; int stacks = soulStacks[owner]; for (int layer = 0; layer < stacks; layer++) storedDamage += soulLayerDamage[owner, layer]; } // Expiry cashes out only what actually remained. A full inscription gets // the stronger execution beat, while both paths remain one area strike. float baseFactor = snapshot.Skill3 switch { 2 => 0.38f, >= 3 => 0.54f, _ => 0.26f }; float storedFactor = snapshot.Damage > 0 ? storedDamage / (float)snapshot.Damage * 0.45f : 0f; float factor = Math.Max(baseFactor * (expired ? 0.72f : 1f), storedFactor); factor = Math.Min(factor, snapshot.Skill3 >= 3 ? 0.72f : 0.52f); ClearSoulFate(owner); int executionActionId = (actionId & 0x3FFFFFFF) ^ 0x40000000; Vector2 direction = (npc.Center - Main.player[owner].MountedCenter) .SafeNormalize(new Vector2(Main.player[owner].direction, 0f)); ReaperStrikeProjectile.Spawn(npc.GetSource_FromAI(), owner, snapshot, ReaperHitKind.PrimaryDerived, 0, npc.Center, direction, snapshot.Skill3 >= 2 ? ReaperStrikeShape.Cross : ReaperStrikeShape.Line, 92f + snapshot.Skill3 * 18f, 22f + snapshot.Skill3 * 3f, factor, expired ? 2 : 8, executionActionId); if (Main.player[owner].GetModPlayer() .TryReserveSkillCue(ReaperFormId.Soul, 2, executionActionId)) { ReaperSkillCueProjectile.Spawn(npc.GetSource_FromAI(), owner, snapshot, 2, npc.Center, direction, variant: 1, actionId: executionActionId); } } private void ClearSoulFate(int owner) { if (owner < 0 || owner >= Main.maxPlayers) return; int count = soulStacks[owner]; for (int layer = 0; layer < count; layer++) { soulLayerTimers[owner, layer] = 0; soulLayerDamage[owner, layer] = 0; } soulStacks[owner] = 0; soulSnapshots[owner] = default; hasSoulSnapshot[owner] = false; } private static float AngleDifference(float first, float second) { float difference = Math.Abs(MathHelper.WrapAngle(first - second)); return Math.Min(difference, MathHelper.TwoPi - difference); } private static bool IsBossLike(NPC npc) { return npc.boss || NPCID.Sets.ShouldBeCountedAsBoss[npc.type] || npc.realLife >= 0 && npc.realLife < Main.maxNPCs && Main.npc[npc.realLife].active && (Main.npc[npc.realLife].boss || NPCID.Sets.ShouldBeCountedAsBoss[Main.npc[npc.realLife].type]); } }