Resume: exiting a match keeps it alive instead of destroying it
Leaving the table (back button, browser/hardware back) no longer resets the game — it minimizes it and stays resumable: - game-store: add `paused` + minimize()/resume(). Single-player (AI) matches pause their local timers so nothing happens while away; live (server-run) matches keep streaming via the still-active subscription (the .NET GameRoom already runs the match to completion and re-broadcasts state on reconnect). - GameScreen: an unmount effect minimizes any in-progress match no matter how you leave; only a finished match (reward dismissed) tears down. - ResumeGameBar: floating "return to game" pill shown from any screen while a match is alive, or while a finished match still has an unseen reward. - page.tsx: after a full reload, re-enter live mode (minimized) when the server re-broadcasts state, and notify when a match you left finishes while away. Verified: tsc + next build clean; web image rebuilt and serving on :1500. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -78,6 +78,8 @@ interface GameStore {
|
||||
live: boolean;
|
||||
/** reward pushed by the server for a server-run (ranked) match. */
|
||||
serverReward: RewardResult | null;
|
||||
/** the match is still alive but the player navigated away (resumable). */
|
||||
paused: boolean;
|
||||
|
||||
newMatch: (settings: GameSettings) => void;
|
||||
newOnlineMatch: (cfg: OnlineMatchConfig) => void;
|
||||
@@ -85,6 +87,10 @@ interface GameStore {
|
||||
applyServerState: (s: ServerGameState) => void;
|
||||
chooseTrump: (suit: Suit) => void;
|
||||
playHuman: (card: Card) => void;
|
||||
/** Leave the table without ending the match — keeps it resumable. */
|
||||
minimize: () => void;
|
||||
/** Return to a minimized match (re-arms local AI timers; live keeps streaming). */
|
||||
resume: () => void;
|
||||
reset: () => void;
|
||||
}
|
||||
|
||||
@@ -293,6 +299,7 @@ export const useGameStore = create<GameStore>((set, get) => {
|
||||
reconnectDeadline: null,
|
||||
live: false,
|
||||
serverReward: null,
|
||||
paused: false,
|
||||
|
||||
newMatch: (settings) => {
|
||||
clearPending();
|
||||
@@ -302,6 +309,7 @@ export const useGameStore = create<GameStore>((set, get) => {
|
||||
game: selectHakem(initial),
|
||||
started: true,
|
||||
mode: "ai",
|
||||
paused: false,
|
||||
matchMeta: { ranked: false, stake: 0 },
|
||||
tally: freshTally(),
|
||||
turnDeadline: null,
|
||||
@@ -325,6 +333,7 @@ export const useGameStore = create<GameStore>((set, get) => {
|
||||
game: selectHakem(initial),
|
||||
started: true,
|
||||
mode: "online",
|
||||
paused: false,
|
||||
matchMeta: { ranked: cfg.ranked, stake: cfg.stake },
|
||||
tally: freshTally(),
|
||||
turnDeadline: null,
|
||||
@@ -353,6 +362,7 @@ export const useGameStore = create<GameStore>((set, get) => {
|
||||
mode: "online",
|
||||
live: true,
|
||||
serverReward: null,
|
||||
paused: false,
|
||||
matchMeta: { ranked: true, stake: 0 },
|
||||
tally: freshTally(),
|
||||
turnDeadline: null,
|
||||
@@ -418,6 +428,23 @@ export const useGameStore = create<GameStore>((set, get) => {
|
||||
scheduleAuto();
|
||||
},
|
||||
|
||||
minimize: () => {
|
||||
// Keep the match alive and resumable. Single-player (AI) games pause their
|
||||
// local timers so nothing happens while you're away; live (server-run)
|
||||
// games keep streaming into the store via the still-active subscription.
|
||||
if (!get().live) {
|
||||
clearPending();
|
||||
set({ turnDeadline: null, reconnectDeadline: null });
|
||||
}
|
||||
set({ paused: true });
|
||||
},
|
||||
|
||||
resume: () => {
|
||||
set({ paused: false });
|
||||
// Re-arm the local driver for AI games; live games are already up to date.
|
||||
if (!get().live && get().started && get().game.phase !== "match-over") scheduleAuto();
|
||||
},
|
||||
|
||||
reset: () => {
|
||||
clearPending();
|
||||
if (liveUnsub) {
|
||||
@@ -435,6 +462,7 @@ export const useGameStore = create<GameStore>((set, get) => {
|
||||
mode: "ai",
|
||||
live: false,
|
||||
serverReward: null,
|
||||
paused: false,
|
||||
seatPlayers: [],
|
||||
tally: freshTally(),
|
||||
turnDeadline: null,
|
||||
|
||||
@@ -29,6 +29,11 @@ const fa: Dict = {
|
||||
"home.lang": "English",
|
||||
"home.onlineCount": "{n} نفر آنلاین",
|
||||
|
||||
"resume.title": "بازی در جریان",
|
||||
"resume.cta": "بازگشت به بازی",
|
||||
"resume.matchEnded": "بازی به پایان رسید",
|
||||
"resume.matchEndedBody": "نتیجه و جایزه را ببینید",
|
||||
|
||||
"seat.you": "شما",
|
||||
"team.us": "ما",
|
||||
"team.them": "حریف",
|
||||
@@ -270,6 +275,11 @@ const en: Dict = {
|
||||
"home.lang": "فارسی",
|
||||
"home.onlineCount": "{n} players online",
|
||||
|
||||
"resume.title": "Game in progress",
|
||||
"resume.cta": "Return to game",
|
||||
"resume.matchEnded": "Match ended",
|
||||
"resume.matchEndedBody": "See the result and reward",
|
||||
|
||||
"seat.you": "You",
|
||||
"team.us": "Us",
|
||||
"team.them": "Them",
|
||||
|
||||
Reference in New Issue
Block a user