diablo2-web/tests/wilderness-superuniques.tes...

488 lines
21 KiB
TypeScript

import { describe, expect, test } from "vitest"
import {
generateWilderness,
buildLandmarkEntitiesFromTables,
spawnLandmarkEntities,
CANONICAL_LANDMARK_ENTITIES,
type LandmarkEntityDef,
type WildernessPiece,
} from "../src/game/wilderness.ts"
import {
planLevelMonsters,
CANONICAL_SUPER_UNIQUES_BY_LEVEL,
CANONICAL_ELITE_MODIFIERS,
lookupSuperUnique,
buildSuperUniqueLandmarkSpecs,
CANONICAL_SUPERUNIQUES_TABLE,
} from "../src/game/monsters.ts"
import { parseTable } from "../src/game/acts.ts"
import type { D2Table } from "../src/game/acts.ts"
import type { Ds1, Ds1Cell } from "../src/formats/ds1.ts"
function mockTable(tsv: string): D2Table {
const encoder = new TextEncoder()
return parseTable(encoder.encode(tsv.trim()))
}
function makeMockDs1(width: number, height: 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: 1, sequence: 0, style: 1, 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,
}
}
describe("Wilderness SuperUniques & Landmark Entities (Issue #57)", () => {
describe("CANONICAL_LANDMARK_ENTITIES Catalog", () => {
test("contains all 19 canonical landmark entities across Acts 2-5", () => {
expect(CANONICAL_LANDMARK_ENTITIES.length).toBe(19)
// Act 1: none. Act I outdoor levels come from the DRLG port (src/game/drlg), whose preset units
// place Bishibosh, Rakanishu, Treehead WoodFist, Blood Raven, the Cow King and Flavie; that is
// checked on the port's output in tests/drlg-act1-population.test.ts.
expect(CANONICAL_LANDMARK_ENTITIES.filter(e => e.act < 2 || e.levelIds.some(id => id < 40))).toEqual([])
// Act 2 (5 entities)
expect(CANONICAL_LANDMARK_ENTITIES.some(e => e.name === "Beetleburst" && e.levelIds.includes(43))).toBe(true)
expect(CANONICAL_LANDMARK_ENTITIES.some(e => e.name === "Dark Elder" && e.levelIds.includes(44))).toBe(true)
expect(CANONICAL_LANDMARK_ENTITIES.some(e => e.name === "Creeping Feature" && e.levelIds.includes(59))).toBe(true)
expect(CANONICAL_LANDMARK_ENTITIES.some(e => e.name === "Bloodwitch the Wild" && e.levelIds.includes(60))).toBe(true)
expect(CANONICAL_LANDMARK_ENTITIES.some(e => e.name === "Fangskin" && e.levelIds.includes(61))).toBe(true)
// Act 3 (4 entities)
expect(CANONICAL_LANDMARK_ENTITIES.some(e => e.name === "Stormtree" && e.levelIds.includes(78))).toBe(true)
expect(CANONICAL_LANDMARK_ENTITIES.some(e => e.name === "Sszark the Burning" && e.levelIds.includes(84))).toBe(true)
expect(CANONICAL_LANDMARK_ENTITIES.some(e => e.name === "Witch Doctor Endugu" && e.levelIds.includes(91))).toBe(true)
expect(CANONICAL_LANDMARK_ENTITIES.some(e => e.name === "Battlemaid Sarina" && e.levelIds.includes(94))).toBe(true)
// Act 4 (3 entities: 2 SuperUniques + 1 NPC)
expect(CANONICAL_LANDMARK_ENTITIES.some(e => e.name === "Izual" && e.levelIds.includes(105))).toBe(true)
expect(CANONICAL_LANDMARK_ENTITIES.some(e => e.name === "Hephasto the Armorer" && e.levelIds.includes(107))).toBe(true)
expect(CANONICAL_LANDMARK_ENTITIES.some(e => e.name === "Hadriel" && e.levelIds.includes(108))).toBe(true)
// Act 5 (7 entities)
expect(CANONICAL_LANDMARK_ENTITIES.some(e => e.name === "Dac Farren" && e.levelIds.includes(110))).toBe(true)
expect(CANONICAL_LANDMARK_ENTITIES.some(e => e.name === "Shenk the Overseer" && e.levelIds.includes(110))).toBe(true)
expect(CANONICAL_LANDMARK_ENTITIES.some(e => e.name === "Eyeback the Unleashed" && e.levelIds.includes(111))).toBe(true)
expect(CANONICAL_LANDMARK_ENTITIES.some(e => e.name === "Thresh Socket" && e.levelIds.includes(112))).toBe(true)
expect(CANONICAL_LANDMARK_ENTITIES.some(e => e.name === "Frozenstein" && e.levelIds.includes(114))).toBe(true)
expect(CANONICAL_LANDMARK_ENTITIES.some(e => e.name === "Bonesaw Breaker" && e.levelIds.includes(115))).toBe(true)
expect(CANONICAL_LANDMARK_ENTITIES.some(e => e.name === "Nihlathak" && e.levelIds.includes(124))).toBe(true)
})
})
describe("buildLandmarkEntitiesFromTables", () => {
test("returns canonical definitions when no tables are supplied", () => {
const entities = buildLandmarkEntitiesFromTables()
expect(entities.length).toBe(CANONICAL_LANDMARK_ENTITIES.length)
expect(entities[0].name).toBe(CANONICAL_LANDMARK_ENTITIES[0].name)
})
test("overrides boss modifiers and properties from SuperUniques.txt", () => {
// Mod id 8 is 'magicresistant', 9 is 'fireenchant', 17 is 'lightenchant', 6 is 'fast'
const superUniquesTsv = [
"Superunique\tClass\tMod1\tMod2\tMod3\tMinGrp\tMaxGrp\tEClass\tAutoPos\tStacks\tUtrans\tTC\tTC(N)\tTC(H)",
"Beetleburst\tscarab3\t8\t9\t0\t2\t5\tscarab1\t0\t0\t0\tAct 2 Super A\tAct 2 Super A\tAct 2 Super A",
"Dark Elder\tzombie4\t17\t6\t0\t3\t6\tzombie1\t0\t0\t0\tAct 2 Super A\tAct 2 Super A\tAct 2 Super A",
].join("\n")
const suTable = mockTable(superUniquesTsv)
const entities = buildLandmarkEntitiesFromTables(suTable)
const beetleburst = entities.find(e => e.id === "Beetleburst")
expect(beetleburst).toBeDefined()
expect(beetleburst?.monsterId).toBe("scarab3")
expect(beetleburst?.modifiers).toEqual(["magicresistant", "fireenchant"])
expect(beetleburst?.minionCount).toEqual([2, 5])
const darkElder = entities.find(e => e.id === "Dark Elder")
expect(darkElder).toBeDefined()
expect(darkElder?.monsterId).toBe("zombie4")
expect(darkElder?.modifiers).toEqual(["lightenchant", "fast"])
expect(darkElder?.minionCount).toEqual([3, 6])
})
test("resolves objectId from MonPreset.txt", () => {
const monPresetTsv = [
"Act\tPlace\tComponent\tObjectId",
"2\tDesert Oasis 1\t1\t105",
"4\tChaos Sanctuary\t1\t350",
].join("\n")
const mpTable = mockTable(monPresetTsv)
const entities = buildLandmarkEntitiesFromTables(undefined, mpTable)
const beetleburst = entities.find(e => e.name === "Beetleburst")
expect(beetleburst?.objectId).toBe(105)
const hadriel = entities.find(e => e.name === "Hadriel")
expect(hadriel?.objectId).toBe(350)
})
})
describe("spawnLandmarkEntities in Wilderness Generator", () => {
// Act I SuperUniques (Rakanishu on the Cairn Stones, ...) are preset units of the DRLG port's
// maps; tests/drlg-act1-population.test.ts checks them. The same anchoring for Acts 2-5:
test("places Creeping Feature centered on the Desert Tomb 1 preset piece", () => {
const tombDs1 = makeMockDs1(16, 16)
const borderDs1 = makeMockDs1(8, 8)
const pieces: WildernessPiece[] = [
{ name: "Act 2 - Desert Border 1", border: true, levels: [borderDs1] },
{ name: "Act 2 - Desert Tomb 1", border: false, levels: [tombDs1] },
]
const result = generateWilderness({
levelId: 41,
levelName: "Rocky Waste",
levelTypeName: "Act 2 - Desert",
sizeX: 80,
sizeY: 80,
subType: 6,
subTheme: 0,
seed: 42,
pieces,
substitutions: [],
specialPresets: ["Act 2 - Desert Tomb 1"],
})
expect(result.stats.landmarkEntities).toBeDefined()
const creeping = result.stats.landmarkEntities?.find(e => e.name === "Creeping Feature")
expect(creeping).toBeDefined()
expect(creeping?.levelId).toBe(41)
// Verify subtile coordinates: tileX * 5 + 2
expect(creeping?.subtileX).toBe(creeping!.tileX * 5 + 2)
expect(creeping?.subtileY).toBe(creeping!.tileY * 5 + 2)
// Verify canvas.objects contains the Type 1 entity
const obj = result.level.objects.find(
o => o.type === 1 && o.x === creeping!.subtileX && o.y === creeping!.subtileY
)
expect(obj).toBeDefined()
expect(obj?.id).toBe(creeping?.objectId)
})
test("places multiple SuperUniques on Level 110 (Bloody Foothills: Dac Farren & Shenk)", () => {
const stripDs1 = makeMockDs1(16, 48)
const pieces: WildernessPiece[] = [
{ name: "Act 5 - Siege To Barricade", border: false, levels: [stripDs1] },
]
const result = generateWilderness({
levelId: 110,
levelName: "Bloody Foothills",
levelTypeName: "Act 5 - Siege",
sizeX: 240,
sizeY: 48,
subType: -1,
subTheme: -1,
seed: 110,
pieces,
substitutions: [],
})
const landmarks = result.stats.landmarkEntities ?? []
const dacFarren = landmarks.find(e => e.name === "Dac Farren")
const shenk = landmarks.find(e => e.name === "Shenk the Overseer")
expect(dacFarren).toBeDefined()
expect(shenk).toBeDefined()
expect(dacFarren?.objectId).toBe(87)
expect(shenk?.objectId).toBe(88)
// Both must be recorded in canvas.objects as type 1
const dacObj = result.level.objects.find(
o => o.type === 1 && o.x === dacFarren!.subtileX && o.y === dacFarren!.subtileY
)
const shenkObj = result.level.objects.find(
o => o.type === 1 && o.x === shenk!.subtileX && o.y === shenk!.subtileY
)
expect(dacObj).toBeDefined()
expect(shenkObj).toBeDefined()
})
test("places Act 4 Hadriel on Chaos Sanctuary (Level 108)", () => {
const csDs1 = makeMockDs1(25, 25)
const pieces: WildernessPiece[] = [
{ name: "Act 4 - Diablo Entry", border: false, levels: [csDs1] },
]
const result = generateWilderness({
levelId: 108,
levelName: "Chaos Sanctuary",
levelTypeName: "Act 4 - Lava",
sizeX: 120,
sizeY: 120,
subType: -1,
subTheme: -1,
seed: 108,
pieces,
substitutions: [],
})
const hadriel = result.stats.landmarkEntities?.find(e => e.name === "Hadriel")
expect(hadriel).toBeDefined()
expect(hadriel?.objectId).toBe(86)
expect(hadriel?.role).toBe("npc_guard")
const obj = result.level.objects.find(
o => o.type === 1 && o.x === hadriel!.subtileX && o.y === hadriel!.subtileY
)
expect(obj).toBeDefined()
expect(obj?.id).toBe(86)
})
test("supports customLandmarks override in WildernessRequest", () => {
const borderDs1 = makeMockDs1(8, 8)
const customLandmarks: LandmarkEntityDef[] = [
{
id: "CustomBoss",
name: "Test Custom Boss",
role: "superunique",
act: 2,
levelIds: [42],
monsterId: "fallen1",
defaultLandmark: "Custom Landmark",
modifiers: ["lightenchant", "fast"],
minionCount: [4, 6],
minionMonsterId: "fallen1",
objectId: 999,
defaultRelativePos: { x: 0.5, y: 0.5 },
},
]
const result = generateWilderness({
levelId: 42,
levelName: "Dry Hills",
levelTypeName: "Act 2 - Desert",
sizeX: 80,
sizeY: 80,
subType: 6,
subTheme: 0,
seed: 123,
pieces: [{ name: "Act 2 - Desert Border 1", border: true, levels: [borderDs1] }],
substitutions: [],
customLandmarks,
})
expect(result.stats.landmarkEntities?.length).toBe(1)
const custom = result.stats.landmarkEntities?.[0]
expect(custom?.name).toBe("Test Custom Boss")
expect(custom?.tileX).toBe(40)
expect(custom?.tileY).toBe(40)
expect(custom?.objectId).toBe(999)
})
test("falls back to relative canvas coordinates when preset pieces are absent", () => {
const borderDs1 = makeMockDs1(8, 8)
const result = generateWilderness({
levelId: 43, // Far Oasis (Beetleburst)
levelName: "Far Oasis",
levelTypeName: "Act 2 - Desert",
sizeX: 100,
sizeY: 100,
subType: 6,
subTheme: 0,
seed: 99,
pieces: [{ name: "Act 2 - Desert Border 1", border: true, levels: [borderDs1] }],
substitutions: [],
})
const beetleburst = result.stats.landmarkEntities?.find(e => e.name === "Beetleburst")
expect(beetleburst).toBeDefined()
expect(beetleburst?.tileX).toBeGreaterThanOrEqual(1)
expect(beetleburst?.tileX).toBeLessThan(99)
expect(beetleburst?.tileY).toBeGreaterThanOrEqual(1)
expect(beetleburst?.tileY).toBeLessThan(99)
})
})
describe("Integration with planLevelMonsters (src/game/monsters.ts)", () => {
test("Aura Enchanted modifier exists in CANONICAL_ELITE_MODIFIERS", () => {
const aura = CANONICAL_ELITE_MODIFIERS.find(m => m.name === "aura")
expect(aura).toBeDefined()
expect(aura?.label).toBe("Aura Enchanted")
expect(aura?.id).toBe(30)
})
test("CANONICAL_SUPER_UNIQUES_BY_LEVEL maps all 5 Acts correctly", () => {
// Act 1
expect(CANONICAL_SUPER_UNIQUES_BY_LEVEL[3]).toBeDefined() // Bishibosh
expect(CANONICAL_SUPER_UNIQUES_BY_LEVEL[4]).toBeDefined() // Rakanishu
expect(CANONICAL_SUPER_UNIQUES_BY_LEVEL[5]).toBeDefined() // Treehead WoodFist
expect(CANONICAL_SUPER_UNIQUES_BY_LEVEL[17]).toBeDefined() // Blood Raven
expect(CANONICAL_SUPER_UNIQUES_BY_LEVEL[25]).toBeDefined() // Countess
// Act 2
expect(CANONICAL_SUPER_UNIQUES_BY_LEVEL[43]).toBeDefined() // Beetleburst
expect(CANONICAL_SUPER_UNIQUES_BY_LEVEL[44]).toBeDefined() // Dark Elder
expect(CANONICAL_SUPER_UNIQUES_BY_LEVEL[59]).toBeDefined() // Creeping Feature
expect(CANONICAL_SUPER_UNIQUES_BY_LEVEL[60]).toBeDefined() // Bloodwitch
expect(CANONICAL_SUPER_UNIQUES_BY_LEVEL[61]).toBeDefined() // Fangskin
// Act 3
expect(CANONICAL_SUPER_UNIQUES_BY_LEVEL[76]).toBeDefined() // Sszark
expect(CANONICAL_SUPER_UNIQUES_BY_LEVEL[78]).toBeDefined() // Stormtree & Endugu
expect(CANONICAL_SUPER_UNIQUES_BY_LEVEL[84]).toBeDefined() // Sszark
expect(CANONICAL_SUPER_UNIQUES_BY_LEVEL[88]).toBeDefined() // Endugu
expect(CANONICAL_SUPER_UNIQUES_BY_LEVEL[91]).toBeDefined() // Endugu
expect(CANONICAL_SUPER_UNIQUES_BY_LEVEL[94]).toBeDefined() // Sarina
// Act 4
expect(CANONICAL_SUPER_UNIQUES_BY_LEVEL[105]).toBeDefined() // Izual
expect(CANONICAL_SUPER_UNIQUES_BY_LEVEL[107]).toBeDefined() // Hephasto
// Act 5
const act5Bloody = CANONICAL_SUPER_UNIQUES_BY_LEVEL[110] // Dac Farren & Shenk
expect(Array.isArray(act5Bloody)).toBe(true)
expect((act5Bloody as readonly unknown[]).length).toBe(2)
expect(CANONICAL_SUPER_UNIQUES_BY_LEVEL[111]).toBeDefined() // Eyeback
expect(CANONICAL_SUPER_UNIQUES_BY_LEVEL[112]).toBeDefined() // Thresh Socket
expect(CANONICAL_SUPER_UNIQUES_BY_LEVEL[114]).toBeDefined() // Frozenstein
expect(CANONICAL_SUPER_UNIQUES_BY_LEVEL[115]).toBeDefined() // Bonesaw
expect(CANONICAL_SUPER_UNIQUES_BY_LEVEL[124]).toBeDefined() // Nihlathak
})
test("planLevelMonsters creates deterministic boss packs for multi-boss level 110", () => {
const levelsTxt = mockTable([
"Id\tName\tMonDen\tMonUMin\tMonUMax\tMonLvl1Ex\tm1\tm2\tm3",
"110\tBloody Foothills\t600\t2\t4\t35\tsiegebeast1\tdemonimp1\tenflayer1",
].join("\n"))
const monstatsTxt = mockTable([
"Id\tBaseId\tNameStr\tLevel\tMinGrp\tMaxGrp\tVelocity\trun\tAC\tExp\tminHP\tmaxHP\tA1MinD\tA1MaxD\tA1TH\tenabled\tissend\tisSpawn\tisMelee",
"imp1\timp1\tDemon Imp\t35\t3\t6\t8\t12\t5\t15\t8\t14\t1\t3\t20\t1\t1\t1\t1",
"demonimp1\tdemonimp1\tDemon Imp\t35\t3\t6\t8\t12\t5\t15\t8\t14\t1\t3\t20\t1\t1\t1\t1",
"overseer1\toverseer1\tOverseer\t35\t1\t2\t7\t10\t10\t35\t16\t26\t3\t6\t30\t1\t1\t1\t1",
"siegebeast1\tsiegebeast1\tSiege Beast\t35\t1\t2\t6\t8\t15\t50\t20\t35\t4\t8\t30\t1\t1\t1\t1",
].join("\n"))
const monlvlTxt = mockTable([
"Level\tAC\tTH\tHP\tDM\tXP",
"35\t100\t100\t100\t100\t100",
].join("\n"))
const tables = { levels: levelsTxt, monstats: monstatsTxt, monlvl: monlvlTxt }
const plan = planLevelMonsters(tables, 110, 240 * 48, 11042, 3.5, "normal")
expect(plan.superUniques).toContain("Dac Farren")
expect(plan.superUniques).toContain("Shenk the Overseer")
const dacPack = plan.packs.find(p => p.superUniqueId === "Dac Farren")
const shenkPack = plan.packs.find(p => p.superUniqueId === "Shenk the Overseer")
expect(dacPack).toBeDefined()
expect(shenkPack).toBeDefined()
expect(dacPack?.members[0]?.rank).toBe("unique")
expect(shenkPack?.members[0]?.rank).toBe("unique")
expect(dacPack?.members[0]?.modifiers?.includes("coldenchant")).toBe(true)
expect(shenkPack?.members[0]?.modifiers?.includes("strong")).toBe(true)
})
test("lookupSuperUnique retrieves canonical superuniques by id or name", () => {
const bishibosh = lookupSuperUnique(CANONICAL_SUPERUNIQUES_TABLE, "Bishibosh")
expect(bishibosh).toBeDefined()
expect(bishibosh?.monsterId).toBe("fallenshaman1")
expect(bishibosh?.minMinions).toBe(5)
expect(bishibosh?.maxMinions).toBe(7)
const caseInsensitive = lookupSuperUnique(CANONICAL_SUPERUNIQUES_TABLE, "rakanishu")
expect(caseInsensitive).toBeDefined()
expect(caseInsensitive?.id).toBe("Rakanishu")
expect(caseInsensitive?.monsterId).toBe("fallen1")
expect(lookupSuperUnique(CANONICAL_SUPERUNIQUES_TABLE, "nonexistent")).toBeUndefined()
})
test("buildSuperUniqueLandmarkSpecs builds specs from custom SuperUniques table", () => {
const customSuTable = mockTable([
"Superunique\tName\tClass\tMod1\tMod2\tMod3\tMinGrp\tMaxGrp\tAutoPos\tStacks\tReplaceable\tUtrans\tTC\tTC(N)\tTC(H)",
"Bishibosh\tBishibosh Custom\tfallenshaman2\t6\t28\t0\t8\t10\t0\t0\t0\t0\tAct 1 Super A\tAct 1 (N) Super A\tAct 1 (H) Super A",
].join("\n"))
const specsByLevel = buildSuperUniqueLandmarkSpecs(customSuTable)
const bishiboshSpec = specsByLevel[3]
expect(bishiboshSpec).toBeDefined()
expect(Array.isArray(bishiboshSpec)).toBe(false)
const spec = bishiboshSpec as import("../src/game/monsters.ts").SuperUniqueLandmarkSpec
expect(spec.name).toBe("Bishibosh Custom")
expect(spec.monsterId).toBe("fallenshaman2")
expect(spec.minMinions).toBe(8)
expect(spec.maxMinions).toBe(10)
expect(spec.modifiers).toEqual(["fast", "stoneskin"])
})
test("planLevelMonsters skips boss without falling back to fallen1 when boss monster is missing", () => {
const levelsTxt = mockTable([
"Id\tName\tMonDen\tMonUMin\tMonUMax\tMonLvl1Ex\tm1\tm2\tm3",
"4\tStony Field\t600\t2\t4\t7\tzombie1\tquillrat1\tbrute1",
].join("\n"))
// Only define zombie1 in monstats - Rakanishu is fallen1, which is absent
const monstatsTxt = mockTable([
"Id\tBaseId\tNameStr\tLevel\tMinGrp\tMaxGrp\tVelocity\trun\tAC\tExp\tminHP\tmaxHP\tA1MinD\tA1MaxD\tA1TH\tenabled\tissend\tisSpawn\tisMelee",
"zombie1\tzombie1\tZombie\t7\t1\t2\t4\t6\t10\t20\t10\t20\t2\t5\t20\t1\t1\t1\t1",
].join("\n"))
const monlvlTxt = mockTable([
"Level\tAC\tTH\tHP\tDM\tXP",
"7\t100\t100\t100\t100\t100",
].join("\n"))
const tables = { levels: levelsTxt, monstats: monstatsTxt, monlvl: monlvlTxt }
const plan = planLevelMonsters(tables, 4, 6400, 12345, 3.5, "normal")
// Rakanishu should NOT be spawned as zombie1 or fallen1 (fallen1 fallback was removed)
expect(plan.superUniques?.includes("Rakanishu")).toBeFalsy()
const rakanishuPack = plan.packs.find(p => p.superUniqueId === "Rakanishu")
expect(rakanishuPack).toBeUndefined()
})
test("planLevelMonsters uses tables.superuniques when provided", () => {
const levelsTxt = mockTable([
"Id\tName\tMonDen\tMonUMin\tMonUMax\tMonLvl1Ex\tm1\tm2\tm3",
"3\tCold Plains\t600\t2\t4\t5\tfallenshaman1\tfallen1\tzombie1",
].join("\n"))
const monstatsTxt = mockTable([
"Id\tBaseId\tNameStr\tLevel\tMinGrp\tMaxGrp\tVelocity\trun\tAC\tExp\tminHP\tmaxHP\tA1MinD\tA1MaxD\tA1TH\tenabled\tissend\tisSpawn\tisMelee",
"fallenshaman1\tfallenshaman1\tFallen Shaman\t5\t1\t1\t6\t9\t10\t20\t15\t25\t2\t4\t20\t1\t1\t1\t1",
"fallen1\tfallen1\tFallen\t5\t3\t5\t8\t12\t5\t10\t5\t10\t1\t2\t15\t1\t1\t1\t1",
].join("\n"))
const monlvlTxt = mockTable([
"Level\tAC\tTH\tHP\tDM\tXP",
"5\t100\t100\t100\t100\t100",
].join("\n"))
const customSuTable = mockTable([
"Superunique\tName\tClass\tMod1\tMod2\tMod3\tMinGrp\tMaxGrp\tAutoPos\tStacks\tReplaceable\tUtrans\tTC\tTC(N)\tTC(H)",
"Bishibosh\tBishibosh Ultra\tfallenshaman1\t6\t0\t0\t3\t3\t0\t0\t0\t0\tAct 1 Super A\tAct 1 (N) Super A\tAct 1 (H) Super A",
].join("\n"))
const tables = { levels: levelsTxt, monstats: monstatsTxt, monlvl: monlvlTxt, superuniques: customSuTable }
const plan = planLevelMonsters(tables, 3, 6400, 54321, 3.5, "normal")
expect(plan.superUniques).toContain("Bishibosh")
const bishiboshPack = plan.packs.find(p => p.superUniqueId === "Bishibosh")
expect(bishiboshPack).toBeDefined()
expect(bishiboshPack?.members[0]?.name).toBe("Bishibosh Ultra")
expect(bishiboshPack?.members[0]?.modifiers).toEqual(["fast"])
// 1 boss + 3 minions = 4 members
expect(bishiboshPack?.members.length).toBe(4)
})
})
})