实现单局 Run 模型的基础落地
- 核心改动在 RunModel.cs、RunStateFactory.cs 和 RunStateAdvanceService.cs。现在项目里有了 RunNodeType / RunNodeStatus / RunNodeSeed / RunNodeState / RunState,并支持用预置节点序列创建 Run,以及在节点完成后推进、更新局内库存快照、标记通关或失败。 - 库存边界也补上了。PlayerInventoryComponent.cs 新增了 ReplaceInventorySnapshot(...),底层通过 PlayerInventoryStateStore.cs 重建工作副本,避免后续把 Run 真值硬塞回 PlayerInventory 内部状态。 - 节点事件载体已经扩展为可承载 Run 上下文,同时保留原来的无参 Create() 以兼容现有调用点。相关改动在 NodeEnterEventArgs.cs 和 NodeCompleteEventArgs.cs。 - 我还补了纯模型编辑器测试,覆盖 Run 创建、节点推进、失败标记和事件快照克隆,文件在 RunStateTests.cs。
This commit is contained in:
@@ -4,4 +4,4 @@
|
||||
# 场景编号 备注 资源名称 背景音乐编号
|
||||
1 菜单场景 Menu 1
|
||||
2 战斗场景 Main 2
|
||||
3 压力测试场景 StressTest 0
|
||||
3 压力测试场景 StressTest 0
|
||||
|
||||
@@ -45,6 +45,12 @@ namespace GeometryTD.CustomComponent
|
||||
return _queryModel.GetSnapshot();
|
||||
}
|
||||
|
||||
public void ReplaceInventorySnapshot(BackpackInventoryData inventorySnapshot)
|
||||
{
|
||||
EnsureServices();
|
||||
_commandModel.ReplaceInventorySnapshot(inventorySnapshot, MaxParticipantTowerCount);
|
||||
}
|
||||
|
||||
public IReadOnlyList<TowerItemData> GetParticipantTowerSnapshot()
|
||||
{
|
||||
EnsureInitialized();
|
||||
|
||||
@@ -89,6 +89,11 @@ namespace GeometryTD.CustomComponent
|
||||
_state.IsInitialized = true;
|
||||
}
|
||||
|
||||
public void ReplaceInventorySnapshot(BackpackInventoryData sourceInventory, int maxParticipantTowerCount)
|
||||
{
|
||||
Initialize(sourceInventory, maxParticipantTowerCount);
|
||||
}
|
||||
|
||||
public PlayerInventoryMergeSummary MergeInventory(BackpackInventoryData gainedInventory)
|
||||
{
|
||||
PlayerInventoryMergeSummary summary = default;
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
using GameFramework;
|
||||
using GameFramework.Event;
|
||||
using GeometryTD.CustomUtility;
|
||||
using GeometryTD.Definition;
|
||||
using GeometryTD.Procedure;
|
||||
|
||||
namespace GeometryTD.CustomEvent
|
||||
{
|
||||
@@ -9,19 +12,49 @@ namespace GeometryTD.CustomEvent
|
||||
|
||||
public override int Id => EventId;
|
||||
|
||||
public string RunId { get; private set; }
|
||||
|
||||
public int NodeId { get; private set; }
|
||||
|
||||
public RunNodeType NodeType { get; private set; }
|
||||
|
||||
public bool Succeeded { get; private set; }
|
||||
|
||||
public BackpackInventoryData InventorySnapshotAfterNode { get; private set; }
|
||||
|
||||
public NodeCompleteEventArgs()
|
||||
{
|
||||
}
|
||||
|
||||
public static NodeCompleteEventArgs Create()
|
||||
{
|
||||
return Create(null, 0, RunNodeType.None, true, null);
|
||||
}
|
||||
|
||||
public static NodeCompleteEventArgs Create(
|
||||
string runId,
|
||||
int nodeId,
|
||||
RunNodeType nodeType,
|
||||
bool succeeded,
|
||||
BackpackInventoryData inventorySnapshotAfterNode)
|
||||
{
|
||||
var args = ReferencePool.Acquire<NodeCompleteEventArgs>();
|
||||
args.RunId = runId;
|
||||
args.NodeId = nodeId;
|
||||
args.NodeType = nodeType;
|
||||
args.Succeeded = succeeded;
|
||||
args.InventorySnapshotAfterNode = InventoryCloneUtility.CloneInventory(inventorySnapshotAfterNode);
|
||||
|
||||
return args;
|
||||
}
|
||||
|
||||
public override void Clear()
|
||||
{
|
||||
RunId = null;
|
||||
NodeId = 0;
|
||||
NodeType = RunNodeType.None;
|
||||
Succeeded = false;
|
||||
InventorySnapshotAfterNode = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using GameFramework;
|
||||
using GameFramework.Event;
|
||||
using GeometryTD.Procedure;
|
||||
|
||||
namespace GeometryTD.CustomEvent
|
||||
{
|
||||
@@ -9,19 +10,40 @@ namespace GeometryTD.CustomEvent
|
||||
|
||||
public override int Id => EventId;
|
||||
|
||||
public string RunId { get; private set; }
|
||||
|
||||
public int NodeId { get; private set; }
|
||||
|
||||
public RunNodeType NodeType { get; private set; }
|
||||
|
||||
public int SequenceIndex { get; private set; }
|
||||
|
||||
public NodeEnterEventArgs()
|
||||
{
|
||||
}
|
||||
|
||||
public static NodeEnterEventArgs Create()
|
||||
{
|
||||
return Create(null, 0, RunNodeType.None, -1);
|
||||
}
|
||||
|
||||
public static NodeEnterEventArgs Create(string runId, int nodeId, RunNodeType nodeType, int sequenceIndex)
|
||||
{
|
||||
var args = ReferencePool.Acquire<NodeEnterEventArgs>();
|
||||
args.RunId = runId;
|
||||
args.NodeId = nodeId;
|
||||
args.NodeType = nodeType;
|
||||
args.SequenceIndex = sequenceIndex;
|
||||
|
||||
return args;
|
||||
}
|
||||
|
||||
public override void Clear()
|
||||
{
|
||||
RunId = null;
|
||||
NodeId = 0;
|
||||
NodeType = RunNodeType.None;
|
||||
SequenceIndex = -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using GeometryTD.CustomUtility;
|
||||
using GeometryTD.Definition;
|
||||
|
||||
namespace GeometryTD.Procedure
|
||||
{
|
||||
public enum RunNodeType
|
||||
{
|
||||
None = 0,
|
||||
Combat = 1,
|
||||
Event = 2,
|
||||
Shop = 3,
|
||||
BossCombat = 4
|
||||
}
|
||||
|
||||
public enum RunNodeStatus
|
||||
{
|
||||
Locked = 0,
|
||||
Available = 1,
|
||||
Completed = 2,
|
||||
Failed = 3,
|
||||
Skipped = 4
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public sealed class RunNodeSeed
|
||||
{
|
||||
public int NodeId { get; set; }
|
||||
public RunNodeType NodeType { get; set; }
|
||||
public LevelThemeType ThemeType { get; set; }
|
||||
public int LinkedLevelId { get; set; }
|
||||
public int SequenceIndex { get; set; } = -1;
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public sealed class RunNodeState
|
||||
{
|
||||
public int NodeId { get; internal set; }
|
||||
public RunNodeType NodeType { get; internal set; }
|
||||
public LevelThemeType ThemeType { get; internal set; }
|
||||
public int LinkedLevelId { get; internal set; }
|
||||
public RunNodeStatus Status { get; internal set; }
|
||||
public int SequenceIndex { get; internal set; }
|
||||
|
||||
internal RunNodeState Clone()
|
||||
{
|
||||
return new RunNodeState
|
||||
{
|
||||
NodeId = NodeId,
|
||||
NodeType = NodeType,
|
||||
ThemeType = ThemeType,
|
||||
LinkedLevelId = LinkedLevelId,
|
||||
Status = Status,
|
||||
SequenceIndex = SequenceIndex
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public sealed class RunState
|
||||
{
|
||||
private readonly List<RunNodeState> _nodes;
|
||||
|
||||
internal RunState(
|
||||
string runId,
|
||||
LevelThemeType themeType,
|
||||
List<RunNodeState> nodes,
|
||||
BackpackInventoryData runInventorySnapshot)
|
||||
{
|
||||
RunId = string.IsNullOrWhiteSpace(runId) ? Guid.NewGuid().ToString("N") : runId;
|
||||
ThemeType = themeType;
|
||||
_nodes = nodes ?? new List<RunNodeState>();
|
||||
RunInventorySnapshot = InventoryCloneUtility.CloneInventory(runInventorySnapshot);
|
||||
CurrentNodeIndex = _nodes.Count > 0 ? 0 : -1;
|
||||
IsCompleted = _nodes.Count <= 0;
|
||||
}
|
||||
|
||||
public string RunId { get; internal set; }
|
||||
|
||||
public LevelThemeType ThemeType { get; internal set; }
|
||||
|
||||
public int CurrentNodeIndex { get; internal set; }
|
||||
|
||||
public bool IsCompleted { get; internal set; }
|
||||
|
||||
public BackpackInventoryData RunInventorySnapshot { get; internal set; }
|
||||
|
||||
public IReadOnlyList<RunNodeState> Nodes => _nodes;
|
||||
|
||||
public RunNodeState CurrentNode
|
||||
{
|
||||
get
|
||||
{
|
||||
if (CurrentNodeIndex < 0 || CurrentNodeIndex >= _nodes.Count)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return _nodes[CurrentNodeIndex];
|
||||
}
|
||||
}
|
||||
|
||||
public bool CanEnterCurrentNode => !IsCompleted && CurrentNode != null && CurrentNode.Status == RunNodeStatus.Available;
|
||||
|
||||
public int CompletedNodeCount
|
||||
{
|
||||
get
|
||||
{
|
||||
int count = 0;
|
||||
foreach (var nodeState in _nodes)
|
||||
{
|
||||
if (nodeState.Status == RunNodeStatus.Completed)
|
||||
{
|
||||
count++;
|
||||
}
|
||||
}
|
||||
|
||||
return count;
|
||||
}
|
||||
}
|
||||
|
||||
internal void ReplaceInventorySnapshot(BackpackInventoryData inventorySnapshot)
|
||||
{
|
||||
RunInventorySnapshot = InventoryCloneUtility.CloneInventory(inventorySnapshot);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4de7f8a43a9f4be8b0e1a5d6c7f80911
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,51 @@
|
||||
using GeometryTD.Definition;
|
||||
|
||||
namespace GeometryTD.Procedure
|
||||
{
|
||||
public static class RunStateAdvanceService
|
||||
{
|
||||
public static bool TryCompleteCurrentNode(
|
||||
RunState runState,
|
||||
bool succeeded,
|
||||
BackpackInventoryData inventorySnapshotAfterNode)
|
||||
{
|
||||
if (runState == null || runState.IsCompleted)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
RunNodeState currentNode = runState.CurrentNode;
|
||||
if (currentNode == null || currentNode.Status != RunNodeStatus.Available)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
runState.ReplaceInventorySnapshot(inventorySnapshotAfterNode);
|
||||
|
||||
if (!succeeded)
|
||||
{
|
||||
currentNode.Status = RunNodeStatus.Failed;
|
||||
return true;
|
||||
}
|
||||
|
||||
currentNode.Status = RunNodeStatus.Completed;
|
||||
|
||||
int nextIndex = runState.CurrentNodeIndex + 1;
|
||||
if (nextIndex >= runState.Nodes.Count)
|
||||
{
|
||||
runState.CurrentNodeIndex = runState.Nodes.Count;
|
||||
runState.IsCompleted = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
runState.CurrentNodeIndex = nextIndex;
|
||||
RunNodeState nextNode = runState.CurrentNode;
|
||||
if (nextNode != null && nextNode.Status == RunNodeStatus.Locked)
|
||||
{
|
||||
nextNode.Status = RunNodeStatus.Available;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8f1c5db7a94a44b9aa5f7745ca0c2202
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,41 @@
|
||||
using System.Collections.Generic;
|
||||
using GeometryTD.Definition;
|
||||
|
||||
namespace GeometryTD.Procedure
|
||||
{
|
||||
public static class RunStateFactory
|
||||
{
|
||||
public static RunState Create(
|
||||
LevelThemeType themeType,
|
||||
BackpackInventoryData initialInventorySnapshot,
|
||||
IEnumerable<RunNodeSeed> nodeSeeds,
|
||||
string runId = null)
|
||||
{
|
||||
List<RunNodeState> nodes = new List<RunNodeState>();
|
||||
if (nodeSeeds != null)
|
||||
{
|
||||
int sequenceIndex = 0;
|
||||
foreach (RunNodeSeed seed in nodeSeeds)
|
||||
{
|
||||
if (seed == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
nodes.Add(new RunNodeState
|
||||
{
|
||||
NodeId = seed.NodeId > 0 ? seed.NodeId : sequenceIndex + 1,
|
||||
NodeType = seed.NodeType,
|
||||
ThemeType = seed.ThemeType == LevelThemeType.None ? themeType : seed.ThemeType,
|
||||
LinkedLevelId = seed.LinkedLevelId,
|
||||
SequenceIndex = seed.SequenceIndex >= 0 ? seed.SequenceIndex : sequenceIndex,
|
||||
Status = sequenceIndex == 0 ? RunNodeStatus.Available : RunNodeStatus.Locked
|
||||
});
|
||||
sequenceIndex++;
|
||||
}
|
||||
}
|
||||
|
||||
return new RunState(runId, themeType, nodes, initialInventorySnapshot);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9b14d5010eb149678d2f4b269d6b8401
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c1a8e1a80db2487ea5ef3e8f700c4203
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a3d90b95afbf46d1be5e59f90b2f3104
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,131 @@
|
||||
using System.Collections.Generic;
|
||||
using GeometryTD.CustomEvent;
|
||||
using GeometryTD.Definition;
|
||||
using GeometryTD.Procedure;
|
||||
using NUnit.Framework;
|
||||
|
||||
namespace GeometryTD.Tests.Editor
|
||||
{
|
||||
public sealed class RunStateTests
|
||||
{
|
||||
[Test]
|
||||
public void Factory_Creates_RunState_With_First_Node_Available()
|
||||
{
|
||||
BackpackInventoryData sourceInventory = new BackpackInventoryData
|
||||
{
|
||||
Gold = 120
|
||||
};
|
||||
sourceInventory.ParticipantTowerInstanceIds.Add(11);
|
||||
|
||||
RunState runState = RunStateFactory.Create(
|
||||
LevelThemeType.Plain,
|
||||
sourceInventory,
|
||||
new List<RunNodeSeed>
|
||||
{
|
||||
new RunNodeSeed { NodeType = RunNodeType.Combat, LinkedLevelId = 1 },
|
||||
new RunNodeSeed { NodeType = RunNodeType.Event },
|
||||
new RunNodeSeed { NodeType = RunNodeType.BossCombat, LinkedLevelId = 4 }
|
||||
},
|
||||
"run-test");
|
||||
|
||||
Assert.That(runState.RunId, Is.EqualTo("run-test"));
|
||||
Assert.That(runState.ThemeType, Is.EqualTo(LevelThemeType.Plain));
|
||||
Assert.That(runState.CurrentNodeIndex, Is.EqualTo(0));
|
||||
Assert.That(runState.IsCompleted, Is.False);
|
||||
Assert.That(runState.Nodes.Count, Is.EqualTo(3));
|
||||
Assert.That(runState.Nodes[0].Status, Is.EqualTo(RunNodeStatus.Available));
|
||||
Assert.That(runState.Nodes[1].Status, Is.EqualTo(RunNodeStatus.Locked));
|
||||
Assert.That(runState.RunInventorySnapshot.Gold, Is.EqualTo(120));
|
||||
|
||||
sourceInventory.Gold = 1;
|
||||
sourceInventory.ParticipantTowerInstanceIds[0] = 99;
|
||||
|
||||
Assert.That(runState.RunInventorySnapshot.Gold, Is.EqualTo(120));
|
||||
Assert.That(runState.RunInventorySnapshot.ParticipantTowerInstanceIds[0], Is.EqualTo(11));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void AdvanceService_Completes_Run_And_Unlocks_Next_Node()
|
||||
{
|
||||
RunState runState = RunStateFactory.Create(
|
||||
LevelThemeType.Plain,
|
||||
new BackpackInventoryData { Gold = 50 },
|
||||
new[]
|
||||
{
|
||||
new RunNodeSeed { NodeId = 101, NodeType = RunNodeType.Combat, LinkedLevelId = 1 },
|
||||
new RunNodeSeed { NodeId = 102, NodeType = RunNodeType.Shop },
|
||||
new RunNodeSeed { NodeId = 103, NodeType = RunNodeType.BossCombat, LinkedLevelId = 4 }
|
||||
});
|
||||
|
||||
bool firstCompleted = RunStateAdvanceService.TryCompleteCurrentNode(
|
||||
runState,
|
||||
true,
|
||||
new BackpackInventoryData { Gold = 80 });
|
||||
|
||||
Assert.That(firstCompleted, Is.True);
|
||||
Assert.That(runState.Nodes[0].Status, Is.EqualTo(RunNodeStatus.Completed));
|
||||
Assert.That(runState.CurrentNodeIndex, Is.EqualTo(1));
|
||||
Assert.That(runState.Nodes[1].Status, Is.EqualTo(RunNodeStatus.Available));
|
||||
Assert.That(runState.RunInventorySnapshot.Gold, Is.EqualTo(80));
|
||||
|
||||
RunStateAdvanceService.TryCompleteCurrentNode(runState, true, new BackpackInventoryData { Gold = 95 });
|
||||
RunStateAdvanceService.TryCompleteCurrentNode(runState, true, new BackpackInventoryData { Gold = 130 });
|
||||
|
||||
Assert.That(runState.IsCompleted, Is.True);
|
||||
Assert.That(runState.CurrentNodeIndex, Is.EqualTo(3));
|
||||
Assert.That(runState.CompletedNodeCount, Is.EqualTo(3));
|
||||
Assert.That(runState.RunInventorySnapshot.Gold, Is.EqualTo(130));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void AdvanceService_Failure_Marks_Current_Node_Failed_Without_Completing_Run()
|
||||
{
|
||||
RunState runState = RunStateFactory.Create(
|
||||
LevelThemeType.Plain,
|
||||
new BackpackInventoryData { Gold = 20 },
|
||||
new[]
|
||||
{
|
||||
new RunNodeSeed { NodeType = RunNodeType.Combat }
|
||||
});
|
||||
|
||||
bool result = RunStateAdvanceService.TryCompleteCurrentNode(
|
||||
runState,
|
||||
false,
|
||||
new BackpackInventoryData { Gold = 5 });
|
||||
|
||||
Assert.That(result, Is.True);
|
||||
Assert.That(runState.IsCompleted, Is.False);
|
||||
Assert.That(runState.CurrentNodeIndex, Is.EqualTo(0));
|
||||
Assert.That(runState.CurrentNode.Status, Is.EqualTo(RunNodeStatus.Failed));
|
||||
Assert.That(runState.RunInventorySnapshot.Gold, Is.EqualTo(5));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void NodeCompleteEventArgs_Clones_Inventory_And_Clears_State()
|
||||
{
|
||||
BackpackInventoryData inventory = new BackpackInventoryData { Gold = 66 };
|
||||
NodeCompleteEventArgs eventArgs = NodeCompleteEventArgs.Create(
|
||||
"run-1",
|
||||
7,
|
||||
RunNodeType.Shop,
|
||||
true,
|
||||
inventory);
|
||||
|
||||
inventory.Gold = 1;
|
||||
|
||||
Assert.That(eventArgs.RunId, Is.EqualTo("run-1"));
|
||||
Assert.That(eventArgs.NodeId, Is.EqualTo(7));
|
||||
Assert.That(eventArgs.NodeType, Is.EqualTo(RunNodeType.Shop));
|
||||
Assert.That(eventArgs.Succeeded, Is.True);
|
||||
Assert.That(eventArgs.InventorySnapshotAfterNode.Gold, Is.EqualTo(66));
|
||||
|
||||
eventArgs.Clear();
|
||||
|
||||
Assert.That(eventArgs.RunId, Is.Null);
|
||||
Assert.That(eventArgs.NodeId, Is.EqualTo(0));
|
||||
Assert.That(eventArgs.NodeType, Is.EqualTo(RunNodeType.None));
|
||||
Assert.That(eventArgs.Succeeded, Is.False);
|
||||
Assert.That(eventArgs.InventorySnapshotAfterNode, Is.Null);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3d3e6fd8ec274118b59f66c6efc41405
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Reference in New Issue
Block a user