fix(client): 角色动画播放速度未关联 1.13c AnimData.d2 与属性装备 (FCR/IAS/WSM) 且单次动作循环播放不恢复待机 (NU) #552

Closed
opened 2026-10-02 07:48:25 +00:00 by troytt · 1 comment
Owner

问题描述

当前客户端角色动画系统(关联 #544 与 #546)存在两个核心失真问题,违背 Diablo II 1.13c 原版动画引擎(D2Common.dll / D2Client.dll + AnimData.d2 + Weapons.txt)规范:

1. 动画播放速度忽略 AnimData.d2 基准速率与角色属性/装备加成(FCR / IAS / 武器 WSM)

  • WorldRenderer.drawSceneUnitDrawable (src/client/scene/world-renderer.ts) 在计算角色帧索引时,忽略了 char-*.json 中从 1.13c AnimData.d2 导出的每段动作 8.8 定点数基准步进速率(clip.speed,256 = 1 帧 / 40ms tick),而是使用硬编码除数:
    • 移动(wl / rn):Math.floor(ctx.animTimeMs / 65) % frameCount
    • 待机与施法/攻击(nu / tn / sc / a1 / a2):Math.floor(ctx.animTimeMs / 140) % frameCount
  • ClientWorld.applySkillCastToUnit (src/client/world/client-world.ts) 使用硬编码的 400ms (untilMs: now + 400) 作为所有施法与攻击动作的统一持续时间,完全未读取:
    • 角色的快速施法速度 FCR (statId = 105, item_fastercastrate)
    • 角色的攻击速度加成 IAS (statId = 93, item_fasterattackrate) 与技能 IAS (statId = 68)
    • 角色已穿戴装备 (world.containers.equipment) 及镶嵌物 (world.containers.sockets) 上的 FCR / IAS 词缀
    • 角色主手/副手已穿戴武器在 1.13c Weapons.txt 中的武器基础速度修正 WSM (speed 列,例如 Phase Blade 7cr 为 -30、Maul mau 为 +20)

2. 单次动作(SC / A1 / A2 / GH / DT 等 loop: false)无限循环且结束后不恢复待机状态(NU)

  • 状态未过期清理:ClientWorld.applySkillCastToUnit 将 unit.mode 设为 10 (SC) 或 7/8 (A1/A2) 并写入 unit.castState(含 untilMs),但在 ClientWorld.tick() 与 OnlineSession.buildRenderableUnits() 中从未检查 nowMs >= castState.untilMs 来清理 castState 并将 unit.mode 恢复为 1 (NU 待机)。
  • 渲染器强制取模循环:WorldRenderer.drawSceneUnitDrawable 对所有动作无条件使用 % frameCount 取模,且使用全局累加的 ctx.animTimeMs 而非动作起始时间 startedAtMs,导致非循环动作(clip.loop === false,如 sc、a1、a2、gh、dt、dd):
    1. 无法从第 0 帧起始播放;
    2. 到达最后一帧(frameCount - 1)后不会停留在末帧或切回 NU 待机,而是周而复始无限鬼畜循环;
    3. 当本地玩家站立施法结束后,朝向(predictor 的 dir64)也未能保持施法目标方向直到下一次移动。

修复方案(1.13c Ground Truth 对齐)

  1. 严格对齐 1.13c AnimData.d2 8.8 定点数动画步进与边际递减公式:
    • 边际递减:diminished = floor(120 * rate / (120 + rate))(与服务端 src/server/engine/anim-dispatcher.ts 保持一致)。
    • 施法 (SC, mode 10):汇总角色 StatList 与已装备物品/镶嵌物的 FCR (statId = 105),计算 EFCR = clamp(diminished(fcr), -85, 75)。
    • 武器攻击 (A1 / A2, mode 7/8):汇总角色 StatList 与已装备物品/镶嵌物的 IAS (statId = 93)、技能 IAS (statId = 68) 以及 Weapons.txt 中已穿戴武器的 WSM (speed),计算 EIAS = clamp(diminished(itemIas) + skillIas - weaponWsm, -85, 75)。
    • 有效步进速率:stepSpeed256 = max(32, floor(baseSpeed * (100 + effRate) / 100))。
    • 单次动作持续时长:durationMs = ceil(frames * 256 / stepSpeed256) * 40(基于 1.13c 25Hz = 40ms tick)。
  2. 非循环动作单次播放与生命周期自动归位:
    • clip.loop === false 的动作从 startedAtMs 的第 0 帧开始按 stepSpeed256 推进,并在 frameCount - 1 处截断(不再使用 % frameCount 循环)。
    • 当 nowMs >= castState.untilMs 时,自动清理 unit.castState 与 world.self.castState,并将 unit.mode 恢复为 1 (NU 待机),同时保持施法结束时的角色朝向直到下次移动。
    • 当角色开始移动(收到 UnitMove 或本地 predictor 移动)或切换其他状态时,立即打断并清理残留的 castState,切入 WL/RN 移动动画。
## 问题描述 当前客户端角色动画系统(关联 #544 与 #546)存在两个核心失真问题,违背 Diablo II 1.13c 原版动画引擎(`D2Common.dll` / `D2Client.dll` + `AnimData.d2` + `Weapons.txt`)规范: ### 1. 动画播放速度忽略 `AnimData.d2` 基准速率与角色属性/装备加成(FCR / IAS / 武器 WSM) - `WorldRenderer.drawSceneUnitDrawable` (`src/client/scene/world-renderer.ts`) 在计算角色帧索引时,忽略了 `char-*.json` 中从 1.13c `AnimData.d2` 导出的每段动作 8.8 定点数基准步进速率(`clip.speed`,`256` = 1 帧 / 40ms tick),而是使用硬编码除数: - 移动(`wl` / `rn`):`Math.floor(ctx.animTimeMs / 65) % frameCount` - 待机与施法/攻击(`nu` / `tn` / `sc` / `a1` / `a2`):`Math.floor(ctx.animTimeMs / 140) % frameCount` - `ClientWorld.applySkillCastToUnit` (`src/client/world/client-world.ts`) 使用硬编码的 `400ms` (`untilMs: now + 400`) 作为所有施法与攻击动作的统一持续时间,完全未读取: - 角色的快速施法速度 **FCR** (`statId = 105`, `item_fastercastrate`) - 角色的攻击速度加成 **IAS** (`statId = 93`, `item_fasterattackrate`) 与技能 IAS (`statId = 68`) - 角色已穿戴装备 (`world.containers.equipment`) 及镶嵌物 (`world.containers.sockets`) 上的 FCR / IAS 词缀 - 角色主手/副手已穿戴武器在 1.13c `Weapons.txt` 中的武器基础速度修正 **WSM** (`speed` 列,例如 Phase Blade `7cr` 为 `-30`、Maul `mau` 为 `+20`) ### 2. 单次动作(`SC` / `A1` / `A2` / `GH` / `DT` 等 `loop: false`)无限循环且结束后不恢复待机状态(`NU`) - **状态未过期清理**:`ClientWorld.applySkillCastToUnit` 将 `unit.mode` 设为 `10` (`SC`) 或 `7`/`8` (`A1`/`A2`) 并写入 `unit.castState`(含 `untilMs`),但在 `ClientWorld.tick()` 与 `OnlineSession.buildRenderableUnits()` 中从未检查 `nowMs >= castState.untilMs` 来清理 `castState` 并将 `unit.mode` 恢复为 `1` (`NU` 待机)。 - **渲染器强制取模循环**:`WorldRenderer.drawSceneUnitDrawable` 对所有动作无条件使用 `% frameCount` 取模,且使用全局累加的 `ctx.animTimeMs` 而非动作起始时间 `startedAtMs`,导致非循环动作(`clip.loop === false`,如 `sc`、`a1`、`a2`、`gh`、`dt`、`dd`): 1. 无法从第 `0` 帧起始播放; 2. 到达最后一帧(`frameCount - 1`)后不会停留在末帧或切回 `NU` 待机,而是周而复始无限鬼畜循环; 3. 当本地玩家站立施法结束后,朝向(`predictor` 的 `dir64`)也未能保持施法目标方向直到下一次移动。 --- ## 修复方案(1.13c Ground Truth 对齐) 1. **严格对齐 1.13c `AnimData.d2` 8.8 定点数动画步进与边际递减公式**: - 边际递减:`diminished = floor(120 * rate / (120 + rate))`(与服务端 `src/server/engine/anim-dispatcher.ts` 保持一致)。 - 施法 (`SC`, mode 10):汇总角色 `StatList` 与已装备物品/镶嵌物的 FCR (`statId = 105`),计算 `EFCR = clamp(diminished(fcr), -85, 75)`。 - 武器攻击 (`A1` / `A2`, mode 7/8):汇总角色 `StatList` 与已装备物品/镶嵌物的 IAS (`statId = 93`)、技能 IAS (`statId = 68`) 以及 `Weapons.txt` 中已穿戴武器的 WSM (`speed`),计算 `EIAS = clamp(diminished(itemIas) + skillIas - weaponWsm, -85, 75)`。 - 有效步进速率:`stepSpeed256 = max(32, floor(baseSpeed * (100 + effRate) / 100))`。 - 单次动作持续时长:`durationMs = ceil(frames * 256 / stepSpeed256) * 40`(基于 1.13c 25Hz = 40ms tick)。 2. **非循环动作单次播放与生命周期自动归位**: - `clip.loop === false` 的动作从 `startedAtMs` 的第 `0` 帧开始按 `stepSpeed256` 推进,并在 `frameCount - 1` 处截断(不再使用 `% frameCount` 循环)。 - 当 `nowMs >= castState.untilMs` 时,自动清理 `unit.castState` 与 `world.self.castState`,并将 `unit.mode` 恢复为 `1` (`NU` 待机),同时保持施法结束时的角色朝向直到下次移动。 - 当角色开始移动(收到 `UnitMove` 或本地 `predictor` 移动)或切换其他状态时,立即打断并清理残留的 `castState`,切入 `WL`/`RN` 移动动画。
Author
Owner

已在提交 7bd10903f6 修复并合并至 main:严格对齐 1.13c AnimData.d2 步进速度与 FCR/IAS/WSM 边际递减计算,消除单次动作无限循环并在动作周期结束后自动复位至 NU 待机模式。

已在提交 7bd10903f63dc87c1449f89deb272af4dd3898da 修复并合并至 main:严格对齐 1.13c AnimData.d2 步进速度与 FCR/IAS/WSM 边际递减计算,消除单次动作无限循环并在动作周期结束后自动复位至 NU 待机模式。
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#552
No description provided.