From 2b86582ff7b73cedfa3fdf4ecec5121fca67ee03 Mon Sep 17 00:00:00 2001 From: SepComet <2428390463@qq.com> Date: Sat, 7 Mar 2026 21:52:39 +0800 Subject: [PATCH] =?UTF-8?q?=E5=9B=BA=E5=AE=9A=2010=20=E8=8A=82=E7=82=B9?= =?UTF-8?q?=E5=A4=A7=E5=85=B3=E7=9A=84=E7=94=9F=E6=88=90=E5=B1=82=EF=BC=8C?= =?UTF-8?q?=E4=BD=86=E8=BF=98=E6=B2=A1=E6=9C=89=E6=8E=A5=E5=85=A5=E4=B8=BB?= =?UTF-8?q?=E6=B5=81=E7=A8=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增了 FixedRunNodeSequenceBuilder.cs,现在 Plain 主题会固定生成 1战 2事 3战 4店 5战 6事 7战 8店 9事 10Boss。普通战斗节点的 LinkedLevelId 固定映射为 1 -> 2 -> 3 -> 1,第 10 个 Boss 节点固定绑定 Level 4。 - RunStateFactory.cs 增加了 CreateFixedRun(...),可以直接创建带 10 节点固定序列的 RunState。RunModel.cs 也补了几个只读查询:NodeCount、GetNodeBySequenceIndex(...)、IsBossNode(...),方便后面接真实流程。 --- .../Procedure/FixedRunNodeSequenceBuilder.cs | 52 ++++++++++++++ .../FixedRunNodeSequenceBuilder.cs.meta | 11 +++ Assets/GameMain/Scripts/Procedure/RunModel.cs | 25 +++++++ .../Scripts/Procedure/RunStateFactory.cs | 9 +++ Assets/GameMain/Tests/Editor/RunStateTests.cs | 70 +++++++++++++++++++ 5 files changed, 167 insertions(+) create mode 100644 Assets/GameMain/Scripts/Procedure/FixedRunNodeSequenceBuilder.cs create mode 100644 Assets/GameMain/Scripts/Procedure/FixedRunNodeSequenceBuilder.cs.meta diff --git a/Assets/GameMain/Scripts/Procedure/FixedRunNodeSequenceBuilder.cs b/Assets/GameMain/Scripts/Procedure/FixedRunNodeSequenceBuilder.cs new file mode 100644 index 0000000..8909b7d --- /dev/null +++ b/Assets/GameMain/Scripts/Procedure/FixedRunNodeSequenceBuilder.cs @@ -0,0 +1,52 @@ +using System.Collections.Generic; +using GeometryTD.Definition; + +namespace GeometryTD.Procedure +{ + public static class FixedRunNodeSequenceBuilder + { + private const int FixedNodeCount = 10; + private static readonly int[] PlainCombatLevelCycle = { 1, 2, 3, 1 }; + + public static IReadOnlyList Build(LevelThemeType themeType) + { + if (themeType != LevelThemeType.Plain) + { + return new List(); + } + + List nodeSeeds = new List(FixedNodeCount) + { + CreateNodeSeed(1, 0, RunNodeType.Combat, themeType, PlainCombatLevelCycle[0]), + CreateNodeSeed(2, 1, RunNodeType.Event, themeType, 0), + CreateNodeSeed(3, 2, RunNodeType.Combat, themeType, PlainCombatLevelCycle[1]), + CreateNodeSeed(4, 3, RunNodeType.Shop, themeType, 0), + CreateNodeSeed(5, 4, RunNodeType.Combat, themeType, PlainCombatLevelCycle[2]), + CreateNodeSeed(6, 5, RunNodeType.Event, themeType, 0), + CreateNodeSeed(7, 6, RunNodeType.Combat, themeType, PlainCombatLevelCycle[3]), + CreateNodeSeed(8, 7, RunNodeType.Shop, themeType, 0), + CreateNodeSeed(9, 8, RunNodeType.Event, themeType, 0), + CreateNodeSeed(10, 9, RunNodeType.BossCombat, themeType, 4) + }; + + return nodeSeeds; + } + + private static RunNodeSeed CreateNodeSeed( + int nodeId, + int sequenceIndex, + RunNodeType nodeType, + LevelThemeType themeType, + int linkedLevelId) + { + return new RunNodeSeed + { + NodeId = nodeId, + SequenceIndex = sequenceIndex, + NodeType = nodeType, + ThemeType = themeType, + LinkedLevelId = linkedLevelId + }; + } + } +} diff --git a/Assets/GameMain/Scripts/Procedure/FixedRunNodeSequenceBuilder.cs.meta b/Assets/GameMain/Scripts/Procedure/FixedRunNodeSequenceBuilder.cs.meta new file mode 100644 index 0000000..3c61b46 --- /dev/null +++ b/Assets/GameMain/Scripts/Procedure/FixedRunNodeSequenceBuilder.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 650d0ca3526d4b3db552d70292267456 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/GameMain/Scripts/Procedure/RunModel.cs b/Assets/GameMain/Scripts/Procedure/RunModel.cs index fc8569a..c4910e2 100644 --- a/Assets/GameMain/Scripts/Procedure/RunModel.cs +++ b/Assets/GameMain/Scripts/Procedure/RunModel.cs @@ -88,6 +88,8 @@ namespace GeometryTD.Procedure public IReadOnlyList Nodes => _nodes; + public int NodeCount => _nodes.Count; + public RunNodeState CurrentNode { get @@ -103,6 +105,29 @@ namespace GeometryTD.Procedure public bool CanEnterCurrentNode => !IsCompleted && CurrentNode != null && CurrentNode.Status == RunNodeStatus.Available; + public RunNodeState GetNodeBySequenceIndex(int sequenceIndex) + { + if (sequenceIndex < 0) + { + return null; + } + + foreach (RunNodeState nodeState in _nodes) + { + if (nodeState.SequenceIndex == sequenceIndex) + { + return nodeState; + } + } + + return null; + } + + public static bool IsBossNode(RunNodeState nodeState) + { + return nodeState != null && nodeState.NodeType == RunNodeType.BossCombat; + } + public int CompletedNodeCount { get diff --git a/Assets/GameMain/Scripts/Procedure/RunStateFactory.cs b/Assets/GameMain/Scripts/Procedure/RunStateFactory.cs index 06814f3..c6e048f 100644 --- a/Assets/GameMain/Scripts/Procedure/RunStateFactory.cs +++ b/Assets/GameMain/Scripts/Procedure/RunStateFactory.cs @@ -5,6 +5,15 @@ namespace GeometryTD.Procedure { public static class RunStateFactory { + public static RunState CreateFixedRun( + LevelThemeType themeType, + BackpackInventoryData initialInventorySnapshot, + string runId = null) + { + IReadOnlyList fixedNodeSeeds = FixedRunNodeSequenceBuilder.Build(themeType); + return Create(themeType, initialInventorySnapshot, fixedNodeSeeds, runId); + } + public static RunState Create( LevelThemeType themeType, BackpackInventoryData initialInventorySnapshot, diff --git a/Assets/GameMain/Tests/Editor/RunStateTests.cs b/Assets/GameMain/Tests/Editor/RunStateTests.cs index 8516438..f9ef14a 100644 --- a/Assets/GameMain/Tests/Editor/RunStateTests.cs +++ b/Assets/GameMain/Tests/Editor/RunStateTests.cs @@ -127,5 +127,75 @@ namespace GeometryTD.Tests.Editor Assert.That(eventArgs.Succeeded, Is.False); Assert.That(eventArgs.InventorySnapshotAfterNode, Is.Null); } + + [Test] + public void FixedRunNodeSequenceBuilder_Builds_Plain_Ten_Node_Sequence() + { + IReadOnlyList nodeSeeds = FixedRunNodeSequenceBuilder.Build(LevelThemeType.Plain); + + Assert.That(nodeSeeds, Is.Not.Null); + Assert.That(nodeSeeds.Count, Is.EqualTo(10)); + + Assert.That(nodeSeeds[0].NodeType, Is.EqualTo(RunNodeType.Combat)); + Assert.That(nodeSeeds[1].NodeType, Is.EqualTo(RunNodeType.Event)); + Assert.That(nodeSeeds[2].NodeType, Is.EqualTo(RunNodeType.Combat)); + Assert.That(nodeSeeds[3].NodeType, Is.EqualTo(RunNodeType.Shop)); + Assert.That(nodeSeeds[4].NodeType, Is.EqualTo(RunNodeType.Combat)); + Assert.That(nodeSeeds[5].NodeType, Is.EqualTo(RunNodeType.Event)); + Assert.That(nodeSeeds[6].NodeType, Is.EqualTo(RunNodeType.Combat)); + Assert.That(nodeSeeds[7].NodeType, Is.EqualTo(RunNodeType.Shop)); + Assert.That(nodeSeeds[8].NodeType, Is.EqualTo(RunNodeType.Event)); + Assert.That(nodeSeeds[9].NodeType, Is.EqualTo(RunNodeType.BossCombat)); + + Assert.That(nodeSeeds[0].LinkedLevelId, Is.EqualTo(1)); + Assert.That(nodeSeeds[2].LinkedLevelId, Is.EqualTo(2)); + Assert.That(nodeSeeds[4].LinkedLevelId, Is.EqualTo(3)); + Assert.That(nodeSeeds[6].LinkedLevelId, Is.EqualTo(1)); + Assert.That(nodeSeeds[9].LinkedLevelId, Is.EqualTo(4)); + Assert.That(nodeSeeds[1].LinkedLevelId, Is.EqualTo(0)); + Assert.That(nodeSeeds[3].LinkedLevelId, Is.EqualTo(0)); + } + + [Test] + public void FixedRunNodeSequenceBuilder_Returns_Empty_For_Unsupported_Theme() + { + IReadOnlyList nodeSeeds = FixedRunNodeSequenceBuilder.Build(LevelThemeType.Volcano); + + Assert.That(nodeSeeds, Is.Not.Null); + Assert.That(nodeSeeds.Count, Is.EqualTo(0)); + } + + [Test] + public void CreateFixedRun_Creates_Ten_Node_Run_And_Completes_After_Ten_Advances() + { + RunState runState = RunStateFactory.CreateFixedRun( + LevelThemeType.Plain, + new BackpackInventoryData { Gold = 100 }, + "fixed-run"); + + Assert.That(runState.RunId, Is.EqualTo("fixed-run")); + Assert.That(runState.NodeCount, Is.EqualTo(10)); + Assert.That(runState.CurrentNodeIndex, Is.EqualTo(0)); + Assert.That(runState.CurrentNode.NodeType, Is.EqualTo(RunNodeType.Combat)); + Assert.That(runState.Nodes[0].Status, Is.EqualTo(RunNodeStatus.Available)); + Assert.That(runState.Nodes[9].Status, Is.EqualTo(RunNodeStatus.Locked)); + Assert.That(runState.GetNodeBySequenceIndex(9), Is.Not.Null); + Assert.That(RunState.IsBossNode(runState.GetNodeBySequenceIndex(9)), Is.True); + + for (int i = 0; i < runState.NodeCount; i++) + { + bool advanced = RunStateAdvanceService.TryCompleteCurrentNode( + runState, + true, + new BackpackInventoryData { Gold = 100 + i }); + + Assert.That(advanced, Is.True); + } + + Assert.That(runState.IsCompleted, Is.True); + Assert.That(runState.CompletedNodeCount, Is.EqualTo(10)); + Assert.That(runState.CurrentNodeIndex, Is.EqualTo(10)); + Assert.That(runState.RunInventorySnapshot.Gold, Is.EqualTo(109)); + } } }