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!
|
||||
|
||||
- [ ] **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.
|
||||
|
||||
@@ -46,4 +46,4 @@ A running list of planned features and content additions. Strike through items a
|
||||
6. ~~Equipment set bonuses~~ ✅
|
||||
7. ~~Auto-prestige toggle~~ ✅
|
||||
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 { gameRouter } from "./routes/game.js";
|
||||
import { prestigeRouter } from "./routes/prestige.js";
|
||||
import { transcendenceRouter } from "./routes/transcendence.js";
|
||||
import { profileRouter } from "./routes/profile.js";
|
||||
|
||||
const app = new Hono();
|
||||
@@ -26,6 +27,7 @@ app.route("/auth", authRouter);
|
||||
app.route("/game", gameRouter);
|
||||
app.route("/boss", bossRouter);
|
||||
app.route("/prestige", prestigeRouter);
|
||||
app.route("/transcendence", transcendenceRouter);
|
||||
app.route("/profile", profileRouter);
|
||||
|
||||
app.get("/health", (context) => context.json({ status: "ok" }));
|
||||
|
||||
@@ -59,7 +59,8 @@ const calculatePartyStats = (
|
||||
partyMaxHp += adventurer.level * 50 * adventurer.count;
|
||||
}
|
||||
|
||||
partyDPS *= equipmentCombatMultiplier * setCombatMultiplier;
|
||||
const echoCombatMultiplier = state.transcendence?.echoCombatMultiplier ?? 1;
|
||||
partyDPS *= equipmentCombatMultiplier * setCombatMultiplier * echoCombatMultiplier;
|
||||
|
||||
return { partyDPS, partyMaxHp };
|
||||
};
|
||||
|
||||
@@ -290,7 +290,19 @@ const validateAndSanitize = (incoming: GameState, previous: GameState): GameStat
|
||||
const 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>();
|
||||
|
||||
@@ -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.
|
||||
* Formula: BASE * SCALE_FACTOR^prestigeCount — each prestige makes the next threshold harder.
|
||||
*/
|
||||
export const calculatePrestigeThreshold = (prestigeCount: number): number =>
|
||||
BASE_PRESTIGE_GOLD_THRESHOLD * Math.pow(THRESHOLD_SCALE_FACTOR, prestigeCount);
|
||||
export const calculatePrestigeThreshold = (prestigeCount: number, thresholdMultiplier = 1): number =>
|
||||
BASE_PRESTIGE_GOLD_THRESHOLD * Math.pow(THRESHOLD_SCALE_FACTOR, prestigeCount) * thresholdMultiplier;
|
||||
|
||||
export const isEligibleForPrestige = (state: GameState): boolean =>
|
||||
state.player.totalGoldEarned >= calculatePrestigeThreshold(state.prestige.count);
|
||||
export const isEligibleForPrestige = (state: GameState): boolean => {
|
||||
const thresholdMultiplier = state.transcendence?.echoPrestigeThresholdMultiplier ?? 1;
|
||||
return state.player.totalGoldEarned >= calculatePrestigeThreshold(state.prestige.count, thresholdMultiplier);
|
||||
};
|
||||
|
||||
const getCategoryMultiplier = (
|
||||
purchasedUpgradeIds: string[],
|
||||
@@ -52,12 +54,13 @@ export const calculateRunestones = (
|
||||
totalGoldEarned: number,
|
||||
prestigeCount: number,
|
||||
purchasedUpgradeIds: string[],
|
||||
echoRunestoneMultiplier = 1,
|
||||
): number => {
|
||||
const threshold = calculatePrestigeThreshold(prestigeCount);
|
||||
const base =
|
||||
Math.floor(Math.sqrt(totalGoldEarned / threshold)) * RUNESTONES_PER_PRESTIGE_LEVEL;
|
||||
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,
|
||||
characterName: string,
|
||||
): { newState: GameState; newPrestigeData: PrestigeData; runestonesEarned: number; milestoneRunestones: number } => {
|
||||
const echoRunestoneMultiplier = currentState.transcendence?.echoPrestigeRunestoneMultiplier ?? 1;
|
||||
const runestonesEarned = calculateRunestones(
|
||||
currentState.player.totalGoldEarned,
|
||||
currentState.prestige.count,
|
||||
currentState.prestige.purchasedUpgradeIds,
|
||||
echoRunestoneMultiplier,
|
||||
);
|
||||
const newPrestigeCount = currentState.prestige.count + 1;
|
||||
const { purchasedUpgradeIds } = currentState.prestige;
|
||||
@@ -113,6 +118,8 @@ export const buildPostPrestigeState = (
|
||||
lastTickAt: Date.now(),
|
||||
// Codex lore persists across prestiges — players keep their discovered entries
|
||||
...(currentState.codex ? { codex: currentState.codex } : {}),
|
||||
// Transcendence data is permanent — never wiped by prestige
|
||||
...(currentState.transcendence ? { transcendence: currentState.transcendence } : {}),
|
||||
};
|
||||
|
||||
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,
|
||||
BossChallengeRequest,
|
||||
BossChallengeResponse,
|
||||
BuyEchoUpgradeRequest,
|
||||
BuyEchoUpgradeResponse,
|
||||
BuyPrestigeUpgradeRequest,
|
||||
BuyPrestigeUpgradeResponse,
|
||||
LoadResponse,
|
||||
@@ -11,6 +13,8 @@ import type {
|
||||
PublicProfileResponse,
|
||||
SaveRequest,
|
||||
SaveResponse,
|
||||
TranscendenceRequest,
|
||||
TranscendenceResponse,
|
||||
UpdateProfileRequest,
|
||||
UpdateProfileResponse,
|
||||
} from "@elysium/types";
|
||||
@@ -91,6 +95,20 @@ export const buyPrestigeUpgrade = async (
|
||||
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 (
|
||||
discordId: string,
|
||||
): Promise<PublicProfileResponse> =>
|
||||
|
||||
@@ -59,6 +59,10 @@ const HOW_TO_PLAY = [
|
||||
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.",
|
||||
},
|
||||
{
|
||||
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 =>
|
||||
|
||||
@@ -14,12 +14,13 @@ import { EditProfileModal } from "./EditProfileModal.js";
|
||||
import { EquipmentPanel } from "./EquipmentPanel.js";
|
||||
import { OfflineModal } from "./OfflineModal.js";
|
||||
import { PrestigePanel } from "./PrestigePanel.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" | "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 }[] = [
|
||||
{ id: "adventurers", label: "⚔️ Adventurers" },
|
||||
@@ -29,6 +30,7 @@ const BASE_TABS: { id: Tab; label: string }[] = [
|
||||
{ id: "equipment", label: "🗡️ Equipment" },
|
||||
{ id: "achievements", label: "🏆 Achievements" },
|
||||
{ id: "prestige", label: "⭐ Prestige" },
|
||||
{ id: "transcendence", label: "🌌 Transcendence" },
|
||||
{ id: "statistics", label: "📊 Statistics" },
|
||||
{ id: "daily", label: "📅 Daily" },
|
||||
{ id: "codex", label: "📖 Codex" },
|
||||
@@ -66,6 +68,7 @@ export const GameLayout = (): React.JSX.Element => {
|
||||
resources={state.resources}
|
||||
runestones={state.prestige.runestones}
|
||||
prestigeCount={state.prestige.count}
|
||||
transcendenceCount={state.transcendence?.count ?? 0}
|
||||
profileUrl={profileUrl}
|
||||
onEditProfile={() => { setEditingProfile(true); }}
|
||||
lastSavedAt={lastSavedAt}
|
||||
@@ -112,6 +115,7 @@ export const GameLayout = (): React.JSX.Element => {
|
||||
{activeTab === "equipment" && <EquipmentPanel />}
|
||||
{activeTab === "achievements" && <AchievementPanel />}
|
||||
{activeTab === "prestige" && <PrestigePanel />}
|
||||
{activeTab === "transcendence" && <TranscendencePanel />}
|
||||
{activeTab === "statistics" && <StatisticsPanel />}
|
||||
{activeTab === "daily" && <DailyChallengePanel />}
|
||||
{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;
|
||||
runestones: number;
|
||||
prestigeCount: number;
|
||||
transcendenceCount: number;
|
||||
profileUrl: string;
|
||||
onEditProfile: () => void;
|
||||
lastSavedAt: number | null;
|
||||
@@ -29,6 +30,7 @@ export const ResourceBar = ({
|
||||
resources,
|
||||
runestones,
|
||||
prestigeCount,
|
||||
transcendenceCount,
|
||||
profileUrl,
|
||||
onEditProfile,
|
||||
lastSavedAt,
|
||||
@@ -63,6 +65,11 @@ export const ResourceBar = ({
|
||||
<span className="resource-value">{formatNumber(runestones)}</span>
|
||||
<span className="resource-label">Runestones</span>
|
||||
</div>
|
||||
{transcendenceCount > 0 && (
|
||||
<div className="transcendence-badge">
|
||||
🌌 Transcendence {transcendenceCount}
|
||||
</div>
|
||||
)}
|
||||
{prestigeCount > 0 && (
|
||||
<div className="prestige-badge">
|
||||
⭐ Prestige {prestigeCount}
|
||||
|
||||
@@ -8,11 +8,13 @@ import {
|
||||
useState,
|
||||
} from "react";
|
||||
import {
|
||||
buyEchoUpgrade as buyEchoUpgradeApi,
|
||||
buyPrestigeUpgrade as buyPrestigeUpgradeApi,
|
||||
challengeBoss as challengeBossApi,
|
||||
loadGame,
|
||||
prestige as prestigeApi,
|
||||
saveGame,
|
||||
transcend as transcendApi,
|
||||
} from "../api/client.js";
|
||||
import { CODEX_ENTRIES } from "../data/codex.js";
|
||||
import { RESOURCE_CAP, applyTick, calculateClickPower } from "../engine/tick.js";
|
||||
@@ -81,6 +83,10 @@ interface GameContextValue {
|
||||
newCodexEntryIds: string[];
|
||||
/** Remove a codex entry ID from the notification queue */
|
||||
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);
|
||||
@@ -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(() => {
|
||||
setState((prev) => {
|
||||
if (!prev) return prev;
|
||||
@@ -713,6 +749,8 @@ export const GameProvider = ({ children }: { children: React.ReactNode }): React
|
||||
toggleAutoPrestige,
|
||||
newCodexEntryIds,
|
||||
dismissCodexEntry,
|
||||
transcend,
|
||||
buyEchoUpgrade,
|
||||
}}
|
||||
>
|
||||
{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 runestonesEssence = state.prestige.runestonesEssenceMultiplier ?? 1;
|
||||
const runestonesCrystal = state.prestige.runestonesCrystalMultiplier ?? 1;
|
||||
const echoIncome = state.transcendence?.echoIncomeMultiplier ?? 1;
|
||||
|
||||
let goldGained = 0;
|
||||
let essenceGained = 0;
|
||||
@@ -90,6 +91,7 @@ export const applyTick = (state: GameState, deltaSeconds: number): GameState =>
|
||||
upgradeMultiplier *
|
||||
prestige *
|
||||
runestonesIncome *
|
||||
echoIncome *
|
||||
equipmentGoldMultiplier *
|
||||
setGoldMultiplier *
|
||||
deltaSeconds;
|
||||
@@ -282,12 +284,14 @@ export const calculateClickPower = (state: GameState): number => {
|
||||
const setClickMultiplier = computeSetBonuses(equippedItems.map((e) => e.id), EQUIPMENT_SETS).clickMultiplier;
|
||||
|
||||
const runestonesClick = state.prestige.runestonesClickMultiplier ?? 1;
|
||||
const echoIncome = state.transcendence?.echoIncomeMultiplier ?? 1;
|
||||
|
||||
return (
|
||||
state.baseClickPower *
|
||||
clickMultiplier *
|
||||
state.prestige.productionMultiplier *
|
||||
runestonesClick *
|
||||
echoIncome *
|
||||
equipmentClickMultiplier *
|
||||
setClickMultiplier
|
||||
);
|
||||
|
||||
@@ -2320,3 +2320,124 @@ body {
|
||||
text-align: center;
|
||||
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,
|
||||
BossChallengeRequest,
|
||||
BossChallengeResponse,
|
||||
BuyEchoUpgradeRequest,
|
||||
BuyEchoUpgradeResponse,
|
||||
BuyPrestigeUpgradeRequest,
|
||||
BuyPrestigeUpgradeResponse,
|
||||
GiteaRelease,
|
||||
@@ -21,6 +23,8 @@ export type {
|
||||
PublicProfileResponse,
|
||||
SaveRequest,
|
||||
SaveResponse,
|
||||
TranscendenceRequest,
|
||||
TranscendenceResponse,
|
||||
UpdateProfileRequest,
|
||||
UpdateProfileResponse,
|
||||
} from "./interfaces/Api.js";
|
||||
@@ -59,3 +63,8 @@ export type {
|
||||
export type { Zone, ZoneStatus } from "./interfaces/Zone.js";
|
||||
export type { NumberFormat, ProfileSettings } 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;
|
||||
}
|
||||
|
||||
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 {
|
||||
error: string;
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import type { Adventurer } from "./Adventurer.js";
|
||||
import type { Boss } from "./Boss.js";
|
||||
import type { CodexState } from "./Codex.js";
|
||||
import type { DailyChallengeState } from "./DailyChallenge.js";
|
||||
import type { TranscendenceData } from "./Transcendence.js";
|
||||
import type { Equipment } from "./Equipment.js";
|
||||
import type { Player } from "./Player.js";
|
||||
import type { PrestigeData } from "./Prestige.js";
|
||||
@@ -30,4 +31,6 @@ export interface GameState {
|
||||
dailyChallenges?: DailyChallengeState;
|
||||
/** Lore codex unlock state — optional for backwards compatibility with old saves */
|
||||
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