diablo2-web/tests/wilderness-siege-strips.tes...

474 lines
14 KiB
TypeScript

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