Files
elysium/apps/api/src/services/consecration.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

202 lines
6.9 KiB
TypeScript

/**
* @file Consecration service handling eligibility checks and post-consecration state building.
* @copyright nhcarrigan
* @license Naomi's Public License
* @author Naomi Carrigan
*/
/* eslint-disable max-lines-per-function -- Function requires many steps */
/* eslint-disable stylistic/max-len -- Service logic requires long lines */
import { defaultConsecrationUpgrades } from "../data/goddessConsecrationUpgrades.js";
import { initialGoddessState } from "../data/initialState.js";
import type { ConsecrationData, GameState } from "@elysium/types";
/**
* Base prayers threshold for the first consecration.
*/
const baseConsecrationThreshold = 50_000;
/**
* Divisor used in the divinity yield formula.
*/
const divinityYieldDivisor = 1000;
/**
* Calculates the prayers threshold required for the next consecration.
* Formula: BASE * (count + 1)^2 * thresholdMultiplier.
* @param consecrationCount - The number of consecrations completed so far.
* @param thresholdMultiplier - An optional stardust-upgrade multiplier applied to the threshold.
* @returns The prayers amount required to consecrate.
*/
const calculateConsecrationThreshold = (
consecrationCount: number,
thresholdMultiplier = 1,
): number => {
return (
baseConsecrationThreshold
* Math.pow(consecrationCount + 1, 2)
* thresholdMultiplier
);
};
/**
* Returns true if the player is eligible to consecrate:
* the total prayers earned in the current run must meet the threshold.
* @param state - The current game state.
* @returns Whether the player is eligible for consecration.
*/
const isEligibleForConsecration = (state: GameState): boolean => {
// eslint-disable-next-line capitalized-comments -- v8 ignore
/* v8 ignore next 3 -- @preserve */
if (state.goddess === undefined) {
return false;
}
const thresholdMultiplier
= state.goddess.enlightenment.stardustConsecrationThresholdMultiplier;
const threshold = calculateConsecrationThreshold(
state.goddess.consecration.count,
thresholdMultiplier,
);
return state.goddess.totalPrayersEarned >= threshold;
};
/**
* Calculates the divinity yield from a consecration.
* Formula: MAX(1, FLOOR(SQRT(totalPrayersEarned / divisor) * divinityMultiplier)).
* @param totalPrayersEarned - Total prayers earned in the current consecration run.
* @param divinityMultiplier - Multiplier from stardust upgrades applied to divinity yield.
* @returns The divinity earned.
*/
const calculateDivinityYield = (
totalPrayersEarned: number,
divinityMultiplier: number,
): number => {
return Math.max(
1,
Math.floor(
Math.sqrt(totalPrayersEarned / divinityYieldDivisor) * divinityMultiplier,
),
);
};
/**
* Computes the consecration production multiplier from the count.
* Each consecration adds 25% to the production multiplier.
* @param count - The number of consecrations completed.
* @returns The computed production multiplier as a number.
*/
const computeConsecrationProductionMultiplier = (count: number): number => {
const bonus = count * 0.25;
return 1 + bonus;
};
const getCategoryMultiplier = (
purchasedUpgradeIds: Array<string>,
category: string,
): number => {
return defaultConsecrationUpgrades.filter((upgrade) => {
return (
upgrade.category === category
&& purchasedUpgradeIds.includes(upgrade.id)
);
}).reduce((mult, upgrade) => {
return mult * upgrade.multiplier;
}, 1);
};
/**
* Computes all three divinity-upgrade multipliers from the purchased upgrade IDs.
* @param purchasedUpgradeIds - The array of purchased consecration upgrade IDs.
* @returns An object containing the three divinity multiplier values.
*/
const computeConsecrationDivinityMultipliers = (
purchasedUpgradeIds: Array<string>,
): Pick<
ConsecrationData,
| "divinityCombatMultiplier"
| "divinityDisciplesMultiplier"
| "divinityPrayersMultiplier"
> => {
return {
divinityCombatMultiplier: getCategoryMultiplier(purchasedUpgradeIds, "combat"),
divinityDisciplesMultiplier: getCategoryMultiplier(purchasedUpgradeIds, "disciples"),
divinityPrayersMultiplier: getCategoryMultiplier(purchasedUpgradeIds, "prayers"),
};
};
/**
* Builds the updated goddess state after a consecration reset.
* Resets the current run (bosses, quests, disciples, upgrades, zones, exploration crafting).
* Preserves: equipment, achievements, consecration data (updated), enlightenment, lifetime stats, sacred materials.
* @param state - The current game state before consecration.
* @returns The divinity earned and the updated goddess state.
*/
const buildPostConsecrationState = (
state: GameState,
): { divinityEarned: 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 divinityMultiplier
= goddess.enlightenment.stardustConsecrationDivinityMultiplier;
const divinityEarned = calculateDivinityYield(
goddess.totalPrayersEarned,
divinityMultiplier,
);
const updatedCount = goddess.consecration.count + 1;
const updatedDivinity = goddess.consecration.divinity + divinityEarned;
const productionMultiplier = computeConsecrationProductionMultiplier(updatedCount);
const updatedConsecration: ConsecrationData = {
...goddess.consecration,
count: updatedCount,
divinity: updatedDivinity,
lastConsecratedAt: Date.now(),
productionMultiplier: productionMultiplier,
...computeConsecrationDivinityMultipliers(
goddess.consecration.purchasedUpgradeIds,
),
};
const freshGoddess = initialGoddessState();
const updatedGoddess: NonNullable<GameState["goddess"]> = {
...freshGoddess,
achievements: goddess.achievements,
bosses: freshGoddess.bosses.map((b) => {
// eslint-disable-next-line capitalized-comments -- v8 ignore
/* v8 ignore next 7 -- @preserve */
const existing = goddess.bosses.find((gb) => {
return gb.id === b.id;
});
return {
...b,
bountyDivinityClaimed: existing?.bountyDivinityClaimed ?? false,
};
}),
consecration: updatedConsecration,
enlightenment: goddess.enlightenment,
equipment: goddess.equipment,
exploration: {
...freshGoddess.exploration,
materials: goddess.exploration.materials,
},
lastTickAt: Date.now(),
lifetimeBossesDefeated: goddess.lifetimeBossesDefeated,
lifetimePrayersEarned: goddess.lifetimePrayersEarned + goddess.totalPrayersEarned,
lifetimeQuestsCompleted: goddess.lifetimeQuestsCompleted,
totalPrayersEarned: 0,
};
return { divinityEarned, updatedGoddess };
};
export {
buildPostConsecrationState,
calculateConsecrationThreshold,
calculateDivinityYield,
computeConsecrationDivinityMultipliers,
computeConsecrationProductionMultiplier,
isEligibleForConsecration,
};