补充事件参数 + 完善 UI 设计

- 事件节点在 EventNodeComponent.cs 的 StartEvent/EndEvent 会携带 runId/nodeId/nodeType/sequenceIndex
- 商店节点在 ShopNodeComponent.cs 也会带同一套字段
- 战斗节点通过 CombatNodeComponent.cs 把上下文传进 CombatScheduler,再由 CombatRunningPhaseState.cs 发 NodeEnterEventArgs、由   CombatSchedulerFlowCoordinator.cs 发 NodeCompleteEventArgs
- NodeCompleteEventArgs.cs 现在新增了 SequenceIndex
- 各种 UI 的覆盖问题
This commit is contained in:
SepComet 2026-03-08 11:51:00 +08:00
parent 5c6f9bf3a4
commit 47ed27bebb
16 changed files with 277 additions and 52 deletions

View File

@ -5,8 +5,8 @@
100 主菜单 MenuForm Medium False True
101 设置 SettingForm Medium False True
102 关于 AboutForm Medium False True
110 主界面 MainForm Medium False True
111 仓库UI RepoForm Medium False True
110 主界面 MainForm Medium False False
111 仓库UI RepoForm Medium False False
112 大地图UI NodeMapForm Medium False True
113 详细信息 ItemDescForm Medium True False
114 奖励选择UI RewardSelectForm Medium False True

View File

@ -3,6 +3,7 @@ using GameFramework.DataTable;
using GeometryTD.CustomEvent;
using GeometryTD.DataTable;
using GeometryTD.Definition;
using GeometryTD.Procedure;
using UnityEngine;
using UnityGameFramework.Runtime;
@ -174,7 +175,12 @@ namespace GeometryTD.CustomComponent
EnsureCombatRuntimeInitialized();
}
public void StartCombat(int levelId = 0)
public void StartCombat(
int levelId = 0,
string runId = null,
int nodeId = 0,
RunNodeType nodeType = RunNodeType.None,
int sequenceIndex = -1)
{
if (!EnsureCombatRuntimeInitialized())
{
@ -221,7 +227,14 @@ namespace GeometryTD.CustomComponent
LastDefeatedEnemyCount = 0;
LastGainedCoin = 0;
LastGainedGold = 0;
if (!_combatScheduler.Start(selectedLevel, phaseList, _selectedSpawnEntriesByPhaseId))
if (!_combatScheduler.Start(
selectedLevel,
phaseList,
_selectedSpawnEntriesByPhaseId,
runId,
nodeId,
nodeType,
sequenceIndex))
{
CurrentLevel = null;
return;

View File

@ -4,6 +4,7 @@ using GeometryTD.CustomEvent;
using GeometryTD.DataTable;
using GeometryTD.Definition;
using GeometryTD.Entity;
using GeometryTD.Procedure;
using GeometryTD.UI;
using UnityEngine;
using UnityGameFramework.Runtime;
@ -67,7 +68,11 @@ namespace GeometryTD.CustomComponent
public bool Start(
DRLevel level,
IReadOnlyList<DRLevelPhase> phases,
IReadOnlyDictionary<int, IReadOnlyList<DRLevelSpawnEntry>> spawnEntriesByPhaseId)
IReadOnlyDictionary<int, IReadOnlyList<DRLevelSpawnEntry>> spawnEntriesByPhaseId,
string runId = null,
int nodeId = 0,
RunNodeType nodeType = RunNodeType.None,
int sequenceIndex = -1)
{
if (!_initialized || _context.Entity == null)
{
@ -89,6 +94,10 @@ namespace GeometryTD.CustomComponent
_context.IsFinishAsVictory = true;
_context.CurrentLevel = level;
_context.RunId = runId;
_context.NodeId = nodeId;
_context.NodeType = nodeType;
_context.SequenceIndex = sequenceIndex;
_context.CombatInRunResourceManager.InitializeForCombat(level);
for (int i = 0; i < phases.Count; i++)
{

View File

@ -2,6 +2,7 @@ using System.Collections.Generic;
using GeometryTD.CustomEvent;
using GeometryTD.DataTable;
using GeometryTD.Definition;
using GeometryTD.Procedure;
using GeometryTD.UI;
using UnityEngine;
using UnityGameFramework.Runtime;
@ -55,6 +56,10 @@ namespace GeometryTD.CustomComponent
_context.IsFinishAsVictory = true;
_context.IsCompleted = false;
_context.NodeEnterFired = false;
_context.RunId = null;
_context.NodeId = 0;
_context.NodeType = RunNodeType.None;
_context.SequenceIndex = -1;
}
public void CleanupAllCombatEntities()
@ -196,7 +201,15 @@ namespace GeometryTD.CustomComponent
public void CompleteNormalCombatAndNotify(bool succeeded)
{
CompleteCombat(succeeded);
GameEntry.Event.Fire(this, NodeCompleteEventArgs.Create());
GameEntry.Event.Fire(
this,
NodeCompleteEventArgs.Create(
_context.RunId,
_context.NodeId,
_context.NodeType,
_context.SequenceIndex,
succeeded,
GameEntry.PlayerInventory != null ? GameEntry.PlayerInventory.GetInventorySnapshot() : null));
}
public void CompleteFailureCombatAndNotify()

View File

@ -2,6 +2,7 @@ using System;
using System.Collections.Generic;
using GeometryTD.DataTable;
using GeometryTD.Entity;
using GeometryTD.Procedure;
using GeometryTD.UI;
using UnityGameFramework.Runtime;
@ -29,5 +30,9 @@ namespace GeometryTD.CustomComponent
public bool IsCompleted { get; set; }
public bool NodeEnterFired { get; set; }
public CombatSettlementContext SettlementContext { get; set; }
public string RunId { get; set; }
public int NodeId { get; set; }
public RunNodeType NodeType { get; set; }
public int SequenceIndex { get; set; }
}
}

View File

@ -38,7 +38,13 @@ namespace GeometryTD.CustomComponent
if (!Context.NodeEnterFired)
{
Context.NodeEnterFired = true;
GameEntry.Event.Fire(Flow, NodeEnterEventArgs.Create());
GameEntry.Event.Fire(
Flow,
NodeEnterEventArgs.Create(
Context.RunId,
Context.NodeId,
Context.NodeType,
Context.SequenceIndex));
}
Log.Info(

View File

@ -3,6 +3,7 @@ using GameFramework.DataTable;
using GeometryTD.CustomEvent;
using GeometryTD.DataTable;
using GeometryTD.Definition;
using GeometryTD.Procedure;
using Newtonsoft.Json.Linq;
using GeometryTD.UI;
using UnityEngine;
@ -12,6 +13,11 @@ namespace GeometryTD.CustomComponent
{
public class EventNodeComponent : GameFrameworkComponent
{
private string _activeRunId;
private int _activeNodeId;
private RunNodeType _activeNodeType = RunNodeType.None;
private int _activeSequenceIndex = -1;
private readonly List<EventItem> _eventItems = new List<EventItem>();
private EventFormUseCase _eventFormUseCase;
@ -47,7 +53,7 @@ namespace GeometryTD.CustomComponent
Log.Info("EventNodeComponent initialized with {0} events.", _eventItems.Count);
}
public void StartEvent()
public void StartEvent(string runId = null, int nodeId = 0, RunNodeType nodeType = RunNodeType.None, int sequenceIndex = -1)
{
if (!_initialized)
{
@ -69,15 +75,36 @@ namespace GeometryTD.CustomComponent
return;
}
_activeRunId = runId;
_activeNodeId = nodeId;
_activeNodeType = nodeType;
_activeSequenceIndex = sequenceIndex;
_eventFormUseCase.SetCurrentEvent(randomEvent);
GameEntry.UIRouter.OpenUI(UIFormType.EventForm);
GameEntry.Event.Fire(this, NodeEnterEventArgs.Create());
GameEntry.Event.Fire(this, NodeEnterEventArgs.Create(runId, nodeId, nodeType, sequenceIndex));
}
public void EndEvent()
{
GameEntry.UIRouter.CloseUI(UIFormType.EventForm);
GameEntry.Event.Fire(this, NodeCompleteEventArgs.Create());
GameEntry.Event.Fire(
this,
NodeCompleteEventArgs.Create(
_activeRunId,
_activeNodeId,
_activeNodeType,
_activeSequenceIndex,
true,
GameEntry.PlayerInventory != null ? GameEntry.PlayerInventory.GetInventorySnapshot() : null));
ClearActiveNodeContext();
}
private void ClearActiveNodeContext()
{
_activeRunId = null;
_activeNodeId = 0;
_activeNodeType = RunNodeType.None;
_activeSequenceIndex = -1;
}
private static EventOption[] ParseOptions(string optionsRaw)

View File

@ -2,11 +2,17 @@ using GeometryTD.UI;
using UnityGameFramework.Runtime;
using GeometryTD.CustomEvent;
using GeometryTD.Definition;
using GeometryTD.Procedure;
namespace GeometryTD.CustomComponent
{
public class ShopNodeComponent : GameFrameworkComponent
{
private string _activeRunId;
private int _activeNodeId;
private RunNodeType _activeNodeType = RunNodeType.None;
private int _activeSequenceIndex = -1;
private ShopFormUseCase _shopFormUseCase;
private bool _initialized;
@ -22,7 +28,7 @@ namespace GeometryTD.CustomComponent
_initialized = true;
}
public void StartShop()
public void StartShop(string runId = null, int nodeId = 0, RunNodeType nodeType = RunNodeType.None, int sequenceIndex = -1)
{
if (!_initialized)
{
@ -35,14 +41,35 @@ namespace GeometryTD.CustomComponent
return;
}
_activeRunId = runId;
_activeNodeId = nodeId;
_activeNodeType = nodeType;
_activeSequenceIndex = sequenceIndex;
GameEntry.UIRouter.OpenUI(UIFormType.ShopForm);
GameEntry.Event.Fire(this, NodeEnterEventArgs.Create());
GameEntry.Event.Fire(this, NodeEnterEventArgs.Create(runId, nodeId, nodeType, sequenceIndex));
}
public void EndShop()
{
GameEntry.UIRouter.CloseUI(UIFormType.ShopForm);
GameEntry.Event.Fire(this, NodeCompleteEventArgs.Create());
GameEntry.Event.Fire(
this,
NodeCompleteEventArgs.Create(
_activeRunId,
_activeNodeId,
_activeNodeType,
_activeSequenceIndex,
true,
GameEntry.PlayerInventory != null ? GameEntry.PlayerInventory.GetInventorySnapshot() : null));
ClearActiveNodeContext();
}
private void ClearActiveNodeContext()
{
_activeRunId = null;
_activeNodeId = 0;
_activeNodeType = RunNodeType.None;
_activeSequenceIndex = -1;
}
}
}

View File

@ -18,6 +18,8 @@ namespace GeometryTD.CustomEvent
public RunNodeType NodeType { get; private set; }
public int SequenceIndex { get; private set; }
public bool Succeeded { get; private set; }
public BackpackInventoryData InventorySnapshotAfterNode { get; private set; }
@ -28,13 +30,14 @@ namespace GeometryTD.CustomEvent
public static NodeCompleteEventArgs Create()
{
return Create(null, 0, RunNodeType.None, true, null);
return Create(null, 0, RunNodeType.None, -1, true, null);
}
public static NodeCompleteEventArgs Create(
string runId,
int nodeId,
RunNodeType nodeType,
int sequenceIndex,
bool succeeded,
BackpackInventoryData inventorySnapshotAfterNode)
{
@ -42,6 +45,7 @@ namespace GeometryTD.CustomEvent
args.RunId = runId;
args.NodeId = nodeId;
args.NodeType = nodeType;
args.SequenceIndex = sequenceIndex;
args.Succeeded = succeeded;
args.InventorySnapshotAfterNode = InventoryCloneUtility.CloneInventory(inventorySnapshotAfterNode);
@ -53,6 +57,7 @@ namespace GeometryTD.CustomEvent
RunId = null;
NodeId = 0;
NodeType = RunNodeType.None;
SequenceIndex = -1;
Succeeded = false;
InventorySnapshotAfterNode = null;
}

View File

@ -78,11 +78,45 @@ namespace GeometryTD.Procedure
private void OnNodeEnter(object sender, GameEventArgs e)
{
if (!(e is NodeEnterEventArgs))
if (!(e is NodeEnterEventArgs args))
{
return;
}
if (!string.IsNullOrWhiteSpace(args.RunId) &&
_currentRunState != null &&
!string.Equals(args.RunId, _currentRunState.RunId))
{
Log.Warning(
"ProcedureMain.OnNodeEnter() ignored. EventRunId={0}, CurrentRunId={1}.",
args.RunId,
_currentRunState.RunId);
return;
}
RunNodeState currentNode = _currentRunState?.CurrentNode;
if (currentNode != null &&
((args.NodeId > 0 && args.NodeId != currentNode.NodeId) ||
(args.NodeType != RunNodeType.None && args.NodeType != currentNode.NodeType) ||
(args.SequenceIndex >= 0 && args.SequenceIndex != currentNode.SequenceIndex)))
{
Log.Warning(
"ProcedureMain.OnNodeEnter() node mismatch. EventNodeId={0}, EventNodeType={1}, EventSequenceIndex={2}; CurrentNodeId={3}, CurrentNodeType={4}, CurrentSequenceIndex={5}.",
args.NodeId,
args.NodeType,
args.SequenceIndex,
currentNode.NodeId,
currentNode.NodeType,
currentNode.SequenceIndex);
}
Log.Info(
"ProcedureMain.OnNodeEnter() accepted. RunId={0}, NodeId={1}, NodeType={2}, SequenceIndex={3}.",
string.IsNullOrWhiteSpace(args.RunId) ? _currentRunState?.RunId : args.RunId,
args.NodeId,
args.NodeType,
args.SequenceIndex);
GameEntry.UIRouter.CloseUI(UIFormType.NodeMapForm);
GameEntry.UIRouter.CloseUI(UIFormType.MainForm);
}
@ -94,13 +128,24 @@ namespace GeometryTD.Procedure
return;
}
if (!string.IsNullOrWhiteSpace(args.RunId) &&
_currentRunState != null &&
!string.Equals(args.RunId, _currentRunState.RunId))
{
Log.Warning(
"ProcedureMain.OnNodeComplete() ignored. EventRunId={0}, CurrentRunId={1}.",
args.RunId,
_currentRunState.RunId);
return;
}
BackpackInventoryData snapshot = args.InventorySnapshotAfterNode;
if (snapshot == null && GameEntry.PlayerInventory != null)
{
snapshot = GameEntry.PlayerInventory.GetInventorySnapshot();
}
AdvanceRunState(true, snapshot);
AdvanceRunState(args.Succeeded, snapshot);
OpenHubUI();
}
@ -153,16 +198,30 @@ namespace GeometryTD.Procedure
return;
}
GameEntry.CombatNode.StartCombat(currentNode.LinkedLevelId);
GameEntry.CombatNode.StartCombat(
currentNode.LinkedLevelId,
_currentRunState.RunId,
currentNode.NodeId,
currentNode.NodeType,
currentNode.SequenceIndex);
return;
case RunNodeType.Event:
GameEntry.EventNode.StartEvent();
GameEntry.EventNode.StartEvent(
_currentRunState.RunId,
currentNode.NodeId,
currentNode.NodeType,
currentNode.SequenceIndex);
return;
case RunNodeType.Shop:
GameEntry.ShopNode.StartShop();
GameEntry.ShopNode.StartShop(
_currentRunState.RunId,
currentNode.NodeId,
currentNode.NodeType,
currentNode.SequenceIndex);
return;
default:
Log.Warning("ProcedureMain.OnNodeMapNodeEnterRequested() encountered unsupported node type: {0}.", currentNode.NodeType);
Log.Warning("ProcedureMain.OnNodeMapNodeEnterRequested() encountered unsupported node type: {0}.",
currentNode.NodeType);
return;
}
}
@ -206,8 +265,8 @@ namespace GeometryTD.Procedure
private void OpenHubUI()
{
_nodeMapFormUseCase?.SetRunState(_currentRunState);
GameEntry.UIRouter.OpenUI(UIFormType.MainForm);
GameEntry.UIRouter.OpenUI(UIFormType.NodeMapForm);
GameEntry.UIRouter.OpenUI(UIFormType.MainForm);
}
}
}

View File

@ -108,6 +108,7 @@ namespace GeometryTD.Tests.Editor
"run-1",
7,
RunNodeType.Shop,
3,
true,
inventory);
@ -116,6 +117,7 @@ namespace GeometryTD.Tests.Editor
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.SequenceIndex, Is.EqualTo(3));
Assert.That(eventArgs.Succeeded, Is.True);
Assert.That(eventArgs.InventorySnapshotAfterNode.Gold, Is.EqualTo(66));
@ -124,6 +126,7 @@ namespace GeometryTD.Tests.Editor
Assert.That(eventArgs.RunId, Is.Null);
Assert.That(eventArgs.NodeId, Is.EqualTo(0));
Assert.That(eventArgs.NodeType, Is.EqualTo(RunNodeType.None));
Assert.That(eventArgs.SequenceIndex, Is.EqualTo(-1));
Assert.That(eventArgs.Succeeded, Is.False);
Assert.That(eventArgs.InventorySnapshotAfterNode, Is.Null);
}

View File

@ -208,12 +208,12 @@ PrefabInstance:
- target: {fileID: 4491355866364659447, guid: 2307f223279813546a43b221ddd496cc,
type: 3}
propertyPath: m_SizeDelta.x
value: 200
value: 160
objectReference: {fileID: 0}
- target: {fileID: 4491355866364659447, guid: 2307f223279813546a43b221ddd496cc,
type: 3}
propertyPath: m_SizeDelta.y
value: 200
value: 160
objectReference: {fileID: 0}
- target: {fileID: 4491355866364659447, guid: 2307f223279813546a43b221ddd496cc,
type: 3}
@ -275,6 +275,26 @@ PrefabInstance:
propertyPath: m_LocalEulerAnglesHint.z
value: 0
objectReference: {fileID: 0}
- target: {fileID: 7744090569424522082, guid: 2307f223279813546a43b221ddd496cc,
type: 3}
propertyPath: m_SizeDelta.x
value: -30
objectReference: {fileID: 0}
- target: {fileID: 7744090569424522082, guid: 2307f223279813546a43b221ddd496cc,
type: 3}
propertyPath: m_SizeDelta.y
value: -30
objectReference: {fileID: 0}
- target: {fileID: 7744090569424522082, guid: 2307f223279813546a43b221ddd496cc,
type: 3}
propertyPath: m_AnchoredPosition.x
value: 0
objectReference: {fileID: 0}
- target: {fileID: 7744090569424522082, guid: 2307f223279813546a43b221ddd496cc,
type: 3}
propertyPath: m_AnchoredPosition.y
value: 0
objectReference: {fileID: 0}
m_RemovedComponents: []
m_RemovedGameObjects: []
m_AddedGameObjects: []
@ -372,12 +392,12 @@ PrefabInstance:
- target: {fileID: 4491355866364659447, guid: 2307f223279813546a43b221ddd496cc,
type: 3}
propertyPath: m_SizeDelta.x
value: 200
value: 160
objectReference: {fileID: 0}
- target: {fileID: 4491355866364659447, guid: 2307f223279813546a43b221ddd496cc,
type: 3}
propertyPath: m_SizeDelta.y
value: 200
value: 160
objectReference: {fileID: 0}
- target: {fileID: 4491355866364659447, guid: 2307f223279813546a43b221ddd496cc,
type: 3}
@ -439,6 +459,26 @@ PrefabInstance:
propertyPath: m_LocalEulerAnglesHint.z
value: 0
objectReference: {fileID: 0}
- target: {fileID: 7744090569424522082, guid: 2307f223279813546a43b221ddd496cc,
type: 3}
propertyPath: m_SizeDelta.x
value: -30
objectReference: {fileID: 0}
- target: {fileID: 7744090569424522082, guid: 2307f223279813546a43b221ddd496cc,
type: 3}
propertyPath: m_SizeDelta.y
value: -30
objectReference: {fileID: 0}
- target: {fileID: 7744090569424522082, guid: 2307f223279813546a43b221ddd496cc,
type: 3}
propertyPath: m_AnchoredPosition.x
value: 0
objectReference: {fileID: 0}
- target: {fileID: 7744090569424522082, guid: 2307f223279813546a43b221ddd496cc,
type: 3}
propertyPath: m_AnchoredPosition.y
value: 0
objectReference: {fileID: 0}
m_RemovedComponents: []
m_RemovedGameObjects: []
m_AddedGameObjects: []

View File

@ -999,7 +999,7 @@ MonoBehaviour:
m_Name:
m_EditorClassIdentifier:
m_Material: {fileID: 0}
m_Color: {r: 0.2, g: 0.2, b: 0.2, a: 0.5019608}
m_Color: {r: 0.2, g: 0.2, b: 0.2, a: 0.8}
m_RaycastTarget: 1
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
m_Maskable: 1

View File

@ -8,7 +8,7 @@
和上一版相比,仓库已经把 Run 相关基础件进一步接到了 `ProcedureMain`,因此这份清单不再把重点放在“有没有 Run 模型”,而是聚焦下面这几个真实阻塞项:
- 已有 `ProcedureMain + TestMenuForm` 的临时 Run 推进闭环,但还没有正式节点地图 / 节点面板
- 已有 `ProcedureMain + NodeMapForm` 的临时 Run 推进闭环,正式节点面板骨架已经接入流程
- 固定 10 节点顺序已经开始驱动战斗 / 事件 / 商店入口,但节点事件上下文仍然是空载版本。
- 出战入口已有“至少有参战塔”的最小校验,但还没收口成严格的最终合法性约束。
- 品质 / Tag / 耐久仍然停留在部分实现状态,尚未和 M1 范围完全对齐。
@ -90,25 +90,42 @@
- `Assets/GameMain/Scripts/UI/Shop/Controller/ShopFormController.cs`
- `Assets/GameMain/Scripts/UI/Shop/View/ShopForm.cs`
### 7. `NodeMapForm` 五层 UI 已经接入主流程
- 已新增 `NodeMapFormRawData / UseCase / Controller / Context / View``NodeItemContext / NodeItem`
- `ProcedureMain` 已不再打开 `TestMenuForm`,而是打开 `NodeMapForm` 作为 Hub 节点入口。
- `NodeMapFormController` 会把 `NodeItem` 点击转换为 `NodeMapNodeEnterRequestedEventArgs`,再交给流程层决定是否进入节点。
- `NodeItem``Icon` 已改成通过 `SpriteCacheComponent` 按资源名异步获取,`Bg` 负责表示节点状态。
关键文件:
- `Assets/GameMain/Scripts/UI/Game/RawData/NodeMapFormRawData.cs`
- `Assets/GameMain/Scripts/UI/Game/UseCase/NodeMapFormUseCase.cs`
- `Assets/GameMain/Scripts/UI/Game/Controller/NodeMapFormController.cs`
- `Assets/GameMain/Scripts/UI/Game/Context/NodeMapFormContext.cs`
- `Assets/GameMain/Scripts/UI/Game/Context/NodeItemContext.cs`
- `Assets/GameMain/Scripts/UI/Game/View/NodeMapForm.cs`
- `Assets/GameMain/Scripts/UI/Game/View/NodeItem.cs`
- `Assets/GameMain/Scripts/Event/Game/NodeMapNodeEnterRequestedEventArgs.cs`
## 当前未完成
### 1. Run 主流程已经形成临时闭环,但还不是正式节点 UI
### 1. Run 主流程已经形成基于 `NodeMapForm` 的临时闭环,但还没完全收口
- `ProcedureMain` 进入后会创建 Run并打开 `MainForm + TestMenuForm`
- `TestMenuForm` 已不再同时提供三种节点入口,而是通过 `TestMenuFormContext.CurrentNodeType` 只显示当前节点对应的唯一按钮。
- 点击按钮后由 `ProcedureMain` 根据当前 `RunState.CurrentNode` 决定是否允许进入战斗 / 事件 / 商店。
- `ProcedureMain` 进入后会创建 Run并打开 `MainForm + NodeMapForm`。
- `NodeMapForm` 已经有五层结构,`NodeItem` 也能按当前 `RunState` 刷出 10 个节点
- 点击当前节点后,由 `NodeMapFormController` 发出 `NodeMapNodeEnterRequestedEventArgs`,再`ProcedureMain` 根据当前 `RunState.CurrentNode` 决定是否允许进入战斗 / 事件 / 商店。
- 节点完成或战斗失败后,`ProcedureMain` 会推进 `RunState`,再重新打开 Hub UI。
- 因此当前已经能从开始游戏后按固定顺序推进一局的临时版本。
- 因此当前已经不是“测试菜单三选一”,而是“节点地图单入口”的临时版本。
当前缺口:
- UI 仍是测试面板,不是正式节点地图或正式节点信息面板。
- `TestMenuFormContext` 目前只传 `CurrentNodeType`还没有显示节点序号、总数、Boss 标记等关键信息。
- `NodeMapForm` 已接入,但当前仍偏 MVP 骨架缺节点连线、节点说明、Boss 特效等正式表现
- `NodeMapFormContext` 当前只提供基础进度和当前节点信息,还没有更完整的地图展示上下文
- Run 完成后的正式结算 / 收尾表现仍未建立。
关键文件:
- `Assets/GameMain/Scripts/Procedure/ProcedureMain.cs`
- `Assets/GameMain/Scripts/UI/Menu/View/TestMenuForm.cs`
- `Assets/GameMain/Scripts/UI/Menu/Controller/TestMenuFormController.cs`
- `Assets/GameMain/Scripts/UI/Game/View/NodeMapForm.cs`
- `Assets/GameMain/Scripts/UI/Game/Controller/NodeMapFormController.cs`
### 2. 固定 10 节点序列已开始驱动真实流程,但上下文仍不完整
@ -131,7 +148,7 @@
- 现在可以单独打开商店、随机生成组件、购买并退出。
- 商店结束后已能通过 `ProcedureMain` 推进当前 `RunState`
- 但退出后仍是回到 `MainForm + TestMenuForm`,而不是真实节点地图或正式节点选择界面
- 但退出后当前回流的是 `MainForm + NodeMapForm` 的 MVP 节点面板,而不是完整表现版地图
关键文件:
- `Assets/GameMain/Scripts/CustomComponent/ShopNodeComponent.cs`
@ -183,21 +200,21 @@
结合静态代码检查,当前更接近下面这个状态:
- `P0-04`:基础模型已完成,并已接入 `ProcedureMain` 的临时 Run 闭环。
- `P0-05`:固定 10 节点序列已由 builder + `ProcedureMain` 开始驱动实际流程,但缺完整事件上下文与正式节点 UI
- `P0-06`:节点进入、完成、失败后回 Hub 的临时闭环已存在,但“正式地图/节点界面 + 正式结算”仍未完成。
- `P0-04`:基础模型已完成,并已接入 `ProcedureMain + NodeMapForm` 的临时 Run 闭环。
- `P0-05`:固定 10 节点序列已由 builder + `ProcedureMain + NodeMapForm` 驱动实际流程,但缺完整事件上下文与地图表现层
- `P0-06`:节点进入、完成、失败后回 `NodeMapForm` 的临时闭环已存在,但正式地图表现与正式结算仍未完成。
- `P0-10`:未完成。
- `P0-11`:未完成。
- `P0-12`:未完成。
换句话说,仓库已经不再是“还没有 Run 模型 / 10 节点 / 商店最小实现”的状态;真正的缺口是“这些能力已经接成临时可跑版本,但还没收口成正式 M1 主流程表现层与上下文系统”。
换句话说,仓库已经不再是“还没有 Run 模型 / 10 节点 / 商店最小实现”的状态;真正的缺口是“这些能力已经接成 `NodeMapForm` 驱动的临时可跑版本,但还没收口成正式 M1 主流程表现层与上下文系统”。
## 推荐的后续执行顺序
1. 先把临时 Hub 升级成正式节点面板
- 保留 `ProcedureMain` 持有 Run 的做法
- 不再把 `TestMenuForm` 当测试菜单,而是改成正式节点信息面板或节点地图
- 至少补上当前节点序号、总节点数、节点名称、Boss 标记
- 继续以 `NodeMapForm` 为基础补正式节点地图表现
- 至少补上节点连线、Boss 视觉强调、当前节点说明和完成态反馈
2. 再把节点事件改成带上下文的真实推进
- `NodeEnterEventArgs``NodeCompleteEventArgs` 传递 `runId / nodeId / nodeType / sequenceIndex`
@ -218,7 +235,7 @@
## 当前做变更时要记住的约束
- 不要再把 `TestMenuForm` 维持成“测试菜单”语义。
- 不要再把 Hub 退回 `TestMenuForm` 语义。
- 优先补“临时闭环 -> 正式节点 UI / 正式上下文”的收口,不要继续只加单点功能。
- 商店已经接入 Run下一步重点不是继续扩商店而是把 Hub/UI 做正式。
- 若 M1 最终不做完整耐久 / 红色品质,要先同步文档再改代码目标。
@ -233,8 +250,9 @@
- `Assets/GameMain/Scripts/Procedure/RunStateFactory.cs`
- `Assets/GameMain/Scripts/Procedure/RunStateAdvanceService.cs`
- `Assets/GameMain/Scripts/Procedure/FixedRunNodeSequenceBuilder.cs`
- 临时 Hub / 节点入口:
- `Assets/GameMain/Scripts/UI/Menu/Controller/TestMenuFormController.cs`
- 当前 Hub / 节点入口:
- `Assets/GameMain/Scripts/UI/Game/Controller/NodeMapFormController.cs`
- `Assets/GameMain/Scripts/UI/Game/View/NodeMapForm.cs`
- 战斗节点 facade
- `Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatNodeComponent.cs`
- 事件节点:
@ -251,4 +269,4 @@
- 这份清单基于 2026-03-08 的静态代码检查更新。
- 本次已修正上一版中“Run 模型不存在”“10 节点未实现”“商店节点基本未实现”等过期判断。
- 当前最值得优先推进的是:`ProcedureMain 继续收口 Run 流程 -> 节点事件带上下文 -> 战斗按节点关卡进入 -> 出战校验`。
- 当前最值得优先推进的是:`NodeMapForm 继续收口表现层 -> 节点事件带上下文 -> 战斗按节点关卡进入 -> 出战校验`。

View File

@ -16,9 +16,9 @@
| [x] | P0-01 | 冻结 MVP 范围(只保留:战斗节点/事件节点/商店节点/节点后组装) | `docs/MVP-Scope.md` | 明确“做/不做”清单,团队评审通过 |
| [x] | P0-02 | 补齐数据表:组件、配件、敌人、波次、节点、事件、商店商品 | `Assets/GameMain/DataTables/*.txt` | 游戏启动可无报错加载全部新增表 |
| [x] | P0-03 | 新增/完善 DataRow 解析类 | `Assets/GameMain/Scripts/DataTable/*.cs` | 每个新增表都有对应 DR 类并成功解析 |
| [~] | P0-04 | 建立单局 Run 状态模型(金币、生命、库存、当前节点) | `Assets/GameMain/Scripts/Procedure/` | 已有 `RunState` / 推进模型 / 测试,并已接入 `ProcedureMain` 的临时 Run 闭环,但还不是正式节点 UI |
| [~] | P0-05 | 实现 10 节点地图生成(最后节点固定 Boss | `Assets/GameMain/Scripts/Procedure/`<br>`Assets/GameMain/Scripts/Scene/` | 已有固定 10 节点序列 builder 与测试,且已开始驱动真实流程,但节点事件上下文与正式节点面板仍未完成 |
| [~] | P0-06 | 实现节点选择与跳转流程(战斗/事件/商店) | `Assets/GameMain/Scripts/Procedure/` | `ProcedureMain + TestMenuForm` 的临时闭环已可推进战斗/事件/商店,但仍缺正式节点地图/节点面板与正式结算收尾 |
| [~] | P0-04 | 建立单局 Run 状态模型(金币、生命、库存、当前节点) | `Assets/GameMain/Scripts/Procedure/` | 已有 `RunState` / 推进模型 / 测试,并已接入 `ProcedureMain + NodeMapForm` 的临时 Run 闭环 |
| [~] | P0-05 | 实现 10 节点地图生成(最后节点固定 Boss | `Assets/GameMain/Scripts/Procedure/`<br>`Assets/GameMain/Scripts/Scene/` | 已有固定 10 节点序列 builder 与测试,且已`NodeMapForm` 驱动节点入口,但节点事件上下文与地图表现层仍未完成 |
| [~] | P0-06 | 实现节点选择与跳转流程(战斗/事件/商店) | `Assets/GameMain/Scripts/Procedure/` | `ProcedureMain + NodeMapForm` 的临时闭环已可推进战斗/事件/商店,但仍缺完整地图表现、正式结算收尾与节点上下文回流 |
| [x] | P0-07 | 战斗节点基础玩法:放置塔、出怪、基地扣血、胜负判定 | `Assets/GameMain/Scripts/Entity/`<br>`Assets/GameMain/Scripts/Scene/` | 可完整打一场并得到胜利/失败结果 |
| [x] | P0-08 | 胜利波次与结算规则100/80/50/<50 | `Assets/GameMain/Scripts/Procedure/` | 结算奖励与惩罚严格匹配设计文档 |
| [x] | P0-09 | 敌人掉落与关卡奖励(组件/配件/金币) | `Assets/GameMain/Scripts/Entity/` | 战斗结束能发放掉落并写入库存 |
@ -55,7 +55,7 @@
## 本周建议开工顺序
1. 先把 `P0-04` ~ `P0-06` 从“临时闭环”收口成正式节点面板 / 节点地图,并补齐节点事件上下文
1. 先把 `P0-04` ~ `P0-06` 从“`NodeMapForm` 临时闭环”收口成正式节点地图表现,并补齐节点事件上下文
2. 完成 `P0-10`(把“至少有参战塔”提升为“满足完整合法参战条件才能出战”)
3. 再处理 `P0-11` ~ `P0-12`(品质 / Tag / 耐久是否纳入 M1 需先统一范围)

Binary file not shown.