[Skill Parity] 取消召唤 (Unsummon) 与 投掷 (Throw) 在不满足使用条件时不应播放人物动作与法术效果 #441

Open
opened 2026-09-24 08:46:16 +00:00 by troytt · 0 comments
Owner

问题描述 (Bug Description)

在当前游戏中,当玩家右键使用 取消召唤 (Unsummon, Skill ID 3) 或 投掷 (Throw, Skill ID 2) 技能时,即使当前环境处于完全不适用的无效状态,角色依然会强制播放动作并产生视觉法术效果:

  1. 取消召唤 (Unsummon):
    • 当场上没有任何己方召唤物/随从(或点击空地/敌人)时,角色依然会抬手播放施法动作 (sc),甚至触发法术层 Overlay;
  2. 投掷 (Throw):
    • 当角色未装备投掷武器(如手持剑盾、法杖、弓弩、空手),或者投掷武器数量耗尽时,角色依然会播放施法动作 (sc),并向鼠标方向发射虚假的投射物。

在原版 Diablo II 1.13c 中,这类前置条件未满足的非法指令属于静默拒绝或仅播放提示音(Refusal),绝对不播放任何人物动作,更不会产生任何法术特效或投射物。


原版 1.13c 地面真理 (Diablo II 1.13c Ground Truth Invariants)

  1. 取消召唤 (Unsummon, Skills.txt ID 3):

    • 依据 Skills.txt: TargetPet: 1, TargetAlly: 0, TargetCorpse: 0, srvdofunc: 4。
    • 该技能仅能对玩家自身召唤的有效生物(骷髅、魔像、影子战士、女武神、野狼、狂狼、灰熊、橡木智者、九头蛇等 unsummonable === true 的实体)生效。
    • 若场上无属于玩家的召唤实体,或点击未命中有效召唤物,该操作属于非法指令:角色保持原样,不播放施法肢体动作,不产生任何光效。
  2. 投掷 (Throw, Skills.txt ID 2):

    • 依据 Skills.txt: skill: Throw, Id: 2。
    • 前置判定:主手必须装备具备投掷属性的武器(标枪 jave、飞刀 tkni、飞斧 taxa)或投掷药剂(tpot),且堆叠数量 quantity > 0。
    • 当角色手持非投掷类武器(剑、杖、弓、斧、钝器等)或数量为 0 时:操作直接被拒绝,角色绝不挥动手臂,不播放任何动作(不播放 th 或 sc),不发射投射物。

缺陷代码定位与根因分析 (Root Cause Analysis)

  1. 动作与特效触发时机颠倒 (Execution Order Inversion):
    在 src/scene/act-scene.ts (第 1235-1253 行) 的 castSkill 函数中:

    // 4b. Trigger character animation
    if (playerAnimator !== undefined && character?.clips !== undefined) {
      if (skillId === 0) {
        const clip = character.clips['a1'] ?? character.clips['a2']
        if (clip !== undefined) playerAnimator.play('a1', clip, { restart: true })
      } else {
        const clip = character.clips['sc'] ?? character.clips['a1']
        if (clip !== undefined) playerAnimator.play('sc', clip, { restart: true })
      }
    }
    
    // 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')
      }
    }
    
    // 6. Dispatch skill
    

    castSkill 在进入第 6 步的具体技能分发与条件校验(如是否有弹药、是否有目标)之前,就在第 4b 步和第 5 步无条件对所有非 0 技能触发了施法动作 (sc) 和施法光效!

  2. 缺失针对 Throw (2) 和 Unsummon (3) 的适用性合法校验:

    • 对 skillId === 2:未校验 weaponInfo.isThrowable 以及 quantity > 0;
    • 对 skillId === 3:未校验场上是否存在属于玩家的 unsummonable 随从;
    • 且在第 6 步没有针对这两项技能的处理分支,直接 fallthrough 到末尾通用的直线投射物生成逻辑(第 2431-2523 行),发射出多余的空白投射物。

修复建议 (Proposed Fix)

  1. 在前置阶段执行快速失败校验 (Fast-fail Preconditions):
    在 castSkill 的动作播放(4b)与特效生成(5)之前,对投掷和取消召唤进行前置合法性检查:
    • 投掷 (Skill 2):
      if (skillId === 2) {
        const weapon = context.equippedWeapon ?? hudManager?.inventory?.equipped?.weapon1
        const weaponInfo = getAttackWeaponInfo(weapon)
        if (!weaponInfo?.isThrowable || (weaponInfo.quantity !== undefined && weaponInfo.quantity <= 0)) {
          status.textContent = !weaponInfo?.isThrowable ? '未装备投掷类武器!' : '投掷武器数量耗尽!'
          return false
        }
      }
      
    • 取消召唤 (Skill 3):
      if (skillId === 3) {
        const hasPets = (engine as any).summonManager?.pets?.some((p: any) => p.unsummonable) ?? false
        if (!hasPets) {
          status.textContent = '无可取消的召唤物!'
          return false
        }
      }
      
  2. 规范动作分发:
    • 投掷成功时应优先播放专有投掷动作 th(若存在)或 a1,而非通用的魔法施法动作 sc。
    • 只有校验完全通过后,才允许驱动角色动画与特效渲染。
### 问题描述 (Bug Description) 在当前游戏中,当玩家右键使用 **取消召唤 (Unsummon, Skill ID 3)** 或 **投掷 (Throw, Skill ID 2)** 技能时,即使当前环境处于**完全不适用的无效状态**,角色依然会强制播放动作并产生视觉法术效果: 1. **取消召唤 (Unsummon)**: - 当场上没有任何己方召唤物/随从(或点击空地/敌人)时,角色依然会抬手播放施法动作 (`sc`),甚至触发法术层 Overlay; 2. **投掷 (Throw)**: - 当角色未装备投掷武器(如手持剑盾、法杖、弓弩、空手),或者投掷武器数量耗尽时,角色依然会播放施法动作 (`sc`),并向鼠标方向发射虚假的投射物。 在原版 Diablo II 1.13c 中,这类前置条件未满足的非法指令属于静默拒绝或仅播放提示音(Refusal),**绝对不播放任何人物动作,更不会产生任何法术特效或投射物**。 --- ### 原版 1.13c 地面真理 (Diablo II 1.13c Ground Truth Invariants) 1. **取消召唤 (Unsummon, Skills.txt ID 3)**: - 依据 `Skills.txt`: `TargetPet: 1`, `TargetAlly: 0`, `TargetCorpse: 0`, `srvdofunc: 4`。 - 该技能仅能对玩家自身召唤的有效生物(骷髅、魔像、影子战士、女武神、野狼、狂狼、灰熊、橡木智者、九头蛇等 `unsummonable === true` 的实体)生效。 - 若场上**无属于玩家的召唤实体**,或点击未命中有效召唤物,该操作属于非法指令:角色保持原样,不播放施法肢体动作,不产生任何光效。 2. **投掷 (Throw, Skills.txt ID 2)**: - 依据 `Skills.txt`: `skill: Throw`, `Id: 2`。 - 前置判定:主手必须装备具备投掷属性的武器(标枪 `jave`、飞刀 `tkni`、飞斧 `taxa`)或投掷药剂(`tpot`),且堆叠数量 `quantity > 0`。 - 当角色手持非投掷类武器(剑、杖、弓、斧、钝器等)或数量为 0 时:操作直接被拒绝,角色绝不挥动手臂,不播放任何动作(不播放 `th` 或 `sc`),不发射投射物。 --- ### 缺陷代码定位与根因分析 (Root Cause Analysis) 1. **动作与特效触发时机颠倒 (Execution Order Inversion)**: 在 `src/scene/act-scene.ts` (第 1235-1253 行) 的 `castSkill` 函数中: ```typescript // 4b. Trigger character animation if (playerAnimator !== undefined && character?.clips !== undefined) { if (skillId === 0) { const clip = character.clips['a1'] ?? character.clips['a2'] if (clip !== undefined) playerAnimator.play('a1', clip, { restart: true }) } else { const clip = character.clips['sc'] ?? character.clips['a1'] if (clip !== undefined) playerAnimator.play('sc', clip, { restart: true }) } } // 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') } } // 6. Dispatch skill ``` `castSkill` 在进入第 6 步的具体技能分发与条件校验(如是否有弹药、是否有目标)之前,就在第 4b 步和第 5 步**无条件对所有非 0 技能触发了施法动作 (`sc`) 和施法光效**! 2. **缺失针对 Throw (2) 和 Unsummon (3) 的适用性合法校验**: - 对 `skillId === 2`:未校验 `weaponInfo.isThrowable` 以及 `quantity > 0`; - 对 `skillId === 3`:未校验场上是否存在属于玩家的 `unsummonable` 随从; - 且在第 6 步没有针对这两项技能的处理分支,直接 fallthrough 到末尾通用的直线投射物生成逻辑(第 2431-2523 行),发射出多余的空白投射物。 --- ### 修复建议 (Proposed Fix) 1. **在前置阶段执行快速失败校验 (Fast-fail Preconditions)**: 在 `castSkill` 的动作播放(4b)与特效生成(5)之前,对投掷和取消召唤进行前置合法性检查: - **投掷 (Skill 2)**: ```typescript if (skillId === 2) { const weapon = context.equippedWeapon ?? hudManager?.inventory?.equipped?.weapon1 const weaponInfo = getAttackWeaponInfo(weapon) if (!weaponInfo?.isThrowable || (weaponInfo.quantity !== undefined && weaponInfo.quantity <= 0)) { status.textContent = !weaponInfo?.isThrowable ? '未装备投掷类武器!' : '投掷武器数量耗尽!' return false } } ``` - **取消召唤 (Skill 3)**: ```typescript if (skillId === 3) { const hasPets = (engine as any).summonManager?.pets?.some((p: any) => p.unsummonable) ?? false if (!hasPets) { status.textContent = '无可取消的召唤物!' return false } } ``` 2. **规范动作分发**: - 投掷成功时应优先播放专有投掷动作 `th`(若存在)或 `a1`,而非通用的魔法施法动作 `sc`。 - 只有校验完全通过后,才允许驱动角色动画与特效渲染。
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#441
No description provided.