From 47ed27bebb6546cb0fca90b4551fdb7d851e82f4 Mon Sep 17 00:00:00 2001 From: SepComet <202308010230@stu.csust.edu.cn> Date: Sun, 8 Mar 2026 11:51:00 +0800 Subject: [PATCH] =?UTF-8?q?=E8=A1=A5=E5=85=85=E4=BA=8B=E4=BB=B6=E5=8F=82?= =?UTF-8?q?=E6=95=B0=20+=20=E5=AE=8C=E5=96=84=20UI=20=E8=AE=BE=E8=AE=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 事件节点在 EventNodeComponent.cs 的 StartEvent/EndEvent 会携带 runId/nodeId/nodeType/sequenceIndex - 商店节点在 ShopNodeComponent.cs 也会带同一套字段 - 战斗节点通过 CombatNodeComponent.cs 把上下文传进 CombatScheduler,再由 CombatRunningPhaseState.cs 发 NodeEnterEventArgs、由 CombatSchedulerFlowCoordinator.cs 发 NodeCompleteEventArgs - NodeCompleteEventArgs.cs 现在新增了 SequenceIndex - 各种 UI 的覆盖问题 --- Assets/GameMain/DataTables/UIForm.txt | 4 +- .../CombatNode/CombatNodeComponent.cs | 17 +++- .../CombatScheduler/CombatScheduler.cs | 11 ++- .../CombatSchedulerFlowCoordinator.cs | 15 +++- .../CombatSchedulerRuntimeContext.cs | 5 ++ .../CombatStates/CombatRunningPhaseState.cs | 8 +- .../CustomComponent/EventNodeComponent.cs | 33 +++++++- .../CustomComponent/ShopNodeComponent.cs | 33 +++++++- .../Event/Game/NodeCompleteEventArgs.cs | 7 +- .../Scripts/Procedure/ProcedureMain.cs | 75 ++++++++++++++++-- Assets/GameMain/Tests/Editor/RunStateTests.cs | 3 + Assets/GameMain/UI/UIForms/MainForm.prefab | 48 ++++++++++- Assets/GameMain/UI/UIForms/RepoForm.prefab | 2 +- docs/CodeX-TODO.md | 60 +++++++++----- docs/TODO.md | 8 +- 数据表/UIForm.xlsx | Bin 11917 -> 11907 bytes 16 files changed, 277 insertions(+), 52 deletions(-) diff --git a/Assets/GameMain/DataTables/UIForm.txt b/Assets/GameMain/DataTables/UIForm.txt index 836c032..accf4d2 100644 --- a/Assets/GameMain/DataTables/UIForm.txt +++ b/Assets/GameMain/DataTables/UIForm.txt @@ -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 diff --git a/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatNodeComponent.cs b/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatNodeComponent.cs index 868ed7a..99ca303 100644 --- a/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatNodeComponent.cs +++ b/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatNodeComponent.cs @@ -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; diff --git a/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/CombatScheduler.cs b/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/CombatScheduler.cs index e86c4d3..1b992c2 100644 --- a/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/CombatScheduler.cs +++ b/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/CombatScheduler.cs @@ -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 phases, - IReadOnlyDictionary> spawnEntriesByPhaseId) + IReadOnlyDictionary> 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++) { diff --git a/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/CombatSchedulerFlowCoordinator.cs b/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/CombatSchedulerFlowCoordinator.cs index d4216ec..dcc451b 100644 --- a/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/CombatSchedulerFlowCoordinator.cs +++ b/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/CombatSchedulerFlowCoordinator.cs @@ -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() diff --git a/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/CombatSchedulerRuntimeContext.cs b/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/CombatSchedulerRuntimeContext.cs index d4e3d9f..bbf451f 100644 --- a/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/CombatSchedulerRuntimeContext.cs +++ b/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/CombatSchedulerRuntimeContext.cs @@ -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; } } } diff --git a/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/CombatStates/CombatRunningPhaseState.cs b/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/CombatStates/CombatRunningPhaseState.cs index cf29545..183ce08 100644 --- a/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/CombatStates/CombatRunningPhaseState.cs +++ b/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/CombatStates/CombatRunningPhaseState.cs @@ -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( diff --git a/Assets/GameMain/Scripts/CustomComponent/EventNodeComponent.cs b/Assets/GameMain/Scripts/CustomComponent/EventNodeComponent.cs index 702c86c..40c49e4 100644 --- a/Assets/GameMain/Scripts/CustomComponent/EventNodeComponent.cs +++ b/Assets/GameMain/Scripts/CustomComponent/EventNodeComponent.cs @@ -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 _eventItems = new List(); 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) diff --git a/Assets/GameMain/Scripts/CustomComponent/ShopNodeComponent.cs b/Assets/GameMain/Scripts/CustomComponent/ShopNodeComponent.cs index ffd81e6..1d85e98 100644 --- a/Assets/GameMain/Scripts/CustomComponent/ShopNodeComponent.cs +++ b/Assets/GameMain/Scripts/CustomComponent/ShopNodeComponent.cs @@ -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; } } } diff --git a/Assets/GameMain/Scripts/Event/Game/NodeCompleteEventArgs.cs b/Assets/GameMain/Scripts/Event/Game/NodeCompleteEventArgs.cs index 86c119b..ead88be 100644 --- a/Assets/GameMain/Scripts/Event/Game/NodeCompleteEventArgs.cs +++ b/Assets/GameMain/Scripts/Event/Game/NodeCompleteEventArgs.cs @@ -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; } diff --git a/Assets/GameMain/Scripts/Procedure/ProcedureMain.cs b/Assets/GameMain/Scripts/Procedure/ProcedureMain.cs index 78439b5..e120ebc 100644 --- a/Assets/GameMain/Scripts/Procedure/ProcedureMain.cs +++ b/Assets/GameMain/Scripts/Procedure/ProcedureMain.cs @@ -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); } } -} +} \ No newline at end of file diff --git a/Assets/GameMain/Tests/Editor/RunStateTests.cs b/Assets/GameMain/Tests/Editor/RunStateTests.cs index f9ef14a..d6e38d3 100644 --- a/Assets/GameMain/Tests/Editor/RunStateTests.cs +++ b/Assets/GameMain/Tests/Editor/RunStateTests.cs @@ -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); } diff --git a/Assets/GameMain/UI/UIForms/MainForm.prefab b/Assets/GameMain/UI/UIForms/MainForm.prefab index daba16e..da6977e 100644 --- a/Assets/GameMain/UI/UIForms/MainForm.prefab +++ b/Assets/GameMain/UI/UIForms/MainForm.prefab @@ -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: [] diff --git a/Assets/GameMain/UI/UIForms/RepoForm.prefab b/Assets/GameMain/UI/UIForms/RepoForm.prefab index faccc68..39c1040 100644 --- a/Assets/GameMain/UI/UIForms/RepoForm.prefab +++ b/Assets/GameMain/UI/UIForms/RepoForm.prefab @@ -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 diff --git a/docs/CodeX-TODO.md b/docs/CodeX-TODO.md index afb1565..dc2ba27 100644 --- a/docs/CodeX-TODO.md +++ b/docs/CodeX-TODO.md @@ -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 继续收口表现层 -> 节点事件带上下文 -> 战斗按节点关卡进入 -> 出战校验`。 diff --git a/docs/TODO.md b/docs/TODO.md index a4cc314..2a393ac 100644 --- a/docs/TODO.md +++ b/docs/TODO.md @@ -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/`
`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/`
`Assets/GameMain/Scripts/Scene/` | 已有固定 10 节点序列 builder 与测试,且已由 `NodeMapForm` 驱动节点入口,但节点事件上下文与地图表现层仍未完成 | +| [~] | P0-06 | 实现节点选择与跳转流程(战斗/事件/商店) | `Assets/GameMain/Scripts/Procedure/` | `ProcedureMain + NodeMapForm` 的临时闭环已可推进战斗/事件/商店,但仍缺完整地图表现、正式结算收尾与节点上下文回流 | | [x] | P0-07 | 战斗节点基础玩法:放置塔、出怪、基地扣血、胜负判定 | `Assets/GameMain/Scripts/Entity/`
`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 需先统一范围) diff --git a/数据表/UIForm.xlsx b/数据表/UIForm.xlsx index efefb141a238db11457de8511fb9090fa4c617de..10457326b4f9d440f7e1808a6af7e438d126fac4 100644 GIT binary patch delta 1896 zcmV-u2bcJbU4vb)lK}*^{e{DmmjNt)wAC>}+kwiS90;o@zQyFOD>i9JqTT+zX}i_R z1RrzH$(M63$yNv11Yclm-KreL5kUbc-ilgPIeISF;SvSbF(p{jD#+0hELs)UR~gro zw+5a}tHHP$Y~V@CQm%8fcTQ7`E#HG=Hu4VT#a(M8bN*;5tQr4c72t%B1(x7{m|%|K z9vJG0h)zhsC#3!~%>WP_Ljw|&vp9}o{Nr_Cr2X^4pv;`Ic1P_yo#M{ff{)_lKG=FP zc3l^B=@7%`kMZl~aXVBEYt`?H11d7XDL24eYl? z{rbJGf)CPmC2eM5JQ#)nX$rb3o3~l`&)?aX`!MKO-mLP9He?olk}mu@|Mi#Yp0@AZ zh7cA&gr=K?8^*SeQPdS1Qh(>&fNn_xJ!{(5IfIYwThwh^lCQ*|x{5}_;Uuc_vI%8` z$M&2UbiFPMlG0*VlZMG4Eva$_>+d$@wo}o*ug;3MYV$qpRjtQZVk&$3c=6a-UtJb0 z?dX~f3R*{UY3?o`q6cfAb>Rf^$g1=9eYgDrfw!=^*Rm?vr`WDq5PxAh&VIii?6=*Z zXb!6|1jTm|Vacvzv`%xDhx4iU2431|K3$byk$?3;OV+dSDSlkW_aSr$q^f4&GKR$J{ui_GpJ9rqpTqoF z&6kP1d1-?cS?9ZoC4aQ~uUxUsEW97wjf~e#sE|MD{x?#-Z5XU=90qj9sn>iM~W1kgdetZ}%2aXN{;e9U$#_?c$t6#VL+#Ci5 zh0uT4fE@_dFIE8WD$wm9zSl39;jBQgWf2J84}nJK4u!Rfp?~nMh7Jce6a9jiXRTp! z2n6pc(78kJ4nyHx4V_%#A(G#$NGhJ^^Jz=>0n`BaibYDmn&j8eCkbq7oAc5YKi)y+ z7My+poVSDlq)`X0AM@cf`hXNI4#cxd#5R$*#KsDhxx^9AtAEpuT;i^uS$Ixe;@&0}KP57kc(7wEPZ5>@IDVHH0Q@HgTp~t^lz0%7 zNvQ`xskHDQD3>x1f|6RZb46k%;2YUCx`=f4K}?GJGVs9r+=%HY&RMt>;E!h;~wnTbdND9F+!bPP)X z)X_@>0B#7-F~!%Ls9r5#gz`J}ASk~V9t2I4nTTJ2OXwJu0Qe1ci2%S20cdxL>NNzQ zqnUaTeKdu}g$L1#7f5CzegQ6_V^{*Ygz zwMbQ{8YZ#oHdG;ximTh^-1P zfq&PjQnyI0O0`H;`a2RCpTw$LtHL@{AA{m7JdCT#oa=}x7tu4>s^GeKoho(T)T&gA zRE0`*602@Q6=G5!fMv*H|4Wro3stqLxd*QruBQ>{w12vxVv0aV?FDyOV_ z7^keejGI*^gFPI|x{DmkD!77Pr%K&nwSOwr!d1aDcl!ZJ0*`xftr|OJ-NQI#-NQIp zWim%pokC<*$>+xM5#Kg{BY))WTiJ9$Mb_}hZaBDuGu+C@!14p5TX8>qr3{{d%d0B< zKLmkW=Wqbe!uH!r+ze%- zz5|{zNB#ei%zv|y4E!7cwUdr1Kmzt7lh7$L0UDD7Dw71Y{e{DmnkvTuLX%=EHUeoS zlaD1FleQ}=0oaq_D?tr;7XScwY%g{>EH(kJlMgLG0cVp? iEqDQqlgTYX0l$+AE;a>$Hr(ullSVE<23aWp0002Zrj51$ delta 1918 zcmV-^2Z8v5U5#C^lK}*aiDhh)mjNt)q?Iv3+kwiS90;o@zQyEjD>i9JqTT+zX*+9W zf{(f9=m@ox8cyAcSP8>jSmjs6n;;yxd`*5^-Y`A#md%!6n3#`C@bICo& zJuo&C5uK4zOi1%#n*ksNh6WU9?=Vd${_%RS%Kdp^P-ae5`;!Ts&T;2#DMoQ}A6-2e zyRJ*RY={x^Q~a`d*bY_WTKBsWfXZA7CMd*8HGq}Eby&7XLt8D)f$y4Nf$ApaY`zZGP+=dy!LPZy5C0}2kM;QUt$)M zWkoWUQg*YP5x)UtjxWAN`2|o*0|b-7DjKs91Iq&iF5Dofj+5aAC4XIS;y4tB?^oLY z5aoUdp%mH*7_|vuN7|LD{k%?tDN&P{O-z~o`#H98Y#*Br2q86KzkY2$IX)cH+1KxN z6}*$SD`_(it*}TrffBw$C+=W5M@@Ab^v?25GgLL87#jn524zzviwuG<% z9GY$(ZW-G>L{V34Nq?Pp1G*y(^lWHb=L|l!uTi&aNxl|?>M9xyhm)wz%O;c#5A8V~ zbh9Z7lG0*dlZHu?mQ*=|^>^EH*D1H}>$CjUZGM2gs`VI4Ol42+7muCw)n(Drj&9hX zpmijd=I-)7y0`XO7mkq!R-LzR``s5X-ofTx%Bo}^V!LWVgnwzD!{IPE?7BhG99Llg zil-4_$-ZN>PIH!ri}Yyi$zGln&fThg9J9U%X#XGAEpqw1y?_UoVSDl zgi!~r?~CCqdWR4#j*2H&5!;HyRg7#!>MCw*#nM%bZAIoPZf(WNRZMzC1bBKz5Ga>0 z;&JskBY#&h?PnI8Q&(|kE0#Z1WUk`g_E>$Uun55MyMzG9e?q`j#3+ywj|4?h>XD#O zT6!cXmNJh71=GqS(a~g)f&+a4sV9E^d93i0gm#yxK7H0=9iTEyJQ5U1OOFJFQf5kC zd`nq_5A0MPCd)oTDiM>F+E z^wAU;mmY~;ypUw3#1Ft#Xb%ej{3N&p0RIKi9|0!`s$K&C&H)|?&H)~YUc3ZgO8fv^ zg@5+20Nh^&0OSS$?J19d>NNnM^p$!fC`~Lq5;Rd}O8fv^h4!!j@M>NbQ?gDM#lQtI z@hE-{@KL-Z=XKXp-y*&!Vth{eZjI>%rLfee(HjucEIpbL(#YQ`VoOj1`bk+fQledK zR8WLnhe}m(8kK61s8BXcV%2S+LO4^O27mc1JsKC4ITz+2h_5UgOw&dM)!KEaRJo^7 zsTPR}rPd@?-3BV8Nqriee?1x(l{pvYa75)wPDK^eYuBMt1)oNxS_CS5xRO|P8>n!i z_GxgY_GnyG=3FOGxsp?;ptQRVm8uOjD%B!TjZq#-_|pfG@C{TrrTa8Er+YLmDt~jX z6R19u*ar}b!0S+{DpI3TEfST!MI)TcS=$UL(P%d7F zO4XYhm1>cwP|8kX)oq|cn$)Lp(z@#tqv*^mpA)D|8!WL=L1B3vDpi?kRH{Xwx^)hq z>NZe0Y2Bl7(z>g0qsr7^4#%|aN`Fp86_lXYp;9%hMx|OfDtP8@KOjlqaWBqQV<)Y9 zG)`LgXq>1rH78JgCXrDkpBu|Ze7pRW{E@e>Wzz)}*+98A98BR1xAHNt{J`i=6mu_> z!834Ks>1&v2vnWJ0f;x0GW7wlZAD)u8sQ^6^RCEq-m)$z=)U0(m*D|I-xnIR59N9u zw%^vGmW}!jc*-30|07xa0FWNDrVQO20>3Ad(I*v?nkgy-03-kalh7>|lin#B0VI?8 zDU$??iDhh)kSfOpOeFvScx;o;Ef