using GeometryTD.Definition; using GeometryTD.Factory; using GeometryTD.Procedure; using GeometryTD.UI; using NUnit.Framework; namespace GeometryTD.Tests.EditMode { public sealed class ProcedureMainServicesTests { [Test] public void TryAdvanceRun_Returns_AdvancedToNextNode_For_Normal_Completion() { RunState runState = CreateTwoNodeRun(); ProcedureMainRunAdvanceResult result = ProcedureMainRunFlowService.TryAdvanceRun( runState, RunNodeCompletionStatus.Completed, CreateCompletionSnapshot(new BackpackInventoryData { Gold = 88 }, continueChallengeLayer: 4)); Assert.That(result, Is.EqualTo(ProcedureMainRunAdvanceResult.AdvancedToNextNode)); Assert.That(runState.IsCompleted, Is.False); Assert.That(runState.Nodes[0].Status, Is.EqualTo(RunNodeStatus.Completed)); Assert.That(runState.CurrentNodeIndex, Is.EqualTo(1)); Assert.That(runState.CurrentNode.Status, Is.EqualTo(RunNodeStatus.Available)); Assert.That(runState.RunInventorySnapshot.Gold, Is.EqualTo(88)); Assert.That(runState.CurrentNodeContinueChallengeLayer, Is.EqualTo(0)); } [Test] public void TryAdvanceRun_Returns_NodeException_For_Exception_Fallback() { RunState runState = CreateTwoNodeRun(); ProcedureMainRunAdvanceResult result = ProcedureMainRunFlowService.TryAdvanceRun( runState, RunNodeCompletionStatus.Exception, CreateCompletionSnapshot(new BackpackInventoryData { Gold = 5 }, continueChallengeLayer: 2)); Assert.That(result, Is.EqualTo(ProcedureMainRunAdvanceResult.NodeException)); Assert.That(runState.IsCompleted, Is.False); Assert.That(runState.CurrentNodeIndex, Is.EqualTo(0)); Assert.That(runState.CurrentNode.Status, Is.EqualTo(RunNodeStatus.Exception)); Assert.That(runState.Nodes[1].Status, Is.EqualTo(RunNodeStatus.Locked)); Assert.That(runState.RunInventorySnapshot.Gold, Is.EqualTo(5)); Assert.That(runState.CurrentNodeContinueChallengeLayer, Is.EqualTo(2)); } [Test] public void TryAdvanceRun_Returns_RunCompleted_When_Last_Node_Finishes() { RunState runState = RunStateFactory.Create( LevelThemeType.Plain, new BackpackInventoryData { Gold = 30 }, new[] { new RunNodeSeed { NodeId = 901, NodeType = RunNodeType.BossCombat, LinkedLevelId = 4 } }); ProcedureMainRunAdvanceResult result = ProcedureMainRunFlowService.TryAdvanceRun( runState, RunNodeCompletionStatus.Completed, CreateCompletionSnapshot(new BackpackInventoryData { Gold = 123 })); Assert.That(result, Is.EqualTo(ProcedureMainRunAdvanceResult.RunCompleted)); Assert.That(runState.IsCompleted, Is.True); Assert.That(runState.CurrentNodeIndex, Is.EqualTo(1)); Assert.That(runState.CompletedNodeCount, Is.EqualTo(1)); Assert.That(runState.RunInventorySnapshot.Gold, Is.EqualTo(123)); } [Test] public void TryAdvanceRun_Returns_NoChange_When_Run_Cannot_Advance() { ProcedureMainRunAdvanceResult nullRunResult = ProcedureMainRunFlowService.TryAdvanceRun( null, RunNodeCompletionStatus.Completed, CreateCompletionSnapshot(new BackpackInventoryData { Gold = 1 })); Assert.That(nullRunResult, Is.EqualTo(ProcedureMainRunAdvanceResult.NoChange)); RunState completedRun = RunStateFactory.Create( LevelThemeType.Plain, new BackpackInventoryData { Gold = 10 }, new RunNodeSeed[0]); ProcedureMainRunAdvanceResult completedRunResult = ProcedureMainRunFlowService.TryAdvanceRun( completedRun, RunNodeCompletionStatus.Completed, CreateCompletionSnapshot(new BackpackInventoryData { Gold = 20 })); Assert.That(completedRunResult, Is.EqualTo(ProcedureMainRunAdvanceResult.NoChange)); Assert.That(completedRun.RunInventorySnapshot.Gold, Is.EqualTo(10)); } [Test] public void TryAdvanceRun_Returns_NoChange_But_Still_Replaces_Snapshot_For_NonTerminal_Status() { RunState runState = CreateTwoNodeRun(); ProcedureMainRunAdvanceResult result = ProcedureMainRunFlowService.TryAdvanceRun( runState, RunNodeCompletionStatus.None, CreateCompletionSnapshot(new BackpackInventoryData { Gold = 999 }, themeStageIndex: 4)); Assert.That(result, Is.EqualTo(ProcedureMainRunAdvanceResult.NoChange)); Assert.That(runState.CurrentNodeIndex, Is.EqualTo(0)); Assert.That(runState.CurrentNode.Status, Is.EqualTo(RunNodeStatus.Available)); Assert.That(runState.RunInventorySnapshot.Gold, Is.EqualTo(999)); Assert.That(runState.ThemeStageIndex, Is.EqualTo(4)); } [Test] public void MatchesCurrentNode_Returns_True_For_Current_Node_And_Default_Wildcards() { RunState runState = CreateTwoNodeRun(); bool exactMatch = ProcedureMainNodeEventGuardService.MatchesCurrentNode( runState, 101, RunNodeType.Combat, 0); bool wildcardMatch = ProcedureMainNodeEventGuardService.MatchesCurrentNode( runState, 0, RunNodeType.None, -1); Assert.That(exactMatch, Is.True); Assert.That(wildcardMatch, Is.True); } [Test] public void MatchesCurrentNode_Returns_False_For_Mismatched_Node_Event() { RunState runState = CreateTwoNodeRun(); bool nodeIdMismatch = ProcedureMainNodeEventGuardService.MatchesCurrentNode( runState, 999, RunNodeType.Combat, 0); bool nodeTypeMismatch = ProcedureMainNodeEventGuardService.MatchesCurrentNode( runState, 101, RunNodeType.Shop, 0); bool sequenceMismatch = ProcedureMainNodeEventGuardService.MatchesCurrentNode( runState, 101, RunNodeType.Combat, 1); Assert.That(nodeIdMismatch, Is.False); Assert.That(nodeTypeMismatch, Is.False); Assert.That(sequenceMismatch, Is.False); } [Test] public void MatchesCurrentNode_Returns_False_When_Run_Has_No_Current_Node() { RunState completedRun = RunStateFactory.Create( LevelThemeType.Plain, new BackpackInventoryData { Gold = 10 }, new RunNodeSeed[0]); bool match = ProcedureMainNodeEventGuardService.MatchesCurrentNode( completedRun, 1, RunNodeType.Combat, 0); Assert.That(match, Is.False); } [Test] public void TryEnterCompletedPendingFinish_Shows_Dialog_Only_Once() { ProcedureMainRunCompletionResult firstResult = ProcedureMainRunCompletionService.TryEnterCompletedPendingFinish(false); ProcedureMainRunCompletionResult secondResult = ProcedureMainRunCompletionService.TryEnterCompletedPendingFinish(true); Assert.That(firstResult, Is.EqualTo(ProcedureMainRunCompletionResult.ShowCompletionDialog)); Assert.That(secondResult, Is.EqualTo(ProcedureMainRunCompletionResult.NoChange)); } [Test] public void TryConfirmReturnToMenu_Returns_Menu_Only_In_Completed_Pending_Finish() { ProcedureMainRunCompletionResult validResult = ProcedureMainRunCompletionService.TryConfirmReturnToMenu( ProcedureMainFlowPhase.RunCompletedPendingFinish, false); ProcedureMainRunCompletionResult hubResult = ProcedureMainRunCompletionService.TryConfirmReturnToMenu( ProcedureMainFlowPhase.Hub, false); ProcedureMainRunCompletionResult pendingResult = ProcedureMainRunCompletionService.TryConfirmReturnToMenu( ProcedureMainFlowPhase.RunCompletedPendingFinish, true); Assert.That(validResult, Is.EqualTo(ProcedureMainRunCompletionResult.ReturnToMenu)); Assert.That(hubResult, Is.EqualTo(ProcedureMainRunCompletionResult.NoChange)); Assert.That(pendingResult, Is.EqualTo(ProcedureMainRunCompletionResult.NoChange)); } [Test] public void ValidateCombatEntry_Returns_InventoryUnavailable_When_Inventory_Is_Null() { ProcedureMainCombatEntryValidationResult result = ProcedureMainCombatEntryValidationService.Validate(null); Assert.That(result.CanEnterCombat, Is.False); Assert.That(result.BlockReason, Is.EqualTo(ProcedureMainCombatEntryBlockReason.InventoryUnavailable)); Assert.That(result.ValidationSummary, Is.Not.Null); Assert.That(result.ValidationSummary.HasAnyValidParticipantTower, Is.False); } [Test] public void ValidateCombatEntry_Returns_NoValidParticipantTower_When_ParticipantArea_Is_Empty() { BackpackInventoryData inventory = CreateCombatInventory(); inventory.ParticipantTowerInstanceIds.Clear(); ProcedureMainCombatEntryValidationResult result = ProcedureMainCombatEntryValidationService.Validate(inventory); Assert.That(result.CanEnterCombat, Is.False); Assert.That(result.BlockReason, Is.EqualTo(ProcedureMainCombatEntryBlockReason.NoValidParticipantTower)); Assert.That(result.ValidationSummary.HasAnyValidParticipantTower, Is.False); Assert.That(result.ValidationSummary.ValidTowers.Count, Is.EqualTo(0)); Assert.That(result.ValidationSummary.InvalidResults.Count, Is.EqualTo(0)); } [Test] public void ValidateCombatEntry_Returns_NoValidParticipantTower_When_AllParticipantTowers_Are_Invalid() { BackpackInventoryData inventory = CreateCombatInventory(); inventory.Towers[0].BaseComponentInstanceId = 99903; ProcedureMainCombatEntryValidationResult result = ProcedureMainCombatEntryValidationService.Validate(inventory); Assert.That(result.CanEnterCombat, Is.False); Assert.That(result.BlockReason, Is.EqualTo(ProcedureMainCombatEntryBlockReason.NoValidParticipantTower)); Assert.That(result.ValidationSummary.ValidTowers.Count, Is.EqualTo(0)); Assert.That(result.ValidationSummary.InvalidResults.Count, Is.EqualTo(1)); Assert.That( result.ValidationSummary.InvalidResults[0].FailureReason, Is.EqualTo(CombatParticipantTowerValidationFailureReason.MissingBaseComponent)); } [Test] public void ValidateCombatEntry_Returns_NoValidParticipantTower_When_AllParticipantTowers_Are_Broken() { BackpackInventoryData inventory = CreateCombatInventory(); inventory.MuzzleComponents[0].Endurance = 0f; ProcedureMainCombatEntryValidationResult result = ProcedureMainCombatEntryValidationService.Validate(inventory); Assert.That(result.CanEnterCombat, Is.False); Assert.That(result.BlockReason, Is.EqualTo(ProcedureMainCombatEntryBlockReason.NoValidParticipantTower)); Assert.That(result.ValidationSummary.ValidTowers.Count, Is.EqualTo(0)); Assert.That(result.ValidationSummary.InvalidResults.Count, Is.EqualTo(1)); Assert.That( result.ValidationSummary.InvalidResults[0].FailureReason, Is.EqualTo(CombatParticipantTowerValidationFailureReason.BrokenMuzzleComponent)); } [Test] public void ValidateCombatEntry_Returns_CanEnter_When_At_Least_One_ParticipantTower_Is_Valid() { BackpackInventoryData inventory = CreateCombatInventory(); inventory.Towers.Add(new TowerItemData { InstanceId = 90002, Name = "非法塔", MuzzleComponentInstanceId = 10001, BearingComponentInstanceId = 20001, BaseComponentInstanceId = 99903 }); inventory.ParticipantTowerInstanceIds.Add(90002); ProcedureMainCombatEntryValidationResult result = ProcedureMainCombatEntryValidationService.Validate(inventory); Assert.That(result.CanEnterCombat, Is.True); Assert.That(result.BlockReason, Is.EqualTo(ProcedureMainCombatEntryBlockReason.None)); Assert.That(result.ValidationSummary.ValidTowers.Count, Is.EqualTo(1)); Assert.That(result.ValidationSummary.ValidTowers[0].InstanceId, Is.EqualTo(90001)); Assert.That(result.ValidationSummary.InvalidResults.Count, Is.EqualTo(1)); } [Test] public void BuildInvalidParticipantTowerLog_Returns_None_When_InvalidResults_Are_Empty() { string log = ProcedureMainCombatEntryValidationService.BuildInvalidParticipantTowerLog( new CombatParticipantTowerValidationSummary()); Assert.That(log, Is.EqualTo("none")); } [Test] public void BuildInvalidParticipantTowerLog_Formats_TowerIds_And_Reasons() { CombatParticipantTowerValidationSummary summary = new CombatParticipantTowerValidationSummary { InvalidResults = new[] { new CombatParticipantTowerValidationResult { TowerInstanceId = 90002, FailureReason = CombatParticipantTowerValidationFailureReason.MissingBaseComponent }, new CombatParticipantTowerValidationResult { TowerInstanceId = 90003, FailureReason = CombatParticipantTowerValidationFailureReason.MissingBearingComponent } } }; string log = ProcedureMainCombatEntryValidationService.BuildInvalidParticipantTowerLog(summary); Assert.That(log, Is.EqualTo("#90002:MissingBaseComponent, #90003:MissingBearingComponent")); } [Test] public void BuildBlockedCombatDialogRawData_Returns_InventoryUnavailable_Message() { DialogFormRawData rawData = ProcedureMainCombatEntryValidationService.BuildBlockedCombatDialogRawData( new ProcedureMainCombatEntryValidationResult { BlockReason = ProcedureMainCombatEntryBlockReason.InventoryUnavailable, ValidationSummary = new CombatParticipantTowerValidationSummary() }); Assert.That(rawData.Title, Is.EqualTo("无法进入战斗")); Assert.That(rawData.Message, Is.EqualTo("当前无法读取库存快照,暂时不能进入战斗。请重新进入本轮流程后再试。")); Assert.That(rawData.ConfirmText, Is.EqualTo("知道了")); Assert.That(rawData.Mode, Is.EqualTo(1)); } [Test] public void BuildBlockedCombatDialogRawData_Returns_Generic_Message_When_NoParticipantTower_Is_Assigned() { DialogFormRawData rawData = ProcedureMainCombatEntryValidationService.BuildBlockedCombatDialogRawData( new ProcedureMainCombatEntryValidationResult { BlockReason = ProcedureMainCombatEntryBlockReason.NoValidParticipantTower, ValidationSummary = new CombatParticipantTowerValidationSummary() }); Assert.That( rawData.Message, Is.EqualTo("参战区至少需要 1 座完整装配了枪口、轴承、底座的塔,才能进入战斗。")); } [Test] public void BuildBlockedCombatDialogRawData_Lists_Invalid_Tower_Reasons() { DialogFormRawData rawData = ProcedureMainCombatEntryValidationService.BuildBlockedCombatDialogRawData( new ProcedureMainCombatEntryValidationResult { BlockReason = ProcedureMainCombatEntryBlockReason.NoValidParticipantTower, ValidationSummary = new CombatParticipantTowerValidationSummary { InvalidResults = new[] { new CombatParticipantTowerValidationResult { TowerInstanceId = 90002, FailureReason = CombatParticipantTowerValidationFailureReason.MissingBaseComponent }, new CombatParticipantTowerValidationResult { TowerInstanceId = 90003, FailureReason = CombatParticipantTowerValidationFailureReason.MissingMuzzleComponent } } } }); Assert.That( rawData.Message, Is.EqualTo("参战区没有可出战的完整塔。\n塔 #90002 缺少底座组件。\n塔 #90003 缺少枪口组件。")); } [Test] public void BuildBlockedCombatDialogRawData_Lists_Broken_Component_Reasons() { DialogFormRawData rawData = ProcedureMainCombatEntryValidationService.BuildBlockedCombatDialogRawData( new ProcedureMainCombatEntryValidationResult { BlockReason = ProcedureMainCombatEntryBlockReason.NoValidParticipantTower, ValidationSummary = new CombatParticipantTowerValidationSummary { InvalidResults = new[] { new CombatParticipantTowerValidationResult { TowerInstanceId = 90002, FailureReason = CombatParticipantTowerValidationFailureReason.BrokenBaseComponent }, new CombatParticipantTowerValidationResult { TowerInstanceId = 90003, FailureReason = CombatParticipantTowerValidationFailureReason.BrokenMuzzleComponent } } } }); Assert.That( rawData.Message, Is.EqualTo("参战区没有可出战的完整塔。\n塔 #90002 底座组件耐久为 0,无法参战。\n塔 #90003 枪口组件耐久为 0,无法参战。")); } private static RunState CreateTwoNodeRun() { return RunStateFactory.Create( LevelThemeType.Plain, new BackpackInventoryData { Gold = 50 }, new[] { new RunNodeSeed { NodeId = 101, NodeType = RunNodeType.Combat, LinkedLevelId = 1 }, new RunNodeSeed { NodeId = 102, NodeType = RunNodeType.Event } }); } private static BackpackInventoryData CreateCombatInventory() { BackpackInventoryData inventory = new BackpackInventoryData(); inventory.MuzzleComponents.Add(new MuzzleCompItemData { InstanceId = 10001, Name = "枪口", Endurance = 100f }); inventory.BearingComponents.Add(new BearingCompItemData { InstanceId = 20001, Name = "轴承", Endurance = 100f }); inventory.BaseComponents.Add(new BaseCompItemData { InstanceId = 30001, Name = "底座", Endurance = 100f }); inventory.Towers.Add(new TowerItemData { InstanceId = 90001, Name = "合法塔", MuzzleComponentInstanceId = 10001, BearingComponentInstanceId = 20001, BaseComponentInstanceId = 30001 }); inventory.ParticipantTowerInstanceIds.Add(90001); return inventory; } private static RunNodeCompletionSnapshot CreateCompletionSnapshot( BackpackInventoryData inventorySnapshot, LevelThemeType themeType = LevelThemeType.Plain, int themeStageIndex = 0, int continueChallengeLayer = 0) { RunNodeCompletionSnapshot snapshot = new RunNodeCompletionSnapshot { InventorySnapshot = inventorySnapshot, ThemeType = themeType, ThemeStageIndex = themeStageIndex, CurrentThemePool = new System.Collections.Generic.List(), ThemeHistory = new System.Collections.Generic.List(), CurrentNodeContinueChallengeLayer = continueChallengeLayer, RunItems = new System.Collections.Generic.List() }; if (themeType != LevelThemeType.None) { snapshot.CurrentThemePool.Add(themeType); snapshot.ThemeHistory.Add(themeType); } return snapshot; } } }