# 开发单元记录

本项目按“小单元、可验证”的方式推进。每个单元都必须有明确目标、非目标、编辑器验证步骤，以及用户验证结果；当前单元通过前，不进入下一个单元。

主要参考：
- `Docs/GameDesign_v0.1.md`：产品方向、网络规则、架构边界、MVP 范围和长期计划。
- `Docs/SystemDesign/`：已导入的系统设计原型文档，提供玩法、UI、战斗、生存压力、目标链和地图图标设计来源。
- `Docs/SystemDesign/UE_EngineDifferences.md`：Unity / URP / UGUI / Cinemachine / C# 到当前 Unreal 5.8 项目的差异解释层。
- `Docs/SystemDesign/UE_P0ImplementationPlan.md`：把导入系统设计转换为 UE 5.8 C++ / Blueprint / DataAsset / UMG 实施计划的文档层说明。

## 当前规则

- 一次只做一个单元。
- 当前活动单元以本文档的状态表为准。
- 单元进行中不要擅自扩大范围。
- 实现后由用户在 Unreal Editor 中验证。
- 验证时发现的问题留在当前单元内修完，再关闭单元。
- 导入的 `Docs/SystemDesign` 文档不等于 UE 当前实现状态；引用前必须先处理引擎差异和当前 MVP 范围。
- 引擎差异、长期平台工作、正式 UI、美术资源等内容，除非当前单元需要，否则不展开。

## 单元状态

| 单元 | 状态 | 目标 | 用户验证 |
| --- | --- | --- | --- |
| 0. 编译与启动 | 完成 | C++ 项目能编译，地图能打开，玩家能生成并完成基础控制。 | 2026-05-29 通过 |
| 1. 单人角色手感 | 完成 | 调整第一人称移动、镜头高度、冲刺、下蹲和体力手感。 | 2026-05-29 通过 |
| 2. 基础交互 | 完成 | 看准资源点按 `E`，通过服务器确认获得资源。 | 2026-05-29 通过 |
| 3. 本地多人 | 完成 | 使用 Listen Server 和一个客户端验证 Steam-like LAN 房间流程。 | 2026-05-29 通过 |
| 4. 最小背包数据 | 完成 | 用服务器拥有的通用物品栈替代临时木头/石头计数。 | 2026-05-29 通过 |
| 5. 拾取与丢弃 | 完成 | 世界物品可拾取、可丢弃，多人下不重复复制。 | 2026-05-29 通过 |
| 6. 第一个合成配方 | 完成 | 服务器验证材料后合成一个基础物品。 | 2026-05-29 通过 |
| 7. 场景站点交互 | 完成 | 使用场景提供的工作台/升级台进行制作或装备升级验证。 | 2026-05-29 通过 |
| 8. 最小存档 | 完成 | 保存和读取玩家位置、背包、掉落物和站点相关状态。 | 2026-05-29 通过 |
| 9. 系统化最小 HUD | 进行中 | 建立简单但可扩展的 HUD 数据流，显示生存状态、时间和背包摘要。 | 待验证 |
| 10. 背包与合成台 UI | 进行中 | B 键打开背包，工作台打开合成界面，显示配方、材料、制作时间、装备与丢弃操作。 | 待验证 |
| 11. 表格驱动物品与配方 | 进行中 | 物品、配方、材料需求、装备性和 UI 显示数据从开发期表格生成，运行时按 ItemId/RecipeId 查询。 | 待验证 |
| 12. 平台配置分层与本地房间入口 | 进行中 | 保留 Null/LAN 本地测试，同时把平台后端、LAN/Internet 模式和房间入口封装到项目层。 | 待验证 |
| 13. S0/L0 测试地图结构 | 进行中 | 建立项目自己的 S0/L0 测试地图目录、灰盒地图和关卡目录规则，供后续电梯切层、资源摆放和 AI 验证使用。 | 待验证 |
| 14. 持久电梯与 S0/L0 Streaming | 进行中 | 建立一个常驻电梯 Persistent Map，交互后垂直移动并在 S0/L0 两个 Streaming Level 之间切换。 | 待验证 |
| 15. 基础战斗首版 | 进行中 | 建立服务器权威近战输入、命中、扣血、体力消耗和测试靶验证路径。 | 待验证 |
| 16. 第三方资源导入与分类 | 进行中 | 把选定第三方资源导入项目路径并按类型归类，避免散落在临时目录。 | 待验证 |
| 17. 主线渲染配置 | 进行中 | 建立主线动态光照、曝光、Lumen 和持久环境光配置规则。 | 待验证 |
| 18. 匕首攻击动画接入 | 进行中 | 用正式 IK Retarget 工作流接入匕首攻击动作，攻击表现纳入第三人称装备动画 Profile。 | 待验证 |
| 19. 快捷装备栏 | 进行中 | 建立 1-4 快捷装备槽、服务器权威切换、HUD 显示和保存读取。 | 待验证 |
| 20. 中心交互提示 UI | 进行中 | 建立中心准星和接口驱动的交互提示，显示当前可交互目标和 E 键动作。 | 待验证 |
| 21. 第一人称手臂 Mesh 接入 | 已替代 | 第一人称手臂 active path 已被 Unit 25 的第三人称角色架构替代。 | 2026-06-03 替代 |
| 22. Search-A 可搜索容器 | 完成 | 建立场景作者摆放的可搜索容器 Actor，使用方块/材质占位，但交互、物品发放、多人防重复和存档走正式服务器架构。 | 2026-06-03 通过 |
| 23. 角色位置扇形交互与近战检测 | 进行中 | 交互和近战从角色位置出发做扇形候选检测，选择最近有效目标，避免第三人称相机后移影响玩法判定。 | 待验证 |
| 24. 默认第三人称相机模式 | 已替代 | 视角切换方案已被 Unit 25 的纯第三人称 SpringArm 相机替代。 | 2026-06-03 替代 |
| 25. 第三人称 Lyra-style 角色动画架构收口 | 完成 | 抛弃第一人称 active path，建立 `ACSPlayerCharacter`、装备动画 Profile、事件驱动装备表现、第三人称相机和武器预览调参工作流。 | 2026-06-03 通过 |
| 26. Search-B 容器内容 UI | 完成 | 容器交互打开内容面板，显示容器内容和玩家背包，拾取按钮走服务器请求。 | 2026-06-03 通过 |
| 27. Search-C LootTable 容器数据 | 完成 | 容器按 `ContainerId -> LootTableId` 从生成数据表产生普通物资，关键物不随机复制。 | 2026-06-04 通过 |
| 28. Save-A 存档结构整理 | 完成 | 把玩家存档、世界进度和世界 Actor 状态分组，保留版本迁移入口和离线玩家记录。 | 2026-06-04 通过 |
| 29. Objective-A 世界状态核心 | 完成 | 建立 `ObjectiveId` / `WorldState` / `ObjectiveEvent` 基础结构，由服务器写入世界进度。 | 2026-06-04 通过 |
| 30. Objective-B 门禁/路线绑定 | 完成 | 电梯目标层读取 `WorldProgress.UnlockedRouteIds` 和 `WorldStateTags`，未解锁时拒绝并显示原因。 | 2026-06-04 通过 |
| 31. GM-A 调试命令面板 | 完成 | 把常用控制台验证命令收敛到 `F10` GM 面板，按钮仍调用项目服务器权威入口。 | 2026-06-04 通过 |
| 32. Elevator-A 层级注册与 GM 调试 | 完成 | 电梯层级从 `LayerDefinitions` 注册表读取，并在 GM 面板显示/解锁当前目标路线。 | 2026-06-04 通过 |
| 33. Elevator-B 过渡 UI 与输入限制 | 完成 | 电梯锁定/移动/到达反馈进入 HUD 状态条，切层期间关闭菜单并临时限制移动输入；已修复 L0 Streaming 竖向偏移，并补齐未注册 Streaming Level 时的动态加载路径。 | 2026-06-07 通过 |
| 34. Equipment-A 装备上半身 Locomotion Profile | 完成 | 装备动画按 Profile 在 idle/walk/run/crouch 状态间切换，并建立 Knife/Pistol/Shotgun 动作族。 | 2026-06-07 通过 |
| 35. Combat-A 数据稳定化 | 完成 | `Items.csv` 收敛为通用物品表，装备表现和近战数值拆到独立表；Knife 现有装备/攻击行为从新表读取。 | 2026-06-07 通过 |
| 36. Firearm-A 基础枪械开火与弹药消耗 | 完成 | Pistol/Shotgun 从枪械表读取弹药、伤害、散布、射程和冷却；服务器扣弹并做 hitscan 命中。 | 2026-06-07 通过 |
| 37. CodeDoc-A 关键 C++ 注释整理 | 完成 | 为战斗、交互、角色、玩家状态、物品数据、存档、容器、电梯和世界进度等关键 C++ 边界补充中文注释。 | 2026-06-04 无需编辑器验证 |
| 38. AimCamera-A 右肩瞄准镜头参数 | 完成 | 新增 `AimingCameraOffset`，瞄准时 SpringArm 可向角色右肩偏移；默认瞄准距离调为 240，FOV 调为 65，便于越肩视角调参。 | 2026-06-07 通过 |
| 39. Footstep-A 材质驱动脚步声 | 完成 | FootIK 后采样脚位和地面 Physical Surface，按脚沿移动方向的落脚相位触发脚步声；支持 Concrete/Metal/Wood/Water/Dirt/Grass/Gravel。 | 2026-06-07 通过 |
| 40. Inventory-A 背包组件抽取 | 完成 | 把背包/装备的复制状态与服务端逻辑从 `ACSPlayerState` 抽到 `UCSInventoryComponent`，PlayerState 仅持有组件并暴露 `GetInventory()`，所有调用点改走组件；行为保持不变。 | 2026-06-07 通过 |
| 41. Crafting-A 合成组件抽取 | 待用户验证 | 把 `CraftingState`（复制）、合成校验/计时/产出、工作台与配方查询从 `ACSPlayerController` 抽到 `UCSCraftingComponent`，Controller 仅保留请求入口/Server RPC 薄转发；行为保持不变。 | 待验证 |
| 42. Blockout-A 二层楼房子灰盒 | 完成 | 生成可复用二层楼灰盒房子 Blueprint、测试地图和 blockout 材质，供场景动线、楼梯、房间和越肩战斗空间预览。 | 2026-06-07 通过 |
| 43. Enemy-A 基础敌人本体与受击 | 完成 | 新增服务器权威 `ACSEnemyCharacter`（复用 `SurvivalStatsComponent` 当血量），静止状态可被现有近战/枪械击杀并复制死亡；AI 大脑、感知、攻击、掉落在后续单元。 | 2026-06-06 通过（单人 PIE 砍杀后死亡回收正常；死亡倒地动画随 Enemy-B 一并接入） |
| 44. InteractOutline-A 屏幕空间可交互描边 | 待用户验证 | 可交互描边从「外壳网格外扩」改为基于摄像机视角的后处理边缘检测（Custom Depth + 后处理材质 `M_InteractOutline_PP`，琥珀金、只在可见时描边）；5 个可交互类聚焦时改写主网格 Custom Depth，旧外壳路径暂禁用待清理。 | 待验证 |
| 45. Enemy-Anim-A 僵尸动作包重定向 | 完成 | 用正式 IK Retarget 工作流把 `GruesomeZombieAnimset` 的 29 个 in-place 僵尸动作输出到 `SK_MC03_Full` / Zombie 骨架的项目自有动画目录；已在 Editor 关闭后用 `GLOBAL_ROTATION_AXES` 完整重导，抽查 Idle/Walk/Atk/Dead 上肢采样正常。 | 2026-06-07 通过 |
| 46. Enemy-B 感知与追击 | 完成 | 新增服务器权威薄 `ACSEnemyAIController`（`UAIPerceptionComponent`+视觉），看到玩家控制 Pawn 即用 `MoveToActor` 直奔玩家、丢失视线即停；`ACSEnemyCharacter` 设 `AIControllerClass`/`AutoPossessAI` 并按 `MoveSpeed` 移动。需场景内有 `NavMeshBoundsVolume` 才能寻路。 | 2026-06-07 通过 |
| 47. Buff-A Buff 数据结构 | 完成 | 建立跨系统底座的 Buff 静态数据层：新增 `BuffDefinitions` + `BuffAttributeModifiers` + `BuffPeriodicEffects` 三张源表，经 `GenerateGameData.py` 枚举/引用校验生成；`UCSItemDataSubsystem` 新增 `FCSBuffDefinition`/`FCSBuffAttributeModifier`/`FCSBuffPeriodicEffect` 与 `GetBuffDefinition`/`IsKnownBuff`/`GetAllBuffIds` 查询（修正/周期按 `BuffId` join）；`PrintBuff` exec 打印解析结果。仅数据层，不施加 Buff、不属性快照、不存档（留给 Buff-B/C/D）。 | 2026-06-07 通过 |
| 48. Buff-B 状态效果组件 | 待用户验证 | 新增服务器权威 `UCSStatusEffectComponent`（挂玩家与敌人）：`ApplyBuff`/`RemoveBuff`/`ClearAllBuffs`，按 `StackPolicy` 叠层/刷新，按服务器 Timer 结算周期效果（Damage/Heal/Stamina 经 `SurvivalStatsComponent`），按 `DurationSeconds` 到期移除；生效摘要 `ActiveEffects` 复制给 Owner 并广播 `OnActiveBuffsChanged`。`ApplyBuffSelf`/`ClearBuffs`/`PrintActiveBuffs` exec 验证。不做属性修正（Attribute-A）、独立多实例（Buff-E）、存档（Buff-I）、UI（Buff-H）、命中附加（Combat-B）。 | 待验证 |
| 49. Enemy-C 移动动画与近战攻击/死亡 | 完成 | 接入 locomotion AnimBP（`ABP_ContaminatedMutant` + `UCSEnemyAnimInstance`：`GroundSpeed`/`bIsMoving`/`LocomotionPlayRate`，BlendSpace 速度驱动 idle/walk/run，`LocomotionPlayRate = bIsMoving ? max(1, GroundSpeed/RefSpeed) : 1` 让移动越快动作越快、idle 恒 1.0 不被加速）。`ACSEnemyCharacter` 加近战攻击：AIController 追到 `AttackRange`(180) 停下转向 → `TryMeleeAttack` 播 `AN_Zombie_Atk01`(DefaultSlot 动态 montage) → `AttackHitDelaySeconds`(0.4) 后命中帧判目标仍在范围则 `ApplyDamage`(15) + 预留 `GrantedBuffIdsOnHit`(Enemy-D) → `AttackCooldownSeconds`(1.6) 冷却；攻击保持静止时长按动作实际长度算（避免后半段身体移动脚原地=滑步）。死亡用单节点 `PlayAnimation` 播 `AN_Zombie_Dead01` 停末帧（不依赖 DefaultSlot、不回 idle）。`MoveSpeed`(默认 400) 在 BeginPlay 应用到 `MaxWalkSpeed`。僵尸重定向最终用 auto-align(CHAIN_TO_CHAIN) 起手 + 编辑器内手动对齐 retarget pose + `CS_ZOMBIE_EXPORT_ONLY=1` 重烤定稿；并去掉脚本对动画强制 `force_root_lock`（曾导致 pivot 钉平面、与手动导出不一致）。 | 2026-06-07 通过 |
| 50. Enemy-Atk-Notify 命中卡帧 | 完成 | 把敌人命中从固定延迟定时器改为攻击动作上的 `UCSAnimNotify_EnemyMeleeHit`（单点 AnimNotify）在挥击帧触发，命中/掉血/受击反馈与动作精确同步、自动随播放倍率；保留临近结束的兜底定时器 + `bHitAppliedThisAttack` 单次去重（有 notify 时它先命中，定时器被挡掉）。用户在 `AN_Zombie_Atk01` 挥击帧手动放该 notify。 | 2026-06-07 通过 |
| 51. Enemy-D 命中污染/流血 | 完成 | 敌人命中给玩家附加 Buff：`PerformAttackHit` 命中时遍历 `GrantedBuffIdsOnHit` 调目标 `UCSStatusEffectComponent::ApplyBuff`（命中钩子在 Enemy-C 已埋）。BP `BP_AI_ContaminatedMutant_Basic` 的 `GrantedBuffIdsOnHit` 填 `Buff_Bleed_Light`（每秒 2 物理伤害、12s、可叠 5 层；Buff-A/B 已就位）。`Buff_ContaminationLeak` 因污染周期类型 Buff-B 未实现暂不挂。 | 2026-06-07 通过 |
| 52. PlayerFeel-A 玩家受击表现 | 完成 | 玩家本机受击反馈（由 `SurvivalStatsComponent.OnHealthChanged` 血量下降、`IsLocallyControlled` 时触发，纯表现不碰权威）：①镜头震动 `UCSHitCameraShake`（纯 Engine 自定义 `UCameraShakePattern` 衰减振荡，零插件依赖；roll/位移压低避免翻滚顶动）；②边缘红晕后处理材质 `M_DamageVignette_PP`（脚本 `CreateDamageVignettePostProcess.py` 生成，中心清晰四周泛红，MID 的 `Intensity` 每帧由 C++ 衰减驱动，挂玩家相机 WeightedBlendables）。受伤**分轻/中/重三级**（按这一下扣血 `LightHitDamageThreshold`/`HeavyHitDamageThreshold`）给不同震动 scale + 红晕强度，再叠每次命中 ±15% 随机抖动 + 震动随机相位，避免表现统一。后续 ③ 受击踉跄动画 + 血液 VFX 待美术资产。 | 2026-06-07 通过 |
| 53. Enemy-E 受击反应与死亡多样化 | 完成 | 敌人受击表现，让基础怪物成为"基准版本"。新增可复用接口 `ICSHitReactable`（`ReceiveHitReaction(攻击者, 伤害)`，BlueprintNativeEvent），`UCSCombatComponent` 近战/枪械命中成功后在服务器调用，攻击者位置即受击方向来源（玩家角色后续也可实现以做受击踉跄）。`ACSEnemyCharacter` 实现：①**方向性 + 分级受击动作**——按攻击者相对自身朝向落在前/后/左/右象限选 `AN_Zombie_HitBody*`(轻击) 或 `HitHeavy*`(重击，伤害≥`HeavyHitDamageThreshold` 25)，C++ 默认装入 8 段、BP 可覆盖；②**短硬直**——`bIsStaggered` + 立即 `AIController->StopMovement()` 取消移动请求、`MaxStaggerSeconds`(0.5) 与动作长度取小者后解除，`AIController::UpdateChase` 据 `IsStaggered()` 暂停追击（与 `IsAttacking()` 同位，避免受击姿势滑步）；`HitReactMinInterval`(0.3s) 节流防连射/连击把怪按住刷硬直；攻击挥击中/死亡不打断（避免与攻击 montage 抢 DefaultSlot）；DoT(流血)走 `StatusEffectComponent` 不经命中钩子故不触发受击。③**死亡多样化**——`DeathAnimationVariants`(Dead01-06) 服务器随机挑一段、复制 `DeathAnimIndex` 使各端倒地一致（与 `bIsDead` 同 bunch 下发，OnRep 读时索引已就位），变体空则回退单个 `DeathAnimation`。受击/死亡多播经 `MulticastPlayHitReact` / 既有死亡链路播放，全程服务器权威。零新增美术：直接用 Enemy-Anim-A 已重定向的 12 段 Hit + 6 段 Dead。 | 2026-06-07 通过（需 Listen Server + 客户端：从不同方向砍/射怪验证方向性受击与硬直；多次击杀验证死亡动作随机；客户端表现与主机一致） |
| 55. Enemy-Data-A 数据表驱动敌人定义 | 完成 | 把敌人从"硬编码 test dummy"收口为数据驱动的可复用基准：新增 `Data/Source/EnemyDefinitions.csv`（按 `EnemyId` 配 血量/速度/伤害/攻击范围与冷却/重击阈值/命中Buff/视野参数/追击半径/掉落表ID），经 `GenerateGameData.py` 校验（数值下限、`LoseSightRadius>=SightRadius`、`SightHalfAngle<=180`、`GrantedBuffIdsOnHit⊆BuffDefinitions`、`LootTableId`(可空)⊆掉落表、`EnemyId` 唯一）生成到 `Data/Generated/`。`UCSItemDataSubsystem` 新增 `FCSEnemyDefinition` + `GetEnemyDefinition`/`IsKnownEnemy`/`GetAllEnemyIds` + `LoadEnemyDefinitionsCsv`（与 Buff 同模式，全端从磁盘加载）。`ACSEnemyCharacter::BeginPlay`（全端，套用后再设 MaxWalkSpeed/初始化血量）与 `ACSEnemyAIController::OnPossess`（仅服务器，套用后 `ConfigureSense` 重配视觉）按 EnemyId 查表覆盖默认；**表里有该 EnemyId 才覆盖**，未入表则保留 C++/BP 默认（向后兼容）。从此 BP_AI_* 只管内容/表现（Mesh/动画/选 EnemyId），新增怪种=加一行数据。随表附 `Enemy_ContaminatedMutant_Basic`(还原现值) 与 `Enemy_ContaminatedMutant_Brute`(示例重型) 两行。`LootTableId` 已预留待 Enemy-F。 | 2026-06-07 通过（PIE：放置怪行为与改表前一致；改 CSV 数值重生成后 PIE 生效；客户端移动速度/攻击播放与主机一致） |
| 56. Enemy-F 尸体搜索（战利品） | 完成 | 击败的敌人变成可搜刮尸体，复用既有容器/搜刮 UI 而非掉地上。新增**可复用接口 `ICSLootSource`**（`GetLootDisplayName`/`GetLootPersistentId`/`CopyRemainingLoot`/`TryTakeLootItem`，纯 C++ 虚函数 UINTERFACE）。`ACSLootContainer` 实现它（转发既有字段，行为不变）。**控制器搜刮流程泛化**：缓存指针 `OpenLootContainer` 与 `ClientOpenLootContainer`/`ServerTakeLootContainerItem`/`TakeLootContainerItemOnServer` 参数由 `ACSLootContainer*` 改为 `AActor*`(实现 `ICSLootSource`)，快照实时读 + 取物 + 回推面板都走接口；**搜刮 UI 控件零改动**（它本就只读 HUD 快照）。`ACSEnemyCharacter` 实现 `ICSInteractable`+`ICSLootSource`：死亡时按 `LootTableId` 生成 `CorpseLoot`(复制)、`ApplyDeadCollision` 把胶囊设为 QueryOnly + 仅 `Visibility` 重叠（被交互射线发现但不挡移动/近战/导航；无战利品则彻底 NoCollision）、聚焦描尸体网格轮廓、提示"搜索<名>"，取物经玩家库存 `AddItem`。服务器权威；尸体瞬时不入档（PersistentId=None），按 `CorpseLifeSpan` 回收。掉落表：`LT_MutantCorpse`(9mm弹/碎石) 给 Basic/Stalker、`LT_MutantCorpse_Brute`(更多弹+霰弹) 给 Brute；已填进 EnemyDefinitions。 | 2026-06-07 通过（Listen Server + 客户端：击杀→走近尸体出现"搜索"描边与提示→交互开面板取物进背包；客户端与主机一致；活着的敌人不可交互；尸体不挡路/不挡刀） |
| 54. Enemy-G 攻击多样化与发现咆哮 | 完成 | 用 Enemy-Anim-A 已重定向但闲置的动作给基础怪物加表现多样性与预警。①**攻击多样化**：`AttackAnimations`(Atk01-03) 每次攻击 `SelectAttackAnim` 随机挑一段，经改签名的 `MulticastPlayAttackMontage(AttackAnim, PlayRate)` 下发各端一致；攻击保持静止时长按所选动作长度算。兜底命中定时器由"临近末尾"改为挥击中段 `AttackHitTimeFraction`(0.45)，让没有命中 notify 的变体(Atk02/03)也能中段命中，有 notify 的(Atk01)仍 notify 先触发、兜底被 `bHitAppliedThisAttack` 去重挡掉。②**发现咆哮**：`AggroScreamAnimation`(AN_Zombie_Screaming) + `OnAggroAcquired`（由 `AIController::StartChase` 仅在"从无目标到有目标"瞬间调用，持续视野内的反复感知刷新不重复触发），复用受击硬直的"暂停追击"机制做可读的发现前摇（`MaxAggroScreamSeconds` 0.8 与动作长度取小者后解除），`AggroScreamCooldownSeconds`(6s) 防丢失/重获目标刷咆哮。全程服务器权威、多播表现；零新增美术。 | 2026-06-07 通过（Listen Server + 客户端：连续攻击应轮换不同挥击动作；首次发现玩家应先咆哮一下再冲；丢失再重获 6s 后会再咆哮；客户端与主机表现一致） |
| 57. Loop-A 最小归档循环闭环 | 计划 | 用现有 WorldProgress 骨干搭最小归档闭环：S0⇄下层往返，带回残留碎片归档→写世界状态→解锁一条新下层路线；携带样本累积污染（`FSurvivalStats` 新增 Contamination），归档即中和。归档台 Actor + 残留碎片物品 + 携带污染 + 一处可见的下次不同。 | — |
| 61. Enemy-Patrol-A 巡逻游荡 + 脱战返回 | 待用户验证（C++ 编译通过；需整编 CoopSurvivalEditor 后 PIE） | 让怪无目标时不再像雕像、追丢不再满地图跑。长在现有 `ACSEnemyAIController` 三态上，全服务器权威。①状态机加 `Returning`（`ECSEnemyAIState{Idle,Investigating,Chasing,Returning}`）。②**Idle=出生点附近随机游荡**：`OnPossess` 记 `HomeLocation`，`InitialWanderDelay`(1s) 后 `EnterIdle`→`BeginWanderStep`（`UNavigationSystemV1::GetRandomReachablePointInRadius(Home,WanderRadius 600)` 选随机可达点 `MoveToLocation`，到点 `OnMoveCompleted` 调度 `WanderPauseSeconds`(3s) 停顿再走；无导航/失败则定时重试）。`WanderSpeedFraction`(0.35) 悠闲慢走。③**脱战 leash**：`UpdateChase` 每 tick 查自身离 `HomeLocation` > `LeashRadius`(2500) → `StopChase`+`StartReturning` 走回出生点（`ReturnSpeedFraction` 0.6）。④**返程不被动追击**：`Returning` 态 `HandleTargetPerceptionUpdated` 直接早退（避免 leash 边缘 Chasing↔Returning 抖动）；但**受伤的 `ForceAggro` 仍能打断返回**（另一条入口）。到家 `OnMoveCompleted`→`EnterIdle` 恢复游荡。⑤`StopChase` 改为只管"停追击+复位平静情绪"、不设终态，终态由 `EnterIdle`/`StartInvestigate`/`StartReturning` 决定（统一汇合点 `EnterIdle`）。`SetWalkSpeedForState` 改 switch：Idle=游荡/Investigating=调查/Returning=返回/Chasing=全速，情绪仍仅追击=攻击性。⑥`SetWalkSpeedForState` 决定 mood→游荡/返回都用平静(Normal)姿态，与 Enemy-Locomotion-B 的 Calm BlendSpace 配合。调试 `cs.ai.DrawPerceptionDebug` 加出生点(紫)+游荡半径(蓝)+leash 半径(品红)+状态文字(IDLE/WANDER·RETURNING·home dist)。需场景有 `NavMeshBoundsVolume`。零新增美术。**参数已下沉数据驱动**：巡逻/调查 6 参数（`WanderRadius`/`WanderSpeedFraction`/`WanderPauseSeconds`/`LeashRadius`/`ReturnSpeedFraction`/`InvestigateSpeedFraction`）入 `EnemyDefinitions.csv`（加列）+ `FCSEnemyDefinition` + 加载器（按列名读、缺列回退默认）+ `ApplyPerceptionDefinition` 套用 + 生成器校验（半径≥0、三比例≤1）。从此调这些 AI 数值=改表+`python Scripts\GenerateGameData.py`+重开 PIE，**不用重编**，每怪可不同（Basic 游荡 0.2/Brute 0.18 重型/Stalker 0.25 漫游更广）。起因：AI 参数原是控制器 C++ 默认值、改一次重编一次（叠加外部整编打乱 Live Coding 基线反复触 `LiveCodingLimitError`）→ 下沉根治。`WanderSpeedFraction` 用户初判 0.35 太快→表里定 0.2。 | 待验证 |
| 60. Enemy-Feedback-A 受击表现（命中点喷血 + 闪白） | 基本通过（2026-06-08 用户 PIE 确认血落命中接触点；闪白默认关） | 让打怪有打击感与反馈。**全事件驱动、零新增逻辑 RPC（仅 1 个表现多播）**：①新增 `UCSEnemyFeedbackComponent`（挂敌人，纯表现、不复制、专用服务器跳过）——`PlayHitFeedback(命中点,命中方向,伤害)`：命中点喷血液 Niagara（朝攻击行进方向、池化 AutoRelease），并整身闪白（`SkeletalMesh->SetOverlayMaterial` 叠 `M_HitFlash_Overlay`，`FlashAmount` 峰值后按 `HitFlashDurationSeconds`(0.12s) 衰减，仅闪白时开 Tick）。②`ACSEnemyCharacter` 加表现多播 `MulticastPlayHitImpact(命中点,命中方向,伤害)`，在 `ReceiveHitReaction`（服务器）里**每次有效命中都调**（放在攻击中/节流早退之前——那只挡受击动画，连射每发都该出血+闪），各端转发给组件。命中点/方向复用既有 `ICSHitReactable::ReceiveHitReaction` 的 HitLocation +（命中点−攻击者）方向。③材质 `M_HitFlash_Overlay`（MCP 建，Additive/Unlit，Emissive=FlashColor×FlashAmount）；血液用第三方 `NS_ShadedBlood_1`（构造默认装入，BP/数据可换）。④Build.cs 加 `Niagara` 依赖。**范围调整（用户拍板）**：血条**不做**（破坏真实感）；伤害数字改为**可选开关**（CVar `cs.combat.ShowDamageNumbers`，留作后置小单元）。零新增美术（复用现有血液包）。**PIE 调试落地**：①闪白起初不显=`M_HitFlash_Overlay` 缺 `used_with_skeletal_mesh` usage 标志（MCP 建材质时漏，已补）；并加开关 `bEnableHitFlash`（**默认关**，调好观感再开）。②血液定位：曾试 `SpawnSystemAttached` 挂命中骨→被吸到骨头轴心（身体中心）造成位置偏，**已回退**为在精确 `Hit.ImpactPoint`（`SpawnSystemAtLocation`）生成→用户确认"差不多"。`HitBoneName` 仍随多播传递（保留供后续按部位换血效/表面投影）。③整编一度被**他单元**编译错误拦截：`CSCharacterAppearanceComponent.cpp` 用了 UE5.8 已无的 `USkeletalMeshComponent::GetLeaderPoseComponent()`→改 `LeaderPoseComponent.Get()`（公有 `TWeakObjectPtr` 成员）后整编通过。用户 PIE 确认血落命中点，单元基本通过。 | 2026-06-08 基本通过（血落命中点；闪白默认关；如血略浮在皮肤外=物理资产壳，留待后续收紧/表面投影） |
| 59. Enemy-Locomotion-B 步态多样化与情绪 | 待用户验证（C++ 已编译；两条 BlendSpace 经 MCP 已建；只剩 AnimGraph 接 blend-by-bool + Ctrl+Alt+F11） | 让不同怪读起来是不同步态（走怪/跑怪），并修速度匹配滑步。问题：单条 `BS_Zombie_Locomotion`（Idle@0/NormalWalk@150/OffensiveRun@300，`axis_to_scale_animation=NONE`）顶点 300 但追击 400→被钳成纯跑；C++ `LocomotionPlayRate=max(1,GroundSpeed/250)` 单参考速度二次缩放对不齐→滑步。①**A 速度匹配**：`UCSEnemyAnimInstance` 播放倍率默认恒 1.0（新增兜底开关 `bDrivePlayRateBySpeed`，旧公式退化到它后面），脚步匹配交给 BlendSpace `axis_to_scale_animation=Speed`；②**B 多步态=数据**：步态是速度在同一条 BlendSpace 上的落点，不 fork 资产——靠 `EnemyDefinitions.MoveSpeed` 分化（Basic 400 跑/Brute 300 重型慢跑/Stalker 150 潜行爬），数据已就位，需 BlendSpace 轴覆盖到 ≥ 最快追击速度；③**C 情绪层**：`ACSEnemyCharacter` 加复制 `bIsAggressive`（`SetCombatMood`，`AIController::SetWalkSpeedForState` 在状态切换处统一推：追击=true 凶/调查·待机=false 平静；非 AI 模型，就是状态机布尔），`UCSEnemyAnimInstance` 暴露给 AnimGraph 在平静(Normal 姿态)/攻击性(Offensive 姿态)两条 BlendSpace 间 Blend Poses by bool。可用动作：NormalWalk01/02(平静走，无平静跑)、OffensiveWalk01/02+OffensiveRun(攻击性走→跑)。编辑器侧待接：拆 Calm/Aggro 两条 BlendSpace + 开 axis-to-scale + 重摆采样 + AnimGraph blend-by-bool（须先整编 `CoopSurvivalEditor` 让 `bIsAggressive` 变量出现在 AnimBP）。零新增美术。**落地补充**：察觉走/跑由用户拍板=纯按怪种 `MoveSpeed`（慢恒慢/快恒快，不加逼近降速）。两条 BlendSpace 经 MCP `execute_python_code` 已建好并存盘——`BS_Zombie_Locomotion`(Aggro)=Idle@0/OffensiveWalk01@150/OffensiveRun@400/轴0-450/axis_to_scale=Speed；`BS_Zombie_Locomotion_Calm`=Idle@0/NormalWalk01@100/NormalWalk02@180/轴0-200/axis_to_scale=Speed。只剩 AnimGraph 手接 `Blend Poses by bool(bIsAggressive)`（True=Aggro/False=Calm，blend ~0.25s）+ Ctrl+Alt+F11。**另修攻击滑步**：经 MCP 量证攻击 clip 是纯原地（pelvis 水平位移≈0、root 位移0），冲进攻击的胶囊残速是一条滑步源 → `TryMeleeAttack` 加 `GetCharacterMovement()->StopMovementImmediately()` 硬停（仅函数调用、Live Coding 可生效）。**已知遗留**：攻击 clip 自带脚滑（右脚 Z~8 贴地却水平移 ~20-26cm，烤进动作里），治法=运行时锁脚 IK（Foot Placement / Two Bone IK+触地曲线），用户拍板**留作后续独立单元**，本轮接受。 | 待验证 |
| 58. Enemy-H 听觉感知与调查 | 待用户验证 | 给基准敌人补**听觉感知**，潜行的核心杠杆。①感知层：`ACSEnemyAIController` 在原视觉基础上加 `UAISenseConfig_Hearing`（`HearingRange` 由 `EnemyDefinitions.csv` 新列套用，affiliation 与视觉一致检测中立方）。UE 判定：听到 ⇔ 距离 ≤ `HearingRange × 噪音响度`（线性缩放），故响度=潜行/暴露程度。②噪音源：新增服务器权威 `UCSNoiseEmitterComponent`（挂玩家，构造期 `CreateDefaultSubobject`，组件内部仅 `HasAuthority` 才发声）——低频定时器按步态发移动噪音（蹲走 0.2/走 0.5/跑 1.0，站定不发；蹲走极低=贴脸才被听到），`UCSCombatComponent::FirearmAttackOnServer` 每发被接受的射击调 `ReportGunshotNoise()`(响度 2.5，远超常规听觉半径)。③行为：AI 新增第三态 `Investigating`——`HandleTargetPerceptionUpdated` 按 `Stimulus.Type` 区分视觉/听觉；听到噪音(非追击中)→`StartInvestigate(噪音点)` 谨慎慢走(`InvestigateSpeedFraction` 0.5)过去查看，路上视觉看到玩家→升级 `StartChase`(全速+咆哮)；到点(`OnMoveCompleted`)原地张望 `InvestigateHoldSeconds`(3s) 靠视觉确认，无果则放弃回待机(`InvestigateMaxSeconds` 8s 总超时兜底)；**丢失视线也复用调查**——去最后所见点搜寻而非立即放弃。`HearingRange` 数据：Basic 1500/Brute 1200(迟钝)/Stalker 2800(声猎者)。调试 `cs.ai.DrawPerceptionDebug` 加听觉半径(橙)与调查点。全程服务器权威；零新增美术。 | 待验证 |
| 61. InteractPrompt-B 世界空间交互提示 | 待用户验证（C++ 编译通过：`CoopSurvival` 运行 + `CoopSurvivalEditor` 编辑器目标 2026-06-08 均整编通过；待 PIE） | 交互聚焦反馈从「屏幕空间后处理描边 + 中心屏幕提示条」整体改为**挂在物体上的世界空间「E」提示**。新增 `UCSInteractionPromptComponent`（`UWidgetComponent` 子类，Slate 键帽 E+动作名，`EWidgetSpace::World` 随距离缩放+绝对缩放、朝相机 billboard、Tick 仅可见时启用）；6 个可交互类（`ACSResourceNode`/`ACSLootContainer`/`ACSWorkbenchStation`/`ACSStreamingElevatorStation`/`ACSWorldItemActor`/`ACSEnemyCharacter` 尸体）各带一个，`SetFocused_Implementation` 按各自可用判定调 `SetPrompt(...)`。移除 `ACSPlayerCharacter` 的相机后处理描边整套（`M_InteractOutline_PP` 资产保留不引用、`r.CustomDepth=3` 保留）、各类 `ApplyFocusedState` 的 `SetRenderCustomDepth`、`SCSHUDWidget`/`CSHUDTypes`/`CSPlayerController` 的中心交互提示快照与控件。`Build.cs` 加 `UMG` 模块。**视觉微调（用户拍板）**：黑底白字（键帽/标签 alpha 0.7-0.88）、整体缩小（`PromptScale` 默认 0.3 绝对缩放 + 小字号 E16/标签12 + 紧凑内边距）、修正镜像（billboard 旋转改用 `(相机−提示).Rotation()` 让控件正面朝相机、文字不反）。**性能**：Slate 控件树**惰性构建**——`BeginPlay` 不预建，首次被聚焦时 `SetPrompt` 内 `IsValid` 兜底建一次并缓存；从未被聚焦的可交互物只挂空壳 WidgetComponent（零 Slate/render target 开销）。稳态至多一个提示可见+Tick、不复制。`PromptScale`/`WorldZOffset` 每实例可在细节面板调。纯本地表现（聚焦只在拥有端算）。替代 Unit 20/44。 | 待验证（Listen Server+客户端：各可交互物上方出现「E+动作名」、黑底白字、文字不镜像、整体不过大、随距离缩放朝相机、无描边、无中心提示、按 E 仍可交互、提示不串客户端） |
| 62. Appearance-A 数据驱动可替换外观组件 | 完成（2026-06-08 通过：默认外观无回退 + 控制台换装实时生效；后续接 UI/遮罩/存档） | 把玩家外观从「角色构造里写死 3 个 SkeletalMeshComponent（头/连体服/鞋）+ `PostInitializeComponents` 里 `SetupModularAppearanceLeaderPose`」收口为**数据驱动、服务器权威、可复制、按槽位换装**的系统，为后续「装备驱动外观」铺底。**前置（第 1 步，已用户验证）**：玩家基础身体改 Collection_22 `SKM_UE5_man_body`（`SK_UE5_Skeleton`，已 `add_compatible_skeleton` 与 Manny 双向互标兼容→现有 `ABP_Player_Body` 直接驱动），默认套装 Man + Overall_1。**本单元（第 2 步）**：新增 `UCSCharacterAppearanceComponent`（挂角色，`SetIsReplicatedByDefault`）——`ECSAppearanceSlot`（Head/Torso/Legs/Feet/Hands/Headgear/Vest/Backpack）+ `FCSAppearancePart`（槽位+`FSoftObjectPath` 骨骼网格+可选 body 遮罩材质）；复制状态 `ActiveParts`(ReplicatedUsing=`OnRep_ActiveParts`) 服务器权威，服务端 API `EquipAppearancePart`/`ClearAppearanceSlot`/`SetOutfit`/`ResetToDefaultOutfit`（均 `HasOwnerAuthority` 闸门，改后服务端手动 `RebuildAppearance`、客户端走 RepNotify）；`RebuildAppearance` 幂等地按槽位运行时创建 `USkeletalMeshComponent`（无碰撞、`AlwaysTickPoseAndRefreshBones`、`AttachToComponent(身体)` + `SetLeaderPoseComponent(身体)`），有部件设网格+显示、无部件清网格+隐藏；部件网格不复制，各端从复制的 `ActiveParts` 重建。`DefaultOutfit`（EditDefaultsOnly）默认 = Man 头 + Overall_1 连体服 + 运动鞋，服务端 BeginPlay 首次套用。`ACSPlayerCharacter` 瘦身：删 3 个写死外观组件 + `PostInitializeComponents`/`SetupModularAppearanceLeaderPose`，改持有 1 个 `AppearanceComponent`。**武器系统不受影响**（武器仍挂 `hand_r` socket、与骨架/网格无关）。**数据驱动层（本单元一并落地）**：新增 `Data/Source/AppearanceDefinitions.csv`（按 `ItemId` 配 `SlotType`/`SkeletalMesh`/`BodyOpacityMaskMaterial`），经 `GenerateGameData.py` 校验（`ItemId`⊆Items.csv、`SlotType`∈`APPEARANCE_SLOTS`枚举、`SkeletalMesh` 非空、`ItemId` 唯一）生成；`Items.csv` 加一组 `Apparel_*` 物品（`Apparel`类型，穿戴是独立通道故 `bCanEquip=false`：Overall/Sneakers/Head 默认套件 + Chemical 套(Suit/Mask/Boots) + Firefighter 头盔 + Hiking 背包，均 Collection_22 已确认 `SKM_ue5_*` 兼容骨架网格）；`UCSItemDataSubsystem` 加 `FCSAppearanceDefinition`（`SlotType` 用 `FName` 与 Items 层解耦）+ `GetAppearanceDefinition`/`LoadAppearanceDefinitionsCsv`。组件加 `EquipAppearanceItem(ItemId)`（查表→`TryGetSlotFromName` 把槽名解析为 `ECSAppearanceSlot`(`StaticEnum::GetValueByNameString`)→建 `FCSAppearancePart`→`EquipAppearancePart`）。**穿戴请求走玩家拥有的 `ACSPlayerController`**（项目规则）：exec `CSWearAppearance <ItemId>`/`CSUnwearAppearance <槽名>`/`CSResetAppearance` → 各自 `Server*` RPC →`*OnServer`（取 pawn 的 `AppearanceComponent` 调对应服务端 API），`ClientDebugLog` 回显。**非目标（留后续子步）**：opacity 遮罩防穿模实装（`BodyOpacityMaskMaterial` 数据字段+hook 已留）；穿戴状态存档持久（当前住 pawn 组件，重生回默认；后续经 PlayerState/存档驱动）；正式换装 UI（现仅 exec 验证）；护甲数值/属性（纯外观，不碰战斗）。 | 待验证（Listen Server + 客户端：①默认外观与第 1 步一致、无回退、跟随姿势正常；②控制台 `CSWearAppearance Apparel_Chemical_Suit`/`Apparel_Chemical_Mask`/`Apparel_Chemical_Boots` 换成防化套、`CSUnwearAppearance Torso` 脱、`CSResetAppearance` 复位；③主机与客户端互看换装一致、实时同步；客户端发命令也生效） |
| 63. Narrative-A AIC 叙事导演 | 完成 | 为可循环 demo 补**轻量叙事/演出层第一单元**：事件驱动的 AIC 文本播报，建在已成熟的 WorldProgress 之上（叙事=表现层，不持权威状态）。新增 `UCSNarrativeDirectorSubsystem`（`UWorldSubsystem`，纯本机表现、不复制）——由本机 `ACSPlayerController::CreateHUD` 唤醒（`NotifyLocalControllerReady`），订阅 `ACSGameState::OnWorldProgressChanged`，对世界状态「新增」求差（`WorldStateTags`/`CompletedObjectiveIds`/`UnlockedRouteIds` 三组）命中 `FCSNarrativeBeat`→拆句入队→Slate 覆盖 `SCSNarrativeOverlay`（底部居中 AIC 字幕，attribute 轮询、`HitTestInvisible` 不吃输入）顺序播放、按 `LineSeconds` 计时自动推进。**节拍数据表驱动**：新增 `Data/Source/NarrativeBeats.csv`（BeatId/TriggerType/TriggerKey/Speaker/Lines(`|`分句)/LineSeconds/PersistPlayed/PlayedTag），经 `GenerateGameData.py` 校验生成，子系统按 `UCSItemDataSubsystem` 同模式从 `Data/Generated/` 加载。**去重/读档安全**：首个世界状态快照只建基线不播（读档 `SetWorldProgressFromSave` 广播全量、中途加入收全量都不重播历史）；每节拍持久去重——播完经本机 Controller 既有 `SetWorldStateTag` 服务器路径写 `Narrative.Played.<BeatId>`（开场白用显式 `Narrative.Intro.Played`），读档后据世界标签跳过。**GameStart 开场白**单独处理（非 delta，本机就绪即按 PlayedTag 判一次）。**演示节拍接现有事件**（不动 Loop-A）：开场白 + 电梯 `CompleteTravel` 写 `Site.FirstDescent`/`Site.ReturnedToS0`、`ACSLootContainer::TryTakeItem` 首次成功搜刮写 `Site.FirstSearch`（均服务器权威、幂等）。**网络**：状态权威仍在 `WorldProgress`，AIC 文本各客户端本机据复制状态演出，本单元零新增播放 RPC；全程事件驱动无 Tick。**人物形态=两者都要**：本单元只做 AIC 旁白，`Speaker` 字段已预留 Varga/Hale，实体 NPC/分支对话留 Unit 64；过场动画/Sequencer 留 Unit 65。无新增 Build.cs 模块。 | 2026-06-08 通过（PIE：AIC 开场白/下潜/搜刮字幕按事件正常播出） |
| 66. Appearance-B 换装面板 UI | 完成（2026-06-08 通过：J 开面板换装实时生效、高亮、Unwear/Reset、互斥正常；补修 Close/Esc/J 关闭——改走 `RequestCloseWardrobePanel` 延迟一帧关，避免在控件自身事件里同帧销毁自己） | 把 Appearance-A 的换装从控制台命令升级为**正式换装界面**（独立面板 + 热键，用户拍板）。新增 `SCSWardrobePanelWidget`（Slate `SCompoundWidget`，右侧面板，`J` 键开关、`Esc`/`J` 关）：按槽位（Head/Torso/Feet/Headgear/Backpack…仅展示有可穿件的槽）列出 `AppearanceDefinitions` 里的可穿件按钮，点击穿；每槽带「Unwear」脱按钮；行内实时显示+高亮当前已穿（`Text_Lambda`/`ButtonColorAndOpacity_Lambda` 读 `GetWornItemId`）；底部「Reset to Default」「Close」。**后端复用 Appearance-A 服务端权威穿戴流**——按钮调控制器 `CSWearAppearance`/`CSUnwearAppearance`/`CSResetAppearance`（→ `Server*` RPC → 组件 API），UI 纯本地、不直接改组件状态。`ACSPlayerController` 加 `ToggleWardrobePanel`/`CloseWardrobePanel`/`Create/RemoveWardrobePanel` + `bWardrobePanelOpen`/`WardrobeWidget`，`J` 绑定，与背包/GM/联机面板互斥（互相开时关对方），纳入 `ApplyMenuInputMode`（焦点+鼠标）/`CloseOpenPanels`/`EndPlay` 清理。**配套数据/组件改进**：`FCSAppearancePart` 加 `SourceItemId`（穿戴时记录来源物品，UI 高亮用、并为 Step 2 存档铺路）；默认套装从构造里写死 3 条网格路径改为 `DefaultOutfitItemIds`（`{Apparel_Head_Default,Apparel_Overall,Apparel_Sneakers}`，服务端 BeginPlay/Reset 按 ID 查表穿戴，默认件也带 SourceItemId、更数据驱动）；`UCSCharacterAppearanceComponent` 加 `GetWornItemId`、`UCSItemDataSubsystem` 加 `GetAllAppearanceDefinitions`。**非目标（留后续）**：穿戴状态存档持久（Step 2，下一个，挪到 PlayerState）；opacity 遮罩防穿模；CJK 字体（槽名/物品名暂用英文标签，默认字体可渲染）；按背包拥有过滤（当前列出全部已知外观，非"仅拥有"）。 | 待验证（Listen Server + 客户端：①`J` 开换装面板、列出各槽可穿件；②点击换装实时生效、当前件高亮、Unwear/Reset 正常；③`Esc`/`J` 关、与背包/GM 面板互斥；④客户端开面板换装也经服务器生效、两端一致） |
| 64. Narrative-C 可对话 NPC | 待用户验证（C++ 编译通过：`CoopSurvival` 运行 + `CoopSurvivalEditor` 编辑器目标 2026-06-08 均整编通过；待 PIE） | "两者都要"的**实体 NPC** 半边:走近 → 世界空间「E 交谈」提示 → 按 E → NPC 在底部字幕说话。**几乎全复用 Narrative-A**(交互/提示/字幕队列/数据管线),不造新系统。**会话模型(可扛很多对话 + 随剧情变化)**:两张表 `Data/Source/Conversations.csv`(`ConversationId/OwnerNPCId/Speaker/RequiredTags/ForbiddenTags/SetTagsOnEnd/Priority`) + `ConversationLines.csv`(`ConversationId/LineIndex/Text/LineSeconds`,**每行一句**便于管理/diff),经 `GenerateGameData.py` 校验生成;导演加载到 `ConversationsById`。新增 `ACSNarrativeNPC`(`AActor`+`ICSInteractable`,纯触发器):`UCapsuleComponent`(根,**Block ECC_Visibility** 才能被交互聚焦+LoS 发现,顺带挡路)+`USkeletalMeshComponent`(编辑器指定外观)+`UCSInteractionPromptComponent`(头顶 E 提示);属性 `NPCId`/`InteractPromptText`(默认「交谈」)。`Interact_Implementation`(服务器,照搬 workbench→`ClientOpenCraftingStation` 模式)→ `ACSPlayerController::ClientPlayNPCConversation(NPCId)`(新 Client RPC,仅发交谈者)→ 本机导演 `PlayNPCConversation`:`PickEligibleConversation`(按 `OwnerNPCId`+当前世界状态:RequiredTags 全在/ForbiddenTags 全不在,取 `Priority` 最高)→ 拆句入队(说话人=会话 `Speaker`)→ 把 `SetTagsOnEnd` 挂到**最后一句**上,该句播完经 Controller 既有 `SetWorldStateTag` 服务器路径写入(**谈完才写、接任务系统**)。导演通用化:`FCSNarrativeQueuedLine` 改存解析后的 `FText SpeakerText`(AIC 节拍/NPC 共用);`RequestPersistPlayedTag`→`RequestSetWorldStateTag`(节拍已播标记 + 会话标签共用)。**对话可反复触发**(无 PlayedTag 去重);**世界状态标签管理**=约定命名(`Site.*`/`Narrative.*`/`Talked.*`/`Obj.*`)+ 文档登记,不做注册表校验。示例:`NPC_Varga` 三段会话(Intro→谈完写 `Talked.Varga.Intro`;下潜后 `Site.FirstDescent` 解锁 AfterDescent;Idle 兜底),演示同一 NPC 随世界状态说不同的话。**不做(留后续)**:分支选择/对话树、专用对话框+头像、NPC AI/转头、语音。无新增 Build.cs 模块。 | 待验证（Listen Server + 客户端：①走近 NPC 出现「E 交谈」、按 E 出现 NPC 名+台词字幕顺序播放；②可反复交谈；③对话只在交谈者屏幕、不串队友、客户端发起也生效；④首次与 Varga 谈完后再谈内容变化(Talked.Varga.Intro)；下潜过后再谈触发 AfterDescent；⑤NPC 挡路/可被聚焦） |
| 65. Narrative-Voice 配音播放与同步 | 待用户验证（C++ 编译通过：`CoopSurvival` 运行 + `CoopSurvivalEditor` 编辑器目标 2026-06-08 均整编通过；待 PIE） | 叙事配音的**运行时播放层(消费者)**:让 63/64 的字幕能配语音、字幕按音长走、多人同步;音频从哪来不管(手配或 Unit 68 的 TTS 生成都行),**无音频时优雅降级为纯文字**(不回归 63/64)。**按 key 自动对接**:每句台词推导资产路径 `/Game/Audio/VO/<key>`(节拍 `<BeatId>_<行序>`、会话 `<ConversationId>_<LineIndex>`),`FSoftObjectPath::TryLoad` 命中即播(复用 `CSCombatComponent` 的 `TryLoad`+`PlaySound` 范式),否则纯文字。**AIC 旁白=2D 本机**(各端各自据复制状态触发,天然同步、零新增 RPC);**NPC 对白=3D 多播**(本机导演→`ACSPlayerController::ServerPlayNarrativeVoice`→`ACSNarrativeNPC::MulticastPlayVoice` 全端在 NPC 处 `PlaySoundAtLocation`,队友同步);`ACSNarrativeNPC` 改 `bReplicates=true`、`ClientPlayNPCConversation` 带上 NPC actor。字幕计时:命中用 `USoundBase::GetDuration()`,否则 `LineSeconds`。`FCSNarrativeQueuedLine` 加 `LineKey`+`VoiceSourceActor`。**网络/权威**:状态权威不变,配音纯表现;Server/Multicast 仅在该句有配音时触发(频率低、Reliable);无 Tick。**打包注意**:按字符串路径加载的 `/Game/Audio/VO` 需加进 cook 目录(PIE 不受影响)。无新增 Build.cs 模块。配音的**生成**(CosyVoice2 本地管线、按 key 产出资产)是 Unit 68。 | 待验证（Listen Server + 客户端：①手丢一个 SoundWave 到 `/Game/Audio/VO/` 命名成某句 key(如 `Beat_Intro_0`/`Conv_Varga_Intro_0`)→ 该句出声、字幕按音长;②NPC 那句双端都在 NPC 处听到 3D、字幕同步;③没放音频的句子仍纯文字按 `LineSeconds`(不回归 63/64)） |
| 67. Appearance-C 穿戴外观持久化 | 完成（2026-06-08 通过：存档/读档外观保留、各端一致） | 把换装的**穿戴状态从 pawn 组件搬到 PlayerState 权威 + 接存档**，让换装跨死亡重生与读档保留（也是通往"不同服装不同效果"装备系统的权威地基）。**权威迁移**：`UCSInventoryComponent`（挂 PlayerState）新增复制 `WornAppearanceItemIds`(ReplicatedUsing=`OnRep_WornAppearance`) + 委托 `OnWornAppearanceChanged` + 服务端 API `WearAppearanceItem`/`UnwearAppearanceSlot`/`ResetAppearanceToDefault`/`SetWornAppearance`（按外观槽去重，槽由 `AppearanceDefinitions` 查 `GetAppearanceSlotType`）；服务端 BeginPlay `EnsureDefaultAppearanceInitialized` 首套默认套装（`bWornAppearanceInitialized` 区分"新玩家空"vs"脱光成空"，避免脱光被重置）。**外观组件改纯表现**：`UCSCharacterAppearanceComponent` 去掉自身复制/服务端权威 API（删 `ActiveParts` 复制 + `OnRep_ActiveParts` + `EquipAppearanceItem`/`EquipAppearancePart`/`ClearAppearanceSlot`/`SetOutfit`/`ResetToDefaultOutfit`/`DefaultOutfitItemIds`/`BeginPlay` 播种），改为 `RebuildFromWornList(穿戴ID列表)`（`BuildPartFromItem` 查表→按槽唯一→`RebuildAppearance`），`ActiveParts` 退化为不复制的本地表现缓存。**事件接线**：角色 `BindPlayerStateEvents` 增订 `OnWornAppearanceChanged`→`HandleWornAppearanceChanged`→`AppearanceComponent->RebuildFromWornList(库存->GetWornAppearanceItemIds())`（与 `OnEquippedItemChanged` 同生命周期，各端一致）。**穿戴流改道**：`ACSPlayerController` 的 `WearAppearanceOnServer`/`UnwearAppearanceOnServer`/`ResetAppearanceOnServer` 改调 PlayerState 库存组件（不再调 pawn 外观组件）；exec/面板入口与回显不变。**存档**：`FCSPlayerSaveData` 加 `WornAppearanceItemIds`，save 写 `Inventory->GetWornAppearanceItemIds()`、load `Inventory->SetWornAppearance(...)`（非空才覆盖，旧档不至于读成裸）。换装面板/`GetWornItemId` 不变（读本地缓存，镜像穿戴列表）。**非目标**：opacity 遮罩防穿模；装备效果系统（护甲/Buff，前置已就位=穿戴状态在 PlayerState）。 | 待验证（Listen Server + 客户端：①换装后**死亡重生**外观保留；②`SaveCoop`→改外观→`LoadCoop` 回到存档时的外观；③退出重进读档外观保留；④主机/客户端互看一致、客户端换装也持久；⑤旧存档读入不变裸、显示默认套装） |
| 68. Narrative-Voice-Gen 配音生成管线（CosyVoice2 中文 + Kokoro 英文） | 已实跑通（2026-06-09 用户机器装通并出资产；游戏音频=英文→英文实际用 Kokoro，CosyVoice2 留作中文本地化） | Unit 65 播放层的**生产端**:本地 CosyVoice2 按台词自动生成配音、按 key 自动对接,改文本才重生成(增量),无需 per-line 手配。**引擎不进工程**(用户拍板):CosyVoice2 仓库+Python 环境+模型(几 G)+中间 wav/清单都在**工程外通用位置**(例 `F:\Tools\CosyVoice2`),以本地 HTTP 服务跑;工程只留瘦客户端 + 配置 + 最终音频资产。**契约=按台词 key 推导路径** `/Game/Audio/VO/<key>`(节拍 `<BeatId>_<行序>`、会话 `<ConversationId>_<LineIndex>`,与运行时一致)。**工程内交付**:`Data/Source/VoiceProfiles.csv`(每角色一行:`SpeakerId`(节拍 Speaker / 会话 OwnerNPCId)/`Mode`(preset|clone|instruct)/`Voice`/`RefText`/`Instruct`/`Speed`,**只给管线读、不进运行时**);`Scripts/voice_pipeline_common.py`(stdlib:读配置/CSV、算 key+sha1、增量 manifest、POST 外部服务);`Scripts/GenerateVoiceLines.py`(增量批量生成 wav 到工程外 work_dir);`Scripts/AuditionVoices.py`(候选音色各念样例→你听着挑、零素材);`Scripts/ImportVoiceLines.py`(UE 编辑器 python `-run=pythonscript`,按 manifest 增量把变更 wav 导成 `/Game/Audio/VO/<key>` SoundWave);`Scripts/voice_pipeline.local.example.json`(本机配置样例)+`.gitignore` 忽略 `voice_pipeline.local.json`/`Data/Voice/`;`Docs/Development/VoicePipeline.md`(装/跑说明)。**直接对接 CosyVoice 官方 FastAPI server**(`runtime/python/fastapi/server.py` 的 `/inference_sft|/inference_zero_shot|/inference_instruct2`),工程不自建适配服务(已据仓库核对 API);官方 server 返裸 16bit PCM,客户端据 `sample_rate` 包 wav。**CosyVoice2-0.5B 无内置音色**——音色靠 clone 一段参考 clip(可用仓库 `asset/*.wav` 或自录);要零素材内置音色须改跑 `CosyVoice-300M-SFT` 的 preset。**分工**:脚本/数据/文档我交付且 `py_compile` 通过;装 CosyVoice2+下模型+起服务+试听挑音色+跑生成/导入+PIE 听效果由用户做(我无音频/无 GPU 访问,替不了)。**隐私**:全本地、台词不出本机。**打包**:`/Game/Audio/VO` 需加进 cook 目录。**不做**:口型同步、VOIP、多语言、cook 目录自动化。 **实跑结果(2026-06-09)**:CosyVoice2 装通(过了一堆 Windows 坑:conda ToS→conda-forge、env 无 pip、openai-whisper 的 pkg_resources→`setuptools<81`、5080 需 cu128 torch、torchaudio≥2.9 的 `load_wav` 改 soundfile+兼容张量、GitHub/HF 国内走镜像)。但**中文模型念英文不自然**→英文改用 **Kokoro**(英文原生、内置音色、in-process,`Scripts/GenerateVoiceLines_Kokoro.py` + `AuditionVoices_Kokoro.py`,HF 走 hf-mirror)。叙事文本已全改英文。14 句英文 VO 已导入 `/Game/Audio/VO/`(AIC=`am_michael`、Varga=`bf_emma`,**干净无特效**——对讲机/广播/隔门/水下等属运行时 UE 音频系统、另立单元、不烘进 wav)。CosyVoice2 保留给中文本地化。 | 已出资产,待 PIE 听（AIC 开场白/Varga 对白按 key 自动出声、字幕跟音长、NPC 双端 3D 同步;改台词/换音色重跑 Kokoro generate→import 增量） |
| 69. Attribute-A 属性快照与负重 | 待用户验证（C++ 编译通过：`CoopSurvival` 运行 + `CoopSurvivalEditor` 编辑器目标 2026-06-08 均整编通过；待 PIE） | **装备效果系统的地基（第一单元）**：定义玩家属性（明面/隐藏）并落地属性快照组件 + 负重。**属性清单（系统契约）**：明面=生命/体力/饥饿/口渴（沿用 `UCSSurvivalStatsComponent`）+ 负重 `Load`/`MaxLoad` + 护甲 `ProtectionPhysical`；隐藏（**仅战斗乘区**，用户拍板）=`ProtectionBallistic`/`Stability`/`MoveSpeedScalar`/`StaminaRegenScalar`；SCP 认知层（AIMStability/污染/孢子抗性/Focus）暂不进结构，留到敌人污染/AIM 系统再加字段。**新增 `UCSAttributeComponent`**（挂角色、与生存组件同位、`SetIsReplicatedByDefault`）：`FCSAttributeSnapshot` 复制快照（ReplicatedUsing=`OnRep_Snapshot`）+ 委托 `OnAttributeSnapshotChanged`；服务端 `RecalculateAttributes` 按设计稿 §5.2 计算顺序 **基础 → 装备修正（空钩子 `ApplyEquipmentModifiers`）→ Buff 修正（空钩子 `ApplyBuffModifiers`）→ 派生（`Load`=Σ背包重量）→ 负重→移速倍率 → Clamp**；客户端只读复制快照，不本地重算权威值。**库存接线**：`UCSInventoryComponent` 加 `OnInventoryStacksChanged` 委托（`AddItem`/`RemoveItem`/`SetInventoryStacks`/`OnRep_InventoryStacks` 均广播），属性组件经角色订阅它重算负重（权威只在服务端）。**角色接线**：挂 `AttributeComponent`，`BindPlayerStateEvents` 增订库存变更（服务端→`RecalculateAttributes`）+ `OnAttributeSnapshotChanged`（→`UpdateMovementSpeed`）；`UpdateMovementSpeed` 末端乘 `MoveSpeedScalar`（各端从复制快照读，保持移动预测一致）；超重（`IsOverEncumbered`）在 `SetSprintingLocal` 禁冲刺。**负重模型**：`Load`=Σ `Items.csv` `Weight`×数量（**复用现有 Weight 字段**，零数据改动），超 `BaseMaxLoad`(25kg) → `OverweightMoveSpeedScalar`(0.55) 降速 + 禁冲刺。**UI（明面+隐藏都给可视，用户要求"做完的都要有对应 UI"）**：①主 HUD——`SCSHUDWidget` 加负重条（`ECSHUDBarKind::Load`，超重红 + `!`）。②**背包面板属性侧栏**（用户拍板 UI 融入背包面板）——`SCSInventoryPanelWidget` 体部改 3 列、加 `BuildAttributePanel`：明面（生命/体力/饥饿/口渴/负重/护甲）+ 隐藏战斗乘区（防弹/Stability/移速倍率/体力恢复倍率），值用 `Text_Lambda`+Mono 字体逐帧读 HUD 快照**实时刷新**（不依赖签名重建），超重时负重行整行变红 + `OVER`。`FCSHUDSnapshot` 相应加 `Load`/`MaxLoad`/`bOverEncumbered`/`ArmorProtection`/`ProtectionBallistic`/`Stability`/`MoveSpeedScalar`/`StaminaRegenScalar`，控制器从属性快照填。护甲/隐藏值本单元基础恒 0/×1（无护甲数据/Buff 修正），数值由 Armor-A/Buff 接入后自然变化（UI 已就位、不用再改）。**与武器表现完全同构**：权威+复制在组件，各端读复制快照驱动表现（移速），跨重生/读档天然一致（快照服务端按当前背包重算）。**非目标（留后续单元）**：①Armor-A——装备/护甲修正（读 PlayerState 穿戴 `WornAppearanceItemIds`/装备槽 → `ArmorDefinitions` → `ProtectionPhysical/Ballistic/Stability/MaxLoad/MovementPenalty`，**接上换装=不同服装不同效果**）；②Buff→属性修正（读 `UCSStatusEffectComponent` 的 `AttributeModifiers`）；③伤害路径减伤（`FCSDamageContext` 走护甲/抗性）；④`MaxHealth`/`MaxStamina`/`StaminaRegen` 从生存组件迁到快照；⑤SCP 认知层属性；⑥**网格背包（塔科夫式，独立大工程，用户拍板的下一阶段；负重沿用本单元，宽×高占格另起）**。 | 待验证（Listen Server + 客户端：①背包加物品→HUD 负重条上升、显示 `Load/MaxLoad kg`；②塞够重的东西超 25kg → 负重条变红 + `!`、移速明显变慢、按住冲刺无效；③丢弃/搜刮物品负重条实时变；④主机/客户端负重条一致、客户端自己也对（快照复制）；⑤死亡重生/读档后负重随背包正确重算；⑥打开背包(Tab/I)右侧 "Attributes" 侧栏显示完整明面+隐藏属性、各值实时刷新、超重时负重行变红 `OVER`） |
| 70. Grid-A 网格背包（三角洲式）数据模型+自动摆放+只读网格 | 待用户验证（C++ 编译通过：`CoopSurvival` 运行 + `CoopSurvivalEditor` 编辑器目标 2026-06-09 均整编通过；待 PIE） | 装备/属性路线图**阶段②（网格背包）首单元**，用户拍板三角洲行动/塔科夫式（占格+旋转+快速丢弃）。**核心策略：网格是叠在"逻辑物品集"之上的摆放层，不推翻数量 API**——`AddItem`/`RemoveItem`/`HasItemQuantity`/`GetItemQuantity` 保持可用，原 14 处调用方基本不动。**物品占格数据**：`Items.csv` 加 `GridWidth,GridHeight`（小件 1×1、手枪 2×1、霰弹枪 4×1、斧 1×3、背包/防化 2×3 等），`GenerateGameData.py` 加两列整数校验，`FCSItemDefinition` 加占格，`UCSItemDataSubsystem` 加 `GetItemFootprint`（未知回退 1×1）。**摆放层**：`FCSInventoryStack`（住 `CSPlayerState.h`，复制+存档）加 `GridX/GridY/bRotated`（-1=未摆放），并把全部字段标 `SaveGame`（原 ItemId/Quantity 漏标，与弹匣结构对齐、保证摆放持久）。**`UCSInventoryComponent` 服务端权威网格逻辑**：固定网格 `BackpackGridWidth/Height`=10×6（Grid-C 改穿戴背包驱动）；占用图构建 + `PlaceStackAutomatically`（先并堆进同物品未满栈→余量首位匹配，先不旋转后旋转）；**`AddItem` 原子化**（并堆+逐占格摆放，任一放不下则整次回滚返 false——调用方据"false=没发生"把物品留世界/容器，这就是"背包满（无空间）"拒绝，与重量双限制并存）；**`GetItemQuantity` 改跨同物品项求和**（溢出后同物品跨多占格，否则合成/装备校验少算，必修）；`RemoveItem` 跨项扣减（扣空占格移除）；`SetInventoryStacks`（读档/迁移同一路径）逐项摆放——尊重合法坐标否则自动摆放、按 MaxStack 切分溢出、**放不下留未摆放但不丢物**；预埋 `MoveStackTo`/`RotateStackInPlace`（权威+校验，Grid-B 接 UI）。**控制器** 加 `RequestMove/RotateInventoryStack`→`ServerMove/Rotate*`(Reliable)→`*OnServer`→组件（照搬 `RequestDropItem` 链，现在就声明、Grid-B 才有控件调）；`BuildHUDSnapshot` 填网格尺寸。**存档** `CurrentSaveVersion` 6→7（摆放惰性在读档 `SetInventoryStacks` 完成，v6→v7 仅版本标记）。**UI（背包面板）**：`SCSInventoryPanelWidget` 加**只读 W×H 网格**（`SConstraintCanvas` 绝对定位：背景格 + 已摆放物品按占格/旋转放置、数量 `xN` 角标，跳过未摆放）+ 保留**紧凑 Equip/Drop 列表**作 Grid-A 操作入口（格内右键留 Grid-B）；`FCSHUDSnapshot` 加网格尺寸，`BuildSignature` 含 `GridX/GridY/bRotated`+尺寸 → 纯移动/旋转也触发重建。**非目标（Grid-B 起）**：拖拽移动、原地旋转(R)、三角洲式快速丢弃（后端 Move/Rotate 接口+RPC 已预埋，Grid-B 纯接 UI）；容器/尸体网格化（Grid-C）；穿戴背包驱动网格尺寸（Grid-C）；物品实例 FGuid（Grid-D）。 | 待验证（Listen Server + 客户端：①拾取物品→按占格填进网格、大件占多格；②背包塞满→再拾取被拒、物品留世界/容器；③可堆叠物超 MaxStack→溢出成新占格；④合成/装备校验数量正确（跨溢出项求和）；⑤丢弃/搜刮/采集后网格实时更新、主机与客户端网格一致；⑥`SaveCoop`→`LoadCoop` 摆放保留；**旧存档**读入不丢物、装不下的进紧凑列表可见可丢；⑦负重(Load)与空间双限各自生效） |
| 71. Grid-B 网格背包交互（拖拽/旋转/快速丢弃） | **PIE 验证通过**（2026-06-09，含拖拽落点高亮、按 R 旋转）。修复：①移除跟随光标装饰物、落点全靠网格内对齐高亮（DPI 准）；②**按 R 旋转转置抓取偏移 `Swap(GrabCellX,GrabCellY)`**（对合，落点贴光标，不再飞走）+ 当帧按光标即时刷新高亮（`RefreshDragHighlightForOp`）。坑见 `UE_Gotchas.md §1.2/§1.3`。 | 阶段②网格背包第二单元，三角洲行动式交互层。**纯 Slate UI**——后端 `MoveStackTo`/`RotateStackInPlace`/`DropItem` 服务端权威接口 + 控制器 `RequestMove/RotateInventoryStack` RPC 在 Grid-A 已预埋，本单元只接交互。新增 `UI/SCSBackpackGridWidget`（自带 W×H 渲染的 `SConstraintCanvas` + Slate 拖放：`OnMouseButtonDown`/`OnDragDetected`/`OnDragOver`/`OnDrop`）+ **`FCSInventoryGridDragDropOp`**（拖放操作，携带条目索引/物品/占格/旋转/抓取偏移；跟随光标的幽灵装饰物**大小与合法色用 `TAttribute::CreateLambda` 绑定本对象可变状态** → 拖动中旋转、悬停合法性变化即时反映、无需重建装饰物）+ `SCSDiscardDropZone`（丢弃区）。**交互**：①LMB **拖拽移动**（记录抓取偏移避免物品跳到光标，落点=鼠标格−偏移）→ `RequestMoveInventoryStack`；②**拖动中 R 旋转**（背包面板 `OnKeyDown` 查 `FSlateApplication::Get().GetDragDroppingContent()` 命中本拖放类型 → 切 `bRotated`，幽灵即时变向）；③RMB **原地旋转** → `RequestRotateInventoryStack`（服务端校验旋转后仍放得下）；④拖到 **Discard 区** → 整栈 `RequestDropItem` 丢世界。悬停做**客户端近似占用校验**（绿/红幽灵预览），**服务端落点权威再校验**（`MoveStackTo` 非法则不变）。命中测试用拖动期间稳定的缓存（`RefreshGrid` 在 HUD 快照签名变化时刷新）+ 格坐标数学（`AbsoluteToLocal`→floor/CellPx）。背包面板把 Grid-A 的只读网格替换为本交互控件 + 丢弃区。**坑**：`FDragDropOperation::Construct()` 是 protected，派生类加公开 `Initialize()` 包装调它（建立跟随光标的装饰窗口）。**非目标**：容器/尸体网格化 + 穿戴背包驱动尺寸（Grid-C）、物品实例 FGuid（Grid-D）。 | 待验证（Listen Server + 客户端：①拖物品到空位→移动生效；②拖到被占/越界→红幽灵 + 松手不动（服务端拒）；③拖动中按 R 旋转→能塞进窄位、绿幽灵；④右键物品→原地旋转 90°；⑤拖物品到 Discard 区→整栈丢到世界；⑥主机/客户端拖拽结果一致、客户端拖拽也经服务端生效） |
| 72. Audio-FX-A 运行时音频特效（声音通道 + 隔门遮挡） | 已出脚手架（`CoopSurvivalEditor` 编辑器目标 2026-06-09 整编通过；FX 资产已建、VO 路由已跑通并验证；待用户 PIE 听 + 编辑器调参） | 游戏级**运行时音频特效层**的第一单元（Phase 1，用户拍板=声音通道+隔门遮挡）。**VO/音效资产保持干净**——效果不烘进 wav，在引擎里按"通道+环境"叠（动态/看情境/可编辑器实时调）。现状：工程原本无任何 Sound Class/Submix/效果链/遮挡，声音全是 fire-and-forget `PlaySound2D`/`PlaySoundAtLocation`。**开 Synthesis 插件**（`CoopSurvival.uproject`，submix 效果预设在它里面；Build.cs 不必加模块——通道靠资产路由、遮挡靠 attenuation 数据）。**FX 资产**（`/Game/Audio/FX/`，`Scripts/CreateAudioFX.py` 编辑器 python 建、给默认参数）：`SM_VoiceBroadcast`（大型广播/PA，效果链=`SubmixFX_BroadcastFilter` **带通 1.2k/Q0.9**（治"假/发闷"，砍 <300Hz 隆隆与 >3.5k fizz、只留喇叭中频）+ `SubmixFX_BroadcastComp` 压缩 + `SubmixFX_BroadcastReverb` 混响）、`SM_VoiceRadio`（对讲机备用，带通 1.8k + 压缩）、`ATT_VoiceOccluded`（`USoundAttenuation`：occlusion 低通 500Hz/音量 0.5/插值 0.15s，遮挡 trace=**Visibility**——门/墙默认 Block 它即触发）。**广播质感**（用户反馈"声音假、要刺啦噪音"补做）：`Scripts/GenerateBroadcastFX.py`（纯 stdlib 合成 wav→`Saved/AudioFXGen`）产 `SW_BroadcastStatic`（无缝循环静电底噪+爆豆 crackle）/`S_PAKeyOn`（开麦"咔—嗞"）/`S_PAKeyOff`（关麦），`CreateAudioFX.py` 导入后全路由进 `SM_VoiceBroadcast`；导演 `UCSNarrativeDirectorSubsystem` 在 AIC 2D 广播台词（`VoiceSourceActor` 空）起止时 `BeginBroadcastAmbience`/`EndBroadcastAmbience`——开头垫循环底噪+开麦声、结尾淡出+关麦声，连续多句节拍只开/关一次，事件驱动无 Tick、资产缺失静默跳过（失真治不了连续静电，故走独立噪声音源）。**WATCHMAN≠基金会PA 重定（2026-06-09，依 SD_46）**：用户指出 AIC-WATCHMAN 是"对你一个人说话"的角色（玩家唯一接口、必须清楚），不该做成通用大喇叭——遂拆成两个声音身份。新建 **`SM_Watchman` 通道**（设施喇叭感=轻低通 7k + 压缩 + 轻房间混响，清晰为红线；剧情阶段失真/变质留 SD_46 §10 在该 submix 叠加），AIC 的 `AudioChannel` 由 `broadcast`→`watchman`、VO 重路由到 SM_Watchman；导演 AIC `PlaySound2D(...,1.5f)` 给 WATCHMAN 默认增益（Kokoro 输出偏轻，音量另可经 SM_Watchman 输出/各 VO 资产调）。静电底噪+PA 开关声归入**基金会站内 PA 环境层**（`SM_VoiceBroadcast`，收容警报/疏散/封锁等无人格播报）——导演 `BeginBroadcastAmbience`/`EndBroadcastAmbience` 由 `bFoundationPALine` 门控、**当前恒 false 不挂任何台词**，留给将来的 PA 内容层。脚本对缺失效果类/字段名 `getattr` 守卫 + try/except（跨引擎版本不崩）；Distortion 是 source 效果不是 submix，已跳过。**通道路由（数据驱动，接 Unit 65 的 VO）**：`Data/Source/VoiceProfiles.csv` 加 `AudioChannel` 列（`broadcast`/`radio`/`direct`；AIC=broadcast、NPC_Varga=direct）；`Scripts/ImportVoiceLines.py` 导入/重跑时按说话人 channel 给其 VO 资产设 `SoundSubmixObject`（broadcast→SM_VoiceBroadcast、radio→SM_VoiceRadio、direct→不设走 master）——2D/3D 都生效、导演 C++ 不用改、改 channel 重跑只重路由不必重生成。**已验证**（编辑器 commandlet 查资产）：Beat_*（AIC）→ SM_VoiceBroadcast、Conv_Varga_*（Varga）→ None。**隔门遮挡（C++ 极小改动）**：`ACSNarrativeNPC` 加 `TObjectPtr<USoundAttenuation> VoiceAttenuation`，`MulticastPlayVoice` 把它传给 `PlaySoundAtLocation`（空=默认）；赋 `ATT_VoiceOccluded` 后 3D 语音被门/墙阻挡自动低通变闷。AIC 是 2D 广播→不空间化不遮挡（符合广播无视距离/遮挡）。**分工**：脚手架（插件/资产/路由/代码，效果默认参数）我交付且编辑器整编通过；**编辑器按耳调各 Submix 效果参数（带通/压缩/混响）+ 给会隔音的门/墙补 Visibility 碰撞 + 把 ATT_VoiceOccluded 赋到 NPC 的 VoiceAttenuation + PIE 听**由用户做（我无音频访问）。文档 `Docs/Development/AudioFX.md`。**非目标（Phase 2/以后）**：水下 Submix（入水切低通+混响，需 `AudioMixer` 模块 + 关卡水体/触发）、混响区（Audio Volume）、对讲机静电/squelch/开关咔哒（MetaSound）、枪声/脚步遮挡、ducking/动态混音。 | 待验证（Listen Server + 客户端：①进游戏听 AIC 开场白/节拍→**清楚的"设施喇叭对你说话"**（轻混响、不发闷不刺啦），音量合适（默认 ×1.5，可调），Varga 当面说话干净；②玩家与 Varga 间隔一道有碰撞的门/墙→Varga 声音变闷（occlusion 低通），走开恢复清晰；③改 VoiceProfiles 某说话人 AudioChannel→重跑 ImportVoiceLines→路由变化；④编辑器调 `SM_Watchman` 音色/输出音量→WATCHMAN 随调变化；⑤队友也能听到 NPC 3D 语音、有方位） |
| 73. Grid-C1 穿戴背包驱动网格尺寸 | 待用户验证（C++ 运行时 + 编辑器双目标 2026-06-09 整编通过；待 PIE） | 阶段②网格背包第三单元（Grid-C 上半）：背包网格从**固定 10×6** 改为**穿戴的 Backpack 槽物品驱动**——兑现"不同装备不同容量"。**数据**：`AppearanceDefinitions.csv` 加 `BackpackGridWidth/BackpackGridHeight` 列（仅 Backpack 槽有效，0=不扩容），`Apparel_Hiking_Backpack`=10×6；`FCSAppearanceDefinition` 加两字段，`GenerateGameData.py` 校验（≥0），子系统 `LoadAppearanceDefinitionsCsv` 解析。**库存组件**：`BackpackGridWidth/Height` 由固定 `EditDefaultsOnly` 改为**派生缓存**（各端从复制的 `WornAppearanceItemIds` + 本地外观数据各自算，**不复制**）；新增 `BaseBackpackGridWidth/Height`=6×4（EditDefaultsOnly，无背包的"口袋"基础尺寸）。`RecomputeEffectiveGridSize`（遍历穿戴找 Backpack 槽物品的 `BackpackGrid*`，无则回退基础）；`ReflowGridPlacement`（**尺寸变化后重排**：原位仍放得下则保留，否则自动摆放，再不行留未摆放不丢物——**变大自动补放原先未摆放的项；变小溢出项留未摆放**）；`HandleWornChangedForGrid`（统一入口：重算尺寸，**尺寸真变了且权威端**才重排——换帽子不动背包就不动摆放）接入所有穿戴变化路径（`WearAppearanceItem`/`Unwear`/`Reset`/`SetWornAppearance`/`EnsureDefaultAppearanceInitialized`/`OnRep_WornAppearance`，客户端只重算渲染尺寸、不重排）。HUD/网格 getter 读缓存 → 各端渲染正确尺寸。**非目标**：容器/尸体网格化 + 跨网格拖拽（Grid-C2）、物品实例 FGuid（Grid-D）；**未摆放项暂只能在紧凑列表见/丢，不能拖回网格**（Grid-B 拖拽限格内，Grid-C2 补跨容器拖拽时一并解决）。 | 待验证（Listen Server + 客户端：①默认（无背包）网格 6×4；②GM 给 `Apparel_Hiking_Backpack`→换装面板（J）穿上→网格变 10×6、原有物品保留、原先放不下的自动补进新空间；③脱下背包→网格回 6×4、装不下的物品进紧凑列表不丢失；④主机/客户端网格尺寸一致；⑤存档→读档后网格尺寸随穿戴正确） |
| 74. Armor-A 护甲与负载（装备→属性→伤害减免） | 待用户验证（`CoopSurvivalEditor` 编辑器目标 2026-06-09 整编通过；待 PIE） | 装备/属性路线图**阶段③（装备效果）**：把穿戴的护甲数值真正接进属性快照与伤害减免，让 Attribute-A 铺好但恒 0 的护甲 UI 活起来。**数据落点=扩 AppearanceDefinitions（用户拍板）**：穿戴件即外观件，复用既有 `WornAppearanceItemIds → AppearanceDefinitions` join，不另起 ArmorDefinitions 表（日后护甲若与视觉解耦——护甲板/实例——再迁出）。`AppearanceDefinitions.csv` 加 6 列 `ArmorPhysical/ArmorBallistic`（减伤**乘区** 0~1）、`StabilityBonus`（平加）、`MaxLoadBonus`（kg）、`MoveSpeedPenalty/StaminaRegenPenalty`（0~1，连乘）；`GenerateGameData.py` 校验（减伤/惩罚走 `require_probability`，加成 `require_float≥0`）；`FCSAppearanceDefinition` 加 6 字段，子系统 `LoadAppearanceDefinitionsCsv` 解析（减伤/惩罚 Clamp 0~1）。给化学服/化学面罩/化学靴/消防头盔/连体服/登山包填有意义值，普通件 0。**属性管线**（`UCSAttributeComponent`）：落地 `ApplyEquipmentModifiers` 空钩子——遍历 `WornAppearanceItemIds` → 取外观护甲 → 防护/稳定平加、`MaxLoad += 加成`、移速/体力恢复按件连乘 `(1-惩罚)`；计算顺序末端把 `ProtectionPhysical/Ballistic` Clamp 到 `MaxDamageReduction`=0.85（防多件全免）。**修管线既有缺陷**：移速倍率原在第 5 步被超重判定**整段覆盖**，改为 `MoveSpeedScalar *= (超重?0.55:1)`——否则装备移速惩罚被冲掉。**穿脱触发重算**：`ACSPlayerCharacter::HandleWornAppearanceChanged` 原只重建外观网格，补服务端 `RecalculateAttributes()`（穿/脱护甲属性才变）。**伤害减免（设计 §7 雏形）**：`UCSAttributeComponent` 加 `GetIncomingDamageScalar(DamageType)`（Ballistic→防弹、其余→物理，返 1-防护）+ 静态 `MitigateDamage(Target, Raw, DamageType)`（找目标属性组件按类型减伤；**无组件=原样返回**，当前敌人不减伤、行为不变，为将来敌人护甲预留同一入口）。接到三处造伤汇点：玩家近战 `ApplyMeleeDamageToTarget`（Physical）、玩家枪械 `ApplyFirearmDamageToTarget`（`Spec.DamageType`）、**敌人近战 `ACSEnemyCharacter::PerformAttackHit`（Physical→玩家护甲真减伤）**。Buff 流血/污染（内部伤害）**不**走护甲。**UI 零改**：属性侧栏 + HUD 的 `ProtectionPhysical` 等已就位，数值从恒 0 自然变活。**非目标（后续）**：污染/孢子/现实抗性（快照无字段，SCP 认知层延后）、护甲耐久、护甲 PassiveBuffIds、完整 `FCSDamageContext` 把 Buff DoT/source 一并上下文化（Combat-B）、物品实例护甲、独立护甲槽。 | 待验证（Listen Server + 客户端：①GM 给消防头盔+化学服→换装面板穿上→属性侧栏 `ProtectionPhysical` 从 0 变 0.25（0.15+0.10）、HUD 同步；②让基准敌人近战打玩家→裸穿 vs 穿甲掉血明显变少（约 ×0.75）；③穿登山包→`MaxLoad` 25→40、移速略降；④脱下护甲→数值回落、减伤消失；⑤主机/客户端属性侧栏一致；⑥存档读档后穿戴护甲数值正确） |
| 75. VG-A 角色声音参数契约 + 数据模型（slice 1：地基，不改播放） | 已落代码（`CoopSurvival` 运行时目标 2026-06-09 整编通过 = Result: Succeeded；编辑器目标整编 + 建表 + PIE 待用户关 Live Coding/编辑器后做） | 角色声音工作流（外部 HTML 节点工具 + UE MetaSound 运行时，见 `Docs/Development/VoiceAuthoringTool.md`）的第一块地基。**slice 1 只落契约 + 数据模型、不动播放**（导演仍走 SoundWave；接播放=slice 2，PIE 可验）。新建 `Source/CoopSurvival/Voice/`：`CSVoiceTypes.h` = 参数契约 `FCSVoiceParams`（7 项：PitchSemitones / FormantShift / ToneTiltDb / PresenceDb / Saturation / GainDb / ReverbSend）+ 每角色 `FCSCharacterVoiceProfile`（DataTable 行：`Channel` + `Base` + 每情绪增量 `EmotionDeltas` + 遮挡 `OcclusionAttenuation` + `SchemaVersion`）+ 枚举 `ECSVoiceChannel`(Direct/Watchman/Broadcast/Radio) / `ECSVoiceEmotion`(Neutral/Calm/Tense/Afraid/Hostile/Weary/Sorrow)；`UCSVoiceProfileSubsystem`（GameInstance 子系统，懒加载 `/Game/Data/DT_CharacterVoiceProfiles`，`ResolveParams(SpeakerId,Emotion)`=Base+情绪增量、`GetChannel`，**表缺失返回默认、不报错**——地基阶段表还没建也能编译运行）。契约是 keystone：UE 运行时 / 未来 MetaSound 输入 / 外部 HTML 工具导出的 JSON / 脚本 共用同一份字段，文档 `Docs/Development/VoiceParamSchema_v0.md`（JSON↔DataTable↔MetaSound 字段一一对应 + 版本 `CS_VOICE_SCHEMA_VERSION=1`）。`Scripts/CreateVoiceProfileTable.py` = create-if-missing 建表 + 种子 AIC(Watchman) / NPC_Varga(Direct + ATT_VoiceOccluded)（需编辑器目标整编出类型后跑）。**情境特效（隔门/广播通道）不进契约**，仍走 Audio-FX-A 的 submix/attenuation 运行时叠加（契约只用 `Channel` 表达路由意图）。**非目标（slice 2+）**：`MS_CharacterVoice` MetaSound 图、导演播放从 SoundWave 迁到 MetaSound、按 profile 驱动 AIC 增益/Varga 清晰度（取代硬编码 1.5f）、情绪列（VG-B）、HTML 工具（VG-C）、情感 TTS（VG-D）。 | 运行时目标整编通过（C++ 语法/链接 OK；先前"ICSLootSource 抽象"报错经查为 OOM 打断整编的假象，独立重编即通过）。**待用户**：①关 Live Coding/编辑器后 `CoopSurvivalEditor` 编辑器目标整编（新增反射类型须关编辑器整编）；②跑 `CreateVoiceProfileTable.py` 建 `DT_CharacterVoiceProfiles` 并种子 AIC/Varga；③编辑器确认 DataTable 行类型 = FCSCharacterVoiceProfile、字段齐全、子系统能查到 AIC/Varga（slice 1 无音频表现变化，按契约/数据存在性验；听感留 slice 2 接播放后）。 |

| 76. Grid-C2a 容器/尸体网格化 + 双网格 + 拖拽取物 | **PIE 验证通过**（运行时 + 编辑器双目标 2026-06-09 整编通过；双网格拖拽取物、堆叠、跨网格图标反馈均确认）。**首轮反馈修复（非反射，Live Coding 热补）**：①可堆叠物从来源拖进背包**不并堆**→`AddItemAtGrid` 改为先并堆进同物品未满栈、余量再放落点（落点被占则自动补位），与 `AddItem` 一致；②跨网格拖拽**光标无图标跟随、发卡**→`FCSInventoryGridDragDropOp::GetDefaultDecorator` 由返回 null 改回**小固定尺寸图标（34px）**（精确落点仍靠网格内高亮；小图标不匹配格子尺寸→无"比原图大/错位"，跨网格拖出给即时反馈、治"发卡"感）。 | 阶段②网格背包第五单元（Grid-C 下半，用户拍板先做 C2a）：把搜刮容器与敌人尸体也网格化，搜刮面板左背包格右来源格并排，从来源拖一件到背包指定格=取物。**① 共享网格工具抽取**：`UCSInventoryComponent` 私有的占用图/首位匹配/自动摆放/重排逻辑提成 `Components/CSGridPlacement`（对 `(TArray<FCSInventoryStack>&, GridW, GridH, ItemData)` 操作的纯几何，不依赖 Actor）：`GetBaseFootprint`/`GetFootprint`/`BuildOccupancy`/`DoesFootprintFit`/`FindFirstFree`/`PlaceAutomatically`/`ReflowAll`。库存组件成员改薄包装委托它（算法逐字搬、行为不变；`ReflowGridPlacement` 也改用 `ReflowAll`），玩家库存/容器/尸体三方共用同一实现。**② 容器/尸体网格模型**：`ICSLootSource` 加 `GetLootGridWidth/Height` + `TryTakeLootItemToGrid`（按下标取整摞到玩家背包指定格）。容器网格尺寸走 `Containers.csv` 加 `GridWidth/GridHeight` 列（`FCSContainerDefinition`+生成器校验+子系统解析，`BasicLocker`=6×6）；尸体走 `ACSEnemyCharacter` EditDefaultsOnly `CorpseLootGridWidth/Height`=5×4。生成战利品/尸体掉落（`EnsureLootGenerated`/`ApplySavedState`/`GenerateCorpseLoot`）后用 `ReflowAll` 把扁平内容摆进网格（旧扁平存档读入自动摆放迁移）。`FCSInventoryStack` 既有 `GridX/GridY/bRotated` 直接复用，随 `RemainingItems`/`CorpseLoot` 复制带坐标到客户端。**③ 拖拽取物 RPC**：`UCSInventoryComponent::AddItemAtGrid(ItemId,Qty,X,Y,bRotated)`（指定格在界内+空闲+Quantity≤MaxStack 才放新摞，否则 false；超摞回退 Take 按钮）；控制器 `RequestTakeLootItemToGrid→ServerTakeLootItemToGrid→TakeLootItemToGridOnServer`（距离校验→`ICSLootSource::TryTakeLootItemToGrid`→`AddItemAtGrid`→成功从来源移除该项、保留其余项格位、回推刷新面板）。保留 Take 1/Take All（自动摆放，处理超摞）。**④ 双网格 UI**：泛化 `SCSBackpackGridWidget`——加 `Source`(PlayerInventory/LootSource) 参数：LootSource 模式从 `FCSHUDSnapshot.OpenLootContainerItems`+`OpenLootContainerGridWidth/Height`（新增，`BuildHUDSnapshot` 从活的来源 Actor 现读）渲染、只读+仅可拖出取物（不接落子、不可 RMB 旋转）。`FCSInventoryGridDragDropOp` 加 `SourceKind`：落到玩家背包格时按来源分流（LootSource→`RequestTakeLootItemToGrid`、PlayerInventory→`RequestMoveInventoryStack`）；玩家背包 `OnDragOver` 按来源决定是否忽略自身占格。丢弃区只接受 PlayerInventory 来源（防把未拥有的战利品当自己物品丢）。背包面板搜刮区加 LootSource 网格 + 改提示"Drag items to your backpack to take"；`BuildSignature`/`RebuildDynamicRows` 纳入战利品网格尺寸+坐标。**非目标（Grid-C2b）**：背包→容器存入、容器内整理、未摆放项(GridX<0)拖回背包网格；物品实例 FGuid（Grid-D）。 | 待验证（Listen Server + 客户端：①搜刮容器→右侧出现网格、物品按占格摆放；②从容器格拖一件到背包空位→取走、落进该格；③拖到放不下处→红高亮、松手留容器；④打死敌人搜尸体→同样双网格拖拽；⑤Take All 仍可用（超摞/兜底）；⑥主机/客户端一致、客户端拖拽经服务端生效；⑦旧存档容器读入不丢物、自动摆放） |

| 77. Grid-C2b 存入 / 容器内整理 / 未摆放项拖回 | 待用户验证（运行时 + 编辑器双目标 2026-06-09 整编通过 = Result: Succeeded；待 PIE） | 阶段②网格背包第六单元，把搜刮搬运闭环补全（紧接 C2a，复用双网格 + 跨网格拖拽框架）。**① 存入（背包→容器/尸体）**：`ICSLootSource` 加 `TryDepositItemToGrid`（按位置把一摞放进来源网格，复用新工具 `CSGridPlacement::PlaceAtPosition`：落点空闲则放、被占自动补位）；容器/尸体实现；库存加 `RemoveStackByIndex`（按下标精确移除被拖那摞、含装备/快捷栏清理）；控制器 `RequestDepositItemToLoot→Server→DepositItemToLootOnServer`（距离校验→读玩家该摞→来源 TryDeposit→成功才移除玩家该摞→回推刷新）。**② 容器内整理**：`ICSLootSource` 加 `TryMoveLootStack`（来源网格内移动某项，CSGridPlacement 校验忽略自身占用）；控制器 `RequestMoveLootStack` RPC。**③ 双网格落子泛化**：`SCSBackpackGridWidget` 的 LootSource 模式不再拒落子——OnDragOver/OnDrop 改 **2×2 分流**（背包格：玩家拖=移动、战利品拖=取物；战利品格：玩家拖=存入、战利品拖=整理），新增 `DragOriginatesFromThisGrid` 决定占用校验是否忽略自身；`RefreshDragHighlightForOp` 加光标包含判断（按 R 时两个网格各刷一次、只有被悬停的画），面板 R 处理对两网格各调一次。**④ 未摆放项"溢出区"（用户拍板）**：背包网格下方加横向 `SScrollBox` 条带，渲染 `GridX<0` 的未摆放项为可拖拽小图标 `SCSGridOverflowChip`（以 PlayerInventory 源、抓取偏移 0,0 起拖），拖回网格→`MoveStackTo` 摆放——补上 Grid-C1 遗留缺口（缩小背包/取物未自动摆下的项之前只能在 Take/紧凑列表见）。**共享工具新增**：`CSGridPlacement::PlaceAtPosition`（按位置放+自动补位回退）。**非目标（Grid-D）**：物品实例 FGuid（逐件耐久/附件/唯一状态）。 | 待验证（Listen Server + 客户端：①背包拖一件到容器/尸体格→存入、玩家背包少了该摞；②容器格内拖动整理；③背包缩小/取物产生未摆放项→出现在溢出区→从溢出区拖回网格摆放；④拖到放不下处红高亮、松手不变；⑤主机/客户端一致、客户端拖拽经服务端生效） |

| 78. Dialogue-UI-A NPC 会话专用对话框（名牌 + 头像槽 + AIC/对话分流） | 已落代码（`CoopSurvival` 运行时 + `CoopSurvivalEditor` 编辑器目标均 2026-06-09 整编通过；纯表现层，不动播放路径、不加 RPC）。**待 PIE**（编辑器目标确认 up-to-date = DLL 已含本单元 + VG-A 反射改动；同批一并建好了 VG-A 的 `DT_CharacterVoiceProfiles` 表）。 | 把 NPC 对话从"借用 AIC 底部字幕"升级为**专用对话框**：终端蓝描边 + 深底 + 左侧头像槽 + 名牌 + 自动翻页台词。`FCSNarrativeQueuedLine` 加 `bIsConversation` 分流（AIC 句→`SCSNarrativeOverlay` 字幕、NPC 会话句→新建 `SCSDialogueWidget`，二者互斥、同刻只一句）；头像取自交谈 NPC 的 `Portrait`（`TSoftObjectPtr<UTexture2D>`，BP 可编辑；未赋则头像槽折叠、仅名牌+台词），导演在该句变当前句时同步加载到 `UPROPERTY ActivePortrait` 防 GC，经 `GetActiveLinePortrait()` 给控件构 `FSlateBrush`。导演队列/世界状态/配音 2D 本机+3D 多播路径**均不变**。对话框 `HitTestInvisible`、`ACSPlayerController::CreateHUD` 挂载（ZOrder 12）。**手动推进/跳过明确不做**——留待对话选项单元（B）：手动翻页要能掐断当前配音，而现配音 `PlaySoundAtLocation`/`PlaySound2D` 是 fire-and-forget 停不掉，做对就得把播放层改成可停的音频组件（与 VG-A slice 2 同一处整改），不在本单元半做留"两句叠播"毛刺。 | 待 PIE（Listen Server + 客户端）：①走近 Varga 按 E 交谈 → 弹**专用对话框**（名牌"Dr. Varga" + 台词；若 BP 给 NPC 赋了 `Portrait` 则左侧显头像），与 AIC 广播字幕**外观可区分**；②AIC 开场白/电梯/搜刮节拍**仍走底部字幕**、不被对话框取代；③对话框只在会话句显示、说完折叠；④客户端走近交谈对话框也正常、3D 配音队友能听到（沿用 Unit 65）。 |
| 79. Search-A 首次搜刮进度（塔科夫式：搜完才揭示） | 待用户验证（运行时 + 编辑器双目标 2026-06-09 整编通过 = Result: Succeeded；待 PIE） | 用户提的"搜索小动画"：首次摸未搜过的容器/尸体不再瞬间出货，先走**搜索进度（几秒 + `SProgressBar` 平滑填充）**，**搜完才揭示内容**（"第一次摸这个东西、切没摸出来的时候"）。塔科夫式、服务端权威防偷看。**接口**：`ICSLootSource` 加 `IsLootSearched`/`GetLootSearchProgress01`/`BeginLootSearch`。**容器/尸体状态**：加复制 `bSearched`/`bSearchInProgress`/`SearchStartServerTime`/`SearchDurationSeconds` + 服务端搜索计时器。**揭示门控**：`CopyRemainingLoot` 未搜完返空（服务端不下发内容→客户端拿不到、UI 无内容）。**流程**：首次 `Interact`→`BeginLootSearch`（设起始服务器时间+时长、起 `FTimerHandle`）+ 立即开面板（搜索态）；容器 `CompleteLootSearch` 才 `EnsureLootGenerated`（**延迟生成**——搜完才 roll 战利品）；尸体 `CorpseLoot` 死亡已生成、`CompleteLootSearch` 只置 `bSearched` 揭示。已搜过的来源再开直接显示。进度 `GetLootSearchProgress01` 用 `GetServerWorldTimeSeconds()`（复制、两端一致）算 0~1。**数据**：`Containers.csv` 加 `SearchSeconds`（`FCSContainerDefinition`+生成器 `require_optional_float`+子系统解析，`BasicLocker`=3s）；尸体 EditDefaultsOnly `CorpseSearchSeconds`=2.5s。**存档**：容器 `ApplySavedState` 把 `bSearched=bLootGenerated`（生成过=搜过，读档直接揭示不重播动画）。**UI**：`FCSHUDSnapshot` 加 `bOpenLootContainerSearched`+`OpenLootContainerSearchProgress01`（控制器 `BuildHUDSnapshot` 从活的来源现读）；搜刮面板未搜完显示 `SProgressBar`+"Searching… X%"（`GetLootSearchVisibility`）、`GetNoLootContainerItemsVisibility` 加 `bOpenLootContainerSearched` 门控（搜索中不显示"空"）；进度条用 `Percent_Lambda` 逐帧读快照平滑动。**非目标（v1）**：离开范围/受击取消搜索、按技能/物品数缩放搜索时长、搜索音效。 | 待验证（Listen Server + 客户端：①首次摸储物柜→面板开但内容隐藏 + 进度条跑 3s→搜完物品揭示、可拖拽取物；②再开同容器→直接显示（已搜过）；③打死敌人搜尸体→2.5s 进度→揭示；④主机/客户端进度一致、客户端搜的容器主机也揭示；⑤存档读档后已搜容器直接显示、不重播搜索） |
| 80. UI-Perf-A Stage 1 HUD 快照单帧缓存 | 待用户验证（运行时 + 编辑器双目标 2026-06-10 整编通过；待 PIE）。**首轮修复（非反射，Live Coding 可热补）**：单帧缓存引入"取物后战利品格残留点不动"——事件刷新与面板 Tick 同帧时 `GetHUDSnapshot` 帧守卫命中、返回**改动前**陈旧快照。修：控制器加 `InvalidateHUDSnapshot()`（缓存帧号置 MAX），在 `RefreshInventoryPanel`（所有改动刷新漏斗）开头调用作废，再 `RefreshFromSnapshot` 重建为最新。坑见 `UE_Gotchas §1.6`。 | UI 性能整改第一刀（设计来源:ui-perf-plan workflow + `EquipmentLoadout`/`project-ui-art-direction` 旁路；问题诊断见 `UE_Gotchas §1.5`）。**问题**:`BuildHUDSnapshot()`(50 字段、含 `InventoryStacks`/`AvailableRecipes` 配方表全扫/`CopyRemainingLoot` 三处重 TArray 拷贝)被 `SCSHUDWidget::Tick` + `SCSInventoryPanelWidget::Tick` + 两个网格 `RefreshGrid` 各自每帧调——开容器+合成时**最多 4 次/帧**全量重建(~50KB churn + 4 次配方扫描),是满背包重场景卡顿源,且随物品数线性恶化。**Stage 1（本单元，止血）**:控制器加 `mutable FCSHUDSnapshot CachedHUDSnapshot` + `CachedHUDSnapshotFrame` + `const GetHUDSnapshot()`（`GFrameCounter` 守卫:同帧只 `BuildHUDSnapshot` 一次、多控件共享；每 PlayerController 各一份，PIE 多世界安全）；`IsAnyMenuPanelOpen()`（=`GetHUDVisibility` 折叠条件）。控件 `BuildHUDSnapshot()`→`GetHUDSnapshot()`(SCSHUDWidget Tick/Construct、SCSInventoryPanelWidget RefreshCachedSnapshot、SCSBackpackGridWidget GetSnapshot)。HUD Tick **折叠跳过**(菜单开着不刷)+ `GetHUDVisibility` 直读控制器(避免折叠期 CachedSnapshot 陈旧)。→ **4 次/帧 → 1 次/帧**，折叠 HUD 0 次，去掉约 75% 每帧 UI 开销；非反射逻辑但**含新成员**(需关编辑器整编)。**非目标（Stage 2，后续）**:把那 1 次也变便宜——`BuildHUDSnapshot` 拆 heavy(三大数组/配方扫描,按 `MarkHUDStaticDirty` 脏标记只在真事件重建)+ light(标量每帧),脏点接 `RefreshInventoryPanel` 漏斗 + 搜刮/合成开关；Stage 2 有漏标脏风险，单独上 + 对抗式审查。 | 待验证（Listen Server + 客户端：①满背包+开容器+合成卡顿明显缓解；②HUD/背包面板数值照常实时：血/体力/负重/弹药/搜索进度条平滑；③换装(J)时 HUD 行为不变；④主机/客户端一致；⑤拿取/移动/搜刮/合成全程 UI 正常无残值） |
| 81. Quest-A 任务定义层与世界状态对接（主线/支线/隐藏） | 已实现（`CoopSurvivalEditor` 编辑器目标 2026-06-10 整编通过 = Result: Succeeded，含本单元新增 `USTRUCT`/`UENUM` 反射；待 console/PIE 验证）。⚠️ runtime 目标 `CoopSurvival` 当前被**未跟踪文件** `UI/Editor/CSWidgetAuthoringLibrary.h`（Unit 78 WIP，UFUNCTION 返回 `UWidgetBlueprint*`，runtime 模块无 UMGEditor 依赖→UHT 解析不到）阻塞，与本单元无关；本单元代码在 editor 目标已编译链接通过。 | 给只有后端状态机（Objective-A/B 的 `ECSObjectiveState` + `WorldProgress`）的任务系统补**缺失的定义数据层**：`Quests.csv`/`Objectives.csv` 两表给每个目标补标题/描述/所属线/类型（主线·支线·隐藏）/层级，经 `GenerateGameData.py` 校验生成、`UCSItemDataSubsystem` 加载；`ACSGameState::BuildResolvedQuestView` 把定义与复制的 `WorldProgress.ObjectiveStates` 活状态 join 成只读"任务视图"；隐藏任务可见性按 `WorldStateTags` **计算**（零存档迁移）。控制台 `PrintQuests` dump 验证。UI（Quest-B 事故记录面板 / Quest-D Site 状态图）与运行时条件求值器（Quest-C）留后续。 | 待验证（console：`PrintQuests` 打印 3 条任务+步骤状态；隐藏任务 `SetWorldStateTag Hidden.SignalDetected` 前不可见、后可见；`CompleteObjective L0_FindFuse` 后该步骤 State 变 Completed；客户端与主机一致） |
| 83. Anim-Pistol-A 手枪持枪整身动作系统（独立 mocap 状态机） | 进行中：**83a 重定向已落地**（2026-06-10 headless commandlet 跑通，213/213 clip 重定向到 `SKM_Manny_Simple`、落 `Content/Animation/Weapons/Pistol/`，`RTG_PistolToManny`+两 IK Rig 已建；引擎报的 1 error 是无关的损坏文件 `Content/Collection_22/Textures/Man/Winter_1/ue.uasset`）。**83b locomotion BlendSpace 已脚本生成**（`Scripts/CreatePistolLocomotionBlendSpaces.py`→`BS_Pistol_Relaxed_Loco`/`BS_Pistol_Aim_Loco`，各 19 样本，Walk@300+Jog@600 四向 IP+idle；复制 `BS_Idle_Walk_Run` 模板覆写 sample_data）。**83b C++ 暴露位已落**（`UCSPlayerAnimInstance` 加 `bIsAiming`(读角色 `IsAiming()`)/`bIsCrouching`(读 `bIsCrouched`)，预览路径同步；2026-06-10 CoopSurvivalEditor 整编 Result: Succeeded）。**83b 双姿态接线修通（2026-06-11）**：站姿 4 向 BlendSpace 改走"真复制+同位换动画"配方落盘（`BS_Pistol_LocoRelaxed`=低位/`BS_Pistol_LocoAim`=举枪，各 11 样本；脚本内存临时建的 BS 不落盘、被 GC 后 ABP 引用变空 = 之前"举枪垮姿势"根因）；ABP 接线根因=relaxed 播放器输出悬空、不瞄准分支仍直连 LocoAim → 常态举枪+右键无视觉变化，已用 MCP pin API 重接：`bUseEquipmentAimOffset` FALSE→LocoRelaxed、TRUE→AimOffset(底座 LocoAim)，编译+存盘。**83d 前置规则已落 C++（2026-06-11，待编译）**：所有枪械必须瞄准才能开火——`TryPrimaryAttack` 客户端预检 + `PrimaryAttackOnServer` 服务端权威校验（读复制的 `bIsAiming`，近战不受影响）；纯 .cpp 无反射改动 → Live Coding（Ctrl+Alt+F11）可热补。**83c 整身旁路+蹲+跳已接通（2026-06-11）**：掏枪/开火/换弹由 Manny 老 clip（`MM_Pistol_*`）换成本套（`DA_EquipAnim_Pistol`→`Stand_Relaxed_Unholster`/`Stand_Fire_Single`/`Stand_Relaxed_Reload`，数据资产改动即时生效）；C++ 加 `bIsFalling`/`bUsePistolFullBodyAnimSet`（2026-06-11 整编通过）；ABP 加**手枪整身旁路**（用户摆节点+MCP 连线 18 根全通、编译 UP_TO_DATE）：装手枪→整条身体走本套（站立 relaxed/aim ↔ 蹲姿 relaxed/aim 四向 ↔ 空中整身跳），不装/刀/长枪→走原 spine_03 分层不受影响；跳跃 clip 一次性不循环；蹲姿瞄准 bool 复用 `bUseEquipmentAimOffset`、空中 bool 用 ABP 模板自带 `IsFalling`（与腿部 Main States 同源）。**83c 腿不动根因已修（2026-06-11）**：脚部 IK `ControlRig_0` alpha→0（编译+落盘；落盘曾被第二个编辑器实例锁文件 Error Code 32 卡住，关掉多余实例后保存成功），重定向腿部动作不再被 ControlRig 覆盖；用户拍板先不用 IK，后续可改条件式 alpha（仅手枪整身旁路时关）或补 ik_foot 骨重定向。**83e 瞄准 AO 已换本套（2026-06-11）**：复制 3 clip 为 `AimOffset/AN_Pistol_AO_Stand_D90/Center/U90` 并设网格空间旋转加性（基准=`AN_Pistol_Stand_Aim_Point_Center` 第0帧，照抄旧 `MF_Pistol_Idle_ADS_AO_*` 配置），ABP 内嵌 AO 的 3 个采样播放器**原位换动画**（每个采样是子图里的 SequencePlayer，直接改 sequence 即可；采样位置 y=-1/0/+1 不动、步枪采样不碰），编译+落盘；修"向下瞄准左手漂向右手"（根因=旧 Manny 加性差量叠在新动捕底座上）。**83f 移速↔动作匹配已落 C++（2026-06-11，纯非反射改动→Live Coding 可热补）**：用户报"移动比动作快/滑步"——用 AnimPose 量 ThirdParty 原始 clip 的 **pelvis 首尾帧位移**（root 轨迹为 0，这套位移在骨盆）得实测速度：Jog_F=306、Run_F=483、Walk_Aim_F=142、CrouchWalk_F=95 cm/s；落 `CSPistolAnimSpeeds` 常量（`CSPlayerCharacter.h`）：装手枪时 `UpdateMovementSpeed` 改用实测速度（常态 306/瞄准 142/蹲 95，不再叠瞄准乘数；**冲刺暂不加速**——BS 无 Run 行，要快先收枪），装备切换钩子里补 `UpdateMovementSpeed()`；AnimInstance 把 BS 速度输入按比例放大对齐名义样本行（站姿 ×600/306、蹲姿 ×300/95）→ 播放速率 1:1 不滑步。**83g 方向跳跃已接通（2026-06-11）**：复制两 Loco BS 同位换跳跃片段（`BS_Pistol_JumpRelaxed`/`BS_Pistol_JumpAim`，11 采样位置不动：原地=`Stand_Relaxed/Aim_Jump`、走排=`Walk_*_Jump_IP`、慢跑排=`Jog_F/L/R_Jump_IP`、慢跑后向无素材用 `Walk_B_Jump_IP` 顶）；ABP 跳跃分支由"永远播站姿跳"换成 BlendBool(`bUseEquipmentAimOffset`)→两个跳跃 BS 播放器（X/Y 沿用方向/速度变量分叉、均不循环），用户摆节点+脚本连线，编译+落盘；注意 **PIE 运行中 duplicate_asset/资产写盘会被拒**（"Editor is currently in a play mode"）。**83h 生硬修复+冲刺恢复（2026-06-11）**：用户报"idle→起步、左右平移切换生硬"——根因=6 个手枪 BS 的 `target_weight_interpolation_speed_per_sec`=0（权重瞬切），Manny 参照是 5.0；已全部调 5.0 落盘。用户报"Shift 冲刺没了"=83f 故意锁的，按用户拍板做 C 方案恢复：`BS_Pistol_LocoRelaxed`/`JumpRelaxed` 加 **Run 行**（16 样本 4 行：0/300/600/900，Run_F/R/L 真动作+正后方 Jog_B 顶、跳跃行 Run_F_Jump_IP），C++ `CSPistolAnimSpeeds::Run=483`、冲刺输入 483×(600/306)≈947 被速度轴上限 900 钳住正好命中 Run 行。**⚠️ 新坑（已记 memory）**：BS 在编辑器里 open 重建网格时会把**不在网格线上的样本吸附走**（扩轴到 947 时 300/600 行被吸到 236.8/710.2）；行位置必须设计成正好落在网格线上（本例 max=900+grid_num=3 → 线在 0/300/600/900）。**83i 蹲姿过渡+跳跃手感+冲刺意图（2026-06-11）**：①站↔蹲真过渡接通——`UCSEquipmentAnimProfile` 加 `CrouchEnterAimAnimation`/`CrouchExitAimAnimation` 两反射字段（瞄准姿态专用，不配回落低位字段；整编通过），`PlayEquipmentCrouchTransition` 瞄准时选 Aim 对、**手枪整身旁路时播全身 `DefaultSlot`**（旧上半身槽在旁路下不可见=之前蹲切换没过渡感的原因），`DA_EquipAnim_Pistol` 四字段已填（`Stand_Relaxed_To_Crouch`/`Crouch_To_Stand_Relaxed`/`Stand_Aim_To_Crouch_Aim`/`Crouch_Aim_To_Stand_Aim`）；②跳跃落地软化：跳↔地选择器混合时间 0.1/0.1→0.15(起跳)/0.25(落地)（落地段被切的硬感先用混合盖，真 Land 状态列 polish）；③冲刺改"按住即意图"——新增本地 `bWantsToSprint`（非反射、不复制），体力耗尽/蹲断后键还按着就 Tick 自动续上（仅本地成功才发 RPC 不刷服务器），跳跃本就不清冲刺；④空中锁转向——非瞄准时 `bOrientRotationToMovement=!IsFalling()`，`OnMovementModeChanged` 起跳/落地重算（空中扭身不再和定向跳跃动作打架）。③④是 .h 加普通成员+override=**类布局变化，需关编辑器外部整编**（待跑）。**用户验后调整（2026-06-11）**：蹲姿过渡**取消**——DA 四字段已清空（机制+字段保留，填回即恢复）；跳跃空中"卡住"=整弧 clip 播完定格末帧（站姿），跳跃 BS 播放器 `play_rate=0.65` 缓解，真 Land 状态列 polish。**83j 枪口特效+枪声位置（2026-06-11）**：`MulticastPlayFirearmShotPresentation` 重写，DrawDebug 黄点占位换真表现——①枪口火光走数据表 `FirearmDefinitions.MuzzleFlashVFXPath`（Cascade/Niagara 资源都接受），附着武器 `MuzzleComponent` 跟随后坐，无武器 Actor 回退世界坐标；②补枪口点光源闪光（60cd 橙光/半径 500/不投影/0.06s 定时销毁、无 Tick——库里火光粒子不带灯光模块，黑暗收容区开枪需要照明反馈）；③枪声从角色脚底改到枪口位置；④枪械定义各端取一次供特效+后坐力共用。CSV 已填（手枪=`VFX_Muzzleflash_01`、霰弹=`03`，`/Game/Bullet/Particles/`，变体改 CSV 即换），`GenerateGameData.py` 重跑，运行时整编 Succeeded（纯 .cpp 函数体改动→Live Coding 可热补）。**发现**：`FirearmPresentationDefinitions.csv`/`SurfaceImpactEffectSets.csv` 两张表现层表早已设计好但没接代码（抛壳/空枪干响/远近枪声分离/按表面弹着点），素材全在 `Content/Bullet/`（弹着点 14 种表面+抛壳粒子现成），列下个枪械表现单元。**待**：①关编辑器整编③④→PIE 验全链（低位常态/右键举枪/蹲走蹲瞄/掏枪新动作/不瞄不能开火/刀枪互不影响/腿部跟随本套/上下瞄准左手不漂/移速=动作速度不滑步/移动中跳跃方向正确+空中不定格不转身+落地不硬切/起步与左右平移不再生硬/Shift 冲刺=Run 动作+体力恢复自动续冲/开枪见枪口火光+瞬时照明+枪声来自枪口）；②急停过渡（`Jog/Run_*_to_Stand_Relaxed` 15 片段有素材未接）与起步（无素材，权重渐变盖）；③旧站姿跳 SequencePlayer 悬空可删；④蹲姿瞄准分支暂无 AO；⑤45° 中间采样细化、转身 polish、真 Land 状态；⑥MotionMatching 评估列独立单元（ALS 已劝退，PoseSearch 再议）。详见 `Docs/Development/WeaponAnimSystem_双姿态重构_开发拆分.md` | 以 `Military_Pistol_01`（MoCap Online 风格 214 动作集、UE4 骨架）为源。**用户决定：装备手枪时整身动作完全由这套驱动**——单独建一个"持枪整身状态机"分支，按 `EquipmentAnimSet==Pistol` 进入，里面 idle/八向 locomotion/aim/下蹲/跳跃/转身/开火/换弹/收拔枪**全用这套的 clip**，不与 Manny 基础混（spine_03 分层只留作跨武器淡入淡出）。一并解决"腿非八向"老缺口。自建 `RTG_PistolToManny`（仿已验证的 Knife 流水线）批量重定向到 `SKM_Manny_Simple`，落地 `Content/Animation/Weapons/Pistol/`，**默认全量 213 个 W1**（脚本 `CS_PISTOL_RETARGET_ALL` 默认 1）。Aim 用 `Aim_Point_*` 加性 AimOffset（补 pitch）；`UCSPlayerAnimInstance` 暴露 `bIsAiming`/`bIsCrouching`（新增反射成员→关编辑器整编）；开火/换弹/收拔枪走动作槽 + `UCSEquipmentAnimProfile`。每类武器各自一套整身集（匕首=已有 Knife 全套、霰弹/步枪=Kubold U84、空手=Manny）。⚠️ 推翻 2026-06-08"暂不迁移"决定。分阶段：83a 重定向落地+预览 → 83b 持枪整身状态机骨架（idle/locomotion/aim）→ 83c 下蹲+跳跃 → 83d 换弹/开火/收拔枪接战斗 → 83e turn-in-place/起停/Fidget/死亡 polish。**重复目录**按规范不裸删、留编辑器归档。**非目标**：长枪（Unit 84）。 | 待验证（Listen Server + 客户端：①装手枪后整身=这套（idle/移动/瞄准/蹲都换了味）；②四向移动腿姿正确无滑步、无转枪鬼影；③上下瞄准头/枪口跟随（pitch）；④开火/换弹/收拔枪与弹匣系统数值同步；⑤主机/客户端一致；⑥换装 LeaderPose 部件跟随不脱节；⑦切回匕首/空手动作正确切换） |
| 82. UI-Perf-B 背包/战利品/配方 全部增量更新（取物不再全量重建） | 待用户验证（编辑器目标 2026-06-10 整编通过 = Result: Succeeded，`SCSBackpackGridWidget` + `SCSInventoryPanelWidget` 均编译链接干净；运行时/游戏目标被**无关**的 `UI/Editor/CSWidgetAuthoringLibrary.h` UHT 报错挡住——见备注，与本单元无关；待 PIE） | 用户痛点："Take 为什么要重建那肯定是不对的，我们是数据和 UI 分离，take 在数据上最多加一行/数字+1"。**问题**：`SCSBackpackGridWidget::RefreshGrid` 每次都 `Canvas->ClearChildren()` 全量重建——背景几十个格 SBorder + 所有物品控件全销毁重造，取一件物 = 重建整张网格（且取物回推同帧/相邻帧各刷一次），违背数据/UI 分离。**改法（增量 diff）**：①**三层叠放**——`BackgroundCanvas`（背景格层，只在网格尺寸变化时 `RebuildBackground`，靠 `BuiltBackgroundW/H` 判定）+ `Canvas`（物品层，增量）+ `HighlightCanvas`（落点高亮）；②**物品层按格位 `(GridX,GridY)` 为键 diff**——成员 `TMap<FIntPoint, FDisplayedGridItem>{ItemId,bRotated,Widget}` 记已显示项，期望集（`CachedItems` 已摆放项）与已显示集比对：同位同物同旋转**保留不动**、变了/没了 `Canvas->RemoveSlot` 删、新位 `AddSlot` 加（取物=删 1 控件、放入=加 1、换物=删+加）；③**数量徽标 `Text_Lambda`/`Visibility_Lambda` 绑 `GetQuantityAtCell(X,Y)`**（读活 `CachedItems`）→ **可堆叠物并堆/取部分只刷数字、零控件改动**。玩家背包与战利品/尸体网格是同一控件，两侧都受益。**堆叠属性确认**（回应"可堆叠/不可堆叠要有属性"）：可堆叠性 = `FCSItemDefinition.MaxStack`（`MaxStack==1` 不可堆叠、`>1` 可堆叠到上限），`Items.csv` 已每件配齐（材料/弹药 99/32，武器/护甲/工具/服装 1），**无需新字段**。**三个列表同法增量化**（`SCSInventoryPanelWidget`）：`RebuildDynamicRows` 由"`ClearChildren`+全量重建三个 `SVerticalBox`"改为 `UpdateInventoryRows`/`UpdateRecipeRows`/`UpdateLootRows` 三个**按稳定键 diff 的更新器**——背包/战利品行**按 ItemId 聚合一行**（成员 `TMap<FName,TSharedPtr<SWidget>>` 记已显示行，缺则 `RemoveSlot`、新则 `AddSlot`），配方行**按 RecipeId**；行内**数量文本/材料进度(3/5)/绿红色全改 `Text_Lambda`/`ColorAndOpacity_Lambda`** 读活快照 → 数量变化、材料增减零行重建（按钮本就按 ItemId/RecipeId 捕获、`IsEnabled` 本就绑定，故安全）。**连带**：更新器幂等 → `RefreshFromSnapshot` 不再需要每次跑那个 O(n) `BuildSignature`（§1.5 点名的逐项 `Printf`+`Join` 费操作），**整个 `BuildSignature`/`LastSignature` 删除**；`Tick` 仍只 `RefreshCachedSnapshot`（逐帧 lambda 读），结构性增删只在事件驱动 `RefreshFromSnapshot` 发生。**非目标**：同帧多次 `RefreshInventoryPanel` 合帧（coalesce，2 次/取物 → 1 次）留后续——增量后两次刷新都已是便宜 diff，优先级降低。**备注（构建阻塞，非本单元引入）**：运行时/游戏目标 `CoopSurvival` 整编在 UHT 阶段失败于上游 `UI/Editor/CSWidgetAuthoringLibrary.h(30)`——UFUNCTION 返回 `UWidgetBlueprint*`（UMGEditor 类型）在 `#if WITH_EDITOR` 内，但 UHT 不分目标都解析其返回类型，而游戏目标模块无 UMGEditor 依赖。编辑器目标已修（editor-gated UMGEditor 依赖）；游戏目标/打包需把该授权库移入独立 Editor 模块或返回 `UBlueprint*`。 | 待验证（Listen Server + 客户端：①取一件物只动那一格、背景不闪、其余物品/格子控件不重建；②可堆叠物并堆/取部分只见数字 +N、不闪；③大件/旋转件取放占格正确；④战利品搜完揭示→取空→容器切换显示均正确；⑤主机/客户端一致） |
| 85. Item-Strict 物品类型严格化（`ECSItemType` + 关键物/样本规则） | 已实现（`CoopSurvival` 运行时 + `CoopSurvivalEditor` 编辑器目标 2026-06-10 均整编通过 = Result: Succeeded，含新增 `UENUM ECSItemType` 反射；编辑器 DLL 已重链；**用户 2026-06-10 PIE 确认关键物不能丢 ✅**） | 回应"道具系统要严格一些" + `SD_47` 前置决策②。把物品"类型"从 `FName` 字符串标签升级为正式 `UENUM ECSItemType`（Material/Ammo/Weapon/Armor/Tool/Apparel/Consumable/Sample/KeyItem/Residue）。`FCSItemDefinition.ItemType` 改枚举，新增三字段 `bCriticalItem`（关键物/任务道具：服务端禁丢 + 不进随机掉落）、`ContaminationPerSecond`（携带涨污染，数据字段，本单元不消费）、`bArchivable`（可归档，数据字段，本单元不消费）。`Items.csv` 加 3 列（`GenerateGameData.py` 加 `VALID_ITEM_TYPES` 枚举校验 + 新列校验）；子系统 `LoadItemsCsv` 加 `ParseItemType` 字符串→枚举 + 解析新列；`IsRandomLootAllowed` 由旧字符串黑名单改为 `bCriticalItem` + Sample/KeyItem/Residue 类型门控。`DropItemOnServer` 加服务端守卫：`bCriticalItem` 物品拒绝丢弃（GM 入口同受限）。补两个 demo 已命名的示例物品：`Item_Fuse`（KeyItem·关键物·L0 电梯保险件）、`Sample_C12_Stable`（Sample·`ContaminationPerSecond=0.5`·可归档·活体过滤循环产出）。**非目标（后续单元）**：样本槽/隔离槽、污染累积结算（接生存属性系统·SD_47 决策①）、S0 归档台、消耗品 `UseItem`、投掷物、物品实例 FGuid、`bCanDrop` 强制（本单元只加 `bCriticalItem` 守卫）。 | 待验证（Listen Server + 客户端：①GM 给 `Item_Fuse` → 拖到丢弃区/RMB 丢 → 被拒提示 "critical item"、物品仍在背包；②普通物（Wood）仍可正常丢；③搜刮容器/尸体不会随机出 `Item_Fuse`/`Sample_C12_Stable`；④主机/客户端一致；⑤现有装备/合成/搜刮/背包/换装行为不变） |
| 86. PlayerFeel-B 镜头手感（跟随平滑/下蹲反补偿/落地下沉/瞄准节奏/室内软碰撞） | 已实现（`CoopSurvivalEditor` 编辑器目标 2026-06-11 两次整编均 Result: Succeeded：先①②③④含新增反射 `UCLASS UCSLandCameraShake`/`UCSLandCameraShakePattern` + 角色多个新 UPROPERTY；后追加⑤新增反射 `UCLASS UCSCameraBoom`，编辑器 DLL 已重链；待 PIE 一次性验全部。⚠️ 反射改动 Live Coding 热补不了，两次都走关编辑器完整整编） | 回应用户"镜头生硬（下蹲/移动跳跃/瞄准）"。诊断：镜头焊死在胶囊体上（`bEnableCameraLag=false`）、各状态切换是裸 `FInterpTo`、落地无反馈。本单元四刀（**纯本机表现、不碰网络权威**，全在 `ACSPlayerCharacter`）：①**相机跟随延迟**——弹簧臂开位置延迟、**不开旋转延迟**（鼠标视角要跟手）；`CameraFollowLagSpeed=10`（探索偏"重"）/`AimingCameraFollowLagSpeed=22`（瞄准接近瞬时、准星不被拖在身后）/`CameraFollowLagMaxDistance=100`，在 `ApplyCameraSettings` 每帧按瞄准态强制写入（防 BP_PlayerCharacter 弹簧臂组件默认值覆盖项目意图）。②**下蹲反补偿**——`OnStartCrouch/OnEndCrouch` 用引擎传入的 `ScaledHalfHeightAdjust` 给弹簧臂加等量反向位移，抵消胶囊体瞬移造成的"硬跳一下"，再由每帧 `ApplyCameraSettings` 平滑降到/升回目标高度（仅本机 `IsLocallyControlled`）。③**落地下沉**——重写 `Landed`，本机按落地竖直冲击速度（`LandShakeMinFallSpeed=350`/`LandShakeMaxFallSpeed=1300`）映射强度 0.25~1.0，播自建单峰下沉镜头 `UCSLandCameraShake`（半正弦"一沉一回"+轻微低头，复用受击 shake 同套 pattern 基础设施、不依赖 GameplayCameras 插件）。④**瞄准节奏**——`AimingCameraFOV` 65→78 收窄进瞄 FOV 跨度（90→78 比 90→65 平缓，进瞄不再"猛地一缩"）。⑤**室内软碰撞**（针对室内为主的生存恐怖追加，3A 镜头讨论里"软碰撞"那条）——新增项目相机臂 `UCSCameraBoom`（`Camera/`，继承 `USpringArmComponent`；角色构造改 instancing 它，成员仍按基类 `USpringArmComponent` 持有，故所有 `CameraSpringArm->...` 调用点不变），override `BlendLocations` 把镜头碰撞做"软"：撞墙/门框/柱子**收近快**（`CollisionPullInSpeed=60`，防穿墙看到角色内部）、**离墙推回慢**（`CollisionPushOutSpeed=5`，不"弹"）；帧率无关指数平滑（`1-e^(-k·dt)`），碰撞缩进量(deficit)按"相对完整臂长"算→与瞄准拉近/拉远**解耦**互不拖累。治理大量室内近距几何下默认弹簧臂瞬时收放的"啪啪跳"。**未做（留后续）**：镜头贴角色过近时角色渐隐/dither（需身体材质支持，本套 Collection_22 材质待确认）。 | 待验证（Listen Server + 客户端：①移动起步/急停/变向镜头有"重量"不再焊死；②下蹲/起身镜头平滑下降/升起、无瞬间下坠硬跳；③从高处跳落地镜头一沉一回、越高越重、下小台阶不触发；④瞄准时镜头跟手、横向 strafe 准星不被拖后；⑤进瞄 FOV 过渡不再猛缩；⑥室内贴墙/掠过门框柱子镜头收近/推回平滑不瞬跳、离墙不"弹"、不穿墙；⑦主机/客户端各自本机表现正常、不影响他人画面；⑧受击震动/开火后坐力照常） |
| 87. FirearmFX-A 枪械开火表现层（远近枪声/枪口/抛壳/弹着点/干响） | 已实现（运行时 + 编辑器双目标 2026-06-11 整编通过 = Result: Succeeded，含新增 USTRUCT/UPROPERTY/RPC 签名反射改动；**待 PIE 验证**）。**根因修复：枪声其实从未响过**——`GunSoundsPackVol1` 2026-06-07 已归档 `/Game/ThirdParty/Audio/`，旧根路径 redirector 被清，CSV 软路径成死链、`TryLoad` 静默失败；全部音频路径已改到归档后真实位置（根目录中文重复副本【UE4.26+】【音效】枪声第1卷音效包 未动，归档遗留）。**数据迁移**：开火声/枪口特效真值从 `FirearmDefinitions.csv`（删 `FireSoundVariants`/`MuzzleFlashVFXPath` 列）迁入 `FirearmPresentationDefinitions.csv`（枪械行加 `PresentationId` 列 join，生成器加交叉校验；列升级 `FireLocalCuePaths`/`FireRemoteCuePaths` 变体列表——本机近距脆响 5 变体 / 远端 Distant 闷响 3 变体）；`SurfaceImpactEffectSets.csv` 特效路径改真实位置 `/Game/Bullet/Particles/`、补 `Gravel` 行（注册的 7 种 PhysicalSurface 全覆盖）、`ImpactAudioCuePaths` 接 `BulletImpactsPro` 按表面 3 变体。**C++**：`FCSFirearmPresentationDefinition`/`FCSSurfaceImpactEffectSet` 新结构 + 子系统两表装载与 getter（弹着未配置表面回退 `Default` 行；`GetSurfaceTypeName` 与脚步声同套 `UPhysicsSettings` 映射）；`ACSFirearmWeaponActor` 加 `EjectPortComponent`（默认机匣右侧，**BP_Weapon_\* 里调真实抛壳窗位置**）；战斗组件——服务器弹道开 `bReturnPhysicalMaterial`、把世界几何命中收集成 `FCSShotImpactInfo[]`（量化坐标/法线/表面名；命中可受击目标不收集，敌人血液反馈已有），多播签名改 `(SourceItemId, LoadedAmmoItemId, Impacts)` 且挪到弹道检测后（枪声/枪口/抛壳/弹着一次 RPC，不再传声音路径数组省带宽）；多播内：**远近枪声分离**（本机=近距池/其他端=远处池、未配回退，枪口位置发声）+ 枪口火光/抛壳走 `SpawnDataDrivenVFX` 文件级辅助（Cascade/Niagara 都收，分别附着 Muzzle/EjectPort）+ 83j 点光源闪光保留 + 弹着点循环（特效沿法线/音效随机变体/贴花机制已留但素材未配）；客户端 `TryPrimaryAttack` 加 `IsFirearmDryFire` **空枪干响**（弹匣空且同口径零储备→本机播干响不发 RPC；有储备仍走服务器自动换弹）。`GenerateGameData.py` 校验通过。**用户首测后补修（2026-06-11，⚠️ 已记 UE_Gotchas §5.3）**："弹着点像没接上"的真正根因=表面识别从未生效——①场景材质全没挂物理材质（`PM_*` 资产是脚步声单元建的，但没有任何脚本/人把它们指到材质上），已用 MCP 给 `Lvl_FirstPerson` 的 7 个常用材质挂上（原型墙地 Colorway/PrototypeGrid→`PM_Concrete`、SciFi 套件金属件→`PM_Metal`，布料/贴花/天空留 Default）并存盘；②**简单碰撞射线读不到渲染材质上的 PhysMaterial**（`bTraceComplex=false` 时物理材质取自 BodySetup），开火弹道与脚步声射线均改 `bTraceComplex=true`（按三角面取命中面材质；纯函数体改动→Live Coding 可热补）——连带修复脚步声表面识别同样从未生效的旧问题。③用户报"特效有方块"：依赖审计发现子弹特效包三份拷贝全是硬盘裸拷、内部一半硬引用指向不存在的原始根目录 `/Game/BulletVFX/*`（烟雾/核心闪光材质、弹壳网格死链→灰方块）；修复几经反转（完整链已记 UE_Gotchas §3.7）：搬运 `/Game/Bullet`→`/Game/BulletVFX` 中途因 PIE 触发断言崩编辑器，且崩溃留下的新拷贝是"加载时死引用解析成 None 后存盘"的**洗空残品**（粒子丢材质/材质丢贴图，依赖审计反而显示干净——用户看 Cascade 发现发射器全是无贴图面片才点破）；最终修复=从 `Z:\游戏资源\Unreal\整理后_UnrealAssets_V2\10_VFX_Particles\Bullet` 原始备份把 81 个文件按哈希校验铺进 `Content\BulletVFX`（`Content\Bullet` 经哈希对比与备份逐字节一致未损坏），两目录均为干净原件后包内全部交叉引用（一半指 Bullet 一半指 BulletVFX 的混合死引用）天然自洽。CSV 两表特效路径指 `/Game/BulletVFX/Particles/`、数据已重生成。**④全表死链清扫（2026-06-11）**：用户问"脚步声有了吗"——`FootstepSurfaceAudio.csv` 同病（表本来就配得很全：Default+7 表面、走/跑各 4 变体、音量倍率，但路径全指归档前老位置=一直哑的），已改 `/Game/ThirdParty/Audio/FootstepSoundsPackLite/`；顺势全量审计 `Data/Source/*.csv` 共 139 条 `/Game/` 资产路径——又揪出 `EquipmentDefinitions.csv` 枪械回退网格指老 `FPSWeaponPackVol2`（已改 ThirdParty 路径），现 139/139 全部在盘上有效；配合 ②的 `bTraceComplex` 修复，脚步声按表面区分（混凝土/金属地不同声）首次真正生效。**待**：Listen Server + 客户端 PIE 验证；BP 调抛壳口位置；弹孔贴花素材；换弹 Foley（包里 Insert_Ammo/Pumping/Load 素材现成）列 polish。 | 接通两张"已设计未消费"的表现层表，统一枪械开火表现的数据归属（多枪可共用表现配置）；修复枪声死链。表现纯客户端、不碰权威结算。非目标：长枪（Unit 84）、submix/遮挡（Audio-FX-A）。 | 待验证（Listen Server + 客户端：①两端都有枪声且来自枪口——本机近距脆响、远端闷响不同；②枪口火光 + 一瞬照明 + 抛壳；③打混凝土/金属/木头出对应弹着特效+声音、未注册表面也有 Default 兜底；④打敌人不出弹着特效（仍是血液受击反馈）；⑤空枪且无储备本机干响、有储备自动换弹；⑥伤害/弹药/换弹数值与之前完全一致（纯表现层改动）） |
| 84. Anim-Rifle-A 步枪整身动作系统 + 步枪武器落地（手枪同架构） | 进行中：**84a 重定向已落地**（2026-06-11 headless commandlet 跑通：**524/524 clip** → `/Game/Animation/Weapons/Rifle/AN_Rifle_*`，新建 `IKR_Rifle_Source`+`RTG_RifleToManny`（复用 `IKR_Manny_Target`，auto_align 手臂修正照搬），`Scripts/RetargetRifleAnimations.py`；退出码 1 = 已知无关坏档噪音，成功判据看 `[RifleRetarget] Completed` 标记）。立项侦察 2026-06-11（用户拍板用 `Rifle_01` 包，⚠️ 推翻早先"步枪=Kubold"的设想）。**侦察**：`Content/Rifle_01` = MoCap Online 军用系列 **W2（步枪）**，与 Military_Pistol_01（W1）同家族同骨架（`UE4_Mannequin_Skeleton`）→ 手枪重定向流水线直接复用。内容：In-Place 448（后缀 `_IPC`/`_Loop_IPC`，含八向走/跑/蹲、分段跳、方向急停过渡）+ AimOffset 48（`W2_Stand/Crouch_Aim_Point_*`，与手枪同布局含 45° 斜角）+ 掏/换/射 42（**背后取枪** `Equip_Back_Get_From_MOB`、Fire_Burst/Continuous/Powerful、Reload，**还有 W1↔W2 切枪过渡 4 个**）+ Root_Motion 348（跳过不用）；附赠 `SM_M4_Rifle_01`/`SM_1911_Pistol_01` 武器网格。⚠️ 两个坑：①`ThirdParty/Weapons/Rifle_01` 是 2026-06-10 归档失败留下的**空壳目录**（真包在根目录；归档脚本"dest exists 即 SKIP"会被它挡住，重跑前先删空壳）；②命名前缀是裸 `W2_`（非手枪的 `MIL2_M3_W1_`），重定向脚本的前缀/改名常量要换。**分阶段**：84a 归档搬运+headless 批量重定向（编辑器关窗时跑：删空壳→搬 ThirdParty→`RetargetRifleAnimations.py`（拷 `RetargetPistolAnimations.py` 改常量：源 `/Game/ThirdParty/Weapons/Rifle_01/Animation`、前缀 `W2_`、跳过 Root_Motion 子目录、改名 `AN_Rifle_*`、落 `/Game/Animation/Weapons/Rifle/`）≈538 clip）→ 84b 数据/物品落地（Items/Equipment/Firearm/Ammo 5.56 口径/`Presentation_Rifle_Default` 表现行【火光=VFX_Muzzleflash_02、本地枪声=Gunshot_5或6系列、远处=Distant_3、干响=Dry_Fire_3】+ `BP_Weapon_Rifle`(M4 网格+Muzzle/EjectPort 调位)）→ 84c ABP 整身旁路加 Rifle 分支（手枪同配方：BS 真复制+同位换动画、AO 内嵌换样、pelvis 实测速度匹配、Run 行 947→900 钳位技巧、weight interp 5.0、跳跃 Reset Child）→ 84d 开火/换弹/背后取枪接战斗 → 84e polish（W1↔W2 切枪、蹲过渡、急停；方向急停这包素材比手枪全）。**非目标**：霰弹枪动作化（仍走旧分层）、背挂步枪的视觉挂点。 | 步枪用 `Rifle_01`（W2）整身集，按 Unit 83 手枪已验证架构平移：`EquipmentAnimSet==Rifle` 进整身旁路，idle/八向/瞄准/蹲/跳/开火/换弹全用本套；特效声效走 Unit 87 表现表（加一行即接通枪口/抛壳/远近枪声/弹着/干响全链）。 | 待启动（验收=Listen Server+客户端：步枪整身动作/两姿态/AO/换弹背枪/远近枪声/弹着点全链与手枪同标准） |
| 88. Loadout-A 类型化 5 槽武器栏 + 路由校验 | 已实现（`CoopSurvival` 运行时 + `CoopSurvivalEditor` 编辑器目标 2026-06-12 均整编通过 = Result: Succeeded，含新增 `UENUM ECSLoadoutSlot` 反射、编辑器 DLL 已验证含新符号；**待 PIE**） | 落地 `EquipmentLoadout_开发拆分与功能逻辑说明.md` §1 锁定规格：快捷栏从"4 槽无类型"改为**5 类型槽**（键 1-5 = 主武器/副武器/近战刀/投掷/快速道具，下标即契约 `ECSLoadoutSlot`，住 `CSItemDataTypes.h`）。①**路由**：新增 `UCSItemDataSubsystem::GetLoadoutSlotForItem`（Consumable→快速道具槽且不要求装备定义、其余按 `EquipmentSlotType` 路由 MainHand→主/Sidearm→副/Melee→近战/Throwable→投掷，Head 等穿戴类不进快捷栏返回 INDEX_NONE）；`EquipItem` 改为服务端按类型路由到唯一匹配槽并选中（不再装进"当前选中槽"），路由不到的拒绝；`SelectEquipmentSlot` 槽内容合法性从 IsItemEquipable 改为"路由槽=本槽"（数据改动/旧档错位自动清理）。②**刀常驻**：`EnsureMeleeSlotResident`（近战槽空且背包有近战武器即回填第一件；无刀=徒手槽留空）接 AddItem（捡到首把刀自动入槽）/装备清空/`ClearEquipmentSlotsForMissingItems`/读档各路径，纯状态回填、广播由调用点统一负责（避免双广播）。③**存档迁移**：`SetEquipmentHotbar` 不再按下标照搬，整表按类型重路由进 5 槽（旧 4 槽档自然升级、同槽先到先得、不路由的剔除出栏但物品保留背包），无存档版本号变更（字段 schema 不变）。④**数据**：`EquipmentDefinitions.csv` Knife 槽类型 MainHand→Melee（斧/霰弹枪仍 MainHand=主武器，手枪 Sidearm）；`GenerateGameData.py` 新增 `EQUIPMENT_SLOT_TYPES` 枚举校验（MainHand/Sidearm/Melee/Throwable/Head），已重新生成 Data/Generated。⑤**输入/UI**：键 5 绑定（控制器 BindKey + 背包面板 OnKeyDown 补 Five），HUD `CSHUDUserWidget` 5 槽全接真数据、空槽显示类型提示字（主/副/刀/投/具）；背包面板 `CanEquipItem` 改用路由判定与服务端一致。非目标（后续单元）：消耗品 UseItem（89）、纸娃娃拖放装备（90）、投掷物（91）、HeadBustHood 穿戴化（不再能进快捷栏，等纸娃娃/外观槽收编）。 | 待验证（Listen Server + 客户端：①刀只能进槽 3、手枪进槽 2、斧/霰弹枪进槽 1，从背包 Equip 自动落对应槽并举起；②键 1-5 切换、HUD 第 5 槽显示与选中高亮、空槽显示类型提示字；③丢弃/合成消耗掉槽内物品后槽自动清理，丢刀后若背包还有第二把刀近战槽自动回填；④旧档读档快捷栏内容按类型归位不丢物；⑤HeadBustHood 不再能进快捷栏（提示 does not fit）；⑥主机/客户端行为一致） |
| 89. Consumable-A 消耗品使用系统（含可堆叠属性） | 已实现（运行时 + 编辑器双目标 2026-06-12 整编通过 = Result: Succeeded，编辑器 DLL 已验证含 `FCSConsumableDefinition`/`ServerUseItem` 新符号；中途曾被 Live Coding 挡、关编辑器后补编通过；**待 PIE**） | 落地规格 §2：消耗品"有类型没法用"→ 可用。①**数据**：新表 `ConsumableDefinitions.csv`（ItemId/RestoreHealth/RestoreHunger/RestoreThirst/GrantedBuffIds/RemovedBuffIds），生成器校验（ItemId 须为 Consumable 类型、Buff 引用须存在、**每个 Consumable 物品必须有定义行**否则报错）；`FCSConsumableDefinition` + 子系统装载/`GetConsumableDefinition`。**可堆叠性 = Items.csv 既有 MaxStack**（用户要求"消耗品有可堆叠和不可堆叠的属性"——网格背包的并堆/占格天然按它生效，不另起字段）：示例 5 件含两类对照——绷带 MaxStack=5/口粮 3/水 2/兴奋剂 2（可堆叠）vs 医疗包 MaxStack=1 占 2×2（不可堆叠）。绷带/医疗包止血走 `RemovedBuffIds=Buff_Bleed_Light`（显式按 Id 移除——Buff 的 RemoveOnTags 标签语义 Buff-B 未实现，留给后续 Buff 单元）；兴奋剂走现成 `Buff_Adrenaline`（周期回体力已生效；属性修正等 ApplyBuffModifiers 钩子接通后自然增益）。②**服务端权威使用**：`UCSInventoryComponent::UseItem`（校验 Consumable 类型+定义+持有+Pawn 存活 → `RemoveItem` 扣 1（自动清理快捷栏失效引用）→ `ApplyHealthDelta`/新增 `ApplyNutritionDelta`（饥渴 0-100 钳制）→ 先 `RemoveBuff` 后 `ApplyBuff`）；控制器 `RequestUseItem`/`ServerUseItem` RPC 链与装备同模式。③**双入口**（用户拍板规格）：背包格**右键** = 消耗品"使用"、非消耗品保持原地旋转（消耗品摆放旋转仍可拖拽中按 R）；**快速道具槽（槽 5）选中 + 攻击键** = 使用（`ACSPlayerCharacter::PrimaryAttack` 先判快速槽再走战斗，服务端 UseItem 最终校验）；另补背包紧凑列表行"Use"按钮（仅消耗品显示）。④**获取路径**：LT_BasicLocker 掉落表加 5 种消耗品概率行；GM 面板 Give ItemId 通用入口可直接发放测试。非目标：使用动画/打断（规格允许最小版）、HUD Buff 图标（Buff-H）、使用时长读条。 | 待验证（关编辑器→`CoopSurvivalEditor` 整编→Listen Server + 客户端：①GM 给 Consumable_Bandage×5 → 背包占 1 格叠 x5，医疗包占 2×2 不叠；②扣血后背包格右键绷带 → 血+15、数量-1、用完格子消失；③绷带装进槽 5（Equip 或拖）→ 键 5 选中 → 左键使用、用完槽自动清空；④被敌人打出流血后用绷带 → 流血 Buff 消失；⑤兴奋剂使用后体力恢复加快（8s）；⑥口粮/水恢复饥饿/口渴 HUD 条变化；⑦容器随机出消耗品；⑧主机/客户端一致） |
| 90. UI-Loadout-A 一屏纸娃娃（换装并入背包面板） | 已实现（运行时 + 编辑器双目标 2026-06-12 整编通过 = Result: Succeeded，编辑器 DLL 已验证含 `SCSPaperdollWidget`/`ClearEquipmentSlot` 新符号；**待 PIE**） | 落地规格 §3 一屏纸娃娃（用户拍板"换装最好和 B 背包在一个界面"）。①**新控件** `UI/SCSPaperdollWidget`（背包面板左列，面板加宽 1180→1400）：**Loadout 区** 5 类型武器槽行（键位+类型标+槽内物名；LMB=选中举起、RMB=卸下回背包（新链 `ClearEquipmentSlot`/`RequestClearEquipmentSlot`/`ServerClearEquipmentSlot`，近战槽受刀常驻约束）；**接受背包网格拖放**——类型匹配本槽亮绿、不匹配亮红（客户端预判用与服务端同一路由 `GetLoadoutSlotForItem`），落下= RequestEquipItem 服务端权威路由装备）；**Appearance 区** 8 外观槽行（当前穿戴名实时显示（读角色 `AppearanceComponent`，与原换装面板同模式）；Change 下拉=该槽全部可穿件（穿着高亮）+ Unwear；RMB=直接脱；**接受拖放**——拖 Apparel 物品到同槽位行=穿上）；底部 Reset Outfit。改动全走既有服务端权威链（装备请求/`CSWearAppearance` 穿戴流），纸娃娃零私有状态。②**J 面板退役**（AGENTS 同单元清理）：删除 `SCSWardrobePanelWidget` .h/.cpp；控制器删 Toggle/Close/RequestClose/Create/RemoveWardrobePanel、`bWardrobePanelOpen`、`WardrobeWidget` 及全部调用点；**J 键保留**改绑 ToggleInventoryPanel（=B），背包面板内 J 也可关闭。③英文槽位标签（默认 Slate 字体不渲染 CJK，与原换装面板同策略）。非目标：3D 角色预览视口、装备图标美术、UCSUITheme 全面接入（背包面板整体仍是 UMG 迁移单元的范围）。 | 待验证（关编辑器→编辑器目标整编→Listen Server + 客户端：①B/J 都开背包面板，左列见 5 武器槽+8 外观槽；②拖刀到 Melee 槽亮绿落下=装备，拖刀到 Primary 槽亮红落不进；③武器槽 LMB 选中（HUD 同步高亮）、RMB 卸下、刀槽卸下后有第二把刀自动回填；④外观行 Change 下拉穿/脱实时生效（与原 J 面板行为一致）、RMB 快速脱、Reset Outfit 复位默认套装；⑤拖 Apparel_* 到对应外观行=穿上；⑥原 J 独立面板不再出现；⑦主机/客户端一致） |
| 91. Throwable-A 投掷物（简易手雷 + 冲击力） | 已实现（运行时 + 编辑器双目标 2026-06-12 整编通过 = Result: Succeeded，编辑器 DLL 已验证含 `CSThrowableProjectile`/`GetThrowableDefinition` 新符号；**待 PIE**）。坑：UE5.8 `FOverlapResult` 需单独 `#include "Engine/OverlapResult.h"` | 落地规格 §2 投掷 + 用户要求"简单做个手雷，带冲击力"（无模型→引擎球占位）。①**数据**：新表 `ThrowableDefinitions.csv`（DamageType/爆心伤害/半径/引信/投速/**冲击力**/噪音响度/冷却/ProjectileClassPath），生成器双向校验（投掷物必须有 Throwable 装备行、Throwable 装备行必须有投掷定义）；`Item_Grenade`（Weapon 类型、**MaxStack=3 可堆叠**、装备行 EquipmentSlotType=Throwable→路由进槽 4、手持=引擎球网格 0.12 占位）；LT_MutantCorpse_Brute 掉落 25%。②**投掷链**：`UCSCombatComponent::TryThrowEquipped`/`ServerThrowEquipped`/`ThrowEquippedOnServer`（与开火/换弹同模式：校验存活/定义/持有/冷却 → RemoveItem 扣 1 → 控制器视点方向+0.12 抬角、胸口前出手（不用相机位防穿模）→ 生成复制投掷物；类从数据 ProjectileClassPath 读、回退 C++ 类）；攻击键分流在 `ACSPlayerCharacter::PrimaryAttack`（投掷槽选中→投掷，与快速道具槽分流合一）。③**投掷物 Actor** `Weapons/CSThrowableProjectile`（复制+复制移动）：球碰撞 + ProjectileMovement（重力/反弹 0.35/摩擦）+ 忽略投掷者；服务端引信到时 `Explode`（只一次）——半径 Overlap（Pawn/PhysicsBody/WorldDynamic）→ 隔墙 LoS 不结算 → 伤害=爆心线性衰减（0.15 保底）→ **Armor-A `MitigateDamage` 统一减伤口** → `ApplyDamage` + `ICSHitReactable` 受击表现（与枪械命中同链，玩家友伤同样吃）；**冲击力**=物理组件 `AddRadialImpulse`（线性衰减速度变化量）+ 角色 `LaunchCharacter` 径向击退+0.35 抬升；`UAISense_Hearing` 爆点上报响度 3.0（枪声 2.5，引怪更远）；多播爆炸表现（占位：复用弹着 Default 表面特效 ×4 放大 + 音效变体低音调）。非目标（后续）：蓄力/投掷动画与抛物预览线、正式爆炸特效/震屏、`BP_Throwable_Grenade` 内容壳（数据 ProjectileClassPath 已留口子，等编辑器空闲时建）、对世界可破坏物。 | 待验证（关编辑器→编辑器目标整编→Listen Server + 客户端：①GM 给 Item_Grenade×3 → 背包 1 格叠 x3、Equip 进槽 4；②键 4 选中手雷（手上有小球）→ 左键朝准星投出、抛物线落地反弹滚动、3 秒后爆炸（特效+爆音两端都有）；③炸到敌人掉血+受击反应+被炸飞击退、近敌伤害高远敌低；④炸到自己/队友也掉血（友伤）+击退；⑤隔墙不吃伤害；⑥附近敌人闻爆炸声来调查；⑦扔完 3 颗槽 4 自动清空；⑧冷却 0.8s 内连点只出一颗；⑨主机/客户端弹道与爆炸一致） |
| 92. Loadout-B 装备进槽不占背包格（槽=真容器） | 已实现（运行时 + 编辑器双目标 2026-06-12 整编通过 = Result: Succeeded，含复制属性 `EquipmentSlots` 反射改动；**待 PIE**） | 回应用户验收反馈"装备在手上就不该在背包占格子"——把 Loadout-A 的"槽=引用网格物品"改成**塔科夫式"槽=真容器"**。①**数据归属**：复制属性 `TArray<FName> EquipmentHotbar` 替换为 `TArray<FCSInventoryStack> EquipmentSlots`（COND_OwnerOnly，持物品+数量、GridX 恒 -1）；**装备=整摞从背包网格移出进槽**（`EquipStackByIndex`：先移出腾格→同槽旧装备自动摆放放回网格（放不下整次回滚拒绝交换）→入槽并选中）；**卸下=槽内整摞移回网格**（`ClearEquipmentSlot`：自动摆放，背包满=拒绝卸下不丢物）；`EquippedItemId` 改为派生（`RefreshEquippedFromSelectedSlot`=选中槽内容物）；刀常驻改为"从网格**移**第一摞近战进槽"；`SelectEquipmentSlot` 不再做网格数量/路由校验（槽自持）。`RemoveItem`/`RemoveStackByIndex`/`SetInventoryStacks` 删除全部"装备引用清理"逻辑（`ClearEquipmentSlotsForMissingItems`/`SetEquipmentSlotItem` 删除）——**制作/丢弃/存取容器等网格路径再也吃不到已装备物品**。②**消耗路径分流**：快速槽使用=新 `UseEquippedQuickItem`（槽内扣 1，新 RPC 链 `RequestUseEquippedQuickItem`），背包格右键仍走 `UseItem`（网格扣 1），共用提取的 `TryGetUsableConsumable`/`ApplyConsumableEffectsToPawn`；投掷=`ConsumeEquippedSlotItem(投掷槽,1)`（战斗组件不再 RemoveItem，且服务端校验选中槽=投掷槽）。③**负重**：`UCSAttributeComponent::ComputeCurrentLoad` 网格+槽内物都计重（装备仍背在身上）。④**存档**：`CurrentSaveVersion` 7→8；`FCSPlayerSaveData` 加 `EquipmentSlotStacks`（旧 `EquipmentHotbar` 字段保留仅供迁移读取）；读档按字段存在性分流——v8 走 `SetEquipmentSlots`（整体覆盖+路由校验，不合法槽内容退回网格不丢物），v7 及更早走 `SetEquipmentHotbar` 旧档迁移（逐个按名 EquipItem=把物品从网格移进槽，同槽先到先得）。⑤**UI**：HUD 快照拆 `EquipmentHotbar`（名）+新 `EquipmentHotbarQuantities`（量）；纸娃娃槽行显示 xN、拖放改 `RequestEquipInventoryStack(EntryIndex)` 精确装备被拖那摞；装备后物品从背包网格消失、卸下回网格。 | 待验证（关编辑器→编辑器目标整编→Listen Server + 客户端：①装备刀/枪后背包网格里**不再占格**、紧凑列表也不显示，纸娃娃槽显示该物品；②卸下（纸娃娃 RMB）物品回网格占格；③背包塞满后卸下被拒绝（提示 backpack full）、物品仍在槽里；④装备第二把主武器=旧的自动回背包（交换）、背包满则拒绝交换；⑤槽 5 绷带 x5 用一次变 x4、用完槽清空，**背包网格里的绷带数量不动**；⑥手雷扔完 3 颗槽清空、网格里另一摞手雷不动；⑦负重数值在装备/卸下前后不变（槽内照样计重）；⑧v7 旧档读档：原快捷栏物品自动移出网格进槽、网格腾出格子、不丢物；⑨主机/客户端一致） |
| 93. FirearmFX-B 战斗一次性声音 3D 距离衰减 | 已实现（运行时 + 编辑器双目标 2026-06-12 整编通过 = Result: Succeeded，编辑器 DLL 已验证含 `GunshotSoundRange`/`ImpactSoundRange`/`ExplosionSoundRange` 等新反射符号；**待 PIE**） | 回应用户"枪的击中声音做成 3D、按距离调大小"。诊断：枪声/弹着/爆炸全走 `PlaySoundAtLocation` 且不传衰减、音频资产自身也没配衰减设置 → **全场等响（等于 2D）**；项目里只有 NPC 语音（ATT_VoiceOccluded）配了衰减，脚步声组件的 `FootstepAttenuation` 属性存在但未赋资产=同病。①**共享运行时衰减** `Audio/CSOneShotAudio`（命名空间工具，非 UObject 类）：`GetSharedAttenuation(MaxRange)` 运行时构造 `USoundAttenuation`（NaturalSound 自然衰减 + bSpatialize 空间化 + **远距低通**越远越闷（35% 距离起渐闷到底）+ 内圈全响=距离 5% 夹 100-400cm），按"距离取整百"档位缓存、transient 包 AddToRoot 常驻（档位有限）；`PlayAt(...)` 封装播放。**不依赖编辑器衰减资产**、距离走数据表；将来音频侧要精调曲线可换 ATT_* 资产、调用点不变。②**接入点**：枪声（本机近距变体贴枪口=内圈全响不影响手感；其他端按距离衰减——与近/远变体池叠加：变体管音色、衰减管音量）+ 干响 + **弹着点声音**（用户点名）+ 手雷爆炸声（注：投掷物 Definition 不复制，客户端用类型默认 20000）+ **脚步声兜底**（`FootstepAttenuation` 资产未配时用共享衰减，修"队友脚步全图等响"，新 `FootstepMaxAudibleRange`=2500 可调）。③**数据列**：`FirearmPresentationDefinitions.csv` 加 `GunshotSoundRange`（120m）、`SurfaceImpactEffectSets.csv` 加 `ImpactSoundRange`（35m，逐表面可调）、`ThrowableDefinitions.csv` 加 `ExplosionSoundRange`（200m）；生成器校验 + 已重生成。纯客户端表现层，不碰 AI 听觉（`UAISense_Hearing` 另一套）与权威结算。 | 待验证（关编辑器→编辑器目标整编→Listen Server + 客户端，戴耳机听：①客户端站远处听主机开枪：近=响亮清脆、拉远=变轻变闷、再远（>120m）听不见；左右站位声音有方位；②打远处墙面弹着声明显比打脚边墙轻；③手雷在远处爆=低闷一声、近处爆=震耳；④队友在另一房间跑动脚步声不再像贴耳边；⑤自己开枪手感不变（贴枪口全响）；⑥干响仍正常） |
| 103. UI-UMG-GMPanel GM 调试面板迁 UMG（迁移路线第 2 单元） | 已实现（运行时 + 编辑器双目标 2026-06-15 整编通过 = Result: Succeeded；`WBP_GMPanel` 已生成（`[CSWidgetGen] OK /Game/UI/GM/WBP_GMPanel`）；新 `UCSGMPanelUserWidget` UCLASS 反射；**待 PIE**） | Slate→UMG 迁移路线第 2 单元（继 Coop），复用等宽字体 + Coop 表单模板。先 Explore 把 Slate `SCSGMPanelWidget` 1:1 测绘（6 段 / 27 命令按钮 / 9 输入框 / 2 个 0.5s 刷新调试文本）。**功能 parity**：`UI/CSGMPanelUserWidget`——9 输入框 + 2 调试文本 BindWidgetOptional，27 命令按钮**按名解析后绑定**（cpp 内 `BINDGM` 局部宏，`AddDynamic` 靠 `STATIC_FUNCTION_FNAME` 从 `&Class::Fn` 取名可嵌套，省 27 个 UPROPERTY），全路由到 `ACSPlayerController` 既有 Exec 方法（Print/Save/Load/Unlock*/Set*/Complete/Craft/Give/Drop/Workbench/Host/Find/Join/Destroy/SpawnDummy/Upgrade...）；`NativeTick` 0.5s 刷世界进度 + 最近电梯层（`FindNearestElevator` TActorIterator）；`NativeOnKeyDown` Esc/F10 关（`RequestCloseGMPanel`）。生成器 `BuildGMPanelWBP`→`/Game/UI/GM/WBP_GMPanel`（全屏 scrim + 右对齐 620 宽面板 + 标题/关闭 + `UScrollBox`[6 段，段/按钮行/输入行/双输入行助手]，等宽字 + 暗底输入框）。`Scripts/CreateGMPanelWBP.py`。控制器 `CreateGMPanel`/`RemoveGMPanel`/`ApplyMenuInputMode` GM 焦点分支 + 成员 `TSharedPtr SCSGMPanelWidget`→`UPROPERTY UUserWidget* GMPanelUMGWidget`。**踩坑**：按钮处理名 `OnDrop` 与 UUserWidget 拖放事件 `OnDrop` 撞 → 改 `OnDropItem`。旧 Slate `SCSGMPanelWidget` 退役暂留回退。 |
| 104. UI-UMG-Inventory（砖3 InvShell + 砖4 纸娃娃）背包整屏迁 UMG | 已实现（运行时 + 编辑器双目标 2026-06-15 整编通过 = Result: Succeeded；**砖3+砖4** 共 6 个 WBP 已生成（`[CSWidgetGen] OK` ×6：`WBP_DragGhost`/`WBP_BackpackGrid`/`WBP_ListRow`/`WBP_PaperdollSlotRow`/`WBP_Paperdoll`/`WBP_InventoryPanel`，外加砖2 的 `WBP_GMPanel`）；新 7 个 UCLASS 反射（砖3 的 5 个 + 砖4 的 `UCSPaperdollSlotRowUserWidget`/`UCSPaperdollUserWidget`）；**2026-06-15 性能修复 + 砖4 纸娃娃 + 砖4 收尾（删旧 Slate + 丢弃数量选择补回）全部完成**，双目标重整编 Succeeded；**迁移路线四砖全做完，背包整屏纯 UMG，旧 Slate 已删**；**待 PIE 整体验收**） | Slate→UMG 迁移路线**第 3 单元**（最复杂屏），按砖推进。**砖1 拖拽地基**：`UI/CSInventoryDragDropOp`（`UDragDropOperation` 子类，带 `SourceKind`(PlayerInventory/LootSource)/`EntryIndex`/`ItemId`/`Quantity`/`BaseWidth/Height`/`bRotated`/`GrabCellX/Y`/`CellPx`/`bValidDrop`/`OwnerController`，`GetEffectiveWidth/Height` 按旋转转置）+ `UI/CSDragGhostUserWidget`（拖拽幽灵，`Setup(色,字形)`）。**砖2 网格** `UI/CSBackpackGridUserWidget`（UMG 网格 = 旧 `SCSBackpackGridWidget` 的 UMG 版）：`InitGrid(源)`、`ReadGridState` 读快照（背包读 `InventoryStacks/BackpackGridWidth/Height`、容器读 `OpenLootContainerItems/Grid*`）、`NativeTick` 尺寸/库存变才重建、`ScreenToCell` 命中格、`CanFootprintFit`（边界 + AABB 重叠，同格拖自身忽略）、`RefreshHighlightForOp` 落点高亮、`NativeOnDrop` 4 路由（背包/容器 × 本格/跨格 → 控制器既有 `RequestTakeLootItemToGrid`/`RequestDepositItemToLoot`/`RequestMoveLootStack`/`RequestMoveInventoryStack`）；幽灵从 `WBP_DragGhost` 加载。**砖3 列表行 + 主壳**：`UI/CSListRowUserWidget`（通用行，因 UMG 按钮 `OnClicked` 不能绑 lambda → 行做成 UserWidget：图标+名+材料行+最多 3 钮；`SetupInvItem`=使用/装备/丢弃、`SetupRecipe`=合成；钮派发到控制器既有 `RequestUseItem/EquipItem/DropItem/CraftItem`）；`UI/CSInventoryPanelUserWidget`（主壳，5 区：装备状态/背包网格宿主+物品列表/合成段+配方列表/搜刮段+网格宿主+搜索进度/属性栏；`EnsureGrids` 运行时 CreateWidget 两个网格进宿主；`NativeTick` 刷快照变才重建列表（InvHash/RecipeHash）+ 属性 + 段显隐；输入 R 旋转拖拽中物品（`GetDragDroppingContent` + 转置抓取格 + 两网格重算高亮）/1-5 选槽/Esc/B/J 关）。生成器 `BuildDragGhostWBP`/`BuildBackpackGridWBP`/`BuildListRowWBP`/`BuildInventoryPanelWBP`（主壳=全屏 scrim + 1760×820 面板 + 头部 + 4 列 HBox；合成/搜刮段外层 Border 命名 `CraftingSection`/`LootSection` 供 BindWidget，默认折叠运行时按 `bCraftingPanelOpen`/`bLootContainerPanelOpen` 显隐）。`Scripts/CreateInventoryWBP.py`（依赖序建 4 件）。控制器 `CreateInventoryPanel`→`LoadClass WBP_InventoryPanel→CreateWidget→AddToViewport(20)`、`RemoveInventoryPanel`→`RemoveFromParent`、`RefreshInventoryPanel`/`ApplyMenuInputMode` 焦点分支换 UMG、成员 `TSharedPtr SCSInventoryPanelWidget InventoryWidget`→`UPROPERTY UCSInventoryPanelUserWidget* InventoryUMGWidget`、4 处 `IsValid()` 守卫换非空。**踩坑**：本地变量 `Slot` 撞 `UWidget::Slot` 成员（warning-as-error C4458）→ 改 `CSlot`。**非目标=砖4**：纸娃娃 13 槽迁 UMG（UMG 网格产 `UCSInventoryDragDropOp`，旧 Slate 槽只收旧 `FCSInventoryGridDragDropOp`，故纸娃娃必须一并 UMG 化），完成后删旧 `SCSBackpackGridWidget`/`SCSInventoryPanelWidget`/`SCSPaperdollWidget`/`FCSInventoryGridDragDropOp`。本砖装备走列表"装备"钮 + 数字键选槽（不丢功能、不回退）。**PIE 性能修复（2026-06-15，用户反馈"第一次拖拽很卡 + 每次拖拽全量刷文字，应只刷那个 item"）**：①**首拖卡**=拖拽幽灵 WBP 类在 `NativeOnDragDetected` 首次同步 `LoadClass` 命中磁盘→`NativeConstruct` 预载缓存 `GhostClass`（面板侧同样预载 `RowClass`）。②**整屏文字每帧刷**=面板 `NativeTick` 每帧无条件 `UpdateAttributes`/`UpdateConditionalSections` 重拼 FString+SetText（即便内容不变，新建 FText→SetText 仍触发 Slate 重排）→属性栏加**值哈希门控**（`LastAttrHash` 仅真值变才 SetText），合成/搜刮段拆**结构哈希门控**（显隐/静态文本仅结构变刷一次）+**活动期实时值**（剩余秒/搜索% 仅合成/搜刮进行时逐帧更新，空闲零触达）。③**列表整体重建**=`RebuildInvList`/`RebuildRecipeList` 原 `ClearChildren`+全行 `CreateWidget` 重建→改**行复用池**（`InvRowPool`/`RecipeRowPool`，复用既有行只重填数据、多余行隐藏不删，SetText 对相同内容自去重→只有真变那行重排）= 用户要的"只刷那个 item"。`RefreshFromSnapshot` 连带作废 4 个哈希。网格物品层本就"数据变才重建一次"（拖动中不动）未改。纯逻辑改但新增 `UPROPERTY` 成员（缓存类/行池）= 反射变更需关编辑器整编（Live Coding 顶不了）。**砖4 纸娃娃迁 UMG（2026-06-15，回应用户"背包没做完"——新 UMG 面板左列原只是 EquippedSlotsText 文本占位，缺旧 Slate 那套可视纸娃娃 + 换装）**：`UI/CSPaperdollSlotRowUserWidget`（一行两用 Loadout/Appearance，**接收 UMG 版 `UCSInventoryDragDropOp`**——这正是砖4 必做的原因：旧 Slate 槽只认旧 `FCSInventoryGridDragDropOp`）：武器槽=键位+类型+槽内物 xN，LMB 选中/RMB 卸下/拖物进来类型匹配亮绿落下 `RequestEquipInventoryStack`；外观槽=当前穿戴名 + 「更换…」下拉(`UComboBoxString` 选项[0]粘性"更换…"/[1]"(脱下)"/[2..]全部可穿件，`OnSelectionChanged` 守 `ESelectInfo::Direct` 防程序化复位误触)+RMB 脱+拖换装件进来穿上；行 `NativeConstruct` 设 self `Visible`（默认 SelfHitTestInvisible 不收拖放）、`NativeTick` 值门控刷显示。`UI/CSPaperdollUserWidget`（容器：OPERATOR 头 + 5 武器槽 + 8 外观槽 + 复位套装；`NativeConstruct` 一次性建 13 行，外观候选按 `TryGetSlotFromName` 分组；全走控制器既有 `RequestSelectEquipmentSlot`/`RequestClearEquipmentSlot`/`RequestEquipInventoryStack`/`RequestEquipItem`/`CSWearAppearance`/`CSUnwearAppearance`/`CSResetAppearance`，零私有状态）。生成器 `BuildPaperdollSlotRowWBP`/`BuildPaperdollWBP`；`BuildInventoryPanelWBP` 列1 由 `EquippedSlotsText` 文本改为 `PaperdollHost`(Overlay)、4 列宽重平衡 0.22/0.36/0.26/0.16；面板 `EnsureGrids` 运行时 CreateWidget(WBP_Paperdoll) 进 PaperdollHost。`Scripts/CreateInventoryWBP.py` 加 2 件（依赖序）。**砖4 收尾（2026-06-15，用户"先做完背包"）：删旧 Slate** `SCSInventoryPanelWidget`/`SCSBackpackGridWidget`(含 `SCSDiscardDropZone`+`FCSInventoryGridDragDropOp`)/`SCSPaperdollWidget` 共 6 文件（先核对：控制器对它们只剩注释引用、无 `#include`、无外部引用——全孤立）。**补回丢弃数量选择**（旧丢弃区 = 拖到 `SCSDiscardDropZone`→`RequestDropItemInteractive`(>1 弹 QtyInput/关键物拒丢)，新面板原列表"丢弃"钮只丢 1=回退）→ `UCSListRowUserWidget` 加 `RowQty`（`SetupInvItem` 传总量），`OnBtn3` 由 `RequestDropItem(id,1)` 改 `RequestDropItemInteractive(id, RowQty)`，丢弃即走数量选择/关键物保护（无需复刻拖放丢弃区，按钮即覆盖且更易发现）。双目标整编 Succeeded。**背包迁 UMG 四砖全完成，旧 Slate 清零。** | 待验证（关编辑器→已整编→Listen Server + 客户端：①B/J 开背包面板 = UMG 版（等宽字、SCP 配色、5 区布局），Esc/B/J 关；②背包网格里物品按 footprint 占格、紧凑物品列表（使用/装备/丢弃钮）并存；③拖背包格物品到另一格 = 移动（落点高亮绿/红、放不下回滚）、拖动中按 R 旋转转置高亮；④开容器（搜完）后右侧搜刮区出网格，拖容器→背包 = 取物、拖背包→容器 = 存物、容器内拖动 = 整理；⑤列表"装备"钮 + 数字键 1-5 选槽仍能装备（与旧版一致）；⑥工作台旁开 = 合成段出现、配方行材料够变绿可点合成、合成中进度条走、状态文字；⑦属性栏生命/体力/饥饿/水分/护甲/负重 + 隐藏战斗属性显示；⑧主机/客户端一致；**性能复验**：⑨第一次拖拽不再明显卡顿；⑩拖拽中/静止时属性栏文字不再持续刷新（值没变就不动）；⑪移动单个物品时只有该物品所在行变化、其余行不被推倒重建；**砖4 纸娃娃**：⑫面板左列出现纸娃娃（5 武器槽 + 8 外观槽 + 复位）；⑬从背包网格拖刀到近战槽亮绿落下=装备、拖到主武器槽亮红落不进；⑭武器槽 LMB 选中（HUD 同步）/RMB 卸下回背包；⑮外观槽「更换…」下拉穿/脱实时生效、RMB 快速脱、拖 Apparel 件到对应槽=穿上、复位套装复原；⑯主机/客户端一致） |
| 102. UI-HUD-B 游戏 HUD 接真数据（拆饥饿/水分 + 体力条情境式 + 世界时钟 + 等宽 + 去占位） | 完成（运行时 + 编辑器双目标 2026-06-14 整编通过 = Result: Succeeded；`WBP_HUD` 已重生成；**用户 2026-06-14 PIE 通过 ✅**） | 用户："HUD 数据都是假的吧，先把游戏 HUD 做一下；玩家应该有体力条"。先据实更新 HTML HUD 稿（标真/灰显占位）给用户审，定两点：饥饿/水分**拆两条**、占位**先不画**。**真相**：HUD 大部分本就真（生命/饥饿/水分/负重/弹药/5槽/天数都已在快照），假的只是污染/Buff/目标/深度/完整度；**体力数据真但 HUD 没画**。落地（`BuildHUDWBP` 生成器 + `UCSHUDUserWidget` 控件）：① **左上**生命(红)/饥饿(琥珀)/水分(青) 三条真值条（原"饱食·水分"合并条拆成饥饿+水分两条）+ 负重 chip；去掉污染条、收容失效 chip、Buff 行。② **体力条（情境式）**：武器栏正上方细条 `HudStaminaWrap`+`BarStamina`，控件 `NativeTick`：冲刺(`bIsSprinting`)或体力未满→显示，满且空闲→`FInterpTo` 平滑淡出 `Collapsed`，低于 25% 转红。③ **右上**`HudClock` 第 N 日·HH:MM（`SurvivalDay`+`ServerWorldTime` 小时浮点）；去目标占位。④ **右下**只留 AMMO；去深度/完整度。⑤ **全 HUD 文字换等宽**（`MakeText`→`FCSUIStyle::GetMonoFont`，UI-Font-A 的 CJK 回退保中文）。武器栏 5 槽/准星/E 提示/AIC 旁白不变。**非目标（待各自系统）**：污染条、Buff 行接 `UCSStatusEffectComponent`、目标接任务系统、深度/完整度跟踪、交互提示接真。 |
| 101. UI-Font-A 终端等宽字体（IBM Plex Mono + CJK 回退）+ 主菜单 feed/旁白修正 | 完成（运行时 + 编辑器双目标 2026-06-14 整编通过 = Result: Succeeded；字体资产 `F_PlexMono` + 3 FontFace 已生成，`WBP_MainMenu`/`WBP_NarrativeOverlay` 已重生成；**用户 2026-06-14 PIE 通过 ✅**（含 AIC 旁白字号调小一档）） | 回应用户 PIE 反馈（主菜单 SCP feed 没边缘渐隐/没涂改黑块/字体不同 + AIC 旁白结构不对）。**根因**：项目从没导过等宽字体，整个主菜单的拉丁文字（HTML 里全等宽 `--mono`）退成默认 Roboto。**① 字体地基**：下载 IBM Plex Mono(Regular/SemiBold，OFL) + 引擎 DroidSansFallback(CJK 回退) 入 `Content/UI/Fonts/`；C++ `BuildMonoFontComposite` 组装 Runtime `UFont F_PlexMono`（DefaultTypeface=Regular+Bold 拉丁等宽，**FallbackTypeface=DroidSansFallback** → 中文等宽不豆腐块，同 HTML"拉丁 mono+中文回退"）；`FCSUIStyle::GetMonoFont(size,bBold)` 加载（缺失回退 Roboto）；`Scripts/CreateMonoFont.py` 编排。**② 主菜单换等宽**：生成器加 `MakeMonoText`，kicker/英文副标/菜单项/页脚/封锁牌/印章/feed 换等宽（大标题保持非 mono）。**③ feed 重做**：单 `FeedText`→逐行 `FeedRow0..11`（VBox），`TickFeed` 逐行设文+底部对齐+**顶部几行降 RenderOpacity 做边缘渐隐**；`PickFeedLine` 用 **█(U+2588) 实心删改块**替 `######`（SCP 经典涂改，打字逐块涂黑）。**④ AIC 旁白结构修正**（U99 照错参照）：HTML `.hud-bl` 是 **HUD 左下角浮动文字**（左30/距底30、**无框/无底/无强调条/无扫描线**、靠投影压背景），不是底部居中终端框；生成器重写为左下角浮动（`.who` 等宽冷青字间距 + `.subline` 大字投影），与 NPC 对话框（居中+头像）区分。**踩坑**：网络下字体需放开沙箱（GitHub raw 重置→jsdelivr）；编辑器开着会自动导入 Content 散 TTF；Latin-only 等宽套中文要 CJK 回退（见 UE_Gotchas）。 |
| 100. UI-UMG-Coop 合作会话面板迁 UMG（Slate→UMG 迁移路线第 1 单元） | 已实现（运行时 + 编辑器双目标 2026-06-14 整编通过 = Result: Succeeded；含新 `UCSCoopSessionUserWidget` UCLASS 反射；`WBP_CoopSession` 已生成；**待 PIE（Listen Server + 客户端）**） | 剩余手写 Slate 屏迁 UMG 的**第 1 单元**（路线：Coop→GMPanel→DragCore→InvShell→Paperdoll→Grid，见当日摘要）。最小自包含面板，干净重验整套迁移流程（数据基类 + BindWidgetOptional + 生成器 + Python + 控制器换挂），产出"模态表单+状态条+按钮行"模板供 GM 面板复用。**功能 parity**：`UI/CSCoopSessionUserWidget`（`NativeTick` 轮询 `UCSPlatformServiceSubsystem` 刷状态条 + 5 按钮主持/搜索/加入/销毁/关闭路由到既有 `ACSPlayerController` 方法，自处理 ESC/F3 关）；生成器 `BuildCoopSessionWBP`→`/Game/UI/Coop/WBP_CoopSession`（全屏 scrim + 居中终端面板 + 标题/关闭 + 状态条 + 4 等宽按钮，FCSUIStyle 着色 + 入场淡入上滑）。控制器 `CreateCoopSessionPanel`→`LoadClass→CreateWidget→AddToViewport(30)`、`RemoveCoopSessionPanel`→`RemoveFromParent`、`ApplyMenuInputMode` coop 焦点分支→`CoopSessionUMGWidget->TakeWidget()`、成员换 `UPROPERTY UUserWidget*`；`bCoopSessionPanelOpen` 标志/延迟关不变。旧 Slate `SCSCoopSessionPanelWidget` 退役暂留回退。 |
| 99. UI-Narrative-A AIC 旁白字幕迁 UMG 终端质感 + 动效 | 完成（运行时 + 编辑器双目标 2026-06-14 整编通过 = Result: Succeeded；含新 `UCSNarrativeOverlayUserWidget` UCLASS 反射；`WBP_NarrativeOverlay` 已生成；**结构在 U101 修正为 HUD 左下角浮动 + 字号调小，用户 2026-06-14 PIE 通过 ✅**） | 回应"AIC 旁白 UI 丑"：旁白字幕是全项目唯一没过"SCP 终端×grimdark"美化的一块（纯 Slate 黑框 `SCSNarrativeOverlay`，引擎默认字体/白盒、无边框/动效）。把它按对话框/HUD 同套 UMG 迁移：**数据基类** `UI/CSNarrativeOverlayUserWidget`（`UUserWidget`，自驱 `NativeTick` 轮询 `UCSNarrativeDirectorSubsystem`，仅"有当前行且**非** NPC 会话"=AIC 旁白时显示，刷新说话人/台词；与对话框 `UCSDialogueUserWidget` 同构、互斥）；**生成器** `BuildNarrativeOverlayWBP`→`/Game/UI/Narrative/WBP_NarrativeOverlay`（底部居中终端框：1px 描边 + inset 暗底 0.9 + 左 cold 强调竖条 + 琥珀 Bold 说话人标签 + ink-hi 自动换行台词 + 低透明扫描线，框 ClipToBounds）；**Python** `CreateNarrativeOverlayWBP.py`；**控制器** `CreateHUD`/`RemoveHUD` 用 UMG 版替换 `SNew(SCSNarrativeOverlay)`（`LoadClass`→`CreateWidget`→`AddToViewport(11)`），`UPROPERTY NarrativeOverlayUMGWidget` 防 GC，仍 HitTestInvisible、仍 `NotifyLocalControllerReady`。**动效**（`NativeTick`）：整框入场淡入上滑（+12px，0.22s ease-out）/ 每换一行台词解码揭示（淡入+6px 上滑，0.28s）/ 细扫描线框内漂移（3.2s 循环）。旧 Slate `SCSNarrativeOverlay`/include 暂留作退役回退，PIE 确认后删。**非目标**：打字机逐字（现为整行解码）、无线电杂讯音质（音频层）、头像（旁白无头像，NPC 对话才有）。 |
| 98. UI-Modal-D 深化游戏内模态/Toast：丢弃数量 QtyInput + 合成结果 Toast | 已实现（运行时 2026-06-14 整编通过 = Result: Succeeded；**无反射变更**→编辑器可 Live Coding 热编；**待 PIE**） | 把已验证的模态/Toast 系统接进真实玩法，用上此前没消费方的 **QtyInput** 模态。**① 丢弃数量**：背包丢弃区 `SCSDiscardDropZone::OnDrop` 原来直接 `RequestDropItem(整摞)`，改调新入口 `ACSPlayerController::RequestDropItemInteractive(ItemId, MaxQty)`——关键物客户端先弹 Danger Toast「关键物品不可丢弃」直接返回（服务端 `DropItemOnServer` 仍是权威拦截，不双弹）；摞内单个/无 UI 层→直接丢（原行为）；多个→弹 **QtyInput 模态**（min1/max=摞数/默认全丢，"全部"按钮取 max）→确认后 `RequestDropItem(选定数)`。模态纯本地、丢弃仍走服务端权威；模态叠在打开着的 Slate 背包面板之上（首个"模态叠 Slate 面板"真实消费，UI-Modal-B 已铺路：关掉后 restorer=`ApplyMenuInputMode` 把焦点还回背包）。**② 合成 Toast**（`UCSCraftingComponent`，在控制器上，经 `Controller->ClientShowToast`）：合成完成（计时结束、玩家可能没盯面板）→ Ok「合成完成：{物品} ×{N}」；产物放不下→ Danger「合成失败：背包放不下产物」；材料不足→ Warn「材料不足：{物品} 需 {X}，现有 {Y}」。物品显示名服务端经 `ResolveCraftItemName`（查 `UCSItemDataSubsystem` 的 `DisplayName`，缺失回退 ItemId）解析后随 RPC 发给属主客户端。经聚焦对抗审查（模态叠 Slate 面板焦点/输入 + 合成 server→client 路径）= 无真实缺陷。**非目标（后续）**：拾取/目标完成/解锁等更多事件 Toast（目标完成与 AIC 叙事字幕重叠，需甄别）、卖出/转移数量也走 QtyInput、危险 GM 操作 Confirm。 |
| 97. UI-Modal-C 游戏内暂停/系统菜单（ESC）经 UI 层管理器接入 | 完成（运行时 + 编辑器双目标 2026-06-14 整编通过 = Result: Succeeded；WBP_PauseMenu 已生成；含还原 HTML 的入场动效；**用户 2026-06-14 PIE 通过 ✅**） | 让模态系统在游戏里有"非 Toast"的第一个真正用途，并填上"游戏内 ESC 什么都不做"的缺口（设计早定"HUD 上 ESC = 开暂停菜单"）。**管理器泛化（纯增量，模态逻辑不动）**：`UCSUILayerSubsystem` 加 `ScreenStack`（全屏 UMG 层，Z200，在 HUD/对话/面板之上、模态 Z300 之下）+ `PushScreen`/`RemoveScreenDeferred`/`HasAnyLayerOpen`；`RecomputeInputMode`（焦点=顶层模态否则顶层全屏）/`HandleEscape`（先消模态再消全屏层）/`ResetForNewWorld`（一并清全屏层栈）全覆盖。**暂停菜单** `UCSPauseMenuUserWidget`（生成器 `BuildPauseMenuWBP` → `WBP_PauseMenu`）：继续（关自己）/ 设置（叠 `WBP_Settings`，复用菜单那套）/ 返回主菜单（`PushConfirm` → `OpenLevel(Lvl_MainMenu)`）/ 退出（`PushConfirm` → `QuitGame`）；自处理 ESC、入场动效还原 HTML `bootin`（scrim+面板上滑淡入）；返回/退出的 Confirm 模态叠在暂停层之上（正好验证"模态叠全屏层"焦点回落）。**控制器**：`CloseOpenPanels`（ESC）末尾——先 `UI->HandleEscape()` 兜底消层，再按 flag 关 GM/Coop/Inv 面板，**都没有时 `OpenPauseMenu()`**（经管理器 `PushScreen`；`HasAnyLayerOpen` 防叠开；WBP 缺失静默不开不影响游戏）。**多人**：暂停菜单纯本地表现（不暂停服务端 sim、不碰权威状态；各客户端各自暂停/返回/退出）。**非目标（后续）**：把现有 Slate 面板（背包/GM/联机）的 ESC 栈/`Toggle*Panel` 也收口进 ScreenStack（需把 Slate 面板包装成层）、键位重绑等设置项、真正暂停 sim（单机可 SetPause，多人不行）。 |
| 96. UI-Modal-B 游戏关控制器接 UI 层管理器 + 服务器→客户端 Toast 通道 | 完成（运行时 + 编辑器双目标 2026-06-14 整编通过 = Result: Succeeded；含新增 `ClientShowToast` Client RPC 反射，编辑器 DLL 已重链；**用户 2026-06-14 PIE 通过 ✅**） | 承 UI-Modal-A，把弹窗/Toast 系统正式接进**游戏关**（之前只接了菜单 PC）。**纯增量、不删现有面板/ESC 逻辑**：① `ACSPlayerController::BeginPlay` 本地控制器分支 `RegisterOwner(this, [恢复底层输入模式])`，恢复函数=既有 `ApplyMenuInputMode`（弹窗关掉后：还有 Slate 面板开着就回面板态、否则回游戏态）——模态弹窗能正确叠在背包/GM/联机面板之上、关闭后焦点回落；这也让管理器在游戏关有有效 `OwningPC`（`ShowToast`/`PushModal` 前置）。② 加 `ClientShowToast(FText, ECSToastLevel)` **服务器→属主客户端** Client RPC（经 `GetUILayer()->ShowToast`，管理器/WBP 缺失则降级为屏幕调试文本，反馈不丢）。③ 接 3 个真实反馈点：丢关键物被服务端拒→Danger「关键物品不可丢弃」、`SaveWorldOnServer` 成功→Ok「世界已保存」、`LoadWorldOnServer` 成功→Ok「世界已读取」。**未改**：ESC（`CloseOpenPanels`）不动——弹窗自己在 `NativeOnKeyDown` 收 ESC，获焦时根本不会走到控制器；Slate 背包/GM/联机面板照旧。**非目标（后续）**：把现有 Slate 面板 ESC 栈/`Toggle*Panel` 收口进管理器（要把管理器从"模态栈"泛化成"UI 层栈"才能托管 Slate 面板）、游戏内 Confirm 弹窗的真实调用方（如手动存档覆盖确认）、QtyInput/TextInput 业务方。 |
| 95. UI-Modal-A 通用弹窗/Toast 系统 + UI 层管理器（接进主菜单首验） | 完成（运行时 + 编辑器双目标 2026-06-14 整编通过 = Result: Succeeded；WBP_Modal/WBP_ToastLayer/WBP_Toast 已生成；含还原 HTML 的入场动效；**用户 2026-06-14 PIE 通过 ✅**） | 把"系统做完都要配套 UI"落到弹窗地基：建**全工程唯一**的 UI 层管理器 `UCSUILayerSubsystem`（挂 `LocalPlayer`，跨 OpenLevel 持久），它是"谁该获焦 + 什么输入模式"的**唯一决策点**（模态栈非空→UIOnly+聚焦栈顶+显鼠标；栈空→调底层所有者注册的恢复函数）。一类三态弹窗 `UCSModalUserWidget`（`ECSModalMode` = Confirm/QtyInput/TextInput），自收 ESC(取消)/Enter(确认)；非模态自消失提示 `UCSToastUserWidget`（4 级配色）。关闭走"结果先广播 → 管理器**同步出栈+重算输入** → 仅延迟一帧销毁控件"（守 UE_Gotchas 1.1）。生成器加 `BuildModalWBP`/`BuildToastLayerWBP`/`BuildToastWBP`，落 `/Game/UI/Modal/`。**首个接线点 = 主菜单**：退出收容/新建档案(有存档时)→确认弹窗、合作下潜占位→Toast；降级判断为"弹窗是否真压栈成功"而非"子系统是否存在"（WBP 缺失则回退直接动作）。**动效还原 HTML**（NativeTick 驱动）：弹窗 = bootin（scrim+面板淡入 + 面板从 +27px 上滑归位，.2s ease-out）；Toast = toastin（从 -20px 滑入淡入→停留→滑到 -13px 淡出，整条包络）。**非目标（后续单元）**：游戏关 `ACSPlayerController` 接管理器（本单元只接菜单 PC）、QtyInput/TextInput 的业务调用方、Alert(三级)/Loading/Tooltip/ContextMenu 控件、背包/合成/图鉴等屏迁 UMG。 | 待验证（打开 `Lvl_MainMenu` PIE/Standalone：①点"退出收容"→屏幕居中弹出确认窗、背景变暗、弹窗淡入上滑；ESC 或"取消"关掉、"确定"退游戏；②有存档时点"新建档案"→覆盖确认窗，同样动效；无存档直接进图不弹；③点"合作下潜"→顶部居中 Toast 滑入提示后自动消失；④弹窗开着时鼠标可点、ESC 优先关弹窗不退菜单） |
| 94. UI-Menu-A 主菜单 + 设置 + 进出存档（UMG，独立菜单关卡） | 已实现（运行时 + 编辑器双目标 2026-06-13 整编通过 = Result: Succeeded；WBP_MainMenu/WBP_Settings/Lvl_MainMenu 已生成；**待 PIE**） | UI 表现层转 UMG 的第三屏（继对话框 U25、HUD U26）：按 `UI_Screens.html` 主界面设计稿做主菜单。**独立菜单关卡** `Lvl_MainMenu`（World Settings GameMode 覆盖 = `ACSMenuGameMode`，无 Pawn、用 `ACSMenuPlayerController` 加载 WBP_MainMenu、UIOnly + 显鼠标），`GameDefaultMap` 改指它（游戏图仍用 `ACSGameMode`，不动全局默认）。生成器 `BuildMainMenuWBP`/`BuildSettingsWBP`。数据基类 `UCSMainMenuUserWidget` 按钮接线（首版"只做一屏"后用户拍板扩范围）：**新建档案→不读档 OpenLevel(Lvl_L0_C12)**、**继续下潜→有存档才可点：读存档 MapName + GameInstance 置读档标志 + OpenLevel（进图后服务端 `BeginPlay` 延迟 0.5s `LoadWorldOnServer` 一次）**、**设置→开 `WBP_Settings`**、退出收容→QuitGame、合作下潜→StubNote 占位。**动效**（`NativeTick` 驱动，受设置「减少动态」控制）：进场淡入上滑 / 扫描线 / 标题 RGB glitch / 菜单悬停红条+文字转红。**设置屏** `UCSSettingsUserWidget`：画面（窗口模式/分辨率/质量/垂直同步，走 `UGameUserSettings` 即时生效+存盘）+ 主音量（`UCSGameUserSettings` + `SetTransientPrimaryVolume`，项目无 SoundClass 故只主音量）+ 减少动态开关 + 返回。 | 待验证（打开 `Lvl_MainMenu` PIE/Standalone：①整屏深井背景+终端菜单，进场淡入、扫描线流动、标题偶发 glitch、悬停菜单项红条亮+字转红；②"新建档案"进 `Lvl_L0_C12` 正常出生+HUD；③游戏里存档→回菜单→"继续下潜"可点、进图后状态被读档恢复；无存档时该按钮灰显；④"设置"开设置屏：改窗口模式/分辨率/画质即时生效、主音量滑条改音量、"减少动态"开后菜单动效停、返回回菜单；⑤"退出收容"退游戏） |
| 105. Economy-A 货币地基 | 已实现 + 对抗式评审修复（运行目标含修复 2026-06-16 整编通过 = Result: Succeeded；编辑器目标修复前已通过、3 处修复为纯逻辑改可 Live Coding 热载；HUD 已显示请购点、`WBP_HUD` 已 commandlet 重生成；**待 PIE**） | 每人独立请购点钱包。`ACSPlayerState` 加 `int32 RequisitionCredits`（`ReplicatedUsing=OnRep_RequisitionCredits` + `COND_OwnerOnly`；**性能**：只复制给属主、仅变化时复制、事件驱动不轮询/不 Tick）+ 服务器 `AddRequisitionCredits/SpendRequisitionCredits/CanAffordRequisition/SetRequisitionCredits`（统一 `ApplyRequisitionCreditsOnServer` 收口：Clamp≥0、仅变化才赋值+广播）+ `OnRequisitionCreditsChanged` 委托（服务端改与客户端 RepNotify 都广播给本机 UI）。存档 `CurrentSaveVersion` 8→9：`FCSPlayerSaveData` 加 `RequisitionCredits`、世界级 `S0RequisitionCredits` 标废弃（旧档恒 0 不回灌）、`MigrateToCurrentVersion` 加 v8→v9 标记（纯加字段无变换）。`CSPlayerController::Save/LoadWorldOnServer` 存读钱包；`FCSHUDSnapshot` 加 `RequisitionCredits` 并在 `BuildHUDSnapshot` 填值。GM 调试：`GiveCredits` exec（控制台 `GiveCredits 500`）+ `ServerGiveCredits` RPC + `GiveCreditsOnServer`（服务器权威 + ClientDebugLog 反馈）。**HUD 显示已接**：`UCSHUDUserWidget` 加 `CreditsText`（BindWidgetOptional，NativeTick 从快照填）+ `BuildHUDWBP` 在右上时钟下加金色"请购点 N RC"，`WBP_HUD` 已 commandlet 重生成（`[CSWidgetGen] OK`）。**经多代理对抗式评审**（5 维度 find→verify、13 agent，确认 7 条收敛为 3 个真问题）修了 3 处：① 读档恢复从 `if(Inventory)` 块内**移出**独立恢复（解耦 + 防 Inventory 初始化失败丢余额）；② `AddRequisitionCredits` 改用 **int64 中间值防溢出**（极大值不再回绕被 Clamp 清 0）；③ 迁移 v8→v9 **显式置 0**（AGENTS 显式迁移）。GM 面板按钮可选（控制台 `GiveCredits` 已够测）。设计见 `SD_49`。 | 待验证（Listen Server + 客户端：①控制台 `GiveCredits 500` 余额增加、ClientDebugLog 显示；②各玩家钱包独立不串、仅属主可见；③存盘读盘后余额保留；④旧 v8 档读档每人 0、不报错） |
| 106. Economy-B 回收出售 + 物品经济分类 | 已实现（**核心 = 数据层 + 出售事务**；运行目标 2026-06-16 整编通过 = Result: Succeeded；对抗式评审修复 1 类红线缺口；**B2（站点 + 回收页 UMG）已实现：运行 + 编辑器双目标 2026-06-16 整编通过 = Result: Succeeded；`WBP_Requisition` 已 commandlet 生成（`[CSWidgetGen] OK`）；共享 UI 函数改动均为加法/带护栏不回归他面板**；**待 PIE**） | **物品三轴分类**：新增 `ECSItemEconomyCategory`（收藏/核心补给/道具料/升级料/装备/受限）+ `ECSItemRarity`（公开→异常级 5 档密级）枚举；`FCSItemDefinition` 加 `EconomyCategory`/`Rarity`/`bKeepOnDeath`。**经济数值表**：新增 `FCSItemEconomyDefinition` + `ItemEconomy.csv`(ItemId/SellValue/bSellable)，`UCSItemDataSubsystem` 加 `LoadItemEconomyCsv` + `GetItemEconomy`/`GetItemSellValue`/`IsItemSellable`（红线统一口径）。**数据校验**：`GenerateGameData.py` 校验新列枚举 + `ItemEconomy.csv`（未知 ItemId/可卖须正价/受限禁卖），已重生成 `Data/Generated`。**服务器权威出售事务**：`ACSPlayerController` `SellItem`(调试 exec) / `RequestSellItem`(UI 入口) → `ServerSellItem` RPC → `SellItemOnServer`（红线判定→数量校验→**先扣物再发点**→`AddRequisitionCredits`→Toast；int64 防溢出；折旧待耐久系统后接，先满价线性）。**红线（SD_49 §13/§4，三层兜底）**：样本/关键物/残留 + `bCriticalItem` + **`EconomyCategory=Restricted`**（覆盖世界解锁物等非样本/关键/残留禁卖类）均禁卖，运行时 `IsItemSellable` + 生成器双拦，且生成器加"轴一禁卖类必须声明轴二 Restricted"的一致性校验。**经多代理对抗式评审**（5 维度 find→verify、10 agent，5 条确认全指向同一根因）修了 1 类：`EconomyCategory::Restricted` 此前只解析未执行——已在运行时门 + 生成器 + 一致性规则三处补齐。**B2 站点 + 回收页 UMG（C++ 已实现、运行整编通过）**：`ACSRequisitionStation`（ICSInteractable 摆放站点，照搬工作台站点；交互 → `ClientOpenRequisitionStation`）；控制器加请购面板开关全套（`bRequisitionPanelOpen` + `Open/Create/Remove/Close/RequestClose` + 接 `CloseOpenPanels`/`ApplyMenuInputMode`/`IsAnyMenuPanelOpen`/HUD 快照折叠，镜像背包/合成面板）；回收页控件 `UCSRequisitionUserWidget`（读单帧 HUD 快照 InventoryStacks 按 ItemId 聚合 → 仅 `IsItemSellable` 通过的进清单、单价×数量=总价 + 出售钮，钱包余额，哈希门控不每帧重刷，复用 `WBP_ListRow` + 新增 `ECSListRowMode::SellItem`/`SetupSellItem` → `RequestSellItem`）；生成器 `BuildRequisitionWBP`（还原 `UI_S0Hub.html` 回收屏：termhead 标题+钱包条+关闭 / 左可卖滚动列 / 右不可卖红线说明）+ `Scripts/CreateRequisitionWBP.py`。**编辑器目标整编通过 + `WBP_Requisition` 已生成**（B2 含新反射须关编辑器重整编，已完成）。设计见 `SD_49 §4`(三轴分类)/`§5`(逐层目录)/`§8`(回收)/`§13`(红线)。 | 待验证（Listen Server + 客户端：①控制台 `GiveCredits 500` 后 `SellItem Wood 5` → 背包 Wood −5、请购点 +10、Toast「回收 Wood ×5 +10 RC」；②`SellItem Sample_C12_Stable 1`/`SellItem Keycard_L1 1` → Toast「该物品无法回收」、物品不减、点数不变；③`SellItem Wood 999`（超量）→「数量不足」；④两人各自回收，钱包独立不串） |
| 107. Economy-C 请购购买（解锁门控） | 计划 | `Vendors/VendorOffers/VendorOfferRequirements.csv` + 买入事务（校验 Offer/门控/点数/材料/背包空间→扣→产出）+ 购买页 UMG；普通货随时买、强力/SCP/消耗门卡解锁后才上架。设计见 `SD_49 §9`(购买/门控矩阵)/`§10`(解锁)。 | — |
| 108. Economy-D S0 安全仓库 | 计划 | 每人持久仓库（取代废弃的 `S0SharedMaterialStock`）+ `ACSStashStation`/页 + 背包⇄仓库服务器校验移动。死亡掉落的公平前置。设计见 `SD_49 §13`(安全仓库)。 | — |
| 109. Economy-E 死亡掉落 + 保险 | 计划 | 死亡把可丢失携行物装进可拾回尸袋（关键样本/残留/关键物自动保护不丢）+ `InsurancePlans.csv` + 未被捡回的保险物延时退回仓库；接现有死亡/复活流程。设计见 `SD_49 §12`(死亡掉落+保险，含边界情况清单)。 | — |
| 110. Economy-F 升级强化 | 计划 | `ACSUpgradeStation` + 升级表（接 `SD_42 §6` 工程线），材料(+可选点数)升级武器/护甲/背包等，服务器校验。可再拆细。设计见 `SD_49 §11`(升级，接 SD_42 §6)。 | — |

| 111. Commission-A 委托地基（单类型打通） | 计划 | 委托=建在**现有任务求值器之上**的可重复带经济奖励合同（不重造）。新增 `Commissions.csv`+`FCSCommissionDefinition` 查表 + per-player 委托状态(存档)；复用 `UCSObjectiveDirectorSubsystem` 的 Kill/Collect 求值；接受/交付事务 + 发请购点(接 Economy-A)。先打通击杀/回收两类，GM/exec 验。设计见 `SD_50`。 | — |
| 112. Commission-B 全类型 + 重置/轮换 | 计划 | 补 调查/开启/修复/守住；`ResetType`(Daily/Session/Never)+`MaxCompletions`+委托板槽位轮换 N 槽 + 过期/放弃。设计见 `SD_50 §3/§7`。 | — |
| 113. Commission-C 委托板 + HUD UI | 计划 | `ACSCommissionBoard`(ICSInteractable)+`WBP_CommissionBoard`(接受/进度/交付/奖励预览)+HUD 活动委托追踪(补项目目前缺的任务 HUD)。设计见 `SD_50 §11`。 | — |
| 114. Commission-D NPC 发放 + 支线小案接入 | 计划 | `ACSNarrativeNPC` 委托钩子(NPC 发专属委托)+把 SD_48 几条支线小案做成一次性委托/Quest 示范。设计见 `SD_50 §12`。 | — |
| 115. Commission-E 下层随机事件 + 护送/解密 | 计划 | 新 `UCSDynamicEventDirector`(服务器确定性触发+节流)+`DynamicEvents.csv`；下层低概率刷事件/事件角色/敌群发经济奖励(不发关键物)；**护送**新支持(护送 Actor+到点存活)、**解密**复用 Interact+HoldTimer。设计见 `SD_50 §4.5`。 | — |

## Unit 57：Loop-A 最小归档循环闭环（计划 / 未开始）

定位：把已存在但还没"连成可玩闭环"的零件接起来，搭出最小归档循环——`S0 ⇄ 下层` 往返一次，带回一份残留碎片归档，产生一个**可见且持久的"下次不同"**。这是开场玩法的地板，后续 NPC、手枪、教学都长在它上面。设计来源：`SD_46`（AIC 口径）、`SD_22` §8 残留 / §14 污染代价、`SD_19` 反枪械、`SD_20` §7 物品字段。

设计决定（本次锁定）：
- 携带代价 = **污染**，污染住在 `FSurvivalStats`（方案 A）。理由：`SD_22` §14 把污染写成有阈值/临界、会致视觉模糊+移速下降+门禁误判率的持久身体量表，只能是真属性；临时 Timer（方案 B）撑不起后续。
- 获取物 = **残留碎片**（单碎片→单世界状态 flag；`SD_22` §8.3 的"3-5 碎片拼接成档案"留后续单元）。
- loop1 的"奖励/下次不同" = **解锁一条之前锁住的下层路线**（最便宜的可见变化；手枪解锁留 Loop-C）。

5 动词的最小实现：

| 动词 | 实现 | 复用 / 新建 |
| --- | --- | --- |
| ① 下潜/上浮 | 配一条 S0→下层 可达路线 + 回程 | 复用 `ACSStreamingElevatorStation`（Unit 14/30/32/33） |
| ② 获取 | 「残留碎片」放进下层容器，可拾取进背包 | 复用 `ACSLootContainer`/`ACSWorldItemActor` + Inventory；新建物品数据行 |
| ③ 携带代价 | 背包含 `ContaminationPerSecond>0` 物品时服务端按有界更新累加污染；到 `MaxContamination` 触发一个真实后果（先用扣血或移速），HUD 显示污染条 | 新增 `FSurvivalStats.Contamination/MaxContamination` + `SurvivalStatsComponent` 逻辑 + HUD（复用 Unit 9 数据流） |
| ④ 归档 | 交互归档台→服务端扣残留碎片→填 `FCSObjectiveEvent`→`GameState->ApplyObjectiveEvent`；归档即清除该样本携带污染来源（封装=中和，cost=cure） | **新建 `ACSArchiveStation`（`ICSInteractable`）**，占位 mesh（参考 Search-A 约定） |
| ⑤ 下次不同 | 归档写的 `UnlockRouteId`/`WorldStateTag` 让电梯解锁新下层路线；存档自动覆盖 | 复用 Unit 30 门禁绑定 + `UCSWorldSaveGame` |

非目标（明确砍，留后续单元）：
- NPC 任务源 / 救援（Loop-B）。
- 手枪作为奖励解锁（Loop-C）；loop1 用开门即可。
- Phase1 线性教学包装（Loop-D）。
- 污染完整后遗症（视觉模糊 / 门禁误判率 / 移速分级，`SD_22` §14 全套）——U1 只做"到阈值有一个真实后果"。
- 残留碎片"多碎片拼接成档案"（`SD_22` §8.3）；网格背包、耐久、多种门禁理由、归档台/残留正式美术。

预估改动：
- `Data/Source/Items.csv` 加「残留碎片」行；若新增 `ContaminationPerSecond`/`bArchivable`/`ArchiveValue` 列，需改 `GenerateGameData.py` + `UCSItemDataSubsystem::FCSItemDefinition`。
- `FSurvivalStats` 加复制字段 + `SurvivalStatsComponent` 累加/RepNotify + HUD 污染条。
- 新 `ACSArchiveStation`（`ICSInteractable`，服务器权威）+ 占位 mesh + EditAnywhere 配 `ArchiveSampleId`/`AddTag`/`UnlockRoute`。
- 电梯路线配置：加一条初始锁定、归档后解锁的下层路线。
- ⚠️ 本单元新增 `USTRUCT`/`UPROPERTY` 字段与新数据列，需关编辑器整编 `CoopSurvivalEditor`；改 CSV 后重跑 `GenerateGameData.py`。

验收（Listen Server + 1 客户端，主机与客户端各自验证）：
1. 下潜→下层拾取残留碎片→HUD 污染开始上涨→回 S0→归档台归档→碎片被扣、污染来源消除→电梯里出现一条新解锁的下层目标。
2. 第二次下潜确实能去到第一次去不了的那条路线（世界状态改变可见）。
3. 存档/读档后解锁与归档保持（WorldProgress 持久）。
4. 客户端与主机的污染读数、解锁状态一致。

排期：本单元为**计划态**。按"一次一个单元"，实际开工待当前 Enemy 系列待验证单元由用户 PIE 通过后开始（或用户明确指示先行）。

## Unit 81：Quest-A 任务定义层与世界状态对接（主线/支线/隐藏）（已实现，待验证）

**实现状态（2026-06-10）**：已按下方拆分落地，`CoopSurvivalEditor` 编辑器目标整编通过（Result: Succeeded，含新增 `ECSQuestType`/`FCSQuestDefinition`/`FCSObjectiveDefinition`/`FCSResolvedQuestView` 反射类型）。落地内容：
- **数据**：新增 `Data/Source/Quests.csv`（3 条种子：`MQ_L0_RestorePower` 主线 / `SQ_L0_SalvageSupplies` 支线 / `HQ_L0_UnloggedSignal` 隐藏，揭示标签 `Hidden.SignalDetected`）+ `Data/Source/Objectives.csv`（5 步，`ObjectiveId` 复用现有 join 键如 `L0_FindFuse`/`L0_RestoreElevatorFuse`/`L0_DescendToL1`/`L0_SearchLockers`/`L0_TraceSignal`）；`Scripts/GenerateGameData.py` 加 `QUEST_TYPES` + 两表校验（QuestType 枚举、ObjectiveId→QuestId 外键、唯一性）并入 `SOURCE_TABLES`，已重跑生成 `Data/Generated/*`。
- **类型**：`World/CSObjectiveTypes.h` 加 `ECSQuestType{Main/Side/Hidden}` + `FCSQuestDefinition` + `FCSObjectiveDefinition` + 只读视图 `FCSResolvedObjectiveView`/`FCSResolvedQuestView`。
- **加载**：`UCSItemDataSubsystem` 加 `LoadQuestsCsv`/`LoadObjectivesCsv`（步骤按所属 QuestId 分组、按 Order 升序）+ `QuestsById`/`ObjectivesByQuest` + 访问器 `GetQuestDefinition`/`GetAllQuestDefinitions`（SortOrder 升序）/`GetObjectivesForQuest`。
- **join**：`ACSGameState::BuildResolvedQuestView()`（读本机复制 `WorldProgress`：可见性 = 非 Hidden 或揭示标签命中；步骤 State 取 `ObjectiveStates`、缺省 Locked）+ `GetQuestViewDebugString()`。
- **验证出口**：`ACSPlayerController` 加 exec `PrintQuests`（仿 `PrintWorldProgress`，UE_LOG + 屏幕；各端从复制状态本地算）。
- **零存档迁移**：未碰 `FCSWorldProgressSaveData` / `CurrentSaveVersion`；零新增 RPC/Tick/复制字段。
- ⚠️ **runtime 目标 `CoopSurvival` 当前无法整编**：被未跟踪文件 `Source/CoopSurvival/UI/Editor/CSWidgetAuthoringLibrary.h`（Unit 78 Dialogue-UI/VG-A WIP）阻塞——它的 `UFUNCTION` 返回 `UWidgetBlueprint*`，runtime 模块不含 UMGEditor 依赖，UHT 解析不到该反射类而中止（报错在该文件，**早于本单元任何代码**）。与 Quest-A 无关；本单元代码在 editor 目标已编译+链接通过。需在 Unit 78 语境处理（迁到编辑器专用模块 / 调整 Build.cs 条件依赖），不在本单元扩范围。

---

定位：把"任务系统"从只有后端状态机（Objective-A/B 的 `ECSObjectiveState` + `ACSGameState.WorldProgress`）补上**缺失的定义数据层**——让每个目标有标题/描述/所属线/类型（主线·支线·隐藏）/所属层级，并把这些静态定义与运行时 `WorldProgress.ObjectiveStates` 的活状态 join 成"可读任务视图"。这是后续一切任务可视化（Quest-B 事故记录面板、Quest-D Site 状态图）和条件推进（Quest-C）的数据地基。本单元**只做定义数据 + 加载 + 只读 join + 控制台/GM dump 验证**，不做 UI、不做运行时条件求值器、不动存档结构。设计来源：`Docs/Development/ObjectiveWorldState_开发拆分与功能逻辑说明.md`（§0 任务=Site 事故处理记录、Objective-D「S0 状态图/正式 UI」未实现）、本轮对话锁定的"主/支/隐藏=同一结构 + Type/可见性配置"。

读法（沿用 ObjectiveWorldState §0）：
1. 任务不是接/交任务列表，是 Site 事故处理记录；关键进度属于世界档案、不属于个人玩家。
2. 主线/支线/隐藏不是三套系统，是同一个 Quest 结构的 `QuestType` + 可见性配置差异。
3. 定义数据是静态只读（与 Items/Recipes 同类），各机一致、不复制、不存档；活状态仍只在复制的 `WorldProgress`。
4. UI（后续单元）只读视图展示，永不写、永不完成任务。

设计决定（本次锁定）：
- 三类任务用一个枚举 `ECSQuestType { Main, Side, Hidden }` 区分，**不写三套逻辑**。SCP 叙事显示名（主事故线/旁支调查/未登记异常）走数据 `Title`，代码层只认枚举。
- **隐藏任务的可见性 = 计算值，不新增存档字段**：`可见 ⇔ QuestType≠Hidden 或 RevealWorldStateTags 任一已在 WorldProgress.WorldStateTags`。→ 本单元对存档**纯加性零迁移**（不碰 `FCSWorldProgressSaveData`、不升 `CurrentSaveVersion`）。
- `ObjectiveId` **复用现有 join 键**：`WorldProgress.ObjectiveStates` 已按 `ObjectiveId` 存活状态，本单元的 `Objectives.csv` 给同一批 ID 补定义（标题/描述/所属 Quest），不新造目标身份。
- **条件求值器留 Quest-C**：本单元目标完成仍靠现有 `FCSObjectiveEvent`/`ApplyObjectiveEvent` 路径写状态；不引入 `Conditions.csv` 和事件订阅。
- **图布局坐标先占列**：`Quests.csv` 含 `GraphColumn/GraphRow`，让 Quest-D 的 Site 状态图运行时只"读坐标画点连线"、不跑布局算法；本单元只解析存着，不渲染。

数据 schema（走 `Data/Source → GenerateGameData.py → Data/Generated → UCSItemDataSubsystem`，与 Items/Recipes 同管线）：

`Quests.csv`（任务线/容器，主键 QuestId）

| 列 | 含义 |
| --- | --- |
| QuestId | 唯一 ID（FName） |
| Title / Summary | 显示名 / 概述（SCP 叙事皮在此） |
| QuestType | Main / Side / Hidden |
| Layer | 所属层级（S0/L0/L1…，分组/泳道用） |
| PrereqWorldStateTags | 竖线分隔，前置门 |
| RevealWorldStateTags | 竖线分隔；Hidden 类的揭示触发（任一命中即可见） |
| BlocksEnding | bool，是否阻塞结局 |
| GraphColumn / GraphRow | Quest-D 节点图离线坐标（本单元只存不渲染） |
| SortOrder | 同层排序 |

`Objectives.csv`（线内步骤，主键 ObjectiveId，外键 QuestId）

| 列 | 含义 |
| --- | --- |
| ObjectiveId | 唯一 ID；**= WorldProgress.ObjectiveStates 的 join 键** |
| QuestId | 所属任务线 |
| Order | 线内顺序 |
| Title / Desc | 步骤显示名 / 描述 |
| bParallel | 是否可与同线兄弟并行推进 |

（`Conditions.csv` + 运行时求值留 Quest-C，不在本单元。）

C++ / 对接点：
- `World/CSObjectiveTypes.h`：新增 `UENUM ECSQuestType`、`USTRUCT FCSQuestDefinition`、`FCSObjectiveDefinition`（与既有 `ECSObjectiveState` 同文件）。
- `Items/CSItemDataSubsystem`（.h/.cpp）：新增 `LoadQuestDefinitionsCsv`/`LoadObjectiveDefinitionsCsv` + 查表 `QuestsById`/`ObjectivesById`/`ObjectiveIdsByQuest` + 访问器（与 `LoadEnemyDefinitionsCsv` 等同模式从 `Data/Generated/` 加载）。
- **只读 join 视图**：新增轻量 `FCSResolvedQuestView`（QuestId/Title/Type/bVisible + 各 `FCSResolvedObjectiveView{ObjectiveId,Title,State}`）；由纯函数据「定义表 × WorldProgress」现算——放 `ACSGameState::BuildResolvedQuestView(const FCSWorldProgressSaveData&)` 或新 `UCSQuestStatusLibrary`（BlueprintFunctionLibrary）。`State` 取 `WorldProgress.ObjectiveStates` 中该 ID、缺省 `Locked`；`bVisible` 按上面可见性规则算。后续 Quest-B 面板与 Quest-D 图都消费这个视图。
- **验证出口（无 UI）**：`ACSPlayerController` 加 exec `PrintQuests`（仿现有 `PrintWorldProgress`），打印解析后的任务视图：每条 Quest → Type → 可见? → 各 Objective + 当前 `State`。可选在 `SCSGMPanelWidget`（Unit 31 F10 面板）加一个 "Dump Quests" 按钮调同一入口。
- **种子数据**：两表填一小撮样例覆盖三类——L0 一条主线（含 2–3 步，复用现有 L0 ObjectiveId）+ 一条支线 + 一条隐藏（带 RevealWorldStateTags），让 `PrintQuests` 能体现"隐藏任务在揭示标签未置位时不可见、置位后出现"。

网络/权威：定义=静态只读、各机一致、不复制不存档；活状态权威仍在复制的 `WorldProgress`；可见性各端从复制状态本地算→一致。零新增 RPC、零 Tick、零存档迁移。

非目标（明确砍，留后续单元）：
- **Quest-B**：事故记录面板（按 Layer 泳道分组的只读卡片列表，挂 `OnWorldProgressChanged` + HUD 快照，状态色编码）；HUD 当前目标追踪条。
- **Quest-C**：`Conditions.csv` + 运行时条件求值器（订阅游戏事件→推进 Objective→`ApplyObjectiveEvent`）+ 隐藏任务 reveal 触发器（命中条件→写 RevealWorldStateTag）。
- **Quest-D**：Site 状态图节点图可视化（用 `GraphColumn/GraphRow` 画依赖 DAG）。
- 奖励/解锁不重做——继续走现有 `FCSObjectiveEvent`（路线/样本/AIM/配方解锁已实现）。

预估改动：
- 新 `Data/Source/Quests.csv`、`Data/Source/Objectives.csv` + 种子行；`Scripts/GenerateGameData.py` 加两表校验/生成（QuestId/ObjectiveId `require_name`、QuestType 枚举校验、QuestId 外键存在性、竖线列解析）；重跑生成 `Data/Generated/*`。
- `World/CSObjectiveTypes.h` 加枚举/结构；`Items/CSItemDataSubsystem.h/.cpp` 加加载+查表+访问器；`Core/CSGameState`（或新 `UCSQuestStatusLibrary`）加 `BuildResolvedQuestView`；`Core/CSPlayerController` 加 `PrintQuests` exec（可选 GM 面板按钮）。
- ⚠️ 新增 `USTRUCT`/`UENUM` → 需关编辑器整编 `CoopSurvivalEditor`；改 CSV → 重跑 `GenerateGameData.py`。

验收（无需网络也可初验；建议仍 Listen Server + 1 客户端确认各端一致）：
1. 控制台 `PrintQuests`（或 GM 面板 Dump）打印任务视图：每条 Quest 显示 Title/Type、每个 Objective 当前 `State`（默认 Locked），主线/支线正确分类。
2. 隐藏任务在其 `RevealWorldStateTags` 未置位时 `可见?=否`；用现有 `SetWorldStateTag` 写入该标签后再 `PrintQuests`，该隐藏任务变可见。
3. 用现有 `CompleteObjective`/`ApplyObjectiveEvent` 改某 Objective 状态后，`PrintQuests` 对应步骤 `State` 同步变化（证明定义层与活状态正确 join）。
4. 读档后视图与世界状态一致（定义静态、状态来自持久 `WorldProgress`）。
5. 客户端 `PrintQuests` 与主机一致（可见性/状态各端从复制状态本地算）。

排期：本单元已实现，是任务可视化链（Quest-A 数据 → Quest-B 面板 → Quest-C 条件/揭示 → Quest-D Site 状态图）的第一步。待用户 console/PIE 验证后进入 Quest-B（事故记录面板）。

## 2026-06-13 今日进度摘要：UI-Menu-A 主菜单屏（UMG，独立菜单关卡）

UI 表现层转 UMG 的**第三屏**（继对话框 U25、HUD U26），也是「正式开始做游戏内 UI 界面」的起点，从开始游戏的主菜单做起。按 `Docs/SystemDesign/UI/UI_Screens.html` 的「主界面」设计稿落地。

**用户拍板的三个方向**（开工前问清）：①菜单形态 = **独立菜单关卡**（不是叠加层）；②本单元范围 = **只做主菜单这一屏**（继续/合作/设置先占位，各留后续单元）；③「新建档案/继续下潜」进入 **`Lvl_Site_Persistent_Test`**（S0/L0 下潜循环入口）。

**落地（全部沿用 U25/U26 的「C++ 数据基类 + 生成器搭 WBP 树 + Python 编排」模式）**：
- **数据基类** `UI/CSMainMenuUserWidget`（`UUserWidget`，与 HUD/对话框不同——是**可点击交互**屏，`NativeOnInitialized` 绑 5 个按钮 `OnClicked`，不做 Tick 轮询）。按钮接线：**新建档案 → `UGameplayStatics::OpenLevel(NewGameLevelName)`**（`FName` 属性默认 `Lvl_Site_Persistent_Test`，设计器可改）；**退出收容 → `UKismetSystemLibrary::QuitGame`**；继续下潜/合作下潜/设置 → `ShowStub`（底部 `StubNote` 文本提示 + 定时器 2.6s 自动清空，纯本机反馈无副作用）。
- **菜单关卡** `Core/CSMenuGameMode`（无 Pawn：`DefaultPawnClass=nullptr`，`PlayerControllerClass=ACSMenuPlayerController`）+ `Core/CSMenuPlayerController`（`BeginPlay` 在本地控制器上 `LoadClass`/`CreateWidget` WBP_MainMenu、`AddToViewport`、`FInputModeUIOnly` + `bShowMouseCursor`）。关卡 `Content/Maps/Menu/Lvl_MainMenu`，**World Settings「GameMode Override」= CSMenuGameMode**（不改全局默认，游戏图仍用 `ACSGameMode`）。`GameDefaultMap` 从 `Lvl_FirstPerson` 改指 `Lvl_MainMenu`（启动进菜单；PIE 需手动打开该图再 Play）。
- **生成器** `CSWidgetAuthoringLibrary::BuildMainMenuWBP(pkg,name,bgTex,emblemTex)`（`#if WITH_EDITOR`，create-only-if-missing，1920×1080 基准 `S=1920/1148`，与 `BuildHUDWBP` 同套 `Px/MakeText/Place/Framed` 助手）。树：背景图（深井 `l7_well`）填满 + 暗化 `Border`（黑 0.45，保可读）→ 左上 `MTop`(徽标+品牌) / 右上封锁牌 / 中部左 `MBody`(kicker `KickText` + 大标题 `TitleText` 46px Bold + 英文副标 `EnSubText` + 菜单 `MenuBox` 5 `UButton`) / 底部左 build 信息 / 右下 CLASSIFIED 印章 / 底部居中 `StubNote`(默认隐藏)。菜单按钮 = 透明常态 + 悬停/按下红底（`FButtonStyle` 三态 `FSlateColorBrush`），项内 `HBox[左红条 + 文本]`。
- **编排脚本** `Scripts/CreateMainMenuWBP.py`（导背景图为 `T_MainMenu_BG` + 调生成器）、`Scripts/CreateMainMenuLevel.py`（建/绑 GameMode/存图）。

**踩坑（已记，详见 UE_Gotchas）**：
- **Interchange 贴图导入在无头 commandlet 下硬崩**（`AssetTools → InterchangeEngine → ContentBrowser → Slate`，post-import 同步内容浏览器/Slate，无头无 UI）——去/留 `-nullrhi` 都崩。背景图第一次跑碰运气导进去了；**结论：UI 皮贴图的导入要在编辑器开着时做，别在 commandlet 里导**。徽标 `SCP_Foundation_red.png` 因此在脚本里改为"已存在则加载、否则 None 退纯色方块兜底"，避开导入路径。
- **`LevelEditorSubsystem.new_level`/`get_editor_world` 在 `-run=pythonscript` commandlet 下不可靠**（`get_editor_world` 返空 → 我脚本里 `raise SystemExit` 静默退出、还报"executed successfully"、umap 建了但 GameMode 没设上）。**改用 `EditorLoadingAndSavingUtils.load_map/new_blank_map/save_map`** 才稳，且用 `log_warning` 确保日志可见、回读 `default_game_mode` 确认属性写入。

**验证状态**：runtime + 编辑器双目标 2026-06-13 整编 Result: Succeeded；`WBP_MainMenu`/`Lvl_MainMenu`/`T_MainMenu_BG`/`T_Emblem` 已落盘，关卡 GameMode 覆盖回读确认 = CSMenuGameMode。**待用户 PIE 验收**（打开 `Lvl_MainMenu` Play）。

**未做（留后续单元，符合一次一单元）**：继续下潜（存档/读档 UI）、合作下潜（大厅屏，可复用 `SCSCoopSessionPanelWidget` 逻辑）、设置屏；标题 RGB 撕裂 glitch / 扫描线 / 暗角等氛围 FX（留 UI-FX 单元）；菜单项悬停的"红左条滑入 + 文字转红"签名效果（v1 用按钮红底反馈代替）；徽标真图（编辑器开着时手动导 `T_Emblem` 再在设计器赋给 `EmblemImage`）。打包时 `GameDefaultMap`/菜单图需进 cook 列表（打包是后续事）。

**扩展（同日 · 用户反馈后续）**：用户看过首版后定"信息结构对，但缺动效，且关卡是 L0、设置和进出存档也一起做"——本单元就地扩展（用户主动扩范围）：
- **关卡 → L0**：`NewGameLevelName` 默认 `Lvl_Site_Persistent_Test` → **`Lvl_L0_C12`**（灰盒所在那张，见 [[project-l0-blockout]]；EditAnywhere 可改，若需电梯持久图外壳再调）。
- **动效**（全在 `UCSMainMenuUserWidget::NativeTick`，受设置「减少动态」控制，减少动态时落终态/关扫描线 glitch）：① 进场**淡入上滑**（IntroGroups = MTop/LockPanel_F/MBody/MFoot/StampPanel_F 按 index 错峰 0.45s 各组 `SetRenderOpacity`+`SetRenderTranslation`）；② **扫描线**（生成器加 `Scanline` 细红 Image，顶边横向拉伸，tick 把 RenderTranslation.Y 上→下 7s 循环）；③ **标题 RGB glitch**（生成器把标题包进 `TitleOverlay`，叠红 `TitleGlitchR`/青 `TitleGlitchC` 两副本默认透明，tick 每 4.6s 一个 0.22s 窗口做错位+闪烁）；④ **菜单悬停**（tick 轮询每个 `Btn*->IsHovered()` → `MenuAccent{i}` 红条亮/`MenuText{i}` 转红，不用逐按钮 hover 委托）。
- **新游戏/继续存档**：`UCSGameInstance` 加 `bPendingLoadOnStart` + `ConsumePendingLoadOnStart()`；**新建档案**=置 false（全新世界，关卡作者默认状态）；**继续下潜**=`DoesSaveGameExist` 真才可点（否则 `SetIsEnabled(false)` 灰显），读存档 `MapName` 决定进哪张图 + 置 true；`ACSPlayerController::BeginPlay` 服务端分支消费标志 → 延迟 0.5s `LoadWorldOnServer` 一次（让世界/玩家先就位）。存档槽名 `CoopSurvival_DevWorld` 与控制器一致（widget 内重复字面量，注释标注耦合）。
- **设置屏** `UI/CSSettingsUserWidget` + 生成器 `BuildSettingsWBP`：全屏暗底（挡菜单+吃点击）+ 居中面板，画面（窗口模式/分辨率/质量/垂直同步 循环按钮，走 `UGameUserSettings` 即时 Apply+Save）+ 音频（主音量 `USlider`）+ 减少动态 toggle + 返回。**音频**：项目无 SoundClass/SoundMix，新建 `Core/CSGameUserSettings : UGameUserSettings`（config `MasterVolume`/`bReduceMotion`，经 `DefaultEngine.ini` `GameUserSettingsClassName` 注册），主音量用 `FAudioDevice::SetTransientPrimaryVolume` 直接套总线（零资产可用 + 持久化），SFX/音乐分离留后续音频单元。主菜单「设置」按钮创建 `WBP_Settings` 叠加（ZOrder 5），返回自 `RemoveFromParent`。
- **新踩坑（已记 UE_Gotchas 3.10/3.11）**：① **块注释里写 `Btn*/Val*` 含 `*/` → 提前闭合注释**（runtime 目标因该段在 `#if WITH_EDITOR` 关闭被跳过、泄漏 token 不编译而"骗过"，编辑器目标块激活才报 C4138/C2143 一串怪错）；② **非 BindWidget 的普通 `UPROPERTY` 成员名与生成器构造的控件同名 → `OnVariableAdded` 撞 "another object already exists" Internal Compiler Error**（`Scanline`/`TitleGlitchR/C` 我先写成 `UPROPERTY(Transient)` 同名，改 `BindWidgetOptional` 即解；BindWidget 名 UMG 编译器有特殊处理不冲突）。
- **扩展验证**：runtime + 编辑器双目标 2026-06-13 再次整编 Result: Succeeded；`WBP_MainMenu`（131KB，含动效）/`WBP_Settings`（113KB）重新生成落盘。**待用户 PIE 验收**（含进出存档：先存再继续）。

**特效增强（同日 · 用户反馈"菜单不错，但基金会故障特效不明显，再多加点特效"）**：把首版收敛的标题 glitch（4.6s 才抖、±5px）做成 SCP 终端**数据损坏爆发**，并加多层氛围特效（全在 `UCSMainMenuUserWidget::TickGlitchFx`，受「减少动态」控制）：① **标题故障爆发状态机**——每 2.6–5.5s 一次随机爆发（35% 概率大爆发），红/青副本大幅错位（±11，大 ±22px）、主标题横向抖+25% 概率瞬闪灭、副本满不透明；② **横向数据错位条** `GlitchBar0..2`（生成器新增全宽细条，爆发时随机 Y / 红青交替 / 半透明）；③ **全屏故障泛色闪 + CRT 断电闪** `GlitchOverlay`（生成器新增全屏 Image，爆发泛色 + 平时每 6–11s 偶发短暂压暗）；④ **封锁状态报警呼吸**（右上 `LockText` 红字持续脉冲明灭）。新 FX 控件按名解析（成员名 ≠ 控件名，避开 3.11）。runtime + 编辑器双目标整编 Succeeded，`WBP_MainMenu` 重生成（141KB）。**待 PIE**。

**对外改名 + HTML 主菜单富化 + 全套搬进 UE（同日 · 用户三连反馈：标题闪烁=HTML 引用 / "我们叫 SCP基金会" / "HTML 主入口太死板加更多动效" / "SCP 图标加全息" / "加文件解密·文档持续录入感" / "都搬进去")**：
- **改名 → 「SCP基金会：深井协议」/「SCP FOUNDATION : ABYSS PROTOCOL」**（对外名，见 [[project-game-title]]）：改了 `UI_Screens.html`（标题/品牌/英文/页签/`<title>`）+ UE 生成器标题/品牌串。**其它 HTML（总纲入口页/风格指南/宣传站）暂未扫**，待用户确认是否统一。
- **`UI_Screens.html` 主菜单富化**（设计源先行）：背景缓漂(bgPan)、整屏 CRT 亮度微颤、标题在 RGB 撕裂上叠"霓虹失灵"整体闪 + glitch 频率 4.6s→3.4s、kicker 终端光标、封锁红点报警呼吸、菜单当前项呼吸辉光(走伪元素避 bootin 覆盖)+ 列表扫掠高光、漂浮尘埃、SCP 徽标**全息**(青冷重着色用 png 做遮罩 + 形状内扫描线 + 高光横扫 + 悬浮 + 失灵闪 + 冷光晕)、右侧 **SCP 文档解密信息流**(JS 持续逐字录入 DECRYPTING/ITEM#/OBJECT CLASS/涂改/CLEARANCE…打满上滚)。全部走 `prefers-reduced-motion` 关闭。
- **全套搬进 UE 菜单**（用户"都搬进去"）：生成器 `BuildMainMenuWBP` 加 徽标全息 Overlay(`EmblemHolo`/`EmblemGlow`/`EmblemScan`，`ClipToBounds`)、`DustDot0..5`、右侧 `FeedBox`+`FeedText`；`UCSMainMenuUserWidget` 新增 `TickBgDrift`(BgImage 缩放平移 Ken Burns)/`TickDust`/`TickEmblemHolo`(悬浮+失灵闪+框内扫描线)/`TickCaret`(kicker 尾 `_`)/`TickFeed`(逐字录入，**字符全 ASCII** 保 UE 默认字体不出豆腐块) + 标题霓虹闪。**近似项（标注待 UI 材质）**：红→青重着色 + 遮罩到 logo 形状的全息扫描线（UMG 无材质做不了，UE 用青光晕背衬 + 框内扫描线近似）；整屏亮度滤镜（UE 用 GlitchOverlay 压暗近似）；标题 sliced clip-path（UE 保留 burst 错位版）。新 FX 控件全按名解析（成员名 ≠ 控件名避 3.11）。runtime + 编辑器双目标整编 Succeeded，`WBP_MainMenu` 重生成（170KB）。

**PIE 反馈纠偏（同日）**：用户 PIE 后指出 ①"电子闪烁怎么是全屏的，HTML 是文字" ②"解密信息流方向/效果跟 HTML 不一样"。修：①`TickGlitchFx` **去掉全屏泛色闪 / CRT 全屏断电 / 全宽数据条**，故障爆发**只作用在标题文字**上（红/青副本错位 + 主标题抖/闪灭 + 霓虹失灵闪）——对齐 HTML 的"文字 glitch"；爆发更勤（2.0–4.2s）、错位量收到文字尺度（±7/12px）。②信息流文字**右对齐→左对齐**（运行时 `FeedW->SetJustification(Left)` + `FeedAnchor` HAlign_Left 纠正，生成器也同步改，免重生成）。**均为纯 .cpp 改（无新反射/不动 WBP），Live Coding `Ctrl+Alt+F11` 即可热补**，runtime 整编 Succeeded。待 PIE 复看。（GlitchBar/GlitchOverlay 控件保留在 WBP 但不再驱动；"效果差异"里 HTML 的上下淡出/多色行=UMG 需 RichText+材质，留后续。）

## 2026-06-14 今日进度摘要：Quest-OR + 前置门控 + 最小循环重排（L0→S0→L0→L1，二选一门）

**背景**：用户要把最小循环敲成 **L0 新手 → 地表 S0 → 回 L0 → 拿门禁卡 _或_ 凑材料 → L1**。排查出三处不一致 + 两个系统洞：①设计文档/任务数据/用户意图的循环顺序各不同；②"二选一"门任务系统表达不了（只有"目标全做完"的与门）；③任务 `PrereqWorldStateTags` 运行时无人读（死数据），跨任务门控只靠电梯/门物理挡。两个洞用户都选"正式补"。

- **系统代码（runtime 整编 Succeeded；editor 目标因编辑器开着 Live Coding 受阻，待关编辑器整编）**：
  - `FCSObjectiveDefinition` 加 `FName OrGroup`（`CSObjectiveTypes.h`）；`CSItemDataSubsystem::LoadObjectivesCsv` 读该列。
  - 求值器 `UCSObjectiveDirectorSubsystem`：新增 `QuestPrereqsMet`（所属任务前置标签须全置位）、`IsStepSatisfied`（OrGroup 空=自身完成；非空=同组任一完成）。`IsArmed` 三道闸：**前置门控**（跨任务解锁线运行时生效）+ **OR 组防重复**（组已被同组满足则本目标不武装）+ **Order 闸改按"步"满足**（OR 组算一步）。`CompleteWithRewards`：完成后把同组未完成兄弟发"仅置 Archived、无奖励"事件归档（供未来任务日志 UI）。
  - 数据列 `OrGroup` 加进 `Data/Source/Objectives.csv`（#CN/#DESC/表头 + 现有行留空）。`GenerateGameData.py` 纯透传无需改。
- **工具链**：`ApplyQuestGraph.py` 的 `OBJECTIVE_COLUMNS` + `graph_to_patch` 加 `OrGroup`；`quest-graph-editor.html` 四处（addNode/objRow/inspector/doImport）接 `OrGroup` + 节点标 `⋔OR:组名` + `validate()` 查"同组应≥2且同Order" + 帮助补"二选一门"段。`_tools/README.md` 补 OR 组用法。
- **内容（CSV 为源，直接改源表 → GenerateGameData 通过 → ApplyQuestGraph 整图 dry-run +0~20-0 无悬空）**：新循环 5 条主线 `MQ_L0_RestorePower`(找保险→修电梯→**上行S0**,写`Site.ReachedS0`) → `MQ_S0_Arrival`(前置`Site.ReachedS0`;闸口→简报,写`Site.S0_Briefed`) → **新 `MQ_L0_Prep`**(前置`Site.S0_Briefed`;**OR组`L1Access`**:`L0_GetKeycard`拿`Keycard_L1` / `L0_GatherMaterials`杀`EG_L0_Hostiles`×6,两条都写`Access.L1`) → `MQ_Containment_Filtration`(前置改`Access.L1`,层 L1) → `MQ_ReturnToS0`。删 `L0_DescendToL1`。新物品 `Keycard_L1`(KeyItem)。工具内嵌默认图 + `Data/Source/QuestGraph.json` 快照同步成新循环。
- **文档**：`关卡流程与世界状态.html` §02 worked example 改成新循环 + "前置门控现已运行时生效"说明。
- **遗留/待办**：①**editor 目标整编**（需用户关编辑器，存盘后跑 build editor 或 Ctrl+Alt+F11）。②**PIE 验收**（Listen Server+客户端）：前置门控、OR 门二选一、整条循环。③**关卡接线**（放置 actor，不在本次代码）：L0 电梯上行写 `Site.ReachedS0` / 下潜段读 `Access.L1`、`Vol_S0_Hub` 触发体、`Resupply_S0`/`FuseBox_L0` 交互物、`Keycard_L1` 拾取物、`EG_L0_Hostiles` 敌人编组。④**canon 待用户定**：`Level_FirstDescent_L0S0.html` 仍把"活体过滤"画在 L0（步骤04+L0地图收容室），与任务数据(L1)是先存分歧——是否把收容搬 L1 重写该文档待用户拍板。

## 2026-06-14 今日进度摘要：_tools 工具文件夹定规矩（任务数据「以 CSV 为唯一源」+ 文件夹 README + 工具挂导航）

**背景**：用户看 `Docs/_tools/` 觉得"不清晰"。核实后确认：任务图编辑器和管线本身扎实，缺的是**没把规矩写下来**——同一份任务数据现存四份副本（浏览器 localStorage / `Data/Source/QuestGraph.json` / 两张 CSV / HTML 内嵌默认图），"谁为准"从没写明，且工具内措辞把 `QuestGraph.json` 叫"可编辑源"，与架构拧着。

- **拍板：以 CSV 为唯一源**（用户确认）。依据：`Quests.csv`/`Objectives.csv` 是构建唯一读的、进 git 的、可手改的，且任务画布坐标已存在 `Quests.csv` 的 `GraphColumn`/`GraphRow`；目标位置/flow/gate 连线都能由「导入 CSV 基线」从两张 CSV 完整重建——`QuestGraph.json` 几乎无 CSV 没有的信息，**降级为可丢的布局快照**。localStorage=草稿、内嵌 default-graph=首开演示种子，都不是源。
- **新增 `Docs/_tools/README.md`**：写明文件夹收什么（会产出进 `Data/Source/` 的"编辑器"，区别于只读设计 HTML）、源头规矩、任务图编辑器完整往返（A 用工具改→回 CSV / B 手改 CSV→重导入 / C 直接手改 CSV）、四份副本各是什么、规划中的配音节点工具（VG-A）占位、加新工具的落地约定。
- **工具内措辞对齐**：`quest-graph-editor.html` 帮助/`_note`/内嵌默认图注释/存图 toast 全部从"JSON 是可编辑源"改成"CSV 是源、JSON 是快照、先导入 CSV 基线再编辑"。
- **工具挂门户导航**：`quest-graph-editor.html` `<head>` 加 `<script defer src="../_portal/portal-launcher.js" data-active="quest-graph">`——以前能从别页跳进来、进去后没返回栏，现补上。
- **说明**：本条**取代** 2026-06-13 QuestGraph-Tool 条目里"`QuestGraph.json`（可编辑源）"的说法（那是历史记录，保留不改）。纯文档/前端改动，无 C++，不需整编。

## 2026-06-14 今日进度摘要：UI-UMG-Coop 合作会话面板迁 UMG（剩余 Slate 屏迁 UMG 路线第 1 单元）

用户"继续 UI"。剩余最大块=把游戏里手写 Slate 屏迁 UMG。先用 workflow 把 5 个待迁 Slate 屏（背包面板/背包网格/纸娃娃/GM/联机）并行读透 + 汇总出迁移计划。

**迁移路线图（6 单元，按风险升序，workflow 产出 + 已认同）**：
1. **UI-UMG-Coop**（联机面板，小/低）← 本单元：最小自包含，干净重验整套流程，产出"模态表单+状态条+按钮行"模板。
2. **UI-UMG-GMPanel**（GM 面板，中/低）：纯表单（6 段/~9 输入框/~30 按钮），复用 Coop 模板；GM 天天用。
3. **UI-UMG-DragCore**（中/中，纯地基无屏）：`UCSInventoryDragDropOp : UDragDropOperation`（typed 字段，非 stringly Payload）+ UMG 幽灵装饰 + `UCSDragSlotReceiverWidget` 基类（NativeOnDrag* + IsDragValidForSlot → 0/1/2 悬停态绑边框色）。**背包前必做**，全新无冲突（repo 现无任何 UDragDropOperation）。
4. **UI-UMG-InvShell**（背包外壳 + 3 个简单子面板：合成列表/搜刮 Take 列表+Search-A 进度/属性栏，大/中）：**网格和纸娃娃先内嵌 Slate 当脚手架**（`UNativeWidgetHost`；混用已是生产现状=UMG Z10-12 / Slate Z20 同视口）。
5. **UI-UMG-Paperdoll**（纸娃娃 13 个 1D 槽，中/中）：DragCore 首个真实消费者，在简单 1D 上跑通拖拽地基。
6. **UI-UMG-Grid**（塔科夫 2D 网格全 UMG，XL/高）：唯一高风险，**放最后**，重写 `SConstraintCanvas`→UMG CanvasPanel cell-snap + 中途 R 旋转重建装饰 + 4 条落点路由 + 跨网格 op 状态，删掉旧 `FCSInventoryGridDragDropOp`。
**关键决策**：网格"先内嵌 Slate 脚手架→最后全 UMG 重写"（不永久内嵌，否则 op 永远要 Slate↔UMG 桥）。共性地基：DragCore 一次做好；FCSHUDSnapshot 事件驱动一处缓存下发；中途旋转/悬停色统一在 DragCore 解决；CJK 每屏验（默认 Roboto+DroidSansFallback 可渲染）；输入模式/ZOrder 沿用现有 `CreateInventoryPanel`/`ApplyMenuInputMode` 不改架构。

**本单元（UI-UMG-Coop）落地**：`UI/CSCoopSessionUserWidget`（`UUserWidget`，`NativeOnInitialized` 绑 5 按钮、`NativeTick` 轮询 `UCSPlatformServiceSubsystem` 刷状态条 + 驱动入场动效、`NativeOnKeyDown` 收 ESC/F3 关；按钮路由到既有控制器方法 `HostCoop`/`FindCoop`/`JoinCoop(0)`/`DestroyCoopSession`/`RequestCloseCoopSessionPanel`，`GetController()=Cast<ACSPlayerController>(GetOwningPlayer())`）。生成器 `BuildCoopSessionWBP`→`/Game/UI/Coop/WBP_CoopSession`（全屏 scrim + 居中 540 终端面板：标题"合作会话"+关闭✕ / 状态框（cold 文本）/ 主持·搜索·加入·销毁 4 等宽按钮；FCSUIStyle 着色 + 入场淡入上滑动效）。`Scripts/CreateCoopSessionWBP.py`。控制器 `CreateCoopSessionPanel`/`RemoveCoopSessionPanel`/`ApplyMenuInputMode` coop 分支 + 成员从 Slate `TSharedPtr CoopSessionWidget` 换 `UPROPERTY UUserWidget* CoopSessionUMGWidget`（防 GC）；`bCoopSessionPanelOpen`/`ToggleCoopSessionPanel`/`RequestCloseCoopSessionPanel` 延迟关全不变。状态条格式与旧 Slate 一致（parity，英文诊断行）；按钮中文化配终端质感。

**验证状态**：runtime + 编辑器双目标 2026-06-14 整编 Result: Succeeded；`WBP_CoopSession`（82KB）已生成落盘。旧 Slate `SCSCoopSessionPanelWidget` + 控制器 include 退役暂留回退。**待 PIE（网络相关→Listen Server + 客户端）**：F3 开合作面板→居中终端面板淡入上滑、状态条显后端/LAN/人数/结果/忙、主持/搜索/加入/销毁各自生效（与旧版行为一致）、ESC/F3/关闭都能关、ZOrder 在 HUD/对话之上。

## 2026-06-14 今日进度摘要：UI-Narrative-A AIC 旁白字幕迁 UMG 终端质感 + 动效

用户 PIE 后问"AIC 这种旁白对话的 UI 有没有设计 + 场景有点丑"。核实：①**AIC 旁白**是 `SCSNarrativeOverlay`——纯手写 Slate 黑框（78% 黑底 + 引擎默认字体/白盒贴图，无终端边框/无动效），是全项目**唯一一块没过"SCP 档案终端×战锤 grimdark"美化**的 UI（对话框/HUD/主菜单/弹窗都迁过了；NPC 对话另有好看的 UMG `WBP_Dialogue`）。②"场景丑"是另一回事=L0 灰盒 blockout（没过美术）+ 厚涂 NPR 只接了一半（`M_Painterly_PP` 母版做了、打光/材质/LUT 没铺），属独立美术轨。用户选先**美化 AIC 旁白 UI**。

**迁移（沿对话框/HUD 同套 UMG 模式）**：
- **数据基类** `UI/CSNarrativeOverlayUserWidget`（`UUserWidget`）：与 `UCSDialogueUserWidget` 同构——`NativeConstruct` 缓存导演、`NativeTick` 自驱轮询 `UCSNarrativeDirectorSubsystem`，仅"`HasActiveLine() && !IsActiveLineConversation()`"（=AIC 旁白，与 NPC 对话互斥）时 `HitTestInvisible` 显示，刷新 `GetActiveSpeakerText()`/`GetActiveLineText()`。数据来源/逻辑同旧 Slate，UI 只显示。
- **生成器** `BuildNarrativeOverlayWBP` → `/Game/UI/Narrative/WBP_NarrativeOverlay`：底部居中终端框 `RootPanel(SizeBox MaxW760)` ▸ `PanelOuter`(1px line2 描边) ▸ `PanelBg`(inset 0.9·`ClipToBounds`) ▸ `Overlay`[ `HBox`[ 左 cold 强调竖条 `AccentBar` + `VBox`[ 琥珀 Bold `SpeakerText` + ink-hi 自动换行 `LineText` ] ] , 低透明 `Scanline` Image(顶部全宽) ]。颜色走 `FCSUIStyle`。
- **Python** `Scripts/CreateNarrativeOverlayWBP.py`（先删后建、看 `[CSWidgetGen] OK`）。
- **控制器** `CreateHUD`/`RemoveHUD`：`SNew(SCSNarrativeOverlay)+AddViewportWidgetContent(11)` → `LoadClass(WBP_NarrativeOverlay)→CreateWidget→AddToViewport(11)`；新 `UPROPERTY NarrativeOverlayUMGWidget` 防 GC；保留 `NotifyLocalControllerReady`。旧 Slate `SCSNarrativeOverlay` 及其 include 暂留作退役回退。

**动效**（`NativeTick` 驱动，对齐弹窗/菜单同套手法）：① 整框入场淡入上滑（`RootPanel` opacity 0→1 + 从 +12px 归位，0.22s ease-out，显隐沿 `bWasShowing` 触发）；② 每换一行台词做解码揭示（`LineText` opacity 0→1 + 从 +6px 上滑，0.28s，靠 `LastLineKey` 比对台词变化重播）；③ 细扫描线在框内上→下漂移（`Scanline` `RenderTranslation.Y`，3.2s 循环，框 ClipToBounds 裁溢出）。

**验证状态**：runtime 2026-06-14 整编 Result: Succeeded。**编辑器目标 + WBP 生成待关编辑器**（新 `UCSNarrativeOverlayUserWidget` UCLASS 是反射变更，Live Coding 顶不了，须关编辑器全编——UE_Gotchas 2.1）。**待 PIE**（触发一段 AIC 旁白：底部终端框淡入上滑、说话人琥珀标签、台词换行时解码揭示、扫描线漂移；与 NPC 对话互斥不同时出）。

**非目标（后续）**：打字机逐字录入（现为整行解码淡入）、AIC 无线电杂讯音质（音频层，见叙事路线 Audio-FX-A）、3D 场景美术（独立轨：打光/厚涂材质/替灰盒）。

## 2026-06-14 今日进度摘要：UI-Modal-D 深化游戏内模态/Toast（丢弃数量 QtyInput + 合成结果 Toast）

UI-Modal-A/B/C 整批 PIE 通过后，用户选"深化游戏内模态/Toast"。这单元把已验证的系统接进真实玩法，重点是用上**此前没消费方的 QtyInput 模态**，并给核心循环（合成）补玩家可见反馈。先用 Explore 子代理摸清两个挂钩点（背包丢弃触发链、合成结果路径）再动。

**① 丢弃数量 QtyInput**：背包丢弃区 `SCSDiscardDropZone::OnDrop` 原来 `Controller->RequestDropItem(Op->ItemId, Op->Quantity)`（整摞），改调新入口 `ACSPlayerController::RequestDropItemInteractive(ItemId, MaxQty)`：
- 关键物：客户端读 `UCSItemDataSubsystem` 查 `bCriticalItem`，是则直接弹 Danger Toast「关键物品不可丢弃」返回（服务端 `DropItemOnServer` 仍权威拦截；客户端先拦只为即时反馈、避免"选完数量才被拒"的别扭，不双弹）。
- 单个 / 无 UI 层：直接 `RequestDropItem`（保持原行为）。
- 多个：弹 `ECSModalMode::QtyInput` 模态（Title「丢弃物品」/ QtyMin1 / QtyMax=摞数 / QtyDefault=全丢 / 确认钮「丢弃」红 / "全部"按钮取 max）→ 确认后 `RequestDropItem(ItemId, Result.IntValue)`。弱引用 lambda 捕获 `WeakThis`+`ItemId`。
- **关键交互**：丢弃时背包 Slate 面板正开着（Z20），模态在 Z300 叠上去——这是 UI-Modal-B 铺好的"模态叠 Slate 面板"的**首个真实消费**。模态开时管理器接管焦点；关掉后 restorer=`ApplyMenuInputMode` 把焦点还回背包面板（背包没关时）。模态纯本地，丢弃仍走 `RequestDropItem→ServerDropItem→DropItemOnServer` 服务端权威。

**② 合成结果 Toast**（`UCSCraftingComponent` 在控制器上，`GetOwningController()->ClientShowToast`）：
- 合成完成 `CompleteCraftOnServer` 成功 → Ok「合成完成：{物品} ×{N}」（计时结束触发，玩家可能没盯着合成面板，Toast 才有意义）。
- 产物放不下（背包满/超重）→ Danger「合成失败：背包放不下产物」。
- 材料不足 `StartCraftOnServer` → Warn「材料不足：{物品} 需 {X}，现有 {Y}」。
- 物品显示名：文件内 `ResolveCraftItemName(World, ItemId)` 服务端查 `UCSItemDataSubsystem.DisplayName`（缺失回退 `FText::FromName(ItemId)`），解析后随 `ClientShowToast` 的 FText 发给属主客户端（服务端有物品数据，含专用服务器）。原 `ClientDebugLog` 保留作开发期细节。

**验证状态**：runtime 2026-06-14 整编 Result: Succeeded。**无反射变更**（`RequestDropItemInteractive` 是普通方法、其余是函数体/Slate 一行）→ 编辑器可 `Ctrl+Alt+F11` Live Coding 热编、无需关编辑器全编。经聚焦对抗审查（模态叠 Slate 面板的焦点/输入回落、从 Slate `OnDrop` 内开模态的生命周期、关键物客户端拦截 vs 服务端拒绝、合成 server→client 路径）= 无真实缺陷（审查提的"背包在丢弃模态开着时被关→焦点悬空"经核验不可达：模态 UIOnly+全屏 scrim 吃输入，B/J 屏蔽、ESC 被模态吃，关不掉背包；即便发生 `ApplyMenuInputMode` 也落 `FInputModeGameOnly` 无卡死）。**待 PIE**（背包丢一摞多个→弹"丢弃多少"、选数/全部/取消都对、关键物直接拒；合成完成/材料不足/背包满各弹对应 Toast；主机+客户端各自不串台）。

**非目标（后续单元）**：拾取/目标完成/路线·配方·样本解锁等更多事件 Toast（目标完成与 AIC 叙事字幕重叠，需甄别哪些该弹）、卖出/转移数量也走 QtyInput、危险 GM 操作的 Confirm。

## 2026-06-14 今日进度摘要：UI-Modal-C 游戏内暂停/系统菜单（ESC）经 UI 层管理器接入

承 UI-Modal-B（游戏关只用了 Toast）。这一单元给模态系统在游戏里第一个**非 Toast** 的真正用途，同时补上一个明显缺口：现在游戏里按 ESC（没开面板时）什么都不做，而 `UI_Hierarchy.md` 早就定了「HUD 上 ESC = 开暂停菜单」。选它是因为：价值高（缺失的基础功能）、在模态线程上（接管理器）、能复用已建好的设置屏/菜单关/退出、且自包含（不碰 Slate 背包那套复杂代码）。

**管理器泛化（`UCSUILayerSubsystem`，纯增量，模态逻辑一行不动）**：之前只有 `ModalStack`（弹窗）。这单元加一个 `ScreenStack`——**全屏 UMG 层**（暂停菜单这种"占满屏、吃输入、但不是 Confirm/Qty/Text 弹窗"的层），Z200（HUD 10 / 对话 12 / Slate 面板 20-40 之上，模态 300 之下）。
- `PushScreen(class)`：管理器建控件 + 入视口 + 入栈 + 重算输入模式，返回控件（PC 失效/类无效/跨世界返 null）。
- `RemoveScreenDeferred(screen)`：与 `RemoveModalDeferred` 同构——**同步出栈 + 同步重算输入模式**（无一帧焦点缺口），**只延迟一帧销毁控件**（守 UE_Gotchas 1.1）。
- `RecomputeInputMode`：焦点目标=栈顶模态优先（Z 最高），否则栈顶全屏层；两者皆空才调底层恢复函数。
- `HandleEscape`：先消模态、再消全屏层。`ResetForNewWorld`：一并清全屏层栈（跨关卡切换时清掉残留暂停菜单）。
- `HasAnyLayerOpen()`：模态或全屏层任一在场。

**暂停菜单 `UI/CSPauseMenuUserWidget`**（生成器 `BuildPauseMenuWBP` → `/Game/UI/Modal/WBP_PauseMenu`，68KB）：四项=继续 / 设置 / 返回主菜单 / 退出游戏。
- 由管理器作为全屏层托管；**自处理 ESC**（`NativeOnKeyDown`：设置开着→先关设置，否则=继续）。
- **设置**：叠 `WBP_Settings`（`AddToViewport(260)`，复用主菜单那套；它自己「返回」`RemoveFromParent`）。坑：设置由自身按钮关掉时暂停菜单不知道→`NativeTick` 轮询 `IsInViewport` 清 `bSettingsOpen`，让 ESC 恢复为"继续"。
- **返回主菜单 / 退出**：用 `PushConfirm` 弹 Confirm 模态（叠在暂停层之上 Z300>200，正好验证"模态叠全屏层"焦点回落）→ 确认后 `OpenLevel(MainMenuLevelName)` / `QuitGame`，弱引用 lambda 捕获。
- **入场动效还原 HTML `bootin`**（`NativeTick`：scrim 淡入 + 面板从 +27px 上滑归位，ease-out），与弹窗一致。

**控制器接钩子（只动 `CSPlayerController.{h,cpp}`）**：`CloseOpenPanels`（ESC 绑定）改成：① 先 `UI->HandleEscape()` 兜底消层（正常时层获焦自己吃 ESC 到不了这）；② 按 flag 关 GM>Coop>Inv 面板（背包分支保留 `InventoryWidget.IsValid()` 维持原"无条件清残留背包"语义）；③ **都没有时 `OpenPauseMenu()`**——经管理器 `PushScreen(WBP_PauseMenu)`，`HasAnyLayerOpen` 防叠开，WBP 缺失静默不开（不影响游戏）。`PauseMenuClass` 懒加载缓存（`UPROPERTY` 防 GC）。

**两个输入模式权威如何共存（关键）**：暂停菜单是管理器的"全屏层"，开时管理器 UIOnly+聚焦+显鼠标；关时管理器调恢复函数=`ApplyMenuInputMode`（无面板→`FInputModeGameOnly`+藏鼠标）。`ApplyMenuInputMode` 不回调管理器→**无递归**。暂停层开着时 UIOnly 屏蔽游戏输入，B/F3/F10 不会触发面板→`ApplyMenuInputMode` 不会在暂停层下被乱调→不打架。

**多人**：暂停菜单**纯本地表现**——不暂停服务端 sim（co-op 没法暂停共享世界）、不碰任何权威状态；各客户端各自开自己的暂停菜单、各自返回/退出。规避了"co-op 暂停"的根本难题。

**验证状态**：runtime + 编辑器双目标 2026-06-14 整编 Result: Succeeded；`WBP_PauseMenu` 已生成落盘（日志 `[CSWidgetGen] OK`）。经独立对抗式审查（3 视角：输入/ESC/再入、生命周期/GC/跨关卡、暂停控件+多人+设置叠加），采纳 2 项修复：① 暂停菜单 `NativeDestruct` 统一清叠加的 `SettingsWidget`（覆盖继续/HandleEscape/跨关卡 ResetForNewWorld 所有移除路径，避免设置屏残留视口——尤其多人下正开着设置时 server travel）；② `RemoveScreenDeferred` 加幂等守卫（重复调用直接返回）。审查另两条判为非真问题：设置叠加不调 RecomputeInputMode（有意保焦点在暂停菜单让 ESC 关设置，与主菜单同模式）、设置屏 OnBackClicked 同帧 RemoveFromParent（UI-Menu-A 既有代码，UButton::OnClicked 是高层事件自移除安全）。**待 PIE**：进 `Lvl_L0_C12`——①没开面板时按 ESC → 居中弹暂停菜单（背景变暗、上滑淡入、鼠标可点）；②继续 / ESC → 关闭回游戏（鼠标藏、能动）；③设置 → 叠设置屏、返回/ESC 关回暂停；④返回主菜单 → Confirm 模态（叠在暂停之上）→ 确定回 `Lvl_MainMenu`；⑤退出 → Confirm → 退游戏；⑥开着背包时按 ESC 先关背包不弹暂停。

**非目标（后续单元）**：把现有 Slate 面板（背包/GM/联机）的 ESC 栈/`Toggle*Panel` 也收口进 `ScreenStack`（需把 Slate 面板包装成"层"）、键位重绑等设置项、单机真正暂停 sim（`SetPause`，多人不可）。

## 2026-06-14 今日进度摘要：UI-Modal-B 游戏关控制器接 UI 层管理器 + 服务器→客户端 Toast 通道

承 UI-Modal-A（弹窗/Toast 地基只接了菜单 PC），这一单元把它正式接进**游戏关**（`ACSPlayerController`，2677 行、所有面板输入与 ESC 的中枢）。原则=**纯增量、不拆现有逻辑**，先把结构接通 + 给一组真实可见的反馈，复杂的"把 Slate 面板栈收口进管理器"留后续。

**先摸清现状再动**（用 Explore 子代理把控制器的 UI/输入/ESC 机制读了一遍）：面板大多是 Slate（背包 `SCSInventoryPanelWidget` ZOrder 20、联机 `SCSCoopSessionPanelWidget` 30、GM `SCSGMPanelWidget` 40），输入模式由 `ApplyMenuInputMode()` 统管（有面板→`FInputModeUIOnly`+光标+焦点优先级 GM>Coop>Inv，无面板→`FInputModeGameOnly`+藏光标），ESC 绑 `CloseOpenPanels()` 按 GM>Coop>Inv 关。**关键发现**：弹窗自己在 `NativeOnKeyDown` 收 ESC，所以 modal 获焦时 ESC 根本走不到控制器——**不必改 `CloseOpenPanels`，零风险**。

**落地（只动 `CSPlayerController.{h,cpp}`）**：
- **注册**：`BeginPlay` 的 `IsLocalController()` 分支里 `GetUILayer()->RegisterOwner(this, [WeakThis]{ ApplyMenuInputMode(); })`。恢复函数选 `ApplyMenuInputMode` 而非裸 `FInputModeGameOnly`——这样模态弹窗叠在 Slate 面板之上、关掉后若面板还开着就回面板态、否则回游戏态。注册也让管理器在游戏关有有效 `OwningPC`（`ShowToast`/`PushModal` 的前置；管理器 `RegisterOwner` 内部会先 `ResetForNewWorld` 清掉上一关如主菜单的残留控件，正好接上跨关卡切换）。
- **Toast 通道**：`GetUILayer()`=`GetLocalPlayer()->GetSubsystem<UCSUILayerSubsystem>()`（远端/专用服务器取不到返 null）；新增 `ClientShowToast(FText, ECSToastLevel)` **服务器→属主客户端** Client RPC（`ECSToastLevel` 是 `UENUM:uint8` 可直接进 RPC 签名），实现里 `IsLocalController` 守卫 → `ShowToast`，**管理器/WBP 缺失（ShowToast 返 false）则降级 `ShowDebugLog`**，反馈不丢。这是一条可复用的"服务端任何事件→在属主客户端弹提示"通道。
- **3 个真实反馈点**：① `DropItemOnServer` 关键物拒绝（原来 `ClientDebugLog`）→ `ClientShowToast("关键物品不可丢弃", Danger)`；② `SaveWorldOnServer` 成功 → `ClientShowToast("世界已保存", Ok)`；③ `LoadWorldOnServer` 成功 → `ClientShowToast("世界已读取", Ok)`（"继续下潜"进图后 0.5s 自动读档也会弹）。

**两个输入模式权威如何共存**：控制器的 `ApplyMenuInputMode`（管 Slate 面板）与管理器的 `RecomputeInputMode`（管模态）通过"管理器的恢复函数=ApplyMenuInputMode"串起来——模态压栈时管理器接管焦点，弹空时调恢复函数把输入还给底层面板/游戏。本单元在游戏里只用了 Toast（非模态、不动输入模式），模态路径是为后续备好；且 modal 自处理 ESC，UIOnly 下面板热键不漏给游戏，二者不打架。

**验证状态**：runtime 2026-06-14 整编 Result: Succeeded。**编辑器目标待关编辑器整编**（`ClientShowToast` 是新反射 UFUNCTION，Live Coding `Ctrl+Alt+F11` 加不了新反射类型，必须关编辑器全编——UE_Gotchas 2.1）。**待 PIE（网络相关，必须 Listen Server + 至少一个客户端）**：①主机/客户端各自丢一个关键物（GM 给 `Item_Fuse`）→ 顶部弹「关键物品不可丢弃」红色 Toast、物品仍在；②GM/控制台触发存档→「世界已保存」、读档→「世界已读取」，主机与客户端各自只在本机弹；③开着背包面板时若未来弹模态→焦点正确叠放（本单元先验 Toast）。

**非目标（后续单元）**：把现有 Slate 面板的 ESC 栈/`Toggle*Panel` 收口进管理器（需把管理器从"模态栈"泛化成"通用 UI 层栈"才能托管 Slate 面板）、游戏内 Confirm 弹窗真实调用方（如手动存档前"覆盖确认"）、QtyInput/TextInput 业务方、背包/合成等屏迁 UMG。

## 2026-06-14 今日进度摘要：UI-Modal-A 通用弹窗 / Toast 系统 + UI 层管理器（把原型 UI 正式接进游戏的第一块地基）

承「把这些界面 UI 正式接入游戏中，保持系统健壮性」。原型（`UI_Screens.html`）里每屏都会用到"确认窗 / 输入窗 / 顶部提示"这类通用件，与其每屏各写一套，先把**弹窗地基**做扎实，再拿主菜单当第一个真实接线点验证。一次只做这一块（弹窗 + 提示 + 一个谁来管输入焦点的管理器），不顺手改背包/合成等其它屏。

**为什么要一个"UI 层管理器"**：弹窗一开，鼠标要能点、ESC 要先关弹窗而不是退菜单、底下的界面要被挡住、关掉后输入又要还原——这套"谁获得焦点、用什么输入模式"的判断，散在各个界面里迟早打架。所以建**全工程唯一**的决策点 `UCSUILayerSubsystem`，挂在 `LocalPlayer` 上（关键：`LocalPlayer` 跨关卡切换不销毁，菜单关和游戏关拿到的是同一个，输入模式本就该"每个本地玩家一份"）。

**落地（4 个新文件 + 生成器 + 菜单接线，沿用 C++ 数据基类 + 生成器搭 WBP 树 + Python 编排）**：
- `UI/CSModalTypes.h`：弹窗用的小类型——`ECSModalMode`（Confirm 确认 / QtyInput 选数量 / TextInput 输文本）、`ECSToastLevel`（Info/Ok/Warn/Danger 四级配色）、结果体 `FCSModalResult`、配置体 `FCSModalConfig`、关闭回调 `FCSOnModalClosed`（**非动态委托**——要能绑 lambda，动态委托绑不了）。
- `UI/CSUILayerSubsystem`（`ULocalPlayerSubsystem`）：管模态栈（`UPROPERTY` 强引用防被 GC）+ 常驻 Toast 容器 + **唯一的输入模式决策**（栈非空→`UIOnly`+聚焦栈顶弹窗+显鼠标；栈空→调底层所有者注册的恢复函数）。对外：`PushConfirm`/`PushModal`/`ShowToast`/`HandleEscape`。控制器 `BeginPlay` 调 `RegisterOwner(this, [恢复底层输入模式的 lambda])`；新关卡新 PC 重注册时先清掉旧关残留控件。
- `UI/CSModalUserWidget`（一类三态）：`Setup` 填配置 → 自己收 ESC（取消）/ Enter（确认）。关闭顺序是**"结果先广播 → 管理器同步出栈+立刻重算输入模式 → 只把控件销毁延迟一帧"**（守 UE_Gotchas 1.1：绝不在自己输入回调里同帧销毁自己；但出栈/换输入模式同步做，避免有一帧焦点悬空）。
- `UI/CSToastUserWidget`（非模态、不吃输入、自动消失）。
- 生成器 `CSWidgetAuthoringLibrary` 加 `BuildModalWBP`/`BuildToastLayerWBP`/`BuildToastWBP`，`Scripts/CreateModalWBP.py` 编排，产物落 `/Game/UI/Modal/`（WBP_Modal 82.7KB / WBP_Toast 29.4KB / WBP_ToastLayer 23.1KB）。

**第一个真实接线点 = 主菜单**：退出收容 → 危险确认窗（确定才退）；新建档案在**有存档时** → 覆盖确认窗（无存档直接进图不弹）；合作下潜占位 → 顶部 Toast。**健壮性关键**（对抗式审查抓的 HIGH）：降级判断必须是"**弹窗是否真的压栈成功**"（`PushConfirm(...) != nullptr` / `ShowToast(...) == true`），不是"子系统是否存在"——因为 `LocalPlayerSubsystem` 永远非空，若按"存在就 return"，一旦 WBP_Modal 没生成，按钮会**静默失效**（既不弹窗也不执行）。现改成压栈失败就落到直接动作（直接退 / 直接新建 / 底部 StubNote 兜底）。

**动效还原 HTML（用户强调"动效一定要还原"）**：都用 `NativeTick` 驱动（与主菜单一致；UE5.8 下 C++ override `NativeTick` 即被调用，无需额外开 Tick）。
- 弹窗 = 还原 HTML `bootin .2s`：scrim（背景暗罩）+ 面板一起淡入，面板从 **+27px 处上滑归位**，ease-out。面板靠新绑的 `ModalPanelBox`（生成器早已把它注册成变量，故 `BindWidgetOptional` 按名直接绑，**不用重生成 WBP**）做 `SetRenderOpacity`+`SetRenderTranslation`。
- Toast = 还原 HTML `toastin`：从 **-20px 滑入 + 淡入**（约 0.26s）→ 停留 → **滑到 -13px + 淡出**（约 0.45s）整条包络跑完自移除（不再用单独定时器）。

**对抗式审查（workflow）后修的真 bug**：① 上面的菜单降级 HIGH；② `ShowToast` 改返回 `bool`（无 World / WBP 缺失返回 false 让调用方兜底）；③ `PushModal`/`ShowToast` 加 PC 有效性 + 同世界校验（`PC->GetWorld()==LP->GetWorld()`），防拆关途中创建；④ `RemoveModalDeferred` 重构为"同步出栈 + 同步重算输入模式，只延迟销毁控件，World 空则跳过销毁"；⑤ 弹窗关闭结果**按模式过滤**（Confirm/Qty 不带 InputBox 脏文本，反之亦然）；⑥ TextInput 程序化 `SetText` 不触发 `OnTextChanged`，`Setup` 主动同步一次确定钮可用态（空串禁用防空确认）。

**非目标（留后续单元）**：游戏关 `ACSPlayerController` 接这个管理器（本单元只接了菜单 PC，2677 行的游戏 PC 不在本单元动）；QtyInput/TextInput 的真实业务调用方（如丢弃数量、命名存档）；Alert（三级警告）/Loading/Tooltip/ContextMenu 这些其它通用件；背包/合成/图鉴等屏迁 UMG。

**验证状态**：runtime + 编辑器双目标 2026-06-14 整编 Result: Succeeded；三个 WBP 已生成落盘（日志三条 `[CSWidgetGen] OK`）。**待用户 PIE 验收**：打开 `Lvl_MainMenu` Play——①退出收容→居中确认窗、背景变暗、淡入上滑，ESC/取消关掉、确定退；②有存档时新建档案→覆盖确认窗同效，无存档直接进图；③合作下潜→顶部 Toast 滑入提示后自动消失；④弹窗开着鼠标可点、ESC 先关弹窗不退菜单。

## 2026-06-13 今日进度摘要：QuestGraph-Tool 任务流程可视化节点编辑器（外部 HTML，增量补丁 → CSV upsert）

**背景**：用户不想把任务/剧情写死，要一个"蓝图式但在 HTML"的可视化工具——节点=任务/目标(可配 ID物品/击杀数/到达点等条件)，可连线，一键导出，且**增量而非全量**。本质是把现有任务数据（`Quests.csv`/`Objectives.csv`，求值器 Quest-C 消费）的编写从手改 CSV 升级成可视化配置。同款思路对齐既有外部 HTML 配置工具路线。

- **`Docs/_tools/quest-graph-editor.html`**（纯前端单文件，无外部库，SCP 终端风格，SVG 连线）：
  - 节点两类：**任务节点**(Quest：Main/Side/Hidden + Layer + 前置/揭示标签 + BlocksEnding + SortOrder) / **目标节点**(Objective：八种 ConditionType=Manual/Interact/CollectItem/KillCount/ReachVolume/RepairCount/HoldTimer/WorldStateTag + ConditionKey + 计数 + 四类奖励)。
  - **链式流程模型（2026-06-13 用户拍板重构，取代早期星型）**：一条任务=任务节点出发的一条连续链。**金线·flow**(连到目标)串 任务→目标→目标→…，**Order/QuestId 由连线拓扑自动推导**(inspector 只读)，分叉=并行(同 Order,bParallel)；**蓝线·gate**(连到下个任务)携带世界标签、自动并进上游目标奖励+下游任务前置。连线语义看终点类型。端口=左进右出。世界设备(电梯)写的标签直接填任务「前置标签」字段或从相关目标连 gate 桥接(如 `L0_DescendToL1`→`MQ_S0_Arrival` 带 `Site.FirstDescent`)。`ApplyQuestGraph.py` 的 `graph_to_patch` 同样做链式派生(与编辑器 deriveChains 对齐)，旧 `contains` 边载入时迁移成 `flow`。
  - 画布平移/缩放、右侧 inspector 编辑全字段、回车改 ID(连线自动跟随)、校验(重复ID/未归属/缺ConditionKey/悬空QuestId)、localStorage 自动存 + 存图/载图 JSON。
  - **增量导出**：图存 `Data/Source/QuestGraph.json`（可编辑源，含节点位置/连线/baseline 快照）；**导出增量补丁**只含相对上次导出/导入CSV基线有新增·改动·删除的行（按 ID diff），导出后把当前设为新基线；另有「导出全量」。可「导入 CSV 基线」粘贴现有两张 CSV 反向建图。
- **`Scripts/ApplyQuestGraph.py`**：把补丁 JSON 按 `QuestId`/`ObjectiveId` **upsert** 进 `Quests.csv`/`Objectives.csv`（存在则就地更新、不存在则追加，补丁没提的手写行原样保留，支持 deleteQuests/deleteObjectives），保留 `#CN`/`#DESC` 注释行 + 列序，写回 `utf-8-sig`，**幂等**。也接受整张 `QuestGraph.json`（折叠成全量 upsert）。校验 Objective→Quest 引用。
- **管线**：`QuestGraph.json`（编辑器）→ 导出 `QuestPatch.json` → `python Scripts/ApplyQuestGraph.py QuestPatch.json` → `python Scripts/GenerateGameData.py` → `Data/Generated/*` → 运行时求值器。
- **种子**：`Data/Source/QuestGraph.json` = 完整第一次下潜循环图，编辑器内嵌同一份作首开默认（空 localStorage 自动载入，另有「载入默认」按钮）。门户 `portal-launcher.js` 新增「工具」组 id=`quest-graph`。
- **验证**：合并脚本实测——整图喂入 dry-run 幂等(+0 -0)、造增量补丁正确新增、**真写回逐字节无 diff**（无引号/列序噪声）。修了一个 bug：`graph_to_patch` 原先没把 gate 连线折叠成前置/奖励标签（只有编辑器 JS 做了），现已对齐——直接喂整图也不丢 `Site.S0_Briefed` 这类依赖。工具为纯前端 file:// 可直接双击打开。
- **填内容（用工具产出）**：把第一次下潜循环任务写进数据——保留现有 3 条 L0（修电梯主线/搜刮支线/隐藏信号），新增 `MQ_S0_Arrival`（S0 身份闸口+整备，ReachVolume `Vol_S0_Hub`/Interact `Resupply_S0`）→ `MQ_Containment_Filtration`（L1 活体过滤：RepairCount/HoldTimer `Enc_Filtration` + KillCount `EG_Containment`）→ `MQ_ReturnToS0`（WorldStateTag `Site.ReturnedToS0`），gate 连线串 `Site.S0_Briefed`/`Site.ContainmentStabilized`。已 `ApplyQuestGraph` 合并进两张 CSV（现有行零 diff）+ `GenerateGameData` 重生成（通过）。**运行时即加载**（`UCSItemDataSubsystem::LoadQuestsCsv/LoadObjectivesCsv`，"Loaded … N quests"），求值器 `UCSObjectiveDirectorSubsystem` 服务端绑 `OnWorldProgressChanged`、按条件武装。**待**：① PIE 验证（Quest-C 仍待 PIE）；② 关卡摆对应触发 actor（`Vol_S0_Hub` 触发体 / `Resupply_S0` 交互物 / `Enc_Filtration` 收容导演 D 未建 / `EG_Containment` 敌人编组 / L0 的 Fuse 拾取 + FuseBox 交互物）——摆好前这些目标只能控制台 `CompleteObjective <id>` 推进，电梯写的 `Site.FirstDescent`/`Site.ReturnedToS0` 已能自动触发。

## 2026-06-13 今日进度摘要：Reward-A 奖励机制扩展（物品奖励 + 资格标签约定，双目标整编通过）

用户要"解锁购买资格/交易资格/给权限卡等等"更多奖励类型。**架构判断**：资格/门禁本质=共享布尔门 → **用世界状态标签**(`RewardWorldStateTags` 已支持，约定 `Buy./Trade./Access.*`)，门已能按 WorldStateTag 门控，未来商店/交易读 `HasWorldStateTag`——**零新字段、零存档迁移**(不塞冗余 WorldProgress 列；路线/配方那种独立列是因有专属消费系统，商店未建先用标签)。唯一真缺的=**发物品**：
- **新 `RewardItemIds`**(目标完成发物品给玩家)：`FCSObjectiveDefinition` 加 `RewardItemIds`/`RewardItemCounts`(源表 token=`ItemId` 或 `ItemId*N`)；`FCSObjectiveEvent` 加 `GrantItemIds`/`GrantItemCounts`；求值器 `CompleteWithRewards` 折叠进事件；**`ACSGameState::ApplyObjectiveEvent` 在 EventId 去重之后**调 `GrantRewardItemsToPlayers`(遍历 `PlayerArray`→`ACSPlayerState::GetInventory`→`AddItem`，发给**当前所有玩家**=共享奖励，服务端权威，背包满则丢弃 v1)。**幂等**：事件去重 + 已完成目标不再触发→读档不重发；**坑**：晚加入玩家拿不到物品奖励(门禁建议用标签=随时可查，物品奖励适合补给)。`UCSItemDataSubsystem::LoadObjectivesCsv` 解析 `RewardItemIds` 列(`*N` 数量)；`Objectives.csv` 加列(生成器逐列透传、无需改)；`#CN/#DESC` 补列说明。
- **工具链**：节点编辑器加「奖励物品 RewardItemIds」字段(objRow/inspector/import)；`ApplyQuestGraph.py` `graph_to_patch` 加该列 + **`CsvTable.upsert` 支持补丁带新列时自动扩展机器表头**(平滑加列无需手改 CSV)。
- **演示**：`S0_Resupply`(整备与归档)奖励 `Buy.S0Requisition`(解锁 S0 购买资格)——纯标签、零资产依赖，证资格解锁链路通。
- **验证**：`CoopSurvival` 运行时 + `CoopSurvivalEditor` 编辑器**双目标整编通过**(编辑器这次也编过=此前已关，门/开关 Interact-Door-A 一并进了编辑器目标)。`GenerateGameData` 重生成、生成表含 `RewardItemIds`。待 PIE。

## 2026-06-13 今日进度摘要：QuestGraph 链式重构 + 编辑器收尾(A) + 任务 HUD 设计稿(B)

**链式流程重构（用户拍板，取代星型）**：`quest-graph-editor.html` 改成"一条任务=任务节点出发的一条连续链"。`flow` 连线(连到目标)串 任务→目标→目标，**Order/QuestId 由拓扑自动推导**(BFS from quest，inspector 只读)，分叉=并行；`gate` 连线(连到下个任务)带世界标签做解锁；连线语义看终点类型；端口左进右出。`ApplyQuestGraph.py` 的 `graph_to_patch` 同步做链式派生(否则直接喂整图 QuestId 全空)；旧 `contains` 边载入迁移成 `flow`。`Data/Source/QuestGraph.json` + 编辑器内嵌默认重排成单条横向 ribbon。

**A·编辑器收尾**：① **连线可点选**(加宽透明 hit path + `#wires` 子元素 `pointer-events:stroke`)→右侧 inspector 删除/改 gate 标签，`Del` 删选中连线或节点；② **校验升级**：flow 环检测(DFS)、悬空前置标签(任务 prereq 无目标产出→提示须世界设备写)、孤儿奖励标签、未归属目标；③ **适应全图**按钮(算 bounds 居中缩放)；④ 左下角**常驻图例**(节点色=类型 Main金/Side蓝/Hidden紫/目标红；连线 flow金/gate蓝；端口左进右出)。node --check 通过。

**B·任务 HUD 设计稿**：`Docs/SystemDesign/UI/UI_QuestHUD.html`(link `ui-theme.css`，portal `ui-questhud`)。四块：①**屏上目标追踪器**(右上，主线+钉选支线；目标行按条件类型变样式：HoldTimer计时条/Count计数/单步勾/并行‖；完成划掉、下一步解锁提示)；②**目标行状态图鉴**(active/done/locked/失败 + 各条件类型显示)；③**状态提示 Toast**(目标完成/任务解锁/任务完成 + 奖励类：权限卡/购买资格/交易资格/配方——蓝色奖励，含用户要的新奖励类型)；④**任务日志面板**(Quest-B 事故记录，按类型分组色条、可展开步骤、Hidden 未揭示不显示、Site 状态世界标签条)。落地说明列了 HUD 快照需加字段 + 奖励类型机制待做。**机制扩展(发权限卡/解锁购买·交易资格)记为后续**：都做成 `ApplyObjectiveEvent` 的奖励字段。

## 2026-06-13 今日进度摘要：Interact-Door-A 服务器权威门 + 数据驱动解锁条件 + 世界状态开关（运行时整编通过，编辑器目标待关编辑器后整编 + PIE）

**背景**：用户接入 ThirdParty/Interaction 门交互包后问"门怎么做、并且要能配条件解锁（做任务 / 拿卡 / 通电才开）"。不整套接门交互包的蓝图（它把门开关/锁状态放客户端事件图、要求角色挂它的组件，违反本项目"服务器权威、客户端只发请求"红线）；改成**用我们的骨架 + 它的门网格/锁面板**，照搬电梯/容器那套服务器权威 + RepNotify 表现。

**新 `ACSDoorActor`（`AActor`+`ICSInteractable`，`World/CSDoorActor.{h,cpp}`）**：
- 组件：门框(根)/门扇(摆动)/锁面板/锁灯(`UPointLightComponent` 红=锁绿=可开)/`UCSInteractionPromptComponent`。默认网格用门包 `SM_DoorFrame`/`SM_MetalDoor`/`SM_CardLock`（编辑器可换）。
- 复制态 `bIsOpen`/`bIsUnlocked`（`ReplicatedUsing=OnRep_DoorVisual`，只服务端改）。门扇摆动=**只在过渡期间开 Tick** 的相对 Yaw 插值（`OpenAngle`/`SwingDuration`），到位自动关 Tick。
- **数据驱动解锁条件 `TArray<FCSDoorRequirement>`**（核心）：每条 `{Type, Value(FName), Quantity, bConsumeOnUse, CustomFailurePrompt}`，`Type∈{WorldStateTag(通电), ObjectiveComplete(做任务), RouteUnlocked(解锁路线), Keycard(背包持卡)}`，全部满足(AND)才可开。空列表=普通门。
- `EvaluateRequirements(pawn)` 服务端裁决 + **客户端也能算**（GameState 复制、本地玩家背包对 owner 复制）→ 聚焦时提示直接显示"缺什么"（`GetInteractionPrompt` 锁着时返回原因文案）。`CanInteract` 永远 true（锁着也要能按 E 拿到"需要 X"反馈）。
- 钥匙卡 `bConsumeOnUse` 开门成功即从背包 `RemoveItem`。`bLatchUnlock`（默认 true=破封后永久解锁；false=每次开重验，配掉电门/每次刷卡）。`bAutoClose`+`AutoCloseDelay` 定时自动关。`WorldTagOnFirstOpen`/`ObjectiveOnFirstOpen`=开门驱动世界进度（如开收容门即完成目标）。
- 锁灯颜色：绿 = 已破封 或（无私有钥匙条件且共享条件已满足）；否则红。订阅 `ACSGameState::OnWorldProgressChanged`（事件驱动非轮询）→ 通电/解锁瞬间锁灯+提示立即刷新（门不会自己开，仍需按 E）。

**新 `ACSWorldStateSwitch`（拉杆/总闸，`World/CSWorldStateSwitch.{h,cpp}`）= "通电"来源**：交互在服务端往 `ACSGameState` 写一个世界状态标签（`AddWorldStateTag`，如 `Power.SectorB.On`）；挂同名 `WorldStateTag` 条件的门即被点亮可开。支持 `RequiredItemId`(保险丝/电池，可消耗)、`bOneShot`(一次性 / 可来回扳)。默认网格用门机关包 `SM_Lever`。状态灯绿/红。

**GameState 扩展**：加 `RemoveWorldStateTag(FName)`（断电/来回扳；同时清掉对应 `<tag>_Set` 事件Id 允许再次置位），服务端权威 + 复制广播。

**存档**：`FCSWorldActorSaveData.Doors`（`FCSDoorSaveData{PersistentId/Transform/bIsOpen/bIsUnlocked}`），`CSPlayerController` 存/读循环照搬容器模式（读档 `Door->ApplySavedState`）；开关状态本就活在 WorldProgress 存档里、无需单独存。

**验证**：`CoopSurvival` 运行时目标整编通过（Result: Succeeded）。**编辑器目标因编辑器开着 Live Coding 占用未编**——新增 UCLASS 无法 Live Coding 热加，需关编辑器后整编 + 重启。待 PIE（Listen Server+客户端）：锁门红灯+"需要X"提示、拿卡/做任务/扳总闸通电后变绿可开、钥匙卡消耗、双端一致、开门世界进度副作用、存读档保留开/解锁态。

## 2026-06-09 今日进度摘要：AI-Chase-Config 追击穷追 + 返回近身重警戒 + AIArchetype 行为模版（数据驱动，PIE 待验证）

**背景**：① 追击中玩家一拐弯/绕柱断视线，怪立刻冷静慢走去"调查"——"拐弯秒怂"违和；② 脱战返回途中怪无视近身玩家（旧代码返程一刀切丢弃所有感知以防 leash 边缘抖动）；③ 三种怪 AI 逻辑相同、只数值不同，要按怪可配置行为模版（用字段/数据区分）。

**已完成（C++ + 数据管线，runtime 编译通过；编辑器/PIE 待验证）**：
- **穷追（Pursuit）新状态** `ECSEnemyAIState::Pursuing`：追击丢失视线 → 保持攻击性 + 全速冲"最后所见点" `PursuitGraceSeconds` 秒；期间重新看到→无缝恢复追击（不重复咆哮）；到点（`OnMoveCompleted`）或超时仍没逮到→降级为谨慎调查。修"拐弯秒怂"。`PursuitGraceSeconds<=0` 回退旧行为（丢视线即调查）。
- **返回近身重警戒**：返程途中"看到玩家且距离 ≤ `ReturnReaggroRadius`"即重新追击；`UpdateChase` 的 leash 加"贴身豁免"（目标在该半径内不因超 leash 放弃）防来回抖。修"返程无视近身玩家"。受伤 ForceAggro 仍是另一条打断入口。
- **AIArchetype 行为模版**（`ECSEnemyAIArchetype` 枚举 + `EnemyDefinitions` 字段）：Wanderer（默认，现行为）/ Brute（派生 `bReactToNoise=false`：只认可见目标、无视脚步枪声）/ Sentry（派生 `bEnableWander=false`：守点不游荡）/ Stalker（已识别，潜伏/突袭行为留待下一单元，暂按游荡）。控制器按 archetype 派生行为开关，数值（穷追/leash/重警戒）走各自字段。
- **数据管线**：`EnemyDefinitions.csv` 新增 `AIArchetype`/`PursuitGraceSeconds`/`ReturnReaggroRadius` 三列；`GenerateGameData.py` 加校验；`FCSEnemyDefinition` + `LoadEnemyDefinitionsCsv` 解析；`ApplyPerceptionDefinition` 套用。现有预设：Basic=Wanderer(3.0/600)、Brute=Brute(4.5/750)、Stalker=Stalker(2.0/500)。改表 → `python Scripts/GenerateGameData.py` → 重启生效，不改逻辑代码。
- 调试：`cs.ai.DrawPerceptionDebug 1` 新增 PURSUING 标注（红线连"最后所见点"）。

**如何验证**：关编辑器 → 编 `CoopSurvivalEditor` → PIE（Listen Server + 一只会动的怪）：① 追击中绕柱/拐角断视线，怪应保持凶 + 全速冲到拐角再搜，而非秒怂；② 引怪出 leash 让其返回，玩家靠近应被重新警戒；③ 把某怪 `EnemyDefinitions` 的 `AIArchetype` 改 Brute/Sentry 看行为差异（无需重编，改表重生重启）。

**待做**：用户 PIE 验证 + 调 `PursuitGraceSeconds`/`ReturnReaggroRadius`；Stalker 潜伏/突袭行为为下一单元；（远期）行为复杂后迁 StateTree、群集型。

**改动文件**：`Items/CSItemDataTypes.h`、`Items/CSItemDataSubsystem.cpp`、`Data/Source/EnemyDefinitions.csv`、`Scripts/GenerateGameData.py`、`AI/CSEnemyAIController.h/.cpp`。

## 2026-06-09 今日进度摘要：Enemy-Locomotion 反脚滑（Stride Warping 接入，C++/插件侧，AnimGraph 连线待用户）

背景：
- 怪物移动仍有滑步。僵尸用 **in-place(原地)** 动作 + 速度驱动 BlendSpace，移动走 NavMesh `MoveTo`/CharacterMovement；原地片段脚步是固定节奏，与变速胶囊移动不匹配 → 滑。原注释"交给 BlendSpace axis-to-scale"对 in-place 片段不成立（那依赖采样自带 root motion 速度），且播放倍率补偿默认关着 → 多数速度下都滑。
- 用户作为项目负责人**批准启用 Experimental 的 `AnimationWarping` 插件**作为本单元的专门授权（`AGENTS.md` 对 Experimental 上生产路径要求专门单元批准；本改动仅动动画表现层，不碰玩法/网络）。

目标：
- 用 Stride Warping（改步幅/脚 IK，不改播放倍率，腿不会风火轮/慢动作）让脚钉地不滑，NavMesh 胶囊移动与服务端权威**完全不变**。

完成内容（C++/插件侧，已 runtime 编译通过）：
- `.uproject` 启用 `AnimationWarping`（引擎自带插件，启用即加载，无需编译）。
- `UCSEnemyAnimInstance`（线程安全）新增并算好喂给 AnimGraph `Stride Warping` 节点的输入：
  - `StrideScale` = 地速 / 参考脚速，夹 `[StrideScaleMin, StrideScaleMax]`（默认 0.5–1.75），站定=1。
  - `LocomotionDirection`（度，相对朝向）供斜/侧向缩放。
  - **参考脚速自适应**：`bDriveStrideRefFromMoveSpeed`（默认 true）→ 参考脚速 = 本怪 `MoveSpeed` × `StrideRefSpeedScale`，按每怪移速归一化（同一 AnimBP 喂 Basic/Brute/Stalker 移速 400/300/150 各自适配，追击全速时 StrideScale≈1=跑片段原生）；关掉则用固定 `StrideWarpReferenceSpeed` 兜底（预览台/非敌人宿主）。
- `ACSEnemyCharacter::GetMoveSpeed()` 读取口（供上面归一化）。
- MCP 实地确认 `SK_MC03_Full` 是**完整 UE5 Mannequin 骨架**（含 `ik_foot_root`/`ik_foot_l/r`/`foot_l/r`/`pelvis`）→ Foot Definitions 按标准填，无需补虚拟骨。

待做（编辑器侧——AnimGraph 节点拓扑 Python 改不了，需手动 + 用户验证）：
- `ABP_ContaminatedMutant` 的 AnimGraph：在移动姿势输出后（`Blend Poses by bool` 合并后、死亡姿势混合前）插 `Stride Warping`，填 Pelvis=`pelvis` / IK Foot Root=`ik_foot_root` / Foot Definitions(`ik_foot_l`+`foot_l`、`ik_foot_r`+`foot_r`) / Stride Direction=(1,0,0)，`Stride Scale` 引脚 Expose 后连 `StrideScale` 变量。
- **前提**：移动 BlendSpace 速度轴采样要覆盖 idle→walk→run（约 40–400），让步态随速度切换；Stride Warping 只在步态内校正、**不能把跑片段变成走片段**。
- PIE 验证 + 调 `StrideRefSpeedScale`（追击脚仍偏快则降到 ~0.8；游荡太慢嫌脚快可降 `StrideScaleMin` 到 ~0.35）。
- 编辑器需 `Ctrl+Alt+F11` 热编或关闭后重编 editor target 才能看到新 AnimInstance 字段。

## 2026-06-09 今日进度摘要：代码审查驱动的性能/正确性收口（C/G/J/F/D/E，PvE 无反作弊前提）

背景：
- 对全 `Source/` 做了一次对照 `AGENTS.md` 红线的系统审查（多子代理分块）。本作为四人联机 PvE、对作弊无要求（与 `AGENTS.md` 第 88 行一致），审查中的"GM/调试 RPC 门控、防远程刷取的距离校验、`WithValidation`"等**反作弊向**问题作废/降级；保留并修复的是与作弊无关的**性能、正确性、网络卫生**问题。
- 本批均按"零回归"（不改变正常系统对外行为）执行，纯 C++，未动数据表/存档结构/对外复制语义契约。

完成内容（按问题）：
- **C 动画线程射线**：`UCSPlayerAnimInstance::NativeUpdateAnimation` 原每帧调 `ResolveMuzzleToCrosshairAimOffset` → 在动画更新线程对 `UWorld` 发射线（线程安全风险 + 与角色 Tick 的瞄准 trace 重复）。改为游戏线程 Tick 里 `UpdateCachedMuzzleAimOffset` 算好缓存（复用 `LocalAimTargetWorldLocation` / 复制的 `ReplicatedAimTargetWorldLocation`，不再额外 trace），AnimInstance 只读 `GetCachedMuzzleAimOffset()`。本机每帧瞄准 trace 2→1。
- **G 两个功能 bug**：(1) `UCSSurvivalStatsComponent::ApplyHealthDelta` 治疗分支补 `IsAlive()` 守卫——禁止把 0 血目标治疗复活（死亡为权威终态）。(2) `UCSStatusEffectComponent` 的 `RemainingSeconds`（从不递减、UI 倒计时卡住）改为绝对到期时刻 `ServerFinishSeconds`（仿 crafting），显示侧用 `结束−当前` 算剩余。
- **J HUD/面板每帧快照**：`ACSPlayerController::BuildHUDSnapshot()`（含配方表全量扫描+排序）原被每个 Slate getter 每帧各调一次（HUD 常驻时 20–40 次/帧）。`SCSHUDWidget` / `SCSInventoryPanelWidget` 改为重写 `SWidget::Tick` 每帧只建一次缓存、getter 读缓存（背包面板事件驱动的 `RefreshFromSnapshot` 用前也刷新缓存保持列表最新）；`SCSGMPanelWidget` 的世界进度串/最近电梯查询（每帧全场 `TActorIterator`）改 0.5s 限频缓存；删死代码 `SCSHUDWidget::GetEquippedText`。
- **F Tick 本地守卫 / 空闲关 Tick**（兼清架构 §6 待办）：角色相机插值 `UpdateCameraHeight` 加 `IsLocallyControlled()` 守卫（服务器/模拟代理不再每帧空跑相机插值）；电梯 `ACSStreamingElevatorStation` 设 `bStartWithTickEnabled=false`，仅 `StartTravel→CompleteTravel` 期间于服务端开 Tick（客户端靠 `SetReplicatingMovement` 插值，本不需本地 Tick）。
- **E owner-only 复制**：`UCSInventoryComponent` 的 `InventoryStacks` / `EquipmentHotbar` / `SelectedEquipmentSlotIndex` / `WeaponMagazines` / `WeaponUpgrades` 改 `COND_OwnerOnly`（唯一非服务端读取点是本机 `BuildHUDSnapshot`）；`EquippedItemId` / `WornAppearanceItemIds` 保持全端复制（驱动队友可见的武器/外观）。四人局不再把每人整背包/弹匣/改装广播给所有人。
- **D 静态世界 Actor 休眠**：`ACSResourceNode` / `ACSLootContainer` / `ACSWorkbenchStation` 设 `NetDormancy = DORM_Initial`（初始复制后休眠，省每次净更新的相关性计算）。补齐 flush 覆盖：ResourceNode 采集处、LootContainer 空容器生成提前 return 分支各补一处 `ForceNetUpdate`（UE5 对休眠 Actor 会自动 `FlushNetDormancy`）。各 Actor 初始可见状态在各自 `BeginPlay` 本地 apply，不依赖初始 OnRep。

刻意未做（保零回归 / 留专门单元）：
- 属性 `UCSAttributeComponent::Snapshot` 改 OwnerOnly：`MoveSpeedScalar` 可能被模拟代理移动预测读取，需先核实，暂留。
- `ACSWorldItemActor` 休眠：动态生成且短命（拾取即 `Destroy`），与 `DORM_Initial` 交互脆弱、收益低。
- 世界时钟 `ServerWorldTime` 改 epoch+DayLength 本地推算：属重构、有时间 desync 风险，留专门单元（架构 §6 待办仍挂）。

验证情况：
- `CoopSurvival Win64 Development` 运行时目标多次编译 + 链接通过（0 error）。纯 C++ 改动，未动数据表/存档结构。

待用户验证（网络相关，建议 Listen Server + 1 客户端）：
1. E：客户端上**队友的武器/换装外观仍正常显示**；本人 HUD/背包/弹药/合成正常。
2. D：**后加入的客户端**看到资源点/容器/工作台正确初始状态；采集耗尽、容器取物、工作台启停在客户端同步。
3. F：电梯在客户端仍平滑往返；相机/瞄准表现不变。
4. G：Heal 类 Buff 不能复活已死目标；Buff 剩余时间正常倒数。

## 2026-06-07 今日进度摘要：敌人死亡/受击迭代收口（布偶 + 地面散落 + 部位受击 + 数据驱动爆头）

这是在 Enemy-E/F/G/Data-A 之上的一轮**迭代修正**（含若干试错后的最终方案），覆盖玩家反馈：

- **受伤即被发现 + 发现转身嘶吼**：`ACSEnemyAIController::ForceAggro(攻击者)` 由 `ACSEnemyCharacter::ReceiveHitReaction`（命中钩子）在任意一次玩家直接命中时调用——背后/视野外偷袭也会锁定并追击；`OnAggroAcquired(Target)` 嘶吼前先转身面向目标。DoT(流血)不触发发现。
- **受击三维度**（Enemy-E 增强）：方向(前/后/左/右) × 部位(头/身) × 力度(轻 `HitBody`/重 `HitHeavy`)。命中点经近战/枪械一路传到受击钩子（`ReceiveHitReaction(攻击者, 伤害, 命中点, 命中骨骼)`）；头部命中（命中点高度 ≥ 胶囊半高 ×`HeadHitHeightFraction`）走专门的 `HitHead` 四方向。
- **数据驱动爆头/弱点**：`EnemyDefinitions` 加 `HeadHitHeightFraction`、`HeadshotDamageMultiplier`（每怪不同）；`ICSHitReactable::GetIncomingDamageMultiplier(命中点)` 由攻击方在结算伤害前查询，头部命中 → 伤害 ×倍率（Basic ×2、Brute ×1.5、Stalker ×2.5）。受击力度仍按武器基础伤害判定。
- **死亡改布偶物理**：`StartCorpseRagdoll()`（`SetSimulatePhysics`，用 `PA_MC03_Full_PhysicsAsset`）取代单节点死亡动画——根因是"站立胶囊浮空 + 单节点 PlayAnimation 与 Custom Depth 描边姿势不同步"导致描边/交互对不上身体（经 VibeUE 实地截图 + 取 PIE 尸体骨骼坐标确认：胶囊停在 Z≈98 站立处、身体骨骼贴地 Z≈3-19）。移除了 Dead01-06 死亡动画序列、复制索引。
- **战利品改"地面散落"**（取代尸体搜索）：`SpawnDeathLoot()` 死亡时按 `LootTableId` roll，在尸体附近地面（向下射线找地面）散落生成可拾取的 `ACSWorldItemActor`（与玩家丢弃/拾取同一套，自带可用描边、持久存在直到拾取）。尸体不再可搜刮（`CorpseLoot` 不再生成 → 不可聚焦），布偶尸体按 `EmptyCorpseLifeSpanSeconds` 回收。尸体搜索那套（`ICSInteractable`/`ICSLootSource` 在敌人上的实现 + `CorpseLoot`）暂留为**休眠死代码**，待后续清理。

诊断工具：本轮用 **VibeUE/MCP**（`execute_python_code` + `take_high_res_screenshot` + PIE 世界 `get_game_world`）实地连编辑器看碰撞/骨骼/截图来定位描边问题，避免凭猜（`.mcp.json` 指向 `http://127.0.0.1:8088/mcp`；需单实例干净编辑器、Play 用 Selected Viewport 才能截到 PIE 视图）。

非目标/待清理：删除敌人上休眠的尸体搜索死代码；霰弹逐弹爆头精确（当前按代表命中点）；不同怪种的散落手感微调。

验证（待用户 PIE）：击杀敌人 → 尸体布偶倒地 + 物资散落地面可拾取（有描边）；从不同方向/瞄头打怪看方向性/头部受击 + 爆头掉血更高；背后偷袭怪会转身嘶吼追击。

## 2026-06-07 今日进度摘要：Enemy-F 尸体搜索（击败敌人可搜刮战利品）

目标：
- 给基准敌人补"击杀→奖励"闭环。用户选择**尸体搜索机制**而非掉地上——更贴生存/SCP 主题，且能**复用项目既有的搜刮容器 + 搜刮 UI**（`ACSLootContainer` + `ICSInteractable` + 掉落表 + `SCSInventoryPanelWidget` 搜刮面板）。

关键发现（决定了实现方式）：
- 搜刮 **UI 控件本就与具体容器解耦**——只读 HUD 快照（`OpenLootContainerItems`/名称/PersistentId），取物走 `Controller->RequestTakeLootContainerItem`。唯一硬绑定 `ACSLootContainer` 的是**玩家控制器**（缓存指针 + 开/取 RPC）。所以复用 = 只泛化控制器，不动 UI。

做了什么：
- 新增 `ICSLootSource` 接口（`Components/CSLootSource.h`，纯 C++ 虚函数 UINTERFACE）：`GetLootDisplayName`/`GetLootPersistentId`/`CopyRemainingLoot`/`TryTakeLootItem`。
- `ACSLootContainer` 实现 `ICSLootSource`（头文件内联转发到既有 `RemainingItems`/`DisplayName`/`PersistentId`/`TryTakeItem`，行为零变化）。
- `ACSPlayerController` 搜刮流程泛化：`OpenLootContainer` 由 `TWeakObjectPtr<ACSLootContainer>` 改为 `TWeakObjectPtr<AActor>`；`ClientOpenLootContainer`/`ServerTakeLootContainerItem`/`TakeLootContainerItemOnServer` 首参 `ACSLootContainer*`→`AActor*`；快照实时读、取物、回推面板都 `Cast<ICSLootSource>` 走接口。方法名保留，故容器/控件调用点零改动。存档仍按 `PersistentId` 迭代真实容器（尸体不入档，不受影响）。
- `ACSEnemyCharacter` 实现 `ICSInteractable`+`ICSLootSource`：
  - 死亡时 `GenerateCorpseLoot()` 按 `LootTableId` 生成 `CorpseLoot`（复制；镜像容器掉落算法但用直接 LootTableId，瞬时随机不需种子）。
  - `ApplyDeadCollision()`（服务器 `HandleDeath` 与客户端 `OnRep_IsDead` 都调用，各端一致）：有战利品→胶囊 `QueryOnly` + 仅 `Visibility=Overlap`（交互 `OverlapMultiByChannel(ECC_Visibility)` 能发现，但忽略 Pawn/世界，不挡移动、不挡近战扫掠、不影响导航）；无战利品→彻底 `NoCollision`。**回应"击败的敌人要消除碰撞体"：既不挡路也能被搜。**
  - `ICSInteractable`：仅死亡且有剩余战利品时可交互；`Interact` 经控制器 `ClientOpenLootContainer(this, ...)` 开面板；`SetFocused` 描尸体网格轮廓；提示"搜索<名>"。
  - `ICSLootSource::TryTakeLootItem`：发到玩家库存 `AddItem`、扣减、归并；`bSearchClaimed` 防同帧重复取。
- 掉落表（`LootTableEntries.csv`）：`LT_MutantCorpse`(9mm弹2-6/碎石) 给 Basic+Stalker、`LT_MutantCorpse_Brute`(9mm弹4-9+霰弹+碎石) 给 Brute；EnemyDefinitions 的 `LootTableId` 列已填。

非目标（留给后续）：
- 把容器与尸体的掉落生成逻辑抽到共享 `UCSLootSourceComponent`（当前尸体侧小幅镜像容器算法，已注释；可后续 DRY）。
- 尸体掉落入档/掉地上世界物品；稀有/关键掉落（关键物仍禁随机表）。

验证（待用户 PIE，Listen Server + 客户端）：
- 击杀敌人 → 走近尸体应出现描边 + "搜索…" 提示 → 交互打开搜刮面板 → 取物进背包；面板剩余实时更新。
- 活着的敌人不可交互（CanInteract 死亡才 true）；尸体不挡移动、不挡刀（近战能打到尸体后面的活怪）。
- 客户端与主机看到同一尸体的可搜状态/内容一致。
- 既有摆放容器搜刮应**照旧可用**（控制器泛化后回归测试）。
- 注意：新增 `UINTERFACE`/`UPROPERTY` + 改 RPC 签名，需关编辑器整编 `CoopSurvivalEditor`（已完成；其间清掉一个占用 DLL 的残留 UnrealEditor-Cmd 进程）。

调优补记（2026-06-07）：搜索碰撞——曾尝试用"贴地专用搜索球 `CorpseSearchCollision`（缩小站立胶囊 + 在 Mesh 地面位置建球）"来对齐倒地身体，但**该改法把搜索检测整个搞坏了**（很可能是交互 LoS 射线打到地面 / 运行时组件未按预期生效），已**回退**到可用的**胶囊检测**（死亡时胶囊 `QueryOnly` + 仅 `Visibility` 重叠；无战利品则 `NoCollision`）。用户最初反馈的"靠近判定与倒地身体略对不上"为已知小手感问题，待后续用可验证的方式（必要时 MCP 连编辑器实地看碰撞体）再修，不再凭猜动检测。

调优补记（2026-06-07）：**发现链路增强**——①`OnAggroAcquired(Target)` 在嘶吼前先**转身面向目标**（仅 yaw，`SetActorRotation`），"发现你了"会先扭头看你；背后被偷袭也会转过来。②**受伤即被发现**：`ACSEnemyAIController::ForceAggro(Target)`（新公开方法）强制锁定攻击者并 `StartChase`，由 `ACSEnemyCharacter::ReceiveHitReaction`（命中钩子，带攻击者）在**任意一次玩家直接命中**时调用——无论是否在攻击中/受击节流都会触发发现，故视野外/背后挨打也会转身嘶吼追击。复用首次锁定路径（带 6s 咆哮冷却）；DoT(流血)经 `StatusEffectComponent` 不走命中钩子，不会反复触发发现。当前"受伤发现"会锁定实时位置追击（即使无视线），"去最后已知位置 / 彻底甩开失去目标"留给 Enemy-H。

## 2026-06-07 今日进度摘要：Enemy-Data-A 数据表驱动敌人定义（基准敌人收口为模板）

目标：
- 回答"AI 基准版还缺什么"时，最关键的架构缺口：敌人是全项目唯一**硬编码数值**的系统（物品/配方/武器/Buff 全是表驱动）。把敌人也收口成表驱动，才算可复用"基准模板"——新增怪种 = 加一行数据，而不是改 C++/复制 BP。这也是后续 Enemy-F 掉落、Enemy-H 感知档案、多怪种平衡的前置底座。

做了什么：
- 新增源表 `Data/Source/EnemyDefinitions.csv`（`#CN`/`#DESC`/机器表头 + 数据行），列：`EnemyId, DisplayName, MaxHealth, MoveSpeed, AttackDamage, AttackRange, AttackHitTolerance, AttackCooldownSeconds, AttackMontagePlayRate, HeavyHitDamageThreshold, GrantedBuffIdsOnHit, SightRadius, LoseSightRadius, SightHalfAngleDegrees, SightMaxAge, ChaseAcceptanceRadius, LootTableId`。
- `GenerateGameData.py`：`SOURCE_TABLES` 加表 + 校验块（数值下限、`LoseSightRadius>=SightRadius`、`SightHalfAngle<=180`、`GrantedBuffIdsOnHit` 每个须存在于 `BuffDefinitions`、`LootTableId` 可空否则须存在于掉落表、`EnemyId` 唯一），生成到 `Data/Generated/EnemyDefinitions.csv`。
- `UCSItemDataSubsystem`：新增 `FCSEnemyDefinition` 结构、`EnemiesById` 表、`GetEnemyDefinition`/`IsKnownEnemy`/`GetAllEnemyIds`、`LoadEnemyDefinitionsCsv`（与 Buff 同模式，挂进 `LoadGeneratedData`/Reset/日志）。
- `ACSEnemyCharacter::BeginPlay` 调 `ApplyEnemyDefinition()`：按 `EnemyId` 查表，命中则覆盖 血量/速度/攻击数值/重击阈值/命中Buff/掉落表（全端套用，使客户端移动预测与攻击播放倍率与服务器一致），随后才设 `MaxWalkSpeed`、服务器初始化血量。
- `ACSEnemyAIController::OnPossess` 调 `ApplyPerceptionDefinition()`（仅服务器）：按 `EnemyId` 查表覆盖 视野/丢失/视野角/记忆/追击半径，并 `ConfigureSense` + `RequestStimuliListenerUpdate` 重配视觉感官。
- **回退策略**：表里没有该 `EnemyId` 时保留 C++/BP 默认，未迁移的旧敌人照常工作。BP_AI_* 从此只负责内容/表现（Mesh、动画、选 `EnemyId`）。
- 附三行示例,按 **MoveSpeed 即威胁等级步态**铺开（locomotion 混合空间按速度 idle→走→跑）：`Enemy_ContaminatedMutant_Basic`（速 400 = 快速冲，还原当前数值）、`Enemy_ContaminatedMutant_Brute`（速 300 = 中速，180血高伤重型）、`Enemy_ContaminatedMutant_Stalker`（速 150 = **缓慢走来**，110血/慢攻/高单伤的潜伏型）。同种怪物不同威胁等级 = 复制 BP 改 `EnemyId` 指向不同行,或直接改某行 `MoveSpeed` 重生成即换步态。

调优补记（2026-06-07，并入 Enemy-G）：修了"咆哮→追击有点移步"——咆哮前摇停顿封顶从 0.8s 抬到 `MaxAggroScreamSeconds=2.0`(取与咆哮动作长度较小者)，使移动在咆哮播完后才恢复，不再拖着原地咆哮姿势滑步。

非目标（留给后续）：
- Enemy-F 死亡掉落（`LootTableId` 字段已就位，待接入死亡时按表生成 `ACSWorldItemActor`）。
- Enemy-H 感知档案的"听觉/警觉/搜索"扩展字段（当前只把现有视觉参数入表）。
- 把受击/咆哮等纯表现 feel 参数入表（当前仍在 actor/BP 默认；表先承载权威数值）。

验证（待用户 PIE，Listen Server + 客户端）：
- 放置现有 `BP_AI_ContaminatedMutant_Basic`，行为/数值应与改表前完全一致（血 80、速 400、伤 15…）。
- 改 `Data/Source/EnemyDefinitions.csv`（如把 Basic 的 MoveSpeed 调到 600）→ 跑 `GenerateGameData.py` 重生成 → PIE 应生效，无需改 C++。
- 客户端看到的怪移动速度、攻击播放倍率与主机一致。
- 注意：本单元新增 `USTRUCT`/`UPROPERTY` 与新数据加载，需关编辑器整编 `CoopSurvivalEditor`（已完成）；改 CSV 后必须重新运行生成脚本（运行时只读 `Data/Generated`）。

## 2026-06-07 今日进度摘要：Enemy-G 攻击多样化与发现咆哮

目标：
- 继续把基础污染变异体收口成"基准敌人"：用 Enemy-Anim-A 已重定向但此前只用 Atk01 的攻击动作和闲置的咆哮动作，补上**攻击动作不重复**与**发现预警**两块表现。

做了什么：
- **攻击多样化**：`ACSEnemyCharacter` 新增 `AttackAnimations`(默认 Atk01-03)，`SelectAttackAnim` 每次攻击随机挑一段有效动作（空则回退单个 `AttackAnimation`）。`MulticastPlayAttackMontage` 改签名为 `(AttackAnim, PlayRate)`，服务器挑好后下发各端一致。攻击保持静止时长改按"所选动作"长度算。
- **兜底命中时机**：把命中兜底定时器从"临近动作末尾"改为挥击中段比例 `AttackHitTimeFraction`(0.45)，使没有手放命中 notify 的变体(Atk02/03)也在中段命中；有 notify 的 Atk01 仍由 notify 先触发、兜底被 `bHitAppliedThisAttack` 单次去重挡掉。（如需 Atk02/03 也帧级精确，可后续在其挥击帧手放 `CS Enemy Melee Hit` notify。）
- **发现咆哮**：新增 `AggroScreamAnimation`(AN_Zombie_Screaming) 与 `OnAggroAcquired`；`AIController::StartChase` 仅在"从无目标到有目标"瞬间调用它（持续视野内的反复感知刷新不重复触发）。复用受击硬直的"暂停追击"机制（`bIsStaggered` + `StopMovement` + 共用定时器），停顿 `min(MaxAggroScreamSeconds 0.8, 动作长度)` 作为可读的发现前摇后再冲；`AggroScreamCooldownSeconds`(6s) 防丢失/重获目标刷咆哮。
- 全程服务器权威、表现走多播；零新增美术资产。

非目标（留给后续）：
- Atk02/03 的逐帧命中 notify（当前用中段兜底，足够基准）；攻击连段/方向化攻击。
- Enemy-F 死亡掉落；Enemy-H 潜伏/感知档案；PlayerFeel-A ③ 玩家受击踉跄（接口 `ICSHitReactable` 已就位）。

验证（待用户 PIE，Listen Server + 客户端）：
- 让怪连续攻击你，应看到挥击动作在 Atk01/02/03 间轮换、不再永远同一个。
- 第一次进入怪的视野时，它应先**咆哮停顿一下**再冲过来；全程跑在视野内不会反复咆哮；离开视野再回来、过了 6s 冷却会再咆哮一次。
- 主机与客户端看到同一只怪的攻击动作 / 咆哮一致。
- 注意：本单元改了多播 RPC 签名 + 新 `UPROPERTY`，需关编辑器整编 `CoopSurvivalEditor`（已完成）。

## 2026-06-07 今日进度摘要：Enemy-E 受击反应与死亡多样化（基础怪物收为基准版本）

目标：
- 在 Enemy-A~D（有血/会追/会打/会附流血）和 PlayerFeel-A（玩家受击表现）之上，补上**敌人自己的受击表现**，把这只基础污染变异体收口成可复用的"基准敌人版本"。
- 充分利用 Enemy-Anim-A 已重定向但此前只用了 Atk01/Dead01 的动作：12 段方向性受击（HitBody/HitHeavy × 前后左右）+ 6 段死亡（Dead01-06）。

做了什么：
- 新增可复用受击接口 `ICSHitReactable`（`Source/CoopSurvival/AI/CSHitReactable.h`，BlueprintNativeEvent `ReceiveHitReaction(攻击者, 伤害)`）。`UCSCombatComponent` 近战 (`ApplyMeleeDamageToTarget`) 与枪械 (`ApplyFirearmDamageToTarget`) 命中成功后，在服务器对实现该接口的目标调用，传入攻击者(`GetOwner()`)与伤害量；命中钩子与具体敌人类解耦，玩家角色后续也可实现做受击踉跄。
- `ACSEnemyCharacter` 实现接口：
  - **方向性 + 分级受击**：`ComputeHitDirection` 用攻击者相对自身前/右向量点积判前后左右；伤害 ≥ `HeavyHitDamageThreshold`(25) 为重击。`SelectHitReactAnim` 选 8 段 `AN_Zombie_HitBody*`/`HitHeavy*` 之一（缺则退化到正面轻击），构造函数装默认、BP 可覆盖。
  - **短硬直**：`bIsStaggered` 置位并立即 `AIController->StopMovement()`（取消路径跟随，否则受击姿势滑步），`MaxStaggerSeconds`(0.5) 与动作长度取小者后 `FinishStagger` 解除；`AIController::UpdateChase` 新增 `IsStaggered()` 闸（与 `IsAttacking()` 同位）暂停追击。
  - **节流与不打断**：`HitReactMinInterval`(0.3s) 防连射/连击刷硬直；攻击挥击中 (`bIsAttacking`)、死亡 (`bIsDead`) 不打断（避免与攻击 montage 抢 DefaultSlot）。致命一击因 `ApplyDamage` 同步广播 → `HandleDeath` 先把 `bIsDead` 置真，命中钩子里受击被挡，直接走死亡。
  - **死亡多样化**：`DeathAnimationVariants`(Dead01-06) 服务器 `FMath::RandRange` 挑一段，复制 `DeathAnimIndex`（与 `bIsDead` 同 bunch，OnRep 读时已就位）使各端倒地一致；变体空时回退单个 `DeathAnimation`。
- DoT（流血）经 `StatusEffectComponent` 直接 `ApplyDamage`，不经命中钩子，故不会每跳触发受击——符合预期。

非目标（留给后续）：
- Enemy-F 死亡掉落；Enemy-H 潜伏/感知档案（蹲伏降感知、听觉、警觉条、去最后已知位置）。
- 攻击动作多样化（Atk02/03 + 各自命中 notify）、首次发现玩家的咆哮（`AN_Zombie_Screaming`）作为下一步表现单元。
- PlayerFeel-A ③ 玩家受击踉跄 + 血液 VFX（待美术资产；接口已就位，玩家角色可实现 `ICSHitReactable`）。

验证（待用户 PIE）：
- Listen Server + 至少 1 客户端：从正面/背后/左右砍或射怪，看是否播放对应方向受击且短暂停顿（硬直）；轻武器=HitBody、重击=HitHeavy。
- 连续快速攻击不应把怪按在原地无限硬直（0.3s 节流）。
- 多次击杀同类怪，死亡倒地动作应出现随机变化；主机与客户端看到的同一只怪倒地动作一致。
- 注意：本单元新增 `UCLASS`(接口) 与 `UPROPERTY`，必须关编辑器后整编 `CoopSurvivalEditor`（Live Coding 无法热更新反射变更）。

## 2026-06-06 今日进度摘要：Enemy-C 移动动画与近战攻击/死亡

目标：
- 让 Enemy-B 的"会追"进化成"会动、会打、会死"——接入 locomotion 动画、近战攻击伤害、死亡动画，使这只基础污染变异体成为完整可玩的威胁。

完成内容：
- **Locomotion AnimBP**：`CreateEnemyAnimation.py` 生成 `BS_Zombie_Locomotion`(1D, Speed 轴 Idle/Walk/Run) + `ABP_ContaminatedMutant`(父类 `UCSEnemyAnimInstance`)；C++ AnimInstance 线程安全算 `GroundSpeed`/`bIsMoving`/`LocomotionPlayRate`/`bIsDead`。AnimGraph 手动连 `GroundSpeed→Speed`、`LocomotionPlayRate→Play Rate`、并加 `Slot 'DefaultSlot'` 供 montage。
- **播放倍率随速度**：`LocomotionPlayRate = bIsMoving ? FMath::Max(1, GroundSpeed/LocomotionReferenceSpeed) : 1`——移动越快走/跑越快、脚步跟上；idle 显式恒 1.0 不被加速（用户明确要求）。
- **近战攻击**：`ACSEnemyAIController::UpdateChase` 距离 ≤ `AttackRange` 时停下并 `TryMeleeAttack(Target)`；角色服务器权威：面向目标→Multicast 播攻击 montage→`AttackHitDelaySeconds` 后 `PerformAttackHit` 判距离命中→`SurvivalStatsComponent::ApplyDamage` + `StatusEffectComponent::ApplyBuff`(命中 Buff 预留)→冷却。复用玩家战斗的伤害/Buff 泛型入口，不挂玩家专用 `UCSCombatComponent`。
- **死亡动画**：单节点 `GetMesh()->PlayAnimation(DeathAnimation, false)` 整体接管 AnimBP，停末帧保持倒地。
- **MoveSpeed 真旋钮**：BeginPlay 应用 `MaxWalkSpeed = MoveSpeed`（默认 400），BP 可覆盖。

踩坑与修正（给后续单元提醒）：
- 重定向 `MESH_TO_MESH` 对"同骨架不同体型"反而全乱，`CHAIN_TO_CHAIN` 起手 + 手动 Edit Pose 对齐才对；retarget pose 只有 pelvis 能平移修沉地。
- 脚本比编辑器"导出动画"多做的 `configure_retargeted_animation` 强制 `force_root_lock=True` 是"脚本结果≠手动导出"的根因（pivot 钉平面）；已默认关闭（`CS_ZOMBIE_FORCE_ROOT_LOCK`）。
- 手调 retarget pose 后只能 `CS_ZOMBIE_EXPORT_ONLY=1` 重烤，跑完整脚本会 reset pose；且 commandlet 从磁盘读 RTG，编辑器里改完必须先 Ctrl+S 存盘。
- 死亡 montage 用大 BlendOut "保持姿势"会弄巧成拙（blend-out 触发点=长度−BlendOut，传超长→首帧就融掉），改单节点动画。
- 攻击保持静止时长须按动作实际长度算，短于动作长度会在后半段恢复移动→滑步。
- 多次外部 Build.bat 会打乱 Live Coding 增量基线（`dep.json` partition 警告 → 超 100-action 限）；新增 UPROPERTY 或外部编译后，关编辑器整编最稳，别依赖 `Ctrl+Alt+F11`。

验证情况：
- 运行时 + 编辑器目标均编译链接通过。
- 用户单机 PIE 验证：追击走/跑动画匹配（idle 不加速）、追到停下播攻击动作并对玩家扣血、攻击后半段不再滑步、击杀（手枪/近战）播死亡动作并保持倒地。

待后续单元：
- Enemy-D：把 `GrantedBuffIdsOnHit` 填上污染/流血 Buff（Buff-A/B 已就位）。
- Enemy-F：死亡掉落。
- Enemy-H：潜伏/感知档案（蹲伏降感知、听觉、警觉条、去最后已知位置）。
- 动画打磨：攻击 3 连/死亡 6 种随机、受击踉跄；尸体改 AnimGraph `bIsDead` 分支或布偶。
- StateTree：当 Enemy-H 状态变多时把 AIController 的攻击/追击迁到 StateTree。

## 2026-06-06 今日进度摘要：Buff-B 状态效果组件（跨系统底座生效层）

目标：
- 让 Buff 从"只是字典"（Buff-A 的静态数据）变成"真的会生效"：服务端能给角色添加状态效果，按服务器 Timer 周期结算（如流血每秒掉血），到期自动移除。
- 这是用户能在 PIE 里**直接看到**的第一个 Buff 效果（血条随流血一秒秒下降）。
- 非目标（留给后续）：属性快照修正（Attribute-A）、独立多实例叠加（Buff-E）、存档（Buff-I）、HUD 图标（Buff-H）、近战命中自动附加（Combat-B）。

完成内容：
- 新增 `Source/CoopSurvival/Components/CSStatusEffectComponent.{h,cpp}`：
  - 服务端权威 `ApplyBuff(BuffId, Source)` / `RemoveBuff` / `ClearAllBuffs` / `HasBuff`；权威经 `GetOwner()->HasAuthority()`。
  - 叠层：`StackCount` 加层封顶（`MaxStacks`），`Refresh`/`Replace`/`StrongestWins` 按单实例刷新持续时间（`Independent` 多实例留 Buff-E）。
  - 周期效果：按 `TickIntervalSeconds` 用服务器 Timer（`FTimerManager`，非 Tick）结算；`Damage→ApplyDamage`、`Heal→ApplyHealthDelta`、`Stamina→RecoverStamina` 经 owner 的 `SurvivalStatsComponent`；`Contamination`/`Noise` 暂无对应属性/系统，故意跳过。
  - 到期：`Duration` 策略用单次 Timer 到点 `RemoveBuff`；`Infinite` 不过期；`Instant` 只结算一次不留驻。
  - 周期把目标打死时清空全部 Buff（短时 Buff 不跨死亡留存）。
  - 复制 `ActiveEffects`（`BuffId`/`Stacks`/`RemainingSeconds`）+ `OnActiveBuffsChanged` 委托，供后续 HUD 事件驱动刷新（非轮询）。
- `ACSPlayerCharacter` / `ACSEnemyCharacter`：构造各加一个 `StatusEffectComponent`，并暴露 `GetStatusEffectComponent()`。敌人也带，使 Combat-B 的命中附加 Buff 能作用到敌人。
- `ACSPlayerController`：新增 `ApplyBuffSelf`/`ClearBuffs`/`PrintActiveBuffs` exec（走 `HasAuthority? 直接 : Server RPC` 与现有调试命令同模式）；`ApplyBuffSelf` 服务端校验 `IsKnownBuff` 后调组件 `ApplyBuff`。

验证情况：
- `CoopSurvival Win64 Development` 运行时目标编译链接通过（新文件 `CSStatusEffectComponent.cpp` 及四个改动文件均编译）；`CoopSurvivalEditor` 目标报告 up-to-date。
- 周期掉血/到期行为需用户在 PIE 观察血条，无法在无头环境确认。

待用户验证（PIE 控制台；编辑器若开着先 `Ctrl+Alt+F11` 让改动生效）：
1. `ApplyBuffSelf Buff_Bleed_Light` → 血条开始每秒掉约 2 点（连按几次叠层，掉得更快，最多 5 层），约 12 秒后自动停。
2. `PrintActiveBuffs` → 显示 `Buff_Bleed_Light xN (..s)`。
3. `ApplyBuffSelf Buff_Adrenaline` → 体力恢复变快（`Buff_Adrenaline` 周期回体力）。
4. `ClearBuffs` → 生效列表清空，掉血停止。
5. `ApplyBuffSelf Buff_Unknown` → 提示 unknown。

后续单元：Attribute-A（`FCSAttributeSnapshot` 让护甲/移动/Focus 等属性修正真正生效）、Combat-B（近战命中读 `MeleeWeaponDefinitions.GrantedBuffIdsOnHit`，对目标 `StatusEffectComponent->ApplyBuff`，实现"砍中→敌人流血掉血"）。

## 2026-06-06 今日进度摘要：Buff-A Buff 数据结构（跨系统底座第一单元）

背景：
- 战斗目前只有裸 `ApplyDamage(float)` 扣血，没有统一属性层，也没有 Buff/状态效果层。流血、中毒、污染、护甲、装备词条、AI 攻击附带状态、AIM/SCP 协议效果都缺少落点。`Docs/Development/CombatAttributeBuffFirearm_开工架构说明.md` 与 `BuffSystem_开发拆分与功能逻辑说明.md` 已定义正式架构（静态数据→属性快照→Active Buff→伤害上下文），但代码侧此前一行没动。
- 决定先铺这层"横向"地基，避免枪械实例/敌人攻击/护甲/AIM 长大后再回头重接。按既定 B1→B9 顺序，本单元只做第一步：数据结构层。

目标：
- 把 Buff 的正式静态数据 schema 接入既有 `CSV源 → GenerateGameData.py → Data/Generated → UCSItemDataSubsystem` 管线，运行时能按 `BuffId` 查到分类/持续策略/叠层/属性修正/周期效果。
- 非目标（留给后续单元）：不新增 `UCSStatusEffectComponent`、不做属性快照、不真正施加或移除 Buff、不做 UI、不做存档。

完成内容：
- 新增三张源表（`Recipes`/`RecipeRequirements` 的一对多 join 模式）：
  - `Data/Source/BuffDefinitions.csv`：`BuffId`、分类、`DurationPolicy`/`DurationSeconds`/`TickIntervalSeconds`、`StackPolicy`/`MaxStacks`、`SavePolicy`、`PresentationId`、`Granted/Blocked/RemoveOn/ResistanceTags`。
  - `Data/Source/BuffAttributeModifiers.csv`：`BuffId`+`AttributeName`+`ModifierOp(Flat/Scalar)`+`Magnitude`（允许负值）。
  - `Data/Source/BuffPeriodicEffects.csv`：`BuffId`+`PeriodicType`+`DamageType`+`MagnitudePerTick`。
  - 初始 5 个示例 Buff 覆盖正向/负向/装备/AIM 与 Flat/Scalar/周期 Damage/Contamination/Stamina：`Buff_Bleed_Light`、`Buff_ContaminationLeak`、`Buff_Adrenaline`、`Buff_ArmorPlate_Body`、`Buff_AIM_Anchored`。
- `Scripts/GenerateGameData.py`：三表加入 `SOURCE_TABLES`；新增枚举校验（分类/持续/叠层/存档/修正方式/周期类型白名单）、副表 `BuffId` 引用完整性校验、`require_signed_float`（修正值允许负）与 `require_enum` 助手。生成成功。
- `Source/CoopSurvival/Items/CSItemDataTypes.h`：新增 `FCSBuffAttributeModifier`、`FCSBuffPeriodicEffect`、`FCSBuffDefinition`（命名与 GAS 概念保持可迁移）。
- `UCSItemDataSubsystem`：新增 `BuffsById` + `LoadBuffDefinitionsCsv`/`LoadBuffAttributeModifiersCsv`/`LoadBuffPeriodicEffectsCsv`（定义先于副表加载，副表按 `BuffId` join），查询 `GetBuffDefinition`/`IsKnownBuff`/`GetAllBuffIds`，加载日志加入 Buff 计数。
- `ACSPlayerController`：新增 `UFUNCTION(Exec) PrintBuff(FName)` 验证入口，无参/`All` 打印全部 `BuffId`，具体 `BuffId` 打印分类/持续/叠层/属性修正/周期效果摘要（走 `ClientDebugLog`，与 `PrintWorldProgress` 同风格）。

验证情况：
- `python Scripts/GenerateGameData.py` 通过，三张 `Data/Generated/*.csv` 已生成（`#CN`/`#DESC` 行被剥离，仅留机器表头+数据）。
- `CoopSurvival Win64 Development` 运行时目标与 `CoopSurvivalEditor` 编辑器目标均编译链接通过（编辑器未开，无 Live Coding 冲突）。

待用户验证（PIE 控制台）：
1. `PrintBuff`（无参）应打印 5 个 BuffId 列表与计数。
2. `PrintBuff Buff_Bleed_Light` 应显示 `Cat=Negative Dur=Duration/12.0s Tick=1.0s Stack=StackCount/5 Save=DoNotSave` 且 Periodic 含 `[Damage 2]`。
3. `PrintBuff Buff_AIM_Anchored` 应显示 `Mods` 含 `[AIMStability Flat 15]`、`[Focus Flat 5]`。
4. `PrintBuff Buff_Unknown` 应提示 unknown。

后续单元：Buff-B（`UCSStatusEffectComponent` 服务器添加/移除/复制摘要）、Attribute-A（`FCSAttributeSnapshot` 基础+装备+Buff 修正按计算顺序合成）、Combat-B（近战命中改 `FCSDamageContext` 并附加 `GrantedBuffIdsOnHit`）。

## 2026-06-06 今日进度摘要：InteractOutline-A 屏幕空间可交互描边

背景：
- 之前可交互高亮用「复制主网格放大一圈的外壳网格」实现，用户反馈这不是想要的——应是基于摄像机视角找物体轮廓再描线的「二次元/cel 描边」。代码里其实已有半成品：角色有禁用的 `bUseInteractionOutlinePostProcess` 后处理通道，`CSResourceNode` 有禁用的 `bUseScreenSpaceFocusOutline`(Custom Depth) 路径，但其余 4 个可交互类只有外壳网格，迁移没做完且不一致。

目标：
- 完成屏幕空间(后处理)描边迁移：可交互物体聚焦时写 Custom Depth，由后处理材质在屏幕空间做边缘检测描轮廓；被遮挡时不描（只在可见时）。
- 统一 5 个可交互类的聚焦描边路径。

完成内容：
- 新增 `Scripts/CreateInteractOutlinePostProcess.py`，生成后处理材质 `/Game/Materials/Interaction/M_InteractOutline_PP`（域=PostProcess，Custom HLSL 节点：采样相邻像素 Custom Depth 求二值轮廓，并与 SceneDepth 比较实现「只在可见时描边」）。参数 `OutlineColor`(琥珀金 1.0,0.6,0.15) 与 `Thickness`(默认 2) 可调。
- `ACSPlayerCharacter`：构造加载该材质到 `InteractionOutlinePostProcessMaterial`，`bUseInteractionOutlinePostProcess` 默认改为 `true`（`ApplyInteractionOutlinePostProcess` 把材质挂到本地相机后处理）。
- 5 个可交互类聚焦时改为主网格 `SetRenderCustomDepth(true)`（电梯为 `ElevatorMeshComponent`，其余 `MeshComponent`），`CustomDepthStencilValue=1`；`CSResourceNode` 翻转开关 `bUseScreenSpaceFocusOutline=true`/`bUseObjectFocusOutline=false`。
- 旧外壳网格 `FocusHighlightMeshComponent` 路径起初**禁用保留**作为回退；用户 2026-06-06 确认后处理描边 OK 后已**清理移除**：5 个可交互类删除 `FocusHighlightMeshComponent` / `FocusOutlineMaterial` / `FocusHighlightScale` / `RefreshFocusHighlightMesh` 及 `bUseObjectFocusOutline` 等开关，聚焦只剩主网格 Custom Depth 单一路径；生成脚本 `CreateObjectOutlineMaterial.py` / `CreateInteractOutlineInstance.py` 已删除。`MI_/M_InteractObjectOutline.uasset` 两个孤立资产待编辑器关闭后删除（已无任何代码引用）。
- `r.CustomDepth=3`（Enabled with Stencil）项目设置原本已开启，无需改动。

验证情况：
- `CoopSurvival Win64 Development` 运行时目标编译链接通过；`CoopSurvivalEditor` 编辑器目标编译链接通过（编辑器关闭后）。`M_InteractOutline_PP` 已由 commandlet 生成成功。
- 描边线宽/颜色无法由我视觉确认，需用户在 PIE 中查看并调 `M_InteractOutline_PP` 的 `Thickness`/`OutlineColor`。

待用户验证：
1. 看准可交互物体（资源点/容器/工作台/电梯/掉落物）时，出现沿轮廓的琥珀金描边，且**不是**整体放大一圈的外壳。
2. 物体被墙/箱遮挡时不显示描边。
3. 线太粗/太细时调材质 `Thickness`（建议 1–4）；颜色调 `OutlineColor`。
4. 如完全不显示：确认 `BP_PlayerCharacter` 未覆盖 `bUseInteractionOutlinePostProcess`（应为 true）、`InteractionOutlinePostProcessMaterial` 已指向 `M_InteractOutline_PP`。

清理（2026-06-06 已完成）：
- 已移除 5 个可交互类的外壳网格路径与生成脚本，描边保持单一实现；仅剩 `MI_/M_InteractObjectOutline.uasset` 两个孤立资产待编辑器关闭后删除。

## 2026-06-06 今日进度摘要：Enemy-B 感知与追击

目标：
- 给 Enemy-A 的静止敌人接上「看见玩家→直奔玩家」的最小 AI：服务器权威的视觉感知 + 追击移动。
- 按设计简化：听见/看见都直接锁玩家本人（不做调查点/诱饵），丢失视线即停（警觉衰减、去最后位置留到 Enemy-H 潜伏单元）。
- 本轮先用很薄的 C++ AIController，不上 StateTree；等 Enemy-C 出现「待机/追/攻击/搜索」多状态时再换 StateTree，本轮感知/追击代码可继续沿用。

完成内容：
- 新增 `Source/CoopSurvival/AI/CSEnemyAIController.h/.cpp`：`AAIController` 子类，构造时建 `UAIPerceptionComponent` + `UAISenseConfig_Sight`（SightRadius 1200 / LoseSight 1400 / 半角 60° / MaxAge 5，敌我中立友方都探测），并 `SetPerceptionComponent` 注册回基类。
  - 注意：`AAIController` 已有私有 `PerceptionComponent` 成员，自建成员改名 `EnemyPerceptionComponent` 以避开 UHT 遮蔽报错。
  - `OnPossess` 仅服务器绑定 `OnTargetPerceptionUpdated`；感知到「玩家控制的 Pawn」且成功感知→`StartChase`，按 0.25s 循环定时器 `MoveToActor`（接受半径 100）；同一目标丢失感知→`StopChase`（清目标 + `StopMovement`）。
  - 追击每次更新检查自身 `IsDead()`，死亡即停；`OnUnPossess` 清定时器并解绑。
- `ACSEnemyCharacter`：新增 `MoveSpeed`(默认 250)，构造设 `AIControllerClass = ACSEnemyAIController::StaticClass()`、`AutoPossessAI = PlacedInWorldOrSpawned`、`MaxWalkSpeed = MoveSpeed`；`bOrientRotationToMovement` 让其追击时面向移动方向。
- `Build.cs` 早前已加 `AIModule`/`GameplayTasks`/`NavigationSystem`。

验证情况：
- `CoopSurvival Win64 Development` 运行时目标编译链接通过；`CoopSurvivalEditor` 编辑器目标编译链接通过。
- 寻路依赖场景 NavMesh，无法由我在无头环境确认追击行为，需用户在 PIE 验证。

待用户验证（前置：默认地图 `Lvl_FirstPerson` 需有 `NavMeshBoundsVolume` 罩住地面，按 `P` 看到绿色导航网格）：
1. 把敌人（`ACSEnemyCharacter` 或其 BP）放到导航网格范围内，PIE 后从正面进入其视野约 12m 内 → 敌人转向并朝玩家移动。
2. 绕到掩体后断开视线并走开 → 敌人在约 1.4s（MaxAge）后停下，不再跟。
3. 砍死敌人 → 立刻停止移动、8s 后回收（与 Enemy-A 行为一致）。
4. 若敌人完全不动：多半是没有 NavMesh（按 `P` 检查），或敌人没被 `ACSEnemyAIController` 接管（确认 BP 未覆盖 `AIControllerClass`/`AutoPossessAI`）。

后续单元：
- Enemy-C：换 StateTree 大脑 + 近战攻击（攻击伤害窗口，复用 `AN_Zombie_Atk*`）。
- 死亡/移动动画接入：`OnDeathPresentation` 在 BP 播 `AN_Zombie_Dead*`，AnimBP 用 `AN_Zombie_*` 走/跑。
- Enemy-H：噪音/听觉管线 + 潜伏（贴脸察觉圈、蹲伏降感知、警觉条与衰减、去最后已知位置）。

## 2026-06-06 今日进度摘要：Enemy-Anim-A 僵尸动作包重定向

目标：
- 先把 `GruesomeZombieAnimset` 的僵尸动作正式重定向到项目当前污染变异体目标骨架 `SK_MC03_Full`，供后续 Zombie BP / AnimBP / Enemy-B 使用。
- 本轮只处理 in-place 动作资产，不接 AI 状态机、攻击伤害窗口、移动逻辑、掉落或污染 Buff。

完成内容：
- 新增 `Scripts/RetargetZombieAnimations.py`，显式创建/更新 `/Game/Animation/Retargeting/IKR_ZombiePack_Source`、`IKR_ContaminatedMutant_Target`、`RTG_ZombiePackToContaminatedMutant`。
- 源 Mesh：`/Game/GruesomeZombieAnimset/Mannequin/Character/Mesh/SK_Mannequin`；目标 Mesh：`/Game/InfiniteCollection/MutantCreatureBundle01/MutantCreature03/MutantCreature03_Character/SK_MC03_Full`。
- 输出 29 个项目自有动画到 `/Game/Animation/AI/Zombie`：Idle、normal/offensive walk、offensive run、3 个攻击、12 个受击、6 个死亡、reborn、screaming。
- 输出动画统一关闭 Root Motion 并启用 Root Lock，避免后续 AI CharacterMovement 首版被源动画 root motion 拉动。
- 脚本使用 `RTMP_` 作为 commandlet 内部临时导出前缀，重命名为正式 `AN_Zombie_*` 后保存；验证无 `RTMP_*.uasset` 残留。
- `Scripts/InspectZombieAnimPose.py` 支持 `CS_ZOMBIE_SOURCE_ANIM` / `CS_ZOMBIE_TARGET_ANIM` 环境变量，便于单独诊断某个源动作与目标动作的关键骨采样。

验证情况：
- `python -m py_compile Scripts\RetargetZombieAnimations.py Scripts\InspectZombieAnimPose.py` 通过。
- 姿态诊断确认问题集中在 clavicle / upperarm / hand 链；`NOALIGN` / `CHAIN_TO_CHAIN` 保持错误姿态，`LOCAL_ROTATION_AXES` / `MESH_TO_MESH` 会让手臂链进一步翻转。
- 临时目录只导出 `AN_Zombie_Idle01` 对比后，`GLOBAL_ROTATION_AXES` 可把左右 clavicle / upperarm / hand 的采样差值压到约 0-2 度，作为脚本默认 retarget pose 对齐方式。
- 2026-06-06 追加诊断：`AN_Zombie_Dead01` 旧正式资产与源 `zombie_Dead01_inplace` 对比时，上肢链差异集中在 clavicle/hand；用非 `EXPORT_ONLY` 路径重建 Retargeter 内存状态并临时导出后，上肢差异降到 0-3 度，确认不是 Skeleton 或蓝图引用错误。
- 2026-06-06 Editor 关闭后完整重跑 `Scripts\RetargetZombieAnimations.py`，正式 `/Game/Animation/AI/Zombie` 29 个 `AN_Zombie_*` 全部覆盖成功，命令返回 0；日志无 error，仅有 `duplicate_and_retarget` API 弃用、源动画无曲线、临时 RTMP import data 依赖等 warning。
- 完整重导后无 `/Game/Animation/AI/Zombie/RTMP_*.uasset` 残留；抽查 `AN_Zombie_Idle01`、`AN_Zombie_OffensiveWalk01`、`AN_Zombie_Atk01`、`AN_Zombie_Dead01` 的上肢关键骨采样，最大单骨差异约 0.8-2.7 度。
- 2026-06-06 16:11：用户确认正式 Zombie 目录动作应全部重导后，再次从空 `/Game/Animation/AI/Zombie` 目录执行全量导出；重新生成 29 个 `AN_Zombie_*`，无 `RTMP_*.uasset` 残留，日志 `Success - 0 error(s)`；同样抽查 Idle/OffensiveWalk/Atk01/Dead01，上肢最大单骨差异约 0.8-2.7 度。
- 旧的 `/Game/Dev/RetargetTests/ZombiePose*` 诊断目录仍存在，非正式动画来源；后续可单独清理。

待用户验证：
1. 在 Content Browser 打开 `/Game/Animation/AI/Zombie/AN_Zombie_Idle01`、`AN_Zombie_OffensiveWalk01`、`AN_Zombie_Atk01`、`AN_Zombie_Dead01`，确认 Preview Mesh 为 `SK_MC03_Full` 时肩膀、手腕、脚底姿态合理。
2. 如蓝图仍显示旧姿态，关闭并重开对应 Anim Sequence / AnimBP，确认引用的是 `/Game/Animation/AI/Zombie/AN_Zombie_*`，不是 `/Game/Dev/RetargetTests` 或 `GruesomeZombieAnimset` 源动画。
3. 后续接 Zombie BP / AnimBP 时只引用 `/Game/Animation/AI/Zombie/AN_Zombie_*`。

## 2026-06-06 今日进度摘要：Enemy-A 基础敌人本体与受击

目标：
- 落地 `SD_45` 污染变异体的第一刀（Enemy-A）：一只静止、能被玩家现有武器击杀的敌人，验证“有血、能挨打、会死”的服务器权威链路。
- 不接 AI 大脑、移动、感知、攻击、掉落——这些排在后续单元。

完成内容：
- 新增 `Source/CoopSurvival/AI/CSEnemyCharacter.{h,cpp}`：服务器权威基础敌人，`bReplicates`，含 `UCSSurvivalStatsComponent` 作为血量（饥饿/口渴/饿伤衰减全部置 0，仅用作生命值）。
- 胶囊显式 `SetCollisionResponseToChannel(ECC_Visibility, ECR_Block)`，对接 `UCSCombatComponent` 的 `SweepMultiByChannel(ECC_Visibility)` + 通用 `FindTargetStats`，无需改动现有战斗代码即可被现有近战/枪械命中扣血。
- `BeginPlay` 服务器初始化 `MaxHealth`（默认 80，对应 SD_45 L0 教学版）；监听 `OnHealthChanged`，血量归零走 `HandleDeath`：停移动、清碰撞、触发 `OnDeathPresentation`（BlueprintImplementableEvent，留给 BP 播死亡表现），并按 `CorpseLifeSpan` 回收尸体。
- `bIsDead` 复制 + `OnRep_IsDead`：客户端同步关闭碰撞并触发死亡表现。
- 默认 Mesh 加载 SD_45 指定的 `SK_MC03_Full`（已确认资源在 Content 内），最终内容/动画/偏移由 `BP_AI_ContaminatedMutant_Basic` 拥有。
- 运行目标编译通过：`CoopSurvival Win64 Development` → Succeeded，生成 `CSEnemyCharacter.cpp.obj`。

上游决策（本轮探讨结论）：
- 血量模型：先复用 `SurvivalStatsComponent`（与 `ACSCombatTargetDummy` 一致），本单元不抽 `ICSDamageable` 接口；待敌人种类变多、脏开始疼时再单独开清理单元迁移。
- AI 大脑选型：C++ AIController + StateTree（后续 Enemy-B）。
- 噪音/听觉、污染 Buff 作为独立上游单元，排在视觉版敌人之后；感知与潜伏设计已写入 `SD_45` v0.2 第 6 章「感知与潜伏系统」。

待编辑器验证步骤：
1. 编辑器内基于 `ACSEnemyCharacter` 创建 `Content/Blueprints/AI/BP_AI_ContaminatedMutant_Basic`（或先直接把 `ACSEnemyCharacter` 拖进测试地图快速验证）。
2. 放进 L0 / 战斗测试地图，进入 PIE。
3. 装备匕首挥砍敌人，确认战斗调试信息出现 `Hit ... -> ...` 且血量下降。
4. 血量归零后敌人进入死亡：停手、不再挡路、按 `CorpseLifeSpan` 消失。
5. Listen Server + 1 客户端：确认客户端也能看到该敌人死亡（碰撞关闭 / 死亡表现触发）。

非目标（明确不在本单元）：
- 移动、感知、追击、攻击玩家、掉落、污染 Buff、动画状态机、遭遇导演、动态难度。

用户验证结果：待验证。

## 2026-06-06 今日进度摘要：Blockout-A 二层楼房子灰盒

目标：
- 用开发期灰盒资产快速验证二层楼建筑的进入、上楼、房间分割、窗口视线和越肩战斗空间。
- 资产走可复用 Blueprint + 测试地图，不把临时房子散落到主线地图里。

完成内容：
- 新增 `Scripts/CreateTwoFloorHouseBlockout.py`，可通过 Unreal Python 重建二层楼灰盒测试内容。
- 新增 `/Game/Blueprints/World/Blockout/BP_House_TwoFloor_Greybox`：包含一层/二层楼板、外墙、室内隔墙、门洞、窗户标记、室内楼梯、屋顶、门框、前廊等灰盒组件。
- 新增 `/Game/Dev/Maps/M_HouseBlockout_Test`：放置二层楼房子、测试地面、PlayerStart、预览相机和基础灯光。
- 新增 `/Game/Materials/Blockout/M_Blockout_*`：墙、地板、楼梯、屋顶、窗、框架、地面等灰盒材质，便于编辑器里快速区分空间功能。

验证情况：
- `python -m py_compile Scripts\CreateTwoFloorHouseBlockout.py` 通过。
- `UnrealEditor-Cmd -run=pythonscript -script=Scripts\CreateTwoFloorHouseBlockout.py` 执行成功，0 error；仅有 UE Python deprecation warning。

待用户验证：
1. 打开 `/Game/Dev/Maps/M_HouseBlockout_Test`。
2. PIE 后确认玩家从入口进入房子、能沿右侧楼梯上二层。
3. 检查门洞、楼梯间、二层地板缺口、窗口视线和右键越肩瞄准时的室内遮挡感。
4. 需要调整体量时，优先改 `BP_House_TwoFloor_Greybox` 或重跑脚本，不直接改主线地图。

## 2026-06-06 今日进度摘要：Crafting-A 合成组件抽取（架构重构）

背景：
- Controller 瘦身三步重构的第二步（第一步见 Inventory-A）。把合成相关的状态与逻辑从 `ACSPlayerController` 移出，进一步缩小「上帝对象」。

目标：
- 把 `CraftingState`（复制进行态）、合成校验/计时/产出流程、工作台查找与配方查询整体迁入新的 `UCSCraftingComponent`（挂在 Controller 上）。
- Controller 只保留请求入口（`RequestCraftItem` / `CraftItem` exec / `ServerCraftItem` RPC）并薄转发到组件；服务端权威、复制语义、对外行为完全不变。

完成内容：
- 新增 `Source/CoopSurvival/Components/CSCraftingComponent.{h,cpp}`：`UActorComponent`，`SetIsReplicatedByDefault(true)`，复制 `CraftingState`（`OnRep_CraftingState` → 拥有者 Controller 刷新背包面板）；`StartCraftOnServer`（校验配方/附近工作台/材料→扣料→计时）+ `CompleteCraftOnServer`（产出）。工作台/配方查询 `FindNearestWorkbench` / `FindUsableWorkbenchForRecipe` / `GetRecipeDefinition` / `GetAvailableRecipesForStation` 一并迁入。权威经 `GetOwner()->HasAuthority()`，计时用组件自己的 `GetWorld()->GetTimerManager()`。
- `ACSPlayerController`：构造创建 `CraftingComponent`；`CraftItem` / `ServerCraftItem_Implementation` 改为转发 `StartCraftOnServer`；移除 `CraftingState` / `PendingCraftRecipeId` / `CraftTimerHandle` / `OnRep_CraftingState` / `CraftItemOnServer` / `CompleteCraftItemOnServer` 及四个工作台/配方辅助函数；因不再有自有复制属性，移除 `GetLifetimeReplicatedProps` override。`BuildHUDSnapshot` 改读 `CraftingComponent->GetCraftingState()` / `GetAvailableRecipesForStation()`；GM「启用最近工作台」改用 `CraftingComponent->FindNearestWorkbench()`。
- UI 未受影响（仍经 `RequestCraftItem` 与 `FCSHUDSnapshot.CraftingState`）。

验证情况：
- `CoopSurvival Win64 Development` 运行时目标编译 + 链接通过（0 error）。编辑器目标编译通过，但用户编辑器正开着锁住 `UnrealEditor-CoopSurvival.dll` 导致链接 `LNK1104`——需关闭编辑器或 `Ctrl+Alt+F11` 后重建编辑器 DLL。纯重构，复制语义与存档结构未变。

待用户验证（建议 Listen Server + 1 客户端）：
1. 靠近工作台合成：材料扣减、产出入包、HUD 进度条与剩余时间正常。
2. 无工作台/材料不足/正在合成时的失败提示正常。
3. GM 面板「启用最近工作台」仍生效。

## 2026-06-05 今日进度摘要：Inventory-A 背包组件抽取（架构重构）

背景：
- 架构审查发现 `ACSPlayerController`（约 1824 行）职责过载，且背包/装备数据在 `PlayerState`、逻辑散在 `Controller`，没有 `AGENTS.md` 命名规则示例里的 `UCSInventoryComponent`。本单元是「Controller 瘦身 / 组件化」三步重构（背包 → 合成 → 存档子系统）的第一步。

目标：
- 把个人背包/装备的**复制状态**（`InventoryStacks` / `EquippedItemId` / `EquipmentHotbar` / `SelectedEquipmentSlotIndex`）和**服务端权威逻辑**（增删/查询/装备/选槽/读档写回）整体迁入新的 `UCSInventoryComponent`。
- `ACSPlayerState` 退化为组件宿主：构造时创建组件、对外暴露 `GetInventory()`，不再直接拥有物品数组或装备逻辑。
- 保持服务端权威、复制语义与对外行为完全不变（纯重构，不改玩法数值或数据契约）。

完成内容：
- 新增 `Source/CoopSurvival/Components/CSInventoryComponent.{h,cpp}`：`UActorComponent`，`SetIsReplicatedByDefault(true)`，四个 `ReplicatedUsing` 属性 + 对应 `OnRep`，`DOREPLIFETIME` 与原 PlayerState 一致；权威检查改用 `GetOwner()->HasAuthority()`，GameInstance/`ForceNetUpdate`/OwningController 均经由 `GetOwner()` 取得。装备变化委托 `OnEquippedItemChanged` 随之迁入组件。
- `ACSPlayerState` 增加构造函数 `CreateDefaultSubobject<UCSInventoryComponent>`，暴露 `GetInventory()`；移除已迁出的背包/装备 API、`OnRep`、辅助方法与 `GetLifetimeReplicatedProps`。`FCSInventoryStack` 结构体与 `FCSOnEquippedItemChanged` 委托类型仍保留在 `CSPlayerState.h`，避免大量 include 改动。
- 旧 `AddResource` 别名删除；`CSResourceNode` 直接调用 `GetInventory()->AddItem`（`ResourceType` 仍作为 `ItemId`，行为一致）。
- 更新全部调用点（7 文件约 30 处）改走 `GetInventory()`：`CSPlayerController`（HUD 快照 / 给予 / 丢弃 / 装备 / 选槽 / 合成扣料与产出 / 存档读写）、`CSCombatComponent`（弹药查询/扣减、当前装备 ItemId）、`CSPlayerCharacter`（装备委托绑定改为追踪库存组件 `BoundInventory`）、`CSPlayerAnimInstance`、`CSLootContainer`、`CSWorldItemActor`、`CSResourceNode`。UI 层未受影响（始终经 Controller 请求与 `FCSHUDSnapshot`）。

验证情况：
- `CoopSurvival Win64 Development` 外部编译通过（29/29，链接成功，0 error）。本单元为纯 C++ 重构，存档结构（`FCSPlayerSaveData` 等）与复制语义未变，旧存档无需迁移。
- 仍需编辑器 + 多端验证（见下）。`UCSInventoryComponent` 作为 `CreateDefaultSubobject` 默认子对象创建，无需改 `BP_PlayerState` 蓝图；若项目存在 `BP_` PlayerState 派生蓝图，打开确认组件已出现即可。

待用户验证（建议 Listen Server + 1 客户端）：
1. 拾取世界物品 / 搜刮容器 / 采集资源点：双端背包数量正确、不重复复制。
2. B 键背包装备、1-4 快捷槽切换：本地与远端持握 Mesh/动画表现正常。
3. 工作台合成：材料扣减、产出入包、HUD 进度条正常。
4. 丢弃物品：生成世界物品、背包扣减；失败回滚正确。
5. 枪械开火：弹药扣减与 HUD 剩余弹药正确。
6. 存档读档往返：背包、装备、快捷槽、当前装备恢复正确。

后续单元（本重构剩余步骤）：
- Inventory-B：抽出 `UCSCraftingComponent`（合成状态/校验/计时/找工作台）。
- Save-B：把 `SaveWorldOnServer/LoadWorldOnServer` 迁入 `UCSWorldSaveSubsystem`。
- 可选优化：`InventoryStacks` 评估 `COND_OwnerOnly`（私有数据无需复制给全体）与 `FFastArraySerializer` 增量复制。

## 2026-06-05 今日进度摘要：Footstep-A 材质驱动脚步声

目标：
- 建立正式脚步声链路：FootIK 后采样脚位，不靠 Actor Tick；脚底 Trace 读取 Physical Material；按脚沿移动方向的落脚相位和 SurfaceType 查表播放走/跑脚步声。
- 支持同一地面多条脚步声 Cue 随机播放，后续只改 `Data/Source/FootstepSurfaceAudio.csv` 和物理材质，不改代码。

完成内容：
- 新增 `UCSFootstepAudioSubsystem`，运行时读取 `Data/Generated/FootstepSurfaceAudio.csv`，按 `SurfaceType` 保存 `WalkCuePath` / `RunCuePath` 多 Cue 列表和 `VolumeScalar`。
- 新增 `UCSFootstepAudioComponent`，挂在 `ACSPlayerCharacter` 上；组件不启用 Actor Tick，主路径由 `UCSPlayerAnimInstance::NativePostEvaluateAnimation()` 在最终姿势评估后调用，左右脚每帧最多各做一次短 Trace。脚在移动方向上完成前摆并进入落脚范围时才播放脚步声，避免 FootIK 把脚长时间压在地面附近导致高度阈值误触发。Dedicated Server 不播放音效。
- 新增 `UCSAnimNotify_Footstep`、`UCSAnimNotify_LeftFootstep`、`UCSAnimNotify_RightFootstep`。动画 Notify 已保留为手工作者入口，但默认 `bPlayAnimationNotifyFootsteps=false`，当前玩家主路径不再由 Notify 直接播放，避免与 FootIK 接地事件重复。
- `DefaultEngine.ini` 注册 `Concrete`、`Metal`、`Wood`、`Water`、`Dirt`、`Grass`、`Gravel` 七个 Physical Surface。
- `Data/Source/FootstepSurfaceAudio.csv` 新增显式 `Default` 行，并把各地面 Walk/Run 改为分号分隔的多 Cue 变体；`GenerateGameData.py` 已生成到 `Data/Generated`。
- 新增 `Scripts/CreateFootstepPhysicalMaterials.py`，创建 `/Game/Materials/Physical/PM_Concrete`、`PM_Metal`、`PM_Wood`、`PM_Water`、`PM_Dirt`、`PM_Grass`、`PM_Gravel`。
- 新增 `Scripts/ConfigureFootstepAnimationNotifies.py`，为 Manny Unarmed/Pistol/Rifle 的 Walk/Jog 动画和 Knife 站立/蹲伏 Walk/Run 动画写入 `CS_Footstep` Notify 轨道。

验证情况：
- `python -m py_compile Scripts\GenerateGameData.py Scripts\CreateFootstepPhysicalMaterials.py Scripts\ConfigureFootstepAnimationNotifies.py` 通过。
- `python Scripts\GenerateGameData.py` 通过。
- `CoopSurvivalEditor Win64 Development` 编译通过。
- `CreateFootstepPhysicalMaterials.py` 通过 `UnrealEditor-Cmd -run=pythonscript` 执行成功，0 error / 0 warning；7 个 Physical Material 资产已创建。
- `ConfigureFootstepAnimationNotifies.py` 通过 `UnrealEditor-Cmd -run=pythonscript` 执行成功，0 error / 0 warning；共配置 80 个走/跑动画。Notify 当前作为手动动画作者标记保留，默认不驱动玩家脚步播放。
- 用户指出项目角色使用 FootIK 后，自动按动画百分比放置的 Notify 会和最终脚位对不上；已改为 FootIK 后脚位采样 + 落脚相位触发主路径。`CoopSurvival Win64 Development` 编译通过；当前 `UnrealEditor.exe` / Live Coding 仍在运行，`CoopSurvivalEditor Win64 Development` 需关闭 Editor 后再外部编译。

关卡/材质使用方式：
- 地面材质或 Mesh 需要指定对应 Physical Material，例如水泥地使用 `/Game/Materials/Physical/PM_Concrete`，金属板使用 `PM_Metal`。
- 新增地面类型时，先在 `DefaultEngine.ini` 注册新 `PhysicalSurface`，创建对应 `PM_*`，再在 `FootstepSurfaceAudio.csv` 加行并运行 `Scripts\GenerateGameData.py`。

待用户验证：
1. PIE 中空手走/跑能听到脚步声，且声音应跟 FootIK 后的脚落地更接近。
2. 给不同地面材质设置 `PM_Concrete` / `PM_Metal` 后，脚步声音色能切换。
3. 装备 Pistol/Rifle/Knife 移动时仍能触发脚步声。
4. Listen Server + Client 下本地和远端角色脚步声表现正常，没有明显重复播放。

## 2026-06-05 今日进度摘要

完成内容：
- 新增右键瞄准镜头偏移参数 `AimingCameraOffset`，可在 `BP_PlayerCharacter` 的 Class Defaults 中调整。
- `ACSPlayerCharacter::ApplyCameraSettings()` 现在会在瞄准且已装备物品时，把 SpringArm 目标位置从普通高度切到右肩偏移位置。
- 默认瞄准镜头从较近的 `AimingCameraDistance=150` 调整为 `240`，默认 `AimingCameraFOV` 从 `55` 调整为 `65`，作为更适合第三人称越肩的起点。

验证情况：
- 本次为 C++ 相机参数和文档记录改动，已做 diff/空白检查。
- 仍需要在 Unreal Editor 中打开 `/Game/Blueprints/Characters/BP_PlayerCharacter`，验证右键瞄准镜头距离和右肩偏移手感。

调参入口：
- `Aiming Camera Distance`：瞄准镜头远近，建议 220-280。
- `Aiming Camera Offset`：瞄准时 SpringArm 本地偏移，Y 正值向角色右侧，建议 Y=35-60，Z=4-12。
- `Aiming Camera FOV`：瞄准视场角，建议 62-70。

## 2026-06-04 今日进度摘要

完成内容：
- 完成 Unit 27-32 的用户验证记录收口：LootTable 容器、存档分组、世界状态核心、路线门禁绑定、GM 面板、电梯层级注册均已标记为通过。
- Unit 33-36 仍保留为待用户验证：电梯过渡 UI/输入限制、装备 locomotion Profile、战斗数据拆分、基础枪械开火与弹药消耗。
- 新增 Unit 37：对关键 C++ 代码补充中文注释，覆盖服务端权威、RPC 请求边界、复制状态归属、数据表读取、枪械扣弹/命中、近战动画通知窗口、容器存档、电梯世界进度门禁等维护重点。

验证情况：
- Unit 37 只修改注释和本文档，不改变运行逻辑。
- 已执行 `git diff --check`，未发现空白/格式错误；当前仅有 Git 的 LF/CRLF 换行提示。

未完成/待验证：
- Unit 33-36 仍需要在 Unreal Editor 中验证具体行为。
- 当前工作区还存在枪械数据、音效/VFX 资源导入等未提交改动；本次注释整理没有回滚或覆盖这些内容。

## 2026-06-05 今日进度摘要：战斗装备动画 + 枪械枪口射线

针对用户反馈"空手却持枪、装枪没持枪姿势、瞄准偏移不对、射击从摄像机出"做修复（覆盖 Unit 34 装备 locomotion 与 Unit 36 枪械方向）。

完成内容：
- 枪口射线（C++）：`ACSWeaponActor` 新增 `MuzzleComponent` 与 `GetMuzzleWorldLocation()`；`ACSPlayerCharacter` 新增 `GetEquippedWeaponActor()`（返回任意已生成装备武器 Actor，不要求 `bUseWeaponSweepForMelee`，否则枪械取不到枪口）；`UCSCombatComponent::FirearmAttackOnServer` 改为先用摄像机求准星命中点，再从枪口起点朝该点发射，散布绕此方向，未命中/无枪口时回退摄像机起点。服务端权威、扣弹、冷却、按目标聚合伤害不变。
- 武器枪口（脚本）：`Scripts/CreateWeaponBlueprints.py` 为枪械 BP 补 `MuzzleComponent`（缺失才建，不覆盖手调）；已存在的 BP_Weapon_Pistol/Shotgun 通过 C++ 构造默认继承枪口，默认在占位网格前端 (50,0,0)。
- 装备动画 System B（AnimBP）：新增 `Scripts/ConfigureFirearmAnimationLayers.py`，在 `ABP_Player_Body` 的 `EquipmentUpperBodySlot` 层前插入 `BlendByBool(bUseEquipmentUpperBodyBlendSpace)`：true→持枪上半身=`AimOffset(AO_Pistol/AO_Rifle)` 叠加在 `BlendSpacePlayer(BS_Pistol/Rifle_UpperBody_Locomotion)` 之上（按 `bUseLongGunEquipmentAnimSet` 选枪型；BlendSpace 方向/速度绑 `EquipmentMoveDirection`/`EquipmentMoveSpeed`，Aim Offset 绑归一化后的 `AimOffsetYaw`/`AimOffsetPitch`）；false→沿用原 `EquipmentUpperBodySlot`（Knife/空手不变）。脚本幂等，并可修复旧图中 AimOffset 误接 `AimYaw`/`AimPitch` 度数变量的问题。
- AimOffset 诊断：当前第三方 Pistol/Rifle AO 资产只有中心、上、下 3 个采样点，X 轴样本全为 0，因此没有真实左右瞄准姿态；左右偏移需要补 `Left/Right` 或 5/9 点 AimOffset 样本，或由角色朝向/Turn-in-place 消化大角度 Yaw。上下抬手手部重叠的首要代码原因是旧 AnimGraph 把度数制 `AimPitch` 直接接到 -1..1 的 AO 轴，几度鼠标就会打满上/下极值；已改为 `AimOffsetPitch = AimPitch / 90`。
- ADS / 腰射规则：新增 `Aim` 输入，默认右键。只有右键按住时 `bIsAiming` 才复制并启用 AimOffset；AimOffset 不再用 Raw ControlRotation，而是用屏幕准星射线求世界准星点，再以当前枪口位置指向该点计算上半身偏移。非 ADS 左键开火不启用 AimOffset，服务器会先让角色 yaw 转向准星点，并把 `HipFireSpreadDegrees` 追加到枪械散布上。
- 临时头部装备：从 `D:\GoogleDownLoad\head+bust+3d+model.zip` 解压并导入 FBX 到 `/Game/Equipment/Head/HeadBust3DModel`，新增 `HeadBustHood` 物品和 `Head` 装备定义。该首版使用现有静态 Mesh 装备挂载链路挂到 `head` 骨骼，`Scale=0.28`、`OffsetZ=-22`，用于快速看头部遮罩效果；正式多槽装备/布料头套仍需后续独立装备槽架构。

诊断结论：
- inspect 确认 on-disk `ABP_Player_Body` 基础 locomotion 已是中性空手（硬依赖仅 `BS_Idle_Walk_Run`/`MM_Idle`/`MM_Jump`/`MM_Fall_Loop`/`MM_Land`/`CR_Mannequin_FootIK`，无任何 Pistol/Rifle/AO 资产）。"空手持枪"不在已保存资产中，判断为此前编辑器会话未保存的实验性改动，关闭时已丢弃；本次未改基础 locomotion。

工具说明：
- AnimBP 接线走离线 `UnrealEditor-Cmd -run=pythonscript`（VibeUE 实时 MCP 需要 API key，本轮未配）。VibeUE 的 `BlueprintGraphEditor.create_node_from_name` / `add_get_member_variable_node` + `AnimGraphService.add_blend_space_player`/`add_blend_by_bool` 足以完成接线，未改动 VibeUE 插件源码。

验证：
- 2026-06-05：关闭编辑器后 `CoopSurvival Win64 Development` 与 `CoopSurvivalEditor Win64 Development` 均编译通过。
- 2026-06-05：`ConfigureFirearmAnimationLayers.py` 跑通；图校验 0 error / 0 warning，AnimGraph 含 2×BlendSpacePlayer + 2×RotationOffsetBlendSpace(AimOffset) + 2×BlendListByBool，7 个变量 getter 类型正确（4 float + 2 bool + 既有 IsFalling）。`CreateWeaponBlueprints.py` 跑通，三把武器 BP 已保存。
- 2026-06-05：`UCSPlayerAnimInstance` 新增 `AimOffsetYaw`，与既有 `AimOffsetPitch` 一样把 -90..90 度瞄准差归一化到 -1..1；`ConfigureFirearmAnimationLayers.py` 已改为连接 `AimOffsetYaw`/`AimOffsetPitch`，并能在已有 System B 图上重接 AO X/Y 输入。`python -m py_compile Scripts\ConfigureFirearmAnimationLayers.py` 通过；`CoopSurvivalEditor Win64 Development` 编译通过。
- 2026-06-05：按右键 ADS / 腰射规则更新后，`python -m py_compile Scripts\GenerateGameData.py Scripts\ConfigureFirearmAnimationLayers.py`、`python Scripts\GenerateGameData.py`、`CoopSurvivalEditor Win64 Development`、`CoopSurvival Win64 Development` 均通过；`ConfigureFirearmAnimationLayers.py` 已重刷并保存 `ABP_Player_Body`，只读验证确认图内存在 `AimOffsetYaw`、`AimOffsetPitch`、`bUseEquipmentAimOffset` 和 4 个枪械布尔混合节点。
- 2026-06-05：HeadBust FBX 导入后文件级验证通过：StaticMesh `/Game/Equipment/Head/HeadBust3DModel/tripo_convert_8500221c-a455-41c1-983d-c18ca471b948` 可加载，`Data\Generated\Items.csv` 与 `Data\Generated\EquipmentDefinitions.csv` 均包含 `HeadBustHood`；`python Scripts\GenerateGameData.py` 与 `CoopSurvivalEditor Win64 Development` 通过。

ADS strafe 操控（C++，2026-06-05）：
- `ACSPlayerCharacter` 新增 `UpdateAimMovementOrientation()` / `IsAimMovementOrientationActive()`：当“`bIsAiming` 且已装备武器”时，把移动组件切到 strafe 朝向——`bOrientRotationToMovement=false`、`bUseControllerDesiredRotation=true`、`RotationRate=AimingRotationRate(默认 900)`，于是身体持续朝相机/准星 yaw，左右输入变横向 strafe（A/D 侧移、不转身）；松开右键或卸下武器恢复默认 `bOrientRotationToMovement=true` + `DefaultRotationRate(500)`。在 `SetAimingLocal`（owning client / listen server）、`ServerSetAiming`（服务器权威）、`UpdateEquipmentPresentation`（装备/卸下重算）三处触发，保证预测端与权威端旋转配置一致。
- `RotationRate` 改为读 `DefaultRotationRate` 配置字段（替换原硬编码 500），与 `AimingRotationRate` 一起为 `EditDefaultsOnly`。
- `UCSPlayerAnimInstance::EquipmentMoveDirection` 用 `Atan2(RightSpeed, ForwardSpeed)` 已是相对身体朝向的方向角（−180..180），strafe 时会正确报告 ~±90°；但腿部要“真的横着走”而非滑步，底层 locomotion BlendSpace 需为方向性（2D/8 向），这是资产侧待确认项。
- 验证：`CoopSurvival Win64 Development` 编译通过；PIE 行为待用户验证。

左手武器 IK（C++ + AnimGraph 脚本，2026-06-05）：
- `UCSPlayerAnimInstance` 新增 `LeftHandWeaponIKLocation`(FVector，Mesh/组件空间)=`LeftHandWeaponIKComponentTransform.GetLocation()`，给 AnimGraph 的 Two Bone IK EffectorLocation 直接用，避免图里再 Break Transform。
- 新增 `Scripts/ConfigureWeaponHandIK.py`：在 `ABP_Player_Body` 输出末端（foot-IK Control Rig 之后、Output Pose 之前）插入 `hand_l` 的 Two Bone IK。因 Two Bone IK 是组件空间骨骼控制而周围图是本地空间，用一对转换节点包起来：`ControlRig.Pose → [本地到组件空间] → IK.ComponentPose`、`IK.Pose → [从组件空间到本地] → Root.Result`（`unreal.BlueprintGraphPinLibrary.try_create_connection` 不会自动插转换节点，必须显式建，菜单名 `动画|转换空间|本地到组件空间` / `动画|转换空间|从组件空间到本地`）。IKBone=hand_l、EffectorLocationSpace=组件空间、AlphaInputType=Float；EffectorLocation←`LeftHandWeaponIKLocation`，Alpha←`LeftHandWeaponIKAlpha`。脚本幂等（已有 TwoBoneIK 则跳过），全步骤成功才保存。
- `Scripts/CreateWeaponBlueprints.py`：枪械 BP 在未启用时打开 `bUseLeftHandIK=true`、`LeftHandIKAlpha=1`（不覆盖作者后调的 alpha）。
- 门控由 alpha 实现：无武器/未持枪时 `LeftHandWeaponIKAlpha`=0，Two Bone IK 为空操作；持枪时左手吸附到武器 `LeftHandIKComponent`。第一版只解算位置，手腕旋转沿用动画；若手腕角度别扭再加 `hand_l` 的 Modify Bone 补旋转。
- 验证：`CoopSurvivalEditor`/`CoopSurvival Win64 Development` 均编译通过；只读校验确认 AnimGraph 含 1×TwoBoneIK + 1×LocalToComponent + 1×ComponentToLocal，三条 IK 输入接线正确；Pistol/Shotgun CDO `use_left_hand_ik=True, alpha=1.0`。每把枪的 `LeftHandIKComponent` 位置仍是估值（Pistol/Shotgun 已配 placeholder/imported 两套），需在编辑器里对着真实模型(SM_1911_X / SM_Shot12_X)微调到护木/握把。PIE 表现待用户验证。

待用户验证（PIE，需 GUI 编辑器）：
1. 空手：中性 idle/走/跑，不再持枪。
2. 装 Knife：与现状一致（System A 未改）。
3. 装 Pistol/Shotgun：出现持枪 BlendSpace 移动 + 上半身随相机俯仰（Aim Offset）；本地与远端一致。
4. 开火：tracer 从枪口飞向准星、扣弹、命中假人扣血；散布/冷却生效。
5. Listen Server + 1 Client 远端表现一致。
6. 临时头部装备：PIE 中执行 `GiveItem HeadBustHood 1`，在背包里装备 `HeadBustHood`，角色头部应出现导入的 HeadBust 模型；如果位置/朝向不合适，调 `Data/Source/EquipmentDefinitions.csv` 中 `HeadBustHood` 的 `EquipmentMeshOffset*`、`EquipmentMeshRotation*` 和 `EquipmentMeshScale` 后重新运行 `python Scripts\GenerateGameData.py`。
- 已知可能需微调：若"装枪/空手姿势相反"或"持枪/瞄准选择反了"，翻转 `ConfigureFirearmAnimationLayers.py` 中 `upper_sel`（或 `gun_sel`）的两个 BlendPose 连接即可（`AnimNode_BlendListByBool` 约定 bActiveValue TRUE→BlendPose_0）。

## 2026-06-05 枪械手感：ADS 镜头 + 开火声效 + 平滑后坐力 + 临时枪口火光

针对用户反馈"装枪后右键应有镜头拉近表现""开火无声效""后坐力生硬"做的一组手感改进（数据驱动，服务端不做严格反作弊验证，表现走客户端预测 + Multicast 同步）。已用户验证手感通过。

完成内容：
- ADS 镜头表现（C++，`ACSPlayerCharacter::ApplyCameraSettings`）：`bIsAiming && 已装备武器` 时把 SpringArm `TargetArmLength` 平滑插值到 `AimingCameraDistance(默认150)`、相机 FOV 插值到 `AimingCameraFOV(默认55)`；松开右键或卸下武器恢复 `ThirdPersonCameraDistance`/`DefaultCameraFOV(90)`。新增 `AimingCameraInterpSpeed`、`AimingFOVInterpSpeed`、`DefaultCameraFOV`、`AimingCameraFOV`、`AimingCameraDistance`，均 `EditDefaultsOnly` 可调；`SetAimingLocal` 与卸装备路径触发即时刷新。
- 开火声效（数据驱动）：`FCSFirearmDefinition` 新增 `FireSoundVariants`(TArray<FSoftObjectPath>)，`MulticastPlayFirearmShotPresentation` 每次随机选一个变体 `PlaySoundAtLocation`。声音资源在 `Content/GunSoundsPackVol1/Gunshot/cue/`，手枪用 `Gunshot_4-1..4-5_Cue`、霰弹枪用 `Gunshot_8-1..8-5_Cue`。CSV 列用分号分隔多个变体路径。
- 平滑后坐力（C++，`ACSPlayerCharacter::ApplyFirearmRecoil`/`UpdateRecoil`）：放弃"开火瞬间 `SetControlRotation` 硬加角度"的生硬做法（用户反馈生硬）。改为 punch+recovery 模型：开火只把冲量累加到 `RecoilTargetOffset`(FVector2D, X=pitch/Y=yaw)，Tick 里 `Vector2DInterpTo` 让 `RecoilAppliedOffset` 平滑追上目标（上抬），只把帧增量叠加到 control rotation（不与鼠标输入打架），同时目标自动插值归零（回落）。连射目标累积形成枪口上爬，停火平滑回正。`FCSFirearmRecoilDefinition` 字段：`RecoilPitchDegrees`/`RecoilYawDegrees`（单发冲量）、`RecoilRiseSpeed`（上抬插值速度）、`RecoilRecoverySpeed`（回落插值速度）、`AimingRecoilDampen`（ADS 衰减）。
- 枪口火光（临时占位）：`MulticastPlayFirearmShotPresentation` 在枪口位置（`GetMuzzleWorldLocation`，无武器 Actor 时回退角色前方）画黄色实心点 + 橙色线框球，显示 0.12s。**这是临时调试占位，待后续替换为 Niagara 粒子或点光源**（见 `Docs/Development/FirearmHandfeel_HandlingDesignPlan.md` Phase 1）。

数据/工具：
- `Data/Source/FirearmDefinitions.csv` 扩列：`FireSoundVariants,MuzzleFlashVFXPath,RecoilPitchDegrees,RecoilYawDegrees,RecoilRiseSpeed,RecoilRecoverySpeed,AimingRecoilDampen`。`Scripts/GenerateGameData.py`（纯 Python，无 UE 依赖，可命令行直接跑）已重生成 `Data/Generated/FirearmDefinitions.csv`。`CSItemDataSubsystem::LoadFirearmDefinitionsCsv` 读取新列（`MuzzleFlashVFXPath` 字段已留，VFX 资源待补）。
- 当前手枪参数：Pitch 1.0 / Yaw 0.4 / Rise 16 / Recovery 9 / ADS 0.6；霰弹枪：Pitch 2.6 / Yaw 1.0 / Rise 12 / Recovery 6 / ADS 0.5。调手感只改 CSV 后跑生成脚本，无需改代码。

验证：
- 2026-06-05：`CoopSurvivalEditor Win64 Development` 编译通过；`python Scripts\GenerateGameData.py` 通过。
- 2026-06-05：用户 PIE 验证 ADS 镜头、随机枪声、平滑后坐力手感均通过（"现在非常好"）。

待办：
- 枪口火光仍是 DrawDebug 占位，需替换为正式 Niagara `MuzzleFlashVFXPath` 资源（CSV 字段已就位）。
- Phase 2/3（后坐力身体动画、命中材质 VFX/音效、弹壳抛出、动态准星）见 `FirearmHandfeel_HandlingDesignPlan.md`。

## Unit 22：Search-A 可搜索容器

目标：
- 新增正式 C++ 场景 Actor `ACSLootContainer`，作为后续搜索/搜刮系统的第一种容器。
- 容器视觉暂用引擎 Cube 和动态材质区分可搜索/已清空状态；视觉占位不承载玩法规则。
- 物品内容使用 `FCSInventoryStack` / `ItemId`，服务端交互后转入 `ACSPlayerState` 背包。
- 容器状态通过 `PersistentId` 保存/读取，避免读档后重复刷新。

非目标：
- 本单元不做完整容器 UI、格子背包、锁定容器、仔细搜索、静音搜索或 LootTable 随机生成。
- 本单元不创建最终 Mesh 或美术资产；缺 Mesh 时继续允许方块/材质占位。

实现记录：
- 新增 `Source/CoopSurvival/World/CSLootContainer.h/.cpp`。
- 新增 `/Game/Blueprints/Interactables/BP_LootContainer`，父类为 `ACSLootContainer`；后续关卡摆放使用该蓝图资产，不直接拖裸 C++ 类。
- 新增 `Scripts/CreateLootContainerBlueprint.py`，用于创建或确认 `BP_LootContainer` 内容资产。
- 新增 `FCSLootContainerSaveData`，并把 `UCSWorldSaveGame::CurrentSaveVersion` 升到 4。
- `ACSPlayerController::SaveWorldOnServer` 现在保存场景中的 `ACSLootContainer` 状态。
- `ACSPlayerController::LoadWorldOnServer` 现在按 `PersistentId` 恢复已摆放容器，不凭空生成缺失容器。
- 保存在线玩家时保留已有离线玩家存档记录，不再用当前在线玩家列表覆盖整个玩家存档集合。
- 项目规则补充：Mesh/美术缺失时可以用方块、Primitive 和材质占位；代码、数据 ID、服务器验证、保存结构和运行时查询不能临时化。
- 项目规则补充：可用事件触发的逻辑默认用事件、RepNotify、委托、Timer、dirty flag 或显式服务器事件，不靠每帧 Tick 轮询；新增 Tick 必须说明理由、限制频率或空闲关闭。
- 本单元 `ACSLootContainer` 不启用 Tick，状态变化只在交互、复制通知、保存/读取时发生。
- 修复背包/制作面板 `Close` 偶发无响应问题：`CloseInventoryPanel()` 现在即使面板状态标志不同步，只要 `InventoryWidget` 仍存在也会强制移除并恢复输入模式。
- `SCSInventoryPanelWidget` 不再每帧 Tick 重建背包/配方列表，改为由 `ACSPlayerController::RefreshInventoryPanel()`、`CraftingState` 复制通知和 `ACSPlayerState` 背包/装备复制通知触发刷新，避免制作或装备时重建 Slate 子树干扰按钮点击。

验证步骤：
1. 在测试地图中放置 `/Game/Blueprints/Interactables/BP_LootContainer`，设置唯一 `PersistentId`，保持默认 `InitialItems` 或配置 `ItemId + Quantity`。
2. PIE 单人看向容器，中心提示应显示 `Search Storage Locker`。
3. 按 `E` 后，服务端把容器物品加入背包，容器变为空状态；再次看向容器不应再显示可搜索提示。
4. Listen Server + 1 Client 同时搜索同一容器，只应有一次成功转移，不应复制物品。
5. 搜索后运行 `SaveCoop`，重开或恢复后运行 `LoadCoop`，同 `PersistentId` 容器应保持已清空或剩余物品状态。

验证：
- 2026-06-02：`CoopSurvival Win64 Development` 编译通过。
- 2026-06-02：`CoopSurvivalEditor Win64 Development` 编译通过。
- 2026-06-03：已通过 `UnrealEditor-Cmd -run=pythonscript` 创建 `/Game/Blueprints/Interactables/BP_LootContainer`。
- 2026-06-03：修复背包 Close 后，`CoopSurvival Win64 Development` 编译通过。
- 2026-06-03：`CoopSurvivalEditor Win64 Development` 外部编译被当前编辑器 Live Coding 拦截，提示需关闭编辑器/游戏或在编辑器内按 `Ctrl+Alt+F11`。

用户验证结果：
2026-06-03 通过。用户确认 Unit 22 可搜索容器已在编辑器验证通过。

## Unit 23：角色位置扇形交互与近战检测

目标：
- 交互目标选择不再从相机位置发射单线 Trace，改为从角色位置出发的扇形候选检测。
- 交互候选只定时刷新，不每帧扫描；在扇形范围内取最近、可交互且无遮挡的目标。
- 近战命中不再从相机位置起算，改为从角色身体位置出发，使用物品表配置的 `MeleeRange` / `MeleeArcDegrees` 做扇形筛选。
- 为后续默认第三人称相机做判定基础，避免相机后移导致交互或近战距离错误。

非目标：
- 本单元不切换默认第三人称相机，不改移动、动画、准星、锁定系统或远程武器。
- 本单元不加入复杂优先级，例如任务物、门、拾取物的类型权重；当前只取最近有效目标。

实现记录：
- `UCSInteractionComponent` 关闭组件 Tick，改用 `FTimerHandle` 按 `FocusUpdateInterval` 刷新 Focus。
- `UCSInteractionComponent` 新增 `InteractionConeDegrees`、`InteractionOriginHeight`、`FocusUpdateInterval`、`MinFocusUpdateInterval` 和 `bDrawDebugFocus`。
- 本地 Focus 使用角色位置扇形规则和 LineOfSight 检查，用于 UI 提示与目标选择。
- 按用户确认，本项目当前允许客户端在目标选择上不做反作弊级约束；服务器交互确认改为轻量状态验证，只确认目标存在、距离仍在合理范围、目标可交互且 `CanInteract` 允许，不再重复执行完整扇形和视线扫描。
- Unit 23 当时的 `UCSCombatComponent` 近战检测曾使用角色位置扇形候选集，这是历史实现记录；该路径已在 Unit 25 被正式武器 Actor + 动画通知窗口 + 多帧刀刃 Sweep 取代，空槽/空手不再有按键瞬间近战伤害 active path。

验证步骤：
1. PIE 单人进入 `/Game/FirstPerson/Lvl_FirstPerson`，在工作台、资源点、掉落物或 `BP_LootContainer` 附近移动视角。
2. 确认可交互提示只在角色正前方扇形内出现，侧后方目标不应抢焦点。
3. 扇形内有多个可交互物时，最近的有效物体应被高亮并成为 `E` 的目标。
4. 切到第三人称视角后，靠近工作台/容器仍应能按 `E` 交互，不应因为相机在身后导致距离判定失败。
5. 放置测试靶，装备或不装备近战武器，从角色正前方攻击应命中；侧后方目标不应被命中。
6. Listen Server + 1 Client 验证客户端请求仍由服务器确认目标状态、距离和 `CanInteract`，多人下不能重复领取容器物品或造成状态不同步。

验证：
- 2026-06-03：`CoopSurvival Win64 Development` 编译通过。
- 2026-06-03：`CoopSurvivalEditor Win64 Development` 编译通过。
- 2026-06-03：按用户确认的“允许客户端负责目标选择，只防状态 Bug”规则，交互服务器确认改为轻量距离/状态验证，并关闭默认 Debug Focus 绘制；`CoopSurvival Win64 Development` 编译通过。
- 2026-06-03：上述轻量验证改动后，`CoopSurvivalEditor Win64 Development` 外部编译被当前编辑器 Live Coding 拦截，提示需关闭编辑器/游戏或在编辑器内按 `Ctrl+Alt+F11`。
- 2026-06-03：用户关闭 Unreal Editor 后重跑 `CoopSurvivalEditor Win64 Development`，编译通过。
- 2026-06-03：修复工作台/装备台模式下背包关闭不稳定：`ToggleInventoryPanel()` 现在把 `bCraftingPanelOpen` 和实际 `InventoryWidget` 也视为已打开状态；Slate 面板内 Close / B / Escape 改为下一帧关闭，避免在当前 Slate 点击事件中移除自身；移除背包 Widget 前清理 Slate 键盘焦点。`CoopSurvival Win64 Development` 和 `CoopSurvivalEditor Win64 Development` 均编译通过。

用户验证结果：
2026-06-04 通过。用户确认 Unit 28 的 `SaveCoop` / `LoadCoop` 存档结构整理流程已在编辑器验证通过。

## Unit 24：默认第三人称相机模式

目标：
- 默认进入第三人称相机模式。
- 不新增第二台 Camera；继续使用现有 `ViewSpringArm` + `FirstPersonCamera`，通过 SpringArm 位置和臂长切换第一/第三人称。
- 保留现有 `ToggleView` 输入，可在第一人称和第三人称之间切换。
- 提供可配置默认项，后续可在角色蓝图默认值中调整是否以第一人称开始。

非目标：
- 本单元不新增独立第三人称 Camera Component。
- 本单元不改准星、锁定、动画蓝图、镜头碰撞规则、相机侧肩偏移或瞄准模式。

实现记录：
- `ACSFirstPersonCharacter` 新增 `bStartInFirstPersonView`，默认 `false`。
- 构造阶段 `ViewSpringArm` 初始位置改为第三人称高度和 `ThirdPersonCameraDistance`。
- `BeginPlay()` 根据 `bStartInFirstPersonView` 初始化运行时视角，并立即应用相机位置，避免开局从第一人称缓慢拉远。
- 相机切换仍复用同一台 `FirstPersonCamera`，只通过 `ApplyCameraMode()` 调整 `ViewSpringArm`。

验证步骤：
1. PIE 单人进入 `/Game/FirstPerson/Lvl_FirstPerson`，默认应看到第三人称身体视角。
2. 按 `ToggleView` 当前绑定键，在第一人称和第三人称之间切换。
3. 第一人称时本地玩家应显示第一人称手臂/装备；第三人称时本地玩家应显示完整角色 Mesh。
4. 第三人称默认视角下，验证 Unit 23 的交互和近战仍从角色位置判定。
5. Listen Server + 1 Client 下，确认本地视角切换只影响本地表现，不影响其他玩家看到的第三人称身体。

验证：
- 2026-06-03：`CoopSurvival Win64 Development` 编译通过。
- 2026-06-03：`CoopSurvivalEditor Win64 Development` 编译通过。

用户验证结果：
2026-06-04 通过。用户确认 S0/L0 电梯路线锁定、`UnlockRoute Route_S0_L0_MainElevator` 解锁和电梯切层测试通过。

## Unit 25：第三人称 Lyra-style 角色动画架构收口

目标：
- 正式抛弃第一人称 active path：不再保留第一人称手臂、owner-only 第一人称武器、第一/第三人称视角切换。
- 把玩家角色 C++ 路径从 `ACSFirstPersonCharacter` 收口到 `ACSPlayerCharacter`。
- 建立项目自有的 Lyra-style 装备动画 Profile 数据契约：`ItemId -> EquipmentAnimProfilePath -> UCSEquipmentAnimProfile`。
- 装备表现从每帧轮询改为 `ACSPlayerState::OnEquippedItemChanged` 事件驱动。
- 建立项目自有第三人称身体 AnimBP `/Game/Animation/Player/ABP_Player_Body`，后续在正式动画单元继续扩展 Linked Anim Layer / 上半身装备状态机。

非目标：
- 本单元不把 Lyra 工程整体迁入项目，不接入 ShooterCore，不铺开 GAS。
- 本单元不一次性完成 Stride Warping、Orientation Warping、Turn In Place、Aim Offset、Hand IK 或 Foot IK。
- 本单元不重命名现有关卡资产路径 `/Game/FirstPerson/Lvl_FirstPerson`；该路径是历史测试地图资产名，后续如要迁移地图路径应作为内容迁移单元处理。

实现记录：
- 删除 `Source/CoopSurvival/Character/CSFirstPersonCharacter.h/.cpp`，新增 `Source/CoopSurvival/Character/CSPlayerCharacter.h/.cpp`。
- `ACSPlayerCharacter` 只保留完整第三人称 Mesh、`CameraSpringArm`、`PlayerCamera`、生存、交互和战斗组件。
- `ACSPlayerCharacter` 默认加载 `/Game/Animation/Player/ABP_Player_Body`，不再把模板 `/Game/Characters/Mannequins/Anims/Unarmed/ABP_Unarmed` 作为玩家 active AnimBP。
- 删除角色运行时第一人称手臂 Mesh、第一人称装备 Mesh/Actor、`ToggleView` 输入绑定、`bStartInFirstPersonView` 和第一/第三人称可见性切换。
- `ACSPlayerState` 新增 `OnEquippedItemChanged` 委托，服务器装备变化和客户端 `OnRep_EquippedItemId` 都会广播。
- `ACSPlayerCharacter` 在 `PossessedBy` / `OnRep_PlayerState` 绑定装备变化事件，不再在 Tick 中轮询装备状态。
- `ACSPlayerCharacter` 现在由 C++ 直接从 `UCSEquipmentAnimProfile` 读取 `UpperBodyIdleAnimation` 和 `UpperBodySlotName`，在装备变化时事件驱动播放持握 idle 到 `EquipmentUpperBodySlot`，卸下或数据无效时停止该 Slot；`BP_PlayerCharacter` 的 `OnEquipmentAnimationProfileChanged` 只保留为可选表现扩展钩子，不再作为持刀 idle 的必需路径。
- 新增 `Source/CoopSurvival/Items/CSEquipmentAnimProfile.h`，定义 `UCSEquipmentAnimProfile`：上半身 Slot、起始骨骼、站立 idle/walk/run、蹲伏 idle/walk/run、locomotion BlendSpace、action Slot、`MeleeComboAnimations`、`MeleeComboResetSeconds`、blend 时间。
- `FCSItemDefinition` 移除旧 `MeleeAnimationPath`、`MeleeAnimationSlotName`、`EquipmentUpperBodyAnimationPath`、`EquipmentUpperBodyWalkAnimationPath`、`EquipmentUpperBodyRunAnimationPath`、`EquipmentUpperBodySlotName`，新增 `EquipmentAnimProfilePath`。
- `UCSCombatComponent` 攻击动画和动画长度冷却从 `EquipmentAnimProfilePath` 加载，不再从 Item 表直接读取动画路径。
- `Data/Source/Items.csv` 和 `Content/Data/Generated/Items.csv` 已迁移为 `EquipmentAnimProfilePath` 字段；Knife 指向 `/Game/Data/EquipmentAnim/DA_EquipAnim_Knife.DA_EquipAnim_Knife`。
- 新增 `Scripts/CreatePlayerAnimationAssets.py`，可重复创建或更新 `/Game/Blueprints/Characters/BP_PlayerCharacter`、`/Game/Animation/Player/ABP_Player_Body` 与 `/Game/Data/EquipmentAnim/DA_EquipAnim_Knife`。
- `Scripts/ConfigureEquipmentAnimationLayers.py` 的目标已切到 `/Game/Animation/Player/ABP_Player_Body`，用于确认项目自有身体 AnimBP 上存在 `EquipmentUpperBodySlot` 与 `UpperBodyActionSlot`。
- 已创建并保存 `/Game/Blueprints/Characters/BP_PlayerCharacter`，父类为 `ACSPlayerCharacter`。
- 已创建并保存 `/Game/Animation/Player/ABP_Player_Body`，作为当前玩家身体 AnimBP。
- `BP_PlayerCharacter` 的 Mesh AnimClass 已显式保存为 `/Game/Animation/Player/ABP_Player_Body.ABP_Player_Body_C`，避免蓝图组件默认值继续持有旧模板 AnimBP。
- `ACSPlayerCharacter` 已迁移为第三人称模板式操作：移动输入按 Controller Yaw 计算相机相对方向，角色使用 `bOrientRotationToMovement` 朝移动方向转身，`bUseControllerRotationYaw` 关闭；正式 Pawn 仍保持 `BP_PlayerCharacter`，不直接替换成缺少项目 gameplay 组件的模板 `BP_ThirdPersonCharacter`。
- 只读诊断确认 `BP_ThirdPersonCharacter` 的模板默认值为 `bUseControllerRotationYaw=false`、`bOrientRotationToMovement=true`、`RotationRate.Yaw=500`；`BP_PlayerCharacter` 资产曾保留旧覆盖值 `true/false/360`，会覆盖 C++ 构造默认值。本轮已在 `ACSPlayerCharacter::BeginPlay()` 运行时强制应用模板控制参数，并把同样配置写入 `Scripts/CreatePlayerAnimationAssets.py`，用于编辑器关闭后重刷保存蓝图资产默认值。
- `Scripts/CreatePlayerAnimationAssets.py` 写入 `CharacterMovement.RotationRate` 时改为显式设置 `pitch/yaw/roll` 属性，避免 UE Python `unreal.Rotator(...)` 构造参数顺序导致 `Yaw=500` 被写成 `Pitch=500`。
- 已创建并保存 `/Game/Data/EquipmentAnim/DA_EquipAnim_Knife`，配置 Knife 站立/蹲伏持握动画、站立/蹲伏移动动画、三段 light melee combo 和 2.0 秒 combo 重置窗口。
- `Config/DefaultInput.ini` 删除 `ToggleView`。
- `Config/DefaultEngine.ini` 的 `GlobalDefaultGameMode` 改为 `/Script/CoopSurvival.CSGameMode`。
- `/Game/FirstPerson/Lvl_FirstPerson` 的 WorldSettings 已保存为 `CSGameMode`，旧测试地图不再依赖运行时 GameInstance 特判。
- `UCSGameInstance::OverrideGameModeClass()` 移除 `/Game/FirstPerson/Blueprints/BP_FirstPersonGameMode` 特判，只在没有 GameMode 时回退到 `ACSGameMode`。
- `AGENTS.md`、`Docs/GameDesign_v0.1.md`、`Docs/Development/README.md` 已同步第三人称项目方向和 `ACSPlayerCharacter` 命名。
- 2026-06-04 文档口径补齐：`Docs/SystemDesign/UE_EngineDifferences.md` 和 `Docs/SystemDesign/UE_P0ImplementationPlan.md` 不再把当前项目解释为 first-person / FPS arms / viewmodel 路径，统一为第三人称 `ACSPlayerCharacter`、完整角色 Mesh、SpringArm follow camera、第三人称装备动画和第三人称武器 Actor。`Docs/SystemDesign/SD_28_横截面地图美术说明与层级标注规范.md` 的地图/实际画面提示词也从 first-person survival horror 改为 third-person survival horror。`Docs/GameDesign_v0.1.md` 的动画蓝图命名示例移除 `ABP_Player_FPSArms`。本次只改文档，不改变 UE 实现状态。
- 新增 `Source/CoopSurvival/Weapons/CSWeaponPreviewActor.h/.cpp`，作为 editor-only 武器握持预览 Actor；它不参与运行时装备、战斗或复制，只用于作者在预览地图中切换 Idle / Walk / Run / Attack 姿态。
- 新增 `Scripts/CreateWeaponPreviewStage.py`，从 `Items.csv` 和 `DA_EquipAnim_Knife` 生成或更新 `/Game/Blueprints/Weapons/Preview/BP_WeaponPreview_Knife` 与 `/Game/Dev/Maps/M_WeaponPreview`。
- 更新 `Scripts/CreateWeaponBlueprints.py`：如果 `BP_Weapon_Knife` 已存在，脚本只补缺失 Mesh 默认值，不再覆盖 `WeaponMeshComponent`、`HitStartComponent`、`HitEndComponent` 或 `HitRadius` 的手工调参结果。
- 已创建并保存 `/Game/Blueprints/Weapons/Preview/BP_WeaponPreview_Knife`。
- 已创建并保存 `/Game/Dev/Maps/M_WeaponPreview`，包含 `WeaponPreview_Knife`、简单地面、主光和预览相机。
- `Docs/SystemDesign/UE_EquipmentAnimationRetargetWorkflow.md` 已同步为当前第三人称 `ACSPlayerCharacter` / `ABP_Player_Body` / `EquipmentAnimProfilePath` / `BP_Weapon_*` 链路，并登记武器视觉调参流程。
- 新增 `Source/CoopSurvival/Animation/CSAnimNotifyState_MeleeWindow.h/.cpp`，作为正式近战攻击动画伤害窗口通知。
- `UCSCombatComponent` 已改为：配置了攻击动画的近战攻击不再按键瞬间结算伤害，而是在 `UCSAnimNotifyState_MeleeWindow` Begin/Tick/End 窗口中结算。
- `UCSCombatComponent` 已删除 Unit 15 遗留的空手近战伤害和按键瞬间扇形结算 active path。近战左键攻击必须拥有当前装备的近战 `ItemId`、当前手上生成的 `BP_Weapon_*`、有效 `EquipmentAnimProfilePath` 和 `MeleeComboAnimations`；否则服务器不会按近战扣血。Unit 36 起，非近战装备会继续尝试枪械定义。
- `UCSCombatComponent` 播放攻击 Montage 后绑定 Montage End 事件，攻击结束或被打断时调用 `ACSPlayerCharacter::RefreshEquipmentHeldAnimationFromCurrentItem()`，防止攻击 Montage 打断 `EquipmentUpperBodySlot` 后角色回到普通 idle 导致刀看起来消失。
- 近战窗口 Tick 内服务器采样当前 `BP_Weapon_*` 的 `HitStartComponent`、`HitEndComponent` 和 `HitRadius`，对上一帧和当前帧的刀刃 Start / Mid / End 做连续 Sweep，并保证同一次挥砍每个目标只受一次伤害。
- 新增 `Scripts/ConfigureMeleeAnimationNotifies.py`，用于给 `/Game/Animation/Weapons/Knife/AN_Knife_Light_Melee01/02/03` 写入唯一的 `CS_MeleeWindow` 通知轨道和 `UCSAnimNotifyState_MeleeWindow` 窗口。
- `UCSCombatComponent` 攻击动画路径已从单个攻击动画改为 Profile 内 `MeleeComboAnimations`：服务器根据 `SourceItemId`、上次攻击时间和 `MeleeComboResetSeconds` 选择 `ComboIndex`，同武器连续攻击推进 1/2/3 段，超时回到第 1 段，并通过 multicast 同步 `SourceItemId + ComboIndex`。
- `Items.csv` 新增 `MeleeAnimationPlayRate` 字段，作为武器攻击速度配置。`UCSCombatComponent` 会用它播放攻击 Montage，并在 `AnimationLength` / `MaxFixedAndAnimation` 冷却模式下按 `动画长度 / MeleeAnimationPlayRate` 计算动画锁定时间；`MeleeCooldownSeconds` 继续表示两次攻击之间的最小固定间隔。
- Knife 当前初调为 `MeleeAnimationPlayRate=1.55`、`MeleeCooldownSeconds=0.28`、`MeleeCooldownMode=MaxFixedAndAnimation`，比原始 1.0 倍速和 0.45s 固定间隔更快；如果手感仍慢或过快，只改 `Data/Source/Items.csv` 后运行 `python Scripts\GenerateGameData.py`。
- `DA_EquipAnim_Knife.ActionSlotName` 已改为 `DefaultSlot`：持握 idle/walk/run 仍走 `EquipmentUpperBodySlot` 上半身层，三段攻击 Montage 走全身动作槽，让攻击动画本身的下半身运动参与，而不是切换整套 AnimBP。
- `UCSEquipmentAnimProfile` 新增 `UpperBodyCrouchEnterAnimation` 和 `UpperBodyCrouchExitAnimation`，站立到蹲伏、蹲伏到站立不再硬编码在角色代码里，而是跟随 `EquipmentAnimProfilePath` 数据配置。
- `ACSPlayerCharacter` 在 `OnStartCrouch` / `OnEndCrouch` 中优先播放当前装备 Profile 的蹲伏过渡 Montage；过渡结束后事件驱动恢复当前装备持握层。Knife crouch idle 使用 `/Game/Animation/Weapons/Knife/AN_Knife_Cr_Idle_00`，`St_to_Cr` 使用 `/Game/Animation/Weapons/Knife/AN_Knife_St_to_Cr`，`Cr_to_St` 使用 `/Game/Animation/Weapons/Knife/AN_Knife_Cr_to_St`，不靠 Tick 检查姿态。
- `Scripts/RetargetKnifeAnimations.py` 已扩展 Knife 正式输出：新增 `AN_Knife_Light_Melee02`、`AN_Knife_Light_Melee03`、`AN_Knife_Cr_Idle_00`、`AN_Cr_Walk_F/L/R/B`、`AN_Cr_Run_F`、`AN_Knife_St_Death_F`、`AN_Knife_St_to_Cr`、`AN_Knife_Cr_to_St`，并补齐基础蹲伏 Walk/Run 方向动作 `Cr_Walk_45L/45R/135L/135R` 与 `Cr_Run_45L/L/45R/R/B/135L/135R`；右手手指修正覆盖站立 idle 与蹲伏 idle。
- `UCSSurvivalStatsComponent` 新增 `OnHealthChanged` 事件；服务器伤害和客户端 `OnRep_Stats` 都能事件驱动通知表现层，不需要测试假人或 UI 每帧轮询血量。
- `ACSCombatTargetDummy` 从 Cube 靶升级为 `Capsule + Manny SkeletalMesh + SurvivalStats + 持刀 Actor`：碰撞仍用于服务器命中，Mesh 用 Single Node 动画播放 idle / 受击，右手挂 `BP_Weapon_Knife`。
- `Scripts/RetargetKnifeAnimations.py` 已新增正式受击目标动画 `/Game/Animation/Weapons/Knife/AN_Knife_St_Hit_Light_F`，来源为 Knife 包的 `Knife_St_Hit_Light_F`，运行时不直接引用第三方源骨架动画。
- `ACSCombatTargetDummy` 新增死亡状态：血量从正数降到 `0` 时播放 `DeathAnimation`，默认冻结到死亡动画最后一帧；受击回 idle timer 会被清掉，不启用 Tick 轮询。
- `Scripts/RetargetKnifeAnimations.py` 已新增正式死亡目标动画 `/Game/Animation/Weapons/Knife/AN_Knife_St_Death_F`，来源为 Knife 包的 `Knife_St_Death_F`。
- 新增 `Scripts/CreateCombatDummyBlueprint.py`，用于创建或补全 `/Game/Blueprints/World/BP_CombatTargetDummy`；该蓝图首次创建时默认使用 Manny、`AN_Knife_St_Idle_00` idle、`AN_Knife_St_Hit_Light_F` 受击、`AN_Knife_St_Death_F` 死亡、`BP_Weapon_Knife` 持刀和 `180` 测试血量。已有蓝图的 Mesh 姿态、Idle/受击/死亡动画、持刀配置和血量属于作者默认值，脚本不得覆盖。
- `ACSPlayerController::SpawnCombatDummy` 默认优先生成 `/Game/Blueprints/World/BP_CombatTargetDummy`，蓝图缺失时才退回 C++ 基类；当前蓝图已创建，控制台命令会使用蓝图假人。
- 新增 `Scripts/PlaceCombatDummiesInTestMap.py`，用于把正式 `BP_CombatTargetDummy` 场景实例放入 `/Game/FirstPerson/Lvl_FirstPerson`。当前关卡已保存 3 个测试假人：`BP_CombatTargetDummy_Test_01/02/03`，带 `CS_TestCombatDummy` 标记，位置在 PlayerStart 前方扇形区域，用于直接 PIE 验证近战伤害、连击和受击动画。已有测试假人实例的 Actor Transform 属于关卡作者摆放结果，脚本默认只保留，不删除重建；只有显式设置 `CS_REPLACE_TEST_DUMMIES=1` 时才允许替换。

验证步骤：
1. 打开 `/Game/FirstPerson/Lvl_FirstPerson` 或当前 S0/L0 测试地图，启动单人 PIE。
2. 确认生成的是 `BP_PlayerCharacter` / `ACSPlayerCharacter`，默认就是第三人称 SpringArm 相机。
3. 确认本地玩家能看到完整第三人称 Manny Mesh，不再出现 owner-only 第一人称手臂 Mesh。
4. 确认按 `U` 不再切换第一/第三人称视角。
5. 不装备任何武器或切到空槽时，对假人按左键，确认假人不播放受击、不扣血，屏幕 Debug 显示当前装备没有攻击定义。
6. 获得并装备 `Knife`，确认右手生成 `BP_Weapon_Knife`，站立 idle 时播放持刀上半身姿态，第三人称本地和远端都可见同一套武器表现。
7. 连续攻击测试靶，确认攻击动画从 `DA_EquipAnim_Knife.MeleeComboAnimations` 播放，三次连续攻击应依次使用 `AN_Knife_Light_Melee01`、`AN_Knife_Light_Melee02`、`AN_Knife_Light_Melee03`；停顿超过 combo reset 后再次攻击应回到第 1 段。伤害、攻击速度、最小攻击间隔和冷却模式仍由 ItemId 数据驱动。
8. 攻击播放时观察角色下半身：Knife 三段攻击应走 `DefaultSlot` 全身 Montage，攻击动作本身的腿部/重心可参与；非攻击持握和跑步仍是上半身装备层。
9. Listen Server + 1 Client 下切换 1-4 快捷槽，确认装备变化会在本地和远端刷新武器表现。
10. 装备 Knife 后按住蹲伏，确认先播放 `AN_Knife_St_to_Cr` 过渡，随后进入 crouch idle；松开蹲伏后先播放 `AN_Knife_Cr_to_St` 过渡，再回到站立持握 idle，刀不应消失。
11. 打开 `/Game/Dev/Maps/M_WeaponPreview`，选中 `WeaponPreview_Knife`。
12. 在 Details 中切换 `PreviewPose = Idle / Walk / Run / Attack`，确认 Manny 手上显示 `BP_Weapon_Knife`，并能用不同姿态观察握持位置。
13. 打开 `/Game/Blueprints/Weapons/BP_Weapon_Knife`，选中 `WeaponMeshComponent` 调整相对位置/旋转；选中 `HitStartComponent`、`HitEndComponent` 和 `HitRadius` 调整攻击段。保存蓝图后，预览地图和运行时装备都应使用同一套蓝图组件配置。
14. 打开 `/Game/Animation/Weapons/Knife/AN_Knife_Light_Melee01/02/03`，确认每个动画都有 `CS_MeleeWindow` 通知轨道，`Melee Window` 覆盖实际挥刀 active frames。
15. PIE 中装备 Knife 攻击测试靶，确认伤害只在挥刀窗口内发生；打开 `bDrawDebugMeleeTrace` 时应看到短暂的连续刀刃轨迹线/球，而不是只有按键瞬间一条线。
16. 在 `/Game/FirstPerson/Lvl_FirstPerson` 中确认已存在 `BP_CombatTargetDummy_Test_01/02/03` 三个场景放置假人；它们应为 Manny 假人而不是 Cube，右手持有 `BP_Weapon_Knife`，站立时循环播放 Knife idle。
17. 如需额外临时靶，PIE 控制台执行 `SpawnCombatDummy`，确认生成的同样是 `BP_CombatTargetDummy` 蓝图假人。
18. 装备 Knife 攻击 `BP_CombatTargetDummy`，确认每次有效命中时假人播放 `AN_Knife_St_Hit_Light_F` 受击动画，随后回到 idle；远离或没扫到时不播放受击。
19. 连续攻击同一个假人直到血量为 0，确认假人播放 `AN_Knife_St_Death_F` 死亡动画，并停在最后一帧；死亡后继续命中不应再次播放受击或回 idle。
20. 在 `BP_CombatTargetDummy` 默认值中调整 `TestHealth`，重新 PIE 后确认死亡所需命中次数跟随蓝图默认血量变化。

验证：
- 2026-06-03：`python Scripts\GenerateGameData.py` 成功，生成表同步到 `Content/Data/Generated`。
- 2026-06-03：`CoopSurvival Win64 Development` 编译通过。
- 2026-06-03：`CoopSurvivalEditor Win64 Development` 编译通过。
- 2026-06-03：修复 `Scripts\CreatePlayerAnimationAssets.py` 缩进错误后，`python -m py_compile Scripts\CreatePlayerAnimationAssets.py Scripts\ConfigureEquipmentAnimationLayers.py` 通过。
- 2026-06-03：`UnrealEditor-Cmd -run=pythonscript -script=Scripts\CreatePlayerAnimationAssets.py` 成功，创建/确认并保存 `BP_PlayerCharacter`、`ABP_Player_Body`、`DA_EquipAnim_Knife` 和 `/Game/FirstPerson/Lvl_FirstPerson` 的 `CSGameMode` WorldSettings。
- 2026-06-03：`Scripts\CreatePlayerAnimationAssets.py` 已确认并保存 `BP_PlayerCharacter.Mesh.AnimClass = /Game/Animation/Player/ABP_Player_Body.ABP_Player_Body_C`。
- 2026-06-03：`UnrealEditor-Cmd -run=pythonscript -script=Scripts\ConfigureEquipmentAnimationLayers.py` 在 `ABP_Player_Body` 上成功，确认 `EquipmentUpperBodySlot` 与 `UpperBodyActionSlot` 已配置；0 error / 0 warning。
- 2026-06-03：移除旧 FirstPerson GameMode 运行时特判后，`CoopSurvival Win64 Development` 编译通过。
- 2026-06-03：移除旧 FirstPerson GameMode 运行时特判后，`CoopSurvivalEditor Win64 Development` 编译通过。
- 2026-06-03：角色默认 AnimBP 改为 `ABP_Player_Body` 后，`CoopSurvival Win64 Development` 编译通过。
- 2026-06-03：角色默认 AnimBP 改为 `ABP_Player_Body` 后，`CoopSurvivalEditor Win64 Development` 编译通过。
- 2026-06-03：`python -m py_compile Scripts\CreateWeaponPreviewStage.py Scripts\CreateWeaponBlueprints.py` 通过。
- 2026-06-03：新增 `ACSWeaponPreviewActor` 后，`CoopSurvival Win64 Development` 编译通过。
- 2026-06-03：当前 GUI Unreal Editor 因 Live Coding 拦截外部 Editor 编译；已通过正常关闭窗口关闭 Editor，没有强制结束进程。
- 2026-06-03：关闭 Editor 后，`CoopSurvivalEditor Win64 Development` 编译通过。
- 2026-06-03：`python Scripts\GenerateGameData.py` 成功。
- 2026-06-03：`UnrealEditor-Cmd -run=pythonscript -script=Scripts\CreateWeaponBlueprints.py` 成功，日志确认 `BP_Weapon_Knife (created=False, preserves existing transforms)`。
- 2026-06-03：`UnrealEditor-Cmd -run=pythonscript -script=Scripts\CreateWeaponPreviewStage.py` 成功，创建/确认 `/Game/Blueprints/Weapons/Preview/BP_WeaponPreview_Knife` 和 `/Game/Dev/Maps/M_WeaponPreview`。
- 2026-06-03：文件验证通过：`Content\Blueprints\Weapons\Preview\BP_WeaponPreview_Knife.uasset`、`Content\Dev\Maps\M_WeaponPreview.umap` 和 `Content\Blueprints\Weapons\BP_Weapon_Knife.uasset` 均存在。
- 2026-06-03：新增近战动画通知窗口和多帧武器轨迹采样后，`python -m py_compile Scripts\ConfigureMeleeAnimationNotifies.py` 通过。
- 2026-06-03：新增近战动画通知窗口和多帧武器轨迹采样后，`CoopSurvival Win64 Development` 编译通过。
- 2026-06-03：用户关闭 GUI Editor 后，`CoopSurvivalEditor Win64 Development` 编译通过。
- 2026-06-03：`Scripts\ConfigureMeleeAnimationNotifies.py` 修正为使用 UE 5.8 当前 Python 导出的 `unreal.AnimationLibrary`，`python -m py_compile` 通过。
- 2026-06-03：`UnrealEditor-Cmd -run=pythonscript -script=Scripts\ConfigureMeleeAnimationNotifies.py` 成功，已保存 `/Game/Animation/Weapons/Knife/AN_Knife_Light_Melee01`，日志确认 `CS_MeleeWindow 0.180s - 0.600s` 且 `Verified CS_MeleeWindow: 1 notify state window`。
- 2026-06-03：只读诊断确认 `/Game/Blueprints/Weapons/BP_Weapon_Knife` 已保存用户调好的 `WeaponMeshComponent` 位置 `(-4, 2, -5)`、旋转 `(Roll=19, Pitch=-8, Yaw=104)`，Mesh 可见且未隐藏；同时确认 `BP_PlayerCharacter` EventGraph 没有装备/蒙太奇节点，根因是持刀 idle 没有被播放，角色回到普通 idle 后刀落到身体侧面/被遮挡。
- 2026-06-03：`ACSPlayerCharacter` 增加事件驱动装备持握 idle 播放/停止逻辑，`UCSCombatComponent` 增加攻击 Montage End 后恢复当前装备持握层逻辑，`CoopSurvival Win64 Development` 编译通过。
- 2026-06-03：同次 `CoopSurvivalEditor Win64 Development` 首次因当前 GUI Unreal Editor Live Coding 仍处于活动状态被 UBT 拦截；确认无 `UnrealEditor` 进程后重跑，`CoopSurvivalEditor Win64 Development` 编译通过。
- 2026-06-03：`python -m py_compile Scripts\RetargetKnifeAnimations.py Scripts\CreatePlayerAnimationAssets.py Scripts\ConfigureMeleeAnimationNotifies.py Scripts\CreateWeaponPreviewStage.py` 通过。
- 2026-06-03：扩展 `UCSEquipmentAnimProfile`、`UCSCombatComponent` 和 `ACSPlayerCharacter` 为三段 Knife combo + crouch 持握刷新后，`CoopSurvivalEditor Win64 Development` 编译通过。
- 2026-06-03：`UnrealEditor-Cmd -run=pythonscript -script=Scripts\RetargetKnifeAnimations.py` 成功，日志确认 `Completed knife retarget: 29 asset(s)`；`AN_Knife_Light_Melee02/03`、`AN_Knife_Cr_Idle_00`、`AN_Cr_Walk_F/L/R/B` 和 `AN_Cr_Run_F` 已输出到 `/Game/Animation/Weapons/Knife/`，站立/蹲伏 idle 均完成右手手指修正。
- 2026-06-03：`UnrealEditor-Cmd -run=pythonscript -script=Scripts\CreatePlayerAnimationAssets.py` 成功，日志确认已保存 `/Game/Data/EquipmentAnim/DA_EquipAnim_Knife`。
- 2026-06-03：`UnrealEditor-Cmd -run=pythonscript -script=Scripts\ConfigureMeleeAnimationNotifies.py` 成功，日志确认三段攻击窗口：`AN_Knife_Light_Melee01 0.180s-0.600s`、`AN_Knife_Light_Melee02 0.160s-0.580s`、`AN_Knife_Light_Melee03 0.180s-0.640s`，每段均 `Verified CS_MeleeWindow: 1 notify state window`。
- 2026-06-03：`UnrealEditor-Cmd -run=pythonscript -script=Scripts\CreateWeaponPreviewStage.py` 成功，日志确认 `BP_WeaponPreview_Knife` 和 `/Game/Dev/Maps/M_WeaponPreview` 已更新；仅有 EditorLevelLibrary 弃用警告。
- 2026-06-03：三连击和 crouch 持握改动后，`CoopSurvival Win64 Development` 编译通过。
- 2026-06-03：攻击 Montage 的 BlendIn / BlendOut 时间改为从 `UCSEquipmentAnimProfile` 读取；最终 `CoopSurvival Win64 Development` 和 `CoopSurvivalEditor Win64 Development` 均编译通过。
- 2026-06-03：按第三人称模板操作方式迁移 `ACSPlayerCharacter` 后，`UnrealEditor-Cmd -run=pythonscript -script=Scripts\CreatePlayerAnimationAssets.py` 成功，日志确认 `BP_PlayerCharacter.Mesh.AnimClass = /Game/Animation/Player/ABP_Player_Body.ABP_Player_Body_C`；`Scripts\ConfigureEquipmentAnimationLayers.py` 成功，确认 `EquipmentUpperBodySlot` 与 `UpperBodyActionSlot` 已存在。
- 2026-06-03：按第三人称模板操作方式迁移 `ACSPlayerCharacter` 后，`CoopSurvival Win64 Development` 编译通过；`CoopSurvivalEditor Win64 Development` 外部编译被当前运行中的 `UnrealEditor.exe` / `LiveCodingConsole.exe` 拦截，需关闭编辑器/游戏或在编辑器内按 `Ctrl+Alt+F11` 后重跑。
- 2026-06-03：用户反馈操作方式不对后，只读诊断发现 `BP_PlayerCharacter` CDO 仍覆盖旧移动参数；已把模板控制参数改为运行时强制应用，并扩展 `Scripts\CreatePlayerAnimationAssets.py` 保存蓝图默认值。当前因 GUI Editor 锁住 `Content/Blueprints/Characters/BP_PlayerCharacter.uasset`，脚本保存资产失败；`CoopSurvival Win64 Development` 编译通过，关闭 Editor 后需重跑脚本和 `CoopSurvivalEditor Win64 Development`。
- 2026-06-03：用户关闭 Editor 后，`UnrealEditor-Cmd -run=pythonscript -script=Scripts\CreatePlayerAnimationAssets.py` 成功保存 `BP_PlayerCharacter`；只读验证确认 `use_controller_rotation_yaw=false`、`orient_rotation_to_movement=true`、`use_controller_desired_rotation=false`、`rotation_rate_yaw=500`，且 `anim_class=/Game/Animation/Player/ABP_Player_Body.ABP_Player_Body_C`。
- 2026-06-03：第三人称模板控制默认值保存并读回验证后，`CoopSurvivalEditor Win64 Development` 编译通过；仅有 `Plugins/VibeUE` 的 UE 5.8 API 弃用警告。
- 2026-06-03：新增 `UCSSurvivalStatsComponent::OnHealthChanged` 和 Manny 持刀假人后，`python -m py_compile Scripts\RetargetKnifeAnimations.py Scripts\CreateCombatDummyBlueprint.py` 通过。
- 2026-06-03：Manny 持刀假人 C++ 改动后，`CoopSurvival Win64 Development` 编译通过。
- 2026-06-03：同轮首次 `CoopSurvivalEditor Win64 Development` 被当前 GUI Editor Live Coding 拦截；已正常关闭 UnrealEditor 窗口，没有强制结束进程；确认无 UnrealEditor / LiveCodingConsole 进程后重跑，`CoopSurvivalEditor Win64 Development` 编译通过。
- 2026-06-03：以 `CS_KNIFE_RETARGET_OVERWRITE=0` 运行 `Scripts\RetargetKnifeAnimations.py` 成功，只生成缺失的 `/Game/Animation/Weapons/Knife/AN_Knife_St_Hit_Light_F`，保留已有三段攻击动画和 `CS_MeleeWindow` 通知窗口；日志确认 `Completed knife retarget: 1 asset(s)`。
- 2026-06-03：`Scripts\CreateCombatDummyBlueprint.py` 成功创建并保存 `/Game/Blueprints/World/BP_CombatTargetDummy`；再次运行同一脚本成功更新该蓝图，最终为 `0 error / 0 warning`。
- 2026-06-03：`python -m py_compile Scripts\PlaceCombatDummiesInTestMap.py` 通过。
- 2026-06-03：用户要求直接在场景中放置假人后，正常关闭 GUI Unreal Editor 以避免关卡保存冲突；`UnrealEditor-Cmd -run=pythonscript -script=Scripts\PlaceCombatDummiesInTestMap.py` 成功，已保存 `/Game/FirstPerson/Lvl_FirstPerson` 和 3 个 World Partition External Actor 包；日志确认 `Placed 3 combat dummies in /Game/FirstPerson/Lvl_FirstPerson; removed existing=0`。本次仅有 UE Python `EditorLevelLibrary` 弃用提示，无保存错误。
- 2026-06-03：修复空槽/空手左键仍能造成伤害的问题：删除 `UCSCombatComponent` 中 Unit 15 遗留的空手伤害和按键瞬间扇形命中 active path，服务器现在要求当前装备近战 ItemId、当前武器 Actor 命中组件和装备动画 Profile 三者同时有效才允许攻击。
- 2026-06-03：空手伤害修复后，`CoopSurvival Win64 Development` 编译通过。
- 2026-06-03：空手伤害修复后，`CoopSurvivalEditor Win64 Development` 编译通过。
- 2026-06-03：新增 `MeleeAnimationPlayRate` 表字段并将 Knife 调整为 `MeleeAnimationPlayRate=1.55`、`MeleeCooldownSeconds=0.28` 后，`python -m py_compile Scripts\GenerateGameData.py` 通过，`python Scripts\GenerateGameData.py` 成功同步 `Content/Data/Generated/Items.csv`。
- 2026-06-03：近战攻击速度配置接入服务器冷却和 Montage 播放后，`CoopSurvival Win64 Development` 编译通过。
- 2026-06-03：近战攻击速度配置接入服务器冷却和 Montage 播放后，`CoopSurvivalEditor Win64 Development` 编译通过。
- 2026-06-03：用户指出测试假人姿态被脚本覆盖后，修正 `Scripts\CreateCombatDummyBlueprint.py` 和 `Scripts\PlaceCombatDummiesInTestMap.py`：已有 `BP_CombatTargetDummy` 不再被重写 Mesh 姿态、Idle/受击动画或持刀默认值；已有 `CS_TestCombatDummy` 场景实例不再被删除重建。`python -m py_compile Scripts\CreateCombatDummyBlueprint.py Scripts\PlaceCombatDummiesInTestMap.py` 通过。
- 2026-06-03：按用户要求让 Knife 攻击动画使用下半身源动作：`Scripts\CreatePlayerAnimationAssets.py` 已把 `DA_EquipAnim_Knife.ActionSlotName` 写为 `DefaultSlot`，持握/移动上半身层保持不变。
- 2026-06-03：`ACSCombatTargetDummy` 新增 `DeathAnimation`、`bFreezeDeathOnFinalFrame`、`DeathFinalFrameHoldOffsetSeconds` 和死亡状态；血量降到 0 时事件驱动播放死亡动画并停最后一帧，不增加 Tick。
- 2026-06-03：以 `CS_KNIFE_RETARGET_OVERWRITE=0` 运行 `Scripts\RetargetKnifeAnimations.py` 成功生成 `/Game/Animation/Weapons/Knife/AN_Knife_St_Death_F`；日志为 `0 error`，仅有 UE Python/API 弃用和无动画曲线警告。
- 2026-06-03：`Scripts\CreatePlayerAnimationAssets.py` 成功保存 `DA_EquipAnim_Knife`；`Scripts\CreateCombatDummyBlueprint.py` 成功补全 `/Game/Blueprints/World/BP_CombatTargetDummy` 的死亡动画默认值，保存时出现一次 `MoveFile Error Code 32` 重试并自动恢复，最终 `0 error`。
- 2026-06-03：`python -m py_compile Scripts\RetargetKnifeAnimations.py Scripts\CreateCombatDummyBlueprint.py Scripts\CreatePlayerAnimationAssets.py` 通过。
- 2026-06-03：假人死亡和 Knife 全身攻击槽 C++ 改动后，`CoopSurvival Win64 Development` 编译通过。
- 2026-06-03：假人死亡和 Knife 全身攻击槽 C++ 改动后，`CoopSurvivalEditor Win64 Development` 编译通过。
- 2026-06-03：新增装备蹲伏过渡数据字段和 `ACSPlayerCharacter` 姿态事件播放逻辑后，`python -m py_compile Scripts\RetargetKnifeAnimations.py Scripts\CreatePlayerAnimationAssets.py` 通过。
- 2026-06-03：同轮 `CoopSurvival Win64 Development` 编译通过。
- 2026-06-03：同轮 `CoopSurvivalEditor Win64 Development` 编译通过；仅有 `Plugins/VibeUE` 的 UE 5.8 API 弃用警告。
- 2026-06-03：以 `CS_KNIFE_RETARGET_OVERWRITE=0` 运行 `Scripts\RetargetKnifeAnimations.py` 成功，日志确认 `Completed knife retarget: 13 asset(s)`；新增项目目标骨架资产包括 `AN_Knife_St_to_Cr`、`AN_Knife_Cr_to_St`、蹲伏 Walk 斜向/后斜向和蹲伏 Run 方向动作。命令行仅有 UE Python/API 弃用、无动画曲线和动画压缩依赖加载警告，0 error。
- 2026-06-03：`Scripts\CreatePlayerAnimationAssets.py` 成功保存 `/Game/Data/EquipmentAnim/DA_EquipAnim_Knife`，Knife Profile 已绑定 `UpperBodyCrouchEnterAnimation = AN_Knife_St_to_Cr`、`UpperBodyCrouchExitAnimation = AN_Knife_Cr_to_St`；地图检查 `Lvl_FirstPerson` 为 0 错误、0 警告。

用户验证结果：
2026-06-03 通过。用户确认角色动作、持刀表现和第三人称模板式操作方式已接近预期，可以进入下个单元。

## Unit 26：Search-B 容器内容 UI

目标：
- `BP_LootContainer` 交互后不再直接把全部物品塞进背包，而是打开容器内容 UI。
- UI 同时显示当前容器剩余内容和玩家背包内容。
- 容器内物品的拾取按钮通过玩家控制器发起服务器请求，由服务器验证容器、距离、状态和物品数量后转移。
- 多人同时打开同一容器时，服务器仍保证同一堆物品只能被转移一次。

非目标：
- 本单元不做完整格子背包、物品形状、旋转、拖拽摆放或重量容量限制。
- 本单元不做 LootTable 随机生成；容器仍使用当前 `InitialItems` / `RemainingItems`。
- 本单元不做正式 UMG 美术；沿用当前 Slate 原型面板。
- 本单元不做锁定容器、搜索时间、搜索噪音、污染风险或样本槽。

实现记录：
- `ACSLootContainer::Interact_Implementation()` 改为服务器生成/确认容器内容后，通过 `ACSPlayerController::ClientOpenLootContainer()` 打开本地容器 UI，不再交互瞬间全量转入背包。
- `ACSLootContainer` 新增 `TryTakeItem()`，服务器端按 `ItemId + Quantity` 从 `RemainingItems` 转移到玩家背包，并在转移后刷新容器复制状态和占位视觉。
- `ACSPlayerController` 新增打开容器面板的客户端 RPC、当前打开容器缓存、`RequestTakeLootContainerItem()` 和 `ServerTakeLootContainerItem()`；服务器会验证控制 Pawn、容器 Actor、距离和物品状态后才调用容器转移。
- `FCSHUDSnapshot` 新增容器面板状态、容器名称、`PersistentId` 和当前容器物品列表。
- `SCSInventoryPanelWidget` 右侧新增容器内容面板；工作台打开时显示合成面板，容器打开时显示容器内容，背包左侧保持现有装备/丢弃操作。
- 容器行提供 `Take 1` 和 `Take All`，按钮只发送请求，不直接修改客户端背包或容器数据。

验证步骤：
1. 在测试地图中放置 `/Game/Blueprints/Interactables/BP_LootContainer`，保持默认 `InitialItems`。
2. PIE 单人靠近容器按 `E`，应打开库存面板，右侧显示容器名称和容器物品，左侧显示玩家背包。
3. 点击容器物品的 `Take 1` 或 `Take All` 后，服务器把对应数量转入玩家背包，容器剩余数量减少，空容器变暗。
4. 关闭面板后再次看向空容器，不应再显示可搜索提示。
5. Listen Server + 1 Client 同时打开同一容器并点击拾取，同一物品只应被一个请求成功转移，不应复制。
6. 搜索后运行 `SaveCoop`，重开或恢复后运行 `LoadCoop`，容器剩余内容应保持转移后的状态。

验证：
- 2026-06-03：`CoopSurvival Win64 Development` 编译通过。
- 2026-06-03：`CoopSurvivalEditor Win64 Development` 外部编译被当前运行中的 `UnrealEditor.exe` / `LiveCodingConsole.exe` 拦截，提示需关闭编辑器/游戏或在编辑器内按 `Ctrl+Alt+F11`；这是 Live Coding 锁，不是代码编译错误。
- 2026-06-03：用户关闭 Editor / Live Coding 后，`CoopSurvivalEditor Win64 Development` 编译通过。

用户验证结果：
2026-06-03 通过。用户确认容器内容 UI 首版差不多通过，可进入下一单元。

## Unit 27：Search-C LootTable 容器数据

目标：
- `BP_LootContainer` 运行时内容不再由 C++ 硬编码或 `InitialItems` 提供，而是按 `ContainerId` 查询容器数据。
- 容器数据从 `Data/Source/Containers.csv` 生成到 `Content/Data/Generated/Containers.csv`。
- LootTable 条目从 `Data/Source/LootTableEntries.csv` 生成到 `Content/Data/Generated/LootTableEntries.csv`。
- 容器生成只允许普通随机物资；关键物、样本、路线权限、AIM 协议等不能通过随机 LootTable 复制。
- 同一个 `PersistentId + ContainerId` 使用稳定随机种子，未保存前重开也能得到稳定结果；保存后仍以存档中的 `RemainingItems` 为准。

非目标：
- 本单元不做格子背包、拖拽、旋转、重量、特殊样本槽或完整战术搜刮 UI。
- 本单元不做锁定容器、搜索时间、搜索噪音、污染风险、稀有度权重或动态刷新。
- 本单元不创建最终容器 Mesh，美术仍可使用当前 `BP_LootContainer` 占位视觉。

实现记录：
- 新增 `FCSContainerDefinition` 和 `FCSLootTableEntry` 数据结构。
- `UCSItemDataSubsystem` 新增容器表、LootTable 表读取接口，以及随机掉落关键物过滤接口。
- `Scripts/GenerateGameData.py` 新增 `Containers.csv` / `LootTableEntries.csv` 校验与生成：检查容器 ID 唯一、LootTable 引用存在、ItemId 存在、数量范围合法、概率在 `0.0-1.0` 内，并拒绝关键物类型进入随机掉落表。
- 新增 `Data/Source/Containers.csv`，当前 `BasicLocker -> LT_BasicLocker`。
- 新增 `Data/Source/LootTableEntries.csv`，当前 `LT_BasicLocker` 产出 `Wood` 和 `Stone` 普通材料。
- `ACSLootContainer` 移除运行时硬编码 `InitialItems` active path；生成时按 `ContainerId -> LootTableId -> entries` 产生 `RemainingItems`。
- `ACSLootContainer` 在 `BeginPlay()` 读取容器定义的显示名，客户端提示和服务器打开 UI 使用同一份数据。
- 容器数据缺失或 LootTable 为空时记录 warning 并生成空容器，不使用硬编码保底物品掩盖配置错误。

验证步骤：
1. 运行 `python Scripts\GenerateGameData.py`，确认生成 `Content/Data/Generated/Containers.csv` 和 `Content/Data/Generated/LootTableEntries.csv`。
2. 在测试地图中放置 `/Game/Blueprints/Interactables/BP_LootContainer`，保持 `ContainerId = BasicLocker`，设置唯一 `PersistentId`。
3. PIE 单人靠近容器按 `E`，右侧容器面板应显示 `Storage Locker` 和由 `LT_BasicLocker` 生成的 `Wood` / `Stone`。
4. 点击 `Take 1` / `Take All` 后，服务器转移对应物品，容器剩余数量减少或清空。
5. 重开未保存 PIE，同一 `PersistentId + ContainerId` 的数量结果应保持稳定。
6. 搜索后运行 `SaveCoop`，重开或恢复后运行 `LoadCoop`，容器剩余内容应以存档状态为准，不重新随机。
7. Listen Server + 1 Client 同时打开同一容器并点击拾取，同一物品仍只能被一个请求成功转移。

验证：
- 2026-06-03：`python -m py_compile Scripts\GenerateGameData.py` 通过。
- 2026-06-03：`python Scripts\GenerateGameData.py` 成功，生成 `Containers.csv` 和 `LootTableEntries.csv` 到 `Content/Data/Generated`。
- 2026-06-03：`CoopSurvival Win64 Development` 编译通过。
- 2026-06-03：`CoopSurvivalEditor Win64 Development` 外部编译被当前运行中的 `UnrealEditor.exe` / `LiveCodingConsole.exe` 拦截，提示需关闭编辑器/游戏或在编辑器内按 `Ctrl+Alt+F11`；这是 Live Coding 锁，不是代码编译错误。
- 2026-06-03：用户关闭 Editor / Live Coding 后，`CoopSurvivalEditor Win64 Development` 编译通过；仅有 `Plugins/VibeUE` 的 UE 5.8 API 弃用警告。

用户验证结果：
2026-06-04 通过。用户确认 Unit 27 的 LootTable 容器数据流程已在编辑器验证通过。

## Unit 28：Save-A 存档结构整理

目标：
- 把当前 SaveGame 结构整理为清晰的 `PlayerSave`、`WorldProgressSave`、`WorldActorSave` 三类归属。
- 保留当前已可用的玩家位置、背包、装备、掉落物、工作台和容器状态保存读取行为。
- 为后续任务世界状态、门禁/路线权限、电梯当前层、SCP 样本归档、AIM 解锁、AI/Boss 状态预留稳定结构。
- 保留离线玩家记录和版本迁移入口，不因当前在线玩家列表覆盖旧玩家存档。

非目标：
- 本单元不实现 Objective 系统、门禁系统、AIM 协议、Boss 存档或正式 UI。
- 本单元不改变当前 `SaveCoop` / `LoadCoop` 控制台使用方式。
- 本单元不做破坏性旧存档迁移；如需 schema 升级，必须记录版本号和默认迁移规则。

实现记录：
2026-06-04：`UCSWorldSaveGame` 结构升级：
- `CurrentSaveVersion` 从 `4` 升到 `5`。
- 新增 `FCSPlayerSaveDataGroup`，当前保存在线和离线玩家记录，字段为 `PlayerSaves.Players`。
- 新增 `FCSWorldProgressSaveData`，预留 Objective、路线、SCP 样本归档、AIM 协议、配方解锁、S0 配给点数和 S0 共享材料库存。
- 新增 `FCSWorldActorSaveData`，当前保存世界掉落物、工作台和可搜索容器状态，字段为 `WorldActors.WorldItems`、`WorldActors.WorkbenchStations`、`WorldActors.LootContainers`。
- 旧的平铺 `Players`、`WorldItems`、`WorkbenchStations`、`LootContainers` 只保留在 `UCSWorldSaveGame` 内作为 `SaveVersion <= 4` 迁移字段，不再作为运行时主读写路径。

2026-06-04：新增 `UCSWorldSaveGame::MigrateToCurrentVersion()`：
- 拒绝 `SaveVersion <= 0` 或高于当前版本的存档。
- 旧版本存档读取时，把旧平铺玩家、掉落物、工作台和容器字段迁移到新的分组字段。
- 迁移后把 `SaveVersion` 写为当前版本。

2026-06-04：`ACSPlayerController::SaveWorldOnServer()` / `LoadWorldOnServer()` 改为分组 schema：
- `SaveCoop` 控制台命令不变。
- 保存前如果主槽位 `CoopSurvival_DevWorld` 已存在，先写入备份槽位 `CoopSurvival_DevWorld_Backup`；备份失败则不覆盖主存档。
- 保存时只更新当前在线玩家记录，同时保留已存在的离线玩家记录。
- 保存时保留已有 `WorldProgress`，避免后续任务/归档/路线解锁在同一存档结构中被覆盖。
- 读取时只从迁移后的 `PlayerSaves` 和 `WorldActors` 读取，确保新旧存档进入同一条加载路径。

验证步骤：
1. 打开 `/Game/FirstPerson/Lvl_FirstPerson`，PIE 单人或 Listen Server + 1 Client。
2. 给玩家拾取资源、装备物品，并在场景里留下至少一个世界掉落物。
3. 搜索一个 `BP_LootContainer`，拿走部分或全部内容。
4. 可选：靠近工作台执行 `SetNearestWorkbenchEnabled 0`，让工作台状态发生变化。
5. 执行 `SaveCoop`，确认 Debug 显示保存成功，并包含 players/items/workbenches/containers 数量。
6. 改变玩家位置、背包、掉落物或容器状态。
7. 执行 `LoadCoop`，确认玩家位置、背包、装备、世界掉落物、工作台和容器剩余内容恢复到保存状态。
8. 再次执行 `SaveCoop`，确认不会因为当前在线玩家覆盖掉旧玩家记录；多人测试时至少确认已连接玩家能保存并恢复。
9. 如本机已有旧 `SaveVersion <= 4` 存档，确认第一次 `LoadCoop` 能读取旧数据；再次 `SaveCoop` 后主槽位应写为 `SaveVersion 5`，并保留 `CoopSurvival_DevWorld_Backup` 备份槽。

验证：
2026-06-04：运行目标编译成功：

```powershell
& 'F:\Unreal\UE_5.8\Engine\Build\BatchFiles\Build.bat' CoopSurvival Win64 Development -Project='F:\Unreal\CoopSurvival\CoopSurvival.uproject' -NoHotReload -NoLiveCoding -NoUBA -NoXGE -NoUBTMakefiles -UBARootDir='F:\Unreal\CoopSurvival\Saved\UnrealBuildTool\UBA'
```

2026-06-04：Editor 目标编译成功：

```powershell
& 'F:\Unreal\UE_5.8\Engine\Build\BatchFiles\Build.bat' CoopSurvivalEditor Win64 Development -Project='F:\Unreal\CoopSurvival\CoopSurvival.uproject' -NoHotReload -NoLiveCoding -NoUBA -NoXGE -NoUBTMakefiles -UBARootDir='F:\Unreal\CoopSurvival\Saved\UnrealBuildTool\UBA'
```

用户验证结果：
2026-06-04 通过。用户确认 Unit 28 的 `SaveCoop` / `LoadCoop` 存档结构整理流程已在编辑器验证通过。

## Unit 29：Objective-A 世界状态核心

目标：
- 建立项目内 `ObjectiveId` / `WorldState` / `ObjectiveEvent` 基础结构。
- 世界进度由服务器写入 `ACSGameState`，并复制给客户端。
- `SaveCoop` / `LoadCoop` 能保存和恢复已完成 Objective、已应用事件和 WorldState 标记。
- 为后续门禁、电梯路线、SCP 归档、AIM 协议和配方解锁提供统一世界状态入口。

非目标：
- 本单元不实现任务 UI、任务列表、S0 状态图或正式 UMG。
- 本单元不把门、电梯、归档台、AIM 协议或 Boss 绑定到世界状态；这些留给 Objective-B/C 和后续单元。
- 本单元不做复杂目标条件 DataAsset，只建立 C++ 基础事件和存档路径。

实现记录：
2026-06-04：新增 `Source/CoopSurvival/World/CSObjectiveTypes.h`：
- `ECSObjectiveState` 定义 `Locked`、`Discovered`、`Active`、`Blocked`、`Completed`、`FailedRecoverable`、`Archived`。
- `FCSObjectiveStateSaveData` 保存单个 Objective 当前状态和最后事件。
- `FCSObjectiveEvent` 表示一次服务器事件，可携带 `ObjectiveId`、前置 `WorldStateTags`、新增 `WorldStateTags`、路线解锁、样本归档、AIM 协议解锁和配方解锁。

2026-06-04：扩展 `FCSWorldProgressSaveData` 并把 `UCSWorldSaveGame::CurrentSaveVersion` 从 `5` 升到 `6`：
- 新增 `ObjectiveStates`、`WorldStateTags`、`ObjectiveEventIds`。
- 旧存档迁移时，如果已有 `CompletedObjectiveIds`，会补齐对应 `ObjectiveStates` 的 `Completed` 状态。

2026-06-04：扩展 `ACSGameState`：
- 新增复制字段 `WorldProgress`，作为服务器权威世界进度状态。
- 新增 `ApplyObjectiveEvent()`、`CompleteObjective()`、`AddWorldStateTag()`、`HasWorldStateTag()`、`IsObjectiveCompleted()`。
- 新增 `OnWorldProgressChanged` 委托，后续 UI、门禁、电梯和归档系统可事件驱动监听，不需要 Tick 轮询。

2026-06-04：扩展 `ACSPlayerController` 验证命令：
- `CompleteObjective <ObjectiveId>`：客户端执行会 RPC 到服务器，由服务器把 Objective 标记为完成。
- `SetWorldStateTag <WorldStateTag>`：客户端执行会 RPC 到服务器，由服务器写入世界状态标记。
- `PrintWorldProgress`：输出当前复制到本地的世界进度计数。
- `SaveCoop` 现在从 `ACSGameState::WorldProgress` 写入存档；`LoadCoop` 会把存档中的 `WorldProgress` 恢复到 `ACSGameState`。

验证步骤：
1. 打开 `/Game/FirstPerson/Lvl_FirstPerson`，PIE 单人或 Listen Server + 1 Client。
2. 在控制台执行 `PrintWorldProgress`，应看到 Objectives/Events/WorldTags 等计数。
3. 执行 `CompleteObjective L0_TestRestorePower`，应看到 Objective completed Debug，Objectives 和 Events 计数增加。
4. 执行 `SetWorldStateTag S0_MainLiftRouteOnline`，应看到 WorldState set Debug，WorldTags 和 Events 计数增加。
5. 执行 `SaveCoop`。
6. 关闭 PIE 或改变世界状态后重新进入，执行 `LoadCoop`。
7. 执行 `PrintWorldProgress`，确认 `L0_TestRestorePower` 和 `S0_MainLiftRouteOnline` 仍在计数内，Load Debug 里 objectives/events 数量不为 0。
8. Listen Server + Client 下，在 Client 执行 `CompleteObjective ClientSideTestObjective`，确认仍由服务器写入，另一个窗口能通过 `PrintWorldProgress` 看到复制后的计数。

验证：
2026-06-04：运行目标编译成功：

```powershell
& 'F:\Unreal\UE_5.8\Engine\Build\BatchFiles\Build.bat' CoopSurvival Win64 Development -Project='F:\Unreal\CoopSurvival\CoopSurvival.uproject' -NoHotReload -NoLiveCoding -NoUBA -NoXGE -NoUBTMakefiles -UBARootDir='F:\Unreal\CoopSurvival\Saved\UnrealBuildTool\UBA'
```

2026-06-04：Editor 目标编译成功：

```powershell
& 'F:\Unreal\UE_5.8\Engine\Build\BatchFiles\Build.bat' CoopSurvivalEditor Win64 Development -Project='F:\Unreal\CoopSurvival\CoopSurvival.uproject' -NoHotReload -NoLiveCoding -NoUBA -NoXGE -NoUBTMakefiles -UBARootDir='F:\Unreal\CoopSurvival\Saved\UnrealBuildTool\UBA'
```

用户验证结果：
2026-06-04 通过。用户确认可以进入下一步。

## Unit 30：Objective-B 门禁/路线绑定

目标：
- 把 S0/L0 测试电梯的目标层可用性接入 `ACSGameState::WorldProgress`。
- 服务器在电梯交互时验证目标层路线是否已解锁。
- 未解锁时不启动移动或 Streaming，并向玩家显示拒绝原因。
- 保持返回 S0 不需要额外权限，避免测试流程中把玩家困在 L0。

非目标：
- 本单元不做正式门 Actor、不做完整层级注册表、不做正式 UMG 门禁界面。
- 本单元不扩展到 L1-L7，也不改变当前 S0/L0 Streaming 地图结构。
- 本单元不保存电梯当前位置；电梯状态存档留给后续 Elevator 单元。

实现记录：
2026-06-04：扩展 `ACSGameState`：
- 新增 `UnlockRoute(FName RouteId, FName EventId)`，通过 `FCSObjectiveEvent.UnlockRouteIds` 写入世界进度。
- 新增 `IsRouteUnlocked(FName RouteId)`，供门禁、电梯和后续路线 Actor 查询。

2026-06-04：扩展 `ACSPlayerController` 验证命令：
- `UnlockRoute <RouteId>`：客户端执行会 RPC 到服务器，由服务器写入 `WorldProgress.UnlockedRouteIds`。
- `PrintWorldProgress` 可继续查看 Routes 计数。

2026-06-04：扩展 `ACSStreamingElevatorStation`：
- 新增 `bRequireWorldProgressAccess`、`RequiredRouteIdToS0`、`RequiredRouteIdToL0`、`RequiredWorldStateTagsToS0`、`RequiredWorldStateTagsToL0`。
- 默认 `RequiredRouteIdToL0 = Route_S0_L0_MainElevator`，`RequiredRouteIdToS0 = None`。
- `Interact` 和 `CanUseElevator` 现在都会检查目标层权限；失败时输出 `Elevator locked: route ... required` 或缺世界状态原因。
- `CanInteract` 保持只检查距离和电梯空闲，让锁定状态下仍能显示锁定提示，而不是从交互候选中消失。

2026-06-04：增强 `ACSStreamingElevatorStation` 测试可视化：
- 同一个电梯 Actor 内新增黄色导向边框、四根立柱、顶部横梁、信标和 `S0 / L0 ELEVATOR` 文本标识。
- 视觉仍属于电梯 Actor 的内容表现，不新增备用电梯或替代交互逻辑。
- `OnConstruction` 会在 Editor 里刷新电梯外观，便于打开 Persistent Map 后直接定位。

验证步骤：
1. 打开当前 S0/L0 Persistent Streaming 测试地图，PIE 单人或 Listen Server + 1 Client。
2. 在未解锁路线前靠近 S0 电梯，应看到锁定提示或按 `E` 后收到 `Route_S0_L0_MainElevator` 需求提示。
3. 控制台执行 `UnlockRoute Route_S0_L0_MainElevator`。
4. 再看向电梯，提示应变为前往 L0；按 `E` 后电梯移动并切到 L0。
5. 在 L0 再次使用电梯，应能返回 S0，不需要额外路线。
6. 执行 `SaveCoop`，重开或重新进入后执行 `LoadCoop` 和 `PrintWorldProgress`，确认 Routes 计数保留，S0 到 L0 仍可用。
7. Listen Server + Client 下，在客户端执行 `UnlockRoute Route_S0_L0_MainElevator`，确认由服务器写入，另一个窗口能看到路线解锁后的电梯可用性。

验证：
2026-06-04：运行目标编译成功：

```powershell
& 'F:\Unreal\UE_5.8\Engine\Build\BatchFiles\Build.bat' CoopSurvival Win64 Development -Project='F:\Unreal\CoopSurvival\CoopSurvival.uproject' -NoHotReload -NoLiveCoding -NoUBA -NoXGE -NoUBTMakefiles -UBARootDir='F:\Unreal\CoopSurvival\Saved\UnrealBuildTool\UBA'
```

2026-06-04：电梯视觉增强后，运行目标再次编译成功：

```powershell
& 'F:\Unreal\UE_5.8\Engine\Build\BatchFiles\Build.bat' CoopSurvival Win64 Development -Project='F:\Unreal\CoopSurvival\CoopSurvival.uproject' -NoHotReload -NoLiveCoding -NoUBA -NoXGE -NoUBTMakefiles -UBARootDir='F:\Unreal\CoopSurvival\Saved\UnrealBuildTool\UBA'
```

2026-06-04：Editor 目标编译成功：

```powershell
& 'F:\Unreal\UE_5.8\Engine\Build\BatchFiles\Build.bat' CoopSurvivalEditor Win64 Development -Project='F:\Unreal\CoopSurvival\CoopSurvival.uproject' -NoHotReload -NoLiveCoding -NoUBA -NoXGE -NoUBTMakefiles -UBARootDir='F:\Unreal\CoopSurvival\Saved\UnrealBuildTool\UBA'
```

2026-06-04：电梯视觉增强后，Editor 目标外部编译被当前打开的 Unreal Editor / Live Coding 拦截，提示需关闭 Editor/游戏或在 Editor 内按 `Ctrl+Alt+F11` 后重跑；这是 Live Coding 锁，不是代码编译错误。

用户验证结果：
2026-06-04 通过。用户确认 S0/L0 电梯路线锁定、`UnlockRoute Route_S0_L0_MainElevator` 解锁和电梯切层测试通过。

## Unit 31：GM-A 调试命令面板

目标：
- 把当前需要手输控制台的常用验证命令收敛到一个项目内 GM 面板。
- 面板由本地 `PlayerController` 打开，但按钮仍调用现有服务器权威入口或项目服务接口。
- 带参数命令提供输入框，常用测试命令提供一键按钮。

非目标：
- 本单元不做正式 CommonUI / MVVM 重构。
- 本单元不做权限账号、线上 GM 权限、安全审计或发布版禁用策略；这些留给后续平台/发布单元。
- 本单元不新增绕过服务器逻辑的调试入口。

实现记录：
2026-06-04：新增 `Source/CoopSurvival/UI/SCSGMPanelWidget.h/.cpp`：
- `F10` 打开 GM 面板，`F10` 或 `Esc` 关闭；避免和 UE 编辑器 `F4` 视口显示模式快捷键冲突。
- `World Progress` 区显示当前 `ACSGameState::GetWorldProgressDebugString()`。
- 一键按钮：`PrintWorldProgress`、`UnlockRoute Route_S0_L0_MainElevator`、`SaveCoop`、`LoadCoop`。
- 参数输入：`UnlockRoute <RouteId>`、`SetWorldStateTag <Tag>`、`CompleteObjective <ObjectiveId>`。
- 背包/制作：`CraftItem <RecipeId>`、`Craft Axe`、`DropItem <ItemId> <Qty>`、最近工作台开关。
- 联机/测试：`HostCoop`、`FindCoop`、`JoinCoop <Index>`、`DestroyCoopSession`、`SpawnCombatDummy`。

2026-06-04：扩展 `ACSPlayerController`：
- 新增 `ToggleGMPanel()`、`CloseGMPanel()`、`CreateGMPanel()`、`RemoveGMPanel()`。
- `SetupInputComponent()` 绑定 `F10`。
- `FCSHUDSnapshot` 增加 `bGMPanelOpen`，GM 面板打开时隐藏普通 HUD，避免 UI 叠加。
- 交互检测改为使用目标 Actor Bounds 中心点做候选距离和视线目标，默认交互距离调到 500，避免贴地电梯平台被地面/原点判定挡掉提示。

2026-06-04：按用户反馈修正：
- 原 `F4` 热键会触发 UE 编辑器视口显示模式，导致灯光/材质显示变化；GM 面板热键改为 `F10`。
- `SCSGMPanelWidget` 关闭提示同步改为 `F10 / Esc`。
- `UCSInteractionComponent` 的候选距离、扇形方向和视线目标改用 Actor Bounds 中心点，不再只瞄准 Actor 原点；这修复电梯地台类 Actor 因原点贴地导致没有交互提示的问题。
- 默认交互距离从 `350` 调整为 `500`，和电梯使用半径更接近。
- `ApplyMenuInputMode()` 把 GM 面板纳入鼠标和焦点管理。
- `CloseOpenPanels()` 优先关闭 GM 面板。

验证步骤：
1. 打开任意当前测试图并 PIE。
2. 按 `F10`，应打开 `GM Panel`，鼠标可用；按 `F4` 不再用于 GM 面板。
3. 点击 `Unlock S0-L0`，应等价于控制台执行 `UnlockRoute Route_S0_L0_MainElevator`。
4. 点击 `Print`，应输出当前世界进度计数。
5. 点击 `Save` / `Load`，应触发当前 `SaveCoop` / `LoadCoop` 流程。
6. 修改 `RouteId` 输入框后点击 `Unlock Route`，应按输入的 RouteId 写入世界进度。
7. 按 `Esc` 或 `F10`，面板关闭并恢复游戏输入。

验证：
2026-06-04：运行目标编译成功：

```powershell
& 'F:\Unreal\UE_5.8\Engine\Build\BatchFiles\Build.bat' CoopSurvival Win64 Development -Project='F:\Unreal\CoopSurvival\CoopSurvival.uproject' -NoHotReload -NoLiveCoding -NoUBA -NoXGE -NoUBTMakefiles -UBARootDir='F:\Unreal\CoopSurvival\Saved\UnrealBuildTool\UBA'
```

2026-06-04：Editor 目标编译成功：

```powershell
& 'F:\Unreal\UE_5.8\Engine\Build\BatchFiles\Build.bat' CoopSurvivalEditor Win64 Development -Project='F:\Unreal\CoopSurvival\CoopSurvival.uproject' -NoHotReload -NoLiveCoding -NoUBA -NoXGE -NoUBTMakefiles -UBARootDir='F:\Unreal\CoopSurvival\Saved\UnrealBuildTool\UBA'
```

2026-06-04：`F10` 热键和电梯交互提示修正后，运行目标编译成功：

```powershell
& 'F:\Unreal\UE_5.8\Engine\Build\BatchFiles\Build.bat' CoopSurvival Win64 Development -Project='F:\Unreal\CoopSurvival\CoopSurvival.uproject' -NoHotReload -NoLiveCoding -NoUBA -NoXGE -NoUBTMakefiles -UBARootDir='F:\Unreal\CoopSurvival\Saved\UnrealBuildTool\UBA'
```

2026-06-04：同次 `CoopSurvivalEditor Win64 Development` 外部编译被当前打开的 Unreal Editor / Live Coding 拦截，提示需关闭 Editor/游戏或在 Editor 内按 `Ctrl+Alt+F11` 后重跑；这是 Live Coding 锁，不是代码编译错误。

用户验证结果：
2026-06-04 通过。用户确认 `F10` GM 面板、电梯交互提示和路线解锁测试通过。

## Unit 32：Elevator-A 层级注册与 GM 调试

目标：
- 电梯切层逻辑不再硬编码 `S0Level` / `L0Level` / `RequiredRouteIdToL0` 这类双字段。
- 使用同一个 `LayerDefinitions` 注册表配置 `LayerId`、显示名、Streaming Level、目标电梯位置、所需路线和所需世界状态。
- GM 面板显示当前最近电梯的层级注册状态，并提供一键解锁当前目标层所需路线，方便 S0/L0/L1 后续扩展测试。

非目标：
- 本单元不做正式玩家电梯目的地 UI、过渡 UI、声音、门动画或多人集合确认。
- 本单元不接入电梯当前层存档；电梯运行时层级仍由当前 Actor 复制，读档恢复留给 Elevator-E。
- 本单元不扩展正式 L1-L7 内容，只让电梯逻辑具备继续添加层级表项的结构。

实现记录：
2026-06-04：重构 `ACSStreamingElevatorStation`：
- 新增 `FCSElevatorLayerDefinition`，包含 `LayerId`、`DisplayName`、`StreamingLevel`、`ElevatorLocation`、`RequiredRouteId`、`RequiredWorldStateTags`。
- 电梯当前层、目标层、上一层改为复制 `FName LayerId`，不再使用 S0/L0 枚举。
- `GetNextLayerId()` 按 `LayerDefinitions` 顺序选择下一层；当前 S0/L0 测试仍表现为两层往返。
- `LoadLayer()` / `UnloadLayer()`、交互提示、权限验证、移动目标位置、标签文本都从 `LayerDefinitions` 查询。
- `EnsureStreamingState()` 会加载当前层并卸载注册表中其他层，避免以后 L1+ 时继续写成双层分支。

2026-06-04：扩展 `SCSGMPanelWidget`：
- 新增 `Elevator / Layers` 区。
- 显示最近 `ACSStreamingElevatorStation::GetLayerRegistryDebugString()`。
- `Print Layers` 输出当前层、目标层、下一层、移动状态和每层 Level/Route。
- `Unlock Target Route` 读取当前目标层的 `RequiredRouteId` 并调用现有 `UnlockRoute` 服务器入口；如果目标层不需要路线，则只输出提示，不写入无效路线。

验证步骤：
1. 打开 `/Game/Maps/Site/Lvl_Site_Persistent_Test`，PIE。
2. 按 `F10` 打开 GM 面板。
3. 在 `Elevator / Layers` 区应看到 `Current=S0`、`Next=L0`，并能看到 S0/L0 的 Streaming Level 路径。
4. 未解锁时看向电梯，应仍显示 `Route_S0_L0_MainElevator` 锁定原因。
5. 在 GM 面板点击 `Unlock Target Route`，应等价于解锁当前目标层的 `Route_S0_L0_MainElevator`。
6. 再看向电梯，提示应变为前往 L0；按 `E` 后电梯移动并切换 Streaming。
7. 到达 L0 后再次打开 GM 面板，应看到 `Current=L0`、`Next=S0`；点击 `Unlock Target Route` 应提示 S0 目标不需要路线。

验证：
2026-06-04：运行目标编译成功：

```powershell
& 'F:\Unreal\UE_5.8\Engine\Build\BatchFiles\Build.bat' CoopSurvival Win64 Development -Project='F:\Unreal\CoopSurvival\CoopSurvival.uproject' -NoHotReload -NoLiveCoding -NoUBA -NoXGE -NoUBTMakefiles -UBARootDir='F:\Unreal\CoopSurvival\Saved\UnrealBuildTool\UBA'
```

2026-06-04：Editor 目标外部编译被当前打开的 Unreal Editor / Live Coding 拦截，提示需关闭 Editor/游戏或在 Editor 内按 `Ctrl+Alt+F11` 后重跑；这是 Live Coding 锁，不是代码编译错误。

用户验证结果：
2026-06-04 通过。用户确认当前层级注册表和 GM 面板电梯调试流程基本可用。

## Unit 33：Elevator-B 过渡 UI 与输入限制

目标：
- 电梯锁定失败、开始移动、到达完成不再依赖 Debug Log 作为玩家主反馈。
- HUD 顶部显示最小电梯状态条，展示电梯锁定原因、移动方向和到达层。
- 电梯开始切层时关闭打开的菜单，并临时限制本地移动输入；到达后解除。
- 继续使用服务器权威电梯流程；客户端 UI 只显示状态，不决定切层结果。

非目标：
- 本单元不做正式 CommonUI / MVVM 重构。
- 本单元不做目的地选择界面、多人集合确认、声音、门动画、加载进度条或关卡过场。
- 本单元不把过渡 UI 状态写入存档；它只是当前运行时表现。

实现记录：
2026-06-04：扩展 `FCSHUDSnapshot` 和 `SCSHUDWidget`：
- 新增 `bHasElevatorStatus`、`ElevatorStatusTitle`、`ElevatorStatusMessage`。
- HUD 顶部中间新增电梯状态条，避免和中心交互提示、左上生存条、底部快捷栏重叠。

2026-06-04：扩展 `ACSPlayerController`：
- 新增 `ClientShowElevatorStatus(Title, Message, Duration)`，由服务器通知本地 HUD 状态。
- 新增 `ClientSetElevatorTransitionLock(bool)`，切层期间关闭当前菜单并限制移动输入，到达后解除。
- 电梯状态使用本地 `FTimerHandle` 清理；`Duration <= 0` 表示保持显示直到下一次状态覆盖。

2026-06-04：扩展 `ACSStreamingElevatorStation`：
- 目标层未解锁时只给当前交互玩家显示 `Elevator Locked` 和具体原因。
- 开始移动时向当前世界所有玩家广播 `Elevator Transit` 和 `S0 -> L0` / `L0 -> S0`。
- 到达后向当前世界所有玩家广播 `Arrived` 和当前层。
- 电梯自己的玩家反馈不再走 `ClientDebugLog`；通用交互组件的失败 Debug 输出仍保留为其他系统既有调试路径。

2026-06-04：修复用户反馈“电梯下去了但没看到地下层”：
- 原因：`Scripts/CreateS0L0TestMaps.py` 创建的 L0 灰盒 Actor 仍在本地 `Z≈0` 附近，而电梯目标位置是 `Z=-1200`；即使 L0 Streaming 成功，玩家也会在 L0 内容下方，看不到地下层。
- `FCSElevatorLayerDefinition` 新增 `StreamingLevelTransform`。
- L0 默认 `StreamingLevelTransform = (0, 0, -1200)`，和 L0 电梯目标高度一致。
- 加载层级时直接设置 `ULevelStreaming::LevelTransform`、`ShouldBeLoaded`、`ShouldBeVisible`，目标层加载使用阻塞刷新，避免电梯移动早于层级可见。
- 如果某个层级没有显式 `StreamingLevelTransform`，但 `ElevatorLocation.Z` 不为 0，则按该 Z 作为层级竖向偏移，保证已摆放的旧电梯实例不用手动重建。
- GM 面板的 Layer Debug 会显示每层 `Offset=(...)`，用于确认 L0 是否在地下。

2026-06-04：继续修复用户反馈“还是没有加载”：
- 原因补充：当前 Persistent Map 资产时间早于最新层级注册改动，且 Editor 目标外部编译被 Live Coding 锁住；如果 Persistent Map 没有把 S0/L0 子关卡注册成运行时 `ULevelStreaming`，旧 `LoadLayer` 只会打印未注册警告并返回。
- `ACSStreamingElevatorStation::LoadLayer` 现在优先使用 Persistent Map 已注册的 `ULevelStreaming`；如果找不到，则按 `LayerDefinitions.StreamingLevel` 通过 `ULevelStreamingDynamic::LoadLevelInstanceBySoftObjectPtr` 创建动态层级实例。
- 动态实例同样使用 `GetLayerStreamingTransform()`，因此 L0 会以 `Z=-1200` 偏移进入地下。
- `UnloadLayer` 同时处理注册层和动态实例，避免留下重复可见层。
- 动态层级实例名带运行时自增序号，避免来回切层时 UE 因同名动态实例仍在卸载队列里而拒绝再次加载。
- 日志会输出 `[CoopElevator] Dynamically loading layer ...`，用于确认当前走的是动态层级加载路径。

验证步骤：
1. 打开 `/Game/Maps/Site/Lvl_Site_Persistent_Test`，PIE。
2. 未解锁路线时靠近 S0 电梯按 `E`，HUD 顶部应显示 `Elevator Locked` 和 `Route_S0_L0_MainElevator` 需求原因。
3. 按 `F10` 用 GM 面板点击 `Unlock Target Route`，关闭面板。
4. 再按 `E` 使用电梯，HUD 顶部应显示 `Elevator Transit` 和 `S0 -> L0`，并在移动期间限制移动输入。
5. 到达 L0 后，HUD 顶部应显示 `Arrived` 和 `Current layer: L0`，约 2 秒后消失，移动输入恢复；同时应能看到 L0 灰盒内容位于地下空间。
6. Listen Server + Client 下使用电梯，所有当前玩家都应看到移动/到达状态；锁定失败只提示触发交互的玩家。

验证：
2026-06-04：运行目标编译成功：

```powershell
& 'F:\Unreal\UE_5.8\Engine\Build\BatchFiles\Build.bat' CoopSurvival Win64 Development -Project='F:\Unreal\CoopSurvival\CoopSurvival.uproject' -NoHotReload -NoLiveCoding -NoUBA -NoXGE -NoUBTMakefiles -UBARootDir='F:\Unreal\CoopSurvival\Saved\UnrealBuildTool\UBA'
```

2026-06-04：Editor 目标外部编译被当前打开的 Unreal Editor / Live Coding 拦截，提示需关闭 Editor/游戏或在 Editor 内按 `Ctrl+Alt+F11` 后重跑；这是 Live Coding 锁，不是代码编译错误。

2026-06-04：L0 Streaming 竖向偏移修复后，运行目标编译成功：

```powershell
& 'F:\Unreal\UE_5.8\Engine\Build\BatchFiles\Build.bat' CoopSurvival Win64 Development -Project='F:\Unreal\CoopSurvival\CoopSurvival.uproject' -NoHotReload -NoLiveCoding -NoUBA -NoXGE -NoUBTMakefiles -UBARootDir='F:\Unreal\CoopSurvival\Saved\UnrealBuildTool\UBA'
```

2026-06-04：同次 Editor 目标外部编译仍被当前打开的 Unreal Editor / Live Coding 拦截；这是 Live Coding 锁，不是代码编译错误。

2026-06-04：补齐未注册 Streaming Level 时的动态加载路径后，运行目标编译成功：

```powershell
& 'F:\Unreal\UE_5.8\Engine\Build\BatchFiles\Build.bat' CoopSurvival Win64 Development -Project='F:\Unreal\CoopSurvival\CoopSurvival.uproject' -NoHotReload -NoLiveCoding -NoUBA -NoXGE -NoUBTMakefiles -UBARootDir='F:\Unreal\CoopSurvival\Saved\UnrealBuildTool\UBA'
```

2026-06-04：同次 Editor 目标外部编译仍被当前打开的 Unreal Editor / Live Coding 拦截，提示 `Unable to build while Live Coding is active`。测试本修复前需要关闭并重开 Unreal Editor，或在 Editor 内按 `Ctrl+Alt+F11` 编译，使 PIE 加载最新 Editor 模块。

用户验证结果：
待验证。用户已确认电梯过渡 UI 与输入限制流程可用；随后发现 L0 内容没有出现在地下，已修复 Streaming 竖向偏移，并补齐未注册 Streaming Level 时的动态加载路径。等待用户在重载 Editor 后再次验证。

## Unit 34：Equipment-A 装备上半身 Locomotion Profile

目标：
- 修复装备 Knife 跑步时上半身仍停在持刀 idle 的问题。
- 装备动画不按单把武器堆角色蓝图，而是按 `UCSEquipmentAnimProfile` 复用动作族。
- Knife 仍按 `Idle / Walk / Run / CrouchIdle / CrouchWalk / CrouchRun` 切换上半身 Slot 序列。
- 建立 `DA_EquipAnim_Pistol` 和 `DA_EquipAnim_Shotgun` 作为数据契约；枪械上半身 locomotion / aim pose 不直接硬接进 `ABP_Player_Body` 主图，后续必须通过正式装备 Anim Layer / Linked Anim Layer 单元实现。
- 数据表新增可装备测试物品 `Pistol` 和 `Shotgun`，用于验证 Profile 切换；本单元不实现枪械射击规则。

非目标：
- 本单元不实现射击、弹药、换弹、散布、后坐力、命中、枪声或枪械网络同步。
- 本单元不创建最终手枪/霰弹枪武器 Mesh 或握持调参蓝图。
- 本单元不把第三方 UE4 骨骼动作直接接到 gameplay 数据；如果后续使用第三方枪械动作，必须先走 IK Retarget 输出到项目 Manny 动作路径。

实现记录：
- `ACSPlayerCharacter` 新增装备持握姿态状态：`StandingIdle`、`StandingWalk`、`StandingRun`、`CrouchIdle`、`CrouchWalk`、`CrouchRun`。
- `ACSPlayerCharacter::Tick()` 只在姿态状态发生变化时刷新 Knife / 序列型 Profile 的上半身 Slot 动画，不每帧重播 Montage。
- 冲刺、蹲伏、装备切换、攻击 Montage 结束后都会刷新当前装备姿态；带 `UpperBodyLocomotionBlendSpace` 的枪械 Profile 不再通过动态 Slot Montage 循环播放 walk / jog。
- 蹲伏过渡 Montage 播放期间会暂停 Tick 触发的 Knife / 序列型持握姿态刷新，避免过渡动作被下一帧打断。
- `UCSEquipmentAnimProfile` 新增 `EquipmentAnimSet` 和 `UpperBodyAimOffsetBlendSpace`，作为后续正式装备 Anim Layer 的数据契约；当前 `ABP_Player_Body` active graph 不直接读取 Pistol / LongGun BlendSpace 或 AimOffset。
- `UCSPlayerAnimInstance` 暴露 `EquipmentMoveDirection`、`EquipmentMoveSpeed`、`AimOffsetYaw`、`AimOffsetPitch`、`bUseEquipmentUpperBodyBlendSpace`、`bUseLongGunEquipmentAnimSet`，运行时从角色本地速度、BaseAimRotation 和当前装备 Profile 计算动画输入；这些输入不会在空装备时驱动任何枪械姿态。
- `Scripts/CreatePlayerAnimationAssets.py` 仍可生成 Pistol / Rifle 上半身 Locomotion BlendSpace 与 `DA_EquipAnim_Pistol`、`DA_EquipAnim_Shotgun` 数据资产，但不负责把枪械节点接进角色身体 AnimBP。
- 用户复测后确认默认空手姿态仍像 Shotgun；最终定位为 `ABP_Player_Body` 主图被脚本拼入了枪械 BlendSpace / AimOffset 节点，而不是单纯运行时变量没清空。
- 已撤销 `Scripts/ConfigureEquipmentAnimationLayers.py` 的枪械 BlendSpace / AimOffset 主图生成逻辑；该脚本现在只负责在 `ABP_Player_Body` 上创建 `EquipmentUpperBodySlot`、`UpperBodyActionSlot` 和对应 `LayeredBoneBlend`。
- 已删除旧的 `/Game/Animation/Player/ABP_Player_Body` 生成资产，并由 `Scripts/CreatePlayerAnimationAssets.py` 从干净的 `/Game/Characters/Mannequins/Anims/Unarmed/ABP_Unarmed` 重新生成，再运行 `Scripts/ConfigureEquipmentAnimationLayers.py` 只接回通用装备 Slot。
- `ACSPlayerCharacter` 在空装备状态也会执行装备表现清理，并显式停止 `EquipmentUpperBodySlot` 与 `UpperBodyActionSlot`，防止旧动态 Montage 残留。
- `UCSPlayerAnimInstance` 新增空装备保护：当 `ACSPlayerState::EquippedItemId` 为空时，强制把 `EquipmentAnimSet` 视为 `None`，避免 Profile 输入残留把空手状态解释成枪械状态。
- `Scripts/CreatePlayerAnimationAssets.py` 新增 `DA_EquipAnim_Pistol` 和 `DA_EquipAnim_Shotgun` 创建/更新逻辑。
- `DA_EquipAnim_Knife.ActionSlotName` 统一改为 `UpperBodyActionSlot`，匹配 `ABP_Player_Body` 的装备动作层。
- `Data/Source/Items.csv` 新增 `Pistol` 和 `Shotgun` 两条可装备测试数据，分别指向对应 `EquipmentAnimProfilePath`。
- `python Scripts\GenerateGameData.py` 已通过，`Content/Data/Generated/Items.csv` 已包含 `Pistol` 和 `Shotgun`。
- GM 面板新增 `Give ItemId + Qty` 调试入口，调用 `ACSPlayerController::GiveItem()`，由服务器验证 `ItemId` 存在后写入玩家背包。
- `ACSPlayerController::GiveItem()` 同时作为控制台 `Exec` 命令可用，例如 `GiveItem Pistol 1`、`GiveItem Shotgun 1`。

验证步骤：
1. PIE 中装备 `Knife`，站立、走路、冲刺、蹲伏移动时，上半身应分别切到 Knife profile 内的 idle/walk/run/crouch 动作。
2. 未装备武器或切到空快捷栏时，上半身应回到普通第三人称身体 locomotion，不应继续播放 Shotgun / LongGun 持握或 AimOffset。
3. 按 `F10` 打开 GM 面板，在 `Give ItemId` 输入 `Pistol`、`Qty=1`，点击 `Give`；按 `B` 打开背包装备 `Pistol`，确认装备状态、快捷栏和武器 Actor 切换正常，但不要把当前单位作为枪械上半身 aim locomotion 的验收依据。
4. 按 `F10` 打开 GM 面板，在 `Give ItemId` 输入 `Shotgun`、`Qty=1`，点击 `Give`；按 `B` 打开背包装备 `Shotgun`，确认装备状态、快捷栏和武器 Actor 切换正常，且未装备/空快捷栏时不再保持 Shotgun / LongGun 上半身姿态。
5. 装备 Knife 攻击后，攻击 Montage 结束应回到当前移动状态对应的 Knife 上半身动作，不应固定回 idle。
6. Listen Server + Client 下切换 Knife / Pistol / Shotgun / 空快捷栏，远端玩家也应看到同一套第三人称装备上半身表现。

验证：
- 2026-06-04：`python -m py_compile Scripts\CreatePlayerAnimationAssets.py Scripts\GenerateGameData.py` 通过。
- 2026-06-04：`python Scripts\GenerateGameData.py` 成功。
- 2026-06-04：用户关闭 Editor / Live Coding 后，`UnrealEditor-Cmd -run=pythonscript -script=Scripts\CreatePlayerAnimationAssets.py` 成功；`DA_EquipAnim_Pistol` 和 `DA_EquipAnim_Shotgun` 已创建，`DA_EquipAnim_Knife` 已更新。
- 2026-06-04：文件验证通过：`Content\Data\EquipmentAnim\DA_EquipAnim_Pistol.uasset`、`Content\Data\EquipmentAnim\DA_EquipAnim_Shotgun.uasset` 和 `Content\Data\EquipmentAnim\DA_EquipAnim_Knife.uasset` 均存在。
- 2026-06-04：`CoopSurvival Win64 Development` 编译通过。
- 2026-06-04：`CoopSurvivalEditor Win64 Development` 编译通过；仅有 `Plugins/VibeUE` 的 UE 5.8 API 弃用警告。
- 2026-06-04：新增 GM `Give Item` 后，`CoopSurvival Win64 Development` 编译通过。
- 2026-06-04：同次 `CoopSurvivalEditor Win64 Development` 外部编译被当前运行中的 `UnrealEditor.exe` / `LiveCodingConsole.exe` 拦截，提示需关闭编辑器/游戏或在编辑器内按 `Ctrl+Alt+F11`；这是 Live Coding 锁，不是代码编译错误。
- 2026-06-04：用户关闭 Editor / Live Coding 后，`CoopSurvivalEditor Win64 Development` 编译通过。
- 2026-06-04：`python -m py_compile Scripts\CreatePlayerAnimationAssets.py Scripts\ConfigureEquipmentAnimationLayers.py` 通过。
- 2026-06-04：`UnrealEditor-Cmd -run=pythonscript -script=Scripts\CreatePlayerAnimationAssets.py` 成功；Pistol / Rifle 上半身 Locomotion BlendSpace 已按动作名缩写生成，Pistol / Shotgun Profile 已写入 BlendSpace 与 AimOffset。
- 2026-06-04：`UnrealEditor-Cmd -run=pythonscript -script=Scripts\ConfigureEquipmentAnimationLayers.py` 成功；二次运行确认幂等，没有重复添加 AnimGraph 节点。
- 2026-06-04：历史尝试曾通过 UE Python 验证 `ABP_Player_Body` 已包含 2 个枪械 BlendSpacePlayer、2 个 AimOffset、2 个 BlendByBool 和 5 个变量 Getter；用户复测证明该主图拼接路径会污染默认空手姿态，后续已撤销，不再作为当前 active path。
- 2026-06-04：`CoopSurvivalEditor Win64 Development` 与 `CoopSurvival Win64 Development` 均已编译通过。
- 2026-06-04：修复用户发现的“未装备武器仍像 Shotgun 动作、动作切换不生效”问题；`Scripts\ConfigureEquipmentAnimationLayers.py` 改为重连已有枪械层，`ACSPlayerCharacter` 增加空装备清理保护。
- 2026-06-04：`python -m py_compile Scripts\ConfigureEquipmentAnimationLayers.py Scripts\CreatePlayerAnimationAssets.py` 通过。
- 2026-06-04：`UnrealEditor-Cmd -run=pythonscript -script=Scripts\CreatePlayerAnimationAssets.py` 成功。
- 2026-06-04：`UnrealEditor-Cmd -run=pythonscript -script=Scripts\ConfigureEquipmentAnimationLayers.py` 成功。
- 2026-06-04：`CoopSurvival Win64 Development` 编译通过。
- 2026-06-04：`CoopSurvivalEditor Win64 Development` 编译通过；仅有 `Plugins/VibeUE` 的 UE 5.8 API 弃用警告。
- 2026-06-04：针对用户澄清的“没有装备枪械时默认上半身仍是 Shotgun 姿态”，重新生成 `ABP_Player_Body`，再运行 `Scripts\ConfigureEquipmentAnimationLayers.py`，命令成功且 0 error / 0 warning。
- 2026-06-04：重新生成 `ABP_Player_Body` 后，UE 命令行验证不再出现 `Failed to find /Game/Animation/Player/ABP_Player_Body.ABP_Player_Body_C`；`CoopSurvivalEditor Win64 Development` 编译通过，目标为最新状态。
- 2026-06-04：根据用户指出“动画蓝图默认动作就是用枪的，蓝图拼错了”，删除并重建 `/Game/Animation/Player/ABP_Player_Body`；新的资产从干净 `ABP_Unarmed` 复制，`Scripts\ConfigureEquipmentAnimationLayers.py` 只写入 `DefaultSlot`、`EquipmentUpperBodySlot`、`UpperBodyActionSlot` 相关通用层，不再写入 Pistol / Rifle / AimOffset 节点。
- 2026-06-04：UE 命令行读取 `ABP_Player_Body.AnimGraph` 验证通过：只发现 `DefaultSlot`、`EquipmentUpperBodySlot`、`UpperBodyActionSlot`，未发现 Pistol / Rifle / BlendSpacePlayer / AimOffset 节点。
- 2026-06-04：`python -m py_compile Scripts\CreatePlayerAnimationAssets.py Scripts\ConfigureEquipmentAnimationLayers.py` 通过。
- 2026-06-04：`CoopSurvivalEditor Win64 Development` 编译通过。

用户验证结果：
待验证。

## Unit 35：Combat-A 数据稳定化

目标：
- 把当前 `Items.csv` 中混在一起的通用物品、装备表现和近战战斗字段拆开。
- 背包、合成、快捷栏和装备仍只以 `ItemId` 作为身份。
- 运行时通过 `UCSItemDataSubsystem` 查询三类静态定义：通用物品、装备定义、近战武器定义。
- 保持 Knife 当前三连击、动画通知窗口、多帧刀刃 Sweep、体力消耗和冷却行为不变，只改变数据归属。

非目标：
- 本单元不实现 Buff、属性组件、护甲、枪械、弹药、耐久消耗落地、装备实例 `ItemInstanceId` 或正式格子背包。
- 本单元不重做武器蓝图、动画 Profile、假人、UI 或关卡内容。
- 本单元不覆盖 `BP_Weapon_Knife` 中手动调好的握持位置、命中段或 `HitRadius`。

实现记录：
- `Data/Source/Items.csv` 现在只保留 `ItemId`、显示名、类型、堆叠、可丢弃、可装备、图标和 `Weight`。
- 新增 `Data/Source/EquipmentDefinitions.csv`，按 `ItemId` 配置装备槽、装备 Actor、动画 Profile、默认 Mesh 来源、Socket、基础 Transform、耐久和装备标签。
- 新增 `Data/Source/MeleeWeaponDefinitions.csv`，按 `ItemId` 配置近战伤害、伤害类型、冷却、动画播放倍率、体力消耗、耐久消耗占位、噪音半径和命中 Buff 占位。
- `Scripts/GenerateGameData.py` 新增装备表和近战表校验，并把三张表同步到 `Content/Data/Generated`。
- 所有 `Data/Source/*.csv` 源表顶部新增 `#CN` 中文属性名行和 `#DESC` 字段用途说明行；机器字段名仍保留在第三行。
- `Scripts/GenerateGameData.py` 会跳过 `#CN` / `#DESC` / 其他 `#` 注释行，生成到 `Content/Data/Generated` 的运行时表只包含英文机器字段名和数据行。
- `Scripts/CreateWeaponBlueprints.py` 和 `Scripts/CreateWeaponPreviewStage.py` 也会跳过源表说明行，避免编辑器辅助工具误读中文表头。
- `UCSItemDataSubsystem` 新增 `GetEquipmentDefinition()` 和 `GetMeleeWeaponDefinition()`，并加载 `EquipmentDefinitions.csv`、`MeleeWeaponDefinitions.csv`。
- `FCSItemDefinition` 已移除装备表现和近战字段，新增 `Weight`；新增 `FCSItemEquipmentDefinition` 和 `FCSMeleeWeaponDefinition`。
- `ACSPlayerCharacter` 装备表现改为读取 `FCSItemEquipmentDefinition`。
- `ACSWeaponActor` 初始化改为 `InitializeFromEquipmentDefinition()`；仍只在蓝图默认 Mesh 为空时用装备定义补 Mesh，不覆盖蓝图手动调参。
- `UCSCombatComponent` 近战攻击规格改为从 `FCSMeleeWeaponDefinition` 读取数值，从 `FCSItemEquipmentDefinition` 读取武器 Actor 和动画 Profile。
- `Scripts/CreateWeaponBlueprints.py` 和 `Scripts/CreateWeaponPreviewStage.py` 改为读取 `EquipmentDefinitions.csv`。

验证步骤：
1. 运行 `python Scripts\GenerateGameData.py`，应生成 `Items.csv`、`EquipmentDefinitions.csv`、`MeleeWeaponDefinitions.csv` 到 `Content\Data\Generated`。
2. PIE 中用现有流程给自己 Knife，装备后应仍显示手中刀。
3. 左键攻击假人，空手不应造成伤害；装备 Knife 后三连击、体力消耗、冷却、受击/死亡反馈应保持 Unit 34 之前的行为。
4. 打开 `BP_Weapon_Knife`，确认手动调好的 `WeaponMeshComponent`、`HitStartComponent`、`HitEndComponent` 和 `HitRadius` 没被脚本重置。

验证：
- 2026-06-04：`python Scripts\GenerateGameData.py` 成功，三张源表已同步到 `Content\Data\Generated`。
- 2026-06-04：`python -m py_compile Scripts\GenerateGameData.py Scripts\CreateWeaponBlueprints.py Scripts\CreateWeaponPreviewStage.py` 通过。
- 2026-06-04：源表新增 `#CN` / `#DESC` 说明行后，`python Scripts\GenerateGameData.py` 通过；生成表验证第一行仍为英文机器字段名，没有把中文说明写入运行时 CSV。
- 2026-06-04：源表说明行支持后，`python -m py_compile Scripts\GenerateGameData.py Scripts\CreateWeaponBlueprints.py Scripts\CreateWeaponPreviewStage.py` 通过。
- 2026-06-04：`CoopSurvival Win64 Development` 编译通过。
- 2026-06-04：`CoopSurvivalEditor Win64 Development` 外部编译被当前 Editor / Live Coding 拦截，提示 `Unable to build while Live Coding is active`。这是 Live Coding 锁，不是运行目标代码编译错误；测试本单元前需要关闭并重开 Editor，或在 Editor 内按 `Ctrl+Alt+F11` 编译最新 Editor 模块。

用户验证结果：
待验证。

## Unit 36：Firearm-A 基础枪械开火与弹药消耗

目标：
- 当前装备 `Pistol` 或 `Shotgun` 时，左键不再走近战失败分支，而是读取枪械定义执行服务器开火。
- 枪械数值从 `Data/Source/FirearmDefinitions.csv` 配置：弹药物品、伤害、弹丸数、射程、散布、开火间隔、每次耗弹、噪音和命中 Buff 占位。
- 右键进入 ADS / AimOffset 模式；非 ADS 腰射左键只让角色转向准星并追加腰射散布。
- 弹药作为普通 `ItemId` 进入背包，服务器开火时从 `ACSPlayerState` 背包扣除。
- 服务器使用当前玩家视角做 hitscan 命中，霰弹枪按 `PelletsPerShot` 做多弹丸散布，并对带 `UCSSurvivalStatsComponent` 的目标扣血。

非目标：
- 本单元不实现弹匣、换弹、空仓挂机、枪械耐久实例、后坐力、镜头晃动、枪口火光、枪声、弹壳、弹孔、最终枪械 Mesh 精调或正式 UI 弹药条。
- 本单元不实现 PvP 级回滚命中或反作弊级服务器重算；当前合作 PvE 只做服务器权威、低成本的最终命中与扣弹。
- 本单元不把枪械数据写回 `Items.csv`；`Items.csv` 只保留通用物品信息。

实现记录：
- 新增 `Data/Source/FirearmDefinitions.csv`，顶部包含 `#CN` 中文属性名和 `#DESC` 字段说明，运行时生成表只保留机器字段。
- `Data/Source/Items.csv` 新增 `Ammo_9mm` 和 `Ammo_Shell`，作为手枪和霰弹枪弹药。
- `Scripts/GenerateGameData.py` 新增 `FirearmDefinitions.csv` 校验与生成，要求枪械 `ItemId` 必须有装备定义，`AmmoItemId` 必须是已知物品。
- `FCSFirearmDefinition` 新增到 `CSItemDataTypes.h`；`UCSItemDataSubsystem` 新增 `GetFirearmDefinition()` 和 `LoadFirearmDefinitionsCsv()`。
- `UCSCombatComponent::PrimaryAttackOnServer()` 在近战定义不存在时继续尝试枪械定义；没有任何攻击定义时才提示当前装备无攻击定义。
- `UCSCombatComponent::FirearmAttackOnServer()` 负责服务器冷却、弹药检查、扣弹、视角 hitscan、散布、多弹丸聚合伤害和 Debug 反馈。
- `ACSPlayerCharacter` 新增复制的 `bIsAiming` 和 `ReplicatedAimTargetWorldLocation`；本地右键按住时按 `AimTargetReplicationInterval` 限频把准星世界点发给服务器，远端 AnimBP 用复制目标做表现。
- `UCSPlayerAnimInstance` 的 AimOffset 输入改为 `枪口位置 -> 屏幕准星点` 的相对旋转，不再直接使用 Raw ControlRotation yaw/pitch；只有 `bIsAiming` 为 true 时 `bUseEquipmentAimOffset` 才为 true。
- `Scripts/ConfigureFirearmAnimationLayers.py` 新增 `bUseEquipmentAimOffset` gate：右键 ADS 时枪械 BlendSpace 进入 AO，非 ADS 时直接使用枪械上半身 BlendSpace，不叠加 AimOffset。
- `Data/Source/EquipmentDefinitions.csv` 已把 `Pistol` / `Shotgun` 指向 `/Game/Blueprints/Weapons/BP_Weapon_Pistol` 和 `/Game/Blueprints/Weapons/BP_Weapon_Shotgun`；默认 Mesh 分别指向 `/Game/FPSWeaponPackVol2/Weapons/Meshes/1911/SM_1911_X` 和 `/Game/FPSWeaponPackVol2/Weapons/Meshes/Shot12/SM_Shot12_X`。握持位置、枪口和后续弹壳 Socket 仍在对应 `BP_Weapon_*` 里调，不写回角色代码。
- `Scripts/CreateWeaponBlueprints.py` 现在会创建或补全 Knife / Pistol / Shotgun 武器蓝图；已有枪械蓝图只在 Mesh 为空或仍是 Engine Cube 占位时替换为装备表 Mesh，且只在组件仍是脚本默认占位 Transform 时初始化导入枪械 Transform，不覆盖手工调好的组件 Transform。
- 武器 Actor 已拆分为基类和类型子类：`ACSWeaponActor` 只保留 `RootSceneComponent` / `WeaponMeshComponent`；`ACSMeleeWeaponActor` 拥有 `HitStartComponent` / `HitEndComponent` / `HitRadius`；`ACSFirearmWeaponActor` 拥有 `MuzzleComponent`。`BP_Weapon_Knife` 应继承近战子类，`BP_Weapon_Pistol` / `BP_Weapon_Shotgun` 应继承枪械子类，避免枪械蓝图出现无关 Hit 调参项。
- 武器 IK 点改为武器蓝图作者权威：`ACSWeaponActor` 暴露 `RightHandGripComponent`、`LeftHandIKComponent`、`AimPointComponent`、`bUseLeftHandIK` 和 `LeftHandIKAlpha`。角色生成武器时通过 `AttachGripToComponent()` 用右手握点对齐角色手部 Socket；默认握点在 Root 原点，因此不改变已有手调武器位置。
- `UCSPlayerAnimInstance` 输出 `LeftHandWeaponIKComponentTransform`、`LeftHandWeaponIKRightHandSpaceTransform`、`WeaponAimPointComponentTransform`、`bUseLeftHandWeaponIK` 和 `LeftHandWeaponIKAlpha`，供 `ABP_Player_Body` / Control Rig 接正式左手 IK 和瞄准修正。IK 点来自当前装备的 `BP_Weapon_*`，不写入 CSV，也不写死到角色代码。
- `Scripts/CreateWeaponBlueprints.py` 为 Pistol / Shotgun 的新 IK 组件填入一次性默认位置；只有当 `LeftHandIKComponent` / `AimPointComponent` 仍是 C++ 默认值时才写入，避免覆盖后续在蓝图里手调的 IK 点。
- `Pistol` 当前配置：`Ammo_9mm`，单丸 22 伤害，射程 4500，ADS 散布 0.8 度，腰射额外散布 3.0 度，开火间隔 0.28 秒。
- `Shotgun` 当前配置：`Ammo_Shell`，8 个弹丸，每丸 11 伤害，射程 2600，ADS 散布 6.5 度，腰射额外散布 4.0 度，开火间隔 0.9 秒。

验证步骤：
1. 关闭并重开 Unreal Editor，或在 Editor 内按 `Ctrl+Alt+F11` 编译，使 Editor 加载最新 C++。
2. 运行 `python Scripts\GenerateGameData.py`，确认 `Data\Generated\FirearmDefinitions.csv` 存在，且 `Content\Data\Generated` 下不应再有 CSV。
3. PIE 中通过 GM 面板或控制台给予物品：`GiveItem Pistol 1`、`GiveItem Ammo_9mm 20`。
4. 选择装备栏中的 `Pistol`，右手应生成使用 `SM_1911_X` 的 `BP_Weapon_Pistol`；对准测试假人按左键，应扣除 `Ammo_9mm`，命中时假人扣血，未命中时提示 missed，不再提示 no equipped melee weapon。
4.1. 不按右键直接左键开火：角色 yaw 应转向准星，枪械不进入 AimOffset，散布应明显大于 ADS。
4.2. 按住右键后移动鼠标上下：枪口到准星方向驱动 AimOffset；松开右键后上半身应回到持枪 BlendSpace，不继续抬手瞄准。
5. 通过 GM 面板或控制台给予物品：`GiveItem Shotgun 1`、`GiveItem Ammo_Shell 8`。
6. 选择装备栏中的 `Shotgun`，右手应生成使用 `SM_Shot12_X` 的 `BP_Weapon_Shotgun`；对准测试假人按左键，应按霰弹多弹丸散布命中并扣除 `Ammo_Shell`。
7. 清空或不发对应弹药时，左键应提示 `Fire failed: no Ammo_9mm` 或 `Fire failed: no Ammo_Shell`，不应造成伤害。
8. Listen Server + Client 下，由客户端装备 Pistol/Shotgun 开火，服务器应扣客户端背包弹药并对服务器认可的目标造成伤害。
9. GM 面板可直接点 `Equip Pistol Kit` 或 `Equip Shotgun Kit`，分别给予枪械、对应弹药并把枪装备到当前选中快捷槽，用于避免手输 `ItemId` 时误测。
10. 打开 `BP_Weapon_Pistol` / `BP_Weapon_Shotgun`，组件树应只显示 `RootSceneComponent`、`WeaponMeshComponent`、`MuzzleComponent`，不应再显示 `HitStartComponent` / `HitEndComponent`。枪械握持方向调 `WeaponMeshComponent`，枪口位置调 `MuzzleComponent`。

验证：
- 2026-06-04：`python Scripts\GenerateGameData.py` 成功，`FirearmDefinitions.csv`、`Items.csv` 已同步到 `Data\Generated`。
- 2026-06-04：`python -m py_compile Scripts\GenerateGameData.py Scripts\ConfigureEquipmentAnimationLayers.py Scripts\CreatePlayerAnimationAssets.py` 通过。
- 2026-06-04：`CoopSurvival Win64 Development` 编译通过。
- 2026-06-04：`CoopSurvivalEditor Win64 Development` 外部编译被当前运行中的 `UnrealEditor.exe` / `LiveCodingConsole.exe` 拦截，提示 `Unable to build while Live Coding is active`。这不是代码错误；测试本单元前需要关闭并重开 Editor，或在 Editor 内按 `Ctrl+Alt+F11`。
- 2026-06-04：用户关闭 Editor 后，`CoopSurvivalEditor Win64 Development` 编译通过。
- 2026-06-04：修复用户发现的“枪械装备后手上没有枪”链路缺口；`Pistol` / `Shotgun` 的 `EquipmentActorClassPath` 已写入源表和生成表，并用 `UnrealEditor-Cmd -run=pythonscript -script=Scripts\CreateWeaponBlueprints.py` 创建 `/Game/Blueprints/Weapons/BP_Weapon_Pistol` 与 `/Game/Blueprints/Weapons/BP_Weapon_Shotgun`。
- 2026-06-04：文件级验证通过：`Content\Blueprints\Weapons\BP_Weapon_Pistol.uasset`、`Content\Blueprints\Weapons\BP_Weapon_Shotgun.uasset`、`Data\Generated\EquipmentDefinitions.csv` 均存在并包含对应类路径。
- 2026-06-04：修复用户打开 PIE 时弹出 DataTable 导入窗口的问题。根因是运行时 CSV 生成在 `Content\Data\Generated`，UE Editor 把新增/修改的 CSV 当作 DataTable 导入源。`Scripts\GenerateGameData.py` 已改为生成到项目根目录 `Data\Generated`，`UCSItemDataSubsystem` 已改为从 `FPaths::ProjectDir()/Data/Generated` 读取，旧 `Content\Data\Generated\*.csv` 已清理，`Content` 目录下文件级验证无残留 CSV。
- 2026-06-04：`python Scripts\GenerateGameData.py` 成功生成 `Data\Generated`；`python -m py_compile Scripts\GenerateGameData.py Scripts\CreateWeaponBlueprints.py` 通过；`CoopSurvival Win64 Development` 编译通过。
- 2026-06-04：用户关闭 Unreal Editor / LiveCodingConsole 后，`CoopSurvivalEditor Win64 Development` 编译通过；PIE 应加载新路径 `Data\Generated`，不再读取旧 `Content\Data\Generated`。
- 2026-06-04：检查用户最新 PIE 日志，`GameData` 已正常加载 `2 firearm definitions`，但本轮日志只出现 `Pistol +1` / `Equipped 9mm Pistol`，没有 `Shotgun +1` 或 `Equipped Shotgun`；因此在 GM 面板新增 `Equip Pistol Kit` / `Equip Shotgun Kit` 测试按钮，按钮仍调用现有服务器 `GiveItem` 和 `RequestEquipItem` 链路。
- 2026-06-04：`CoopSurvival Win64 Development` 编译通过；当前 `UnrealEditor.exe` / `LiveCodingConsole.exe` 仍在运行，Editor target 需要关闭 Editor 后再外部编译。
- 2026-06-04：用户关闭 Editor 后，`CoopSurvivalEditor Win64 Development` 编译通过；重开 Editor 后 GM 面板应显示 `Equip Pistol Kit` / `Equip Shotgun Kit`。
- 2026-06-05：按用户要求导入已解压的 `FPS Weapon Pack Vol 2` 武器资源到 `/Game/FPSWeaponPackVol2/Weapons`，未导入示例关卡；`Safe House VOL.1` 当前仍是 4.5GB 压缩包，现有 7-Zip 无法列目录，本单元不接入。
- 2026-06-05：`EquipmentDefinitions.csv` 的 `Pistol` / `Shotgun` 默认 Mesh 改为 `SM_1911_X` / `SM_Shot12_X`；`UnrealEditor-Cmd -run=pythonscript -script=Scripts\CreateWeaponBlueprints.py` 成功保存 `BP_Weapon_Pistol` / `BP_Weapon_Shotgun`，文件级验证确认蓝图已引用新 Mesh，0 error / 0 warning。
- 2026-06-05：按用户要求移除枪械蓝图中的近战 Hit 调参项。C++ 拆出 `ACSMeleeWeaponActor` 和 `ACSFirearmWeaponActor`，脚本改为 Knife 继承近战子类、Pistol/Shotgun 继承枪械子类；用户关闭 Editor 后，`CoopSurvivalEditor Win64 Development` 编译通过，`CreateWeaponBlueprints.py` 和 `CreateWeaponPreviewStage.py` 均执行成功。文件级验证确认 `BP_Weapon_Pistol` / `BP_Weapon_Shotgun` 继承 `CSFirearmWeaponActor` 且只含 `MuzzleComponent`，不含 `HitStartComponent` / `HitEndComponent`；`BP_Weapon_Knife` 继承 `CSMeleeWeaponActor` 并保留 `HitStartComponent` / `HitEndComponent`。
- 2026-06-05：武器 IK 数据链路 C++ 已实现并完成 Editor 目标验证。`python -m py_compile Scripts\CreateWeaponBlueprints.py Scripts\CreateWeaponPreviewStage.py` 通过；用户关闭 Editor 后，`CoopSurvivalEditor Win64 Development` 编译通过；`CreateWeaponBlueprints.py` 与 `CreateWeaponPreviewStage.py` 均通过 `UnrealEditor-Cmd -run=pythonscript` 执行成功。文件级验证确认 `BP_Weapon_Pistol` / `BP_Weapon_Shotgun` 拥有 `RightHandGripComponent`、`LeftHandIKComponent`、`AimPointComponent`、`MuzzleComponent`，且不再包含近战 `HitStartComponent` / `HitEndComponent`；`BP_Weapon_Knife` 保留近战 Hit 组件并同步拥有 IK 调参组件。
- 2026-06-05：右键 ADS / 腰射规则接入后，`python -m py_compile Scripts\GenerateGameData.py Scripts\ConfigureFirearmAnimationLayers.py` 通过；`python Scripts\GenerateGameData.py` 成功，`Data\Generated\FirearmDefinitions.csv` 已包含 `HipFireSpreadDegrees`；`CoopSurvivalEditor Win64 Development` 与 `CoopSurvival Win64 Development` 均编译通过；`ConfigureFirearmAnimationLayers.py` 通过命令行重刷 `ABP_Player_Body`，只读验证确认图内存在 `AimOffsetYaw`、`AimOffsetPitch`、`bUseEquipmentAimOffset` 和 4 个枪械布尔混合节点。

用户验证结果：
待验证。

## 设计文档导入记录

2026-05-29：已把 `Z:\游戏资源\Docs\SystemDesign` 下的 20 个 Markdown 文档导入到 `Docs/SystemDesign/`，作为当前 UE 项目的设计原型来源。

处理结果：
- 原始系统设计文档保留在 `Docs/SystemDesign/`。
- 新增 `Docs/SystemDesign/UE_EngineDifferences.md`，记录 Unity / URP / UGUI / Cinemachine / C# 概念到 Unreal 5.8 的转换规则。
- `AGENTS.md` 和 `Docs/GameDesign_v0.1.md` 已登记新的本地设计来源。

2026-05-30 清理：`SD_01` 到 `SD_18` 不再整体作为当前 P0 入口。`SD_07`、`SD_08`、`SD_12`、`SD_17`、`SD_18` 暂保留为可被未来单元抽取的系统参考；其余早期导入文档降级为历史来源，不能覆盖 `SD_19`、`SD_20`、`SD_22`、`SD_28`、`SD_31` 和当前 UE 项目规则。

2026-06-01 归档：旧导入文档 `SD_01` 到 `SD_18` 已从 `Docs/SystemDesign/` 根目录移动到 `Docs/SystemDesign/Archive/LegacyImported/`。根目录现在只保留当前有效设定、UE 转换说明和后续逐层细化文档；归档目录保留旧资料追溯和未来局部抽取用途。后续如要彻底删除旧档案，应先把仍有价值的战斗、HUD、快捷栏、UI 流程和地图图标规则抽成新的 UE-native 文档。

验证：
- 文件级验证通过：`Docs/SystemDesign/` 根目录保留当前有效系统文档和 UE 转换说明；旧 `SD_01` 到 `SD_18` 文件已集中到 `Docs/SystemDesign/Archive/LegacyImported/`，并新增归档说明。
- 本次只改文档，不改 C++、Blueprint 或项目配置；无需编译。

## 玩法深度方向记录

2026-05-29：用户明确不希望项目做成浅层“生存 + 打污染者/僵尸 + 靠枪械杀穿通关”的游戏。新增 `Docs/SystemDesign/SD_19_玩法深度与反枪械通关规则.md`，将后续设计方向收敛为：武器负责解决常规战斗压力，但不能替代设施权限、污染/身份风险、核心异常规则、样本归档、安全点、路线解锁、工具/站点和合作分工。

本文档只更新设计方向，不改变当前 Unit 7 的实现状态。

## 战斗装备与 AIM 协议成长系统记录

2026-06-02：按用户要求，将战斗系统整理为结构化系统文档 `Docs/SystemDesign/SD_42_战斗装备与AIM协议成长系统.md`。该文档把战斗定义为“武器/护甲/背包/工具的工程材料升级线”与“SCP 样本/AIM 协议能力线”的双成长结构：普通材料用于提升武器数值、护甲、防护器、钥匙卡、背包和工具；SCP 样本、残留和档案用于解锁决定性、独特但带污染和身份风险的异常协议能力。

同步更新：`Docs/SystemDesign/README.md` 已把 `SD_42` 加入当前有效档案结构；`Docs/SystemDesign/SD_32_世界观到细节设定分层索引.md` 已把 `SD_42` 加入后续文档表，并说明战斗实现应从该文档拆成基础武器数值、护甲减伤、背包负载、材料升级站、AIM 协议槽位和 SCP 样本归档解锁等小单元。

设计边界：本文只记录战斗、装备和技能成长的系统框架，不改变当前 Unit 15 的实现状态，不新增 C++、Blueprint、DataAsset、UMG、AI、动画或关卡资产。后续实现必须继续按 `Docs/DevelopmentUnits.md` 拆成小单元，并保持服务器权威、DataAsset 配置和 Blueprint 表现分层。

2026-06-02 图表化补充：按用户反馈，`SD_42` 前部新增“图表速览”，用 Mermaid 总览图、战斗决策循环图、双成长线图、装备盘表、AIM 协议解锁时序图、单人优先职责矩阵和首版实现路线图展示战斗系统，降低纯文字阅读成本。本次仍只改设计文档，不改变当前 Unit 15 的 UE 实现状态。

2026-06-02 颜色标记补充：继续按用户反馈，为 `SD_42` 图表速览新增统一颜色图例，并给 Mermaid 流程图添加节点颜色。当前颜色语义为蓝色 S0/安全整备、青色装备盘、绿色工程升级、紫色 SCP/AIM、橙色战斗压力、红色风险代价。本次仍只改设计文档。

2026-06-02 单人优先修正：用户确认战斗和核心体验必须以单人为主。已同步更新 `AGENTS.md`、`Docs/GameDesign_v0.1.md` 和 `SD_42`：项目定位调整为“单人完整可玩，最多四人可选合作”；战斗、修复、收容、归档、撤回和关键路线必须按单人可完成设计；多人合作只用于降低压力、提高效率和形成职责专精，不能成为核心进度硬门槛。

2026-06-02 世界存档解锁修正：用户确认关键物品和 SCP/AIM 解锁不能按每个玩家个人进度重复计算，而应按当前游戏世界存档设计。已同步更新 `AGENTS.md`、`Docs/GameDesign_v0.1.md` 和 `SD_42`：SCP 样本归档、AIM 协议可用性、关键路线、层级权限、关键设施修复和唯一科技/配方解锁写入世界存档 / Site 档案状态；玩家个人只保存背包、装备、当前装载协议、生存状态和位置。后加入玩家不需要重复打同一个 SCP，但也不会自动获得个人物品。

2026-06-02 动态难度曲线补充：按用户反馈，`SD_42` 新增人数动态难度与物资缩放规则。当前原则为单人基准、按有效参与玩家数缩放，不按最大房间人数缩放；人数增加时提高普通敌人刷怪预算、精英比例、Boss 血量和阶段压力，同时略微提高普通材料与消耗品总量；关键 SCP 样本、关键物、路线解锁和 AIM 协议可用性不随人数复制。本次仍只改设计文档。

2026-06-02 开发拆分文档：按用户要求，新增 `Docs/Development/CombatSystem_开发拆分与功能逻辑说明.md`，作为交给开发的战斗系统拆分入口。该文档把 `SD_42` 的总设计拆成 M01-M12 模块，覆盖战斗输入、武器数据、伤害状态、护甲负载、敌人接口、动态难度、Boss 缩放、物资缩放、世界解锁、AIM 协议、UI 反馈和存档迁移，并为每个模块写明功能目标、服务器逻辑、数据归属和验收标准。同步更新 `Docs/Development/README.md`、`Docs/SystemDesign/README.md` 和 `SD_32`。本次仍只改设计文档，不改变当前 UE 实现状态。

2026-06-02 开发交付文档补全：按用户列出的缺口，在 `Docs/Development/` 新增 `SearchInventory_开发拆分与功能逻辑说明.md`、`ObjectiveWorldState_开发拆分与功能逻辑说明.md`、`SaveSystem_开发拆分与功能逻辑说明.md`、`AIEncounterBoss_开发拆分与功能逻辑说明.md`、`ElevatorStreaming_开发拆分与功能逻辑说明.md` 和 `LimitedBuilding_开发拆分与功能逻辑说明.md`。这些文档分别覆盖搜索背包、任务世界状态、存档、AI/遭遇/Boss、电梯 Streaming、有限 Deployable 的模块拆分、服务器逻辑、数据归属、建议开发单元和验收标准。`LimitedBuilding` 明确不重开自由基地建造，只保留未来有限 Deployable 边界。本次仍只改文档，不改变当前 UE 实现状态。

2026-06-02 开发交付文档 review 修正：按用户指出的问题修正 `Docs/Development/` 文档口径。`AIEncounterBoss` 改为 AI/Boss 只发出 `ObjectiveEvent`，由 `ObjectiveWorldState` 写入世界状态；`SaveSystem` 补充保存在线玩家时必须保留已有离线玩家存档记录；`ElevatorStreaming` 明确全局环境光、天空、主后处理和曝光基准只属于 Persistent Map，Streaming 层不得放重复 `SkyLight` / `SkyAtmosphere` / 全局曝光控制；新增交付文档补齐“本文不代表 UE 当前实现状态”声明；配置源口径统一为 CSV/表格生成项目内数据或 DataAsset，运行时不读取 WPS/Excel 原文件。

2026-06-04 战斗解法边界修正：用户确认“非常规手段击败”只适用于部分 SCP 关卡、特定 Boss、环境机关、收容流程或关键异常规则，常规敌人、大多数精英敌人和常规 Boss 仍应以武器击败或逼退为主。已同步更新 `AGENTS.md`、`Docs/GameDesign_v0.1.md`、`Docs/SystemDesign/SD_19_玩法深度与反枪械通关规则.md`、`Docs/SystemDesign/SD_42_战斗装备与AIM协议成长系统.md` 和 `Docs/Development/CombatSystem_开发拆分与功能逻辑说明.md`。边界：反对的是“纯火力替代所有系统并自动通关”，不是反对武器战斗；非常规解法必须由数据或关卡文档明确标记为 SCP 规则目标、收容谜题或 SetPiece Boss。本次仍只改文档，不改变当前 UE 实现状态。

2026-06-04 枪械战斗工程交付文档：按用户确认的枪械设计方向，新增 `Docs/Development/FirearmCombat_开发拆分与功能逻辑说明.md`。该文档把第三人称枪械开火、弹药、换弹、后坐力、散布、耐久、卡壳、噪音、附件、枪械升级、特殊弹药、UI 和存档拆成 FR01-FR10 模块，并提出 Firearm-A 到 Firearm-I 的开发单元。同步更新 `Docs/Development/README.md`、`Docs/Development/SystemImplementationRoadmap.md` 和 `Docs/Development/CombatSystem_开发拆分与功能逻辑说明.md`。本次只改工程文档，不新增 C++、Blueprint、DataAsset、UMG、AI、动画或地图资产。

2026-06-04 枪械表现特效与音效补充：按用户补充的枪口火光、命中特效、枪声和掏枪声音要求，扩展 `Docs/Development/FirearmCombat_开发拆分与功能逻辑说明.md`。新增 FR09 表现特效与音效模块，明确枪口火光、弹道/曳光、弹壳、命中材质特效、弹孔、远近枪声、拔枪/收枪、换弹、清卡、空枪、卡壳和受击反馈的资产归属、网络广播边界和验收标准；UI/存档顺延为 FR10，特殊弹药/异常改装顺延为 FR11。同步更新 `Docs/Development/README.md` 和 `Docs/Development/SystemImplementationRoadmap.md`。本次仍只改工程文档，不新增 C++、Blueprint、DataAsset、UMG、Niagara、Audio 或动画资产。

2026-06-04 Buff/状态效果系统文档：按用户确认“Buff 会影响属性或增加额外效果”的方向，新增 `Docs/Development/BuffSystem_开发拆分与功能逻辑说明.md`。该文档把玩家属性、正向 Buff、负面 Debuff、流血、中毒、污染、装备词条、药品效果、区域影响、AIM/SCP 协议效果、叠层、持续时间、周期效果、额外标签、UI、复制和存档拆成 BF01-BF10 模块，并提出 Buff-A 到 Buff-I 开发单元。同步更新 `AGENTS.md`、`Docs/GameDesign_v0.1.md`、`Docs/Development/README.md`、`Docs/Development/SystemImplementationRoadmap.md`、`Docs/Development/CombatSystem_开发拆分与功能逻辑说明.md`、`Docs/Development/SearchInventory_开发拆分与功能逻辑说明.md`、`Docs/Development/SaveSystem_开发拆分与功能逻辑说明.md` 和 `Docs/Development/AIEncounterBoss_开发拆分与功能逻辑说明.md`。本次仍只改文档，不新增 C++、Blueprint、DataAsset、UMG、AI、Niagara、Audio 或动画资产。

2026-06-04 怪物资源包导入：用户要求检查 Z 盘 Unreal 怪物资源并导入当前项目。`Z:\Unreal` 不存在，实际资源位于 `Z:\游戏资源\Unreal\03_角色生物`。本次选择小体量原型资源 `【UE4.27+】【怪物生物】变异生物03`，按资源安装说明保持原始 `/Game/InfiniteCollection/...` 路径复制到 `Content/InfiniteCollection/MutantCreatureBundle01/MutantCreature03`。导入内容共 24 个文件，约 70 MB，包含 `SK_MC03_Full`、`S_MC03_Full_Skeleton`、`PA_MC03_Full_PhysicsAsset`、材质、贴图、示例动画和 2 个示例地图。当前 Unreal Editor / LiveCodingConsole 正在运行，未执行 UE 命令行加载、重保存或材质验证；后续需要关闭或重启 Editor 后，在内容浏览器加载 `MC03_ShowcaseMap` 或打开 `SK_MC03_Full` 验证资产升级、材质和骨骼是否正常。本次只导入美术资源，不代表 AI、敌人蓝图、碰撞、行为树、掉落或战斗数据已经接入。

2026-06-04 基础怪物设计：按用户要求先设计一个基础怪物，新增 `Docs/SystemDesign/SD_45_基础敌人_污染变异体设计.md` 和 `Docs/Development/BasicEnemy_污染变异体开发拆分与功能逻辑说明.md`。当前首个基础普通敌人定义为“污染变异体”，内部 ID 建议 `Enemy_ContaminatedMutant_Basic`，定位为 L0/L1 普通近战追击者，可被武器正常击杀，攻击通过 BuffSystem 添加轻度污染，死亡只掉普通怪物材料，不产出关键样本、路线权限、AIM 协议或世界解锁。设计允许首版视觉使用刚导入的 `SK_MC03_Full` 资源，但资源只作为 Mesh/Skeleton/动画来源，不承载敌人规则。同步更新 `Docs/SystemDesign/README.md`、`Docs/Development/README.md` 和 `Docs/Development/AIEncounterBoss_开发拆分与功能逻辑说明.md`。本次只改文档，不新增 C++、Blueprint、DataAsset、AI、动画或地图投放。

2026-06-04 战斗音效与子弹 VFX 资源导入：按用户要求从 NAS/Z 盘资源导入枪声、脚步声、枪口/子弹命中特效。实际来源为 `Z:\游戏资源\Unreal\11_声音音频\【UE4.26+】枪声音效包Vol1`、`Z:\游戏资源\Unreal\11_声音音频\【UE4.26+】脚步声音效包Lite版` 和 `Z:\游戏资源\Unreal\10_特效粒子\【UE5.0+】子弹VFX特效包`。音效包按内部引用路径放入 `Content/GunSoundsPackVol1` 和 `Content/FootstepSoundsPackLite`；子弹 VFX 按资源原始路径合入 `Content/Particles`、`Content/Materials`、`Content/Meshes` 和 `Content/Textures`。已新增 `Scripts/ValidateImportedCombatAudioVFX.py` 并通过 UE 5.8 命令行抽样加载验证：枪声 Cue、空枪 Cue、Concrete 脚步 Cue、`VFX_Muzzleflash_01`、`VFX_Impact_Metal`、`VFX_Impact_Flesh` 均可加载，0 error / 0 warning。`Z:\游戏资源\Unreal\10_特效粒子\【UE4.26+】20种枪击粒子特效` 只有 PSD/TGA 源贴图，本次不导入 active Content。同步新增 `Data/Source/FirearmPresentationDefinitions.csv`、`Data/Source/SurfaceImpactEffectSets.csv` 和 `Data/Source/FootstepSurfaceAudio.csv`，并更新 `Scripts/GenerateGameData.py` 生成到 `Data/Generated/`。规则口径：材质命中特效按 `SurfaceType -> ImpactEffectSet` 轻量配置，代码只查表并使用 `Default` fallback，不在枪械/战斗代码里写死材质分支。本次未新增 C++、Blueprint、DataAsset 或运行时接入逻辑。

## 关卡推进、难度与结局框架记录

2026-06-04：用户提供新版总结文档 `SD_43_关卡推进结构与难度结局框架.md`，已导入到 `Docs/SystemDesign/`。该文档确认主线采用线性层级推进，不用同层多难度回刷解锁下一层；开局选择全局难度并写入世界存档；L1-L6 骨干揭示点随主线自然归档，不额外设置“收集 X 个才开 L7”的硬门；结局可选项由归档完整度决定，L7 由玩家最终决断；层级专属物资作为线性资源链和 Combine 能力来源，稳定层可重复探索刷普通养成材料，但关键解锁不重复产出。

同步更新：`AGENTS.md`、`Docs/GameDesign_v0.1.md`、`Docs/SystemDesign/README.md`、`Docs/SystemDesign/SD_20_搜索搜刮与背包管理系统.md`、`Docs/SystemDesign/SD_22_深井协议_世界观故事与三结局总纲.md`、`Docs/SystemDesign/SD_32_世界观到细节设定分层索引.md`、`Docs/SystemDesign/SD_33_层级功能玩法与背景故事总览.md`、`Docs/SystemDesign/SD_37_Site器官化真相与阶段揭示.md`、`Docs/SystemDesign/SD_42_战斗装备与AIM协议成长系统.md`、`Docs/SystemDesign/UE_P0ImplementationPlan.md`、`Docs/Development/README.md`、`Docs/Development/SystemImplementationRoadmap.md`、`Docs/Development/ObjectiveWorldState_开发拆分与功能逻辑说明.md`、`Docs/Development/SaveSystem_开发拆分与功能逻辑说明.md`、`Docs/Development/SearchInventory_开发拆分与功能逻辑说明.md`、`Docs/Development/CombatSystem_开发拆分与功能逻辑说明.md` 和 `Docs/Development/AIEncounterBoss_开发拆分与功能逻辑说明.md`。新增 `Docs/Development/LevelProgressionDifficultyEnding_开发拆分与功能逻辑说明.md`，用于交付开发拆分。

设计边界：本文档同步只改变设计和开发交付口径，不新增 C++、Blueprint、DataAsset、UMG、AI、地图或美术资产。后续实现全局难度、骨干揭示点、归档完整度、结局选项或 S0 认知面板时，仍必须拆成 `Docs/DevelopmentUnits.md` 中的小单元并完成编辑器验证。

2026-06-04 反复探索与 RPG 养成修正：用户确认保留反复探索。已将口径修正为“主线不靠同层回刷或多难度复刷解锁下一层；已稳定、主任务完成或安全路线登记后的层可以重复探索，刷取普通材料、普通怪物材料、消耗品和非关键高价值物”。关键 SCP 样本、唯一关键物、路线权限、AIM 协议可用性、骨干揭示点、结局核心变量仍只写入世界状态一次，不重复产出。同步更新 `AGENTS.md`、`Docs/GameDesign_v0.1.md`、`SD_20`、`SD_32`、`SD_33`、`SD_42`、`SD_43`、`Docs/SystemDesign/README.md`、`LevelProgressionDifficultyEnding`、`SearchInventory`、`SaveSystem`、`CombatSystem`、`AIEncounterBoss` 和 `SystemImplementationRoadmap`。本次仍只改文档，不改变当前 UE 实现状态。

## SCP 世界观命名记录

2026-05-29：用户明确本项目就是 SCP 游戏，文档不需要回避 SCP 基金会语义。后续设计文档可以直接使用 SCP、基金会、Site、D级、收容失效、SCP项目、权限等级、收容流程、样本归档和基金会终端等术语。商业发布前另开合规/署名检查单元；当前仅记录设计方向，不改变当前 Unit 7 的实现状态。

## 搜索与背包管理方向记录

2026-05-29：用户希望补充一部分类似《三角洲行动》一类战术搜刮游戏的搜索设计，并且要有背包管理。新增 `Docs/SystemDesign/SD_20_搜索搜刮与背包管理系统.md`，将搜索系统定义为 SCP Site 内的容器搜索、物品识别、空间取舍、污染/认知风险、样本携带和安全点归档链路。

设计边界：可以参考“打开容器、看物品、整理背包、决定带走什么”的搜刮手感；不采用撤离卖钱、刷高价值垃圾、死亡装备经济或靠刷枪通关。后续实现必须拆成小单元，例如可搜索容器、基础背包 UI、样本特殊容器、格子背包和高级搜索模式。当前仅记录设计方向，不改变 Unit 8 的实现状态。

2026-06-03 背包管理与 S0 后勤经济修正：用户确认正式背包需要格子形状管理、物品是否可堆叠、堆叠上限、旋转、重量和特殊槽限制，并允许传统货币/高价值物在 S0 地表前进站出售为配给点数或共享物资，再兑换普通材料和消耗品。已同步更新 `AGENTS.md`、`Docs/GameDesign_v0.1.md`、`Docs/SystemDesign/SD_20_搜索搜刮与背包管理系统.md`、`Docs/Development/SearchInventory_开发拆分与功能逻辑说明.md`、`Docs/Development/SaveSystem_开发拆分与功能逻辑说明.md` 和 `Docs/Development/README.md`。边界：关键 SCP 样本、主线关键物、路线权限、AIM 协议解锁、世界解锁和归档关键记录不能出售或通过经济复制；S0 配给点数和共享材料库存属于世界存档，玩家背包中的旧货币/高价值物属于玩家存档。本次仍只改文档，不改变当前 UE 实现状态。

## Site 世界结构与故事骨架记录

2026-05-29：用户确认地表主线基地、主电梯下潜、L0-L7 巨型 Site、层内必要路线/深度路线、旧层回访清理、关键物作为下一层条件、以及巨型 SCP 收容空间作为深层视觉和玩法锚点的宏观结构。新增 `Docs/SystemDesign/SD_21_Site世界结构与主线关卡骨架.md`，作为后续世界观、关卡、故事、交通网络和结局条件设计总纲。

设计边界：本文只记录宏观设计和故事结构，不改变当前 Unit 9 / Unit 10 的实现状态，不新增关卡、蓝图、C++ 或数据资产。后续实现必须从 S0、主电梯、L0 新手目标链、层级状态数据等小单元逐步拆分。

2026-05-30 清理：`SD_21_Site世界结构与主线关卡骨架.md` 已删除。其宏观世界结构、层级命名、故事总纲、地图美术和每层事故影响分别由 `SD_22_深井协议_世界观故事与三结局总纲.md`、`SD_28_横截面地图美术说明与层级标注规范.md` 和 `SD_31_各层级SCP收容状态与事故影响.md` 接管。

## 深井协议世界观与三结局记录

2026-05-30：已将用户提供的设计文档导入为 `Docs/SystemDesign/SD_22_深井协议_世界观故事与三结局总纲.md`。该文档后续已升级为 v2.0；当前以《基金会:深井协议》、AIM、残留系统、`L7 禁区` 和三结局结构为准。

设计边界：本文只作为世界观、主线剧情、`L3` 地下海收容层、外部组织介入、结局条件和后续拆分文档的总纲；不改变当前 Unit 9 / Unit 10 的实现状态，不新增关卡、蓝图、C++ 或数据资产。后续实现必须继续拆分为具体开发单元，例如 `L3` 关卡白盒、三结局条件、AIC 对白、`C-12` 身份真相和外部组织事件变量。

2026-05-30：用户提供新版世界观总纲 v2.0，已更新 `Docs/SystemDesign/SD_22_深井协议_世界观故事与三结局总纲.md`。新版本确认正式设计名为《基金会:深井协议》，简称《深井协议》，英文暂定 `Foundation: Abyss Protocol`；新增 `AIM` 异常身份标记、残留信号/碎片/档案、`L3` 巨型 SCP“锚鲸”、核心角色 Dr. Elise Varga/Captain Marcus Hale/D-7710/AIC-WATCHMAN、内部文档 `SD-22` / `SD-22-03` / `SD-22-ARC`，并把层级显示名调整为 `L1 净化与后勤层`、`L2 生物过滤舱`、`L3 地下海`、`L4 档案库`、`L7 禁区`。

同步更新：`AGENTS.md`、`Docs/GameDesign_v0.1.md`、`Docs/SystemDesign/README.md`、`Docs/SystemDesign/SD_20_搜索搜刮与背包管理系统.md` 和 `Docs/SystemDesign/SD_28_横截面地图美术说明与层级标注规范.md` 已记录新版命名、残留机制和地图提示词影响。

2026-05-31：按用户要求，核心人物与 AIC 代号已完成非中文命名迁移。当前规范名为 `Dr. Elise Varga` / `Elise Varga`、`Captain Marcus Hale` / `Marcus Hale`、`Dr. Nora Weiss`、`Security Officer Evan Reed` 和 `AIC-WATCHMAN`。同步范围包括 `AGENTS.md`、`Docs/SystemDesign/` 核心设定文档、`Docs/Web/DeepWellAtlas/app.js` 和本文档；不改变项目标题、中文层级名、SCP/Foundation 术语或当前 UE 实现状态。

## 层级 SCP 收容状态记录

2026-05-30：新增 `Docs/SystemDesign/SD_31_各层级SCP收容状态与事故影响.md`。文档定义事故起因和连锁反应：GOC 穿透打击导致供电、门禁、冷却、现实锚和身份系统连锁失效；MC&D 趁乱潜入；删除队列卡死；C-12 身份残留未清除；Marcus Hale 利用混乱拖延封锁 17 分钟，为 D 级人员留下逃脱窗口。

该文档同时定义 L0-L7 每层的 SCP 项目、事故后状态、GOC/MC&D/基金会痕迹和环境压力。后续关卡、敌人、异常交互、地图提示词和目标链设计，需要优先使用 `SD_31`，再回到 `SD_22` 确认叙事目的。

设计边界：本文只记录设计方向，不改变当前 Unit 9 / Unit 10 的实现状态，不新增 C++、Blueprint、DataAsset、AI 或关卡资产。后续实现必须拆成小单元，例如 L1 自动消毒单元、L2 菌丝母体控制、L3 声呐/锚鲸事件、L4 删除蠕虫限时压力、L5 封印引擎冷却和 L6 现实锚校准。

## 横截面地图美术与提示词记录

2026-05-30：新增 `Docs/SystemDesign/SD_28_横截面地图美术说明与层级标注规范.md`，记录 Site 竖向总剖面、S0/L0-L7 分层横向地图、标注框、图像提示词、当前生成图评估和保留/重构决策。当前判断为：`L2`、`L3`、`L5`、`L7` 可作为方向保留并局部强化；`S0`、`L0`、`L1` 需要强化功能区和玩法目标；`L4`、`L6` 需要重构。

文档格式决策：项目设计源文件继续使用 Markdown；HTML 只作为后续展示页或导出版，不替代当前 `Docs/` 源文档。

设计边界：本文只记录美术方向、地图读图标准和提示词，不改变当前 Unit 9 / Unit 10 的实现状态，不新增关卡、蓝图、C++、UMG 或图像资产。最终图片如确认保留，建议保存到 `Docs/ConceptArt/LayerMaps/` 并使用文档中的命名规则。

## 世界观到细节设定索引记录

2026-05-31：新增 `Docs/SystemDesign/SD_32_世界观到细节设定分层索引.md`，用于把《基金会:深井协议》后续设计工作从大世界观拆到 Site、事故链、层级、SCP 项目、人物残留、玩法转译、物品、UI 和美术细节。该文档明确当前已锁定的大设定、后续层级设定包模板、SCP 项目设定包模板，以及后续拆分文档的优先顺序。

同步更新：`Docs/SystemDesign/README.md` 已中文化，并把 `SD_32` 加入当前有效档案结构。

设计边界：本文只记录设定工作顺序和文档拆分规则，不改变当前 Unit 9 / Unit 10 的实现状态，不新增 C++、Blueprint、DataAsset、地图、UI 或美术资产。后续若进入某一层或某个 SCP 的实现，仍必须另拆开发单元。

## Site 交互设定网页记录

2026-05-31：新增 `Docs/Web/DeepWellAtlas/` 静态网页版原型，用于随时查看《基金会:深井协议》的 Site 总剖面、分层 2D 地图节点、侧边故事和每层 SCP 信息。

当前文件：
- `Docs/Web/DeepWellAtlas/index.html`
- `Docs/Web/DeepWellAtlas/styles.css`
- `Docs/Web/DeepWellAtlas/app.js`
- `Docs/Web/DeepWellAtlas/README.md`

当前功能：总剖面层级悬停详情、点击层级进入 2D 地图、节点悬停/点击更新侧边故事、按层查看 SCP 信息。内容来源为 `SD_22`、`SD_31`、`SD_28` 和 `Docs/ConceptArt/Site_CrossSection_AfterAccident.png`。

设计边界：该网页是文档展示原型和查看工具，不替代 Markdown 设计源文档，不代表 UE 游戏内 UI、地图、关卡或美术资产已经实现。后续如需要更高保真地图，可替换横截面图和每层地图资源，同时保留交互数据结构。

2026-05-31 调整：分层 2D 地图视图从“方块房间平面图”改为“环境底图 + 独立交互组件”结构。底图由岩石、地表、隧道、水域、环境光和深井轮廓组成；可交互组件包括菌丝、核心、档案服务器、封印引擎、现实锚、医疗舱、终端和深井节点。后续美术资产应优先拆分成可替换组件，而不是输出一整张不可交互的完整地图图片。

2026-05-31 再调整：确认 SVG 只作为交互热区、节点编号和标记文字，不作为最终视觉目标。新增 `Docs/Web/DeepWellAtlas/assets/layers/README.md`，约定每层使用 `base_sketch.png`、`base_render.png` 和 `components/*.png` 的 Image2 图层化资产管线。网页会优先加载这些 PNG；缺图时显示等待 Image2 资产状态，不再显示程序化骨架、隧道或组件占位。

2026-05-31 视觉方向调整：关卡网页本身改为参考古斯塔夫·多雷式版画语言，强调强明暗、密集交叉排线、深井纵深、旧纸档案感和压迫性垂直构图。该方向只作为网页和地图资产的视觉参考，不直接复制具体作品；基金会 UI 仍保持档案化、编号化和冷静信息展示。

2026-05-31 总剖面管线调整：总剖面也改为 Imagegen / Image2 图层化拼接，不再依赖一整张不可拆的完整地图。`Docs/Web/DeepWellAtlas/assets/cross_section/README.md` 已更新为背景部件 + 层级部件结构：当前可先用 `background/00_cutaway_background.png` 做整体背景验证；结构确认后再拆成 `01_rock_walls.png`、`02_surface_forest.png`、`03_main_shaft.png`、`04_branch_tunnels.png`、`05_atmosphere_hatching.png`，并叠加 `components/S0-L7_*.png`。网页现在加载这些拆分部件，并在其上叠加交互热区。

2026-05-31 总剖面 v1 拆件资产：按用户要求生成一张总剖面背景和每层独立部件。新增 `Docs/Web/DeepWellAtlas/assets/cross_section/background/00_cutaway_background.png`、`components/S0_surface_station.png`、`L0_c12_test_zone.png`、`L1_purification_logistics.png`、`L2_bio_filter_chamber.png`、`L3_underground_sea.png`、`L4_archive_vault.png`、`L5_mechanical_core.png`、`L6_reality_anchor_array.png`、`L7_forbidden_well.png`，并保留绿幕源图到 `source_chroma/`。新增无 UI 静态合成预览 `site_cross_section_composite_v1.png`。网页首页总剖面已接入 `00_cutaway_background.png` 和 9 个层级部件。验证：`node --check Docs/Web/DeepWellAtlas/app.js` 通过；浏览器中总剖面加载 1 张背景、9 个层级部件、9 个交互热区，等待占位消失，缺失图片为 0。

2026-05-31 总剖面交通结构修正：用户指出总剖面不应表现为一部主电梯直接贯穿整个游戏。已把规则同步到 `AGENTS.md`、`Docs/SystemDesign/SD_28_横截面地图美术说明与层级标注规范.md`、`Docs/SystemDesign/SD_34_S0地表前进站完整设定.md`、`Docs/Web/DeepWellAtlas/README.md` 和 `Docs/Web/DeepWellAtlas/assets/cross_section/README.md`：主电梯只承担 S0-L0-L1 上层入口和回基地循环；L1 之后通过生物隔离门、压力准备门、地下重载货运隧道、隐藏档案竖井、机械维护通道、L6 现实锚最终门禁和 L7 深井入口进行交通接力。已生成 `Docs/Web/DeepWellAtlas/assets/cross_section/background/00_cutaway_background_v2_transport_network.png` 并覆盖当前 `00_cutaway_background.png`；新增 `site_cross_section_composite_v2_transport_network.png`，并把旧兼容预览 `site_cross_section_composite_v1.png` 覆盖为 v2 结构。验证：`node --check Docs/Web/DeepWellAtlas/app.js` 通过；浏览器中总剖面停留在 `sectionView`，加载 1 张背景、9 个层级部件、9 个交互热区，缺失图片为 0，等待占位消失。

2026-05-31 总剖面 v3 结构修正：用户进一步确认主升降通道可以到 L6 终端，但不能直接插入 `L5 机械核心` 或贯穿到 `L7`；`L3 地下海` 需要表现开放、无边际的海域；总剖面可采用轻微斜切视角，并明确层级空间尺寸。已更新 `AGENTS.md`、`SD_28_横截面地图美术说明与层级标注规范.md`、`SD_34_S0地表前进站完整设定.md`、`Docs/Web/DeepWellAtlas/README.md` 和 `assets/cross_section/README.md`：当前规则为偏置主升降通道服务到 L6 硬终端，绕开 L5 核心；L7 只能从 L6 最终门禁进入；L3 必须开放到画面边界和黑暗远景。已生成 `Docs/Web/DeepWellAtlas/assets/cross_section/background/00_cutaway_background_v3_l6_terminal_open_sea.png` 并覆盖当前 `00_cutaway_background.png`；已把 `components/L3_underground_sea.png` 替换为开放海域版，旧封闭水舱版保留为 `components/L3_underground_sea_v1_closed_cavern.png`；新增静态预览 `site_cross_section_composite_v3_l6_terminal_open_sea.png`，并把 `site_cross_section_composite_v1.png` 覆盖为 v3 兼容预览。网页结构也调整为更贴近新比例的 941:1672 画幅，并给总剖面层级部件加入轻微旋转。验证：`node --check Docs/Web/DeepWellAtlas/app.js` 通过；浏览器中总剖面停留在 `sectionView`，加载 1 张背景、9 个层级部件、9 个交互热区，缺失图片为 0，等待占位消失；L3 部件加载 `L3_underground_sea.png`，原始尺寸 1536x1024，并带轻微斜切旋转。

2026-05-31 美术参考规则补充：用户确认项目艺术风格参考《Control》。已同步到 `AGENTS.md`、`SD_28_横截面地图美术说明与层级标注规范.md`、`Docs/Web/DeepWellAtlas/README.md`、`assets/layers/README.md` 和 `assets/cross_section/README.md`。规则定义为参考其粗野主义机构建筑、联邦档案感、红色封锁/警报光、冷白实用灯、异常空间错位和克制超现实；只作为气质和生产设计参考，不复制 `Control` 的名称、标志、人物、Oldest House、FBC branding 或具体场景。`base_sketch` 仍保持简笔画结构评审风格；`base_render`、组件渲染、概念图和实际画面模拟可以进入 Control-inspired 高保真方向。本文只改文档和提示词规则，不改变 UE 实现状态。

2026-05-31 线稿规则收束：正式线稿、地图底图和可交互组件必须通过 Image2 / Imagegen 生成 bitmap PNG；禁止把 SVG、Pillow、CSS 或脚本绘制结果作为正式可见线稿或地图资产。已把 Image2 总剖面线稿参考图保存到 `Docs/Web/DeepWellAtlas/assets/cross_section/image2_reference/site_cross_section_image2_lineart.png`，仅作为拆分资产参考，不作为最终整张背景图。

2026-05-31 资产生成粒度确认：由于完整地图结构过大，正式资产不再一次性生成完整总剖面或完整层地图。后续按小单位逐张生成：总剖面拆成岩壁、地表、主竖井、分叉隧道、氛围排线和 S0-L7 层级部件；每层地图拆成底图和单个节点组件。完整大图只作为结构参考或氛围探索。

2026-05-31 L2 样板资产启动：已用 Image2 / Imagegen 生成 `Docs/Web/DeepWellAtlas/assets/layers/L2/base_sketch.png`，作为 L2 生物过滤舱的第一张正式线稿底图。该图只承担地形骨架、功能区分布和组件落点职责，后续菌丝母体、孢子记忆区、隐藏档案竖井仍需分别生成透明 PNG 组件。浏览器验证：L2 页面已加载 1 张 `raster-sketch`，等待提示消失，3 个交互热区仍可用，程序化可见 SVG 地图图形为 0。

2026-05-31 地图风格与尺度规则：确认 L2 样板的多雷式旧档案版画风格可保留，提炼为地图规则：单色蚀刻、密集交叉排线、粗糙岩石、强明暗、潮湿工业设施、低照度实用光源。同时确认当前 L2 功能区偏小；后续搜索型地图必须放大功能区，减少碎小房间，突出可搜索实验台、样本架、储物货架、设备间、维修凹室、检查站和被封堵侧室。

2026-05-31 L2 v2 地图底图：已保留旧版为 `Docs/Web/DeepWellAtlas/assets/layers/L2/base_sketch_v1_small_zones.png`，并生成新版 `base_sketch_v2_large_search_zones.png` 覆盖当前 `base_sketch.png`。新版保留旧基金会档案版画风格，同时放大左侧过滤温室、右下样本/储物大厅、上方研究区和右上隐藏分支，使功能区更适合搜索搜刮玩法。浏览器验证：L2 页面加载 `./assets/layers/L2/base_sketch.png`，1 张 `raster-sketch`，等待状态消失，3 个交互热区保留，程序化可见 SVG 地图图形为 0。

2026-05-31 S0 地图风格定版：按 `SD_34_S0地表前进站完整设定.md` 生成 S0 底图后，发现旧档案版画版 AI 噪点偏重。已保留噪点版为 `Docs/Web/DeepWellAtlas/assets/layers/S0/base_sketch_v1_noisy_engraving.png`，干净手绘线稿版为 `base_sketch_v2_clean_handdrawn.png`，并生成当前简笔画结构评审版 `base_sketch_v3_simple_line_drawing.png` 覆盖 `base_sketch.png`。浏览器验证：S0 页面加载 1 张 `raster-sketch`，6 个交互热区保留，程序化可见 SVG 地图图形为 0。用户确认后，后续从 S0 往下的 `base_sketch` 默认采用该简笔画 / 手绘结构线稿风格：清晰轮廓、低噪点、少排线、少阴影，优先服务结构评审。

2026-05-31 S0 主电梯尺度修正：用户指出 v3 中主电梯像油井且尺度过小。已把规则同步到 `AGENTS.md`、`SD_28_横截面地图美术说明与层级标注规范.md`、`SD_34_S0地表前进站完整设定.md`、`Docs/Web/DeepWellAtlas/README.md` 和 `assets/layers/README.md`：S0 主电梯必须是 Site 级重型升降井口，包含混凝土井口、控制楼、货运平台、封锁门、重型井塔/龙门架、卷缆机和车辆/轨道接驳，不能像油井、小钻塔、抽油机或临时井架。已生成 `Docs/Web/DeepWellAtlas/assets/layers/S0/base_sketch_v4_site_elevator_headworks.png`，并覆盖当前 `base_sketch.png`；网页节点已从“井架”调整为“井口”并重新对齐新版构图，右侧节点标签改为靠近地图边缘时向左展开。验证：`node --check Docs/Web/DeepWellAtlas/app.js` 通过；浏览器中 S0 层级地图加载 1 张 `raster-sketch`，6 个交互热区保留，等待占位消失，程序化可见 SVG 地图图形为 0。

2026-05-31 L0 v1 地图底图：按 `SD_35_L0_C12测试区完整设定.md` 生成 `Docs/Web/DeepWellAtlas/assets/layers/L0/base_sketch.png`，并保留备份 `base_sketch_v1_c12_test_zone.png`。该图沿用 S0 确认的简笔画结构评审风格，重点表现 D-Class 等待区、C-12 身份输入室、观察控制室、污染事故节点、门禁误判走廊和主电梯机房。网页 L0 节点已从旧 3 点调整为 5 点：D-Class 等待区、C-12 身份输入室、观察控制室、污染事故节点、主电梯保险件。验证：`node --check Docs/Web/DeepWellAtlas/app.js` 通过；浏览器中 L0 层级地图加载 `./assets/layers/L0/base_sketch.png`，1 张 `raster-sketch`，5 个交互热区保留，等待占位消失，程序化可见 SVG 地图图形为 0。

2026-05-31 L0 v2 尺度修正：用户指出 L0 设计显得狭小。已把“首版可玩区只是巨型 Site 的一段切片”写入 `AGENTS.md` 和 `Docs/Web/DeepWellAtlas/assets/layers/README.md`，并在 `SD_35_L0_C12测试区完整设定.md` 的美术提示中补充大型 C-12 主厅、批次化 D-Class 等待区、宽主通道、封锁背景翼、二层观察廊和未开放设施带要求。已生成 `Docs/Web/DeepWellAtlas/assets/layers/L0/base_sketch_v2_site_scale_c12_hall.png`，覆盖当前 `base_sketch.png`；网页 L0 节点已重新对齐 v2 构图。验证：`node --check Docs/Web/DeepWellAtlas/app.js` 通过；浏览器中 L0 层级地图加载 `./assets/layers/L0/base_sketch.png`，1 张 `raster-sketch`，5 个交互热区保留，等待占位消失，程序化可见 SVG 地图图形为 0。

2026-05-31 L1 v1 地图底图：按 `SD_36_L1净化与后勤层完整设定.md` 生成 `Docs/Web/DeepWellAtlas/assets/layers/L1/base_sketch.png`，并保留备份 `base_sketch_v1_purification_logistics_hub.png`。该图继续使用简笔画结构评审风格，重点表现偏心空气过滤主机房、医疗净化区、异常医疗舱、维修工区、后勤仓库和地下重载货运隧道，避免把 L1 画成普通小工厂或仓库迷宫。网页 L1 节点已从旧 3 点扩展为 6 点：主电梯到达区、医疗净化区、异常医疗舱、空气过滤主机房、后勤仓库、重载货运隧道。验证：`node --check Docs/Web/DeepWellAtlas/app.js` 通过；浏览器中 L1 层级地图加载 `./assets/layers/L1/base_sketch.png`，1 张 `raster-sketch`，6 个交互热区保留，等待占位消失，程序化可见 SVG 地图图形为 0。

2026-06-01 UX 提升：`Docs/Web/DeepWellAtlas/` 增加当前层节点索引、地图节点持久选中态、详情面板节点摘要、完整 tab/ARIA 状态、键盘聚焦更新、触屏可用的按钮入口，以及 `#view=layer&layer=S0&node=A` 这类可分享 hash 跳转。当前网页定位进一步明确为“设定浏览工具”，首屏继续直接展示地图，不增加营销 Hero 或说明性页面。验证：`node --check Docs/Web/DeepWellAtlas/app.js` 通过；`http://127.0.0.1:43122/Web/DeepWellAtlas/` 返回 200；Playwright 已截取桌面总剖面、层级地图、SCP 档案和移动端层级地图截图到 `output/playwright/`。本次只改静态网页和文档，不改变 UE 实现状态。

2026-06-01 总剖面层级图片修正：用户指出部分 L 层级图片被压缩且位置未对准。已将总剖面层级部件拆成两套参数：`piece` 负责 PNG 图片显示坐标和尺寸，`section` 继续负责点击/标注热区。`.cross-section-piece` 改为按比例 `object-fit: contain` 显示，避免把不同宽高比的 S0/L0-L7 部件强行拉伸。同步微调 L1-L7 的 `piece` 坐标，使部件更贴合当前 v3 总剖面背景。验证：`node --check Docs/Web/DeepWellAtlas/app.js` 通过；已重新截取 `output/playwright/deepwellatlas-cross-section-adjusted.png`。

2026-06-01 总剖面 v4 反馈修正：用户指出 L4 档案库偏下、L1 比例和剖面视角不对、地表左右结构不足，以及白色纸底和网页背景冲突。已生成并接入 `Docs/Web/DeepWellAtlas/assets/cross_section/components/L1_purification_logistics_v2_side_cutaway.png`，覆盖当前 `L1_purification_logistics.png`，旧俯视版保留为 `L1_purification_logistics_v1_topdown_floorplan.png`；新增 `background/02_surface_forest.png` 补足 S0 左右地表结构；上移 L4 的 `piece`/`section` 坐标；调整总剖面底色、暗角和背景亮度，并让网页只请求当前已存在的背景部件。新增静态预览 `site_cross_section_composite_v4_surface_l1_archive.png`，并把本次规则同步到 `SD_28`、`Docs/Web/DeepWellAtlas/README.md` 和 `assets/cross_section/README.md`。验证：`node --check Docs/Web/DeepWellAtlas/app.js` 通过；Playwright 本地服务器验证总剖面控制台 0 errors / 0 warnings，并截取 `output/playwright/deepwellatlas-cross-section-v4.png`。

2026-06-01 总剖面 v5 视角统一：用户要求所有总剖面视角都要矫正，地表底盘最好读成左右无限延展。已把规则同步到 `AGENTS.md` 和 `SD_28`：总剖面层级部件必须统一使用 side-section / slight-oblique vertical cutaway，俯视横向地图不能直接贴入总剖面。已生成并接入 `components/L5_mechanical_core_v2_side_cutaway.png`，覆盖当前 `L5_mechanical_core.png`，旧俯视圆盘版保留为 `L5_mechanical_core_v1_topdown_core.png`；`background/02_surface_forest.png` 改为中心透明的 flank 层，并在网页中用 `cross-section-surface-flanks` 横向放大、溢出 portrait 画框，形成地表底盘左右延展观感。同步更新 `Docs/Web/DeepWellAtlas/README.md` 和 `assets/cross_section/README.md`，新增静态预览 `site_cross_section_composite_v5_perspective_corrected.png` 和部件审查图 `output/playwright/deepwellatlas-component-perspective-v5.png`。验证：`node --check Docs/Web/DeepWellAtlas/app.js` 通过；Playwright 本地服务器验证控制台 0 errors / 0 warnings，并截取 `output/playwright/deepwellatlas-cross-section-v5-perspective.png`。

2026-06-01 总剖面 v6 比例网格修正：用户反馈整体比例仍不对。已按 `SD_28` 的空间尺寸表重设 `app.js` 中 S0-L7 的 `piece` 与 `section` 坐标：S0 改为扁宽地表控制端，L0/L1 收回标准层高，L3 保持最大开放地下海，L4 保持偏置隐藏，L5 保留机械核心但不再占过厚层高，L6 压成狭长现实锚屏障，L7 保持破碎深井轮廓。`.cross-section-piece` 改为按比例网格填充显示框，避免 PNG 原始宽高比反向撑大层高；同时压暗主背景并降低地表 flank 亮度，减少白色纸底和网页背景冲突。新增静态预览 `Docs/Web/DeepWellAtlas/assets/cross_section/site_cross_section_composite_v6_proportion_grid.png`，并同步更新 `SD_28`、`Docs/Web/DeepWellAtlas/README.md` 和 `assets/cross_section/README.md`。验证：`node --check Docs/Web/DeepWellAtlas/app.js` 通过；Playwright 本地服务器验证控制台 0 errors / 0 warnings，并截取 `output/playwright/deepwellatlas-cross-section-v6-proportion.png`。

2026-06-01 DeepWellAtlas 动效增强：按用户反馈增加网页运行态动效。总剖面新增地表雨雾/档案颗粒漂移、主井扫描光、L3 水面闪动、L5 机械核心脉冲、L6 现实锚阵列脉冲、当前层高亮脉冲；层级地图新增底图呼吸、节点标记/选中反馈等低强度动效，并保留 `prefers-reduced-motion` 降级。为避免未生成的可选 PNG 造成浏览器 404 控制台错误，新增 `layerAssetManifest` 显式清单：当前只请求 S0/L0/L1/L2 已存在的 `base_sketch.png`，`base_render` 和节点组件 PNG 在清单补齐前不会请求。验证：`node --check Docs/Web/DeepWellAtlas/app.js` 通过；Playwright 本地服务器验证总剖面 13 个动画、5 个 motion overlay、控制台 0 errors / 0 warnings，并截取 `output/playwright/deepwellatlas-motion-section.png`；L2 层级地图验证 6 个动画、3 个节点标记、控制台 0 errors / 0 warnings，并截取 `output/playwright/deepwellatlas-motion-layer.png`。

2026-06-01 DeepWellAtlas GSAP 本地依赖：用户要求下载 `ui-ux-pro-max` 和 `Gasp`。本机已存在 `C:\Users\10304\.codex\skills\ui-ux-pro-max`，无需重复安装。已按常见动画库 `GSAP` 处理，从 npm 下载 `gsap@3.15.0`，提取 `gsap.min.js`、`ScrollTrigger.min.js`、上游 README 和 `package.json` 到 `Docs/Web/DeepWellAtlas/vendor/gsap/`，并在 `index.html` 中改为本地脚本接入，不依赖 CDN。上游许可证说明保存在 `vendor/gsap/README.upstream.md` 和 `vendor/gsap/package.json`。验证：`node --check Docs/Web/DeepWellAtlas/app.js` 通过。

2026-06-01 DeepWellAtlas GASP 包下载：用户进一步要求下载 `GASP` 库。npm 上存在 `gasp@0.0.2`，描述为 `Promise based throttling`，不是 GreenSock 动画库。已将 `index.js`、`package.json`、`LICENSE` 和上游 README 保存到 `Docs/Web/DeepWellAtlas/vendor/gasp/`，并新增本地说明 `vendor/gasp/README.md`。当前不在 `index.html` 中加载该包，避免把无关 Node/CommonJS 节流库接入网页运行时。

2026-06-01 DeepWellAtlas UI/UX Pro Max + GSAP 重构：按用户要求使用 `ui-ux-pro-max` 和动画库重构网页。已运行 `ui-ux-pro-max` 设计系统查询，采用“沉浸式交互 + 数据密集 dashboard + OLED 暗色 + 技术化等宽字体 + 克制动效”的本地规则，并新增 `Docs/Web/DeepWellAtlas/DESIGN_SYSTEM.md`。网页视觉从偏棕档案纸感收束为深 slate/OLED 面板、绿色系统状态、琥珀档案强调和分层 accent；新增工具栏 `动效` 开关作为 skip option，并保留 `prefers-reduced-motion`。总剖面运行态动效改由本地 `GSAP 3.15.0` 控制，避免初始 warning；npm `gasp@0.0.2` 仍只作 vendor 备份，不加载到浏览器。验证：`node --check Docs/Web/DeepWellAtlas/app.js` 通过；Playwright 本地服务器验证 `window.gsap.version = 3.15.0`、`gsap-motion = true`、动效开关可关闭为 `motion-disabled = true`，桌面和移动视图控制台均为 0 errors / 0 warnings，并截取 `output/playwright/deepwellatlas-uiux-gsap-section.png` 与 `output/playwright/deepwellatlas-uiux-gsap-mobile.png`。

2026-06-01 DeepWellAtlas 地表 flank 拉伸修正：用户指出总剖面地表左右两侧图片被拉伸。原因是旧 `02_surface_forest.png` 只有顶部 83-433px 有效像素，却在网页中以 `width:196%; height:100%; object-fit:fill` 方式铺满，导致左右地表结构被横向/纵向拉伸。已从源图裁出 `background/02_surface_left_flank.png` 与 `background/02_surface_right_flank.png`，网页改为分别加载左右独立 PNG，并使用 `cross-section-surface-flank` + `object-fit: contain` 锚定到总剖面画框两侧。同步更新 `Docs/Web/DeepWellAtlas/README.md` 与 `assets/cross_section/README.md`。验证：`node --check Docs/Web/DeepWellAtlas/app.js` 通过；Playwright 本地服务器验证 GSAP 正常、2 个 flank 均加载为 `object-fit: contain`、控制台 0 errors / 0 warnings，并截取 `output/playwright/deepwellatlas-surface-flanks-fixed.png`。

2026-06-01 L1 总剖面再次修正：用户指出 L1 层图片仍然整体不对。判断问题不是旧俯视版，而是完整侧剖面在总剖面里读成一个独立多层建筑，并且左侧竖井会干扰主交通结构。已基于 v2 侧剖面裁出 `Docs/Web/DeepWellAtlas/assets/cross_section/components/L1_purification_logistics_v3_cropped_logistics_band.png`，覆盖当前 `L1_purification_logistics.png`；`app.js` 中 L1 的 `piece` 改为横向后勤带显示，并新增单层 `fit: cover` / `position` 支持，避免 L1 拉伸或显得过厚。验证：`node --check Docs/Web/DeepWellAtlas/app.js` 通过；已截取 `output/playwright/deepwellatlas-cross-section-l1-fixed.png`。

## 层级功能玩法与背景故事记录

2026-05-31：新增 `Docs/SystemDesign/SD_33_层级功能玩法与背景故事总览.md`。该文档把 `S0` 和 `L0-L7` 每一层整理为第一版层级设定包，覆盖层级功能、事故前背景、事故后状态、玩家主要玩法、必要路线、深度路线、关键物、S0 解锁、回访价值和结局影响。

设计边界：本文只记录层级玩法和故事方向，不改变当前 Unit 9 / Unit 10 的实现状态，不新增 C++、Blueprint、DataAsset、地图、UI 或美术资产。后续若进入具体层级开发，仍需从 `S0`、`L0` 或 `L1` 开始拆成小单元并完成编辑器验证。

## S0 地表前进站细化记录

2026-05-31：新增 `Docs/SystemDesign/SD_34_S0地表前进站完整设定.md`，作为逐层细化的第一份草案。本文定义 S0 的事故前/事故后背景、空间布局、主电梯、归档台、装备台、医疗净化、仓库、Site 状态图、交通网络、AIC-WATCHMAN 表现、S0 事件、多人成员分工和 UE 后续拆分建议。

设计边界：本文只细化 S0 的设计，不改变当前 Unit 9 / Unit 10 的实现状态，不新增 C++、Blueprint、DataAsset、地图、UI 或美术资产。后续如果进入 S0 实作，需要独立拆出白盒地图、主电梯、归档台或状态图等小开发单元。

## L0 C-12 测试区细化记录

2026-05-31：新增 `Docs/SystemDesign/SD_35_L0_C12测试区完整设定.md`，作为逐层细化的第二份草案。本文定义 L0 的 C-12 身份输入背景、事故后状态、D-Class 开场流程、AIM 初次暴露、主电梯保险件修复、第一批搜索/背包/门禁/战斗/残留教学、必要路线、深度路线、S0 衔接、多人合作和 UE 后续拆分建议。

同步更新：`Docs/SystemDesign/README.md` 已把 `SD_35` 加入当前有效档案结构；`Docs/SystemDesign/SD_32_世界观到细节设定分层索引.md` 已标记 `SD_35` 建立完成，并把下一步建议推进到 `SD_36 L1净化与后勤层完整设定`。

设计边界：本文只细化 L0 的设计，不改变当前 Unit 9 / Unit 10 / Unit 11 / Unit 12 的实现状态，不新增 C++、Blueprint、DataAsset、地图、UI、AI 或美术资产。后续如果进入 L0 实作，需要独立拆出白盒地图、目标链、门禁拒绝原因、搜索容器、主电梯保险件修复、第一敌人或 S0-L0 电梯过渡等小开发单元。

2026-05-31 尺度补充：用户强调 Site 必须保持巨型设施感，后续层级文档必须包含空间结构和尺寸设计。已在 `SD_32_世界观到细节设定分层索引.md` 的层级设定包模板中加入“空间结构与尺寸”，在 `SD_33_层级功能玩法与背景故事总览.md` 中补充“巨型设施尺度”总原则，并在 `SD_35_L0_C12测试区完整设定.md` 中补充 L0 的首版可玩范围、完整事故区、背景设施带和关键空间尺寸建议。

2026-05-31 地下货运隧道补充：用户提出需要在某一层加入用于运送货物和大型设备的地下隧道。已将其定位为由 `L1 净化与后勤层` 控制的 `地下重载货运隧道`，连接 S0 货场、L1 后勤转运站、L3 人工海岸/潜航码头、L5 机械核心外环，并可有封闭支线通往 L6 现实锚维护入口。`SD_33` 已补充 L1 事故前背景、事故后状态、主要空间、深度路线和关键物；`SD_34` 已补充 S0 交通网络；`SD_32` 已把该内容加入 `SD_36 L1净化与后勤层完整设定` 的后续范围。

## L1 净化与后勤层细化记录

2026-05-31：新增 `Docs/SystemDesign/SD_36_L1净化与后勤层完整设定.md`，作为逐层细化的第三份草案。本文定义 L1 的净化、医疗、后勤、维修和大型货运调度职责，细化自动消毒单元、异常医疗舱、空气过滤主机房、后勤仓库、GOC/MC&D 痕迹、地下重载货运隧道、L2/L3 分叉、S0 解锁、回访价值和 UE 后续拆分建议。

同步更新：`Docs/SystemDesign/README.md` 已把 `SD_36` 加入当前有效档案结构；`Docs/SystemDesign/SD_32_世界观到细节设定分层索引.md` 已标记 `SD_36` 建立完成。后续因 Site 器官化真相被确认，L2 详细设定已顺延为 `SD_38 L2生物过滤舱完整设定`。

设计边界：本文只细化 L1 的设计，不改变当前 Unit 9 / Unit 10 / Unit 11 / Unit 12 的实现状态，不新增 C++、Blueprint、DataAsset、地图、UI、AI 或美术资产。后续如果进入 L1 实作，需要独立拆出白盒地图、过滤芯目标链、自动消毒单元、异常医疗舱、搜索容器、重载货运隧道短区段或 L2/L3 分叉门等小开发单元。

2026-05-31 L3 潜水玩法补充：用户确认 `L3 地下海` 必须包含潜水玩法。已在 `SD_33_层级功能玩法与背景故事总览.md` 中补充减压舱/潜水装备间、水下维护通道、短距离水下维修、潜水装具、压力密封模块和氧气补给罐；在 `SD_28_横截面地图美术说明与层级标注规范.md` 的 L3 地图提示词中补充减压舱、潜水装备柜、氧气补给站、系绳潜水路线、水下维护隧道和深潜观察环路线。后续因 `SD_37 Site器官化真相与阶段揭示` 插入为上游文档，L3 详细设定已顺延为 `SD_39 L3地下海完整设定`。

## Site 器官化真相记录

2026-06-01：新增 `Docs/SystemDesign/SD_37_Site器官化真相与阶段揭示.md`。用户确认 Site 深层真相方向：玩家表面看到的是基金会工业收容设施，但后期逐步发现 Site 是用人、异常、机器和档案系统缝合成的巨型消化/代谢器官系统。本文定义 Act 1-5 的阶段揭示：L2 菌丝是死者身份残留构成的皮肤/神经/结缔组织，L3 残留信号是水下声带/神经回声，L5 封印引擎是机械外壳包住的心脏群，L6 现实锚核心材料来自去名化骨性结构，L7 是消化活人身份并生成封印建材的胃/子宫。

同步更新：`Docs/SystemDesign/README.md` 已把 `SD_37` 加入当前有效档案结构；`Docs/SystemDesign/SD_22_深井协议_世界观故事与三结局总纲.md` 已加入深层真相摘要；`Docs/SystemDesign/SD_32_世界观到细节设定分层索引.md` 已把 L2/L3 后续文档顺延到 `SD_38` / `SD_39`；`Docs/SystemDesign/SD_33_层级功能玩法与背景故事总览.md` 已把“器官”从设计比喻升级为后期真相。

设计边界：本文只记录世界观和阶段揭示，不改变当前 Unit 9 / Unit 10 / Unit 11 / Unit 12 的实现状态，不新增 C++、Blueprint、DataAsset、地图、UI、AI 或美术资产。后续进入 L2-L7 详细设计时，必须基于该文档分别拆出工业表象、器官真相、玩家发现顺序和玩法转译。

## Unit 0：编译与启动

目标：
让项目作为 C++ Unreal 项目成功编译，并在默认第一人称地图中使用新的多人准备 C++ GameMode 启动。

非目标：
不做背包 UI、制作、建造、AI、Steam、Epic 或存档系统。

已有实现：
- `CoopSurvival` 运行时模块。
- `CSGameMode`、`CSGameState`、`CSPlayerController`、`CSPlayerState`。
- `CSFirstPersonCharacter`：移动、视角、跳跃、冲刺、下蹲、交互输入。
- `CSSurvivalStatsComponent`：复制的生命、饥饿、口渴、体力。
- `CSInteractionComponent` 和 `CSResourceNode`。
- `Config/DefaultEngine.ini` 中配置默认地图和 GameMode。

验证步骤：
1. 如果 Unreal Editor 正在运行，先关闭。
2. 编译 Editor 目标。
3. 打开项目。
4. 在 `/Game/FirstPerson/Lvl_FirstPerson` 按 Play。
5. 验证 WASD、鼠标视角、Space 跳跃、Left Shift 冲刺、Left Ctrl 下蹲。

历史修复记录：
- 2026-05-28：本机旧 MSVC `14.39.33519` 被 UE 5.8 禁用；之后切到 Visual Studio 2026 / MSVC `14.50.x`。
- 2026-05-28：删除重复 Target 文件，只保留 `CoopSurvival.Target.cs` 和 `CoopSurvivalEditor.Target.cs`。
- 2026-05-28：项目从中文路径迁移到 ASCII 路径 `F:\Unreal\CoopSurvival`，避免 UBT/UBA 参数拆分问题。
- 2026-05-28：项目本地 `Saved/UnrealBuildTool/BuildConfiguration.xml` 禁用 UBA/XGE 远程执行，并把 UBA 存储放到项目目录下。
- 2026-05-28：禁用 `VisualStudioTools` 插件，因为本地插件不兼容 UE 5.8，且只是编辑器工具。
- 2026-05-29：`Lvl_FirstPerson` 原本覆盖了项目默认 GameMode，仍生成模板角色；已通过 `CSGameInstance` 在运行时重定向到 `CSGameMode`。

用户验证结果：
2026-05-29 通过。项目能打开，第一人称地图能 Play，玩家能生成，移动和鼠标视角正常，Space 跳跃、Left Shift 冲刺、下蹲输入可用。下蹲手感和镜头过渡放到 Unit 1。

## Unit 1：单人角色手感

目标：
在 C++ 角色已经成为实际 Pawn 后，调整单人第一人称基础手感。

范围：
- 行走速度、冲刺速度、下蹲速度、加速度、刹车、跳跃手感。
- 第一人称相机高度和下蹲过渡。
- 如有需要，增加临时第三人称/Debug 观察方式。
- 添加基础冲刺和下蹲反馈，不承诺最终美术。

非目标：
不做背包、制作、建造、AI、平台房间、最终动画 polish 或正式 UI。

验证步骤：
1. 在 `/Game/FirstPerson/Lvl_FirstPerson` 按 Play。
2. 验证行走速度可控。
3. 按住 Left Shift，确认冲刺明显更快但不过快。
4. 按住 Left Ctrl，确认视角下降、移动变慢、松开后站起。
5. 确认冲刺和下蹲不会互相冲突。

实现记录：
- 2026-05-29：调整下蹲镜头平滑。下蹲/起身时缓存相机世界高度，避免胶囊高度瞬变直接传到镜头，再在 Tick 中缓动到目标高度。
- `CrouchCameraInterpSpeed` 当前默认值为 `3.0`。
- 下蹲键从 `C` 改为 `Left Ctrl`。
- 创建蓝图调参路径：`Content/Blueprints/Characters/BP_PlayerCharacter` 继承 `ACSFirstPersonCharacter` 后，可在蓝图默认值里调整 `Movement`、`Camera`、`Survival` 分类参数。

用户验证结果：
2026-05-29 通过。第一人称移动和下蹲手感足够进入下一单元。后续小幅调参可通过 `BP_PlayerCharacter` 默认值继续做，不重开本单元。

## Unit 2：基础交互

目标：
建立第一个服务器拥有的交互闭环：看准资源点按 `E`，获得玩家状态中记录的临时资源。

范围：
- 在第一人称测试地图中确认或放置一个简单资源节点。
- 使用玩家拥有的角色/组件发起交互 Trace。
- 交互请求进入服务器逻辑。
- 在 `CSPlayerState` 上发放临时木头/石头资源。
- 通过日志或屏幕 Debug 给出确认。

非目标：
不做完整背包格子、拾取/丢弃物品 Actor、制作、建造、背包 UI、资源枯竭美术，除了保持服务器权威外不做多人 polish。

验证步骤：
1. 在 `/Game/FirstPerson/Lvl_FirstPerson` 按 Play。
2. 看向测试资源节点。
3. 按 `E`。
4. 确认每次有效交互只增加一次服务器拥有的资源数量。
5. 确认看向别处或距离过远不会获得资源。
6. 如可行，用 Listen Server + 一个客户端验证客户端请求由服务器确认。

实现记录：
- 2026-05-29：`CSInteractionComponent` 增加本地 Focus Trace 和 Debug 线/球。绿色表示命中可交互物，黄色表示命中不可交互物，红色表示没命中。
- 2026-05-29：`CSInteractable` 增加 Focus 事件；`CSResourceNode` Focus 时显示高亮。
- 2026-05-29：服务器交互验证从“必须完全命中同一个 Actor”放宽为距离、视角角度和遮挡校验，减少客户端/服务器视角差导致的误拒绝。
- 2026-05-29：资源获取和交互失败原因通过 `CSPlayerController` 显示到屏幕。
- 2026-05-29：Focus Trace 不再每帧执行，默认 `1/30` 秒一次，最小间隔 `1/60` 秒；只有本地控制 Pawn 做 Focus Trace。
- 2026-05-29：放弃脏的屏幕空间描边方案，改用资源节点上的 `FocusHighlightMeshComponent` 作为稍微放大的物体外壳高亮，并使用 `/Game/Materials/Interaction/M_InteractObjectOutline`。
- 2026-05-29：安装并兼容修复 `VibeUE` 插件；`StructUtils` 仍有弃用警告，但不影响编译。

用户验证结果：
2026-05-29 通过。按 `E` 交互资源节点能获得服务器拥有的资源，失败原因和 Focus/Debug 反馈足够关闭本单元。后续交互 UI、背包接入或多人 polish 放到后续单元。

## Unit 3：本地多人

目标：
使用 `OnlineSubsystemNull` 做 Steam-like 开发期房间流程：创建 LAN 房间、搜索房间、按索引加入，并验证两个玩家在同一张图中同步。

范围：
- 新增项目自己的平台服务 Subsystem，封装 UE Online Session。
- 后端保持 `OnlineSubsystemNull`。
- 用控制台命令临时完成开房、搜房、加入。
- 验证 Listen Server + 一个客户端。

非目标：
不接真实 Steam AppID，不做 Steam Overlay、好友邀请、Epic/EOS 登录、正式 Lobby UI、匹配筛选或 Dedicated Server。

验证步骤：
1. 主机在编辑器打开 `/Game/FirstPerson/Lvl_FirstPerson`。
2. 控制台执行 `HostCoop`。
3. 第二台机器或第二个进程运行同一 Windows Development 客户端。
4. 控制台执行 `FindCoop`。
5. 确认主机房间显示为索引 `0`，有玩家数和 Ping。
6. 执行 `JoinCoop 0`。
7. 确认双方进入同一张图，并能看到彼此移动、跳跃、冲刺、下蹲状态。
8. 客户端按 `E` 采集资源，确认只增加客户端自己的服务器拥有资源。

实现记录：
- 2026-05-29：新增 `UCSPlatformServiceSubsystem`，封装 `CreateSession`、`FindSessions`、`JoinSession`、`DestroySession`。
- `HostCoop` 创建 4 人 LAN Session 并以 `listen` 打开当前地图。
- `FindCoop` 搜索 LAN 房间并打印索引结果。
- `JoinCoop 0` 按缓存搜索结果加入并 Travel 到解析出的地址。
- `DestroyCoopSession` 用于重复测试清理房间。
- `CoopSurvival.Build.cs` 增加 `OnlineSubsystem`、`OnlineSubsystemUtils`。
- 2026-05-29：打包 Windows Development 成功，归档到 `Saved/Packages/Win64`。
- 2026-05-29：Session 接口改用 `Online::GetSessionInterface(GetWorld())`，避免 PIE、Standalone、Package 使用不同 OnlineSubsystem World Context。
- 2026-05-29：第三人称角色网格临时绑定 `/Game/Characters/Mannequins/Anims/Unarmed/ABP_Unarmed`，解决远端角色滑行无动画的问题。

编译结果：
2026-05-29 通过。`CoopSurvivalEditor Win64 Development` 和 `CoopSurvival Win64 Development` 均编译成功。

用户验证结果：
2026-05-29 通过。同机多人可进入同一 Session，移动和状态复制可见。基础移动动画存在。第三人称下蹲动画仍不完整，但玩法下蹲状态可用，作为后续动画 polish，不阻塞 Unit 3。

## Unit 4：最小背包数据

目标：
用一个服务器拥有的最小背包数据模型替代临时 `WoodCount` / `StoneCount` 字段，为拾取、丢弃、制作和后续消耗打基础。

范围：
- 玩家拥有一个最小复制背包容器。
- 物品用 ItemId + Quantity 的栈表示。
- 资源获取走服务器背包函数。
- 保留临时 Debug 输出显示背包内容。

非目标：
不做背包 UI、拖拽、快捷栏、装备槽、世界物品 Actor、持久化、制作或重量系统。

验证步骤：
1. 在 `/Game/FirstPerson/Lvl_FirstPerson` 按 Play。
2. 与木头资源节点交互。
3. 确认服务器拥有的背包中增加 `Wood` 栈。
4. 重复交互，确认同一个栈的数量增加。
5. 如可行，用 Listen Server + 一个客户端确认客户端采集只更新自己的背包 Debug。

实现记录：
- 2026-05-29：`ACSPlayerState` 新增复制数组 `InventoryStacks`，元素为 ItemId + Quantity。
- 移除旧的 `WoodCount` 和 `StoneCount` 复制字段。
- `AddResource()` 保留为资源节点兼容入口，但内部转到通用服务器函数 `AddItem()`。
- Debug 改成 `Inventory | Wood: N`。
- Inventory Debug 增加玩家标签，例如 `[P256 Server]`、`[P257 Client]`，并为固定行使用玩家相关 key，方便同进程 PIE 区分输出。

编译结果：
2026-05-29：`CoopSurvival Win64 Development` 编译成功。Editor 目标曾被 Live Coding 阻止，需关闭 Editor 或按 `Ctrl+Alt+F11` 后再外部编译。

用户验证结果：
2026-05-29 通过。资源采集更新每个玩家自己的物品栈，重复采集增加同一栈数量，Debug 玩家标签足以区分同进程多人测试输出。

## Unit 5：拾取与丢弃

目标：
实现世界物品 Actor 的拾取和丢弃，确保多人下不会重复复制物品。

范围：
- 新增一个最小复制世界物品 Actor，包含 ItemId 和 Quantity。
- 复用现有 `E` 交互路径拾取。
- 丢弃时从服务器背包扣除物品。
- 在正式背包 UI 前，使用临时控制台命令丢弃物品。
- 保留拾取、丢弃和失败路径的屏幕 Debug。

非目标：
不做背包 UI、拖拽格子、快捷栏、装备、正式 ItemData/DataAsset、重量、图标、物品持久化或正式美术。

验证步骤：
1. 在 `/Game/FirstPerson/Lvl_FirstPerson` 按 Play。
2. 至少采集一个 `Wood`。
3. 打开控制台执行 `DropItem Wood 1`。
4. 确认背包 Debug 中 `Wood` 减少。
5. 看向掉落的方块并按 `E`。
6. 确认掉落 Actor 消失，自己的背包栈把物品加回来。
7. 如可行，用 Listen Server + 一个客户端确认同一个掉落物只能被一个玩家拾取，另一个玩家不会获得重复物品。

实现记录：
- 2026-05-29：新增 `ACSWorldItemActor`，位置在 `Source/CoopSurvival/Items`。
- 世界物品复制 `ItemId` 和 `Quantity`，实现 `ICSInteractable`，并复用资源节点的物体外壳高亮反馈。
- 只有服务器成功把物品加入交互玩家背包后，世界物品才销毁。
- `ACSPlayerState` 增加服务器函数 `RemoveItem()` 和 `HasItemQuantity()`。
- `ACSPlayerController` 增加临时控制台命令 `DropItem <ItemId> <Quantity>`，服务器校验后从背包扣除并在玩家前方生成复制世界物品。

编译结果：
2026-05-29：`CoopSurvival Win64 Development` 和 `CoopSurvivalEditor Win64 Development` 均编译成功。

用户验证结果：
2026-05-29 通过。玩家可用 `DropItem Wood 1` 丢出采集到的 `Wood`，再按 `E` 拾回，背包 Debug 正确更新。

## Unit 6：第一个合成配方

目标：
服务器验证材料后，从采集资源中合成一个基础物品。

范围：
- 新增一个临时合成配方。
- 在服务器检查材料是否足够。
- 扣除材料并通过现有背包模型发放产物。
- 在正式 UI 前继续使用控制台命令和屏幕 Debug。
- 世界物品和合成产物先使用方块占位视觉。

非目标：
不做制作 UI、工作台限制、建筑菜单、物品图标、配方 DataAsset、科技树、制作队列/计时或最终物品模型。

验证步骤：
1. 在 `/Game/FirstPerson/Lvl_FirstPerson` 按 Play。
2. 采集 `Wood x3`。
3. 打开控制台执行 `CraftItem Axe`。
4. 确认 Debug 显示 `Wood -3`、`Axe +1`、`Crafted Axe x1`。
5. 执行 `DropItem Axe 1`。
6. 确认掉落的 `Axe` 使用深色细长方块占位。
7. 按 `E` 拾回掉落的 `Axe`，确认背包栈再次增加。
8. 如可行，用 Listen Server + 一个客户端确认客户端合成请求只在服务器验证后修改该客户端自己的背包。

实现记录：
- 2026-05-29：`ACSPlayerController` 新增临时控制台命令 `CraftItem Axe`。
- 命令通过拥有者 Server RPC 执行。
- 当前硬编码配方为 `Wood x3 -> Axe x1`。
- 成功时从服务器背包扣 `Wood x3`，加 `Axe x1`。
- 如果加产物失败，会回滚已扣除的木头。
- 未知配方和材料不足会显示明确 Debug 失败原因。
- `ACSWorldItemActor` 新增物品占位视觉规则：`Wood` 为棕色长条方块，`Stone` 为灰色矮方块，`Axe` 为深色细长方块。规则暴露在 `Item|Placeholder Visual`，后续蓝图可调。

编译结果：
2026-05-29：`CoopSurvival Win64 Development` 和 `CoopSurvivalEditor Win64 Development` 均编译成功。

用户验证结果：
2026-05-29 通过。玩家可用 `CraftItem Axe` 将 `Wood x3` 合成为 `Axe x1`，并可丢弃、拾取这个斧子占位物。

## Unit 7：场景站点交互

目标：
用场景提供的工作台/升级台交互替代原先的自由建造步骤。当前 MVP 不包含墙/地板放置、建造预览、玩家基地或通用建造系统。

范围：
- 新增一个场景放置的站点 Actor，例如 `Workbench` 或 `UpgradeStation`。
- 复用现有 `E` 交互路径和物体高亮反馈。
- 在服务器验证站点距离和站点类型。
- 至少让一个动作通过站点校验，例如只有靠近 `Workbench` 才能制作 `Axe`，或先做一个临时升级动作。
- 在正式站点 UI 前继续使用控制台和 Debug 输出。

非目标：
不做自由建造、墙/地板放置、Ghost Preview、支撑检测、建造权限、基地归属或正式站点 UI。

设计更新：
2026-05-29：用户明确装备台、升级地点和类似玩法点由场景提供，不做玩家自由建造系统。`AGENTS.md` 和 `Docs/GameDesign_v0.1.md` 已标记自由建造不在当前 MVP 范围内。

实现更新：
2026-05-29：新增 `ACSWorkbenchStation`，位置为 `Source/CoopSurvival/World`。它是场景放置的可交互站点 Actor，默认 `StationId=Workbench`，默认 `CraftRecipeId=Axe`，拥有服务器侧 `UseRadius`、复制的启用状态、基础工作台占位视觉，以及与资源点/掉落物一致的物体描边反馈。按 `E` 交互会继续走 `CSInteractionComponent` 的服务器校验，并在服务器上尝试合成 `Axe`。

2026-05-29：`CraftItem Axe` 现在必须由服务器找到附近启用的 `Workbench` 才能成功。服务器会验证站点类型和玩家到站点的距离，然后才扣 `Wood x3` 并添加 `Axe x1`。远离工作台或地图里没有可用工作台时，合成失败并提示 `Axe requires a nearby Workbench`。

蓝图/地图更新：
2026-05-29：已创建 `/Game/Blueprints/Interactables/BP_Workbench`，父类为 `ACSWorkbenchStation`，后续关卡可以直接拖放这个蓝图。测试地图 `/Game/FirstPerson/Lvl_FirstPerson` 中已有一个 `Unit7_Workbench` 实例。

编译结果：
2026-05-29：`CoopSurvivalEditor Win64 Development` 和 `CoopSurvival Win64 Development` 均编译成功。

建议验证步骤：
1. 打开 `/Game/FirstPerson/Lvl_FirstPerson`。
2. 在测试区确认有场景放置的 `Unit7_Workbench`，也可以从 `/Game/Blueprints/Interactables/BP_Workbench` 拖放新的工作台。
3. 看准工作台按 `E`，确认高亮，并在材料足够时直接尝试合成 `Axe`。
4. 远离工作台执行 `CraftItem Axe`，应失败并提示需要附近的 `Workbench`。
5. 采集 `Wood x3`，站到工作台附近按 `E`，或执行 `CraftItem Axe`。
6. 确认服务器扣 `Wood x3`、加 `Axe x1`，并显示 `Crafted Axe x1 at ...`。
7. 多人下确认客户端只能使用自己距离内的有效工作台，不能远程调用场景站点。

用户验证结果：
2026-05-29 通过。工作台场景交互、材料采集和 `Axe` 合成验证通过。

## Unit 8：最小存档

目标：
实现一个服务器权威的最小 SaveGame 流程，能保存和读取玩家位置、玩家背包、世界掉落物，以及场景工作台的基础状态。

范围：
- 新增版本化 SaveGame 数据结构。
- 临时使用控制台命令触发保存和读取。
- 保存当前连接玩家的 Transform 和 InventoryStacks。
- 保存世界中的 `ACSWorldItemActor` 掉落物。
- 保存场景 `ACSWorkbenchStation` 的 `PersistentId`、Transform、StationId 和启用状态。
- 保持服务器执行真实写入和恢复，客户端只发送请求。

非目标：
不做正式存档 UI、存档列表、缩略图、手动命名、多槽位选择、云存档、资源节点刷新存档、旧版本复杂迁移、存档加密或跨地图完整迁移。

实现更新：
2026-05-29：新增 `UCSWorldSaveGame`，位置为 `Source/CoopSurvival/World`。当前 `SaveVersion=1`，包含玩家数据、世界掉落物数据和工作台站点数据。

2026-05-29：`ACSPlayerController` 新增临时控制台命令 `SaveCoop` 和 `LoadCoop`。客户端执行时会走 Server RPC，由服务器调用 `SaveGameToSlot` / `LoadGameFromSlot`。默认槽位为 `CoopSurvival_DevWorld`。

2026-05-29：`ACSPlayerState` 新增 `SetInventoryStacks()`，读档时由服务器恢复背包数组并推送 Debug 背包显示。

2026-05-29：`ACSWorkbenchStation` 新增 `PersistentId` 和 `SetStationEnabled()`。当前默认 `PersistentId=Workbench_001`，用于 Unit 8 保存测试地图中的单个工作台状态。

2026-05-29：新增临时调试命令 `SetNearestWorkbenchEnabled 0/1`，用于验证工作台启用状态能被保存和读取。

编译结果：
2026-05-29：`CoopSurvivalEditor Win64 Development` 和 `CoopSurvival Win64 Development` 均编译成功。

建议验证步骤：
1. 打开 `/Game/FirstPerson/Lvl_FirstPerson`，使用单人 PIE 或 Listen Server + 1 Client。
2. 采集 `Wood x5`，靠近工作台按 `E` 合成 `Axe`。
3. 执行 `DropItem Axe 1`，让地图上出现一个 `Axe` 掉落物。
4. 站到一个明显位置，执行 `SaveCoop`，应看到保存成功 Debug，包含 players/items/workbenches 数量。
5. 移动到别处，拾取或制造更多物品，改变当前状态。
6. 执行 `LoadCoop`，确认玩家位置回到保存点，背包恢复，掉落物恢复。
7. 可选：靠近工作台执行 `SetNearestWorkbenchEnabled 0`，再执行 `SaveCoop`；重新执行 `SetNearestWorkbenchEnabled 1` 后 `LoadCoop`，确认工作台恢复为禁用状态。
8. 多人下确认客户端执行 `SaveCoop` / `LoadCoop` 会由服务器恢复当前连接玩家和世界掉落物。

用户验证结果：
2026-05-29 通过。玩家位置、背包、掉落物和工作台状态的最小保存/读取流程验证可用。

## Unit 9：系统化最小 HUD

目标：
先做一个简单、可扩展的运行时 HUD 骨架，替代只靠屏幕 Debug 看状态的方式。UI 先显示关键状态，不做完整菜单。

范围：
- 建立统一 HUD 数据快照，供当前 Slate HUD 和后续 UMG 蓝图复用。
- 本地玩家自动创建 HUD。
- 显示生存状态：Health、Hunger、Thirst、Stamina。
- 显示世界时间：Day 和小时分钟。
- 显示当前背包摘要。
- 继续保持真实状态来自服务器复制数据，UI 只读取和表现。

非目标：
不做暂停菜单、正式背包格子、制作界面、拖拽、物品图标、快捷栏、设置页、主菜单、平台大厅 UI 或美术定稿。

实现更新：
2026-05-29：新增 `FCSHUDSnapshot`，位置为 `Source/CoopSurvival/UI/CSHUDTypes.h`。它统一承载 HUD 需要的生存状态、世界时间和背包摘要。

2026-05-29：新增 `SCSHUDWidget`，位置为 `Source/CoopSurvival/UI`。当前用 Slate 做最小 HUD，左上显示 Day/Time 和四条状态条，左下显示 Inventory 摘要。

2026-05-29：`ACSPlayerController` 新增 `BuildHUDSnapshot()`，本地控制器在 `BeginPlay()` 创建 HUD，在 `EndPlay()` 移除 HUD。HUD 读取复制后的 `ACSGameState`、`ACSPlayerState` 和 `UCSSurvivalStatsComponent`。

编译结果：
2026-05-29：外部编译最初被当前打开的 Unreal Editor Live Coding 阻止。关闭编辑器后，`CoopSurvivalEditor Win64 Development` 和 `CoopSurvival Win64 Development` 均编译成功。

建议验证步骤：
1. 打开 `/Game/FirstPerson/Lvl_FirstPerson`，启动 PIE。
2. 左上应看到 Day/Time、Health、Hunger、Thirst、Stamina。
3. 左下应看到 Inventory 摘要。
4. 采集木头、合成或丢弃物品后，确认 Inventory 摘要跟随背包变化。
5. 冲刺消耗体力时，确认 Stamina 条变化。

用户验证结果：
待验证。

## Unit 10：背包与合成台 UI

目标：
在现有系统化 HUD 数据流上增加一个最小但可继续扩展的操作界面：玩家可按 `B` 打开背包，靠近工作台按 `E` 打开合成台界面，查看配方、当前背包和材料需求，并通过服务器计时制作。

范围：
- `B` 键打开/关闭背包面板。
- 背包面板显示当前物品、数量和临时生成的色块/字母图标。
- 背包内支持 `Drop 1` 和 `Equip` 操作。
- 当前装备物品复制到客户端，并在 HUD/背包面板显示。
- 工作台交互从“直接制作”改为“打开合成台界面”。
- 合成台显示配方、输出物、制作时间、当前材料数量和需求数量。
- 制作按钮走服务器 RPC，服务器验证附近工作台、材料、已有制作状态；开始时扣材料，计时完成后给产物。

非目标：
不做拖拽格子、快捷栏、装备模型上手、正式物品图标资产、多个配方 DataAsset、制作队列、取消返还材料、正式音效、正式美术或复杂容器 UI。

实现更新：
2026-05-29：`FCSHUDSnapshot` 增加 `InventoryStacks`、`EquippedItemId`、面板打开状态、可用配方和服务器复制的 `FCSCraftingState`。

2026-05-29：`ACSPlayerState` 新增复制字段 `EquippedItemId` 和服务器侧 `EquipItem()`。当前只有 `Axe` 标记为可装备，后续武器/工具系统会接管具体表现。

2026-05-29：新增 `SCSInventoryPanelWidget`，位置为 `Source/CoopSurvival/UI`。它显示背包、装备、工作台配方、材料需求和制作进度，并提供 `Equip`、`Drop 1`、`Craft` 按钮。

2026-05-29：`ACSPlayerController` 绑定 `B` 打开背包，`Esc` 关闭面板；新增 `ClientOpenCraftingStation()`、`RequestCraftItem()`、`RequestEquipItem()` 和面板输入模式切换。

2026-05-29：`ACSWorkbenchStation` 的 `E` 交互改为打开合成台界面，不再直接制作。

2026-05-29：修正制作台界面文字重叠：背包/制作面板打开时隐藏底层 HUD 和屏幕 Debug；配方卡片改为标题与制作按钮一行、材料需求单独一行，并扩大面板尺寸和启用材料文本换行。

2026-05-29：修正制作后面板按钮无响应风险：制作进度的剩余时间不再触发背包/配方列表每帧重建，进度文本和进度条继续通过绑定实时刷新；背包/制作面板现在支持键盘焦点，并可用 `Esc` 或 `B` 直接关闭；打开面板时显式把输入焦点交给面板。

编译结果：
2026-05-29：`CoopSurvival Win64 Development` 编译成功。`CoopSurvivalEditor Win64 Development` 当前被打开的 Unreal Editor / Live Coding 阻止，需要关闭编辑器或按 `Ctrl+Alt+F11` 后再验证。

建议验证步骤：
1. 编译 Editor 目标后打开 `/Game/FirstPerson/Lvl_FirstPerson`。
2. PIE 中按 `B`，应打开背包面板；`Esc` 或 Close 关闭。
3. 采集木头后按 `B`，背包应显示 `Wood`、数量和临时图标。
4. 背包中点击 `Drop 1`，服务器扣除物品并在玩家前方生成掉落物。
5. 采集 `Wood x3`，靠近工作台按 `E`，应打开工作台界面。
6. 工作台界面应显示 `Stone Axe`、`Wood 当前/需求` 和 `Craft 4s`。
7. 点击 `Craft` 后材料立即扣除，进度条开始；约 4 秒后背包获得 `Axe x1`。
8. 在背包中点击 `Axe` 的 `Equip`，HUD 右下和背包顶部应显示当前装备为 `Axe`。
9. 多人下客户端按钮请求仍应由服务器验证，远离工作台不能开始制作。

用户验证结果：
待验证。

## Unit 11：表格驱动物品与配方

目标：
把当前 `Wood`、`Stone`、`Axe` 和 `Axe` 配方从 C++ 硬编码迁到开发期表格配置。背包、合成、装备继续只认 `ItemId` / `RecipeId`，显示名、类型、图标标识、图标色、最大堆叠、是否可装备、制作时间和材料需求从表格生成数据里查。

范围：
- 新增 `Data/Source/Items.csv`、`Data/Source/Recipes.csv`、`Data/Source/RecipeRequirements.csv` 作为开发期源表。
- 新增 `Scripts/GenerateGameData.py`，校验表格引用关系并生成 `Content/Data/Generated/*.csv`。
- 新增 `FCSItemDefinition`、`FCSRecipeDefinition`、`FCSRecipeRequirement` 数据结构。
- 新增 `UCSItemDataSubsystem`，统一加载生成后的项目数据并提供查询接口。
- 合成逻辑、可用配方、装备校验和背包 UI 显示改为查 `UCSItemDataSubsystem`。

非目标：
不做 WPS/Excel 运行时读取，不依赖玩家电脑办公软件；不做完整 `UDataTable` uasset 自动导入，不做正式图标纹理，不做复杂配方队列、科技树、掉落表或容器表。当前生成后的 CSV 是编辑器开发期运行数据，后续发布打包前应接 `UDataTable` 导入或显式配置非资产数据打包。

实现更新：
2026-05-31：新增 `Source/CoopSurvival/Items/CSItemDataTypes.h`，把物品和配方结构从 UI 层抽到 Items 数据层。

2026-05-31：新增 `Source/CoopSurvival/Items/CSItemDataSubsystem.*`。它在 GameInstance 生命周期加载 `Content/Data/Generated/Items.csv`、`Recipes.csv` 和 `RecipeRequirements.csv`，并提供 `GetItemDefinition()`、`GetRecipeDefinition()`、`GetRecipesForStation()`、`IsItemEquipable()`、`GetItemDisplayName()` 和图标颜色/标识查询。

2026-05-31：移除 `ACSPlayerController` 中的 `MakeAxeRecipe()` 硬编码配方。`CraftItem Axe`、工作台 UI 可用配方、材料需求和制作时间现在来自生成数据。

2026-05-31：`ACSPlayerState::EquipItem()` 不再硬编码只有 `Axe` 可装备，而是查物品表里的 `bCanEquip`。

2026-05-31：`SCSInventoryPanelWidget` 的物品显示名、临时图标字母、临时图标颜色和 `Equip` 按钮启用状态改为查物品表。

2026-05-31：`ACSPlayerState::GetInventoryDebugString()` 和 `SCSHUDWidget` 当前装备显示也改为优先使用物品表显示名，避免 HUD/Debug 与背包面板显示不一致。

2026-05-31：新增源表：
- `Data/Source/Items.csv`
- `Data/Source/Recipes.csv`
- `Data/Source/RecipeRequirements.csv`

2026-05-31：运行 `python Scripts\GenerateGameData.py` 成功，生成：
- `Content/Data/Generated/Items.csv`
- `Content/Data/Generated/Recipes.csv`
- `Content/Data/Generated/RecipeRequirements.csv`

编译结果：
2026-05-31：`CoopSurvival Win64 Development` 编译成功；`CoopSurvivalEditor Win64 Development` 编译成功。

建议验证步骤：
1. 打开 `/Game/FirstPerson/Lvl_FirstPerson`。
2. 采集 `Wood x3`，靠近工作台按 `E`。
3. 工作台界面应显示表格中的 `Stone Axe`、`Wood 3/3` 和 `Craft 4s`。
4. 点击制作，约 4 秒后获得 `Axe x1`。
5. 在背包里确认 `Axe` 可装备，`Wood` 不可装备。
6. 修改 `Data/Source/RecipeRequirements.csv` 中 `Axe,Wood,3` 为 `Axe,Wood,2`。
7. 运行 `python Scripts\GenerateGameData.py`，重新打开/运行游戏，确认 UI 和服务器合成需求都变成 `Wood x2`。

用户验证结果：
待验证。

## Unit 12：平台配置分层与本地房间入口

目标：
继续使用 `OnlineSubsystemNull` 做本地/LAN 联机验证，但把房间系统入口和平台差异点收敛到 `CSPlatformServiceSubsystem`。当前玩法代码仍只调用项目自己的 `HostCoop`、`FindCoop`、`JoinCoop`、`DestroyCoopSession`，后续 Steam/EOS 切换不应侵入背包、合成、角色或 UI 玩法逻辑。

范围：
- 保留当前 Listen Server 架构。
- 保留当前 `OnlineSubsystemNull` 和 LAN Session 作为开发期默认。
- `CSPlatformServiceSubsystem` 增加后端名、LAN/Internet 模式、最大人数、搜索结果数量和忙碌状态查询。
- `CSPlatformServiceSubsystem` 使用配置项控制 `bUseLanSessions` 和 `MaxPublicConnections`，不再把 LAN 和 4 人写死在创建/搜索逻辑内部。
- 增加开发期房间面板，按 `F3` 打开，支持 `Host`、`Find`、`Join 0`、`Destroy`、`Close`。
- 新增平台配置 profile 占位，给后续打包 Profile 或平台配置切换使用。

非目标：
不接真实 Steam AppId，不接 EOS 登录/产品配置，不做公网 NAT 穿透，不做好友邀请，不做正式大厅 UI，不做 Dedicated Server，不做跨城市自动房间发现。当前仍是本地/LAN 开发架构。

实现更新：
2026-05-31：`UCSPlatformServiceSubsystem` 增加配置项：
- `bUseLanSessions`
- `MaxPublicConnections`

2026-05-31：`UCSPlatformServiceSubsystem` 增加查询接口：
- `GetOnlineBackendName()`
- `IsUsingLanSessions()`
- `GetMaxPublicConnections()`
- `GetLastSearchResultCount()`
- `IsSessionOperationInProgress()`

2026-05-31：Session 创建和搜索现在读取 `bUseLanSessions` 与 `MaxPublicConnections`。当前默认仍是 Null + LAN + 4 人。

2026-05-31：新增 `SCSCoopSessionPanelWidget`，位置为 `Source/CoopSurvival/UI`。本地玩家按 `F3` 可打开开发期房间面板；面板显示当前后端、LAN/Internet 模式、最大人数、搜索结果数量和忙碌状态，并可触发 Host/Find/Join 0/Destroy。

2026-05-31：`ACSPlayerController` 增加 Coop 面板创建/移除、`F3` 切换和 `Esc` 关闭当前面板。背包/制作面板与 Coop 面板互斥，打开其中一个会关闭另一个。

2026-05-31：新增配置：
- `Config/DefaultGame.ini` 中的 `[/Script/CoopSurvival.CSPlatformServiceSubsystem]`
- `Config/Profiles/Online_Null.ini`
- `Config/Profiles/Online_Steam.placeholder.ini`
- `Config/Profiles/Online_EOS.placeholder.ini`

编译结果：
2026-05-31：`CoopSurvival Win64 Development` 编译成功；`CoopSurvivalEditor Win64 Development` 编译成功。

建议验证步骤：
1. 打开 `/Game/FirstPerson/Lvl_FirstPerson`。
2. PIE 中按 `F3`，应打开 `Coop Session` 面板。
3. 面板应显示 `Backend: Null`、`Mode: LAN`、`Max Players: 4`。
4. 点击 `Host`，应创建 LAN session 并以 listen server 重新打开当前地图。
5. 第二个客户端/窗口按 `F3`，点击 `Find`，应找到 LAN session。
6. 点击 `Join 0`，应加入第一个搜索结果。
7. 主机点击 `Destroy` 或执行 `DestroyCoopSession`，应销毁当前 session。

用户验证结果：
待验证。

## Unit 13：S0/L0 测试地图结构

目标：
建立项目自己的 S0/L0 测试地图结构，不再把后续关卡工作继续堆在模板 `/Game/FirstPerson/Lvl_FirstPerson` 里。当前仍保留 `Lvl_FirstPerson` 作为系统验证入口，S0/L0 先用于关卡加载、切层、资源摆放和后续 AI 测试。

范围：
- 新增 `/Game/Maps/S0`、`/Game/Maps/L0` 和 `/Game/Maps/Shared`。
- 新增 S0 与 L0 两张最小灰盒测试地图。
- 新增交互物 Blueprint 分类目录：资源、站点、层级切换。
- 新增关卡目录说明文档。
- 保持旧模板地图原位置，不移动现有资产。

非目标：
不做正式 S0/L0 美术，不做电梯切层 Actor，不做 AI，不做资源蓝图子类，不改默认启动地图，不改变当前房间系统默认的 `Lvl_FirstPerson`。

实现更新：
2026-05-31：新增 Unreal Python 脚本 `Scripts/CreateS0L0TestMaps.py`，用于创建项目地图目录和两张测试地图。脚本可重复运行；已有地图会跳过，不覆盖关卡编辑结果。

2026-05-31：新增目录：
- `/Game/Maps/S0`
- `/Game/Maps/L0`
- `/Game/Maps/Shared`
- `/Game/Blueprints/Interactables/Resources`
- `/Game/Blueprints/Interactables/Stations`
- `/Game/Blueprints/Interactables/Travel`
- `/Game/Blueprints/World`
- `/Game/Dev/Maps`

2026-05-31：新增测试地图：
- `/Game/Maps/S0/Lvl_S0_ForwardStation_Test`
- `/Game/Maps/L0/Lvl_L0_C12_TestArea_Test`

2026-05-31：S0 测试图包含 `PlayerStart_Main`、基础光照、主平台、指挥棚、工作台区、医疗净化区、仓库区和通往 L0 的主电梯占位块。L0 测试图包含 `PlayerStart_Main`、入口走廊、C-12 测试房、观察室、资源凹室、返回 S0 的损坏电梯占位块、封锁门和资源节点占位块。

2026-05-31：新增 `Docs/LevelDesign/S0_L0_TestMaps.md`，记录地图目录、用途和控制台验证命令。

验证结果：
2026-05-31：运行 `UnrealEditor-Cmd.exe ... -run=pythonscript -script=Scripts/CreateS0L0TestMaps.py` 成功，脚本日志显示目录已创建、两张地图已保存。

2026-05-31：无渲染命令行加载验证通过：
- `/Game/Maps/S0/Lvl_S0_ForwardStation_Test`：日志显示 `Load map complete`。
- `/Game/Maps/L0/Lvl_L0_C12_TestArea_Test`：日志显示 `Load map complete`。

建议验证步骤：
1. 在 Content Browser 中打开 `/Game/Maps/S0/Lvl_S0_ForwardStation_Test`。
2. 确认能看到 S0 灰盒平台、工作台区、医疗区、仓库区和主电梯占位。
3. 按 Play，确认玩家从 `PlayerStart_Main` 生成。
4. 打开 `/Game/Maps/L0/Lvl_L0_C12_TestArea_Test`。
5. 确认能看到 L0 入口走廊、C-12 测试房、资源凹室和返回电梯占位。
6. 按 Play，确认玩家从 `PlayerStart_Main` 生成。
7. 可选：用控制台执行 `open /Game/Maps/S0/Lvl_S0_ForwardStation_Test` 和 `open /Game/Maps/L0/Lvl_L0_C12_TestArea_Test` 做手动切图验证。

用户验证结果：
待验证。

## Unit 14：持久电梯与 S0/L0 Streaming

目标：
验证类似 Unity Persistent Scene + Additive Scene 的加载模式：电梯放在常驻 Persistent Map 中，S0/L0 作为 Streaming Level，被电梯交互控制加载和卸载。

范围：
- 新增一个 C++ 常驻电梯 Actor，复用 `ICSInteractable` 和现有 `E` 交互路径。
- 电梯交互由服务器验证，电梯移动和当前层状态复制。
- 电梯在 S0 与 L0 的 Z 高度之间移动。
- 服务器通过 Multicast 在所有客户端加载目标 Streaming Level、卸载旧层。
- 新增 `/Game/Maps/Site/Lvl_Site_Persistent_Test`。
- 将 S0/L0 测试地图注册为 Persistent Map 的 Streaming Level。
- 调整 L0 测试图到 S0 下方高度，便于同一个电梯在两层之间移动。

非目标：
不做正式电梯舱美术、声音、UI 黑屏过渡、多人全员确认、电梯门动画、跨层存档恢复、AI 暂停/恢复或正式任务流。

实现更新：
2026-05-31：新增 `ACSStreamingElevatorStation`：
- 文件：`Source/CoopSurvival/World/CSStreamingElevatorStation.h`
- 文件：`Source/CoopSurvival/World/CSStreamingElevatorStation.cpp`

当前行为：
- 默认当前层为 `S0`。
- `BeginPlay` 确保 S0 loaded/visible，L0 unloaded。
- 玩家看准电梯按 `E`，服务器验证距离和电梯空闲状态。
- 电梯开始向目标层移动，并 Multicast 加载目标层。
- 到达后更新 `CurrentLayer`，Multicast 卸载旧层。
- 电梯 Actor 自身留在 Persistent Map 中，不随 S0/L0 卸载。

2026-05-31：新增脚本 `Scripts/CreateSiteStreamingTestMap.py`：
- 对齐 S0 主电梯占位到主井 XY。
- 将 L0 测试图整体降低到地下层高度。
- 创建 `/Game/Maps/Site/Lvl_Site_Persistent_Test`。
- 放置 `MainElevator_Persistent_S0_L0`。
- 添加 S0/L0 两个 Streaming Level。

2026-05-31：更新 `Docs/LevelDesign/S0_L0_TestMaps.md`，记录 Persistent Map、Streaming Level 和测试流程。

2026-06-01：新增测试地图光照架构整理脚本 `Scripts/NormalizeStreamingTestLighting.py`，并更新地图生成脚本：
- 新生成的 S0/L0 测试地图不再放置 `SkyLight`、`SkyAtmosphere` 或 `DirectionalLight`。
- S0/L0 只放本层局部 `PointLight`，全局环境光由 Persistent Map 统一持有。
- `Scripts/FixTestMapSkyLighting.py` 已被 `Scripts/NormalizeStreamingTestLighting.py` 替代并删除，避免长期保留重复保底脚本。

编译结果：
2026-05-31：`CoopSurvival Win64 Development` 编译成功。

2026-05-31：首次 `CoopSurvivalEditor Win64 Development` 被打开的 Editor Live Coding 阻止；关闭 `LiveCodingConsole` 后，`CoopSurvivalEditor Win64 Development` 编译成功。

验证结果：
2026-05-31：运行 `Scripts/CreateSiteStreamingTestMap.py` 成功，日志显示：
- `Added streaming level: /Game/Maps/S0/Lvl_S0_ForwardStation_Test`
- `Added streaming level: /Game/Maps/L0/Lvl_L0_C12_TestArea_Test`
- `Saved persistent streaming test map: /Game/Maps/Site/Lvl_Site_Persistent_Test`

2026-05-31：无渲染命令行启动 `/Game/Maps/Site/Lvl_Site_Persistent_Test` 成功；日志显示 Persistent Map 加载完成，并激活 S0、保持 L0 未激活：
- `ActivateLevel /Game/Maps/S0/Lvl_S0_ForwardStation_Test 1 1 0`
- `ActivateLevel /Game/Maps/L0/Lvl_L0_C12_TestArea_Test 0 0 0`

2026-06-01：运行旧的 `Scripts/FixTestMapSkyLighting.py`：
- `/Game/Maps/S0/Lvl_S0_ForwardStation_Test` 已保存光照修复。
- `/Game/Maps/Site/Lvl_Site_Persistent_Test` 已保存光照修复。
- `/Game/Maps/L0/Lvl_L0_C12_TestArea_Test` 在用户当前 Editor 中打开，外部命令进程保存失败，日志显示 Windows `Error Code 32` 文件锁。需要关闭该地图/关闭 Editor 后重新运行脚本，或在当前 Editor 中手动添加 `SkyAtmosphere` 并关闭 `Blockout_SkyLight` 的 `Real Time Capture`。

2026-06-01：关闭 Editor 后运行 `Scripts/NormalizeStreamingTestLighting.py` 成功：
- `/Game/Maps/S0/Lvl_S0_ForwardStation_Test` 删除 `Blockout_DirectionalLight`、`Blockout_SkyLight`、`TestMap_SkyAtmosphere`，新增 3 个 S0 局部灯。
- `/Game/Maps/L0/Lvl_L0_C12_TestArea_Test` 删除 `Blockout_DirectionalLight`、`Blockout_SkyLight`，新增 3 个 L0 局部灯。
- `/Game/Maps/Site/Lvl_Site_Persistent_Test` 保留唯一全局光照栈：`Global_DirectionalLight`、`Global_SkyLight`、`Global_SkyAtmosphere`。
- 命令行日志显示 `Success - 0 error(s)`。

建议验证步骤：
1. 重启 Unreal Editor，确保加载新编译的 `ACSStreamingElevatorStation` 类。
2. 打开 `/Game/Maps/Site/Lvl_Site_Persistent_Test`。
3. 按 Play，应出生在 S0 附近，并看到常驻电梯 `MainElevator_Persistent_S0_L0`。
4. 看准电梯按 `E`，电梯应向下移动，加载 L0 并卸载 S0。
5. 到 L0 后再次看准电梯按 `E`，电梯应向上移动，加载 S0 并卸载 L0。
6. 多人 Listen Server 测试时，由主机和客户端一起观察电梯移动与层级切换是否同步。

用户验证结果：
待验证。

## Unit 15：基础战斗首版

目标：
按用户要求优先启动战斗系统，但只做第一块可验证基础：玩家可用左键发起近战攻击，服务器验证冷却、体力、装备近战数据、攻击距离和攻击扇形后，对带有 `UCSSurvivalStatsComponent` 的目标扣血。

范围：
- 新增玩家 `PrimaryAttack` 输入，默认绑定鼠标左键。
- 新增 `UCSCombatComponent`，作为玩家拥有组件发起 Server RPC。
- 近战命中由服务器使用视角方向做短距离球扫和视线校验。
- 命中只修改服务器权威生命值，并通过现有生存属性复制到客户端。
- `Axe` 从物品表读取近战伤害、距离、角度、冷却和体力消耗。
- 新增 `ACSCombatTargetDummy` 测试靶。
- 新增控制台命令 `SpawnCombatDummy`，在玩家前方生成一个测试靶。

非目标：
不做枪械、弹药、AI、敌人追击、死亡掉落、受击动画、武器模型、命中特效、污染伤害、倒地救援、友伤规则或正式战斗 UI。

实现更新：
2026-05-31：新增 `UCSCombatComponent`：
- 文件：`Source/CoopSurvival/Components/CSCombatComponent.h`
- 文件：`Source/CoopSurvival/Components/CSCombatComponent.cpp`

当前行为：
- 本地按左键调用 `TryPrimaryAttack()`。
- 客户端只发送攻击请求，不发送命中结果。
- 服务器读取控制器视角，按当前近战数据扫描目标。
- 无装备时使用低伤害徒手攻击；装备 `Axe` 时读取表格近战数据。
- 服务器扣除攻击体力并记录冷却。
- 命中成功会向攻击者输出调试日志：目标名、攻击来源、生命变化。

2026-05-31：扩展 `FCSItemDefinition` 和物品表：
- `MeleeDamage`
- `MeleeRange`
- `MeleeArcDegrees`
- `MeleeCooldownSeconds`
- `MeleeStaminaCost`

当前 `Axe` 数值：
- 伤害：25
- 距离：170 cm
- 扇形：80 度
- 冷却：0.75 秒
- 体力消耗：12

2026-05-31：新增 `ACSCombatTargetDummy`：
- 文件：`Source/CoopSurvival/World/CSCombatTargetDummy.h`
- 文件：`Source/CoopSurvival/World/CSCombatTargetDummy.cpp`
- 默认生命 75，不掉饥饿/口渴，可作为单机和多人下的最小战斗验证目标。

2026-05-31：`ACSPlayerController` 新增 `SpawnCombatDummy` 控制台命令。客户端执行时走 Server RPC，由服务器生成测试靶。

2026-05-31：运行 `python Scripts\GenerateGameData.py` 成功，已重新生成：
- `Content/Data/Generated/Items.csv`
- `Content/Data/Generated/Recipes.csv`
- `Content/Data/Generated/RecipeRequirements.csv`

编译结果：
2026-05-31：首次 `CoopSurvival Win64 Development` 编译失败，原因是 `ACSPlayerController::SpawnCombatDummyOnServer()` 中局部变量 `SpawnLocation` 遮蔽了 `APlayerController::SpawnLocation`。已改名为 `DummySpawnLocation`。

2026-05-31：`CoopSurvival Win64 Development` 编译成功。

2026-05-31：`CoopSurvivalEditor Win64 Development` 被当前打开的 Unreal Editor Live Coding 阻止。需要关闭 Editor/Game，或在编辑器里按 `Ctrl+Alt+F11` 后再验证 Editor 目标。

建议验证步骤：
1. 重启 Unreal Editor，或在编辑器里使用 `Ctrl+Alt+F11` 编译。
2. 打开 `/Game/FirstPerson/Lvl_FirstPerson`。
3. Play 后打开控制台，执行 `SpawnCombatDummy`。
4. 站到测试靶近距离，左键攻击；无装备时应显示低伤害命中日志。
5. 采集木头并在工作台合成 `Axe`，打开背包装备 `Axe`。
6. 再次左键攻击测试靶，应看到每次约 25 点伤害，测试靶 75 血约 3 次打到 0。
7. 连续快速点击应看到冷却或体力限制，不应每帧扣血。
8. Listen Server + 一个客户端下重复步骤 3-7，确认客户端左键只是请求，服务器统一生成测试靶和扣血。

用户验证结果：
待验证。

## Unit 16：第三方资源导入与分类

目标：
把用户选定的六个外部 UE 资源包导入项目，并按项目内容目录规则放入 `Content/ThirdParty` 分类目录，避免旧导入根目录和新分类目录并存。

范围：
- 暗色岩石地貌
- 模块化科幻治疗站环境
- 木星模块化科幻环境
- 匕首动作捕捉动画包
- 手枪入门动作捕捉包
- 物品拾取套装

实现更新：
2026-06-01：从 `Z:\游戏资源\Unreal` 解压选定资源包，只提取 UE 资源根目录，不导入压缩包内的安装说明或网址杂项。

2026-06-01：新增 `Scripts/OrganizeImportedThirdPartyAssets.py`，通过 UE 资产系统移动 `.uasset` 目录到分类路径：
- `/Game/ThirdParty/Environment/SciFi/JUPITER_SciFi_Kit`
- `/Game/ThirdParty/Environment/SciFi/TreatmentStation`
- `/Game/ThirdParty/Environment/Nature/DarkRockLandscape`
- `/Game/ThirdParty/Animation/Knife_MocapAnimPack`
- `/Game/ThirdParty/Animation/Pistol_Starter`
- `/Game/ThirdParty/Interaction/ItemPickupSet`

2026-06-01：清理 UE 移动后遗留的旧根目录：
- `/Game/JUPITER_SciFi_Kit`
- `/Game/TreatmentStation`
- `/Game/DarkRockLandscape`
- `/Game/Knife_MocapAnimPack`
- `/Game/Pistol_Starter`
- `/Game/ItemPickupSet`

2026-06-01：新增 `Docs/ExternalAssets.md`，记录导入资源、分类路径、文件数量、体积和验证命令。

2026-06-01：用户打开导入资源后触发 `XGEController` Ensure：
- `Ensure condition failed: GetShadowIndex() == 0`
- 栈位于 `UnrealEditor-XGEController.dll`
- 日志显示当时有 96 个 Shader job，并且 Editor 正在使用 XGE Controller 做 Shader 编译调度。

处理：
- 在 `CoopSurvival.uproject` 中显式禁用 `XGEController`。
- 在 `CoopSurvival.uproject` 中显式禁用 `UbaController`。
- 这与项目构建规则保持一致：本项目默认使用 `-NoUBA -NoXGE`，不依赖 UBA/XGE。

验证结果：
2026-06-01：运行 `Scripts/OrganizeImportedThirdPartyAssets.py` 成功，命令行日志显示 `Success - 0 error(s), 0 warning(s)`。

2026-06-01：文件系统复核结果：
- 木星模块化科幻环境：269 个文件，约 0.74 GB。
- 模块化科幻治疗站环境：339 个文件，约 1.17 GB。
- 暗色岩石地貌：57 个文件，约 1.70 GB。
- 匕首动作捕捉动画包：269 个文件，约 0.08 GB。
- 手枪入门动作捕捉包：60 个文件，约 0.04 GB。
- 物品拾取套装：317 个文件，约 0.52 GB。

建议验证步骤：
1. 打开 Unreal Editor。
2. 在 Content Browser 中确认 `Content/ThirdParty` 下存在 `Environment`、`Animation`、`Interaction` 三类目录。
3. 搜索旧根目录名，确认不再作为项目根目录出现。
4. 随机打开一个环境网格、一个材质、一个动画资源，确认资源能正常加载。

用户验证结果：
待验证。

## Unit 17：主线渲染配置

目标：
把项目渲染配置收束到当前阶段的主线组合：DX12、SM6、Lumen、Nanite、Virtual Shadow Maps，关闭项目级 Full RayTracing、默认自动曝光和运动模糊，减少 Shader 编译成本和曝光不稳定。

实现更新：
2026-06-01：更新 `Config/DefaultEngine.ini`：
- `DefaultGraphicsRHI=DefaultGraphicsRHI_DX12`
- D3D12 只保留 `PCD3D_SM6`
- `r.DynamicGlobalIlluminationMethod=1`
- `r.ReflectionMethod=1`
- `r.GenerateMeshDistanceFields=True`
- `r.Nanite.ProjectEnabled=True`
- `r.Shadow.Virtual.Enable=1`
- `r.VirtualTextures=True`
- `r.RayTracing=False`
- `RayTracingMode=Disabled`
- `r.DefaultFeature.AutoExposure=False`
- `r.DefaultFeature.MotionBlur=False`
- `r.DefaultFeature.Bloom=True`

2026-06-01：新增 `Scripts/NormalizeStreamingTestRendering.py`：
- 给 `/Game/Maps/Site/Lvl_Site_Persistent_Test` 创建或更新全局 `PostProcessVolume`。
- 锁定测试地图曝光基准，降低 Bloom 和 Motion Blur。
- 将 `Global_DirectionalLight` 强度调整为 2.0。
- 将 `Global_SkyLight` 强度调整为 0.35，并尝试关闭实时捕获。

当前限制：
2026-06-01：用户当前 Unreal Editor 仍在运行。项目配置已经写入，但测试地图资产脚本尚未执行，以避免和打开的 Editor 抢占地图文件保存。

建议验证步骤：
1. 关闭当前 Unreal Editor。
2. 重新打开项目，使 `DefaultEngine.ini` 渲染配置生效。
3. 如需写入测试地图曝光基准，关闭 Editor 后运行：

```powershell
& 'F:\Unreal\UE_5.8\Engine\Binaries\Win64\UnrealEditor-Cmd.exe' 'F:\Unreal\CoopSurvival\CoopSurvival.uproject' -run=pythonscript -script='F:\Unreal\CoopSurvival\Scripts\NormalizeStreamingTestRendering.py' -unattended -nop4 -nosplash -nullrhi
```

用户验证结果：
待验证。

## Unit 18：匕首攻击动画接入

目标：
先把匕首动作包接入当前装备/战斗链路，用最小范围验证“装备 Knife 后主攻击播放匕首攻击动画，伤害仍由服务器权威战斗逻辑结算”。

实现更新：
2026-06-01：新增物品数据字段 `MeleeAnimationPath`：
- `FCSItemDefinition` 增加可选攻击动画软路径。
- `UCSItemDataSubsystem` 从 `Items.csv` 读取该字段。
- 背包、装备、战斗仍只认 `ItemId`；显示名、类型、图标、战斗参数和动画路径都从表里查。

2026-06-01：新增 `Knife` 数据：
- `Data/Source/Items.csv` 增加 `Knife, Combat Knife`。
- `Data/Source/Recipes.csv` 增加 Workbench 配方 `Knife`。
- `Data/Source/RecipeRequirements.csv` 暂用 `Wood x2` 作为测试材料。
- 已重新生成 `Content/Data/Generated/*.csv`。

2026-06-01：更新 `UCSCombatComponent`：
- 服务器通过现有 `PrimaryAttackOnServer()` 验证生命、冷却、耐力、装备和攻击参数。
- 服务器确认攻击后通过 `NetMulticast, Unreliable` 播放 `MeleeAnimationPath` 指向的攻击动画。
- 命中、伤害和 Debug 信息不依赖动画帧，仍保持服务器权威。

2026-06-01：已撤销“兼容骨架先验证”的临时处理：
- 项目规则已强化：系统和功能必须走完整工程路径，不能把临时兼容、保底或演示路径放入 active path。
- 已删除临时脚本 `Scripts/ConfigureKnifeAnimationCompatibility.py`。
- 已清空 Manny 骨架上临时登记的 compatible skeleton 列表，日志显示 `[KnifeAnimCleanup] Cleared Manny compatible skeletons`。

2026-06-01：检查当前 `ABP_Unarmed` 资产，确认已有 `DefaultSlot`，可以接收 `PlaySlotAnimationAsDynamicMontage` 播放的攻击动作。

2026-06-01：正式建立匕首 Retarget 工作流：
- 新增 `Scripts/RetargetKnifeAnimations.py`，用于创建或更新项目自有的源 IK Rig、目标 IK Rig、IK Retargeter，并把选定匕首攻击动画导出到项目动画目录。
- 创建 `/Game/Animation/Retargeting/IKR_Knife_Source`。
- 创建 `/Game/Animation/Retargeting/IKR_Manny_Target`。
- 创建 `/Game/Animation/Retargeting/RTG_KnifeToManny`。
- 创建 `/Game/Animation/Weapons/Knife/AN_Knife_Light_Melee01`。
- `Data/Source/Items.csv` 的 `Knife.MeleeAnimationPath` 已指向重定向后的项目自有动画资产，不直接引用第三方源骨架动画。
- 当前 active path 不再依赖 compatible skeleton、临时兼容脚本或保底动画路径。

2026-06-01：修正匕首 Retarget 根部配置：
- `Scripts/RetargetKnifeAnimations.py` 不再只在自动生成链失败时才配置 retarget root。
- 脚本现在每次都会强制设置 source/target IK Rig 的 retarget root 为 `pelvis`，并补齐项目统一的 Spine、Head、LeftArm、RightArm、LeftLeg、RightLeg 链。
- 输出动画会在生成后关闭 Root Motion，并尝试启用 Root Lock，保持战斗动画只作为表现层输入，真实移动仍由角色移动和服务器逻辑控制。
- 脚本现在会先删除旧目标 `/Game/Animation/Weapons/Knife/AN_Knife_Light_Melee01`，如果旧文件被打开的 Unreal Editor 占用导致无法删除，会直接报错停止，避免生成 `AN_Knife_Light_Melee02` 之类的重复活动路径。
- 本轮曾因旧动画文件被编辑器占用临时生成 `AN_Knife_Light_Melee02`，已删除该重复文件。

2026-06-01：第一人称阶段性共用第三人称动作曾尝试使用额外 owner-only 表现 Mesh：
- 该尝试已在 Unit 21 的用户验证后撤销。
- 当前 active path 不再包含额外第一人称表现 Mesh、骨骼隐藏方案或第一人称手臂保底资产。
- 匕首攻击动画仍只从 `ItemId -> MeleeAnimationPath` 读取，并在主 `GetMesh()` 播放。
- 第一人称表现后续以 `BP_FirstPersonCharacter` 风格为准：本地第一人称隐藏主 Mesh，第三人称和其他客户端显示主 Manny Mesh。

2026-06-01：检查装备匕首后的动作集切换，发现原实现只替换攻击 Montage：
- `/Game/Animation/Weapons/Knife/AN_Knife_Light_Melee01` 已是项目自有 Manny 目标骨架动画。
- 但 `ACSFirstPersonCharacter` 仍固定使用 `/Game/Characters/Mannequins/Anims/Unarmed/ABP_Unarmed`。
- `ABP_Unarmed` 的 Idle、BlendSpace locomotion、Jump/Fall/Land 仍引用 Unarmed 动作，因此装备 `Knife` 后待机和移动姿态不会自动变成匕首动作。

2026-06-01：补齐匕首装备动作集 active path：
- `Scripts/RetargetKnifeAnimations.py` 扩展为完整匕首动作集工作流，除攻击外还生成待机、8 向走路、8 向跑步和可用跳跃动作到 `/Game/Animation/Weapons/Knife/`。
- 新增 `/Game/Animation/Weapons/Knife/BS_Knife_Idle_Walk_Run`，复制项目现有 2D locomotion BlendSpace 结构，但 27 个采样点全部替换为匕首目标骨架动画。
- 新增 `/Game/Animation/Weapons/Knife/ABP_Knife`，作为 `ABP_Unarmed` 子 AnimBlueprint，通过节点资产覆盖替换 `BS_Idle_Walk_Run`、`MM_Idle`、`MM_Jump`、`MM_Fall_Loop` 和 `MM_Land`。
- `Items.csv` 新增 `AnimationBlueprintPath` 字段；`Knife` 指向 `/Game/Animation/Weapons/Knife/ABP_Knife.ABP_Knife_C`。
- `FCSItemDefinition` / `UCSItemDataSubsystem` 读取该字段，仍保持 `ItemId` 数据驱动，不把装备动画路径硬编码进角色或 UI。
- `ACSFirstPersonCharacter` 会根据当前 `EquippedItemId` 从物品表读取装备 AnimBP；装备 `Knife` 时切到 `ABP_Knife`，空手或装备未配置 AnimBP 的物品时回到 `ABP_Unarmed`。
- 第一人称 owner-only `FirstPersonArmsMesh` 继续 `SetLeaderPoseComponent(GetMesh())`，因此也跟随同一套匕首装备姿态；没有引入第二套第一人称临时动画库。
- 脚本默认仍覆盖已有目标动画并阻止重复命名；仅为当前编辑器已打开且旧攻击动画被占用的情况增加 `CS_KNIFE_RETARGET_OVERWRITE=0` 缺失资产生成模式，该模式不是运行时保底路径。

后续扩展要求：
- 验证通过后再扩展多段轻击、重击、装备/收刀、武器 Mesh 挂点和更精细的第一人称遮挡处理。
- 当前不单独做 FPS 手臂专属动画库；需要时再作为正式动画表现单元建立，不在本单元加入临时补丁。

验证结果：
2026-06-01：运行数据生成成功：

```powershell
python Scripts\GenerateGameData.py
```

2026-06-01：运行匕首 Retarget 工作流成功：

```powershell
& 'F:\Unreal\UE_5.8\Engine\Binaries\Win64\UnrealEditor-Cmd.exe' 'F:\Unreal\CoopSurvival\CoopSurvival.uproject' -run=pythonscript -script='F:\Unreal\CoopSurvival\Scripts\RetargetKnifeAnimations.py' -unattended -nop4 -nosplash -nullrhi
```

输出确认：
- `[KnifeRetarget] IK Rig ready: /Game/Animation/Retargeting/IKR_Knife_Source.IKR_Knife_Source`
- `[KnifeRetarget] IK Rig ready: /Game/Animation/Retargeting/IKR_Manny_Target.IKR_Manny_Target`
- `[KnifeRetarget] IK Retargeter ready: /Game/Animation/Retargeting/RTG_KnifeToManny.RTG_KnifeToManny`
- `[KnifeRetarget] Retargeted asset: /Game/Animation/Weapons/Knife/AN_Knife_Light_Melee01`

2026-06-01：根部配置修正后重跑 Retarget 工作流被打开的 Unreal Editor 阻止：

```text
Existing target animation package is still on disk after delete: .../Content/Animation/Weapons/Knife/AN_Knife_Light_Melee01.uasset.
Close Unreal Editor if it has /Game/Animation/Weapons/Knife/AN_Knife_Light_Melee01.AN_Knife_Light_Melee01 loaded, then rerun this script.
```

当前状态：
- `Scripts/RetargetKnifeAnimations.py` 已修正并通过 `python -m py_compile`。
- `/Game/Animation/Retargeting/IKR_Knife_Source` 和 `/Game/Animation/Retargeting/IKR_Manny_Target` 已在本轮脚本执行中保存了新的 `pelvis` retarget root 配置。
- `/Game/Animation/Weapons/Knife/AN_Knife_Light_Melee01` 尚未被新版脚本覆盖，因为当前 Unreal Editor 进程占用旧 `.uasset`。
- 需要关闭 Unreal Editor 后重新运行 Retarget 命令，才能生成新的根部配置版 `AN_Knife_Light_Melee01`。

2026-06-01：根部配置脚本修正后运行项目标准编译，运行目标和 Editor 目标均成功，UBT 判断目标为 up to date：

```powershell
& 'F:\Unreal\UE_5.8\Engine\Build\BatchFiles\Build.bat' CoopSurvival Win64 Development -Project='F:\Unreal\CoopSurvival\CoopSurvival.uproject' -NoHotReload -NoLiveCoding -NoUBA -NoXGE -NoUBTMakefiles -UBARootDir='F:\Unreal\CoopSurvival\Saved\UnrealBuildTool\UBA'
& 'F:\Unreal\UE_5.8\Engine\Build\BatchFiles\Build.bat' CoopSurvivalEditor Win64 Development -Project='F:\Unreal\CoopSurvival\CoopSurvival.uproject' -NoHotReload -NoLiveCoding -NoUBA -NoXGE -NoUBTMakefiles -UBARootDir='F:\Unreal\CoopSurvival\Saved\UnrealBuildTool\UBA'
```

2026-06-01：再次运行数据生成成功，确认 `Content/Data/Generated/Items.csv` 中 `Knife.MeleeAnimationPath` 已同步为 `/Game/Animation/Weapons/Knife/AN_Knife_Light_Melee01.AN_Knife_Light_Melee01`：

```powershell
python Scripts\GenerateGameData.py
```

2026-06-01：运行运行目标编译成功：

```powershell
& 'F:\Unreal\UE_5.8\Engine\Build\BatchFiles\Build.bat' CoopSurvival Win64 Development -Project='F:\Unreal\CoopSurvival\CoopSurvival.uproject' -NoHotReload -NoLiveCoding -NoUBA -NoXGE -NoUBTMakefiles -UBARootDir='F:\Unreal\CoopSurvival\Saved\UnrealBuildTool\UBA'
```

2026-06-01：第一人称表现 Mesh 改动后，再次运行运行目标编译成功：

```powershell
& 'F:\Unreal\UE_5.8\Engine\Build\BatchFiles\Build.bat' CoopSurvival Win64 Development -Project='F:\Unreal\CoopSurvival\CoopSurvival.uproject' -NoHotReload -NoLiveCoding -NoUBA -NoXGE -NoUBTMakefiles -UBARootDir='F:\Unreal\CoopSurvival\Saved\UnrealBuildTool\UBA'
```

2026-06-01：运行 Editor 目标编译成功：

```powershell
& 'F:\Unreal\UE_5.8\Engine\Build\BatchFiles\Build.bat' CoopSurvivalEditor Win64 Development -Project='F:\Unreal\CoopSurvival\CoopSurvival.uproject' -NoHotReload -NoLiveCoding -NoUBA -NoXGE -NoUBTMakefiles -UBARootDir='F:\Unreal\CoopSurvival\Saved\UnrealBuildTool\UBA'
```

2026-06-01：第一人称表现 Mesh 改动后，Editor 目标外部编译被当前 Live Coding 状态阻止：

```text
Unable to build while Live Coding is active. Exit the editor and game, or press Ctrl+Alt+F11 if iterating on code in the editor or game
```

需要关闭编辑器后重跑 Editor 编译，或在编辑器内按 `Ctrl+Alt+F11` 做 Live Coding 编译验证。

2026-06-01：扩展匕首动作集后运行脚本语法检查和数据生成成功：

```powershell
python -m py_compile Scripts\RetargetKnifeAnimations.py Scripts\GenerateGameData.py
python Scripts\GenerateGameData.py
```

2026-06-01：默认覆盖模式重跑 Retarget 时被当前打开的 Unreal Editor 占用旧攻击动画阻止：

```text
Existing target animation package is still on disk after delete: .../Content/Animation/Weapons/Knife/AN_Knife_Light_Melee01.uasset.
Close Unreal Editor if it has /Game/Animation/Weapons/Knife/AN_Knife_Light_Melee01.AN_Knife_Light_Melee01 loaded, then rerun this script.
```

处理结果：
- 未强制关闭用户当前 Unreal Editor。
- 使用 `CS_KNIFE_RETARGET_OVERWRITE=0` 仅生成缺失动作集，保留已存在的攻击动画路径，不创建 `AN_Knife_Light_Melee02` 等重复活动资产。
- 成功生成 20 个新增目标动画，并创建/更新 `BS_Knife_Idle_Walk_Run` 和 `ABP_Knife`。

2026-06-01：运行匕首动作集资产验证脚本成功：
- 21 个匕首动画资产均存在，且目标骨架均为 `/Game/Characters/Mannequins/Meshes/SK_Mannequin`。
- `BS_Knife_Idle_Walk_Run` 存在，27 个采样点没有残留 Unarmed 动画。
- `ABP_Knife` 存在，生成类为 `/Game/Animation/Weapons/Knife/ABP_Knife.ABP_Knife_C`。

2026-06-01：扩展装备 AnimBP 切换代码后运行目标编译成功：

```powershell
& 'F:\Unreal\UE_5.8\Engine\Build\BatchFiles\Build.bat' CoopSurvival Win64 Development -Project='F:\Unreal\CoopSurvival\CoopSurvival.uproject' -NoHotReload -NoLiveCoding -NoUBA -NoXGE -NoUBTMakefiles -UBARootDir='F:\Unreal\CoopSurvival\Saved\UnrealBuildTool\UBA'
```

2026-06-01：扩展装备 AnimBP 切换代码后，Editor 目标外部编译仍被当前 Live Coding 状态阻止：

```text
Unable to build while Live Coding is active. Exit the editor and game, or press Ctrl+Alt+F11 if iterating on code in the editor or game
```

需要关闭编辑器后重跑 Editor 编译，或在编辑器内按 `Ctrl+Alt+F11` 做 Live Coding 编译验证。

2026-06-01：按用户要求重跑完整匕首 Retarget 覆盖流程成功：

```powershell
& 'F:\Unreal\UE_5.8\Engine\Binaries\Win64\UnrealEditor-Cmd.exe' 'F:\Unreal\CoopSurvival\CoopSurvival.uproject' -run=pythonscript -script='F:\Unreal\CoopSurvival\Scripts\RetargetKnifeAnimations.py' -unattended -nop4 -nosplash -nullrhi
```

输出确认：
- `[KnifeRetarget] Retargeted asset: /Game/Animation/Weapons/Knife/AN_Knife_Light_Melee01`
- `[KnifeRetarget] Retargeted asset: /Game/Animation/Weapons/Knife/AN_Knife_St_Idle_00`
- `[KnifeRetarget] Completed knife retarget: 21 asset(s)`
- 本次没有生成 `AN_Knife_Light_Melee02` 或其他重复活动资产。

2026-06-01：完整覆盖后运行 Editor 目标编译成功：

```powershell
& 'F:\Unreal\UE_5.8\Engine\Build\BatchFiles\Build.bat' CoopSurvivalEditor Win64 Development -Project='F:\Unreal\CoopSurvival\CoopSurvival.uproject' -NoHotReload -NoLiveCoding -NoUBA -NoXGE -NoUBTMakefiles -UBARootDir='F:\Unreal\CoopSurvival\Saved\UnrealBuildTool\UBA'
```

2026-06-01：完整覆盖后运行匕首动作集资产验证成功：
- 21 个匕首动画均存在。
- 21 个匕首动画的目标骨架均为 `/Game/Characters/Mannequins/Meshes/SK_Mannequin`。
- `BS_Knife_Idle_Walk_Run` 仍为 27 个采样点，且没有残留 Unarmed 动作采样。
- `ABP_Knife` 生成类为 `/Game/Animation/Weapons/Knife/ABP_Knife.ABP_Knife_C`。

2026-06-01：用户验证发现挥刀仍 T-Pose，继续定位到资产内容层问题：
- `/Game/Animation/Weapons/Knife/AN_Knife_Light_Melee01` 虽然存在且有 41 个采样帧，但 RAW Pose 检测显示 `hand_r`、`spine_03` 等骨骼在第 0 帧到第 20 帧的位移和旋转变化均为 0，说明正式目标攻击动画本身被烘成静态参考姿势。
- 源动画 `/Game/ThirdParty/Animation/Knife_MocapAnimPack/Animations/Knife_Stand_Style_St/Knife_MeleeSet/Knife_Light_Melee01` 的同一检测显示 `hand_r` 有明显挥刀运动，因此问题不在源动画。
- 根因是 `RTG_KnifeToManny` 之前没有 Retarget Op Stack（`num_ops=0`），批量导出时只输出参考姿势；同时旧脚本把 `FBoneChain` 结构直接 `str()` 比较，导致 IK Rig 残留 `Spine_0`、`RightArm_0` 等重复链。
- 已修正 `Scripts/RetargetKnifeAnimations.py`：按 `chain_name` 读取现有链，清理旧 `_0` 重复链，并在 Retargeter 中确定性重建默认 5 个 retarget op、重新 Exact 映射链、验证必需链映射。
- 修正后的 Retargeter 临时导出验证通过：临时目标攻击动画第 0 帧到第 20 帧 `hand_r_loc_delta=72.266`、`hand_r_rot_delta=98.006`、`spine_03_loc_delta=14.549`、`spine_03_rot_delta=78.419`，临时资产已删除。
- 当时正式覆盖被正在运行的 GUI Unreal Editor 锁住：`AN_Knife_Light_Melee01.uasset` 删除时报 `Error Code 32`。用户关闭 Editor 后已解除锁。

2026-06-01：关闭 Unreal Editor 后重跑正式匕首 Retarget 覆盖成功：

```powershell
& 'F:\Unreal\UE_5.8\Engine\Binaries\Win64\UnrealEditor-Cmd.exe' 'F:\Unreal\CoopSurvival\CoopSurvival.uproject' -run=pythonscript -script='F:\Unreal\CoopSurvival\Scripts\RetargetKnifeAnimations.py' -unattended -nop4 -nosplash -nullrhi -stdout -FullStdOutLogOutput
```

输出确认：
- `RTG_KnifeToManny` 重建为 5 个 Retarget Ops。
- 21 个正式 Knife 目标动画全部重新覆盖生成。
- 当时 `ABP_Knife` 已重新编译并保存；后续用户验证发现全身装备 AnimBP 替换会污染腿部和基础待机，因此该路径已撤销。

2026-06-01：正式资产运动验证通过：
- 源/目标 IK Rig 均不再存在 `*_0` 重复 retarget chain。
- `RTG_KnifeToManny` 为 5 个 Retarget Ops，`RightArm` 映射到 `RightArm`。
- 21 个 Knife 动画均使用目标骨架 `/Game/Characters/Mannequins/Meshes/SK_Mannequin`。
- 正式 `/Game/Animation/Weapons/Knife/AN_Knife_Light_Melee01` 第 0 帧到第 20 帧：`hand_r_loc_delta=72.266`、`hand_r_rot_delta=98.006`、`spine_03_loc_delta=14.549`、`spine_03_rot_delta=78.419`，不再是静态 T-Pose。
- 当时 `BS_Knife_Idle_Walk_Run` 和 `ABP_Knife` 存在；后续已作为错误全身装备动画路径删除。

2026-06-02：根据用户反馈“腿部和待机仍不正常”，撤销错误的全身装备 AnimBP 活动路径：
- 结论：装备型动画不应该通过 `ItemId -> AnimationBlueprintPath -> SetAnimInstanceClass` 切换整套 AnimBP。这样会重置动画状态，并把第三方武器包的全身 idle / locomotion / jump 直接替换项目基础 locomotion，导致腿部、待机和移动姿态污染。
- 正确配置方向：角色长期使用同一个基础 AnimBP；基础 locomotion、跳跃、蹲伏、死亡等由项目统一控制；装备只通过数据驱动的上半身装备层、Montage Slot、Layered Blend Per Bone、Aim/hand IK 或 Linked Anim Layer 叠加。
- 具体到 Knife：当前活动路径改回 `ABP_Unarmed` 基础 locomotion + `MeleeAnimationPath` 攻击 Montage。Knife 的待机/移动上半身持刀姿态应在后续 AnimBP 分层单元实现，不再用全身子 AnimBP 伪装。
- `Data/Source/Items.csv` 和 `Content/Data/Generated/Items.csv` 已删除 `AnimationBlueprintPath` 列；`Knife` 只保留 `/Game/Animation/Weapons/Knife/AN_Knife_Light_Melee01` 作为攻击动画。
- `ACSFirstPersonCharacter` 已删除每 Tick 检查装备并切换 AnimInstanceClass 的逻辑，避免装备切换重置/替换整套动画蓝图。
- `Scripts/RetargetKnifeAnimations.py` 已删除创建 `ABP_Knife` 和 `BS_Knife_Idle_Walk_Run` 的逻辑，只保留正式 Retarget 动画资产生成，并会清理废弃全身装备动画资产。
- 已删除 `/Game/Animation/Weapons/Knife/ABP_Knife` 和 `/Game/Animation/Weapons/Knife/BS_Knife_Idle_Walk_Run`，未保留隐藏备选活动路径。
- `python -m py_compile Scripts\RetargetKnifeAnimations.py Scripts\GenerateGameData.py` 成功。
- `python Scripts\GenerateGameData.py` 成功。
- `Scripts\RetargetKnifeAnimations.py` 重跑成功，21 个 Knife 目标动画仍生成到 `/Game/Animation/Weapons/Knife/`。
- `CoopSurvivalEditor Win64 Development` 编译成功。

装备型动画后续正式方案：
- 新增项目级装备动画配置，而不是整套 AnimBP class 字段。例如 `DA_EquipAnim_Knife` 或物品表字段指向装备动画配置。
- 配置内包含上半身 idle/hold pose、攻击/使用 Montage、Slot 名、Layered Blend 起始骨骼（建议 `spine_01` 或 `spine_03`）、blend in/out、是否影响下半身、是否启用手 IK / 武器挂点。
- AnimBP 结构应为：基础 locomotion cache pose -> 装备上半身持姿层 -> 攻击/使用 Montage Slot -> aim/IK 修正 -> 输出 Pose。
- Knife 这种轻武器优先只覆盖上半身；下半身继续使用基础 locomotion。只有重武器、处决、翻越、受击等明确全身动作才临时进入 full-body slot。

2026-06-02：按上面的正式方向做了第一版 Knife 装备上半身动画层：
- 新增 `Scripts/ConfigureEquipmentAnimationLayers.py`，通过 UE Python 修改 `/Game/Characters/Mannequins/Anims/Unarmed/ABP_Unarmed`，不创建新的全身装备 AnimBP。
- `ABP_Unarmed` 的 `AnimGraph` 现在保留基础 `Main States`，增加 `CS_EquipmentBasePose` / `CS_EquipmentHeldPose` cache pose、`EquipmentUpperBodySlot`、`UpperBodyActionSlot` 和两个 `LayeredBoneBlend`。
- 两个 `LayeredBoneBlend` 都从 `spine_03` 开始，`BlendDepth=0`，`mesh_space_rotation_blend=True`；下半身、骨盆和腿仍由基础 locomotion 驱动。
- `DefaultSlot` 仍保留在后面，继续接入原 ControlRig 输出；新增装备层不会删除原 `DefaultSlot`。
- `Items.csv` 新增 `MeleeAnimationSlotName`、`EquipmentUpperBodyAnimationPath`、`EquipmentUpperBodySlotName` 三个字段。
- `Knife` 的攻击动画仍是 `/Game/Animation/Weapons/Knife/AN_Knife_Light_Melee01`，但攻击 Slot 改为 `UpperBodyActionSlot`。
- `Knife` 的装备持握动画指向 `/Game/Animation/Weapons/Knife/AN_Knife_St_Idle_00`，通过 `EquipmentUpperBodySlot` 循环播放。
- `ACSFirstPersonCharacter` 会在复制到的 `EquippedItemId` 变化时启动或停止装备上半身持握 Montage；不再每 Tick 切换 AnimBP class。
- `UCSCombatComponent` 从物品数据读取 `MeleeAnimationSlotName`，为空时才回退到 `DefaultSlot`。

2026-06-02：验证结果：
- `python -m py_compile Scripts\ConfigureEquipmentAnimationLayers.py Scripts\RetargetKnifeAnimations.py Scripts\GenerateGameData.py` 成功。
- `python Scripts\GenerateGameData.py` 成功。
- `Scripts\ConfigureEquipmentAnimationLayers.py` 首次运行成功，保存 `ABP_Unarmed`；再次运行只输出 `Already configured`，不会重复生成节点。
- `Saved\Logs\VerifyEquipmentAnimationLayers.log` 验证通过：`ABP_Unarmed` 存在 `DefaultSlot`、`EquipmentUpperBodySlot`、`UpperBodyActionSlot`；两个 `LayeredBoneBlend` 均为 `spine_03:0`；生成物品表包含 Knife 的 idle/attack 动画路径和 Slot 名。
- `CoopSurvivalEditor Win64 Development` 编译成功。
- `CoopSurvival Win64 Development` 编译成功。

2026-06-02：近战攻击冷却改为配表模式，支持“动作播放完毕前不能再次攻击”：
- `Items.csv` 新增 `MeleeCooldownMode` 字段。
- 支持模式：
  - 空值或 `FixedSeconds`：只使用 `MeleeCooldownSeconds`。
  - `AnimationLength`：使用 `MeleeAnimationPath` 指向动画的播放时长；动画缺失时回退到 `MeleeCooldownSeconds`。
  - `MaxFixedAndAnimation`：取 `MeleeCooldownSeconds` 和动画播放时长的较大值，保证动作没播完不能再次攻击，同时保留最小固定 CD。
- `Knife` 设置为 `MaxFixedAndAnimation`，`MeleeCooldownSeconds=0.45`，攻击动画 `/Game/Animation/Weapons/Knife/AN_Knife_Light_Melee01` 当前长度为 `1.333s`，所以服务器实际攻击锁约 `1.333s`。
- `Axe` 设置为 `FixedSeconds`，仍使用固定 `0.75s` 冷却。
- `UCSCombatComponent` 在服务器确认攻击前解析冷却模式，冷却未结束直接拒绝攻击请求；客户端连点不会绕过服务器锁。
- `Scripts\GenerateGameData.py` 已校验 `MeleeCooldownMode` 只允许空值、`FixedSeconds`、`AnimationLength`、`MaxFixedAndAnimation`。

2026-06-02：近战冷却模式验证：
- `python -m py_compile Scripts\GenerateGameData.py Scripts\ConfigureEquipmentAnimationLayers.py Scripts\RetargetKnifeAnimations.py` 成功。
- `python Scripts\GenerateGameData.py` 成功。
- CSV 列对齐检查通过：`Data/Source/Items.csv` 和 `Content/Data/Generated/Items.csv` 均为 18 列。
- `Saved\Logs\CheckKnifeAttackLength.log` 确认 Knife 攻击动画长度为 `1.333s`。
- `CoopSurvival Win64 Development` 编译成功。
- 外部 `CoopSurvivalEditor Win64 Development` 编译仍被当前打开的 Unreal Editor / Live Coding 拦截：`Unable to build while Live Coding is active`。关闭 Editor 后重跑外部 Editor 编译，或在 Editor 内按 `Ctrl+Alt+F11`。

2026-06-02：根据用户反馈“持刀手指坏掉”，补充 Knife 待机右手手指修正：
- 根因：当前 IK Retarget 工作流没有单独的手指链或持握修正层，`AN_Knife_St_Idle_00` 的右手手指会保留目标骨架重定向后的不稳定姿态。
- `Scripts\RetargetKnifeAnimations.py` 增加确定性后处理：在生成或保留 Knife 动画后，对 `/Game/Animation/Weapons/Knife/AN_Knife_St_Idle_00` 写入源动画 `/Game/ThirdParty/Animation/Knife_MocapAnimPack/Animations/Knife_Stand_Style_St/Knife_St_IdleSet/Knife_St_Idle_00` 的右手手指旋转，同时保留目标动画的位置、缩放、长度和关键帧数。
- 使用 `CS_KNIFE_RETARGET_OVERWRITE=0` 运行脚本，只保留已有 21 个 Knife 目标动画并应用手指修正，没有重新创建重复动画资产。
- 写入 15 个源/目标共有右手指骨：`thumb_01_r`、`thumb_02_r`、`thumb_03_r`、`index_01_r`、`index_02_r`、`index_03_r`、`middle_01_r`、`middle_02_r`、`middle_03_r`、`ring_01_r`、`ring_02_r`、`ring_03_r`、`pinky_01_r`、`pinky_02_r`、`pinky_03_r`。
- 跳过 4 个源动画不存在的掌骨：`index_metacarpal_r`、`middle_metacarpal_r`、`ring_metacarpal_r`、`pinky_metacarpal_r`。
- `Saved\Logs\VerifyKnifeFingerPoseFix.log` 验证通过：目标待机动画长度 `3.666667s`、`111` 个关键帧；15 个共有右手指骨逐关键帧最大旋转差为 `0.000031°`，满足对齐阈值。
- 本轮只修正装备持刀待机 `AN_Knife_St_Idle_00`；如果用户后续看到挥砍过程中手指仍坏，再按同一方式扩展到 `AN_Knife_Light_Melee01` 攻击动画。
- 已新增 `Docs\SystemDesign\UE_EquipmentAnimationRetargetWorkflow.md`，沉淀本轮装备动画 Retarget、T-Pose 排查、上半身装备层、冷却配表和手指修正流程。

2026-06-02：按用户要求接入 Knife 持刀模型显示，并按性能反馈改为资产化模型：
- `Items.csv` 新增装备模型字段：`EquipmentStaticMeshPath`、`EquipmentMeshSocketName`、`EquipmentMeshOffsetX/Y/Z`、`EquipmentMeshRotationPitch/Yaw/Roll`、`EquipmentMeshScale`。
- 当前 Knife 动画包没有独立 Knife 静态网格；命令行 OBJ 导入会在 UE 5.8 Interchange 下崩溃并留下不可加载残片，已清理坏资产。
- 根据用户提供的 NAS 资源，导入 `Knives VOL.1 - Hunting` 中的 `SM_Knife_Hunt_01a` 及其 13 个直接/递归依赖，迁入 `/Game/Weapons/Knife/HuntingKnives`。
- Knife 当前 `EquipmentStaticMeshPath` 指向 `/Game/Weapons/Knife/HuntingKnives/Meshes/SM_Knife_Hunt_01a.SM_Knife_Hunt_01a`；运行时不再生成 StaticMesh，只加载资产并创建显示组件。
- `ACSFirstPersonCharacter` 根据复制到客户端的 `EquippedItemId` 更新装备模型；第三人称组件挂到 `GetMesh()`，第一人称组件挂到 `FirstPersonArmsMesh`，默认 socket 为 `hand_r`。
- 第一人称视角只显示 owner-only 装备模型；第三人称视角和远端客户端显示第三人称装备模型；切换 U 视角会同步更新可见性。
- `UCSItemDataSubsystem` 支持 `EquipmentStaticMeshPath` 使用分号或竖线分隔多个 StaticMesh 路径，方便后续装备由多个部件组成。
- `Scripts\GenerateGameData.py` 已校验新增偏移、旋转、缩放字段；`Data\Source\Items.csv` 和 `Content\Data\Generated\Items.csv` 均为 27 列，Knife `EquipmentStaticMeshPath=/Game/Weapons/Knife/HuntingKnives/Meshes/SM_Knife_Hunt_01a.SM_Knife_Hunt_01a`、`EquipmentMeshSocketName=hand_r`、当前 `EquipmentMeshOffsetX=-12`、`EquipmentMeshRotationPitch=-90`。
- `Scripts\CreateKnifeBlockoutMesh.py` 和 `/Game/Weapons/Knife/SM_Knife_Blockout` 已删除，不再保留占位 active path。
- `/Game/Hunting_Knives` 原始临时导入目录已删除，只保留当前活动刀具资产和依赖。
- UE Python 验证通过：`SM_Knife_Hunt_01a` 是 `StaticMesh`，bounds extent 为 `(1.279, 3.017, 18.013)`，材质指向 `/Game/Weapons/Knife/HuntingKnives/Materials/MI_Knife_Hunt_01a`。
- UE Python 数据验证通过：`Data\Source\Items.csv` 与 `Content\Data\Generated\Items.csv` 的 Knife 均指向 `SM_Knife_Hunt_01a`，并使用 `hand_r`、当前 `OffsetX=-12`、`RotationPitch=-90`。
- `python -m py_compile Scripts\GenerateGameData.py` 成功。
- `CoopSurvival Win64 Development` 编译成功，目标已是最新。
- `CoopSurvivalEditor Win64 Development` 编译成功。
- `Docs\SystemDesign\UE_EquipmentAnimationRetargetWorkflow.md` 已补充装备模型显示链路和新装备接入清单。

2026-06-02：根据用户反馈“持刀 idle 和走路动作没有配置上”，补齐 Knife 上半身持刀移动层：
- 结论：此前只配置了 `EquipmentUpperBodyAnimationPath`，装备后会在 `EquipmentUpperBodySlot` 循环播放持刀 idle；没有配置移动时的持刀 walk/run 上半身动画。
- `Items.csv` 新增 `EquipmentUpperBodyWalkAnimationPath` 和 `EquipmentUpperBodyRunAnimationPath` 字段，继续由物品表驱动，不恢复 `AnimationBlueprintPath` 或整套 `ABP_Knife`。
- `Knife` 当前配置：idle=`/Game/Animation/Weapons/Knife/AN_Knife_St_Idle_00`，walk=`/Game/Animation/Weapons/Knife/AN_Act_Walk_F`，run=`/Game/Animation/Weapons/Knife/AN_Act_Run_F`，三者都播放到 `EquipmentUpperBodySlot`。
- `ACSFirstPersonCharacter` 缓存当前装备定义，并在 Tick 中根据水平速度和 `bIsSprinting` 切换上半身装备 Montage：静止用 idle，移动用 walk，冲刺用 run。
- 该实现仍只影响 `ABP_Unarmed` 的上半身装备层；腿部、骨盆、跳跃和基础 locomotion 继续由基础 AnimBP 控制，避免再次污染腿部动作。
- `Scripts\GenerateGameData.py` 修正装备模型偏移/旋转校验，允许 `EquipmentMeshOffsetX=-5`、`EquipmentMeshRotationPitch=-90` 这类合法负值。
- UE Python 验证通过：`AN_Knife_St_Idle_00`、`AN_Act_Walk_F`、`AN_Act_Run_F` 均能加载，长度分别为 `3.667s`、`1.033s`、`0.767s`；`ABP_Unarmed` 仍存在。
- `python -m py_compile Scripts\GenerateGameData.py Scripts\ConfigureEquipmentAnimationLayers.py Scripts\RetargetKnifeAnimations.py` 成功。
- `python Scripts\GenerateGameData.py` 成功，`Content\Data\Generated\Items.csv` 已同步为 29 列。
- `CoopSurvival Win64 Development` 编译成功。
- `CoopSurvivalEditor Win64 Development` 编译成功。

2026-06-03：根据用户确认“武器位置和攻击范围应该在 Editor 中按武器单独调整”，将 Knife 从运行时裸 StaticMesh 组件升级为正式武器 Actor 蓝图路径：
- 新增 `ACSWeaponActor` C++ 基类，包含 `WeaponMeshComponent`、`HitStartComponent`、`HitEndComponent` 和 `HitRadius`，不启用 Tick，不参与碰撞导航。
- 新增 `/Game/Blueprints/Weapons/BP_Weapon_Knife`，用于在 Editor 中调整刀模型握持位置、刀身朝向和近战命中线段。
- `Items.csv` 新增 `EquipmentActorClassPath` 字段；`Knife` 指向 `/Game/Blueprints/Weapons/BP_Weapon_Knife.BP_Weapon_Knife_C`，`EquipmentStaticMeshPath` 继续作为该武器 Actor 自动填充 Mesh 的资源来源。
- `ACSFirstPersonCharacter` 在装备带 `EquipmentActorClassPath` 的物品时分别生成第三人称和第一人称武器 Actor；可见性仍遵循本地第一人称 owner-only 与第三人称/远端可见规则。
- `UCSCombatComponent` 优先读取当前装备武器 Actor 的 `HitStart/HitEnd/HitRadius` 做服务器短扫掠命中；没有武器 Actor 时才使用空手/旧扇形判定。
- `Scripts\CreateWeaponBlueprints.py` 创建并保存 `BP_Weapon_Knife`；后续新武器应照此创建 `BP_Weapon_*`，不要在角色或战斗组件里硬编码单个武器范围。
- `Data\Source\Items.csv` 和 `Content\Data\Generated\Items.csv` 均为 30 列，Knife 已包含 `EquipmentActorClassPath`。
- `python Scripts\GenerateGameData.py` 成功。
- `CoopSurvival Win64 Development` 编译成功。
- `CoopSurvivalEditor Win64 Development` 编译成功。编译前 Unreal Editor 有 `ABP_Unarmed` 未保存弹窗，本轮选择保存选中项后正常关闭编辑器再外部编译。
- UE Python 资产脚本运行成功并保存 `/Game/Blueprints/Weapons/BP_Weapon_Knife`。

建议验证步骤：
1. 重新打开 Unreal Editor，进入 `/Game/FirstPerson/Lvl_FirstPerson`。
2. 捡 `Wood x2`。
3. 靠近 Workbench，打开制作界面，制作 `Combat Knife`。
4. 在背包里装备 `Combat Knife`。
5. 确认装备 `Knife` 后右手通过 `BP_Weapon_Knife` 显示 `SM_Knife_Hunt_01a` 短刀模型；第一人称能看到 owner-only 短刀，按 `U` 切第三人称后能看到第三人称短刀。
6. 确认装备后站立待机、WASD 移动、跑动和跳跃仍保持 `ABP_Unarmed` 下半身基础动作，不再出现 Knife 全身 locomotion 导致的腿部异常。
7. 确认装备 `Knife` 后上半身进入持刀姿态；如果躯干影响太弱或太强，下一轮只调整分层起始骨骼（`spine_03` / `spine_01`）和具体持握动画，不回退到全身 AnimBP。
8. 执行 `SpawnCombatDummy` 生成测试靶。
9. 对准测试靶左键攻击，确认：
   - 屏幕 Debug 显示用 `Knife` 命中。
   - 攻击时通过 `UpperBodyActionSlot` 播放 `/Game/Animation/Weapons/Knife/AN_Knife_Light_Melee01`。
   - 命中范围来自 `BP_Weapon_Knife` 的 `HitStartComponent`、`HitEndComponent` 和 `HitRadius`，调整刀攻击范围时改该蓝图组件。
   - 连续点击左键时，Knife 需要等本次挥砍动画播完后才能再次攻击，不再按 `0.45s` 提前重砍。
   - 第一人称/第三人称表现不再因装备 Knife 切到全身 `ABP_Knife`。
   - Listen Server + Client 下，其他客户端能看到攻击动作和服务器权威伤害结果。
10. 切回空槽或卸下 Knife，确认持刀上半身姿态停止，角色回到基础空手待机。

用户验证结果：
待验证。

## Unit 19：快捷装备栏

目标：
建立一个简单但系统化的 4 槽快捷装备栏。玩家可以用 `1`、`2`、`3`、`4` 切换当前装备槽；背包里的 `Equip` 按钮把物品放入当前选中的槽位；装备和战斗仍只认 `ItemId`，具体数据从物品表读取。

非目标：
- 不做拖拽式快捷栏管理。
- 不做武器模型挂点、收刀/换刀动画或多武器姿态切换。
- 不把 UI 本地状态作为真实装备状态。

实现更新：
2026-06-01：`ACSPlayerState` 新增服务器拥有并复制的装备槽状态：
- `EquipmentHotbar`：4 个 `ItemId` 槽位。
- `SelectedEquipmentSlotIndex`：当前选中槽位。
- `EquipItem()` 会把物品放进当前选中槽，并设置 `EquippedItemId`。
- `SelectEquipmentSlot()` 会由服务器验证槽位、物品是否存在、是否可装备，然后切换 `EquippedItemId`；空槽会卸下当前装备。
- 物品数量归零时，服务器会清理对应快捷槽，避免装备栏引用不存在的物品。

2026-06-01：`ACSPlayerController` 新增快捷槽输入和 RPC：
- 本地绑定 `1`、`2`、`3`、`4`。
- 客户端只发送 `RequestSelectEquipmentSlot()` / `ServerSelectEquipmentSlot()`。
- 服务器通过 `ACSPlayerState::SelectEquipmentSlot()` 决定实际装备结果。

2026-06-01：`SCSHUDWidget` 新增底部 4 槽 HUD：
- 显示槽位数字、物品临时图标字母和名称。
- 当前选中槽高亮。
- HUD 数据来自 `BuildHUDSnapshot()`，不在 UI 内保存真实装备状态。

2026-06-01：`SCSInventoryPanelWidget` 轻量接入快捷槽：
- 顶部显示当前选中槽和当前装备。
- 背包面板打开时按 `1-4` 也会请求切换槽位。
- 点击 `Equip` 会把物品装备到当前选中槽。

2026-06-01：保存结构升级：
- `UCSWorldSaveGame::CurrentSaveVersion` 从 `2` 升到 `3`。
- `FCSPlayerSaveData` 增加 `EquipmentHotbar` 和 `SelectedEquipmentSlotIndex`。
- 旧存档没有快捷栏时，读取后仍可用旧的 `EquippedItemId` 迁移到当前选中槽。

验证结果：
2026-06-01：运行运行目标编译成功：

```powershell
& 'F:\Unreal\UE_5.8\Engine\Build\BatchFiles\Build.bat' CoopSurvival Win64 Development -Project='F:\Unreal\CoopSurvival\CoopSurvival.uproject' -NoHotReload -NoLiveCoding -NoUBA -NoXGE -NoUBTMakefiles -UBARootDir='F:\Unreal\CoopSurvival\Saved\UnrealBuildTool\UBA'
```

2026-06-01：运行 Editor 目标编译成功：

```powershell
& 'F:\Unreal\UE_5.8\Engine\Build\BatchFiles\Build.bat' CoopSurvivalEditor Win64 Development -Project='F:\Unreal\CoopSurvival\CoopSurvival.uproject' -NoHotReload -NoLiveCoding -NoUBA -NoXGE -NoUBTMakefiles -UBARootDir='F:\Unreal\CoopSurvival\Saved\UnrealBuildTool\UBA'
```

建议验证步骤：
1. 打开 `/Game/FirstPerson/Lvl_FirstPerson`，启动 Listen Server + 1 Client。
2. 制作或获得 `Axe` 和 `Combat Knife`。
3. 按 `1` 选中槽 1，在背包点 `Axe` 的 `Equip`。
4. 按 `2` 选中槽 2，在背包点 `Combat Knife` 的 `Equip`。
5. 关闭背包，在 HUD 底部确认 1/2/3/4 快捷槽显示和高亮。
6. 按 `1`、`2` 切换，确认 HUD 当前装备和攻击伤害/动画跟随切换。
7. 丢弃当前槽物品到数量归零，确认对应槽清空并不再保持无效装备。
8. 执行 `SaveCoop`、重开或 `LoadCoop`，确认快捷栏槽位和选中槽恢复。

用户验证结果：
待验证。

## Unit 20：中心交互提示 UI

目标：
建立一个系统化的中心交互提示 UI。HUD 中心显示准星；当玩家看向可交互对象时，显示 `E` 和该对象提供的交互提示文本。提示文本来自交互接口，不在 HUD 里按具体 Actor 类型硬编码。

非目标：
- 不做完整 UMG 皮肤或动效。
- 不做按键重绑定 UI。
- 不做复杂扫描、距离条、长按进度或目标信息面板。

实现更新：
2026-06-01：扩展 `ICSInteractable`：
- 新增 `GetInteractionPrompt(APawn* InstigatorPawn) const`。
- 交互对象自己提供显示文本，HUD 只显示接口返回值。

2026-06-01：扩展 `UCSInteractionComponent`：
- 新增 `GetFocusedActor()`。
- 新增 `GetFocusedInteractionPrompt()`。
- 继续使用现有本地 Focus Trace 和服务器交互验证；UI 不参与真实交互判定。

2026-06-01：已有交互对象接入提示文本：
- `ACSResourceNode`：`Gather <ResourceType>`。
- `ACSWorldItemActor`：`Pick up <ItemName> x<Quantity>`，物品名从物品表查。
- `ACSWorkbenchStation`：`Open Workbench`。
- `ACSStreamingElevatorStation`：`Take Elevator to S0/L0`。

2026-06-01：`SCSHUDWidget` 新增中心 UI：
- 中心常驻简易准星。
- 有可交互目标时显示 `E  <Prompt>`。
- 背包、工作台、房间面板打开时 HUD 仍整体隐藏，避免 UI 重叠。

验证结果：
2026-06-01：运行运行目标编译成功：

```powershell
& 'F:\Unreal\UE_5.8\Engine\Build\BatchFiles\Build.bat' CoopSurvival Win64 Development -Project='F:\Unreal\CoopSurvival\CoopSurvival.uproject' -NoHotReload -NoLiveCoding -NoUBA -NoXGE -NoUBTMakefiles -UBARootDir='F:\Unreal\CoopSurvival\Saved\UnrealBuildTool\UBA'
```

2026-06-01：运行 Editor 目标编译成功：

```powershell
& 'F:\Unreal\UE_5.8\Engine\Build\BatchFiles\Build.bat' CoopSurvivalEditor Win64 Development -Project='F:\Unreal\CoopSurvival\CoopSurvival.uproject' -NoHotReload -NoLiveCoding -NoUBA -NoXGE -NoUBTMakefiles -UBARootDir='F:\Unreal\CoopSurvival\Saved\UnrealBuildTool\UBA'
```

建议验证步骤：
1. 打开 `/Game/FirstPerson/Lvl_FirstPerson`，单人 PIE 或 Listen Server + Client。
2. 看向资源点，确认中心显示 `E  Gather Wood` 或对应资源类型。
3. 看向世界掉落物，确认显示 `E  Pick up <ItemName> x<N>`。
4. 看向 Workbench，确认显示 `E  Open Workbench`。
5. 看向电梯，确认显示目标层提示。
6. 打开背包或工作台界面，确认中心提示隐藏，不与面板重叠。
7. 按 `E` 交互，确认真实交互仍由服务器验证，没有因为 UI 显示改变权限逻辑。

用户验证结果：
待验证。

## Unit 21：第一人称表现收口到 BP_FirstPersonCharacter 风格

目标：
保留 `BP_PlayerCharacter` / `ACSFirstPersonCharacter` 作为正式玩家角色，采用双 Mesh 表现结构：主 Manny Mesh 给第三人称和其他客户端看，本地第一人称使用 owner-only 的第二个 Manny Mesh 只显示需要的手臂相关面片。

非目标：
- 不直接切换项目默认 Pawn 到 `BP_FirstPersonCharacter`，因为它不包含当前背包、合成、交互、装备和战斗组件。
- 不保留独立第三方第一人称手臂包、示例角色、示例 GameMode、示例地图或临时 viewmodel 代码作为 active path。
- 不在本单元建立新的第一人称手臂 Retarget、专用 AnimBP 或武器挂点系统。

实现更新：
2026-06-01：用户验证后撤销 `Animated FP Civilian Arms Pack.zip` 接入尝试：
- 删除 `/Game/ThirdParty/Characters/FirstPersonCivilianArms` 活动资源目录。
- `ACSFirstPersonCharacter` 删除额外第一人称袖子 Mesh、手套 Mesh、包内 Idle/Sprint 单节点动画、相机前移参数和 viewmodel 相对变换参数。
- `FirstPersonCamera` 恢复到胶囊中心高度位置：第一人称为 `(0, 0, StandingCameraHeight)`，第三人称仍使用后拉相机位置。
- 该轮临时让 `UpdateViewMeshVisibility()` 只负责主 Mesh 的 `OwnerNoSee`，随后按下面的双 Mesh 路径重建第一人称表现。
- 主 `GetMesh()` 继续加载 Manny 和 `ABP_Unarmed`，保持第三人称动作、匕首攻击 Montage、联机可见性和现有 gameplay 组件路径不变。

2026-06-01：按项目内 `BP_FirstPersonCharacter` 的双 Mesh 思路重建正式第一人称表现：
- `ACSFirstPersonCharacter` 新增 `FirstPersonArmsMesh`，使用同一个 `/Game/Characters/Mannequins/Meshes/SKM_Manny_Simple`。
- `FirstPersonArmsMesh` 挂在胶囊组件下，使用与主 Mesh 一致的位置和旋转，并通过 `SetLeaderPoseComponent(GetMesh())` 跟随主 Mesh 姿态，不复制第二套 AnimBP 状态机。
- `FirstPersonArmsMesh` 设置为 `OnlyOwnerSee=true`、无碰撞、无阴影，默认隐藏；第一人称本地显示，第三人称和其他客户端不显示。
- 主 `GetMesh()` 在第一人称本地继续 `OwnerNoSee=true`，避免完整身体和第一人称 Mesh 重叠。
- 新增 `FirstPersonHiddenMaterialIDs` 和 `FirstPersonMaterialMaskLODCount`，用于隐藏第一人称 Mesh 的材质 section。
- 当前 Manny Simple 的材质槽检查结果：`M_HeadLegs` 为 Material ID `0`，`M_Torso` 为 Material ID `1`。
- 默认隐藏 Material ID `0`，也就是头、脸、眼球、头发、腿所在面片；保留 Material ID `1`，因为当前 Manny 的躯干和手臂在同一个 `M_Torso` 槽里，直接隐藏会连胳膊一起隐藏。
- 如果后续需要“上半身只剩胳膊、躯干完全消失”，需要制作真正的 arms-only Skeletal Mesh，或把 Manny 的 torso/arms 拆成独立材质 section 后再通过 `FirstPersonHiddenMaterialIDs` 配置隐藏。

当前 active path：
- 第一人称：本地隐藏主 Manny Mesh，显示 owner-only 的 `FirstPersonArmsMesh`，默认隐藏 `M_HeadLegs` 面片。
- 第三人称 / 其他客户端：显示主 Manny Mesh。
- 本地玩家按 `U` 执行 `ToggleView`，在第一人称和第三人称之间切换。
- 相机通过 `ViewSpringArm` 承载；第一人称 `TargetArmLength=0`，第三人称使用 `ThirdPersonCameraDistance`，并启用 SpringArm 遮挡检测。
- 动画：第一人称 Mesh 跟随主 Manny Mesh 姿态；后续如果要做专用第一人称手臂，必须作为正式动画表现单元重新建立，不接回这次被撤销的临时手臂包路径。

2026-06-02：把本地视角切换改为正式 SpringArm 第三人称相机：
- `ACSFirstPersonCharacter` 新增 `ViewSpringArm`，挂在胶囊组件上，`FirstPersonCamera` 挂到 SpringArm socket。
- 第一人称时 SpringArm 长度插值到 `0`，保持本地 owner-only 手臂 Mesh 可见、主 Mesh 对 owner 隐藏。
- 第三人称时 SpringArm 长度插值到 `ThirdPersonCameraDistance`，高度插值到 `ThirdPersonCameraHeight`，本地主 Mesh 可见、第一人称手臂 Mesh 隐藏。
- `Config/DefaultInput.ini` 已确认 `ToggleView` 绑定到 `U`。

验证结果：
2026-06-01：运行目标编译成功：

```powershell
& 'F:\Unreal\UE_5.8\Engine\Build\BatchFiles\Build.bat' CoopSurvival Win64 Development -Project='F:\Unreal\CoopSurvival\CoopSurvival.uproject' -NoHotReload -NoLiveCoding -NoUBA -NoXGE -NoUBTMakefiles -UBARootDir='F:\Unreal\CoopSurvival\Saved\UnrealBuildTool\UBA'
```

2026-06-01：Editor 目标编译成功：

```powershell
& 'F:\Unreal\UE_5.8\Engine\Build\BatchFiles\Build.bat' CoopSurvivalEditor Win64 Development -Project='F:\Unreal\CoopSurvival\CoopSurvival.uproject' -NoHotReload -NoLiveCoding -NoUBA -NoXGE -NoUBTMakefiles -UBARootDir='F:\Unreal\CoopSurvival\Saved\UnrealBuildTool\UBA'
```

备注：Editor 编译过程中出现 VibeUE 插件弃用警告和一次低内存重试，最终结果为 `Succeeded`。

2026-06-01：双 Mesh 第一人称表现改动后再次运行目标和 Editor 目标编译，均成功：

```powershell
& 'F:\Unreal\UE_5.8\Engine\Build\BatchFiles\Build.bat' CoopSurvival Win64 Development -Project='F:\Unreal\CoopSurvival\CoopSurvival.uproject' -NoHotReload -NoLiveCoding -NoUBA -NoXGE -NoUBTMakefiles -UBARootDir='F:\Unreal\CoopSurvival\Saved\UnrealBuildTool\UBA'
& 'F:\Unreal\UE_5.8\Engine\Build\BatchFiles\Build.bat' CoopSurvivalEditor Win64 Development -Project='F:\Unreal\CoopSurvival\CoopSurvival.uproject' -NoHotReload -NoLiveCoding -NoUBA -NoXGE -NoUBTMakefiles -UBARootDir='F:\Unreal\CoopSurvival\Saved\UnrealBuildTool\UBA'
```

2026-06-02：SpringArm 视角切换改动后运行目标编译成功：

```powershell
& 'F:\Unreal\UE_5.8\Engine\Build\BatchFiles\Build.bat' CoopSurvival Win64 Development -Project='F:\Unreal\CoopSurvival\CoopSurvival.uproject' -NoHotReload -NoLiveCoding -NoUBA -NoXGE -NoUBTMakefiles -UBARootDir='F:\Unreal\CoopSurvival\Saved\UnrealBuildTool\UBA'
```

2026-06-02：同轮外部 Editor 目标编译被当前打开的 Unreal Editor / Live Coding 拦截：

```text
Unable to build while Live Coding is active. Exit the editor and game, or press Ctrl+Alt+F11 if iterating on code in the editor or game
```

处理条件：关闭 Editor 后重跑 `CoopSurvivalEditor Win64 Development`，或在 Editor 内按 `Ctrl+Alt+F11` 使用 Live Coding 编译。

建议验证步骤：
1. 打开 `/Game/FirstPerson/Lvl_FirstPerson`。
2. PIE 单人或 Listen Server + Client。
3. 第一人称确认能看到本地手臂相关 Mesh，且头、脸、眼球、头发、腿不显示。
4. 如果看到躯干，确认它来自 `M_Torso` 与手臂同材质槽；该问题需要后续 arms-only Mesh 或拆 section 解决，不通过隐藏 spine 骨骼处理。
5. 按 `U` 切换第三人称，确认相机拉到角色后方，本地能看到完整 Manny，且不会贴脸显示第一人称手臂 Mesh。
6. 再按 `U` 切回第一人称，确认相机回到头部高度，主 Mesh 重新对 owner 隐藏，第一人称手臂 Mesh 恢复显示。
7. 第三人称靠近墙体或物体，确认 SpringArm 遮挡检测会缩短相机距离，避免明显穿墙。
8. Listen Server + Client 下确认切视角只影响本地玩家视角，不改变其他客户端看到的该玩家第三人称 Mesh。
9. 验证背包、合成、交互、装备、攻击仍可用，因为默认角色没有切到 `BP_FirstPersonCharacter`。

用户验证结果：
待验证。

## Unit 22：UI 中央样式地基试点（FCSUIStyle + 首张 AI 皮肤接线）

目标：
验证「AI 出图 → 9-slice PNG → Slate 画刷 → 真实游戏控件」这条 UI 皮肤管线在引擎里真能跑通、能编译。落地 UI 实装路线（详见 `Docs/Development/UI_Implementation.md`、记忆 `project-ui-art-direction`）的第一块地基单元 **UI-Style-A** 的最小切片。

非目标：
- 不做整套样式系统（颜色/字体/全部画刷 tokens 灌入）——本单元只注册一张试点画刷 `CS.Frame`。
- 不引入 CommonUI/MVVM；不改 UI 的事件驱动/服务器权威结构（UI 只显示）。
- 不把其余 SCS* 控件改造接入；只挑最直观的 `SCSDialogueWidget`（NPC 对话框）做一处可视替换。

实现更新：
2026-06-09：
- 新增 `Source/CoopSurvival/UI/Style/CSUIStyle.{h,cpp}`：`FCSUIStyle` 封装一个 `FSlateStyleSet`（名 `CSUIStyle`），`Get()` 惰性初始化并注册。`SetContentRoot(ProjectContentDir/UI/Skin)`，注册画刷 `CS.Frame` = `FSlateBoxBrush(RootToContentDir("CS_Frame",".png"), ImageSize 256x256, Margin 0.15)`（9-slice，margin 取自 `uiskin_meta.json`）。
- AI 生成的 9-slice 金属边框皮 `Docs/SystemDesign/img/ui_frame_9s.png` 拷入 `Content/UI/Skin/CS_Frame.png`（442KB）。
- `SCSDialogueWidget::Construct` 外层 SBorder 的 `BorderImage` 由 `FCoreStyle GenericWhiteBox`+终端蓝 tint 改为 `FCSUIStyle::Get().GetBrush("CS.Frame")`、tint 改 `White`（让金属本色显示）、`Padding` 由 2 改 `(34,30)`，让内层深色面板坐进金属环内。内层深底面板不变。弃用的 `FrameColor` 局部变量已移除。

验证结果：
2026-06-09：运行目标编译成功（`CSUIStyle.cpp` + `SCSDialogueWidget.cpp` 编译+链接，约 10s，无报错）：

```powershell
& 'F:\Unreal\UE_5.8\Engine\Build\BatchFiles\Build.bat' CoopSurvival Win64 Development -Project='F:\Unreal\CoopSurvival\CoopSurvival.uproject' -NoHotReload -NoLiveCoding -NoUBA -NoXGE -NoUBTMakefiles -UBARootDir='F:\Unreal\CoopSurvival\Saved\UnrealBuildTool\UBA'
```

2026-06-09：编辑器目标编译成功（关编辑器后整编，`UnrealEditor-CoopSurvival.dll` 重链；中途一次低内存进程重试，最终 `Succeeded`）：

```powershell
& 'F:\Unreal\UE_5.8\Engine\Build\BatchFiles\Build.bat' CoopSurvivalEditor Win64 Development -Project='F:\Unreal\CoopSurvival\CoopSurvival.uproject' -NoHotReload -NoLiveCoding -NoUBA -NoXGE -NoUBTMakefiles -UBARootDir='F:\Unreal\CoopSurvival\Saved\UnrealBuildTool\UBA'
```

教训：**改编辑器可见的 Slate 控件，运行时目标编过不算数**——PIE 跑的是编辑器模块 `UnrealEditor-CoopSurvival.dll`，必须编 `CoopSurvivalEditor` 目标（且要关编辑器，Live Coding 会拦截）改动才进编辑器。第一次只编运行时目标导致「看不到替换」。

已知限制 / 后续：
- **散图 PNG 仅在编辑器/PIE 下由 Slate 从磁盘按路径加载有效**；正式打包 cook 不会带散图，shipping 需把该 PNG 导成 `UTexture2D` 资产、或加入 `AdditionalNonAssetDirectories` 暂存。本单元只验证 PIE 可视。
- 这是**首过概念皮、非终稿**；「生产级」指管线具备（法线/深度/染色/9-slice 可控），不是这张图的成色。环厚/角尺寸若不合适，调 `ImageSize` / `Margin` / `Padding` 即可，无需重出图。
- `FCSUIStyle` 目前惰性注册、未在模块 Startup/Shutdown 显式 Init/Shutdown（退出时可能一条注销警告）；正式做 UI-Style-A 时收口到模块生命周期，并把全部 tokens 灌入。

建议验证步骤：
1. 关闭外部编译后打开编辑器（本单元改了运行时控件，编辑器目标无需单独整编即可 PIE，但若编辑器已开需先让其加载新 DLL）。
2. PIE 进入有可对话 NPC 的场景，触发一次 NPC 对话（`SCSDialogueWidget` 在叙事总监有 active conversation line 时显示）。
3. 确认对话框外框由原细蓝描边变成 AI 金属边框皮，中间深色底可读、文字/头像正常。
4. 若金属环过厚/过薄或角尺寸不对，回报，我调 `ImageSize`/`Margin`/`Padding`（纯数据，不重编美术）。

用户验证结果：
待验证。

## Unit 23：UI 中央主题数据资产（UCSUITheme + FCSUIStyle 消费）—— 治迭代痛

目标：
把 UI 的「值」（颜色/数值/9-slice 边距/皮贴图）与「接线」（控件搭建）分离，让调色/调间距/换皮在编辑器 Details 面板直接改、存盘即生效，不必重编 C++、不必重开编辑器——解决 Slate 手写 UI 的迭代慢痛点。落实 UI 路线图地基单元 UI-Style-A。

背景决策（2026-06-09，查实后定）：
- **不上 HTML 中间件**。Coherent GameFace 公开文档 Unreal 支持最高 5.6（known issues/changelog 无 5.7/5.8），项目在 5.8 无现成插件，且发布需商业授权（Demo 带水印/30天不可发布）。免费替代（UE5.8 自带 Chromium WebBrowser/CEF + 开源 WebUI/BLUI，全 CSS 兼容、源码可移植）记为后备（运行时重、透明叠层+输入焦点要调）。最终选：**留原生 Slate，用数据驱动主题治迭代**。

实现更新（2026-06-09）：
- 新增 `UI/Style/CSUITheme.{h,cpp}`：`UCSUITheme : UPrimaryDataAsset`，三个 TMap —— `Colors`(FName→FLinearColor)、`Metrics`(FName→float)、`Brushes`(FName→`FCSUIBrushDef`{Texture/ImageSize/Margin/Tint})。`GetColor/GetMetric/FindBrush` 带回退；`#if WITH_EDITOR` 下 `PostEditChangeProperty` 广播静态 `OnThemeChanged`。
- 重构 `UI/Style/CSUIStyle.{h,cpp}`：消费 `UCSUITheme`（`LoadObject` 约定路径 `/Game/UI/Theme/DA_CSUITheme`，`TStrongObjectPtr` 持有防 GC）。`GetColor/GetMetric` 读资产、缺失回退**内置 ui-theme.css 默认色板**（sRGB→linear via `FColor`）。`PopulateBrushes` 优先用主题 `Frame` 画刷（贴图资产，可 cook），否则回退 `Content/UI/Skin/CS_Frame.png` 散图。新增 `CS.UI.Reload` 控制台命令 + 编辑器 `OnThemeChanged`→`Reload()` 热重建样式并广播 `OnStyleChanged`。
- `SCSDialogueWidget`：内底/名牌/台词颜色由硬编码改为 `FCSUIStyle::GetColor(...)` 的 `_Lambda` 绑定（dialogBg/amber/ink）——改主题颜色后下一帧生效。名牌色顺势从旧的临时终端蓝改为设计系统的 amber。
- 新增 `Scripts/CreateUITheme.py`（commandlet）：建 `DA_CSUITheme` + 导入 `T_CS_Frame` 贴图（LODGroup=UI）+ 灌入色板/数值/Frame 画刷。

迭代闭环（本单元交付的核心价值）：
- **改颜色/数值** → 编辑器 Details 改 `DA_CSUITheme` 存盘 → 下一帧生效（TAttribute 绑定）。零编译、零重开。
- **换皮/改 9-slice 边距** → 改 DA 的 Brushes/贴图 → `CS.UI.Reload` 控制台命令热重建（或重开面板）。
- **改控件接线** → C++，走 `Ctrl+Alt+F11` Live Coding 秒级；只有新文件/新 UPROPERTY/新类才需 `CoopSurvivalEditor` 整编（且关编辑器）。

验证结果：
2026-06-09：运行目标编译成功（`CSUITheme.cpp`+`CSUIStyle.cpp`+`SCSDialogueWidget.cpp`+UHT `Module.CoopSurvival.1.cpp` 编译+链接；过程有低内存进程重试，最终 `Succeeded`）：

```powershell
& 'F:\Unreal\UE_5.8\Engine\Build\BatchFiles\Build.bat' CoopSurvival Win64 Development -Project='F:\Unreal\CoopSurvival\CoopSurvival.uproject' -NoHotReload -NoLiveCoding -NoUBA -NoXGE -NoUBTMakefiles -UBARootDir='F:\Unreal\CoopSurvival\Saved\UnrealBuildTool\UBA'
```

待做：编辑器目标整编（关编辑器）→ 跑 `Scripts/CreateUITheme.py` 建 `DA_CSUITheme` + 导贴图 → PIE 在 Details 改色，确认对话框颜色实时变（不重编不重开）。

已知限制 / 后续：
- `DA_CSUITheme` 与导入贴图需被 cook 才进打包（PIE/编辑器下正常加载；shipping 需确保资产被引用/纳入 cook）。
- 目前只迁了 `SCSDialogueWidget` 一处；HUD/背包等其余 SCS* 后续单元逐个迁到 `FCSUIStyle`，并把全部 ui-theme.css tokens（字体/更多画刷/语义色）灌入。
- 画刷热替换目前重建整个 style set（指针换新），控件靠 `OnStyleChanged` 重取；颜色/数值无需（TAttribute 每帧读）。

用户验证结果：
待验证。

## Unit 24：UI 表现层转 UMG + "HTML→WBP"代码生成器（首屏 WBP_Dialogue）

目标：
解决用户对手写 Slate 的核心痛点——**看不见布局/比例/层级、Slot 不直观**。改用 UMG 可视化表现层（设计器所见即所得），但**不手拖**：写代码按 HTML 结构生成带完整控件树的 WBP，用户打开即见、可视化微调。基准分辨率锁定 **1920×1080**（HTML px → UMG Canvas px 1:1，DPI 缩放兜其它分辨率）。

背景决策（2026-06-10）：
- 痛点厘清：不是迭代速度，而是**手写 Slate 盲搭**（写 `SVerticalBox::Slot()` 树，编译跑起来才看得见比例层级）。UMG 设计器正治此。逻辑/数据/服务器权威全留 C++，只换表现层；主题色板（`UCSUITheme`/`FCSUIStyle`）、AI 皮（`T_CS_Frame`）复用。
- MCP（VibeUE）现未桥接到本会话，且即便桥接也不改变"WBP 树必须 C++ 建"的结论（见下），故走 commandlet。

实现更新（2026-06-10）：
- `CoopSurvival.Build.cs`：新增首个 `if (Target.bBuildEditor)` 门控块，加 `UMGEditor`/`UnrealEd`/`Kismet`/`AssetRegistry` 私有依赖（运行时/Shipping 不链接）。
- `UI/CSDialogueUserWidget.{h,cpp}`：UMG 版对话框数据基类（`UUserWidget`），`BindWidgetOptional` 绑 `SpeakerText`/`LineText`/`PortraitImage`，`NativeTick` 轮询叙事总监刷新（与旧 `SCSDialogueWidget` 同源数据）。
- `UI/Editor/CSWidgetAuthoringLibrary.{h,cpp}`：**编辑器专用 `UBlueprintFunctionLibrary`**，`BuildDialogueWBP(...)` 用 C++ 把控件树灌进 WBP。整段 `#if WITH_EDITOR`（含 UFUNCTION 声明）；`FKismetEditorUtilities::CreateBlueprint`（不走 Factory 绕开类选择器崩溃）→ `WidgetTree->ConstructWidget` + `AddChild`+Cast 槽 → `OnVariableAdded`（每控件一次、仅新建）→ `CompileBlueprint`。create-only-if-missing。树：SizeBox(底部居中,820 宽)▸Border(金属皮 9-slice,DrawAs=Box+Margin .15)▸Border(深底 dialogBg)▸HBox▸[Image=PortraitImage(96), VBox▸[SpeakerText(amber 15), LineText(ink 17 自动换行)]]。颜色/字复用 `FCSUIStyle`。
- `Scripts/CreateDialogueWBP.py`：加载 `T_CS_Frame` → 调 `unreal.CSWidgetAuthoringLibrary.build_dialogue_wbp(...)` → `EditorAssetLibrary.save_asset`。

为什么是这套架构（实测核实，见 UE_Gotchas 3.3）：
UE5.8 实测——`WidgetTree::ConstructWidget` 是 C++ 模板非 UFUNCTION、`WidgetBlueprint::WidgetTree` 对 Python protected 不可读。故 Python 只能建空 WBP、填不了树；树搭建必须 C++。Hybrid（C++ 助手 + Python 编排）是唯一可靠且 headless 安全的路。

验证结果：
2026-06-10：编辑器目标编译成功（首个 bBuildEditor 依赖块 + 生成器编入）。**注意**：首轮整模块重编遇 OOM 杀进程污染 PCH，`SCSBackpackGridWidget.cpp` 报与现文件对不上的幻象错误；加 `-MaxParallelActions=4` 降并行后干净通过（见 UE_Gotchas 2.3）。随后 commandlet 生成成功——日志 `[CSWidgetGen] OK /Game/UI/Dialogue/WBP_Dialogue` + `SAVEPACKAGE` + `CreateDialogueWBP: saved`，`Content/UI/Dialogue/WBP_Dialogue.uasset`(35KB) 落盘。**全程零手拖**。

```powershell
# 生成器编译（编辑器须关；OOM 时降并行）
& 'F:\Unreal\UE_5.8\Engine\Build\BatchFiles\Build.bat' CoopSurvivalEditor Win64 Development -Project='F:\Unreal\CoopSurvival\CoopSurvival.uproject' -NoHotReload -NoLiveCoding -NoUBA -NoXGE -NoUBTMakefiles -UBARootDir='F:\Unreal\CoopSurvival\Saved\UnrealBuildTool\UBA' -MaxParallelActions=4
# 生成 WBP（编辑器须关）
& 'F:\Unreal\UE_5.8\Engine\Binaries\Win64\UnrealEditor-Cmd.exe' 'F:\Unreal\CoopSurvival\CoopSurvival.uproject' -run=pythonscript -script='F:\Unreal\CoopSurvival\Scripts\CreateDialogueWBP.py' -unattended -nullrhi -nosplash
```

待做 / 待用户验证：
- 用户重开编辑器打开 `WBP_Dialogue`：确认①设计器里树可见可编辑、②`BindWidget` 行 SpeakerText/LineText/PortraitImage 绑定为绿、③可拖动微调比例。
- PIE 暂看不到它（无代码实例化 WBP_Dialogue；旧 Slate `SCSDialogueWidget` 仍是 HUD 装配的那个）。让它在 NPC 对话时真显示=一小步 C++ 接线（创建 WBP 加视口 + 停旧 Slate），用户认可布局后再接。
- 认可后：用同一生成器把其余屏（HUD/背包/属性/搜刮/合成/...）各自从 HTML 结构生成 WBP（1080 基准）。

用户验证结果：
待验证。

## Unit 25：对话框 UMG 转换闭环 —— WBP_Dialogue 接进运行时 + 修运行时编译潜伏坑

目标：
方向决策（2026-06-10，用户拍板）：**UI 表现层全面转 UMG 代码生成**（Unit 24 的 HTML→WBP 生成器为终态，逐步淘汰手写 Slate `SCS*`）。本单元是该方向的**第一步验证闭环**——把 Unit 24 生成的 `WBP_Dialogue` 真正实例化到运行时、替换旧的 Slate `SCSDialogueWidget`，打通"生成 WBP → 运行时实例化 → 退役旧 Slate"这条链。链路通了，后续 HUD/背包/属性各屏照搬同一套。

实现更新（2026-06-10）：
- `Core/CSPlayerController.{h,cpp}`：
  - 成员 `TSharedPtr<SWidget> DialogueWidget`（旧 Slate）→ `UPROPERTY() TObjectPtr<UUserWidget> DialogueUMGWidget`（持有防 GC）。
  - `CreateHUD()`：删 `SNew(SCSDialogueWidget)`，改 `LoadClass<UUserWidget>("/Game/UI/Dialogue/WBP_Dialogue.WBP_Dialogue_C")` → `CreateWidget<UUserWidget>(this, Class)` → `AddToViewport(12)`（ZOrder 仍高于字幕 11；UMG `AddToViewport` 与 Slate `AddViewportWidgetContent` 共用同一 ZOrder 空间）。加载失败打 `UE_LOG(Error)`（PIE 可见，不静默消失）。UMG 控件**自驱**：`UCSDialogueUserWidget::NativeTick` 自己轮询导演显隐，故无需像旧 Slate 那样传 `.Director(...)`。
  - `RemoveHUD()`：`DialogueUMGWidget->RemoveFromParent()` + 置空。
  - 去掉 `#include "UI/SCSDialogueWidget.h"`，加 `#include "Blueprint/UserWidget.h"`。
- `UI/Editor/CSWidgetAuthoringLibrary.{h,cpp}`：**修运行时编译潜伏坑**。`BuildDialogueWBP` 返回类型 `UWidgetBlueprint*` → `bool`。根因：该 UFUNCTION 虽整段 `#if WITH_EDITOR`，但 UHT 解析其签名生成反射元数据时会解析返回类型，而 `UWidgetBlueprint` 是 `UMGEditor`（编辑器模块）的反射类型——运行时/Shipping 目标不链 `UMGEditor`，UHT 找不到该 UClass 报 `Unable to find 'class'... 'UWidgetBlueprint'`。Unit 24 只编过编辑器目标，本单元首次编运行时目标才暴露。**结论（写入 UE_Gotchas）：运行时模块里的 UFUNCTION 签名禁止出现编辑器专用反射类型，哪怕被 `#if WITH_EDITOR` 包着也不行**。Python 端 `CreateDialogueWBP.py` 只把返回值当真值判断、保存走路径，改 bool 完全兼容。

验证结果：
2026-06-10：**运行时 + 编辑器两目标均编译链接成功**。
```powershell
# 运行时（首次暴露并修复 UWidgetBlueprint UHT 坑后）
& '...Build.bat' CoopSurvival Win64 Development ...   # Result: Succeeded
# 编辑器（UMGEditor 已链，生成器签名改动 + 控制器改动均过）
& '...Build.bat' CoopSurvivalEditor Win64 Development ...   # Result: Succeeded
```

待做 / 待用户验证（PIE）：
- PIE 进有可对话 NPC 的场景，触发一次 NPC 对话：确认底部出现 **WBP_Dialogue**（UMG 版，金属边框皮 + 深底 + 头像 + 名牌/台词），且与 AIC 字幕互斥（旁白走字幕、会话走对话框）、对话结束自动隐藏。
- 若 WBP 加载失败，Output Log 会有 `[CSPlayerController] 无法加载对话框 WBP` 红字——回报我查资产路径/cook。
- **PIE 确认 UMG 对话框正常后**，删除已退役的死代码 `UI/SCSDialogueWidget.{h,cpp}`（现已无任何引用，仅留作回退；确认前不删以防 UMG 版有问题需对比）。
- 之后：用同一生成器把 HUD/背包/属性/搜刮/合成各屏从 HTML 结构生成 WBP（1080 基准）+ 运行时实例化替换对应 `SCS*`，逐屏推进。

用户验证结果：
待验证。

## Unit 26：主 HUD 转 UMG —— WBP_HUD 接进运行时（UMG 迁移第二屏）

目标：
沿用 Unit 25 打通的链路，把主 HUD 从手写 Slate `SCSHUDWidget` 迁到生成的 `WBP_HUD`（UMG 代码生成第二屏）。HUD 是最常驻的屏，验证 UMG 在游戏态（非菜单）下的实时刷新表现。逻辑/数据/服务器权威不变，只换表现层。

实现更新（2026-06-10）：
- 新 `UI/CSHUDUserWidget.{h,cpp}`：UMG 主 HUD 数据基类（`UUserWidget`）。`NativeTick` **自驱**读 owning `ACSPlayerController` 的单帧 `FCSHUDSnapshot`（`GetHUDSnapshot()`）+ `IsAnyMenuPanelOpen()`，刷新：时钟 / 5 条状态条（Health/Hunger/Thirst/Stamina/Load 的百分比+数值文本，Load 超重转红）/ 电梯状态（可折叠）/ 弹药（可折叠）/ 背包摘要 / 4 装备槽（序号/字形/名称/选中高亮）。**菜单面板开着时折叠 `ContentRoot`**（本体保持可见以持续 Tick，与 `SCSHUDWidget::GetHUDVisibility` 同条件），折叠时不读快照。单例元素用 `BindWidgetOptional`；重复结构（5 条/4 槽）用**固定命名 + `GetWidgetFromName` 按名解析成数组**（避免几十个 BindWidget 成员）。颜色走 `FCSUIStyle`（bar 用 red/amber/cold/ok 主题 token；面板/文字用 inset/ink/dim/faint）。
- `UI/Editor/CSWidgetAuthoringLibrary`：加 `BuildHUDWBP(PackagePath, AssetName)`（返回 `bool`）。用 helper lambda（`MakeText`/`MakePanel`/`Place` 锚定）建整棵树：RootCanvas ▸ ContentRoot(填满) ▸ [左上 StatsPanel(ClockText + 5×{BarRow/BarHdr/Lbl/Val + BarBox(高12)/Bar}) / 顶部 ElevatorPanel / 中心 Crosshair / 底部 AmmoPanel / 底部 HotbarPanel(4×{SlotBox 84×78/Slot_Border/SlotV/Slot_Num/Slot_Glyph/Slot_Name}) / 左下 InventoryPanel]。**坑：`UProgressBar` 在 `AutoHeight` 槽里期望高度=0 → 必须套 `SizeBox(HeightOverride 12)`**。同 create-only-if-missing + 全控件 `OnVariableAdded` 一次。
- `Scripts/CreateHUDWBP.py`：调 `build_hudwbp` + `save_asset`。
- `Core/CSPlayerController.{h,cpp}`：成员 `TSharedPtr<SWidget> HUDWidget` → `UPROPERTY() TObjectPtr<UUserWidget> HUDUMGWidget`。`CreateHUD()` 改 `LoadClass("/Game/UI/HUD/WBP_HUD.WBP_HUD_C")`→`CreateWidget`→`AddToViewport(10)`（失败打 Error 降级，HUD 不显示但叙事/对话仍走）。`RemoveHUD()` 重写为**各控件独立清理**（不因主 HUD 缺失而早退，否则字幕/对话会泄漏）。去 `#include "UI/SCSHUDWidget.h"`。

验证结果：
2026-06-10：**编辑器 + 运行时两目标编译链接成功**；生成器 commandlet 跑出 `[CSWidgetGen] OK /Game/UI/HUD/WBP_HUD`，`Content/UI/HUD/WBP_HUD.uasset`(125KB) 落盘（commandlet exit 1 是已知坏档退出码，看 UE_LOG 标记，见 UE_Gotchas 3.3）。
```powershell
& '...Build.bat' CoopSurvivalEditor Win64 Development ...   # Succeeded（新类+生成器）
& '...UnrealEditor-Cmd.exe' ... -script='Scripts\CreateHUDWBP.py' -unattended -nullrhi -nosplash   # [CSWidgetGen] OK
& '...Build.bat' CoopSurvival Win64 Development ...         # Succeeded（运行时路径）
```

待做 / 待用户验证（PIE）：
- PIE 进游戏：确认左上状态面板（时钟+5条状态条，颜色对：生命红/饥饿琥珀/口渴青/体力绿/负重琥珀）、底部装备栏（4 槽，选中槽青色高亮+数字/字形）、底部弹药（持枪时显示弹匣/储备）、左下背包摘要、顶部电梯状态（坐电梯时）、中心准星都正常实时刷新；开背包/合成/GM/联机面板时整块 HUD 折叠、关闭后恢复。
- 若 Output Log 出现 `[CSPlayerController] 无法加载 HUD WBP` 红字 → 资产路径/cook 问题，回报。
- **PIE 确认 HUD 正常后**，删除已退役的 `UI/SCSHUDWidget.{h,cpp}`（连同 Unit 25 的 `SCSDialogueWidget.{h,cpp}` 一并清，二者现都无引用）。
- 布局/比例若要微调：直接开 `WBP_HUD` 在设计器拖（这正是转 UMG 的目的），无需改 C++。
- 下一屏候选：背包 `SCSInventoryPanelWidget`（最复杂，含网格拖放）或换装/GM 等较简单屏先练手。CJK 字体地基建议在背包屏前补（物品名是中文）。

修订（2026-06-10，字体乱码）：首版 PIE 文字全乱码（每字符同一占位字形）。根因=生成器用 `FCoreStyle::GetDefaultFontStyle` 设字体——那是 Slate 运行时字体、非资产引用，烘进 WBP 后字形解析失败。修法=继承 UMG 控件默认 Roboto 资产引用、只改字号（`FSlateFontInfo F=T->GetFont(); F.Size=N; T->SetFont(F)`），HUD+对话框两生成器同改，删旧资产重生成。详见 `UE_Gotchas` 3.5。

修订二（2026-06-10，按设计稿对齐）：用户指出**首版 HUD 是「旧 Slate 功能复刻+套色」，没按 `Docs/SystemDesign/UI/UI_Screens.html` 的终端美术稿来**。决策（用户拍板）：完整还原设计的布局/风格，有真数据接真、无数据按设计静态占位；武器栏画 5 槽。**重写** `UCSHUDUserWidget`+`BuildHUDWBP` 为设计布局：浮动式（无朴素深底面板）——左上 生命/污染/饱食·水分(合并条)+负重chip+buff行、右上当前目标、中心准星、底中 交互提示+5槽武器栏(主/副/刀/投/道具+类型标+序号)、底右 AMMO大字+深度/站点完整度；chip/键帽/槽用**双层 Border**(外1px强调描边+内底色)、状态条用 `FProgressBarStyle`(bg=inset/fill=主题色)套 1px 框。**真数据**：生命条、饱食·水分条、负重chip、AMMO、武器栏前 4 槽(接 `EquipmentHotbar`，选中红边)。**静态占位（无对应系统/快照字段）**：污染条、收容失效chip、buff行、右上目标、深度、站点完整度、第 5 武器槽——待 污染系统/Buff快照/Quest目标串/深度/loadout 4→5 落地再接线。`EquipmentHotbarSlotCount` 当前=4，HUD 画 5 槽对齐 loadout 规格。**字体**：经查引擎 `DroidSansFallback.ufont` 已 cook=默认 Roboto 复合字体含 CJK 回退，故中文标签(生命/污染/搜刮…)用默认字体即可渲染、不必先做 CJK 地基（之前"中文必豆腐块"判断有误）。**未做（后续打磨）**：等宽终端字(Share Tech Mono 等需导外部 UFont)、扫描线/暗角/受击红晕等氛围动效(需贴图/材质/UMG 动画)、把占位接真数据。重新生成 `WBP_HUD`(182KB)，编辑器+运行时两目标编过。

修订三（2026-06-10，1920×1080 画布标准化）：用户反馈 PIE 里 HUD 偏小。根因=**设计稿 `UI_Screens.html` 不是按 1920×1080 画的**——它的 `.frame` 是 `max-width:1180px` 的 16:9 预览框，里面所有 px 都相对 ~1148px 预览框；首版把这些 px **1:1 当 1920 UMG px** 用，于是小了约 1.63×。决策（用户拍板）：把 12 屏设计稿全部 1920 化、px=游戏px，再同步 UMG。落地：① `Scripts/rescale_ui_screens_to_1080.py` 把 `UI_Screens.html` 全文件每个 `<num>px` ×`1920/1148`(≈1.6725) 四舍五入（0px 保持、负号保留、JS/百分比不动）+ 注入 body zoom 视口适配 JS（1920 屏≈1:1，窄窗等比缩小）；原 1148 版备份 `UI_Screens.html.bak1148`（脚本非幂等，重跑前先 git 还原）。② `BuildHUDWBP` 加 `const float S=1920/1148` + `Px/PxI` 助手，字号/坐标/尺寸/间距统一经 Px 放大到 1920（设计 px 仍按设计稿书写、缩放集中一处；1px 描边保持 crisp 不放大）。重新生成 `WBP_HUD`(184KB)，编辑器目标编过；运行时无需重编（数据类无 px）。**约定：UMG 表现层基准分辨率=1920×1080，设计稿 px=游戏 px；后续各屏迁移同走 `Px(S=1920/1148)` 把设计稿 px 放大。**

**待用户 PIE 复看**：HUD 已是终端美术风（浮动状态条+chip+5槽武器栏+右上目标+底右AMMO）**且尺寸正常不再偏小**，中文标签正常渲染（不再乱码/豆腐块）。占位元素(污染38/buff/目标/深度/站点完整度/第5槽)显示的是设计占位值，非真实数据——这是预期内。

用户验证结果：
待验证。

## Unit 27：厚涂美术基调首试 —— 全屏后处理母版 M_Painterly_PP（NPR 第一层）

方向（用户提出，2026-06-10）：希望游戏内场景向设定文档里的概念图（`Docs/SystemDesign/img/`，Z-Image 厚涂底）的**厚涂感**靠拢。实时 3D 没有"一键变手绘"的开关，只能多层近似。本单元做**性价比最高的第一层——全屏后处理**：作用在场景已有的所有几何/资产上，不改任何模型/贴图，先让用户判断"厚涂方向对不对"。这是一次原型试探（用户「可以尝试一下」），不是锁定终态。

实现（`Scripts/CreatePainterlyPostProcess.py`，沿用 `CreateInteractOutlinePostProcess.py`/`CreateDamageVignettePostProcess.py` 的 `MD_POST_PROCESS` + `SceneTexture(PostProcessInput0)` + `Custom` HLSL → Emissive 套路）：
- 产出 `Content/Materials/Stylize/M_Painterly_PP`（母版）+ `MI_Painterly_PP`（调参实例，按项目规矩调参只在 MI 上做）。
- Custom 节点 HLSL 叠 6 层：① **各向异性 Kuwahara 油画滤镜**（取以中心为公共角的 4 个重叠象限均值，选方差最小象限——"笔刷扫过"的块面感来源，采样半径 `R=3` 编译期常量、7×7=49 次采样，笔触粗细用 `BrushSize` 缩步长）② **Posterize 色阶压缩** ③ **去饱和+提对比** ④ **冷暗/暖亮 split-tone 分离调色**（chiaroscuro）⑤ **亮度梯度边缘压暗**（画笔收边）⑥ **暗角**。`Amount` 总控 0→1，PIE 里直接 A/B。
- 全部暴露为 `Painterly` 组参数（9 标量 + 2 向量 tint），默认值已调到一个能直接看的起点。

验证：
- `UnrealEditor-Cmd -run=pythonscript -script=Scripts\CreatePainterlyPostProcess.py` 执行成功，两资产落盘；整份日志无 `LogMaterial` / `error X####` / Custom 节点编译错误（唯一 error 是早就坏的第三方 `Content/Collection_22/.../ue.uasset` 0 字节，与本单元无关）。即 **Custom HLSL 着色器编译通过**。
- 纯客户端视觉、服务器无关，无 C++ 改动，不动 `Source/`，故不涉及 `UE_CodeArchitecture.md`。
- **待用户 PIE 看实际效果**：开编辑器→把 `MI_Painterly_PP` 拖到一个 **Unbound PostProcessVolume** 的 `Post Process Materials` 数组（或相机 `PostProcess Blendables`）→PIE。调 `MI` 上参数找感觉：`Amount` 开关对比、`BrushSize` 笔触粗细、`Posterize*` 色块化、`Saturation/Contrast` grimdark 程度、`Shadow/HighlightTint` 冷暖、`EdgeStrength` 墨线、`VignetteStrength` 暗角。
- 性能：Kuwahara 每像素 ~49 次 SceneTexture 采样，5080/1080p 无压力；若要兼顾低端，降 `BrushSize` 或改脚本里 `R` 后重跑。

后续（若方向认可，逐层加）：从概念图烤 **Color Grade LUT** 替代/补强 split-tone 调色 → 场景**打光 chiaroscuro**（少光源/硬主光/深 AO/红光井 emissive）→ 风格化环境母材（压平 PBR + 笔刷 detail-normal 伪 impasto）。呼应 `Docs/SystemDesign/img/` 概念图来源与 UI 美术基调。

补充（2026-06-10，用户反馈"还是没有概念图的感觉"后）：根因=**后处理只能重染已渲染的像素，概念图的"厚"绝大部分在像素生成前（构图/打光/材质/色盘）就定了**——对一张平光写实帧再糊滤镜，只会得到"照片+油画滤镜"，不是画。即"打光 ≫ 调色 > 材质压平 > 厚涂滤镜"，我先上滤镜是把糖霜抹在了还没烤的蛋糕上。为让用户直观看到这点，建**对照小样场景** `Scripts/CreatePainterlyDemoMap.py` → `/Game/Dev/Maps/M_PainterlyDemo`：一张图两个**几何/材质/调色/厚涂滤镜全相同**的封闭石室，唯一变量是打光——左室 `RoomA_Flat`（多盏柔和面光均匀铺=常规渲染观感）vs 右室 `RoomB_Chiaro`（单盏强冷主光+深阴影+红光井 point rim=戏剧明暗）；全局无天光/方向光（地下封闭、背景黑、两室互不串光）。无界 PostProcessVolume 锁曝光(直方图 min=max=1)+参数化 grimdark grade(饱和0.82/对比1.10)+挂 `MI_Painterly_PP` blendable。看法：开图 Pilot `PainterlyDemo_ViewCamera` 看两室对比，再到 MI 拉 `Amount`(0/1) 看"平光/戏剧 × 滤镜关/开"四象限。commandlet 跑通、25 个 demo actor + blendable + 灯/曝光/grade 全经独立校验命中。**踩坑（写入 UE_Gotchas 候选，Python 内容脚本通用）：①`unreal.Color` 位置参数按 FColor 内存序 (B,G,R,A) 解析，`Color(255,38,26)` 会得到蓝色——必须 `Color(r=,g=,b=)`，否则灯光红蓝互换。②`unreal.Rotator` 位置参数是 (roll, pitch, yaw)（不是 FRotator 的 Pitch,Yaw,Roll）——`Rotator(-12,90,0)` 实得 pitch=90（镜头朝下）；摆相机/灯朝向要么写全 `Rotator(roll,pitch,yaw)`、要么命名参数。两者都靠独立校验脚本（`log_warning` 通道，Display 级 print/log 在 commandlet 下被缓冲不进捕获日志）抓出来的。** 调色这层 demo 暂用 PP 体积参数化 grade；"从概念图烤 LUT"（更贴色盘，但 LUT 贴图导入设置无法离线肉眼验证）拆成下一步单独做、一步一验。**待用户在 M_PainterlyDemo 里看对比、确认"打光才是主因"后**，再决定走哪条主线（重打光既有测试图 / 烤 LUT / 风格化材质）。

补充二（2026-06-10，用户反馈"右边太亮/左右都很亮"+"要多种光照/多种滤镜"，全程用 MCP 连实时编辑器实地截图校准）：
- **过曝真相（截图实证）**：旧 M_PainterlyDemo 把直方图自动曝光锁在 `min=max=1.0` → 强制把整屏平均亮度拉到中灰、不管灯多暗都提亮 → 两室爆白、明暗差被抹平。**多室/多情绪对比必须用手动曝光**，否则自动曝光逐帧把每室拉到中灰、抹平差异。
- **打光情绪画廊** `Scripts/CreatePainterlyLightingGallery.py` → `/Game/Dev/Maps/M_PainterlyLightGallery`：6 个几何更复杂、四面实墙隔开（杜绝串光）的石室，每室一种情绪（R1 平光基准 / R2 冷主光+红rim=概念图候选 / R3 暖工作灯池 / R4 红色收容失效+门洞逆光 / R5 冷顶光+体积雾光柱 / R6 门洞强逆光剪影），全局薄体积雾。**实测校准（截图迭代）**：手动曝光 `auto_exposure_bias` 在 UE Manual 模式是曝光补偿、**正值更亮**（我初设 +11 全爆白；校到 **-0.5**）；R1 平光点光 1100→**320**、R3 暖灯 2800/780→**1100/620**、雾 0.025→**0.008**。校准值已写回脚本+存盘。R2 看感即概念图候选（明暗/红冷/厚涂笔触齐全）。
- **多种 NPR 后处理材质** `Scripts/CreatePostProcessVariants.py` → `/Game/Materials/Stylize/`：除已有 Kuwahara 厚涂 `M_Painterly_PP` 外，新增 `M_PP_Posterize`(色阶+墨边/海报)、`M_PP_Toon`(3级量化+粗黑轮廓)、`M_PP_Watercolor`(软化+纸纹+颜料压边)、`M_PP_Grade`(亮度→grimdark渐变映射/纯色盘)、`M_PP_Sketch`(交叉排线/铅笔)。后处理是**整屏**效果（按相机位生效，非世界区域）→ 不能同屏多滤镜，正确做法=同一场景逐个换 blendable 各截一张。6 张对比图实拍于 `Saved/Screenshots/WindowsEditor/f1..f6_*.png`。**结论：Kuwahara 最贴厚涂；Grade 是可叠在任何方案上的色盘层；Watercolor 是另一支柔感；Posterize/Toon/Sketch 偏图形/线稿，离厚涂远。**
- **工作流踩坑（UE_Gotchas 候选）**：① MCP `execute_python_code` 把任何 Python warning（含弃用警告）当异常抛 → 代码开头 `warnings.filterwarnings("ignore")`。② 编辑器内 `HighResShot` 发给 GameViewport（编辑器下为空）不触发；用 `AutomationLibrary.take_high_res_screenshot`。③ **编辑器失焦时视口不实时刷新 → 高分截图排队、要等几十秒才落盘**（焦点在时~1s）；逐张换 blendable 必须等文件落盘再换否则错配。④ `SceneCapture2D` 的 `post_process_settings.weighted_blendables`（后处理材质）**不被应用**——截 NPR 滤镜只能走视口截图。

用户验证结果：
待验证。

## Unit 28：Demo 第一次下潜循环 —— 任务运行时(A) + 世界事件接线(B) + 死亡复活(E)

目标 / 背景：
把 Demo 收敛到**第一次下潜循环**（L0 醒来→修电梯上 S0 整备→再下潜清异常「活体过滤循环」→返回 S0；死亡=简单检查点复活），让 HUD 的占位（污染/buff/目标/深度）逐步接真数据。完整计划见 `~/.claude/plans/l-merry-spark.md`（用户已审批）。本单元落地计划的 A/B/E 三块（代码层，编译通过；含敌人/会话等系统全部复用，无重建）。剩 C(污染)/D(收容关卡导演)/F(HUD 接真) 待续。

实现更新（2026-06-10）：
- **A 任务运行时 / 条件求值器（Quest-C）**：
  - `World/CSObjectiveTypes.h`：`FCSObjectiveDefinition` 加 `ECSObjectiveConditionType ConditionType`(Manual/Interact/CollectItem/KillCount/ReachVolume/RepairCount/HoldTimer/WorldStateTag) + `ConditionKey`/`ConditionCount` + 奖励列 `RewardRouteIds/RewardRecipeIds/RewardSampleIds/RewardWorldStateTags`。默认 Manual → **旧表/控制台/GM 完成路径全不变**。
  - `Data/Source/Objectives.csv` 扩列并填 L0 链：`L0_FindFuse`=CollectItem `Item_Fuse`、`L0_RestoreElevatorFuse`=Interact `FuseBox_L0`+奖励解锁 `Route_S0_L0_MainElevator`、`L0_DescendToL1`=WorldStateTag `Site.FirstDescent`。`LoadObjectivesCsv` 解析新列；`GenerateGameData.py` 加 `OBJECTIVE_CONDITION_TYPES` 枚举校验 + ConditionCount 正整数校验。
  - 新 `World/CSObjectiveDirectorSubsystem`（`UWorldSubsystem`，**仅权威、无复制状态、事件驱动**）：重试绑 `ACSGameState::OnWorldProgressChanged`，按 `Order` 算"已武装"目标集（严格顺序）。被动条件(WorldStateTag)每次世界进度变化重算（带重入保护循环到稳定）；主动条件经 `Notify*` 推进，达阈值调 `ApplyObjectiveEvent` 自动完成 + 折叠奖励（幂等）。`Get(WorldContext)` 静态取用。
- **B 世界内事件接线**（全在服务端、零新增 RPC）：
  - 交互→`CSInteractionComponent::InteractOnServer` 在 `Execute_Interact` 后扫目标 actor 的 `Obj.<Key>` **actor tag**→`NotifyInteract`（任何可交互物编辑器加 tag 即挂目标，零代码）。
  - 拾取→`CSInventoryComponent::AddItem` 成功后 `NotifyCollect(ItemId,Qty)`。
  - 击杀→`ACSEnemyCharacter` 加 `EncounterGroupId` 属性，`HandleDeath`(权威)发 `NotifyKill`。
  - 到点→新 `World/CSObjectiveTriggerVolume : ATriggerBox`（`VolumeId`/`bStampCheckpoint`，服务端重叠玩家 pawn→`NotifyReachVolume` + 盖检查点）。
- **E 死亡→检查点复活**（简单，无结算屏）：
  - `CSSurvivalStatsComponent` 加 `ReviveFull()`（绕过 `ApplyHealthDelta` 死亡守卫，生命/体力/饥饿/口渴满血）。
  - `CSPlayerCharacter` 加**服务端**死亡监听 `HandleHealthChangedServer`（HP 跨 0→`ACSGameMode::HandlePlayerDied`；与本机 Feedback 分开，因 Feedback 仅本机触发）+ `ServerRespawnAtCheckpoint`（同 pawn 传送+`ReviveFull`+`ClearAllBuffs`，保背包/占有；污染清零待 C）。
  - `ACSGameMode::HandlePlayerDied`：解析复活点（`ACSPlayerState` 新 `SetCheckpoint/HasCheckpoint/GetCheckpointTransform` 瞬时检查点，回退 `FindPlayerStart`）→ 调角色复活。
  - `CSObjectiveTriggerVolume` 的 `bStampCheckpoint` 接 PlayerState 盖戳。

验证结果：
2026-06-10：**运行时目标三次增量编译均成功**（A：新子系统+objective 模型；B：交互/背包/敌人/触发体；E：survival/character/gamemode/playerstate 反射改动）。`python Scripts\GenerateGameData.py` 校验通过、重生成。

待做 / 待用户验证（PIE，需关编辑器后整编编辑器目标 + 摆放内容）：
- **死亡复活**（现可验）：受伤至 HP=0 → 同点满血复活、背包保留；客户端同步。无检查点时回退 PlayerStart。
- **任务自动完成**（需少量内容或控制台）：① 控制台 `CompleteObjective L0_FindFuse` + `CompleteObjective L0_RestoreElevatorFuse` 推进 → 坐电梯下潜（写 `Site.FirstDescent`）→ `L0_DescendToL1` 自动完成（验 WorldStateTag 路径）；② 摆一个带 `Obj.FuseBox_L0` actor tag 的可交互物 + 一个发 `Item_Fuse` 的拾取物 → 拾取自动完成 FindFuse、交互自动完成 RestoreFuse 并解锁电梯路线。用 exec `PrintQuests` 看 resolved 视图推进。
- **内容作者工作**（编辑器，用户）：L0 摆 Fuse 拾取物 + `Obj.FuseBox_L0` 交互物；按需摆 `ACSObjectiveTriggerVolume`（含 `bStampCheckpoint` 检查点）；S0/L0 各一 PlayerStart。
- 剩余单元：**C 污染**（`UCSContaminationComponent` + StatusEffect Contamination 分支 + 数据 TickInterval）、**D 收容关卡导演**（`ACSFilterNode`+`ACSContainmentEncounter`，RepairCount/HoldTimer 经求值器）、**F HUD 接真**（快照加 buff/污染/目标/深度 + WBP 重绑占位）。

用户验证结果：
待验证。

## 2026-06-12 今日进度摘要：武器预览台与游戏握持对齐 + 三把武器持枪/瞄准预览（编辑器，PIE/视口待验证）

背景：用户在 `M_WeaponPreview` 预览台调好了手枪位置，但进游戏让 GM 刷出来的枪位置对不上。根因是两条挂载路径用了不同规则——
- 游戏装备路径 `ACSPlayerCharacter::SpawnEquipmentActor` → `ACSWeaponActor::AttachGripToComponent`：把武器**右手握点 `RightHandGripComponent`** 对齐到 `hand_r`（握点落在手心）。
- 预览台 `ACSWeaponPreviewActor::RefreshWeaponActor`：把武器**根 `RootSceneComponent`** 直接贴到 `hand_r`、相对变换写死 Identity，完全没用握点；且每次刷新强制 `SetRelativeTransform(Identity)`，预览 actor 上的手动位移存不住、也无数据通道同步进游戏。握点一旦不在根原点，两边必然错位。

改动：
- `Weapons/CSWeaponPreviewActor.cpp::RefreshWeaponActor`：子武器 Actor 生成后，按 `握点相对根` 的逆变换偏移 `WeaponActorComponent`，镜像 `AttachGripToComponent` 的数学。预览台从此 = 游戏（WYSIWYG）；握点在根原点时变换为 Identity，旧表现不变。正式握持权威仍是 `BP_Weapon_*` 组件（`EquipmentDefinitions.csv` 的网格偏移列只补默认网格）。
- `Scripts/CreateWeaponPreviewStage.py`：三把预览（Knife/Pistol/Shotgun）统一开 `bUseAnimBlueprintPreview` + 指定 `ABP_Player_Body` + `PreviewEquipmentAnimSet` 取各自 Profile 的权威值；火器（Pistol/Shotgun）`bPreviewAiming` 默认开（idle 低位看不清握持，举枪姿手枪都抬到眼前），刀保持 idle；站姿↔瞄准用预览 actor 上该复选框现场切换（驱动 `UCSPlayerAnimInstance::SetPreviewState` → AimOffset，无需重编译）。
- **预览角色身体从 Manny 改为游戏真身体 `SKM_UE5_man_body`**（脚本 `PREVIEW_BODY_MESH`）。关键坑：`SK_UE5_Skeleton` 与 `SK_Mannequin` 虽互标兼容、`hand_r` 位置一致，但**绑定姿势/手骨扭转(roll)不一定一致**——武器按 `hand_r` 完整变换（含旋转）对齐，手骨朝向差几度枪就歪几度，故对着 Manny 调到正、游戏里反而歪。必须对游戏真身体调握点。详见记忆 `project-weapon-preview-stage`。
- 应用方式注记：编辑器开着时这些资产的存盘改动走 VibeUE MCP `execute_python_code` 在编辑器进程内完成；另起 `UnrealEditor-Cmd` commandlet 会被编辑器文件锁挡住（`LogSavePackage: Failed to move ... to temp directory`），存盘静默失败。

验证结果：
2026-06-12：运行时目标 `CoopSurvival Win64 Development` 编译成功（C++ 语法/链接通过）。

待做 / 待用户验证：
- C++ 改动需在编辑器内 `Ctrl+Alt+F11` 热编译（或关编辑器整编编辑器目标）后生效；**改前先存盘**。
- 重跑 `CreateWeaponPreviewStage.py`（关编辑器走 `UnrealEditor-Cmd -run=pythonscript`，或在开着的编辑器里用 MCP `execute_python_code` 跑 `main()`）配置三把 AnimBP 预览。
- 视口验证：打开 `M_WeaponPreview`，确认三把握持位置与 PIE 里 GM 刷出的武器一致；勾 `bPreviewAiming` 看瞄准姿。长枪整身瞄准集仍在 Unit 83 重构中，瞄准姿可能为半成品。

### 追加：举枪近距离持枪歪（第三人称汇聚视差）修复

诊断：预览台（已用真身体 + 举枪姿）持枪是正的，只有 PIE 里一举枪才歪 → 排除握点/骨架，定位到「枪口→准星汇聚」逻辑。第三人称摄像机在肩后、枪口在手上（视差），准星打到近处（墙/箱）时 `ResolveMuzzleToCrosshairAimOffset`(`CSPlayerCharacter.cpp:674`) 算出的汇聚角暴涨，上半身/枪为对准近点过度内收 → 举枪歪。查 `BP_Weapon_Pistol` 枪口点 `MuzzleComponent` 已被作者摆到 (18,0,4)≈枪管口（非默认 50,0,0、非回退原点），故枪口配置不是元凶，汇聚本身才是。

改动：`ACSPlayerCharacter` 加 `MinAimConvergenceDistance`(EditDefaultsOnly，默认 500cm)；`ResolveCrosshairAimTarget` 在求出准星命中点后，若命中点沿视线方向距离 < 该值，则把汇聚目标顶到最小距离处。仅影响**持枪表现/开火转身朝向**（视觉 `LocalAimTargetWorldLocation`/`ReplicatedAimTargetWorldLocation`）；**子弹判定在 `CSCombatComponent` 用摄像机射线另算（独立 trace），命中仍精确落准星，零精度影响**。设 0 = 旧行为。运行时目标编译通过。

待用户验证（PIE）：贴近墙/箱举枪，枪不再大幅内收歪斜；正常交战距离表现不变；远端代理（客户端看队友）持枪同样不歪。仍偏歪就把 `MinAimConvergenceDistance` 调大（如 800~1000）。**注意：本改加了新 UPROPERTY（反射变更），Live Coding 接不了——须关编辑器整编 `CoopSurvivalEditor` 目标后再 PIE。**

## 2026-06-12 今日进度摘要：第一人称视角切换（按 U 切 FP + 藏头，纯本地表现）

背景：玩家发现可隐藏头部，提出按 U 直接进第一人称——藏自己的头、相机放到头部。定位为**纯本地表现单元**：视角与"只藏自己脑袋"是本机表现，不碰服务器权威、不复制；队友仍看见你全身。决策（用户拍板）：相机=**眼高固定**（绑胶囊、稳不晃，非头骨绑定）；FP 下藏**头 + 头部装备**（头盔/兜帽，避免相机卡帽子里）。

改动：
- `ACSPlayerCharacter`：加第二个相机 `FirstPersonCamera`(UCameraComponent，挂胶囊、`bUsePawnControlRotation=true`、`bAutoActivate=false`，相对偏移 `FirstPersonEyeOffset` 默认 (12,0,86)≈眼高)；加 `bFirstPersonView` 本地态、`ToggleFirstPerson()`/`SetFirstPersonView(bool)`；`SetupPlayerInputComponent` 用 `BindKey(EKeys::U, ...)` 绑 U（**不新增 ini ActionMapping**，与控制器面板键 J/B/F10 同模式）。`SetFirstPersonView` 仅 `IsLocallyControlled` 生效：在 FP/TP 两相机间 `SetActive` 切换（`APawn::CalcCamera` 取激活相机），并调外观组件藏头。
- `UCSCharacterAppearanceComponent`：加 `SetHeadHiddenForOwner(bool)` + `bHeadHiddenForOwner` 标志 + `ApplyHeadHiddenState()`（对 Head/Headgear 槽部件设 `bOwnerNoSee`——仅本机拥有者看不见，队友照常看见）。`RebuildAppearance` 末尾调 `ApplyHeadHiddenState()`，换装重建后自动重新套用隐藏态，不会换衣服又把头显出来。
- 瞄准在 FP 下天然顺：相机在头、枪在手下方，视差极小，上一单元的汇聚修复几乎不偏；越肩偏移那套只对第三人称相机生效，FP 下不可见、无副作用。

true-FP 打磨（保留全身方向，用户选 A）：加 `FirstPersonFieldOfView`(默认 95，比第三人称 90 略宽) + `FirstPersonAimFieldOfView`(默认 70，ADS 收窄，0=不改)；新 `ApplyFirstPersonCameraSettings(bInstant,Dt)` 镜像 `ApplyCameraSettings`——眼高在 `StandingCameraHeight`/`CrouchedCameraHeight` 间随蹲伏插值（保留 `FirstPersonEyeOffset` 前/右分量）、FOV 按瞄准态插值。仅 FP 激活时在 `UpdateCameraHeight` 每帧调；切入 FP 时 `bInstant` 立即对位（不从默认 FOV/高度插过来）。

验证：运行时目标 `CoopSurvival Win64 Development` 编译通过（FP 主体 + 打磨两轮均干净编过）。

待做 / 待用户验证（PIE）：
- 本改加了新 UPROPERTY（`FirstPersonCamera`/`FirstPersonEyeOffset`/`FirstPersonFieldOfView`/`FirstPersonAimFieldOfView`，反射变更），**Live Coding 接不了**——须关编辑器整编 `CoopSurvivalEditor` 目标后再 PIE（可与并行的 Loadout Unit 88-91 编辑器目标整编一起做）。
- PIE 验证：按 U 切第一/三人称；FP 下自己看不到头/头盔、队友仍看见你全身；蹲下眼高跟着降、举枪 FOV 收窄。视高/前移调 `FirstPersonEyeOffset`，视野/拉近调 `FirstPersonFieldOfView`/`FirstPersonAimFieldOfView`（均 Camera 分类）。
- 后续可选：与背包/面板打开时的输入模式互斥（当前 U 仅切相机，不涉面板）；FP 下身体自阴影/近裁面穿插若明显再处理。

追加（用户要求：胸口相机 + 藏胸部）：① 隐藏从"头 bool"泛化为可配置槽集——外观组件 `SetHeadHiddenForOwner(bool)`→`SetOwnerHiddenSlots(TArray<ECSAppearanceSlot>)`、`ApplyHeadHiddenState`→`ApplyOwnerHiddenSlots`（遍历所有部件按隐藏集设 `bOwnerNoSee`），状态存 `OwnerHiddenSlots`、重建后重套。角色加 `FirstPersonHiddenSlots`(EditDefaultsOnly，默认 Head+Headgear+**Torso**)，`SetFirstPersonView` 进 FP 传该集、退出传空集。② 视点高度改由 `FirstPersonEyeOffset.Z` 真正驱动（之前被站/蹲高覆盖、Z 被忽略）：站立=Z，蹲下=Z-`FirstPersonCrouchViewDrop`(默认12)。`FirstPersonEyeOffset` 默认改 `(20,0,50)`＝胸口高度+略前移（让胸部落到近裁面之后）。**已知局限**：基础身体网格 `GetMesh()` 是一整张，**没穿上衣时裸身体胸口的皮肤单独藏不掉**——靠相机略前移规避；若仍穿插，调大 `FirstPersonEyeOffset.X`/`.Z` 或后续加"对本机隐藏整张身体"开关（会连自己手臂腿一起没，偏离全身方向）。运行时编译通过。

## 2026-06-12 敌人发声 + 受击血效优化（Enemy-Audio-A / Feedback-B，纯表现）

背景（用户要求）：给敌人加丧尸音效（巡逻 / 发现 / 攻击 等），并优化受击表现——现在血效"一直飘在头顶、且比较单一"。两件都是表现层、不碰任何权威状态。

**1) 受击血效"飘头顶"根因 + 修复**：旧 `UCSEnemyFeedbackComponent::PlayHitFeedback` 在世界命中点用 `SpawnSystemAtLocation` 生成血雾、**不挂任何东西**——血停在空中怪原先所在处，怪一走开就看着像飘在半空/头顶；且永远只用一个 `NS_ShadedBlood_1`，所以单调。改：① 把血**挂到被击中的骨骼**（`SpawnSystemAttached` + `EAttachLocation::KeepWorldPosition` 保留世界命中点）→ 血从该肢体喷出且**跟随身体移动**，不再飘半空（之前试挂骨被吸到骨轴心是因为用了 `SnapToTarget` 清零偏移，`KeepWorldPosition` 即解决）；命中无有效骨骼（胶囊命中/骨名 None）才退回旧的世界点生成。② 血效**随机变体**：构造装入 `NS_ShadedBlood_1..5`，每次命中随机挑一个（`HitBloodVFXVariants`，空则回退 `HitBloodVFX`）。③ **按伤害缩放**：伤害 `[BloodMinDamage,BloodMaxDamage]`→缩放 `[BloodMinScale,BloodMaxScale]`（轻伤一小撮、重伤一大蓬），**头部命中再 ×`HeadshotBloodScale`(1.4)**。闪白部分不动（仍默认关）。

**2) 敌人发声（新组件 `UCSEnemyVocalComponent`）**：挂敌人、纯表现 / 不复制 / 专用服务器跳过，"单口"模型（同一时刻只发一句，事件音打断、环境音让位）。声音库构造从 `Content/ThirdParty/Audio/ZombiesVoiceSFX`（Moan 01-11 / Roar 01-19，**MCP 已验 35/35 资产存在、类型 SoundCue/NiagaraSystem 正确**）按编号装入默认子集，BP/数据可整体替换换音色：
- **巡逻环境音**（本机定时循环）：按怪当前情绪从声库随机挑一句——平静=低吟（Moan 1-6，间隔 6-12s）/ 追击=躁动喘息（Moan 7-11，间隔 2.5-5s）；首次开播随机延迟，错开同屏多怪。情绪/死亡读自复制态 `IsAggressive()`/`IsDead()`。
- **一次性事件发声**：发现（Roar 1-8，接 `MulticastPlayScream`）/ 攻击（Roar 9-16，接 `MulticastPlayAttackMontage`）/ 受击痛吟（Roar 17-19，接 `MulticastPlayHitImpact`，按 `PainMinInterval` 0.6s 节流不刷屏）/ 死亡哀嚎（Moan 5/10/11，接死亡——监听服 host 在 `HandleDeath` 发、纯客户端在 `OnRep_IsDead` 发，不重复；同时停掉巡逻环境音）。
- 发声挂 Mesh 的 `head` 骨（无则 Mesh 原点）、3D 衰减复用 `CSOneShotAudio::GetSharedAttenuation`（与枪声同一套），随机音高 ±10%。可调：各声库、`MaxAudibleRange`、`IdleVolume`/`EventVolume`、音高/间隔区间、痛吟节流。

落地：`ACSEnemyCharacter` 加 `VocalComponent` 子对象 + 在上述四处表现多播里转调 `PlayVocal(...)`。运行时目标 `CoopSurvival Win64 Development` 编译通过。

待用户验证（PIE，**Listen Server + ≥1 客户端**）：① 受击血从被打的肢体喷、跟着怪走、不再飘头顶；连射 / 头部命中血量有大小差别、效果有变化。② 巡逻时听到低吟、发现你转身咆哮、扑上来攻击吼、挨打哼、死亡哀嚎；队友端也听得到（多播 + 各端本机环境音）。**注意：新增组件 + 新 UPROPERTY = 反射变更，Live Coding 接不了——须关编辑器整编 `CoopSurvivalEditor` 目标后再 PIE（可与并行的 Loadout / 第一人称单元一起整编）。** 调音观感全在组件属性上改、不用重编。

## 2026-06-12 L0「C-12 测试区」灰盒结构（脚本生成，按精确米数 + 四层标高）

背景（用户要求）：手动建模太慢，让我按设计把 L0 关卡结构在 UE 里建出来，并**注意层级和米数**。

数据源 = `Level_Structure_L0S0.html` 平面图（每个房间 `<g data-id data-floor><rect x y w h>`）+ `SD_35 §4.7` 房间标准尺寸表。**比例尺自洽**：平面图 0.20 m/px，且 rect 像素尺寸正好等于 §4.7 的米数（验证 D-Class 等待区 rect 185×215px → 37×43m ✓）→ 本脚本统一 **1px = 20cm**。四层楼面标高来自 `data-floor`：夹层 `mezz` +6m / 入口层 `entry` ±0 / 下沉收容 `sunken` −4m / 井底机房 `pit` −9m；净高按 §4.7 每间配。

落地：写了幂等脚本 `Scripts/CreateL0Blockout.py`，经 VibeUE MCP 建进**当前打开的 `Content/Maps/L0/Lvl_L0_C12_TestArea_Test`** 并存盘。建出 **21 间全房间、四层**：每间=地板+四面墙（开顶，便于俯视编辑 + 免烤光），相邻房间自动开 2.4m 门洞、空隙用走廊地板桥接，**跨层 4 条斜坡**连通（input→observe +6 / pods→containment −4 / dsearch→cryoprep −4 / service→machine −9，~32° 可走），主电梯轿厢在井底。另加：等待区 PlayerStart、可移动太阳光+天光（real-time capture，免 Build Lighting）、覆盖全层的 NavMeshBoundsVolume。共 **173 个 actor**，全在 World Outliner 文件夹 `L0_Blockout/{Entry,Mezz,Sunken,Pit}`，整组可删可重建。

验证：脚本回读确认 floor top Z = 入口 0 / 夹层 +600 / 下沉 −400 / 井底 −900（精确对齐设计标高）；footprint 226×121m（≈ §4.4 的 240×130m）。已 `save_current_level`。

待用户做 / 已知点：① AI 寻路需 **Build → Build Paths** 烤 NavMesh（体已放好）。② PIE 直接能走（PlayerStart 在等待区）。③ 灰盒=结构骨架，门洞/走廊是按"相邻就连"自动生成的近似，跨层斜坡朝向/坡度若个别不顺手动翻一下即可。④ 改尺寸/连通：改 `Scripts/CreateL0Blockout.py` 的 `RAW`（px）/`CONN`（房间对）重跑（幂等，只清自己的 `L0_Blockout` 文件夹，不碰你手摆的东西）。⑤ 房间标识：actor 标签 `L0BB_flr_<id>`/`L0BB_w<边><id>`/`L0BB_cor_*`/`L0BB_ramp_*`。

**追加（材质 + 天花 + 地下照明，2026-06-12）**：用户要求整理 UV/材质、找水泥墙地面材质、别出现重复纹理、天花封顶；并提示**这是地下世界**。

- **世界对齐(三平面)材质**解决缩放立方体 UV 拉伸：新建母材 `/Game/Maps/L0/Materials/M_L0_Greybox`——用引擎 `WorldAlignedTexture`/`WorldAlignedNormal` 按**世界坐标**投影（与网格缩放无关，墙再细再长都不拉伸、无需调 UV），叠 `T_MacroVariation` 大尺度宏变化**破重复纹理**；参数 BaseColor/Normal/TileSize/Tint/Roughness/MacroAmount。法线走世界空间（母材 `tangent_space_normal=False`）。材质源全部取自 `ThirdParty/Environment/SciFi/TreatmentStation`（含整套 `T_Concrete_*`/`T_ConcreteFlat_*`/`T_Concrete_Block_*` + `T_MacroVariation`）。
- **三个实例区分面**：墙 `MI_L0_Wall`(ConcreteFlat 水泥，冷灰) / 地 `MI_L0_Floor`(Concrete_Block 混凝土砖，暗) / 顶 `MI_L0_Ceiling`(Concrete_3 粗面，更暗)，各自不同贴图+tint+tile 尺寸。脚本 `box()` 加 `mat=` 形参按面赋 `set_material(0, MIC)`。
- **天花封顶**：每间加 `L0BB_ceil_<id>` 顶板（底面=楼面+净高）。
- **地下照明（无太阳）**：删掉方向光；天光 `L0BB_SkyFill` 仅 0.15 极暗冷色环境补光；每间天花下放**可移动点光**（`L0BB_lt_<id>_*`，免烤），**下沉收容层=红色应急灯**(1.0,0.32,0.26)，其余冷白工业灯；通高大厅更亮、长房间按长边多放几盏。共 41 盏。
- 重建后 **234 个 actor**（floor21/ceil21/wall126/cor18/ramp4/light41/PlayerStart/Sky/Nav），回读确认材质已正确赋到各面。`Scripts/CreateL0Blockout.py` 已更新为这版（含材质加载+天花+地下灯），幂等重跑即得全套。母材+实例由会话内材质脚本先建（世界对齐图全连接成功）。**注意：点光/天光都是 Movable，免 Build Lighting；AI 寻路仍需 Build Paths。**

**再追加（封廊 + 门框 + 标记 + 闪烁 + 雾，v3，2026-06-12）**：用户要"走廊封顶/红灯闪烁/摆门框交互物敌人点"，又加"基础光照 + 雾气"。

- **封廊封顶 + 门洞带门楣**：门洞从"整面墙满高开口"改成 **2.4m 宽 × 2.5m 高（`DW`/`DH`）**，**上方补门楣（header）封实**——不再从门洞上方漏光。走廊补**两侧墙 + 顶**（`make_corridor`），跨层斜坡做成**围合井道**（斜坡 + 两侧墙 + 顶，`make_ramp`），整条动线封闭。
- **门框**：每个门洞放灰盒门框（两根门柱 + 门楣，深色 `MI_L0_Ceiling`），共 44 个门洞 ×3 = 132 件。
- **交互物 / 敌人生成点标记**（占位，待换真 actor）：发光小方块（新 unlit 母材 `M_L0_Marker` + 7 个颜色实例 `MI_Mark_*`：搜刮/终端/医疗/危险/目标/电梯/敌人），按 SD_35 摆 28 个交互物 + 5 个敌人点（敌人点带 `EnemySpawn` actor tag，供后续生成器查找），均关碰撞不挡路。
- **红色应急灯闪烁**：新建光函数母材 `M_L0_Flicker`（Domain=`MD_LIGHT_FUNCTION`，Time+双 Sine 叠加→Clamp→Emissive 做不规则明灭），赋给下沉收容层的 6 盏红灯 `light_function_material`。
- **基础光照 + 雾气**：天光补光 0.15→**0.45**；新增 **`ExponentialHeightFog`（开体积雾 `volumetric_fog`）**——地下潮湿雾感，点光在雾里出光束/光晕；雾色暗冷青、density 0.035。
- 重建后 **507 个 actor**；回读确认 corridor 72 / ramp 16 / header 41 / doorframe 132 / prop 28 / spawn 5 / light 41(其中红灯 6 挂了闪烁) / fog 1。已存盘。

**踩坑（崩溃恢复）**：批量"删+建+recompile 材质"在一个 MCP 调用里触发引擎 `MassEntity` 观察者锁断言 → 编辑器崩。教训：① 材质生成**拆小步、避免 delete churn、跑脚本别开 PIE**；② 几何 spawn/destroy 不触发该断言（v2/v3 都正常）；③ 崩前关卡已存盘故无损失。另：`unreal.MaterialDomain` 成员名是 `MD_LIGHT_FUNCTION`（非 `LIGHT_FUNCTION`），`MaterialShadingModel` 是 `MSM_UNLIT`。材质资产清单（`/Game/Maps/L0/Materials/`）：`M_L0_Greybox`(世界对齐母材) + `MI_L0_Wall/Floor/Ceiling` + `M_L0_Flicker` + `M_L0_Marker` + `MI_Mark_*`×7。

**v4（跨层楼梯修复 + 大门 + 电梯，2026-06-12）**：用户反馈"二楼/一楼跨层处梯子被完全挡住"、门偏小、想要电梯结构。

- **跨层坡道彻底重做（解决"梯子被挡住"）**：v3 的坡道**居中摆在两房之间的窄缝**里→在低层门口坡道已经悬在半空 ~2m、上方还有门楣，人踩不上去=被堵。改：坡道**落在低层房间内部地面**（低端 `lowc=eH−d·R` 伸进低层房，从 zL 地面起步），按 ~27° 缓坡爬升、经低层那面墙的**满高井口**（`hdr=False`，无门楣、整面满高开口）升出去、跨缝、从高层房间的**正常地面门**进。只围合"两房之间的缝"那段井道。开口数据结构 `r['open'][side]` 从 `[中线]` 改成 `[(中线, 是否普通门)]`：同层走廊两侧都普通门(带门楣)、跨层**低层=满高井口/高层=普通门**；门框只给普通门。4 条坡道回读中心标高 +300/−200/−200/−650 全对。
- **门加大**：`DW 240→300`、`DH 250→300`（3×3m），走廊 `CW→330`、净高 `HCOR→350`（仍高于门）。
- **电梯结构**（新 `build_elevator`，`Elevator` 文件夹，10 件）：轿厢(`car`)**不封顶**，上方接**竖井实心管**（四面井壁从轿厢顶 −500 通到 **S0 楼面 +1800**）+ 顶部 S0 落点板 + 轿厢平台 + 四角导轨。井位在东侧 void 区（不撞其它房）。轿厢与机房的门保留。
- 重建后 **499 个 actor**（含电梯 10、门框 120、灯 41、坡道 4），已存盘。

**v5（真台阶 + 电梯多层停靠，2026-06-12）**：用户"1 2 做完整"。
- **坡道→真台阶**：`make_ramp` 内把单块斜坡换成**一串台阶**（每级升 ~22cm、`srun=R/n`），仍落低层房间内、保留满高井口+围合。4 条跨层共 **86 级台阶**。
- **电梯多层停靠门**：`build_elevator` 西面改成**停靠墙**（按 Z 分段：层间实心带 + 各层 `DW×DH` 门洞两侧 Y 段，13 段），井顶升到 **+2210**（S0 门 +1800 上方留墙）；每层（下沉 −400 / 入口 0 / 夹层 +600 / S0 +1800）向西伸一块**停靠平台**。回读：86 台阶 / 电梯 26 件 / 平台标高 −400·0·+600·+1800 / 井顶 +2210，全对。
- 重建后 **597 个 actor**，已存盘。**停靠平台目前是向井外伸的"挑台"（落点在 void）——意在标出电梯每层停靠口，后续把平台接到该层最近房间即成完整通道。**

**v6（电梯接最近房间 + 分级门，2026-06-12）**：
- **电梯停靠门接最近房间**：每层算出最近房间、在**朝它那面**井壁开门洞 + 接一条走廊进房间（入口→武器库 N 面 / 夹层→证物柜 W 面 / 下沉→服务通道 S 面；井底由轿厢↔机房既有门；S0 仍是 void 挑台）。井壁改成"按面、按 Z 分段、可在任意面任意层开洞"的通用 `shaft_wall(face)`，洞位存 `SHAFT_HOLES[face]=[(z,中线,dw,dh)]`。开口结构升到四元组 `(中线, 是否普通门, dw, dh)`。
- **分级门**：`DOORCLASS`+`DSIZE` 三档——**NORMAL 3.2×3.2 / KEY 4.2×3.6（武器库/观察/证物/安保/检查点/广播/电梯）/ BIG 5.6×4.4（input/pods/containment 大厅 + 机房）**；每条连接取两房较大档；走廊宽=门宽+60、走廊净高=门高+50；门洞/门楣/门框尺寸全随该连接 dw/dh。回读：input(BIG)门楣 600cm vs waiting(NORMAL)360cm，分级生效。
- 重建后 **607 个 actor**，已存盘。脚本升 v6（`make_ramp`→`make_stairs` 真台阶、通用 `shaft_wall`、`door_size`）。

**v6.1（楼梯/电梯结构修复，2026-06-12）**：用户反馈"楼梯和电梯部分结构很差"。截图诊断（编辑器需聚焦才渲染高分截图；`set_level_viewport_camera_info`+`take_high_res_screenshot`，藏 `set_is_temporarily_hidden_in_editor` 天花后俯视）确认三处真问题并修：
- **电梯=通天塔**：竖井实心 26×24m 管一直顶到 +1800(S0)，而 S0 没建→一根矗立在屋顶之上、通向虚空的巨型方柱。修：井**封顶降到 +1060**(只在 L0 内部 pit↔mezz 停靠，不强行通 S0)，去掉 S0 门洞与挑台。回读 `elev_top` 2360→1060。
- **楼梯=巨楔**：台阶宽度取了整个门宽(BIG 可达 620cm)→6m 宽大楔子塞满大厅。修：台阶**固定窄 280cm**、坡度 33°更紧凑、跨层门固定 300×≤280；井道围合随之收窄。
- **门高穿天花**：分级 BIG/KEY 门高(440/360)在净高仅 320/240 的小房/服务通道里→门框/门楣浮在天花之上。修：**所有门高夹到 `min(净高)−40`**(同层取两房、跨层取高层、电梯停靠取目标房)。
- 重建 **628 个 actor**，已存盘。截图复看：房间/墙/门洞/标记形态正常，电梯不再是塔。**停靠/楼梯细节仍建议 PIE 实走确认。**

**v7（嵌入式夹层：叠在大厅上方 + 玻璃俯瞰 + 室内楼梯，2026-06-12）**：用户选定结构优化第 1 条（夹层该"架在大厅上方"而非"隔壁的二楼"，对齐 SD_35「观察控制室位于输入室侧上方、隔玻璃俯瞰主厅」）。

- **嵌入式夹层模型**（`EMBED={'observe':'input','broadcast':'pods'}`）：观察控制室搬进**输入大厅内部**、悬在北侧 20m 条带上方（夹层板顶 +600，板底 +580 = 大厅下方仍有 5.8m 净空可走、板下是"暗厅"）；广播室同样嵌进实验舱群上方（22m 条带）。嵌入房**共用宿主四墙和顶**（不再自建墙/天花，只建楼板），从大厅里抬头直接看到夹层底/护墙/玻璃——"小可玩范围、大设施观感"。
- **玻璃观察窗**：夹层南缘 = 1.1m 矮护墙 + 0.7~1.8m **玻璃带**（新translucent 材质 `M_L0_Glass`，opacity 0.22）+ 竖梃；站观察室里隔玻璃俯瞰整个主厅（SD_35 的 money shot）。
- **室内楼梯**：大厅内沿侧墙一道 30 级直跑楼梯（input 西墙 / pods 东墙）直上夹层，护墙在楼梯口留缺——上下楼在大厅里可见可读，替代旧的"外挂楼梯井"（已删，跨层台阶从 4 组减到 3 组）。
- **开口模型升级（数据模型解耦）**：`r['open'][side]` 从"(中线,门?,dw,dh) 落地门"升级为 **(中线, dw, z0, z1) 任意高度墙洞**——墙生成器自动补"**下槛带 sill**（z0 上方悬空洞的洞下墙）+ 门楣带 header"。大厅侧墙因此能同时开"入口层大门(z 0–440) + 夹层走廊洞(z 600–880)"（横向错开、已核对全部不重叠）。这是后续"任意层间结构"的地基。
- **夹层网络重排**：值班室/证物柜（下方无大厅）平移到与夹层链对齐（值班室 y→5700-7500 悬在安全室上方岩层里、证物柜 y→4600-6900 兼顾广播室走廊与电梯夹层停靠口两侧夹距），走廊链 = 值班室↔观察室↔广播室↔证物柜↔电梯夹层口，全部经核对 cl 双侧 clamp 一致。大厅灯移到开敞区免落入夹层房内，夹层板底加暖色工作灯 ×2。
- 重建 **653 actor**；回读：observe 楼板 (10050,6700,590) 在 input 内 ✓、护墙×2/玻璃×2(M_L0_Glass ✓)/竖梃×5/室内楼梯 60 级/夹层走廊 12 件/宿主墙下槛带 4 件 ✓。已存盘。**待 PIE**：大厅里抬头看夹层+玻璃、走室内楼梯上观察室、俯瞰大厅；夹层链一路走到证物柜搭电梯下楼。

**v10（可玩化：真交互物 + 真敌人 + 走廊照明，2026-06-13）**：用户"还是不够完整"→ 把布景升级成可玩关卡，重建共 **1216 actor**，已存盘：

- **真战利品柜 ×17**：原 `loot` 占位标记全部换成 **`ACSLootContainer` 实体**（ContainerId=BasicLocker，PersistentId=`L0_Loot_<房间>_<n>` 确定性命名→重建不变、存档键稳定），分布等待区/安全室/观察室/检查点/中控/武器库/储藏×2/搜检/冷冻预备/证物/机房/岩窟/矿洞等。heal/terminal/hazard 等仍是占位标记（待对应系统单元）。
- **采石节点 ×5**：`ACSResourceNode`(ResourceType=Stone→直接进背包) 摆在矿洞×3+岩窟×2——矿洞主题第一次有真实采集玩法。
- **工作台 ×2**：`ACSWorkbenchStation` 安全屋（新手可达）+ 井底服务间（带 WORKSHOP 标牌），合成走 `UCSCraftingComponent` 就近校验。
- **真敌人 ×7**：`ACSEnemyCharacter` 直接摆放（收容×2/实验舱/服务间/检查点/岩窟×2，C++ 默认带 Mutant 模型 + `AutoPossessAI=PlacedInWorldOrSpawned`→PIE 一开 AI 即活）。原 `EnemySpawn` tag 标记保留（未来生成器数据源）。已触发 `RebuildNavigation`。
- **走廊照明**：`make_corridor` 每段自动加暖白顶灯+发光灯板（每 ~5.2m 一盏）+ 通长线缆桥架——走廊不再是黑管子。
- **脚本健壮性**：大批量销毁后 `spawn_actor_from_class` 偶发返回 None → 销毁后主动 `collect_garbage`，`box()` 失败时 GC+重试一次。
- **MegaLights 悬案了结**：引擎源码里**不存在 `r.MegaLights.Enabled` 这个 cvar**（全 Runtime/Config 搜无），之前读到的 0 是查询不存在变量的默认返回（幽灵变量，已用 `r.MegaLights.DoesNotExist` 同样读 0 实证）。真实开关=`MegaLights::IsEnabled()`=后处理 bMegaLights+Allowed+ShowFlags+HWRT 可用；本机 `Supported/Allowed/EnableForProject` 全 1、HWRT tier 1.1 已初始化、scalability 正常下发 MegaLights 参数→**实际已生效**。验证法：视口 `stat gpu` 看 MegaLights 通道耗时。
- **待 PIE（Listen Server+客户端，网络相关）**：开柜搜刮（塔科夫式进度条）、敲石头进背包、工作台合成、7 只敌人寻路/索敌/嚎叫，电梯载人。

**v9.1（性能：MegaLights 启用，2026-06-12）**：用户报"特别卡"→ 诊断三步走：① 先按传统管线整治（46 盏投影点光关影/体积雾按灯裁剪/天光改单次捕捉/197 件装饰不投影——这部分已存盘且仍保留装饰不投影）；② WMI 报 GPU="GTX 750 Ti" 一度误判为老卡、开了"生存预设"（关 Lumen/VSM/TSR/体积雾，会话级）——**用户纠正：实为 RTX 5080，WMI 名字是其自改的硬件信息**（nvidia-smi 与 UE 日志 DeviceId 2C02/16GB 证实，UE 也确选了它），预设已回滚；③ **启用 MegaLights**：`DefaultEngine.ini` 加 `r.MegaLights.EnableForProject=True`、`r.RayTracing=True`，WindowsTargetSettings `RayTracingMode=Disabled→Inline`（5.8 枚举=Disabled/Inline/Full，引擎头文件注明需 r.RayTracing 同开；MegaLights 走 inline 光追，Inline 档编译量小）。46 盏灯阴影已翻回 ON 并存盘；脚本 `plight` 默认 shadow=True（无 HWRT 机器需改回 False）。**待用户：重启编辑器**（首启编 RT shader 较久），重启后控制台验 `r.MegaLights.Enabled=1`。卡顿另一来源=v9 新摆设首次出现的 shader/PSO 现编，缓存热后自消。

**v9（场景化摆设 Set Dressing，2026-06-12）**：用户"还能优化场景，接近游戏场景"。给空盒子房间配上家具/道具/事故痕迹，全部脚本生成（共 **1099 actor**，摆设 201 件 + 灯具实体 39 + 标牌 7）：

- **真网格资产盘点并接入**（运行时 `list_assets` 建 名字→路径 索引，杜绝猜路径）：TreatmentStation 工业件（桶/箱/路障/铁丝网/发电机 Generator_2/反应堆 Reactor3/大小管线 Pipe1·Grandpipe1/电箱/风扇）、DoorInteractionKit（SM_Door/SM_MetalDoor/**SM_CardLock 门禁卡锁**）、BodyBags 尸袋全家桶、**BiomassAndLifeforms2 肉鞭 Flagella/肉花 Meat_Flower=菌丝污染实体**。
- **`prop()` 归一化摆放器**：spawn→实测 `get_actor_bounds`→按目标高度(`target_h`)或长度(`target_len`,自动转长轴朝向 `along`)归一化缩放→底面贴地（pivot 不可知也稳）。**踩坑**：①门按宽度归一化出 6.5m 巨门→**门必须按高度归一化**；②带支架的管线模型实高远超管径（SM_Pipe1_Lh 14m 长时高 3.6m）→压短压低免捅穿天花；③朝向逻辑必须 opt-in（默认 along=None），否则对称物体/发电机被乱转。
- **按房间类型摆**：输入大厅=两排输入舱(带荧光屏缝)+翻倒舱+血渍+残骸；等待区=长椅阵+翻椅+尸袋；搜检=储物柜墙；预检=扫描拱门×2(荧光横梁)+路障；储藏=货架×3+纸箱木箱油桶；武器库=铁丝网隔笼×3+枪架；医疗=病床×3+染血布盖尸+台上盖尸布；中控=L桌+**监控屏墙 3×2**；检查点=路障×2+沙袋堆；夹层观察室=**朝玻璃的控制台+倾斜屏幕**；收容室(恐怖核心)=冻存舱两排(2 翻倒)+**肉花/肉鞭长进来**+尸袋×2+血渍×4；机房(工业核心)=发电机+反应堆+大管线+电箱+风扇+油桶；岩窟喉道=**Flagella_Giant 巨肉鞭(污染源头)**+肉花；矿洞=矿工黄尸袋+杂物。
- **灯具实体化**：每盏房间点光下挂发光灯板(`MI_Fixture` 自发光,39 块)——灯不再凭空悬浮。**门扇**：10 樘门"疏散时被推开"平贴墙边(KEY 房金属门)；**门禁卡锁**×4(武器库/中控/证物/休息柜,SCP 门禁叙事)。**标牌**×7：TextRenderActor 英文(C-12 INPUT CHAMBER/CONTAINMENT/ARMORY/MINE-KEEP OUT 等,**英文避 CJK 字体缺失**)。**血渍**=贴地深红薄片(`MI_Blood`)。新 MIC：MI_Fixture/MI_Screen/MI_Blood。
- 注意：轿厢(移动平台)**刻意不摆**任何道具——平台移动道具会悬空。

**v8（夹层加高 + 三类空间分区 + 纵深景观 + 岩窟/矿洞，2026-06-12）**：用户反馈夹层偏矮，并点了结构优化 #3(分区)/#4(纵深景观)，新加需求"地下可以有半露天矿洞/洞穴区域"。

- **夹层加高**：输入大厅 9→**12m**、实验舱群 9→**11m**（SD_35 本就给到 10–16m 档）；夹层板仍 +6m，但上方从 3m 压顶变 **5–6m 开敞天廊**；玻璃带加高到 1.1–3.3m、上方留空。值班室/证物柜净高升 3.4m。
- **三类空间分区（#3，SD_48）**：`ZONE` 四簇——人员/后勤(staff 暖白灯+蓝地面色带) / D级处理(dclass 冷白+橙带) / 收容监管(contain 青白+红带，下沉层仍红色闪烁应急灯) / 工业井底(industry 钠黄灯+黄带)。落地=每房**地面油漆色带**（80 条，M_L0_Marker 压暗子实例 MI_Zone_*）+ 分区灯色，一眼读出"我在哪类区"。
- **纵深景观（#4，看得见去不了）**：① 输入大厅南墙 **3.5–8m 高窗**（玻璃）后建封锁舱群 vista 室（6 个倾斜舱体+红闪+幽青灯，不可进）——"还有更深的 C-12 区被封锁"；② **防爆封锁门 ×2**（收容室东墙[替代历史上几何错位的死走廊 containment↔service，该 CONN 已移除]、机房东墙"通往 L1"），门板+工业黄条纹+红灯。
- **岩窟+矿洞（用户新需求，半露天）**：收容室南墙**满高破口**→喉道→ **75×30m 岩窟**（−4m 层）：歪扭岩壁盒(MI_L0_Rock=粗混凝土棕褐 tint)+倾斜穹顶+**塌陷天窗竖井**（上通 15m、顶部开口、6 万流明冷光打下来+体积雾=god-ray，"半露天"）+**真岩石网格**（DarkRockLandscape `SM_Rock_01..05` ×10 + AbyssalGrottos `SM_Rock_Pillar_09` ×3，按实测 bounds 缩放）+钟乳石+地面起伏浮板；岩窟西口→**矿洞隧道**（44m，坑木支架 9 组 MI_L0_Timber、暖吊灯、端头塌方碎石堆封死）。洞区配 2 敌人生成点+危险/物资标记——污染从这里上来，接活体过滤循环。NavMesh 边界扩到含洞区。
- 新材质实例：MI_L0_Rock / MI_L0_Timber / MI_Zone_*×4（均 MIC，安全模式建）。重建 **852 actor**，footprint 226×155m，已存盘。回读：input 顶 1210 ✓ / 岩窟 42 件+10 石+3 柱 ✓ / 矿洞 41 件 ✓ / vista 12 件 ✓ / 防爆门 ×2 ✓ / 分区色带 80 ✓ / 天窗灯 ✓。**待 PIE**：大厅抬头看加高后的夹层天廊；走分区看色带/灯色差；大厅高窗看封锁舱群；从收容室破口进岩窟看天窗光柱、走矿洞到塌方端头。

**v6.2（可用电梯：按钮 + 平台上下移动，2026-06-12）**：用户要"天梯按钮 + 平台上下移动效果"。**复用既有 `ACSStreamingElevatorStation`** 当"无流送的上下移动平台"——它本就是服务器权威、复制、可交互(E)、Tick 内把根(平台)在各层 `ElevatorLocation` 间 `Lerp` 移动的。配置(纯 MCP，**无需 C++ 重编**)：4 个停靠层 `Pit −915 / Sunken −415 / Entry −15 / Mezz +585`(楼面−15，平台顶≈楼面)，每层 `StreamingLevel 留空`→`GetLayerPackageName` 空→`LoadLayer/UnloadLayer` 直接早退(不流送)；`require_world_progress_access=False`(不门禁)；`use_radius 1700`。按 **E** 循环 Pit→Sunken→Entry→Mezz→(回)Pit，平台 3s 平滑移动、`Movable BlockAll` 平台=角色 MovementBase 故载着玩家走。已写进 `CreateL0Blockout.py`(folder `L0_Elevator`，清理扩到该 folder；删了原静态轿厢平台)。**坑**：`ElevatorMeshScale` 是 `EditDefaultsOnly` 且 `ApplyElevatorVisuals` 每次构造重设→实例改不动、平台锁死 1.8m。解法=**改 CDO**(`unreal.get_default_object(...).set_editor_property('elevator_mesh_scale', Vector(25,23,0.35))`)让货运大平台填满 26×24 井道；但 **CDO 改动 session-only**(重开编辑器复位)→脚本每次跑都重设 CDO+重建站点故脚本驱动下 OK；要永久(不跑脚本也大)需把 C++ 默认 `ElevatorMeshScale` 调大/改 `EditAnywhere` 再整编(待用户要)。**待 PIE 实走**：站平台上按 E 看上下移动+载人；停靠口对位/踏出顺不顺。

## Loading-A/B：加载屏接入 UE（电梯下行深度计 + 菜单→进游戏开机屏，真预加载驱动，2026-06-16）

把浏览器定稿的两屏加载动画（`Docs/SystemDesign/UI/UI_Loading.html` 下行深度计 / `UI_Boot.html` 终端开机）做进 UE。核心诉求=进度接**真实异步加载/流送**，不是假计时；顺带补电梯过场缺口（原无输入锁定、可被伤）。范围决策：两屏都做、屏内默认中文（中英双语 `T(En,Zh)`）、声效留下个单元、电梯过场=锁输入+服务器免伤、菜单→进游戏只显开机屏（深度计只给电梯）。

**新增/改动（runtime + editor 双目标均整编 Succeeded）**：
- 复用 `UCSAssetPreloadSubsystem`：加 `GetProgress()`（真 `FStreamableHandle::GetProgress` 0–1）+ `GetLoadedRequested` + `OnPreloadCompleteEvent` + **会话级预加载** `StartSessionPreload/GetSessionProgress`；`GPreloadPaths[]` 追加两个 Loading WBP 常驻。
- `UCSGameUserSettings` 加 `UILanguage`（0=EN/1=中文，默认中文）。
- 新建 `UI/CSLoadingScreenUserWidget`（抽象基类：i18n / 真进度驱动 / 扫描线+标题 RGB 撕裂壳动效 / 日志匀速逐行+finish flush / 最短显示 / 自消 RemoveScreenDeferred）+ `UI/CSDescentLoadingScreenUserWidget`（深度轨运行时建行/深度插值/到层点亮/遥测/ken-burns，`ECSDescentContext`）+ `UI/CSBootLoadingScreenUserWidget`（SCP 徽记旋转 30s+随真进度由暗变亮/终端自检日志）。
- `CSStreamingElevatorStation`：`StartTravel` 改 `MulticastLoadLayer(.,false)` **异步流送**；`Tick` 里 `CompleteTravel` **双门控**（时长到 **且** `IsTargetLayerReady()=IsLevelLoaded&&IsLevelVisible`，+8s 超时兜底，防落进未流送层穿地）；`BroadcastDescentTransition(show/hide)` 遍历 PC；`SetTransitImmunity` 给在场玩家属性组件设/清免伤。
- `CSPlayerController`：`ClientShowDescentTransition(From,To,Duration)`/`ClientHideDescentTransition()` Client RPC（PushScreen 下行屏→UIOnly 锁输入；Hide→MarkArrived 补满自消）；`BeginPlay` 本地分支 PushScreen 开机屏（读真预热进度、自消，盖 OpenLevel 黑屏间隙）。
- `UCSAttributeComponent::MitigateDamage`（近战/枪械/敌人统一伤害入口）加 `IsTransitInvulnerable()` 守卫→过场返回 0 伤害（服务器权威）。
- `CSWidgetAuthoringLibrary` 加 `BuildDescentLoadingWBP`/`BuildBootLoadingWBP`（返回 bool，守 Gotcha 3.4）；`Scripts/CreateLoadingWBP.py` 跑 commandlet 出 `/Game/UI/Loading/WBP_DescentLoading`+`WBP_BootLoading`（复用现成 `T_MainMenu_BG` 深井底图 + `T_Emblem` SCP 徽记，`[CSWidgetGen] OK` ×2 已落盘 73KB/63KB）。commandlet 退码 1 = 既有坏档 `BFL_HelpfulFunctions`（480 无关错误），看 OK 标记。

**真进度**：开机屏 bar=`GetProgress()`；电梯屏 bar 按过场时长 smoothstep、到层前封顶 0.98，服务器确认到层（loaded+visible）→`MarkArrived` 补满自消（"条满==真就绪"）。

**待 PIE（Listen Server + 1 客户端，网络相关必须）**：① 主菜单→新建/继续→开机屏盖黑屏、进度=真预热、自检日志推进、消失到已填充世界（继续=已套存档，host+client 各自）；② 世界内两端坐主电梯→下行深度计屏（深度轨逐层亮/真流送进度/UIOnly 锁输入/过场免伤/到层 `bTraveling=false`+`OnRep_CurrentLayerId` 同步撤屏）；③ 设置切 ReduceMotion→动效冻结、进度仍真；④ 中文默认无豆腐、EN 可切（`UILanguage`）；⑤ 节流客户端进度/到层仍诚实。**后续单元**：Loading-C 撤离屏（UI_Evac）、加载屏声效（SM_LoadingBroadcast submix + 音频资产 + PA 人声）、贴图可换更专属（现复用菜单底图/徽记）。

### Loading-Fidelity：质感整改（代码生成 WBP 全纯色块 → UI 材质套件，2026-06-16）

用户反馈"质感和网页版差很多"。三路并行审计（HTML 视觉处理 / 生成器实际产出 / 现有 UMG 工具）定位根因：**生成器只会 `FSlateColorBrush` 纯色块**，工程缺 UI 渐变/扫描线/辉光材质，画不出网页那套渐变+外发光+满屏细 CRT 线+多层叠加。用户拍板"全套材质套件"。

- 新建 `Scripts/CreateUIMaterials.py`（被 `CreateLoadingWBP.py` 同进程先 `exec`，保证生成器 `LoadObject` 能拿到）：4 个 **MD_UI 半透明**材质——`M_UI_Vignette`（径向暗角，井道纵深）/`M_UI_Scanline`（满屏细 CRT 线，`ScrollY`/`Flicker` 运行时驱动）/`M_UI_GradientFill`（双色渐变+顶部高光）/`M_UI_Glow`（径向柔光）+ 2 渐变实例 `MI_UI_GradWarm`(琥珀→红,下行)/`MI_UI_GradCold`(青→冰,开机)。逻辑收在 Custom HLSL 节点、可调值走 Scalar/Vector 参数节点（照 `CreateInteractOutlinePostProcess.py` 套路）；create-only 先删后建，避免 commandlet 重建现存材质图崩(!IsRooted)。
- `CSWidgetAuthoringLibrary` 两个 Build 函数改用 **`FSlateMaterialBrush`**（`SetBrushFromMaterial` / 进度条 `FillImage` 设材质 resource + 填充色白）：加满屏暗角层、把单条红扫描线换成满屏 CRT 材质（盖内容上层）、进度条/竖向计走渐变实例、大深度数字 + SCP 徽记加 `M_UI_Glow` 背衬。`LoadUIMat` 材质缺失全部 null-safe 退回原纯色，不崩。
- 运行时：基类 `CSLoadingScreenUserWidget` 扫描线改驱动材质 `ScrollY`/`Flicker`（`GetDynamicMaterial`；非材质画刷退回老的整层平移）；新增绑定 `DepthGlow`(下行)/`EmblemGlow`(开机) `UImage`，子类按进度+呼吸脉冲 `Intensity` 并设 `GlowColor`。
- **开发开关 `CS.UI.LoadingScreens`**（`CSPlayerController` cvar）：`-1`=自动（**PIE 跳过开机屏**、下行屏照常）`0`=全关（连过场输入锁）`1`=全开（PIE 也演）；可写进 `DefaultEngine.ini [ConsoleVariables]`。解决"编辑器里每次 Play 都弹开机屏"。
- **runtime + editor 双目标整编 Succeeded**；commandlet `[UIMat] OK`×6 + `[CSWidgetGen] OK`×2，材质 + WBP 重生成落盘（WBP 涨到 82KB/70KB）。**待 PIE**：暗角纵深 / 满屏细扫描线（不再是红横条）/ 大数字+徽记发光 / 琥珀→红渐变进度条 是否对齐网页观感；ReduceMotion 下动效冻结但仍可读；`CS.UI.LoadingScreens 1` 能在 PIE 演开机屏。

## CodeReview-A/B/C：全模块代码审查 + 三批修复落地（屎山度/性能，2026-06-16）

用户要求"review 代码确保不是屎山、性能优秀"。分六块并行静态审查 `Source/CoopSurvival`（178 文件 / ~3.86 万行），**屎山度 ~3.9/5**——非屎山，架构清醒（服务端权威/事件驱动/Tick 自律核实均真做到）。完整发现 + 落地状态见 `Docs/Development/CodeReview_2026-06-16.md`。用户决定 **A（真问题）/B（性能）/C（维护债）三批全做**。

**已落地（runtime + editor 双目标整编 Succeeded，待 PIE 验收，A1/A2 为网络项必须 Listen Server+客户端）**：
- **A 真问题**：A1 GM/世界状态 13 个 Server RPC 加 `ACSPlayerController::IsGMActionAuthorized()` 房主闸（`#if UE_BUILD_SHIPPING` no-op，防客户端 `GiveCredits`/`CompleteObjective` 作弊；外观 3 RPC 故意不闸）；A2 loot `RemainingItems` 去 `DOREPLIFETIME` 改 `ClientOpenLootContainer` 定向下发 + HUD 读本地副本 `OpenLootContainerItems`（塔科夫"搜完才揭示"联机保密，Host 路径自洽）；A3 `bDrawDebugMeleeTrace/Firearm` 默认 false。
- **B 性能**：B1 melee `TInlineAllocator`+武器/QueryParams 窗口缓存（消每 tick 5 TArray+重复 Cast）；B2 枪口闪光复用常驻 `UPointLightComponent`（消每发 new/destroy）；B3 单发枪快路径（跳 3-TMap）；B4 `UCSItemDataSubsystem::FindItemDefinition`/`GetItemWeight` 只读指针 API（负重重算改用，免深拷整定义）；B5 `RecipesByStation` 缓存（HUD 路径免每帧扫全表+排序）；B6 网格增量 occupancy（`ReflowAll`/`SetInventoryStacks` O(n²)→O(n·W·H)）；B7 `CSHUDUserWidget::NativeTick` 哈希门控；B8 交互焦点 `OverlapBuffer` 复用 + 锥判定传参免重算；B9 AI 三处 bank BeginPlay 压实免临时数组；B10 持握姿态跳过专用服务器；B11 脚步组件缓存指针；C9 三元加括号。
- **C 维护债**：C1 Controller 收口（`GetOwningInventory()` 收 ~18 处双解包 + `CS_DISPATCH_TO_SERVER` 宏收 23 组转发 + `CreateViewportWidget` 收 7 处面板创建；不对称组如装备族 D4 保留两分支不强并）；C2 删 5 对退役 Slate（`SCSHUDWidget`/`SCSGMPanelWidget`/`SCSCoopSessionPanelWidget`/`SCSNarrativeOverlay`/`SCSDialogueWidget`）+ 废 include；C3 敌人**两处**掉落改共享 `UCSItemDataSubsystem::RollLootTable`；C4 新建 `World/CSInteractableActorBase`（抽 `SetFocused`/`IsInUseRange`，**只抽行为不挪 UPROPERTY/组件**防关卡实例调参丢失；门/开关保留 override 调 `Super` 记 `FocusedPawn`；顺带消 C7 死代码）；C8 `MaxLootContainerUseDistance` 单一常量。

**故意未做 / 留后续**：C3 容器侧 `EnsureLootGenerated` 用种子化 `FRandomStream`（需 `RollLootTable(Id, FRandomStream&)` 重载）；A2 敌人尸体 `CorpseLoot` 仍全场复制（同法待修）；A1 外观 3 RPC；C5（拆超长函数）、C6（CSV 解析器抽共享）、`IsHeadHit/IsHeadBone` 去重、D1–D8 其它正确性观察（EndPlay 清 Timer / Server RPC `WithValidation` / loot `bSearchClaimed` 锁不一致 等）。

**配套文档**：新建可视化 **程序框架图** `Docs/SystemDesign/程序框架图.html`（`UE_CodeArchitecture.md` 镜像：分层架构主图/模块职责/数据所有权与权威/请求流/Tick 纪律），已挂进门户导航（`_portal` 新增"工程"栏）。`UE_CodeArchitecture.md` §6 加审查批次条目。

## 常用命令

成功打包命令：

```powershell
& 'F:\Unreal\UE_5.8\Engine\Build\BatchFiles\RunUAT.bat' BuildCookRun -project='F:\Unreal\CoopSurvival\CoopSurvival.uproject' -noP4 -platform=Win64 -clientconfig=Development -skipbuild -cook -stage -pak -skipiostore -archive -archivedirectory='F:\Unreal\CoopSurvival\Saved\Packages\Win64' -map=/Game/FirstPerson/Lvl_FirstPerson -AdditionalCookerOptions=-SkipZenStore -NoUBA -NoXGE -utf8output
```

常用 Editor 编译命令：

```powershell
& 'F:\Unreal\UE_5.8\Engine\Build\BatchFiles\Build.bat' CoopSurvivalEditor Win64 Development -Project='F:\Unreal\CoopSurvival\CoopSurvival.uproject' -NoHotReload -NoLiveCoding -NoUBA -NoXGE -NoUBTMakefiles -UBARootDir='F:\Unreal\CoopSurvival\Saved\UnrealBuildTool\UBA'
```

常用运行目标编译命令：

```powershell
& 'F:\Unreal\UE_5.8\Engine\Build\BatchFiles\Build.bat' CoopSurvival Win64 Development -Project='F:\Unreal\CoopSurvival\CoopSurvival.uproject' -NoHotReload -NoLiveCoding -NoUBA -NoXGE -NoUBTMakefiles -UBARootDir='F:\Unreal\CoopSurvival\Saved\UnrealBuildTool\UBA'
```

常用数据生成命令：

```powershell
python Scripts\GenerateGameData.py
```

用户验证回报格式：

```text
单元：
结果：通过 / 失败
发生了什么：
错误文本或截图：
```
