generated from nhcarrigan/template
feat: add public leaderboards with opt-out privacy setting
This commit is contained in:
@@ -9,6 +9,7 @@ import { bossRouter } from "./routes/boss.js";
|
||||
import { craftRouter } from "./routes/craft.js";
|
||||
import { exploreRouter } from "./routes/explore.js";
|
||||
import { gameRouter } from "./routes/game.js";
|
||||
import { leaderboardRouter } from "./routes/leaderboards.js";
|
||||
import { prestigeRouter } from "./routes/prestige.js";
|
||||
import { transcendenceRouter } from "./routes/transcendence.js";
|
||||
import { profileRouter } from "./routes/profile.js";
|
||||
@@ -34,6 +35,7 @@ app.route("/craft", craftRouter);
|
||||
app.route("/prestige", prestigeRouter);
|
||||
app.route("/transcendence", transcendenceRouter);
|
||||
app.route("/apotheosis", apotheosisRouter);
|
||||
app.route("/leaderboards", leaderboardRouter);
|
||||
app.route("/profile", profileRouter);
|
||||
|
||||
app.get("/health", (context) => context.json({ status: "ok" }));
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
import type { GameState } from "@elysium/types";
|
||||
import { Hono } from "hono";
|
||||
import type { HonoEnv } from "../types/hono.js";
|
||||
import { prisma } from "../db/client.js";
|
||||
import { TITLES } from "../data/titles.js";
|
||||
|
||||
export const leaderboardRouter = new Hono<HonoEnv>();
|
||||
|
||||
const VALID_CATEGORIES = new Set([
|
||||
"totalGold",
|
||||
"bossesDefeated",
|
||||
"questsCompleted",
|
||||
"achievementsUnlocked",
|
||||
"prestigeCount",
|
||||
"transcendenceCount",
|
||||
"apotheosisCount",
|
||||
]);
|
||||
|
||||
const GAMESTATE_CATEGORIES = new Set(["prestigeCount", "transcendenceCount", "apotheosisCount"]);
|
||||
|
||||
const parseShowOnLeaderboards = (raw: unknown): boolean => {
|
||||
if (raw !== null && typeof raw === "object" && !Array.isArray(raw)) {
|
||||
return (raw as Record<string, unknown>).showOnLeaderboards !== false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
leaderboardRouter.get("/", async (context) => {
|
||||
const category = context.req.query("category") ?? "totalGold";
|
||||
const limitRaw = Number(context.req.query("limit") ?? "100");
|
||||
const limit = Math.min(Math.max(1, limitRaw), 100);
|
||||
|
||||
if (!VALID_CATEGORIES.has(category)) {
|
||||
return context.json({ error: "Invalid category" }, 400);
|
||||
}
|
||||
|
||||
const [players, gameStates] = await Promise.all([
|
||||
prisma.player.findMany(),
|
||||
GAMESTATE_CATEGORIES.has(category) ? prisma.gameState.findMany() : Promise.resolve([]),
|
||||
]);
|
||||
|
||||
const stateMap = new Map(
|
||||
gameStates.map((gs) => [gs.discordId, gs.state as unknown as GameState]),
|
||||
);
|
||||
|
||||
const entries = players
|
||||
.filter((p) => parseShowOnLeaderboards(p.profileSettings))
|
||||
.map((p) => {
|
||||
let value = 0;
|
||||
if (category === "totalGold") {
|
||||
value = p.lifetimeGoldEarned;
|
||||
} else if (category === "bossesDefeated") {
|
||||
value = p.lifetimeBossesDefeated;
|
||||
} else if (category === "questsCompleted") {
|
||||
value = p.lifetimeQuestsCompleted;
|
||||
} else if (category === "achievementsUnlocked") {
|
||||
value = p.lifetimeAchievementsUnlocked;
|
||||
} else {
|
||||
const state = stateMap.get(p.discordId);
|
||||
if (category === "prestigeCount") {
|
||||
value = state?.prestige?.count ?? 0;
|
||||
} else if (category === "transcendenceCount") {
|
||||
value = state?.transcendence?.count ?? 0;
|
||||
} else if (category === "apotheosisCount") {
|
||||
value = state?.apotheosis?.count ?? 0;
|
||||
}
|
||||
}
|
||||
const titleId = p.activeTitle ?? "";
|
||||
const titleName = titleId
|
||||
? (TITLES.find((t) => t.id === titleId)?.name ?? titleId)
|
||||
: "";
|
||||
return {
|
||||
discordId: p.discordId,
|
||||
characterName: p.characterName,
|
||||
username: p.username,
|
||||
avatar: p.avatar ?? null,
|
||||
activeTitle: titleName,
|
||||
value,
|
||||
};
|
||||
})
|
||||
.sort((a, b) => b.value - a.value)
|
||||
.slice(0, limit)
|
||||
.map((entry, index) => ({ ...entry, rank: index + 1 }));
|
||||
|
||||
return context.json({ category, entries });
|
||||
});
|
||||
@@ -39,6 +39,7 @@ const parseProfileSettings = (raw: unknown): ProfileSettings => {
|
||||
showAdventurersRecruited: obj.showAdventurersRecruited !== false,
|
||||
showAchievementsUnlocked: obj.showAchievementsUnlocked !== false,
|
||||
numberFormat,
|
||||
showOnLeaderboards: obj.showOnLeaderboards !== false,
|
||||
};
|
||||
}
|
||||
return { ...DEFAULT_PROFILE_SETTINGS };
|
||||
@@ -146,6 +147,7 @@ profileRouter.put("/", authMiddleware, async (context) => {
|
||||
showAdventurersRecruited: body.profileSettings?.showAdventurersRecruited !== false,
|
||||
showAchievementsUnlocked: body.profileSettings?.showAchievementsUnlocked !== false,
|
||||
numberFormat,
|
||||
showOnLeaderboards: body.profileSettings?.showOnLeaderboards !== false,
|
||||
};
|
||||
|
||||
if (!characterName) {
|
||||
|
||||
Reference in New Issue
Block a user