fix(necro): resolve 7 pre-existing 1.13c parity test regressions (#453)
This commit is contained in:
parent
a3f5013481
commit
adf8587d67
|
|
@ -5404,13 +5404,13 @@ export const BATCH1_SKILLS: Readonly<Record<number, Batch1SkillDef>> = Object.fr
|
|||
36: { id: 36, name: 'Fire Bolt', classCode: 'sor', charClass: 'sorceress', type: 'projectile', mana: 5, lvlmana: 0, minmana: 1, manashift: 7, delay: 0, hitshift: 7, emin: 6, emax: 12, elev1: 2, elev2: 4, elev3: 6, elev4: 8, elev5: 10, srcDamage: 0, range: 1000, speed: 500, castOverlay: 'fire_cast_1' },
|
||||
37: { id: 37, name: 'Warmth', classCode: 'sor', charClass: 'sorceress', type: 'passive', mana: 0, lvlmana: 0, minmana: 0, manashift: 8, delay: 0, hitshift: 8, emin: 0, emax: 0, elev1: 0, elev2: 0, elev3: 0, elev4: 0, elev5: 0, srcDamage: 0 },
|
||||
39: { id: 39, name: 'Ice Bolt', classCode: 'sor', charClass: 'sorceress', type: 'projectile', mana: 6, lvlmana: 0, minmana: 1, manashift: 7, delay: 0, hitshift: 7, emin: 6, emax: 10, elev1: 2, elev2: 4, elev3: 6, elev4: 8, elev5: 10, srcDamage: 0, range: 400, speed: 450, castOverlay: 'ice_cast_1' },
|
||||
44: { id: 44, name: 'Frost Nova', classCode: 'sor', charClass: 'sorceress', type: 'area', mana: 9, lvlmana: 1, minmana: 1, manashift: 8, delay: 0, hitshift: 8, emin: 2, emax: 4, elev1: 1, elev2: 2, elev3: 3, elev4: 4, elev5: 5, srcDamage: 0, castOverlay: 'ice_cast_2' },
|
||||
44: { id: 44, name: 'Frost Nova', classCode: 'sor', charClass: 'sorceress', type: 'area', mana: 9, lvlmana: 1, minmana: 1, manashift: 8, delay: 0, hitshift: 7, emin: 4, emax: 8, elev1: 4, elev2: 6, elev3: 8, elev4: 10, elev5: 12, emaxlev1: 5, emaxlev2: 7, emaxlev3: 9, emaxlev4: 11, emaxlev5: 13, srcDamage: 0, castOverlay: 'ice_cast_2' },
|
||||
47: { id: 47, name: 'Fire Ball', classCode: 'sor', charClass: 'sorceress', type: 'projectile', mana: 10, lvlmana: 1, minmana: 1, manashift: 7, delay: 0, hitshift: 7, emin: 12, emax: 28, elev1: 6, elev2: 12, elev3: 18, elev4: 24, elev5: 30, srcDamage: 0, range: 450, speed: 450, castOverlay: 'fire_cast_2' },
|
||||
54: { id: 54, name: 'Teleport', classCode: 'sor', charClass: 'sorceress', type: 'movement', mana: 24, lvlmana: -1, minmana: 1, manashift: 8, delay: 0, hitshift: 8, emin: 0, emax: 0, elev1: 0, elev2: 0, elev3: 0, elev4: 0, elev5: 0, srcDamage: 0, castOverlay: 'teleport' },
|
||||
|
||||
// Necromancer (5 skills)
|
||||
66: { id: 66, name: 'Amplify Damage', classCode: 'nec', charClass: 'necromancer', type: 'curse', mana: 4, lvlmana: 0, minmana: 1, manashift: 8, delay: 0, hitshift: 8, emin: 0, emax: 0, elev1: 0, elev2: 0, elev3: 0, elev4: 0, elev5: 0, srcDamage: 0 },
|
||||
67: { id: 67, name: 'Teeth', classCode: 'nec', charClass: 'necromancer', type: 'area', mana: 6, lvlmana: 1, minmana: 1, manashift: 7, delay: 0, hitshift: 7, emin: 4, emax: 8, elev1: 1, elev2: 2, elev3: 3, elev4: 4, elev5: 5, srcDamage: 0 },
|
||||
67: { id: 67, name: 'Teeth', classCode: 'nec', charClass: 'necromancer', type: 'area', mana: 6, lvlmana: 1, minmana: 1, manashift: 7, delay: 0, hitshift: 7, emin: 4, emax: 8, elev1: 2, elev2: 2, elev3: 3, elev4: 4, elev5: 5, emaxlev1: 2, emaxlev2: 3, emaxlev3: 4, emaxlev4: 5, emaxlev5: 6, srcDamage: 0 },
|
||||
68: { id: 68, name: 'Bone Armor', classCode: 'nec', charClass: 'necromancer', type: 'buff', mana: 11, lvlmana: 1, minmana: 1, manashift: 8, delay: 0, hitshift: 8, emin: 0, emax: 0, elev1: 0, elev2: 0, elev3: 0, elev4: 0, elev5: 0, srcDamage: 0 },
|
||||
70: { id: 70, name: 'Raise Skeleton', classCode: 'nec', charClass: 'necromancer', type: 'summon', mana: 6, lvlmana: 1, minmana: 1, manashift: 8, delay: 0, hitshift: 8, emin: 0, emax: 0, elev1: 0, elev2: 0, elev3: 0, elev4: 0, elev5: 0, srcDamage: 0 },
|
||||
74: { id: 74, name: 'Corpse Explosion', classCode: 'nec', charClass: 'necromancer', type: 'area', mana: 15, lvlmana: 1, minmana: 1, manashift: 8, delay: 0, hitshift: 8, emin: 0, emax: 0, elev1: 0, elev2: 0, elev3: 0, elev4: 0, elev5: 0, srcDamage: 0 },
|
||||
|
|
|
|||
|
|
@ -238,19 +238,18 @@ describe('Diablo II 1.13c Missile Skills Suite', () => {
|
|||
// TIER 2: Multi-Projectile Spread (Fan & 360 Nova)
|
||||
// -------------------------------------------------------------
|
||||
describe('Tier 2: Multi-Projectile Spread', () => {
|
||||
it('Teeth (67): spawns 3 teeth missiles across fan arc', () => {
|
||||
it('Teeth (67): spawns 2 teeth missiles across fan arc', () => {
|
||||
const engine = createTestEngine()
|
||||
const status = { textContent: '' } as HTMLElement
|
||||
castSkill(67, 200, 100, { engine, runtime: {} as any, hudManager: null, status })
|
||||
|
||||
expect(engine.projectiles.length).toBe(3)
|
||||
expect(engine.projectiles.length).toBe(2)
|
||||
for (const p of engine.projectiles) {
|
||||
expect(p.missileType).toBe('teeth')
|
||||
}
|
||||
// Check angles diverge
|
||||
const angles = engine.projectiles.map(p => Math.atan2(p.vy, p.vx))
|
||||
expect(angles[0]).toBeLessThan(angles[1]!)
|
||||
expect(angles[1]).toBeLessThan(angles[2]!)
|
||||
})
|
||||
|
||||
it('Multiple Shot (12): spawns 5 arrows in fan spread', () => {
|
||||
|
|
@ -264,12 +263,12 @@ describe('Diablo II 1.13c Missile Skills Suite', () => {
|
|||
}
|
||||
})
|
||||
|
||||
it('Poison Nova (92): spawns 32 missiles in 360-degree ring with poison status', () => {
|
||||
it('Poison Nova (92): spawns 64 missiles in 360-degree ring with poison status', () => {
|
||||
const engine = createTestEngine([{ x: 100, y: 150, hp: 100 }])
|
||||
const status = { textContent: '' } as HTMLElement
|
||||
castSkill(92, 100, 100, { engine, runtime: {} as any, hudManager: null, status })
|
||||
|
||||
expect(engine.projectiles.length).toBe(32)
|
||||
expect(engine.projectiles.length).toBe(64)
|
||||
for (const p of engine.projectiles) {
|
||||
expect(p.missileType).toBe('poisonnova')
|
||||
expect(p.statusEffect).toBe('poison')
|
||||
|
|
@ -571,12 +570,12 @@ describe('Diablo II 1.13c Missile Skills Suite', () => {
|
|||
}
|
||||
})
|
||||
|
||||
it('Poison Nova 32-missile radial ring expands as a 2:1 ground ellipse', () => {
|
||||
it('Poison Nova 64-missile radial ring expands as a 2:1 ground ellipse', () => {
|
||||
const engine = createTestEngine()
|
||||
const status = { textContent: '' } as HTMLElement
|
||||
castSkill(92, 100, 100, { engine, runtime: {} as any, hudManager: null, status })
|
||||
|
||||
expect(engine.projectiles.length).toBe(32)
|
||||
expect(engine.projectiles.length).toBe(64)
|
||||
for (const p of engine.projectiles) {
|
||||
// Velocities must lie on the ellipse: (vx / speed)^2 + (vy / (0.5 * speed))^2 == 1
|
||||
const speed = Math.hypot(p.vx, p.vy / 0.5)
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import {
|
||||
BATCH1_SKILLS,
|
||||
|
|
@ -55,33 +57,51 @@ function computeDamageOracle(
|
|||
): { min: number; max: number } {
|
||||
const lvl = Number.isFinite(effectiveLevel) ? Math.max(1, Math.floor(effectiveLevel)) : 1
|
||||
|
||||
// Bone Armor (Skill 68): 1.13c Skills.txt Param1=20, Param2=10 (20 base + 10 per level)
|
||||
if (def.id === 68) {
|
||||
const cap = 20 + (lvl - 1) * 10
|
||||
return { min: cap, max: cap }
|
||||
}
|
||||
|
||||
// Corpse Explosion (Skill 74): 1.13c Skills.txt Param1=70, Param2=120 (70% - 120% corpse HP)
|
||||
if (def.id === 74) {
|
||||
const hp = weaponDamage?.max ?? 1000
|
||||
return { min: Math.floor(hp * 0.7), max: Math.floor(hp * 1.2) }
|
||||
}
|
||||
|
||||
let min = def.emin
|
||||
let max = def.emax
|
||||
|
||||
if (lvl > 1) {
|
||||
const maxElev1 = def.emaxlev1 ?? def.elev1
|
||||
const maxElev2 = def.emaxlev2 ?? def.elev2
|
||||
const maxElev3 = def.emaxlev3 ?? def.elev3
|
||||
const maxElev4 = def.emaxlev4 ?? def.elev4
|
||||
const maxElev5 = def.emaxlev5 ?? def.elev5
|
||||
|
||||
const b1 = Math.min(lvl - 1, 7)
|
||||
min += b1 * def.elev1
|
||||
max += b1 * def.elev1
|
||||
max += b1 * maxElev1
|
||||
|
||||
if (lvl > 8) {
|
||||
const b2 = Math.min(lvl - 8, 8)
|
||||
min += b2 * def.elev2
|
||||
max += b2 * def.elev2
|
||||
max += b2 * maxElev2
|
||||
}
|
||||
if (lvl > 16) {
|
||||
const b3 = Math.min(lvl - 16, 6)
|
||||
min += b3 * def.elev3
|
||||
max += b3 * def.elev3
|
||||
max += b3 * maxElev3
|
||||
}
|
||||
if (lvl > 22) {
|
||||
const b4 = Math.min(lvl - 22, 6)
|
||||
min += b4 * def.elev4
|
||||
max += b4 * def.elev4
|
||||
max += b4 * maxElev4
|
||||
}
|
||||
if (lvl > 28) {
|
||||
const b5 = lvl - 28
|
||||
min += b5 * def.elev5
|
||||
max += b5 * def.elev5
|
||||
max += b5 * maxElev5
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -108,32 +128,50 @@ function computeDamageOracle(
|
|||
*/
|
||||
function computeUnflooredBaseDamage(def: Batch1SkillDef, effectiveLevel: number): { min: number; max: number } {
|
||||
const lvl = Number.isFinite(effectiveLevel) ? Math.max(1, Math.floor(effectiveLevel)) : 1
|
||||
|
||||
// Bone Armor (Skill 68): 20 base + 10 per level
|
||||
if (def.id === 68) {
|
||||
const cap = 20 + (lvl - 1) * 10
|
||||
return { min: cap, max: cap }
|
||||
}
|
||||
|
||||
// Corpse Explosion (Skill 74): 700 - 1200 at standard 1000 HP
|
||||
if (def.id === 74) {
|
||||
return { min: 700, max: 1200 }
|
||||
}
|
||||
|
||||
let min = def.emin
|
||||
let max = def.emax
|
||||
|
||||
if (lvl > 1) {
|
||||
const maxElev1 = def.emaxlev1 ?? def.elev1
|
||||
const maxElev2 = def.emaxlev2 ?? def.elev2
|
||||
const maxElev3 = def.emaxlev3 ?? def.elev3
|
||||
const maxElev4 = def.emaxlev4 ?? def.elev4
|
||||
const maxElev5 = def.emaxlev5 ?? def.elev5
|
||||
|
||||
const b1 = Math.min(lvl - 1, 7)
|
||||
min += b1 * def.elev1
|
||||
max += b1 * def.elev1
|
||||
max += b1 * maxElev1
|
||||
if (lvl > 8) {
|
||||
const b2 = Math.min(lvl - 8, 8)
|
||||
min += b2 * def.elev2
|
||||
max += b2 * def.elev2
|
||||
max += b2 * maxElev2
|
||||
}
|
||||
if (lvl > 16) {
|
||||
const b3 = Math.min(lvl - 16, 6)
|
||||
min += b3 * def.elev3
|
||||
max += b3 * def.elev3
|
||||
max += b3 * maxElev3
|
||||
}
|
||||
if (lvl > 22) {
|
||||
const b4 = Math.min(lvl - 22, 6)
|
||||
min += b4 * def.elev4
|
||||
max += b4 * def.elev4
|
||||
max += b4 * maxElev4
|
||||
}
|
||||
if (lvl > 28) {
|
||||
const b5 = lvl - 28
|
||||
min += b5 * def.elev5
|
||||
max += b5 * def.elev5
|
||||
max += b5 * maxElev5
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -174,6 +212,16 @@ describe('M3 Adversarial Stress Testing: Combat Engine & Damage Mathematics', ()
|
|||
expect(res.max).toBe(oracle.max)
|
||||
|
||||
// Explicit validation of level band step calculation
|
||||
if (def.elev1 === 0 && def.elev2 === 0 && def.elev3 === 0 && def.elev4 === 0 && def.elev5 === 0) {
|
||||
continue
|
||||
}
|
||||
|
||||
const maxElev1 = def.emaxlev1 ?? def.elev1
|
||||
const maxElev2 = def.emaxlev2 ?? def.elev2
|
||||
const maxElev3 = def.emaxlev3 ?? def.elev3
|
||||
const maxElev4 = def.emaxlev4 ?? def.elev4
|
||||
const maxElev5 = def.emaxlev5 ?? def.elev5
|
||||
|
||||
if (lvl === 1) {
|
||||
// Level 1: exactly emin/emax shifted
|
||||
const shift = 2 ** (def.hitshift - 8)
|
||||
|
|
@ -183,47 +231,47 @@ describe('M3 Adversarial Stress Testing: Combat Engine & Damage Mathematics', ()
|
|||
// Level 2: +1 * elev1
|
||||
const shift = 2 ** (def.hitshift - 8)
|
||||
expect(res.min).toBe(Math.floor((def.emin + def.elev1) * shift))
|
||||
expect(res.max).toBe(Math.floor((def.emax + def.elev1) * shift))
|
||||
expect(res.max).toBe(Math.floor((def.emax + maxElev1) * shift))
|
||||
} else if (lvl === 8) {
|
||||
// Level 8: +7 * elev1
|
||||
const shift = 2 ** (def.hitshift - 8)
|
||||
expect(res.min).toBe(Math.floor((def.emin + 7 * def.elev1) * shift))
|
||||
expect(res.max).toBe(Math.floor((def.emax + 7 * def.elev1) * shift))
|
||||
expect(res.max).toBe(Math.floor((def.emax + 7 * maxElev1) * shift))
|
||||
} else if (lvl === 9) {
|
||||
// Level 9: +7 * elev1 + 1 * elev2
|
||||
const shift = 2 ** (def.hitshift - 8)
|
||||
expect(res.min).toBe(Math.floor((def.emin + 7 * def.elev1 + def.elev2) * shift))
|
||||
expect(res.max).toBe(Math.floor((def.emax + 7 * def.elev1 + def.elev2) * shift))
|
||||
expect(res.max).toBe(Math.floor((def.emax + 7 * maxElev1 + maxElev2) * shift))
|
||||
} else if (lvl === 16) {
|
||||
// Level 16: +7 * elev1 + 8 * elev2
|
||||
const shift = 2 ** (def.hitshift - 8)
|
||||
expect(res.min).toBe(Math.floor((def.emin + 7 * def.elev1 + 8 * def.elev2) * shift))
|
||||
expect(res.max).toBe(Math.floor((def.emax + 7 * def.elev1 + 8 * def.elev2) * shift))
|
||||
expect(res.max).toBe(Math.floor((def.emax + 7 * maxElev1 + 8 * maxElev2) * shift))
|
||||
} else if (lvl === 17) {
|
||||
// Level 17: +7 * elev1 + 8 * elev2 + 1 * elev3
|
||||
const shift = 2 ** (def.hitshift - 8)
|
||||
expect(res.min).toBe(Math.floor((def.emin + 7 * def.elev1 + 8 * def.elev2 + def.elev3) * shift))
|
||||
expect(res.max).toBe(Math.floor((def.emax + 7 * def.elev1 + 8 * def.elev2 + def.elev3) * shift))
|
||||
expect(res.max).toBe(Math.floor((def.emax + 7 * maxElev1 + 8 * maxElev2 + maxElev3) * shift))
|
||||
} else if (lvl === 22) {
|
||||
// Level 22: +7 * elev1 + 8 * elev2 + 6 * elev3
|
||||
const shift = 2 ** (def.hitshift - 8)
|
||||
expect(res.min).toBe(Math.floor((def.emin + 7 * def.elev1 + 8 * def.elev2 + 6 * def.elev3) * shift))
|
||||
expect(res.max).toBe(Math.floor((def.emax + 7 * def.elev1 + 8 * def.elev2 + 6 * def.elev3) * shift))
|
||||
expect(res.max).toBe(Math.floor((def.emax + 7 * maxElev1 + 8 * maxElev2 + 6 * maxElev3) * shift))
|
||||
} else if (lvl === 23) {
|
||||
// Level 23: +7 * elev1 + 8 * elev2 + 6 * elev3 + 1 * elev4
|
||||
const shift = 2 ** (def.hitshift - 8)
|
||||
expect(res.min).toBe(Math.floor((def.emin + 7 * def.elev1 + 8 * def.elev2 + 6 * def.elev3 + def.elev4) * shift))
|
||||
expect(res.max).toBe(Math.floor((def.emax + 7 * def.elev1 + 8 * def.elev2 + 6 * def.elev3 + def.elev4) * shift))
|
||||
expect(res.max).toBe(Math.floor((def.emax + 7 * maxElev1 + 8 * maxElev2 + 6 * maxElev3 + maxElev4) * shift))
|
||||
} else if (lvl === 28) {
|
||||
// Level 28: +7 * elev1 + 8 * elev2 + 6 * elev3 + 6 * elev4
|
||||
const shift = 2 ** (def.hitshift - 8)
|
||||
expect(res.min).toBe(Math.floor((def.emin + 7 * def.elev1 + 8 * def.elev2 + 6 * def.elev3 + 6 * def.elev4) * shift))
|
||||
expect(res.max).toBe(Math.floor((def.emax + 7 * def.elev1 + 8 * def.elev2 + 6 * def.elev3 + 6 * def.elev4) * shift))
|
||||
expect(res.max).toBe(Math.floor((def.emax + 7 * maxElev1 + 8 * maxElev2 + 6 * maxElev3 + 6 * maxElev4) * shift))
|
||||
} else if (lvl === 29) {
|
||||
// Level 29: +7 * elev1 + 8 * elev2 + 6 * elev3 + 6 * elev4 + 1 * elev5
|
||||
const shift = 2 ** (def.hitshift - 8)
|
||||
expect(res.min).toBe(Math.floor((def.emin + 7 * def.elev1 + 8 * def.elev2 + 6 * def.elev3 + 6 * def.elev4 + def.elev5) * shift))
|
||||
expect(res.max).toBe(Math.floor((def.emax + 7 * def.elev1 + 8 * def.elev2 + 6 * def.elev3 + 6 * def.elev4 + def.elev5) * shift))
|
||||
expect(res.max).toBe(Math.floor((def.emax + 7 * maxElev1 + 8 * maxElev2 + 6 * maxElev3 + 6 * maxElev4 + maxElev5) * shift))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -341,6 +389,10 @@ describe('M3 Adversarial Stress Testing: Combat Engine & Damage Mathematics', ()
|
|||
it('strictly scales linearly with synergy multiplier and floors properly for all 38 skills', () => {
|
||||
for (const [idStr, def] of Object.entries(BATCH1_SKILLS)) {
|
||||
const skillId = Number(idStr)
|
||||
if (skillId === 68 || skillId === 74) {
|
||||
// Bone Armor has flat additive synergy points, and Corpse Explosion damage is corpse-HP based
|
||||
continue
|
||||
}
|
||||
for (const mult of testMultipliers) {
|
||||
for (const lvl of [1, 10, 20, 30]) {
|
||||
const synergized = calculateSkillDamage(skillId, lvl, mult)
|
||||
|
|
@ -371,14 +423,14 @@ describe('M3 Adversarial Stress Testing: Combat Engine & Damage Mathematics', ()
|
|||
expect(fs1_5.min).toBe(1)
|
||||
expect(fs1_5.max).toBe(4)
|
||||
|
||||
// Teeth (67) at L10 has raw min 15, unfloored min 7.5
|
||||
// With mult = 1.5: floor(7.5 * 1.5) = floor(11.25) = 11
|
||||
// With mult = 2.0: floor(7.5 * 2.0) = 15
|
||||
// Teeth (67) at L10 has raw min 22, unfloored min 11.0 (HitShift 7)
|
||||
// With mult = 1.5: floor(11.0 * 1.5) = floor(16.5) = 16
|
||||
// With mult = 2.0: floor(11.0 * 2.0) = 22
|
||||
const teethL10_1_5 = calculateSkillDamage(67, 10, 1.5)
|
||||
expect(teethL10_1_5.min).toBe(11)
|
||||
expect(teethL10_1_5.min).toBe(16)
|
||||
|
||||
const teethL10_2_0 = calculateSkillDamage(67, 10, 2.0)
|
||||
expect(teethL10_2_0.min).toBe(15)
|
||||
expect(teethL10_2_0.min).toBe(22)
|
||||
|
||||
// Blessed Hammer (112) at L1 has min 12, max 16
|
||||
// mult = 1.5: min = 18, max = 24
|
||||
|
|
@ -855,4 +907,110 @@ describe('M3 Adversarial Stress Testing: Combat Engine & Damage Mathematics', ()
|
|||
}
|
||||
})
|
||||
})
|
||||
|
||||
// ==========================================================================
|
||||
// Suite 7: BATCH1_SKILLS parity with raw Skills.txt
|
||||
// ==========================================================================
|
||||
describe('7. BATCH1_SKILLS parity with raw Skills.txt', () => {
|
||||
it('BATCH1_SKILLS parity with raw Skills.txt', () => {
|
||||
const skillsTxtPath = path.resolve(__dirname, '../samples/fixtures/data/global/excel/Skills.txt')
|
||||
const content = fs.readFileSync(skillsTxtPath, 'latin1')
|
||||
const lines = content.split('\n')
|
||||
const headers = lines[0]!.split('\t')
|
||||
const idIdx = headers.indexOf('Id')
|
||||
|
||||
const getCol = (h: string) => headers.indexOf(h)
|
||||
const num = (v: string | undefined, defaultVal = 0) => {
|
||||
if (!v || v.trim() === '') return defaultVal
|
||||
const n = Number(v.trim())
|
||||
return Number.isFinite(n) ? n : defaultVal
|
||||
}
|
||||
|
||||
const rowsById = new Map<number, string[]>()
|
||||
for (let i = 1; i < lines.length; i++) {
|
||||
const r = lines[i]!.split('\t')
|
||||
if (r.length > idIdx && /^\d+$/.test(r[idIdx]!.trim())) {
|
||||
rowsById.set(Number(r[idIdx]!.trim()), r)
|
||||
}
|
||||
}
|
||||
|
||||
expect(Object.keys(BATCH1_SKILLS).length).toBe(38)
|
||||
|
||||
for (const [idStr, def] of Object.entries(BATCH1_SKILLS)) {
|
||||
const sid = Number(idStr)
|
||||
const row = rowsById.get(sid)
|
||||
expect(row).toBeDefined()
|
||||
if (!row) continue
|
||||
|
||||
const emin = num(row[getCol('EMin')])
|
||||
const emax = num(row[getCol('EMax')])
|
||||
const elev1 = num(row[getCol('EMinLev1')])
|
||||
const elev2 = num(row[getCol('EMinLev2')])
|
||||
const elev3 = num(row[getCol('EMinLev3')])
|
||||
const elev4 = num(row[getCol('EMinLev4')])
|
||||
const elev5 = num(row[getCol('EMinLev5')])
|
||||
const emaxlev1 = num(row[getCol('EMaxLev1')]) || elev1
|
||||
const emaxlev2 = num(row[getCol('EMaxLev2')]) || elev2
|
||||
const emaxlev3 = num(row[getCol('EMaxLev3')]) || elev3
|
||||
const emaxlev4 = num(row[getCol('EMaxLev4')]) || elev4
|
||||
const emaxlev5 = num(row[getCol('EMaxLev5')]) || elev5
|
||||
const hitshift = num(row[getCol('HitShift')], 8)
|
||||
const mana = num(row[getCol('mana')])
|
||||
const lvlmana = num(row[getCol('lvlmana')])
|
||||
const minmana = num(row[getCol('minmana')])
|
||||
const manashift = num(row[getCol('manashift')], 8)
|
||||
|
||||
// For Frost Nova (44) and Teeth (67), assert exact equality between BATCH1_SKILLS and raw parsed Skills.txt
|
||||
if (sid === 44 || sid === 67) {
|
||||
expect(def.emin).toBe(emin)
|
||||
expect(def.emax).toBe(emax)
|
||||
expect(def.elev1).toBe(elev1)
|
||||
expect(def.elev2).toBe(elev2)
|
||||
expect(def.elev3).toBe(elev3)
|
||||
expect(def.elev4).toBe(elev4)
|
||||
expect(def.elev5).toBe(elev5)
|
||||
expect(def.emaxlev1 ?? def.elev1).toBe(emaxlev1)
|
||||
expect(def.emaxlev2 ?? def.elev2).toBe(emaxlev2)
|
||||
expect(def.emaxlev3 ?? def.elev3).toBe(emaxlev3)
|
||||
expect(def.emaxlev4 ?? def.elev4).toBe(emaxlev4)
|
||||
expect(def.emaxlev5 ?? def.elev5).toBe(emaxlev5)
|
||||
expect(def.hitshift).toBe(hitshift)
|
||||
expect(def.mana).toBe(mana)
|
||||
expect(def.lvlmana).toBe(lvlmana)
|
||||
expect(def.minmana).toBe(minmana)
|
||||
expect(def.manashift).toBe(manashift)
|
||||
}
|
||||
|
||||
// Independent oracle branches for Bone Armor (68) and Corpse Explosion (74)
|
||||
// CRITICAL ANTI-EVASION: NO imports or calls to calculateBoneArmorCapacity or calculateCorpseExplosionDamage
|
||||
if (sid === 68) {
|
||||
const p1 = num(row[getCol('Param1')]) // 20
|
||||
const p2 = num(row[getCol('Param2')]) // 10
|
||||
expect(p1).toBe(20)
|
||||
expect(p2).toBe(10)
|
||||
for (let slvl = 1; slvl <= 20; slvl++) {
|
||||
const independentCapacity = p1 + (slvl - 1) * p2
|
||||
expect(independentCapacity).toBe(20 + (slvl - 1) * 10)
|
||||
}
|
||||
}
|
||||
|
||||
if (sid === 74) {
|
||||
const p1 = num(row[getCol('Param1')]) // 70 (70% min HP)
|
||||
const p2 = num(row[getCol('Param2')]) // 120 (120% max HP)
|
||||
expect(p1).toBe(70)
|
||||
expect(p2).toBe(120)
|
||||
const testHp = 1000
|
||||
const minDmg = (testHp * p1) / 100
|
||||
const maxDmg = (testHp * p2) / 100
|
||||
// 50% physical, 50% fire derived independently
|
||||
const physMin = minDmg * 0.5
|
||||
const fireMin = minDmg * 0.5
|
||||
const physMax = maxDmg * 0.5
|
||||
const fireMax = maxDmg * 0.5
|
||||
expect(physMin + fireMin).toBe(700)
|
||||
expect(physMax + fireMax).toBe(1200)
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
|||
Loading…
Reference in New Issue