using System.Collections.Generic; using BumpCombat.Constants; using BumpCombat.Core; using BumpCombat.Spawning; using UnityEngine; namespace BumpCombat.Enemies { /// /// Owns the Necromancer's one-time desperation summon phase. Timed events /// and ordinary attack patterns remain owned by RunManager and /// EnemyController. /// [RequireComponent(typeof(EnemyController))] public sealed class NecromancerBossController : MonoBehaviour, IEnemyDamageGate, IEnemyAttackPatternGate, IEnemyActionGate { private float generalSummonCooldown => GameplayConstants.Current.Enemies.GeneralSummonCooldown; private float crowdSummonCooldown => GameplayConstants.Current.Enemies.CrowdSummonCooldown; private float multiAoeCooldown => GameplayConstants.Current.Enemies.MultiAoeCooldown; private float summonVisualDuration => GameplayConstants.Current.Enemies.SummonVisualDuration; private float desperationHealth => GameplayConstants.Current.Enemies.DesperationHealthFraction; private readonly List phaseSummons = new(); private EnemyController controller; private SpawnDirector spawnDirector; private bool desperationStarted; private bool desperationPending; private bool desperationPhaseStarted; private bool desperationCompleted; private float nextGeneralSummonTime; private float nextCrowdSummonTime; private float nextMultiAoeTime; private float summonLockUntil; public bool IsDamageBlocked => desperationPending || phaseSummons.Count > 0; public bool IsPhaseActive => phaseSummons.Count > 0; public int PhaseSummonCount => phaseSummons.Count; public float MultiAoeCooldown => Mathf.Max(0.5f, multiAoeCooldown); public bool IsAttackLocked => desperationPending || Time.time < summonLockUntil; public bool DesperationStarted => desperationStarted; public bool CanUseAttackPattern(int patternIndex) { return patternIndex != 2 || Time.time >= nextMultiAoeTime; } public void NotifyAttackPatternUsed(int patternIndex) { if (patternIndex == 2) { nextMultiAoeTime = Time.time + MultiAoeCooldown; } } private void Awake() { controller = GetComponent(); } private void OnEnable() { if (controller != null) { controller.OnDamageApplied += HandleDamageApplied; controller.OnDied += HandleOwnerDied; } } private void Start() { spawnDirector = FindAnyObjectByType(); nextGeneralSummonTime = Time.time + generalSummonCooldown; nextCrowdSummonTime = Time.time + crowdSummonCooldown; } private void OnDestroy() { if (controller != null) { controller.OnDamageApplied -= HandleDamageApplied; controller.OnDied -= HandleOwnerDied; } ClearPhaseSummons(); StageEnemyEffectVisual.SetProtection(gameObject, false); } private void OnDisable() { if (controller != null) { controller.OnDamageApplied -= HandleDamageApplied; controller.OnDied -= HandleOwnerDied; } spawnDirector?.DespawnSummonsOwnedBy(controller, true); DespawnOwnedPhaseSummons(); StageEnemyEffectVisual.ClearOwnedEffects(gameObject); StageEnemyEffectVisual.SetProtection(gameObject, false); desperationStarted = false; desperationPending = false; desperationPhaseStarted = false; desperationCompleted = false; summonLockUntil = 0f; } private void Update() { if (controller == null || controller.IsDead) { return; } if (!RunManager.GameplayInputEnabled) { return; } if (controller.IsGroggy) { return; } RemoveDeadPhaseSummons(); if (controller.IsGroggy) { return; } if (desperationPending) { TryBeginDesperationPhase(); return; } if (IsAttackLocked) { return; } if (controller.IsStunned || controller.IsKnockbackActive) { return; } if (controller.IsAttackSequenceInProgress) { return; } if (Time.time >= nextGeneralSummonTime) { bool spawned = spawnDirector != null && spawnDirector.TrySpawnAmbientSummons( controller, 1, false, out List generalSummons) && generalSummons.Count > 0; nextGeneralSummonTime = Time.time + generalSummonCooldown; if (spawned) { PlaySummonTelegraph(false); return; } } if (Time.time >= nextCrowdSummonTime) { bool spawned = spawnDirector != null && spawnDirector.TrySpawnAmbientSummons( controller, 3, true, out List crowdSummons) && crowdSummons.Count > 0; nextCrowdSummonTime = Time.time + crowdSummonCooldown; if (spawned) { PlaySummonTelegraph(false); } } } public float FilterDamage(float currentHealth, float requestedDamage) { if (requestedDamage <= 0f || controller == null) { return 0f; } if (desperationPending || phaseSummons.Count > 0) { return 0f; } if (controller.IsGroggy && !desperationStarted) { float desperationTarget = Mathf.Max( 0.0001f, controller.MaximumHealth * desperationHealth); if (currentHealth > desperationTarget) { return Mathf.Min( requestedDamage, currentHealth - desperationTarget); } return 0f; } return requestedDamage; } private void HandleDamageApplied(float healthBefore, float damageApplied) { if (controller == null || controller.IsDead) { return; } float healthFraction = controller.CurrentHealth / Mathf.Max(0.0001f, controller.MaximumHealth); if (controller.IsGroggy && !desperationStarted && healthFraction <= desperationHealth + 0.0001f) { desperationStarted = true; desperationPending = true; controller.EndGroggyForProtection(); TryBeginDesperationPhase(); return; } if (desperationPending) { return; } } private void TryBeginDesperationPhase() { if (!RunManager.GameplayInputEnabled) { return; } if (spawnDirector == null) { spawnDirector = FindAnyObjectByType(); } if (spawnDirector == null) { return; } if (!spawnDirector.TrySpawnPhaseSummons( controller, 2, true, out List spawned)) { return; } phaseSummons.AddRange(spawned); for (int i = 0; i < spawned.Count; i++) { spawned[i].OnDied += HandlePhaseSummonDied; } desperationPhaseStarted = true; desperationPending = false; StageEnemyEffectVisual.SetProtection(gameObject, true); PlaySummonTelegraph(true); } private void PlaySummonTelegraph(bool interruptAttack) { if (interruptAttack) { controller.CancelCurrentAttack(); } float visualDuration = controller.GetSummonAnimationDuration(); if (visualDuration <= 0f) { visualDuration = summonVisualDuration; } summonLockUntil = Time.time + visualDuration; StageEnemyEffectVisual.PlaySummon( gameObject, transform.position, visualDuration); controller.PlaySummonAnimation(); } private void HandlePhaseSummonDied(EnemyController summon) { if (summon == null) { return; } summon.OnDied -= HandlePhaseSummonDied; phaseSummons.Remove(summon); if (phaseSummons.Count == 0 && desperationPhaseStarted && !desperationPending && !desperationCompleted) { CompleteDesperationPhase(); } } private void HandleOwnerDied(EnemyController owner) { spawnDirector ??= FindAnyObjectByType(); spawnDirector?.DespawnSummonsOwnedBy(owner, true); DespawnOwnedPhaseSummons(); StageEnemyEffectVisual.ClearOwnedEffects(gameObject); StageEnemyEffectVisual.SetProtection(gameObject, false); } private void RemoveDeadPhaseSummons() { for (int i = phaseSummons.Count - 1; i >= 0; i--) { if (phaseSummons[i] == null || phaseSummons[i].IsDead) { if (phaseSummons[i] != null) { phaseSummons[i].OnDied -= HandlePhaseSummonDied; } phaseSummons.RemoveAt(i); } } if (phaseSummons.Count == 0 && desperationPhaseStarted && !desperationPending && !desperationCompleted) { CompleteDesperationPhase(); } } private void CompleteDesperationPhase() { if (controller == null || controller.IsDead || desperationCompleted) { return; } if (controller.BeginGroggyAfterProtection()) { desperationCompleted = true; StageEnemyEffectVisual.SetProtection(gameObject, false); } } private void ClearPhaseSummons() { for (int i = 0; i < phaseSummons.Count; i++) { if (phaseSummons[i] != null) { phaseSummons[i].OnDied -= HandlePhaseSummonDied; } } phaseSummons.Clear(); } private void DespawnOwnedPhaseSummons() { for (int i = 0; i < phaseSummons.Count; i++) { EnemyController summon = phaseSummons[i]; if (summon != null && !summon.IsDead) { // OnDisable also runs during scene unload. Suppress the // retired-event feedback there so unload cannot create // transient visual objects after the scene is closing. summon.RetireFromRun(true, true); } } ClearPhaseSummons(); } } }