import type { GameState, ProfileSettings, UpdateProfileRequest, } from "@elysium/types"; import { DEFAULT_PROFILE_SETTINGS } from "@elysium/types"; import { Hono } from "hono"; import type { HonoEnv } from "../types/hono.js"; import { prisma } from "../db/client.js"; import { authMiddleware } from "../middleware/auth.js"; export const profileRouter = new Hono(); const VALID_NUMBER_FORMATS = new Set(["suffix", "scientific", "engineering"]); const parseProfileSettings = (raw: unknown): ProfileSettings => { if (raw !== null && typeof raw === "object" && !Array.isArray(raw)) { const obj = raw as Record; const numberFormat = VALID_NUMBER_FORMATS.has(obj.numberFormat as string) ? (obj.numberFormat as ProfileSettings["numberFormat"]) : "suffix"; return { showTotalGold: obj.showTotalGold !== false, showTotalClicks: obj.showTotalClicks !== false, showLifetimeBossesDefeated: obj.showLifetimeBossesDefeated !== false, showLifetimeQuestsCompleted: obj.showLifetimeQuestsCompleted !== false, showLifetimeAdventurersRecruited: obj.showLifetimeAdventurersRecruited !== false, showLifetimeAchievementsUnlocked: obj.showLifetimeAchievementsUnlocked !== false, showGuildFounded: obj.showGuildFounded !== false, showCurrentGold: obj.showCurrentGold !== false, showCurrentClicks: obj.showCurrentClicks !== false, showPrestige: obj.showPrestige !== false, showBossesDefeated: obj.showBossesDefeated !== false, showQuestsCompleted: obj.showQuestsCompleted !== false, showAdventurersRecruited: obj.showAdventurersRecruited !== false, showAchievementsUnlocked: obj.showAchievementsUnlocked !== false, numberFormat, }; } return { ...DEFAULT_PROFILE_SETTINGS }; }; profileRouter.get("/:discordId", async (context) => { const { discordId } = context.req.param(); const [player, gameStateRecord] = await Promise.all([ prisma.player.findUnique({ where: { discordId } }), prisma.gameState.findUnique({ where: { discordId } }), ]); if (!player) { return context.json({ error: "Player not found" }, 404); } const state = gameStateRecord?.state as unknown as GameState | undefined; const prestigeCount = state?.prestige.count ?? 0; const profileSettings = parseProfileSettings(player.profileSettings); const bossesDefeated = state?.bosses.filter((b) => b.status === "defeated").length ?? 0; const questsCompleted = state?.quests.filter((q) => q.status === "completed").length ?? 0; const adventurersRecruited = state?.adventurers.reduce((sum, a) => sum + a.count, 0) ?? 0; const achievementsUnlocked = (state?.achievements ?? []).filter((a) => a.unlockedAt !== null).length; return context.json({ characterName: player.characterName, username: player.username, avatar: player.avatar ?? null, bio: player.bio ?? "", profileSettings, createdAt: player.createdAt, // All Time stats — cumulative across all runs, never reset totalGoldEarned: player.lifetimeGoldEarned, totalClicks: player.lifetimeClicks, lifetimeBossesDefeated: player.lifetimeBossesDefeated, lifetimeQuestsCompleted: player.lifetimeQuestsCompleted, lifetimeAdventurersRecruited: player.lifetimeAdventurersRecruited, lifetimeAchievementsUnlocked: player.lifetimeAchievementsUnlocked, // Current Run stats — from live GameState, reset on prestige & transcendence currentRunGold: state?.player.totalGoldEarned ?? 0, currentRunClicks: state?.player.totalClicks ?? 0, prestigeCount, bossesDefeated, questsCompleted, adventurersRecruited, achievementsUnlocked, }); }); profileRouter.put("/", authMiddleware, async (context) => { const discordId = context.get("discordId") as string; const body = await context.req.json(); const characterName = (body.characterName ?? "").trim().slice(0, 32); const bio = (body.bio ?? "").trim().slice(0, 200); const numberFormat = VALID_NUMBER_FORMATS.has(body.profileSettings?.numberFormat as string) ? (body.profileSettings?.numberFormat as ProfileSettings["numberFormat"]) : "suffix"; const profileSettings: ProfileSettings = { showTotalGold: body.profileSettings?.showTotalGold !== false, showTotalClicks: body.profileSettings?.showTotalClicks !== false, showLifetimeBossesDefeated: body.profileSettings?.showLifetimeBossesDefeated !== false, showLifetimeQuestsCompleted: body.profileSettings?.showLifetimeQuestsCompleted !== false, showLifetimeAdventurersRecruited: body.profileSettings?.showLifetimeAdventurersRecruited !== false, showLifetimeAchievementsUnlocked: body.profileSettings?.showLifetimeAchievementsUnlocked !== false, showGuildFounded: body.profileSettings?.showGuildFounded !== false, showCurrentGold: body.profileSettings?.showCurrentGold !== false, showCurrentClicks: body.profileSettings?.showCurrentClicks !== false, showPrestige: body.profileSettings?.showPrestige !== false, showBossesDefeated: body.profileSettings?.showBossesDefeated !== false, showQuestsCompleted: body.profileSettings?.showQuestsCompleted !== false, showAdventurersRecruited: body.profileSettings?.showAdventurersRecruited !== false, showAchievementsUnlocked: body.profileSettings?.showAchievementsUnlocked !== false, numberFormat, }; if (!characterName) { return context.json({ error: "Character name cannot be empty" }, 400); } const updated = await prisma.player.update({ where: { discordId }, data: { characterName, bio, profileSettings: profileSettings as object }, }); return context.json({ characterName: updated.characterName, bio: updated.bio, profileSettings, }); });