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