diff --git a/index.js b/index.js index 53afdea..ffe824b 100644 --- a/index.js +++ b/index.js @@ -22,6 +22,14 @@ import mysql from 'mysql'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); +var packageJSON; + +fs.readFile(path.join(__dirname, 'package.json'), function (err, data) { + if (err) throw err; + packageJSON = JSON.parse(data); +}); + + const app = express(); const flags = process.env.flags ? process.env.flags.split(",") : null; @@ -41,6 +49,12 @@ const redis = createClient(); redis.on('error', err => console.log('Redis Client Error', err)); await redis.connect(); +const prefixes = ["game:*", "timer:*", "loginTimer:*"]; + +prefixes.forEach(prefix => { + redis.eval(`for _,k in ipairs(redis.call('keys', '${prefix}')) do redis.call('del', k) end`, 0); +}); + const limiter = rateLimit({ windowMs: 40 * 1000, limit: 500, @@ -107,6 +121,19 @@ app.get('/privacy', (req, res) => { }); app.get('/', async (req, res) => { + const locale = new Lang(req.acceptsLanguages()); + + if (req.session.activeGame && await redis.json.get(req.session.activeGame)) { + res.render("error", { + helpers: { + error: "Your account is currently taking part in a game from another session", + fallback: "/", + t: (key) => { return locale.t(key) } + } + }); + return; + } + let login = loginState(req); if (login == 0) { @@ -143,7 +170,8 @@ app.get('/', async (req, res) => { res.render('index', { helpers: { - t: (key) => { return locale.t(key) } + t: (key) => { return locale.t(key) }, + ver: packageJSON.version } }); } else { @@ -164,7 +192,8 @@ app.get('/', async (req, res) => { res.render('index', { helpers: { - t: (key) => { return locale.t(key) } + t: (key) => { return locale.t(key) }, + ver: packageJSON.version } }); }); @@ -255,7 +284,7 @@ app.post('/api/login', (req, res) => { res.render("error", { helpers: { - error: "Wystąpił nieznany błąd logowania", + error: "Unknown login error occured", fallback: "/login", t: (key) => { return locale.t(key) } } @@ -267,7 +296,7 @@ app.post('/api/login', (req, res) => { res.render("error", { helpers: { - error: "Niepoprawny adres e-mail", + error: "Wrong e-mail address", fallback: "/login", t: (key) => { return locale.t(key) } } @@ -289,7 +318,7 @@ app.post('/api/auth', async (req, res) => { res.render("error", { helpers: { - error: "Niepoprawny kod logowania", + error: "Wrong authorisation code", fallback: "/auth", t: (key) => { return locale.t(key) } } @@ -300,7 +329,7 @@ app.post('/api/auth', async (req, res) => { res.render("error", { helpers: { - error: "Niepoprawny kod logowania", + error: "Wrong authorisation code", fallback: "/login", t: (key) => { return locale.t(key) } } @@ -309,16 +338,18 @@ app.post('/api/auth', async (req, res) => { }); app.post('/api/nickname', (req, res) => { - if (loginState(req) == 2 && req.session.nickname == null && req.body.nickname != null && 3 <= req.body.nickname.length && req.body.nickname.length <= 16) { + if (loginState(req) == 2 && req.body.nickname != null && 3 <= req.body.nickname.length && req.body.nickname.length <= 16) { req.session.nickname = req.body.nickname; req.session.activeGame = null; auth.setNickname(req.session.userId, req.body.nickname).then(() => { res.redirect('/'); }); } else { + const locale = new Lang(req.acceptsLanguages()); + res.render("error", { helpers: { - error: "Nazwa nie spełnia wymogów: Od 3 do 16 znaków, nie może być pusta", + error: "The nickname does not meet the requirements: length from 3 to 16 characters", fallback: "/nickname", t: (key) => { return locale.t(key) } } @@ -327,11 +358,22 @@ app.post('/api/nickname', (req, res) => { }); app.get('/game', async (req, res) => { - const game = await redis.json.get(`game:${req.query.id}`); + + // if (req.session.activeGame) { + // res.render("error", { + // helpers: { + // error: "Your account is currently taking part in a game from another session", + // fallback: "/", + // t: (key) => { return locale.t(key) } + // } + // }); + // return; + // } + if (req.session.nickname == null) { res.redirect('/setup'); - } else if (req.query.id == null || game == null || game.state == 'expired' || req.session.activeGame == null) { + } else if (!req.query.id || !game || !req.session.activeGame || req.session.activeGame !== req.query.id) { auth.getLanguage(req.session.userId).then(language => { var locale; if (language) { @@ -344,7 +386,7 @@ app.get('/game', async (req, res) => { res.render("error", { helpers: { - error: "Nie znaleziono wskazanej gry", + error: "The specified game was not found", fallback: "/", t: (key) => { return locale.t(key) } } @@ -477,7 +519,7 @@ io.on('connection', async (socket) => { }); } - if (!await GInfo.isPlayerInGame(socket) && session.nickname != null) { + if (!await GInfo.isPlayerInGame(socket) && session.nickname) { // if (session.nickname == null) { // socket.disconnect(); // return; @@ -576,6 +618,7 @@ io.on('connection', async (socket) => { // Teraz utwórz objekt partii w trakcie w bazie Redis const gameId = uuidv4(); redis.json.set(`game:${gameId}`, '$', { + type: 'pvp', hostId: opp.request.session.userId, state: "pregame", startTs: (new Date()).getTime() / 1000, @@ -628,6 +671,10 @@ io.on('connection', async (socket) => { const s = io.sockets.sockets.get(sid); s.leave(msg); }); + + GInfo.timer(gameId, 60, () => { + AFKEnd(gameId); + }); } else { callback({ status: "alreadyInLobby", @@ -636,6 +683,83 @@ io.on('connection', async (socket) => { } }); + socket.on('create pve', (difficulty, callback) => { + if (socket.rooms.size === 1) { + callback({ + status: "ok" + }); + + switch (difficulty) { + case 'simple': + difficulty = 0; + break; + + case 'smart': + difficulty = 1; + break; + + case 'overkill': + difficulty = 2; + break; + + default: + difficulty = 1; + break; + } + + // Teraz utwórz objekt partii w trakcie w bazie Redis + const gameId = uuidv4(); + redis.json.set(`game:${gameId}`, '$', { + type: 'pve', + difficulty: difficulty, + hostId: session.userId, + state: "pregame", + startTs: (new Date()).getTime() / 1000, + ready: [false, true], + boards: [ + { + ships: [], + shots: [], + stats: { + shots: 0, + hits: 0, + placedShips: 0, + sunkShips: 0, + }, + }, + { + ships: [], + shots: [], + stats: { + shots: 0, + hits: 0, + placedShips: 0, + sunkShips: 0, + }, + } + ], + nextPlayer: 0, + }); + + session.reload((err) => { + if (err) return socket.disconnect(); + + session.activeGame = gameId; + session.save(); + }); + + socket.emit("gameReady", gameId); + + GInfo.timer(gameId, 60, () => { + AFKEnd(gameId); + }); + } else { + callback({ + status: "alreadyInLobby", + }); + } + }); + socket.on('logout', () => { session.destroy(); }); @@ -645,7 +769,7 @@ io.on('connection', async (socket) => { io.to(socket.rooms[1]).emit("player left"); } }); - } else if (session.nickname != null) { + } else if (session.nickname && (await GInfo.getPlayerGameData(socket)).data.type === "pvp") { const playerGame = await GInfo.getPlayerGameData(socket); if (playerGame.data.state === 'pregame') { @@ -679,6 +803,8 @@ io.on('connection', async (socket) => { AFKEnd(playerGame.id); }); } + } else { + socket.disconnect(); } socket.on('ready', async (callback) => { @@ -775,7 +901,7 @@ io.on('connection', async (socket) => { if (bships.checkTurn(playerGame.data, session.userId)) { const enemyIdx = session.userId === playerGame.data.hostId ? 1 : 0; - let hit = await GInfo.shootShip(socket, posX, posY); + let hit = await GInfo.shootShip(socket, enemyIdx, posX, posY); await redis.json.arrAppend(`game:${playerGame.id}`, `.boards[${enemyIdx}].shots`, { posX: posX, posY: posY }); await GInfo.incrStat(socket, 'shots'); @@ -824,7 +950,7 @@ io.on('connection', async (socket) => { } else if (hit.status === -1) { const locale = new Lang(session.langs); - socket.emit("toast", locale.t("You have already shot at this field")); + socket.emit("toast", locale.t("board.You have already shot at this field")); return; } @@ -837,6 +963,223 @@ io.on('connection', async (socket) => { } }); + socket.on('disconnecting', async () => { + const playerGame = await GInfo.getPlayerGameData(socket); + if (playerGame !== null) { + AFKEnd(playerGame.id); + await GInfo.resetTimer(playerGame.id); + } + }); + } else if (session.nickname && (await GInfo.getPlayerGameData(socket)).data.type === "pve") { + const playerGame = await GInfo.getPlayerGameData(socket); + + if (playerGame.data.state === 'pregame') { + socket.join(playerGame.id); + if (io.sockets.adapter.rooms.get(playerGame.id).size === 1) { + GInfo.resetTimer(playerGame.id); + io.to(playerGame.id).emit('players ready'); + + socket.emit('player idx', 0); + + let UTCTs = Math.floor((new Date()).getTime() / 1000 + 180); + io.to(playerGame.id).emit('turn update', { turn: 0, phase: "preparation", timerToUTC: UTCTs }); + GInfo.timer(playerGame.id, 180, async () => { + finishPrepPhase(socket, playerGame); + placeAIShips(socket); + }); + + await redis.json.set(`game:${playerGame.id}`, '$.state', "preparation"); + } else if (io.sockets.adapter.rooms.get(playerGame.id).size > 2) { + socket.disconnect(); + } + } else { + socket.disconnect(); + } + + socket.on('ready', async (callback) => { + if (!(callback && typeof callback === 'function')) { + return; + } + + const playerGame = await GInfo.getPlayerGameData(socket); + + const playerIdx = 0; + const userNotReady = !playerGame.data.ready[playerIdx]; + + if (playerGame && playerGame.data.state === 'preparation' && userNotReady) { + await GInfo.setReady(socket); + const playerGame = await GInfo.getPlayerGameData(socket); + + if (playerGame.data.ready[0] && playerGame.data.ready[1]) { + // Both set ready + await GInfo.resetTimer(playerGame.id); + + callback(); + + await finishPrepPhase(socket, playerGame); + await placeAIShips(socket); + } + } + }); + + socket.on('place ship', async (type, posX, posY, rot) => { + const playerGame = await GInfo.getPlayerGameData(socket); + + if (type < 0 || type > 3) { + return; + } + + if (playerGame && playerGame.data.state === 'preparation') { + const playerShips = await GInfo.getPlayerShips(socket); + let canPlace = bships.validateShipPosition(playerShips, type, posX, posY, rot); + let shipAvailable = bships.getShipsAvailable(playerShips)[type] > 0; + + if (!canPlace) { + const locale = new Lang(session.langs); + + socket.emit("toast", locale.t("board.You cannot place a ship like this")); + } else if (!shipAvailable) { + const locale = new Lang(session.langs); + + socket.emit("toast", locale.t("board.You have ran out of ships of that type")); + } else { + await GInfo.placeShip(socket, { type: type, posX: posX, posY: posY, rot: rot, hits: Array.from(new Array(type + 1), () => false) }); + socket.emit("placed ship", { type: type, posX: posX, posY: posY, rot: rot }); + await GInfo.incrStat(socket, 'placedShips'); + } + } + }); + + socket.on('remove ship', async (posX, posY) => { + const playerGame = await GInfo.getPlayerGameData(socket); + + if (playerGame && playerGame.data.state === 'preparation') { + const deletedShip = await GInfo.removeShip(socket, posX, posY); + socket.emit("removed ship", { posX: posX, posY: posY, type: deletedShip.type }); + await GInfo.incrStat(socket, 'placedShips', -1); + } + }); + + socket.on('shoot', async (posX, posY) => { + let playerGame = await GInfo.getPlayerGameData(socket); + + if (playerGame && playerGame.data.state === 'action') { + if (bships.checkTurn(playerGame.data, session.userId)) { + const enemyIdx = 1; + + let hit = await GInfo.shootShip(socket, enemyIdx, posX, posY); + + await redis.json.arrAppend(`game:${playerGame.id}`, `.boards[${enemyIdx}].shots`, { posX: posX, posY: posY }); + await GInfo.incrStat(socket, 'shots'); + + if (!hit.status) { + socket.emit("shot missed", enemyIdx, posX, posY); + } else if (hit.status === 1) { + socket.emit("shot hit", enemyIdx, posX, posY); + await GInfo.incrStat(socket, 'hits'); + } else if (hit.status === 2) { + socket.emit("shot hit", enemyIdx, posX, posY); + await GInfo.incrStat(socket, 'hits'); + io.to(playerGame.id).emit("ship sunk", enemyIdx, hit.ship); + await GInfo.incrStat(socket, 'sunkShips'); + + if (hit.gameFinished) { + let hostNickname = session.nickname; + + let difficulty; + + switch (playerGame.data.difficulty) { + case 0: + difficulty = "simple"; + break; + + case 1: + difficulty = "smart"; + break; + + case 2: + difficulty = "overkill"; + break; + } + + let guestNickname = `AI (${difficulty})`; + + socket.emit("game finished", 0, guestNickname); + + playerGame = await GInfo.getPlayerGameData(socket); + auth.saveMatch(playerGame.id, (new Date).getTime() / 1000 - playerGame.data.startTs, "pve", session.userId, '77777777-77777777-77777777-77777777', playerGame.data.boards, 1, difficulty); + + GInfo.resetTimer(playerGame.id); + endGame(playerGame.id); + return; + } + } else if (hit.status === -1) { + const locale = new Lang(session.langs); + + socket.emit("toast", locale.t("board.You have already shot at this field")); + return; + } + + await GInfo.passTurn(socket); + + [posX, posY] = await GInfo.makeAIMove(socket, playerGame.difficulty); + + hit = await GInfo.shootShip(socket, 0, posX, posY); + + await redis.json.arrAppend(`game:${playerGame.id}`, `.boards[0].shots`, { posX: posX, posY: posY }); + await GInfo.incrStat(socket, 'shots', 1, 1); + + if (!hit.status) { + socket.emit("shot missed", 0, posX, posY); + } else if (hit.status === 1) { + socket.emit("shot hit", 0, posX, posY); + await GInfo.incrStat(socket, 'hits', 1, 1); + } else if (hit.status === 2) { + socket.emit("shot hit", 0, posX, posY); + await GInfo.incrStat(socket, 'hits', 1, 1); + socket.emit("ship sunk", 0, hit.ship); + await GInfo.incrStat(socket, 'sunkShips', 1, 1); + + if (hit.gameFinished) { + let difficulty; + + switch (playerGame.data.difficulty) { + case 0: + difficulty = "simple"; + break; + + case 1: + difficulty = "smart"; + break; + + case 2: + difficulty = "overkill"; + break; + } + + let guestNickname = `AI (${difficulty})`; + + socket.emit("game finished", 1, guestNickname); + + playerGame = await GInfo.getPlayerGameData(socket); + auth.saveMatch(playerGame.id, (new Date).getTime() / 1000 - playerGame.data.startTs, "pve", session.userId, '77777777-77777777-77777777-77777777', playerGame.data.boards, 0, difficulty); + + GInfo.resetTimer(playerGame.id); + endGame(playerGame.id); + return; + } + } + + await GInfo.passTurn(socket); + + GInfo.resetTimer(playerGame.id); + GInfo.timer(playerGame.id, 30, () => { + AFKEnd(playerGame.id); + }); + } + } + }); + socket.on('disconnecting', async () => { const playerGame = await GInfo.getPlayerGameData(socket); if (playerGame !== null) { @@ -876,7 +1219,7 @@ function endGame(gameId) { } } - redis.json.del(`game:${gameId}`); + redis.unlink(`game:${gameId}`); } function AFKEnd(gameId) { @@ -884,6 +1227,34 @@ function AFKEnd(gameId) { endGame(gameId); } +async function finishPrepPhase(socket, playerGame) { + await GInfo.endPrepPhase(socket); + + const members = [...roomMemberIterator(playerGame.id)]; + for (let i = 0; i < members.length; i++) { + const sid = members[i][0]; + const socket = io.sockets.sockets.get(sid); + + let placedShips = await GInfo.depleteShips(socket); + placedShips.forEach(shipData => { + socket.emit("placed ship", shipData) + }); + + if (placedShips.length > 0) { + const locale = new Lang(socket.session.langs); + socket.emit("toast", locale.t("board.Your remaining ships have been randomly placed")) + } + } + + GInfo.timer(playerGame.id, 30, () => { + AFKEnd(playerGame.id); + }); +} + +async function placeAIShips(socket, playerGame) { + await GInfo.depleteShips(socket, 1); +} + function roomMemberIterator(id) { return io.sockets.adapter.rooms.get(id) == undefined ? null : io.sockets.adapter.rooms.get(id).entries(); } @@ -926,28 +1297,4 @@ function checkFlag(key) { } else { return false; } -} - -async function finishPrepPhase(socket, playerGame) { - await GInfo.endPrepPhase(socket); - - const members = [...roomMemberIterator(playerGame.id)]; - for (let i = 0; i < members.length; i++) { - const sid = members[i][0]; - const socket = io.sockets.sockets.get(sid); - - let placedShips = await GInfo.depleteShips(socket); - placedShips.forEach(shipData => { - socket.emit("placed ship", shipData) - }); - - if (placedShips.length > 0) { - const locale = new Lang(socket.session.langs); - socket.emit("toast", locale.t("board.Your remaining ships have been randomly placed")) - } - } - - GInfo.timer(playerGame.id, 30, () => { - AFKEnd(playerGame.id); - }); } \ No newline at end of file diff --git a/lang/en.json b/lang/en.json index 2119106..8c0ade0 100644 --- a/lang/en.json +++ b/lang/en.json @@ -65,6 +65,22 @@ "You will be redirected soon": "You will be redirected soon", "Opponent:": "Opponent" }, + "PvE": { + "Create": "PvE / Create", + "Choose the difficulty mode": "Choose the difficulty mode", + "difficulty": { + "Simple": { + "description": "In this mode, the bot is just randomly shooting at fields, without any intelligence." + }, + "Smart": { + "description": "In this mode, the bot understands the rules of the game and will try to predict where your ships could be and knows where they couldn't." + }, + "Overkill": { + "description": "This mode is an absolute overkill - the bot knows exact positions of all your ships and will shoot at them in random order with 100% accuracy. To win in this mode, you need to have 100% accuracy for the entire game." + } + }, + "Begin": "Begin" + }, "Profile": { "Loading": "Loading...", "Player since:": "Player since:", @@ -78,7 +94,12 @@ }, "Settings": { "General": "General", - "Log out": "Log out" + "Account": "Account", + + "Log out": "Log out", + + "Current nickname:": "Current nickname:", + "Change nickname": "Change nickname" }, "General": { "Unknown error occured": "Unknown error occured", @@ -123,7 +144,7 @@ "Four-masted": "Four-masted", "Available:": "Available:", - "Sunk ships": "Sunk ships", + "To sunk": "To sunk", "Single-mastedPlu": "Single-masted:", "Two-mastedPlu": "Two-masted:", "Three-mastedPlu": "Three-masted:", diff --git a/lang/pl.json b/lang/pl.json index 61cf4bd..5b206fe 100644 --- a/lang/pl.json +++ b/lang/pl.json @@ -65,6 +65,22 @@ "You will be redirected soon": "Wkrótce nastąpi przekierowanie", "Opponent:": "Przeciwnik" }, + "PvE": { + "Create": "PvE / Stwórz", + "Choose the difficulty mode": "Wybierz tryb trudności", + "difficulty": { + "Simple": { + "description": "W tym trybie, bot po prostu losowo strzela w pola, bez żadnej inteligencji." + }, + "Smart": { + "description": "W tym trybie, bot rozumie zasady gry i próbuje przewidywać gdzie Twoje statki mogą być oraz wie gdzie ich na pewno nie ma. Jest to tryb najbardziej zbliżony to przeciętnego gracza." + }, + "Overkill": { + "description": "Ten tryb to kompletna przesada - bot zna dokładne pozycje wszystkich Twoich statków i będzie strzelał do nich w losowej kolejności ze 100% celnością. By wygrać na tym trybie, musisz mieć 100% celność przez całą grę." + } + }, + "Begin": "Rozpocznij" + }, "Profile": { "Loading": "Wczytywanie...", "Player since:": "Gracz od:", @@ -79,7 +95,12 @@ }, "Settings": { "General": "Ogólne", - "Log out": "Wyloguj się" + "Account": "Konto", + + "Log out": "Wyloguj się", + + "Current nickname:": "Aktualny nickname:", + "Change nickname": "Zmień nickname" }, "General": { "Unknown error occured": "Wystąpił nieznany błąd", @@ -124,7 +145,7 @@ "Four-masted": "Czteromasztowiec", "Available:": "Dostępne:", - "Sunk ships": "Zatopione statki", + "To sunk": "Do zatopienia", "Single-mastedPlu": "Jednomasztowce:", "Two-mastedPlu": "Dwumasztowce:", "Three-mastedPlu": "Trójmasztowce:", diff --git a/package.json b/package.json index c66c965..3a83ff9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "statki-backend", - "version": "0.7.0", + "version": "0.7.4", "description": "Backend do gry w statki", "main": "index.js", "type": "module", diff --git a/public/pwa/manifest.json b/public/app/manifest.json similarity index 69% rename from public/pwa/manifest.json rename to public/app/manifest.json index d56aa76..ce20e63 100644 --- a/public/pwa/manifest.json +++ b/public/app/manifest.json @@ -4,7 +4,7 @@ "start_url": "/", "background_color": "black", "theme_color": "black", - "orientation": "landscape", + "orientation": "any", "icons": [ { "src": "/assets/img/statki-logo-crop.png", @@ -50,6 +50,27 @@ "type": "image/png", "form_factor": "wide", "label": "Preparation phase" + }, + { + "src": "/assets/img/screenshot_mainmenu_mobile.png", + "sizes": "1082x2402", + "type": "image/png", + "form_factor": "narrow", + "label": "Main menu screen" + }, + { + "src": "/assets/img/screenshot_profile_mobile.png", + "sizes": "1082x2402", + "type": "image/png", + "form_factor": "narrow", + "label": "Profile view" + }, + { + "src": "/assets/img/screenshot_create_mobile.png", + "sizes": "1082x2402", + "type": "image/png", + "form_factor": "narrow", + "label": "Create game screen" } ], "display": "standalone" diff --git a/public/assets/css/main.css b/public/assets/css/main.css index efc086e..c228307 100644 --- a/public/assets/css/main.css +++ b/public/assets/css/main.css @@ -108,13 +108,12 @@ nav span:hover { } #pvpCreateView .modes div { - height: 17rem; width: 15rem; background-color: black; border: solid 1px white; border-radius: 15px; user-select: none; - padding: 1rem 3rem; + padding: 2rem 3rem; display: flex; flex-direction: column; gap: 0.5rem; @@ -351,6 +350,30 @@ nav span:hover { text-decoration: underline; } +#settingsView .versionInfo { + margin-top: 3rem; + font-family: 'Roboto Mono', monospace; + font-size: 15px; +} + +#settingsView button { + background-color: black; + color: white; + border-radius: 15px; + border: 1px solid white; + text-align: center; + padding: 0.5rem 2rem; + outline: none; + cursor: pointer; + font-size: 1rem; + transition: all 0.3s; +} + +#settingsView button:hover { + background-color: white; + color: black; +} + #logout { color: var(--danger); cursor: pointer; @@ -360,4 +383,61 @@ nav span:hover { #logout:hover { opacity: 0.6; +} + +/* PvE */ +#vsAiView .modes { + display: flex; + flex-wrap: wrap; + justify-content: center; + gap: 2rem; +} + +#vsAiView .modes div { + /* height: 7rem; */ + width: 15rem; + background-color: black; + border: solid 1px white; + border-radius: 15px; + user-select: none; + padding: 2rem 3rem; +} + +#vsAiView form { + display: flex; + flex-direction: column; + gap: 0.5rem; +} + +#vsAiView form input { + background-color: black; + color: white; + border-radius: 15px; + border: 1px solid white; + font-size: 1.5rem; + text-align: center; + padding: 0.5rem 2rem; + outline: none; +} + +#vsAiView form input[type=submit] { + cursor: pointer; + font-size: 1rem; + transition: all 0.3s; +} + +#vsAiView form input[type=submit]:hover { + background-color: white; + color: black; +} + +#vsAiView select { + padding: 0.5rem 1rem; + padding: 0.5rem 2rem; + font-size: 1rem; + background-color: black; + border: solid 1px white; + color: white; + border-radius: 15px; + outline: none; } \ No newline at end of file diff --git a/public/assets/css/responsive.css b/public/assets/css/responsive.css index 10bd320..b522e23 100644 --- a/public/assets/css/responsive.css +++ b/public/assets/css/responsive.css @@ -17,7 +17,6 @@ @media only screen and (max-width: 820px) { #pvpCreateView .modes div { max-width: 90vw; - height: 19rem; padding: 2rem 1.5rem; } diff --git a/public/assets/img/screenshot_create_mobile.png b/public/assets/img/screenshot_create_mobile.png new file mode 100644 index 0000000..1ed99a4 Binary files /dev/null and b/public/assets/img/screenshot_create_mobile.png differ diff --git a/public/assets/img/screenshot_mainmenu_mobile.png b/public/assets/img/screenshot_mainmenu_mobile.png new file mode 100644 index 0000000..332b291 Binary files /dev/null and b/public/assets/img/screenshot_mainmenu_mobile.png differ diff --git a/public/assets/img/screenshot_profile_mobile.png b/public/assets/img/screenshot_profile_mobile.png new file mode 100644 index 0000000..99a009b Binary files /dev/null and b/public/assets/img/screenshot_profile_mobile.png differ diff --git a/public/assets/js/battleships-lib.js b/public/assets/js/battleships-lib.js index 50166ef..48bfa3e 100644 --- a/public/assets/js/battleships-lib.js +++ b/public/assets/js/battleships-lib.js @@ -22,20 +22,18 @@ class Battleships { } getField(x, y) { - if (0 <= x && x < this.boardSize && 0 <= y && y < this.boardSize) { - x++; - y++; - return $(`#board .row:nth-child(${y}) .field:nth-child(${x})`); + console.log(x, y); + if (0 <= x && x < this.boardSize && 0 <= y && y <= this.boardSize) { + return $(`#board .row:nth-child(${y + 1}) .field:nth-child(${x + 1})`); } else { throw new RangeError("getField position out of range."); } } getFieldSecondary(x, y) { - if (0 <= x && x < this.boardSize && 0 <= y && y < this.boardSize) { - x++; - y++; - return $(`#secondaryBoard .row:nth-child(${y}) .field:nth-child(${x})`); + console.log(x, y); + if (0 <= x && x < this.boardSize && 0 <= y && y <= this.boardSize) { + return $(`#secondaryBoard .row:nth-child(${y + 1}) .field:nth-child(${x + 1})`); } else { throw new RangeError("getField position out of range."); } diff --git a/public/assets/js/landing.js b/public/assets/js/landing.js index a45633d..24cf5b0 100644 --- a/public/assets/js/landing.js +++ b/public/assets/js/landing.js @@ -4,37 +4,39 @@ String.prototype.replaceAt = function (index, replacement) { const socket = io(); -const charset = ["0", "1", "!", "@", "#", "$", "%", "&"]; +// Temporarily commented out, as it causes huge graphical glitches -const initialContent = $("#scrolldowntext").html(); +// const charset = ["0", "1", "!", "@", "#", "$", "%", "&"]; -setInterval(() => { - var content = $("#scrolldowntext").html(); - const len = content.length; +// const initialContent = $("#scrolldowntext").html(); - for (let i = 0; i < len; i++) { - const duration = Math.random() * 20 + 40; +// setInterval(() => { +// var content = $("#scrolldowntext").html(); +// const len = content.length; - setTimeout(() => { - let previousChar = content.charAt(i); +// for (let i = 0; i < len; i++) { +// const duration = Math.random() * 20 + 40; - let randomChar = charset[Math.floor(Math.random() * charset.length)]; - content = content.replaceAt(i, randomChar); +// setTimeout(() => { +// let previousChar = content.charAt(i); - $("#scrolldowntext").html(content); +// let randomChar = charset[Math.floor(Math.random() * charset.length)]; +// content = content.replaceAt(i, randomChar); - setTimeout(() => { - content = content.replaceAt(i, previousChar); - $("#scrolldowntext").html(content); +// $("#scrolldowntext").html(content); - if (i == len - 1) { - content = initialContent; - $("#scrolldowntext").html(initialContent); - } - }, duration * len + duration * i); - }, duration * i); - } -}, 5000); +// setTimeout(() => { +// content = content.replaceAt(i, previousChar); +// $("#scrolldowntext").html(content); + +// if (i == len - 1) { +// content = initialContent; +// $("#scrolldowntext").html(initialContent); +// } +// }, duration * len + duration * i); +// }, duration * i); +// } +// }, 5000); document.addEventListener("wheel", (event) => { if (event.deltaY > 0) { diff --git a/public/assets/js/service-worker.js b/public/assets/js/service-worker.js index aee2207..3250efa 100644 --- a/public/assets/js/service-worker.js +++ b/public/assets/js/service-worker.js @@ -22,4 +22,5 @@ self.addEventListener("install", installEvent => { cache.addAll(assets); }) ); -}); \ No newline at end of file +}); + diff --git a/public/assets/js/socket-game.js b/public/assets/js/socket-game.js index dca9258..8f23d56 100644 --- a/public/assets/js/socket-game.js +++ b/public/assets/js/socket-game.js @@ -50,11 +50,9 @@ if ($(window).width() <= 820) { } $('#board .field').on('click', function () { - if (new Date().getTime() / 1000 - lastTimeClick > 0.3) { - if ($(window).width() > 820) { - socket.emit("place ship", selectedShip, $(this).data('pos-x'), $(this).data('pos-y'), shipRotation); - lastTimeClick = new Date().getTime() / 1000; - } + if (new Date().getTime() / 1000 - lastTimeClick > 0.3 && $(window).width() > 820 && !postPrep) { + socket.emit("place ship", selectedShip, $(this).data('pos-x'), $(this).data('pos-y'), shipRotation); + lastTimeClick = new Date().getTime() / 1000; } }); @@ -66,11 +64,9 @@ function manualPlace(posX, posY) { } $('#secondaryBoard .field').on('click', function () { - if (new Date().getTime() / 1000 - lastTimeClick > 0.3) { - if ($(window).width() > 820) { - socket.emit("shoot", $(this).data('pos-x'), $(this).data('pos-y')); - lastTimeClick = new Date().getTime() / 1000; - } + if (new Date().getTime() / 1000 - lastTimeClick > 0.3 && $(window).width() > 820 && myTurn) { + socket.emit("shoot", $(this).data('pos-x'), $(this).data('pos-y')); + lastTimeClick = new Date().getTime() / 1000; } }); @@ -178,7 +174,7 @@ socket.on("ship sunk", (victimIdx, ship) => { break; } - let l = !ship.type ? ship.type + 1 : ship.type + 2; + let l = ship.type + 1; if (victimIdx === playerIdx) { for (let i = 0; i < l; i++) { setTimeout(() => { @@ -332,10 +328,10 @@ function getAccuracy() { } function updateShipsSunk() { - $("#singlemasted").html(shipsSunk[0]); - $("#twomasted").html(shipsSunk[1]); - $("#threemasted").html(shipsSunk[2]); - $("#fourmasted").html(shipsSunk[3]); + $("#singlemasted").html(4 - shipsSunk[0]); + $("#twomasted").html(3 - shipsSunk[1]); + $("#threemasted").html(2 - shipsSunk[2]); + $("#fourmasted").html(1 - shipsSunk[3]); } function readyUp() { diff --git a/public/assets/js/socket.js b/public/assets/js/socket.js index 2e72f88..91b4fbd 100644 --- a/public/assets/js/socket.js +++ b/public/assets/js/socket.js @@ -7,16 +7,15 @@ socket.on("joined", (nick) => { returnLock = false; lockUI(true); $("#oppNameField").html(nick); - switchView("preparingGame"); lockUI(false); + switchView("preparingGame"); console.log("Player joined the game:", nick); }); socket.on("player left", () => { - lockUI(true); - switchView("mainMenuView"); lockUI(false); + switchView("mainMenuView"); console.log("Player left the game"); }); @@ -62,11 +61,12 @@ $("#languages").on("change", function() { socket.emit("my profile", (profile) => { console.log("Received user data. UID:", profile.uid); + console.log("Profile data:", profile); // General profile data let options = { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' }; $("#playerSince").html(new Date(profile.profile.account_creation).toLocaleDateString(undefined, options)); - $("#nickname").html(profile.profile.nickname); + $(".nickname").html(profile.profile.nickname); // Profile stats $("#monthlyPlayed").html(profile.stats.monthly_matches); @@ -89,7 +89,8 @@ socket.emit("my profile", (profile) => { const duration = `${minutes}:${seconds}`; - matchHistoryDOM += `
- {{ t 'board.Single-mastedPlu' }} 0
- {{ t 'board.Two-mastedPlu' }} 0
- {{ t 'board.Three-mastedPlu' }} 0
- {{ t 'board.Four-mastedPlu' }} 0
+ {{ t 'board.Single-mastedPlu' }} 4
+ {{ t 'board.Two-mastedPlu' }} 3
+ {{ t 'board.Three-mastedPlu' }} 2
+ {{ t 'board.Four-mastedPlu' }} 1
{{ t 'menu.index.Play against another player' }}
{{ t 'menu.index.Play against the computer' }}
statki ver. {{ ver }}
© 2024 MCJK {{ t 'landing.Privacy policy' }}