added animated move timer

This commit is contained in:
Wenszel 2023-11-21 21:16:07 +01:00
parent e702b912f7
commit 6fef82f36a
14 changed files with 205 additions and 94 deletions

View File

@ -115,6 +115,7 @@ module.exports = (io, socket) => {
socket.join(room._id.toString()); socket.join(room._id.toString());
// Sending data to the user, after which player will be redirected to the game // Sending data to the user, after which player will be redirected to the game
socket.emit('player:data', JSON.stringify(req.session)); socket.emit('player:data', JSON.stringify(req.session));
socket.emit('room:data', JSON.stringify(updatedRoom));
}); });
}); });
} }

View File

@ -27,6 +27,8 @@ module.exports = (io, socket) => {
room.players[index + 1].nowMoving = true; room.players[index + 1].nowMoving = true;
} }
room.nextMoveTime = Date.now() + 15000; room.nextMoveTime = Date.now() + 15000;
if (this.timeoutID) clearTimeout(this.timeoutID);
this.timeoutID = null;
RoomModel.findOneAndUpdate({ _id: req.session.roomId }, room, function (err, updatedRoom) { RoomModel.findOneAndUpdate({ _id: req.session.roomId }, room, function (err, updatedRoom) {
io.to(req.session.roomId).emit('room:data', JSON.stringify(updatedRoom)); io.to(req.session.roomId).emit('room:data', JSON.stringify(updatedRoom));
}); });

View File

@ -36,6 +36,7 @@ RoomSchema.methods.changeMovingPlayer = function () {
this.nextMoveTime = Date.now() + 15000; this.nextMoveTime = Date.now() + 15000;
this.rolledNumber = null; this.rolledNumber = null;
if (this.timeoutID) clearTimeout(this.timeoutID); if (this.timeoutID) clearTimeout(this.timeoutID);
this.timeoutID = null;
}; };
RoomSchema.methods.movePawn = function (pawn) { RoomSchema.methods.movePawn = function (pawn) {

27
package-lock.json generated
View File

@ -20,6 +20,7 @@
"react-loading": "^2.0.3", "react-loading": "^2.0.3",
"react-router-dom": "^5.2.0", "react-router-dom": "^5.2.0",
"react-scripts": "^5.0.1", "react-scripts": "^5.0.1",
"react-transition-group": "^4.4.5",
"socket.io": "^4.5.1", "socket.io": "^4.5.1",
"socket.io-client": "^4.5.1", "socket.io-client": "^4.5.1",
"web-vitals": "^1.1.0" "web-vitals": "^1.1.0"
@ -16735,9 +16736,9 @@
} }
}, },
"node_modules/react-transition-group": { "node_modules/react-transition-group": {
"version": "4.4.1", "version": "4.4.5",
"resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.1.tgz", "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz",
"integrity": "sha512-Djqr7OQ2aPUiYurhPalTrVy9ddmFCCzwhqQmtN+J3+3DzLO209Fdr70QrN8Z3DsglWql6iY1lDWAfpFiBtuKGw==", "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==",
"dependencies": { "dependencies": {
"@babel/runtime": "^7.5.5", "@babel/runtime": "^7.5.5",
"dom-helpers": "^5.0.1", "dom-helpers": "^5.0.1",
@ -18664,16 +18665,16 @@
} }
}, },
"node_modules/typescript": { "node_modules/typescript": {
"version": "5.2.2", "version": "4.9.5",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz", "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz",
"integrity": "sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==", "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==",
"peer": true, "peer": true,
"bin": { "bin": {
"tsc": "bin/tsc", "tsc": "bin/tsc",
"tsserver": "bin/tsserver" "tsserver": "bin/tsserver"
}, },
"engines": { "engines": {
"node": ">=14.17" "node": ">=4.2.0"
} }
}, },
"node_modules/unbox-primitive": { "node_modules/unbox-primitive": {
@ -31757,9 +31758,9 @@
} }
}, },
"react-transition-group": { "react-transition-group": {
"version": "4.4.1", "version": "4.4.5",
"resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.1.tgz", "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz",
"integrity": "sha512-Djqr7OQ2aPUiYurhPalTrVy9ddmFCCzwhqQmtN+J3+3DzLO209Fdr70QrN8Z3DsglWql6iY1lDWAfpFiBtuKGw==", "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==",
"requires": { "requires": {
"@babel/runtime": "^7.5.5", "@babel/runtime": "^7.5.5",
"dom-helpers": "^5.0.1", "dom-helpers": "^5.0.1",
@ -33178,9 +33179,9 @@
} }
}, },
"typescript": { "typescript": {
"version": "5.2.2", "version": "4.9.5",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz", "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz",
"integrity": "sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==", "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==",
"peer": true "peer": true
}, },
"unbox-primitive": { "unbox-primitive": {

View File

@ -15,6 +15,7 @@
"react-loading": "^2.0.3", "react-loading": "^2.0.3",
"react-router-dom": "^5.2.0", "react-router-dom": "^5.2.0",
"react-scripts": "^5.0.1", "react-scripts": "^5.0.1",
"react-transition-group": "^4.4.5",
"socket.io": "^4.5.1", "socket.io": "^4.5.1",
"socket.io-client": "^4.5.1", "socket.io-client": "^4.5.1",
"web-vitals": "^1.1.0" "web-vitals": "^1.1.0"

View File

@ -40,9 +40,6 @@ function App() {
<Gameboard /> <Gameboard />
</PlayerDataContext.Provider> </PlayerDataContext.Provider>
) : null} ) : null}
<a href='https://www.flaticon.com/free-icons/hand' title='hand icons'>
Hand icons created by berkahicon - Flaticon
</a>
</Route> </Route>
</Switch> </Switch>
</Router> </Router>

View File

@ -67,8 +67,8 @@ const Gameboard = () => {
return ( return (
<> <>
{players ? ( {(players[0] && !started) || (time && started) ? (
<> <div className='container'>
<Navbar <Navbar
players={players} players={players}
started={started} started={started}
@ -80,7 +80,7 @@ const Gameboard = () => {
rolledNumberCallback={rolledNumberCallback} rolledNumberCallback={rolledNumberCallback}
/> />
<Map pawns={pawns} nowMoving={nowMoving} rolledNumber={rolledNumber} /> <Map pawns={pawns} nowMoving={nowMoving} rolledNumber={rolledNumber} />
</> </div>
) : ( ) : (
<ReactLoading type='spinningBubbles' color='white' height={667} width={375} /> <ReactLoading type='spinningBubbles' color='white' height={667} width={375} />
)} )}

View File

@ -1,26 +1,3 @@
.red {
position: relative;
left: 176px;
}
.yellow {
position: relative;
flex-direction: row-reverse;
right: 170px;
}
.blue {
position: relative;
right: 28px;
top: 538px;
}
.green {
position: relative;
flex-direction: row-reverse;
top: 538px;
left: 36px;
}
.player-container {
display: flex;
}
.dice-container { .dice-container {
margin-left: 20px; margin-left: 20px;
margin-right: 20px; margin-right: 20px;
@ -30,3 +7,49 @@
.roll { .roll {
cursor: pointer; cursor: pointer;
} }
.ready-container {
display: flex;
width: 300px;
justify-content: center;
align-items: center;
flex-direction: column;
flex-flow: row-reverse;
background-color: grey;
border-radius: 10px;
border: 2px solid white;
}
.ready-container > label {
margin-left: 10px;
margin-right: 10px;
width: 100px;
color: white;
}
.player-container {
display: flex;
align-items: center;
flex-direction: row;
width: 100%;
}
.red {
margin-bottom: 50px;
grid-column: 1;
grid-row: 1;
}
.yellow {
margin-bottom: 50px;
flex-flow: row-reverse;
grid-column: 2;
grid-row: 1;
}
.blue {
margin-top: 50px;
grid-column: 1;
grid-row: 4;
}
.green {
margin-top: 50px;
flex-flow: row-reverse;
grid-column: 2;
grid-row: 4;
}
/* Styl dla overlay */

View File

@ -3,11 +3,13 @@ import Dice from './game-board-components/Dice';
import NameContainer from './navbar-components/NameContainer'; import NameContainer from './navbar-components/NameContainer';
import ReadyButton from './navbar-components/ReadyButton'; import ReadyButton from './navbar-components/ReadyButton';
import './Navbar.css'; import './Navbar.css';
import { useContext } from 'react';
import { PlayerDataContext } from '../App';
const Navbar = ({ players, started, time, isReady, rolledNumber, nowMoving, rolledNumberCallback, movingPlayer }) => { const Navbar = ({ players, started, time, isReady, rolledNumber, nowMoving, rolledNumberCallback, movingPlayer }) => {
const context = useContext(PlayerDataContext);
const colors = ['red', 'blue', 'green', 'yellow']; const colors = ['red', 'blue', 'green', 'yellow'];
return ( return (
<div className='navbar-container'> <>
{players.map((player, index) => ( {players.map((player, index) => (
<div className={`player-container ${colors[index]}`} key={index}> <div className={`player-container ${colors[index]}`} key={index}>
<NameContainer player={player} time={time} /> <NameContainer player={player} time={time} />
@ -18,10 +20,10 @@ const Navbar = ({ players, started, time, isReady, rolledNumber, nowMoving, roll
color={colors[index]} color={colors[index]}
rolledNumberCallback={rolledNumberCallback} rolledNumberCallback={rolledNumberCallback}
/> />
{context.color !== player.color || started ? null : <ReadyButton isReady={isReady} />}
</div> </div>
))} ))}
{started ? null : <ReadyButton isReady={isReady} />} </>
</div>
); );
}; };
export default Navbar; export default Navbar;

View File

@ -0,0 +1,25 @@
import React, { useState, useEffect } from 'react';
import { CSSTransition } from 'react-transition-group';
import './TimerAnimation.js';
const AnimatedOverlay = ({ time }) => {
const [animationDelay, setAnimationDelay] = useState();
useEffect(() => {
setAnimationDelay(15 - Math.ceil((time - Date.now()) / 1000));
}, [time]);
return (
<CSSTransition
in={true}
timeout={0}
classNames='overlay'
style={{ 'animation-delay': `-${animationDelay}s` }}
unmountOnExit
>
<div className='overlay'></div>
</CSSTransition>
);
};
export default AnimatedOverlay;

View File

@ -1,51 +1,15 @@
import React, { useState, useEffect, useContext } from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { SocketContext } from '../../App'; import AnimatedOverlay from './AnimatedOverlay';
/*
Component responsible for:
- displaying the player's name
- informing players about the readiness of other players by changing the color of container from gray to the player's color
- counting time to the end of the move
Props:
- player (object):
The player to whom the container belongs
Player's properties used in this component:
- ready (boolean):
is the player ready for the start of the game, if so, change color from gray to the player's color
when the game is started all players are ready not matter if they clicked ready button before
- nowMoving (boolean) is this player move now, if true display timer
- name (string)
- time (number) - time remaining until the move is made in milliseconds
*/
const NameContainer = ({ player, time }) => { const NameContainer = ({ player, time }) => {
const [remainingTime, setRemainingTime] = useState();
const socket = useContext(SocketContext);
// Function responsible for counting down to the end of time every second
const countdown = () => {
setRemainingTime(Math.ceil((time - Date.now()) / 1000));
};
useEffect(() => {
// Starts the countdown from the beginning if the server returned information about skipping the turn
socket.on('game:skip', () => {
setRemainingTime(15);
});
setRemainingTime(Math.ceil((time - Date.now()) / 1000));
const interval = setInterval(countdown, 1000);
return () => clearInterval(interval);
}, [countdown]);
return ( return (
<div <div
className='name-container' className='name-container'
style={player.ready ? { backgroundColor: player.color } : { backgroundColor: 'lightgrey' }} style={player.ready ? { backgroundColor: player.color } : { backgroundColor: 'lightgrey' }}
> >
<p>{player.name}</p> <p>{player.name}</p>
{player.nowMoving ? <div className='timer'> {remainingTime} </div> : null} {player.nowMoving ? <AnimatedOverlay time={time} /> : null}
</div> </div>
); );
}; };

View File

@ -1,6 +1,8 @@
import React, { useState, useContext, useEffect } from 'react'; import React, { useState, useContext, useEffect } from 'react';
import { SocketContext } from '../../App'; import { SocketContext } from '../../App';
import Switch from '@material-ui/core/Switch'; import Switch from '@material-ui/core/Switch';
import '../Navbar.css';
import './TimerAnimation';
const ReadyButton = ({ isReady }) => { const ReadyButton = ({ isReady }) => {
const socket = useContext(SocketContext); const socket = useContext(SocketContext);

View File

@ -0,0 +1,63 @@
const keyframes = [];
const steps = 86;
let count = 0;
let s = 'polygon(50% 50%, 50% 0%, 50% 0%';
for (let i = 50; i <= 100; i += 5) {
s += `, ${i}% 0%`;
handle();
}
for (let i = 0; i <= 100; i += 5) {
s += `, 100% ${i}%`;
handle();
}
for (let i = 100; i >= 0; i -= 5) {
s += `, ${i}% 100%`;
handle();
}
for (let i = 100; i >= 0; i -= 5) {
s += `, 0% ${i}%`;
handle();
}
for (let i = 0; i <= 50; i += 5) {
s += `, ${i}% 0%`;
handle();
}
function handle() {
const percentage = (count / steps) * 100;
let step;
if (percentage <= 75 && percentage >= 73) {
step = `${percentage}% {
background-color: orange;
clip-path: ${s})
}`;
} else if (percentage > 97.5 && percentage < 100) {
step = `${percentage}% {
background-color: red;
clip-path: ${s})
}`;
} else if (percentage > 0 && percentage < 2.5) {
step = `${percentage}% {
background-color: green;
clip-path: ${s})
}`;
} else {
step = `${percentage}% {
clip-path: ${s})
}`;
}
keyframes.push(step);
count++;
}
const animation = document.styleSheets[0].insertRule(
`
@keyframes timerAnimation {
${keyframes.join('\n')}
}
`,
document.styleSheets[0].cssRules.length
);

View File

@ -6,13 +6,16 @@ body {
rgba(0, 138, 255, 1) 16%, rgba(0, 138, 255, 1) 16%,
rgba(9, 9, 121, 1) 81% rgba(9, 9, 121, 1) 81%
); );
overflow: hidden;
}
#root {
display: flex; display: flex;
justify-content: center;
flex-direction: column; flex-direction: column;
align-items: center; height: 100vh;
} width: 100vw;
.canvas-container {
margin: 10px;
} }
canvas { canvas {
border-radius: 15px; border-radius: 15px;
border: 2px solid black; border: 2px solid black;
@ -25,12 +28,14 @@ canvas {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
} }
.navbar-container > div {
margin-right: 10px;
}
.name-container { .name-container {
width: 100px; position: relative;
height: 50px; min-width: 100px;
min-height: 50px;
display: flex;
justify-content: center;
align-items: center;
border: 2px solid white; border: 2px solid white;
border-radius: 5px; border-radius: 5px;
color: white; color: white;
@ -49,9 +54,33 @@ canvas {
height: 20px; height: 20px;
border-radius: 5px; border-radius: 5px;
} }
.overlay {
width: 100%;
height: 100%;
position: absolute;
top: 0;
left: 0;
opacity: 0.9;
animation: timerAnimation 15s linear infinite;
transition-duration: 15s;
}
#root { #root {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
} }
.container {
display: grid;
align-items: center;
justify-items: center;
grid-template-columns: 230px 230px;
grid-template-rows: 50px 250px 250px 50px;
}
.canvas-container {
place-self: center;
grid-column: 1 / span 2;
grid-row: 2 / span 2;
}