using GameFramework.Event; using GameFramework.Fsm; using GameFramework.Procedure; using System; using GeometryTD.CustomEvent; using GeometryTD.CustomUtility; using GeometryTD.Definition; using GeometryTD.Factory; using GeometryTD.UI; using UnityGameFramework.Runtime; namespace GeometryTD.Procedure { public class ProcedureMain : ProcedureBase { private const int MaxParticipantTowerCount = 4; 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.TagRegistry.OnInit(); GameEntry.EventNode.OnInit(); GameEntry.CombatNode.OnInit(LevelThemeType.Plain); GameEntry.ShopNode.OnInit(); string runId = Guid.NewGuid().ToString("N"); int runSeed = RunStateFactory.CreateRunSeed(); BackpackInventoryData initialInventory = InventorySeedUtility.CreateSampleInventory(runSeed); GameEntry.PlayerInventory?.OnInit(initialInventory); _currentRunState = RunStateFactory.CreateFixedRun( LevelThemeType.Plain, GameEntry.PlayerInventory != null ? GameEntry.PlayerInventory.GetInventorySnapshot() : initialInventory, runId, runSeed); _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(); } ProcedureMainParticipantTowerCleanupResult cleanupResult = ProcedureMainParticipantTowerCleanupService.RemoveBrokenParticipantTowers( snapshot, MaxParticipantTowerCount); if (cleanupResult.HasAnyRemovedTower && GameEntry.PlayerInventory != null) { GameEntry.PlayerInventory.ReplaceInventorySnapshot(snapshot); } ProcedureMainRunAdvanceResult advanceResult = ProcedureMainRunFlowService.TryAdvanceRun(_currentRunState, args.CompletionStatus, snapshot); HandleRunAdvanceResult(advanceResult); if (cleanupResult.HasAnyRemovedTower && advanceResult != ProcedureMainRunAdvanceResult.RunCompleted) { OpenRemovedParticipantTowerDialog(cleanupResult); } } 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, _currentRunState.RunSeed, 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, _currentRunState.RunSeed, 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 OpenRemovedParticipantTowerDialog(ProcedureMainParticipantTowerCleanupResult cleanupResult) { GameEntry.UIRouter.CloseUI(UIFormType.DialogForm); GameEntry.UIRouter.OpenUI( UIFormType.DialogForm, ProcedureMainParticipantTowerCleanupService.BuildRemovedTowerDialogRawData(cleanupResult)); } 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; } } }