Files
healer-man/src/game/runewakerSpellMapping.test.ts
T
2026-09-08 10:40:26 -04:00

113 lines
4.0 KiB
TypeScript

import { readFileSync } from "node:fs";
import { describe, expect, it } from "vitest";
import { DUNGEON_DEFINITIONS } from "./dungeonRegistry";
interface CatalogRuntime {
readonly delivery: string;
readonly target: string;
readonly school: string;
readonly animation: string;
readonly range: number;
readonly cooldownMs: number;
readonly damageMultiplier: number;
}
interface CatalogSpell {
readonly spellId: number;
readonly name: string;
readonly mappingStatus: "damage-family" | "evidence-only";
readonly runtime?: CatalogRuntime;
}
interface ExecutableAttack {
readonly name: string;
readonly spellId?: number;
readonly delivery: string;
readonly target: string;
readonly school: string;
readonly animation: string;
readonly range: number;
readonly cooldownMs: number;
readonly damageMultiplier: number;
readonly runewakerFormula?: { readonly spellId: number; readonly runtimeBaselineMultiplier: number };
}
const catalog = JSON.parse(readFileSync(
"scripts/runewaker-pipeline/snapshots/runewaker-npc-spell-catalog.json",
"utf8",
)) as {
readonly status: string;
readonly spellCount: number;
readonly damageFamilyCount: number;
readonly evidenceOnlyCount: number;
readonly safety: {
readonly runtimeSqlDependency: boolean;
readonly unknownEffectsExecute: boolean;
readonly coefficientsAreSourceExact: boolean;
readonly pointValuedDamageUsesLabeledScaledAdapter: boolean;
readonly unavailableNativeInputsDefaulted: boolean;
};
readonly spells: readonly CatalogSpell[];
};
const gameplayReport = JSON.parse(readFileSync(
"scripts/runewaker-pipeline/RUNEWAKER_GAMEPLAY_CONTENT_REPORT.json",
"utf8",
)) as {
readonly summary: {
readonly sourceBackedDamageFamilyAttacks: number;
};
};
const spellsById = new Map(catalog.spells.map((spell) => [spell.spellId, spell]));
describe("portable RuneWaker source-spell mappings", () => {
it("keeps SQL and unsupported effects out of the runtime contract", () => {
expect(catalog.status).toBe("portable");
expect(catalog.spellCount).toBe(catalog.spells.length);
expect(catalog.damageFamilyCount + catalog.evidenceOnlyCount).toBe(catalog.spellCount);
expect(catalog.safety).toEqual({
runtimeSqlDependency: false,
unknownEffectsExecute: false,
coefficientsAreSourceExact: false,
pointValuedDamageUsesLabeledScaledAdapter: true,
unavailableNativeInputsDefaulted: false,
});
});
it("executes only positively classified direct-damage families", () => {
const runewaker = DUNGEON_DEFINITIONS.filter(
(definition) => definition.expansion === "runewaker",
);
let sourceBackedAttacks = 0;
for (const dungeon of runewaker) {
for (const entity of Object.values(dungeon.entities)) {
for (const attackValue of entity.combat?.attacks ?? []) {
const attack = attackValue as ExecutableAttack;
if (!attack.spellId) continue;
sourceBackedAttacks += 1;
const source = spellsById.get(attack.spellId);
expect(source, dungeon.id + ":" + entity.id + ":" + attack.spellId).toBeDefined();
expect(source?.mappingStatus).toBe("damage-family");
expect(source?.runtime).toBeDefined();
expect(attack).toMatchObject({
name: source?.name,
school: source?.runtime?.school,
animation: source?.runtime?.animation,
cooldownMs: source?.runtime?.cooldownMs,
});
expect(attack.damageMultiplier).toBeGreaterThan(0);
expect(attack.runewakerFormula).toMatchObject({ spellId: attack.spellId });
expect(["melee", "projectile", "area"]).toContain(attack.delivery);
expect(["primary", "nearby-party", "random-party"]).toContain(attack.target);
expect(attack.range).toBeGreaterThanOrEqual(source?.runtime?.range ?? 0);
}
}
}
expect(sourceBackedAttacks).toBeGreaterThan(0);
expect(sourceBackedAttacks).toBe(
gameplayReport.summary.sourceBackedDamageFamilyAttacks,
);
});
});