using SoulHarvest.Buffs;
using SoulHarvest.Items;
using SoulHarvest.Projectiles;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using System;
using System.IO;
using Terraria;
using Terraria.GameContent;
using Terraria.ID;
using Terraria.ModLoader;
using Terraria.ModLoader.IO;
namespace SoulHarvest.Common;
public class MyGlobalNPC : GlobalNPC
{
internal const int DeathMarkDuration = 60 * 60 * 10;
internal const int AbsoluteMaximumFatedStacks = NormalSickle.MaxFatedStackCapacity;
public override bool InstancePerEntity => true;
private readonly bool[] sickleParticipants = new bool[Main.maxPlayers];
private readonly int[] fatedStackTimers = new int[AbsoluteMaximumFatedStacks];
private readonly int[] fatedStackOwners = new int[AbsoluteMaximumFatedStacks];
private readonly int[] fatedStackLifeStealLevels = new int[AbsoluteMaximumFatedStacks];
private int lastSicklePlayer = -1;
private int fatedSecondTimer;
private bool deathMarked;
private int deathDomainHitStopFrames;
private Vector2 deathDomainStoredVelocity;
public int FatedStackCount { get; private set; }
public override void SetDefaults(NPC npc)
{
Array.Clear(sickleParticipants);
Array.Clear(fatedStackTimers);
Array.Fill(fatedStackOwners, -1);
Array.Clear(fatedStackLifeStealLevels);
lastSicklePlayer = -1;
fatedSecondTimer = 0;
deathMarked = false;
deathDomainHitStopFrames = 0;
deathDomainStoredVelocity = Vector2.Zero;
FatedStackCount = 0;
}
public override bool PreAI(NPC npc)
{
if (deathDomainHitStopFrames <= 0)
return true;
deathDomainHitStopFrames--;
npc.velocity = Vector2.Zero;
if (deathDomainHitStopFrames <= 0)
{
// Restore only part of the previous movement so the pause reads as an
// impact without launching fast enemies immediately out of the blade.
npc.velocity = deathDomainStoredVelocity * 0.35f;
deathDomainStoredVelocity = Vector2.Zero;
if (Main.netMode != NetmodeID.MultiplayerClient)
npc.netUpdate = true;
}
return false;
}
public override void ModifyHitByItem(NPC npc, Player player, Item item, ref NPC.HitModifiers modifiers)
{
if (item.ModItem is not (NormalSickle or LegacyDeath))
return;
if (item.ModItem is NormalSickle sickle)
modifiers.ScalingArmorPenetration += sickle.ArmorPenetrationPercent;
if (Main.netMode == NetmodeID.MultiplayerClient)
{
return;
}
// The server records the reaper before damage is applied. This avoids a one-shot kill
// racing ahead of the client's early registration packet.
RegisterSickleHit(npc, player.whoAmI);
}
public override void OnHitByItem(NPC npc, Player player, Item item, NPC.HitInfo hit, int damageDone)
{
if (item.ModItem is NormalSickle or LegacyDeath)
RegisterSickleHit(npc, player.whoAmI);
}
public override void OnHitByProjectile(NPC npc, Projectile projectile, NPC.HitInfo hit, int damageDone)
{
if (projectile.GetGlobalProjectile<MyGlobalProjectile>().IsSickleProjectile)
RegisterSickleHit(npc, projectile.owner);
}
public override void PostAI(NPC npc)
{
if (Main.netMode != NetmodeID.MultiplayerClient && lastSicklePlayer >= 0)
npc.AddBuff(ModContent.BuffType<DeathMarkBuff>(), 2);
UpdateFatedState(npc);
SpawnStatusParticles(npc);
}
public override void PostDraw(NPC npc, SpriteBatch spriteBatch, Vector2 screenPos, Color drawColor)
{
if (!deathMarked)
return;
Texture2D markTexture = TextureAssets.Buff[ModContent.BuffType<DeathMarkBuff>()].Value;
float pulse = 0.52f + (float)Math.Sin(Main.GlobalTimeWrappedHourly * 5f + npc.whoAmI) * 0.035f;
Vector2 position = new Vector2(npc.Center.X, npc.Top.Y - 13f) - screenPos;
Color auraColor = FatedStackCount > 0 ? new Color(235, 55, 150, 0) : new Color(65, 235, 255, 0);
for (int index = 0; index < 4; index++)
{
Vector2 offset = (Main.GlobalTimeWrappedHourly * 2f + index * MathHelper.PiOver2).ToRotationVector2() * 2f;
spriteBatch.Draw(markTexture, position + offset, null, auraColor * 0.32f, 0f, markTexture.Size() / 2f, pulse * 1.12f, SpriteEffects.None, 0f);
}
spriteBatch.Draw(markTexture, position, null, Color.White, 0f, markTexture.Size() / 2f, pulse, SpriteEffects.None, 0f);
if (FatedStackCount <= 0)
return;
Texture2D fatedTexture = TextureAssets.Buff[ModContent.BuffType<FatedBuff>()].Value;
Vector2 fatedPosition = position + new Vector2(18f, 1f);
spriteBatch.Draw(fatedTexture, fatedPosition, null, Color.White, 0f, fatedTexture.Size() / 2f, 0.44f, SpriteEffects.None, 0f);
Utils.DrawBorderStringFourWay(spriteBatch, FontAssets.ItemStack.Value, FatedStackCount.ToString(), fatedPosition.X + 5f, fatedPosition.Y + 3f, new Color(255, 95, 180), Color.Black, Vector2.Zero, 0.58f);
}
public override void SendExtraAI(NPC npc, BitWriter bitWriter, BinaryWriter binaryWriter)
{
bitWriter.WriteBit(deathMarked);
binaryWriter.Write((byte)FatedStackCount);
}
public override void ReceiveExtraAI(NPC npc, BitReader bitReader, BinaryReader binaryReader)
{
deathMarked = bitReader.ReadBit();
FatedStackCount = Math.Clamp((int)binaryReader.ReadByte(), 0, AbsoluteMaximumFatedStacks);
}
public override void OnKill(NPC npc)
{
if (Main.netMode == NetmodeID.MultiplayerClient || npc.friendly || npc.lifeMax <= 5)
return;
// Dungeon Guardians and Paladins are exceptional normal enemies: their unusual
// health values should not distort the regular curve, and they always cap it.
if (IsFixedMaximumSoulEnemy(npc.type))
{
if (lastSicklePlayer >= 0 && lastSicklePlayer < Main.maxPlayers && Main.player[lastSicklePlayer].active)
Main.player[lastSicklePlayer].GetModPlayer<MyPlayer>().AddSouls(50);
return;
}
if (TryGetBossParticipants(npc, out bool[] bossParticipants, out int encounterLifeMax))
{
int reward = CalculateSoulReward(encounterLifeMax, boss: true);
for (int playerIndex = 0; playerIndex < Main.maxPlayers; playerIndex++)
{
if (bossParticipants[playerIndex] && Main.player[playerIndex].active)
Main.player[playerIndex].GetModPlayer<MyPlayer>().AddSouls(reward, announce: true);
}
return;
}
if (IsCompositeBossPart(npc.type))
return;
if (npc.realLife >= 0 && npc.realLife < Main.maxNPCs && Main.npc[npc.realLife].boss)
return;
if (!npc.boss && lastSicklePlayer >= 0 && lastSicklePlayer < Main.maxPlayers && Main.player[lastSicklePlayer].active)
Main.player[lastSicklePlayer].GetModPlayer<MyPlayer>().AddSouls(CalculateSoulReward(npc.lifeMax, boss: false));
}
internal void RegisterSickleHit(NPC npc, int playerIndex, bool applyDeathMark = true)
{
if (Main.netMode == NetmodeID.MultiplayerClient
|| playerIndex < 0
|| playerIndex >= Main.maxPlayers
|| npc.friendly)
return;
if (applyDeathMark)
MarkForReaping(npc);
sickleParticipants[playerIndex] = true;
lastSicklePlayer = playerIndex;
BossSoulParticipationSystem.RecordCompositeBossHit(npc, playerIndex);
if (npc.realLife >= 0 && npc.realLife < Main.maxNPCs && npc.realLife != npc.whoAmI)
{
MyGlobalNPC root = Main.npc[npc.realLife].GetGlobalNPC<MyGlobalNPC>();
root.sickleParticipants[playerIndex] = true;
root.lastSicklePlayer = playerIndex;
}
}
#if DEBUG
internal bool HasSickleHarvester(int playerIndex)
=> playerIndex >= 0
&& playerIndex < Main.maxPlayers
&& sickleParticipants[playerIndex]
&& lastSicklePlayer == playerIndex;
#endif
internal void AddFatedStacks(NPC npc, int amount, int maximumStacks, int durationFrames, int playerIndex, int lifeStealLevel)
{
if (amount <= 0 || Main.netMode == NetmodeID.MultiplayerClient || npc.friendly || npc.lifeMax <= 5)
return;
maximumStacks = Math.Clamp(maximumStacks, 1, AbsoluteMaximumFatedStacks);
durationFrames = Math.Clamp(durationFrames, 60, ushort.MaxValue);
int stacksToAdd = Math.Min(amount, Math.Max(0, maximumStacks - FatedStackCount));
for (int index = 0; index < stacksToAdd; index++)
{
fatedStackTimers[FatedStackCount] = durationFrames;
fatedStackOwners[FatedStackCount] = playerIndex is >= 0 and < Main.maxPlayers ? playerIndex : -1;
fatedStackLifeStealLevels[FatedStackCount] = Math.Max(0, lifeStealLevel);
FatedStackCount++;
}
for (int index = 0; index < FatedStackCount; index++)
fatedStackTimers[index] = Math.Max(fatedStackTimers[index], durationFrames);
npc.AddBuff(ModContent.BuffType<FatedBuff>(), durationFrames);
npc.netUpdate = true;
}
internal void MarkForReaping(NPC npc)
{
bool newlyMarked = !deathMarked;
deathMarked = true;
npc.AddBuff(ModContent.BuffType<DeathMarkBuff>(), DeathMarkDuration);
SyncBuffState(npc);
if (newlyMarked)
SoulHarvest.BroadcastDeathMark(npc, marked: true);
}
internal void ApplyDeathDomainHitStop(NPC npc, int frames)
{
frames = Math.Clamp(frames, 1, 4);
if (deathDomainHitStopFrames <= 0)
deathDomainStoredVelocity = npc.velocity;
deathDomainHitStopFrames = Math.Max(deathDomainHitStopFrames, frames);
npc.velocity = Vector2.Zero;
if (Main.netMode != NetmodeID.MultiplayerClient)
npc.netUpdate = true;
}
internal void ReceiveDeathMarkState(bool marked)
{
deathMarked = marked;
}
internal static void SyncBuffState(NPC npc)
{
npc.netUpdate = true;
if (Main.netMode == NetmodeID.Server)
NetMessage.SendData(MessageID.NPCBuffs, -1, -1, null, npc.whoAmI);
}
private void UpdateFatedState(NPC npc)
{
if (Main.netMode == NetmodeID.MultiplayerClient || FatedStackCount <= 0)
return;
int writeIndex = 0;
for (int index = 0; index < FatedStackCount; index++)
{
if (--fatedStackTimers[index] > 0)
{
fatedStackTimers[writeIndex] = fatedStackTimers[index];
fatedStackOwners[writeIndex] = fatedStackOwners[index];
fatedStackLifeStealLevels[writeIndex] = fatedStackLifeStealLevels[index];
writeIndex++;
}
}
if (writeIndex != FatedStackCount)
{
FatedStackCount = writeIndex;
npc.netUpdate = true;
}
if (FatedStackCount <= 0)
{
fatedSecondTimer = 0;
return;
}
npc.AddBuff(ModContent.BuffType<FatedBuff>(), 2);
if (++fatedSecondTimer < 60)
return;
fatedSecondTimer = 0;
bool bossOrBossPart = npc.boss || npc.realLife >= 0 && npc.realLife < Main.maxNPCs && Main.npc[npc.realLife].boss;
float ratePerStack = bossOrBossPart ? 0.01f : 1f / 3f;
int damage = Math.Max(1, (int)Math.Ceiling(npc.lifeMax * ratePerStack * FatedStackCount));
int lifeBeforeHit = npc.life;
Vector2 lifeStealSource = npc.Center;
int[] stackCountsByPlayer = new int[Main.maxPlayers];
int[] lifeStealLevelByPlayer = new int[Main.maxPlayers];
for (int index = 0; index < FatedStackCount; index++)
{
int owner = fatedStackOwners[index];
if (owner < 0 || owner >= Main.maxPlayers)
continue;
stackCountsByPlayer[owner]++;
lifeStealLevelByPlayer[owner] = Math.Max(lifeStealLevelByPlayer[owner], fatedStackLifeStealLevels[index]);
}
NPC.HitInfo fateHit = new()
{
Damage = damage,
SourceDamage = damage,
HitDirection = 0,
Knockback = 0f,
DamageType = DamageClass.Generic,
Crit = false
};
npc.StrikeNPC(fateHit, fromNet: false, noPlayerInteraction: false);
if (Main.netMode == NetmodeID.Server)
NetMessage.SendStrikeNPC(npc, in fateHit);
int actualDamage = Math.Min(Math.Max(0, lifeBeforeHit), damage);
for (int playerIndex = 0; playerIndex < Main.maxPlayers; playerIndex++)
{
int stackCount = stackCountsByPlayer[playerIndex];
int lifeStealLevel = lifeStealLevelByPlayer[playerIndex];
if (stackCount <= 0 || lifeStealLevel <= 0 || !Main.player[playerIndex].active || Main.player[playerIndex].dead)
continue;
int attributedDamage = Math.Max(1, (int)Math.Round(actualDamage * stackCount / (double)FatedStackCount));
int healedLife = Main.player[playerIndex].GetModPlayer<MyPlayer>().TryLifeSteal(attributedDamage, lifeStealLevel);
LifeStealVisuals.Spawn(lifeStealSource, playerIndex, lifeStealLevel, healedLife);
}
}
private void SpawnStatusParticles(NPC npc)
{
if (Main.netMode == NetmodeID.Server || !npc.active)
return;
if (deathMarked && Main.rand.NextBool(4))
{
float angle = Main.GlobalTimeWrappedHourly * 3.1f + npc.whoAmI * 0.73f + Main.rand.NextFloat(-0.35f, 0.35f);
Vector2 orbit = new Vector2(Math.Max(12f, npc.width * 0.58f), Math.Max(9f, npc.height * 0.36f));
Vector2 position = npc.Center + new Vector2((float)Math.Cos(angle) * orbit.X, (float)Math.Sin(angle) * orbit.Y);
Dust markDust = Dust.NewDustPerfect(position, DustID.DungeonSpirit, -npc.velocity * 0.08f + Vector2.UnitY * -0.25f, 80, new Color(70, 230, 255), 0.72f);
markDust.noGravity = true;
}
if (FatedStackCount <= 0)
return;
int particleCount = Main.rand.NextBool(2) ? Math.Min(2, FatedStackCount) : 1;
for (int index = 0; index < particleCount; index++)
{
Vector2 position = npc.Center + Main.rand.NextVector2CircularEdge(npc.width * 0.62f + 8f, npc.height * 0.5f + 8f);
Vector2 velocity = (npc.Center - position).SafeNormalize(Vector2.Zero) * Main.rand.NextFloat(0.45f, 1.2f);
Dust fateDust = Dust.NewDustPerfect(position, DustID.Shadowflame, velocity, 55, new Color(245, 45, 145), 0.9f + FatedStackCount * 0.08f);
fateDust.noGravity = true;
}
}
internal static int CalculateSoulReward(int lifeMax, bool boss)
{
lifeMax = Math.Max(1, lifeMax);
if (boss)
return Math.Clamp((int)Math.Ceiling(1.3d * Math.Sqrt(lifeMax)), 1, 500);
double normalizedLife = lifeMax / 25d;
return Math.Clamp((int)Math.Ceiling(Math.Pow(normalizedLife, 0.55d)), 1, 50);
}
private bool TryGetBossParticipants(NPC npc, out bool[] participants, out int encounterLifeMax)
{
if (BossSoulParticipationSystem.TryTakeCompositeParticipants(npc, out participants, out encounterLifeMax))
return true;
if (IsCompositeBossPart(npc.type))
{
encounterLifeMax = 0;
return false;
}
if (!npc.boss)
{
encounterLifeMax = 0;
return false;
}
if (npc.realLife >= 0 && npc.realLife < Main.maxNPCs && npc.realLife != npc.whoAmI && Main.npc[npc.realLife].active)
{
encounterLifeMax = 0;
return false;
}
if (npc.realLife >= 0 && npc.realLife < Main.maxNPCs)
{
participants = (bool[])Main.npc[npc.realLife].GetGlobalNPC<MyGlobalNPC>().sickleParticipants.Clone();
encounterLifeMax = BossSoulParticipationSystem.GetEncounterLifeMax(npc);
return true;
}
participants = (bool[])sickleParticipants.Clone();
encounterLifeMax = BossSoulParticipationSystem.GetEncounterLifeMax(npc);
return true;
}
private static bool IsCompositeBossPart(int npcType)
{
return npcType is NPCID.Retinazer
or NPCID.Spazmatism
or NPCID.EaterofWorldsBody
or NPCID.EaterofWorldsHead
or NPCID.EaterofWorldsTail
or NPCID.Creeper
or NPCID.GolemHead
or NPCID.GolemHeadFree
or NPCID.MoonLordHead
or NPCID.MoonLordHand;
}
private static bool IsFixedMaximumSoulEnemy(int npcType)
{
return npcType is NPCID.DungeonGuardian or NPCID.Paladin;
}
}
using SoulHarvest.Buffs;
using SoulHarvest.Items;
using SoulHarvest.Projectiles;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using System;
using System.IO;
using Terraria;
using Terraria.GameContent;
using Terraria.ID;
using Terraria.ModLoader;
using Terraria.ModLoader.IO;
namespace SoulHarvest.Common;
public class MyGlobalNPC : GlobalNPC
{
internal const int DeathMarkDuration = 60 * 60 * 10;
internal const int AbsoluteMaximumFatedStacks = NormalSickle.MaxFatedStackCapacity;
public override bool InstancePerEntity => true;
private readonly bool[] sickleParticipants = new bool[Main.maxPlayers];
private readonly int[] fatedStackTimers = new int[AbsoluteMaximumFatedStacks];
private readonly int[] fatedStackOwners = new int[AbsoluteMaximumFatedStacks];
private readonly int[] fatedStackLifeStealLevels = new int[AbsoluteMaximumFatedStacks];
private int lastSicklePlayer = -1;
private int fatedSecondTimer;
private bool deathMarked;
private int deathDomainHitStopFrames;
private Vector2 deathDomainStoredVelocity;
public int FatedStackCount { get; private set; }
public override void SetDefaults(NPC npc)
{
Array.Clear(sickleParticipants);
Array.Clear(fatedStackTimers);
Array.Fill(fatedStackOwners, -1);
Array.Clear(fatedStackLifeStealLevels);
lastSicklePlayer = -1;
fatedSecondTimer = 0;
deathMarked = false;
deathDomainHitStopFrames = 0;
deathDomainStoredVelocity = Vector2.Zero;
FatedStackCount = 0;
}
public override bool PreAI(NPC npc)
{
if (deathDomainHitStopFrames <= 0)
return true;
deathDomainHitStopFrames--;
npc.velocity = Vector2.Zero;
if (deathDomainHitStopFrames <= 0)
{
// Restore only part of the previous movement so the pause reads as an
// impact without launching fast enemies immediately out of the blade.
npc.velocity = deathDomainStoredVelocity * 0.35f;
deathDomainStoredVelocity = Vector2.Zero;
if (Main.netMode != NetmodeID.MultiplayerClient)
npc.netUpdate = true;
}
return false;
}
public override void ModifyHitByItem(NPC npc, Player player, Item item, ref NPC.HitModifiers modifiers)
{
if (item.ModItem is not (NormalSickle or LegacyDeath))
return;
if (item.ModItem is NormalSickle sickle)
modifiers.ScalingArmorPenetration += sickle.ArmorPenetrationPercent;
if (Main.netMode == NetmodeID.MultiplayerClient)
{
return;
}
// The server records the reaper before damage is applied. This avoids a one-shot kill
// racing ahead of the client's early registration packet.
RegisterSickleHit(npc, player.whoAmI);
}
public override void OnHitByItem(NPC npc, Player player, Item item, NPC.HitInfo hit, int damageDone)
{
if (item.ModItem is NormalSickle or LegacyDeath)
RegisterSickleHit(npc, player.whoAmI);
}
public override void OnHitByProjectile(NPC npc, Projectile projectile, NPC.HitInfo hit, int damageDone)
{
if (projectile.GetGlobalProjectile<MyGlobalProjectile>().IsSickleProjectile)
RegisterSickleHit(npc, projectile.owner);
}
public override void PostAI(NPC npc)
{
if (Main.netMode != NetmodeID.MultiplayerClient && lastSicklePlayer >= 0)
npc.AddBuff(ModContent.BuffType<DeathMarkBuff>(), 2);
UpdateFatedState(npc);
SpawnStatusParticles(npc);
}
public override void PostDraw(NPC npc, SpriteBatch spriteBatch, Vector2 screenPos, Color drawColor)
{
if (!deathMarked)
return;
Texture2D markTexture = TextureAssets.Buff[ModContent.BuffType<DeathMarkBuff>()].Value;
float pulse = 0.52f + (float)Math.Sin(Main.GlobalTimeWrappedHourly * 5f + npc.whoAmI) * 0.035f;
Vector2 position = new Vector2(npc.Center.X, npc.Top.Y - 13f) - screenPos;
Color auraColor = FatedStackCount > 0 ? new Color(235, 55, 150, 0) : new Color(65, 235, 255, 0);
for (int index = 0; index < 4; index++)
{
Vector2 offset = (Main.GlobalTimeWrappedHourly * 2f + index * MathHelper.PiOver2).ToRotationVector2() * 2f;
spriteBatch.Draw(markTexture, position + offset, null, auraColor * 0.32f, 0f, markTexture.Size() / 2f, pulse * 1.12f, SpriteEffects.None, 0f);
}
spriteBatch.Draw(markTexture, position, null, Color.White, 0f, markTexture.Size() / 2f, pulse, SpriteEffects.None, 0f);
if (FatedStackCount <= 0)
return;
Texture2D fatedTexture = TextureAssets.Buff[ModContent.BuffType<FatedBuff>()].Value;
Vector2 fatedPosition = position + new Vector2(18f, 1f);
spriteBatch.Draw(fatedTexture, fatedPosition, null, Color.White, 0f, fatedTexture.Size() / 2f, 0.44f, SpriteEffects.None, 0f);
Utils.DrawBorderStringFourWay(spriteBatch, FontAssets.ItemStack.Value, FatedStackCount.ToString(), fatedPosition.X + 5f, fatedPosition.Y + 3f, new Color(255, 95, 180), Color.Black, Vector2.Zero, 0.58f);
}
public override void SendExtraAI(NPC npc, BitWriter bitWriter, BinaryWriter binaryWriter)
{
bitWriter.WriteBit(deathMarked);
binaryWriter.Write((byte)FatedStackCount);
}
public override void ReceiveExtraAI(NPC npc, BitReader bitReader, BinaryReader binaryReader)
{
deathMarked = bitReader.ReadBit();
FatedStackCount = Math.Clamp((int)binaryReader.ReadByte(), 0, AbsoluteMaximumFatedStacks);
}
public override void OnKill(NPC npc)
{
if (Main.netMode == NetmodeID.MultiplayerClient || npc.friendly || npc.lifeMax <= 5)
return;
// Dungeon Guardians and Paladins are exceptional normal enemies: their unusual
// health values should not distort the regular curve, and they always cap it.
if (IsFixedMaximumSoulEnemy(npc.type))
{
if (lastSicklePlayer >= 0 && lastSicklePlayer < Main.maxPlayers && Main.player[lastSicklePlayer].active)
Main.player[lastSicklePlayer].GetModPlayer<MyPlayer>().AddSouls(50);
return;
}
if (TryGetBossParticipants(npc, out bool[] bossParticipants, out int encounterLifeMax))
{
int reward = CalculateSoulReward(encounterLifeMax, boss: true);
for (int playerIndex = 0; playerIndex < Main.maxPlayers; playerIndex++)
{
if (bossParticipants[playerIndex] && Main.player[playerIndex].active)
Main.player[playerIndex].GetModPlayer<MyPlayer>().AddSouls(reward, announce: true);
}
return;
}
if (IsCompositeBossPart(npc.type))
return;
if (npc.realLife >= 0 && npc.realLife < Main.maxNPCs && Main.npc[npc.realLife].boss)
return;
if (!npc.boss && lastSicklePlayer >= 0 && lastSicklePlayer < Main.maxPlayers && Main.player[lastSicklePlayer].active)
Main.player[lastSicklePlayer].GetModPlayer<MyPlayer>().AddSouls(CalculateSoulReward(npc.lifeMax, boss: false));
}
internal void RegisterSickleHit(NPC npc, int playerIndex, bool applyDeathMark = true)
{
if (Main.netMode == NetmodeID.MultiplayerClient
|| playerIndex < 0
|| playerIndex >= Main.maxPlayers
|| npc.friendly)
return;
if (applyDeathMark)
MarkForReaping(npc);
sickleParticipants[playerIndex] = true;
lastSicklePlayer = playerIndex;
BossSoulParticipationSystem.RecordCompositeBossHit(npc, playerIndex);
if (npc.realLife >= 0 && npc.realLife < Main.maxNPCs && npc.realLife != npc.whoAmI)
{
MyGlobalNPC root = Main.npc[npc.realLife].GetGlobalNPC<MyGlobalNPC>();
root.sickleParticipants[playerIndex] = true;
root.lastSicklePlayer = playerIndex;
}
}
#if DEBUG
internal bool HasSickleHarvester(int playerIndex)
=> playerIndex >= 0
&& playerIndex < Main.maxPlayers
&& sickleParticipants[playerIndex]
&& lastSicklePlayer == playerIndex;
#endif
internal void AddFatedStacks(NPC npc, int amount, int maximumStacks, int durationFrames, int playerIndex, int lifeStealLevel)
{
if (amount <= 0 || Main.netMode == NetmodeID.MultiplayerClient || npc.friendly || npc.lifeMax <= 5)
return;
maximumStacks = Math.Clamp(maximumStacks, 1, AbsoluteMaximumFatedStacks);
durationFrames = Math.Clamp(durationFrames, 60, ushort.MaxValue);
int stacksToAdd = Math.Min(amount, Math.Max(0, maximumStacks - FatedStackCount));
for (int index = 0; index < stacksToAdd; index++)
{
fatedStackTimers[FatedStackCount] = durationFrames;
fatedStackOwners[FatedStackCount] = playerIndex is >= 0 and < Main.maxPlayers ? playerIndex : -1;
fatedStackLifeStealLevels[FatedStackCount] = Math.Max(0, lifeStealLevel);
FatedStackCount++;
}
for (int index = 0; index < FatedStackCount; index++)
fatedStackTimers[index] = Math.Max(fatedStackTimers[index], durationFrames);
npc.AddBuff(ModContent.BuffType<FatedBuff>(), durationFrames);
npc.netUpdate = true;
}
internal void MarkForReaping(NPC npc)
{
bool newlyMarked = !deathMarked;
deathMarked = true;
npc.AddBuff(ModContent.BuffType<DeathMarkBuff>(), DeathMarkDuration);
SyncBuffState(npc);
if (newlyMarked)
SoulHarvest.BroadcastDeathMark(npc, marked: true);
}
internal void ApplyDeathDomainHitStop(NPC npc, int frames)
{
frames = Math.Clamp(frames, 1, 4);
if (deathDomainHitStopFrames <= 0)
deathDomainStoredVelocity = npc.velocity;
deathDomainHitStopFrames = Math.Max(deathDomainHitStopFrames, frames);
npc.velocity = Vector2.Zero;
if (Main.netMode != NetmodeID.MultiplayerClient)
npc.netUpdate = true;
}
internal void ReceiveDeathMarkState(bool marked)
{
deathMarked = marked;
}
internal static void SyncBuffState(NPC npc)
{
npc.netUpdate = true;
if (Main.netMode == NetmodeID.Server)
NetMessage.SendData(MessageID.NPCBuffs, -1, -1, null, npc.whoAmI);
}
private void UpdateFatedState(NPC npc)
{
if (Main.netMode == NetmodeID.MultiplayerClient || FatedStackCount <= 0)
return;
int writeIndex = 0;
for (int index = 0; index < FatedStackCount; index++)
{
if (--fatedStackTimers[index] > 0)
{
fatedStackTimers[writeIndex] = fatedStackTimers[index];
fatedStackOwners[writeIndex] = fatedStackOwners[index];
fatedStackLifeStealLevels[writeIndex] = fatedStackLifeStealLevels[index];
writeIndex++;
}
}
if (writeIndex != FatedStackCount)
{
FatedStackCount = writeIndex;
npc.netUpdate = true;
}
if (FatedStackCount <= 0)
{
fatedSecondTimer = 0;
return;
}
npc.AddBuff(ModContent.BuffType<FatedBuff>(), 2);
if (++fatedSecondTimer < 60)
return;
fatedSecondTimer = 0;
bool bossOrBossPart = npc.boss || npc.realLife >= 0 && npc.realLife < Main.maxNPCs && Main.npc[npc.realLife].boss;
float ratePerStack = bossOrBossPart ? 0.01f : 1f / 3f;
int damage = Math.Max(1, (int)Math.Ceiling(npc.lifeMax * ratePerStack * FatedStackCount));
int lifeBeforeHit = npc.life;
Vector2 lifeStealSource = npc.Center;
int[] stackCountsByPlayer = new int[Main.maxPlayers];
int[] lifeStealLevelByPlayer = new int[Main.maxPlayers];
for (int index = 0; index < FatedStackCount; index++)
{
int owner = fatedStackOwners[index];
if (owner < 0 || owner >= Main.maxPlayers)
continue;
stackCountsByPlayer[owner]++;
lifeStealLevelByPlayer[owner] = Math.Max(lifeStealLevelByPlayer[owner], fatedStackLifeStealLevels[index]);
}
NPC.HitInfo fateHit = new()
{
Damage = damage,
SourceDamage = damage,
HitDirection = 0,
Knockback = 0f,
DamageType = DamageClass.Generic,
Crit = false
};
npc.StrikeNPC(fateHit, fromNet: false, noPlayerInteraction: false);
if (Main.netMode == NetmodeID.Server)
NetMessage.SendStrikeNPC(npc, in fateHit);
int actualDamage = Math.Min(Math.Max(0, lifeBeforeHit), damage);
for (int playerIndex = 0; playerIndex < Main.maxPlayers; playerIndex++)
{
int stackCount = stackCountsByPlayer[playerIndex];
int lifeStealLevel = lifeStealLevelByPlayer[playerIndex];
if (stackCount <= 0 || lifeStealLevel <= 0 || !Main.player[playerIndex].active || Main.player[playerIndex].dead)
continue;
int attributedDamage = Math.Max(1, (int)Math.Round(actualDamage * stackCount / (double)FatedStackCount));
int healedLife = Main.player[playerIndex].GetModPlayer<MyPlayer>().TryLifeSteal(attributedDamage, lifeStealLevel);
LifeStealVisuals.Spawn(lifeStealSource, playerIndex, lifeStealLevel, healedLife);
}
}
private void SpawnStatusParticles(NPC npc)
{
if (Main.netMode == NetmodeID.Server || !npc.active)
return;
if (deathMarked && Main.rand.NextBool(4))
{
float angle = Main.GlobalTimeWrappedHourly * 3.1f + npc.whoAmI * 0.73f + Main.rand.NextFloat(-0.35f, 0.35f);
Vector2 orbit = new Vector2(Math.Max(12f, npc.width * 0.58f), Math.Max(9f, npc.height * 0.36f));
Vector2 position = npc.Center + new Vector2((float)Math.Cos(angle) * orbit.X, (float)Math.Sin(angle) * orbit.Y);
Dust markDust = Dust.NewDustPerfect(position, DustID.DungeonSpirit, -npc.velocity * 0.08f + Vector2.UnitY * -0.25f, 80, new Color(70, 230, 255), 0.72f);
markDust.noGravity = true;
}
if (FatedStackCount <= 0)
return;
int particleCount = Main.rand.NextBool(2) ? Math.Min(2, FatedStackCount) : 1;
for (int index = 0; index < particleCount; index++)
{
Vector2 position = npc.Center + Main.rand.NextVector2CircularEdge(npc.width * 0.62f + 8f, npc.height * 0.5f + 8f);
Vector2 velocity = (npc.Center - position).SafeNormalize(Vector2.Zero) * Main.rand.NextFloat(0.45f, 1.2f);
Dust fateDust = Dust.NewDustPerfect(position, DustID.Shadowflame, velocity, 55, new Color(245, 45, 145), 0.9f + FatedStackCount * 0.08f);
fateDust.noGravity = true;
}
}
internal static int CalculateSoulReward(int lifeMax, bool boss)
{
lifeMax = Math.Max(1, lifeMax);
if (boss)
return Math.Clamp((int)Math.Ceiling(1.3d * Math.Sqrt(lifeMax)), 1, 500);
double normalizedLife = lifeMax / 25d;
return Math.Clamp((int)Math.Ceiling(Math.Pow(normalizedLife, 0.55d)), 1, 50);
}
private bool TryGetBossParticipants(NPC npc, out bool[] participants, out int encounterLifeMax)
{
if (BossSoulParticipationSystem.TryTakeCompositeParticipants(npc, out participants, out encounterLifeMax))
return true;
if (IsCompositeBossPart(npc.type))
{
encounterLifeMax = 0;
return false;
}
if (!npc.boss)
{
encounterLifeMax = 0;
return false;
}
if (npc.realLife >= 0 && npc.realLife < Main.maxNPCs && npc.realLife != npc.whoAmI && Main.npc[npc.realLife].active)
{
encounterLifeMax = 0;
return false;
}
if (npc.realLife >= 0 && npc.realLife < Main.maxNPCs)
{
participants = (bool[])Main.npc[npc.realLife].GetGlobalNPC<MyGlobalNPC>().sickleParticipants.Clone();
encounterLifeMax = BossSoulParticipationSystem.GetEncounterLifeMax(npc);
return true;
}
participants = (bool[])sickleParticipants.Clone();
encounterLifeMax = BossSoulParticipationSystem.GetEncounterLifeMax(npc);
return true;
}
private static bool IsCompositeBossPart(int npcType)
{
return npcType is NPCID.Retinazer
or NPCID.Spazmatism
or NPCID.EaterofWorldsBody
or NPCID.EaterofWorldsHead
or NPCID.EaterofWorldsTail
or NPCID.Creeper
or NPCID.GolemHead
or NPCID.GolemHeadFree
or NPCID.MoonLordHead
or NPCID.MoonLordHand;
}
private static bool IsFixedMaximumSoulEnemy(int npcType)
{
return npcType is NPCID.DungeonGuardian or NPCID.Paladin;
}
}