generated from nhcarrigan/template
feat: initial prototype — core game systems (#30)
## Summary This PR represents the full v1 prototype, implementing the core game systems for Elysium. - Full idle/clicker RPG loop: resource collection, crafting, boss fights, exploration, and quests - Adventurer hiring with batch size selector and progressive tier cost scaling - Prestige, transcendence, and apotheosis systems with auto-prestige support - Character sheet, titles, leaderboards, companion system, and daily login bonuses - Auto-quest and auto-boss toggles - Discord webhook notifications on prestige/transcendence/apotheosis - Discord role awarded on apotheosis - Responsive design and overarching story/lore system - In-game sound effects and browser notifications for key events - Support link button in the resource bar - Full test coverage (100% on `apps/api` and `packages/types`) - CI pipeline: lint → build → test ## Closes Closes #1 Closes #2 Closes #3 Closes #4 Closes #5 Closes #6 Closes #7 Closes #8 Closes #9 Closes #10 Closes #11 Closes #12 Closes #13 Closes #14 Closes #16 Closes #19 Closes #20 Closes #21 Closes #22 Closes #23 Closes #24 Closes #25 Closes #26 Closes #27 Closes #29 ✨ This issue was created with help from Hikari~ 🌸 Co-authored-by: Naomi Carrigan <commits@nhcarrigan.com> Reviewed-on: #30 Co-authored-by: Hikari <hikari@nhcarrigan.com> Co-committed-by: Hikari <hikari@nhcarrigan.com>
This commit was merged in pull request #30.
This commit is contained in:
@@ -0,0 +1,246 @@
|
||||
/**
|
||||
* @file Prestige eligibility checks, runestone calculations, and post-prestige state builder.
|
||||
* @copyright nhcarrigan
|
||||
* @license Naomi's Public License
|
||||
* @author Naomi Carrigan
|
||||
*/
|
||||
/* eslint-disable max-lines-per-function -- buildPostPrestigeState requires constructing a large composite state object */
|
||||
import { initialGameState } from "../data/initialState.js";
|
||||
import { defaultPrestigeUpgrades } from "../data/prestigeUpgrades.js";
|
||||
import type {
|
||||
GameState,
|
||||
PrestigeData,
|
||||
PrestigeUpgradeCategory,
|
||||
} from "@elysium/types";
|
||||
|
||||
const basePrestigeGoldThreshold = 1_000_000;
|
||||
const thresholdScaleFactor = 5;
|
||||
const runestonesPerPrestigeLevel = 10;
|
||||
const milestoneInterval = 5;
|
||||
const milestoneRunestonesPerInterval = 25;
|
||||
|
||||
/**
|
||||
* Calculates the gold threshold required for the next prestige.
|
||||
* Formula: BASE * SCALE_FACTOR^prestigeCount — each prestige makes the next threshold harder.
|
||||
* @param prestigeCount - The current number of prestiges completed.
|
||||
* @param thresholdMultiplier - An optional echo-upgrade multiplier applied to the threshold.
|
||||
* @returns The gold amount required to prestige.
|
||||
*/
|
||||
const calculatePrestigeThreshold = (
|
||||
prestigeCount: number,
|
||||
thresholdMultiplier = 1,
|
||||
): number => {
|
||||
return (
|
||||
basePrestigeGoldThreshold
|
||||
* Math.pow(thresholdScaleFactor, prestigeCount)
|
||||
* thresholdMultiplier
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns true if the player has earned enough gold to prestige.
|
||||
* @param state - The current game state.
|
||||
* @returns Whether the player is eligible for a prestige reset.
|
||||
*/
|
||||
const isEligibleForPrestige = (state: GameState): boolean => {
|
||||
const thresholdMultiplier
|
||||
= state.transcendence?.echoPrestigeThresholdMultiplier ?? 1;
|
||||
return (
|
||||
state.player.totalGoldEarned
|
||||
>= calculatePrestigeThreshold(state.prestige.count, thresholdMultiplier)
|
||||
);
|
||||
};
|
||||
|
||||
const getCategoryMultiplier = (
|
||||
purchasedUpgradeIds: Array<string>,
|
||||
category: PrestigeUpgradeCategory,
|
||||
): number => {
|
||||
return defaultPrestigeUpgrades.filter((upgrade) => {
|
||||
const matchesCategory = upgrade.category === category;
|
||||
const isPurchased = purchasedUpgradeIds.includes(upgrade.id);
|
||||
return matchesCategory && isPurchased;
|
||||
}).reduce((mult, upgrade) => {
|
||||
return mult * upgrade.multiplier;
|
||||
}, 1);
|
||||
};
|
||||
|
||||
/**
|
||||
* Computes all four runestone multipliers from the purchased upgrade IDs.
|
||||
* @param purchasedUpgradeIds - The array of purchased prestige upgrade IDs.
|
||||
* @returns An object containing all four runestone multiplier values.
|
||||
*/
|
||||
const computeRunestoneMultipliers = (
|
||||
purchasedUpgradeIds: Array<string>,
|
||||
): {
|
||||
runestonesIncomeMultiplier: number;
|
||||
runestonesClickMultiplier: number;
|
||||
runestonesEssenceMultiplier: number;
|
||||
runestonesCrystalMultiplier: number;
|
||||
} => {
|
||||
return {
|
||||
runestonesClickMultiplier: getCategoryMultiplier(
|
||||
purchasedUpgradeIds,
|
||||
"click",
|
||||
),
|
||||
runestonesCrystalMultiplier: getCategoryMultiplier(
|
||||
purchasedUpgradeIds,
|
||||
"crystals",
|
||||
),
|
||||
runestonesEssenceMultiplier: getCategoryMultiplier(
|
||||
purchasedUpgradeIds,
|
||||
"essence",
|
||||
),
|
||||
runestonesIncomeMultiplier: getCategoryMultiplier(
|
||||
purchasedUpgradeIds,
|
||||
"income",
|
||||
),
|
||||
};
|
||||
};
|
||||
|
||||
interface RunestoneParameters {
|
||||
totalGoldEarned: number;
|
||||
prestigeCount: number;
|
||||
purchasedUpgradeIds: Array<string>;
|
||||
echoRunestoneMultiplier?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates how many runestones the player earns from a prestige.
|
||||
* Formula: floor(sqrt(totalGoldEarned / threshold)) * RUNESTONES_PER_PRESTIGE_LEVEL * runestoneMultiplier.
|
||||
* @param parameters - The parameters for the runestone calculation.
|
||||
* @param parameters.totalGoldEarned - The total gold earned in the current run.
|
||||
* @param parameters.prestigeCount - The current prestige count.
|
||||
* @param parameters.purchasedUpgradeIds - The purchased prestige upgrade IDs.
|
||||
* @param parameters.echoRunestoneMultiplier - An optional echo-upgrade multiplier.
|
||||
* @returns The number of runestones earned.
|
||||
*/
|
||||
const calculateRunestones = (parameters: RunestoneParameters): number => {
|
||||
const {
|
||||
totalGoldEarned,
|
||||
prestigeCount,
|
||||
purchasedUpgradeIds,
|
||||
echoRunestoneMultiplier = 1,
|
||||
} = parameters;
|
||||
const threshold = calculatePrestigeThreshold(prestigeCount);
|
||||
const base
|
||||
= Math.floor(Math.sqrt(totalGoldEarned / threshold))
|
||||
* runestonesPerPrestigeLevel;
|
||||
const runestoneMult = getCategoryMultiplier(
|
||||
purchasedUpgradeIds,
|
||||
"runestones",
|
||||
);
|
||||
return Math.floor(base * runestoneMult * echoRunestoneMultiplier);
|
||||
};
|
||||
|
||||
/**
|
||||
* Calculates the new prestige production multiplier.
|
||||
* Formula: 1.15^prestigeCount — exponential scaling per prestige.
|
||||
* @param prestigeCount - The new prestige count.
|
||||
* @returns The production multiplier for the new prestige level.
|
||||
*/
|
||||
const calculateProductionMultiplier = (
|
||||
prestigeCount: number,
|
||||
): number => {
|
||||
return Math.pow(1.15, prestigeCount);
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns the milestone runestone bonus for the given prestige count.
|
||||
* Every MILESTONE_INTERVAL prestiges awards milestone_number * MILESTONE_RUNESTONES_PER_INTERVAL stones.
|
||||
* @param prestigeCount - The prestige count after the current prestige.
|
||||
* @returns The milestone runestone bonus, or 0 if not a milestone prestige.
|
||||
*/
|
||||
const calculateMilestoneBonus = (prestigeCount: number): number => {
|
||||
if (prestigeCount % milestoneInterval !== 0) {
|
||||
return 0;
|
||||
}
|
||||
const milestoneNumber = prestigeCount / milestoneInterval;
|
||||
return milestoneNumber * milestoneRunestonesPerInterval;
|
||||
};
|
||||
|
||||
/**
|
||||
* Generates the reset game state after a prestige.
|
||||
* Carries over prestige data and runestones; resets everything else.
|
||||
* @param currentState - The game state at the time of the prestige.
|
||||
* @param characterName - The player's character name to carry forward.
|
||||
* @returns The new game state, prestige data, and runestone counts.
|
||||
*/
|
||||
const buildPostPrestigeState = (
|
||||
currentState: GameState,
|
||||
characterName: string,
|
||||
): {
|
||||
prestigeState: GameState;
|
||||
prestigeData: PrestigeData;
|
||||
runestonesEarned: number;
|
||||
milestoneRunestones: number;
|
||||
} => {
|
||||
const {
|
||||
autoPrestigeEnabled,
|
||||
count: currentPrestigeCount,
|
||||
purchasedUpgradeIds,
|
||||
runestones: currentRunestones,
|
||||
} = currentState.prestige;
|
||||
const echoRunestoneMultiplier
|
||||
= currentState.transcendence?.echoPrestigeRunestoneMultiplier ?? 1;
|
||||
const runestonesEarned = calculateRunestones({
|
||||
echoRunestoneMultiplier: echoRunestoneMultiplier,
|
||||
prestigeCount: currentPrestigeCount,
|
||||
purchasedUpgradeIds: purchasedUpgradeIds,
|
||||
totalGoldEarned: currentState.player.totalGoldEarned,
|
||||
});
|
||||
const updatedPrestigeCount = currentPrestigeCount + 1;
|
||||
const milestoneRunestones = calculateMilestoneBonus(updatedPrestigeCount);
|
||||
|
||||
const prestigeData: PrestigeData = {
|
||||
count: updatedPrestigeCount,
|
||||
lastPrestigedAt: Date.now(),
|
||||
productionMultiplier: calculateProductionMultiplier(updatedPrestigeCount),
|
||||
purchasedUpgradeIds: purchasedUpgradeIds,
|
||||
runestones:
|
||||
currentRunestones + runestonesEarned + milestoneRunestones,
|
||||
...computeRunestoneMultipliers(purchasedUpgradeIds),
|
||||
...autoPrestigeEnabled === undefined
|
||||
? {}
|
||||
: { autoPrestigeEnabled },
|
||||
};
|
||||
|
||||
const freshState = initialGameState(currentState.player, characterName);
|
||||
const prestigeState: GameState = {
|
||||
...freshState,
|
||||
lastTickAt: Date.now(),
|
||||
prestige: prestigeData,
|
||||
// Codex lore persists across prestiges — players keep their discovered entries
|
||||
...currentState.codex === undefined
|
||||
? {}
|
||||
: { codex: currentState.codex },
|
||||
// Transcendence data is permanent — never wiped by prestige
|
||||
...currentState.transcendence === undefined
|
||||
? {}
|
||||
: { transcendence: currentState.transcendence },
|
||||
// Apotheosis data is eternal — never wiped by prestige
|
||||
...currentState.apotheosis === undefined
|
||||
? {}
|
||||
: { apotheosis: currentState.apotheosis },
|
||||
// Story chapter progress is permanent — survives all resets
|
||||
...currentState.story === undefined
|
||||
? {}
|
||||
: { story: currentState.story },
|
||||
};
|
||||
|
||||
return {
|
||||
milestoneRunestones,
|
||||
prestigeData,
|
||||
prestigeState,
|
||||
runestonesEarned,
|
||||
};
|
||||
};
|
||||
|
||||
export {
|
||||
buildPostPrestigeState,
|
||||
calculateMilestoneBonus,
|
||||
calculatePrestigeThreshold,
|
||||
calculateProductionMultiplier,
|
||||
calculateRunestones,
|
||||
computeRunestoneMultipliers,
|
||||
isEligibleForPrestige,
|
||||
};
|
||||
Reference in New Issue
Block a user