diff --git a/src/game/wilderness-acts.ts b/src/game/wilderness-acts.ts index 54b053b..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`, ) @@ -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 { - const placed = new Set() - 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 { const placed = new Set() 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`, ) diff --git a/src/game/wilderness-jungles.ts b/src/game/wilderness-jungles.ts index 571984f..fcb0776 100644 --- a/src/game/wilderness-jungles.ts +++ b/src/game/wilderness-jungles.ts @@ -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 { const placed = new Set() @@ -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 diff --git a/src/game/wilderness-siege.ts b/src/game/wilderness-siege.ts index e223e70..baa12c1 100644 --- a/src/game/wilderness-siege.ts +++ b/src/game/wilderness-siege.ts @@ -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 diff --git a/src/game/wilderness.ts b/src/game/wilderness.ts index 9671e58..de64289 100644 --- a/src/game/wilderness.ts +++ b/src/game/wilderness.ts @@ -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 { - 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, + 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) : /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() 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, + 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, + 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, + 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, + 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, 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, diff --git a/tests/wilderness-acts.test.ts b/tests/wilderness-acts.test.ts index 33f727d..4e4dbe3 100644 --- a/tests/wilderness-acts.test.ts +++ b/tests/wilderness-acts.test.ts @@ -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({ diff --git a/tests/wilderness-roads-multi-act.test.ts b/tests/wilderness-roads-multi-act.test.ts index e818425..8166ca3 100644 --- a/tests/wilderness-roads-multi-act.test.ts +++ b/tests/wilderness-roads-multi-act.test.ts @@ -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) + }) }) }) diff --git a/tests/wilderness-siege-strips.test.ts b/tests/wilderness-siege-strips.test.ts index 204b05f..54751a7 100644 --- a/tests/wilderness-siege-strips.test.ts +++ b/tests/wilderness-siege-strips.test.ts @@ -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() + const walls2 = new Set() + + 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) + }) }) 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) + }) +}) + +