diablo2-web/src/game/maze-special-passes.ts

1355 lines
47 KiB
TypeScript

/**
* Diablo II DRLG (Dungeon Random Level Generator) Maze Special Passes.
*
* Implements the 11 official Blizzard / D2MOO core dungeon special passes:
* - `DRLGMAZE_PlaceAct2TombStuff`: Tal Rasha's Tombs (True tomb Horadric staff receptacle, 6 false tombs golden chests, Kaa, Cube, Leatherarm).
* - `DRLGMAZE_PlaceAct2LairStuff`: Maggot Lair Level 3 (Coldworm the Burrower & Staff of Kings chest).
* - `DRLGMAZE_PlaceAct3DungeonStuff`: Flayer Dungeon (Brain chest) & Ruined Temple (Lam Esen's Tome altar).
* - `DRLGMAZE_PlaceAct3SewerStuff`: Kurast Sewers (drain lever & descent to Level 2).
* - `DRLGMAZE_PlaceAct3MephistoStuff`: Durance of Hate Level 3 (blood moat, council hall, red portal to Act 4).
* - `DRLGMAZE_PlaceAct5TempleStuff`: Halls of Vaught (4-way branch & Nihlathak altar) & Halls of Pain.
* - `DRLGMAZE_PlaceAct5BaalStuff`: Throne of Destruction & Worldstone Keep.
* - `DRLGMAZE_PlaceAct5IceStuff`: Frozen River (Anya frozen platform) & Glacial Trail.
* - `DRLGMAZE_PlaceAct1Barracks`: Monastery Barracks (Smith & Charsi's Malus forge/anvil).
* - `DRLGMAZE_PlaceAct4Lava`: River of Flame (Hephasto the Armorer & Hellforge, Bridge connecting to Chaos Sanctuary).
* - `DRLGMAZE_ScanReplaceSpecialAct2SewersPresets`: Lut Gholein Sewers (Radament's Lair & Atma's quest).
*/
import { Rng } from './rng.ts'
import {
DIRECTION_WEST,
DIRECTION_NORTH,
DIRECTION_EAST,
DIRECTION_SOUTH,
sideMaskFromToken,
sideTokenFromMask,
type MazeLevel,
type Room,
type MazePiece,
type MazePieceKind,
type MazeRequest,
type MazeStats,
type PieceIndex,
bfsDistances,
addAdjacentMazeRoom,
noteSpecial,
findSpecial,
roomSideMask,
fillBlankSpaces,
} from './maze.ts'
export type SpecialPassName =
| 'DRLGMAZE_PlaceAct2TombStuff'
| 'DRLGMAZE_PlaceAct2LairStuff'
| 'DRLGMAZE_PlaceAct3DungeonStuff'
| 'DRLGMAZE_PlaceAct3SewerStuff'
| 'DRLGMAZE_PlaceAct3MephistoStuff'
| 'DRLGMAZE_PlaceAct5TempleStuff'
| 'DRLGMAZE_PlaceAct5BaalStuff'
| 'DRLGMAZE_PlaceAct5IceStuff'
| 'DRLGMAZE_PlaceAct1Barracks'
| 'DRLGMAZE_PlaceAct4Lava'
| 'DRLGMAZE_ScanReplaceSpecialAct2SewersPresets'
export interface SpecialPassContext {
readonly level: MazeLevel
readonly request: MazeRequest
readonly pieces: readonly MazePiece[]
readonly index: PieceIndex
readonly stats: MazeStats
readonly rng?: Rng
}
function getPassRng(ctx: SpecialPassContext): Rng {
return ctx.rng ?? ctx.level?.rng ?? new Rng(ctx.request.seed)
}
const DIRS = ['N', 'E', 'S', 'W'] as const
/**
* Blizzard D2 1.13c `DrlgMaze.cpp` 4-bit Room Door Bitmask (`West=1, East=2, South=4, North=8`).
* In 1.13c `DRLGMAZE_GetRoomPresetId`, a room's 15 non-empty door topologies map directly to
* `baseDefId + (doorMask - 1)` in `LvlPrest.txt`.
*/
export const MAZE_DOOR_WEST = 0x01
export const MAZE_DOOR_EAST = 0x02
export const MAZE_DOOR_SOUTH = 0x04
export const MAZE_DOOR_NORTH = 0x08
/**
* Compute the canonical 1.13c `DrlgMaze.cpp` 4-bit door mask (`1..15`) for a maze room.
*/
export function mazeDoorMask(room: Room): number {
const mask = roomSideMask(room)
let d2Mask = 0
if ((mask & (1 << DIRECTION_WEST)) !== 0) d2Mask |= MAZE_DOOR_WEST
if ((mask & (1 << DIRECTION_EAST)) !== 0) d2Mask |= MAZE_DOOR_EAST
if ((mask & (1 << DIRECTION_SOUTH)) !== 0) d2Mask |= MAZE_DOOR_SOUTH
if ((mask & (1 << DIRECTION_NORTH)) !== 0) d2Mask |= MAZE_DOOR_NORTH
return d2Mask
}
/**
* Compute the exact `LvlPrest.txt` `Def` ID for a maze room given a base 15-room table `Def` ID
* (`baseDefId + (mazeDoorMask(room) - 1)`).
*/
export function mazePresetDefIdForDoorMask(baseDefId: number, room: Room): number {
const doorMask = mazeDoorMask(room)
if (doorMask <= 0 || doorMask > 15) return baseDefId
return baseDefId + (doorMask - 1)
}
/**
* Canonical Blizzard D2 1.13c `LvlPrest.txt` `Def` IDs used by `DrlgMaze.cpp` special passes.
*/
export const LVLPREST_ACT2_TOMB_TALRASHA_BASE = 625
export const LVLPREST_ACT2_TOMB_KAA_BASE = 629
export const LVLPREST_ACT2_TOMB_TAINTED_SUN_X = 633
export const LVLPREST_ACT3_SEWER_SW = 585
export const LVLPREST_ACT3_SEWER_SE = 586
export const LVLPREST_ACT3_SEWER_NW = 587
export const LVLPREST_ACT3_SEWER_NE = 588
export const LVLPREST_ACT3_SEWER_DRAIN_BASE = 589
export const LVLPREST_ACT3_SEWER_CHEST_BASE = 593
export const LVLPREST_ACT5_ICE_PREV_BASE = 1018
export const LVLPREST_ACT5_ICE_WAYPOINT_BASE = 1026
export const LVLPREST_ACT5_ICE_RIVER_A = 1030
export const LVLPREST_ACT5_ICE_RIVER_B = 1031
export const LVLPREST_ACT5_ICE_POOL_A = 1032
export const LVLPREST_ACT5_ICE_POOL_B = 1033
export const LVLPREST_ACT5_TEMPLE_REMAPPING_BASE = 1044
export const LVLPREST_ACT5_TEMPLE_FINAL_ROOM = 1059
export const LVLPREST_ACT5_TEMPLE_NIHLATHAK_WING_BASE = 1060
export const LVLPREST_ACT2_CORRUPT_HAREM_NE = 354
export const LVLPREST_ACT2_CORRUPT_HAREM_SE = 355
export const LVLPREST_ACT2_CORRUPT_HAREM_SW = 356
export const LVLPREST_ACT2_CORRUPT_HAREM_NW = 357
export const LVLPREST_ACT2_BASEMENT_NE = 358
export const LVLPREST_ACT2_BASEMENT_SE = 359
export const LVLPREST_ACT2_BASEMENT_SW = 360
export const LVLPREST_ACT2_BASEMENT_NW = 361
export const LVLPREST_ACT3_SPIDER_SW = 659
export const LVLPREST_ACT3_SPIDER_SE = 660
export const LVLPREST_ACT3_SPIDER_NW = 661
export const LVLPREST_ACT3_SPIDER_NE = 662
export const LVLPREST_ACT3_SPIDER_CHEST_NW = 663
export const LVLPREST_ACT3_SPIDER_CHEST_NE = 664
export const LVLPREST_ACT5_LAVA_N = 1053
export const LVLPREST_ACT5_LAVA_S = 1054
export const LVLPREST_ACT5_LAVA_E = 1055
export const LVLPREST_ACT5_LAVA_W = 1056
export const LVLPREST_ACT5_LAVA_EW = 1057
export const LVLPREST_ACT5_LAVA_NS = 1058
/**
* 1.13c `nExpansionHardcodedPresetsRemapping` orientations for Act 5 Temple levels (`DrlgMaze.cpp`).
*/
export const ACT5_TEMPLE_HARDCODED_PRESETS_REMAPPING = ['SW', 'SE', 'NW', 'NE'] as const
export const ACT5_NIHLATHAK_WINGS = ['NW', 'NE', 'SE', 'SW'] as const
/**
* Canonical mapping from 1.13c `LvlPrest.txt` `Def` IDs to `LvlPrest` piece names and roles.
*/
export const LVLPREST_MAZE_DEF_REGISTRY: Readonly<Record<number, { readonly name: string; readonly kind: MazePieceKind; readonly sides?: string }>> = {
[LVLPREST_ACT3_SEWER_SW]: { name: 'Act 3 - Sewer SW', kind: 'prev', sides: 'S' },
[LVLPREST_ACT3_SEWER_SE]: { name: 'Act 3 - Sewer SE', kind: 'prev', sides: 'E' },
[LVLPREST_ACT3_SEWER_NW]: { name: 'Act 3 - Sewer NW', kind: 'prev', sides: 'W' },
[LVLPREST_ACT3_SEWER_NE]: { name: 'Act 3 - Sewer NE', kind: 'prev', sides: 'N' },
[LVLPREST_ACT2_TOMB_TAINTED_SUN_X]: { name: 'Act 2 - Tomb Tainted Sun X', kind: 'quest' },
[LVLPREST_ACT5_ICE_RIVER_A]: { name: 'Act 5 - Ice River A', kind: 'quest' },
[LVLPREST_ACT5_ICE_RIVER_B]: { name: 'Act 5 - Ice River B', kind: 'quest' },
[LVLPREST_ACT5_ICE_POOL_A]: { name: 'Act 5 - Ice Pool A', kind: 'quest' },
[LVLPREST_ACT5_ICE_POOL_B]: { name: 'Act 5 - Ice Pool B', kind: 'quest' },
[LVLPREST_ACT5_TEMPLE_FINAL_ROOM]: { name: 'Act 5 - Temple Final Room', kind: 'quest' },
[LVLPREST_ACT3_SPIDER_CHEST_NW]: { name: 'Act 3 - Spider Chest NW', kind: 'treasure', sides: 'NW' },
[LVLPREST_ACT3_SPIDER_CHEST_NE]: { name: 'Act 3 - Spider Chest NE', kind: 'treasure', sides: 'NE' },
}
/**
* Place or replace a maze room by its 1.13c `LvlPrest.txt` numeric `Def` ID.
*/
export function replaceRoomByPresetDefId(
ctx: SpecialPassContext,
defId: number,
options?: Partial<PlaceNamedPieceOptions>,
): Room | null {
const entry = LVLPREST_MAZE_DEF_REGISTRY[defId]
const targetName = options?.name ?? entry?.name
const targetKind = options?.kind ?? entry?.kind ?? 'quest'
const placed = placeNamedPiece(ctx, {
kind: targetKind,
...(targetName !== undefined ? { name: targetName } : {}),
...(options?.namePrefix !== undefined ? { namePrefix: options.namePrefix } : {}),
...(options?.sides !== undefined ? { sides: options.sides } : entry?.sides !== undefined ? { sides: entry.sides } : {}),
...(options?.plainSides !== undefined ? { plainSides: options.plainSides } : {}),
...(options?.direction !== undefined ? { direction: options.direction } : {}),
...(options?.inPlaceOnly !== undefined ? { inPlaceOnly: options.inPlaceOnly } : {}),
...(options?.preferFurthest !== undefined ? { preferFurthest: options.preferFurthest } : {}),
})
if (placed !== null) {
ctx.stats.notes.push(`lvlPrestDef=${String(defId)} (${placed.piece?.name ?? targetName ?? 'special'})`)
}
return placed
}
export interface PlaceNamedPieceOptions {
readonly kind: MazePieceKind
readonly name?: string
readonly namePrefix?: string
readonly sides?: string
readonly plainSides?: string
readonly direction?: number
readonly inPlaceOnly?: boolean
readonly preferFurthest?: boolean
}
/**
* Locate and place a special piece into the maze level.
*
* Tries in-place replacement first (preferring rooms matching `plainSides` and optimal BFS distance),
* then falls back to growing a new dead-end room off an existing room if allowed.
*/
export function placeNamedPiece(
ctx: SpecialPassContext,
options: PlaceNamedPieceOptions,
): Room | null {
const { kind, name, namePrefix, sides, plainSides, inPlaceOnly = false } = options
if (kind === 'waypoint' && ctx.request.hasWaypoint === false) {
return null
}
const isExit = options.preferFurthest ?? (kind === 'next' || kind === 'down' || kind === 'quest' || kind === 'treasure' || kind === 'waypoint')
// 1. Find the target piece in ctx.pieces
let piece: MazePiece | undefined
if (name !== undefined) {
piece = ctx.pieces.find(p => p.name === name)
?? ctx.pieces.find(p => p.name.toLowerCase() === name.toLowerCase())
}
if (piece === undefined && namePrefix !== undefined) {
if (sides !== undefined && sides !== '') {
const fullName = `${namePrefix} ${sides}`
piece = ctx.pieces.find(p => p.name === fullName)
?? ctx.pieces.find(p => p.name.toLowerCase() === fullName.toLowerCase())
}
if (piece === undefined) {
piece = ctx.pieces.find(p => p.name.startsWith(namePrefix))
}
}
if (piece === undefined && sides !== undefined && sides !== '') {
piece = findSpecial(ctx.pieces, kind, sides)
}
if (piece === undefined) {
piece = ctx.pieces.find(p => p.kind === kind && (sides === undefined || sides === '' || p.sides === sides))
}
if (piece === undefined) {
const missingIdentifier = name ?? (namePrefix !== undefined ? `${namePrefix} ${sides ?? ''}`.trim() : `${kind} ${sides ?? ''}`.trim())
ctx.stats.unresolvedRoles.push(missingIdentifier)
ctx.stats.notes.push(`special piece not found: ${missingIdentifier}`)
return null
}
const token = sides ?? piece.sides ?? ''
const distMap = bfsDistances(ctx.level)
// 2. Phase 1: In-place replacement
const wantedMask = plainSides !== undefined && plainSides !== '' ? sideMaskFromToken(plainSides) : null
const candidates = ctx.level.rooms.filter(r => {
if (r.hasDs1) return false
if (wantedMask !== null && roomSideMask(r) !== wantedMask) return false
return true
})
if (candidates.length > 0) {
candidates.sort((a, b) => {
const da = distMap.get(a) ?? 0
const db = distMap.get(b) ?? 0
return isExit ? db - da : da - db
})
const chosen = candidates[0]!
chosen.piece = piece
chosen.variant = 0
chosen.hasDs1 = true
noteSpecial(chosen, piece, token !== '' ? token : sideTokenFromMask(roomSideMask(chosen)), ctx.stats)
return chosen
}
if (inPlaceOnly) {
ctx.stats.notes.push(`no matching room for in-place special piece: ${piece.name}`)
return null
}
// 3. Phase 2: Grow an adjacent room
const parents = ctx.level.rooms.filter(r => !r.hasDs1 || r.piece?.kind === 'theme')
parents.sort((a, b) => {
const da = distMap.get(a) ?? 0
const db = distMap.get(b) ?? 0
return isExit ? db - da : da - db
})
const primaryDir = options.direction ?? DIRECTION_NORTH
const tryDirs = [primaryDir, (primaryDir + 1) % 4, (primaryDir + 2) % 4, (primaryDir + 3) % 4]
for (const parent of parents) {
for (const dir of tryDirs) {
const wasHasDs1 = parent.hasDs1
parent.hasDs1 = false
const created = addAdjacentMazeRoom(ctx.level, parent, dir, false, true, ctx.index, ctx.stats)
if (created !== null) {
created.piece = piece
created.variant = 0
created.hasDs1 = true
noteSpecial(created, piece, token, ctx.stats)
return created
}
parent.hasDs1 = wasHasDs1
}
}
ctx.stats.notes.push(`could not place special piece ${piece.name}: no adjacent space clear`)
return null
}
/* ------------------------------------------------------------------------- *
* 1. Act 2 - Tomb
* ------------------------------------------------------------------------- */
/**
* Tal Rasha's Tombs, Stony Tomb, Halls of the Dead, Claw Viper Temple.
*
* Places True Tomb Horadric Staff orifice, False Tomb golden chest rooms, Ancient Kaa the Soulless,
* Horadric Cube chest, Leatherarm, and staircases.
*/
export function placeAct2TombStuff(ctx: SpecialPassContext): void {
const rng = getPassRng(ctx)
const levelId = ctx.request.levelId
const staffTomb = ctx.request.staffTombLevelId ?? 66
const bossTomb = ctx.request.bossTombLevelId
// Determine initial entrance direction from warp room
let nDirection = 0
const warpRoom = ctx.level.rooms.find(r => r.piece?.kind === 'prev')
if (warpRoom?.piece !== null && warpRoom?.piece !== undefined) {
const name = warpRoom.piece.name.toUpperCase()
if (name.includes('NSW')) nDirection = 0
else if (name.includes('NEW')) nDirection = 1
else if (name.includes('NSE')) nDirection = 2
else if (name.includes('SEW')) nDirection = 3
else nDirection = rng.int(0, 3)
} else {
nDirection = rng.int(0, 3)
}
const GROW_DIRS = [DIRECTION_SOUTH, DIRECTION_WEST, DIRECTION_NORTH, DIRECTION_EAST]
const placeTombPiece = (kind: MazePieceKind, role: string) => {
const side = DIRS[nDirection]!
const growDir = GROW_DIRS[nDirection]!
placeNamedPiece(ctx, {
kind,
namePrefix: `Act 2 - Tomb ${role}`,
sides: side,
plainSides: side,
direction: growDir,
preferFurthest: role !== 'Prev',
})
nDirection = (nDirection + 1) % 4
}
// 0. Claw Viper Temple Level 2 (levelId === 61): 1.13c fixed Tainted Sun Altar chamber (Def 633)
if (levelId === 61) {
const altarCandidates = ['Act 2 - Tomb Tainted Sun X', 'Act 2 - Tomb Sanctuary', 'Act 2 - Tomb Talrasha']
if (ctx.level.rooms.length === 1 && ctx.level.first !== null) {
const singlePiece = ctx.pieces.find(p => altarCandidates.some(c => p.name.startsWith(c)))
if (singlePiece !== undefined) {
ctx.level.first.piece = singlePiece
ctx.level.first.hasDs1 = true
noteSpecial(ctx.level.first, singlePiece, '', ctx.stats)
ctx.stats.notes.push(`lvlPrestDef=${String(LVLPREST_ACT2_TOMB_TAINTED_SUN_X)} (${singlePiece.name})`)
return
}
}
const matchedAltarPiece = ctx.pieces.find(p => altarCandidates.some(c => p.name === c || p.name.startsWith(c)))
const placedAltar = replaceRoomByPresetDefId(ctx, LVLPREST_ACT2_TOMB_TAINTED_SUN_X, {
kind: 'quest',
...(matchedAltarPiece !== undefined ? { name: matchedAltarPiece.name } : { name: 'Act 2 - Tomb Tainted Sun X' }),
preferFurthest: true,
})
if (placedAltar === null) {
placeTombPiece('quest', 'Talrasha')
}
if (
ctx.level.rooms.length > 1 &&
!ctx.level.rooms.some(r => r.piece?.kind === 'prev') &&
ctx.pieces.some(p => p.name.startsWith('Act 2 - Tomb Prev'))
) {
placeTombPiece('prev', 'Prev')
}
return
}
// 1. Stony Tomb 1, Stony Tomb 2, Halls of the Dead 1, Claw Viper Temple 1 get Next stairs
if (levelId >= 55 && levelId <= 58) {
placeTombPiece('next', 'Next')
}
// 2. Halls of the Dead 2 gets Waypoint
if (levelId === 57 || levelId === 59 || ctx.request.hasWaypoint === true) {
placeTombPiece('waypoint', 'Waypoint')
}
// 3. Stony Tomb 2 gets golden chest
if (levelId === 56) {
placeTombPiece('treasure', 'Chest')
}
// 4. False Tal Rasha Tombs (66..72 except staff tomb) get golden chest
if (levelId >= 66 && levelId <= 72 && levelId !== staffTomb) {
placeTombPiece('treasure', 'Chest')
}
// 5. Stony Tomb 2 gets Leatherarm
if (levelId === 56) {
placeTombPiece('quest', 'Leatherarm')
}
// 6. Halls of the Dead 3 gets Horadric Cube
if (levelId === 60) {
placeTombPiece('quest', 'Cube')
}
// 7. Stony Tomb 2 gets Treasure
if (levelId === 56) {
placeTombPiece('treasure', 'Treasure')
}
// 8. True Tal Rasha Tomb gets Horadric Staff Orifice (Def 625..628)
if (levelId === staffTomb) {
const talrashaDef = LVLPREST_ACT2_TOMB_TALRASHA_BASE + (nDirection % 4)
ctx.stats.notes.push(`lvlPrestDef=${String(talrashaDef)} (Act 2 - Tomb Talrasha)`)
placeTombPiece('quest', 'Talrasha')
}
// 9. Boss Tomb gets Ancient Kaa the Soulless (Def 629..632)
if (bossTomb !== undefined && levelId === bossTomb) {
const kaaDef = LVLPREST_ACT2_TOMB_KAA_BASE + (nDirection % 4)
ctx.stats.notes.push(`lvlPrestDef=${String(kaaDef)} (Act 2 - Tomb Kaa)`)
placeTombPiece('quest', 'Kaa')
}
}
/* ------------------------------------------------------------------------- *
* 2. Act 2 - Lair
* ------------------------------------------------------------------------- */
/**
* Maggot Lair Level 1, 2, 3.
*
* Level 3 places Coldworm the Burrower (Tight Spot S) and the Staff of Kings chest (Treasure W).
*/
export function placeAct2LairStuff(ctx: SpecialPassContext): void {
const rng = getPassRng(ctx)
const levelId = ctx.request.levelId
if (levelId === 64) {
// Coldworm the Burrower room
placeNamedPiece(ctx, {
kind: 'quest',
name: 'Act 2 - Lair Tight Spot S',
plainSides: 'S',
direction: DIRECTION_NORTH,
preferFurthest: true,
})
// Staff of Kings chest room
placeNamedPiece(ctx, {
kind: 'treasure',
name: 'Act 2 - Lair Treasure W',
plainSides: 'W',
direction: DIRECTION_EAST,
preferFurthest: true,
})
// Up staircase
placeNamedPiece(ctx, {
kind: 'prev',
namePrefix: 'Act 2 - Lair Prev',
sides: 'N',
direction: DIRECTION_SOUTH,
preferFurthest: false,
})
} else {
const nRand = rng.int(0, 3)
placeNamedPiece(ctx, {
kind: 'next',
namePrefix: 'Act 2 - Lair Next',
sides: DIRS[nRand],
plainSides: DIRS[nRand],
preferFurthest: true,
})
placeNamedPiece(ctx, {
kind: 'prev',
namePrefix: 'Act 2 - Lair Prev',
sides: DIRS[(nRand + 1) % 4],
plainSides: DIRS[(nRand + 1) % 4],
preferFurthest: false,
})
}
}
/* ------------------------------------------------------------------------- *
* 3. Act 3 - Dungeon
* ------------------------------------------------------------------------- */
/**
* Spider Cavern, Arachnid Lair, Swampy Pit 1..3, Flayer Dungeon 1..3, and Kurast Temples.
*
* Flayer Dungeon 3 places Khalim's Brain chest (Treasure 2).
* Swampy Pit 3 places Treasure 1.
* Temples (levels 94..99) place Lam Esen's Tome altar.
*/
export function placeAct3DungeonStuff(ctx: SpecialPassContext): void {
const rng = getPassRng(ctx)
const levelId = ctx.request.levelId
const nDirection = rng.int(0, 3)
if (levelId === 91) {
// Flayer Dungeon Level 3: Khalim's Brain chest
placeNamedPiece(ctx, {
kind: 'treasure',
name: 'Act 3 - Dungeon Treasure 2',
preferFurthest: true,
})
placeNamedPiece(ctx, {
kind: 'prev',
namePrefix: 'Act 3 - Dungeon Prev',
sides: DIRS[nDirection],
preferFurthest: false,
})
} else if (levelId === 90) {
// Swampy Pit Level 3
placeNamedPiece(ctx, {
kind: 'treasure',
name: 'Act 3 - Dungeon Treasure 1',
preferFurthest: true,
})
placeNamedPiece(ctx, {
kind: 'prev',
namePrefix: 'Act 3 - Dungeon Prev',
sides: DIRS[nDirection],
preferFurthest: false,
})
} else if (levelId >= 94 && levelId <= 99) {
// Ruined Temple, Disused Fane, Forgotten Reliquary, etc. (Lam Esen's Tome)
placeNamedPiece(ctx, {
kind: 'quest',
name: `Act 3 - Temple ${levelId - 93}`,
preferFurthest: true,
})
} else {
// Levels 86..89
placeNamedPiece(ctx, {
kind: 'prev',
namePrefix: 'Act 3 - Dungeon Prev',
sides: DIRS[nDirection],
plainSides: DIRS[nDirection],
preferFurthest: false,
})
placeNamedPiece(ctx, {
kind: 'next',
namePrefix: 'Act 3 - Dungeon Next',
sides: DIRS[(nDirection + 1) % 4],
plainSides: DIRS[(nDirection + 1) % 4],
preferFurthest: true,
})
}
}
/* ------------------------------------------------------------------------- *
* 4. Act 3 - Sewer
* ------------------------------------------------------------------------- */
/**
* Kurast Sewers Level 1 & 2 (`DRLGMAZE_PlaceAct3SewerStuff`).
*
* In 1.13c, Level 1 (levelId === 92) places the 4 corner entrance stair rooms (`LVLPREST_ACT3_SEWER_SW/SE/NW/NE`, Def 585..588),
* the drain descent lever (`LVLPREST_ACT3_SEWER_DRAIN`, Def 589..592), and the chest room (`Def 593..596`).
*/
export function placeAct3SewerStuff(ctx: SpecialPassContext): void {
const rng = getPassRng(ctx)
const levelId = ctx.request.levelId
const nDirection = rng.int(0, 3)
if (levelId === 92) {
// 1.13c 4 corner entrance stair rooms (SW=585, SE=586, NW=587, NE=588)
const cornerDefs = [
LVLPREST_ACT3_SEWER_SW,
LVLPREST_ACT3_SEWER_SE,
LVLPREST_ACT3_SEWER_NW,
LVLPREST_ACT3_SEWER_NE,
] as const
for (const defId of cornerDefs) {
const entry = LVLPREST_MAZE_DEF_REGISTRY[defId]!
if (ctx.pieces.some(p => p.name.toLowerCase() === entry.name.toLowerCase())) {
replaceRoomByPresetDefId(ctx, defId, { preferFurthest: false })
} else {
ctx.stats.notes.push(`lvlPrestDef=${String(defId)} (${entry.name})`)
}
}
// Sewer Level 1: Drain lever / stairs down to Sewer 2 (Def 589..592)
const drainDef = LVLPREST_ACT3_SEWER_DRAIN_BASE + nDirection
ctx.stats.notes.push(`lvlPrestDef=${String(drainDef)} (Act 3 - Sewer Drain ${DIRS[nDirection]})`)
placeNamedPiece(ctx, {
kind: 'next',
namePrefix: 'Act 3 - Sewer Drain',
sides: DIRS[nDirection],
plainSides: DIRS[nDirection],
direction: DIRECTION_NORTH,
preferFurthest: true,
})
// Chest room (Def 593..596)
const chestDir = (nDirection + 1) % 4
const chestDef = LVLPREST_ACT3_SEWER_CHEST_BASE + chestDir
ctx.stats.notes.push(`lvlPrestDef=${String(chestDef)} (Act 3 - Sewer Chest ${DIRS[chestDir]})`)
placeNamedPiece(ctx, {
kind: 'treasure',
namePrefix: 'Act 3 - Sewer Chest',
sides: DIRS[chestDir],
plainSides: DIRS[chestDir],
direction: DIRECTION_SOUTH,
preferFurthest: true,
})
} else {
// Sewer Level 2 (or other)
placeNamedPiece(ctx, {
kind: 'treasure',
namePrefix: 'Act 3 - Sewer Chest',
sides: DIRS[nDirection],
plainSides: DIRS[nDirection],
preferFurthest: true,
})
placeNamedPiece(ctx, {
kind: 'treasure',
name: 'Act 3 - Sewer Treasure X',
preferFurthest: true,
})
}
}
/* ------------------------------------------------------------------------- *
* 5. Act 3 - Mephisto
* ------------------------------------------------------------------------- */
/**
* Durance of Hate Level 1, 2, 3.
*
* Level 3 places the Mephisto Complex (Council Hall, blood moat, red portal to Act 4).
* Level 2 places Waypoint, Next and Prev.
*/
export function placeAct3MephistoStuff(ctx: SpecialPassContext): void {
const rng = getPassRng(ctx)
const levelId = ctx.request.levelId
const nRand = rng.int(0, 3)
if (levelId === 102) {
// Durance of Hate Level 3: Mephisto Complex
if (ctx.level.rooms.length === 1 && ctx.level.first !== null) {
const complexPiece = ctx.pieces.find(p => p.name === 'Act 3 - Mephisto Complex')
if (complexPiece !== undefined) {
ctx.level.first.piece = complexPiece
ctx.level.first.hasDs1 = true
noteSpecial(ctx.level.first, complexPiece, '', ctx.stats)
return
}
}
placeNamedPiece(ctx, {
kind: 'quest',
name: 'Act 3 - Mephisto Complex',
preferFurthest: true,
})
} else if (levelId === 101) {
// Durance of Hate Level 2: Prev, Waypoint, Next
placeNamedPiece(ctx, {
kind: 'prev',
namePrefix: 'Act 3 - Mephisto Prev',
sides: DIRS[nRand],
plainSides: DIRS[nRand],
preferFurthest: false,
})
placeNamedPiece(ctx, {
kind: 'waypoint',
namePrefix: 'Act 3 - Mephisto Waypoint',
sides: DIRS[(nRand + 1) % 4],
plainSides: DIRS[(nRand + 1) % 4],
preferFurthest: true,
})
placeNamedPiece(ctx, {
kind: 'next',
namePrefix: 'Act 3 - Mephisto Next',
sides: DIRS[(nRand + 2) % 4],
plainSides: DIRS[(nRand + 2) % 4],
preferFurthest: true,
})
} else {
// Durance of Hate Level 1: Prev, Next
placeNamedPiece(ctx, {
kind: 'prev',
namePrefix: 'Act 3 - Mephisto Prev',
sides: DIRS[nRand],
plainSides: DIRS[nRand],
preferFurthest: false,
})
placeNamedPiece(ctx, {
kind: 'next',
namePrefix: 'Act 3 - Mephisto Next',
sides: DIRS[(nRand + 1) % 4],
plainSides: DIRS[(nRand + 1) % 4],
preferFurthest: true,
})
}
}
/* ------------------------------------------------------------------------- *
* 6. Act 5 - Temple
* ------------------------------------------------------------------------- */
/**
* Halls of Anguish, Halls of Pain, Halls of Vaught (`DRLGMAZE_PlaceAct5TempleStuff`).
*
* Enforces 1.13c `nExpansionHardcodedPresetsRemapping` for SW/SE/NW/NE orientations (`Def 1044..1047`).
* Halls of Vaught (level 124) places Nihlathak's altar (`Def 1059`) and deterministically rolls
* Nihlathak's active wing orientation (`NW/NE/SE/SW`, `Def 1060..1063`).
*/
export function placeAct5TempleStuff(ctx: SpecialPassContext): void {
const rng = getPassRng(ctx)
const levelId = ctx.request.levelId
if (levelId === 124) {
// Halls of Vaught: Nihlathak Final Room (Def 1059) + 1 of 4 Nihlathak wing orientations (Def 1060..1063)
const wingIdx = rng.int(0, 3)
const nihlathakWing = ACT5_NIHLATHAK_WINGS[wingIdx]!
const wingDefId = LVLPREST_ACT5_TEMPLE_NIHLATHAK_WING_BASE + wingIdx
ctx.stats.notes.push(`lvlPrestDef=${String(wingDefId)} (nihlathakWing=${nihlathakWing})`)
if (ctx.level.rooms.length === 1 && ctx.level.first !== null) {
const finalPiece = ctx.pieces.find(p => p.name === 'Act 5 - Temple Final Room')
if (finalPiece !== undefined) {
ctx.level.first.piece = finalPiece
ctx.level.first.hasDs1 = true
noteSpecial(ctx.level.first, finalPiece, '', ctx.stats)
ctx.stats.notes.push(`lvlPrestDef=${String(LVLPREST_ACT5_TEMPLE_FINAL_ROOM)} (Act 5 - Temple Final Room)`)
return
}
}
replaceRoomByPresetDefId(ctx, LVLPREST_ACT5_TEMPLE_FINAL_ROOM, {
kind: 'quest',
name: 'Act 5 - Temple Final Room',
preferFurthest: true,
})
} else {
// Levels 122 and 123: apply 1.13c nExpansionHardcodedPresetsRemapping (SW=1044, SE=1045, NW=1046, NE=1047)
const templeDownDirs = ['NE', 'NW', 'SW'] as const
const nDirection = rng.int(0, templeDownDirs.length - 1)
const downSide = templeDownDirs[nDirection]!
const remapIdx = ACT5_TEMPLE_HARDCODED_PRESETS_REMAPPING.indexOf(downSide)
const remappedDefId = LVLPREST_ACT5_TEMPLE_REMAPPING_BASE + (remapIdx >= 0 ? remapIdx : 0)
ctx.stats.notes.push(`lvlPrestDef=${String(remappedDefId)} (Act 5 - Temple ${downSide} Down)`)
placeNamedPiece(ctx, {
kind: 'down',
name: `Act 5 - Temple ${downSide} Down`,
sides: downSide,
plainSides: downSide,
preferFurthest: true,
})
if (levelId === 123) {
const wpSide = ACT5_TEMPLE_HARDCODED_PRESETS_REMAPPING[rng.int(0, ACT5_TEMPLE_HARDCODED_PRESETS_REMAPPING.length - 1)]!
placeNamedPiece(ctx, {
kind: 'waypoint',
name: `Act 5 - Temple ${wpSide} Waypoint`,
sides: wpSide,
plainSides: wpSide,
preferFurthest: true,
})
}
placeNamedPiece(ctx, {
kind: 'prev',
name: 'Act 5 - Temple SE up',
sides: 'SE',
plainSides: 'SE',
preferFurthest: false,
})
}
}
/* ------------------------------------------------------------------------- *
* 7. Act 5 - Baal
* ------------------------------------------------------------------------- */
/**
* Worldstone Keep Level 1, 2, 3 and Throne of Destruction.
*
* Throne of Destruction (level 131) places Baal Entrance / ThroneRoom.
* Worldstone Keep 2 places Waypoint and Next stairs.
*/
export function placeAct5BaalStuff(ctx: SpecialPassContext): void {
const rng = getPassRng(ctx)
const levelId = ctx.request.levelId
const nDirection = rng.int(0, 3)
if (levelId === 131) {
// Throne of Destruction
if (ctx.level.rooms.length === 1 && ctx.level.first !== null) {
const thronePiece = ctx.pieces.find(p => p.name === 'Act 5 - ThroneRoom' || p.name === 'Act 5 - Baal Entrance')
if (thronePiece !== undefined) {
ctx.level.first.piece = thronePiece
ctx.level.first.hasDs1 = true
noteSpecial(ctx.level.first, thronePiece, '', ctx.stats)
return
}
}
const placed = placeNamedPiece(ctx, {
kind: 'quest',
name: 'Act 5 - ThroneRoom',
preferFurthest: true,
})
if (placed === null) {
placeNamedPiece(ctx, {
kind: 'quest',
name: 'Act 5 - Baal Entrance',
preferFurthest: true,
})
}
} else if (levelId === 129) {
// Worldstone Keep Level 2: Next and Waypoint
placeNamedPiece(ctx, {
kind: 'next',
namePrefix: 'Act 5 - Baal Next',
sides: DIRS[nDirection],
plainSides: DIRS[nDirection],
preferFurthest: true,
})
placeNamedPiece(ctx, {
kind: 'waypoint',
namePrefix: 'Act 5 - Baal Waypoint',
sides: DIRS[(nDirection + 1) % 4],
plainSides: DIRS[(nDirection + 1) % 4],
preferFurthest: true,
})
} else {
// Worldstone Keep Levels 1, 3
placeNamedPiece(ctx, {
kind: 'next',
namePrefix: 'Act 5 - Baal Next',
sides: DIRS[nDirection],
plainSides: DIRS[nDirection],
preferFurthest: true,
})
}
}
/* ------------------------------------------------------------------------- *
* 8. Act 5 - Ice
* ------------------------------------------------------------------------- */
/**
* Crystalline Passage, Frozen River, Glacial Trail, Drifter Cavern, Ancients' Way, Icy Cellar (`DRLGMAZE_PlaceAct5IceStuff`).
*
* Frozen River (level 114) rolls Anya's frozen platform (`LVLPREST_ACT5_ICE_RIVER_A = 1030` vs `B = 1031`) and places entry stairs.
* Drifter Cavern (116) places `LVLPREST_ACT5_ICE_POOL_A = 1032`; Icy Cellar (119) places `LVLPREST_ACT5_ICE_POOL_B = 1033`.
* Crystalline Passage (113), Glacial Trail (115), and Ancients' Way (118) place Prev, Next, Down, Theme, and Waypoint.
*/
export function placeAct5IceStuff(ctx: SpecialPassContext): void {
const rng = getPassRng(ctx)
const levelId = ctx.request.levelId
if (levelId === 114) {
// Frozen River (Anya platform): 1.13c rolls Def 1030 (River A) vs Def 1031 (River B)
const variantIdx = rng.int(0, 1)
const variant = variantIdx === 0 ? 'A' : 'B'
const riverDefId = variantIdx === 0 ? LVLPREST_ACT5_ICE_RIVER_A : LVLPREST_ACT5_ICE_RIVER_B
if (ctx.level.rooms.length === 1 && ctx.level.first !== null) {
const riverPiece = ctx.pieces.find(p => p.name === `Act 5 - Ice River ${variant}`)
?? ctx.pieces.find(p => p.name.startsWith('Act 5 - Ice River'))
if (riverPiece !== undefined) {
ctx.level.first.piece = riverPiece
ctx.level.first.hasDs1 = true
noteSpecial(ctx.level.first, riverPiece, '', ctx.stats)
ctx.stats.notes.push(`lvlPrestDef=${String(riverDefId)} (${riverPiece.name})`)
return
}
}
replaceRoomByPresetDefId(ctx, riverDefId, {
kind: 'quest',
name: `Act 5 - Ice River ${variant}`,
preferFurthest: true,
})
if (
ctx.level.rooms.length > 1 &&
!ctx.level.rooms.some(r => r.piece?.kind === 'prev') &&
ctx.pieces.some(p => p.name.startsWith('Act 5 - Ice Prev'))
) {
const prevDir = rng.int(0, 3)
placeNamedPiece(ctx, {
kind: 'prev',
namePrefix: 'Act 5 - Ice Prev',
sides: DIRS[prevDir],
plainSides: DIRS[prevDir],
preferFurthest: false,
})
}
} else if (levelId === 116) {
if (ctx.level.rooms.length === 1 && ctx.level.first !== null) {
const poolPiece = ctx.pieces.find(p => p.name === 'Act 5 - Ice Pool A')
if (poolPiece !== undefined) {
ctx.level.first.piece = poolPiece
ctx.level.first.hasDs1 = true
noteSpecial(ctx.level.first, poolPiece, '', ctx.stats)
ctx.stats.notes.push(`lvlPrestDef=${String(LVLPREST_ACT5_ICE_POOL_A)} (${poolPiece.name})`)
return
}
}
replaceRoomByPresetDefId(ctx, LVLPREST_ACT5_ICE_POOL_A, {
kind: 'quest',
name: 'Act 5 - Ice Pool A',
preferFurthest: true,
})
} else if (levelId === 119) {
if (ctx.level.rooms.length === 1 && ctx.level.first !== null) {
const poolPiece = ctx.pieces.find(p => p.name === 'Act 5 - Ice Pool B')
if (poolPiece !== undefined) {
ctx.level.first.piece = poolPiece
ctx.level.first.hasDs1 = true
noteSpecial(ctx.level.first, poolPiece, '', ctx.stats)
ctx.stats.notes.push(`lvlPrestDef=${String(LVLPREST_ACT5_ICE_POOL_B)} (${poolPiece.name})`)
return
}
}
replaceRoomByPresetDefId(ctx, LVLPREST_ACT5_ICE_POOL_B, {
kind: 'quest',
name: 'Act 5 - Ice Pool B',
preferFurthest: true,
})
} else {
// Levels 113, 115, 118
let nDirection = rng.int(0, 3)
placeNamedPiece(ctx, {
kind: 'prev',
namePrefix: 'Act 5 - Ice Prev',
sides: DIRS[nDirection],
plainSides: DIRS[nDirection],
preferFurthest: false,
})
nDirection = (nDirection + 1) % 4
placeNamedPiece(ctx, {
kind: 'next',
namePrefix: 'Act 5 - Ice Next',
sides: DIRS[nDirection],
plainSides: DIRS[nDirection],
preferFurthest: true,
})
nDirection = (nDirection + 1) % 4
placeNamedPiece(ctx, {
kind: 'down',
namePrefix: 'Act 5 - Ice Down',
sides: DIRS[nDirection],
plainSides: DIRS[nDirection],
preferFurthest: true,
})
if (levelId === 115) {
nDirection = (nDirection + 1) % 4
placeNamedPiece(ctx, {
kind: 'theme',
namePrefix: 'Act 5 - Ice Theme',
sides: DIRS[nDirection],
plainSides: DIRS[nDirection],
preferFurthest: true,
})
}
if (levelId === 113 || levelId === 115 || levelId === 118) {
nDirection = (nDirection + 1) % 4
const wpDef = LVLPREST_ACT5_ICE_WAYPOINT_BASE + nDirection
ctx.stats.notes.push(`lvlPrestDef=${String(wpDef)} (Act 5 - Ice waypoint ${DIRS[nDirection]})`)
placeNamedPiece(ctx, {
kind: 'waypoint',
namePrefix: 'Act 5 - Ice waypoint',
sides: DIRS[nDirection],
plainSides: DIRS[nDirection],
preferFurthest: true,
})
}
}
}
/* ------------------------------------------------------------------------- *
* 9. Act 1 - Barracks
* ------------------------------------------------------------------------- */
/**
* Monastery Barracks (level 28).
*
* Places Court Connect, the Smith / Charsi's Malus forge, and stairs to Jail.
*/
export function placeAct1Barracks(ctx: SpecialPassContext): void {
const rng = getPassRng(ctx)
// Connect towards Outer Cloister
placeNamedPiece(ctx, {
kind: 'prev',
name: 'Act 1 - Barracks Court Connect',
direction: DIRECTION_WEST,
preferFurthest: false,
})
const nDirection = rng.int(0, 3)
const roll = rng.int(0, 1)
if (roll === 1) {
placeNamedPiece(ctx, {
kind: 'next',
namePrefix: 'Act 1 - Barracks Next',
sides: DIRS[nDirection],
plainSides: DIRS[nDirection],
preferFurthest: true,
})
placeNamedPiece(ctx, {
kind: 'quest',
namePrefix: 'Act 1 - Barracks Forge',
sides: DIRS[(nDirection + 1) % 4],
plainSides: DIRS[(nDirection + 1) % 4],
preferFurthest: true,
})
} else {
placeNamedPiece(ctx, {
kind: 'quest',
namePrefix: 'Act 1 - Barracks Forge',
sides: DIRS[nDirection],
plainSides: DIRS[nDirection],
preferFurthest: true,
})
placeNamedPiece(ctx, {
kind: 'next',
namePrefix: 'Act 1 - Barracks Next',
sides: DIRS[(nDirection + 1) % 4],
plainSides: DIRS[(nDirection + 1) % 4],
preferFurthest: true,
})
}
}
/* ------------------------------------------------------------------------- *
* 10. Act 4 - Lava
* ------------------------------------------------------------------------- */
/**
* River of Flame (level 107).
*
* 1.13c D2MOO DRLGMAZE_PlaceAct4Lava parity:
* 1. Places Warp N (entrance from City of Damned) at max Y going DIRECTION_SOUTH.
* 2. Chains 3 bridge rooms going DIRECTION_NORTH towards Chaos Sanctuary:
* - Bridge 1 (LVLPREST_ACT4_BRIDGE_1)
* - Bridge 2 segment 1 (LVLPREST_ACT4_BRIDGE_2)
* - Bridge 2 segment 2 (LVLPREST_ACT4_BRIDGE_2)
* 3. Hellforge (Hephasto the Armorer):
* - Roll forgeSide ('W' or 'E').
* - Scan & Replace: In-place replace matching plain room (sides === forgeSide) if found.
* - Fallback: Grow adjacent room in DIRECTION_EAST (for W) or DIRECTION_WEST (for E).
* 4. Fill blank maze spaces with Act 4 - Lava X, IGNORING Bridge 2 segment 2 (bridge3)
* so the threshold to Chaos Sanctuary is not walled off.
*/
export function placeAct4Lava(ctx: SpecialPassContext): void {
const rng = getPassRng(ctx)
// Target pieces
const warpNPiece = ctx.pieces.find(p => p.name === 'Act 4 - Lava Warp N')
?? ctx.pieces.find(p => p.name.startsWith('Act 4 - Lava Warp'))
?? findSpecial(ctx.pieces, 'prev', 'N')
const bridge1Piece = ctx.pieces.find(p => p.name === 'Act 4 - Bridge 1')
?? ctx.pieces.find(p => p.name.toLowerCase() === 'act 4 - bridge 1')
const bridge2Piece = ctx.pieces.find(p => p.name === 'Act 4 - Bridge 2')
?? ctx.pieces.find(p => p.name.toLowerCase() === 'act 4 - bridge 2')
// 1. Warp N (entrance from City of Damned)
// D2MOO DRLGMAZE_GetFreeLocationForRoomNorth: find room with max Y that can place DIRECTION_SOUTH
const southCandidates = [...ctx.level.rooms].sort((a, b) => b.y - a.y)
let warpRoom: Room | null = null
for (const parent of southCandidates) {
warpRoom = addAdjacentMazeRoom(ctx.level, parent, DIRECTION_SOUTH, false, true, ctx.index, ctx.stats)
if (warpRoom !== null) break
}
if (warpRoom === null && warpNPiece !== undefined) {
warpRoom = placeNamedPiece(ctx, {
kind: 'prev',
namePrefix: 'Act 4 - Lava Warp',
sides: 'N',
plainSides: 'N',
direction: DIRECTION_SOUTH,
preferFurthest: false,
})
} else if (warpRoom !== null && warpNPiece !== undefined) {
warpRoom.piece = warpNPiece
warpRoom.variant = 0
warpRoom.hasDs1 = true
noteSpecial(warpRoom, warpNPiece, 'N', ctx.stats)
}
// 2. Bridges towards Chaos Sanctuary
// D2MOO DRLGMAZE_GetFreeLocationForRoomSouth: find room with min Y that can place DIRECTION_NORTH
// Chains 3 bridge rooms: Bridge 1, Bridge 2 (seg 1), Bridge 2 (seg 2)
const northCandidates = [...ctx.level.rooms].filter(r => r !== warpRoom).sort((a, b) => a.y - b.y)
let bridge1: Room | null = null
let bridge2: Room | null = null
let bridge3: Room | null = null
for (const parent of northCandidates) {
bridge1 = addAdjacentMazeRoom(ctx.level, parent, DIRECTION_NORTH, false, true, ctx.index, ctx.stats)
if (bridge1 !== null) {
bridge2 = addAdjacentMazeRoom(ctx.level, bridge1, DIRECTION_NORTH, false, false, ctx.index, ctx.stats)
if (bridge2 !== null) {
bridge3 = addAdjacentMazeRoom(ctx.level, bridge2, DIRECTION_NORTH, false, false, ctx.index, ctx.stats)
if (bridge3 !== null) break
}
}
}
// Fallback if tight space prevented placing all 3 chained bridges
if (bridge1 === null) {
bridge1 = placeNamedPiece(ctx, {
kind: 'quest',
name: 'Act 4 - Bridge 1',
direction: DIRECTION_NORTH,
preferFurthest: true,
})
} else if (bridge1Piece !== undefined) {
bridge1.piece = bridge1Piece
bridge1.variant = 0
bridge1.hasDs1 = true
noteSpecial(bridge1, bridge1Piece, '', ctx.stats)
}
if (bridge2 === null) {
bridge2 = placeNamedPiece(ctx, {
kind: 'quest',
name: 'Act 4 - Bridge 2',
direction: DIRECTION_NORTH,
preferFurthest: true,
})
} else if (bridge2Piece !== undefined) {
bridge2.piece = bridge2Piece
bridge2.variant = 0
bridge2.hasDs1 = true
noteSpecial(bridge2, bridge2Piece, '', ctx.stats)
}
if (bridge3 === null && bridge2 !== null) {
bridge3 = addAdjacentMazeRoom(ctx.level, bridge2, DIRECTION_NORTH, false, false, ctx.index, ctx.stats)
}
if (bridge3 !== null && bridge2Piece !== undefined) {
bridge3.piece = bridge2Piece
bridge3.variant = 0
bridge3.hasDs1 = true
noteSpecial(bridge3, bridge2Piece, '', ctx.stats)
}
// 3. Hellforge (Hephasto the Armorer)
// D2MOO DRLGMAZE_ScanReplaceSpecialPreset with nAct4LavaForgeIds:
// Forge W (Def 853) replaces Lava W (Def 837) or attaches in direction 2 (East)
// Forge E (Def 854) replaces Lava E (Def 838) or attaches in direction 0 (West)
const forgeSide = rng.int(0, 1) === 0 ? 'W' : 'E'
const forgeName = `Act 4 - Lava Forge ${forgeSide}`
const forgePiece = ctx.pieces.find(p => p.name === forgeName)
?? ctx.pieces.find(p => p.name.startsWith('Act 4 - Lava Forge') && p.sides === forgeSide)
?? findSpecial(ctx.pieces, 'quest', forgeSide)
?? ctx.pieces.find(p => p.kind === 'quest' && p.sides === forgeSide)
if (forgePiece !== undefined) {
const targetMask = sideMaskFromToken(forgeSide)
// Scan existing rooms: first check if a room already matches the single-door side
const replaceableRooms = ctx.level.rooms.filter(r => !r.hasDs1 && roomSideMask(r) === targetMask)
let forgeRoom: Room | null = replaceableRooms[0] ?? null
if (forgeRoom !== null) {
// In-place replace
forgeRoom.piece = forgePiece
forgeRoom.variant = 0
forgeRoom.hasDs1 = true
noteSpecial(forgeRoom, forgePiece, forgeSide, ctx.stats)
} else {
// Not found: attempt to attach in the designated direction (East for W, West for E)
const attachDir = forgeSide === 'W' ? DIRECTION_EAST : DIRECTION_WEST
const attachCandidates = ctx.level.rooms.filter(r => !r.hasDs1)
for (const parent of attachCandidates) {
forgeRoom = addAdjacentMazeRoom(ctx.level, parent, attachDir, false, true, ctx.index, ctx.stats)
if (forgeRoom !== null) break
}
if (forgeRoom === null) {
// Fallback to general placement
forgeRoom = placeNamedPiece(ctx, {
kind: 'quest',
name: forgeName,
plainSides: forgeSide,
preferFurthest: true,
})
} else {
forgeRoom.piece = forgePiece
forgeRoom.variant = 0
forgeRoom.hasDs1 = true
noteSpecial(forgeRoom, forgePiece, forgeSide, ctx.stats)
}
}
}
// 4. Fill blank maze spaces with Act 4 - Lava X, ignoring bridge3
fillBlankSpaces(ctx.level, ctx.pieces, ctx.index, ctx.stats, bridge3)
}
/* ------------------------------------------------------------------------- *
* 11. Act 2 - Sewers
* ------------------------------------------------------------------------- */
/**
* Lut Gholein Sewers Level 1, 2, 3 and Ancient Tunnels.
*
* Level 3 places Radament's Lair (Book of Skill / Atma's quest).
* Level 1 places Prev E & Prev NS.
* Level 2 places Waypoint, Prev, Next.
* Ancient Tunnels (level 50) places Chest and Prev.
*/
export function scanReplaceSpecialAct2SewersPresets(ctx: SpecialPassContext): void {
const rng = getPassRng(ctx)
const levelId = ctx.request.levelId
let nRand = rng.int(0, 3)
if (levelId === 49) {
// Sewers Level 3: Radament's Lair
placeNamedPiece(ctx, {
kind: 'prev',
namePrefix: 'Act 2 - Sewer Prev',
sides: DIRS[nRand],
plainSides: DIRS[nRand],
preferFurthest: false,
})
nRand = (nRand + 1) % 4
placeNamedPiece(ctx, {
kind: 'quest',
namePrefix: "Act 2 - Sewer Radament's Lair",
sides: DIRS[nRand],
plainSides: DIRS[nRand],
preferFurthest: true,
})
} else if (levelId === 47) {
// Sewers Level 1: Dock and City entrances
placeNamedPiece(ctx, {
kind: 'prev',
name: 'Act 2 - Sewer Prev E',
sides: 'E',
direction: DIRECTION_WEST,
preferFurthest: false,
})
placeNamedPiece(ctx, {
kind: 'prev',
name: 'Act 2 - Sewer Prev NS',
sides: 'NS',
direction: DIRECTION_NORTH,
preferFurthest: false,
})
placeNamedPiece(ctx, {
kind: 'next',
namePrefix: 'Act 2 - Sewer Next',
sides: DIRS[nRand],
plainSides: DIRS[nRand],
preferFurthest: true,
})
} else if (levelId === 48) {
// Sewers Level 2: Prev, Waypoint, Next
placeNamedPiece(ctx, {
kind: 'prev',
namePrefix: 'Act 2 - Sewer Prev',
sides: DIRS[nRand],
plainSides: DIRS[nRand],
preferFurthest: false,
})
nRand = (nRand + 1) % 4
placeNamedPiece(ctx, {
kind: 'waypoint',
namePrefix: 'Act 2 - Sewer Waypoint',
sides: DIRS[nRand],
plainSides: DIRS[nRand],
preferFurthest: true,
})
nRand = (nRand + 1) % 4
placeNamedPiece(ctx, {
kind: 'next',
namePrefix: 'Act 2 - Sewer Next',
sides: DIRS[nRand],
plainSides: DIRS[nRand],
preferFurthest: true,
})
} else {
// Ancient Tunnels (level 50) or other
placeNamedPiece(ctx, {
kind: 'prev',
namePrefix: 'Act 2 - Sewer Prev',
sides: DIRS[nRand],
plainSides: DIRS[nRand],
preferFurthest: false,
})
nRand = (nRand + 1) % 4
placeNamedPiece(ctx, {
kind: 'treasure',
namePrefix: 'Act 2 - Sewer Chest',
sides: DIRS[nRand],
plainSides: DIRS[nRand],
preferFurthest: true,
})
}
}
/* ------------------------------------------------------------------------- *
* Master Runner
* ------------------------------------------------------------------------- */
export function runMazeSpecialPasses(passName: SpecialPassName, ctx: SpecialPassContext): void {
ctx.stats.specialPassesApplied.push(passName)
switch (passName) {
case 'DRLGMAZE_PlaceAct2TombStuff':
placeAct2TombStuff(ctx)
break
case 'DRLGMAZE_PlaceAct2LairStuff':
placeAct2LairStuff(ctx)
break
case 'DRLGMAZE_PlaceAct3DungeonStuff':
placeAct3DungeonStuff(ctx)
break
case 'DRLGMAZE_PlaceAct3SewerStuff':
placeAct3SewerStuff(ctx)
break
case 'DRLGMAZE_PlaceAct3MephistoStuff':
placeAct3MephistoStuff(ctx)
break
case 'DRLGMAZE_PlaceAct5TempleStuff':
placeAct5TempleStuff(ctx)
break
case 'DRLGMAZE_PlaceAct5BaalStuff':
placeAct5BaalStuff(ctx)
break
case 'DRLGMAZE_PlaceAct5IceStuff':
placeAct5IceStuff(ctx)
break
case 'DRLGMAZE_PlaceAct1Barracks':
placeAct1Barracks(ctx)
break
case 'DRLGMAZE_PlaceAct4Lava':
placeAct4Lava(ctx)
break
case 'DRLGMAZE_ScanReplaceSpecialAct2SewersPresets':
scanReplaceSpecialAct2SewersPresets(ctx)
break
}
}