diff --git a/assets/css/style.mineseeker.scss b/assets/css/style.mineseeker.scss index f439562..274fe92 100644 --- a/assets/css/style.mineseeker.scss +++ b/assets/css/style.mineseeker.scss @@ -1027,3 +1027,78 @@ main { } } +// ── Game Timer ──────────────────────────────────────────────────────────── + +#mine-wrapper .game-timer-container { + display: flex; + gap: 12px; + justify-content: center; + margin-bottom: 10px; +} + +#mine-wrapper .game-timer { + display: flex; + gap: 10px; + align-items: center; + justify-content: center; + min-width: 115px; + border-radius: 8px; + padding: 8px 18px; + font-family: 'Rajdhani', sans-serif; + font-weight: bold; + border: 2px solid transparent; + transition: all 0.4s ease; +} + +// Red – waiting +#mine-wrapper .game-timer.red-timer { + background: linear-gradient(to bottom, #4a0603 0%, #6b2515 100%); + border-color: #7a1e10; + color: rgba(246, 125, 82, 0.55); +} + +// Red – active (thinking) +#mine-wrapper .game-timer.red-timer.active { + background: linear-gradient(to bottom, #ad0a05 0%, #f67d52 100%); + border-color: #ff9b6b; + color: #fff; + box-shadow: 0 0 16px rgba(173, 10, 5, 0.75), 0 0 5px rgba(246, 125, 82, 0.5); +} + +// Blue – waiting +#mine-wrapper .game-timer.blue-timer { + background: linear-gradient(to bottom, #0b2530 0%, #163d55 100%); + border-color: #173650; + color: rgba(149, 207, 245, 0.55); +} + +// Blue – active (thinking) +#mine-wrapper .game-timer.blue-timer.active { + background: linear-gradient(to bottom, #236f87 0%, #95cff5 100%); + border-color: #b8e5ff; + color: #fff; + box-shadow: 0 0 16px rgba(35, 111, 135, 0.75), 0 0 5px rgba(149, 207, 245, 0.5); +} + +#mine-wrapper .game-timer .timer-icon { + font-size: 15px; + opacity: 0.7; + flex-shrink: 0; +} + +#mine-wrapper .game-timer.active .timer-icon { + opacity: 1; + animation: timer-icon-pulse 1.6s ease-in-out infinite; +} + +@keyframes timer-icon-pulse { + 0%, 100% { transform: scale(1); opacity: 0.85; } + 50% { transform: scale(1.2); opacity: 1; } +} + +#mine-wrapper .game-timer .timer-display { + font-family: 'Courier New', monospace; + font-size: 20px; + letter-spacing: 2px; +} + diff --git a/assets/js/mine-seeker/components/GameTimer.jsx b/assets/js/mine-seeker/components/GameTimer.jsx new file mode 100644 index 0000000..80bc87a --- /dev/null +++ b/assets/js/mine-seeker/components/GameTimer.jsx @@ -0,0 +1,165 @@ +/** + * This file is part of the SplendidBear Websites' projects. + * + * Copyright (c) 2026 @ www.splendidbear.org + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import React, { useEffect, useRef, useState } from 'react'; +import { useGame } from '@mine-contexts'; + +const GameTimer = () => { + const { overlay, connectionLost, endRef, activePlayer, webPlayer } = useGame(); + const [redTime, setRedTime] = useState(0); + const [blueTime, setBlueTime] = useState(0); + const [isRunning, setIsRunning] = useState(false); + const timerIntervalRef = useRef(null); + const gameStartedRef = useRef(false); + + // Use timestamps instead of counters for more reliable background tracking + const redStartTimeRef = useRef(null); + const blueStartTimeRef = useRef(null); + const lastActivePlayerRef = useRef(null); + const pausedRedTimeRef = useRef(0); + const pausedBlueTimeRef = useRef(0); + + // Start timer when overlay is hidden (both players connected and game started) + useEffect(() => { + if (!overlay && !gameStartedRef.current) { + gameStartedRef.current = true; + setIsRunning(true); + setRedTime(0); + setBlueTime(0); + redStartTimeRef.current = Date.now(); + blueStartTimeRef.current = Date.now(); + pausedRedTimeRef.current = 0; + pausedBlueTimeRef.current = 0; + lastActivePlayerRef.current = activePlayer; + } + }, [overlay]); + + // Stop timer on game end (resign/win) + useEffect(() => { + if (endRef.current) { + setIsRunning(false); + } + }, [endRef.current]); + + // Stop timer on connection loss + useEffect(() => { + if (connectionLost) { + setIsRunning(false); + } + }, [connectionLost]); + + // Handle player switch - pause one timer, resume the other + useEffect(() => { + if (!isRunning) return; + + if (lastActivePlayerRef.current !== activePlayer) { + // Player switched, save current accumulated time for whoever was active + const startRef = lastActivePlayerRef.current ? blueStartTimeRef.current : redStartTimeRef.current; + if (startRef) { + const elapsed = Math.floor((Date.now() - startRef) / 1000); + if (lastActivePlayerRef.current) { + pausedBlueTimeRef.current += elapsed; + } else { + pausedRedTimeRef.current += elapsed; + } + } + + // Start the new active player's timer + if (activePlayer) { + blueStartTimeRef.current = Date.now(); + } else { + redStartTimeRef.current = Date.now(); + } + + lastActivePlayerRef.current = activePlayer; + } + }, [activePlayer, isRunning]); + + // Main timer effect - update display every 100ms + useEffect(() => { + if (!isRunning) { + if (timerIntervalRef.current) { + clearInterval(timerIntervalRef.current); + } + return; + } + + timerIntervalRef.current = setInterval(() => { + let currentRedTime = pausedRedTimeRef.current; + let currentBlueTime = pausedBlueTimeRef.current; + + // Add elapsed time for the active player + if (!activePlayer && redStartTimeRef.current) { + currentRedTime += Math.floor((Date.now() - redStartTimeRef.current) / 1000); + } else if (activePlayer && blueStartTimeRef.current) { + currentBlueTime += Math.floor((Date.now() - blueStartTimeRef.current) / 1000); + } + + setRedTime(currentRedTime); + setBlueTime(currentBlueTime); + }, 100); + + return () => { + if (timerIntervalRef.current) { + clearInterval(timerIntervalRef.current); + } + }; + }, [isRunning, activePlayer]); + + // Handle focus/blur to synchronize timer when tab regains focus + useEffect(() => { + const handleFocus = () => { + // Force update when tab regains focus to sync any background drift + if (isRunning) { + let currentRedTime = pausedRedTimeRef.current; + let currentBlueTime = pausedBlueTimeRef.current; + + if (!activePlayer && redStartTimeRef.current) { + currentRedTime += Math.floor((Date.now() - redStartTimeRef.current) / 1000); + } else if (activePlayer && blueStartTimeRef.current) { + currentBlueTime += Math.floor((Date.now() - blueStartTimeRef.current) / 1000); + } + + setRedTime(currentRedTime); + setBlueTime(currentBlueTime); + } + }; + + window.addEventListener('focus', handleFocus); + return () => window.removeEventListener('focus', handleFocus); + }, [isRunning, activePlayer]); + + // Cleanup on unmount + useEffect(() => () => { + if (timerIntervalRef.current) { + clearInterval(timerIntervalRef.current); + } + }, []); + + const formatTime = seconds => { + const mins = Math.floor(seconds / 60); + const secs = seconds % 60; + return `${String(mins).padStart(2, '0')}:${String(secs).padStart(2, '0')}`; + }; + + return ( +