Files
elysium/apps/api/src/routes/enlightenment.ts
T
hikari 0d36b255ee feat: goddess API routes, services, and tests (chunk 4)
Add six new goddess-mode API routes (boss fight, consecration,
enlightenment, upgrade purchase, crafting, exploration) alongside
matching service modules and full test suites at 100% coverage.
2026-04-13 15:48:35 -07:00

181 lines
5.7 KiB
TypeScript

/**
* @file Enlightenment routes handling enlightenment resets and stardust 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 */
/* eslint-disable stylistic/max-len -- Route logic requires long lines */
import { Hono } from "hono";
import { defaultEnlightenmentUpgrades } from "../data/goddessEnlightenmentUpgrades.js";
import { prisma } from "../db/client.js";
import { authMiddleware } from "../middleware/auth.js";
import {
buildPostEnlightenmentState,
computeEnlightenmentMultipliers,
isEligibleForEnlightenment,
} from "../services/enlightenment.js";
import { logger } from "../services/logger.js";
import type { HonoEnvironment } from "../types/hono.js";
import type {
BuyEnlightenmentUpgradeRequest,
EnlightenmentResponse,
GameState,
} from "@elysium/types";
const enlightenmentRouter = new Hono<HonoEnvironment>();
enlightenmentRouter.use("*", authMiddleware);
enlightenmentRouter.post("/", async(context) => {
try {
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 (!state.goddess) {
return context.json({ error: "Goddess realm not unlocked" }, 400);
}
if (!isEligibleForEnlightenment(state)) {
return context.json(
{
error: "Not eligible for enlightenment — defeat the Divine Heart Sovereign first",
},
400,
);
}
const { stardustEarned, updatedGoddess } = buildPostEnlightenmentState(state);
const updatedEnlightenmentCount = updatedGoddess.enlightenment.count;
const updatedState: GameState = {
...state,
goddess: updatedGoddess,
resources: {
...state.resources,
prayers: 0,
},
};
const now = Date.now();
await prisma.gameState.update({
/* eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- Prisma requires object */
data: { state: updatedState as object, updatedAt: now },
where: { discordId },
});
void logger.metric("enlightenment", 1, { discordId, updatedEnlightenmentCount });
const response: EnlightenmentResponse = {
// eslint-disable-next-line unicorn/no-keyword-prefix -- API response field name required by client
newEnlightenmentCount: updatedEnlightenmentCount,
stardustEarned: stardustEarned,
};
return context.json(response);
} catch (error) {
void logger.error(
"enlightenment",
error instanceof Error
? error
: new Error(String(error)),
);
return context.json({ error: "Internal server error" }, 500);
}
});
enlightenmentRouter.post("/buy-upgrade", async(context) => {
try {
const discordId = context.get("discordId");
const body = await context.req.json<BuyEnlightenmentUpgradeRequest>();
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 = defaultEnlightenmentUpgrades.find((enlightenmentUpgrade) => {
return enlightenmentUpgrade.id === upgradeId;
});
if (!upgrade) {
return context.json({ error: "Unknown enlightenment 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;
if (!state.goddess) {
return context.json({ error: "Goddess realm not unlocked" }, 400);
}
const { purchasedUpgradeIds, stardust } = state.goddess.enlightenment;
if (purchasedUpgradeIds.includes(upgradeId)) {
return context.json({ error: "Upgrade already purchased" }, 400);
}
if (stardust < upgrade.cost) {
return context.json({ error: "Not enough stardust" }, 400);
}
const updatedStardust = stardust - upgrade.cost;
const updatedPurchasedIds = [ ...purchasedUpgradeIds, upgradeId ];
const updatedMultipliers = computeEnlightenmentMultipliers(updatedPurchasedIds);
const updatedState: GameState = {
...state,
goddess: {
...state.goddess,
enlightenment: {
...state.goddess.enlightenment,
purchasedUpgradeIds: updatedPurchasedIds,
stardust: updatedStardust,
...updatedMultipliers,
},
},
};
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 },
});
void logger.metric("enlightenment_upgrade_purchased", 1, { discordId, upgradeId });
return context.json({
purchasedUpgradeIds: updatedPurchasedIds,
stardustRemaining: updatedStardust,
...updatedMultipliers,
});
} catch (error) {
void logger.error(
"enlightenment_buy_upgrade",
error instanceof Error
? error
: new Error(String(error)),
);
return context.json({ error: "Internal server error" }, 500);
}
});
export { enlightenmentRouter };