Merge branch 'fix/issue-62'
refactor(map): 处理凭空发明的程序化地形 pass、消除破坏性预设涂抹与增加种子多样性 1. 删除 wilderness-acts.ts 中从未被主动调用的死代码 generateAct3Jungle 及相关孤儿引入 2. 消除 stampThemedRoads 铺路时破坏性清空墙体(walls.length = 0)与 stampKurastCauseway 覆盖已铺设 Act 3 - Bridge 预设的缺陷,引入预设区域与既有瓦片保护 3. 在 WildernessTelemetry / WildernessResult.stats 中完整追踪程序化道路、分隔墙、水体与障碍物实际绘制瓦片数,杜绝静态假数据 4. 改造血腥丘陵障碍物静态坐标矩阵,接入关卡 PRNG 流实现位置微扰与组件随机化,打破地图固化并保持中央通道畅通 验证:typecheck 零错误;vitest 61 files / 1003 tests passed(基线 61 / 996,新增 7 个测试全绿,测试总数破千);verify-packs 1671 项断言全过。 Closes #62 TAG=agy CONV=2a1934de-30ef-464e-b3f3-fcbcdbbc49f1
This commit is contained in:
commit
c377e8ea01
|
|
@ -398,6 +398,9 @@ export function initAct2OutdoorLevel(
|
||||||
}
|
}
|
||||||
|
|
||||||
stats.act2Topography = { canyonWalls, sandTrailCells, oasisPoolCells, tombEntrances }
|
stats.act2Topography = { canyonWalls, sandTrailCells, oasisPoolCells, tombEntrances }
|
||||||
|
stats.proceduralWallCells = (stats.proceduralWallCells ?? 0) + canyonWalls
|
||||||
|
stats.proceduralRoadCells = (stats.proceduralRoadCells ?? 0) + sandTrailCells
|
||||||
|
stats.proceduralWaterCells = (stats.proceduralWaterCells ?? 0) + oasisPoolCells
|
||||||
stats.notes.push(
|
stats.notes.push(
|
||||||
`Act 2 Desert Topography: ${String(canyonWalls)} canyon walls, ${String(sandTrailCells)} sand trail cells, ${String(oasisPoolCells)} oasis cells, ${String(tombEntrances)} tomb entrances`,
|
`Act 2 Desert Topography: ${String(canyonWalls)} canyon walls, ${String(sandTrailCells)} sand trail cells, ${String(oasisPoolCells)} oasis cells, ${String(tombEntrances)} tomb entrances`,
|
||||||
)
|
)
|
||||||
|
|
@ -409,123 +412,6 @@ export function initAct2OutdoorLevel(
|
||||||
* Act 3: DRLGOUTPLACE_InitAct3OutdoorLevel & DRLG_GenerateJungles
|
* Act 3: DRLGOUTPLACE_InitAct3OutdoorLevel & DRLG_GenerateJungles
|
||||||
* ------------------------------------------------------------------------- */
|
* ------------------------------------------------------------------------- */
|
||||||
|
|
||||||
/**
|
|
||||||
* Generate Act 3 Jungle River & Clearings layout (`DRLG_GenerateJungles`):
|
|
||||||
* - Spans wooden river bridges across the East river channel at row crossings.
|
|
||||||
* - Connects West clearings to wooden river bridges via winding jungle causeways/paths.
|
|
||||||
*/
|
|
||||||
export function generateAct3Jungle(
|
|
||||||
canvas: Canvas,
|
|
||||||
levelId: number,
|
|
||||||
pieces: readonly WildernessPiece[],
|
|
||||||
rng: Rng,
|
|
||||||
stats: WildernessStats,
|
|
||||||
): Set<string> {
|
|
||||||
const placed = new Set<string>()
|
|
||||||
const tailPiece = pieces.find(p => p.name === 'Act 3 - Jungle Tail')
|
|
||||||
const headPiece = pieces.find(p => p.name === 'Act 3 - Jungle Head')
|
|
||||||
|
|
||||||
if (tailPiece?.levels[0]) {
|
|
||||||
stampDs1(canvas, tailPiece.levels[0], 0, 0)
|
|
||||||
placed.add(tailPiece.name)
|
|
||||||
stats.substitutions.push({ name: tailPiece.name, role: 'object', enabled: true, clusters: 1 })
|
|
||||||
}
|
|
||||||
if (headPiece?.levels[0]) {
|
|
||||||
stampDs1(canvas, headPiece.levels[0], 0, 160)
|
|
||||||
placed.add(headPiece.name)
|
|
||||||
stats.substitutions.push({ name: headPiece.name, role: 'object', enabled: true, clusters: 1 })
|
|
||||||
}
|
|
||||||
|
|
||||||
const clearingPrefix =
|
|
||||||
levelId === 76
|
|
||||||
? 'Act 3 - Clearing Webby'
|
|
||||||
: levelId === 77
|
|
||||||
? 'Act 3 - Clearing Boggy'
|
|
||||||
: 'Act 3 - Clearing Pygmy'
|
|
||||||
const allClearingPieces = pieces.filter(p => p.name.startsWith(clearingPrefix) && p.levels.length > 0)
|
|
||||||
const eastOpeningClearings = allClearingPieces.filter(p => /\b(E|EW|SE|NE)\b/i.test(p.name))
|
|
||||||
const clearingPool = eastOpeningClearings.length > 0 ? eastOpeningClearings : allClearingPieces
|
|
||||||
|
|
||||||
const allRiverPieces = pieces.filter(
|
|
||||||
p =>
|
|
||||||
p.name.startsWith('Act 3 - Jungle ') &&
|
|
||||||
p.name !== 'Act 3 - Jungle Head' &&
|
|
||||||
p.name !== 'Act 3 - Jungle Tail' &&
|
|
||||||
p.levels.length > 0,
|
|
||||||
)
|
|
||||||
const nsWestRivers = allRiverPieces.filter(p => /NS W|NS$/i.test(p.name))
|
|
||||||
const riverPool = nsWestRivers.length > 0 ? nsWestRivers : allRiverPieces
|
|
||||||
|
|
||||||
for (let row = 0; row < 4; row += 1) {
|
|
||||||
const y = 32 + row * 32
|
|
||||||
// West column (x = 0): Jungle Clearing opening East
|
|
||||||
if (clearingPool.length > 0) {
|
|
||||||
const chosenClearing = clearingPool[rng.int(0, clearingPool.length - 1)]!
|
|
||||||
const ds1 = chosenClearing.levels[rng.int(0, chosenClearing.levels.length - 1)]!
|
|
||||||
stampDs1(canvas, ds1, 0, y)
|
|
||||||
placed.add(chosenClearing.name)
|
|
||||||
stats.substitutions.push({ name: chosenClearing.name, role: 'object', enabled: true, clusters: 1 })
|
|
||||||
}
|
|
||||||
// East column (x = 32): Continuous North-South River channel
|
|
||||||
if (riverPool.length > 0) {
|
|
||||||
const chosenRiver = riverPool[rng.int(0, riverPool.length - 1)]!
|
|
||||||
const ds1 = chosenRiver.levels[rng.int(0, chosenRiver.levels.length - 1)]!
|
|
||||||
stampDs1(canvas, ds1, 32, y)
|
|
||||||
placed.add(chosenRiver.name)
|
|
||||||
stats.substitutions.push({ name: chosenRiver.name, role: 'object', enabled: true, clusters: 1 })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Stamp Wooden River Bridges & Jungle Paths spanning the river channel
|
|
||||||
let riverBridges = 0
|
|
||||||
let junglePathCells = 0
|
|
||||||
|
|
||||||
// Two canonical wooden bridge crossings at row 1 (y = 78..81) and row 3 (y = 142..145)
|
|
||||||
const bridgeYPositions = [78, 142]
|
|
||||||
for (const by of bridgeYPositions) {
|
|
||||||
for (let y = by; y < by + 4; y += 1) {
|
|
||||||
for (let x = 32; x < 60; x += 1) {
|
|
||||||
// Style 4: wooden bridge planks
|
|
||||||
paintFloor(canvas, x, y, makeFloor(4, 0, 2), true)
|
|
||||||
clearWalls(canvas, x, y)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
riverBridges += 1
|
|
||||||
}
|
|
||||||
|
|
||||||
// Jungle Trails connecting clearings across to the river bridges
|
|
||||||
for (const by of bridgeYPositions) {
|
|
||||||
// West clearing connection (x = 12..32)
|
|
||||||
for (let x = 12; x < 32; x += 1) {
|
|
||||||
for (let dy = 0; dy < 3; dy += 1) {
|
|
||||||
const y = by + dy
|
|
||||||
paintFloor(canvas, x, y, makeFloor(5, 0, 194))
|
|
||||||
clearWalls(canvas, x, y)
|
|
||||||
junglePathCells += 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// East trail along riverbank (x = 58..61, North-South trail)
|
|
||||||
for (let y = by - 14; y <= by + 14; y += 1) {
|
|
||||||
if (y >= 4 && y < canvas.height - 4) {
|
|
||||||
paintFloor(canvas, 59, y, makeFloor(5, 0, 194))
|
|
||||||
clearWalls(canvas, 59, y)
|
|
||||||
junglePathCells += 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
stats.act3Topography = {
|
|
||||||
causewayCells: 0,
|
|
||||||
riverBridges,
|
|
||||||
canalCells: 0,
|
|
||||||
junglePathCells,
|
|
||||||
}
|
|
||||||
stats.notes.push(
|
|
||||||
`Act 3 Jungle Topography: ${String(riverBridges)} river bridges, ${String(junglePathCells)} jungle path cells`,
|
|
||||||
)
|
|
||||||
|
|
||||||
return placed
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Stamp Kurast Causeway (LevelId 82):
|
* Stamp Kurast Causeway (LevelId 82):
|
||||||
|
|
@ -535,6 +421,7 @@ export function stampAct3KurastCauseway(
|
||||||
canvas: Canvas,
|
canvas: Canvas,
|
||||||
pieces: readonly WildernessPiece[],
|
pieces: readonly WildernessPiece[],
|
||||||
stats: WildernessStats,
|
stats: WildernessStats,
|
||||||
|
specialPlacedRects?: PlacedPresetRect[],
|
||||||
): Set<string> {
|
): Set<string> {
|
||||||
const placed = new Set<string>()
|
const placed = new Set<string>()
|
||||||
const piece = pieces.find(p => p.name === 'Act 3 - Bridge')
|
const piece = pieces.find(p => p.name === 'Act 3 - Bridge')
|
||||||
|
|
@ -544,17 +431,36 @@ export function stampAct3KurastCauseway(
|
||||||
stampDs1(canvas, ds1, 0, 0)
|
stampDs1(canvas, ds1, 0, 0)
|
||||||
placed.add('Act 3 - Bridge')
|
placed.add('Act 3 - Bridge')
|
||||||
stats.substitutions.push({ name: 'Act 3 - Bridge', role: 'object', enabled: true, clusters: 1 })
|
stats.substitutions.push({ name: 'Act 3 - Bridge', role: 'object', enabled: true, clusters: 1 })
|
||||||
|
if (specialPlacedRects) {
|
||||||
|
specialPlacedRects.push({
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
w: ds1.width,
|
||||||
|
h: ds1.height,
|
||||||
|
name: 'Act 3 - Bridge',
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Construct / ensure canonical causeway layout:
|
// Construct / ensure canonical causeway layout:
|
||||||
// Central paved walkway flanked by canal water and balustrades
|
// Central paved walkway flanked by canal water and balustrades
|
||||||
let causewayCells = 0
|
let causewayCells = 0
|
||||||
let canalCells = 0
|
let canalCells = 0
|
||||||
|
let balustradeCells = 0
|
||||||
const walkwayLeft = Math.max(2, Math.floor((canvas.width - 8) / 2))
|
const walkwayLeft = Math.max(2, Math.floor((canvas.width - 8) / 2))
|
||||||
const walkwayRight = walkwayLeft + 7
|
const walkwayRight = walkwayLeft + 7
|
||||||
|
|
||||||
for (let y = 0; y < canvas.height; y += 1) {
|
for (let y = 0; y < canvas.height; y += 1) {
|
||||||
for (let x = 0; x < canvas.width; x += 1) {
|
for (let x = 0; x < canvas.width; x += 1) {
|
||||||
|
if (ds1 && x >= 0 && x < ds1.width && y >= 0 && y < ds1.height) {
|
||||||
|
const ds1Cell = ds1.cells[y]?.[x]
|
||||||
|
const hasFloor = ds1Cell?.floors && ds1Cell.floors.length > 0 && !ds1Cell.floors[0]?.hidden
|
||||||
|
const hasWall = ds1Cell?.walls && ds1Cell.walls.length > 0 && !ds1Cell.walls[0]?.hidden
|
||||||
|
if (hasFloor || hasWall) {
|
||||||
|
// Keep artist-placed preset tiles from ds1; do not destructively wipe
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
if (x >= walkwayLeft && x <= walkwayRight) {
|
if (x >= walkwayLeft && x <= walkwayRight) {
|
||||||
// Paved stone causeway: style 5, prop1 194
|
// Paved stone causeway: style 5, prop1 194
|
||||||
paintFloor(canvas, x, y, makeFloor(5, 0, 194), true)
|
paintFloor(canvas, x, y, makeFloor(5, 0, 194), true)
|
||||||
|
|
@ -563,6 +469,7 @@ export function stampAct3KurastCauseway(
|
||||||
} else if (x === walkwayLeft - 1 || x === walkwayRight + 1) {
|
} else if (x === walkwayLeft - 1 || x === walkwayRight + 1) {
|
||||||
// Stone balustrade wall: style 1, prop1 129
|
// Stone balustrade wall: style 1, prop1 129
|
||||||
paintWall(canvas, x, y, makeWall(1, 0, 1, 129))
|
paintWall(canvas, x, y, makeWall(1, 0, 1, 129))
|
||||||
|
balustradeCells += 1
|
||||||
} else {
|
} else {
|
||||||
// Deep canal water: style 2, sequence 0, prop1 2
|
// Deep canal water: style 2, sequence 0, prop1 2
|
||||||
paintFloor(canvas, x, y, makeFloor(2, 0, 2), true)
|
paintFloor(canvas, x, y, makeFloor(2, 0, 2), true)
|
||||||
|
|
@ -578,6 +485,9 @@ export function stampAct3KurastCauseway(
|
||||||
canalCells,
|
canalCells,
|
||||||
junglePathCells: 0,
|
junglePathCells: 0,
|
||||||
}
|
}
|
||||||
|
stats.proceduralRoadCells = (stats.proceduralRoadCells ?? 0) + causewayCells
|
||||||
|
stats.proceduralWaterCells = (stats.proceduralWaterCells ?? 0) + canalCells
|
||||||
|
stats.proceduralWallCells = (stats.proceduralWallCells ?? 0) + balustradeCells
|
||||||
stats.notes.push(
|
stats.notes.push(
|
||||||
`Act 3 Causeway Topography: ${String(causewayCells)} causeway cells, ${String(canalCells)} canal water cells`,
|
`Act 3 Causeway Topography: ${String(causewayCells)} causeway cells, ${String(canalCells)} canal water cells`,
|
||||||
)
|
)
|
||||||
|
|
@ -605,6 +515,7 @@ export function initAct3OutdoorLevel(
|
||||||
let causewayCells = 0
|
let causewayCells = 0
|
||||||
let riverBridges = 0
|
let riverBridges = 0
|
||||||
let canalCells = 0
|
let canalCells = 0
|
||||||
|
let embankmentWalls = 0
|
||||||
const junglePathCells = 0
|
const junglePathCells = 0
|
||||||
|
|
||||||
const hubX = inX * TILES_PER_BLOCK + 4
|
const hubX = inX * TILES_PER_BLOCK + 4
|
||||||
|
|
@ -656,10 +567,14 @@ export function initAct3OutdoorLevel(
|
||||||
// Stone canal embankments
|
// Stone canal embankments
|
||||||
paintWall(canvas, canalX - 1, y, makeWall(1, 0, 1, 129))
|
paintWall(canvas, canalX - 1, y, makeWall(1, 0, 1, 129))
|
||||||
paintWall(canvas, canalX + 4, y, makeWall(1, 0, 1, 129))
|
paintWall(canvas, canalX + 4, y, makeWall(1, 0, 1, 129))
|
||||||
|
embankmentWalls += 2
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
stats.act3Topography = { causewayCells, riverBridges, canalCells, junglePathCells }
|
stats.act3Topography = { causewayCells, riverBridges, canalCells, junglePathCells }
|
||||||
|
stats.proceduralRoadCells = (stats.proceduralRoadCells ?? 0) + causewayCells
|
||||||
|
stats.proceduralWaterCells = (stats.proceduralWaterCells ?? 0) + canalCells
|
||||||
|
stats.proceduralWallCells = (stats.proceduralWallCells ?? 0) + embankmentWalls
|
||||||
stats.notes.push(
|
stats.notes.push(
|
||||||
`Act 3 Kurast Topography: ${String(causewayCells)} causeway cells, ${String(canalCells)} canal cells, ${String(riverBridges)} bridges`,
|
`Act 3 Kurast Topography: ${String(causewayCells)} causeway cells, ${String(canalCells)} canal cells, ${String(riverBridges)} bridges`,
|
||||||
)
|
)
|
||||||
|
|
@ -788,6 +703,8 @@ export function initAct4OutdoorLevel(
|
||||||
}
|
}
|
||||||
|
|
||||||
stats.act4Topography = { lavaChasmCells, rockyBridges, plateauCells }
|
stats.act4Topography = { lavaChasmCells, rockyBridges, plateauCells }
|
||||||
|
stats.proceduralWaterCells = (stats.proceduralWaterCells ?? 0) + lavaChasmCells
|
||||||
|
stats.proceduralRoadCells = (stats.proceduralRoadCells ?? 0) + rockyBridges
|
||||||
stats.notes.push(
|
stats.notes.push(
|
||||||
`Act 4 Mesa/Lava Topography: ${String(lavaChasmCells)} lava cells, ${String(rockyBridges)} rocky bridge cells, ${String(plateauCells)} plateau cells`,
|
`Act 4 Mesa/Lava Topography: ${String(lavaChasmCells)} lava cells, ${String(rockyBridges)} rocky bridge cells, ${String(plateauCells)} plateau cells`,
|
||||||
)
|
)
|
||||||
|
|
@ -909,6 +826,9 @@ export function initAct5OutdoorLevel(
|
||||||
}
|
}
|
||||||
|
|
||||||
stats.act5Topography = { barricadeWalls, trenchCells, frozenLakeCells }
|
stats.act5Topography = { barricadeWalls, trenchCells, frozenLakeCells }
|
||||||
|
stats.proceduralWallCells = (stats.proceduralWallCells ?? 0) + barricadeWalls
|
||||||
|
stats.proceduralRoadCells = (stats.proceduralRoadCells ?? 0) + trenchCells
|
||||||
|
stats.proceduralWaterCells = (stats.proceduralWaterCells ?? 0) + frozenLakeCells
|
||||||
stats.notes.push(
|
stats.notes.push(
|
||||||
`Act 5 Siege/Barricade Topography: ${String(barricadeWalls)} barricade walls, ${String(trenchCells)} trench cells, ${String(frozenLakeCells)} frozen lake cells`,
|
`Act 5 Siege/Barricade Topography: ${String(barricadeWalls)} barricade walls, ${String(trenchCells)} trench cells, ${String(frozenLakeCells)} frozen lake cells`,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,7 @@
|
||||||
|
|
||||||
import type { Ds1, Ds1Cell, Ds1Floor, Ds1Wall } from '../formats/ds1.ts'
|
import type { Ds1, Ds1Cell, Ds1Floor, Ds1Wall } from '../formats/ds1.ts'
|
||||||
import { Rng } from './rng.ts'
|
import { Rng } from './rng.ts'
|
||||||
import type { Canvas, SubstitutionReport, WildernessPiece, WildernessStats } from './wilderness.ts'
|
import type { Canvas, PlacedPresetRect, SubstitutionReport, WildernessPiece, WildernessStats } from './wilderness.ts'
|
||||||
|
|
||||||
/** Connection between two Act 3 zones. */
|
/** Connection between two Act 3 zones. */
|
||||||
export interface JungleZoneConnection {
|
export interface JungleZoneConnection {
|
||||||
|
|
@ -242,6 +242,7 @@ function synthesizeBridgePlanks(
|
||||||
bridgeX: number,
|
bridgeX: number,
|
||||||
bridgeY: number,
|
bridgeY: number,
|
||||||
orientation: 'horizontal' | 'vertical',
|
orientation: 'horizontal' | 'vertical',
|
||||||
|
bridgeDs1?: Ds1,
|
||||||
): void {
|
): void {
|
||||||
const makeBridgeFloor = (): Ds1Floor => ({
|
const makeBridgeFloor = (): Ds1Floor => ({
|
||||||
prop1: 2,
|
prop1: 2,
|
||||||
|
|
@ -261,6 +262,14 @@ function synthesizeBridgePlanks(
|
||||||
if (cy >= 0 && cy < canvas.height && cx >= 0 && cx < canvas.width) {
|
if (cy >= 0 && cy < canvas.height && cx >= 0 && cx < canvas.width) {
|
||||||
const row = canvas.cells[cy]
|
const row = canvas.cells[cy]
|
||||||
if (row?.[cx]) {
|
if (row?.[cx]) {
|
||||||
|
if (bridgeDs1 && dx < bridgeDs1.width && dy < bridgeDs1.height) {
|
||||||
|
const ds1Cell = bridgeDs1.cells[dy]?.[dx]
|
||||||
|
const hasPresetWall = ds1Cell?.walls && ds1Cell.walls.length > 0 && !ds1Cell.walls[0]?.hidden
|
||||||
|
const hasPresetFloor = ds1Cell?.floors && ds1Cell.floors.length > 0 && !ds1Cell.floors[0]?.hidden && ds1Cell.floors[0]?.style !== 1
|
||||||
|
if (hasPresetWall || hasPresetFloor) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
;(row[cx]!.floors as Ds1Floor[])[0] = makeBridgeFloor()
|
;(row[cx]!.floors as Ds1Floor[])[0] = makeBridgeFloor()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -274,6 +283,14 @@ function synthesizeBridgePlanks(
|
||||||
if (cy >= 0 && cy < canvas.height && cx >= 0 && cx < canvas.width) {
|
if (cy >= 0 && cy < canvas.height && cx >= 0 && cx < canvas.width) {
|
||||||
const row = canvas.cells[cy]
|
const row = canvas.cells[cy]
|
||||||
if (row?.[cx]) {
|
if (row?.[cx]) {
|
||||||
|
if (bridgeDs1 && dx < bridgeDs1.width && dy < bridgeDs1.height) {
|
||||||
|
const ds1Cell = bridgeDs1.cells[dy]?.[dx]
|
||||||
|
const hasPresetWall = ds1Cell?.walls && ds1Cell.walls.length > 0 && !ds1Cell.walls[0]?.hidden
|
||||||
|
const hasPresetFloor = ds1Cell?.floors && ds1Cell.floors.length > 0 && !ds1Cell.floors[0]?.hidden && ds1Cell.floors[0]?.style !== 1
|
||||||
|
if (hasPresetWall || hasPresetFloor) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
;(row[cx]!.floors as Ds1Floor[])[0] = makeBridgeFloor()
|
;(row[cx]!.floors as Ds1Floor[])[0] = makeBridgeFloor()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -343,6 +360,7 @@ export function DRLG_GenerateJungles(
|
||||||
rng: Rng,
|
rng: Rng,
|
||||||
stats: WildernessStats,
|
stats: WildernessStats,
|
||||||
stampDs1Fn: (canvas: Canvas, source: Ds1, originX: number, originY: number) => number,
|
stampDs1Fn: (canvas: Canvas, source: Ds1, originX: number, originY: number) => number,
|
||||||
|
placedRects?: PlacedPresetRect[],
|
||||||
): Set<string> {
|
): Set<string> {
|
||||||
const placed = new Set<string>()
|
const placed = new Set<string>()
|
||||||
|
|
||||||
|
|
@ -523,8 +541,9 @@ export function DRLG_GenerateJungles(
|
||||||
const offsetX = Math.max(0, Math.floor((32 - bDs1.width) / 2))
|
const offsetX = Math.max(0, Math.floor((32 - bDs1.width) / 2))
|
||||||
const offsetY = Math.max(0, Math.floor((32 - bDs1.height) / 2))
|
const offsetY = Math.max(0, Math.floor((32 - bDs1.height) / 2))
|
||||||
stampDs1Fn(canvas, bDs1, bx + offsetX, by + offsetY)
|
stampDs1Fn(canvas, bDs1, bx + offsetX, by + offsetY)
|
||||||
|
placedRects?.push({ x: bx + offsetX, y: by + offsetY, w: bDs1.width, h: bDs1.height, name: bridgePiece.name })
|
||||||
}
|
}
|
||||||
synthesizeBridgePlanks(canvas, bx, by, orientation)
|
synthesizeBridgePlanks(canvas, bx, by, orientation, bridgePiece?.levels[0])
|
||||||
placed.add('Act 3 - Bridge')
|
placed.add('Act 3 - Bridge')
|
||||||
stats.substitutions.push({ name: 'Act 3 - Bridge', role: 'object', enabled: true, clusters: 1 })
|
stats.substitutions.push({ name: 'Act 3 - Bridge', role: 'object', enabled: true, clusters: 1 })
|
||||||
}
|
}
|
||||||
|
|
@ -538,9 +557,11 @@ export function DRLG_GenerateJungles(
|
||||||
bridges.push({ col: bridgeCol, row: bridgeRow, x: bx, y: by, pieceName: 'Act 3 - Bridge' })
|
bridges.push({ col: bridgeCol, row: bridgeRow, x: bx, y: by, pieceName: 'Act 3 - Bridge' })
|
||||||
|
|
||||||
if (bridgePiece?.levels[0]) {
|
if (bridgePiece?.levels[0]) {
|
||||||
stampDs1Fn(canvas, bridgePiece.levels[0], bx, by)
|
const bDs1 = bridgePiece.levels[0]
|
||||||
|
stampDs1Fn(canvas, bDs1, bx, by)
|
||||||
|
placedRects?.push({ x: bx, y: by, w: bDs1.width, h: bDs1.height, name: bridgePiece.name })
|
||||||
}
|
}
|
||||||
synthesizeBridgePlanks(canvas, bx, by, 'horizontal')
|
synthesizeBridgePlanks(canvas, bx, by, 'horizontal', bridgePiece?.levels[0])
|
||||||
placed.add('Act 3 - Bridge')
|
placed.add('Act 3 - Bridge')
|
||||||
stats.substitutions.push({ name: 'Act 3 - Bridge', role: 'object', enabled: true, clusters: 1 })
|
stats.substitutions.push({ name: 'Act 3 - Bridge', role: 'object', enabled: true, clusters: 1 })
|
||||||
}
|
}
|
||||||
|
|
@ -553,11 +574,13 @@ export function DRLG_GenerateJungles(
|
||||||
stampDs1Fn(canvas, tailPiece.levels[0], 0, 0)
|
stampDs1Fn(canvas, tailPiece.levels[0], 0, 0)
|
||||||
placed.add(tailPiece.name)
|
placed.add(tailPiece.name)
|
||||||
stats.substitutions.push({ name: tailPiece.name, role: 'object', enabled: true, clusters: 1 })
|
stats.substitutions.push({ name: tailPiece.name, role: 'object', enabled: true, clusters: 1 })
|
||||||
|
placedRects?.push({ x: 0, y: 0, w: tailPiece.levels[0].width, h: tailPiece.levels[0].height, name: tailPiece.name })
|
||||||
}
|
}
|
||||||
if (headPiece?.levels[0]) {
|
if (headPiece?.levels[0]) {
|
||||||
stampDs1Fn(canvas, headPiece.levels[0], 0, (numRows - 1) * 32)
|
stampDs1Fn(canvas, headPiece.levels[0], 0, (numRows - 1) * 32)
|
||||||
placed.add(headPiece.name)
|
placed.add(headPiece.name)
|
||||||
stats.substitutions.push({ name: headPiece.name, role: 'object', enabled: true, clusters: 1 })
|
stats.substitutions.push({ name: headPiece.name, role: 'object', enabled: true, clusters: 1 })
|
||||||
|
placedRects?.push({ x: 0, y: (numRows - 1) * 32, w: headPiece.levels[0].width, h: headPiece.levels[0].height, name: headPiece.name })
|
||||||
}
|
}
|
||||||
|
|
||||||
// 8. Stamp Interior River and Clearing Presets
|
// 8. Stamp Interior River and Clearing Presets
|
||||||
|
|
@ -575,6 +598,7 @@ export function DRLG_GenerateJungles(
|
||||||
cell.pieceName = riverPiece.name
|
cell.pieceName = riverPiece.name
|
||||||
placed.add(riverPiece.name)
|
placed.add(riverPiece.name)
|
||||||
stats.substitutions.push({ name: riverPiece.name, role: 'object', enabled: true, clusters: 1 })
|
stats.substitutions.push({ name: riverPiece.name, role: 'object', enabled: true, clusters: 1 })
|
||||||
|
placedRects?.push({ x, y, w: ds1.width, h: ds1.height, name: riverPiece.name })
|
||||||
}
|
}
|
||||||
} else if (cell.type === 'clearing') {
|
} else if (cell.type === 'clearing') {
|
||||||
const preferredOpening =
|
const preferredOpening =
|
||||||
|
|
@ -591,6 +615,7 @@ export function DRLG_GenerateJungles(
|
||||||
cell.pieceName = clearingPiece.name
|
cell.pieceName = clearingPiece.name
|
||||||
placed.add(clearingPiece.name)
|
placed.add(clearingPiece.name)
|
||||||
stats.substitutions.push({ name: clearingPiece.name, role: 'object', enabled: true, clusters: 1 })
|
stats.substitutions.push({ name: clearingPiece.name, role: 'object', enabled: true, clusters: 1 })
|
||||||
|
placedRects?.push({ x, y, w: ds1.width, h: ds1.height, name: clearingPiece.name })
|
||||||
clearings.push({
|
clearings.push({
|
||||||
col: c,
|
col: c,
|
||||||
row: r,
|
row: r,
|
||||||
|
|
@ -605,9 +630,11 @@ export function DRLG_GenerateJungles(
|
||||||
// 9. Re-stamp Bridges on top of river water tiles to ensure bridge plank floor integrity
|
// 9. Re-stamp Bridges on top of river water tiles to ensure bridge plank floor integrity
|
||||||
for (const b of bridges) {
|
for (const b of bridges) {
|
||||||
if (bridgePiece?.levels[0]) {
|
if (bridgePiece?.levels[0]) {
|
||||||
stampDs1Fn(canvas, bridgePiece.levels[0], b.x, b.y)
|
const ds1 = bridgePiece.levels[0]
|
||||||
|
stampDs1Fn(canvas, ds1, b.x, b.y)
|
||||||
|
placedRects?.push({ x: b.x, y: b.y, w: ds1.width, h: ds1.height, name: bridgePiece.name })
|
||||||
}
|
}
|
||||||
synthesizeBridgePlanks(canvas, b.x, b.y, 'horizontal')
|
synthesizeBridgePlanks(canvas, b.x, b.y, 'horizontal', bridgePiece?.levels[0])
|
||||||
}
|
}
|
||||||
|
|
||||||
// 10. Record Layout Statistics
|
// 10. Record Layout Statistics
|
||||||
|
|
|
||||||
|
|
@ -416,34 +416,34 @@ export function stampAct5SiegeStrips(
|
||||||
// Zone 1: West Frontline (x in [0, 80)) - High density (Shenk's stronghold)
|
// Zone 1: West Frontline (x in [0, 80)) - High density (Shenk's stronghold)
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
// Trenches (3 trenches)
|
// Trenches (3 trenches)
|
||||||
totalTrenchCells += stampTrench(canvas, 18, 8, 10, 'vertical', rng)
|
totalTrenchCells += stampTrench(canvas, 18 + rng.int(-2, 2), 8 + rng.int(-1, 1), 10 + rng.int(-1, 1), 'vertical', rng)
|
||||||
totalTrenchCells += stampTrench(canvas, 50, 28, 10, 'vertical', rng)
|
totalTrenchCells += stampTrench(canvas, 50 + rng.int(-2, 2), 28 + rng.int(-1, 1), 10 + rng.int(-1, 1), 'vertical', rng)
|
||||||
totalTrenchCells += stampTrench(canvas, 34, 12, 7, 'horizontal', rng)
|
totalTrenchCells += stampTrench(canvas, 34 + rng.int(-2, 2), 12 + rng.int(-1, 1), 7 + rng.int(-1, 1), 'horizontal', rng)
|
||||||
westObstacles += 3
|
westObstacles += 3
|
||||||
|
|
||||||
// Breached Barricades (3 barricades)
|
// Breached Barricades (3 barricades)
|
||||||
const b1 = stampBreachedBarricade(canvas, 26, 10, 24, 21, 5, rng)
|
const b1 = stampBreachedBarricade(canvas, 26 + rng.int(-2, 2), 10 + rng.int(-1, 1), 24, 21 + rng.int(0, 1), 5, rng)
|
||||||
totalBarricadeWalls += b1.wallCount
|
totalBarricadeWalls += b1.wallCount
|
||||||
totalTrenchCells += b1.breachFloorCount
|
totalTrenchCells += b1.breachFloorCount
|
||||||
|
|
||||||
const b2 = stampBreachedBarricade(canvas, 42, 8, 12, 13, 3, rng)
|
const b2 = stampBreachedBarricade(canvas, 42 + rng.int(-2, 2), 8 + rng.int(-1, 1), 12, 13, 3, rng)
|
||||||
totalBarricadeWalls += b2.wallCount
|
totalBarricadeWalls += b2.wallCount
|
||||||
totalTrenchCells += b2.breachFloorCount
|
totalTrenchCells += b2.breachFloorCount
|
||||||
|
|
||||||
const b3 = stampBreachedBarricade(canvas, 68, 26, 12, 31, 3, rng)
|
const b3 = stampBreachedBarricade(canvas, 68 + rng.int(-2, 2), 26 + rng.int(-1, 1), 12, 31, 3, rng)
|
||||||
totalBarricadeWalls += b3.wallCount
|
totalBarricadeWalls += b3.wallCount
|
||||||
totalTrenchCells += b3.breachFloorCount
|
totalTrenchCells += b3.breachFloorCount
|
||||||
westObstacles += 3
|
westObstacles += 3
|
||||||
|
|
||||||
// Catapult Platforms (2 platforms)
|
// Catapult Platforms (2 platforms)
|
||||||
stampCatapultPlatform(canvas, 12, 10, 4, 4, rng)
|
stampCatapultPlatform(canvas, 12 + rng.int(-2, 2), 10 + rng.int(-1, 1), 4, 4, rng)
|
||||||
stampCatapultPlatform(canvas, 58, 30, 4, 4, rng)
|
stampCatapultPlatform(canvas, 58 + rng.int(-2, 2), 30 + rng.int(-1, 1), 4, 4, rng)
|
||||||
catapultPlatformCount += 2
|
catapultPlatformCount += 2
|
||||||
westObstacles += 2
|
westObstacles += 2
|
||||||
|
|
||||||
// Fort Ruins (2 ruins)
|
// Fort Ruins (2 ruins)
|
||||||
stampFortRuin(canvas, 30, 30, 4, rng)
|
stampFortRuin(canvas, 30 + rng.int(-2, 2), 30 + rng.int(-1, 1), 4, rng)
|
||||||
stampFortRuin(canvas, 64, 10, 4, rng)
|
stampFortRuin(canvas, 64 + rng.int(-2, 2), 10 + rng.int(-1, 1), 4, rng)
|
||||||
fortRuinCount += 2
|
fortRuinCount += 2
|
||||||
westObstacles += 2
|
westObstacles += 2
|
||||||
|
|
||||||
|
|
@ -451,27 +451,27 @@ export function stampAct5SiegeStrips(
|
||||||
// Zone 2: Mid Field (x in [80, 160)) - Medium density (No-man's land)
|
// Zone 2: Mid Field (x in [80, 160)) - Medium density (No-man's land)
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
// Trenches (2 trenches)
|
// Trenches (2 trenches)
|
||||||
totalTrenchCells += stampTrench(canvas, 98, 10, 10, 'vertical', rng)
|
totalTrenchCells += stampTrench(canvas, 98 + rng.int(-2, 2), 10 + rng.int(-1, 1), 10 + rng.int(-1, 1), 'vertical', rng)
|
||||||
totalTrenchCells += stampTrench(canvas, 138, 28, 10, 'vertical', rng)
|
totalTrenchCells += stampTrench(canvas, 138 + rng.int(-2, 2), 28 + rng.int(-1, 1), 10 + rng.int(-1, 1), 'vertical', rng)
|
||||||
midObstacles += 2
|
midObstacles += 2
|
||||||
|
|
||||||
// Breached Barricades (2 barricades)
|
// Breached Barricades (2 barricades)
|
||||||
const b4 = stampBreachedBarricade(canvas, 114, 10, 24, 21, 5, rng)
|
const b4 = stampBreachedBarricade(canvas, 114 + rng.int(-2, 2), 10 + rng.int(-1, 1), 24, 21 + rng.int(0, 1), 5, rng)
|
||||||
totalBarricadeWalls += b4.wallCount
|
totalBarricadeWalls += b4.wallCount
|
||||||
totalTrenchCells += b4.breachFloorCount
|
totalTrenchCells += b4.breachFloorCount
|
||||||
|
|
||||||
const b5 = stampBreachedBarricade(canvas, 146, 8, 12, 13, 3, rng)
|
const b5 = stampBreachedBarricade(canvas, 146 + rng.int(-2, 2), 8 + rng.int(-1, 1), 12, 13, 3, rng)
|
||||||
totalBarricadeWalls += b5.wallCount
|
totalBarricadeWalls += b5.wallCount
|
||||||
totalTrenchCells += b5.breachFloorCount
|
totalTrenchCells += b5.breachFloorCount
|
||||||
midObstacles += 2
|
midObstacles += 2
|
||||||
|
|
||||||
// Catapult Platforms (1 platform)
|
// Catapult Platforms (1 platform)
|
||||||
stampCatapultPlatform(canvas, 86, 30, 4, 4, rng)
|
stampCatapultPlatform(canvas, 86 + rng.int(-2, 2), 30 + rng.int(-1, 1), 4, 4, rng)
|
||||||
catapultPlatformCount += 1
|
catapultPlatformCount += 1
|
||||||
midObstacles += 1
|
midObstacles += 1
|
||||||
|
|
||||||
// Fort Ruins (1 ruin)
|
// Fort Ruins (1 ruin)
|
||||||
stampFortRuin(canvas, 124, 10, 4, rng)
|
stampFortRuin(canvas, 124 + rng.int(-2, 2), 10 + rng.int(-1, 1), 4, rng)
|
||||||
fortRuinCount += 1
|
fortRuinCount += 1
|
||||||
midObstacles += 1
|
midObstacles += 1
|
||||||
|
|
||||||
|
|
@ -479,11 +479,11 @@ export function stampAct5SiegeStrips(
|
||||||
// Zone 3: East Rear (x in [160, 240)) - Low density (Harrogath outskirts)
|
// Zone 3: East Rear (x in [160, 240)) - Low density (Harrogath outskirts)
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
// Trenches (1 trench)
|
// Trenches (1 trench)
|
||||||
totalTrenchCells += stampTrench(canvas, 186, 12, 10, 'vertical', rng)
|
totalTrenchCells += stampTrench(canvas, 186 + rng.int(-2, 2), 12 + rng.int(-1, 1), 10 + rng.int(-1, 1), 'vertical', rng)
|
||||||
eastObstacles += 1
|
eastObstacles += 1
|
||||||
|
|
||||||
// Breached Barricades (1 barricade)
|
// Breached Barricades (1 barricade)
|
||||||
const b6 = stampBreachedBarricade(canvas, 202, 10, 24, 21, 5, rng)
|
const b6 = stampBreachedBarricade(canvas, 202 + rng.int(-2, 2), 10 + rng.int(-1, 1), 24, 21 + rng.int(0, 1), 5, rng)
|
||||||
totalBarricadeWalls += b6.wallCount
|
totalBarricadeWalls += b6.wallCount
|
||||||
totalTrenchCells += b6.breachFloorCount
|
totalTrenchCells += b6.breachFloorCount
|
||||||
eastObstacles += 1
|
eastObstacles += 1
|
||||||
|
|
@ -491,7 +491,7 @@ export function stampAct5SiegeStrips(
|
||||||
// Catapult Platforms (0 platforms in rear reserve)
|
// Catapult Platforms (0 platforms in rear reserve)
|
||||||
|
|
||||||
// Fort Ruins (1 ruin)
|
// Fort Ruins (1 ruin)
|
||||||
stampFortRuin(canvas, 170, 30, 4, rng)
|
stampFortRuin(canvas, 170 + rng.int(-2, 2), 30 + rng.int(-1, 1), 4, rng)
|
||||||
fortRuinCount += 1
|
fortRuinCount += 1
|
||||||
eastObstacles += 1
|
eastObstacles += 1
|
||||||
|
|
||||||
|
|
@ -510,6 +510,8 @@ export function stampAct5SiegeStrips(
|
||||||
}
|
}
|
||||||
|
|
||||||
stats.siegeTopography = siegeStats
|
stats.siegeTopography = siegeStats
|
||||||
|
stats.proceduralWallCells = (stats.proceduralWallCells ?? 0) + totalBarricadeWalls
|
||||||
|
stats.proceduralRoadCells = (stats.proceduralRoadCells ?? 0) + totalTrenchCells
|
||||||
stats.notes.push('generated dynamic siege trenches and fortifications')
|
stats.notes.push('generated dynamic siege trenches and fortifications')
|
||||||
|
|
||||||
return placed
|
return placed
|
||||||
|
|
|
||||||
|
|
@ -82,7 +82,6 @@ import type { Side } from './world-graph.ts'
|
||||||
import {
|
import {
|
||||||
initAct2OutdoorLevel,
|
initAct2OutdoorLevel,
|
||||||
initAct3OutdoorLevel,
|
initAct3OutdoorLevel,
|
||||||
generateAct3Jungle,
|
|
||||||
stampAct3KurastCauseway,
|
stampAct3KurastCauseway,
|
||||||
initAct4OutdoorLevel,
|
initAct4OutdoorLevel,
|
||||||
initAct5OutdoorLevel,
|
initAct5OutdoorLevel,
|
||||||
|
|
@ -1071,6 +1070,14 @@ export interface LandmarkEntity {
|
||||||
readonly objectId?: number
|
readonly objectId?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Procedural terrain pass telemetry counters (Issue #62). */
|
||||||
|
export interface WildernessTelemetry {
|
||||||
|
readonly proceduralRoadCells: number
|
||||||
|
readonly proceduralDividerCells: number
|
||||||
|
readonly proceduralWaterCells: number
|
||||||
|
readonly proceduralWallCells: number
|
||||||
|
}
|
||||||
|
|
||||||
/** The report collector. */
|
/** The report collector. */
|
||||||
export interface WildernessStats {
|
export interface WildernessStats {
|
||||||
readonly substitutions: SubstitutionReport[]
|
readonly substitutions: SubstitutionReport[]
|
||||||
|
|
@ -1104,6 +1111,11 @@ export interface WildernessStats {
|
||||||
riverForks?: number
|
riverForks?: number
|
||||||
meanderCount?: number
|
meanderCount?: number
|
||||||
siegeTopography?: SiegeTopographyStats
|
siegeTopography?: SiegeTopographyStats
|
||||||
|
subTheme?: number
|
||||||
|
proceduralRoadCells?: number
|
||||||
|
proceduralDividerCells?: number
|
||||||
|
proceduralWaterCells?: number
|
||||||
|
proceduralWallCells?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -3123,7 +3135,7 @@ function stampAct3Jungle(
|
||||||
stats: WildernessStats,
|
stats: WildernessStats,
|
||||||
placedRects?: PlacedPresetRect[],
|
placedRects?: PlacedPresetRect[],
|
||||||
): Set<string> {
|
): Set<string> {
|
||||||
return DRLG_GenerateJungles(canvas, levelId, pieces, rng, stats, stampDs1)
|
return DRLG_GenerateJungles(canvas, levelId, pieces, rng, stats, stampDs1, placedRects)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Theme configuration for outdoor road rasterization. */
|
/** Theme configuration for outdoor road rasterization. */
|
||||||
|
|
@ -3154,10 +3166,13 @@ export function stampThemedRoads(
|
||||||
rng: Rng,
|
rng: Rng,
|
||||||
theme: OutdoorRoadTheme,
|
theme: OutdoorRoadTheme,
|
||||||
openGates?: ReadonlyMap<string, number>,
|
openGates?: ReadonlyMap<string, number>,
|
||||||
|
stats?: WildernessStats,
|
||||||
): void {
|
): void {
|
||||||
|
const paintedRoadCells = new Set<string>()
|
||||||
|
|
||||||
const isInsideProtectedPreset = (x: number, y: number): boolean => {
|
const isInsideProtectedPreset = (x: number, y: number): boolean => {
|
||||||
for (const r of placedRects) {
|
for (const r of placedRects) {
|
||||||
if (theme.isProtectedPreset ? !theme.isProtectedPreset(r.name) : /River|Bridge/i.test(r.name)) {
|
if (theme.isProtectedPreset ? !theme.isProtectedPreset(r.name) : false) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if (x >= r.x && x < r.x + r.w && y >= r.y && y < r.y + r.h) {
|
if (x >= r.x && x < r.x + r.w && y >= r.y && y < r.y + r.h) {
|
||||||
|
|
@ -3177,7 +3192,11 @@ export function stampThemedRoads(
|
||||||
if (existingFloor && (existingFloor.style === 2 || existingFloor.style === 3 || existingFloor.style === 4)) {
|
if (existingFloor && (existingFloor.style === 2 || existingFloor.style === 3 || existingFloor.style === 4)) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
const belongsToPreset = placedRects.some(r => x >= r.x && x < r.x + r.w && y >= r.y && y < r.y + r.h)
|
||||||
if (cell.walls.some(w => !w.hidden && w.prop1 !== 0)) {
|
if (cell.walls.some(w => !w.hidden && w.prop1 !== 0)) {
|
||||||
|
if (belongsToPreset) {
|
||||||
|
return
|
||||||
|
}
|
||||||
if (clearStrayClutter) {
|
if (clearStrayClutter) {
|
||||||
// Clear random LvlSub bush/tree clutter on the main highway spine
|
// Clear random LvlSub bush/tree clutter on the main highway spine
|
||||||
;(cell.walls as Ds1Wall[]).length = 0
|
;(cell.walls as Ds1Wall[]).length = 0
|
||||||
|
|
@ -3186,6 +3205,7 @@ export function stampThemedRoads(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
;(cell.floors as Ds1Floor[])[0] = theme.roadFloor(rng)
|
;(cell.floors as Ds1Floor[])[0] = theme.roadFloor(rng)
|
||||||
|
paintedRoadCells.add(`${x},${y}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
const hubX = inX * TILES_PER_BLOCK + 4
|
const hubX = inX * TILES_PER_BLOCK + 4
|
||||||
|
|
@ -3284,6 +3304,10 @@ export function stampThemedRoads(
|
||||||
paintSpurSegment(doorX, doorY, doorX, hubY)
|
paintSpurSegment(doorX, doorY, doorX, hubY)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (stats) {
|
||||||
|
stats.proceduralRoadCells = (stats.proceduralRoadCells ?? 0) + paintedRoadCells.size
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -3299,11 +3323,13 @@ export function stampThemedDividers(
|
||||||
placedRects: readonly PlacedPresetRect[],
|
placedRects: readonly PlacedPresetRect[],
|
||||||
rng: Rng,
|
rng: Rng,
|
||||||
theme: OutdoorDividerTheme,
|
theme: OutdoorDividerTheme,
|
||||||
|
stats?: WildernessStats,
|
||||||
): void {
|
): void {
|
||||||
if (gridWidth < 6 || gridHeight < 6) return
|
if (gridWidth < 6 || gridHeight < 6) return
|
||||||
|
|
||||||
const wallStyle = theme.wallStyle
|
const wallStyle = theme.wallStyle
|
||||||
const wallProp1 = theme.wallProp1 ?? 129
|
const wallProp1 = theme.wallProp1 ?? 129
|
||||||
|
const placedCells = new Set<string>()
|
||||||
|
|
||||||
const makeWall = (type: number, sequence: number): Ds1Wall => ({
|
const makeWall = (type: number, sequence: number): Ds1Wall => ({
|
||||||
prop1: wallProp1,
|
prop1: wallProp1,
|
||||||
|
|
@ -3324,6 +3350,7 @@ export function stampThemedDividers(
|
||||||
} else {
|
} else {
|
||||||
targetWalls.push(wall)
|
targetWalls.push(wall)
|
||||||
}
|
}
|
||||||
|
placedCells.add(`${x},${y}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Offset divider lines by 2 blocks (16 tiles) from the main road spine
|
// Offset divider lines by 2 blocks (16 tiles) from the main road spine
|
||||||
|
|
@ -3419,6 +3446,11 @@ export function stampThemedDividers(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (stats) {
|
||||||
|
stats.proceduralDividerCells = (stats.proceduralDividerCells ?? 0) + placedCells.size
|
||||||
|
stats.proceduralWallCells = (stats.proceduralWallCells ?? 0) + placedCells.size
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -3434,6 +3466,7 @@ export function stampAct1DirtRoads(
|
||||||
placedRects: readonly PlacedPresetRect[],
|
placedRects: readonly PlacedPresetRect[],
|
||||||
rng: Rng,
|
rng: Rng,
|
||||||
openGates?: ReadonlyMap<string, number>,
|
openGates?: ReadonlyMap<string, number>,
|
||||||
|
stats?: WildernessStats,
|
||||||
): void {
|
): void {
|
||||||
stampThemedRoads(
|
stampThemedRoads(
|
||||||
canvas,
|
canvas,
|
||||||
|
|
@ -3456,6 +3489,7 @@ export function stampAct1DirtRoads(
|
||||||
isIgnoredSpurPreset: (name) => name === 'Act 1 - River' || /Stone Fill|Swamp Fill|Tree Fill|Fence Fill|Corral Fill/i.test(name),
|
isIgnoredSpurPreset: (name) => name === 'Act 1 - River' || /Stone Fill|Swamp Fill|Tree Fill|Fence Fill|Corral Fill/i.test(name),
|
||||||
},
|
},
|
||||||
openGates,
|
openGates,
|
||||||
|
stats,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -3471,12 +3505,13 @@ export function stampAct1InteriorStoneWalls(
|
||||||
outY: number,
|
outY: number,
|
||||||
placedRects: readonly PlacedPresetRect[],
|
placedRects: readonly PlacedPresetRect[],
|
||||||
rng: Rng,
|
rng: Rng,
|
||||||
|
stats?: WildernessStats,
|
||||||
): void {
|
): void {
|
||||||
stampThemedDividers(canvas, gridWidth, gridHeight, inX, outY, placedRects, rng, {
|
stampThemedDividers(canvas, gridWidth, gridHeight, inX, outY, placedRects, rng, {
|
||||||
wallStyle: 2,
|
wallStyle: 2,
|
||||||
wallProp1: 129,
|
wallProp1: 129,
|
||||||
roadStyles: [5],
|
roadStyles: [5],
|
||||||
})
|
}, stats)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -3491,6 +3526,7 @@ export function stampAct2DesertRoads(
|
||||||
placedRects: readonly PlacedPresetRect[],
|
placedRects: readonly PlacedPresetRect[],
|
||||||
rng: Rng,
|
rng: Rng,
|
||||||
openGates?: ReadonlyMap<string, number>,
|
openGates?: ReadonlyMap<string, number>,
|
||||||
|
stats?: WildernessStats,
|
||||||
): void {
|
): void {
|
||||||
stampThemedRoads(
|
stampThemedRoads(
|
||||||
canvas,
|
canvas,
|
||||||
|
|
@ -3509,10 +3545,11 @@ export function stampAct2DesertRoads(
|
||||||
unknown2: 0,
|
unknown2: 0,
|
||||||
hidden: false,
|
hidden: false,
|
||||||
}),
|
}),
|
||||||
isProtectedPreset: (name) => !/River|Bridge/i.test(name),
|
isProtectedPreset: (name) => name !== 'Act 1 - River',
|
||||||
isIgnoredSpurPreset: (name) => /Fill|Berms/i.test(name),
|
isIgnoredSpurPreset: (name) => /Fill|Berms/i.test(name),
|
||||||
},
|
},
|
||||||
openGates,
|
openGates,
|
||||||
|
stats,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -3527,12 +3564,13 @@ export function stampAct2SandstoneBerms(
|
||||||
outY: number,
|
outY: number,
|
||||||
placedRects: readonly PlacedPresetRect[],
|
placedRects: readonly PlacedPresetRect[],
|
||||||
rng: Rng,
|
rng: Rng,
|
||||||
|
stats?: WildernessStats,
|
||||||
): void {
|
): void {
|
||||||
stampThemedDividers(canvas, gridWidth, gridHeight, inX, outY, placedRects, rng, {
|
stampThemedDividers(canvas, gridWidth, gridHeight, inX, outY, placedRects, rng, {
|
||||||
wallStyle: 1,
|
wallStyle: 1,
|
||||||
wallProp1: 129,
|
wallProp1: 129,
|
||||||
roadStyles: [1],
|
roadStyles: [1],
|
||||||
})
|
}, stats)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -3547,6 +3585,7 @@ export function stampAct3KurastAvenues(
|
||||||
placedRects: readonly PlacedPresetRect[],
|
placedRects: readonly PlacedPresetRect[],
|
||||||
rng: Rng,
|
rng: Rng,
|
||||||
openGates?: ReadonlyMap<string, number>,
|
openGates?: ReadonlyMap<string, number>,
|
||||||
|
stats?: WildernessStats,
|
||||||
): void {
|
): void {
|
||||||
stampThemedRoads(
|
stampThemedRoads(
|
||||||
canvas,
|
canvas,
|
||||||
|
|
@ -3565,10 +3604,11 @@ export function stampAct3KurastAvenues(
|
||||||
unknown2: 0,
|
unknown2: 0,
|
||||||
hidden: false,
|
hidden: false,
|
||||||
}),
|
}),
|
||||||
isProtectedPreset: (name) => !/River|Bridge/i.test(name),
|
isProtectedPreset: (name) => name !== 'Act 1 - River',
|
||||||
isIgnoredSpurPreset: (name) => /Fill/i.test(name),
|
isIgnoredSpurPreset: (name) => /Fill/i.test(name),
|
||||||
},
|
},
|
||||||
openGates,
|
openGates,
|
||||||
|
stats,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -3583,12 +3623,13 @@ export function stampAct3KurastBalustrades(
|
||||||
outY: number,
|
outY: number,
|
||||||
placedRects: readonly PlacedPresetRect[],
|
placedRects: readonly PlacedPresetRect[],
|
||||||
rng: Rng,
|
rng: Rng,
|
||||||
|
stats?: WildernessStats,
|
||||||
): void {
|
): void {
|
||||||
stampThemedDividers(canvas, gridWidth, gridHeight, inX, outY, placedRects, rng, {
|
stampThemedDividers(canvas, gridWidth, gridHeight, inX, outY, placedRects, rng, {
|
||||||
wallStyle: 1,
|
wallStyle: 1,
|
||||||
wallProp1: 129,
|
wallProp1: 129,
|
||||||
roadStyles: [1],
|
roadStyles: [1],
|
||||||
})
|
}, stats)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -3603,6 +3644,7 @@ export function stampAct5SnowRoads(
|
||||||
placedRects: readonly PlacedPresetRect[],
|
placedRects: readonly PlacedPresetRect[],
|
||||||
rng: Rng,
|
rng: Rng,
|
||||||
openGates?: ReadonlyMap<string, number>,
|
openGates?: ReadonlyMap<string, number>,
|
||||||
|
stats?: WildernessStats,
|
||||||
): void {
|
): void {
|
||||||
stampThemedRoads(
|
stampThemedRoads(
|
||||||
canvas,
|
canvas,
|
||||||
|
|
@ -3621,10 +3663,11 @@ export function stampAct5SnowRoads(
|
||||||
unknown2: 0,
|
unknown2: 0,
|
||||||
hidden: false,
|
hidden: false,
|
||||||
}),
|
}),
|
||||||
isProtectedPreset: (name) => !/River|Bridge/i.test(name),
|
isProtectedPreset: (name) => name !== 'Act 1 - River',
|
||||||
isIgnoredSpurPreset: (name) => /Fill|Filler/i.test(name),
|
isIgnoredSpurPreset: (name) => /Fill|Filler/i.test(name),
|
||||||
},
|
},
|
||||||
openGates,
|
openGates,
|
||||||
|
stats,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -3639,12 +3682,13 @@ export function stampAct5FrozenTrailDivides(
|
||||||
outY: number,
|
outY: number,
|
||||||
placedRects: readonly PlacedPresetRect[],
|
placedRects: readonly PlacedPresetRect[],
|
||||||
rng: Rng,
|
rng: Rng,
|
||||||
|
stats?: WildernessStats,
|
||||||
): void {
|
): void {
|
||||||
stampThemedDividers(canvas, gridWidth, gridHeight, inX, outY, placedRects, rng, {
|
stampThemedDividers(canvas, gridWidth, gridHeight, inX, outY, placedRects, rng, {
|
||||||
wallStyle: 2,
|
wallStyle: 2,
|
||||||
wallProp1: 129,
|
wallProp1: 129,
|
||||||
roadStyles: [1],
|
roadStyles: [1],
|
||||||
})
|
}, stats)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -3663,47 +3707,48 @@ export function stampOutdoorRoads(
|
||||||
levelTypeName?: string,
|
levelTypeName?: string,
|
||||||
openGates?: ReadonlyMap<string, number>,
|
openGates?: ReadonlyMap<string, number>,
|
||||||
levelsTable?: D2Table,
|
levelsTable?: D2Table,
|
||||||
|
stats?: WildernessStats,
|
||||||
): void {
|
): void {
|
||||||
if (levelId === 39) return // Cow Level has no roads
|
if (levelId === 39) return // Cow Level has no roads
|
||||||
|
|
||||||
// Level ID takes highest precedence
|
// Level ID takes highest precedence
|
||||||
if (levelId >= 41 && levelId <= 46) {
|
if (levelId >= 41 && levelId <= 46) {
|
||||||
stampAct2DesertRoads(canvas, gridWidth, gridHeight, inX, outY, placedRects, rng, openGates)
|
stampAct2DesertRoads(canvas, gridWidth, gridHeight, inX, outY, placedRects, rng, openGates, stats)
|
||||||
return
|
return
|
||||||
} else if (levelId >= 79 && levelId <= 82) {
|
} else if (levelId >= 79 && levelId <= 82) {
|
||||||
stampAct3KurastAvenues(canvas, gridWidth, gridHeight, inX, outY, placedRects, rng, openGates)
|
stampAct3KurastAvenues(canvas, gridWidth, gridHeight, inX, outY, placedRects, rng, openGates, stats)
|
||||||
return
|
return
|
||||||
} else if (levelId === 111 || levelId === 112 || levelId === 117) {
|
} else if (levelId === 111 || levelId === 112 || levelId === 117) {
|
||||||
stampAct5SnowRoads(canvas, gridWidth, gridHeight, inX, outY, placedRects, rng, openGates)
|
stampAct5SnowRoads(canvas, gridWidth, gridHeight, inX, outY, placedRects, rng, openGates, stats)
|
||||||
return
|
return
|
||||||
} else if (levelId >= 2 && levelId <= 7) {
|
} else if (levelId >= 2 && levelId <= 7) {
|
||||||
stampAct1DirtRoads(canvas, gridWidth, gridHeight, inX, outY, placedRects, rng, openGates)
|
stampAct1DirtRoads(canvas, gridWidth, gridHeight, inX, outY, placedRects, rng, openGates, stats)
|
||||||
return
|
return
|
||||||
// Level type name takes second precedence
|
// Level type name takes second precedence
|
||||||
} else if (levelTypeName === 'Act 2 - Desert') {
|
} else if (levelTypeName === 'Act 2 - Desert') {
|
||||||
stampAct2DesertRoads(canvas, gridWidth, gridHeight, inX, outY, placedRects, rng, openGates)
|
stampAct2DesertRoads(canvas, gridWidth, gridHeight, inX, outY, placedRects, rng, openGates, stats)
|
||||||
return
|
return
|
||||||
} else if (levelTypeName === 'Act 3 - Kurast') {
|
} else if (levelTypeName === 'Act 3 - Kurast') {
|
||||||
stampAct3KurastAvenues(canvas, gridWidth, gridHeight, inX, outY, placedRects, rng, openGates)
|
stampAct3KurastAvenues(canvas, gridWidth, gridHeight, inX, outY, placedRects, rng, openGates, stats)
|
||||||
return
|
return
|
||||||
} else if (levelTypeName === 'Act 5 - Barricade' || levelTypeName === 'Act 5 - Siege') {
|
} else if (levelTypeName === 'Act 5 - Barricade' || levelTypeName === 'Act 5 - Siege') {
|
||||||
stampAct5SnowRoads(canvas, gridWidth, gridHeight, inX, outY, placedRects, rng, openGates)
|
stampAct5SnowRoads(canvas, gridWidth, gridHeight, inX, outY, placedRects, rng, openGates, stats)
|
||||||
return
|
return
|
||||||
} else if (levelTypeName === 'Act 1 - Wilderness') {
|
} else if (levelTypeName === 'Act 1 - Wilderness') {
|
||||||
stampAct1DirtRoads(canvas, gridWidth, gridHeight, inX, outY, placedRects, rng, openGates)
|
stampAct1DirtRoads(canvas, gridWidth, gridHeight, inX, outY, placedRects, rng, openGates, stats)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Unified actOfLevel fallback
|
// Unified actOfLevel fallback
|
||||||
const effectiveAct = actOfLevel(levelId, levelTypeName, act, levelsTable)
|
const effectiveAct = actOfLevel(levelId, levelTypeName, act, levelsTable)
|
||||||
if (effectiveAct === 2) {
|
if (effectiveAct === 2) {
|
||||||
stampAct2DesertRoads(canvas, gridWidth, gridHeight, inX, outY, placedRects, rng, openGates)
|
stampAct2DesertRoads(canvas, gridWidth, gridHeight, inX, outY, placedRects, rng, openGates, stats)
|
||||||
} else if (effectiveAct === 3) {
|
} else if (effectiveAct === 3) {
|
||||||
stampAct3KurastAvenues(canvas, gridWidth, gridHeight, inX, outY, placedRects, rng, openGates)
|
stampAct3KurastAvenues(canvas, gridWidth, gridHeight, inX, outY, placedRects, rng, openGates, stats)
|
||||||
} else if (effectiveAct === 5) {
|
} else if (effectiveAct === 5) {
|
||||||
stampAct5SnowRoads(canvas, gridWidth, gridHeight, inX, outY, placedRects, rng, openGates)
|
stampAct5SnowRoads(canvas, gridWidth, gridHeight, inX, outY, placedRects, rng, openGates, stats)
|
||||||
} else if (effectiveAct === 1) {
|
} else if (effectiveAct === 1) {
|
||||||
stampAct1DirtRoads(canvas, gridWidth, gridHeight, inX, outY, placedRects, rng, openGates)
|
stampAct1DirtRoads(canvas, gridWidth, gridHeight, inX, outY, placedRects, rng, openGates, stats)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -3722,49 +3767,50 @@ export function stampOutdoorDividers(
|
||||||
rng: Rng,
|
rng: Rng,
|
||||||
levelTypeName?: string,
|
levelTypeName?: string,
|
||||||
levelsTable?: D2Table,
|
levelsTable?: D2Table,
|
||||||
|
stats?: WildernessStats,
|
||||||
): void {
|
): void {
|
||||||
if (levelId === 39) return // Cow Level has no dividers
|
if (levelId === 39) return // Cow Level has no dividers
|
||||||
|
|
||||||
// Level ID takes highest precedence
|
// Level ID takes highest precedence
|
||||||
if (levelId >= 41 && levelId <= 46) {
|
if (levelId >= 41 && levelId <= 46) {
|
||||||
stampAct2SandstoneBerms(canvas, gridWidth, gridHeight, inX, outY, placedRects, rng)
|
stampAct2SandstoneBerms(canvas, gridWidth, gridHeight, inX, outY, placedRects, rng, stats)
|
||||||
return
|
return
|
||||||
} else if (levelId >= 79 && levelId <= 82) {
|
} else if (levelId >= 79 && levelId <= 82) {
|
||||||
stampAct3KurastBalustrades(canvas, gridWidth, gridHeight, inX, outY, placedRects, rng)
|
stampAct3KurastBalustrades(canvas, gridWidth, gridHeight, inX, outY, placedRects, rng, stats)
|
||||||
return
|
return
|
||||||
} else if (levelId === 111 || levelId === 112 || levelId === 117) {
|
} else if (levelId === 111 || levelId === 112 || levelId === 117) {
|
||||||
stampAct5FrozenTrailDivides(canvas, gridWidth, gridHeight, inX, outY, placedRects, rng)
|
stampAct5FrozenTrailDivides(canvas, gridWidth, gridHeight, inX, outY, placedRects, rng, stats)
|
||||||
return
|
return
|
||||||
} else if (levelId >= 2 && levelId <= 7) {
|
} else if (levelId >= 2 && levelId <= 7) {
|
||||||
stampAct1InteriorStoneWalls(canvas, gridWidth, gridHeight, inX, outY, placedRects, rng)
|
stampAct1InteriorStoneWalls(canvas, gridWidth, gridHeight, inX, outY, placedRects, rng, stats)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Level type name takes second precedence
|
// Level type name takes second precedence
|
||||||
if (levelTypeName === 'Act 2 - Desert') {
|
if (levelTypeName === 'Act 2 - Desert') {
|
||||||
stampAct2SandstoneBerms(canvas, gridWidth, gridHeight, inX, outY, placedRects, rng)
|
stampAct2SandstoneBerms(canvas, gridWidth, gridHeight, inX, outY, placedRects, rng, stats)
|
||||||
return
|
return
|
||||||
} else if (levelTypeName === 'Act 3 - Kurast') {
|
} else if (levelTypeName === 'Act 3 - Kurast') {
|
||||||
stampAct3KurastBalustrades(canvas, gridWidth, gridHeight, inX, outY, placedRects, rng)
|
stampAct3KurastBalustrades(canvas, gridWidth, gridHeight, inX, outY, placedRects, rng, stats)
|
||||||
return
|
return
|
||||||
} else if (levelTypeName === 'Act 5 - Barricade' || levelTypeName === 'Act 5 - Siege') {
|
} else if (levelTypeName === 'Act 5 - Barricade' || levelTypeName === 'Act 5 - Siege') {
|
||||||
stampAct5FrozenTrailDivides(canvas, gridWidth, gridHeight, inX, outY, placedRects, rng)
|
stampAct5FrozenTrailDivides(canvas, gridWidth, gridHeight, inX, outY, placedRects, rng, stats)
|
||||||
return
|
return
|
||||||
} else if (levelTypeName === 'Act 1 - Wilderness') {
|
} else if (levelTypeName === 'Act 1 - Wilderness') {
|
||||||
stampAct1InteriorStoneWalls(canvas, gridWidth, gridHeight, inX, outY, placedRects, rng)
|
stampAct1InteriorStoneWalls(canvas, gridWidth, gridHeight, inX, outY, placedRects, rng, stats)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Unified actOfLevel fallback
|
// Unified actOfLevel fallback
|
||||||
const effectiveAct = actOfLevel(levelId, levelTypeName, act, levelsTable)
|
const effectiveAct = actOfLevel(levelId, levelTypeName, act, levelsTable)
|
||||||
if (effectiveAct === 2) {
|
if (effectiveAct === 2) {
|
||||||
stampAct2SandstoneBerms(canvas, gridWidth, gridHeight, inX, outY, placedRects, rng)
|
stampAct2SandstoneBerms(canvas, gridWidth, gridHeight, inX, outY, placedRects, rng, stats)
|
||||||
} else if (effectiveAct === 3) {
|
} else if (effectiveAct === 3) {
|
||||||
stampAct3KurastBalustrades(canvas, gridWidth, gridHeight, inX, outY, placedRects, rng)
|
stampAct3KurastBalustrades(canvas, gridWidth, gridHeight, inX, outY, placedRects, rng, stats)
|
||||||
} else if (effectiveAct === 5) {
|
} else if (effectiveAct === 5) {
|
||||||
stampAct5FrozenTrailDivides(canvas, gridWidth, gridHeight, inX, outY, placedRects, rng)
|
stampAct5FrozenTrailDivides(canvas, gridWidth, gridHeight, inX, outY, placedRects, rng, stats)
|
||||||
} else if (effectiveAct === 1) {
|
} else if (effectiveAct === 1) {
|
||||||
stampAct1InteriorStoneWalls(canvas, gridWidth, gridHeight, inX, outY, placedRects, rng)
|
stampAct1InteriorStoneWalls(canvas, gridWidth, gridHeight, inX, outY, placedRects, rng, stats)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -5070,6 +5116,10 @@ export function generateWilderness(request: WildernessRequest): WildernessResult
|
||||||
anchors: 0,
|
anchors: 0,
|
||||||
specialPresets: [],
|
specialPresets: [],
|
||||||
entrances: [],
|
entrances: [],
|
||||||
|
proceduralRoadCells: 0,
|
||||||
|
proceduralDividerCells: 0,
|
||||||
|
proceduralWaterCells: 0,
|
||||||
|
proceduralWallCells: 0,
|
||||||
substitutions: [],
|
substitutions: [],
|
||||||
substitutedClusters: 0,
|
substitutedClusters: 0,
|
||||||
objects: canvas.objects.length,
|
objects: canvas.objects.length,
|
||||||
|
|
@ -5087,6 +5137,7 @@ export function generateWilderness(request: WildernessRequest): WildernessResult
|
||||||
substitutions: [], borderPieces: {}, unresolved: [], notes: [],
|
substitutions: [], borderPieces: {}, unresolved: [], notes: [],
|
||||||
borderStamped: 0, groundCells: 0, groundTile: null, groundTiles: undefined, sizeSource,
|
borderStamped: 0, groundCells: 0, groundTile: null, groundTiles: undefined, sizeSource,
|
||||||
roadCells: 0, roadSegments: 0, anchors: 0, specialPresets: [], entrances: [],
|
roadCells: 0, roadSegments: 0, anchors: 0, specialPresets: [], entrances: [],
|
||||||
|
proceduralRoadCells: 0, proceduralDividerCells: 0, proceduralWaterCells: 0, proceduralWallCells: 0,
|
||||||
}
|
}
|
||||||
|
|
||||||
let wallLayers = 1
|
let wallLayers = 1
|
||||||
|
|
@ -5142,7 +5193,7 @@ export function generateWilderness(request: WildernessRequest): WildernessResult
|
||||||
if (request.levelId === 83) {
|
if (request.levelId === 83) {
|
||||||
placedSpecial = stampAct3Travincal(canvas, request.pieces, stats, specialPlacedRects)
|
placedSpecial = stampAct3Travincal(canvas, request.pieces, stats, specialPlacedRects)
|
||||||
} else if (request.levelId === 82) {
|
} else if (request.levelId === 82) {
|
||||||
placedSpecial = stampAct3KurastCauseway(canvas, request.pieces, stats)
|
placedSpecial = stampAct3KurastCauseway(canvas, request.pieces, stats, specialPlacedRects)
|
||||||
} else if (request.levelId >= 76 && request.levelId <= 78) {
|
} else if (request.levelId >= 76 && request.levelId <= 78) {
|
||||||
placedSpecial = stampAct3Jungle(canvas, request.levelId, request.pieces, rng, stats, specialPlacedRects)
|
placedSpecial = stampAct3Jungle(canvas, request.levelId, request.pieces, rng, stats, specialPlacedRects)
|
||||||
const populationRng = new Rng(request.seed ^ 0x6c07)
|
const populationRng = new Rng(request.seed ^ 0x6c07)
|
||||||
|
|
@ -5211,6 +5262,10 @@ export function generateWilderness(request: WildernessRequest): WildernessResult
|
||||||
anchors: stats.anchors,
|
anchors: stats.anchors,
|
||||||
entrances: stats.entrances,
|
entrances: stats.entrances,
|
||||||
specialPresets: stats.specialPresets,
|
specialPresets: stats.specialPresets,
|
||||||
|
proceduralRoadCells: stats.proceduralRoadCells ?? 0,
|
||||||
|
proceduralDividerCells: stats.proceduralDividerCells ?? 0,
|
||||||
|
proceduralWaterCells: stats.proceduralWaterCells ?? 0,
|
||||||
|
proceduralWallCells: stats.proceduralWallCells ?? 0,
|
||||||
substitutions: stats.substitutions,
|
substitutions: stats.substitutions,
|
||||||
substitutedClusters: placedSpecial.size,
|
substitutedClusters: placedSpecial.size,
|
||||||
objects: canvas.objects.length,
|
objects: canvas.objects.length,
|
||||||
|
|
@ -5278,6 +5333,7 @@ export function generateWilderness(request: WildernessRequest): WildernessResult
|
||||||
dirtPathGrid = roadRes.dirtPathGrid
|
dirtPathGrid = roadRes.dirtPathGrid
|
||||||
stats.roadCells = roadRes.roadCells
|
stats.roadCells = roadRes.roadCells
|
||||||
stats.roadSegments = roadRes.roadSegments
|
stats.roadSegments = roadRes.roadSegments
|
||||||
|
stats.proceduralRoadCells = (stats.proceduralRoadCells ?? 0) + roadRes.roadCells
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -5434,6 +5490,7 @@ export function generateWilderness(request: WildernessRequest): WildernessResult
|
||||||
request.levelTypeName,
|
request.levelTypeName,
|
||||||
act1Plan?.openGates,
|
act1Plan?.openGates,
|
||||||
request.levelsTable,
|
request.levelsTable,
|
||||||
|
stats,
|
||||||
)
|
)
|
||||||
stampOutdoorDividers(
|
stampOutdoorDividers(
|
||||||
canvas,
|
canvas,
|
||||||
|
|
@ -5447,6 +5504,7 @@ export function generateWilderness(request: WildernessRequest): WildernessResult
|
||||||
rng,
|
rng,
|
||||||
request.levelTypeName,
|
request.levelTypeName,
|
||||||
request.levelsTable,
|
request.levelsTable,
|
||||||
|
stats,
|
||||||
)
|
)
|
||||||
|
|
||||||
// Act 2 Desert Topography: Canyons, oasis pools, sand caravan trails, tomb entrances (DRLGOUTDESR_InitAct2OutdoorLevel)
|
// Act 2 Desert Topography: Canyons, oasis pools, sand caravan trails, tomb entrances (DRLGOUTDESR_InitAct2OutdoorLevel)
|
||||||
|
|
@ -5569,6 +5627,10 @@ export function generateWilderness(request: WildernessRequest): WildernessResult
|
||||||
act3Topography: stats.act3Topography,
|
act3Topography: stats.act3Topography,
|
||||||
act4Topography: stats.act4Topography,
|
act4Topography: stats.act4Topography,
|
||||||
act5Topography: stats.act5Topography,
|
act5Topography: stats.act5Topography,
|
||||||
|
proceduralRoadCells: stats.proceduralRoadCells ?? 0,
|
||||||
|
proceduralDividerCells: stats.proceduralDividerCells ?? 0,
|
||||||
|
proceduralWaterCells: stats.proceduralWaterCells ?? 0,
|
||||||
|
proceduralWallCells: stats.proceduralWallCells ?? 0,
|
||||||
cells: canvas.width * canvas.height,
|
cells: canvas.width * canvas.height,
|
||||||
unresolved: stats.unresolved,
|
unresolved: stats.unresolved,
|
||||||
unimplementedPasses: UNIMPLEMENTED_PASSES,
|
unimplementedPasses: UNIMPLEMENTED_PASSES,
|
||||||
|
|
|
||||||
|
|
@ -13,15 +13,21 @@ import type {
|
||||||
} from '../src/game/wilderness-acts.ts'
|
} from '../src/game/wilderness-acts.ts'
|
||||||
import type { Ds1, Ds1Cell } from '../src/formats/ds1.ts'
|
import type { Ds1, Ds1Cell } from '../src/formats/ds1.ts'
|
||||||
|
|
||||||
function makeMockDs1(width: number, height: number, hasGapInWalls: boolean = false): Ds1 {
|
function makeMockDs1(
|
||||||
|
width: number,
|
||||||
|
height: number,
|
||||||
|
hasGapInWalls: boolean = false,
|
||||||
|
floorStyle: number = 1,
|
||||||
|
wallStyle: number = 1,
|
||||||
|
): Ds1 {
|
||||||
const cells: Ds1Cell[][] = []
|
const cells: Ds1Cell[][] = []
|
||||||
for (let y = 0; y < height; y += 1) {
|
for (let y = 0; y < height; y += 1) {
|
||||||
const row: Ds1Cell[] = []
|
const row: Ds1Cell[] = []
|
||||||
for (let x = 0; x < width; x += 1) {
|
for (let x = 0; x < width; x += 1) {
|
||||||
const isGap = hasGapInWalls && x === Math.floor(width / 2)
|
const isGap = hasGapInWalls && x === Math.floor(width / 2)
|
||||||
row.push({
|
row.push({
|
||||||
walls: isGap ? [] : [{ prop1: 2, sequence: 0, style: 1, type: 0, unknown1: 0, unknown2: 0, hidden: false }],
|
walls: isGap ? [] : [{ prop1: 2, sequence: 0, style: wallStyle, type: 0, unknown1: 0, unknown2: 0, hidden: false }],
|
||||||
floors: [{ prop1: 2, sequence: 0, style: 1, unknown1: 0, unknown2: 0, hidden: false }],
|
floors: [{ prop1: 2, sequence: 0, style: floorStyle, unknown1: 0, unknown2: 0, hidden: false }],
|
||||||
shadows: [],
|
shadows: [],
|
||||||
substitutions: [],
|
substitutions: [],
|
||||||
})
|
})
|
||||||
|
|
@ -231,9 +237,9 @@ describe('Wilderness Outdoor Initializers for Acts 2-5 (Issue #51)', () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
test('Kurast Causeway (82) stamps causeway with central walkway, balustrades, and canal water', () => {
|
test('Kurast Causeway (82) stamps causeway with central walkway, balustrades, and canal water', () => {
|
||||||
const bridgeDs1 = makeMockDs1(48, 16)
|
const groundDs1 = makeMockDs1(48, 16)
|
||||||
const pieces: WildernessPiece[] = [
|
const pieces: WildernessPiece[] = [
|
||||||
{ name: 'Act 3 - Bridge', border: false, levels: [bridgeDs1] },
|
{ name: 'Act 3 - Ground', border: false, levels: [groundDs1] },
|
||||||
]
|
]
|
||||||
|
|
||||||
const result = generateWilderness({
|
const result = generateWilderness({
|
||||||
|
|
@ -271,6 +277,38 @@ describe('Wilderness Outdoor Initializers for Acts 2-5 (Issue #51)', () => {
|
||||||
expect(foundCanalWater).toBe(true)
|
expect(foundCanalWater).toBe(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('Kurast Causeway (82) preserves preset when Act 3 - Bridge is provided', () => {
|
||||||
|
const bridgeDs1 = makeMockDs1(48, 16, false, 29, 31)
|
||||||
|
const pieces: WildernessPiece[] = [
|
||||||
|
{ name: 'Act 3 - Bridge', border: false, levels: [bridgeDs1] },
|
||||||
|
]
|
||||||
|
|
||||||
|
const result = generateWilderness({
|
||||||
|
levelId: 82,
|
||||||
|
levelName: 'Kurast Causeway',
|
||||||
|
levelTypeName: 'Act 3 - Kurast',
|
||||||
|
sizeX: 48,
|
||||||
|
sizeY: 16,
|
||||||
|
subType: 0,
|
||||||
|
subTheme: 0,
|
||||||
|
seed: 82001,
|
||||||
|
pieces,
|
||||||
|
substitutions: [],
|
||||||
|
})
|
||||||
|
|
||||||
|
let foundPresetFloor = false
|
||||||
|
let foundPresetWall = false
|
||||||
|
for (let y = 0; y < result.level.height; y += 1) {
|
||||||
|
for (let x = 0; x < result.level.width; x += 1) {
|
||||||
|
const cell = result.level.cells[y]![x]!
|
||||||
|
if (cell.floors.some(f => f.style === 29)) foundPresetFloor = true
|
||||||
|
if (cell.walls.some(w => w.style === 31)) foundPresetWall = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
expect(foundPresetFloor).toBe(true)
|
||||||
|
expect(foundPresetWall).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
test('Kurast Bazaar (79) generates paved stone avenues, canals, and stone bridges', () => {
|
test('Kurast Bazaar (79) generates paved stone avenues, canals, and stone bridges', () => {
|
||||||
const pieces = makeMockBorderPieces('Act 3')
|
const pieces = makeMockBorderPieces('Act 3')
|
||||||
const result = generateWilderness({
|
const result = generateWilderness({
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ import {
|
||||||
} from '../src/game/wilderness.ts'
|
} from '../src/game/wilderness.ts'
|
||||||
import type { WildernessPiece, PlacedPresetRect, Canvas } from '../src/game/wilderness.ts'
|
import type { WildernessPiece, PlacedPresetRect, Canvas } from '../src/game/wilderness.ts'
|
||||||
import { Rng } from '../src/game/rng.ts'
|
import { Rng } from '../src/game/rng.ts'
|
||||||
import type { Ds1, Ds1Cell } from '../src/formats/ds1.ts'
|
import type { Ds1, Ds1Cell, Ds1Wall } from '../src/formats/ds1.ts'
|
||||||
|
|
||||||
function makeMockDs1(width: number, height: number): Ds1 {
|
function makeMockDs1(width: number, height: number): Ds1 {
|
||||||
const cells: Ds1Cell[][] = []
|
const cells: Ds1Cell[][] = []
|
||||||
|
|
@ -412,5 +412,33 @@ describe('Multi-Act Outdoor Roads & Boundary Divider Networks', () => {
|
||||||
expect(roadFloors).toBe(0)
|
expect(roadFloors).toBe(0)
|
||||||
expect(dividerWalls).toBe(0)
|
expect(dividerWalls).toBe(0)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('stampThemedRoads preserves preset walls on the highway spine', () => {
|
||||||
|
const canvas = createCanvas(width, height, 1, 1, 0)
|
||||||
|
const rng = new Rng(42)
|
||||||
|
const hubX = inX * TILES_PER_BLOCK + 4
|
||||||
|
const hubY = outY * TILES_PER_BLOCK + 4
|
||||||
|
// Preset directly intersects the road spine
|
||||||
|
const placedRects: PlacedPresetRect[] = [
|
||||||
|
{ name: 'Act 1 - Ruin', x: hubX - 2, y: hubY - 2, w: 5, h: 5 },
|
||||||
|
]
|
||||||
|
// Paint preset wall inside the preset on the highway spine
|
||||||
|
const targetCell = canvas.cells[hubY]?.[hubX]
|
||||||
|
expect(targetCell).toBeDefined()
|
||||||
|
;(targetCell!.walls as Ds1Wall[]).push({
|
||||||
|
prop1: 100,
|
||||||
|
sequence: 0,
|
||||||
|
style: 99,
|
||||||
|
type: 1,
|
||||||
|
unknown1: 0,
|
||||||
|
unknown2: 0,
|
||||||
|
hidden: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
stampAct1DirtRoads(canvas, gridW, gridH, inX, outY, placedRects, rng)
|
||||||
|
|
||||||
|
// The wall inside the preset must NOT have been wiped
|
||||||
|
expect(targetCell!.walls.some(w => w.style === 99 && w.prop1 === 100)).toBe(true)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -415,4 +415,59 @@ describe("Act 5 Bloody Foothills Dynamic Siege Generation (DRLGOUTSIEGE)", () =>
|
||||||
expect(notes).toContain("generated dynamic siege trenches and fortifications")
|
expect(notes).toContain("generated dynamic siege trenches and fortifications")
|
||||||
expect(result.stats.siegeTopography).toBeDefined()
|
expect(result.stats.siegeTopography).toBeDefined()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test("generates varied obstacle layouts with different seeds (seed diversity)", () => {
|
||||||
|
const pieces = createMockSiegePieces()
|
||||||
|
const result1 = generateWilderness({
|
||||||
|
levelId: 110,
|
||||||
|
levelName: "Bloody Foothills",
|
||||||
|
levelTypeName: "Act 5 - Siege",
|
||||||
|
sizeX: 240,
|
||||||
|
sizeY: 48,
|
||||||
|
subType: -1,
|
||||||
|
subTheme: -1,
|
||||||
|
seed: 11001,
|
||||||
|
pieces,
|
||||||
|
substitutions: [],
|
||||||
|
})
|
||||||
|
|
||||||
|
const result2 = generateWilderness({
|
||||||
|
levelId: 110,
|
||||||
|
levelName: "Bloody Foothills",
|
||||||
|
levelTypeName: "Act 5 - Siege",
|
||||||
|
sizeX: 240,
|
||||||
|
sizeY: 48,
|
||||||
|
subType: -1,
|
||||||
|
subTheme: -1,
|
||||||
|
seed: 11002,
|
||||||
|
pieces,
|
||||||
|
substitutions: [],
|
||||||
|
})
|
||||||
|
|
||||||
|
// Collect obstacle wall coordinates for both seeds
|
||||||
|
const walls1 = new Set<string>()
|
||||||
|
const walls2 = new Set<string>()
|
||||||
|
|
||||||
|
for (let y = 0; y < result1.level.height; y += 1) {
|
||||||
|
for (let x = 0; x < result1.level.width; x += 1) {
|
||||||
|
if (result1.level.cells[y]![x]!.walls.some(w => !w.hidden && w.prop1 !== 0)) {
|
||||||
|
walls1.add(`${x},${y}`)
|
||||||
|
}
|
||||||
|
if (result2.level.cells[y]![x]!.walls.some(w => !w.hidden && w.prop1 !== 0)) {
|
||||||
|
walls2.add(`${x},${y}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Both should have placed walls
|
||||||
|
expect(walls1.size).toBeGreaterThan(0)
|
||||||
|
expect(walls2.size).toBeGreaterThan(0)
|
||||||
|
|
||||||
|
// The obstacle coordinates must differ across different seeds
|
||||||
|
let diffCount = 0
|
||||||
|
for (const coord of walls1) {
|
||||||
|
if (!walls2.has(coord)) diffCount += 1
|
||||||
|
}
|
||||||
|
expect(diffCount).toBeGreaterThan(0)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -43,6 +43,10 @@ function makeStats(): WildernessStats {
|
||||||
anchors: 0,
|
anchors: 0,
|
||||||
specialPresets: [],
|
specialPresets: [],
|
||||||
entrances: [],
|
entrances: [],
|
||||||
|
proceduralRoadCells: 0,
|
||||||
|
proceduralDividerCells: 0,
|
||||||
|
proceduralWaterCells: 0,
|
||||||
|
proceduralWallCells: 0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -276,3 +280,109 @@ describe('BordType 0 really is one cluster for the whole level', () => {
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('Procedural terrain pass telemetry tracks real cell counts (Issue #62)', () => {
|
||||||
|
test('Act 1 wilderness tracks procedural road and divider wall cells matching canvas', () => {
|
||||||
|
const result = generateWilderness({
|
||||||
|
levelId: 2,
|
||||||
|
levelName: 'Cold Plains',
|
||||||
|
levelTypeName: 'Act 1 - Wilderness',
|
||||||
|
sizeX: 64,
|
||||||
|
sizeY: 64,
|
||||||
|
subType: 1,
|
||||||
|
subTheme: 0,
|
||||||
|
seed: 2001,
|
||||||
|
pieces: makeMockBorderPieces('Act 1'),
|
||||||
|
substitutions: [],
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(result.stats.proceduralRoadCells).toBeGreaterThan(0)
|
||||||
|
expect(result.stats.proceduralDividerCells).toBeGreaterThan(0)
|
||||||
|
expect(result.stats.proceduralWallCells).toBe(result.stats.proceduralDividerCells)
|
||||||
|
|
||||||
|
// Count actual divider wall cells on canvas (walls with style 2, prop1 129)
|
||||||
|
let wallCount = 0
|
||||||
|
for (let y = 0; y < result.level.height; y += 1) {
|
||||||
|
for (let x = 0; x < result.level.width; x += 1) {
|
||||||
|
const cell = result.level.cells[y]![x]!
|
||||||
|
if (cell.walls.some(w => !w.hidden && w.style === 2 && w.prop1 === 129)) {
|
||||||
|
wallCount += 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
expect(result.stats.proceduralWallCells).toBe(wallCount)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('Act 2 desert tracks procedural roads, water (oasis), and walls (canyon + berms)', () => {
|
||||||
|
const rocky = generateWilderness({
|
||||||
|
levelId: 41,
|
||||||
|
levelName: 'Rocky Waste',
|
||||||
|
levelTypeName: 'Act 2 - Desert',
|
||||||
|
sizeX: 64,
|
||||||
|
sizeY: 64,
|
||||||
|
subType: 0,
|
||||||
|
subTheme: 0,
|
||||||
|
seed: 41001,
|
||||||
|
pieces: makeMockBorderPieces('Act 2'),
|
||||||
|
substitutions: [],
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(rocky.stats.proceduralRoadCells).toBeGreaterThan(0)
|
||||||
|
expect(rocky.stats.proceduralWallCells).toBeGreaterThan(0)
|
||||||
|
|
||||||
|
const oasis = generateWilderness({
|
||||||
|
levelId: 43,
|
||||||
|
levelName: 'Far Oasis',
|
||||||
|
levelTypeName: 'Act 2 - Desert',
|
||||||
|
sizeX: 64,
|
||||||
|
sizeY: 64,
|
||||||
|
subType: 0,
|
||||||
|
subTheme: 0,
|
||||||
|
seed: 43001,
|
||||||
|
pieces: makeMockBorderPieces('Act 2'),
|
||||||
|
substitutions: [],
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(oasis.stats.proceduralRoadCells).toBeGreaterThan(0)
|
||||||
|
expect(oasis.stats.proceduralWaterCells).toBeGreaterThan(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('Act 3 Kurast Causeway tracks causeway, canal, and balustrade cells', () => {
|
||||||
|
const result = generateWilderness({
|
||||||
|
levelId: 82,
|
||||||
|
levelName: 'Kurast Causeway',
|
||||||
|
levelTypeName: 'Act 3 - Kurast',
|
||||||
|
sizeX: 32,
|
||||||
|
sizeY: 64,
|
||||||
|
subType: 0,
|
||||||
|
subTheme: 0,
|
||||||
|
seed: 82001,
|
||||||
|
pieces: [{ name: 'Act 3 - Bridge', border: false, levels: [makeMockDs1(16, 16)] }],
|
||||||
|
substitutions: [],
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(result.stats.proceduralRoadCells).toBeGreaterThan(0)
|
||||||
|
expect(result.stats.proceduralWaterCells).toBeGreaterThan(0)
|
||||||
|
expect(result.stats.proceduralWallCells).toBeGreaterThan(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('Act 5 Siege levels track barricade walls, trenches, and frozen lakes', () => {
|
||||||
|
const result = generateWilderness({
|
||||||
|
levelId: 112,
|
||||||
|
levelName: 'Arreat Plateau',
|
||||||
|
levelTypeName: 'Act 5 - Barricade',
|
||||||
|
sizeX: 80,
|
||||||
|
sizeY: 80,
|
||||||
|
subType: 0,
|
||||||
|
subTheme: 0,
|
||||||
|
seed: 112001,
|
||||||
|
pieces: makeMockBorderPieces('Act 5'),
|
||||||
|
substitutions: [],
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(result.stats.proceduralWallCells).toBeGreaterThan(0)
|
||||||
|
expect(result.stats.proceduralRoadCells).toBeGreaterThan(0)
|
||||||
|
expect(result.stats.proceduralWaterCells).toBeGreaterThan(0)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue