fix(wilderness): add seed diversity to Bloody Foothills obstacle placement

- Apply RNG jitter to obstacle placement coordinates and lengths across frontline zones
- Preserve the unblocked central corridor along y=24
- Track procedural walls and roads in siege topography stats
- Add unit test in tests/wilderness-siege-strips.test.ts verifying seed-based obstacle variance

TAG=agy
CONV=2a1934de-30ef-464e-b3f3-fcbcdbbc49f1
This commit is contained in:
troytt 2026-09-17 15:25:45 +00:00
parent 53d6cdd08c
commit 6de9fce610
2 changed files with 76 additions and 19 deletions

View File

@ -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

View File

@ -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)
})
})