Add basic multiplayer

This commit is contained in:
schmelczerandras 2020-10-05 19:07:17 +02:00
parent 0f0a1eaf67
commit 46a48e7c15
113 changed files with 1362 additions and 754 deletions

View file

@ -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"
}
}

View file

@ -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();
});
});

View file

@ -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;
}

View file

@ -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];
}
}

View file

@ -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 };
}
}

View file

@ -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];
}
}

View file

@ -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];
}
}

View file

@ -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,

View file

@ -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<ImmutableBoundingBox> = [];
private staticBoundingBoxes = new BoundingBoxTree();
private dynamicBoundingBoxes = new BoundingBoxList();
protected objects: Map<Id, GameObject> = new Map();
private objectsGroupedByAbilities: Map<string, Array<GameObject>> = 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<PhysicalGameObject> {
return [
...this.staticBoundingBoxes.findIntersecting(box),
...this.dynamicBoundingBoxes.findIntersecting(box),
].map((b) => b.owner);
}
}

View file

@ -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;
}

View file

@ -1,13 +0,0 @@
import { Player } from './player';
export class PlayerContainer {
private socketIdToPlayer = new Map<string, Player>();
public addPlayer(player: Player) {
this.socketIdToPlayer.set(player.socketId, player);
}
public removePlayerBySocketId(socketId: string) {
this.socketIdToPlayer.delete(socketId);
}
}

View file

@ -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<PhysicalGameObject> = [];
private objectsInViewArea: Array<PhysicalGameObject> = [];
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);
}
}

2
backend/src/serialize.ts Normal file
View file

@ -0,0 +1,2 @@
export const jsonSerialize = (o: any): string =>
JSON.stringify(o, (key, value) => (value?.toFixed ? Number(value.toFixed(3)) : value));

View file

@ -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
}
}

View file

@ -11,7 +11,11 @@ module.exports = {
entry: {
main: [PATHS.entryPoint],
},
externals: [nodeExternals()],
externals: [
nodeExternals({
allowlist: [/(^shared)/],
}),
],
target: 'node',
output: {
filename: '[name].js',

View file

@ -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"
}
}

View file

@ -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);

View file

@ -1,11 +0,0 @@
import { CommandReceiver } from './command-receiver';
import { CommandGenerator } from './command-generator';
export class CommandBroadcaster {
constructor(
commandGenerators: Array<CommandGenerator>,
commandReceivers: Array<CommandReceiver>
) {
commandReceivers.forEach((r) => commandGenerators.forEach((g) => g.subscribe(r)));
}
}

View file

@ -1,5 +0,0 @@
import { Command } from './command';
export interface CommandReceiver {
sendCommand(command: Command): void;
}

View file

@ -1,6 +0,0 @@
import { Typed } from '../transport/serializable';
import { Id } from '../identity/identity';
export abstract class Command extends Typed {
target?: Id;
}

View file

@ -0,0 +1,42 @@
import { vec2 } from 'gl-matrix';
import { CommandGenerator, MoveActionCommand } from 'shared';
export class KeyboardListener extends CommandGenerator {
private keysDown: Set<string> = 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();
}
}

View file

@ -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);
}
}

View file

@ -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);
}
}

View file

@ -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';
}
}

View file

@ -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));
}
}

View file

@ -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';
}
}

View file

@ -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';
}
}

View file

@ -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';
}
}

View file

@ -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();
}
}

View file

@ -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();
}
}

View file

@ -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();
}
}

View file

@ -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<Renderer>;
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<void> {
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<void> {
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<void> {
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<void> {
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<BoundingBoxBase> {
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);
}
}

View file

@ -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<BoundingBoxBase>;
displayToWorldCoordinates(p: vec2): vec2;
addObject(o: GameObject): void;
removeObject(o: GameObject): void;
}

View file

@ -1 +0,0 @@
export type Id = string;

View file

@ -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';
}
}

View file

@ -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';
}
}

View file

@ -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';
}
}

View file

@ -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';
}
}

View file

@ -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';
}
}

View file

@ -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';
}
}

View file

@ -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';
}
}

View file

@ -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));
});
}
}

View file

@ -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);
}
}

View file

@ -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);
}
}

View file

@ -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);
}
}

View file

@ -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;
}
}

View file

@ -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<Id, GameObject> = 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);
}
}

View file

@ -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<T extends Command>(
commandType: new () => T,
handler: (command: T) => void
) {
this.commandExecutors[new commandType().type] = handler;
}
}

View file

@ -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);
}
}

View file

@ -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<Id, GameObject> = new Map();
private objectsGroupedByAbilities: Map<string, Array<GameObject>> = 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);
}
}

View file

@ -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);
}
}

View file

@ -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;
}
}

View file

@ -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 {
);
}
}
*/

View file

@ -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;
}
}

View file

@ -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);
}
}
*/

View file

@ -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);
}
}

View file

@ -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;
}
}
*/

View file

@ -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<BoundingCircle>;
protected addToPhysics() {
super.addToPhysics(true);
}
}

View file

@ -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 {
}
}
}
*/

View file

@ -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;
}
}
*/

View file

@ -1,7 +0,0 @@
import { PhysicalObject } from './physical-object';
export abstract class StaticPhysicalObject extends PhysicalObject {
protected addToPhysics() {
super.addToPhysics(false);
}
}

View file

@ -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 {

View file

@ -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<any>) => 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<GameObject> => {
return (JSON.parse(json) as Array<any>).map(
([type, ...values]) => new constructors[type](...values)
);
};

1
shared/.eslintignore Normal file
View file

@ -0,0 +1 @@
**/*.js

29
shared/.eslintrc.json Normal file
View file

@ -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"
}
}

4
shared/.gitignore vendored Normal file
View file

@ -0,0 +1,4 @@
dist
node_modules
package-lock.json
.firebase

7
shared/.prettierrc Normal file
View file

@ -0,0 +1,7 @@
{
"trailingComma": "es5",
"printWidth": 90,
"tabWidth": 2,
"singleQuote": true,
"endOfLine": "lf"
}

16
shared/firebase.json Normal file
View file

@ -0,0 +1,16 @@
{
"hosting": {
"public": "dist",
"ignore": [
"firebase.json",
"**/.*",
"**/node_modules/**"
],
"rewrites": [
{
"source": "**",
"destination": "/index.html"
}
]
}
}

17
shared/package.json Normal file
View file

@ -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"
}
}

View file

@ -0,0 +1,7 @@
import { CommandReceiver } from './command-receiver';
import { CommandGenerator } from './command-generator';
export const broadcastCommands = (
commandGenerators: Array<CommandGenerator>,
commandReceivers: Array<CommandReceiver>
) => commandReceivers.forEach((r) => commandGenerators.forEach((g) => g.subscribe(r)));

View file

@ -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));
}
}

View file

@ -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);
}
}
}

View file

@ -0,0 +1,7 @@
import { Id } from '../transport/identity';
export abstract class Command {
public get type(): string {
return (this as any).constructor.type;
}
}

View file

@ -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];
}
}

View file

@ -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];
}
}

View file

@ -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<Id>) {
super();
}
public toJSON(): any {
return [this.type, this.ids];
}
}

View file

@ -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];
}
}

View file

@ -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];
}
}

View file

@ -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];
}
}

View file

@ -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];
}
}

View file

@ -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];
}
}

View file

@ -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];
}
}

View file

@ -0,0 +1,31 @@
declare global {
interface Array<T> {
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);
};

View file

@ -0,0 +1,5 @@
import { vec2 } from 'gl-matrix';
export class Circle {
constructor(public center: vec2, public radius: number) {}
}

View file

@ -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));

View file

@ -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;
}
}
}

View file

@ -0,0 +1,3 @@
export function last<T>(a: Array<T>): T | null {
return a.length > 0 ? a[a.length - 1] : null;
}

1
shared/src/helper/mix.ts Normal file
View file

@ -0,0 +1 @@
export const mix = (from: number, to: number, q: number) => from + (to - from) * q;

View file

@ -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');

View file

@ -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;
}
}

View file

@ -0,0 +1,5 @@
import { vec2 } from 'gl-matrix';
export class Rectangle {
constructor(public topLeft = vec2.create(), public size = vec2.create()) {}
}

3
shared/src/helper/rgb.ts Normal file
View file

@ -0,0 +1,3 @@
import { vec3 } from 'gl-matrix';
export const rgb = (r: number, g: number, b: number): vec3 => vec3.fromValues(r, g, b);

View file

@ -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);

View file

@ -0,0 +1,3 @@
import { vec2 } from 'gl-matrix';
export const rotate90Deg = (vec: vec2): vec2 => vec2.fromValues(-vec.y, vec.x);

View file

@ -0,0 +1 @@
export const toPercent = (value: number) => `${Math.round(value * 100)}%`;

View file

@ -0,0 +1,5 @@
let currentId = 0;
export const id = (): number => {
return currentId++;
};

Some files were not shown because too many files have changed in this diff Show more