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,212 @@
/**
* @file Statistics panel component showing player progress and all-time stats.
* @copyright nhcarrigan
* @license Naomi's Public License
* @author Naomi Carrigan
*/
/* eslint-disable react/no-multi-comp -- Sub-component is tightly coupled to the panel */
/* eslint-disable max-lines-per-function -- Complex component with many render paths */
/* eslint-disable react/require-default-props -- TypeScript optional props with default parameters are sufficient */
import { useGame } from "../../context/gameContext.js";
import { PRESTIGE_UPGRADES } from "../../data/prestigeUpgrades.js";
import type { JSX } from "react";
const formatDate = (timestamp: number): string => {
return new Date(timestamp).toLocaleDateString("en-GB", {
day: "numeric",
month: "short",
year: "numeric",
});
};
interface StatCardProperties {
readonly icon: string;
readonly label: string;
readonly value: string;
readonly sub?: string | undefined;
}
/**
* Renders a single statistic card.
* @param props - The stat card properties.
* @param props.icon - The icon to display.
* @param props.label - The label for the stat.
* @param props.value - The value to display.
* @param props.sub - Optional sub-label.
* @returns The JSX element.
*/
const StatCard = ({
icon,
label,
value,
sub = undefined,
}: StatCardProperties): JSX.Element => {
return (
<div className="profile-stat">
<span className="profile-stat-icon">{icon}</span>
<span className="profile-stat-value">{value}</span>
<span className="profile-stat-label">{label}</span>
{sub === undefined
? null
: <span className="profile-stat-date">{sub}</span>
}
</div>
);
};
/**
* Renders the statistics panel with player progress and all-time stats.
* @returns The JSX element.
*/
const StatisticsPanel = (): JSX.Element => {
const { state, formatNumber } = useGame();
if (state === null) {
return (
<section className="panel">
<p>{"Loading..."}</p>
</section>
);
}
const {
player,
resources,
prestige,
bosses,
quests,
zones,
adventurers,
upgrades,
equipment,
achievements,
} = state;
const bossesDefeated = bosses.filter((boss) => {
return boss.status === "defeated";
}).length;
const questsCompleted = quests.filter((quest) => {
return quest.status === "completed";
}).length;
const zonesUnlocked = zones.filter((zone) => {
return zone.status === "unlocked";
}).length;
const adventurersRecruited = adventurers.reduce((sum, adventurer) => {
return sum + adventurer.count;
}, 0);
const equipmentOwned = equipment.filter((item) => {
return item.owned;
}).length;
const upgradesPurchased = upgrades.filter((upgrade) => {
return upgrade.purchased;
}).length;
const achievementsUnlocked = achievements.filter((achievement) => {
return achievement.unlockedAt !== null;
}).length;
const prestigeUpgradesPurchased = prestige.purchasedUpgradeIds.length;
return (
<section className="panel statistics-panel">
<h2>{"📊 Statistics"}</h2>
<h3 className="stats-section-header">{"All-Time"}</h3>
<div className="profile-stats">
<StatCard
icon="🪙"
label="Total Gold Earned"
sub="across all runs"
value={formatNumber(player.totalGoldEarned)}
/>
<StatCard
icon="👆"
label="Total Clicks"
value={formatNumber(player.totalClicks)}
/>
<StatCard icon="⭐" label="Prestiges" value={String(prestige.count)} />
<StatCard
icon="📅"
label="Guild Founded"
value={formatDate(player.createdAt)}
/>
<StatCard
icon="☁️"
label="Last Cloud Save"
value={formatDate(player.lastSavedAt)}
/>
<StatCard
icon="✖️"
label="Production Multiplier"
sub="from prestige"
value={`×${prestige.productionMultiplier.toFixed(2)}`}
/>
</div>
<h3 className="stats-section-header">{"Current Run"}</h3>
<div className="profile-stats">
<StatCard icon="🪙" label="Gold" value={formatNumber(resources.gold)} />
<StatCard
icon="✨"
label="Essence"
value={formatNumber(resources.essence)}
/>
<StatCard
icon="💎"
label="Crystals"
value={formatNumber(resources.crystals)}
/>
<StatCard
icon="🔮"
label="Runestones"
sub="permanent currency"
value={formatNumber(prestige.runestones)}
/>
</div>
<h3 className="stats-section-header">{"Progress"}</h3>
<div className="profile-stats">
<StatCard
icon="👹"
label="Bosses Defeated"
value={`${String(bossesDefeated)} / ${String(bosses.length)}`}
/>
<StatCard
icon="📜"
label="Quests Completed"
value={`${String(questsCompleted)} / ${String(quests.length)}`}
/>
<StatCard
icon="🗺️"
label="Zones Unlocked"
value={`${String(zonesUnlocked)} / ${String(zones.length)}`}
/>
<StatCard
icon="⚔️"
label="Adventurers Recruited"
value={formatNumber(adventurersRecruited)}
/>
<StatCard
icon="🗡️"
label="Equipment Owned"
value={`${String(equipmentOwned)} / ${String(equipment.length)}`}
/>
<StatCard
icon="🔧"
label="Upgrades Purchased"
value={`${String(upgradesPurchased)} / ${String(upgrades.length)}`}
/>
<StatCard
icon="🏆"
label="Achievements"
value={`${String(achievementsUnlocked)} / ${String(achievements.length)}`}
/>
<StatCard
icon="🔮"
label="Prestige Upgrades"
value={`${String(prestigeUpgradesPurchased)} / ${String(PRESTIGE_UPGRADES.length)}`}
/>
</div>
</section>
);
};
export { StatisticsPanel };