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
+71
View File
@@ -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 });
});
+8 -1
View File
@@ -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>();