"use client"; import { AnimatePresence, motion } from "framer-motion"; import { useEffect, useRef, useState } from "react"; import { GameTable } from "@/components/GameTable"; import { PostMatchRewardsModal } from "@/components/online/PostMatchRewardsModal"; import { MatchIntroOverlay } from "@/components/online/MatchIntroOverlay"; import { useGameStore } from "@/lib/game-store"; import { useSessionStore } from "@/lib/session-store"; import { useUIStore } from "@/lib/ui-store"; import { useI18n } from "@/lib/i18n"; import { getService } from "@/lib/online/service"; import { pushNotification } from "@/lib/notification-store"; import { celebrate } from "@/lib/celebration-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 forfeited = useGameStore((s) => s.forfeited); const forfeitRequest = useGameStore((s) => s.forfeitRequest); const respondForfeit = useGameStore((s) => s.respondForfeit); const introPending = useGameStore((s) => s.matchIntroPending); const consumeIntro = useGameStore((s) => s.consumeIntro); const returnTo = useUIStore((s) => s.returnTo); const go = useUIStore((s) => s.go); const refreshProfile = useSessionStore((s) => s.refreshProfile); const { t } = useI18n(); const [reward, setReward] = useState(null); const submitted = useRef(false); // Leaving the table (back button, browser/hardware back) keeps the match alive // & resumable — pause is handled by the unmount effect below. A finished match // is torn down instead. const exit = () => { if (useGameStore.getState().game.phase === "match-over") reset(); go(returnTo); }; // Match truly finished (reward dismissed): tear the match down. const finish = () => { reset(); go(returnTo); }; // Any way the table unmounts (exit button, hardware/browser back), keep an // in-progress match alive and resumable instead of letting it run unattended. useEffect(() => { return () => { const gs = useGameStore.getState(); if (gs.started && gs.game.phase !== "match-over") gs.minimize(); }; }, []); 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, }); }; // Splashy celebration overlay for every achievement / level-up earned in the // match — fired once the post-match reward summary is dismissed so each unlock // gets its own animated moment (queued by the celebration store). const celebrateRewards = (r: RewardResult | null) => { if (!r) return; if (r.leveledUp) { celebrate({ variant: "xp", icon: "🎚️", title: t("reward.levelUp"), levelBefore: r.levelBefore, levelAfter: r.levelAfter, }); } for (const a of r.newAchievements) { celebrate({ variant: "purchase", icon: a.icon, title: t("reward.newAchievement"), achievements: [a], }); } }; // 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, // shutout = you won and the opponent never scored a round (e.g. 7–0) shutout: !forfeited && game.matchWinner === 0 && game.matchScore[1] === 0, hakemRounds: tally.hakemRounds, roundsWon: game.matchScore[0], forfeit: forfeited, }; getService() .submitMatchResult(summary) .then((r) => { setReward(r); refreshProfile(); notifyAchievements(r); }); } }, [live, mode, game.phase, game.matchWinner, game.matchScore, game.trump, meta, tally, forfeited, 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]); const canForfeit = game.phase !== "match-over" && !forfeited; return ( <> useGameStore.getState().forfeit() : undefined} /> {/* UNO-style "players joining the table" intro (online matches, once) */} {introPending && mode === "online" && ( )} {/* teammate asked to forfeit — confirm or decline */} {forfeitRequest && (
🏳️

{t("forfeit.title")}

{t("forfeit.teammateAsks").replace("{name}", forfeitRequest.byName)}

{t("forfeit.rule")}

)} {reward && ( { const r = reward; setReward(null); celebrateRewards(r); finish(); }} /> )} ); }