604 lines
20 KiB
TypeScript
604 lines
20 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,
|
|
PRESET_LEVEL_IDS,
|
|
PRESET_LEVEL_ID_SET,
|
|
ALL_PRESET_LEVEL_IDS,
|
|
isPresetLevel,
|
|
slicePresetRooms,
|
|
createD2Seed,
|
|
rollD2Random,
|
|
rollD2LimitedRandom,
|
|
resolvePickedFileIndex,
|
|
resolvePickedFile,
|
|
} 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)
|
|
})
|
|
})
|
|
|
|
describe("Canonical Preset Levels (Levels.txt DrlgType == 2)", () => {
|
|
test("PRESET_LEVEL_IDS contains all 35 canonical DrlgType == 2 levels across Acts 1-5", () => {
|
|
expect(PRESET_LEVEL_IDS.length).toBe(35)
|
|
// Act 1 (13)
|
|
for (const id of [1, 13, 14, 15, 16, 20, 25, 26, 27, 32, 33, 37, 38]) {
|
|
expect(PRESET_LEVEL_ID_SET.has(id)).toBe(true)
|
|
}
|
|
// Act 2 (3)
|
|
for (const id of [40, 50, 73]) {
|
|
expect(PRESET_LEVEL_ID_SET.has(id)).toBe(true)
|
|
}
|
|
// Act 3 (11)
|
|
for (const id of [75, 90, 91, 93, 94, 95, 96, 97, 98, 99, 102]) {
|
|
expect(PRESET_LEVEL_ID_SET.has(id)).toBe(true)
|
|
}
|
|
// Act 4 (1)
|
|
expect(PRESET_LEVEL_ID_SET.has(103)).toBe(true)
|
|
// Act 5 (7)
|
|
for (const id of [109, 120, 121, 124, 131, 132, 136]) {
|
|
expect(PRESET_LEVEL_ID_SET.has(id)).toBe(true)
|
|
}
|
|
})
|
|
|
|
test("ALL_PRESET_LEVEL_IDS includes the 35 canonical levels plus Level 108 Chaos Sanctuary", () => {
|
|
expect(ALL_PRESET_LEVEL_IDS.length).toBe(36)
|
|
expect(ALL_PRESET_LEVEL_IDS).toContain(108)
|
|
for (const id of PRESET_LEVEL_IDS) {
|
|
expect(ALL_PRESET_LEVEL_IDS).toContain(id)
|
|
}
|
|
})
|
|
|
|
test("isPresetLevel correctly classifies preset vs non-preset level IDs", () => {
|
|
// 35 canonical preset levels
|
|
for (const id of PRESET_LEVEL_IDS) {
|
|
expect(isPresetLevel(id)).toBe(true)
|
|
}
|
|
// Level 108 Chaos Sanctuary (stitched preset level)
|
|
expect(isPresetLevel(108)).toBe(true)
|
|
|
|
// Non-preset levels
|
|
expect(isPresetLevel(2)).toBe(false) // Blood Moor (outdoor)
|
|
expect(isPresetLevel(3)).toBe(false) // Cold Plains (outdoor)
|
|
expect(isPresetLevel(8)).toBe(false) // Catacombs Level 1 (maze)
|
|
expect(isPresetLevel(39)).toBe(false) // Moo Moo Farm (outdoor)
|
|
expect(isPresetLevel(0)).toBe(false)
|
|
expect(isPresetLevel(-1)).toBe(false)
|
|
expect(isPresetLevel(999)).toBe(false)
|
|
})
|
|
})
|
|
|
|
describe("8x8 Room Geometry Assembly and Slicing (DRLGPRESET_BuildArea)", () => {
|
|
test("slices 16x16 canvas into 4 exact 8x8 rooms", () => {
|
|
const rooms = slicePresetRooms(16, 16)
|
|
expect(rooms.length).toBe(4)
|
|
|
|
expect(rooms[0]).toMatchObject({
|
|
id: "preset_room_0_0",
|
|
x: 0,
|
|
y: 0,
|
|
width: 8,
|
|
height: 8,
|
|
gridX: 0,
|
|
gridY: 0,
|
|
cellX: 0,
|
|
cellY: 0,
|
|
cellsX: 8,
|
|
cellsY: 8,
|
|
objects: [],
|
|
})
|
|
expect(rooms[1]).toMatchObject({
|
|
id: "preset_room_1_0",
|
|
x: 8,
|
|
y: 0,
|
|
width: 8,
|
|
height: 8,
|
|
gridX: 1,
|
|
gridY: 0,
|
|
})
|
|
expect(rooms[2]).toMatchObject({
|
|
id: "preset_room_0_1",
|
|
x: 0,
|
|
y: 8,
|
|
width: 8,
|
|
height: 8,
|
|
gridX: 0,
|
|
gridY: 1,
|
|
})
|
|
expect(rooms[3]).toMatchObject({
|
|
id: "preset_room_1_1",
|
|
x: 8,
|
|
y: 8,
|
|
width: 8,
|
|
height: 8,
|
|
gridX: 1,
|
|
gridY: 1,
|
|
})
|
|
})
|
|
|
|
test("clamps non-multiple-of-8 canvas boundaries (20x20 canvas -> 9 rooms)", () => {
|
|
const rooms = slicePresetRooms(20, 20)
|
|
// 20 = 8 + 8 + 4 -> 3 x 3 = 9 rooms
|
|
expect(rooms.length).toBe(9)
|
|
|
|
// Check bottom-right clamped room
|
|
const cornerRoom = rooms.find(r => r.gridX === 2 && r.gridY === 2)
|
|
expect(cornerRoom).toBeDefined()
|
|
expect(cornerRoom?.x).toBe(16)
|
|
expect(cornerRoom?.y).toBe(16)
|
|
expect(cornerRoom?.width).toBe(4)
|
|
expect(cornerRoom?.height).toBe(4)
|
|
expect(cornerRoom?.cellsX).toBe(4)
|
|
expect(cornerRoom?.cellsY).toBe(4)
|
|
|
|
// Check edge rooms with 8x4 or 4x8
|
|
const topEdgeClamped = rooms.find(r => r.gridX === 2 && r.gridY === 0)
|
|
expect(topEdgeClamped?.width).toBe(4)
|
|
expect(topEdgeClamped?.height).toBe(8)
|
|
|
|
const leftEdgeClamped = rooms.find(r => r.gridX === 0 && r.gridY === 2)
|
|
expect(leftEdgeClamped?.width).toBe(8)
|
|
expect(leftEdgeClamped?.height).toBe(4)
|
|
})
|
|
|
|
test("handles asymmetric canvas (25x13 -> 8 rooms)", () => {
|
|
// Width 25: 8 + 8 + 8 + 1 -> 4 columns
|
|
// Height 13: 8 + 5 -> 2 rows
|
|
// Total: 4 * 2 = 8 rooms
|
|
const rooms = slicePresetRooms(25, 13)
|
|
expect(rooms.length).toBe(8)
|
|
|
|
const lastColTopRow = rooms.find(r => r.gridX === 3 && r.gridY === 0)
|
|
expect(lastColTopRow?.width).toBe(1)
|
|
expect(lastColTopRow?.height).toBe(8)
|
|
|
|
const lastColBottomRow = rooms.find(r => r.gridX === 3 && r.gridY === 1)
|
|
expect(lastColBottomRow?.width).toBe(1)
|
|
expect(lastColBottomRow?.height).toBe(5)
|
|
})
|
|
|
|
test("returns empty array for zero or negative dimensions", () => {
|
|
expect(slicePresetRooms(0, 0)).toEqual([])
|
|
expect(slicePresetRooms(-10, 20)).toEqual([])
|
|
expect(slicePresetRooms(20, -5)).toEqual([])
|
|
})
|
|
|
|
test("partitions objects by room boundaries using sub-tile coordinates (5 sub-tiles per cell)", () => {
|
|
const objects = [
|
|
// In room (0, 0): cell (2, 3) -> sub-tiles (10, 15)
|
|
{ type: 2, id: 1, x: 10, y: 15, flags: 0 },
|
|
// In room (1, 0): cell (9, 2) -> sub-tiles (45, 10)
|
|
{ type: 2, id: 2, x: 45, y: 10, flags: 0 },
|
|
// In room (0, 1): cell (1, 10) -> sub-tiles (5, 50)
|
|
{ type: 2, id: 3, x: 5, y: 50, flags: 0 },
|
|
// In room (1, 1): cell (15, 15) -> sub-tiles (75, 75)
|
|
{ type: 2, id: 4, x: 75, y: 75, flags: 0 },
|
|
// Outside canvas: cell (25, 25) -> sub-tiles (125, 125)
|
|
{ type: 2, id: 5, x: 125, y: 125, flags: 0 },
|
|
]
|
|
|
|
const rooms = slicePresetRooms(16, 16, objects)
|
|
expect(rooms.length).toBe(4)
|
|
|
|
const room00 = rooms.find(r => r.gridX === 0 && r.gridY === 0)
|
|
expect(room00?.objects.map(o => o.id)).toEqual([1])
|
|
|
|
const room10 = rooms.find(r => r.gridX === 1 && r.gridY === 0)
|
|
expect(room10?.objects.map(o => o.id)).toEqual([2])
|
|
|
|
const room01 = rooms.find(r => r.gridX === 0 && r.gridY === 1)
|
|
expect(room01?.objects.map(o => o.id)).toEqual([3])
|
|
|
|
const room11 = rooms.find(r => r.gridX === 1 && r.gridY === 1)
|
|
expect(room11?.objects.map(o => o.id)).toEqual([4])
|
|
})
|
|
|
|
test("supports direct cell coordinate objects when cellCoords option is enabled", () => {
|
|
const objects = [
|
|
{ type: 2, id: 10, x: 4, y: 4, flags: 0 },
|
|
{ type: 2, id: 20, x: 12, y: 4, flags: 0 },
|
|
]
|
|
|
|
const rooms = slicePresetRooms(16, 16, objects, { cellCoords: true })
|
|
const r00 = rooms.find(r => r.gridX === 0 && r.gridY === 0)
|
|
const r10 = rooms.find(r => r.gridX === 1 && r.gridY === 0)
|
|
|
|
expect(r00?.objects.map(o => o.id)).toEqual([10])
|
|
expect(r10?.objects.map(o => o.id)).toEqual([20])
|
|
})
|
|
})
|
|
|
|
describe("1.13c D2Seed PRNG and Multi-File Variant Selection (DRLGPRESET_AllocDrlgMap)", () => {
|
|
test("createD2Seed initializes low and default high = 666 matching 1.13c SEED_InitLowSeed", () => {
|
|
const seed = createD2Seed(12345)
|
|
expect(seed.low).toBe(12345)
|
|
expect(seed.high).toBe(666)
|
|
})
|
|
|
|
test("rollD2Random advances seed deterministically with 0x6AC690C5 multiplier", () => {
|
|
const seed = createD2Seed(100)
|
|
const roll1 = rollD2Random(seed)
|
|
// 666 + 0x6AC690C5 * 100 = 179139809166
|
|
// low 32 bits = 179139809166 % 2^32 = 3046150030
|
|
// high 32 bits = 179139809166 >> 32 = 41
|
|
expect(roll1).toBe(3046150030)
|
|
expect(seed.low).toBe(3046150030)
|
|
expect(seed.high).toBe(41)
|
|
|
|
// Second roll
|
|
const roll2 = rollD2Random(seed)
|
|
expect(typeof roll2).toBe("number")
|
|
expect(roll2).not.toBe(roll1)
|
|
})
|
|
|
|
test("rollD2LimitedRandom implements power-of-2 bitmask and non-power-of-2 modulo branches", () => {
|
|
// Power of 2 (max = 4): uses roll & 3
|
|
const p2Roll = rollD2LimitedRandom(100, 4)
|
|
expect(p2Roll).toBeGreaterThanOrEqual(0)
|
|
expect(p2Roll).toBeLessThan(4)
|
|
|
|
// Non power of 2 (max = 3): uses roll % 3
|
|
const np2Roll = rollD2LimitedRandom(100, 3)
|
|
expect(np2Roll).toBeGreaterThanOrEqual(0)
|
|
expect(np2Roll).toBeLessThan(3)
|
|
|
|
// max <= 0 returns 0
|
|
expect(rollD2LimitedRandom(100, 0)).toBe(0)
|
|
expect(rollD2LimitedRandom(100, -5)).toBe(0)
|
|
// max = 1 returns 0
|
|
expect(rollD2LimitedRandom(100, 1)).toBe(0)
|
|
})
|
|
|
|
test("resolvePickedFileIndex picks deterministic variant indices", () => {
|
|
const idxA1 = resolvePickedFileIndex(4, 42)
|
|
const idxA2 = resolvePickedFileIndex(4, 42)
|
|
expect(idxA1).toBe(idxA2)
|
|
|
|
const idxB = resolvePickedFileIndex(4, 99999)
|
|
expect(idxA1).toBeGreaterThanOrEqual(0)
|
|
expect(idxA1).toBeLessThan(4)
|
|
expect(idxB).toBeGreaterThanOrEqual(0)
|
|
expect(idxB).toBeLessThan(4)
|
|
})
|
|
|
|
test("resolvePickedFile returns matching element or undefined for empty array", () => {
|
|
const variants = ["fileA.ds1", "fileB.ds1", "fileC.ds1"]
|
|
const picked = resolvePickedFile(variants, 12345)
|
|
expect(variants).toContain(picked)
|
|
expect(resolvePickedFile([], 12345)).toBeUndefined()
|
|
})
|
|
})
|
|
|
|
describe("Preset Generation Room Integration and Variant Resolution", () => {
|
|
test("generateChaosSanctuary attaches 225 sliced 8x8 rooms and partitions wing objects", () => {
|
|
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",
|
|
"Bridge 2",
|
|
]
|
|
const pieces: PresetPiece[] = names.map((name, idx) => {
|
|
const baseDs1 = makeMockDs1(24, 24, 10 + idx)
|
|
const ds1: Ds1 = {
|
|
...baseDs1,
|
|
objects: [{ type: 2, id: 300 + idx, x: 5, y: 5, flags: 0 }],
|
|
}
|
|
return { name, levels: [ds1] }
|
|
})
|
|
|
|
const result = generateChaosSanctuary({
|
|
levelId: CHAOS_SANCTUARY_LEVEL_ID,
|
|
pieces,
|
|
})
|
|
|
|
// 120 / 8 = 15 -> 15 x 15 = 225 rooms
|
|
expect(result.rooms?.length).toBe(225)
|
|
expect(result.stats.rooms).toBe(225)
|
|
|
|
// Arm N object at (48, 0) in cells -> cell (49, 1) -> room (gridX: 6, gridY: 0)
|
|
const armNRoom = result.rooms?.find(r => r.gridX === 6 && r.gridY === 0)
|
|
expect(armNRoom?.objects.some(o => o.id === 300)).toBe(true)
|
|
})
|
|
|
|
test("generatePreset resolves multi-level variants and records nPickedFile", () => {
|
|
const variant0 = makeMockDs1(20, 20, 1)
|
|
const variant1 = makeMockDs1(20, 20, 2)
|
|
const variant2 = makeMockDs1(20, 20, 3)
|
|
|
|
const pieces: PresetPiece[] = [
|
|
{
|
|
name: "MultiVariantLevel",
|
|
levels: [variant0, variant1, variant2],
|
|
},
|
|
]
|
|
|
|
const seed = 54321
|
|
const expectedIndex = resolvePickedFileIndex(3, seed)
|
|
|
|
const result = generatePreset({
|
|
levelId: 38,
|
|
levelName: "MultiVariantLevel",
|
|
seed,
|
|
pieces,
|
|
})
|
|
|
|
expect(result.stats.nPickedFile).toBe(expectedIndex)
|
|
expect(result.level.cells[0]?.[0]?.floors[0]?.style).toBe(expectedIndex + 1)
|
|
expect(result.rooms).toBeDefined()
|
|
expect(result.rooms?.length).toBe(9) // 20x20 -> 3x3 = 9 rooms
|
|
expect(result.stats.rooms).toBe(9)
|
|
})
|
|
|
|
test("generatePreset resolves multi-piece candidates and records nPickedFile", () => {
|
|
const pieces: PresetPiece[] = [
|
|
{ name: "VariantPiece_0", levels: [makeMockDs1(16, 16, 1)] },
|
|
{ name: "VariantPiece_1", levels: [makeMockDs1(16, 16, 2)] },
|
|
{ name: "VariantPiece_2", levels: [makeMockDs1(16, 16, 3)] },
|
|
{ name: "VariantPiece_3", levels: [makeMockDs1(16, 16, 4)] },
|
|
]
|
|
|
|
const seed = 777
|
|
const expectedIndex = resolvePickedFileIndex(4, seed)
|
|
|
|
const result = generatePreset({
|
|
levelId: 14,
|
|
levelName: "CountessTower",
|
|
seed,
|
|
pieces,
|
|
})
|
|
|
|
expect(result.stats.nPickedFile).toBe(expectedIndex)
|
|
expect(result.level.cells[0]?.[0]?.floors[0]?.style).toBe(expectedIndex + 1)
|
|
expect(result.rooms?.length).toBe(4) // 16x16 -> 2x2 = 4 rooms
|
|
})
|
|
|
|
test("generatePreset stamps onto canvas when requested size differs from DS1", () => {
|
|
const ds1 = makeMockDs1(16, 16, 1)
|
|
const pieces: PresetPiece[] = [{ name: "SmallPiece", levels: [ds1] }]
|
|
|
|
const result = generatePreset({
|
|
levelId: 33,
|
|
sizeX: 32,
|
|
sizeY: 24,
|
|
pieces,
|
|
})
|
|
|
|
expect(result.level.width).toBe(32)
|
|
expect(result.level.height).toBe(24)
|
|
// 32/8 = 4 columns, 24/8 = 3 rows -> 12 rooms
|
|
expect(result.rooms?.length).toBe(12)
|
|
expect(result.stats.rooms).toBe(12)
|
|
})
|
|
})
|