198 lines
6.8 KiB
TypeScript
198 lines
6.8 KiB
TypeScript
import { describe, expect, it } from 'vitest'
|
|
import * as fs from 'node:fs'
|
|
import {
|
|
loadActTables,
|
|
resolveLevel,
|
|
resolveLevelLibraries,
|
|
UNIVERSAL_DT1S,
|
|
} from '../src/game/acts.ts'
|
|
import type { ActTables, D2Table } from '../src/game/acts.ts'
|
|
import { buildIsoMapScene } from '../src/game/d2map.ts'
|
|
import type { Ds1, Ds1Cell } from '../src/formats/ds1.ts'
|
|
import { decodeDt1 } from '../src/formats/dt1.ts'
|
|
import type { Dt1, Dt1Tile } from '../src/formats/dt1.ts'
|
|
import { MpqArchive } from '../src/mpq/archive.ts'
|
|
import { fileSource } from '../src/mpq/file-source.ts'
|
|
import { MountedArchives } from '../src/mpq/mount.ts'
|
|
|
|
function createMockTables(): ActTables {
|
|
const emptyTable: D2Table = { header: [], rows: [] }
|
|
const levels: D2Table = {
|
|
header: ['Id', 'Name', 'Pal', 'LevelType', 'DrlgType', 'SizeX', 'SizeY', 'Act', 'Waypoint', 'Position', 'Portal'],
|
|
rows: [
|
|
['1', 'Act 1 - Town', '0', '1', '2', '56', '40', '0', '255', '0', '0'],
|
|
['79', 'Act 3 - Kurast 1', '2', '22', '3', '100', '100', '2', '79', '0', '0'],
|
|
['125', 'Act 5 - Hell 1', '4', '35', '1', '80', '80', '4', '255', '0', '0'],
|
|
['135', 'Act 5 - Pandemonium 3', '4', '35', '1', '80', '80', '4', '255', '0', '0'],
|
|
],
|
|
}
|
|
const lvltypes: D2Table = {
|
|
header: ['Id', 'Name', 'File 1', 'File 2'],
|
|
rows: [
|
|
['1', 'Act 1 - Town', 'Act1/Town/Floor.dt1', 'Act1/Town/Walls.dt1'],
|
|
['22', 'Act 3 - Jungle', 'Act3/Jungle/Floor.dt1', 'Act3/Jungle/Tree.dt1'],
|
|
['35', 'Act 5 - Lava', 'Act4/Lava/Floor.dt1', 'Act4/Lava/Walls.dt1'],
|
|
],
|
|
}
|
|
const lvlprest: D2Table = {
|
|
header: ['Def', 'LevelId', 'File1', 'Dt1Mask', 'Animate'],
|
|
rows: [
|
|
['1', '1', 'TownN1.ds1', '3', '80'],
|
|
],
|
|
}
|
|
return {
|
|
levels,
|
|
lvltypes,
|
|
lvlprest,
|
|
monstats: emptyTable,
|
|
monpreset: emptyTable,
|
|
monstats2: emptyTable,
|
|
monlvl: emptyTable,
|
|
montype: emptyTable,
|
|
monumod: emptyTable,
|
|
superuniques: emptyTable,
|
|
lvlwarp: emptyTable,
|
|
}
|
|
}
|
|
|
|
function makeSingleTileDs1(style: number, sequence: number, prop1 = 1): Ds1 {
|
|
const width = 35
|
|
const height = 25
|
|
const cells: Ds1Cell[][] = []
|
|
for (let y = 0; y < height; y += 1) {
|
|
const row: Ds1Cell[] = []
|
|
for (let x = 0; x < width; x += 1) {
|
|
if (x === 30 && y === 18) {
|
|
row.push({
|
|
walls: [],
|
|
floors: [{ prop1, style, sequence, unknown1: 0, unknown2: 0, hidden: false }],
|
|
shadows: [],
|
|
substitutions: [],
|
|
})
|
|
} else {
|
|
row.push({ walls: [], floors: [], shadows: [], substitutions: [] })
|
|
}
|
|
}
|
|
cells.push(row)
|
|
}
|
|
return {
|
|
version: 18,
|
|
width,
|
|
height,
|
|
act: 5,
|
|
substitutionType: 0,
|
|
wallLayers: 0,
|
|
floorLayers: 1,
|
|
cells,
|
|
objects: [],
|
|
npcPathOffset: null,
|
|
}
|
|
}
|
|
|
|
function createMockInvisWalDt1(): Dt1 {
|
|
const dummyFlags = Array.from({ length: 25 }, () => ({
|
|
blockWalk: true,
|
|
blockLos: false,
|
|
blockJump: false,
|
|
blockPlayerWalk: false,
|
|
blockLight: false,
|
|
raw: 1,
|
|
}))
|
|
const mockTile: Dt1Tile = {
|
|
direction: 0,
|
|
height: -80,
|
|
width: 160,
|
|
type: 0, // Floor
|
|
style: 49,
|
|
sequence: 7,
|
|
materialFlags: 0,
|
|
roofHeight: 0,
|
|
animated: 0,
|
|
rarityFrameIndex: 0,
|
|
subTileFlags: dummyFlags,
|
|
minBlockY: 0,
|
|
bitmapHeight: 80,
|
|
blocks: [],
|
|
}
|
|
return {
|
|
versionMajor: 7,
|
|
versionMinor: 6,
|
|
warnings: [],
|
|
tiles: [mockTile],
|
|
}
|
|
}
|
|
|
|
describe('Blizzard 1.13c DRLGROOM_LoadDt1Files Universal DT1s (Issue #112)', () => {
|
|
it('exports UNIVERSAL_DT1S with exact Blizzard paths and casing', () => {
|
|
expect(UNIVERSAL_DT1S).toEqual([
|
|
'data\\global\\tiles\\Act1\\Outdoors\\Blank.dt1',
|
|
'data\\global\\tiles\\Act1\\Barracks\\InvisWal.dt1',
|
|
'data\\global\\tiles\\Act1\\Barracks\\Warp.dt1',
|
|
])
|
|
})
|
|
|
|
it('unconditionally appends universal DT1s in resolveLevelLibraries for all test levels', () => {
|
|
const mockTables = createMockTables()
|
|
for (const levelId of [1, 79, 125, 135]) {
|
|
const { dt1Names } = resolveLevelLibraries(mockTables, levelId)
|
|
expect(dt1Names).toContain('data\\global\\tiles\\Act1\\Outdoors\\Blank.dt1')
|
|
expect(dt1Names).toContain('data\\global\\tiles\\Act1\\Barracks\\InvisWal.dt1')
|
|
expect(dt1Names).toContain('data\\global\\tiles\\Act1\\Barracks\\Warp.dt1')
|
|
}
|
|
})
|
|
|
|
it('unconditionally appends universal DT1s in resolveLevel', () => {
|
|
const mockTables = createMockTables()
|
|
const levelInfo = resolveLevel(mockTables, 1)
|
|
expect(levelInfo.dt1Names).toContain('data\\global\\tiles\\Act1\\Outdoors\\Blank.dt1')
|
|
expect(levelInfo.dt1Names).toContain('data\\global\\tiles\\Act1\\Barracks\\InvisWal.dt1')
|
|
expect(levelInfo.dt1Names).toContain('data\\global\\tiles\\Act1\\Barracks\\Warp.dt1')
|
|
})
|
|
|
|
it('recognizes floor style=49 sequence=7 when InvisWal.dt1 is in the pool and flags missing without it', () => {
|
|
const ds1 = makeSingleTileDs1(49, 7)
|
|
const sceneWithout = buildIsoMapScene(ds1, [])
|
|
expect(sceneWithout.missingTiles).toBe(1)
|
|
expect(sceneWithout.missingRefs[0]).toContain('floor style=49 sequence=7:0 @cell(30,18)')
|
|
|
|
const mockDt1 = createMockInvisWalDt1()
|
|
const sceneWith = buildIsoMapScene(ds1, [mockDt1])
|
|
expect(sceneWith.missingTiles).toBe(0)
|
|
expect(sceneWith.floors).toHaveLength(1)
|
|
expect(sceneWith.floors[0]!.cellX).toBe(30)
|
|
expect(sceneWith.floors[0]!.cellY).toBe(18)
|
|
})
|
|
|
|
const hasRealData = fs.existsSync('samples/d2/d2data.mpq')
|
|
it.runIf(hasRealData)('verifies real MPQ tables and InvisWal.dt1 decoding on levels 1, 79, 125, 135', async () => {
|
|
const archives = new MountedArchives()
|
|
for (const name of ['d2data.mpq', 'd2exp.mpq', 'Patch_D2.mpq']) {
|
|
try {
|
|
archives.add(name, await MpqArchive.open(await fileSource(`samples/d2/${name}`)))
|
|
} catch {}
|
|
}
|
|
const realTables = await loadActTables(archives)
|
|
|
|
for (const levelId of [1, 79, 125, 135]) {
|
|
const { dt1Names } = resolveLevelLibraries(realTables, levelId)
|
|
expect(dt1Names).toContain('data\\global\\tiles\\Act1\\Outdoors\\Blank.dt1')
|
|
expect(dt1Names).toContain('data\\global\\tiles\\Act1\\Barracks\\InvisWal.dt1')
|
|
expect(dt1Names).toContain('data\\global\\tiles\\Act1\\Barracks\\Warp.dt1')
|
|
}
|
|
|
|
// Decode real InvisWal.dt1 from MPQ and ensure style=49 sequence=7 floor tile is present
|
|
const invisWalBytes = await archives.read('data\\global\\tiles\\Act1\\Barracks\\InvisWal.dt1')
|
|
const realInvisWal = decodeDt1(invisWalBytes)
|
|
const match = realInvisWal.tiles.find(t => t.type === 0 && t.style === 49 && t.sequence === 7)
|
|
expect(match).toBeDefined()
|
|
expect(match?.style).toBe(49)
|
|
expect(match?.sequence).toBe(7)
|
|
|
|
// Build scene with real InvisWal.dt1
|
|
const ds1 = makeSingleTileDs1(49, 7)
|
|
const scene = buildIsoMapScene(ds1, [realInvisWal])
|
|
expect(scene.missingTiles).toBe(0)
|
|
expect(scene.floors).toHaveLength(1)
|
|
})
|
|
})
|