393 lines
12 KiB
C#
393 lines
12 KiB
C#
using System.Collections.Generic;
|
|
using BumpCombat.Constants;
|
|
using BumpCombat.Core;
|
|
using BumpCombat.Spawning;
|
|
using UnityEngine;
|
|
|
|
namespace BumpCombat.Enemies
|
|
{
|
|
/// <summary>
|
|
/// Owns the Necromancer's one-time desperation summon phase. Timed events
|
|
/// and ordinary attack patterns remain owned by RunManager and
|
|
/// EnemyController.
|
|
/// </summary>
|
|
[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<EnemyController> 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<EnemyController>();
|
|
}
|
|
|
|
private void OnEnable()
|
|
{
|
|
if (controller != null)
|
|
{
|
|
controller.OnDamageApplied += HandleDamageApplied;
|
|
controller.OnDied += HandleOwnerDied;
|
|
}
|
|
}
|
|
|
|
private void Start()
|
|
{
|
|
spawnDirector = FindAnyObjectByType<SpawnDirector>();
|
|
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<EnemyController> 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<EnemyController> 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<SpawnDirector>();
|
|
}
|
|
if (spawnDirector == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (!spawnDirector.TrySpawnPhaseSummons(
|
|
controller,
|
|
2,
|
|
true,
|
|
out List<EnemyController> 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>();
|
|
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();
|
|
}
|
|
}
|
|
}
|