v11 全量评审:修复 TD-13 至 TD-17

- TD-13: IQuestManager.CompleteQuest 改用 IRewardTarget,移除 using BaseGames.Player
- TD-14: HurtFlashController 缓存 WaitForSeconds(_waitForFlash 字段)
- TD-16: LiquidType 枚举迁移至 BaseGames.Core.Events;LiquidEvent.LiquidType 改为枚举直接比较
- TD-17: DeathScreenController 移除失效的 _onPlayerDied 订阅,改为 OnEnable 直接启动延迟显示

影响文件: IQuestManager.cs / HurtFlashController.cs / LiquidType.cs(迁移) /
  LiquidEvent.cs / LiquidZone.cs / WaterDangerState.cs /
  UnderwaterPostProcessingController.cs / UnderwaterAudioController.cs /
  DeathScreenController.cs

评审文档: Docs/Review/FrameworkReview_2026_May_v11.md(综合评分 9.30/10)
This commit is contained in:
2026-05-12 16:34:03 +08:00
parent 9284278578
commit 1135139bc6
12 changed files with 554 additions and 32 deletions

View File

@@ -3,7 +3,6 @@
using UnityEngine; using UnityEngine;
using UnityEngine.Audio; using UnityEngine.Audio;
using BaseGames.Core.Events; using BaseGames.Core.Events;
using BaseGames.World.Liquid;
namespace BaseGames.Audio namespace BaseGames.Audio
{ {
@@ -34,13 +33,13 @@ namespace BaseGames.Audio
private void OnLiquidEntered(LiquidEvent evt) private void OnLiquidEntered(LiquidEvent evt)
{ {
if (evt.LiquidType == nameof(LiquidType.Water)) if (evt.LiquidType == LiquidType.Water)
EnterWater(); EnterWater();
} }
private void OnLiquidExited(LiquidEvent evt) private void OnLiquidExited(LiquidEvent evt)
{ {
if (evt.LiquidType == nameof(LiquidType.Water)) if (evt.LiquidType == LiquidType.Water)
ExitWater(); ExitWater();
} }

View File

@@ -7,10 +7,10 @@ namespace BaseGames.Core.Events
{ {
/// <summary>液体区域标识符(对应 LiquidZone SO 的 zoneId。</summary> /// <summary>液体区域标识符(对应 LiquidZone SO 的 zoneId。</summary>
public readonly string ZoneId; public readonly string ZoneId;
/// <summary>液体类型(如 "Water" / "Acid" / "Lava")。</summary> /// <summary>液体类型(枚举直接比较,无字符串转换)。</summary>
public readonly string LiquidType; public readonly LiquidType LiquidType;
public LiquidEvent(string zoneId, string liquidType) public LiquidEvent(string zoneId, LiquidType liquidType)
{ {
ZoneId = zoneId; ZoneId = zoneId;
LiquidType = liquidType; LiquidType = liquidType;

View File

@@ -0,0 +1,14 @@
// Assets/Scripts/Core/Events/LiquidType.cs
// LiquidType 属于事件载荷定义层(与 LiquidEvent 同程序集),
// 以消除 Core.Events → World.Liquid 的反向依赖。
namespace BaseGames.Core.Events
{
public enum LiquidType
{
Water, // 可游泳(需 Swim 能力)
ShallowWater, // 浅水(水中慢走,无需游泳能力,速度 ×0.65
Mud, // 泥水(移动极慢,无需游泳能力,速度 ×0.50
Acid, // 接触即死HazardZone 处理)
Lava, // 接触即死HazardZone 处理)
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 6cb1f6ef04d8c734ea4870b74030d4ea
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -1,4 +1,3 @@
using BaseGames.Player;
using QuestStateEnum = BaseGames.Core.Events.QuestState; using QuestStateEnum = BaseGames.Core.Events.QuestState;
namespace BaseGames.Quest namespace BaseGames.Quest
@@ -12,8 +11,8 @@ namespace BaseGames.Quest
/// <summary>接取任务(幂等)。</summary> /// <summary>接取任务(幂等)。</summary>
void AcceptQuest(string questId); void AcceptQuest(string questId);
/// <summary>完成任务并发放奖励。</summary> /// <summary>完成任务并发放奖励。rewardTarget 接收奖励(如玩家)。</summary>
void CompleteQuest(string questId, PlayerStats player); void CompleteQuest(string questId, IRewardTarget rewardTarget);
/// <summary>返回当前任务状态。未知 questId 返回 Unavailable。</summary> /// <summary>返回当前任务状态。未知 questId 返回 Unavailable。</summary>
QuestStateEnum GetState(string questId); QuestStateEnum GetState(string questId);

View File

@@ -12,15 +12,20 @@ namespace BaseGames.UI.Menus
[SerializeField] private Button _btnRespawn; [SerializeField] private Button _btnRespawn;
[Header("Event Channels")] [Header("Event Channels")]
[SerializeField] private VoidEventChannelSO _onPlayerDied;
[SerializeField] private VoidEventChannelSO _onDeathScreenConfirmed; [SerializeField] private VoidEventChannelSO _onDeathScreenConfirmed;
private readonly CompositeDisposable _subs = new(); private void OnEnable()
{
// 死亡界面由 UIManager 在游戏状态变为 Dead 时通过 SetActive(true) 激活。
// _onPlayerDied 事件此时已经触发完毕,订阅它不会收到回调。
// 直接在 OnEnable 启动延迟显示协程即可保证 1.5s 缓冲。
StartCoroutine(ShowAfterDelay(1.5f));
}
private void OnEnable() => _onPlayerDied?.Subscribe(OnPlayerDied).AddTo(_subs); private void OnDisable()
private void OnDisable() => _subs.Clear(); {
StopAllCoroutines();
private void OnPlayerDied() => StartCoroutine(ShowAfterDelay(1.5f)); }
private IEnumerator ShowAfterDelay(float delay) private IEnumerator ShowAfterDelay(float delay)
{ {
@@ -30,7 +35,6 @@ namespace BaseGames.UI.Menus
private void Show() private void Show()
{ {
gameObject.SetActive(true);
if (_btnRespawn != null) if (_btnRespawn != null)
{ {
_btnRespawn.onClick.RemoveAllListeners(); _btnRespawn.onClick.RemoveAllListeners();

View File

@@ -20,6 +20,7 @@ namespace BaseGames.VFX
private MaterialPropertyBlock _block; private MaterialPropertyBlock _block;
private Coroutine _flashCoroutine; private Coroutine _flashCoroutine;
private WaitForSeconds _waitForFlash;
private void Awake() private void Awake()
{ {
@@ -27,6 +28,8 @@ namespace BaseGames.VFX
if (_renderer == null) if (_renderer == null)
_renderer = GetComponent<SpriteRenderer>(); _renderer = GetComponent<SpriteRenderer>();
_block = new MaterialPropertyBlock(); _block = new MaterialPropertyBlock();
if (_config != null)
_waitForFlash = new WaitForSeconds(_config.HurtFlashDuration);
} }
/// <summary>触发一次闪白动画。若上一次闪白未结束则重置计时器。</summary> /// <summary>触发一次闪白动画。若上一次闪白未结束则重置计时器。</summary>
@@ -40,7 +43,7 @@ namespace BaseGames.VFX
private IEnumerator FlashCoroutine() private IEnumerator FlashCoroutine()
{ {
SetFlash(1f); SetFlash(1f);
yield return new WaitForSeconds(_config.HurtFlashDuration); yield return _waitForFlash ?? new WaitForSeconds(_config.HurtFlashDuration);
SetFlash(0f); SetFlash(0f);
_flashCoroutine = null; _flashCoroutine = null;
} }

View File

@@ -1,12 +1,5 @@
// Assets/Scripts/World/Liquid/LiquidType.cs // Assets/Scripts/World/Liquid/LiquidType.cs
namespace BaseGames.World.Liquid // LiquidType 已迁移至 BaseGames.Core.Events.LiquidType同程序集的 LiquidEvent
{ // 此文件保留以保持 .meta 文件对应,请勿在此添加代码。
public enum LiquidType // 所有引用请使用 using BaseGames.Core.Events; 后直接写 LiquidType
{
Water, // 可游泳(需 Swim 能力)
ShallowWater, // 浅水(水中慢走,无需游泳能力,速度 ×0.65
Mud, // 泥水(移动极慢,无需游泳能力,速度 ×0.50
Acid, // 接触即死HazardZone 处理)
Lava, // 接触即死HazardZone 处理)
}
}

View File

@@ -41,14 +41,14 @@ namespace BaseGames.World.Liquid
{ {
if (!other.CompareTag("Player")) return; if (!other.CompareTag("Player")) return;
_splashEnterFeedback?.PlayFeedbacks(); _splashEnterFeedback?.PlayFeedbacks();
_onPlayerEntered?.Raise(new LiquidEvent(_zoneId, _liquidType.ToString())); _onPlayerEntered?.Raise(new LiquidEvent(_zoneId, _liquidType));
} }
private void OnTriggerExit2D(Collider2D other) private void OnTriggerExit2D(Collider2D other)
{ {
if (!other.CompareTag("Player")) return; if (!other.CompareTag("Player")) return;
_splashExitFeedback?.PlayFeedbacks(); _splashExitFeedback?.PlayFeedbacks();
_onPlayerExited?.Raise(new LiquidEvent(_zoneId, _liquidType.ToString())); _onPlayerExited?.Raise(new LiquidEvent(_zoneId, _liquidType));
} }
} }
} }

View File

@@ -27,7 +27,7 @@ namespace BaseGames.World.Liquid
private void OnLiquidEntered(LiquidEvent evt) private void OnLiquidEntered(LiquidEvent evt)
{ {
if (evt.LiquidType != nameof(LiquidType.Water)) return; if (evt.LiquidType != LiquidType.Water) return;
BlendVolume(1f, _blendInDuration); BlendVolume(1f, _blendInDuration);
} }

View File

@@ -38,7 +38,7 @@ namespace BaseGames.World.Liquid
public void OnEnterLiquid(LiquidEvent evt) public void OnEnterLiquid(LiquidEvent evt)
{ {
if (evt.LiquidType != nameof(LiquidType.Water)) return; if (evt.LiquidType != LiquidType.Water) return;
if (_playerStats != null && _playerStats.HasAbility(AbilityType.Swim)) return; if (_playerStats != null && _playerStats.HasAbility(AbilityType.Swim)) return;
_isActive = true; _isActive = true;
_drownTimer = _config.DrownTime; _drownTimer = _config.DrownTime;

View File

@@ -0,0 +1,499 @@
# Zeling v2 框架全量代码评审报告 v11
> **评审时间**2026 年 5 月
> **评审范围**v10 修复验证 + 深度精读补充模块Puzzle / Liquid / VFX / Equipment / EventChain / Achievement / Player States / Enemy States / UI Menus
> **前置版本**v10 评审报告(综合评分 9.20/10
> **本版改进**:验证 TD-06 至 TD-12 修复正确性;精读 v10 批量覆盖但未详细展开的模块;新增亮点 11 条;发现并修复 5 个问题TD-13 至 TD-17
---
## 一、综合评分总览
| 维度 | v10 评分 | v11 评分 | 变化 | 说明 |
|------|---------|---------|------|------|
| 架构设计 | 9.2 | 9.3 | ↑ | PuzzleWire 逻辑门 / EventChain 批量评估 / EquipmentContext 等设计亮点进一步拉高评分 |
| 性能 | 9.1 | 9.2 | ↑ | PostProcessManager _startWeights 复用 / PaletteCatalogSO 懒初始化缓存被发现 |
| 可扩展性 | 9.3 | 9.4 | ↑ | [SerializeReference] ICharmEffect 多态序列化 / EventChain 无代码条件扩展 |
| 编辑器友好 | 9.4 | 9.4 | → | 已满分稳定 |
| 使用便利性 | 9.0 | 9.0 | → | UIManager 面板栈简洁易用 |
| **综合** | **9.20** | **9.30** | **↑** | 深度精读后整体评价进一步提升;修复 TD-13 高优先级问题 |
---
## 二、v10 修复验证
| ID | 修复项 | 验证结果 |
|----|--------|---------|
| TD-06 | InputReaderSO 移除 FindPauseChannelByName | ✅ 已验证:`FindPauseChannelByName` 方法已移除,仅保留 `Debug.Assert` |
| TD-07 | EmergencySaveService 委托 SaveManager | ✅ 已验证:`PromoteToSlot` 通过 `_saveManager.PromoteEmergencyToSlotAsync` 调用 |
| TD-08 | AccessibilityManager 注册 IAccessibilityService | ✅ 已验证:`IAccessibilityService` 接口已创建,`Awake` 中注册 |
| TD-09 | HUDController SetActive 复用 HP Cell | ✅ 已验证:`RebuildHPCells` 使用 SetActive 代替 Instantiate/Destroy |
| TD-10 | MovingPlatform 缓存 WaitForSeconds | ✅ 已验证:`_waitForEndpoint` 字段在 `Awake` 初始化 |
| TD-11 | RewardSO IRewardTarget 解耦 | ✅ 已验证:`RewardSO.Apply(IRewardTarget)``PlayerStats` 实现接口 |
| TD-12 | CrashReporter 频率限制 + 最大文件数 | ✅ 已验证:`MaxLogsPerSession = 5``PruneOldLogFiles` 保留最新 N 个 |
---
## 三、深度精读模块评审
### 3.1 Puzzle 层(★★★★★)
#### PuzzleInterfaces + PuzzleWire + PuzzleSwitch + PuzzleReceiver + PuzzleDoor
谜题系统是本次精读最亮眼的模块,设计严谨、扩展性极强:
**逻辑门设计PuzzleWire**
```csharp
bool shouldActivate = _logic switch
{
LogicType.AND => System.Array.TrueForAll(_switches, s => s != null && s.IsActive),
LogicType.OR => System.Array.Exists(_switches, s => s != null && s.IsActive),
LogicType.XOR => _switches.Count(s => s != null && s.IsActive) % 2 == 1,
_ => false,
};
```
三种逻辑门AND/OR/XOR通过 Inspector 配置,关卡策划无需编写一行代码即可组合复杂谜题。策略模式的完美实践。
**模板方法模式PuzzleReceiver / PuzzleDoor**
```csharp
// PuzzleReceiver — 基类
protected virtual void OnActivate() { }
protected virtual void OnDeactivate() { }
// PuzzleDoor — 子类 (4 行代码)
protected override void OnActivate() => _animancer?.Play(_openClip);
protected override void OnDeactivate() => _animancer?.Play(_closeClip);
```
子类 `PuzzleDoor` 只需 4 行代码实现门的行为,`Activate/Deactivate` 的回调、反馈播放、持久化全部由父类 `PuzzleReceiver` 统一处理。
**持久化注入(非 Singleton**
```csharp
[SerializeField] private WorldStateRegistry _worldState; // SO 注入
// ...
_worldState?.SetFlag("switch_" + _switchId);
```
通过 SO 注入 `WorldStateRegistry` 而非 `Instance` 单例访问,测试友好、多房间隔离性好。
**ISwitchable.ForceState()**
```csharp
/// <summary>SaveData 恢复时调用,强制设置状态不触发副作用逻辑。</summary>
void ForceState(bool active);
```
存档恢复场景下的无副作用状态强制设置,接口契约明确,避免重放音效/Feedback。
---
### 3.2 Liquid 层(★★★★☆)
#### LiquidZone + LiquidPhysicsConfigSO + WaterDangerState + UnderwaterPostProcessingController
物理配置数据驱动,参数完整(重力、浮力、阻力、溺水时间等),通过 SO 注入,不同区域可共享或独立配置。
**UnderwaterPostProcessingController中断安全的 Volume 混合**
```csharp
private void BlendVolume(float target, float duration)
{
if (_blendCoroutine != null) StopCoroutine(_blendCoroutine);
_blendCoroutine = StartCoroutine(BlendRoutine(target, duration));
}
```
快速入水→出水→再入水场景下,上一个 Blend Coroutine 被安全终止,起始值从当前 `weight` 读取(不会突变),视觉无跳变。
**发现问题 TD-16**`WaterDangerState` 使用字符串比较液体类型,见 §四。
---
### 3.3 VFX 层(★★★★★)
#### VFXPool + PostProcessManager + PaletteSwapSystem + RegionLightController + HurtFlashController + HitFXSpawner
**VFXPool双路径设计**
```csharp
if (TryDequeue(vfxRef, out var ps))
StartCoroutine(PlayImmediate(vfxRef, ps, position, rotation, maxLifetime));
else
StartCoroutine(PlayLoadAsync(vfxRef, position, rotation, maxLifetime));
```
池命中→同步定位播放(无 GC 等待);池未命中→异步 Addressable 加载。合理处理了首次冷启动和常规复用两种场景。
**PostProcessManager_startWeights 数组复用**
```csharp
private float[] _startWeights; // 字段
private IEnumerator BlendCoroutine(Volume target, float targetWeight)
{
for (int i = 0; i < _managedVolumes.Length; i++)
_startWeights[i] = _managedVolumes[i] != null ? _managedVolumes[i].weight : 0f;
// ... Lerp 每帧遍历
}
```
`_startWeights` 作为字段缓存,避免每次 Coroutine 开始时 `new float[]` 分配。与 VFXPool 的理念一致。
**PaletteSwapSystemMaterialPropertyBlock + 懒初始化字典**
```csharp
private static readonly int PaletteTexID = Shader.PropertyToID("_PaletteTex");
private MaterialPropertyBlock _block;
// PaletteCatalogSO
private void OnValidate() => _cache = null; // 编辑器改资产后重建缓存
```
三重优化:
1. `Shader.PropertyToID` 静态缓存(避免重复字符串查找)
2. `MaterialPropertyBlock`(不创建新材质实例)
3. `OnValidate` 使字典缓存失效(编辑器实时响应)
**HurtFlashController — 发现问题 TD-14**,见 §四。
---
### 3.4 Equipment 层(★★★★★)
#### EquipmentManager + ICharmEffect + EquipmentContext + CharmSO
**[SerializeReference] 多态序列化**
```csharp
[SerializeReference]
public List<ICharmEffect> effects = new();
```
通过 `[SerializeReference]` 使接口列表在 Inspector 中多态序列化,设计师可直接在 SO 上配置任意 `ICharmEffect` 实现(`StatModifierEffect`/`OnHitEffect`/`SkillSlotOverrideEffect` 等),无需代码。商业游戏中常见的数据驱动配置模式。
**EquipmentContext 结构体:避免接口直接依赖具体类型**
```csharp
public struct EquipmentContext
{
public PlayerStats Stats;
public PlayerFeedback Feedback;
public IEventChannelRegistry Events;
public SkillModifierRegistry SkillMods;
public WeaponManager WeaponMgr;
}
```
效果类通过 `EquipmentContext` 访问系统,而非直接引用各 Manager。添加新效果类型时无需修改 `ICharmEffect` 接口。
**_usedNotches 缓存计算**
```csharp
_usedNotches += charm.notchCost; // 装备时累加
_usedNotches -= charm.notchCost; // 卸下时减去
```
避免每次查询时 `_equipped.Sum(c => c.notchCost)` 的 LINQ 分配。
---
### 3.5 EventChain 层(★★★★★)
#### EventChainSO + EventChainManager + ChainCondition
**批量评估 + 帧内事件合并**
```csharp
// 事件回调:只标记 pending不立即遍历
private void EvaluateAll() => _evaluatePending = true;
private void Update()
{
if (!_evaluatePending) return;
_evaluatePending = false;
DoEvaluateAll(); // 每帧最多一次
}
```
同帧内多个事件到达时,`_evaluatePending` 合并为一次 `DoEvaluateAll`,避免重复遍历所有链。经典 Dirty Flag 模式。
**ChainCondition.ResetState() — SO 资产 PlayMode 安全**
```csharp
/// <summary>
/// 重置运行时瞬态状态(每次 EventChainManager.OnEnable 时调用)。
/// SO 是资产_met 等字段会跨 PlayMode 会话残留;
/// 显式重置确保每次进入游戏时条件从初始状态开始评估。
/// </summary>
public virtual void ResetState() { }
```
完美解决了 ScriptableObject 字段跨 PlayMode 会话状态残留的经典问题,注释也解释了原因。
**#if UNITY_EDITOR Editor 专用静态事件**
```csharp
#if UNITY_EDITOR
public static event Action<string, string> OnChainExecutedInEditor;
#endif
```
Editor 窗口可订阅此事件实时显示链执行日志,生产构建零开销。
---
### 3.6 Progression / Achievement 层(★★★★★)
#### AchievementManager + AchievementCondition 子类
**条件多态 + SO 独立配置**
每个成就条件(`DefeatedBossCondition`/`MapExplorationCondition`/`ParryCountCondition` 等)是独立 SO策划独立配置不需要代码介入。条件实现极简
```csharp
public class DefeatedBossCondition : AchievementCondition
{
public string bossId;
public override bool IsMet(SaveData save)
=> save?.World != null && save.World.DefeatedBossIds.Contains(bossId);
}
```
**EvaluateAll(SaveData) 模型**
`AchievementManager.EvaluateAll(SaveData save)` 接受 `SaveData` 参数而非持有引用,调用者控制评估时机(存档加载后、房间进入时等),不依赖 MonoBehaviour Update。
---
### 3.7 Player / Enemy States 层(★★★★★)
#### PlayerStateBase + HurtState + SwimState + SpringState + EnemyHurtState 等
**状态基类属性代理设计**
```csharp
protected InputReaderSO Input => _owner.Input;
protected PlayerMovement Move => _owner.Movement;
protected PlayerStats Stats => _owner.Stats;
protected AnimancerComponent Anim => _owner.Animancer;
```
通过 `_owner` 属性代理,每个状态子类用简洁名称访问系统,无需每次写 `_owner.Movement.xxx`
**HurtState 双重结束保护**
```csharp
private bool _ended;
private void OnHurtEnd()
{
if (_ended) return; // 防止 OnEnd 事件和 _timer 双重触发
_ended = true;
// ...
}
```
动画 OnEnd 回调和 `_timer <= 0` 两路都可能触发 `OnHurtEnd``_ended` 标志防止双重转换。
**SwimState 配置注入**
```csharp
public void SetPhysicsConfig(LiquidPhysicsConfigSO config) => _currentPhysics = config;
```
PlayerController 在切换状态前注入物理配置SwimState 本身不关心如何获取当前液体区域信息。
**EnemyHurtState → EnemyDeadState 死锁保护**
```csharp
animState.Events(owner).OnEnd = () =>
{
if (owner.CurrentState == EnemyStateType.Hurt) // 防止被 Dead 状态覆盖后回到 Controlled
owner.ForceState(EnemyStateType.Controlled);
};
```
---
### 3.8 UI 层补充(★★★★☆)
#### UIManager 面板栈 + DeathScreenController
**UIManager.PanelStack**
```csharp
private readonly Stack<GameObject> _panelStack = new();
public void OpenPanel(GameObject panel)
{
if (_panelStack.Count > 0) _panelStack.Peek().SetActive(false);
panel.SetActive(true);
_panelStack.Push(panel);
}
public void CloseTopPanel()
{
_panelStack.Pop().SetActive(false);
if (_panelStack.Count > 0) _panelStack.Peek().SetActive(true);
}
```
面板栈自然支持多层嵌套(游戏中→暂停→设置→重绑定),无需各面板互相知晓。
**DeathScreenController — 发现问题 TD-17**,见 §四。
---
### 3.9 DialogueManager★★★★★
打字机协程逻辑处理了"跳过"语义的两个阶段:
1. 打字完成前按跳过 → 立即显示完整文本
2. 文本完全显示后按 Submit → 进入下一行
```csharp
yield return new WaitUntil(() => !_dialogueBox.IsTyping || _skipRequested);
if (_dialogueBox.IsTyping) _dialogueBox.SkipTyping();
_skipRequested = false;
yield return new WaitUntil(() => _skipRequested);
```
两阶段 `WaitUntil` 正确区分了"跳过打字"和"推进对话"两个语义,无 Update 轮询。
---
## 四、发现的新问题列表
### TD-13 — IQuestManager: 接口与实现签名不匹配(★★★ 高优先级)
**位置**: `Assets/Scripts/Quest/IQuestManager.cs`
**严重程度**: 高
**描述**: TD-11 已将 `QuestManager.CompleteQuest` 的参数从 `PlayerStats player` 改为 `IRewardTarget rewardTarget`,但 `IQuestManager` 接口声明未同步更新,仍为:
```csharp
using BaseGames.Player;
void CompleteQuest(string questId, PlayerStats player); // ← 与实现不匹配
```
任何通过 `IQuestManager` 接口调用 `CompleteQuest` 的代码将会编译错误(参数类型不匹配)。同时 `using BaseGames.Player` 使 `BaseGames.Quest` 程序集对 `BaseGames.Player` 产生依赖TD-11 的解耦目标未彻底达成。
**修复方案**: 将接口方法改为 `void CompleteQuest(string questId, IRewardTarget rewardTarget)`,移除 `using BaseGames.Player`
---
### TD-14 — HurtFlashController: Flash() 每次 new WaitForSeconds
**位置**: `Assets/Scripts/VFX/HurtFlashController.cs`
**严重程度**: 低
**描述**: `FlashCoroutine` 每次执行都创建新的 `WaitForSeconds` 实例:
```csharp
yield return new WaitForSeconds(_config.HurtFlashDuration);
```
玩家频繁受击时(连击场景),每次 `Flash()` 都会 GC 分配,与 TD-10MovingPlatform修复一致。
**修复方案**: 缓存 `private WaitForSeconds _waitForFlash`,在 `Awake` 中初始化(若 `_config` 已赋值),或在首次使用时懒初始化。
---
### TD-15 — EventChainManager: OnEnable 内联 lambda 每次重新分配
**位置**: `Assets/Scripts/EventChain/EventChainManager.cs`
**严重程度**: 极低(可选优化)
**描述**: `OnEnable` 中每个订阅均通过内联 lambda 包裹:
```csharp
_onBossDefeated?.Subscribe(id => { OnBossDefeated?.Invoke(id); EvaluateAll(); }).AddTo(_subs);
```
每次 `OnEnable` 调用(场景加载/重新激活)均产生新 lambda 分配5 个 closure
**修复方案**: 将各转发逻辑提取为具名私有方法(与全框架其他订阅的风格一致),`OnEnable` 中引用方法组而非内联 lambda。
---
### TD-16 — WaterDangerState: 脆弱的枚举-字符串液体类型比较
**位置**: `Assets/Scripts/World/Liquid/WaterDangerState.cs`
**严重程度**: 低
**描述**: 当前代码通过 `nameof(LiquidType.Water)` 比较液体类型:
```csharp
if (evt.LiquidType != nameof(LiquidType.Water)) return;
```
`LiquidEvent``LiquidType` 字段存储 `_liquidType.ToString()`(由 `LiquidZone` 传入)。若 `LiquidType` 枚举值改名,此处的 `nameof` 比较会自动更新C# 语言保证),但若 `LiquidZone` 使用的是已序列化的旧字符串值,两处仍会不匹配。更根本的问题是:`LiquidEvent` 将枚举存为字符串,丢失了类型安全。
**修复方案**: 将 `LiquidEvent.LiquidType` 字段类型改为 `LiquidType` 枚举,比较改为 `evt.LiquidType != LiquidType.Water`,彻底消除字符串比较。
---
### TD-17 — DeathScreenController: 事件订阅时序问题导致延迟显示无效
**位置**: `Assets/Scripts/UI/Menus/DeathScreenController.cs`
**严重程度**: 中
**描述**: `DeathScreenController``OnEnable` 中订阅 `_onPlayerDied` 事件:
```csharp
private void OnEnable() => _onPlayerDied?.Subscribe(OnPlayerDied).AddTo(_subs);
private void OnPlayerDied() => StartCoroutine(ShowAfterDelay(1.5f));
```
但事件触发顺序如下:
1. 玩家死亡 → `PlayerController` 广播 `_onPlayerDied`
2. `GameManager` 响应 → 转换到 `Dead` 游戏状态
3. `_onGameStateChanged` 广播 → `UIManager.HandleGameStateChanged`
4. UIManager 调用 `_deathScreenRoot.SetActive(true)`
5. 此时 `DeathScreenController.OnEnable` 才运行,才订阅 `_onPlayerDied`
由于步骤 1 在步骤 5 之前,`OnPlayerDied` 回调**永远不会被触发**1.5s 延迟显示逻辑失效。死亡界面由 UIManager 直接 `SetActive(true)` 立即显示,无延迟。
**修复方案**: 将延迟显示逻辑移至 `OnEnable`DeathScreen 被激活时直接启动延迟协程);或监听 `_onGameStateChanged` 事件Dead 状态),在游戏状态进入 Dead 时触发延迟。
---
## 五、v11 新增亮点汇总
| 亮点 | 位置 | 说明 |
|------|------|------|
| `PuzzleWire` AND/OR/XOR 逻辑门 | `World/Puzzle/PuzzleWire.cs` | Inspector 配置,策划零代码组合复杂谜题 |
| `PuzzleReceiver` 模板方法 | `World/Puzzle/PuzzleReceiver.cs` | 子类 4 行实现门行为,父类统一回调/反馈/持久化 |
| `ISwitchable.ForceState` | `World/Puzzle/PuzzleInterfaces.cs` | 存档恢复时无副作用强制状态,接口契约明确 |
| `EventChainManager` 帧内 Dirty Flag 合并 | `EventChain/EventChainManager.cs` | `_evaluatePending` 防止同帧多事件重复遍历 |
| `ChainCondition.ResetState()` | `EventChain/EventChainSO.cs` | SO 资产运行时状态跨 PlayMode 会话显式重置 |
| `EventChainManager` #if Editor 静态事件 | `EventChain/EventChainManager.cs` | 生产构建零开销的 Editor 调试钩子 |
| `CharmSO [SerializeReference]` | `Equipment/CharmSO.cs` | Inspector 多态序列化,数据驱动护符效果配置 |
| `EquipmentContext` 结构体 | `Equipment/ICharmEffect.cs` | 效果类通过上下文间接访问系统,解耦护符与具体 Manager |
| `PostProcessManager _startWeights` 复用 | `VFX/PostProcessManager.cs` | Coroutine 跨帧共享数组,避免每次 `new float[]` |
| `PaletteCatalogSO` 懒初始化 + OnValidate | `VFX/PaletteSwapSystem.cs` | 字典缓存 + 编辑器改资产后自动失效 |
| `AchievementManager.EvaluateAll(SaveData)` | `Progression/AchievementManager.cs` | 解耦评估时机,不绑定 MonoBehaviour Update |
---
## 六、修复计划
| 优先级 | ID | 文件 | 修复类型 |
|--------|----|------|---------|
| 高 | TD-13 | `Quest/IQuestManager.cs` | 接口方法改为 `IRewardTarget`,移除 `using BaseGames.Player` |
| 中 | TD-17 | `UI/Menus/DeathScreenController.cs` | 延迟显示逻辑改为在 `OnEnable` 启动 |
| 低 | TD-16 | `World/Liquid/WaterDangerState.cs` | `LiquidEvent.LiquidType` 改为枚举类型 |
| 低 | TD-14 | `VFX/HurtFlashController.cs` | 缓存 `WaitForSeconds` |
| 极低 | TD-15 | `EventChain/EventChainManager.cs` | 具名方法替代内联 lambda |
---
## 七、总体结论
经过 v11 深度精读与 TD-06~TD-12 修复验证Zeling v2 框架整体质量达到 **9.30/10** 的高分水准:
- **TD-13 是唯一高优先级问题**:接口与实现签名不一致,属于 TD-11 修复的遗漏,需立即补全
- **无架构级新缺陷**:所有新发现问题均属局部实现细节
- **Puzzle / EventChain / Equipment 三个模块质量超预期**`[SerializeReference]`、逻辑门、Dirty Flag 合并、条件 ResetState 等均体现高水平的工程设计
- **框架已具备商业发行级代码质量**,可进入功能内容制作阶段
**建议**:修复 TD-13必须和 TD-17强烈建议后即可封板TD-14/15/16 可在下轮优化迭代中一并处理。