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
@@ -0,0 +1,78 @@
import type { Quest } from "@elysium/types";
import { useGame } from "../../context/GameContext.js";
const formatDuration = (seconds: number): string => {
if (seconds >= 3600) return `${Math.floor(seconds / 3600)}h ${Math.floor((seconds % 3600) / 60)}m`;
if (seconds >= 60) return `${Math.floor(seconds / 60)}m ${seconds % 60}s`;
return `${seconds}s`;
};
const questTimeRemaining = (quest: Quest): number => {
if (quest.status !== "active" || quest.startedAt == null) return 0;
const elapsed = (Date.now() - quest.startedAt) / 1000;
return Math.max(0, quest.durationSeconds - elapsed);
};
interface QuestCardProps {
quest: Quest;
}
const QuestCard = ({ quest }: QuestCardProps): React.JSX.Element => {
const { startQuest } = useGame();
return (
<div className={`quest-card quest-${quest.status}`}>
<div className="quest-info">
<h3>{quest.name}</h3>
<p>{quest.description}</p>
<div className="quest-rewards">
{quest.rewards.map((reward, index) => (
// eslint-disable-next-line react/no-array-index-key -- rewards have no unique id
<span key={index} className="reward-tag">
{reward.type === "gold" && `🪙 ${reward.amount?.toLocaleString()}`}
{reward.type === "essence" && `${reward.amount?.toLocaleString()}`}
{reward.type === "crystals" && `💎 ${reward.amount?.toLocaleString()}`}
{reward.type === "upgrade" && "🔓 Upgrade"}
{reward.type === "adventurer" && "👥 New Adventurer"}
</span>
))}
</div>
</div>
<div className="quest-action">
{quest.status === "locked" && <span className="quest-badge locked">🔒 Locked</span>}
{quest.status === "available" && (
<button
className="start-quest-button"
onClick={() => { startQuest(quest.id); }}
type="button"
>
Send Party ({formatDuration(quest.durationSeconds)})
</button>
)}
{quest.status === "active" && (
<span className="quest-badge active">
{formatDuration(Math.ceil(questTimeRemaining(quest)))} remaining
</span>
)}
{quest.status === "completed" && <span className="quest-badge completed"> Complete</span>}
</div>
</div>
);
};
export const QuestPanel = (): React.JSX.Element => {
const { state } = useGame();
if (!state) return <section className="panel"><p>Loading...</p></section>;
return (
<section className="panel quest-panel">
<h2>Quests</h2>
<div className="quest-list">
{state.quests.map((quest) => (
<QuestCard key={quest.id} quest={quest} />
))}
</div>
</section>
);
};