diff --git a/apps/api/src/data/achievements.ts b/apps/api/src/data/achievements.ts index b40976d..11f3114 100644 --- a/apps/api/src/data/achievements.ts +++ b/apps/api/src/data/achievements.ts @@ -334,8 +334,8 @@ export const defaultAchievements: Array = [ unlockedAt: null, }, { - condition: { amount: 112, type: "questsCompleted" }, - description: "Complete all 112 quests across the known multiverse.", + condition: { amount: 122, type: "questsCompleted" }, + description: "Complete all 122 quests across the known multiverse.", icon: "🌌", id: "quest_eternal", name: "Quest Eternal", diff --git a/apps/api/src/routes/boss.ts b/apps/api/src/routes/boss.ts index efdaccb..bbf026a 100644 --- a/apps/api/src/routes/boss.ts +++ b/apps/api/src/routes/boss.ts @@ -18,6 +18,7 @@ import { import { Hono } from "hono"; import { defaultBosses } from "../data/bosses.js"; import { defaultEquipmentSets } from "../data/equipmentSets.js"; +import { defaultExplorations } from "../data/explorations.js"; import { prisma } from "../db/client.js"; import { authMiddleware } from "../middleware/auth.js"; import { updateChallengeProgress } from "../services/dailyChallenges.js"; @@ -284,6 +285,19 @@ bossRouter.post("/challenge", async(context) => { continue; } zone.status = "unlocked"; + + // Unlock exploration areas for the newly unlocked zone + for (const area of state.exploration?.areas ?? []) { + const areaDefinition = defaultExplorations.find((explorationArea) => { + return explorationArea.id === area.id; + }); + // eslint-disable-next-line capitalized-comments -- v8 ignore + /* v8 ignore next 3 -- @preserve */ + if (areaDefinition?.zoneId === zone.id && area.status === "locked") { + area.status = "available"; + } + } + const updatedZoneBosses = state.bosses.filter((b) => { return b.zoneId === zone.id; }); diff --git a/apps/api/src/routes/timers.ts b/apps/api/src/routes/timers.ts index a409ea5..ea506dc 100644 --- a/apps/api/src/routes/timers.ts +++ b/apps/api/src/routes/timers.ts @@ -36,10 +36,11 @@ const getQuestTimers = ( }> => { return state.quests. filter((quest) => { - return quest.status === "in_progress" && quest.startedAt !== undefined; + return quest.status === "active" && quest.startedAt !== undefined; }). map((quest) => { const durationMs = quest.durationSeconds * 1000; + // eslint-disable-next-line capitalized-comments -- v8 ignore /* v8 ignore next -- @preserve */ const endsAt = (quest.startedAt ?? 0) + durationMs; return { @@ -71,6 +72,7 @@ const getExplorationTimers = ( return area.status === "in_progress" && area.endsAt !== undefined; }). map((area) => { + // eslint-disable-next-line capitalized-comments -- v8 ignore /* v8 ignore next -- @preserve */ const endsAt = area.endsAt ?? 0; return { diff --git a/apps/api/test/routes/boss.spec.ts b/apps/api/test/routes/boss.spec.ts index 98fa037..b3f9e6d 100644 --- a/apps/api/test/routes/boss.spec.ts +++ b/apps/api/test/routes/boss.spec.ts @@ -294,6 +294,52 @@ describe("boss route", () => { expect(body.won).toBe(true); }); + it("handles zone unlock gracefully when exploration state is undefined", async () => { + const state = makeState({ + bosses: [makeBoss({ currentHp: 100, maxHp: 100, damagePerSecond: 1 })] as GameState["bosses"], + adventurers: [makeAdventurer()] as GameState["adventurers"], + zones: [{ id: "test_zone", status: "locked", unlockBossId: "test_boss", unlockQuestId: null }] as GameState["zones"], + quests: [], + exploration: undefined, + }); + vi.mocked(prisma.gameState.findUnique).mockResolvedValueOnce({ state } as never); + vi.mocked(prisma.gameState.update).mockResolvedValueOnce({} as never); + const res = await challenge({ bossId: "test_boss" }); + expect(res.status).toBe(200); + const body = await res.json() as { won: boolean }; + expect(body.won).toBe(true); + }); + + it("unlocks exploration areas when a zone is unlocked on boss defeat", async () => { + const state = makeState({ + bosses: [makeBoss({ currentHp: 100, maxHp: 100, damagePerSecond: 1 })] as GameState["bosses"], + adventurers: [makeAdventurer()] as GameState["adventurers"], + zones: [{ id: "test_zone", status: "locked", unlockBossId: "test_boss", unlockQuestId: null }] as GameState["zones"], + quests: [], + exploration: { + areas: [{ id: "test_area", status: "locked" as const }], + materials: [], + craftedRecipeIds: [], + craftedGoldMultiplier: 1, + craftedEssenceMultiplier: 1, + craftedClickMultiplier: 1, + craftedCombatMultiplier: 1, + }, + }); + vi.mocked(prisma.gameState.findUnique).mockResolvedValueOnce({ state } as never); + let savedState: GameState | undefined; + vi.mocked(prisma.gameState.update).mockImplementationOnce(async (args) => { + /* eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- Test assertion */ + savedState = (args as { data: { state: GameState } }).data.state; + return {} as never; + }); + const res = await challenge({ bossId: "test_boss" }); + expect(res.status).toBe(200); + // Exploration area should remain locked — no matching defaultExploration for "test_area" + const area = savedState?.exploration?.areas.find((a) => a.id === "test_area"); + expect(area?.status).toBe("locked"); + }); + it("returns 500 when the database throws", async () => { vi.mocked(prisma.gameState.findUnique).mockRejectedValueOnce(new Error("DB error")); const res = await challenge({ bossId: "test_boss" }); diff --git a/apps/api/test/routes/timers.spec.ts b/apps/api/test/routes/timers.spec.ts index fea395e..7ec2910 100644 --- a/apps/api/test/routes/timers.spec.ts +++ b/apps/api/test/routes/timers.spec.ts @@ -71,7 +71,7 @@ describe("timers route", () => { { id: "q1", name: "Forest Patrol", - status: "in_progress", + status: "active", startedAt: startedAt, durationSeconds: 600, }, @@ -110,7 +110,7 @@ describe("timers route", () => { { id: "q1", name: "Old Quest", - status: "in_progress", + status: "active", startedAt: startedAt, durationSeconds: 600, },