[P0][CR-B1] 玩家状态双源:HP/MP/金币/背包在 GameEngine 与 HudManager 各存一份 #520

Closed
opened 2026-09-29 06:43:55 +00:00 by troytt · 1 comment
Owner

来源:#516|优先级 P0|评审编号 B1|基线 e475c2e

问题描述

玩家的血量、法力、金币和背包,在 GameEngine(模拟与存档)和 HudManager(界面)里各存了一份。两份由不同的代码路径修改,而且没有同步机制。直接后果:

  • 满血球却死亡:受伤只扣引擎的血,血球显示的却是 HUD 的数。
  • 喝药无效:喝药只回 HUD 的数,不回真实血量。
  • 蓝量判断失真:施法先按 HUD 的蓝量判断能不能放,再分别扣两边。
  • 可以刷金币:金币是两本账,从 HUD 丢金币再捡回来,会凭空生成金币(#521,已复现)。
  • 存档丢物品:心灵传动捡到的物品进了 HUD 网格,存档里却没有。

证据

1. 初值有三套,没有一套是 D2 的值

以默认的女法师为例:

来源 HP / MP 位置
HUD 字段初值和 switchClass 1180 / 940(85 级展示角色 Isendra) hud-manager.ts:264-267、class-starter-profiles.ts:1540-1541
场景启动时,act-scene 用 HERO_CLASSES 覆盖 HUD 850 / 620 act-scene.ts:697-700、scene-population.ts:315
引擎 createWorld 的默认参数 60 / 30 combat.ts:435(由 engine.ts:332 调用,没有传职业数值)
CharStats.txt(1.13c 真值;仓库里已有,但生产代码没用) 40 / 35 classes.ts:105、calculateVitalsAtLevel(classes.ts:396)

之后在工具栏切换职业,只会调用 switchClass(toolbar-chat-ui.ts:401),用的又是 CLASS_STARTER_ATTRS。所以同样是亚马逊,开局就选是 920 / 320,中途切过去是 1250 / 450。引擎则始终是 60 / 30。

2. 两份各自被修改

事件 引擎 HUD 位置
受到伤害 改 不改 combat.ts:1218、combat.ts:1321
回蓝 改 不改 engine.ts:509
喝药 不改 改 hud-manager.ts:708-715
NPC 治疗 改 改(两边分别写) mouse-controller.ts:808-813
加点(体力 / 精力) 不改 改,并直接回满 hud-manager.ts:987-996
普通施法 扣蓝 扣蓝(先查 HUD 蓝量,再两边分别扣) skill-caster.ts:190、skill-caster.ts:199-201
Inferno 施法 扣蓝 被引擎蓝量直接覆盖 skill-caster.ts:152-155、skill-caster.ts:174-177
点击拾取 / 心灵传动拾取金币 记账 记账(两本账各记一笔) mouse-controller.ts:458-464、skill-caster.ts:373-379
走路自动拾取金币 记账 不记 engine.ts:1093-1102
买、卖、赌博、修理、存取仓库金币 不改 改 world-panels.ts:1031、world-panels.ts:1069、world-panels.ts:1130、world-panels.ts:1205、world-panels.ts:1262、world-panels.ts:1800-1816
心灵传动拾取物品 有 HUD 时不进引擎背包 进 HUD 网格 skill-caster.ts:407

补充说明:

  • HP 没有任何 engine→HUD 的同步。MP 只有 Inferno 分支会把引擎蓝量覆盖到 HUD,而开局时引擎的蓝量上限只有 30。
  • 死亡动画看的是引擎 HP(world-renderer.ts:194)。
  • 存档只序列化引擎这一份(engine.ts:1178-1182)。HUD 里的装备、背包网格、仓库,以及交易���得失,都不进存档。
  • 同一本 HUD 账里,金币上限也不一致:

3. 成因(git 历史)

双源早于 e475c2e,不是那次拆分造成的,但那次拆分也没有修它。

  • 09-13 897b373(初始提交):引擎血蓝为 60 / 30。
  • 09-21 e2a74cc(完整 UI/HUD 系统):HUD 自带展示用的数值,喝药只改 HUD。同一个提交加入了正确的 classes.ts,但没有接入。
  • 09-24 b5754c0(#407 拾取桥接):拾取改成两边各记一笔。
  • 09-29 e475c2e:把 act-scene.ts 里的这些双写代码原样搬进了新建的 scene/backend/skill-caster.ts 和 scene/frontend/mouse-controller.ts。原位置在父提交的 L2200、L2222、L2515、L4698。

根因

项目从来没有规定"玩家状态归谁"。

HUD 为了能独立演示,先有了一份展示用的数值。之后每个跨越前后端的功能(拾取、施法、治疗、交易),都靠"两边各写一笔"来打补丁。

修复指南

目标:引擎持有唯一的 PlayerState,HUD 只读,所有修改都通过引擎命令完成。 建议分 3 个 PR。

PR1:状态归一(最小可合入)

  1. 在 src/game/ 定义 PlayerState:
    interface PlayerState {
      classId: ClassId
      level: number
      xp: number
      stats: { str: number; dex: number; vit: number; ene: number; unspent: number }
      vitals: { hp: number; maxHp: number; mana: number; maxMana: number; stamina: number; maxStamina: number }
      gold: number
      stashGold: number
      inventory: Inventory
      belt: Belt
      equipment: Equipment
      stash: Stash
      skillLevels: Readonly<Record<number, number>>
    }
    
    它由 GameEngine 持有,并纳入 saveSnapshot。快照 schema 版本加 1;旧存档要么显式拒绝,要么写迁移。
  2. 初值:
    • 用 calculateVitalsAtLevel 计算,数据来自 CharStats.txt(LifePerLevel、ManaPerLevel、LifePerVitality、ManaPerMagic 等列)。
    • 删掉 createWorld 的 maxHp = 60, maxMana = 30 默认参数,改为必传,缺失就抛错。
    • 85 级展示角色如果要保留,就作为显式的"预设存档"加载进引擎,而不是当作 HUD 字段的初值。
  3. HUD 改为只读投影:
    • HudManager ���除 hp / maxHp / mana / maxMana / stamina 这些可写字段,以及 inventory.gold 这本账。
    • 改为每帧从引擎读取的只读投影(例如 HudView)。
    • switchClass 用的 classProfiles 快照移进引擎,或者直接删除。

PR2:写入口收敛为引擎命令

  1. 所有写操作都走 engine.execute(cmd)。命令至少包括:

    • drinkPotion(beltSlot)
    • pickup(groundId)
    • dropGold(amount)
    • buy / sell / gamble / repair
    • depositGold / withdrawGold
    • allocateStat(stat)
    • npcHeal()
    • castSkill(...)

    扣蓝、扣金币、加血都只在命令里发生一次。UI 只负责发命令和显示结果。

  2. 命令统一在 onTick 里应用,和 #523 的输入队列是同一个机制。

PR3:规则迁回 game/,并以 1.13c 为准

  1. 金币上限:统一为引擎里的一个函数,即 clvl × 10000。把 getInventoryGoldLimit 从 ui/inventory.ts 移进 game/,并去掉把 PLAYER_GOLD_CAP 当背包上限的用法(仓库上限单独处理)。
  2. 加点:1.13c 中加 1 点体力或精力,只会按 LifePerVitality / ManaPerMagic(以 1/4 为单位)同时增加当前值和最大值,不会回满。现在"加点即回满"的逻辑要去掉。
  3. 药水:回复量和持续时间来自数据表和 1.13c 的逻辑,不在 UI 里写死(见 #524 的药水部分)。
  4. 回蓝:按 1.13c 的回蓝逻辑实现,替换 engine.ts:509 的"每 25 tick +1"(见 #524)。

验收标准

  • 全仓只有引擎这一处可写的 hp / mana / gold;搜不到 HUD 对这些字段的赋值。
  • 新建女法师时,引擎和 HUD 都显示 40 / 35(CharStats.txt);换职业后的数值同样来自 CharStats.txt。
  • 新增测试覆盖以下场景:
    • 受伤后,HUD 同步下降;
    • 喝药后,引擎 HP 上升;
    • 丢金币再捡回,引擎和 HUD 的金币都不变(#521 的复现脚本转为回归测试);
    • 买卖之后存档、读档,金币一致;
    • 心灵传动捡的物品,存档后仍然在。
  • 加 1 点体力:当前生命和最大生命各增加 LifePerVitality/4,不回满。
  • npm run typecheck 0 error;npx vitest run 全部通过。

相关

  • #521:本 issue 的一个已复现后果,可以先单独止血。
  • #522:地面物品同样有两份存储。
  • #526:前后端边界。
  • #523:输入命令队列。
  • #511(WorldView 解耦):建议先完成本 issue,WorldView 才有唯一的数据源可读。
> 来源:#516|优先级 P0|评审编号 B1|基线 `e475c2e` ## 问题描述 玩家的血量、法力、金币和背包,在 `GameEngine`(模拟与存档)和 `HudManager`(界面)里各存了一份。两份由不同的代码路径修改,而且**没有同步机制**。直接后果: - **满血球却死亡**:受伤只扣引擎的血,血球显示的却是 HUD 的数。 - **喝药无效**:喝药只回 HUD 的数,不回真实血量。 - **蓝量判断失真**:施法先按 HUD 的蓝量判断能不能放,再分别扣两边。 - **可以刷金币**:金币是两本账,从 HUD 丢金币再捡回来,会凭空生成金币(#521,已复现)。 - **存档丢物品**:心灵传动捡到的物品进了 HUD 网格,存档里却没有。 ## 证据 ### 1. 初值有三套,没有一套是 D2 的值 以默认的女法师为例: | 来源 | HP / MP | 位置 | |---|---|---| | HUD 字段初值和 `switchClass` | 1180 / 940(85 级展示角色 Isendra) | [hud-manager.ts:264-267](https://git.projectdiablo2.cn/troytt/diablo2-web/src/commit/e475c2e6d8a6e85eabb607525d9d87beca9fbee3/src/ui/hud-manager.ts#L264-L267)、[class-starter-profiles.ts:1540-1541](https://git.projectdiablo2.cn/troytt/diablo2-web/src/commit/e475c2e6d8a6e85eabb607525d9d87beca9fbee3/src/game/class-starter-profiles.ts#L1540-L1541) | | 场景启动时,act-scene 用 `HERO_CLASSES` 覆盖 HUD | 850 / 620 | [act-scene.ts:697-700](https://git.projectdiablo2.cn/troytt/diablo2-web/src/commit/e475c2e6d8a6e85eabb607525d9d87beca9fbee3/src/scene/act-scene.ts#L697-L700)、[scene-population.ts:315](https://git.projectdiablo2.cn/troytt/diablo2-web/src/commit/e475c2e6d8a6e85eabb607525d9d87beca9fbee3/src/scene/backend/scene-population.ts#L315) | | 引擎 `createWorld` 的默认参数 | 60 / 30 | [combat.ts:435](https://git.projectdiablo2.cn/troytt/diablo2-web/src/commit/e475c2e6d8a6e85eabb607525d9d87beca9fbee3/src/game/combat.ts#L435)(由 [engine.ts:332](https://git.projectdiablo2.cn/troytt/diablo2-web/src/commit/e475c2e6d8a6e85eabb607525d9d87beca9fbee3/src/game/engine.ts#L332) 调用,没有传职业数值) | | CharStats.txt(1.13c 真值;仓库里已有,但生产代码没用) | 40 / 35 | [classes.ts:105](https://git.projectdiablo2.cn/troytt/diablo2-web/src/commit/e475c2e6d8a6e85eabb607525d9d87beca9fbee3/src/game/classes.ts#L105)、`calculateVitalsAtLevel`([classes.ts:396](https://git.projectdiablo2.cn/troytt/diablo2-web/src/commit/e475c2e6d8a6e85eabb607525d9d87beca9fbee3/src/game/classes.ts#L396)) | 之后在工具栏切换职业,只会调用 `switchClass`([toolbar-chat-ui.ts:401](https://git.projectdiablo2.cn/troytt/diablo2-web/src/commit/e475c2e6d8a6e85eabb607525d9d87beca9fbee3/src/scene/frontend/toolbar-chat-ui.ts#L401)),用的又是 `CLASS_STARTER_ATTRS`。所以同样是亚马逊,开局就选是 920 / 320,中途切过去是 1250 / 450。引擎则始终是 60 / 30。 ### 2. 两份各自被修改 | 事件 | 引擎 | HUD | 位置 | |---|---|---|---| | 受到伤害 | 改 | 不改 | [combat.ts:1218](https://git.projectdiablo2.cn/troytt/diablo2-web/src/commit/e475c2e6d8a6e85eabb607525d9d87beca9fbee3/src/game/combat.ts#L1218)、[combat.ts:1321](https://git.projectdiablo2.cn/troytt/diablo2-web/src/commit/e475c2e6d8a6e85eabb607525d9d87beca9fbee3/src/game/combat.ts#L1321) | | 回蓝 | 改 | 不改 | [engine.ts:509](https://git.projectdiablo2.cn/troytt/diablo2-web/src/commit/e475c2e6d8a6e85eabb607525d9d87beca9fbee3/src/game/engine.ts#L509) | | 喝药 | 不改 | 改 | [hud-manager.ts:708-715](https://git.projectdiablo2.cn/troytt/diablo2-web/src/commit/e475c2e6d8a6e85eabb607525d9d87beca9fbee3/src/ui/hud-manager.ts#L708-L715) | | NPC 治疗 | 改 | 改(两边分别写) | [mouse-controller.ts:808-813](https://git.projectdiablo2.cn/troytt/diablo2-web/src/commit/e475c2e6d8a6e85eabb607525d9d87beca9fbee3/src/scene/frontend/mouse-controller.ts#L808-L813) | | 加点(体力 / 精力) | 不改 | 改,并直接回满 | [hud-manager.ts:987-996](https://git.projectdiablo2.cn/troytt/diablo2-web/src/commit/e475c2e6d8a6e85eabb607525d9d87beca9fbee3/src/ui/hud-manager.ts#L987-L996) | | 普通施法 | 扣蓝 | 扣蓝(先查 HUD 蓝量,再两边分别扣) | [skill-caster.ts:190](https://git.projectdiablo2.cn/troytt/diablo2-web/src/commit/e475c2e6d8a6e85eabb607525d9d87beca9fbee3/src/scene/backend/skill-caster.ts#L190)、[skill-caster.ts:199-201](https://git.projectdiablo2.cn/troytt/diablo2-web/src/commit/e475c2e6d8a6e85eabb607525d9d87beca9fbee3/src/scene/backend/skill-caster.ts#L199-L201) | | Inferno 施法 | 扣蓝 | 被引擎蓝量直接覆盖 | [skill-caster.ts:152-155](https://git.projectdiablo2.cn/troytt/diablo2-web/src/commit/e475c2e6d8a6e85eabb607525d9d87beca9fbee3/src/scene/backend/skill-caster.ts#L152-L155)、[skill-caster.ts:174-177](https://git.projectdiablo2.cn/troytt/diablo2-web/src/commit/e475c2e6d8a6e85eabb607525d9d87beca9fbee3/src/scene/backend/skill-caster.ts#L174-L177) | | 点击拾取 / 心灵传动拾取金币 | 记账 | 记账(两本账各记一笔) | [mouse-controller.ts:458-464](https://git.projectdiablo2.cn/troytt/diablo2-web/src/commit/e475c2e6d8a6e85eabb607525d9d87beca9fbee3/src/scene/frontend/mouse-controller.ts#L458-L464)、[skill-caster.ts:373-379](https://git.projectdiablo2.cn/troytt/diablo2-web/src/commit/e475c2e6d8a6e85eabb607525d9d87beca9fbee3/src/scene/backend/skill-caster.ts#L373-L379) | | 走路自动拾取金币 | 记账 | 不记 | [engine.ts:1093-1102](https://git.projectdiablo2.cn/troytt/diablo2-web/src/commit/e475c2e6d8a6e85eabb607525d9d87beca9fbee3/src/game/engine.ts#L1093-L1102) | | 买、卖、赌博、修理、存取仓库金币 | 不改 | 改 | [world-panels.ts:1031](https://git.projectdiablo2.cn/troytt/diablo2-web/src/commit/e475c2e6d8a6e85eabb607525d9d87beca9fbee3/src/ui/world-panels.ts#L1031)、[world-panels.ts:1069](https://git.projectdiablo2.cn/troytt/diablo2-web/src/commit/e475c2e6d8a6e85eabb607525d9d87beca9fbee3/src/ui/world-panels.ts#L1069)、[world-panels.ts:1130](https://git.projectdiablo2.cn/troytt/diablo2-web/src/commit/e475c2e6d8a6e85eabb607525d9d87beca9fbee3/src/ui/world-panels.ts#L1130)、[world-panels.ts:1205](https://git.projectdiablo2.cn/troytt/diablo2-web/src/commit/e475c2e6d8a6e85eabb607525d9d87beca9fbee3/src/ui/world-panels.ts#L1205)、[world-panels.ts:1262](https://git.projectdiablo2.cn/troytt/diablo2-web/src/commit/e475c2e6d8a6e85eabb607525d9d87beca9fbee3/src/ui/world-panels.ts#L1262)、[world-panels.ts:1800-1816](https://git.projectdiablo2.cn/troytt/diablo2-web/src/commit/e475c2e6d8a6e85eabb607525d9d87beca9fbee3/src/ui/world-panels.ts#L1800-L1816) | | 心灵传动拾取物品 | 有 HUD 时不进引擎背包 | 进 HUD 网格 | [skill-caster.ts:407](https://git.projectdiablo2.cn/troytt/diablo2-web/src/commit/e475c2e6d8a6e85eabb607525d9d87beca9fbee3/src/scene/backend/skill-caster.ts#L407) | 补充说明: - **HP 没有任何 engine→HUD 的同步**。MP 只有 Inferno 分支会把引擎蓝量覆盖到 HUD,而开局时引擎的蓝量上限只有 30。 - **死亡动画看的是引擎 HP**([world-renderer.ts:194](https://git.projectdiablo2.cn/troytt/diablo2-web/src/commit/e475c2e6d8a6e85eabb607525d9d87beca9fbee3/src/scene/frontend/world-renderer.ts#L194))。 - **存档只序列化引擎这一份**([engine.ts:1178-1182](https://git.projectdiablo2.cn/troytt/diablo2-web/src/commit/e475c2e6d8a6e85eabb607525d9d87beca9fbee3/src/game/engine.ts#L1178-L1182))。HUD 里的装备、背包网格、仓库,以及交易���得失,都不进存档。 - **同一本 HUD 账里,金币上限也不一致**: - 拾取走 `getInventoryGoldLimit`,即 `clvl × 10000`([inventory.ts:507-513](https://git.projectdiablo2.cn/troytt/diablo2-web/src/commit/e475c2e6d8a6e85eabb607525d9d87beca9fbee3/src/ui/inventory.ts#L507-L513)),这是 1.13c 的规则; - 卖东西和从仓库取钱,用的却是写死的 `PLAYER_GOLD_CAP = 2_500_000`([world-panels.ts:75-76](https://git.projectdiablo2.cn/troytt/diablo2-web/src/commit/e475c2e6d8a6e85eabb607525d9d87beca9fbee3/src/ui/world-panels.ts#L75-L76)、[world-panels.ts:1130](https://git.projectdiablo2.cn/troytt/diablo2-web/src/commit/e475c2e6d8a6e85eabb607525d9d87beca9fbee3/src/ui/world-panels.ts#L1130)、[world-panels.ts:1811](https://git.projectdiablo2.cn/troytt/diablo2-web/src/commit/e475c2e6d8a6e85eabb607525d9d87beca9fbee3/src/ui/world-panels.ts#L1811))。 ### 3. 成因(git 历史) 双源早于 `e475c2e`,不是那次拆分造成的,但那次拆分也没有修它。 - **09-13 [897b373](https://git.projectdiablo2.cn/troytt/diablo2-web/commit/897b3735d604a1cc98308ffa533d5f3be0e4a15b)**(初始提交):引擎血蓝为 60 / 30。 - **09-21 [e2a74cc](https://git.projectdiablo2.cn/troytt/diablo2-web/commit/e2a74cc8f054013d54c2d5def5f171cbb502bb63)**(完整 UI/HUD 系统):HUD 自带展示用的数值,喝药只改 HUD。同一个提交加入了正确的 `classes.ts`,但没有接入。 - **09-24 [b5754c0](https://git.projectdiablo2.cn/troytt/diablo2-web/commit/b5754c0197f0e25ffd10fadc4986e60c3d4044a4)**(#407 拾取桥接):拾取改成两边各记一笔。 - **09-29 [e475c2e](https://git.projectdiablo2.cn/troytt/diablo2-web/commit/e475c2e6d8a6e85eabb607525d9d87beca9fbee3)**:把 `act-scene.ts` 里的这些双写代码原样搬进了新建的 `scene/backend/skill-caster.ts` 和 `scene/frontend/mouse-controller.ts`。原位置在父提交的 [L2200](https://git.projectdiablo2.cn/troytt/diablo2-web/src/commit/9077b5889d5eb018c3c6b202f2965840ee381034/src/scene/act-scene.ts#L2200)、[L2222](https://git.projectdiablo2.cn/troytt/diablo2-web/src/commit/9077b5889d5eb018c3c6b202f2965840ee381034/src/scene/act-scene.ts#L2222)、[L2515](https://git.projectdiablo2.cn/troytt/diablo2-web/src/commit/9077b5889d5eb018c3c6b202f2965840ee381034/src/scene/act-scene.ts#L2515)、[L4698](https://git.projectdiablo2.cn/troytt/diablo2-web/src/commit/9077b5889d5eb018c3c6b202f2965840ee381034/src/scene/act-scene.ts#L4698)。 ## 根因 项目从来没有规定"玩家状态归谁"。 HUD 为了能独立演示,先有了一份展示用的数值。之后每个跨越前后端的功能(拾取、施法、治疗、交易),都靠"两边各写一笔"来打补丁。 ## 修复指南 目标:**引擎持有唯一的 `PlayerState`,HUD 只读,所有修改都通过引擎命令完成。** 建议分 3 个 PR。 ### PR1:状态归一(最小可合入) 1. 在 `src/game/` 定义 `PlayerState`: ```ts interface PlayerState { classId: ClassId level: number xp: number stats: { str: number; dex: number; vit: number; ene: number; unspent: number } vitals: { hp: number; maxHp: number; mana: number; maxMana: number; stamina: number; maxStamina: number } gold: number stashGold: number inventory: Inventory belt: Belt equipment: Equipment stash: Stash skillLevels: Readonly<Record<number, number>> } ``` 它由 `GameEngine` 持有,并纳入 `saveSnapshot`。快照 schema 版本加 1;旧存档要么显式拒绝,要么写迁移。 2. **初值**: - 用 `calculateVitalsAtLevel` 计算,数据来自 CharStats.txt(`LifePerLevel`、`ManaPerLevel`、`LifePerVitality`、`ManaPerMagic` 等列)。 - 删掉 `createWorld` 的 `maxHp = 60, maxMana = 30` 默认参数,改为必传,缺失就抛错。 - 85 级展示角色如果要保留,就作为显式的"预设存档"加载进引擎,而不是当作 HUD 字段的初值。 3. **HUD 改为只读投影**: - `HudManager` ���除 `hp / maxHp / mana / maxMana / stamina` 这些可写字段,以及 `inventory.gold` 这本账。 - 改为每帧从引擎读取的只读投影(例如 `HudView`)。 - `switchClass` 用的 `classProfiles` 快照移进引擎,或者直接删除。 ### PR2:写入口收敛为引擎命令 4. 所有写操作都走 `engine.execute(cmd)`。命令至少包括: - `drinkPotion(beltSlot)` - `pickup(groundId)` - `dropGold(amount)` - `buy` / `sell` / `gamble` / `repair` - `depositGold` / `withdrawGold` - `allocateStat(stat)` - `npcHeal()` - `castSkill(...)` 扣蓝、扣金币、加血都只在命令里发生一次。UI 只负责发命令和显示结果。 5. 命令统一在 `onTick` 里应用,和 #523 的输入队列是同一个机制。 ### PR3:规则迁回 `game/`,并以 1.13c 为准 6. **金币上限**:统一为引擎里的一个函数,即 `clvl × 10000`。把 `getInventoryGoldLimit` 从 `ui/inventory.ts` 移进 `game/`,并去掉把 `PLAYER_GOLD_CAP` 当背包上限的用法(仓库上限单独处理)。 7. **加点**:1.13c 中加 1 点体力或精力,只会按 `LifePerVitality` / `ManaPerMagic`(以 1/4 为单位)同时增加当前值和最大值,不会回满。现在"加点即回满"的逻辑要去掉。 8. **药水**:回复量和持续时间来自数据表和 1.13c 的逻辑,不在 UI 里写死(见 #524 的药水部分)。 9. **回蓝**:按 1.13c 的回蓝逻辑实现,替换 `engine.ts:509` 的"每 25 tick +1"(见 #524)。 ## 验收标准 - [ ] 全仓只有引擎这一处可写的 `hp / mana / gold`;搜不到 HUD 对这些字段的赋值。 - [ ] 新建女法师时,引擎和 HUD 都显示 40 / 35(CharStats.txt);换职业后的数值同样来自 CharStats.txt。 - [ ] 新增测试覆盖以下场景: - 受伤后,HUD 同步下降; - 喝药后,引擎 HP 上升; - 丢金币再捡回,引擎和 HUD 的金币都不变(#521 的复现脚本转为回归测试); - 买卖之后存档、读档,金币一致; - 心灵传动捡的物品,存档后仍然在。 - [ ] 加 1 点体力:当前生命和最大生命各增加 `LifePerVitality/4`,不回满。 - [ ] `npm run typecheck` 0 error;`npx vitest run` 全部通过。 ## 相关 - #521:本 issue 的一个已复现后果,可以先单独止血。 - #522:地面物品同样有两份存储。 - #526:前后端边界。 - #523:输入命令队列。 - #511(`WorldView` 解耦):建议先完成本 issue,`WorldView` 才有唯一的数据源可读。
Author
Owner

已在提交 7367f2a(fix(state): unify player HP/MP/gold/inventory single source of truth in GameEngine (#520))中完成修复,并在 5de27f8 中补充端到端与 136 关全量验证套件。

修复与验证摘要

  • 修复内容:将玩家 HP/maxHp、MP/maxMana、耐力、属性点、等级经验、金币、背包、腰带、装备与储物箱统一收口到 GameEngine 作为唯一权威状态源,消除 HudManager 与引擎之间的双本账分裂;受伤、喝药、施法耗蓝、NPC 治疗、属性加点与存档读写全部读写同一份状态。
  • 专项回归测试:tests/p0-520-player-state.test.ts
  • 总体验收门禁:npm run build 0 错误、14 个 P0/E2E 测试套件(190/190 用例)100% 通过、全仓 Vitest 6,460/6,460 通过、136/136 关无头浏览器巡检通过。
已在提交 [`7367f2a`](https://git.projectdiablo2.cn/troytt/diablo2-web/commit/7367f2a36a178e41050ba7fbd4b9c564a3e45cc9)(`fix(state): unify player HP/MP/gold/inventory single source of truth in GameEngine (#520)`)中完成修复,并在 [`5de27f8`](https://git.projectdiablo2.cn/troytt/diablo2-web/commit/5de27f8177d43524444de7aa301cc470c4f0844e) 中补充端到端与 136 关全量验证套件。 ### 修复与验证摘要 - **修复内容**:将玩家 HP/maxHp、MP/maxMana、耐力、属性点、等级经验、金币、背包、腰带、装备与储物箱统一收口到 `GameEngine` 作为唯一权威状态源,消除 `HudManager` 与引擎之间的双本账分裂;受伤、喝药、施法耗蓝、NPC 治疗、属性加点与存档读写全部读写同一份状态。 - **专项回归测试**:[`tests/p0-520-player-state.test.ts`](https://git.projectdiablo2.cn/troytt/diablo2-web/src/commit/5de27f8177d43524444de7aa301cc470c4f0844e/tests/p0-520-player-state.test.ts) - **总体验收门禁**:`npm run build` 0 错误、14 个 P0/E2E 测试套件(190/190 用例)100% 通过、全仓 Vitest 6,460/6,460 通过、136/136 关无头浏览器巡检通过。
Sign in to join this conversation.
No Label
No Milestone
No project
No Assignees
1 Participants
Notifications
Due Date
The due date is invalid or out of range. Please use the format 'yyyy-mm-dd'.

No due date set.

Dependencies

No dependencies set.

Reference: troytt/diablo2-web#520
No description provided.