using System; using Terraria; using Terraria.ID; using Terraria.ModLoader; namespace SoulHarvest.Common; /// /// Awards one progression seal for every world-unlocked stage per completed boss /// encounter to each online player who dealt damage. Dead participants remain /// eligible because Player.active, not Player.dead, is the eligibility condition. /// internal sealed class ReaperBossRewardGlobalNPC : GlobalNPC { private readonly bool[] participants = new bool[Main.maxPlayers]; public override bool InstancePerEntity => true; public override void SetDefaults(NPC npc) { Array.Clear(participants); } public override void OnHitByItem(NPC npc, Player player, Item item, NPC.HitInfo hit, int damageDone) { if (Main.netMode != NetmodeID.MultiplayerClient && damageDone > 0) RecordHit(npc, player.whoAmI); } public override void OnHitByProjectile(NPC npc, Projectile projectile, NPC.HitInfo hit, int damageDone) { if (Main.netMode != NetmodeID.MultiplayerClient && damageDone > 0) RecordHit(npc, projectile.owner); } public override void OnKill(NPC npc) { if (Main.netMode == NetmodeID.MultiplayerClient || npc.friendly || npc.lifeMax <= 5) return; ReaperBossFamily family = ReaperBossParticipationSystem.GetFamily(npc.type); bool belongsToGenericBoss = npc.boss || npc.realLife >= 0 && npc.realLife < Main.maxNPCs && Main.npc[npc.realLife].active && Main.npc[npc.realLife].boss; if (family == ReaperBossFamily.None && !belongsToGenericBoss) return; // Vanilla records player interaction before resolving CheckDead/OnKill. OR // that authoritative record into our encounter ledger so a lethal first hit // and client-authoritative melee hits cannot miss the reward window. CaptureVanillaParticipants(npc); bool[] encounterParticipants; if (family != ReaperBossFamily.None) { if (!ReaperBossParticipationSystem.TryTakeParticipants(npc, out encounterParticipants)) return; } else { if (!npc.boss) return; encounterParticipants = (bool[])participants.Clone(); } if (IsMechanicalBoss(npc.type, family) && ReaperBossParticipationSystem.IsCompositeMechanicalEncounter()) { // Defer cross-family encounters until every mechanical component is // gone. The shared ledger unions participation and produces one reward // bundle per player for the entire encounter. ReaperBossParticipationSystem.RegisterMechanicalCompletion(family, encounterParticipants); return; } ReaperStage maximumRewardStage = GetHighestUnlockedRewardStage( npc.type, family, mechanicalEncounterCompletedTrio: false); if (maximumRewardStage == ReaperStage.Locked) return; for (int playerIndex = 0; playerIndex < Main.maxPlayers; playerIndex++) { Player player = Main.player[playerIndex]; if (!encounterParticipants[playerIndex] || !player.active) continue; GrantUnlockedStageRewards(player, maximumRewardStage); } } internal static void AwardMechanicalEncounter(bool[] encounterParticipants, bool completedTrio) { ReaperStage maximumRewardStage = GetHighestUnlockedRewardStage( NPCID.None, ReaperBossFamily.None, completedTrio); if (maximumRewardStage == ReaperStage.Locked) return; for (int playerIndex = 0; playerIndex < Main.maxPlayers; playerIndex++) { Player player = Main.player[playerIndex]; if (!encounterParticipants[playerIndex] || !player.active) continue; GrantUnlockedStageRewards(player, maximumRewardStage); } } internal void ClearParticipant(int playerIndex) { if (playerIndex >= 0 && playerIndex < participants.Length) participants[playerIndex] = false; } private void RecordHit(NPC npc, int playerIndex) { if (playerIndex < 0 || playerIndex >= Main.maxPlayers || !Main.player[playerIndex].active) return; participants[playerIndex] = true; ReaperBossParticipationSystem.RecordHit(npc, playerIndex); // Generic segmented modded bosses commonly point every segment at realLife. // Preserve their participants on the root even when they are not in a known // vanilla family. if (npc.realLife >= 0 && npc.realLife < Main.maxNPCs && npc.realLife != npc.whoAmI) Main.npc[npc.realLife].GetGlobalNPC().participants[playerIndex] = true; } private void CaptureVanillaParticipants(NPC npc) { int count = Math.Min(Main.maxPlayers, npc.playerInteraction.Length); for (int playerIndex = 0; playerIndex < count; playerIndex++) { if (npc.playerInteraction[playerIndex]) RecordHit(npc, playerIndex); } } private static void GrantUnlockedStageRewards(Player player, ReaperStage maximumStage) { MyPlayer modPlayer = player.GetModPlayer(); for (ReaperStage stage = ReaperStage.StageI; stage <= maximumStage; stage++) modPlayer.TryGrantSoulSeal(stage); } private static ReaperStage GetHighestUnlockedRewardStage( int defeatedNpcType, ReaperBossFamily defeatedFamily, bool mechanicalEncounterCompletedTrio) { if (AllMechanicalBossesDefeated() || mechanicalEncounterCompletedTrio || IsMechanicalBoss(defeatedNpcType, defeatedFamily) && WouldCompleteMechanicalTrio(defeatedNpcType, defeatedFamily)) { return ReaperStage.StageIII; } if (Main.hardMode || defeatedNpcType == NPCID.WallofFlesh) return ReaperStage.StageII; if (NPC.downedBoss3 || defeatedNpcType == NPCID.SkeletronHead) return ReaperStage.StageI; return ReaperStage.Locked; } private static bool WouldCompleteMechanicalTrio(int npcType, ReaperBossFamily family) { bool destroyer = NPC.downedMechBoss1 || family == ReaperBossFamily.Destroyer || npcType == NPCID.TheDestroyer; bool twins = NPC.downedMechBoss2 || family == ReaperBossFamily.Twins || npcType is NPCID.Retinazer or NPCID.Spazmatism; bool prime = NPC.downedMechBoss3 || family == ReaperBossFamily.SkeletronPrime || npcType == NPCID.SkeletronPrime; return destroyer && twins && prime; } private static bool AllMechanicalBossesDefeated() { return NPC.downedMechBoss1 && NPC.downedMechBoss2 && NPC.downedMechBoss3; } private static bool IsMechanicalBoss(int npcType, ReaperBossFamily family) { return family is ReaperBossFamily.Twins or ReaperBossFamily.Destroyer or ReaperBossFamily.SkeletronPrime || npcType is NPCID.Retinazer or NPCID.Spazmatism or NPCID.TheDestroyer or NPCID.SkeletronPrime; } }