fix: adventurer unlocks not applied by force-unlock tool
Security Scan and Upload / Security & DefectDojo Upload (pull_request) Successful in 1m9s
CI / Lint, Build & Test (pull_request) Failing after 1m13s

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
This commit is contained in:
2026-03-20 09:44:58 -07:00
committed by Naomi Carrigan
parent bb60ae3390
commit 715ccd3fc7
4 changed files with 61 additions and 4 deletions
+48 -3
View File
@@ -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<string>();
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<HonoEnvironment>();
@@ -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,
+5 -1
View File
@@ -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."
+3
View File
@@ -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,
+5
View File
@@ -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.
*/