generated from nhcarrigan/template
fix: adventurer unlocks not applied by force-unlock tool #93
@@ -7,6 +7,11 @@
|
||||
/* eslint-disable max-lines-per-function -- Route handlers require many steps */
|
||||
/* eslint-disable max-lines -- Multiple route handlers and helper functions in one file */
|
||||
import { createHmac } from "node:crypto";
|
||||
import {
|
||||
STORY_CHAPTERS,
|
||||
isStoryChapterUnlocked,
|
||||
type GameState,
|
||||
} from "@elysium/types";
|
||||
import { Hono } from "hono";
|
||||
import { defaultBosses } from "../data/bosses.js";
|
||||
import { defaultExplorations } from "../data/explorations.js";
|
||||
@@ -18,7 +23,6 @@ import { prisma } from "../db/client.js";
|
||||
import { authMiddleware } from "../middleware/auth.js";
|
||||
import { logger } from "../services/logger.js";
|
||||
import type { HonoEnvironment } from "../types/hono.js";
|
||||
import type { GameState } from "@elysium/types";
|
||||
|
||||
/**
|
||||
* Computes the HMAC-SHA256 of data using the given secret.
|
||||
@@ -288,6 +292,109 @@ const applyAdventurerUnlocks = (state: GameState): number => {
|
||||
return count;
|
||||
};
|
||||
|
||||
/**
|
||||
* Collects all upgrade IDs the player has legitimately earned via boss defeats
|
||||
* and completed quest rewards.
|
||||
* @param state - The player's current game state.
|
||||
* @returns A set of earned upgrade IDs.
|
||||
*/
|
||||
const collectEarnedUpgradeIds = (state: GameState): Set<string> => {
|
||||
const earnedIds = new Set<string>();
|
||||
for (const boss of state.bosses) {
|
||||
if (boss.status === "defeated") {
|
||||
for (const upgradeId of boss.upgradeRewards) {
|
||||
earnedIds.add(upgradeId);
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const quest of state.quests) {
|
||||
if (quest.status !== "completed") {
|
||||
continue;
|
||||
}
|
||||
for (const reward of quest.rewards) {
|
||||
if (reward.type === "upgrade" && reward.targetId !== undefined) {
|
||||
earnedIds.add(reward.targetId);
|
||||
}
|
||||
}
|
||||
}
|
||||
return earnedIds;
|
||||
};
|
||||
|
||||
/**
|
||||
* Unlocks any upgrades that were granted as rewards for defeated bosses or
|
||||
* 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 upgrades that were unlocked.
|
||||
*/
|
||||
const applyUpgradeUnlocks = (state: GameState): number => {
|
||||
let count = 0;
|
||||
const earnedUpgradeIds = collectEarnedUpgradeIds(state);
|
||||
|
||||
for (const upgrade of state.upgrades) {
|
||||
if (!upgrade.unlocked && earnedUpgradeIds.has(upgrade.id)) {
|
||||
upgrade.unlocked = true;
|
||||
count = count + 1;
|
||||
}
|
||||
}
|
||||
|
||||
return count;
|
||||
};
|
||||
|
||||
/**
|
||||
* Marks as owned any equipment that was granted as a reward for defeated bosses
|
||||
* but is still unowned in the player's state.
|
||||
* @param state - The player's current game state (mutated directly).
|
||||
* @returns The number of equipment items that were marked as owned.
|
||||
*/
|
||||
const applyEquipmentUnlocks = (state: GameState): number => {
|
||||
let count = 0;
|
||||
const earnedEquipmentIds = new Set<string>();
|
||||
|
||||
for (const boss of state.bosses) {
|
||||
if (boss.status !== "defeated") {
|
||||
continue;
|
||||
}
|
||||
for (const equipmentId of boss.equipmentRewards) {
|
||||
earnedEquipmentIds.add(equipmentId);
|
||||
}
|
||||
}
|
||||
|
||||
for (const item of state.equipment) {
|
||||
if (!item.owned && earnedEquipmentIds.has(item.id)) {
|
||||
item.owned = true;
|
||||
count = count + 1;
|
||||
}
|
||||
}
|
||||
|
||||
return count;
|
||||
};
|
||||
|
||||
/**
|
||||
* Unlocks any story chapters whose conditions are met by the current game state
|
||||
* but are still absent from the player's unlockedChapterIds list.
|
||||
* @param state - The player's current game state (mutated directly).
|
||||
* @returns The number of story chapters that were unlocked.
|
||||
*/
|
||||
const applyStoryUnlocks = (state: GameState): number => {
|
||||
if (state.story === undefined) {
|
||||
return 0;
|
||||
}
|
||||
let count = 0;
|
||||
const alreadyUnlocked = new Set(state.story.unlockedChapterIds);
|
||||
|
||||
for (const chapter of STORY_CHAPTERS) {
|
||||
if (alreadyUnlocked.has(chapter.id)) {
|
||||
continue;
|
||||
}
|
||||
if (isStoryChapterUnlocked(chapter, state)) {
|
||||
state.story.unlockedChapterIds.push(chapter.id);
|
||||
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).
|
||||
@@ -334,8 +441,11 @@ const applyForceUnlocks = (
|
||||
): {
|
||||
adventurersUnlocked: number;
|
||||
bossesUnlocked: number;
|
||||
equipmentUnlocked: number;
|
||||
explorationUnlocked: number;
|
||||
questsUnlocked: number;
|
||||
storyUnlocked: number;
|
||||
upgradesUnlocked: number;
|
||||
zonesUnlocked: number;
|
||||
} => {
|
||||
const zonesUnlocked = applyZoneUnlocks(state);
|
||||
@@ -343,11 +453,17 @@ const applyForceUnlocks = (
|
||||
const bossesUnlocked = applyBossUnlocks(state);
|
||||
const explorationUnlocked = applyExplorationUnlocks(state);
|
||||
const adventurersUnlocked = applyAdventurerUnlocks(state);
|
||||
const upgradesUnlocked = applyUpgradeUnlocks(state);
|
||||
const equipmentUnlocked = applyEquipmentUnlocks(state);
|
||||
const storyUnlocked = applyStoryUnlocks(state);
|
||||
return {
|
||||
adventurersUnlocked,
|
||||
bossesUnlocked,
|
||||
equipmentUnlocked,
|
||||
explorationUnlocked,
|
||||
questsUnlocked,
|
||||
storyUnlocked,
|
||||
upgradesUnlocked,
|
||||
zonesUnlocked,
|
||||
};
|
||||
};
|
||||
@@ -372,8 +488,11 @@ debugRouter.post("/force-unlocks", async(context) => {
|
||||
const {
|
||||
adventurersUnlocked,
|
||||
bossesUnlocked,
|
||||
equipmentUnlocked,
|
||||
explorationUnlocked,
|
||||
questsUnlocked,
|
||||
storyUnlocked,
|
||||
upgradesUnlocked,
|
||||
zonesUnlocked,
|
||||
} = applyForceUnlocks(state);
|
||||
|
||||
@@ -393,10 +512,13 @@ debugRouter.post("/force-unlocks", async(context) => {
|
||||
return context.json({
|
||||
adventurersUnlocked,
|
||||
bossesUnlocked,
|
||||
equipmentUnlocked,
|
||||
explorationUnlocked,
|
||||
questsUnlocked,
|
||||
signature,
|
||||
state,
|
||||
storyUnlocked,
|
||||
upgradesUnlocked,
|
||||
zonesUnlocked,
|
||||
});
|
||||
} catch (error) {
|
||||
|
||||
@@ -12,6 +12,49 @@ import { ConfirmationModal } from "../ui/confirmationModal.js";
|
||||
|
||||
type ActiveModal = "force-unlocks" | "hard-reset" | null;
|
||||
|
||||
interface ForceUnlocksResult {
|
||||
adventurersUnlocked: number;
|
||||
bossesUnlocked: number;
|
||||
equipmentUnlocked: number;
|
||||
explorationUnlocked: number;
|
||||
questsUnlocked: number;
|
||||
storyUnlocked: number;
|
||||
upgradesUnlocked: number;
|
||||
zonesUnlocked: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a human-readable summary of what the force-unlock operation corrected.
|
||||
* @param result - The counts returned by the force-unlock operation.
|
||||
* @returns A message string describing what was fixed, or a confirmation that nothing needed fixing.
|
||||
*/
|
||||
const buildForceUnlocksMessage = (result: ForceUnlocksResult): string => {
|
||||
const entries: Array<[ number, string ]> = [
|
||||
[ result.zonesUnlocked, "zone(s)" ],
|
||||
[ result.questsUnlocked, "quest(s)" ],
|
||||
[ result.bossesUnlocked, "boss(es)" ],
|
||||
[ result.explorationUnlocked, "exploration area(s)" ],
|
||||
[ result.adventurersUnlocked, "adventurer tier(s)" ],
|
||||
[ result.upgradesUnlocked, "upgrade(s)" ],
|
||||
[ result.equipmentUnlocked, "equipment item(s)" ],
|
||||
[ result.storyUnlocked, "story chapter(s)" ],
|
||||
];
|
||||
const parts = entries.
|
||||
filter(([ count ]) => {
|
||||
return count > 0;
|
||||
}).
|
||||
map(([ count, label ]) => {
|
||||
return `${String(count)} ${label}`;
|
||||
});
|
||||
if (parts.length === 0) {
|
||||
return "Everything looks correct — no missing unlocks were found.";
|
||||
}
|
||||
const total = entries.reduce((sum, [ count ]) => {
|
||||
return sum + count;
|
||||
}, 0);
|
||||
return `Fixed ${String(total)} unlock(s): ${parts.join(", ")}.`;
|
||||
};
|
||||
|
||||
/**
|
||||
* Renders the debug panel with tools for fixing stuck game state.
|
||||
* @returns The JSX element.
|
||||
@@ -38,33 +81,7 @@ const DebugPanel = (): JSX.Element => {
|
||||
setActiveModal(null);
|
||||
void (async(): Promise<void> => {
|
||||
const result = await forceUnlocks();
|
||||
const parts: Array<string> = [];
|
||||
if (result.zonesUnlocked > 0) {
|
||||
parts.push(`${String(result.zonesUnlocked)} zone(s)`);
|
||||
}
|
||||
if (result.questsUnlocked > 0) {
|
||||
parts.push(`${String(result.questsUnlocked)} quest(s)`);
|
||||
}
|
||||
if (result.bossesUnlocked > 0) {
|
||||
parts.push(`${String(result.bossesUnlocked)} boss(es)`);
|
||||
}
|
||||
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.adventurersUnlocked;
|
||||
const message
|
||||
= parts.length === 0
|
||||
? "Everything looks correct — no missing unlocks were found."
|
||||
: `Fixed ${String(total)} unlock(s): ${parts.join(", ")}.`;
|
||||
setForceUnlocksResult(message);
|
||||
setForceUnlocksResult(buildForceUnlocksMessage(result));
|
||||
})();
|
||||
}
|
||||
|
||||
|
||||
@@ -560,8 +560,11 @@ interface GameContextValue {
|
||||
forceUnlocks: ()=> Promise<{
|
||||
adventurersUnlocked: number;
|
||||
bossesUnlocked: number;
|
||||
equipmentUnlocked: number;
|
||||
explorationUnlocked: number;
|
||||
questsUnlocked: number;
|
||||
storyUnlocked: number;
|
||||
upgradesUnlocked: number;
|
||||
zonesUnlocked: number;
|
||||
}>;
|
||||
|
||||
@@ -2107,8 +2110,11 @@ export const GameProvider = ({
|
||||
return {
|
||||
adventurersUnlocked: data.adventurersUnlocked,
|
||||
bossesUnlocked: data.bossesUnlocked,
|
||||
equipmentUnlocked: data.equipmentUnlocked,
|
||||
explorationUnlocked: data.explorationUnlocked,
|
||||
questsUnlocked: data.questsUnlocked,
|
||||
storyUnlocked: data.storyUnlocked,
|
||||
upgradesUnlocked: data.upgradesUnlocked,
|
||||
zonesUnlocked: data.zonesUnlocked,
|
||||
};
|
||||
} catch (error_: unknown) {
|
||||
@@ -2120,8 +2126,11 @@ export const GameProvider = ({
|
||||
return {
|
||||
adventurersUnlocked: 0,
|
||||
bossesUnlocked: 0,
|
||||
equipmentUnlocked: 0,
|
||||
explorationUnlocked: 0,
|
||||
questsUnlocked: 0,
|
||||
storyUnlocked: 0,
|
||||
upgradesUnlocked: 0,
|
||||
zonesUnlocked: 0,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -430,6 +430,21 @@ interface ForceUnlocksResponse {
|
||||
*/
|
||||
adventurersUnlocked: number;
|
||||
|
||||
/**
|
||||
* Number of upgrades that were unlocked by this operation.
|
||||
*/
|
||||
upgradesUnlocked: number;
|
||||
|
||||
/**
|
||||
* Number of equipment items that were marked as owned by this operation.
|
||||
*/
|
||||
equipmentUnlocked: number;
|
||||
|
||||
/**
|
||||
* Number of story chapters that were unlocked by this operation.
|
||||
*/
|
||||
storyUnlocked: number;
|
||||
|
||||
/**
|
||||
* HMAC-SHA256 signature of the corrected state for anti-cheat chain continuity.
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user