diablo2-web/tests/maze-113c.test.ts

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' })
})
})