commit 66758c5d991f40e24856a13266ca715d8521c8e8 Author: Benjamin Kraft Date: Fri Mar 24 13:59:48 2023 +0100 Init diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c6ef218 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +.idea + diff --git a/project.json b/project.json new file mode 100644 index 0000000..e69de29 diff --git a/public/data/images/airplane.jpeg b/public/data/images/airplane.jpeg new file mode 100644 index 0000000..0a2bfc8 Binary files /dev/null and b/public/data/images/airplane.jpeg differ diff --git a/public/data/images/airplane_high.jpeg b/public/data/images/airplane_high.jpeg new file mode 100644 index 0000000..e465020 Binary files /dev/null and b/public/data/images/airplane_high.jpeg differ diff --git a/public/data/images/ant.jpeg b/public/data/images/ant.jpeg new file mode 100644 index 0000000..ee1e416 Binary files /dev/null and b/public/data/images/ant.jpeg differ diff --git a/public/data/images/ant_nest.jpeg b/public/data/images/ant_nest.jpeg new file mode 100644 index 0000000..24d150d Binary files /dev/null and b/public/data/images/ant_nest.jpeg differ diff --git a/public/data/images/background.jpg b/public/data/images/background.jpg new file mode 100644 index 0000000..13c474c Binary files /dev/null and b/public/data/images/background.jpg differ diff --git a/public/data/images/ball.jpg b/public/data/images/ball.jpg new file mode 100644 index 0000000..ee29948 Binary files /dev/null and b/public/data/images/ball.jpg differ diff --git a/public/data/images/ball_2.jpg b/public/data/images/ball_2.jpg new file mode 100644 index 0000000..64c6e5f Binary files /dev/null and b/public/data/images/ball_2.jpg differ diff --git a/public/data/images/big_ben.jpeg b/public/data/images/big_ben.jpeg new file mode 100644 index 0000000..1e456ad Binary files /dev/null and b/public/data/images/big_ben.jpeg differ diff --git a/public/data/images/bumblebee.jpeg b/public/data/images/bumblebee.jpeg new file mode 100644 index 0000000..18f0219 Binary files /dev/null and b/public/data/images/bumblebee.jpeg differ diff --git a/public/data/images/butterfly.jpeg b/public/data/images/butterfly.jpeg new file mode 100644 index 0000000..dfe6b3c Binary files /dev/null and b/public/data/images/butterfly.jpeg differ diff --git a/public/data/images/cars.jpeg b/public/data/images/cars.jpeg new file mode 100644 index 0000000..b8b9b0d Binary files /dev/null and b/public/data/images/cars.jpeg differ diff --git a/public/data/images/cat_2.jpeg b/public/data/images/cat_2.jpeg new file mode 100644 index 0000000..6e5a05e Binary files /dev/null and b/public/data/images/cat_2.jpeg differ diff --git a/public/data/images/cereal.jpeg b/public/data/images/cereal.jpeg new file mode 100644 index 0000000..0d004de Binary files /dev/null and b/public/data/images/cereal.jpeg differ diff --git a/public/data/images/cereal_making.jpeg b/public/data/images/cereal_making.jpeg new file mode 100644 index 0000000..62c830d Binary files /dev/null and b/public/data/images/cereal_making.jpeg differ diff --git a/public/data/images/cliff.jpeg b/public/data/images/cliff.jpeg new file mode 100644 index 0000000..29ea46b Binary files /dev/null and b/public/data/images/cliff.jpeg differ diff --git a/public/data/images/cliff_forest.jpeg b/public/data/images/cliff_forest.jpeg new file mode 100644 index 0000000..5664451 Binary files /dev/null and b/public/data/images/cliff_forest.jpeg differ diff --git a/public/data/images/cloudy_sky.jpeg b/public/data/images/cloudy_sky.jpeg new file mode 100644 index 0000000..eb7abfe Binary files /dev/null and b/public/data/images/cloudy_sky.jpeg differ diff --git a/public/data/images/cloudy_sky_high.jpeg b/public/data/images/cloudy_sky_high.jpeg new file mode 100644 index 0000000..1905d48 Binary files /dev/null and b/public/data/images/cloudy_sky_high.jpeg differ diff --git a/public/data/images/cloudy_sky_sun.jpeg b/public/data/images/cloudy_sky_sun.jpeg new file mode 100644 index 0000000..505fb51 Binary files /dev/null and b/public/data/images/cloudy_sky_sun.jpeg differ diff --git a/public/data/images/cloudy_sky_sun_2.jpeg b/public/data/images/cloudy_sky_sun_2.jpeg new file mode 100644 index 0000000..e6027d1 Binary files /dev/null and b/public/data/images/cloudy_sky_sun_2.jpeg differ diff --git a/public/data/images/clover.jpeg b/public/data/images/clover.jpeg new file mode 100644 index 0000000..d3ef0d7 Binary files /dev/null and b/public/data/images/clover.jpeg differ diff --git a/public/data/images/drink.jpeg b/public/data/images/drink.jpeg new file mode 100644 index 0000000..7884576 Binary files /dev/null and b/public/data/images/drink.jpeg differ diff --git a/public/data/images/evening_sky.jpeg b/public/data/images/evening_sky.jpeg new file mode 100644 index 0000000..0b7340d Binary files /dev/null and b/public/data/images/evening_sky.jpeg differ diff --git a/public/data/images/eye.jpeg b/public/data/images/eye.jpeg new file mode 100644 index 0000000..b5db2dd Binary files /dev/null and b/public/data/images/eye.jpeg differ diff --git a/public/data/images/favicon.ico b/public/data/images/favicon.ico new file mode 100644 index 0000000..3172667 Binary files /dev/null and b/public/data/images/favicon.ico differ diff --git a/public/data/images/feast.jpeg b/public/data/images/feast.jpeg new file mode 100644 index 0000000..325c47f Binary files /dev/null and b/public/data/images/feast.jpeg differ diff --git a/public/data/images/fire.jpeg b/public/data/images/fire.jpeg new file mode 100644 index 0000000..7bcdfaf Binary files /dev/null and b/public/data/images/fire.jpeg differ diff --git a/public/data/images/flower.jpeg b/public/data/images/flower.jpeg new file mode 100644 index 0000000..247e6a9 Binary files /dev/null and b/public/data/images/flower.jpeg differ diff --git a/public/data/images/forest.jpeg b/public/data/images/forest.jpeg new file mode 100644 index 0000000..03f5e1a Binary files /dev/null and b/public/data/images/forest.jpeg differ diff --git a/public/data/images/hidden.png b/public/data/images/hidden.png new file mode 100644 index 0000000..696b84f Binary files /dev/null and b/public/data/images/hidden.png differ diff --git a/public/data/images/insect.jpeg b/public/data/images/insect.jpeg new file mode 100644 index 0000000..c6fe817 Binary files /dev/null and b/public/data/images/insect.jpeg differ diff --git a/public/data/images/moss.jpeg b/public/data/images/moss.jpeg new file mode 100644 index 0000000..5557e68 Binary files /dev/null and b/public/data/images/moss.jpeg differ diff --git a/public/data/images/ocean.jpeg b/public/data/images/ocean.jpeg new file mode 100644 index 0000000..950b38c Binary files /dev/null and b/public/data/images/ocean.jpeg differ diff --git a/public/data/images/pancake.jpeg b/public/data/images/pancake.jpeg new file mode 100644 index 0000000..7991f03 Binary files /dev/null and b/public/data/images/pancake.jpeg differ diff --git a/public/data/images/pine_trees.jpeg b/public/data/images/pine_trees.jpeg new file mode 100644 index 0000000..9934346 Binary files /dev/null and b/public/data/images/pine_trees.jpeg differ diff --git a/public/data/images/rabbit.jpeg b/public/data/images/rabbit.jpeg new file mode 100644 index 0000000..e1cadb6 Binary files /dev/null and b/public/data/images/rabbit.jpeg differ diff --git a/public/data/images/river.jpeg b/public/data/images/river.jpeg new file mode 100644 index 0000000..8e5f4ef Binary files /dev/null and b/public/data/images/river.jpeg differ diff --git a/public/data/images/sledge.jpeg b/public/data/images/sledge.jpeg new file mode 100644 index 0000000..ebf292b Binary files /dev/null and b/public/data/images/sledge.jpeg differ diff --git a/public/data/images/snow_forest.jpeg b/public/data/images/snow_forest.jpeg new file mode 100644 index 0000000..3b9415f Binary files /dev/null and b/public/data/images/snow_forest.jpeg differ diff --git a/public/data/images/snowman.jpeg b/public/data/images/snowman.jpeg new file mode 100644 index 0000000..97f13fd Binary files /dev/null and b/public/data/images/snowman.jpeg differ diff --git a/public/data/images/snowy_landscape.jpeg b/public/data/images/snowy_landscape.jpeg new file mode 100644 index 0000000..b887a45 Binary files /dev/null and b/public/data/images/snowy_landscape.jpeg differ diff --git a/public/data/images/stage.jpeg b/public/data/images/stage.jpeg new file mode 100644 index 0000000..f24e78f Binary files /dev/null and b/public/data/images/stage.jpeg differ diff --git a/public/data/images/strawberries.jpg b/public/data/images/strawberries.jpg new file mode 100644 index 0000000..b6952b4 Binary files /dev/null and b/public/data/images/strawberries.jpg differ diff --git a/public/data/images/street_forest.jpg b/public/data/images/street_forest.jpg new file mode 100644 index 0000000..729c4c5 Binary files /dev/null and b/public/data/images/street_forest.jpg differ diff --git a/public/data/images/street_ocean.jpeg b/public/data/images/street_ocean.jpeg new file mode 100644 index 0000000..94537b2 Binary files /dev/null and b/public/data/images/street_ocean.jpeg differ diff --git a/public/data/images/sunrise.jpeg b/public/data/images/sunrise.jpeg new file mode 100644 index 0000000..b41ae04 Binary files /dev/null and b/public/data/images/sunrise.jpeg differ diff --git a/public/data/images/tower_bridge.jpeg b/public/data/images/tower_bridge.jpeg new file mode 100644 index 0000000..b5de199 Binary files /dev/null and b/public/data/images/tower_bridge.jpeg differ diff --git a/public/data/images/tree.jpeg b/public/data/images/tree.jpeg new file mode 100644 index 0000000..a41293e Binary files /dev/null and b/public/data/images/tree.jpeg differ diff --git a/public/data/images/tree_blue_sky.jpeg b/public/data/images/tree_blue_sky.jpeg new file mode 100644 index 0000000..74e4881 Binary files /dev/null and b/public/data/images/tree_blue_sky.jpeg differ diff --git a/public/data/images/tree_broken.jpeg b/public/data/images/tree_broken.jpeg new file mode 100644 index 0000000..3038c96 Binary files /dev/null and b/public/data/images/tree_broken.jpeg differ diff --git a/public/data/images/tree_yellow_sky.jpeg b/public/data/images/tree_yellow_sky.jpeg new file mode 100644 index 0000000..86cfa1c Binary files /dev/null and b/public/data/images/tree_yellow_sky.jpeg differ diff --git a/public/data/images/volcano.jpeg b/public/data/images/volcano.jpeg new file mode 100644 index 0000000..386e0e7 Binary files /dev/null and b/public/data/images/volcano.jpeg differ diff --git a/public/data/images/wasteland.jpeg b/public/data/images/wasteland.jpeg new file mode 100644 index 0000000..0de4771 Binary files /dev/null and b/public/data/images/wasteland.jpeg differ diff --git a/public/data/scripts/Card.js b/public/data/scripts/Card.js new file mode 100644 index 0000000..c22d9c9 --- /dev/null +++ b/public/data/scripts/Card.js @@ -0,0 +1,125 @@ +class Card{ + + constructor(id, src){ + this.id = id; + this.src = src; + this.revealed = true; + this.createDomElement(); + } + + static SortDomElements(){ + function byIndex(a, b){ + return $(a).data("index") - $(b).data("index"); + } + for (let c of cards){ + $(c.dom).data("index", cards.indexOf(c)); + } + let list = $("#table img").get(); + list.sort(byIndex); + $("#table").append(list); + } + + static getById(id){ + for (let c of cards){ + if (c.id === id) return c; + } + } + + static hideTime(){ + return 300; + } + + reveal(){ + if (this.revealed) return; + let card = this; + this.dom.delay(Card.hideTime() / 2).queue(function(next){ + $(this).attr("src", card.src); + next(); + }); + this.dom.css("transform", "rotateY(0deg)"); + this.revealed = true; + } + + hide(){ + if (!this.revealed) return; + this.dom.delay(Card.hideTime() / 2).queue(function(next){ + $(this).attr("src", hidden); + next(); + }); + this.dom.css("transform", "rotateY(180deg)"); + this.revealed = false; + } + + savePosition(){ + let x = this.dom.position().left; + let y = this.dom.position().top; + this.dom.css({ + "left": x, + "top": y + }); + this.pos = {left: x, top: y}; + } + + moveToPosition(type, staple){ + let card = this; + this.dom.css("position", "absolute"); + this.moving = true; + this.dom.animate({ + left: this.pos.left, + top: this.pos.top + }, 750, function(){ + card.moving = false; + if (type === "collect") { + card.appendToStaple(staple); + } + for (let c of cards) if (c.moving) return; + if (type === "collect"){ + + } else { + for (let c of cards) c.dom.css("position", "static"); + Card.SortDomElements(); + game.shuffled = true; + } + }); + } + + appendToStaple(staple){ + this.dom.remove(); + this.dom.css({ + "position": "", + "top": "", + "left": "" + }); + this.dom.attr("class", "card card_on_staple"); + staple.append(this.dom); + } + + createDomElement(){ + this.dom = $(""); + this.dom.data("card", this); + this.dom.attr({ + "class": "card_on_table card", + "id": this.id, + "src": this.revealed ? this.src : hidden + }); + this.dom.click(() => cardClicked(this)); + let card = this; + this.dom.hover(() => card.debugShow(), () => card.debugHide()); + $("#table").append(this.dom); + } + + equals(other){ + return this.src === other.src; + } + + debugShow(){ + if (this.revealed || !debug) return; + this.dom.attr("src", this.src); + } + + debugHide(){ + if (this.revealed || !debug) return; + this.dom.attr("src", hidden); + } + +} \ No newline at end of file diff --git a/public/data/scripts/Game.js b/public/data/scripts/Game.js new file mode 100644 index 0000000..39153a9 --- /dev/null +++ b/public/data/scripts/Game.js @@ -0,0 +1,190 @@ +class Game{ + + constructor(playercount){ + this.players = []; + this.online = false; + for (let i = 0; i < playercount; i++){ + this.players.push(new Player(i, null, "Player")); + } + playerDomSetup(this.players.length, this.players); + this.cards = []; + this.cardSize = $($(".card_on_table")[0]).width(); + $("#interface, #showInterface_btn").show(); + } + + static startShuffle(){ + while (!Game.allPaired()){ + let c1 = random(cards); + let c2 = random(cards); + if (!c1.equals(c2) && !c2.hasSwitchPartner){ + Game.switchPartner(c1, c2); + } + } + for (let c of cards){ + c.moveToPosition(); + c.hasSwitchPartner = false; + } + } + + static switchPartner(c1, c2){ + c1.hasSwitchPartner = true; + c2.hasSwitchPartner = true; + let temp = c1.pos; + c1.pos = c2.pos; + c2.pos = temp; + let index = {c1: cards.indexOf(c1), + c2: cards.indexOf(c2)}; + temp = cards[index.c1]; + cards[index.c1] = cards[index.c2]; + cards[index.c2] = temp; + } + + static allPaired(){ + for (let c of cards){ + if (!c.hasSwitchPartner){ + return false; + } + } + return true; + } + + start(){ + if (this.running || !this.shuffled) return; + this.running = true; + random(game.players).hasTurn(true); + } + + currentPlayer(){ + for (let p of this.players){ + if (p.hasTurn()) return p; + } + } + + revealCard(calledByServer, cardId){ + if (this.nextTurn){ + if (!this.pairFound()) for (let c of this.cards) c.hide(); + this.cards = []; + this.nextTurn = false; + } + let card = Card.getById(cardId); + card.reveal(); + this.cards.push(card); + if (this.cards.length == 2) this.next(); + } + + pairFound(){ + if (this.cards.length < 2) return false; + return this.cards[0].equals(this.cards[1]); + } + + next(){ + this.nextTurn = true; + if (this.pairFound()) this.thisPlayer(); + else this.nextPlayer(); + } + + nextPlayer(){ + let p = this.currentPlayer(); + p.hasTurn(false); + let i = this.players.indexOf(p) + 1; + if (i == this.players.length) i = 0; + this.players[i].hasTurn(true); + } + + thisPlayer(){ + this.collectPair(); + } + + getLeader(){ + return this.players[0]; + } + + collectPair(){ + let p = this.currentPlayer(); + let stapleOffset = 2; + let staple = p.dom.find(".staple"); + p.cards.push(this.cards[0], this.cards[1]); + p.addPairs(1); + let last = staple.find("img").last(); + let count = 1; + for (let c of this.cards) { + let x = (p.pairs - 1) ? last.offset().left + stapleOffset * count : staple.offset().left; + let y = (p.pairs - 1) ? last.offset().top : staple.offset().top; + count++; + c.savePosition(); + c.pos = {left: x, top: y}; + c.moveToPosition("collect", p.dom.find(".staple")); + c.dom.css("z-index", p.cards.length - 2 + this.cards.indexOf(c)); + c.dom.after(createEmpty()); + cards.splice(cards.indexOf(c), 1); + } + + } + + shuffle(){ + for (let c of cards){ + c.savePosition(); + c.hide(); + } + setTimeout(Game.startShuffle, Card.hideTime()); + } + +} + +class OnlineGame extends Game{ + + constructor(players, seed){ + super(0); + this.online = true; + randomSeed(seed); + for (let p of players){ + this.players.push(new Player(players.indexOf(p), p.id, p.name)); + } + playerDomSetup(this.players.length, this.players); + if (socket.id === this.players[0].id){ + $("#interface, #showInterface_btn").show(); + } else { + $("#interface, #showInterface_btn").hide(); + } + } + + start(calledByServer){ + if (calledByServer){ + super.start(); + } else + socket.emit("game-action", "start"); + } + + revealCard(calledByServer, cardId){ + if (calledByServer){ + super.revealCard(false, cardId); + } else { + if (socket.id !== this.currentPlayer().id) return; + socket.emit("game-action", "revealCard", cardId); + } + } + + shuffle(calledByServer){ + if (calledByServer){ + super.shuffle(); + } else + socket.emit("game-action", "shuffle"); + } + + setPlayers(players){ + /*let newPlayers = []; + for (let p of this.players){ + if (p.hasTurn() && !p.isIn(players)){ + this.nextPlayer(); + } + if (p.isIn(players)) newPlayers.push(p); + else if (p.hasTurn()) + } + playerDomSetup(this.players.length, this.players); + if (socket.id === this.players[0].id){ + $("#interface, #showInterface_btn").show(); + }*/ + //TODO + } + +} \ No newline at end of file diff --git a/public/data/scripts/Online.js b/public/data/scripts/Online.js new file mode 100644 index 0000000..da89f86 --- /dev/null +++ b/public/data/scripts/Online.js @@ -0,0 +1,149 @@ +let formState = "type", lobby; + +function joinLobby(dom){ + loader(dom); + let name = playerNameEntered(); + if (name === '') return; + let codeTyped = $('#join-lobby > input').val(); + socket.emit('join-lobby', codeTyped, name); +} + +function leaveLobby(dom){ + loader(dom); + socket.emit('leave-lobby', lobby.id); + + if (game){ + $('#initialize-wrapper, ' + + '.setup:eq(1), .setup:eq(2), #join-lobby').show(); + game = null; + } + + lobby = null; + $('#interface').hide(); + $('#initialize').show(); + $('#initialize *').attr('disabled', false); + $('#lobby').hide(); +} + +function createLobby(dom){ + loader(dom); + let name = playerNameEntered(); + if (name === '') return; + socket.emit('create-lobby', settings); + showLobby(); +} + +function addSocketEvents(){ + socket.on('member-joined', (lobby) => setLobby(lobby)); + socket.on('member-left', (lobby) => setLobby(lobby)); + socket.on('join-failed', (message) => joinFailed(message)); + socket.on('start-game', (lobby, seed) => startOnlineGame(lobby, seed)); + socket.on('game-action', (lobby, action, argument) => startAction(action, argument)); +} + +function startAction(action, argument){ + game[action].call(game, true, argument); +} + +function startLobby(){ + socket.emit('start-game', lobby.id); +} + +function startOnlineGame(lobby, seed){ + setLobby(lobby); + $("#initialize-wrapper").hide(); + console.log(seed); + game = new OnlineGame(lobby.clients, seed); +} + +function continueForm(){ + + if (formState === "type"){ + let type = $('input[name="game-type"]:checked').val(); + $("#game-type input").attr("disabled", "disabled"); + if (type === "online"){ + $("#game-type").hide(); + $("#player-name").show(); + formState = "player-name"; + } + if (type === "offline"){ + $("#player-count").show(); + formState = "player-count"; + } + } + + else if (formState === "player-count"){ + let playerCount = int($("#player-count select").val()); + $("#initialize-wrapper").hide(); + game = new Game(playerCount); + } else if (formState === "player-name"){ + if (playerNameEntered() != ""){ + $(".setup:eq(0)").hide(); + $(".setup:eq(1), .setup:eq(2), #join-lobby").show(); + $("#player-name").hide(); + let queries = "?game=memory&name=" + playerNameEntered(); + $.get('data/settings/get_port.php', port => { + socket = io('https://' + location.hostname + ':' + port + queries); + addSocketEvents(); + }); + } + } + +} + +function joinFailed(reason){ + $("#join-lobby .error-label").html(reason); + $("#join-lobby .error-label").show(); +} + +function showLobby(){ + $("#initialize").hide(); + $("#lobby").show(); +} + +function setLobby(newLobby){ + lobby = newLobby; + let leader = lobby.clients[0]; + $("#lobby-members").html(""); + for (let p of lobby.clients){ + $("#lobby-members").append(createLobbyMember(p, p.id === leader.id)); + } + if (lobby.clientCounts.indexOf(lobby.clients.length) === -1){ + let strCounts = ''; + for (let c of lobby.clientCounts){ + let comma = lobby.clientCounts.indexOf(c) === lobby.clientCounts.length - 1 ? '' : ', '; + strCounts += c + comma; + } + $('#lobby > .error-label').html('Only as ' + strCounts); + $('#lobby > .error-label').show(); + $("#start-lobby").attr("disabled", "disabled"); + } else { + $('#lobby > .error-label').hide(); + $("#start-lobby").attr("disabled", false); + } + if (!(socket.id === leader.id)){ + $("#start-lobby").attr({"disabled": "disabled"}); + } else { + $("#start-lobby").attr("disabled", false); + } + $("#lobby-id").html("Id: " + lobby.id); + showLobby(); + if (game) game.setPlayers(lobby.clients); +} + +function createLobbyMember(player, isLeader){ + let member = $("
"); + member.attr("class", "lobby-member"); + member.html(player.name); + if (isLeader) member.attr("id", "lobby-leader"); + return member; +} + +function playerNameEntered(){ + let name = $("#player-name > input").val(); + if (name === ""){ + $("#player-name .error-label").html("Please enter a name!"); + $("#player-name .error-label").show(); + } + return name; +} \ No newline at end of file diff --git a/public/data/scripts/Player.js b/public/data/scripts/Player.js new file mode 100644 index 0000000..971009e --- /dev/null +++ b/public/data/scripts/Player.js @@ -0,0 +1,49 @@ +class Player{ + + constructor(i, id, name){ + this.turn = false; + this.pairs = 0; + this.cards = []; + this.id = id; + this.index = i; + this.name = name; + this.createDom(); + this.updateDom(); + } + + static getById(id){ + for (let p of game.players){ + if (p.id === id) return p; + } + } + + addPairs(pairs){ + this.pairs += pairs; + this.updateDom(); + } + + updateDom(){ + this.dom.find(".pairs-label").html("Pairs: " + this.pairs); + } + + createDom(){ + if (this.index % 2 == 0) this.dom = $("#left > div:eq(" + this.index / 2 + ")"); + else this.dom = $("#right > div:eq(" + (this.index - 1) / 2 + ")"); + this.dom.find("span").html(this.name); + } + + isIn(players){ + for (let p of players){ + if (p.id === this.id) return true; + } + return false; + } + + hasTurn(turn){ + if (turn == null) return this.turn; + this.turn = turn; + if (turn) this.dom.find(".has-turn").css("background-color", "green"); + else this.dom.find(".has-turn").css("background-color", "rgba(0, 0, 0, 0)"); + } + +} \ No newline at end of file diff --git a/public/data/scripts/events.js b/public/data/scripts/events.js new file mode 100644 index 0000000..e741a6b --- /dev/null +++ b/public/data/scripts/events.js @@ -0,0 +1,35 @@ +"use strict" + +function keyPressed(){ + +} + +function keyReleased(){ + +} + +function mouseMoved(){ + +} + +function mouseDragged(){ + +} + +function mousePressed(){ + +} + +function mouseReleased(){ + +} + +window.onresize = function(){ + let i = 0; + function resize(){ + resizeCanvas($("#canvasHolder").outerWidth(), $("#canvasHolder").outerHeight(), true); + i++; + if (i < 5) window.setTimeout(resize, 0); + } + resize(); +} \ No newline at end of file diff --git a/public/data/scripts/lib/benjo_library.js b/public/data/scripts/lib/benjo_library.js new file mode 100644 index 0000000..7f2ad08 --- /dev/null +++ b/public/data/scripts/lib/benjo_library.js @@ -0,0 +1,443 @@ +"use strict" + +class Collision{ + + static ellipseToEllipse(e1, e2){ + + //Colliding angle of ball 1 to ball 2 using arc tan of both x and y differences + let collisionAngle = atan2((e2.pos.y - e1.pos.y), (e2.pos.x - e1.pos.x)); + + //Converting directions of velocity vector of balls into angles + let d1 = atan2(e1.vel.y, e1.vel.x); + let d2 = atan2(e2.vel.y, e2.vel.x); + + //Ignoring mass effects new velocites are simply magnitude multiplied with value of angle differences + let newXspeed1 = e1.vel.mag() * cos(d1 - collisionAngle); + let newYspeed1 = e1.vel.mag() * sin(d1 - collisionAngle); + let newXspeed2 = e2.vel.mag() * cos(d2 - collisionAngle); + let newYspeed2 = e2.vel.mag() * sin(d2 - collisionAngle); + + //According to the principle of linear momentum, kinetic energy stays the same after collision, so velocities are now related to masses + let finalXspeed1 = ((e1.radius - e2.radius) * newXspeed1 + e2.radius * 2 * newXspeed2) / (e1.radius + e2.radius); + let finalYspeed1 = newYspeed1; + let finalXspeed2 = (e1.radius * 2 * newXspeed1 + (e2.radius - e1.radius) * newXspeed2) / (e1.radius + e2.radius); + let finalYspeed2 = newYspeed2; + + //Values of collisionAngle + let cosAngle = cos(collisionAngle); + let sinAngle = sin(collisionAngle); + + //To also keep velocites relative to pure collisionAngle, subtract sin*x from cos*x and add sin*y to cos*y because coordSystem has y = 0 on the top + let u1x = cosAngle * finalXspeed1 - sinAngle * finalYspeed1; + let u1y = sinAngle * finalXspeed1 + cosAngle * finalYspeed1; + let u2x = cosAngle * finalXspeed2 - sinAngle * finalYspeed2; + let u2y = sinAngle * finalXspeed2 + cosAngle * finalYspeed2; + + //Set new velocities to both balls + e1.vel.x = u1x; + e1.vel.y = u1y; + e2.vel.x = u2x; + e2.vel.y = u2y; + + //Move balls one vx/vy forward to avoid double inverting collision detection + e1.pos.x += e1.vel.x; + e1.pos.y += e1.vel.y; + e2.pos.x += e2.vel.x; + e2.pos.y += e2.vel.y; + } + +} + + +function toTimeString(time, hoursWanted){ + + time = floor(time / 10); + + let hs = String(floor(time % 100)); + let fs = String(floor((time / 100) % 60)); + + if (hoursWanted){ + let min = String(floor(((time / 100) / 60) % 60)); + let hr = String(floor(((time / 100) / 60) / 60)); + + if (hs.length < 2) hs = "0" + hs; + if (fs.length < 2) fs = "0" + fs; + if (min.length < 2) min = "0" + min; + if (hr.length < 2) hr = "0" + hr; + + let timeString = hr + ":" + min + ":" + fs + ":" + hs; + } else { + let min = String(floor(((time / 100) / 60) % 60)); + + if (hs.length < 2) hs = "0" + hs; + if (fs.length < 2) fs = "0" + fs; + if (min.length < 2) min = "0" + min; + + let timeString = min + ":" + fs + ":" + hs; + } + + + + return timeString; +} + + +function setCookie(name, value, years){ + let expires = ""; + if (years){ + let date = new Date(); + date.setTime(date.getTime() + (years * 365 * 24 * 60 * 60 * 1000)); + expires = "; expires=" + date.toUTCString(); + } + document.cookie = name + "=" + value + expires + "; path=/"; +} + +function getCookie(name){ + let nameEQ = name + "="; + let ca = document.cookie.split(';'); + for (let i = 0; i < ca.length; i++){ + let c = ca[i]; + while (c.charAt(0) == ' ') c = c.substring(1, c.length); + if (c.indexOf(nameEQ) == 0) return c.substring(nameEQ.length, c.length); + } + return null; +} + +function deleteCookies(){ + for (let i = 0; i < arguments.length; i++) setCookie(arguments[i], "", -1); +} + +function deleteAllCookies(){ + let cookies = document.cookie.split(";"); + for (let i = 0; i < cookies.length; i++) deleteCookies(cookies[i].split("=")[0]); +} + +Array.prototype.shuffle = function(){ + let currentIndex = this.length, temporaryValue, randomIndex; + while (0 != currentIndex){ + randomIndex = floor(random() * currentIndex); + currentIndex -= 1; + temporaryValue = this[currentIndex]; + this[currentIndex] = this[randomIndex]; + this[randomIndex] = temporaryValue; + } +} + +Array.prototype.copy = function(){ + return this.slice(0); +}; + +//Divides big Array into big multidimensional Array +Array.prototype.partitiate = function(dimensions){ + if (!dimensions) return this; + let parts = []; + while(this.length) parts.push(this.splice(0, round(pow(this.length, 1 / (1 + 1 / dimensions)))).partitiate(dimensions - 1)); + return parts; +} + +String.prototype.isValidHEX = function(){ + return /(^#[0-9A-Fa-f]{6}$)|(^#[0-9A-Fa-f]{3}$)/i.test(this); +} + +String.prototype.capitalize = function(){ + return this.charAt(0).toUpperCase() + this.slice(1); +} + +function debugInformation(x, y){ + push(); + textSize(12); + textStyle(NORMAL); + stroke(255); + strokeWeight(1); + fill(255); + text("FPS : " + round(frameRate()), 10 + x, 10 + textAscent("FPS : ") + y); + text("MouseX : " + round(mouseX + x), 10 + x, 10 + textAscent("FPS : ") + 10 + textAscent("MouseX : ") + y); + text("MouseY : " + round(-mouseY - y), 10 + x, 10 + textAscent("FPS : ") + 10 + textAscent("MouseX : ") + 10 + textAscent("MouseY : ") + y); + pop(); +} + + + +class ColorPicker { + + constructor(){ + this.movingObject = ""; + } + + updateFromGraphical(){ + this.h = (1 - $("#hue_picker").position().top / $("#hue").height()) * 360; + this.s = ($("#sb_picker").position().left + 8) / $("#saturation").width() * 100; + this.v = (1 - ($("#sb_picker").position().top + 8) / $("#value").height()) * 100; + + this.r = HSVtoRGB(this.h, this.s, this.v).r; + this.g = HSVtoRGB(this.h, this.s, this.v).g; + this.b = HSVtoRGB(this.h, this.s, this.v).b; + + this.hex = RGBtoHEX(this.r, this.g, this.b); + + this.updateInterface(); + } + + updateFromHSV(){ + this.h = $($("#color_picker_hsv input")[0]).val(); + this.s = $($("#color_picker_hsv input")[1]).val(); + this.v = $($("#color_picker_hsv input")[2]).val(); + + this.r = HSVtoRGB(this.h, this.s, this.v).r; + this.g = HSVtoRGB(this.h, this.s, this.v).g; + this.b = HSVtoRGB(this.h, this.s, this.v).b; + + this.hex = RGBtoHEX(this.r, this.g, this.b); + + this.updateFromHEX(null, true); + + this.updateInterface(); + } + + updateFromRGB(){ + this.r = $($("#color_picker_rgb input")[0]).val(); + this.g = $($("#color_picker_rgb input")[1]).val(); + this.b = $($("#color_picker_rgb input")[2]).val(); + + this.h = RGBtoHSV(this.r, this.g, this.b).h; + this.s = RGBtoHSV(this.r, this.g, this.b).s; + this.v = RGBtoHSV(this.r, this.g, this.b).v; + + this.hex = RGBtoHEX(this.r, this.g, this.b); + + this.updateFromHEX(null, true); + + this.updateInterface(); + } + + updateFromHEX(input, otf){ + if (!otf){ //Not on the fly + if ($(input).val().isValidHEX()) this.hex = $(input).val(); + else { + alert("Error!"); + return; + } + } + + this.r = HEXtoRGB(this.hex).r; + this.g = HEXtoRGB(this.hex).g; + this.b = HEXtoRGB(this.hex).b; + + this.h = RGBtoHSV(this.r, this.g, this.b).h; + this.s = RGBtoHSV(this.r, this.g, this.b).s; + this.v = RGBtoHSV(this.r, this.g, this.b).v; + + this.updateInterface(); + } + + updateInterface(){ + + let r = $($("#color_picker_rgb input")[0]), + g = $($("#color_picker_rgb input")[1]), + b = $($("#color_picker_rgb input")[2]), + h = $($("#color_picker_hsv input")[0]), + s = $($("#color_picker_hsv input")[1]), + v = $($("#color_picker_hsv input")[2]), + hex = $("#color_picker_hex"), + bgColor; + + r.val(round(this.r)); + g.val(round(this.g)); + b.val(round(this.b)); + h.val(round(this.h)); + s.val(round(this.s)); + v.val(round(this.v)); + bgColor = color(this.r, 0, 0); + r.css({ + "background-color": bgColor.toString(), + "color": fontColor(bgColor) + }); + bgColor = color(0, this.g, 0); + g.css({ + "background-color": bgColor.toString(), + "color": fontColor(bgColor) + }); + bgColor = color(0, 0, this.b); + b.css({ + "background-color": bgColor.toString(), + "color": fontColor(bgColor) + }); + colorMode(HSL); + bgColor = color(this.h, 100, 50); + h.css({ + "background-color": bgColor.toString(), + "color": fontColor(bgColor) + }); + bgColor = color(this.h, this.s, 100 - this.s / 2); + s.css({ + "background-color": bgColor.toString(), + "color": fontColor(bgColor) + }); + bgColor = color(this.h, 100, this.v / 2); + v.css({ + "background-color": bgColor.toString(), + "color": fontColor(bgColor) + }); + colorMode(RGB); + hex.val(this.hex); + hex.css({ + "background-color": this.hex, + "color": fontColor(color(this.hex)) + }); + + + let sRGB = HSVtoRGB(this.h, 100, 100); + let saturationBackground = "linear-gradient(to right, #FFF 0%, rgb(" + + sRGB.r + "," + + sRGB.g + "," + + sRGB.b + ") 100%)"; + $("#hue_picker").css("top", (1 - this.h / 360) * $("#hue").height()); + $("#sb_picker").css({ + "left": this.s / 100 * $("#saturation").width() - 8, + "top": (1 - this.v / 100) * $("#value").height() - 8 + }); + $("#saturation").css("background", saturationBackground); + + } + + mousePressed(){ + let x = winMouseX - $("#saturation").offset().left; + let y = winMouseY - $("#value").offset().top; + if (x > 0 && x < $("#saturation").width() && y > 0 && y < $("#value").height()){ + this.movingObject = "sb"; + } + if (x > $("#saturation").width() + 6 && x < $("#saturation").width() + 6 + $("#hue").width() && y > 0 && y < $("#hue").height()){ + this.movingObject = "hue"; + } + this.mouseDragged(); + } + + mouseDragged(){ + if (this.movingObject == "hue"){ + let objH = $("#hue"); + let picker = $("#hue_picker"); + let h = winMouseY - objH.offset().top; + if (h > 0 && h < objH.height()){ + picker.css("top", h - 1); + } else if (h > objH.height()){ + picker.css("top", objH.height() - 1); + } else if (h < 0){ + picker.css("top", -1); + } + } + + if (this.movingObject == "sb"){ + let objS = $("#saturation"); + let objV = $("#value"); + let picker = $("#sb_picker"); + let s = winMouseX - objS.offset().left; + let v = winMouseY - objV.offset().top; + if (s > 0 && s < objS.width()){ + picker.css("left", s - 8); + } else if (s < 0){ + picker.css("left", -8); + } else if (s < objS.width()){ + picker.css("left", objS.width() - 8); + } + if (v > 0 && v < objV.height()){ + picker.css("top", v - 8); + } else if (v < 0){ + picker.css("top", -8); + } else if (v > objV.height()){ + picker.css("top", objV.height() - 8); + } + } + + this.updateFromGraphical(); + } + + mouseReleased(){ + this.movingObject = ""; + } + + getColor(){ + return this.hex; + } +} + +function fontColor(bg){ + //https://www.nbdtech.com/Blog/archive/2008/04/27/Calculating-the-Perceived-Brightness-of-a-Color.aspx + let o = (red(bg) * 299 + green(bg) * 587 + blue(bg) * 114) / 1000; + return (o > 125) ? "#000" : "#CCC"; +} + +//www.stackoverflow.com --> +function RGBtoHEX(r, g, b) { + let rgb = b | (g << 8) | (r << 16); + return '#' + (0x1000000 + rgb).toString(16).slice(1); +} + +function HEXtoRGB(hex) { + let shorthandRegex = /^#?([a-fA-F\d])([a-fA-F\d])([a-fA-F\d])$/i; + hex = hex.replace(shorthandRegex, function(m, r, g, b) { + return r + r + g + g + b + b; + }); + let result = /^#?([a-fA-F\d]{2})([a-fA-F\d]{2})([a-fA-F\d]{2})$/i.exec(hex); + return result ? { + r: parseInt(result[1], 16), + g: parseInt(result[2], 16), + b: parseInt(result[3], 16) + } : null; +} + +function HSVtoRGB(h, s, v) { + let r, g, b, i, f, p, q, t; + if (arguments.length === 1) { + s = h.s, v = h.v, h = h.h; + } + h /= 360; + s /= 100; + v /= 100; + i = Math.floor(h * 6); + f = h * 6 - i; + p = v * (1 - s); + q = v * (1 - f * s); + t = v * (1 - (1 - f) * s); + switch (i % 6) { + case 0: r = v, g = t, b = p; break; + case 1: r = q, g = v, b = p; break; + case 2: r = p, g = v, b = t; break; + case 3: r = p, g = q, b = v; break; + case 4: r = t, g = p, b = v; break; + case 5: r = v, g = p, b = q; break; + } + return { + r: r * 255, + g: g * 255, + b: b * 255 + }; +} + +function RGBtoHSV(r, g, b) { + if (arguments.length === 1) { + g = r.g, b = r.b, r = r.r; + } + let max = Math.max(r, g, b), min = Math.min(r, g, b), + d = max - min, + h, + s = (max === 0 ? 0 : d / max), + v = max / 255; + + switch (max) { + case min: h = 0; break; + case r: h = (g - b) + d * (g < b ? 6: 0); h /= 6 * d; break; + case g: h = (b - r) + d * 2; h /= 6 * d; break; + case b: h = (r - g) + d * 4; h /= 6 * d; break; + } + + return { + h: h * 360, + s: s * 100, + v: v * 100 + }; +} + +function ranBool(chance){ + if (!chance) chance = 2; + return Math.floor(Math.random() * chance + 1) == chance ? true : false; +} \ No newline at end of file diff --git a/public/data/scripts/sketch.js b/public/data/scripts/sketch.js new file mode 100644 index 0000000..a159c13 --- /dev/null +++ b/public/data/scripts/sketch.js @@ -0,0 +1,90 @@ +"use strict" + +let socket; + +let debug = false, + viewPort = {x: 0, y: 0}, + font, + settings; + +let userInterface = true, + cards = [], + hidden, + game, + cardSize; + +function preload(){ + font = loadFont("data/styles/font.ttf"); + settings = loadJSON('data/settings/settings.json'); + loadCards(); +} + +function loadCards(){ + loadJSON("data/settings/sources.json", createCards); +} + +function setup(){ + let ctx = createCanvas(50, 50); + ctx.parent("loader"); + playerDomSetup(0); + hideDomElements(); + $("body").show(); +} + +function draw(){ + clear(); + let angleStart = frameCount / 10 * PI; + noFill(); + stroke("#33AA33"); + strokeWeight(5); + arc(25, 25, 45, 45, angleStart, angleStart + PI + HALF_PI); +} + +function loader(dom){ + //$(dom).attr("disabled", "disabled"); + //$("#loader").show(); +} + +function createCards(json){ + cards = []; + let id = 0; + hidden = json.hidden; + for (let src of json.sources){ + cards.push(new Card(id, src), + new Card(id + 1, src)); + id += 2; + } +} + +function cardClicked(card){ + if (!game || !game.running || game.cards.includes(card)) return; + game.revealCard(false, card.id); +} + +function playerDomSetup(playerCount, players){ + for (let i = 0; i < playerCount; i++){ + if (i % 2 == 0) $("#left > div:eq(" + i / 2 + ")").show(); + else $("#right > div:eq(" + (i - 1) / 2 + ")").show(); + } + for (let i = playerCount; i < 8; i++){ + if (i % 2 == 0) $("#left > div:eq(" + i / 2 + ")").hide(); + else $("#right > div:eq(" + (i - 1) / 2 + ")").hide(); + } + if (players != null) for (let p of players) p.hasTurn(false); +} + +function hideDomElements(){ + //Only leaders shall see those + $("#showInterface_btn, #interface").hide(); + + //For all + $("#player-count, #player-name, .error-label, " + + "#join-lobby, #lobby, .setup:eq(1), .setup:eq(2), #p5_loading, #loader").hide(); +} + +function createEmpty(){ + let empty = $("
"); + empty.attr("class", "card empty"); + return empty; +} + diff --git a/public/data/settings/get_port.php b/public/data/settings/get_port.php new file mode 100644 index 0000000..a2dd41f --- /dev/null +++ b/public/data/settings/get_port.php @@ -0,0 +1,2 @@ + + + + + + + + + + + + + + + + + + + Memory + + +
+
+
+
+
+ + +
+ +
+
+
+
+ + +
+ +
+
+
+
+ + +
+ +
+
+
+
+ + +
+ +
+
+
+
+ +
+
+ + +
+
+
+
+ Type +
+ +
+ + +
+
+
+ Player + +
+
+ Enter your name:
+
+ +
+ + +
+ Enter Lobby Code:
+
+ +
+ +
+
+
+
+ + + + +
+
+ + \ No newline at end of file diff --git a/public/styles.css b/public/styles.css new file mode 100644 index 0000000..b8eccec --- /dev/null +++ b/public/styles.css @@ -0,0 +1,251 @@ +a:link, a:hover, a:active, a:visited{color: #000;} + +html, body{margin: 0; padding: 0; height: 100%; width: 100%;} + +canvas{margin: 0; padding: 0; border: none; display: block;} + +button:hover{cursor: pointer;} + +@font-face{ + font-family: "Rametto"; + src: url("data/styles/font.ttf"); +} + +*{ + font-family: "Rametto"; +} + +html{ + background-image: url(data/images/background.jpg); + background-size: 100% 100%; +} + +#interface{ + width: 100%; + position: fixed; + bottom: 0; + margin: auto; + visibility: visible; + opacity: 1; + float: left; + text-align: center; +} + +#interface button{ + width: 100px; + padding: 5px; + margin: 2px; + border-radius: 4px; + box-shadow: 2px 2px 4px #000; + background-color: rgb(36, 126, 248); + float: left; +} + +#interface button:hover{ + filter: brightness(70%); +} + +#game{ + display: flex; + width: 100%; + height: 100%; + background-image: url(data/images/background.jpg); + background-size: 100% 100%; +} + +#table{ + margin: auto; + width: 100vh; + height: 100vh; + display: flex; + flex-wrap: wrap; + justify-content: space-around; + align-items: center; +} + +.card_on_table{ + margin: 0 calc(1vh - 2px); + transition: filter 150ms ease-in-out, transform 300ms ease-in-out; + cursor: pointer; +} +.card_on_table:hover{ + filter: brightness(70%); +} + +.card_on_staple{ + position: relative; + margin-left: calc((-8vh - 2px) + 4px - 1px); + top: 0px; + float: left; +} +.card_on_staple:first-child{ + margin-left: 0px; +} + +.card_on_staple:hover{ + transform: none; +} + +.card{ + width: 8vh; + height: 8vh; + background-size: 100% 100%; + border-radius: 7px; + border: 1px solid #000; + box-shadow: 10px 10px 15px #000; +} + +.empty{ + margin: 0 calc(1vh - 2px); + border: 1px solid rgba(0, 0, 0, 0); + box-shadow: none; +} + +.player-holder{ + display: flex; + flex-wrap: wrap; + justify-content: space-around; + align-items: center; + width: calc((100vw - 100vh) / 2); +} + +.player{ + background-color: rgba(0, 0, 0, 0.5); + width: 90%; + padding: 10px; + border: 3px solid #000; + border-radius: 10px; + box-shadow: 10px 10px 15px #000; +} + +.staple{ + width: 95%; + height: calc(8vh + 2px); + margin: 50px 2.5% 2.5% 2.5%; +} + +.has-turn{ + width: 30px; + height: 30px; + border: 1px solid #000; + box-shadow: 2px 2px 5px #000; + border-radius: 2px; +} + +.pairs-label{ + font-size: 15px; +} + +#right .pairs-label{ + float: right; +} + +#left .pairs-label{ + float: left; +} + +.player > span{ + font-size: 23px; + line-height: 30px; +} + +#left .has-turn{ + margin-left: 10px; +} + +#right .has-turn{ + margin-right: 10px; +} + +#left .has-turn, #left .player > span{ + float: right; +} + +#right .has-turn, #right .player > span{ + float: left; +} + +#initialize-wrapper{ + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: rgba(0, 0, 0, 0.8); + display: flex; + justify-content: center; + align-items: center; +} + +.initialize-lobby{ + padding: 20px; + background-color: rgb(224, 224, 224); + border-radius: 10px; + border: 5px solid #000; + box-shadow: 5px 5px 10px #000; + display: flex; + align-items: center; + flex-direction: column; +} + +#initialize fieldset{ + border: 2px solid #000; + border-radius: 5px; + background-color: rgb(150, 150, 150); + text-align: center; +} + +#initialize > fieldset{ + margin: 10px; + box-shadow: 4px 4px 8px #000; +} + +#loader{ + +} + +.setup{ + width: 200px; + border-radius: 3px; + border: 2px solid #000; + background-color: rgb(53, 204, 53); + font-size: 20px; + margin: 10px; + box-shadow: 3px 3px 6px #000; +} + +.setup:hover{ + filter: brightness(70%); +} + + + +.error-label{ + font-size: 10px; + color: red; +} + +.lobby-member{ + padding: 5px; + margin: 5px; + border: 1px solid #000; + background-color: rgb(180, 180, 180); + border-radius: 5px; + box-shadow: 4px 4px 6px #000; +} + +#lobby-leader{ + border-width: 4px; + border-color: #009a06; +} + +#lobby > span{ + font-size: 10px; +} + +#start-lobby:disabled{ + filter: brightness(40%); +} +#start-lobby:disabled:hover{ + filter: brightness(40%); +} \ No newline at end of file diff --git a/public/thumbnail.png b/public/thumbnail.png new file mode 100644 index 0000000..ec54262 Binary files /dev/null and b/public/thumbnail.png differ diff --git a/server/.gitignore b/server/.gitignore new file mode 100644 index 0000000..4446669 --- /dev/null +++ b/server/.gitignore @@ -0,0 +1,4 @@ +logs +node_modules +out +.env \ No newline at end of file diff --git a/server/package-lock.json b/server/package-lock.json new file mode 100644 index 0000000..d090b32 --- /dev/null +++ b/server/package-lock.json @@ -0,0 +1,439 @@ +{ + "name": "chainreact-server", + "version": "2.0", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "chainreact-server", + "version": "2.0", + "dependencies": { + "dotenv": "^16.0.3", + "https": "^1.0.0", + "socket.io": "^4.4.1", + "typescript": "^5.0.2" + }, + "devDependencies": { + "@types/node": "^18.15.3" + } + }, + "base": { + "name": "game-server", + "version": "2.0", + "extraneous": true, + "dependencies": { + "dotenv": "^16.0.3", + "https": "^1.0.0", + "ini": "^2.0.0", + "socket.io": "^4.4.1" + }, + "devDependencies": { + "@types/node": "^17.0.18", + "typescript": "^4.8.4" + } + }, + "node_modules/@socket.io/component-emitter": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz", + "integrity": "sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg==" + }, + "node_modules/@types/cookie": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.4.1.tgz", + "integrity": "sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q==" + }, + "node_modules/@types/cors": { + "version": "2.8.13", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.13.tgz", + "integrity": "sha512-RG8AStHlUiV5ysZQKq97copd2UmVYw3/pRMLefISZ3S1hK104Cwm7iLQ3fTKx+lsUH2CE8FlLaYeEA2LSeqYUA==", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/node": { + "version": "18.15.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.15.3.tgz", + "integrity": "sha512-p6ua9zBxz5otCmbpb5D3U4B5Nanw6Pk3PPyX05xnxbB/fRv71N7CPmORg7uAD5P70T0xmx1pzAx/FUfa5X+3cw==" + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/base64id": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", + "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==", + "engines": { + "node": "^4.5.0 || >= 5.9" + } + }, + "node_modules/cookie": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz", + "integrity": "sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/dotenv": { + "version": "16.0.3", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.0.3.tgz", + "integrity": "sha512-7GO6HghkA5fYG9TYnNxi14/7K9f5occMlp3zXAuSxn7CKCxt9xbNWG7yF8hTCSUchlfWSe3uLmlPfigevRItzQ==", + "engines": { + "node": ">=12" + } + }, + "node_modules/engine.io": { + "version": "6.4.1", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.4.1.tgz", + "integrity": "sha512-JFYQurD/nbsA5BSPmbaOSLa3tSVj8L6o4srSwXXY3NqE+gGUNmmPTbhn8tjzcCtSqhFgIeqef81ngny8JM25hw==", + "dependencies": { + "@types/cookie": "^0.4.1", + "@types/cors": "^2.8.12", + "@types/node": ">=10.0.0", + "accepts": "~1.3.4", + "base64id": "2.0.0", + "cookie": "~0.4.1", + "cors": "~2.8.5", + "debug": "~4.3.1", + "engine.io-parser": "~5.0.3", + "ws": "~8.11.0" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/engine.io-parser": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.0.6.tgz", + "integrity": "sha512-tjuoZDMAdEhVnSFleYPCtdL2GXwVTGtNjoeJd9IhIG3C1xs9uwxqRNEu5WpnDZCaozwVlK/nuQhpodhXSIMaxw==", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/https": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/https/-/https-1.0.0.tgz", + "integrity": "sha512-4EC57ddXrkaF0x83Oj8sM6SLQHAWXw90Skqu2M4AEWENZ3F02dFJE/GARA8igO79tcgYqGrD7ae4f5L3um2lgg==" + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/socket.io": { + "version": "4.6.1", + "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.6.1.tgz", + "integrity": "sha512-KMcaAi4l/8+xEjkRICl6ak8ySoxsYG+gG6/XfRCPJPQ/haCRIJBTL4wIl8YCsmtaBovcAXGLOShyVWQ/FG8GZA==", + "dependencies": { + "accepts": "~1.3.4", + "base64id": "~2.0.0", + "debug": "~4.3.2", + "engine.io": "~6.4.1", + "socket.io-adapter": "~2.5.2", + "socket.io-parser": "~4.2.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/socket.io-adapter": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.2.tgz", + "integrity": "sha512-87C3LO/NOMc+eMcpcxUBebGjkpMDkNBS9tf7KJqcDsmL936EChtVva71Dw2q4tQcuVC+hAUy4an2NO/sYXmwRA==", + "dependencies": { + "ws": "~8.11.0" + } + }, + "node_modules/socket.io-parser": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.2.tgz", + "integrity": "sha512-DJtziuKypFkMMHCm2uIshOYC7QaylbtzQwiMYDuCKy3OPkjLzu4B2vAhTlqipRHHzrI0NJeBAizTK7X+6m1jVw==", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/typescript": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.0.2.tgz", + "integrity": "sha512-wVORMBGO/FAs/++blGNeAVdbNKtIh1rbBL2EyQ1+J9lClJ93KiiKe8PmFIVdXhHcyv44SL9oglmfeSsndo0jRw==", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=12.20" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/ws": { + "version": "8.11.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.11.0.tgz", + "integrity": "sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + } + }, + "dependencies": { + "@socket.io/component-emitter": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz", + "integrity": "sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg==" + }, + "@types/cookie": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.4.1.tgz", + "integrity": "sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q==" + }, + "@types/cors": { + "version": "2.8.13", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.13.tgz", + "integrity": "sha512-RG8AStHlUiV5ysZQKq97copd2UmVYw3/pRMLefISZ3S1hK104Cwm7iLQ3fTKx+lsUH2CE8FlLaYeEA2LSeqYUA==", + "requires": { + "@types/node": "*" + } + }, + "@types/node": { + "version": "18.15.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.15.3.tgz", + "integrity": "sha512-p6ua9zBxz5otCmbpb5D3U4B5Nanw6Pk3PPyX05xnxbB/fRv71N7CPmORg7uAD5P70T0xmx1pzAx/FUfa5X+3cw==" + }, + "accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "requires": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + } + }, + "base64id": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", + "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==" + }, + "cookie": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz", + "integrity": "sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==" + }, + "cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "requires": { + "object-assign": "^4", + "vary": "^1" + } + }, + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "requires": { + "ms": "2.1.2" + } + }, + "dotenv": { + "version": "16.0.3", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.0.3.tgz", + "integrity": "sha512-7GO6HghkA5fYG9TYnNxi14/7K9f5occMlp3zXAuSxn7CKCxt9xbNWG7yF8hTCSUchlfWSe3uLmlPfigevRItzQ==" + }, + "engine.io": { + "version": "6.4.1", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.4.1.tgz", + "integrity": "sha512-JFYQurD/nbsA5BSPmbaOSLa3tSVj8L6o4srSwXXY3NqE+gGUNmmPTbhn8tjzcCtSqhFgIeqef81ngny8JM25hw==", + "requires": { + "@types/cookie": "^0.4.1", + "@types/cors": "^2.8.12", + "@types/node": ">=10.0.0", + "accepts": "~1.3.4", + "base64id": "2.0.0", + "cookie": "~0.4.1", + "cors": "~2.8.5", + "debug": "~4.3.1", + "engine.io-parser": "~5.0.3", + "ws": "~8.11.0" + } + }, + "engine.io-parser": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.0.6.tgz", + "integrity": "sha512-tjuoZDMAdEhVnSFleYPCtdL2GXwVTGtNjoeJd9IhIG3C1xs9uwxqRNEu5WpnDZCaozwVlK/nuQhpodhXSIMaxw==" + }, + "https": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/https/-/https-1.0.0.tgz", + "integrity": "sha512-4EC57ddXrkaF0x83Oj8sM6SLQHAWXw90Skqu2M4AEWENZ3F02dFJE/GARA8igO79tcgYqGrD7ae4f5L3um2lgg==" + }, + "mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==" + }, + "mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "requires": { + "mime-db": "1.52.0" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==" + }, + "object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==" + }, + "socket.io": { + "version": "4.6.1", + "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.6.1.tgz", + "integrity": "sha512-KMcaAi4l/8+xEjkRICl6ak8ySoxsYG+gG6/XfRCPJPQ/haCRIJBTL4wIl8YCsmtaBovcAXGLOShyVWQ/FG8GZA==", + "requires": { + "accepts": "~1.3.4", + "base64id": "~2.0.0", + "debug": "~4.3.2", + "engine.io": "~6.4.1", + "socket.io-adapter": "~2.5.2", + "socket.io-parser": "~4.2.1" + } + }, + "socket.io-adapter": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.2.tgz", + "integrity": "sha512-87C3LO/NOMc+eMcpcxUBebGjkpMDkNBS9tf7KJqcDsmL936EChtVva71Dw2q4tQcuVC+hAUy4an2NO/sYXmwRA==", + "requires": { + "ws": "~8.11.0" + } + }, + "socket.io-parser": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.2.tgz", + "integrity": "sha512-DJtziuKypFkMMHCm2uIshOYC7QaylbtzQwiMYDuCKy3OPkjLzu4B2vAhTlqipRHHzrI0NJeBAizTK7X+6m1jVw==", + "requires": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.1" + } + }, + "typescript": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.0.2.tgz", + "integrity": "sha512-wVORMBGO/FAs/++blGNeAVdbNKtIh1rbBL2EyQ1+J9lClJ93KiiKe8PmFIVdXhHcyv44SL9oglmfeSsndo0jRw==" + }, + "vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==" + }, + "ws": { + "version": "8.11.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.11.0.tgz", + "integrity": "sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==", + "requires": {} + } + } +} diff --git a/server/package.json b/server/package.json new file mode 100644 index 0000000..e4f5b4a --- /dev/null +++ b/server/package.json @@ -0,0 +1,18 @@ +{ + "name": "chainreact-server", + "version": "2.0", + "private": true, + "scripts": { + "start": "node out/index.js", + "tsc": "npx tsc" + }, + "dependencies": { + "dotenv": "^16.0.3", + "https": "^1.0.0", + "socket.io": "^4.4.1", + "typescript": "^5.0.2" + }, + "devDependencies": { + "@types/node": "^18.15.3" + } +} diff --git a/server/src/client.ts b/server/src/client.ts new file mode 100644 index 0000000..873e6a2 --- /dev/null +++ b/server/src/client.ts @@ -0,0 +1,168 @@ +import {Room} from "./room.js" +import {ConnectionManager, serializeObject} from "./manager.js" +import {log} from "./logger.js"; +import * as SocketIO from "socket.io"; + +export class Client { + + socket: SocketIO.Socket; + name: string; + game: string; + id: string; + isReady: boolean; + isPlayer: boolean; + isSpectator: boolean; + + constructor(socket: SocketIO.Socket, manager: ConnectionManager) { + this.socket = socket; + // @ts-ignore + this.name = socket.handshake.query.name; + // @ts-ignore + this.game = socket.handshake.query.game; + this.id = socket.id; + this.setEvents(manager) + } + + get serialized(): Serialized.Client { + return { + id: this.id, + name: this.name, + game: this.game, + isReady: this.isReady, + isPlayer: this.isPlayer, + isSpectator: this.isSpectator + }; + } + + setEvents(mng: ConnectionManager): void { + let s = this.socket; + s.on('room-list', () => this.sendRoomList()); + s.on('client-list', () => this.sendClientList()); + s.on('set-ready', ready => this.setReady(ready)); + s.on('game-settings', settings => this.setGameSettings(settings)); + s.on('create-lobby', (settings, name) => this.createRoom(settings, name)); + s.on('join-lobby', roomId => this.joinRoom(roomId)); + s.on('leave-lobby', roomId => this.leaveRoom(roomId)); + s.on('join-spectators', () => this.joinSpectators()); + s.on('join-players', () => this.joinPlayers()); + s.on('start-game', lobbyId => mng.startGame(this, lobbyId)); + s.on('stop-game', lobbyId => mng.stopGame(this, lobbyId)); + s.on('feedback', content => mng.saveFeedbackToFile(this, content)); + s.on('disconnect', () => mng.disconnected(this)); + + this.send('connected') + } + + sendRoomList(): void { + let rooms = ConnectionManager.RoomListByGame(this.game); + this.send('room-list', rooms) + } + + sendClientList(): void { + let clients = ConnectionManager.ClientListByClientId(this.id); + this.send('client-list', clients) + } + + setReady(ready: boolean): void { + let room = Room.getByClientId(this.id, ConnectionManager.Instance.rooms); + + if (room) { + this.isReady = ready; + room.toAll('client-list', room.clients) + } + } + + setGameSettings(settings: any): void { + let room = Room.getByClientId(this.id, ConnectionManager.Instance.rooms); + + if (room) { + room.gameSettings = settings; + room.toAll('game-settings', settings) + } + } + + createRoom(settings: Settings.Global, name: string): void { + let room = ConnectionManager.Instance.createRoom(settings, name); + + room.add(this); + this.send('created-lobby', room); + + log('lobby-created', this, room) + } + + joinRoom(roomId: string): Room { + let room = Room.getByRoomId(roomId, ConnectionManager.Instance.rooms); + + if (!room) { + this.send('join-failed', 'Room does not exist!'); + log('join-non-existent', this, new Room('not-existent', roomId)) + } else if (room.hasStarted && !room.settings.spectators) { + this.send('join-failed', 'Game has started yet!'); + log('join-started', this, room) + } else { + room.add(this); + log('member-joined', this, room) + } + return room + } + + leaveRoom(_roomId: string): void { + let room = Room.getByClientId(this.id, ConnectionManager.Instance.rooms); + + if (!room) + return; + + this.leave(room.id); + if (room.runningGame) + room.runningGame.removeClient(this); + room.clients.splice(room.clients.indexOf(this), 1); + room.toAll('member-left', this.id, this.name); + room.toAll('client-list', room.clients); + + this.send('left-lobby'); + + log('member-left', this, room); + + if (room.isEmpty && !room.settings.always) { + ConnectionManager.Instance.deleteRoom(room) + } + } + + joinSpectators() { + let room = Room.getByClientId(this.id, ConnectionManager.Instance.rooms); + if (!room) + return; + + this.isSpectator = true; + this.isPlayer = false; + + room.toAll('client-list', room.clients) + } + + joinPlayers() { + let room = Room.getByClientId(this.id, ConnectionManager.Instance.rooms); + if (!room) + return; + + if (room.hasStarted) + return; + + this.isSpectator = false; + this.isPlayer = true; + + room.toAll('client-list', room.clients) + } + + send(event: string, ...args: any[]): void { + this.socket.emit(event, ...serializeObject(args)) + } + + join(roomId: string): void { + this.socket.join(roomId) + } + + leave(roomId: string): void { + this.socket.leave(roomId) + } + +} \ No newline at end of file diff --git a/server/src/definitions/serialized.d.ts b/server/src/definitions/serialized.d.ts new file mode 100644 index 0000000..b7b6a04 --- /dev/null +++ b/server/src/definitions/serialized.d.ts @@ -0,0 +1,20 @@ +declare namespace Serialized { + + interface Lobby { + id: string + name: string + game: string + clientCounts: number[] + clients: Client[] + hasStarted: boolean + } + + interface Client { + id: string + name: string + game: string + isReady: boolean + isPlayer: boolean + isSpectator: boolean + } +} \ No newline at end of file diff --git a/server/src/definitions/settings.d.ts b/server/src/definitions/settings.d.ts new file mode 100644 index 0000000..0d73fef --- /dev/null +++ b/server/src/definitions/settings.d.ts @@ -0,0 +1,74 @@ +declare module Settings { + interface Global { + project: Project + frameWork: FrameWork + game: any + always: boolean + spectators: boolean + } + + interface Project { + name: string + author: string + playerCounts: number[] + } + + interface FrameWork { + frameRate: number + updateRate: number + width: number + height: number + } + + interface Game { + ball: Ball + player: Player + cw: number + ch: number + } + + interface Ball { + radius: number + velocity: number + acceleration: number + runUp: Ball.RunUp + color: Color + cw: number + ch: number + } + + interface Player { + width: number + height: number + margin: number + points: number + normal: State + weakened: State + enhanced: State + cw: number + ch: number + } + + interface Color { + stroke: string + fill: string + } + + interface State { + vel: Vector + color: Color + moveMargin: number + } + + interface Vector { + x: number + y: number + } + + module Ball { + interface RunUp { + min: number + max: number + } + } +} \ No newline at end of file diff --git a/server/src/game_standard.ts b/server/src/game_standard.ts new file mode 100644 index 0000000..33b4ca8 --- /dev/null +++ b/server/src/game_standard.ts @@ -0,0 +1,37 @@ +import {Room} from "./room.js" +import {Client} from "./client.js" + +export class ServerGame { + + room: Room; + settings: Settings.Global; + game: any; + + constructor(room: Room, settings: Settings.Global) { + this.settings = settings; + this.room = room; + this.room.clients.forEach(c => this.addClient(c)) + } + + addClient(client: Client): void { + this.setEvents(client) + } + + removeClient(client: Client): void { + this.removeEvents(client) + } + + gameAction(action: string, ...args: any[]): void { + } + + setEvents(client: Client): void { + let socket = client.socket; + socket.on('game-action', (action, ...args) => this.gameAction(action, ...args)) + } + + removeEvents(client: Client): void { + let socket = client.socket; + socket.removeAllListeners('game-action') + } + +} \ No newline at end of file diff --git a/server/src/index.ts b/server/src/index.ts new file mode 100644 index 0000000..478ef86 --- /dev/null +++ b/server/src/index.ts @@ -0,0 +1,7 @@ +import {Memory} from "./memory"; +import {StartServer} from "./start"; + +StartServer({ + useP2P: false, + gameClass: Memory +}); \ No newline at end of file diff --git a/server/src/logger.ts b/server/src/logger.ts new file mode 100644 index 0000000..2b4e936 --- /dev/null +++ b/server/src/logger.ts @@ -0,0 +1,105 @@ +import {Room} from "./room.js" +import {Client} from "./client.js" + +import * as fs from "fs"; +import * as util from "util"; + +let logFolder = "./logs"; + +if (!fs.existsSync(logFolder)) { + fs.mkdirSync(logFolder); +} + +let logFile = fs.createWriteStream(logFolder + '/' + new Date().getTime() + '.log', {flags: 'a'}); +let logStdout = process.stdout; + +console.log = function () { + logFile.write(util.format.apply(null, arguments) + '\n'); + logStdout.write(util.format.apply(null, arguments) + '\n'); +}; + +console.error = console.log; + +process.on('uncaughtException', err => { + console.error('Uncaught error: ', err); + process.exit(1); +}); + +process.stdin.pipe(logFile); + + +export function log(type: string, client: Client, lobby?: Room, msg?: string) { + let now = new Date(Date.now()).toString(), message, name, game; + let date = '[' + now.substring(0, now.indexOf('GMT') - 1) + ']'; + + if (client) { + game = '[' + client.game + ']'; + let short = client.id.substring(0, Math.round(client.id.length / 3)); + name = '"' + client.name + '(' + short + '...)"'; + } else { + if (type === 'lobby-deleted') { + game = '[' + lobby.gameName + ']'; + } else { + game = '[undefined]'; + } + name = 'UNKNOWN'; + } + if (lobby) { + game = '[' + lobby.gameName + ']'; + } + switch (type) { + case 'join-non-existent': + message = name + ' tried to join non-existent lobby "' + lobby.id + '"'; + break; + case 'join-started': + message = name + ' tried to join the started game "' + lobby.id + '"'; + break; + case 'lobby-created': + message = name + ' created new lobby: "' + lobby.id + '"'; + break; + case 'game-started': + message = name + ' started the game: "' + lobby.id + '"'; + break; + case 'game-stopped': + message = name + ' stopped the game: "' + lobby.id + '"'; + break; + case 'member-joined': + message = name + ' joined the lobby "' + lobby.id + '"'; + break; + case 'member-left': + message = name + ' left the lobby "' + lobby.id + '"'; + break; + case 'lobby-deleted': + message = 'Lobby "' + lobby.id + '" was deleted'; + break; + case 'save-success': + message = msg; + break; + case 'save-error': + message = 'Failed to save contents to file: ' + msg; + break; + case 'load-success': + message = 'Successfully loaded and parsed file contents'; + break; + case 'load-error': + message = 'Failed to load file: ' + msg; + break; + case 'parse-error': + message = 'Failed to parse contents: ' + msg; + break; + case 'feedback': + message = 'Saved feedback to file: ' + msg; + break; + case 'connection': + message = name + ' connected'; + break; + case 'disconnection': + message = name + ' disconnected'; + break; + case 'startup': + message = msg; + break; + } + + console.log(date + game + ' ---> {' + message + '}'); +} \ No newline at end of file diff --git a/server/src/manager.ts b/server/src/manager.ts new file mode 100644 index 0000000..a15fabf --- /dev/null +++ b/server/src/manager.ts @@ -0,0 +1,148 @@ +import {Room} from "./room.js" +import {Client} from "./client.js" +import {log} from "./logger.js" +import * as fs from "fs"; +import * as SocketIO from "socket.io" + +export class ConnectionManager { + + static Instance: ConnectionManager; + io: SocketIO.Server; + rooms: Room[]; + + constructor(io: SocketIO.Server) { + ConnectionManager.Instance = this; + + this.io = io; + this.rooms = []; + + /*let drawSettings = { + project: { + name: 'global-draw', + playerCounts: null + }, + always: true, + spectators: true + }; + let drawRoom = this.createRoom(drawSettings, ''); + drawRoom.id = 'global-draw-room'; + drawRoom.startGame(); + this.rooms.push(drawRoom);*/ + } + + static RoomListByGame(game: string): Room[] { + return this.Instance.rooms.filter(l => l.gameName === game) + } + + static ClientListByClientId(clientId: string): Client[] { + let room = Room.getByClientId(clientId, this.Instance.rooms); + + return room.clients + } + + newSocket(socket: SocketIO.Socket): void { + let client = new Client(socket, this); + log('connection', client) + } + + roomListUpdate(): void { + this.io.sockets.emit('room-list', serializeObject(this.rooms)) + } + + createRoom(settings: Settings.Global | any, name: string): Room { + let roomId = Room.generateCode(10); + let room = new Room(name, roomId, settings, this.io); + + this.rooms.push(room); + this.roomListUpdate(); + + return room + } + + deleteRoom(room: Room): void { + this.rooms.splice(this.rooms.indexOf(room), 1); + this.roomListUpdate(); + + log('lobby-deleted', null, room) + } + + //Starts the game of a room with given id + startGame(client: Client, _roomId: string): void { + let lobby = Room.getByClientId(client.id, this.rooms); + if (!lobby) return; + + if (!lobby.hasStarted) { + lobby.startGame(); + log('game-started', client, lobby) + } + + this.io.sockets.emit('room-list', serializeObject(this.rooms)) + } + + //Stops the game of a lobby with given id + stopGame(client: Client, lobbyId: string): void { + let lobby = Room.getByRoomId(lobbyId, this.rooms); + if (!lobby) return; + + lobby.stopGame(client); + log('game-stopped', client, lobby) + } + + + //Saves user feedback to a file + saveFeedbackToFile(client: Client, content: string): void { + let date = new Date(Date.now()).toString(); + let path = "feedback/" + client.game + '.txt'; + let saveToFile = (content: string) => { + fs.writeFile(path, content, (err: any) => { + if (err) + log('save-error', client, null, err.message); + else + log('feedback', client, null, path) + }); + }; + if (fs.existsSync(path)) { + fs.readFile(path, 'utf8', (err, data) => { + if (err) + log('load-error', client, null, err.message); + else { + log('load-success', client, null); + let newContent = data + '\n\n\n\n' + date + '\n\n' + content; + saveToFile(newContent) + } + }) + } else { + saveToFile(date + '\n' + content) + } + } + + //Removes a disconnected client from all references + disconnected(client: Client): void { + let room = Room.getByClientId(client.id, this.rooms); + + if (room) + client.leaveRoom(room.id); + + log('disconnection', client) + } + +} + +export function serializeObject(object: any): any { + function serialize(obj: any) { + if (!obj) + return obj; + if (obj.serialized) + return obj.serialized; + else if (obj instanceof Array) { + let content = []; + obj.forEach(o => { + content.push(serialize(o)) + }); + return content + } + return obj + } + + return serialize(object) +} \ No newline at end of file diff --git a/server/src/memory.ts b/server/src/memory.ts new file mode 100644 index 0000000..47e081b --- /dev/null +++ b/server/src/memory.ts @@ -0,0 +1,14 @@ +import {Room} from "./room" +import {ServerGame} from "./game_standard" + +export class Memory extends ServerGame { + + constructor(room: Room, settings: Settings.Global) { + super(room, settings); + } + + gameAction(action: string, ...args: any[]) { + this.room.toAll('game-action', action, ...args); + } + +} \ No newline at end of file diff --git a/server/src/room.ts b/server/src/room.ts new file mode 100644 index 0000000..8510b03 --- /dev/null +++ b/server/src/room.ts @@ -0,0 +1,135 @@ +import {Client} from "./client.js" +import {ServerGame} from "./game_standard.js" +import {serializeObject} from "./manager.js"; +import {Server} from "socket.io"; + +export class Room { + + id: string; + gameName: string; + clientCounts: number[]; + io: Server; + clients: Client[]; + runningGame: ServerGame; + settings: Settings.Global; + gameSettings: any; + name: string; + + static GameClass: typeof ServerGame + + constructor(name: string, id: string, settings?: Settings.Global, io?: Server) { + this.id = id; + this.name = name; + if (!io || !settings) return; + this.settings = settings; + this.gameName = settings.project.name; + this.clientCounts = settings.project.playerCounts; + this.io = io; + this.clients = []; + this.gameSettings = {} + } + + get leader(): Client { + return this.players[0] + } + + get players(): Client[] { + return this.clients.filter(c => c.isPlayer) + } + + get spectators(): Client[] { + return this.clients.filter(c => c.isSpectator) + } + + get serialized(): Serialized.Lobby { + return { + id: this.id, + name: this.name, + game: this.gameName, + clientCounts: this.clientCounts, + clients: serializeObject(this.clients), + hasStarted: this.hasStarted + }; + } + + get isEmpty(): boolean { + return !(this.clients.length) + } + + get hasStarted(): boolean { + return this.runningGame != null + } + + static getByRoomId(id: string, lobbies: Room[]): Room { + for (let l of lobbies) { + if (l.id === id) + return l + } + return null; + } + + static getByClientId(id: string, lobbies: Room[]): Room { + for (let l of lobbies) { + for (let c of l.clients) { + if (c.id === id) + return l + } + } + return null; + } + + static generateCode(elements: number): string { + let code = ''; + let possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'; + while (elements--) { + code += possible.charAt(Math.floor(Math.random() * possible.length)) + } + return code + } + + startGame(): void { + let seed = Math.random() * 10000; + this.toAll('start-game', seed); + this.runGame() + } + + stopGame(client: Client): void { + this.toAll('stop-game', client); + this.runningGame = null + } + + add(client: Client): void { + this.clients.push(client); + + let isPlayer = !this.hasStarted && this.hasValidPlayerCount(); + client.isPlayer = isPlayer; + client.isSpectator = !isPlayer; + client.isReady = false; + client.join(this.id); + + this.toAll('member-joined', client.id, client.name); + this.toAll('client-list', this.clients); + this.toAll('game-settings', this.gameSettings); + + if (this.hasStarted) + this.runningGame.addClient(client) + } + + hasValidPlayerCount(): boolean { + let valid = false; + this.clientCounts.forEach(c => { + if (c === this.clients.length) + valid = true + }); + return valid + } + + runGame(): void { + this.runningGame = new Room.GameClass(this, this.settings); + } + + toAll(event: string, ...args: any[]): void { + this.io.to(this.id).emit(event, serializeObject(this), ...serializeObject(args)) + } + +} \ No newline at end of file diff --git a/server/src/start.ts b/server/src/start.ts new file mode 100644 index 0000000..82293ed --- /dev/null +++ b/server/src/start.ts @@ -0,0 +1,39 @@ +import {ConnectionManager} from "./manager.js"; +import {log} from "./logger.js"; + +import {Server} from 'socket.io'; +import {Room} from "./room.js"; +import * as https from "https"; +import * as fs from "fs"; + +export function StartServer(settings: any){ + + require("dotenv").config(); + const httpsPort = parseInt(process.env.HTTPS_PORT); + + let cert = fs.readFileSync(`${process.env.SSL_PATH}/cert.pem`); + let key = fs.readFileSync(`${process.env.SSL_PATH}/key.pem`); + + let httpsServer = https.createServer({key: key, cert: cert}); + + let sIO = new Server(httpsServer, { + cors: { + origin: ["https://play.benjamin-kraft.local", "https://play.benjamin-kraft.eu"] + } + }); + if (settings.useP2P){ + const p2p = require('socket.io-p2p-server').Server; + sIO.use(p2p); + } + + httpsServer.listen(httpsPort); + + Room.GameClass = settings.gameClass; + + let connectionManager = new ConnectionManager(sIO); + + // On new connection + sIO.on('connection', socket => connectionManager.newSocket(socket)); + + log('startup', null, null, 'Server is listening on port ' + httpsPort); +} \ No newline at end of file diff --git a/server/tsconfig.json b/server/tsconfig.json new file mode 100644 index 0000000..3be4fdb --- /dev/null +++ b/server/tsconfig.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "module": "CommonJS", + "outDir": "./out", + "sourceMap": true, + "alwaysStrict": true + }, + "include": [ + "./src" + ] +} \ No newline at end of file