import { describe, it, expect } from 'vitest' import { createCanvas, stampAct1DirtRoads, stampAct1InteriorStoneWalls, stampAct2DesertRoads, stampAct2SandstoneBerms, stampAct3KurastAvenues, stampAct3KurastBalustrades, stampAct5SnowRoads, stampAct5FrozenTrailDivides, stampOutdoorRoads, stampOutdoorDividers, generateWilderness, TILES_PER_BLOCK, } 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, Ds1Wall } from '../src/formats/ds1.ts' function makeMockDs1(width: number, height: 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: [{ 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 }], shadows: [], substitutions: [], }) } cells.push(row) } return { version: 18, width, height, act: 1, substitutionType: 0, wallLayers: 1, floorLayers: 1, cells, objects: [], npcPathOffset: null, } } function countFloorTiles(canvas: Canvas, style: number, prop1: number): number { let count = 0 for (let y = 0; y < canvas.height; y += 1) { const row = canvas.cells[y] if (!row) continue for (let x = 0; x < canvas.width; x += 1) { const cell = row[x] if (cell && cell.floors.some(f => f.style === style && f.prop1 === prop1)) { count += 1 } } } return count } function countWallTiles(canvas: Canvas, style: number, prop1: number): number { let count = 0 for (let y = 0; y < canvas.height; y += 1) { const row = canvas.cells[y] if (!row) continue for (let x = 0; x < canvas.width; x += 1) { const cell = row[x] if (cell && cell.walls.some(w => w.style === style && w.prop1 === prop1)) { count += 1 } } } return count } describe('Multi-Act Outdoor Roads & Boundary Divider Networks', () => { const gridW = 10 const gridH = 10 const width = gridW * TILES_PER_BLOCK const height = gridH * TILES_PER_BLOCK const inX = Math.floor(gridW / 2) const outY = Math.floor(gridH / 2) describe('Act 2 Desert Roads & Sandstone Berms', () => { it('stampAct2DesertRoads paints caravan routes with style: 1, prop1: 194 and connects spurs', () => { const canvas = createCanvas(width, height, 1, 1, 0) const rng = new Rng(42) const placedRects: PlacedPresetRect[] = [ { name: 'Act 2 - Desert Tomb 1', x: 24, y: 24, w: 16, h: 16 }, ] stampAct2DesertRoads(canvas, gridW, gridH, inX, outY, placedRects, rng) const roadFloors = countFloorTiles(canvas, 1, 194) expect(roadFloors).toBeGreaterThan(50) // Verify the main highway corridor has caravan road floors const highwayTile = canvas.cells[outY * TILES_PER_BLOCK + 4]?.[inX * TILES_PER_BLOCK + 4] expect(highwayTile).toBeDefined() expect(highwayTile?.floors.some(f => f.style === 1 && f.prop1 === 194)).toBe(true) // Verify spur path connected towards the Tomb preset let spurFound = false for (let y = 30; y <= 34; y += 1) { for (let x = 38; x <= 44; x += 1) { const c = canvas.cells[y]?.[x] if (c?.floors.some(f => f.style === 1 && f.prop1 === 194)) { spurFound = true break } } } expect(spurFound).toBe(true) }) it('stampAct2SandstoneBerms is a no-op removing artificial cross dividers (Issue #65)', () => { const canvas = createCanvas(width, height, 1, 1, 0) const rng = new Rng(42) const placedRects: PlacedPresetRect[] = [] // First stamp roads so the divider can create 3-tile archway breaks across them stampAct2DesertRoads(canvas, gridW, gridH, inX, outY, placedRects, rng) stampAct2SandstoneBerms(canvas, gridW, gridH, inX, outY, placedRects, rng) const bermWalls = countWallTiles(canvas, 1, 129) expect(bermWalls).toBe(0) }) }) describe('Act 3 Kurast Avenues & Balustrades', () => { it('stampAct3KurastAvenues paints flagstone avenues with style: 1, prop1: 194', () => { const canvas = createCanvas(width, height, 1, 1, 0) const rng = new Rng(100) const placedRects: PlacedPresetRect[] = [ { name: 'Act 3 - Burbs Temple', x: 20, y: 20, w: 16, h: 16 }, ] stampAct3KurastAvenues(canvas, gridW, gridH, inX, outY, placedRects, rng) const avenueFloors = countFloorTiles(canvas, 1, 194) expect(avenueFloors).toBeGreaterThan(50) }) it('stampAct3KurastBalustrades is a no-op removing artificial cross dividers (Issue #65)', () => { const canvas = createCanvas(width, height, 1, 1, 0) const rng = new Rng(100) const placedRects: PlacedPresetRect[] = [] stampAct3KurastAvenues(canvas, gridW, gridH, inX, outY, placedRects, rng) stampAct3KurastBalustrades(canvas, gridW, gridH, inX, outY, placedRects, rng) const balustradeWalls = countWallTiles(canvas, 1, 129) expect(balustradeWalls).toBe(0) }) }) describe('Act 5 Snow Roads & Frozen Trail Divides', () => { it('stampAct5SnowRoads paints snow march roads with style: 1, prop1: 194', () => { const canvas = createCanvas(width, height, 1, 1, 0) const rng = new Rng(555) const placedRects: PlacedPresetRect[] = [ { name: 'Act 5 - Barricade Building', x: 20, y: 20, w: 16, h: 16 }, ] stampAct5SnowRoads(canvas, gridW, gridH, inX, outY, placedRects, rng) const snowRoadFloors = countFloorTiles(canvas, 1, 194) expect(snowRoadFloors).toBeGreaterThan(50) }) it('stampAct5FrozenTrailDivides is a no-op removing artificial cross dividers (Issue #65)', () => { const canvas = createCanvas(width, height, 1, 1, 0) const rng = new Rng(555) const placedRects: PlacedPresetRect[] = [] stampAct5SnowRoads(canvas, gridW, gridH, inX, outY, placedRects, rng) stampAct5FrozenTrailDivides(canvas, gridW, gridH, inX, outY, placedRects, rng) const frozenDivides = countWallTiles(canvas, 2, 129) expect(frozenDivides).toBe(0) }) }) describe('Unified Dispatchers: stampOutdoorRoads & stampOutdoorDividers', () => { it('dispatches Act 2 Desert based on levelId 41 without artificial cross dividers', () => { const canvas = createCanvas(width, height, 1, 1, 0) const rng = new Rng(123) stampOutdoorRoads(canvas, 1, 41, gridW, gridH, inX, outY, [], rng) stampOutdoorDividers(canvas, 1, 41, gridW, gridH, inX, outY, [], rng) expect(countFloorTiles(canvas, 1, 194)).toBeGreaterThan(40) expect(countWallTiles(canvas, 1, 129)).toBe(0) }) it('dispatches Act 3 Kurast based on levelId 80 without artificial cross dividers', () => { const canvas = createCanvas(width, height, 1, 1, 0) const rng = new Rng(123) stampOutdoorRoads(canvas, 1, 80, gridW, gridH, inX, outY, [], rng) stampOutdoorDividers(canvas, 1, 80, gridW, gridH, inX, outY, [], rng) expect(countFloorTiles(canvas, 1, 194)).toBeGreaterThan(40) expect(countWallTiles(canvas, 1, 129)).toBe(0) }) it('dispatches Act 5 Snow based on levelId 111 without artificial cross dividers', () => { const canvas = createCanvas(width, height, 1, 1, 0) const rng = new Rng(123) stampOutdoorRoads(canvas, 1, 111, gridW, gridH, inX, outY, [], rng) stampOutdoorDividers(canvas, 1, 111, gridW, gridH, inX, outY, [], rng) expect(countFloorTiles(canvas, 1, 194)).toBeGreaterThan(40) expect(countWallTiles(canvas, 2, 129)).toBe(0) }) it('dispatches Act 1 Dirt Roads based on levelId 3 without artificial cross dividers', () => { const canvas = createCanvas(width, height, 1, 1, 0) const rng = new Rng(123) stampOutdoorRoads(canvas, 1, 3, gridW, gridH, inX, outY, [], rng) stampOutdoorDividers(canvas, 1, 3, gridW, gridH, inX, outY, [], rng) expect(countFloorTiles(canvas, 0, 194)).toBeGreaterThan(40) expect(countWallTiles(canvas, 2, 129)).toBe(0) }) it('dispatches based on levelTypeName when levelId is generic', () => { const c2 = createCanvas(width, height, 1, 1, 0) stampOutdoorRoads(c2, 1, 999, gridW, gridH, inX, outY, [], new Rng(1), 'Act 2 - Desert') stampOutdoorDividers(c2, 1, 999, gridW, gridH, inX, outY, [], new Rng(1), 'Act 2 - Desert') expect(countFloorTiles(c2, 1, 194)).toBeGreaterThan(40) expect(countWallTiles(c2, 1, 129)).toBe(0) const c3 = createCanvas(width, height, 1, 1, 0) stampOutdoorRoads(c3, 1, 999, gridW, gridH, inX, outY, [], new Rng(1), 'Act 3 - Kurast') stampOutdoorDividers(c3, 1, 999, gridW, gridH, inX, outY, [], new Rng(1), 'Act 3 - Kurast') expect(countFloorTiles(c3, 1, 194)).toBeGreaterThan(40) expect(countWallTiles(c3, 1, 129)).toBe(0) const c5 = createCanvas(width, height, 1, 1, 0) stampOutdoorRoads(c5, 1, 999, gridW, gridH, inX, outY, [], new Rng(1), 'Act 5 - Barricade') stampOutdoorDividers(c5, 1, 999, gridW, gridH, inX, outY, [], new Rng(1), 'Act 5 - Barricade') expect(countFloorTiles(c5, 1, 194)).toBeGreaterThan(40) expect(countWallTiles(c5, 2, 129)).toBe(0) }) it('dispatches based on act number fallback', () => { const c2 = createCanvas(width, height, 1, 1, 0) stampOutdoorRoads(c2, 2, 999, gridW, gridH, inX, outY, [], new Rng(1)) stampOutdoorDividers(c2, 2, 999, gridW, gridH, inX, outY, [], new Rng(1)) expect(countFloorTiles(c2, 1, 194)).toBeGreaterThan(40) expect(countWallTiles(c2, 1, 129)).toBe(0) }) it('strictly skips Cow Level (levelId 39)', () => { const canvas = createCanvas(width, height, 1, 1, 0) const rng = new Rng(42) stampOutdoorRoads(canvas, 1, 39, gridW, gridH, inX, outY, [], rng, 'Act 1 - Wilderness') stampOutdoorDividers(canvas, 1, 39, gridW, gridH, inX, outY, [], rng, 'Act 1 - Wilderness') expect(countFloorTiles(canvas, 0, 194)).toBe(0) expect(countFloorTiles(canvas, 1, 194)).toBe(0) expect(countFloorTiles(canvas, 5, 194)).toBe(0) expect(countWallTiles(canvas, 1, 129)).toBe(0) expect(countWallTiles(canvas, 2, 129)).toBe(0) }) }) describe('generateWilderness Integration for Acts 2, 3, 5', () => { const borderDs1 = makeMockDs1(8, 8) const borderPiece: WildernessPiece = { name: 'Act 2 - Desert Border 1', border: true, levels: [borderDs1], } it('generates Act 2 Rocky Waste (Level 41) with caravan routes and authentic canyon topography walls', () => { const result = generateWilderness({ levelId: 41, levelName: 'Rocky Waste', levelTypeName: 'Act 2 - Desert', sizeX: 80, sizeY: 80, subType: 0, subTheme: 0, seed: 77777, pieces: [borderPiece], substitutions: [], }) expect(result.level.act).toBe(2) let caravanRoads = 0 let sandstoneBerms = 0 for (let y = 8; y < 72; y += 1) { for (let x = 8; x < 72; x += 1) { const cell = result.level.cells[y]?.[x] if (!cell) continue if (cell.floors.some(f => (f.style === 1 || f.style === 5) && f.prop1 === 194)) caravanRoads += 1 if (cell.walls.some(w => w.style === 1 && w.prop1 === 129)) sandstoneBerms += 1 } } expect(caravanRoads).toBeGreaterThan(30) expect(sandstoneBerms).toBeGreaterThan(15) }) it('generates Act 3 Kurast Bazaar (Level 80) with Kurast avenues and authentic causeway balustrades', () => { const act3Border: WildernessPiece = { name: 'Act 3 - Kurast Border', border: true, levels: [borderDs1], } const result = generateWilderness({ levelId: 80, levelName: 'Kurast Bazaar', levelTypeName: 'Act 3 - Kurast', sizeX: 80, sizeY: 80, subType: 0, subTheme: 0, seed: 88888, pieces: [act3Border], substitutions: [], }) expect(result.level.act).toBe(3) let avenues = 0 let balustrades = 0 for (let y = 8; y < 72; y += 1) { for (let x = 8; x < 72; x += 1) { const cell = result.level.cells[y]?.[x] if (!cell) continue if (cell.floors.some(f => (f.style === 1 || f.style === 5 || f.style === 20) && f.prop1 === 194)) avenues += 1 if (cell.walls.some(w => (w.style === 1 || w.style === 20) && w.prop1 === 129)) balustrades += 1 } } expect(avenues).toBeGreaterThan(30) expect(balustrades).toBeGreaterThan(15) }) it('generates Act 5 Frigid Highlands (Level 111) with snow roads and authentic siege barricade walls', () => { const act5Border: WildernessPiece = { name: 'Act 5 - Barricade Border', border: true, levels: [borderDs1], } const result = generateWilderness({ levelId: 111, levelName: 'Frigid Highlands', levelTypeName: 'Act 5 - Barricade', sizeX: 80, sizeY: 80, subType: 0, subTheme: 0, seed: 99999, pieces: [act5Border], substitutions: [], }) expect(result.level.act).toBe(5) let snowRoads = 0 let frozenDivides = 0 for (let y = 8; y < 72; y += 1) { for (let x = 8; x < 72; x += 1) { const cell = result.level.cells[y]?.[x] if (!cell) continue if (cell.floors.some(f => f.style === 1 && f.prop1 === 194)) snowRoads += 1 if (cell.walls.some(w => w.style === 2 && w.prop1 === 129)) frozenDivides += 1 } } expect(snowRoads).toBeGreaterThan(30) expect(frozenDivides).toBeGreaterThan(15) }) it('generates Cow Level (Level 39) without roads or stone divider networks', () => { const cowBorder: WildernessPiece = { name: 'Act 1 - Wild Border 1', border: true, levels: [borderDs1], } const result = generateWilderness({ levelId: 39, levelName: 'Moo Moo Farm', levelTypeName: 'Act 1 - Wilderness', sizeX: 80, sizeY: 80, subType: 0, subTheme: 0, seed: 12345, pieces: [cowBorder], substitutions: [], }) let roadFloors = 0 let dividerWalls = 0 for (let y = 8; y < 72; y += 1) { for (let x = 8; x < 72; x += 1) { const cell = result.level.cells[y]?.[x] if (!cell) continue if (cell.floors.some(f => (f.style === 0 || f.style === 1 || f.style === 5) && f.prop1 === 194)) roadFloors += 1 if (cell.walls.some(w => (w.style === 1 || w.style === 2) && w.prop1 === 129)) dividerWalls += 1 } } 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) }) }) })