generated from nhcarrigan/template
feat: v1 prototype — core game systems #30
@@ -8,7 +8,7 @@ A running list of planned features and content additions. Strike through items a
|
|||||||
|
|
||||||
- [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] **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!
|
||||||
|
|
||||||
- [ ] **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] **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] **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.
|
||||||
|
|
||||||
@@ -46,4 +46,4 @@ A running list of planned features and content additions. Strike through items a
|
|||||||
6. ~~Equipment set bonuses~~ ✅
|
6. ~~Equipment set bonuses~~ ✅
|
||||||
7. ~~Auto-prestige toggle~~ ✅
|
7. ~~Auto-prestige toggle~~ ✅
|
||||||
8. ~~The Codex / Lore Book~~ ✅
|
8. ~~The Codex / Lore Book~~ ✅
|
||||||
9. Second prestige layer / Transcendence (big feature, save for later)
|
9. ✅ Second prestige layer / Transcendence (big feature, save for later)
|
||||||
|
|||||||
@@ -0,0 +1,133 @@
|
|||||||
|
import type { TranscendenceUpgrade } from "@elysium/types";
|
||||||
|
|
||||||
|
export const DEFAULT_TRANSCENDENCE_UPGRADES: TranscendenceUpgrade[] = [
|
||||||
|
// ── Income multipliers ──────────────────────────────────────────────────────
|
||||||
|
{
|
||||||
|
id: "echo_income_1",
|
||||||
|
name: "Whisper of Power",
|
||||||
|
description: "The echoes of past runs linger, amplifying your guild's income by 25%.",
|
||||||
|
category: "income",
|
||||||
|
cost: 5,
|
||||||
|
multiplier: 1.25,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "echo_income_2",
|
||||||
|
name: "Resonance",
|
||||||
|
description: "Your transcendent experience resonates through your guild, boosting income by 50%.",
|
||||||
|
category: "income",
|
||||||
|
cost: 10,
|
||||||
|
multiplier: 1.5,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "echo_income_3",
|
||||||
|
name: "Harmonic Surge",
|
||||||
|
description: "The harmony of multiple timelines surges through your guild, doubling its income.",
|
||||||
|
category: "income",
|
||||||
|
cost: 20,
|
||||||
|
multiplier: 2.0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "echo_income_4",
|
||||||
|
name: "Ethereal Overflow",
|
||||||
|
description: "Ethereal energy overflows from your transcendence, tripling your guild's income.",
|
||||||
|
category: "income",
|
||||||
|
cost: 40,
|
||||||
|
multiplier: 3.0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "echo_income_5",
|
||||||
|
name: "Infinite Chorus",
|
||||||
|
description: "The infinite chorus of every run you've ever played amplifies your guild fivefold.",
|
||||||
|
category: "income",
|
||||||
|
cost: 80,
|
||||||
|
multiplier: 5.0,
|
||||||
|
},
|
||||||
|
|
||||||
|
// ── Combat multipliers ──────────────────────────────────────────────────────
|
||||||
|
{
|
||||||
|
id: "echo_combat_1",
|
||||||
|
name: "Battle-Hardened",
|
||||||
|
description: "Memories of countless battles harden your adventurers, increasing party DPS by 25%.",
|
||||||
|
category: "combat",
|
||||||
|
cost: 5,
|
||||||
|
multiplier: 1.25,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "echo_combat_2",
|
||||||
|
name: "Veteran's Edge",
|
||||||
|
description: "Veterans of transcendence know how to fight smarter, boosting party DPS by 50%.",
|
||||||
|
category: "combat",
|
||||||
|
cost: 15,
|
||||||
|
multiplier: 1.5,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "echo_combat_3",
|
||||||
|
name: "Transcendent Warrior",
|
||||||
|
description: "Your warriors carry the strength of every fallen timeline, doubling party DPS.",
|
||||||
|
category: "combat",
|
||||||
|
cost: 35,
|
||||||
|
multiplier: 2.0,
|
||||||
|
},
|
||||||
|
|
||||||
|
// ── Prestige threshold reductions ──────────────────────────────────────────
|
||||||
|
{
|
||||||
|
id: "echo_prestige_threshold_1",
|
||||||
|
name: "Accelerated Path",
|
||||||
|
description: "Experience from past lives shortens the road to prestige — threshold reduced by 10%.",
|
||||||
|
category: "prestige_threshold",
|
||||||
|
cost: 8,
|
||||||
|
multiplier: 0.9,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "echo_prestige_threshold_2",
|
||||||
|
name: "Shortcut Through Time",
|
||||||
|
description: "You've walked this path so many times you know every shortcut — threshold reduced by 20%.",
|
||||||
|
category: "prestige_threshold",
|
||||||
|
cost: 20,
|
||||||
|
multiplier: 0.8,
|
||||||
|
},
|
||||||
|
|
||||||
|
// ── Prestige runestone multipliers ─────────────────────────────────────────
|
||||||
|
{
|
||||||
|
id: "echo_prestige_runestones_1",
|
||||||
|
name: "Runic Attunement",
|
||||||
|
description: "Transcendent insight attunes you to the runestones, earning 50% more per prestige.",
|
||||||
|
category: "prestige_runestones",
|
||||||
|
cost: 8,
|
||||||
|
multiplier: 1.5,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "echo_prestige_runestones_2",
|
||||||
|
name: "Master Runesmith",
|
||||||
|
description: "You have mastered the art of runestone crafting, doubling your prestige runestone yield.",
|
||||||
|
category: "prestige_runestones",
|
||||||
|
cost: 20,
|
||||||
|
multiplier: 2.0,
|
||||||
|
},
|
||||||
|
|
||||||
|
// ── Echo meta multipliers ───────────────────────────────────────────────────
|
||||||
|
{
|
||||||
|
id: "echo_meta_1",
|
||||||
|
name: "Resonant Awakening",
|
||||||
|
description: "Your transcendence resonates deeper, amplifying future echo yields by 25%.",
|
||||||
|
category: "echo_meta",
|
||||||
|
cost: 10,
|
||||||
|
multiplier: 1.25,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "echo_meta_2",
|
||||||
|
name: "Transcendent Loop",
|
||||||
|
description: "Each loop of existence makes the next more powerful — future echo yields +50%.",
|
||||||
|
category: "echo_meta",
|
||||||
|
cost: 25,
|
||||||
|
multiplier: 1.5,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "echo_meta_3",
|
||||||
|
name: "Infinite Spiral",
|
||||||
|
description: "You have mastered the infinite spiral of transcendence, doubling all future echo yields.",
|
||||||
|
category: "echo_meta",
|
||||||
|
cost: 50,
|
||||||
|
multiplier: 2.0,
|
||||||
|
},
|
||||||
|
];
|
||||||
@@ -7,6 +7,7 @@ import { authRouter } from "./routes/auth.js";
|
|||||||
import { bossRouter } from "./routes/boss.js";
|
import { bossRouter } from "./routes/boss.js";
|
||||||
import { gameRouter } from "./routes/game.js";
|
import { gameRouter } from "./routes/game.js";
|
||||||
import { prestigeRouter } from "./routes/prestige.js";
|
import { prestigeRouter } from "./routes/prestige.js";
|
||||||
|
import { transcendenceRouter } from "./routes/transcendence.js";
|
||||||
import { profileRouter } from "./routes/profile.js";
|
import { profileRouter } from "./routes/profile.js";
|
||||||
|
|
||||||
const app = new Hono();
|
const app = new Hono();
|
||||||
@@ -26,6 +27,7 @@ app.route("/auth", authRouter);
|
|||||||
app.route("/game", gameRouter);
|
app.route("/game", gameRouter);
|
||||||
app.route("/boss", bossRouter);
|
app.route("/boss", bossRouter);
|
||||||
app.route("/prestige", prestigeRouter);
|
app.route("/prestige", prestigeRouter);
|
||||||
|
app.route("/transcendence", transcendenceRouter);
|
||||||
app.route("/profile", profileRouter);
|
app.route("/profile", profileRouter);
|
||||||
|
|
||||||
app.get("/health", (context) => context.json({ status: "ok" }));
|
app.get("/health", (context) => context.json({ status: "ok" }));
|
||||||
|
|||||||
@@ -59,7 +59,8 @@ const calculatePartyStats = (
|
|||||||
partyMaxHp += adventurer.level * 50 * adventurer.count;
|
partyMaxHp += adventurer.level * 50 * adventurer.count;
|
||||||
}
|
}
|
||||||
|
|
||||||
partyDPS *= equipmentCombatMultiplier * setCombatMultiplier;
|
const echoCombatMultiplier = state.transcendence?.echoCombatMultiplier ?? 1;
|
||||||
|
partyDPS *= equipmentCombatMultiplier * setCombatMultiplier * echoCombatMultiplier;
|
||||||
|
|
||||||
return { partyDPS, partyMaxHp };
|
return { partyDPS, partyMaxHp };
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -290,7 +290,19 @@ const validateAndSanitize = (incoming: GameState, previous: GameState): GameStat
|
|||||||
const prestige =
|
const prestige =
|
||||||
incoming.prestige.count < previous.prestige.count ? previous.prestige : incoming.prestige;
|
incoming.prestige.count < previous.prestige.count ? previous.prestige : incoming.prestige;
|
||||||
|
|
||||||
return { ...incoming, resources, bosses, quests, achievements, prestige };
|
// Echoes are only granted server-side via transcendence and can only decrease between
|
||||||
|
// saves (spent on echo upgrades). Cap at the previous value to block inflation.
|
||||||
|
const cappedEchoes = Math.min(
|
||||||
|
incoming.transcendence?.echoes ?? 0,
|
||||||
|
previous.transcendence?.echoes ?? 0,
|
||||||
|
);
|
||||||
|
const transcendenceSpread = incoming.transcendence
|
||||||
|
? { transcendence: { ...incoming.transcendence, echoes: cappedEchoes } }
|
||||||
|
: previous.transcendence
|
||||||
|
? { transcendence: previous.transcendence }
|
||||||
|
: {};
|
||||||
|
|
||||||
|
return { ...incoming, resources, bosses, quests, achievements, prestige, ...transcendenceSpread };
|
||||||
};
|
};
|
||||||
|
|
||||||
export const gameRouter = new Hono<HonoEnv>();
|
export const gameRouter = new Hono<HonoEnv>();
|
||||||
|
|||||||
@@ -0,0 +1,140 @@
|
|||||||
|
import type { BuyEchoUpgradeRequest, GameState, TranscendenceRequest } 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 { DEFAULT_TRANSCENDENCE_UPGRADES } from "../data/transcendenceUpgrades.js";
|
||||||
|
import {
|
||||||
|
buildPostTranscendenceState,
|
||||||
|
computeTranscendenceMultipliers,
|
||||||
|
isEligibleForTranscendence,
|
||||||
|
} from "../services/transcendence.js";
|
||||||
|
|
||||||
|
export const transcendenceRouter = new Hono<HonoEnv>();
|
||||||
|
|
||||||
|
transcendenceRouter.use("*", authMiddleware);
|
||||||
|
|
||||||
|
transcendenceRouter.post("/", async (context) => {
|
||||||
|
const discordId = context.get("discordId") as string;
|
||||||
|
const body = await context.req.json<TranscendenceRequest>();
|
||||||
|
|
||||||
|
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 (!isEligibleForTranscendence(state)) {
|
||||||
|
return context.json(
|
||||||
|
{ error: "Not eligible for transcendence — defeat The Absolute One first" },
|
||||||
|
400,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { newState, newTranscendenceData, echoesEarned } = buildPostTranscendenceState(
|
||||||
|
state,
|
||||||
|
characterName,
|
||||||
|
);
|
||||||
|
|
||||||
|
// 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 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 (same as prestige)
|
||||||
|
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({
|
||||||
|
echoes: echoesEarned,
|
||||||
|
newTranscendenceCount: newTranscendenceData.count,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
transcendenceRouter.post("/buy-upgrade", async (context) => {
|
||||||
|
const discordId = context.get("discordId") as string;
|
||||||
|
const body = await context.req.json<BuyEchoUpgradeRequest>();
|
||||||
|
|
||||||
|
const { upgradeId } = body;
|
||||||
|
if (!upgradeId) {
|
||||||
|
return context.json({ error: "upgradeId is required" }, 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
const upgrade = DEFAULT_TRANSCENDENCE_UPGRADES.find((u) => u.id === upgradeId);
|
||||||
|
if (!upgrade) {
|
||||||
|
return context.json({ error: "Unknown echo upgrade" }, 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
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 (!state.transcendence) {
|
||||||
|
return context.json({ error: "No transcendence data found" }, 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { purchasedUpgradeIds, echoes } = state.transcendence;
|
||||||
|
|
||||||
|
if (purchasedUpgradeIds.includes(upgradeId)) {
|
||||||
|
return context.json({ error: "Upgrade already purchased" }, 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (echoes < upgrade.cost) {
|
||||||
|
return context.json({ error: "Not enough echoes" }, 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
const newEchoes = echoes - upgrade.cost;
|
||||||
|
const newPurchasedIds = [...purchasedUpgradeIds, upgradeId];
|
||||||
|
const newMultipliers = computeTranscendenceMultipliers(newPurchasedIds);
|
||||||
|
|
||||||
|
const newState: GameState = {
|
||||||
|
...state,
|
||||||
|
transcendence: {
|
||||||
|
...state.transcendence,
|
||||||
|
echoes: newEchoes,
|
||||||
|
purchasedUpgradeIds: newPurchasedIds,
|
||||||
|
...newMultipliers,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
await prisma.gameState.update({
|
||||||
|
where: { discordId },
|
||||||
|
data: { state: newState as object, updatedAt: Date.now() },
|
||||||
|
});
|
||||||
|
|
||||||
|
return context.json({
|
||||||
|
echoesRemaining: newEchoes,
|
||||||
|
purchasedUpgradeIds: newPurchasedIds,
|
||||||
|
...newMultipliers,
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -16,11 +16,13 @@ const MILESTONE_RUNESTONES_PER_INTERVAL = 25;
|
|||||||
* Calculates the gold threshold required for the next prestige.
|
* Calculates the gold threshold required for the next prestige.
|
||||||
* Formula: BASE * SCALE_FACTOR^prestigeCount — each prestige makes the next threshold harder.
|
* Formula: BASE * SCALE_FACTOR^prestigeCount — each prestige makes the next threshold harder.
|
||||||
*/
|
*/
|
||||||
export const calculatePrestigeThreshold = (prestigeCount: number): number =>
|
export const calculatePrestigeThreshold = (prestigeCount: number, thresholdMultiplier = 1): number =>
|
||||||
BASE_PRESTIGE_GOLD_THRESHOLD * Math.pow(THRESHOLD_SCALE_FACTOR, prestigeCount);
|
BASE_PRESTIGE_GOLD_THRESHOLD * Math.pow(THRESHOLD_SCALE_FACTOR, prestigeCount) * thresholdMultiplier;
|
||||||
|
|
||||||
export const isEligibleForPrestige = (state: GameState): boolean =>
|
export const isEligibleForPrestige = (state: GameState): boolean => {
|
||||||
state.player.totalGoldEarned >= calculatePrestigeThreshold(state.prestige.count);
|
const thresholdMultiplier = state.transcendence?.echoPrestigeThresholdMultiplier ?? 1;
|
||||||
|
return state.player.totalGoldEarned >= calculatePrestigeThreshold(state.prestige.count, thresholdMultiplier);
|
||||||
|
};
|
||||||
|
|
||||||
const getCategoryMultiplier = (
|
const getCategoryMultiplier = (
|
||||||
purchasedUpgradeIds: string[],
|
purchasedUpgradeIds: string[],
|
||||||
@@ -52,12 +54,13 @@ export const calculateRunestones = (
|
|||||||
totalGoldEarned: number,
|
totalGoldEarned: number,
|
||||||
prestigeCount: number,
|
prestigeCount: number,
|
||||||
purchasedUpgradeIds: string[],
|
purchasedUpgradeIds: string[],
|
||||||
|
echoRunestoneMultiplier = 1,
|
||||||
): number => {
|
): number => {
|
||||||
const threshold = calculatePrestigeThreshold(prestigeCount);
|
const threshold = calculatePrestigeThreshold(prestigeCount);
|
||||||
const base =
|
const base =
|
||||||
Math.floor(Math.sqrt(totalGoldEarned / threshold)) * RUNESTONES_PER_PRESTIGE_LEVEL;
|
Math.floor(Math.sqrt(totalGoldEarned / threshold)) * RUNESTONES_PER_PRESTIGE_LEVEL;
|
||||||
const runestoneMult = getCategoryMultiplier(purchasedUpgradeIds, "runestones");
|
const runestoneMult = getCategoryMultiplier(purchasedUpgradeIds, "runestones");
|
||||||
return Math.floor(base * runestoneMult);
|
return Math.floor(base * runestoneMult * echoRunestoneMultiplier);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -85,10 +88,12 @@ export const buildPostPrestigeState = (
|
|||||||
currentState: GameState,
|
currentState: GameState,
|
||||||
characterName: string,
|
characterName: string,
|
||||||
): { newState: GameState; newPrestigeData: PrestigeData; runestonesEarned: number; milestoneRunestones: number } => {
|
): { newState: GameState; newPrestigeData: PrestigeData; runestonesEarned: number; milestoneRunestones: number } => {
|
||||||
|
const echoRunestoneMultiplier = currentState.transcendence?.echoPrestigeRunestoneMultiplier ?? 1;
|
||||||
const runestonesEarned = calculateRunestones(
|
const runestonesEarned = calculateRunestones(
|
||||||
currentState.player.totalGoldEarned,
|
currentState.player.totalGoldEarned,
|
||||||
currentState.prestige.count,
|
currentState.prestige.count,
|
||||||
currentState.prestige.purchasedUpgradeIds,
|
currentState.prestige.purchasedUpgradeIds,
|
||||||
|
echoRunestoneMultiplier,
|
||||||
);
|
);
|
||||||
const newPrestigeCount = currentState.prestige.count + 1;
|
const newPrestigeCount = currentState.prestige.count + 1;
|
||||||
const { purchasedUpgradeIds } = currentState.prestige;
|
const { purchasedUpgradeIds } = currentState.prestige;
|
||||||
@@ -113,6 +118,8 @@ export const buildPostPrestigeState = (
|
|||||||
lastTickAt: Date.now(),
|
lastTickAt: Date.now(),
|
||||||
// Codex lore persists across prestiges — players keep their discovered entries
|
// Codex lore persists across prestiges — players keep their discovered entries
|
||||||
...(currentState.codex ? { codex: currentState.codex } : {}),
|
...(currentState.codex ? { codex: currentState.codex } : {}),
|
||||||
|
// Transcendence data is permanent — never wiped by prestige
|
||||||
|
...(currentState.transcendence ? { transcendence: currentState.transcendence } : {}),
|
||||||
};
|
};
|
||||||
|
|
||||||
return { newState, newPrestigeData, runestonesEarned, milestoneRunestones };
|
return { newState, newPrestigeData, runestonesEarned, milestoneRunestones };
|
||||||
|
|||||||
@@ -0,0 +1,84 @@
|
|||||||
|
import type { GameState, TranscendenceData, TranscendenceUpgradeCategory } from "@elysium/types";
|
||||||
|
import { INITIAL_GAME_STATE } from "../data/initialState.js";
|
||||||
|
import { DEFAULT_TRANSCENDENCE_UPGRADES } from "../data/transcendenceUpgrades.js";
|
||||||
|
|
||||||
|
/** ID of the boss that must be defeated to unlock transcendence */
|
||||||
|
const FINAL_BOSS_ID = "the_absolute_one";
|
||||||
|
|
||||||
|
/** Base constant used in the echo yield formula */
|
||||||
|
const ECHO_FORMULA_CONSTANT = 853;
|
||||||
|
|
||||||
|
const getCategoryMultiplier = (
|
||||||
|
purchasedIds: string[],
|
||||||
|
category: TranscendenceUpgradeCategory,
|
||||||
|
): number =>
|
||||||
|
DEFAULT_TRANSCENDENCE_UPGRADES
|
||||||
|
.filter((u) => u.category === category && purchasedIds.includes(u.id))
|
||||||
|
.reduce((mult, u) => mult * u.multiplier, 1);
|
||||||
|
|
||||||
|
export const computeTranscendenceMultipliers = (
|
||||||
|
purchasedUpgradeIds: string[],
|
||||||
|
): Omit<TranscendenceData, "count" | "echoes" | "purchasedUpgradeIds"> => ({
|
||||||
|
echoIncomeMultiplier: getCategoryMultiplier(purchasedUpgradeIds, "income"),
|
||||||
|
echoCombatMultiplier: getCategoryMultiplier(purchasedUpgradeIds, "combat"),
|
||||||
|
echoPrestigeThresholdMultiplier: getCategoryMultiplier(purchasedUpgradeIds, "prestige_threshold"),
|
||||||
|
echoPrestigeRunestoneMultiplier: getCategoryMultiplier(purchasedUpgradeIds, "prestige_runestones"),
|
||||||
|
echoMetaMultiplier: getCategoryMultiplier(purchasedUpgradeIds, "echo_meta"),
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true when the player is eligible to transcend:
|
||||||
|
* they must have defeated the final boss at least once.
|
||||||
|
*/
|
||||||
|
export const isEligibleForTranscendence = (state: GameState): boolean =>
|
||||||
|
state.bosses.some((b) => b.id === FINAL_BOSS_ID && b.status === "defeated");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculates echo yield for a transcendence.
|
||||||
|
* Formula: floor(CONSTANT / sqrt(prestigeCount)) × echoMetaMultiplier
|
||||||
|
* Fewer prestiges = more echoes (rewards efficient play).
|
||||||
|
* Minimum prestige count of 1 is enforced to avoid division by zero.
|
||||||
|
*/
|
||||||
|
export const calculateEchoes = (
|
||||||
|
prestigeCount: number,
|
||||||
|
echoMetaMultiplier: number,
|
||||||
|
): number => {
|
||||||
|
const safeCount = Math.max(prestigeCount, 1);
|
||||||
|
return Math.floor((ECHO_FORMULA_CONSTANT / Math.sqrt(safeCount)) * echoMetaMultiplier);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds the new game state after a transcendence (nuclear reset).
|
||||||
|
* Wipes everything except codex, dailyChallenges, and transcendence data.
|
||||||
|
*/
|
||||||
|
export const buildPostTranscendenceState = (
|
||||||
|
currentState: GameState,
|
||||||
|
characterName: string,
|
||||||
|
): { newState: GameState; newTranscendenceData: TranscendenceData; echoesEarned: number } => {
|
||||||
|
const previousTranscendence = currentState.transcendence;
|
||||||
|
const echoMetaMultiplier = previousTranscendence?.echoMetaMultiplier ?? 1;
|
||||||
|
|
||||||
|
const echoesEarned = calculateEchoes(currentState.prestige.count, echoMetaMultiplier);
|
||||||
|
const previousEchoes = previousTranscendence?.echoes ?? 0;
|
||||||
|
const newCount = (previousTranscendence?.count ?? 0) + 1;
|
||||||
|
const newPurchasedIds = previousTranscendence?.purchasedUpgradeIds ?? [];
|
||||||
|
|
||||||
|
const newTranscendenceData: TranscendenceData = {
|
||||||
|
count: newCount,
|
||||||
|
echoes: previousEchoes + echoesEarned,
|
||||||
|
purchasedUpgradeIds: newPurchasedIds,
|
||||||
|
...computeTranscendenceMultipliers(newPurchasedIds),
|
||||||
|
};
|
||||||
|
|
||||||
|
const freshState = INITIAL_GAME_STATE(currentState.player, characterName);
|
||||||
|
const newState: GameState = {
|
||||||
|
...freshState,
|
||||||
|
lastTickAt: Date.now(),
|
||||||
|
// Codex lore persists through all resets
|
||||||
|
...(currentState.codex ? { codex: currentState.codex } : {}),
|
||||||
|
// Transcendence data is permanent
|
||||||
|
transcendence: newTranscendenceData,
|
||||||
|
};
|
||||||
|
|
||||||
|
return { newState, newTranscendenceData, echoesEarned };
|
||||||
|
};
|
||||||
@@ -3,6 +3,8 @@ import type {
|
|||||||
AuthResponse,
|
AuthResponse,
|
||||||
BossChallengeRequest,
|
BossChallengeRequest,
|
||||||
BossChallengeResponse,
|
BossChallengeResponse,
|
||||||
|
BuyEchoUpgradeRequest,
|
||||||
|
BuyEchoUpgradeResponse,
|
||||||
BuyPrestigeUpgradeRequest,
|
BuyPrestigeUpgradeRequest,
|
||||||
BuyPrestigeUpgradeResponse,
|
BuyPrestigeUpgradeResponse,
|
||||||
LoadResponse,
|
LoadResponse,
|
||||||
@@ -11,6 +13,8 @@ import type {
|
|||||||
PublicProfileResponse,
|
PublicProfileResponse,
|
||||||
SaveRequest,
|
SaveRequest,
|
||||||
SaveResponse,
|
SaveResponse,
|
||||||
|
TranscendenceRequest,
|
||||||
|
TranscendenceResponse,
|
||||||
UpdateProfileRequest,
|
UpdateProfileRequest,
|
||||||
UpdateProfileResponse,
|
UpdateProfileResponse,
|
||||||
} from "@elysium/types";
|
} from "@elysium/types";
|
||||||
@@ -91,6 +95,20 @@ export const buyPrestigeUpgrade = async (
|
|||||||
body: JSON.stringify(body),
|
body: JSON.stringify(body),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const transcend = async (body: TranscendenceRequest): Promise<TranscendenceResponse> =>
|
||||||
|
request<TranscendenceResponse>("/transcendence", {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify(body),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const buyEchoUpgrade = async (
|
||||||
|
body: BuyEchoUpgradeRequest,
|
||||||
|
): Promise<BuyEchoUpgradeResponse> =>
|
||||||
|
request<BuyEchoUpgradeResponse>("/transcendence/buy-upgrade", {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify(body),
|
||||||
|
});
|
||||||
|
|
||||||
export const getPublicProfile = async (
|
export const getPublicProfile = async (
|
||||||
discordId: string,
|
discordId: string,
|
||||||
): Promise<PublicProfileResponse> =>
|
): Promise<PublicProfileResponse> =>
|
||||||
|
|||||||
@@ -59,6 +59,10 @@ const HOW_TO_PLAY = [
|
|||||||
title: "☁️ Cloud Saves",
|
title: "☁️ Cloud Saves",
|
||||||
body: "Your progress is automatically saved to the cloud every 30 seconds whilst you play. You can also force a manual save at any time using the sync button in the resource bar. Your save is protected by HMAC validation to ensure data integrity.",
|
body: "Your progress is automatically saved to the cloud every 30 seconds whilst you play. You can also force a manual save at any time using the sync button in the resource bar. Your save is protected by HMAC validation to ensure data integrity.",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
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.",
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const formatDate = (dateStr: string): string =>
|
const formatDate = (dateStr: string): string =>
|
||||||
|
|||||||
@@ -14,12 +14,13 @@ import { EditProfileModal } from "./EditProfileModal.js";
|
|||||||
import { EquipmentPanel } from "./EquipmentPanel.js";
|
import { EquipmentPanel } from "./EquipmentPanel.js";
|
||||||
import { OfflineModal } from "./OfflineModal.js";
|
import { OfflineModal } from "./OfflineModal.js";
|
||||||
import { PrestigePanel } from "./PrestigePanel.js";
|
import { PrestigePanel } from "./PrestigePanel.js";
|
||||||
|
import { TranscendencePanel } from "./TranscendencePanel.js";
|
||||||
import { QuestPanel } from "./QuestPanel.js";
|
import { QuestPanel } from "./QuestPanel.js";
|
||||||
import { StatisticsPanel } from "./StatisticsPanel.js";
|
import { StatisticsPanel } from "./StatisticsPanel.js";
|
||||||
import { UpgradePanel } from "./UpgradePanel.js";
|
import { UpgradePanel } from "./UpgradePanel.js";
|
||||||
import { DailyChallengePanel } from "./DailyChallengePanel.js";
|
import { DailyChallengePanel } from "./DailyChallengePanel.js";
|
||||||
|
|
||||||
type Tab = "adventurers" | "upgrades" | "quests" | "bosses" | "equipment" | "achievements" | "prestige" | "statistics" | "daily" | "codex" | "about";
|
type Tab = "adventurers" | "upgrades" | "quests" | "bosses" | "equipment" | "achievements" | "prestige" | "transcendence" | "statistics" | "daily" | "codex" | "about";
|
||||||
|
|
||||||
const BASE_TABS: { id: Tab; label: string }[] = [
|
const BASE_TABS: { id: Tab; label: string }[] = [
|
||||||
{ id: "adventurers", label: "⚔️ Adventurers" },
|
{ id: "adventurers", label: "⚔️ Adventurers" },
|
||||||
@@ -29,6 +30,7 @@ const BASE_TABS: { id: Tab; label: string }[] = [
|
|||||||
{ id: "equipment", label: "🗡️ Equipment" },
|
{ id: "equipment", label: "🗡️ Equipment" },
|
||||||
{ id: "achievements", label: "🏆 Achievements" },
|
{ id: "achievements", label: "🏆 Achievements" },
|
||||||
{ id: "prestige", label: "⭐ Prestige" },
|
{ id: "prestige", label: "⭐ Prestige" },
|
||||||
|
{ id: "transcendence", label: "🌌 Transcendence" },
|
||||||
{ id: "statistics", label: "📊 Statistics" },
|
{ id: "statistics", label: "📊 Statistics" },
|
||||||
{ id: "daily", label: "📅 Daily" },
|
{ id: "daily", label: "📅 Daily" },
|
||||||
{ id: "codex", label: "📖 Codex" },
|
{ id: "codex", label: "📖 Codex" },
|
||||||
@@ -66,6 +68,7 @@ export const GameLayout = (): React.JSX.Element => {
|
|||||||
resources={state.resources}
|
resources={state.resources}
|
||||||
runestones={state.prestige.runestones}
|
runestones={state.prestige.runestones}
|
||||||
prestigeCount={state.prestige.count}
|
prestigeCount={state.prestige.count}
|
||||||
|
transcendenceCount={state.transcendence?.count ?? 0}
|
||||||
profileUrl={profileUrl}
|
profileUrl={profileUrl}
|
||||||
onEditProfile={() => { setEditingProfile(true); }}
|
onEditProfile={() => { setEditingProfile(true); }}
|
||||||
lastSavedAt={lastSavedAt}
|
lastSavedAt={lastSavedAt}
|
||||||
@@ -112,6 +115,7 @@ export const GameLayout = (): React.JSX.Element => {
|
|||||||
{activeTab === "equipment" && <EquipmentPanel />}
|
{activeTab === "equipment" && <EquipmentPanel />}
|
||||||
{activeTab === "achievements" && <AchievementPanel />}
|
{activeTab === "achievements" && <AchievementPanel />}
|
||||||
{activeTab === "prestige" && <PrestigePanel />}
|
{activeTab === "prestige" && <PrestigePanel />}
|
||||||
|
{activeTab === "transcendence" && <TranscendencePanel />}
|
||||||
{activeTab === "statistics" && <StatisticsPanel />}
|
{activeTab === "statistics" && <StatisticsPanel />}
|
||||||
{activeTab === "daily" && <DailyChallengePanel />}
|
{activeTab === "daily" && <DailyChallengePanel />}
|
||||||
{activeTab === "codex" && <CodexPanel />}
|
{activeTab === "codex" && <CodexPanel />}
|
||||||
|
|||||||
@@ -0,0 +1,224 @@
|
|||||||
|
import type { TranscendenceUpgradeCategory } from "@elysium/types";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { useGame } from "../../context/GameContext.js";
|
||||||
|
import {
|
||||||
|
TRANSCENDENCE_UPGRADES,
|
||||||
|
TRANSCENDENCE_UPGRADE_CATEGORY_LABELS,
|
||||||
|
} from "../../data/transcendenceUpgrades.js";
|
||||||
|
|
||||||
|
const ECHO_FORMULA_CONSTANT = 853;
|
||||||
|
const FINAL_BOSS_ID = "the_absolute_one";
|
||||||
|
|
||||||
|
const calculateEchoPreview = (prestigeCount: number, echoMetaMultiplier: number): number => {
|
||||||
|
const safeCount = Math.max(prestigeCount, 1);
|
||||||
|
return Math.floor((ECHO_FORMULA_CONSTANT / Math.sqrt(safeCount)) * echoMetaMultiplier);
|
||||||
|
};
|
||||||
|
|
||||||
|
const CATEGORY_ORDER: TranscendenceUpgradeCategory[] = [
|
||||||
|
"income",
|
||||||
|
"combat",
|
||||||
|
"prestige_threshold",
|
||||||
|
"prestige_runestones",
|
||||||
|
"echo_meta",
|
||||||
|
];
|
||||||
|
|
||||||
|
export const TranscendencePanel = (): React.JSX.Element => {
|
||||||
|
const { state, formatNumber, transcend, buyEchoUpgrade } = useGame();
|
||||||
|
const [characterName, setCharacterName] = useState("");
|
||||||
|
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);
|
||||||
|
const [activeTab, setActiveTab] = useState<"transcend" | "shop">("transcend");
|
||||||
|
|
||||||
|
if (!state) return <section className="panel"><p>Loading...</p></section>;
|
||||||
|
|
||||||
|
const { bosses, prestige: prestigeData, transcendence } = state;
|
||||||
|
const hasDefeatedFinalBoss = bosses.some((b) => b.id === FINAL_BOSS_ID && b.status === "defeated");
|
||||||
|
const echoMetaMultiplier = transcendence?.echoMetaMultiplier ?? 1;
|
||||||
|
const echoPreview = calculateEchoPreview(prestigeData.count, echoMetaMultiplier);
|
||||||
|
const currentEchoes = transcendence?.echoes ?? 0;
|
||||||
|
const transcendenceCount = transcendence?.count ?? 0;
|
||||||
|
|
||||||
|
const handleTranscend = async (): Promise<void> => {
|
||||||
|
if (!characterName.trim()) return;
|
||||||
|
setIsPending(true);
|
||||||
|
setError(null);
|
||||||
|
try {
|
||||||
|
const data = await transcend(characterName.trim());
|
||||||
|
setResult({ echoes: data.echoes, count: data.newTranscendenceCount });
|
||||||
|
} catch (err) {
|
||||||
|
setError(err instanceof Error ? err.message : "Transcendence failed");
|
||||||
|
} finally {
|
||||||
|
setIsPending(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleBuyUpgrade = async (upgradeId: string): Promise<void> => {
|
||||||
|
setBuyingId(upgradeId);
|
||||||
|
try {
|
||||||
|
await buyEchoUpgrade(upgradeId);
|
||||||
|
} finally {
|
||||||
|
setBuyingId(null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const upgradesByCategory = CATEGORY_ORDER.map((category) => ({
|
||||||
|
category,
|
||||||
|
label: TRANSCENDENCE_UPGRADE_CATEGORY_LABELS[category] ?? category,
|
||||||
|
upgrades: TRANSCENDENCE_UPGRADES.filter((u) => u.category === category),
|
||||||
|
}));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className="panel transcendence-panel">
|
||||||
|
<h2>🌌 Transcendence</h2>
|
||||||
|
|
||||||
|
<div className="prestige-tabs">
|
||||||
|
<button
|
||||||
|
className={`prestige-tab ${activeTab === "transcend" ? "active" : ""}`}
|
||||||
|
onClick={() => { setActiveTab("transcend"); }}
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
Transcend
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className={`prestige-tab ${activeTab === "shop" ? "active" : ""}`}
|
||||||
|
onClick={() => { setActiveTab("shop"); }}
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
✨ Echo Shop ({formatNumber(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 from this point forward.
|
||||||
|
</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>{formatNumber(currentEchoes)}</strong></p>
|
||||||
|
<p>Current prestige count: <strong>{prestigeData.count}</strong></p>
|
||||||
|
{hasDefeatedFinalBoss && (
|
||||||
|
<p className="echo-preview">
|
||||||
|
Echoes on transcendence: <strong>+{formatNumber(echoPreview)}</strong>
|
||||||
|
{echoMetaMultiplier > 1 && (
|
||||||
|
<span className="echo-meta-bonus">
|
||||||
|
{" "}(×{echoMetaMultiplier.toFixed(2)} meta bonus applied)
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{!hasDefeatedFinalBoss && (
|
||||||
|
<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>
|
||||||
|
<p>Choose your new character 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="transcendence-button"
|
||||||
|
disabled={isPending || !characterName.trim()}
|
||||||
|
onClick={() => { void handleTranscend(); }}
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
{isPending
|
||||||
|
? "Transcending..."
|
||||||
|
: `🌌 Transcend (+${formatNumber(echoPreview)} Echoes)`}
|
||||||
|
</button>
|
||||||
|
{error && <p className="error">{error}</p>}
|
||||||
|
{result && (
|
||||||
|
<p className="success">
|
||||||
|
Transcended! Earned{" "}
|
||||||
|
<strong>{formatNumber(result.echoes)} Echoes</strong>. This is
|
||||||
|
Transcendence {result.count}. A new cycle begins.
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{activeTab === "shop" && (
|
||||||
|
<div className="echo-shop">
|
||||||
|
<p className="shop-balance">
|
||||||
|
Balance: <strong>{formatNumber(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(({ category, label, upgrades }) => (
|
||||||
|
<div key={category} className="shop-category">
|
||||||
|
<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;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={upgrade.id}
|
||||||
|
className={`shop-upgrade-card echo-upgrade-card ${purchased ? "purchased" : ""} ${!canAfford && !purchased ? "unaffordable" : ""}`}
|
||||||
|
>
|
||||||
|
<div className="shop-upgrade-info">
|
||||||
|
<h4>{upgrade.name}</h4>
|
||||||
|
<p>{upgrade.description}</p>
|
||||||
|
<p className="upgrade-cost">
|
||||||
|
{purchased
|
||||||
|
? "✅ Purchased"
|
||||||
|
: `✨ ${formatNumber(upgrade.cost)} Echoes`}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{!purchased && (
|
||||||
|
<button
|
||||||
|
className="buy-upgrade-button echo-buy-button"
|
||||||
|
disabled={!canAfford || isLoading || buyingId !== null}
|
||||||
|
onClick={() => { void handleBuyUpgrade(upgrade.id); }}
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
{isLoading ? "Buying..." : "Buy"}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -6,6 +6,7 @@ interface ResourceBarProps {
|
|||||||
resources: Resource;
|
resources: Resource;
|
||||||
runestones: number;
|
runestones: number;
|
||||||
prestigeCount: number;
|
prestigeCount: number;
|
||||||
|
transcendenceCount: number;
|
||||||
profileUrl: string;
|
profileUrl: string;
|
||||||
onEditProfile: () => void;
|
onEditProfile: () => void;
|
||||||
lastSavedAt: number | null;
|
lastSavedAt: number | null;
|
||||||
@@ -29,6 +30,7 @@ export const ResourceBar = ({
|
|||||||
resources,
|
resources,
|
||||||
runestones,
|
runestones,
|
||||||
prestigeCount,
|
prestigeCount,
|
||||||
|
transcendenceCount,
|
||||||
profileUrl,
|
profileUrl,
|
||||||
onEditProfile,
|
onEditProfile,
|
||||||
lastSavedAt,
|
lastSavedAt,
|
||||||
@@ -63,6 +65,11 @@ export const ResourceBar = ({
|
|||||||
<span className="resource-value">{formatNumber(runestones)}</span>
|
<span className="resource-value">{formatNumber(runestones)}</span>
|
||||||
<span className="resource-label">Runestones</span>
|
<span className="resource-label">Runestones</span>
|
||||||
</div>
|
</div>
|
||||||
|
{transcendenceCount > 0 && (
|
||||||
|
<div className="transcendence-badge">
|
||||||
|
🌌 Transcendence {transcendenceCount}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
{prestigeCount > 0 && (
|
{prestigeCount > 0 && (
|
||||||
<div className="prestige-badge">
|
<div className="prestige-badge">
|
||||||
⭐ Prestige {prestigeCount}
|
⭐ Prestige {prestigeCount}
|
||||||
|
|||||||
@@ -8,11 +8,13 @@ import {
|
|||||||
useState,
|
useState,
|
||||||
} from "react";
|
} from "react";
|
||||||
import {
|
import {
|
||||||
|
buyEchoUpgrade as buyEchoUpgradeApi,
|
||||||
buyPrestigeUpgrade as buyPrestigeUpgradeApi,
|
buyPrestigeUpgrade as buyPrestigeUpgradeApi,
|
||||||
challengeBoss as challengeBossApi,
|
challengeBoss as challengeBossApi,
|
||||||
loadGame,
|
loadGame,
|
||||||
prestige as prestigeApi,
|
prestige as prestigeApi,
|
||||||
saveGame,
|
saveGame,
|
||||||
|
transcend as transcendApi,
|
||||||
} from "../api/client.js";
|
} from "../api/client.js";
|
||||||
import { CODEX_ENTRIES } from "../data/codex.js";
|
import { CODEX_ENTRIES } from "../data/codex.js";
|
||||||
import { RESOURCE_CAP, applyTick, calculateClickPower } from "../engine/tick.js";
|
import { RESOURCE_CAP, applyTick, calculateClickPower } from "../engine/tick.js";
|
||||||
@@ -81,6 +83,10 @@ interface GameContextValue {
|
|||||||
newCodexEntryIds: string[];
|
newCodexEntryIds: string[];
|
||||||
/** Remove a codex entry ID from the notification queue */
|
/** Remove a codex entry ID from the notification queue */
|
||||||
dismissCodexEntry: (id: string) => void;
|
dismissCodexEntry: (id: string) => void;
|
||||||
|
/** Perform a transcendence — nuclear reset, earning echoes */
|
||||||
|
transcend: (characterName: string) => Promise<{ echoes: number; newTranscendenceCount: number }>;
|
||||||
|
/** Buy an echo upgrade from the transcendence shop */
|
||||||
|
buyEchoUpgrade: (upgradeId: string) => Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const GameContext = createContext<GameContextValue | null>(null);
|
const GameContext = createContext<GameContextValue | null>(null);
|
||||||
@@ -531,6 +537,36 @@ export const GameProvider = ({ children }: { children: React.ReactNode }): React
|
|||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const transcend = useCallback(async (characterName: string) => {
|
||||||
|
const result = await transcendApi({ characterName });
|
||||||
|
await reload();
|
||||||
|
return result;
|
||||||
|
}, [reload]);
|
||||||
|
|
||||||
|
const buyEchoUpgrade = useCallback(async (upgradeId: string) => {
|
||||||
|
try {
|
||||||
|
const result = await buyEchoUpgradeApi({ upgradeId });
|
||||||
|
setState((prev) => {
|
||||||
|
if (!prev || !prev.transcendence) return prev;
|
||||||
|
return {
|
||||||
|
...prev,
|
||||||
|
transcendence: {
|
||||||
|
...prev.transcendence,
|
||||||
|
echoes: result.echoesRemaining,
|
||||||
|
purchasedUpgradeIds: result.purchasedUpgradeIds,
|
||||||
|
echoIncomeMultiplier: result.echoIncomeMultiplier,
|
||||||
|
echoCombatMultiplier: result.echoCombatMultiplier,
|
||||||
|
echoPrestigeThresholdMultiplier: result.echoPrestigeThresholdMultiplier,
|
||||||
|
echoPrestigeRunestoneMultiplier: result.echoPrestigeRunestoneMultiplier,
|
||||||
|
echoMetaMultiplier: result.echoMetaMultiplier,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
// Silently ignore server errors
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
const toggleAutoPrestige = useCallback(() => {
|
const toggleAutoPrestige = useCallback(() => {
|
||||||
setState((prev) => {
|
setState((prev) => {
|
||||||
if (!prev) return prev;
|
if (!prev) return prev;
|
||||||
@@ -713,6 +749,8 @@ export const GameProvider = ({ children }: { children: React.ReactNode }): React
|
|||||||
toggleAutoPrestige,
|
toggleAutoPrestige,
|
||||||
newCodexEntryIds,
|
newCodexEntryIds,
|
||||||
dismissCodexEntry,
|
dismissCodexEntry,
|
||||||
|
transcend,
|
||||||
|
buyEchoUpgrade,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
|||||||
@@ -0,0 +1,142 @@
|
|||||||
|
import type { TranscendenceUpgrade } from "@elysium/types";
|
||||||
|
|
||||||
|
export const DEFAULT_TRANSCENDENCE_UPGRADES: TranscendenceUpgrade[] = [
|
||||||
|
// ── Income multipliers ──────────────────────────────────────────────────────
|
||||||
|
{
|
||||||
|
id: "echo_income_1",
|
||||||
|
name: "Whisper of Power",
|
||||||
|
description: "The echoes of past runs linger, amplifying your guild's income by 25%.",
|
||||||
|
category: "income",
|
||||||
|
cost: 5,
|
||||||
|
multiplier: 1.25,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "echo_income_2",
|
||||||
|
name: "Resonance",
|
||||||
|
description: "Your transcendent experience resonates through your guild, boosting income by 50%.",
|
||||||
|
category: "income",
|
||||||
|
cost: 10,
|
||||||
|
multiplier: 1.5,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "echo_income_3",
|
||||||
|
name: "Harmonic Surge",
|
||||||
|
description: "The harmony of multiple timelines surges through your guild, doubling its income.",
|
||||||
|
category: "income",
|
||||||
|
cost: 20,
|
||||||
|
multiplier: 2.0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "echo_income_4",
|
||||||
|
name: "Ethereal Overflow",
|
||||||
|
description: "Ethereal energy overflows from your transcendence, tripling your guild's income.",
|
||||||
|
category: "income",
|
||||||
|
cost: 40,
|
||||||
|
multiplier: 3.0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "echo_income_5",
|
||||||
|
name: "Infinite Chorus",
|
||||||
|
description: "The infinite chorus of every run you've ever played amplifies your guild fivefold.",
|
||||||
|
category: "income",
|
||||||
|
cost: 80,
|
||||||
|
multiplier: 5.0,
|
||||||
|
},
|
||||||
|
|
||||||
|
// ── Combat multipliers ──────────────────────────────────────────────────────
|
||||||
|
{
|
||||||
|
id: "echo_combat_1",
|
||||||
|
name: "Battle-Hardened",
|
||||||
|
description: "Memories of countless battles harden your adventurers, increasing party DPS by 25%.",
|
||||||
|
category: "combat",
|
||||||
|
cost: 5,
|
||||||
|
multiplier: 1.25,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "echo_combat_2",
|
||||||
|
name: "Veteran's Edge",
|
||||||
|
description: "Veterans of transcendence know how to fight smarter, boosting party DPS by 50%.",
|
||||||
|
category: "combat",
|
||||||
|
cost: 15,
|
||||||
|
multiplier: 1.5,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "echo_combat_3",
|
||||||
|
name: "Transcendent Warrior",
|
||||||
|
description: "Your warriors carry the strength of every fallen timeline, doubling party DPS.",
|
||||||
|
category: "combat",
|
||||||
|
cost: 35,
|
||||||
|
multiplier: 2.0,
|
||||||
|
},
|
||||||
|
|
||||||
|
// ── Prestige threshold reductions ──────────────────────────────────────────
|
||||||
|
{
|
||||||
|
id: "echo_prestige_threshold_1",
|
||||||
|
name: "Accelerated Path",
|
||||||
|
description: "Experience from past lives shortens the road to prestige — threshold reduced by 10%.",
|
||||||
|
category: "prestige_threshold",
|
||||||
|
cost: 8,
|
||||||
|
multiplier: 0.9,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "echo_prestige_threshold_2",
|
||||||
|
name: "Shortcut Through Time",
|
||||||
|
description: "You've walked this path so many times you know every shortcut — threshold reduced by 20%.",
|
||||||
|
category: "prestige_threshold",
|
||||||
|
cost: 20,
|
||||||
|
multiplier: 0.8,
|
||||||
|
},
|
||||||
|
|
||||||
|
// ── Prestige runestone multipliers ─────────────────────────────────────────
|
||||||
|
{
|
||||||
|
id: "echo_prestige_runestones_1",
|
||||||
|
name: "Runic Attunement",
|
||||||
|
description: "Transcendent insight attunes you to the runestones, earning 50% more per prestige.",
|
||||||
|
category: "prestige_runestones",
|
||||||
|
cost: 8,
|
||||||
|
multiplier: 1.5,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "echo_prestige_runestones_2",
|
||||||
|
name: "Master Runesmith",
|
||||||
|
description: "You have mastered the art of runestone crafting, doubling your prestige runestone yield.",
|
||||||
|
category: "prestige_runestones",
|
||||||
|
cost: 20,
|
||||||
|
multiplier: 2.0,
|
||||||
|
},
|
||||||
|
|
||||||
|
// ── Echo meta multipliers ───────────────────────────────────────────────────
|
||||||
|
{
|
||||||
|
id: "echo_meta_1",
|
||||||
|
name: "Resonant Awakening",
|
||||||
|
description: "Your transcendence resonates deeper, amplifying future echo yields by 25%.",
|
||||||
|
category: "echo_meta",
|
||||||
|
cost: 10,
|
||||||
|
multiplier: 1.25,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "echo_meta_2",
|
||||||
|
name: "Transcendent Loop",
|
||||||
|
description: "Each loop of existence makes the next more powerful — future echo yields +50%.",
|
||||||
|
category: "echo_meta",
|
||||||
|
cost: 25,
|
||||||
|
multiplier: 1.5,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "echo_meta_3",
|
||||||
|
name: "Infinite Spiral",
|
||||||
|
description: "You have mastered the infinite spiral of transcendence, doubling all future echo yields.",
|
||||||
|
category: "echo_meta",
|
||||||
|
cost: 50,
|
||||||
|
multiplier: 2.0,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
export const TRANSCENDENCE_UPGRADES = DEFAULT_TRANSCENDENCE_UPGRADES;
|
||||||
|
|
||||||
|
export const TRANSCENDENCE_UPGRADE_CATEGORY_LABELS: Record<string, string> = {
|
||||||
|
income: "✨ Income Multipliers",
|
||||||
|
combat: "⚔️ Combat Multipliers",
|
||||||
|
prestige_threshold: "🎯 Prestige Quality of Life — Threshold",
|
||||||
|
prestige_runestones: "🔮 Prestige Quality of Life — Runestones",
|
||||||
|
echo_meta: "🌌 Echo Meta Upgrades",
|
||||||
|
};
|
||||||
@@ -64,6 +64,7 @@ export const applyTick = (state: GameState, deltaSeconds: number): GameState =>
|
|||||||
const runestonesIncome = state.prestige.runestonesIncomeMultiplier ?? 1;
|
const runestonesIncome = state.prestige.runestonesIncomeMultiplier ?? 1;
|
||||||
const runestonesEssence = state.prestige.runestonesEssenceMultiplier ?? 1;
|
const runestonesEssence = state.prestige.runestonesEssenceMultiplier ?? 1;
|
||||||
const runestonesCrystal = state.prestige.runestonesCrystalMultiplier ?? 1;
|
const runestonesCrystal = state.prestige.runestonesCrystalMultiplier ?? 1;
|
||||||
|
const echoIncome = state.transcendence?.echoIncomeMultiplier ?? 1;
|
||||||
|
|
||||||
let goldGained = 0;
|
let goldGained = 0;
|
||||||
let essenceGained = 0;
|
let essenceGained = 0;
|
||||||
@@ -90,6 +91,7 @@ export const applyTick = (state: GameState, deltaSeconds: number): GameState =>
|
|||||||
upgradeMultiplier *
|
upgradeMultiplier *
|
||||||
prestige *
|
prestige *
|
||||||
runestonesIncome *
|
runestonesIncome *
|
||||||
|
echoIncome *
|
||||||
equipmentGoldMultiplier *
|
equipmentGoldMultiplier *
|
||||||
setGoldMultiplier *
|
setGoldMultiplier *
|
||||||
deltaSeconds;
|
deltaSeconds;
|
||||||
@@ -282,12 +284,14 @@ export const calculateClickPower = (state: GameState): number => {
|
|||||||
const setClickMultiplier = computeSetBonuses(equippedItems.map((e) => e.id), EQUIPMENT_SETS).clickMultiplier;
|
const setClickMultiplier = computeSetBonuses(equippedItems.map((e) => e.id), EQUIPMENT_SETS).clickMultiplier;
|
||||||
|
|
||||||
const runestonesClick = state.prestige.runestonesClickMultiplier ?? 1;
|
const runestonesClick = state.prestige.runestonesClickMultiplier ?? 1;
|
||||||
|
const echoIncome = state.transcendence?.echoIncomeMultiplier ?? 1;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
state.baseClickPower *
|
state.baseClickPower *
|
||||||
clickMultiplier *
|
clickMultiplier *
|
||||||
state.prestige.productionMultiplier *
|
state.prestige.productionMultiplier *
|
||||||
runestonesClick *
|
runestonesClick *
|
||||||
|
echoIncome *
|
||||||
equipmentClickMultiplier *
|
equipmentClickMultiplier *
|
||||||
setClickMultiplier
|
setClickMultiplier
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -2320,3 +2320,124 @@ body {
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* ── Transcendence ─────────────────────────────────────────────────────── */
|
||||||
|
|
||||||
|
.transcendence-badge {
|
||||||
|
background: linear-gradient(135deg, #4c1d95, #7c3aed);
|
||||||
|
border-radius: 999px;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
font-weight: 600;
|
||||||
|
padding: 0.25rem 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.transcendence-panel .transcendence-intro {
|
||||||
|
color: var(--colour-text-muted);
|
||||||
|
font-size: 0.95rem;
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.transcendence-status {
|
||||||
|
background: var(--colour-surface);
|
||||||
|
border: 1px solid #7c3aed;
|
||||||
|
border-radius: var(--radius);
|
||||||
|
margin: 1rem 0;
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.transcendence-status p {
|
||||||
|
margin: 0.25rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.echo-preview {
|
||||||
|
color: #a78bfa;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-top: 0.5rem !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.echo-meta-bonus {
|
||||||
|
color: var(--colour-text-muted);
|
||||||
|
font-size: 0.85rem;
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
|
||||||
|
.transcendence-locked {
|
||||||
|
background: rgba(124, 58, 237, 0.1);
|
||||||
|
border: 1px solid #7c3aed;
|
||||||
|
border-radius: var(--radius);
|
||||||
|
padding: 1rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.transcendence-hint {
|
||||||
|
color: var(--colour-text-muted);
|
||||||
|
font-size: 0.85rem;
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.transcendence-button {
|
||||||
|
background: linear-gradient(135deg, #4c1d95, #7c3aed);
|
||||||
|
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%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.transcendence-button:hover:not(:disabled) {
|
||||||
|
opacity: 0.85;
|
||||||
|
}
|
||||||
|
|
||||||
|
.transcendence-button:disabled {
|
||||||
|
cursor: not-allowed;
|
||||||
|
opacity: 0.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.echo-shop {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.echo-shop-description {
|
||||||
|
color: var(--colour-text-muted);
|
||||||
|
font-size: 0.9rem;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.echo-upgrade-card {
|
||||||
|
border-color: #7c3aed !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.echo-upgrade-card.purchased {
|
||||||
|
border-color: #6d28d9 !important;
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.echo-buy-button {
|
||||||
|
background: linear-gradient(135deg, #4c1d95, #7c3aed);
|
||||||
|
border: none;
|
||||||
|
border-radius: var(--radius);
|
||||||
|
color: #fff;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 600;
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
transition: opacity 0.2s;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.echo-buy-button:hover:not(:disabled) {
|
||||||
|
opacity: 0.85;
|
||||||
|
}
|
||||||
|
|
||||||
|
.echo-buy-button:disabled {
|
||||||
|
cursor: not-allowed;
|
||||||
|
opacity: 0.4;
|
||||||
|
}
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ export type {
|
|||||||
AuthResponse,
|
AuthResponse,
|
||||||
BossChallengeRequest,
|
BossChallengeRequest,
|
||||||
BossChallengeResponse,
|
BossChallengeResponse,
|
||||||
|
BuyEchoUpgradeRequest,
|
||||||
|
BuyEchoUpgradeResponse,
|
||||||
BuyPrestigeUpgradeRequest,
|
BuyPrestigeUpgradeRequest,
|
||||||
BuyPrestigeUpgradeResponse,
|
BuyPrestigeUpgradeResponse,
|
||||||
GiteaRelease,
|
GiteaRelease,
|
||||||
@@ -21,6 +23,8 @@ export type {
|
|||||||
PublicProfileResponse,
|
PublicProfileResponse,
|
||||||
SaveRequest,
|
SaveRequest,
|
||||||
SaveResponse,
|
SaveResponse,
|
||||||
|
TranscendenceRequest,
|
||||||
|
TranscendenceResponse,
|
||||||
UpdateProfileRequest,
|
UpdateProfileRequest,
|
||||||
UpdateProfileResponse,
|
UpdateProfileResponse,
|
||||||
} from "./interfaces/Api.js";
|
} from "./interfaces/Api.js";
|
||||||
@@ -59,3 +63,8 @@ export type {
|
|||||||
export type { Zone, ZoneStatus } from "./interfaces/Zone.js";
|
export type { Zone, ZoneStatus } from "./interfaces/Zone.js";
|
||||||
export type { NumberFormat, ProfileSettings } from "./interfaces/ProfileSettings.js";
|
export type { NumberFormat, ProfileSettings } from "./interfaces/ProfileSettings.js";
|
||||||
export { DEFAULT_PROFILE_SETTINGS } from "./interfaces/ProfileSettings.js";
|
export { DEFAULT_PROFILE_SETTINGS } from "./interfaces/ProfileSettings.js";
|
||||||
|
export type {
|
||||||
|
TranscendenceData,
|
||||||
|
TranscendenceUpgrade,
|
||||||
|
TranscendenceUpgradeCategory,
|
||||||
|
} from "./interfaces/Transcendence.js";
|
||||||
|
|||||||
@@ -127,6 +127,29 @@ export interface UpdateProfileResponse {
|
|||||||
profileSettings: ProfileSettings;
|
profileSettings: ProfileSettings;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface TranscendenceRequest {
|
||||||
|
characterName: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TranscendenceResponse {
|
||||||
|
echoes: number;
|
||||||
|
newTranscendenceCount: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BuyEchoUpgradeRequest {
|
||||||
|
upgradeId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BuyEchoUpgradeResponse {
|
||||||
|
echoesRemaining: number;
|
||||||
|
purchasedUpgradeIds: string[];
|
||||||
|
echoIncomeMultiplier: number;
|
||||||
|
echoCombatMultiplier: number;
|
||||||
|
echoPrestigeThresholdMultiplier: number;
|
||||||
|
echoPrestigeRunestoneMultiplier: number;
|
||||||
|
echoMetaMultiplier: number;
|
||||||
|
}
|
||||||
|
|
||||||
export interface ApiError {
|
export interface ApiError {
|
||||||
error: string;
|
error: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import type { Adventurer } from "./Adventurer.js";
|
|||||||
import type { Boss } from "./Boss.js";
|
import type { Boss } from "./Boss.js";
|
||||||
import type { CodexState } from "./Codex.js";
|
import type { CodexState } from "./Codex.js";
|
||||||
import type { DailyChallengeState } from "./DailyChallenge.js";
|
import type { DailyChallengeState } from "./DailyChallenge.js";
|
||||||
|
import type { TranscendenceData } from "./Transcendence.js";
|
||||||
import type { Equipment } from "./Equipment.js";
|
import type { Equipment } from "./Equipment.js";
|
||||||
import type { Player } from "./Player.js";
|
import type { Player } from "./Player.js";
|
||||||
import type { PrestigeData } from "./Prestige.js";
|
import type { PrestigeData } from "./Prestige.js";
|
||||||
@@ -30,4 +31,6 @@ export interface GameState {
|
|||||||
dailyChallenges?: DailyChallengeState;
|
dailyChallenges?: DailyChallengeState;
|
||||||
/** Lore codex unlock state — optional for backwards compatibility with old saves */
|
/** Lore codex unlock state — optional for backwards compatibility with old saves */
|
||||||
codex?: CodexState;
|
codex?: CodexState;
|
||||||
|
/** Transcendence (second prestige layer) state — optional for backwards compatibility */
|
||||||
|
transcendence?: TranscendenceData;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
export type TranscendenceUpgradeCategory =
|
||||||
|
| "income"
|
||||||
|
| "combat"
|
||||||
|
| "prestige_threshold"
|
||||||
|
| "prestige_runestones"
|
||||||
|
| "echo_meta";
|
||||||
|
|
||||||
|
export interface TranscendenceUpgrade {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
category: TranscendenceUpgradeCategory;
|
||||||
|
/** Echo cost to purchase */
|
||||||
|
cost: number;
|
||||||
|
/** Multiplicative effect of this upgrade */
|
||||||
|
multiplier: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TranscendenceData {
|
||||||
|
/** Number of times the player has transcended */
|
||||||
|
count: number;
|
||||||
|
/** Echoes accumulated across all transcendences */
|
||||||
|
echoes: number;
|
||||||
|
/** IDs of echo upgrades purchased with echoes */
|
||||||
|
purchasedUpgradeIds: string[];
|
||||||
|
/** Pre-computed: multiplier applied to all passive gold income */
|
||||||
|
echoIncomeMultiplier: number;
|
||||||
|
/** Pre-computed: multiplier applied to party DPS in boss fights */
|
||||||
|
echoCombatMultiplier: number;
|
||||||
|
/** Pre-computed: multiplier applied to the prestige gold threshold (< 1 lowers requirement) */
|
||||||
|
echoPrestigeThresholdMultiplier: number;
|
||||||
|
/** Pre-computed: multiplier applied to runestones earned per prestige */
|
||||||
|
echoPrestigeRunestoneMultiplier: number;
|
||||||
|
/** Pre-computed: multiplier applied to echo yield on future transcendences */
|
||||||
|
echoMetaMultiplier: number;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user