Files
healer-man/src/game/combatPresentation.test.ts
T
2026-09-23 16:14:19 -04:00

198 lines
7.5 KiB
TypeScript

import { readFileSync } from "node:fs";
import path from "node:path";
import { describe, expect, it } from "vitest";
import { abilityById } from "./abilityCatalog";
import { WOW335_ABILITIES_BY_CLASS } from "./wow335AbilityCatalog";
import {
DEFAULT_COMBAT_PRESENTATION_SETTINGS,
combatEffectManifestEntry,
combatEffectStyle,
combatPresentationSessionId,
emitCombatPresentation,
normalizeCombatPresentationSettings,
originalCombatPhaseAssets,
presentationDeliveryForAbility,
presentationSchoolForAbility,
resetCombatPresentationSession,
subscribeCombatPresentation,
type CombatEffectManifestEntry,
type CombatPresentationPhase,
type CombatPresentationMessage,
} from "./combatPresentation";
describe("combat presentation", () => {
it("normalizes owner settings without leaking partial or invalid values", () => {
expect(normalizeCombatPresentationSettings({
vfxQuality: "low",
vfxEnabled: { player: false },
audioEnabled: { enemy: false },
audioVolume: { player: 2, party: -1, enemy: 0.35 },
})).toEqual({
originalMediaEnabled: true,
vfxMode: "original",
vfxQuality: "low",
vfxEnabled: { player: false, party: true, enemy: true },
audioEnabled: { player: true, party: true, enemy: false },
audioVolume: { player: 1, party: 0, enemy: 0.35 },
});
expect(normalizeCombatPresentationSettings({
...DEFAULT_COMBAT_PRESENTATION_SETTINGS,
originalMediaEnabled: true,
vfxMode: "original",
}).vfxMode).toBe("original");
expect(normalizeCombatPresentationSettings({
...DEFAULT_COMBAT_PRESENTATION_SETTINGS,
originalMediaEnabled: false,
vfxMode: "original",
}).vfxMode).toBe("original");
expect(normalizeCombatPresentationSettings({
...DEFAULT_COMBAT_PRESENTATION_SETTINGS,
vfxMode: "hybrid",
}).vfxMode).toBe("original");
expect(normalizeCombatPresentationSettings(null)).toEqual(DEFAULT_COMBAT_PRESENTATION_SETTINGS);
});
it("delivers immutable, exactly-once events and a session reset", () => {
const messages: CombatPresentationMessage[] = [];
const unsubscribe = subscribeCombatPresentation((message) => messages.push(message));
const origin = [1, 2, 3] as [number, number, number];
const event = emitCombatPresentation({
occurredAt: 1_000,
phase: "release",
actorGroup: "player",
sourceActorId: "player",
targetActorId: "mob",
abilityId: "fixture",
sourceSpellId: 116,
name: "Frostbolt",
source: "wow335",
school: "frost",
delivery: "projectile",
origin,
targetPosition: [8, 0, 0],
});
origin[0] = 99;
const nextSession = resetCombatPresentationSession();
unsubscribe();
expect(messages).toHaveLength(2);
expect(messages[0]).toEqual({ kind: "event", event });
expect(event.origin).toEqual([1, 2, 3]);
expect(messages[1]).toEqual({ kind: "reset", sessionId: nextSession });
expect(combatPresentationSessionId()).toBe(nextSession);
});
it("uses authentic source descriptors and school/delivery fallbacks", () => {
const frostbolt = abilityById("mage-frostbolt");
expect(frostbolt).not.toBeNull();
expect(presentationSchoolForAbility(frostbolt!)).toBe("frost");
expect(presentationDeliveryForAbility(frostbolt!)).toBe("projectile");
expect(combatEffectManifestEntry(999_999, "wow335")).toBeNull();
const style = combatEffectStyle({
id: 1,
sessionId: 1,
occurredAt: 1,
phase: "release",
actorGroup: "player",
sourceActorId: "player",
abilityId: "mage-frostbolt",
sourceSpellId: 999_999,
name: "Frostbolt",
source: "wow335",
school: "frost",
delivery: "projectile",
origin: [0, 0, 0],
});
expect(style.authentic).toBe(false);
expect(style.sourceModelNames).toEqual([]);
});
it("exposes only packaged original phase graphs", () => {
const entry = {
key: "wow335:2061",
source: "wow335" as const,
spellId: 2061,
phases: {
impact: [
{ model: "Spells/FlashHeal_Base.mdx" },
{
model: "Spells/FlashHeal_Base.mdx",
effectKind: "wow-m2-particles" as const,
effectGraphUrl: "/assets/game/combat-effects/original/wow335/priest/graphs/flash-heal.json",
},
{
effectKind: "wow-m2-particles" as const,
effectGraphUrl: "https://example.invalid/not-a-packaged-effect.json",
},
{
model: "Spells/HolyDivineShield_State_Base.mdx",
effectKind: "wow-m2-static-mesh" as const,
effectGraphUrl: "/assets/game/combat-effects/original/wow335/priest/graphs/shield.json",
},
],
},
};
expect(originalCombatPhaseAssets(entry, "impact")).toEqual([
entry.phases.impact[1],
entry.phases.impact[3],
]);
expect(originalCombatPhaseAssets(entry, "release")).toEqual([]);
});
it("plays the original Holy Nova burst once at impact", () => {
const manifest = JSON.parse(readFileSync(
path.resolve("public/assets/game/combat-effects/manifest.json"),
"utf8",
)) as { readonly entries: readonly CombatEffectManifestEntry[] };
for (const spellId of [15237, 15430, 15431, 27799, 27800, 27801, 25331, 48077, 48078]) {
const entry = manifest.entries.find((candidate) => candidate.spellId === spellId && candidate.source === "wow335");
expect(originalCombatPhaseAssets(entry, "cast-start", "wow335-priest-holy-nova")).toEqual([]);
expect(originalCombatPhaseAssets(entry, "release", "wow335-priest-holy-nova")).toEqual([]);
expect(originalCombatPhaseAssets(entry, "impact", "wow335-priest-holy-nova").map((asset) => asset.model))
.toEqual(["spells/holynova_impact_base.mdx"]);
}
});
it("keeps every source-backed instant WoW rank on a renderable phase", () => {
const manifest = JSON.parse(readFileSync(
path.resolve("public/assets/game/combat-effects/manifest.json"),
"utf8",
)) as { readonly entries: readonly CombatEffectManifestEntry[] };
const entriesBySpellId = new Map(manifest.entries
.filter((entry) => entry.source === "wow335")
.map((entry) => [entry.spellId, entry]));
const reachablePhases: readonly CombatPresentationPhase[] = [
"cast-start",
"release",
"impact",
"aura-start",
];
const sourceBackedInstantRanks = Object.entries(WOW335_ABILITIES_BY_CLASS)
.flatMap(([classId, abilities]) => abilities
.flatMap((ability) => (ability.ranks ?? []).map((rank) => ({ ability, classId, rank }))))
.filter(({ rank }) => rank.castMode === "instant")
.flatMap(({ ability, classId, rank }) => {
const entry = entriesBySpellId.get(rank.spellId);
if (!entry) return [];
const hasOriginal = Object.keys(entry.phases ?? {}).some((phase) => (
originalCombatPhaseAssets(entry, phase as CombatPresentationPhase).length > 0
));
return hasOriginal ? [{
abilityId: ability.id,
classId,
spellId: rank.spellId,
entry,
}] : [];
});
const unreachable = sourceBackedInstantRanks.filter(({ entry }) => (
!reachablePhases.some((phase) => originalCombatPhaseAssets(entry, phase).length > 0)
));
expect(unreachable.map(({ abilityId, spellId }) => `${abilityId}:${spellId}`)).toEqual([]);
expect([...new Set(sourceBackedInstantRanks.map(({ classId }) => classId))].sort())
.toEqual(Object.keys(WOW335_ABILITIES_BY_CLASS).sort());
expect(sourceBackedInstantRanks.length).toBeGreaterThan(1_000);
});
});