generated from nhcarrigan/template
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
This commit is contained in:
@@ -1,88 +1,170 @@
|
||||
import type { GameState, TranscendenceData, TranscendenceUpgradeCategory } from "@elysium/types";
|
||||
import { INITIAL_GAME_STATE } from "../data/initialState.js";
|
||||
import { DEFAULT_TRANSCENDENCE_UPGRADES } from "../data/transcendenceUpgrades.js";
|
||||
/**
|
||||
* @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 FINAL_BOSS_ID = "the_absolute_one";
|
||||
/**
|
||||
* 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 ECHO_FORMULA_CONSTANT = 853;
|
||||
/**
|
||||
* Base constant used in the echo yield formula.
|
||||
*/
|
||||
const echoFormulaConstant = 853;
|
||||
|
||||
const getCategoryMultiplier = (
|
||||
purchasedIds: string[],
|
||||
purchasedIds: Array<string>,
|
||||
category: TranscendenceUpgradeCategory,
|
||||
): number =>
|
||||
DEFAULT_TRANSCENDENCE_UPGRADES
|
||||
.filter((u) => u.category === category && purchasedIds.includes(u.id))
|
||||
.reduce((mult, u) => mult * u.multiplier, 1);
|
||||
): number => {
|
||||
return defaultTranscendenceUpgrades.filter((upgrade) => {
|
||||
return upgrade.category === category && purchasedIds.includes(upgrade.id);
|
||||
}).reduce((mult, upgrade) => {
|
||||
return mult * upgrade.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"),
|
||||
});
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
export const isEligibleForTranscendence = (state: GameState): boolean =>
|
||||
state.bosses.some((b) => b.id === FINAL_BOSS_ID && b.status === "defeated");
|
||||
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
|
||||
* 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.
|
||||
*/
|
||||
export const calculateEchoes = (
|
||||
const calculateEchoes = (
|
||||
prestigeCount: number,
|
||||
echoMetaMultiplier: number,
|
||||
): number => {
|
||||
const safeCount = Math.max(prestigeCount, 1);
|
||||
return Math.floor((ECHO_FORMULA_CONSTANT / Math.sqrt(safeCount)) * echoMetaMultiplier);
|
||||
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.
|
||||
*/
|
||||
export const buildPostTranscendenceState = (
|
||||
const buildPostTranscendenceState = (
|
||||
currentState: GameState,
|
||||
characterName: string,
|
||||
): { newState: GameState; newTranscendenceData: TranscendenceData; echoesEarned: number } => {
|
||||
): {
|
||||
transcendenceState: GameState;
|
||||
transcendenceData: TranscendenceData;
|
||||
echoesEarned: number;
|
||||
} => {
|
||||
const previousTranscendence = currentState.transcendence;
|
||||
const echoMetaMultiplier = previousTranscendence?.echoMetaMultiplier ?? 1;
|
||||
|
||||
const echoesEarned = calculateEchoes(currentState.prestige.count, echoMetaMultiplier);
|
||||
const echoesEarned = calculateEchoes(
|
||||
currentState.prestige.count,
|
||||
echoMetaMultiplier,
|
||||
);
|
||||
const previousEchoes = previousTranscendence?.echoes ?? 0;
|
||||
const newCount = (previousTranscendence?.count ?? 0) + 1;
|
||||
const newPurchasedIds = previousTranscendence?.purchasedUpgradeIds ?? [];
|
||||
const updatedCount = (previousTranscendence?.count ?? 0) + 1;
|
||||
const updatedPurchasedIds = previousTranscendence?.purchasedUpgradeIds ?? [];
|
||||
|
||||
const newTranscendenceData: TranscendenceData = {
|
||||
count: newCount,
|
||||
echoes: previousEchoes + echoesEarned,
|
||||
purchasedUpgradeIds: newPurchasedIds,
|
||||
...computeTranscendenceMultipliers(newPurchasedIds),
|
||||
const transcendenceData: TranscendenceData = {
|
||||
count: updatedCount,
|
||||
echoes: previousEchoes + echoesEarned,
|
||||
purchasedUpgradeIds: updatedPurchasedIds,
|
||||
...computeTranscendenceMultipliers(updatedPurchasedIds),
|
||||
};
|
||||
|
||||
const freshState = INITIAL_GAME_STATE(currentState.player, characterName);
|
||||
const newState: GameState = {
|
||||
const freshState = initialGameState(currentState.player, characterName);
|
||||
const transcendenceState: GameState = {
|
||||
...freshState,
|
||||
lastTickAt: Date.now(),
|
||||
// Codex lore persists through all resets
|
||||
...(currentState.codex ? { codex: currentState.codex } : {}),
|
||||
// Transcendence data is permanent
|
||||
transcendence: newTranscendenceData,
|
||||
// Apotheosis data is eternal — never wiped by transcendence
|
||||
...(currentState.apotheosis ? { apotheosis: currentState.apotheosis } : {}),
|
||||
// Story chapter progress is permanent — survives all resets
|
||||
...(currentState.story ? { story: currentState.story } : {}),
|
||||
...buildPermanentSpreads(currentState, transcendenceData),
|
||||
};
|
||||
|
||||
return { newState, newTranscendenceData, echoesEarned };
|
||||
return { echoesEarned, transcendenceData, transcendenceState };
|
||||
};
|
||||
|
||||
export {
|
||||
buildPostTranscendenceState,
|
||||
calculateEchoes,
|
||||
computeTranscendenceMultipliers,
|
||||
isEligibleForTranscendence,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user