Major changes

- Huge improvements in UI design and animations
- Improvements in code organisation (separated multiple functions into an util file)
- Multiple bug fixes and improvements to stability
- Client side events improved
This commit is contained in:
MaciejkaG 2024-03-02 18:28:33 +01:00
parent dbb3ad0f1d
commit 72da84c3d6
7 changed files with 210 additions and 54 deletions

View File

@ -3,10 +3,11 @@ import { createServer } from 'node:http';
import { Server } from 'socket.io';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { v4 as uuidv4 } from 'uuid';
import { v4 as uuidv4, validate } from 'uuid';
import session from "express-session";
import { engine } from 'express-handlebars';
import { createClient } from 'redis';
import * as bships from './utils/battleships.js'
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
@ -27,6 +28,8 @@ redis.on('error', err => console.log('Redis Client Error', err));
await redis.connect();
redis.flushDb();
const GInfo = new bships.GameInfo(redis, io);
app.set('trust proxy', 1);
const sessionMiddleware = session({
secret: uuidv4(),
@ -69,7 +72,7 @@ app.get("/game", async (req, res) => {
const game = await redis.json.get(`game:${req.query.id}`);
if (req.session.nickname == null) {
res.redirect("/setup");
} else if (req.query.id == null || game == null || game.state == "expired") {
} else if (req.query.id == null || game == null || game.state == "expired" || req.session.activeGame == null) {
res.status(400).send('badGameId');
} else {
res.render('board');
@ -89,7 +92,7 @@ io.on('connection', async (socket) => {
return;
}
if (!await isPlayerInGame(socket)) {
if (!await GInfo.isPlayerInGame(socket)) {
socket.on('whats my nick', (callback) => {
callback(session.nickname);
});
@ -132,18 +135,19 @@ io.on('connection', async (socket) => {
// Teraz utwórz objekt partii w trakcie w bazie Redis
const gameId = uuidv4();
redis.json.set(`game:${gameId}`, '$', {
hostId: opp.id,
state: "pregame",
boards: {
host: { // typ 2 to trójmasztowiec pozycja i obrót na planszy które pola zostały trafione
boards: [
{ // typ 2 to trójmasztowiec pozycja i obrót na planszy które pola zostały trafione
ships: [], // zawiera np. {type: 2, posX: 3, posY: 4, rot: 2, hits: [false, false, true]}
// pozycja na planszy czy strzał miał udział w zatopieniu statku?
shots: [], // zawiera np. {posX: 3, posY: 5, sunk: true}
},
guest: {
{
ships: [],
shots: [],
}
},
],
nextPlayer: 0,
});
@ -193,12 +197,12 @@ io.on('connection', async (socket) => {
});
socket.on('disconnecting', () => {
if (isPlayerInRoom(socket)) {
if (bships.isPlayerInRoom(socket)) {
io.to(socket.rooms[1]).emit("player left");
}
});
} else {
const playerGame = await getPlayerGameData(socket);
const playerGame = await GInfo.getPlayerGameData(socket);
if (playerGame.data.state === 'pregame') {
socket.join(playerGame.id);
@ -213,22 +217,33 @@ io.on('connection', async (socket) => {
let UTCTs = Math.floor((new Date()).getTime() / 1000 + 90);
io.to(playerGame.id).emit('turn update', { turn: 0, phase: "preparation", timerToUTC: UTCTs });
io.to(playerGame.id).emit();
bships.timer(90, () => GInfo.endPrepPhase(socket));
}
}
// socket.on('shoot', async () => {
// const playerGame = await getPlayerGameData(socket);
socket.on('place ship', async (type, posX, posY, rot) => {
const playerGame = await GInfo.getPlayerGameData(socket);
// if (playerGame.state === 'action') {
if (playerGame.state === 'preparation') {
// }
// });
}
});
socket.on('shoot', async () => {
const playerGame = await GInfo.getPlayerGameData(socket);
if (playerGame.state === 'action') {
if (bships.checkTurn(playerGame, socket.id)) {
}
}
});
socket.on('disconnecting', async () => {
io.to(playerGame.id).emit("player left");
redis.json.del(`game:${playerGame.id}`);
const playerGame = await GInfo.getPlayerGameData(socket);
if (playerGame !== null) {
AFKEnd(playerGame.id);
}
});
}
});
@ -241,35 +256,32 @@ function genID() {
return Math.floor(100000 + Math.random() * 900000).toString();
}
// async function emitToParty(partyuuid) {
// const party = gameData.find((element) => element.partyId===partyuuid);
function resetUserGame(req) {
req.session.reload((err) => {
if (err) return socket.disconnect();
// if (party!==null) {
// party.members.forEach(socketId => {
// io.to(socketId).emit();
// });
// }
// }
function isPlayerInRoom(socket) {
return !socket.rooms.size === 1;
req.session.activeGame = null;
req.session.save();
});
}
async function isPlayerInGame(socket) {
const game = await redis.json.get(`game:${socket.session.activeGame}`);
return game != null;
function endGame(gameId) {
const members = [...roomMemberIterator(gameId)];
for (let i = 0; i < members.length; i++) {
const sid = members[i][0];
const socket = io.sockets.sockets.get(sid);
resetUserGame(socket.request);
socket.leave(gameId);
}
redis.json.del(`game:${gameId}`);
}
async function getPlayerGameData(socket) {
const game = await redis.json.get(`game:${socket.session.activeGame}`);
return game == null ? null : {id: socket.session.activeGame, data: game};
function AFKEnd(gameId) {
io.to(gameId).emit("player left");
endGame(gameId);
}
function roomMemberIterator(id) {
return io.sockets.adapter.rooms.get(id).entries();
}
function getPlayerRoom(socket) {
return socket.rooms.entries()[1] === undefined ? null : socket.rooms.entries()[1][0];
}

View File

@ -1,9 +1,20 @@
body {
transition: opacity 0.1s;
}
.container {
display: none;
transition: opacity 0.175s;
}
@keyframes OutAnim {
from {transform: translateY(0)}
to {transform: translateY(5%);}
}
@keyframes InAnim {
from {transform: translateY(-5%)}
to {transform: translateY(0%);}
}
.header {
text-align: center;
}
/* index */

View File

@ -18,7 +18,12 @@ socket.on("player idx", (idx) => {
});
socket.on('turn update', (turnData) => {
turnData.turn == playerIdx ? $("#whosTurn").html("Ty") : $("#whosTurn").html("Przeciwnik");
if (turnData.phase === "preparation") {
$("#whosTurn").html("Faza przygotowań");
} else {
turnData.turn === playerIdx ? $("#whosTurn").html("Twoja tura") : $("#whosTurn").html("Tura przeciwnika");
}
timerDestination = turnData.timerToUTC;
gamePhase = turnData.phase;

View File

@ -1,10 +1,10 @@
var activeView;
function switchView(viewContainerId, useReplaceState=false) {
$(`.container`).css("opacity", 0);
$(`.container`).css({ opacity: 0, animation: "OutAnim 0.2s 1 ease" });
setTimeout(() => {
$(`.container`).css("display", "none");
$(`.container#${viewContainerId}`).css({"display": "flex", "opacity": "1"});
$(`.container#${viewContainerId}`).css({ display: "flex", opacity: 1, animation: "InAnim 0.2s 1 ease" });
let path = $(`.container#${viewContainerId}`).data("path");
let title = $(`.container#${viewContainerId}`).data("title");
if (useReplaceState) {
@ -14,7 +14,7 @@ function switchView(viewContainerId, useReplaceState=false) {
}
activeView = viewContainerId;
}, 150);
}, 200);
}
function lockUI(doLock) {

131
utils/battleships.js Normal file
View File

@ -0,0 +1,131 @@
// export class Client {
// constructor(clientId, clientSecret, redirectUri) {
// this.clientId = clientId;
// this.clientSecret = clientSecret;
// this.redirectUri = redirectUri;
// }
// getAccessToken(code) {
// }
// }
export class GameInfo {
constructor(redis, io) {
this.redis = redis;
this.io = io;
}
async isPlayerInGame(socket) {
const game = await this.redis.json.get(`game:${socket.session.activeGame}`);
return game != null;
}
async getPlayerGameData(socket) {
const game = await this.redis.json.get(`game:${socket.session.activeGame}`);
return game == null ? null : { id: socket.session.activeGame, data: game };
}
async endPrepPhase(socket) {
const gameId = socket.session.activeGame;
const key = `game:${gameId}`;
await this.redis.json.set(key, '$.state', 'action');
await this.redis.json.set(key, '$.nextPlayer', 0);
const UTCTs = Math.floor((new Date()).getTime() / 1000 + 30);
this.io.to(gameId).emit('turn update', { turn: 0, phase: "action", timerToUTC: UTCTs });
}
async passTurn(socket) {
const gameId = socket.session.activeGame;
const key = `game:${gameId}`;
await this.redis.json.set(key, '$.state', 'action');
let nextPlayer = await this.redis.json.get(key, '$.nextPlayer');
nextPlayer = nextPlayer === 0 ? 1 : 0;
await this.redis.json.set(key, '$.nextPlayer', nextPlayer);
const UTCTs = Math.floor((new Date()).getTime() / 1000 + 30);
this.io.to(gameId).emit('turn update', { turn: 0, phase: "action", timerToUTC: UTCTs });
}
}
export function isPlayerInRoom(socket) {
return !socket.rooms.size === 1;
}
var lastTimeChange = new Date().getTime();
export function timer(time, callback) {
let localLastChange = lastTimeChange;
let timeout = setTimeout(callback, time * 1000);
let interval = setInterval(() => {
if (timeout._destroyed) {
// timer is finished, stop monitoring turn changes
clearInterval(interval);
}
if (localLastChange != lastTimeChange) {
// timer has been reset
clearTimeout(timeout);
clearInterval(interval);
}
}, 200);
}
export function resetTimers() {
lastTimeChange = -lastTimeChange;
}
// export function getShipsLeft(data, playerIdx) {
// let shipsLeft = [4, 3, 2, 1];
// const playerShips = shipsLeft.boards[playerIdx].ships;
// playerShips.forEach(ship => {
// var isSunk = true;
// ship.hits.every(isHit => {
// isSunk = isHit;
// return isHit;
// });
// switch (ship.type) {
// case 0:
// shipsLeft[0]--;
// break;
// default:
// break;
// }
// });
// }
export function getShipsAvailable(data, playerIdx) {
let shipsLeft = [4, 3, 2, 1];
const playerShips = shipsLeft.boards[playerIdx].ships;
playerShips.forEach(ship => {
shipsLeft[ship.type]--;
});
return shipsLeft;
}
export function checkShot(data, playerIdx) {
playerIdx = playerIdx === 0 ? 1 : 0
data.boards[playerIdx]
}
export function checkTurn(data, playerId) {
// Check if it's player's turn
if (playerId == data.hostId) {
return data.nextPlayer === 0;
} else {
return data.nextPlayer === 1;
}
}
// timer(5, () => {
// console.log("out of time");
// });

View File

@ -18,7 +18,7 @@
<h3 class="controlsOwnBoard"><span class="important">R</span> Obrót statku</h3>
<h3><span class="important">B</span> Zamiana planszy</h3>
<span class="break"></span>
<h3>Ruch: <span class="dynamic" id="whosTurn">Przeciwnik</span></h3>
<h3><span class="dynamic" id="whosTurn"></span></h3>
<h2 class="important" id="timer">∞</h2>
</div>
<div class="boardContainer">

View File

@ -1,6 +1,7 @@
<h1 class="header">Statki</h1>
<div class="container" id="mainMenuView" data-title="Statki" data-path="/">
<div>
<h1>Statki</h1>
<h2>Wybierz tryb gry</h2>
<div class="modes">
<div id="pvpMenuButton">
@ -17,7 +18,6 @@
<div class="container" id="pvpMenuView" data-title="Statki / PvP" data-path="/pvp">
<div>
<h1>Statki</h1>
<h2>PvP</h2>
<div class="modes">
<div id="createGameButton">
@ -34,7 +34,6 @@
<div class="container" id="pvpCreateView" data-title="Statki / PvP / Stwórz" data-path="/pvp/create">
<div>
<h1>Statki</h1>
<h2>PvP / Stwórz</h2>
<div class="modes">
<div>
@ -49,7 +48,6 @@
<div class="container" id="pvpJoinView" data-title="Statki / PvP / Dołącz" data-path="/pvp/join">
<div>
<h1>Statki</h1>
<h2>PvP / Dołącz</h2>
<div class="modes">
<div>
@ -64,7 +62,6 @@
<div class="container" id="preparingGame" data-title="Statki / PvP / Przygotowywanie" data-path="/pvp/prepairing">
<div>
<h1>Statki</h1>
<h2>PvP / Wczytywanie</h2>
<div class="modes">
<div>