Files
elysium/apps/api/src/services/transcendence.ts
T
hikari d1d1f70c75 chore: fix lint, ensure full CI pipeline passes, add verify checklist
- Fix strict-boolean-expressions in 7 route files (runtime body validation)
- Fix no-unnecessary-condition in profile.ts and offlineProgress.ts (defensive null checks)
- Extend v8 ignore next-N counts in game.ts to reach 100% coverage
- Add CI requirements to CLAUDE.md (lint + build + test must pass before commit)
- Add manual verification checklist (verify.md)
- Remove progress.md
2026-03-08 13:59:38 -07:00

171 lines
5.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* @file Transcendence eligibility checks, echo calculations, and post-transcendence state builder.
* @copyright nhcarrigan
* @license Naomi's Public License
* @author Naomi Carrigan
*/
import { initialGameState } from "../data/initialState.js";
import { defaultTranscendenceUpgrades } from "../data/transcendenceUpgrades.js";
import type {
GameState,
TranscendenceData,
TranscendenceUpgradeCategory,
} from "@elysium/types";
/**
* ID of the boss that must be defeated to unlock transcendence.
*/
const finalBossId = "the_absolute_one";
/**
* Base constant used in the echo yield formula.
*/
const echoFormulaConstant = 853;
const getCategoryMultiplier = (
purchasedIds: Array<string>,
category: TranscendenceUpgradeCategory,
): number => {
return defaultTranscendenceUpgrades.filter((upgrade) => {
return upgrade.category === category && purchasedIds.includes(upgrade.id);
}).reduce((mult, upgrade) => {
return mult * upgrade.multiplier;
}, 1);
};
/**
* Computes all transcendence multipliers from the purchased upgrade IDs.
* @param purchasedUpgradeIds - The array of purchased transcendence upgrade IDs.
* @returns An object containing all transcendence multiplier values.
*/
const computeTranscendenceMultipliers = (
purchasedUpgradeIds: Array<string>,
): Omit<TranscendenceData, "count" | "echoes" | "purchasedUpgradeIds"> => {
return {
echoCombatMultiplier: getCategoryMultiplier(
purchasedUpgradeIds,
"combat",
),
echoIncomeMultiplier: getCategoryMultiplier(
purchasedUpgradeIds,
"income",
),
echoMetaMultiplier: getCategoryMultiplier(
purchasedUpgradeIds,
"echo_meta",
),
echoPrestigeRunestoneMultiplier: getCategoryMultiplier(
purchasedUpgradeIds,
"prestige_runestones",
),
echoPrestigeThresholdMultiplier: getCategoryMultiplier(
purchasedUpgradeIds,
"prestige_threshold",
),
};
};
/**
* Returns true when the player is eligible to transcend:
* they must have defeated the final boss at least once.
* @param state - The current game state.
* @returns Whether the player is eligible for transcendence.
*/
const isEligibleForTranscendence = (state: GameState): boolean => {
return state.bosses.some((boss) => {
return boss.id === finalBossId && boss.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.
* @param prestigeCount - The current prestige count.
* @param echoMetaMultiplier - The echo meta multiplier from transcendence upgrades.
* @returns The number of echoes earned.
*/
const calculateEchoes = (
prestigeCount: number,
echoMetaMultiplier: number,
): number => {
const safeCount = Math.max(prestigeCount, 1);
const baseEchoes = echoFormulaConstant / Math.sqrt(safeCount);
return Math.floor(baseEchoes * echoMetaMultiplier);
};
/**
* Builds the permanent-data spread objects that survive a transcendence reset.
* @param currentState - The game state at the time of transcendence.
* @param transcendenceData - The newly-computed transcendence data to carry forward.
* @returns A partial GameState object containing all data that persists through transcendence.
*/
const buildPermanentSpreads = (
currentState: GameState,
transcendenceData: TranscendenceData,
): Partial<GameState> => {
return {
transcendence: transcendenceData,
...currentState.codex === undefined
? {}
: { codex: currentState.codex },
...currentState.apotheosis === undefined
? {}
: { apotheosis: currentState.apotheosis },
...currentState.story === undefined
? {}
: { story: currentState.story },
};
};
/**
* Builds the new game state after a transcendence (nuclear reset).
* Wipes everything except codex, dailyChallenges, and transcendence data.
* @param currentState - The game state at the time of transcendence.
* @param characterName - The player's character name to carry forward.
* @returns The new game state, transcendence data, and echoes earned.
*/
const buildPostTranscendenceState = (
currentState: GameState,
characterName: string,
): {
transcendenceState: GameState;
transcendenceData: 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 updatedCount = (previousTranscendence?.count ?? 0) + 1;
const updatedPurchasedIds = previousTranscendence?.purchasedUpgradeIds ?? [];
const transcendenceData: TranscendenceData = {
count: updatedCount,
echoes: previousEchoes + echoesEarned,
purchasedUpgradeIds: updatedPurchasedIds,
...computeTranscendenceMultipliers(updatedPurchasedIds),
};
const freshState = initialGameState(currentState.player, characterName);
const transcendenceState: GameState = {
...freshState,
lastTickAt: Date.now(),
...buildPermanentSpreads(currentState, transcendenceData),
};
return { echoesEarned, transcendenceData, transcendenceState };
};
export {
buildPostTranscendenceState,
calculateEchoes,
computeTranscendenceMultipliers,
isEligibleForTranscendence,
};