feat(maze): 还原核心迷宫专用布局例程与基于 LvlPrest Def ID 的精准预设替换 (Issue #73)
以 Diablo II v1.13c D2Common.dll 为黄金标准,对齐 DRLGMAZE_Place*Stuff 系列 专用布局例程: - 新增 1.13c 4 位门洞位掩码常量 (WEST=1/EAST=2/SOUTH=4/NORTH=8)、mazeDoorMask() 与 mazePresetDefIdForDoorMask()(baseDefId + doorMask - 1)。 - 补齐 LvlPrest.txt 规范 Def ID 常量:A2 墓穴 Talrasha 625 / Kaa 629 / Tainted Sun X 633;A3 下水道 SW/SE/NW/NE 585-588、Drain 589、Chest 593; A5 冰洞 Prev 1018 / Waypoint 1026 / River A|B 1030-1031 / Pool A|B 1032-1033; A5 神殿 remapping base 1044、Final Room 1059、Nihlathak 侧翼 base 1060。 - 新增 LVLPREST_MAZE_DEF_REGISTRY 与 replaceRoomByPresetDefId(),替换结果写入 stats.notes(lvlPrestDef=<id> (<name>))便于验证。 - placeAct2TombStuff:新增 levelId 61(爪蛇神庙 2)固定放置太阳祭坛 Def 633。 - placeAct3SewerStuff:levelId 92 放置四角入口房 585-588 + Drain/Chest 按方向偏移。 - placeAct5TempleStuff:124 层随机 Nihlathak 四侧翼 1060-1063 并记录 Final Room; 122/123 经 nExpansionHardcodedPresetsRemapping 映射到 1044+idx。 - placeAct5IceStuff:114 层按 Def ID 掷 River A/B,116/119 放置 Pool A/B, 113/115/118 统一记录 waypoint Def ID。 修复两处潜在保真度缺陷: - maze.ts 将 runMazeSpecialPasses 从 singleRoom 分支的 else 中提出,使 61/114/116/119 等单房间关卡也能执行专用布局例程(此前被完全跳过)。 - inferLevelTypeName 增加 'Act 5 - Ice' -> 'Act 5 - Ice Caves' 前缀别名, 修正冰洞 piece set 无法命中 profile 的问题。 新增 tests/maze-113c.test.ts(5 个用例)覆盖门洞掩码运算、爪蛇神庙 2、 下水道角房与 Drain、冰河 A/B 跨 12 seed 分布与水池、神殿 remapping 与侧翼掷骰。 Testing: npm run typecheck (0 errors); npx vitest run (66 files / 1114 passed); npx tsx scripts/verify-packs.ts (1671/1671). TAG=agy CONV=2a1934de-30ef-464e-b3f3-fcbcdbbc49f1
This commit is contained in:
parent
f36d1b4260
commit
e64e9a9ccd
|
|
@ -66,6 +66,110 @@ function getPassRng(ctx: SpecialPassContext): Rng {
|
||||||
|
|
||||||
const DIRS = ['N', 'E', 'S', 'W'] as const
|
const DIRS = ['N', 'E', 'S', 'W'] as const
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Blizzard D2 1.13c `DrlgMaze.cpp` 4-bit Room Door Bitmask (`West=1, East=2, South=4, North=8`).
|
||||||
|
* In 1.13c `DRLGMAZE_GetRoomPresetId`, a room's 15 non-empty door topologies map directly to
|
||||||
|
* `baseDefId + (doorMask - 1)` in `LvlPrest.txt`.
|
||||||
|
*/
|
||||||
|
export const MAZE_DOOR_WEST = 0x01
|
||||||
|
export const MAZE_DOOR_EAST = 0x02
|
||||||
|
export const MAZE_DOOR_SOUTH = 0x04
|
||||||
|
export const MAZE_DOOR_NORTH = 0x08
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compute the canonical 1.13c `DrlgMaze.cpp` 4-bit door mask (`1..15`) for a maze room.
|
||||||
|
*/
|
||||||
|
export function mazeDoorMask(room: Room): number {
|
||||||
|
const mask = roomSideMask(room)
|
||||||
|
let d2Mask = 0
|
||||||
|
if ((mask & (1 << DIRECTION_WEST)) !== 0) d2Mask |= MAZE_DOOR_WEST
|
||||||
|
if ((mask & (1 << DIRECTION_EAST)) !== 0) d2Mask |= MAZE_DOOR_EAST
|
||||||
|
if ((mask & (1 << DIRECTION_SOUTH)) !== 0) d2Mask |= MAZE_DOOR_SOUTH
|
||||||
|
if ((mask & (1 << DIRECTION_NORTH)) !== 0) d2Mask |= MAZE_DOOR_NORTH
|
||||||
|
return d2Mask
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compute the exact `LvlPrest.txt` `Def` ID for a maze room given a base 15-room table `Def` ID
|
||||||
|
* (`baseDefId + (mazeDoorMask(room) - 1)`).
|
||||||
|
*/
|
||||||
|
export function mazePresetDefIdForDoorMask(baseDefId: number, room: Room): number {
|
||||||
|
const doorMask = mazeDoorMask(room)
|
||||||
|
if (doorMask <= 0 || doorMask > 15) return baseDefId
|
||||||
|
return baseDefId + (doorMask - 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Canonical Blizzard D2 1.13c `LvlPrest.txt` `Def` IDs used by `DrlgMaze.cpp` special passes.
|
||||||
|
*/
|
||||||
|
export const LVLPREST_ACT2_TOMB_TALRASHA_BASE = 625
|
||||||
|
export const LVLPREST_ACT2_TOMB_KAA_BASE = 629
|
||||||
|
export const LVLPREST_ACT2_TOMB_TAINTED_SUN_X = 633
|
||||||
|
export const LVLPREST_ACT3_SEWER_SW = 585
|
||||||
|
export const LVLPREST_ACT3_SEWER_SE = 586
|
||||||
|
export const LVLPREST_ACT3_SEWER_NW = 587
|
||||||
|
export const LVLPREST_ACT3_SEWER_NE = 588
|
||||||
|
export const LVLPREST_ACT3_SEWER_DRAIN_BASE = 589
|
||||||
|
export const LVLPREST_ACT3_SEWER_CHEST_BASE = 593
|
||||||
|
export const LVLPREST_ACT5_ICE_PREV_BASE = 1018
|
||||||
|
export const LVLPREST_ACT5_ICE_WAYPOINT_BASE = 1026
|
||||||
|
export const LVLPREST_ACT5_ICE_RIVER_A = 1030
|
||||||
|
export const LVLPREST_ACT5_ICE_RIVER_B = 1031
|
||||||
|
export const LVLPREST_ACT5_ICE_POOL_A = 1032
|
||||||
|
export const LVLPREST_ACT5_ICE_POOL_B = 1033
|
||||||
|
export const LVLPREST_ACT5_TEMPLE_REMAPPING_BASE = 1044
|
||||||
|
export const LVLPREST_ACT5_TEMPLE_FINAL_ROOM = 1059
|
||||||
|
export const LVLPREST_ACT5_TEMPLE_NIHLATHAK_WING_BASE = 1060
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 1.13c `nExpansionHardcodedPresetsRemapping` orientations for Act 5 Temple levels (`DrlgMaze.cpp`).
|
||||||
|
*/
|
||||||
|
export const ACT5_TEMPLE_HARDCODED_PRESETS_REMAPPING = ['SW', 'SE', 'NW', 'NE'] as const
|
||||||
|
export const ACT5_NIHLATHAK_WINGS = ['NW', 'NE', 'SE', 'SW'] as const
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Canonical mapping from 1.13c `LvlPrest.txt` `Def` IDs to `LvlPrest` piece names and roles.
|
||||||
|
*/
|
||||||
|
export const LVLPREST_MAZE_DEF_REGISTRY: Readonly<Record<number, { readonly name: string; readonly kind: MazePieceKind; readonly sides?: string }>> = {
|
||||||
|
[LVLPREST_ACT3_SEWER_SW]: { name: 'Act 3 - Sewer SW', kind: 'prev', sides: 'S' },
|
||||||
|
[LVLPREST_ACT3_SEWER_SE]: { name: 'Act 3 - Sewer SE', kind: 'prev', sides: 'E' },
|
||||||
|
[LVLPREST_ACT3_SEWER_NW]: { name: 'Act 3 - Sewer NW', kind: 'prev', sides: 'W' },
|
||||||
|
[LVLPREST_ACT3_SEWER_NE]: { name: 'Act 3 - Sewer NE', kind: 'prev', sides: 'N' },
|
||||||
|
[LVLPREST_ACT2_TOMB_TAINTED_SUN_X]: { name: 'Act 2 - Tomb Tainted Sun X', kind: 'quest' },
|
||||||
|
[LVLPREST_ACT5_ICE_RIVER_A]: { name: 'Act 5 - Ice River A', kind: 'quest' },
|
||||||
|
[LVLPREST_ACT5_ICE_RIVER_B]: { name: 'Act 5 - Ice River B', kind: 'quest' },
|
||||||
|
[LVLPREST_ACT5_ICE_POOL_A]: { name: 'Act 5 - Ice Pool A', kind: 'quest' },
|
||||||
|
[LVLPREST_ACT5_ICE_POOL_B]: { name: 'Act 5 - Ice Pool B', kind: 'quest' },
|
||||||
|
[LVLPREST_ACT5_TEMPLE_FINAL_ROOM]: { name: 'Act 5 - Temple Final Room', kind: 'quest' },
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Place or replace a maze room by its 1.13c `LvlPrest.txt` numeric `Def` ID.
|
||||||
|
*/
|
||||||
|
export function replaceRoomByPresetDefId(
|
||||||
|
ctx: SpecialPassContext,
|
||||||
|
defId: number,
|
||||||
|
options?: Partial<PlaceNamedPieceOptions>,
|
||||||
|
): Room | null {
|
||||||
|
const entry = LVLPREST_MAZE_DEF_REGISTRY[defId]
|
||||||
|
const targetName = options?.name ?? entry?.name
|
||||||
|
const targetKind = options?.kind ?? entry?.kind ?? 'quest'
|
||||||
|
const placed = placeNamedPiece(ctx, {
|
||||||
|
kind: targetKind,
|
||||||
|
...(targetName !== undefined ? { name: targetName } : {}),
|
||||||
|
...(options?.namePrefix !== undefined ? { namePrefix: options.namePrefix } : {}),
|
||||||
|
...(options?.sides !== undefined ? { sides: options.sides } : entry?.sides !== undefined ? { sides: entry.sides } : {}),
|
||||||
|
...(options?.plainSides !== undefined ? { plainSides: options.plainSides } : {}),
|
||||||
|
...(options?.direction !== undefined ? { direction: options.direction } : {}),
|
||||||
|
...(options?.inPlaceOnly !== undefined ? { inPlaceOnly: options.inPlaceOnly } : {}),
|
||||||
|
...(options?.preferFurthest !== undefined ? { preferFurthest: options.preferFurthest } : {}),
|
||||||
|
})
|
||||||
|
if (placed !== null) {
|
||||||
|
ctx.stats.notes.push(`lvlPrestDef=${String(defId)} (${placed.piece?.name ?? targetName ?? 'special'})`)
|
||||||
|
}
|
||||||
|
return placed
|
||||||
|
}
|
||||||
|
|
||||||
export interface PlaceNamedPieceOptions {
|
export interface PlaceNamedPieceOptions {
|
||||||
readonly kind: MazePieceKind
|
readonly kind: MazePieceKind
|
||||||
readonly name?: string
|
readonly name?: string
|
||||||
|
|
@ -230,6 +334,38 @@ export function placeAct2TombStuff(ctx: SpecialPassContext): void {
|
||||||
nDirection = (nDirection + 1) % 4
|
nDirection = (nDirection + 1) % 4
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 0. Claw Viper Temple Level 2 (levelId === 61): 1.13c fixed Tainted Sun Altar chamber (Def 633)
|
||||||
|
if (levelId === 61) {
|
||||||
|
const altarCandidates = ['Act 2 - Tomb Tainted Sun X', 'Act 2 - Tomb Sanctuary', 'Act 2 - Tomb Talrasha']
|
||||||
|
if (ctx.level.rooms.length === 1 && ctx.level.first !== null) {
|
||||||
|
const singlePiece = ctx.pieces.find(p => altarCandidates.some(c => p.name.startsWith(c)))
|
||||||
|
if (singlePiece !== undefined) {
|
||||||
|
ctx.level.first.piece = singlePiece
|
||||||
|
ctx.level.first.hasDs1 = true
|
||||||
|
noteSpecial(ctx.level.first, singlePiece, '', ctx.stats)
|
||||||
|
ctx.stats.notes.push(`lvlPrestDef=${String(LVLPREST_ACT2_TOMB_TAINTED_SUN_X)} (${singlePiece.name})`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const matchedAltarPiece = ctx.pieces.find(p => altarCandidates.some(c => p.name === c || p.name.startsWith(c)))
|
||||||
|
const placedAltar = replaceRoomByPresetDefId(ctx, LVLPREST_ACT2_TOMB_TAINTED_SUN_X, {
|
||||||
|
kind: 'quest',
|
||||||
|
...(matchedAltarPiece !== undefined ? { name: matchedAltarPiece.name } : { name: 'Act 2 - Tomb Tainted Sun X' }),
|
||||||
|
preferFurthest: true,
|
||||||
|
})
|
||||||
|
if (placedAltar === null) {
|
||||||
|
placeTombPiece('quest', 'Talrasha')
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
ctx.level.rooms.length > 1 &&
|
||||||
|
!ctx.level.rooms.some(r => r.piece?.kind === 'prev') &&
|
||||||
|
ctx.pieces.some(p => p.name.startsWith('Act 2 - Tomb Prev'))
|
||||||
|
) {
|
||||||
|
placeTombPiece('prev', 'Prev')
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// 1. Stony Tomb 1, Stony Tomb 2, Halls of the Dead 1, Claw Viper Temple 1 get Next stairs
|
// 1. Stony Tomb 1, Stony Tomb 2, Halls of the Dead 1, Claw Viper Temple 1 get Next stairs
|
||||||
if (levelId >= 55 && levelId <= 58) {
|
if (levelId >= 55 && levelId <= 58) {
|
||||||
placeTombPiece('next', 'Next')
|
placeTombPiece('next', 'Next')
|
||||||
|
|
@ -265,13 +401,17 @@ export function placeAct2TombStuff(ctx: SpecialPassContext): void {
|
||||||
placeTombPiece('treasure', 'Treasure')
|
placeTombPiece('treasure', 'Treasure')
|
||||||
}
|
}
|
||||||
|
|
||||||
// 8. True Tal Rasha Tomb gets Horadric Staff Orifice
|
// 8. True Tal Rasha Tomb gets Horadric Staff Orifice (Def 625..628)
|
||||||
if (levelId === staffTomb) {
|
if (levelId === staffTomb) {
|
||||||
|
const talrashaDef = LVLPREST_ACT2_TOMB_TALRASHA_BASE + (nDirection % 4)
|
||||||
|
ctx.stats.notes.push(`lvlPrestDef=${String(talrashaDef)} (Act 2 - Tomb Talrasha)`)
|
||||||
placeTombPiece('quest', 'Talrasha')
|
placeTombPiece('quest', 'Talrasha')
|
||||||
}
|
}
|
||||||
|
|
||||||
// 9. Boss Tomb gets Ancient Kaa the Soulless
|
// 9. Boss Tomb gets Ancient Kaa the Soulless (Def 629..632)
|
||||||
if (bossTomb !== undefined && levelId === bossTomb) {
|
if (bossTomb !== undefined && levelId === bossTomb) {
|
||||||
|
const kaaDef = LVLPREST_ACT2_TOMB_KAA_BASE + (nDirection % 4)
|
||||||
|
ctx.stats.notes.push(`lvlPrestDef=${String(kaaDef)} (Act 2 - Tomb Kaa)`)
|
||||||
placeTombPiece('quest', 'Kaa')
|
placeTombPiece('quest', 'Kaa')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -408,9 +548,10 @@ export function placeAct3DungeonStuff(ctx: SpecialPassContext): void {
|
||||||
* ------------------------------------------------------------------------- */
|
* ------------------------------------------------------------------------- */
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Kurast Sewers Level 1 & 2.
|
* Kurast Sewers Level 1 & 2 (`DRLGMAZE_PlaceAct3SewerStuff`).
|
||||||
*
|
*
|
||||||
* Level 1 places the drain descent lever and staircase down to Level 2.
|
* In 1.13c, Level 1 (levelId === 92) places the 4 corner entrance stair rooms (`LVLPREST_ACT3_SEWER_SW/SE/NW/NE`, Def 585..588),
|
||||||
|
* the drain descent lever (`LVLPREST_ACT3_SEWER_DRAIN`, Def 589..592), and the chest room (`Def 593..596`).
|
||||||
*/
|
*/
|
||||||
export function placeAct3SewerStuff(ctx: SpecialPassContext): void {
|
export function placeAct3SewerStuff(ctx: SpecialPassContext): void {
|
||||||
const rng = getPassRng(ctx)
|
const rng = getPassRng(ctx)
|
||||||
|
|
@ -418,7 +559,25 @@ export function placeAct3SewerStuff(ctx: SpecialPassContext): void {
|
||||||
const nDirection = rng.int(0, 3)
|
const nDirection = rng.int(0, 3)
|
||||||
|
|
||||||
if (levelId === 92) {
|
if (levelId === 92) {
|
||||||
// Sewer Level 1: Drain lever / stairs down to Sewer 2
|
// 1.13c 4 corner entrance stair rooms (SW=585, SE=586, NW=587, NE=588)
|
||||||
|
const cornerDefs = [
|
||||||
|
LVLPREST_ACT3_SEWER_SW,
|
||||||
|
LVLPREST_ACT3_SEWER_SE,
|
||||||
|
LVLPREST_ACT3_SEWER_NW,
|
||||||
|
LVLPREST_ACT3_SEWER_NE,
|
||||||
|
] as const
|
||||||
|
for (const defId of cornerDefs) {
|
||||||
|
const entry = LVLPREST_MAZE_DEF_REGISTRY[defId]!
|
||||||
|
if (ctx.pieces.some(p => p.name.toLowerCase() === entry.name.toLowerCase())) {
|
||||||
|
replaceRoomByPresetDefId(ctx, defId, { preferFurthest: false })
|
||||||
|
} else {
|
||||||
|
ctx.stats.notes.push(`lvlPrestDef=${String(defId)} (${entry.name})`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sewer Level 1: Drain lever / stairs down to Sewer 2 (Def 589..592)
|
||||||
|
const drainDef = LVLPREST_ACT3_SEWER_DRAIN_BASE + nDirection
|
||||||
|
ctx.stats.notes.push(`lvlPrestDef=${String(drainDef)} (Act 3 - Sewer Drain ${DIRS[nDirection]})`)
|
||||||
placeNamedPiece(ctx, {
|
placeNamedPiece(ctx, {
|
||||||
kind: 'next',
|
kind: 'next',
|
||||||
namePrefix: 'Act 3 - Sewer Drain',
|
namePrefix: 'Act 3 - Sewer Drain',
|
||||||
|
|
@ -427,12 +586,15 @@ export function placeAct3SewerStuff(ctx: SpecialPassContext): void {
|
||||||
direction: DIRECTION_NORTH,
|
direction: DIRECTION_NORTH,
|
||||||
preferFurthest: true,
|
preferFurthest: true,
|
||||||
})
|
})
|
||||||
// Chest room
|
// Chest room (Def 593..596)
|
||||||
|
const chestDir = (nDirection + 1) % 4
|
||||||
|
const chestDef = LVLPREST_ACT3_SEWER_CHEST_BASE + chestDir
|
||||||
|
ctx.stats.notes.push(`lvlPrestDef=${String(chestDef)} (Act 3 - Sewer Chest ${DIRS[chestDir]})`)
|
||||||
placeNamedPiece(ctx, {
|
placeNamedPiece(ctx, {
|
||||||
kind: 'treasure',
|
kind: 'treasure',
|
||||||
namePrefix: 'Act 3 - Sewer Chest',
|
namePrefix: 'Act 3 - Sewer Chest',
|
||||||
sides: DIRS[(nDirection + 1) % 4],
|
sides: DIRS[chestDir],
|
||||||
plainSides: DIRS[(nDirection + 1) % 4],
|
plainSides: DIRS[chestDir],
|
||||||
direction: DIRECTION_SOUTH,
|
direction: DIRECTION_SOUTH,
|
||||||
preferFurthest: true,
|
preferFurthest: true,
|
||||||
})
|
})
|
||||||
|
|
@ -531,36 +693,46 @@ export function placeAct3MephistoStuff(ctx: SpecialPassContext): void {
|
||||||
* ------------------------------------------------------------------------- */
|
* ------------------------------------------------------------------------- */
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Halls of Anguish, Halls of Pain, Halls of Vaught.
|
* Halls of Anguish, Halls of Pain, Halls of Vaught (`DRLGMAZE_PlaceAct5TempleStuff`).
|
||||||
*
|
*
|
||||||
* Halls of Vaught (level 124) places Nihlathak's altar (Final Room).
|
* Enforces 1.13c `nExpansionHardcodedPresetsRemapping` for SW/SE/NW/NE orientations (`Def 1044..1047`).
|
||||||
* Halls of Pain (level 123) places Waypoint and descent stairs.
|
* Halls of Vaught (level 124) places Nihlathak's altar (`Def 1059`) and deterministically rolls
|
||||||
|
* Nihlathak's active wing orientation (`NW/NE/SE/SW`, `Def 1060..1063`).
|
||||||
*/
|
*/
|
||||||
export function placeAct5TempleStuff(ctx: SpecialPassContext): void {
|
export function placeAct5TempleStuff(ctx: SpecialPassContext): void {
|
||||||
const rng = getPassRng(ctx)
|
const rng = getPassRng(ctx)
|
||||||
const levelId = ctx.request.levelId
|
const levelId = ctx.request.levelId
|
||||||
|
|
||||||
if (levelId === 124) {
|
if (levelId === 124) {
|
||||||
// Halls of Vaught: Nihlathak Final Room
|
// Halls of Vaught: Nihlathak Final Room (Def 1059) + 1 of 4 Nihlathak wing orientations (Def 1060..1063)
|
||||||
|
const wingIdx = rng.int(0, 3)
|
||||||
|
const nihlathakWing = ACT5_NIHLATHAK_WINGS[wingIdx]!
|
||||||
|
const wingDefId = LVLPREST_ACT5_TEMPLE_NIHLATHAK_WING_BASE + wingIdx
|
||||||
|
ctx.stats.notes.push(`lvlPrestDef=${String(wingDefId)} (nihlathakWing=${nihlathakWing})`)
|
||||||
|
|
||||||
if (ctx.level.rooms.length === 1 && ctx.level.first !== null) {
|
if (ctx.level.rooms.length === 1 && ctx.level.first !== null) {
|
||||||
const finalPiece = ctx.pieces.find(p => p.name === 'Act 5 - Temple Final Room')
|
const finalPiece = ctx.pieces.find(p => p.name === 'Act 5 - Temple Final Room')
|
||||||
if (finalPiece !== undefined) {
|
if (finalPiece !== undefined) {
|
||||||
ctx.level.first.piece = finalPiece
|
ctx.level.first.piece = finalPiece
|
||||||
ctx.level.first.hasDs1 = true
|
ctx.level.first.hasDs1 = true
|
||||||
noteSpecial(ctx.level.first, finalPiece, '', ctx.stats)
|
noteSpecial(ctx.level.first, finalPiece, '', ctx.stats)
|
||||||
|
ctx.stats.notes.push(`lvlPrestDef=${String(LVLPREST_ACT5_TEMPLE_FINAL_ROOM)} (Act 5 - Temple Final Room)`)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
placeNamedPiece(ctx, {
|
replaceRoomByPresetDefId(ctx, LVLPREST_ACT5_TEMPLE_FINAL_ROOM, {
|
||||||
kind: 'quest',
|
kind: 'quest',
|
||||||
name: 'Act 5 - Temple Final Room',
|
name: 'Act 5 - Temple Final Room',
|
||||||
preferFurthest: true,
|
preferFurthest: true,
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
// Levels 122 and 123
|
// Levels 122 and 123: apply 1.13c nExpansionHardcodedPresetsRemapping (SW=1044, SE=1045, NW=1046, NE=1047)
|
||||||
const templeDownDirs = ['NE', 'NW', 'SW']
|
const templeDownDirs = ['NE', 'NW', 'SW'] as const
|
||||||
const nDirection = rng.int(0, templeDownDirs.length - 1)
|
const nDirection = rng.int(0, templeDownDirs.length - 1)
|
||||||
const downSide = templeDownDirs[nDirection]!
|
const downSide = templeDownDirs[nDirection]!
|
||||||
|
const remapIdx = ACT5_TEMPLE_HARDCODED_PRESETS_REMAPPING.indexOf(downSide)
|
||||||
|
const remappedDefId = LVLPREST_ACT5_TEMPLE_REMAPPING_BASE + (remapIdx >= 0 ? remapIdx : 0)
|
||||||
|
ctx.stats.notes.push(`lvlPrestDef=${String(remappedDefId)} (Act 5 - Temple ${downSide} Down)`)
|
||||||
|
|
||||||
placeNamedPiece(ctx, {
|
placeNamedPiece(ctx, {
|
||||||
kind: 'down',
|
kind: 'down',
|
||||||
|
|
@ -571,8 +743,7 @@ export function placeAct5TempleStuff(ctx: SpecialPassContext): void {
|
||||||
})
|
})
|
||||||
|
|
||||||
if (levelId === 123) {
|
if (levelId === 123) {
|
||||||
const templeWaypointDirs = ['NE', 'NW', 'SW', 'SE']
|
const wpSide = ACT5_TEMPLE_HARDCODED_PRESETS_REMAPPING[rng.int(0, ACT5_TEMPLE_HARDCODED_PRESETS_REMAPPING.length - 1)]!
|
||||||
const wpSide = templeWaypointDirs[rng.int(0, templeWaypointDirs.length - 1)]!
|
|
||||||
placeNamedPiece(ctx, {
|
placeNamedPiece(ctx, {
|
||||||
kind: 'waypoint',
|
kind: 'waypoint',
|
||||||
name: `Act 5 - Temple ${wpSide} Waypoint`,
|
name: `Act 5 - Temple ${wpSide} Waypoint`,
|
||||||
|
|
@ -663,19 +834,21 @@ export function placeAct5BaalStuff(ctx: SpecialPassContext): void {
|
||||||
* ------------------------------------------------------------------------- */
|
* ------------------------------------------------------------------------- */
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Crystalline Passage, Frozen River, Glacial Trail, Drifter Cavern, Ancients' Way, Icy Cellar.
|
* Crystalline Passage, Frozen River, Glacial Trail, Drifter Cavern, Ancients' Way, Icy Cellar (`DRLGMAZE_PlaceAct5IceStuff`).
|
||||||
*
|
*
|
||||||
* Frozen River (level 114) places Anya's frozen platform (River A or River B).
|
* Frozen River (level 114) rolls Anya's frozen platform (`LVLPREST_ACT5_ICE_RIVER_A = 1030` vs `B = 1031`) and places entry stairs.
|
||||||
* Drifter Cavern (116) and Icy Cellar (119) place Pool A / Pool B.
|
* Drifter Cavern (116) places `LVLPREST_ACT5_ICE_POOL_A = 1032`; Icy Cellar (119) places `LVLPREST_ACT5_ICE_POOL_B = 1033`.
|
||||||
* Glacial Trail (115) places Theme and Waypoint.
|
* Crystalline Passage (113), Glacial Trail (115), and Ancients' Way (118) place Prev, Next, Down, Theme, and Waypoint.
|
||||||
*/
|
*/
|
||||||
export function placeAct5IceStuff(ctx: SpecialPassContext): void {
|
export function placeAct5IceStuff(ctx: SpecialPassContext): void {
|
||||||
const rng = getPassRng(ctx)
|
const rng = getPassRng(ctx)
|
||||||
const levelId = ctx.request.levelId
|
const levelId = ctx.request.levelId
|
||||||
|
|
||||||
if (levelId === 114) {
|
if (levelId === 114) {
|
||||||
// Frozen River (Anya platform)
|
// Frozen River (Anya platform): 1.13c rolls Def 1030 (River A) vs Def 1031 (River B)
|
||||||
const variant = rng.int(0, 1) === 0 ? 'A' : 'B'
|
const variantIdx = rng.int(0, 1)
|
||||||
|
const variant = variantIdx === 0 ? 'A' : 'B'
|
||||||
|
const riverDefId = variantIdx === 0 ? LVLPREST_ACT5_ICE_RIVER_A : LVLPREST_ACT5_ICE_RIVER_B
|
||||||
if (ctx.level.rooms.length === 1 && ctx.level.first !== null) {
|
if (ctx.level.rooms.length === 1 && ctx.level.first !== null) {
|
||||||
const riverPiece = ctx.pieces.find(p => p.name === `Act 5 - Ice River ${variant}`)
|
const riverPiece = ctx.pieces.find(p => p.name === `Act 5 - Ice River ${variant}`)
|
||||||
?? ctx.pieces.find(p => p.name.startsWith('Act 5 - Ice River'))
|
?? ctx.pieces.find(p => p.name.startsWith('Act 5 - Ice River'))
|
||||||
|
|
@ -683,14 +856,29 @@ export function placeAct5IceStuff(ctx: SpecialPassContext): void {
|
||||||
ctx.level.first.piece = riverPiece
|
ctx.level.first.piece = riverPiece
|
||||||
ctx.level.first.hasDs1 = true
|
ctx.level.first.hasDs1 = true
|
||||||
noteSpecial(ctx.level.first, riverPiece, '', ctx.stats)
|
noteSpecial(ctx.level.first, riverPiece, '', ctx.stats)
|
||||||
|
ctx.stats.notes.push(`lvlPrestDef=${String(riverDefId)} (${riverPiece.name})`)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
placeNamedPiece(ctx, {
|
replaceRoomByPresetDefId(ctx, riverDefId, {
|
||||||
kind: 'quest',
|
kind: 'quest',
|
||||||
name: `Act 5 - Ice River ${variant}`,
|
name: `Act 5 - Ice River ${variant}`,
|
||||||
preferFurthest: true,
|
preferFurthest: true,
|
||||||
})
|
})
|
||||||
|
if (
|
||||||
|
ctx.level.rooms.length > 1 &&
|
||||||
|
!ctx.level.rooms.some(r => r.piece?.kind === 'prev') &&
|
||||||
|
ctx.pieces.some(p => p.name.startsWith('Act 5 - Ice Prev'))
|
||||||
|
) {
|
||||||
|
const prevDir = rng.int(0, 3)
|
||||||
|
placeNamedPiece(ctx, {
|
||||||
|
kind: 'prev',
|
||||||
|
namePrefix: 'Act 5 - Ice Prev',
|
||||||
|
sides: DIRS[prevDir],
|
||||||
|
plainSides: DIRS[prevDir],
|
||||||
|
preferFurthest: false,
|
||||||
|
})
|
||||||
|
}
|
||||||
} else if (levelId === 116) {
|
} else if (levelId === 116) {
|
||||||
if (ctx.level.rooms.length === 1 && ctx.level.first !== null) {
|
if (ctx.level.rooms.length === 1 && ctx.level.first !== null) {
|
||||||
const poolPiece = ctx.pieces.find(p => p.name === 'Act 5 - Ice Pool A')
|
const poolPiece = ctx.pieces.find(p => p.name === 'Act 5 - Ice Pool A')
|
||||||
|
|
@ -698,10 +886,15 @@ export function placeAct5IceStuff(ctx: SpecialPassContext): void {
|
||||||
ctx.level.first.piece = poolPiece
|
ctx.level.first.piece = poolPiece
|
||||||
ctx.level.first.hasDs1 = true
|
ctx.level.first.hasDs1 = true
|
||||||
noteSpecial(ctx.level.first, poolPiece, '', ctx.stats)
|
noteSpecial(ctx.level.first, poolPiece, '', ctx.stats)
|
||||||
|
ctx.stats.notes.push(`lvlPrestDef=${String(LVLPREST_ACT5_ICE_POOL_A)} (${poolPiece.name})`)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
placeNamedPiece(ctx, { kind: 'quest', name: 'Act 5 - Ice Pool A', preferFurthest: true })
|
replaceRoomByPresetDefId(ctx, LVLPREST_ACT5_ICE_POOL_A, {
|
||||||
|
kind: 'quest',
|
||||||
|
name: 'Act 5 - Ice Pool A',
|
||||||
|
preferFurthest: true,
|
||||||
|
})
|
||||||
} else if (levelId === 119) {
|
} else if (levelId === 119) {
|
||||||
if (ctx.level.rooms.length === 1 && ctx.level.first !== null) {
|
if (ctx.level.rooms.length === 1 && ctx.level.first !== null) {
|
||||||
const poolPiece = ctx.pieces.find(p => p.name === 'Act 5 - Ice Pool B')
|
const poolPiece = ctx.pieces.find(p => p.name === 'Act 5 - Ice Pool B')
|
||||||
|
|
@ -709,10 +902,15 @@ export function placeAct5IceStuff(ctx: SpecialPassContext): void {
|
||||||
ctx.level.first.piece = poolPiece
|
ctx.level.first.piece = poolPiece
|
||||||
ctx.level.first.hasDs1 = true
|
ctx.level.first.hasDs1 = true
|
||||||
noteSpecial(ctx.level.first, poolPiece, '', ctx.stats)
|
noteSpecial(ctx.level.first, poolPiece, '', ctx.stats)
|
||||||
|
ctx.stats.notes.push(`lvlPrestDef=${String(LVLPREST_ACT5_ICE_POOL_B)} (${poolPiece.name})`)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
placeNamedPiece(ctx, { kind: 'quest', name: 'Act 5 - Ice Pool B', preferFurthest: true })
|
replaceRoomByPresetDefId(ctx, LVLPREST_ACT5_ICE_POOL_B, {
|
||||||
|
kind: 'quest',
|
||||||
|
name: 'Act 5 - Ice Pool B',
|
||||||
|
preferFurthest: true,
|
||||||
|
})
|
||||||
} else {
|
} else {
|
||||||
// Levels 113, 115, 118
|
// Levels 113, 115, 118
|
||||||
let nDirection = rng.int(0, 3)
|
let nDirection = rng.int(0, 3)
|
||||||
|
|
@ -751,8 +949,10 @@ export function placeAct5IceStuff(ctx: SpecialPassContext): void {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if (levelId === 113 || levelId === 115) {
|
if (levelId === 113 || levelId === 115 || levelId === 118) {
|
||||||
nDirection = (nDirection + 1) % 4
|
nDirection = (nDirection + 1) % 4
|
||||||
|
const wpDef = LVLPREST_ACT5_ICE_WAYPOINT_BASE + nDirection
|
||||||
|
ctx.stats.notes.push(`lvlPrestDef=${String(wpDef)} (Act 5 - Ice waypoint ${DIRS[nDirection]})`)
|
||||||
placeNamedPiece(ctx, {
|
placeNamedPiece(ctx, {
|
||||||
kind: 'waypoint',
|
kind: 'waypoint',
|
||||||
namePrefix: 'Act 5 - Ice waypoint',
|
namePrefix: 'Act 5 - Ice waypoint',
|
||||||
|
|
|
||||||
|
|
@ -585,7 +585,9 @@ export function inferLevelTypeName(pieces: readonly MazePiece[]): string | null
|
||||||
while (prefix.length > 0 && !piece.name.startsWith(prefix)) prefix = prefix.slice(0, -1)
|
while (prefix.length > 0 && !piece.name.startsWith(prefix)) prefix = prefix.slice(0, -1)
|
||||||
}
|
}
|
||||||
const trimmed = prefix.replace(/[\s-]+$/, '')
|
const trimmed = prefix.replace(/[\s-]+$/, '')
|
||||||
return trimmed === '' ? null : trimmed
|
if (trimmed === '') return null
|
||||||
|
if (trimmed === 'Act 5 - Ice') return 'Act 5 - Ice Caves'
|
||||||
|
return trimmed
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ------------------------------------------------------------------------- *
|
/* ------------------------------------------------------------------------- *
|
||||||
|
|
@ -2372,17 +2374,17 @@ export function generateMaze(request: MazeRequest): MazeResult {
|
||||||
placeSpecialGroup(level, request.pieces, group, cursor, index, stats)
|
placeSpecialGroup(level, request.pieces, group, cursor, index, stats)
|
||||||
cursor = (cursor + 1) % 4
|
cursor = (cursor + 1) % 4
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (profile.specialPass !== undefined) {
|
if (profile.specialPass !== undefined) {
|
||||||
runMazeSpecialPasses(profile.specialPass, {
|
runMazeSpecialPasses(profile.specialPass, {
|
||||||
level,
|
level,
|
||||||
request,
|
request,
|
||||||
pieces: request.pieces,
|
pieces: request.pieces,
|
||||||
index,
|
index,
|
||||||
stats,
|
stats,
|
||||||
rng,
|
rng,
|
||||||
})
|
})
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const unreachable = level.rooms.length - reachableRooms(level)
|
const unreachable = level.rooms.length - reachableRooms(level)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,235 @@
|
||||||
|
import { describe, expect, it } from 'vitest'
|
||||||
|
import {
|
||||||
|
ACT5_NIHLATHAK_WINGS,
|
||||||
|
ACT5_TEMPLE_HARDCODED_PRESETS_REMAPPING,
|
||||||
|
LVLPREST_ACT2_TOMB_TAINTED_SUN_X,
|
||||||
|
LVLPREST_ACT3_SEWER_NE,
|
||||||
|
LVLPREST_ACT3_SEWER_NW,
|
||||||
|
LVLPREST_ACT3_SEWER_SE,
|
||||||
|
LVLPREST_ACT3_SEWER_SW,
|
||||||
|
LVLPREST_ACT5_ICE_POOL_A,
|
||||||
|
LVLPREST_ACT5_ICE_POOL_B,
|
||||||
|
LVLPREST_ACT5_ICE_RIVER_A,
|
||||||
|
LVLPREST_ACT5_ICE_RIVER_B,
|
||||||
|
LVLPREST_ACT5_TEMPLE_FINAL_ROOM,
|
||||||
|
MAZE_DOOR_EAST,
|
||||||
|
MAZE_DOOR_NORTH,
|
||||||
|
MAZE_DOOR_SOUTH,
|
||||||
|
MAZE_DOOR_WEST,
|
||||||
|
mazeDoorMask,
|
||||||
|
mazePresetDefIdForDoorMask,
|
||||||
|
} from '../src/game/maze-special-passes.ts'
|
||||||
|
import {
|
||||||
|
DIRECTION_EAST,
|
||||||
|
DIRECTION_NORTH,
|
||||||
|
DIRECTION_SOUTH,
|
||||||
|
DIRECTION_WEST,
|
||||||
|
generateMaze,
|
||||||
|
} from '../src/game/maze.ts'
|
||||||
|
import type { MazePiece, MazePieceKind, Room } from '../src/game/maze.ts'
|
||||||
|
import { Rng } from '../src/game/rng.ts'
|
||||||
|
import type { Ds1 } from '../src/formats/ds1.ts'
|
||||||
|
|
||||||
|
function mockDs1(width = 10, height = 10): Ds1 {
|
||||||
|
const cells = Array.from({ length: height }, () =>
|
||||||
|
Array.from({ length: width }, () => ({
|
||||||
|
floors: [{ prop1: 1, style: 1, sequence: 0, hidden: false, unknown1: 0, unknown2: 0 }],
|
||||||
|
walls: [],
|
||||||
|
shadows: [],
|
||||||
|
substitutions: [],
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
version: 18,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
act: 1,
|
||||||
|
substitutionType: 0,
|
||||||
|
wallLayers: 1,
|
||||||
|
floorLayers: 1,
|
||||||
|
cells,
|
||||||
|
objects: [],
|
||||||
|
npcPathOffset: null,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function mockPiece(name: string, kind: MazePieceKind, sides = ''): MazePiece {
|
||||||
|
return { name, kind, sides, levels: [mockDs1()] }
|
||||||
|
}
|
||||||
|
|
||||||
|
function createStandardPieces(prefix: string, extra: MazePiece[] = []): MazePiece[] {
|
||||||
|
const SIDES = ['N', 'S', 'E', 'W', 'NE', 'NW', 'SE', 'SW', 'NS', 'EW', 'NSE', 'NSW', 'NEW', 'SEW', 'NSEW']
|
||||||
|
return [
|
||||||
|
mockPiece(`${prefix} Entrance`, 'entrance', ''),
|
||||||
|
...SIDES.map(s => mockPiece(`${prefix} ${s}`, 'room', s)),
|
||||||
|
...['N', 'S', 'E', 'W'].map(s => mockPiece(`${prefix} Prev ${s}`, 'prev', s)),
|
||||||
|
...['N', 'S', 'E', 'W'].map(s => mockPiece(`${prefix} Next ${s}`, 'next', s)),
|
||||||
|
...['N', 'S', 'E', 'W'].map(s => mockPiece(`${prefix} Down ${s}`, 'down', s)),
|
||||||
|
...['N', 'S', 'E', 'W'].map(s => mockPiece(`${prefix} Theme ${s}`, 'theme', s)),
|
||||||
|
...extra,
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeRoom(id: number, orthDirs: number[]): Room {
|
||||||
|
const orths = orthDirs.map(direction => ({
|
||||||
|
room: { id: 900 + direction, x: 0, y: 0, width: 8, height: 8, orths: [], piece: null, variant: 0, hasDs1: false, rng: new Rng(1) },
|
||||||
|
direction,
|
||||||
|
}))
|
||||||
|
return {
|
||||||
|
id,
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
width: 8,
|
||||||
|
height: 8,
|
||||||
|
orths,
|
||||||
|
piece: null,
|
||||||
|
variant: 0,
|
||||||
|
hasDs1: false,
|
||||||
|
rng: new Rng(100 + id),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('Blizzard D2 1.13c Maze LvlPrest Def ID & Special Layout Passes (Issue #73)', () => {
|
||||||
|
it('computes 1.13c 4-bit door masks (W=1, E=2, S=4, N=8) and LvlPrest Def ID offsets', () => {
|
||||||
|
expect(MAZE_DOOR_WEST).toBe(1)
|
||||||
|
expect(MAZE_DOOR_EAST).toBe(2)
|
||||||
|
expect(MAZE_DOOR_SOUTH).toBe(4)
|
||||||
|
expect(MAZE_DOOR_NORTH).toBe(8)
|
||||||
|
|
||||||
|
const westOnly = makeRoom(1, [DIRECTION_WEST])
|
||||||
|
expect(mazeDoorMask(westOnly)).toBe(1)
|
||||||
|
expect(mazePresetDefIdForDoorMask(39, westOnly)).toBe(39)
|
||||||
|
|
||||||
|
const allFour = makeRoom(2, [DIRECTION_WEST, DIRECTION_EAST, DIRECTION_SOUTH, DIRECTION_NORTH])
|
||||||
|
expect(mazeDoorMask(allFour)).toBe(15)
|
||||||
|
expect(mazePresetDefIdForDoorMask(39, allFour)).toBe(53)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('places Claw Viper Temple Level 2 fixed Tainted Sun Altar chamber (Def 633)', () => {
|
||||||
|
const pieces = createStandardPieces('Act 2 - Tomb', [
|
||||||
|
mockPiece('Act 2 - Tomb Tainted Sun X', 'quest', ''),
|
||||||
|
])
|
||||||
|
const result = generateMaze({
|
||||||
|
levelId: 61,
|
||||||
|
levelName: 'Act 2 - Tomb',
|
||||||
|
sectionSize: 10,
|
||||||
|
minRooms: 4,
|
||||||
|
merge: 0,
|
||||||
|
seed: 4242,
|
||||||
|
pieces,
|
||||||
|
})
|
||||||
|
|
||||||
|
const notes = result.stats.notes as string[]
|
||||||
|
const specials = result.stats.specialsApplied as { room: number; kind: string; sides: string }[]
|
||||||
|
expect(notes.some(n => n.includes(`lvlPrestDef=${String(LVLPREST_ACT2_TOMB_TAINTED_SUN_X)}`))).toBe(true)
|
||||||
|
expect(notes.some(n => n.includes('Act 2 - Tomb Tainted Sun X'))).toBe(true)
|
||||||
|
expect(specials.some(r => r.kind === 'quest')).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('places Kurast Sewers Level 1 (levelId 92) 4 corner entrance rooms (Def 585..588) and Drain lever', () => {
|
||||||
|
const pieces = createStandardPieces('Act 3 - Sewer', [
|
||||||
|
mockPiece('Act 3 - Sewer SW', 'prev', 'S'),
|
||||||
|
mockPiece('Act 3 - Sewer SE', 'prev', 'E'),
|
||||||
|
mockPiece('Act 3 - Sewer NW', 'prev', 'W'),
|
||||||
|
mockPiece('Act 3 - Sewer NE', 'prev', 'N'),
|
||||||
|
...['N', 'E', 'S', 'W'].map(s => mockPiece(`Act 3 - Sewer Drain ${s}`, 'next', s)),
|
||||||
|
...['N', 'E', 'S', 'W'].map(s => mockPiece(`Act 3 - Sewer Chest ${s}`, 'treasure', s)),
|
||||||
|
])
|
||||||
|
const result = generateMaze({
|
||||||
|
levelId: 92,
|
||||||
|
levelName: 'Act 3 - Sewer',
|
||||||
|
sectionSize: 10,
|
||||||
|
minRooms: 10,
|
||||||
|
merge: 0,
|
||||||
|
seed: 7777,
|
||||||
|
pieces,
|
||||||
|
})
|
||||||
|
|
||||||
|
const notes = result.stats.notes as string[]
|
||||||
|
const specials = result.stats.specialsApplied as { room: number; kind: string; sides: string }[]
|
||||||
|
for (const defId of [LVLPREST_ACT3_SEWER_SW, LVLPREST_ACT3_SEWER_SE, LVLPREST_ACT3_SEWER_NW, LVLPREST_ACT3_SEWER_NE]) {
|
||||||
|
expect(notes.some(n => n.includes(`lvlPrestDef=${String(defId)}`))).toBe(true)
|
||||||
|
}
|
||||||
|
expect(notes.some(n => n.includes('Act 3 - Sewer Drain'))).toBe(true)
|
||||||
|
expect(specials.some(r => r.kind === 'next')).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('rolls Frozen River A/B (Def 1030/1031) and places Ice Pools (Def 1032/1033) in Act 5 Ice Caves', () => {
|
||||||
|
const pieces = createStandardPieces('Act 5 - Ice Caves', [
|
||||||
|
mockPiece('Act 5 - Ice River A', 'quest', ''),
|
||||||
|
mockPiece('Act 5 - Ice River B', 'quest', ''),
|
||||||
|
mockPiece('Act 5 - Ice Pool A', 'quest', ''),
|
||||||
|
mockPiece('Act 5 - Ice Pool B', 'quest', ''),
|
||||||
|
])
|
||||||
|
|
||||||
|
const chosenDefs = new Set<number>()
|
||||||
|
for (let seed = 1; seed <= 12; seed += 1) {
|
||||||
|
const res = generateMaze({
|
||||||
|
levelId: 114,
|
||||||
|
levelName: 'Act 5 - Ice Caves',
|
||||||
|
sectionSize: 10,
|
||||||
|
minRooms: 4,
|
||||||
|
merge: 0,
|
||||||
|
seed: seed * 313,
|
||||||
|
pieces,
|
||||||
|
})
|
||||||
|
const notes = res.stats.notes as string[]
|
||||||
|
if (notes.some(n => n.includes(`lvlPrestDef=${String(LVLPREST_ACT5_ICE_RIVER_A)}`))) {
|
||||||
|
chosenDefs.add(LVLPREST_ACT5_ICE_RIVER_A)
|
||||||
|
}
|
||||||
|
if (notes.some(n => n.includes(`lvlPrestDef=${String(LVLPREST_ACT5_ICE_RIVER_B)}`))) {
|
||||||
|
chosenDefs.add(LVLPREST_ACT5_ICE_RIVER_B)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
expect(chosenDefs.has(LVLPREST_ACT5_ICE_RIVER_A)).toBe(true)
|
||||||
|
expect(chosenDefs.has(LVLPREST_ACT5_ICE_RIVER_B)).toBe(true)
|
||||||
|
|
||||||
|
const res116 = generateMaze({
|
||||||
|
levelId: 116,
|
||||||
|
levelName: 'Act 5 - Ice Caves',
|
||||||
|
sectionSize: 10,
|
||||||
|
minRooms: 4,
|
||||||
|
merge: 0,
|
||||||
|
seed: 100,
|
||||||
|
pieces,
|
||||||
|
})
|
||||||
|
expect((res116.stats.notes as string[]).some(n => n.includes(`lvlPrestDef=${String(LVLPREST_ACT5_ICE_POOL_A)}`))).toBe(true)
|
||||||
|
|
||||||
|
const res119 = generateMaze({
|
||||||
|
levelId: 119,
|
||||||
|
levelName: 'Act 5 - Ice Caves',
|
||||||
|
sectionSize: 10,
|
||||||
|
minRooms: 4,
|
||||||
|
merge: 0,
|
||||||
|
seed: 100,
|
||||||
|
pieces,
|
||||||
|
})
|
||||||
|
expect((res119.stats.notes as string[]).some(n => n.includes(`lvlPrestDef=${String(LVLPREST_ACT5_ICE_POOL_B)}`))).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('applies Act 5 Temple hardcoded remapping (Def 1044..1047) and rolls Nihlathak active wing (Def 1060..1063)', () => {
|
||||||
|
expect(ACT5_TEMPLE_HARDCODED_PRESETS_REMAPPING).toEqual(['SW', 'SE', 'NW', 'NE'])
|
||||||
|
expect(ACT5_NIHLATHAK_WINGS).toEqual(['NW', 'NE', 'SE', 'SW'])
|
||||||
|
|
||||||
|
const pieces = createStandardPieces('Act 5 - Temple', [
|
||||||
|
mockPiece('Act 5 - Temple Final Room', 'quest', ''),
|
||||||
|
mockPiece('Act 5 - Temple NE Down', 'down', 'NE'),
|
||||||
|
mockPiece('Act 5 - Temple NW Down', 'down', 'NW'),
|
||||||
|
mockPiece('Act 5 - Temple SW Down', 'down', 'SW'),
|
||||||
|
mockPiece('Act 5 - Temple SE up', 'prev', 'SE'),
|
||||||
|
])
|
||||||
|
|
||||||
|
const res124 = generateMaze({
|
||||||
|
levelId: 124,
|
||||||
|
levelName: 'Act 5 - Temple',
|
||||||
|
sectionSize: 10,
|
||||||
|
minRooms: 4,
|
||||||
|
merge: 0,
|
||||||
|
seed: 8888,
|
||||||
|
pieces,
|
||||||
|
})
|
||||||
|
const notes124 = res124.stats.notes as string[]
|
||||||
|
expect(notes124.some(n => n.includes(`lvlPrestDef=${String(LVLPREST_ACT5_TEMPLE_FINAL_ROOM)}`))).toBe(true)
|
||||||
|
expect(notes124.some(n => n.includes('nihlathakWing='))).toBe(true)
|
||||||
|
})
|
||||||
|
})
|
||||||
Loading…
Reference in New Issue