Files
elysium/apps/web/src/components/game/transcendencePanel.tsx
T
hikari 1195b657a0
Security Scan and Upload / Security & DefectDojo Upload (push) Successful in 1m10s
CI / Lint, Build & Test (push) Successful in 1m13s
feat: another balance and bug fix pass (#238)
Working through open issues — fixes, balance changes, and features.

## Closed

- Closes #161
- Closes #181
- Closes #191
- Closes #199
- Closes #201
- Closes #202
- Closes #203
- Closes #204
- Closes #205
- Closes #206
- Closes #208
- Closes #211
- Closes #212
- Closes #213
- Closes #214
- Closes #216
- Closes #219
- Closes #220
- Closes #221
- Closes #222
- Closes #224
- Closes #225
- Closes #226
- Closes #228
- Closes #229
- Closes #230
- Closes #231
- Closes #232
- Closes #233
- Closes #234
- Closes #235
- Closes #236

 This PR was created with help from Hikari~ 🌸

Reviewed-on: #238
Co-authored-by: Hikari <hikari@nhcarrigan.com>
Co-committed-by: Hikari <hikari@nhcarrigan.com>
2026-04-06 18:17:00 -07:00

349 lines
11 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 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 */
/* eslint-disable max-lines -- Transcendence panel with CDN images exceeds line limit */
import { useState, type JSX } from "react";
import { useGame } from "../../context/gameContext.js";
import {
TRANSCENDENCE_UPGRADES,
TRANSCENDENCE_UPGRADE_CATEGORY_LABELS,
} from "../../data/transcendenceUpgrades.js";
import { cdnImage } from "../../utils/cdn.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, formatInteger, 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 ("}
{formatInteger(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>{formatInteger(currentEchoes)}</strong>
</p>
<p>
{"Current prestige count: "}
<strong>{prestigeData.count}</strong>
</p>
{hasDefeatedFinalBoss
? <p className="echo-preview">
{"Echoes on transcendence: "}
<strong>
{"+"}
{formatInteger(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 (+${formatInteger(echoPreview)} Echoes)`}
</button>
{error === null
? null
: <p className="error">{error}</p>}
{result === null
? null
: <p className="success">
{"Transcended! Earned "}
<strong>
{formatInteger(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>
{formatInteger(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}
>
<img
alt={upgrade.name}
className="card-thumbnail"
src={cdnImage("transcendence-upgrades", upgrade.id)}
/>
<div className="shop-upgrade-info">
<h4>{upgrade.name}</h4>
<p>{upgrade.description}</p>
<p className="upgrade-cost">
{purchased
? "✅ Purchased"
: `${formatInteger(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 };