using System.Text; using GameFramework.Event; using GameFramework.Fsm; using GameFramework.Procedure; using GeometryTD.CustomEvent; using GeometryTD.Definition; using GeometryTD.UI; using UnityGameFramework.Runtime; namespace GeometryTD.Procedure { public enum ProcedureMainFlowPhase { Hub = 0, NodeActive = 1, RunCompletedPendingFinish = 2 } public enum ProcedureMainRunAdvanceResult { NoChange = 0, NodeException = 1, AdvancedToNextNode = 2, RunCompleted = 3 } public enum ProcedureMainRunCompletionResult { NoChange = 0, ShowCompletionDialog = 1, ReturnToMenu = 2 } public enum ProcedureMainCombatEntryBlockReason { None = 0, InventoryUnavailable = 1, NoValidParticipantTower = 2 } public sealed class ProcedureMainCombatEntryValidationResult { public bool CanEnterCombat => BlockReason == ProcedureMainCombatEntryBlockReason.None; public ProcedureMainCombatEntryBlockReason BlockReason { get; set; } public CombatParticipantTowerValidationSummary ValidationSummary { get; set; } } public static class ProcedureMainRunFlowService { public static ProcedureMainRunAdvanceResult TryAdvanceRun( RunState runState, RunNodeCompletionStatus completionStatus, BackpackInventoryData snapshot) { if (!RunStateAdvanceService.TryCompleteCurrentNode(runState, completionStatus, snapshot)) { return ProcedureMainRunAdvanceResult.NoChange; } if (runState != null && runState.IsCompleted) { return ProcedureMainRunAdvanceResult.RunCompleted; } 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; } } public static class ProcedureMainNodeEventGuardService { public static bool MatchesCurrentNode( RunState runState, int nodeId, RunNodeType nodeType, int sequenceIndex) { RunNodeState currentNode = runState?.CurrentNode; if (currentNode == null) { return false; } return (nodeId <= 0 || nodeId == currentNode.NodeId) && (nodeType == RunNodeType.None || nodeType == currentNode.NodeType) && (sequenceIndex < 0 || sequenceIndex == currentNode.SequenceIndex); } } public static class ProcedureMainCombatEntryValidationService { public static ProcedureMainCombatEntryValidationResult Validate(BackpackInventoryData inventory) { if (inventory == null) { return new ProcedureMainCombatEntryValidationResult { BlockReason = ProcedureMainCombatEntryBlockReason.InventoryUnavailable, ValidationSummary = new CombatParticipantTowerValidationSummary() }; } CombatParticipantTowerValidationSummary summary = CombatParticipantTowerValidationService.ValidateParticipantTowers(inventory); return new ProcedureMainCombatEntryValidationResult { BlockReason = summary.HasAnyValidParticipantTower ? ProcedureMainCombatEntryBlockReason.None : ProcedureMainCombatEntryBlockReason.NoValidParticipantTower, ValidationSummary = summary }; } public static string BuildInvalidParticipantTowerLog( CombatParticipantTowerValidationSummary summary) { if (summary?.InvalidResults == null || summary.InvalidResults.Count <= 0) { return "none"; } StringBuilder builder = new StringBuilder(); for (int i = 0; i < summary.InvalidResults.Count; i++) { CombatParticipantTowerValidationResult result = summary.InvalidResults[i]; if (result == null) { continue; } if (builder.Length > 0) { builder.Append(", "); } builder.Append('#'); builder.Append(result.TowerInstanceId); builder.Append(':'); builder.Append(result.FailureReason); } return builder.Length > 0 ? builder.ToString() : "none"; } public static DialogFormRawData BuildBlockedCombatDialogRawData( ProcedureMainCombatEntryValidationResult validationResult) { return new DialogFormRawData { Mode = 1, Title = "无法进入战斗", Message = BuildBlockedCombatDialogMessage(validationResult), PauseGame = false, ConfirmText = "知道了" }; } private static string BuildBlockedCombatDialogMessage( ProcedureMainCombatEntryValidationResult validationResult) { if (validationResult == null) { return "当前无法确认出战信息,请稍后重试。"; } switch (validationResult.BlockReason) { case ProcedureMainCombatEntryBlockReason.InventoryUnavailable: return "当前无法读取库存快照,暂时不能进入战斗。请重新进入本轮流程后再试。"; case ProcedureMainCombatEntryBlockReason.NoValidParticipantTower: return BuildNoValidParticipantTowerMessage(validationResult.ValidationSummary); default: return "当前出战校验未通过,暂时不能进入战斗。"; } } private static string BuildNoValidParticipantTowerMessage( CombatParticipantTowerValidationSummary summary) { if (summary?.InvalidResults == null || summary.InvalidResults.Count <= 0) { return "参战区至少需要 1 座完整装配了枪口、轴承、底座的塔,才能进入战斗。"; } StringBuilder builder = new StringBuilder(); builder.Append("参战区没有可出战的完整塔。"); for (int i = 0; i < summary.InvalidResults.Count; i++) { CombatParticipantTowerValidationResult result = summary.InvalidResults[i]; if (result == null) { continue; } builder.Append('\n'); builder.Append("塔 #"); builder.Append(result.TowerInstanceId); builder.Append(' '); builder.Append(GetFailureReasonMessage(result.FailureReason)); } return builder.ToString(); } private static string GetFailureReasonMessage(CombatParticipantTowerValidationFailureReason failureReason) { switch (failureReason) { case CombatParticipantTowerValidationFailureReason.TowerMissing: return "已不存在,无法参战。"; case CombatParticipantTowerValidationFailureReason.MissingMuzzleComponent: return "缺少枪口组件。"; case CombatParticipantTowerValidationFailureReason.MissingBearingComponent: return "缺少轴承组件。"; case CombatParticipantTowerValidationFailureReason.MissingBaseComponent: return "缺少底座组件。"; default: return "不满足当前参战条件。"; } } } public class ProcedureMain : ProcedureBase { public override bool UseNativeDialog => false; private RepoFormUseCase _repoFormUseCase; private NodeMapFormUseCase _nodeMapFormUseCase; private RunState _currentRunState; private ProcedureMainFlowPhase _flowPhase = ProcedureMainFlowPhase.Hub; private bool _isRunCompleteDialogShown; private bool _isReturnToMenuPending; #region FSM protected override void OnEnter(IFsm procedureOwner) { base.OnEnter(procedureOwner); GameEntry.Event.Subscribe(NodeCompleteEventArgs.EventId, OnNodeComplete); GameEntry.Event.Subscribe(NodeEnterEventArgs.EventId, OnNodeEnter); GameEntry.Event.Subscribe(NodeMapNodeEnterRequestedEventArgs.EventId, OnNodeMapNodeEnterRequested); GameEntry.EventNode.OnInit(); GameEntry.CombatNode.OnInit(LevelThemeType.Plain); GameEntry.ShopNode.OnInit(); GameEntry.PlayerInventory?.OnInit(); _currentRunState = RunStateFactory.CreateFixedRun( LevelThemeType.Plain, GameEntry.PlayerInventory != null ? GameEntry.PlayerInventory.GetInventorySnapshot() : null); _repoFormUseCase = new RepoFormUseCase(); GameEntry.UIRouter.BindUIUseCase(UIFormType.RepoForm, _repoFormUseCase); _nodeMapFormUseCase = new NodeMapFormUseCase(); _nodeMapFormUseCase.SetRunState(_currentRunState); GameEntry.UIRouter.BindUIUseCase(UIFormType.NodeMapForm, _nodeMapFormUseCase); _isRunCompleteDialogShown = false; _isReturnToMenuPending = false; EnterHubFlow(); } protected override void OnUpdate(IFsm procedureOwner, float elapseSeconds, float realElapseSeconds) { base.OnUpdate(procedureOwner, elapseSeconds, realElapseSeconds); if (_isReturnToMenuPending) { _isReturnToMenuPending = false; procedureOwner.SetData("NextSceneId", (int)SceneType.Menu); ChangeState(procedureOwner); return; } GameEntry.CombatNode.OnUpdate(elapseSeconds, realElapseSeconds); } protected override void OnLeave(IFsm procedureOwner, bool isShutdown) { GameEntry.CombatNode.OnShutdown(); GameEntry.Event.Unsubscribe(NodeMapNodeEnterRequestedEventArgs.EventId, OnNodeMapNodeEnterRequested); 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); } #endregion private void OnNodeEnter(object sender, GameEventArgs e) { if (!(e is NodeEnterEventArgs args)) { return; } if (!string.IsNullOrWhiteSpace(args.RunId) && _currentRunState != null && !string.Equals(args.RunId, _currentRunState.RunId)) { Log.Warning( "ProcedureMain.OnNodeEnter() ignored. EventRunId={0}, CurrentRunId={1}.", args.RunId, _currentRunState.RunId); return; } RunNodeState currentNode = _currentRunState?.CurrentNode; if (!ProcedureMainNodeEventGuardService.MatchesCurrentNode( _currentRunState, args.NodeId, args.NodeType, args.SequenceIndex)) { Log.Warning( "ProcedureMain.OnNodeEnter() node mismatch. EventNodeId={0}, EventNodeType={1}, EventSequenceIndex={2}; CurrentNodeId={3}, CurrentNodeType={4}, CurrentSequenceIndex={5}.", args.NodeId, args.NodeType, args.SequenceIndex, currentNode?.NodeId ?? 0, currentNode?.NodeType ?? RunNodeType.None, currentNode?.SequenceIndex ?? -1); return; } Log.Info( "ProcedureMain.OnNodeEnter() accepted. RunId={0}, NodeId={1}, NodeType={2}, SequenceIndex={3}.", string.IsNullOrWhiteSpace(args.RunId) ? _currentRunState?.RunId : args.RunId, args.NodeId, args.NodeType, args.SequenceIndex); EnterNodeFlow(); } private void OnNodeComplete(object sender, GameEventArgs e) { if (!(e is NodeCompleteEventArgs args)) { return; } if (!string.IsNullOrWhiteSpace(args.RunId) && _currentRunState != null && !string.Equals(args.RunId, _currentRunState.RunId)) { Log.Warning( "ProcedureMain.OnNodeComplete() ignored. EventRunId={0}, CurrentRunId={1}.", args.RunId, _currentRunState.RunId); return; } RunNodeState currentNode = _currentRunState?.CurrentNode; if (!ProcedureMainNodeEventGuardService.MatchesCurrentNode( _currentRunState, args.NodeId, args.NodeType, args.SequenceIndex)) { Log.Warning( "ProcedureMain.OnNodeComplete() node mismatch. EventNodeId={0}, EventNodeType={1}, EventSequenceIndex={2}; CurrentNodeId={3}, CurrentNodeType={4}, CurrentSequenceIndex={5}.", args.NodeId, args.NodeType, args.SequenceIndex, currentNode?.NodeId ?? 0, currentNode?.NodeType ?? RunNodeType.None, currentNode?.SequenceIndex ?? -1); 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.CompletionStatus, snapshot)); } private void OnNodeMapNodeEnterRequested(object sender, GameEventArgs e) { if (!(sender is NodeMapForm) || !(e is NodeMapNodeEnterRequestedEventArgs args)) { return; } RunNodeState currentNode = _currentRunState?.CurrentNode; if (_currentRunState == null || _flowPhase != ProcedureMainFlowPhase.Hub || !_currentRunState.CanEnterCurrentNode || currentNode == null) { Log.Warning( "ProcedureMain.OnNodeMapNodeEnterRequested() ignored. FlowPhase={0}, HasEnterableNode={1}.", _flowPhase, _currentRunState != null && _currentRunState.CanEnterCurrentNode); return; } if (args.SequenceIndex != currentNode.SequenceIndex || args.NodeType != currentNode.NodeType) { Log.Warning( "ProcedureMain.OnNodeMapNodeEnterRequested() ignored. Requested={0}#{1}, CurrentNode={2}#{3}.", args.NodeType, args.SequenceIndex, currentNode.NodeType, currentNode.SequenceIndex); return; } switch (currentNode.NodeType) { case RunNodeType.Combat: case RunNodeType.BossCombat: ProcedureMainCombatEntryValidationResult validationResult = ProcedureMainCombatEntryValidationService.Validate( GameEntry.PlayerInventory != null ? GameEntry.PlayerInventory.GetInventorySnapshot() : null); if (!validationResult.CanEnterCombat) { LogCombatEntryBlocked(currentNode, validationResult); OpenBlockedCombatDialog(validationResult); return; } GameEntry.CombatNode.StartCombat( currentNode.LinkedLevelId, _currentRunState.RunId, currentNode.NodeId, currentNode.NodeType, currentNode.SequenceIndex); return; case RunNodeType.Event: GameEntry.EventNode.StartEvent( _currentRunState.RunId, currentNode.NodeId, currentNode.NodeType, currentNode.SequenceIndex); return; case RunNodeType.Shop: GameEntry.ShopNode.StartShop( _currentRunState.RunId, currentNode.NodeId, currentNode.NodeType, currentNode.SequenceIndex); return; default: Log.Warning("ProcedureMain.OnNodeMapNodeEnterRequested() encountered unsupported node type: {0}.", currentNode.NodeType); return; } } private void OpenBlockedCombatDialog(ProcedureMainCombatEntryValidationResult validationResult) { GameEntry.UIRouter.CloseUI(UIFormType.DialogForm); GameEntry.UIRouter.OpenUI( UIFormType.DialogForm, ProcedureMainCombatEntryValidationService.BuildBlockedCombatDialogRawData(validationResult)); } private void LogCombatEntryBlocked( RunNodeState currentNode, ProcedureMainCombatEntryValidationResult validationResult) { switch (validationResult?.BlockReason) { case ProcedureMainCombatEntryBlockReason.InventoryUnavailable: Log.Warning( "ProcedureMain blocked combat start. RunId={0}, NodeId={1}, NodeType={2}, SequenceIndex={3}, Reason={4}.", _currentRunState?.RunId, currentNode?.NodeId ?? 0, currentNode?.NodeType ?? RunNodeType.None, currentNode?.SequenceIndex ?? -1, ProcedureMainCombatEntryBlockReason.InventoryUnavailable); return; case ProcedureMainCombatEntryBlockReason.NoValidParticipantTower: Log.Warning( "ProcedureMain blocked combat start. RunId={0}, NodeId={1}, NodeType={2}, SequenceIndex={3}, Reason={4}, InvalidParticipantTowers={5}.", _currentRunState?.RunId, currentNode?.NodeId ?? 0, currentNode?.NodeType ?? RunNodeType.None, currentNode?.SequenceIndex ?? -1, ProcedureMainCombatEntryBlockReason.NoValidParticipantTower, ProcedureMainCombatEntryValidationService.BuildInvalidParticipantTowerLog( validationResult.ValidationSummary)); return; default: Log.Warning( "ProcedureMain blocked combat start. RunId={0}, NodeId={1}, NodeType={2}, SequenceIndex={3}, Reason=Unknown.", _currentRunState?.RunId, currentNode?.NodeId ?? 0, currentNode?.NodeType ?? RunNodeType.None, currentNode?.SequenceIndex ?? -1); return; } } private void HandleRunAdvanceResult(ProcedureMainRunAdvanceResult result) { switch (result) { case ProcedureMainRunAdvanceResult.NoChange: return; case ProcedureMainRunAdvanceResult.NodeException: Log.Info( "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, _currentRunState?.CurrentNode?.SequenceIndex ?? -1); EnterHubFlow(); return; case ProcedureMainRunAdvanceResult.AdvancedToNextNode: LogNextNode(); EnterHubFlow(); return; case ProcedureMainRunAdvanceResult.RunCompleted: EnterRunCompletedPendingFinish(); return; default: return; } } private void EnterHubFlow() { _flowPhase = ProcedureMainFlowPhase.Hub; _nodeMapFormUseCase?.SetRunState(_currentRunState); GameEntry.UIRouter.OpenUI(UIFormType.NodeMapForm); GameEntry.UIRouter.OpenUI(UIFormType.MainForm); } private void EnterNodeFlow() { _flowPhase = ProcedureMainFlowPhase.NodeActive; CloseHubUI(); } private void EnterRunCompletedPendingFinish() { _flowPhase = ProcedureMainFlowPhase.RunCompletedPendingFinish; _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() { GameEntry.UIRouter.CloseUI(UIFormType.NodeMapForm); GameEntry.UIRouter.CloseUI(UIFormType.MainForm); } private void LogNextNode() { RunNodeState nextNode = _currentRunState?.CurrentNode; if (nextNode == null) { return; } Log.Info( "ProcedureMain advanced run. RunId={0}, NextNodeType={1}, SequenceIndex={2}, LevelId={3}.", _currentRunState.RunId, nextNode.NodeType, 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; } } }