A lot of changes including design, backend setup etc.

This commit is contained in:
MaciejkaG 2024-01-05 23:35:11 +01:00
parent b04d67a8cf
commit c6275be106
15 changed files with 2365 additions and 0 deletions

60
index.js Normal file
View File

@ -0,0 +1,60 @@
import express from 'express';
import { createServer } from 'node:http';
import { Server } from 'socket.io';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { v4 as uuidv4 } from 'uuid';
import session from "express-session";
import { engine } from 'express-handlebars';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const app = express();
app.engine('handlebars', engine());
app.set('view engine', 'handlebars');
app.set('views', './views');
const server = createServer(app);
const io = new Server(server);
const sessionMiddleware = session({
secret: uuidv4(),
resave: true,
saveUninitialized: true,
});
app.use(sessionMiddleware);
app.use(express.static(path.join(__dirname, 'public')));
io.engine.use(sessionMiddleware);
app.get("/", (req, res) => {
res.render('index');
});
app.get("/pvp", (req, res) => {
res.render('pvp/index');
});
app.get("/pvp/join", (req, res) => {
res.render('pvp/join');
});
app.get("/pvp/create", (req, res) => {
res.render('pvp/create');
});
io.on('connection', (socket) => {
const session = socket.request.session;
socket.on('chat message', (msg) => {
console.log('message: ' + msg);
});
});
server.listen(7777, () => {
console.log('Server running at http://localhost:7777');
});

1459
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

28
package.json Normal file
View File

@ -0,0 +1,28 @@
{
"name": "statki-backend",
"version": "1.0.0",
"description": "Backend do gry w statki",
"main": "index.js",
"type": "module",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"repository": {
"type": "git",
"url": "git+https://github.com/MaciejkaG/statki-backend.git"
},
"author": "Maciejka",
"license": "ISC",
"bugs": {
"url": "https://github.com/MaciejkaG/statki-backend/issues"
},
"homepage": "https://github.com/MaciejkaG/statki-backend#readme",
"dependencies": {
"express": "^4.18.2",
"express-handlebars": "^7.1.2",
"express-session": "^1.17.3",
"socket.io": "^4.7.2",
"uuid": "^9.0.1",
"uuid4": "^2.0.3"
}
}

165
public/assets/css/board.css Normal file
View File

@ -0,0 +1,165 @@
:root {
font-size: 20px;
--field: rgb(201, 201, 201);
--mark-line: rgb(136, 136, 136);
--mark-spot: rgb(68, 68, 68);
--mark-ship-valid: hsl(120, 100%, 80%);
--mark-ship-invalid: hsl(0, 100%, 80%);
--ship-valid: hsl(120, 70%, 55%);
--ship-invalid: hsl(0, 70%, 55%);
--dynamic: rgb(83, 83, 245);
--danger: rgb(243, 56, 56);
--important: rgb(203, 50, 241);
color: rgb(136, 136, 136)
}
body {
background: black;
color: white;
font-family: 'Lato', sans-serif;
transition: opacity 0.3s ease;
animation: fadeIn 1 0.3s ease;
}
@keyframes fadeIn {
from {opacity: 0;}
to {opacity: 1;}
}
h1,h2,h3,h4,h5,h6 {
font-family: 'Poppins', sans-serif;
}
.designerTag {
position: fixed;
bottom: 5px;
left: 5px;
color: rgb(0, 255, 0);
font-size: 9px;
pointer-events: none;
user-select: none;
z-index: 999;
font-family: 'Roboto Mono', monospace;
}
.board {
display: flex;
grid-template-columns: auto auto auto;
flex-direction: column;
}
.row {
margin-bottom: 0.4rem;
display: flex;
gap: 0.4rem;
}
.field {
background-color: var(--field);
width: 2rem;
aspect-ratio: 1;
border-radius: 20%;
cursor: pointer;
transition: background 0.15s;
}
.field svg {
opacity: 0;
width: 100%;
height: 100%;
transition: opacity 0.3s;
}
.field.hit svg {
opacity: 1;
}
.dynamic {
color: var(--dynamic);
}
.danger {
color: var(--danger);
}
.important {
color: var(--important);
}
.container {
display: flex;
justify-content: center;
text-align: center;
}
.shapes h2, .shapes h3 {
margin: 0;
}
.shapes, .spaceFiller {
width: 15rem;
user-select: none;
pointer-events: none;
}
.break {
display: block;
margin-top: 3rem;
}
.panelContainer {
display: flex;
gap: 1rem;
}
.boardContainer {
width: 25rem;
display: flex;
justify-content: center;
}
#secondaryBoard, #board {
position: absolute;
transition: transform 0.3s;
background: black;
}
.ownBoardInfo {
transition: opacity 0.3s;
}
.secondary {
transform: translateX(75%) scale(0.3);
pointer-events: none;
z-index: 1;
}
#board:not(.secondary), #secondaryBoard:not(.secondary) {
z-index: 2;
}
.controlsOwnBoard {
transition: opacity 0.3s;
}
@keyframes changingOut {
from {transform: translateX(0);opacity:1;}
to {transform: translateX(30%);opacity:0;}
}
@keyframes changingIn {
from {transform: translateX(-30%);opacity:0;}
to {transform: translateX(0);opacity:1;}
}
#selectedShip.changing {
opacity: 0;
animation: changingOut 1 0.2s ease;
}
#selectedShip {
animation: changingIn 1 0.2s ease;
}

View File

@ -0,0 +1,49 @@
class Battleships {
constructor(boardSize) {
if (boardSize>0) {
this.boardSize = boardSize;
} else {
throw new Error("Wrong boardSize for the 'Battleships' class");
}
}
generateDOMBoard(size) {
let board = "";
for (var i = 0; i < size; i++) {
let row = "<div class=\"row\">";
for (var n = 0; n < size; n++) {
row += `<div class="field" data-pos-x="${i}" data-pos-y="${n}"><svg xmlns='http://www.w3.org/2000/svg' version='1.1' preserveAspectRatio='none' viewBox='0 0 100 100'><path d='M100 0 L0 100 ' stroke='black' stroke-width='3'/><path d='M0 0 L100 100 ' stroke='black' stroke-width='3'/></svg></div>`;
}
row += "</div>";
board += row;
}
return board;
}
getField(x, y) {
if (0 < x && x <= this.boardSize && 0 < y && y <= this.boardSize) {
return $(`#board .row:nth-child(${y}) .field:nth-child(${x})`);
} else {
throw new RangeError("getField position out of range.");
}
}
getRow(row) {
row = parseInt(row)+1
if (row<=this.boardSize) {
return $(`#board .row:nth-child(${row}) .field`);
} else {
throw new RangeError("getColumn position out of range.");
}
}
getColumn(column) {
column = parseInt(column)+1
if (column<=this.boardSize) {
return $(`#board .row .field:nth-child(${column})`);
} else {
throw new RangeError("getColumn position out of range.");
}
}
}

View File

@ -0,0 +1,13 @@
document.addEventListener("keydown", (e) => {
switch (e.code) {
case "KeyB":
switchBoards();
break;
case "KeyS":
switchShips();
break;
case "KeyR":
switchRotation();
break;
}
});

225
public/assets/js/main.js Normal file
View File

@ -0,0 +1,225 @@
const mapSize = 10;
const bsc = new Battleships(mapSize);
let board = bsc.generateDOMBoard(mapSize);
$("#board").html(board);
$("#secondaryBoard").html(board);
var previousRow = $(":not(*)");
var previousColumn = $(":not(*)");
var selectedShip = 0;
var shipRotation = 0;
var changedFields = [];
var hoveredField = null;
$("#board .field").hover(function () {
hoveredField = this;
// Pokaż "miarki"
let posY = parseInt($(this).data("pos-x"));
let posX = parseInt($(this).data("pos-y"));
let row = bsc.getRow(posY);
let column = bsc.getColumn(posX);
changedFields.push(row, column, $(this));
row.css("background", "rgb(136, 136, 136)");
column.css("background", "rgb(136, 136, 136)");
$(this).css("background", "rgb(68, 68, 68)");
previousRow = row;
previousColumn = column;
// Pokaż podgląd statku
posX++;
posY++;
var fields = [];
switch (shipRotation) {
case 0:
for (let i = 0; i < 4; i++) {
fields.push([posX + i, posY]);
}
break;
case 1:
for (let i = 0; i < 4; i++) {
fields.push([posX, posY + i]);
}
break;
case 2:
for (let i = 0; i < 4; i++) {
fields.push([posX - i, posY]);
}
break;
case 3:
for (let i = 0; i < 4; i++) {
fields.push([posX, posY - i]);
}
break;
}
var fieldElem;
let failed = false;
for (let i = 0; i < selectedShip+1; i++) {
const field = fields[i];
try {
fieldElem = bsc.getField(field[0], field[1]);
} catch {
if (!failed) {
failed = true;
i = -1;
}
}
if (failed) {
fieldElem.css("background", "rgb(255, 163, 163)");
} else {
fieldElem.css("background", "rgb(163, 255, 163)");
}
changedFields.push(fieldElem);
}
}, function () {
hoveredField = null;
// Wyłącz "miarki" po wyjściu kursora z pola (aby się nie duplikowały w przyszłości)
changedFields.forEach(field => {
field.css("background", "rgb(201, 201, 201)");
});
changedFields.length = 0;
});
$("#board .field").on("click", function() {
});
var ownBoardIsActive = true;
function switchBoards() {
if (ownBoardIsActive) { // Aktywna jest plansza użytkownika
$("#secondaryBoard").removeClass("secondary");
$("#board").addClass("secondary");
$(".ownBoardInfo").css("opacity", 0);
$(".controlsOwnBoard").css("opacity", 0.3);
} else { // Aktywna jest plansza przeciwnika
$("#board").removeClass("secondary");
$("#secondaryBoard").addClass("secondary");
$(".ownBoardInfo").css("opacity", 1);
$(".controlsOwnBoard").css("opacity", 1);
}
ownBoardIsActive = !ownBoardIsActive;
}
function switchShips() {
if (selectedShip===3) {
selectedShip = 0;
} else {
selectedShip++;
}
refreshBoardView();
$("#selectedShip").addClass("changing");
setTimeout(() => {
switch (selectedShip) {
case 0:
$("#selectedShip").html("Jednomasztowiec");
break;
case 1:
$("#selectedShip").html("Dwumasztowiec");
break;
case 2:
$("#selectedShip").html("Trójmasztowiec");
break;
case 3:
$("#selectedShip").html("Czteromasztowiec");
break;
}
$("#selectedShip").removeClass("changing");
}, 200);
}
function switchRotation() {
if (shipRotation === 3) {
shipRotation = 0;
} else {
shipRotation++;
}
refreshBoardView();
}
function refreshBoardView() {
if (hoveredField) {
changedFields.forEach(field => {
field.css("background", "rgb(201, 201, 201)");
});
changedFields.length = 0;
let posY = parseInt($(hoveredField).data("pos-x"));
let posX = parseInt($(hoveredField).data("pos-y"));
let row = bsc.getRow(posY);
let column = bsc.getColumn(posX);
changedFields.push(row, column, $(hoveredField));
row.css("background", "rgb(136, 136, 136)");
column.css("background", "rgb(136, 136, 136)");
$(hoveredField).css("background", "rgb(68, 68, 68)");
previousRow = row;
previousColumn = column;
posX++;
posY++;
var fields = [];
switch (shipRotation) {
case 0:
for (let i = 0; i < 4; i++) {
fields.push([posX + i, posY]);
}
break;
case 1:
for (let i = 0; i < 4; i++) {
fields.push([posX, posY + i]);
}
break;
case 2:
for (let i = 0; i < 4; i++) {
fields.push([posX - i, posY]);
}
break;
case 3:
for (let i = 0; i < 4; i++) {
fields.push([posX, posY - i]);
}
break;
}
var fieldElem;
let failed = false;
for (let i = 0; i < selectedShip + 1; i++) {
const field = fields[i];
try {
fieldElem = bsc.getField(field[0], field[1]);
} catch {
if (!failed) {
failed = true;
i = -1;
}
}
if (failed) {
fieldElem.css("background", "rgb(255, 163, 163)");
} else {
fieldElem.css("background", "rgb(163, 255, 163)");
}
changedFields.push(fieldElem);
}
}
}

View File

47
views/game.handlebars Normal file
View File

@ -0,0 +1,47 @@
<!DOCTYPE html>
<html lang="pl">
<head>
<meta charset="UTF-8">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Statki</title>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.7.1/jquery.min.js"></script>
<link href="https://fonts.googleapis.com/css2?family=Lato:ital,wght@0,400;0,700;1,400;1,700&family=Poppins:ital,wght@0,600;1,600&family=Roboto+Mono:ital@0;1&display=swap" rel="stylesheet"> <link rel="stylesheet" href="assets/css/board.css">
</head>
<body>
<div class="container">
<div>
<h1>Statki</h1>
<div class="panelContainer">
<div class="shapes">
<div class="ownBoardInfo">
<h3>Wybrany statek</h3>
<h2 class="dynamic" id="selectedShip">Jednomasztowiec</h2>
<h3>Dostępne: <span class="dynamic danger">1</span></h3>
</div>
<span class="break"></span>
<h2>Sterowanie</h2>
<h3 class="controlsOwnBoard"><span class="important">S</span> Zmiana statku</h3>
<h3 class="controlsOwnBoard"><span class="important">R</span> Obrót statku</h3>
<h3><span class="important">B</span> Zamiana planszy</h3>
<span class="break"></span>
<h3>Ruch: <span class="dynamic">Przeciwnik</span></h3>
<h2 class="important">∞</h2>
</div>
<div class="boardContainer">
<div id="board" oncontextmenu="return false;"></div>
<div id="secondaryBoard" class="secondary" oncontextmenu="return false;"></div>
</div>
<div class="spaceFiller"></div>
</div>
<span class="designerTag">Designed by Maciejka</span>
</div>
</div>
<script src="/assets/js/battleships_lib.js"></script>
<script src="/assets/js/main.js"></script>
<script src="/assets/js/key_handling.js"></script>
</body>
</html>

16
views/index-old.html Normal file
View File

@ -0,0 +1,16 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<script src="https://cdn.socket.io/4.7.2/socket.io.min.js"></script>
</head>
<body>
<h1>Test</h1>
<script>
const socket = io();
</script>
</body>
</html>

63
views/index.handlebars Normal file
View File

@ -0,0 +1,63 @@
<!DOCTYPE html>
<html lang="pl">
<head>
<meta charset="UTF-8">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Statki</title>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.7.1/jquery.min.js"></script>
<link href="https://fonts.googleapis.com/css2?family=Lato:ital,wght@0,400;0,700;1,400;1,700&family=Poppins:ital,wght@0,600;1,600&family=Roboto+Mono:ital@0;1&display=swap" rel="stylesheet">
<link rel="stylesheet" href="/assets/css/board.css">
<style>
.modes {
display: flex;
gap: 2rem;
}
.modes div {
height: 10rem;
width: 10rem;
background: black;
border: solid 1px white;
border-radius: 15px;
user-select: none;
cursor: pointer;
transition: all 0.3s;
}
.modes div:hover {
background: white;
color: black;
}
</style>
</head>
<body>
<div class="container">
<div>
<h1>Statki</h1>
<h2>Wybierz tryb gry</h2>
<div class="modes">
<div id="pvp">
<h2>PvP</h2>
<p>Graj przeciwko innemu graczowi</p>
</div>
<div>
<h2 id="ai">Vs. AI</h2>
<p>Graj przeciwko komputerowi</p>
</div>
</div>
<span class="designerTag">Designed by Maciejka</span>
<script>
$("#pvp").on("click", function() {
$("body").css("opacity", "0");
setTimeout(() => {
window.location.href = "pvp";
}, 300);
});
</script>
</div>
</div>
</body>
</html>

View File

@ -0,0 +1 @@
{{{body}}}

View File

@ -0,0 +1,83 @@
<!DOCTYPE html>
<html lang="pl">
<head>
<meta charset="UTF-8">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Statki</title>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.7.1/jquery.min.js"></script>
<link href="https://fonts.googleapis.com/css2?family=Lato:ital,wght@0,400;0,700;1,400;1,700&family=Poppins:ital,wght@0,600;1,600&family=Roboto+Mono:ital@0;1&display=swap" rel="stylesheet">
<link rel="stylesheet" href="/assets/css/board.css">
<style>
.modes {
display: flex;
gap: 2rem;
}
.modes div {
height: 17rem;
width: 15rem;
background: black;
border: solid 1px white;
border-radius: 15px;
user-select: none;
padding: 1rem 3rem;
display: flex;
flex-direction: column;
gap: 0.5rem;
}
input, button {
background: black;
color: white;
border-radius: 15px;
border: 1px solid white;
font-size: 1.5rem;
text-align: center;
padding: 0.5rem 2rem;
outline: none;
}
button {
cursor: pointer;
font-size: 1rem;
transition: all 0.3s;
}
button:hover {
background: white;
color: black;
}
</style>
</head>
<body>
<div class="container">
<div>
<h1>Statki</h1>
<h2>PvP / Stwórz</h2>
<div class="modes">
<div>
<h2>Kod pokoju:</h2>
<input type="text" maxlength="6" readonly value="123456">
<h3>Oczekiwanie na gracza...</h3>
<button>Opuść pokój</button>
</div>
</div>
<span class="designerTag">Designed by Maciejka</span>
<script>
const form = document.getElementById('form');
const input = document.getElementById('input');
form.addEventListener('submit', (e) => {
e.preventDefault();
if (input.value && input.value.length===6) {
socket.emit('join game', input.value);
input.value = '';
}
});
</script>
</div>
</div>
</body>
</html>

View File

@ -0,0 +1,70 @@
<!DOCTYPE html>
<html lang="pl">
<head>
<meta charset="UTF-8">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Statki</title>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.7.1/jquery.min.js"></script>
<link href="https://fonts.googleapis.com/css2?family=Lato:ital,wght@0,400;0,700;1,400;1,700&family=Poppins:ital,wght@0,600;1,600&family=Roboto+Mono:ital@0;1&display=swap" rel="stylesheet">
<link rel="stylesheet" href="/assets/css/board.css">
<style>
.modes {
display: flex;
gap: 2rem;
}
.modes div {
height: 10rem;
width: 10rem;
background: black;
border: solid 1px white;
border-radius: 15px;
user-select: none;
cursor: pointer;
transition: all 0.3s;
}
.modes div:hover {
background: white;
color: black;
}
</style>
</head>
<body>
<div class="container">
<div>
<h1>Statki</h1>
<h2>PvP</h2>
<div class="modes">
<div id="create">
<h2>Stwórz</h2>
<p>Stwórz własny pokój</p>
</div>
<div id="join">
<h2>Dołącz</h2>
<p>Dołącz do czyjegoś pokoju poprzez kod</p>
</div>
</div>
<span class="designerTag">Designed by Maciejka</span>
<script>
$("#create").on("click", function() {
$("body").css("opacity", "0");
setTimeout(() => {
window.location.href = "/pvp/create";
}, 300);
});
$("#join").on("click", function() {
$("body").css("opacity", "0");
setTimeout(() => {
window.location.href = "/pvp/join";
}, 300);
});
</script>
</div>
</div>
</body>
</html>

86
views/pvp/join.handlebars Normal file
View File

@ -0,0 +1,86 @@
<!DOCTYPE html>
<html lang="pl">
<head>
<meta charset="UTF-8">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Statki</title>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.7.1/jquery.min.js"></script>
<link href="https://fonts.googleapis.com/css2?family=Lato:ital,wght@0,400;0,700;1,400;1,700&family=Poppins:ital,wght@0,600;1,600&family=Roboto+Mono:ital@0;1&display=swap" rel="stylesheet">
<link rel="stylesheet" href="/assets/css/board.css">
<style>
.modes {
display: flex;
gap: 2rem;
}
.modes div {
height: 7rem;
width: 15rem;
background: black;
border: solid 1px white;
border-radius: 15px;
user-select: none;
padding: 2rem 3rem;
}
form {
display: flex;
flex-direction: column;
gap: 2rem;
}
form input {
background: black;
color: white;
border-radius: 15px;
border: 1px solid white;
font-size: 1.5rem;
text-align: center;
padding: 0.5rem 2rem;
outline: none;
}
form input[type=submit] {
cursor: pointer;
font-size: 1rem;
transition: all 0.3s;
}
form input[type=submit]:hover {
background: white;
color: black;
}
</style>
</head>
<body>
<div class="container">
<div>
<h1>Statki</h1>
<h2>PvP / Dołącz</h2>
<div class="modes">
<div>
<form action="/api/joinme" id="form">
<input type="text" maxlength="6" id="input" placeholder="Kod pokoju">
<input type="submit" value="Dołącz">
</form>
</div>
</div>
<span class="designerTag">Designed by Maciejka</span>
<script>
const form = document.getElementById('form');
const input = document.getElementById('input');
form.addEventListener('submit', (e) => {
e.preventDefault();
if (input.value && input.value.length===6) {
socket.emit('join game', input.value);
input.value = '';
}
});
</script>
</div>
</div>
</body>
</html>