mirror of
https://github.com/MaciejkaG/statki.git
synced 2024-11-30 03:42:55 +01:00
Major changes
- Improved PWA support on mobile. - Added Vs. AI game menu. - Added the first AI algorithm. - Vs. AI is now available with every option leading to simple bot - Fixed a bug where placing ships in certain rotation around the boundaries of the board would wrongly colour them when sunk. - Improved code clarity and comments - Added an option in the settings to change your nickname - Added version display in the settings - Slightly improved box scaling and layout - Made multiple improvements to the PWA support - Added multiple minor features - Brought back full SPA URL functionality, now if you copy a URL to specific view/page in the menu and go into it, you will actually arrive at that page if it's not restricted.
This commit is contained in:
parent
27342f1c2c
commit
a212161733
427
index.js
427
index.js
@ -22,6 +22,14 @@ import mysql from 'mysql';
|
|||||||
const __filename = fileURLToPath(import.meta.url);
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
const __dirname = path.dirname(__filename);
|
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 app = express();
|
||||||
|
|
||||||
const flags = process.env.flags ? process.env.flags.split(",") : null;
|
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));
|
redis.on('error', err => console.log('Redis Client Error', err));
|
||||||
await redis.connect();
|
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({
|
const limiter = rateLimit({
|
||||||
windowMs: 40 * 1000,
|
windowMs: 40 * 1000,
|
||||||
limit: 500,
|
limit: 500,
|
||||||
@ -107,6 +121,19 @@ app.get('/privacy', (req, res) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
app.get('/', async (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);
|
let login = loginState(req);
|
||||||
|
|
||||||
if (login == 0) {
|
if (login == 0) {
|
||||||
@ -143,7 +170,8 @@ app.get('/', async (req, res) => {
|
|||||||
|
|
||||||
res.render('index', {
|
res.render('index', {
|
||||||
helpers: {
|
helpers: {
|
||||||
t: (key) => { return locale.t(key) }
|
t: (key) => { return locale.t(key) },
|
||||||
|
ver: packageJSON.version
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
@ -164,7 +192,8 @@ app.get('/', async (req, res) => {
|
|||||||
|
|
||||||
res.render('index', {
|
res.render('index', {
|
||||||
helpers: {
|
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", {
|
res.render("error", {
|
||||||
helpers: {
|
helpers: {
|
||||||
error: "Wystąpił nieznany błąd logowania",
|
error: "Unknown login error occured",
|
||||||
fallback: "/login",
|
fallback: "/login",
|
||||||
t: (key) => { return locale.t(key) }
|
t: (key) => { return locale.t(key) }
|
||||||
}
|
}
|
||||||
@ -267,7 +296,7 @@ app.post('/api/login', (req, res) => {
|
|||||||
|
|
||||||
res.render("error", {
|
res.render("error", {
|
||||||
helpers: {
|
helpers: {
|
||||||
error: "Niepoprawny adres e-mail",
|
error: "Wrong e-mail address",
|
||||||
fallback: "/login",
|
fallback: "/login",
|
||||||
t: (key) => { return locale.t(key) }
|
t: (key) => { return locale.t(key) }
|
||||||
}
|
}
|
||||||
@ -289,7 +318,7 @@ app.post('/api/auth', async (req, res) => {
|
|||||||
|
|
||||||
res.render("error", {
|
res.render("error", {
|
||||||
helpers: {
|
helpers: {
|
||||||
error: "Niepoprawny kod logowania",
|
error: "Wrong authorisation code",
|
||||||
fallback: "/auth",
|
fallback: "/auth",
|
||||||
t: (key) => { return locale.t(key) }
|
t: (key) => { return locale.t(key) }
|
||||||
}
|
}
|
||||||
@ -300,7 +329,7 @@ app.post('/api/auth', async (req, res) => {
|
|||||||
|
|
||||||
res.render("error", {
|
res.render("error", {
|
||||||
helpers: {
|
helpers: {
|
||||||
error: "Niepoprawny kod logowania",
|
error: "Wrong authorisation code",
|
||||||
fallback: "/login",
|
fallback: "/login",
|
||||||
t: (key) => { return locale.t(key) }
|
t: (key) => { return locale.t(key) }
|
||||||
}
|
}
|
||||||
@ -309,16 +338,18 @@ app.post('/api/auth', async (req, res) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
app.post('/api/nickname', (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.nickname = req.body.nickname;
|
||||||
req.session.activeGame = null;
|
req.session.activeGame = null;
|
||||||
auth.setNickname(req.session.userId, req.body.nickname).then(() => {
|
auth.setNickname(req.session.userId, req.body.nickname).then(() => {
|
||||||
res.redirect('/');
|
res.redirect('/');
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
|
const locale = new Lang(req.acceptsLanguages());
|
||||||
|
|
||||||
res.render("error", {
|
res.render("error", {
|
||||||
helpers: {
|
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",
|
fallback: "/nickname",
|
||||||
t: (key) => { return locale.t(key) }
|
t: (key) => { return locale.t(key) }
|
||||||
}
|
}
|
||||||
@ -327,11 +358,22 @@ app.post('/api/nickname', (req, res) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
app.get('/game', async (req, res) => {
|
app.get('/game', async (req, res) => {
|
||||||
|
|
||||||
const game = await redis.json.get(`game:${req.query.id}`);
|
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) {
|
if (req.session.nickname == null) {
|
||||||
res.redirect('/setup');
|
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 => {
|
auth.getLanguage(req.session.userId).then(language => {
|
||||||
var locale;
|
var locale;
|
||||||
if (language) {
|
if (language) {
|
||||||
@ -344,7 +386,7 @@ app.get('/game', async (req, res) => {
|
|||||||
|
|
||||||
res.render("error", {
|
res.render("error", {
|
||||||
helpers: {
|
helpers: {
|
||||||
error: "Nie znaleziono wskazanej gry",
|
error: "The specified game was not found",
|
||||||
fallback: "/",
|
fallback: "/",
|
||||||
t: (key) => { return locale.t(key) }
|
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) {
|
// if (session.nickname == null) {
|
||||||
// socket.disconnect();
|
// socket.disconnect();
|
||||||
// return;
|
// return;
|
||||||
@ -576,6 +618,7 @@ io.on('connection', async (socket) => {
|
|||||||
// Teraz utwórz objekt partii w trakcie w bazie Redis
|
// Teraz utwórz objekt partii w trakcie w bazie Redis
|
||||||
const gameId = uuidv4();
|
const gameId = uuidv4();
|
||||||
redis.json.set(`game:${gameId}`, '$', {
|
redis.json.set(`game:${gameId}`, '$', {
|
||||||
|
type: 'pvp',
|
||||||
hostId: opp.request.session.userId,
|
hostId: opp.request.session.userId,
|
||||||
state: "pregame",
|
state: "pregame",
|
||||||
startTs: (new Date()).getTime() / 1000,
|
startTs: (new Date()).getTime() / 1000,
|
||||||
@ -628,6 +671,10 @@ io.on('connection', async (socket) => {
|
|||||||
const s = io.sockets.sockets.get(sid);
|
const s = io.sockets.sockets.get(sid);
|
||||||
s.leave(msg);
|
s.leave(msg);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
GInfo.timer(gameId, 60, () => {
|
||||||
|
AFKEnd(gameId);
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
callback({
|
callback({
|
||||||
status: "alreadyInLobby",
|
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', () => {
|
socket.on('logout', () => {
|
||||||
session.destroy();
|
session.destroy();
|
||||||
});
|
});
|
||||||
@ -645,7 +769,7 @@ io.on('connection', async (socket) => {
|
|||||||
io.to(socket.rooms[1]).emit("player left");
|
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);
|
const playerGame = await GInfo.getPlayerGameData(socket);
|
||||||
|
|
||||||
if (playerGame.data.state === 'pregame') {
|
if (playerGame.data.state === 'pregame') {
|
||||||
@ -679,6 +803,8 @@ io.on('connection', async (socket) => {
|
|||||||
AFKEnd(playerGame.id);
|
AFKEnd(playerGame.id);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
socket.disconnect();
|
||||||
}
|
}
|
||||||
|
|
||||||
socket.on('ready', async (callback) => {
|
socket.on('ready', async (callback) => {
|
||||||
@ -775,7 +901,7 @@ io.on('connection', async (socket) => {
|
|||||||
if (bships.checkTurn(playerGame.data, session.userId)) {
|
if (bships.checkTurn(playerGame.data, session.userId)) {
|
||||||
const enemyIdx = session.userId === playerGame.data.hostId ? 1 : 0;
|
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 redis.json.arrAppend(`game:${playerGame.id}`, `.boards[${enemyIdx}].shots`, { posX: posX, posY: posY });
|
||||||
await GInfo.incrStat(socket, 'shots');
|
await GInfo.incrStat(socket, 'shots');
|
||||||
@ -824,7 +950,7 @@ io.on('connection', async (socket) => {
|
|||||||
} else if (hit.status === -1) {
|
} else if (hit.status === -1) {
|
||||||
const locale = new Lang(session.langs);
|
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;
|
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 () => {
|
socket.on('disconnecting', async () => {
|
||||||
const playerGame = await GInfo.getPlayerGameData(socket);
|
const playerGame = await GInfo.getPlayerGameData(socket);
|
||||||
if (playerGame !== null) {
|
if (playerGame !== null) {
|
||||||
@ -876,7 +1219,7 @@ function endGame(gameId) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
redis.json.del(`game:${gameId}`);
|
redis.unlink(`game:${gameId}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
function AFKEnd(gameId) {
|
function AFKEnd(gameId) {
|
||||||
@ -884,6 +1227,34 @@ function AFKEnd(gameId) {
|
|||||||
endGame(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) {
|
function roomMemberIterator(id) {
|
||||||
return io.sockets.adapter.rooms.get(id) == undefined ? null : io.sockets.adapter.rooms.get(id).entries();
|
return io.sockets.adapter.rooms.get(id) == undefined ? null : io.sockets.adapter.rooms.get(id).entries();
|
||||||
}
|
}
|
||||||
@ -927,27 +1298,3 @@ function checkFlag(key) {
|
|||||||
return false;
|
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);
|
|
||||||
});
|
|
||||||
}
|
|
25
lang/en.json
25
lang/en.json
@ -65,6 +65,22 @@
|
|||||||
"You will be redirected soon": "You will be redirected soon",
|
"You will be redirected soon": "You will be redirected soon",
|
||||||
"Opponent:": "Opponent"
|
"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": {
|
"Profile": {
|
||||||
"Loading": "Loading...",
|
"Loading": "Loading...",
|
||||||
"Player since:": "Player since:",
|
"Player since:": "Player since:",
|
||||||
@ -78,7 +94,12 @@
|
|||||||
},
|
},
|
||||||
"Settings": {
|
"Settings": {
|
||||||
"General": "General",
|
"General": "General",
|
||||||
"Log out": "Log out"
|
"Account": "Account",
|
||||||
|
|
||||||
|
"Log out": "Log out",
|
||||||
|
|
||||||
|
"Current nickname:": "Current nickname:",
|
||||||
|
"Change nickname": "Change nickname"
|
||||||
},
|
},
|
||||||
"General": {
|
"General": {
|
||||||
"Unknown error occured": "Unknown error occured",
|
"Unknown error occured": "Unknown error occured",
|
||||||
@ -123,7 +144,7 @@
|
|||||||
"Four-masted": "Four-masted",
|
"Four-masted": "Four-masted",
|
||||||
"Available:": "Available:",
|
"Available:": "Available:",
|
||||||
|
|
||||||
"Sunk ships": "Sunk ships",
|
"To sunk": "To sunk",
|
||||||
"Single-mastedPlu": "Single-masted:",
|
"Single-mastedPlu": "Single-masted:",
|
||||||
"Two-mastedPlu": "Two-masted:",
|
"Two-mastedPlu": "Two-masted:",
|
||||||
"Three-mastedPlu": "Three-masted:",
|
"Three-mastedPlu": "Three-masted:",
|
||||||
|
25
lang/pl.json
25
lang/pl.json
@ -65,6 +65,22 @@
|
|||||||
"You will be redirected soon": "Wkrótce nastąpi przekierowanie",
|
"You will be redirected soon": "Wkrótce nastąpi przekierowanie",
|
||||||
"Opponent:": "Przeciwnik"
|
"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": {
|
"Profile": {
|
||||||
"Loading": "Wczytywanie...",
|
"Loading": "Wczytywanie...",
|
||||||
"Player since:": "Gracz od:",
|
"Player since:": "Gracz od:",
|
||||||
@ -79,7 +95,12 @@
|
|||||||
},
|
},
|
||||||
"Settings": {
|
"Settings": {
|
||||||
"General": "Ogólne",
|
"General": "Ogólne",
|
||||||
"Log out": "Wyloguj się"
|
"Account": "Konto",
|
||||||
|
|
||||||
|
"Log out": "Wyloguj się",
|
||||||
|
|
||||||
|
"Current nickname:": "Aktualny nickname:",
|
||||||
|
"Change nickname": "Zmień nickname"
|
||||||
},
|
},
|
||||||
"General": {
|
"General": {
|
||||||
"Unknown error occured": "Wystąpił nieznany błąd",
|
"Unknown error occured": "Wystąpił nieznany błąd",
|
||||||
@ -124,7 +145,7 @@
|
|||||||
"Four-masted": "Czteromasztowiec",
|
"Four-masted": "Czteromasztowiec",
|
||||||
"Available:": "Dostępne:",
|
"Available:": "Dostępne:",
|
||||||
|
|
||||||
"Sunk ships": "Zatopione statki",
|
"To sunk": "Do zatopienia",
|
||||||
"Single-mastedPlu": "Jednomasztowce:",
|
"Single-mastedPlu": "Jednomasztowce:",
|
||||||
"Two-mastedPlu": "Dwumasztowce:",
|
"Two-mastedPlu": "Dwumasztowce:",
|
||||||
"Three-mastedPlu": "Trójmasztowce:",
|
"Three-mastedPlu": "Trójmasztowce:",
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "statki-backend",
|
"name": "statki-backend",
|
||||||
"version": "0.7.0",
|
"version": "0.7.4",
|
||||||
"description": "Backend do gry w statki",
|
"description": "Backend do gry w statki",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
"start_url": "/",
|
"start_url": "/",
|
||||||
"background_color": "black",
|
"background_color": "black",
|
||||||
"theme_color": "black",
|
"theme_color": "black",
|
||||||
"orientation": "landscape",
|
"orientation": "any",
|
||||||
"icons": [
|
"icons": [
|
||||||
{
|
{
|
||||||
"src": "/assets/img/statki-logo-crop.png",
|
"src": "/assets/img/statki-logo-crop.png",
|
||||||
@ -50,6 +50,27 @@
|
|||||||
"type": "image/png",
|
"type": "image/png",
|
||||||
"form_factor": "wide",
|
"form_factor": "wide",
|
||||||
"label": "Preparation phase"
|
"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"
|
"display": "standalone"
|
@ -108,13 +108,12 @@ nav span:hover {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#pvpCreateView .modes div {
|
#pvpCreateView .modes div {
|
||||||
height: 17rem;
|
|
||||||
width: 15rem;
|
width: 15rem;
|
||||||
background-color: black;
|
background-color: black;
|
||||||
border: solid 1px white;
|
border: solid 1px white;
|
||||||
border-radius: 15px;
|
border-radius: 15px;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
padding: 1rem 3rem;
|
padding: 2rem 3rem;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
@ -351,6 +350,30 @@ nav span:hover {
|
|||||||
text-decoration: underline;
|
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 {
|
#logout {
|
||||||
color: var(--danger);
|
color: var(--danger);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
@ -361,3 +384,60 @@ nav span:hover {
|
|||||||
#logout:hover {
|
#logout:hover {
|
||||||
opacity: 0.6;
|
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;
|
||||||
|
}
|
@ -17,7 +17,6 @@
|
|||||||
@media only screen and (max-width: 820px) {
|
@media only screen and (max-width: 820px) {
|
||||||
#pvpCreateView .modes div {
|
#pvpCreateView .modes div {
|
||||||
max-width: 90vw;
|
max-width: 90vw;
|
||||||
height: 19rem;
|
|
||||||
padding: 2rem 1.5rem;
|
padding: 2rem 1.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
BIN
public/assets/img/screenshot_create_mobile.png
Normal file
BIN
public/assets/img/screenshot_create_mobile.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 116 KiB |
BIN
public/assets/img/screenshot_mainmenu_mobile.png
Normal file
BIN
public/assets/img/screenshot_mainmenu_mobile.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 126 KiB |
BIN
public/assets/img/screenshot_profile_mobile.png
Normal file
BIN
public/assets/img/screenshot_profile_mobile.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 138 KiB |
@ -22,20 +22,18 @@ class Battleships {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getField(x, y) {
|
getField(x, y) {
|
||||||
if (0 <= x && x < this.boardSize && 0 <= y && y < this.boardSize) {
|
console.log(x, y);
|
||||||
x++;
|
if (0 <= x && x < this.boardSize && 0 <= y && y <= this.boardSize) {
|
||||||
y++;
|
return $(`#board .row:nth-child(${y + 1}) .field:nth-child(${x + 1})`);
|
||||||
return $(`#board .row:nth-child(${y}) .field:nth-child(${x})`);
|
|
||||||
} else {
|
} else {
|
||||||
throw new RangeError("getField position out of range.");
|
throw new RangeError("getField position out of range.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getFieldSecondary(x, y) {
|
getFieldSecondary(x, y) {
|
||||||
if (0 <= x && x < this.boardSize && 0 <= y && y < this.boardSize) {
|
console.log(x, y);
|
||||||
x++;
|
if (0 <= x && x < this.boardSize && 0 <= y && y <= this.boardSize) {
|
||||||
y++;
|
return $(`#secondaryBoard .row:nth-child(${y + 1}) .field:nth-child(${x + 1})`);
|
||||||
return $(`#secondaryBoard .row:nth-child(${y}) .field:nth-child(${x})`);
|
|
||||||
} else {
|
} else {
|
||||||
throw new RangeError("getField position out of range.");
|
throw new RangeError("getField position out of range.");
|
||||||
}
|
}
|
||||||
|
@ -4,37 +4,39 @@ String.prototype.replaceAt = function (index, replacement) {
|
|||||||
|
|
||||||
const socket = io();
|
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(() => {
|
// const initialContent = $("#scrolldowntext").html();
|
||||||
var content = $("#scrolldowntext").html();
|
|
||||||
const len = content.length;
|
|
||||||
|
|
||||||
for (let i = 0; i < len; i++) {
|
// setInterval(() => {
|
||||||
const duration = Math.random() * 20 + 40;
|
// var content = $("#scrolldowntext").html();
|
||||||
|
// const len = content.length;
|
||||||
|
|
||||||
setTimeout(() => {
|
// for (let i = 0; i < len; i++) {
|
||||||
let previousChar = content.charAt(i);
|
// const duration = Math.random() * 20 + 40;
|
||||||
|
|
||||||
let randomChar = charset[Math.floor(Math.random() * charset.length)];
|
// setTimeout(() => {
|
||||||
content = content.replaceAt(i, randomChar);
|
// let previousChar = content.charAt(i);
|
||||||
|
|
||||||
$("#scrolldowntext").html(content);
|
// let randomChar = charset[Math.floor(Math.random() * charset.length)];
|
||||||
|
// content = content.replaceAt(i, randomChar);
|
||||||
|
|
||||||
setTimeout(() => {
|
// $("#scrolldowntext").html(content);
|
||||||
content = content.replaceAt(i, previousChar);
|
|
||||||
$("#scrolldowntext").html(content);
|
|
||||||
|
|
||||||
if (i == len - 1) {
|
// setTimeout(() => {
|
||||||
content = initialContent;
|
// content = content.replaceAt(i, previousChar);
|
||||||
$("#scrolldowntext").html(initialContent);
|
// $("#scrolldowntext").html(content);
|
||||||
}
|
|
||||||
}, duration * len + duration * i);
|
// if (i == len - 1) {
|
||||||
}, duration * i);
|
// content = initialContent;
|
||||||
}
|
// $("#scrolldowntext").html(initialContent);
|
||||||
}, 5000);
|
// }
|
||||||
|
// }, duration * len + duration * i);
|
||||||
|
// }, duration * i);
|
||||||
|
// }
|
||||||
|
// }, 5000);
|
||||||
|
|
||||||
document.addEventListener("wheel", (event) => {
|
document.addEventListener("wheel", (event) => {
|
||||||
if (event.deltaY > 0) {
|
if (event.deltaY > 0) {
|
||||||
|
@ -23,3 +23,4 @@ self.addEventListener("install", installEvent => {
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -50,11 +50,9 @@ if ($(window).width() <= 820) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
$('#board .field').on('click', function () {
|
$('#board .field').on('click', function () {
|
||||||
if (new Date().getTime() / 1000 - lastTimeClick > 0.3) {
|
if (new Date().getTime() / 1000 - lastTimeClick > 0.3 && $(window).width() > 820 && !postPrep) {
|
||||||
if ($(window).width() > 820) {
|
socket.emit("place ship", selectedShip, $(this).data('pos-x'), $(this).data('pos-y'), shipRotation);
|
||||||
socket.emit("place ship", selectedShip, $(this).data('pos-x'), $(this).data('pos-y'), shipRotation);
|
lastTimeClick = new Date().getTime() / 1000;
|
||||||
lastTimeClick = new Date().getTime() / 1000;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -66,11 +64,9 @@ function manualPlace(posX, posY) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
$('#secondaryBoard .field').on('click', function () {
|
$('#secondaryBoard .field').on('click', function () {
|
||||||
if (new Date().getTime() / 1000 - lastTimeClick > 0.3) {
|
if (new Date().getTime() / 1000 - lastTimeClick > 0.3 && $(window).width() > 820 && myTurn) {
|
||||||
if ($(window).width() > 820) {
|
socket.emit("shoot", $(this).data('pos-x'), $(this).data('pos-y'));
|
||||||
socket.emit("shoot", $(this).data('pos-x'), $(this).data('pos-y'));
|
lastTimeClick = new Date().getTime() / 1000;
|
||||||
lastTimeClick = new Date().getTime() / 1000;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -178,7 +174,7 @@ socket.on("ship sunk", (victimIdx, ship) => {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
let l = !ship.type ? ship.type + 1 : ship.type + 2;
|
let l = ship.type + 1;
|
||||||
if (victimIdx === playerIdx) {
|
if (victimIdx === playerIdx) {
|
||||||
for (let i = 0; i < l; i++) {
|
for (let i = 0; i < l; i++) {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
@ -332,10 +328,10 @@ function getAccuracy() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function updateShipsSunk() {
|
function updateShipsSunk() {
|
||||||
$("#singlemasted").html(shipsSunk[0]);
|
$("#singlemasted").html(4 - shipsSunk[0]);
|
||||||
$("#twomasted").html(shipsSunk[1]);
|
$("#twomasted").html(3 - shipsSunk[1]);
|
||||||
$("#threemasted").html(shipsSunk[2]);
|
$("#threemasted").html(2 - shipsSunk[2]);
|
||||||
$("#fourmasted").html(shipsSunk[3]);
|
$("#fourmasted").html(1 - shipsSunk[3]);
|
||||||
}
|
}
|
||||||
|
|
||||||
function readyUp() {
|
function readyUp() {
|
||||||
|
@ -7,16 +7,15 @@ socket.on("joined", (nick) => {
|
|||||||
returnLock = false;
|
returnLock = false;
|
||||||
lockUI(true);
|
lockUI(true);
|
||||||
$("#oppNameField").html(nick);
|
$("#oppNameField").html(nick);
|
||||||
switchView("preparingGame");
|
|
||||||
lockUI(false);
|
lockUI(false);
|
||||||
|
switchView("preparingGame");
|
||||||
|
|
||||||
console.log("Player joined the game:", nick);
|
console.log("Player joined the game:", nick);
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on("player left", () => {
|
socket.on("player left", () => {
|
||||||
lockUI(true);
|
|
||||||
switchView("mainMenuView");
|
|
||||||
lockUI(false);
|
lockUI(false);
|
||||||
|
switchView("mainMenuView");
|
||||||
|
|
||||||
console.log("Player left the game");
|
console.log("Player left the game");
|
||||||
});
|
});
|
||||||
@ -62,11 +61,12 @@ $("#languages").on("change", function() {
|
|||||||
|
|
||||||
socket.emit("my profile", (profile) => {
|
socket.emit("my profile", (profile) => {
|
||||||
console.log("Received user data. UID:", profile.uid);
|
console.log("Received user data. UID:", profile.uid);
|
||||||
|
console.log("Profile data:", profile);
|
||||||
|
|
||||||
// General profile data
|
// General profile data
|
||||||
let options = { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' };
|
let options = { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' };
|
||||||
$("#playerSince").html(new Date(profile.profile.account_creation).toLocaleDateString(undefined, options));
|
$("#playerSince").html(new Date(profile.profile.account_creation).toLocaleDateString(undefined, options));
|
||||||
$("#nickname").html(profile.profile.nickname);
|
$(".nickname").html(profile.profile.nickname);
|
||||||
|
|
||||||
// Profile stats
|
// Profile stats
|
||||||
$("#monthlyPlayed").html(profile.stats.monthly_matches);
|
$("#monthlyPlayed").html(profile.stats.monthly_matches);
|
||||||
@ -89,7 +89,8 @@ socket.emit("my profile", (profile) => {
|
|||||||
|
|
||||||
const duration = `${minutes}:${seconds}`;
|
const duration = `${minutes}:${seconds}`;
|
||||||
|
|
||||||
matchHistoryDOM += `<div class="match" data-matchid="${match.match_id}"><div><h1 class="dynamic${match.won === 1 ? "" : " danger"}">${match.won === 1 ? window.locale["Victory"] : window.locale["Defeat"]}</h1><span> vs. ${match.match_type === "pvp" ? match.opponent : "AI"}</span></div><h2 class="statsButton">${window.locale["Click to view match statistics"]}</h2><span>${date}</span><br><span>${duration}</span></div>`;
|
console.log(match);
|
||||||
|
matchHistoryDOM += `<div class="match" data-matchid="${match.match_id}"><div><h1 class="dynamic${match.won === 1 ? "" : " danger"}">${match.won === 1 ? window.locale["Victory"] : window.locale["Defeat"]}</h1><span> vs. ${match.match_type === "pvp" ? match.opponent : "<span class=\"important\">AI ("+match.ai_type+")</span>"}</span></div><h2 class="statsButton">${window.locale["Click to view match statistics"]}</h2><span>${date}</span><br><span>${duration}</span></div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!matchHistoryDOM) {
|
if (!matchHistoryDOM) {
|
||||||
@ -140,25 +141,41 @@ $("#leaveGameButton").on("click", function () {
|
|||||||
window.location.reload();
|
window.location.reload();
|
||||||
});
|
});
|
||||||
|
|
||||||
$("#pvpMenuButton").on("click", function () {
|
|
||||||
switchView('pvpMenuView');
|
|
||||||
});
|
|
||||||
|
|
||||||
$("#logout").on("click", function() {
|
$("#logout").on("click", function() {
|
||||||
lockUI(true);
|
lockUI(true);
|
||||||
socket.emit("logout");
|
socket.emit("logout");
|
||||||
window.location.reload();
|
window.location.reload();
|
||||||
});
|
});
|
||||||
|
|
||||||
const form = document.getElementById('pvpJoinForm');
|
$("#pveDifficulty").on("change", function() {
|
||||||
const input = document.getElementById('pvpJoinCode');
|
switch (this.value) {
|
||||||
|
case 'simple':
|
||||||
|
$('#difficultyDescription').html(locale["Simple description"]);
|
||||||
|
break;
|
||||||
|
|
||||||
form.addEventListener('submit', (e) => {
|
case 'smart':
|
||||||
|
$('#difficultyDescription').html(locale["Smart description"]);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'overkill':
|
||||||
|
$('#difficultyDescription').html(locale["Overkill description"]);
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
$('#difficultyDescription').html('');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const joinForm = document.getElementById('pvpJoinForm');
|
||||||
|
const joinCodeInput = document.getElementById('pvpJoinCode');
|
||||||
|
|
||||||
|
joinForm.addEventListener('submit', (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (input.value && input.value.length === 6) {
|
if (joinCodeInput.value && joinCodeInput.value.length === 6) {
|
||||||
lockUI(true);
|
lockUI(true);
|
||||||
console.log("Joining a lobby with code:", input.value);
|
console.log("Joining a lobby with code:", joinCodeInput.value);
|
||||||
socket.emit("join lobby", input.value, (response) => {
|
socket.emit("join lobby", joinCodeInput.value, (response) => {
|
||||||
switch (response.status) {
|
switch (response.status) {
|
||||||
case "ok":
|
case "ok":
|
||||||
console.log("Joined a lobby by:", response.oppNickname);
|
console.log("Joined a lobby by:", response.oppNickname);
|
||||||
@ -174,6 +191,41 @@ form.addEventListener('submit', (e) => {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
input.value = '';
|
joinCodeInput.value = '';
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const pveForm = document.getElementById('pveCreateForm');
|
||||||
|
const pveDifficulty = document.getElementById('pveDifficulty').value;
|
||||||
|
|
||||||
|
pveForm.addEventListener('submit', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (pveDifficulty) {
|
||||||
|
lockUI(true);
|
||||||
|
console.log("Creating a PvE game with difficulty:", pveDifficulty);
|
||||||
|
socket.emit("create pve", pveDifficulty, (response) => {
|
||||||
|
switch (response.status) {
|
||||||
|
case "ok":
|
||||||
|
console.log("Joined a PvE lobby: ", response.oppNickname);
|
||||||
|
$("#oppNameField").html(`AI (${pveDifficulty})`);
|
||||||
|
lockUI(false);
|
||||||
|
switchView("preparingGame");
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
alert(`${window.locale["Unknown error occured"]}\n${window.locale["Status:"]} ${response.status}`);
|
||||||
|
lockUI(false);
|
||||||
|
switchView("mainMenuView");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
joinCodeInput.value = '';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// const isInStandaloneMode = () =>
|
||||||
|
// (window.matchMedia('(display-mode: standalone)').matches) || (window.navigator.standalone) || document.referrer.includes('android-app://');
|
||||||
|
|
||||||
|
// if (isInStandaloneMode()) {
|
||||||
|
// alert("Thanks for using the PWA!");
|
||||||
|
// }
|
@ -34,15 +34,13 @@ const initialURLParams = new URLSearchParams(window.location.search);
|
|||||||
const initialPath = initialURLParams.get('path');
|
const initialPath = initialURLParams.get('path');
|
||||||
|
|
||||||
window.addEventListener("load", () => {
|
window.addEventListener("load", () => {
|
||||||
// if (initialPath != null) {
|
if (initialPath != null) {
|
||||||
// let elem = document.querySelector(`.container[data-path="${initialPath}"]`);
|
let elem = document.querySelector(`.container[data-path="${initialPath}"]:not(.container[data-pathlock])`);
|
||||||
|
|
||||||
// if (elem != null) {
|
if (elem != null) {
|
||||||
// switchView(elem.id, true);
|
switchView(elem.id, true);
|
||||||
// }
|
}
|
||||||
// } else {
|
}
|
||||||
// switchView("mainMenuView");
|
|
||||||
//}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
addEventListener("popstate", (event) => {
|
addEventListener("popstate", (event) => {
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
if ("serviceWorker" in navigator) {
|
if ("serviceWorker" in navigator) {
|
||||||
window.addEventListener("load", () => {
|
window.addEventListener("load", () => {
|
||||||
navigator.serviceWorker
|
navigator.serviceWorker
|
||||||
.register("/serviceWorker.js")
|
.register("/assets/js/service-worker.js")
|
||||||
.then(res => console.log("Service worker registered"))
|
.then(res => console.log("Service worker registered"))
|
||||||
.catch(err => console.log("Service worker not registered", err));
|
.catch(err => console.log("Service worker not registered", err));
|
||||||
});
|
});
|
||||||
|
@ -30,24 +30,40 @@ export class MailAuth {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async timer(tId, time, callback) {
|
async timer(tId, time, callback) {
|
||||||
await this.redis.set(`loginTimer:${tId}`, new Date().getTime() / 1000);
|
let timerEnd = new Date().getTime() / 1000 + time;
|
||||||
let localLastUpdate = await this.redis.get(`loginTimer:${tId}`);
|
|
||||||
|
|
||||||
let timeout = setTimeout(callback, time * 1000);
|
await this.redis.set(`loginTimer:${tId}`, timerEnd);
|
||||||
|
|
||||||
let interval = setInterval(async () => {
|
let interval = setInterval(async () => {
|
||||||
if (timeout._destroyed) {
|
if (new Date().getTime() < timerEnd) {
|
||||||
clearInterval(interval);
|
clearInterval(interval);
|
||||||
|
|
||||||
|
callback();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let lastUpdate = await this.redis.get(`loginTimer:${tId}`);
|
let lastUpdate = await this.redis.get(`loginTimer:${tId}`);
|
||||||
if (localLastUpdate != lastUpdate) {
|
if (timerEnd != lastUpdate) {
|
||||||
clearTimeout(timeout);
|
|
||||||
clearInterval(interval);
|
clearInterval(interval);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}, 200);
|
}, 1000);
|
||||||
|
|
||||||
|
// let timeout = setTimeout(callback, time * 1000);
|
||||||
|
|
||||||
|
// let interval = setInterval(async () => {
|
||||||
|
// if (timeout._destroyed) {
|
||||||
|
// clearInterval(interval);
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// let lastUpdate = await this.redis.get(`loginTimer:${tId}`);
|
||||||
|
// if (localLastUpdate != lastUpdate) {
|
||||||
|
// clearTimeout(timeout);
|
||||||
|
// clearInterval(interval);
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
// }, 200);
|
||||||
}
|
}
|
||||||
|
|
||||||
async resetTimer(tId) {
|
async resetTimer(tId) {
|
||||||
@ -133,7 +149,7 @@ export class MailAuth {
|
|||||||
await this.redis.set(`codeAuth:${authCode}`, row.user_id);
|
await this.redis.set(`codeAuth:${authCode}`, row.user_id);
|
||||||
|
|
||||||
await this.timer(row.user_id, 600, async () => {
|
await this.timer(row.user_id, 600, async () => {
|
||||||
await this.redis.json.del(`codeAuth:${authCode}`);
|
await this.redis.unlink(`codeAuth:${authCode}`);
|
||||||
});
|
});
|
||||||
|
|
||||||
authCode = authCode.slice(0, 4) + " " + authCode.slice(4);
|
authCode = authCode.slice(0, 4) + " " + authCode.slice(4);
|
||||||
@ -173,7 +189,7 @@ export class MailAuth {
|
|||||||
await this.redis.set(`codeAuth:${authCode}`, row.user_id);
|
await this.redis.set(`codeAuth:${authCode}`, row.user_id);
|
||||||
|
|
||||||
await this.timer(row.user_id, 600, async () => {
|
await this.timer(row.user_id, 600, async () => {
|
||||||
await this.redis.json.del(`codeAuth:${authCode}`);
|
await this.redis.unlink(`codeAuth:${authCode}`);
|
||||||
});
|
});
|
||||||
|
|
||||||
authCode = authCode.slice(0, 4) + " " + authCode.slice(4);
|
authCode = authCode.slice(0, 4) + " " + authCode.slice(4);
|
||||||
@ -205,10 +221,10 @@ export class MailAuth {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
saveMatch(matchId, duration, type, hostId, guestId, boards, winnerIdx) {
|
saveMatch(matchId, duration, type, hostId, guestId, boards, winnerIdx, aitype = null) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const conn = mysql.createConnection(this.mysqlOptions);
|
const conn = mysql.createConnection(this.mysqlOptions);
|
||||||
conn.query(`INSERT INTO matches(match_id, match_type, host_id, guest_id, duration) VALUES (${conn.escape(matchId)}, ${conn.escape(type)}, ${conn.escape(hostId)}, ${conn.escape(guestId)}, ${conn.escape(duration)})`, async (error) => {
|
conn.query(`INSERT INTO matches(match_id, match_type, host_id, guest_id, duration${aitype == null ? "" : ", ai_type"}) VALUES (${conn.escape(matchId)}, ${conn.escape(type)}, ${conn.escape(hostId)}, ${conn.escape(guestId)}, ${conn.escape(duration)}${aitype == null ? "" : ", " + conn.escape(aitype)})`, async (error) => {
|
||||||
if (error) reject(error);
|
if (error) reject(error);
|
||||||
else conn.query(`INSERT INTO statistics(match_id, user_id, board, won) VALUES (${conn.escape(matchId)}, ${conn.escape(hostId)}, ${conn.escape(JSON.stringify(boards[0]))}, ${conn.escape(winnerIdx ? 1 : 0)}), (${conn.escape(matchId)}, ${conn.escape(guestId)}, ${conn.escape(JSON.stringify(boards[1]))}, ${conn.escape(winnerIdx ? 0 : 1)})`, async (error, response) => {
|
else conn.query(`INSERT INTO statistics(match_id, user_id, board, won) VALUES (${conn.escape(matchId)}, ${conn.escape(hostId)}, ${conn.escape(JSON.stringify(boards[0]))}, ${conn.escape(winnerIdx ? 1 : 0)}), (${conn.escape(matchId)}, ${conn.escape(guestId)}, ${conn.escape(JSON.stringify(boards[1]))}, ${conn.escape(winnerIdx ? 0 : 1)})`, async (error, response) => {
|
||||||
if (error) reject(error);
|
if (error) reject(error);
|
||||||
@ -223,7 +239,7 @@ export class MailAuth {
|
|||||||
getProfile(userId) {
|
getProfile(userId) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const conn = mysql.createConnection(this.mysqlOptions);
|
const conn = mysql.createConnection(this.mysqlOptions);
|
||||||
conn.query(`SELECT nickname, account_creation FROM accounts WHERE user_id = ${conn.escape(userId)}; SELECT ROUND((AVG(statistics.won)) * 100) AS winrate, COUNT(statistics.match_id) AS alltime_matches, COUNT(CASE WHEN (YEAR(matches.date) = YEAR(NOW()) AND MONTH(matches.date) = MONTH(NOW())) THEN matches.match_id END) AS monthly_matches FROM accounts NATURAL JOIN statistics NATURAL JOIN matches WHERE accounts.user_id = ${conn.escape(userId)}; SELECT statistics.match_id, accounts.nickname AS opponent, matches.match_type, statistics.won, matches.duration, matches.date FROM statistics JOIN matches ON matches.match_id = statistics.match_id JOIN accounts ON accounts.user_id = (CASE WHEN matches.host_id != statistics.user_id THEN matches.host_id ELSE matches.guest_id END) WHERE statistics.user_id = ${conn.escape(userId)} ORDER BY matches.date DESC LIMIT 10;`, async (error, response) => {
|
conn.query(`SELECT nickname, account_creation FROM accounts WHERE user_id = ${conn.escape(userId)}; SELECT ROUND((AVG(statistics.won)) * 100) AS winrate, COUNT(statistics.match_id) AS alltime_matches, COUNT(CASE WHEN (YEAR(matches.date) = YEAR(NOW()) AND MONTH(matches.date) = MONTH(NOW())) THEN matches.match_id END) AS monthly_matches FROM accounts NATURAL JOIN statistics NATURAL JOIN matches WHERE accounts.user_id = ${conn.escape(userId)}; SELECT statistics.match_id, accounts.nickname AS opponent, matches.match_type, statistics.won, matches.ai_type, matches.duration, matches.date FROM statistics JOIN matches ON matches.match_id = statistics.match_id JOIN accounts ON accounts.user_id = (CASE WHEN matches.host_id != statistics.user_id THEN matches.host_id ELSE matches.guest_id END) WHERE statistics.user_id = ${conn.escape(userId)} ORDER BY matches.date DESC LIMIT 10;`, async (error, response) => {
|
||||||
if (error) reject(error);
|
if (error) reject(error);
|
||||||
else {
|
else {
|
||||||
if (response[0].length === 0 || response[1].length === 0) {
|
if (response[0].length === 0 || response[1].length === 0) {
|
||||||
@ -246,7 +262,7 @@ export class MailAuth {
|
|||||||
const rUid = await this.redis.get(`codeAuth:${authCode}`);
|
const rUid = await this.redis.get(`codeAuth:${authCode}`);
|
||||||
if (rUid != null && rUid === uid) {
|
if (rUid != null && rUid === uid) {
|
||||||
this.resetTimer(rUid);
|
this.resetTimer(rUid);
|
||||||
await this.redis.del(`codeAuth:${authCode}`);
|
await this.redis.unlink(`codeAuth:${authCode}`);
|
||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
return false;
|
return false;
|
||||||
|
@ -59,9 +59,11 @@ export class GameInfo {
|
|||||||
return stats;
|
return stats;
|
||||||
}
|
}
|
||||||
|
|
||||||
async incrStat(socket, statKey, by = 1) {
|
async incrStat(socket, statKey, by = 1, idx) {
|
||||||
const game = await this.redis.json.get(`game:${socket.session.activeGame}`);
|
if (!idx) {
|
||||||
const idx = socket.request.session.userId === game.hostId ? 0 : 1;
|
const game = await this.redis.json.get(`game:${socket.session.activeGame}`);
|
||||||
|
idx = socket.request.session.userId === game.hostId ? 0 : 1;
|
||||||
|
}
|
||||||
|
|
||||||
this.redis.json.numIncrBy(`game:${socket.session.activeGame}`, `.boards[${idx}].stats.${statKey}`, by);
|
this.redis.json.numIncrBy(`game:${socket.session.activeGame}`, `.boards[${idx}].stats.${statKey}`, by);
|
||||||
}
|
}
|
||||||
@ -105,12 +107,14 @@ export class GameInfo {
|
|||||||
await this.redis.json.arrAppend(key, `.boards[${playerIdx}].ships`, shipData);
|
await this.redis.json.arrAppend(key, `.boards[${playerIdx}].ships`, shipData);
|
||||||
}
|
}
|
||||||
|
|
||||||
async depleteShips(socket) {
|
async depleteShips(socket, playerIdx) {
|
||||||
const gameId = socket.session.activeGame;
|
const gameId = socket.session.activeGame;
|
||||||
const key = `game:${gameId}`;
|
const key = `game:${gameId}`;
|
||||||
const hostId = (await this.redis.json.get(key, { path: '.hostId' }));
|
const hostId = (await this.redis.json.get(key, { path: '.hostId' }));
|
||||||
|
|
||||||
const playerIdx = socket.request.session.userId === hostId ? 0 : 1;
|
if (!playerIdx) {
|
||||||
|
playerIdx = socket.request.session.userId === hostId ? 0 : 1;
|
||||||
|
}
|
||||||
|
|
||||||
var playerShips = (await this.redis.json.get(key, { path: `.boards[${playerIdx}].ships` }));
|
var playerShips = (await this.redis.json.get(key, { path: `.boards[${playerIdx}].ships` }));
|
||||||
|
|
||||||
@ -237,13 +241,9 @@ export class GameInfo {
|
|||||||
return deletedShip;
|
return deletedShip;
|
||||||
}
|
}
|
||||||
|
|
||||||
async shootShip(socket, posX, posY) {
|
async shootShip(socket, enemyIdx, posX, posY) {
|
||||||
const gameId = socket.session.activeGame;
|
const gameId = socket.session.activeGame;
|
||||||
const key = `game:${gameId}`;
|
const key = `game:${gameId}`;
|
||||||
const hostId = (await this.redis.json.get(key, { path: '.hostId' }));
|
|
||||||
|
|
||||||
const enemyIdx = socket.request.session.userId === hostId ? 1 : 0;
|
|
||||||
// const playerIdx = enemyIdx ? 0 : 1;
|
|
||||||
|
|
||||||
let playerBoard = await this.redis.json.get(key, { path: `.boards[${enemyIdx}]` });
|
let playerBoard = await this.redis.json.get(key, { path: `.boards[${enemyIdx}]` });
|
||||||
|
|
||||||
@ -286,7 +286,46 @@ export class GameInfo {
|
|||||||
return { status: 1, ship: shotShip };
|
return { status: 1, ship: shotShip };
|
||||||
}
|
}
|
||||||
|
|
||||||
async setReady(socket) {
|
async makeAIMove(socket, difficulty) {
|
||||||
|
difficulty = 0;
|
||||||
|
|
||||||
|
const gameId = socket.session.activeGame;
|
||||||
|
const key = `game:${gameId}`;
|
||||||
|
|
||||||
|
const boards = await this.redis.json.get(key, { path: `.boards` });
|
||||||
|
|
||||||
|
if (difficulty == 1) { // If difficulty mode is set to smart, check if there are any shot but not sunk ships
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
if (difficulty != 2) { // If difficulty mode is not set to Overkill
|
||||||
|
var foundAppropriateTarget = false;
|
||||||
|
|
||||||
|
var [posX, posY] = [Math.floor(Math.random() * 10), Math.floor(Math.random() * 10)]; // Randomise first set of coordinates
|
||||||
|
|
||||||
|
while (!foundAppropriateTarget) { // As long as no appropriate target was found
|
||||||
|
[posX, posY] = [Math.floor(Math.random() * 10), Math.floor(Math.random() * 10)]; // Randomise another set of coordinates
|
||||||
|
|
||||||
|
// let check = checkHit(boards[0].ships, posX, posY);
|
||||||
|
let shot = boards[0].shots.find((shot) => shot.posX === posX && shot.posY === posY);
|
||||||
|
// If shot == null then the field with coordinates posX and posY was not shot at yet
|
||||||
|
|
||||||
|
if (!shot) {
|
||||||
|
if (difficulty == 1) { // If difficulty mode is set to smart, check if the shot wasn't near any sunk ship
|
||||||
|
foundAppropriateTarget = true;
|
||||||
|
} else { // If difficulty mode is set to simple, just accept that field
|
||||||
|
foundAppropriateTarget = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return [posX, posY];
|
||||||
|
} else {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async setReady(socket) { // This makes the socket go ready in a match
|
||||||
const gameId = socket.session.activeGame;
|
const gameId = socket.session.activeGame;
|
||||||
const key = `game:${gameId}`;
|
const key = `game:${gameId}`;
|
||||||
const hostId = (await this.redis.json.get(key, { path: '.hostId' }));
|
const hostId = (await this.redis.json.get(key, { path: '.hostId' }));
|
||||||
@ -297,11 +336,11 @@ export class GameInfo {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isPlayerInRoom(socket) {
|
export function isPlayerInRoom(socket) { // Returns true if the socket is in any socket.io room, otherwise false
|
||||||
return !socket.rooms.size === 1;
|
return !socket.rooms.size === 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getShipsAvailable(ships) {
|
export function getShipsAvailable(ships) { // Returns the amount ships left for each type from list of already placed ships (can be obtained from player's board object)
|
||||||
let shipsLeft = [4, 3, 2, 1];
|
let shipsLeft = [4, 3, 2, 1];
|
||||||
|
|
||||||
ships.forEach(ship => {
|
ships.forEach(ship => {
|
||||||
@ -311,9 +350,10 @@ export function getShipsAvailable(ships) {
|
|||||||
return shipsLeft;
|
return shipsLeft;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function checkHit(ships, posX, posY) {
|
export function checkHit(ships, posX, posY) { // Checks if a shot at posX and posY is hit (ships is the opponent's ship list)
|
||||||
let boardRender = [];
|
let boardRender = []; // Create a two-dimensional array that will contain a render of the entire enemy board
|
||||||
|
|
||||||
|
// Fill the array with false values
|
||||||
for (let i = 0; i < 10; i++) {
|
for (let i = 0; i < 10; i++) {
|
||||||
var array = [];
|
var array = [];
|
||||||
for (let i = 0; i < 10; i++) {
|
for (let i = 0; i < 10; i++) {
|
||||||
@ -321,11 +361,12 @@ export function checkHit(ships, posX, posY) {
|
|||||||
}
|
}
|
||||||
boardRender.push(array);
|
boardRender.push(array);
|
||||||
}
|
}
|
||||||
|
// The array is now 10x10 filled with false values
|
||||||
|
|
||||||
ships.forEach(ship => {
|
ships.forEach(ship => {
|
||||||
let multips;
|
let multips;
|
||||||
|
|
||||||
switch (ship.rot) {
|
switch (ship.rot) { // Set up proper multipliers for each possible rotation
|
||||||
case 0:
|
case 0:
|
||||||
multips = [1, 0];
|
multips = [1, 0];
|
||||||
break;
|
break;
|
||||||
@ -343,24 +384,37 @@ export function checkHit(ships, posX, posY) {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If multips[0] == 1 then each ship's field will go further by one field from left to right
|
||||||
|
// If multips[0] == -1 then each ship's field will go further by one field from right to left
|
||||||
|
// If multips[1] == 1 then each ship's field will go further by one field from top to bottom
|
||||||
|
// If multips[1] == -1 then each ship's field will go further by one field from bottom to top
|
||||||
|
|
||||||
|
// Iterate through all ship's fields
|
||||||
for (let i = 0; i <= ship.type; i++) {
|
for (let i = 0; i <= ship.type; i++) {
|
||||||
|
// Calculate the X and Y coordinates of the current field, clamp them in case they overflow the array
|
||||||
let x = clamp(ship.posX + multips[0] * i, 0, 9);
|
let x = clamp(ship.posX + multips[0] * i, 0, 9);
|
||||||
let y = clamp(ship.posY + multips[1] * i, 0, 9);
|
let y = clamp(ship.posY + multips[1] * i, 0, 9);
|
||||||
|
|
||||||
boardRender[x][y] = {fieldIdx: i, originPosX: ship.posX, originPosY: ship.posY};
|
boardRender[x][y] = {fieldIdx: i, originPosX: ship.posX, originPosY: ship.posY}; // Set the field in the board render
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// The function returns false if no ship has been hit and an array including following keys if it does:
|
||||||
|
// fieldIdx, originPosX, originPosY
|
||||||
|
// where fieldIdx is the amount of fields from the originating point of the ship (in the direction appropriate to set rotation)
|
||||||
|
// and originPosX and originPosY are the coordinates of the originating point of the ship
|
||||||
|
// the originating point of the ship is essentialy the field on which a user clicked to place a ship
|
||||||
return boardRender[posX][posY];
|
return boardRender[posX][posY];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function validateShipPosition(ships, type, posX, posY, rot) {
|
export function validateShipPosition(ships, type, posX, posY, rot) { // This function checks whether a certain position of a new ship is valid. It returns true if it is and false otherwise
|
||||||
if (type < 0 || type > 3 || rot < 0 || rot > 3) {
|
if (type == null || posX == null || posY == null || rot == null || type < 0 || type > 3 || rot < 0 || rot > 3 || posX < 0 || posY < 0 || posX > 9 || posY > 9) {
|
||||||
return false;
|
return false; // Return false when any of the values is incorrect
|
||||||
}
|
}
|
||||||
|
|
||||||
let boardRender = [];
|
let boardRender = []; // Create a two-dimensional array that will contain a render of the entire enemy board
|
||||||
|
|
||||||
|
// Fill the array with false values
|
||||||
for (let i = 0; i < 10; i++) {
|
for (let i = 0; i < 10; i++) {
|
||||||
var array = [];
|
var array = [];
|
||||||
for (let i = 0; i < 10; i++) {
|
for (let i = 0; i < 10; i++) {
|
||||||
@ -368,11 +422,13 @@ export function validateShipPosition(ships, type, posX, posY, rot) {
|
|||||||
}
|
}
|
||||||
boardRender.push(array);
|
boardRender.push(array);
|
||||||
}
|
}
|
||||||
|
// The array is now 10x10 filled with false values
|
||||||
|
|
||||||
|
// Iterate through all ships that are already placed, for rendering them on the boardRender array
|
||||||
ships.forEach(ship => {
|
ships.forEach(ship => {
|
||||||
let multips;
|
let multips;
|
||||||
|
|
||||||
switch (ship.rot) {
|
switch (ship.rot) { // Set up multipliers for each possible rotation, this basically defines the direction of the fields, depending on the rotation
|
||||||
case 0:
|
case 0:
|
||||||
multips = [1, 0];
|
multips = [1, 0];
|
||||||
break;
|
break;
|
||||||
@ -390,11 +446,19 @@ export function validateShipPosition(ships, type, posX, posY, rot) {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If multips[0] == 1 then each ship's field will go further by one field from left to right
|
||||||
|
// If multips[0] == -1 then each ship's field will go further by one field from right to left
|
||||||
|
// If multips[1] == 1 then each ship's field will go further by one field from top to bottom
|
||||||
|
// If multips[1] == -1 then each ship's field will go further by one field from bottom to top
|
||||||
|
|
||||||
|
// Iterate through all ship's fields
|
||||||
for (let i = 0; i <= ship.type; i++) {
|
for (let i = 0; i <= ship.type; i++) {
|
||||||
|
// Set the boardRender value under the field's coordinates to true
|
||||||
boardRender[ship.posX + multips[0] * i][ship.posY + multips[1] * i] = true;
|
boardRender[ship.posX + multips[0] * i][ship.posY + multips[1] * i] = true;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Set up multipliers again, this time for the ship to place
|
||||||
let multips;
|
let multips;
|
||||||
|
|
||||||
switch (rot) {
|
switch (rot) {
|
||||||
@ -415,21 +479,30 @@ export function validateShipPosition(ships, type, posX, posY, rot) {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Iterate through each ship's field
|
||||||
for (let x = 0; x <= type; x++) {
|
for (let x = 0; x <= type; x++) {
|
||||||
if (posX + multips[0] * x > 9 || posX + multips[0] * x < 0 || posY + multips[1] * x > 9 || posY + multips[1] * x < 0) {
|
if (posX + multips[0] * x > 9 || posX + multips[0] * x < 0 || posY + multips[1] * x > 9 || posY + multips[1] * x < 0) {
|
||||||
return false;
|
return false; // Return false if the ship's field exceeds the boards bounderies
|
||||||
}
|
}
|
||||||
|
|
||||||
let subtrahents = [[0, 0], [0, 1], [1, 0], [0, -1], [-1, 0], [1, 1], [-1, -1], [1, -1], [-1, 1]]; // Usuń cztery ostatnie elementy jeżeli chcesz by statki mogły się stykać rogami
|
// Set up subtrahents that we will use to calculate fields around the ship and check if they do not contain another ship to prevent ships from being placed next to each other
|
||||||
|
let subtrahents = [[0, 0], [0, 1], [1, 0], [0, -1], [-1, 0], [1, 1], [-1, -1], [1, -1], [-1, 1]];
|
||||||
|
|
||||||
|
// Iterate through each subtrahents set
|
||||||
for (let y = 0; y < subtrahents.length; y++) {
|
for (let y = 0; y < subtrahents.length; y++) {
|
||||||
|
|
||||||
|
// Calculate the field's indexes
|
||||||
const idxX = posX - subtrahents[y][0] + multips[0] * x;
|
const idxX = posX - subtrahents[y][0] + multips[0] * x;
|
||||||
const idxY = posY - subtrahents[y][1] + multips[1] * x;
|
const idxY = posY - subtrahents[y][1] + multips[1] * x;
|
||||||
|
|
||||||
|
// If the field's index does not exceed the boards boundaries, check whether it's set as true in the boardRender
|
||||||
if (!(idxX < 0 || idxX > 9 || idxY < 0 || idxY > 9) && boardRender[idxX][idxY]) {
|
if (!(idxX < 0 || idxX > 9 || idxY < 0 || idxY > 9) && boardRender[idxX][idxY]) {
|
||||||
return false;
|
return false; // Return false if the ship is next to another
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If all the checks pass, return true
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -442,7 +515,7 @@ export function checkTurn(data, playerId) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function findEmptyFields(grid, len) {
|
function findEmptyFields(grid, len) { // Find all empty fields in the board
|
||||||
const shipPlacements = [];
|
const shipPlacements = [];
|
||||||
|
|
||||||
// Helper function to check if a row can be placed horizontally at a given position
|
// Helper function to check if a row can be placed horizontally at a given position
|
||||||
|
@ -18,12 +18,12 @@
|
|||||||
<h3>{{ t 'board.Available:' }} <span class="dynamic danger" id="shipsLeft">-</span></h3>
|
<h3>{{ t 'board.Available:' }} <span class="dynamic danger" id="shipsLeft">-</span></h3>
|
||||||
</div>
|
</div>
|
||||||
<div class="lateBoardInfo">
|
<div class="lateBoardInfo">
|
||||||
<h3>{{ t 'board.Sunk ships' }}</h3>
|
<h3>{{ t 'board.To sunk' }}</h3>
|
||||||
<p>
|
<p>
|
||||||
{{ t 'board.Single-mastedPlu' }} <span class="dynamic shipnote" id="singlemasted">0</span><br>
|
{{ t 'board.Single-mastedPlu' }} <span class="dynamic shipnote" id="singlemasted">4</span><br>
|
||||||
{{ t 'board.Two-mastedPlu' }} <span class="dynamic shipnote" id="twomasted">0</span><br>
|
{{ t 'board.Two-mastedPlu' }} <span class="dynamic shipnote" id="twomasted">3</span><br>
|
||||||
{{ t 'board.Three-mastedPlu' }} <span class="dynamic shipnote" id="threemasted">0</span><br>
|
{{ t 'board.Three-mastedPlu' }} <span class="dynamic shipnote" id="threemasted">2</span><br>
|
||||||
{{ t 'board.Four-mastedPlu' }} <span class="dynamic shipnote" id="fourmasted">0</span><br>
|
{{ t 'board.Four-mastedPlu' }} <span class="dynamic shipnote" id="fourmasted">1</span><br>
|
||||||
</p>
|
</p>
|
||||||
<h3>{{ t 'board.Your accuracy' }}</h3>
|
<h3>{{ t 'board.Your accuracy' }}</h3>
|
||||||
<h2 id="accuracy">-</h2>
|
<h2 id="accuracy">-</h2>
|
||||||
|
@ -4,18 +4,40 @@
|
|||||||
|
|
||||||
<h2>{{ t 'menu.index.Select game mode' }}</h2>
|
<h2>{{ t 'menu.index.Select game mode' }}</h2>
|
||||||
<div class="modes">
|
<div class="modes">
|
||||||
<div id="pvpMenuButton">
|
<div onclick="switchView('pvpMenuView')">
|
||||||
<h2>{{ t 'menu.index.PvP' }}</h2>
|
<h2>{{ t 'menu.index.PvP' }}</h2>
|
||||||
<p>{{ t 'menu.index.Play against another player' }}</p>
|
<p>{{ t 'menu.index.Play against another player' }}</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div onclick="switchView('vsAiView')">
|
||||||
<h2 id="ai">{{ t 'menu.index.Vs AI' }}</h2>
|
<h2>{{ t 'menu.index.Vs AI' }}</h2>
|
||||||
<p>{{ t 'menu.index.Play against the computer' }}</p>
|
<p>{{ t 'menu.index.Play against the computer' }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="container" id="vsAiView" data-path="/pve">
|
||||||
|
<div>
|
||||||
|
<h2>{{ t 'menu.PvE.Create' }}</h2>
|
||||||
|
<h3>{{ t 'menu.PvE.Choose the difficulty mode' }}</h3>
|
||||||
|
<div class="modes">
|
||||||
|
<div>
|
||||||
|
<form action="#" id="pveCreateForm">
|
||||||
|
<select name="difficulty" id="pveDifficulty">
|
||||||
|
<option value="simple">Simple</option>
|
||||||
|
<option value="smart">Smart</option>
|
||||||
|
<option value="overkill">Overkill</option>
|
||||||
|
</select>
|
||||||
|
<p id="difficultyDescription">
|
||||||
|
{{ t 'menu.PvE.difficulty.Simple.description' }}
|
||||||
|
</p>
|
||||||
|
<input type="submit" value="{{ t 'menu.PvE.Begin' }}">
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="container" id="pvpMenuView" data-path="/pvp">
|
<div class="container" id="pvpMenuView" data-path="/pvp">
|
||||||
<div>
|
<div>
|
||||||
<h2>{{ t 'menu.PvP.PvP' }}</h2>
|
<h2>{{ t 'menu.PvP.PvP' }}</h2>
|
||||||
@ -32,12 +54,12 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="container" id="pvpCreateView" data-path="/pvp/create">
|
<div class="container" id="pvpCreateView" data-path="/pvp/create" data-pathlock>
|
||||||
<div>
|
<div>
|
||||||
<h2>{{ t 'menu.PvP/Create.PvP / Create' }}</h2>
|
<h2>{{ t 'menu.PvP/Create.PvP / Create' }}</h2>
|
||||||
<div class="modes">
|
<div class="modes">
|
||||||
<div>
|
<div>
|
||||||
<h2>{{ t 'menu.PvP/Create.Room code' }}</h2>
|
<h2>{{ t 'menu.PvP/Create.Room code:' }}</h2>
|
||||||
<input type="text" maxlength="6" readonly value="-" id="createGameCode">
|
<input type="text" maxlength="6" readonly value="-" id="createGameCode">
|
||||||
<h3>{{ t 'menu.PvP/Create.Waiting for an opponent' }}</h3>
|
<h3>{{ t 'menu.PvP/Create.Waiting for an opponent' }}</h3>
|
||||||
<button id="leaveGameButton">{{ t 'menu.PvP/Create.Leave the room' }}</button>
|
<button id="leaveGameButton">{{ t 'menu.PvP/Create.Leave the room' }}</button>
|
||||||
@ -60,7 +82,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="container" id="preparingGame" data-path="/pvp/prepairing">
|
<div class="container" id="preparingGame" data-path="/pvp/prepairing" data-pathlock>
|
||||||
<div>
|
<div>
|
||||||
<h2>{{ t 'menu.PvP/Loading.PvP / Loading' }}</h2>
|
<h2>{{ t 'menu.PvP/Loading.PvP / Loading' }}</h2>
|
||||||
<div class="modes">
|
<div class="modes">
|
||||||
@ -76,7 +98,7 @@
|
|||||||
|
|
||||||
<div class="container" id="profileView" data-path="/profile">
|
<div class="container" id="profileView" data-path="/profile">
|
||||||
<div class="profile">
|
<div class="profile">
|
||||||
<h1 id="nickname">{{ t 'menu.Profile.Loading' }}</h1>
|
<h1 class="nickname">{{ t 'menu.Profile.Loading' }}</h1>
|
||||||
<div>
|
<div>
|
||||||
<span>{{ t 'menu.Profile.Player since:' }} </span>
|
<span>{{ t 'menu.Profile.Player since:' }} </span>
|
||||||
<span id="playerSince">-</span>
|
<span id="playerSince">-</span>
|
||||||
@ -100,8 +122,12 @@
|
|||||||
<select name="language" id="languages">
|
<select name="language" id="languages">
|
||||||
|
|
||||||
</select>
|
</select>
|
||||||
<h3><a href="/privacy" target="_blank">{{ t 'landing.Privacy policy' }}</a></h3>
|
|
||||||
|
<h2>{{ t 'menu.Settings.Account' }}</h2>
|
||||||
|
<h3>{{ t 'menu.Settings.Current nickname:' }} <span class="nickname dynamic">{{ t 'menu.Profile.Loading' }}</span></h3>
|
||||||
|
<button onclick="window.location.href = '/nickname'">{{ t 'menu.Settings.Change nickname' }}</button>
|
||||||
<h3 id="logout">{{ t 'menu.Settings.Log out' }}</h3>
|
<h3 id="logout">{{ t 'menu.Settings.Log out' }}</h3>
|
||||||
|
<p class="versionInfo">statki ver. {{ ver }}<br>© 2024 MCJK <a href="/privacy" target="_blank">{{ t 'landing.Privacy policy' }}</a></p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -121,6 +147,9 @@
|
|||||||
"Disconnected": "{{ t 'errors.Disconnected' }}",
|
"Disconnected": "{{ t 'errors.Disconnected' }}",
|
||||||
"Try to refresh the page if this error reoccurs": "{{ t 'errors.Try to refresh the page if this error reoccurs' }}",
|
"Try to refresh the page if this error reoccurs": "{{ t 'errors.Try to refresh the page if this error reoccurs' }}",
|
||||||
"Connection error": "{{ t 'errors.Connection error' }}",
|
"Connection error": "{{ t 'errors.Connection error' }}",
|
||||||
|
"Simple description": "{{ t 'menu.PvE.difficulty.Simple.description' }}",
|
||||||
|
"Smart description": "{{ t 'menu.PvE.difficulty.Smart.description' }}",
|
||||||
|
"Overkill description": "{{ t 'menu.PvE.difficulty.Overkill.description' }}"
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
<script src="/assets/js/spa_lib.js"></script>
|
<script src="/assets/js/spa_lib.js"></script>
|
||||||
|
@ -20,7 +20,7 @@
|
|||||||
<script src="https://unpkg.com/tippy.js@6"></script>
|
<script src="https://unpkg.com/tippy.js@6"></script>
|
||||||
<link rel="stylesheet" href="https://unpkg.com/tippy.js@6/animations/shift-toward-subtle.css" />
|
<link rel="stylesheet" href="https://unpkg.com/tippy.js@6/animations/shift-toward-subtle.css" />
|
||||||
<link rel="stylesheet" href="https://unpkg.com/tippy.js@6/themes/translucent.css" />
|
<link rel="stylesheet" href="https://unpkg.com/tippy.js@6/themes/translucent.css" />
|
||||||
<link rel="manifest" href="/pwa/manifest.json" />
|
<link rel="manifest" href="/app/manifest.json" />
|
||||||
<meta name="theme-color" content="#000000"/>
|
<meta name="theme-color" content="#000000"/>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
Loading…
Reference in New Issue
Block a user