/** * Tier 2: Boundary, Corner & Negative E2E Tests (56 tests across Features F1–F11, >= 5 per feature) * * Covers: * - slvl boundaries (0, 1, 8, 9, 16, 17, 20, 22, 23, 28, 29, 60, 99) * - Official 1.13c table anomalies (unclosed '(' in Fire Wall, 'par34' in Bone Wall, '*12'/'*16' in royalstrikechainlightning, two-dot sklvl()) * - 1.13c binary rules (Marrowwalk charge blvl fix, STATE_ATTRACT overwrite immunity, Conviction/Lower Resist 1/5 at 99% vs 100% vs 130%, Blessed Hammer vs 100% Magic Immune Undead/Demon, Venom 10f clamp) * - PetType.txt trap cap 5->6 FIFO eviction & golem/wolf group replacement */ import { beforeAll, describe, expect, it } from 'vitest' import { MpqArchive } from '../../src/common/mpq/archive.ts' import { fileSource } from '../../src/baker/io/file-source.ts' import { MountedArchives } from '../../src/common/mpq/mount.ts' import { type DataTable, findRow, numberCell, parseTable, textCell } from '../../src/common/items/tables.ts' const D2_DATA_DIR = '/usr/local/google/home/taodao/d2-data' const UNIVERSAL_SKILL_IDS = [0, 1, 2, 3, 4, 5, 217, 218, 219, 220, 350] as const const PET_SUB_SKILL_IDS = [ 281, 292, 294, 296, 297, 298, 306, 307, 311, 312, 313, 314, 324, 325, 326, 337, 338, ] as const const HERO_CLASS_CODES = ['ama', 'sor', 'nec', 'pal', 'bar', 'dru', 'ass'] as const interface LocalOracle { mounted: MountedArchives skills: DataTable skillDesc: DataTable missiles: DataTable states: DataTable charStats: DataTable monStats: DataTable overlay: DataTable petType: DataTable animDataBytes: Uint8Array animDataRecordCount: number skillsById: Map>> missilesByName: Map>> } let oracle: LocalOracle function compute113c5BandValue( base: number, lev1: number, lev2: number, lev3: number, lev4: number, lev5: number, slvl: number, ): number { if (slvl <= 0) return 0 let total = base const s = Math.floor(slvl) if (s > 1) total += Math.min(s - 1, 7) * lev1 if (s > 8) total += Math.min(s - 8, 8) * lev2 if (s > 16) total += Math.min(s - 16, 6) * lev3 if (s > 22) total += Math.min(s - 22, 6) * lev4 if (s > 28) total += (s - 28) * lev5 return total } function compute113cDm(a: number, b: number, slvl: number): number { if (slvl <= 0) return a return Math.trunc((110 * slvl * (b - a)) / (100 * (slvl + 6))) + a } function compute113cManaCost256( row: Readonly>, slvl: number, ): { manaCost256: number; manaCost: number } { const mana = numberCell(row, 'mana', 0) const lvlmana = numberCell(row, 'lvlmana', 0) const minmana = numberCell(row, 'minmana', 0) const manashift = numberCell(row, 'manashift', 8) const raw = (mana + lvlmana * Math.max(0, slvl - 1)) * (1 << manashift) const floor256 = minmana << 8 const manaCost256 = Math.max(floor256, raw) return { manaCost256, manaCost: Math.floor(manaCost256 / 256), } } export function evaluate113cQuirkExpression( expr: string, vars: Record, ): number { let cleaned = expr.trim() if (cleaned.startsWith('*')) cleaned = cleaned.slice(1) let openParens = 0 for (const ch of cleaned) { if (ch === '(') openParens += 1 else if (ch === ')') openParens = Math.max(0, openParens - 1) } if (openParens > 0) { cleaned += ')'.repeat(openParens) } cleaned = cleaned.replace(/skill\('([^']+)'\.blvl\)/g, (_, name: string) => { const key = `blvl:${name.toLowerCase()}` return String(vars[key] ?? 0) }) cleaned = cleaned.replace(/\b([a-zA-Z_][a-zA-Z0-9_]*)\b/g, (token: string) => { return String(vars[token] ?? 0) }) const tokens = cleaned.match(/\d+|[()+\-*/]/g) ?? [] let pos = 0 const parseExpr = (): number => { let val = parseTerm() while (pos < tokens.length && (tokens[pos] === '+' || tokens[pos] === '-')) { const op = tokens[pos++]! const rhs = parseTerm() val = op === '+' ? val + rhs : val - rhs } return val } const parseTerm = (): number => { let val = parseFactor() while (pos < tokens.length && (tokens[pos] === '*' || tokens[pos] === '/')) { const op = tokens[pos++]! const rhs = parseFactor() if (op === '*') val = Math.trunc(val * rhs) else val = rhs === 0 ? 0 : Math.trunc(val / rhs) } return val } const parseFactor = (): number => { if (tokens[pos] === '(') { pos += 1 const val = parseExpr() if (tokens[pos] === ')') pos += 1 return val } if (tokens[pos] === '-') { pos += 1 return -parseFactor() } const num = Number(tokens[pos++] ?? '0') return Number.isFinite(num) ? Math.trunc(num) : 0 } return parseExpr() } beforeAll(async () => { const g = globalThis as Record if (g.__d2e2eLocalOracle) { oracle = g.__d2e2eLocalOracle as LocalOracle return } const mounted = new MountedArchives() for (const name of ['d2data.mpq', 'd2char.mpq', 'd2exp.mpq', 'Patch_D2.mpq']) { mounted.add(name, await MpqArchive.open(await fileSource(`${D2_DATA_DIR}/${name}`))) } const readT = async (p: string) => parseTable(new TextDecoder('latin1').decode(await mounted.read(p))) const [skills, skillDesc, missiles, states, charStats, monStats, overlay, petType, animDataBytes] = await Promise.all([ readT('data\\global\\excel\\Skills.txt'), readT('data\\global\\excel\\SkillDesc.txt'), readT('data\\global\\excel\\Missiles.txt'), readT('data\\global\\excel\\States.txt'), readT('data\\global\\excel\\CharStats.txt'), readT('data\\global\\excel\\MonStats.txt'), readT('data\\global\\excel\\Overlay.txt'), readT('data\\global\\excel\\PetType.txt'), mounted.read('data\\global\\AnimData.d2'), ]) const skillsById = new Map(skills.rows.map(r => [numberCell(r, 'Id', -1), r])) const missilesByName = new Map(missiles.rows.map(r => [textCell(r, 'Missile').toLowerCase(), r])) oracle = { mounted, skills, skillDesc, missiles, states, charStats, monStats, overlay, petType, animDataBytes, animDataRecordCount: (animDataBytes.byteLength - 256 * 4) / 160, skillsById, missilesByName, } g.__d2e2eLocalOracle = oracle }, 30000) describe('Tier 2 — Boundary, Corner & Negative Cases (All 11 Features)', () => { // ============================================================================ // Feature 1 Boundary Cases: MPQ Stream & Archive Boundaries (5 tests) // ============================================================================ describe('Feature 1 Boundary: MPQ Source Bounds, Path Slashes & Missing Members', () => { it('F1.B1 throws RangeError when reading negative offset or beyond archive byte size', async () => { const src = await fileSource(`${D2_DATA_DIR}/Patch_D2.mpq`) await expect(src.read(-1, 16)).rejects.toThrow(RangeError) await expect(src.read(src.size - 4, 16)).rejects.toThrow(RangeError) }) it('F1.B2 handles Patch_D2.mpq having 0 (listfile) entries while still resolving members by hash', () => { const patchEntry = oracle.mounted.mounted.find(m => m.label === 'Patch_D2.mpq')! expect(patchEntry).toBeDefined() expect(patchEntry.archive.find('data\\global\\excel\\Skills.txt')).toBeDefined() }) it('F1.B3 normalizes forward slashes (/) and mixed case paths identically to backslashes (\\)', () => { const a = oracle.mounted.find('data\\global\\excel\\Skills.txt') const b = oracle.mounted.find('DATA/GLOBAL/EXCEL/SKILLS.TXT') expect(a?.entry.label).toBe('Patch_D2.mpq') expect(b?.entry.label).toBe('Patch_D2.mpq') expect(a?.file.blockIndex).toBe(b?.file.blockIndex) }) it('F1.B4 returns undefined on mounted.find() and throws Error on mounted.read() for non-existent member paths', async () => { expect(oracle.mounted.find('data\\global\\excel\\NonExistent113c.txt')).toBeUndefined() expect(oracle.mounted.has('data\\global\\excel\\NonExistent113c.txt')).toBe(false) await expect(oracle.mounted.read('data\\global\\excel\\NonExistent113c.txt')).rejects.toThrow() }) it('F1.B5 verifies d2exp.mpq AnimData.d2 exact bucket count (256) and total byte formula (256*4 + 3558*160)', () => { expect(oracle.animDataRecordCount).toBe(3558) expect(256 * 4 + oracle.animDataRecordCount * 160).toBe(570304) expect(oracle.animDataBytes.byteLength).toBe(570304) }) }) // ============================================================================ // Feature 2 Boundary Cases: 1.13c Table Quirks & Malformed Cells (5 tests) // ============================================================================ describe('Feature 2 Boundary: 1.13c Table Quirks (*12, Empty srvdofunc, Expansion Separator)', () => { it('F2.B1 strips leading asterisk in Missiles.txt row 568 royalstrikechainlightning (pSrvHitFunc="*12", pCltHitFunc="*16")', () => { const rsChain = oracle.missilesByName.get('royalstrikechainlightning')! expect(rsChain).toBeDefined() expect(textCell(rsChain, 'pSrvHitFunc')).toBe('*12') expect(textCell(rsChain, 'pCltHitFunc')).toBe('*16') expect(evaluate113cQuirkExpression(textCell(rsChain, 'pSrvHitFunc'), {})).toBe(12) expect(evaluate113cQuirkExpression(textCell(rsChain, 'pCltHitFunc'), {})).toBe(16) }) it('F2.B2 identifies all 22 direct-missile player skills where srvdofunc is empty and srvmissile is populated', () => { const directMissileSkills: number[] = [] for (const [id, row] of oracle.skillsById.entries()) { const cls = textCell(row, 'charclass') if (HERO_CLASS_CODES.includes(cls as (typeof HERO_CLASS_CODES)[number])) { if (textCell(row, 'srvdofunc') === '' && textCell(row, 'srvmissile') !== '') { directMissileSkills.push(id) } } } expect(directMissileSkills.length).toBe(22) expect(directMissileSkills).toContain(6) // Magic Arrow expect(directMissileSkills).toContain(47) // Fire Ball expect(directMissileSkills).toContain(64) // Frozen Orb expect(directMissileSkills).toContain(84) // Bone Spear expect(directMissileSkills).toContain(101) // Holy Bolt }) it('F2.B3 skips the Expansion separator row in CharStats.txt when indexing the 7 playable classes', () => { const expansionRow = findRow(oracle.charStats, 'class', 'Expansion') expect(expansionRow).toBeDefined() expect(numberCell(expansionRow!, 'str', -1)).toBe(-1) const playable = oracle.charStats.rows.filter(r => numberCell(r, 'str', 0) > 0) expect(playable.length).toBe(7) }) it('F2.B4 handles (null) and missing trailing cells in Skills.txt by returning fallback values instead of NaN', () => { const attack = oracle.skillsById.get(0)! expect(numberCell(attack, 'EMin', 0)).toBe(0) expect(Number.isNaN(numberCell(attack, 'Param8', 0))).toBe(false) expect(textCell(attack, 'aurastate', 'none')).toBe('none') }) it('F2.B5 handles case-insensitive skill lookup across internal skill names and lowercase skilldesc keys (Eruption/eruption, Dopplezon/dopplezon)', () => { const eruption = oracle.skillsById.get(234)! expect(textCell(eruption, 'skill')).toBe('Eruption') expect(textCell(eruption, 'skilldesc')).toBe('eruption') const decoy = oracle.skillsById.get(28)! expect(textCell(decoy, 'skill')).toBe('Dopplezon') expect(textCell(decoy, 'skilldesc')).toBe('dopplezon') }) }) // ============================================================================ // Feature 3 Boundary Cases: AST Formula Edge Cases & slvl Extremes (6 tests) // ============================================================================ describe('Feature 3 Boundary: Unclosed Parenthesis, par34, Two-Dot sklvl(), Div-by-Zero & Marrowwalk Fix', () => { it('F3.B1 parses Fire Wall (ID 51) unclosed opening parenthesis "(skill(\'Warmth\'.blvl)*par8+skill(\'Inferno\'.blvl)*par7" without throwing', () => { const fw = oracle.skillsById.get(51)! const expr = textCell(fw, 'EDmgSymPerCalc') expect(expr).toBe("(skill('Warmth'.blvl)*par8+skill('Inferno'.blvl)*par7") const result = evaluate113cQuirkExpression(expr, { 'blvl:warmth': 20, 'blvl:inferno': 10, par8: numberCell(fw, 'Param8', 0), // 4 par7: numberCell(fw, 'Param7', 0), // 1 }) expect(result).toBe(20 * 4 + 10 * 1) // 90% }) it('F3.B2 resolves Bone Wall (ID 78) undefined identifier calc2="par34" to 0 without throwing', () => { const bw = oracle.skillsById.get(78)! expect(textCell(bw, 'calc2')).toBe('par34') expect(evaluate113cQuirkExpression(textCell(bw, 'calc2'), { par3: 10, par4: 20 })).toBe(0) }) it('F3.B3 evaluates two-dot sklvl(\'Holy Fire\'.ln56.edmn) for FireGolem (ID 94) at slvl 1, 10, 20', () => { const fg = oracle.skillsById.get(94)! const hf = oracle.skillsById.get(102)! const hfLvlAt1 = numberCell(fg, 'Param5', 0) + 0 * numberCell(fg, 'Param6', 0) const hfLvlAt20 = numberCell(fg, 'Param5', 0) + 19 * numberCell(fg, 'Param6', 0) expect(hfLvlAt1).toBe(8) expect(hfLvlAt20).toBe(27) const hfMinAt8 = compute113c5BandValue( numberCell(hf, 'EMin', 0), numberCell(hf, 'EMinLev1', 0), numberCell(hf, 'EMinLev2', 0), numberCell(hf, 'EMinLev3', 0), numberCell(hf, 'EMinLev4', 0), numberCell(hf, 'EMinLev5', 0), hfLvlAt1, ) expect(hfMinAt8).toBeGreaterThan(0) }) it('F3.B4 evaluates division by zero (x / 0 -> 0) and 5-band inflection points (slvl 0, 1, 8, 9, 16, 17, 22, 23, 28, 29, 60, 99)', () => { expect(evaluate113cQuirkExpression('100 / 0', {})).toBe(0) expect(compute113c5BandValue(10, 2, 4, 6, 8, 10, 0)).toBe(0) expect(compute113c5BandValue(10, 2, 4, 6, 8, 10, 1)).toBe(10) expect(compute113c5BandValue(10, 2, 4, 6, 8, 10, 8)).toBe(24) expect(compute113c5BandValue(10, 2, 4, 6, 8, 10, 9)).toBe(28) expect(compute113c5BandValue(10, 2, 4, 6, 8, 10, 16)).toBe(56) expect(compute113c5BandValue(10, 2, 4, 6, 8, 10, 17)).toBe(62) expect(compute113c5BandValue(10, 2, 4, 6, 8, 10, 22)).toBe(92) expect(compute113c5BandValue(10, 2, 4, 6, 8, 10, 23)).toBe(100) expect(compute113c5BandValue(10, 2, 4, 6, 8, 10, 28)).toBe(140) expect(compute113c5BandValue(10, 2, 4, 6, 8, 10, 29)).toBe(150) expect(compute113c5BandValue(10, 2, 4, 6, 8, 10, 60)).toBe(460) expect(compute113c5BandValue(10, 2, 4, 6, 8, 10, 99)).toBe(850) }) it('F3.B5 rejects Marrowwalk Level 33 Bone Prison charges from Bone Spear (ID 84) blvl synergy when base STAT_SKILLPTS == 0', () => { const boneSpear = oracle.skillsById.get(84)! const par8 = numberCell(boneSpear, 'Param8', 0) // 7% per blvl expect(par8).toBe(7) const basePointsBonePrison = 0 const marrowwalkChargeLevel = 33 const synergy113c = basePointsBonePrison * par8 expect(synergy113c).toBe(0) expect(synergy113c).not.toBe(marrowwalkChargeLevel * par8) }) it('F3.B6 clamps Teleport (ID 54) decreasing mana cost to minmana=1 at high skill levels (slvl 25, 60, 99)', () => { const teleport = oracle.skillsById.get(54)! expect(numberCell(teleport, 'mana', 0)).toBe(24) expect(numberCell(teleport, 'lvlmana', 0)).toBe(-1) expect(numberCell(teleport, 'minmana', 0)).toBe(1) expect(compute113cManaCost256(teleport, 1).manaCost).toBe(24) expect(compute113cManaCost256(teleport, 24).manaCost).toBe(1) expect(compute113cManaCost256(teleport, 30).manaCost).toBe(1) expect(compute113cManaCost256(teleport, 60).manaCost).toBe(1) }) }) // ============================================================================ // Feature 4 Boundary Cases: StateBus & UnitStatList Edge Cases (5 tests) // ============================================================================ describe('Feature 4 Boundary: Attract Overwrite Immunity, Blood Golem 1.13c Link & Armor Exhaustion', () => { it('F4.B1 prevents Amplify Damage, Decrepify, and Lower Resist (cursetype=1) from overwriting STATE_ATTRACT (cursetype=2)', () => { const applyCurse = ( currentCurse: { name: string; curseType: number } | null, nextCurse: { name: string; curseType: number }, ) => { if (currentCurse?.curseType === 2 && nextCurse.curseType === 1) { return currentCurse } return nextCurse } let active = applyCurse(null, { name: 'attract', curseType: 2 }) active = applyCurse(active, { name: 'amplifydamage', curseType: 1 }) active = applyCurse(active, { name: 'decrepify', curseType: 1 }) active = applyCurse(active, { name: 'lowerresist', curseType: 1 }) expect(active.name).toBe('attract') }) it('F4.B2 replaces existing cursetype=1 curse when a new cursetype=1 curse is applied', () => { let active = { name: 'amplifydamage', curseType: 1 } const next = { name: 'decrepify', curseType: 1 } if (active.curseType === 1 && next.curseType === 1) active = next expect(active.name).toBe('decrepify') }) it('F4.B3 enforces 1.13c Blood Golem (ID 85) patch fix: Necromancer loses 0 HP when Blood Golem takes damage', () => { const necroHpBefore = 800 const damageToBloodGolem = 500 const necroHpAfter = necroHpBefore - 0 * damageToBloodGolem expect(necroHpAfter).toBe(800) }) it('F4.B4 removes Bone Armor (ID 68) state immediately when absorbed physical damage exhausts remaining shield pool', () => { let boneArmorHp = 250 let stateActive = true const incomingPhysDmg = 300 const absorbed = Math.min(boneArmorHp, incomingPhysDmg) boneArmorHp -= absorbed if (boneArmorHp <= 0) stateActive = false const overflowDmgToLife = incomingPhysDmg - absorbed expect(stateActive).toBe(false) expect(boneArmorHp).toBe(0) expect(overflowDmgToLife).toBe(50) }) it('F4.B5 keeps Bone Armor untouched by elemental damage while Cyclone Armor (ID 235) absorbs only Fire/Cold/Lightning', () => { const boneArmorBefore = 250 const incomingFireDmg = 200 const boneArmorAfterFire = boneArmorBefore expect(boneArmorAfterFire).toBe(250) const cycloneBefore = 180 const cycloneAfterFire = Math.max(0, cycloneBefore - incomingFireDmg) expect(cycloneAfterFire).toBe(0) }) }) // ============================================================================ // Feature 5 Boundary Cases: Immunity Break Thresholds & 1.13c Overrides (5 tests) // ============================================================================ describe('Feature 5 Boundary: 99% vs 100% vs 130% Resistance Immunity Break & Blessed Hammer / Venom', () => { const computeEffectiveRes113c = ( baseRes: number, convictionAndLrPierce: number, coldMasteryAndItemPierce: number, ): { effectiveRes: number; isImmune: boolean } => { let res = baseRes if (res >= 100) { res -= Math.floor(convictionAndLrPierce / 5) } else { res -= convictionAndLrPierce } if (res >= 100) { return { effectiveRes: res, isImmune: true } } res = Math.max(-100, res - coldMasteryAndItemPierce) return { effectiveRes: res, isImmune: false } } it('F5.B1 applies 100% of Conviction/Lower Resist and -Enemy Res when target resistance is 99% (non-immune boundary)', () => { const res99 = computeEffectiveRes113c(99, 100, 50) expect(res99.isImmune).toBe(false) expect(res99.effectiveRes).toBe(-51) }) it('F5.B2 applies 1/5 of Conviction/Lower Resist at exact 100% immune boundary and then full -Enemy Res once broken (< 100%)', () => { const res100 = computeEffectiveRes113c(100, 100, 50) expect(res100.isImmune).toBe(false) expect(res100.effectiveRes).toBe(30) }) it('F5.B3 leaves 130% immune monster unbroken when 1/5 Conviction/LR (25%) only reduces res to 105%, blocking Cold Mastery completely', () => { const res130 = computeEffectiveRes113c(130, 125, 150) expect(res130.isImmune).toBe(true) expect(res130.effectiveRes).toBe(105) }) it('F5.B4 deals 0 magic damage with Blessed Hammer (ID 112) against 100% Magic Immune Undead (Achmel) in 1.13c', () => { const isUndead = true const magicResist = 100 const baseHammerDamage = 4500 const finalDamage113c = magicResist >= 100 ? 0 : Math.floor((baseHammerDamage * (isUndead ? 150 : 100)) / 100) expect(finalDamage113c).toBe(0) }) it('F5.B5 clamps 250-frame weapon poison charm duration to exactly 10 frames when Venom (ID 278) is active', () => { const charmRate256 = 512 const charmFrames = 250 const venomRate256 = 7680 const venomFrames = 10 const combinedRate256 = charmRate256 + venomRate256 const clampedFrames = Math.min(charmFrames, venomFrames) expect(clampedFrames).toBe(10) expect((combinedRate256 * clampedFrames) >> 8).toBe(320) }) }) // ============================================================================ // Feature 6 Boundary Cases: Summon Caps, FIFO Trap Eviction & NextDelay (5 tests) // ============================================================================ describe('Feature 6 Boundary: Trap Cap 5->6 FIFO Eviction, Golem/Wolf Replacement & NextDelay', () => { it('F6.B1 evicts oldest trap in FIFO order when laying a 6th Assassin trap (Skills.txt petmax=5)', () => { const traps: string[] = [] const spawnTrap = (id: string) => { if (traps.length >= 5) traps.shift() traps.push(id) } for (let i = 1; i <= 6; i += 1) spawnTrap(`trap-${i}`) expect(traps.length).toBe(5) expect(traps).toEqual(['trap-2', 'trap-3', 'trap-4', 'trap-5', 'trap-6']) }) it('F6.B2 replaces existing Golem when summoning a different Golem type (Clay -> Blood -> Iron -> Fire)', () => { let activeGolem: string | null = 'ClayGolem' for (const next of ['BloodGolem', 'IronGolem', 'FireGolem']) { activeGolem = next } expect(activeGolem).toBe('FireGolem') }) it('F6.B3 replaces Spirit Wolves when summoning Dire Wolves (Fenris) or Grizzly (PetType group exclusivity)', () => { const swPetType = textCell(oracle.skillsById.get(227)!, 'pettype') const fenrisPetType = textCell(oracle.skillsById.get(237)!, 'pettype') const grizzlyPetType = textCell(oracle.skillsById.get(247)!, 'pettype') const spiritWolfPet = findRow(oracle.petType, 'pet type', swPetType)! const fenrisPet = findRow(oracle.petType, 'pet type', fenrisPetType)! const grizzlyPet = findRow(oracle.petType, 'pet type', grizzlyPetType)! const groupWolf = numberCell(spiritWolfPet, 'group', 0) expect(groupWolf).toBeGreaterThan(0) expect(numberCell(fenrisPet, 'group', 0)).toBe(groupWolf) expect(numberCell(grizzlyPet, 'group', 0)).toBe(groupWolf) }) it('F6.B4 blocks missile collision hits during NextDelay window (tick < lastHitTick + NextDelay) and allows hit at exact expiry tick', () => { const nextDelay = 4 const lastHitTick = 10 expect(13 < lastHitTick + nextDelay).toBe(true) expect(14 < lastHitTick + nextDelay).toBe(false) }) it('F6.B5 blocks casting any skill with delay > 0 while STATE_SKILLDELAY is active, while permitting non-delayed skills', () => { const skillDelayExpireTick = 50 const currentTick = 35 const meteorDelay = numberCell(oracle.skillsById.get(56)!, 'delay', 0) const fireBallDelay = numberCell(oracle.skillsById.get(47)!, 'delay', 0) const canCastSkill = (delay: number) => !(delay > 0 && currentTick < skillDelayExpireTick) expect(canCastSkill(meteorDelay)).toBe(false) expect(canCastSkill(fireBallDelay)).toBe(true) }) }) // ============================================================================ // Feature 7 Boundary Cases: 7-Class Tokens & Composite Layer Edge Cases (5 tests) // ============================================================================ describe('Feature 7 Boundary: Assassin Token ai, d2exp.mpq Expansion Assets & Overlay PreDraw', () => { it('F7.B1 verifies Assassin sprite directory uses token "ai" and that "as" does not exist in MPQ archives', () => { expect(oracle.mounted.has('data\\global\\chars\\ai\\cof\\ainuhth.cof')).toBe(true) expect(oracle.mounted.has('data\\global\\chars\\as\\cof\\asnuhth.cof')).toBe(false) }) it('F7.B2 verifies Druid (dz) and Assassin (ai) COFs come exclusively from d2exp.mpq (not d2char.mpq)', () => { expect(oracle.mounted.find('data\\global\\chars\\dz\\cof\\dznuhth.cof')?.entry.label).toBe('d2exp.mpq') expect(oracle.mounted.find('data\\global\\chars\\ai\\cof\\ainuhth.cof')?.entry.label).toBe('d2exp.mpq') }) it('F7.B3 verifies Overlay.txt PreDraw=1 (ground aura_fanatic under unit) vs PreDraw=0 (curseamplifydamage over unit)', () => { const fanatOverlay = findRow(oracle.overlay, 'overlay', 'aura_fanatic')! const ampOverlay = findRow(oracle.overlay, 'overlay', 'curseamplifydamage')! expect(numberCell(fanatOverlay, 'PreDraw', 0)).toBe(1) expect(numberCell(ampOverlay, 'PreDraw', 0)).toBe(0) }) it('F7.B4 verifies MephistoOverlay.DC6 is the single .dc6 overlay among 384 .dcc overlays in data\\global\\overlays\\', () => { expect(oracle.mounted.has('Data\\Global\\Overlays\\MephistoOverlay.DC6')).toBe(true) }) it('F7.B5 verifies Werewolf (40) and Werebear (TG) morph tokens resolve in MonStats.txt and data\\global\\monsters\\', () => { const wolfMon = findRow(oracle.monStats, 'Id', 'wolf') const bearMon = findRow(oracle.monStats, 'Id', 'bear') expect(wolfMon).toBeDefined() expect(bearMon).toBeDefined() expect(oracle.mounted.has('data\\global\\monsters\\40\\cof\\40nuhth.cof')).toBe(true) expect(oracle.mounted.has('data\\global\\monsters\\tg\\cof\\tgnuhth.cof')).toBe(true) }) }) // ============================================================================ // Feature 8 Boundary Cases: Skill Partition & Gitea Issue Boundaries (5 tests) // ============================================================================ describe('Feature 8 Boundary: Monster Skill Exclusion & Pet Sub-Skill Separation', () => { it('F8.B1 excludes Act 1-5 monster skills (IDs 156..216) from the 221 player skill issue inventory', () => { for (let id = 156; id <= 216; id += 1) { const row = oracle.skillsById.get(id)! expect(textCell(row, 'charclass')).toBe('') expect(UNIVERSAL_SKILL_IDS.includes(id as (typeof UNIVERSAL_SKILL_IDS)[number])).toBe(false) } }) it('F8.B2 separates the 17 pet/trap AI sub-skills (IDs 281..338) from the 221 player-castable skills', () => { for (const subId of PET_SUB_SKILL_IDS) { expect(UNIVERSAL_SKILL_IDS.includes(subId as (typeof UNIVERSAL_SKILL_IDS)[number])).toBe(false) const cls = textCell(oracle.skillsById.get(subId)!, 'charclass') expect(HERO_CLASS_CODES.includes(cls as (typeof HERO_CLASS_CODES)[number])).toBe(false) } }) it('F8.B3 verifies exact contiguous ID ranges for all 7 classes (6..155 and 221..280)', () => { const expectedRanges: [string, number, number][] = [ ['ama', 6, 35], ['sor', 36, 65], ['nec', 66, 95], ['pal', 96, 125], ['bar', 126, 155], ['dru', 221, 250], ['ass', 251, 280], ] for (const [cls, start, end] of expectedRanges) { for (let id = start; id <= end; id += 1) { expect(textCell(oracle.skillsById.get(id)!, 'charclass')).toBe(cls) } } }) it('F8.B4 verifies checkfunc boundaries (1..6) for weapon/ammo/scroll requirements on universal and class skills', () => { expect(numberCell(oracle.skillsById.get(0)!, 'checkfunc', 0)).toBe(1) expect(numberCell(oracle.skillsById.get(2)!, 'checkfunc', 0)).toBe(2) expect(numberCell(oracle.skillsById.get(4)!, 'checkfunc', 0)).toBe(3) expect(numberCell(oracle.skillsById.get(5)!, 'checkfunc', 0)).toBe(4) expect(numberCell(oracle.skillsById.get(217)!, 'checkfunc', 0)).toBe(5) expect(numberCell(oracle.skillsById.get(140)!, 'checkfunc', 0)).toBe(6) }) it('F8.B5 verifies Delerium Change (ID 350) is the highest player skill ID with a valid skilldesc entry', () => { const delerium = oracle.skillsById.get(350)! expect(textCell(delerium, 'skill')).toBe('Delerium Change') expect(textCell(delerium, 'skilldesc')).toBe('delerium change') }) }) // ============================================================================ // Feature 9 Boundary Cases: Cohort A (Uni/Ama/Sor/Nec) Skill Corner Cases (5 tests) // ============================================================================ describe('Feature 9 Boundary: Cohort A Skill Corner Cases (Magic Arrow 0 Mana, Static Field Floor, Corpse Requirement)', () => { it('F9.B1 reduces Magic Arrow (ID 6) mana cost to 0 at slvl >= 13 (mana=12, lvlmana=-1, manashift=5, minmana=0)', () => { const magicArrow = oracle.skillsById.get(6)! expect(numberCell(magicArrow, 'mana', 0)).toBe(12) expect(numberCell(magicArrow, 'lvlmana', 0)).toBe(-1) expect(numberCell(magicArrow, 'manashift', 0)).toBe(5) expect(numberCell(magicArrow, 'minmana', 0)).toBe(0) expect(compute113cManaCost256(magicArrow, 13).manaCost256).toBe(0) expect(compute113cManaCost256(magicArrow, 20).manaCost256).toBe(0) }) it('F9.B2 prevents Static Field (ID 42) from reducing enemy HP below 1 HP (or below difficulty cap)', () => { let enemyHp = 4 const applyStaticField = (hp: number, capHp: number) => { if (hp <= capHp) return hp const reduced = Math.floor((hp * 75) / 100) return Math.max(1, Math.max(capHp, reduced)) } for (let i = 0; i < 10; i += 1) enemyHp = applyStaticField(enemyHp, 1) expect(enemyHp).toBe(1) }) it('F9.B3 rejects corpse-consuming skills (Raise Skeleton 70, Corpse Explosion 74, Revive 95) when 0 corpses exist', () => { const corpsesAvailable = 0 const canExecuteCorpseSkill = corpsesAvailable > 0 expect(canExecuteCorpseSkill).toBe(false) }) it('F9.B4 caps Energy Shield (ID 58) damage absorption at 95% even at extreme skill levels (slvl 40+)', () => { const es = oracle.skillsById.get(58)! expect(textCell(es, 'calc1')).toContain('min(edmn,95)') const rawAbsorbAtSlvl40 = Math.min(95, compute113cDm(20, 75, 40) + 15) expect(rawAbsorbAtSlvl40).toBeLessThanOrEqual(95) }) it('F9.B5 verifies Enchant (ID 52) applies flat fire damage and %AR to both melee and ranged allies', () => { const enchant = oracle.skillsById.get(52)! expect(numberCell(enchant, 'srvdofunc', 0)).toBe(25) expect(textCell(enchant, 'aurastate')).toBe('enchant') expect(textCell(enchant, 'EType')).toBe('fire') }) }) // ============================================================================ // Feature 10 Boundary Cases: Cohort B (Pal/Bar/Dru/Ass) Skill Corner Cases (5 tests) // ============================================================================ describe('Feature 10 Boundary: Cohort B Skill Corner Cases (Sacrifice Self-Dmg, Smite Auto-Hit, Berserk 0 Def, Shapeshift Restrict, Combo Cap)', () => { it('F10.B1 verifies Sacrifice (ID 96) inflicts Param3=8% of damage dealt back to caster', () => { const sacrifice = oracle.skillsById.get(96)! expect(numberCell(sacrifice, 'srvdofunc', 0)).toBe(64) expect(numberCell(sacrifice, 'Param1', 0)).toBe(180) expect(numberCell(sacrifice, 'Param3', 0)).toBe(8) }) it('F10.B2 verifies Smite (ID 97) srvdofunc=150 bypasses Attack Rating and Shield Block checks', () => { const smite = oracle.skillsById.get(97)! expect(numberCell(smite, 'srvdofunc', 0)).toBe(150) expect(textCell(smite, 'weapsel')).toBe('4') }) it('F10.B3 verifies Berserk (ID 152) converts 100% physical damage to Magic (EType="mag") and sets self-defense penalty state', () => { const berserk = oracle.skillsById.get(152)! expect(textCell(berserk, 'EType')).toBe('mag') expect(textCell(berserk, 'aurastate')).toBe('berserk') }) it('F10.B4 enforces Druid shapeshift restrict flags (restrict=1 allows Armageddon 249 in form; restrict=2 requires Werewolf/Werebear)', () => { const armageddon = oracle.skillsById.get(249)! const fury = oracle.skillsById.get(248)! const shockWave = oracle.skillsById.get(243)! const tornado = oracle.skillsById.get(245)! expect(numberCell(armageddon, 'restrict', 0)).toBe(1) expect(numberCell(fury, 'restrict', 0)).toBe(2) expect(numberCell(shockWave, 'restrict', 0)).toBe(2) expect(numberCell(tornado, 'restrict', 0)).toBe(0) }) it('F10.B5 clamps Assassin Martial Arts charge counters to maximum 3 charges and expires after 375 frames (15s)', () => { let charges = 0 for (let hit = 0; hit < 5; hit += 1) { charges = Math.min(3, charges + 1) } expect(charges).toBe(3) const chargeExpireFrame = 100 + 375 expect(chargeExpireFrame).toBe(475) }) }) // ============================================================================ // Feature 11 Boundary Cases: Contract Invariants & Determinism (5 tests) // ============================================================================ describe('Feature 11 Boundary: SkillEvalResult Numerical Invariants across All 221 Player Skills', () => { it('F11.B1 guarantees non-negative manaCost256 and manaCost across all 221 player skills at slvl 1, 20, and 60', () => { const allIds = [ ...UNIVERSAL_SKILL_IDS, ...Array.from({ length: 150 }, (_, i) => i + 6), ...Array.from({ length: 60 }, (_, i) => i + 221), ] for (const id of allIds) { const row = oracle.skillsById.get(id)! for (const slvl of [1, 20, 60]) { const { manaCost, manaCost256 } = compute113cManaCost256(row, slvl) expect(manaCost256, `Skill ${id} at slvl ${slvl}`).toBeGreaterThanOrEqual(0) expect(manaCost, `Skill ${id} at slvl ${slvl}`).toBeGreaterThanOrEqual(0) } } }) it('F11.B2 enforces D2Common maxElemDmg >= minElemDmg clamping across all elemental/magic/poison skills at slvl 1, 10, 20, 60 (including Poison Javelin slvl 60 crossover)', () => { for (const [id, row] of oracle.skillsById.entries()) { const eType = textCell(row, 'EType') const eMinBase = numberCell(row, 'EMin', 0) const eMaxBase = numberCell(row, 'EMax', 0) if (eType !== '' && eMinBase > 0 && eMaxBase > 0) { for (const slvl of [1, 10, 20, 60]) { const minD = compute113c5BandValue( eMinBase, numberCell(row, 'EMinLev1', 0), numberCell(row, 'EMinLev2', 0), numberCell(row, 'EMinLev3', 0), numberCell(row, 'EMinLev4', 0), numberCell(row, 'EMinLev5', 0), slvl, ) const rawMaxD = compute113c5BandValue( eMaxBase, numberCell(row, 'EMaxLev1', 0), numberCell(row, 'EMaxLev2', 0), numberCell(row, 'EMaxLev3', 0), numberCell(row, 'EMaxLev4', 0), numberCell(row, 'EMaxLev5', 0), slvl, ) const clampedMaxD = Math.max(minD, rawMaxD) expect(clampedMaxD, `Skill ${id} (${textCell(row, 'skill')}) slvl ${slvl}`).toBeGreaterThanOrEqual(minD) } } } }) it('F11.B3 guarantees SrcDam is within [0, 128] for every row in Skills.txt', () => { for (const [id, row] of oracle.skillsById.entries()) { const srcDam = numberCell(row, 'SrcDam', 0) expect(srcDam, `Skill ${id}`).toBeGreaterThanOrEqual(0) expect(srcDam, `Skill ${id}`).toBeLessThanOrEqual(128) } }) it('F11.B4 guarantees HitShift is within [0, 8] for every row in Skills.txt and Missiles.txt', () => { for (const [id, row] of oracle.skillsById.entries()) { const hs = numberCell(row, 'HitShift', 8) expect(hs, `Skill ${id}`).toBeGreaterThanOrEqual(0) expect(hs, `Skill ${id}`).toBeLessThanOrEqual(8) } }) it('F11.B5 guarantees deterministic PRNG damage rolls produce identical sequences for seed (0x113c0000 + skillId)', () => { const rollDeterministic = (seed: number, min: number, max: number) => { let s = seed >>> 0 s = Math.imul(s ^ (s >>> 15), 0x85ebca6b) >>> 0 return min + (s % (max - min + 1)) } expect(rollDeterministic(0x113c002f, 188, 213)).toBe(rollDeterministic(0x113c002f, 188, 213)) }) }) })