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,170 @@
|
||||
/**
|
||||
* @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,
|
||||
};
|
||||
Reference in New Issue
Block a user