generated from nhcarrigan/template
1195b657a0
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>
349 lines
11 KiB
TypeScript
349 lines
11 KiB
TypeScript
/**
|
||
* @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 };
|