# 手动测试 10 · 进程与养成系统 > **测试类型**:Unity Editor 手动测试(Play Mode) > **覆盖模块**:`BaseGames.Skills`、`BaseGames.Equipment`、`BaseGames.Quest`、`BaseGames.Progression`、`BaseGames.World.Shop` > **依赖组件**:`SkillManager`、`EquipmentManager`、`QuestManager`、`AchievementManager`、`ShopKeeper` > **场景要求**:含存档点、商店 NPC、任务触发器的完整测试场景 --- ## 快速工具 | 工具 | 用途 | 菜单路径 | |------|------|----------| | **Validate All ScriptableObjects** | 遍历所有实现 `IValidatable` 的 SO,输出验证结果(含 FormSkillSO、QuestDatabaseSO 等) | `BaseGames → Tools → Validate All ScriptableObjects` | > **注意**:PlayModeDebugOverlay 已移除。Geo 注入、能力解锁测试等请通过 Inspector 直接修改对应 SO/Manager 字段,或临时编写 Editor 脚本触发。 > Tab 10 的场景摆放工具(Add Quest Trigger、Add Shop NPC)已不再提供;请参照下方各节**手动步骤**手工创建对应对象。 > > **重要:本项目没有"技能点 + 技能树解锁"系统。** 技能(`FormSkillSO`)随形态绑定,由 `FormController` 在切换形态时注入 `SkillManager`,无需花费技能点解锁;`SkillTreePanel` 仅为只读的形态技能一览面板。能力(二段跳/冲刺/形态等)通过 `AbilityType` 位掩码解锁(见 MT-PROG-06),不涉及任何技能树。 **典型工作流**: 1. 测试前:`BaseGames → Tools → Validate All ScriptableObjects` 一键确认 SO 存在;若有缺失,Console 给出路径提示。 2. `MT-PROG-01` 技能:进入 Play Mode,通过 `FormController` 切换形态,确认 `SkillManager` 注入对应形态的三个技能,施放消耗魂力/灵力并进入冷却。 3. `MT-PROG-03` 任务:手动放置 `QuestGiver` NPC(见下方步骤),将 `QuestSO` 拖入 Inspector,Play Mode 中交互验证。 4. `MT-PROG-05` 商店:手动放置 `ShopNPC`(见下方步骤),通过 Inspector 修改 `_geo` 字段给玩家加钱,交互购买。 --- ## 目录 1. [前置数据检查](#1-前置数据检查) 2. [MT-PROG-01:形态技能与施放](#mt-prog-01形态技能与施放) 3. [MT-PROG-02:装备系统(护符/武器)](#mt-prog-02装备系统护符武器) 4. [MT-PROG-03:任务系统(QuestManager)](#mt-prog-03任务系统questmanager) 5. [MT-PROG-04:成就系统(AchievementManager)](#mt-prog-04成就系统achievementmanager) 6. [MT-PROG-05:商店系统(Shop)](#mt-prog-05商店系统shop) 7. [MT-PROG-06:能力解锁(AbilityUnlock)](#mt-prog-06能力解锁abilityunlock) --- ## 1. 前置数据检查 | 资产 | 路径(示例) | 必要性 | ✓ | |------|------------|--------|---| | FormSkillSO ×N | `Assets/_Game/Data/Skills/SKL_*.asset` | 技能测试必须(每形态 3 个:魂技+灵技1+灵技2) | ☐ | | EquipmentSlotConfigSO | `Assets/Data/Equipment/SlotConfig.asset` | 装备测试必须 | ☐ | | QuestDatabaseSO | `Assets/Data/Quests/` | 任务测试必须 | ☐ | | AchievementDatabaseSO | `Assets/Data/Achievements/` | 成就测试必须 | ☐ | | ShopInventorySO | `Assets/Data/Shop/` | 商店测试必须 | ☐ | > **🔧 一键检查 + 资产创建** > > **步骤 1 — 验证资产存在性**: > 菜单 `BaseGames → Tools → Validate All ScriptableObjects` > - Console 输出每项 ✅(通过)或 ❌(失败/未找到) > > **步骤 2 — 创建所有占位 SO**(若有缺失): > 按照下方**步骤 3** 手动通过 Project 右键菜单创建对应资产。 > > **步骤 3 — 创建尚未覆盖的数据资产(手动)**: > > | 资产 | 创建方法 | > |------|----------| > | `FormSkillSO` | Project 右键 → Create → BaseGames → Skills → Form Skill,保存到 `Assets/_Game/Data/Skills/`,命名 `SKL_{skillId}`;如有近战/爆炸判定再配套 `SKL_{skillId}_HitBox` 预制体 | > | `EquipmentSlotConfigSO` | Project 右键 → Create → BaseGames → Equipment → Slot Config,保存到 `Assets/_Game/Data/Equipment/` | > | `QuestDatabaseSO` | Project 右键 → Create → BaseGames → Quest → Quest Database,保存到 `Assets/_Game/Data/Quests/` | > | `AchievementDatabaseSO` | Project 右键 → Create → BaseGames → Progression → Achievement Database,保存到 `Assets/_Game/Data/Achievements/` | > | `ShopInventorySO` | 已由 Create Test Assets 创建为 `ShopInventory_Test.asset`;点 Inspector 的 `+` 按钮添加 `ShopItem` 条目 | > > **步骤 4 — 绑定 Manager 字段**(Play Mode 前): > - 找到 Player 上的 `SkillManager` 组件 → Inspector → `_formSkillSets` 数组,按形态(天魂/地魂/命魂)逐项填入 `soulSkill` + `spiritSkill1` + `spiritSkill2`(对应 `FormSkillSO` 资产) > - 找到 `ShopNPC` GameObject(Tab 10 → 添加商店 NPC)→ `_inventory` 字段拖入 `ShopInventory_Test.asset` --- ## MT-PROG-01:形态技能与施放 **目的**:验证 `SkillManager` 的形态技能注入、技能施放、冷却管理、魂力/灵力消耗,以及 `FormSkillPanel`(只读技能一览)的展示。 > **架构说明(务必先读)** > - 技能 = `FormSkillSO` 资产,**不通过技能点解锁**。每个形态绑定 3 个技能槽:魂技(`soulSkill`)+ 灵技1(`spiritSkill1`)+ 灵技2(`spiritSkill2`)。 > - `FormController` 在切换形态时回调 `SkillManager.UpdateSkillSet(...)`,把当前形态的三个技能注入到输入槽。玩家无须"学习"技能,切到该形态即可用。 > - 施放消耗的是**魂力(SoulPower)或灵力(SpiritPower)**(由 `FormSkillSO.resourceType` 决定),不是 MP。 > - `FormSkillPanel`(形态技能一览面板)只是只读一览:翻页查看各形态的技能图标/名称/描述/消耗/冷却,**没有解锁交互、没有技能点、没有节点**。 > - 技能数值可被护符改写,见 MT-PROG-02 与 `SkillModifierRegistry`。 > **🔧 资源准备** > 1. 确认 Player 上 `SkillManager._formSkillSets` 已为待测形态填好三个 `FormSkillSO`。 > 2. 若待测形态尚未解锁(地魂/命魂),先解锁其形态能力:编辑 `PlayerConfigSO.InitialAbilities` 勾选 `FormDiHun` / `FormMingHun`,或临时写 Editor 脚本调用 `PlayerStats.UnlockAbility(AbilityType.FormDiHun)`。天魂(`FormTianHun`)默认初始解锁。 > 3. 确认输入动作已绑定:魂技 = `SoulSkillEvent`,灵技1 = `SpiritSkill1StartedEvent`,灵技2 = `SpiritSkill2StartedEvent`(见 `InputReaderSO`)。 ### 步骤 **步骤 A:形态切换注入技能** 1. 进入 Play Mode,通过 `FormController` 切换到目标形态(切换输入键,或 Inspector 下调用 `FormController.SwitchForm(FormType.XXX)`)。 2. 观察 `SkillManager` 的当前技能(`SoulSkill` / `Spirit1` / `Spirit2` 属性)。 **预期**: - 切换形态后,`SkillManager` 的三个技能引用更新为该形态 `_formSkillSets` 中配置的技能。 - 冷却字典 `_cooldowns` 重建并清零(切换后立即可施放)。 - 若切换的是未解锁形态,`FormController.SwitchForm` 被 `HasAbility(FormXXX)` 拦截,技能集不变。 **步骤 B:技能施放** 1. 进入战斗场景,按魂技/灵技1/灵技2 输入键。 **预期**: - 技能动画播放(`AnimancerComponent.Play(castAnimation)`)。 - 资源扣减:`SoulPower`/`SpiritPower -= effectiveCost`(`PlayerStats.ConsumeSoulPower/ConsumeSpiritPower`)。 - 命中判定生成(若技能配了 `SkillHitBoxPrefab`,从对象池取实例并 `Activate`)。 - 进入冷却(`effectiveCooldown` 秒内再次按键无效)。 - 触发 `skill_cast` 反馈预设。 **步骤 C:资源不足时** 1. 使当前魂力/灵力 < `effectiveCost`(可在 Inspector 消耗资源或调小上限)。 2. 按技能键。 **预期**:`ConsumeXxxPower` 返回 false,技能不释放(无动画、无判定、不进冷却)。 **步骤 D:技能冷却** 1. 施放技能后立即再次按同一技能键。 **预期**:冷却期内 `_cooldowns[skill] > 0`,不触发;HUD 冷却指示(`SoulCooldownRatio`)显示剩余比例。 **步骤 E:FormSkillPanel 只读一览** 1. 打开技能一览面板(`FormSkillPanel`)。 2. 左右翻页浏览各形态,确认当前实际形态被高亮(`_activeFormIndicator`)。 **预期**:面板展示各形态三技能的图标/名称/描述/消耗/冷却;**无任何"解锁/锁定"状态或点击解锁交互**(纯查看)。 | 检查点 | 期望 | ✓ | |--------|------|---| | 形态注入 | 切换形态后 SkillManager 三技能引用随之更新 | ☐ | | 未解锁形态拦截 | 切到未解锁形态被 HasAbility 拦截 | ☐ | | 技能施放 | 动画播放,判定生成,魂力/灵力扣减 | ☐ | | 资源不足 | 无法释放,不进冷却 | ☐ | | 冷却 | 冷却期无法再次使用,比例显示 | ☐ | | 一览面板 | 只读展示各形态技能,无解锁交互 | ☐ | --- ## MT-PROG-02:装备系统(护符/武器) **目的**:验证 `EquipmentManager` 护符槽管理、装备属性叠加、超出槽数无法装备。 ### 步骤 **步骤 A:装备护符** 1. 打开装备 UI(默认 Tab 键或 E 键进入背包) 2. 拖拽/确认护符到空槽位 **预期**: - 护符装备成功,护符图标显示在槽位 - 护符效果立即生效(如 HP+20,查看 `PlayerStats.MaxHP`) - `EquipmentManager.IsEquipped(amuletId) == true` **步骤 B:槽位已满** 1. 将所有护符槽填满(`maxAmuletSlots` 个护符) 2. 尝试装备第 `maxAmuletSlots + 1` 个护符 **预期**:系统提示"护符栏已满",无法装备(不会覆盖现有护符)。 **步骤 C:卸下护符** 1. 选中已装备的护符,点击"卸下" **预期**: - 护符移回背包 - 护符提供的属性加成撤销(HP 恢复原值) **步骤 D:武器切换(FormController 联动)** 1. 切换形态(Sky/Earth/Death) **预期**:装备的武器 SO 根据形态切换,攻击力/攻击动画随形态变化。 | 检查点 | 期望 | ✓ | |--------|------|---| | 装备护符 | 效果立即生效,图标显示 | ☐ | | 槽位限制 | 超出槽数无法装备 | ☐ | | 卸下护符 | 属性加成撤销 | ☐ | | 形态武器 | 不同形态武器属性不同 | ☐ | --- ## MT-PROG-03:任务系统(QuestManager) **目的**:验证 `QuestManager` 的任务激活→进度追踪→完成→奖励全流程。 > **🔧 资源准备** > 1. 在 **Tab 10 → `添加任务触发器(QuestTrigger)`** 一键放置 `QuestTrigger` GameObject(含 CapsuleCollider2D) > 2. 在 Inspector 中将 `QuestTriggerSO`(手动创建:Project 右键 → Create → BaseGames → Quest → QuestSO)拖入 `QuestTrigger._questToStart` > 3. 确认 `QuestDatabaseSO` 中已注册该 QuestSO 条目 ### 步骤 **步骤 A:任务激活** 1. 找到场景中的任务触发器(如 NPC 对话后触发任务)或手动调用 `QuestManager.StartQuest(questId)` 2. 打开任务日志 UI **预期**:任务出现在"进行中"列表,任务目标文本正确显示。 **步骤 B:进度追踪** 1. 完成部分任务目标(如击杀 X 敌人/收集 X 物品) 2. 查看任务日志 **预期**:任务进度更新(如 "击杀 2/5 只敌人"),`EVT_QuestProgressUpdated` 事件触发。 **步骤 C:任务完成** 1. 完成所有任务目标 **预期**: - `EVT_QuestCompleted` 事件触发 - 任务移入"已完成"列表 - 奖励自动发放(Geo/能力解锁/道具) - 存档文件中 `SaveData.Quests[questId].IsCompleted == true` | 检查点 | 期望 | ✓ | |--------|------|---| | 任务激活 | 任务出现在进行中列表 | ☐ | | 进度追踪 | 完成目标后进度数字更新 | ☐ | | 完成奖励 | 奖励正确发放 | ☐ | | 持久化 | 存档中 IsCompleted == true | ☐ | --- ## MT-PROG-04:成就系统(AchievementManager) **目的**:验证 `AchievementManager` 触发条件监听、达成弹窗、持久化。 ### 步骤 1. 触发某个成就的条件(如"首次击杀 Boss"、"连续弹反 5 次"等) **预期**: - 屏幕右上角弹出成就解锁通知(`AchievementPopup`) - 通知显示成就名称和图标 - `EVT_AchievementUnlocked` 触发 2. 打开成就列表 UI **预期**:该成就显示为已解锁状态(金色)。 3. 退出并重新进入 Play Mode **预期**:成就状态仍为已解锁(`SaveData.Achievements` 持久化)。 | 检查点 | 期望 | ✓ | |--------|------|---| | 触发弹窗 | 条件达成后弹出通知 | ☐ | | 列表状态 | 成就列表中显示已解锁 | ☐ | | 持久化 | 重进后仍为已解锁 | ☐ | --- ## MT-PROG-05:商店系统(Shop) **目的**:验证 `ShopKeeper`/`ShopInventory` 的购买/售出流程、Geo 扣减、背包更新。 > **🔧 资源准备** > 1. 手动在场景中创建 `ShopNPC` GameObject,添加 `CapsuleCollider2D`,并挂载 `ShopNPC` 组件 > 2. 在 Inspector 中将 `ShopInventory_Test.asset` 拖入 `ShopNPC._inventory` > 3. 打开 `ShopInventory_Test.asset`,在 Inspector 展开 `_items` 数组,添加几个 `ShopItem`(配置 itemId、price、count) > 4. Play Mode 中在 Inspector 直接将 `PlayerController` / `GeoManager._geoCount` 设为 500 快速获取购物用 Geo ### 步骤 **步骤 A:打开商店** 1. 走到商店 NPC,按交互键 **预期**:商店 UI 打开,显示 `ShopInventorySO` 中的物品列表(价格、图标、名称)。 **步骤 B:购买物品** 1. 选择一个 Geo 充足的物品,确认购买 **预期**: - `CurrentGeo -= item.price` - 物品出现在背包 - HUD Geo 数量更新 **步骤 C:Geo 不足** 1. 选择价格超过当前 Geo 的物品 **预期**:购买失败,提示"Geo 不足",Geo 不变。 **步骤 D:售出物品** 1. 在商店卖出背包中的物品 **预期**: - `CurrentGeo += item.sellPrice` - 物品从背包移除 - HUD Geo 数量更新 | 检查点 | 期望 | ✓ | |--------|------|---| | 商店 UI 打开 | 物品列表正确显示 | ☐ | | 购买成功 | Geo 减少,物品进背包 | ☐ | | Geo 不足 | 购买失败,Geo 不变 | ☐ | | 售出 | Geo 增加,物品移除 | ☐ | --- ## MT-PROG-06:能力解锁(AbilityUnlock) **目的**:验证特殊能力(DoubleJump、WallCling、Dash 等 `AbilityType` 位)的解锁与 `AbilityGate` 联动。 ### 步骤 1. 确认某能力(如 DoubleJump)当前**未解锁** 2. 找到对应的能力解锁点(Boss 击败后掉落,或特定区域触发) 3. 触发解锁 **预期**: - `EVT_AbilityUnlocked(AbilityType)` 触发(`AbilityTypeEventChannelSO`,payload 为解锁的能力位) - `PlayerStats.HasAbility(ability) == true`(位掩码 `_unlockedAbilities |= ability`) - 存档中 `SaveData.Player.AbilityFlags` 含该能力位(`(uint)_unlockedAbilities`) - 对应 `AbilityGate` 自动开启(若当前场景有联动门) 4. 测试新解锁的能力(如 DoubleJump:跳跃后再次跳跃) **预期**:能力生效(二段跳可用)。 | 检查点 | 期望 | ✓ | |--------|------|---| | 解锁事件 | EVT_AbilityUnlocked 触发 | ☐ | | HasAbility == true | PlayerStats.HasAbility 返回 true | ☐ | | AbilityGate 开启 | 对应能力门自动开启 | ☐ | | 能力可用 | 新能力实际可使用 | ☐ | | 持久化 | 存档 AbilityFlags 含该能力位 | ☐ |