Files
elysium/apps/web/src/components/game/statisticsPanel.tsx
T
hikari 29c817230d
Security Scan and Upload / Security & DefectDojo Upload (push) Successful in 1m1s
CI / Lint, Build & Test (push) Successful in 1m6s
feat: initial prototype — core game systems (#30)
## 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>
2026-03-08 15:53:39 -07:00

213 lines
6.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* @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 };