实现单局 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:
2026-03-07 21:45:13 +08:00
parent 380f901c1a
commit 5afcaafff7
16 changed files with 654 additions and 293 deletions
@@ -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: