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>
This commit is contained in:
@@ -13,6 +13,8 @@ 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);
|
||||
@@ -28,8 +30,21 @@ export function GameScreen() {
|
||||
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 (mode === "online" && game.phase === "match-over" && !submitted.current) {
|
||||
if (!live && mode === "online" && game.phase === "match-over" && !submitted.current) {
|
||||
submitted.current = true;
|
||||
const summary: MatchSummary = {
|
||||
ranked: meta.ranked,
|
||||
@@ -46,18 +61,20 @@ export function GameScreen() {
|
||||
.then((r) => {
|
||||
setReward(r);
|
||||
refreshProfile();
|
||||
for (const a of r.newAchievements)
|
||||
pushNotification({
|
||||
kind: "achievement",
|
||||
titleFa: "دستاورد جدید",
|
||||
titleEn: "New achievement",
|
||||
bodyFa: a.nameFa,
|
||||
bodyEn: a.nameEn,
|
||||
icon: a.icon,
|
||||
});
|
||||
notifyAchievements(r);
|
||||
});
|
||||
}
|
||||
}, [mode, game.phase, game.matchWinner, game.matchScore, game.trump, meta, tally, refreshProfile]);
|
||||
}, [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 (
|
||||
<>
|
||||
|
||||
Reference in New Issue
Block a user