From 46a48e7c15f55d7ed744103a17814390eb944927 Mon Sep 17 00:00:00 2001 From: schmelczerandras Date: Mon, 5 Oct 2020 19:07:17 +0200 Subject: [PATCH] Add basic multiplayer --- backend/package.json | 13 +- backend/src/main.ts | 38 +++-- .../src/map}/create-dungeon.ts | 24 ++- backend/src/objects/character-physics.ts | 31 ++++ .../src/objects/circle-physics.ts | 35 ++--- backend/src/objects/lamp-physics.ts | 36 +++++ backend/src/objects/tunnel-physics.ts | 48 ++++++ .../src}/physics/bounds/bounding-box-base.ts | 4 +- .../src}/physics/bounds/bounding-box.ts | 0 .../physics/bounds/immutable-bounding-box.ts | 0 .../physics/containers/bounding-box-list.ts | 0 .../physics/containers/bounding-box-tree.ts | 0 .../physics/physical-game-object-container.ts | 86 +++++++++++ backend/src/physics/physical-game-object.ts | 10 ++ backend/src/players/player-container.ts | 13 -- backend/src/players/player.ts | 120 ++++++++++++++- backend/src/serialize.ts | 2 + backend/tsconfig.json | 11 +- backend/webpack.config.js | 6 +- frontend/package.json | 3 +- frontend/src/index.ts | 18 +-- .../scripts/commands/command-broadcaster.ts | 11 -- .../src/scripts/commands/command-receiver.ts | 5 - frontend/src/scripts/commands/command.ts | 6 - .../commands/generators/keyboard-listener.ts | 42 ++++++ .../commands/generators/mouse-listener.ts | 36 +++++ .../commands/generators/touch-listener.ts | 57 +++++++ frontend/src/scripts/commands/move-to.ts | 12 -- .../receivers/command-receiver-socket.ts | 11 ++ frontend/src/scripts/commands/render.ts | 12 -- frontend/src/scripts/commands/step.ts | 11 -- frontend/src/scripts/commands/teleport-to.ts | 12 -- .../src/scripts/commands/types/move-to.ts | 10 ++ frontend/src/scripts/commands/types/render.ts | 10 ++ frontend/src/scripts/commands/types/step.ts | 9 ++ frontend/src/scripts/game.ts | 142 +++++++++--------- frontend/src/scripts/i-game.ts | 11 -- frontend/src/scripts/identity/identity.ts | 1 - .../input/commands/cursor-move-command.ts | 12 -- .../src/scripts/input/commands/key-down.ts | 11 -- frontend/src/scripts/input/commands/key-up.ts | 11 -- .../scripts/input/commands/primary-action.ts | 12 -- .../input/commands/secondary-action.ts | 12 -- frontend/src/scripts/input/commands/swipe.ts | 12 -- frontend/src/scripts/input/commands/zoom.ts | 11 -- .../src/scripts/input/keyboard-listener.ts | 19 --- frontend/src/scripts/input/mouse-listener.ts | 63 -------- frontend/src/scripts/input/touch-listener.ts | 43 ------ frontend/src/scripts/objects/camera.ts | 36 +++++ .../src/scripts/objects/character-view.ts | 19 +++ .../scripts/objects/game-object-container.ts | 63 ++++++++ frontend/src/scripts/objects/game-object.ts | 31 ---- frontend/src/scripts/objects/lamp-view.ts | 17 +++ frontend/src/scripts/objects/objects.ts | 58 ------- frontend/src/scripts/objects/tunnel-view.ts | 17 +++ frontend/src/scripts/objects/types/camera.ts | 56 ------- .../src/scripts/objects/types/character.ts | 17 +-- frontend/src/scripts/objects/types/lamp.ts | 45 ------ .../src/scripts/objects/types/projectile.ts | 9 +- frontend/src/scripts/objects/types/tunnel.ts | 54 ------- .../scripts/physics/bounds/physics-circle.ts | 7 +- .../physics/dynamic-physical-object.ts | 10 -- .../src/scripts/physics/physical-object.ts | 7 +- frontend/src/scripts/physics/physics.ts | 3 +- .../scripts/physics/static-physical-object.ts | 7 - frontend/src/scripts/shapes/blob-shape.ts | 22 ++- frontend/src/scripts/transport/deserialize.ts | 28 ++++ shared/.eslintignore | 1 + shared/.eslintrc.json | 29 ++++ shared/.gitignore | 4 + shared/.prettierrc | 7 + shared/firebase.json | 16 ++ shared/package.json | 17 +++ shared/src/commands/broadcast-commands.ts | 7 + .../src}/commands/command-generator.ts | 2 +- shared/src/commands/command-receiver.ts | 25 +++ shared/src/commands/command.ts | 7 + shared/src/commands/types/create-objects.ts | 13 ++ shared/src/commands/types/create-player.ts | 13 ++ shared/src/commands/types/delete-objects.ts | 14 ++ shared/src/commands/types/move-action.ts | 14 ++ shared/src/commands/types/primary-action.ts | 14 ++ shared/src/commands/types/secondary-action.ts | 14 ++ .../commands/types/set-view-area-action.ts | 14 ++ shared/src/commands/types/ternary-action.ts | 14 ++ shared/src/commands/types/update-objects.ts | 13 ++ shared/src/helper/array.ts | 31 ++++ shared/src/helper/circle.ts | 5 + shared/src/helper/clamp.ts | 4 + shared/src/helper/delta-time-calculator.ts | 23 +++ shared/src/helper/last.ts | 3 + shared/src/helper/mix.ts | 1 + shared/src/helper/pretty-print.ts | 4 + shared/src/helper/random.ts | 18 +++ shared/src/helper/rectangle.ts | 5 + shared/src/helper/rgb.ts | 3 + shared/src/helper/rgb255.ts | 4 + shared/src/helper/rotate-90-deg.ts | 3 + shared/src/helper/to-percent.ts | 1 + shared/src/helper/unique.ts | 5 + shared/src/helper/wait.ts | 3 + shared/src/main.ts | 29 ++++ shared/src/objects/deserialize.ts | 26 ++++ shared/src/objects/game-object.ts | 12 ++ shared/src/objects/types/character-base.ts | 17 +++ shared/src/objects/types/lamp-base.ts | 11 ++ shared/src/objects/types/tunnel-base.ts | 17 +++ shared/src/settings.ts | 13 ++ .../src/transport}/identity-manager.ts | 0 shared/src/transport/identity.ts | 1 + shared/src/transport/transport-events.ts | 5 +- shared/src/transport/typed.ts | 11 ++ shared/tsconfig.json | 15 ++ 113 files changed, 1362 insertions(+), 754 deletions(-) rename {frontend/src/scripts/objects/world => backend/src/map}/create-dungeon.ts (66%) create mode 100644 backend/src/objects/character-physics.ts rename frontend/src/scripts/physics/bounds/bounding-circle.ts => backend/src/objects/circle-physics.ts (71%) create mode 100644 backend/src/objects/lamp-physics.ts create mode 100644 backend/src/objects/tunnel-physics.ts rename {frontend/src/scripts => backend/src}/physics/bounds/bounding-box-base.ts (91%) rename {frontend/src/scripts => backend/src}/physics/bounds/bounding-box.ts (100%) rename {frontend/src/scripts => backend/src}/physics/bounds/immutable-bounding-box.ts (100%) rename {frontend/src/scripts => backend/src}/physics/containers/bounding-box-list.ts (100%) rename {frontend/src/scripts => backend/src}/physics/containers/bounding-box-tree.ts (100%) create mode 100644 backend/src/physics/physical-game-object-container.ts create mode 100644 backend/src/physics/physical-game-object.ts delete mode 100644 backend/src/players/player-container.ts create mode 100644 backend/src/serialize.ts delete mode 100644 frontend/src/scripts/commands/command-broadcaster.ts delete mode 100644 frontend/src/scripts/commands/command-receiver.ts delete mode 100644 frontend/src/scripts/commands/command.ts create mode 100644 frontend/src/scripts/commands/generators/keyboard-listener.ts create mode 100644 frontend/src/scripts/commands/generators/mouse-listener.ts create mode 100644 frontend/src/scripts/commands/generators/touch-listener.ts delete mode 100644 frontend/src/scripts/commands/move-to.ts create mode 100644 frontend/src/scripts/commands/receivers/command-receiver-socket.ts delete mode 100644 frontend/src/scripts/commands/render.ts delete mode 100644 frontend/src/scripts/commands/step.ts delete mode 100644 frontend/src/scripts/commands/teleport-to.ts create mode 100644 frontend/src/scripts/commands/types/move-to.ts create mode 100644 frontend/src/scripts/commands/types/render.ts create mode 100644 frontend/src/scripts/commands/types/step.ts delete mode 100644 frontend/src/scripts/i-game.ts delete mode 100644 frontend/src/scripts/identity/identity.ts delete mode 100644 frontend/src/scripts/input/commands/cursor-move-command.ts delete mode 100644 frontend/src/scripts/input/commands/key-down.ts delete mode 100644 frontend/src/scripts/input/commands/key-up.ts delete mode 100644 frontend/src/scripts/input/commands/primary-action.ts delete mode 100644 frontend/src/scripts/input/commands/secondary-action.ts delete mode 100644 frontend/src/scripts/input/commands/swipe.ts delete mode 100644 frontend/src/scripts/input/commands/zoom.ts delete mode 100644 frontend/src/scripts/input/keyboard-listener.ts delete mode 100644 frontend/src/scripts/input/mouse-listener.ts delete mode 100644 frontend/src/scripts/input/touch-listener.ts create mode 100644 frontend/src/scripts/objects/camera.ts create mode 100644 frontend/src/scripts/objects/character-view.ts create mode 100644 frontend/src/scripts/objects/game-object-container.ts delete mode 100644 frontend/src/scripts/objects/game-object.ts create mode 100644 frontend/src/scripts/objects/lamp-view.ts delete mode 100644 frontend/src/scripts/objects/objects.ts create mode 100644 frontend/src/scripts/objects/tunnel-view.ts delete mode 100644 frontend/src/scripts/objects/types/camera.ts delete mode 100644 frontend/src/scripts/objects/types/lamp.ts delete mode 100644 frontend/src/scripts/objects/types/tunnel.ts delete mode 100644 frontend/src/scripts/physics/dynamic-physical-object.ts delete mode 100644 frontend/src/scripts/physics/static-physical-object.ts create mode 100644 frontend/src/scripts/transport/deserialize.ts create mode 100644 shared/.eslintignore create mode 100644 shared/.eslintrc.json create mode 100644 shared/.gitignore create mode 100644 shared/.prettierrc create mode 100644 shared/firebase.json create mode 100644 shared/package.json create mode 100644 shared/src/commands/broadcast-commands.ts rename {frontend/src/scripts => shared/src}/commands/command-generator.ts (84%) create mode 100644 shared/src/commands/command-receiver.ts create mode 100644 shared/src/commands/command.ts create mode 100644 shared/src/commands/types/create-objects.ts create mode 100644 shared/src/commands/types/create-player.ts create mode 100644 shared/src/commands/types/delete-objects.ts create mode 100644 shared/src/commands/types/move-action.ts create mode 100644 shared/src/commands/types/primary-action.ts create mode 100644 shared/src/commands/types/secondary-action.ts create mode 100644 shared/src/commands/types/set-view-area-action.ts create mode 100644 shared/src/commands/types/ternary-action.ts create mode 100644 shared/src/commands/types/update-objects.ts create mode 100644 shared/src/helper/array.ts create mode 100644 shared/src/helper/circle.ts create mode 100644 shared/src/helper/clamp.ts create mode 100644 shared/src/helper/delta-time-calculator.ts create mode 100644 shared/src/helper/last.ts create mode 100644 shared/src/helper/mix.ts create mode 100644 shared/src/helper/pretty-print.ts create mode 100644 shared/src/helper/random.ts create mode 100644 shared/src/helper/rectangle.ts create mode 100644 shared/src/helper/rgb.ts create mode 100644 shared/src/helper/rgb255.ts create mode 100644 shared/src/helper/rotate-90-deg.ts create mode 100644 shared/src/helper/to-percent.ts create mode 100644 shared/src/helper/unique.ts create mode 100644 shared/src/helper/wait.ts create mode 100644 shared/src/main.ts create mode 100644 shared/src/objects/deserialize.ts create mode 100644 shared/src/objects/game-object.ts create mode 100644 shared/src/objects/types/character-base.ts create mode 100644 shared/src/objects/types/lamp-base.ts create mode 100644 shared/src/objects/types/tunnel-base.ts create mode 100644 shared/src/settings.ts rename {frontend/src/scripts/identity => shared/src/transport}/identity-manager.ts (100%) create mode 100644 shared/src/transport/identity.ts create mode 100644 shared/src/transport/typed.ts create mode 100644 shared/tsconfig.json diff --git a/backend/package.json b/backend/package.json index 89bbad7..08b0a60 100644 --- a/backend/package.json +++ b/backend/package.json @@ -15,7 +15,6 @@ "@types/express": "^4.17.8", "@types/gl-matrix": "^2.4.5", "@types/node": "^14.11.2", - "@types/uuid": "^8.0.0", "@typescript-eslint/eslint-plugin": "^3.9.1", "@typescript-eslint/parser": "^3.9.1", "clean-webpack-plugin": "^3.0.0", @@ -32,20 +31,22 @@ "terser-webpack-plugin": "^2.3.5", "ts-loader": "^8.0.1", "typescript": "^3.8.3", + "@types/uuid": "^8.0.0", "uuid": "^8.2.0", "webpack": "^4.43.0", "webpack-cli": "^3.3.11", "webpack-dev-server": "^3.10.3", - "file-loader": "^6.1.0" - }, - "dependencies": { + "file-loader": "^6.1.0", "@types/cors": "^2.8.7", "@types/socket.io": "^2.1.11", + "webpack-node-externals": "^2.5.2", + "shared": "file:../shared" + }, + "dependencies": { "cors": "^2.8.5", "express": "^4.17.1", "http": "0.0.1-security", "socket.io": "^2.3.0", - "uws": "^10.148.1", - "webpack-node-externals": "^2.5.2" + "uws": "^10.148.1" } } diff --git a/backend/src/main.ts b/backend/src/main.ts index e3872df..6f11a53 100644 --- a/backend/src/main.ts +++ b/backend/src/main.ts @@ -2,11 +2,27 @@ import ioserver, { Socket } from 'socket.io'; import express from 'express'; import { Server } from 'http'; import cors from 'cors'; - -import { PlayerContainer } from './players/player-container'; +import { applyArrayPlugins, Random, TransportEvents, deserializeCommand } from 'shared'; import './index.html'; -import { TransportEvents } from '../../shared/src/transport/transport-events'; import { Player } from './players/player'; +import { PhysicalGameObjectContainer } from './physics/physical-game-object-container'; +import { createDungeon } from './map/create-dungeon'; +import { glMatrix } from 'gl-matrix'; + +glMatrix.setMatrixArrayType(Array); + +applyArrayPlugins(); + +Random.seed = 42; + +const objects = new PhysicalGameObjectContainer(); + +createDungeon(objects); +createDungeon(objects); +createDungeon(objects); +createDungeon(objects); + +objects.initialize(); const app = express(); @@ -23,8 +39,6 @@ const port = 3000; const server = new Server(app); const io = ioserver(server); -const players = new PlayerContainer(); - const log = (text: string) => { io.to('insights').emit('insights', text + '\n'); }; @@ -35,17 +49,15 @@ app.get('/', function (req, res) { io.on('connection', (socket: SocketIO.Socket) => { socket.on(TransportEvents.PlayerJoining, () => { - log('player joined'); + const player = new Player(objects, socket); - const player = new Player(socket); - players.addPlayer(player); - - socket.on(TransportEvents.PlayerSendingInfo, () => {}); + socket.on(TransportEvents.PlayerToServer, (text: string) => { + const command = deserializeCommand(JSON.parse(text)); + player.sendCommand(command); + }); socket.on('disconnect', () => { - log('player disconnected'); - - players.removePlayerBySocketId(player.socketId); + player.destroy(); }); }); diff --git a/frontend/src/scripts/objects/world/create-dungeon.ts b/backend/src/map/create-dungeon.ts similarity index 66% rename from frontend/src/scripts/objects/world/create-dungeon.ts rename to backend/src/map/create-dungeon.ts index 254c037..157f52d 100644 --- a/frontend/src/scripts/objects/world/create-dungeon.ts +++ b/backend/src/map/create-dungeon.ts @@ -1,11 +1,10 @@ import { vec2, vec3 } from 'gl-matrix'; -import { Random } from '../../helper/random'; -import { Physics } from '../../physics/physics'; -import { Objects } from '../objects'; -import { Lamp } from '../types/lamp'; -import { Tunnel } from '../types/tunnel'; +import { Random } from 'shared'; +import { LampPhysics } from '../objects/lamp-physics'; +import { TunnelPhysics } from '../objects/tunnel-physics'; +import { PhysicalGameObjectContainer } from '../physics/physical-game-object-container'; -export const createDungeon = (objects: Objects, physics: Physics) => { +export const createDungeon = (objects: PhysicalGameObjectContainer) => { let previousRadius = 350; let previousEnd = vec2.create(); @@ -17,27 +16,26 @@ export const createDungeon = (objects: Objects, physics: Physics) => { const currentEnd = vec2.fromValues(i, height); const currentToRadius = Random.getRandom() * 300 + 150; - const tunnel = new Tunnel( - physics, + const tunnel = new TunnelPhysics( previousEnd, currentEnd, previousRadius, currentToRadius ); - objects.addObject(tunnel); + objects.addObject(tunnel, false); if (++tunnelsCountSinceLastLight > 3 && Random.getRandom() > 0.7) { objects.addObject( - new Lamp( + new LampPhysics( currentEnd, vec3.normalize( vec3.create(), vec3.fromValues(Random.getRandom(), 0, Random.getRandom()) ), - 0.5, - physics - ) + 0.5 + ), + false ); tunnelsCountSinceLastLight = 0; } diff --git a/backend/src/objects/character-physics.ts b/backend/src/objects/character-physics.ts new file mode 100644 index 0000000..bcd1f45 --- /dev/null +++ b/backend/src/objects/character-physics.ts @@ -0,0 +1,31 @@ +import { id, CharacterBase } from 'shared'; + +import { ImmutableBoundingBox } from '../physics/bounds/immutable-bounding-box'; +import { PhysicalGameObject } from '../physics/physical-game-object'; +import { CirclePhysics } from './circle-physics'; + +export class CharacterPhysics extends CharacterBase implements PhysicalGameObject { + public readonly canCollide = true; + public readonly isInverted = false; + public readonly canMove = true; + + constructor(head: CirclePhysics, leftFoot: CirclePhysics, rightFoot: CirclePhysics) { + super(id(), head, leftFoot, rightFoot); + } + + private boundingBox?: ImmutableBoundingBox; + + public getBoundingBox(): ImmutableBoundingBox { + if (!this.boundingBox) { + this.boundingBox = (this.head as CirclePhysics).boundingBox; + (this.head as CirclePhysics).boundingBox.owner = this; + } + + return this.boundingBox; + } + + public toJSON(): any { + const { type, id, head, leftFoot, rightFoot } = this; + return [type, id, head, leftFoot, rightFoot]; + } +} diff --git a/frontend/src/scripts/physics/bounds/bounding-circle.ts b/backend/src/objects/circle-physics.ts similarity index 71% rename from frontend/src/scripts/physics/bounds/bounding-circle.ts rename to backend/src/objects/circle-physics.ts index 9047620..f543480 100644 --- a/frontend/src/scripts/physics/bounds/bounding-circle.ts +++ b/backend/src/objects/circle-physics.ts @@ -1,20 +1,20 @@ import { vec2 } from 'gl-matrix'; -import { PhysicalObject } from '../physical-object'; -import { BoundingBox } from './bounding-box'; -import { BoundingBoxBase } from './bounding-box-base'; +import { Circle } from 'shared'; +import { BoundingBox } from '../physics/bounds/bounding-box'; +import { BoundingBoxBase } from '../physics/bounds/bounding-box-base'; -export class BoundingCircle { +export class CirclePhysics implements Circle { private _boundingBox: BoundingBox; - constructor( - public readonly owner: PhysicalObject, - private _center: vec2, - private _radius: number - ) { - this._boundingBox = new BoundingBox(owner); + constructor(private _center: vec2, private _radius: number) { + this._boundingBox = new BoundingBox(null); this.recalculateBoundingBox(); } + public get boundingBox(): BoundingBoxBase { + return this._boundingBox; + } + public get center(): vec2 { return this._center; } @@ -37,15 +37,15 @@ export class BoundingCircle { return vec2.distance(target, this.center) - this.radius; } - public distanceBetween(target: BoundingCircle): number { + public distanceBetween(target: CirclePhysics): number { return vec2.distance(target.center, this.center) - this.radius - target.radius; } - public areIntersecting(other: PhysicalObject): boolean { + public areIntersecting(other: CirclePhysics): boolean { return other.distance(this.center) < this.radius; } - public isInside(other: PhysicalObject): boolean { + public isInside(other: CirclePhysics): boolean { return other.distance(this.center) < -this.radius; } @@ -62,14 +62,15 @@ export class BoundingCircle { return result; } - public get boundingBox(): BoundingBoxBase { - return this._boundingBox; - } - private recalculateBoundingBox() { this._boundingBox.xMin = this.center.x - this._radius; this._boundingBox.xMax = this.center.x + this._radius; this._boundingBox.yMin = this.center.y - this._radius; this._boundingBox.yMax = this.center.y + this._radius; } + + public toJSON(): any { + const { center, radius } = this; + return { center, radius }; + } } diff --git a/backend/src/objects/lamp-physics.ts b/backend/src/objects/lamp-physics.ts new file mode 100644 index 0000000..3947a57 --- /dev/null +++ b/backend/src/objects/lamp-physics.ts @@ -0,0 +1,36 @@ +import { vec2, vec3 } from 'gl-matrix'; +import { LampBase, settings, id } from 'shared'; + +import { ImmutableBoundingBox } from '../physics/bounds/immutable-bounding-box'; +import { PhysicalGameObject } from '../physics/physical-game-object'; + +export class LampPhysics extends LampBase implements PhysicalGameObject { + public readonly canCollide = false; + public readonly isInverted = false; + public readonly canMove = false; + + constructor(center: vec2, color: vec3, lightness: number) { + super(id(), center, color, lightness); + } + + private boundingBox?: ImmutableBoundingBox; + + public getBoundingBox(): ImmutableBoundingBox { + if (!this.boundingBox) { + this.boundingBox = new ImmutableBoundingBox( + this, + this.center.x - settings.lightCutoffDistance, + this.center.x + settings.lightCutoffDistance, + this.center.y - settings.lightCutoffDistance, + this.center.y + settings.lightCutoffDistance + ); + } + + return this.boundingBox; + } + + public toJSON(): any { + const { type, id, center, color, lightness } = this; + return [type, id, center, color, lightness]; + } +} diff --git a/backend/src/objects/tunnel-physics.ts b/backend/src/objects/tunnel-physics.ts new file mode 100644 index 0000000..b89c68e --- /dev/null +++ b/backend/src/objects/tunnel-physics.ts @@ -0,0 +1,48 @@ +import { vec2 } from 'gl-matrix'; +import { clamp01, mix, TunnelBase, id } from 'shared'; +import { PhysicalGameObject } from '../physics/physical-game-object'; + +import { ImmutableBoundingBox } from '../physics/bounds/immutable-bounding-box'; + +export class TunnelPhysics extends TunnelBase implements PhysicalGameObject { + public readonly canCollide = true; + public readonly isInverted = true; + public readonly canMove = false; + + private boundingBox?: ImmutableBoundingBox; + + constructor(from: vec2, to: vec2, fromRadius: number, toRadius: number) { + super(id(), from, to, fromRadius, toRadius); + } + + public distance(target: vec2): number { + const toFromDelta = vec2.subtract(vec2.create(), this.to, this.from); + const targetFromDelta = vec2.subtract(vec2.create(), target, this.from); + + const h = clamp01( + vec2.dot(targetFromDelta, toFromDelta) / vec2.dot(toFromDelta, toFromDelta) + ); + + return ( + vec2.distance(targetFromDelta, vec2.scale(vec2.create(), toFromDelta, h)) - + mix(this.fromRadius, this.toRadius, h) + ); + } + + public getBoundingBox(): ImmutableBoundingBox { + if (!this.boundingBox) { + const xMin = Math.min(this.from.x - this.fromRadius, this.to.x - this.toRadius); + const yMin = Math.min(this.from.y - this.fromRadius, this.to.y - this.toRadius); + const xMax = Math.max(this.from.x + this.fromRadius, this.to.x + this.toRadius); + const yMax = Math.max(this.from.y + this.fromRadius, this.to.y + this.toRadius); + this.boundingBox = new ImmutableBoundingBox(this, xMin, xMax, yMin, yMax, true); + } + + return this.boundingBox; + } + + public toJSON(): any { + const { type, id, from, to, fromRadius, toRadius } = this; + return [type, id, from, to, fromRadius, toRadius]; + } +} diff --git a/frontend/src/scripts/physics/bounds/bounding-box-base.ts b/backend/src/physics/bounds/bounding-box-base.ts similarity index 91% rename from frontend/src/scripts/physics/bounds/bounding-box-base.ts rename to backend/src/physics/bounds/bounding-box-base.ts index d2c7bd7..3a62165 100644 --- a/frontend/src/scripts/physics/bounds/bounding-box-base.ts +++ b/backend/src/physics/bounds/bounding-box-base.ts @@ -1,9 +1,9 @@ import { vec2 } from 'gl-matrix'; -import { PhysicalObject } from '../physical-object'; +import { PhysicalGameObject } from '../physical-game-object'; export abstract class BoundingBoxBase { constructor( - public readonly owner: PhysicalObject, + public owner: PhysicalGameObject, protected _xMin: number = 0, protected _xMax: number = 0, protected _yMin: number = 0, diff --git a/frontend/src/scripts/physics/bounds/bounding-box.ts b/backend/src/physics/bounds/bounding-box.ts similarity index 100% rename from frontend/src/scripts/physics/bounds/bounding-box.ts rename to backend/src/physics/bounds/bounding-box.ts diff --git a/frontend/src/scripts/physics/bounds/immutable-bounding-box.ts b/backend/src/physics/bounds/immutable-bounding-box.ts similarity index 100% rename from frontend/src/scripts/physics/bounds/immutable-bounding-box.ts rename to backend/src/physics/bounds/immutable-bounding-box.ts diff --git a/frontend/src/scripts/physics/containers/bounding-box-list.ts b/backend/src/physics/containers/bounding-box-list.ts similarity index 100% rename from frontend/src/scripts/physics/containers/bounding-box-list.ts rename to backend/src/physics/containers/bounding-box-list.ts diff --git a/frontend/src/scripts/physics/containers/bounding-box-tree.ts b/backend/src/physics/containers/bounding-box-tree.ts similarity index 100% rename from frontend/src/scripts/physics/containers/bounding-box-tree.ts rename to backend/src/physics/containers/bounding-box-tree.ts diff --git a/backend/src/physics/physical-game-object-container.ts b/backend/src/physics/physical-game-object-container.ts new file mode 100644 index 0000000..c6814d6 --- /dev/null +++ b/backend/src/physics/physical-game-object-container.ts @@ -0,0 +1,86 @@ +import { GameObject, Id } from 'shared'; +import { BoundingBoxBase } from './bounds/bounding-box-base'; + +import { BoundingBoxList } from './containers/bounding-box-list'; +import { BoundingBoxTree } from './containers/bounding-box-tree'; +import { Command } from 'shared'; +import { PhysicalGameObject } from './physical-game-object'; +import { ImmutableBoundingBox } from './bounds/immutable-bounding-box'; + +export class PhysicalGameObjectContainer { + private isTreeInitialized = false; + private staticBoundingBoxesWaitList: Array = []; + private staticBoundingBoxes = new BoundingBoxTree(); + private dynamicBoundingBoxes = new BoundingBoxList(); + + protected objects: Map = new Map(); + private objectsGroupedByAbilities: Map> = new Map(); + + public initialize() { + this.staticBoundingBoxes.build(this.staticBoundingBoxesWaitList); + this.isTreeInitialized = true; + } + + public addObject(object: PhysicalGameObject, isDynamic) { + this.objects.set(object.id, object); + + for (const command of this.objectsGroupedByAbilities.keys()) { + if (object.reactsToCommand(command)) { + this.objectsGroupedByAbilities.get(command).push(object); + } + } + + if (isDynamic) { + this.dynamicBoundingBoxes.insert(object.getBoundingBox()); + } else { + if (!this.isTreeInitialized) { + this.staticBoundingBoxesWaitList.push(object.getBoundingBox()); + } else { + this.staticBoundingBoxes.insert(object.getBoundingBox()); + } + } + } + + public removeObject(object: PhysicalGameObject) { + this.objects.delete(object.id); + + for (const command of this.objectsGroupedByAbilities.keys()) { + if (object.reactsToCommand(command)) { + const array = this.objectsGroupedByAbilities.get(command); + array.splice( + array.findIndex((i) => i.id == object.id), + 1 + ); + } + } + + this.dynamicBoundingBoxes.remove(object.getBoundingBox()); + } + + public sendCommand(e: Command) { + if (!this.objectsGroupedByAbilities.has(e.type)) { + this.createGroupForCommand(e.type); + } + + this.objectsGroupedByAbilities.get(e.type).forEach((o, _) => o.sendCommand(e)); + } + + private createGroupForCommand(commandType: string) { + const objectsReactingToCommand = []; + + this.objects.forEach((o, _) => { + if (o.reactsToCommand(commandType)) { + objectsReactingToCommand.push(o); + } + }); + + this.objectsGroupedByAbilities.set(commandType, objectsReactingToCommand); + } + + public findIntersecting(box: BoundingBoxBase): Array { + return [ + ...this.staticBoundingBoxes.findIntersecting(box), + ...this.dynamicBoundingBoxes.findIntersecting(box), + ].map((b) => b.owner); + } +} diff --git a/backend/src/physics/physical-game-object.ts b/backend/src/physics/physical-game-object.ts new file mode 100644 index 0000000..a359e02 --- /dev/null +++ b/backend/src/physics/physical-game-object.ts @@ -0,0 +1,10 @@ +import { GameObject } from 'shared'; +import { BoundingBoxBase } from './bounds/bounding-box-base'; + +export interface PhysicalGameObject extends GameObject { + getBoundingBox(): BoundingBoxBase; + //distance(target: vec2): number; + readonly isInverted: boolean; + readonly canCollide: boolean; + readonly canMove: boolean; +} diff --git a/backend/src/players/player-container.ts b/backend/src/players/player-container.ts deleted file mode 100644 index 5c4f732..0000000 --- a/backend/src/players/player-container.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { Player } from './player'; - -export class PlayerContainer { - private socketIdToPlayer = new Map(); - - public addPlayer(player: Player) { - this.socketIdToPlayer.set(player.socketId, player); - } - - public removePlayerBySocketId(socketId: string) { - this.socketIdToPlayer.delete(socketId); - } -} diff --git a/backend/src/players/player.ts b/backend/src/players/player.ts index a88b375..1eeae67 100644 --- a/backend/src/players/player.ts +++ b/backend/src/players/player.ts @@ -1,9 +1,119 @@ -import { Socket } from 'dgram'; +import { vec2 } from 'gl-matrix'; +import { + Command, + CommandExecutors, + CommandReceiver, + CreateObjectsCommand, + CreatePlayerCommand, + DeleteObjectsCommand, + MoveActionCommand, + SetViewAreaActionCommand, + TransportEvents, + UpdateObjectsCommand, +} from 'shared'; +import { CharacterPhysics } from '../objects/character-physics'; +import { CirclePhysics } from '../objects/circle-physics'; -export class Player { - constructor(private readonly socket: SocketIO.Socket) {} +import { BoundingBox } from '../physics/bounds/bounding-box'; +import { PhysicalGameObject } from '../physics/physical-game-object'; +import { PhysicalGameObjectContainer } from '../physics/physical-game-object-container'; +import { jsonSerialize } from '../serialize'; - public get socketId(): string { - return this.socket.id; +export class Player extends CommandReceiver { + public isActive = true; + + private character: CharacterPhysics = new CharacterPhysics( + new CirclePhysics(vec2.fromValues(50, 50), 50), + new CirclePhysics(vec2.fromValues(50, 50), 50), + new CirclePhysics(vec2.fromValues(50, 50), 50) + ); + + private objectsPreviouslyInViewArea: Array = []; + private objectsInViewArea: Array = []; + + protected commandExecutors: CommandExecutors = { + [SetViewAreaActionCommand.type]: this.setViewArea.bind(this), + [MoveActionCommand.type]: (c: MoveActionCommand) => { + vec2.normalize(c.delta, c.delta); + vec2.scale(c.delta, c.delta, 40); + this.character.head.center = vec2.add( + this.character.head.center, + this.character.head.center, + c.delta + ); + }, + }; + + protected defaultCommandExecutor(command: Command) {} + + constructor( + private readonly objects: PhysicalGameObjectContainer, + private readonly socket: SocketIO.Socket + ) { + super(); + this.objectsPreviouslyInViewArea.push(this.character); + this.objectsInViewArea.push(this.character); + + this.objects.addObject(this.character, true); + + socket.emit( + TransportEvents.ServerToPlayer, + jsonSerialize(new CreatePlayerCommand(jsonSerialize(this.character))) + ); + + this.sendObjects(); + } + + public setViewArea(c: SetViewAreaActionCommand) { + const viewArea = new BoundingBox(null); + viewArea.topLeft = c.viewArea.topLeft; + viewArea.size = c.viewArea.size; + + this.objectsInViewArea = this.objects.findIntersecting(viewArea); + } + + public sendObjects() { + const newlyIntersecting = this.objectsInViewArea.filter( + (o) => !this.objectsPreviouslyInViewArea.includes(o) + ); + + const noLongerIntersecting = this.objectsPreviouslyInViewArea.filter( + (o) => !this.objectsInViewArea.includes(o) + ); + + this.objectsPreviouslyInViewArea = this.objectsInViewArea; + + if (noLongerIntersecting.length > 0) { + this.socket.emit( + TransportEvents.ServerToPlayer, + jsonSerialize(new DeleteObjectsCommand(noLongerIntersecting.map((o) => o.id))) + ); + } + + if (newlyIntersecting.length > 0) { + this.socket.emit( + TransportEvents.ServerToPlayer, + jsonSerialize(new CreateObjectsCommand(jsonSerialize(newlyIntersecting))) + ); + } + + this.socket.emit( + TransportEvents.ServerToPlayer, + jsonSerialize( + new UpdateObjectsCommand( + jsonSerialize(this.objectsInViewArea.filter((o) => o.canMove)) + ) + ) + ); + + if (this.isActive) { + //setImmediate(this.sendObjects.bind(this)); + setTimeout(this.sendObjects.bind(this), 5); + } + } + + public destroy() { + this.isActive = false; + this.objects.removeObject(this.character); } } diff --git a/backend/src/serialize.ts b/backend/src/serialize.ts new file mode 100644 index 0000000..973cedd --- /dev/null +++ b/backend/src/serialize.ts @@ -0,0 +1,2 @@ +export const jsonSerialize = (o: any): string => + JSON.stringify(o, (key, value) => (value?.toFixed ? Number(value.toFixed(3)) : value)); diff --git a/backend/tsconfig.json b/backend/tsconfig.json index 140b170..5521a05 100644 --- a/backend/tsconfig.json +++ b/backend/tsconfig.json @@ -3,17 +3,12 @@ "outDir": "./dist/", "sourceMap": true, "noImplicitAny": false, - "target": "es6", + "target": "es5", "downlevelIteration": true, "allowJs": true, "experimentalDecorators": true, "moduleResolution": "node", "module": "commonjs", - "esModuleInterop": true, - "baseUrl": ".", - "paths": { - "*": ["node_modules/*"] - } - }, - "include": ["src/**/*"] + "esModuleInterop": true + } } diff --git a/backend/webpack.config.js b/backend/webpack.config.js index 6d3e794..9aa03bb 100644 --- a/backend/webpack.config.js +++ b/backend/webpack.config.js @@ -11,7 +11,11 @@ module.exports = { entry: { main: [PATHS.entryPoint], }, - externals: [nodeExternals()], + externals: [ + nodeExternals({ + allowlist: [/(^shared)/], + }), + ], target: 'node', output: { filename: '[name].js', diff --git a/frontend/package.json b/frontend/package.json index 97e8f23..780f266 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -63,6 +63,7 @@ "webpack-dev-server": "^3.10.3", "firebase": "^7.22.0", "@types/socket.io-client": "^1.4.34", - "socket.io-client": "^2.3.1" + "socket.io-client": "^2.3.1", + "shared": "file:../shared" } } diff --git a/frontend/src/index.ts b/frontend/src/index.ts index 1889116..9979b81 100644 --- a/frontend/src/index.ts +++ b/frontend/src/index.ts @@ -1,29 +1,13 @@ import { glMatrix } from 'gl-matrix'; -import io from 'socket.io-client'; -import { TransportEvents } from '../../shared/src/transport/transport-events'; -import { Configuration } from './scripts/config/configuration'; import { Game } from './scripts/game'; import { Random } from './scripts/helper/random'; import './styles/main.scss'; glMatrix.setMatrixArrayType(Array); +Random.seed = 42; const main = async () => { - await Configuration.initialize(); - - const socket = io(Configuration.servers[0], { - reconnectionDelayMax: 10000, - transports: ['websocket'], - }); - - socket.on('reconnect_attempt', () => { - socket.io.opts.transports = ['polling', 'websocket']; - }); - - socket.emit(TransportEvents.PlayerJoining, null); - try { - Random.seed = 42; await new Game().start(); } catch (e) { console.error(e); diff --git a/frontend/src/scripts/commands/command-broadcaster.ts b/frontend/src/scripts/commands/command-broadcaster.ts deleted file mode 100644 index 256a002..0000000 --- a/frontend/src/scripts/commands/command-broadcaster.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { CommandReceiver } from './command-receiver'; -import { CommandGenerator } from './command-generator'; - -export class CommandBroadcaster { - constructor( - commandGenerators: Array, - commandReceivers: Array - ) { - commandReceivers.forEach((r) => commandGenerators.forEach((g) => g.subscribe(r))); - } -} diff --git a/frontend/src/scripts/commands/command-receiver.ts b/frontend/src/scripts/commands/command-receiver.ts deleted file mode 100644 index bf6e119..0000000 --- a/frontend/src/scripts/commands/command-receiver.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { Command } from './command'; - -export interface CommandReceiver { - sendCommand(command: Command): void; -} diff --git a/frontend/src/scripts/commands/command.ts b/frontend/src/scripts/commands/command.ts deleted file mode 100644 index ed5a9ff..0000000 --- a/frontend/src/scripts/commands/command.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { Typed } from '../transport/serializable'; -import { Id } from '../identity/identity'; - -export abstract class Command extends Typed { - target?: Id; -} diff --git a/frontend/src/scripts/commands/generators/keyboard-listener.ts b/frontend/src/scripts/commands/generators/keyboard-listener.ts new file mode 100644 index 0000000..a3008a3 --- /dev/null +++ b/frontend/src/scripts/commands/generators/keyboard-listener.ts @@ -0,0 +1,42 @@ +import { vec2 } from 'gl-matrix'; +import { CommandGenerator, MoveActionCommand } from 'shared'; + +export class KeyboardListener extends CommandGenerator { + private keysDown: Set = new Set(); + + constructor(target: Element, private moveScale: number) { + super(); + + target.addEventListener('keydown', (event: KeyboardEvent) => { + const key = this.normalize(event.key); + this.keysDown.add(key); + }); + + target.addEventListener('keyup', (event: KeyboardEvent) => { + const key = this.normalize(event.key); + this.keysDown.delete(key); + }); + } + + public generateCommands() { + const up = ~~( + this.keysDown.has('w') || + this.keysDown.has('arrowup') || + this.keysDown.has(' ') + ); + const down = ~~(this.keysDown.has('s') || this.keysDown.has('arrowdown')); + const left = ~~(this.keysDown.has('a') || this.keysDown.has('arrowleft')); + const right = ~~(this.keysDown.has('d') || this.keysDown.has('arrowright')); + + const movement = vec2.fromValues(right - left, up - down); + if (vec2.squaredLength(movement) > 0) { + vec2.normalize(movement, movement); + vec2.scale(movement, movement, this.moveScale); + this.sendCommandToSubcribers(new MoveActionCommand(movement)); + } + } + + private normalize(key: string): string { + return key.toLowerCase(); + } +} diff --git a/frontend/src/scripts/commands/generators/mouse-listener.ts b/frontend/src/scripts/commands/generators/mouse-listener.ts new file mode 100644 index 0000000..9b11688 --- /dev/null +++ b/frontend/src/scripts/commands/generators/mouse-listener.ts @@ -0,0 +1,36 @@ +import { vec2 } from 'gl-matrix'; +import { + CommandGenerator, + PrimaryActionCommand, + SecondaryActionCommand, + TernaryActionCommand, +} from 'shared'; + +export class MouseListener extends CommandGenerator { + constructor(target: Element) { + super(); + + target.addEventListener('mousemove', (event: MouseEvent) => { + const position = this.positionFromEvent(event); + this.sendCommandToSubcribers(new PrimaryActionCommand(position)); + }); + + target.addEventListener('mousedown', (event: MouseEvent) => { + const position = this.positionFromEvent(event); + + if (event.button == 0) { + this.sendCommandToSubcribers(new SecondaryActionCommand(position)); + } + }); + + target.addEventListener('contextmenu', (event: MouseEvent) => { + event.preventDefault(); + const position = this.positionFromEvent(event); + this.sendCommandToSubcribers(new TernaryActionCommand(position)); + }); + } + + private positionFromEvent(event: MouseEvent): vec2 { + return vec2.fromValues(event.clientX, event.clientY); + } +} diff --git a/frontend/src/scripts/commands/generators/touch-listener.ts b/frontend/src/scripts/commands/generators/touch-listener.ts new file mode 100644 index 0000000..2760483 --- /dev/null +++ b/frontend/src/scripts/commands/generators/touch-listener.ts @@ -0,0 +1,57 @@ +import { vec2 } from 'gl-matrix'; +import { + CommandGenerator, + MoveActionCommand, + PrimaryActionCommand, + SecondaryActionCommand, + TernaryActionCommand, +} from 'shared'; + +export class TouchListener extends CommandGenerator { + private previousPosition = vec2.create(); + + constructor(target: HTMLElement) { + super(); + + target.addEventListener('touchstart', (event: TouchEvent) => { + event.preventDefault(); + + const touchCount = event.touches.length; + const position = this.positionFromEvent(event); + this.previousPosition = position; + + if (touchCount == 1) { + this.sendCommandToSubcribers(new PrimaryActionCommand(position)); + } else if (touchCount == 2) { + this.sendCommandToSubcribers(new SecondaryActionCommand(position)); + } else { + this.sendCommandToSubcribers(new TernaryActionCommand(position)); + } + }); + + target.addEventListener('touchmove', (event: TouchEvent) => { + event.preventDefault(); + + const position = this.positionFromEvent(event); + + this.sendCommandToSubcribers( + new MoveActionCommand( + vec2.subtract(vec2.create(), position, this.previousPosition) + ) + ); + + this.previousPosition = position; + }); + } + + private positionFromEvent(event: TouchEvent): vec2 { + const center = Array.prototype.reduce.call( + event.touches, + (center: vec2, touch: Touch) => + vec2.add(center, center, vec2.fromValues(-touch.clientX, touch.clientY)), + vec2.create() + ); + + return vec2.scale(center, center, 1 / event.touches.length); + } +} diff --git a/frontend/src/scripts/commands/move-to.ts b/frontend/src/scripts/commands/move-to.ts deleted file mode 100644 index f762b3c..0000000 --- a/frontend/src/scripts/commands/move-to.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { vec2 } from 'gl-matrix'; -import { Command } from './command'; - -export class MoveToCommand extends Command { - public constructor(public readonly position?: vec2) { - super(); - } - - public get type(): string { - return 'MoveToCommand'; - } -} diff --git a/frontend/src/scripts/commands/receivers/command-receiver-socket.ts b/frontend/src/scripts/commands/receivers/command-receiver-socket.ts new file mode 100644 index 0000000..cb6fc59 --- /dev/null +++ b/frontend/src/scripts/commands/receivers/command-receiver-socket.ts @@ -0,0 +1,11 @@ +import { Command, CommandReceiver, TransportEvents } from 'shared'; + +export class CommandReceiverSocket extends CommandReceiver { + constructor(private readonly socket: SocketIOClient.Socket) { + super(); + } + + protected defaultCommandExecutor(command: Command) { + this.socket.emit(TransportEvents.PlayerToServer, JSON.stringify(command)); + } +} diff --git a/frontend/src/scripts/commands/render.ts b/frontend/src/scripts/commands/render.ts deleted file mode 100644 index 0a05d7a..0000000 --- a/frontend/src/scripts/commands/render.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { Renderer } from 'sdf-2d'; -import { Command } from './command'; - -export class RenderCommand extends Command { - public constructor(public readonly renderer?: Renderer) { - super(); - } - - public get type(): string { - return 'RenderCommand'; - } -} diff --git a/frontend/src/scripts/commands/step.ts b/frontend/src/scripts/commands/step.ts deleted file mode 100644 index 4fa2291..0000000 --- a/frontend/src/scripts/commands/step.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { Command } from './command'; - -export class StepCommand extends Command { - public constructor(public readonly deltaTimeInMiliseconds?: DOMHighResTimeStamp) { - super(); - } - - public get type(): string { - return 'StepCommand'; - } -} diff --git a/frontend/src/scripts/commands/teleport-to.ts b/frontend/src/scripts/commands/teleport-to.ts deleted file mode 100644 index 9f0b942..0000000 --- a/frontend/src/scripts/commands/teleport-to.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { vec2 } from 'gl-matrix'; -import { Command } from './command'; - -export class TeleportToCommand extends Command { - public constructor(public readonly position?: vec2) { - super(); - } - - public get type(): string { - return 'TeleportToCommand'; - } -} diff --git a/frontend/src/scripts/commands/types/move-to.ts b/frontend/src/scripts/commands/types/move-to.ts new file mode 100644 index 0000000..c062b4c --- /dev/null +++ b/frontend/src/scripts/commands/types/move-to.ts @@ -0,0 +1,10 @@ +import { vec2 } from 'gl-matrix'; +import { Command } from 'shared'; + +export class MoveToCommand extends Command { + public static readonly type = 'MoveToCommand'; + + public constructor(public readonly position: vec2) { + super(); + } +} diff --git a/frontend/src/scripts/commands/types/render.ts b/frontend/src/scripts/commands/types/render.ts new file mode 100644 index 0000000..027e111 --- /dev/null +++ b/frontend/src/scripts/commands/types/render.ts @@ -0,0 +1,10 @@ +import { Renderer } from 'sdf-2d'; +import { Command } from 'shared'; + +export class RenderCommand extends Command { + public static readonly type = 'RenderCommand'; + + public constructor(public readonly renderer: Renderer) { + super(); + } +} diff --git a/frontend/src/scripts/commands/types/step.ts b/frontend/src/scripts/commands/types/step.ts new file mode 100644 index 0000000..f10f9fb --- /dev/null +++ b/frontend/src/scripts/commands/types/step.ts @@ -0,0 +1,9 @@ +import { Command } from 'shared'; + +export class StepCommand extends Command { + public static readonly type = 'StepCommand'; + + public constructor(public readonly deltaTimeInMiliseconds: DOMHighResTimeStamp) { + super(); + } +} diff --git a/frontend/src/scripts/game.ts b/frontend/src/scripts/game.ts index c7d752b..41a4e67 100644 --- a/frontend/src/scripts/game.ts +++ b/frontend/src/scripts/game.ts @@ -10,50 +10,69 @@ import { renderNoise, WrapOptions, } from 'sdf-2d'; -import { CommandBroadcaster } from './commands/command-broadcaster'; -import { MoveToCommand } from './commands/move-to'; -import { RenderCommand } from './commands/render'; -import { StepCommand } from './commands/step'; +import { broadcastCommands, SetViewAreaActionCommand, TransportEvents } from 'shared'; +import io from 'socket.io-client'; +import { KeyboardListener } from './commands/generators/keyboard-listener'; +import { MouseListener } from './commands/generators/mouse-listener'; +import { TouchListener } from './commands/generators/touch-listener'; +import { CommandReceiverSocket } from './commands/receivers/command-receiver-socket'; +import { RenderCommand } from './commands/types/render'; +import { StepCommand } from './commands/types/step'; +import { Configuration } from './config/configuration'; import { DeltaTimeCalculator } from './helper/delta-time-calculator'; import { prettyPrint } from './helper/pretty-print'; import { rgb } from './helper/rgb'; -import { IGame } from './i-game'; -import { KeyboardListener } from './input/keyboard-listener'; -import { MouseListener } from './input/mouse-listener'; -import { TouchListener } from './input/touch-listener'; -import { GameObject } from './objects/game-object'; -import { Objects } from './objects/objects'; -import { Camera } from './objects/types/camera'; -import { Character } from './objects/types/character'; -import { createDungeon } from './objects/world/create-dungeon'; -import { BoundingBoxBase } from './physics/bounds/bounding-box-base'; -import { Physics } from './physics/physics'; +import { GameObjectContainer } from './objects/game-object-container'; import { settings } from './settings'; import { BlobShape } from './shapes/blob-shape'; +import { deserialize } from './transport/deserialize'; -export class Game implements IGame { - public readonly objects = new Objects(); - public readonly physics = new Physics(); - public readonly camera = new Camera(); - private character: Character; +export class Game { + public readonly gameObjects = new GameObjectContainer(); + private readonly canvas: HTMLCanvasElement = document.querySelector('canvas#main'); private renderer: Renderer; - private rendererPromise: Promise; + private socket: SocketIOClient.Socket; + private deltaTimeCalculator = new DeltaTimeCalculator(); private overlay: HTMLElement = document.querySelector('#overlay'); + private keyboardListener: KeyboardListener; - constructor() { - const canvas: HTMLCanvasElement = document.querySelector('canvas#main'); + private async setupCommunication(): Promise { + await Configuration.initialize(); - new CommandBroadcaster( + this.socket = io(Configuration.servers[0], { + reconnectionDelayMax: 10000, + transports: ['websocket'], + }); + + this.socket.on('reconnect_attempt', () => { + this.socket.io.opts.transports = ['polling', 'websocket']; + }); + + this.socket.on(TransportEvents.ServerToPlayer, (serialized: string) => { + const command = deserialize(serialized); + console.log(command); + this.gameObjects.sendCommand(command); + }); + + this.socket.emit(TransportEvents.PlayerJoining, null); + + this.keyboardListener = new KeyboardListener(document.body, 100); + + broadcastCommands( [ - new KeyboardListener(document.body), - new MouseListener(canvas), - new TouchListener(canvas), + this.keyboardListener, + new MouseListener(this.canvas), + new TouchListener(this.canvas), ], - [this.objects] + [this.gameObjects, new CommandReceiverSocket(this.socket)] ); + } - this.rendererPromise = compile( - canvas, + private async setupRenderer(): Promise { + const noiseTexture = await renderNoise([1024, 1], 60, 1 / 8); + + this.renderer = await compile( + this.canvas, [ { ...InvertedTunnel.descriptor, @@ -61,7 +80,7 @@ export class Game implements IGame { }, { ...BlobShape.descriptor, - shaderCombinationSteps: [0, 1], + shaderCombinationSteps: [0, 1, 2, 3, 4, 5, 8], }, { ...Circle.descriptor, @@ -82,12 +101,7 @@ export class Game implements IGame { enableStopwatch: true, } ); - } - public async start(): Promise { - const noiseTexture = await renderNoise([1024, 1], 60, 1 / 8); - - this.renderer = await this.rendererPromise; this.renderer.setRuntimeSettings({ isWorldInverted: true, ambientLight: rgb(0.35, 0.1, 0.45), @@ -112,62 +126,46 @@ export class Game implements IGame { }, }, }); - this.initializeScene(); - this.physics.start(); + } + + public async start(): Promise { + await Promise.all([this.setupCommunication(), this.setupRenderer()]); requestAnimationFrame(this.gameLoop.bind(this)); } - public get viewArea(): BoundingBoxBase { - return this.camera.viewArea; - } - - public findIntersecting(box: BoundingBoxBase): Array { - return this.physics.findIntersecting(box); - } - public displayToWorldCoordinates(p: vec2): vec2 { return this.renderer.displayToWorldCoordinates(p); } - private initializeScene() { - createDungeon(this.objects, this.physics); - createDungeon(this.objects, this.physics); - createDungeon(this.objects, this.physics); - createDungeon(this.objects, this.physics); - - this.character = new Character(this.physics, this); - this.objects.addObject(this.character); - this.objects.addObject(this.camera); - } - - private deltaTimeCalculator = new DeltaTimeCalculator(); - private gameLoop(time: DOMHighResTimeStamp) { const deltaTime = this.deltaTimeCalculator.getNextDeltaTime(time); + this.keyboardListener.generateCommands(); - this.objects.sendCommand(new StepCommand(deltaTime)); - this.camera.sendCommand(new MoveToCommand(this.character.position)); - this.camera.sendCommand(new RenderCommand(this.renderer)); + if (this.gameObjects.camera) { + // todo: Should only send aspect ratio + this.socket.emit( + TransportEvents.PlayerToServer, + JSON.stringify(new SetViewAreaActionCommand(this.gameObjects.camera.viewArea)) + ); + } - const shouldBeDrawn = this.physics + this.gameObjects.sendCommand(new StepCommand(deltaTime)); + /*this.camera.sendCommand(new MoveToCommand(this.character.position)); + this.camera.sendCommand(new RenderCommand(this.renderer));*/ + + /*const shouldBeDrawn = this.physics .findIntersecting(this.camera.viewArea) .map((b) => b.owner); for (const object of shouldBeDrawn) { object?.sendCommand(new RenderCommand(this.renderer)); - } + }*/ + + this.gameObjects.sendCommand(new RenderCommand(this.renderer) as any); this.renderer.renderDrawables(); this.overlay.innerText = prettyPrint(this.renderer.insights); requestAnimationFrame(this.gameLoop.bind(this)); } - - public addObject(o: GameObject) { - this.objects.addObject(o); - } - - public removeObject(o: GameObject) { - this.objects.removeObject(o); - } } diff --git a/frontend/src/scripts/i-game.ts b/frontend/src/scripts/i-game.ts deleted file mode 100644 index 13b5c82..0000000 --- a/frontend/src/scripts/i-game.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { vec2 } from 'gl-matrix'; -import { GameObject } from './objects/game-object'; -import { BoundingBoxBase } from './physics/bounds/bounding-box-base'; - -export interface IGame { - readonly viewArea: BoundingBoxBase; - findIntersecting(box: BoundingBoxBase): Array; - displayToWorldCoordinates(p: vec2): vec2; - addObject(o: GameObject): void; - removeObject(o: GameObject): void; -} diff --git a/frontend/src/scripts/identity/identity.ts b/frontend/src/scripts/identity/identity.ts deleted file mode 100644 index 5dfbae8..0000000 --- a/frontend/src/scripts/identity/identity.ts +++ /dev/null @@ -1 +0,0 @@ -export type Id = string; diff --git a/frontend/src/scripts/input/commands/cursor-move-command.ts b/frontend/src/scripts/input/commands/cursor-move-command.ts deleted file mode 100644 index 5116fa6..0000000 --- a/frontend/src/scripts/input/commands/cursor-move-command.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { vec2 } from 'gl-matrix'; -import { Command } from '../../commands/command'; - -export class CursorMoveCommand extends Command { - public constructor(public readonly position?: vec2) { - super(); - } - - public get type(): string { - return 'CursorMoveCommand'; - } -} diff --git a/frontend/src/scripts/input/commands/key-down.ts b/frontend/src/scripts/input/commands/key-down.ts deleted file mode 100644 index 41dcddc..0000000 --- a/frontend/src/scripts/input/commands/key-down.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { Command } from '../../commands/command'; - -export class KeyDownCommand extends Command { - public constructor(public readonly key?: string) { - super(); - } - - public get type(): string { - return 'KeyDownCommand'; - } -} diff --git a/frontend/src/scripts/input/commands/key-up.ts b/frontend/src/scripts/input/commands/key-up.ts deleted file mode 100644 index 48446a5..0000000 --- a/frontend/src/scripts/input/commands/key-up.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { Command } from '../../commands/command'; - -export class KeyUpCommand extends Command { - public constructor(public readonly key?: string) { - super(); - } - - public get type(): string { - return 'KeyUpCommand'; - } -} diff --git a/frontend/src/scripts/input/commands/primary-action.ts b/frontend/src/scripts/input/commands/primary-action.ts deleted file mode 100644 index 54f6e02..0000000 --- a/frontend/src/scripts/input/commands/primary-action.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { vec2 } from 'gl-matrix'; -import { Command } from '../../commands/command'; - -export class PrimaryActionCommand extends Command { - public constructor(public readonly position?: vec2) { - super(); - } - - public get type(): string { - return 'PrimaryActionCommand'; - } -} diff --git a/frontend/src/scripts/input/commands/secondary-action.ts b/frontend/src/scripts/input/commands/secondary-action.ts deleted file mode 100644 index 2cb310e..0000000 --- a/frontend/src/scripts/input/commands/secondary-action.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { vec2 } from 'gl-matrix'; -import { Command } from '../../commands/command'; - -export class SecondaryActionCommand extends Command { - public constructor(public readonly position?: vec2) { - super(); - } - - public get type(): string { - return 'SecondaryActionCommand'; - } -} diff --git a/frontend/src/scripts/input/commands/swipe.ts b/frontend/src/scripts/input/commands/swipe.ts deleted file mode 100644 index 7f0d795..0000000 --- a/frontend/src/scripts/input/commands/swipe.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { vec2 } from 'gl-matrix'; -import { Command } from '../../commands/command'; - -export class SwipeCommand extends Command { - public constructor(public readonly delta?: vec2) { - super(); - } - - public get type(): string { - return 'SwipeCommand'; - } -} diff --git a/frontend/src/scripts/input/commands/zoom.ts b/frontend/src/scripts/input/commands/zoom.ts deleted file mode 100644 index df036cb..0000000 --- a/frontend/src/scripts/input/commands/zoom.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { Command } from '../../commands/command'; - -export class ZoomCommand extends Command { - public constructor(public readonly factor?: number) { - super(); - } - - public get type(): string { - return 'ZoomCommand'; - } -} diff --git a/frontend/src/scripts/input/keyboard-listener.ts b/frontend/src/scripts/input/keyboard-listener.ts deleted file mode 100644 index 32496d4..0000000 --- a/frontend/src/scripts/input/keyboard-listener.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { CommandGenerator } from '../commands/command-generator'; -import { KeyDownCommand } from './commands/key-down'; -import { KeyUpCommand } from './commands/key-up'; - -export class KeyboardListener extends CommandGenerator { - constructor(target: Element) { - super(); - - target.addEventListener('keydown', (event: KeyboardEvent) => { - const key = event.key.toLowerCase(); - this.sendCommand(new KeyDownCommand(key)); - }); - - target.addEventListener('keyup', (event: KeyboardEvent) => { - const key = event.key.toLowerCase(); - this.sendCommand(new KeyUpCommand(key)); - }); - } -} diff --git a/frontend/src/scripts/input/mouse-listener.ts b/frontend/src/scripts/input/mouse-listener.ts deleted file mode 100644 index 42eac13..0000000 --- a/frontend/src/scripts/input/mouse-listener.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { vec2 } from 'gl-matrix'; -import { CommandGenerator } from '../commands/command-generator'; -import { CursorMoveCommand } from './commands/cursor-move-command'; -import { PrimaryActionCommand } from './commands/primary-action'; -import { SecondaryActionCommand } from './commands/secondary-action'; -import { SwipeCommand } from './commands/swipe'; -import { ZoomCommand } from './commands/zoom'; - -export class MouseListener extends CommandGenerator { - private previousPosition = vec2.create(); - - private isMouseDown = false; - - constructor(target: Element) { - super(); - - target.addEventListener('mousedown', (event: MouseEvent) => { - const position = this.positionFromEvent(event); - - this.previousPosition = position; - this.isMouseDown = true; - - if (event.button == 0) { - this.sendCommand(new PrimaryActionCommand(position)); - } - }); - - target.addEventListener('mousemove', (event: MouseEvent) => { - const position = this.positionFromEvent(event); - - if (this.isMouseDown) { - this.sendCommand( - new SwipeCommand(vec2.subtract(vec2.create(), this.previousPosition, position)) - ); - this.previousPosition = position; - } - - this.sendCommand(new CursorMoveCommand(position)); - }); - - target.addEventListener('mouseup', (_: MouseEvent) => { - this.isMouseDown = false; - }); - - target.addEventListener('mouseleave', (_: MouseEvent) => { - this.isMouseDown = false; - }); - - target.addEventListener('contextmenu', (event: MouseEvent) => { - event.preventDefault(); - const position = this.positionFromEvent(event); - this.sendCommand(new SecondaryActionCommand(position)); - }); - - target.addEventListener('wheel', (event: MouseWheelEvent) => { - this.sendCommand(new ZoomCommand(event.deltaY > 0 ? 1.3 : 1 / 1.3)); - }); - } - - private positionFromEvent(event: MouseEvent): vec2 { - return vec2.fromValues(event.clientX, event.clientY); - } -} diff --git a/frontend/src/scripts/input/touch-listener.ts b/frontend/src/scripts/input/touch-listener.ts deleted file mode 100644 index 0d75936..0000000 --- a/frontend/src/scripts/input/touch-listener.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { vec2 } from 'gl-matrix'; -import { CommandGenerator } from '../commands/command-generator'; -import { PrimaryActionCommand } from './commands/primary-action'; -import { SecondaryActionCommand } from './commands/secondary-action'; -import { SwipeCommand } from './commands/swipe'; - -export class TouchListener extends CommandGenerator { - private previousPosition = vec2.create(); - - constructor(private target: HTMLElement) { - super(); - - target.addEventListener('touchstart', (event: TouchEvent) => { - event.preventDefault(); - - const touchCount = event.touches.length; - const position = this.positionFromEvent(event); - this.previousPosition = position; - - if (touchCount == 1) { - this.sendCommand(new PrimaryActionCommand(position)); - } else { - this.sendCommand(new SecondaryActionCommand(position)); - } - }); - - target.addEventListener('touchmove', (event: TouchEvent) => { - event.preventDefault(); - - const position = this.positionFromEvent(event); - - this.sendCommand( - new SwipeCommand(vec2.subtract(vec2.create(), position, this.previousPosition)) - ); - - this.previousPosition = position; - }); - } - - private positionFromEvent(event: TouchEvent): vec2 { - return vec2.fromValues(event.touches[0].clientX, event.touches[0].clientY); - } -} diff --git a/frontend/src/scripts/objects/camera.ts b/frontend/src/scripts/objects/camera.ts new file mode 100644 index 0000000..86ed5e4 --- /dev/null +++ b/frontend/src/scripts/objects/camera.ts @@ -0,0 +1,36 @@ +import { vec2 } from 'gl-matrix'; +import { CommandExecutors, GameObject, Rectangle, settings } from 'shared'; +import { RenderCommand } from '../commands/types/render'; + +export class Camera extends GameObject { + private static readonly inViewAreaSize = settings.inViewAreaSize; + private _viewArea = new Rectangle(); + + protected commandExecutors: CommandExecutors = { + [RenderCommand.type]: this.draw.bind(this), + }; + + constructor(public center: vec2 = vec2.create()) { + super(null); + } + + public get viewArea(): Rectangle { + return this._viewArea; + } + + private draw(c: RenderCommand) { + const canvasAspectRatio = c.renderer.canvasSize.x / c.renderer.canvasSize.y; + + this._viewArea.topLeft = vec2.fromValues( + this.center.x - this._viewArea.size.x / 2, + this.center.y + this._viewArea.size.y / 2 + ); + + this._viewArea.size = vec2.fromValues( + Math.sqrt(Camera.inViewAreaSize * canvasAspectRatio), + Math.sqrt(Camera.inViewAreaSize / canvasAspectRatio) + ); + + c.renderer.setViewArea(this._viewArea.topLeft, this._viewArea.size); + } +} diff --git a/frontend/src/scripts/objects/character-view.ts b/frontend/src/scripts/objects/character-view.ts new file mode 100644 index 0000000..8cb63b6 --- /dev/null +++ b/frontend/src/scripts/objects/character-view.ts @@ -0,0 +1,19 @@ +import { vec2 } from 'gl-matrix'; +import { CharacterBase, CommandExecutors } from 'shared'; +import { RenderCommand } from '../commands/types/render'; +import { BlobShape } from '../shapes/blob-shape'; + +export class CharacterView extends CharacterBase { + private shape = new BlobShape(); + + protected commandExecutors: CommandExecutors = { + [RenderCommand.type]: (c: RenderCommand) => { + this.shape.setCircles([this.head, this.leftFoot, this.rightFoot]); + c.renderer.addDrawable(this.shape); + }, + }; + + public get position(): vec2 { + return this.head.center; + } +} diff --git a/frontend/src/scripts/objects/game-object-container.ts b/frontend/src/scripts/objects/game-object-container.ts new file mode 100644 index 0000000..99459f7 --- /dev/null +++ b/frontend/src/scripts/objects/game-object-container.ts @@ -0,0 +1,63 @@ +import { + Command, + CommandExecutors, + CommandReceiver, + CreateObjectsCommand, + CreatePlayerCommand, + DeleteObjectsCommand, + GameObject, + Id, + UpdateObjectsCommand, +} from 'shared'; +import { StepCommand } from '../commands/types/step'; +import { deserialize, deserializeJsonArray } from '../transport/deserialize'; +import { Camera } from './camera'; +import { CharacterView } from './character-view'; + +export class GameObjectContainer extends CommandReceiver { + protected objects: Map = new Map(); + public player: CharacterView; + public camera: Camera; + + protected commandExecutors: CommandExecutors = { + [CreatePlayerCommand.type]: (c: CreatePlayerCommand) => { + this.player = deserialize(c.serializedPlayer) as CharacterView; + this.camera = new Camera(); + this.addObject(this.player); + this.addObject(this.camera); + }, + + [StepCommand.type]: (c: StepCommand) => { + if (this.player) { + this.camera.center = this.player.position; + } + }, + + [CreateObjectsCommand.type]: (c: CreateObjectsCommand) => + deserializeJsonArray(c.serializedObjects).forEach((o) => this.addObject(o)), + + [DeleteObjectsCommand.type]: (c: DeleteObjectsCommand) => + c.ids.forEach((id: Id) => this.objects.delete(id)), + + [UpdateObjectsCommand.type]: (c: UpdateObjectsCommand) => + deserializeJsonArray(c.serializedObjects).forEach((o) => { + this.objects.delete(o.id); + this.addObject(o); + if (o.id === this.player.id) { + this.player = o as CharacterView; + } + }), + }; + + protected defaultCommandExecutor(c: Command) { + this.objects.forEach((o) => o.sendCommand(c)); + } + + public sendCommandToSingleObject(id: Id, e: Command) { + this.objects.get(id)!.sendCommand(e); + } + + private addObject(object: GameObject) { + this.objects.set(object.id, object); + } +} diff --git a/frontend/src/scripts/objects/game-object.ts b/frontend/src/scripts/objects/game-object.ts deleted file mode 100644 index 9a7f987..0000000 --- a/frontend/src/scripts/objects/game-object.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { Command } from '../commands/command'; -import { CommandReceiver } from '../commands/command-receiver'; -import { IdentityManager } from '../identity/identity-manager'; - -export abstract class GameObject implements CommandReceiver { - public readonly id = IdentityManager.generateId(); - - private commandExecutors: { - [commandType: string]: (e: Command) => void; - } = {}; - - public reactsToCommand(commandType: string): boolean { - return Object.prototype.hasOwnProperty.call(this.commandExecutors, commandType); - } - - public sendCommand(command: Command) { - const commandType = command.type; - - if (Object.prototype.hasOwnProperty.call(this.commandExecutors, commandType)) { - this.commandExecutors[commandType](command); - } - } - - // can only be called inside the constructor - protected addCommandExecutor( - commandType: new () => T, - handler: (command: T) => void - ) { - this.commandExecutors[new commandType().type] = handler; - } -} diff --git a/frontend/src/scripts/objects/lamp-view.ts b/frontend/src/scripts/objects/lamp-view.ts new file mode 100644 index 0000000..a2b1045 --- /dev/null +++ b/frontend/src/scripts/objects/lamp-view.ts @@ -0,0 +1,17 @@ +import { vec2, vec3 } from 'gl-matrix'; +import { CircleLight } from 'sdf-2d'; +import { CommandExecutors, Id, LampBase } from 'shared'; +import { RenderCommand } from '../commands/types/render'; + +export class LampView extends LampBase { + private light: CircleLight; + + protected commandExecutors: CommandExecutors = { + [RenderCommand.type]: (c: RenderCommand) => c.renderer.addDrawable(this.light), + } as any; + + constructor(id: Id, center: vec2, color: vec3, lightness: number) { + super(id, center, color, lightness); + this.light = new CircleLight(center, color, lightness); + } +} diff --git a/frontend/src/scripts/objects/objects.ts b/frontend/src/scripts/objects/objects.ts deleted file mode 100644 index 7d1fe21..0000000 --- a/frontend/src/scripts/objects/objects.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { Command } from '../commands/command'; -import { CommandReceiver } from '../commands/command-receiver'; -import { Id } from '../identity/identity'; -import { GameObject } from './game-object'; - -export class Objects implements CommandReceiver { - private objects: Map = new Map(); - - private objectsGroupedByAbilities: Map> = new Map(); - - public addObject(o: GameObject) { - this.objects.set(o.id, o); - - for (const command of this.objectsGroupedByAbilities.keys()) { - if (o.reactsToCommand(command)) { - this.objectsGroupedByAbilities.get(command).push(o); - } - } - } - - public removeObject(o: GameObject) { - this.objects.delete(o.id); - - for (const command of this.objectsGroupedByAbilities.keys()) { - if (o.reactsToCommand(command)) { - const array = this.objectsGroupedByAbilities.get(command); - array.splice( - array.findIndex((i) => i.id == o.id), - 1 - ); - } - } - } - - public sendCommandToSingleObject(id: Id, e: Command) { - this.objects.get(id)?.sendCommand(e); - } - - public sendCommand(e: Command) { - if (!this.objectsGroupedByAbilities.has(e.type)) { - this.createGroupForCommand(e.type); - } - - this.objectsGroupedByAbilities.get(e.type).forEach((o, _) => o.sendCommand(e)); - } - - private createGroupForCommand(commandType: string) { - const objectsReactingToCommand = []; - - this.objects.forEach((o, _) => { - if (o.reactsToCommand(commandType)) { - objectsReactingToCommand.push(o); - } - }); - - this.objectsGroupedByAbilities.set(commandType, objectsReactingToCommand); - } -} diff --git a/frontend/src/scripts/objects/tunnel-view.ts b/frontend/src/scripts/objects/tunnel-view.ts new file mode 100644 index 0000000..ecd6638 --- /dev/null +++ b/frontend/src/scripts/objects/tunnel-view.ts @@ -0,0 +1,17 @@ +import { vec2 } from 'gl-matrix'; +import { InvertedTunnel } from 'sdf-2d'; +import { CommandExecutors, Id, TunnelBase } from 'shared'; +import { RenderCommand } from '../commands/types/render'; + +export class TunnelView extends TunnelBase { + private shape: InvertedTunnel; + + protected commandExecutors: CommandExecutors = { + [RenderCommand.type]: (c: RenderCommand) => c.renderer.addDrawable(this.shape), + } as any; + + constructor(id: Id, from: vec2, to: vec2, fromRadius: number, toRadius: number) { + super(id, from, to, fromRadius, toRadius); + this.shape = new InvertedTunnel(from, to, fromRadius, toRadius); + } +} diff --git a/frontend/src/scripts/objects/types/camera.ts b/frontend/src/scripts/objects/types/camera.ts deleted file mode 100644 index 13c5948..0000000 --- a/frontend/src/scripts/objects/types/camera.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { vec2 } from 'gl-matrix'; -import { MoveToCommand } from '../../commands/move-to'; -import { RenderCommand } from '../../commands/render'; -import { CursorMoveCommand } from '../../input/commands/cursor-move-command'; -import { ZoomCommand } from '../../input/commands/zoom'; -import { BoundingBox } from '../../physics/bounds/bounding-box'; -import { GameObject } from '../game-object'; - -export class Camera extends GameObject { - private inViewAreaSize = 1920 * 1080 * 2; - private cursorPosition = vec2.create(); - - private _viewArea: BoundingBox; - - constructor() { - super(); - - this._viewArea = new BoundingBox(null); - - this.addCommandExecutor(RenderCommand, this.draw.bind(this)); - this.addCommandExecutor(MoveToCommand, this.moveTo.bind(this)); - this.addCommandExecutor(CursorMoveCommand, this.setCursorPosition.bind(this)); - this.addCommandExecutor(ZoomCommand, this.zoom.bind(this)); - } - - public get viewArea(): BoundingBox { - return this._viewArea; - } - - private draw(c: RenderCommand) { - const canvasAspectRatio = c.renderer.canvasSize.x / c.renderer.canvasSize.y; - - this.viewArea.size = vec2.fromValues( - Math.sqrt(this.inViewAreaSize * canvasAspectRatio), - Math.sqrt(this.inViewAreaSize / canvasAspectRatio) - ); - - c.renderer.setViewArea(this._viewArea.topLeft, this.viewArea.size); - //c.renderer.setCursorPosition(this.cursorPosition); - } - - private moveTo(c: MoveToCommand) { - this._viewArea.topLeft = vec2.fromValues( - c.position.x - this._viewArea.size.x / 2, - c.position.y + this._viewArea.size.y / 2 - ); - } - - private zoom(c: ZoomCommand) { - this.inViewAreaSize *= c.factor; - } - - private setCursorPosition(c: CursorMoveCommand) { - this.cursorPosition = c.position; - } -} diff --git a/frontend/src/scripts/objects/types/character.ts b/frontend/src/scripts/objects/types/character.ts index 6d1c3ed..10da8c0 100644 --- a/frontend/src/scripts/objects/types/character.ts +++ b/frontend/src/scripts/objects/types/character.ts @@ -1,25 +1,17 @@ -import { vec2 } from 'gl-matrix'; +/*import { vec2 } from 'gl-matrix'; import { Flashlight } from 'sdf-2d'; -import { RenderCommand } from '../../commands/render'; -import { StepCommand } from '../../commands/step'; -import { TeleportToCommand } from '../../commands/teleport-to'; import { rgb } from '../../helper/rgb'; import { IGame } from '../../i-game'; -import { CursorMoveCommand } from '../../input/commands/cursor-move-command'; -import { KeyDownCommand } from '../../input/commands/key-down'; -import { KeyUpCommand } from '../../input/commands/key-up'; -import { PrimaryActionCommand } from '../../input/commands/primary-action'; -import { SwipeCommand } from '../../input/commands/swipe'; import { BoundingBoxBase } from '../../physics/bounds/bounding-box-base'; import { BoundingCircle } from '../../physics/bounds/bounding-circle'; import { PhysicsCircle } from '../../physics/bounds/physics-circle'; -import { DynamicPhysicalObject } from '../../physics/dynamic-physical-object'; +import { PhysicalGameObject } from '../../physics/physical-object'; import { Physics } from '../../physics/physics'; import { settings } from '../../settings'; import { BlobShape } from '../../shapes/blob-shape'; import { Projectile } from './projectile'; -export class Character extends DynamicPhysicalObject { +export class Character extends PhysicalGameObject { protected head: PhysicsCircle; protected leftFoot: PhysicsCircle; protected rightFoot: PhysicsCircle; @@ -69,7 +61,7 @@ export class Character extends DynamicPhysicalObject { this.shape.setCircles([this.head, this.leftFoot, this.rightFoot]); - this.addToPhysics(); + this.addToPhysics(true); } public distance(target: vec2): number { @@ -186,3 +178,4 @@ export class Character extends DynamicPhysicalObject { ); } } +*/ diff --git a/frontend/src/scripts/objects/types/lamp.ts b/frontend/src/scripts/objects/types/lamp.ts deleted file mode 100644 index e961cd4..0000000 --- a/frontend/src/scripts/objects/types/lamp.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { vec2, vec3 } from 'gl-matrix'; -import { CircleLight } from 'sdf-2d'; -import { MoveToCommand } from '../../commands/move-to'; -import { RenderCommand } from '../../commands/render'; -import { BoundingBoxBase } from '../../physics/bounds/bounding-box-base'; -import { ImmutableBoundingBox } from '../../physics/bounds/immutable-bounding-box'; -import { PhysicalObject } from '../../physics/physical-object'; -import { Physics } from '../../physics/physics'; -import { settings } from '../../settings'; - -export class Lamp extends PhysicalObject { - private light: CircleLight; - - constructor(center: vec2, color: vec3, lightness: number, physics: Physics) { - super(physics, false); - - this.light = new CircleLight(center, color, lightness); - this.addToPhysics(); - - this.addCommandExecutor(RenderCommand, this.draw.bind(this)); - this.addCommandExecutor(MoveToCommand, this.moveTo.bind(this)); - } - - public distance(target: vec2): number { - return this.light.minDistance(target); - } - - public getBoundingBox(): BoundingBoxBase { - return new ImmutableBoundingBox( - this, - this.light.center.x - settings.lightCutoffDistance, - this.light.center.x + settings.lightCutoffDistance, - this.light.center.y - settings.lightCutoffDistance, - this.light.center.y + settings.lightCutoffDistance - ); - } - - private draw(c: RenderCommand) { - c.renderer.addDrawable(this.light); - } - - private moveTo(c: MoveToCommand) { - this.light.center = c.position; - } -} diff --git a/frontend/src/scripts/objects/types/projectile.ts b/frontend/src/scripts/objects/types/projectile.ts index 6fbef3e..ea5bfd6 100644 --- a/frontend/src/scripts/objects/types/projectile.ts +++ b/frontend/src/scripts/objects/types/projectile.ts @@ -1,4 +1,4 @@ -import { vec2 } from 'gl-matrix'; +/*import { vec2 } from 'gl-matrix'; import { Circle } from 'sdf-2d'; import { RenderCommand } from '../../commands/render'; import { StepCommand } from '../../commands/step'; @@ -6,11 +6,11 @@ import { IGame } from '../../i-game'; import { BoundingBoxBase } from '../../physics/bounds/bounding-box-base'; import { BoundingCircle } from '../../physics/bounds/bounding-circle'; import { PhysicsCircle } from '../../physics/bounds/physics-circle'; -import { DynamicPhysicalObject } from '../../physics/dynamic-physical-object'; +import { PhysicalGameObject } from '../../physics/physical-object'; import { Physics } from '../../physics/physics'; import { settings } from '../../settings'; -export class Projectile extends DynamicPhysicalObject { +export class Projectile extends PhysicalGameObject { private shape: Circle; private bounding: PhysicsCircle; @@ -25,7 +25,7 @@ export class Projectile extends DynamicPhysicalObject { this.addCommandExecutor(RenderCommand, this.draw.bind(this)); this.addCommandExecutor(StepCommand, this.step.bind(this)); - this.addToPhysics(); + this.addToPhysics(true); } private draw(c: RenderCommand) { @@ -52,3 +52,4 @@ export class Projectile extends DynamicPhysicalObject { return this.bounding.distance(target); } } +*/ diff --git a/frontend/src/scripts/objects/types/tunnel.ts b/frontend/src/scripts/objects/types/tunnel.ts deleted file mode 100644 index 57bfd6f..0000000 --- a/frontend/src/scripts/objects/types/tunnel.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { vec2 } from 'gl-matrix'; -import { InvertedTunnel } from 'sdf-2d'; -import { RenderCommand } from '../../commands/render'; -import { BoundingBoxBase } from '../../physics/bounds/bounding-box-base'; -import { ImmutableBoundingBox } from '../../physics/bounds/immutable-bounding-box'; -import { Physics } from '../../physics/physics'; -import { StaticPhysicalObject } from '../../physics/static-physical-object'; - -export class Tunnel extends StaticPhysicalObject { - public readonly isInverted = true; - private shape: InvertedTunnel; - - constructor( - physics: Physics, - from: vec2, - to: vec2, - fromRadius: number, - toRadius: number - ) { - super(physics, true); - this.shape = new InvertedTunnel(from, to, fromRadius, toRadius); - this.addToPhysics(); - - this.addCommandExecutor(RenderCommand, this.draw.bind(this)); - } - - public distance(target: vec2): number { - return this.shape.minDistance(target); - } - - public getBoundingBox(): BoundingBoxBase { - const xMin = Math.min( - this.shape.from.x - this.shape.fromRadius, - this.shape.to.x - this.shape.toRadius - ); - const yMin = Math.min( - this.shape.from.y - this.shape.fromRadius, - this.shape.to.y - this.shape.toRadius - ); - const xMax = Math.max( - this.shape.from.x + this.shape.fromRadius, - this.shape.to.x + this.shape.toRadius - ); - const yMax = Math.max( - this.shape.from.y + this.shape.fromRadius, - this.shape.to.y + this.shape.toRadius - ); - return new ImmutableBoundingBox(this, xMin, xMax, yMin, yMax, true); - } - - private draw(c: RenderCommand) { - c.renderer.addDrawable(this.shape); - } -} diff --git a/frontend/src/scripts/physics/bounds/physics-circle.ts b/frontend/src/scripts/physics/bounds/physics-circle.ts index eddce28..9c2d39e 100644 --- a/frontend/src/scripts/physics/bounds/physics-circle.ts +++ b/frontend/src/scripts/physics/bounds/physics-circle.ts @@ -1,7 +1,7 @@ -import { vec2 } from 'gl-matrix'; +/*import { vec2 } from 'gl-matrix'; import { clamp } from '../../helper/clamp'; import { settings } from '../../settings'; -import { PhysicalObject } from '../physical-object'; +import { PhysicalGameObject } from '../physical-object'; import { Physics } from '../physics'; import { BoundingCircle } from './bounding-circle'; @@ -15,7 +15,7 @@ export class PhysicsCircle extends BoundingCircle { constructor( private readonly physics: Physics, - owner: PhysicalObject, + owner: PhysicalGameObject, center: vec2, radius: number ) { @@ -71,3 +71,4 @@ export class PhysicsCircle extends BoundingCircle { return wasHit; } } +*/ diff --git a/frontend/src/scripts/physics/dynamic-physical-object.ts b/frontend/src/scripts/physics/dynamic-physical-object.ts deleted file mode 100644 index b697b47..0000000 --- a/frontend/src/scripts/physics/dynamic-physical-object.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { BoundingCircle } from './bounds/bounding-circle'; -import { PhysicalObject } from './physical-object'; - -export abstract class DynamicPhysicalObject extends PhysicalObject { - public abstract getBoundingCircles(): Array; - - protected addToPhysics() { - super.addToPhysics(true); - } -} diff --git a/frontend/src/scripts/physics/physical-object.ts b/frontend/src/scripts/physics/physical-object.ts index 5409a8a..49e0bb9 100644 --- a/frontend/src/scripts/physics/physical-object.ts +++ b/frontend/src/scripts/physics/physical-object.ts @@ -1,9 +1,9 @@ -import { vec2 } from 'gl-matrix'; +/*import { vec2 } from 'gl-matrix'; import { GameObject } from '../objects/game-object'; import { BoundingBoxBase } from './bounds/bounding-box-base'; import { Physics } from './physics'; -export abstract class PhysicalObject extends GameObject { +export abstract class PhysicalGameObject extends GameObject { public abstract getBoundingBox(): BoundingBoxBase; public abstract distance(target: vec2): number; public readonly isInverted: boolean = false; @@ -12,7 +12,7 @@ export abstract class PhysicalObject extends GameObject { super(); } - protected addToPhysics(isDynamic = false) { + protected addToPhysics(isDynamic: boolean) { if (isDynamic) { this.physics.addDynamicBoundingBox(this.getBoundingBox()); } else { @@ -20,3 +20,4 @@ export abstract class PhysicalObject extends GameObject { } } } +*/ diff --git a/frontend/src/scripts/physics/physics.ts b/frontend/src/scripts/physics/physics.ts index 345b5a8..8ae44f5 100644 --- a/frontend/src/scripts/physics/physics.ts +++ b/frontend/src/scripts/physics/physics.ts @@ -1,4 +1,4 @@ -import { vec2 } from 'gl-matrix'; +/*import { vec2 } from 'gl-matrix'; import { rotate90Deg } from '../helper/rotate-90-deg'; import { settings } from '../settings'; import { BoundingBoxBase } from './bounds/bounding-box-base'; @@ -118,3 +118,4 @@ export class Physics { this.isTreeInitialized = true; } } +*/ diff --git a/frontend/src/scripts/physics/static-physical-object.ts b/frontend/src/scripts/physics/static-physical-object.ts deleted file mode 100644 index cf27574..0000000 --- a/frontend/src/scripts/physics/static-physical-object.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { PhysicalObject } from './physical-object'; - -export abstract class StaticPhysicalObject extends PhysicalObject { - protected addToPhysics() { - super.addToPhysics(false); - } -} diff --git a/frontend/src/scripts/shapes/blob-shape.ts b/frontend/src/scripts/shapes/blob-shape.ts index e4224e5..6be10e5 100644 --- a/frontend/src/scripts/shapes/blob-shape.ts +++ b/frontend/src/scripts/shapes/blob-shape.ts @@ -1,6 +1,6 @@ import { mat2d, vec2 } from 'gl-matrix'; import { Drawable, DrawableDescriptor } from 'sdf-2d'; -import { BoundingCircle } from '../physics/bounds/bounding-circle'; +import { Circle } from 'shared'; export class BlobShape extends Drawable { public static descriptor: DrawableDescriptor = { @@ -80,35 +80,33 @@ export class BlobShape extends Drawable { empty: new BlobShape(), }; - protected head: BoundingCircle; - protected leftFoot: BoundingCircle; - protected rightFoot: BoundingCircle; + protected head: Circle; + protected leftFoot: Circle; + protected rightFoot: Circle; public constructor() { super(); - const circle = new BoundingCircle(null, vec2.create(), 200); + const circle = new Circle(vec2.create(), 200); this.setCircles([circle, circle, circle]); } - public setCircles([head, leftFoot, rightFoot]: [ - BoundingCircle, - BoundingCircle, - BoundingCircle - ]) { + public setCircles([head, leftFoot, rightFoot]: [Circle, Circle, Circle]) { this.head = head; this.leftFoot = leftFoot; this.rightFoot = rightFoot; } public minDistance(target: vec2): number { - return ( + // todo + return 0; + /*return ( Math.min( this.head.distance(target), this.leftFoot.distance(target), this.rightFoot.distance(target) ) / 2 - ); + );*/ } protected getObjectToSerialize(transform2d: mat2d, transform1d: number): any { diff --git a/frontend/src/scripts/transport/deserialize.ts b/frontend/src/scripts/transport/deserialize.ts new file mode 100644 index 0000000..5705673 --- /dev/null +++ b/frontend/src/scripts/transport/deserialize.ts @@ -0,0 +1,28 @@ +import { + CharacterBase, + commandConstructors, + GameObject, + LampBase, + TunnelBase, +} from 'shared'; +import { CharacterView } from '../objects/character-view'; +import { LampView } from '../objects/lamp-view'; +import { TunnelView } from '../objects/tunnel-view'; + +const constructors: { [type: string]: new (...values: Array) => GameObject } = { + [TunnelBase.type]: TunnelView, + [LampBase.type]: LampView, + [CharacterBase.type]: CharacterView, + ...commandConstructors, +}; + +export const deserialize = (json: string): GameObject => { + const [type, ...values] = JSON.parse(json); + return new constructors[type](...values); +}; + +export const deserializeJsonArray = (json: string): Array => { + return (JSON.parse(json) as Array).map( + ([type, ...values]) => new constructors[type](...values) + ); +}; diff --git a/shared/.eslintignore b/shared/.eslintignore new file mode 100644 index 0000000..2cb7d2a --- /dev/null +++ b/shared/.eslintignore @@ -0,0 +1 @@ +**/*.js diff --git a/shared/.eslintrc.json b/shared/.eslintrc.json new file mode 100644 index 0000000..8275ed8 --- /dev/null +++ b/shared/.eslintrc.json @@ -0,0 +1,29 @@ +{ + "root": true, + "env": { + "browser": true, + "es2020": true + }, + "extends": [ + "eslint:recommended", + "plugin:@typescript-eslint/eslint-recommended", + "plugin:@typescript-eslint/recommended", + "prettier", + "prettier/@typescript-eslint" + ], + "parser": "@typescript-eslint/parser", + "parserOptions": { + "ecmaVersion": 11, + "sourceType": "module" + }, + "plugins": ["unused-imports", "@typescript-eslint", "prettier"], + "rules": { + "prettier/prettier": "error", + "no-unused-vars": "off", + "unused-imports/no-unused-imports-ts": "error", + "@typescript-eslint/no-unused-vars": ["warn", { "argsIgnorePattern": "^_" }], + "@typescript-eslint/no-explicit-any": "off", + "@typescript-eslint/explicit-module-boundary-types": "off", + "@typescript-eslint/no-non-null-assertion": "off" + } +} diff --git a/shared/.gitignore b/shared/.gitignore new file mode 100644 index 0000000..bf0cd0d --- /dev/null +++ b/shared/.gitignore @@ -0,0 +1,4 @@ +dist +node_modules +package-lock.json +.firebase diff --git a/shared/.prettierrc b/shared/.prettierrc new file mode 100644 index 0000000..5aae580 --- /dev/null +++ b/shared/.prettierrc @@ -0,0 +1,7 @@ +{ + "trailingComma": "es5", + "printWidth": 90, + "tabWidth": 2, + "singleQuote": true, + "endOfLine": "lf" +} diff --git a/shared/firebase.json b/shared/firebase.json new file mode 100644 index 0000000..2c33c29 --- /dev/null +++ b/shared/firebase.json @@ -0,0 +1,16 @@ +{ + "hosting": { + "public": "dist", + "ignore": [ + "firebase.json", + "**/.*", + "**/node_modules/**" + ], + "rewrites": [ + { + "source": "**", + "destination": "/index.html" + } + ] + } +} diff --git a/shared/package.json b/shared/package.json new file mode 100644 index 0000000..9e28138 --- /dev/null +++ b/shared/package.json @@ -0,0 +1,17 @@ +{ + "name": "shared", + "version": "0.0.0", + "description": "Shared library between backend and frontend", + "main": "src/main.ts", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "author": "", + "license": "ISC", + "devDependencies": { + "@types/gl-matrix": "^3.2.0", + "@types/uuid": "^8.0.0", + "gl-matrix": "^3.3.0", + "uuid": "^8.2.0" + } +} diff --git a/shared/src/commands/broadcast-commands.ts b/shared/src/commands/broadcast-commands.ts new file mode 100644 index 0000000..773f58c --- /dev/null +++ b/shared/src/commands/broadcast-commands.ts @@ -0,0 +1,7 @@ +import { CommandReceiver } from './command-receiver'; +import { CommandGenerator } from './command-generator'; + +export const broadcastCommands = ( + commandGenerators: Array, + commandReceivers: Array +) => commandReceivers.forEach((r) => commandGenerators.forEach((g) => g.subscribe(r))); diff --git a/frontend/src/scripts/commands/command-generator.ts b/shared/src/commands/command-generator.ts similarity index 84% rename from frontend/src/scripts/commands/command-generator.ts rename to shared/src/commands/command-generator.ts index 01fb65b..b4bf67d 100644 --- a/frontend/src/scripts/commands/command-generator.ts +++ b/shared/src/commands/command-generator.ts @@ -8,7 +8,7 @@ export class CommandGenerator { this.subscribers.push(subscriber); } - protected sendCommand(command: Command): void { + protected sendCommandToSubcribers(command: Command): void { this.subscribers.forEach((s) => s.sendCommand(command)); } } diff --git a/shared/src/commands/command-receiver.ts b/shared/src/commands/command-receiver.ts new file mode 100644 index 0000000..139dcfd --- /dev/null +++ b/shared/src/commands/command-receiver.ts @@ -0,0 +1,25 @@ +import { Command } from './command'; + +export type CommandExecutors = { + [type: string]: (command: Command) => void; +}; + +export abstract class CommandReceiver { + protected commandExecutors: CommandExecutors = {}; + + protected defaultCommandExecutor(command: Command) {} + + public reactsToCommand(commandType: string): boolean { + return Object.prototype.hasOwnProperty.call(this.commandExecutors, commandType); + } + + public sendCommand(command: Command) { + const commandType = command.type; + + if (Object.prototype.hasOwnProperty.call(this.commandExecutors, commandType)) { + this.commandExecutors[commandType](command); + } else { + this.defaultCommandExecutor(command); + } + } +} diff --git a/shared/src/commands/command.ts b/shared/src/commands/command.ts new file mode 100644 index 0000000..b835dd5 --- /dev/null +++ b/shared/src/commands/command.ts @@ -0,0 +1,7 @@ +import { Id } from '../transport/identity'; + +export abstract class Command { + public get type(): string { + return (this as any).constructor.type; + } +} diff --git a/shared/src/commands/types/create-objects.ts b/shared/src/commands/types/create-objects.ts new file mode 100644 index 0000000..7d4aa59 --- /dev/null +++ b/shared/src/commands/types/create-objects.ts @@ -0,0 +1,13 @@ +import { Command } from '../command'; + +export class CreateObjectsCommand extends Command { + public static readonly type = 'CreateObjectsCommand'; + + public constructor(public readonly serializedObjects: string) { + super(); + } + + public toJSON(): any { + return [this.type, this.serializedObjects]; + } +} diff --git a/shared/src/commands/types/create-player.ts b/shared/src/commands/types/create-player.ts new file mode 100644 index 0000000..6dce44e --- /dev/null +++ b/shared/src/commands/types/create-player.ts @@ -0,0 +1,13 @@ +import { Command } from '../command'; + +export class CreatePlayerCommand extends Command { + public static readonly type = 'CreatePlayerCommand'; + + public constructor(public readonly serializedPlayer: string) { + super(); + } + + public toJSON(): any { + return [this.type, this.serializedPlayer]; + } +} diff --git a/shared/src/commands/types/delete-objects.ts b/shared/src/commands/types/delete-objects.ts new file mode 100644 index 0000000..8d140f0 --- /dev/null +++ b/shared/src/commands/types/delete-objects.ts @@ -0,0 +1,14 @@ +import { Id } from '../../transport/identity'; +import { Command } from '../command'; + +export class DeleteObjectsCommand extends Command { + public static readonly type = 'DeleteObjectsCommand'; + + public constructor(public readonly ids: Array) { + super(); + } + + public toJSON(): any { + return [this.type, this.ids]; + } +} diff --git a/shared/src/commands/types/move-action.ts b/shared/src/commands/types/move-action.ts new file mode 100644 index 0000000..cde42ea --- /dev/null +++ b/shared/src/commands/types/move-action.ts @@ -0,0 +1,14 @@ +import { vec2 } from 'gl-matrix'; +import { Command } from '../command'; + +export class MoveActionCommand extends Command { + public static readonly type = 'MoveActionCommand'; + + public constructor(public readonly delta: vec2) { + super(); + } + + public toJSON(): any { + return [this.type, this.delta]; + } +} diff --git a/shared/src/commands/types/primary-action.ts b/shared/src/commands/types/primary-action.ts new file mode 100644 index 0000000..b57ddef --- /dev/null +++ b/shared/src/commands/types/primary-action.ts @@ -0,0 +1,14 @@ +import { vec2 } from 'gl-matrix'; +import { Command } from '../command'; + +export class PrimaryActionCommand extends Command { + public static readonly type = 'PrimaryActionCommand'; + + public constructor(public readonly position: vec2) { + super(); + } + + public toJSON(): any { + return [this.type, this.position]; + } +} diff --git a/shared/src/commands/types/secondary-action.ts b/shared/src/commands/types/secondary-action.ts new file mode 100644 index 0000000..d7bb9e4 --- /dev/null +++ b/shared/src/commands/types/secondary-action.ts @@ -0,0 +1,14 @@ +import { vec2 } from 'gl-matrix'; +import { Command } from '../command'; + +export class SecondaryActionCommand extends Command { + public static readonly type = 'SecondaryActionCommand'; + + public constructor(public readonly position: vec2) { + super(); + } + + public toJSON(): any { + return [this.type, this.position]; + } +} diff --git a/shared/src/commands/types/set-view-area-action.ts b/shared/src/commands/types/set-view-area-action.ts new file mode 100644 index 0000000..03ccc11 --- /dev/null +++ b/shared/src/commands/types/set-view-area-action.ts @@ -0,0 +1,14 @@ +import { Rectangle } from '../../helper/rectangle'; +import { Command } from '../command'; + +export class SetViewAreaActionCommand extends Command { + public static readonly type = 'SetViewAreaAction'; + + public constructor(public readonly viewArea: Rectangle) { + super(); + } + + public toJSON(): any { + return [this.type, this.viewArea]; + } +} diff --git a/shared/src/commands/types/ternary-action.ts b/shared/src/commands/types/ternary-action.ts new file mode 100644 index 0000000..9cd2d69 --- /dev/null +++ b/shared/src/commands/types/ternary-action.ts @@ -0,0 +1,14 @@ +import { vec2 } from 'gl-matrix'; +import { Command } from '../command'; + +export class TernaryActionCommand extends Command { + public static readonly type = 'TernaryActionCommand'; + + public constructor(public readonly position: vec2) { + super(); + } + + public toJSON(): any { + return [this.type, this.position]; + } +} diff --git a/shared/src/commands/types/update-objects.ts b/shared/src/commands/types/update-objects.ts new file mode 100644 index 0000000..31beece --- /dev/null +++ b/shared/src/commands/types/update-objects.ts @@ -0,0 +1,13 @@ +import { Command } from '../command'; + +export class UpdateObjectsCommand extends Command { + public static readonly type = 'UpdateObjectsCommand'; + + public constructor(public readonly serializedObjects: string) { + super(); + } + + public toJSON(): any { + return [this.type, this.serializedObjects]; + } +} diff --git a/shared/src/helper/array.ts b/shared/src/helper/array.ts new file mode 100644 index 0000000..5a0ff29 --- /dev/null +++ b/shared/src/helper/array.ts @@ -0,0 +1,31 @@ +declare global { + interface Array { + x: number; + y: number; + } + + interface Float32Array { + x: number; + y: number; + } +} + +const setIndexAlias = (name: string, index: number, type: any) => { + if (!Object.prototype.hasOwnProperty.call(type.prototype, name)) { + Object.defineProperty(type.prototype, name, { + get() { + return this[index]; + }, + set(value) { + this[index] = value; + }, + }); + } +}; + +export const applyArrayPlugins = () => { + setIndexAlias('x', 0, Array); + setIndexAlias('y', 1, Array); + setIndexAlias('x', 0, Float32Array); + setIndexAlias('y', 1, Float32Array); +}; diff --git a/shared/src/helper/circle.ts b/shared/src/helper/circle.ts new file mode 100644 index 0000000..39451c5 --- /dev/null +++ b/shared/src/helper/circle.ts @@ -0,0 +1,5 @@ +import { vec2 } from 'gl-matrix'; + +export class Circle { + constructor(public center: vec2, public radius: number) {} +} diff --git a/shared/src/helper/clamp.ts b/shared/src/helper/clamp.ts new file mode 100644 index 0000000..45da555 --- /dev/null +++ b/shared/src/helper/clamp.ts @@ -0,0 +1,4 @@ +export const clamp = (value: number, min: number, max: number): number => + Math.min(max, Math.max(min, value)); + +export const clamp01 = (value: number): number => Math.min(1, Math.max(0, value)); diff --git a/shared/src/helper/delta-time-calculator.ts b/shared/src/helper/delta-time-calculator.ts new file mode 100644 index 0000000..fb7686d --- /dev/null +++ b/shared/src/helper/delta-time-calculator.ts @@ -0,0 +1,23 @@ +export class DeltaTimeCalculator { + private previousTime: DOMHighResTimeStamp | null = null; + + constructor() { + document.addEventListener('visibilitychange', this.handleVisibilityChange.bind(this)); + } + + public getNextDeltaTime(currentTime: DOMHighResTimeStamp): DOMHighResTimeStamp { + if (this.previousTime === null) { + this.previousTime = currentTime; + } + + const delta = currentTime - this.previousTime; + this.previousTime = currentTime; + return delta; + } + + private handleVisibilityChange() { + if (!document.hidden) { + this.previousTime = null; + } + } +} diff --git a/shared/src/helper/last.ts b/shared/src/helper/last.ts new file mode 100644 index 0000000..5c319b0 --- /dev/null +++ b/shared/src/helper/last.ts @@ -0,0 +1,3 @@ +export function last(a: Array): T | null { + return a.length > 0 ? a[a.length - 1] : null; +} diff --git a/shared/src/helper/mix.ts b/shared/src/helper/mix.ts new file mode 100644 index 0000000..16a76ed --- /dev/null +++ b/shared/src/helper/mix.ts @@ -0,0 +1 @@ +export const mix = (from: number, to: number, q: number) => from + (to - from) * q; diff --git a/shared/src/helper/pretty-print.ts b/shared/src/helper/pretty-print.ts new file mode 100644 index 0000000..704a6fc --- /dev/null +++ b/shared/src/helper/pretty-print.ts @@ -0,0 +1,4 @@ +export const prettyPrint = (o: any): string => + JSON.stringify(o, (_, v) => (v.toFixed ? Number(v.toFixed(3)) : v), ' ') + .replace(/("|,|{|^\n)/g, '') + .replace(/(\W*}\n?)+/g, '\n\n'); diff --git a/shared/src/helper/random.ts b/shared/src/helper/random.ts new file mode 100644 index 0000000..10e93bb --- /dev/null +++ b/shared/src/helper/random.ts @@ -0,0 +1,18 @@ +// src +// https://stackoverflow.com/questions/521295/seeding-the-random-number-generator-in-javascript +// Mulberry32 + +export abstract class Random { + private static _seed = Math.random(); + + public static set seed(value: number) { + Random._seed = value; + } + + public static getRandom(): number { + let t = (Random._seed += 0x6d2b79f5); + t = Math.imul(t ^ (t >>> 15), t | 1); + t ^= t + Math.imul(t ^ (t >>> 7), t | 61); + return ((t ^ (t >>> 14)) >>> 0) / 4294967296; + } +} diff --git a/shared/src/helper/rectangle.ts b/shared/src/helper/rectangle.ts new file mode 100644 index 0000000..df856a6 --- /dev/null +++ b/shared/src/helper/rectangle.ts @@ -0,0 +1,5 @@ +import { vec2 } from 'gl-matrix'; + +export class Rectangle { + constructor(public topLeft = vec2.create(), public size = vec2.create()) {} +} diff --git a/shared/src/helper/rgb.ts b/shared/src/helper/rgb.ts new file mode 100644 index 0000000..a6ef20a --- /dev/null +++ b/shared/src/helper/rgb.ts @@ -0,0 +1,3 @@ +import { vec3 } from 'gl-matrix'; + +export const rgb = (r: number, g: number, b: number): vec3 => vec3.fromValues(r, g, b); diff --git a/shared/src/helper/rgb255.ts b/shared/src/helper/rgb255.ts new file mode 100644 index 0000000..56c47ff --- /dev/null +++ b/shared/src/helper/rgb255.ts @@ -0,0 +1,4 @@ +import { vec3 } from 'gl-matrix'; + +export const rgb255 = (r: number, g: number, b: number): vec3 => + vec3.fromValues(r / 255, g / 255, b / 255); diff --git a/shared/src/helper/rotate-90-deg.ts b/shared/src/helper/rotate-90-deg.ts new file mode 100644 index 0000000..1911a63 --- /dev/null +++ b/shared/src/helper/rotate-90-deg.ts @@ -0,0 +1,3 @@ +import { vec2 } from 'gl-matrix'; + +export const rotate90Deg = (vec: vec2): vec2 => vec2.fromValues(-vec.y, vec.x); diff --git a/shared/src/helper/to-percent.ts b/shared/src/helper/to-percent.ts new file mode 100644 index 0000000..0047591 --- /dev/null +++ b/shared/src/helper/to-percent.ts @@ -0,0 +1 @@ +export const toPercent = (value: number) => `${Math.round(value * 100)}%`; diff --git a/shared/src/helper/unique.ts b/shared/src/helper/unique.ts new file mode 100644 index 0000000..fdd153e --- /dev/null +++ b/shared/src/helper/unique.ts @@ -0,0 +1,5 @@ +let currentId = 0; + +export const id = (): number => { + return currentId++; +}; diff --git a/shared/src/helper/wait.ts b/shared/src/helper/wait.ts new file mode 100644 index 0000000..73dd2c2 --- /dev/null +++ b/shared/src/helper/wait.ts @@ -0,0 +1,3 @@ +export const wait = (ms: number): Promise => { + return new Promise((resolve, _) => setTimeout(resolve, ms)); +}; diff --git a/shared/src/main.ts b/shared/src/main.ts new file mode 100644 index 0000000..df570b0 --- /dev/null +++ b/shared/src/main.ts @@ -0,0 +1,29 @@ +export * from './commands/command'; +export * from './commands/types/create-objects'; +export * from './commands/types/create-player'; +export * from './commands/types/delete-objects'; +export * from './commands/types/update-objects'; +export * from './commands/types/ternary-action'; +export * from './commands/types/move-action'; +export * from './commands/types/set-view-area-action'; +export * from './commands/types/primary-action'; +export * from './commands/types/secondary-action'; +export * from './commands/command-receiver'; +export * from './commands/command-generator'; +export * from './commands/broadcast-commands'; +export * from './helper/array'; +export * from './helper/clamp'; +export * from './helper/circle'; +export * from './helper/rectangle'; +export * from './helper/mix'; +export * from './helper/random'; +export * from './helper/unique'; +export * from './helper/rotate-90-deg'; +export * from './objects/game-object'; +export * from './objects/deserialize'; +export * from './objects/types/character-base'; +export * from './objects/types/lamp-base'; +export * from './objects/types/tunnel-base'; +export * from './settings'; +export * from './transport/transport-events'; +export * from './transport/identity'; diff --git a/shared/src/objects/deserialize.ts b/shared/src/objects/deserialize.ts new file mode 100644 index 0000000..a6c3861 --- /dev/null +++ b/shared/src/objects/deserialize.ts @@ -0,0 +1,26 @@ +import { CreatePlayerCommand } from '../commands/types/create-player'; +import { MoveActionCommand } from '../commands/types/move-action'; +import { PrimaryActionCommand } from '../commands/types/primary-action'; +import { SecondaryActionCommand } from '../commands/types/secondary-action'; +import { SetViewAreaActionCommand } from '../commands/types/set-view-area-action'; +import { TernaryActionCommand } from '../commands/types/ternary-action'; +import { UpdateObjectsCommand } from '../commands/types/update-objects'; +import { Command, CreateObjectsCommand, DeleteObjectsCommand } from '../main'; + +export const commandConstructors: { + [type: string]: new (...values: Array) => any; +} = { + [CreateObjectsCommand.type]: CreateObjectsCommand, + [DeleteObjectsCommand.type]: DeleteObjectsCommand, + [CreatePlayerCommand.type]: CreatePlayerCommand, + [MoveActionCommand.type]: MoveActionCommand, + [PrimaryActionCommand.type]: PrimaryActionCommand, + [SecondaryActionCommand.type]: SecondaryActionCommand, + [TernaryActionCommand.type]: TernaryActionCommand, + [SetViewAreaActionCommand.type]: SetViewAreaActionCommand, + [UpdateObjectsCommand.type]: UpdateObjectsCommand, +}; + +export const deserializeCommand = ([type, ...values]: [string, Array]): Command => { + return new commandConstructors[type](...values); +}; diff --git a/shared/src/objects/game-object.ts b/shared/src/objects/game-object.ts new file mode 100644 index 0000000..468907a --- /dev/null +++ b/shared/src/objects/game-object.ts @@ -0,0 +1,12 @@ +import { CommandReceiver } from '../commands/command-receiver'; +import { Id } from '../transport/identity'; + +export abstract class GameObject extends CommandReceiver { + public get type(): string { + return (this as any).constructor.type; + } + + constructor(public readonly id: Id) { + super(); + } +} diff --git a/shared/src/objects/types/character-base.ts b/shared/src/objects/types/character-base.ts new file mode 100644 index 0000000..88a2ed7 --- /dev/null +++ b/shared/src/objects/types/character-base.ts @@ -0,0 +1,17 @@ +import { vec2 } from 'gl-matrix'; +import { Circle } from '../../helper/circle'; +import { Id } from '../../main'; +import { GameObject } from '../game-object'; + +export abstract class CharacterBase extends GameObject { + public static readonly type = 'Character'; + + constructor( + id: Id, + public head: Circle, + public leftFoot: Circle, + public rightFoot: Circle + ) { + super(id); + } +} diff --git a/shared/src/objects/types/lamp-base.ts b/shared/src/objects/types/lamp-base.ts new file mode 100644 index 0000000..1552478 --- /dev/null +++ b/shared/src/objects/types/lamp-base.ts @@ -0,0 +1,11 @@ +import { vec2, vec3 } from 'gl-matrix'; +import { Id } from '../../main'; +import { GameObject } from '../game-object'; + +export abstract class LampBase extends GameObject { + public static readonly type = 'Lamp'; + + constructor(id: Id, public center: vec2, public color: vec3, public lightness: number) { + super(id); + } +} diff --git a/shared/src/objects/types/tunnel-base.ts b/shared/src/objects/types/tunnel-base.ts new file mode 100644 index 0000000..f227d83 --- /dev/null +++ b/shared/src/objects/types/tunnel-base.ts @@ -0,0 +1,17 @@ +import { vec2 } from 'gl-matrix'; +import { GameObject, Id } from '../../main'; +import { typed } from '../../transport/typed'; + +export abstract class TunnelBase extends GameObject { + public static readonly type = 'Tunnel'; + + constructor( + id: Id, + public readonly from: vec2, + public readonly to: vec2, + public readonly fromRadius: number, + public readonly toRadius: number + ) { + super(id); + } +} diff --git a/shared/src/settings.ts b/shared/src/settings.ts new file mode 100644 index 0000000..e3c10a0 --- /dev/null +++ b/shared/src/settings.ts @@ -0,0 +1,13 @@ +import { vec2 } from 'gl-matrix'; + +export const settings = { + lightCutoffDistance: 600, + hitDetectionCirclePointCount: 32, + hitDetectionMaxOverlap: 0.01, + physicsMaxStep: 5, + gravitationalForce: vec2.fromValues(0, -0.015), + maxVelocityX: 1.5, + maxVelocityY: 8, + velocityAttenuation: 0.99, + inViewAreaSize: 1920 * 1080 * 3, +}; diff --git a/frontend/src/scripts/identity/identity-manager.ts b/shared/src/transport/identity-manager.ts similarity index 100% rename from frontend/src/scripts/identity/identity-manager.ts rename to shared/src/transport/identity-manager.ts diff --git a/shared/src/transport/identity.ts b/shared/src/transport/identity.ts new file mode 100644 index 0000000..f45be1b --- /dev/null +++ b/shared/src/transport/identity.ts @@ -0,0 +1 @@ +export type Id = number; diff --git a/shared/src/transport/transport-events.ts b/shared/src/transport/transport-events.ts index 1f4333e..6f10543 100644 --- a/shared/src/transport/transport-events.ts +++ b/shared/src/transport/transport-events.ts @@ -1,4 +1,5 @@ export enum TransportEvents { - PlayerJoining = "PlayerJoining", - PlayerSendingInfo = "PlayerSendingInfo", + PlayerJoining = 'PlayerJoining', + PlayerToServer = 'PlayerToServer', + ServerToPlayer = 'ServerToPlayer', } diff --git a/shared/src/transport/typed.ts b/shared/src/transport/typed.ts new file mode 100644 index 0000000..c2bdc7d --- /dev/null +++ b/shared/src/transport/typed.ts @@ -0,0 +1,11 @@ +/** + * static type: string must be implemented + * @param constructor + */ +export const typed = (constructor: T) => { + return class extends constructor { + public get type(): string { + return (this as any).constructor.type; + } + }; +}; diff --git a/shared/tsconfig.json b/shared/tsconfig.json new file mode 100644 index 0000000..3ce307e --- /dev/null +++ b/shared/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "outDir": "./dist/", + "sourceMap": true, + "noImplicitAny": false, + "target": "es6", + "downlevelIteration": true, + "allowJs": true, + "experimentalDecorators": true, + "allowSyntheticDefaultImports": true, + "moduleResolution": "Node", + "module": "es6", + "lib": ["es2015", "dom"] + } +}