From 34446ae42a15ed2d215de170c3873e9118eceb20 Mon Sep 17 00:00:00 2001 From: SepComet <202308010230@stu.csust.edu.cn> Date: Fri, 6 Mar 2026 21:02:40 +0800 Subject: [PATCH] Update CombatNodeArchitecture.md --- .../CombatNode/EnemyManager/EnemyManager.cs | 2 +- .../CombatSelectUseCaseConfigurator.cs | 158 +++++ .../Scripts/Entity/EntityLogic/MapEntity.cs | 164 ++---- docs/CombatNodeArchitecture.md | 554 +++++++++++++++--- docs/MapEntityArchitecture.md | 45 +- 5 files changed, 694 insertions(+), 229 deletions(-) create mode 100644 Assets/GameMain/Scripts/Entity/EntityLogic/CombatSelectUseCaseConfigurator.cs diff --git a/Assets/GameMain/Scripts/CustomComponent/CombatNode/EnemyManager/EnemyManager.cs b/Assets/GameMain/Scripts/CustomComponent/CombatNode/EnemyManager/EnemyManager.cs index 3c99985..f240184 100644 --- a/Assets/GameMain/Scripts/CustomComponent/CombatNode/EnemyManager/EnemyManager.cs +++ b/Assets/GameMain/Scripts/CustomComponent/CombatNode/EnemyManager/EnemyManager.cs @@ -204,7 +204,7 @@ namespace GeometryTD.CustomComponent { return; } - + bool wasKilled = EnemyEntity.TryConsumeKilledFlag(ne.EntityId); bool isCombatRunning = _combatScheduler != null && _combatScheduler.IsRunning; int baseDamage = 0; diff --git a/Assets/GameMain/Scripts/Entity/EntityLogic/CombatSelectUseCaseConfigurator.cs b/Assets/GameMain/Scripts/Entity/EntityLogic/CombatSelectUseCaseConfigurator.cs new file mode 100644 index 0000000..fcfc781 --- /dev/null +++ b/Assets/GameMain/Scripts/Entity/EntityLogic/CombatSelectUseCaseConfigurator.cs @@ -0,0 +1,158 @@ +using System; +using System.Collections.Generic; +using GeometryTD.CustomUtility; +using GeometryTD.Definition; +using GeometryTD.UI; +using UnityEngine; + +namespace GeometryTD.Entity +{ + public sealed class CombatSelectUseCaseConfigurator + { + private const int BuildOptionCount = 4; + + private readonly BuildTowerVisualInfo[] _buildTowerVisualInfos = new BuildTowerVisualInfo[BuildOptionCount]; + + public void Configure( + CombatSelectFormUseCase useCase, + Func coinProvider, + Func> buildActionFactory, + Func upgradeAction, + int upgradeCost, + Func destroyAction, + int destroyGain, + int[] buildTowerCosts, + int currentBuildTowerCount, + BackpackInventoryData inventorySnapshot, + IReadOnlyList participantTowers) + { + if (useCase == null) + { + return; + } + + useCase.SetCoinProvider(coinProvider); + + Dictionary muzzleMap = BuildComponentMap(inventorySnapshot?.MuzzleComponents); + Dictionary bearingMap = BuildComponentMap(inventorySnapshot?.BearingComponents); + Dictionary baseMap = BuildComponentMap(inventorySnapshot?.BaseComponents); + + int availableBuildCount = Mathf.Clamp(currentBuildTowerCount, 0, BuildOptionCount); + for (int i = 0; i < BuildOptionCount; i++) + { + bool isBuildAvailable = i < availableBuildCount; + BuildTowerVisualInfo buildVisual = ResolveBuildTowerVisual(participantTowers, i, muzzleMap, bearingMap, baseMap); + _buildTowerVisualInfos[i] = buildVisual; + useCase.SetBuildAction( + i, + isBuildAvailable && buildActionFactory != null ? buildActionFactory.Invoke(i) : null, + GetBuildTowerCost(buildTowerCosts, i), + null, + isBuildAvailable, + buildVisual.BaseColor, + buildVisual.BearingColor, + buildVisual.MuzzleColor); + useCase.SetBuildVisible(i, isBuildAvailable); + } + + useCase.SetUpgradeAction(upgradeAction, Mathf.Max(0, upgradeCost)); + useCase.SetDestroyAction(destroyAction, Mathf.Max(0, destroyGain)); + } + + public BuildTowerVisualInfo GetBuildVisualInfo(int buildIndex) + { + if (buildIndex < 0 || buildIndex >= _buildTowerVisualInfos.Length) + { + return BuildTowerVisualInfo.Default; + } + + return _buildTowerVisualInfos[buildIndex]; + } + + private static int GetBuildTowerCost(int[] buildTowerCosts, int buildIndex) + { + if (buildTowerCosts == null || buildIndex < 0 || buildIndex >= buildTowerCosts.Length) + { + return 0; + } + + return Mathf.Max(0, buildTowerCosts[buildIndex]); + } + + private static BuildTowerVisualInfo ResolveBuildTowerVisual( + IReadOnlyList participantTowers, + int buildIndex, + IReadOnlyDictionary muzzleMap, + IReadOnlyDictionary bearingMap, + IReadOnlyDictionary baseMap) + { + if (participantTowers == null || buildIndex < 0 || buildIndex >= participantTowers.Count) + { + return BuildTowerVisualInfo.Default; + } + + TowerItemData tower = participantTowers[buildIndex]; + if (tower == null) + { + return BuildTowerVisualInfo.Default; + } + + Color muzzleColor = ResolveComponentColor(muzzleMap, tower.MuzzleComponentInstanceId); + Color bearingColor = ResolveComponentColor(bearingMap, tower.BearingComponentInstanceId); + Color baseColor = ResolveComponentColor(baseMap, tower.BaseComponentInstanceId); + return new BuildTowerVisualInfo(muzzleColor, bearingColor, baseColor); + } + + private static Color ResolveComponentColor(IReadOnlyDictionary componentMap, long componentId) + where TComp : TowerCompItemData + { + if (componentMap == null || componentId <= 0) + { + return Color.white; + } + + return componentMap.TryGetValue(componentId, out TComp component) && component != null + ? IconColorGenerator.GenerateForComponent(component) + : Color.white; + } + + private static Dictionary BuildComponentMap(IReadOnlyList items) + where TComp : TowerCompItemData + { + Dictionary map = new Dictionary(); + if (items == null) + { + return map; + } + + for (int i = 0; i < items.Count; i++) + { + TComp item = items[i]; + if (item == null || item.InstanceId <= 0) + { + continue; + } + + map[item.InstanceId] = item; + } + + return map; + } + } + + public readonly struct BuildTowerVisualInfo + { + public static BuildTowerVisualInfo Default => new BuildTowerVisualInfo(Color.white, Color.white, Color.white); + + public BuildTowerVisualInfo(Color muzzleColor, Color bearingColor, Color baseColor) + { + MuzzleColor = muzzleColor; + BearingColor = bearingColor; + BaseColor = baseColor; + } + + public Color MuzzleColor { get; } + public Color BearingColor { get; } + public Color BaseColor { get; } + } +} diff --git a/Assets/GameMain/Scripts/Entity/EntityLogic/MapEntity.cs b/Assets/GameMain/Scripts/Entity/EntityLogic/MapEntity.cs index d9af5dc..5778ebb 100644 --- a/Assets/GameMain/Scripts/Entity/EntityLogic/MapEntity.cs +++ b/Assets/GameMain/Scripts/Entity/EntityLogic/MapEntity.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using GeometryTD.CustomComponent; -using GeometryTD.CustomUtility; using GeometryTD.Map; using GeometryTD.Definition; using GeometryTD.Entity.EntityData; @@ -37,7 +36,7 @@ namespace GeometryTD.Entity private TowerPlacementService _towerPlacementService; private TowerSelectionPresenter _towerSelectionPresenter; - private readonly BuildTowerVisualInfo[] _buildTowerVisualInfos = new BuildTowerVisualInfo[4]; + private CombatSelectUseCaseConfigurator _combatSelectUseCaseConfigurator; public IReadOnlyList PathCells => _mapTopologyService != null ? _mapTopologyService.PathCells @@ -102,6 +101,7 @@ namespace GeometryTD.Entity } InitializeCombatSelectUseCase(); + InitializeCombatSelectUseCaseConfigurator(); InitializeCombatSelectInputService(); InitializeMapTopologyService(); InitializeTowerPlacementService(); @@ -127,7 +127,9 @@ namespace GeometryTD.Entity { HideCombatSelectForm(); _towerPlacementService?.HideAndClearAllPlacedTowers(); - ClearRuntimeData(); + ClearSelectionState(); + ClearTowerTracking(); + ClearMapTopology(); base.OnHide(isShutdown, userData); } @@ -139,7 +141,9 @@ namespace GeometryTD.Entity private void RefreshTiles() { - ClearRuntimeData(); + ClearMapTopology(); + ClearTowerTracking(); + ClearSelectionState(); if (_mapDataRefs == null) { @@ -161,10 +165,18 @@ namespace GeometryTD.Entity _mapTopologyService?.Refresh(tilemap, Spawners, House, name, _mapData != null ? _mapData.LevelId : 0); } - private void ClearRuntimeData() + private void ClearMapTopology() { _mapTopologyService?.Clear(); + } + + private void ClearTowerTracking() + { _towerPlacementService?.ClearTracking(); + } + + private void ClearSelectionState() + { _towerSelectionPresenter?.ClearSelectedObject(); } @@ -178,6 +190,14 @@ namespace GeometryTD.Entity GameEntry.UIRouter.BindUIUseCase(UIFormType.CombatSelectForm, _combatSelectFormUseCase); } + private void InitializeCombatSelectUseCaseConfigurator() + { + if (_combatSelectUseCaseConfigurator == null) + { + _combatSelectUseCaseConfigurator = new CombatSelectUseCaseConfigurator(); + } + } + private void InitializeTowerSelectionPresenter() { if (_towerSelectionPresenter == null) @@ -212,11 +232,6 @@ namespace GeometryTD.Entity private void ConfigureCombatSelectUseCase() { - if (_combatSelectFormUseCase == null) - { - return; - } - _combatSelectFormUseCase.SetCoinProvider(GetCurrentCoin); BackpackInventoryData inventorySnapshot = GameEntry.PlayerInventory != null ? GameEntry.PlayerInventory.GetInventorySnapshot() @@ -224,32 +239,18 @@ namespace GeometryTD.Entity IReadOnlyList participantTowers = GameEntry.PlayerInventory != null ? GameEntry.PlayerInventory.GetParticipantTowerSnapshot() : null; - Dictionary muzzleMap = BuildComponentMap(inventorySnapshot?.MuzzleComponents); - Dictionary bearingMap = BuildComponentMap(inventorySnapshot?.BearingComponents); - Dictionary baseMap = BuildComponentMap(inventorySnapshot?.BaseComponents); - - int currentBuildTowerCount = GetCurrentBuildTowerCount(); - for (int i = 0; i < 4; i++) - { - int buildIndex = i; - int buildCost = GetBuildTowerCost(buildIndex); - bool isBuildAvailable = buildIndex < currentBuildTowerCount; - BuildTowerVisualInfo buildVisual = ResolveBuildTowerVisual(participantTowers, buildIndex, muzzleMap, bearingMap, baseMap); - _buildTowerVisualInfos[buildIndex] = buildVisual; - _combatSelectFormUseCase.SetBuildAction( - buildIndex, - isBuildAvailable ? () => TryBuildTower(buildIndex) : (Func)null, - buildCost, - null, - isBuildAvailable, - buildVisual.BaseColor, - buildVisual.BearingColor, - buildVisual.MuzzleColor); - _combatSelectFormUseCase.SetBuildVisible(buildIndex, isBuildAvailable); - } - - _combatSelectFormUseCase.SetUpgradeAction(TryUpgradeTower, Mathf.Max(0, _upgradeCost)); - _combatSelectFormUseCase.SetDestroyAction(TryDestroyTower, Mathf.Max(0, _destroyGain)); + _combatSelectUseCaseConfigurator?.Configure( + _combatSelectFormUseCase, + GetCurrentCoin, + buildIndex => () => TryBuildTower(buildIndex), + TryUpgradeTower, + _upgradeCost, + TryDestroyTower, + _destroyGain, + _buildTowerCosts, + GetCurrentBuildTowerCount(), + inventorySnapshot, + participantTowers); } private void HandleCombatSelectInput() @@ -306,8 +307,8 @@ namespace GeometryTD.Entity return false; } - BuildTowerVisualInfo buildVisual = buildIndex >= 0 && buildIndex < _buildTowerVisualInfos.Length - ? _buildTowerVisualInfos[buildIndex] + BuildTowerVisualInfo buildVisual = _combatSelectUseCaseConfigurator != null + ? _combatSelectUseCaseConfigurator.GetBuildVisualInfo(buildIndex) : BuildTowerVisualInfo.Default; if (!_towerPlacementService.TryBuildTower(selectedFoundationCell, IsFoundationCell, buildIndex, _buildTowerCosts, @@ -375,16 +376,6 @@ namespace GeometryTD.Entity GameEntry.UIRouter.CloseUI(UIFormType.CombatSelectForm); } - private int GetBuildTowerCost(int buildIndex) - { - if (_buildTowerCosts == null || buildIndex < 0 || buildIndex >= _buildTowerCosts.Length) - { - return 0; - } - - return Mathf.Max(0, _buildTowerCosts[buildIndex]); - } - private static int GetCurrentBuildTowerCount() { if (GameEntry.CombatNode == null) @@ -395,67 +386,6 @@ namespace GeometryTD.Entity return Mathf.Clamp(GameEntry.CombatNode.CurrentBuildTowerCount, 0, 4); } - - private static BuildTowerVisualInfo ResolveBuildTowerVisual( - IReadOnlyList participantTowers, - int buildIndex, - IReadOnlyDictionary muzzleMap, - IReadOnlyDictionary bearingMap, - IReadOnlyDictionary baseMap) - { - if (participantTowers == null || buildIndex < 0 || buildIndex >= participantTowers.Count) - { - return BuildTowerVisualInfo.Default; - } - - TowerItemData tower = participantTowers[buildIndex]; - if (tower == null) - { - return BuildTowerVisualInfo.Default; - } - - Color muzzleColor = ResolveComponentColor(muzzleMap, tower.MuzzleComponentInstanceId); - Color bearingColor = ResolveComponentColor(bearingMap, tower.BearingComponentInstanceId); - Color baseColor = ResolveComponentColor(baseMap, tower.BaseComponentInstanceId); - return new BuildTowerVisualInfo(muzzleColor, bearingColor, baseColor); - } - - private static Color ResolveComponentColor(IReadOnlyDictionary componentMap, long componentId) - where TComp : TowerCompItemData - { - if (componentMap == null || componentId <= 0) - { - return Color.white; - } - - return componentMap.TryGetValue(componentId, out TComp component) && component != null - ? IconColorGenerator.GenerateForComponent(component) - : Color.white; - } - - private static Dictionary BuildComponentMap(IReadOnlyList items) - where TComp : TowerCompItemData - { - Dictionary map = new Dictionary(); - if (items == null) - { - return map; - } - - for (int i = 0; i < items.Count; i++) - { - TComp item = items[i]; - if (item == null || item.InstanceId <= 0) - { - continue; - } - - map[item.InstanceId] = item; - } - - return map; - } - private static bool TryConsumeCoin(int cost) { int requiredCoin = Mathf.Max(0, cost); @@ -477,22 +407,6 @@ namespace GeometryTD.Entity return GameEntry.CombatNode != null ? Mathf.Max(0, GameEntry.CombatNode.CurrentCoin) : 0; } - private readonly struct BuildTowerVisualInfo - { - public static BuildTowerVisualInfo Default => new BuildTowerVisualInfo(Color.white, Color.white, Color.white); - - public BuildTowerVisualInfo(Color muzzleColor, Color bearingColor, Color baseColor) - { - MuzzleColor = muzzleColor; - BearingColor = bearingColor; - BaseColor = baseColor; - } - - public Color MuzzleColor { get; } - public Color BearingColor { get; } - public Color BaseColor { get; } - } - private static void AddCoin(int coin) { int amount = Mathf.Max(0, coin); diff --git a/docs/CombatNodeArchitecture.md b/docs/CombatNodeArchitecture.md index a39a143..fb23b8b 100644 --- a/docs/CombatNodeArchitecture.md +++ b/docs/CombatNodeArchitecture.md @@ -1,127 +1,497 @@ -# CombatNode Architecture +# CombatNode 设计规范(开发约束) -最后更新:2026-03-02 +最后更新:2026-03-06 -## 1. 总览 -CombatNode 当前分为三层: -- `CombatNodeComponent`:入口与关卡数据装配。 -- `CombatScheduler`:战斗状态机与阶段推进,以及关卡内资源结算。 -- `EnemyManager`:敌人系统 Facade(对外接口保持稳定,内部由多个子服务协作)。 +## 1. 适用范围与目标 -本文重点记录 `EnemyManager` 与 `CombatScheduler` 的职责边界(尤其资源管理收口后的模型)。 +本文描述 `CombatNode` 域的后续开发标准。 -## 2. EnemyManager 相关类 +说明: +- 本文是“目标架构约束”,不要求当前代码已经完全达成。 +- 后续新增功能、重构、拆分类、review 职责边界时,以本文为准。 +- 如果当前实现与本文不一致,新增代码优先向本文收敛,而不是继续扩大旧结构。 + +核心目标: +- `CombatScheduler` 收敛为“状态机管理器”,不再继续堆积加载、结算、奖励选择等业务细节。 +- 战斗内资源收口到独立资源服务,由内部管理,不再由 `CombatNodeComponent` 直接持有真值。 +- `MapEntity` 通过 `MapData + Event` 获取战斗上下文,不反查 `CombatNode` 域内部状态。 + +--- + +## 2. 架构总览 + +### 2.1 CombatNodeComponent(入口 Facade) + +文件:`Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatNodeComponent.cs` + +长期职责: +- 读取并缓存 `DRLevel / DRLevelPhase / DRLevelSpawnEntry`。 +- 按主题筛选关卡。 +- 启动/停止 `CombatScheduler`。 +- 对外暴露只读运行时属性。 +- 提供少量用户入口,例如 `StartCombat`、`TryEndCombatByPlayer`。 + +长期不负责: +- 不直接持有 `Coin / Gold / BaseHp / Loot Backpack` 的真值。 +- 不直接缓存本局建塔属性快照。 +- 不直接发布战斗流程事件。 +- 不直接处理敌人掉落、结算、奖励选择、地图加载。 + +### 2.2 CombatScheduler(状态机管理器) + +文件:`Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/CombatScheduler.cs` + +长期职责: +- 持有共享运行时数据与共享服务实例。 +- 管理状态实例。 +- 提供统一的 `ChangeState(...)` 状态迁移入口。 +- 提供敌人事件的公共处理入口。 +- 作为状态机生命周期边界,统一做运行时重置。 + +长期不负责: +- 不直接硬编码加载流程。 +- 不直接硬编码结算流程。 +- 不直接硬编码奖励选择 UI 逻辑。 +- 不直接硬编码 `PhaseEndType` 结束条件。 + +推荐状态类命名: +- `CombatLoadingState` +- `CombatRunningPhaseState` +- `CombatWaitingForPhaseEndState` +- `CombatSettlementState` +- `CombatRewardSelectionState` +- `CombatFinishFormState` +- `CombatWaitingForReturnState` +- `CombatFailedState` + +实现约束: +- 上述状态类作为 `CombatScheduler` 的嵌套类实现。 +- 共享数据与共享服务统一放在 `CombatScheduler` 上。 +- 所有状态切换只能通过 `CombatScheduler.ChangeState(...)` 完成。 +- 状态类不能彼此直接操控。 + +### 2.3 EnemyManager(敌人域 Facade) -### 2.1 EnemyManager(Facade) 文件:`Assets/GameMain/Scripts/CustomComponent/CombatNode/EnemyManager/EnemyManager.cs` +长期职责: +- 对状态机提供统一敌人域接口。 +- 编排敌人子服务。 +- 暴露只读事实: + - `AliveEnemyCount` + - `IsPhaseSpawnCompleted` + - `HasAliveBoss` +- 在敌人死亡或到家时,通过公共入口向 `CombatScheduler` 上报: + - `OnEnemyDefeated(DREnemy enemy)` + - `OnEnemyReachedBase(DREnemy enemy)` + +长期不负责: +- 不直接给资源入账。 +- 不直接扣基地血量。 +- 不直接决定状态切换。 + +--- + +## 3. 状态机模型 + +### 3.1 状态列表(目标) + +- `Loading` +- `RunningPhase` +- `WaitingForPhaseEnd` +- `Settlement` +- `RewardSelection` +- `FinishForm` +- `WaitingForReturn` +- `Failed` + +说明: +- 正常结束流只有一条状态链: + - `Settlement -> RewardSelection(可选) -> FinishForm -> WaitingForReturn` +- 正常通关、玩家主动结束、基地血量归零都走同一条结束链。 +- `Failed` 仅用于异常失败,不用于“基地被击破”这类正常战斗失败。 + +### 3.2 CombatLoadingState + 职责: -- 对 `CombatScheduler` 提供统一接口:`OnInit / BeginPhase / OnUpdate / EndPhase / OnDestroy`。 -- 编排敌人域子服务,不承载具体业务细节。 -- 转发状态:`AliveEnemyCount`、`IsPhaseRunning`、`IsPhaseSpawnCompleted`。 -- 处理敌人实体事件(Show/Hide)后的结果上报: - - 击杀:上报 `coin/gold` 到 `CombatScheduler.OnEnemyDefeatedRewardResolved(...)` - - 到家:上报 `baseDamage` 到 `CombatScheduler.OnEnemyReachedBase(...)` +- 通过 `CombatLoadSession` 执行地图与基础战斗 UI 加载。 +- 从局内资源管理器读取本局快照。 +- 组装 `MapData` 并发起 `ShowEntity(MapEntity)`。 + +约束: +- 只负责加载,不负责初始化局内资源。 +- 局内资源必须在进入状态机前初始化完成。 + +### 3.3 CombatRunningPhaseState + +职责: +- 执行当前 `DRLevelPhase` 的行为。 +- 推进 `SpawnEntry` 时序与出怪。 +- 管理 `EnemySpawnDirector` 的阶段级初始化与重置。 +- 在新 phase 开始时发布: + - `CombatProcessEventArgs` + - `CombatEnemyHpRateChangedEventArgs` + +退出条件: +- 当前 phase 的所有 `SpawnEntry` 已执行完毕时,进入 `WaitingForPhaseEnd`。 +- 若共享“结束战斗请求标记”已置位,也可结束当前运行态并转入正常结束链。 不负责: -- 不直接维护刷怪时间轴。 -- 不直接维护出生点缓存与路径查询。 -- 不直接维护敌人追踪结构。 -- 不直接维护掉落池抽样与背包库存。 -- 不直接维护敌人配置兜底和血量倍率算法。 +- 不根据 `PhaseEndType` 判断 phase 是否真正结束。 +- 不直接根据 `BaseHp` 或敌人死亡事件切状态。 + +### 3.4 CombatWaitingForPhaseEndState + +职责: +- 不再生成新敌人。 +- 根据 `PhaseEndType` 判断当前 phase 是否结束。 + +约束: +- `PhaseEndType` 的判断由独立判定服务负责,不在状态内硬编码。 +- 每种 `PhaseEndType` 对应一个实现类。 +- 该判定服务为本状态专用,不作为全局共享服务常驻在 `CombatScheduler` 上。 + +### 3.5 CombatSettlementState + +职责: +- 进入时统一构造结算上下文。 +- 根据共享资源状态完成结算修正。 +- 决定后续进入 `RewardSelection` 还是 `FinishForm`。 + +负责的逻辑包括: +- 基地血量奖励/惩罚。 +- 满血奖励选择的准入判断。 +- 生成最终展示摘要。 +- 准备待合并的结算背包快照。 + +约束: +- 不依赖单独的 `CombatEndReason` 字段。 +- `BaseHp <= 0` 表示基地被击破。 +- 正常通关与玩家主动结束在结算产出上不区分原因。 + +### 3.6 CombatRewardSelectionState + +职责: +- 绑定、配置、打开、关闭 `RewardSelectForm`。 +- 处理奖励选择过程。 +- 将奖励选择结果写入“结束状态链持有的结算上下文”。 + +约束: +- 不重新判断“是否应该出现奖励选择”。 +- 只处理选择过程本身。 + +### 3.7 CombatFinishFormState + +职责: +- 绑定、配置、打开、关闭 `CombatFinishForm`。 +- 读取结算上下文并展示最终结算结果。 + +### 3.8 CombatWaitingForReturnState + +职责: +- 等待玩家从结算返回。 +- 完成地图与战斗基础 UI 清理。 +- 完成正常退出收尾。 +- 在整场战斗真正退出时发布 `NodeCompleteEventArgs`。 + +### 3.9 CombatFailedState + +职责: +- 表示异常失败。 +- 保存并展示错误信息。 +- 执行异常收尾与剩余资源回收。 + +约束: +- `Failed` 只处理异常路径。 +- “基地血量为 0”不进入 `Failed`。 + +--- + +## 4. 共享服务与推荐命名 + +### 4.1 CombatLoadSession + +文件:`Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/CombatLoadSession.cs` + +长期定位: +- 长期保留的独立加载服务。 +- 专门负责地图与战斗内基础 UI 的加载/清理。 + +职责: +- 加载地图实体。 +- 打开/关闭 `CombatInfoForm`。 +- 跟踪加载成功/失败状态。 +- 对外提供 `CurrentMap` 与 `IsReady`。 + +### 4.2 PhaseLoopRuntime + +文件:`Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/PhaseLoopRuntime.cs` + +长期定位: +- 长期保留的独立 phase runtime 服务。 + +职责: +- 维护当前 `DRLevelPhase`。 +- 维护 `DisplayPhaseIndex`、`PhaseCount`。 +- 维护统一的 phase 时间基准,例如 `phaseElapsedTime` 或 `phaseStartTime`。 +- 负责进入下一 phase。 +- 持有统一“请求结束战斗”标记。 + +约束: +- 只做 phase 运行时数据管理,不直接切状态。 + +### 4.3 CombatInRunResourceManager(推荐命名) + +当前相关文件:`Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/CombatResourceManager.cs` + +目标职责: +- 持有本局 `Coin` 真值。 +- 持有本局累计 `Gold` 真值。 +- 持有本局 `BaseHp` 真值。 +- 持有本局战利品背包。 +- 持有本局建塔属性快照。 +- 提供只读快照给结束状态链与加载状态使用。 +- 发布资源变化事件: + - `CombatCoinChangedEventArgs` + - `CombatBaseHpChangedEventArgs` + +初始化约束: +- 在进入状态机前完成初始化。 +- 由内部从 `PlayerInventory` 获取并缓存本局建塔快照。 + +事件约束: +- `Coin / BaseHp` 变化事件同时携带“当前值”和“变化量”。 +- `Gold` 只是结算累计值,不要求战斗内实时事件驱动。 + +### 4.4 EnemyDropResolver(推荐命名) + +当前相关文件:`Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/CombatResourceManager.cs` + +目标职责: +- 只负责敌人死亡后的掉落判定。 +- 输入: + - `DREnemy` + - 当前阶段索引 + - 当前主题或关卡上下文 +- 输出: + - 掉落结果对象(`coin / gold / loot`) + +约束: +- 不直接修改资源状态。 +- 不直接读取 `CombatNodeComponent`、`MapEntity`、`EnemyManager` 内部状态。 + +### 4.5 IPhaseEndCondition(推荐命名) + +目标职责: +- 作为 `PhaseEndType` 判定接口。 +- 每种 `PhaseEndType` 对应一个实现类。 + +只读输入: +- 当前 `DRLevelPhase` +- phase 时间信息 +- `AliveEnemyCount` +- `IsPhaseSpawnCompleted` +- `HasAliveBoss` + +输出: +- `bool ShouldExit` + +约束: +- 不直接切状态。 +- 不直接发事件。 +- 不直接改资源。 + +--- + +## 5. EnemyManager 子服务边界 + +### 5.1 EnemySpawnDirector -### 2.2 EnemySpawnDirector(刷怪时序) 文件:`Assets/GameMain/Scripts/CustomComponent/CombatNode/EnemyManager/EnemySpawnDirector.cs` 职责: -- 管理 `DRLevelSpawnEntry` 的运行时实例(`SpawnEntryRuntime`)。 -- 按时间推进 `Stream / Burst / Boss` 生成逻辑。 -- 输出“当前应生成多少敌人”,通过回调交给 `EnemyManager.SpawnEnemies` 执行。 -- 维护阶段刷怪状态:`IsPhaseRunning`、`IsPhaseSpawnCompleted`。 +- 长期保留为独立服务。 +- 基于 `spawnEntries + phase time` 计算当前应执行的刷怪行为。 +- 提供“当前 phase 的 `SpawnEntry` 是否已全部执行完”的事实。 + +生命周期: +- 由 `CombatRunningPhaseState` 在状态进入/退出时初始化与重置。 + +### 5.2 SpawnerResolver -### 2.3 SpawnerResolver(出生点与路径) 文件:`Assets/GameMain/Scripts/CustomComponent/CombatNode/EnemyManager/SpawnerResolver.cs` 职责: - 缓存当前地图可用 `Spawner`。 -- 支持 `SpawnOrder` 映射与 fallback 轮询。 -- 对外提供 `TryResolveSpawnPath(...)`,返回世界坐标路径点。 -- 在地图切换时刷新缓存,避免每次刷怪全量扫描。 +- 提供出生点与路径解析。 + +### 5.3 EnemyLifecycleTracker -### 2.4 EnemyLifecycleTracker(敌人生命周期追踪) 文件:`Assets/GameMain/Scripts/CustomComponent/CombatNode/EnemyManager/EnemyLifecycleTracker.cs` 职责: -- 追踪本局敌人 `entityId -> DREnemy`。 -- 维护 `AliveEnemyCount`。 -- 处理 `ShowSuccess / ShowFailure / HideComplete` 对追踪状态的变更。 -- 提供批量导出 tracked ids,用于 `CleanupTrackedEnemies` 清场。 +- 维护 `AliveEnemyCount` 真值。 +- 维护 `HasAliveBoss` 真值。 +- 追踪本局 tracked 敌人。 +- 导出 tracked ids 供清场使用。 + +Boss 识别规则: +- Boss 身份由 `DRLevelSpawnEntry.EntryType == Boss` 决定。 +- 不由 `DREnemy` 自身类型决定。 + +### 5.4 EnemyConfigService -### 2.5 EnemyConfigService(敌人配置与倍率) 文件:`Assets/GameMain/Scripts/CustomComponent/CombatNode/EnemyManager/EnemyConfigService.cs` 职责: -- 读取 `DREnemy`,处理默认配置兜底。 -- 计算循环周目下的基础血量倍率(按 `displayPhaseIndex / phaseCount` 推导 loop)。 -- 缓存数据表引用并在 `Reset` 时清理。 +- 读取 `DREnemy`。 +- 处理默认配置兜底。 +- 计算循环周目下的基础血量倍率。 -## 3. CombatScheduler 资源收口 +--- -### 3.1 CombatResourceManager(关卡内资源统一管理) -文件:`Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/CombatResourceManager.cs` +## 6. 事件与数据流规范 -职责: -- 统一维护关卡内资源状态: - - `GainedCoin`、`GainedGold` - - `BackpackInventoryData`(结算背包快照) -- 处理击杀奖励入账:`AddEnemyDefeatedReward(...)` -- 处理局外掉落抽样与物品构建:`TryRollOutGameItemDrop(...)` -- 对结算 UI 提供只读快照:`GetRewardInventorySnapshot()` +### 6.1 MapEntity 与 Combat 域解耦 -### 3.2 CombatScheduler 与资源类关系 -文件:`Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/CombatScheduler.cs` +必须保持: +- `MapEntity` 不直接查询 `CombatNodeComponent` 的运行时资源字段。 +- 战斗初始上下文通过 `MapData` 注入。 +- `Coin` 初值通过 `MapData` 传入。 +- 后续 `Coin` 变化通过 `CombatCoinChangedEventArgs` 同步。 +- `TowerStatsData` 等本局不变量直接放进 `MapData`。 +- `MapEntity` 不反查 Combat 域内部服务。 -当前模型: -- `CombatScheduler` 持有 `_combatResourceManager`。 -- `GainedCoin/GainedGold` 属性透传资源类。 -- `OnEnemyDefeatedRewardResolved(...)` 统一触发: - - 货币入账 - - 关卡内掉落判定 -- `EnterFinishFlow(...)` 从资源类取 `BackpackInventoryData` 作为结算数据。 +`MapData` 组装规则: +- 由 `CombatLoadingState` 从局内资源管理器读取快照。 +- 由 `CombatLoadingState` 打包成 `MapData` 后再 `ShowEntity(MapEntity)`。 -## 4. 协作流程(简版) -1. `CombatScheduler.BeginNextPhase()` 调 `EnemyManager.BeginPhase(...)`。 -2. `EnemyManager` 刷新 `SpawnerResolver` 缓存,并通知 `EnemySpawnDirector.BeginPhase(...)`。 -3. 每帧 `EnemyManager.OnUpdate(...)`: - - `SpawnerResolver.RefreshCache(...)` - - `EnemySpawnDirector.OnUpdate(..., SpawnEnemies)` -4. `SpawnEnemies(...)` 执行实际生成: - - `SpawnerResolver.TryResolveSpawnPath(...)` - - `EnemyConfigService.GetEnemyConfig(...)` - - `EnemyConfigService.ResolveScaledEnemyBaseHp(...)` - - `EnemyLifecycleTracker.TrackEnemy(...)` - - `GameEntry.Entity.ShowEnemy(...)` -5. 敌人回收时 `EnemyManager.OnHideEntityComplete(...)`: - - `EnemyLifecycleTracker.TryHandleHideComplete(...)` - - 若击杀:上报 `coin/gold` 给 `CombatScheduler.OnEnemyDefeatedRewardResolved(...)` - - 若到家:上报 `baseDamage` 给 `CombatScheduler.OnEnemyReachedBase(...)` -6. `CombatScheduler.OnEnemyDefeatedRewardResolved(...)`: - - `CombatResourceManager.AddEnemyDefeatedReward(...)` - - `CombatResourceManager.TryRollOutGameItemDrop(...)` -7. `CombatScheduler.EnterFinishFlow(...)`: - - 读取 `CombatResourceManager.GetRewardInventorySnapshot()` - - 打开结算 UI +### 6.2 敌人事件处理 -## 5. 关键不变量 -- 存活敌人数以 `EnemyLifecycleTracker.AliveEnemyCount` 为唯一真值来源。 -- 刷怪阶段状态以 `EnemySpawnDirector` 为唯一真值来源。 -- 关卡内资源以 `CombatResourceManager` 为唯一真值来源。 -- `EnemyManager` 只做敌人域编排与结果上报,不再持有奖励库存。 -- 清场必须只作用于本局 tracked 敌人,避免误伤其他实体。 +统一边界: +- `EnemyManager` 只上报: + - `OnEnemyDefeated(DREnemy enemy)` + - `OnEnemyReachedBase(DREnemy enemy)` +- `CombatScheduler` 公共层负责处理敌人事件的通用副作用: + - 击杀:调用 `EnemyDropResolver`,再调用局内资源管理器入账。 + - 到家:调用局内资源管理器扣减 `BaseHp`。 -## 6. 维护建议 -- 新增刷怪类型:优先改 `EnemySpawnDirector`。 -- 新增路径/出生规则:优先改 `SpawnerResolver`。 -- 新增敌人追踪策略:优先改 `EnemyLifecycleTracker`。 -- 新增敌人配置兜底或倍率策略:优先改 `EnemyConfigService`。 -- 新增货币/掉落/结算背包规则:优先改 `CombatResourceManager`。 +约束: +- 敌人事件入口不直接调用 `ChangeState(...)`。 +- `BaseHp <= 0` 的判断由当前状态在 `OnUpdate` 中处理。 + +### 6.3 战斗流程事件 + +发布边界: +- 资源变化事件由局内资源管理器发布。 +- 流程/阶段事件由状态机或具体状态发布。 + +发布时间: +- `NodeEnterEventArgs`:`Loading` 完成并正式进入首个 `RunningPhase` 时。 +- `CombatProcessEventArgs`:新 phase 的 `RunningPhase.OnEnter`。 +- `CombatEnemyHpRateChangedEventArgs`:与 `CombatProcessEventArgs` 同时发布。 +- `NodeCompleteEventArgs`:`WaitingForReturn` 完成清理、整场战斗真正退出时。 + +--- + +## 7. 结束链与结算上下文 + +### 7.1 统一结束链 + +正常结束统一走: +- `Settlement` +- `RewardSelection`(可选) +- `FinishForm` +- `WaitingForReturn` + +### 7.2 结算上下文 + +归属: +- 作为 `CombatScheduler` 上的共享字段存在。 +- `Settlement` 在 `OnEnter` 时统一构造。 +- `RewardSelection` 只追加奖励结果。 +- `FinishForm` 与 `WaitingForReturn` 只读取。 + +最小字段集合: +- 最终结算的 `Gold/Coin` 结果 +- 待合并的背包快照 +- `BaseHp` 结算结果 +- 是否进入过奖励选择 +- `FinishForm` 所需摘要数据 + +奖励选择约束: +- 满血奖励选择结果只写入结算上下文。 +- 不直接写入局内资源管理器。 +- 最终由结束状态链统一合并到玩家背包。 + +--- + +## 8. 核心不变量(必须保持) + +1. `CombatScheduler` 只做状态机管理与共享运行时收口,不继续吸收具体业务细节。 +2. `CombatNodeComponent` 不再持有战斗内资源真值。 +3. 局内 `Coin / Gold / BaseHp / Loot Backpack / BuildTowerSnapshots` 以 `CombatInRunResourceManager` 为唯一真值来源。 +4. 敌人死亡掉落判定以 `EnemyDropResolver` 为唯一判定入口。 +5. 存活敌人数与 `HasAliveBoss` 以 `EnemyLifecycleTracker` 为唯一真值来源。 +6. Phase 运行时信息与统一结束标记以 `PhaseLoopRuntime` 为唯一真值来源。 +7. `PhaseEndType` 的退出条件以 `IPhaseEndCondition` 实现类为唯一判定入口。 +8. 状态切换只能通过 `CombatScheduler.ChangeState(...)` 完成。 +9. 敌人事件处理入口不直接切状态,状态只能在自己的 `OnUpdate` 中决定迁移。 +10. `MapEntity` 通过 `MapData + Event` 获取战斗上下文,不反查 Combat 域内部运行时。 + +--- + +## 9. 清理职责 + +- 敌人清理:`EnemyManager`,且只清理本局 tracked 敌人。 +- 地图与战斗基础 UI 清理:`CombatLoadSession`。 +- 结算/奖励 UI 清理:结束状态链或 `Failed` 状态。 +- 运行时数据重置:`CombatScheduler` 在状态机生命周期边界统一执行。 + +--- + +## 10. 扩展开发规范 + +### 10.1 新增刷怪类型或 SpawnEntry 行为 + +优先改 `EnemySpawnDirector`,不要把时序细节塞进 `CombatRunningPhaseState`。 + +### 10.2 新增 Phase 结束条件 + +新增 `IPhaseEndCondition` 实现类,不要在 `CombatWaitingForPhaseEndState` 里写大分支。 + +### 10.3 新增敌人掉落规则 + +优先改 `EnemyDropResolver`,不要在 `EnemyManager` 或状态类里直接计算掉落。 + +### 10.4 新增战斗内资源或建塔快照规则 + +优先改 `CombatInRunResourceManager`,不要回流到 `CombatNodeComponent`。 + +### 10.5 新增地图/战斗基础 UI 加载规则 + +优先改 `CombatLoadSession` 或 `CombatLoadingState`,不要把加载细节塞回 `CombatScheduler` 本体。 + +### 10.6 新增强化结算、奖励选择、结算 UI 逻辑 + +优先改结束状态链: +- `CombatSettlementState` +- `CombatRewardSelectionState` +- `CombatFinishFormState` +- `CombatWaitingForReturnState` + +### 10.7 新增战斗流程事件 + +优先由具体状态或局内资源管理器发布,不要回流到 `CombatNodeComponent`。 + +--- + +## 11. 代码变更检查清单(PR 自检) + +1. 新逻辑是否落在正确的状态或服务,而不是继续堆进 `CombatScheduler` 本体? +2. `CombatNodeComponent` 是否仍然保持为轻量入口 Facade? +3. 是否破坏了局内资源、掉落判定、phase runtime、phase end 判定的唯一真值来源? +4. 敌人事件处理是否仍然只做公共副作用,而不直接切状态? +5. 状态迁移是否仍然统一走 `ChangeState(...)`? +6. `MapEntity` 是否仍然只通过 `MapData + Event` 获取战斗上下文? +7. 清理是否仍按“敌人 / 地图基础 UI / 结算 UI / 运行时数据”分工执行? diff --git a/docs/MapEntityArchitecture.md b/docs/MapEntityArchitecture.md index e173252..adcc0fc 100644 --- a/docs/MapEntityArchitecture.md +++ b/docs/MapEntityArchitecture.md @@ -1,6 +1,6 @@ # MapEntity 设计规范(开发约束) -最后更新:2026-03-02 +最后更新:2026-03-06 ## 1. 目标与边界 @@ -14,6 +14,7 @@ - 防御塔映射字典的增删改查细节。 - 选中状态与攻击范围显示细节。 - 鼠标拾取与 `CombatSelectFormUserData` 组装细节。 +- 战斗选择 UI 的库存快照解析、颜色映射与 Build 选项配置细节。 --- @@ -28,10 +29,11 @@ - 子服务初始化与清理 - UI 用例绑定(`CombatSelectFormUseCase`) - 将输入结果分发到选择器/建造器 +- 收集运行时上下文并委托给专用服务配置战斗选择 UI ### 2.2 MapTopologyService(地图拓扑层) -当前文件:`Assets/GameMain/Scripts/Scene/Map/TowerPlacementService.cs`(同文件内) +文件:`Assets/GameMain/Scripts/Scene/Map/MapTopologyService.cs` 职责: - 扫描 Tilemap,构建 `PathCells` / `FoundationCells` @@ -58,7 +60,21 @@ - 只读上下文,不改变游戏状态。 - Foundation/Tower 点击时,UI 定位由 Cell 中心决定(稳定定位)。 -### 2.4 TowerPlacementService(塔部署层) +### 2.4 CombatSelectUseCaseConfigurator(战斗选择 UI 配置层) + +文件:`Assets/GameMain/Scripts/Entity/EntityLogic/CombatSelectUseCaseConfigurator.cs` + +职责: +- 读取库存快照与参战塔快照 +- 构建组件映射与塔外观颜色 +- 配置 `CombatSelectFormUseCase` 的 Build/Upgrade/Destroy action 与显示参数 +- 缓存当前 Build 槽位对应的视觉信息,供建塔流程复用 + +约束: +- 只负责 UI 选项配置与只读数据组装,不直接改变战斗状态。 +- 不处理鼠标输入、地图拓扑、塔实体生命周期。 + +### 2.5 TowerPlacementService(塔部署层) 当前文件:`Assets/GameMain/Scripts/Scene/Map/TowerPlacementService.cs` @@ -73,13 +89,13 @@ 约束: - 仅处理塔生命周期与映射,不处理选中态和 UI。 -### 2.5 TowerSelectionPresenter(选择展示层) +### 2.6 TowerSelectionPresenter(选择展示层) 文件:`Assets/GameMain/Scripts/Scene/Map/TowerSelectionPresenter.cs` 职责: - 维护当前选中对象 -- 根据选中状态切换攻击范围显示(通过 `DefenseTowerEntity.SetAttackRangeVisible`) +- 根据选中状态切换攻击范围显示(通过 `TowerEntity.SetAttackRangeVisible`) 约束: - 不做建造/升级/销毁。 @@ -88,12 +104,14 @@ ## 3. 运行时主流程(简版) -1. `MapEntity.OnInit` 初始化 4 个服务: +1. `MapEntity.OnInit` 初始化并绑定 6 个对象: - `MapTopologyService` - `CombatSelectInputService` + - `CombatSelectUseCaseConfigurator` - `TowerPlacementService` - `TowerSelectionPresenter` -2. `MapEntity.OnShow` 刷新地图拓扑,配置 UI action。 + - `CombatSelectFormUseCase` +2. `MapEntity.OnShow` 刷新地图拓扑,配置 UI action 与 Build 视觉数据。 3. 每帧 `OnUpdate`: - 采集输入(InputService) - 更新选中对象(SelectionPresenter) @@ -103,8 +121,9 @@ - 通过 SelectionPresenter 同步选中和范围显示 5. `OnHide`: - 关闭 UI - - 清理塔实体 - - 清理拓扑/选择/映射运行时状态 + - 隐藏并清理塔实体 + - 清理选择态与塔映射运行时状态 + - 清理拓扑缓存 --- @@ -114,7 +133,7 @@ 2. `TowerPlacementService` 是塔映射状态的唯一写入口。 3. `MapTopologyService` 是 Path/Foundation 数据的唯一来源。 4. “当前可见攻击范围”同一时刻最多一个塔。 -5. 清场顺序固定:先隐藏塔,再清空映射与选择,再清理拓扑缓存。 +5. 清场顺序固定:先关闭 UI,再隐藏塔,再清空选择与映射,再清理拓扑缓存。 --- @@ -132,7 +151,11 @@ 优先改 `CombatSelectInputService`。 -### 5.4 新增选中表现(特效、描边、信息面板联动) +### 5.4 新增 Build 选项展示、塔外观颜色或库存驱动 UI 配置 + +优先改 `CombatSelectUseCaseConfigurator`。 + +### 5.5 新增选中表现(特效、描边、信息面板联动) 优先改 `TowerSelectionPresenter`。