Add basic multiplayer
This commit is contained in:
parent
0f0a1eaf67
commit
46a48e7c15
113 changed files with 1362 additions and 754 deletions
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;
|
||||
}
|
||||
};
|
||||
Loading…
Add table
Add a link
Reference in a new issue