diff --git a/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/CombatLoadSession.cs b/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/CombatLoadSession.cs index 0690feb..100b76f 100644 --- a/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/CombatLoadSession.cs +++ b/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/CombatLoadSession.cs @@ -45,7 +45,7 @@ namespace GeometryTD.CustomComponent _currentMap = null; } - public bool StartLoading(DRLevel level, MapData mapData, CombatScheduler scheduler, out string errorMessage) + public bool StartLoading(DRLevel level, MapEntityLoadContext mapLoadContext, CombatScheduler scheduler, out string errorMessage) { errorMessage = null; if (_entity == null) @@ -54,7 +54,7 @@ namespace GeometryTD.CustomComponent return false; } - if (!TryShowMap(level, mapData, out errorMessage)) + if (!TryShowMap(level, mapLoadContext, out errorMessage)) { return false; } @@ -224,7 +224,7 @@ namespace GeometryTD.CustomComponent } } - private bool TryShowMap(DRLevel level, MapData mapData, out string errorMessage) + private bool TryShowMap(DRLevel level, MapEntityLoadContext mapLoadContext, out string errorMessage) { errorMessage = null; if (level == null) @@ -242,10 +242,14 @@ namespace GeometryTD.CustomComponent } _loadingMapEntityId = _entity.GenerateSerialId(); - MapData resolvedMapData = mapData != null - ? mapData.CloneForEntity(_loadingMapEntityId, Vector3.zero) + MapData resolvedMapData = mapLoadContext?.InitialMapData != null + ? mapLoadContext.InitialMapData.CloneForEntity(_loadingMapEntityId, Vector3.zero) : new MapData(_loadingMapEntityId, level.Id, Vector3.zero); - _entity.ShowMap(resolvedMapData, mapAssetName); + MapEntityLoadContext resolvedLoadContext = new MapEntityLoadContext( + resolvedMapData, + mapLoadContext?.TryConsumeCoin, + mapLoadContext?.AddCoin); + _entity.ShowMap(resolvedLoadContext, mapAssetName); return true; } diff --git a/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/CombatScheduler.cs b/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/CombatScheduler.cs index 15bd847..86f3322 100644 --- a/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/CombatScheduler.cs +++ b/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/CombatScheduler.cs @@ -11,65 +11,54 @@ namespace GeometryTD.CustomComponent { public partial class CombatScheduler { - 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 CombatInRunResourceManager _combatInRunResourceManager = new(); - private readonly EnemyDropResolver _enemyDropResolver = new(); - private readonly CombatSettlementFlowService _settlementFlowService = new(); + private readonly CombatSchedulerRuntimeContext _context = new(); + private readonly CombatSchedulerFlowCoordinator _flowCoordinator; - private EntityComponent _entity; - private DRLevel _currentLevel; - private CombatFinishFormUseCase _combatFinishFormUseCase; - private RewardSelectFormUseCase _rewardSelectFormUseCase; - private CombatStateBase _currentState; private bool _initialized; - private bool _isFinishAsVictory = true; - private bool _isCompleted; - private bool _nodeEnterFired; - private CombatSettlementContext _settlementContext; + + public CombatScheduler() + { + _flowCoordinator = new CombatSchedulerFlowCoordinator(this, _context); + } public bool IsRunning => - _currentState is CombatLoadingState or CombatRunningPhaseState or CombatWaitingForPhaseEndState; + _context.CurrentState is CombatLoadingState or CombatRunningPhaseState or CombatWaitingForPhaseEndState; - public bool IsCompleted => _isCompleted; - 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 CurrentCoin => _combatInRunResourceManager.CurrentCoin; - public int CurrentGold => _combatInRunResourceManager.CurrentGold; - public int CurrentBaseHp => _combatInRunResourceManager.CurrentBaseHp; - public int CurrentBuildTowerCount => _combatInRunResourceManager.CurrentBuildTowerCount; - public int DefeatedEnemyCount => _enemyManager.DefeatedEnemyCount; - public int GainedCoin => _combatInRunResourceManager.GainedCoin; - public int GainedGold => _combatInRunResourceManager.GainedGold; + public bool IsCompleted => _context.IsCompleted; + public DRLevel CurrentLevel => _context.CurrentLevel; + public DRLevelPhase CurrentPhase => _context.PhaseLoopRuntime.CurrentPhase; + public MapEntity CurrentMap => _context.LoadSession.CurrentMap; + public int DisplayPhaseIndex => _context.PhaseLoopRuntime.DisplayPhaseIndex; + public int PhaseCount => _context.PhaseLoopRuntime.PhaseCount; + public bool CanEndCombat => _context.PhaseLoopRuntime.CanEndCombat; + public int CurrentCoin => _context.CombatInRunResourceManager.CurrentCoin; + public int CurrentGold => _context.CombatInRunResourceManager.CurrentGold; + public int CurrentBaseHp => _context.CombatInRunResourceManager.CurrentBaseHp; + public int CurrentBuildTowerCount => _context.CombatInRunResourceManager.CurrentBuildTowerCount; + public int DefeatedEnemyCount => _context.EnemyManager.DefeatedEnemyCount; + public int GainedCoin => _context.CombatInRunResourceManager.GainedCoin; + public int GainedGold => _context.CombatInRunResourceManager.GainedGold; public void OnInit() { if (!_initialized) { - _entity = GameEntry.Entity; - _eventBridge.Bind( + _context.Entity = GameEntry.Entity; + _context.EventBridge.Bind( OnShowEntitySuccess, OnShowEntityFailure, OnHideEntityComplete, OnOpenUIFormSuccess, OnOpenUIFormFailure, OnCloseUIFormComplete); - _enemyManager.OnInit(this); - _loadSession.OnInit(_entity); - EnsureCombatFinishFormUseCaseBound(); - EnsureRewardSelectFormUseCaseBound(); + _context.EnemyManager.OnInit(this); + _context.LoadSession.OnInit(_context.Entity); + _flowCoordinator.EnsureCombatFinishFormUseCaseBound(); + _flowCoordinator.EnsureRewardSelectFormUseCaseBound(); _initialized = true; } - ResetRuntime(); + _flowCoordinator.ResetRuntime(); } public bool Start( @@ -77,32 +66,33 @@ namespace GeometryTD.CustomComponent IReadOnlyList phases, IReadOnlyDictionary> spawnEntriesByPhaseId) { - if (!_initialized || _entity == null) + if (!_initialized || _context.Entity == null) { - return HandleStartFailure("CombatScheduler start failed. Runtime is not initialized."); + return _flowCoordinator.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."); + return _flowCoordinator.HandleStartFailure("CombatScheduler start failed. Invalid level or phase data."); } - CleanupAllCombatEntities(); - CloseCombatFinishForm(); - CloseRewardSelectForm(); - _enemyManager.EndPhase(); - _enemyManager.ResetCombatStats(); - ResetRuntime(); - _isFinishAsVictory = true; + _flowCoordinator.CleanupAllCombatEntities(); + _flowCoordinator.CloseCombatFinishForm(); + _flowCoordinator.CloseRewardSelectForm(); + _flowCoordinator.CloseDialogForm(); + _context.EnemyManager.EndPhase(); + _context.EnemyManager.ResetCombatStats(); + _flowCoordinator.ResetRuntime(); + _context.IsFinishAsVictory = true; - _currentLevel = level; - _combatInRunResourceManager.InitializeForCombat(level); + _context.CurrentLevel = level; + _context.CombatInRunResourceManager.InitializeForCombat(level); for (int i = 0; i < phases.Count; i++) { DRLevelPhase phase = phases[i]; if (phase != null) { - _phaseBuffer.Add(phase); + _context.PhaseBuffer.Add(phase); } } @@ -110,27 +100,27 @@ namespace GeometryTD.CustomComponent { foreach (var pair in spawnEntriesByPhaseId) { - _spawnEntriesByPhaseId[pair.Key] = pair.Value; + _context.SpawnEntriesByPhaseId[pair.Key] = pair.Value; } } - _phaseLoopRuntime.SetPhases(_phaseBuffer); - if (_phaseLoopRuntime.PhaseCount <= 0) + _context.PhaseLoopRuntime.SetPhases(_context.PhaseBuffer); + if (_context.PhaseLoopRuntime.PhaseCount <= 0) { - return HandleStartFailure($"CombatScheduler start failed. Level '{level.Id}' has no phase data."); + return _flowCoordinator.HandleStartFailure($"CombatScheduler start failed. Level '{level.Id}' has no phase data."); } - ChangeState(new CombatLoadingState(this)); + ChangeState(new CombatLoadingState(_context, _flowCoordinator)); Log.Info( "CombatScheduler started. Level={0}, PhaseCount={1}.", - _currentLevel.Id, - _phaseLoopRuntime.PhaseCount); + _context.CurrentLevel.Id, + _context.PhaseLoopRuntime.PhaseCount); return true; } public void OnUpdate(float elapseSeconds, float realElapseSeconds) { - _currentState?.OnUpdate(elapseSeconds, realElapseSeconds); + _context.CurrentState?.OnUpdate(elapseSeconds, realElapseSeconds); } public void OnDestroy() @@ -140,31 +130,32 @@ namespace GeometryTD.CustomComponent return; } - _currentState?.OnExit(); - _currentState?.OnDestroy(); - _currentState = null; - CleanupAllCombatEntities(); - CloseCombatFinishForm(); - CloseRewardSelectForm(); - _enemyManager.OnDestroy(); - ResetRuntime(); - _eventBridge.Unbind(); - _combatFinishFormUseCase = null; - _rewardSelectFormUseCase = null; + _context.CurrentState?.OnExit(); + _context.CurrentState?.OnDestroy(); + _context.CurrentState = null; + _flowCoordinator.CleanupAllCombatEntities(); + _flowCoordinator.CloseCombatFinishForm(); + _flowCoordinator.CloseRewardSelectForm(); + _flowCoordinator.CloseDialogForm(); + _context.EnemyManager.OnDestroy(); + _flowCoordinator.ResetRuntime(); + _context.EventBridge.Unbind(); + _context.CombatFinishFormUseCase = null; + _context.RewardSelectFormUseCase = null; - _entity = null; + _context.Entity = null; _initialized = false; } public bool TryEndCombatByPlayer() { - if (_currentState is not CombatRunningPhaseState && - _currentState is not CombatWaitingForPhaseEndState) + if (_context.CurrentState is not CombatRunningPhaseState && + _context.CurrentState is not CombatWaitingForPhaseEndState) { return false; } - return _phaseLoopRuntime.TryRequestEndCombat(); + return _context.PhaseLoopRuntime.TryRequestEndCombat(); } public void OnEnemyReachedBase(DREnemy enemy) @@ -180,7 +171,7 @@ namespace GeometryTD.CustomComponent return; } - ApplyBaseDamage(resolvedBaseDamage); + _flowCoordinator.ApplyBaseDamage(resolvedBaseDamage); } public void OnEnemyDefeated(DREnemy enemy) @@ -192,24 +183,24 @@ namespace GeometryTD.CustomComponent EnemyDropResolveContext context = new( enemy, - _phaseLoopRuntime.DisplayPhaseIndex, - ResolveCurrentThemeType()); - EnemyDropResolveResult result = _enemyDropResolver.Resolve(context); - _combatInRunResourceManager.AddEnemyDefeatedReward(result.Coin, result.Gold); + _context.PhaseLoopRuntime.DisplayPhaseIndex, + _flowCoordinator.ResolveCurrentThemeType()); + EnemyDropResolveResult result = _context.EnemyDropResolver.Resolve(context); + _context.CombatInRunResourceManager.AddEnemyDefeatedReward(result.Coin, result.Gold); if (!result.ShouldRollOutGameItem) { return; } - _combatInRunResourceManager.TryRollOutGameItemDrop( + _context.CombatInRunResourceManager.TryRollOutGameItemDrop( context.DisplayPhaseIndex, context.ThemeType); } public bool OnCombatFinishReturnRequested() { - if (_currentState is not CombatWaitingForReturnState waitingForReturnState) + if (_context.CurrentState is not CombatWaitingForReturnState waitingForReturnState) { return false; } @@ -220,294 +211,63 @@ namespace GeometryTD.CustomComponent public bool TryConsumeCoin(int coin) { - return _combatInRunResourceManager.TryConsumeCoin(coin); + return _context.CombatInRunResourceManager.TryConsumeCoin(coin); } public void AddCoin(int coin) { - _combatInRunResourceManager.AddCoin(coin); + _context.CombatInRunResourceManager.AddCoin(coin); } public bool TryGetBuildTowerStats(int buildIndex, out TowerStatsData stats) { - return _combatInRunResourceManager.TryGetBuildTowerStats(buildIndex, out stats); + return _context.CombatInRunResourceManager.TryGetBuildTowerStats(buildIndex, out stats); } public bool TryDebugFail(string errorMessage) { - if (_isCompleted || _currentState == null || _currentState is CombatFailedState) + if (_context.IsCompleted || _context.CurrentState == null || _context.CurrentState is CombatFailedState) { return false; } - EnterFailureFallback(string.IsNullOrWhiteSpace(errorMessage) + _flowCoordinator.EnterFailureFallback(string.IsNullOrWhiteSpace(errorMessage) ? "Manual debug fail." : errorMessage); - return _currentState is CombatFailedState; + return _context.CurrentState is CombatFailedState; } - private void ResetRuntime() + internal void ChangeState(CombatStateBase nextState) { - _currentState = null; - _phaseBuffer.Clear(); - _spawnEntriesByPhaseId.Clear(); - _phaseLoopRuntime.Reset(); - _loadSession.Reset(); - _combatInRunResourceManager.Reset(); - _settlementContext = null; - _currentLevel = null; - _isFinishAsVictory = true; - _isCompleted = false; - _nodeEnterFired = false; - } - - private void CleanupAllCombatEntities() - { - _loadSession.Cleanup(); - _enemyManager.CleanupTrackedEnemies(); - } - - private void EnsureCombatFinishFormUseCaseBound() - { - _combatFinishFormUseCase ??= new CombatFinishFormUseCase(this); - GameEntry.UIRouter.BindUIUseCase(UIFormType.CombatFinishForm, _combatFinishFormUseCase); - } - - private void EnsureRewardSelectFormUseCaseBound() - { - _rewardSelectFormUseCase ??= new RewardSelectFormUseCase(); - GameEntry.UIRouter.BindUIUseCase(UIFormType.RewardSelectForm, _rewardSelectFormUseCase); - } - - private void OpenCombatFailureDialog(string errorMessage) - { - CloseDialogForm(); - GameEntry.UIRouter.OpenUI(UIFormType.DialogForm, new DialogFormRawData - { - Mode = 1, - Title = "Combat Error", - Message = string.IsNullOrWhiteSpace(errorMessage) ? "Combat failed unexpectedly." : errorMessage, - PauseGame = false, - ConfirmText = "Return Menu", - OnClickConfirm = OnCombatFailureDialogConfirmed - }); - } - - private void ChangeState(CombatStateBase nextState) - { - if (ReferenceEquals(_currentState, nextState)) + if (ReferenceEquals(_context.CurrentState, nextState)) { return; } - _currentState?.OnExit(); - _currentState?.OnDestroy(); - _currentState = nextState; - _currentState?.OnInit(); - _currentState?.OnEnter(); - } - - private bool TryBeginNextPhase() - { - if (!_phaseLoopRuntime.TryEnterNextPhase(out DRLevelPhase nextPhase)) - { - ChangeState(new CombatSettlementState(this, "Combat ended after loop completion.", true)); - return false; - } - - _spawnEntriesByPhaseId.TryGetValue(nextPhase.Id, out IReadOnlyList spawnEntries); - ChangeState(new CombatRunningPhaseState(this, nextPhase, spawnEntries)); - return true; - } - - private void EnterWaitingForPhaseEnd() - { - ChangeState(new CombatWaitingForPhaseEndState(this)); - } - - 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); - - TryBeginNextPhase(); - } - - private bool ShouldEnterSettlementFromActiveState(out string reason, out bool isVictory) - { - if (GetCurrentBaseHp() <= 0) - { - reason = "Combat ended because base HP reached zero."; - isVictory = false; - return true; - } - - if (_phaseLoopRuntime.IsEndCombatRequested) - { - reason = "Combat ended by player."; - isVictory = true; - return true; - } - - reason = null; - isVictory = true; - return false; - } - - private void OnFullBaseHpRewardSelected(RewardSelectItemRawData selectedReward) - { - if (_currentState is not CombatRewardSelectionState || _settlementContext == null) - { - return; - } - - _settlementFlowService.ApplySelectedReward(_settlementContext, selectedReward); - ChangeState(new CombatFinishFormState(this)); - } - - private void OnFullBaseHpRewardGiveUp() - { - if (_currentState is not CombatRewardSelectionState || _settlementContext == null) - { - return; - } - - ChangeState(new CombatFinishFormState(this)); - } - - private LevelThemeType ResolveCurrentThemeType() - { - if (_currentLevel != null) - { - return _currentLevel.LevelThemeType; - } - - if (GameEntry.CombatNode != null) - { - return GameEntry.CombatNode.CurrentThemeType; - } - - return LevelThemeType.None; - } - - private int ApplyBaseDamage(int damage) - { - return _combatInRunResourceManager.ApplyBaseDamage(damage); - } - - private int GetCurrentBaseHp() - { - return Mathf.Max(0, _combatInRunResourceManager.CurrentBaseHp); - } - - private void CloseCombatFinishForm() - { - GameEntry.UIRouter.CloseUI(UIFormType.CombatFinishForm); - } - - private void CloseRewardSelectForm() - { - GameEntry.UIRouter.CloseUI(UIFormType.RewardSelectForm); - } - - private void CloseDialogForm() - { - GameEntry.UIRouter.CloseUI(UIFormType.DialogForm); - } - - private void CompleteCombat(bool succeeded) - { - _isCompleted = true; - _currentState = null; - _combatInRunResourceManager.MarkCombatEnded(); - GameEntry.CombatNode?.OnCombatEndedByScheduler(succeeded); - } - - private void CompleteNormalCombatAndNotify(bool succeeded) - { - CompleteCombat(succeeded); - GameEntry.Event.Fire(this, NodeCompleteEventArgs.Create()); - } - - private void CompleteFailureCombatAndNotify() - { - CleanupAllCombatEntities(); - CloseCombatFinishForm(); - CloseRewardSelectForm(); - CloseDialogForm(); - CompleteCombat(false); - GameEntry.Event.Fire(this, CombatFailureReturnEventArgs.Create()); - } - - private bool HandleStartFailure(string errorMessage) - { - Log.Warning("{0}", errorMessage); - _enemyManager.EndPhase(); - CleanupAllCombatEntities(); - CloseCombatFinishForm(); - CloseRewardSelectForm(); - ResetRuntime(); - return false; - } - - private void EnterFailureFallback(string errorMessage) - { - if (_currentState is CombatFailedState || _isCompleted) - { - return; - } - - ChangeState(new CombatFailedState(this, errorMessage)); - } - - 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; - } - - private void OnCombatFailureDialogConfirmed(object userData) - { - _ = userData; - if (_currentState is not CombatFailedState || _isCompleted) - { - return; - } - - CompleteFailureCombatAndNotify(); + _context.CurrentState?.OnExit(); + _context.CurrentState?.OnDestroy(); + _context.CurrentState = nextState; + _context.CurrentState?.OnInit(); + _context.CurrentState?.OnEnter(); } #region Event Handlers private void OnShowEntitySuccess(ShowEntitySuccessEventArgs args) { - var status = _loadSession.HandleShowEntitySuccess(args, out string errorMessage); + var status = _context.LoadSession.HandleShowEntitySuccess(args, out string errorMessage); if (status == CombatLoadSession.EventHandleStatus.Failed) { - EnterFailureFallback(errorMessage); + _flowCoordinator.EnterFailureFallback(errorMessage); return; } if (status == CombatLoadSession.EventHandleStatus.Succeeded) { - MapEntity map = _loadSession.CurrentMap; + MapEntity map = _context.LoadSession.CurrentMap; Log.Info( "Map ready. LevelId={0}, PathCells={1}, FoundationCells={2}, Spawners={3}, House={4}.", - _currentLevel != null ? _currentLevel.Id : 0, + _context.CurrentLevel != null ? _context.CurrentLevel.Id : 0, map.PathCells.Count, map.FoundationCells.Count, map.Spawners.Length, @@ -517,39 +277,39 @@ namespace GeometryTD.CustomComponent private void OnShowEntityFailure(ShowEntityFailureEventArgs args) { - var status = _loadSession.HandleShowEntityFailure(args, out string errorMessage); + var status = _context.LoadSession.HandleShowEntityFailure(args, out string errorMessage); if (status == CombatLoadSession.EventHandleStatus.Failed) { - EnterFailureFallback(errorMessage); + _flowCoordinator.EnterFailureFallback(errorMessage); } } private void OnHideEntityComplete(HideEntityCompleteEventArgs args) { - _loadSession.HandleHideEntityComplete(args); + _context.LoadSession.HandleHideEntityComplete(args); } private void OnOpenUIFormSuccess(OpenUIFormSuccessEventArgs args) { - var status = _loadSession.HandleOpenUIFormSuccess(args, out string errorMessage); + var status = _context.LoadSession.HandleOpenUIFormSuccess(args, out string errorMessage); if (status == CombatLoadSession.EventHandleStatus.Failed) { - EnterFailureFallback(errorMessage); + _flowCoordinator.EnterFailureFallback(errorMessage); } } private void OnOpenUIFormFailure(OpenUIFormFailureEventArgs args) { - var status = _loadSession.HandleOpenUIFormFailure(args, out string errorMessage); + var status = _context.LoadSession.HandleOpenUIFormFailure(args, out string errorMessage); if (status == CombatLoadSession.EventHandleStatus.Failed) { - EnterFailureFallback(errorMessage); + _flowCoordinator.EnterFailureFallback(errorMessage); } } private void OnCloseUIFormComplete(CloseUIFormCompleteEventArgs args) { - _loadSession.HandleCloseUIFormComplete(args); + _context.LoadSession.HandleCloseUIFormComplete(args); } #endregion diff --git a/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/CombatSchedulerFlowCoordinator.cs b/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/CombatSchedulerFlowCoordinator.cs new file mode 100644 index 0000000..d39a0bb --- /dev/null +++ b/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/CombatSchedulerFlowCoordinator.cs @@ -0,0 +1,256 @@ +using System.Collections.Generic; +using GeometryTD.CustomEvent; +using GeometryTD.DataTable; +using GeometryTD.Definition; +using GeometryTD.UI; +using UnityEngine; +using UnityGameFramework.Runtime; + +namespace GeometryTD.CustomComponent +{ + internal sealed class CombatSchedulerFlowCoordinator + { + private readonly CombatScheduler _scheduler; + private readonly CombatSchedulerRuntimeContext _context; + + public CombatScheduler Scheduler => _scheduler; + + public CombatSchedulerFlowCoordinator(CombatScheduler scheduler, CombatSchedulerRuntimeContext context) + { + _scheduler = scheduler; + _context = context; + } + + public 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; + } + + public void ResetRuntime() + { + _context.CurrentState = null; + _context.PhaseBuffer.Clear(); + _context.SpawnEntriesByPhaseId.Clear(); + _context.PhaseLoopRuntime.Reset(); + _context.LoadSession.Reset(); + _context.CombatInRunResourceManager.Reset(); + _context.SettlementContext = null; + _context.CurrentLevel = null; + _context.IsFinishAsVictory = true; + _context.IsCompleted = false; + _context.NodeEnterFired = false; + } + + public void CleanupAllCombatEntities() + { + _context.LoadSession.Cleanup(); + _context.EnemyManager.CleanupTrackedEnemies(); + } + + public void EnsureCombatFinishFormUseCaseBound() + { + _context.CombatFinishFormUseCase ??= new CombatFinishFormUseCase(_scheduler); + GameEntry.UIRouter.BindUIUseCase(UIFormType.CombatFinishForm, _context.CombatFinishFormUseCase); + } + + public void EnsureRewardSelectFormUseCaseBound() + { + _context.RewardSelectFormUseCase ??= new RewardSelectFormUseCase(); + GameEntry.UIRouter.BindUIUseCase(UIFormType.RewardSelectForm, _context.RewardSelectFormUseCase); + } + + public void OpenCombatFailureDialog(string errorMessage) + { + CloseDialogForm(); + GameEntry.UIRouter.OpenUI(UIFormType.DialogForm, new DialogFormRawData + { + Mode = 1, + Title = "Combat Error", + Message = string.IsNullOrWhiteSpace(errorMessage) ? "Combat failed unexpectedly." : errorMessage, + PauseGame = false, + ConfirmText = "Return Menu", + OnClickConfirm = OnCombatFailureDialogConfirmed + }); + } + + public bool TryBeginNextPhase() + { + if (!_context.PhaseLoopRuntime.TryEnterNextPhase(out DRLevelPhase nextPhase)) + { + _scheduler.ChangeState(new CombatSettlementState(_context, this, "Combat ended after loop completion.", true)); + return false; + } + + _context.SpawnEntriesByPhaseId.TryGetValue(nextPhase.Id, out IReadOnlyList spawnEntries); + _scheduler.ChangeState(new CombatRunningPhaseState(_context, this, nextPhase, spawnEntries)); + return true; + } + + public void EnterWaitingForPhaseEnd() + { + _scheduler.ChangeState(new CombatWaitingForPhaseEndState(_context, this)); + } + + public void CompleteCurrentPhase() + { + _context.EnemyManager.EndPhase(); + Log.Info( + "CombatScheduler phase completed. Level={0}, Phase={1}, Elapsed={2:F2}s.", + _context.CurrentLevel != null ? _context.CurrentLevel.Id : 0, + _context.PhaseLoopRuntime.CurrentPhase != null ? _context.PhaseLoopRuntime.CurrentPhase.Id : 0, + _context.PhaseLoopRuntime.CurrentPhaseElapsed); + + TryBeginNextPhase(); + } + + public bool ShouldEnterSettlementFromActiveState(out string reason, out bool isVictory) + { + if (GetCurrentBaseHp() <= 0) + { + reason = "Combat ended because base HP reached zero."; + isVictory = false; + return true; + } + + if (_context.PhaseLoopRuntime.IsEndCombatRequested) + { + reason = "Combat ended by player."; + isVictory = true; + return true; + } + + reason = null; + isVictory = true; + return false; + } + + public void OnFullBaseHpRewardSelected(RewardSelectItemRawData selectedReward) + { + if (_context.CurrentState is not CombatRewardSelectionState || _context.SettlementContext == null) + { + return; + } + + _context.SettlementFlowService.ApplySelectedReward(_context.SettlementContext, selectedReward); + _scheduler.ChangeState(new CombatFinishFormState(_context, this)); + } + + public void OnFullBaseHpRewardGiveUp() + { + if (_context.CurrentState is not CombatRewardSelectionState || _context.SettlementContext == null) + { + return; + } + + _scheduler.ChangeState(new CombatFinishFormState(_context, this)); + } + + public LevelThemeType ResolveCurrentThemeType() + { + if (_context.CurrentLevel != null) + { + return _context.CurrentLevel.LevelThemeType; + } + + if (GameEntry.CombatNode != null) + { + return GameEntry.CombatNode.CurrentThemeType; + } + + return LevelThemeType.None; + } + + public int ApplyBaseDamage(int damage) + { + return _context.CombatInRunResourceManager.ApplyBaseDamage(damage); + } + + public int GetCurrentBaseHp() + { + return Mathf.Max(0, _context.CombatInRunResourceManager.CurrentBaseHp); + } + + public void CloseCombatFinishForm() + { + GameEntry.UIRouter.CloseUI(UIFormType.CombatFinishForm); + } + + public void CloseRewardSelectForm() + { + GameEntry.UIRouter.CloseUI(UIFormType.RewardSelectForm); + } + + public void CloseDialogForm() + { + GameEntry.UIRouter.CloseUI(UIFormType.DialogForm); + } + + public void CompleteNormalCombatAndNotify(bool succeeded) + { + CompleteCombat(succeeded); + GameEntry.Event.Fire(this, NodeCompleteEventArgs.Create()); + } + + public void CompleteFailureCombatAndNotify() + { + CleanupAllCombatEntities(); + CloseCombatFinishForm(); + CloseRewardSelectForm(); + CloseDialogForm(); + CompleteCombat(false); + GameEntry.Event.Fire(this, CombatFailureReturnEventArgs.Create()); + } + + public bool HandleStartFailure(string errorMessage) + { + Log.Warning("{0}", errorMessage); + _context.EnemyManager.EndPhase(); + CleanupAllCombatEntities(); + CloseCombatFinishForm(); + CloseRewardSelectForm(); + CloseDialogForm(); + ResetRuntime(); + return false; + } + + public void EnterFailureFallback(string errorMessage) + { + if (_context.CurrentState is CombatFailedState || _context.IsCompleted) + { + return; + } + + _scheduler.ChangeState(new CombatFailedState(_context, this, errorMessage)); + } + + public void OnCombatFailureDialogConfirmed(object userData) + { + _ = userData; + if (_context.CurrentState is not CombatFailedState || _context.IsCompleted) + { + return; + } + + CompleteFailureCombatAndNotify(); + } + + private void CompleteCombat(bool succeeded) + { + _context.IsCompleted = true; + _context.CurrentState = null; + _context.CombatInRunResourceManager.MarkCombatEnded(); + GameEntry.CombatNode?.OnCombatEndedByScheduler(succeeded); + } + } +} diff --git a/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/CombatSchedulerFlowCoordinator.cs.meta b/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/CombatSchedulerFlowCoordinator.cs.meta new file mode 100644 index 0000000..4bbdf67 --- /dev/null +++ b/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/CombatSchedulerFlowCoordinator.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 672e14aaea7d4135874ef7f990eb78f2 +timeCreated: 1772865001 diff --git a/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/CombatSchedulerRuntimeContext.cs b/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/CombatSchedulerRuntimeContext.cs new file mode 100644 index 0000000..e7c1f29 --- /dev/null +++ b/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/CombatSchedulerRuntimeContext.cs @@ -0,0 +1,31 @@ +using System.Collections.Generic; +using GeometryTD.DataTable; +using GeometryTD.Entity; +using GeometryTD.UI; +using UnityGameFramework.Runtime; + +namespace GeometryTD.CustomComponent +{ + internal sealed class CombatSchedulerRuntimeContext + { + public List PhaseBuffer { get; } = new(); + public Dictionary> SpawnEntriesByPhaseId { get; } = new(); + public EnemyManager EnemyManager { get; } = new(); + public PhaseLoopRuntime PhaseLoopRuntime { get; } = new(); + public CombatLoadSession LoadSession { get; } = new(); + public CombatEventBridge EventBridge { get; } = new(); + public CombatInRunResourceManager CombatInRunResourceManager { get; } = new(); + public EnemyDropResolver EnemyDropResolver { get; } = new(); + public CombatSettlementFlowService SettlementFlowService { get; } = new(); + + public EntityComponent Entity { get; set; } + public DRLevel CurrentLevel { get; set; } + public CombatFinishFormUseCase CombatFinishFormUseCase { get; set; } + public RewardSelectFormUseCase RewardSelectFormUseCase { get; set; } + public CombatStateBase CurrentState { get; set; } + public bool IsFinishAsVictory { get; set; } = true; + public bool IsCompleted { get; set; } + public bool NodeEnterFired { get; set; } + public CombatSettlementContext SettlementContext { get; set; } + } +} diff --git a/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/CombatSchedulerRuntimeContext.cs.meta b/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/CombatSchedulerRuntimeContext.cs.meta new file mode 100644 index 0000000..6508cfb --- /dev/null +++ b/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/CombatSchedulerRuntimeContext.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: ebdf96ddef6945efa0e1446597a9c776 +timeCreated: 1772865000 diff --git a/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/CombatStates/CombatFailedState.cs b/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/CombatStates/CombatFailedState.cs index 34e67e9..74a22f4 100644 --- a/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/CombatStates/CombatFailedState.cs +++ b/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/CombatStates/CombatFailedState.cs @@ -2,33 +2,33 @@ using UnityGameFramework.Runtime; namespace GeometryTD.CustomComponent { - public partial class CombatScheduler + internal sealed class CombatFailedState : CombatStateBase { - private sealed class CombatFailedState : CombatStateBase + private readonly string _errorMessage; + + public CombatFailedState( + CombatSchedulerRuntimeContext context, + CombatSchedulerFlowCoordinator flow, + string errorMessage) : base(context, flow) { - private readonly string _errorMessage; + _errorMessage = errorMessage; + } - public CombatFailedState(CombatScheduler scheduler, string errorMessage) : base(scheduler) - { - _errorMessage = errorMessage; - } + public override void OnEnter() + { + Log.Error( + "CombatScheduler failed. LevelId={0}, {1}", + Context.CurrentLevel != null ? Context.CurrentLevel.Id : 0, + _errorMessage); + Context.EnemyManager.EndPhase(); + Flow.CloseCombatFinishForm(); + Flow.CloseRewardSelectForm(); + Flow.OpenCombatFailureDialog(_errorMessage); + } - public override void OnEnter() - { - Log.Error( - "CombatScheduler failed. LevelId={0}, {1}", - Scheduler._currentLevel != null ? Scheduler._currentLevel.Id : 0, - _errorMessage); - Scheduler._enemyManager.EndPhase(); - Scheduler.CloseCombatFinishForm(); - Scheduler.CloseRewardSelectForm(); - Scheduler.OpenCombatFailureDialog(_errorMessage); - } - - public override void OnExit() - { - Scheduler.CloseDialogForm(); - } + public override void OnExit() + { + Flow.CloseDialogForm(); } } } diff --git a/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/CombatStates/CombatFinishFormState.cs b/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/CombatStates/CombatFinishFormState.cs index c455e96..b7dee89 100644 --- a/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/CombatStates/CombatFinishFormState.cs +++ b/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/CombatStates/CombatFinishFormState.cs @@ -1,28 +1,26 @@ namespace GeometryTD.CustomComponent { - public partial class CombatScheduler + internal sealed class CombatFinishFormState : CombatStateBase { - private sealed class CombatFinishFormState : CombatStateBase + public CombatFinishFormState(CombatSchedulerRuntimeContext context, CombatSchedulerFlowCoordinator flow) + : base(context, flow) { - public CombatFinishFormState(CombatScheduler scheduler) : base(scheduler) + } + + public override void OnEnter() + { + if (Context.SettlementContext == null) { + Flow.EnterFailureFallback("Combat finish form failed. Settlement context is missing."); + return; } - public override void OnEnter() - { - if (Scheduler._settlementContext == null) - { - Scheduler.EnterFailureFallback("Combat finish form failed. Settlement context is missing."); - return; - } - - Scheduler._settlementFlowService.CommitSettlementInventory(Scheduler._settlementContext); - Scheduler.EnsureCombatFinishFormUseCaseBound(); - Scheduler._settlementFlowService.OpenCombatFinishForm( - Scheduler._settlementContext, - Scheduler._combatFinishFormUseCase); - Scheduler.ChangeState(new CombatWaitingForReturnState(Scheduler)); - } + Context.SettlementFlowService.CommitSettlementInventory(Context.SettlementContext); + Flow.EnsureCombatFinishFormUseCaseBound(); + Context.SettlementFlowService.OpenCombatFinishForm( + Context.SettlementContext, + Context.CombatFinishFormUseCase); + Flow.Scheduler.ChangeState(new CombatWaitingForReturnState(Context, Flow)); } } } diff --git a/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/CombatStates/CombatLoadingState.cs b/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/CombatStates/CombatLoadingState.cs index 33050ae..f35e2d7 100644 --- a/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/CombatStates/CombatLoadingState.cs +++ b/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/CombatStates/CombatLoadingState.cs @@ -6,70 +6,67 @@ using UnityEngine; namespace GeometryTD.CustomComponent { - public partial class CombatScheduler + internal sealed class CombatLoadingState : CombatStateBase { - private sealed class CombatLoadingState : CombatStateBase + public CombatLoadingState(CombatSchedulerRuntimeContext context, CombatSchedulerFlowCoordinator flow) + : base(context, flow) { - public CombatLoadingState(CombatScheduler scheduler) : base(scheduler) + } + + public override void OnEnter() + { + if (Context.CurrentLevel == null) { + Flow.EnterFailureFallback("Combat loading failed. Current level is null."); + return; } - public override void OnEnter() + MapEntityLoadContext mapLoadContext = BuildMapLoadContext(); + if (!Context.LoadSession.StartLoading(Context.CurrentLevel, mapLoadContext, Flow.Scheduler, out string errorMessage)) { - if (Scheduler._currentLevel == null) - { - Scheduler.EnterFailureFallback("Combat loading failed. Current level is null."); - return; - } + Flow.EnterFailureFallback($"Combat loading failed. {errorMessage}"); + } + } - MapData mapData = BuildMapData(); - if (!Scheduler._loadSession.StartLoading(Scheduler._currentLevel, mapData, Scheduler, out string errorMessage)) + public override void OnUpdate(float elapseSeconds, float realElapseSeconds) + { + _ = elapseSeconds; + _ = realElapseSeconds; + + if (!Context.LoadSession.IsReady) + { + return; + } + + Flow.TryBeginNextPhase(); + } + + private MapEntityLoadContext BuildMapLoadContext() + { + List buildTowerStatsSnapshot = new(); + for (int i = 0; i < Context.CombatInRunResourceManager.CurrentBuildTowerCount; i++) + { + if (Context.CombatInRunResourceManager.TryGetBuildTowerStats(i, out TowerStatsData stats) && + stats != null) { - Scheduler.EnterFailureFallback($"Combat loading failed. {errorMessage}"); + buildTowerStatsSnapshot.Add(stats); } } - public override void OnUpdate(float elapseSeconds, float realElapseSeconds) - { - _ = elapseSeconds; - _ = realElapseSeconds; - - if (!Scheduler._loadSession.IsReady) - { - return; - } - - Scheduler.TryBeginNextPhase(); - } - - private MapData BuildMapData() - { - List buildTowerStatsSnapshot = new(); - for (int i = 0; i < Scheduler._combatInRunResourceManager.CurrentBuildTowerCount; i++) - { - if (Scheduler._combatInRunResourceManager.TryGetBuildTowerStats(i, out TowerStatsData stats) && - stats != null) - { - buildTowerStatsSnapshot.Add(stats); - } - } - - return new MapData( - entityId: 0, - typeId: 0, - levelId: Scheduler._currentLevel.Id, - position: Vector3.zero, - initialCoin: Scheduler._combatInRunResourceManager.CurrentCoin, - buildTowerStatsSnapshot: buildTowerStatsSnapshot, - inventorySnapshot: GameEntry.PlayerInventory != null - ? GameEntry.PlayerInventory.GetInventorySnapshot() - : null, - participantTowerSnapshot: GameEntry.PlayerInventory != null - ? GameEntry.PlayerInventory.GetParticipantTowerSnapshot() - : null, - tryConsumeCoin: Scheduler.TryConsumeCoin, - addCoin: Scheduler.AddCoin); - } + MapData mapData = new MapData( + entityId: 0, + typeId: 0, + levelId: Context.CurrentLevel.Id, + position: Vector3.zero, + initialCoin: Context.CombatInRunResourceManager.CurrentCoin, + buildTowerStatsSnapshot: buildTowerStatsSnapshot, + inventorySnapshot: GameEntry.PlayerInventory != null + ? GameEntry.PlayerInventory.GetInventorySnapshot() + : null, + participantTowerSnapshot: GameEntry.PlayerInventory != null + ? GameEntry.PlayerInventory.GetParticipantTowerSnapshot() + : null); + return new MapEntityLoadContext(mapData, Flow.Scheduler.TryConsumeCoin, Flow.Scheduler.AddCoin); } } } diff --git a/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/CombatStates/CombatRewardSelectionState.cs b/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/CombatStates/CombatRewardSelectionState.cs index f9dc03d..eeb8578 100644 --- a/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/CombatStates/CombatRewardSelectionState.cs +++ b/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/CombatStates/CombatRewardSelectionState.cs @@ -1,39 +1,37 @@ namespace GeometryTD.CustomComponent { - public partial class CombatScheduler + internal sealed class CombatRewardSelectionState : CombatStateBase { - private sealed class CombatRewardSelectionState : CombatStateBase + public CombatRewardSelectionState(CombatSchedulerRuntimeContext context, CombatSchedulerFlowCoordinator flow) + : base(context, flow) { - public CombatRewardSelectionState(CombatScheduler scheduler) : base(scheduler) + } + + public override void OnEnter() + { + if (Context.SettlementContext == null) { + Flow.EnterFailureFallback("Combat reward selection failed. Settlement context is missing."); + return; } - public override void OnEnter() + Flow.EnsureRewardSelectFormUseCaseBound(); + if (!Context.SettlementFlowService.TryPrepareRewardSelection( + Context.SettlementContext, + Context.CombatInRunResourceManager, + Context.PhaseLoopRuntime.DisplayPhaseIndex, + Flow.ResolveCurrentThemeType(), + Context.RewardSelectFormUseCase, + Flow.OnFullBaseHpRewardSelected, + Flow.OnFullBaseHpRewardGiveUp)) { - if (Scheduler._settlementContext == null) - { - Scheduler.EnterFailureFallback("Combat reward selection failed. Settlement context is missing."); - return; - } - - Scheduler.EnsureRewardSelectFormUseCaseBound(); - if (!Scheduler._settlementFlowService.TryPrepareRewardSelection( - Scheduler._settlementContext, - Scheduler._combatInRunResourceManager, - Scheduler._phaseLoopRuntime.DisplayPhaseIndex, - Scheduler.ResolveCurrentThemeType(), - Scheduler._rewardSelectFormUseCase, - Scheduler.OnFullBaseHpRewardSelected, - Scheduler.OnFullBaseHpRewardGiveUp)) - { - Scheduler.ChangeState(new CombatFinishFormState(Scheduler)); - } + Flow.Scheduler.ChangeState(new CombatFinishFormState(Context, Flow)); } + } - public override void OnExit() - { - Scheduler.CloseRewardSelectForm(); - } + public override void OnExit() + { + Flow.CloseRewardSelectForm(); } } } diff --git a/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/CombatStates/CombatRunningPhaseState.cs b/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/CombatStates/CombatRunningPhaseState.cs index 6e9511a..f5ba188 100644 --- a/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/CombatStates/CombatRunningPhaseState.cs +++ b/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/CombatStates/CombatRunningPhaseState.cs @@ -5,72 +5,70 @@ using UnityGameFramework.Runtime; namespace GeometryTD.CustomComponent { - public partial class CombatScheduler + internal sealed class CombatRunningPhaseState : CombatStateBase { - private sealed class CombatRunningPhaseState : CombatStateBase + private readonly DRLevelPhase _phase; + private readonly IReadOnlyList _spawnEntries; + + public CombatRunningPhaseState( + CombatSchedulerRuntimeContext context, + CombatSchedulerFlowCoordinator flow, + DRLevelPhase phase, + IReadOnlyList spawnEntries) : base(context, flow) { - private readonly DRLevelPhase _phase; - private readonly IReadOnlyList _spawnEntries; + _phase = phase; + _spawnEntries = spawnEntries; + } - public CombatRunningPhaseState( - CombatScheduler scheduler, - DRLevelPhase phase, - IReadOnlyList spawnEntries) : base(scheduler) + public override void OnEnter() + { + Context.EnemyManager.BeginPhase(_phase, _spawnEntries); + GameEntry.Event.Fire( + Flow, + CombatProcessEventArgs.Create( + Context.PhaseLoopRuntime.DisplayPhaseIndex, + Context.PhaseLoopRuntime.PhaseCount)); + GameEntry.Event.Fire( + Flow, + CombatEnemyHpRateChangedEventArgs.Create( + Flow.ResolveEnemyHpRateMultiplier( + Context.PhaseLoopRuntime.DisplayPhaseIndex, + Context.PhaseLoopRuntime.PhaseCount))); + + if (!Context.NodeEnterFired) { - _phase = phase; - _spawnEntries = spawnEntries; + Context.NodeEnterFired = true; + GameEntry.Event.Fire(Flow, NodeEnterEventArgs.Create()); } - public override void OnEnter() + Log.Info( + "CombatScheduler phase started. Level={0}, Phase={1}, EndType={2}, Entries={3}.", + Context.CurrentLevel != null ? Context.CurrentLevel.Id : 0, + Context.PhaseLoopRuntime.DisplayPhaseIndex, + _phase.EndType, + _spawnEntries != null ? _spawnEntries.Count : 0); + } + + public override void OnUpdate(float elapseSeconds, float realElapseSeconds) + { + if (Context.PhaseLoopRuntime.CurrentPhase == null) { - Scheduler._enemyManager.BeginPhase(_phase, _spawnEntries); - GameEntry.Event.Fire( - Scheduler, - CombatProcessEventArgs.Create( - Scheduler._phaseLoopRuntime.DisplayPhaseIndex, - Scheduler._phaseLoopRuntime.PhaseCount)); - GameEntry.Event.Fire( - Scheduler, - CombatEnemyHpRateChangedEventArgs.Create( - ResolveEnemyHpRateMultiplier( - Scheduler._phaseLoopRuntime.DisplayPhaseIndex, - Scheduler._phaseLoopRuntime.PhaseCount))); - - if (!Scheduler._nodeEnterFired) - { - Scheduler._nodeEnterFired = true; - GameEntry.Event.Fire(Scheduler, NodeEnterEventArgs.Create()); - } - - Log.Info( - "CombatScheduler phase started. Level={0}, Phase={1}, EndType={2}, Entries={3}.", - Scheduler._currentLevel != null ? Scheduler._currentLevel.Id : 0, - Scheduler._phaseLoopRuntime.DisplayPhaseIndex, - _phase.EndType, - _spawnEntries != null ? _spawnEntries.Count : 0); + Flow.EnterFailureFallback("CombatScheduler update failed. Current phase is null."); + return; } - public override void OnUpdate(float elapseSeconds, float realElapseSeconds) + Context.PhaseLoopRuntime.AdvancePhaseElapsed(elapseSeconds); + Context.EnemyManager.OnUpdate(elapseSeconds, realElapseSeconds); + + if (Flow.ShouldEnterSettlementFromActiveState(out string reason, out bool isVictory)) { - if (Scheduler._phaseLoopRuntime.CurrentPhase == null) - { - Scheduler.EnterFailureFallback("CombatScheduler update failed. Current phase is null."); - return; - } + Flow.Scheduler.ChangeState(new CombatSettlementState(Context, Flow, reason, isVictory)); + return; + } - Scheduler._phaseLoopRuntime.AdvancePhaseElapsed(elapseSeconds); - Scheduler._enemyManager.OnUpdate(elapseSeconds, realElapseSeconds); - - if (Scheduler.ShouldEnterSettlementFromActiveState(out string reason, out bool isVictory)) - { - Scheduler.ChangeState(new CombatSettlementState(Scheduler, reason, isVictory)); - return; - } - - if (Scheduler._enemyManager.IsPhaseSpawnCompleted) - { - Scheduler.EnterWaitingForPhaseEnd(); - } + if (Context.EnemyManager.IsPhaseSpawnCompleted) + { + Flow.EnterWaitingForPhaseEnd(); } } } diff --git a/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/CombatStates/CombatSettlementState.cs b/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/CombatStates/CombatSettlementState.cs index 27ba896..09eff33 100644 --- a/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/CombatStates/CombatSettlementState.cs +++ b/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/CombatStates/CombatSettlementState.cs @@ -1,37 +1,38 @@ namespace GeometryTD.CustomComponent { - public partial class CombatScheduler + internal sealed class CombatSettlementState : CombatStateBase { - private sealed class CombatSettlementState : CombatStateBase + private readonly string _reason; + private readonly bool _isVictory; + + public CombatSettlementState( + CombatSchedulerRuntimeContext context, + CombatSchedulerFlowCoordinator flow, + string reason, + bool isVictory) : base(context, flow) { - private readonly string _reason; - private readonly bool _isVictory; + _reason = reason; + _isVictory = isVictory; + } - public CombatSettlementState(CombatScheduler scheduler, string reason, bool isVictory) : base(scheduler) + public override void OnEnter() + { + Context.EnemyManager.EndPhase(); + Context.EnemyManager.CleanupTrackedEnemies(); + Context.IsFinishAsVictory = _isVictory; + Context.SettlementContext = Context.SettlementFlowService.BuildSettlementContext( + _reason, + _isVictory, + Context.CurrentLevel, + Context.EnemyManager.DefeatedEnemyCount, + Context.CombatInRunResourceManager); + if (Context.SettlementContext.ShouldOpenRewardSelection) { - _reason = reason; - _isVictory = isVictory; + Flow.Scheduler.ChangeState(new CombatRewardSelectionState(Context, Flow)); + return; } - public override void OnEnter() - { - Scheduler._enemyManager.EndPhase(); - Scheduler._enemyManager.CleanupTrackedEnemies(); - Scheduler._isFinishAsVictory = _isVictory; - Scheduler._settlementContext = Scheduler._settlementFlowService.BuildSettlementContext( - _reason, - _isVictory, - Scheduler._currentLevel, - Scheduler._enemyManager.DefeatedEnemyCount, - Scheduler._combatInRunResourceManager); - if (Scheduler._settlementContext.ShouldOpenRewardSelection) - { - Scheduler.ChangeState(new CombatRewardSelectionState(Scheduler)); - return; - } - - Scheduler.ChangeState(new CombatFinishFormState(Scheduler)); - } + Flow.Scheduler.ChangeState(new CombatFinishFormState(Context, Flow)); } } } diff --git a/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/CombatStates/CombatStateBase.cs b/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/CombatStates/CombatStateBase.cs index 57436c7..5658a6f 100644 --- a/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/CombatStates/CombatStateBase.cs +++ b/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/CombatStates/CombatStateBase.cs @@ -1,35 +1,34 @@ namespace GeometryTD.CustomComponent { - public partial class CombatScheduler + internal abstract class CombatStateBase { - private abstract class CombatStateBase + protected CombatSchedulerRuntimeContext Context { get; } + protected CombatSchedulerFlowCoordinator Flow { get; } + + protected CombatStateBase(CombatSchedulerRuntimeContext context, CombatSchedulerFlowCoordinator flow) { - protected CombatScheduler Scheduler { get; } + Context = context; + Flow = flow; + } - protected CombatStateBase(CombatScheduler scheduler) - { - Scheduler = scheduler; - } + public virtual void OnInit() + { + } - public virtual void OnInit() - { - } + public virtual void OnEnter() + { + } - public virtual void OnEnter() - { - } + public virtual void OnExit() + { + } - public virtual void OnExit() - { - } + public virtual void OnUpdate(float elapseSeconds, float realElapseSeconds) + { + } - public virtual void OnUpdate(float elapseSeconds, float realElapseSeconds) - { - } - - public virtual void OnDestroy() - { - } + public virtual void OnDestroy() + { } } } diff --git a/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/CombatStates/CombatWaitingForPhaseEndState.cs b/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/CombatStates/CombatWaitingForPhaseEndState.cs index b3da1ad..f4a1978 100644 --- a/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/CombatStates/CombatWaitingForPhaseEndState.cs +++ b/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/CombatStates/CombatWaitingForPhaseEndState.cs @@ -1,46 +1,46 @@ +using GeometryTD.DataTable; + namespace GeometryTD.CustomComponent { - public partial class CombatScheduler + internal sealed class CombatWaitingForPhaseEndState : CombatStateBase { - private sealed class CombatWaitingForPhaseEndState : CombatStateBase + public CombatWaitingForPhaseEndState(CombatSchedulerRuntimeContext context, CombatSchedulerFlowCoordinator flow) + : base(context, flow) { - public CombatWaitingForPhaseEndState(CombatScheduler scheduler) : base(scheduler) + } + + public override void OnUpdate(float elapseSeconds, float realElapseSeconds) + { + _ = realElapseSeconds; + + DRLevelPhase currentPhase = Context.PhaseLoopRuntime.CurrentPhase; + if (currentPhase == null) { + Flow.EnterFailureFallback("CombatScheduler waiting phase failed. Current phase is null."); + return; } - public override void OnUpdate(float elapseSeconds, float realElapseSeconds) + Context.PhaseLoopRuntime.AdvancePhaseElapsed(elapseSeconds); + + if (Flow.ShouldEnterSettlementFromActiveState(out string reason, out bool isVictory)) { - _ = realElapseSeconds; - - var currentPhase = Scheduler._phaseLoopRuntime.CurrentPhase; - if (currentPhase == null) - { - Scheduler.EnterFailureFallback("CombatScheduler waiting phase failed. Current phase is null."); - return; - } - - Scheduler._phaseLoopRuntime.AdvancePhaseElapsed(elapseSeconds); - - if (Scheduler.ShouldEnterSettlementFromActiveState(out string reason, out bool isVictory)) - { - Scheduler.ChangeState(new CombatSettlementState(Scheduler, reason, isVictory)); - return; - } - - PhaseEndConditionContext context = new( - currentPhase, - Scheduler._phaseLoopRuntime.CurrentPhaseElapsed, - Scheduler._enemyManager.IsPhaseSpawnCompleted, - Scheduler._enemyManager.AliveEnemyCount, - Scheduler._enemyManager.HasAliveBoss); - IPhaseEndCondition endCondition = PhaseEndConditionFactory.Create(currentPhase.EndType); - if (!endCondition.ShouldExit(context)) - { - return; - } - - Scheduler.CompleteCurrentPhase(); + Flow.Scheduler.ChangeState(new CombatSettlementState(Context, Flow, reason, isVictory)); + return; } + + PhaseEndConditionContext conditionContext = new( + currentPhase, + Context.PhaseLoopRuntime.CurrentPhaseElapsed, + Context.EnemyManager.IsPhaseSpawnCompleted, + Context.EnemyManager.AliveEnemyCount, + Context.EnemyManager.HasAliveBoss); + IPhaseEndCondition endCondition = PhaseEndConditionFactory.Create(currentPhase.EndType); + if (!endCondition.ShouldExit(conditionContext)) + { + return; + } + + Flow.CompleteCurrentPhase(); } } -} +} \ No newline at end of file diff --git a/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/CombatStates/CombatWaitingForReturnState.cs b/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/CombatStates/CombatWaitingForReturnState.cs index eef5d28..0bbad42 100644 --- a/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/CombatStates/CombatWaitingForReturnState.cs +++ b/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/CombatStates/CombatWaitingForReturnState.cs @@ -1,35 +1,33 @@ namespace GeometryTD.CustomComponent { - public partial class CombatScheduler + internal sealed class CombatWaitingForReturnState : CombatStateBase { - private sealed class CombatWaitingForReturnState : CombatStateBase + private bool _returnRequested; + + public CombatWaitingForReturnState(CombatSchedulerRuntimeContext context, CombatSchedulerFlowCoordinator flow) + : base(context, flow) { - private bool _returnRequested; + } - public CombatWaitingForReturnState(CombatScheduler scheduler) : base(scheduler) + public void RequestReturn() + { + _returnRequested = true; + } + + public override void OnUpdate(float elapseSeconds, float realElapseSeconds) + { + _ = elapseSeconds; + _ = realElapseSeconds; + + if (!_returnRequested) { + return; } - public void RequestReturn() - { - _returnRequested = true; - } - - public override void OnUpdate(float elapseSeconds, float realElapseSeconds) - { - _ = elapseSeconds; - _ = realElapseSeconds; - - if (!_returnRequested) - { - return; - } - - Scheduler._loadSession.Cleanup(); - Scheduler.CloseCombatFinishForm(); - Scheduler.CloseRewardSelectForm(); - Scheduler.CompleteNormalCombatAndNotify(Scheduler._isFinishAsVictory); - } + Context.LoadSession.Cleanup(); + Flow.CloseCombatFinishForm(); + Flow.CloseRewardSelectForm(); + Flow.CompleteNormalCombatAndNotify(Context.IsFinishAsVictory); } } } diff --git a/Assets/GameMain/Scripts/Entity/EntityData/MapData.cs b/Assets/GameMain/Scripts/Entity/EntityData/MapData.cs index 730f5c3..0a8b151 100644 --- a/Assets/GameMain/Scripts/Entity/EntityData/MapData.cs +++ b/Assets/GameMain/Scripts/Entity/EntityData/MapData.cs @@ -15,9 +15,6 @@ namespace GeometryTD.Entity.EntityData [SerializeField] private BackpackInventoryData _inventorySnapshot; [SerializeField] private TowerItemData[] _participantTowerSnapshot = Array.Empty(); - [NonSerialized] private Func _tryConsumeCoin; - [NonSerialized] private Action _addCoin; - public MapData(int entityId, int levelId, Vector3 position) : this(entityId, 0, levelId, position) { } @@ -36,9 +33,7 @@ namespace GeometryTD.Entity.EntityData int initialCoin, IReadOnlyList buildTowerStatsSnapshot, BackpackInventoryData inventorySnapshot, - IReadOnlyList participantTowerSnapshot, - Func tryConsumeCoin, - Action addCoin) : base(entityId, typeId) + IReadOnlyList participantTowerSnapshot) : base(entityId, typeId) { _levelId = levelId; Position = position; @@ -48,8 +43,6 @@ namespace GeometryTD.Entity.EntityData ? InventoryCloneUtility.CloneInventory(inventorySnapshot) : null; SetParticipantTowerSnapshot(participantTowerSnapshot); - _tryConsumeCoin = tryConsumeCoin; - _addCoin = addCoin; } public int LevelId @@ -70,34 +63,6 @@ namespace GeometryTD.Entity.EntityData : null; public IReadOnlyList ParticipantTowerSnapshot => _participantTowerSnapshot; - public void BindCombatCallbacks(Func tryConsumeCoin, Action addCoin) - { - _tryConsumeCoin = tryConsumeCoin; - _addCoin = addCoin; - } - - public bool TryConsumeCoin(int coin) - { - int requiredCoin = Mathf.Max(0, coin); - if (requiredCoin <= 0) - { - return true; - } - - return _tryConsumeCoin != null && _tryConsumeCoin.Invoke(requiredCoin); - } - - public void AddCoin(int coin) - { - int amount = Mathf.Max(0, coin); - if (amount <= 0) - { - return; - } - - _addCoin?.Invoke(amount); - } - public bool TryGetBuildTowerStats(int buildIndex, out TowerStatsData stats) { stats = null; @@ -141,9 +106,7 @@ namespace GeometryTD.Entity.EntityData _initialCoin, _buildTowerStatsSnapshot, _inventorySnapshot, - _participantTowerSnapshot, - _tryConsumeCoin, - _addCoin); + _participantTowerSnapshot); } public void SetParticipantTowerSnapshot(IReadOnlyList participantTowerSnapshot) diff --git a/Assets/GameMain/Scripts/Entity/EntityData/MapEntityLoadContext.cs b/Assets/GameMain/Scripts/Entity/EntityData/MapEntityLoadContext.cs new file mode 100644 index 0000000..b38659e --- /dev/null +++ b/Assets/GameMain/Scripts/Entity/EntityData/MapEntityLoadContext.cs @@ -0,0 +1,18 @@ +using System; + +namespace GeometryTD.Entity.EntityData +{ + public sealed class MapEntityLoadContext + { + public MapEntityLoadContext(MapData initialMapData, Func tryConsumeCoin, Action addCoin) + { + InitialMapData = initialMapData; + TryConsumeCoin = tryConsumeCoin; + AddCoin = addCoin; + } + + public MapData InitialMapData { get; } + public Func TryConsumeCoin { get; } + public Action AddCoin { get; } + } +} diff --git a/Assets/GameMain/Scripts/Entity/EntityData/MapEntityLoadContext.cs.meta b/Assets/GameMain/Scripts/Entity/EntityData/MapEntityLoadContext.cs.meta new file mode 100644 index 0000000..ad55fe5 --- /dev/null +++ b/Assets/GameMain/Scripts/Entity/EntityData/MapEntityLoadContext.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 4ba39a6e9c0948b386b8b0189e3541da +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/GameMain/Scripts/Entity/EntityExtension.cs b/Assets/GameMain/Scripts/Entity/EntityExtension.cs index 933a66f..52ebd0e 100644 --- a/Assets/GameMain/Scripts/Entity/EntityExtension.cs +++ b/Assets/GameMain/Scripts/Entity/EntityExtension.cs @@ -56,6 +56,17 @@ namespace GeometryTD public static void ShowMap(this EntityComponent entityComponent, MapData data, string mapAssetName) { + ShowMap(entityComponent, new MapEntityLoadContext(data, null, null), mapAssetName); + } + + public static void ShowMap(this EntityComponent entityComponent, MapEntityLoadContext loadContext) + { + ShowMap(entityComponent, loadContext, null); + } + + public static void ShowMap(this EntityComponent entityComponent, MapEntityLoadContext loadContext, string mapAssetName) + { + MapData data = loadContext?.InitialMapData; if (data == null) { Log.Warning("Map data is invalid."); @@ -64,7 +75,7 @@ namespace GeometryTD string resolvedMapAssetName = string.IsNullOrEmpty(mapAssetName) ? data.LevelId.ToString() : mapAssetName; string mapAssetPath = AssetUtility.GetLevelMapAsset(resolvedMapAssetName); - entityComponent.ShowEntity(data.Id, typeof(MapEntity), mapAssetPath, "Map", Constant.AssetPriority.MapAsset, data); + entityComponent.ShowEntity(data.Id, typeof(MapEntity), mapAssetPath, "Map", Constant.AssetPriority.MapAsset, loadContext); } private static void ShowEntity(this EntityComponent entityComponent, Type logicType, string entityGroup, diff --git a/Assets/GameMain/Scripts/Entity/EntityLogic/MapEntity.cs b/Assets/GameMain/Scripts/Entity/EntityLogic/MapEntity.cs index 43fa1b1..fe05c11 100644 --- a/Assets/GameMain/Scripts/Entity/EntityLogic/MapEntity.cs +++ b/Assets/GameMain/Scripts/Entity/EntityLogic/MapEntity.cs @@ -1,8 +1,6 @@ using System; using System.Collections.Generic; using GeometryTD.CustomComponent; -using GeometryTD.CustomEvent; -using GameFramework.Event; using GeometryTD.Map; using GeometryTD.Definition; using GeometryTD.Entity.EntityData; @@ -37,10 +35,8 @@ namespace GeometryTD.Entity private CombatSelectInputService _combatSelectInputService; private TowerPlacementService _towerPlacementService; private TowerSelectionPresenter _towerSelectionPresenter; - private CombatSelectUseCaseConfigurator _combatSelectUseCaseConfigurator; - private int _currentCoin; - private bool _isCoinEventSubscribed; + private MapCombatRuntimeBridge _combatRuntimeBridge; public IReadOnlyList PathCells => _mapTopologyService != null ? _mapTopologyService.PathCells @@ -110,22 +106,21 @@ namespace GeometryTD.Entity InitializeMapTopologyService(); InitializeTowerPlacementService(); InitializeTowerSelectionPresenter(); + InitializeCombatRuntimeBridge(); } protected override void OnShow(object userData) { - base.OnShow(userData); - - _mapData = userData as MapData; + MapEntityLoadContext loadContext = ResolveLoadContext(userData); + _mapData = loadContext?.InitialMapData; if (_mapData == null) { Log.Warning("MapData is invalid for map entity '{0}'.", Id); } - else - { - _currentCoin = Mathf.Max(0, _mapData.InitialCoin); - SubscribeCombatEvents(); - } + + base.OnShow(_mapData); + + _combatRuntimeBridge?.Initialize(loadContext, name); RefreshTiles(); ConfigureCombatSelectUseCase(); @@ -134,7 +129,7 @@ namespace GeometryTD.Entity protected override void OnHide(bool isShutdown, object userData) { - UnsubscribeCombatEvents(); + _combatRuntimeBridge?.Reset(); HideCombatSelectForm(); _towerPlacementService?.HideAndClearAllPlacedTowers(); ClearSelectionState(); @@ -240,6 +235,14 @@ namespace GeometryTD.Entity } } + private void InitializeCombatRuntimeBridge() + { + if (_combatRuntimeBridge == null) + { + _combatRuntimeBridge = new MapCombatRuntimeBridge(); + } + } + private void ConfigureCombatSelectUseCase() { _combatSelectFormUseCase.SetCoinProvider(GetCurrentCoin); @@ -257,6 +260,21 @@ namespace GeometryTD.Entity _mapData != null ? _mapData.ParticipantTowerSnapshot : null); } + private MapEntityLoadContext ResolveLoadContext(object userData) + { + if (userData is MapEntityLoadContext loadContext) + { + return loadContext; + } + + if (userData is MapData legacyMapData) + { + return new MapEntityLoadContext(legacyMapData, null, null); + } + + return null; + } + private void HandleCombatSelectInput() { if (!_enableCombatSelectInput || !Input.GetMouseButtonDown(0)) @@ -379,38 +397,6 @@ namespace GeometryTD.Entity GameEntry.UIRouter.CloseUI(UIFormType.CombatSelectForm); } - private void SubscribeCombatEvents() - { - if (_isCoinEventSubscribed) - { - return; - } - - GameEntry.Event.Subscribe(CombatCoinChangedEventArgs.EventId, OnCombatCoinChanged); - _isCoinEventSubscribed = true; - } - - private void UnsubscribeCombatEvents() - { - if (!_isCoinEventSubscribed) - { - return; - } - - GameEntry.Event.Unsubscribe(CombatCoinChangedEventArgs.EventId, OnCombatCoinChanged); - _isCoinEventSubscribed = false; - } - - private void OnCombatCoinChanged(object sender, GameEventArgs e) - { - if (e is not CombatCoinChangedEventArgs args) - { - return; - } - - _currentCoin = Mathf.Max(0, args.CurrentCoin); - } - private int GetCurrentBuildTowerCount() { if (_mapData == null) @@ -434,12 +420,12 @@ namespace GeometryTD.Entity return false; } - return _mapData.TryConsumeCoin(requiredCoin); + return _combatRuntimeBridge != null && _combatRuntimeBridge.TryConsumeCoin(requiredCoin); } private int GetCurrentCoin() { - return Mathf.Max(0, _currentCoin); + return Mathf.Max(0, _combatRuntimeBridge != null ? _combatRuntimeBridge.CurrentCoin : 0); } private void AddCoin(int coin) @@ -450,7 +436,7 @@ namespace GeometryTD.Entity return; } - _mapData?.AddCoin(amount); + _combatRuntimeBridge?.AddCoin(amount); } } } diff --git a/Assets/GameMain/Scripts/Scene/Map/MapCombatRuntimeBridge.cs b/Assets/GameMain/Scripts/Scene/Map/MapCombatRuntimeBridge.cs new file mode 100644 index 0000000..8324626 --- /dev/null +++ b/Assets/GameMain/Scripts/Scene/Map/MapCombatRuntimeBridge.cs @@ -0,0 +1,85 @@ +using System; +using GameFramework.Event; +using GeometryTD.CustomEvent; +using GeometryTD.Entity.EntityData; +using UnityEngine; +using UnityGameFramework.Runtime; + +namespace GeometryTD.Map +{ + public sealed class MapCombatRuntimeBridge + { + private Func _tryConsumeCoin; + private Action _addCoin; + private bool _isCoinEventSubscribed; + + public int CurrentCoin { get; private set; } + + public void Initialize(MapEntityLoadContext loadContext, string mapName) + { + Reset(); + + MapData mapData = loadContext?.InitialMapData; + CurrentCoin = Mathf.Max(0, mapData != null ? mapData.InitialCoin : 0); + _tryConsumeCoin = loadContext?.TryConsumeCoin; + _addCoin = loadContext?.AddCoin; + + if (_tryConsumeCoin == null || _addCoin == null) + { + Log.Warning( + "Map combat runtime bridge has incomplete callbacks. Map='{0}', TryConsumeCoin={1}, AddCoin={2}.", + mapName, + _tryConsumeCoin != null, + _addCoin != null); + } + + GameEntry.Event.Subscribe(CombatCoinChangedEventArgs.EventId, OnCombatCoinChanged); + _isCoinEventSubscribed = true; + } + + public void Reset() + { + if (_isCoinEventSubscribed) + { + GameEntry.Event.Unsubscribe(CombatCoinChangedEventArgs.EventId, OnCombatCoinChanged); + _isCoinEventSubscribed = false; + } + + _tryConsumeCoin = null; + _addCoin = null; + CurrentCoin = 0; + } + + public bool TryConsumeCoin(int cost) + { + int requiredCoin = Mathf.Max(0, cost); + if (requiredCoin <= 0) + { + return true; + } + + return _tryConsumeCoin != null && _tryConsumeCoin.Invoke(requiredCoin); + } + + public void AddCoin(int coin) + { + int amount = Mathf.Max(0, coin); + if (amount <= 0) + { + return; + } + + _addCoin?.Invoke(amount); + } + + private void OnCombatCoinChanged(object sender, GameEventArgs e) + { + if (e is not CombatCoinChangedEventArgs args) + { + return; + } + + CurrentCoin = Mathf.Max(0, args.CurrentCoin); + } + } +} diff --git a/Assets/GameMain/Scripts/Scene/Map/MapCombatRuntimeBridge.cs.meta b/Assets/GameMain/Scripts/Scene/Map/MapCombatRuntimeBridge.cs.meta new file mode 100644 index 0000000..1fad508 --- /dev/null +++ b/Assets/GameMain/Scripts/Scene/Map/MapCombatRuntimeBridge.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 8309cfa985d6427c83ac5fc2b7e92d31 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/GameMain/Scripts/Scene/Map/MapTopologyService.cs b/Assets/GameMain/Scripts/Scene/Map/MapTopologyService.cs index 415b6e8..9a5f829 100644 --- a/Assets/GameMain/Scripts/Scene/Map/MapTopologyService.cs +++ b/Assets/GameMain/Scripts/Scene/Map/MapTopologyService.cs @@ -109,9 +109,8 @@ namespace GeometryTD.Map } float minDistance = float.MaxValue; - for (int i = 0; i < _pathCells.Count; i++) + foreach (var candidate in _pathCells) { - Vector3Int candidate = _pathCells[i]; float distance = (tilemap.GetCellCenterWorld(candidate) - worldPosition).sqrMagnitude; if (distance >= minDistance) { diff --git a/docs/CodeX-TODO.md b/docs/CodeX-TODO.md index 55a2c68..ab3cf94 100644 --- a/docs/CodeX-TODO.md +++ b/docs/CodeX-TODO.md @@ -4,32 +4,35 @@ ## 当前目标 -按 `docs/CombatNodeArchitecture.md` 收敛 `CombatNode` 域职责,重点是: -- `CombatScheduler` 收敛为“状态机管理器”,不再继续堆业务细节。 -- 局内 `Coin / Gold / BaseHp / Loot Backpack / BuildTowerSnapshots` 由 `CombatInRunResourceManager` 作为唯一真值来源。 -- `EnemyManager` 只上报敌人事件,不直接决定资源入账或状态切换。 -- `PhaseEndType` 退出条件从 `PhaseLoopRuntime` 中抽离到 `IPhaseEndCondition` 实现类。 -- 结束链、加载链、奖励选择链逐步从 `CombatScheduler.cs` 本体挪到状态类或专用服务。 +按 `docs/CombatNodeArchitecture.md` 继续收敛 `CombatNode` 域职责。当前骨架已经基本到位,后续重点是: +- 继续保持 `CombatScheduler` 作为唯一状态机边界,避免把新业务重新堆回本体。 +- 继续完成 `MapData + Event` 解耦收尾,确认 `MapEntity` 不再反查 `CombatNode` 域运行时。 +- 稳定 `CombatSettlementContext` 的模型边界,避免流程控制字段和展示摘要继续混杂增长。 +- 补 Unity 编译、PlayMode 和失败路径回归验证,把这轮结构调整真正跑通。 ## 已完成 -### 1. 状态类拆分完成 -- `CombatScheduler.cs` 内部的嵌套状态类已经迁移到 `Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatStates/`。 -- 保留了 `partial class CombatScheduler + 嵌套类` 的结构。 -- 旧 `CombatState` 已统一替换为 `CombatStateBase`。 +### 1. 状态类已收口到 CombatScheduler/CombatStates +- 状态类当前位于 `Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/CombatStates/`。 +- `CombatStateBase` 已统一改为 `Context + Flow` 双引用模式。 +- 各状态不再直接访问 `CombatScheduler._xxx` 私有字段,而是通过: + - `CombatSchedulerRuntimeContext` + - `CombatSchedulerFlowCoordinator` 关键文件: - `Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/CombatScheduler.cs` -- `Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatStates/CombatStateBase.cs` -- `Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatStates/*.cs` +- `Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/CombatStates/CombatStateBase.cs` +- `Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/CombatStates/*.cs` +- `Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/CombatSchedulerRuntimeContext.cs` +- `Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/CombatSchedulerFlowCoordinator.cs` -### 2. 第一轮目标命名和骨架已建立 +### 2. 第一轮目标命名、骨架与接线已建立 - `CombatResourceManager` 已重命名为 `CombatInRunResourceManager`。 - 已新增掉落解析骨架: - `EnemyDropResolveContext` - `EnemyDropResolveResult` - `EnemyDropResolver` -- 已新增 phase end 骨架: +- 已新增 phase end 骨架并接入等待退出状态: - `IPhaseEndCondition` - `PhaseEndConditionContext` - `PhaseEndConditionFactory` @@ -88,7 +91,22 @@ - `TryConsumeCoin(...)` - `AddCoin(...)` - `TryGetBuildTowerStats(...)` +- 失败调试入口: + - `TryDebugFail(...)` - 不再通过 `CombatNodeComponent` 读写 baseHp/coin 真值 +- 当前主职责已收紧为: + - 生命周期入口 + - 状态切换入口 + - 对外公开查询/操作接口 + - 敌人事件公共入口 + - 事件桥回调入口 + +#### CombatScheduler 内部实现细化 +- 已新增 `CombatSchedulerRuntimeContext` + - 承载共享运行时字段与共享服务引用 +- 已新增 `CombatSchedulerFlowCoordinator` + - 承载多个状态共用的流程辅助方法 +- 当前 `CombatScheduler` 本体不再直接堆所有共享字段和公用流程辅助 #### CombatNodeComponent - 不再持有这些字段: @@ -105,85 +123,161 @@ 关键文件: - `Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/CombatScheduler.cs` +- `Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/CombatSchedulerRuntimeContext.cs` +- `Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/CombatSchedulerFlowCoordinator.cs` - `Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatNodeComponent.cs` +### 5. EnemyManager 事件边界已改为只上报敌人事实 +- `EnemyManager` 现在只向 `CombatScheduler` 上报: + - `OnEnemyDefeated(DREnemy enemy)` + - `OnEnemyReachedBase(DREnemy enemy)` +- coin/gold/baseDamage 的公共副作用已统一收口到 `CombatScheduler` +- `EnemyDropResolver + CombatInRunResourceManager` 已接到公共敌人事件入口上 + +关键文件: +- `Assets/GameMain/Scripts/CustomComponent/CombatNode/EnemyManager/EnemyManager.cs` +- `Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/EnemyDropResolver.cs` +- `Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/CombatScheduler.cs` + +### 6. PhaseEndCondition 已正式接入 WaitingForPhaseEnd +- `CombatWaitingForPhaseEndState` 已改为通过: + - `PhaseEndConditionFactory.Create(...)` + - `IPhaseEndCondition.ShouldExit(...)` + 判定 phase 结束 +- `PhaseLoopRuntime` 当前只保留 phase runtime 数据与 phase 进入逻辑,不再负责 `PhaseEndType` 判定 + +关键文件: +- `Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/CombatStates/CombatWaitingForPhaseEndState.cs` +- `Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/PhaseEndConditions/` +- `Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/PhaseLoopRuntime.cs` + +### 7. 结束链与异常失败链已做协议收口 +- 正常结束: + - `NodeCompleteEventArgs` 只在 `WaitingForReturn` 完成后发布 +- 异常失败: + - 使用 `CombatFailedState` + - 弹出单按钮 `DialogForm` + - 点击 `Return Menu` 后发布 `CombatFailureReturnEventArgs` +- `ProcedureMenu` 已区分正常结束与异常失败两条返回协议 + +关键文件: +- `Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/CombatStates/CombatWaitingForReturnState.cs` +- `Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/CombatStates/CombatFailedState.cs` +- `Assets/GameMain/Scripts/Event/Combat/CombatFailureReturnEventArgs.cs` +- `Assets/GameMain/Scripts/Procedure/ProcedureMenu.cs` + +### 8. CombatSettlementContext 已收紧为结束链共享上下文 +- 当前由 `Settlement -> RewardSelection -> FinishForm -> WaitingForReturn` 共享 +- 已显式承载: + - 结算事实 + - 奖励背包 + - 奖励选择相关流程标记 + - 低血惩罚相关事实 +- 低血惩罚已改为“先记录事实,提交阶段再统一落库”,不再在结算构造期直接写库存 +- `CombatFinishForm` 当前只消费它真正展示需要的摘要,不再把额外结算事实继续灌进 UI Context + +关键文件: +- `Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/CombatSettlementContext.cs` +- `Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/CombatSettlementFlowService.cs` +- `Assets/GameMain/Scripts/UI/Combat/UseCase/CombatFinishFormUseCase.cs` + +### 9. CombatInfoForm 已补手动失败测试入口 +- `CombatInfoForm.OnFailButtonClick()` 可手动触发异常失败链 +- 新增 `CombatDebugFailEventArgs` +- 当前可直接用于测试: + - `CombatFailedState` + - 失败 Dialog + - `CombatFailureReturnEventArgs` + +关键文件: +- `Assets/GameMain/Scripts/UI/Combat/View/CombatInfoForm.cs` +- `Assets/GameMain/Scripts/UI/Combat/Controller/CombatInfoFormController.cs` +- `Assets/GameMain/Scripts/UI/Combat/UseCase/CombatInfoFormUseCase.cs` +- `Assets/GameMain/Scripts/Event/Combat/CombatDebugFailEventArgs.cs` + ## 还没完成 -### 1. EnemyManager 事件边界还没改干净 -当前仍然存在的问题: -- `EnemyManager` 还在自己计算 `droppedCoin / droppedGold / baseDamage` -- 然后把这些原始数值直接传给 `CombatScheduler` - -目标状态: -- `EnemyManager` 只上报: - - `OnEnemyDefeated(DREnemy enemy)` - - `OnEnemyReachedBase(DREnemy enemy)` -- `CombatScheduler` 统一调用: - - `EnemyDropResolver` - - `CombatInRunResourceManager` - -下一步建议: -- 先改 `EnemyManager` 上报签名 -- 再把 coin/gold/baseDamage 的公共副作用挪到 `CombatScheduler` - -### 2. PhaseEndCondition 骨架已建,但还没接入 -当前仍然是旧逻辑: -- `PhaseLoopRuntime.ShouldEndCurrentPhase(...)` 还在使用 -- `CombatWaitingForPhaseEndState` 还没有改成通过 `IPhaseEndCondition` 判定 - -目标状态: -- `PhaseLoopRuntime` 只保留 phase runtime 数据 -- `CombatWaitingForPhaseEndState` 通过 `PhaseEndConditionFactory.Create(...)` 获取当前判定器 -- `PhaseEndType` 的规则不再写在 `PhaseLoopRuntime` 里 - -### 3. CombatScheduler 本体仍然过重 -当前仍然还在 `CombatScheduler.cs` 里的业务: -- 结算上下文构建 -- 基地血量结算修正 -- 奖励选择 UI 准备 -- FinishForm 打开逻辑 -- 一部分加载和清理编排 - -目标状态: -- `Loading` 负责加载和 `MapData` 组装 -- `Settlement` 负责结算上下文和奖励准入判断 -- `RewardSelection` 只处理选择流程 -- `FinishForm` 只处理展示 -- `WaitingForReturn` 只处理回退和最终清理 - -### 4. MapData + Event 解耦还没开始 +### 1. MapData + Event 解耦还没完全收口 当前: -- `MapData` 只有 `LevelId` -- `MapEntity` 仍通过 `GameEntry.CombatNode` 反查 coin / build stats +- `CombatLoadingState` 已经会组装更完整的 `MapData` +- `MapEntity` 已经主要从 `MapData` 取初始上下文 +- 但还需要继续确认地图侧事件和运行时依赖是否已经完全摆脱对 `CombatNode` 域的反查 目标状态: - `CombatLoadingState` 从 `CombatInRunResourceManager` 读取快照 - `CombatLoadingState` 组装更完整的 `MapData` - `MapEntity` 后续通过 `MapData + Event` 获取上下文,而不是反查 `CombatNode` +### 2. CombatScheduler 本体仍然还有收紧空间 +当前: +- 生命周期入口、状态切换入口、对外公开接口、事件桥回调入口仍在 `CombatScheduler.cs` +- 共享字段和共享流程已经拆到了 `RuntimeContext + FlowCoordinator` +- 但 `FlowCoordinator` 目前仍直接依赖 `CombatScheduler` 宿主对象 + +目标状态: +- 对外公开接口与内部宿主职责进一步分离 +- 若继续收紧,可评估是否引入极小宿主接口,只暴露: + - `ChangeState(...)` + - 必要的回调/转发入口 +- 继续避免把新业务重新塞回 `CombatScheduler.cs` + +### 3. CombatSettlementContext 还可以继续整理字段分层 +当前: +- `CombatSettlementContext` 已能支撑结束链 +- 但字段仍是平铺增长 + +目标状态: +- 更明确地区分: + - 流程控制字段 + - 结算事实字段 + - 展示摘要字段 +- 避免后续继续把 UI 需求和流程控制混在一起 + +### 4. 需要补实际运行验证 +当前改动已覆盖: +- 状态机结构 +- 结束链/失败链协议 +- 手动失败测试入口 +- `CombatSchedulerRuntimeContext + CombatSchedulerFlowCoordinator` +- `CombatSettlementContext` 与延迟提交惩罚 + +但仍缺: +- Unity 编译验证 +- PlayMode/手点验证 +- 失败路径回收验证 +- 新开局后无残留状态验证 + ## 推荐的后续执行顺序 -1. 改 `EnemyManager` 上报边界,接入 `EnemyDropResolver` -2. 接入 `IPhaseEndCondition`,移除 `PhaseLoopRuntime.ShouldEndCurrentPhase(...)` -3. 把结算链逻辑从 `CombatScheduler.cs` 继续剥离到状态类 -4. 开始做 `MapData + Event` 解耦 +1. 做 `MapData + Event` 解耦收尾,排查 `MapEntity` 和地图侧事件是否还反查 `CombatNode` +2. 评估是否给 `FlowCoordinator` 引入极小宿主接口,继续收紧 `CombatScheduler` 本体 +3. 继续整理 `CombatSettlementContext` 的字段分层 +4. 补 Unity 编译与手动回归验证 ## 当前做变更时要记住的约束 - 状态切换只能通过 `CombatScheduler.ChangeState(...)` -- 不要把新业务继续堆回 `CombatScheduler.cs` +- 不要把新业务继续堆回 `CombatScheduler.cs`,优先考虑: + - 状态私有逻辑 + - `CombatSchedulerFlowCoordinator` + - 现有独立服务 - `CombatNodeComponent` 现在应该保持轻量 facade,不要再把 coin/baseHp/build snapshot 回流到它 - coin/baseHp 变化事件应继续由 `CombatInRunResourceManager` 发布 - `Failed` 只处理异常失败,不处理“基地血量归零”的正常失败路径 +- 状态类应继续只通过 `CombatSchedulerRuntimeContext + CombatSchedulerFlowCoordinator` 访问共享状态与共享流程 ## 当前关键入口文件速查 - 状态机宿主: - `Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/CombatScheduler.cs` +- 共享运行时承载: + - `Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/CombatSchedulerRuntimeContext.cs` +- 共享流程协调: + - `Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/CombatSchedulerFlowCoordinator.cs` - 局内资源真值: - `Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/CombatInRunResourceManager.cs` - 状态类: - - `Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatStates/` + - `Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/CombatStates/` - 加载服务: - `Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/CombatLoadSession.cs` - phase runtime: @@ -194,6 +288,10 @@ - `Assets/GameMain/Scripts/CustomComponent/CombatNode/EnemyManager/EnemyManager.cs` - CombatNode 入口 facade: - `Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatNodeComponent.cs` +- 结束链共享上下文: + - `Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/CombatSettlementContext.cs` +- 异常失败返回事件: + - `Assets/GameMain/Scripts/Event/Combat/CombatFailureReturnEventArgs.cs` ## 备注 diff --git a/docs/CombatNodeArchitecture.md b/docs/CombatNodeArchitecture.md index fb23b8b..4a30bbc 100644 --- a/docs/CombatNodeArchitecture.md +++ b/docs/CombatNodeArchitecture.md @@ -65,8 +65,12 @@ - `CombatFailedState` 实现约束: -- 上述状态类作为 `CombatScheduler` 的嵌套类实现。 -- 共享数据与共享服务统一放在 `CombatScheduler` 上。 +- 上述状态类可以作为 `CombatScheduler` 的嵌套类实现,也可以拆成独立文件;但必须只服务于 `CombatScheduler` 状态机,不形成独立业务边界。 +- 共享数据与共享服务统一收口到 `CombatScheduler` 内部持有的运行时承载体,不允许散落在各状态类中。 +- 若 `CombatScheduler` 体量过大,允许在其内部实现中继续拆出: + - `CombatSchedulerRuntimeContext`:承载共享运行时字段与共享服务引用 + - `CombatSchedulerFlowCoordinator`:承载多个状态共用的流程辅助方法 +- 上述拆分只属于 `CombatScheduler` 的内部实现细化,不改变 `CombatScheduler` 作为唯一状态机边界的职责。 - 所有状态切换只能通过 `CombatScheduler.ChangeState(...)` 完成。 - 状态类不能彼此直接操控。 @@ -223,6 +227,19 @@ - 跟踪加载成功/失败状态。 - 对外提供 `CurrentMap` 与 `IsReady`。 +### 4.1.x CombatSchedulerRuntimeContext / CombatSchedulerFlowCoordinator(实现细化) + +当前实现允许: +- 用 `CombatSchedulerRuntimeContext` 承载所有状态共享的运行时字段与共享服务引用。 +- 用 `CombatSchedulerFlowCoordinator` 承载多个状态共用的流程辅助逻辑。 + +约束: +- 两者都必须由 `CombatScheduler` 持有并统一管理生命周期。 +- 两者都不替代 `CombatScheduler` 对外暴露状态机边界。 +- `RuntimeContext` 不负责状态切换。 +- `FlowCoordinator` 不持有独立业务真值,只能围绕共享运行时做编排辅助。 +- 状态类只允许通过 `RuntimeContext + FlowCoordinator` 访问共享状态与共享流程,不应再直接耦合其他状态实现细节。 + ### 4.2 PhaseLoopRuntime 文件:`Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/PhaseLoopRuntime.cs`