feat: initial elysium idle game prototype

Sets up the full monorepo with pnpm workspaces. Includes shared types
package, Hono API with Discord OAuth/JWT auth, Prisma v6 + MongoDB
Atlas, and React + Vite frontend with game loop, five tabs, and
Discord-linked save/load.
This commit is contained in:
2026-03-06 11:26:19 -08:00
committed by Naomi Carrigan
parent c69e155de3
commit a3daed1683
64 changed files with 9011 additions and 0 deletions
+96
View File
@@ -0,0 +1,96 @@
import type { GameState } from "@elysium/types";
/**
* Pure function — applies one game tick to the state.
* deltaSeconds: time elapsed since last tick.
* Returns a new GameState (does not mutate the original).
*/
export const applyTick = (state: GameState, deltaSeconds: number): GameState => {
let goldGained = 0;
let essenceGained = 0;
for (const adventurer of state.adventurers) {
if (!adventurer.unlocked || adventurer.count === 0) {
continue;
}
const upgradeMultiplier = state.upgrades
.filter(
(u) =>
u.purchased &&
(u.target === "global" ||
(u.target === "adventurer" && u.adventurerId === adventurer.id)),
)
.reduce((mult, upgrade) => mult * upgrade.multiplier, 1);
const prestige = state.prestige.productionMultiplier;
goldGained +=
adventurer.goldPerSecond * adventurer.count * upgradeMultiplier * prestige * deltaSeconds;
essenceGained +=
adventurer.essencePerSecond *
adventurer.count *
upgradeMultiplier *
prestige *
deltaSeconds;
}
// Complete active quests
const now = Date.now();
let questGold = 0;
let questEssence = 0;
let questCrystals = 0;
const updatedQuests = state.quests.map((quest) => {
if (
quest.status !== "active" ||
quest.startedAt == null ||
now < quest.startedAt + quest.durationSeconds * 1000
) {
return quest;
}
const completed = { ...quest, status: "completed" as const };
for (const reward of quest.rewards) {
if (reward.type === "gold" && reward.amount != null) {
questGold += reward.amount;
} else if (reward.type === "essence" && reward.amount != null) {
questEssence += reward.amount;
} else if (reward.type === "crystals" && reward.amount != null) {
questCrystals += reward.amount;
}
}
return completed;
});
const newGold = state.resources.gold + goldGained + questGold;
const newEssence = state.resources.essence + essenceGained + questEssence;
return {
...state,
resources: {
...state.resources,
gold: newGold,
essence: newEssence,
crystals: state.resources.crystals + questCrystals,
},
player: {
...state.player,
totalGoldEarned: state.player.totalGoldEarned + goldGained + questGold,
},
quests: updatedQuests,
lastTickAt: now,
};
};
/**
* Calculates the effective click power, including upgrades.
*/
export const calculateClickPower = (state: GameState): number => {
const clickMultiplier = state.upgrades
.filter((u) => u.purchased && u.target === "click")
.reduce((mult, upgrade) => mult * upgrade.multiplier, 1);
return state.baseClickPower * clickMultiplier * state.prestige.productionMultiplier;
};