feat: initial prototype — core game systems (#30)
Security Scan and Upload / Security & DefectDojo Upload (push) Successful in 1m1s
CI / Lint, Build & Test (push) Successful in 1m6s

## Summary

This PR represents the full v1 prototype, implementing the core game systems for Elysium.

- Full idle/clicker RPG loop: resource collection, crafting, boss fights, exploration, and quests
- Adventurer hiring with batch size selector and progressive tier cost scaling
- Prestige, transcendence, and apotheosis systems with auto-prestige support
- Character sheet, titles, leaderboards, companion system, and daily login bonuses
- Auto-quest and auto-boss toggles
- Discord webhook notifications on prestige/transcendence/apotheosis
- Discord role awarded on apotheosis
- Responsive design and overarching story/lore system
- In-game sound effects and browser notifications for key events
- Support link button in the resource bar
- Full test coverage (100% on `apps/api` and `packages/types`)
- CI pipeline: lint → build → test

## Closes

Closes #1
Closes #2
Closes #3
Closes #4
Closes #5
Closes #6
Closes #7
Closes #8
Closes #9
Closes #10
Closes #11
Closes #12
Closes #13
Closes #14
Closes #16
Closes #19
Closes #20
Closes #21
Closes #22
Closes #23
Closes #24
Closes #25
Closes #26
Closes #27
Closes #29

 This issue was created with help from Hikari~ 🌸

Co-authored-by: Naomi Carrigan <commits@nhcarrigan.com>
Reviewed-on: #30
Co-authored-by: Hikari <hikari@nhcarrigan.com>
Co-committed-by: Hikari <hikari@nhcarrigan.com>
This commit was merged in pull request #30.
This commit is contained in:
2026-03-08 15:53:39 -07:00
committed by Naomi Carrigan
parent c69e155de3
commit 29c817230d
172 changed files with 50706 additions and 0 deletions
@@ -0,0 +1,273 @@
/**
* @file Leaderboard page component showing top players across categories.
* @copyright nhcarrigan
* @license Naomi's Public License
* @author Naomi Carrigan
*/
/* eslint-disable max-lines-per-function -- Complex component with many render paths */
/* eslint-disable complexity -- Many conditional render paths for categories and entries */
import { useEffect, useState, type JSX } from "react";
import type { LeaderboardCategory, LeaderboardEntry } from "@elysium/types";
interface CategoryConfig {
id: LeaderboardCategory;
label: string;
icon: string;
formatValue: (value: number)=> string;
}
const goldSuffixes = [
"",
"K",
"M",
"B",
"T",
"Qa",
"Qt",
"S",
"Sp",
"O",
"N",
"D",
];
/**
* Formats a gold value with a short suffix.
* @param value - The gold amount to format.
* @returns The formatted string.
*/
const formatGold = (value: number): string => {
if (value === 0) {
return "0";
}
const tier = Math.floor(Math.log10(Math.abs(value)) / 3);
const clamped = Math.min(tier, goldSuffixes.length - 1);
const scaled = value / Math.pow(1000, clamped);
return `${String(Number.parseFloat(scaled.toFixed(2)))}${goldSuffixes[clamped] ?? ""}`;
};
const categories: Array<CategoryConfig> = [
{
formatValue: (v): string => {
return formatGold(v);
},
icon: "🪙",
id: "totalGold",
label: "Lifetime Gold",
},
{
formatValue: (v): string => {
return v.toLocaleString();
},
icon: "💀",
id: "bossesDefeated",
label: "Bosses Defeated",
},
{
formatValue: (v): string => {
return v.toLocaleString();
},
icon: "📜",
id: "questsCompleted",
label: "Quests Completed",
},
{
formatValue: (v): string => {
return v.toLocaleString();
},
icon: "🏆",
id: "achievementsUnlocked",
label: "Achievements",
},
{
formatValue: (v): string => {
return v.toLocaleString();
},
icon: "⭐",
id: "prestigeCount",
label: "Prestige",
},
{
formatValue: (v): string => {
return v.toLocaleString();
},
icon: "🌌",
id: "transcendenceCount",
label: "Transcendence",
},
{
formatValue: (v): string => {
return v.toLocaleString();
},
icon: "✨",
id: "apotheosisCount",
label: "Apotheosis",
},
];
const rankBadges: Record<number, string> = { 1: "🥇", 2: "🥈", 3: "🥉" };
/**
* Renders the leaderboard page with category tabs and player rankings.
* @returns The JSX element.
*/
const LeaderboardPage = (): JSX.Element => {
const [ category, setCategory ] = useState<LeaderboardCategory>("totalGold");
const [ entries, setEntries ] = useState<Array<LeaderboardEntry>>([]);
const [ loading, setLoading ] = useState(true);
const [ error, setError ] = useState<string | null>(null);
useEffect(() => {
setLoading(true);
setError(null);
fetch(`/api/leaderboards?category=${category}&limit=100`).
then(async(response) => {
if (!response.ok) {
throw new Error("Failed to load leaderboard");
}
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- API response cast
const data = (await response.json()) as {
entries: Array<LeaderboardEntry>;
};
setEntries(data.entries);
}).
catch((error_: unknown) => {
setError(
error_ instanceof Error
? error_.message
: "Failed to load leaderboard",
);
}).
finally(() => {
setLoading(false);
});
}, [ category ]);
const currentConfig
= categories.find((cat) => {
return cat.id === category;
}) ?? categories[0];
return (
<div className="leaderboard-page">
<div className="leaderboard-card">
<div className="leaderboard-header">
<h1 className="leaderboard-title">{"🏆 Leaderboards"}</h1>
<p className="leaderboard-subtitle">
{"The mightiest adventurers in Elysium"}
</p>
</div>
<div className="leaderboard-tabs">
{categories.map((cat) => {
function handleCategoryClick(): void {
setCategory(cat.id);
}
return (
<button
className={`leaderboard-tab ${
category === cat.id
? "leaderboard-tab--active"
: ""
}`}
key={cat.id}
onClick={handleCategoryClick}
type="button"
>
{cat.icon} {cat.label}
</button>
);
})}
</div>
{loading
? <div className="leaderboard-loading">{"Loading…"}</div>
: null}
{error === null
? null
: <div className="leaderboard-error">
{"⚠️ "}
{error}
</div>
}
{!loading && error === null && entries.length === 0
&& <div className="leaderboard-empty">
{"No entries yet — be the first on the board!"}
</div>
}
{!loading && error === null && entries.length > 0
&& <div className="leaderboard-table">
<div className="leaderboard-table-header">
<span className="leaderboard-col-rank">{"Rank"}</span>
<span className="leaderboard-col-player">{"Player"}</span>
<span className="leaderboard-col-value">
{currentConfig?.icon} {currentConfig?.label}
</span>
</div>
{entries.map((entry) => {
const avatarUrl
= entry.avatar === null
? `https://cdn.discordapp.com/embed/avatars/${String(Number.parseInt(entry.discordId, 10) % 5)}.png`
: `https://cdn.discordapp.com/avatars/${entry.discordId}/${entry.avatar}.png?size=32`;
const displayName
= entry.characterName === ""
? entry.username
: entry.characterName;
return (
<a
className={`leaderboard-row ${
entry.rank <= 3
? `leaderboard-row--top${String(entry.rank)}`
: ""
}`}
href={`/character/${entry.discordId}`}
key={entry.discordId}
>
<span className="leaderboard-col-rank">
{rankBadges[entry.rank] ?? `#${String(entry.rank)}`}
</span>
<span className="leaderboard-col-player">
<img
alt={displayName}
className="leaderboard-avatar"
src={avatarUrl}
/>
<span className="leaderboard-player-info">
<span className="leaderboard-player-name">
{displayName}
</span>
{entry.activeTitle === ""
? null
: <span className="leaderboard-player-title">
{entry.activeTitle}
</span>
}
</span>
</span>
<span className="leaderboard-col-value">
{currentConfig?.formatValue(entry.value)}
</span>
</a>
);
})}
</div>
}
<div className="leaderboard-footer">
<a className="leaderboard-play-link" href="/">
{"⚔️ Play Elysium"}
</a>
<p className="leaderboard-privacy-note">
{"Players can opt out via their profile settings."}
</p>
</div>
</div>
</div>
);
};
export { LeaderboardPage };