From a027313139c4778ad961e129547ebb252974d007 Mon Sep 17 00:00:00 2001 From: MaciejkaG Date: Fri, 8 Mar 2024 19:18:53 +0100 Subject: [PATCH] Major changes - Multiple visual improvements to animation - Added ship color dimming when they sunk - Added game ending (looks and works primitvely for now) - Added socket.io client error handling and alerts - Finished and improved hit registration and client hit display To-do: - Fix timers (currently resetting the timer is global, which by mistake affects other independent lobbies, critical bug) - Post-match stats - Improve many mechanics - Improve overall quality of the game --- index.js | 14 +++-- public/assets/css/board.css | 13 ++++- public/assets/js/battleships-lib.js | 12 ++--- public/assets/js/socket-err-handler.js | 72 +++++++++++++++++++++++++ public/assets/js/socket-game.js | 74 ++++++++++++++++++++++---- public/assets/js/socket.js | 1 + utils/battleships.js | 63 ++++++++++------------ views/board.handlebars | 3 +- views/index.handlebars | 1 + 9 files changed, 198 insertions(+), 55 deletions(-) create mode 100644 public/assets/js/socket-err-handler.js diff --git a/index.js b/index.js index c2380b4..abb9b51 100644 --- a/index.js +++ b/index.js @@ -281,10 +281,18 @@ io.on('connection', async (socket) => { if (bships.checkTurn(playerGame.data, socket.request.session.id)) { const enemyIdx = socket.request.session.id === playerGame.data.hostId ? 1 : 0; - if (await GInfo.shootShip(socket, posX, posY)) { - io.to(playerGame.id).emit("shot hit", enemyIdx, posX, posY); - } else { + let hit = await GInfo.shootShip(socket, posX, posY); + if (!hit.status) { io.to(playerGame.id).emit("shot missed", enemyIdx, posX, posY); + } else if (hit.status === 1) { + io.to(playerGame.id).emit("shot hit", enemyIdx, posX, posY); + } else if (hit.status === 2) { + io.to(playerGame.id).emit("shot hit", enemyIdx, posX, posY); + io.to(playerGame.id).emit("ship sunk", enemyIdx, hit.ship); + + if (hit.gameFinished) { + io.to(playerGame.id).emit("game finished", !enemyIdx ? 1 : 0); + } } await GInfo.passTurn(socket); diff --git a/public/assets/css/board.css b/public/assets/css/board.css index 535027a..5fed73d 100644 --- a/public/assets/css/board.css +++ b/public/assets/css/board.css @@ -14,6 +14,9 @@ --ship-invalid: hsl(0, 70%, 55%); --ship-miss: hsl(0, 0%, 18%); + --ship-ally-sunken: hsl(120, 30%, 40%); + --ship-enemy-sunken: hsl(0, 30%, 40%); + --dynamic: rgb(83, 83, 245); --danger: rgb(243, 56, 56); --important: rgb(203, 50, 241); @@ -83,7 +86,7 @@ h1,h2,h3,h4,h5,h6 { pointer-events: none; opacity: 0; transform: scale(0); - transition: opacity 0.25s, transform 0.25s 0.05s; + transition: opacity 0.25s, transform 0.25s 0.05s, background-color 0.25s; } #secondaryBoard .field .shipField { @@ -110,6 +113,14 @@ h1,h2,h3,h4,h5,h6 { opacity: 1; } +#board .field.sunken .shipField { + background-color: var(--ship-ally-sunken); +} + +#secondaryBoard .field.sunken .shipField { + background-color: var(--ship-enemy-sunken); +} + .dynamic { color: var(--dynamic); } diff --git a/public/assets/js/battleships-lib.js b/public/assets/js/battleships-lib.js index 3c6ead3..54594f1 100644 --- a/public/assets/js/battleships-lib.js +++ b/public/assets/js/battleships-lib.js @@ -59,7 +59,7 @@ class Battleships { } } - setField(x, y, state, primary = false) { + setField(x, y, state) { if (state==="hit") { this.getField(x, y).children().children("svg").html(""); this.getField(x, y).addClass("hit"); @@ -67,12 +67,12 @@ class Battleships { this.getField(x, y).children(".shipField").css("background-color", "var(--ship-miss)"); this.getField(x, y).addClass("active hit"); this.getField(x, y).children().children("svg").html(""); + } else if (state === "sunken") { + this.getField(x, y).addClass("sunken"); } - - this.getFieldSecondary(x, y).addClass("hit"); } - setFieldEnemy(x, y, state, primary = false) { + setFieldEnemy(x, y, state) { if (state === "hit") { this.getFieldSecondary(x, y).children().children("svg").html(""); this.getFieldSecondary(x, y).addClass("active hit"); @@ -80,9 +80,9 @@ class Battleships { this.getFieldSecondary(x, y).children(".shipField").css("background-color", "var(--ship-miss)"); this.getFieldSecondary(x, y).addClass("active hit"); this.getFieldSecondary(x, y).children().children("svg").html(""); + } else if (state === "sunken") { + this.getFieldSecondary(x, y).addClass("sunken"); } - - this.getFieldSecondary(x, y).addClass("hit"); } placeShip(data) { diff --git a/public/assets/js/socket-err-handler.js b/public/assets/js/socket-err-handler.js new file mode 100644 index 0000000..d8a5d29 --- /dev/null +++ b/public/assets/js/socket-err-handler.js @@ -0,0 +1,72 @@ +// Handling connection errors +socket.on("reconnecting", (number) => { + Toastify({ + text: `Ponowne łączenie... ${number}`, + duration: 5000, + newWindow: true, + gravity: "bottom", + position: "right", + stopOnFocus: true, + className: "bshipstoast", + }).showToast(); +}); + +socket.on("reconnect", () => { + Toastify({ + text: "Połączono ponownie", + duration: 5000, + newWindow: true, + gravity: "bottom", + position: "right", + stopOnFocus: true, + className: "bshipstoast", + }).showToast(); +}); + +socket.on("reconnect_error", () => { + Toastify({ + text: "Wystąpił problem w trakcie ponownego łączenia", + duration: 5000, + newWindow: true, + gravity: "bottom", + position: "right", + stopOnFocus: true, + className: "bshipstoast", + }).showToast(); +}); + +socket.on("reconnect_failed", () => { + Toastify({ + text: "Nie udało się połączyć ponownie", + duration: 5000, + newWindow: true, + gravity: "bottom", + position: "right", + stopOnFocus: true, + className: "bshipstoast", + }).showToast(); +}); + +socket.on("disconnect", () => { + Toastify({ + text: "Rozłączono z serwerem\nSpróbuj odświeżyć stronę jeżeli błąd będzie się powtarzał", + duration: 5000, + newWindow: true, + gravity: "bottom", + position: "right", + stopOnFocus: true, + className: "bshipstoast", + }).showToast(); +}); + +socket.on("error", () => { + Toastify({ + text: "Błąd połączenia", + duration: 5000, + newWindow: true, + gravity: "bottom", + position: "right", + stopOnFocus: true, + className: "bshipstoast", + }).showToast(); +}); \ No newline at end of file diff --git a/public/assets/js/socket-game.js b/public/assets/js/socket-game.js index 3b56b6f..8bf73c7 100644 --- a/public/assets/js/socket-game.js +++ b/public/assets/js/socket-game.js @@ -5,20 +5,29 @@ var timerDestination = null; var gamePhase = 'pregame'; var occupiedFields = []; +var lastTimeClick = 0; + $('#board .field').on('click', function () { - socket.emit("place ship", selectedShip, $(this).data('pos-x'), $(this).data('pos-y'), shipRotation); + console.log(new Date().getTime() / 1000 - lastTimeClick); + if (new Date().getTime() / 1000 - lastTimeClick > 0.3) { + socket.emit("place ship", selectedShip, $(this).data('pos-x'), $(this).data('pos-y'), shipRotation); + lastTimeClick = new Date().getTime() / 1000; + } }); $('#secondaryBoard .field').on('click', function () { - socket.emit("shoot", $(this).data('pos-x'), $(this).data('pos-y')); + if (new Date().getTime() / 1000 - lastTimeClick > 0.3) { + socket.emit("shoot", $(this).data('pos-x'), $(this).data('pos-y')); + lastTimeClick = new Date().getTime() / 1000; + } }); - $('.field').on('contextmenu', function () { - if ($(this).hasClass('active')) { + if ($(this).hasClass('active') && new Date().getTime() / 1000 - lastTimeClick > 0.3) { let originPos = occupiedFields.find((elem) => elem.pos[0] == $(this).data('pos-x') && elem.pos[1] == $(this).data('pos-y')).origin; socket.emit("remove ship", originPos[0], originPos[1]); + lastTimeClick = new Date().getTime() / 1000; } }); @@ -48,19 +57,20 @@ socket.on("removed ship", (data) => { return elem.origin[0] == data.posX && elem.origin[1] == data.posY; }); - shipFields.forEach(field => { - bsc.getField(field.pos[0], field.pos[1]).removeClass("active"); - }); + for (let i = 0; i < shipFields.length; i++) { + const field = shipFields[i]; + setTimeout(() => { + bsc.getField(field.pos[0], field.pos[1]).removeClass("active"); + }, i * 150); + } occupiedFields = occupiedFields.filter(n => !shipFields.includes(n)); - console.log(`shipsLeft[${data.type}] = ${shipsLeft[data.type]}`) shipsLeft[data.type]++; refreshBoardView(); }); socket.on("shot hit", (victimIdx, posX, posY) => { - console.log("hit"); if (victimIdx === playerIdx) { bsc.setField(posX, posY, "hit"); } else { @@ -69,7 +79,6 @@ socket.on("shot hit", (victimIdx, posX, posY) => { }); socket.on("shot missed", (victimIdx, posX, posY) => { - console.log("missed"); if (victimIdx === playerIdx) { bsc.setField(posX, posY, "miss"); } else { @@ -77,6 +86,51 @@ socket.on("shot missed", (victimIdx, posX, posY) => { } }); +socket.on("ship sunk", (victimIdx, ship) => { + switch (ship.rot) { + case 0: + multips = [1, 0]; + break; + + case 1: + multips = [0, 1]; + break; + + case 2: + multips = [-1, 0]; + break; + + case 3: + multips = [0, -1]; + break; + } + + let l = !ship.type ? ship.type + 1 : ship.type + 2; + if (victimIdx === playerIdx) { + for (let i = 0; i < l; i++) { + console.log("ourship"); + setTimeout(() => { + bsc.setField(ship.posX + multips[0] * i, ship.posY + multips[1] * i, "sunken"); + }, i * 150); + } + } else { + for (let i = 0; i < l; i++) { + console.log("theirship"); + setTimeout(() => { + bsc.setFieldEnemy(ship.posX + multips[0] * i, ship.posY + multips[1] * i, "sunken"); + }, i * 150); + } + } +}); + +socket.on("game finished", (winnerIdx) => { + if (winnerIdx === playerIdx) { + alert("Wygrałeś!"); + } else { + alert("Przegrałeś!"); + } +}); + socket.on('connect', () => { $(".cover h1").html("Oczekiwanie na serwer..."); }); diff --git a/public/assets/js/socket.js b/public/assets/js/socket.js index 0baa791..f4ea422 100644 --- a/public/assets/js/socket.js +++ b/public/assets/js/socket.js @@ -1,5 +1,6 @@ const socket = io(); +// Handling server-sent events socket.on("joined", (nick) => { lockUI(true); $("#oppNameField").html(nick); diff --git a/utils/battleships.js b/utils/battleships.js index a295b77..7fdfb23 100644 --- a/utils/battleships.js +++ b/utils/battleships.js @@ -37,7 +37,7 @@ export class GameInfo { await this.redis.json.set(key, '.state', 'action'); let nextPlayer = await this.redis.json.get(key, { path:'.nextPlayer' }); - nextPlayer = nextPlayer === 0 ? 1 : 0; + nextPlayer = !nextPlayer ? 1 : 0; await this.redis.json.set(key, '.nextPlayer', nextPlayer); const UTCTs = Math.floor((new Date()).getTime() / 1000 + 30); @@ -80,14 +80,14 @@ export class GameInfo { const hostId = (await this.redis.json.get(key, { path: '.hostId' })); const enemyIdx = socket.request.session.id === hostId ? 1 : 0; - const playerIdx = enemyIdx ? 0 : 1; + // const playerIdx = enemyIdx ? 0 : 1; let playerShips = await this.redis.json.get(key, { path: `.boards[${enemyIdx}].ships` }); var check = checkHit(playerShips, posX, posY); if (!check) { - return false; + return { status: 0 }; } var shotShip; @@ -97,12 +97,26 @@ export class GameInfo { if (ship.posX === check.originPosX & ship.posY === check.originPosY) { shotShip = ship; playerShips[i].hits[check.fieldIdx] = true; + if (!playerShips[i].hits.includes(false)) { + console.log(playerShips); + let gameFinished = true; + await this.redis.json.set(key, `.boards[${enemyIdx}].ships`, playerShips); + playerShips.every(ship => { + if (ship.hits.includes(false)) { + gameFinished = false; + return false; + } else { + return true; + } + }); + + return { status: 2, ship: ship, gameFinished: gameFinished }; + } } } await this.redis.json.set(key, `.boards[${enemyIdx}].ships`, playerShips); - - return true; + return { status: 1, ship: shotShip }; } } @@ -198,9 +212,13 @@ export function checkHit(ships, posX, posY) { break; } - for (let i = 0; i < ship.type + 2; i++) { - console.log(`boardRender[${ship.posX + multips[1] * i}][${ship.posY + multips[0] * i}]`) - boardRender[ship.posX + multips[1] * i][ship.posY + multips[0] * i] = {fieldIdx: i, originPosX: ship.posX, originPosY: ship.posY}; + let l = !ship.type ? ship.type + 1 : ship.type + 2; + for (let i = 0; i < l; i++) { + // console.log("a"); + let x = clamp(ship.posX + multips[0] * i, 0, 9); + let y = clamp(ship.posY + multips[1] * i, 0, 9); + + boardRender[x][y] = {fieldIdx: i, originPosX: ship.posX, originPosY: ship.posY}; } }); @@ -295,29 +313,6 @@ export function checkTurn(data, playerId) { } } -// let type = 3; -// let posX = 3; -// let posY = 0; -// let rot = 2; - -// let data = { -// hostId: "123456", -// state: "action", -// boards: [ -// { -// ships: [ -// { type: type, posX: posX, posY: posY, rot: rot, hits: [false, false, false] }, -// ], -// shots: [], -// }, -// { -// ships: [], -// shots: [], -// } -// ], -// nextPlayer: 0, -// } - -// checkHit(data, 1, 0, 0); - -// console.log(validateShipPosition(type, posX, posY, rot)); \ No newline at end of file +function clamp(n, min, max) { + return Math.min(Math.max(n, min), max); +} \ No newline at end of file diff --git a/views/board.handlebars b/views/board.handlebars index 8c3bde7..0a2525c 100644 --- a/views/board.handlebars +++ b/views/board.handlebars @@ -33,4 +33,5 @@ - \ No newline at end of file + + \ No newline at end of file diff --git a/views/index.handlebars b/views/index.handlebars index 3fa608f..d28fbe2 100644 --- a/views/index.handlebars +++ b/views/index.handlebars @@ -75,4 +75,5 @@ + \ No newline at end of file