fix(skill): 修复寒冰装甲/碎冰甲/冰封装甲/能量护盾等持续性 Buff 视觉效果在施放其他技能时被意外清除的缺陷 #432

Closed
opened 2026-09-24 08:02:15 +00:00 by troytt · 1 comment
Owner

问题背景与缺陷表现 (Background & Defect Analysis)

在当前客户端实战(src/scene/act-scene.ts 与 src/game/engine.ts)中,法师的持续性增益状态技能(Buffs)存在严重的视觉特效生命周期缺陷:

  1. 施放其他技能导致 Buff 光效瞬间消失:玩家施放 寒冰装甲 (Chilling Armor #60)、碎冰甲 (Shiver Armor #50)、冰封装甲 (Frozen Armor #40)、能量护盾 (Energy Shield #58) 或 强化 (Enchant #52) 后,角色身上本已正常显现对应的循环动画(如身侧旋转冰盾、头顶能量护盾金色光芒球等)。然而,一旦玩家随后施放任何其他攻击或位移法术(如火弹、冰弹、冰尖柱、传送、静态力场等),刚刚激活的 Buff 视觉特效会立即被强制清空消失。
  2. 违背暗黑破坏神 2 原版 1.13c 机制规范:在 1.13c 原版设定中,持续性状态特效(State Overlay)与一次性施法光晕(Cast Overlay)属于完全正交的不同层级。装甲与护盾光效必须持续环绕角色,直到该状态持续时间耗尽(Duration Expire)、或者被同类互斥技能取代(如施放碎冰甲顶替寒冰装甲)。角色在 Buff 存续期间进行任何普通攻击、施法、走跑或传送,Buff 光效均绝对不应中断。

暴雪 1.13c 原版权威数据与底层机制基准 (Blizzard 1.13c Ground Truth Invariants)

根据 Diablo II v1.13c 官方数据表(Skills.txt、States.txt、Overlay.txt)及底层生命周期规范:

1. 特效类型分层规范 (Cast Overlay vs State Overlay)

  • 一次性施法光晕 (castoverlay):
    • 由 Skills.txt 的 castoverlay 列指定(如 ice_cast_1, ice_cast_2, fire_cast_1, light_cast_1, teleport 等);
    • 仅作为施法动作当下的视觉反馈,不可循环(loop = false),常规播放 14–16 帧(约 0.5 秒)后自然消亡;
    • 快速连发同一法术时,为防止颜色过度泛白叠加,可由新的施法光晕覆盖前一个未播放完毕的施法光晕。
  • 持续性状态光晕 (States.txt Overlay):
    • 与角色身上的具体增益状态严格绑定(如 frozenarmor, shiverarmor, chillingarmor, energyshield, enchant, thunderstorm);
    • 属于长期宿主光效,持续循环播放(loop = true),其生命周期完全由 Buff 剩余帧数(coldArmorTicks, energyshieldTicks 等)控制。

2. 状态互斥与共存规则 (Exclusion & Coexistence Rules)

  • 冰系三装甲互斥: 冰封装甲(State 40)、碎冰甲(State 50)、寒冰装甲(State 60)属于同系护甲,不可叠加。施放新的冰系装甲会刷新剩余时间,并仅替换上一级冰系装甲的 Overlay。
  • 多维度 Buff 独立共存: 能量护盾(Energy Shield)、强化(Enchant)、雷云风暴(Thunder Storm)与冰系装甲为不同状态维度,三者均可与任意冰系装甲同时激活并同时渲染对应的持续光效。

缺陷根因深度剖析 (Root Cause Analysis)

根因 1:src/game/engine.ts 中 spawnOverlay 误将所有非光环特效视为施法光晕并强制清除 (Line 358–365)

在 src/game/engine.ts 中:

spawnOverlay(
  overlayName: string,
  caster?: { x: number; y: number },
  casterId = 'player',
  options?: CreateActiveOverlayOptions,
): ActiveOverlay {
  const pos = caster ?? { x: this.world.player.x, y: this.world.player.y }
  // Cleanly replace any ongoing non-aura cast overlay for this caster to prevent oversaturated additive stacking
  for (const ov of this.overlays) {
    if (ov.casterId === casterId && !ov.isAura) {
      ov.expired = true // 💥 致命缺陷:把该角色身上所有非光环特效一网打尽全部设为 expired!
    }
  }
  this.overlays = this.overlays.filter(ov => !ov.expired)
  const overlay = createActiveOverlay(overlayName, pos, casterId, undefined, options)
  this.overlays.push(overlay)
  return overlay
}

开发者在实现 Issue #386(防施法光晕叠加过饱和)时,误以为角色身上除了光环(isAura)以外就只有瞬时施法光晕。
由于冰系装甲与能量护盾虽然传入了 { loop: true },但并未带有 isAura 标记,因此被该循环无差别误杀!

根因 2:src/scene/act-scene.ts 每次施法均前置生成瞬发光晕 (Line 1225–1229)

在 src/scene/act-scene.ts 的 castSkill 主调度入口中:

// 5. Spawn cast overlay if defined for this skill
if (skillId !== 0 && (skillId !== 41 || isFirstInfernoCast)) {
  const castOverlayName = getSkillCastOverlay(skillId)
  if (castOverlayName) {
    engine.spawnOverlay(castOverlayName, { x: player.x, y: player.y }, 'player')
  }
}

无论玩家施放火弹(fire_cast_1)、冰尖柱(ice_cast_2)还是传送(teleport),都会无条件调用一次 engine.spawnOverlay。
这直接触发了根因 1 的擦除逻辑,导致已挂载在角色身上的 chillingarmor、shiverarmor、frozenarmor、energyshield 等持续光效在一瞬间被清除出特效数组。

根因 3:Buff 光效的自然消亡代码被架空

在 src/game/engine.ts 第 632–675 行中,虽然已经写好了根据 player.coldArmorTicks <= 0 或 player.energyshieldTicks <= 0 清理 Overlay 的正确代码,但由于特效在施法第一秒就被根因 1 误杀,这些正确的计时到期清理逻辑实际沦为了摆设。


修复方案与任务规划 (Remediation Plan)

  1. 重构 spawnOverlay 中的替换判定逻辑 (src/game/engine.ts):
    • 明确分离 瞬时施法光晕 与 持续状态 Buff 光效:
      • 若新生成的特效为施法光晕(!options?.loop && !options?.isBuff),则仅清理旧的施法光晕(即 !ov.loop && !ov.isAura && !ov.isBuff);
      • 绝对不得清除带有 loop: true 或属于 Buff 状态的持续特效!
  2. 规范 Buff 技能特效之间的互斥与共存 (src/scene/act-scene.ts):
    • 冰系三装甲(Skill 40 / 50 / 60)在施放时,仅针对性清理同系装甲特效(frozenarmor / shiverarmor / chillingarmor),确保新装甲替换旧装甲;
    • 能量护盾(Skill 58)与强化(Skill 52)在施放时,仅清理自身同名旧特效,互不干扰且不影响冰甲。
  3. 保持状态全生命周期与玩家移动同步:
    • 确保 engine.tick 中持续性 Buff 光效全程紧跟玩家坐标移动,直到持续时间归零(coldArmorTicks <= 0 / energyshieldTicks <= 0)后才被清除。
  4. 自动化单元测试与实机回归验证:
    • 编写专门的单元测试用例:
      • Case 1:施放寒冰装甲后,角色连续施放 10 次火弹、冰弹、传送与普通攻击,断言寒冰装甲的 Overlay 实体始终存活(expired === false),且坐标与玩家保持一致;
      • Case 2:施放寒冰装甲后再施放碎冰甲,断言寒冰装甲特效被替换为碎冰甲特效;
      • Case 3:寒冰装甲与能量护盾同时激活,断言两个持续光效共存且在施放其他攻击技能时均不消失;
      • Case 4:当 coldArmorTicks 倒计时归零时,装甲特效正常自然过期清除。
## 问题背景与缺陷表现 (Background & Defect Analysis) 在当前客户端实战(`src/scene/act-scene.ts` 与 `src/game/engine.ts`)中,法师的持续性增益状态技能(Buffs)存在严重的视觉特效生命周期缺陷: 1. **施放其他技能导致 Buff 光效瞬间消失**:玩家施放 **寒冰装甲 (Chilling Armor #60)**、**碎冰甲 (Shiver Armor #50)**、**冰封装甲 (Frozen Armor #40)**、**能量护盾 (Energy Shield #58)** 或 **强化 (Enchant #52)** 后,角色身上本已正常显现对应的循环动画(如身侧旋转冰盾、头顶能量护盾金色光芒球等)。然而,一旦玩家随后施放任何其他攻击或位移法术(如火弹、冰弹、冰尖柱、传送、静态力场等),刚刚激活的 Buff 视觉特效会**立即被强制清空消失**。 2. **违背暗黑破坏神 2 原版 1.13c 机制规范**:在 1.13c 原版设定中,持续性状态特效(State Overlay)与一次性施法光晕(Cast Overlay)属于完全正交的不同层级。装甲与护盾光效必须持续环绕角色,直到该状态持续时间耗尽(Duration Expire)、或者被同类互斥技能取代(如施放碎冰甲顶替寒冰装甲)。角色在 Buff 存续期间进行任何普通攻击、施法、走跑或传送,Buff 光效均绝对不应中断。 --- ## 暴雪 1.13c 原版权威数据与底层机制基准 (Blizzard 1.13c Ground Truth Invariants) 根据 Diablo II v1.13c 官方数据表(`Skills.txt`、`States.txt`、`Overlay.txt`)及底层生命周期规范: ### 1. 特效类型分层规范 (Cast Overlay vs State Overlay) - **一次性施法光晕 (`castoverlay`)**: - 由 `Skills.txt` 的 `castoverlay` 列指定(如 `ice_cast_1`, `ice_cast_2`, `fire_cast_1`, `light_cast_1`, `teleport` 等); - 仅作为施法动作当下的视觉反馈,不可循环(`loop = false`),常规播放 14–16 帧(约 0.5 秒)后自然消亡; - 快速连发同一法术时,为防止颜色过度泛白叠加,可由新的施法光晕覆盖前一个**未播放完毕的施法光晕**。 - **持续性状态光晕 (`States.txt` Overlay)**: - 与角色身上的具体增益状态严格绑定(如 `frozenarmor`, `shiverarmor`, `chillingarmor`, `energyshield`, `enchant`, `thunderstorm`); - 属于长期宿主光效,持续循环播放(`loop = true`),其生命周期完全由 Buff 剩余帧数(`coldArmorTicks`, `energyshieldTicks` 等)控制。 ### 2. 状态互斥与共存规则 (Exclusion & Coexistence Rules) - **冰系三装甲互斥**: 冰封装甲(State 40)、碎冰甲(State 50)、寒冰装甲(State 60)属于同系护甲,不可叠加。施放新的冰系装甲会刷新剩余时间,并**仅替换**上一级冰系装甲的 Overlay。 - **多维度 Buff 独立共存**: 能量护盾(Energy Shield)、强化(Enchant)、雷云风暴(Thunder Storm)与冰系装甲为不同状态维度,三者均可与任意冰系装甲同时激活并同时渲染对应的持续光效。 --- ## 缺陷根因深度剖析 (Root Cause Analysis) ### 根因 1:`src/game/engine.ts` 中 `spawnOverlay` 误将所有非光环特效视为施法光晕并强制清除 (Line 358–365) 在 `src/game/engine.ts` 中: ```typescript spawnOverlay( overlayName: string, caster?: { x: number; y: number }, casterId = 'player', options?: CreateActiveOverlayOptions, ): ActiveOverlay { const pos = caster ?? { x: this.world.player.x, y: this.world.player.y } // Cleanly replace any ongoing non-aura cast overlay for this caster to prevent oversaturated additive stacking for (const ov of this.overlays) { if (ov.casterId === casterId && !ov.isAura) { ov.expired = true // 💥 致命缺陷:把该角色身上所有非光环特效一网打尽全部设为 expired! } } this.overlays = this.overlays.filter(ov => !ov.expired) const overlay = createActiveOverlay(overlayName, pos, casterId, undefined, options) this.overlays.push(overlay) return overlay } ``` 开发者在实现 Issue #386(防施法光晕叠加过饱和)时,误以为角色身上除了光环(`isAura`)以外就只有瞬时施法光晕。 由于冰系装甲与能量护盾虽然传入了 `{ loop: true }`,但并未带有 `isAura` 标记,因此被该循环无差别误杀! ### 根因 2:`src/scene/act-scene.ts` 每次施法均前置生成瞬发光晕 (Line 1225–1229) 在 `src/scene/act-scene.ts` 的 `castSkill` 主调度入口中: ```typescript // 5. Spawn cast overlay if defined for this skill if (skillId !== 0 && (skillId !== 41 || isFirstInfernoCast)) { const castOverlayName = getSkillCastOverlay(skillId) if (castOverlayName) { engine.spawnOverlay(castOverlayName, { x: player.x, y: player.y }, 'player') } } ``` 无论玩家施放火弹(`fire_cast_1`)、冰尖柱(`ice_cast_2`)还是传送(`teleport`),都会无条件调用一次 `engine.spawnOverlay`。 这直接触发了根因 1 的擦除逻辑,导致已挂载在角色身上的 `chillingarmor`、`shiverarmor`、`frozenarmor`、`energyshield` 等持续光效在一瞬间被清除出特效数组。 ### 根因 3:Buff 光效的自然消亡代码被架空 在 `src/game/engine.ts` 第 632–675 行中,虽然已经写好了根据 `player.coldArmorTicks <= 0` 或 `player.energyshieldTicks <= 0` 清理 Overlay 的正确代码,但由于特效在施法第一秒就被根因 1 误杀,这些正确的计时到期清理逻辑实际沦为了摆设。 --- ## 修复方案与任务规划 (Remediation Plan) 1. **重构 `spawnOverlay` 中的替换判定逻辑 (`src/game/engine.ts`)**: - 明确分离 **瞬时施法光晕** 与 **持续状态 Buff 光效**: - 若新生成的特效为施法光晕(`!options?.loop && !options?.isBuff`),则**仅清理**旧的施法光晕(即 `!ov.loop && !ov.isAura && !ov.isBuff`); - **绝对不得**清除带有 `loop: true` 或属于 Buff 状态的持续特效! 2. **规范 Buff 技能特效之间的互斥与共存 (`src/scene/act-scene.ts`)**: - 冰系三装甲(Skill 40 / 50 / 60)在施放时,仅针对性清理同系装甲特效(`frozenarmor` / `shiverarmor` / `chillingarmor`),确保新装甲替换旧装甲; - 能量护盾(Skill 58)与强化(Skill 52)在施放时,仅清理自身同名旧特效,互不干扰且不影响冰甲。 3. **保持状态全生命周期与玩家移动同步**: - 确保 `engine.tick` 中持续性 Buff 光效全程紧跟玩家坐标移动,直到持续时间归零(`coldArmorTicks <= 0` / `energyshieldTicks <= 0`)后才被清除。 4. **自动化单元测试与实机回归验证**: - 编写专门的单元测试用例: - Case 1:施放寒冰装甲后,角色连续施放 10 次火弹、冰弹、传送与普通攻击,断言寒冰装甲的 Overlay 实体始终存活(`expired === false`),且坐标与玩家保持一致; - Case 2:施放寒冰装甲后再施放碎冰甲,断言寒冰装甲特效被替换为碎冰甲特效; - Case 3:寒冰装甲与能量护盾同时激活,断言两个持续光效共存且在施放其他攻击技能时均不消失; - Case 4:当 `coldArmorTicks` 倒计时归零时,装甲特效正常自然过期清除。
Author
Owner

Resolved in commit d9ad6df:

  • Decoupled Cast Overlays (isCastOverlay) from State Overlays (isStateOverlay).
  • Enforced Group 1 mutual exclusion for Cold Armors (frozenarmor, shiverarmor, chillingarmor).
  • Enabled simultaneous coexistence of active buffs (Cold Armor, Energy Shield, Enchant, Thunder Storm) across secondary spell casting.
  • Added comprehensive test suite tests/skills/sor/issue-432-buff-visual-persistence.test.ts (9 tests passing).
  • Passed npm run typecheck (tsc --noEmit) and all related test suites.
Resolved in commit d9ad6df: - Decoupled Cast Overlays (`isCastOverlay`) from State Overlays (`isStateOverlay`). - Enforced Group 1 mutual exclusion for Cold Armors (`frozenarmor`, `shiverarmor`, `chillingarmor`). - Enabled simultaneous coexistence of active buffs (Cold Armor, Energy Shield, Enchant, Thunder Storm) across secondary spell casting. - Added comprehensive test suite `tests/skills/sor/issue-432-buff-visual-persistence.test.ts` (9 tests passing). - Passed `npm run typecheck` (`tsc --noEmit`) and all related test suites.
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#432
No description provided.