fix: GameOver overlay blocks click-to-focus, Enter restart dead after tab switch#49
fix: GameOver overlay blocks click-to-focus, Enter restart dead after tab switch#49moKshagna-p wants to merge 1 commit into
Conversation
Greptile SummaryThis PR fixes the game-over overlay blocking click-to-focus and adds pause/resume behaviour when the user switches tabs, replacing the previous approach of immediately ending the game on blur or visibility change.
Confidence Score: 4/5Safe to merge after fixing the double-pause timer bug; the view change is clean. When the user switches tabs, both apps/frontend/src/features/games/falling-words/engine.ts — specifically the
|
| Filename | Overview |
|---|---|
| apps/frontend/src/features/games/falling-words/engine.ts | Introduces pause/resume logic for tab-switch and window-blur events, but pauseGame is called twice on every tab switch (visibilitychange then blur), corrupting elapsedBeforeRun and inflating the score by roughly 3× after a single pause/resume cycle. |
| apps/frontend/src/features/games/falling-words/view.tsx | Adds onClick={actions.focusInput} to the game container so clicks on the GameOver overlay propagate up and refocus the hidden input, matching the existing survival game pattern. Change is minimal and correct. |
Reviews (2): Last reviewed commit: "fix: GameOver overlay blocks click-to-fo..." | Re-trigger Greptile
| endGame(); | ||
| return; | ||
| } | ||
| if (!document.hidden) setTimeout(focusInput, 0); |
There was a problem hiding this comment.
Unconditional focus grab on tab return
setTimeout(focusInput, 0) fires for every visibility-change to visible, regardless of which element held focus before the user switched tabs. If a player had keyboard-tabbed to a header or footer link and then alt-tabbed to another app, returning to the page will silently move focus back to the hidden game input. The same pattern is added to survival/engine.ts, so both games are affected. Consider only calling focusInput when document.activeElement is not an interactive element outside the game container, or when no focused element exists (i.e. !document.activeElement || document.activeElement === document.body).
Root cause: Falling words game container had no onClick handler. GameOver overlay (z-20) blocked clicks from reaching Field's onFieldClick, so focusInput() never fired — Enter restart was dead. Also: visibilitychange and blur were calling endGame(), but the user expects the game to resume when returning from GitHub. Fix: - Add onClick to game container div so overlay clicks refocus input - Pause game loop on visibilitychange/blur instead of ending - Resume loop on visibilitychange/focus with adjusted timing - Add loopKey signal to force createEffect re-run on resume
ff48a9e to
d8f9065
Compare
| const pauseGame = () => { | ||
| if (phase() !== "running") return; | ||
| stopLoop(); | ||
| elapsedBeforeRun = getElapsedMsNow(); | ||
| pauseStartTime = performance.now(); | ||
| }; |
There was a problem hiding this comment.
Double-pause on tab switch corrupts elapsed time and score
When the user switches tabs, the browser fires visibilitychange (hidden) first, then window blur — so pauseGame is called twice in quick succession. The second call overwrites elapsedBeforeRun with getElapsedMsNow(), which at that point returns the already-saved elapsedBeforeRun plus the full performance.now() - runStartTime delta (still using the original runStartTime). This roughly doubles the recorded elapsed time before resume even starts. Then resumeGame only compensates for the gap since the second pause call, not the first, so the final in-game timer shows approximately 3× the actual play time.
Concrete example: 5 s of play, 3 s tab-away → on return the timer reads ~15 s instead of 5 s.
The simplest fix is to make pauseGame idempotent — skip the elapsedBeforeRun update if a pause is already in progress:
const pauseGame = () => {
if (phase() !== "running") return;
if (pauseStartTime !== 0) return; // already paused
stopLoop();
elapsedBeforeRun = getElapsedMsNow();
pauseStartTime = performance.now();
};Alternatively, guard handleWindowBlur with if (document.hidden) return; so it short-circuits when the tab is already hidden.
Problem
Clicking the "enter to restart" overlay during game-over doesn't refocus the hidden input — so pressing Enter does nothing.
Two root causes:
GameOver overlay blocks clicks. The falling words game container had no
onClickhandler. TheGameOveroverlay renders atz-20on top ofField(which hasonFieldClick={actions.focusInput}). Clicks on the overlay never reachfocusInput().Survival already handled this correctly — its container div had
onClick={actions.focusInput}.No refocus on tab return. When
endGame()fires fromvisibilitychange(switching to another tab), the browser ignoresfocus()on hidden tabs. On return, the input stays blurred.Fix
3 files changed, 30 insertions, 3 deletions:
falling-words/view.tsxonClick={actions.focusInput}to game container (same pattern as survival)falling-words/engine.tsvisibilitychangehandler refocuses input when tab becomes visiblesurvival/engine.tsTab navigation through header/footer links is completely untouched.
Fixes #47