generated from nhcarrigan/template
feat: add transcendence second prestige layer
Implements the full Transcendence system — the ultimate endgame mechanic, unlocked by defeating The Absolute One (requires Prestige 90). Nuclear reset model: wipes resources, prestige, runestones, upgrades, equipment, bosses, quests, zones, and achievements. Codex entries and lifetime profile stats are preserved. Transcendence data is permanent and accumulates across all future resets. Echo formula: floor(853 / sqrt(prestigeCount)) × echoMetaMultiplier Fewer prestiges = more Echoes, rewarding optimised play. 15 Echo upgrades across 5 categories: - Income multipliers (×1.25 → ×5): 5 tiers, cost 5–80 echoes - Combat multipliers (×1.25 → ×2): 3 tiers, cost 5–35 echoes - Prestige threshold reductions (×0.9, ×0.8): cost 8–20 echoes - Prestige runestone multipliers (×1.5, ×2): cost 8–20 echoes - Echo meta multipliers (×1.25 → ×2): cost 10–50 echoes New files: Transcendence.ts types, transcendence service, route, data files (API + web), TranscendencePanel.tsx component. Modified: GameState, Api, types/index, prestige service (carries transcendence through resets, applies echo multipliers), boss route (echoCombatMultiplier), game.ts anti-cheat (echo cap), tick.ts (echoIncomeMultiplier), GameContext, API client, GameLayout (new tab), ResourceBar (transcendence badge alongside prestige badge), styles.css, AboutPanel, IDEAS.md.
This commit is contained in:
@@ -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 };
|
||||
};
|
||||
Reference in New Issue
Block a user