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