feat(map-gen/maze): integrate waypoint room into maze preset replacement pass (fixes #47)
This commit is contained in:
parent
1fa3f8b7a2
commit
776e2d3c20
|
|
@ -46,7 +46,7 @@ import { decodeDcc } from '../src/formats/dcc.ts'
|
||||||
import { decodeDc6 } from '../src/formats/dc6.ts'
|
import { decodeDc6 } from '../src/formats/dc6.ts'
|
||||||
import type { SpriteFrame } from '../src/formats/sprite.ts'
|
import type { SpriteFrame } from '../src/formats/sprite.ts'
|
||||||
import { generateMaze, classifyMazePieceName } from '../src/game/maze.ts'
|
import { generateMaze, classifyMazePieceName } from '../src/game/maze.ts'
|
||||||
import type { MazePiece, MazePieceKind } from '../src/game/maze.ts'
|
import type { MazePiece, MazePieceKind, MazeWaypoint } from '../src/game/maze.ts'
|
||||||
import { generateWilderness } from '../src/game/wilderness.ts'
|
import { generateWilderness } from '../src/game/wilderness.ts'
|
||||||
import type { PlannedGate, WildernessEntrance, WildernessPiece, WildernessSubstitution } from '../src/game/wilderness.ts'
|
import type { PlannedGate, WildernessEntrance, WildernessPiece, WildernessSubstitution } from '../src/game/wilderness.ts'
|
||||||
export interface MazeWarp {
|
export interface MazeWarp {
|
||||||
|
|
@ -537,6 +537,7 @@ function buildSceneLinks(
|
||||||
entrances: readonly WildernessEntrance[],
|
entrances: readonly WildernessEntrance[],
|
||||||
mazeWarps: readonly MazeWarp[],
|
mazeWarps: readonly MazeWarp[],
|
||||||
waypointCells: readonly { x: number; y: number }[],
|
waypointCells: readonly { x: number; y: number }[],
|
||||||
|
mazeWaypoints?: readonly MazeWaypoint[],
|
||||||
): SceneLinks {
|
): SceneLinks {
|
||||||
const outEntrances: SceneEntrance[] = []
|
const outEntrances: SceneEntrance[] = []
|
||||||
const outWarps: SceneWarp[] = []
|
const outWarps: SceneWarp[] = []
|
||||||
|
|
@ -781,7 +782,7 @@ function buildSceneLinks(
|
||||||
const waypointId = worldGraph.levels.get(levelId)?.waypoint ?? 255
|
const waypointId = worldGraph.levels.get(levelId)?.waypoint ?? 255
|
||||||
const waypoints: SceneWaypoint[] = []
|
const waypoints: SceneWaypoint[] = []
|
||||||
if (waypointId !== 255) {
|
if (waypointId !== 255) {
|
||||||
const fromArt = waypointCells[0]
|
const fromArt = waypointCells[0] ?? (mazeWaypoints?.[0] ? { x: mazeWaypoints[0].entityX, y: mazeWaypoints[0].entityY } : undefined)
|
||||||
const spot = fromArt ?? findWaypointSpot(grid, region)
|
const spot = fromArt ?? findWaypointSpot(grid, region)
|
||||||
if (spot !== null && spot !== undefined) {
|
if (spot !== null && spot !== undefined) {
|
||||||
const arrive = nearestWalkable(grid, spot.x + 2, spot.y + 2, 16, region) ?? spot
|
const arrive = nearestWalkable(grid, spot.x + 2, spot.y + 2, 16, region) ?? spot
|
||||||
|
|
@ -1154,6 +1155,7 @@ async function bakeDs1Variant(
|
||||||
generatorLinks: {
|
generatorLinks: {
|
||||||
readonly entrances: readonly WildernessEntrance[]
|
readonly entrances: readonly WildernessEntrance[]
|
||||||
readonly mazeWarps: readonly MazeWarp[]
|
readonly mazeWarps: readonly MazeWarp[]
|
||||||
|
readonly mazeWaypoints?: readonly MazeWaypoint[]
|
||||||
readonly landmarks?: readonly { readonly id: string; readonly tileX: number; readonly tileY: number }[]
|
readonly landmarks?: readonly { readonly id: string; readonly tileX: number; readonly tileY: number }[]
|
||||||
} = { entrances: [], mazeWarps: [] },
|
} = { entrances: [], mazeWarps: [] },
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
|
|
@ -1383,6 +1385,7 @@ async function bakeDs1Variant(
|
||||||
generatorLinks.entrances,
|
generatorLinks.entrances,
|
||||||
generatorLinks.mazeWarps,
|
generatorLinks.mazeWarps,
|
||||||
waypointCells,
|
waypointCells,
|
||||||
|
generatorLinks.mazeWaypoints,
|
||||||
)
|
)
|
||||||
|
|
||||||
const sceneJson = {
|
const sceneJson = {
|
||||||
|
|
@ -1565,7 +1568,11 @@ for (const entry of LEVELS) {
|
||||||
await bakeDs1Variant(
|
await bakeDs1Variant(
|
||||||
entry, entry.name, palette, paletteName, libInfo.dt1Names, libraries,
|
entry, entry.name, palette, paletteName, libInfo.dt1Names, libraries,
|
||||||
result.level, `generated:${label}`, label, seed,
|
result.level, `generated:${label}`, label, seed,
|
||||||
{ entrances: [], mazeWarps: (result.stats.warps ?? []) as MazeWarp[] },
|
{
|
||||||
|
entrances: [],
|
||||||
|
mazeWarps: (result.stats.warps ?? []) as MazeWarp[],
|
||||||
|
mazeWaypoints: (result.stats.waypoints ?? []) as MazeWaypoint[],
|
||||||
|
},
|
||||||
)
|
)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(`failed to bake maze ${label}: ${(err as Error).message}`)
|
console.error(`failed to bake maze ${label}: ${(err as Error).message}`)
|
||||||
|
|
|
||||||
115
src/game/maze.ts
115
src/game/maze.ts
|
|
@ -80,6 +80,7 @@
|
||||||
import type { Ds1, Ds1Cell, Ds1Floor, Ds1Object, Ds1Wall } from '../formats/ds1.ts'
|
import type { Ds1, Ds1Cell, Ds1Floor, Ds1Object, Ds1Wall } from '../formats/ds1.ts'
|
||||||
import { Rng } from './rng.ts'
|
import { Rng } from './rng.ts'
|
||||||
import { SUB_TILES_PER_TILE } from './map.ts'
|
import { SUB_TILES_PER_TILE } from './map.ts'
|
||||||
|
export { SUB_TILES_PER_TILE }
|
||||||
|
|
||||||
/* ------------------------------------------------------------------------- *
|
/* ------------------------------------------------------------------------- *
|
||||||
* Directions
|
* Directions
|
||||||
|
|
@ -192,6 +193,27 @@ export type MazePieceKind =
|
||||||
| 'down'
|
| 'down'
|
||||||
| 'quest'
|
| 'quest'
|
||||||
| 'treasure'
|
| 'treasure'
|
||||||
|
| 'waypoint'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A waypoint room, as reported in `stats.waypoints`.
|
||||||
|
*/
|
||||||
|
export interface MazeWaypoint {
|
||||||
|
/** The room the piece went into, matching `specialsApplied`. */
|
||||||
|
readonly room: number
|
||||||
|
/** The room's top-left corner in the final map, in cells. */
|
||||||
|
readonly x: number
|
||||||
|
readonly y: number
|
||||||
|
/** The room's extent, in cells. */
|
||||||
|
readonly width: number
|
||||||
|
readonly height: number
|
||||||
|
/** The room's centre, where the player lands or clicks. */
|
||||||
|
readonly centreX: number
|
||||||
|
readonly centreY: number
|
||||||
|
/** Sub-tile coordinates of the waypoint entity within the synthesized map. */
|
||||||
|
readonly entityX: number
|
||||||
|
readonly entityY: number
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* One `LvlPrest.txt` row that can be stamped into a maze level.
|
* One `LvlPrest.txt` row that can be stamped into a maze level.
|
||||||
|
|
@ -265,6 +287,13 @@ export interface MazeRequest {
|
||||||
readonly staffTombLevelId?: number
|
readonly staffTombLevelId?: number
|
||||||
/** Boss tomb (`LvlMaze.Rooms` doubled); see {@link staffTombLevelId}. */
|
/** Boss tomb (`LvlMaze.Rooms` doubled); see {@link staffTombLevelId}. */
|
||||||
readonly bossTombLevelId?: number
|
readonly bossTombLevelId?: number
|
||||||
|
/**
|
||||||
|
* Explicit override for waypoint placement.
|
||||||
|
* If true, forces a waypoint room to be placed (using waypoint pieces).
|
||||||
|
* If false, skips waypoint room placement even if the level profile includes it.
|
||||||
|
* If omitted, defaults to whether the level id has a waypoint in the profile.
|
||||||
|
*/
|
||||||
|
readonly hasWaypoint?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
/** The generated level. */
|
/** The generated level. */
|
||||||
|
|
@ -294,6 +323,8 @@ const KIND_PREFIXES: readonly (readonly [string, MazePieceKind])[] = [
|
||||||
['Next', 'next'],
|
['Next', 'next'],
|
||||||
['Prev', 'prev'],
|
['Prev', 'prev'],
|
||||||
['Theme', 'theme'],
|
['Theme', 'theme'],
|
||||||
|
['Waypoint', 'waypoint'],
|
||||||
|
['waypoint', 'waypoint'],
|
||||||
]
|
]
|
||||||
|
|
||||||
/** Which `LvlPrest` name families belong to which maze level type. */
|
/** Which `LvlPrest` name families belong to which maze level type. */
|
||||||
|
|
@ -338,17 +369,17 @@ export function classifyMazePieceName(name: string, levelTypeName: string): { ki
|
||||||
const rest = name.slice(matchedFamily.length).trim()
|
const rest = name.slice(matchedFamily.length).trim()
|
||||||
if (rest === '') return { kind: 'entrance', sides: '' }
|
if (rest === '') return { kind: 'entrance', sides: '' }
|
||||||
for (const [prefix, kind] of KIND_PREFIXES) {
|
for (const [prefix, kind] of KIND_PREFIXES) {
|
||||||
if (!rest.startsWith(prefix)) continue
|
if (!rest.toLowerCase().startsWith(prefix.toLowerCase())) continue
|
||||||
const tail = rest.slice(prefix.length).trim()
|
const tail = rest.slice(prefix.length).trim()
|
||||||
// `Treasure 1` is numbered rather than sided; everything else uses sides.
|
// `Treasure 1` is numbered rather than sided; everything else uses sides.
|
||||||
if (kind === 'treasure' && /^\d+$/.test(tail)) return { kind, sides: '' }
|
if (kind === 'treasure' && /^\d+$/.test(tail)) return { kind, sides: '' }
|
||||||
return { kind, sides: tail }
|
return { kind, sides: tail }
|
||||||
}
|
}
|
||||||
const reversedMatch = /^([NSEW]+)\s+(up|down)$/i.exec(rest)
|
const reversedMatch = /^([NSEW]+)\s+(up|down|waypoint)$/i.exec(rest)
|
||||||
if (reversedMatch !== null) {
|
if (reversedMatch !== null) {
|
||||||
const sides = reversedMatch[1]!.toUpperCase()
|
const sides = reversedMatch[1]!.toUpperCase()
|
||||||
const role = reversedMatch[2]!.toLowerCase()
|
const role = reversedMatch[2]!.toLowerCase()
|
||||||
return { kind: role === 'up' ? 'prev' : 'next', sides }
|
return { kind: role === 'up' ? 'prev' : role === 'down' ? 'next' : 'waypoint', sides }
|
||||||
}
|
}
|
||||||
// Anything left is a bare side token: `W`, `EW`, `NSEW`, ...
|
// Anything left is a bare side token: `W`, `EW`, `NSEW`, ...
|
||||||
if (/^[NSEW]+$/.test(rest)) return { kind: 'room', sides: rest }
|
if (/^[NSEW]+$/.test(rest)) return { kind: 'room', sides: rest }
|
||||||
|
|
@ -395,7 +426,7 @@ export function inferLevelTypeName(pieces: readonly MazePiece[]): string | null
|
||||||
*/
|
*/
|
||||||
interface SpecialStep {
|
interface SpecialStep {
|
||||||
/** Which kind of special piece to place. */
|
/** Which kind of special piece to place. */
|
||||||
readonly kind: 'prev' | 'next' | 'down' | 'quest' | 'treasure'
|
readonly kind: 'prev' | 'next' | 'down' | 'quest' | 'treasure' | 'waypoint'
|
||||||
/** Side token of the plain room this step replaces, e.g. `N`. */
|
/** Side token of the plain room this step replaces, e.g. `N`. */
|
||||||
readonly plainSides: string
|
readonly plainSides: string
|
||||||
/** Side token of the special piece. */
|
/** Side token of the special piece. */
|
||||||
|
|
@ -534,7 +565,7 @@ export const MAZE_LEVEL_TYPE_PROFILES: readonly MazeLevelProfile[] = [
|
||||||
theme: true,
|
theme: true,
|
||||||
specials: [
|
specials: [
|
||||||
{ kind: 'prev', entries: entries() },
|
{ kind: 'prev', entries: entries() },
|
||||||
{ kind: 'treasure', entries: entries(), levelIds: [29] },
|
{ kind: 'waypoint', entries: entries(), levelIds: [29] },
|
||||||
{ kind: 'quest', entries: entries(), levelIds: [30] },
|
{ kind: 'quest', entries: entries(), levelIds: [30] },
|
||||||
// D2MOO's `else` hangs off the `JAILLEV3` test, so levels 1 and 2 get a
|
// D2MOO's `else` hangs off the `JAILLEV3` test, so levels 1 and 2 get a
|
||||||
// `Next` staircase and level 3 gets the Cathedral descent instead.
|
// `Next` staircase and level 3 gets the Cathedral descent instead.
|
||||||
|
|
@ -549,7 +580,7 @@ export const MAZE_LEVEL_TYPE_PROFILES: readonly MazeLevelProfile[] = [
|
||||||
opening: 'catacombs',
|
opening: 'catacombs',
|
||||||
specials: [
|
specials: [
|
||||||
{ kind: 'next', entries: entries() },
|
{ kind: 'next', entries: entries() },
|
||||||
{ kind: 'treasure', entries: entries(), levelIds: [35] },
|
{ kind: 'waypoint', entries: entries(), levelIds: [35] },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -560,6 +591,7 @@ export const MAZE_LEVEL_TYPE_PROFILES: readonly MazeLevelProfile[] = [
|
||||||
specials: [
|
specials: [
|
||||||
{ kind: 'prev', entries: entries() },
|
{ kind: 'prev', entries: entries() },
|
||||||
{ kind: 'next', entries: entries(), levelIds: [47, 48] },
|
{ kind: 'next', entries: entries(), levelIds: [47, 48] },
|
||||||
|
{ kind: 'waypoint', entries: entries(), levelIds: [48] },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -570,6 +602,7 @@ export const MAZE_LEVEL_TYPE_PROFILES: readonly MazeLevelProfile[] = [
|
||||||
singleRoom: { 61: ['Act 2 - Tomb Tainted Sun X'] },
|
singleRoom: { 61: ['Act 2 - Tomb Tainted Sun X'] },
|
||||||
specials: [
|
specials: [
|
||||||
{ kind: 'next', entries: entries(), levelIds: [55, 56, 57, 58] },
|
{ kind: 'next', entries: entries(), levelIds: [55, 56, 57, 58] },
|
||||||
|
{ kind: 'waypoint', entries: entries(), levelIds: [57] },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -600,6 +633,7 @@ export const MAZE_LEVEL_TYPE_PROFILES: readonly MazeLevelProfile[] = [
|
||||||
specials: [
|
specials: [
|
||||||
{ kind: 'prev', entries: entries(), levelIds: [100, 101] },
|
{ kind: 'prev', entries: entries(), levelIds: [100, 101] },
|
||||||
{ kind: 'next', entries: entries(), levelIds: [100, 101] },
|
{ kind: 'next', entries: entries(), levelIds: [100, 101] },
|
||||||
|
{ kind: 'waypoint', entries: entries(), levelIds: [101] },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -681,6 +715,7 @@ export const MAZE_LEVEL_TYPE_PROFILES: readonly MazeLevelProfile[] = [
|
||||||
{ kind: 'prev', entries: entries(), levelIds: [113, 115, 118] },
|
{ kind: 'prev', entries: entries(), levelIds: [113, 115, 118] },
|
||||||
{ kind: 'next', entries: entries(), levelIds: [113, 115, 118] },
|
{ kind: 'next', entries: entries(), levelIds: [113, 115, 118] },
|
||||||
{ kind: 'down', entries: entries(), levelIds: [113, 115, 118] },
|
{ kind: 'down', entries: entries(), levelIds: [113, 115, 118] },
|
||||||
|
{ kind: 'waypoint', entries: entries(), levelIds: [113, 115, 118] },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -711,6 +746,17 @@ export const MAZE_LEVEL_TYPE_PROFILES: readonly MazeLevelProfile[] = [
|
||||||
levelIds: [122, 123],
|
levelIds: [122, 123],
|
||||||
inPlaceOnly: true,
|
inPlaceOnly: true,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
kind: 'waypoint',
|
||||||
|
entries: [
|
||||||
|
{ plainSides: 'NE', sides: 'NE', direction: DIRECTION_NORTH },
|
||||||
|
{ plainSides: 'NW', sides: 'NW', direction: DIRECTION_NORTH },
|
||||||
|
{ plainSides: 'SE', sides: 'SE', direction: DIRECTION_SOUTH },
|
||||||
|
{ plainSides: 'SW', sides: 'SW', direction: DIRECTION_SOUTH },
|
||||||
|
],
|
||||||
|
levelIds: [123],
|
||||||
|
inPlaceOnly: true,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -720,6 +766,7 @@ export const MAZE_LEVEL_TYPE_PROFILES: readonly MazeLevelProfile[] = [
|
||||||
prevChain: 'baal',
|
prevChain: 'baal',
|
||||||
specials: [
|
specials: [
|
||||||
{ kind: 'next', entries: entries(), levelIds: [128, 129, 130] },
|
{ kind: 'next', entries: entries(), levelIds: [128, 129, 130] },
|
||||||
|
{ kind: 'waypoint', entries: entries(), levelIds: [129] },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{ levelTypeName: 'Act 5 - Lava', grow: false, theme: false, lavaPairs: true, specials: [] },
|
{ levelTypeName: 'Act 5 - Lava', grow: false, theme: false, lavaPairs: true, specials: [] },
|
||||||
|
|
@ -888,6 +935,8 @@ interface MazeStats {
|
||||||
placementAttempts: number
|
placementAttempts: number
|
||||||
/** Stair rooms, with the coordinates only `stamp` can work out. */
|
/** Stair rooms, with the coordinates only `stamp` can work out. */
|
||||||
warps: MazeWarp[]
|
warps: MazeWarp[]
|
||||||
|
/** Waypoint rooms, with the coordinates and entity positions only `stamp` can work out. */
|
||||||
|
waypoints: MazeWaypoint[]
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -1630,7 +1679,7 @@ function placeSpecialGroup(
|
||||||
index: PieceIndex,
|
index: PieceIndex,
|
||||||
stats: MazeStats,
|
stats: MazeStats,
|
||||||
): void {
|
): void {
|
||||||
const isExit = group.kind === 'next' || group.kind === 'down' || group.kind === 'quest'
|
const isExit = group.kind === 'next' || group.kind === 'down' || group.kind === 'quest' || group.kind === 'waypoint'
|
||||||
const distMap = bfsDistances(level)
|
const distMap = bfsDistances(level)
|
||||||
|
|
||||||
// Phase 1: Try to replace an existing unfinished room in place, preferring
|
// Phase 1: Try to replace an existing unfinished room in place, preferring
|
||||||
|
|
@ -1871,6 +1920,36 @@ function stamp(level: MazeLevel, stats: MazeStats): Ds1 {
|
||||||
centreY: originY + Math.floor(variant.height / 2),
|
centreY: originY + Math.floor(variant.height / 2),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
if (piece.kind === 'waypoint') {
|
||||||
|
const wpObj = variant.objects.find(obj => obj.type === 2 && (obj.id === 119 || obj.id === 145 || obj.id === 156 || obj.id === 157 || obj.id === 237 || obj.id === 238 || obj.id === 288 || obj.id === 323 || obj.id === 324 || obj.id === 398 || obj.id === 402 || obj.id === 429)) ?? variant.objects.find(obj => obj.type === 2)
|
||||||
|
let entityX: number
|
||||||
|
let entityY: number
|
||||||
|
if (wpObj !== undefined) {
|
||||||
|
entityX = originX * SUB_TILES_PER_TILE + wpObj.x
|
||||||
|
entityY = originY * SUB_TILES_PER_TILE + wpObj.y
|
||||||
|
} else {
|
||||||
|
entityX = (originX + Math.floor(variant.width / 2)) * SUB_TILES_PER_TILE + 2
|
||||||
|
entityY = (originY + Math.floor(variant.height / 2)) * SUB_TILES_PER_TILE + 2
|
||||||
|
objects.push({
|
||||||
|
type: 2,
|
||||||
|
id: 119,
|
||||||
|
x: entityX,
|
||||||
|
y: entityY,
|
||||||
|
flags: 0,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
stats.waypoints.push({
|
||||||
|
room: room.id,
|
||||||
|
x: originX,
|
||||||
|
y: originY,
|
||||||
|
width: variant.width,
|
||||||
|
height: variant.height,
|
||||||
|
centreX: originX + Math.floor(variant.width / 2),
|
||||||
|
centreY: originY + Math.floor(variant.height / 2),
|
||||||
|
entityX,
|
||||||
|
entityY,
|
||||||
|
})
|
||||||
|
}
|
||||||
stampedRooms += 1
|
stampedRooms += 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1960,7 +2039,7 @@ export function generateMaze(request: MazeRequest): MazeResult {
|
||||||
|
|
||||||
const stats: MazeStats = {
|
const stats: MazeStats = {
|
||||||
patterns: {}, fallbacks: [], specialsApplied: [], unresolvedRoles: [], notes: [],
|
patterns: {}, fallbacks: [], specialsApplied: [], unresolvedRoles: [], notes: [],
|
||||||
ringRooms: 0, placementAttempts: 0, warps: [],
|
ringRooms: 0, placementAttempts: 0, warps: [], waypoints: [],
|
||||||
}
|
}
|
||||||
const rng = new Rng(request.seed)
|
const rng = new Rng(request.seed)
|
||||||
const mergePerMille = Math.max(0, Math.min(1000, Math.floor(request.merge)))
|
const mergePerMille = Math.max(0, Math.min(1000, Math.floor(request.merge)))
|
||||||
|
|
@ -2025,8 +2104,16 @@ export function generateMaze(request: MazeRequest): MazeResult {
|
||||||
// One counter shared by every table, advanced once per call that happens.
|
// One counter shared by every table, advanced once per call that happens.
|
||||||
let cursor = rng.int(0, 3)
|
let cursor = rng.int(0, 3)
|
||||||
for (const group of profile.specials) {
|
for (const group of profile.specials) {
|
||||||
if (group.levelIds !== undefined && !group.levelIds.includes(request.levelId)) continue
|
if (group.kind === 'waypoint') {
|
||||||
if (group.exceptLevelIds !== undefined && group.exceptLevelIds.includes(request.levelId)) continue
|
if (request.hasWaypoint === false) continue
|
||||||
|
if (request.hasWaypoint !== true) {
|
||||||
|
if (group.levelIds !== undefined && !group.levelIds.includes(request.levelId)) continue
|
||||||
|
if (group.exceptLevelIds !== undefined && group.exceptLevelIds.includes(request.levelId)) continue
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (group.levelIds !== undefined && !group.levelIds.includes(request.levelId)) continue
|
||||||
|
if (group.exceptLevelIds !== undefined && group.exceptLevelIds.includes(request.levelId)) continue
|
||||||
|
}
|
||||||
placeSpecialGroup(level, request.pieces, group, cursor, index, stats)
|
placeSpecialGroup(level, request.pieces, group, cursor, index, stats)
|
||||||
cursor = (cursor + 1) % 4
|
cursor = (cursor + 1) % 4
|
||||||
}
|
}
|
||||||
|
|
@ -2066,6 +2153,14 @@ export function generateMaze(request: MazeRequest): MazeResult {
|
||||||
fallbacks: stats.fallbacks,
|
fallbacks: stats.fallbacks,
|
||||||
specialsApplied: stats.specialsApplied,
|
specialsApplied: stats.specialsApplied,
|
||||||
warps: stats.warps,
|
warps: stats.warps,
|
||||||
|
waypoints: stats.waypoints,
|
||||||
|
waypoint: stats.waypoints[0] ?? null,
|
||||||
|
waypointTile: stats.waypoints[0]
|
||||||
|
? {
|
||||||
|
x: Math.floor(stats.waypoints[0].entityX / SUB_TILES_PER_TILE),
|
||||||
|
y: Math.floor(stats.waypoints[0].entityY / SUB_TILES_PER_TILE),
|
||||||
|
}
|
||||||
|
: null,
|
||||||
unresolvedRoles: stats.unresolvedRoles,
|
unresolvedRoles: stats.unresolvedRoles,
|
||||||
unimplementedPasses: UNIMPLEMENTED_PASSES,
|
unimplementedPasses: UNIMPLEMENTED_PASSES,
|
||||||
notes: stats.notes,
|
notes: stats.notes,
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,528 @@
|
||||||
|
import { describe, expect, test } from 'vitest'
|
||||||
|
import type { Ds1, Ds1Cell, Ds1Object } from '../src/formats/ds1.ts'
|
||||||
|
import {
|
||||||
|
classifyMazePieceName,
|
||||||
|
generateMaze,
|
||||||
|
SUB_TILES_PER_TILE,
|
||||||
|
} from '../src/game/maze.ts'
|
||||||
|
import type { MazePiece, MazeRequest } from '../src/game/maze.ts'
|
||||||
|
|
||||||
|
function createMockDs1(width = 8, height = 8, objects: Ds1Object[] = []): Ds1 {
|
||||||
|
const cells: Ds1Cell[][] = []
|
||||||
|
for (let y = 0; y < height; y += 1) {
|
||||||
|
const row: Ds1Cell[] = []
|
||||||
|
for (let x = 0; x < width; x += 1) {
|
||||||
|
row.push({
|
||||||
|
walls: [{ prop1: 2, sequence: 0, style: 1, type: 0, unknown1: 0, unknown2: 0, hidden: false }],
|
||||||
|
floors: [{ prop1: 2, sequence: 0, style: 1, unknown1: 0, unknown2: 0, hidden: false }],
|
||||||
|
shadows: [],
|
||||||
|
substitutions: [],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
cells.push(row)
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
version: 18,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
act: 1,
|
||||||
|
substitutionType: 0,
|
||||||
|
wallLayers: 1,
|
||||||
|
floorLayers: 1,
|
||||||
|
cells,
|
||||||
|
objects,
|
||||||
|
npcPathOffset: null,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function createPiece(
|
||||||
|
name: string,
|
||||||
|
levelTypeName: string,
|
||||||
|
ds1Objects: Ds1Object[] = [],
|
||||||
|
width = 8,
|
||||||
|
height = 8,
|
||||||
|
): MazePiece {
|
||||||
|
const classified = classifyMazePieceName(name, levelTypeName)
|
||||||
|
if (!classified) throw new Error(`Could not classify ${name} for ${levelTypeName}`)
|
||||||
|
return {
|
||||||
|
name,
|
||||||
|
kind: classified.kind,
|
||||||
|
sides: classified.sides,
|
||||||
|
levels: [createMockDs1(width, height, ds1Objects)],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function createCatacombsPieces(wpObjects: Ds1Object[] = []): MazePiece[] {
|
||||||
|
const lt = 'Act 1 - Catacombs'
|
||||||
|
const sided = [
|
||||||
|
'NSEW', 'NSE', 'NSW', 'NEW', 'SEW', 'NS', 'EW', 'NE', 'NW', 'SE', 'SW', 'N', 'S', 'E', 'W',
|
||||||
|
]
|
||||||
|
const pieces: MazePiece[] = []
|
||||||
|
for (const s of sided) {
|
||||||
|
pieces.push(createPiece(`${lt} ${s}`, lt))
|
||||||
|
}
|
||||||
|
pieces.push(createPiece(`${lt} Prev EW`, lt))
|
||||||
|
pieces.push(createPiece(`${lt} Prev NS`, lt))
|
||||||
|
for (const s of ['N', 'E', 'S', 'W']) {
|
||||||
|
pieces.push(createPiece(`${lt} Next ${s}`, lt))
|
||||||
|
pieces.push(createPiece(`${lt} Waypoint ${s}`, lt, wpObjects))
|
||||||
|
}
|
||||||
|
return pieces
|
||||||
|
}
|
||||||
|
|
||||||
|
function createJailPieces(wpObjects: Ds1Object[] = []): MazePiece[] {
|
||||||
|
const lt = 'Act 1 - Jail'
|
||||||
|
const sided = [
|
||||||
|
'NSEW', 'NSE', 'NSW', 'NEW', 'SEW', 'NS', 'EW', 'NE', 'NW', 'SE', 'SW', 'N', 'S', 'E', 'W',
|
||||||
|
]
|
||||||
|
const pieces: MazePiece[] = []
|
||||||
|
for (const s of sided) {
|
||||||
|
pieces.push(createPiece(`${lt} ${s}`, lt))
|
||||||
|
}
|
||||||
|
for (const s of ['N', 'E', 'S', 'W']) {
|
||||||
|
pieces.push(createPiece(`${lt} Prev ${s}`, lt))
|
||||||
|
pieces.push(createPiece(`${lt} Next ${s}`, lt))
|
||||||
|
pieces.push(createPiece(`${lt} Waypoint ${s}`, lt, wpObjects))
|
||||||
|
}
|
||||||
|
return pieces
|
||||||
|
}
|
||||||
|
|
||||||
|
function createKurastPieces(): MazePiece[] {
|
||||||
|
const lt = 'Act 3 - Kurast'
|
||||||
|
const sided = [
|
||||||
|
'NSEW', 'NSE', 'NSW', 'NEW', 'SEW', 'NS', 'EW', 'NE', 'NW', 'SE', 'SW', 'N', 'S', 'E', 'W',
|
||||||
|
]
|
||||||
|
const pieces: MazePiece[] = []
|
||||||
|
for (const s of sided) {
|
||||||
|
pieces.push(createPiece(`${lt} ${s}`, lt))
|
||||||
|
}
|
||||||
|
for (const s of ['N', 'E', 'S', 'W']) {
|
||||||
|
pieces.push(createPiece(`Act 3 - Mephisto Prev ${s}`, lt))
|
||||||
|
pieces.push(createPiece(`Act 3 - Mephisto Next ${s}`, lt))
|
||||||
|
pieces.push(createPiece(`Act 3 - Mephisto Waypoint ${s}`, lt))
|
||||||
|
}
|
||||||
|
return pieces
|
||||||
|
}
|
||||||
|
|
||||||
|
function createBaalPieces(): MazePiece[] {
|
||||||
|
const lt = 'Act 5 - Baal'
|
||||||
|
const sided = [
|
||||||
|
'NSEW', 'NSE', 'NSW', 'NEW', 'SEW', 'NS', 'EW', 'NE', 'NW', 'SE', 'SW', 'N', 'S', 'E', 'W',
|
||||||
|
]
|
||||||
|
const pieces: MazePiece[] = []
|
||||||
|
for (const s of sided) {
|
||||||
|
pieces.push(createPiece(`${lt} ${s}`, lt))
|
||||||
|
}
|
||||||
|
// prevChain requires Prev NSE, SEW, NSW, NEW
|
||||||
|
for (const s of ['NSE', 'SEW', 'NSW', 'NEW']) {
|
||||||
|
pieces.push(createPiece(`${lt} Prev ${s}`, lt))
|
||||||
|
}
|
||||||
|
for (const s of ['N', 'E', 'S', 'W']) {
|
||||||
|
pieces.push(createPiece(`${lt} Next ${s}`, lt))
|
||||||
|
pieces.push(createPiece(`${lt} Waypoint ${s}`, lt))
|
||||||
|
}
|
||||||
|
return pieces
|
||||||
|
}
|
||||||
|
|
||||||
|
function createSewerPieces(): MazePiece[] {
|
||||||
|
const lt = 'Act 2 - Sewer'
|
||||||
|
const sided = [
|
||||||
|
'NSEW', 'NSE', 'NSW', 'NEW', 'SEW', 'NS', 'EW', 'NE', 'NW', 'SE', 'SW', 'N', 'S', 'E', 'W',
|
||||||
|
]
|
||||||
|
const pieces: MazePiece[] = []
|
||||||
|
for (const s of sided) {
|
||||||
|
pieces.push(createPiece(`${lt} ${s}`, lt))
|
||||||
|
}
|
||||||
|
for (const s of ['N', 'E', 'S', 'W']) {
|
||||||
|
pieces.push(createPiece(`${lt} Prev ${s}`, lt))
|
||||||
|
pieces.push(createPiece(`${lt} Next ${s}`, lt))
|
||||||
|
pieces.push(createPiece(`${lt} Waypoint ${s}`, lt))
|
||||||
|
}
|
||||||
|
return pieces
|
||||||
|
}
|
||||||
|
|
||||||
|
function createTombPieces(): MazePiece[] {
|
||||||
|
const lt = 'Act 2 - Tomb'
|
||||||
|
const sided = [
|
||||||
|
'NSEW', 'NSE', 'NSW', 'NEW', 'SEW', 'NS', 'EW', 'NE', 'NW', 'SE', 'SW', 'N', 'S', 'E', 'W',
|
||||||
|
]
|
||||||
|
const pieces: MazePiece[] = []
|
||||||
|
for (const s of sided) {
|
||||||
|
pieces.push(createPiece(`${lt} ${s}`, lt))
|
||||||
|
}
|
||||||
|
for (const s of ['NSE', 'SEW', 'NSW', 'NEW']) {
|
||||||
|
pieces.push(createPiece(`${lt} Prev ${s}`, lt))
|
||||||
|
}
|
||||||
|
for (const s of ['N', 'E', 'S', 'W']) {
|
||||||
|
pieces.push(createPiece(`${lt} Next ${s}`, lt))
|
||||||
|
pieces.push(createPiece(`${lt} Waypoint ${s}`, lt))
|
||||||
|
}
|
||||||
|
return pieces
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('Issue #47: Maze Waypoint preset replacement pass', () => {
|
||||||
|
describe('classifyMazePieceName', () => {
|
||||||
|
test('classifies waypoint pieces for all acts correctly', () => {
|
||||||
|
expect(classifyMazePieceName('Act 1 - Jail Waypoint W', 'Act 1 - Jail')).toEqual({
|
||||||
|
kind: 'waypoint',
|
||||||
|
sides: 'W',
|
||||||
|
})
|
||||||
|
expect(classifyMazePieceName('Act 1 - Catacombs Waypoint N', 'Act 1 - Catacombs')).toEqual({
|
||||||
|
kind: 'waypoint',
|
||||||
|
sides: 'N',
|
||||||
|
})
|
||||||
|
expect(classifyMazePieceName('Act 2 - Sewer Waypoint S', 'Act 2 - Sewer')).toEqual({
|
||||||
|
kind: 'waypoint',
|
||||||
|
sides: 'S',
|
||||||
|
})
|
||||||
|
expect(classifyMazePieceName('Act 2 - Tomb Waypoint E', 'Act 2 - Tomb')).toEqual({
|
||||||
|
kind: 'waypoint',
|
||||||
|
sides: 'E',
|
||||||
|
})
|
||||||
|
expect(classifyMazePieceName('Act 3 - Mephisto Waypoint W', 'Act 3 - Kurast')).toEqual({
|
||||||
|
kind: 'waypoint',
|
||||||
|
sides: 'W',
|
||||||
|
})
|
||||||
|
expect(classifyMazePieceName('Act 5 - Ice waypoint N', 'Act 5 - Ice Caves')).toEqual({
|
||||||
|
kind: 'waypoint',
|
||||||
|
sides: 'N',
|
||||||
|
})
|
||||||
|
expect(classifyMazePieceName('Act 5 - Temple NE Waypoint', 'Act 5 - Temple')).toEqual({
|
||||||
|
kind: 'waypoint',
|
||||||
|
sides: 'NE',
|
||||||
|
})
|
||||||
|
expect(classifyMazePieceName('Act 5 - Temple SW Waypoint', 'Act 5 - Temple')).toEqual({
|
||||||
|
kind: 'waypoint',
|
||||||
|
sides: 'SW',
|
||||||
|
})
|
||||||
|
expect(classifyMazePieceName('Act 5 - Baal Waypoint N', 'Act 5 - Baal')).toEqual({
|
||||||
|
kind: 'waypoint',
|
||||||
|
sides: 'N',
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test('preserves classification of other piece kinds', () => {
|
||||||
|
expect(classifyMazePieceName('Act 1 - Jail Prev W', 'Act 1 - Jail')).toEqual({
|
||||||
|
kind: 'prev',
|
||||||
|
sides: 'W',
|
||||||
|
})
|
||||||
|
expect(classifyMazePieceName('Act 1 - Catacombs Next S', 'Act 1 - Catacombs')).toEqual({
|
||||||
|
kind: 'next',
|
||||||
|
sides: 'S',
|
||||||
|
})
|
||||||
|
expect(classifyMazePieceName('Act 1 - Cave NSEW', 'Act 1 - Cave')).toEqual({
|
||||||
|
kind: 'room',
|
||||||
|
sides: 'NSEW',
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('generateMaze waypoint placement', () => {
|
||||||
|
test('places waypoint room in Catacombs 2 (level 35) and records stats', () => {
|
||||||
|
const pieces = createCatacombsPieces()
|
||||||
|
const req: MazeRequest = {
|
||||||
|
levelId: 35,
|
||||||
|
levelName: 'Catacombs Level 2',
|
||||||
|
levelTypeName: 'Act 1 - Catacombs',
|
||||||
|
sectionSize: 8,
|
||||||
|
minRooms: 10,
|
||||||
|
merge: 100,
|
||||||
|
seed: 42,
|
||||||
|
pieces,
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = generateMaze(req)
|
||||||
|
const waypoints = result.stats.waypoints as unknown[]
|
||||||
|
expect(waypoints).toHaveLength(1)
|
||||||
|
|
||||||
|
const wp = result.stats.waypoint as {
|
||||||
|
room: number
|
||||||
|
x: number
|
||||||
|
y: number
|
||||||
|
width: number
|
||||||
|
height: number
|
||||||
|
centreX: number
|
||||||
|
centreY: number
|
||||||
|
entityX: number
|
||||||
|
entityY: number
|
||||||
|
}
|
||||||
|
expect(wp).not.toBeNull()
|
||||||
|
expect(result.stats.waypointTile).toEqual({
|
||||||
|
x: Math.floor(wp.entityX / SUB_TILES_PER_TILE),
|
||||||
|
y: Math.floor(wp.entityY / SUB_TILES_PER_TILE),
|
||||||
|
})
|
||||||
|
|
||||||
|
const specials = result.stats.specialsApplied as { room: number; kind: string; sides: string }[]
|
||||||
|
const wpSpecial = specials.find(s => s.kind === 'waypoint')
|
||||||
|
expect(wpSpecial).toBeDefined()
|
||||||
|
expect(wpSpecial?.room).toBe(wp.room)
|
||||||
|
|
||||||
|
// Verify synthesized waypoint entity is present in result.level.objects
|
||||||
|
const wpObj = result.level.objects.find(o => o.type === 2 && o.id === 119)
|
||||||
|
expect(wpObj).toBeDefined()
|
||||||
|
expect(wpObj?.x).toBe(wp.entityX)
|
||||||
|
expect(wpObj?.y).toBe(wp.entityY)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('does NOT place waypoint room in Catacombs 1 (level 34)', () => {
|
||||||
|
const pieces = createCatacombsPieces()
|
||||||
|
const req: MazeRequest = {
|
||||||
|
levelId: 34,
|
||||||
|
levelName: 'Catacombs Level 1',
|
||||||
|
levelTypeName: 'Act 1 - Catacombs',
|
||||||
|
sectionSize: 8,
|
||||||
|
minRooms: 10,
|
||||||
|
merge: 100,
|
||||||
|
seed: 42,
|
||||||
|
pieces,
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = generateMaze(req)
|
||||||
|
expect(result.stats.waypoints).toEqual([])
|
||||||
|
expect(result.stats.waypoint).toBeNull()
|
||||||
|
expect(result.stats.waypointTile).toBeNull()
|
||||||
|
|
||||||
|
const specials = result.stats.specialsApplied as { kind: string }[]
|
||||||
|
expect(specials.some(s => s.kind === 'waypoint')).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('places waypoint room in Jail 1 (level 29)', () => {
|
||||||
|
const pieces = createJailPieces()
|
||||||
|
const req: MazeRequest = {
|
||||||
|
levelId: 29,
|
||||||
|
levelName: 'Jail Level 1',
|
||||||
|
levelTypeName: 'Act 1 - Jail',
|
||||||
|
sectionSize: 8,
|
||||||
|
minRooms: 10,
|
||||||
|
merge: 100,
|
||||||
|
seed: 101,
|
||||||
|
pieces,
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = generateMaze(req)
|
||||||
|
expect(result.stats.waypoints).toHaveLength(1)
|
||||||
|
expect(result.stats.waypoint).not.toBeNull()
|
||||||
|
})
|
||||||
|
|
||||||
|
test('honors hasWaypoint override: forces waypoint when true on level without default waypoint', () => {
|
||||||
|
const pieces = createCatacombsPieces()
|
||||||
|
const req: MazeRequest = {
|
||||||
|
levelId: 34,
|
||||||
|
levelName: 'Catacombs Level 1',
|
||||||
|
levelTypeName: 'Act 1 - Catacombs',
|
||||||
|
sectionSize: 8,
|
||||||
|
minRooms: 10,
|
||||||
|
merge: 100,
|
||||||
|
seed: 42,
|
||||||
|
pieces,
|
||||||
|
hasWaypoint: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = generateMaze(req)
|
||||||
|
expect(result.stats.waypoints).toHaveLength(1)
|
||||||
|
expect(result.stats.waypoint).not.toBeNull()
|
||||||
|
})
|
||||||
|
|
||||||
|
test('honors hasWaypoint override: disables waypoint when false on level with default waypoint', () => {
|
||||||
|
const pieces = createCatacombsPieces()
|
||||||
|
const req: MazeRequest = {
|
||||||
|
levelId: 35,
|
||||||
|
levelName: 'Catacombs Level 2',
|
||||||
|
levelTypeName: 'Act 1 - Catacombs',
|
||||||
|
sectionSize: 8,
|
||||||
|
minRooms: 10,
|
||||||
|
merge: 100,
|
||||||
|
seed: 42,
|
||||||
|
pieces,
|
||||||
|
hasWaypoint: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = generateMaze(req)
|
||||||
|
expect(result.stats.waypoints).toEqual([])
|
||||||
|
expect(result.stats.waypoint).toBeNull()
|
||||||
|
})
|
||||||
|
|
||||||
|
test('uses authored DS1 object entity coordinates when present', () => {
|
||||||
|
const authoredObj: Ds1Object = {
|
||||||
|
type: 2,
|
||||||
|
id: 119,
|
||||||
|
x: 18,
|
||||||
|
y: 22,
|
||||||
|
flags: 0,
|
||||||
|
}
|
||||||
|
const pieces = createCatacombsPieces([authoredObj])
|
||||||
|
const req: MazeRequest = {
|
||||||
|
levelId: 35,
|
||||||
|
levelName: 'Catacombs Level 2',
|
||||||
|
levelTypeName: 'Act 1 - Catacombs',
|
||||||
|
sectionSize: 8,
|
||||||
|
minRooms: 10,
|
||||||
|
merge: 100,
|
||||||
|
seed: 88,
|
||||||
|
pieces,
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = generateMaze(req)
|
||||||
|
const wp = result.stats.waypoint as {
|
||||||
|
x: number
|
||||||
|
y: number
|
||||||
|
entityX: number
|
||||||
|
entityY: number
|
||||||
|
}
|
||||||
|
expect(wp.entityX).toBe(wp.x * SUB_TILES_PER_TILE + 18)
|
||||||
|
expect(wp.entityY).toBe(wp.y * SUB_TILES_PER_TILE + 22)
|
||||||
|
|
||||||
|
const objInLevel = result.level.objects.find(o => o.type === 2 && o.id === 119)
|
||||||
|
expect(objInLevel).toBeDefined()
|
||||||
|
expect(objInLevel?.x).toBe(wp.entityX)
|
||||||
|
expect(objInLevel?.y).toBe(wp.entityY)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('places waypoint in Durance of Hate 2 (101) but not Durance 1 (100)', () => {
|
||||||
|
const pieces = createKurastPieces()
|
||||||
|
const req101: MazeRequest = {
|
||||||
|
levelId: 101,
|
||||||
|
levelName: 'Durance of Hate Level 2',
|
||||||
|
levelTypeName: 'Act 3 - Kurast',
|
||||||
|
sectionSize: 8,
|
||||||
|
minRooms: 10,
|
||||||
|
merge: 100,
|
||||||
|
seed: 777,
|
||||||
|
pieces,
|
||||||
|
}
|
||||||
|
const res101 = generateMaze(req101)
|
||||||
|
expect(res101.stats.waypoints).toHaveLength(1)
|
||||||
|
expect(res101.stats.waypoint).not.toBeNull()
|
||||||
|
|
||||||
|
const req100: MazeRequest = {
|
||||||
|
levelId: 100,
|
||||||
|
levelName: 'Durance of Hate Level 1',
|
||||||
|
levelTypeName: 'Act 3 - Kurast',
|
||||||
|
sectionSize: 8,
|
||||||
|
minRooms: 10,
|
||||||
|
merge: 100,
|
||||||
|
seed: 777,
|
||||||
|
pieces,
|
||||||
|
}
|
||||||
|
const res100 = generateMaze(req100)
|
||||||
|
expect(res100.stats.waypoints).toEqual([])
|
||||||
|
expect(res100.stats.waypoint).toBeNull()
|
||||||
|
})
|
||||||
|
|
||||||
|
test('places waypoint in Worldstone Keep 2 (129) but not Worldstone 1 (128)', () => {
|
||||||
|
const pieces = createBaalPieces()
|
||||||
|
const req129: MazeRequest = {
|
||||||
|
levelId: 129,
|
||||||
|
levelName: 'Worldstone Keep Level 2',
|
||||||
|
levelTypeName: 'Act 5 - Baal',
|
||||||
|
sectionSize: 8,
|
||||||
|
minRooms: 10,
|
||||||
|
merge: 100,
|
||||||
|
seed: 999,
|
||||||
|
pieces,
|
||||||
|
}
|
||||||
|
const res129 = generateMaze(req129)
|
||||||
|
expect(res129.stats.waypoints).toHaveLength(1)
|
||||||
|
expect(res129.stats.waypoint).not.toBeNull()
|
||||||
|
|
||||||
|
const req128: MazeRequest = {
|
||||||
|
levelId: 128,
|
||||||
|
levelName: 'Worldstone Keep Level 1',
|
||||||
|
levelTypeName: 'Act 5 - Baal',
|
||||||
|
sectionSize: 8,
|
||||||
|
minRooms: 10,
|
||||||
|
merge: 100,
|
||||||
|
seed: 999,
|
||||||
|
pieces,
|
||||||
|
}
|
||||||
|
const res128 = generateMaze(req128)
|
||||||
|
expect(res128.stats.waypoints).toEqual([])
|
||||||
|
expect(res128.stats.waypoint).toBeNull()
|
||||||
|
})
|
||||||
|
|
||||||
|
test('places waypoint in Lut Gholein Sewer 2 (48) but not Sewer 1 (47)', () => {
|
||||||
|
const pieces = createSewerPieces()
|
||||||
|
const req48: MazeRequest = {
|
||||||
|
levelId: 48,
|
||||||
|
levelName: 'Sewers Level 2',
|
||||||
|
levelTypeName: 'Act 2 - Sewer',
|
||||||
|
sectionSize: 8,
|
||||||
|
minRooms: 10,
|
||||||
|
merge: 100,
|
||||||
|
seed: 333,
|
||||||
|
pieces,
|
||||||
|
}
|
||||||
|
const res48 = generateMaze(req48)
|
||||||
|
expect(res48.stats.waypoints).toHaveLength(1)
|
||||||
|
expect(res48.stats.waypoint).not.toBeNull()
|
||||||
|
|
||||||
|
const req47: MazeRequest = {
|
||||||
|
levelId: 47,
|
||||||
|
levelName: 'Sewers Level 1',
|
||||||
|
levelTypeName: 'Act 2 - Sewer',
|
||||||
|
sectionSize: 8,
|
||||||
|
minRooms: 10,
|
||||||
|
merge: 100,
|
||||||
|
seed: 333,
|
||||||
|
pieces,
|
||||||
|
}
|
||||||
|
const res47 = generateMaze(req47)
|
||||||
|
expect(res47.stats.waypoints).toEqual([])
|
||||||
|
expect(res47.stats.waypoint).toBeNull()
|
||||||
|
})
|
||||||
|
|
||||||
|
test('places waypoint in Halls of the Dead 2 (57) but not Tomb 1 (56)', () => {
|
||||||
|
const pieces = createTombPieces()
|
||||||
|
const req57: MazeRequest = {
|
||||||
|
levelId: 57,
|
||||||
|
levelName: 'Halls of the Dead Level 2',
|
||||||
|
levelTypeName: 'Act 2 - Tomb',
|
||||||
|
sectionSize: 8,
|
||||||
|
minRooms: 10,
|
||||||
|
merge: 100,
|
||||||
|
seed: 444,
|
||||||
|
pieces,
|
||||||
|
}
|
||||||
|
const res57 = generateMaze(req57)
|
||||||
|
expect(res57.stats.waypoints).toHaveLength(1)
|
||||||
|
expect(res57.stats.waypoint).not.toBeNull()
|
||||||
|
|
||||||
|
const req56: MazeRequest = {
|
||||||
|
levelId: 56,
|
||||||
|
levelName: 'Halls of the Dead Level 1',
|
||||||
|
levelTypeName: 'Act 2 - Tomb',
|
||||||
|
sectionSize: 8,
|
||||||
|
minRooms: 10,
|
||||||
|
merge: 100,
|
||||||
|
seed: 444,
|
||||||
|
pieces,
|
||||||
|
}
|
||||||
|
const res56 = generateMaze(req56)
|
||||||
|
expect(res56.stats.waypoints).toEqual([])
|
||||||
|
expect(res56.stats.waypoint).toBeNull()
|
||||||
|
})
|
||||||
|
|
||||||
|
test('places waypoint at a distance from the entrance (isExit distance sorting)', () => {
|
||||||
|
const pieces = createCatacombsPieces()
|
||||||
|
const req: MazeRequest = {
|
||||||
|
levelId: 35,
|
||||||
|
levelName: 'Catacombs Level 2',
|
||||||
|
levelTypeName: 'Act 1 - Catacombs',
|
||||||
|
sectionSize: 8,
|
||||||
|
minRooms: 12,
|
||||||
|
merge: 0,
|
||||||
|
seed: 12345,
|
||||||
|
pieces,
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = generateMaze(req)
|
||||||
|
const wp = result.stats.waypoint as { room: number }
|
||||||
|
const specials = result.stats.specialsApplied as { room: number; kind: string }[]
|
||||||
|
const prevSpecial = specials.find(s => s.kind === 'prev')
|
||||||
|
if (prevSpecial) {
|
||||||
|
// Waypoint room should not be placed in the entrance room
|
||||||
|
expect(wp.room).not.toBe(prevSpecial.room)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
Loading…
Reference in New Issue