Merge branch 'fix/issue-55'
This commit is contained in:
commit
dfce2f6cef
|
|
@ -1399,9 +1399,18 @@ export function getPerimeterOpenings(
|
|||
return openings
|
||||
}
|
||||
|
||||
// Act 1 wilderness levels (2..7) & Act 4 Mesa wilderness levels (104..106):
|
||||
// entrance on South edge, exit on East edge
|
||||
if ((levelId >= 2 && levelId <= 7) || (levelId >= 104 && levelId <= 106)) {
|
||||
// Outdoor wilderness levels across Acts 1, 2, 3, 4, 5:
|
||||
// Act 1 (2..7), Act 2 desert (41..46), Act 3 Kurast (79..82),
|
||||
// Act 4 Mesa (104..106), Act 5 outdoor (111..112, 117):
|
||||
// Default entrance on South edge, exit on East edge
|
||||
if (
|
||||
(levelId >= 2 && levelId <= 7) ||
|
||||
(levelId >= 41 && levelId <= 46) ||
|
||||
(levelId >= 79 && levelId <= 82) ||
|
||||
(levelId >= 104 && levelId <= 106) ||
|
||||
(levelId >= 111 && levelId <= 112) ||
|
||||
levelId === 117
|
||||
) {
|
||||
const inX = Math.floor(gridWidth / 2)
|
||||
openings.add(key(inX, gridHeight - 1))
|
||||
const outY = Math.floor(gridHeight / 2)
|
||||
|
|
@ -2974,12 +2983,25 @@ function stampAct3Jungle(
|
|||
return DRLG_GenerateJungles(canvas, levelId, pieces, rng, stats, stampDs1)
|
||||
}
|
||||
|
||||
/** Theme configuration for outdoor road rasterization. */
|
||||
export interface OutdoorRoadTheme {
|
||||
readonly roadFloor: (rng: Rng) => Ds1Floor
|
||||
readonly isProtectedPreset?: (presetName: string) => boolean
|
||||
readonly isIgnoredSpurPreset?: (presetName: string) => boolean
|
||||
}
|
||||
|
||||
/** Theme configuration for outdoor stone wall divider networks. */
|
||||
export interface OutdoorDividerTheme {
|
||||
readonly wallStyle: number
|
||||
readonly wallProp1?: number
|
||||
readonly roadStyles?: readonly number[]
|
||||
}
|
||||
|
||||
/**
|
||||
* Lay a 3-tile-wide main dirt highway connecting the South Border Gate (`Bord1o.ds1` at `x = inX*8 + 3..5`),
|
||||
* the Central Crossroads (`hubX, hubY`), the River Bridge (if present), and the East Border Gate
|
||||
* (`Bord4o.ds1` at `y = outY*8 + 3..5`), plus 2-tile-wide spur paths to every spawned landmark.
|
||||
* General outdoor path and highway rasterizer connecting entrance, central crossroads,
|
||||
* exit gates, and landmark presets across wilderness maps.
|
||||
*/
|
||||
function stampAct1DirtRoads(
|
||||
export function stampThemedRoads(
|
||||
canvas: Canvas,
|
||||
gridWidth: number,
|
||||
gridHeight: number,
|
||||
|
|
@ -2987,19 +3009,14 @@ function stampAct1DirtRoads(
|
|||
outY: number,
|
||||
placedRects: readonly PlacedPresetRect[],
|
||||
rng: Rng,
|
||||
theme: OutdoorRoadTheme,
|
||||
openGates?: ReadonlyMap<string, number>,
|
||||
): void {
|
||||
const makeRoadFloor = (): Ds1Floor => ({
|
||||
prop1: 194,
|
||||
sequence: rng.int(0, 6) === 0 ? 25 : 0,
|
||||
style: 5,
|
||||
unknown1: 0,
|
||||
unknown2: 0,
|
||||
hidden: false,
|
||||
})
|
||||
|
||||
const isInsideProtectedPreset = (x: number, y: number): boolean => {
|
||||
for (const r of placedRects) {
|
||||
if (r.name === 'Act 1 - River') continue
|
||||
if (theme.isProtectedPreset ? !theme.isProtectedPreset(r.name) : /River|Bridge/i.test(r.name)) {
|
||||
continue
|
||||
}
|
||||
if (x >= r.x && x < r.x + r.w && y >= r.y && y < r.y + r.h) {
|
||||
return true
|
||||
}
|
||||
|
|
@ -3025,15 +3042,25 @@ function stampAct1DirtRoads(
|
|||
return
|
||||
}
|
||||
}
|
||||
;(cell.floors as Ds1Floor[])[0] = makeRoadFloor()
|
||||
;(cell.floors as Ds1Floor[])[0] = theme.roadFloor(rng)
|
||||
}
|
||||
|
||||
const hubX = inX * TILES_PER_BLOCK + 4
|
||||
const hubY = outY * TILES_PER_BLOCK + 4
|
||||
const southGateY = (gridHeight - 1) * TILES_PER_BLOCK + 3
|
||||
const eastGateX = (gridWidth - 1) * TILES_PER_BLOCK + 3
|
||||
const northEnd = Math.max(12, Math.floor(canvas.height * 0.2))
|
||||
const westEnd = Math.max(12, Math.floor(canvas.width * 0.2))
|
||||
let northEnd = Math.max(12, Math.floor(canvas.height * 0.2))
|
||||
let westEnd = Math.max(12, Math.floor(canvas.width * 0.2))
|
||||
|
||||
if (openGates !== undefined) {
|
||||
for (const key of openGates.keys()) {
|
||||
const [bxStr, byStr] = key.split(',')
|
||||
const bx = Number(bxStr)
|
||||
const by = Number(byStr)
|
||||
if (by === 0) northEnd = 4
|
||||
if (bx === 0) westEnd = 4
|
||||
}
|
||||
}
|
||||
|
||||
// 1. 3-tile-wide South-to-North main highway (`x = hubX - 1, hubX, hubX + 1`)
|
||||
for (let y = northEnd; y <= southGateY; y += 1) {
|
||||
|
|
@ -3049,6 +3076,34 @@ function stampAct1DirtRoads(
|
|||
}
|
||||
}
|
||||
|
||||
// If openGates contains non-standard gate blocks, connect them into the highway network
|
||||
if (openGates !== undefined) {
|
||||
for (const key of openGates.keys()) {
|
||||
const [bxStr, byStr] = key.split(',')
|
||||
const bx = Number(bxStr)
|
||||
const by = Number(byStr)
|
||||
const gateX = bx * TILES_PER_BLOCK + 4
|
||||
const gateY = by * TILES_PER_BLOCK + 4
|
||||
if (by === 0 && Math.abs(gateX - hubX) > 2) {
|
||||
// North gate branch to hubY
|
||||
for (let y = 4; y <= hubY; y += 1) {
|
||||
for (let dx = -1; dx <= 1; dx += 1) paintRoadTile(gateX + dx, y, true)
|
||||
}
|
||||
for (let x = Math.min(gateX, hubX); x <= Math.max(gateX, hubX); x += 1) {
|
||||
for (let dy = -1; dy <= 1; dy += 1) paintRoadTile(x, hubY + dy, true)
|
||||
}
|
||||
} else if (bx === 0 && Math.abs(gateY - hubY) > 2) {
|
||||
// West gate branch to hubX
|
||||
for (let x = 4; x <= hubX; x += 1) {
|
||||
for (let dy = -1; dy <= 1; dy += 1) paintRoadTile(x, gateY + dy, true)
|
||||
}
|
||||
for (let y = Math.min(gateY, hubY); y <= Math.max(gateY, hubY); y += 1) {
|
||||
for (let dx = -1; dx <= 1; dx += 1) paintRoadTile(hubX + dx, y, true)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 2-tile-wide spur roads connecting each major spawned landmark to the main highway
|
||||
const paintSpurSegment = (x0: number, y0: number, x1: number, y1: number): void => {
|
||||
let curX = x0
|
||||
|
|
@ -3068,10 +3123,7 @@ function stampAct1DirtRoads(
|
|||
}
|
||||
|
||||
for (const r of placedRects) {
|
||||
if (
|
||||
r.name === 'Act 1 - River' ||
|
||||
/Stone Fill|Swamp Fill|Tree Fill/i.test(r.name)
|
||||
) {
|
||||
if (theme.isIgnoredSpurPreset ? theme.isIgnoredSpurPreset(r.name) : /River|Bridge|Fill|Berms/i.test(r.name)) {
|
||||
continue
|
||||
}
|
||||
const centerX = Math.floor(r.x + r.w / 2)
|
||||
|
|
@ -3092,15 +3144,10 @@ function stampAct1DirtRoads(
|
|||
}
|
||||
|
||||
/**
|
||||
* Lay connected stone wall divider lines (`style=2, prop1=129`) across the interior of Act 1
|
||||
* wilderness levels (LevelId 2..7), using exact Blizzard tile sequences:
|
||||
* - Continuous East-West wall: `style=2, sequence=0, type=2` (`s2.q0.t2`)
|
||||
* - Continuous North-South wall: `style=2, sequence=0, type=1` (`s2.q0.t1`)
|
||||
* - Corner intersection: `style=2, sequence=0, type=3` (`s2.q0.t3`)
|
||||
* - East-West gate end-caps: `sequence=1, type=2` (West/Right-of-gap) & `sequence=2, type=2` (East/Left-of-gap)
|
||||
* - North-South gate end-caps: `sequence=1, type=1` (North/Bottom-of-gap) & `sequence=2, type=1` (South/Top-of-gap)
|
||||
* General outdoor divider network laying connected stone walls, sandstone berms,
|
||||
* or balustrades with proper corner joins, gate archways, and endcaps.
|
||||
*/
|
||||
function stampAct1InteriorStoneWalls(
|
||||
export function stampThemedDividers(
|
||||
canvas: Canvas,
|
||||
gridWidth: number,
|
||||
gridHeight: number,
|
||||
|
|
@ -3108,13 +3155,17 @@ function stampAct1InteriorStoneWalls(
|
|||
outY: number,
|
||||
placedRects: readonly PlacedPresetRect[],
|
||||
rng: Rng,
|
||||
theme: OutdoorDividerTheme,
|
||||
): void {
|
||||
if (gridWidth < 8 || gridHeight < 8) return
|
||||
if (gridWidth < 6 || gridHeight < 6) return
|
||||
|
||||
const wallStyle = theme.wallStyle
|
||||
const wallProp1 = theme.wallProp1 ?? 129
|
||||
|
||||
const makeWall = (type: number, sequence: number): Ds1Wall => ({
|
||||
prop1: 129,
|
||||
prop1: wallProp1,
|
||||
sequence,
|
||||
style: 2,
|
||||
style: wallStyle,
|
||||
type,
|
||||
unknown1: 0,
|
||||
unknown2: 0,
|
||||
|
|
@ -3132,26 +3183,26 @@ function stampAct1InteriorStoneWalls(
|
|||
}
|
||||
}
|
||||
|
||||
// Offset stone wall dividers by 2 blocks (16 tiles) from the main road spine
|
||||
// Offset divider lines by 2 blocks (16 tiles) from the main road spine
|
||||
const offsetBlockY = rng.int(0, 1) === 0 ? -2 : 2
|
||||
const offsetBlockX = rng.int(0, 1) === 0 ? -2 : 2
|
||||
const wallY = Math.max(16, Math.min(canvas.height - 16, (outY + offsetBlockY) * TILES_PER_BLOCK))
|
||||
const wallX = Math.max(16, Math.min(canvas.width - 16, (inX + offsetBlockX) * TILES_PER_BLOCK))
|
||||
const wallY = Math.max(12, Math.min(canvas.height - 12, (outY + offsetBlockY) * TILES_PER_BLOCK))
|
||||
const wallX = Math.max(12, Math.min(canvas.width - 12, (inX + offsetBlockX) * TILES_PER_BLOCK))
|
||||
|
||||
const isCellBlockedForWall = (x: number, y: number): boolean => {
|
||||
if (x < 10 || x >= canvas.width - 10 || y < 10 || y >= canvas.height - 10) return true
|
||||
// Leave 2-tile margin around all placed special presets (houses, caves, rivers, corrals)
|
||||
if (x < 6 || x >= canvas.width - 6 || y < 6 || y >= canvas.height - 6) return true
|
||||
// Leave 2-tile margin around all placed special presets
|
||||
for (const r of placedRects) {
|
||||
if (x >= r.x - 2 && x < r.x + r.w + 2 && y >= r.y - 2 && y < r.y + r.h + 2) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
// Leave 3-tile wide archway gate openings wherever a dirt road (style 5), bridge (style 4), or water (style 2/3) passes
|
||||
// Leave 3-tile wide archway gate openings wherever a road, bridge, or water passes
|
||||
for (let dy = -1; dy <= 1; dy += 1) {
|
||||
for (let dx = -1; dx <= 1; dx += 1) {
|
||||
const c = canvas.cells[y + dy]?.[x + dx]
|
||||
if (!c) continue
|
||||
if (c.floors.some(f => !f.hidden && (f.style === 5 || f.style === 4 || f.style === 2 || f.style === 3))) {
|
||||
if (c.floors.some(f => !f.hidden && (f.style === 5 || f.style === 1 || f.style === 4 || f.style === 2 || f.style === 3 || theme.roadStyles?.includes(f.style)))) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
|
@ -3196,37 +3247,358 @@ function stampAct1InteriorStoneWalls(
|
|||
const nsCoversCorner = nsRuns.some(r => wallY >= r.start && wallY <= r.end)
|
||||
const hasCornerJoin = ewCoversCorner && nsCoversCorner
|
||||
|
||||
// Stamp East-West stone wall segments (`type = 2`)
|
||||
// Stamp East-West divider wall segments (`type = 2`)
|
||||
for (const run of ewRuns) {
|
||||
for (let x = run.start; x <= run.end; x += 1) {
|
||||
if (x === wallX && hasCornerJoin) {
|
||||
placeWall(x, wallY, makeWall(3, 0)) // Corner / Cross join (`s2.q0.t3`)
|
||||
placeWall(x, wallY, makeWall(3, 0)) // Corner / Cross join (`type=3`)
|
||||
} else if (x === run.start) {
|
||||
placeWall(x, wallY, makeWall(2, 1)) // West / Right-of-gap End-Cap (`s2.q1.t2`)
|
||||
placeWall(x, wallY, makeWall(2, 1)) // West / Right-of-gap End-Cap (`type=2, seq=1`)
|
||||
} else if (x === run.end) {
|
||||
placeWall(x, wallY, makeWall(2, 2)) // East / Left-of-gap End-Cap (`s2.q2.t2`)
|
||||
placeWall(x, wallY, makeWall(2, 2)) // East / Left-of-gap End-Cap (`type=2, seq=2`)
|
||||
} else {
|
||||
placeWall(x, wallY, makeWall(2, 0)) // Continuous EW Stone Wall (`s2.q0.t2`)
|
||||
placeWall(x, wallY, makeWall(2, 0)) // Continuous EW Wall (`type=2, seq=0`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Stamp North-South stone wall segments (`type = 1`)
|
||||
// Stamp North-South divider wall segments (`type = 1`)
|
||||
for (const run of nsRuns) {
|
||||
for (let y = run.start; y <= run.end; y += 1) {
|
||||
if (y === wallY && hasCornerJoin) {
|
||||
placeWall(wallX, y, makeWall(3, 0)) // Corner / Cross join (`s2.q0.t3`)
|
||||
placeWall(wallX, y, makeWall(3, 0)) // Corner / Cross join (`type=3`)
|
||||
} else if (y === run.start) {
|
||||
placeWall(wallX, y, makeWall(1, 1)) // North / Bottom-of-gap End-Cap (`s2.q1.t1`)
|
||||
placeWall(wallX, y, makeWall(1, 1)) // North / Bottom-of-gap End-Cap (`type=1, seq=1`)
|
||||
} else if (y === run.end) {
|
||||
placeWall(wallX, y, makeWall(1, 2)) // South / Top-of-gap End-Cap (`s2.q2.t1`)
|
||||
placeWall(wallX, y, makeWall(1, 2)) // South / Top-of-gap End-Cap (`type=1, seq=2`)
|
||||
} else {
|
||||
placeWall(wallX, y, makeWall(1, 0)) // Continuous NS Stone Wall (`s2.q0.t1`)
|
||||
placeWall(wallX, y, makeWall(1, 0)) // Continuous NS Wall (`type=1, seq=0`)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Act 1: Lay a 3-tile-wide dirt highway connecting the South Border Gate, central crossroads,
|
||||
* river bridge, and East Border Gate, plus 2-tile-wide spur paths to landmarks.
|
||||
*/
|
||||
export function stampAct1DirtRoads(
|
||||
canvas: Canvas,
|
||||
gridWidth: number,
|
||||
gridHeight: number,
|
||||
inX: number,
|
||||
outY: number,
|
||||
placedRects: readonly PlacedPresetRect[],
|
||||
rng: Rng,
|
||||
openGates?: ReadonlyMap<string, number>,
|
||||
): void {
|
||||
stampThemedRoads(
|
||||
canvas,
|
||||
gridWidth,
|
||||
gridHeight,
|
||||
inX,
|
||||
outY,
|
||||
placedRects,
|
||||
rng,
|
||||
{
|
||||
roadFloor: (r) => ({
|
||||
prop1: 194,
|
||||
sequence: r.int(0, 6) === 0 ? 25 : 0,
|
||||
style: 5,
|
||||
unknown1: 0,
|
||||
unknown2: 0,
|
||||
hidden: false,
|
||||
}),
|
||||
isProtectedPreset: (name) => name !== 'Act 1 - River',
|
||||
isIgnoredSpurPreset: (name) => name === 'Act 1 - River' || /Stone Fill|Swamp Fill|Tree Fill|Fence Fill|Corral Fill/i.test(name),
|
||||
},
|
||||
openGates,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Act 1: Lay connected stone wall divider lines (`style=2, prop1=129`) across the interior
|
||||
* meadow of Act 1 wilderness levels (LevelId 2..7).
|
||||
*/
|
||||
export function stampAct1InteriorStoneWalls(
|
||||
canvas: Canvas,
|
||||
gridWidth: number,
|
||||
gridHeight: number,
|
||||
inX: number,
|
||||
outY: number,
|
||||
placedRects: readonly PlacedPresetRect[],
|
||||
rng: Rng,
|
||||
): void {
|
||||
stampThemedDividers(canvas, gridWidth, gridHeight, inX, outY, placedRects, rng, {
|
||||
wallStyle: 2,
|
||||
wallProp1: 129,
|
||||
roadStyles: [5],
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Act 2: Desert sandstone caravan routes connecting entrance to exit gates across dunes.
|
||||
*/
|
||||
export function stampAct2DesertRoads(
|
||||
canvas: Canvas,
|
||||
gridWidth: number,
|
||||
gridHeight: number,
|
||||
inX: number,
|
||||
outY: number,
|
||||
placedRects: readonly PlacedPresetRect[],
|
||||
rng: Rng,
|
||||
openGates?: ReadonlyMap<string, number>,
|
||||
): void {
|
||||
stampThemedRoads(
|
||||
canvas,
|
||||
gridWidth,
|
||||
gridHeight,
|
||||
inX,
|
||||
outY,
|
||||
placedRects,
|
||||
rng,
|
||||
{
|
||||
roadFloor: (r) => ({
|
||||
prop1: 194,
|
||||
sequence: r.int(0, 5) === 0 ? 1 : 0,
|
||||
style: 1,
|
||||
unknown1: 0,
|
||||
unknown2: 0,
|
||||
hidden: false,
|
||||
}),
|
||||
isProtectedPreset: (name) => !/River|Bridge/i.test(name),
|
||||
isIgnoredSpurPreset: (name) => /Fill|Berms/i.test(name),
|
||||
},
|
||||
openGates,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Act 2: Sandstone divider berms (style=1, prop1=129) separating desert dunes.
|
||||
*/
|
||||
export function stampAct2SandstoneBerms(
|
||||
canvas: Canvas,
|
||||
gridWidth: number,
|
||||
gridHeight: number,
|
||||
inX: number,
|
||||
outY: number,
|
||||
placedRects: readonly PlacedPresetRect[],
|
||||
rng: Rng,
|
||||
): void {
|
||||
stampThemedDividers(canvas, gridWidth, gridHeight, inX, outY, placedRects, rng, {
|
||||
wallStyle: 1,
|
||||
wallProp1: 129,
|
||||
roadStyles: [1],
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Act 3: Kurast flagstone avenues across temples, plazas, and causeways.
|
||||
*/
|
||||
export function stampAct3KurastAvenues(
|
||||
canvas: Canvas,
|
||||
gridWidth: number,
|
||||
gridHeight: number,
|
||||
inX: number,
|
||||
outY: number,
|
||||
placedRects: readonly PlacedPresetRect[],
|
||||
rng: Rng,
|
||||
openGates?: ReadonlyMap<string, number>,
|
||||
): void {
|
||||
stampThemedRoads(
|
||||
canvas,
|
||||
gridWidth,
|
||||
gridHeight,
|
||||
inX,
|
||||
outY,
|
||||
placedRects,
|
||||
rng,
|
||||
{
|
||||
roadFloor: (r) => ({
|
||||
prop1: 194,
|
||||
sequence: r.int(0, 5) === 0 ? 1 : 0,
|
||||
style: 1,
|
||||
unknown1: 0,
|
||||
unknown2: 0,
|
||||
hidden: false,
|
||||
}),
|
||||
isProtectedPreset: (name) => !/River|Bridge/i.test(name),
|
||||
isIgnoredSpurPreset: (name) => /Fill/i.test(name),
|
||||
},
|
||||
openGates,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Act 3: Kurast balustrades across temples and causeways (style=1, prop1=129).
|
||||
*/
|
||||
export function stampAct3KurastBalustrades(
|
||||
canvas: Canvas,
|
||||
gridWidth: number,
|
||||
gridHeight: number,
|
||||
inX: number,
|
||||
outY: number,
|
||||
placedRects: readonly PlacedPresetRect[],
|
||||
rng: Rng,
|
||||
): void {
|
||||
stampThemedDividers(canvas, gridWidth, gridHeight, inX, outY, placedRects, rng, {
|
||||
wallStyle: 1,
|
||||
wallProp1: 129,
|
||||
roadStyles: [1],
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Act 5: Snow march roads in Frigid Highlands & Arreat Plateau.
|
||||
*/
|
||||
export function stampAct5SnowRoads(
|
||||
canvas: Canvas,
|
||||
gridWidth: number,
|
||||
gridHeight: number,
|
||||
inX: number,
|
||||
outY: number,
|
||||
placedRects: readonly PlacedPresetRect[],
|
||||
rng: Rng,
|
||||
openGates?: ReadonlyMap<string, number>,
|
||||
): void {
|
||||
stampThemedRoads(
|
||||
canvas,
|
||||
gridWidth,
|
||||
gridHeight,
|
||||
inX,
|
||||
outY,
|
||||
placedRects,
|
||||
rng,
|
||||
{
|
||||
roadFloor: (r) => ({
|
||||
prop1: 194,
|
||||
sequence: r.int(0, 5) === 0 ? 1 : 0,
|
||||
style: 1,
|
||||
unknown1: 0,
|
||||
unknown2: 0,
|
||||
hidden: false,
|
||||
}),
|
||||
isProtectedPreset: (name) => !/River|Bridge/i.test(name),
|
||||
isIgnoredSpurPreset: (name) => /Fill|Filler/i.test(name),
|
||||
},
|
||||
openGates,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Act 5: Frozen trail divides in Frigid Highlands & Arreat Plateau (style=2, prop1=129).
|
||||
*/
|
||||
export function stampAct5FrozenTrailDivides(
|
||||
canvas: Canvas,
|
||||
gridWidth: number,
|
||||
gridHeight: number,
|
||||
inX: number,
|
||||
outY: number,
|
||||
placedRects: readonly PlacedPresetRect[],
|
||||
rng: Rng,
|
||||
): void {
|
||||
stampThemedDividers(canvas, gridWidth, gridHeight, inX, outY, placedRects, rng, {
|
||||
wallStyle: 2,
|
||||
wallProp1: 129,
|
||||
roadStyles: [1],
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatch outdoor road networks based on act, levelId, and levelTypeName.
|
||||
*/
|
||||
export function stampOutdoorRoads(
|
||||
canvas: Canvas,
|
||||
act: number,
|
||||
levelId: number,
|
||||
gridWidth: number,
|
||||
gridHeight: number,
|
||||
inX: number,
|
||||
outY: number,
|
||||
placedRects: readonly PlacedPresetRect[],
|
||||
rng: Rng,
|
||||
levelTypeName?: string,
|
||||
openGates?: ReadonlyMap<string, number>,
|
||||
): void {
|
||||
if (levelId === 39) return // Cow Level has no roads
|
||||
|
||||
// Level ID takes highest precedence
|
||||
if (levelId >= 41 && levelId <= 46) {
|
||||
stampAct2DesertRoads(canvas, gridWidth, gridHeight, inX, outY, placedRects, rng, openGates)
|
||||
} else if (levelId >= 79 && levelId <= 82) {
|
||||
stampAct3KurastAvenues(canvas, gridWidth, gridHeight, inX, outY, placedRects, rng, openGates)
|
||||
} else if (levelId === 111 || levelId === 112 || levelId === 117) {
|
||||
stampAct5SnowRoads(canvas, gridWidth, gridHeight, inX, outY, placedRects, rng, openGates)
|
||||
} else if (levelId >= 2 && levelId <= 7) {
|
||||
stampAct1DirtRoads(canvas, gridWidth, gridHeight, inX, outY, placedRects, rng, openGates)
|
||||
// Level type name takes second precedence
|
||||
} else if (levelTypeName === 'Act 2 - Desert') {
|
||||
stampAct2DesertRoads(canvas, gridWidth, gridHeight, inX, outY, placedRects, rng, openGates)
|
||||
} else if (levelTypeName === 'Act 3 - Kurast') {
|
||||
stampAct3KurastAvenues(canvas, gridWidth, gridHeight, inX, outY, placedRects, rng, openGates)
|
||||
} else if (levelTypeName === 'Act 5 - Barricade' || levelTypeName === 'Act 5 - Siege') {
|
||||
stampAct5SnowRoads(canvas, gridWidth, gridHeight, inX, outY, placedRects, rng, openGates)
|
||||
} else if (levelTypeName === 'Act 1 - Wilderness') {
|
||||
stampAct1DirtRoads(canvas, gridWidth, gridHeight, inX, outY, placedRects, rng, openGates)
|
||||
// Act number takes fallback precedence
|
||||
} else if (act === 2) {
|
||||
stampAct2DesertRoads(canvas, gridWidth, gridHeight, inX, outY, placedRects, rng, openGates)
|
||||
} else if (act === 3) {
|
||||
stampAct3KurastAvenues(canvas, gridWidth, gridHeight, inX, outY, placedRects, rng, openGates)
|
||||
} else if (act === 5) {
|
||||
stampAct5SnowRoads(canvas, gridWidth, gridHeight, inX, outY, placedRects, rng, openGates)
|
||||
} else if (act === 1) {
|
||||
stampAct1DirtRoads(canvas, gridWidth, gridHeight, inX, outY, placedRects, rng, openGates)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatch outdoor boundary divider networks based on act, levelId, and levelTypeName.
|
||||
*/
|
||||
export function stampOutdoorDividers(
|
||||
canvas: Canvas,
|
||||
act: number,
|
||||
levelId: number,
|
||||
gridWidth: number,
|
||||
gridHeight: number,
|
||||
inX: number,
|
||||
outY: number,
|
||||
placedRects: readonly PlacedPresetRect[],
|
||||
rng: Rng,
|
||||
levelTypeName?: string,
|
||||
): void {
|
||||
if (levelId === 39) return // Cow Level has no dividers
|
||||
|
||||
// Level ID takes highest precedence
|
||||
if (levelId >= 41 && levelId <= 46) {
|
||||
stampAct2SandstoneBerms(canvas, gridWidth, gridHeight, inX, outY, placedRects, rng)
|
||||
} else if (levelId >= 79 && levelId <= 82) {
|
||||
stampAct3KurastBalustrades(canvas, gridWidth, gridHeight, inX, outY, placedRects, rng)
|
||||
} else if (levelId === 111 || levelId === 112 || levelId === 117) {
|
||||
stampAct5FrozenTrailDivides(canvas, gridWidth, gridHeight, inX, outY, placedRects, rng)
|
||||
} else if (levelId >= 2 && levelId <= 7) {
|
||||
stampAct1InteriorStoneWalls(canvas, gridWidth, gridHeight, inX, outY, placedRects, rng)
|
||||
// Level type name takes second precedence
|
||||
} else if (levelTypeName === 'Act 2 - Desert') {
|
||||
stampAct2SandstoneBerms(canvas, gridWidth, gridHeight, inX, outY, placedRects, rng)
|
||||
} else if (levelTypeName === 'Act 3 - Kurast') {
|
||||
stampAct3KurastBalustrades(canvas, gridWidth, gridHeight, inX, outY, placedRects, rng)
|
||||
} else if (levelTypeName === 'Act 5 - Barricade' || levelTypeName === 'Act 5 - Siege') {
|
||||
stampAct5FrozenTrailDivides(canvas, gridWidth, gridHeight, inX, outY, placedRects, rng)
|
||||
} else if (levelTypeName === 'Act 1 - Wilderness') {
|
||||
stampAct1InteriorStoneWalls(canvas, gridWidth, gridHeight, inX, outY, placedRects, rng)
|
||||
// Act number takes fallback precedence
|
||||
} else if (act === 2) {
|
||||
stampAct2SandstoneBerms(canvas, gridWidth, gridHeight, inX, outY, placedRects, rng)
|
||||
} else if (act === 3) {
|
||||
stampAct3KurastBalustrades(canvas, gridWidth, gridHeight, inX, outY, placedRects, rng)
|
||||
} else if (act === 5) {
|
||||
stampAct5FrozenTrailDivides(canvas, gridWidth, gridHeight, inX, outY, placedRects, rng)
|
||||
} else if (act === 1) {
|
||||
stampAct1InteriorStoneWalls(canvas, gridWidth, gridHeight, inX, outY, placedRects, rng)
|
||||
}
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------------- *
|
||||
* Outdoor Population Phase: Waypoints, Roadside Shrines/Chests, SuperUniques & Flavie
|
||||
* (`DRLGOUTDOORS_SpawnAct12Waypoint`, `DRLGOUTDOORS_SpawnAct12Shrines`)
|
||||
|
|
@ -3779,7 +4151,17 @@ export function generateWilderness(request: WildernessRequest): WildernessResult
|
|||
let floorLayers = 1
|
||||
let substitutionType = 0
|
||||
let version = 0
|
||||
let act = 1
|
||||
let inferredAct = 1
|
||||
if (request.levelId >= 40 && request.levelId < 75) inferredAct = 2
|
||||
else if (request.levelId >= 75 && request.levelId < 103) inferredAct = 3
|
||||
else if (request.levelId >= 103 && request.levelId < 109) inferredAct = 4
|
||||
else if (request.levelId >= 109) inferredAct = 5
|
||||
else if (/Act 2/i.test(request.levelTypeName)) inferredAct = 2
|
||||
else if (/Act 3/i.test(request.levelTypeName)) inferredAct = 3
|
||||
else if (/Act 4/i.test(request.levelTypeName)) inferredAct = 4
|
||||
else if (/Act 5/i.test(request.levelTypeName)) inferredAct = 5
|
||||
|
||||
let act = inferredAct
|
||||
const allLevels: Ds1[] = []
|
||||
for (const piece of request.pieces) for (const level of piece.levels) allLevels.push(level)
|
||||
for (const row of [...request.substitutions, ...(request.shrineSubstitutions ?? [])]) {
|
||||
|
|
@ -3790,7 +4172,10 @@ export function generateWilderness(request: WildernessRequest): WildernessResult
|
|||
floorLayers = Math.max(floorLayers, level.floorLayers)
|
||||
substitutionType = Math.max(substitutionType, level.substitutionType)
|
||||
version = Math.max(version, level.version)
|
||||
act = level.act
|
||||
if (level.act > 0) act = level.act
|
||||
}
|
||||
if (act === 1 && inferredAct > 1) {
|
||||
act = inferredAct
|
||||
}
|
||||
const substitutionLayers = substitutionType === 1 || substitutionType === 2 ? 1 : 0
|
||||
|
||||
|
|
@ -4013,7 +4398,24 @@ export function generateWilderness(request: WildernessRequest): WildernessResult
|
|||
const inX = Math.floor(gridWidth / 2)
|
||||
const outY = Math.floor(gridHeight / 2)
|
||||
const reservedRoadBlocks = new Set<string>()
|
||||
if (request.levelId >= 2 && request.levelId <= 7) {
|
||||
if (
|
||||
request.levelId !== 39 &&
|
||||
((request.levelId >= 2 && request.levelId <= 7) ||
|
||||
(request.levelId >= 41 && request.levelId <= 46) ||
|
||||
(request.levelId >= 79 && request.levelId <= 82) ||
|
||||
request.levelId === 111 ||
|
||||
request.levelId === 112 ||
|
||||
request.levelId === 117 ||
|
||||
request.levelTypeName === 'Act 1 - Wilderness' ||
|
||||
request.levelTypeName === 'Act 2 - Desert' ||
|
||||
request.levelTypeName === 'Act 3 - Kurast' ||
|
||||
request.levelTypeName === 'Act 5 - Barricade' ||
|
||||
request.levelTypeName === 'Act 5 - Siege' ||
|
||||
act === 1 ||
|
||||
act === 2 ||
|
||||
act === 3 ||
|
||||
act === 5)
|
||||
) {
|
||||
for (let by = 1; by < gridHeight - 1; by += 1) reservedRoadBlocks.add(`${String(inX)},${String(by)}`)
|
||||
for (let bx = 1; bx < gridWidth - 1; bx += 1) reservedRoadBlocks.add(`${String(bx)},${String(outY)}`)
|
||||
}
|
||||
|
|
@ -4056,12 +4458,33 @@ export function generateWilderness(request: WildernessRequest): WildernessResult
|
|||
dirtPathGrid,
|
||||
)
|
||||
|
||||
// For Act 1 outdoor wilderness maps (LevelId 2..7), lay dirt road networks and
|
||||
// interior stone wall divider lines across the meadow after random clutter substitutions.
|
||||
if (request.levelId >= 2 && request.levelId <= 7) {
|
||||
stampAct1DirtRoads(canvas, gridWidth, gridHeight, inX, outY, allPlacedRects, rng)
|
||||
stampAct1InteriorStoneWalls(canvas, gridWidth, gridHeight, inX, outY, allPlacedRects, rng)
|
||||
}
|
||||
// Stamp outdoor road networks and boundary divider lines across the wilderness
|
||||
// after random clutter substitutions.
|
||||
stampOutdoorRoads(
|
||||
canvas,
|
||||
act,
|
||||
request.levelId,
|
||||
gridWidth,
|
||||
gridHeight,
|
||||
inX,
|
||||
outY,
|
||||
allPlacedRects,
|
||||
rng,
|
||||
request.levelTypeName,
|
||||
act1Plan?.openGates,
|
||||
)
|
||||
stampOutdoorDividers(
|
||||
canvas,
|
||||
act,
|
||||
request.levelId,
|
||||
gridWidth,
|
||||
gridHeight,
|
||||
inX,
|
||||
outY,
|
||||
allPlacedRects,
|
||||
rng,
|
||||
request.levelTypeName,
|
||||
)
|
||||
|
||||
// Act 2 Desert Topography: Canyons, oasis pools, sand caravan trails, tomb entrances (DRLGOUTDESR_InitAct2OutdoorLevel)
|
||||
if (
|
||||
|
|
|
|||
|
|
@ -0,0 +1,416 @@
|
|||
import { describe, it, expect } from 'vitest'
|
||||
import {
|
||||
createCanvas,
|
||||
stampAct1DirtRoads,
|
||||
stampAct1InteriorStoneWalls,
|
||||
stampAct2DesertRoads,
|
||||
stampAct2SandstoneBerms,
|
||||
stampAct3KurastAvenues,
|
||||
stampAct3KurastBalustrades,
|
||||
stampAct5SnowRoads,
|
||||
stampAct5FrozenTrailDivides,
|
||||
stampOutdoorRoads,
|
||||
stampOutdoorDividers,
|
||||
generateWilderness,
|
||||
TILES_PER_BLOCK,
|
||||
} from '../src/game/wilderness.ts'
|
||||
import type { WildernessPiece, PlacedPresetRect, Canvas } from '../src/game/wilderness.ts'
|
||||
import { Rng } from '../src/game/rng.ts'
|
||||
import type { Ds1, Ds1Cell } from '../src/formats/ds1.ts'
|
||||
|
||||
function makeMockDs1(width: number, height: number): Ds1 {
|
||||
const cells: Ds1Cell[][] = []
|
||||
for (let y = 0; y < height; y += 1) {
|
||||
const row: Ds1Cell[] = []
|
||||
for (let x = 0; x < width; x += 1) {
|
||||
row.push({
|
||||
walls: [{ prop1: 2, sequence: 0, style: 1, type: 0, unknown1: 0, unknown2: 0, hidden: false }],
|
||||
floors: [{ prop1: 2, sequence: 0, style: 1, unknown1: 0, unknown2: 0, hidden: false }],
|
||||
shadows: [],
|
||||
substitutions: [],
|
||||
})
|
||||
}
|
||||
cells.push(row)
|
||||
}
|
||||
return {
|
||||
version: 18,
|
||||
width,
|
||||
height,
|
||||
act: 1,
|
||||
substitutionType: 0,
|
||||
wallLayers: 1,
|
||||
floorLayers: 1,
|
||||
cells,
|
||||
objects: [],
|
||||
npcPathOffset: null,
|
||||
}
|
||||
}
|
||||
|
||||
function countFloorTiles(canvas: Canvas, style: number, prop1: number): number {
|
||||
let count = 0
|
||||
for (let y = 0; y < canvas.height; y += 1) {
|
||||
const row = canvas.cells[y]
|
||||
if (!row) continue
|
||||
for (let x = 0; x < canvas.width; x += 1) {
|
||||
const cell = row[x]
|
||||
if (cell && cell.floors.some(f => f.style === style && f.prop1 === prop1)) {
|
||||
count += 1
|
||||
}
|
||||
}
|
||||
}
|
||||
return count
|
||||
}
|
||||
|
||||
function countWallTiles(canvas: Canvas, style: number, prop1: number): number {
|
||||
let count = 0
|
||||
for (let y = 0; y < canvas.height; y += 1) {
|
||||
const row = canvas.cells[y]
|
||||
if (!row) continue
|
||||
for (let x = 0; x < canvas.width; x += 1) {
|
||||
const cell = row[x]
|
||||
if (cell && cell.walls.some(w => w.style === style && w.prop1 === prop1)) {
|
||||
count += 1
|
||||
}
|
||||
}
|
||||
}
|
||||
return count
|
||||
}
|
||||
|
||||
describe('Multi-Act Outdoor Roads & Boundary Divider Networks', () => {
|
||||
const gridW = 10
|
||||
const gridH = 10
|
||||
const width = gridW * TILES_PER_BLOCK
|
||||
const height = gridH * TILES_PER_BLOCK
|
||||
const inX = Math.floor(gridW / 2)
|
||||
const outY = Math.floor(gridH / 2)
|
||||
|
||||
describe('Act 2 Desert Roads & Sandstone Berms', () => {
|
||||
it('stampAct2DesertRoads paints caravan routes with style: 1, prop1: 194 and connects spurs', () => {
|
||||
const canvas = createCanvas(width, height, 1, 1, 0)
|
||||
const rng = new Rng(42)
|
||||
const placedRects: PlacedPresetRect[] = [
|
||||
{ name: 'Act 2 - Desert Tomb 1', x: 24, y: 24, w: 16, h: 16 },
|
||||
]
|
||||
|
||||
stampAct2DesertRoads(canvas, gridW, gridH, inX, outY, placedRects, rng)
|
||||
|
||||
const roadFloors = countFloorTiles(canvas, 1, 194)
|
||||
expect(roadFloors).toBeGreaterThan(50)
|
||||
|
||||
// Verify the main highway corridor has caravan road floors
|
||||
const highwayTile = canvas.cells[outY * TILES_PER_BLOCK + 4]?.[inX * TILES_PER_BLOCK + 4]
|
||||
expect(highwayTile).toBeDefined()
|
||||
expect(highwayTile?.floors.some(f => f.style === 1 && f.prop1 === 194)).toBe(true)
|
||||
|
||||
// Verify spur path connected towards the Tomb preset
|
||||
let spurFound = false
|
||||
for (let y = 30; y <= 34; y += 1) {
|
||||
for (let x = 38; x <= 44; x += 1) {
|
||||
const c = canvas.cells[y]?.[x]
|
||||
if (c?.floors.some(f => f.style === 1 && f.prop1 === 194)) {
|
||||
spurFound = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
expect(spurFound).toBe(true)
|
||||
})
|
||||
|
||||
it('stampAct2SandstoneBerms paints sandstone berms with style: 1, prop1: 129, corner joins, and road archway breaks', () => {
|
||||
const canvas = createCanvas(width, height, 1, 1, 0)
|
||||
const rng = new Rng(42)
|
||||
const placedRects: PlacedPresetRect[] = []
|
||||
|
||||
// First stamp roads so the divider can create 3-tile archway breaks across them
|
||||
stampAct2DesertRoads(canvas, gridW, gridH, inX, outY, placedRects, rng)
|
||||
stampAct2SandstoneBerms(canvas, gridW, gridH, inX, outY, placedRects, rng)
|
||||
|
||||
const bermWalls = countWallTiles(canvas, 1, 129)
|
||||
expect(bermWalls).toBeGreaterThan(20)
|
||||
|
||||
// Berms should not block the road where the road crosses the divider
|
||||
// Highway crosses dividing lines at (inX * 8 + 4, outY * 8 + 4)
|
||||
const crossRoad = canvas.cells[outY * TILES_PER_BLOCK + 4]?.[inX * TILES_PER_BLOCK + 4]
|
||||
expect(crossRoad?.walls.some(w => w.style === 1 && w.prop1 === 129)).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Act 3 Kurast Avenues & Balustrades', () => {
|
||||
it('stampAct3KurastAvenues paints flagstone avenues with style: 1, prop1: 194', () => {
|
||||
const canvas = createCanvas(width, height, 1, 1, 0)
|
||||
const rng = new Rng(100)
|
||||
const placedRects: PlacedPresetRect[] = [
|
||||
{ name: 'Act 3 - Burbs Temple', x: 20, y: 20, w: 16, h: 16 },
|
||||
]
|
||||
|
||||
stampAct3KurastAvenues(canvas, gridW, gridH, inX, outY, placedRects, rng)
|
||||
|
||||
const avenueFloors = countFloorTiles(canvas, 1, 194)
|
||||
expect(avenueFloors).toBeGreaterThan(50)
|
||||
})
|
||||
|
||||
it('stampAct3KurastBalustrades paints balustrades with style: 1, prop1: 129', () => {
|
||||
const canvas = createCanvas(width, height, 1, 1, 0)
|
||||
const rng = new Rng(100)
|
||||
const placedRects: PlacedPresetRect[] = []
|
||||
|
||||
stampAct3KurastAvenues(canvas, gridW, gridH, inX, outY, placedRects, rng)
|
||||
stampAct3KurastBalustrades(canvas, gridW, gridH, inX, outY, placedRects, rng)
|
||||
|
||||
const balustradeWalls = countWallTiles(canvas, 1, 129)
|
||||
expect(balustradeWalls).toBeGreaterThan(20)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Act 5 Snow Roads & Frozen Trail Divides', () => {
|
||||
it('stampAct5SnowRoads paints snow march roads with style: 1, prop1: 194', () => {
|
||||
const canvas = createCanvas(width, height, 1, 1, 0)
|
||||
const rng = new Rng(555)
|
||||
const placedRects: PlacedPresetRect[] = [
|
||||
{ name: 'Act 5 - Barricade Building', x: 20, y: 20, w: 16, h: 16 },
|
||||
]
|
||||
|
||||
stampAct5SnowRoads(canvas, gridW, gridH, inX, outY, placedRects, rng)
|
||||
|
||||
const snowRoadFloors = countFloorTiles(canvas, 1, 194)
|
||||
expect(snowRoadFloors).toBeGreaterThan(50)
|
||||
})
|
||||
|
||||
it('stampAct5FrozenTrailDivides paints frozen trail divides with style: 2, prop1: 129', () => {
|
||||
const canvas = createCanvas(width, height, 1, 1, 0)
|
||||
const rng = new Rng(555)
|
||||
const placedRects: PlacedPresetRect[] = []
|
||||
|
||||
stampAct5SnowRoads(canvas, gridW, gridH, inX, outY, placedRects, rng)
|
||||
stampAct5FrozenTrailDivides(canvas, gridW, gridH, inX, outY, placedRects, rng)
|
||||
|
||||
const frozenDivides = countWallTiles(canvas, 2, 129)
|
||||
expect(frozenDivides).toBeGreaterThan(20)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Unified Dispatchers: stampOutdoorRoads & stampOutdoorDividers', () => {
|
||||
it('dispatches Act 2 Desert based on levelId 41', () => {
|
||||
const canvas = createCanvas(width, height, 1, 1, 0)
|
||||
const rng = new Rng(123)
|
||||
stampOutdoorRoads(canvas, 1, 41, gridW, gridH, inX, outY, [], rng)
|
||||
stampOutdoorDividers(canvas, 1, 41, gridW, gridH, inX, outY, [], rng)
|
||||
|
||||
expect(countFloorTiles(canvas, 1, 194)).toBeGreaterThan(40)
|
||||
expect(countWallTiles(canvas, 1, 129)).toBeGreaterThan(15)
|
||||
})
|
||||
|
||||
it('dispatches Act 3 Kurast based on levelId 80', () => {
|
||||
const canvas = createCanvas(width, height, 1, 1, 0)
|
||||
const rng = new Rng(123)
|
||||
stampOutdoorRoads(canvas, 1, 80, gridW, gridH, inX, outY, [], rng)
|
||||
stampOutdoorDividers(canvas, 1, 80, gridW, gridH, inX, outY, [], rng)
|
||||
|
||||
expect(countFloorTiles(canvas, 1, 194)).toBeGreaterThan(40)
|
||||
expect(countWallTiles(canvas, 1, 129)).toBeGreaterThan(15)
|
||||
})
|
||||
|
||||
it('dispatches Act 5 Snow based on levelId 111', () => {
|
||||
const canvas = createCanvas(width, height, 1, 1, 0)
|
||||
const rng = new Rng(123)
|
||||
stampOutdoorRoads(canvas, 1, 111, gridW, gridH, inX, outY, [], rng)
|
||||
stampOutdoorDividers(canvas, 1, 111, gridW, gridH, inX, outY, [], rng)
|
||||
|
||||
expect(countFloorTiles(canvas, 1, 194)).toBeGreaterThan(40)
|
||||
expect(countWallTiles(canvas, 2, 129)).toBeGreaterThan(15)
|
||||
})
|
||||
|
||||
it('dispatches Act 1 Dirt Roads based on levelId 3', () => {
|
||||
const canvas = createCanvas(width, height, 1, 1, 0)
|
||||
const rng = new Rng(123)
|
||||
stampOutdoorRoads(canvas, 1, 3, gridW, gridH, inX, outY, [], rng)
|
||||
stampOutdoorDividers(canvas, 1, 3, gridW, gridH, inX, outY, [], rng)
|
||||
|
||||
expect(countFloorTiles(canvas, 5, 194)).toBeGreaterThan(40)
|
||||
expect(countWallTiles(canvas, 2, 129)).toBeGreaterThan(15)
|
||||
})
|
||||
|
||||
it('dispatches based on levelTypeName when levelId is generic', () => {
|
||||
const c2 = createCanvas(width, height, 1, 1, 0)
|
||||
stampOutdoorRoads(c2, 1, 999, gridW, gridH, inX, outY, [], new Rng(1), 'Act 2 - Desert')
|
||||
stampOutdoorDividers(c2, 1, 999, gridW, gridH, inX, outY, [], new Rng(1), 'Act 2 - Desert')
|
||||
expect(countFloorTiles(c2, 1, 194)).toBeGreaterThan(40)
|
||||
expect(countWallTiles(c2, 1, 129)).toBeGreaterThan(15)
|
||||
|
||||
const c3 = createCanvas(width, height, 1, 1, 0)
|
||||
stampOutdoorRoads(c3, 1, 999, gridW, gridH, inX, outY, [], new Rng(1), 'Act 3 - Kurast')
|
||||
stampOutdoorDividers(c3, 1, 999, gridW, gridH, inX, outY, [], new Rng(1), 'Act 3 - Kurast')
|
||||
expect(countFloorTiles(c3, 1, 194)).toBeGreaterThan(40)
|
||||
expect(countWallTiles(c3, 1, 129)).toBeGreaterThan(15)
|
||||
|
||||
const c5 = createCanvas(width, height, 1, 1, 0)
|
||||
stampOutdoorRoads(c5, 1, 999, gridW, gridH, inX, outY, [], new Rng(1), 'Act 5 - Barricade')
|
||||
stampOutdoorDividers(c5, 1, 999, gridW, gridH, inX, outY, [], new Rng(1), 'Act 5 - Barricade')
|
||||
expect(countFloorTiles(c5, 1, 194)).toBeGreaterThan(40)
|
||||
expect(countWallTiles(c5, 2, 129)).toBeGreaterThan(15)
|
||||
})
|
||||
|
||||
it('dispatches based on act number fallback', () => {
|
||||
const c2 = createCanvas(width, height, 1, 1, 0)
|
||||
stampOutdoorRoads(c2, 2, 999, gridW, gridH, inX, outY, [], new Rng(1))
|
||||
stampOutdoorDividers(c2, 2, 999, gridW, gridH, inX, outY, [], new Rng(1))
|
||||
expect(countFloorTiles(c2, 1, 194)).toBeGreaterThan(40)
|
||||
expect(countWallTiles(c2, 1, 129)).toBeGreaterThan(15)
|
||||
})
|
||||
|
||||
it('strictly skips Cow Level (levelId 39)', () => {
|
||||
const canvas = createCanvas(width, height, 1, 1, 0)
|
||||
const rng = new Rng(42)
|
||||
stampOutdoorRoads(canvas, 1, 39, gridW, gridH, inX, outY, [], rng, 'Act 1 - Wilderness')
|
||||
stampOutdoorDividers(canvas, 1, 39, gridW, gridH, inX, outY, [], rng, 'Act 1 - Wilderness')
|
||||
|
||||
expect(countFloorTiles(canvas, 1, 194)).toBe(0)
|
||||
expect(countFloorTiles(canvas, 5, 194)).toBe(0)
|
||||
expect(countWallTiles(canvas, 1, 129)).toBe(0)
|
||||
expect(countWallTiles(canvas, 2, 129)).toBe(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe('generateWilderness Integration for Acts 2, 3, 5', () => {
|
||||
const borderDs1 = makeMockDs1(8, 8)
|
||||
const borderPiece: WildernessPiece = {
|
||||
name: 'Act 2 - Desert Border 1',
|
||||
border: true,
|
||||
levels: [borderDs1],
|
||||
}
|
||||
|
||||
it('generates Act 2 Rocky Waste (Level 41) with caravan routes and sandstone berms', () => {
|
||||
const result = generateWilderness({
|
||||
levelId: 41,
|
||||
levelName: 'Rocky Waste',
|
||||
levelTypeName: 'Act 2 - Desert',
|
||||
sizeX: 80,
|
||||
sizeY: 80,
|
||||
subType: 0,
|
||||
subTheme: 0,
|
||||
seed: 77777,
|
||||
pieces: [borderPiece],
|
||||
substitutions: [],
|
||||
})
|
||||
|
||||
expect(result.level.act).toBe(2)
|
||||
let caravanRoads = 0
|
||||
let sandstoneBerms = 0
|
||||
for (let y = 8; y < 72; y += 1) {
|
||||
for (let x = 8; x < 72; x += 1) {
|
||||
const cell = result.level.cells[y]?.[x]
|
||||
if (!cell) continue
|
||||
if (cell.floors.some(f => f.style === 1 && f.prop1 === 194)) caravanRoads += 1
|
||||
if (cell.walls.some(w => w.style === 1 && w.prop1 === 129)) sandstoneBerms += 1
|
||||
}
|
||||
}
|
||||
|
||||
expect(caravanRoads).toBeGreaterThan(30)
|
||||
expect(sandstoneBerms).toBeGreaterThan(15)
|
||||
})
|
||||
|
||||
it('generates Act 3 Kurast Bazaar (Level 80) with Kurast avenues and balustrades', () => {
|
||||
const act3Border: WildernessPiece = {
|
||||
name: 'Act 3 - Kurast Border',
|
||||
border: true,
|
||||
levels: [borderDs1],
|
||||
}
|
||||
const result = generateWilderness({
|
||||
levelId: 80,
|
||||
levelName: 'Kurast Bazaar',
|
||||
levelTypeName: 'Act 3 - Kurast',
|
||||
sizeX: 80,
|
||||
sizeY: 80,
|
||||
subType: 0,
|
||||
subTheme: 0,
|
||||
seed: 88888,
|
||||
pieces: [act3Border],
|
||||
substitutions: [],
|
||||
})
|
||||
|
||||
expect(result.level.act).toBe(3)
|
||||
let avenues = 0
|
||||
let balustrades = 0
|
||||
for (let y = 8; y < 72; y += 1) {
|
||||
for (let x = 8; x < 72; x += 1) {
|
||||
const cell = result.level.cells[y]?.[x]
|
||||
if (!cell) continue
|
||||
if (cell.floors.some(f => f.style === 1 && f.prop1 === 194)) avenues += 1
|
||||
if (cell.walls.some(w => w.style === 1 && w.prop1 === 129)) balustrades += 1
|
||||
}
|
||||
}
|
||||
|
||||
expect(avenues).toBeGreaterThan(30)
|
||||
expect(balustrades).toBeGreaterThan(15)
|
||||
})
|
||||
|
||||
it('generates Act 5 Frigid Highlands (Level 111) with snow roads and frozen trail divides', () => {
|
||||
const act5Border: WildernessPiece = {
|
||||
name: 'Act 5 - Barricade Border',
|
||||
border: true,
|
||||
levels: [borderDs1],
|
||||
}
|
||||
const result = generateWilderness({
|
||||
levelId: 111,
|
||||
levelName: 'Frigid Highlands',
|
||||
levelTypeName: 'Act 5 - Barricade',
|
||||
sizeX: 80,
|
||||
sizeY: 80,
|
||||
subType: 0,
|
||||
subTheme: 0,
|
||||
seed: 99999,
|
||||
pieces: [act5Border],
|
||||
substitutions: [],
|
||||
})
|
||||
|
||||
expect(result.level.act).toBe(5)
|
||||
let snowRoads = 0
|
||||
let frozenDivides = 0
|
||||
for (let y = 8; y < 72; y += 1) {
|
||||
for (let x = 8; x < 72; x += 1) {
|
||||
const cell = result.level.cells[y]?.[x]
|
||||
if (!cell) continue
|
||||
if (cell.floors.some(f => f.style === 1 && f.prop1 === 194)) snowRoads += 1
|
||||
if (cell.walls.some(w => w.style === 2 && w.prop1 === 129)) frozenDivides += 1
|
||||
}
|
||||
}
|
||||
|
||||
expect(snowRoads).toBeGreaterThan(30)
|
||||
expect(frozenDivides).toBeGreaterThan(15)
|
||||
})
|
||||
|
||||
it('generates Cow Level (Level 39) without roads or stone divider networks', () => {
|
||||
const cowBorder: WildernessPiece = {
|
||||
name: 'Act 1 - Wild Border 1',
|
||||
border: true,
|
||||
levels: [borderDs1],
|
||||
}
|
||||
const result = generateWilderness({
|
||||
levelId: 39,
|
||||
levelName: 'Moo Moo Farm',
|
||||
levelTypeName: 'Act 1 - Wilderness',
|
||||
sizeX: 80,
|
||||
sizeY: 80,
|
||||
subType: 0,
|
||||
subTheme: 0,
|
||||
seed: 12345,
|
||||
pieces: [cowBorder],
|
||||
substitutions: [],
|
||||
})
|
||||
|
||||
let roadFloors = 0
|
||||
let dividerWalls = 0
|
||||
for (let y = 8; y < 72; y += 1) {
|
||||
for (let x = 8; x < 72; x += 1) {
|
||||
const cell = result.level.cells[y]?.[x]
|
||||
if (!cell) continue
|
||||
if (cell.floors.some(f => (f.style === 1 || f.style === 5) && f.prop1 === 194)) roadFloors += 1
|
||||
if (cell.walls.some(w => (w.style === 1 || w.style === 2) && w.prop1 === 129)) dividerWalls += 1
|
||||
}
|
||||
}
|
||||
|
||||
expect(roadFloors).toBe(0)
|
||||
expect(dividerWalls).toBe(0)
|
||||
})
|
||||
})
|
||||
})
|
||||
Loading…
Reference in New Issue