From 715ccd3fc71eb51aa85415eee3b08e456f13145e Mon Sep 17 00:00:00 2001 From: Hikari Date: Fri, 20 Mar 2026 09:44:58 -0700 Subject: [PATCH] fix: adventurer unlocks not applied by force-unlock tool The force-unlock debug route now scans completed quests for adventurer rewards and ensures those tiers are marked as unlocked in game state. The UI and API response type both surface the new adventurersUnlocked count alongside existing zone/quest/boss/exploration counts. Closes #88 --- apps/api/src/routes/debug.ts | 51 +++++++++++++++++++-- apps/web/src/components/game/debugPanel.tsx | 6 ++- apps/web/src/context/gameContext.tsx | 3 ++ packages/types/src/interfaces/api.ts | 5 ++ 4 files changed, 61 insertions(+), 4 deletions(-) diff --git a/apps/api/src/routes/debug.ts b/apps/api/src/routes/debug.ts index 3cc345e..ecf482e 100644 --- a/apps/api/src/routes/debug.ts +++ b/apps/api/src/routes/debug.ts @@ -257,6 +257,37 @@ const applyBossUnlocks = (state: GameState): number => { return count; }; +/** + * Unlocks any adventurer tiers that were granted as rewards for completed quests + * but are still locked in the player's state. + * @param state - The player's current game state (mutated directly). + * @returns The number of adventurer tiers that were unlocked. + */ +const applyAdventurerUnlocks = (state: GameState): number => { + let count = 0; + const earnedAdventurerIds = new Set(); + + for (const quest of state.quests) { + if (quest.status !== "completed") { + continue; + } + for (const reward of quest.rewards) { + if (reward.type === "adventurer" && reward.targetId !== undefined) { + earnedAdventurerIds.add(reward.targetId); + } + } + } + + for (const adventurer of state.adventurers) { + if (!adventurer.unlocked && earnedAdventurerIds.has(adventurer.id)) { + adventurer.unlocked = true; + count = count + 1; + } + } + + return count; +}; + /** * Makes available any exploration areas whose parent zone is now unlocked. * @param state - The player's current game state (mutated directly). @@ -301,6 +332,7 @@ const applyExplorationUnlocks = (state: GameState): number => { const applyForceUnlocks = ( state: GameState, ): { + adventurersUnlocked: number; bossesUnlocked: number; explorationUnlocked: number; questsUnlocked: number; @@ -310,7 +342,14 @@ const applyForceUnlocks = ( const questsUnlocked = applyQuestUnlocks(state); const bossesUnlocked = applyBossUnlocks(state); const explorationUnlocked = applyExplorationUnlocks(state); - return { bossesUnlocked, explorationUnlocked, questsUnlocked, zonesUnlocked }; + const adventurersUnlocked = applyAdventurerUnlocks(state); + return { + adventurersUnlocked, + bossesUnlocked, + explorationUnlocked, + questsUnlocked, + zonesUnlocked, + }; }; const debugRouter = new Hono(); @@ -330,8 +369,13 @@ debugRouter.post("/force-unlocks", async(context) => { /* eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- Prisma stores state as JSON object */ const state = gameStateRecord.state as unknown as GameState; - const { bossesUnlocked, explorationUnlocked, questsUnlocked, zonesUnlocked } - = applyForceUnlocks(state); + const { + adventurersUnlocked, + bossesUnlocked, + explorationUnlocked, + questsUnlocked, + zonesUnlocked, + } = applyForceUnlocks(state); const updatedAt = Date.now(); await prisma.gameState.update({ @@ -347,6 +391,7 @@ debugRouter.post("/force-unlocks", async(context) => { : computeHmac(JSON.stringify(state), secret); return context.json({ + adventurersUnlocked, bossesUnlocked, explorationUnlocked, questsUnlocked, diff --git a/apps/web/src/components/game/debugPanel.tsx b/apps/web/src/components/game/debugPanel.tsx index 0abc231..8bfc410 100644 --- a/apps/web/src/components/game/debugPanel.tsx +++ b/apps/web/src/components/game/debugPanel.tsx @@ -51,11 +51,15 @@ const DebugPanel = (): JSX.Element => { if (result.explorationUnlocked > 0) { parts.push(`${String(result.explorationUnlocked)} exploration area(s)`); } + if (result.adventurersUnlocked > 0) { + parts.push(`${String(result.adventurersUnlocked)} adventurer tier(s)`); + } const total = result.zonesUnlocked + result.questsUnlocked + result.bossesUnlocked - + result.explorationUnlocked; + + result.explorationUnlocked + + result.adventurersUnlocked; const message = parts.length === 0 ? "Everything looks correct — no missing unlocks were found." diff --git a/apps/web/src/context/gameContext.tsx b/apps/web/src/context/gameContext.tsx index a33b9d1..b65db14 100644 --- a/apps/web/src/context/gameContext.tsx +++ b/apps/web/src/context/gameContext.tsx @@ -558,6 +558,7 @@ interface GameContextValue { * @returns Counts of what was corrected. */ forceUnlocks: ()=> Promise<{ + adventurersUnlocked: number; bossesUnlocked: number; explorationUnlocked: number; questsUnlocked: number; @@ -2104,6 +2105,7 @@ export const GameProvider = ({ localStorage.setItem("elysium_save_signature", data.signature); } return { + adventurersUnlocked: data.adventurersUnlocked, bossesUnlocked: data.bossesUnlocked, explorationUnlocked: data.explorationUnlocked, questsUnlocked: data.questsUnlocked, @@ -2116,6 +2118,7 @@ export const GameProvider = ({ : "Failed to force unlocks", ); return { + adventurersUnlocked: 0, bossesUnlocked: 0, explorationUnlocked: 0, questsUnlocked: 0, diff --git a/packages/types/src/interfaces/api.ts b/packages/types/src/interfaces/api.ts index 8da988e..ef5e7f0 100644 --- a/packages/types/src/interfaces/api.ts +++ b/packages/types/src/interfaces/api.ts @@ -425,6 +425,11 @@ interface ForceUnlocksResponse { */ explorationUnlocked: number; + /** + * Number of adventurer tiers that were unlocked by this operation. + */ + adventurersUnlocked: number; + /** * HMAC-SHA256 signature of the corrected state for anti-cheat chain continuity. */