Add basic multiplayer
This commit is contained in:
parent
0f0a1eaf67
commit
46a48e7c15
113 changed files with 1362 additions and 754 deletions
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
31
backend/src/objects/character-physics.ts
Normal file
31
backend/src/objects/character-physics.ts
Normal 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];
|
||||
}
|
||||
}
|
||||
|
|
@ -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 };
|
||||
}
|
||||
}
|
||||
36
backend/src/objects/lamp-physics.ts
Normal file
36
backend/src/objects/lamp-physics.ts
Normal 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];
|
||||
}
|
||||
}
|
||||
48
backend/src/objects/tunnel-physics.ts
Normal file
48
backend/src/objects/tunnel-physics.ts
Normal 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];
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
86
backend/src/physics/physical-game-object-container.ts
Normal file
86
backend/src/physics/physical-game-object-container.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
10
backend/src/physics/physical-game-object.ts
Normal file
10
backend/src/physics/physical-game-object.ts
Normal 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;
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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
2
backend/src/serialize.ts
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
export const jsonSerialize = (o: any): string =>
|
||||
JSON.stringify(o, (key, value) => (value?.toFixed ? Number(value.toFixed(3)) : value));
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,7 +11,11 @@ module.exports = {
|
|||
entry: {
|
||||
main: [PATHS.entryPoint],
|
||||
},
|
||||
externals: [nodeExternals()],
|
||||
externals: [
|
||||
nodeExternals({
|
||||
allowlist: [/(^shared)/],
|
||||
}),
|
||||
],
|
||||
target: 'node',
|
||||
output: {
|
||||
filename: '[name].js',
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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)));
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
import { Command } from './command';
|
||||
|
||||
export interface CommandReceiver {
|
||||
sendCommand(command: Command): void;
|
||||
}
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
import { Typed } from '../transport/serializable';
|
||||
import { Id } from '../identity/identity';
|
||||
|
||||
export abstract class Command extends Typed {
|
||||
target?: Id;
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
36
frontend/src/scripts/commands/generators/mouse-listener.ts
Normal file
36
frontend/src/scripts/commands/generators/mouse-listener.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
57
frontend/src/scripts/commands/generators/touch-listener.ts
Normal file
57
frontend/src/scripts/commands/generators/touch-listener.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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';
|
||||
}
|
||||
}
|
||||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
@ -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';
|
||||
}
|
||||
}
|
||||
|
|
@ -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';
|
||||
}
|
||||
}
|
||||
|
|
@ -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';
|
||||
}
|
||||
}
|
||||
10
frontend/src/scripts/commands/types/move-to.ts
Normal file
10
frontend/src/scripts/commands/types/move-to.ts
Normal 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();
|
||||
}
|
||||
}
|
||||
10
frontend/src/scripts/commands/types/render.ts
Normal file
10
frontend/src/scripts/commands/types/render.ts
Normal 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();
|
||||
}
|
||||
}
|
||||
9
frontend/src/scripts/commands/types/step.ts
Normal file
9
frontend/src/scripts/commands/types/step.ts
Normal 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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -1 +0,0 @@
|
|||
export type Id = string;
|
||||
|
|
@ -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';
|
||||
}
|
||||
}
|
||||
|
|
@ -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';
|
||||
}
|
||||
}
|
||||
|
|
@ -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';
|
||||
}
|
||||
}
|
||||
|
|
@ -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';
|
||||
}
|
||||
}
|
||||
|
|
@ -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';
|
||||
}
|
||||
}
|
||||
|
|
@ -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';
|
||||
}
|
||||
}
|
||||
|
|
@ -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';
|
||||
}
|
||||
}
|
||||
|
|
@ -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));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
36
frontend/src/scripts/objects/camera.ts
Normal file
36
frontend/src/scripts/objects/camera.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
19
frontend/src/scripts/objects/character-view.ts
Normal file
19
frontend/src/scripts/objects/character-view.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
63
frontend/src/scripts/objects/game-object-container.ts
Normal file
63
frontend/src/scripts/objects/game-object-container.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
17
frontend/src/scripts/objects/lamp-view.ts
Normal file
17
frontend/src/scripts/objects/lamp-view.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
17
frontend/src/scripts/objects/tunnel-view.ts
Normal file
17
frontend/src/scripts/objects/tunnel-view.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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 {
|
|||
);
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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 {
|
|||
}
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -1,7 +0,0 @@
|
|||
import { PhysicalObject } from './physical-object';
|
||||
|
||||
export abstract class StaticPhysicalObject extends PhysicalObject {
|
||||
protected addToPhysics() {
|
||||
super.addToPhysics(false);
|
||||
}
|
||||
}
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
28
frontend/src/scripts/transport/deserialize.ts
Normal file
28
frontend/src/scripts/transport/deserialize.ts
Normal 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
1
shared/.eslintignore
Normal file
|
|
@ -0,0 +1 @@
|
|||
**/*.js
|
||||
29
shared/.eslintrc.json
Normal file
29
shared/.eslintrc.json
Normal 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
4
shared/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
dist
|
||||
node_modules
|
||||
package-lock.json
|
||||
.firebase
|
||||
7
shared/.prettierrc
Normal file
7
shared/.prettierrc
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"trailingComma": "es5",
|
||||
"printWidth": 90,
|
||||
"tabWidth": 2,
|
||||
"singleQuote": true,
|
||||
"endOfLine": "lf"
|
||||
}
|
||||
16
shared/firebase.json
Normal file
16
shared/firebase.json
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
{
|
||||
"hosting": {
|
||||
"public": "dist",
|
||||
"ignore": [
|
||||
"firebase.json",
|
||||
"**/.*",
|
||||
"**/node_modules/**"
|
||||
],
|
||||
"rewrites": [
|
||||
{
|
||||
"source": "**",
|
||||
"destination": "/index.html"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
17
shared/package.json
Normal file
17
shared/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
7
shared/src/commands/broadcast-commands.ts
Normal file
7
shared/src/commands/broadcast-commands.ts
Normal 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)));
|
||||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
25
shared/src/commands/command-receiver.ts
Normal file
25
shared/src/commands/command-receiver.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
7
shared/src/commands/command.ts
Normal file
7
shared/src/commands/command.ts
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
import { Id } from '../transport/identity';
|
||||
|
||||
export abstract class Command {
|
||||
public get type(): string {
|
||||
return (this as any).constructor.type;
|
||||
}
|
||||
}
|
||||
13
shared/src/commands/types/create-objects.ts
Normal file
13
shared/src/commands/types/create-objects.ts
Normal 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];
|
||||
}
|
||||
}
|
||||
13
shared/src/commands/types/create-player.ts
Normal file
13
shared/src/commands/types/create-player.ts
Normal 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];
|
||||
}
|
||||
}
|
||||
14
shared/src/commands/types/delete-objects.ts
Normal file
14
shared/src/commands/types/delete-objects.ts
Normal 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];
|
||||
}
|
||||
}
|
||||
14
shared/src/commands/types/move-action.ts
Normal file
14
shared/src/commands/types/move-action.ts
Normal 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];
|
||||
}
|
||||
}
|
||||
14
shared/src/commands/types/primary-action.ts
Normal file
14
shared/src/commands/types/primary-action.ts
Normal 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];
|
||||
}
|
||||
}
|
||||
14
shared/src/commands/types/secondary-action.ts
Normal file
14
shared/src/commands/types/secondary-action.ts
Normal 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];
|
||||
}
|
||||
}
|
||||
14
shared/src/commands/types/set-view-area-action.ts
Normal file
14
shared/src/commands/types/set-view-area-action.ts
Normal 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];
|
||||
}
|
||||
}
|
||||
14
shared/src/commands/types/ternary-action.ts
Normal file
14
shared/src/commands/types/ternary-action.ts
Normal 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];
|
||||
}
|
||||
}
|
||||
13
shared/src/commands/types/update-objects.ts
Normal file
13
shared/src/commands/types/update-objects.ts
Normal 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];
|
||||
}
|
||||
}
|
||||
31
shared/src/helper/array.ts
Normal file
31
shared/src/helper/array.ts
Normal 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);
|
||||
};
|
||||
5
shared/src/helper/circle.ts
Normal file
5
shared/src/helper/circle.ts
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
import { vec2 } from 'gl-matrix';
|
||||
|
||||
export class Circle {
|
||||
constructor(public center: vec2, public radius: number) {}
|
||||
}
|
||||
4
shared/src/helper/clamp.ts
Normal file
4
shared/src/helper/clamp.ts
Normal 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));
|
||||
23
shared/src/helper/delta-time-calculator.ts
Normal file
23
shared/src/helper/delta-time-calculator.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
3
shared/src/helper/last.ts
Normal file
3
shared/src/helper/last.ts
Normal 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
1
shared/src/helper/mix.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export const mix = (from: number, to: number, q: number) => from + (to - from) * q;
|
||||
4
shared/src/helper/pretty-print.ts
Normal file
4
shared/src/helper/pretty-print.ts
Normal 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');
|
||||
18
shared/src/helper/random.ts
Normal file
18
shared/src/helper/random.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
5
shared/src/helper/rectangle.ts
Normal file
5
shared/src/helper/rectangle.ts
Normal 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
3
shared/src/helper/rgb.ts
Normal 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);
|
||||
4
shared/src/helper/rgb255.ts
Normal file
4
shared/src/helper/rgb255.ts
Normal 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);
|
||||
3
shared/src/helper/rotate-90-deg.ts
Normal file
3
shared/src/helper/rotate-90-deg.ts
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
import { vec2 } from 'gl-matrix';
|
||||
|
||||
export const rotate90Deg = (vec: vec2): vec2 => vec2.fromValues(-vec.y, vec.x);
|
||||
1
shared/src/helper/to-percent.ts
Normal file
1
shared/src/helper/to-percent.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export const toPercent = (value: number) => `${Math.round(value * 100)}%`;
|
||||
5
shared/src/helper/unique.ts
Normal file
5
shared/src/helper/unique.ts
Normal 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
Loading…
Add table
Add a link
Reference in a new issue