fix(map-gen): fix Act 1 road/stone wall connectivity, Blood Moor river+bridge layout & Act 3 jungle river alignment
This commit is contained in:
parent
602565030f
commit
1f2f19641f
|
|
@ -769,8 +769,17 @@ export function selectBorderVariant(
|
||||||
isOpen: boolean,
|
isOpen: boolean,
|
||||||
rng: Rng,
|
rng: Rng,
|
||||||
): Ds1 {
|
): Ds1 {
|
||||||
const closedLevels = piece.levels.filter(l => isClosedBorder(l, orientation))
|
// In Blizzard LvlPrest.txt tables with >= 4 variants (Act 1 Bord1..4 and Act 4 Border1..4),
|
||||||
const openLevels = piece.levels.filter(l => !isClosedBorder(l, orientation))
|
// slots 0..2 are closed borders (a/b/c) and slots 3+ are open exit/gate borders (o/oe).
|
||||||
|
let closedLevels: readonly Ds1[]
|
||||||
|
let openLevels: readonly Ds1[]
|
||||||
|
if (piece.levels.length >= 4) {
|
||||||
|
closedLevels = piece.levels.slice(0, 3)
|
||||||
|
openLevels = piece.levels.slice(3)
|
||||||
|
} else {
|
||||||
|
closedLevels = piece.levels.filter(l => isClosedBorder(l, orientation))
|
||||||
|
openLevels = piece.levels.filter(l => !isClosedBorder(l, orientation))
|
||||||
|
}
|
||||||
|
|
||||||
if (isOpen && openLevels.length > 0) {
|
if (isOpen && openLevels.length > 0) {
|
||||||
return openLevels[rng.int(0, openLevels.length - 1)]!
|
return openLevels[rng.int(0, openLevels.length - 1)]!
|
||||||
|
|
@ -797,17 +806,9 @@ export function getPerimeterOpenings(
|
||||||
return openings
|
return openings
|
||||||
}
|
}
|
||||||
|
|
||||||
if (levelId === 2) {
|
// Act 1 wilderness levels (2..7) & Act 4 Mesa wilderness levels (104..106):
|
||||||
// Blood Moor: exactly 1 entrance (South edge) and 1 exit (East edge)
|
// entrance on South edge, exit on East edge
|
||||||
const inX = Math.floor(gridWidth / 2)
|
if ((levelId >= 2 && levelId <= 7) || (levelId >= 104 && levelId <= 106)) {
|
||||||
openings.add(key(inX, gridHeight - 1))
|
|
||||||
const outY = Math.floor(gridHeight / 2)
|
|
||||||
openings.add(key(gridWidth - 1, outY))
|
|
||||||
return openings
|
|
||||||
}
|
|
||||||
|
|
||||||
// Act 1 wilderness levels (3..7): entrance on South, exit on East
|
|
||||||
if (levelId >= 3 && levelId <= 7) {
|
|
||||||
const inX = Math.floor(gridWidth / 2)
|
const inX = Math.floor(gridWidth / 2)
|
||||||
openings.add(key(inX, gridHeight - 1))
|
openings.add(key(inX, gridHeight - 1))
|
||||||
const outY = Math.floor(gridHeight / 2)
|
const outY = Math.floor(gridHeight / 2)
|
||||||
|
|
@ -1311,6 +1312,97 @@ export const SPECIAL_PRESETS_BY_LEVEL: Readonly<Record<number, readonly string[]
|
||||||
* @param stats - report collector.
|
* @param stats - report collector.
|
||||||
* @returns set of piece names that were placed.
|
* @returns set of piece names that were placed.
|
||||||
*/
|
*/
|
||||||
|
/** Bounding rectangle of a placed special preset in canvas tile coordinates. */
|
||||||
|
export interface PlacedPresetRect {
|
||||||
|
readonly name: string
|
||||||
|
readonly x: number
|
||||||
|
readonly y: number
|
||||||
|
readonly w: number
|
||||||
|
readonly h: number
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stamp a continuous 2-block-wide (16-tile) North-South river stream across the level with
|
||||||
|
* a 2-block wooden bridge crossing at `by = outY` where the main East-West road passes.
|
||||||
|
*/
|
||||||
|
function stampAct1RiverAndBridge(
|
||||||
|
canvas: Canvas,
|
||||||
|
pieces: readonly WildernessPiece[],
|
||||||
|
gridWidth: number,
|
||||||
|
gridHeight: number,
|
||||||
|
inX: number,
|
||||||
|
outY: number,
|
||||||
|
occupied: boolean[][],
|
||||||
|
placedPieces: Set<string>,
|
||||||
|
placedRects: PlacedPresetRect[],
|
||||||
|
stats: WildernessStats,
|
||||||
|
): boolean {
|
||||||
|
const upperPiece = pieces.find(p => p.name === 'Act 1 - River Upper' || p.name.startsWith('Act 1 - River Upper'))
|
||||||
|
const lowerPiece = pieces.find(p => p.name === 'Act 1 - River Lower' || p.name.startsWith('Act 1 - River Lower'))
|
||||||
|
const bridgePiece = pieces.find(p => p.name === 'Act 1 - Bridge' || p.name.startsWith('Act 1 - Bridge'))
|
||||||
|
if (!upperPiece?.levels[0] || !lowerPiece?.levels[0] || !bridgePiece?.levels[0]) return false
|
||||||
|
|
||||||
|
// Place the 2-column vertical river stream just East of the central crossroads (e.g. blocks 6 & 7 on 10x10 grid)
|
||||||
|
const riverBlockX = Math.min(gridWidth - 3, inX + 1)
|
||||||
|
if (riverBlockX < 1 || riverBlockX + 1 >= gridWidth - 1) return false
|
||||||
|
|
||||||
|
for (let by = 1; by <= gridHeight - 2; by += 1) {
|
||||||
|
const tileX0 = riverBlockX * TILES_PER_BLOCK
|
||||||
|
const tileX1 = (riverBlockX + 1) * TILES_PER_BLOCK
|
||||||
|
const tileY = by * TILES_PER_BLOCK
|
||||||
|
|
||||||
|
if (by === outY) {
|
||||||
|
// Main East-West road crossing: stamp BridgeU (west half) + BridgeL (east half)
|
||||||
|
const bridgeU = bridgePiece.levels[0]!
|
||||||
|
const bridgeL = bridgePiece.levels[1] ?? bridgePiece.levels[0]!
|
||||||
|
stampDs1(canvas, bridgeU, tileX0, tileY)
|
||||||
|
stampDs1(canvas, bridgeL, tileX1, tileY)
|
||||||
|
} else if (by === 1) {
|
||||||
|
// North river source/mouth (URiverN + LRiverN)
|
||||||
|
const uNorth = upperPiece.levels[1] ?? upperPiece.levels[0]!
|
||||||
|
const lNorth = lowerPiece.levels[1] ?? lowerPiece.levels[0]!
|
||||||
|
stampDs1(canvas, uNorth, tileX0, tileY)
|
||||||
|
stampDs1(canvas, lNorth, tileX1, tileY)
|
||||||
|
} else if (by === gridHeight - 2) {
|
||||||
|
// South river mouth (URiverS + LRiverS)
|
||||||
|
const uSouth = upperPiece.levels[2] ?? upperPiece.levels[0]!
|
||||||
|
const lSouth = lowerPiece.levels[2] ?? lowerPiece.levels[0]!
|
||||||
|
stampDs1(canvas, uSouth, tileX0, tileY)
|
||||||
|
stampDs1(canvas, lSouth, tileX1, tileY)
|
||||||
|
} else {
|
||||||
|
// Center continuous river channel (URiverC + LRiverC)
|
||||||
|
stampDs1(canvas, upperPiece.levels[0]!, tileX0, tileY)
|
||||||
|
stampDs1(canvas, lowerPiece.levels[0]!, tileX1, tileY)
|
||||||
|
}
|
||||||
|
|
||||||
|
occupied[by]![riverBlockX] = true
|
||||||
|
occupied[by]![riverBlockX + 1] = true
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const name of [upperPiece.name, lowerPiece.name, bridgePiece.name]) {
|
||||||
|
placedPieces.add(name)
|
||||||
|
stats.substitutions.push({ name, role: 'object', enabled: true, clusters: 1 })
|
||||||
|
}
|
||||||
|
placedRects.push({
|
||||||
|
name: 'Act 1 - River',
|
||||||
|
x: riverBlockX * TILES_PER_BLOCK,
|
||||||
|
y: TILES_PER_BLOCK,
|
||||||
|
w: 2 * TILES_PER_BLOCK,
|
||||||
|
h: (gridHeight - 2) * TILES_PER_BLOCK,
|
||||||
|
})
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scatter special presets across the level's interior.
|
||||||
|
*
|
||||||
|
* Follows D2MOO's `DRLGOUTWILD_SpawnSpecialPresets` and
|
||||||
|
* `DRLGOUTDOORS_SpawnOutdoorLevelPreset`: candidate interior grid coordinates
|
||||||
|
* (excluding the outer 1-block border and any reserved main road corridors) are
|
||||||
|
* shuffled deterministically with the level's RNG, and each preset in sequence
|
||||||
|
* scans for the first free block rectangle where it can fit without overlapping
|
||||||
|
* already-occupied blocks or the perimeter.
|
||||||
|
*/
|
||||||
function spawnSpecialPresets(
|
function spawnSpecialPresets(
|
||||||
canvas: Canvas,
|
canvas: Canvas,
|
||||||
pieces: readonly WildernessPiece[],
|
pieces: readonly WildernessPiece[],
|
||||||
|
|
@ -1319,12 +1411,14 @@ function spawnSpecialPresets(
|
||||||
gridHeight: number,
|
gridHeight: number,
|
||||||
rng: Rng,
|
rng: Rng,
|
||||||
stats: WildernessStats,
|
stats: WildernessStats,
|
||||||
): Set<string> {
|
reservedBlocks?: ReadonlySet<string>,
|
||||||
|
): { placedPieces: Set<string>; placedRects: PlacedPresetRect[] } {
|
||||||
const innerWidth = gridWidth - 2
|
const innerWidth = gridWidth - 2
|
||||||
const innerHeight = gridHeight - 2
|
const innerHeight = gridHeight - 2
|
||||||
const totalSlots = innerWidth * innerHeight
|
const totalSlots = innerWidth * innerHeight
|
||||||
const placedPieces = new Set<string>()
|
const placedPieces = new Set<string>()
|
||||||
if (totalSlots <= 0 || presetNames.length === 0) return placedPieces
|
const placedRects: PlacedPresetRect[] = []
|
||||||
|
if (totalSlots <= 0 || presetNames.length === 0) return { placedPieces, placedRects }
|
||||||
|
|
||||||
const occupied: boolean[][] = Array.from({ length: gridHeight }, () => Array.from({ length: gridWidth }, () => false))
|
const occupied: boolean[][] = Array.from({ length: gridHeight }, () => Array.from({ length: gridWidth }, () => false))
|
||||||
for (let x = 0; x < gridWidth; x += 1) {
|
for (let x = 0; x < gridWidth; x += 1) {
|
||||||
|
|
@ -1335,8 +1429,52 @@ function spawnSpecialPresets(
|
||||||
occupied[y]![0] = true
|
occupied[y]![0] = true
|
||||||
occupied[y]![gridWidth - 1] = true
|
occupied[y]![gridWidth - 1] = true
|
||||||
}
|
}
|
||||||
|
if (reservedBlocks && gridWidth >= 8 && gridHeight >= 8) {
|
||||||
|
for (const key of reservedBlocks) {
|
||||||
|
const [bxStr, byStr] = key.split(',')
|
||||||
|
const bx = Number(bxStr)
|
||||||
|
const by = Number(byStr)
|
||||||
|
if (occupied[by]?.[bx] !== undefined) {
|
||||||
|
occupied[by]![bx] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
for (const presetName of presetNames) {
|
// Handle continuous river + bridge layout for Blood Moor when requested on standard grids
|
||||||
|
let remainingPresets = [...presetNames]
|
||||||
|
const wantsRiver =
|
||||||
|
gridWidth >= 8 &&
|
||||||
|
gridHeight >= 6 &&
|
||||||
|
remainingPresets.some(n => n.startsWith('Act 1 - River Upper')) &&
|
||||||
|
remainingPresets.some(n => n.startsWith('Act 1 - River Lower')) &&
|
||||||
|
remainingPresets.some(n => n.startsWith('Act 1 - Bridge'))
|
||||||
|
if (wantsRiver) {
|
||||||
|
const inX = Math.floor(gridWidth / 2)
|
||||||
|
const outY = Math.floor(gridHeight / 2)
|
||||||
|
if (
|
||||||
|
stampAct1RiverAndBridge(
|
||||||
|
canvas,
|
||||||
|
pieces,
|
||||||
|
gridWidth,
|
||||||
|
gridHeight,
|
||||||
|
inX,
|
||||||
|
outY,
|
||||||
|
occupied,
|
||||||
|
placedPieces,
|
||||||
|
placedRects,
|
||||||
|
stats,
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
remainingPresets = remainingPresets.filter(
|
||||||
|
n =>
|
||||||
|
!n.startsWith('Act 1 - River Upper') &&
|
||||||
|
!n.startsWith('Act 1 - River Lower') &&
|
||||||
|
!n.startsWith('Act 1 - Bridge'),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const presetName of remainingPresets) {
|
||||||
const piece = pieces.find(p => p.name === presetName || p.name.startsWith(presetName))
|
const piece = pieces.find(p => p.name === presetName || p.name.startsWith(presetName))
|
||||||
if (!piece || piece.levels.length === 0) continue
|
if (!piece || piece.levels.length === 0) continue
|
||||||
|
|
||||||
|
|
@ -1376,7 +1514,9 @@ function spawnSpecialPresets(
|
||||||
}
|
}
|
||||||
if (blocked) continue
|
if (blocked) continue
|
||||||
|
|
||||||
stampDs1(canvas, ds1, bx * TILES_PER_BLOCK, by * TILES_PER_BLOCK)
|
const tileX = bx * TILES_PER_BLOCK
|
||||||
|
const tileY = by * TILES_PER_BLOCK
|
||||||
|
stampDs1(canvas, ds1, tileX, tileY)
|
||||||
|
|
||||||
for (let dy = 0; dy < blocksY; dy += 1) {
|
for (let dy = 0; dy < blocksY; dy += 1) {
|
||||||
for (let dx = 0; dx < blocksX; dx += 1) {
|
for (let dx = 0; dx < blocksX; dx += 1) {
|
||||||
|
|
@ -1391,6 +1531,13 @@ function spawnSpecialPresets(
|
||||||
clusters: 1,
|
clusters: 1,
|
||||||
})
|
})
|
||||||
placedPieces.add(piece.name)
|
placedPieces.add(piece.name)
|
||||||
|
placedRects.push({
|
||||||
|
name: piece.name,
|
||||||
|
x: tileX,
|
||||||
|
y: tileY,
|
||||||
|
w: ds1.width,
|
||||||
|
h: ds1.height,
|
||||||
|
})
|
||||||
placed = true
|
placed = true
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
@ -1400,7 +1547,7 @@ function spawnSpecialPresets(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return placedPieces
|
return { placedPieces, placedRects }
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ------------------------------------------------------------------------- *
|
/* ------------------------------------------------------------------------- *
|
||||||
|
|
@ -1409,17 +1556,6 @@ function spawnSpecialPresets(
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Derive the block grid for a level whose `Levels.txt` size is unset.
|
* Derive the block grid for a level whose `Levels.txt` size is unset.
|
||||||
*
|
|
||||||
* `Act 5 - Barricade 1` and `2` ship `SizeX = SizeY = -1`. The engine does not
|
|
||||||
* read a size for them: `DRLGOUTSIEGE_ConnectBarricadeAndSiege` positions the
|
|
||||||
* barricade level against the siege level and sizes it from the barricade piece
|
|
||||||
* itself. The barricade piece is the `LvlSub` row with `GridSize = 2` whose name
|
|
||||||
* is `Barricade`, so its DS1's extent is the level's extent — that is what this
|
|
||||||
* reproduces.
|
|
||||||
*
|
|
||||||
* @param request - the request.
|
|
||||||
* @returns the size in cells and where it came from.
|
|
||||||
* @throws when no barricade piece is available to size the level from.
|
|
||||||
*/
|
*/
|
||||||
function resolveUnsetSize(request: WildernessRequest): { sizeX: number; sizeY: number; source: string } {
|
function resolveUnsetSize(request: WildernessRequest): { sizeX: number; sizeY: number; source: string } {
|
||||||
if (request.sizeOverride !== undefined) {
|
if (request.sizeOverride !== undefined) {
|
||||||
|
|
@ -1433,30 +1569,8 @@ function resolveUnsetSize(request: WildernessRequest): { sizeX: number; sizeY: n
|
||||||
return { sizeX: 160, sizeY: 64, source: 'D2MOO canonical barricade size' }
|
return { sizeX: 160, sizeY: 64, source: 'D2MOO canonical barricade size' }
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Generate one wilderness level.
|
|
||||||
*
|
|
||||||
* The pipeline follows `DRLGOUTDOORS_GenerateLevel`: derive the block grid from
|
|
||||||
* the level's declared size, lay the ground, walk the outer ring stamping the
|
|
||||||
* border pieces, then scatter the `LvlSub` substitutions according to
|
|
||||||
* `SubType`/`SubTheme`. What it does **not** do is the per-act work that decides
|
|
||||||
* the level's outline and where it meets its neighbours — see
|
|
||||||
* {@link UNIMPLEMENTED_PASSES} and the module header.
|
|
||||||
*
|
|
||||||
* @param request - the real table data for one `DrlgType == 3` level.
|
|
||||||
* @returns the synthesized map plus a report of what was done.
|
|
||||||
* @throws when the level cannot be built: a size that yields no usable grid, or
|
|
||||||
* an unset size with nothing to derive one from. The message always names the
|
|
||||||
* level, because an empty map silently baked into an asset pack is worse than a
|
|
||||||
* crash.
|
|
||||||
*/
|
|
||||||
/**
|
/**
|
||||||
* Assemble the 15 sequential 16x48 siege strips for Act 5 - Siege 1 (Bloody Foothills, LevelId 110).
|
* Assemble the 15 sequential 16x48 siege strips for Act 5 - Siege 1 (Bloody Foothills, LevelId 110).
|
||||||
*
|
|
||||||
* In Diablo II (`DRLGOUTSIEGE_InitAct5Siege`), Bloody Foothills is a 240x48 uphill battleground
|
|
||||||
* constructed from 15 sequential 16x48 DS1 strips: `Siege To Barricade` at x=0 (West/North exit to
|
|
||||||
* Frigid Highlands), `Siege Strip 13` (Shenk the Overseer's boss platform) at x=16, down through
|
|
||||||
* `Siege Strip 1` at x=208, and `Siege To Town` (Harrogath gate) at x=224.
|
|
||||||
*/
|
*/
|
||||||
function stampAct5SiegeStrips(
|
function stampAct5SiegeStrips(
|
||||||
canvas: Canvas,
|
canvas: Canvas,
|
||||||
|
|
@ -1571,6 +1685,13 @@ function stampAct3KurastCauseway(
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Assemble the 64x192 (2x6 blocks of 32x32) river and clearing layout for Act 3 Jungles (LevelId 76, 77, 78).
|
* 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(
|
function stampAct3Jungle(
|
||||||
canvas: Canvas,
|
canvas: Canvas,
|
||||||
|
|
@ -1599,145 +1720,190 @@ function stampAct3Jungle(
|
||||||
: levelId === 77
|
: levelId === 77
|
||||||
? 'Act 3 - Clearing Boggy'
|
? 'Act 3 - Clearing Boggy'
|
||||||
: 'Act 3 - Clearing Pygmy'
|
: 'Act 3 - Clearing Pygmy'
|
||||||
const clearingPieces = pieces.filter(p => p.name.startsWith(clearingPrefix) && p.levels.length > 0)
|
const allClearingPieces = pieces.filter(p => p.name.startsWith(clearingPrefix) && p.levels.length > 0)
|
||||||
const riverPieces = pieces.filter(
|
// 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 =>
|
||||||
p.name.startsWith('Act 3 - Jungle ') &&
|
p.name.startsWith('Act 3 - Jungle ') &&
|
||||||
p.name !== 'Act 3 - Jungle Head' &&
|
p.name !== 'Act 3 - Jungle Head' &&
|
||||||
p.name !== 'Act 3 - Jungle Tail' &&
|
p.name !== 'Act 3 - Jungle Tail' &&
|
||||||
p.levels.length > 0,
|
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) {
|
for (let row = 0; row < 4; row += 1) {
|
||||||
const y = 32 + row * 32
|
const y = 32 + row * 32
|
||||||
const riverOnLeft = (row + rng.int(0, 1)) % 2 === 0
|
// West column (x = 0): Jungle Clearing opening East
|
||||||
for (let col = 0; col < 2; col += 1) {
|
if (clearingPool.length > 0) {
|
||||||
const x = col * 32
|
const chosenClearing = clearingPool[rng.int(0, clearingPool.length - 1)]!
|
||||||
const isRiverSlot = (col === 0 && riverOnLeft) || (col === 1 && !riverOnLeft)
|
const ds1 = chosenClearing.levels[rng.int(0, chosenClearing.levels.length - 1)]!
|
||||||
const pool = isRiverSlot || clearingPieces.length === 0 ? riverPieces : clearingPieces
|
stampDs1(canvas, ds1, 0, y)
|
||||||
if (pool.length === 0) continue
|
placed.add(chosenClearing.name)
|
||||||
const chosen = pool[rng.int(0, pool.length - 1)]!
|
stats.substitutions.push({ name: chosenClearing.name, role: 'object', enabled: true, clusters: 1 })
|
||||||
const ds1 = chosen.levels[rng.int(0, chosen.levels.length - 1)]!
|
}
|
||||||
stampDs1(canvas, ds1, x, y)
|
// East column (x = 32): Continuous North-South River channel
|
||||||
placed.add(chosen.name)
|
if (riverPool.length > 0) {
|
||||||
stats.substitutions.push({ name: chosen.name, role: 'object', enabled: true, clusters: 1 })
|
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
|
return placed
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Lay a 2-tile-wide meandering dirt road network connecting the South entrance, East exit,
|
* Lay a 3-tile-wide main dirt highway connecting the South Border Gate (`Bord1o.ds1` at `x = inX*8 + 3..5`),
|
||||||
* and interior landmarks in Act 1 wilderness levels (LevelId 2..7).
|
* 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.
|
||||||
*/
|
*/
|
||||||
function stampAct1DirtRoads(canvas: Canvas, rng: Rng): void {
|
function stampAct1DirtRoads(
|
||||||
const roadFloor: Ds1Floor = {
|
canvas: Canvas,
|
||||||
|
gridWidth: number,
|
||||||
|
gridHeight: number,
|
||||||
|
inX: number,
|
||||||
|
outY: number,
|
||||||
|
placedRects: readonly PlacedPresetRect[],
|
||||||
|
rng: Rng,
|
||||||
|
): void {
|
||||||
|
const makeRoadFloor = (): Ds1Floor => ({
|
||||||
prop1: 194,
|
prop1: 194,
|
||||||
sequence: 0,
|
sequence: rng.int(0, 6) === 0 ? 25 : 0,
|
||||||
style: 5,
|
style: 5,
|
||||||
unknown1: 0,
|
unknown1: 0,
|
||||||
unknown2: 0,
|
unknown2: 0,
|
||||||
hidden: false,
|
hidden: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
const isInsideProtectedPreset = (x: number, y: number): boolean => {
|
||||||
|
for (const r of placedRects) {
|
||||||
|
if (r.name === 'Act 1 - River') continue
|
||||||
|
if (x >= r.x && x < r.x + r.w && y >= r.y && y < r.y + r.h) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
const paintRoadCell = (cx: number, cy: number): void => {
|
const paintRoadTile = (x: number, y: number, clearStrayClutter: boolean): void => {
|
||||||
for (let dy = 0; dy <= 1; dy += 1) {
|
if (x < 4 || x >= canvas.width - 4 || y < 4 || y >= canvas.height - 4) return
|
||||||
for (let dx = 0; dx <= 1; dx += 1) {
|
if (isInsideProtectedPreset(x, y)) return
|
||||||
const x = cx + dx
|
const cell = canvas.cells[y]?.[x]
|
||||||
const y = cy + dy
|
if (!cell) return
|
||||||
if (x < 8 || x >= canvas.width - 8 || y < 8 || y >= canvas.height - 8) continue
|
const existingFloor = cell.floors[0]
|
||||||
const cell = canvas.cells[y]?.[x]
|
// Never overwrite water (`style 2/3`) or wooden bridge planks (`style 4`)
|
||||||
if (!cell) continue
|
if (existingFloor && (existingFloor.style === 2 || existingFloor.style === 3 || existingFloor.style === 4)) {
|
||||||
// Do not overwrite cells that already carry walls or water/bridge floors
|
return
|
||||||
if (cell.walls.some(w => !w.hidden && w.prop1 !== 0)) continue
|
}
|
||||||
const existingFloor = cell.floors[0]
|
if (cell.walls.some(w => !w.hidden && w.prop1 !== 0)) {
|
||||||
if (existingFloor && existingFloor.style !== 0) continue
|
if (clearStrayClutter) {
|
||||||
;(cell.floors as Ds1Floor[])[0] = roadFloor
|
// Clear random LvlSub bush/tree clutter on the main highway spine
|
||||||
|
;(cell.walls as Ds1Wall[]).length = 0
|
||||||
|
} else {
|
||||||
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
;(cell.floors as Ds1Floor[])[0] = makeRoadFloor()
|
||||||
|
}
|
||||||
|
|
||||||
|
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))
|
||||||
|
|
||||||
|
// 1. 3-tile-wide South-to-North main highway (`x = hubX - 1, hubX, hubX + 1`)
|
||||||
|
for (let y = northEnd; y <= southGateY; y += 1) {
|
||||||
|
for (let dx = -1; dx <= 1; dx += 1) {
|
||||||
|
paintRoadTile(hubX + dx, y, true)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const connectPoints = (x0: number, y0: number, x1: number, y1: number): void => {
|
// 2. 3-tile-wide West-to-East main highway (`y = hubY - 1, hubY, hubY + 1`)
|
||||||
|
for (let x = westEnd; x <= eastGateX; x += 1) {
|
||||||
|
for (let dy = -1; dy <= 1; dy += 1) {
|
||||||
|
paintRoadTile(x, hubY + dy, 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
|
let curX = x0
|
||||||
let curY = y0
|
let curY = y0
|
||||||
let steps = 0
|
let steps = 0
|
||||||
const maxSteps = canvas.width + canvas.height + 40
|
const maxSteps = canvas.width + canvas.height
|
||||||
while ((Math.abs(curX - x1) > 1 || Math.abs(curY - y1) > 1) && steps < maxSteps) {
|
while ((curX !== x1 || curY !== y1) && steps < maxSteps) {
|
||||||
paintRoadCell(curX, curY)
|
for (let dy = 0; dy <= 1; dy += 1) {
|
||||||
const dx = x1 - curX
|
for (let dx = 0; dx <= 1; dx += 1) {
|
||||||
const dy = y1 - curY
|
paintRoadTile(curX + dx, curY + dy, false)
|
||||||
if (Math.abs(dx) > Math.abs(dy)) {
|
}
|
||||||
curX += dx > 0 ? 1 : -1
|
|
||||||
if (rng.int(0, 3) === 0 && dy !== 0) curY += dy > 0 ? 1 : -1
|
|
||||||
} else {
|
|
||||||
curY += dy > 0 ? 1 : -1
|
|
||||||
if (rng.int(0, 3) === 0 && dx !== 0) curX += dx > 0 ? 1 : -1
|
|
||||||
}
|
}
|
||||||
|
if (curX !== x1) curX += x1 > curX ? 1 : -1
|
||||||
|
else if (curY !== y1) curY += y1 > curY ? 1 : -1
|
||||||
steps += 1
|
steps += 1
|
||||||
}
|
}
|
||||||
paintRoadCell(x1, y1)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const hubX = Math.floor(canvas.width / 2) + rng.int(-5, 5)
|
for (const r of placedRects) {
|
||||||
const hubY = Math.floor(canvas.height / 2) + rng.int(-5, 5)
|
if (
|
||||||
const southX = Math.floor(canvas.width / 2)
|
r.name === 'Act 1 - River' ||
|
||||||
const southY = canvas.height - 9
|
/Stone Fill|Swamp Fill|Tree Fill/i.test(r.name)
|
||||||
const eastX = canvas.width - 9
|
) {
|
||||||
const eastY = Math.floor(canvas.height / 2)
|
continue
|
||||||
const nwX = Math.floor(canvas.width * 0.28)
|
}
|
||||||
const nwY = Math.floor(canvas.height * 0.28)
|
const centerX = Math.floor(r.x + r.w / 2)
|
||||||
const swX = Math.floor(canvas.width * 0.28)
|
const centerY = Math.floor(r.y + r.h / 2)
|
||||||
const swY = Math.floor(canvas.height * 0.72)
|
// Pick the edge of the preset closest to the main crossroads
|
||||||
|
const distToNS = Math.abs(centerX - hubX)
|
||||||
connectPoints(southX, southY, hubX, hubY)
|
const distToEW = Math.abs(centerY - hubY)
|
||||||
connectPoints(hubX, hubY, eastX, eastY)
|
if (distToNS <= distToEW) {
|
||||||
connectPoints(hubX, hubY, nwX, nwY)
|
const doorX = centerX < hubX ? r.x + r.w : r.x - 1
|
||||||
connectPoints(hubX, hubY, swX, swY)
|
const doorY = Math.max(r.y + 2, Math.min(r.y + r.h - 3, centerY))
|
||||||
|
paintSpurSegment(doorX, doorY, hubX, doorY)
|
||||||
|
} else {
|
||||||
|
const doorX = Math.max(r.x + 2, Math.min(r.x + r.w - 3, centerX))
|
||||||
|
const doorY = centerY < hubY ? r.y + r.h : r.y - 1
|
||||||
|
paintSpurSegment(doorX, doorY, doorX, hubY)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Lay connected stone wall divider lines (`style=2, prop1=129`) across the interior of Act 1
|
* Lay connected stone wall divider lines (`style=2, prop1=129`) across the interior of Act 1
|
||||||
* wilderness levels (LevelId 2..7), creating classic stone-walled courtyards and meadows with
|
* wilderness levels (LevelId 2..7), using exact Blizzard tile sequences:
|
||||||
* archway gate openings where roads pass through.
|
* - 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)
|
||||||
*/
|
*/
|
||||||
function stampAct1InteriorStoneWalls(canvas: Canvas, gridWidth: number, gridHeight: number, rng: Rng): void {
|
function stampAct1InteriorStoneWalls(
|
||||||
const ewWall = (seq: number): Ds1Wall => ({
|
canvas: Canvas,
|
||||||
prop1: 129,
|
gridWidth: number,
|
||||||
sequence: seq % 3,
|
gridHeight: number,
|
||||||
style: 2,
|
inX: number,
|
||||||
type: 2,
|
outY: number,
|
||||||
unknown1: 0,
|
placedRects: readonly PlacedPresetRect[],
|
||||||
unknown2: 0,
|
rng: Rng,
|
||||||
hidden: false,
|
): void {
|
||||||
})
|
if (gridWidth < 8 || gridHeight < 8) return
|
||||||
const nsWall = (seq: number): Ds1Wall => ({
|
|
||||||
prop1: 129,
|
|
||||||
sequence: seq % 3,
|
|
||||||
style: 2,
|
|
||||||
type: 1,
|
|
||||||
unknown1: 0,
|
|
||||||
unknown2: 0,
|
|
||||||
hidden: false,
|
|
||||||
})
|
|
||||||
const cornerPillar: Ds1Wall = {
|
|
||||||
prop1: 129,
|
|
||||||
sequence: 0,
|
|
||||||
style: 2,
|
|
||||||
type: 3,
|
|
||||||
unknown1: 0,
|
|
||||||
unknown2: 0,
|
|
||||||
hidden: false,
|
|
||||||
}
|
|
||||||
|
|
||||||
const canPlaceWall = (x: number, y: number): boolean => {
|
const makeWall = (type: number, sequence: number): Ds1Wall => ({
|
||||||
if (x < 10 || x >= canvas.width - 10 || y < 10 || y >= canvas.height - 10) return false
|
prop1: 129,
|
||||||
const cell = canvas.cells[y]?.[x]
|
sequence,
|
||||||
if (!cell) return false
|
style: 2,
|
||||||
if (cell.walls.some(w => !w.hidden && w.prop1 !== 0)) return false
|
type,
|
||||||
// Leave gate openings wherever a dirt road (`style === 5`) or water crosses
|
unknown1: 0,
|
||||||
if (cell.floors.some(f => !f.hidden && f.style !== 0)) return false
|
unknown2: 0,
|
||||||
return true
|
hidden: false,
|
||||||
}
|
})
|
||||||
|
|
||||||
const placeWall = (x: number, y: number, wall: Ds1Wall): void => {
|
const placeWall = (x: number, y: number, wall: Ds1Wall): void => {
|
||||||
const cell = canvas.cells[y]?.[x]
|
const cell = canvas.cells[y]?.[x]
|
||||||
|
|
@ -1750,50 +1916,98 @@ function stampAct1InteriorStoneWalls(canvas: Canvas, gridWidth: number, gridHeig
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Primary horizontal (East-West) stone wall divider near the middle of the map
|
// Offset stone wall dividers by 2 blocks (16 tiles) from the main road spine
|
||||||
const midBlockY = Math.floor(gridHeight / 2) + (rng.int(0, 1) === 0 ? -1 : 1)
|
const offsetBlockY = rng.int(0, 1) === 0 ? -2 : 2
|
||||||
const wallY = Math.max(16, Math.min(canvas.height - 16, midBlockY * TILES_PER_BLOCK))
|
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))
|
||||||
|
|
||||||
// Primary vertical (North-South) stone wall divider
|
const isCellBlockedForWall = (x: number, y: number): boolean => {
|
||||||
const midBlockX = Math.floor(gridWidth / 2) + (rng.int(0, 1) === 0 ? -1 : 1)
|
if (x < 10 || x >= canvas.width - 10 || y < 10 || y >= canvas.height - 10) return true
|
||||||
const wallX = Math.max(16, Math.min(canvas.width - 16, midBlockX * TILES_PER_BLOCK))
|
// Leave 2-tile margin around all placed special presets (houses, caves, rivers, corrals)
|
||||||
|
for (const r of placedRects) {
|
||||||
// Draw East-West stone wall line with periodic gate gaps
|
if (x >= r.x - 2 && x < r.x + r.w + 2 && y >= r.y - 2 && y < r.y + r.h + 2) {
|
||||||
let prevPlacedEW = false
|
return true
|
||||||
for (let x = 11; x < canvas.width - 11; x += 1) {
|
|
||||||
const isGate = (x % 18) >= 7 && (x % 18) <= 10
|
|
||||||
if (isGate || Math.abs(x - wallX) <= 1 || !canPlaceWall(x, wallY)) {
|
|
||||||
if (prevPlacedEW && canPlaceWall(x - 1, wallY)) {
|
|
||||||
placeWall(x - 1, wallY, cornerPillar)
|
|
||||||
}
|
}
|
||||||
prevPlacedEW = false
|
|
||||||
continue
|
|
||||||
}
|
}
|
||||||
if (!prevPlacedEW) {
|
// Leave 3-tile wide archway gate openings wherever a dirt road (style 5), bridge (style 4), or water (style 2/3) passes
|
||||||
placeWall(x, wallY, cornerPillar)
|
for (let dy = -1; dy <= 1; dy += 1) {
|
||||||
} else {
|
for (let dx = -1; dx <= 1; dx += 1) {
|
||||||
placeWall(x, wallY, ewWall(x))
|
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))) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
prevPlacedEW = true
|
const cell = canvas.cells[y]?.[x]
|
||||||
|
if (!cell) return true
|
||||||
|
if (cell.walls.some(w => !w.hidden && w.prop1 !== 0)) return true
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// Draw North-South stone wall line with periodic gate gaps
|
// Build valid tile masks along wallY (EW) and wallX (NS), inserting periodic pasture gates
|
||||||
let prevPlacedNS = false
|
const validEW: boolean[] = Array.from({ length: canvas.width }, (_, x) => {
|
||||||
for (let y = 11; y < canvas.height - 11; y += 1) {
|
const isPastureGate = (x % 24) >= 11 && (x % 24) <= 13 && Math.abs(x - wallX) > 3
|
||||||
const isGate = (y % 18) >= 7 && (y % 18) <= 10
|
return !isPastureGate && !isCellBlockedForWall(x, wallY)
|
||||||
if (isGate || Math.abs(y - wallY) <= 1 || !canPlaceWall(wallX, y)) {
|
})
|
||||||
if (prevPlacedNS && canPlaceWall(wallX, y - 1)) {
|
const validNS: boolean[] = Array.from({ length: canvas.height }, (_, y) => {
|
||||||
placeWall(wallX, y - 1, cornerPillar)
|
const isPastureGate = (y % 24) >= 11 && (y % 24) <= 13 && Math.abs(y - wallY) > 3
|
||||||
|
return !isPastureGate && !isCellBlockedForWall(wallX, y)
|
||||||
|
})
|
||||||
|
|
||||||
|
const extractRuns = (mask: readonly boolean[]): Array<{ start: number; end: number }> => {
|
||||||
|
const runs: Array<{ start: number; end: number }> = []
|
||||||
|
let start = -1
|
||||||
|
for (let i = 0; i < mask.length; i += 1) {
|
||||||
|
if (mask[i]) {
|
||||||
|
if (start === -1) start = i
|
||||||
|
} else if (start !== -1) {
|
||||||
|
if (i - start >= 4) runs.push({ start, end: i - 1 })
|
||||||
|
start = -1
|
||||||
}
|
}
|
||||||
prevPlacedNS = false
|
|
||||||
continue
|
|
||||||
}
|
}
|
||||||
if (!prevPlacedNS) {
|
if (start !== -1 && mask.length - start >= 4) {
|
||||||
placeWall(wallX, y, cornerPillar)
|
runs.push({ start, end: mask.length - 1 })
|
||||||
} else {
|
}
|
||||||
placeWall(wallX, y, nsWall(y))
|
return runs
|
||||||
|
}
|
||||||
|
|
||||||
|
const ewRuns = extractRuns(validEW)
|
||||||
|
const nsRuns = extractRuns(validNS)
|
||||||
|
|
||||||
|
const ewCoversCorner = ewRuns.some(r => wallX >= r.start && wallX <= r.end)
|
||||||
|
const nsCoversCorner = nsRuns.some(r => wallY >= r.start && wallY <= r.end)
|
||||||
|
const hasCornerJoin = ewCoversCorner && nsCoversCorner
|
||||||
|
|
||||||
|
// Stamp East-West stone 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`)
|
||||||
|
} else if (x === run.start) {
|
||||||
|
placeWall(x, wallY, makeWall(2, 1)) // West / Right-of-gap End-Cap (`s2.q1.t2`)
|
||||||
|
} else if (x === run.end) {
|
||||||
|
placeWall(x, wallY, makeWall(2, 2)) // East / Left-of-gap End-Cap (`s2.q2.t2`)
|
||||||
|
} else {
|
||||||
|
placeWall(x, wallY, makeWall(2, 0)) // Continuous EW Stone Wall (`s2.q0.t2`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stamp North-South stone 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`)
|
||||||
|
} else if (y === run.start) {
|
||||||
|
placeWall(wallX, y, makeWall(1, 1)) // North / Bottom-of-gap End-Cap (`s2.q1.t1`)
|
||||||
|
} else if (y === run.end) {
|
||||||
|
placeWall(wallX, y, makeWall(1, 2)) // South / Top-of-gap End-Cap (`s2.q2.t1`)
|
||||||
|
} else {
|
||||||
|
placeWall(wallX, y, makeWall(1, 0)) // Continuous NS Stone Wall (`s2.q0.t1`)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
prevPlacedNS = true
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -2034,15 +2248,25 @@ export function generateWilderness(request: WildernessRequest): WildernessResult
|
||||||
specialPresetNames = SPECIAL_PRESETS_BY_LEVEL[request.levelId] ?? []
|
specialPresetNames = SPECIAL_PRESETS_BY_LEVEL[request.levelId] ?? []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const placedSpecialPieces = spawnSpecialPresets(canvas, fillPieces, specialPresetNames, gridWidth, gridHeight, rng, stats)
|
const inX = Math.floor(gridWidth / 2)
|
||||||
|
const outY = Math.floor(gridHeight / 2)
|
||||||
// For Act 1 outdoor wilderness maps (LevelId 2..7), lay dirt road networks and
|
const reservedRoadBlocks = new Set<string>()
|
||||||
// interior stone wall divider lines across the meadow.
|
|
||||||
if (request.levelId >= 2 && request.levelId <= 7) {
|
if (request.levelId >= 2 && request.levelId <= 7) {
|
||||||
stampAct1DirtRoads(canvas, rng)
|
for (let by = 1; by < gridHeight - 1; by += 1) reservedRoadBlocks.add(`${String(inX)},${String(by)}`)
|
||||||
stampAct1InteriorStoneWalls(canvas, gridWidth, gridHeight, rng)
|
for (let bx = 1; bx < gridWidth - 1; bx += 1) reservedRoadBlocks.add(`${String(bx)},${String(outY)}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const { placedPieces: placedSpecialPieces, placedRects } = spawnSpecialPresets(
|
||||||
|
canvas,
|
||||||
|
fillPieces,
|
||||||
|
specialPresetNames,
|
||||||
|
gridWidth,
|
||||||
|
gridHeight,
|
||||||
|
rng,
|
||||||
|
stats,
|
||||||
|
reservedRoadBlocks,
|
||||||
|
)
|
||||||
|
|
||||||
const themeIndex = Math.max(0, Math.min(4, Math.floor(request.subTheme)))
|
const themeIndex = Math.max(0, Math.min(4, Math.floor(request.subTheme)))
|
||||||
const objectRows = request.substitutions.filter(row => classifySubstitutionRole(row.name, row.type) !== 'border')
|
const objectRows = request.substitutions.filter(row => classifySubstitutionRole(row.name, row.type) !== 'border')
|
||||||
const borderRows = [...request.substitutions, ...(request.shrineSubstitutions ?? [])]
|
const borderRows = [...request.substitutions, ...(request.shrineSubstitutions ?? [])]
|
||||||
|
|
@ -2058,6 +2282,14 @@ export function generateWilderness(request: WildernessRequest): WildernessResult
|
||||||
rng,
|
rng,
|
||||||
stats,
|
stats,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// 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, placedRects, rng)
|
||||||
|
stampAct1InteriorStoneWalls(canvas, gridWidth, gridHeight, inX, outY, placedRects, rng)
|
||||||
|
}
|
||||||
|
|
||||||
for (const piece of fillPieces) {
|
for (const piece of fillPieces) {
|
||||||
if (!placedSpecialPieces.has(piece.name)) {
|
if (!placedSpecialPieces.has(piece.name)) {
|
||||||
stats.notes.push(`fill piece "${piece.name}" is not placed by this port`)
|
stats.notes.push(`fill piece "${piece.name}" is not placed by this port`)
|
||||||
|
|
|
||||||
|
|
@ -325,4 +325,71 @@ describe("Wilderness Features & Border Rules", () => {
|
||||||
expect(result.stats.substitutedClusters).toBe(6)
|
expect(result.stats.substitutedClusters).toBe(6)
|
||||||
expect(result.level.objects.length).toBe(6)
|
expect(result.level.objects.length).toBe(6)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test("Act 1 Blood Moor (Level 2) connects South/East border gates with 3-tile road and uses authentic stone wall sequences", () => {
|
||||||
|
const borderDs1 = makeMockDs1(8, 8, false)
|
||||||
|
const openBorderDs1 = makeMockDs1(8, 8, true)
|
||||||
|
const presetDs1 = makeMockDs1(16, 16, false)
|
||||||
|
const riverDs1 = makeMockDs1(8, 8, false)
|
||||||
|
|
||||||
|
const pieces: WildernessPiece[] = [
|
||||||
|
{ name: "Act 1 - Wild Border 1", border: true, levels: [borderDs1, borderDs1, borderDs1, openBorderDs1] },
|
||||||
|
{ name: "Act 1 - Wild Border 2", border: true, levels: [borderDs1] },
|
||||||
|
{ name: "Act 1 - Wild Border 3", border: true, levels: [borderDs1] },
|
||||||
|
{ name: "Act 1 - Wild Border 4", border: true, levels: [borderDs1, borderDs1, borderDs1, openBorderDs1] },
|
||||||
|
{ name: "Act 1 - Wild Border 5", border: true, levels: [borderDs1] },
|
||||||
|
{ name: "Act 1 - Wild Border 6", border: true, levels: [borderDs1] },
|
||||||
|
{ name: "Act 1 - Wild Border 7", border: true, levels: [borderDs1] },
|
||||||
|
{ name: "Act 1 - Wild Border 8", border: true, levels: [borderDs1] },
|
||||||
|
{ name: "Act 1 - DOE Entrance", border: false, levels: [presetDs1] },
|
||||||
|
{ name: "Act 1 - River Upper", border: false, levels: [riverDs1, riverDs1, riverDs1] },
|
||||||
|
{ name: "Act 1 - River Lower", border: false, levels: [riverDs1, riverDs1, riverDs1] },
|
||||||
|
{ name: "Act 1 - Bridge", border: false, levels: [riverDs1, riverDs1] },
|
||||||
|
{ name: "Act 1 - Cottages 1", border: false, levels: [presetDs1] },
|
||||||
|
]
|
||||||
|
|
||||||
|
const result = generateWilderness({
|
||||||
|
levelId: 2,
|
||||||
|
levelName: "Blood Moor",
|
||||||
|
levelTypeName: "Act 1 - Wilderness",
|
||||||
|
sizeX: 80,
|
||||||
|
sizeY: 80,
|
||||||
|
subType: -1,
|
||||||
|
subTheme: -1,
|
||||||
|
seed: 1234,
|
||||||
|
pieces,
|
||||||
|
substitutions: [],
|
||||||
|
specialPresets: [
|
||||||
|
"Act 1 - DOE Entrance",
|
||||||
|
"Act 1 - River Upper",
|
||||||
|
"Act 1 - River Lower",
|
||||||
|
"Act 1 - Bridge",
|
||||||
|
"Act 1 - Cottages 1",
|
||||||
|
],
|
||||||
|
})
|
||||||
|
|
||||||
|
// 1. Verify 3-tile wide main highway at central crossroads (44, 44)
|
||||||
|
expect(result.level.cells[44]![44]!.floors[0]!.style).toBe(5)
|
||||||
|
expect(result.level.cells[44]![43]!.floors[0]!.style).toBe(5)
|
||||||
|
expect(result.level.cells[44]![45]!.floors[0]!.style).toBe(5)
|
||||||
|
|
||||||
|
// 2. Verify South border gate road connection at (44, 75) and East border gate connection at (75, 44)
|
||||||
|
expect(result.level.cells[75]![44]!.floors[0]!.style).toBe(5)
|
||||||
|
expect(result.level.cells[44]![75]!.floors[0]!.style).toBe(5)
|
||||||
|
|
||||||
|
// 3. Verify interior stone wall segments (style=2, prop1=129) use continuous sequence=0 and end-caps sequence=1/2
|
||||||
|
let foundContinuousEW = false
|
||||||
|
let foundEndCapEW = false
|
||||||
|
for (let y = 10; y < 70; y += 1) {
|
||||||
|
for (let x = 10; x < 70; x += 1) {
|
||||||
|
const wall = result.level.cells[y]![x]!.walls.find(w => w.style === 2 && w.prop1 === 129)
|
||||||
|
if (wall && wall.type === 2) {
|
||||||
|
if (wall.sequence === 0) foundContinuousEW = true
|
||||||
|
if (wall.sequence === 1 || wall.sequence === 2) foundEndCapEW = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
expect(foundContinuousEW).toBe(true)
|
||||||
|
expect(foundEndCapEW).toBe(true)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue