Files
elysium/apps/api/src/routes/vampireAwakening.ts
T
hikari 8fa5d12f05 feat: vampire expansion chunk 4 — API routes and services
Adds six new routes (vampire-boss, vampire-upgrade, vampire-craft,
vampire-explore, siring, vampire-awakening) with matching siring and
awakening services, and all necessary request/response types.
2026-04-16 09:48:50 -07:00

183 lines
6.2 KiB
TypeScript

/**
* @file Vampire Awakening routes handling awakening resets and soul shard 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 { defaultVampireAwakeningUpgrades } from "../data/vampireAwakeningUpgrades.js";
import { prisma } from "../db/client.js";
import { authMiddleware } from "../middleware/auth.js";
import {
buildPostAwakeningState,
computeAwakeningMultipliers,
isEligibleForAwakening,
} from "../services/awakening.js";
import { logger } from "../services/logger.js";
import type { HonoEnvironment } from "../types/hono.js";
import type {
AwakeningResponse,
BuyAwakeningUpgradeRequest,
BuyAwakeningUpgradeResponse,
GameState,
} from "@elysium/types";
const vampireAwakeningRouter = new Hono<HonoEnvironment>();
vampireAwakeningRouter.use("*", authMiddleware);
vampireAwakeningRouter.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.vampire) {
return context.json({ error: "Vampire realm not unlocked" }, 400);
}
if (!isEligibleForAwakening(state)) {
return context.json(
{
error: "Not eligible for awakening — defeat the Eternal Darkness first",
},
400,
);
}
const { soulShardsEarned, updatedVampire } = buildPostAwakeningState(state);
const updatedAwakeningCount = updatedVampire.awakening.count;
const updatedState: GameState = {
...state,
resources: { ...state.resources, blood: 0, ichor: 0 },
vampire: updatedVampire,
};
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("vampire_awakening", 1, { discordId, updatedAwakeningCount });
const response: AwakeningResponse = {
// eslint-disable-next-line unicorn/no-keyword-prefix -- API field name matches server response
newAwakeningCount: updatedAwakeningCount,
soulShardsEarned: soulShardsEarned,
};
return context.json(response);
} catch (error) {
void logger.error(
"vampire_awakening",
error instanceof Error
? error
: new Error(String(error)),
);
return context.json({ error: "Internal server error" }, 500);
}
});
vampireAwakeningRouter.post("/buy-upgrade", async(context) => {
try {
const discordId = context.get("discordId");
const body = await context.req.json<BuyAwakeningUpgradeRequest>();
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 = defaultVampireAwakeningUpgrades.find((awakeningUpgrade) => {
return awakeningUpgrade.id === upgradeId;
});
if (!upgrade) {
return context.json({ error: "Unknown awakening 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.vampire) {
return context.json({ error: "Vampire realm not unlocked" }, 400);
}
const { purchasedUpgradeIds, soulShards } = state.vampire.awakening;
if (purchasedUpgradeIds.includes(upgradeId)) {
return context.json({ error: "Upgrade already purchased" }, 400);
}
if (soulShards < upgrade.cost) {
return context.json({ error: "Not enough soul shards" }, 400);
}
const updatedSoulShards = soulShards - upgrade.cost;
const updatedPurchasedIds = [ ...purchasedUpgradeIds, upgradeId ];
const updatedMultipliers = computeAwakeningMultipliers(updatedPurchasedIds);
const updatedState: GameState = {
...state,
vampire: {
...state.vampire,
awakening: {
...state.vampire.awakening,
purchasedUpgradeIds: updatedPurchasedIds,
soulShards: updatedSoulShards,
...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("vampire_awakening_upgrade_purchased", 1, { discordId, upgradeId });
const response: BuyAwakeningUpgradeResponse = {
purchasedUpgradeIds: updatedPurchasedIds,
soulShardsBloodMultiplier: updatedMultipliers.soulShardsBloodMultiplier,
soulShardsCombatMultiplier: updatedMultipliers.soulShardsCombatMultiplier,
soulShardsMetaMultiplier: updatedMultipliers.soulShardsMetaMultiplier,
soulShardsRemaining: updatedSoulShards,
soulShardsSiringIchorMultiplier: updatedMultipliers.soulShardsSiringIchorMultiplier,
soulShardsSiringThresholdMultiplier: updatedMultipliers.soulShardsSiringThresholdMultiplier,
};
return context.json(response);
} catch (error) {
void logger.error(
"vampire_awakening_buy_upgrade",
error instanceof Error
? error
: new Error(String(error)),
);
return context.json({ error: "Internal server error" }, 500);
}
});
export { vampireAwakeningRouter };