import { describe, expect, test } from "vitest" import { generateWilderness, UNIMPLEMENTED_PASSES, } from "../src/game/wilderness.ts" import type { WildernessPiece } from "../src/game/wilderness.ts" import { ACT5_SIEGE_STRIP_NAMES, type SiegeTopographyStats, } from "../src/game/wilderness-siege.ts" import type { Ds1, Ds1Cell } from "../src/formats/ds1.ts" function makeMockStripDs1( width: number, height: number, tagSequence: number, ): Ds1 { const cells: Ds1Cell[][] = [] for (let y = 0; y < height; y += 1) { const row: Ds1Cell[] = [] for (let x = 0; x < width; x += 1) { row.push({ walls: [], floors: [ { prop1: 2, sequence: x === 0 && y === 0 ? tagSequence : 0, style: 0, unknown1: 0, unknown2: 0, hidden: false, }, ], shadows: [], substitutions: [], }) } cells.push(row) } return { version: 18, width, height, act: 5, substitutionType: 0, wallLayers: 1, floorLayers: 1, cells, objects: [], npcPathOffset: null, } } function createMockSiegePieces(): WildernessPiece[] { return ACT5_SIEGE_STRIP_NAMES.map((name, idx) => ({ name, border: false, levels: [makeMockStripDs1(16, 48, 100 + idx)], })) } describe("Act 5 Bloody Foothills Dynamic Siege Generation (DRLGOUTSIEGE)", () => { test("DRLGOUTSIEGE_InitAct5OutdoorLevel is removed from UNIMPLEMENTED_PASSES", () => { expect(UNIMPLEMENTED_PASSES).not.toContain( "DRLGOUTSIEGE_InitAct5OutdoorLevel", ) }) test("generates 240x48 Bloody Foothills canvas with 15 sequential strips from West to East", () => { const pieces = createMockSiegePieces() const result = generateWilderness({ levelId: 110, levelName: "Bloody Foothills", levelTypeName: "Act 5 - Siege", sizeX: 240, sizeY: 48, subType: -1, subTheme: -1, seed: 12345, pieces, substitutions: [], }) expect(result.level.width).toBe(240) expect(result.level.height).toBe(48) expect(result.stats.blockGrid).toEqual({ width: 30, height: 6 }) expect(result.stats.substitutedClusters).toBe(15) // Strip 0 (Siege To Barricade) is at x=0, Strip 14 (Siege To Town) is at x=224 expect(result.level.cells[0]![0]!.floors[0]!.sequence).toBe(100) expect(result.level.cells[0]![224]!.floors[0]!.sequence).toBe(114) }) test("supports dynamic strip preset level variants selected by seed", () => { const pieces: WildernessPiece[] = ACT5_SIEGE_STRIP_NAMES.map((name, idx) => { // Provide 3 different DS1 variants with distinct floor tags at (0,1) const varA = makeMockStripDs1(16, 48, 100 + idx) const varB = makeMockStripDs1(16, 48, 200 + idx) const varC = makeMockStripDs1(16, 48, 300 + idx) ;(varA.cells[1]![0]!.floors as any)[0].sequence = 1 ;(varB.cells[1]![0]!.floors as any)[0].sequence = 2 ;(varC.cells[1]![0]!.floors as any)[0].sequence = 3 return { name, border: false, levels: [varA, varB, varC], } }) const runA = generateWilderness({ levelId: 110, levelName: "Bloody Foothills", levelTypeName: "Act 5 - Siege", sizeX: 240, sizeY: 48, subType: -1, subTheme: -1, seed: 42, pieces, substitutions: [], }) const runB = generateWilderness({ levelId: 110, levelName: "Bloody Foothills", levelTypeName: "Act 5 - Siege", sizeX: 240, sizeY: 48, subType: -1, subTheme: -1, seed: 999999, pieces, substitutions: [], }) // Compare variant selections across strips const tagsA = [16, 32, 48, 64, 80, 96, 112].map( x => runA.level.cells[1]![x]!.floors[0]!.sequence, ) const tagsB = [16, 32, 48, 64, 80, 96, 112].map( x => runB.level.cells[1]![x]!.floors[0]!.sequence, ) // At least one strip variant differs between the two seeds expect(tagsA).not.toEqual(tagsB) }) test("supports dynamic strip selection with named variant pieces", () => { const pieces = createMockSiegePieces() // Replace Strip 5 with a named variant piece const strip5Idx = pieces.findIndex(p => p.name === "Act 5 - Siege Strip 5") pieces[strip5Idx] = { name: "Act 5 - Siege Strip 5 Variant Bravo", border: false, levels: [makeMockStripDs1(16, 48, 777)], } const result = generateWilderness({ levelId: 110, levelName: "Bloody Foothills", levelTypeName: "Act 5 - Siege", sizeX: 240, sizeY: 48, subType: -1, subTheme: -1, seed: 777, pieces, substitutions: [], }) // Strip 5 is at slot index 9 (x = 9 * 16 = 144) expect(result.level.cells[0]![144]!.floors[0]!.sequence).toBe(777) }) test("generates battlefield trenches with cleared wall channels", () => { const pieces = createMockSiegePieces() const result = generateWilderness({ levelId: 110, levelName: "Bloody Foothills", levelTypeName: "Act 5 - Siege", sizeX: 240, sizeY: 48, subType: -1, subTheme: -1, seed: 42, pieces, substitutions: [], }) const stats = result.stats.siegeTopography as SiegeTopographyStats expect(stats).toBeDefined() expect(stats.trenchCells).toBeGreaterThan(60) // Scan level for trench floor cells (style: 5, prop1: 194) let foundTrenchCells = 0 let wallsClearedInTrenches = true 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]! const hasTrenchFloor = cell.floors.some( f => f.style === 5 && f.prop1 === 194, ) if (hasTrenchFloor) { foundTrenchCells += 1 // Trenches must be cleared of blocking walls if (cell.walls.some(w => !w.hidden && w.prop1 !== 0)) { wallsClearedInTrenches = false } } } } expect(foundTrenchCells).toBe(stats.trenchCells) expect(wallsClearedInTrenches).toBe(true) }) test("generates breached wooden barricades with splintered end-caps and open breach gaps", () => { const pieces = createMockSiegePieces() const result = generateWilderness({ levelId: 110, levelName: "Bloody Foothills", levelTypeName: "Act 5 - Siege", sizeX: 240, sizeY: 48, subType: -1, subTheme: -1, seed: 42, pieces, substitutions: [], }) const stats = result.stats.siegeTopography as SiegeTopographyStats expect(stats.barricadeWalls).toBeGreaterThan(30) // Check for barricade walls (style: 2, prop1: 129) let foundBarricadeWalls = 0 let foundSplinteredEndCaps = 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]! const barricadeWall = cell.walls.find( w => !w.hidden && w.style === 2 && w.prop1 === 129, ) if (barricadeWall) { foundBarricadeWalls += 1 if (barricadeWall.type === 7 && (barricadeWall.sequence === 10 || barricadeWall.sequence === 11)) { foundSplinteredEndCaps += 1 } } } } expect(foundBarricadeWalls).toBe(stats.barricadeWalls) // Splintered end-caps (type: 7, seq: 10/11) must flank the breaches expect(foundSplinteredEndCaps).toBeGreaterThan(0) }) test("generates catapult defense platforms with parapets", () => { const pieces = createMockSiegePieces() const result = generateWilderness({ levelId: 110, levelName: "Bloody Foothills", levelTypeName: "Act 5 - Siege", sizeX: 240, sizeY: 48, subType: -1, subTheme: -1, seed: 42, pieces, substitutions: [], }) const stats = result.stats.siegeTopography as SiegeTopographyStats expect(stats.catapultPlatforms).toBe(3) // Scan for catapult platform floors (style: 4, prop1: 2) let platformFloorCount = 0 let platformParapetCount = 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.floors.some(f => f.style === 4 && f.prop1 === 2)) { platformFloorCount += 1 } if (cell.walls.some(w => !w.hidden && (w.style === 4 || w.style === 2) && w.prop1 === 2)) { platformParapetCount += 1 } } } expect(platformFloorCount).toBeGreaterThan(30) expect(platformParapetCount).toBeGreaterThan(15) }) test("generates fort ruins interspersed between siege strips", () => { const pieces = createMockSiegePieces() const result = generateWilderness({ levelId: 110, levelName: "Bloody Foothills", levelTypeName: "Act 5 - Siege", sizeX: 240, sizeY: 48, subType: -1, subTheme: -1, seed: 42, pieces, substitutions: [], }) const stats = result.stats.siegeTopography as SiegeTopographyStats expect(stats.fortRuins).toBe(4) // Scan for fort ruin flagstone floors (style: 1 or 18, prop1: 2) and stone walls (style: 1 or 18, prop1: 129) let ruinFloorCount = 0 let ruinWallCount = 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.floors.some(f => (f.style === 1 || f.style === 18) && f.prop1 === 2)) { ruinFloorCount += 1 } if (cell.walls.some(w => !w.hidden && (w.style === 1 || w.style === 18) && w.prop1 === 129)) { ruinWallCount += 1 } } } expect(ruinFloorCount).toBeGreaterThan(40) expect(ruinWallCount).toBeGreaterThan(15) }) test("enforces progressive frontline fortification density from East (Harrogath) to West (Barricade)", () => { const pieces = createMockSiegePieces() const result = generateWilderness({ levelId: 110, levelName: "Bloody Foothills", levelTypeName: "Act 5 - Siege", sizeX: 240, sizeY: 48, subType: -1, subTheme: -1, seed: 42, pieces, substitutions: [], }) const stats = result.stats.siegeTopography as SiegeTopographyStats const { westFrontlineObstacles, midFieldObstacles, eastRearObstacles } = stats.frontlineDensity // West Frontline (Shenk / Arreat Barricade) > Mid Field > East Rear (Harrogath) expect(westFrontlineObstacles).toBeGreaterThan(midFieldObstacles) expect(midFieldObstacles).toBeGreaterThanOrEqual(eastRearObstacles) expect(eastRearObstacles).toBeGreaterThan(0) expect(westFrontlineObstacles).toBe(10) expect(midFieldObstacles).toBe(6) expect(eastRearObstacles).toBe(3) }) test("preserves traversable central thoroughfare along y=24 from Harrogath exit to Barricade entrance", () => { const pieces = createMockSiegePieces() const result = generateWilderness({ levelId: 110, levelName: "Bloody Foothills", levelTypeName: "Act 5 - Siege", sizeX: 240, sizeY: 48, subType: -1, subTheme: -1, seed: 42, pieces, substitutions: [], }) // Walk horizontally across y = 24 from East (x = 239) to West (x = 0) const yCorridor = 24 let blockedCells = 0 for (let x = 0; x < result.level.width; x += 1) { const cell = result.level.cells[yCorridor]![x]! const hasBlockingWall = cell.walls.some( w => !w.hidden && w.prop1 !== 0, ) if (hasBlockingWall) { blockedCells += 1 } } // Central line along y = 24 should have 0 blocking walls expect(blockedCells).toBe(0) }) test("records notes and telemetry in result.stats", () => { const pieces = createMockSiegePieces() const result = generateWilderness({ levelId: 110, levelName: "Bloody Foothills", levelTypeName: "Act 5 - Siege", sizeX: 240, sizeY: 48, subType: -1, subTheme: -1, seed: 42, pieces, substitutions: [], }) const notes = result.stats.notes as string[] 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) }) })