Add basic multiplayer
This commit is contained in:
parent
0f0a1eaf67
commit
46a48e7c15
113 changed files with 1362 additions and 754 deletions
|
|
@ -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,14 +0,0 @@
|
|||
import { CommandReceiver } from './command-receiver';
|
||||
import { Command } from './command';
|
||||
|
||||
export class CommandGenerator {
|
||||
private subscribers: Array<CommandReceiver> = [];
|
||||
|
||||
public subscribe(subscriber: CommandReceiver): void {
|
||||
this.subscribers.push(subscriber);
|
||||
}
|
||||
|
||||
protected sendCommand(command: Command): void {
|
||||
this.subscribers.forEach((s) => s.sendCommand(command));
|
||||
}
|
||||
}
|
||||
|
|
@ -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,8 +0,0 @@
|
|||
import { v4 } from 'uuid';
|
||||
import { Id } from './identity';
|
||||
|
||||
export class IdentityManager {
|
||||
public static generateId(): Id {
|
||||
return v4();
|
||||
}
|
||||
}
|
||||
|
|
@ -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,48 +0,0 @@
|
|||
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';
|
||||
|
||||
export const createDungeon = (objects: Objects, physics: Physics) => {
|
||||
let previousRadius = 350;
|
||||
let previousEnd = vec2.create();
|
||||
|
||||
let tunnelsCountSinceLastLight = 0;
|
||||
|
||||
for (let i = 0; i < 500000; i += 500) {
|
||||
const deltaHeight = (Random.getRandom() - 0.5) * 2000;
|
||||
const height = previousEnd.y + deltaHeight;
|
||||
const currentEnd = vec2.fromValues(i, height);
|
||||
const currentToRadius = Random.getRandom() * 300 + 150;
|
||||
|
||||
const tunnel = new Tunnel(
|
||||
physics,
|
||||
previousEnd,
|
||||
currentEnd,
|
||||
previousRadius,
|
||||
currentToRadius
|
||||
);
|
||||
|
||||
objects.addObject(tunnel);
|
||||
|
||||
if (++tunnelsCountSinceLastLight > 3 && Random.getRandom() > 0.7) {
|
||||
objects.addObject(
|
||||
new Lamp(
|
||||
currentEnd,
|
||||
vec3.normalize(
|
||||
vec3.create(),
|
||||
vec3.fromValues(Random.getRandom(), 0, Random.getRandom())
|
||||
),
|
||||
0.5,
|
||||
physics
|
||||
)
|
||||
);
|
||||
tunnelsCountSinceLastLight = 0;
|
||||
}
|
||||
|
||||
previousEnd = currentEnd;
|
||||
previousRadius = currentToRadius;
|
||||
}
|
||||
};
|
||||
|
|
@ -1,62 +0,0 @@
|
|||
import { vec2 } from 'gl-matrix';
|
||||
import { PhysicalObject } from '../physical-object';
|
||||
|
||||
export abstract class BoundingBoxBase {
|
||||
constructor(
|
||||
public readonly owner: PhysicalObject,
|
||||
protected _xMin: number = 0,
|
||||
protected _xMax: number = 0,
|
||||
protected _yMin: number = 0,
|
||||
protected _yMax: number = 0,
|
||||
public readonly isInverted = false
|
||||
) {}
|
||||
|
||||
public get 0(): number {
|
||||
return this._xMin;
|
||||
}
|
||||
|
||||
public get 1(): number {
|
||||
return this._xMax;
|
||||
}
|
||||
|
||||
public get 2(): number {
|
||||
return this._yMin;
|
||||
}
|
||||
|
||||
public get 3(): number {
|
||||
return this._yMax;
|
||||
}
|
||||
|
||||
public get xMin(): number {
|
||||
return this._xMin;
|
||||
}
|
||||
|
||||
public get xMax(): number {
|
||||
return this._xMax;
|
||||
}
|
||||
|
||||
public get yMin(): number {
|
||||
return this._yMin;
|
||||
}
|
||||
|
||||
public get yMax(): number {
|
||||
return this._yMax;
|
||||
}
|
||||
|
||||
public get topLeft(): vec2 {
|
||||
return vec2.fromValues(this._xMin, this._yMax);
|
||||
}
|
||||
|
||||
public get size(): vec2 {
|
||||
return vec2.fromValues(this._xMax - this._xMin, this._yMax - this._yMin);
|
||||
}
|
||||
|
||||
public intersects(other: BoundingBoxBase): boolean {
|
||||
return (
|
||||
this._xMin < other._xMax &&
|
||||
this._xMax > other._xMin &&
|
||||
this._yMin < other._yMax &&
|
||||
this._yMax > other._yMin
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,65 +0,0 @@
|
|||
import { vec2 } from 'gl-matrix';
|
||||
import { BoundingBoxBase } from './bounding-box-base';
|
||||
import { ImmutableBoundingBox } from './immutable-bounding-box';
|
||||
|
||||
export class BoundingBox extends BoundingBoxBase {
|
||||
public get xMin(): number {
|
||||
return this._xMin;
|
||||
}
|
||||
|
||||
public set xMin(value: number) {
|
||||
this._xMin = value;
|
||||
}
|
||||
|
||||
public set xMax(value: number) {
|
||||
this._xMax = value;
|
||||
}
|
||||
|
||||
public get xMax(): number {
|
||||
return this._xMax;
|
||||
}
|
||||
|
||||
public set yMin(value: number) {
|
||||
this._yMin = value;
|
||||
}
|
||||
|
||||
public get yMin(): number {
|
||||
return this._yMin;
|
||||
}
|
||||
|
||||
public set yMax(value: number) {
|
||||
this._yMax = value;
|
||||
}
|
||||
|
||||
public get yMax(): number {
|
||||
return this._yMax;
|
||||
}
|
||||
|
||||
public get topLeft(): vec2 {
|
||||
return vec2.fromValues(this._xMin, this._yMax);
|
||||
}
|
||||
|
||||
public set topLeft(value: vec2) {
|
||||
this._xMin = value.x;
|
||||
this._yMax = value.y;
|
||||
}
|
||||
|
||||
public set size(value: vec2) {
|
||||
this._xMax = this.xMin + value.x;
|
||||
this._yMin = this.yMax - value.y;
|
||||
}
|
||||
|
||||
public get size(): vec2 {
|
||||
return vec2.fromValues(this._xMax - this._xMin, this._yMax - this._yMin);
|
||||
}
|
||||
|
||||
public cloneAsImmutable(): ImmutableBoundingBox {
|
||||
return new ImmutableBoundingBox(
|
||||
this.owner,
|
||||
this.xMin,
|
||||
this.xMax,
|
||||
this.yMin,
|
||||
this.yMax
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,75 +0,0 @@
|
|||
import { vec2 } from 'gl-matrix';
|
||||
import { PhysicalObject } from '../physical-object';
|
||||
import { BoundingBox } from './bounding-box';
|
||||
import { BoundingBoxBase } from './bounding-box-base';
|
||||
|
||||
export class BoundingCircle {
|
||||
private _boundingBox: BoundingBox;
|
||||
|
||||
constructor(
|
||||
public readonly owner: PhysicalObject,
|
||||
private _center: vec2,
|
||||
private _radius: number
|
||||
) {
|
||||
this._boundingBox = new BoundingBox(owner);
|
||||
this.recalculateBoundingBox();
|
||||
}
|
||||
|
||||
public get center(): vec2 {
|
||||
return this._center;
|
||||
}
|
||||
|
||||
public set center(value: vec2) {
|
||||
this._center = value;
|
||||
this.recalculateBoundingBox();
|
||||
}
|
||||
|
||||
public get radius(): number {
|
||||
return this._radius;
|
||||
}
|
||||
|
||||
public set radius(value: number) {
|
||||
this._radius = value;
|
||||
this.recalculateBoundingBox();
|
||||
}
|
||||
|
||||
public distance(target: vec2): number {
|
||||
return vec2.distance(target, this.center) - this.radius;
|
||||
}
|
||||
|
||||
public distanceBetween(target: BoundingCircle): number {
|
||||
return vec2.distance(target.center, this.center) - this.radius - target.radius;
|
||||
}
|
||||
|
||||
public areIntersecting(other: PhysicalObject): boolean {
|
||||
return other.distance(this.center) < this.radius;
|
||||
}
|
||||
|
||||
public isInside(other: PhysicalObject): boolean {
|
||||
return other.distance(this.center) < -this.radius;
|
||||
}
|
||||
|
||||
public getPerimeterPoints(count: number): Array<vec2> {
|
||||
const result: Array<vec2> = [];
|
||||
for (let i = 0; i < count; i++) {
|
||||
result.push(
|
||||
vec2.fromValues(
|
||||
Math.cos((2 * Math.PI * i) / count) * this.radius + this.center.x,
|
||||
Math.sin((2 * Math.PI * i) / count) * this.radius + this.center.y
|
||||
)
|
||||
);
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
import { BoundingBoxBase } from './bounding-box-base';
|
||||
|
||||
export class ImmutableBoundingBox extends BoundingBoxBase {}
|
||||
|
|
@ -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,20 +0,0 @@
|
|||
import { BoundingBoxBase } from '../bounds/bounding-box-base';
|
||||
|
||||
export class BoundingBoxList {
|
||||
constructor(private boundingBoxes: Array<BoundingBoxBase> = []) {}
|
||||
|
||||
public insert(box: BoundingBoxBase) {
|
||||
this.boundingBoxes.push(box);
|
||||
}
|
||||
|
||||
public remove(box: BoundingBoxBase) {
|
||||
this.boundingBoxes.splice(
|
||||
this.boundingBoxes.findIndex((i) => i === box),
|
||||
1
|
||||
);
|
||||
}
|
||||
|
||||
public findIntersecting(box: BoundingBoxBase): Array<BoundingBoxBase> {
|
||||
return this.boundingBoxes.filter((b) => b.intersects(box));
|
||||
}
|
||||
}
|
||||
|
|
@ -1,121 +0,0 @@
|
|||
// source: https://github.com/ubilabs/kd-tree-javascript/blob/master/kdTree.js
|
||||
|
||||
import { ImmutableBoundingBox } from '../bounds/immutable-bounding-box';
|
||||
|
||||
class Node {
|
||||
public left?: Node = null;
|
||||
public right?: Node = null;
|
||||
constructor(public rectangle: ImmutableBoundingBox, public parent: Node) {}
|
||||
}
|
||||
|
||||
export class BoundingBoxTree {
|
||||
root?: Node;
|
||||
|
||||
constructor(boxes: Array<ImmutableBoundingBox> = []) {
|
||||
this.build(boxes);
|
||||
}
|
||||
|
||||
public build(boxes: Array<ImmutableBoundingBox>) {
|
||||
this.root = this.buildRecursive(boxes, 0, null);
|
||||
}
|
||||
|
||||
private buildRecursive(
|
||||
boxes: Array<ImmutableBoundingBox>,
|
||||
depth: number,
|
||||
parent: Node
|
||||
): Node {
|
||||
if (boxes.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (boxes.length === 1) {
|
||||
return new Node(boxes[0], parent);
|
||||
}
|
||||
|
||||
const dimension = depth % 4;
|
||||
|
||||
boxes.sort((a, b) => a[dimension] - b[dimension]);
|
||||
|
||||
const median = Math.floor(boxes.length / 2);
|
||||
|
||||
const node = new Node(boxes[median], parent);
|
||||
node.left = this.buildRecursive(boxes.slice(0, median), depth + 1, node);
|
||||
node.right = this.buildRecursive(boxes.slice(median + 1), depth + 1, node);
|
||||
|
||||
return node;
|
||||
}
|
||||
|
||||
public insert(box: ImmutableBoundingBox) {
|
||||
const [insertPosition, depth] = this.findParent(box, this.root, 0, null);
|
||||
|
||||
if (insertPosition === null) {
|
||||
this.root = new Node(box, null);
|
||||
} else {
|
||||
const node = new Node(box, insertPosition);
|
||||
const dimension = depth % 4;
|
||||
|
||||
if (box[dimension] < insertPosition.rectangle[dimension]) {
|
||||
insertPosition.left = node;
|
||||
} else {
|
||||
insertPosition.right = node;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public findIntersecting(box: ImmutableBoundingBox): Array<ImmutableBoundingBox> {
|
||||
const maybeResults = this.findMaybeIntersecting(box, this.root, 0);
|
||||
const results = maybeResults.filter((b) => b.intersects(box));
|
||||
return results;
|
||||
}
|
||||
|
||||
private findMaybeIntersecting(
|
||||
box: ImmutableBoundingBox,
|
||||
node: Node,
|
||||
depth: number
|
||||
): Array<ImmutableBoundingBox> {
|
||||
if (node === null) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (depth % 4 == 0 && box.xMax < node.rectangle.xMin) {
|
||||
return this.findMaybeIntersecting(box, node.left, depth + 1);
|
||||
}
|
||||
|
||||
if (depth % 4 == 1 && box.xMin > node.rectangle.xMax) {
|
||||
return this.findMaybeIntersecting(box, node.right, depth + 1);
|
||||
}
|
||||
|
||||
if (depth % 4 == 2 && box.yMax < node.rectangle.yMin) {
|
||||
return this.findMaybeIntersecting(box, node.left, depth + 1);
|
||||
}
|
||||
|
||||
if (depth % 4 == 3 && box.yMin > node.rectangle.yMax) {
|
||||
return this.findMaybeIntersecting(box, node.right, depth + 1);
|
||||
}
|
||||
|
||||
return [
|
||||
node.rectangle,
|
||||
...this.findMaybeIntersecting(box, node.left, depth + 1),
|
||||
...this.findMaybeIntersecting(box, node.right, depth + 1),
|
||||
];
|
||||
}
|
||||
|
||||
private findParent(
|
||||
box: ImmutableBoundingBox,
|
||||
node: Node,
|
||||
depth: number,
|
||||
parent: Node
|
||||
): [Node, number] {
|
||||
if (node === null) {
|
||||
return [parent, depth - 1];
|
||||
}
|
||||
|
||||
const dimension = depth % 4;
|
||||
|
||||
if (box[dimension] < node.rectangle[dimension]) {
|
||||
return this.findParent(box, node.left, depth + 1, node);
|
||||
}
|
||||
|
||||
return this.findParent(box, node.right, depth + 1, node);
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
);
|
||||
};
|
||||
Loading…
Add table
Add a link
Reference in a new issue