514 lines
21 KiB
TypeScript
514 lines
21 KiB
TypeScript
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<typeof forge>,
|
|
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<Record<string, { readonly kind: string }>>;
|
|
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)");
|
|
});
|
|
});
|