995 lines
30 KiB
TypeScript
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
|
|
}
|
|
}
|