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