forked from phenom/healer-man
148 lines
6.7 KiB
TypeScript
148 lines
6.7 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import rawCatalog from "./romPlayerCatalog.generated.json";
|
|
import {
|
|
romSemanticCoverage,
|
|
translateRomAbilitySemantics,
|
|
type RomGeneratedAbilityLike,
|
|
type RomSourceComponent,
|
|
} from "./romAbilitySemantics";
|
|
|
|
const skills = rawCatalog.skills as unknown as readonly RomGeneratedAbilityLike[];
|
|
|
|
function named(name: string): RomGeneratedAbilityLike {
|
|
const result = skills.find((skill) => skill.name === name);
|
|
if (!result) throw new Error(`Missing generated RuneWaker skill: ${name}`);
|
|
return result;
|
|
}
|
|
|
|
function semantics(name: string) {
|
|
return translateRomAbilitySemantics(named(name));
|
|
}
|
|
|
|
describe("RuneWaker component-backed ability semantics", () => {
|
|
it("retains authoritative attack, flags, DOT, modifiers, and magic-shield fields", () => {
|
|
const sneak = named("Sneak Attack");
|
|
const direct = sneak.source?.components?.find((component) => component.id === 620284);
|
|
const stun = sneak.source?.components?.find((component) => component.id === 500725);
|
|
const bleed = sneak.source?.components?.find((component) => component.id === 500726);
|
|
expect(direct).toMatchObject({
|
|
specialActionFlags: 4,
|
|
attack: { type: 0, damagePower: -1.3, fixedValue: -0.3 },
|
|
});
|
|
expect(stun).toMatchObject({ effectTime: 3, effectFlagWords: [16_777_283, 0] });
|
|
expect(bleed).toMatchObject({ dot: { timeSeconds: 2, type: 0, base: -8 } });
|
|
|
|
const fireWard = named("Fire Ward").source?.components?.[0];
|
|
expect(fireWard).toMatchObject({
|
|
effectTime: 600,
|
|
abilityModifiers: [{ type: 29, value: 10 }],
|
|
abilitySkillLevelArg: 20,
|
|
});
|
|
|
|
const electrostatic = named("Electrostatic Charge").source?.components?.[0];
|
|
expect(electrostatic).toMatchObject({
|
|
settingFlags: 355,
|
|
magicShield: { rawBaseWord: 0, type: 3, effect: 0, point: 2, skillLevelArg: 28 },
|
|
});
|
|
});
|
|
|
|
it("translates taunts and stuns instead of the old healing fallback", () => {
|
|
const taunt = semantics("Taunt");
|
|
expect(taunt).toEqual(expect.arrayContaining([
|
|
expect.objectContaining({ kind: "taunt", scope: "single", hateDelta: 30 }),
|
|
]));
|
|
expect(taunt.some((semantic) => semantic.kind === "heal")).toBe(false);
|
|
|
|
const shout = semantics("Shout");
|
|
expect(shout).toEqual(expect.arrayContaining([
|
|
expect.objectContaining({ kind: "control", effect: "stun", durationSeconds: 3 }),
|
|
]));
|
|
expect(shout.some((semantic) => semantic.kind === "heal")).toBe(false);
|
|
});
|
|
|
|
it("translates Cleanse as a harmful-effect dispel", () => {
|
|
expect(semantics("Cleanse")).toEqual(expect.arrayContaining([
|
|
expect.objectContaining({ kind: "dispel", polarity: "harmful", mode: "remove" }),
|
|
]));
|
|
});
|
|
|
|
it("keeps every Sneak Attack effect and its specialized use conditions", () => {
|
|
const result = semantics("Sneak Attack");
|
|
expect(result).toEqual(expect.arrayContaining([
|
|
expect.objectContaining({ kind: "damage", school: "physical", power: 1.3 }),
|
|
expect.objectContaining({ kind: "control", effect: "stun", durationSeconds: 3 }),
|
|
expect.objectContaining({ kind: "dot", intervalSeconds: 2, durationSeconds: 6, basePerTick: 8 }),
|
|
expect.objectContaining({ kind: "custom", mechanicId: "conditional-use", unresolved: false }),
|
|
]));
|
|
});
|
|
|
|
it("translates resistance, immunity, silence, and source-authored absorbs", () => {
|
|
expect(semantics("Fire Ward")).toEqual(expect.arrayContaining([
|
|
expect.objectContaining({ kind: "aura", effect: "fire-resistance", value: 10, durationSeconds: 600 }),
|
|
]));
|
|
expect(semantics("Holy Aura")).toEqual(expect.arrayContaining([
|
|
expect.objectContaining({ kind: "aura", effect: "damage-immunity", durationSeconds: 5 }),
|
|
]));
|
|
expect(semantics("Silence")).toEqual(expect.arrayContaining([
|
|
expect.objectContaining({ kind: "control", effect: "silence", durationSeconds: 4 }),
|
|
]));
|
|
expect(semantics("Electrostatic Charge")).toEqual(expect.arrayContaining([
|
|
expect.objectContaining({
|
|
kind: "absorb",
|
|
basis: "max-health-percent",
|
|
school: "all",
|
|
amount: 2,
|
|
perSkillLevel: 28,
|
|
durationSeconds: 60,
|
|
}),
|
|
]));
|
|
});
|
|
|
|
it("retains multi-effect interrupt/resource skills and passive cooldown modifiers", () => {
|
|
const bindingSilence = semantics("Binding Silence");
|
|
expect(bindingSilence).toEqual(expect.arrayContaining([
|
|
expect.objectContaining({ kind: "damage" }),
|
|
expect.objectContaining({ kind: "interrupt", lockoutSeconds: 0 }),
|
|
expect.objectContaining({ kind: "resource", resource: "nature's power", operation: "restore", amount: 1 }),
|
|
]));
|
|
expect(semantics("Electrostatic Charge Expertise")).toEqual(expect.arrayContaining([
|
|
expect.objectContaining({ kind: "cooldown-reset", mode: "reduce", seconds: 30 }),
|
|
]));
|
|
});
|
|
|
|
it("emits proc semantics from component trigger records", () => {
|
|
const procSkill = skills.find((skill) => skill.source?.components?.some((component) => (
|
|
(component.onAttackTrigger?.id ?? 0) > 0
|
|
|| Object.entries(component.triggers ?? {}).some(([key, value]) => key.endsWith("MagicId") && value > 0)
|
|
)));
|
|
expect(procSkill).toBeDefined();
|
|
expect(translateRomAbilitySemantics(procSkill!).some((semantic) => semantic.kind === "proc")).toBe(true);
|
|
});
|
|
|
|
it("gives every generated ability an honest semantic and reports fallback coverage", () => {
|
|
const coverage = romSemanticCoverage(skills);
|
|
expect(coverage.abilities).toBe(929);
|
|
expect(coverage.semantics).toBeGreaterThanOrEqual(coverage.abilities);
|
|
expect(skills.every((skill) => translateRomAbilitySemantics(skill).length > 0)).toBe(true);
|
|
expect(coverage.customSemantics).toBeGreaterThan(0);
|
|
expect(coverage.unresolvedAbilities).toBeGreaterThanOrEqual(0);
|
|
});
|
|
|
|
it("does not expose generic mark or substitute-shield semantics", () => {
|
|
const serialized = JSON.stringify(skills.flatMap((skill) => translateRomAbilitySemantics(skill)));
|
|
expect(serialized).not.toContain('"effect":"marked"');
|
|
expect(serialized).not.toContain("generic-mark");
|
|
expect(serialized).not.toContain("substitute-shield");
|
|
});
|
|
|
|
it("allows the pure translator to consume an individual generated rank", () => {
|
|
const rank = (rawCatalog.skills.find((skill) => skill.name === "Sneak Attack")?.ranks[0]) as unknown as RomGeneratedAbilityLike;
|
|
expect(translateRomAbilitySemantics(rank).map((semantic) => semantic.kind)).toEqual(expect.arrayContaining(["damage", "dot", "control"]));
|
|
});
|
|
});
|
|
|
|
// Structural sentinel: generated components remain assignable to the public
|
|
// neutral source type without importing runtime ability definitions.
|
|
const _componentShapeSentinel: RomSourceComponent | undefined = named("Taunt").source?.components?.[0];
|
|
void _componentShapeSentinel;
|