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,341 @@
/**
* @file Transcendence panel component for the second prestige layer.
* @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 */
/* eslint-disable max-statements -- Transcendence panel manages many local state variables */
import { useState, type JSX } from "react";
import { useGame } from "../../context/gameContext.js";
import {
TRANSCENDENCE_UPGRADES,
TRANSCENDENCE_UPGRADE_CATEGORY_LABELS,
} from "../../data/transcendenceUpgrades.js";
import type { TranscendenceUpgradeCategory } from "@elysium/types";
const echoFormulaConstant = 853;
const finalBossId = "the_absolute_one";
/**
* Calculates the echo preview for a transcendence.
* @param prestigeCount - The current prestige count.
* @param echoMetaMultiplier - The echo meta multiplier from upgrades.
* @returns The predicted echo reward.
*/
const calculateEchoPreview = (
prestigeCount: number,
echoMetaMultiplier: number,
): number => {
const safeCount = Math.max(prestigeCount, 1);
return Math.floor(
// eslint-disable-next-line stylistic/no-extra-parens -- Required by no-mixed-operators rule
(echoFormulaConstant / Math.sqrt(safeCount)) * echoMetaMultiplier,
);
};
const categoryOrder: Array<TranscendenceUpgradeCategory> = [
"income",
"combat",
"prestige_threshold",
"prestige_runestones",
"echo_meta",
];
/**
* Renders the transcendence panel with transcendence and echo shop tabs.
* @returns The JSX element.
*/
const TranscendencePanel = (): JSX.Element => {
const { state, formatNumber, transcend, buyEchoUpgrade } = useGame();
const [ isPending, setIsPending ] = useState(false);
const [ result, setResult ] = useState<{
echoes: number;
count: number;
} | null>(null);
const [ error, setError ] = useState<string | null>(null);
const [ buyingId, setBuyingId ] = useState<string | null>(null);
type TranscendTab = "transcend" | "shop";
const [ activeTab, setActiveTab ] = useState<TranscendTab>("transcend");
if (state === null) {
return (
<section className="panel">
<p>{"Loading..."}</p>
</section>
);
}
const { bosses, prestige: prestigeData, transcendence } = state;
const hasDefeatedFinalBoss = bosses.some((boss) => {
return boss.id === finalBossId && boss.status === "defeated";
});
const echoMetaMultiplier = transcendence?.echoMetaMultiplier ?? 1;
const echoPreview = calculateEchoPreview(
prestigeData.count,
echoMetaMultiplier,
);
const currentEchoes = transcendence?.echoes ?? 0;
const transcendenceCount = transcendence?.count ?? 0;
async function handleTranscend(): Promise<void> {
setIsPending(true);
setError(null);
try {
const data = await transcend();
setResult({ count: data.newTranscendenceCount, echoes: data.echoes });
} catch (error_: unknown) {
setError(
error_ instanceof Error
? error_.message
: "Transcendence failed",
);
} finally {
setIsPending(false);
}
}
async function handleBuyUpgrade(upgradeId: string): Promise<void> {
setBuyingId(upgradeId);
try {
await buyEchoUpgrade(upgradeId);
} finally {
setBuyingId(null);
}
}
const upgradesByCategory = categoryOrder.map((catId) => {
const categoryLabels = TRANSCENDENCE_UPGRADE_CATEGORY_LABELS;
const label = categoryLabels[catId] ?? catId;
const upgrades = TRANSCENDENCE_UPGRADES.filter((upgrade) => {
return upgrade.category === catId;
});
return { catId, label, upgrades };
});
function handleTranscendClick(): void {
void handleTranscend();
}
function handleTranscendTabClick(): void {
setActiveTab("transcend");
}
function handleShopTabClick(): void {
setActiveTab("shop");
}
return (
<section className="panel transcendence-panel">
<h2>{"🌌 Transcendence"}</h2>
<div className="prestige-tabs">
<button
className={`prestige-tab ${
activeTab === "transcend"
? "active"
: ""
}`}
onClick={handleTranscendTabClick}
type="button"
>
{"Transcend"}
</button>
<button
className={`prestige-tab ${activeTab === "shop"
? "active"
: ""}`}
onClick={handleShopTabClick}
type="button"
>
{"✨ Echo Shop ("}
{formatNumber(currentEchoes)}
{" echoes)"}
</button>
</div>
{activeTab === "transcend"
&& <>
<p className="transcendence-intro">
{"Transcendence is the ultimate reset. It wipes "}
<strong>{"everything"}</strong>
{" — resources, prestige, runestones, upgrades, and equipment"
+ " — but grants "}
<strong>{"Echoes"}</strong>
{", a permanent currency that survives all future resets."}
{" Echoes power upgrades that permanently amplify every run."}
</p>
<p className="transcendence-intro">
<em>
{"Fewer prestiges = more Echoes."}
{" Optimise your run for maximum yield!"}
</em>
</p>
<div className="transcendence-status">
{transcendenceCount > 0
&& <p>
{"Transcendence count: "}
<strong>{transcendenceCount}</strong>
</p>
}
<p>
{"Current Echoes: "}
<strong>{formatNumber(currentEchoes)}</strong>
</p>
<p>
{"Current prestige count: "}
<strong>{prestigeData.count}</strong>
</p>
{hasDefeatedFinalBoss
? <p className="echo-preview">
{"Echoes on transcendence: "}
<strong>
{"+"}
{formatNumber(echoPreview)}
</strong>
{echoMetaMultiplier > 1
&& <span className="echo-meta-bonus">
{" (×"}
{echoMetaMultiplier.toFixed(2)}
{" meta bonus applied)"}
</span>
}
</p>
: null}
</div>
{hasDefeatedFinalBoss
? null
: <div className="transcendence-locked">
<p>
{"🔒 "}
<strong>{"Defeat The Absolute One"}</strong>
{" to unlock transcendence."}
</p>
<p className="transcendence-hint">
{"The Absolute One is the final boss of The Absolute zone,"
+ " requiring Prestige 90 to challenge."}
</p>
</div>
}
{hasDefeatedFinalBoss
? <div className="prestige-form">
<p>
{"You are ready to transcend. This action is "}
<strong>{"irreversible"}</strong>
{"."}
</p>
<button
className="transcendence-button"
disabled={isPending}
onClick={handleTranscendClick}
type="button"
>
{isPending
? "Transcending..."
: `🌌 Transcend (+${formatNumber(echoPreview)} Echoes)`}
</button>
{error === null
? null
: <p className="error">{error}</p>}
{result === null
? null
: <p className="success">
{"Transcended! Earned "}
<strong>
{formatNumber(result.echoes)}
{" Echoes"}
</strong>
{". This is Transcendence "}
{result.count}
{". A new cycle begins."}
</p>
}
</div>
: null}
</>
}
{activeTab === "shop"
&& <div className="echo-shop">
<p className="shop-balance">
{"Balance: "}
<strong>
{formatNumber(currentEchoes)}
{" Echoes"}
</strong>
</p>
<p className="echo-shop-description">
{"Echo upgrades are "}
<strong>{"permanent"}</strong>
{" — they survive all future prestiges and transcendences."}
</p>
{upgradesByCategory.map(({ catId, label, upgrades }) => {
return (
<div className="shop-category" key={catId}>
<h3>{label}</h3>
<div className="shop-upgrades">
{upgrades.map((upgrade) => {
const purchased = (
transcendence?.purchasedUpgradeIds ?? []
).includes(upgrade.id);
const canAfford = currentEchoes >= upgrade.cost;
const isLoading = buyingId === upgrade.id;
function handleBuyClick(): void {
void handleBuyUpgrade(upgrade.id);
}
return (
<div
className={`shop-upgrade-card echo-upgrade-card ${
purchased
? "purchased"
: ""
} ${!canAfford && !purchased
? "unaffordable"
: ""}`}
key={upgrade.id}
>
<div className="shop-upgrade-info">
<h4>{upgrade.name}</h4>
<p>{upgrade.description}</p>
<p className="upgrade-cost">
{purchased
? "✅ Purchased"
: `${formatNumber(upgrade.cost)} Echoes`}
</p>
</div>
{purchased
? null
: <button
className="buy-upgrade-button echo-buy-button"
disabled={
!canAfford || isLoading || buyingId !== null
}
onClick={handleBuyClick}
type="button"
>
{isLoading
? "Buying..."
: "Buy"}
</button>
}
</div>
);
})}
</div>
</div>
);
})}
</div>
}
</section>
);
};
export { TranscendencePanel };