diablo2-web/tests/data-contract-lighting.test.ts

163 lines
6.0 KiB
TypeScript

import { describe, it, expect } from 'vitest'
import { readFileSync } from 'node:fs'
import { join } from 'node:path'
import { parseMissilesTxt, getMissileTxtData, parseOverlayTxt, getOverlayTxtData, parseColorChannel } from '../src/game/skills.ts'
import { numberCell, parseTable } from '../src/game/tables.ts'
import { CANONICAL_LEVEL_LIGHTING, getLevelLighting } from '../src/game/levels-meta.ts'
describe('P0 Data Contract & Table Repair Tests', () => {
it('1. parseColorChannel correctly preserves authentic 0 values and falls back for empty', () => {
expect(parseColorChannel('0')).toBe(0)
expect(parseColorChannel(' 0 ')).toBe(0)
expect(parseColorChannel('255')).toBe(255)
expect(parseColorChannel('128')).toBe(128)
expect(parseColorChannel('')).toBe(255)
expect(parseColorChannel(' ')).toBe(255)
expect(parseColorChannel(undefined)).toBe(255)
expect(parseColorChannel(undefined, 100)).toBe(100)
expect(parseColorChannel('invalid', 100)).toBe(100)
})
it('2. numberCell in tables.ts preserves 0 while returning fallback on empty/whitespace cells', () => {
const row = {
zeroVal: '0',
emptyVal: '',
whitespaceVal: ' ',
positiveVal: '128',
}
expect(numberCell(row, 'zeroVal', 255)).toBe(0)
expect(numberCell(row, 'emptyVal', 255)).toBe(255)
expect(numberCell(row, 'whitespaceVal', 255)).toBe(255)
expect(numberCell(row, 'missingVal', 255)).toBe(255)
expect(numberCell(row, 'positiveVal', 255)).toBe(128)
})
it('3. authentic 1.13c Missiles with 0 color channels retain 0 instead of defaulting to 255', () => {
const txtPath = join(process.cwd(), 'samples/d2/Patch_D2.mpq') // or txt
// Test with actual table rows from 1.13c Missiles.txt
const sampleTsv = [
'Missile\tId\tVel\tMaxVel\tRange\tLevRange\tCelFile\tAnimLen\tAnimSpeed\tLoopAnim\tNumDirections\tExplosionMissile\tLight\tRed\tGreen\tBlue\tTrans',
'unholybolt1\t100\t20\t20\t50\t0\tUnholyBolt\t16\t16\t1\t1\t\t5\t0\t0\t0\t2',
'curseeffectred\t101\t20\t20\t50\t0\tCurse\t16\t16\t1\t1\t\t1\t255\t0\t0\t1',
'firearrow\t12\t24\t24\t40\t0\tFireArrow\t8\t16\t0\t32\tfireexplode\t6\t255\t178\t64\t1',
'arrow\t0\t24\t24\t40\t0\tArrow\t1\t16\t0\t32\t\t0\t\t\t\t0',
].join('\n')
const map = parseMissilesTxt(sampleTsv)
const unholy = map.get('unholybolt1')!
expect(unholy).toBeDefined()
expect(unholy.light).toBe(5)
expect(unholy.red).toBe(0)
expect(unholy.green).toBe(0)
expect(unholy.blue).toBe(0)
expect(unholy.trans).toBe(2)
const curseRed = map.get('curseeffectred')!
expect(curseRed).toBeDefined()
expect(curseRed.light).toBe(1)
expect(curseRed.red).toBe(255)
expect(curseRed.green).toBe(0)
expect(curseRed.blue).toBe(0)
expect(curseRed.trans).toBe(1)
const fireArrow = map.get('firearrow')!
expect(fireArrow.trans).toBe(1)
const arrow = map.get('arrow')!
expect(arrow.trans).toBe(0)
expect(arrow.red).toBe(255) // fallback on empty
})
it('4. authentic 1.13c Overlay curse lines retain true 0 color channels', () => {
const sampleTsv = [
'overlay\tFilename\tFrames\tAnimRate\tTrans\tPreDraw\tInitRadius\tRadius\tRed\tGreen\tBlue',
'curseironmaiden\tironmaiden\t14\t16\t3\t0\t1\t6\t255\t0\t0',
'curseconfuse\tconfuse\t14\t16\t3\t0\t1\t6\t0\t255\t0',
'cursedecrepify\tdecrepify\t14\t16\t3\t0\t1\t6\t0\t0\t255',
].join('\n')
const map = parseOverlayTxt(sampleTsv)
const im = map.get('curseironmaiden')!
expect(im.red).toBe(255)
expect(im.green).toBe(0)
expect(im.blue).toBe(0)
const confuse = map.get('curseconfuse')!
expect(confuse.red).toBe(0)
expect(confuse.green).toBe(255)
expect(confuse.blue).toBe(0)
const decrepify = map.get('cursedecrepify')!
expect(decrepify.red).toBe(0)
expect(decrepify.green).toBe(0)
expect(decrepify.blue).toBe(255)
})
it('5. getMissileTxtData returns authentic trans values for physical vs magical missiles', () => {
const arrow = getMissileTxtData('arrow')
expect(arrow.trans).toBe(0)
const multi = getMissileTxtData('multipleshotarrow')
expect(multi.trans).toBe(0)
const meteor = getMissileTxtData('meteor')
expect(meteor.trans).toBe(0)
const firebolt = getMissileTxtData('firebolt')
expect(firebolt.trans).toBe(1)
const chargedbolt = getMissileTxtData('chargedbolt')
expect(chargedbolt.trans).toBe(1)
const firearrow = getMissileTxtData('firearrow')
expect(firearrow.trans).toBe(1)
const magicarrow = getMissileTxtData('magicarrow')
expect(magicarrow.trans).toBe(1)
const icearrow = getMissileTxtData('icearrow')
expect(icearrow.trans).toBe(1)
const javelin = getMissileTxtData('lightningjavelin')
expect(javelin.trans).toBe(1)
})
it('6. CANONICAL_LEVEL_LIGHTING covers all 136 1.13c levels with authentic metadata', () => {
expect(Object.keys(CANONICAL_LEVEL_LIGHTING).length).toBe(136)
// Rogue Encampment (Act 1 Town, outdoor)
const town1 = getLevelLighting(1)!
expect(town1).toBeDefined()
expect(town1.isInside).toBe(false)
expect(town1.act).toBe(1)
// Catacombs 4 (Andariel, inside)
const cata4 = getLevelLighting(39)! // Catacombs Level 4 is id 37 or 39
// Level 37: Catacombs 4
const level37 = getLevelLighting(37)!
expect(level37.isInside).toBe(true)
expect(level37.intensity).toBe(0) // 0 intensity = pitch dark, authentic 1.13c ground truth
// Lut Gholein Sewer 1 (Intensity = 8)
const sewer1 = getLevelLighting(47)!
expect(sewer1.isInside).toBe(true)
expect(sewer1.intensity).toBe(8)
// Harem (Intensity = 128)
const harem = getLevelLighting(50)!
expect(harem.isInside).toBe(true)
expect(harem.intensity).toBe(128)
// Arcane Sanctuary (NoPer = true)
const arcane = getLevelLighting(74)!
expect(arcane.noPer).toBe(true)
// Duriel's Lair (NoPer = true, inside = true)
const duriel = getLevelLighting(73)!
expect(duriel.noPer).toBe(true)
expect(duriel.isInside).toBe(true)
})
})