From ef376178df9ab407a5b5e0ca5c1d7e37960928ed Mon Sep 17 00:00:00 2001 From: A Bass Date: Wed, 20 Apr 2022 14:53:36 -0400 Subject: [PATCH] Working Multiplayer system --- Assets/Sheet.xcf | Bin 101816 -> 102626 bytes TODO.md | 42 +++++++++++++++ index.js | 134 +++++++++++++++++++++++++++-------------------- util.js | 5 ++ worldgen.js | 40 ++++++++++++++ www/index.html | 14 +++-- www/script.js | 81 +++++++++++++++++++--------- www/sheet.png | Bin 0 -> 7422 bytes www/style.css | 33 ++++++++---- 9 files changed, 254 insertions(+), 95 deletions(-) create mode 100644 TODO.md create mode 100644 util.js create mode 100644 worldgen.js create mode 100644 www/sheet.png diff --git a/Assets/Sheet.xcf b/Assets/Sheet.xcf index 2a0f138db419baff9c2161058dfc535e1e1d7917..a2aa2457e6cd8b2c6cb2a807e42be7d37bc90e47 100644 GIT binary patch delta 2345 zcmds$ZAg<*6vvE#M3_l)w0t8|oGf)F2^Q1o()m)DR>tu0gD;Vq z)-{EStO&m)(j8_Y7MXqsln62CMMNYNOe(PtGFaDprujj#91?Wl-rw&3oO|xsIrka3 zEzH~(`sR_iz_AhA06gpqJYokX!~j!bfoZwG^iRO!?}1rrVE$KN(F0(K6<96+&yN7t zR#MD=h{F4X7W__~fx_@N!H17Niq)Z>d~_bki|&yk2@^H`Ow?#FiZ3S*HOmOyVg_wL zGib-jxYo?0XeR0mOw?KMxYkIX>Qp=$V}`hSW{5`w#UJC7Q`hpKG|}lzr<6`Q63c)o zkwCVZ=(_4dM`8;wIfAq#4htxQ#ZN6`@l)$bed=c(l`>J^#6*2J>DHg%(J~VaN~|<^ zp>Pszc*&y$re!LaV`cye$PD3=Q{V=+UZU%U?2M&g%b(rEhf2m~&`%AKW}|H{Qn2{u zU>4u3BRca%39?gBg9v+fkoWq5iWp|7NGItP zEiw;8Z`+-eZ!SGkdA7Quki!Y;E__dr;>9c-eo_@2=qhS85Va4n^icb27bmf!%3lnD zq#N%5+fD*+rUCB;1AC@{k6ix!jj(_22OM$THZ}_T$muu=b3-q|vWc^4a7I=nD!jQ++UlK}A|K`yu(H8OdWUTY zdo&zi_BXu&jAKLbN%vv0@#Ul|F0ZuY)A4Rm#cI?+=!c$GzWToHYD)?#* zS3jQL{YPlPra{YH3{JTlWE=12 TuOFKmg@W|{uWEO7w4&gj?hCql delta 1462 zcmds$T}TvB6oB{4IHM)hfKmQDteT~Q__Ii>Ho5C!xTLMCYtl>Vwz{R}Cc3Ud3mawD zLyOuK3<}YpHL3^Ug42UoSol(*D1}lFwvCwkXYRRYX6}T? z#3y&ei7yaX*Ri7@h&-BxJT`?i%g8b_@`Mvv@d;TOMphRhoj;IG50T9s$hJMm&ezE8 zk)G$?WfND~5Sh!fLoQhp6FEZ#$xGkQ85(JgAru9%Od8XfG@59R@hk`Tm@J%TvhXdb zlN~gnNaCP~CFEL`kUg|cc60EC$>KHgq4)soB00u3>NYI{*onC$DbtJ1l;v+XrLsXi z<1sE7LLN8J-je4coMy$WUbceuCiPoaIp}1vyr0SPaXMUnii1riZ3#@;@@R@}hJ$q` zt0X3?GH86&VGb&Z$&r-Vi3>TuA=%iYj#?uQg8s}bz3TAq1&LL1WwA=GQkw7T=3z>e zapb8x29}t2N9c-skcTUpkLFrNSfXW;hFY3=NdA-X$Xkiso|^6DslGkLtHUBB>U3}! zPnL|eI9;vhIxcp38i^zhk$SgG6mMDzk5jv%7$JzS4uYdv{Fwam>I12GSFg5Lf)f~% z5#(Jj@_rWbk+yf}D~zWakTVwKON4Gw`%ug6D8d=Yui9RY+{QTY)6OmcJ|d$RkkSKW zLIYB7LuTMl0lDunJ~E9o`H@x$dGb5579R-eOEGRrMz$?6#)0nw3cnS3O-HkQ8-g+) z3rj#EeKn+g_Jxvr2v!9;aeV>k=;l6{%!UPlMtm~d>MkJCKrgiqZt(vyXC)LCKvdu$ zr`?oSYJpgIH0kmbC5C8xG diff --git a/TODO.md b/TODO.md new file mode 100644 index 0000000..f22a8c0 --- /dev/null +++ b/TODO.md @@ -0,0 +1,42 @@ +# Project To Do list + +## Short Term + - [] Kill socket when browser closes/inactive. + - [] Make client get textures from single sprite sheet + +## Mid Term + - [] Implement Rooms with different games on server + - [] Add room selection menu on client + - [] Implement Players + - [] Player Names + - [] Player Colors + - [] Prevent Duplicate player info +## Long Term + - [] Power and money system for players + - [] Add land ownership + - [] Land Claiming + - [] Difficulty to claim hills and seas + - [] Create bouy flag sprite + - [] Make differentiation between land owned by different players + - [] System to have animated widgets overtop of the canvas. + - [] Explosion + - [] Selection cursor + - [] Color for different land status (claimed/open/unclaimable) + - [] Create spites for classes of buildings for economy. + - [x] Factory + - [x] Farmland/town + - [] Fishing area + - [] Building selection menu +## Longer Term + + - [] Add tests to ensure features act as they should + - [] Handle player death/leaving + - [] Dead player's resources go to killer with some tax. + - [] Leaving player's resources go to player +## Finishing Touches + - [] Sounds + - [] Graphics Redo + - [] Fancy webpage decorations + +## Maybe + - [] Chat diff --git a/index.js b/index.js index bba5be2..f55f727 100644 --- a/index.js +++ b/index.js @@ -1,9 +1,61 @@ -var noise = require('noisejs'); -var express = require('express'); +var express = require('express'); +const worldgen = require("./worldgen.js") +const util = require("./util.js") const gridSize = [18, 18]; const perlinScale = 3; -var world = generateWorld(); + +class Player { + constructor(id, color) { + this.id = id; + this.color = color; + } +} + +class Game { + constructor() { + this.players = []; + this.world = worldgen.generateWorld(gridSize, perlinScale); + } + + addPlayer(id) { + var color + switch(util.randomNumber(1,4)) { + case 1: + color = "red"; + break; + + case 2: + color = "aquamarine"; + break; + + case 3: + color = "green"; + break; + + case 4: + color = "yellow"; + break; + } + const player = new Player(id, color); + this.players.push(player); + } + + removePlayer(id) { + this.players = this.players.filter(obj => obj.id != id); + console.log("removed player - " + id) + } + + getPlayerByID(id) { + console.log(this.players) + console.log(id) + const player = this.players.filter(obj => obj.id == id)[0] + return player + } + +} +var game = new Game(); + var app = express() //Static resources server app.use(express.static(__dirname + '/www/'));var server = app.listen(8082, function () { @@ -14,66 +66,36 @@ app.use(express.static(__dirname + '/www/'));var server = app.listen(8082, funct var io = require('socket.io')(server);/* Connection events */ io.on('connection', function(client) { console.log('User connected'); - client.on('joinGame', function(tank){ - console.log(client.id + ' joined the game'); - // client.emit('addTank', { id: tank.id, type: tank.type, isLocal: true, x: initX, y: initY, hp: TANK_INIT_HP }); - // client.broadcast.emit('addTank', { id: tank.id, type: tank.type, isLocal: false, x: initX, y: initY, hp: TANK_INIT_HP} ); - // game.addTank({ id: tank.id, type: tank.type, hp: TANK_INIT_HP}); - client.emit('gameVars', {gridSize: gridSize, world: world}) + client.on('disconnect', function(){ + console.log(client.id + ' disconnected.') + game.removePlayer(client.id) + client.broadcast.emit('playerList', game.players) + }) + + client.on('joinGame', function(tank){ + game.addPlayer(client.id); + console.log(client.id + ' joined the game'); + client.emit('gameVars', {gridSize: gridSize, world: game.world}) + client.broadcast.emit('playerList', game.players) + client.emit('playerList', game.players) + }) + client.on('leaveGame', function(tank){ + console.log(client.id + ' disconnected.') + game.removePlayer(client.id) + client.broadcast.emit('playerList', game.players) }) client.on('clickCanvas', function(data){ const xu = data.tilePosition[0] const yu = data.tilePosition[1] - world[xu][yu].structure = data.structure - client.broadcast.emit('sync',{world: world}) - client.emit('sync',{world: world}) + game.world[xu][yu].structure = data.structure + game.world[xu][yu].owner = game.getPlayerByID(client.id).color; + // console.log(world[xu][yu].owner = game.getPlayerbyID(client.id)) + + client.broadcast.emit('sync',{world: game.world}) + client.emit('sync',{world: game.world}) }) }); - - - - - function randomNumber(min, max) { - return Math.floor(Math.random() * (max - min) + min); - } - - function generateWorld(){ - let x, y = 0 - - let tempWorld = Array.from(Array(gridSize[0]), () => new Array(gridSize[1])); - for (x = 0; x < gridSize[0]; x++){ - for (y = 0; y < gridSize[1]; y++){ - tempWorld[x][y] = {type: 0, structure: 0} - } - } - - noise.seed(Math.random()) - for(x = 0; x < gridSize[0]; x++){ - for(y = 0; y < gridSize[1]; y++){ - - var value = (noise.perlin2(x/perlinScale, y/perlinScale))*10; - if (value >= -3) { - tempWorld[x][y].type = 0 - } else if (value < -3) { - tempWorld[x][y].type = 1 - } - if (value > 1.4) { - tempWorld[x][y].type = 2 - } - - } - } - for (i = 0; i < gridSize[0]*gridSize[1]/15; i){ - x = randomNumber(0,gridSize[0]); - y = randomNumber(0,gridSize[1]); - if (tempWorld[x][y].type != 1) { - i++; - tempWorld[x][y].structure = 1 - } - } - return tempWorld; - } diff --git a/util.js b/util.js new file mode 100644 index 0000000..b3f9e1a --- /dev/null +++ b/util.js @@ -0,0 +1,5 @@ +function randomNumber(min, max) { + return Math.floor(Math.random() * (max - min) + min); +} + +module.exports = {randomNumber} diff --git a/worldgen.js b/worldgen.js new file mode 100644 index 0000000..e41ef1e --- /dev/null +++ b/worldgen.js @@ -0,0 +1,40 @@ +var noise = require('noisejs'); +const util = require("./util.js") + +function generateWorld(gridSize, perlinScale){ + let x, y = 0 + + let tempWorld = Array.from(Array(gridSize[0]), () => new Array(gridSize[1])); + for (x = 0; x < gridSize[0]; x++){ + for (y = 0; y < gridSize[1]; y++){ + tempWorld[x][y] = {type: 0, structure: 0, owner: null} + } + } + + noise.seed(Math.random()) + for(x = 0; x < gridSize[0]; x++){ + for(y = 0; y < gridSize[1]; y++){ + + var value = (noise.perlin2(x/perlinScale, y/perlinScale))*10; + if (value >= -3) { + tempWorld[x][y].type = 0 + } else if (value < -3) { + tempWorld[x][y].type = 1 + } + if (value > 1.4) { + tempWorld[x][y].type = 2 + } + + } + } + for (i = 0; i < gridSize[0]*gridSize[1]/15; i){ + x = util.randomNumber(0,gridSize[0]); + y = util.randomNumber(0,gridSize[1]); + if (tempWorld[x][y].type != 1) { + i++; + tempWorld[x][y].structure = 1 + } + } + return tempWorld; +} +module.exports = { generateWorld }; diff --git a/www/index.html b/www/index.html index f74ec17..7bf2e98 100644 --- a/www/index.html +++ b/www/index.html @@ -9,12 +9,18 @@

Uber Secret Project

+
- -
-

Info


-

$0

+
+ +
+ +
+ +
+

Info


+

$0

diff --git a/www/script.js b/www/script.js index 5e0a097..80a780b 100644 --- a/www/script.js +++ b/www/script.js @@ -4,24 +4,35 @@ var gridSize; var cash = 99; var world var canvas = document.getElementById('canvas'); +var hud = document.getElementById('hud'); var ctx = canvas.getContext('2d'); +var hctx = hud.getContext('2d'); var socket = io.connect('http://127.0.0.1:8082/'); -function joinGame(tankName, tankType, socket){ - if(tankName != ''){ - socket.emit('joinGame', {name: tankName, type: tankType}); - } +function joinGame(socket){ + socket.emit('joinGame', {}); +} +function leaveGame(socket){ + socket.emit('leaveGame', {}); } -joinGame("aaa", "blue", socket); +joinGame(socket); + +window.onbeforeunload = function(){ + socket.disconnect(); + world = [] +} socket.on('gameVars', function(tank){ gridSize = tank.gridSize; world = tank.world; - console.log(gridSize, world) - console.log(tank) + fill_canvas() }); +socket.on('playerList', function(data){ + console.log(data) +}); + socket.on('sync', function (sync){ world = sync.world; render() @@ -49,13 +60,14 @@ window.onload = function () { function fill_canvas() { // CREATE CANVAS CONTEXT. - canvas.addEventListener("mousedown", function(e) + hud.addEventListener("mousedown", function(e) { getMousePosition(canvas, e); }); canvas.width = tileSize*gridSize[0]; canvas.height = tileSize*gridSize[1]; - + hud.width = canvas.width + hud.height = canvas.height render() @@ -74,21 +86,42 @@ function render() { // DRAW THE IMAGE TO THE CANVAS. for(y = 0; y < gridSize[1]; y++){ const xu = x*tileSize; const yu = y*tileSize; - if (world[x][y].type == 0) { - ctx.drawImage(tiles[0], xu,yu) - } else if (world[x][y].type == 1) { - ctx.drawImage(tiles[2], xu,yu) - } - if (world[x][y].type == 2) { - ctx.drawImage(tiles[1], xu,yu) - } - if (world[x][y].structure == 1) { - ctx.drawImage(tiles[3], xu,yu) - } - if (world[x][y].structure == 2) { - ctx.drawImage(tiles[4], xu,yu) - } + // Draw buildings + switch (world[x][y].type) { + case (0): + ctx.drawImage(tiles[0], xu,yu) + break; + case (1): + ctx.drawImage(tiles[2], xu,yu) + break; + case (2): + ctx.drawImage(tiles[1], xu,yu) + break; + } + + // Draw Structures + switch (world[x][y].structure) { + case (1): + ctx.drawImage(tiles[3], xu,yu) + break; + + case (2): + ctx.drawImage(tiles[4], xu,yu) + break; + + } + + // Draw Property overlays + if (world[x][y].owner != null){ + ctx.beginPath(); + ctx.fillStyle = world[x][y].owner; + + ctx.rect(xu, yu, tileSize, tileSize) + ctx.globalAlpha = 0.5; + ctx.fill() + ctx.globalAlpha = 1; + } } } @@ -109,7 +142,7 @@ function getMousePosition(canvas, event) { console.log("Click!") - + console.log(xu,yu) socket.emit('clickCanvas', {tilePosition: [xu,yu], structure: 2}); // if (tile.type != 1 && tile.structure != 1 && cash > 10) { // tile.structure = 2 diff --git a/www/sheet.png b/www/sheet.png new file mode 100644 index 0000000000000000000000000000000000000000..36a07914ef8ba5e9788380a08006242906bb24f3 GIT binary patch literal 7422 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 R3CJ%1cFXu?=?%xoe*