2026-04-11 15:19:59 +02:00
|
|
|
/**
|
|
|
|
|
* 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';
|
2026-04-18 12:57:20 +02:00
|
|
|
import BonusBox from './BonusBox';
|
|
|
|
|
import BonusStatsDialog from './BonusStatsDialog';
|
2026-04-11 15:19:59 +02:00
|
|
|
|
2026-04-13 21:09:27 +02:00
|
|
|
const renderAvatar = player => {
|
|
|
|
|
if (!player.registered) return null;
|
|
|
|
|
return (
|
|
|
|
|
<div className="timer-avatar">
|
|
|
|
|
{player.avatar
|
|
|
|
|
? <img src={player.avatar} alt={player.name} className="timer-avatar__img" />
|
|
|
|
|
: <span className="timer-avatar__initials">{player.name.slice(0, 2).toUpperCase()}</span>
|
|
|
|
|
}
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
|
2026-04-11 15:19:59 +02:00
|
|
|
const GameTimer = () => {
|
2026-04-13 21:09:27 +02:00
|
|
|
const { overlay, connectionLost, endRef, activePlayer, red, blue } = useGame();
|
2026-04-11 15:19:59 +02:00
|
|
|
const [redTime, setRedTime] = useState(0);
|
|
|
|
|
const [blueTime, setBlueTime] = useState(0);
|
|
|
|
|
const [isRunning, setIsRunning] = useState(false);
|
2026-04-18 12:57:20 +02:00
|
|
|
const [bonusDialogOpen, setBonusDialogOpen] = useState(false);
|
2026-04-11 15:19:59 +02:00
|
|
|
const timerIntervalRef = useRef(null);
|
|
|
|
|
const gameStartedRef = useRef(false);
|
|
|
|
|
|
|
|
|
|
const redStartTimeRef = useRef(null);
|
|
|
|
|
const blueStartTimeRef = useRef(null);
|
|
|
|
|
const lastActivePlayerRef = useRef(null);
|
|
|
|
|
const pausedRedTimeRef = useRef(0);
|
|
|
|
|
const pausedBlueTimeRef = useRef(0);
|
|
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
}
|
2026-04-19 21:04:15 +02:00
|
|
|
}, [activePlayer, overlay]);
|
2026-04-11 15:19:59 +02:00
|
|
|
|
|
|
|
|
useEffect(() => {
|
2026-04-19 21:31:08 +02:00
|
|
|
if (endRef.current) setIsRunning(false);
|
2026-04-19 21:04:15 +02:00
|
|
|
}, [endRef]);
|
2026-04-11 15:19:59 +02:00
|
|
|
|
|
|
|
|
useEffect(() => {
|
2026-04-19 21:31:08 +02:00
|
|
|
if (connectionLost) setIsRunning(false);
|
2026-04-11 15:19:59 +02:00
|
|
|
}, [connectionLost]);
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (!isRunning) return;
|
|
|
|
|
|
|
|
|
|
if (lastActivePlayerRef.current !== activePlayer) {
|
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (activePlayer) {
|
|
|
|
|
blueStartTimeRef.current = Date.now();
|
|
|
|
|
} else {
|
|
|
|
|
redStartTimeRef.current = Date.now();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
lastActivePlayerRef.current = activePlayer;
|
|
|
|
|
}
|
|
|
|
|
}, [activePlayer, isRunning]);
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (!isRunning) {
|
|
|
|
|
if (timerIntervalRef.current) {
|
|
|
|
|
clearInterval(timerIntervalRef.current);
|
|
|
|
|
}
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
timerIntervalRef.current = setInterval(() => {
|
|
|
|
|
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);
|
|
|
|
|
}, 100);
|
|
|
|
|
|
|
|
|
|
return () => {
|
|
|
|
|
if (timerIntervalRef.current) {
|
|
|
|
|
clearInterval(timerIntervalRef.current);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
}, [isRunning, activePlayer]);
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
const handleFocus = () => {
|
|
|
|
|
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]);
|
|
|
|
|
|
|
|
|
|
useEffect(() => () => {
|
2026-04-19 21:31:08 +02:00
|
|
|
if (timerIntervalRef.current) clearInterval(timerIntervalRef.current);
|
2026-04-11 15:19:59 +02:00
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
const formatTime = seconds => {
|
|
|
|
|
const mins = Math.floor(seconds / 60);
|
|
|
|
|
const secs = seconds % 60;
|
|
|
|
|
return `${String(mins).padStart(2, '0')}:${String(secs).padStart(2, '0')}`;
|
|
|
|
|
};
|
|
|
|
|
|
2026-04-18 12:57:20 +02:00
|
|
|
const openBonusDialog = () => setBonusDialogOpen(true);
|
|
|
|
|
const closeBonusDialog = () => setBonusDialogOpen(false);
|
|
|
|
|
|
2026-04-11 15:19:59 +02:00
|
|
|
return (
|
|
|
|
|
<div className="game-timer-container">
|
2026-04-18 12:57:20 +02:00
|
|
|
<BonusBox color="red" points={red.bonusPoints ?? 0} onClick={openBonusDialog} />
|
2026-04-11 15:19:59 +02:00
|
|
|
<div className={`game-timer red-timer ${!activePlayer ? 'active' : ''}`}>
|
2026-04-13 21:09:27 +02:00
|
|
|
{renderAvatar(red)}
|
2026-04-11 15:19:59 +02:00
|
|
|
<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' : ''}`}>
|
2026-04-13 21:09:27 +02:00
|
|
|
{renderAvatar(blue)}
|
2026-04-11 15:19:59 +02:00
|
|
|
<i className={`fa ${activePlayer ? 'fa-hourglass-half' : 'fa-hourglass-start'} timer-icon`} />
|
|
|
|
|
<span className="timer-display">{formatTime(blueTime)}</span>
|
|
|
|
|
</div>
|
2026-04-18 12:57:20 +02:00
|
|
|
<BonusBox color="blue" points={blue.bonusPoints ?? 0} onClick={openBonusDialog} />
|
|
|
|
|
<BonusStatsDialog open={bonusDialogOpen} onClose={closeBonusDialog} red={red} blue={blue} />
|
2026-04-11 15:19:59 +02:00
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export default GameTimer;
|