geometry-tower-defense/Assets/Tests/EditMode/ProcedureMainFlowTests.cs

532 lines
23 KiB
C#

using System.Collections.Generic;
using System.Reflection;
using CustomComponent;
using GeometryTD.CustomComponent;
using GeometryTD.CustomEvent;
using GeometryTD.Definition;
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<UIRouterComponent>();
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<PlayerInventoryComponent>();
SetStaticPlayerInventory(inventoryComponent);
inventoryComponent.ReplaceInventorySnapshot(inventory);
return inventoryComponent;
}
private NodeMapForm CreateNodeMapFormSender()
{
_nodeMapFormObject = new GameObject("ProcedureMainFlowTests-NodeMapForm");
return _nodeMapFormObject.AddComponent<NodeMapForm>();
}
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(
"<UIRouter>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(
"<PlayerInventory>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<T>(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<ProcedureMainFlowPhase>(procedure, "_flowPhase");
}
private static bool GetReturnToMenuPending(ProcedureMain procedure)
{
return GetPrivateField<bool>(procedure, "_isReturnToMenuPending");
}
private static NodeMapFormUseCase GetNodeMapUseCase(ProcedureMain procedure)
{
return GetPrivateField<NodeMapFormUseCase>(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<UIFormType, RecorderController> _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<UIFormType, IUIFormController> routeControllers =
(Dictionary<UIFormType, IUIFormController>)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;
}
}
}
}