fix(map-gen): assemble Act 5 Bloody Foothills 15 siege strips, Barricade fortifications & Act 1 stone walls/dirt roads (Issue #35)
This commit is contained in:
parent
dcd3da0a22
commit
25ae7289ee
|
|
@ -589,10 +589,9 @@ function getWildernessPieces(levelTypeName: string): Promise<WildernessPiece[]>
|
|||
for (const row of tables.lvlprest.rows) {
|
||||
const name = cell(tables.lvlprest, row, 'Name')
|
||||
if (!families.some(family => name.startsWith(family))) continue
|
||||
if (levelTypeName === 'Act 5 - Barricade' && name.includes('Snow')) continue
|
||||
const levels = await rowDs1s(tables.lvlprest, row)
|
||||
if (levels.length === 0) continue
|
||||
pieces.push({ name, levels, border: /border|cliff/i.test(name) })
|
||||
pieces.push({ name, levels, border: /border|cliff|ravine/i.test(name) })
|
||||
}
|
||||
return pieces
|
||||
})()
|
||||
|
|
@ -975,9 +974,10 @@ async function bakeDs1Variant(
|
|||
}
|
||||
|
||||
const filterLevel = process.env.FILTER_LEVEL
|
||||
const filterLevels = filterLevel !== undefined ? new Set(filterLevel.split(',').map(s => s.trim())) : null
|
||||
|
||||
for (const entry of LEVELS) {
|
||||
if (filterLevel !== undefined && String(entry.levelId) !== filterLevel) continue
|
||||
if (filterLevels !== null && !filterLevels.has(String(entry.levelId))) continue
|
||||
|
||||
const levelRow = tables.levels.rows.find(candidate => Number(cell(tables.levels, candidate, 'Id')) === entry.levelId)!
|
||||
const paletteIndex = Number(cell(tables.levels, levelRow, 'Pal'))
|
||||
|
|
@ -1109,7 +1109,9 @@ if (filterLevel !== undefined) {
|
|||
}
|
||||
}
|
||||
await writeFile(indexPath, JSON.stringify(index, null, 1))
|
||||
const { bakeEntities } = await import('./pack-entity-assets.ts')
|
||||
await bakeEntities(archiveDir, outDir)
|
||||
if (!process.env.SKIP_ENTITIES) {
|
||||
const { bakeEntities } = await import('./pack-entity-assets.ts')
|
||||
await bakeEntities(archiveDir, outDir)
|
||||
}
|
||||
console.log(`\n打包完成:${String(totalLevels)} 张地图,PNG 合计 ${(totalPngBytes / 1048576).toFixed(1)} MB,输出 ${outDir}`)
|
||||
console.log(`Skipped artless spawns: ${skippedArtlessSpawns}, Skipped no-art NPCs: ${skippedMissingArtSpawns}`)
|
||||
|
|
|
|||
|
|
@ -1087,65 +1087,98 @@ function applySubstitutions(
|
|||
*/
|
||||
export const SPECIAL_PRESETS_BY_LEVEL: Readonly<Record<number, readonly string[]>> = {
|
||||
2: [
|
||||
'Act 1 - DOE Entrance',
|
||||
'Act 1 - River Upper',
|
||||
'Act 1 - River Lower',
|
||||
'Act 1 - Bridge',
|
||||
'Act 1 - Fence Fill 1',
|
||||
'Act 1 - Fence Fill 3',
|
||||
'Act 1 - Ruin',
|
||||
'Act 1 - Cottages 1',
|
||||
'Act 1 - Corral Fill',
|
||||
'Act 1 - Fence Fill 4',
|
||||
'Act 1 - Fence Fill 5',
|
||||
'Act 1 - Stone Fill 1',
|
||||
'Act 1 - Stone Fill 2',
|
||||
],
|
||||
3: [
|
||||
'Act 1 - Cave Entrance',
|
||||
'Act 1 - Fallen Camp Bishibosh',
|
||||
'Act 1 - Fence Fill 1',
|
||||
'Act 1 - Fence Fill 2',
|
||||
'Act 1 - Fence Fill 3',
|
||||
'Act 1 - Ruin',
|
||||
'Act 1 - Cottages 2',
|
||||
'Act 1 - Cottages 3',
|
||||
'Act 1 - Fallen Camp Bishibosh',
|
||||
'Act 1 - Corral Fill',
|
||||
'Act 1 - Fence Fill 4',
|
||||
'Act 1 - Fence Fill 6',
|
||||
'Act 1 - Stone Fill 1',
|
||||
'Act 1 - Stone Fill 2',
|
||||
],
|
||||
4: [
|
||||
'Act 1 - Cairn Stones',
|
||||
'Act 1 - Camp',
|
||||
'Act 1 - Cave Entrance',
|
||||
'Act 1 - Tower Tome',
|
||||
'Act 1 - Fence Fill 1',
|
||||
'Act 1 - Fence Fill 3',
|
||||
'Act 1 - Ruin',
|
||||
'Act 1 - Camp',
|
||||
'Act 1 - Cottages 1',
|
||||
'Act 1 - Cottages 3',
|
||||
'Act 1 - Fallen Camp 1',
|
||||
'Act 1 - Corral Fill',
|
||||
'Act 1 - Fence Fill 4',
|
||||
'Act 1 - Fence Fill 5',
|
||||
],
|
||||
5: [
|
||||
'Act 1 - Inifus',
|
||||
'Act 1 - Cave Entrance',
|
||||
'Act 1 - Ruin',
|
||||
'Act 1 - Fence Fill 1',
|
||||
'Act 1 - Fence Fill 2',
|
||||
'Act 1 - Fence Fill 3',
|
||||
'Act 1 - Tree Fill',
|
||||
'Act 1 - Cottages 2',
|
||||
'Act 1 - Cottages 3',
|
||||
'Act 1 - Fallen Camp 2',
|
||||
'Act 1 - Fence Fill 5',
|
||||
'Act 1 - Stone Fill 1',
|
||||
'Act 1 - Stone Fill 2',
|
||||
],
|
||||
6: [
|
||||
'Act 1 - Tower 1',
|
||||
'Act 1 - Cave Entrance',
|
||||
'Act 1 - Fence Fill 1',
|
||||
'Act 1 - Fence Fill 3',
|
||||
'Act 1 - Ruin',
|
||||
'Act 1 - Swamp Fill 1',
|
||||
'Act 1 - Swamp Fill 2',
|
||||
'Act 1 - Cottages 1',
|
||||
'Act 1 - Cottages 3',
|
||||
'Act 1 - Fallen Camp 1',
|
||||
'Act 1 - Fence Fill 6',
|
||||
'Act 1 - Stone Fill 1',
|
||||
'Act 1 - Stone Fill 2',
|
||||
],
|
||||
7: [
|
||||
'Act 1 - Fence Fill 1',
|
||||
'Act 1 - Fence Fill 3',
|
||||
'Act 1 - Ruin',
|
||||
'Act 1 - Cottages 2',
|
||||
'Act 1 - Fallen Camp 2',
|
||||
'Act 1 - Corral Fill',
|
||||
'Act 1 - Fence Fill 4',
|
||||
'Act 1 - Fence Fill 5',
|
||||
],
|
||||
17: [
|
||||
'Act 1 - Graveyard',
|
||||
],
|
||||
39: [
|
||||
'Act 1 - Bivouac',
|
||||
'Act 1 - Fence Fill 1',
|
||||
'Act 1 - Fence Fill 3',
|
||||
'Act 1 - Ruin',
|
||||
'Act 1 - Corral Fill',
|
||||
'Act 1 - Corral Fill',
|
||||
'Act 1 - Fence Fill 4',
|
||||
|
|
@ -1156,6 +1189,34 @@ export const SPECIAL_PRESETS_BY_LEVEL: Readonly<Record<number, readonly string[]
|
|||
'Act 1 - Stone Fill 1',
|
||||
'Act 1 - Stone Fill 2',
|
||||
],
|
||||
111: [
|
||||
'Act 5 - Barricade Waypoint Dirt',
|
||||
'Act 5 - Barricade Hell Portal N',
|
||||
'Act 5 - Barricade Prison 1',
|
||||
'Act 5 - Barricade Prison 2',
|
||||
'Act 5 - Barricade Prison 3',
|
||||
'Act 5 - Barricade 1',
|
||||
'Act 5 - Barricade 2',
|
||||
'Act 5 - Barricade 3',
|
||||
'Act 5 - Barricade 4',
|
||||
'Act 5 - Barricade Building',
|
||||
'Act 5 - Barricade Filler Treasure',
|
||||
'Act 5 - Barricade Ruins N',
|
||||
'Act 5 - Barricade Ruins W',
|
||||
],
|
||||
112: [
|
||||
'Act 5 - Barricade Waypoint Snow',
|
||||
'Act 5 - Barricade Hell Portal N',
|
||||
'Act 5 - Barricade Snow Unique',
|
||||
'Act 5 - Barricade 1 Snow',
|
||||
'Act 5 - Barricade 2 Snow',
|
||||
'Act 5 - Barricade 3 Snow',
|
||||
'Act 5 - Barricade 4 Snow',
|
||||
'Act 5 - Barricade Snow Lake 1',
|
||||
'Act 5 - Barricade Snow Lake 2',
|
||||
'Act 5 - Barricade Ruins N Treasure',
|
||||
'Act 5 - Barricade Ruins W Treasure',
|
||||
],
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -1316,6 +1377,220 @@ function resolveUnsetSize(request: WildernessRequest): { sizeX: number; sizeY: n
|
|||
* 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).
|
||||
*
|
||||
* 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(
|
||||
canvas: Canvas,
|
||||
pieces: readonly WildernessPiece[],
|
||||
stats: WildernessStats,
|
||||
): Set<string> {
|
||||
const stripOrder = [
|
||||
'Act 5 - Siege To Barricade',
|
||||
'Act 5 - Siege Strip 13',
|
||||
'Act 5 - Siege Strip 12',
|
||||
'Act 5 - Siege Strip 11',
|
||||
'Act 5 - Siege Strip 10',
|
||||
'Act 5 - Siege Strip 9',
|
||||
'Act 5 - Siege Strip 8',
|
||||
'Act 5 - Siege Strip 7',
|
||||
'Act 5 - Siege Strip 6',
|
||||
'Act 5 - Siege Strip 5',
|
||||
'Act 5 - Siege Strip 4',
|
||||
'Act 5 - Siege Strip 3',
|
||||
'Act 5 - Siege Strip 2',
|
||||
'Act 5 - Siege Strip 1',
|
||||
'Act 5 - Siege To Town',
|
||||
]
|
||||
const placed = new Set<string>()
|
||||
for (let i = 0; i < stripOrder.length; i += 1) {
|
||||
const name = stripOrder[i]!
|
||||
const piece = pieces.find(p => p.name === name)
|
||||
const ds1 = piece?.levels[0]
|
||||
if (!ds1) continue
|
||||
stampDs1(canvas, ds1, i * 16, 0)
|
||||
placed.add(name)
|
||||
stats.substitutions.push({ name, role: 'object', enabled: true, clusters: 1 })
|
||||
}
|
||||
return placed
|
||||
}
|
||||
|
||||
/**
|
||||
* Lay a 2-tile-wide meandering dirt road network connecting the South entrance, East exit,
|
||||
* and interior landmarks in Act 1 wilderness levels (LevelId 2..7).
|
||||
*/
|
||||
function stampAct1DirtRoads(canvas: Canvas, rng: Rng): void {
|
||||
const roadFloor: Ds1Floor = {
|
||||
prop1: 194,
|
||||
sequence: 0,
|
||||
style: 5,
|
||||
unknown1: 0,
|
||||
unknown2: 0,
|
||||
hidden: false,
|
||||
}
|
||||
|
||||
const paintRoadCell = (cx: number, cy: number): void => {
|
||||
for (let dy = 0; dy <= 1; dy += 1) {
|
||||
for (let dx = 0; dx <= 1; dx += 1) {
|
||||
const x = cx + dx
|
||||
const y = cy + dy
|
||||
if (x < 8 || x >= canvas.width - 8 || y < 8 || y >= canvas.height - 8) continue
|
||||
const cell = canvas.cells[y]?.[x]
|
||||
if (!cell) continue
|
||||
// Do not overwrite cells that already carry walls or water/bridge floors
|
||||
if (cell.walls.some(w => !w.hidden && w.prop1 !== 0)) continue
|
||||
const existingFloor = cell.floors[0]
|
||||
if (existingFloor && existingFloor.style !== 0) continue
|
||||
;(cell.floors as Ds1Floor[])[0] = roadFloor
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const connectPoints = (x0: number, y0: number, x1: number, y1: number): void => {
|
||||
let curX = x0
|
||||
let curY = y0
|
||||
let steps = 0
|
||||
const maxSteps = canvas.width + canvas.height + 40
|
||||
while ((Math.abs(curX - x1) > 1 || Math.abs(curY - y1) > 1) && steps < maxSteps) {
|
||||
paintRoadCell(curX, curY)
|
||||
const dx = x1 - curX
|
||||
const dy = y1 - curY
|
||||
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
|
||||
}
|
||||
steps += 1
|
||||
}
|
||||
paintRoadCell(x1, y1)
|
||||
}
|
||||
|
||||
const hubX = Math.floor(canvas.width / 2) + rng.int(-5, 5)
|
||||
const hubY = Math.floor(canvas.height / 2) + rng.int(-5, 5)
|
||||
const southX = Math.floor(canvas.width / 2)
|
||||
const southY = canvas.height - 9
|
||||
const eastX = canvas.width - 9
|
||||
const eastY = Math.floor(canvas.height / 2)
|
||||
const nwX = Math.floor(canvas.width * 0.28)
|
||||
const nwY = Math.floor(canvas.height * 0.28)
|
||||
const swX = Math.floor(canvas.width * 0.28)
|
||||
const swY = Math.floor(canvas.height * 0.72)
|
||||
|
||||
connectPoints(southX, southY, hubX, hubY)
|
||||
connectPoints(hubX, hubY, eastX, eastY)
|
||||
connectPoints(hubX, hubY, nwX, nwY)
|
||||
connectPoints(hubX, hubY, swX, swY)
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
* archway gate openings where roads pass through.
|
||||
*/
|
||||
function stampAct1InteriorStoneWalls(canvas: Canvas, gridWidth: number, gridHeight: number, rng: Rng): void {
|
||||
const ewWall = (seq: number): Ds1Wall => ({
|
||||
prop1: 129,
|
||||
sequence: seq % 3,
|
||||
style: 2,
|
||||
type: 2,
|
||||
unknown1: 0,
|
||||
unknown2: 0,
|
||||
hidden: false,
|
||||
})
|
||||
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 => {
|
||||
if (x < 10 || x >= canvas.width - 10 || y < 10 || y >= canvas.height - 10) return false
|
||||
const cell = canvas.cells[y]?.[x]
|
||||
if (!cell) return false
|
||||
if (cell.walls.some(w => !w.hidden && w.prop1 !== 0)) return false
|
||||
// Leave gate openings wherever a dirt road (`style === 5`) or water crosses
|
||||
if (cell.floors.some(f => !f.hidden && f.style !== 0)) return false
|
||||
return true
|
||||
}
|
||||
|
||||
const placeWall = (x: number, y: number, wall: Ds1Wall): void => {
|
||||
const cell = canvas.cells[y]?.[x]
|
||||
if (!cell) return
|
||||
const targetWalls = cell.walls as Ds1Wall[]
|
||||
if (targetWalls.length > 0) {
|
||||
targetWalls[0] = wall
|
||||
} else {
|
||||
targetWalls.push(wall)
|
||||
}
|
||||
}
|
||||
|
||||
// Primary horizontal (East-West) stone wall divider near the middle of the map
|
||||
const midBlockY = Math.floor(gridHeight / 2) + (rng.int(0, 1) === 0 ? -1 : 1)
|
||||
const wallY = Math.max(16, Math.min(canvas.height - 16, midBlockY * TILES_PER_BLOCK))
|
||||
|
||||
// Primary vertical (North-South) stone wall divider
|
||||
const midBlockX = Math.floor(gridWidth / 2) + (rng.int(0, 1) === 0 ? -1 : 1)
|
||||
const wallX = Math.max(16, Math.min(canvas.width - 16, midBlockX * TILES_PER_BLOCK))
|
||||
|
||||
// Draw East-West stone wall line with periodic gate gaps
|
||||
let prevPlacedEW = false
|
||||
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) {
|
||||
placeWall(x, wallY, cornerPillar)
|
||||
} else {
|
||||
placeWall(x, wallY, ewWall(x))
|
||||
}
|
||||
prevPlacedEW = true
|
||||
}
|
||||
|
||||
// Draw North-South stone wall line with periodic gate gaps
|
||||
let prevPlacedNS = false
|
||||
for (let y = 11; y < canvas.height - 11; y += 1) {
|
||||
const isGate = (y % 18) >= 7 && (y % 18) <= 10
|
||||
if (isGate || Math.abs(y - wallY) <= 1 || !canPlaceWall(wallX, y)) {
|
||||
if (prevPlacedNS && canPlaceWall(wallX, y - 1)) {
|
||||
placeWall(wallX, y - 1, cornerPillar)
|
||||
}
|
||||
prevPlacedNS = false
|
||||
continue
|
||||
}
|
||||
if (!prevPlacedNS) {
|
||||
placeWall(wallX, y, cornerPillar)
|
||||
} else {
|
||||
placeWall(wallX, y, nsWall(y))
|
||||
}
|
||||
prevPlacedNS = true
|
||||
}
|
||||
}
|
||||
|
||||
export function generateWilderness(request: WildernessRequest): WildernessResult {
|
||||
const where = `level ${String(request.levelId)} (${request.levelName})`
|
||||
if (request.pieces.length === 0) throw new Error(`${where}: no LvlPrest pieces for this level type`)
|
||||
|
|
@ -1421,8 +1696,73 @@ export function generateWilderness(request: WildernessRequest): WildernessResult
|
|||
}
|
||||
stats.groundCells = fillGround(canvas, groundTile, 0, 0, canvas.width, canvas.height)
|
||||
|
||||
const borderPieces = request.pieces.filter(piece => piece.border)
|
||||
const fillPieces = request.pieces.filter(piece => !piece.border)
|
||||
// Special case: Act 5 Bloody Foothills (LevelId 110 / Act 5 - Siege 1) is assembled
|
||||
// from 15 sequential 16x48 siege strips rather than perimeter borders + random scatter.
|
||||
if (request.levelId === 110 || request.levelTypeName === 'Act 5 - Siege') {
|
||||
const placedSiege = stampAct5SiegeStrips(canvas, request.pieces, stats)
|
||||
let finalGroundCells = 0
|
||||
for (let y = 0; y < canvas.height; y += 1) {
|
||||
const row = canvas.cells[y]
|
||||
if (row === undefined) continue
|
||||
for (let x = 0; x < canvas.width; x += 1) {
|
||||
const c = row[x]
|
||||
if (c !== undefined && c.floors.some(f => !f.hidden && f.prop1 !== 0)) finalGroundCells += 1
|
||||
}
|
||||
}
|
||||
stats.groundCells = finalGroundCells
|
||||
return {
|
||||
level: {
|
||||
version: version === 0 ? 18 : version,
|
||||
width: canvas.width,
|
||||
height: canvas.height,
|
||||
act,
|
||||
substitutionType,
|
||||
wallLayers: canvas.wallLayers,
|
||||
floorLayers: canvas.floorLayers,
|
||||
cells: canvas.cells,
|
||||
objects: canvas.objects,
|
||||
npcPathOffset: null,
|
||||
},
|
||||
stats: {
|
||||
levelId: request.levelId,
|
||||
levelName: request.levelName,
|
||||
levelTypeName: request.levelTypeName,
|
||||
seed: request.seed,
|
||||
sizeX,
|
||||
sizeY,
|
||||
sizeSource,
|
||||
tilesPerBlock: TILES_PER_BLOCK,
|
||||
blockGrid: { width: gridWidth, height: gridHeight },
|
||||
dt1Mask: wildernessDt1Mask(request.levelTypeName),
|
||||
subType: request.subType,
|
||||
subTheme: 0,
|
||||
groundTile,
|
||||
groundCells: stats.groundCells,
|
||||
borderPiecesAvailable: 0,
|
||||
borderBlocks: 0,
|
||||
borderUsage: {},
|
||||
substitutions: stats.substitutions,
|
||||
substitutedClusters: placedSiege.size,
|
||||
objects: canvas.objects.length,
|
||||
cells: canvas.width * canvas.height,
|
||||
unresolved: stats.unresolved,
|
||||
unimplementedPasses: UNIMPLEMENTED_PASSES,
|
||||
notes: stats.notes,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Filter pieces for Act 5 Barricade dirt (111) vs snow (112)
|
||||
let levelPieces = request.pieces
|
||||
if (request.levelId === 111) {
|
||||
levelPieces = request.pieces.filter(p => !/snow/i.test(p.name))
|
||||
} else if (request.levelId === 112) {
|
||||
const snowOnly = request.pieces.filter(p => /snow/i.test(p.name) || /ruins/i.test(p.name))
|
||||
if (snowOnly.length > 0) levelPieces = snowOnly
|
||||
}
|
||||
|
||||
const borderPieces = levelPieces.filter(piece => piece.border)
|
||||
const fillPieces = levelPieces.filter(piece => !piece.border)
|
||||
layBorder(canvas, borderPieces, gridWidth, gridHeight, rng, stats, request.levelId)
|
||||
|
||||
let specialPresetNames = request.specialPresets
|
||||
|
|
@ -1432,20 +1772,30 @@ export function generateWilderness(request: WildernessRequest): WildernessResult
|
|||
const hasRiver = rng.int(0, 1) === 0
|
||||
specialPresetNames = hasRiver
|
||||
? [
|
||||
'Act 1 - DOE Entrance',
|
||||
'Act 1 - River Upper',
|
||||
'Act 1 - River Lower',
|
||||
'Act 1 - Bridge',
|
||||
'Act 1 - Fence Fill 1',
|
||||
'Act 1 - Fence Fill 3',
|
||||
'Act 1 - Ruin',
|
||||
'Act 1 - Cottages 1',
|
||||
'Act 1 - Corral Fill',
|
||||
'Act 1 - Fence Fill 4',
|
||||
'Act 1 - Fence Fill 5',
|
||||
'Act 1 - Stone Fill 1',
|
||||
'Act 1 - Stone Fill 2',
|
||||
]
|
||||
: [
|
||||
'Act 1 - DOE Entrance',
|
||||
'Act 1 - Pond',
|
||||
'Act 1 - Fence Fill 1',
|
||||
'Act 1 - Fence Fill 3',
|
||||
'Act 1 - Ruin',
|
||||
'Act 1 - Cottages 1',
|
||||
'Act 1 - Corral Fill',
|
||||
'Act 1 - Fence Fill 4',
|
||||
'Act 1 - Fence Fill 5',
|
||||
'Act 1 - Stone Fill 1',
|
||||
'Act 1 - Stone Fill 2',
|
||||
]
|
||||
|
|
@ -1455,6 +1805,13 @@ export function generateWilderness(request: WildernessRequest): WildernessResult
|
|||
}
|
||||
const placedSpecialPieces = spawnSpecialPresets(canvas, fillPieces, specialPresetNames, gridWidth, gridHeight, rng, stats)
|
||||
|
||||
// For Act 1 outdoor wilderness maps (LevelId 2..7), lay dirt road networks and
|
||||
// interior stone wall divider lines across the meadow.
|
||||
if (request.levelId >= 2 && request.levelId <= 7) {
|
||||
stampAct1DirtRoads(canvas, rng)
|
||||
stampAct1InteriorStoneWalls(canvas, gridWidth, gridHeight, rng)
|
||||
}
|
||||
|
||||
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 borderRows = [...request.substitutions, ...(request.shrineSubstitutions ?? [])]
|
||||
|
|
|
|||
|
|
@ -134,4 +134,120 @@ describe("Wilderness Features & Border Rules", () => {
|
|||
expect(result.level.width).toBe(80)
|
||||
expect(result.level.height).toBe(80)
|
||||
})
|
||||
|
||||
test("Act 1 Wilderness generates interior stone walls and dirt road network", () => {
|
||||
const closedDs1 = makeMockDs1(8, 8, false)
|
||||
const piece: WildernessPiece = {
|
||||
name: "Act 1 - Wild Border 1",
|
||||
border: true,
|
||||
levels: [closedDs1],
|
||||
}
|
||||
|
||||
const result = generateWilderness({
|
||||
levelId: 3,
|
||||
levelName: "Cold Plains",
|
||||
levelTypeName: "Act 1 - Wilderness",
|
||||
sizeX: 80,
|
||||
sizeY: 80,
|
||||
subType: 6,
|
||||
subTheme: 0,
|
||||
seed: 99999,
|
||||
pieces: [piece],
|
||||
substitutions: [],
|
||||
})
|
||||
|
||||
let stoneWallCount = 0
|
||||
let dirtRoadCount = 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.walls.some(w => w.style === 2 && w.prop1 === 129)) {
|
||||
stoneWallCount += 1
|
||||
}
|
||||
if (cell.floors.some(f => f.style === 5 && f.prop1 === 194)) {
|
||||
dirtRoadCount += 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
expect(stoneWallCount).toBeGreaterThan(20)
|
||||
expect(dirtRoadCount).toBeGreaterThan(30)
|
||||
})
|
||||
|
||||
test("Act 5 Bloody Foothills (Level 110) stamps 15 canonical siege strips in West-to-East order", () => {
|
||||
const stripNames = [
|
||||
"Act 5 - Siege To Barricade",
|
||||
"Act 5 - Siege Strip 13",
|
||||
"Act 5 - Siege Strip 12",
|
||||
"Act 5 - Siege Strip 11",
|
||||
"Act 5 - Siege Strip 10",
|
||||
"Act 5 - Siege Strip 9",
|
||||
"Act 5 - Siege Strip 8",
|
||||
"Act 5 - Siege Strip 7",
|
||||
"Act 5 - Siege Strip 6",
|
||||
"Act 5 - Siege Strip 5",
|
||||
"Act 5 - Siege Strip 4",
|
||||
"Act 5 - Siege Strip 3",
|
||||
"Act 5 - Siege Strip 2",
|
||||
"Act 5 - Siege Strip 1",
|
||||
"Act 5 - Siege To Town",
|
||||
]
|
||||
|
||||
const pieces: WildernessPiece[] = stripNames.map((name, idx) => {
|
||||
const baseDs1 = makeMockDs1(17, 49, false)
|
||||
// Create a copy with tagged floor sequence at (0,0) and one object
|
||||
const cells = baseDs1.cells.map((row, y) =>
|
||||
row.map((cell, x) =>
|
||||
x === 0 && y === 0
|
||||
? {
|
||||
...cell,
|
||||
floors: [{ ...cell.floors[0], sequence: idx + 10 }],
|
||||
}
|
||||
: cell,
|
||||
),
|
||||
)
|
||||
const ds1: Ds1 = {
|
||||
...baseDs1,
|
||||
cells,
|
||||
objects: [
|
||||
{
|
||||
type: 2,
|
||||
id: 100 + idx,
|
||||
x: 5,
|
||||
y: 5,
|
||||
flags: 0,
|
||||
},
|
||||
],
|
||||
}
|
||||
return {
|
||||
name,
|
||||
border: false,
|
||||
levels: [ds1],
|
||||
}
|
||||
})
|
||||
|
||||
const result = generateWilderness({
|
||||
levelId: 110,
|
||||
levelName: "Bloody Foothills",
|
||||
levelTypeName: "Act 5 - Siege",
|
||||
sizeX: 240,
|
||||
sizeY: 48,
|
||||
subType: -1,
|
||||
subTheme: -1,
|
||||
seed: 42,
|
||||
pieces,
|
||||
substitutions: [],
|
||||
})
|
||||
|
||||
expect(result.stats.blockGrid).toEqual({ width: 30, height: 6 })
|
||||
expect(result.stats.substitutedClusters).toBe(15)
|
||||
expect(result.level.width).toBe(240)
|
||||
expect(result.level.height).toBe(48)
|
||||
expect(result.level.objects.length).toBe(15)
|
||||
|
||||
// Verify strip 0 (Siege To Barricade) is at x=0 and strip 14 (Siege To Town) is at x=224
|
||||
expect(result.level.cells[0][0].floors[0].sequence).toBe(10)
|
||||
expect(result.level.cells[0][224].floors[0].sequence).toBe(24)
|
||||
})
|
||||
})
|
||||
|
|
|
|||
Loading…
Reference in New Issue