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:
2026-03-07 02:37:08 -08:00
committed by Naomi Carrigan
parent e8881a81d5
commit a6f9844120
18 changed files with 365 additions and 51 deletions
+8
View File
@@ -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>
);
};
+5 -1
View File
@@ -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}
+10
View File
@@ -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}
+81
View File
@@ -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;
}