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
+97
View File
@@ -0,0 +1,97 @@
import type { Player } from "@elysium/types";
import { Hono } from "hono";
import { prisma } from "../db/client.js";
import { INITIAL_GAME_STATE } from "../data/initialState.js";
import {
buildOAuthUrl,
exchangeCode,
fetchDiscordUser,
} from "../services/discord.js";
import { signToken } from "../services/jwt.js";
export const authRouter = new Hono();
authRouter.get("/url", (context) => {
try {
const url = buildOAuthUrl();
return context.json({ url });
} catch {
return context.json({ error: "Failed to build OAuth URL" }, 500);
}
});
authRouter.get("/callback", async (context) => {
const code = context.req.query("code");
if (!code) {
return context.json({ error: "Missing code parameter" }, 400);
}
try {
const tokenData = await exchangeCode(code);
const discordUser = await fetchDiscordUser(tokenData.access_token);
const existing = await prisma.player.findUnique({
where: { discordId: discordUser.id },
});
const now = Date.now();
if (!existing) {
const player = await prisma.player.create({
data: {
discordId: discordUser.id,
username: discordUser.username,
discriminator: discordUser.discriminator,
avatar: discordUser.avatar,
characterName: discordUser.username,
createdAt: now,
lastSavedAt: now,
totalGoldEarned: 0,
totalClicks: 0,
},
});
const playerShape: Player = {
discordId: player.discordId,
username: player.username,
discriminator: player.discriminator,
avatar: player.avatar ?? null,
characterName: player.characterName,
createdAt: player.createdAt,
lastSavedAt: player.lastSavedAt,
totalGoldEarned: player.totalGoldEarned,
totalClicks: player.totalClicks,
};
const initialState = INITIAL_GAME_STATE(playerShape, playerShape.characterName);
await prisma.gameState.create({
data: {
discordId: player.discordId,
state: initialState,
updatedAt: now,
},
});
const jwtToken = signToken(player.discordId);
const clientUrl = process.env["CORS_ORIGIN"] ?? "http://localhost:5173";
return context.redirect(`${clientUrl}/auth/callback?token=${jwtToken}&isNew=true`);
}
const updated = await prisma.player.update({
where: { discordId: discordUser.id },
data: {
username: discordUser.username,
discriminator: discordUser.discriminator,
avatar: discordUser.avatar,
},
});
const jwtToken = signToken(updated.discordId);
const clientUrl = process.env["CORS_ORIGIN"] ?? "http://localhost:5173";
return context.redirect(`${clientUrl}/auth/callback?token=${jwtToken}&isNew=false`);
} catch {
const clientUrl = process.env["CORS_ORIGIN"] ?? "http://localhost:5173";
return context.redirect(`${clientUrl}/auth/callback?error=auth_failed`);
}
});