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.proceduralWallCells = (stats.proceduralWallCells ?? 0) + canyonWalls
|
||||
stats.proceduralRoadCells = (stats.proceduralRoadCells ?? 0) + sandTrailCells
|
||||
stats.proceduralWaterCells = (stats.proceduralWaterCells ?? 0) + oasisPoolCells
|
||||
stats.notes.push(
|
||||
`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
|
||||
* ------------------------------------------------------------------------- */
|
||||
|
||||
/**
|
||||
* 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):
|
||||
|
|
@ -535,6 +421,7 @@ export function stampAct3KurastCauseway(
|
|||
canvas: Canvas,
|
||||
pieces: readonly WildernessPiece[],
|
||||
stats: WildernessStats,
|
||||
specialPlacedRects?: PlacedPresetRect[],
|
||||
): Set<string> {
|
||||
const placed = new Set<string>()
|
||||
const piece = pieces.find(p => p.name === 'Act 3 - Bridge')
|
||||
|
|
@ -544,17 +431,36 @@ export function stampAct3KurastCauseway(
|
|||
stampDs1(canvas, ds1, 0, 0)
|
||||
placed.add('Act 3 - Bridge')
|
||||
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:
|
||||
// Central paved walkway flanked by canal water and balustrades
|
||||
let causewayCells = 0
|
||||
let canalCells = 0
|
||||
let balustradeCells = 0
|
||||
const walkwayLeft = Math.max(2, Math.floor((canvas.width - 8) / 2))
|
||||
const walkwayRight = walkwayLeft + 7
|
||||
|
||||
for (let y = 0; y < canvas.height; y += 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) {
|
||||
// Paved stone causeway: style 5, prop1 194
|
||||
paintFloor(canvas, x, y, makeFloor(5, 0, 194), true)
|
||||
|
|
@ -563,6 +469,7 @@ export function stampAct3KurastCauseway(
|
|||
} else if (x === walkwayLeft - 1 || x === walkwayRight + 1) {
|
||||
// Stone balustrade wall: style 1, prop1 129
|
||||
paintWall(canvas, x, y, makeWall(1, 0, 1, 129))
|
||||
balustradeCells += 1
|
||||
} else {
|
||||
// Deep canal water: style 2, sequence 0, prop1 2
|
||||
paintFloor(canvas, x, y, makeFloor(2, 0, 2), true)
|
||||
|
|
@ -578,6 +485,9 @@ export function stampAct3KurastCauseway(
|
|||
canalCells,
|
||||
junglePathCells: 0,
|
||||
}
|
||||
stats.proceduralRoadCells = (stats.proceduralRoadCells ?? 0) + causewayCells
|
||||
stats.proceduralWaterCells = (stats.proceduralWaterCells ?? 0) + canalCells
|
||||
stats.proceduralWallCells = (stats.proceduralWallCells ?? 0) + balustradeCells
|
||||
stats.notes.push(
|
||||
`Act 3 Causeway Topography: ${String(causewayCells)} causeway cells, ${String(canalCells)} canal water cells`,
|
||||
)
|
||||
|
|
@ -605,6 +515,7 @@ export function initAct3OutdoorLevel(
|
|||
let causewayCells = 0
|
||||
let riverBridges = 0
|
||||
let canalCells = 0
|
||||
let embankmentWalls = 0
|
||||
const junglePathCells = 0
|
||||
|
||||
const hubX = inX * TILES_PER_BLOCK + 4
|
||||
|
|
@ -656,10 +567,14 @@ export function initAct3OutdoorLevel(
|
|||
// Stone canal embankments
|
||||
paintWall(canvas, canalX - 1, 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.proceduralRoadCells = (stats.proceduralRoadCells ?? 0) + causewayCells
|
||||
stats.proceduralWaterCells = (stats.proceduralWaterCells ?? 0) + canalCells
|
||||
stats.proceduralWallCells = (stats.proceduralWallCells ?? 0) + embankmentWalls
|
||||
stats.notes.push(
|
||||
`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.proceduralWaterCells = (stats.proceduralWaterCells ?? 0) + lavaChasmCells
|
||||
stats.proceduralRoadCells = (stats.proceduralRoadCells ?? 0) + rockyBridges
|
||||
stats.notes.push(
|
||||
`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.proceduralWallCells = (stats.proceduralWallCells ?? 0) + barricadeWalls
|
||||
stats.proceduralRoadCells = (stats.proceduralRoadCells ?? 0) + trenchCells
|
||||
stats.proceduralWaterCells = (stats.proceduralWaterCells ?? 0) + frozenLakeCells
|
||||
stats.notes.push(
|
||||
`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 { 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. */
|
||||
export interface JungleZoneConnection {
|
||||
|
|
@ -242,6 +242,7 @@ function synthesizeBridgePlanks(
|
|||
bridgeX: number,
|
||||
bridgeY: number,
|
||||
orientation: 'horizontal' | 'vertical',
|
||||
bridgeDs1?: Ds1,
|
||||
): void {
|
||||
const makeBridgeFloor = (): Ds1Floor => ({
|
||||
prop1: 2,
|
||||
|
|
@ -261,6 +262,14 @@ function synthesizeBridgePlanks(
|
|||
if (cy >= 0 && cy < canvas.height && cx >= 0 && cx < canvas.width) {
|
||||
const row = canvas.cells[cy]
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
|
@ -274,6 +283,14 @@ function synthesizeBridgePlanks(
|
|||
if (cy >= 0 && cy < canvas.height && cx >= 0 && cx < canvas.width) {
|
||||
const row = canvas.cells[cy]
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
|
@ -343,6 +360,7 @@ export function DRLG_GenerateJungles(
|
|||
rng: Rng,
|
||||
stats: WildernessStats,
|
||||
stampDs1Fn: (canvas: Canvas, source: Ds1, originX: number, originY: number) => number,
|
||||
placedRects?: PlacedPresetRect[],
|
||||
): 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 offsetY = Math.max(0, Math.floor((32 - bDs1.height) / 2))
|
||||
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')
|
||||
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' })
|
||||
|
||||
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')
|
||||
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)
|
||||
placed.add(tailPiece.name)
|
||||
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]) {
|
||||
stampDs1Fn(canvas, headPiece.levels[0], 0, (numRows - 1) * 32)
|
||||
placed.add(headPiece.name)
|
||||
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
|
||||
|
|
@ -575,6 +598,7 @@ export function DRLG_GenerateJungles(
|
|||
cell.pieceName = riverPiece.name
|
||||
placed.add(riverPiece.name)
|
||||
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') {
|
||||
const preferredOpening =
|
||||
|
|
@ -591,6 +615,7 @@ export function DRLG_GenerateJungles(
|
|||
cell.pieceName = clearingPiece.name
|
||||
placed.add(clearingPiece.name)
|
||||
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({
|
||||
col: c,
|
||||
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
|
||||
for (const b of bridges) {
|
||||
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
|
||||
|
|
|
|||
|
|
@ -416,34 +416,34 @@ export function stampAct5SiegeStrips(
|
|||
// Zone 1: West Frontline (x in [0, 80)) - High density (Shenk's stronghold)
|
||||
// -------------------------------------------------------------------------
|
||||
// Trenches (3 trenches)
|
||||
totalTrenchCells += stampTrench(canvas, 18, 8, 10, 'vertical', rng)
|
||||
totalTrenchCells += stampTrench(canvas, 50, 28, 10, 'vertical', rng)
|
||||
totalTrenchCells += stampTrench(canvas, 34, 12, 7, 'horizontal', 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 + rng.int(-2, 2), 28 + rng.int(-1, 1), 10 + rng.int(-1, 1), 'vertical', rng)
|
||||
totalTrenchCells += stampTrench(canvas, 34 + rng.int(-2, 2), 12 + rng.int(-1, 1), 7 + rng.int(-1, 1), 'horizontal', rng)
|
||||
westObstacles += 3
|
||||
|
||||
// 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
|
||||
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
|
||||
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
|
||||
totalTrenchCells += b3.breachFloorCount
|
||||
westObstacles += 3
|
||||
|
||||
// Catapult Platforms (2 platforms)
|
||||
stampCatapultPlatform(canvas, 12, 10, 4, 4, rng)
|
||||
stampCatapultPlatform(canvas, 58, 30, 4, 4, rng)
|
||||
stampCatapultPlatform(canvas, 12 + rng.int(-2, 2), 10 + rng.int(-1, 1), 4, 4, rng)
|
||||
stampCatapultPlatform(canvas, 58 + rng.int(-2, 2), 30 + rng.int(-1, 1), 4, 4, rng)
|
||||
catapultPlatformCount += 2
|
||||
westObstacles += 2
|
||||
|
||||
// Fort Ruins (2 ruins)
|
||||
stampFortRuin(canvas, 30, 30, 4, rng)
|
||||
stampFortRuin(canvas, 64, 10, 4, rng)
|
||||
stampFortRuin(canvas, 30 + rng.int(-2, 2), 30 + rng.int(-1, 1), 4, rng)
|
||||
stampFortRuin(canvas, 64 + rng.int(-2, 2), 10 + rng.int(-1, 1), 4, rng)
|
||||
fortRuinCount += 2
|
||||
westObstacles += 2
|
||||
|
||||
|
|
@ -451,27 +451,27 @@ export function stampAct5SiegeStrips(
|
|||
// Zone 2: Mid Field (x in [80, 160)) - Medium density (No-man's land)
|
||||
// -------------------------------------------------------------------------
|
||||
// Trenches (2 trenches)
|
||||
totalTrenchCells += stampTrench(canvas, 98, 10, 10, 'vertical', rng)
|
||||
totalTrenchCells += stampTrench(canvas, 138, 28, 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 + rng.int(-2, 2), 28 + rng.int(-1, 1), 10 + rng.int(-1, 1), 'vertical', rng)
|
||||
midObstacles += 2
|
||||
|
||||
// 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
|
||||
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
|
||||
totalTrenchCells += b5.breachFloorCount
|
||||
midObstacles += 2
|
||||
|
||||
// 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
|
||||
midObstacles += 1
|
||||
|
||||
// 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
|
||||
midObstacles += 1
|
||||
|
||||
|
|
@ -479,11 +479,11 @@ export function stampAct5SiegeStrips(
|
|||
// Zone 3: East Rear (x in [160, 240)) - Low density (Harrogath outskirts)
|
||||
// -------------------------------------------------------------------------
|
||||
// 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
|
||||
|
||||
// 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
|
||||
totalTrenchCells += b6.breachFloorCount
|
||||
eastObstacles += 1
|
||||
|
|
@ -491,7 +491,7 @@ export function stampAct5SiegeStrips(
|
|||
// Catapult Platforms (0 platforms in rear reserve)
|
||||
|
||||
// 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
|
||||
eastObstacles += 1
|
||||
|
||||
|
|
@ -510,6 +510,8 @@ export function stampAct5SiegeStrips(
|
|||
}
|
||||
|
||||
stats.siegeTopography = siegeStats
|
||||
stats.proceduralWallCells = (stats.proceduralWallCells ?? 0) + totalBarricadeWalls
|
||||
stats.proceduralRoadCells = (stats.proceduralRoadCells ?? 0) + totalTrenchCells
|
||||
stats.notes.push('generated dynamic siege trenches and fortifications')
|
||||
|
||||
return placed
|
||||
|
|
|
|||
|
|
@ -82,7 +82,6 @@ import type { Side } from './world-graph.ts'
|
|||
import {
|
||||
initAct2OutdoorLevel,
|
||||
initAct3OutdoorLevel,
|
||||
generateAct3Jungle,
|
||||
stampAct3KurastCauseway,
|
||||
initAct4OutdoorLevel,
|
||||
initAct5OutdoorLevel,
|
||||
|
|
@ -1071,6 +1070,14 @@ export interface LandmarkEntity {
|
|||
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. */
|
||||
export interface WildernessStats {
|
||||
readonly substitutions: SubstitutionReport[]
|
||||
|
|
@ -1104,6 +1111,11 @@ export interface WildernessStats {
|
|||
riverForks?: number
|
||||
meanderCount?: number
|
||||
siegeTopography?: SiegeTopographyStats
|
||||
subTheme?: number
|
||||
proceduralRoadCells?: number
|
||||
proceduralDividerCells?: number
|
||||
proceduralWaterCells?: number
|
||||
proceduralWallCells?: number
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -3123,7 +3135,7 @@ function stampAct3Jungle(
|
|||
stats: WildernessStats,
|
||||
placedRects?: PlacedPresetRect[],
|
||||
): 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. */
|
||||
|
|
@ -3154,10 +3166,13 @@ export function stampThemedRoads(
|
|||
rng: Rng,
|
||||
theme: OutdoorRoadTheme,
|
||||
openGates?: ReadonlyMap<string, number>,
|
||||
stats?: WildernessStats,
|
||||
): void {
|
||||
const paintedRoadCells = new Set<string>()
|
||||
|
||||
const isInsideProtectedPreset = (x: number, y: number): boolean => {
|
||||
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
|
||||
}
|
||||
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)) {
|
||||
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 (belongsToPreset) {
|
||||
return
|
||||
}
|
||||
if (clearStrayClutter) {
|
||||
// Clear random LvlSub bush/tree clutter on the main highway spine
|
||||
;(cell.walls as Ds1Wall[]).length = 0
|
||||
|
|
@ -3186,6 +3205,7 @@ export function stampThemedRoads(
|
|||
}
|
||||
}
|
||||
;(cell.floors as Ds1Floor[])[0] = theme.roadFloor(rng)
|
||||
paintedRoadCells.add(`${x},${y}`)
|
||||
}
|
||||
|
||||
const hubX = inX * TILES_PER_BLOCK + 4
|
||||
|
|
@ -3284,6 +3304,10 @@ export function stampThemedRoads(
|
|||
paintSpurSegment(doorX, doorY, doorX, hubY)
|
||||
}
|
||||
}
|
||||
|
||||
if (stats) {
|
||||
stats.proceduralRoadCells = (stats.proceduralRoadCells ?? 0) + paintedRoadCells.size
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -3299,11 +3323,13 @@ export function stampThemedDividers(
|
|||
placedRects: readonly PlacedPresetRect[],
|
||||
rng: Rng,
|
||||
theme: OutdoorDividerTheme,
|
||||
stats?: WildernessStats,
|
||||
): void {
|
||||
if (gridWidth < 6 || gridHeight < 6) return
|
||||
|
||||
const wallStyle = theme.wallStyle
|
||||
const wallProp1 = theme.wallProp1 ?? 129
|
||||
const placedCells = new Set<string>()
|
||||
|
||||
const makeWall = (type: number, sequence: number): Ds1Wall => ({
|
||||
prop1: wallProp1,
|
||||
|
|
@ -3324,6 +3350,7 @@ export function stampThemedDividers(
|
|||
} else {
|
||||
targetWalls.push(wall)
|
||||
}
|
||||
placedCells.add(`${x},${y}`)
|
||||
}
|
||||
|
||||
// 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[],
|
||||
rng: Rng,
|
||||
openGates?: ReadonlyMap<string, number>,
|
||||
stats?: WildernessStats,
|
||||
): void {
|
||||
stampThemedRoads(
|
||||
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),
|
||||
},
|
||||
openGates,
|
||||
stats,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -3471,12 +3505,13 @@ export function stampAct1InteriorStoneWalls(
|
|||
outY: number,
|
||||
placedRects: readonly PlacedPresetRect[],
|
||||
rng: Rng,
|
||||
stats?: WildernessStats,
|
||||
): void {
|
||||
stampThemedDividers(canvas, gridWidth, gridHeight, inX, outY, placedRects, rng, {
|
||||
wallStyle: 2,
|
||||
wallProp1: 129,
|
||||
roadStyles: [5],
|
||||
})
|
||||
}, stats)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -3491,6 +3526,7 @@ export function stampAct2DesertRoads(
|
|||
placedRects: readonly PlacedPresetRect[],
|
||||
rng: Rng,
|
||||
openGates?: ReadonlyMap<string, number>,
|
||||
stats?: WildernessStats,
|
||||
): void {
|
||||
stampThemedRoads(
|
||||
canvas,
|
||||
|
|
@ -3509,10 +3545,11 @@ export function stampAct2DesertRoads(
|
|||
unknown2: 0,
|
||||
hidden: false,
|
||||
}),
|
||||
isProtectedPreset: (name) => !/River|Bridge/i.test(name),
|
||||
isProtectedPreset: (name) => name !== 'Act 1 - River',
|
||||
isIgnoredSpurPreset: (name) => /Fill|Berms/i.test(name),
|
||||
},
|
||||
openGates,
|
||||
stats,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -3527,12 +3564,13 @@ export function stampAct2SandstoneBerms(
|
|||
outY: number,
|
||||
placedRects: readonly PlacedPresetRect[],
|
||||
rng: Rng,
|
||||
stats?: WildernessStats,
|
||||
): void {
|
||||
stampThemedDividers(canvas, gridWidth, gridHeight, inX, outY, placedRects, rng, {
|
||||
wallStyle: 1,
|
||||
wallProp1: 129,
|
||||
roadStyles: [1],
|
||||
})
|
||||
}, stats)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -3547,6 +3585,7 @@ export function stampAct3KurastAvenues(
|
|||
placedRects: readonly PlacedPresetRect[],
|
||||
rng: Rng,
|
||||
openGates?: ReadonlyMap<string, number>,
|
||||
stats?: WildernessStats,
|
||||
): void {
|
||||
stampThemedRoads(
|
||||
canvas,
|
||||
|
|
@ -3565,10 +3604,11 @@ export function stampAct3KurastAvenues(
|
|||
unknown2: 0,
|
||||
hidden: false,
|
||||
}),
|
||||
isProtectedPreset: (name) => !/River|Bridge/i.test(name),
|
||||
isProtectedPreset: (name) => name !== 'Act 1 - River',
|
||||
isIgnoredSpurPreset: (name) => /Fill/i.test(name),
|
||||
},
|
||||
openGates,
|
||||
stats,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -3583,12 +3623,13 @@ export function stampAct3KurastBalustrades(
|
|||
outY: number,
|
||||
placedRects: readonly PlacedPresetRect[],
|
||||
rng: Rng,
|
||||
stats?: WildernessStats,
|
||||
): void {
|
||||
stampThemedDividers(canvas, gridWidth, gridHeight, inX, outY, placedRects, rng, {
|
||||
wallStyle: 1,
|
||||
wallProp1: 129,
|
||||
roadStyles: [1],
|
||||
})
|
||||
}, stats)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -3603,6 +3644,7 @@ export function stampAct5SnowRoads(
|
|||
placedRects: readonly PlacedPresetRect[],
|
||||
rng: Rng,
|
||||
openGates?: ReadonlyMap<string, number>,
|
||||
stats?: WildernessStats,
|
||||
): void {
|
||||
stampThemedRoads(
|
||||
canvas,
|
||||
|
|
@ -3621,10 +3663,11 @@ export function stampAct5SnowRoads(
|
|||
unknown2: 0,
|
||||
hidden: false,
|
||||
}),
|
||||
isProtectedPreset: (name) => !/River|Bridge/i.test(name),
|
||||
isProtectedPreset: (name) => name !== 'Act 1 - River',
|
||||
isIgnoredSpurPreset: (name) => /Fill|Filler/i.test(name),
|
||||
},
|
||||
openGates,
|
||||
stats,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -3639,12 +3682,13 @@ export function stampAct5FrozenTrailDivides(
|
|||
outY: number,
|
||||
placedRects: readonly PlacedPresetRect[],
|
||||
rng: Rng,
|
||||
stats?: WildernessStats,
|
||||
): void {
|
||||
stampThemedDividers(canvas, gridWidth, gridHeight, inX, outY, placedRects, rng, {
|
||||
wallStyle: 2,
|
||||
wallProp1: 129,
|
||||
roadStyles: [1],
|
||||
})
|
||||
}, stats)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -3663,47 +3707,48 @@ export function stampOutdoorRoads(
|
|||
levelTypeName?: string,
|
||||
openGates?: ReadonlyMap<string, number>,
|
||||
levelsTable?: D2Table,
|
||||
stats?: WildernessStats,
|
||||
): void {
|
||||
if (levelId === 39) return // Cow Level has no roads
|
||||
|
||||
// Level ID takes highest precedence
|
||||
if (levelId >= 41 && levelId <= 46) {
|
||||
stampAct2DesertRoads(canvas, gridWidth, gridHeight, inX, outY, placedRects, rng, openGates)
|
||||
stampAct2DesertRoads(canvas, gridWidth, gridHeight, inX, outY, placedRects, rng, openGates, stats)
|
||||
return
|
||||
} 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
|
||||
} 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
|
||||
} 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
|
||||
// Level type name takes second precedence
|
||||
} 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
|
||||
} 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
|
||||
} 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
|
||||
} 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
|
||||
}
|
||||
|
||||
// Unified actOfLevel fallback
|
||||
const effectiveAct = actOfLevel(levelId, levelTypeName, act, levelsTable)
|
||||
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) {
|
||||
stampAct3KurastAvenues(canvas, gridWidth, gridHeight, inX, outY, placedRects, rng, openGates)
|
||||
stampAct3KurastAvenues(canvas, gridWidth, gridHeight, inX, outY, placedRects, rng, openGates, stats)
|
||||
} 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) {
|
||||
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,
|
||||
levelTypeName?: string,
|
||||
levelsTable?: D2Table,
|
||||
stats?: WildernessStats,
|
||||
): void {
|
||||
if (levelId === 39) return // Cow Level has no dividers
|
||||
|
||||
// Level ID takes highest precedence
|
||||
if (levelId >= 41 && levelId <= 46) {
|
||||
stampAct2SandstoneBerms(canvas, gridWidth, gridHeight, inX, outY, placedRects, rng)
|
||||
stampAct2SandstoneBerms(canvas, gridWidth, gridHeight, inX, outY, placedRects, rng, stats)
|
||||
return
|
||||
} else if (levelId >= 79 && levelId <= 82) {
|
||||
stampAct3KurastBalustrades(canvas, gridWidth, gridHeight, inX, outY, placedRects, rng)
|
||||
stampAct3KurastBalustrades(canvas, gridWidth, gridHeight, inX, outY, placedRects, rng, stats)
|
||||
return
|
||||
} 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
|
||||
} else if (levelId >= 2 && levelId <= 7) {
|
||||
stampAct1InteriorStoneWalls(canvas, gridWidth, gridHeight, inX, outY, placedRects, rng)
|
||||
stampAct1InteriorStoneWalls(canvas, gridWidth, gridHeight, inX, outY, placedRects, rng, stats)
|
||||
return
|
||||
}
|
||||
|
||||
// Level type name takes second precedence
|
||||
if (levelTypeName === 'Act 2 - Desert') {
|
||||
stampAct2SandstoneBerms(canvas, gridWidth, gridHeight, inX, outY, placedRects, rng)
|
||||
stampAct2SandstoneBerms(canvas, gridWidth, gridHeight, inX, outY, placedRects, rng, stats)
|
||||
return
|
||||
} else if (levelTypeName === 'Act 3 - Kurast') {
|
||||
stampAct3KurastBalustrades(canvas, gridWidth, gridHeight, inX, outY, placedRects, rng)
|
||||
stampAct3KurastBalustrades(canvas, gridWidth, gridHeight, inX, outY, placedRects, rng, stats)
|
||||
return
|
||||
} 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
|
||||
} else if (levelTypeName === 'Act 1 - Wilderness') {
|
||||
stampAct1InteriorStoneWalls(canvas, gridWidth, gridHeight, inX, outY, placedRects, rng)
|
||||
stampAct1InteriorStoneWalls(canvas, gridWidth, gridHeight, inX, outY, placedRects, rng, stats)
|
||||
return
|
||||
}
|
||||
|
||||
// Unified actOfLevel fallback
|
||||
const effectiveAct = actOfLevel(levelId, levelTypeName, act, levelsTable)
|
||||
if (effectiveAct === 2) {
|
||||
stampAct2SandstoneBerms(canvas, gridWidth, gridHeight, inX, outY, placedRects, rng)
|
||||
stampAct2SandstoneBerms(canvas, gridWidth, gridHeight, inX, outY, placedRects, rng, stats)
|
||||
} 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) {
|
||||
stampAct5FrozenTrailDivides(canvas, gridWidth, gridHeight, inX, outY, placedRects, rng)
|
||||
stampAct5FrozenTrailDivides(canvas, gridWidth, gridHeight, inX, outY, placedRects, rng, stats)
|
||||
} 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,
|
||||
specialPresets: [],
|
||||
entrances: [],
|
||||
proceduralRoadCells: 0,
|
||||
proceduralDividerCells: 0,
|
||||
proceduralWaterCells: 0,
|
||||
proceduralWallCells: 0,
|
||||
substitutions: [],
|
||||
substitutedClusters: 0,
|
||||
objects: canvas.objects.length,
|
||||
|
|
@ -5087,6 +5137,7 @@ export function generateWilderness(request: WildernessRequest): WildernessResult
|
|||
substitutions: [], borderPieces: {}, unresolved: [], notes: [],
|
||||
borderStamped: 0, groundCells: 0, groundTile: null, groundTiles: undefined, sizeSource,
|
||||
roadCells: 0, roadSegments: 0, anchors: 0, specialPresets: [], entrances: [],
|
||||
proceduralRoadCells: 0, proceduralDividerCells: 0, proceduralWaterCells: 0, proceduralWallCells: 0,
|
||||
}
|
||||
|
||||
let wallLayers = 1
|
||||
|
|
@ -5142,7 +5193,7 @@ export function generateWilderness(request: WildernessRequest): WildernessResult
|
|||
if (request.levelId === 83) {
|
||||
placedSpecial = stampAct3Travincal(canvas, request.pieces, stats, specialPlacedRects)
|
||||
} 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) {
|
||||
placedSpecial = stampAct3Jungle(canvas, request.levelId, request.pieces, rng, stats, specialPlacedRects)
|
||||
const populationRng = new Rng(request.seed ^ 0x6c07)
|
||||
|
|
@ -5211,6 +5262,10 @@ export function generateWilderness(request: WildernessRequest): WildernessResult
|
|||
anchors: stats.anchors,
|
||||
entrances: stats.entrances,
|
||||
specialPresets: stats.specialPresets,
|
||||
proceduralRoadCells: stats.proceduralRoadCells ?? 0,
|
||||
proceduralDividerCells: stats.proceduralDividerCells ?? 0,
|
||||
proceduralWaterCells: stats.proceduralWaterCells ?? 0,
|
||||
proceduralWallCells: stats.proceduralWallCells ?? 0,
|
||||
substitutions: stats.substitutions,
|
||||
substitutedClusters: placedSpecial.size,
|
||||
objects: canvas.objects.length,
|
||||
|
|
@ -5278,6 +5333,7 @@ export function generateWilderness(request: WildernessRequest): WildernessResult
|
|||
dirtPathGrid = roadRes.dirtPathGrid
|
||||
stats.roadCells = roadRes.roadCells
|
||||
stats.roadSegments = roadRes.roadSegments
|
||||
stats.proceduralRoadCells = (stats.proceduralRoadCells ?? 0) + roadRes.roadCells
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -5434,6 +5490,7 @@ export function generateWilderness(request: WildernessRequest): WildernessResult
|
|||
request.levelTypeName,
|
||||
act1Plan?.openGates,
|
||||
request.levelsTable,
|
||||
stats,
|
||||
)
|
||||
stampOutdoorDividers(
|
||||
canvas,
|
||||
|
|
@ -5447,6 +5504,7 @@ export function generateWilderness(request: WildernessRequest): WildernessResult
|
|||
rng,
|
||||
request.levelTypeName,
|
||||
request.levelsTable,
|
||||
stats,
|
||||
)
|
||||
|
||||
// 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,
|
||||
act4Topography: stats.act4Topography,
|
||||
act5Topography: stats.act5Topography,
|
||||
proceduralRoadCells: stats.proceduralRoadCells ?? 0,
|
||||
proceduralDividerCells: stats.proceduralDividerCells ?? 0,
|
||||
proceduralWaterCells: stats.proceduralWaterCells ?? 0,
|
||||
proceduralWallCells: stats.proceduralWallCells ?? 0,
|
||||
cells: canvas.width * canvas.height,
|
||||
unresolved: stats.unresolved,
|
||||
unimplementedPasses: UNIMPLEMENTED_PASSES,
|
||||
|
|
|
|||
|
|
@ -13,15 +13,21 @@ import type {
|
|||
} from '../src/game/wilderness-acts.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[][] = []
|
||||
for (let y = 0; y < height; y += 1) {
|
||||
const row: Ds1Cell[] = []
|
||||
for (let x = 0; x < width; x += 1) {
|
||||
const isGap = hasGapInWalls && x === Math.floor(width / 2)
|
||||
row.push({
|
||||
walls: isGap ? [] : [{ prop1: 2, sequence: 0, style: 1, type: 0, unknown1: 0, unknown2: 0, hidden: false }],
|
||||
floors: [{ prop1: 2, sequence: 0, style: 1, unknown1: 0, unknown2: 0, hidden: false }],
|
||||
walls: isGap ? [] : [{ prop1: 2, sequence: 0, style: wallStyle, type: 0, unknown1: 0, unknown2: 0, hidden: false }],
|
||||
floors: [{ prop1: 2, sequence: 0, style: floorStyle, unknown1: 0, unknown2: 0, hidden: false }],
|
||||
shadows: [],
|
||||
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', () => {
|
||||
const bridgeDs1 = makeMockDs1(48, 16)
|
||||
const groundDs1 = makeMockDs1(48, 16)
|
||||
const pieces: WildernessPiece[] = [
|
||||
{ name: 'Act 3 - Bridge', border: false, levels: [bridgeDs1] },
|
||||
{ name: 'Act 3 - Ground', border: false, levels: [groundDs1] },
|
||||
]
|
||||
|
||||
const result = generateWilderness({
|
||||
|
|
@ -271,6 +277,38 @@ describe('Wilderness Outdoor Initializers for Acts 2-5 (Issue #51)', () => {
|
|||
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', () => {
|
||||
const pieces = makeMockBorderPieces('Act 3')
|
||||
const result = generateWilderness({
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ import {
|
|||
} from '../src/game/wilderness.ts'
|
||||
import type { WildernessPiece, PlacedPresetRect, Canvas } from '../src/game/wilderness.ts'
|
||||
import { Rng } from '../src/game/rng.ts'
|
||||
import type { Ds1, Ds1Cell } from '../src/formats/ds1.ts'
|
||||
import type { Ds1, Ds1Cell, Ds1Wall } from '../src/formats/ds1.ts'
|
||||
|
||||
function makeMockDs1(width: number, height: number): Ds1 {
|
||||
const cells: Ds1Cell[][] = []
|
||||
|
|
@ -412,5 +412,33 @@ describe('Multi-Act Outdoor Roads & Boundary Divider Networks', () => {
|
|||
expect(roadFloors).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(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,
|
||||
specialPresets: [],
|
||||
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