Files
HokmPlay/src/components/screens/GameScreen.tsx
T
soroush.asadi 4f2e4e14ea Server-authoritative economy: wire client to server; entry + rewards on hub
Server:
- daily (/api/daily, /api/daily/claim) + shop (/api/shop/buy) + ChargeEntry
- GameRoom (via IServiceScopeFactory) deducts ranked entry at match start and
  applies match rewards at match-over, broadcasting profile + reward over the hub
- tested: daily, shop (owned-guard), ranked entry deduction pushed over hub

Client:
- SignalrService routes profile/coins/plan/daily/shop/match to the server (Bearer);
  onProfile/onReward hub events; guest/offline fall back to local
- session-store syncs profile from hub; game-store serverReward; GameScreen shows
  live ranked reward from hub (no double submit), submits client-run games
- single source of truth in live mode (no economy divergence)

Postgres-ready via config (Provider=postgres); EnsureCreated for now.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 17:32:47 +03:30

95 lines
2.9 KiB
TypeScript

"use client";
import { useEffect, useRef, useState } from "react";
import { GameTable } from "@/components/GameTable";
import { PostMatchRewardsModal } from "@/components/online/PostMatchRewardsModal";
import { useGameStore } from "@/lib/game-store";
import { useSessionStore } from "@/lib/session-store";
import { useUIStore } from "@/lib/ui-store";
import { getService } from "@/lib/online/service";
import { pushNotification } from "@/lib/notification-store";
import { MatchSummary, RewardResult } from "@/lib/online/types";
export function GameScreen() {
const game = useGameStore((s) => s.game);
const mode = useGameStore((s) => s.mode);
const live = useGameStore((s) => s.live);
const serverReward = useGameStore((s) => s.serverReward);
const tally = useGameStore((s) => s.tally);
const meta = useGameStore((s) => s.matchMeta);
const reset = useGameStore((s) => s.reset);
const returnTo = useUIStore((s) => s.returnTo);
const go = useUIStore((s) => s.go);
const refreshProfile = useSessionStore((s) => s.refreshProfile);
const [reward, setReward] = useState<RewardResult | null>(null);
const submitted = useRef(false);
const exit = () => {
reset();
go(returnTo);
};
const notifyAchievements = (r: RewardResult) => {
for (const a of r.newAchievements)
pushNotification({
kind: "achievement",
titleFa: "دستاورد جدید",
titleEn: "New achievement",
bodyFa: a.nameFa,
bodyEn: a.nameEn,
icon: a.icon,
});
};
// Client-run games (private rooms / casual): submit the result to the server.
useEffect(() => {
if (!live && mode === "online" && game.phase === "match-over" && !submitted.current) {
submitted.current = true;
const summary: MatchSummary = {
ranked: meta.ranked,
stake: meta.stake,
won: game.matchWinner === 0,
kotFor: tally.kotFor,
kotAgainst: tally.kotAgainst,
tricksWon: tally.tricksTeam0,
rounds: game.matchScore[0] + game.matchScore[1],
trump: game.trump,
};
getService()
.submitMatchResult(summary)
.then((r) => {
setReward(r);
refreshProfile();
notifyAchievements(r);
});
}
}, [live, mode, game.phase, game.matchWinner, game.matchScore, game.trump, meta, tally, refreshProfile]);
// Server-run ranked games: the reward arrives via the hub.
useEffect(() => {
if (live && serverReward && !submitted.current) {
submitted.current = true;
setReward(serverReward);
refreshProfile();
notifyAchievements(serverReward);
}
}, [live, serverReward, refreshProfile]);
return (
<>
<GameTable onExit={exit} />
{reward && (
<PostMatchRewardsModal
reward={reward}
won={game.matchWinner === 0}
onClose={() => {
setReward(null);
exit();
}}
/>
)}
</>
);
}