Initial commit: Tiny Tackle Heroes Unity project

This commit is contained in:
2026-09-15 17:37:42 +09:00
commit 591fa9a826
1623 changed files with 182420 additions and 0 deletions
@@ -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();
}
}
}