Files
elysium/apps/api/src/services/enlightenment.ts
T
hikari 0d36b255ee feat: goddess API routes, services, and tests (chunk 4)
Add six new goddess-mode API routes (boss fight, consecration,
enlightenment, upgrade purchase, crafting, exploration) alongside
matching service modules and full test suites at 100% coverage.
2026-04-13 15:48:35 -07:00

138 lines
5.2 KiB
TypeScript

/**
* @file Enlightenment service handling eligibility checks and post-enlightenment state building.
* @copyright nhcarrigan
* @license Naomi's Public License
* @author Naomi Carrigan
*/
/* eslint-disable stylistic/max-len -- Service logic requires long lines */
import { defaultEnlightenmentUpgrades } from "../data/goddessEnlightenmentUpgrades.js";
import { initialGoddessState } from "../data/initialState.js";
import type { EnlightenmentData, GameState } from "@elysium/types";
/**
* ID of the final goddess boss — must be defeated to unlock Enlightenment.
*/
const finalGoddessBossId = "divine_heart_sovereign";
const getCategoryMultiplier = (
purchasedIds: Array<string>,
category: string,
): number => {
return defaultEnlightenmentUpgrades.filter((upgrade) => {
return upgrade.category === category && purchasedIds.includes(upgrade.id);
}).reduce((mult, upgrade) => {
return mult * upgrade.multiplier;
}, 1);
};
/**
* Computes all five stardust multipliers from the purchased enlightenment upgrade IDs.
* @param purchasedUpgradeIds - The array of purchased enlightenment upgrade IDs.
* @returns An object containing all five stardust multiplier values.
*/
const computeEnlightenmentMultipliers = (
purchasedUpgradeIds: Array<string>,
): Omit<EnlightenmentData, "count" | "stardust" | "purchasedUpgradeIds"> => {
return {
stardustCombatMultiplier: getCategoryMultiplier(purchasedUpgradeIds, "combat"),
stardustConsecrationDivinityMultiplier: getCategoryMultiplier(purchasedUpgradeIds, "consecration_divinity"),
stardustConsecrationThresholdMultiplier: getCategoryMultiplier(purchasedUpgradeIds, "consecration_threshold"),
stardustMetaMultiplier: getCategoryMultiplier(purchasedUpgradeIds, "stardust_meta"),
stardustPrayersMultiplier: getCategoryMultiplier(purchasedUpgradeIds, "prayers"),
};
};
/**
* Returns true when the player is eligible for Enlightenment:
* they must have defeated the final goddess boss at least once.
* @param state - The current game state.
* @returns Whether the player is eligible for Enlightenment.
*/
const isEligibleForEnlightenment = (state: GameState): boolean => {
// eslint-disable-next-line capitalized-comments -- v8 ignore
/* v8 ignore next 3 -- @preserve */
if (state.goddess === undefined) {
return false;
}
return state.goddess.bosses.some((boss) => {
return boss.id === finalGoddessBossId && boss.status === "defeated";
});
};
/**
* Calculates the stardust yield from an Enlightenment.
* Formula: MAX(1, FLOOR(SQRT(consecrationCount) * metaMultiplier)).
* @param consecrationCount - The number of consecrations completed before this Enlightenment.
* @param metaMultiplier - Multiplier from prior enlightenment upgrades applied to stardust yield.
* @returns The stardust earned.
*/
const calculateStardustYield = (
consecrationCount: number,
metaMultiplier: number,
): number => {
return Math.max(1, Math.floor(Math.sqrt(consecrationCount) * metaMultiplier));
};
/**
* Builds the updated goddess state after an Enlightenment — a full goddess reset.
* Wipes everything including consecration, preserving only equipment, achievements, and enlightenment data.
* @param state - The current game state before enlightenment.
* @returns The stardust earned and the updated goddess state.
*/
const buildPostEnlightenmentState = (
state: GameState,
): { stardustEarned: number; updatedGoddess: NonNullable<GameState["goddess"]> } => {
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- Caller must ensure goddess exists
const goddess = state.goddess as NonNullable<GameState["goddess"]>;
const metaMultiplier = goddess.enlightenment.stardustMetaMultiplier;
const stardustEarned = calculateStardustYield(
goddess.consecration.count,
metaMultiplier,
);
const updatedCount = goddess.enlightenment.count + 1;
const updatedStardust = goddess.enlightenment.stardust + stardustEarned;
const updatedPurchasedIds = goddess.enlightenment.purchasedUpgradeIds;
const updatedMultipliers = computeEnlightenmentMultipliers(updatedPurchasedIds);
const updatedEnlightenment: EnlightenmentData = {
count: updatedCount,
purchasedUpgradeIds: updatedPurchasedIds,
stardust: updatedStardust,
...updatedMultipliers,
};
const freshGoddess = initialGoddessState();
const updatedGoddess: NonNullable<GameState["goddess"]> = {
...freshGoddess,
achievements: goddess.achievements,
bosses: freshGoddess.bosses.map((b) => {
const existing = goddess.bosses.find((gb) => {
return gb.id === b.id;
});
return {
...b,
bountyDivinityClaimed: existing?.bountyDivinityClaimed ?? false,
};
}),
enlightenment: updatedEnlightenment,
equipment: goddess.equipment,
lastTickAt: Date.now(),
lifetimeBossesDefeated: goddess.lifetimeBossesDefeated,
lifetimePrayersEarned: goddess.lifetimePrayersEarned,
lifetimeQuestsCompleted: goddess.lifetimeQuestsCompleted,
totalPrayersEarned: 0,
};
return { stardustEarned, updatedGoddess };
};
export {
buildPostEnlightenmentState,
calculateStardustYield,
computeEnlightenmentMultipliers,
isEligibleForEnlightenment,
};