generated from nhcarrigan/template
feat: add statistics panel with all-time, current run, and progress stats
This commit is contained in:
@@ -6,7 +6,7 @@ A running list of planned features and content additions. Strike through items a
|
|||||||
|
|
||||||
## ๐ New Systems
|
## ๐ New Systems
|
||||||
|
|
||||||
- [ ] **Offline earnings** โ When returning to the game, earn a percentage of what you'd have earned offline (cap at ~8โ12 hours). Upgradeable via the prestige shop to increase the % and the time cap. Essential for an idle game!
|
- [x] **Offline earnings** โ When returning to the game, earn a percentage of what you'd have earned offline (cap at ~8โ12 hours). Upgradeable via the prestige shop to increase the % and the time cap. Essential for an idle game!
|
||||||
|
|
||||||
- [ ] **Second prestige layer (Transcendence)** โ Unlocked after ~10 prestiges. Sacrifice all runestones for a new currency ("Echoes"?). Echoes are permanent account-wide currency that persist across prestiges. Has its own upgrade tree with truly game-changing bonuses. Gives endgame players a long-term goal.
|
- [ ] **Second prestige layer (Transcendence)** โ Unlocked after ~10 prestiges. Sacrifice all runestones for a new currency ("Echoes"?). Echoes are permanent account-wide currency that persist across prestiges. Has its own upgrade tree with truly game-changing bonuses. Gives endgame players a long-term goal.
|
||||||
|
|
||||||
@@ -38,7 +38,7 @@ A running list of planned features and content additions. Strike through items a
|
|||||||
|
|
||||||
## ๐ Priority Order (Suggested)
|
## ๐ Priority Order (Suggested)
|
||||||
|
|
||||||
1. Offline earnings (core idle game feature)
|
1. ~~Offline earnings~~ โ
|
||||||
2. Statistics panel (low effort, high satisfaction)
|
2. Statistics panel (low effort, high satisfaction)
|
||||||
3. Daily challenges (retention driver)
|
3. Daily challenges (retention driver)
|
||||||
4. Boss first-kill bounties (easy content win)
|
4. Boss first-kill bounties (easy content win)
|
||||||
|
|||||||
@@ -12,9 +12,10 @@ import { EquipmentPanel } from "./EquipmentPanel.js";
|
|||||||
import { OfflineModal } from "./OfflineModal.js";
|
import { OfflineModal } from "./OfflineModal.js";
|
||||||
import { PrestigePanel } from "./PrestigePanel.js";
|
import { PrestigePanel } from "./PrestigePanel.js";
|
||||||
import { QuestPanel } from "./QuestPanel.js";
|
import { QuestPanel } from "./QuestPanel.js";
|
||||||
|
import { StatisticsPanel } from "./StatisticsPanel.js";
|
||||||
import { UpgradePanel } from "./UpgradePanel.js";
|
import { UpgradePanel } from "./UpgradePanel.js";
|
||||||
|
|
||||||
type Tab = "adventurers" | "upgrades" | "quests" | "bosses" | "equipment" | "achievements" | "prestige";
|
type Tab = "adventurers" | "upgrades" | "quests" | "bosses" | "equipment" | "achievements" | "prestige" | "statistics";
|
||||||
|
|
||||||
const TABS: { id: Tab; label: string }[] = [
|
const TABS: { id: Tab; label: string }[] = [
|
||||||
{ id: "adventurers", label: "โ๏ธ Adventurers" },
|
{ id: "adventurers", label: "โ๏ธ Adventurers" },
|
||||||
@@ -24,6 +25,7 @@ const TABS: { id: Tab; label: string }[] = [
|
|||||||
{ id: "equipment", label: "๐ก๏ธ Equipment" },
|
{ id: "equipment", label: "๐ก๏ธ Equipment" },
|
||||||
{ id: "achievements", label: "๐ Achievements" },
|
{ id: "achievements", label: "๐ Achievements" },
|
||||||
{ id: "prestige", label: "โญ Prestige" },
|
{ id: "prestige", label: "โญ Prestige" },
|
||||||
|
{ id: "statistics", label: "๐ Statistics" },
|
||||||
];
|
];
|
||||||
|
|
||||||
export const GameLayout = (): React.JSX.Element => {
|
export const GameLayout = (): React.JSX.Element => {
|
||||||
@@ -98,6 +100,7 @@ export const GameLayout = (): React.JSX.Element => {
|
|||||||
{activeTab === "equipment" && <EquipmentPanel />}
|
{activeTab === "equipment" && <EquipmentPanel />}
|
||||||
{activeTab === "achievements" && <AchievementPanel />}
|
{activeTab === "achievements" && <AchievementPanel />}
|
||||||
{activeTab === "prestige" && <PrestigePanel />}
|
{activeTab === "prestige" && <PrestigePanel />}
|
||||||
|
{activeTab === "statistics" && <StatisticsPanel />}
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,153 @@
|
|||||||
|
import { useGame } from "../../context/GameContext.js";
|
||||||
|
import { PRESTIGE_UPGRADES } from "../../data/prestigeUpgrades.js";
|
||||||
|
|
||||||
|
const formatDate = (timestamp: number): string =>
|
||||||
|
new Date(timestamp).toLocaleDateString("en-GB", {
|
||||||
|
day: "numeric",
|
||||||
|
month: "short",
|
||||||
|
year: "numeric",
|
||||||
|
});
|
||||||
|
|
||||||
|
interface StatCardProps {
|
||||||
|
icon: string;
|
||||||
|
label: string;
|
||||||
|
value: string;
|
||||||
|
sub?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const StatCard = ({ icon, label, value, sub }: StatCardProps): React.JSX.Element => (
|
||||||
|
<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 && <span className="profile-stat-date">{sub}</span>}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
export const StatisticsPanel = (): React.JSX.Element => {
|
||||||
|
const { state, formatNumber } = useGame();
|
||||||
|
|
||||||
|
if (!state) return <section className="panel"><p>Loading...</p></section>;
|
||||||
|
|
||||||
|
const { player, resources, prestige, bosses, quests, zones, adventurers, upgrades, equipment, achievements } = state;
|
||||||
|
|
||||||
|
const bossesDefeated = bosses.filter((b) => b.status === "defeated").length;
|
||||||
|
const questsCompleted = quests.filter((q) => q.status === "completed").length;
|
||||||
|
const zonesUnlocked = zones.filter((z) => z.status === "unlocked").length;
|
||||||
|
const adventurersRecruited = adventurers.reduce((sum, a) => sum + a.count, 0);
|
||||||
|
const equipmentOwned = (equipment ?? []).filter((e) => e.owned).length;
|
||||||
|
const upgradesPurchased = upgrades.filter((u) => u.purchased).length;
|
||||||
|
const achievementsUnlocked = (achievements ?? []).filter((a) => a.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"
|
||||||
|
value={formatNumber(player.totalGoldEarned)}
|
||||||
|
sub="across all runs"
|
||||||
|
/>
|
||||||
|
<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"
|
||||||
|
value={`ร${prestige.productionMultiplier.toFixed(2)}`}
|
||||||
|
sub="from prestige"
|
||||||
|
/>
|
||||||
|
</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"
|
||||||
|
value={formatNumber(prestige.runestones)}
|
||||||
|
sub="permanent currency"
|
||||||
|
/>
|
||||||
|
</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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1084,6 +1084,23 @@ body {
|
|||||||
background: var(--colour-accent-light);
|
background: var(--colour-accent-light);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ===================== STATISTICS ===================== */
|
||||||
|
.stats-section-header {
|
||||||
|
border-bottom: 1px solid var(--colour-border);
|
||||||
|
color: var(--colour-text-muted);
|
||||||
|
font-size: 0.8rem;
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: 0.08em;
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
|
margin-top: 1.5rem;
|
||||||
|
padding-bottom: 0.4rem;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.statistics-panel .stats-section-header:first-of-type {
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
/* ===================== ACHIEVEMENTS ===================== */
|
/* ===================== ACHIEVEMENTS ===================== */
|
||||||
.achievement-progress {
|
.achievement-progress {
|
||||||
color: var(--colour-text-muted);
|
color: var(--colour-text-muted);
|
||||||
|
|||||||
Reference in New Issue
Block a user