243 lines
8.5 KiB
TypeScript
243 lines
8.5 KiB
TypeScript
import { describe, expect, it } from 'vitest'
|
|
import {
|
|
ACT5_NIHLATHAK_WINGS,
|
|
ACT5_TEMPLE_HARDCODED_PRESETS_REMAPPING,
|
|
LVLPREST_ACT2_TOMB_TAINTED_SUN_X,
|
|
LVLPREST_ACT3_SEWER_NE,
|
|
LVLPREST_ACT3_SEWER_NW,
|
|
LVLPREST_ACT3_SEWER_SE,
|
|
LVLPREST_ACT3_SEWER_SW,
|
|
LVLPREST_ACT5_ICE_POOL_A,
|
|
LVLPREST_ACT5_ICE_POOL_B,
|
|
LVLPREST_ACT5_ICE_RIVER_A,
|
|
LVLPREST_ACT5_ICE_RIVER_B,
|
|
LVLPREST_ACT5_TEMPLE_FINAL_ROOM,
|
|
MAZE_DOOR_EAST,
|
|
MAZE_DOOR_NORTH,
|
|
MAZE_DOOR_SOUTH,
|
|
MAZE_DOOR_WEST,
|
|
mazeDoorMask,
|
|
mazePresetDefIdForDoorMask,
|
|
} from '../src/game/maze-special-passes.ts'
|
|
import {
|
|
DIRECTION_EAST,
|
|
DIRECTION_NORTH,
|
|
DIRECTION_SOUTH,
|
|
DIRECTION_WEST,
|
|
classifyMazePieceName,
|
|
generateMaze,
|
|
} from '../src/game/maze.ts'
|
|
import type { MazePiece, MazePieceKind, Room } from '../src/game/maze.ts'
|
|
import { Rng } from '../src/game/rng.ts'
|
|
import type { Ds1 } from '../src/formats/ds1.ts'
|
|
|
|
function mockDs1(width = 10, height = 10): Ds1 {
|
|
const cells = Array.from({ length: height }, () =>
|
|
Array.from({ length: width }, () => ({
|
|
floors: [{ prop1: 1, style: 1, sequence: 0, hidden: false, unknown1: 0, unknown2: 0 }],
|
|
walls: [],
|
|
shadows: [],
|
|
substitutions: [],
|
|
})),
|
|
)
|
|
return {
|
|
version: 18,
|
|
width,
|
|
height,
|
|
act: 1,
|
|
substitutionType: 0,
|
|
wallLayers: 1,
|
|
floorLayers: 1,
|
|
cells,
|
|
objects: [],
|
|
npcPathOffset: null,
|
|
}
|
|
}
|
|
|
|
function mockPiece(name: string, kind: MazePieceKind, sides = ''): MazePiece {
|
|
return { name, kind, sides, levels: [mockDs1()] }
|
|
}
|
|
|
|
function createStandardPieces(prefix: string, extra: MazePiece[] = []): MazePiece[] {
|
|
const SIDES = ['N', 'S', 'E', 'W', 'NE', 'NW', 'SE', 'SW', 'NS', 'EW', 'NSE', 'NSW', 'NEW', 'SEW', 'NSEW']
|
|
return [
|
|
mockPiece(`${prefix} Entrance`, 'entrance', ''),
|
|
...SIDES.map(s => mockPiece(`${prefix} ${s}`, 'room', s)),
|
|
...['N', 'S', 'E', 'W'].map(s => mockPiece(`${prefix} Prev ${s}`, 'prev', s)),
|
|
...['N', 'S', 'E', 'W'].map(s => mockPiece(`${prefix} Next ${s}`, 'next', s)),
|
|
...['N', 'S', 'E', 'W'].map(s => mockPiece(`${prefix} Down ${s}`, 'down', s)),
|
|
...['N', 'S', 'E', 'W'].map(s => mockPiece(`${prefix} Theme ${s}`, 'theme', s)),
|
|
...extra,
|
|
]
|
|
}
|
|
|
|
function makeRoom(id: number, orthDirs: number[]): Room {
|
|
const orths = orthDirs.map(direction => ({
|
|
room: { id: 900 + direction, x: 0, y: 0, width: 8, height: 8, orths: [], piece: null, variant: 0, hasDs1: false, rng: new Rng(1) },
|
|
direction,
|
|
}))
|
|
return {
|
|
id,
|
|
x: 0,
|
|
y: 0,
|
|
width: 8,
|
|
height: 8,
|
|
orths,
|
|
piece: null,
|
|
variant: 0,
|
|
hasDs1: false,
|
|
rng: new Rng(100 + id),
|
|
}
|
|
}
|
|
|
|
describe('Blizzard D2 1.13c Maze LvlPrest Def ID & Special Layout Passes (Issue #73)', () => {
|
|
it('computes 1.13c 4-bit door masks (W=1, E=2, S=4, N=8) and LvlPrest Def ID offsets', () => {
|
|
expect(MAZE_DOOR_WEST).toBe(1)
|
|
expect(MAZE_DOOR_EAST).toBe(2)
|
|
expect(MAZE_DOOR_SOUTH).toBe(4)
|
|
expect(MAZE_DOOR_NORTH).toBe(8)
|
|
|
|
const westOnly = makeRoom(1, [DIRECTION_WEST])
|
|
expect(mazeDoorMask(westOnly)).toBe(1)
|
|
expect(mazePresetDefIdForDoorMask(39, westOnly)).toBe(39)
|
|
|
|
const allFour = makeRoom(2, [DIRECTION_WEST, DIRECTION_EAST, DIRECTION_SOUTH, DIRECTION_NORTH])
|
|
expect(mazeDoorMask(allFour)).toBe(15)
|
|
expect(mazePresetDefIdForDoorMask(39, allFour)).toBe(53)
|
|
})
|
|
|
|
it('places Claw Viper Temple Level 2 fixed Tainted Sun Altar chamber (Def 633)', () => {
|
|
const pieces = createStandardPieces('Act 2 - Tomb', [
|
|
mockPiece('Act 2 - Tomb Tainted Sun X', 'quest', ''),
|
|
])
|
|
const result = generateMaze({
|
|
levelId: 61,
|
|
levelName: 'Act 2 - Tomb',
|
|
sectionSize: 10,
|
|
minRooms: 4,
|
|
merge: 0,
|
|
seed: 4242,
|
|
pieces,
|
|
})
|
|
|
|
const notes = result.stats.notes as string[]
|
|
const specials = result.stats.specialsApplied as { room: number; kind: string; sides: string }[]
|
|
expect(notes.some(n => n.includes(`lvlPrestDef=${String(LVLPREST_ACT2_TOMB_TAINTED_SUN_X)}`))).toBe(true)
|
|
expect(notes.some(n => n.includes('Act 2 - Tomb Tainted Sun X'))).toBe(true)
|
|
expect(specials.some(r => r.kind === 'quest')).toBe(true)
|
|
})
|
|
|
|
it('places Kurast Sewers Level 1 (levelId 92) 4 corner entrance rooms (Def 585..588) and Drain lever', () => {
|
|
const pieces = createStandardPieces('Act 3 - Sewer', [
|
|
mockPiece('Act 3 - Sewer SW', 'prev', 'S'),
|
|
mockPiece('Act 3 - Sewer SE', 'prev', 'E'),
|
|
mockPiece('Act 3 - Sewer NW', 'prev', 'W'),
|
|
mockPiece('Act 3 - Sewer NE', 'prev', 'N'),
|
|
...['N', 'E', 'S', 'W'].map(s => mockPiece(`Act 3 - Sewer Drain ${s}`, 'next', s)),
|
|
...['N', 'E', 'S', 'W'].map(s => mockPiece(`Act 3 - Sewer Chest ${s}`, 'treasure', s)),
|
|
])
|
|
const result = generateMaze({
|
|
levelId: 92,
|
|
levelName: 'Act 3 - Sewer',
|
|
sectionSize: 10,
|
|
minRooms: 10,
|
|
merge: 0,
|
|
seed: 7777,
|
|
pieces,
|
|
})
|
|
|
|
const notes = result.stats.notes as string[]
|
|
const specials = result.stats.specialsApplied as { room: number; kind: string; sides: string }[]
|
|
for (const defId of [LVLPREST_ACT3_SEWER_SW, LVLPREST_ACT3_SEWER_SE, LVLPREST_ACT3_SEWER_NW, LVLPREST_ACT3_SEWER_NE]) {
|
|
expect(notes.some(n => n.includes(`lvlPrestDef=${String(defId)}`))).toBe(true)
|
|
}
|
|
expect(notes.some(n => n.includes('Act 3 - Sewer Drain'))).toBe(true)
|
|
expect(specials.some(r => r.kind === 'next')).toBe(true)
|
|
})
|
|
|
|
it('rolls Frozen River A/B (Def 1030/1031) and places Ice Pools (Def 1032/1033) in Act 5 Ice Caves', () => {
|
|
const pieces = createStandardPieces('Act 5 - Ice Caves', [
|
|
mockPiece('Act 5 - Ice River A', 'quest', ''),
|
|
mockPiece('Act 5 - Ice River B', 'quest', ''),
|
|
mockPiece('Act 5 - Ice Pool A', 'quest', ''),
|
|
mockPiece('Act 5 - Ice Pool B', 'quest', ''),
|
|
])
|
|
|
|
const chosenDefs = new Set<number>()
|
|
for (let seed = 1; seed <= 12; seed += 1) {
|
|
const res = generateMaze({
|
|
levelId: 114,
|
|
levelName: 'Act 5 - Ice Caves',
|
|
sectionSize: 10,
|
|
minRooms: 4,
|
|
merge: 0,
|
|
seed: seed * 313,
|
|
pieces,
|
|
})
|
|
const notes = res.stats.notes as string[]
|
|
if (notes.some(n => n.includes(`lvlPrestDef=${String(LVLPREST_ACT5_ICE_RIVER_A)}`))) {
|
|
chosenDefs.add(LVLPREST_ACT5_ICE_RIVER_A)
|
|
}
|
|
if (notes.some(n => n.includes(`lvlPrestDef=${String(LVLPREST_ACT5_ICE_RIVER_B)}`))) {
|
|
chosenDefs.add(LVLPREST_ACT5_ICE_RIVER_B)
|
|
}
|
|
}
|
|
expect(chosenDefs.has(LVLPREST_ACT5_ICE_RIVER_A)).toBe(true)
|
|
expect(chosenDefs.has(LVLPREST_ACT5_ICE_RIVER_B)).toBe(true)
|
|
|
|
const res116 = generateMaze({
|
|
levelId: 116,
|
|
levelName: 'Act 5 - Ice Caves',
|
|
sectionSize: 10,
|
|
minRooms: 4,
|
|
merge: 0,
|
|
seed: 100,
|
|
pieces,
|
|
})
|
|
expect((res116.stats.notes as string[]).some(n => n.includes(`lvlPrestDef=${String(LVLPREST_ACT5_ICE_POOL_A)}`))).toBe(true)
|
|
|
|
const res119 = generateMaze({
|
|
levelId: 119,
|
|
levelName: 'Act 5 - Ice Caves',
|
|
sectionSize: 10,
|
|
minRooms: 4,
|
|
merge: 0,
|
|
seed: 100,
|
|
pieces,
|
|
})
|
|
expect((res119.stats.notes as string[]).some(n => n.includes(`lvlPrestDef=${String(LVLPREST_ACT5_ICE_POOL_B)}`))).toBe(true)
|
|
})
|
|
|
|
it('applies Act 5 Temple hardcoded remapping (Def 1044..1047) and rolls Nihlathak active wing (Def 1060..1063)', () => {
|
|
expect(ACT5_TEMPLE_HARDCODED_PRESETS_REMAPPING).toEqual(['SW', 'SE', 'NW', 'NE'])
|
|
expect(ACT5_NIHLATHAK_WINGS).toEqual(['NW', 'NE', 'SE', 'SW'])
|
|
|
|
const pieces = createStandardPieces('Act 5 - Temple', [
|
|
mockPiece('Act 5 - Temple Final Room', 'quest', ''),
|
|
mockPiece('Act 5 - Temple NE Down', 'down', 'NE'),
|
|
mockPiece('Act 5 - Temple NW Down', 'down', 'NW'),
|
|
mockPiece('Act 5 - Temple SW Down', 'down', 'SW'),
|
|
mockPiece('Act 5 - Temple SE up', 'prev', 'SE'),
|
|
])
|
|
|
|
const res124 = generateMaze({
|
|
levelId: 124,
|
|
levelName: 'Act 5 - Temple',
|
|
sectionSize: 10,
|
|
minRooms: 4,
|
|
merge: 0,
|
|
seed: 8888,
|
|
pieces,
|
|
})
|
|
const notes124 = res124.stats.notes as string[]
|
|
expect(notes124.some(n => n.includes(`lvlPrestDef=${String(LVLPREST_ACT5_TEMPLE_FINAL_ROOM)}`))).toBe(true)
|
|
expect(notes124.some(n => n.includes('nihlathakWing='))).toBe(true)
|
|
})
|
|
|
|
it('correctly classifies Act 4 - Lava X and standard directional rooms', () => {
|
|
expect(classifyMazePieceName('Act 4 - Lava X', 'Act 4 - Lava')).toEqual({ kind: 'room', sides: '' })
|
|
expect(classifyMazePieceName('Act 4 - Lava W', 'Act 4 - Lava')).toEqual({ kind: 'room', sides: 'W' })
|
|
expect(classifyMazePieceName('Act 4 - Lava NSEW', 'Act 4 - Lava')).toEqual({ kind: 'room', sides: 'NSEW' })
|
|
})
|
|
})
|