238 lines
7.6 KiB
TypeScript
238 lines
7.6 KiB
TypeScript
import { describe, expect, test } from "vitest"
|
|
import {
|
|
generatePreset,
|
|
generateChaosSanctuary,
|
|
CHAOS_SANCTUARY_LEVEL_ID,
|
|
CHAOS_SANCTUARY_SIZE_X,
|
|
CHAOS_SANCTUARY_SIZE_Y,
|
|
CHAOS_SANCTUARY_PLACEMENTS,
|
|
} from "../src/game/preset.ts"
|
|
import type { PresetPiece } from "../src/game/preset.ts"
|
|
import { generateWilderness } from "../src/game/wilderness.ts"
|
|
import type { Ds1, Ds1Cell } from "../src/formats/ds1.ts"
|
|
|
|
function makeMockDs1(width: number, height: number, style = 1): 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: [{ prop1: 2, sequence: 0, style, type: 0, unknown1: 0, unknown2: 0, hidden: false }],
|
|
floors: [{ prop1: 2, sequence: 0, style, unknown1: 0, unknown2: 0, hidden: false }],
|
|
shadows: [],
|
|
substitutions: [],
|
|
})
|
|
}
|
|
cells.push(row)
|
|
}
|
|
return {
|
|
version: 18,
|
|
width,
|
|
height,
|
|
act: 4,
|
|
substitutionType: 0,
|
|
wallLayers: 1,
|
|
floorLayers: 1,
|
|
cells,
|
|
objects: [],
|
|
npcPathOffset: null,
|
|
}
|
|
}
|
|
|
|
describe("Preset Map Pipeline (DrlgType 2)", () => {
|
|
test("generatePreset generates Level 108 Chaos Sanctuary stitching 7 cross-wing presets on 120x120 canvas", () => {
|
|
const names = [
|
|
"Act 4 - Diablo Arm N",
|
|
"Act 4 - Diablo Arm W",
|
|
"Act 4 - Diablo Heart",
|
|
"Act 4 - Diablo Arm E",
|
|
"Act 4 - Diablo Arm S",
|
|
"Act 4 - Diablo Entry",
|
|
"Act 4 - Bridge 2",
|
|
]
|
|
const pieces: PresetPiece[] = names.map((name, idx) => {
|
|
const baseDs1 = makeMockDs1(24, 24, 10 + idx)
|
|
const ds1: Ds1 = {
|
|
...baseDs1,
|
|
objects: [{ type: 2, id: 200 + idx, x: 5, y: 5, flags: 0 }],
|
|
}
|
|
return { name, levels: [ds1] }
|
|
})
|
|
|
|
const result = generatePreset({
|
|
levelId: CHAOS_SANCTUARY_LEVEL_ID,
|
|
levelName: "Chaos Sanctuary",
|
|
levelTypeName: "Act 4 - Lava",
|
|
sizeX: CHAOS_SANCTUARY_SIZE_X,
|
|
sizeY: CHAOS_SANCTUARY_SIZE_Y,
|
|
seed: 108,
|
|
pieces,
|
|
})
|
|
|
|
expect(result.level.width).toBe(120)
|
|
expect(result.level.height).toBe(120)
|
|
expect(result.level.act).toBe(4)
|
|
expect(result.stats.substitutedClusters).toBe(7)
|
|
expect(result.stats.substitutions.length).toBe(7)
|
|
expect(result.level.objects.length).toBe(7)
|
|
expect(result.stats.groundCells).toBeGreaterThan(0)
|
|
|
|
// Verify object placement offsets (5 sub-tiles per cell)
|
|
// Arm N at (48, 0): object at (48*5 + 5, 0*5 + 5) = (245, 5)
|
|
const armNObj = result.level.objects.find(o => o.id === 200)
|
|
expect(armNObj).toBeDefined()
|
|
expect(armNObj?.x).toBe(48 * 5 + 5)
|
|
expect(armNObj?.y).toBe(0 * 5 + 5)
|
|
|
|
// Arm W at (24, 24): object at (24*5 + 5, 24*5 + 5) = (125, 125)
|
|
const armWObj = result.level.objects.find(o => o.id === 201)
|
|
expect(armWObj).toBeDefined()
|
|
expect(armWObj?.x).toBe(24 * 5 + 5)
|
|
expect(armWObj?.y).toBe(24 * 5 + 5)
|
|
|
|
// Heart at (48, 24): object at (48*5 + 5, 24*5 + 5) = (245, 125)
|
|
const heartObj = result.level.objects.find(o => o.id === 202)
|
|
expect(heartObj).toBeDefined()
|
|
expect(heartObj?.x).toBe(48 * 5 + 5)
|
|
expect(heartObj?.y).toBe(24 * 5 + 5)
|
|
|
|
// Bridge 2 at (48, 96): object at (48*5 + 5, 96*5 + 5) = (245, 485)
|
|
const bridgeObj = result.level.objects.find(o => o.id === 206)
|
|
expect(bridgeObj).toBeDefined()
|
|
expect(bridgeObj?.x).toBe(48 * 5 + 5)
|
|
expect(bridgeObj?.y).toBe(96 * 5 + 5)
|
|
|
|
// Verify tile cell content stamped at origins
|
|
expect(result.level.cells[0]?.[48]?.floors[0]?.style).toBe(10) // Arm N style
|
|
expect(result.level.cells[24]?.[24]?.floors[0]?.style).toBe(11) // Arm W style
|
|
expect(result.level.cells[24]?.[48]?.floors[0]?.style).toBe(12) // Heart style
|
|
expect(result.level.cells[96]?.[48]?.floors[0]?.style).toBe(16) // Bridge style
|
|
})
|
|
|
|
test("generatePreset dispatches by levelName when levelId is not 108", () => {
|
|
const pieces: PresetPiece[] = CHAOS_SANCTUARY_PLACEMENTS.map((item, idx) => ({
|
|
name: item.name,
|
|
levels: [makeMockDs1(24, 24, 5)],
|
|
}))
|
|
|
|
const result = generatePreset({
|
|
levelId: 0,
|
|
levelName: "Act 4 - Chaos Sanctuary",
|
|
pieces,
|
|
})
|
|
|
|
expect(result.level.width).toBe(120)
|
|
expect(result.level.height).toBe(120)
|
|
expect(result.stats.substitutedClusters).toBe(7)
|
|
})
|
|
|
|
test("generateChaosSanctuary matches piece names flexibly (tokens, stripped prefixes, filenames)", () => {
|
|
// Pieces with alternative naming styles (e.g. without 'Act 4 - ', or file paths)
|
|
const pieces: PresetPiece[] = [
|
|
{ name: "Diablo Arm N", levels: [makeMockDs1(24, 24, 1)] },
|
|
{ name: "data\\global\\tiles\\act4\\diablo\\ArmW.ds1", levels: [makeMockDs1(24, 24, 2)] },
|
|
{ name: "Act 4 - Diablo Heart", levels: [makeMockDs1(24, 24, 3)] },
|
|
{ name: "Diablo Arm E", levels: [makeMockDs1(24, 24, 4)] },
|
|
{ name: "Arm S", levels: [makeMockDs1(24, 24, 5)] },
|
|
{ name: "Diablo Entry", levels: [makeMockDs1(24, 24, 6)] },
|
|
{ name: "Bridge 2", levels: [makeMockDs1(24, 24, 7)] },
|
|
]
|
|
|
|
const result = generateChaosSanctuary({
|
|
levelId: 108,
|
|
pieces,
|
|
})
|
|
|
|
expect(result.stats.substitutedClusters).toBe(7)
|
|
expect(result.level.width).toBe(120)
|
|
expect(result.level.height).toBe(120)
|
|
})
|
|
|
|
test("generatePreset handles standalone single-DS1 preset levels", () => {
|
|
const baseDs1 = makeMockDs1(40, 40, 7)
|
|
const mockDs1: Ds1 = {
|
|
...baseDs1,
|
|
objects: [{ type: 2, id: 99, x: 10, y: 10, flags: 0 }],
|
|
}
|
|
const pieces: PresetPiece[] = [
|
|
{
|
|
name: "Tristram",
|
|
levels: [mockDs1],
|
|
},
|
|
]
|
|
|
|
const result = generatePreset({
|
|
levelId: 38,
|
|
levelName: "Tristram",
|
|
sizeX: 40,
|
|
sizeY: 40,
|
|
pieces,
|
|
})
|
|
|
|
expect(result.level.width).toBe(40)
|
|
expect(result.level.height).toBe(40)
|
|
expect(result.level.objects.length).toBe(1)
|
|
expect(result.stats.substitutedClusters).toBe(1)
|
|
})
|
|
|
|
test("generatePreset supports custom piece placements", () => {
|
|
const pieces: PresetPiece[] = [
|
|
{ name: "RoomA", levels: [makeMockDs1(20, 20, 1)] },
|
|
{ name: "RoomB", levels: [makeMockDs1(20, 20, 2)] },
|
|
]
|
|
|
|
const result = generatePreset({
|
|
levelId: 999,
|
|
sizeX: 60,
|
|
sizeY: 60,
|
|
pieces,
|
|
placements: [
|
|
{ name: "RoomA", x: 0, y: 0 },
|
|
{ name: "RoomB", x: 30, y: 30 },
|
|
],
|
|
})
|
|
|
|
expect(result.level.width).toBe(60)
|
|
expect(result.level.height).toBe(60)
|
|
expect(result.stats.substitutedClusters).toBe(2)
|
|
expect(result.level.cells[0]?.[0]?.floors[0]?.style).toBe(1)
|
|
expect(result.level.cells[30]?.[30]?.floors[0]?.style).toBe(2)
|
|
})
|
|
|
|
test("wilderness generator no longer hardcodes Level 108 Chaos Sanctuary special case", () => {
|
|
// Wilderness generator does not hijack Level 108 anymore; without special preset routing,
|
|
// it will not produce the 7-cluster Chaos Sanctuary layout.
|
|
const names = [
|
|
"Act 4 - Diablo Arm N",
|
|
"Act 4 - Diablo Arm W",
|
|
"Act 4 - Diablo Heart",
|
|
"Act 4 - Diablo Arm E",
|
|
"Act 4 - Diablo Arm S",
|
|
"Act 4 - Diablo Entry",
|
|
"Act 4 - Bridge 2",
|
|
]
|
|
const pieces = names.map(name => ({
|
|
name,
|
|
border: false,
|
|
levels: [makeMockDs1(24, 24)],
|
|
}))
|
|
|
|
const result = generateWilderness({
|
|
levelId: 108,
|
|
levelName: "Chaos Sanctuary",
|
|
levelTypeName: "Act 4 - Lava",
|
|
sizeX: 120,
|
|
sizeY: 120,
|
|
subType: -1,
|
|
subTheme: -1,
|
|
seed: 108,
|
|
pieces,
|
|
substitutions: [],
|
|
})
|
|
|
|
// Previously this returned substitutedClusters: 7 via stampAct4ChaosSanctuary.
|
|
// Now wilderness treats it as regular wilderness, not the 7-preset Chaos Sanctuary.
|
|
expect(result.stats.substitutedClusters).not.toBe(7)
|
|
})
|
|
})
|