Files
tis/src/lib/games/progress.ts
T
ogrechkoandClaude Sonnet 5 e682c7df13 Fix mobile header layout, add resumable progress + coin wallet
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
2026-07-31 21:56:49 +00:00

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;
}