From d8f9065474ddcc4d08a8ee6216f14aa3b0482dfc Mon Sep 17 00:00:00 2001 From: moKshagna-p Date: Mon, 22 Jun 2026 01:22:46 +0530 Subject: [PATCH] fix: GameOver overlay blocks click-to-focus, resume game on tab return MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .../features/games/falling-words/engine.ts | 35 ++++++++++++++++--- .../src/features/games/falling-words/view.tsx | 5 ++- 2 files changed, 35 insertions(+), 5 deletions(-) diff --git a/apps/frontend/src/features/games/falling-words/engine.ts b/apps/frontend/src/features/games/falling-words/engine.ts index 6b3110d..22ca6e2 100644 --- a/apps/frontend/src/features/games/falling-words/engine.ts +++ b/apps/frontend/src/features/games/falling-words/engine.ts @@ -78,6 +78,8 @@ export function useEngine( let runStartTime = 0; let elapsedBeforeRun = 0; + let pauseStartTime = 0; + const [phase, setPhase] = createSignal("idle"); const [difficulty, setDifficulty] = createSignal("easy"); const [fieldWidth, setFieldWidth] = createSignal(0); @@ -85,6 +87,7 @@ export function useEngine( const [activeWords, setActiveWords] = createSignal([]); const [currentInput, setCurrentInput] = createSignal(""); const [elapsedMs, setElapsedMs] = createSignal(0); + const [loopKey, setLoopKey] = createSignal(0); const config = createMemo(() => difficultyConfigs[difficulty()]); const score = createMemo(() => formatScore(elapsedMs())); @@ -269,15 +272,37 @@ export function useEngine( } }; - const handleVisibilityChange = () => { - if (document.hidden && phase() === "running") endGame(); + const pauseGame = () => { + if (phase() !== "running") return; + stopLoop(); + elapsedBeforeRun = getElapsedMsNow(); + pauseStartTime = performance.now(); + }; + + const resumeGame = () => { + if (phase() !== "running" || pauseStartTime === 0) return; + const duration = performance.now() - pauseStartTime; + runStartTime += duration; + lastSpawnTime += duration; + pauseStartTime = 0; + setLoopKey((k) => k + 1); }; - const handleWindowBlur = () => { - if (phase() === "running") endGame(); + const handleVisibilityChange = () => { + if (document.hidden) { + pauseGame(); + return; + } + resumeGame(); + setTimeout(focusInput, 0); }; + const handleWindowBlur = () => pauseGame(); + + const handleWindowFocus = () => resumeGame(); + createEffect(() => { + loopKey(); if (phase() !== "running") { stopLoop(); return; @@ -349,11 +374,13 @@ export function useEngine( if (fieldRef) observer.observe(fieldRef); window.addEventListener("blur", handleWindowBlur); + window.addEventListener("focus", handleWindowFocus); document.addEventListener("visibilitychange", handleVisibilityChange); onCleanup(() => { observer.disconnect(); window.removeEventListener("blur", handleWindowBlur); + window.removeEventListener("focus", handleWindowFocus); document.removeEventListener("visibilitychange", handleVisibilityChange); }); }); diff --git a/apps/frontend/src/features/games/falling-words/view.tsx b/apps/frontend/src/features/games/falling-words/view.tsx index faa0b4c..7891657 100644 --- a/apps/frontend/src/features/games/falling-words/view.tsx +++ b/apps/frontend/src/features/games/falling-words/view.tsx @@ -33,7 +33,10 @@ function View(props: GameViewProps) { -
+