diablo2-web/tests/wilderness-features.test.ts

254 lines
7.6 KiB
TypeScript

import { describe, expect, test } from "vitest"
import {
isClosedBorder,
selectBorderVariant,
getPerimeterOpenings,
SPECIAL_PRESETS_BY_LEVEL,
generateWilderness,
} 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("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 contains stone walls and river presets", () => {
const bloodMoorPresets = SPECIAL_PRESETS_BY_LEVEL[2]
expect(bloodMoorPresets).toBeDefined()
expect(bloodMoorPresets).toContain("Act 1 - Corral Fill")
expect(bloodMoorPresets).toContain("Act 1 - Fence Fill 4")
expect(bloodMoorPresets).toContain("Act 1 - River Upper")
expect(bloodMoorPresets).toContain("Act 1 - Bridge")
const cowPresets = SPECIAL_PRESETS_BY_LEVEL[39]
expect(cowPresets).toBeDefined()
expect(cowPresets).toContain("Act 1 - Corral Fill")
expect(cowPresets).toContain("Act 1 - Fence Fill 4")
expect(cowPresets).toContain("Act 1 - Fence Fill 5")
expect(cowPresets).toContain("Act 1 - Bivouac")
expect(cowPresets).toContain("Act 1 - Pond")
})
test("generateWilderness generates Level 2 with river or pond deterministically by seed", () => {
const closedDs1 = makeMockDs1(8, 8, false)
const piece: WildernessPiece = {
name: "Act 1 - Wild Border 1",
border: true,
levels: [closedDs1],
}
const riverPiece: WildernessPiece = {
name: "Act 1 - River Upper",
border: false,
levels: [closedDs1],
}
const pondPiece: WildernessPiece = {
name: "Act 1 - Pond",
border: false,
levels: [closedDs1],
}
const corralPiece: WildernessPiece = {
name: "Act 1 - Corral Fill",
border: false,
levels: [closedDs1],
}
const result = generateWilderness({
levelId: 2,
levelName: "Blood Moor",
levelTypeName: "Act 1 - Wilderness",
sizeX: 80,
sizeY: 80,
subType: 6,
subTheme: 0,
seed: 12345,
pieces: [piece, riverPiece, pondPiece, corralPiece],
substitutions: [],
})
expect(result.stats.blockGrid).toEqual({ width: 10, height: 10 })
expect(result.level.width).toBe(80)
expect(result.level.height).toBe(80)
})
test("Act 1 Wilderness generates interior stone walls and dirt road network", () => {
const closedDs1 = makeMockDs1(8, 8, false)
const piece: WildernessPiece = {
name: "Act 1 - Wild Border 1",
border: true,
levels: [closedDs1],
}
const result = generateWilderness({
levelId: 3,
levelName: "Cold Plains",
levelTypeName: "Act 1 - Wilderness",
sizeX: 80,
sizeY: 80,
subType: 6,
subTheme: 0,
seed: 99999,
pieces: [piece],
substitutions: [],
})
let stoneWallCount = 0
let dirtRoadCount = 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.walls.some(w => w.style === 2 && w.prop1 === 129)) {
stoneWallCount += 1
}
if (cell.floors.some(f => f.style === 5 && f.prop1 === 194)) {
dirtRoadCount += 1
}
}
}
expect(stoneWallCount).toBeGreaterThan(20)
expect(dirtRoadCount).toBeGreaterThan(30)
})
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.length).toBe(15)
// 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)
})
})