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) {
|
for (const row of tables.lvlprest.rows) {
|
||||||
const name = cell(tables.lvlprest, row, 'Name')
|
const name = cell(tables.lvlprest, row, 'Name')
|
||||||
if (!families.some(family => name.startsWith(family))) continue
|
if (!families.some(family => name.startsWith(family))) continue
|
||||||
if (levelTypeName === 'Act 5 - Barricade' && name.includes('Snow')) continue
|
|
||||||
const levels = await rowDs1s(tables.lvlprest, row)
|
const levels = await rowDs1s(tables.lvlprest, row)
|
||||||
if (levels.length === 0) continue
|
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
|
return pieces
|
||||||
})()
|
})()
|
||||||
|
|
@ -975,9 +974,10 @@ async function bakeDs1Variant(
|
||||||
}
|
}
|
||||||
|
|
||||||
const filterLevel = process.env.FILTER_LEVEL
|
const filterLevel = process.env.FILTER_LEVEL
|
||||||
|
const filterLevels = filterLevel !== undefined ? new Set(filterLevel.split(',').map(s => s.trim())) : null
|
||||||
|
|
||||||
for (const entry of LEVELS) {
|
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 levelRow = tables.levels.rows.find(candidate => Number(cell(tables.levels, candidate, 'Id')) === entry.levelId)!
|
||||||
const paletteIndex = Number(cell(tables.levels, levelRow, 'Pal'))
|
const paletteIndex = Number(cell(tables.levels, levelRow, 'Pal'))
|
||||||
|
|
@ -1109,7 +1109,9 @@ if (filterLevel !== undefined) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
await writeFile(indexPath, JSON.stringify(index, null, 1))
|
await writeFile(indexPath, JSON.stringify(index, null, 1))
|
||||||
const { bakeEntities } = await import('./pack-entity-assets.ts')
|
if (!process.env.SKIP_ENTITIES) {
|
||||||
await bakeEntities(archiveDir, outDir)
|
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(`\n打包完成:${String(totalLevels)} 张地图,PNG 合计 ${(totalPngBytes / 1048576).toFixed(1)} MB,输出 ${outDir}`)
|
||||||
console.log(`Skipped artless spawns: ${skippedArtlessSpawns}, Skipped no-art NPCs: ${skippedMissingArtSpawns}`)
|
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[]>> = {
|
export const SPECIAL_PRESETS_BY_LEVEL: Readonly<Record<number, readonly string[]>> = {
|
||||||
2: [
|
2: [
|
||||||
|
'Act 1 - DOE Entrance',
|
||||||
'Act 1 - River Upper',
|
'Act 1 - River Upper',
|
||||||
'Act 1 - River Lower',
|
'Act 1 - River Lower',
|
||||||
'Act 1 - Bridge',
|
'Act 1 - Bridge',
|
||||||
|
'Act 1 - Fence Fill 1',
|
||||||
|
'Act 1 - Fence Fill 3',
|
||||||
|
'Act 1 - Ruin',
|
||||||
'Act 1 - Cottages 1',
|
'Act 1 - Cottages 1',
|
||||||
'Act 1 - Corral Fill',
|
'Act 1 - Corral Fill',
|
||||||
'Act 1 - Fence Fill 4',
|
'Act 1 - Fence Fill 4',
|
||||||
|
'Act 1 - Fence Fill 5',
|
||||||
'Act 1 - Stone Fill 1',
|
'Act 1 - Stone Fill 1',
|
||||||
'Act 1 - Stone Fill 2',
|
'Act 1 - Stone Fill 2',
|
||||||
],
|
],
|
||||||
3: [
|
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 2',
|
||||||
'Act 1 - Cottages 3',
|
'Act 1 - Cottages 3',
|
||||||
'Act 1 - Fallen Camp Bishibosh',
|
|
||||||
'Act 1 - Corral Fill',
|
'Act 1 - Corral Fill',
|
||||||
'Act 1 - Fence Fill 4',
|
'Act 1 - Fence Fill 4',
|
||||||
|
'Act 1 - Fence Fill 6',
|
||||||
'Act 1 - Stone Fill 1',
|
'Act 1 - Stone Fill 1',
|
||||||
'Act 1 - Stone Fill 2',
|
'Act 1 - Stone Fill 2',
|
||||||
],
|
],
|
||||||
4: [
|
4: [
|
||||||
'Act 1 - Cairn Stones',
|
'Act 1 - Cairn Stones',
|
||||||
'Act 1 - Camp',
|
'Act 1 - Cave Entrance',
|
||||||
'Act 1 - Tower Tome',
|
'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 1',
|
||||||
'Act 1 - Cottages 3',
|
'Act 1 - Cottages 3',
|
||||||
'Act 1 - Fallen Camp 1',
|
'Act 1 - Fallen Camp 1',
|
||||||
'Act 1 - Corral Fill',
|
'Act 1 - Corral Fill',
|
||||||
'Act 1 - Fence Fill 4',
|
'Act 1 - Fence Fill 4',
|
||||||
|
'Act 1 - Fence Fill 5',
|
||||||
],
|
],
|
||||||
5: [
|
5: [
|
||||||
'Act 1 - Inifus',
|
'Act 1 - Inifus',
|
||||||
|
'Act 1 - Cave Entrance',
|
||||||
'Act 1 - Ruin',
|
'Act 1 - Ruin',
|
||||||
|
'Act 1 - Fence Fill 1',
|
||||||
|
'Act 1 - Fence Fill 2',
|
||||||
|
'Act 1 - Fence Fill 3',
|
||||||
'Act 1 - Tree Fill',
|
'Act 1 - Tree Fill',
|
||||||
'Act 1 - Cottages 2',
|
'Act 1 - Cottages 2',
|
||||||
'Act 1 - Cottages 3',
|
'Act 1 - Cottages 3',
|
||||||
'Act 1 - Fallen Camp 2',
|
'Act 1 - Fallen Camp 2',
|
||||||
|
'Act 1 - Fence Fill 5',
|
||||||
'Act 1 - Stone Fill 1',
|
'Act 1 - Stone Fill 1',
|
||||||
'Act 1 - Stone Fill 2',
|
'Act 1 - Stone Fill 2',
|
||||||
],
|
],
|
||||||
6: [
|
6: [
|
||||||
'Act 1 - Tower 1',
|
'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 1',
|
||||||
'Act 1 - Swamp Fill 2',
|
'Act 1 - Swamp Fill 2',
|
||||||
'Act 1 - Cottages 1',
|
'Act 1 - Cottages 1',
|
||||||
'Act 1 - Cottages 3',
|
'Act 1 - Cottages 3',
|
||||||
'Act 1 - Fallen Camp 1',
|
'Act 1 - Fallen Camp 1',
|
||||||
|
'Act 1 - Fence Fill 6',
|
||||||
'Act 1 - Stone Fill 1',
|
'Act 1 - Stone Fill 1',
|
||||||
'Act 1 - Stone Fill 2',
|
'Act 1 - Stone Fill 2',
|
||||||
],
|
],
|
||||||
7: [
|
7: [
|
||||||
|
'Act 1 - Fence Fill 1',
|
||||||
|
'Act 1 - Fence Fill 3',
|
||||||
|
'Act 1 - Ruin',
|
||||||
'Act 1 - Cottages 2',
|
'Act 1 - Cottages 2',
|
||||||
'Act 1 - Fallen Camp 2',
|
'Act 1 - Fallen Camp 2',
|
||||||
'Act 1 - Corral Fill',
|
'Act 1 - Corral Fill',
|
||||||
'Act 1 - Fence Fill 4',
|
'Act 1 - Fence Fill 4',
|
||||||
|
'Act 1 - Fence Fill 5',
|
||||||
],
|
],
|
||||||
17: [
|
17: [
|
||||||
'Act 1 - Graveyard',
|
'Act 1 - Graveyard',
|
||||||
],
|
],
|
||||||
39: [
|
39: [
|
||||||
'Act 1 - Bivouac',
|
'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 - Corral Fill',
|
'Act 1 - Corral Fill',
|
||||||
'Act 1 - Fence Fill 4',
|
'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 1',
|
||||||
'Act 1 - Stone Fill 2',
|
'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
|
* level, because an empty map silently baked into an asset pack is worse than a
|
||||||
* crash.
|
* 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 {
|
export function generateWilderness(request: WildernessRequest): WildernessResult {
|
||||||
const where = `level ${String(request.levelId)} (${request.levelName})`
|
const where = `level ${String(request.levelId)} (${request.levelName})`
|
||||||
if (request.pieces.length === 0) throw new Error(`${where}: no LvlPrest pieces for this level type`)
|
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)
|
stats.groundCells = fillGround(canvas, groundTile, 0, 0, canvas.width, canvas.height)
|
||||||
|
|
||||||
const borderPieces = request.pieces.filter(piece => piece.border)
|
// Special case: Act 5 Bloody Foothills (LevelId 110 / Act 5 - Siege 1) is assembled
|
||||||
const fillPieces = request.pieces.filter(piece => !piece.border)
|
// 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)
|
layBorder(canvas, borderPieces, gridWidth, gridHeight, rng, stats, request.levelId)
|
||||||
|
|
||||||
let specialPresetNames = request.specialPresets
|
let specialPresetNames = request.specialPresets
|
||||||
|
|
@ -1432,20 +1772,30 @@ export function generateWilderness(request: WildernessRequest): WildernessResult
|
||||||
const hasRiver = rng.int(0, 1) === 0
|
const hasRiver = rng.int(0, 1) === 0
|
||||||
specialPresetNames = hasRiver
|
specialPresetNames = hasRiver
|
||||||
? [
|
? [
|
||||||
|
'Act 1 - DOE Entrance',
|
||||||
'Act 1 - River Upper',
|
'Act 1 - River Upper',
|
||||||
'Act 1 - River Lower',
|
'Act 1 - River Lower',
|
||||||
'Act 1 - Bridge',
|
'Act 1 - Bridge',
|
||||||
|
'Act 1 - Fence Fill 1',
|
||||||
|
'Act 1 - Fence Fill 3',
|
||||||
|
'Act 1 - Ruin',
|
||||||
'Act 1 - Cottages 1',
|
'Act 1 - Cottages 1',
|
||||||
'Act 1 - Corral Fill',
|
'Act 1 - Corral Fill',
|
||||||
'Act 1 - Fence Fill 4',
|
'Act 1 - Fence Fill 4',
|
||||||
|
'Act 1 - Fence Fill 5',
|
||||||
'Act 1 - Stone Fill 1',
|
'Act 1 - Stone Fill 1',
|
||||||
'Act 1 - Stone Fill 2',
|
'Act 1 - Stone Fill 2',
|
||||||
]
|
]
|
||||||
: [
|
: [
|
||||||
|
'Act 1 - DOE Entrance',
|
||||||
'Act 1 - Pond',
|
'Act 1 - Pond',
|
||||||
|
'Act 1 - Fence Fill 1',
|
||||||
|
'Act 1 - Fence Fill 3',
|
||||||
|
'Act 1 - Ruin',
|
||||||
'Act 1 - Cottages 1',
|
'Act 1 - Cottages 1',
|
||||||
'Act 1 - Corral Fill',
|
'Act 1 - Corral Fill',
|
||||||
'Act 1 - Fence Fill 4',
|
'Act 1 - Fence Fill 4',
|
||||||
|
'Act 1 - Fence Fill 5',
|
||||||
'Act 1 - Stone Fill 1',
|
'Act 1 - Stone Fill 1',
|
||||||
'Act 1 - Stone Fill 2',
|
'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)
|
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 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 ?? [])]
|
||||||
|
|
|
||||||
|
|
@ -134,4 +134,120 @@ describe("Wilderness Features & Border Rules", () => {
|
||||||
expect(result.level.width).toBe(80)
|
expect(result.level.width).toBe(80)
|
||||||
expect(result.level.height).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