163 lines
6.0 KiB
TypeScript
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)
|
|
})
|
|
})
|