/** * @file Prestige routes handling prestige resets and upgrade purchases. * @copyright nhcarrigan * @license Naomi's Public License * @author Naomi Carrigan */ /* eslint-disable max-lines-per-function -- Route handlers require many steps */ /* eslint-disable max-statements -- Route handlers require many statements */ import { Hono } from "hono"; import { defaultPrestigeUpgrades } from "../data/prestigeUpgrades.js"; import { prisma } from "../db/client.js"; import { authMiddleware } from "../middleware/auth.js"; import { updateChallengeProgress } from "../services/dailyChallenges.js"; import { buildPostPrestigeState, computeRunestoneMultipliers, isEligibleForPrestige, } from "../services/prestige.js"; import { postMilestoneWebhook } from "../services/webhook.js"; import type { HonoEnvironment } from "../types/hono.js"; import type { BuyPrestigeUpgradeRequest, GameState } from "@elysium/types"; const prestigeRouter = new Hono(); prestigeRouter.use("*", authMiddleware); prestigeRouter.post("/", async(context) => { const discordId = context.get("discordId"); const record = await prisma.gameState.findUnique({ where: { discordId } }); if (!record) { return context.json({ error: "No save found" }, 404); } /* eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- Prisma returns JsonValue */ const state = record.state as unknown as GameState; if (!isEligibleForPrestige(state)) { return context.json( { error: "Not eligible for prestige — collect 1,000,000 total gold first", }, 400, ); } // Update daily prestige challenge progress before resetting the run let updatedDailyChallenges = state.dailyChallenges; let challengeCrystals = 0; if (updatedDailyChallenges) { const result = updateChallengeProgress( updatedDailyChallenges, "prestige", 1, ); updatedDailyChallenges = result.updatedChallenges; challengeCrystals = result.crystalsAwarded; } const { milestoneRunestones, prestigeData, prestigeState, runestonesEarned, } = buildPostPrestigeState(state, state.player.characterName); // Preserve daily challenges across the prestige reset and apply any crystal rewards const finalState: GameState = { ...prestigeState, ...updatedDailyChallenges === undefined ? {} : { dailyChallenges: updatedDailyChallenges }, resources: { ...prestigeState.resources, crystals: prestigeState.resources.crystals + challengeCrystals, }, }; // Capture current-run stats to accumulate into lifetime totals before resetting // eslint-disable-next-line capitalized-comments -- v8 ignore /* v8 ignore next 10 -- @preserve */ const runBossesDefeated = state.bosses.filter((boss) => { return boss.status === "defeated"; }).length; const runQuestsCompleted = state.quests.filter((quest) => { return quest.status === "completed"; }).length; let runAdventurersRecruited = 0; for (const adventurer of state.adventurers) { runAdventurersRecruited = runAdventurersRecruited + adventurer.count; } // eslint-disable-next-line capitalized-comments -- v8 ignore /* v8 ignore next 3 -- @preserve */ const runAchievementsUnlocked = state.achievements.filter((achievement) => { return achievement.unlockedAt !== null; }).length; const now = Date.now(); await prisma.gameState.update({ /* eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- Prisma requires object */ data: { state: finalState as object, updatedAt: now }, where: { discordId }, }); await prisma.player.update({ data: { characterName: state.player.characterName, lastSavedAt: now, lifetimeAchievementsUnlocked: { increment: runAchievementsUnlocked }, lifetimeAdventurersRecruited: { increment: runAdventurersRecruited }, lifetimeBossesDefeated: { increment: runBossesDefeated }, lifetimeClicks: { increment: state.player.totalClicks }, // Accumulate into lifetime totals — never reset lifetimeGoldEarned: { increment: state.player.totalGoldEarned }, lifetimeQuestsCompleted: { increment: runQuestsCompleted }, totalClicks: 0, // Reset current-run counters totalGoldEarned: 0, }, where: { discordId }, }); void postMilestoneWebhook(discordId, "prestige", { // eslint-disable-next-line capitalized-comments -- v8 ignore /* v8 ignore next -- @preserve */ apotheosis: prestigeState.apotheosis?.count ?? 0, prestige: prestigeData.count, // eslint-disable-next-line capitalized-comments -- v8 ignore /* v8 ignore next 2 -- @preserve */ transcendence: prestigeState.transcendence?.count ?? 0, }); return context.json({ milestoneRunestones: milestoneRunestones, newPrestigeCount: prestigeData.count, // eslint-disable-line unicorn/no-keyword-prefix -- API response field name required by client runestones: runestonesEarned, }); }); prestigeRouter.post("/buy-upgrade", async(context) => { const discordId = context.get("discordId"); const body = await context.req.json(); const { upgradeId } = body; // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions -- Runtime body validation if (!upgradeId) { return context.json({ error: "upgradeId is required" }, 400); } const upgrade = defaultPrestigeUpgrades.find((prestigeUpgrade) => { return prestigeUpgrade.id === upgradeId; }); if (!upgrade) { return context.json({ error: "Unknown prestige upgrade" }, 404); } const record = await prisma.gameState.findUnique({ where: { discordId } }); if (!record) { return context.json({ error: "No save found" }, 404); } /* eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- Prisma returns JsonValue */ const state = record.state as unknown as GameState; const { purchasedUpgradeIds, runestones } = state.prestige; if (purchasedUpgradeIds.includes(upgradeId)) { return context.json({ error: "Upgrade already purchased" }, 400); } if (runestones < upgrade.runestonesCost) { return context.json({ error: "Not enough runestones" }, 400); } const updatedRunestones = runestones - upgrade.runestonesCost; const updatedPurchasedUpgradeIds = [ ...purchasedUpgradeIds, upgradeId ]; const updatedState: GameState = { ...state, prestige: { ...state.prestige, purchasedUpgradeIds: updatedPurchasedUpgradeIds, runestones: updatedRunestones, ...computeRunestoneMultipliers(updatedPurchasedUpgradeIds), }, }; await prisma.gameState.update({ /* eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- Prisma requires object */ data: { state: updatedState as object, updatedAt: Date.now() }, where: { discordId }, }); const multipliers = computeRunestoneMultipliers(updatedPurchasedUpgradeIds); return context.json({ purchasedUpgradeIds: updatedPurchasedUpgradeIds, runestonesRemaining: updatedRunestones, ...multipliers, }); }); export { prestigeRouter };