From f87938ae08bd97f4fd461bb9bb63cafdc542ad06 Mon Sep 17 00:00:00 2001 From: Wenszel Date: Fri, 8 Dec 2023 17:25:51 +0100 Subject: [PATCH] refactored frontend --- backend/controllers/roomController.js | 8 +- backend/handlers/gameHandler.js | 3 +- backend/handlers/roomHandler.js | 2 +- backend/models/pawn.js | 64 ++++- backend/models/room.js | 21 +- src/App.js | 2 +- .../Dice}/Dice.jsx | 24 +- src/components/{ => Gameboard}/Gameboard.jsx | 22 +- src/components/Gameboard/Map/Map.jsx | 111 +++++++++ src/components/Gameboard/Map/canPawnMove.js | 26 ++ .../Gameboard/Map/getPositionAfterMove.js | 20 +- .../positions.js | 0 .../LoginPage/AddServer/AddServer.jsx | 1 - .../LoginPage/NameInput/NameInput.jsx | 11 +- .../LoginPage/ServerList/ServerList.jsx | 6 +- .../AnimatedOverlay}/AnimatedOverlay.jsx | 8 +- .../AnimatedOverlay}/TimerAnimation.js | 0 .../NameContainer}/NameContainer.jsx | 2 +- src/components/{ => Navbar}/Navbar.css | 0 src/components/{ => Navbar}/Navbar.jsx | 8 +- .../ReadyButton}/ReadyButton.jsx | 10 +- src/components/game-board-components/Map.jsx | 230 ------------------ src/constants/pawnImages.js | 13 + src/constants/positions.js | 117 +++++++++ src/hooks/useInput.js | 11 + src/images/map.jpg | Bin 0 -> 40140 bytes 26 files changed, 407 insertions(+), 313 deletions(-) rename src/components/{game-board-components => Gameboard/Dice}/Dice.jsx (62%) rename src/components/{ => Gameboard}/Gameboard.jsx (88%) create mode 100644 src/components/Gameboard/Map/Map.jsx create mode 100644 src/components/Gameboard/Map/canPawnMove.js rename backend/utils/functions.js => src/components/Gameboard/Map/getPositionAfterMove.js (79%) rename src/components/{game-board-components => Gameboard}/positions.js (100%) rename src/components/{navbar-components => Navbar/NameContainer/AnimatedOverlay}/AnimatedOverlay.jsx (67%) rename src/components/{navbar-components => Navbar/NameContainer/AnimatedOverlay}/TimerAnimation.js (100%) rename src/components/{navbar-components => Navbar/NameContainer}/NameContainer.jsx (89%) rename src/components/{ => Navbar}/Navbar.css (100%) rename src/components/{ => Navbar}/Navbar.jsx (82%) rename src/components/{navbar-components => Navbar/ReadyButton}/ReadyButton.jsx (77%) delete mode 100644 src/components/game-board-components/Map.jsx create mode 100644 src/constants/pawnImages.js create mode 100644 src/constants/positions.js create mode 100644 src/hooks/useInput.js create mode 100644 src/images/map.jpg diff --git a/backend/controllers/roomController.js b/backend/controllers/roomController.js index 80a36ac..8a5bf2a 100644 --- a/backend/controllers/roomController.js +++ b/backend/controllers/roomController.js @@ -23,14 +23,8 @@ const createNewRoom = data => { return room; }; -const findPlayer = async sessionID => { - const player = await Room.findOne({ 'players.sessionID': sessionID }).exec(); - console.log(player); - return await Room.findOne({ 'players.sessionID': sessionID }).exec(); -}; - Room.watch().on('change', async data => { sendToPlayersData(await getRoom(data.documentKey._id)); }); -module.exports = { getRoom, getRooms, updateRoom, getJoinableRoom, createNewRoom, findPlayer }; +module.exports = { getRoom, getRooms, updateRoom, getJoinableRoom, createNewRoom }; diff --git a/backend/handlers/gameHandler.js b/backend/handlers/gameHandler.js index a13ff04..9ef9315 100644 --- a/backend/handlers/gameHandler.js +++ b/backend/handlers/gameHandler.js @@ -1,6 +1,5 @@ const { getRoom, updateRoom } = require('../controllers/roomController'); const { sendToPlayersRolledNumber } = require('../socket/emits'); -const { getPawnPositionAfterMove } = require('../utils/functions'); const { rollDice, isMoveValid } = require('./handlersFunctions'); module.exports = socket => { @@ -10,7 +9,7 @@ module.exports = socket => { const room = await getRoom(req.session.roomId); const pawn = room.getPawn(pawnId); if (isMoveValid(req.session, pawn, room)) { - const newPositionOfMovedPawn = getPawnPositionAfterMove(room.rolledNumber, pawn); + const newPositionOfMovedPawn = pawn.getPositionAfterMove(room.rolledNumber); room.changePositionOfPawn(pawn, newPositionOfMovedPawn); room.beatPawns(newPositionOfMovedPawn, req.session.color); room.changeMovingPlayer(); diff --git a/backend/handlers/roomHandler.js b/backend/handlers/roomHandler.js index bee90de..f99324c 100644 --- a/backend/handlers/roomHandler.js +++ b/backend/handlers/roomHandler.js @@ -1,5 +1,5 @@ const { getRooms, getRoom, updateRoom, createNewRoom } = require('../controllers/roomController'); -const { sendToOnePlayerRooms, sendToOnePlayerData, sendToPlayersData } = require('../socket/emits'); +const { sendToOnePlayerRooms, sendToOnePlayerData } = require('../socket/emits'); module.exports = socket => { const req = socket.request; diff --git a/backend/models/pawn.js b/backend/models/pawn.js index 3df2429..2680cad 100644 --- a/backend/models/pawn.js +++ b/backend/models/pawn.js @@ -2,8 +2,6 @@ const mongoose = require('mongoose'); const Schema = mongoose.Schema; -const { getPawnPositionAfterMove } = require('../utils/functions'); - const PawnSchema = new Schema({ color: String, basePos: Number, @@ -15,10 +13,70 @@ PawnSchema.methods.canMove = function (rolledNumber) { return true; } // (if player's pawn is near finish line) if the move does not go beyond the win line - if (this.position !== getPawnPositionAfterMove(rolledNumber, this) && this.position !== this.basePos) { + if (this.position !== this.getPositionAfterMove(rolledNumber) && this.position !== this.basePos) { return true; } return false; }; +PawnSchema.methods.getPositionAfterMove = function (rolledNumber) { + const { position, color } = this; + switch (color) { + case 'red': + if (position + rolledNumber <= 73) { + if (position >= 0 && position <= 3) { + return 16; + } else if (position <= 66 && position + rolledNumber >= 67) { + return position + rolledNumber + 1; + } else { + return position + rolledNumber; + } + } else { + return position; + } + case 'blue': + if (position + rolledNumber <= 79) { + if (position >= 4 && position <= 7) { + return 55; + } else if (position <= 67 && position + rolledNumber > 67) { + return position + rolledNumber - 52; + } else if (position <= 53 && position + rolledNumber >= 54) { + return position + rolledNumber + 20; + } else { + return position + rolledNumber; + } + } else { + return position; + } + case 'green': + if (position + rolledNumber <= 85) { + if (position >= 8 && position <= 11) { + return 42; + } else if (position <= 67 && position + rolledNumber > 67) { + return position + rolledNumber - 52; + } else if (position <= 40 && position + rolledNumber >= 41) { + return position + rolledNumber + 39; + } else { + return position + rolledNumber; + } + } else { + return position; + } + case 'yellow': + if (position + rolledNumber <= 85) { + if (position >= 12 && position <= 15) { + return 29; + } else if (position <= 67 && position + rolledNumber > 67) { + return position + rolledNumber - 52; + } else if (position <= 27 && position + rolledNumber >= 28) { + return position + rolledNumber + 58; + } else { + return position + rolledNumber; + } + } else { + return position; + } + } +}; + module.exports = PawnSchema; diff --git a/backend/models/room.js b/backend/models/room.js index 2c7ca34..8c3f25a 100644 --- a/backend/models/room.js +++ b/backend/models/room.js @@ -1,6 +1,5 @@ const mongoose = require('mongoose'); const { colors } = require('../utils/constants'); -const { getPawnPositionAfterMove, getStartPositions } = require('../utils/functions'); const { makeRandomMove } = require('../handlers/handlersFunctions'); const PawnSchema = require('./pawn'); const PlayerSchema = require('./player'); @@ -16,7 +15,23 @@ const RoomSchema = new mongoose.Schema({ timeoutID: Number, rolledNumber: Number, players: [PlayerSchema], - pawns: { type: [PawnSchema], default: getStartPositions() }, + pawns: { + type: [PawnSchema], + default: () => { + const startPositions = []; + for (let i = 0; i < 16; i++) { + let pawn = {}; + pawn.basePos = i; + pawn.position = i; + if (i < 4) pawn.color = colors[0]; + else if (i < 8) pawn.color = colors[1]; + else if (i < 12) pawn.color = colors[2]; + else if (i < 16) pawn.color = colors[3]; + startPositions.push(pawn); + } + return startPositions; + }, + }, }); RoomSchema.methods.beatPawns = function (position, attackingPawnColor) { @@ -44,7 +59,7 @@ RoomSchema.methods.changeMovingPlayer = function () { }; RoomSchema.methods.movePawn = function (pawn) { - const newPositionOfMovedPawn = getPawnPositionAfterMove(this.rolledNumber, pawn); + const newPositionOfMovedPawn = pawn.getPositionAfterMove(this.rolledNumber); this.changePositionOfPawn(pawn, newPositionOfMovedPawn); this.beatPawns(newPositionOfMovedPawn, pawn.color); }; diff --git a/src/App.js b/src/App.js index 770998b..6853ef8 100644 --- a/src/App.js +++ b/src/App.js @@ -2,7 +2,7 @@ import React, { useEffect, useState, createContext } from 'react'; import { io } from 'socket.io-client'; import { BrowserRouter as Router, Route, Redirect, Switch } from 'react-router-dom'; import ReactLoading from 'react-loading'; -import Gameboard from './components/Gameboard'; +import Gameboard from './components/Gameboard/Gameboard'; import LoginPage from './components/LoginPage/LoginPage'; export const PlayerDataContext = createContext(); diff --git a/src/components/game-board-components/Dice.jsx b/src/components/Gameboard/Dice/Dice.jsx similarity index 62% rename from src/components/game-board-components/Dice.jsx rename to src/components/Gameboard/Dice/Dice.jsx index b18131e..98a0751 100644 --- a/src/components/game-board-components/Dice.jsx +++ b/src/components/Gameboard/Dice/Dice.jsx @@ -1,24 +1,28 @@ -import React, { useState, useEffect, useContext } from 'react'; -import { SocketContext } from '../../App'; -import one from '../../images/dice/1.png'; -import two from '../../images/dice/2.png'; -import three from '../../images/dice/3.png'; -import four from '../../images/dice/4.png'; -import five from '../../images/dice/5.png'; -import six from '../../images/dice/6.png'; -import roll from '../../images/dice/roll.png'; +import React, { useEffect, useContext } from 'react'; +import { SocketContext } from '../../../App'; +import one from '../../../images/dice/1.png'; +import two from '../../../images/dice/2.png'; +import three from '../../../images/dice/3.png'; +import four from '../../../images/dice/4.png'; +import five from '../../../images/dice/5.png'; +import six from '../../../images/dice/6.png'; +import roll from '../../../images/dice/roll.png'; const Dice = ({ rolledNumberCallback, rolledNumber, nowMoving, color, movingPlayer }) => { const socket = useContext(SocketContext); - const [images] = useState([one, two, three, four, five, six, roll]); + + const images = [one, two, three, four, five, six, roll]; + const handleRoll = () => { socket.emit('game:roll'); }; + useEffect(() => { socket.on('game:roll', number => { rolledNumberCallback(number); }); }, []); + return (
{movingPlayer === color ? ( diff --git a/src/components/Gameboard.jsx b/src/components/Gameboard/Gameboard.jsx similarity index 88% rename from src/components/Gameboard.jsx rename to src/components/Gameboard/Gameboard.jsx index e0764d9..45f7ae8 100644 --- a/src/components/Gameboard.jsx +++ b/src/components/Gameboard/Gameboard.jsx @@ -1,17 +1,16 @@ -import React, { useState, useEffect, useContext, useCallback } from 'react'; +import React, { useState, useEffect, useContext } from 'react'; import ReactLoading from 'react-loading'; -import { PlayerDataContext, SocketContext } from '../App'; -import Map from './game-board-components/Map'; -import Navbar from './Navbar'; +import { PlayerDataContext, SocketContext } from '../../App'; +import Map from './Map/Map'; +import Navbar from '../Navbar/Navbar'; const Gameboard = () => { - // Context data const socket = useContext(SocketContext); const context = useContext(PlayerDataContext); - // Render data + const [pawns, setPawns] = useState([]); const [players, setPlayers] = useState([]); - // Game logic data + const [rolledNumber, setRolledNumber] = useState(null); const [time, setTime] = useState(); const [isReady, setIsReady] = useState(); @@ -19,7 +18,8 @@ const Gameboard = () => { const [started, setStarted] = useState(false); const [movingPlayer, setMovingPlayer] = useState('red'); - const checkWin = useCallback(() => { + + const checkWin = () => { // Player wins when all pawns with same color are inside end base if (pawns.filter(pawn => pawn.color === 'red' && pawn.position === 73).length === 4) { alert('Red Won'); @@ -30,7 +30,8 @@ const Gameboard = () => { } else if (pawns.filter(pawn => pawn.color === 'yellow' && pawn.position === 91).length === 4) { alert('Yellow Won'); } - }, [pawns]); + }; + useEffect(() => { socket.emit('room:data', context.roomId); socket.on('room:data', data => { @@ -59,9 +60,8 @@ const Gameboard = () => { setTime(data.nextMoveTime); setStarted(data.started); }); - }, []); + }, [socket]); - // Callback to handle dice rolling between dice and map component const rolledNumberCallback = number => { setRolledNumber(number); }; diff --git a/src/components/Gameboard/Map/Map.jsx b/src/components/Gameboard/Map/Map.jsx new file mode 100644 index 0000000..1af250e --- /dev/null +++ b/src/components/Gameboard/Map/Map.jsx @@ -0,0 +1,111 @@ +import React, { useEffect, useRef, useState, useContext } from 'react'; +import { PlayerDataContext, SocketContext } from '../../../App'; + +import mapImage from '../../../images/map.jpg'; +import positions from '../positions'; +import pawnImages from '../../../constants/pawnImages'; +import canPawnMove from './canPawnMove'; +import getPositionAfterMove from './getPositionAfterMove'; + +const Map = ({ pawns, nowMoving, rolledNumber }) => { + const player = useContext(PlayerDataContext); + const socket = useContext(SocketContext); + const canvasRef = useRef(null); + + const [hintPawn, setHintPawn] = useState(); + + const paintPawn = (context, x, y, color) => { + const touchableArea = new Path2D(); + touchableArea.arc(x, y, 12, 0, 2 * Math.PI); + const image = new Image(); + image.src = pawnImages[color]; + image.onload = function () { + context.drawImage(image, x - 17, y - 14, 35, 30); + }; + return touchableArea; + }; + + const handleCanvasClick = event => { + if (hintPawn) { + const canvas = canvasRef.current; + const ctx = canvas.getContext('2d'); + const rect = canvas.getBoundingClientRect(), + cursorX = event.clientX - rect.left, + cursorY = event.clientY - rect.top; + for (const pawn of pawns) { + if (ctx.isPointInPath(pawn.touchableArea, cursorX, cursorY)) { + socket.emit('game:move', pawn._id); + } + } + setHintPawn(null); + } + }; + + const handleMouseMove = event => { + if (nowMoving && rolledNumber) { + const canvas = canvasRef.current; + const ctx = canvas.getContext('2d'); + const rect = canvas.getBoundingClientRect(), + x = event.clientX - rect.left, + y = event.clientY - rect.top; + canvas.style.cursor = 'default'; + for (const pawn of pawns) { + if (pawn.touchableArea) { + if ( + ctx.isPointInPath(pawn.touchableArea, x, y) && + player.color === pawn.color && + canPawnMove(pawn, rolledNumber) + ) { + const pawnPosition = getPositionAfterMove(pawn, rolledNumber); + if (pawnPosition) { + canvas.style.cursor = 'pointer'; + setHintPawn({ id: pawn._id, position: pawnPosition, color: 'grey' }); + break; + } + } else { + setHintPawn(null); + } + } else { + setHintPawn(null); + } + } + } else { + setHintPawn(null); + } + }; + const rerenderCanvas = () => { + const canvas = canvasRef.current; + const ctx = canvas.getContext('2d'); + const image = new Image(); + image.src = mapImage; + image.onload = function () { + ctx.drawImage(image, 0, 0); + pawns.forEach((pawn, index) => { + pawns[index].touchableArea = paintPawn( + ctx, + positions[pawn.position].x, + positions[pawn.position].y, + pawn.color + ); + }); + if (hintPawn) { + paintPawn(ctx, positions[hintPawn.position].x, positions[hintPawn.position].y, hintPawn.color); + } + }; + }; + useEffect(() => { + rerenderCanvas(); + }, [hintPawn, pawns, rerenderCanvas]); + + return ( + + ); +}; +export default Map; diff --git a/src/components/Gameboard/Map/canPawnMove.js b/src/components/Gameboard/Map/canPawnMove.js new file mode 100644 index 0000000..1a5de63 --- /dev/null +++ b/src/components/Gameboard/Map/canPawnMove.js @@ -0,0 +1,26 @@ +export default (pawn, rolledNumber) => { + // If is in base + if ((rolledNumber === 1 || rolledNumber === 6) && pawn.position === pawn.basePos) { + return true; + // Other situations: pawn is on map or pawn is in end positions + } else if (pawn.position !== pawn.basePos) { + switch (pawn.color) { + case 'red': + if (pawn.position + rolledNumber <= 73) return true; + break; + case 'blue': + if (pawn.position + rolledNumber <= 79) return true; + break; + case 'green': + if (pawn.position + rolledNumber <= 85) return true; + break; + case 'yellow': + if (pawn.position + rolledNumber <= 91) return true; + break; + default: + return false; + } + } else { + return false; + } +}; diff --git a/backend/utils/functions.js b/src/components/Gameboard/Map/getPositionAfterMove.js similarity index 79% rename from backend/utils/functions.js rename to src/components/Gameboard/Map/getPositionAfterMove.js index 81f5522..6b403ca 100644 --- a/backend/utils/functions.js +++ b/src/components/Gameboard/Map/getPositionAfterMove.js @@ -1,19 +1,4 @@ -const { colors } = require('./constants'); -function getStartPositions() { - const startPositions = []; - for (let i = 0; i < 16; i++) { - let pawn = {}; - pawn.basePos = i; - pawn.position = i; - if (i < 4) pawn.color = colors[0]; - else if (i < 8) pawn.color = colors[1]; - else if (i < 12) pawn.color = colors[2]; - else if (i < 16) pawn.color = colors[3]; - startPositions.push(pawn); - } - return startPositions; -} -function getPawnPositionAfterMove(rolledNumber, pawn) { +export default (pawn, rolledNumber) => { const { position, color } = pawn; switch (color) { case 'red': @@ -71,5 +56,4 @@ function getPawnPositionAfterMove(rolledNumber, pawn) { return position; } } -} -module.exports = { getStartPositions, getPawnPositionAfterMove }; +}; diff --git a/src/components/game-board-components/positions.js b/src/components/Gameboard/positions.js similarity index 100% rename from src/components/game-board-components/positions.js rename to src/components/Gameboard/positions.js diff --git a/src/components/LoginPage/AddServer/AddServer.jsx b/src/components/LoginPage/AddServer/AddServer.jsx index 054bf54..4333c50 100644 --- a/src/components/LoginPage/AddServer/AddServer.jsx +++ b/src/components/LoginPage/AddServer/AddServer.jsx @@ -10,7 +10,6 @@ const AddServer = () => { useEffect(() => { socket.on('room:created', () => { - console.log('ewa'); socket.emit('room:rooms'); }); }, []); diff --git a/src/components/LoginPage/NameInput/NameInput.jsx b/src/components/LoginPage/NameInput/NameInput.jsx index e1212e4..307518a 100644 --- a/src/components/LoginPage/NameInput/NameInput.jsx +++ b/src/components/LoginPage/NameInput/NameInput.jsx @@ -1,13 +1,14 @@ import React, { useState, useContext, useEffect } from 'react'; import { SocketContext } from '../../../App'; +import useInput from '../../../hooks/useInput'; import './NameInput.css'; const NameInput = ({ isRoomPrivate, roomId }) => { const socket = useContext(SocketContext); - const [nickname, setNickname] = useState(''); - const [password, setPassword] = useState(''); + const nickname = useInput(''); + const password = useInput(''); const [isPasswordWrong, setIsPasswordWrong] = useState(false); const handleButtonClick = () => { - socket.emit('player:login', { name: nickname, password: password, roomId: roomId }); + socket.emit('player:login', { name: nickname.value, password: password.value, roomId: roomId }); }; useEffect(() => { socket.on('error:wrongPassword', () => { @@ -28,12 +29,12 @@ const NameInput = ({ isRoomPrivate, roomId }) => { return (
- setNickname(e.target.value)} /> + {isRoomPrivate ? ( setPassword(e.target.value)} + onChange={password.onChange} style={{ backgroundColor: isPasswordWrong ? 'red' : null }} /> ) : null} diff --git a/src/components/LoginPage/ServerList/ServerList.jsx b/src/components/LoginPage/ServerList/ServerList.jsx index 28e55a2..c23601e 100644 --- a/src/components/LoginPage/ServerList/ServerList.jsx +++ b/src/components/LoginPage/ServerList/ServerList.jsx @@ -12,7 +12,7 @@ const ServerList = () => { const [rooms, setRooms] = useState([]); const [joining, setJoining] = useState(false); const [clickedRoom, setClickedRoom] = useState(null); - useEffect(async () => { + useEffect(() => { socket.emit('room:rooms'); socket.on('room:rooms', data => { data = JSON.parse(data); @@ -70,9 +70,7 @@ const ServerList = () => {
)}
- {joining ? ( - - ) : null} + {joining ? : null}
); }; diff --git a/src/components/navbar-components/AnimatedOverlay.jsx b/src/components/Navbar/NameContainer/AnimatedOverlay/AnimatedOverlay.jsx similarity index 67% rename from src/components/navbar-components/AnimatedOverlay.jsx rename to src/components/Navbar/NameContainer/AnimatedOverlay/AnimatedOverlay.jsx index 5fc82fa..ba0558a 100644 --- a/src/components/navbar-components/AnimatedOverlay.jsx +++ b/src/components/Navbar/NameContainer/AnimatedOverlay/AnimatedOverlay.jsx @@ -1,13 +1,9 @@ -import React, { useState, useEffect } from 'react'; +import React, { useMemo } 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]); + const animationDelay = useMemo(() => 15 - Math.ceil((time - Date.now()) / 1000), [time]); return ( { return ( diff --git a/src/components/Navbar.css b/src/components/Navbar/Navbar.css similarity index 100% rename from src/components/Navbar.css rename to src/components/Navbar/Navbar.css diff --git a/src/components/Navbar.jsx b/src/components/Navbar/Navbar.jsx similarity index 82% rename from src/components/Navbar.jsx rename to src/components/Navbar/Navbar.jsx index 0c67c8a..1b05d12 100644 --- a/src/components/Navbar.jsx +++ b/src/components/Navbar/Navbar.jsx @@ -1,10 +1,10 @@ import React from 'react'; -import Dice from './game-board-components/Dice'; -import NameContainer from './navbar-components/NameContainer'; -import ReadyButton from './navbar-components/ReadyButton'; +import Dice from '../Gameboard/Dice/Dice'; +import NameContainer from './NameContainer/NameContainer'; +import ReadyButton from './ReadyButton/ReadyButton'; import './Navbar.css'; import { useContext } from 'react'; -import { PlayerDataContext } from '../App'; +import { PlayerDataContext } from '../../App'; const Navbar = ({ players, started, time, isReady, rolledNumber, nowMoving, rolledNumberCallback, movingPlayer }) => { const context = useContext(PlayerDataContext); const colors = ['red', 'blue', 'green', 'yellow']; diff --git a/src/components/navbar-components/ReadyButton.jsx b/src/components/Navbar/ReadyButton/ReadyButton.jsx similarity index 77% rename from src/components/navbar-components/ReadyButton.jsx rename to src/components/Navbar/ReadyButton/ReadyButton.jsx index 6addc94..6249a9e 100644 --- a/src/components/navbar-components/ReadyButton.jsx +++ b/src/components/Navbar/ReadyButton/ReadyButton.jsx @@ -1,19 +1,17 @@ import React, { useState, useContext, useEffect } from 'react'; -import { SocketContext } from '../../App'; +import { SocketContext } from '../../../App'; import Switch from '@material-ui/core/Switch'; import '../Navbar.css'; -import './TimerAnimation'; +import '../NameContainer/AnimatedOverlay/TimerAnimation'; const ReadyButton = ({ isReady }) => { const socket = useContext(SocketContext); - const [checked, setChecked] = useState(); + const [checked, setChecked] = useState(isReady); + const handleCheckboxChange = () => { socket.emit('player:ready'); setChecked(!checked); }; - useEffect(() => { - setChecked(isReady); - }); return (
diff --git a/src/components/game-board-components/Map.jsx b/src/components/game-board-components/Map.jsx deleted file mode 100644 index 0d93155..0000000 --- a/src/components/game-board-components/Map.jsx +++ /dev/null @@ -1,230 +0,0 @@ -import React, { useEffect, useRef, useState, useContext, useCallback } from 'react'; -import { PlayerDataContext, SocketContext } from '../../App'; -import positions from './positions'; -import bluePawn from '../../images/pawns/blue-pawn.png'; -import greenPawn from '../../images/pawns/green-pawn.png'; -import yellowPawn from '../../images/pawns/yellow-pawn.png'; -import redPawn from '../../images/pawns/red-pawn.png'; -import greyPawn from '../../images/pawns/grey-pawn.png'; -const Map = ({ pawns, nowMoving, rolledNumber }) => { - const context = useContext(PlayerDataContext); - const socket = useContext(SocketContext); - const [hintPawn, setHintPawn] = useState(); - const paintPawn = (context, x, y, color) => { - const circle = new Path2D(); - circle.arc(x, y, 12, 0, 2 * Math.PI); - const image = new Image(); - switch (color) { - case 'green': - image.src = greenPawn; - break; - case 'blue': - image.src = bluePawn; - break; - case 'red': - image.src = redPawn; - break; - case 'yellow': - image.src = yellowPawn; - break; - case 'grey': - image.src = greyPawn; - break; - } - context.drawImage(image, x - 17, y - 14, 35, 30); - return circle; - }; - - const canvasRef = useRef(null); - - // Return true when pawn can move - const checkIfPawnCanMove = useCallback( - pawn => { - // If is in base - if ((rolledNumber === 1 || rolledNumber === 6) && pawn.position === pawn.basePos) { - return true; - // Other situations: pawn is on map or pawn is in end positions - } else if (pawn.position !== pawn.basePos) { - switch (pawn.color) { - case 'red': - if (pawn.position + rolledNumber <= 73) return true; - break; - case 'blue': - if (pawn.position + rolledNumber <= 79) return true; - break; - case 'green': - if (pawn.position + rolledNumber <= 85) return true; - break; - case 'yellow': - if (pawn.position + rolledNumber <= 91) return true; - break; - default: - return false; - } - } else { - return false; - } - }, - [rolledNumber] - ); - - const handleCanvasClick = event => { - // If hint pawn exist it means that pawn can move - if (hintPawn) { - const canvas = canvasRef.current; - const ctx = canvas.getContext('2d'); - const rect = canvas.getBoundingClientRect(), - x = event.clientX - rect.left, - y = event.clientY - rect.top; - for (const pawn of pawns) { - if (ctx.isPointInPath(pawn.circle, x, y)) { - socket.emit('game:move', pawn._id); - } - } - setHintPawn(null); - } - }; - const getHintPawnPosition = pawn => { - // Based on color (because specific color have specific base and end positions) - let { position } = pawn; - switch (context.color) { - case 'red': - // When in base - if (position >= 0 && position <= 3) { - return 16; - // Next to end - } else if (position <= 66 && position + rolledNumber >= 67) { - return position + rolledNumber + 1; // 1 is difference between last position on map and first on end - // Normal move - } else { - return position + rolledNumber; - } - case 'blue': - // When in base - if (position >= 4 && position <= 7) { - return 55; - // Next to red base - } else if (position <= 67 && position + rolledNumber > 67) { - return position + rolledNumber - 52; - // Next to base - } else if (position <= 53 && position + rolledNumber >= 54) { - return position + rolledNumber + 20; - // Normal move - } else { - return position + rolledNumber; - } - case 'green': - // When in base - if (position >= 8 && position <= 11) { - return 42; - // Next to red base - } else if (position <= 67 && position + rolledNumber > 67) { - return position + rolledNumber - 52; - // Next to base - } else if (position <= 40 && position + rolledNumber >= 41) { - return position + rolledNumber + 39; - // Normal move - } else { - return position + rolledNumber; - } - case 'yellow': - // When in base - if (position >= 12 && position <= 15) { - return 29; - // Next to red base - } else if (position <= 67 && position + rolledNumber > 67) { - return position + rolledNumber - 52; - // Next to base - } else if (position <= 27 && position + rolledNumber >= 28) { - return position + rolledNumber + 58; - // Normal move - } else { - return position + rolledNumber; - } - default: - return position; - } - }; - const handleMouseMove = event => { - if (nowMoving && rolledNumber) { - const canvas = canvasRef.current; - const ctx = canvas.getContext('2d'); - // Gets x and y cords of mouse on canvas - const rect = canvas.getBoundingClientRect(), - x = event.clientX - rect.left, - y = event.clientY - rect.top; - canvas.style.cursor = 'default'; - for (const pawn of pawns) { - if (pawn.circle) { - /* - This condition checks if mouse location is: - 1) on pawn - 2) is color of pawn same as player's - 3) if pawn can move - And then sets cursor to pointer and paints hint pawn - where will be pawn after click - */ - if ( - ctx.isPointInPath(pawn.circle, x, y) && - context.color === pawn.color && - checkIfPawnCanMove(pawn) - ) { - const pawnPosition = getHintPawnPosition(pawn); - // Checks if pawn can make a move - if (pawnPosition) { - canvas.style.cursor = 'pointer'; - setHintPawn({ id: pawn._id, position: pawnPosition, color: 'grey' }); - break; - } - } else { - setHintPawn(null); - } - } - } - } - }; - const rerenderCanvas = useCallback(() => { - const canvas = canvasRef.current; - const ctx = canvas.getContext('2d'); - const image = new Image(); - image.src = 'https://img-9gag-fun.9cache.com/photo/a8GdpYZ_460s.jpg'; - image.onload = function () { - ctx.drawImage(image, 0, 0); - pawns.forEach((pawn, index) => { - pawns[index].circle = paintPawn( - ctx, - positions[pawn.position].x, - positions[pawn.position].y, - pawn.color - ); - }); - if (hintPawn) { - paintPawn(ctx, positions[hintPawn.position].x, positions[hintPawn.position].y, hintPawn.color); - } - }; - }, [checkIfPawnCanMove, context.color, hintPawn, nowMoving, pawns, rolledNumber]); - - // Rerender canvas when pawns have changed - useEffect(() => { - rerenderCanvas(); - }, [hintPawn, pawns, rerenderCanvas]); - - useEffect(() => { - socket.on('game:move', () => { - setHintPawn(null); - }); - socket.on('game:roll', () => { - setHintPawn(null); - }); - }, [socket]); - return ( - - ); -}; -export default Map; diff --git a/src/constants/pawnImages.js b/src/constants/pawnImages.js new file mode 100644 index 0000000..e568106 --- /dev/null +++ b/src/constants/pawnImages.js @@ -0,0 +1,13 @@ +import bluePawn from '../images/pawns/blue-pawn.png'; +import greenPawn from '../images/pawns/green-pawn.png'; +import redPawn from '../images/pawns/red-pawn.png'; +import yellowPawn from '../images/pawns/yellow-pawn.png'; +import greyPawn from '../images/pawns/grey-pawn.png'; + +export default { + green: greenPawn, + blue: bluePawn, + red: redPawn, + yellow: yellowPawn, + grey: greyPawn, +}; diff --git a/src/constants/positions.js b/src/constants/positions.js new file mode 100644 index 0000000..c437f9f --- /dev/null +++ b/src/constants/positions.js @@ -0,0 +1,117 @@ +const positions = [ + // Red base + { x: 67, y: 67 }, // 0 + { x: 67, y: 116 }, + { x: 117, y: 67 }, + { x: 117, y: 116 }, + // Blue base + { x: 67, y: 343 }, + { x: 67, y: 392 }, + { x: 117, y: 343 }, + { x: 117, y: 392 }, + // Green base + { x: 343, y: 343 }, + { x: 392, y: 392 }, + { x: 392, y: 343 }, // 10 + { x: 343, y: 392 }, + // Yellow base + { x: 343, y: 67 }, + { x: 392, y: 116 }, + { x: 392, y: 67 }, + { x: 343, y: 116 }, + // Map - starting from red field + { x: 45, y: 200 }, + { x: 76, y: 200 }, + { x: 107, y: 200 }, + { x: 138, y: 200 }, + { x: 169, y: 200 }, // 20 + + { x: 200, y: 169 }, + { x: 200, y: 138 }, + { x: 200, y: 107 }, + { x: 200, y: 76 }, + { x: 200, y: 45 }, + { x: 200, y: 14 }, + // Top + { x: 230, y: 14 }, + { x: 261, y: 14 }, + { x: 261, y: 45 }, + { x: 261, y: 76 }, // 30 + { x: 261, y: 107 }, + { x: 261, y: 138 }, + { x: 261, y: 169 }, + + { x: 291, y: 200 }, + { x: 321, y: 200 }, + { x: 352, y: 200 }, + { x: 383, y: 200 }, + { x: 414, y: 200 }, + { x: 445, y: 200 }, + // Right + { x: 445, y: 230 }, // 40 + + { x: 445, y: 261 }, + { x: 414, y: 261 }, + { x: 383, y: 261 }, + { x: 352, y: 261 }, + { x: 321, y: 261 }, + { x: 291, y: 261 }, + + { x: 261, y: 291 }, + { x: 261, y: 322 }, + { x: 261, y: 353 }, + { x: 261, y: 384 }, // 50 + { x: 261, y: 414 }, + { x: 261, y: 445 }, + // Bottom + { x: 230, y: 445 }, + + { x: 200, y: 445 }, + { x: 200, y: 414 }, + { x: 200, y: 384 }, + { x: 200, y: 353 }, + { x: 200, y: 322 }, + { x: 200, y: 291 }, + + { x: 169, y: 261 }, // 60 + { x: 138, y: 261 }, + { x: 107, y: 261 }, + { x: 76, y: 261 }, + { x: 45, y: 261 }, + + { x: 15, y: 261 }, + // Left + { x: 15, y: 231 }, // 66 + // One behind red base + { x: 15, y: 200 }, //67 + // Red end + { x: 45, y: 231 }, // 68 + { x: 76, y: 231 }, + { x: 107, y: 231 }, + { x: 138, y: 231 }, + { x: 169, y: 231 }, + { x: 200, y: 231 }, // 73 + // Blue end + { x: 231, y: 414 }, // 74 + { x: 231, y: 384 }, + { x: 231, y: 353 }, + { x: 231, y: 322 }, + { x: 231, y: 291 }, + { x: 231, y: 260 }, // 79 + // Green end + { x: 414, y: 231 }, // 80 + { x: 383, y: 231 }, + { x: 352, y: 231 }, + { x: 321, y: 231 }, + { x: 290, y: 231 }, + { x: 259, y: 231 }, // 85 + // Yellow base + { x: 230, y: 45 }, // 86 + { x: 230, y: 76 }, + { x: 230, y: 107 }, + { x: 230, y: 138 }, + { x: 230, y: 169 }, + { x: 230, y: 200 }, // 91 +]; + +export default positions; diff --git a/src/hooks/useInput.js b/src/hooks/useInput.js new file mode 100644 index 0000000..1144f96 --- /dev/null +++ b/src/hooks/useInput.js @@ -0,0 +1,11 @@ +import { useState } from 'react'; +export default function useInput({ initialValue }) { + const [value, setValue] = useState(initialValue); + const handleChange = e => { + setValue(e.target.value); + }; + return { + value, + onChange: handleChange, + }; +} diff --git a/src/images/map.jpg b/src/images/map.jpg new file mode 100644 index 0000000000000000000000000000000000000000..8c158fd510ae0d3b49323f56b092de8d342ef022 GIT binary patch literal 40140 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 I($Crd2U~y9zW@LL literal 0 HcmV?d00001