import { describe, expect, it } from "vitest"; import { createDungeonEncounterSnapshot, reduceDungeonEncounter, resetDungeonEncounterSnapshot, validateDungeonEncounterDefinition, } from "./dungeonEncounterRuntime"; import type { DungeonEncounterAction, DungeonEncounterGuard, DungeonEncounterTrigger, } from "./dungeonEncounterTypes"; import { DUNGEON_REGISTRY } from "./dungeonRegistry"; import { validateDungeonWorldObject } from "./dungeonWorldObjects"; import type { DungeonDefinition } from "./dungeonTypes"; import { ASCENSION_CUSTOM_ENCOUNTER_EVIDENCE, withAscensionCustomEncounterContent, } from "./ascensionCustomEncounterCatalog"; const CUSTOM_DUNGEON_IDS = [ "frozen-reach", "forgotten-mine", "tinkertech-showdown", ] as const; function adaptedDungeon(id: typeof CUSTOM_DUNGEON_IDS[number]): DungeonDefinition { const source = DUNGEON_REGISTRY[id]; expect(source, id).toBeDefined(); return withAscensionCustomEncounterContent(source); } function runtimeSpawnIds(definition: DungeonDefinition): readonly string[] { return [ ...definition.staticSpawns.map((spawn) => spawn.id), ...definition.roamingPacks.flatMap((pack) => pack.members.map((member) => `${pack.id}:${member.id}`)), ...(definition.encounterSpawnGroups ?? []).flatMap((group) => group.spawns.map((spawn) => spawn.id)), ]; } function expectTriggerReferences( trigger: DungeonEncounterTrigger, spawnIds: ReadonlySet, objectIds: ReadonlySet, ): void { if (trigger.kind === "entity-death" || trigger.kind === "entity-engaged" || trigger.kind === "entity-health") { expect(spawnIds, `trigger entity ${trigger.entityId}`).toContain(trigger.entityId); } if (trigger.kind === "entities-dead") { for (const entityId of trigger.entityIds) expect(spawnIds, `trigger entity ${entityId}`).toContain(entityId); } if (trigger.kind === "interact") expect(objectIds, `trigger object ${trigger.objectId}`).toContain(trigger.objectId); } function expectGuardReferences( guard: DungeonEncounterGuard, groupIds: ReadonlySet, objectIds: ReadonlySet, objectiveIds: ReadonlySet, ): void { if (guard.kind === "spawn-group-active") expect(groupIds).toContain(guard.spawnGroupId); if (guard.kind === "world-object-state") expect(objectIds).toContain(guard.objectId); if (guard.kind === "objective-complete") expect(objectiveIds).toContain(guard.objectiveId); } function expectActionReferences( action: DungeonEncounterAction, groupIds: ReadonlySet, objectsById: ReadonlyMap, objectiveIds: ReadonlySet, ): void { if (action.kind === "activate-spawn-group" || action.kind === "deactivate-spawn-group") { expect(groupIds, `action group ${action.spawnGroupId}`).toContain(action.spawnGroupId); } if (action.kind === "set-world-object-state") { const object = objectsById.get(action.objectId); expect(object, `action object ${action.objectId}`).toBeDefined(); expect(object?.states.map((state) => state.id), `object state ${action.objectId}:${action.state}`) .toContain(action.state); } if (action.kind === "complete-objective") expect(objectiveIds).toContain(action.objectiveId); } describe("Ascension custom encounter content", () => { it("attaches idempotently with no dangling or duplicate static/runtime references", () => { expect(ASCENSION_CUSTOM_ENCOUNTER_EVIDENCE.map((entry) => entry.dungeonId).sort()) .toEqual([...CUSTOM_DUNGEON_IDS].sort()); for (const id of CUSTOM_DUNGEON_IDS) { const definition = adaptedDungeon(id); expect(withAscensionCustomEncounterContent(definition)).toBe(definition); const spawnIdList = runtimeSpawnIds(definition); const spawnIds = new Set(spawnIdList); expect(spawnIds.size, `${id} duplicate runtime spawn ids`).toBe(spawnIdList.length); const groupIds = new Set((definition.encounterSpawnGroups ?? []).map((group) => group.id)); const objectIds = new Set(definition.serverGameObjects.map((object) => object.id)); const objectsById = new Map(definition.serverGameObjects.map((object) => [object.id, object] as const)); const objectiveIds = new Set([ ...definition.bosses.map((objective) => objective.id), ...definition.objectiveOrder.map((objective) => objective.id), ]); for (const spawn of definition.staticSpawns) { expect(definition.entities[spawn.entityId], `${id} static ${spawn.id}`).toBeDefined(); } for (const group of definition.encounterSpawnGroups ?? []) { for (const spawn of group.spawns) { expect(definition.entities[spawn.entityId], `${id} group ${group.id}:${spawn.id}`).toBeDefined(); } } for (const object of definition.serverGameObjects) validateDungeonWorldObject(object); for (const encounter of definition.encounters ?? []) { validateDungeonEncounterDefinition(encounter); expect(encounter.dungeonId).toBe(id); for (const rule of encounter.rules) { expectTriggerReferences(rule.trigger, spawnIds, objectIds); for (const guard of rule.guards ?? []) { expectGuardReferences(guard, groupIds, objectIds, objectiveIds); } for (const action of rule.actions) { expectActionReferences(action, groupIds, objectsById, objectiveIds); } } } } }); it("removes Frozen Reach placeholder trash and drives the Arktos-to-vengeance lifecycle", () => { const definition = adaptedDungeon("frozen-reach"); expect(definition.staticSpawns.map((spawn) => spawn.id)).toEqual(["encounter-2168"]); expect(definition.entities["frozen-reach-guardian"]).toBeUndefined(); expect(definition.entities["frozen-reach-caster"]).toBeUndefined(); expect(definition.entities["frozen-reach-brute"]).toBeUndefined(); expect(definition.entities["entry-97700"]).toMatchObject({ name: "Alva", combat: { serverEntry: 97_700 }, }); expect(definition.entities["frozen-reach:arktos"]).toMatchObject({ name: "Arktos", combat: { serverEntry: 97_701 }, }); const alvaSpellIds = definition.entities["entry-97700"].combat?.attacks .flatMap((entry) => entry.spellId ?? []); expect(alvaSpellIds).toEqual(expect.arrayContaining([ 255_347, 255_349, 255_352, 255_358, 255_363, 255_394, 255_397, 255_410, 255_419, 255_421, ])); const encounter = definition.encounters?.find((entry) => entry.id === "frozen-reach:alva-and-arktos"); expect(encounter?.source?.fidelity).toBe("source-backed-adapted"); const arktosGroup = definition.encounterSpawnGroups ?.find((group) => group.id === "frozen-reach:arktos-attempt"); expect(arktosGroup?.spawns).toMatchObject([{ id: "frozen-reach:arktos", entityId: "frozen-reach:arktos", }]); let snapshot = createDungeonEncounterSnapshot(encounter!, 0); snapshot = reduceDungeonEncounter(encounter!, snapshot, { kind: "tick", now: 1, }).snapshot; expect(snapshot).toMatchObject({ phase: "duo-active", activeSpawnGroupIds: ["frozen-reach:arktos-attempt"], }); snapshot = reduceDungeonEncounter(encounter!, snapshot, { kind: "entity-death", entityId: "frozen-reach:arktos", now: 1_000, }).snapshot; expect(snapshot).toMatchObject({ phase: "vengeance", flags: { "arktos-defeated": true }, activeSpawnGroupIds: ["frozen-reach:rebirth-alvajar"], }); const addIds = definition.encounterSpawnGroups ?.find((group) => group.id === "frozen-reach:rebirth-alvajar") ?.spawns.map((spawn) => spawn.id) ?? []; snapshot = reduceDungeonEncounter(encounter!, snapshot, { kind: "tick", now: 2_000, facts: { registeredEntityIds: addIds, deadEntityIds: addIds }, }).snapshot; expect(snapshot.phase).toBe("vengeance"); expect(snapshot.activeSpawnGroupIds).toEqual([]); const reset = resetDungeonEncounterSnapshot(encounter!, snapshot, "boss-wipe", 3_000); expect(reset).toMatchObject({ phase: "duo-active", flags: { "arktos-defeated": false }, activeSpawnGroupIds: [], }); snapshot = reduceDungeonEncounter(encounter!, reset, { kind: "tick", now: 3_001, }).snapshot; expect(snapshot.activeSpawnGroupIds).toEqual(["frozen-reach:arktos-attempt"]); snapshot = reduceDungeonEncounter(encounter!, snapshot, { kind: "entity-death", entityId: "frozen-reach:arktos", now: 4_000, }).snapshot; expect(snapshot).toMatchObject({ phase: "vengeance", flags: { "arktos-defeated": true }, activeSpawnGroupIds: ["frozen-reach:rebirth-alvajar"], }); }); it("runs Vek's Geospike, Resonance, and repeatable Resonite shard lifecycle", () => { const definition = adaptedDungeon("forgotten-mine"); expect(definition.entities["entry-254272"]).toMatchObject({ name: "Geolord Vek", combat: { serverEntry: 254_272 }, }); expect(definition.entities["forgotten-mine-guardian"].name).toBe("Forgotten Mine Stoneguard"); expect(definition.entities["forgotten-mine-caster"].name).toBe("Forgotten Mine Geomancer"); expect(definition.entities["forgotten-mine-brute"].name).toBe("Forgotten Mine Quarry Brute"); expect(definition.entities["entry-254272"].combat?.attacks.flatMap((entry) => entry.spellId ?? [])) .toEqual(expect.arrayContaining([354_000, 354_001, 354_002, 354_004, 354_013, 354_015, 354_017])); const shards = definition.serverGameObjects.filter((object) => object.source?.templateId === 254_051); expect(shards).toHaveLength(3); for (const shard of shards) { expect(shard).toMatchObject({ kind: "resource", halfExtents: [2.25, 2.4, 2.25], interaction: { range: 4.5, allowedStates: ["ready"] }, source: { fidelity: "source-backed-adapted", templateId: 254_051 }, }); } const encounter = definition.encounters?.find((entry) => entry.id === "forgotten-mine:geolord-vek"); let snapshot = createDungeonEncounterSnapshot(encounter!, 0); snapshot = reduceDungeonEncounter(encounter!, snapshot, { kind: "signal", signalId: "forgotten-mine:geospike-cast", now: 100, }).snapshot; expect(snapshot.phase).toBe("pre-resonance-geospike"); expect(snapshot.activeSpawnGroupIds).toEqual(["forgotten-mine:geospike"]); snapshot = reduceDungeonEncounter(encounter!, snapshot, { kind: "tick", now: 3_599 }).snapshot; expect(snapshot.activeSpawnGroupIds).toEqual(["forgotten-mine:geospike"]); snapshot = reduceDungeonEncounter(encounter!, snapshot, { kind: "tick", now: 3_600 }).snapshot; expect(snapshot.phase).toBe("boss-active"); expect(snapshot.activeSpawnGroupIds).toEqual([]); snapshot = reduceDungeonEncounter(encounter!, snapshot, { kind: "entity-health", entityId: "encounter-5069", healthPercent: 50, now: 4_000, }).snapshot; expect(snapshot.phase).toBe("resonance-active"); expect(Object.values(snapshot.worldObjectStates)).toEqual(["ready", "ready", "ready"]); snapshot = reduceDungeonEncounter(encounter!, snapshot, { kind: "interact", objectId: "forgotten-mine:resonite-shard-1", now: 5_000, }).snapshot; expect(snapshot).toMatchObject({ phase: "resonance-cleansed", counters: { "resonite-cleanses": 1 }, worldObjectStates: { "forgotten-mine:resonite-shard-1": "consumed" }, }); snapshot = reduceDungeonEncounter(encounter!, snapshot, { kind: "tick", now: 12_999 }).snapshot; expect(snapshot.phase).toBe("resonance-cleansed"); snapshot = reduceDungeonEncounter(encounter!, snapshot, { kind: "tick", now: 13_000 }).snapshot; expect(snapshot.phase).toBe("resonance-recharge"); snapshot = reduceDungeonEncounter(encounter!, snapshot, { kind: "tick", now: 35_000 }).snapshot; expect(snapshot.phase).toBe("resonance-active"); expect(Object.values(snapshot.worldObjectStates)).toEqual(["ready", "ready", "ready"]); snapshot = reduceDungeonEncounter(encounter!, snapshot, { kind: "interact", objectId: "forgotten-mine:resonite-shard-1", now: 36_000, }).snapshot; expect(snapshot.counters["resonite-cleanses"]).toBe(2); const reset = resetDungeonEncounterSnapshot(encounter!, snapshot, "boss-wipe", 37_000); expect(reset.phase).toBe("boss-active"); expect(Object.values(reset.worldObjectStates)).toEqual(["dormant", "dormant", "dormant"]); expect(reset.counters["resonite-cleanses"]).toBe(0); }); it("keeps Tinkertech explicitly authored and switches Mechatron to faster overdrive with reinforcements", () => { const definition = adaptedDungeon("tinkertech-showdown"); expect(definition.entities["entry-663012"]).toMatchObject({ name: "Mechatron", combat: { serverEntry: 663_012 } }); expect(definition.entities["tinkertech-showdown-guardian"].name).toBe("Tinkertech Guardbot"); expect(definition.entities["tinkertech-showdown-caster"].name).toBe("Tinkertech Coilcaster"); expect(definition.entities["tinkertech-showdown-brute"].name).toBe("Tinkertech Demolisher"); const review = ASCENSION_CUSTOM_ENCOUNTER_EVIDENCE.find((entry) => entry.dungeonId === definition.id)!; expect(review.fidelity).toBe("healer-man-authored"); expect(review.exactSpellFamilies).toEqual({}); expect(review.candidateUnboundSpellFamilies).toHaveProperty("gatlingMissileBarrage"); const attacks = definition.entities["entry-663012"].combat?.attacks ?? []; for (const spellId of [257_438, 257_459, 257_470, 257_472]) { const normal = attacks.find((entry) => entry.spellId === spellId && entry.id.endsWith("-normal")); const overdrive = attacks.find((entry) => entry.spellId === spellId && entry.id.endsWith("-overdrive")); expect(normal, `normal spell ${spellId}`).toBeDefined(); expect(overdrive?.cooldownMs, `overdrive spell ${spellId}`).toBe(Math.round(normal!.cooldownMs * 0.8)); } const encounter = definition.encounters?.find((entry) => entry.id === "tinkertech-showdown:mechatron"); expect(encounter?.source?.fidelity).toBe("healer-man-authored"); let snapshot = createDungeonEncounterSnapshot(encounter!, 0); expect(reduceDungeonEncounter(encounter!, snapshot, { kind: "entity-health", entityId: "encounter-9013", healthPercent: 36, now: 1_000, }).snapshot).toBe(snapshot); snapshot = reduceDungeonEncounter(encounter!, snapshot, { kind: "entity-health", entityId: "encounter-9013", healthPercent: 35, now: 2_000, }).snapshot; expect(snapshot).toMatchObject({ phase: "overdrive", flags: { overdrive: true }, activeSpawnGroupIds: ["tinkertech-showdown:overdrive-reinforcements"], }); const reinforcements = definition.encounterSpawnGroups ?.find((group) => group.id === "tinkertech-showdown:overdrive-reinforcements"); expect(reinforcements?.spawns).toHaveLength(2); expect(reinforcements?.spawns.every((spawn) => spawn.entityId === "tinkertech-showdown-caster")) .toBe(true); const reset = resetDungeonEncounterSnapshot(encounter!, snapshot, "boss-wipe", 3_000); expect(reset).toMatchObject({ phase: "active", flags: { overdrive: false }, activeSpawnGroupIds: [] }); }); });