456 lines
16 KiB
TypeScript
456 lines
16 KiB
TypeScript
import { describe, expect, test } from 'vitest'
|
|
import {
|
|
generateWilderness,
|
|
getPerimeterOpenings,
|
|
UNIMPLEMENTED_PASSES,
|
|
} from '../src/game/wilderness.ts'
|
|
import type { WildernessPiece } from '../src/game/wilderness.ts'
|
|
import type {
|
|
Act2TopographyStats,
|
|
Act3TopographyStats,
|
|
Act4TopographyStats,
|
|
Act5TopographyStats,
|
|
} from '../src/game/wilderness-acts.ts'
|
|
import type { Ds1, Ds1Cell } from '../src/formats/ds1.ts'
|
|
|
|
function makeMockDs1(
|
|
width: number,
|
|
height: number,
|
|
hasGapInWalls: boolean = false,
|
|
floorStyle: number = 1,
|
|
wallStyle: number = 1,
|
|
): 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: wallStyle, type: 0, unknown1: 0, unknown2: 0, hidden: false }],
|
|
floors: [{ prop1: 2, sequence: 0, style: floorStyle, 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 makeMockBorderPieces(actPrefix: string): WildernessPiece[] {
|
|
const borderDs1 = makeMockDs1(8, 8, false)
|
|
const openBorderDs1 = makeMockDs1(8, 8, true)
|
|
return [
|
|
{ name: `${actPrefix} - Border 1`, border: true, levels: [borderDs1, borderDs1, borderDs1, openBorderDs1] },
|
|
{ name: `${actPrefix} - Border 2`, border: true, levels: [borderDs1] },
|
|
{ name: `${actPrefix} - Border 3`, border: true, levels: [borderDs1] },
|
|
{ name: `${actPrefix} - Border 4`, border: true, levels: [borderDs1, borderDs1, borderDs1, openBorderDs1] },
|
|
{ name: `${actPrefix} - Border 5`, border: true, levels: [borderDs1] },
|
|
{ name: `${actPrefix} - Border 6`, border: true, levels: [borderDs1] },
|
|
{ name: `${actPrefix} - Border 7`, border: true, levels: [borderDs1] },
|
|
{ name: `${actPrefix} - Border 8`, border: true, levels: [borderDs1] },
|
|
]
|
|
}
|
|
|
|
describe('Wilderness Outdoor Initializers for Acts 2-5 (Issue #51)', () => {
|
|
test('UNIMPLEMENTED_PASSES has 5 outdoor passes removed', () => {
|
|
expect(UNIMPLEMENTED_PASSES).not.toContain('DRLGOUTDESR_InitAct2OutdoorLevel')
|
|
expect(UNIMPLEMENTED_PASSES).not.toContain('DRLGOUTPLACE_InitAct3OutdoorLevel')
|
|
expect(UNIMPLEMENTED_PASSES).not.toContain('DRLGOUTDOORS_InitAct4OutdoorLevel')
|
|
expect(UNIMPLEMENTED_PASSES).not.toContain('DRLGOUTSIEGE_InitAct5OutdoorLevel')
|
|
expect(UNIMPLEMENTED_PASSES).not.toContain('DRLG_GenerateJungles')
|
|
expect(UNIMPLEMENTED_PASSES).not.toContain('DRLGVER_CreateVertices')
|
|
expect(UNIMPLEMENTED_PASSES).toEqual([
|
|
'DRLGOUTPLACE_CreateLevelConnections',
|
|
'DRLGOUTDOORS_SpawnAct3Mephisto',
|
|
])
|
|
})
|
|
|
|
describe('Act 2: DRLGOUTDESR_InitAct2OutdoorLevel', () => {
|
|
test('Rocky Waste (41) generates sand caravan trails and canyon ridge walls', () => {
|
|
const pieces = makeMockBorderPieces('Act 2')
|
|
const result = generateWilderness({
|
|
levelId: 41,
|
|
levelName: 'Rocky Waste',
|
|
levelTypeName: 'Act 2 - Desert',
|
|
sizeX: 80,
|
|
sizeY: 80,
|
|
subType: 0,
|
|
subTheme: 0,
|
|
seed: 41001,
|
|
pieces,
|
|
substitutions: [],
|
|
})
|
|
|
|
expect(result.stats.act2Topography).toBeDefined()
|
|
const topo = result.stats.act2Topography as Act2TopographyStats
|
|
expect(topo.sandTrailCells).toBeGreaterThan(50)
|
|
expect(topo.canyonWalls).toBeGreaterThan(20)
|
|
|
|
let foundSandTrail = false
|
|
let foundCanyonWall = false
|
|
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 === 5 && f.prop1 === 194)) foundSandTrail = true
|
|
if (cell.walls.some(w => w.style === 1 && w.prop1 === 129)) foundCanyonWall = true
|
|
}
|
|
}
|
|
expect(foundSandTrail).toBe(true)
|
|
expect(foundCanyonWall).toBe(true)
|
|
})
|
|
|
|
test('Far Oasis (43) generates lush water oasis pools', () => {
|
|
const pieces = makeMockBorderPieces('Act 2')
|
|
const result = generateWilderness({
|
|
levelId: 43,
|
|
levelName: 'Far Oasis',
|
|
levelTypeName: 'Act 2 - Desert',
|
|
sizeX: 80,
|
|
sizeY: 80,
|
|
subType: 0,
|
|
subTheme: 0,
|
|
seed: 43001,
|
|
pieces,
|
|
substitutions: [],
|
|
})
|
|
|
|
expect(result.stats.act2Topography).toBeDefined()
|
|
const topo = result.stats.act2Topography as Act2TopographyStats
|
|
expect(topo.oasisPoolCells).toBeGreaterThan(15)
|
|
|
|
let foundOasisWater = false
|
|
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 === 2 && f.prop1 === 2)) foundOasisWater = true
|
|
}
|
|
}
|
|
expect(foundOasisWater).toBe(true)
|
|
})
|
|
|
|
test('Valley of Snakes (45) creates narrow canyon gorge', () => {
|
|
const pieces = makeMockBorderPieces('Act 2')
|
|
const result = generateWilderness({
|
|
levelId: 45,
|
|
levelName: 'Valley of Snakes',
|
|
levelTypeName: 'Act 2 - Desert',
|
|
sizeX: 80,
|
|
sizeY: 80,
|
|
subType: 0,
|
|
subTheme: 0,
|
|
seed: 45001,
|
|
pieces,
|
|
substitutions: [],
|
|
})
|
|
|
|
expect(result.stats.act2Topography).toBeDefined()
|
|
const topo = result.stats.act2Topography as Act2TopographyStats
|
|
expect(topo.canyonWalls).toBeGreaterThan(30)
|
|
})
|
|
|
|
test('Canyon of the Magi (46) places 7 Tal Rasha tombs, central dais, and has 0 perimeter openings', () => {
|
|
const pieces = makeMockBorderPieces('Act 2')
|
|
expect(getPerimeterOpenings(46, 10, 10).size).toBe(0)
|
|
|
|
const result = generateWilderness({
|
|
levelId: 46,
|
|
levelName: 'Canyon of the Magi',
|
|
levelTypeName: 'Act 2 - Desert',
|
|
sizeX: 80,
|
|
sizeY: 80,
|
|
subType: 0,
|
|
subTheme: 0,
|
|
seed: 46001,
|
|
pieces,
|
|
substitutions: [],
|
|
})
|
|
|
|
expect(result.stats.act2Topography).toBeDefined()
|
|
const topo = result.stats.act2Topography as Act2TopographyStats
|
|
expect(topo.tombEntrances).toBe(7)
|
|
|
|
// Verify central Arcane Sanctuary warp platform (portal/dais)
|
|
let foundCentralDais = false
|
|
for (let y = 35; y <= 45; y += 1) {
|
|
for (let x = 35; x <= 45; x += 1) {
|
|
const cell = result.level.cells[y]![x]!
|
|
if (cell.floors.some(f => f.style === 3)) foundCentralDais = true
|
|
}
|
|
}
|
|
expect(foundCentralDais).toBe(true)
|
|
})
|
|
})
|
|
|
|
describe('Act 3: DRLGOUTPLACE_InitAct3OutdoorLevel & DRLG_GenerateJungles', () => {
|
|
test('Jungles (76 Spider Forest) generate river channel, wooden river bridges, and jungle paths', () => {
|
|
const tailDs1 = makeMockDs1(64, 32)
|
|
const headDs1 = makeMockDs1(64, 32)
|
|
const clearingDs1 = makeMockDs1(32, 32)
|
|
const riverDs1 = makeMockDs1(32, 32)
|
|
|
|
const pieces: WildernessPiece[] = [
|
|
{ name: 'Act 3 - Jungle Tail', border: false, levels: [tailDs1] },
|
|
{ name: 'Act 3 - Jungle Head', border: false, levels: [headDs1] },
|
|
{ name: 'Act 3 - Clearing Webby E', border: false, levels: [clearingDs1] },
|
|
{ name: 'Act 3 - Jungle NS W', border: false, levels: [riverDs1] },
|
|
]
|
|
|
|
const result = generateWilderness({
|
|
levelId: 76,
|
|
levelName: 'Spider Forest',
|
|
levelTypeName: 'Act 3 - Jungle',
|
|
sizeX: 64,
|
|
sizeY: 192,
|
|
subType: 0,
|
|
subTheme: 0,
|
|
seed: 76001,
|
|
pieces,
|
|
substitutions: [],
|
|
})
|
|
|
|
expect(result.stats.act3Topography).toBeDefined()
|
|
const topo = result.stats.act3Topography as Act3TopographyStats
|
|
expect(topo.riverBridges).toBeGreaterThanOrEqual(2)
|
|
expect(topo.junglePathCells).toBeGreaterThan(20)
|
|
|
|
// Verify wooden river bridge planks (style 4, prop1 2) spanning across East channel
|
|
let foundBridgePlank = false
|
|
for (let y = 70; y <= 90; y += 1) {
|
|
for (let x = 32; x < 60; x += 1) {
|
|
const cell = result.level.cells[y]![x]!
|
|
if (cell.floors.some(f => f.style === 4 && f.prop1 === 2)) foundBridgePlank = true
|
|
}
|
|
}
|
|
expect(foundBridgePlank).toBe(true)
|
|
})
|
|
|
|
test('Kurast Causeway (82) stamps causeway with central walkway, balustrades, and canal water', () => {
|
|
const groundDs1 = makeMockDs1(48, 16)
|
|
const pieces: WildernessPiece[] = [
|
|
{ name: 'Act 3 - Ground', border: false, levels: [groundDs1] },
|
|
]
|
|
|
|
const result = generateWilderness({
|
|
levelId: 82,
|
|
levelName: 'Kurast Causeway',
|
|
levelTypeName: 'Act 3 - Kurast',
|
|
sizeX: 48,
|
|
sizeY: 16,
|
|
subType: 0,
|
|
subTheme: 0,
|
|
seed: 82001,
|
|
pieces,
|
|
substitutions: [],
|
|
})
|
|
|
|
expect(result.stats.act3Topography).toBeDefined()
|
|
const topo = result.stats.act3Topography as Act3TopographyStats
|
|
expect(topo.causewayCells).toBeGreaterThan(50)
|
|
expect(topo.canalCells).toBeGreaterThan(100)
|
|
|
|
// Verify central walkway (style 5), stone balustrade walls (style 1), and canal water (style 2)
|
|
let foundWalkway = false
|
|
let foundBalustrade = false
|
|
let foundCanalWater = false
|
|
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 === 5 || f.style === 20) && f.prop1 === 194)) foundWalkway = true
|
|
if (cell.walls.some(w => (w.style === 1 || w.style === 20) && w.prop1 === 129)) foundBalustrade = true
|
|
if (cell.floors.some(f => f.style === 2 && f.prop1 === 2)) foundCanalWater = true
|
|
}
|
|
}
|
|
expect(foundWalkway).toBe(true)
|
|
expect(foundBalustrade).toBe(true)
|
|
expect(foundCanalWater).toBe(true)
|
|
})
|
|
|
|
test('Kurast Causeway (82) preserves preset when Act 3 - Bridge is provided', () => {
|
|
const bridgeDs1 = makeMockDs1(48, 16, false, 29, 31)
|
|
const pieces: WildernessPiece[] = [
|
|
{ name: 'Act 3 - Bridge', border: false, levels: [bridgeDs1] },
|
|
]
|
|
|
|
const result = generateWilderness({
|
|
levelId: 82,
|
|
levelName: 'Kurast Causeway',
|
|
levelTypeName: 'Act 3 - Kurast',
|
|
sizeX: 48,
|
|
sizeY: 16,
|
|
subType: 0,
|
|
subTheme: 0,
|
|
seed: 82001,
|
|
pieces,
|
|
substitutions: [],
|
|
})
|
|
|
|
let foundPresetFloor = false
|
|
let foundPresetWall = false
|
|
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 === 29)) foundPresetFloor = true
|
|
if (cell.walls.some(w => w.style === 31)) foundPresetWall = true
|
|
}
|
|
}
|
|
expect(foundPresetFloor).toBe(true)
|
|
expect(foundPresetWall).toBe(true)
|
|
})
|
|
|
|
test('Kurast Bazaar (79) generates paved stone avenues, canals, and stone bridges', () => {
|
|
const pieces = makeMockBorderPieces('Act 3')
|
|
const result = generateWilderness({
|
|
levelId: 79,
|
|
levelName: 'Kurast Bazaar',
|
|
levelTypeName: 'Act 3 - Kurast',
|
|
sizeX: 80,
|
|
sizeY: 80,
|
|
subType: 0,
|
|
subTheme: 0,
|
|
seed: 79001,
|
|
pieces,
|
|
substitutions: [],
|
|
})
|
|
|
|
expect(result.stats.act3Topography).toBeDefined()
|
|
const topo = result.stats.act3Topography as Act3TopographyStats
|
|
expect(topo.causewayCells).toBeGreaterThan(50)
|
|
expect(topo.canalCells).toBeGreaterThan(30)
|
|
expect(topo.riverBridges).toBeGreaterThanOrEqual(1)
|
|
})
|
|
})
|
|
|
|
describe('Act 4: DRLGOUTDOORS_InitAct4OutdoorLevel', () => {
|
|
test('Outer Steppes (104) generates lava chasms, basalt lips, and rocky bridge causeways', () => {
|
|
const pieces = makeMockBorderPieces('Act 4')
|
|
const result = generateWilderness({
|
|
levelId: 104,
|
|
levelName: 'Outer Steppes',
|
|
levelTypeName: 'Act 4 - Mesa',
|
|
sizeX: 80,
|
|
sizeY: 80,
|
|
subType: 0,
|
|
subTheme: 0,
|
|
seed: 104001,
|
|
pieces,
|
|
substitutions: [],
|
|
})
|
|
|
|
expect(result.stats.act4Topography).toBeDefined()
|
|
const topo = result.stats.act4Topography as Act4TopographyStats
|
|
expect(topo.lavaChasmCells).toBeGreaterThan(100)
|
|
expect(topo.rockyBridges).toBeGreaterThan(5)
|
|
|
|
// Verify boiling molten lava (style 20, sequence 3, prop1 2) and rocky bridges (style 10, sequence 0, prop1 2)
|
|
let foundLava = false
|
|
let foundRockyBridge = false
|
|
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 === 20 || f.style === 10) && f.prop1 === 2)) foundLava = true
|
|
if (cell.floors.some(f => f.style === 10 && f.sequence === 0 && f.prop1 === 2)) foundRockyBridge = true
|
|
}
|
|
}
|
|
expect(foundLava).toBe(true)
|
|
expect(foundRockyBridge).toBe(true)
|
|
})
|
|
|
|
test('Plains of Despair (105) creates Izual prison plateau', () => {
|
|
const pieces = makeMockBorderPieces('Act 4')
|
|
const result = generateWilderness({
|
|
levelId: 105,
|
|
levelName: 'Plains of Despair',
|
|
levelTypeName: 'Act 4 - Mesa',
|
|
sizeX: 80,
|
|
sizeY: 80,
|
|
subType: 0,
|
|
subTheme: 0,
|
|
seed: 105001,
|
|
pieces,
|
|
substitutions: [],
|
|
})
|
|
|
|
expect(result.stats.act4Topography).toBeDefined()
|
|
const topo = result.stats.act4Topography as Act4TopographyStats
|
|
expect(topo.plateauCells).toBeGreaterThan(20)
|
|
})
|
|
})
|
|
|
|
describe('Act 5: DRLGOUTSIEGE_InitAct5OutdoorLevel', () => {
|
|
test('Frigid Highlands (111) generates transverse siege barricades, choke-point gates, and trenches', () => {
|
|
const pieces = makeMockBorderPieces('Act 5')
|
|
const result = generateWilderness({
|
|
levelId: 111,
|
|
levelName: 'Frigid Highlands',
|
|
levelTypeName: 'Act 5 - Barricade',
|
|
sizeX: 80,
|
|
sizeY: 80,
|
|
subType: 0,
|
|
subTheme: 0,
|
|
seed: 111001,
|
|
pieces,
|
|
substitutions: [],
|
|
})
|
|
|
|
expect(result.stats.act5Topography).toBeDefined()
|
|
const topo = result.stats.act5Topography as Act5TopographyStats
|
|
expect(topo.barricadeWalls).toBeGreaterThan(40)
|
|
expect(topo.trenchCells).toBeGreaterThan(30)
|
|
|
|
// Verify spiked palisade walls (style 2, prop1 129) and trench floors (style 5, prop1 194)
|
|
let foundBarricadeWall = false
|
|
let foundTrench = false
|
|
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.walls.some(w => w.style === 2 && w.prop1 === 129)) foundBarricadeWall = true
|
|
if (cell.floors.some(f => f.style === 5 && f.prop1 === 194)) foundTrench = true
|
|
}
|
|
}
|
|
expect(foundBarricadeWall).toBe(true)
|
|
expect(foundTrench).toBe(true)
|
|
})
|
|
|
|
test('Arreat Plateau (112) generates snow crater frozen lakes with ice floors', () => {
|
|
const pieces = makeMockBorderPieces('Act 5')
|
|
const result = generateWilderness({
|
|
levelId: 112,
|
|
levelName: 'Arreat Plateau',
|
|
levelTypeName: 'Act 5 - Barricade',
|
|
sizeX: 80,
|
|
sizeY: 80,
|
|
subType: 0,
|
|
subTheme: 0,
|
|
seed: 112001,
|
|
pieces,
|
|
substitutions: [],
|
|
})
|
|
|
|
expect(result.stats.act5Topography).toBeDefined()
|
|
const topo = result.stats.act5Topography as Act5TopographyStats
|
|
expect(topo.frozenLakeCells).toBeGreaterThan(20)
|
|
|
|
let foundIceFloor = false
|
|
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 === 2 && f.sequence === 0 && f.prop1 === 2)) foundIceFloor = true
|
|
}
|
|
}
|
|
expect(foundIceFloor).toBe(true)
|
|
})
|
|
})
|
|
})
|