import { describe, expect, it } from "vitest"; import type { CombatPresentationEvent } from "../game/combatPresentation"; import { COMBAT_EFFECT_SYSTEM_LIMIT, addOriginalInstantCastEffect, addOriginalOnlyEffect, advanceCombatEffectFrame, originalInstantCastEvent, originalInstantTerminalEvent, updateCombatEffectQueue, type ActiveCombatEffect, } from "./combatEffectRuntime"; function event( id: number, patch: Partial = {}, ): CombatPresentationEvent { return { id, sessionId: 1, occurredAt: id, phase: "impact", actorGroup: "enemy", sourceActorId: `enemy-${id}`, targetActorId: "player", abilityId: "fixture", name: "Fixture", source: "fallback", school: "shadow", delivery: "projectile", origin: [0, 0, 0], targetPosition: [1, 0, 0], ...patch, }; } function enqueue( events: readonly CombatPresentationEvent[], quality: "off" | "low" | "high", ): readonly ActiveCombatEffect[] { return events.reduce( (current, next) => updateCombatEffectQueue(current, next, quality), [], ); } describe("combat-effect runtime budget", () => { it("expires effects by wall time while bounding motion after a hitch", () => { let frame = advanceCombatEffectFrame(0, 1 / 60, 420); frame = advanceCombatEffectFrame(frame.elapsedMs, 1 / 60, 420); frame = advanceCombatEffectFrame(frame.elapsedMs, 0.5, 420); expect(frame.elapsedMs).toBeCloseTo(533.333, 2); expect(frame.motionDeltaSeconds).toBe(0.05); expect(frame.progress).toBe(1); expect(frame.done).toBe(true); expect(advanceCombatEffectFrame(frame.elapsedMs, Number.NaN, 420).elapsedMs) .toBe(frame.elapsedMs); }); it("bounds large bursts while retaining player feedback and the newest event", () => { const player = event(1, { actorGroup: "player", sourceActorId: "player", school: "holy", }); const enemies = Array.from({ length: 512 }, (_, index) => event(index + 2, { phase: "tick", })); const high = enqueue([player, ...enemies], "high"); const low = enqueue([player, ...enemies], "low"); expect(high).toHaveLength(COMBAT_EFFECT_SYSTEM_LIMIT.high); expect(low).toHaveLength(COMBAT_EFFECT_SYSTEM_LIMIT.low); expect(enqueue([player], "off")).toEqual([]); expect(updateCombatEffectQueue(enqueue([player], "high"), enemies[0], "off")) .toEqual([]); expect(high.some(({ event: active }) => active.id === player.id)).toBe(true); expect(high.at(-1)?.event.id).toBe(enemies.at(-1)?.id); expect(new Set(high.map(({ key }) => key)).size).toBe(high.length); }); it("keeps combat-significant enemy casts ahead of low-value friendly ticks", () => { const partyTicks = Array.from({ length: COMBAT_EFFECT_SYSTEM_LIMIT.low }, (_, index) => event(index + 1, { actorGroup: "party", sourceActorId: `party-${index}`, phase: "tick", })); const enemyCast = event(100, { actorGroup: "enemy", sourceActorId: "boss", phase: "cast-start", }); const effects = enqueue([...partyTicks, enemyCast], "low"); expect(effects).toHaveLength(COMBAT_EFFECT_SYSTEM_LIMIT.low); expect(effects.some(({ event: active }) => active.id === enemyCast.id)).toBe(true); expect(effects.filter(({ event: active }) => active.phase === "tick")) .toHaveLength(COMBAT_EFFECT_SYSTEM_LIMIT.low - 1); }); it("coalesces a single area burst but preserves projectile phases", () => { const areaRelease = event(1, { occurredAt: 100, phase: "release", delivery: "area", sourceActorId: "caster", }); const areaImpacts = Array.from({ length: 5 }, (_, index) => event(index + 2, { occurredAt: 100, phase: "impact", delivery: "area", sourceActorId: "caster", targetActorId: `target-${index}`, })); const area = enqueue([areaRelease, ...areaImpacts], "high"); expect(area).toHaveLength(1); expect(area[0].event.phase).toBe("impact"); const projectile = enqueue([ event(10, { occurredAt: 200, phase: "release", sourceActorId: "caster" }), event(11, { occurredAt: 200, phase: "impact", sourceActorId: "caster" }), ], "high"); expect(projectile).toHaveLength(2); }); it("lets an instant source cast overlap its release while completing timed casts", () => { const release = event(2, { phase: "release", sourceActorId: "instant-caster", instantCast: true, }); const sourceCast = originalInstantCastEvent(release); expect(sourceCast).toMatchObject({ id: -2, phase: "cast-start", sourceActorId: "instant-caster", instantCast: true, }); const instant = updateCombatEffectQueue( addOriginalInstantCastEffect([], release, "high"), release, "high", ); expect(instant.map(({ event: active }) => active.phase)).toEqual(["cast-start", "release"]); expect(instant[0].originalOnly).toBe(true); const timed = enqueue([ event(3, { phase: "cast-start", sourceActorId: "timed-caster" }), event(4, { phase: "release", sourceActorId: "timed-caster" }), ], "high"); expect(timed.map(({ event: active }) => active.phase)).toEqual(["release"]); expect(originalInstantCastEvent(timed[0].event)).toBeNull(); }); it("preserves source terminal semantics for instant impact and aura fallbacks", () => { const impact = event(20, { phase: "impact", instantCast: true, delivery: "area" }); const auraFallback = originalInstantTerminalEvent(impact); expect(auraFallback).toMatchObject({ id: -20, phase: "aura-start", instantCast: true }); const aura = event(21, { phase: "aura-start", instantCast: true, delivery: "aura" }); const impactFallback = originalInstantTerminalEvent(aura); expect(impactFallback).toMatchObject({ id: -21, phase: "impact", instantCast: true }); expect(addOriginalOnlyEffect([], impactFallback!, "high")[0]).toMatchObject({ originalOnly: true, event: { phase: "impact" }, }); expect(originalInstantTerminalEvent(event(22, { phase: "impact" }))).toBeNull(); }); });