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) {
|
||||
|
||||
Reference in New Issue
Block a user