import { describe, expect, it } from "vitest"; import campaign from "../../dungeon-pipeline/dungeon-campaign.json"; import epochInstances from "../../dungeon-pipeline/epoch-five-player-instances.json"; import generated from "./generated/dungeonCampaignCatalog.json"; import manastormPackages from "./generated/manastormAssetPackages.json"; import { DUNGEON_DEFINITIONS, requireDungeonDefinition } from "./dungeonRegistry"; const SOURCE_NATIVE_DUNGEON_IDS = [ "ahnkahet-old-kingdom", "arcatraz", "auchenai-crypts", "azjol-nerub", "black-morass", "blackfathom-deeps", "blackrock-depths", "blackrock-spire", "blood-furnace", "botanica", "culling-of-stratholme", "draktharon-keep", "escape-from-durnholde", "forge-of-souls", "gnomeregan", "gundrak", "halls-of-lightning", "halls-of-reflection", "halls-of-stone", "magisters-terrace", "mana-tombs", "maraudon", "mechanar", "pit-of-saron", "ragefire-chasm", "scarlet-monastery", "sethekk-halls", "shadow-labyrinth", "shadowfang-keep", "shattered-halls", "slave-pens", "steamvault", "stormwind-stockade", "sunken-temple", "the-nexus", "the-oculus", "trial-of-the-champion", "uldaman", "underbog", "utgarde-keep", "utgarde-pinnacle", "violet-hold", "zulfarrak", ] as const; const WDT_PLACEMENT_DUNGEON_IDS = [ "dire-maul", "hellfire-ramparts", "razorfen-downs", "razorfen-kraul", "scholomance", "stratholme", ] as const; describe("generated dungeon campaign catalog", () => { it("covers only reviewed five-player instances with loadable assets and combat content", () => { expect(generated.definitions).toHaveLength(54); expect(DUNGEON_DEFINITIONS).toHaveLength(84); expect(generated.definitions.map((dungeon) => dungeon.id)) .toEqual(campaign.dungeons.map((dungeon) => dungeon.slug)); for (const dungeon of generated.definitions) { expect(dungeon.assets.visual.length, dungeon.id).toBeGreaterThan(0); expect(dungeon.assets.collision.length, dungeon.id).toBeGreaterThan(0); expect(dungeon.assets.navigation.url, dungeon.id) .toMatch(/^\/assets\/game\/(?:dungeons|manastorm)\//); expect(Object.keys(dungeon.entities).length, dungeon.id).toBeGreaterThan(0); expect(dungeon.staticSpawns.length + dungeon.roamingPacks.length, dungeon.id).toBeGreaterThan(0); expect(dungeon.objectiveOrder.length, dungeon.id).toBeGreaterThan(0); } }); it("runs Wailing Caverns with the complete server population and authored presentation", () => { const wailing = requireDungeonDefinition("wailing-caverns"); const runtimePopulation = wailing.staticSpawns.length + wailing.roamingPacks.reduce((total, pack) => total + pack.members.length, 0); expect(runtimePopulation).toBeGreaterThan(250); expect(wailing.staticSpawns.some((spawn) => spawn.id === "creature-18678")).toBe(true); expect(wailing.staticSpawns.some((spawn) => spawn.id === "lady-anacondra-spawn")).toBe(true); expect(wailing.roamingPacks.some((pack) => pack.id === "kresh-patrol")).toBe(true); expect(wailing.entities["entry-3636"]).toMatchObject({ name: "Deviate Ravager", combat: { serverEntry: 3636 }, visual: { model: { url: "/assets/creatures/wailing-caverns/deviate-ravager-animated.glb" }, }, }); }); it("never replaces a generated dungeon with a smaller live population", () => { for (const source of generated.definitions) { const runtime = requireDungeonDefinition(source.id); const sourcePopulation = source.staticSpawns.length + source.roamingPacks.reduce((total, pack) => total + pack.members.length, 0); const runtimePopulation = runtime.staticSpawns.length + runtime.roamingPacks.reduce((total, pack) => total + pack.members.length, 0); expect(runtimePopulation, source.id).toBeGreaterThanOrEqual(sourcePopulation); } }); it("preserves source-side random rare spawns without making rare bosses mandatory", () => { const chanceSpawns = generated.definitions.flatMap((dungeon) => [ ...dungeon.staticSpawns.filter((spawn) => "spawnChance" in spawn), ...dungeon.roamingPacks.filter((pack) => "spawnChance" in pack), ]) as unknown as ReadonlyArray<{ readonly spawnChance: number }>; const optionalBosses = generated.definitions.flatMap((dungeon) => ( dungeon.objectiveOrder.filter((objective) => "optional" in objective && objective.optional) )); expect(chanceSpawns).toHaveLength(24); expect(chanceSpawns.every((spawn) => spawn.spawnChance > 0 && spawn.spawnChance < 1)).toBe(true); expect(optionalBosses.map((boss) => boss.name)).toEqual([ "Earthcaller Halmgar", "Ragglesnout", "Crystal Fang", "Spirestone Battle Lord", "Spirestone Butcher", "Meshlok the Harvester", ]); }); it("reuses each complete optimized map package for dungeons and Manastorms", () => { const packagesByMap = new Map( manastormPackages.packages.map((assetPackage) => [assetPackage.mapId, assetPackage]), ); const identity = (descriptor: { readonly url: string; readonly checksum: string }) => ({ url: descriptor.url, checksum: descriptor.checksum, }); for (const dungeon of generated.definitions) { const assetPackage = packagesByMap.get(dungeon.mapId); expect(assetPackage, dungeon.id).toBeTruthy(); expect(dungeon.assets.visual.map(identity), dungeon.id).toEqual(assetPackage!.visual.map(identity)); expect(dungeon.assets.collision.map(identity), dungeon.id) .toEqual(assetPackage!.collision.map(identity)); expect(identity(dungeon.assets.navigation), dungeon.id).toEqual(identity(assetPackage!.navigation)); } const shadowfang = generated.definitions.find((dungeon) => dungeon.id === "shadowfang-keep")!; expect(shadowfang.assets.navigation.url) .toMatch(/^\/assets\/game\/manastorm\/33-shadowfang\//); expect(Object.values(shadowfang.entities).every((entity) => entity.visual.model)).toBe(true); expect( Object.values(shadowfang.entities).every( (entity) => ( entity.visual.model?.url.includes("/dungeons/shadowfang-keep/creatures/") || entity.visual.model?.url.includes("/game/creatures/shared/") ), ), ).toBe(true); }); it("ships every selected Epoch map as a true normal/heroic instanced dungeon", () => { expect(epochInstances.dungeons).toHaveLength(18); for (const source of epochInstances.dungeons) { const dungeon = generated.definitions.find((candidate) => candidate.id === source.slug); expect(dungeon, source.slug).toBeTruthy(); expect(dungeon!.mapId, source.slug).toBe(source.mapId); expect(dungeon!.provenance.clientBuild, source.slug).toBe("3.3.5a-Epoch"); expect(dungeon!.difficultyVariants.map((variant) => variant.id), source.slug) .toEqual(["normal", "heroic"]); expect(dungeon!.objectiveOrder.length, source.slug).toBeGreaterThan(0); expect(dungeon!.staticSpawns.length + dungeon!.roamingPacks.length, source.slug) .toBeGreaterThan(0); const entities = Object.values(dungeon!.entities) as ReadonlyArray<{ readonly id: string; readonly name: string; readonly visual: { readonly model?: { readonly url: string } }; }>; const eventOnlyNames = new Set(( dungeon!.objectiveOrder as unknown as ReadonlyArray<{ readonly name: string; readonly eventOnly?: boolean; }> ).filter((objective) => objective.eventOnly).map((objective) => objective.name)); for (const entity of entities) { if (entity.visual.model) { expect(entity.visual.model.url, `${source.slug}:${entity.name}`) .toMatch(new RegExp( `^/assets/(?:game/creatures/shared|game/dungeons/${source.slug}/creatures)/.+\\.glb$`, )); } else { expect( eventOnlyNames.has(entity.name) || /^entry-8\d{8}$/.test(entity.id), `${source.slug}:${entity.name}`, ).toBe(true); } } } }); it("compiles exact scripted boss abilities before generic fallbacks", () => { const forge = generated.definitions.find((dungeon) => dungeon.id === "forge-of-souls"); const culling = generated.definitions.find((dungeon) => dungeon.id === "culling-of-stratholme"); expect(forge).toBeTruthy(); expect(culling).toBeTruthy(); const attackNames = ( dungeon: NonNullable, bossName: string, ): string[] => { const entity = Object.values(dungeon.entities).find((candidate) => candidate.name === bossName); return entity?.combat.attacks.map((attack: { readonly name: string }) => attack.name) ?? []; }; expect(attackNames(forge!, "Bronjahm")).toEqual([ "Basic Attack", "Shadow Bolt", "Fear", "Magic's Bane", "Corrupt Soul", "Soulstorm", ]); expect(attackNames(culling!, "Mal'Ganis")).toEqual([ "Basic Attack", "Carrion Swarm", "Mind Blast", "Sleep", "Vampiric Touch", ]); const attackFor = ( dungeonId: string, bossName: string, attackName: string, ): { readonly effect?: unknown } | undefined => { const dungeon = generated.definitions.find((candidate) => candidate.id === dungeonId); const entity = Object.values(dungeon?.entities ?? {}).find( (candidate) => candidate.name === bossName, ) as undefined | { readonly combat: { readonly attacks: ReadonlyArray<{ readonly name: string; readonly effect?: unknown }>; }; }; return entity?.combat.attacks.find((attack) => attack.name === attackName); }; expect(attackFor("forge-of-souls", "Bronjahm", "Fear")).toMatchObject({ effect: { kind: "control", mechanic: "fear", durationMs: 4_500 }, }); expect(attackFor("culling-of-stratholme", "Mal'Ganis", "Sleep")).toMatchObject({ effect: { kind: "control", mechanic: "sleep", durationMs: 4_500 }, }); expect( attackFor("escape-from-durnholde", "Captain Skarloc", "Holy Light"), ).toMatchObject({ effect: { kind: "heal", multiplier: 1.2 }, }); }); it("compiles Archaedas's timed and health-based encounter mechanics", () => { const uldaman = generated.definitions.find((dungeon) => dungeon.id === "uldaman")!; const archaedas = Object.values(uldaman.entities).find( (entity) => entity.name === "Archaedas", ) as unknown as { readonly combat: { readonly mechanicIds: readonly string[]; readonly attacks: ReadonlyArray<{ readonly id: string; readonly initialCooldownMs?: readonly [number, number]; readonly repeatCooldownMs?: readonly [number, number]; readonly condition?: { readonly kind: string; readonly maximum?: number }; readonly effect?: { readonly kind: string; readonly creatureEntry?: number; readonly count?: number }; readonly telegraph?: { readonly shape: string; readonly anchor: string; readonly radius?: number }; }>; }; }; expect(archaedas.combat.mechanicIds).toEqual([ "archaedas-awaken-earthen-custodian", "archaedas-ground-tremor", "archaedas-awaken-earthen-guardians", "archaedas-awaken-vault-warders", ]); expect(archaedas.combat.attacks.find((attack) => attack.id === "archaedas-ground-tremor")) .toMatchObject({ initialCooldownMs: [0, 10_000], repeatCooldownMs: [14_000, 20_000], telegraph: { shape: "circle", anchor: "source", radius: 18 }, }); expect(archaedas.combat.attacks.find( (attack) => attack.id === "archaedas-awaken-earthen-guardians", )).toMatchObject({ condition: { kind: "self-health-percent", maximum: 70 }, effect: { kind: "summon", creatureEntry: 7076, count: 6 }, }); expect(archaedas.combat.attacks.find( (attack) => attack.id === "archaedas-awaken-vault-warders", )).toMatchObject({ condition: { kind: "self-health-percent", maximum: 40 }, effect: { kind: "summon", creatureEntry: 10120, count: 2 }, }); }); it("does not leak outdoor zones, arenas, or raids into the dungeon browser", () => { const ids = new Set(generated.definitions.map((dungeon) => dungeon.id)); for (const excluded of [ "ashamanes-fall", "redridge-mountains", "westfall-outdoor", "tanaris-outdoor", "nagrand-outdoor", "black-rook-hold-arena", "molten-core", "karazhan", ]) { expect(ids.has(excluded), excluded).toBe(false); } }); it("gives every generated creature a complete animation-driving attack kit", () => { for (const dungeon of generated.definitions) { for (const entity of Object.values(dungeon.entities)) { expect(entity.combat.attacks.length, `${dungeon.id}:${entity.id}`).toBeGreaterThan(0); for (const attack of entity.combat.attacks) { expect(["attack", "cast"], `${dungeon.id}:${entity.id}:${attack.id}`) .toContain(attack.animation); expect(attack.cooldownMs, `${dungeon.id}:${entity.id}:${attack.id}`).toBeGreaterThan(0); if (attack.target === "self") { expect(attack.range, `${dungeon.id}:${entity.id}:${attack.id}`).toBeGreaterThanOrEqual(0); } else { expect(attack.range, `${dungeon.id}:${entity.id}:${attack.id}`).toBeGreaterThan(0); } } } } }); it("binds every boss objective to an actual boss spawn", () => { for (const dungeon of generated.definitions) { const spawns = new Map(dungeon.staticSpawns.map((spawn) => [spawn.id, spawn])); const entities = dungeon.entities as unknown as Readonly>; for (const boss of dungeon.objectiveOrder) { const spawn = spawns.get(boss.id); if ("eventOnly" in boss && boss.eventOnly) { expect(spawn, `${dungeon.id}:${boss.name}`).toBeUndefined(); expect( Object.values(dungeon.entities).some((entity) => ( entity.kind === "boss" && entity.name === boss.name )), `${dungeon.id}:${boss.name}`, ).toBe(true); continue; } expect(spawn, `${dungeon.id}:${boss.name}`).toBeTruthy(); expect(entities[spawn!.entityId]?.kind, `${dungeon.id}:${boss.name}`).toBe("boss"); } } }); it("aligns the Deadmines source population to its packaged interior route", () => { const deadmines = requireDungeonDefinition("deadmines"); const firstPack = deadmines.staticSpawns.find( (spawn) => spawn.id === "creature-79127", ); expect(deadmines.environmentMode).toBe("adt-hybrid"); expect(deadmines.provenance.coordinateTransform) .toBe("three(x,y,z)=(wow.y,wow.z,wow.x)"); expect(deadmines.entrance.footPosition).toEqual([-383.0017, 61.9028, -16.4018]); expect(firstPack?.position).toEqual([-401.1297, 55.8039, -50.2669]); expect(deadmines.objectiveOrder[0]?.position) .toEqual([-448.1427, 54.5567, -192.9168]); expect(deadmines.objectiveOrder.at(-1)?.name).toBe("Edwin VanCleef"); expect(deadmines.objectiveOrder.at(-1)?.position) .toEqual([-819.8267, 39.4232, -87.3708]); }); it("keeps Stockades creatures in the package's native world coordinates", () => { const stockades = requireDungeonDefinition("stormwind-stockade"); const firstSpawn = stockades.staticSpawns.find( (spawn) => spawn.id === "creature-79035", ); const targorr = stockades.staticSpawns.find( (spawn) => spawn.id === "creature-84027", ); expect(stockades.environmentMode).toBe("global-wmo"); expect(stockades.provenance.coordinateTransform) .toBe("three(x,y,z)=draft(x,y,z)"); expect(stockades.entrance.footPosition).toEqual([-54.23, -18.34, 0.28]); expect(firstSpawn?.position).toEqual([-82.3245, -26.4396, -10.6106]); expect(targorr?.position).toEqual([-159.582, -25.6062, 1.2531]); }); it("starts Ragefire Chasm in the dungeon instead of the Orgrimmar portal annex", () => { const ragefire = requireDungeonDefinition("ragefire-chasm"); const firstSpawn = ragefire.staticSpawns.find( (spawn) => spawn.id === "creature-38041", ); const jergosh = ragefire.staticSpawns.find( (spawn) => spawn.id === "creature-48740", ); expect(ragefire.environmentMode).toBe("global-wmo"); expect(ragefire.provenance.coordinateTransform) .toBe("three(x,y,z)=draft(x,y,z)"); expect(ragefire.entrance.footPosition).toEqual([-3.81, -17.84, -14.82]); expect(firstSpawn?.position).toEqual([45.0298, -21.2917, -27.7645]); expect(jergosh?.position).toEqual([376.811, -21.801, 209.224]); }); it("keeps every navigation-audited dungeon in its native source coordinates", () => { const sourceAligned = generated.definitions .filter((dungeon) => dungeon.provenance.coordinateAlignment === "source") .map((dungeon) => dungeon.id) .sort(); expect(sourceAligned).toEqual([...SOURCE_NATIVE_DUNGEON_IDS].sort()); for (const dungeonId of SOURCE_NATIVE_DUNGEON_IDS) { const dungeon = requireDungeonDefinition(dungeonId); expect(dungeon.provenance.coordinateAlignment, dungeonId).toBe("source"); expect(dungeon.provenance.coordinateTransform, dungeonId) .not.toContain("package-anchor-translation"); expect( dungeon.validation.warnings.some((warning) => ( warning.includes("native world coordinates") )), dungeonId, ).toBe(true); } }); it("pins representative global-WMO and ADT entrances to their source basis", () => { const expectedEntrances = { "blackfathom-deeps": [151.89, -39.87, 106.96], "blackrock-depths": [-456.929, -68.0896, 34.0923], "blood-furnace": [3.9967, -44.8009, 14.6363], "the-nexus": [-145.87, -16.636, -10.554], "violet-hold": [803.93, 44.364, 1808.82], "black-morass": [7034.7, 32.5619, -1496.24], "culling-of-stratholme": [556.92, 36.69, 1431.1], } as const; for (const [dungeonId, expected] of Object.entries(expectedEntrances)) { expect(requireDungeonDefinition(dungeonId).entrance.footPosition, dungeonId) .toEqual(expected); } }); it("keeps scripted Trial of the Champion objectives on the native arena floor", () => { const trial = requireDungeonDefinition("trial-of-the-champion"); const [entranceX, , entranceZ] = trial.entrance.footPosition; expect(trial.objectiveOrder).toHaveLength(5); for (const objective of trial.objectiveOrder) { expect(objective.position[1], objective.name).toBeGreaterThanOrEqual(411); expect(objective.position[1], objective.name).toBeLessThanOrEqual(413); expect( Math.hypot(objective.position[0] - entranceX, objective.position[2] - entranceZ), objective.name, ).toBeLessThan(150); } }); it("uses client WDT placement records for rotated or translated root WMOs", () => { for (const dungeonId of WDT_PLACEMENT_DUNGEON_IDS) { expect( requireDungeonDefinition(dungeonId).provenance.coordinateAlignment, dungeonId, ).toBe("source-transform"); expect( requireDungeonDefinition(dungeonId).provenance.coordinateTransform, dungeonId, ).toContain("rotate-y"); expect( requireDungeonDefinition(dungeonId).provenance.coordinateTransform, dungeonId, ).not.toContain("package-anchor-translation"); } }); it("pins every map found by the portal-map coordinate audit to its dungeon entrance", () => { const expectedEntrances = { arcatraz: [1.2316, -0.2043, 0.0143], "dire-maul": [44.4499, -2.712, 154.822], "hellfire-ramparts": [5.7204, 129.1393, -94.4661], "razorfen-downs": [-190.2691, -10.71, -100.8219], "razorfen-kraul": [-37.901, -5.684, 22.7908], "scarlet-monastery": [1053.48, 18.6775, 1688.99], scholomance: [-55.0407, -13.1583, 20.4141], "shadowfang-keep": [2109.18, 76.8898, -229.135], stratholme: [-143.4892, -5.8848, -266.8634], zulfarrak: [841.59, 8.93, 1213.52], } as const; for (const [dungeonId, expected] of Object.entries(expectedEntrances)) { expect(requireDungeonDefinition(dungeonId).entrance.footPosition, dungeonId) .toEqual(expected); } }); it("starts Razorfen Kraul inside the portal and maps its first pack into the WMO", () => { const razorfenKraul = requireDungeonDefinition("razorfen-kraul"); const firstPackSpawn = razorfenKraul.staticSpawns.find( (spawn) => spawn.id === "creature-87284", ); expect(razorfenKraul.entrance.footPosition).toEqual([-37.901, -5.684, 22.7908]); expect(firstPackSpawn).toBeTruthy(); expect(firstPackSpawn!.position[0]).toBeGreaterThan(razorfenKraul.entrance.footPosition[0]); expect(firstPackSpawn!.position[0]).toBeLessThan(100); expect(razorfenKraul.provenance.coordinateTransform) .toBe("three(x,y,z)=rotate-y(draft(x,y,z)-[-1952.1803,87.684,1587.8923],-133deg)"); }); });