diff --git a/Assets/GameFramework/Scripts/Editor/Misc/Type.cs b/Assets/GameFramework/Scripts/Editor/Misc/Type.cs index 1cb34d1..5b2a6ce 100644 --- a/Assets/GameFramework/Scripts/Editor/Misc/Type.cs +++ b/Assets/GameFramework/Scripts/Editor/Misc/Type.cs @@ -22,6 +22,7 @@ namespace UnityGameFramework.Editor "UnityGameFramework.Runtime", #endif "Assembly-CSharp", + "GeometryTD.Runtime" }; private static readonly string[] RuntimeOrEditorAssemblyNames = diff --git a/Assets/GameMain/GeometryTD.Runtime.asmdef b/Assets/GameMain/GeometryTD.Runtime.asmdef index 3c004a3..1006865 100644 --- a/Assets/GameMain/GeometryTD.Runtime.asmdef +++ b/Assets/GameMain/GeometryTD.Runtime.asmdef @@ -2,6 +2,7 @@ "name": "GeometryTD.Runtime", "rootNamespace": "GeometryTD", "references": [ + "UnityGameFramework.Editor", "UnityGameFramework.Runtime", "Unity.InputSystem", "Unity.TextMeshPro" @@ -15,4 +16,4 @@ "defineConstraints": [], "versionDefines": [], "noEngineReferences": false -} +} \ No newline at end of file diff --git a/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/CombatScheduler.cs b/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/CombatScheduler.cs index ffbe01f..0eb542e 100644 --- a/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/CombatScheduler.cs +++ b/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/CombatScheduler.cs @@ -91,7 +91,7 @@ namespace GeometryTD.CustomComponent _runtime.EnemyManager.EndPhase(); _runtime.EnemyManager.ResetCombatStats(); _coordinator.ResetRuntime(); - _runtime.IsFinishAsVictory = true; + _runtime.DidCombatWin = true; _runtime.CurrentLevel = level; _runtime.RunId = runId; diff --git a/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/CombatSchedulerCoordinator.cs b/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/CombatSchedulerCoordinator.cs index bf13646..d3d4845 100644 --- a/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/CombatSchedulerCoordinator.cs +++ b/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/CombatSchedulerCoordinator.cs @@ -53,7 +53,7 @@ namespace GeometryTD.CustomComponent _runtime.EnemyDropResolver.Reset(); _runtime.SettlementContext = null; _runtime.CurrentLevel = null; - _runtime.IsFinishAsVictory = true; + _runtime.DidCombatWin = true; _runtime.IsCompleted = false; _runtime.NodeEnterFired = false; _runtime.RunId = null; @@ -124,21 +124,21 @@ namespace GeometryTD.CustomComponent TryBeginNextPhase(); } - public bool ShouldEnterSettlementFromActiveState(out bool isVictory) + public bool ShouldEnterSettlementFromActiveState(out bool didCombatWin) { if (GetCurrentBaseHp() <= 0) { - isVictory = false; + didCombatWin = false; return true; } if (_runtime.PhaseLoopRuntime.IsEndCombatRequested) { - isVictory = true; + didCombatWin = true; return true; } - isVictory = true; + didCombatWin = true; return false; } @@ -198,9 +198,9 @@ namespace GeometryTD.CustomComponent GameEntry.UIRouter.CloseUI(UIFormType.DialogForm); } - public void CompleteNormalCombatAndNotify(bool succeeded) + public void CompleteNormalCombatAndNotify(bool didCombatWin) { - CompleteCombat(succeeded); + CompleteCombat(didCombatWin); GameEntry.Event.Fire( this, NodeCompleteEventArgs.Create( @@ -208,7 +208,8 @@ namespace GeometryTD.CustomComponent _runtime.NodeId, _runtime.NodeType, _runtime.SequenceIndex, - succeeded, + RunNodeCompletionStatus.Completed, + didCombatWin, GameEntry.PlayerInventory != null ? GameEntry.PlayerInventory.GetInventorySnapshot() : null)); } @@ -219,7 +220,16 @@ namespace GeometryTD.CustomComponent CloseRewardSelectForm(); CloseDialogForm(); CompleteCombat(false); - GameEntry.Event.Fire(this, CombatFailureReturnEventArgs.Create()); + GameEntry.Event.Fire( + this, + NodeCompleteEventArgs.Create( + _runtime.RunId, + _runtime.NodeId, + _runtime.NodeType, + _runtime.SequenceIndex, + RunNodeCompletionStatus.Exception, + false, + GameEntry.PlayerInventory != null ? GameEntry.PlayerInventory.GetInventorySnapshot() : null)); } public bool HandleStartFailure(string errorMessage) diff --git a/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/CombatSchedulerRuntime.cs b/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/CombatSchedulerRuntime.cs index b815aee..3f09009 100644 --- a/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/CombatSchedulerRuntime.cs +++ b/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/CombatSchedulerRuntime.cs @@ -26,7 +26,7 @@ namespace GeometryTD.CustomComponent public RewardSelectFormUseCase RewardSelectFormUseCase { get; set; } public CombatStateBase CurrentState { get; set; } public Action CombatEndedCallback { get; set; } - public bool IsFinishAsVictory { get; set; } = true; + public bool DidCombatWin { 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/CombatSettlementContext.cs b/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/CombatSettlementContext.cs index 92738b5..527ce96 100644 --- a/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/CombatSettlementContext.cs +++ b/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/CombatSettlementContext.cs @@ -18,7 +18,7 @@ namespace GeometryTD.CustomComponent internal sealed class CombatSettlementResult { - public bool IsVictory; + public bool DidCombatWin; public int FinalCoin; public int FinalBaseHp; public int MaxBaseHp; diff --git a/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/CombatSettlementService.cs b/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/CombatSettlementService.cs index e464d75..13e461f 100644 --- a/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/CombatSettlementService.cs +++ b/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/CombatSettlementService.cs @@ -18,14 +18,14 @@ namespace GeometryTD.CustomComponent private const float LowBaseHpTowerEndurancePenalty = 10f; public CombatSettlementContext BuildSettlementContext( - bool isVictory, + bool didCombatWin, DRLevel currentLevel, int defeatedEnemyCount, CombatRunResourceStore resourceStore) { bool shouldOpenFullBaseHpRewardSelect = false; ResolveSettlementByBaseHp( - isVictory, + didCombatWin, currentLevel, resourceStore, out int currentBaseHp, @@ -39,7 +39,7 @@ namespace GeometryTD.CustomComponent CombatSettlementContext settlementContext = new CombatSettlementContext { }; - settlementContext.Result.IsVictory = isVictory; + settlementContext.Result.DidCombatWin = didCombatWin; settlementContext.Result.FinalCoin = Mathf.Max(0, resourceStore != null ? resourceStore.CurrentCoin : 0); settlementContext.Result.FinalBaseHp = currentBaseHp; settlementContext.Result.MaxBaseHp = maxBaseHp; @@ -173,7 +173,7 @@ namespace GeometryTD.CustomComponent } private static void ResolveSettlementByBaseHp( - bool isVictory, + bool didCombatWin, DRLevel currentLevel, CombatRunResourceStore resourceStore, out int currentBaseHp, @@ -197,7 +197,7 @@ namespace GeometryTD.CustomComponent appliedLowBaseHpPenalty = false; shouldOpenFullBaseHpRewardSelect = false; - if (!isVictory || resourceStore == null) + if (!didCombatWin || resourceStore == null) { return; } diff --git a/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/CombatStates/CombatRunningPhaseState.cs b/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/CombatStates/CombatRunningPhaseState.cs index 9f791df..0721717 100644 --- a/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/CombatStates/CombatRunningPhaseState.cs +++ b/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/CombatStates/CombatRunningPhaseState.cs @@ -66,9 +66,9 @@ namespace GeometryTD.CustomComponent Runtime.PhaseLoopRuntime.AdvancePhaseElapsed(elapseSeconds); Runtime.EnemyManager.OnUpdate(elapseSeconds, realElapseSeconds); - if (Coordinator.ShouldEnterSettlementFromActiveState(out bool isVictory)) + if (Coordinator.ShouldEnterSettlementFromActiveState(out bool didCombatWin)) { - Coordinator.ChangeState(new CombatSettlementState(Runtime, Coordinator, isVictory)); + Coordinator.ChangeState(new CombatSettlementState(Runtime, Coordinator, didCombatWin)); return; } diff --git a/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/CombatStates/CombatSettlementState.cs b/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/CombatStates/CombatSettlementState.cs index b365b38..09656f9 100644 --- a/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/CombatStates/CombatSettlementState.cs +++ b/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/CombatStates/CombatSettlementState.cs @@ -2,23 +2,23 @@ namespace GeometryTD.CustomComponent { internal sealed class CombatSettlementState : CombatStateBase { - private readonly bool _isVictory; + private readonly bool _didCombatWin; public CombatSettlementState( CombatSchedulerRuntime runtime, CombatSchedulerCoordinator coordinator, - bool isVictory) : base(runtime, coordinator) + bool didCombatWin) : base(runtime, coordinator) { - _isVictory = isVictory; + _didCombatWin = didCombatWin; } public override void OnEnter() { Runtime.EnemyManager.EndPhase(); Runtime.EnemyManager.CleanupTrackedEnemies(); - Runtime.IsFinishAsVictory = _isVictory; + Runtime.DidCombatWin = _didCombatWin; Runtime.SettlementContext = Runtime.CombatSettlementService.BuildSettlementContext( - _isVictory, + _didCombatWin, Runtime.CurrentLevel, Runtime.EnemyManager.DefeatedEnemyCount, Runtime.CombatRunResourceStore); diff --git a/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/CombatStates/CombatWaitingForPhaseEndState.cs b/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/CombatStates/CombatWaitingForPhaseEndState.cs index 6c9bccb..8a82d9f 100644 --- a/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/CombatStates/CombatWaitingForPhaseEndState.cs +++ b/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/CombatStates/CombatWaitingForPhaseEndState.cs @@ -22,9 +22,9 @@ namespace GeometryTD.CustomComponent Runtime.PhaseLoopRuntime.AdvancePhaseElapsed(elapseSeconds); - if (Coordinator.ShouldEnterSettlementFromActiveState(out bool isVictory)) + if (Coordinator.ShouldEnterSettlementFromActiveState(out bool didCombatWin)) { - Coordinator.ChangeState(new CombatSettlementState(Runtime, Coordinator, isVictory)); + Coordinator.ChangeState(new CombatSettlementState(Runtime, Coordinator, didCombatWin)); return; } diff --git a/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/CombatStates/CombatWaitingForReturnState.cs b/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/CombatStates/CombatWaitingForReturnState.cs index 593c2ac..fa84c0a 100644 --- a/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/CombatStates/CombatWaitingForReturnState.cs +++ b/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/CombatStates/CombatWaitingForReturnState.cs @@ -34,7 +34,7 @@ namespace GeometryTD.CustomComponent Runtime.LoadSession.Cleanup(); Coordinator.CloseCombatFinishForm(); Coordinator.CloseRewardSelectForm(); - Coordinator.CompleteNormalCombatAndNotify(Runtime.IsFinishAsVictory); + Coordinator.CompleteNormalCombatAndNotify(Runtime.DidCombatWin); } } } diff --git a/Assets/GameMain/Scripts/CustomComponent/EventNodeComponent.cs b/Assets/GameMain/Scripts/CustomComponent/EventNodeComponent.cs index 40c49e4..2d3c8e9 100644 --- a/Assets/GameMain/Scripts/CustomComponent/EventNodeComponent.cs +++ b/Assets/GameMain/Scripts/CustomComponent/EventNodeComponent.cs @@ -94,6 +94,7 @@ namespace GeometryTD.CustomComponent _activeNodeId, _activeNodeType, _activeSequenceIndex, + RunNodeCompletionStatus.Completed, true, GameEntry.PlayerInventory != null ? GameEntry.PlayerInventory.GetInventorySnapshot() : null)); ClearActiveNodeContext(); diff --git a/Assets/GameMain/Scripts/CustomComponent/ShopNodeComponent.cs b/Assets/GameMain/Scripts/CustomComponent/ShopNodeComponent.cs index 1d85e98..ccda831 100644 --- a/Assets/GameMain/Scripts/CustomComponent/ShopNodeComponent.cs +++ b/Assets/GameMain/Scripts/CustomComponent/ShopNodeComponent.cs @@ -59,6 +59,7 @@ namespace GeometryTD.CustomComponent _activeNodeId, _activeNodeType, _activeSequenceIndex, + RunNodeCompletionStatus.Completed, true, GameEntry.PlayerInventory != null ? GameEntry.PlayerInventory.GetInventorySnapshot() : null)); ClearActiveNodeContext(); diff --git a/Assets/GameMain/Scripts/Entity/EntityLogic/EnemyEntity.cs b/Assets/GameMain/Scripts/Entity/EntityLogic/EnemyEntity.cs index 58226ee..bd36282 100644 --- a/Assets/GameMain/Scripts/Entity/EntityLogic/EnemyEntity.cs +++ b/Assets/GameMain/Scripts/Entity/EntityLogic/EnemyEntity.cs @@ -114,12 +114,10 @@ namespace GeometryTD.Entity { GameEntry.HPBar?.ShowHPBar(this, (float)previousHealth / _maxHealth, (float)_currentHealth / _maxHealth); - Log.Info($"ShowBar: {_currentHealth}/{_maxHealth}"); } if (_currentHealth <= 0) { - Log.Info("Enemy Dead"); _killedEnemyEntityIds.Add(Id); RequestDespawn(); } diff --git a/Assets/GameMain/Scripts/Event/Combat/CombatFailureReturnEventArgs.cs b/Assets/GameMain/Scripts/Event/Combat/CombatFailureReturnEventArgs.cs deleted file mode 100644 index f134dd2..0000000 --- a/Assets/GameMain/Scripts/Event/Combat/CombatFailureReturnEventArgs.cs +++ /dev/null @@ -1,21 +0,0 @@ -using GameFramework; -using GameFramework.Event; - -namespace GeometryTD.CustomEvent -{ - public class CombatFailureReturnEventArgs : GameEventArgs - { - public static int EventId => typeof(CombatFailureReturnEventArgs).GetHashCode(); - - public override int Id => EventId; - - public static CombatFailureReturnEventArgs Create() - { - return ReferencePool.Acquire(); - } - - public override void Clear() - { - } - } -} diff --git a/Assets/GameMain/Scripts/Event/Combat/CombatFailureReturnEventArgs.cs.meta b/Assets/GameMain/Scripts/Event/Combat/CombatFailureReturnEventArgs.cs.meta deleted file mode 100644 index d6e2ab1..0000000 --- a/Assets/GameMain/Scripts/Event/Combat/CombatFailureReturnEventArgs.cs.meta +++ /dev/null @@ -1,3 +0,0 @@ -fileFormatVersion: 2 -guid: 7db1bf4f6f5f435d8d3086510e178d1d -timeCreated: 1772863200 diff --git a/Assets/GameMain/Scripts/Event/Game/NodeCompleteEventArgs.cs b/Assets/GameMain/Scripts/Event/Game/NodeCompleteEventArgs.cs index ead88be..248ba4d 100644 --- a/Assets/GameMain/Scripts/Event/Game/NodeCompleteEventArgs.cs +++ b/Assets/GameMain/Scripts/Event/Game/NodeCompleteEventArgs.cs @@ -20,7 +20,9 @@ namespace GeometryTD.CustomEvent public int SequenceIndex { get; private set; } - public bool Succeeded { get; private set; } + public RunNodeCompletionStatus CompletionStatus { get; private set; } + + public bool CombatWon { get; private set; } public BackpackInventoryData InventorySnapshotAfterNode { get; private set; } @@ -30,7 +32,7 @@ namespace GeometryTD.CustomEvent public static NodeCompleteEventArgs Create() { - return Create(null, 0, RunNodeType.None, -1, true, null); + return Create(null, 0, RunNodeType.None, -1, RunNodeCompletionStatus.Completed, true, null); } public static NodeCompleteEventArgs Create( @@ -38,7 +40,8 @@ namespace GeometryTD.CustomEvent int nodeId, RunNodeType nodeType, int sequenceIndex, - bool succeeded, + RunNodeCompletionStatus completionStatus, + bool combatWon, BackpackInventoryData inventorySnapshotAfterNode) { var args = ReferencePool.Acquire(); @@ -46,7 +49,8 @@ namespace GeometryTD.CustomEvent args.NodeId = nodeId; args.NodeType = nodeType; args.SequenceIndex = sequenceIndex; - args.Succeeded = succeeded; + args.CompletionStatus = completionStatus; + args.CombatWon = combatWon; args.InventorySnapshotAfterNode = InventoryCloneUtility.CloneInventory(inventorySnapshotAfterNode); return args; @@ -58,7 +62,8 @@ namespace GeometryTD.CustomEvent NodeId = 0; NodeType = RunNodeType.None; SequenceIndex = -1; - Succeeded = false; + CompletionStatus = RunNodeCompletionStatus.None; + CombatWon = false; InventorySnapshotAfterNode = null; } } diff --git a/Assets/GameMain/Scripts/Procedure/ProcedureMain.cs b/Assets/GameMain/Scripts/Procedure/ProcedureMain.cs index 0f0ddd5..b846abd 100644 --- a/Assets/GameMain/Scripts/Procedure/ProcedureMain.cs +++ b/Assets/GameMain/Scripts/Procedure/ProcedureMain.cs @@ -18,19 +18,26 @@ namespace GeometryTD.Procedure public enum ProcedureMainRunAdvanceResult { NoChange = 0, - NodeFailed = 1, + NodeException = 1, AdvancedToNextNode = 2, RunCompleted = 3 } + public enum ProcedureMainRunCompletionResult + { + NoChange = 0, + ShowCompletionDialog = 1, + ReturnToMenu = 2 + } + public static class ProcedureMainRunFlowService { public static ProcedureMainRunAdvanceResult TryAdvanceRun( RunState runState, - bool succeeded, + RunNodeCompletionStatus completionStatus, BackpackInventoryData snapshot) { - if (!RunStateAdvanceService.TryCompleteCurrentNode(runState, succeeded, snapshot)) + if (!RunStateAdvanceService.TryCompleteCurrentNode(runState, completionStatus, snapshot)) { return ProcedureMainRunAdvanceResult.NoChange; } @@ -40,9 +47,31 @@ namespace GeometryTD.Procedure return ProcedureMainRunAdvanceResult.RunCompleted; } - return succeeded - ? ProcedureMainRunAdvanceResult.AdvancedToNextNode - : ProcedureMainRunAdvanceResult.NodeFailed; + return completionStatus == RunNodeCompletionStatus.Exception + ? ProcedureMainRunAdvanceResult.NodeException + : ProcedureMainRunAdvanceResult.AdvancedToNextNode; + } + } + + public static class ProcedureMainRunCompletionService + { + public static ProcedureMainRunCompletionResult TryEnterCompletedPendingFinish(bool isCompletionDialogShown) + { + return isCompletionDialogShown + ? ProcedureMainRunCompletionResult.NoChange + : ProcedureMainRunCompletionResult.ShowCompletionDialog; + } + + public static ProcedureMainRunCompletionResult TryConfirmReturnToMenu( + ProcedureMainFlowPhase flowPhase, + bool isReturnToMenuPending) + { + if (flowPhase != ProcedureMainFlowPhase.RunCompletedPendingFinish || isReturnToMenuPending) + { + return ProcedureMainRunCompletionResult.NoChange; + } + + return ProcedureMainRunCompletionResult.ReturnToMenu; } } @@ -54,6 +83,8 @@ namespace GeometryTD.Procedure private NodeMapFormUseCase _nodeMapFormUseCase; private RunState _currentRunState; private ProcedureMainFlowPhase _flowPhase = ProcedureMainFlowPhase.Hub; + private bool _isRunCompleteDialogShown; + private bool _isReturnToMenuPending; #region FSM @@ -61,7 +92,6 @@ namespace GeometryTD.Procedure { base.OnEnter(procedureOwner); - GameEntry.Event.Subscribe(CombatFailureReturnEventArgs.EventId, OnCombatFailureReturn); GameEntry.Event.Subscribe(NodeCompleteEventArgs.EventId, OnNodeComplete); GameEntry.Event.Subscribe(NodeEnterEventArgs.EventId, OnNodeEnter); GameEntry.Event.Subscribe(NodeMapNodeEnterRequestedEventArgs.EventId, OnNodeMapNodeEnterRequested); @@ -83,6 +113,8 @@ namespace GeometryTD.Procedure _nodeMapFormUseCase = new NodeMapFormUseCase(); _nodeMapFormUseCase.SetRunState(_currentRunState); GameEntry.UIRouter.BindUIUseCase(UIFormType.NodeMapForm, _nodeMapFormUseCase); + _isRunCompleteDialogShown = false; + _isReturnToMenuPending = false; EnterHubFlow(); } @@ -92,6 +124,14 @@ namespace GeometryTD.Procedure { base.OnUpdate(procedureOwner, elapseSeconds, realElapseSeconds); + if (_isReturnToMenuPending) + { + _isReturnToMenuPending = false; + procedureOwner.SetData("NextSceneId", (int)SceneType.Menu); + ChangeState(procedureOwner); + return; + } + GameEntry.CombatNode.OnUpdate(elapseSeconds, realElapseSeconds); } @@ -99,16 +139,18 @@ namespace GeometryTD.Procedure { GameEntry.CombatNode.OnShutdown(); GameEntry.Event.Unsubscribe(NodeMapNodeEnterRequestedEventArgs.EventId, OnNodeMapNodeEnterRequested); - GameEntry.Event.Unsubscribe(CombatFailureReturnEventArgs.EventId, OnCombatFailureReturn); GameEntry.Event.Unsubscribe(NodeEnterEventArgs.EventId, OnNodeEnter); GameEntry.Event.Unsubscribe(NodeCompleteEventArgs.EventId, OnNodeComplete); GameEntry.UIRouter.CloseUI(UIFormType.RepoForm); GameEntry.UIRouter.CloseUI(UIFormType.NodeMapForm); GameEntry.UIRouter.CloseUI(UIFormType.MainForm); + GameEntry.UIRouter.CloseUI(UIFormType.DialogForm); _repoFormUseCase = null; _nodeMapFormUseCase = null; _currentRunState = null; _flowPhase = ProcedureMainFlowPhase.Hub; + _isRunCompleteDialogShown = false; + _isReturnToMenuPending = false; base.OnLeave(procedureOwner, isShutdown); } @@ -178,26 +220,23 @@ namespace GeometryTD.Procedure return; } + Log.Info( + "ProcedureMain.OnNodeComplete() accepted. RunId={0}, NodeId={1}, NodeType={2}, SequenceIndex={3}, CompletionStatus={4}, CombatWon={5}.", + string.IsNullOrWhiteSpace(args.RunId) ? _currentRunState?.RunId : args.RunId, + args.NodeId, + args.NodeType, + args.SequenceIndex, + args.CompletionStatus, + args.CombatWon); + BackpackInventoryData snapshot = args.InventorySnapshotAfterNode; if (snapshot == null && GameEntry.PlayerInventory != null) { snapshot = GameEntry.PlayerInventory.GetInventorySnapshot(); } - HandleRunAdvanceResult(ProcedureMainRunFlowService.TryAdvanceRun(_currentRunState, args.Succeeded, snapshot)); - } - - private void OnCombatFailureReturn(object sender, GameEventArgs e) - { - if (!(e is CombatFailureReturnEventArgs)) - { - return; - } - - BackpackInventoryData snapshot = GameEntry.PlayerInventory != null - ? GameEntry.PlayerInventory.GetInventorySnapshot() - : null; - HandleRunAdvanceResult(ProcedureMainRunFlowService.TryAdvanceRun(_currentRunState, false, snapshot)); + HandleRunAdvanceResult( + ProcedureMainRunFlowService.TryAdvanceRun(_currentRunState, args.CompletionStatus, snapshot)); } private void OnNodeMapNodeEnterRequested(object sender, GameEventArgs e) @@ -275,9 +314,9 @@ namespace GeometryTD.Procedure { case ProcedureMainRunAdvanceResult.NoChange: return; - case ProcedureMainRunAdvanceResult.NodeFailed: + case ProcedureMainRunAdvanceResult.NodeException: Log.Info( - "ProcedureMain current node failed. RunId={0}, NodeId={1}, NodeType={2}, SequenceIndex={3}.", + "ProcedureMain current node entered exception state. RunId={0}, NodeId={1}, NodeType={2}, SequenceIndex={3}.", _currentRunState?.RunId, _currentRunState?.CurrentNode?.NodeId ?? 0, _currentRunState?.CurrentNode?.NodeType ?? RunNodeType.None, @@ -322,6 +361,14 @@ namespace GeometryTD.Procedure _nodeMapFormUseCase?.SetRunState(_currentRunState); CloseHubUI(); Log.Info("ProcedureMain run completed. RunId={0}", _currentRunState?.RunId); + + ProcedureMainRunCompletionResult result = + ProcedureMainRunCompletionService.TryEnterCompletedPendingFinish(_isRunCompleteDialogShown); + if (result == ProcedureMainRunCompletionResult.ShowCompletionDialog) + { + _isRunCompleteDialogShown = true; + OpenRunCompleteDialog(); + } } private void CloseHubUI() @@ -345,5 +392,37 @@ namespace GeometryTD.Procedure nextNode.SequenceIndex, nextNode.LinkedLevelId); } + + private void OpenRunCompleteDialog() + { + GameEntry.UIRouter.OpenUI(UIFormType.DialogForm, new DialogFormRawData + { + Mode = 1, + Title = "Run Complete", + Message = "Boss node completed. This run is finished and will return to the main menu.", + PauseGame = false, + ConfirmText = "Return to Menu", + OnClickConfirm = OnRunCompleteDialogConfirmed + }); + } + + private void OnRunCompleteDialogConfirmed(object userData) + { + _ = userData; + + ProcedureMainRunCompletionResult result = ProcedureMainRunCompletionService.TryConfirmReturnToMenu( + _flowPhase, + _isReturnToMenuPending); + if (result != ProcedureMainRunCompletionResult.ReturnToMenu) + { + Log.Warning( + "ProcedureMain ignored run completion confirm. FlowPhase={0}, ReturnPending={1}.", + _flowPhase, + _isReturnToMenuPending); + return; + } + + _isReturnToMenuPending = true; + } } } diff --git a/Assets/GameMain/Scripts/Procedure/RunModel.cs b/Assets/GameMain/Scripts/Procedure/RunModel.cs index c4910e2..99f0c35 100644 --- a/Assets/GameMain/Scripts/Procedure/RunModel.cs +++ b/Assets/GameMain/Scripts/Procedure/RunModel.cs @@ -19,10 +19,17 @@ namespace GeometryTD.Procedure Locked = 0, Available = 1, Completed = 2, - Failed = 3, + Exception = 3, Skipped = 4 } + public enum RunNodeCompletionStatus + { + None = 0, + Completed = 1, + Exception = 2 + } + [Serializable] public sealed class RunNodeSeed { diff --git a/Assets/GameMain/Scripts/Procedure/RunStateAdvanceService.cs b/Assets/GameMain/Scripts/Procedure/RunStateAdvanceService.cs index 7625940..b59f2cd 100644 --- a/Assets/GameMain/Scripts/Procedure/RunStateAdvanceService.cs +++ b/Assets/GameMain/Scripts/Procedure/RunStateAdvanceService.cs @@ -6,7 +6,7 @@ namespace GeometryTD.Procedure { public static bool TryCompleteCurrentNode( RunState runState, - bool succeeded, + RunNodeCompletionStatus completionStatus, BackpackInventoryData inventorySnapshotAfterNode) { if (runState == null || runState.IsCompleted) @@ -22,12 +22,17 @@ namespace GeometryTD.Procedure runState.ReplaceInventorySnapshot(inventorySnapshotAfterNode); - if (!succeeded) + if (completionStatus == RunNodeCompletionStatus.Exception) { - currentNode.Status = RunNodeStatus.Failed; + currentNode.Status = RunNodeStatus.Exception; return true; } + if (completionStatus != RunNodeCompletionStatus.Completed) + { + return false; + } + currentNode.Status = RunNodeStatus.Completed; int nextIndex = runState.CurrentNodeIndex + 1; diff --git a/Assets/GameMain/Scripts/UI/Game/Context/NodeItemContext.cs b/Assets/GameMain/Scripts/UI/Game/Context/NodeItemContext.cs index 391abc8..db22b88 100644 --- a/Assets/GameMain/Scripts/UI/Game/Context/NodeItemContext.cs +++ b/Assets/GameMain/Scripts/UI/Game/Context/NodeItemContext.cs @@ -10,7 +10,7 @@ namespace GeometryTD.UI public bool IsLocked; public bool IsCurrent; public bool IsCompleted; - public bool IsFailed; + public bool IsException; public bool CanClick; } } diff --git a/Assets/GameMain/Scripts/UI/Game/Controller/NodeMapFormController.cs b/Assets/GameMain/Scripts/UI/Game/Controller/NodeMapFormController.cs index 327beb8..a36e02e 100644 --- a/Assets/GameMain/Scripts/UI/Game/Controller/NodeMapFormController.cs +++ b/Assets/GameMain/Scripts/UI/Game/Controller/NodeMapFormController.cs @@ -143,7 +143,7 @@ namespace GeometryTD.UI IsLocked = node != null && node.Status == RunNodeStatus.Locked, IsCurrent = node != null && node.IsCurrentNode, IsCompleted = node != null && node.Status == RunNodeStatus.Completed, - IsFailed = node != null && node.Status == RunNodeStatus.Failed, + IsException = node != null && node.Status == RunNodeStatus.Exception, CanClick = node != null && node.CanEnter }; } diff --git a/Assets/GameMain/Scripts/UI/Game/UseCase/NodeMapFormUseCase.cs b/Assets/GameMain/Scripts/UI/Game/UseCase/NodeMapFormUseCase.cs index eb5408b..c47cebc 100644 --- a/Assets/GameMain/Scripts/UI/Game/UseCase/NodeMapFormUseCase.cs +++ b/Assets/GameMain/Scripts/UI/Game/UseCase/NodeMapFormUseCase.cs @@ -144,8 +144,8 @@ namespace GeometryTD.UI return "可进入"; case RunNodeStatus.Completed: return "已完成"; - case RunNodeStatus.Failed: - return "失败"; + case RunNodeStatus.Exception: + return "异常"; case RunNodeStatus.Skipped: return "跳过"; default: diff --git a/Assets/GameMain/Scripts/UI/Game/View/NodeItem.cs b/Assets/GameMain/Scripts/UI/Game/View/NodeItem.cs index bc8a3b9..bf190c4 100644 --- a/Assets/GameMain/Scripts/UI/Game/View/NodeItem.cs +++ b/Assets/GameMain/Scripts/UI/Game/View/NodeItem.cs @@ -19,7 +19,7 @@ namespace GeometryTD.UI [SerializeField] private Color _lockedColor = Color.gray; [SerializeField] private Color _activeColor = Color.cyan; [SerializeField] private Color _passedColor = Color.green; - [SerializeField] private Color _failedColor = Color.red; + [SerializeField] private Color _exceptionColor = Color.red; [SerializeField] private Color _defaultIconColor = Color.white; [SerializeField] private Color _lockedIconColor = Color.gray; @@ -44,9 +44,9 @@ namespace GeometryTD.UI { targetColor = _passedColor; } - else if (context.IsFailed) + else if (context.IsException) { - targetColor = _failedColor; + targetColor = _exceptionColor; } else if (context.IsCurrent) { diff --git a/Assets/Tests/EditMode/ProcedureMainServicesTests.cs b/Assets/Tests/EditMode/ProcedureMainServicesTests.cs new file mode 100644 index 0000000..e0ce049 --- /dev/null +++ b/Assets/Tests/EditMode/ProcedureMainServicesTests.cs @@ -0,0 +1,139 @@ +using GeometryTD.Definition; +using GeometryTD.Procedure; +using NUnit.Framework; + +namespace GeometryTD.Tests.EditMode +{ + public sealed class ProcedureMainServicesTests + { + [Test] + public void TryAdvanceRun_Returns_AdvancedToNextNode_For_Normal_Completion() + { + RunState runState = CreateTwoNodeRun(); + + ProcedureMainRunAdvanceResult result = ProcedureMainRunFlowService.TryAdvanceRun( + runState, + RunNodeCompletionStatus.Completed, + new BackpackInventoryData { Gold = 88 }); + + Assert.That(result, Is.EqualTo(ProcedureMainRunAdvanceResult.AdvancedToNextNode)); + Assert.That(runState.IsCompleted, Is.False); + Assert.That(runState.Nodes[0].Status, Is.EqualTo(RunNodeStatus.Completed)); + Assert.That(runState.CurrentNodeIndex, Is.EqualTo(1)); + Assert.That(runState.CurrentNode.Status, Is.EqualTo(RunNodeStatus.Available)); + Assert.That(runState.RunInventorySnapshot.Gold, Is.EqualTo(88)); + } + + [Test] + public void TryAdvanceRun_Returns_NodeException_For_Exception_Fallback() + { + RunState runState = CreateTwoNodeRun(); + + ProcedureMainRunAdvanceResult result = ProcedureMainRunFlowService.TryAdvanceRun( + runState, + RunNodeCompletionStatus.Exception, + new BackpackInventoryData { Gold = 5 }); + + Assert.That(result, Is.EqualTo(ProcedureMainRunAdvanceResult.NodeException)); + Assert.That(runState.IsCompleted, Is.False); + Assert.That(runState.CurrentNodeIndex, Is.EqualTo(0)); + Assert.That(runState.CurrentNode.Status, Is.EqualTo(RunNodeStatus.Exception)); + Assert.That(runState.Nodes[1].Status, Is.EqualTo(RunNodeStatus.Locked)); + Assert.That(runState.RunInventorySnapshot.Gold, Is.EqualTo(5)); + } + + [Test] + public void TryAdvanceRun_Returns_RunCompleted_When_Last_Node_Finishes() + { + RunState runState = RunStateFactory.Create( + LevelThemeType.Plain, + new BackpackInventoryData { Gold = 30 }, + new[] + { + new RunNodeSeed { NodeId = 901, NodeType = RunNodeType.BossCombat, LinkedLevelId = 4 } + }); + + ProcedureMainRunAdvanceResult result = ProcedureMainRunFlowService.TryAdvanceRun( + runState, + RunNodeCompletionStatus.Completed, + new BackpackInventoryData { Gold = 123 }); + + Assert.That(result, Is.EqualTo(ProcedureMainRunAdvanceResult.RunCompleted)); + Assert.That(runState.IsCompleted, Is.True); + Assert.That(runState.CurrentNodeIndex, Is.EqualTo(1)); + Assert.That(runState.CompletedNodeCount, Is.EqualTo(1)); + Assert.That(runState.RunInventorySnapshot.Gold, Is.EqualTo(123)); + } + + [Test] + public void TryAdvanceRun_Returns_NoChange_When_Run_Cannot_Advance() + { + ProcedureMainRunAdvanceResult nullRunResult = ProcedureMainRunFlowService.TryAdvanceRun( + null, + RunNodeCompletionStatus.Completed, + new BackpackInventoryData { Gold = 1 }); + + Assert.That(nullRunResult, Is.EqualTo(ProcedureMainRunAdvanceResult.NoChange)); + + RunState completedRun = RunStateFactory.Create( + LevelThemeType.Plain, + new BackpackInventoryData { Gold = 10 }, + new RunNodeSeed[0]); + + ProcedureMainRunAdvanceResult completedRunResult = ProcedureMainRunFlowService.TryAdvanceRun( + completedRun, + RunNodeCompletionStatus.Completed, + new BackpackInventoryData { Gold = 20 }); + + Assert.That(completedRunResult, Is.EqualTo(ProcedureMainRunAdvanceResult.NoChange)); + Assert.That(completedRun.RunInventorySnapshot.Gold, Is.EqualTo(10)); + } + + [Test] + public void TryEnterCompletedPendingFinish_Shows_Dialog_Only_Once() + { + ProcedureMainRunCompletionResult firstResult = + ProcedureMainRunCompletionService.TryEnterCompletedPendingFinish(false); + ProcedureMainRunCompletionResult secondResult = + ProcedureMainRunCompletionService.TryEnterCompletedPendingFinish(true); + + Assert.That(firstResult, Is.EqualTo(ProcedureMainRunCompletionResult.ShowCompletionDialog)); + Assert.That(secondResult, Is.EqualTo(ProcedureMainRunCompletionResult.NoChange)); + } + + [Test] + public void TryConfirmReturnToMenu_Returns_Menu_Only_In_Completed_Pending_Finish() + { + ProcedureMainRunCompletionResult validResult = + ProcedureMainRunCompletionService.TryConfirmReturnToMenu( + ProcedureMainFlowPhase.RunCompletedPendingFinish, + false); + + ProcedureMainRunCompletionResult hubResult = + ProcedureMainRunCompletionService.TryConfirmReturnToMenu( + ProcedureMainFlowPhase.Hub, + false); + + ProcedureMainRunCompletionResult pendingResult = + ProcedureMainRunCompletionService.TryConfirmReturnToMenu( + ProcedureMainFlowPhase.RunCompletedPendingFinish, + true); + + Assert.That(validResult, Is.EqualTo(ProcedureMainRunCompletionResult.ReturnToMenu)); + Assert.That(hubResult, Is.EqualTo(ProcedureMainRunCompletionResult.NoChange)); + Assert.That(pendingResult, Is.EqualTo(ProcedureMainRunCompletionResult.NoChange)); + } + + private static RunState CreateTwoNodeRun() + { + return RunStateFactory.Create( + LevelThemeType.Plain, + new BackpackInventoryData { Gold = 50 }, + new[] + { + new RunNodeSeed { NodeId = 101, NodeType = RunNodeType.Combat, LinkedLevelId = 1 }, + new RunNodeSeed { NodeId = 102, NodeType = RunNodeType.Event } + }); + } + } +} diff --git a/Assets/Tests/EditMode/ProcedureMainServicesTests.cs.meta b/Assets/Tests/EditMode/ProcedureMainServicesTests.cs.meta new file mode 100644 index 0000000..3745da9 --- /dev/null +++ b/Assets/Tests/EditMode/ProcedureMainServicesTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: bb7e69f8c974497c9431d4e1d9e6e8b4 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Tests/EditMode/RunStateTests.cs b/Assets/Tests/EditMode/RunStateTests.cs index 2b73358..9dc1d3f 100644 --- a/Assets/Tests/EditMode/RunStateTests.cs +++ b/Assets/Tests/EditMode/RunStateTests.cs @@ -59,7 +59,7 @@ namespace GeometryTD.Tests.EditMode bool firstCompleted = RunStateAdvanceService.TryCompleteCurrentNode( runState, - true, + RunNodeCompletionStatus.Completed, new BackpackInventoryData { Gold = 80 }); Assert.That(firstCompleted, Is.True); @@ -68,8 +68,14 @@ namespace GeometryTD.Tests.EditMode Assert.That(runState.Nodes[1].Status, Is.EqualTo(RunNodeStatus.Available)); Assert.That(runState.RunInventorySnapshot.Gold, Is.EqualTo(80)); - RunStateAdvanceService.TryCompleteCurrentNode(runState, true, new BackpackInventoryData { Gold = 95 }); - RunStateAdvanceService.TryCompleteCurrentNode(runState, true, new BackpackInventoryData { Gold = 130 }); + RunStateAdvanceService.TryCompleteCurrentNode( + runState, + RunNodeCompletionStatus.Completed, + new BackpackInventoryData { Gold = 95 }); + RunStateAdvanceService.TryCompleteCurrentNode( + runState, + RunNodeCompletionStatus.Completed, + new BackpackInventoryData { Gold = 130 }); Assert.That(runState.IsCompleted, Is.True); Assert.That(runState.CurrentNodeIndex, Is.EqualTo(3)); @@ -78,7 +84,7 @@ namespace GeometryTD.Tests.EditMode } [Test] - public void AdvanceService_Failure_Marks_Current_Node_Failed_Without_Completing_Run() + public void AdvanceService_Exception_Marks_Current_Node_Exception_Without_Completing_Run() { RunState runState = RunStateFactory.Create( LevelThemeType.Plain, @@ -90,13 +96,13 @@ namespace GeometryTD.Tests.EditMode bool result = RunStateAdvanceService.TryCompleteCurrentNode( runState, - false, + RunNodeCompletionStatus.Exception, new BackpackInventoryData { Gold = 5 }); Assert.That(result, Is.True); Assert.That(runState.IsCompleted, Is.False); Assert.That(runState.CurrentNodeIndex, Is.EqualTo(0)); - Assert.That(runState.CurrentNode.Status, Is.EqualTo(RunNodeStatus.Failed)); + Assert.That(runState.CurrentNode.Status, Is.EqualTo(RunNodeStatus.Exception)); Assert.That(runState.RunInventorySnapshot.Gold, Is.EqualTo(5)); } @@ -109,6 +115,7 @@ namespace GeometryTD.Tests.EditMode 7, RunNodeType.Shop, 3, + RunNodeCompletionStatus.Completed, true, inventory); @@ -118,7 +125,8 @@ namespace GeometryTD.Tests.EditMode Assert.That(eventArgs.NodeId, Is.EqualTo(7)); Assert.That(eventArgs.NodeType, Is.EqualTo(RunNodeType.Shop)); Assert.That(eventArgs.SequenceIndex, Is.EqualTo(3)); - Assert.That(eventArgs.Succeeded, Is.True); + Assert.That(eventArgs.CompletionStatus, Is.EqualTo(RunNodeCompletionStatus.Completed)); + Assert.That(eventArgs.CombatWon, Is.True); Assert.That(eventArgs.InventorySnapshotAfterNode.Gold, Is.EqualTo(66)); eventArgs.Clear(); @@ -127,7 +135,8 @@ namespace GeometryTD.Tests.EditMode Assert.That(eventArgs.NodeId, Is.EqualTo(0)); Assert.That(eventArgs.NodeType, Is.EqualTo(RunNodeType.None)); Assert.That(eventArgs.SequenceIndex, Is.EqualTo(-1)); - Assert.That(eventArgs.Succeeded, Is.False); + Assert.That(eventArgs.CompletionStatus, Is.EqualTo(RunNodeCompletionStatus.None)); + Assert.That(eventArgs.CombatWon, Is.False); Assert.That(eventArgs.InventorySnapshotAfterNode, Is.Null); } @@ -189,7 +198,7 @@ namespace GeometryTD.Tests.EditMode { bool advanced = RunStateAdvanceService.TryCompleteCurrentNode( runState, - true, + RunNodeCompletionStatus.Completed, new BackpackInventoryData { Gold = 100 + i }); Assert.That(advanced, Is.True); diff --git a/docs/CodeX-TODO.md b/docs/CodeX-TODO.md index b9a8006..26c74b3 100644 --- a/docs/CodeX-TODO.md +++ b/docs/CodeX-TODO.md @@ -16,9 +16,46 @@ | 状态 | ID | 任务 | 交付物路径 | 验收标准 | |-----|-------|--------------------------------|----------------------------------------------------------------------------|--------------------------| | [x] | S1-01 | 统一梳理 M1 当前状态与文档口径 | `docs/CodeX-TODO.md`
`docs/TODO.md` | 当前实现、目标状态、未完成项三者口径一致 | -| [ ] | S1-02 | 收口 `ProcedureMain` 的 Run 推进主链路 | `Assets/GameMain/Scripts/Procedure/` | 从开始游戏到 Boss 结算可稳定走完整条链 | -| [ ] | S1-03 | 收口节点进入、完成、失败后的统一回流 | `Assets/GameMain/Scripts/Procedure/`
`Assets/GameMain/Scripts/Event/` | 战斗 / 事件 / 商店都能统一推进当前 Run | -| [ ] | S1-04 | 收口 Run 完成后的正式结束态 | `Assets/GameMain/Scripts/Procedure/`
`Assets/GameMain/Scripts/UI/Game/` | 10 节点完成后有明确的结束表现与收尾逻辑 | +| [x] | S1-02 | 收口 `ProcedureMain` 的 Run 推进主链路 | `Assets/GameMain/Scripts/Procedure/` | 从开始游戏到 Boss 结算可稳定走完整条链 | +| [x] | S1-03 | 收口节点进入、完成、失败后的统一回流 | `Assets/GameMain/Scripts/Procedure/`
`Assets/GameMain/Scripts/Event/` | 战斗 / 事件 / 商店都能统一推进当前 Run | +| [x] | S1-04 | 收口 Run 完成后的正式结束态 | `Assets/GameMain/Scripts/Procedure/`
`Assets/GameMain/Scripts/UI/Game/` | 10 节点完成后有明确的结束表现与收尾逻辑 | + +## S1 验收清单 + +### 手工流程验收 + +- [x] 从主菜单进入游戏后,能进入 `ProcedureMain`,并看到 `NodeMapForm + MainForm`。 +- [x] 当前节点可进入,非当前节点不可进入。 +- [x] 普通战斗节点胜利后,会返回节点地图;当前节点变 `Completed`,下一节点变可进入。 +- [x] 普通战斗节点在正常非胜利结算后(例如基地血量归零),会返回节点地图;当前节点仍按 `Completed` 处理,并推进到下一节点。 +- [x] 战斗节点只在出现异常回退时,才会返回节点地图并把当前节点标记为 `Exception`;不会错误推进到下一节点。 +- [x] 事件节点完成后,会返回节点地图,并推进到下一节点。 +- [x] 商店节点退出后,会返回节点地图,并推进到下一节点。 +- [x] 连续完成到第 10 个 Boss 节点后,不会再回普通 `NodeMapForm`。 +- [x] Boss 完成后,会弹出正式结束对话框,而不是只停在流程内部状态。 +- [x] 点击结束对话框确认后,会切回 `Menu` 场景并重新进入主菜单。 +- [x] 回到主菜单后,不残留 `NodeMapForm`、`MainForm`、`DialogForm`。 + +### 运行时状态验收 + +- [x] `Hub` 阶段下,只允许当前节点进入。 +- [x] `NodeActive` 阶段下,Hub UI 已关闭,不能重复从节点地图进入节点。 +- [x] 普通节点成功后,会回到 `Hub`。 +- [x] 节点发生异常回退后,会回到 `Hub`,但不会推进下一节点。 +- [x] Boss 成功完成后,会进入 `RunCompletedPendingFinish`,并由正式结束态接管。 +- [x] 正式结束态确认后,会走 `ProcedureChangeScene -> ProcedureMenu` 的回菜单链路。 + +### 回归测试验收 + +- [x] Unity Test Runner 的 `Assets/Tests/EditMode` 全部通过。 +- [x] `RunState` 创建、固定序列、成功推进、异常标记相关测试全部通过。 +- [x] `NodeCompleteEventArgs` 正常完成、正常非胜利、异常回退三种语义与库存快照 clone 测试全部通过。 +- [x] `ProcedureMainRunFlowService` 的成功推进、异常回退、`RunCompleted`、`NoChange` 分支测试已补齐并全部通过。 +- [x] `ProcedureMainRunCompletionService` 的“只弹一次结束对话框”和“只在完成态允许回菜单”测试已补齐并全部通过。 + +### S1 通过标准 + +- [x] 从主菜单开始,一条 Run 可以稳定经历“节点进入 -> 节点完成 / 异常回流 -> Boss 完成 -> 正式结束态 -> 返回主菜单”,且过程中不会出现错误推进、重复进入、Boss 后回到普通 Hub、或 UI 残留。 ## S1-01 对齐结论 diff --git a/docs/TODO.md b/docs/TODO.md index d737cc0..25b6564 100644 --- a/docs/TODO.md +++ b/docs/TODO.md @@ -11,8 +11,8 @@ ## M1 当前口径(2026-03-08) -- 当前仓库已经具备 `ProcedureMain + NodeMapForm + CombatNode + EventNode + ShopNode` 的临时 Run 闭环,可以创建固定 10 节点流程并推进到各类节点入口。 -- M1 现在的真实缺口,不是“有没有 Run 雏形”,而是“能否稳定完成 Boss 后收尾、统一节点回流、把节点地图与合法出战规则收口成正式口径”。 +- 当前仓库已经具备 `ProcedureMain + NodeMapForm + CombatNode + EventNode + ShopNode` 的主流程 Run 闭环,可以从主菜单进入游戏,完成固定 10 节点流程,并在 Boss 结算后进入正式结束态并回到主菜单。 +- M1 现在的真实缺口,不是“有没有 Run 雏形”,而是“节点地图表现是否收口为正式口径,以及合法出战 / 品质 / Tag / 耐久规则是否真正统一收口”。 - `P0-10 ~ P0-12` 在代码里都已有局部实现,因此文档里统一按“部分完成但未收口”处理;只有满足最终验收标准后才可改成完成。 ## 里程碑 M1(P0)- 最小可玩闭环 @@ -22,9 +22,9 @@ | [x] | P0-01 | 冻结 MVP 范围(只保留:战斗节点/事件节点/商店节点/节点后组装) | `docs/MVP-Scope.md` | 明确“做/不做”清单,团队评审通过 | | [x] | P0-02 | 补齐数据表:组件、配件、敌人、波次、节点、事件、商店商品 | `Assets/GameMain/DataTables/*.txt` | 游戏启动可无报错加载全部新增表 | | [x] | P0-03 | 新增/完善 DataRow 解析类 | `Assets/GameMain/Scripts/DataTable/*.cs` | 每个新增表都有对应 DR 类并成功解析 | -| [~] | P0-04 | 建立单局 Run 状态模型(金币、生命、库存、当前节点) | `Assets/GameMain/Scripts/Procedure/` | 已有 `RunState` / 推进模型 / 测试,并已接入 `ProcedureMain + NodeMapForm` 的临时 Run 闭环 | -| [~] | P0-05 | 实现 10 节点地图生成(最后节点固定 Boss) | `Assets/GameMain/Scripts/Procedure/`
`Assets/GameMain/Scripts/Scene/` | 已有固定 10 节点序列 builder 与测试,且已由 `NodeMapForm` 驱动节点入口,但节点事件上下文与地图表现层仍未完成 | -| [~] | P0-06 | 实现节点选择与跳转流程(战斗/事件/商店) | `Assets/GameMain/Scripts/Procedure/` | `ProcedureMain + NodeMapForm` 的临时闭环已可推进战斗/事件/商店,但仍缺完整地图表现、正式结算收尾与节点上下文回流 | +| [x] | P0-04 | 建立单局 Run 状态模型(金币、生命、库存、当前节点) | `Assets/GameMain/Scripts/Procedure/` | 已有 `RunState` / 推进模型 / 固定序列 / EditMode 测试,并已接入 `ProcedureMain` 主流程闭环 | +| [~] | P0-05 | 实现 10 节点地图生成(最后节点固定 Boss) | `Assets/GameMain/Scripts/Procedure/`
`Assets/GameMain/Scripts/Scene/` | 已有固定 10 节点序列、当前节点限制与 Boss 终点链路,但 `NodeMapForm` 表现层仍未收口为正式节点地图 | +| [x] | P0-06 | 实现节点选择与跳转流程(战斗/事件/商店) | `Assets/GameMain/Scripts/Procedure/` | 战斗 / 事件 / 商店已统一接入节点进入、完成、异常回流;Boss 完成后会进入正式结束态并返回主菜单 | | [x] | P0-07 | 战斗节点基础玩法:放置塔、出怪、基地扣血、胜负判定 | `Assets/GameMain/Scripts/Entity/`
`Assets/GameMain/Scripts/Scene/` | 可完整打一场并得到胜利/失败结果 | | [x] | P0-08 | 胜利波次与结算规则(100/80/50/<50) | `Assets/GameMain/Scripts/Procedure/` | 结算奖励与惩罚严格匹配设计文档 | | [x] | P0-09 | 敌人掉落与关卡奖励(组件/配件/金币) | `Assets/GameMain/Scripts/Entity/` | 战斗结束能发放掉落并写入库存 | @@ -56,12 +56,12 @@ | [ ] | P2-03 | 组装/拆解交互优化(预览属性变化) | `Assets/GameMain/Scripts/UI/Templates/GameScene/` | 改装前后差异可视化展示 | | [ ] | P2-04 | 存档与读档(局外货币、库存、解锁进度) | `Assets/GameMain/Scripts/Utility/`
`Assets/GameMain/Scripts/Procedure/` | 重启游戏后进度一致恢复 | | [ ] | P2-05 | 平衡性首轮调参(敌人曲线、经济曲线、掉落曲线) | `Assets/GameMain/DataTables/*.txt` | 3 局平均时长与胜率落在预期区间 | -| [ ] | P2-06 | 最低限度自动化测试(公式与关键流程) | `Assets/` 下新增 `Editor`/`Runtime` 测试 | 关键计算与流程回归可自动验证 | +| [~] | P2-06 | 最低限度自动化测试(公式与关键流程) | `Assets/` 下新增 `Editor`/`Runtime` 测试 | 已有 `Assets/Tests/EditMode` 覆盖 `RunState`、`NodeCompleteEventArgs`、`ProcedureMain` 关键服务;更广的公式与流程回归仍待补齐 | | [ ] | P2-07 | 性能与稳定性检查(长局、内存、异常日志) | `docs/PerformanceReport.md` | 连续游玩 30 分钟无阻断性问题 | ## 本周建议开工顺序 -1. 先把 `P0-04` ~ `P0-06` 从“`NodeMapForm` 临时闭环”收口成正式主流程,并补齐 Boss 完成后的结束态与节点回流口径 +1. 先把 `P0-05` 的 `NodeMapForm` 表现层从当前占位地图收口成正式节点地图 2. 完成 `P0-10`(把“至少有参战塔”提升为“满足完整合法参战条件才能出战”) 3. 再处理 `P0-11` ~ `P0-12`(先统一 M1 范围,再决定是完整收口还是同步缩范围)