generated from nhcarrigan/template
feat: add apotheosis third prestige layer and remove IDEAS.md
Apotheosis is the ultimate reset — wipes absolutely everything including prestige and transcendence — in exchange for a pure bragging-rights badge. No mechanical benefit whatsoever. Unlock condition: all 15 Transcendence echo upgrades purchased. Can be achieved multiple times; each cycle requires repurchasing all Transcendence upgrades again. What survives: Codex lore entries and lifetime profile statistics. What is wiped: resources, prestige, runestones, transcendence data (echoes, echo upgrades, multipliers), equipment, upgrades, bosses, quests, zones, adventurers, achievements. New files: Apotheosis.ts type, apotheosis service, apotheosis route, ApotheosisPanel.tsx component. Modified: GameState (apotheosis field), Api.ts, types/index.ts, prestige service (carry apotheosis), transcendence service (carry apotheosis), game.ts anti-cheat (cap apotheosis count), API client, GameContext (apotheosis() function), GameLayout (new tab), ResourceBar (gold apotheosis badge shown above transcendence and prestige badges), styles.css, AboutPanel how-to-play. Also removes IDEAS.md — all planned features are now implemented!
This commit is contained in:
@@ -1,5 +1,7 @@
|
||||
import type {
|
||||
AboutResponse,
|
||||
ApotheosisRequest,
|
||||
ApotheosisResponse,
|
||||
AuthResponse,
|
||||
BossChallengeRequest,
|
||||
BossChallengeResponse,
|
||||
@@ -109,6 +111,12 @@ export const buyEchoUpgrade = async (
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
|
||||
export const achieveApotheosis = async (body: ApotheosisRequest): Promise<ApotheosisResponse> =>
|
||||
request<ApotheosisResponse>("/apotheosis", {
|
||||
method: "POST",
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
|
||||
export const getPublicProfile = async (
|
||||
discordId: string,
|
||||
): Promise<PublicProfileResponse> =>
|
||||
|
||||
@@ -63,6 +63,10 @@ const HOW_TO_PLAY = [
|
||||
title: "🌌 Transcendence",
|
||||
body: "Transcendence is the ultimate prestige layer, unlocked by defeating The Absolute One (requires Prestige 90). Transcending performs a nuclear reset — wiping resources, prestige, runestones, upgrades, and equipment — but grants Echoes based on your prestige count (fewer prestiges = more Echoes). Echoes are permanent and survive all future resets. Spend them in the Echo Shop on lasting multipliers: passive income, combat power, prestige quality-of-life, and Echo meta upgrades that amplify future Echo yields.",
|
||||
},
|
||||
{
|
||||
title: "✨ Apotheosis",
|
||||
body: "Apotheosis is the final act — a complete dissolution of everything you have built, including your prestige and transcendence progress. It is unlocked once you have purchased every Transcendence upgrade. In exchange for this total reset, you receive the Apotheosis badge: pure bragging rights, a mark of reaching the absolute pinnacle of the game. Apotheosis can be achieved multiple times; each cycle requires purchasing all Transcendence upgrades again. Your Codex entries and lifetime profile statistics are always preserved.",
|
||||
},
|
||||
];
|
||||
|
||||
const formatDate = (dateStr: string): string =>
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
import { useState } from "react";
|
||||
import { useGame } from "../../context/GameContext.js";
|
||||
import { TRANSCENDENCE_UPGRADES } from "../../data/transcendenceUpgrades.js";
|
||||
|
||||
const TOTAL_ECHO_UPGRADES = TRANSCENDENCE_UPGRADES.length;
|
||||
|
||||
export const ApotheosisPanel = (): React.JSX.Element => {
|
||||
const { state, apotheosis } = useGame();
|
||||
const [characterName, setCharacterName] = useState("");
|
||||
const [isPending, setIsPending] = useState(false);
|
||||
const [result, setResult] = useState<number | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
if (!state) return <section className="panel"><p>Loading...</p></section>;
|
||||
|
||||
const purchasedIds = state.transcendence?.purchasedUpgradeIds ?? [];
|
||||
const purchasedCount = TRANSCENDENCE_UPGRADES.filter((u) => purchasedIds.includes(u.id)).length;
|
||||
const isEligible = purchasedCount >= TOTAL_ECHO_UPGRADES;
|
||||
const apotheosisCount = state.apotheosis?.count ?? 0;
|
||||
|
||||
const handleApotheosis = async (): Promise<void> => {
|
||||
if (!characterName.trim()) return;
|
||||
setIsPending(true);
|
||||
setError(null);
|
||||
try {
|
||||
const data = await apotheosis(characterName.trim());
|
||||
setResult(data.newApotheosisCount);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : "Apotheosis failed");
|
||||
} finally {
|
||||
setIsPending(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<section className="panel apotheosis-panel">
|
||||
<h2>✨ Apotheosis</h2>
|
||||
|
||||
<p className="apotheosis-intro">
|
||||
Apotheosis is the final act — a complete dissolution of everything you have built.
|
||||
Prestige, Transcendence, Echoes, upgrades, equipment, resources: all of it returns
|
||||
to nothing. In exchange, you receive only one thing:
|
||||
</p>
|
||||
<p className="apotheosis-reward">
|
||||
The <strong>✨ Apotheosis</strong> badge. Proof that you have done it all.
|
||||
</p>
|
||||
<p className="apotheosis-intro">
|
||||
Apotheosis can be achieved multiple times. Each cycle requires you to purchase
|
||||
every Transcendence upgrade again before the next Apotheosis becomes available.
|
||||
There is no mechanical benefit — only the knowledge that you have reached the
|
||||
pinnacle, dissolved it, and climbed back up.
|
||||
</p>
|
||||
|
||||
{apotheosisCount > 0 && (
|
||||
<div className="apotheosis-count">
|
||||
<span>You have achieved Apotheosis <strong>{apotheosisCount}</strong> time{apotheosisCount === 1 ? "" : "s"}.</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="apotheosis-status">
|
||||
<p>
|
||||
Transcendence upgrades purchased:{" "}
|
||||
<strong>{purchasedCount} / {TOTAL_ECHO_UPGRADES}</strong>
|
||||
</p>
|
||||
{!isEligible && (
|
||||
<p className="apotheosis-missing">
|
||||
🔒 Purchase all {TOTAL_ECHO_UPGRADES} Transcendence upgrades to unlock Apotheosis.
|
||||
({TOTAL_ECHO_UPGRADES - purchasedCount} remaining)
|
||||
</p>
|
||||
)}
|
||||
{isEligible && (
|
||||
<p className="apotheosis-ready">✅ All Transcendence upgrades purchased. You are ready.</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{isEligible && (
|
||||
<div className="prestige-form">
|
||||
<p>This action is <strong>permanent and irreversible</strong>. Choose your name for the next cycle:</p>
|
||||
<input
|
||||
disabled={isPending}
|
||||
maxLength={32}
|
||||
onChange={(e) => { setCharacterName(e.target.value); }}
|
||||
placeholder="Character name..."
|
||||
type="text"
|
||||
value={characterName}
|
||||
/>
|
||||
<button
|
||||
className="apotheosis-button"
|
||||
disabled={isPending || !characterName.trim()}
|
||||
onClick={() => { void handleApotheosis(); }}
|
||||
type="button"
|
||||
>
|
||||
{isPending ? "Ascending..." : "✨ Achieve Apotheosis"}
|
||||
</button>
|
||||
{error && <p className="error">{error}</p>}
|
||||
{result !== null && (
|
||||
<p className="success">
|
||||
Apotheosis achieved. This is cycle <strong>{result}</strong>.
|
||||
The infinite loop continues.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
);
|
||||
};
|
||||
@@ -14,13 +14,14 @@ import { EditProfileModal } from "./EditProfileModal.js";
|
||||
import { EquipmentPanel } from "./EquipmentPanel.js";
|
||||
import { OfflineModal } from "./OfflineModal.js";
|
||||
import { PrestigePanel } from "./PrestigePanel.js";
|
||||
import { ApotheosisPanel } from "./ApotheosisPanel.js";
|
||||
import { TranscendencePanel } from "./TranscendencePanel.js";
|
||||
import { QuestPanel } from "./QuestPanel.js";
|
||||
import { StatisticsPanel } from "./StatisticsPanel.js";
|
||||
import { UpgradePanel } from "./UpgradePanel.js";
|
||||
import { DailyChallengePanel } from "./DailyChallengePanel.js";
|
||||
|
||||
type Tab = "adventurers" | "upgrades" | "quests" | "bosses" | "equipment" | "achievements" | "prestige" | "transcendence" | "statistics" | "daily" | "codex" | "about";
|
||||
type Tab = "adventurers" | "upgrades" | "quests" | "bosses" | "equipment" | "achievements" | "prestige" | "transcendence" | "apotheosis" | "statistics" | "daily" | "codex" | "about";
|
||||
|
||||
const BASE_TABS: { id: Tab; label: string }[] = [
|
||||
{ id: "adventurers", label: "⚔️ Adventurers" },
|
||||
@@ -31,6 +32,7 @@ const BASE_TABS: { id: Tab; label: string }[] = [
|
||||
{ id: "achievements", label: "🏆 Achievements" },
|
||||
{ id: "prestige", label: "⭐ Prestige" },
|
||||
{ id: "transcendence", label: "🌌 Transcendence" },
|
||||
{ id: "apotheosis", label: "✨ Apotheosis" },
|
||||
{ id: "statistics", label: "📊 Statistics" },
|
||||
{ id: "daily", label: "📅 Daily" },
|
||||
{ id: "codex", label: "📖 Codex" },
|
||||
@@ -69,6 +71,7 @@ export const GameLayout = (): React.JSX.Element => {
|
||||
runestones={state.prestige.runestones}
|
||||
prestigeCount={state.prestige.count}
|
||||
transcendenceCount={state.transcendence?.count ?? 0}
|
||||
apotheosisCount={state.apotheosis?.count ?? 0}
|
||||
profileUrl={profileUrl}
|
||||
onEditProfile={() => { setEditingProfile(true); }}
|
||||
lastSavedAt={lastSavedAt}
|
||||
@@ -116,6 +119,7 @@ export const GameLayout = (): React.JSX.Element => {
|
||||
{activeTab === "achievements" && <AchievementPanel />}
|
||||
{activeTab === "prestige" && <PrestigePanel />}
|
||||
{activeTab === "transcendence" && <TranscendencePanel />}
|
||||
{activeTab === "apotheosis" && <ApotheosisPanel />}
|
||||
{activeTab === "statistics" && <StatisticsPanel />}
|
||||
{activeTab === "daily" && <DailyChallengePanel />}
|
||||
{activeTab === "codex" && <CodexPanel />}
|
||||
|
||||
@@ -7,6 +7,7 @@ interface ResourceBarProps {
|
||||
runestones: number;
|
||||
prestigeCount: number;
|
||||
transcendenceCount: number;
|
||||
apotheosisCount: number;
|
||||
profileUrl: string;
|
||||
onEditProfile: () => void;
|
||||
lastSavedAt: number | null;
|
||||
@@ -31,6 +32,7 @@ export const ResourceBar = ({
|
||||
runestones,
|
||||
prestigeCount,
|
||||
transcendenceCount,
|
||||
apotheosisCount,
|
||||
profileUrl,
|
||||
onEditProfile,
|
||||
lastSavedAt,
|
||||
@@ -65,6 +67,11 @@ export const ResourceBar = ({
|
||||
<span className="resource-value">{formatNumber(runestones)}</span>
|
||||
<span className="resource-label">Runestones</span>
|
||||
</div>
|
||||
{apotheosisCount > 0 && (
|
||||
<div className="apotheosis-badge">
|
||||
✨ Apotheosis {apotheosisCount}
|
||||
</div>
|
||||
)}
|
||||
{transcendenceCount > 0 && (
|
||||
<div className="transcendence-badge">
|
||||
🌌 Transcendence {transcendenceCount}
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
useState,
|
||||
} from "react";
|
||||
import {
|
||||
achieveApotheosis as achieveApotheosisApi,
|
||||
buyEchoUpgrade as buyEchoUpgradeApi,
|
||||
buyPrestigeUpgrade as buyPrestigeUpgradeApi,
|
||||
challengeBoss as challengeBossApi,
|
||||
@@ -87,6 +88,8 @@ interface GameContextValue {
|
||||
transcend: (characterName: string) => Promise<{ echoes: number; newTranscendenceCount: number }>;
|
||||
/** Buy an echo upgrade from the transcendence shop */
|
||||
buyEchoUpgrade: (upgradeId: string) => Promise<void>;
|
||||
/** Achieve Apotheosis — the ultimate nuclear reset, bragging rights only */
|
||||
apotheosis: (characterName: string) => Promise<{ newApotheosisCount: number }>;
|
||||
}
|
||||
|
||||
const GameContext = createContext<GameContextValue | null>(null);
|
||||
@@ -543,6 +546,12 @@ export const GameProvider = ({ children }: { children: React.ReactNode }): React
|
||||
return result;
|
||||
}, [reload]);
|
||||
|
||||
const apotheosis = useCallback(async (characterName: string) => {
|
||||
const result = await achieveApotheosisApi({ characterName });
|
||||
await reload();
|
||||
return result;
|
||||
}, [reload]);
|
||||
|
||||
const buyEchoUpgrade = useCallback(async (upgradeId: string) => {
|
||||
try {
|
||||
const result = await buyEchoUpgradeApi({ upgradeId });
|
||||
@@ -751,6 +760,7 @@ export const GameProvider = ({ children }: { children: React.ReactNode }): React
|
||||
dismissCodexEntry,
|
||||
transcend,
|
||||
buyEchoUpgrade,
|
||||
apotheosis,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
|
||||
@@ -2441,3 +2441,84 @@ body {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
/* ── Apotheosis ─────────────────────────────────────────────────────────── */
|
||||
|
||||
.apotheosis-badge {
|
||||
background: linear-gradient(135deg, #78350f, #d97706, #fbbf24);
|
||||
border-radius: 999px;
|
||||
color: #1c1917;
|
||||
font-size: 0.85rem;
|
||||
font-weight: 700;
|
||||
padding: 0.25rem 0.75rem;
|
||||
}
|
||||
|
||||
.apotheosis-panel .apotheosis-intro {
|
||||
color: var(--colour-text-muted);
|
||||
font-size: 0.95rem;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.apotheosis-reward {
|
||||
background: linear-gradient(135deg, rgba(120, 53, 15, 0.2), rgba(217, 119, 6, 0.2));
|
||||
border: 1px solid #d97706;
|
||||
border-radius: var(--radius);
|
||||
font-size: 1rem;
|
||||
margin: 1rem 0;
|
||||
padding: 0.75rem 1rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.apotheosis-count {
|
||||
background: var(--colour-surface);
|
||||
border: 1px solid #d97706;
|
||||
border-radius: var(--radius);
|
||||
margin: 0.75rem 0;
|
||||
padding: 0.75rem 1rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.apotheosis-status {
|
||||
background: var(--colour-surface);
|
||||
border: 1px solid #d97706;
|
||||
border-radius: var(--radius);
|
||||
margin: 1rem 0;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.apotheosis-status p {
|
||||
margin: 0.25rem 0;
|
||||
}
|
||||
|
||||
.apotheosis-missing {
|
||||
color: var(--colour-text-muted);
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.apotheosis-ready {
|
||||
color: #fbbf24;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.apotheosis-button {
|
||||
background: linear-gradient(135deg, #78350f, #d97706);
|
||||
border: none;
|
||||
border-radius: var(--radius);
|
||||
color: #fff;
|
||||
cursor: pointer;
|
||||
font-size: 1rem;
|
||||
font-weight: 700;
|
||||
margin-top: 0.5rem;
|
||||
padding: 0.75rem 2rem;
|
||||
transition: opacity 0.2s;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.apotheosis-button:hover:not(:disabled) {
|
||||
opacity: 0.85;
|
||||
}
|
||||
|
||||
.apotheosis-button:disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user