diff --git a/common/npc.js b/common/npc.js new file mode 100644 index 0000000..5a80e43 --- /dev/null +++ b/common/npc.js @@ -0,0 +1,30 @@ +import { distF, uuidv4 } from './util.js' + +class NPC { + constructor(you) { + let pos = { x: Math.random() * 1000 - 50, y: Math.random() * 1000 - 50 }; + let vel = { x: 0, y: 0 }; + + this.pos = pos; + this.vel = vel; + + this.you = you || uuidv4(); + + this.type = 'NPC'; + + this.immortal = true; + } + handleTick(game) { + let { entities } = game; + + let rEntity = entities[Math.floor(Math.random() * entities.length)]; + + this.vel.x += (rEntity.pos.x - this.pos.x) * 0.0001; + this.vel.y += (rEntity.pos.y - this.pos.y) * 0.0001; + + this.pos.x += this.vel.x; + this.pos.y += this.vel.y; + } +} + +export default NPC; \ No newline at end of file diff --git a/common/player.js b/common/player.js index cc589de..5dce097 100644 --- a/common/player.js +++ b/common/player.js @@ -1,93 +1,94 @@ -import crypto from "../crypto.js"; +import { distF, uuidv4 } from './util.js' -function distF(ent, target) { - return ((ent.pos.x - target.pos.x) ** 2) + ((ent.pos.y - target.pos.y) ** 2) -} +class Player { + constructor(you, isPlayer) { + let pos = { x: Math.random() * 1000 - 50, y: Math.random() * 1000 - 50 }; + let camera = { x: -pos.x, y: -pos.y }; + let vel = { x: 0, y: 0 }; -function uuidv4() { - return "#000000-000000".replace(/[018]/g, c => - (+c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> +c / 4).toString(16) - ); -} + this.camera = camera; + this.pos = pos; + this.vel = vel; + this.rot = 0; + this.dir = 1; + this.ticks = 0; -function Player(you, isPlayer) { - let pos = { x: Math.random() * 1000 - 50, y: Math.random() * 1000 - 50 }; - let camera = { x: -pos.x, y: -pos.y }; - let vel = { x: 0, y: 0 }; + this.health = 100; - this.camera = camera; - this.pos = pos; - this.vel = vel; - this.rot = 0; - this.dir = 1; - this.ticks = 0; + this.you = you || uuidv4(); - this.health = 100; + this.isPlayer = isPlayer; - this.you = you || uuidv4(); + this.headCount = 0; - this.isPlayer = isPlayer; + this.type = 'Player'; - this.headCount = 0; -} + this.isMenu = false; -Player.prototype.bump = function () { - let player = this; - - if (player.ticks < 7) { - player.dir *= -1; + this.r = 1; } + bump() { + let player = this; - console.log(player.ticks) + if (player.ticks < 7) { + player.dir *= -1; + } - player.vel.x *= 0.3; - player.vel.y *= 0.3; + console.log(player.ticks) - player.vel.x += Math.sin(player.rot) * 12; - player.vel.y -= Math.cos(player.rot) * 12; + player.vel.x *= 0.3; + player.vel.y *= 0.3; - player.ticks = 0; -} + player.vel.x += Math.sin(player.rot) * 12; + player.vel.y -= Math.cos(player.rot) * 12; -Player.prototype.handleTick = function (game) { - let { entities, width, height } = game; + player.ticks = 0; + } + handleTick(game) { + let { entities, width, height } = game; - let ent = this; + let ent = this; - if (ent.health <= 0) return; + if (ent.health <= 0) return; - ent.pos.x += ent.vel.x; - ent.pos.y += ent.vel.y; + ent.pos.x += ent.vel.x; + ent.pos.y += ent.vel.y; - ent.pos.x = Math.max(Math.min(ent.pos.x, width / 2), - width / 2); - ent.pos.y = Math.max(Math.min(ent.pos.y, height / 2), - height / 2); + ent.pos.x = Math.max(Math.min(ent.pos.x, width / 2), - width / 2); + ent.pos.y = Math.max(Math.min(ent.pos.y, height / 2), - height / 2); - ent.vel.x *= 0.9; - ent.vel.y *= 0.9; + ent.vel.x *= 0.9; + ent.vel.y *= 0.9; - ent.rot += 0.03 * ent.dir; - ent.rot = ent.rot % (Math.PI * 10); + ent.rot += 0.03 * ent.dir; + ent.rot = ent.rot % (Math.PI * 10); - ent.camera.x = -ent.pos.x * 0.1 + ent.camera.x * 0.9; - ent.camera.y = -ent.pos.y * 0.1 + ent.camera.y * 0.9; + ent.camera.x = -ent.pos.x * 0.1 + ent.camera.x * 0.9; + ent.camera.y = -ent.pos.y * 0.1 + ent.camera.y * 0.9; - ent.ticks++; + ent.ticks++; - for (let target of entities) { - if (target.you == ent.you) continue; + for (let target of entities) { + if (target.you == ent.you) continue; - let dist = distF(ent, target); + let dist = distF(ent, target); - let dp = (Math.sin(ent.rot) * (ent.pos.x - target.pos.x)) - - (Math.cos(ent.rot) * (ent.pos.y - target.pos.y)); + let dp = (Math.sin(ent.rot) * (ent.pos.x - target.pos.x)) + - (Math.cos(ent.rot) * (ent.pos.y - target.pos.y)); - dp /= Math.sqrt(dist + 0.1); + dp /= Math.sqrt(dist + 0.1); - if (Math.sqrt(dist) < 128 && 1 / dp < -0.2) { - target.health--; + if (Math.sqrt(dist) < 128 && 1 / dp < -0.2) { + if (target.type == 'NPC') { + ent.isMenu = true; + } + if (target.immortal) continue; - if (target.health == 0) { - ent.headCount++; + target.health--; + + if (target.health == 0) { + ent.headCount++; + } } } } diff --git a/common/util.js b/common/util.js new file mode 100644 index 0000000..268728d --- /dev/null +++ b/common/util.js @@ -0,0 +1,16 @@ +import crypto from "../crypto.js"; + +function distF(ent, target) { + return ((ent.pos.x - target.pos.x) ** 2) + ((ent.pos.y - target.pos.y) ** 2) +} + +function uuidv4() { + return "#000000-000000".replace(/[018]/g, c => + (+c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> +c / 4).toString(16) + ); +} + +export { + distF, + uuidv4 +} \ No newline at end of file diff --git a/game.js b/game.js index 099ef35..fb52290 100644 --- a/game.js +++ b/game.js @@ -1,4 +1,5 @@ import GameBasic from "./common/game_basic.js"; +import NPC from "./common/npc.js"; class Game extends GameBasic { constructor() { @@ -9,8 +10,8 @@ class Game extends GameBasic { let { entities } = this; let entList = []; for (let entity of entities) { - let { pos, vel, rot, dir, health, headCount, you, camera, ticks } = entity; - entList.push({ pos, vel, rot, dir, health, headCount, you, camera, ticks }); + let { pos, vel, rot, dir, health, headCount, you, camera, ticks, type, isMenu, r, playing } = entity; + entList.push({ pos, vel, rot, dir, health, headCount, you, camera, ticks, type, isMenu, r, playing }); } if (entList.length == 0) return; @@ -19,6 +20,10 @@ class Game extends GameBasic { if (!client.active) continue; client.send(JSON.stringify(entList)); } + + for (let entity of entities) { + entity.r = 1; + } } init() { super.init(); @@ -26,6 +31,10 @@ class Game extends GameBasic { let that = this; that.entities = []; + for (let i = 0; i < 10; i++) { + that.entities.push(new NPC()) + } + setInterval(function () { that.sync() }, 1000 / 5); } } diff --git a/index.js b/index.js index 47ecff8..11df49e 100644 --- a/index.js +++ b/index.js @@ -21,9 +21,9 @@ app.ws('/', function (ws, req) { game.entities[playerI] = player; ws.active = true; - - console.log(`A player ${player.you} joined under IP ${req.headers["x-real-ip"]}`) + // This will only work under NGINX. + console.log(`A player ${player.you} joined under IP ${req.headers["x-real-ip"]}`) ws.on('message', function message(msg) { let data = {}; @@ -33,8 +33,8 @@ app.ws('/', function (ws, req) { console.log(err); data = {}; } - let { vel, dir, you, ticks } = data; - let data2 = { vel, dir, you, ticks }; + let { vel, dir, you, ticks, isMenu, r, playing } = data; + let data2 = { vel, dir, you, ticks, isMenu, r, playing }; let you2 = game.entities[playerI].you; diff --git a/static/assets/Elec Piano Loop.wav b/static/assets/Elec Piano Loop.wav new file mode 100644 index 0000000..e0eeec5 Binary files /dev/null and b/static/assets/Elec Piano Loop.wav differ diff --git a/static/assets/No.wav b/static/assets/No.wav new file mode 100644 index 0000000..cdc862b Binary files /dev/null and b/static/assets/No.wav differ diff --git a/static/assets/Troll.wav b/static/assets/Troll.wav new file mode 100644 index 0000000..3129ddd Binary files /dev/null and b/static/assets/Troll.wav differ diff --git a/static/assets/Yes.wav b/static/assets/Yes.wav new file mode 100644 index 0000000..cf77e9a Binary files /dev/null and b/static/assets/Yes.wav differ diff --git a/static/assets/npc.svg b/static/assets/npc.svg new file mode 100644 index 0000000..70fc43d --- /dev/null +++ b/static/assets/npc.svg @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + diff --git a/static/js/index.js b/static/js/index.js index afbe7fa..cce4539 100644 --- a/static/js/index.js +++ b/static/js/index.js @@ -1,13 +1,26 @@ import Player from "./player.js"; import GameBasic from "./game_basic.js"; +import NPC from "./npc.js"; const cs = 1024; const assets = [ 'assets/player.svg', 'assets/head.svg', - 'assets/map.svg' + 'assets/map.svg', + 'assets/npc.svg' ]; -const origin = 'https://ub.zenoverse.net/' +const legalTypes = { + Player, + NPC +} +const options = [ + 'Troll', + 'Exit', + 'Elec Piano Loop', + 'No', + 'Yes' +] +const origin = (window.location.href.indexOf('localhost') != -1) ? window.location.href : 'https://ub.zenoverse.net/' class Game extends GameBasic { constructor() { @@ -42,20 +55,33 @@ class Game extends GameBasic { for (let ent of entities) { if (ent.health <= 0) continue; - ctx.save(); + if (ent.type == 'Player') { + ctx.save(); - ctx.translate(ent.pos.x, ent.pos.y); - ctx.rotate(ent.rot); - ctx.drawImage(assetsIn[1], -64 / 2, -128, 64, 128); + ctx.translate(ent.pos.x, ent.pos.y); + ctx.rotate(ent.rot); + ctx.drawImage(assetsIn[1], -64 / 2, -128, 64, 128); - ctx.restore(); + ctx.restore(); - ctx.fillStyle = ent.you.split('-')[0]; - - ctx.beginPath(); - ctx.arc(ent.pos.x, ent.pos.y, 32,0,2*Math.PI); - ctx.fill(); - ctx.drawImage(assetsIn[0], ent.pos.x - 64 / 2, ent.pos.y - 64 / 2, 64, 64); + if (ent.playing) { + ctx.strokeStyle = 'white'; + ctx.lineWidth = "20"; + ctx.beginPath(); + ctx.arc(ent.pos.x, ent.pos.y, 32, 0, 2 * Math.PI); + ctx.stroke(); + } + + ctx.fillStyle = ent.you.split('-')[0]; + + ctx.beginPath(); + ctx.arc(ent.pos.x, ent.pos.y, 32, 0, 2 * Math.PI); + ctx.fill(); + } + + ctx.drawImage((ent.type == 'NPC') ? assetsIn[3] : assetsIn[0], ent.pos.x - 64 / 2, ent.pos.y - 64 / 2, 64, 64); + + if (ent.type != 'Player') continue; ctx.strokeStyle = "rgb(255,255,255)"; ctx.lineWidth = "8"; @@ -71,14 +97,20 @@ class Game extends GameBasic { ctx.restore(); - if (player.health <= 0) { + if (player.health <= 0 || player.isMenu) { ctx.fillStyle = 'rgba(0,0,0,0.5)'; ctx.fillRect(0, 0, cs, cs); ctx.fillStyle = 'rgb(255,255,255)'; ctx.textAlign = "center"; ctx.textBaseline = "middle"; ctx.font = "bold 48px sans-serif"; - ctx.fillText('You died! Click to respawn', cs / 2, cs / 2); + if (player.health <= 0) ctx.fillText('You died! Click to respawn', cs / 2, cs / 2); + if (player.isMenu) { + let r = Math.floor(Math.abs(player.rot / 1.2) % options.length); + ctx.fillText(`Click to react ${options[r]}`, cs / 2, cs / 2) + ctx.font = "bold 16px sans-serif"; + ctx.fillText(`Wait for options ${options.join(', ')}`, cs / 2, cs / 2 + 50) + } } } click() { @@ -88,6 +120,10 @@ class Game extends GameBasic { this.ws = new WebSocket(origin); this.player = new Player(false, true); this.entities.push(this.player); + } else if (player.isMenu) { + player.r = Math.floor(Math.abs(player.rot / 1.2) % options.length); + player.isMenu = false; + this.sync(); } else { player.bump(); this.sync(); @@ -96,17 +132,34 @@ class Game extends GameBasic { sync() { let { player } = this; - let { vel, dir, you, ticks } = player; + let { vel, dir, you, ticks, isMenu, r, playing } = player; - this.ws.send(JSON.stringify({ vel, dir, you, ticks })); + this.ws.send(JSON.stringify({ vel, dir, you, ticks, isMenu, r, playing })); } recv({ data }) { let { player } = this; let you = player.you; + let that = this; + let entList = JSON.parse(data); entList = entList.map(x => { - x.handleTick = Player.prototype.handleTick; + let type = (Object.keys(legalTypes).indexOf(x.type) == -1) ? Player : legalTypes[x.type]; + x.handleTick = type.prototype.handleTick; + if (x.r != 1) { + let a = new Audio(`assets/${options[x.r]}.wav`); + a.addEventListener('ended', () => { + if (x.you == you) { + that.player.playing = false; + that.sync(); + } + a.remove(); + }) + if (you != x.you) { + x.r = 1; + } + a.play(); + } return x; }) @@ -114,6 +167,12 @@ class Game extends GameBasic { this.player = Object.assign(this.player || new Player(false, false), matchingPlayer[0]); + if (this.player.r != 1) { + this.player.playing = true; + this.player.r = 1; + that.sync(); + } + this.entities = entList; if (this.entities.length == 0) this.entities = [this.player];