From 53d6cdd08c46768576e9f00db7f8f59f2120de35 Mon Sep 17 00:00:00 2001 From: troytt <47798984@qq.com> Date: Thu, 17 Sep 2026 15:23:31 +0000 Subject: [PATCH] feat(wilderness): track procedural passes in telemetry stats - Add proceduralRoadCells, proceduralDividerCells, proceduralWaterCells, proceduralWallCells to WildernessTelemetry and WildernessStats - Measure actual painted cells in stampThemedRoads, stampThemedDividers, generateDirtPaths, and act outdoor level topography passes - Add unit tests in tests/wilderness-telemetry.test.ts verifying counted telemetry against canvas state TAG=agy CONV=2a1934de-30ef-464e-b3f3-fcbcdbbc49f1 --- src/game/wilderness-acts.ts | 18 +++++ src/game/wilderness.ts | 115 ++++++++++++++++++++++------- tests/wilderness-telemetry.test.ts | 110 +++++++++++++++++++++++++++ 3 files changed, 215 insertions(+), 28 deletions(-) diff --git a/src/game/wilderness-acts.ts b/src/game/wilderness-acts.ts index 5c5a162..3a31bef 100644 --- a/src/game/wilderness-acts.ts +++ b/src/game/wilderness-acts.ts @@ -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`, ) @@ -443,6 +446,7 @@ export function stampAct3KurastCauseway( // 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 @@ -465,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) @@ -480,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`, ) @@ -507,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 @@ -558,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`, ) @@ -690,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`, ) @@ -811,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`, ) diff --git a/src/game/wilderness.ts b/src/game/wilderness.ts index fe1f011..de64289 100644 --- a/src/game/wilderness.ts +++ b/src/game/wilderness.ts @@ -1070,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[] @@ -1103,6 +1111,11 @@ export interface WildernessStats { riverForks?: number meanderCount?: number siegeTopography?: SiegeTopographyStats + subTheme?: number + proceduralRoadCells?: number + proceduralDividerCells?: number + proceduralWaterCells?: number + proceduralWallCells?: number } /** @@ -3153,7 +3166,10 @@ export function stampThemedRoads( rng: Rng, theme: OutdoorRoadTheme, openGates?: ReadonlyMap, + stats?: WildernessStats, ): void { + const paintedRoadCells = new Set() + const isInsideProtectedPreset = (x: number, y: number): boolean => { for (const r of placedRects) { if (theme.isProtectedPreset ? !theme.isProtectedPreset(r.name) : false) { @@ -3189,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 @@ -3287,6 +3304,10 @@ export function stampThemedRoads( paintSpurSegment(doorX, doorY, doorX, hubY) } } + + if (stats) { + stats.proceduralRoadCells = (stats.proceduralRoadCells ?? 0) + paintedRoadCells.size + } } /** @@ -3302,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() const makeWall = (type: number, sequence: number): Ds1Wall => ({ prop1: wallProp1, @@ -3327,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 @@ -3422,6 +3446,11 @@ export function stampThemedDividers( } } } + + if (stats) { + stats.proceduralDividerCells = (stats.proceduralDividerCells ?? 0) + placedCells.size + stats.proceduralWallCells = (stats.proceduralWallCells ?? 0) + placedCells.size + } } /** @@ -3437,6 +3466,7 @@ export function stampAct1DirtRoads( placedRects: readonly PlacedPresetRect[], rng: Rng, openGates?: ReadonlyMap, + stats?: WildernessStats, ): void { stampThemedRoads( canvas, @@ -3459,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, ) } @@ -3474,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) } /** @@ -3494,6 +3526,7 @@ export function stampAct2DesertRoads( placedRects: readonly PlacedPresetRect[], rng: Rng, openGates?: ReadonlyMap, + stats?: WildernessStats, ): void { stampThemedRoads( canvas, @@ -3516,6 +3549,7 @@ export function stampAct2DesertRoads( isIgnoredSpurPreset: (name) => /Fill|Berms/i.test(name), }, openGates, + stats, ) } @@ -3530,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) } /** @@ -3550,6 +3585,7 @@ export function stampAct3KurastAvenues( placedRects: readonly PlacedPresetRect[], rng: Rng, openGates?: ReadonlyMap, + stats?: WildernessStats, ): void { stampThemedRoads( canvas, @@ -3572,6 +3608,7 @@ export function stampAct3KurastAvenues( isIgnoredSpurPreset: (name) => /Fill/i.test(name), }, openGates, + stats, ) } @@ -3586,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) } /** @@ -3606,6 +3644,7 @@ export function stampAct5SnowRoads( placedRects: readonly PlacedPresetRect[], rng: Rng, openGates?: ReadonlyMap, + stats?: WildernessStats, ): void { stampThemedRoads( canvas, @@ -3628,6 +3667,7 @@ export function stampAct5SnowRoads( isIgnoredSpurPreset: (name) => /Fill|Filler/i.test(name), }, openGates, + stats, ) } @@ -3642,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) } /** @@ -3666,47 +3707,48 @@ export function stampOutdoorRoads( levelTypeName?: string, openGates?: ReadonlyMap, 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) } } @@ -3725,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) } } @@ -5073,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, @@ -5090,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 @@ -5214,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, @@ -5281,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 } } @@ -5437,6 +5490,7 @@ export function generateWilderness(request: WildernessRequest): WildernessResult request.levelTypeName, act1Plan?.openGates, request.levelsTable, + stats, ) stampOutdoorDividers( canvas, @@ -5450,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) @@ -5572,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, diff --git a/tests/wilderness-telemetry.test.ts b/tests/wilderness-telemetry.test.ts index 1e55c54..a73dc4d 100644 --- a/tests/wilderness-telemetry.test.ts +++ b/tests/wilderness-telemetry.test.ts @@ -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) + }) +}) + +