Refactor frontend to use commands

This commit is contained in:
Schmelczer András 2020-11-06 19:56:09 +01:00
commit 503c99cb1f
15 changed files with 11098 additions and 26151 deletions

30679
frontend/package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -6,9 +6,6 @@ import {
CharacterBase,
ProjectileBase,
} from 'shared';
import { LampView } from './scripts/objects/lamp-view';
import { ProjectileView } from './scripts/objects/projectile-view';
import { PlanetView } from './scripts/objects/planet-view';
import './main.scss';
import '../static/og-image.png';
import '../static/favicons/apple-touch-icon.png';
@ -19,7 +16,6 @@ import { LandingPageBackground } from './scripts/landing-page-background';
import { JoinFormHandler } from './scripts/join-form-handler';
import { handleFullScreen } from './scripts/helper/handle-full-screen';
import { Game } from './scripts/game';
import { CharacterView } from './scripts/objects/character-view';
import { handleInsights } from './scripts/handle-insights';
import { getInsightsFromRenderer } from './scripts/get-insights-from-renderer';
import { Renderer } from 'sdf-2d';
@ -29,6 +25,10 @@ import { hide } from './scripts/helper/hide';
import { show } from './scripts/helper/show';
import { SoundHandler, Sounds } from './scripts/sound-handler';
import { VibrationHandler } from './scripts/vibration-handler';
import { CharacterView } from './scripts/objects/types/character-view';
import { LampView } from './scripts/objects/types/lamp-view';
import { PlanetView } from './scripts/objects/types/planet-view';
import { ProjectileView } from './scripts/objects/types/projectile-view';
glMatrix.setMatrixArrayType(Array);

View file

@ -0,0 +1,7 @@
import { Command } from 'shared';
export class BeforeDestroyCommand extends Command {
public constructor() {
super();
}
}

View file

@ -0,0 +1,13 @@
import { vec2 } from 'gl-matrix';
import { Renderer } from 'sdf-2d';
import { Command } from 'shared';
export class RenderCommand extends Command {
public constructor(
public readonly renderer: Renderer,
public readonly overlay: HTMLElement,
public readonly shouldChangeLayout: boolean,
) {
super();
}
}

View file

@ -0,0 +1,7 @@
import { Command } from 'shared';
export class StepCommand extends Command {
public constructor(public readonly deltaTimeInSeconds: number) {
super();
}
}

View file

@ -33,6 +33,8 @@ import { GameObjectContainer } from './objects/game-object-container';
import parser from 'socket.io-msgpack-parser';
import { BlobShape } from './shapes/blob-shape';
import { PlanetShape } from './shapes/planet-shape';
import { RenderCommand } from './commands/types/render';
import { StepCommand } from './commands/types/step';
export class Game extends CommandReceiver {
public gameObjects = new GameObjectContainer(this);
@ -107,7 +109,7 @@ export class Game extends CommandReceiver {
this.socket.on(TransportEvents.ServerToPlayer, (serializedCommands: string) => {
const commands: Array<Command> = deserialize(serializedCommands);
commands.forEach((c) => this.sendCommand(c));
commands.forEach((c) => this.handleCommand(c));
});
this.socketReceiver = new CommandSocket(this.socket);
@ -124,7 +126,7 @@ export class Game extends CommandReceiver {
}
protected defaultCommandExecutor(c: Command) {
this.gameObjects.sendCommand(c);
this.gameObjects.handleCommand(c);
}
private lastGameState?: UpdateGameState;
@ -248,7 +250,7 @@ export class Game extends CommandReceiver {
}
public aspectRatioChanged(aspectRatio: number) {
this.socketReceiver.sendCommand(new SetAspectRatioActionCommand(aspectRatio));
this.socketReceiver.handleCommand(new SetAspectRatioActionCommand(aspectRatio));
}
private isActive = true;
@ -284,9 +286,12 @@ export class Game extends CommandReceiver {
this.renderer = renderer;
this.gameObjects.handleCommand(new StepCommand(deltaTime));
this.gameObjects.handleCommand(
new RenderCommand(this.renderer, this.overlay, shouldChangeLayout),
);
this.socketReceiver.sendQueuedCommands();
this.gameObjects.stepObjects(deltaTime);
this.gameObjects.drawObjects(this.renderer, this.overlay, shouldChangeLayout);
return this.isActive;
}

View file

@ -1,21 +1,24 @@
import { Renderer } from 'sdf-2d';
import {
Command,
CommandExecutors,
CommandReceiver,
CreateObjectsCommand,
CreatePlayerCommand,
DeleteObjectsCommand,
GameObject,
Id,
PropertyUpdatesForObjects,
RemoteCallsForObjects,
} from 'shared';
import { BeforeDestroyCommand } from '../commands/types/before-destroy';
import { StepCommand } from '../commands/types/step';
import { Game } from '../game';
import { Camera } from './camera';
import { CharacterView } from './character-view';
import { ViewObject } from './view-object';
import { Camera } from './types/camera';
import { CharacterView } from './types/character-view';
export class GameObjectContainer extends CommandReceiver {
protected objects: Map<Id, ViewObject> = new Map();
protected objects: Map<Id, GameObject> = new Map();
public player!: CharacterView;
public camera!: Camera;
@ -35,7 +38,15 @@ export class GameObjectContainer extends CommandReceiver {
},
[CreateObjectsCommand.type]: (c: CreateObjectsCommand) =>
c.objects.forEach((o) => this.addObject(o as ViewObject)),
c.objects.forEach((o) => this.addObject(o as GameObject)),
[StepCommand.type]: (c: StepCommand) => {
this.objects.forEach((o) => o.handleCommand(c));
if (this.player) {
this.camera.center = this.player.position;
}
},
[RemoteCallsForObjects.type]: (c: RemoteCallsForObjects) =>
c.callsForObjects.forEach((c) =>
@ -43,7 +54,9 @@ export class GameObjectContainer extends CommandReceiver {
),
[PropertyUpdatesForObjects.type]: (c: PropertyUpdatesForObjects) =>
c.updates.forEach((c) => this.objects.get(c.id)?.updateProperties(c.updates)),
c.updates.forEach((u) =>
u.updates.forEach((au) => this.objects.get(u.id)?.handleCommand(au)),
),
[DeleteObjectsCommand.type]: (c: DeleteObjectsCommand) =>
c.ids.forEach((id: Id) => this.deleteObject(id)),
@ -53,29 +66,17 @@ export class GameObjectContainer extends CommandReceiver {
super();
}
public stepObjects(deltaTimeInSeconds: number) {
this.objects.forEach((o) => o.step(deltaTimeInSeconds));
if (this.player) {
this.camera.center = this.player.position;
}
protected defaultCommandExecutor(c: Command) {
this.objects.forEach((o) => o.handleCommand(c));
}
public drawObjects(
renderer: Renderer,
overlay: HTMLElement,
shouldChangeLayout: boolean,
) {
this.objects.forEach((o) => o.draw(renderer, overlay, shouldChangeLayout));
}
private addObject(object: ViewObject) {
private addObject(object: GameObject) {
this.objects.set(object.id, object);
}
private deleteObject(id: Id) {
const object = this.objects.get(id);
object?.beforeDestroy();
object?.handleCommand(new BeforeDestroyCommand());
this.objects.delete(id);
}
}

View file

@ -1,27 +0,0 @@
import { vec2, vec3 } from 'gl-matrix';
import { CircleLight, Renderer } from 'sdf-2d';
import { CommandExecutors, Id, LampBase, UpdateProperty } from 'shared';
import { ViewObject } from './view-object';
export class LampView extends LampBase implements ViewObject {
private light: CircleLight;
constructor(id: Id, center: vec2, color: vec3, lightness: number) {
super(id, center, color, lightness);
this.light = new CircleLight(center, color, lightness);
}
public updateProperties(update: UpdateProperty[]): void {}
public step(deltaTimeInSeconds: number): void {}
public beforeDestroy(): void {}
public draw(
renderer: Renderer,
overlay: HTMLElement,
shouldChangeLayout: boolean,
): void {
renderer.addDrawable(this.light);
}
}

View file

@ -1,51 +0,0 @@
import { vec2 } from 'gl-matrix';
import { CircleLight, Renderer } from 'sdf-2d';
import { CharacterTeam, Id, ProjectileBase, settings, UpdateProperty } from 'shared';
import { Vec2Extrapolator } from '../helper/vec2-extrapolator';
import { ViewObject } from './view-object';
export class ProjectileView extends ProjectileBase implements ViewObject {
private light: CircleLight;
private centerExtrapolator: Vec2Extrapolator;
constructor(
id: Id,
center: vec2,
radius: number,
team: CharacterTeam,
strength: number,
) {
super(id, center, radius, team, strength);
this.light = new CircleLight(
center,
settings.paletteDim[settings.colorIndices[team]],
0,
);
this.centerExtrapolator = new Vec2Extrapolator(center);
}
public updateProperties(update: UpdateProperty[]): void {
update.forEach((u) => {
this.centerExtrapolator.addFrame(u.propertyValue, u.rateOfChange);
});
}
public step(deltaTimeInSeconds: number): void {
super.step(deltaTimeInSeconds);
this.center = this.centerExtrapolator.getValue(deltaTimeInSeconds);
this.light.center = this.center;
this.light.intensity = (0.15 * this.strength) / settings.projectileMaxStrength;
}
public beforeDestroy(): void {}
public draw(
renderer: Renderer,
overlay: HTMLElement,
shouldChangeLayout: boolean,
): void {
renderer.addDrawable(this.light);
}
}

View file

@ -0,0 +1,42 @@
import { vec2 } from 'gl-matrix';
import {
calculateViewArea,
CommandExecutors,
GameObject,
mixRgb,
settings,
} from 'shared';
import { RenderCommand } from '../../commands/types/render';
import { Game } from '../../game';
export class Camera extends GameObject {
public center: vec2 = vec2.create();
private aspectRatio?: number;
constructor(private game: Game) {
super(null);
}
protected commandExecutors: CommandExecutors = {
[RenderCommand.type]: this.draw.bind(this),
};
private draw({ renderer }: RenderCommand) {
const canvasAspectRatio = renderer.canvasSize.x / renderer.canvasSize.y;
if (canvasAspectRatio !== this.aspectRatio) {
this.aspectRatio = canvasAspectRatio;
this.game.aspectRatioChanged(canvasAspectRatio);
}
const viewArea = calculateViewArea(this.center, canvasAspectRatio);
renderer.setViewArea(viewArea.topLeft, viewArea.size);
renderer.setRuntimeSettings({
ambientLight: mixRgb(
settings.backgroundGradient[0],
settings.backgroundGradient[1],
vec2.length(this.center) / settings.worldRadius,
),
});
}
}

View file

@ -0,0 +1,154 @@
import { vec2 } from 'gl-matrix';
import { Renderer } from 'sdf-2d';
import {
Circle,
Id,
CharacterBase,
CharacterTeam,
settings,
CommandExecutors,
UpdatePropertyCommand,
} from 'shared';
import { BeforeDestroyCommand } from '../../commands/types/before-destroy';
import { RenderCommand } from '../../commands/types/render';
import { StepCommand } from '../../commands/types/step';
import { CircleExtrapolator } from '../../helper/circle-extrapolator';
import { BlobShape } from '../../shapes/blob-shape';
import { SoundHandler, Sounds } from '../../sound-handler';
import { VibrationHandler } from '../../vibration-handler';
export class CharacterView extends CharacterBase {
private shape: BlobShape;
private nameElement: HTMLElement = document.createElement('div');
private statsElement: HTMLElement = document.createElement('div');
private healthElement: HTMLElement = document.createElement('div');
public isMainCharacter = false;
private leftFootExtrapolator: CircleExtrapolator;
private rightFootExtrapolator: CircleExtrapolator;
private headExtrapolator: CircleExtrapolator;
protected commandExecutors: CommandExecutors = {
[RenderCommand.type]: this.draw.bind(this),
[StepCommand.type]: this.step.bind(this),
[BeforeDestroyCommand.type]: this.beforeDestroy.bind(this),
[UpdatePropertyCommand.type]: this.updateProperty.bind(this),
};
constructor(
id: Id,
name: string,
killCount: number,
deathCount: number,
team: CharacterTeam,
health: number,
head?: Circle,
leftFoot?: Circle,
rightFoot?: Circle,
) {
super(id, name, killCount, deathCount, team, health, head, leftFoot, rightFoot);
this.shape = new BlobShape(settings.colorIndices[team]);
this.leftFootExtrapolator = new CircleExtrapolator(this.leftFoot!);
this.rightFootExtrapolator = new CircleExtrapolator(this.rightFoot!);
this.headExtrapolator = new CircleExtrapolator(this.head!);
this.nameElement.className = 'player-tag ' + this.team;
this.nameElement.innerText = this.name;
this.nameElement.appendChild(this.healthElement);
this.nameElement.appendChild(this.statsElement);
}
public get position(): vec2 {
return this.head!.center;
}
private updateProperty({
propertyKey,
propertyValue,
rateOfChange,
}: UpdatePropertyCommand) {
if (propertyKey === 'head') {
this.headExtrapolator.addFrame(propertyValue, rateOfChange);
}
if (propertyKey === 'leftFoot') {
this.leftFootExtrapolator.addFrame(propertyValue, rateOfChange);
}
if (propertyKey === 'rightFoot') {
this.rightFootExtrapolator.addFrame(propertyValue, rateOfChange);
}
}
public setHealth(health: number) {
const previousHealth = this.health;
super.setHealth(health);
SoundHandler.play(
Sounds.hit,
(0.4 * 2 * (previousHealth - health)) / settings.playerMaxStrength,
);
if (this.isMainCharacter) {
VibrationHandler.vibrate(Math.min(200, (previousHealth - this.health) * 4));
}
}
public onDie() {
if (this.isMainCharacter) {
VibrationHandler.vibrate(150);
}
}
private step({ deltaTimeInSeconds }: StepCommand): void {
this.head! = this.headExtrapolator.getValue(deltaTimeInSeconds);
this.leftFoot! = this.leftFootExtrapolator.getValue(deltaTimeInSeconds);
this.rightFoot! = this.rightFootExtrapolator.getValue(deltaTimeInSeconds);
}
public onShoot(strength: number) {
SoundHandler.play(Sounds.shoot, (0.6 * strength) / settings.playerMaxStrength);
}
private beforeDestroy(): void {
this.nameElement.parentElement?.removeChild(this.nameElement);
}
private draw({ renderer, overlay, shouldChangeLayout }: RenderCommand): void {
if (shouldChangeLayout) {
if (!this.nameElement.parentElement) {
overlay.appendChild(this.nameElement);
}
const screenPosition = renderer.worldToDisplayCoordinates(
this.calculateTextPosition(),
);
this.nameElement.style.transform = `translateX(${screenPosition.x}px) translateY(${screenPosition.y}px) translateX(-50%) translateY(-50%) rotate(-15deg)`;
this.healthElement.style.width =
(50 * this.health) / settings.playerMaxHealth + 'px';
this.statsElement.innerText = this.getStatsText();
}
this.shape.setCircles([this.head!, this.leftFoot!, this.rightFoot!]);
renderer.addDrawable(this.shape);
}
private getStatsText(): string {
return `${this.killCount}⚔/${this.deathCount}☠`;
}
private calculateTextPosition(): vec2 {
const footAverage = vec2.add(
vec2.create(),
this.leftFoot!.center,
this.rightFoot!.center,
);
vec2.scale(footAverage, footAverage, 0.5);
const headFeetDelta = vec2.subtract(footAverage, this.head!.center, footAverage);
vec2.normalize(headFeetDelta, headFeetDelta);
const textOffset = vec2.scale(headFeetDelta, headFeetDelta, this.head!.radius + 80);
return vec2.add(textOffset, this.head!.center, textOffset);
}
}

View file

@ -0,0 +1,21 @@
import { vec2, vec3 } from 'gl-matrix';
import { CircleLight, Renderer } from 'sdf-2d';
import { CommandExecutors, Id, LampBase, UpdatePropertyCommand } from 'shared';
import { RenderCommand } from '../../commands/types/render';
export class LampView extends LampBase {
private light: CircleLight;
protected commandExecutors: CommandExecutors = {
[RenderCommand.type]: this.draw.bind(this),
};
constructor(id: Id, center: vec2, color: vec3, lightness: number) {
super(id, center, color, lightness);
this.light = new CircleLight(center, color, lightness);
}
private draw({ renderer }: RenderCommand): void {
renderer.addDrawable(this.light);
}
}

View file

@ -0,0 +1,142 @@
import { vec2 } from 'gl-matrix';
import { Id, Random, PlanetBase, UpdatePropertyCommand, CommandExecutors } from 'shared';
import { BeforeDestroyCommand } from '../../commands/types/before-destroy';
import { RenderCommand } from '../../commands/types/render';
import { StepCommand } from '../../commands/types/step';
import { PlanetShape } from '../../shapes/planet-shape';
type FallingPoint = {
velocity: vec2;
position: vec2;
element: HTMLElement;
addedToOverlay: boolean;
timeToLive: number;
};
export class PlanetView extends PlanetBase {
private shape: PlanetShape;
private ownershipProgress: HTMLElement;
protected commandExecutors: CommandExecutors = {
[RenderCommand.type]: this.draw.bind(this),
[StepCommand.type]: this.step.bind(this),
[BeforeDestroyCommand.type]: this.beforeDestroy.bind(this),
[UpdatePropertyCommand.type]: this.updateProperty.bind(this),
};
constructor(id: Id, vertices: Array<vec2>, ownership: number) {
super(id, vertices);
this.shape = new PlanetShape(vertices, ownership);
(this.shape as any).randomOffset = Random.getRandom();
this.ownershipProgress = document.createElement('div');
this.ownershipProgress.className = 'ownership';
}
private step(deltaTimeInSeconds: number): void {
this.shape.randomOffset += deltaTimeInSeconds / 4;
this.shape.colorMixQ = this.ownership;
this.generatedPointElements.forEach((p) => {
vec2.add(
p.velocity,
p.velocity,
vec2.scale(vec2.create(), vec2.fromValues(0, 50), deltaTimeInSeconds),
);
vec2.add(
p.position,
p.position,
vec2.scale(vec2.create(), p.velocity, deltaTimeInSeconds),
);
p.timeToLive -= deltaTimeInSeconds;
});
}
private generatedPointElements: Array<FallingPoint> = [];
private lastGeneratedPoint?: number;
public generatedPoints(value: number) {
this.lastGeneratedPoint = value;
}
private beforeDestroy(): void {
this.ownershipProgress.parentElement?.removeChild(this.ownershipProgress);
this.generatedPointElements.forEach((p) =>
p.element.parentElement?.removeChild(p.element),
);
}
private updateProperty({ propertyValue }: UpdatePropertyCommand): void {
this.ownership = propertyValue;
}
private draw({ renderer, overlay, shouldChangeLayout }: RenderCommand): void {
if (shouldChangeLayout) {
if (!this.ownershipProgress.parentElement) {
overlay.appendChild(this.ownershipProgress);
}
const screenPosition = renderer.worldToDisplayCoordinates(this.center);
this.generatedPointElements.forEach((p) => {
if (!p.addedToOverlay) {
overlay.appendChild(p.element);
}
p.element.style.transform = `translateX(${
screenPosition.x + p.position.x
}px) translateY(${screenPosition.y + p.position.y}px)`;
if (p.timeToLive <= 0) {
p.element.parentElement?.removeChild(p.element);
} else {
p.element.style.opacity = Math.min(1, p.timeToLive).toString();
}
});
this.generatedPointElements = this.generatedPointElements.filter(
(p) => p.timeToLive > 0,
);
this.ownershipProgress.style.transform = `translateX(${screenPosition.x}px) translateY(${screenPosition.y}px) translateX(-50%) translateY(-50%)`;
this.ownershipProgress.style.background = this.getGradient();
if (this.lastGeneratedPoint !== undefined) {
const element = document.createElement('div');
element.className = 'falling-point ' + (this.ownership < 0.5 ? 'decla' : 'red');
element.innerText = '+' + this.lastGeneratedPoint;
this.generatedPointElements.push({
element,
addedToOverlay: false,
timeToLive: Random.getRandomInRange(2, 3),
position: vec2.create(),
velocity: vec2.fromValues(Random.getRandomInRange(-30, 30), 0),
});
this.lastGeneratedPoint = undefined;
}
}
renderer.addDrawable(this.shape);
}
private getGradient(): string {
const sideDecla = this.ownership < 0.5;
const sidePercent = (Math.abs(this.ownership - 0.5) / 0.5) * 100;
return sideDecla
? `conic-gradient(
var(--bright-decla) ${sidePercent}%,
var(--bright-decla) ${sidePercent}%,
rgba(0, 0, 0, 0) ${sidePercent}%,
rgba(0, 0, 0, 0) 100%
)`
: `conic-gradient(
rgba(0, 0, 0, 0) 0%,
rgba(0, 0, 0, 0) ${100 - sidePercent}%,
var(--bright-red) ${100 - sidePercent}%,
var(--bright-red) 100%
)`;
}
}

View file

@ -0,0 +1,57 @@
import { vec2 } from 'gl-matrix';
import { CircleLight, Renderer } from 'sdf-2d';
import {
CharacterTeam,
CommandExecutors,
Id,
ProjectileBase,
settings,
UpdatePropertyCommand,
} from 'shared';
import { RenderCommand } from '../../commands/types/render';
import { StepCommand } from '../../commands/types/step';
import { Vec2Extrapolator } from '../../helper/vec2-extrapolator';
export class ProjectileView extends ProjectileBase {
private light: CircleLight;
private centerExtrapolator: Vec2Extrapolator;
protected commandExecutors: CommandExecutors = {
[RenderCommand.type]: this.draw.bind(this),
[StepCommand.type]: this.handleStep.bind(this),
[UpdatePropertyCommand.type]: this.updateProperty.bind(this),
};
constructor(
id: Id,
center: vec2,
radius: number,
team: CharacterTeam,
strength: number,
) {
super(id, center, radius, team, strength);
this.light = new CircleLight(
center,
settings.paletteDim[settings.colorIndices[team]],
0,
);
this.centerExtrapolator = new Vec2Extrapolator(center);
}
private updateProperty({ propertyValue, rateOfChange }: UpdatePropertyCommand): void {
this.centerExtrapolator.addFrame(propertyValue, rateOfChange);
}
private handleStep({ deltaTimeInSeconds }: StepCommand): void {
this.step(deltaTimeInSeconds);
this.center = this.centerExtrapolator.getValue(deltaTimeInSeconds);
this.light.center = this.center;
this.light.intensity = (0.15 * this.strength) / settings.projectileMaxStrength;
}
private draw({ renderer }: RenderCommand): void {
renderer.addDrawable(this.light);
}
}

View file

@ -1,9 +0,0 @@
import { Renderer } from 'sdf-2d';
import { GameObject, UpdateProperty } from 'shared';
export interface ViewObject extends GameObject {
step(deltaTimeInMilliseconds: number): void;
draw(renderer: Renderer, overlay: HTMLElement, shouldChangeLayout: boolean): void;
updateProperties(update: Array<UpdateProperty>): void;
beforeDestroy(): void;
}