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