forked from phenom/healer-man
588 lines
31 KiB
TypeScript
588 lines
31 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import { gunzipSync, strFromU8 } from "fflate";
|
|
import { createHash } from "node:crypto";
|
|
import { readFileSync } from "node:fs";
|
|
import { resolve } from "node:path";
|
|
import {
|
|
createDungeonEncounterSnapshot,
|
|
reduceDungeonEncounter,
|
|
resetDungeonEncounterSnapshot,
|
|
validateDungeonEncounterDefinition,
|
|
} from "./dungeonEncounterRuntime";
|
|
import type {
|
|
DungeonEncounterDefinition,
|
|
DungeonEncounterEvent,
|
|
DungeonEncounterSnapshot,
|
|
} from "./dungeonEncounterTypes";
|
|
import type { DungeonDefinition, DungeonEncounterSpawnGroupDefinition } from "./dungeonTypes";
|
|
import { validateDungeonWorldObject } from "./dungeonWorldObjects";
|
|
import compressedCampaign from "./generated/dungeonCampaignCatalog.compressed";
|
|
import { withWowEncounterContent } from "./wowEncounterContentCatalog";
|
|
import { withWowProgressionObjectContent } from "./wowProgressionObjectContentCatalog";
|
|
import { withWowTbcEncounterContent } from "./wowTbcEncounterContentCatalog";
|
|
import {
|
|
WOW_TBC_ENCOUNTER_BATCH_TWO_REVIEWS,
|
|
withWowTbcEncounterBatchTwoContent,
|
|
} from "./wowTbcEncounterBatchTwoCatalog";
|
|
|
|
const TARGETS = [
|
|
"underbog",
|
|
"slave-pens",
|
|
"mana-tombs",
|
|
"auchenai-crypts",
|
|
"magisters-terrace",
|
|
] as const;
|
|
|
|
const EXPECTED_SOURCE_HASHES = Object.freeze({
|
|
underbog: [
|
|
"89a7406ee90f7a068a9ca0e3c4d8b9f8995325a704bbf9e15dfa75a17c78deca",
|
|
"57aa7cbbdeef9b2bf6bf911e99a118f8b8daa51b9d71e305c673c15b0d101dca",
|
|
"c10c6e02bc9af2e9c8c876f77cda8abf295f79c3240e789315afd156511a25d9",
|
|
"28a3e2b836b349b2104d187fdf8b424455ded2b067f221b209b636c509933ac3",
|
|
"875f6aadf0a44d3119de5cc5751fba232d156ece30bfa5e522bb24f4c005884d",
|
|
"622b5865f171b0b7a4dd93ecba55afd673c393c579485c17f827b31cb6e47dd2",
|
|
"5109b88955dfd4f718e581ca88f0f9ec478fc64cdf0904cea07e17157bd68ccb",
|
|
],
|
|
"slave-pens": [
|
|
"c1dc38b81d430d16b7a55ff128bb8989fa8ef487bc033540cb7cd5bf5d2f2967",
|
|
"1e83c465f3cf74797474ee51e3b8a7f6682ede63ba9b2fdd18841ce1b4bf91e8",
|
|
"09c7f29871b5f2e429c5f643db6f645f0c33a163b143e48e4806ac7231d174d6",
|
|
"622b5865f171b0b7a4dd93ecba55afd673c393c579485c17f827b31cb6e47dd2",
|
|
"f9130f5df57a62c4037dbd21b004537ffc6a7651cf9efe64785998d8fbe0828f",
|
|
"23ef87c37ae66698a2b1aedd23b30b139b5be3bf6040220e6cd45cdcb28b9e67",
|
|
"3e5713c2a0fa480c82b1753bfea6f1b94a4dec4f29a052ba979c049915a90a42",
|
|
"0066e4fd35b809986dd325487515087f5a95f4ffe50ea03846c8d8bf5196870a",
|
|
"d5cce1a83550dcfa9eb2f0251dbb11fd24c272534b2b1a9b230924a44d817ab3",
|
|
],
|
|
"mana-tombs": [
|
|
"f243c4ee42d272202023b0c81762221b96e62029006fcfbafbcd5d2bf7b40c7d",
|
|
"dbb6971658836e2137e6c7a590ce13a790b3d75f79a97edc84e6c8ac8624de1b",
|
|
"490d328811925cb815949e55ba88919512ae11fed6ce7e0c243947a6014a342c",
|
|
"f9407bd388db7434746b6f650ae98772933ef47908fb484e19a8460a16e0e783",
|
|
"622b5865f171b0b7a4dd93ecba55afd673c393c579485c17f827b31cb6e47dd2",
|
|
"f9130f5df57a62c4037dbd21b004537ffc6a7651cf9efe64785998d8fbe0828f",
|
|
"23ef87c37ae66698a2b1aedd23b30b139b5be3bf6040220e6cd45cdcb28b9e67",
|
|
"3e5713c2a0fa480c82b1753bfea6f1b94a4dec4f29a052ba979c049915a90a42",
|
|
"0066e4fd35b809986dd325487515087f5a95f4ffe50ea03846c8d8bf5196870a",
|
|
"d5cce1a83550dcfa9eb2f0251dbb11fd24c272534b2b1a9b230924a44d817ab3",
|
|
],
|
|
"auchenai-crypts": [
|
|
"5a5192f9f89726bf0d73a9b92fccd5e684c47ef22a53465a1ccc6153dabf490c",
|
|
"ee1e867c866a9c023d1257f90c99925975a321a67f2303e83a8373e2da3315e0",
|
|
"970edcd88536186ea675f84ef28cdb3f99b19554f289c5fbb1b42e895d13c8be",
|
|
"6e34fbe51bed87ca4495a8e0b006017ea2de52673d00d6d5148b00e138ebf752",
|
|
"622b5865f171b0b7a4dd93ecba55afd673c393c579485c17f827b31cb6e47dd2",
|
|
"0066e4fd35b809986dd325487515087f5a95f4ffe50ea03846c8d8bf5196870a",
|
|
"d5cce1a83550dcfa9eb2f0251dbb11fd24c272534b2b1a9b230924a44d817ab3",
|
|
],
|
|
"magisters-terrace": [
|
|
"8aa316ceaa914b64e88fe420a0f37537bf016bf5635f8106bf32f82b3eb25728",
|
|
"43ff46dc05da63bfc4ed49cc5fce8451656857cce544f4ed146736285d4f626d",
|
|
"458daa44853b6a94a058ab8ccfe08ae4c8ccd34a0387cb766544e12bcbf45dee",
|
|
"9f5b4a05eb6b6e2acc361f764fd1e472ec255bb9a32369c16e7cc8303e5693e2",
|
|
"25884cccec0c8bf123c73294bbcdcaee7f6da9a7a0a4af8b3e4d79b66c6b3024",
|
|
"ed4a734dcdf8b7e186dd4ceeb0ee7d890784aa18edfeab5acf03180a8da1d47f",
|
|
"3e5713c2a0fa480c82b1753bfea6f1b94a4dec4f29a052ba979c049915a90a42",
|
|
],
|
|
});
|
|
|
|
function decodeBase64(value: string): Uint8Array {
|
|
const decoded = atob(value);
|
|
const bytes = new Uint8Array(decoded.length);
|
|
for (let index = 0; index < decoded.length; index += 1) bytes[index] = decoded.charCodeAt(index);
|
|
return bytes;
|
|
}
|
|
|
|
const campaign = JSON.parse(strFromU8(gunzipSync(decodeBase64(compressedCampaign)))) as {
|
|
readonly definitions: readonly DungeonDefinition[];
|
|
};
|
|
|
|
function adapted(id: typeof TARGETS[number]): DungeonDefinition {
|
|
const generated = campaign.definitions.find((definition) => definition.id === id);
|
|
if (!generated) throw new Error(`Missing generated dungeon ${id}`);
|
|
const baseline = withWowTbcEncounterContent(withWowProgressionObjectContent(withWowEncounterContent(generated)));
|
|
return withWowTbcEncounterBatchTwoContent(baseline);
|
|
}
|
|
|
|
function encounter(definition: DungeonDefinition): DungeonEncounterDefinition {
|
|
const value = definition.encounters?.find((candidate) => candidate.id === `${definition.id}:tbc-batch-two-instance`);
|
|
if (!value) throw new Error(`Missing TBC batch-two encounter for ${definition.id}`);
|
|
return value;
|
|
}
|
|
|
|
function group(definition: DungeonDefinition, id: string): DungeonEncounterSpawnGroupDefinition {
|
|
const value = definition.encounterSpawnGroups?.find((candidate) => candidate.id === id);
|
|
if (!value) throw new Error(`Missing spawn group ${id}`);
|
|
return value;
|
|
}
|
|
|
|
function objectIdForSpawn(definition: DungeonDefinition, spawnId: number): string {
|
|
const object = definition.serverGameObjects.find((candidate) => candidate.source?.spawnId === spawnId);
|
|
if (!object) throw new Error(`${definition.id} missing object spawn ${spawnId}`);
|
|
return object.id;
|
|
}
|
|
|
|
function entryForSpawn(definition: DungeonDefinition, spawnId: string): number | undefined {
|
|
const spawn = definition.staticSpawns.find((candidate) => candidate.id === spawnId)
|
|
?? definition.encounterSpawnGroups?.flatMap((candidate) => candidate.spawns).find((candidate) => candidate.id === spawnId);
|
|
return spawn ? definition.entities[spawn.entityId]?.combat?.serverEntry : undefined;
|
|
}
|
|
|
|
function entityForEntry(definition: DungeonDefinition, entry: number) {
|
|
const entity = Object.values(definition.entities).find((candidate) => candidate.combat?.serverEntry === entry);
|
|
if (!entity) throw new Error(`${definition.id} missing entity entry ${entry}`);
|
|
return entity;
|
|
}
|
|
|
|
function bossIdForEntry(definition: DungeonDefinition, entry: number): string {
|
|
const spawn = definition.staticSpawns.find((candidate) => entryForSpawn(definition, candidate.id) === entry);
|
|
if (!spawn) throw new Error(`${definition.id} missing static boss entry ${entry}`);
|
|
return spawn.id;
|
|
}
|
|
|
|
function dispatch(
|
|
definition: DungeonEncounterDefinition,
|
|
snapshot: DungeonEncounterSnapshot,
|
|
event: DungeonEncounterEvent,
|
|
difficultyId?: "normal" | "heroic",
|
|
): DungeonEncounterSnapshot {
|
|
return reduceDungeonEncounter(definition, snapshot, event, { difficultyId }).snapshot;
|
|
}
|
|
|
|
function killGroup(
|
|
definition: DungeonEncounterDefinition,
|
|
snapshot: DungeonEncounterSnapshot,
|
|
spawnGroup: DungeonEncounterSpawnGroupDefinition,
|
|
now: number,
|
|
difficultyId?: "normal" | "heroic",
|
|
): DungeonEncounterSnapshot {
|
|
const ids = spawnGroup.spawns.map((spawn) => spawn.id);
|
|
return dispatch(definition, snapshot, {
|
|
kind: "tick",
|
|
now,
|
|
facts: { registeredEntityIds: ids, deadEntityIds: ids },
|
|
}, difficultyId);
|
|
}
|
|
|
|
describe("source-backed adapted TBC encounter batch two", () => {
|
|
it("pins every reviewed source hash/anchor and publishes explicit adaptations and gaps", () => {
|
|
expect(WOW_TBC_ENCOUNTER_BATCH_TWO_REVIEWS.map((review) => review.dungeonId)).toEqual(TARGETS);
|
|
for (const review of WOW_TBC_ENCOUNTER_BATCH_TWO_REVIEWS) {
|
|
expect(review.fidelity).toBe("source-backed-adapted");
|
|
expect(review.sourceFiles.map((source) => source.sha256)).toEqual(EXPECTED_SOURCE_HASHES[review.dungeonId]);
|
|
expect(review.adaptations.length).toBeGreaterThan(0);
|
|
expect(review.remainingGaps.length).toBeGreaterThan(0);
|
|
for (const source of review.sourceFiles) {
|
|
expect(source.path).toMatch(/azerothcore-(?:scripts|world).+\.(?:cpp|h|sql)$|epoch-client.+\.dbc$/);
|
|
expect(source.sha256).toMatch(/^[a-f0-9]{64}$/);
|
|
expect(source.anchors.length).toBeGreaterThan(0);
|
|
expect(source.anchors.every((anchor) => /lines? \d+|records? \d+/.test(anchor))).toBe(true);
|
|
expect(createHash("sha256").update(readFileSync(resolve(source.path))).digest("hex"), source.path)
|
|
.toBe(source.sha256);
|
|
}
|
|
const sourceText = encounter(adapted(review.dungeonId)).source?.files.join(" ") ?? "";
|
|
for (const hash of EXPECTED_SOURCE_HASHES[review.dungeonId]) expect(sourceText).toContain(hash);
|
|
}
|
|
});
|
|
|
|
it("gives each reviewed event actor its source schedule and keeps Frozen Core passive", () => {
|
|
const auchenai = adapted("auchenai-crypts");
|
|
const avatar = entityForEntry(auchenai, 18_478);
|
|
expect(avatar.combat?.attacks).toMatchObject([
|
|
{ id: "basic-attack", cooldownMs: 1_391 },
|
|
{
|
|
id: "mortal-strike-16856",
|
|
name: "Mortal Strike",
|
|
spellId: 16_856,
|
|
target: "primary",
|
|
school: "physical",
|
|
initialCooldownMs: [8_500, 16_800],
|
|
repeatCooldownMs: [10_900, 24_100],
|
|
damageMultiplier: 2,
|
|
source: { entry: 18_478, rowId: 0, eventType: 0, actionType: 11, targetType: 2 },
|
|
},
|
|
{
|
|
id: "sunder-armor-16145",
|
|
name: "Sunder Armor",
|
|
spellId: 16_145,
|
|
target: "primary",
|
|
initialCooldownMs: [6_500, 11_500],
|
|
repeatCooldownMs: [6_200, 15_700],
|
|
effect: { kind: "target-aura", aura: "sunder-armor", durationMs: 30_000 },
|
|
source: { entry: 18_478, rowId: 1, eventType: 0, actionType: 11, targetType: 2 },
|
|
},
|
|
]);
|
|
|
|
const manaTombs = adapted("mana-tombs");
|
|
const yor = entityForEntry(manaTombs, 22_930);
|
|
expect(yor.combat?.attacks).toMatchObject([
|
|
{ id: "basic-attack", cooldownMs: 2_000 },
|
|
{
|
|
id: "double-breath-38361",
|
|
name: "Double Breath",
|
|
spellId: 38_361,
|
|
target: "nearby-party",
|
|
school: "fire",
|
|
range: 5,
|
|
initialCooldownMs: [25_500, 30_500],
|
|
repeatCooldownMs: [10_000, 20_000],
|
|
telegraph: { shape: "cone", anchor: "source", length: 20 },
|
|
},
|
|
{
|
|
id: "stomp-36405",
|
|
name: "Stomp",
|
|
spellId: 36_405,
|
|
target: "nearby-party",
|
|
school: "physical",
|
|
initialCooldownMs: [12_000, 18_000],
|
|
repeatCooldownMs: [14_000, 24_000],
|
|
},
|
|
]);
|
|
|
|
const slavePens = adapted("slave-pens");
|
|
const ahune = entityForEntry(slavePens, 25_740);
|
|
const frozenCore = entityForEntry(slavePens, 25_865);
|
|
expect(ahune).toMatchObject({ kind: "boss", hasLoot: true });
|
|
expect(ahune.combat?.attacks).toMatchObject([
|
|
{
|
|
id: "ahunes-shield-45954",
|
|
name: "Ahune's Shield",
|
|
spellId: 45_954,
|
|
target: "self",
|
|
castTimeMs: 0,
|
|
initialCooldownMs: [4, 4],
|
|
maximumCasts: 1,
|
|
effect: { kind: "aura", aura: "ahunes-shield", durationMs: 0, magnitude: 0.75 },
|
|
},
|
|
{
|
|
id: "ahune-spanky-hands-46146",
|
|
name: "[PH] Ahune Spanky Hands",
|
|
spellId: 46_146,
|
|
target: "self",
|
|
castTimeMs: 0,
|
|
initialCooldownMs: [4, 4],
|
|
maximumCasts: 1,
|
|
},
|
|
]);
|
|
expect(frozenCore).toMatchObject({ kind: "mob", hasLoot: false });
|
|
expect(frozenCore.combat?.healthMultiplier).toBe(4.3);
|
|
expect(frozenCore.combat?.attacks).toMatchObject([
|
|
{
|
|
id: "ice-spear-control-aura-46371",
|
|
name: "Ice Spear Control Aura",
|
|
spellId: 46_371,
|
|
target: "self",
|
|
castTimeMs: 0,
|
|
maximumCasts: 1,
|
|
},
|
|
{
|
|
id: "synch-health-46430",
|
|
name: "Synch Health",
|
|
spellId: 46_430,
|
|
target: "self",
|
|
castTimeMs: 0,
|
|
initialCooldownMs: [3_000, 3_000],
|
|
repeatCooldownMs: [3_000, 3_000],
|
|
effect: { kind: "encounter-event", eventId: "ahune-synch-health" },
|
|
},
|
|
]);
|
|
|
|
for (const entity of [avatar, yor, ahune, frozenCore]) {
|
|
expect(entity.combat?.attacks.some((attack) => attack.id !== "basic-attack")).toBe(true);
|
|
expect(entity.combat?.mechanicIds).toEqual(
|
|
entity.combat?.attacks.filter((attack) => attack.id !== "basic-attack").map((attack) => attack.id),
|
|
);
|
|
}
|
|
expect(WOW_TBC_ENCOUNTER_BATCH_TWO_REVIEWS.find((review) => review.dungeonId === "slave-pens")
|
|
?.remainingGaps.join(" ")).toContain("proc-driven Cold Slap");
|
|
});
|
|
|
|
it("validates definitions, stable action ids, references, timeouts, and wrapper idempotency", () => {
|
|
for (const id of TARGETS) {
|
|
const definition = adapted(id);
|
|
const value = encounter(definition);
|
|
validateDungeonEncounterDefinition(value);
|
|
definition.serverGameObjects.forEach(validateDungeonWorldObject);
|
|
const groups = new Set((definition.encounterSpawnGroups ?? []).map((candidate) => candidate.id));
|
|
const objects = new Set(definition.serverGameObjects.map((candidate) => candidate.id));
|
|
const spawns = new Set([
|
|
...definition.staticSpawns.map((spawn) => spawn.id),
|
|
...(definition.encounterSpawnGroups ?? []).flatMap((candidate) => candidate.spawns.map((spawn) => spawn.id)),
|
|
]);
|
|
const actionIds = value.rules.flatMap((rule) => rule.actions.map((action) => action.id));
|
|
expect(new Set(actionIds).size, id).toBe(actionIds.length);
|
|
for (const rule of value.rules) {
|
|
if (rule.trigger.kind === "entities-dead") {
|
|
expect(rule.trigger.timeoutMs, `${id}:${rule.id}`).toBeGreaterThan(0);
|
|
rule.trigger.entityIds.forEach((entityId) => expect(spawns.has(entityId), entityId).toBe(true));
|
|
}
|
|
if (rule.trigger.kind === "entity-engaged" || rule.trigger.kind === "entity-health" || rule.trigger.kind === "entity-death") {
|
|
expect(spawns.has(rule.trigger.entityId), `${id}:${rule.id}:${rule.trigger.entityId}`).toBe(true);
|
|
}
|
|
if (rule.trigger.kind === "interact") expect(objects.has(rule.trigger.objectId), rule.id).toBe(true);
|
|
for (const action of rule.actions) {
|
|
if (action.kind === "activate-spawn-group" || action.kind === "deactivate-spawn-group") {
|
|
expect(groups.has(action.spawnGroupId), action.id).toBe(true);
|
|
}
|
|
if (action.kind === "set-world-object-state") expect(objects.has(action.objectId), action.id).toBe(true);
|
|
}
|
|
}
|
|
expect(withWowTbcEncounterBatchTwoContent(definition)).toBe(definition);
|
|
}
|
|
});
|
|
|
|
it("suppresses event-only/static duplicates and preserves source order and cardinality", () => {
|
|
const underbog = adapted("underbog");
|
|
expect(underbog.staticSpawns.some((spawn) => [18_105, 17_827].includes(entryForSpawn(underbog, spawn.id) ?? -1))).toBe(false);
|
|
expect(group(underbog, "underbog:ghazan-platform").spawns).toHaveLength(1);
|
|
expect(group(underbog, "underbog:claw").spawns).toHaveLength(1);
|
|
expect(underbog.bosses.find((boss) => boss.name === "Ghaz'an")).toMatchObject({ eventOnly: true });
|
|
|
|
const slavePens = adapted("slave-pens");
|
|
expect(slavePens.staticSpawns.some((spawn) => entryForSpawn(slavePens, spawn.id) === 17_893)).toBe(false);
|
|
expect(group(slavePens, "slave-pens:naturalist-attackers").spawns).toHaveLength(3);
|
|
expect(group(slavePens, "slave-pens:ahune-coldwaves").spawns).toHaveLength(2);
|
|
expect(slavePens.objectiveOrder.at(-1)).toMatchObject({ id: "slave-pens:ahune", optional: true, eventOnly: true });
|
|
|
|
const manaTombs = adapted("mana-tombs");
|
|
expect(group(manaTombs, "mana-tombs:pandemonius-room-pull").spawns).toHaveLength(12);
|
|
expect(group(manaTombs, "mana-tombs:shaheen-wave-1").spawns).toHaveLength(4);
|
|
expect(group(manaTombs, "mana-tombs:shaheen-wave-2").spawns).toHaveLength(4);
|
|
expect(group(manaTombs, "mana-tombs:shaheen-wave-3").spawns).toHaveLength(1);
|
|
expect(manaTombs.staticSpawns.some((spawn) => /^creature-833(0[9]|1[0-9]|21)$/.test(spawn.id))).toBe(false);
|
|
expect(manaTombs.objectiveOrder.at(-1)).toMatchObject({ id: "mana-tombs:yor", optional: true, eventOnly: true });
|
|
|
|
const auchenai = adapted("auchenai-crypts");
|
|
expect(group(auchenai, "auchenai-crypts:stolen-soul").spawns).toHaveLength(1);
|
|
expect(group(auchenai, "auchenai-crypts:avatar").spawns).toHaveLength(1);
|
|
|
|
const magisters = adapted("magisters-terrace");
|
|
expect(magisters.staticSpawns.some((spawn) => entryForSpawn(magisters, spawn.id) === 24_560)).toBe(false);
|
|
expect(group(magisters, "magisters-terrace:selin-crystals").spawns).toHaveLength(5);
|
|
expect(group(magisters, "magisters-terrace:delrissa-party").spawns).toHaveLength(5);
|
|
expect(group(magisters, "magisters-terrace:kael-spheres").spawns).toHaveLength(3);
|
|
expect(magisters.objectiveOrder.map((boss) => boss.name)).toEqual([
|
|
"Selin Fireheart", "Vexallus", "Priestess Delrissa", "Kael'thas Sunstrider",
|
|
]);
|
|
});
|
|
|
|
it("replaces only reviewed GO rows by exact source spawnId with visible procedural states", () => {
|
|
const expected = Object.freeze({
|
|
underbog: [],
|
|
"slave-pens": [50_458, 1_267],
|
|
"mana-tombs": [43_146, 2_134_514],
|
|
"auchenai-crypts": [],
|
|
"magisters-terrace": [99_800, 99_802, 99_807, 99_808, 99_809, 99_813],
|
|
} as const);
|
|
for (const id of TARGETS) {
|
|
const definition = adapted(id);
|
|
for (const spawnId of expected[id]) {
|
|
const object = definition.serverGameObjects.find((candidate) => candidate.source?.spawnId === spawnId)!;
|
|
expect(object.source?.fidelity).toBe("source-backed-adapted");
|
|
expect(object.source?.files.length).toBeGreaterThan(0);
|
|
expect(object.visual?.hidden).not.toBe(true);
|
|
}
|
|
}
|
|
const magisters = adapted("magisters-terrace");
|
|
for (const spawnId of [99_802, 99_807, 99_808, 99_809, 99_813]) {
|
|
const door = magisters.serverGameObjects.find((candidate) => candidate.source?.spawnId === spawnId)!;
|
|
expect(door.states.find((state) => state.id === "closed")).toMatchObject({
|
|
visible: true,
|
|
blocksMovement: true,
|
|
blocksSight: true,
|
|
});
|
|
expect(door.states.find((state) => state.id === "open")).toMatchObject({
|
|
visible: false,
|
|
blocksMovement: false,
|
|
blocksSight: false,
|
|
});
|
|
}
|
|
});
|
|
|
|
it("runs Underbog difficulty branches, Ghaz'an release, Claw rescue, and wipe retry", () => {
|
|
const definition = adapted("underbog");
|
|
const value = encounter(definition);
|
|
const hungarfen = bossIdForEntry(definition, 17_770);
|
|
let normal = createDungeonEncounterSnapshot(value);
|
|
normal = dispatch(value, normal, { kind: "entity-engaged", entityId: hungarfen, now: 0 }, "normal");
|
|
normal = dispatch(value, normal, { kind: "tick", now: 9_999 }, "normal");
|
|
expect(normal.activeSpawnGroupIds).toEqual([]);
|
|
normal = dispatch(value, normal, { kind: "tick", now: 10_000 }, "normal");
|
|
expect(normal.activeSpawnGroupIds).toEqual(["underbog:hungarfen-mushroom-normal"]);
|
|
|
|
let heroic = createDungeonEncounterSnapshot(value);
|
|
heroic = dispatch(value, heroic, { kind: "entity-engaged", entityId: hungarfen, now: 0 }, "heroic");
|
|
heroic = dispatch(value, heroic, { kind: "tick", now: 3_000 }, "heroic");
|
|
expect(heroic.activeSpawnGroupIds).toEqual(["underbog:hungarfen-mushroom-heroic"]);
|
|
heroic = resetDungeonEncounterSnapshot(value, heroic, "boss-wipe", 3_001);
|
|
heroic = dispatch(value, heroic, { kind: "tick", now: 3_002 }, "heroic");
|
|
expect(heroic).toMatchObject({ phase: "ready", activeSpawnGroupIds: [] });
|
|
|
|
let snapshot = dispatch(value, heroic, { kind: "interact", objectId: "underbog:ghazan-platform-trigger", now: 4_000 });
|
|
expect(snapshot.activeSpawnGroupIds).toEqual(["underbog:ghazan-platform"]);
|
|
snapshot = dispatch(value, snapshot, { kind: "entity-death", entityId: "creature-138392", now: 4_001 });
|
|
expect(snapshot.completedObjectiveIds).toContain("creature-138392");
|
|
expect(snapshot.flags["ghazan-complete"]).toBe(true);
|
|
const reset = resetDungeonEncounterSnapshot(value, snapshot, "checkpoint", 4_002);
|
|
expect(reset.worldObjectStates["underbog:ghazan-platform-trigger"]).toBe("complete");
|
|
|
|
snapshot = dispatch(value, reset, { kind: "tick", now: 4_003 });
|
|
snapshot = dispatch(value, snapshot, { kind: "entity-engaged", entityId: bossIdForEntry(definition, 17_826), now: 4_004 });
|
|
expect(snapshot.activeSpawnGroupIds).toContain("underbog:claw");
|
|
snapshot = dispatch(value, snapshot, { kind: "entity-health", entityId: "creature-138499", healthPercent: 20, now: 4_005 });
|
|
expect(snapshot.flags["claw-rescued"]).toBe(true);
|
|
expect(snapshot.activeSpawnGroupIds).not.toContain("underbog:claw");
|
|
expect(snapshot.worldObjectStates["underbog:rescued-claw"]).toBe("complete");
|
|
});
|
|
|
|
it("runs Naturalist and Ahune as finite, persistent, retryable Slave Pens events", () => {
|
|
const definition = adapted("slave-pens");
|
|
const value = encounter(definition);
|
|
const cage = objectIdForSpawn(definition, 50_458);
|
|
const iceStone = objectIdForSpawn(definition, 1_267);
|
|
let snapshot = createDungeonEncounterSnapshot(value);
|
|
snapshot = dispatch(value, snapshot, { kind: "interact", objectId: cage, now: 0 });
|
|
snapshot = killGroup(value, snapshot, group(definition, "slave-pens:naturalist-attackers"), 1);
|
|
expect(snapshot.phase).toBe("naturalist-handoff");
|
|
snapshot = dispatch(value, snapshot, { kind: "tick", now: 3_441 });
|
|
expect(snapshot.flags["naturalist-complete"]).toBe(true);
|
|
expect(snapshot.worldObjectStates[cage]).toBe("complete");
|
|
|
|
snapshot = dispatch(value, snapshot, { kind: "interact", objectId: iceStone, now: 4_000 });
|
|
snapshot = dispatch(value, snapshot, { kind: "tick", now: 14_000 });
|
|
expect(snapshot.activeSpawnGroupIds).toEqual(["slave-pens:ahune"]);
|
|
snapshot = resetDungeonEncounterSnapshot(value, snapshot, "boss-wipe", 14_001);
|
|
snapshot = dispatch(value, snapshot, { kind: "tick", now: 14_002 });
|
|
expect(snapshot).toMatchObject({ phase: "ready", activeSpawnGroupIds: [] });
|
|
expect(snapshot.flags["naturalist-complete"]).toBe(true);
|
|
|
|
snapshot = dispatch(value, snapshot, { kind: "interact", objectId: iceStone, now: 15_000 });
|
|
snapshot = dispatch(value, snapshot, { kind: "tick", now: 25_000 });
|
|
snapshot = dispatch(value, snapshot, { kind: "tick", now: 29_000 });
|
|
expect(snapshot.activeSpawnGroupIds).toContain("slave-pens:ahune-hailstone");
|
|
snapshot = dispatch(value, snapshot, { kind: "tick", now: 37_000 });
|
|
expect(snapshot.activeSpawnGroupIds).toContain("slave-pens:ahune-coldwaves");
|
|
snapshot = dispatch(value, snapshot, { kind: "tick", now: 123_000 });
|
|
expect(snapshot.phase).toBe("ahune-core");
|
|
expect(snapshot.activeSpawnGroupIds).toContain("slave-pens:frozen-core");
|
|
snapshot = dispatch(value, snapshot, { kind: "entity-death", entityId: group(definition, "slave-pens:frozen-core").spawns[0].id, now: 123_001 });
|
|
expect(snapshot).toMatchObject({ phase: "ready", flags: { "ahune-complete": true }, activeSpawnGroupIds: [] });
|
|
expect(snapshot.completedObjectiveIds).toContain("slave-pens:ahune");
|
|
});
|
|
|
|
it("runs Mana-Tombs room pull, Sha'heen ambushes, and heroic-only Yor", () => {
|
|
const definition = adapted("mana-tombs");
|
|
const value = encounter(definition);
|
|
let snapshot = createDungeonEncounterSnapshot(value);
|
|
snapshot = dispatch(value, snapshot, { kind: "entity-engaged", entityId: bossIdForEntry(definition, 18_341), now: 0 });
|
|
expect(snapshot.activeSpawnGroupIds).toEqual(["mana-tombs:pandemonius-room-pull"]);
|
|
snapshot = resetDungeonEncounterSnapshot(value, snapshot, "boss-wipe", 1);
|
|
snapshot = dispatch(value, snapshot, { kind: "tick", now: 2 }, "normal");
|
|
expect(snapshot.activeSpawnGroupIds).toEqual([]);
|
|
|
|
const panel = objectIdForSpawn(definition, 43_146);
|
|
snapshot = dispatch(value, snapshot, { kind: "interact", objectId: panel, now: 3 });
|
|
for (let index = 1; index <= 3; index += 1) {
|
|
snapshot = killGroup(value, snapshot, group(definition, `mana-tombs:shaheen-wave-${index}`), 3 + index);
|
|
}
|
|
expect(snapshot.flags["shaheen-complete"]).toBe(true);
|
|
expect(snapshot.worldObjectStates[panel]).toBe("complete");
|
|
const persisted = resetDungeonEncounterSnapshot(value, snapshot, "checkpoint", 7);
|
|
expect(persisted.flags["shaheen-complete"]).toBe(true);
|
|
|
|
const stasis = objectIdForSpawn(definition, 2_134_514);
|
|
let normal = createDungeonEncounterSnapshot(value);
|
|
normal = dispatch(value, normal, { kind: "tick", now: 0 }, "normal");
|
|
expect(normal.worldObjectStates[stasis]).toBe("hidden");
|
|
normal = dispatch(value, normal, { kind: "interact", objectId: stasis, now: 1 }, "normal");
|
|
expect(normal.activeSpawnGroupIds).toEqual([]);
|
|
|
|
let heroic = createDungeonEncounterSnapshot(value);
|
|
heroic = dispatch(value, heroic, { kind: "tick", now: 0 }, "heroic");
|
|
expect(heroic.worldObjectStates[stasis]).toBe("ready");
|
|
heroic = dispatch(value, heroic, { kind: "interact", objectId: stasis, now: 1 }, "heroic");
|
|
expect(heroic.activeSpawnGroupIds).toEqual(["mana-tombs:yor"]);
|
|
heroic = dispatch(value, heroic, { kind: "entity-death", entityId: group(definition, "mana-tombs:yor").spawns[0].id, now: 2 }, "heroic");
|
|
expect(heroic.flags["yor-complete"]).toBe(true);
|
|
expect(heroic.completedObjectiveIds).toContain("mana-tombs:yor");
|
|
});
|
|
|
|
it("runs Maladaar's finite soul/avatar transition and keeps the avatar independently killable", () => {
|
|
const definition = adapted("auchenai-crypts");
|
|
const value = encounter(definition);
|
|
const maladaar = bossIdForEntry(definition, 18_373);
|
|
let snapshot = createDungeonEncounterSnapshot(value);
|
|
snapshot = dispatch(value, snapshot, { kind: "entity-engaged", entityId: maladaar, now: 0 });
|
|
snapshot = dispatch(value, snapshot, { kind: "tick", now: 25_000 });
|
|
expect(snapshot.activeSpawnGroupIds).toContain("auchenai-crypts:stolen-soul");
|
|
snapshot = dispatch(value, snapshot, { kind: "entity-health", entityId: maladaar, healthPercent: 25, now: 25_001 });
|
|
expect(snapshot.activeSpawnGroupIds).toContain("auchenai-crypts:avatar");
|
|
snapshot = dispatch(value, snapshot, { kind: "entity-death", entityId: maladaar, now: 25_002 });
|
|
expect(snapshot.activeSpawnGroupIds).not.toContain("auchenai-crypts:stolen-soul");
|
|
expect(snapshot.activeSpawnGroupIds).toContain("auchenai-crypts:avatar");
|
|
snapshot = dispatch(value, snapshot, { kind: "entity-death", entityId: group(definition, "auchenai-crypts:avatar").spawns[0].id, now: 25_003 });
|
|
expect(snapshot.activeSpawnGroupIds).toEqual([]);
|
|
const reset = resetDungeonEncounterSnapshot(value, snapshot, "boss-wipe", 25_004);
|
|
expect(dispatch(value, reset, { kind: "tick", now: 25_005 }).phase).toBe("ready");
|
|
});
|
|
|
|
it("runs Magisters' exact doors, difficulty branches, Delrissa recovery, and reachable completion", () => {
|
|
const definition = adapted("magisters-terrace");
|
|
const value = encounter(definition);
|
|
const selin = bossIdForEntry(definition, 24_723);
|
|
const vexallus = bossIdForEntry(definition, 24_744);
|
|
const kael = bossIdForEntry(definition, 24_664);
|
|
const selinPassage = objectIdForSpawn(definition, 99_802);
|
|
const selinRoom = objectIdForSpawn(definition, 99_809);
|
|
const vexPassage = objectIdForSpawn(definition, 99_813);
|
|
const delrissaPassage = objectIdForSpawn(definition, 99_807);
|
|
const kaelRoom = objectIdForSpawn(definition, 99_808);
|
|
const orb = objectIdForSpawn(definition, 99_800);
|
|
|
|
let normal = createDungeonEncounterSnapshot(value);
|
|
normal = dispatch(value, normal, { kind: "entity-health", entityId: vexallus, healthPercent: 84, now: 1 }, "normal");
|
|
expect(normal.activeSpawnGroupIds).toEqual(["magisters-terrace:vexallus-85-normal"]);
|
|
expect(group(definition, normal.activeSpawnGroupIds[0]).spawns).toHaveLength(1);
|
|
let heroic = createDungeonEncounterSnapshot(value);
|
|
heroic = dispatch(value, heroic, { kind: "entity-health", entityId: vexallus, healthPercent: 84, now: 1 }, "heroic");
|
|
expect(heroic.activeSpawnGroupIds).toEqual(["magisters-terrace:vexallus-85-heroic"]);
|
|
expect(group(definition, heroic.activeSpawnGroupIds[0]).spawns).toHaveLength(2);
|
|
|
|
let snapshot = createDungeonEncounterSnapshot(value);
|
|
snapshot = dispatch(value, snapshot, { kind: "entity-engaged", entityId: selin, now: 0 });
|
|
expect(snapshot.worldObjectStates[selinRoom]).toBe("closed");
|
|
expect(snapshot.activeSpawnGroupIds).toContain("magisters-terrace:selin-crystals");
|
|
snapshot = dispatch(value, snapshot, { kind: "entity-death", entityId: selin, now: 1 });
|
|
expect(snapshot.worldObjectStates[selinPassage]).toBe("open");
|
|
expect(snapshot.worldObjectStates[selinRoom]).toBe("open");
|
|
|
|
snapshot = dispatch(value, snapshot, { kind: "entity-death", entityId: vexallus, now: 2 });
|
|
expect(snapshot.worldObjectStates[vexPassage]).toBe("open");
|
|
expect(snapshot.activeSpawnGroupIds).toContain("magisters-terrace:delrissa-party");
|
|
const prematureKael = dispatch(value, snapshot, { kind: "entity-death", entityId: kael, now: 2.5 });
|
|
expect(prematureKael.flags["dungeon-complete"]).not.toBe(true);
|
|
expect(prematureKael.completedObjectiveIds).not.toContain("creature-96966");
|
|
snapshot = resetDungeonEncounterSnapshot(value, snapshot, "boss-wipe", 3);
|
|
snapshot = dispatch(value, snapshot, { kind: "tick", now: 4 });
|
|
expect(snapshot.phase).toBe("delrissa-active");
|
|
expect(snapshot.activeSpawnGroupIds).toEqual(["magisters-terrace:delrissa-party"]);
|
|
snapshot = killGroup(value, snapshot, group(definition, "magisters-terrace:delrissa-party"), 5);
|
|
expect(snapshot.completedObjectiveIds).toContain("creature-96966");
|
|
expect(snapshot.worldObjectStates[delrissaPassage]).toBe("open");
|
|
|
|
snapshot = dispatch(value, snapshot, { kind: "entity-engaged", entityId: kael, now: 6 });
|
|
expect(snapshot.worldObjectStates[kaelRoom]).toBe("closed");
|
|
snapshot = dispatch(value, snapshot, { kind: "tick", now: 15_006 });
|
|
expect(snapshot.activeSpawnGroupIds).toContain("magisters-terrace:kael-phoenix");
|
|
snapshot = dispatch(value, snapshot, { kind: "entity-health", entityId: kael, healthPercent: 50, now: 15_007 });
|
|
expect(snapshot.activeSpawnGroupIds).toContain("magisters-terrace:kael-spheres");
|
|
snapshot = dispatch(value, snapshot, { kind: "entity-death", entityId: kael, now: 15_008 });
|
|
expect(snapshot).toMatchObject({ phase: "complete", flags: { "dungeon-complete": true } });
|
|
expect(snapshot.worldObjectStates[kaelRoom]).toBe("open");
|
|
expect(snapshot.worldObjectStates[orb]).toBe("unlocked");
|
|
const reset = resetDungeonEncounterSnapshot(value, snapshot, "checkpoint", 15_009);
|
|
const resumed = dispatch(value, reset, { kind: "tick", now: 15_010 });
|
|
expect(resumed.phase).toBe("complete");
|
|
});
|
|
});
|