Header nav was splitting into three lines on mobile (logo / nav / logout floating right with a big gap) — applied the same order-last wrap pattern already proven in top-tickets/project-claude, so nav drops to its own scrollable row instead. Bigger feature: player progress now survives closing the tab, for all three templates, not just Clicker. Each Phaser scene reports its state through the same registry-callback pattern already used for onFinish (onProgress), and reads an initialState to resume instead of always starting fresh. Autosave fires every 10 minutes for free; an instant manual save costs coins. New coin economy (src/lib/wallet/): a wallet + transaction ledger per user, atomic grant/spend via db.transaction() so two concurrent spends against a low balance can't both succeed (verified with a real race test — exactly one wins, balance never goes negative). New accounts get a 20-coin signup bonus so manual save is testable before any real payment method exists. Real-money purchases are scaffolded but deliberately not wired to a provider yet — user hasn't picked one. /api/wallet/purchase creates a pending order and returns a clean "not configured" error, same shape as the AI/LDAP not-configured responses elsewhere in this project. The payment abstraction (src/lib/payments/provider.ts) is the one seam a real adapter needs to fill in later. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QcXH24ky6zjk2UyK5oZUPH
35 lines
1.3 KiB
TypeScript
35 lines
1.3 KiB
TypeScript
import { eq, and } from "drizzle-orm";
|
|
import { db } from "@/lib/db/client";
|
|
import { gameProgress } from "@/lib/db/schema";
|
|
import { safeParseProgress, type TemplateType, type GameProgress } from "./types";
|
|
|
|
export async function getProgress(gameId: string, userId: string): Promise<GameProgress | null> {
|
|
const row = await db.query.gameProgress.findFirst({
|
|
where: and(eq(gameProgress.gameId, gameId), eq(gameProgress.userId, userId)),
|
|
});
|
|
if (!row) return null;
|
|
|
|
const templateType = await getTemplateTypeForGame(gameId);
|
|
if (!templateType) return null;
|
|
const parsed = safeParseProgress(templateType, JSON.parse(row.state));
|
|
return parsed.success ? parsed.data : null;
|
|
}
|
|
|
|
export async function saveProgress(gameId: string, userId: string, state: GameProgress): Promise<void> {
|
|
await db
|
|
.insert(gameProgress)
|
|
.values({ gameId, userId, state: JSON.stringify(state) })
|
|
.onConflictDoUpdate({
|
|
target: [gameProgress.gameId, gameProgress.userId],
|
|
set: { state: JSON.stringify(state), updatedAt: new Date() },
|
|
});
|
|
}
|
|
|
|
async function getTemplateTypeForGame(gameId: string): Promise<TemplateType | null> {
|
|
const game = await db.query.games.findFirst({
|
|
where: (g, { eq: eqOp }) => eqOp(g.id, gameId),
|
|
columns: { templateType: true },
|
|
});
|
|
return game?.templateType ?? null;
|
|
}
|