generated from nhcarrigan/template
fix: adventurer unlocks not applied by force-unlock tool (#93)
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 the existing zone/quest/boss/exploration counts. Closes #88 ✨ This PR was created with help from Hikari~ 🌸 Reviewed-on: #93 Co-authored-by: Hikari <hikari@nhcarrigan.com> Co-committed-by: Hikari <hikari@nhcarrigan.com>
This commit was merged in pull request #93.
This commit is contained in:
@@ -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.
|
||||
@@ -257,6 +261,180 @@ 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 completedQuestIds = new Set(
|
||||
state.quests.
|
||||
filter((q) => {
|
||||
return q.status === "completed";
|
||||
}).
|
||||
map((q) => {
|
||||
return q.id;
|
||||
}),
|
||||
);
|
||||
const earnedAdventurerIds = new Set<string>();
|
||||
|
||||
for (const questDefinition of defaultQuests) {
|
||||
if (!completedQuestIds.has(questDefinition.id)) {
|
||||
continue;
|
||||
}
|
||||
for (const reward of questDefinition.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;
|
||||
};
|
||||
|
||||
/**
|
||||
* Collects all upgrade IDs the player has legitimately earned via boss defeats
|
||||
* and completed quest rewards, sourcing reward data from game definitions.
|
||||
* @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>();
|
||||
const defeatedBossIds = new Set(
|
||||
state.bosses.
|
||||
filter((b) => {
|
||||
return b.status === "defeated";
|
||||
}).
|
||||
map((b) => {
|
||||
return b.id;
|
||||
}),
|
||||
);
|
||||
const completedQuestIds = new Set(
|
||||
state.quests.
|
||||
filter((q) => {
|
||||
return q.status === "completed";
|
||||
}).
|
||||
map((q) => {
|
||||
return q.id;
|
||||
}),
|
||||
);
|
||||
|
||||
for (const bossDefinition of defaultBosses) {
|
||||
if (!defeatedBossIds.has(bossDefinition.id)) {
|
||||
continue;
|
||||
}
|
||||
for (const upgradeId of bossDefinition.upgradeRewards) {
|
||||
earnedIds.add(upgradeId);
|
||||
}
|
||||
}
|
||||
|
||||
for (const questDefinition of defaultQuests) {
|
||||
if (!completedQuestIds.has(questDefinition.id)) {
|
||||
continue;
|
||||
}
|
||||
for (const reward of questDefinition.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 defeatedBossIds = new Set(
|
||||
state.bosses.
|
||||
filter((b) => {
|
||||
return b.status === "defeated";
|
||||
}).
|
||||
map((b) => {
|
||||
return b.id;
|
||||
}),
|
||||
);
|
||||
const earnedEquipmentIds = new Set<string>();
|
||||
|
||||
for (const bossDefinition of defaultBosses) {
|
||||
if (!defeatedBossIds.has(bossDefinition.id)) {
|
||||
continue;
|
||||
}
|
||||
for (const equipmentId of bossDefinition.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).
|
||||
@@ -301,16 +479,33 @@ const applyExplorationUnlocks = (state: GameState): number => {
|
||||
const applyForceUnlocks = (
|
||||
state: GameState,
|
||||
): {
|
||||
adventurersUnlocked: number;
|
||||
bossesUnlocked: number;
|
||||
equipmentUnlocked: number;
|
||||
explorationUnlocked: number;
|
||||
questsUnlocked: number;
|
||||
storyUnlocked: number;
|
||||
upgradesUnlocked: number;
|
||||
zonesUnlocked: number;
|
||||
} => {
|
||||
const zonesUnlocked = applyZoneUnlocks(state);
|
||||
const questsUnlocked = applyQuestUnlocks(state);
|
||||
const bossesUnlocked = applyBossUnlocks(state);
|
||||
const explorationUnlocked = applyExplorationUnlocks(state);
|
||||
return { bossesUnlocked, explorationUnlocked, questsUnlocked, zonesUnlocked };
|
||||
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,
|
||||
};
|
||||
};
|
||||
|
||||
const debugRouter = new Hono<HonoEnvironment>();
|
||||
@@ -330,8 +525,16 @@ 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,
|
||||
equipmentUnlocked,
|
||||
explorationUnlocked,
|
||||
questsUnlocked,
|
||||
storyUnlocked,
|
||||
upgradesUnlocked,
|
||||
zonesUnlocked,
|
||||
} = applyForceUnlocks(state);
|
||||
|
||||
const updatedAt = Date.now();
|
||||
await prisma.gameState.update({
|
||||
@@ -347,11 +550,15 @@ debugRouter.post("/force-unlocks", async(context) => {
|
||||
: computeHmac(JSON.stringify(state), secret);
|
||||
|
||||
return context.json({
|
||||
adventurersUnlocked,
|
||||
bossesUnlocked,
|
||||
equipmentUnlocked,
|
||||
explorationUnlocked,
|
||||
questsUnlocked,
|
||||
signature,
|
||||
state,
|
||||
storyUnlocked,
|
||||
upgradesUnlocked,
|
||||
zonesUnlocked,
|
||||
});
|
||||
} catch (error) {
|
||||
|
||||
@@ -366,6 +366,161 @@ describe("debug route", () => {
|
||||
expect(body.explorationUnlocked).toBe(0);
|
||||
});
|
||||
|
||||
it("unlocks adventurer tier when its quest has been completed", async () => {
|
||||
const state = makeState({
|
||||
adventurers: [ { id: "scout", unlocked: false } ] as GameState["adventurers"],
|
||||
quests: [ { id: "haunted_mine", status: "completed" } ] as GameState["quests"],
|
||||
});
|
||||
vi.mocked(prisma.gameState.findUnique).mockResolvedValueOnce({ state } as never);
|
||||
vi.mocked(prisma.gameState.update).mockResolvedValueOnce({} as never);
|
||||
const res = await forceUnlocks();
|
||||
const body = await res.json() as { adventurersUnlocked: number };
|
||||
expect(body.adventurersUnlocked).toBe(1);
|
||||
});
|
||||
|
||||
it("does not unlock adventurer tier when it is already unlocked", async () => {
|
||||
const state = makeState({
|
||||
adventurers: [ { id: "scout", unlocked: true } ] as GameState["adventurers"],
|
||||
quests: [ { id: "haunted_mine", status: "completed" } ] as GameState["quests"],
|
||||
});
|
||||
vi.mocked(prisma.gameState.findUnique).mockResolvedValueOnce({ state } as never);
|
||||
vi.mocked(prisma.gameState.update).mockResolvedValueOnce({} as never);
|
||||
const res = await forceUnlocks();
|
||||
const body = await res.json() as { adventurersUnlocked: number };
|
||||
expect(body.adventurersUnlocked).toBe(0);
|
||||
});
|
||||
|
||||
it("unlocks upgrade when its boss has been defeated", async () => {
|
||||
const state = makeState({
|
||||
bosses: [ { id: "troll_king", status: "defeated" } ] as GameState["bosses"],
|
||||
upgrades: [ { id: "click_2", unlocked: false } ] as GameState["upgrades"],
|
||||
});
|
||||
vi.mocked(prisma.gameState.findUnique).mockResolvedValueOnce({ state } as never);
|
||||
vi.mocked(prisma.gameState.update).mockResolvedValueOnce({} as never);
|
||||
const res = await forceUnlocks();
|
||||
const body = await res.json() as { upgradesUnlocked: number };
|
||||
expect(body.upgradesUnlocked).toBe(1);
|
||||
});
|
||||
|
||||
it("does not unlock upgrade when boss is not defeated", async () => {
|
||||
const state = makeState({
|
||||
bosses: [ { id: "troll_king", status: "available" } ] as GameState["bosses"],
|
||||
upgrades: [ { id: "click_2", unlocked: false } ] as GameState["upgrades"],
|
||||
});
|
||||
vi.mocked(prisma.gameState.findUnique).mockResolvedValueOnce({ state } as never);
|
||||
vi.mocked(prisma.gameState.update).mockResolvedValueOnce({} as never);
|
||||
const res = await forceUnlocks();
|
||||
const body = await res.json() as { upgradesUnlocked: number };
|
||||
expect(body.upgradesUnlocked).toBe(0);
|
||||
});
|
||||
|
||||
it("does not unlock upgrade when it is already unlocked", async () => {
|
||||
const state = makeState({
|
||||
bosses: [ { id: "troll_king", status: "defeated" } ] as GameState["bosses"],
|
||||
upgrades: [ { id: "click_2", unlocked: true } ] as GameState["upgrades"],
|
||||
});
|
||||
vi.mocked(prisma.gameState.findUnique).mockResolvedValueOnce({ state } as never);
|
||||
vi.mocked(prisma.gameState.update).mockResolvedValueOnce({} as never);
|
||||
const res = await forceUnlocks();
|
||||
const body = await res.json() as { upgradesUnlocked: number };
|
||||
expect(body.upgradesUnlocked).toBe(0);
|
||||
});
|
||||
|
||||
it("unlocks upgrade granted as a quest reward", async () => {
|
||||
const state = makeState({
|
||||
quests: [ { id: "haunted_mine", status: "completed" } ] as GameState["quests"],
|
||||
upgrades: [ { id: "global_1", unlocked: false } ] as GameState["upgrades"],
|
||||
});
|
||||
vi.mocked(prisma.gameState.findUnique).mockResolvedValueOnce({ state } as never);
|
||||
vi.mocked(prisma.gameState.update).mockResolvedValueOnce({} as never);
|
||||
const res = await forceUnlocks();
|
||||
const body = await res.json() as { upgradesUnlocked: number };
|
||||
expect(body.upgradesUnlocked).toBe(1);
|
||||
});
|
||||
|
||||
it("marks equipment as owned when its boss has been defeated", async () => {
|
||||
const state = makeState({
|
||||
bosses: [ { id: "troll_king", status: "defeated" } ] as GameState["bosses"],
|
||||
equipment: [ { id: "iron_sword", owned: false } ] as GameState["equipment"],
|
||||
});
|
||||
vi.mocked(prisma.gameState.findUnique).mockResolvedValueOnce({ state } as never);
|
||||
vi.mocked(prisma.gameState.update).mockResolvedValueOnce({} as never);
|
||||
const res = await forceUnlocks();
|
||||
const body = await res.json() as { equipmentUnlocked: number };
|
||||
expect(body.equipmentUnlocked).toBe(1);
|
||||
});
|
||||
|
||||
it("does not mark equipment as owned when boss is not defeated", async () => {
|
||||
const state = makeState({
|
||||
bosses: [ { id: "troll_king", status: "available" } ] as GameState["bosses"],
|
||||
equipment: [ { id: "iron_sword", owned: false } ] as GameState["equipment"],
|
||||
});
|
||||
vi.mocked(prisma.gameState.findUnique).mockResolvedValueOnce({ state } as never);
|
||||
vi.mocked(prisma.gameState.update).mockResolvedValueOnce({} as never);
|
||||
const res = await forceUnlocks();
|
||||
const body = await res.json() as { equipmentUnlocked: number };
|
||||
expect(body.equipmentUnlocked).toBe(0);
|
||||
});
|
||||
|
||||
it("does not mark equipment as owned when it is already owned", async () => {
|
||||
const state = makeState({
|
||||
bosses: [ { id: "troll_king", status: "defeated" } ] as GameState["bosses"],
|
||||
equipment: [ { id: "iron_sword", owned: true } ] as GameState["equipment"],
|
||||
});
|
||||
vi.mocked(prisma.gameState.findUnique).mockResolvedValueOnce({ state } as never);
|
||||
vi.mocked(prisma.gameState.update).mockResolvedValueOnce({} as never);
|
||||
const res = await forceUnlocks();
|
||||
const body = await res.json() as { equipmentUnlocked: number };
|
||||
expect(body.equipmentUnlocked).toBe(0);
|
||||
});
|
||||
|
||||
it("returns storyUnlocked=0 when story is undefined", async () => {
|
||||
const state = makeState({
|
||||
story: undefined as unknown as GameState["story"],
|
||||
});
|
||||
vi.mocked(prisma.gameState.findUnique).mockResolvedValueOnce({ state } as never);
|
||||
vi.mocked(prisma.gameState.update).mockResolvedValueOnce({} as never);
|
||||
const res = await forceUnlocks();
|
||||
const body = await res.json() as { storyUnlocked: number };
|
||||
expect(body.storyUnlocked).toBe(0);
|
||||
});
|
||||
|
||||
it("unlocks story chapter when its boss has been defeated", async () => {
|
||||
const state = makeState({
|
||||
bosses: [ { id: "forest_giant", status: "defeated" } ] as GameState["bosses"],
|
||||
story: { completedChapters: [], unlockedChapterIds: [] },
|
||||
});
|
||||
vi.mocked(prisma.gameState.findUnique).mockResolvedValueOnce({ state } as never);
|
||||
vi.mocked(prisma.gameState.update).mockResolvedValueOnce({} as never);
|
||||
const res = await forceUnlocks();
|
||||
const body = await res.json() as { storyUnlocked: number };
|
||||
expect(body.storyUnlocked).toBe(1);
|
||||
});
|
||||
|
||||
it("does not unlock story chapter when boss is not defeated", async () => {
|
||||
const state = makeState({
|
||||
bosses: [ { id: "forest_giant", status: "available" } ] as GameState["bosses"],
|
||||
story: { completedChapters: [], unlockedChapterIds: [] },
|
||||
});
|
||||
vi.mocked(prisma.gameState.findUnique).mockResolvedValueOnce({ state } as never);
|
||||
vi.mocked(prisma.gameState.update).mockResolvedValueOnce({} as never);
|
||||
const res = await forceUnlocks();
|
||||
const body = await res.json() as { storyUnlocked: number };
|
||||
expect(body.storyUnlocked).toBe(0);
|
||||
});
|
||||
|
||||
it("does not unlock story chapter when it is already unlocked", async () => {
|
||||
const state = makeState({
|
||||
bosses: [ { id: "forest_giant", status: "defeated" } ] as GameState["bosses"],
|
||||
story: { completedChapters: [], unlockedChapterIds: [ "story_ch_01" ] },
|
||||
});
|
||||
vi.mocked(prisma.gameState.findUnique).mockResolvedValueOnce({ state } as never);
|
||||
vi.mocked(prisma.gameState.update).mockResolvedValueOnce({} as never);
|
||||
const res = await forceUnlocks();
|
||||
const body = await res.json() as { storyUnlocked: number };
|
||||
expect(body.storyUnlocked).toBe(0);
|
||||
});
|
||||
|
||||
it("computes HMAC signature when ANTI_CHEAT_SECRET is set", async () => {
|
||||
process.env.ANTI_CHEAT_SECRET = "test_secret";
|
||||
const state = makeState();
|
||||
|
||||
@@ -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,29 +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)`);
|
||||
}
|
||||
const total
|
||||
= result.zonesUnlocked
|
||||
+ result.questsUnlocked
|
||||
+ result.bossesUnlocked
|
||||
+ result.explorationUnlocked;
|
||||
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));
|
||||
})();
|
||||
}
|
||||
|
||||
|
||||
@@ -558,9 +558,13 @@ interface GameContextValue {
|
||||
* @returns Counts of what was corrected.
|
||||
*/
|
||||
forceUnlocks: ()=> Promise<{
|
||||
adventurersUnlocked: number;
|
||||
bossesUnlocked: number;
|
||||
equipmentUnlocked: number;
|
||||
explorationUnlocked: number;
|
||||
questsUnlocked: number;
|
||||
storyUnlocked: number;
|
||||
upgradesUnlocked: number;
|
||||
zonesUnlocked: number;
|
||||
}>;
|
||||
|
||||
@@ -2104,9 +2108,13 @@ export const GameProvider = ({
|
||||
localStorage.setItem("elysium_save_signature", data.signature);
|
||||
}
|
||||
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) {
|
||||
@@ -2116,9 +2124,13 @@ export const GameProvider = ({
|
||||
: "Failed to force unlocks",
|
||||
);
|
||||
return {
|
||||
adventurersUnlocked: 0,
|
||||
bossesUnlocked: 0,
|
||||
equipmentUnlocked: 0,
|
||||
explorationUnlocked: 0,
|
||||
questsUnlocked: 0,
|
||||
storyUnlocked: 0,
|
||||
upgradesUnlocked: 0,
|
||||
zonesUnlocked: 0,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -425,6 +425,26 @@ interface ForceUnlocksResponse {
|
||||
*/
|
||||
explorationUnlocked: number;
|
||||
|
||||
/**
|
||||
* Number of adventurer tiers that were unlocked by this operation.
|
||||
*/
|
||||
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