import { describe, expect, test } from "vitest" import { isClosedBorder, selectBorderVariant, getPerimeterOpenings, SPECIAL_PRESETS_BY_LEVEL, generateWilderness, resolveUnsetSize, UnresolvedLevelSizeError, } from "../src/game/wilderness.ts" import type { WildernessPiece } from "../src/game/wilderness.ts" import { Rng } from "../src/game/rng.ts" import type { Ds1, Ds1Cell } from "../src/formats/ds1.ts" function makeMockDs1(width: number, height: number, hasGapInWalls: boolean): 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 }], shadows: [], substitutions: [], }) } cells.push(row) } return { version: 18, width, height, act: 1, substitutionType: 0, wallLayers: 1, floorLayers: 1, cells, objects: [], npcPathOffset: null, } } describe("Wilderness Features & Border Rules", () => { test("isClosedBorder identifies continuous vs open wall presets", () => { const closed = makeMockDs1(8, 8, false) const open = makeMockDs1(8, 8, true) expect(isClosedBorder(closed, "south")).toBe(true) expect(isClosedBorder(closed, "north")).toBe(true) expect(isClosedBorder(open, "south")).toBe(false) expect(isClosedBorder(open, "north")).toBe(false) }) test("getPerimeterOpenings enforces 0 exits for Cow Level and 1 entrance/1 exit for Blood Moor", () => { const cowOpenings = getPerimeterOpenings(39, 10, 10) expect(cowOpenings.size).toBe(0) const bloodMoorOpenings = getPerimeterOpenings(2, 10, 10) expect(bloodMoorOpenings.size).toBe(2) expect(bloodMoorOpenings.has("5,9")).toBe(true) expect(bloodMoorOpenings.has("9,5")).toBe(true) }) test("getPerimeterOpenings defaults to open borders for Act 3 Jungles and outdoor levels", () => { // Act 3: Spider Forest (76), Great Marsh (77), Flayer Jungle (78) const spiderForestOpenings = getPerimeterOpenings(76, 10, 10) expect(spiderForestOpenings.size).toBe(2) expect(spiderForestOpenings.has("5,9")).toBe(true) expect(spiderForestOpenings.has("9,5")).toBe(true) // Act 5: Arreat Plateau (113) const arreatOpenings = getPerimeterOpenings(113, 10, 10) expect(arreatOpenings.size).toBe(2) }) test("getPerimeterOpenings maps planned gates to perimeter block coordinates", () => { const plannedGates = [ { side: 'north' as const, toLevelId: 75, label: 'To Town' }, { side: 'east' as const, toLevelId: 77, label: 'To Marsh' }, ] const openings = getPerimeterOpenings(76, 12, 14, plannedGates) expect(openings.size).toBe(2) expect(openings.has("6,0")).toBe(true) // north: (12/2, 0) expect(openings.has("11,7")).toBe(true) // east: (12-1, 14/2) }) test("selectBorderVariant picks open variant for openings and closed variant for closed walls", () => { const closedDs1 = makeMockDs1(8, 8, false) const openDs1 = makeMockDs1(8, 8, true) const piece: WildernessPiece = { name: "Act 1 - Wild Border 1", border: true, levels: [closedDs1, openDs1], } const rng = new Rng(12345) const pickedOpen = selectBorderVariant(piece, "south", true, rng) expect(isClosedBorder(pickedOpen, "south")).toBe(false) const pickedClosed = selectBorderVariant(piece, "south", false, rng) expect(isClosedBorder(pickedClosed, "south")).toBe(true) }) test("SPECIAL_PRESETS_BY_LEVEL has no Act I outdoor entries (the DRLG port places those presets)", () => { for (const levelId of [2, 3, 4, 5, 6, 7, 17, 39]) { expect(SPECIAL_PRESETS_BY_LEVEL[levelId]).toBeUndefined() } expect(SPECIAL_PRESETS_BY_LEVEL[41]).toContain("Act 2 - Desert Tomb 1") }) test("Act 5 Bloody Foothills (Level 110) stamps 15 canonical siege strips in West-to-East order", () => { const stripNames = [ "Act 5 - Siege To Barricade", "Act 5 - Siege Strip 13", "Act 5 - Siege Strip 12", "Act 5 - Siege Strip 11", "Act 5 - Siege Strip 10", "Act 5 - Siege Strip 9", "Act 5 - Siege Strip 8", "Act 5 - Siege Strip 7", "Act 5 - Siege Strip 6", "Act 5 - Siege Strip 5", "Act 5 - Siege Strip 4", "Act 5 - Siege Strip 3", "Act 5 - Siege Strip 2", "Act 5 - Siege Strip 1", "Act 5 - Siege To Town", ] const pieces: WildernessPiece[] = stripNames.map((name, idx) => { const baseDs1 = makeMockDs1(17, 49, false) // Create a copy with tagged floor sequence at (0,0) and one object const cells = baseDs1.cells.map((row, y) => row.map((cell, x) => x === 0 && y === 0 ? { ...cell, floors: [{ ...cell.floors[0], sequence: idx + 10 }], } : cell, ), ) const ds1: Ds1 = { ...baseDs1, cells, objects: [ { type: 2, id: 100 + idx, x: 5, y: 5, flags: 0, }, ], } return { name, border: false, levels: [ds1], } }) const result = generateWilderness({ levelId: 110, levelName: "Bloody Foothills", levelTypeName: "Act 5 - Siege", sizeX: 240, sizeY: 48, subType: -1, subTheme: -1, seed: 42, pieces, substitutions: [], }) expect(result.stats.blockGrid).toEqual({ width: 30, height: 6 }) expect(result.stats.substitutedClusters).toBe(15) expect(result.level.width).toBe(240) expect(result.level.height).toBe(48) expect(result.level.objects.filter(o => o.type === 2).length).toBe(15) expect(result.level.objects.length).toBe(17) // Verify strip 0 (Siege To Barricade) is at x=0 and strip 14 (Siege To Town) is at x=224 expect(result.level.cells[0][0].floors[0].sequence).toBe(10) expect(result.level.cells[0][224].floors[0].sequence).toBe(24) }) test("Act 3 Travincal (Level 83) stamps all 6 temple complex pieces even with trailing spaces", () => { const names = [ "Act 3 - Travincal NW", "Act 3 - Travincal N ", // trailing space as in LvlPrest.txt "Act 3 - Travincal NE", "Act 3 - Travincal SW", "Act 3 - Travincal S ", // trailing space as in LvlPrest.txt "Act 3 - Travincal SE", ] const pieces: WildernessPiece[] = names.map((name, idx) => { const baseDs1 = makeMockDs1(name.includes(" N ") || name.includes(" S ") ? 33 : 17, 33, false) const ds1: Ds1 = { ...baseDs1, objects: [{ type: 2, id: 300 + idx, x: 5, y: 5, flags: 0 }], } return { name, border: false, levels: [ds1] } }) const result = generateWilderness({ levelId: 83, levelName: "Travincal", levelTypeName: "Act 3 - Kurast", sizeX: 64, sizeY: 64, subType: -1, subTheme: -1, seed: 83, pieces, substitutions: [], }) expect(result.level.width).toBe(64) expect(result.level.height).toBe(64) expect(result.stats.substitutedClusters).toBe(6) expect(result.level.objects.length).toBe(6) }) test("resolveUnsetSize dynamically derives vertical corridor dimensions for Level 111 and 112", () => { const entranceDs1 = makeMockDs1(17, 33, false) const exitDs1 = makeMockDs1(17, 33, false) const wallDs1 = makeMockDs1(17, 17, false) const cliffDs1 = makeMockDs1(17, 17, false) const pieces: WildernessPiece[] = [ { name: "Act 5 - Barricade Entrance 16x32", border: false, levels: [entranceDs1] }, { name: "Act 5 - Barricade Exit 16x32", border: false, levels: [exitDs1] }, { name: "Act 5 - Barricade 1", border: false, levels: [wallDs1] }, { name: "Act 5 - Barricade 2", border: false, levels: [wallDs1] }, { name: "Act 5 - Barricade Cliff Border 1", border: true, levels: [cliffDs1] }, ] for (const levelId of [111, 112]) { const resolved = resolveUnsetSize({ levelId, levelName: `Act 5 - Barricade ${levelId - 110}`, levelTypeName: "Act 5 - Barricade", sizeX: -1, sizeY: -1, subType: 10, subTheme: 0, seed: 12345, pieces, substitutions: [], }) expect(resolved.sizeX).toBeGreaterThanOrEqual(48) expect(resolved.sizeX).toBeLessThanOrEqual(64) expect(resolved.sizeY).toBeGreaterThanOrEqual(160) expect(resolved.sizeY).toBeLessThanOrEqual(192) expect(resolved.sizeY).toBeGreaterThan(resolved.sizeX) expect(resolved.sizeY / resolved.sizeX).toBeGreaterThanOrEqual(2.5) expect(resolved.source).toBe("LvlPrest Barricade corridor dynamic extent") } }) test("resolveUnsetSize throws UnresolvedLevelSizeError when pieces are absent (anti-silent failure)", () => { expect(() => { resolveUnsetSize({ levelId: 111, levelName: "Act 5 - Barricade 1", levelTypeName: "Act 5 - Barricade", sizeX: -1, sizeY: -1, subType: 10, subTheme: 0, seed: 12345, pieces: [], substitutions: [], }) }).toThrow(UnresolvedLevelSizeError) }) test("resolveUnsetSize throws UnresolvedLevelSizeError when non-barricade level lacks barricade pieces", () => { const mockDs1 = makeMockDs1(8, 8, false) expect(() => { resolveUnsetSize({ levelId: 2, levelName: "Blood Moor", levelTypeName: "Act 1 - Wilderness", sizeX: -1, sizeY: -1, subType: 6, subTheme: 0, seed: 12345, pieces: [{ name: "Act 1 - Wild Border 1", border: true, levels: [mockDs1] }], substitutions: [], }) }).toThrow(UnresolvedLevelSizeError) }) test("resolveUnsetSize respects sizeOverride when provided", () => { const resolved = resolveUnsetSize({ levelId: 111, levelName: "Act 5 - Barricade 1", levelTypeName: "Act 5 - Barricade", sizeX: -1, sizeY: -1, sizeOverride: { x: 80, y: 120 }, subType: 10, subTheme: 0, seed: 12345, pieces: [], substitutions: [], }) expect(resolved.sizeX).toBe(80) expect(resolved.sizeY).toBe(120) expect(resolved.source).toBe("sizeOverride") }) })