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:
+43
-1
@@ -16,6 +16,7 @@ import { NotificationsScreen } from "@/components/screens/NotificationsScreen";
|
||||
import { AuthScreen } from "@/components/screens/AuthScreen";
|
||||
import { DailyRewardModal } from "@/components/online/DailyRewardModal";
|
||||
import { NotificationToaster } from "@/components/online/NotificationToaster";
|
||||
import { ResumeGameBar } from "@/components/online/ResumeGameBar";
|
||||
import { CapacitorBack } from "@/components/CapacitorBack";
|
||||
import { useSessionStore } from "@/lib/session-store";
|
||||
import { useGameStore } from "@/lib/game-store";
|
||||
@@ -85,12 +86,52 @@ export default function Page() {
|
||||
})
|
||||
.catch(() => {});
|
||||
|
||||
// Resume an in-progress server match after a full reload: the server
|
||||
// re-broadcasts state on (re)connect — if we're not already in a game and
|
||||
// not mid-matchmaking, re-enter live mode (minimized) so the resume pill shows.
|
||||
const svc = getService();
|
||||
let resumeUnsub: (() => void) | undefined;
|
||||
let rewardUnsub: (() => void) | undefined;
|
||||
if (svc.live) {
|
||||
resumeUnsub = svc.onState((s) => {
|
||||
const gs = useGameStore.getState();
|
||||
const scr = useUIStore.getState().screen;
|
||||
if (
|
||||
!gs.started &&
|
||||
scr !== "matchmaking" &&
|
||||
scr !== "game" &&
|
||||
s.matchWinner == null &&
|
||||
s.phase !== "match-over"
|
||||
) {
|
||||
gs.enterServerMatch(svc);
|
||||
gs.applyServerState(s);
|
||||
gs.minimize();
|
||||
}
|
||||
});
|
||||
// Nudge the player when a match they left finishes while they're away.
|
||||
rewardUnsub = svc.onReward(() => {
|
||||
if (useUIStore.getState().screen !== "game")
|
||||
pushNotification({
|
||||
kind: "system",
|
||||
titleFa: "بازی به پایان رسید",
|
||||
titleEn: "Match ended",
|
||||
bodyFa: "نتیجه و جایزه را ببینید",
|
||||
bodyEn: "See the result and reward",
|
||||
icon: "🏆",
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
const onPop = (e: PopStateEvent) => {
|
||||
const raw = ((e.state?.screen as Screen) ?? screenFromHash());
|
||||
useUIStore.getState().syncFromPop(resolveScreen(raw));
|
||||
};
|
||||
window.addEventListener("popstate", onPop);
|
||||
return () => window.removeEventListener("popstate", onPop);
|
||||
return () => {
|
||||
window.removeEventListener("popstate", onPop);
|
||||
resumeUnsub?.();
|
||||
rewardUnsub?.();
|
||||
};
|
||||
}, [init]);
|
||||
|
||||
return (
|
||||
@@ -98,6 +139,7 @@ export default function Page() {
|
||||
{renderScreen(screen)}
|
||||
<DailyRewardModal />
|
||||
<NotificationToaster />
|
||||
<ResumeGameBar />
|
||||
<CapacitorBack />
|
||||
{loading && null}
|
||||
</>
|
||||
|
||||
Reference in New Issue
Block a user