Initial commit: Tiny Tackle Heroes Unity project
This commit is contained in:
@@ -0,0 +1,392 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user