import { afterEach, describe, expect, it } from "vitest"; import type { OnlineSharedWorld } from "../app/onlineSessionTypes"; import { activateDungeonEncounterDefinitions, dungeonEncounterSnapshot, resetDungeonEncounters, } from "./dungeonEncounterRuntime"; import { applySharedWorld, eventForRelay } from "./OnlineSessionBridge"; import { PLAYER_AGGRO_ID } from "./aggro"; import { usePartyStore } from "./partyStore"; import type { StructuredCombatEvent } from "./combatEvents"; import { useCombatStore } from "./combatStore"; import { useGameStore } from "./store"; import { WAILING_NARALEX_ENCOUNTER, useWailingEncounterStore, } from "./wailingCavernsEncounter"; const EMPTY_SHARED_WORLD = { dungeonId: "wailing-caverns", mobs: {}, mobTransforms: {}, partyMembers: [], partyPositions: {}, partyRuntime: { command: "defend", activeMobId: null, objectiveBossId: null, memberTargetIds: {}, }, wailing: null, manastorm: null, } as const satisfies OnlineSharedWorld; describe("online shared-world hydration", () => { afterEach(() => { useWailingEncounterStore.getState().reset("dungeon", 0); useGameStore.getState().activateDungeon("wailing-caverns"); }); it("accepts an authority snapshot older than a follower's local revision", () => { activateDungeonEncounterDefinitions( "wailing-caverns", [WAILING_NARALEX_ENCOUNTER], "normal", ); useGameStore.getState().activateDungeon("wailing-caverns"); const authorityBase = dungeonEncounterSnapshot(WAILING_NARALEX_ENCOUNTER.id)!; resetDungeonEncounters("dungeon", "wailing-caverns"); const follower = dungeonEncounterSnapshot(WAILING_NARALEX_ENCOUNTER.id)!; const authority = { ...authorityBase, phase: "ready", revision: Math.max(0, follower.revision - 1), } as const; expect(authority.revision).toBeLessThan(follower.revision); applySharedWorld({ ...EMPTY_SHARED_WORLD, encounters: [authority] }); expect(dungeonEncounterSnapshot(WAILING_NARALEX_ENCOUNTER.id)).toMatchObject({ phase: "ready", revision: authority.revision, }); }); it("awards the authority's card rolls once to a follower, including after reconnect", () => { useCombatStore.getState().initializeCharacter({ classId: "priest", level: 20 }); useCombatStore.getState().registerMob("shared-boss", { name: "Lady Anacondra", boss: true, maxHealth: 1 }); const mob = useCombatStore.getState().mobs["shared-boss"]; const world = { ...EMPTY_SHARED_WORLD, mobs: { "shared-boss": { ...mob, dead: true, looted: true, health: 0, bossCardDrop: { bossKey: "wow:lady anacondra", spawnId: mob.id, claimId: "shared-kill:1", rarities: ["normal", "epic"] as const } } } }; applySharedWorld(world); applySharedWorld(world); expect(useCombatStore.getState().bossCards.counts["wow:lady anacondra"]).toEqual({ normal: 1, epic: 1 }); const bossCards = JSON.parse(JSON.stringify(useCombatStore.getState().bossCards)); useCombatStore.getState().initializeCharacter({ classId: "priest", level: 20, bossCards }); applySharedWorld(world); expect(useCombatStore.getState().bossCards.counts["wow:lady anacondra"]).toEqual({ normal: 1, epic: 1 }); expect(useCombatStore.getState().inventory).toEqual([]); }); }); describe("online damage presentation relay", () => { it("relays authority companion hits so followers can display them", () => { useCombatStore.getState().initializeCharacter({ classId: "priest", level: 20 }); useCombatStore.getState().registerMob("relay-mob", { name: "Relay mob", maxHealth: 100 }); const oldMembers = usePartyStore.getState().members; usePartyStore.setState({ members: [{ id: "companion" } as (typeof oldMembers)[number]] }); try { const event = { id: 1, occurredAt: 1_000, kind: "damage", sourceActorId: "companion", targetActorId: "relay-mob", rawAmount: 55, effectiveAmount: 40, overheal: 0, absorbed: 0, critical: true, } satisfies StructuredCombatEvent; expect(eventForRelay(event, "leader", true)).toMatchObject({ kind: "damage", sourceActorId: "companion", targetActorId: "relay-mob", effectiveAmount: 40, critical: true, }); expect(eventForRelay(event, "leader", false)).toBeNull(); } finally { usePartyStore.setState({ members: oldMembers }); } }); it("relays authority AI hits on the leader and companions with their crit flag", () => { useCombatStore.getState().initializeCharacter({ classId: "priest", level: 20 }); const oldMembers = usePartyStore.getState().members; usePartyStore.setState({ members: [{ id: "companion" } as (typeof oldMembers)[number]] }); try { const hit = { id: 2, occurredAt: 2_000, kind: "damage", sourceActorId: "mob", targetActorId: PLAYER_AGGRO_ID, rawAmount: 80, effectiveAmount: 50, overheal: 0, absorbed: 30, critical: true, } satisfies StructuredCombatEvent; expect(eventForRelay(hit, "leader", true)).toMatchObject({ sourceActorId: "mob", targetActorId: "online-player:leader", effectiveAmount: 50, critical: true, }); expect(eventForRelay({ ...hit, targetActorId: "companion" }, "leader", true)).toMatchObject({ sourceActorId: "mob", targetActorId: "companion", effectiveAmount: 50, critical: true, }); expect(eventForRelay(hit, "leader", false)).toBeNull(); } finally { usePartyStore.setState({ members: oldMembers }); } }); });