using System.Collections.Generic; using GeometryTD.CustomEvent; using GeometryTD.DataTable; using GeometryTD.Definition; using GeometryTD.Entity; using GeometryTD.UI; using UnityEngine; using UnityGameFramework.Runtime; namespace GeometryTD.CustomComponent { public class CombatScheduler { private enum SchedulerState : byte { Idle = 0, WaitingForLoading = 1, RunningPhase = 2, WaitingForFinishReturn = 3, Completed = 4, Failed = 5 } private readonly List _phaseBuffer = new(); private readonly Dictionary> _spawnEntriesByPhaseId = new(); private readonly EnemyManager _enemyManager = new(); private readonly PhaseLoopRuntime _phaseLoopRuntime = new(); private readonly CombatLoadSession _loadSession = new(); private readonly CombatEventBridge _eventBridge = new(); private readonly CombatResourceManager _combatResourceManager = new(); private EntityComponent _entity; private DRLevel _currentLevel; private CombatFinishFormUseCase _combatFinishFormUseCase; private SchedulerState _state = SchedulerState.Idle; private bool _initialized; private bool _isFinishAsVictory = true; public bool IsRunning => _state == SchedulerState.WaitingForLoading || _state == SchedulerState.RunningPhase; public bool IsCompleted => _state == SchedulerState.Completed; public DRLevel CurrentLevel => _currentLevel; public DRLevelPhase CurrentPhase => _phaseLoopRuntime.CurrentPhase; public MapEntity CurrentMap => _loadSession.CurrentMap; public int DisplayPhaseIndex => _phaseLoopRuntime.DisplayPhaseIndex; public int PhaseCount => _phaseLoopRuntime.PhaseCount; public bool CanEndCombat => _phaseLoopRuntime.CanEndCombat; public int DefeatedEnemyCount => _enemyManager.DefeatedEnemyCount; public int GainedCoin => _combatResourceManager.GainedCoin; public int GainedGold => _combatResourceManager.GainedGold; public void OnInit() { if (!_initialized) { _entity = GameEntry.Entity; _eventBridge.Bind( OnShowEntitySuccess, OnShowEntityFailure, OnHideEntityComplete, OnOpenUIFormSuccess, OnOpenUIFormFailure, OnCloseUIFormComplete); _enemyManager.OnInit(this); _loadSession.OnInit(_entity); EnsureCombatFinishFormUseCaseBound(); _initialized = true; } ResetRuntime(); } public bool Start( DRLevel level, IReadOnlyList phases, IReadOnlyDictionary> spawnEntriesByPhaseId) { if (!_initialized || _entity == null) { return HandleStartFailure("CombatScheduler start failed. Runtime is not initialized."); } if (level == null || phases == null || phases.Count <= 0) { return HandleStartFailure("CombatScheduler start failed. Invalid level or phase data."); } CleanupAllCombatEntities(); CloseCombatFinishForm(); _enemyManager.EndPhase(); _enemyManager.ResetCombatStats(); ResetRuntime(); _combatResourceManager.Reset(); _isFinishAsVictory = true; _currentLevel = level; for (int i = 0; i < phases.Count; i++) { DRLevelPhase phase = phases[i]; if (phase != null) { _phaseBuffer.Add(phase); } } if (spawnEntriesByPhaseId != null) { foreach (var pair in spawnEntriesByPhaseId) { _spawnEntriesByPhaseId[pair.Key] = pair.Value; } } _phaseLoopRuntime.SetPhases(_phaseBuffer); if (_phaseLoopRuntime.PhaseCount <= 0) { return HandleStartFailure($"CombatScheduler start failed. Level '{level.Id}' has no phase data."); } if (!_loadSession.StartLoading(level, out string loadError)) { return HandleStartFailure($"CombatScheduler start failed. {loadError}"); } _state = SchedulerState.WaitingForLoading; Log.Info("CombatScheduler started. Level={0}, PhaseCount={1}.", _currentLevel.Id, _phaseLoopRuntime.PhaseCount); return true; } public void OnUpdate(float elapseSeconds, float realElapseSeconds) { switch (_state) { case SchedulerState.WaitingForLoading: if (!_loadSession.IsReady) { return; } BeginNextPhase(); return; case SchedulerState.RunningPhase: UpdateCurrentPhase(elapseSeconds, realElapseSeconds); return; default: return; } } public void OnDestroy() { if (!_initialized) { return; } CleanupAllCombatEntities(); CloseCombatFinishForm(); _enemyManager.OnDestroy(); ResetRuntime(); _eventBridge.Unbind(); _combatFinishFormUseCase = null; _entity = null; _initialized = false; } public bool TryEndCombatByPlayer() { if (_state != SchedulerState.RunningPhase) { return false; } if (!_phaseLoopRuntime.TryRequestEndCombat()) { return false; } EnterFinishFlow("Combat ended by player.", true); return true; } private void UpdateCurrentPhase(float elapseSeconds, float realElapseSeconds) { DRLevelPhase currentPhase = _phaseLoopRuntime.CurrentPhase; if (currentPhase == null) { EnterFailureFallback("CombatScheduler update failed. Current phase is null."); return; } _phaseLoopRuntime.AdvancePhaseElapsed(elapseSeconds); _enemyManager.OnUpdate(elapseSeconds, realElapseSeconds); if (!_phaseLoopRuntime.ShouldEndCurrentPhase(_enemyManager.IsPhaseSpawnCompleted, _enemyManager.AliveEnemyCount)) { return; } CompleteCurrentPhase(); } private void CompleteCurrentPhase() { _enemyManager.EndPhase(); Log.Info( "CombatScheduler phase completed. Level={0}, Phase={1}, Elapsed={2:F2}s.", _currentLevel != null ? _currentLevel.Id : 0, _phaseLoopRuntime.CurrentPhase != null ? _phaseLoopRuntime.CurrentPhase.Id : 0, _phaseLoopRuntime.CurrentPhaseElapsed); BeginNextPhase(); } private void BeginNextPhase() { if (!_phaseLoopRuntime.TryEnterNextPhase(out DRLevelPhase nextPhase)) { EnterFinishFlow("Combat ended after loop completion.", true); return; } _spawnEntriesByPhaseId.TryGetValue(nextPhase.Id, out IReadOnlyList spawnEntries); _enemyManager.BeginPhase(nextPhase, spawnEntries); _state = SchedulerState.RunningPhase; GameEntry.Event.Fire( this, CombatProcessEventArgs.Create(_phaseLoopRuntime.DisplayPhaseIndex, _phaseLoopRuntime.PhaseCount)); GameEntry.Event.Fire( this, CombatEnemyHpRateChangedEventArgs.Create( ResolveEnemyHpRateMultiplier(_phaseLoopRuntime.DisplayPhaseIndex, _phaseLoopRuntime.PhaseCount))); Log.Info( "CombatScheduler phase started. Level={0}, Phase={1}, EndType={2}, Entries={3}.", _currentLevel != null ? _currentLevel.Id : 0, _phaseLoopRuntime.DisplayPhaseIndex, nextPhase.EndType, spawnEntries != null ? spawnEntries.Count : 0); } private void EnterFinishFlow(string reason, bool isVictory) { int defeatedEnemyCount = _enemyManager.DefeatedEnemyCount; BackpackInventoryData rewardInventory = _combatResourceManager.GetRewardInventorySnapshot(); // Step 1: stop runtime and clear enemy entities only. _enemyManager.EndPhase(); _state = SchedulerState.WaitingForFinishReturn; _isFinishAsVictory = isVictory; _enemyManager.CleanupTrackedEnemies(); Log.Info( "CombatScheduler entered finish flow. Level={0}. Reason={1}", _currentLevel != null ? _currentLevel.Id : 0, reason); OpenCombatFinishForm(defeatedEnemyCount, _combatResourceManager.GainedGold, rewardInventory); } public void OnEnemyReachedBase(int baseDamage) { if (_state != SchedulerState.RunningPhase) { return; } int resolvedBaseDamage = Mathf.Max(0, baseDamage); if (resolvedBaseDamage <= 0) { return; } CombatNodeComponent combatNode = GameEntry.CombatNode; if (combatNode == null) { return; } int currentBaseHp = combatNode.ApplyBaseDamage(resolvedBaseDamage); if (currentBaseHp > 0) { return; } EnterFinishFlow("Combat ended because base HP reached zero.", false); } private void ResetRuntime() { _phaseBuffer.Clear(); _spawnEntriesByPhaseId.Clear(); _phaseLoopRuntime.Reset(); _loadSession.Reset(); _combatResourceManager.Reset(); _currentLevel = null; _isFinishAsVictory = true; _state = SchedulerState.Idle; } private void CleanupAllCombatEntities() { _loadSession.Cleanup(); _enemyManager.CleanupTrackedEnemies(); } private void EnsureCombatFinishFormUseCaseBound() { _combatFinishFormUseCase ??= new CombatFinishFormUseCase(this); GameEntry.UIRouter.BindUIUseCase(UIFormType.CombatFinishForm, _combatFinishFormUseCase); } private void OpenCombatFinishForm(int defeatedEnemyCount, int gainedGold, BackpackInventoryData rewardInventory) { EnsureCombatFinishFormUseCaseBound(); _combatFinishFormUseCase.SetSummary( defeatedEnemyCount, gainedGold, rewardInventory); GameEntry.UIRouter.OpenUI(UIFormType.CombatFinishForm); } public void OnEnemyDefeatedRewardResolved(int gainedCoin, int gainedGold) { _combatResourceManager.AddEnemyDefeatedReward(gainedCoin, gainedGold); if (_state != SchedulerState.RunningPhase) { return; } _combatResourceManager.TryRollOutGameItemDrop( _phaseLoopRuntime.DisplayPhaseIndex, ResolveCurrentThemeType()); } private LevelThemeType ResolveCurrentThemeType() { if (_currentLevel != null) { return _currentLevel.LevelThemeType; } if (GameEntry.CombatNode != null) { return GameEntry.CombatNode.CurrentThemeType; } return LevelThemeType.None; } private void CloseCombatFinishForm() { GameEntry.UIRouter.CloseUI(UIFormType.CombatFinishForm); } public bool OnCombatFinishReturnRequested() { if (_state != SchedulerState.WaitingForFinishReturn) { return false; } // Step 2: clear remaining map/UI resources and close finish form. _loadSession.Cleanup(); CloseCombatFinishForm(); _state = SchedulerState.Completed; GameEntry.CombatNode?.OnCombatEndedByScheduler(_isFinishAsVictory); return true; } private bool HandleStartFailure(string errorMessage) { Log.Warning("{0}", errorMessage); _state = SchedulerState.Failed; _enemyManager.EndPhase(); CleanupAllCombatEntities(); CloseCombatFinishForm(); ResetRuntime(); return false; } private void EnterFailureFallback(string errorMessage) { if (_state == SchedulerState.Failed || _state == SchedulerState.Completed) { return; } _state = SchedulerState.Failed; Log.Error("CombatScheduler failed. LevelId={0}, {1}", _currentLevel != null ? _currentLevel.Id : 0, errorMessage); _enemyManager.EndPhase(); CleanupAllCombatEntities(); CloseCombatFinishForm(); GameEntry.CombatNode?.OnCombatEndedByScheduler(false); } private static int ResolveEnemyHpRateMultiplier(int displayPhaseIndex, int phaseCount) { if (displayPhaseIndex <= 0 || phaseCount <= 0) { return 1; } int completedLoopCount = Mathf.Max(0, (displayPhaseIndex - 1) / phaseCount); if (completedLoopCount >= 30) { return int.MaxValue; } return 1 << completedLoopCount; } #region Event Handlers private void OnShowEntitySuccess(ShowEntitySuccessEventArgs args) { var status = _loadSession.HandleShowEntitySuccess(args, out string errorMessage); if (status == CombatLoadSession.EventHandleStatus.Failed) { EnterFailureFallback(errorMessage); return; } if (status == CombatLoadSession.EventHandleStatus.Succeeded) { MapEntity map = _loadSession.CurrentMap; Log.Info( "Map ready. LevelId={0}, PathCells={1}, FoundationCells={2}, Spawners={3}, House={4}.", _currentLevel != null ? _currentLevel.Id : 0, map.PathCells.Count, map.FoundationCells.Count, map.Spawners.Length, map.House != null ? map.House.name : "None"); } } private void OnShowEntityFailure(ShowEntityFailureEventArgs args) { var status = _loadSession.HandleShowEntityFailure(args, out string errorMessage); if (status == CombatLoadSession.EventHandleStatus.Failed) { EnterFailureFallback(errorMessage); } } private void OnHideEntityComplete(HideEntityCompleteEventArgs args) { _loadSession.HandleHideEntityComplete(args); } private void OnOpenUIFormSuccess(OpenUIFormSuccessEventArgs args) { var status = _loadSession.HandleOpenUIFormSuccess(args, out string errorMessage); if (status == CombatLoadSession.EventHandleStatus.Failed) { EnterFailureFallback(errorMessage); } } private void OnOpenUIFormFailure(OpenUIFormFailureEventArgs args) { var status = _loadSession.HandleOpenUIFormFailure(args, out string errorMessage); if (status == CombatLoadSession.EventHandleStatus.Failed) { EnterFailureFallback(errorMessage); } } private void OnCloseUIFormComplete(CloseUIFormCompleteEventArgs args) { _loadSession.HandleCloseUIFormComplete(args); } #endregion } }