import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { BITE_SIZED_STOCKADES_DUNGEON_ID, RANDOMIZED_BITE_SIZED_STOCKADES_DUNGEON_ID, } from "../game/dungeonContentSource"; import type { DungeonRunProgress } from "../game/dungeonRunTypes"; import { DEFAULT_DUNGEON_QUEUE_POLICY } from "../game/dungeonQueuePolicy"; import { canStartAnotherBiteSizedDungeon, startAnotherBiteSizedDungeon } from "./biteSizedDungeonRuntime"; import { useOnlineGroupStore } from "./onlineGroupStore"; import { useOnlineSessionStore } from "./onlineSessionStore"; import { useShellStore } from "./shellStore"; const progress: DungeonRunProgress = { id: "completed-bite-sized-run", dungeonId: BITE_SIZED_STOCKADES_DUNGEON_ID, difficultyId: "normal", bossDifficulty: 1, startedAt: 1_000, finishedAt: 9_000, durationMs: 8_000, timed: true, killedBossIds: ["creature-84027"], party: [{ name: "Tester", classId: "priest", level: 17, role: "healer", npc: false }], }; const originalShell = useShellStore.getState(); const originalGroup = useOnlineGroupStore.getState(); const originalOnline = useOnlineSessionStore.getState(); beforeEach(() => { useOnlineGroupStore.getState().reset(); useOnlineSessionStore.getState().reset(); }); afterEach(() => { useShellStore.setState(originalShell, true); useOnlineGroupStore.setState(originalGroup, true); useOnlineSessionStore.setState(originalOnline, true); }); describe("bite-sized replay", () => { it("does not replay a disabled randomized location", async () => { const enterDungeon = vi.fn(() => true); useShellStore.setState({ session: { kind: "offline", ownerId: "local-owner", displayName: "Tester" }, activeDungeonRole: "healer", enterDungeon, setNotice: vi.fn(), }); await expect(startAnotherBiteSizedDungeon( { ...progress, dungeonId: RANDOMIZED_BITE_SIZED_STOCKADES_DUNGEON_ID }, { random: () => 0, loadPolicy: async () => ({ schemaVersion: 1, disabled: { regular: [], biteSized: [], randomizedBiteSized: [RANDOMIZED_BITE_SIZED_STOCKADES_DUNGEON_ID], } }), createRunId: () => "unused", contentIsUsable: () => true, prepareRandomizedContent: vi.fn(), }, )).resolves.toBe(false); expect(enterDungeon).not.toHaveBeenCalled(); }); it("starts a fresh local run and treats the local player as party leader", async () => { const enterDungeon = vi.fn(() => true); const saveCheckpoint = vi.fn(); useShellStore.setState({ session: { kind: "offline", ownerId: "local-owner", displayName: "Tester" }, activeDungeonRole: "healer", enterDungeon, saveActiveDungeonRunCheckpoint: saveCheckpoint, }); expect(canStartAnotherBiteSizedDungeon()).toBe(true); await expect(startAnotherBiteSizedDungeon(progress)).resolves.toBe(true); expect(saveCheckpoint).toHaveBeenCalledWith(null); expect(enterDungeon).toHaveBeenCalledWith(expect.stringMatching(/^bite-sized-/), "healer", { difficultyId: "normal", bossDifficulty: 1, freshRun: true, runId: expect.any(String), }); expect(enterDungeon).not.toHaveBeenCalledWith(BITE_SIZED_STOCKADES_DUNGEON_ID, expect.anything(), expect.anything()); }); it("replays randomized mode with a new seed and prepares that seed's creature assets", async () => { const enterDungeon = vi.fn(() => true); const saveCheckpoint = vi.fn(); const prepareRandomizedContent = vi.fn(async () => undefined); useShellStore.setState({ session: { kind: "offline", ownerId: "local-owner", displayName: "Tester" }, activeDungeonRole: "healer", enterDungeon, saveActiveDungeonRunCheckpoint: saveCheckpoint, }); const randomizedProgress = { ...progress, dungeonId: RANDOMIZED_BITE_SIZED_STOCKADES_DUNGEON_ID }; await expect(startAnotherBiteSizedDungeon(randomizedProgress, { random: () => 0, loadPolicy: async () => DEFAULT_DUNGEON_QUEUE_POLICY, createRunId: () => "next-randomized-seed", contentIsUsable: () => true, prepareRandomizedContent, })).resolves.toBe(true); expect(prepareRandomizedContent).toHaveBeenCalledWith("next-randomized-seed", RANDOMIZED_BITE_SIZED_STOCKADES_DUNGEON_ID); expect(saveCheckpoint).toHaveBeenCalledWith(null); expect(enterDungeon).toHaveBeenCalledWith(RANDOMIZED_BITE_SIZED_STOCKADES_DUNGEON_ID, "healer", { difficultyId: "normal", bossDifficulty: 1, freshRun: true, runId: "next-randomized-seed", }); }); it("lets only the online group leader publish the next shared activity", async () => { const startActivity = vi.fn(async () => true); const saveCheckpoint = vi.fn(); const activity = { id: "shared-bite-sized-run", type: "dungeon" as const, contentId: BITE_SIZED_STOCKADES_DUNGEON_ID, fillWithAi: true, partySize: 4, startingLevel: null, startedAt: 1_000, }; useShellStore.setState({ session: { kind: "account", ownerId: "leader", displayName: "Leader", accessToken: "test-token" }, saveActiveDungeonRunCheckpoint: saveCheckpoint, }); useOnlineGroupStore.setState({ startActivity, snapshot: { invitations: [], group: { id: "group", leaderAccountId: "leader", members: [], activity, activityRevision: 1, createdAt: 1, updatedAt: 1 } }, }); expect(canStartAnotherBiteSizedDungeon()).toBe(true); await expect(startAnotherBiteSizedDungeon(progress)).resolves.toBe(true); expect(startActivity).toHaveBeenCalledWith(expect.objectContaining({ ownerId: "leader" }), { type: "dungeon", contentId: expect.stringMatching(/^bite-sized-/), difficultyId: "normal", bossDifficulty: 1, partySize: 4, }); expect(saveCheckpoint).toHaveBeenCalledWith(null); useShellStore.setState({ session: { kind: "account", ownerId: "follower", displayName: "Follower", accessToken: "test-token" } }); expect(canStartAnotherBiteSizedDungeon()).toBe(false); }); it("publishes another randomized activity instead of crossing an online group into regular Bite-Sized", async () => { const startActivity = vi.fn(async () => true); useShellStore.setState({ session: { kind: "account", ownerId: "leader", displayName: "Leader", accessToken: "test-token" }, saveActiveDungeonRunCheckpoint: vi.fn(), }); useOnlineGroupStore.setState({ startActivity, snapshot: { invitations: [], group: { id: "group", leaderAccountId: "leader", members: [], activity: null, activityRevision: 2, createdAt: 1, updatedAt: 2 } }, }); await expect(startAnotherBiteSizedDungeon( { ...progress, dungeonId: RANDOMIZED_BITE_SIZED_STOCKADES_DUNGEON_ID }, { random: () => 0, createRunId: () => "unused-local-seed", contentIsUsable: () => true, loadPolicy: async () => DEFAULT_DUNGEON_QUEUE_POLICY, prepareRandomizedContent: vi.fn() }, )).resolves.toBe(true); expect(startActivity).toHaveBeenCalledWith(expect.objectContaining({ ownerId: "leader" }), { type: "dungeon", contentId: RANDOMIZED_BITE_SIZED_STOCKADES_DUNGEON_ID, difficultyId: "normal", bossDifficulty: 1, partySize: 5, }); }); });