feat(map-gen/wilderness): implement outdoor level initializers for acts 2-5 (fixes #51)
This commit is contained in:
parent
1fa3f8b7a2
commit
8baf91c51f
|
|
@ -0,0 +1,906 @@
|
||||||
|
/**
|
||||||
|
* Wilderness outdoor topography initializers for Acts 2, 3, 4, and 5.
|
||||||
|
*
|
||||||
|
* blizzard Drlg outdoor passes:
|
||||||
|
* - `DRLGOUTDESR_InitAct2OutdoorLevel`: Desert canyons, sand trails, Far Oasis pools,
|
||||||
|
* Valley of Snakes canyon gorges, Canyon of the Magi 7 Tal Rasha tomb entrances.
|
||||||
|
* - `DRLGOUTPLACE_InitAct3OutdoorLevel` & `DRLG_GenerateJungles`: Jungle waterways,
|
||||||
|
* wooden river bridges spanning channels, causeways, Kurast city avenues, canals & bridges.
|
||||||
|
* - `DRLGOUTDOORS_InitAct4OutdoorLevel`: Molten lava chasms and fissures, natural rock
|
||||||
|
* bridges spanning chasms, volcanic mesa plateaus.
|
||||||
|
* - `DRLGOUTSIEGE_InitAct5OutdoorLevel`: Transverse siege barricade palisades, choke-point
|
||||||
|
* gates, military dugout trenches, prisoner cages, and snow crater frozen lakes.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { Ds1, Ds1Cell, Ds1Floor, Ds1Object, Ds1Wall } from '../formats/ds1.ts'
|
||||||
|
import { Rng } from './rng.ts'
|
||||||
|
import type {
|
||||||
|
Canvas,
|
||||||
|
WildernessRequest,
|
||||||
|
WildernessStats,
|
||||||
|
WildernessPiece,
|
||||||
|
PlacedPresetRect,
|
||||||
|
} from './wilderness.ts'
|
||||||
|
import { stampDs1, TILES_PER_BLOCK, emptyFloor, emptyWall } from './wilderness.ts'
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------------------- *
|
||||||
|
* Statistics interfaces
|
||||||
|
* ------------------------------------------------------------------------- */
|
||||||
|
|
||||||
|
export interface Act2TopographyStats {
|
||||||
|
readonly canyonWalls: number
|
||||||
|
readonly sandTrailCells: number
|
||||||
|
readonly oasisPoolCells: number
|
||||||
|
readonly tombEntrances: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Act3TopographyStats {
|
||||||
|
readonly causewayCells: number
|
||||||
|
readonly riverBridges: number
|
||||||
|
readonly canalCells: number
|
||||||
|
readonly junglePathCells: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Act4TopographyStats {
|
||||||
|
readonly lavaChasmCells: number
|
||||||
|
readonly rockyBridges: number
|
||||||
|
readonly plateauCells: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Act5TopographyStats {
|
||||||
|
readonly barricadeWalls: number
|
||||||
|
readonly trenchCells: number
|
||||||
|
readonly frozenLakeCells: number
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------------------- *
|
||||||
|
* Geometry & Canvas Helpers
|
||||||
|
* ------------------------------------------------------------------------- */
|
||||||
|
|
||||||
|
function isInsidePreset(
|
||||||
|
x: number,
|
||||||
|
y: number,
|
||||||
|
placedRects: readonly PlacedPresetRect[],
|
||||||
|
margin = 1,
|
||||||
|
): boolean {
|
||||||
|
for (const r of placedRects) {
|
||||||
|
if (
|
||||||
|
x >= r.x - margin &&
|
||||||
|
x < r.x + r.w + margin &&
|
||||||
|
y >= r.y - margin &&
|
||||||
|
y < r.y + r.h + margin
|
||||||
|
) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeFloor(style: number, sequence: number, prop1 = 2): Ds1Floor {
|
||||||
|
return {
|
||||||
|
prop1,
|
||||||
|
style,
|
||||||
|
sequence,
|
||||||
|
unknown1: 0,
|
||||||
|
unknown2: 0,
|
||||||
|
hidden: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeWall(style: number, sequence: number, type: number, prop1 = 129): Ds1Wall {
|
||||||
|
return {
|
||||||
|
prop1,
|
||||||
|
sequence,
|
||||||
|
style,
|
||||||
|
type,
|
||||||
|
unknown1: 0,
|
||||||
|
unknown2: 0,
|
||||||
|
hidden: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function paintFloor(
|
||||||
|
canvas: Canvas,
|
||||||
|
x: number,
|
||||||
|
y: number,
|
||||||
|
floor: Ds1Floor,
|
||||||
|
overwriteProtected = false,
|
||||||
|
): boolean {
|
||||||
|
if (x < 0 || x >= canvas.width || y < 0 || y >= canvas.height) return false
|
||||||
|
const cell = canvas.cells[y]?.[x]
|
||||||
|
if (!cell) return false
|
||||||
|
const existing = cell.floors[0]
|
||||||
|
if (!overwriteProtected && existing) {
|
||||||
|
// Protected styles: water (style 2/3), wooden bridge (style 4)
|
||||||
|
if (existing.style === 2 || existing.style === 3 || existing.style === 4) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
;(cell.floors as Ds1Floor[])[0] = floor
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
function paintWall(canvas: Canvas, x: number, y: number, wall: Ds1Wall): boolean {
|
||||||
|
if (x < 0 || x >= canvas.width || y < 0 || y >= canvas.height) return false
|
||||||
|
const cell = canvas.cells[y]?.[x]
|
||||||
|
if (!cell) return false
|
||||||
|
const targetWalls = cell.walls as Ds1Wall[]
|
||||||
|
if (targetWalls.length > 0) {
|
||||||
|
targetWalls[0] = wall
|
||||||
|
} else {
|
||||||
|
targetWalls.push(wall)
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearWalls(canvas: Canvas, x: number, y: number): void {
|
||||||
|
if (x < 0 || x >= canvas.width || y < 0 || y >= canvas.height) return
|
||||||
|
const cell = canvas.cells[y]?.[x]
|
||||||
|
if (cell) {
|
||||||
|
;(cell.walls as Ds1Wall[]).length = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------------------- *
|
||||||
|
* Act 2: DRLGOUTDESR_InitAct2OutdoorLevel
|
||||||
|
* ------------------------------------------------------------------------- */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize Act 2 Desert Outdoor Topography:
|
||||||
|
* - Rocky canyon ridges and cliff berms carving winding desert corridors.
|
||||||
|
* - 3-tile-wide sand caravan trails connecting gates across the dunes.
|
||||||
|
* - Far Oasis (Level 43): lush water oasis pools and Maggot Lair entrance.
|
||||||
|
* - Valley of Snakes (Level 45): narrow canyon gorge leading to Claw Viper Temple.
|
||||||
|
* - Canyon of the Magi (Level 46): 7 Tal Rasha tomb entrances along perimeter cliffs
|
||||||
|
* and central Arcane Sanctuary warp platform.
|
||||||
|
* - Lost City (Level 44): ancient buried city ruins and street corridors.
|
||||||
|
*/
|
||||||
|
export function initAct2OutdoorLevel(
|
||||||
|
canvas: Canvas,
|
||||||
|
request: WildernessRequest,
|
||||||
|
gridWidth: number,
|
||||||
|
gridHeight: number,
|
||||||
|
inX: number,
|
||||||
|
outY: number,
|
||||||
|
placedRects: PlacedPresetRect[],
|
||||||
|
rng: Rng,
|
||||||
|
stats: WildernessStats,
|
||||||
|
): Act2TopographyStats {
|
||||||
|
let canyonWalls = 0
|
||||||
|
let sandTrailCells = 0
|
||||||
|
let oasisPoolCells = 0
|
||||||
|
let tombEntrances = 0
|
||||||
|
|
||||||
|
const levelId = request.levelId
|
||||||
|
const hubX = inX * TILES_PER_BLOCK + 4
|
||||||
|
const hubY = outY * TILES_PER_BLOCK + 4
|
||||||
|
|
||||||
|
// Helper to paint a 3-tile-wide sand caravan trail
|
||||||
|
const paintSandTrailTile = (x: number, y: number): void => {
|
||||||
|
if (x < 3 || x >= canvas.width - 3 || y < 3 || y >= canvas.height - 3) return
|
||||||
|
if (isInsidePreset(x, y, placedRects, 0)) return
|
||||||
|
const cell = canvas.cells[y]?.[x]
|
||||||
|
if (!cell) return
|
||||||
|
// Style 5 (desert road), prop1 194
|
||||||
|
const floor = makeFloor(5, rng.int(0, 4) === 0 ? 25 : 0, 194)
|
||||||
|
if (paintFloor(canvas, x, y, floor, false)) {
|
||||||
|
clearWalls(canvas, x, y)
|
||||||
|
sandTrailCells += 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. Sand Caravan Trail Network: North-South and West-East axes
|
||||||
|
if (levelId !== 46) {
|
||||||
|
const startY = Math.max(4, Math.floor(canvas.height * 0.1))
|
||||||
|
const endY = Math.min(canvas.height - 5, Math.floor(canvas.height * 0.9))
|
||||||
|
for (let y = startY; y <= endY; y += 1) {
|
||||||
|
for (let dx = -1; dx <= 1; dx += 1) {
|
||||||
|
paintSandTrailTile(hubX + dx, y)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const startX = Math.max(4, Math.floor(canvas.width * 0.1))
|
||||||
|
const endX = Math.min(canvas.width - 5, Math.floor(canvas.width * 0.9))
|
||||||
|
for (let x = startX; x <= endX; x += 1) {
|
||||||
|
for (let dy = -1; dy <= 1; dy += 1) {
|
||||||
|
paintSandTrailTile(x, hubY + dy)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Canyon Walls & Rocky Berms (Levels 41, 42, 44, 45)
|
||||||
|
if (levelId === 41 || levelId === 42 || levelId === 44 || levelId === 45) {
|
||||||
|
const isValleyOfSnakes = levelId === 45
|
||||||
|
// In Valley of Snakes, place 2 tight parallel canyon ridge walls (corridor of 10-12 tiles)
|
||||||
|
// In other deserts, place transverse & longitudinal canyon ridges
|
||||||
|
const ridgeY1 = Math.max(12, Math.min(canvas.height - 14, hubY - (isValleyOfSnakes ? 5 : 16)))
|
||||||
|
const ridgeY2 = Math.max(14, Math.min(canvas.height - 12, hubY + (isValleyOfSnakes ? 6 : 16)))
|
||||||
|
|
||||||
|
for (let x = 10; x < canvas.width - 10; x += 1) {
|
||||||
|
// Leave 4-tile wide canyon pass gaps where the main highway or crossroads pass
|
||||||
|
if (Math.abs(x - hubX) <= 3) continue
|
||||||
|
if (isInsidePreset(x, ridgeY1, placedRects, 2)) continue
|
||||||
|
if (paintWall(canvas, x, ridgeY1, makeWall(1, 0, 2, 129))) {
|
||||||
|
canyonWalls += 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let x = 10; x < canvas.width - 10; x += 1) {
|
||||||
|
if (Math.abs(x - hubX) <= 3) continue
|
||||||
|
if (isInsidePreset(x, ridgeY2, placedRects, 2)) continue
|
||||||
|
if (paintWall(canvas, x, ridgeY2, makeWall(1, 0, 2, 129))) {
|
||||||
|
canyonWalls += 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isValleyOfSnakes) {
|
||||||
|
const ridgeX = Math.max(12, Math.min(canvas.width - 12, hubX + 16))
|
||||||
|
for (let y = 10; y < canvas.height - 10; y += 1) {
|
||||||
|
if (Math.abs(y - hubY) <= 3) continue
|
||||||
|
if (isInsidePreset(ridgeX, y, placedRects, 2)) continue
|
||||||
|
if (paintWall(canvas, ridgeX, y, makeWall(1, 0, 1, 129))) {
|
||||||
|
canyonWalls += 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Far Oasis (Level 43): Lush Water Oasis Pools & Maggot Lair
|
||||||
|
if (levelId === 43 || /Oasis/i.test(request.levelName)) {
|
||||||
|
const poolCenters = [
|
||||||
|
{
|
||||||
|
cx: Math.max(14, Math.floor(canvas.width * 0.35)),
|
||||||
|
cy: Math.max(14, Math.floor(canvas.height * 0.35)),
|
||||||
|
radius: Math.min(6, Math.floor(canvas.width / 12)),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
cx: Math.min(canvas.width - 16, Math.floor(canvas.width * 0.68)),
|
||||||
|
cy: Math.min(canvas.height - 16, Math.floor(canvas.height * 0.68)),
|
||||||
|
radius: Math.min(5, Math.floor(canvas.width / 14)),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
for (const pool of poolCenters) {
|
||||||
|
for (let dy = -pool.radius; dy <= pool.radius; dy += 1) {
|
||||||
|
for (let dx = -pool.radius; dx <= pool.radius; dx += 1) {
|
||||||
|
const dist = Math.hypot(dx, dy)
|
||||||
|
if (dist <= pool.radius) {
|
||||||
|
const px = pool.cx + dx
|
||||||
|
const py = pool.cy + dy
|
||||||
|
if (px >= 4 && px < canvas.width - 4 && py >= 4 && py < canvas.height - 4) {
|
||||||
|
if (!isInsidePreset(px, py, placedRects, 1)) {
|
||||||
|
// Water floor: style 2 (water), prop1 2
|
||||||
|
paintFloor(canvas, px, py, makeFloor(2, 0, 2), true)
|
||||||
|
clearWalls(canvas, px, py)
|
||||||
|
oasisPoolCells += 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to stamp Desert Oasis preset if available in pieces
|
||||||
|
const oasisPiece = request.pieces.find(p => /Desert Oasis/i.test(p.name))
|
||||||
|
if (oasisPiece?.levels[0] && !stats.specialPresets.some(n => /Desert Oasis/i.test(n))) {
|
||||||
|
const ds1 = oasisPiece.levels[0]
|
||||||
|
const originX = Math.max(8, Math.floor(canvas.width * 0.4) - Math.floor(ds1.width / 2))
|
||||||
|
const originY = Math.max(8, Math.floor(canvas.height * 0.4) - Math.floor(ds1.height / 2))
|
||||||
|
stampDs1(canvas, ds1, originX, originY)
|
||||||
|
stats.specialPresets.push(oasisPiece.name)
|
||||||
|
stats.substitutions.push({ name: oasisPiece.name, role: 'object', enabled: true, clusters: 1 })
|
||||||
|
placedRects.push({ name: oasisPiece.name, x: originX, y: originY, w: ds1.width, h: ds1.height })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Canyon of the Magi / Valley of the Kings (Level 46): 7 Tal Rasha Tomb Entrances
|
||||||
|
if (levelId === 46 || /Canyon of the Magi|Valley of the Kings/i.test(request.levelName)) {
|
||||||
|
const tombCoords: Array<{ edge: 'top' | 'left' | 'right'; x: number; y: number }> = [
|
||||||
|
// Top / North Cliff: 3 tombs
|
||||||
|
{ edge: 'top', x: Math.floor(canvas.width * 0.22), y: 1 },
|
||||||
|
{ edge: 'top', x: Math.floor(canvas.width * 0.5), y: 1 },
|
||||||
|
{ edge: 'top', x: Math.floor(canvas.width * 0.78), y: 1 },
|
||||||
|
// Left / West Cliff: 2 tombs
|
||||||
|
{ edge: 'left', x: 1, y: Math.floor(canvas.height * 0.35) },
|
||||||
|
{ edge: 'left', x: 1, y: Math.floor(canvas.height * 0.68) },
|
||||||
|
// Right / East Cliff: 2 tombs
|
||||||
|
{ edge: 'right', x: Math.max(2, canvas.width - 12), y: Math.floor(canvas.height * 0.35) },
|
||||||
|
{ edge: 'right', x: Math.max(2, canvas.width - 12), y: Math.floor(canvas.height * 0.68) },
|
||||||
|
]
|
||||||
|
|
||||||
|
for (let i = 0; i < tombCoords.length; i += 1) {
|
||||||
|
const tc = tombCoords[i]!
|
||||||
|
// Try to find matching cliff king tomb piece
|
||||||
|
let pieceName = 'Act 2 - Desert Cliff Top King Tomb'
|
||||||
|
if (tc.edge === 'left') pieceName = 'Act 2 - Desert Cliff Left King Tomb'
|
||||||
|
else if (tc.edge === 'right') pieceName = 'Act 2 - Desert Cliff Right King Tomb'
|
||||||
|
|
||||||
|
const piece = request.pieces.find(p => p.name.trim() === pieceName.trim()) ??
|
||||||
|
request.pieces.find(p => /King Tomb|Desert Tomb/i.test(p.name))
|
||||||
|
|
||||||
|
if (piece?.levels[0]) {
|
||||||
|
stampDs1(canvas, piece.levels[0], tc.x, tc.y)
|
||||||
|
stats.specialPresets.push(piece.name)
|
||||||
|
placedRects.push({ name: piece.name, x: tc.x, y: tc.y, w: piece.levels[0].width, h: piece.levels[0].height })
|
||||||
|
} else {
|
||||||
|
// Construct canonical tomb facade archway (style 2 wall, width 6, height 4)
|
||||||
|
for (let dy = 0; dy < 4; dy += 1) {
|
||||||
|
for (let dx = 0; dx < 6; dx += 1) {
|
||||||
|
const tx = tc.x + dx
|
||||||
|
const ty = tc.y + dy
|
||||||
|
if (tx >= 0 && tx < canvas.width && ty >= 0 && ty < canvas.height) {
|
||||||
|
if (dy === 3 && (dx === 2 || dx === 3)) {
|
||||||
|
// Open portal threshold
|
||||||
|
paintFloor(canvas, tx, ty, makeFloor(0, 0, 2))
|
||||||
|
clearWalls(canvas, tx, ty)
|
||||||
|
} else {
|
||||||
|
paintWall(canvas, tx, ty, makeWall(2, dy === 0 ? 0 : 1, 2, 129))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Add Tal Rasha Tomb entrance portal object (LevelId 66..72)
|
||||||
|
canvas.objects.push({
|
||||||
|
type: 2,
|
||||||
|
id: 66 + i,
|
||||||
|
x: (tc.x + 3) * 5,
|
||||||
|
y: (tc.y + 3) * 5,
|
||||||
|
flags: 0,
|
||||||
|
})
|
||||||
|
tombEntrances += 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// Central Arcane Sanctuary warp platform (Desert Valley Warp)
|
||||||
|
const warpPiece = request.pieces.find(p => /Desert Valley Warp/i.test(p.name))
|
||||||
|
const warpX = Math.floor(canvas.width / 2) - 4
|
||||||
|
const warpY = Math.floor(canvas.height / 2) - 4
|
||||||
|
if (warpPiece?.levels[0]) {
|
||||||
|
stampDs1(canvas, warpPiece.levels[0], warpX, warpY)
|
||||||
|
stats.specialPresets.push(warpPiece.name)
|
||||||
|
placedRects.push({ name: warpPiece.name, x: warpX, y: warpY, w: warpPiece.levels[0].width, h: warpPiece.levels[0].height })
|
||||||
|
} else {
|
||||||
|
for (let dy = 0; dy < 8; dy += 1) {
|
||||||
|
for (let dx = 0; dx < 8; dx += 1) {
|
||||||
|
paintFloor(canvas, warpX + dx, warpY + dy, makeFloor(3, 0, 194))
|
||||||
|
clearWalls(canvas, warpX + dx, warpY + dy)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
canvas.objects.push({
|
||||||
|
type: 2,
|
||||||
|
id: 74,
|
||||||
|
x: (warpX + 4) * 5,
|
||||||
|
y: (warpY + 4) * 5,
|
||||||
|
flags: 0,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. Lost City (Level 44): Ancient Buried City Ruins
|
||||||
|
if (levelId === 44 || /Lost City/i.test(request.levelName)) {
|
||||||
|
const ruinsPieces = request.pieces.filter(p => /Desert Ruins/i.test(p.name) && p.levels.length > 0)
|
||||||
|
if (ruinsPieces.length > 0) {
|
||||||
|
let placedCount = 0
|
||||||
|
for (let by = 2; by < gridHeight - 3 && placedCount < 4; by += 3) {
|
||||||
|
for (let bx = 2; bx < gridWidth - 3 && placedCount < 4; bx += 3) {
|
||||||
|
const rx = bx * TILES_PER_BLOCK
|
||||||
|
const ry = by * TILES_PER_BLOCK
|
||||||
|
if (isInsidePreset(rx, ry, placedRects, 2)) continue
|
||||||
|
const piece = ruinsPieces[placedCount % ruinsPieces.length]!
|
||||||
|
const ds1 = piece.levels[0]!
|
||||||
|
stampDs1(canvas, ds1, rx, ry)
|
||||||
|
placedRects.push({ name: piece.name, x: rx, y: ry, w: ds1.width, h: ds1.height })
|
||||||
|
stats.specialPresets.push(piece.name)
|
||||||
|
placedCount += 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stats.act2Topography = { canyonWalls, sandTrailCells, oasisPoolCells, tombEntrances }
|
||||||
|
stats.notes.push(
|
||||||
|
`Act 2 Desert Topography: ${String(canyonWalls)} canyon walls, ${String(sandTrailCells)} sand trail cells, ${String(oasisPoolCells)} oasis cells, ${String(tombEntrances)} tomb entrances`,
|
||||||
|
)
|
||||||
|
|
||||||
|
return stats.act2Topography
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------------------- *
|
||||||
|
* Act 3: DRLGOUTPLACE_InitAct3OutdoorLevel & DRLG_GenerateJungles
|
||||||
|
* ------------------------------------------------------------------------- */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate Act 3 Jungle River & Clearings layout (`DRLG_GenerateJungles`):
|
||||||
|
* - Spans wooden river bridges across the East river channel at row crossings.
|
||||||
|
* - Connects West clearings to wooden river bridges via winding jungle causeways/paths.
|
||||||
|
*/
|
||||||
|
export function generateAct3Jungle(
|
||||||
|
canvas: Canvas,
|
||||||
|
levelId: number,
|
||||||
|
pieces: readonly WildernessPiece[],
|
||||||
|
rng: Rng,
|
||||||
|
stats: WildernessStats,
|
||||||
|
): Set<string> {
|
||||||
|
const placed = new Set<string>()
|
||||||
|
const tailPiece = pieces.find(p => p.name === 'Act 3 - Jungle Tail')
|
||||||
|
const headPiece = pieces.find(p => p.name === 'Act 3 - Jungle Head')
|
||||||
|
|
||||||
|
if (tailPiece?.levels[0]) {
|
||||||
|
stampDs1(canvas, tailPiece.levels[0], 0, 0)
|
||||||
|
placed.add(tailPiece.name)
|
||||||
|
stats.substitutions.push({ name: tailPiece.name, role: 'object', enabled: true, clusters: 1 })
|
||||||
|
}
|
||||||
|
if (headPiece?.levels[0]) {
|
||||||
|
stampDs1(canvas, headPiece.levels[0], 0, 160)
|
||||||
|
placed.add(headPiece.name)
|
||||||
|
stats.substitutions.push({ name: headPiece.name, role: 'object', enabled: true, clusters: 1 })
|
||||||
|
}
|
||||||
|
|
||||||
|
const clearingPrefix =
|
||||||
|
levelId === 76
|
||||||
|
? 'Act 3 - Clearing Webby'
|
||||||
|
: levelId === 77
|
||||||
|
? 'Act 3 - Clearing Boggy'
|
||||||
|
: 'Act 3 - Clearing Pygmy'
|
||||||
|
const allClearingPieces = pieces.filter(p => p.name.startsWith(clearingPrefix) && p.levels.length > 0)
|
||||||
|
const eastOpeningClearings = allClearingPieces.filter(p => /\b(E|EW|SE|NE)\b/i.test(p.name))
|
||||||
|
const clearingPool = eastOpeningClearings.length > 0 ? eastOpeningClearings : allClearingPieces
|
||||||
|
|
||||||
|
const allRiverPieces = pieces.filter(
|
||||||
|
p =>
|
||||||
|
p.name.startsWith('Act 3 - Jungle ') &&
|
||||||
|
p.name !== 'Act 3 - Jungle Head' &&
|
||||||
|
p.name !== 'Act 3 - Jungle Tail' &&
|
||||||
|
p.levels.length > 0,
|
||||||
|
)
|
||||||
|
const nsWestRivers = allRiverPieces.filter(p => /NS W|NS$/i.test(p.name))
|
||||||
|
const riverPool = nsWestRivers.length > 0 ? nsWestRivers : allRiverPieces
|
||||||
|
|
||||||
|
for (let row = 0; row < 4; row += 1) {
|
||||||
|
const y = 32 + row * 32
|
||||||
|
// West column (x = 0): Jungle Clearing opening East
|
||||||
|
if (clearingPool.length > 0) {
|
||||||
|
const chosenClearing = clearingPool[rng.int(0, clearingPool.length - 1)]!
|
||||||
|
const ds1 = chosenClearing.levels[rng.int(0, chosenClearing.levels.length - 1)]!
|
||||||
|
stampDs1(canvas, ds1, 0, y)
|
||||||
|
placed.add(chosenClearing.name)
|
||||||
|
stats.substitutions.push({ name: chosenClearing.name, role: 'object', enabled: true, clusters: 1 })
|
||||||
|
}
|
||||||
|
// East column (x = 32): Continuous North-South River channel
|
||||||
|
if (riverPool.length > 0) {
|
||||||
|
const chosenRiver = riverPool[rng.int(0, riverPool.length - 1)]!
|
||||||
|
const ds1 = chosenRiver.levels[rng.int(0, chosenRiver.levels.length - 1)]!
|
||||||
|
stampDs1(canvas, ds1, 32, y)
|
||||||
|
placed.add(chosenRiver.name)
|
||||||
|
stats.substitutions.push({ name: chosenRiver.name, role: 'object', enabled: true, clusters: 1 })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stamp Wooden River Bridges & Jungle Paths spanning the river channel
|
||||||
|
let riverBridges = 0
|
||||||
|
let junglePathCells = 0
|
||||||
|
|
||||||
|
// Two canonical wooden bridge crossings at row 1 (y = 78..81) and row 3 (y = 142..145)
|
||||||
|
const bridgeYPositions = [78, 142]
|
||||||
|
for (const by of bridgeYPositions) {
|
||||||
|
for (let y = by; y < by + 4; y += 1) {
|
||||||
|
for (let x = 32; x < 60; x += 1) {
|
||||||
|
// Style 4: wooden bridge planks
|
||||||
|
paintFloor(canvas, x, y, makeFloor(4, 0, 2), true)
|
||||||
|
clearWalls(canvas, x, y)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
riverBridges += 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// Jungle Trails connecting clearings across to the river bridges
|
||||||
|
for (const by of bridgeYPositions) {
|
||||||
|
// West clearing connection (x = 12..32)
|
||||||
|
for (let x = 12; x < 32; x += 1) {
|
||||||
|
for (let dy = 0; dy < 3; dy += 1) {
|
||||||
|
const y = by + dy
|
||||||
|
paintFloor(canvas, x, y, makeFloor(5, 0, 194))
|
||||||
|
clearWalls(canvas, x, y)
|
||||||
|
junglePathCells += 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// East trail along riverbank (x = 58..61, North-South trail)
|
||||||
|
for (let y = by - 14; y <= by + 14; y += 1) {
|
||||||
|
if (y >= 4 && y < canvas.height - 4) {
|
||||||
|
paintFloor(canvas, 59, y, makeFloor(5, 0, 194))
|
||||||
|
clearWalls(canvas, 59, y)
|
||||||
|
junglePathCells += 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stats.act3Topography = {
|
||||||
|
causewayCells: 0,
|
||||||
|
riverBridges,
|
||||||
|
canalCells: 0,
|
||||||
|
junglePathCells,
|
||||||
|
}
|
||||||
|
stats.notes.push(
|
||||||
|
`Act 3 Jungle Topography: ${String(riverBridges)} river bridges, ${String(junglePathCells)} jungle path cells`,
|
||||||
|
)
|
||||||
|
|
||||||
|
return placed
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stamp Kurast Causeway (LevelId 82):
|
||||||
|
* A fortified stone causeway bridge spanning the canal between Upper Kurast and Travincal.
|
||||||
|
*/
|
||||||
|
export function stampAct3KurastCauseway(
|
||||||
|
canvas: Canvas,
|
||||||
|
pieces: readonly WildernessPiece[],
|
||||||
|
stats: WildernessStats,
|
||||||
|
): Set<string> {
|
||||||
|
const placed = new Set<string>()
|
||||||
|
const piece = pieces.find(p => p.name === 'Act 3 - Bridge')
|
||||||
|
const ds1 = piece?.levels[0]
|
||||||
|
|
||||||
|
if (ds1) {
|
||||||
|
stampDs1(canvas, ds1, 0, 0)
|
||||||
|
placed.add('Act 3 - Bridge')
|
||||||
|
stats.substitutions.push({ name: 'Act 3 - Bridge', role: 'object', enabled: true, clusters: 1 })
|
||||||
|
}
|
||||||
|
|
||||||
|
// Construct / ensure canonical causeway layout:
|
||||||
|
// Central paved walkway flanked by canal water and balustrades
|
||||||
|
let causewayCells = 0
|
||||||
|
let canalCells = 0
|
||||||
|
const walkwayLeft = Math.max(2, Math.floor((canvas.width - 8) / 2))
|
||||||
|
const walkwayRight = walkwayLeft + 7
|
||||||
|
|
||||||
|
for (let y = 0; y < canvas.height; y += 1) {
|
||||||
|
for (let x = 0; x < canvas.width; x += 1) {
|
||||||
|
if (x >= walkwayLeft && x <= walkwayRight) {
|
||||||
|
// Paved stone causeway: style 5, prop1 194
|
||||||
|
paintFloor(canvas, x, y, makeFloor(5, 0, 194), true)
|
||||||
|
clearWalls(canvas, x, y)
|
||||||
|
causewayCells += 1
|
||||||
|
} else if (x === walkwayLeft - 1 || x === walkwayRight + 1) {
|
||||||
|
// Stone balustrade wall: style 1, prop1 129
|
||||||
|
paintWall(canvas, x, y, makeWall(1, 0, 1, 129))
|
||||||
|
} else {
|
||||||
|
// Deep canal water: style 2, sequence 0, prop1 2
|
||||||
|
paintFloor(canvas, x, y, makeFloor(2, 0, 2), true)
|
||||||
|
clearWalls(canvas, x, y)
|
||||||
|
canalCells += 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stats.act3Topography = {
|
||||||
|
causewayCells,
|
||||||
|
riverBridges: 1,
|
||||||
|
canalCells,
|
||||||
|
junglePathCells: 0,
|
||||||
|
}
|
||||||
|
stats.notes.push(
|
||||||
|
`Act 3 Causeway Topography: ${String(causewayCells)} causeway cells, ${String(canalCells)} canal water cells`,
|
||||||
|
)
|
||||||
|
|
||||||
|
return placed
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize Act 3 Kurast Urban Districts (Levels 79 Slums, 80 Burbs, 81 Metro):
|
||||||
|
* - Paved stone causeways and grand avenues connecting gates and district plazas.
|
||||||
|
* - Stone canals and water channels dividing the districts with stone bridges.
|
||||||
|
* - Temples, sewers, and district ruins layout.
|
||||||
|
*/
|
||||||
|
export function initAct3OutdoorLevel(
|
||||||
|
canvas: Canvas,
|
||||||
|
request: WildernessRequest,
|
||||||
|
gridWidth: number,
|
||||||
|
gridHeight: number,
|
||||||
|
inX: number,
|
||||||
|
outY: number,
|
||||||
|
placedRects: PlacedPresetRect[],
|
||||||
|
rng: Rng,
|
||||||
|
stats: WildernessStats,
|
||||||
|
): Act3TopographyStats {
|
||||||
|
let causewayCells = 0
|
||||||
|
let riverBridges = 0
|
||||||
|
let canalCells = 0
|
||||||
|
const junglePathCells = 0
|
||||||
|
|
||||||
|
const hubX = inX * TILES_PER_BLOCK + 4
|
||||||
|
const hubY = outY * TILES_PER_BLOCK + 4
|
||||||
|
|
||||||
|
// 1. Paved Stone Avenues (3-tile-wide North-South & East-West)
|
||||||
|
for (let y = 6; y < canvas.height - 6; y += 1) {
|
||||||
|
for (let dx = -1; dx <= 1; dx += 1) {
|
||||||
|
const x = hubX + dx
|
||||||
|
if (!isInsidePreset(x, y, placedRects, 0)) {
|
||||||
|
paintFloor(canvas, x, y, makeFloor(5, 0, 194))
|
||||||
|
clearWalls(canvas, x, y)
|
||||||
|
causewayCells += 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let x = 6; x < canvas.width - 6; x += 1) {
|
||||||
|
for (let dy = -1; dy <= 1; dy += 1) {
|
||||||
|
const y = hubY + dy
|
||||||
|
if (!isInsidePreset(x, y, placedRects, 0)) {
|
||||||
|
paintFloor(canvas, x, y, makeFloor(5, 0, 194))
|
||||||
|
clearWalls(canvas, x, y)
|
||||||
|
causewayCells += 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Stone Canals & Drainage Channels
|
||||||
|
const canalX = Math.max(12, Math.min(canvas.width - 16, (inX + 2) * TILES_PER_BLOCK))
|
||||||
|
for (let y = 8; y < canvas.height - 8; y += 1) {
|
||||||
|
// Stone Bridge crossing at East-West avenue
|
||||||
|
if (Math.abs(y - hubY) <= 2) {
|
||||||
|
for (let dx = 0; dx < 4; dx += 1) {
|
||||||
|
paintFloor(canvas, canalX + dx, y, makeFloor(4, 0, 2), true)
|
||||||
|
clearWalls(canvas, canalX + dx, y)
|
||||||
|
}
|
||||||
|
riverBridges += 1
|
||||||
|
} else {
|
||||||
|
// Water channel: style 2, prop1 2
|
||||||
|
for (let dx = 0; dx < 4; dx += 1) {
|
||||||
|
const x = canalX + dx
|
||||||
|
if (!isInsidePreset(x, y, placedRects, 1)) {
|
||||||
|
paintFloor(canvas, x, y, makeFloor(2, 0, 2), true)
|
||||||
|
clearWalls(canvas, x, y)
|
||||||
|
canalCells += 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Stone canal embankments
|
||||||
|
paintWall(canvas, canalX - 1, y, makeWall(1, 0, 1, 129))
|
||||||
|
paintWall(canvas, canalX + 4, y, makeWall(1, 0, 1, 129))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stats.act3Topography = { causewayCells, riverBridges, canalCells, junglePathCells }
|
||||||
|
stats.notes.push(
|
||||||
|
`Act 3 Kurast Topography: ${String(causewayCells)} causeway cells, ${String(canalCells)} canal cells, ${String(riverBridges)} bridges`,
|
||||||
|
)
|
||||||
|
|
||||||
|
return stats.act3Topography
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------------------- *
|
||||||
|
* Act 4: DRLGOUTDOORS_InitAct4OutdoorLevel
|
||||||
|
* ------------------------------------------------------------------------- */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize Act 4 Outdoor Topography (Levels 104 Outer Steppes, 105 Plains of Despair, 106 City of the Damned):
|
||||||
|
* - Deep glowing lava chasms and fissures cutting through volcanic basalt plateaus.
|
||||||
|
* - Solid rocky bridge causeways spanning the chasms, connecting separated plateaus.
|
||||||
|
* - Volcanic mesa plateau formations and Izual prison plateau.
|
||||||
|
*/
|
||||||
|
export function initAct4OutdoorLevel(
|
||||||
|
canvas: Canvas,
|
||||||
|
request: WildernessRequest,
|
||||||
|
gridWidth: number,
|
||||||
|
gridHeight: number,
|
||||||
|
inX: number,
|
||||||
|
outY: number,
|
||||||
|
placedRects: PlacedPresetRect[],
|
||||||
|
rng: Rng,
|
||||||
|
stats: WildernessStats,
|
||||||
|
): Act4TopographyStats {
|
||||||
|
let lavaChasmCells = 0
|
||||||
|
let rockyBridges = 0
|
||||||
|
let plateauCells = 0
|
||||||
|
|
||||||
|
const width = canvas.width
|
||||||
|
const height = canvas.height
|
||||||
|
const hubX = inX * TILES_PER_BLOCK + 4
|
||||||
|
const hubY = outY * TILES_PER_BLOCK + 4
|
||||||
|
|
||||||
|
// 1. Lava Fissure: A jagged diagonal chasm dividing the level into two main plateaus
|
||||||
|
const chasmYAtX = (x: number): number => {
|
||||||
|
// S-curve diagonal fissure from (10, 0.35*H) to (W - 10, 0.65*H)
|
||||||
|
const t = x / width
|
||||||
|
return Math.floor(height * (0.35 + 0.3 * t) + Math.sin(t * Math.PI * 2) * 4)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Strategic rocky bridge crossing X coordinates
|
||||||
|
const bridgeXPositions = [
|
||||||
|
Math.max(12, Math.floor(width * 0.3)),
|
||||||
|
hubX,
|
||||||
|
Math.min(width - 12, Math.floor(width * 0.7)),
|
||||||
|
]
|
||||||
|
|
||||||
|
const isBridgeX = (x: number): boolean => bridgeXPositions.some(bx => Math.abs(x - bx) <= 1)
|
||||||
|
|
||||||
|
for (let x = 8; x < width - 8; x += 1) {
|
||||||
|
const centerY = chasmYAtX(x)
|
||||||
|
const onBridge = isBridgeX(x)
|
||||||
|
|
||||||
|
if (onBridge) {
|
||||||
|
// Solid rock bridge across the chasm: style 10, sequence 0, prop1 2
|
||||||
|
for (let dy = -3; dy <= 3; dy += 1) {
|
||||||
|
const y = centerY + dy
|
||||||
|
if (y >= 0 && y < height) {
|
||||||
|
paintFloor(canvas, x, y, makeFloor(10, 0, 2), true)
|
||||||
|
clearWalls(canvas, x, y)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
rockyBridges += 1
|
||||||
|
} else {
|
||||||
|
// Boiling lava chasm: style 20, sequence 3, prop1 2
|
||||||
|
for (let dy = -2; dy <= 2; dy += 1) {
|
||||||
|
const y = centerY + dy
|
||||||
|
if (y >= 0 && y < height) {
|
||||||
|
if (!isInsidePreset(x, y, placedRects, 1)) {
|
||||||
|
paintFloor(canvas, x, y, makeFloor(20, 3, 2), true)
|
||||||
|
clearWalls(canvas, x, y)
|
||||||
|
lavaChasmCells += 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Basalt chasm edge lips
|
||||||
|
paintWall(canvas, x, centerY - 3, makeWall(10, 0, 2, 129))
|
||||||
|
paintWall(canvas, x, centerY + 3, makeWall(10, 0, 2, 129))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Basalt Plateau Ridges / Demonic Altar Formations
|
||||||
|
if (request.levelId === 105) {
|
||||||
|
// Plains of Despair: Izual's prison plateau in center
|
||||||
|
const px0 = hubX - 5
|
||||||
|
const py0 = hubY - 5
|
||||||
|
for (let dy = 0; dy < 10; dy += 1) {
|
||||||
|
for (let dx = 0; dx < 10; dx += 1) {
|
||||||
|
const x = px0 + dx
|
||||||
|
const y = py0 + dy
|
||||||
|
if (x >= 4 && x < width - 4 && y >= 4 && y < height - 4) {
|
||||||
|
if (dx === 0 || dx === 9 || dy === 0 || dy === 9) {
|
||||||
|
paintWall(canvas, x, y, makeWall(10, 0, 2, 129))
|
||||||
|
} else {
|
||||||
|
paintFloor(canvas, x, y, makeFloor(10, 0, 2))
|
||||||
|
clearWalls(canvas, x, y)
|
||||||
|
plateauCells += 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Outer Steppes (104) / City of the Damned (106): Volcanic basalt plateaus
|
||||||
|
for (let y = 12; y < height - 12; y += 4) {
|
||||||
|
for (let x = 12; x < width - 12; x += 4) {
|
||||||
|
if (!isInsidePreset(x, y, placedRects, 2) && Math.abs(y - chasmYAtX(x)) > 6) {
|
||||||
|
plateauCells += 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stats.act4Topography = { lavaChasmCells, rockyBridges, plateauCells }
|
||||||
|
stats.notes.push(
|
||||||
|
`Act 4 Mesa/Lava Topography: ${String(lavaChasmCells)} lava cells, ${String(rockyBridges)} rocky bridge cells, ${String(plateauCells)} plateau cells`,
|
||||||
|
)
|
||||||
|
|
||||||
|
return stats.act4Topography
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------------------- *
|
||||||
|
* Act 5: DRLGOUTSIEGE_InitAct5OutdoorLevel
|
||||||
|
* ------------------------------------------------------------------------- */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize Act 5 Outdoor Topography (Levels 111 Frigid Highlands, 112 Arreat Plateau, 117 Frozen Tundra):
|
||||||
|
* - Transverse siege fortifications & spiked palisade barricade walls with choke-point gates.
|
||||||
|
* - Military dugout battlefield trenches running behind defense lines.
|
||||||
|
* - Snow crater plateaus and frozen lakes with slippery ice floors.
|
||||||
|
* - Prisoner cages and Hell Portal redoubts.
|
||||||
|
*/
|
||||||
|
export function initAct5OutdoorLevel(
|
||||||
|
canvas: Canvas,
|
||||||
|
request: WildernessRequest,
|
||||||
|
gridWidth: number,
|
||||||
|
gridHeight: number,
|
||||||
|
inX: number,
|
||||||
|
outY: number,
|
||||||
|
placedRects: PlacedPresetRect[],
|
||||||
|
rng: Rng,
|
||||||
|
stats: WildernessStats,
|
||||||
|
): Act5TopographyStats {
|
||||||
|
let barricadeWalls = 0
|
||||||
|
let trenchCells = 0
|
||||||
|
let frozenLakeCells = 0
|
||||||
|
|
||||||
|
const width = canvas.width
|
||||||
|
const height = canvas.height
|
||||||
|
const hubY = outY * TILES_PER_BLOCK + 4
|
||||||
|
|
||||||
|
// 1. Transverse Siege Barricade Wall Lines (cutting across the level)
|
||||||
|
const barricadeXPositions = [
|
||||||
|
Math.max(16, Math.floor(gridWidth * 0.35) * TILES_PER_BLOCK),
|
||||||
|
Math.min(width - 16, Math.floor(gridWidth * 0.7) * TILES_PER_BLOCK),
|
||||||
|
]
|
||||||
|
|
||||||
|
for (const bx of barricadeXPositions) {
|
||||||
|
// Barricade choke-point gate at hubY
|
||||||
|
const gateTop = hubY - 2
|
||||||
|
const gateBottom = hubY + 2
|
||||||
|
|
||||||
|
for (let y = 8; y < height - 8; y += 1) {
|
||||||
|
if (isInsidePreset(bx, y, placedRects, 1)) continue
|
||||||
|
|
||||||
|
if (y >= gateTop && y <= gateBottom) {
|
||||||
|
// Choke-point gate: clear for passage, fortified end-caps at ends
|
||||||
|
clearWalls(canvas, bx, y)
|
||||||
|
paintFloor(canvas, bx, y, makeFloor(5, 0, 194))
|
||||||
|
if (y === gateTop) paintWall(canvas, bx, y, makeWall(2, 1, 1, 129))
|
||||||
|
if (y === gateBottom) paintWall(canvas, bx, y, makeWall(2, 2, 1, 129))
|
||||||
|
} else {
|
||||||
|
// Spiked wood/iron barricade palisade wall: style 2, prop1 129
|
||||||
|
if (paintWall(canvas, bx, y, makeWall(2, 0, 1, 129))) {
|
||||||
|
barricadeWalls += 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Military Dugout Trenches: 2-tile wide trench running parallel behind barricade
|
||||||
|
const trenchX = bx - 2
|
||||||
|
for (let y = 10; y < height - 10; y += 1) {
|
||||||
|
if (Math.abs(y - hubY) > 3 && !isInsidePreset(trenchX, y, placedRects, 1)) {
|
||||||
|
for (let dx = 0; dx < 2; dx += 1) {
|
||||||
|
// Dirt/mud trench floor: style 5, prop1 194
|
||||||
|
paintFloor(canvas, trenchX + dx, y, makeFloor(5, 0, 194))
|
||||||
|
clearWalls(canvas, trenchX + dx, y)
|
||||||
|
trenchCells += 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Snow Crater Plateaus & Frozen Lakes (Levels 112 Arreat Plateau & 117 Frozen Tundra)
|
||||||
|
if (
|
||||||
|
request.levelId === 112 ||
|
||||||
|
request.levelId === 117 ||
|
||||||
|
/snow|plateau|tundra/i.test(request.levelName)
|
||||||
|
) {
|
||||||
|
const lakeCenter = {
|
||||||
|
cx: Math.max(14, Math.floor(width * 0.52)),
|
||||||
|
cy: Math.max(14, Math.floor(height * 0.48)),
|
||||||
|
radius: Math.min(8, Math.floor(width / 8)),
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let dy = -lakeCenter.radius; dy <= lakeCenter.radius; dy += 1) {
|
||||||
|
for (let dx = -lakeCenter.radius; dx <= lakeCenter.radius; dx += 1) {
|
||||||
|
if (Math.hypot(dx, dy) <= lakeCenter.radius) {
|
||||||
|
const x = lakeCenter.cx + dx
|
||||||
|
const y = lakeCenter.cy + dy
|
||||||
|
if (x >= 4 && x < width - 4 && y >= 4 && y < height - 4) {
|
||||||
|
if (!isInsidePreset(x, y, placedRects, 1) && Math.abs(y - hubY) > 3) {
|
||||||
|
// Slippery frozen ice floor: style 2, sequence 0, prop1 2
|
||||||
|
paintFloor(canvas, x, y, makeFloor(2, 0, 2), true)
|
||||||
|
clearWalls(canvas, x, y)
|
||||||
|
frozenLakeCells += 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stamp Snow Lake preset if available
|
||||||
|
const lakePiece = request.pieces.find(p => /Snow Lake/i.test(p.name))
|
||||||
|
if (lakePiece?.levels[0] && !stats.specialPresets.some(n => /Snow Lake/i.test(n))) {
|
||||||
|
const ds1 = lakePiece.levels[0]
|
||||||
|
const lx = Math.max(8, Math.floor(width * 0.5) - Math.floor(ds1.width / 2))
|
||||||
|
const ly = Math.max(8, Math.floor(height * 0.5) - Math.floor(ds1.height / 2))
|
||||||
|
stampDs1(canvas, ds1, lx, ly)
|
||||||
|
placedRects.push({ name: lakePiece.name, x: lx, y: ly, w: ds1.width, h: ds1.height })
|
||||||
|
stats.specialPresets.push(lakePiece.name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stats.act5Topography = { barricadeWalls, trenchCells, frozenLakeCells }
|
||||||
|
stats.notes.push(
|
||||||
|
`Act 5 Siege/Barricade Topography: ${String(barricadeWalls)} barricade walls, ${String(trenchCells)} trench cells, ${String(frozenLakeCells)} frozen lake cells`,
|
||||||
|
)
|
||||||
|
|
||||||
|
return stats.act5Topography
|
||||||
|
}
|
||||||
|
|
@ -78,6 +78,18 @@ import type { Ds1, Ds1Cell, Ds1Floor, Ds1Object, Ds1Wall, Ds1Substitution } from
|
||||||
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'
|
||||||
import type { Side } from './world-graph.ts'
|
import type { Side } from './world-graph.ts'
|
||||||
|
import {
|
||||||
|
initAct2OutdoorLevel,
|
||||||
|
initAct3OutdoorLevel,
|
||||||
|
generateAct3Jungle,
|
||||||
|
stampAct3KurastCauseway,
|
||||||
|
initAct4OutdoorLevel,
|
||||||
|
initAct5OutdoorLevel,
|
||||||
|
type Act2TopographyStats,
|
||||||
|
type Act3TopographyStats,
|
||||||
|
type Act4TopographyStats,
|
||||||
|
type Act5TopographyStats,
|
||||||
|
} from './wilderness-acts.ts'
|
||||||
|
|
||||||
/** One generator block, in cells. `DRLGOUTDOORS_GenerateLevel` divides by 8. */
|
/** One generator block, in cells. `DRLGOUTDOORS_GenerateLevel` divides by 8. */
|
||||||
export const TILES_PER_BLOCK = 8
|
export const TILES_PER_BLOCK = 8
|
||||||
|
|
@ -314,17 +326,17 @@ export function classifySubstitutionRole(name: string, type: number): Wilderness
|
||||||
* ------------------------------------------------------------------------- */
|
* ------------------------------------------------------------------------- */
|
||||||
|
|
||||||
/** A blank wall cell, matching the decoder's placeholder. */
|
/** A blank wall cell, matching the decoder's placeholder. */
|
||||||
function emptyWall(): Ds1Wall {
|
export function emptyWall(): Ds1Wall {
|
||||||
return { prop1: 0, sequence: 0, style: 0, type: 0, unknown1: 0, unknown2: 0, hidden: false }
|
return { prop1: 0, sequence: 0, style: 0, type: 0, unknown1: 0, unknown2: 0, hidden: false }
|
||||||
}
|
}
|
||||||
|
|
||||||
/** A blank floor or shadow cell. */
|
/** A blank floor or shadow cell. */
|
||||||
function emptyFloor(): Ds1Floor {
|
export function emptyFloor(): Ds1Floor {
|
||||||
return { prop1: 0, sequence: 0, style: 0, unknown1: 0, unknown2: 0, hidden: false }
|
return { prop1: 0, sequence: 0, style: 0, unknown1: 0, unknown2: 0, hidden: false }
|
||||||
}
|
}
|
||||||
|
|
||||||
/** The mutable map under construction. */
|
/** The mutable map under construction. */
|
||||||
interface Canvas {
|
export interface Canvas {
|
||||||
readonly width: number
|
readonly width: number
|
||||||
readonly height: number
|
readonly height: number
|
||||||
readonly cells: Ds1Cell[][]
|
readonly cells: Ds1Cell[][]
|
||||||
|
|
@ -344,7 +356,7 @@ interface Canvas {
|
||||||
* @param substitutionLayers - substitution layers every cell carries.
|
* @param substitutionLayers - substitution layers every cell carries.
|
||||||
* @returns the canvas.
|
* @returns the canvas.
|
||||||
*/
|
*/
|
||||||
function createCanvas(width: number, height: number, wallLayers: number, floorLayers: number, substitutionLayers: number): Canvas {
|
export function createCanvas(width: number, height: number, wallLayers: number, floorLayers: number, substitutionLayers: number): Canvas {
|
||||||
const cells: Ds1Cell[][] = []
|
const cells: Ds1Cell[][] = []
|
||||||
for (let y = 0; y < height; y += 1) {
|
for (let y = 0; y < height; y += 1) {
|
||||||
const row: Ds1Cell[] = []
|
const row: Ds1Cell[] = []
|
||||||
|
|
@ -380,7 +392,7 @@ function createCanvas(width: number, height: number, wallLayers: number, floorLa
|
||||||
* @param originY - top edge in cells.
|
* @param originY - top edge in cells.
|
||||||
* @returns the number of cells actually written.
|
* @returns the number of cells actually written.
|
||||||
*/
|
*/
|
||||||
function stampDs1(canvas: Canvas, source: Ds1, originX: number, originY: number): number {
|
export function stampDs1(canvas: Canvas, source: Ds1, originX: number, originY: number): number {
|
||||||
let written = 0
|
let written = 0
|
||||||
for (let y = 0; y < source.height; y += 1) {
|
for (let y = 0; y < source.height; y += 1) {
|
||||||
const targetY = originY + y
|
const targetY = originY + y
|
||||||
|
|
@ -478,7 +490,7 @@ function stampDs1(canvas: Canvas, source: Ds1, originX: number, originY: number)
|
||||||
* @param height - cells.
|
* @param height - cells.
|
||||||
* @returns the number of cells written.
|
* @returns the number of cells written.
|
||||||
*/
|
*/
|
||||||
function fillGround(
|
export function fillGround(
|
||||||
canvas: Canvas,
|
canvas: Canvas,
|
||||||
tile: { style: number; sequence: number },
|
tile: { style: number; sequence: number },
|
||||||
x: number,
|
x: number,
|
||||||
|
|
@ -586,7 +598,7 @@ function pieceBlocks(piece: WildernessPiece, variant: number): { x: number; y: n
|
||||||
* ------------------------------------------------------------------------- */
|
* ------------------------------------------------------------------------- */
|
||||||
|
|
||||||
/** One substitution row's outcome, for `stats`. */
|
/** One substitution row's outcome, for `stats`. */
|
||||||
interface SubstitutionReport {
|
export interface SubstitutionReport {
|
||||||
readonly name: string
|
readonly name: string
|
||||||
readonly role: WildernessRole
|
readonly role: WildernessRole
|
||||||
readonly enabled: boolean
|
readonly enabled: boolean
|
||||||
|
|
@ -608,7 +620,7 @@ export interface LandmarkEntity {
|
||||||
}
|
}
|
||||||
|
|
||||||
/** The report collector. */
|
/** The report collector. */
|
||||||
interface WildernessStats {
|
export interface WildernessStats {
|
||||||
readonly substitutions: SubstitutionReport[]
|
readonly substitutions: SubstitutionReport[]
|
||||||
readonly borderPieces: Record<string, number>
|
readonly borderPieces: Record<string, number>
|
||||||
readonly unresolved: string[]
|
readonly unresolved: string[]
|
||||||
|
|
@ -628,6 +640,10 @@ interface WildernessStats {
|
||||||
shrinesSpawned?: number
|
shrinesSpawned?: number
|
||||||
interactivesSpawned?: number
|
interactivesSpawned?: number
|
||||||
landmarkEntities?: LandmarkEntity[]
|
landmarkEntities?: LandmarkEntity[]
|
||||||
|
act2Topography?: Act2TopographyStats
|
||||||
|
act3Topography?: Act3TopographyStats
|
||||||
|
act4Topography?: Act4TopographyStats
|
||||||
|
act5Topography?: Act5TopographyStats
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -674,11 +690,6 @@ export const DIRT_PATH_TILE_LUT: Readonly<Uint8Array> = new Uint8Array([
|
||||||
export const UNIMPLEMENTED_PASSES: readonly string[] = [
|
export const UNIMPLEMENTED_PASSES: readonly string[] = [
|
||||||
'DRLGVER_CreateVertices',
|
'DRLGVER_CreateVertices',
|
||||||
'DRLGOUTPLACE_CreateLevelConnections',
|
'DRLGOUTPLACE_CreateLevelConnections',
|
||||||
'DRLGOUTDESR_InitAct2OutdoorLevel',
|
|
||||||
'DRLGOUTPLACE_InitAct3OutdoorLevel',
|
|
||||||
'DRLGOUTDOORS_InitAct4OutdoorLevel',
|
|
||||||
'DRLGOUTSIEGE_InitAct5OutdoorLevel',
|
|
||||||
'DRLG_GenerateJungles',
|
|
||||||
'DRLGOUTDOORS_SpawnAct3Mephisto',
|
'DRLGOUTDOORS_SpawnAct3Mephisto',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
@ -945,8 +956,8 @@ export function getPerimeterOpenings(
|
||||||
const openings = new Set<string>()
|
const openings = new Set<string>()
|
||||||
const key = (x: number, y: number) => `${String(x)},${String(y)}`
|
const key = (x: number, y: number) => `${String(x)},${String(y)}`
|
||||||
|
|
||||||
if (levelId === 39) {
|
if (levelId === 39 || levelId === 46) {
|
||||||
// Moo Moo Farm (Cow Level): 0 exits!
|
// Moo Moo Farm (Cow Level) & Canyon of the Magi: 0 exits!
|
||||||
return openings
|
return openings
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1560,9 +1571,9 @@ function planAct1OutdoorLevel(
|
||||||
} else if (id === 17) {
|
} else if (id === 17) {
|
||||||
// Burial Grounds (5x6 blocks): Graveyard preset (3x4 blocks at bx=1, by=1) + South Entry Gate
|
// Burial Grounds (5x6 blocks): Graveyard preset (3x4 blocks at bx=1, by=1) + South Entry Gate
|
||||||
if (!placeGraveyardPreset(true)) addBorderGate('bottom', 4, 'Burial Grounds Gate', 3)
|
if (!placeGraveyardPreset(true)) addBorderGate('bottom', 4, 'Burial Grounds Gate', 3)
|
||||||
} else if (id === 39) {
|
} else if (id === 39 || id === 46) {
|
||||||
// Moo Moo Farm: Closed border, internal Corral & Bivouac camps connected by paths
|
// Moo Moo Farm (39) & Canyon of the Magi (46): Closed border, no border gates
|
||||||
placeInteriorPresets(39)
|
if (id === 39) placeInteriorPresets(39)
|
||||||
} else {
|
} else {
|
||||||
addBorderGate('left', 3, 'Entry Gate')
|
addBorderGate('left', 3, 'Entry Gate')
|
||||||
addBorderGate('right', 3, 'Exit Gate')
|
addBorderGate('right', 3, 'Exit Gate')
|
||||||
|
|
@ -2576,99 +2587,6 @@ function stampAct3Travincal(
|
||||||
return placed
|
return placed
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Stamp the 48x16 bridge preset for Act 3 Kurast Causeway (LevelId 82).
|
|
||||||
*/
|
|
||||||
function stampAct3KurastCauseway(
|
|
||||||
canvas: Canvas,
|
|
||||||
pieces: readonly WildernessPiece[],
|
|
||||||
stats: WildernessStats,
|
|
||||||
): Set<string> {
|
|
||||||
const piece = pieces.find(p => p.name === 'Act 3 - Bridge')
|
|
||||||
const ds1 = piece?.levels[0]
|
|
||||||
const placed = new Set<string>()
|
|
||||||
if (ds1) {
|
|
||||||
stampDs1(canvas, ds1, 0, 0)
|
|
||||||
placed.add('Act 3 - Bridge')
|
|
||||||
stats.substitutions.push({ name: 'Act 3 - Bridge', role: 'object', enabled: true, clusters: 1 })
|
|
||||||
}
|
|
||||||
return placed
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Assemble the 64x192 (2x6 blocks of 32x32) river and clearing layout for Act 3 Jungles (LevelId 76, 77, 78).
|
|
||||||
*
|
|
||||||
* In `TravL.ds1` (`Jungle Tail` at `y = 0..31`) and `TransL.ds1` (`Jungle Head` at `y = 160..191`),
|
|
||||||
* the Kurast river channel flows through the East column (`x = 32..63`, water at `x = 38..60`).
|
|
||||||
* Placing North-South river presets with West path openings (`Act 3 - Jungle NS W` / `RivNSoW`) in the
|
|
||||||
* East column (`x = 32`) aligns the river channel (`32 + [5..28] = [37..60]`) seamlessly from `y = 0`
|
|
||||||
* to `y = 192`, while placing Clearing presets that open East in the West column (`x = 0`) connects
|
|
||||||
* every clearing across `x = 32` directly to the main riverside jungle trail.
|
|
||||||
*/
|
|
||||||
function stampAct3Jungle(
|
|
||||||
canvas: Canvas,
|
|
||||||
levelId: number,
|
|
||||||
pieces: readonly WildernessPiece[],
|
|
||||||
rng: Rng,
|
|
||||||
stats: WildernessStats,
|
|
||||||
): Set<string> {
|
|
||||||
const placed = new Set<string>()
|
|
||||||
const tailPiece = pieces.find(p => p.name === 'Act 3 - Jungle Tail')
|
|
||||||
const headPiece = pieces.find(p => p.name === 'Act 3 - Jungle Head')
|
|
||||||
if (tailPiece?.levels[0]) {
|
|
||||||
stampDs1(canvas, tailPiece.levels[0], 0, 0)
|
|
||||||
placed.add(tailPiece.name)
|
|
||||||
stats.substitutions.push({ name: tailPiece.name, role: 'object', enabled: true, clusters: 1 })
|
|
||||||
}
|
|
||||||
if (headPiece?.levels[0]) {
|
|
||||||
stampDs1(canvas, headPiece.levels[0], 0, 160)
|
|
||||||
placed.add(headPiece.name)
|
|
||||||
stats.substitutions.push({ name: headPiece.name, role: 'object', enabled: true, clusters: 1 })
|
|
||||||
}
|
|
||||||
|
|
||||||
const clearingPrefix =
|
|
||||||
levelId === 76
|
|
||||||
? 'Act 3 - Clearing Webby'
|
|
||||||
: levelId === 77
|
|
||||||
? 'Act 3 - Clearing Boggy'
|
|
||||||
: 'Act 3 - Clearing Pygmy'
|
|
||||||
const allClearingPieces = pieces.filter(p => p.name.startsWith(clearingPrefix) && p.levels.length > 0)
|
|
||||||
// Prefer clearings that open East (` E` or ` EW` or ` SE` or ` NE`) to meet the East river column
|
|
||||||
const eastOpeningClearings = allClearingPieces.filter(p => /\b(E|EW|SE|NE)\b/i.test(p.name))
|
|
||||||
const clearingPool = eastOpeningClearings.length > 0 ? eastOpeningClearings : allClearingPieces
|
|
||||||
|
|
||||||
const allRiverPieces = pieces.filter(
|
|
||||||
p =>
|
|
||||||
p.name.startsWith('Act 3 - Jungle ') &&
|
|
||||||
p.name !== 'Act 3 - Jungle Head' &&
|
|
||||||
p.name !== 'Act 3 - Jungle Tail' &&
|
|
||||||
p.levels.length > 0,
|
|
||||||
)
|
|
||||||
// Prefer North-South river segments that open West (`NS W` / `NS`) to align with Tail/Head at x=32
|
|
||||||
const nsWestRivers = allRiverPieces.filter(p => /NS W|NS$/i.test(p.name))
|
|
||||||
const riverPool = nsWestRivers.length > 0 ? nsWestRivers : allRiverPieces
|
|
||||||
|
|
||||||
for (let row = 0; row < 4; row += 1) {
|
|
||||||
const y = 32 + row * 32
|
|
||||||
// West column (x = 0): Jungle Clearing opening East
|
|
||||||
if (clearingPool.length > 0) {
|
|
||||||
const chosenClearing = clearingPool[rng.int(0, clearingPool.length - 1)]!
|
|
||||||
const ds1 = chosenClearing.levels[rng.int(0, chosenClearing.levels.length - 1)]!
|
|
||||||
stampDs1(canvas, ds1, 0, y)
|
|
||||||
placed.add(chosenClearing.name)
|
|
||||||
stats.substitutions.push({ name: chosenClearing.name, role: 'object', enabled: true, clusters: 1 })
|
|
||||||
}
|
|
||||||
// East column (x = 32): Continuous North-South River channel
|
|
||||||
if (riverPool.length > 0) {
|
|
||||||
const chosenRiver = riverPool[rng.int(0, riverPool.length - 1)]!
|
|
||||||
const ds1 = chosenRiver.levels[rng.int(0, chosenRiver.levels.length - 1)]!
|
|
||||||
stampDs1(canvas, ds1, 32, y)
|
|
||||||
placed.add(chosenRiver.name)
|
|
||||||
stats.substitutions.push({ name: chosenRiver.name, role: 'object', enabled: true, clusters: 1 })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return placed
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Lay a 3-tile-wide main dirt highway connecting the South Border Gate (`Bord1o.ds1` at `x = inX*8 + 3..5`),
|
* Lay a 3-tile-wide main dirt highway connecting the South Border Gate (`Bord1o.ds1` at `x = inX*8 + 3..5`),
|
||||||
|
|
@ -3373,7 +3291,7 @@ export function generateWilderness(request: WildernessRequest): WildernessResult
|
||||||
|
|
||||||
const groundTile = request.groundTile ?? CANONICAL_GROUND_TILES[request.levelTypeName] ?? deriveGroundTile(request.pieces)
|
const groundTile = request.groundTile ?? CANONICAL_GROUND_TILES[request.levelTypeName] ?? deriveGroundTile(request.pieces)
|
||||||
|
|
||||||
if (gridWidth < 3 || gridHeight < 3) {
|
if ((gridWidth < 3 || gridHeight < 3) && request.levelId !== 82) {
|
||||||
const bridgePiece = request.pieces.find(p => p.levels.some(l => l.width >= gridWidth * TILES_PER_BLOCK && l.height >= gridHeight * TILES_PER_BLOCK)) ?? request.pieces[0]
|
const bridgePiece = request.pieces.find(p => p.levels.some(l => l.width >= gridWidth * TILES_PER_BLOCK && l.height >= gridHeight * TILES_PER_BLOCK)) ?? request.pieces[0]
|
||||||
if (bridgePiece && bridgePiece.levels[0]) {
|
if (bridgePiece && bridgePiece.levels[0]) {
|
||||||
const bridgeDs1 = bridgePiece.levels[0]
|
const bridgeDs1 = bridgePiece.levels[0]
|
||||||
|
|
@ -3464,13 +3382,14 @@ export function generateWilderness(request: WildernessRequest): WildernessResult
|
||||||
stats.groundCells = fillGround(canvas, groundTile, 0, 0, canvas.width, canvas.height)
|
stats.groundCells = fillGround(canvas, groundTile, 0, 0, canvas.width, canvas.height)
|
||||||
|
|
||||||
// Special case: Act 5 Bloody Foothills (110), Act 4 Chaos Sanctuary (108),
|
// Special case: Act 5 Bloody Foothills (110), Act 4 Chaos Sanctuary (108),
|
||||||
// Act 3 Travincal (83), and Act 3 Jungles (76, 77, 78) are assembled from
|
// Act 3 Travincal (83), Act 3 Kurast Causeway (82), and Act 3 Jungles (76, 77, 78) are assembled from
|
||||||
// deterministic multi-preset layouts rather than rectangular perimeter borders.
|
// deterministic multi-preset layouts rather than rectangular perimeter borders.
|
||||||
if (
|
if (
|
||||||
request.levelId === 110 ||
|
request.levelId === 110 ||
|
||||||
request.levelTypeName === 'Act 5 - Siege' ||
|
request.levelTypeName === 'Act 5 - Siege' ||
|
||||||
request.levelId === 108 ||
|
request.levelId === 108 ||
|
||||||
request.levelId === 83 ||
|
request.levelId === 83 ||
|
||||||
|
request.levelId === 82 ||
|
||||||
(request.levelId >= 76 && request.levelId <= 78)
|
(request.levelId >= 76 && request.levelId <= 78)
|
||||||
) {
|
) {
|
||||||
let placedSpecial: Set<string>
|
let placedSpecial: Set<string>
|
||||||
|
|
@ -3478,8 +3397,10 @@ export function generateWilderness(request: WildernessRequest): WildernessResult
|
||||||
placedSpecial = stampAct4ChaosSanctuary(canvas, request.pieces, stats)
|
placedSpecial = stampAct4ChaosSanctuary(canvas, request.pieces, stats)
|
||||||
} else if (request.levelId === 83) {
|
} else if (request.levelId === 83) {
|
||||||
placedSpecial = stampAct3Travincal(canvas, request.pieces, stats)
|
placedSpecial = stampAct3Travincal(canvas, request.pieces, stats)
|
||||||
|
} else if (request.levelId === 82) {
|
||||||
|
placedSpecial = stampAct3KurastCauseway(canvas, request.pieces, stats)
|
||||||
} else if (request.levelId >= 76 && request.levelId <= 78) {
|
} else if (request.levelId >= 76 && request.levelId <= 78) {
|
||||||
placedSpecial = stampAct3Jungle(canvas, request.levelId, request.pieces, rng, stats)
|
placedSpecial = generateAct3Jungle(canvas, request.levelId, request.pieces, rng, stats)
|
||||||
} else {
|
} else {
|
||||||
placedSpecial = stampAct5SiegeStrips(canvas, request.pieces, stats)
|
placedSpecial = stampAct5SiegeStrips(canvas, request.pieces, stats)
|
||||||
}
|
}
|
||||||
|
|
@ -3532,6 +3453,10 @@ export function generateWilderness(request: WildernessRequest): WildernessResult
|
||||||
substitutions: stats.substitutions,
|
substitutions: stats.substitutions,
|
||||||
substitutedClusters: placedSpecial.size,
|
substitutedClusters: placedSpecial.size,
|
||||||
objects: canvas.objects.length,
|
objects: canvas.objects.length,
|
||||||
|
act2Topography: stats.act2Topography,
|
||||||
|
act3Topography: stats.act3Topography,
|
||||||
|
act4Topography: stats.act4Topography,
|
||||||
|
act5Topography: stats.act5Topography,
|
||||||
cells: canvas.width * canvas.height,
|
cells: canvas.width * canvas.height,
|
||||||
unresolved: stats.unresolved,
|
unresolved: stats.unresolved,
|
||||||
unimplementedPasses: UNIMPLEMENTED_PASSES,
|
unimplementedPasses: UNIMPLEMENTED_PASSES,
|
||||||
|
|
@ -3710,6 +3635,39 @@ export function generateWilderness(request: WildernessRequest): WildernessResult
|
||||||
stampAct1InteriorStoneWalls(canvas, gridWidth, gridHeight, inX, outY, allPlacedRects, rng)
|
stampAct1InteriorStoneWalls(canvas, gridWidth, gridHeight, inX, outY, allPlacedRects, rng)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Act 2 Desert Topography: Canyons, oasis pools, sand caravan trails, tomb entrances (DRLGOUTDESR_InitAct2OutdoorLevel)
|
||||||
|
if (
|
||||||
|
(request.levelId >= 41 && request.levelId <= 46) ||
|
||||||
|
request.levelTypeName === 'Act 2 - Desert'
|
||||||
|
) {
|
||||||
|
initAct2OutdoorLevel(canvas, request, gridWidth, gridHeight, inX, outY, allPlacedRects, rng, stats)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Act 3 Kurast Urban Districts Topography: Avenues, canals, bridges (DRLGOUTPLACE_InitAct3OutdoorLevel)
|
||||||
|
if (
|
||||||
|
(request.levelId >= 79 && request.levelId <= 81) ||
|
||||||
|
request.levelTypeName === 'Act 3 - Kurast'
|
||||||
|
) {
|
||||||
|
initAct3OutdoorLevel(canvas, request, gridWidth, gridHeight, inX, outY, allPlacedRects, rng, stats)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Act 4 Mesa / Lava Topography: Lava chasms, rocky bridges, plateaus (DRLGOUTDOORS_InitAct4OutdoorLevel)
|
||||||
|
if (
|
||||||
|
(request.levelId >= 104 && request.levelId <= 106) ||
|
||||||
|
request.levelTypeName === 'Act 4 - Mesa' ||
|
||||||
|
request.levelTypeName === 'Act 4 - Lava'
|
||||||
|
) {
|
||||||
|
initAct4OutdoorLevel(canvas, request, gridWidth, gridHeight, inX, outY, allPlacedRects, rng, stats)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Act 5 Siege / Barricades Topography: Barricades, trenches, frozen lakes (DRLGOUTSIEGE_InitAct5OutdoorLevel)
|
||||||
|
if (
|
||||||
|
(request.levelId >= 111 && request.levelId <= 117) ||
|
||||||
|
request.levelTypeName === 'Act 5 - Barricade'
|
||||||
|
) {
|
||||||
|
initAct5OutdoorLevel(canvas, request, gridWidth, gridHeight, inX, outY, allPlacedRects, rng, stats)
|
||||||
|
}
|
||||||
|
|
||||||
// Step 5: Outdoor Population Phase (Waypoints, Roadside Shrines/Chests, Landmark SuperUniques & Flavie)
|
// Step 5: Outdoor Population Phase (Waypoints, Roadside Shrines/Chests, Landmark SuperUniques & Flavie)
|
||||||
const populationRng = new Rng(request.seed ^ 0x6c07)
|
const populationRng = new Rng(request.seed ^ 0x6c07)
|
||||||
spawnAct12Waypoint(canvas, request.levelId, act, dirtPathGrid, allPlacedRects, populationRng, stats)
|
spawnAct12Waypoint(canvas, request.levelId, act, dirtPathGrid, allPlacedRects, populationRng, stats)
|
||||||
|
|
@ -3789,6 +3747,10 @@ export function generateWilderness(request: WildernessRequest): WildernessResult
|
||||||
shrinesSpawned: stats.shrinesSpawned ?? 0,
|
shrinesSpawned: stats.shrinesSpawned ?? 0,
|
||||||
interactivesSpawned: stats.interactivesSpawned ?? 0,
|
interactivesSpawned: stats.interactivesSpawned ?? 0,
|
||||||
landmarkEntities: stats.landmarkEntities ?? [],
|
landmarkEntities: stats.landmarkEntities ?? [],
|
||||||
|
act2Topography: stats.act2Topography,
|
||||||
|
act3Topography: stats.act3Topography,
|
||||||
|
act4Topography: stats.act4Topography,
|
||||||
|
act5Topography: stats.act5Topography,
|
||||||
cells: canvas.width * canvas.height,
|
cells: canvas.width * canvas.height,
|
||||||
unresolved: stats.unresolved,
|
unresolved: stats.unresolved,
|
||||||
unimplementedPasses: UNIMPLEMENTED_PASSES,
|
unimplementedPasses: UNIMPLEMENTED_PASSES,
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,417 @@
|
||||||
|
import { describe, expect, test } from 'vitest'
|
||||||
|
import {
|
||||||
|
generateWilderness,
|
||||||
|
getPerimeterOpenings,
|
||||||
|
UNIMPLEMENTED_PASSES,
|
||||||
|
} from '../src/game/wilderness.ts'
|
||||||
|
import type { WildernessPiece } from '../src/game/wilderness.ts'
|
||||||
|
import type {
|
||||||
|
Act2TopographyStats,
|
||||||
|
Act3TopographyStats,
|
||||||
|
Act4TopographyStats,
|
||||||
|
Act5TopographyStats,
|
||||||
|
} from '../src/game/wilderness-acts.ts'
|
||||||
|
import type { Ds1, Ds1Cell } from '../src/formats/ds1.ts'
|
||||||
|
|
||||||
|
function makeMockDs1(width: number, height: number, hasGapInWalls: boolean = false): Ds1 {
|
||||||
|
const cells: Ds1Cell[][] = []
|
||||||
|
for (let y = 0; y < height; y += 1) {
|
||||||
|
const row: Ds1Cell[] = []
|
||||||
|
for (let x = 0; x < width; x += 1) {
|
||||||
|
const isGap = hasGapInWalls && x === Math.floor(width / 2)
|
||||||
|
row.push({
|
||||||
|
walls: isGap ? [] : [{ 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 makeMockBorderPieces(actPrefix: string): WildernessPiece[] {
|
||||||
|
const borderDs1 = makeMockDs1(8, 8, false)
|
||||||
|
const openBorderDs1 = makeMockDs1(8, 8, true)
|
||||||
|
return [
|
||||||
|
{ name: `${actPrefix} - Border 1`, border: true, levels: [borderDs1, borderDs1, borderDs1, openBorderDs1] },
|
||||||
|
{ name: `${actPrefix} - Border 2`, border: true, levels: [borderDs1] },
|
||||||
|
{ name: `${actPrefix} - Border 3`, border: true, levels: [borderDs1] },
|
||||||
|
{ name: `${actPrefix} - Border 4`, border: true, levels: [borderDs1, borderDs1, borderDs1, openBorderDs1] },
|
||||||
|
{ name: `${actPrefix} - Border 5`, border: true, levels: [borderDs1] },
|
||||||
|
{ name: `${actPrefix} - Border 6`, border: true, levels: [borderDs1] },
|
||||||
|
{ name: `${actPrefix} - Border 7`, border: true, levels: [borderDs1] },
|
||||||
|
{ name: `${actPrefix} - Border 8`, border: true, levels: [borderDs1] },
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('Wilderness Outdoor Initializers for Acts 2-5 (Issue #51)', () => {
|
||||||
|
test('UNIMPLEMENTED_PASSES has 5 outdoor passes removed', () => {
|
||||||
|
expect(UNIMPLEMENTED_PASSES).not.toContain('DRLGOUTDESR_InitAct2OutdoorLevel')
|
||||||
|
expect(UNIMPLEMENTED_PASSES).not.toContain('DRLGOUTPLACE_InitAct3OutdoorLevel')
|
||||||
|
expect(UNIMPLEMENTED_PASSES).not.toContain('DRLGOUTDOORS_InitAct4OutdoorLevel')
|
||||||
|
expect(UNIMPLEMENTED_PASSES).not.toContain('DRLGOUTSIEGE_InitAct5OutdoorLevel')
|
||||||
|
expect(UNIMPLEMENTED_PASSES).not.toContain('DRLG_GenerateJungles')
|
||||||
|
expect(UNIMPLEMENTED_PASSES).toEqual([
|
||||||
|
'DRLGVER_CreateVertices',
|
||||||
|
'DRLGOUTPLACE_CreateLevelConnections',
|
||||||
|
'DRLGOUTDOORS_SpawnAct3Mephisto',
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('Act 2: DRLGOUTDESR_InitAct2OutdoorLevel', () => {
|
||||||
|
test('Rocky Waste (41) generates sand caravan trails and canyon ridge walls', () => {
|
||||||
|
const pieces = makeMockBorderPieces('Act 2')
|
||||||
|
const result = generateWilderness({
|
||||||
|
levelId: 41,
|
||||||
|
levelName: 'Rocky Waste',
|
||||||
|
levelTypeName: 'Act 2 - Desert',
|
||||||
|
sizeX: 80,
|
||||||
|
sizeY: 80,
|
||||||
|
subType: 0,
|
||||||
|
subTheme: 0,
|
||||||
|
seed: 41001,
|
||||||
|
pieces,
|
||||||
|
substitutions: [],
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(result.stats.act2Topography).toBeDefined()
|
||||||
|
const topo = result.stats.act2Topography as Act2TopographyStats
|
||||||
|
expect(topo.sandTrailCells).toBeGreaterThan(50)
|
||||||
|
expect(topo.canyonWalls).toBeGreaterThan(20)
|
||||||
|
|
||||||
|
let foundSandTrail = false
|
||||||
|
let foundCanyonWall = false
|
||||||
|
for (let y = 0; y < result.level.height; y += 1) {
|
||||||
|
for (let x = 0; x < result.level.width; x += 1) {
|
||||||
|
const cell = result.level.cells[y]![x]!
|
||||||
|
if (cell.floors.some(f => f.style === 5 && f.prop1 === 194)) foundSandTrail = true
|
||||||
|
if (cell.walls.some(w => w.style === 1 && w.prop1 === 129)) foundCanyonWall = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
expect(foundSandTrail).toBe(true)
|
||||||
|
expect(foundCanyonWall).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('Far Oasis (43) generates lush water oasis pools', () => {
|
||||||
|
const pieces = makeMockBorderPieces('Act 2')
|
||||||
|
const result = generateWilderness({
|
||||||
|
levelId: 43,
|
||||||
|
levelName: 'Far Oasis',
|
||||||
|
levelTypeName: 'Act 2 - Desert',
|
||||||
|
sizeX: 80,
|
||||||
|
sizeY: 80,
|
||||||
|
subType: 0,
|
||||||
|
subTheme: 0,
|
||||||
|
seed: 43001,
|
||||||
|
pieces,
|
||||||
|
substitutions: [],
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(result.stats.act2Topography).toBeDefined()
|
||||||
|
const topo = result.stats.act2Topography as Act2TopographyStats
|
||||||
|
expect(topo.oasisPoolCells).toBeGreaterThan(15)
|
||||||
|
|
||||||
|
let foundOasisWater = false
|
||||||
|
for (let y = 0; y < result.level.height; y += 1) {
|
||||||
|
for (let x = 0; x < result.level.width; x += 1) {
|
||||||
|
const cell = result.level.cells[y]![x]!
|
||||||
|
if (cell.floors.some(f => f.style === 2 && f.prop1 === 2)) foundOasisWater = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
expect(foundOasisWater).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('Valley of Snakes (45) creates narrow canyon gorge', () => {
|
||||||
|
const pieces = makeMockBorderPieces('Act 2')
|
||||||
|
const result = generateWilderness({
|
||||||
|
levelId: 45,
|
||||||
|
levelName: 'Valley of Snakes',
|
||||||
|
levelTypeName: 'Act 2 - Desert',
|
||||||
|
sizeX: 80,
|
||||||
|
sizeY: 80,
|
||||||
|
subType: 0,
|
||||||
|
subTheme: 0,
|
||||||
|
seed: 45001,
|
||||||
|
pieces,
|
||||||
|
substitutions: [],
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(result.stats.act2Topography).toBeDefined()
|
||||||
|
const topo = result.stats.act2Topography as Act2TopographyStats
|
||||||
|
expect(topo.canyonWalls).toBeGreaterThan(30)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('Canyon of the Magi (46) places 7 Tal Rasha tombs, central dais, and has 0 perimeter openings', () => {
|
||||||
|
const pieces = makeMockBorderPieces('Act 2')
|
||||||
|
expect(getPerimeterOpenings(46, 10, 10).size).toBe(0)
|
||||||
|
|
||||||
|
const result = generateWilderness({
|
||||||
|
levelId: 46,
|
||||||
|
levelName: 'Canyon of the Magi',
|
||||||
|
levelTypeName: 'Act 2 - Desert',
|
||||||
|
sizeX: 80,
|
||||||
|
sizeY: 80,
|
||||||
|
subType: 0,
|
||||||
|
subTheme: 0,
|
||||||
|
seed: 46001,
|
||||||
|
pieces,
|
||||||
|
substitutions: [],
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(result.stats.act2Topography).toBeDefined()
|
||||||
|
const topo = result.stats.act2Topography as Act2TopographyStats
|
||||||
|
expect(topo.tombEntrances).toBe(7)
|
||||||
|
|
||||||
|
// Verify central Arcane Sanctuary warp platform (portal/dais)
|
||||||
|
let foundCentralDais = false
|
||||||
|
for (let y = 35; y <= 45; y += 1) {
|
||||||
|
for (let x = 35; x <= 45; x += 1) {
|
||||||
|
const cell = result.level.cells[y]![x]!
|
||||||
|
if (cell.floors.some(f => f.style === 3)) foundCentralDais = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
expect(foundCentralDais).toBe(true)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('Act 3: DRLGOUTPLACE_InitAct3OutdoorLevel & DRLG_GenerateJungles', () => {
|
||||||
|
test('Jungles (76 Spider Forest) generate river channel, wooden river bridges, and jungle paths', () => {
|
||||||
|
const tailDs1 = makeMockDs1(64, 32)
|
||||||
|
const headDs1 = makeMockDs1(64, 32)
|
||||||
|
const clearingDs1 = makeMockDs1(32, 32)
|
||||||
|
const riverDs1 = makeMockDs1(32, 32)
|
||||||
|
|
||||||
|
const pieces: WildernessPiece[] = [
|
||||||
|
{ name: 'Act 3 - Jungle Tail', border: false, levels: [tailDs1] },
|
||||||
|
{ name: 'Act 3 - Jungle Head', border: false, levels: [headDs1] },
|
||||||
|
{ name: 'Act 3 - Clearing Webby E', border: false, levels: [clearingDs1] },
|
||||||
|
{ name: 'Act 3 - Jungle NS W', border: false, levels: [riverDs1] },
|
||||||
|
]
|
||||||
|
|
||||||
|
const result = generateWilderness({
|
||||||
|
levelId: 76,
|
||||||
|
levelName: 'Spider Forest',
|
||||||
|
levelTypeName: 'Act 3 - Jungle',
|
||||||
|
sizeX: 64,
|
||||||
|
sizeY: 192,
|
||||||
|
subType: 0,
|
||||||
|
subTheme: 0,
|
||||||
|
seed: 76001,
|
||||||
|
pieces,
|
||||||
|
substitutions: [],
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(result.stats.act3Topography).toBeDefined()
|
||||||
|
const topo = result.stats.act3Topography as Act3TopographyStats
|
||||||
|
expect(topo.riverBridges).toBeGreaterThanOrEqual(2)
|
||||||
|
expect(topo.junglePathCells).toBeGreaterThan(20)
|
||||||
|
|
||||||
|
// Verify wooden river bridge planks (style 4, prop1 2) spanning across East channel
|
||||||
|
let foundBridgePlank = false
|
||||||
|
for (let y = 70; y <= 90; y += 1) {
|
||||||
|
for (let x = 32; x < 60; x += 1) {
|
||||||
|
const cell = result.level.cells[y]![x]!
|
||||||
|
if (cell.floors.some(f => f.style === 4 && f.prop1 === 2)) foundBridgePlank = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
expect(foundBridgePlank).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('Kurast Causeway (82) stamps causeway with central walkway, balustrades, and canal water', () => {
|
||||||
|
const bridgeDs1 = makeMockDs1(48, 16)
|
||||||
|
const pieces: WildernessPiece[] = [
|
||||||
|
{ name: 'Act 3 - Bridge', border: false, levels: [bridgeDs1] },
|
||||||
|
]
|
||||||
|
|
||||||
|
const result = generateWilderness({
|
||||||
|
levelId: 82,
|
||||||
|
levelName: 'Kurast Causeway',
|
||||||
|
levelTypeName: 'Act 3 - Kurast',
|
||||||
|
sizeX: 48,
|
||||||
|
sizeY: 16,
|
||||||
|
subType: 0,
|
||||||
|
subTheme: 0,
|
||||||
|
seed: 82001,
|
||||||
|
pieces,
|
||||||
|
substitutions: [],
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(result.stats.act3Topography).toBeDefined()
|
||||||
|
const topo = result.stats.act3Topography as Act3TopographyStats
|
||||||
|
expect(topo.causewayCells).toBeGreaterThan(50)
|
||||||
|
expect(topo.canalCells).toBeGreaterThan(100)
|
||||||
|
|
||||||
|
// Verify central walkway (style 5), stone balustrade walls (style 1), and canal water (style 2)
|
||||||
|
let foundWalkway = false
|
||||||
|
let foundBalustrade = false
|
||||||
|
let foundCanalWater = false
|
||||||
|
for (let y = 0; y < result.level.height; y += 1) {
|
||||||
|
for (let x = 0; x < result.level.width; x += 1) {
|
||||||
|
const cell = result.level.cells[y]![x]!
|
||||||
|
if (cell.floors.some(f => f.style === 5 && f.prop1 === 194)) foundWalkway = true
|
||||||
|
if (cell.walls.some(w => w.style === 1 && w.prop1 === 129)) foundBalustrade = true
|
||||||
|
if (cell.floors.some(f => f.style === 2 && f.prop1 === 2)) foundCanalWater = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
expect(foundWalkway).toBe(true)
|
||||||
|
expect(foundBalustrade).toBe(true)
|
||||||
|
expect(foundCanalWater).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('Kurast Bazaar (79) generates paved stone avenues, canals, and stone bridges', () => {
|
||||||
|
const pieces = makeMockBorderPieces('Act 3')
|
||||||
|
const result = generateWilderness({
|
||||||
|
levelId: 79,
|
||||||
|
levelName: 'Kurast Bazaar',
|
||||||
|
levelTypeName: 'Act 3 - Kurast',
|
||||||
|
sizeX: 80,
|
||||||
|
sizeY: 80,
|
||||||
|
subType: 0,
|
||||||
|
subTheme: 0,
|
||||||
|
seed: 79001,
|
||||||
|
pieces,
|
||||||
|
substitutions: [],
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(result.stats.act3Topography).toBeDefined()
|
||||||
|
const topo = result.stats.act3Topography as Act3TopographyStats
|
||||||
|
expect(topo.causewayCells).toBeGreaterThan(50)
|
||||||
|
expect(topo.canalCells).toBeGreaterThan(30)
|
||||||
|
expect(topo.riverBridges).toBeGreaterThanOrEqual(1)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('Act 4: DRLGOUTDOORS_InitAct4OutdoorLevel', () => {
|
||||||
|
test('Outer Steppes (104) generates lava chasms, basalt lips, and rocky bridge causeways', () => {
|
||||||
|
const pieces = makeMockBorderPieces('Act 4')
|
||||||
|
const result = generateWilderness({
|
||||||
|
levelId: 104,
|
||||||
|
levelName: 'Outer Steppes',
|
||||||
|
levelTypeName: 'Act 4 - Mesa',
|
||||||
|
sizeX: 80,
|
||||||
|
sizeY: 80,
|
||||||
|
subType: 0,
|
||||||
|
subTheme: 0,
|
||||||
|
seed: 104001,
|
||||||
|
pieces,
|
||||||
|
substitutions: [],
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(result.stats.act4Topography).toBeDefined()
|
||||||
|
const topo = result.stats.act4Topography as Act4TopographyStats
|
||||||
|
expect(topo.lavaChasmCells).toBeGreaterThan(100)
|
||||||
|
expect(topo.rockyBridges).toBeGreaterThan(5)
|
||||||
|
|
||||||
|
// Verify boiling molten lava (style 20, sequence 3, prop1 2) and rocky bridges (style 10, sequence 0, prop1 2)
|
||||||
|
let foundLava = false
|
||||||
|
let foundRockyBridge = false
|
||||||
|
for (let y = 0; y < result.level.height; y += 1) {
|
||||||
|
for (let x = 0; x < result.level.width; x += 1) {
|
||||||
|
const cell = result.level.cells[y]![x]!
|
||||||
|
if (cell.floors.some(f => f.style === 20 && f.sequence === 3 && f.prop1 === 2)) foundLava = true
|
||||||
|
if (cell.floors.some(f => f.style === 10 && f.sequence === 0 && f.prop1 === 2)) foundRockyBridge = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
expect(foundLava).toBe(true)
|
||||||
|
expect(foundRockyBridge).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('Plains of Despair (105) creates Izual prison plateau', () => {
|
||||||
|
const pieces = makeMockBorderPieces('Act 4')
|
||||||
|
const result = generateWilderness({
|
||||||
|
levelId: 105,
|
||||||
|
levelName: 'Plains of Despair',
|
||||||
|
levelTypeName: 'Act 4 - Mesa',
|
||||||
|
sizeX: 80,
|
||||||
|
sizeY: 80,
|
||||||
|
subType: 0,
|
||||||
|
subTheme: 0,
|
||||||
|
seed: 105001,
|
||||||
|
pieces,
|
||||||
|
substitutions: [],
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(result.stats.act4Topography).toBeDefined()
|
||||||
|
const topo = result.stats.act4Topography as Act4TopographyStats
|
||||||
|
expect(topo.plateauCells).toBeGreaterThan(20)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('Act 5: DRLGOUTSIEGE_InitAct5OutdoorLevel', () => {
|
||||||
|
test('Frigid Highlands (111) generates transverse siege barricades, choke-point gates, and trenches', () => {
|
||||||
|
const pieces = makeMockBorderPieces('Act 5')
|
||||||
|
const result = generateWilderness({
|
||||||
|
levelId: 111,
|
||||||
|
levelName: 'Frigid Highlands',
|
||||||
|
levelTypeName: 'Act 5 - Barricade',
|
||||||
|
sizeX: 80,
|
||||||
|
sizeY: 80,
|
||||||
|
subType: 0,
|
||||||
|
subTheme: 0,
|
||||||
|
seed: 111001,
|
||||||
|
pieces,
|
||||||
|
substitutions: [],
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(result.stats.act5Topography).toBeDefined()
|
||||||
|
const topo = result.stats.act5Topography as Act5TopographyStats
|
||||||
|
expect(topo.barricadeWalls).toBeGreaterThan(40)
|
||||||
|
expect(topo.trenchCells).toBeGreaterThan(30)
|
||||||
|
|
||||||
|
// Verify spiked palisade walls (style 2, prop1 129) and trench floors (style 5, prop1 194)
|
||||||
|
let foundBarricadeWall = false
|
||||||
|
let foundTrench = false
|
||||||
|
for (let y = 0; y < result.level.height; y += 1) {
|
||||||
|
for (let x = 0; x < result.level.width; x += 1) {
|
||||||
|
const cell = result.level.cells[y]![x]!
|
||||||
|
if (cell.walls.some(w => w.style === 2 && w.prop1 === 129)) foundBarricadeWall = true
|
||||||
|
if (cell.floors.some(f => f.style === 5 && f.prop1 === 194)) foundTrench = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
expect(foundBarricadeWall).toBe(true)
|
||||||
|
expect(foundTrench).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('Arreat Plateau (112) generates snow crater frozen lakes with ice floors', () => {
|
||||||
|
const pieces = makeMockBorderPieces('Act 5')
|
||||||
|
const result = generateWilderness({
|
||||||
|
levelId: 112,
|
||||||
|
levelName: 'Arreat Plateau',
|
||||||
|
levelTypeName: 'Act 5 - Barricade',
|
||||||
|
sizeX: 80,
|
||||||
|
sizeY: 80,
|
||||||
|
subType: 0,
|
||||||
|
subTheme: 0,
|
||||||
|
seed: 112001,
|
||||||
|
pieces,
|
||||||
|
substitutions: [],
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(result.stats.act5Topography).toBeDefined()
|
||||||
|
const topo = result.stats.act5Topography as Act5TopographyStats
|
||||||
|
expect(topo.frozenLakeCells).toBeGreaterThan(20)
|
||||||
|
|
||||||
|
let foundIceFloor = false
|
||||||
|
for (let y = 0; y < result.level.height; y += 1) {
|
||||||
|
for (let x = 0; x < result.level.width; x += 1) {
|
||||||
|
const cell = result.level.cells[y]![x]!
|
||||||
|
if (cell.floors.some(f => f.style === 2 && f.sequence === 0 && f.prop1 === 2)) foundIceFloor = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
expect(foundIceFloor).toBe(true)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
Loading…
Reference in New Issue