chg: usr: add timers to each player - renew the whole migration #4
This commit is contained in:
165
assets/js/mine-seeker/components/GameTimer.jsx
Normal file
165
assets/js/mine-seeker/components/GameTimer.jsx
Normal file
@@ -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 (
|
||||
<div className="game-timer-container">
|
||||
<div className={`game-timer red-timer ${!activePlayer ? 'active' : ''}`}>
|
||||
<i className={`fa ${!activePlayer ? 'fa-hourglass-half' : 'fa-hourglass-start'} timer-icon`} />
|
||||
<span className="timer-display">{formatTime(redTime)}</span>
|
||||
</div>
|
||||
<div className={`game-timer blue-timer ${activePlayer ? 'active' : ''}`}>
|
||||
<i className={`fa ${activePlayer ? 'fa-hourglass-half' : 'fa-hourglass-start'} timer-icon`} />
|
||||
<span className="timer-display">{formatTime(blueTime)}</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default GameTimer;
|
||||
@@ -7,10 +7,11 @@
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import React, { Fragment } from 'react';
|
||||
import { useGame } from '@mine-contexts';
|
||||
import GridField from './GridField';
|
||||
import UserControl from '../user/UserControl';
|
||||
import GameTimer from '../GameTimer';
|
||||
import { BOMB_SYMBOLS, bombRadius } from '@mine-utils';
|
||||
|
||||
const GridControl = ({ onClick, resign }) => {
|
||||
@@ -40,31 +41,34 @@ const GridControl = ({ onClick, resign }) => {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="game-wrapper">
|
||||
<div className={`game-overlay${overlay ? '' : ' hide'}`}>
|
||||
<div className="game-overlay-window">
|
||||
<h1>{overlayTitle}</h1>
|
||||
<h2>{overlaySubTitle}</h2>
|
||||
<Fragment>
|
||||
<GameTimer />
|
||||
<div className="game-wrapper">
|
||||
<div className={`game-overlay${overlay ? '' : ' hide'}`}>
|
||||
<div className="game-overlay-window">
|
||||
<h1>{overlayTitle}</h1>
|
||||
<h2>{overlaySubTitle}</h2>
|
||||
</div>
|
||||
</div>
|
||||
<UserControl
|
||||
resign={resign}
|
||||
/>
|
||||
<div className="grid-container">
|
||||
<div className="grid">
|
||||
{cells.flatMap((row, r) =>
|
||||
row.map((cell, c) => (
|
||||
<GridField
|
||||
key={`${r}_${c}`}
|
||||
cell={cell}
|
||||
onClick={() => onClick([r, c])}
|
||||
onMouseEnter={() => handleHover(r, c)}
|
||||
/>
|
||||
)),
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<UserControl
|
||||
resign={resign}
|
||||
/>
|
||||
<div className="grid-container">
|
||||
<div className="grid">
|
||||
{cells.flatMap((row, r) =>
|
||||
row.map((cell, c) => (
|
||||
<GridField
|
||||
key={`${r}_${c}`}
|
||||
cell={cell}
|
||||
onClick={() => onClick([r, c])}
|
||||
onMouseEnter={() => handleHover(r, c)}
|
||||
/>
|
||||
)),
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
import React, { memo } from 'react';
|
||||
import React, { memo, useMemo } from 'react';
|
||||
import { IMAGES } from '@mine-utils';
|
||||
|
||||
const bombSrc = area => {
|
||||
@@ -25,26 +25,47 @@ const GridField = memo(function GridField({ cell, onClick, onMouseEnter }) {
|
||||
+ (active ? ' active' : '')
|
||||
+ (active && 'm' === currentObj ? ' mine' : '')
|
||||
+ ' color-' + currentObj;
|
||||
|
||||
const inner = isNaN(currentImage)
|
||||
? (
|
||||
<div className="flag-mine"><img src={currentImage} alt="" />
|
||||
<div className="flag-mine-base" />
|
||||
</div>
|
||||
)
|
||||
: currentImage ? <div className="flag-number">{currentImage}</div> : null;
|
||||
|
||||
const bSrc = bombSrc(bombTargetArea);
|
||||
const showLast = lastClickedRed || lastClickedBlue;
|
||||
const lastClass = 'field-' + (lastClickedRed ? 'red' : 'blue') + '-last last-clicked';
|
||||
const bombSourceString = useMemo(() => bombSrc(bombTargetArea), [bombTargetArea]);
|
||||
|
||||
return (
|
||||
<div className="field-wrapper" onClick={onClick} onMouseEnter={onMouseEnter}>
|
||||
<img className="field-target" src={IMAGES.target} alt="" />
|
||||
{bSrc && <img className="field-bomb-target" src={bSrc} alt="" />}
|
||||
{showLast && <img className={lastClass} src={IMAGES.last(lastClickedRed ? 'red' : 'blue')} alt="" />}
|
||||
<div
|
||||
className="field-wrapper"
|
||||
onClick={onClick}
|
||||
onMouseEnter={onMouseEnter}
|
||||
>
|
||||
<img
|
||||
className="field-target"
|
||||
src={IMAGES.target}
|
||||
alt="Field of target"
|
||||
/>
|
||||
{bombSourceString && (
|
||||
<img
|
||||
className="field-bomb-target"
|
||||
src={bombSourceString}
|
||||
alt="Field of bomb target"
|
||||
/>
|
||||
)}
|
||||
{(lastClickedRed || lastClickedBlue) && (
|
||||
<img
|
||||
className={`field-${lastClickedRed ? 'red' : 'blue'}-last last-clicked`}
|
||||
src={IMAGES.last(lastClickedRed ? 'red' : 'blue')}
|
||||
alt="Last clicked area"
|
||||
/>
|
||||
)}
|
||||
<div className={fieldClass}>
|
||||
<div className="field-corner">{inner}</div>
|
||||
<div className="field-corner">
|
||||
{isNaN(currentImage) && (
|
||||
<div className="flag-mine">
|
||||
<img src={currentImage} alt="" />
|
||||
<div className="flag-mine-base" />
|
||||
</div>
|
||||
)}
|
||||
{!isNaN(currentImage) && 0 !== currentImage && (
|
||||
<div className="flag-number">
|
||||
{currentImage}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
*/
|
||||
|
||||
export { GameBoard } from './GameBoard';
|
||||
export { default as GameTimer } from './GameTimer';
|
||||
export { default as GridControl } from './grid/GridControl';
|
||||
export { default as GridField } from './grid/GridField';
|
||||
export { default as User } from './user/User';
|
||||
|
||||
Reference in New Issue
Block a user