forked from phenom/healer-man
Release Healer Man 0.1.22
This commit is contained in:
1 parent
f6f2375145
commit
7f6001723a
144 files changed
+54107
-690
No files matched your search
@@ -1,9 +1,18 @@
|
||||
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 gameObjectAssetManifest from "../../dungeon-pipeline/WOW_GAMEOBJECT_ASSET_MANIFEST.json";
|
||||
import generated from "./generated/dungeonCampaignCatalog.json";
|
||||
import manastormPackages from "./generated/manastormAssetPackages.json";
|
||||
import { DUNGEON_DEFINITIONS, requireDungeonDefinition } from "./dungeonRegistry";
|
||||
import {
|
||||
createDungeonEncounterSnapshot,
|
||||
reduceDungeonEncounter,
|
||||
resetDungeonEncounterSnapshot,
|
||||
validateDungeonEncounterDefinition,
|
||||
} from "./dungeonEncounterRuntime";
|
||||
import type { DungeonEncounterDefinition } from "./dungeonEncounterTypes";
|
||||
import { validateDungeonWorldObject } from "./dungeonWorldObjects";
|
||||
|
||||
const SOURCE_NATIVE_DUNGEON_IDS = [
|
||||
"ahnkahet-old-kingdom",
|
||||
@@ -77,6 +86,313 @@ describe("generated dungeon campaign catalog", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("never exposes raw database gameobject rows to the live world-object renderer", () => {
|
||||
for (const dungeon of DUNGEON_DEFINITIONS) {
|
||||
for (const object of dungeon.serverGameObjects) {
|
||||
expect(() => validateDungeonWorldObject(object), `${dungeon.id}:${object.id}`).not.toThrow();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it("gives every WoW dungeon executable persisted boss progression", () => {
|
||||
const wowDungeons = DUNGEON_DEFINITIONS.filter((dungeon) => (
|
||||
campaign.dungeons.some((entry) => entry.slug === dungeon.id)
|
||||
));
|
||||
expect(wowDungeons).toHaveLength(54);
|
||||
|
||||
for (const dungeon of wowDungeons) {
|
||||
const objectiveIds = dungeon.objectiveOrder.map((objective) => objective.id);
|
||||
expect(new Set(objectiveIds).size, `${dungeon.id}:duplicate-objective-id`).toBe(objectiveIds.length);
|
||||
const encounters = dungeon.encounters ?? [];
|
||||
for (const encounter of encounters) {
|
||||
expect(() => validateDungeonEncounterDefinition(encounter), `${dungeon.id}:${encounter.id}`).not.toThrow();
|
||||
}
|
||||
const allCompletedObjectiveIds = new Set(encounters.flatMap((encounter) => (
|
||||
encounter.rules.flatMap((rule) => rule.actions.flatMap((action) => (
|
||||
action.kind === "complete-objective" ? [action.objectiveId] : []
|
||||
)))
|
||||
)));
|
||||
for (const objective of dungeon.objectiveOrder) {
|
||||
expect(allCompletedObjectiveIds.has(objective.id), `${dungeon.id}:${objective.name}`).toBe(true);
|
||||
}
|
||||
const progression = dungeon.encounters?.find(
|
||||
(encounter) => encounter.source?.scope === "boss-progression",
|
||||
) as DungeonEncounterDefinition | undefined;
|
||||
const instanceCompletion = encounters.find((encounter) => (
|
||||
encounter.source?.scope === "instance-orchestration"
|
||||
&& encounter.rules.some((rule) => rule.actions.some((action) => (
|
||||
action.kind === "set-flag"
|
||||
&& action.flagId === "dungeon-complete"
|
||||
&& action.value
|
||||
)))
|
||||
));
|
||||
const authority = progression ?? instanceCompletion;
|
||||
expect(authority, dungeon.id).toBeDefined();
|
||||
expect(authority!.source?.fidelity, dungeon.id).toBe(
|
||||
dungeon.id === "tinkertech-showdown"
|
||||
? "healer-man-authored"
|
||||
: "source-backed-adapted",
|
||||
);
|
||||
|
||||
if (!progression) {
|
||||
expect(instanceCompletion?.source?.scope, dungeon.id).toBe("instance-orchestration");
|
||||
const completedObjectiveIds = new Set(instanceCompletion!.rules.flatMap((rule) => (
|
||||
rule.actions.flatMap((action) => (
|
||||
action.kind === "complete-objective" ? [action.objectiveId] : []
|
||||
))
|
||||
)));
|
||||
for (const objective of dungeon.objectiveOrder.filter((candidate) => !candidate.optional)) {
|
||||
expect(completedObjectiveIds.has(objective.id), `${dungeon.id}:${objective.name}`).toBe(true);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
const progressionObjectiveIds = new Set(progression.rules.flatMap((rule) => (
|
||||
rule.actions.flatMap((action) => (
|
||||
action.kind === "complete-objective" ? [action.objectiveId] : []
|
||||
))
|
||||
)));
|
||||
const runtimeBosses = dungeon.objectiveOrder.filter((objective) => (
|
||||
progressionObjectiveIds.has(objective.id)
|
||||
));
|
||||
let snapshot = createDungeonEncounterSnapshot(progression, 1_000);
|
||||
const deadEntityIds: string[] = [];
|
||||
for (const boss of runtimeBosses) {
|
||||
deadEntityIds.push(boss.id);
|
||||
snapshot = reduceDungeonEncounter(progression, snapshot, {
|
||||
kind: "tick",
|
||||
now: 1_000 + deadEntityIds.length * 100,
|
||||
facts: {
|
||||
registeredEntityIds: runtimeBosses.map((objective) => objective.id),
|
||||
deadEntityIds,
|
||||
},
|
||||
}).snapshot;
|
||||
expect(snapshot.completedObjectiveIds, `${dungeon.id}:${boss.id}`).toContain(boss.id);
|
||||
}
|
||||
|
||||
const checkpoint = resetDungeonEncounterSnapshot(progression, snapshot, "checkpoint", 5_000);
|
||||
expect(checkpoint.completedObjectiveIds, dungeon.id)
|
||||
.toEqual(expect.arrayContaining(runtimeBosses.map((boss) => boss.id)));
|
||||
const completesFromObjectivesAlone = progression.rules.some((rule) => (
|
||||
rule.actions.some((action) => (
|
||||
action.kind === "set-flag" && action.flagId === "dungeon-complete" && action.value
|
||||
))
|
||||
&& (rule.guards ?? []).every((guard) => guard.kind === "objective-complete")
|
||||
));
|
||||
if (!completesFromObjectivesAlone) {
|
||||
// A full source adapter may own completion inside a separate encounter;
|
||||
// its focused lifecycle suite exercises that path.
|
||||
expect(snapshot.flags["dungeon-complete"], dungeon.id).toBe(false);
|
||||
} else {
|
||||
expect(snapshot.flags["dungeon-complete"], dungeon.id).toBe(true);
|
||||
expect(snapshot.phase, dungeon.id).toBe("complete");
|
||||
expect(checkpoint.flags["dungeon-complete"], dungeon.id).toBe(true);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it("has an instance-level source review for every campaign dungeon", () => {
|
||||
const wowDungeons = DUNGEON_DEFINITIONS.filter((dungeon) => (
|
||||
campaign.dungeons.some((entry) => entry.slug === dungeon.id)
|
||||
));
|
||||
const verifiedNoOps: string[] = [];
|
||||
for (const dungeon of wowDungeons) {
|
||||
const reviewed = (dungeon.encounters ?? []).filter(
|
||||
(encounter) => encounter.source?.scope === "instance-orchestration",
|
||||
);
|
||||
expect(reviewed.length, dungeon.id).toBeGreaterThan(0);
|
||||
for (const encounter of reviewed) {
|
||||
expect(encounter.source?.files.length, `${dungeon.id}:${encounter.id}:files`).toBeGreaterThan(0);
|
||||
expect(encounter.source?.reviewedBehavior.trim(), `${dungeon.id}:${encounter.id}:review`).not.toBe("");
|
||||
if (!encounter.rules.length) {
|
||||
expect(encounter.source?.fidelity, `${dungeon.id}:${encounter.id}`).toBe("source-exact");
|
||||
expect(encounter.source?.adaptations, `${dungeon.id}:${encounter.id}`).toEqual([]);
|
||||
expect(encounter.source?.remainingGaps, `${dungeon.id}:${encounter.id}`).toEqual([]);
|
||||
verifiedNoOps.push(dungeon.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
expect(wowDungeons).toHaveLength(54);
|
||||
expect(verifiedNoOps.sort()).toEqual([
|
||||
"ragefire-chasm",
|
||||
"razorfen-kraul",
|
||||
"stormwind-stockade",
|
||||
]);
|
||||
});
|
||||
|
||||
it("gates every required event-only objective behind an instance completion authority", () => {
|
||||
const wowDungeons = DUNGEON_DEFINITIONS.filter((dungeon) => (
|
||||
campaign.dungeons.some((entry) => entry.slug === dungeon.id)
|
||||
));
|
||||
for (const dungeon of wowDungeons) {
|
||||
const requiredEvents = dungeon.objectiveOrder.filter(
|
||||
(objective) => objective.eventOnly && !objective.optional,
|
||||
);
|
||||
if (!requiredEvents.length) continue;
|
||||
const progression = dungeon.encounters?.find(
|
||||
(encounter) => encounter.source?.scope === "boss-progression",
|
||||
);
|
||||
const baselineGatedObjectiveIds = new Set((progression?.rules ?? []).flatMap((rule) => (
|
||||
rule.actions.some((action) => (
|
||||
action.kind === "set-flag"
|
||||
&& action.flagId === "dungeon-complete"
|
||||
&& action.value
|
||||
))
|
||||
? (rule.guards ?? []).flatMap((guard) => (
|
||||
guard.kind === "objective-complete" && guard.equals ? [guard.objectiveId] : []
|
||||
))
|
||||
: []
|
||||
)));
|
||||
const completionAuthorities = (dungeon.encounters ?? []).filter((encounter) => (
|
||||
encounter.source?.scope === "instance-orchestration"
|
||||
&& encounter.rules.some((rule) => rule.actions.some((action) => (
|
||||
action.kind === "set-flag"
|
||||
&& action.flagId === "dungeon-complete"
|
||||
&& action.value
|
||||
)))
|
||||
));
|
||||
for (const objective of requiredEvents) {
|
||||
expect(
|
||||
baselineGatedObjectiveIds.has(objective.id) || completionAuthorities.length > 0,
|
||||
`${dungeon.id}:${objective.name}`,
|
||||
).toBe(true);
|
||||
if (progression && completionAuthorities.length) {
|
||||
const completionRules = completionAuthorities.flatMap((encounter) => (
|
||||
encounter.rules.filter((rule) => rule.actions.some((action) => (
|
||||
action.kind === "set-flag"
|
||||
&& action.flagId === "dungeon-complete"
|
||||
&& action.value
|
||||
)))
|
||||
));
|
||||
expect(completionRules.length, `${dungeon.id}:completion-authority`).toBeGreaterThan(0);
|
||||
expect(completionRules.every((rule) => (
|
||||
(rule.guards ?? []).some((guard) => (
|
||||
guard.kind === "objective-complete"
|
||||
&& guard.objectiveId === objective.id
|
||||
&& guard.equals
|
||||
))
|
||||
|| rule.actions.some((action) => (
|
||||
action.kind === "complete-objective" && action.objectiveId === objective.id
|
||||
))
|
||||
)), `${dungeon.id}:${objective.name}:premature-completion`).toBe(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it("only uses difficulty guards that match declared dungeon variants", () => {
|
||||
const wowDungeons = DUNGEON_DEFINITIONS.filter((dungeon) => (
|
||||
campaign.dungeons.some((entry) => entry.slug === dungeon.id)
|
||||
));
|
||||
for (const dungeon of wowDungeons) {
|
||||
const difficultyIds = new Set(dungeon.difficultyVariants.map((variant) => variant.id));
|
||||
for (const encounter of dungeon.encounters ?? []) {
|
||||
for (const rule of encounter.rules) {
|
||||
for (const guard of rule.guards ?? []) {
|
||||
if (guard.kind !== "difficulty") continue;
|
||||
expect(guard.difficultyIds.length, `${dungeon.id}:${rule.id}`).toBeGreaterThan(0);
|
||||
for (const difficultyId of guard.difficultyIds) {
|
||||
expect(difficultyIds.has(difficultyId), `${dungeon.id}:${rule.id}:${difficultyId}`).toBe(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it("completes ordinary dungeons regardless of required boss kill order", () => {
|
||||
const dungeon = requireDungeonDefinition("deadmines");
|
||||
const progression = dungeon.encounters?.find(
|
||||
(encounter) => encounter.id === "deadmines:boss-progression",
|
||||
);
|
||||
expect(progression).toBeDefined();
|
||||
const required = dungeon.objectiveOrder.filter((objective) => !objective.optional && !objective.eventOnly);
|
||||
expect(required.length).toBeGreaterThan(1);
|
||||
const killOrder = [required.at(-1)!, ...required.slice(0, -1)];
|
||||
const deadEntityIds: string[] = [];
|
||||
let snapshot = createDungeonEncounterSnapshot(progression!, 1_000);
|
||||
for (const boss of killOrder) {
|
||||
deadEntityIds.push(boss.id);
|
||||
snapshot = reduceDungeonEncounter(progression!, snapshot, {
|
||||
kind: "tick",
|
||||
now: 1_000 + deadEntityIds.length * 100,
|
||||
facts: {
|
||||
registeredEntityIds: required.map((objective) => objective.id),
|
||||
deadEntityIds,
|
||||
},
|
||||
}).snapshot;
|
||||
}
|
||||
expect(snapshot.flags["dungeon-complete"]).toBe(true);
|
||||
expect(snapshot.phase).toBe("complete");
|
||||
});
|
||||
|
||||
it("compiles every sourced WoW gameobject row with stable source provenance", () => {
|
||||
const wowDungeons = DUNGEON_DEFINITIONS.filter((dungeon) => dungeon.expansion !== "runewaker");
|
||||
const objects = wowDungeons.flatMap((dungeon) => dungeon.serverGameObjects);
|
||||
const sourceRows = objects.filter((object) => (
|
||||
object.source?.templateId !== undefined && object.source?.spawnId !== undefined
|
||||
));
|
||||
expect(wowDungeons).toHaveLength(54);
|
||||
expect(sourceRows).toHaveLength(3_943);
|
||||
expect(sourceRows.filter((object) => object.source?.clientDisplayId)).toHaveLength(3_905);
|
||||
expect(sourceRows.filter((object) => object.source?.clientModelPath)).toHaveLength(3_837);
|
||||
expect(sourceRows.filter((object) => object.source?.clientBounds)).toHaveLength(3_547);
|
||||
for (const dungeon of wowDungeons) {
|
||||
expect(new Set(dungeon.serverGameObjects.map((object) => object.id)).size)
|
||||
.toBe(dungeon.serverGameObjects.length);
|
||||
for (const object of dungeon.serverGameObjects) {
|
||||
expect(object.source?.fidelity).toBe("source-backed-adapted");
|
||||
expect(object.source?.files.length).toBeGreaterThan(0);
|
||||
if (object.source?.spawnId !== undefined) {
|
||||
expect(object.source.templateId).toBeGreaterThan(0);
|
||||
expect(object.source.spawnId).toBeGreaterThan(0);
|
||||
expect(object.source.gameObjectType).toBeGreaterThanOrEqual(0);
|
||||
expect(object.source.gameObjectType).toBeLessThanOrEqual(35);
|
||||
}
|
||||
if (object.visual?.hidden) {
|
||||
expect(object.interaction).toBeUndefined();
|
||||
expect(object.states.every((state) => !state.blocksMovement && !state.blocksSight)).toBe(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it("publishes a deduplicated export manifest for source-backed gameobject visuals", () => {
|
||||
expect(gameObjectAssetManifest.summary).toEqual({
|
||||
typedInstances: 3_943,
|
||||
visualDisplayInstances: 3_905,
|
||||
nonVisualInstances: 38,
|
||||
resolvedModelInstances: 3_837,
|
||||
boundedInstances: 3_547,
|
||||
uniqueAssets: 458,
|
||||
unresolvedModelInstances: 68,
|
||||
unresolvedBoundInstances: 358,
|
||||
unresolvedModelDisplayIds: 9,
|
||||
unresolvedBoundDisplayIds: 68,
|
||||
});
|
||||
expect(gameObjectAssetManifest.assets).toHaveLength(458);
|
||||
expect(new Set(gameObjectAssetManifest.assets.map(
|
||||
(asset) => asset.sourceModelPath.toLowerCase(),
|
||||
)).size).toBe(gameObjectAssetManifest.assets.length);
|
||||
expect(gameObjectAssetManifest.assets.every((asset) => (
|
||||
["mdl", "mdx"].includes(asset.sourceType)
|
||||
&& asset.targetUrl.startsWith("/assets/game/world-objects/")
|
||||
&& asset.targetUrl.endsWith(".glb")
|
||||
&& asset.instances > 0
|
||||
&& asset.displayIds.length > 0
|
||||
&& asset.dungeons.length > 0
|
||||
))).toBe(true);
|
||||
expect(gameObjectAssetManifest.assets.reduce((total, asset) => total + asset.instances, 0))
|
||||
.toBe(gameObjectAssetManifest.summary.resolvedModelInstances);
|
||||
expect(gameObjectAssetManifest.nonVisualObjects.reduce(
|
||||
(total, object) => total + object.instances,
|
||||
0,
|
||||
)).toBe(gameObjectAssetManifest.summary.nonVisualInstances);
|
||||
expect(gameObjectAssetManifest.unresolvedModels.map((entry) => entry.displayId)).toEqual([
|
||||
9158, 9159, 9160, 9161, 9162, 9163, 9236, 9467, 9468,
|
||||
]);
|
||||
});
|
||||
|
||||
it("runs Wailing Caverns with the complete server population and authored presentation", () => {
|
||||
const wailing = requireDungeonDefinition("wailing-caverns");
|
||||
const runtimePopulation = wailing.staticSpawns.length
|
||||
@@ -101,9 +417,17 @@ describe("generated dungeon campaign catalog", () => {
|
||||
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);
|
||||
+ runtime.roamingPacks.reduce((total, pack) => total + pack.members.length, 0)
|
||||
+ new Set((runtime.encounterSpawnGroups ?? [])
|
||||
.flatMap((group) => group.spawns.map((spawn) => spawn.id))
|
||||
.filter((id) => !runtime.staticSpawns.some((spawn) => spawn.id === id))).size;
|
||||
|
||||
expect(runtimePopulation, source.id).toBeGreaterThanOrEqual(sourcePopulation);
|
||||
if (source.id === "frozen-reach") {
|
||||
expect(runtime.staticSpawns.some((spawn) => spawn.id.startsWith("generated-trash-"))).toBe(false);
|
||||
expect(runtime.validation.warnings.join(" ")).toMatch(/recovered Alva\/Arktos/i);
|
||||
} else {
|
||||
expect(runtimePopulation, source.id).toBeGreaterThanOrEqual(sourcePopulation);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -113,7 +437,11 @@ describe("generated dungeon campaign catalog", () => {
|
||||
...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)
|
||||
dungeon.objectiveOrder.filter((objective) => {
|
||||
if (!("optional" in objective) || !objective.optional) return false;
|
||||
const spawn = dungeon.staticSpawns.find((candidate) => candidate.id === objective.id);
|
||||
return Boolean(spawn && "spawnChance" in spawn);
|
||||
})
|
||||
));
|
||||
|
||||
expect(chanceSpawns).toHaveLength(24);
|
||||
@@ -355,6 +683,44 @@ describe("generated dungeon campaign catalog", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("makes every required boss objective available in every declared difficulty", () => {
|
||||
for (const dungeon of generated.definitions) {
|
||||
const spawns = new Map(dungeon.staticSpawns.map((spawn) => [spawn.id, spawn]));
|
||||
for (const boss of dungeon.objectiveOrder) {
|
||||
if (("eventOnly" in boss && boss.eventOnly) || ("optional" in boss && boss.optional)) continue;
|
||||
const spawn = spawns.get(boss.id);
|
||||
expect(spawn, `${dungeon.id}:${boss.name}`).toBeTruthy();
|
||||
for (const difficulty of dungeon.difficultyVariants) {
|
||||
expect(
|
||||
(spawn!.spawnMask & difficulty.spawnMask) !== 0,
|
||||
`${dungeon.id}:${boss.name}:${difficulty.id}`,
|
||||
).toBe(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it("preserves heroic-only Epoch bosses without gating normal completion", () => {
|
||||
for (const [dungeonId, bossName] of [
|
||||
["the-nexus", "Frozen Commander"],
|
||||
["ahnkahet-old-kingdom", "Amanitar"],
|
||||
["gundrak", "Eck the Ferocious"],
|
||||
] as const) {
|
||||
const dungeon = generated.definitions.find((entry) => entry.id === dungeonId)!;
|
||||
const boss = dungeon.objectiveOrder.find((objective) => objective.name === bossName)!;
|
||||
const spawn = dungeon.staticSpawns.find((entry) => entry.id === boss.id)!;
|
||||
expect("optional" in boss && boss.optional, `${dungeonId}:${bossName}`).toBe(true);
|
||||
expect(spawn.spawnMask, `${dungeonId}:${bossName}`).toBe(2);
|
||||
}
|
||||
|
||||
const blackMorass = generated.definitions.find((dungeon) => dungeon.id === "black-morass")!;
|
||||
for (const boss of blackMorass.objectiveOrder) {
|
||||
const spawn = blackMorass.staticSpawns.find((entry) => entry.id === boss.id)!;
|
||||
expect("optional" in boss && boss.optional, boss.name).not.toBe(true);
|
||||
expect(spawn.spawnMask, boss.name).toBe(3);
|
||||
}
|
||||
});
|
||||
|
||||
it("aligns the Deadmines source population to its packaged interior route", () => {
|
||||
const deadmines = requireDungeonDefinition("deadmines");
|
||||
const firstPack = deadmines.staticSpawns.find(
|
||||
|
||||
Reference in new issue
Block a user