using System.Collections.Generic; using System.Reflection; using CustomComponent; using GeometryTD.CustomComponent; using GeometryTD.CustomEvent; using GeometryTD.Definition; using GeometryTD.Factory; using GeometryTD.Procedure; using GeometryTD.UI; using NUnit.Framework; using UnityEngine; namespace GeometryTD.Tests.EditMode { public sealed class ProcedureMainFlowTests { private GameObject _uiRouterObject; private GameObject _inventoryObject; private GameObject _nodeMapFormObject; private UIRouterComponent _originalUIRouter; private PlayerInventoryComponent _originalPlayerInventory; [TearDown] public void TearDown() { SetStaticUIRouter(_originalUIRouter); SetStaticPlayerInventory(_originalPlayerInventory); if (_uiRouterObject != null) { Object.DestroyImmediate(_uiRouterObject); _uiRouterObject = null; } if (_inventoryObject != null) { Object.DestroyImmediate(_inventoryObject); _inventoryObject = null; } if (_nodeMapFormObject != null) { Object.DestroyImmediate(_nodeMapFormObject); _nodeMapFormObject = null; } } [Test] public void OnNodeEnter_Accepted_Event_Closes_Hub_And_Enters_NodeActive() { RecorderUIRouter recorderRouter = CreateBoundUIRouter(); ProcedureMain procedure = CreateProcedureMain(CreateTwoNodeRun(), ProcedureMainFlowPhase.Hub); InvokePrivate( procedure, "OnNodeEnter", null, NodeEnterEventArgs.Create("testrun", 101, RunNodeType.Combat, 0)); Assert.That(GetFlowPhase(procedure), Is.EqualTo(ProcedureMainFlowPhase.NodeActive)); Assert.That(recorderRouter.NodeMapController.CloseCount, Is.EqualTo(1)); Assert.That(recorderRouter.MainController.CloseCount, Is.EqualTo(1)); Assert.That(recorderRouter.DialogController.OpenCount, Is.EqualTo(0)); } [Test] public void OnNodeEnter_Ignores_Mismatched_Node_And_Leaves_Hub_Visible() { RecorderUIRouter recorderRouter = CreateBoundUIRouter(); ProcedureMain procedure = CreateProcedureMain(CreateTwoNodeRun(), ProcedureMainFlowPhase.Hub); InvokePrivate( procedure, "OnNodeEnter", null, NodeEnterEventArgs.Create("testrun", 999, RunNodeType.Combat, 0)); Assert.That(GetFlowPhase(procedure), Is.EqualTo(ProcedureMainFlowPhase.Hub)); Assert.That(recorderRouter.NodeMapController.CloseCount, Is.EqualTo(0)); Assert.That(recorderRouter.MainController.CloseCount, Is.EqualTo(0)); } [Test] public void OnNodeComplete_Normal_Completion_Returns_To_Hub_And_Advances_Run() { RecorderUIRouter recorderRouter = CreateBoundUIRouter(); RunState runState = CreateTwoNodeRun(); ProcedureMain procedure = CreateProcedureMain(runState, ProcedureMainFlowPhase.NodeActive); InvokePrivate( procedure, "OnNodeComplete", null, NodeCompleteEventArgs.Create( "testrun", 101, RunNodeType.Combat, 0, RunNodeCompletionStatus.Completed, true, new BackpackInventoryData { Gold = 77 })); Assert.That(GetFlowPhase(procedure), Is.EqualTo(ProcedureMainFlowPhase.Hub)); Assert.That(runState.CurrentNodeIndex, Is.EqualTo(1)); Assert.That(runState.Nodes[0].Status, Is.EqualTo(RunNodeStatus.Completed)); Assert.That(runState.CurrentNode.Status, Is.EqualTo(RunNodeStatus.Available)); Assert.That(runState.RunInventorySnapshot.Gold, Is.EqualTo(77)); Assert.That(recorderRouter.NodeMapController.OpenCount, Is.EqualTo(1)); Assert.That(recorderRouter.MainController.OpenCount, Is.EqualTo(1)); Assert.That(recorderRouter.DialogController.OpenCount, Is.EqualTo(0)); NodeMapFormUseCase nodeMapUseCase = GetNodeMapUseCase(procedure); NodeMapFormRawData rawData = nodeMapUseCase.CreateInitialModel(); Assert.That(rawData.ProgressText, Is.EqualTo("1 / 2")); Assert.That(rawData.CurrentNodeText, Is.EqualTo("当前节点: 事件")); } [Test] public void OnNodeComplete_Exception_Returns_To_Hub_And_Marks_Current_Node_Exception() { RecorderUIRouter recorderRouter = CreateBoundUIRouter(); RunState runState = CreateTwoNodeRun(); ProcedureMain procedure = CreateProcedureMain(runState, ProcedureMainFlowPhase.NodeActive); InvokePrivate( procedure, "OnNodeComplete", null, NodeCompleteEventArgs.Create( "testrun", 101, RunNodeType.Combat, 0, RunNodeCompletionStatus.Exception, false, new BackpackInventoryData { Gold = 12 })); Assert.That(GetFlowPhase(procedure), Is.EqualTo(ProcedureMainFlowPhase.Hub)); Assert.That(runState.CurrentNodeIndex, Is.EqualTo(0)); Assert.That(runState.CurrentNode.Status, Is.EqualTo(RunNodeStatus.Exception)); Assert.That(runState.RunInventorySnapshot.Gold, Is.EqualTo(12)); Assert.That(recorderRouter.NodeMapController.OpenCount, Is.EqualTo(1)); Assert.That(recorderRouter.MainController.OpenCount, Is.EqualTo(1)); Assert.That(recorderRouter.DialogController.OpenCount, Is.EqualTo(0)); } [Test] public void OnNodeComplete_With_Broken_Participant_Tower_Opens_Removal_Dialog_After_Returning_To_Hub() { RecorderUIRouter recorderRouter = CreateBoundUIRouter(); PlayerInventoryComponent inventoryComponent = CreateBoundPlayerInventory(CreateParticipantInventory(100f)); RunState runState = CreateTwoNodeRun(); ProcedureMain procedure = CreateProcedureMain(runState, ProcedureMainFlowPhase.NodeActive); BackpackInventoryData brokenSnapshot = inventoryComponent.GetInventorySnapshot(); brokenSnapshot.MuzzleComponents[0].Endurance = 0f; InvokePrivate( procedure, "OnNodeComplete", null, NodeCompleteEventArgs.Create( "testrun", 101, RunNodeType.Combat, 0, RunNodeCompletionStatus.Completed, true, brokenSnapshot)); BackpackInventoryData replacedInventory = inventoryComponent.GetInventorySnapshot(); DialogFormRawData dialogRawData = recorderRouter.DialogController.LastOpenedUserData as DialogFormRawData; Assert.That(GetFlowPhase(procedure), Is.EqualTo(ProcedureMainFlowPhase.Hub)); Assert.That(replacedInventory.ParticipantTowerInstanceIds.Count, Is.EqualTo(0)); Assert.That(recorderRouter.NodeMapController.OpenCount, Is.EqualTo(1)); Assert.That(recorderRouter.MainController.OpenCount, Is.EqualTo(1)); Assert.That(recorderRouter.DialogController.OpenCount, Is.EqualTo(1)); Assert.That(dialogRawData, Is.Not.Null); Assert.That(dialogRawData.Title, Is.EqualTo("出战塔已损坏")); } [Test] public void OnNodeComplete_Boss_Run_Completed_Shows_Run_Complete_Dialog_And_Does_Not_Override_It_With_Cleanup_Dialog() { RecorderUIRouter recorderRouter = CreateBoundUIRouter(); PlayerInventoryComponent inventoryComponent = CreateBoundPlayerInventory(CreateParticipantInventory(100f)); RunState runState = RunStateFactory.Create( LevelThemeType.Plain, inventoryComponent.GetInventorySnapshot(), new[] { new RunNodeSeed { NodeId = 901, NodeType = RunNodeType.BossCombat, LinkedLevelId = 4 } }, "testrun"); ProcedureMain procedure = CreateProcedureMain(runState, ProcedureMainFlowPhase.NodeActive); BackpackInventoryData brokenSnapshot = inventoryComponent.GetInventorySnapshot(); brokenSnapshot.MuzzleComponents[0].Endurance = 0f; InvokePrivate( procedure, "OnNodeComplete", null, NodeCompleteEventArgs.Create( "testrun", 901, RunNodeType.BossCombat, 0, RunNodeCompletionStatus.Completed, true, brokenSnapshot)); BackpackInventoryData replacedInventory = inventoryComponent.GetInventorySnapshot(); DialogFormRawData dialogRawData = recorderRouter.DialogController.LastOpenedUserData as DialogFormRawData; Assert.That(GetFlowPhase(procedure), Is.EqualTo(ProcedureMainFlowPhase.RunCompletedPendingFinish)); Assert.That(runState.IsCompleted, Is.True); Assert.That(replacedInventory.ParticipantTowerInstanceIds.Count, Is.EqualTo(0)); Assert.That(recorderRouter.NodeMapController.CloseCount, Is.EqualTo(1)); Assert.That(recorderRouter.MainController.CloseCount, Is.EqualTo(1)); Assert.That(recorderRouter.DialogController.OpenCount, Is.EqualTo(1)); Assert.That(dialogRawData, Is.Not.Null); Assert.That(dialogRawData.Title, Is.EqualTo("Run Complete")); Assert.That(dialogRawData.Message, Is.EqualTo("Boss node completed. This run is finished and will return to the main menu.")); Assert.That(dialogRawData.OnClickConfirm, Is.Not.Null); dialogRawData.OnClickConfirm(null); Assert.That(GetReturnToMenuPending(procedure), Is.True); } [Test] public void OnNodeMapNodeEnterRequested_Ignores_Request_When_FlowPhase_Is_Not_Hub() { RecorderUIRouter recorderRouter = CreateBoundUIRouter(); ProcedureMain procedure = CreateProcedureMain(CreateTwoNodeRun(), ProcedureMainFlowPhase.NodeActive); InvokePrivate( procedure, "OnNodeMapNodeEnterRequested", CreateNodeMapFormSender(), NodeMapNodeEnterRequestedEventArgs.Create("testrun", 101, RunNodeType.Combat, 0)); Assert.That(GetFlowPhase(procedure), Is.EqualTo(ProcedureMainFlowPhase.NodeActive)); Assert.That(recorderRouter.DialogController.OpenCount, Is.EqualTo(0)); Assert.That(recorderRouter.NodeMapController.CloseCount, Is.EqualTo(0)); Assert.That(recorderRouter.MainController.CloseCount, Is.EqualTo(0)); } [Test] public void OnNodeMapNodeEnterRequested_Ignores_Request_When_Target_Node_Does_Not_Match_Current_Node() { RecorderUIRouter recorderRouter = CreateBoundUIRouter(); ProcedureMain procedure = CreateProcedureMain(CreateTwoNodeRun(), ProcedureMainFlowPhase.Hub); InvokePrivate( procedure, "OnNodeMapNodeEnterRequested", CreateNodeMapFormSender(), NodeMapNodeEnterRequestedEventArgs.Create("testrun", 102, RunNodeType.Event, 1)); Assert.That(GetFlowPhase(procedure), Is.EqualTo(ProcedureMainFlowPhase.Hub)); Assert.That(recorderRouter.DialogController.OpenCount, Is.EqualTo(0)); Assert.That(recorderRouter.NodeMapController.CloseCount, Is.EqualTo(0)); Assert.That(recorderRouter.MainController.CloseCount, Is.EqualTo(0)); } [Test] public void OnNodeMapNodeEnterRequested_Blocked_Combat_With_Null_Inventory_Opens_Block_Dialog() { RecorderUIRouter recorderRouter = CreateBoundUIRouter(); ProcedureMain procedure = CreateProcedureMain(CreateTwoNodeRun(), ProcedureMainFlowPhase.Hub); InvokePrivate( procedure, "OnNodeMapNodeEnterRequested", CreateNodeMapFormSender(), NodeMapNodeEnterRequestedEventArgs.Create("testrun", 101, RunNodeType.Combat, 0)); DialogFormRawData dialogRawData = recorderRouter.DialogController.LastOpenedUserData as DialogFormRawData; Assert.That(GetFlowPhase(procedure), Is.EqualTo(ProcedureMainFlowPhase.Hub)); Assert.That(recorderRouter.DialogController.CloseCount, Is.EqualTo(1)); Assert.That(recorderRouter.DialogController.OpenCount, Is.EqualTo(1)); Assert.That(dialogRawData, Is.Not.Null); Assert.That(dialogRawData.Title, Is.EqualTo("无法进入战斗")); Assert.That(dialogRawData.Message, Is.EqualTo("当前无法读取库存快照,暂时不能进入战斗。请重新进入本轮流程后再试。")); } [Test] public void OnNodeMapNodeEnterRequested_Blocked_Combat_With_No_Valid_Participant_Tower_Opens_Block_Dialog() { RecorderUIRouter recorderRouter = CreateBoundUIRouter(); PlayerInventoryComponent inventoryComponent = CreateBoundPlayerInventory(CreateParticipantInventory(100f)); BackpackInventoryData invalidInventory = inventoryComponent.GetInventorySnapshot(); invalidInventory.ParticipantTowerInstanceIds.Clear(); inventoryComponent.ReplaceInventorySnapshot(invalidInventory); ProcedureMain procedure = CreateProcedureMain(CreateTwoNodeRun(), ProcedureMainFlowPhase.Hub); InvokePrivate( procedure, "OnNodeMapNodeEnterRequested", CreateNodeMapFormSender(), NodeMapNodeEnterRequestedEventArgs.Create("testrun", 101, RunNodeType.Combat, 0)); DialogFormRawData dialogRawData = recorderRouter.DialogController.LastOpenedUserData as DialogFormRawData; Assert.That(GetFlowPhase(procedure), Is.EqualTo(ProcedureMainFlowPhase.Hub)); Assert.That(recorderRouter.DialogController.CloseCount, Is.EqualTo(1)); Assert.That(recorderRouter.DialogController.OpenCount, Is.EqualTo(1)); Assert.That(dialogRawData, Is.Not.Null); Assert.That(dialogRawData.Title, Is.EqualTo("无法进入战斗")); Assert.That( dialogRawData.Message, Is.EqualTo("参战区至少需要 1 座完整装配了枪口、轴承、底座的塔,才能进入战斗。")); } private RecorderUIRouter CreateBoundUIRouter() { _originalUIRouter = GameEntry.UIRouter; _uiRouterObject = new GameObject("ProcedureMainFlowTests-UIRouter"); UIRouterComponent uiRouter = _uiRouterObject.AddComponent(); SetStaticUIRouter(uiRouter); RecorderUIRouter recorderRouter = new RecorderUIRouter(uiRouter); recorderRouter.Bind(UIFormType.NodeMapForm); recorderRouter.Bind(UIFormType.MainForm); recorderRouter.Bind(UIFormType.DialogForm); recorderRouter.Bind(UIFormType.RepoForm); return recorderRouter; } private PlayerInventoryComponent CreateBoundPlayerInventory(BackpackInventoryData inventory) { _originalPlayerInventory = GameEntry.PlayerInventory; _inventoryObject = new GameObject("ProcedureMainFlowTests-Inventory"); PlayerInventoryComponent inventoryComponent = _inventoryObject.AddComponent(); SetStaticPlayerInventory(inventoryComponent); inventoryComponent.ReplaceInventorySnapshot(inventory); return inventoryComponent; } private NodeMapForm CreateNodeMapFormSender() { _nodeMapFormObject = new GameObject("ProcedureMainFlowTests-NodeMapForm"); return _nodeMapFormObject.AddComponent(); } private static ProcedureMain CreateProcedureMain(RunState runState, ProcedureMainFlowPhase flowPhase) { ProcedureMain procedure = new ProcedureMain(); NodeMapFormUseCase nodeMapUseCase = new NodeMapFormUseCase(); nodeMapUseCase.SetRunState(runState); SetPrivateField(procedure, "_currentRunState", runState); SetPrivateField(procedure, "_nodeMapFormUseCase", nodeMapUseCase); SetPrivateField(procedure, "_flowPhase", flowPhase); SetPrivateField(procedure, "_isRunCompleteDialogShown", false); SetPrivateField(procedure, "_isReturnToMenuPending", false); return procedure; } private static void SetStaticUIRouter(UIRouterComponent uiRouter) { FieldInfo backingField = typeof(GameEntry).GetField( "k__BackingField", BindingFlags.Static | BindingFlags.NonPublic); Assert.That(backingField, Is.Not.Null); backingField.SetValue(null, uiRouter); } private static void SetStaticPlayerInventory(PlayerInventoryComponent inventoryComponent) { FieldInfo backingField = typeof(GameEntry).GetField( "k__BackingField", BindingFlags.Static | BindingFlags.NonPublic); Assert.That(backingField, Is.Not.Null); backingField.SetValue(null, inventoryComponent); } private static void InvokePrivate(object instance, string methodName, params object[] args) { MethodInfo method = instance.GetType().GetMethod( methodName, BindingFlags.Instance | BindingFlags.NonPublic); Assert.That(method, Is.Not.Null); method.Invoke(instance, args); } private static void SetPrivateField(object instance, string fieldName, object value) { FieldInfo field = instance.GetType().GetField( fieldName, BindingFlags.Instance | BindingFlags.NonPublic); Assert.That(field, Is.Not.Null); field.SetValue(instance, value); } private static T GetPrivateField(object instance, string fieldName) { FieldInfo field = instance.GetType().GetField( fieldName, BindingFlags.Instance | BindingFlags.NonPublic); Assert.That(field, Is.Not.Null); return (T)field.GetValue(instance); } private static ProcedureMainFlowPhase GetFlowPhase(ProcedureMain procedure) { return GetPrivateField(procedure, "_flowPhase"); } private static bool GetReturnToMenuPending(ProcedureMain procedure) { return GetPrivateField(procedure, "_isReturnToMenuPending"); } private static NodeMapFormUseCase GetNodeMapUseCase(ProcedureMain procedure) { return GetPrivateField(procedure, "_nodeMapFormUseCase"); } private static RunState CreateTwoNodeRun() { return RunStateFactory.Create( LevelThemeType.Plain, new BackpackInventoryData { Gold = 50 }, new[] { new RunNodeSeed { NodeId = 101, NodeType = RunNodeType.Combat, LinkedLevelId = 1, SequenceIndex = 0 }, new RunNodeSeed { NodeId = 102, NodeType = RunNodeType.Event, SequenceIndex = 1 } }, "testrun"); } private static BackpackInventoryData CreateParticipantInventory(float endurance) { BackpackInventoryData inventory = new BackpackInventoryData(); inventory.MuzzleComponents.Add(new MuzzleCompItemData { InstanceId = 10001, Name = "枪口", Endurance = endurance }); inventory.BearingComponents.Add(new BearingCompItemData { InstanceId = 20001, Name = "轴承", Endurance = endurance }); inventory.BaseComponents.Add(new BaseCompItemData { InstanceId = 30001, Name = "底座", Endurance = endurance }); inventory.Towers.Add(new TowerItemData { InstanceId = 90001, Name = "合法塔", MuzzleComponentInstanceId = 10001, BearingComponentInstanceId = 20001, BaseComponentInstanceId = 30001 }); inventory.ParticipantTowerInstanceIds.Add(90001); return inventory; } private sealed class RecorderUIRouter { private readonly Dictionary _controllers = new(); public RecorderUIRouter(UIRouterComponent uiRouter) { UIRouter = uiRouter; } public UIRouterComponent UIRouter { get; } public RecorderController NodeMapController => _controllers[UIFormType.NodeMapForm]; public RecorderController MainController => _controllers[UIFormType.MainForm]; public RecorderController DialogController => _controllers[UIFormType.DialogForm]; public void Bind(UIFormType formType) { RecorderController controller = new RecorderController(); _controllers[formType] = controller; FieldInfo routeControllersField = typeof(UIRouterComponent).GetField( "_routeControllers", BindingFlags.Instance | BindingFlags.NonPublic); Assert.That(routeControllersField, Is.Not.Null); Dictionary routeControllers = (Dictionary)routeControllersField.GetValue(UIRouter); Assert.That(routeControllers, Is.Not.Null); routeControllers[formType] = controller; } } private sealed class RecorderController : IUIFormController { public int OpenCount { get; private set; } public int CloseCount { get; private set; } public object LastOpenedUserData { get; private set; } public IUIUseCase BoundUseCase { get; private set; } public int? OpenUI(object userData = null) { OpenCount++; LastOpenedUserData = userData; return OpenCount; } public void CloseUI() { CloseCount++; } public void BindUseCase(IUIUseCase useCase) { BoundUseCase = useCase; } } } }