using System; using System.Collections.Generic; using GeometryTD.DataTable; using GeometryTD.Definition; using UnityEngine; namespace GeometryTD.CustomComponent { internal sealed class EnemySpawnDirector { private sealed class SpawnEntryRuntime { public DRLevelSpawnEntry Entry; public bool Completed; public float NextTriggerTime; public float EndTime; public int RemainingCount; } private const float MinStreamInterval = 0.05f; private const float MinBurstGap = 0.01f; private readonly List _spawnRuntimes = new(); private float _phaseElapsed; public bool IsPhaseSpawnCompleted { get; private set; } = true; public bool IsPhaseRunning { get; private set; } public void Reset() { EndPhase(); } public void BeginPhase(IReadOnlyList spawnEntries) { EndPhase(); _phaseElapsed = 0f; IsPhaseRunning = true; IsPhaseSpawnCompleted = false; if (spawnEntries != null) { for (int i = 0; i < spawnEntries.Count; i++) { SpawnEntryRuntime runtime = BuildSpawnRuntime(spawnEntries[i]); if (runtime != null) { _spawnRuntimes.Add(runtime); } } } IsPhaseSpawnCompleted = _spawnRuntimes.Count <= 0; } public void OnUpdate(float elapseSeconds, Action spawnAction) { if (!IsPhaseRunning || spawnAction == null) { return; } _phaseElapsed += elapseSeconds; UpdateSpawnRuntimes(spawnAction); } public void EndPhase() { IsPhaseRunning = false; _phaseElapsed = 0f; _spawnRuntimes.Clear(); IsPhaseSpawnCompleted = true; } private SpawnEntryRuntime BuildSpawnRuntime(DRLevelSpawnEntry entry) { if (entry == null || entry.EntryType == EntryType.None) { return null; } SpawnEntryRuntime runtime = new SpawnEntryRuntime { Entry = entry, Completed = false, NextTriggerTime = Mathf.Max(0f, entry.StartTime), EndTime = Mathf.Max(0f, entry.StartTime), RemainingCount = Mathf.Max(0, entry.Count) }; switch (entry.EntryType) { case EntryType.Stream: { float duration = Mathf.Max(0f, entry.Duration); runtime.EndTime = duration > 0f ? runtime.NextTriggerTime + duration : runtime.NextTriggerTime; runtime.Completed = entry.Count <= 0; return runtime; } case EntryType.Burst: case EntryType.Boss: runtime.Completed = runtime.RemainingCount <= 0; return runtime; default: return null; } } private void UpdateSpawnRuntimes(Action spawnAction) { bool allCompleted = true; for (int i = 0; i < _spawnRuntimes.Count; i++) { SpawnEntryRuntime runtime = _spawnRuntimes[i]; if (runtime.Completed) { continue; } switch (runtime.Entry.EntryType) { case EntryType.Stream: ProcessStreamRuntime(runtime, spawnAction); break; case EntryType.Burst: case EntryType.Boss: ProcessBurstRuntime(runtime, spawnAction); break; default: runtime.Completed = true; break; } if (!runtime.Completed) { allCompleted = false; } } IsPhaseSpawnCompleted = allCompleted; } private void ProcessStreamRuntime(SpawnEntryRuntime runtime, Action spawnAction) { if (_phaseElapsed < runtime.NextTriggerTime) { return; } int countPerWave = Mathf.Max(0, runtime.Entry.Count); if (countPerWave <= 0) { runtime.Completed = true; return; } float interval = runtime.Entry.Interval > 0f ? runtime.Entry.Interval : MinStreamInterval; while (_phaseElapsed >= runtime.NextTriggerTime && runtime.NextTriggerTime <= runtime.EndTime) { spawnAction(runtime.Entry, countPerWave); runtime.NextTriggerTime += interval; } if (runtime.NextTriggerTime > runtime.EndTime) { runtime.Completed = true; } } private void ProcessBurstRuntime(SpawnEntryRuntime runtime, Action spawnAction) { if (_phaseElapsed < runtime.NextTriggerTime) { return; } if (runtime.RemainingCount <= 0) { runtime.Completed = true; return; } float gap = runtime.Entry.Gap; if (gap <= 0f) { spawnAction(runtime.Entry, runtime.RemainingCount); runtime.RemainingCount = 0; runtime.Completed = true; return; } gap = Mathf.Max(gap, MinBurstGap); while (_phaseElapsed >= runtime.NextTriggerTime && runtime.RemainingCount > 0) { spawnAction(runtime.Entry, 1); runtime.RemainingCount--; runtime.NextTriggerTime += gap; } runtime.Completed = runtime.RemainingCount <= 0; } } }