474 lines
14 KiB
TypeScript
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)
|
|
})
|
|
})
|