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, ); }); });