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,49 +0,0 @@
|
||||
# Elysium — Content Ideas
|
||||
|
||||
A running list of planned features and content additions. Strike through items as they're completed!
|
||||
|
||||
---
|
||||
|
||||
## 🌟 New Systems
|
||||
|
||||
- [x] **Offline earnings** — When returning to the game, earn a percentage of what you'd have earned offline (cap at ~8–12 hours). Upgradeable via the prestige shop to increase the % and the time cap. Essential for an idle game!
|
||||
|
||||
- [x] **Second prestige layer (Transcendence)** — Unlocked after ~10 prestiges. Sacrifice all runestones for a new currency ("Echoes"?). Echoes are permanent account-wide currency that persist across prestiges. Has its own upgrade tree with truly game-changing bonuses. Gives endgame players a long-term goal.
|
||||
|
||||
- [x] **Daily challenges** — Three rotating objectives each day (e.g. kill X boss, earn X gold this run, complete X quests). Reward bonus crystals. Encourages daily logins even when idling comfortably.
|
||||
|
||||
- [x] **Boss first-kill bounties** — Defeating a boss for the very first time grants a one-time runestone bonus. Rewards exploration and makes conquering a new zone feel extra satisfying.
|
||||
|
||||
- [x] **Auto-prestige toggle** — Unlockable via the prestige shop. Automatically prestiges the moment the threshold is reached. Late-game convenience that dedicated players will love.
|
||||
|
||||
---
|
||||
|
||||
## 📦 Content Additions
|
||||
|
||||
- [x] **Equipment set bonuses** — Group existing equipment into named sets (e.g. "Shadow Infiltrator"). Wearing 2/3/4 pieces of a set grants escalating bonuses. Adds strategic depth without requiring lots of new items.
|
||||
|
||||
- [x] **The Codex / Lore Book** — Defeating bosses and completing quests unlocks lore entries about the world. Pure flavour, but gives the world depth and a collection mechanic. Show a ✨ notification when new lore unlocks.
|
||||
|
||||
- [x] **Milestone prestige bonuses** — Every 5th prestige, earn a free prestige upgrade or a large runestone windfall. Gives players mini-goals within the prestige loop.
|
||||
|
||||
---
|
||||
|
||||
## 📊 UI / Statistics
|
||||
|
||||
- [x] **Statistics panel** — All-time totals: gold earned across all runs, total prestiges, bosses defeated, quests completed, time played. Idle game players love seeing big numbers about their big numbers.
|
||||
|
||||
- [x] **Last cloud save date + Force cloud save button** — Display when the last cloud save occurred (always visible, e.g. in the ResourceBar). Include a manual "Force Save" button for peace of mind.
|
||||
|
||||
---
|
||||
|
||||
## 💜 Priority Order (Suggested)
|
||||
|
||||
1. ~~Offline earnings~~ ✅
|
||||
2. ~~Statistics panel~~ ✅
|
||||
3. ~~Daily challenges~~ ✅
|
||||
4. ~~Boss first-kill bounties~~ ✅
|
||||
5. ~~Milestone prestige bonuses~~ ✅
|
||||
6. ~~Equipment set bonuses~~ ✅
|
||||
7. ~~Auto-prestige toggle~~ ✅
|
||||
8. ~~The Codex / Lore Book~~ ✅
|
||||
9. ✅ Second prestige layer / Transcendence (big feature, save for later)
|
||||
@@ -3,6 +3,7 @@ import { Hono } from "hono";
|
||||
import { cors } from "hono/cors";
|
||||
import { logger } from "hono/logger";
|
||||
import { aboutRouter } from "./routes/about.js";
|
||||
import { apotheosisRouter } from "./routes/apotheosis.js";
|
||||
import { authRouter } from "./routes/auth.js";
|
||||
import { bossRouter } from "./routes/boss.js";
|
||||
import { gameRouter } from "./routes/game.js";
|
||||
@@ -28,6 +29,7 @@ app.route("/game", gameRouter);
|
||||
app.route("/boss", bossRouter);
|
||||
app.route("/prestige", prestigeRouter);
|
||||
app.route("/transcendence", transcendenceRouter);
|
||||
app.route("/apotheosis", apotheosisRouter);
|
||||
app.route("/profile", profileRouter);
|
||||
|
||||
app.get("/health", (context) => context.json({ status: "ok" }));
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
import type { ApotheosisRequest, GameState } from "@elysium/types";
|
||||
import { Hono } from "hono";
|
||||
import type { HonoEnv } from "../types/hono.js";
|
||||
import { prisma } from "../db/client.js";
|
||||
import { authMiddleware } from "../middleware/auth.js";
|
||||
import {
|
||||
buildPostApotheosisState,
|
||||
isEligibleForApotheosis,
|
||||
} from "../services/apotheosis.js";
|
||||
|
||||
export const apotheosisRouter = new Hono<HonoEnv>();
|
||||
|
||||
apotheosisRouter.use("*", authMiddleware);
|
||||
|
||||
apotheosisRouter.post("/", async (context) => {
|
||||
const discordId = context.get("discordId") as string;
|
||||
const body = await context.req.json<ApotheosisRequest>();
|
||||
|
||||
const characterName = body.characterName?.trim();
|
||||
if (!characterName) {
|
||||
return context.json({ error: "characterName is required" }, 400);
|
||||
}
|
||||
|
||||
const record = await prisma.gameState.findUnique({ where: { discordId } });
|
||||
if (!record) {
|
||||
return context.json({ error: "No save found" }, 404);
|
||||
}
|
||||
|
||||
const state = record.state as unknown as GameState;
|
||||
|
||||
if (!isEligibleForApotheosis(state)) {
|
||||
return context.json(
|
||||
{ error: "Not eligible for Apotheosis — purchase all Transcendence upgrades first" },
|
||||
400,
|
||||
);
|
||||
}
|
||||
|
||||
// Capture current-run stats before the nuclear reset
|
||||
const runBossesDefeated = state.bosses.filter((b) => b.status === "defeated").length;
|
||||
const runQuestsCompleted = state.quests.filter((q) => q.status === "completed").length;
|
||||
const runAdventurersRecruited = state.adventurers.reduce((sum, a) => sum + a.count, 0);
|
||||
const runAchievementsUnlocked = (state.achievements ?? []).filter((a) => a.unlockedAt !== null).length;
|
||||
|
||||
const { newState, newApotheosisData } = buildPostApotheosisState(state, characterName);
|
||||
|
||||
const now = Date.now();
|
||||
await prisma.gameState.update({
|
||||
where: { discordId },
|
||||
data: { state: newState as object, updatedAt: now },
|
||||
});
|
||||
|
||||
await prisma.player.update({
|
||||
where: { discordId },
|
||||
data: {
|
||||
characterName,
|
||||
// Reset current-run counters
|
||||
totalGoldEarned: 0,
|
||||
totalClicks: 0,
|
||||
// Accumulate into lifetime totals
|
||||
lifetimeGoldEarned: { increment: state.player.totalGoldEarned },
|
||||
lifetimeClicks: { increment: state.player.totalClicks },
|
||||
lifetimeBossesDefeated: { increment: runBossesDefeated },
|
||||
lifetimeQuestsCompleted: { increment: runQuestsCompleted },
|
||||
lifetimeAdventurersRecruited: { increment: runAdventurersRecruited },
|
||||
lifetimeAchievementsUnlocked: { increment: runAchievementsUnlocked },
|
||||
lastSavedAt: now,
|
||||
},
|
||||
});
|
||||
|
||||
return context.json({ newApotheosisCount: newApotheosisData.count });
|
||||
});
|
||||
@@ -302,7 +302,14 @@ const validateAndSanitize = (incoming: GameState, previous: GameState): GameStat
|
||||
? { transcendence: previous.transcendence }
|
||||
: {};
|
||||
|
||||
return { ...incoming, resources, bosses, quests, achievements, prestige, ...transcendenceSpread };
|
||||
// Apotheosis count can only increase server-side — cap at the previous value.
|
||||
const apotheosisSpread = incoming.apotheosis
|
||||
? { apotheosis: { count: Math.min(incoming.apotheosis.count, previous.apotheosis?.count ?? 0) } }
|
||||
: previous.apotheosis
|
||||
? { apotheosis: previous.apotheosis }
|
||||
: {};
|
||||
|
||||
return { ...incoming, resources, bosses, quests, achievements, prestige, ...transcendenceSpread, ...apotheosisSpread };
|
||||
};
|
||||
|
||||
export const gameRouter = new Hono<HonoEnv>();
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
import type { ApotheosisData, GameState } from "@elysium/types";
|
||||
import { INITIAL_GAME_STATE } from "../data/initialState.js";
|
||||
import { DEFAULT_TRANSCENDENCE_UPGRADES } from "../data/transcendenceUpgrades.js";
|
||||
|
||||
/** Total number of echo upgrades — all must be purchased to unlock Apotheosis */
|
||||
const TOTAL_ECHO_UPGRADES = DEFAULT_TRANSCENDENCE_UPGRADES.length;
|
||||
|
||||
/**
|
||||
* Returns true when the player is eligible to achieve Apotheosis:
|
||||
* all Transcendence echo upgrades must be purchased.
|
||||
*/
|
||||
export const isEligibleForApotheosis = (state: GameState): boolean => {
|
||||
const purchasedIds = state.transcendence?.purchasedUpgradeIds ?? [];
|
||||
return purchasedIds.length >= TOTAL_ECHO_UPGRADES &&
|
||||
DEFAULT_TRANSCENDENCE_UPGRADES.every((u) => purchasedIds.includes(u.id));
|
||||
};
|
||||
|
||||
/**
|
||||
* Builds the new game state after Apotheosis — the ultimate nuclear reset.
|
||||
* Wipes absolutely everything including prestige and transcendence.
|
||||
* Only codex lore entries and the apotheosis count itself are preserved.
|
||||
*/
|
||||
export const buildPostApotheosisState = (
|
||||
currentState: GameState,
|
||||
characterName: string,
|
||||
): { newState: GameState; newApotheosisData: ApotheosisData } => {
|
||||
const newCount = (currentState.apotheosis?.count ?? 0) + 1;
|
||||
const newApotheosisData: ApotheosisData = { count: newCount };
|
||||
|
||||
const freshState = INITIAL_GAME_STATE(currentState.player, characterName);
|
||||
const newState: GameState = {
|
||||
...freshState,
|
||||
lastTickAt: Date.now(),
|
||||
// Codex lore persists through all resets — players keep their discovered entries
|
||||
...(currentState.codex ? { codex: currentState.codex } : {}),
|
||||
// Apotheosis data is eternal — never wiped by any reset
|
||||
apotheosis: newApotheosisData,
|
||||
};
|
||||
|
||||
return { newState, newApotheosisData };
|
||||
};
|
||||
@@ -120,6 +120,8 @@ export const buildPostPrestigeState = (
|
||||
...(currentState.codex ? { codex: currentState.codex } : {}),
|
||||
// Transcendence data is permanent — never wiped by prestige
|
||||
...(currentState.transcendence ? { transcendence: currentState.transcendence } : {}),
|
||||
// Apotheosis data is eternal — never wiped by prestige
|
||||
...(currentState.apotheosis ? { apotheosis: currentState.apotheosis } : {}),
|
||||
};
|
||||
|
||||
return { newState, newPrestigeData, runestonesEarned, milestoneRunestones };
|
||||
|
||||
@@ -78,6 +78,8 @@ export const buildPostTranscendenceState = (
|
||||
...(currentState.codex ? { codex: currentState.codex } : {}),
|
||||
// Transcendence data is permanent
|
||||
transcendence: newTranscendenceData,
|
||||
// Apotheosis data is eternal — never wiped by transcendence
|
||||
...(currentState.apotheosis ? { apotheosis: currentState.apotheosis } : {}),
|
||||
};
|
||||
|
||||
return { newState, newTranscendenceData, echoesEarned };
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
export type { ApotheosisData } from "./interfaces/Apotheosis.js";
|
||||
export type { CodexEntry, CodexState } from "./interfaces/Codex.js";
|
||||
export type {
|
||||
Achievement,
|
||||
@@ -9,6 +10,8 @@ export type { Adventurer, AdventurerClass } from "./interfaces/Adventurer.js";
|
||||
export type {
|
||||
AboutResponse,
|
||||
ApiError,
|
||||
ApotheosisRequest,
|
||||
ApotheosisResponse,
|
||||
AuthResponse,
|
||||
BossChallengeRequest,
|
||||
BossChallengeResponse,
|
||||
|
||||
@@ -150,6 +150,14 @@ export interface BuyEchoUpgradeResponse {
|
||||
echoMetaMultiplier: number;
|
||||
}
|
||||
|
||||
export interface ApotheosisRequest {
|
||||
characterName: string;
|
||||
}
|
||||
|
||||
export interface ApotheosisResponse {
|
||||
newApotheosisCount: number;
|
||||
}
|
||||
|
||||
export interface ApiError {
|
||||
error: string;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
export interface ApotheosisData {
|
||||
/** Number of times the player has achieved Apotheosis */
|
||||
count: number;
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { Achievement } from "./Achievement.js";
|
||||
import type { Adventurer } from "./Adventurer.js";
|
||||
import type { Boss } from "./Boss.js";
|
||||
import type { ApotheosisData } from "./Apotheosis.js";
|
||||
import type { CodexState } from "./Codex.js";
|
||||
import type { DailyChallengeState } from "./DailyChallenge.js";
|
||||
import type { TranscendenceData } from "./Transcendence.js";
|
||||
@@ -33,4 +34,6 @@ export interface GameState {
|
||||
codex?: CodexState;
|
||||
/** Transcendence (second prestige layer) state — optional for backwards compatibility */
|
||||
transcendence?: TranscendenceData;
|
||||
/** Apotheosis (third prestige layer) state — optional for backwards compatibility */
|
||||
apotheosis?: ApotheosisData;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user