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

995 lines
30 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 {
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
}
const DIRS = ['N', 'E', 'S', 'W'] as const
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
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 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 = ctx.request.seed % 4
} else {
nDirection = ctx.request.seed % 4
}
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
}
// 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 === 59) {
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
if (levelId === staffTomb) {
placeTombPiece('quest', 'Talrasha')
}
// 9. Boss Tomb gets Ancient Kaa the Soulless
if (bossTomb !== undefined && levelId === bossTomb) {
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 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 = ctx.request.seed % 4
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 levelId = ctx.request.levelId
const nDirection = ctx.request.seed % 4
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.
*
* Level 1 places the drain descent lever and staircase down to Level 2.
*/
export function placeAct3SewerStuff(ctx: SpecialPassContext): void {
const levelId = ctx.request.levelId
const nDirection = ctx.request.seed % 4
if (levelId === 92) {
// Sewer Level 1: Drain lever / stairs down to Sewer 2
placeNamedPiece(ctx, {
kind: 'next',
namePrefix: 'Act 3 - Sewer Drain',
sides: DIRS[nDirection],
plainSides: DIRS[nDirection],
direction: DIRECTION_NORTH,
preferFurthest: true,
})
// Chest room
placeNamedPiece(ctx, {
kind: 'treasure',
namePrefix: 'Act 3 - Sewer Chest',
sides: DIRS[(nDirection + 1) % 4],
plainSides: DIRS[(nDirection + 1) % 4],
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 levelId = ctx.request.levelId
const nRand = ctx.request.seed % 4
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.
*
* Halls of Vaught (level 124) places Nihlathak's altar (Final Room).
* Halls of Pain (level 123) places Waypoint and descent stairs.
*/
export function placeAct5TempleStuff(ctx: SpecialPassContext): void {
const levelId = ctx.request.levelId
if (levelId === 124) {
// Halls of Vaught: Nihlathak Final Room
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)
return
}
}
placeNamedPiece(ctx, {
kind: 'quest',
name: 'Act 5 - Temple Final Room',
preferFurthest: true,
})
} else {
// Levels 122 and 123
const templeDownDirs = ['NE', 'NW', 'SW']
const nDirection = (ctx.request.seed >>> 0) % 3
const downSide = templeDownDirs[nDirection]!
placeNamedPiece(ctx, {
kind: 'down',
name: `Act 5 - Temple ${downSide} Down`,
sides: downSide,
plainSides: downSide,
preferFurthest: true,
})
if (levelId === 123) {
const templeWaypointDirs = ['NE', 'NW', 'SW', 'SE']
const wpSide = templeWaypointDirs[(nDirection + 1) % 4]!
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 levelId = ctx.request.levelId
const nDirection = ctx.request.seed % 4
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.
*
* Frozen River (level 114) places Anya's frozen platform (River A or River B).
* Drifter Cavern (116) and Icy Cellar (119) place Pool A / Pool B.
* Glacial Trail (115) places Theme and Waypoint.
*/
export function placeAct5IceStuff(ctx: SpecialPassContext): void {
const levelId = ctx.request.levelId
if (levelId === 114) {
// Frozen River (Anya platform)
const variant = (ctx.request.seed % 2) === 0 ? 'A' : '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)
return
}
}
placeNamedPiece(ctx, {
kind: 'quest',
name: `Act 5 - Ice River ${variant}`,
preferFurthest: true,
})
} 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)
return
}
}
placeNamedPiece(ctx, { 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)
return
}
}
placeNamedPiece(ctx, { kind: 'quest', name: 'Act 5 - Ice Pool B', preferFurthest: true })
} else {
// Levels 113, 115, 118
let nDirection = ctx.request.seed % 4
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) {
nDirection = (nDirection + 1) % 4
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 {
// Connect towards Outer Cloister
placeNamedPiece(ctx, {
kind: 'prev',
name: 'Act 1 - Barracks Court Connect',
direction: DIRECTION_WEST,
preferFurthest: false,
})
const nDirection = ctx.request.seed % 4
const roll = (ctx.request.seed >>> 2) % 2
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).
*
* Places Warp N (towards City of Damned), Bridges 1 & 2 towards Chaos Sanctuary,
* Hephasto the Armorer / Hellforge, and fills blanks with Lava X.
*/
export function placeAct4Lava(ctx: SpecialPassContext): void {
// 1. Warp N (entrance from City of Damned)
placeNamedPiece(ctx, {
kind: 'prev',
namePrefix: 'Act 4 - Lava Warp',
sides: 'N',
plainSides: 'N',
direction: DIRECTION_SOUTH,
preferFurthest: false,
})
// 2. Bridges towards Chaos Sanctuary
placeNamedPiece(ctx, {
kind: 'quest',
name: 'Act 4 - Bridge 1',
direction: DIRECTION_NORTH,
preferFurthest: true,
})
placeNamedPiece(ctx, {
kind: 'quest',
name: 'Act 4 - Bridge 2',
direction: DIRECTION_NORTH,
preferFurthest: true,
})
// 3. Hellforge (Hephasto the Armorer)
const forgeSide = (ctx.request.seed % 2) === 0 ? 'W' : 'E'
placeNamedPiece(ctx, {
kind: 'quest',
name: `Act 4 - Lava Forge ${forgeSide}`,
plainSides: forgeSide,
preferFurthest: true,
})
// 4. Fill blank maze spaces with Act 4 - Lava X
fillBlankSpaces(ctx.level, ctx.pieces, ctx.index, ctx.stats)
}
/* ------------------------------------------------------------------------- *
* 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 levelId = ctx.request.levelId
let nRand = ctx.request.seed % 4
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
}
}