diablo2-web/docs/frozen-orb-skill-developmen...

28 KiB
Raw Permalink Blame History

暗黑破坏神 II (v1.13c) 复杂投射物技能实装开发指导 —— 以冰封球 (Frozen Orb) 为例

文档状态:已落盘 / 核心规范
适用版本:Diablo II: Lord of Destruction v1.13c Decompiled Ground Truth
面向对象:Web 端图形/游戏逻辑工程师、逆向研究员与自动化测试开发者
权威参考:D2Game.dll (v1.13c), D2Common.dll, Skills.txt, Missiles.txt, Overlay.txt


核心设计哲学与项目铁律 (Design Philosophy & Project Invariants)

在《暗黑破坏神 II》Web 端复现工程中,所有复杂法术与物理实装必须严格遵守以下四大不可动摇的项目铁律(Project Invariants):

  1. 1.13c 逆向反汇编真值原则 (Ground Truth Invariant):
    • 严格遵循原版反汇编二进制代码(D2Game.dll、D2Common.dll)以及官方 MPQ 数据表(Skills.txt、Missiles.txt、Overlay.txt 等)。
    • 禁止主观凭空臆造规则或视觉表现。任何飞行速度、碰撞判定、子弹发射时序、穿透机制与伤害计算,必须直接溯源至 1.13c 汇编地址与数据表定义。
  2. 零运行时 MPQ 依赖与离线预烘焙 (Zero Runtime MPQ Invariant):
    • 浏览器 Web 客户端禁止在运行时直接解包庞大的 MPQ 归档或实时解析 DCC/DC6 字节流。
    • 所有法术、特效、投射物动画精灵必须通过离线打包脚本(scripts/pack-*.ts)烘焙为紧凑的 PNG 纹理图集与 JSON 元数据,并自动生成强类型常量定义。
  3. 拒绝静默失败与假退化 (Anti-Silent Failures):
    • 严禁使用诸如 if (!art) return true 或假数据 fallback 掩盖缺失资源。
    • 缺失材质、越界朝向、未知投射物类型必须在开发与测试环境中快速失败 (Fail-Fast),确保错误能被单元测试与静态类型检查立即捕获。
  4. 零黑边 Alpha 阈值与混合模式规范 (Zero Black Borders & Additive Blending):
    • 原版 DCC 调色板索引 0 为全透明。在烘焙为 32-bit RGBA PNG 时,透明像素必须明确置零(A = 0),并剔除微弱灰阶毛边。
    • 具有发光属性的法术投射物(Trans: 1 或 Trans: 3)在 WebGL / Canvas 渲染阶段必须使用加色混合(Additive Blending / ctx.globalCompositeOperation = 'lighter'),呈现剔透纯净的魔法质感。

第一章:核心机制与原版底层溯源 (Ground Truth Analysis)

冰封球(Frozen Orb,技能 ID 64)是暗黑破坏神 II 中数学结构最严谨、层次最丰富的多级级联投射物(Multi-stage Cascading Missile)系统。

1.1 Skills.txt 数据定义 (Skill ID: 64)

字段 (Column) 1.13c 取值 说明
skill Frozen Orb 技能标识
Id 64 索格林法师冰霜系技能全局 ID
charclass sor 巫师 (Sorceress)
skillreq Blizzard (59) 前置技能要求:暴风雪
reqlevel 30 学习等级门槛:30 级终极法术
srvmissile frozenorb 服务端生成的主投射物标识(指向 Missiles.txt)
delay 25 施法公共冷却时间 (Cast Delay):25 逻辑刻 = 1.0 秒
manashift 7 法力精度移位系数(2^7 = 128),原版法力公式缩放常量
mana 25 基础法力消耗
lvlmana 1 每级增加法力消耗
castoverlay ice_cast_3 施法时脚下触发的 3 级高阶冰霜包裹 Overlay

1.2 Missiles.txt 级联投射物族群体系

在原版引擎中,一个“冰封球”技能实际上由 4 种不同的投射物在不同生命周期分工协作构成:

flowchart TD
    Player["玩家施法 (Skill 64)"] --> CastOverlay["施法特效: ice_cast_3 (Overlay)"]
    Player --> FO["主投射物: frozenorb (ID 260)\n- 匀速飞行 30 刻\n- 穿透怪物 (CollideKill=0)\n- 无直接接触伤害"]
    
    FO -- "逐帧发射 (pSrvDoFunc=15, 步长 19)" --> Bolt["子弹: frozenorbbolt (ID 261)\n- 64 点离散圆周发射\n- 匀速向外飞行 25 刻\n- 接触伤害 + 减速"]
    
    FO -- "寿命终点 (Tick 30) 或 撞墙" --> Hit29["触发 pSrvHitFunc=29"]
    Hit29 --> Nova["爆裂新星: frozenorbnova (ID 262)\n- 16 弹均布放射 (步长 4)\n- 末端 6 帧仿射卷曲阻尼 (pSrvDoFunc=16)"]
    Hit29 --> Exp["爆炸视觉: frozenorbexplode (ID 263)\n- 原地播放 18 帧冰爆 (无伤害)"]

投射物数据表详细对照

投射物标识 ID pSrvDoFunc pSrvHitFunc 速度 (Vel) 射程 (Range) 碰撞杀伤 (CollideKill) 碰撞类型 (CollideType) 穿透 (Pierce) 子弹绑定 动画资源 (CelFile) 帧数
frozenorb 260 15 29 10 (250 px/s) 30 刻 (1.2s) 0 (穿透怪物) 1 (怪物阻拦) 1 (穿透) SubMissile1=frozenorbbolt
HitSubMissile1=frozenorbnova
IceOrb 16
frozenorbbolt 261 1 0 18 (450 px/s) 25 刻 (1.0s) 1 (命中即毁) 3 (怪物+障碍) 0 无 (explosionMissile=iceexplode) IceBolt 6
frozenorbnova 262 16 0 24 (600 px/s) 25 刻 (1.0s) 1 (命中即毁) 3 (怪物+障碍) 0 无 (explosionMissile=iceexplode) IceBolt 6
frozenorbexplode 263 0 0 0 (原地) 16/18 刻 0 (纯视觉) 0 (无碰撞) 0 无 IceOrbExplode 16/18

1.3 D2Game.dll 反汇编关键函数与代码指针

在《暗黑破坏神 II》v1.13c 的服务端引擎 D2Game.dll 中,冰封球的核心机制由三个专属代码指针驱动:

1. MISSMODE_SrvDo15_FrozenOrb (代码偏移 0x6FC581F0)

  • 执行时机:主冰球存活期间的**每一帧(Every Tick)**调用。
  • 功能描述:
    • 读取 64 点离散圆周位置表。
    • 维护一个内部角度偏移指针 nSubMissileIdx。
    • 按照数据表配置的频率(param1 = 1 帧)向外发射 1 枚 frozenorbbolt。
    • 发射后,角度指针累加步长(param2 = 19),并对 64 取模:
      \text{idx}_{\text{next}} = (\text{idx}_{\text{current}} + 19) \pmod{64}

2. MISSMODE_SrvHit29_FrozenOrb (代码偏移 0x6FC5CE10)

  • 执行时机:主冰球达到寿命最大值(ageTicks >= 30)或撞击不可逾越实心墙体/障碍时触发。
  • 功能描述:
    • 校验命中首帧标记 currentHitFrame == 0,防止重复触发。
    • 产生 16 枚均布向外扩散的终极爆发子弹 frozenorbnova。其遍历角度步长为 sHitPar1 = 4:
      \text{indices} = \{0, 4, 8, 12, \dots, 60\} \quad (64 / 4 = 16 \text{ 枚})
    • 在爆裂中心原地召唤爆炸动画投射物 frozenorbexplode。
    • 将主冰球标记为销毁(expired = true)。

3. MISSMODE_SrvDo16_FrozenOrbNova (代码偏移 0x6FC58340)

  • 执行时机:终极爆发子弹 frozenorbnova 存活期间每帧调用。
  • 功能描述:
    • 监测子弹剩余寿命 remainingTicks = maxRange - ageTicks。
    • 当 remainingTicks < param1(原版常数 6 帧),且处于衰减步长(param2 = 2 帧)时,执行仿射阻尼旋转变换:
      \begin{cases}
      v_x' = \lfloor(v_x - v_y) / 2\rfloor \\
      v_y' = \lfloor(v_x + v_y) / 2\rfloor
      \end{cases}$$
      
    • 这段精妙的汇编算法在子弹消散前赋予了冰刺独特的顺时针旋转微卷曲线条(Spiral Curl Damping),这是暗黑 2 标志性的冷冽视觉印记。

1.4 核心物理与数学模型精要

(1) 64 点离散圆周查表与步长 19 互质旋转设计

原版 D2 为了避免在 1999 年的 CPU 上频繁调用高开销的浮点 sin/cos 指令,在 D2Game.dll 的静态数据段硬编码了一张 64 项的半径为 30 的离散圆周坐标表:

export const FROZEN_ORB_X_POSITIONS: readonly number[] = Object.freeze([
  30, 29, 29, 28, 27, 26, 24, 23, 21, 19, 16, 14, 11, 8, 5, 2,
  0, -2, -5, -8, -11, -14, -16, -19, -21, -23, -24, -26, -27, -28, -29, -29,
  -30, -29, -29, -28, -27, -26, -24, -23, -21, -19, -16, -14, -11, -8, -5, -2,
  0, 2, 5, 8, 11, 14, 16, 19, 21, 23, 24, 26, 27, 28, 29, 29,
])

export const FROZEN_ORB_Y_POSITIONS: readonly number[] = Object.freeze([
  0, 2, 5, 8, 11, 14, 16, 19, 21, 23, 24, 26, 27, 28, 29, 29,
  30, 29, 29, 28, 27, 26, 24, 23, 21, 19, 16, 14, 11, 8, 5, 2,
  0, -2, -5, -8, -11, -14, -16, -19, -21, -23, -24, -26, -27, -28, -29, -29,
  -30, -29, -29, -28, -27, -26, -24, -23, -21, -19, -16, -14, -11, -8, -5, -2,
])
  • 数学互质原理:因为 19 与 64 互质(\gcd(19, 64) = 1),在模 64 加法群上,步长 19 是一个本原生成元。主冰球在飞行的 30 帧内,每一帧向四周不同方向均匀发射冰刺,生成令人叹为观止的阿基米德风车旋转弹幕,绝不会在某一角度产生重叠或死角。

(2) 2:1 二等角投影大地缩放 (ISO_GROUND_ASPECT_RATIO = 0.5)

暗黑破坏神 II 的地表视角为 26.565^\circ 的二等角斜投影(2:1 Isometric Projection)。

  • 在地面物理世界中,水平方向 1.0\text{m} 投影在屏幕上为 X 轴全长,而垂直方向 1.0\text{m} 投影在屏幕 Y 轴上只有 一半高度 (0.5)。
  • 若直接采用正圆公式(X^2 + Y^2 = R^2),弹幕在暗黑的菱形地表上将被拉长为纵向椭圆。
  • 真值规范:所有地面放射弹幕的垂直方向位移必须乘以 ISO_GROUND_ASPECT_RATIO = 0.5:
    \Delta x = X[i], \quad \Delta y = Y[i] \times 0.5

(3) 无害发射体 (isHarmlessEmitter) 与穿透特性

  • 在 1.13c 真实设定中,冰球母体(frozenorb)本身完全不产生直接碰撞伤害。它的 CollideKill = 0,会无阻碍地穿透怪物躯体(仅有实心墙体会阻挡它)。
  • 所有伤害全部由其派生出的子弹(frozenorbbolt 与 frozenorbnova)承担。若将主冰球做成撞怪即毁或带碰撞伤害,将直接颠覆该技能的原版操作手法。

(4) 零 NextDelay 与“甜点位”重叠受击杀伤机制

  • 暗黑 2 中的许多多弹道法术(如多重箭 Multiple Shot、充能弹 Charged Bolt)具有 NextDelay 机制(即目标受到同一技能伤害后,在接下来的若干帧内进入全局法术免疫期),防止怪物同时吃下多发弹幕。
  • 然而在 Missiles.txt 中,frozenorbbolt 与 frozenorbnova 的 NextDelay = 0 (无免疫期)!
  • 这构成了冰封球闻名遐迩的**“甜点位(Sweet Spot)”**机制:如果玩家精确把控施法距离,让冰球在恰好飞抵 Boss 身体中心的瞬间达到 30 刻寿命并引爆,16 枚爆裂冰刺将在同一瞬间于怪物体内生成并全数命中,造成高达 16 倍的毁灭性瞬间爆发伤害!

第二章:离线资产解包与烘焙流水线 (Offline Asset Pipeline)

Web 端必须保持轻量、高效,所有资产必须在构建期离线完成提取和烘焙。

2.1 DCC 资源溯源与依赖解析

通过扫描 d2data.mpq 与 Patch_D2.mpq,冰封球所涉及的核心动画文件如下:

逻辑投射物 CelFile 属性 内部 MPQ 路径 资源特征
frozenorb IceOrb data/global/missiles/IceOrb.dcc 旋转冰球,单向 16 帧循环动画
frozenorbexplode IceOrbExplode data/global/missiles/IceOrbExplode.dcc 爆裂冰雾,单向 18 帧消散动画
frozenorbbolt IceBolt data/global/missiles/IceBolt.dcc 飞行冰刺,16 方向 \times 6 帧动画
frozenorbnova IceBolt data/global/missiles/IceBolt.dcc 完全复用 IceBolt.dcc

2.2 离线打包脚本编写与执行 (scripts/pack-missiles.ts)

在 scripts/pack-missiles.ts 中注册 frozenorb 与 frozenorbexplode 任务,执行高保真纹理烘焙:

// scripts/pack-missiles.ts 核心注册项
{
  name: 'frozenorb',
  dccRelPath: 'data/global/missiles/IceOrb.dcc',
  celFile: 'IceOrb',
  trans: 1, // 增色混合透明度
},
{
  name: 'frozenorbexplode',
  dccRelPath: 'data/global/missiles/IceOrbExplode.dcc',
  celFile: 'IceOrbExplode',
  trans: 1,
}

执行打包命令:

npx tsx scripts/pack-missiles.ts

打包流水线产出物:

  • public/missiles/frozenorb.png / public/missiles/frozenorb.json (512x107, 16 帧)
  • public/missiles/frozenorbexplode.png / public/missiles/frozenorbexplode.json (1024x959, 18 帧)
  • 自动更新强类型代码文件:src/render/missiles-meta.ts

2.3 资产复用策略(别名映射避免网络冗余)

在 Missiles.txt 中,frozenorbbolt 与 frozenorbnova 的 CelFile 均为 IceBolt。 为遵循 Web 性能优化与 Zero Duplicate Invariant 原则:

  • 禁止生成重复的 frozenorbbolt.png 或 frozenorbnova.png。
  • 在客户端资源字典(loadMissileArtMap)与渲染循环中直接建立别名映射:
    // 别名绑定:指向已有的 icebolt 动画图集
    map.set('frozenorbbolt', iceboltArt)
    map.set('frozenorbnova', iceboltArt)
    

2.4 Trans: 1 规则:32-bit RGBA、Alpha 剔除与 WebGL Additive 混合

在 DCC 解码转写 PNG 过程中,暗黑 2 原版的冷光和透明边缘极易在现代抗锯齿渲染器下形成一圈“黑色脏边(Black Halos)”:

  • 离线阈值处理:在写入 PNG RGBA 缓冲时,对透明索引或亮度接近 0 的像素,强制设置 A = 0。
  • 渲染加色合成:在 2D Canvas 或 WebGL 中,凡是 trans === 1 或 trans === 3 的投射物与覆盖层,必须启用 Additive Blending:
    ctx.save()
    if (missileMeta.trans === 1 || missileMeta.trans === 3) {
      ctx.globalCompositeOperation = 'lighter' // WebGL: gl.blendFunc(gl.ONE, gl.ONE)
    }
    ctx.drawImage(atlasImage, sx, sy, sw, sh, dx, dy, dw, dh)
    ctx.restore()
    

第三章:复杂投射物物理引擎实装架构 (Simulation Architecture)

在 Web 架构中,物理模拟与视图渲染严格解耦。模拟核心主要分布在 src/game/skills.ts 与 src/game/engine/missile-engine.ts。

3.1 投射物状态机扩展

在 Projectile 接口中扩展支持风车旋转与末端卷曲的特异性字段:

export interface Projectile {
  readonly skillId?: string | undefined
  readonly missileType?: string | undefined
  x: number
  y: number
  vx: number
  vy: number
  damage: number
  ttl: number
  fromPlayer: boolean
  statusEffect?: 'chill' | 'freeze' | 'poison' | undefined
  statusDuration?: number | undefined
  pierce?: boolean | undefined
  
  // === 复杂投射物专用状态 ===
  subMissileIdx?: number | undefined  // 冰球 64 点离散圆周指针
  isNovaCurl?: boolean | undefined     // 是否触发末端仿射阻尼卷曲
  hitTargets?: Set<number> | undefined // 穿透去重集合
}

3.2 级联派生与事件输出 (ProjectileOutcome.spawnedProjectiles)

传统的投射物更新循环通常只返回存活与命中。为了支撑级联发射,ProjectileOutcome 必须增加派生投射物容器:

export interface ProjectileOutcome {
  readonly alive: readonly Projectile[]
  readonly hits: readonly ProjectileHit[]
  readonly expired: number
  readonly wallHits: number
  readonly wallHitEvents: readonly WallHitEvent[]
  readonly spawnedProjectiles: readonly Projectile[] // 级联生成的子弹
}

3.3 逐帧主弹发射器循环 (pSrvDoFunc = 15)

在 tickProjectiles 循环中,检测 projectile.missileType === 'frozenorb':

if (projectile.missileType === 'frozenorb') {
  const idx = Math.abs((subMissileIdx ?? 0) % 64)
  const offX = FROZEN_ORB_X_POSITIONS[idx]!
  const offY = FROZEN_ORB_Y_POSITIONS[idx]! * ISO_GROUND_ASPECT_RATIO
  const angle = Math.atan2(offY, offX)
  const speed = 18 // 1.13c Missiles.txt: vel = 18 (450 px/s)

  spawned.push({
    skillId: projectile.skillId,
    x,
    y,
    vx: Math.cos(angle) * speed,
    vy: Math.sin(angle) * speed,
    damage: projectile.damage > 0 ? projectile.damage : 40,
    ttl: 25,
    fromPlayer: projectile.fromPlayer,
    missileType: 'frozenorbbolt',
    statusEffect: 'chill',
    statusDuration: 100,
    pierce: false,
  })

  // 步长 19 互质旋转前进
  subMissileIdx = (idx + 19) % 64
}

3.4 碰撞判定:怪物穿透与实心墙体拦截

// 1. 障碍与实心墙体判定(墙体阻断优于怪物判定)
const blockedByTerrain = options.isMissileBlocked !== undefined
  ? options.isMissileBlocked(x, y, projectile.x, projectile.y)
  : options.overlap(x, y) > 0

if (blockedByTerrain) {
  wallHits += 1
  if (projectile.missileType === 'frozenorb') {
    // 撞墙立即引爆 16 弹新星并销毁!
    triggerFrozenOrbDetonation(x, y, projectile, spawned)
  }
  continue // 销毁当前弹体
}

// 2. 怪物接触判定
const isHarmlessEmitter = projectile.missileType === 'frozenorb'
if (!isHarmlessEmitter) {
  // 仅有子弹(bolt / nova)在此判定命中与造成伤害
  for (const target of targets) {
    if (!target.alive) continue
    if (hitTargets.has(target.index)) continue
    if (Math.hypot(target.x - x, target.y - y) <= target.radius + radius) {
      hits.push({ targetIndex: target.index, damage: projectile.damage, ... })
      hitTargets.add(target.index)
      if (!projectile.pierce) {
        struck = true
        break
      }
    }
  }
}

3.5 终点/撞墙派生:16 弹新星与爆炸动画生成 (pSrvHitFunc = 29)

无论是自然寿终(ttl <= 0)还是撞击障碍,必须调用一致的爆裂逻辑:

function triggerFrozenOrbDetonation(
  x: number,
  y: number,
  projectile: Projectile,
  spawned: Projectile[]
) {
  // 64 角度中每隔 4 个点取 1 个 -> 恰好 16 枚均布冰刺
  for (let i = 0; i < 64; i += 4) {
    const offX = FROZEN_ORB_X_POSITIONS[i]!
    const offY = FROZEN_ORB_Y_POSITIONS[i]! * ISO_GROUND_ASPECT_RATIO
    const angle = Math.atan2(offY, offX)
    const speed = 24 // 1.13c Missiles.txt: vel = 24 (600 px/s)

    spawned.push({
      skillId: projectile.skillId,
      x,
      y,
      vx: Math.cos(angle) * speed,
      vy: Math.sin(angle) * speed,
      damage: projectile.damage > 0 ? projectile.damage : 40,
      ttl: 25,
      fromPlayer: projectile.fromPlayer,
      missileType: 'frozenorbnova',
      statusEffect: 'chill',
      statusDuration: 100,
      isNovaCurl: true, // 开启末端阻尼卷曲
      pierce: false,
    })
  }
}

3.6 子弹新星的仿射卷曲阻尼模拟 (pSrvDoFunc = 16)

if (
  (projectile.isNovaCurl || projectile.missileType === 'frozenorbnova') &&
  projectile.ttl <= 6 &&
  projectile.ttl % 2 === 0
) {
  const diff = vx - vy
  const sum = vx + vy
  vx = diff / 2
  vy = sum / 2
  x = projectile.x + vx
  y = projectile.y + vy
}

第四章:运行时客户端与渲染集成 (Web Engine Integration)

4.1 施法生命周期与技能参数注册 (src/scene/act-scene.ts)

在 castSpell 中注册技能 ID 64,绑定 1.13c 官方基础参数:

else if (skillId === 64) {
  const missile = getMissileTxtData('frozenorb')
  speed = missile.speedPxPerSec || 250 // 10 * 25
  range = missile.distancePx || 300    // 10 * 30
  damage = 40
  delay = 25                          // 1.0 秒施法公共冷却
  pierce = true                       // 穿透怪物
  statusEffect = 'chill'              // 冰冻减速
  statusDuration = 100                // 4 秒减速时长
  missileType = 'frozenorb'
  skillName = '冰封球'
}

4.2 施法动作与覆盖特效 (castoverlay: ice_cast_3)

在玩家释放冰封球时,除了在世界生成 frozenorb 投射物外,还需同步唤起单位脚底的高阶冰霜覆盖层 ice_cast_3:

if (skillRecord.castOverlay) {
  engine.spawnOverlay({
    overlayName: skillRecord.castOverlay, // 'ice_cast_3'
    casterId: player.id,
    x: player.x,
    y: player.y,
  })
}

4.3 渲染循环与投射物朝向计算

在渲染循环(drawProjectiles)中:

  1. 多向投射物朝向映射:
    • IceBolt 具有 16 个方向。根据速度矢量计算极角:
      \theta = \text{atan2}(v_y, v_x)
    • 将弧度换算为 0..15 的方向索引 dir = Math.round((theta + Math.PI) / (2 * Math.PI / 16)) % 16。
  2. 单向投射物自转动画:
    • IceOrb 为 1 方向、16 帧自转动画。其当前渲染帧由存活时间决定:
      \text{frame} = (\text{ageTicks} \times \text{animRate} / 1024) \pmod{16}

第五章:规范化验证标准与单测编写指南 (Verification Protocol)

任何技能实装必须建立起全覆盖的自动化测试矩阵。冰封球的验证套件严格执行 7 维全覆盖原则:

graph LR
    D1["1. 静态数据真值校验\nMissiles.txt 解析"] --> V["自动化验证流 (Vitest)"]
    D2["2. 烘焙资产完整性\nPNG/JSON/Meta 存在"] --> V
    D3["3. 离散圆周步进验证\n互质风车发射"] --> V
    D4["4. 无害发射体与穿透\n母体穿透零直接伤"] --> V
    D5["5. 爆裂与撞墙拦截\n16 弹均布爆炸"] --> V
    D6["6. 终极新星仿射卷曲\n末端阻尼旋转"] --> V
    D7["7. 零 NextDelay 爆发\n甜点位重叠杀伤"] --> V

5.1 7 维全覆盖验证原则及示例代码

维度 1:静态表解析与真值常量

校验 samples/fixtures/data/global/excel/Missiles.txt 中 4 种投射物的 ID、速度、射程、动画帧长与 CelFile 字段完全匹配。

维度 2:离线资产打包与元数据完整性

校验 public/missiles/frozenorb.png、frozenorbexplode.png 存在,且 src/render/missiles-meta.ts 中导出的 FROZENORB_META.frames === 16,FROZENORBEXPLODE_META.frames === 18。

维度 3:逐帧发射器步进与 2:1 椭圆修正

校验在 30 刻飞行周期中,恰好生成 30 枚子弹,且发射方向严格按照步长 19 前进,垂直分量严格满足 Y \times 0.5:

it('discrete circle emitter emits 30 bolts with step 19 over 30 ticks', () => {
  let orb: Projectile = {
    x: 0, y: 0, vx: 10, vy: 0, ttl: 30,
    missileType: 'frozenorb', subMissileIdx: 0, damage: 40, fromPlayer: true,
  }
  const spawnedList: Projectile[] = []
  for (let tick = 0; tick < 30; tick++) {
    const outcome = tickProjectiles([orb], [], { overlap: () => 0 })
    expect(outcome.spawnedProjectiles.length).toBe(1)
    spawnedList.push(outcome.spawnedProjectiles[0]!)
    orb = outcome.alive.find(p => p.missileType === 'frozenorb')!
  }
  expect(spawnedList.length).toBe(30)
})

维度 4:无害发射体与穿透特性

将怪物置于冰球飞行正前方,断言冰球穿透怪物且怪物生命值未减:

it('main orb pierces monsters without dying and deals no direct damage', () => {
  const orb: Projectile = {
    x: 0, y: 0, vx: 10, vy: 0, ttl: 20,
    missileType: 'frozenorb', damage: 100, fromPlayer: true, pierce: true,
  }
  const monster = { index: 1, x: 50, y: 0, radius: 10, alive: true }
  // 5 帧移动 50 像素,正好穿过怪物
  let current = [orb]
  for (let i = 0; i < 5; i++) {
    const res = tickProjectiles(current, [monster], { overlap: () => 0 })
    expect(res.hits.length).toBe(0) // 主球绝对不造成直接命中伤害
    current = res.alive.filter(p => p.missileType === 'frozenorb')
  }
  expect(current.length).toBe(1) // 主球未被销毁,继续存活
})

维度 5:撞墙拦截与即时爆裂派生

当冰球遇到实心障碍时,主冰球立即被销毁,并在障碍点瞬间爆裂出 16 枚 frozenorbnova 子弹。

维度 6:新星末端仿射卷曲

单测断言在 ttl <= 6 且 ttl % 2 === 0 时,速度矢量严格发生 (v_x - v_y)/2 与 (v_x + v_y)/2 变换。

维度 7:零 NextDelay 甜点位爆发受击

构造处于爆炸点的敌对单位,断言在同一刻该单位同时吃下多个爆裂子弹伤害,无任何免疫阻断。


第六章:新技能实装标准 Checklist (Standard Operating Procedure)

为规范后续复杂法术(如九头海蛇 Hydra、祝福之槌 Blessed Hammer、陨石 Meteor、剧毒新星 Poison Nova 等)的开发,制定如下 8 步标准作业流程(SOP):

8 步标准作业流程 (SOP Checklist)

  • Step 1: 查表与反汇编溯源 (Ground Truth Auditing)
    • 查阅 Skills.txt,确认 Id、srvmissile、delay、manashift、castoverlay。
    • 查阅 Missiles.txt,分析主弹与所有级联子弹(SubMissile、HitSubMissile、ExplosionMissile)的 pSrvDoFunc 与 pSrvHitFunc。
    • 必要时反汇编 D2Game.dll 确认核心数学公式与特殊逻辑。
  • Step 2: 资产解包与格式校验 (Asset Extraction)
    • 确认所有关联 DCC/DC6 文件在 MPQ 归档中的确切路径。
    • 检查调色板(ACT1~ACT5 pal.pl2)与方向数(1 / 8 / 16 / 32 方向)。
  • Step 3: 运行打包脚本 (Offline Baking)
    • 在 scripts/pack-missiles.ts(或 pack-overlays.ts)中注册项。
    • 运行 npx tsx scripts/pack-*.ts,产出透明度修正的 32-bit RGBA PNG 与 JSON 图集。
    • 检查自动生成的 src/render/*-meta.ts 类型常量。
  • Step 4: 物理引擎核心逻辑扩展 (Physics Simulation)
    • 在 src/game/skills.ts 中注册 CANONICAL_113C_MISSILES 项。
    • 在 src/game/engine/missile-engine.ts 实现对应的 pSrvDoFunc 与 pSrvHitFunc 物理计算。
    • 涉及地面投影时,必须乘入 ISO_GROUND_ASPECT_RATIO = 0.5 椭圆修正。
  • Step 5: 客户端生命周期集成 (Client Integration)
    • 在 src/scene/act-scene.ts 中实现技能施法触发逻辑、施法延时(delay)与法力扣除。
    • 在 loadMissileArtMap 中建立纹理加载与别名复用映射。
    • 确认渲染循环中的混合模式(发光特效配置 ctx.globalCompositeOperation = 'lighter')。
  • Step 6: 编写 7 维全覆盖测试 (Vitest Unit Suite)
    • 编写独立的测试文件 tests/skills/<class>/skill-<id>-<name>.test.ts。
    • 确保数据解析、资产存在性、发射器时序、穿透判定、碰撞阻断、伤害分发全部有断言覆盖。
  • Step 7: 全量回归与类型安全验证 (Regression & Typecheck)
    • 运行 npm run typecheck,确保零 TypeScript 语法与类型报错。
    • 运行 npx vitest run,确保全工程数千个单测 100% 通过,无回归破坏。
  • Step 8: 规范化 Git 提交与审查 (Git & Documentation)
    • 遵循分支隔离开发规范,在独立的 worktree 或特性分支上作业。
    • 经测试验证无误后,通过 Fast-forward 合并入 main 主干。
    • 归档开发文档与规范说明至 docs/。

附录:经典复杂技能对照与踩坑避雷手册

技能名称 技能 ID 级联投射物体系 核心底层机制 常见避坑要点
祝福之槌 (Blessed Hammer) 112 blessedhammer 阿基米德螺线飞行(pSrvDoFunc = 1 扩展),半径与角度随刻数同步线性放大 必须乘入 0.5 椭圆地面修正,否则螺线在菱形地面上严重失真变形;无视恶魔/不死系魔法抗性。
九头海蛇 (Hydra) 62 hydra (哨兵) \rightarrow hydrafire (火弹) 静态地面炮台(召唤物),存活 10 秒,具备索敌 AI 与射击仰角调整 海蛇本身是无速度投射物,需具备朝向最近敌怪旋转动画的能力;发射火弹拥有独立的射程与伤害。
陨石 (Meteor) 56 meteor (天火) \rightarrow meteorexplode \rightarrow meteorfire (留存火海) 延时降落打击(空投判定),碰撞后在地面生成多格持续灼烧的火墙实体(pSrvHitFunc 派生) 初始天顶坐标与地面落点之间有视觉视差与到达延时(约 1.2 秒);地面火海拥有独立的地块碰撞与持续跳字伤害。
暴风雪 (Blizzard) 59 blizzard (天顶发射器) \rightarrow blizzardice (落冰) 范围矩阵随机点降落(泊松/网格随机分布),多次间歇性区域轰炸 落冰并非全屏同时判定,而是根据时间戳在选定矩形区域内连续随机生成碎冰;每次落冰有独立的碰撞体积。
剧毒新星 (Poison Nova) 92 poisonnova (64 弹放射) 64 枚毒环冰刺瞬时同心圆扩散,采用 NextDelay = 0 但毒素拥有全局持续时间覆盖机制 毒素伤害是基于帧的周期性扣血,刷新中毒状态时遵循暗黑 2 独特的毒素速率(PoisonRate)与时长合并规则。

本文档为暗黑破坏神 II Web 端复杂法术系统实装的权威准则,所有后续法术技能开发均须以此规范为基准落地执行。