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 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 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 (currentNode != null && ((args.NodeId > 0 && args.NodeId != currentNode.NodeId) || (args.NodeType != RunNodeType.None && args.NodeType != currentNode.NodeType) || (args.SequenceIndex >= 0 && args.SequenceIndex != currentNode.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, currentNode.NodeType, currentNode.SequenceIndex); } 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; } 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: if (!HasAvailableParticipantTower()) { Log.Warning("ProcedureMain blocked combat start. No participant tower is available."); 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 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 static bool HasAvailableParticipantTower() { return GameEntry.PlayerInventory != null && GameEntry.PlayerInventory.GetParticipantTowerSnapshot().Count > 0; } 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; } } }