Change gameplay
This commit is contained in:
parent
d79900e3ea
commit
34dae300da
56 changed files with 906 additions and 400 deletions
|
|
@ -4,6 +4,7 @@ export const defaultOptions: Options = {
|
||||||
port: 3000,
|
port: 3000,
|
||||||
name: 'Localhost',
|
name: 'Localhost',
|
||||||
playerLimit: 16,
|
playerLimit: 16,
|
||||||
seed: 51,
|
seed: Math.random(),
|
||||||
|
scoreLimit: 500,
|
||||||
worldSize: 10000,
|
worldSize: 10000,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,12 @@ import {
|
||||||
settings,
|
settings,
|
||||||
ServerInformation,
|
ServerInformation,
|
||||||
PlayerInformation,
|
PlayerInformation,
|
||||||
|
serializable,
|
||||||
|
UpdateGameState,
|
||||||
|
serialize,
|
||||||
|
CharacterTeam,
|
||||||
|
GameEnd,
|
||||||
|
GameStart,
|
||||||
} from 'shared';
|
} from 'shared';
|
||||||
import { createWorld } from './map/create-world';
|
import { createWorld } from './map/create-world';
|
||||||
import { DeltaTimeCalculator } from './helper/delta-time-calculator';
|
import { DeltaTimeCalculator } from './helper/delta-time-calculator';
|
||||||
|
|
@ -15,22 +21,40 @@ import { PlayerContainer } from './players/player-container';
|
||||||
const playerCountSubscribedRoom = 'playerCountUpdates';
|
const playerCountSubscribedRoom = 'playerCountUpdates';
|
||||||
|
|
||||||
export class GameServer {
|
export class GameServer {
|
||||||
private objects = new PhysicalContainer();
|
private objects!: PhysicalContainer;
|
||||||
private players: PlayerContainer;
|
private players!: PlayerContainer;
|
||||||
private deltaTimes: Array<number> = [];
|
private deltaTimes!: Array<number>;
|
||||||
private deltaTimeCalculator = new DeltaTimeCalculator();
|
private deltaTimeCalculator!: DeltaTimeCalculator;
|
||||||
|
|
||||||
|
private declaPoints = 0;
|
||||||
|
private redPoints = 0;
|
||||||
|
|
||||||
|
private isInEndGame = false;
|
||||||
|
private timeScaling = 1;
|
||||||
|
|
||||||
private serverName: string;
|
private serverName: string;
|
||||||
private playerLimit: number;
|
private playerLimit: number;
|
||||||
|
|
||||||
constructor(private readonly io: ioserver.Server, options: Options) {
|
private initialize() {
|
||||||
|
const previousPlayers = this.players;
|
||||||
|
this.objects = new PhysicalContainer();
|
||||||
this.players = new PlayerContainer(this.objects);
|
this.players = new PlayerContainer(this.objects);
|
||||||
|
this.deltaTimeCalculator = new DeltaTimeCalculator();
|
||||||
|
this.deltaTimes = [];
|
||||||
|
this.declaPoints = 0;
|
||||||
|
this.redPoints = 0;
|
||||||
|
this.isInEndGame = false;
|
||||||
|
this.timeScaling = 1;
|
||||||
|
createWorld(this.objects, this.options.worldSize);
|
||||||
|
this.objects.initialize();
|
||||||
|
previousPlayers?.sendOnSocket(serialize(new GameStart()));
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(private readonly io: ioserver.Server, private options: Options) {
|
||||||
this.serverName = options.name;
|
this.serverName = options.name;
|
||||||
this.playerLimit = options.playerLimit;
|
this.playerLimit = options.playerLimit;
|
||||||
|
|
||||||
createWorld(this.objects, options.worldSize);
|
this.initialize();
|
||||||
this.objects.initialize();
|
|
||||||
|
|
||||||
io.on('connection', (socket: SocketIO.Socket) => {
|
io.on('connection', (socket: SocketIO.Socket) => {
|
||||||
socket.on(TransportEvents.PlayerJoining, (playerInfo: PlayerInformation) => {
|
socket.on(TransportEvents.PlayerJoining, (playerInfo: PlayerInformation) => {
|
||||||
|
|
@ -40,33 +64,61 @@ export class GameServer {
|
||||||
player.sendCommand(command);
|
player.sendCommand(command);
|
||||||
});
|
});
|
||||||
|
|
||||||
this.sendPlayerCountUpdate();
|
this.sendServerStateUpdate();
|
||||||
|
|
||||||
socket.on('disconnect', () => {
|
socket.on('disconnect', () => {
|
||||||
player.destroy();
|
player.destroy();
|
||||||
this.players.deletePlayer(player);
|
this.players.deletePlayer(player);
|
||||||
this.sendPlayerCountUpdate();
|
this.sendServerStateUpdate();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on(TransportEvents.SubscribeForPlayerCount, () => {
|
socket.on(TransportEvents.SubscribeForServerInfoUpdates, () => {
|
||||||
socket.join(playerCountSubscribedRoom);
|
socket.join(playerCountSubscribedRoom);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public sendPlayerCountUpdate() {
|
private timeSinceLastServerStateUpdate = 0;
|
||||||
|
public sendServerStateUpdate() {
|
||||||
this.io
|
this.io
|
||||||
.to(playerCountSubscribedRoom)
|
.to(playerCountSubscribedRoom)
|
||||||
.emit(TransportEvents.PlayerCountUpdate, this.players.count);
|
.emit(TransportEvents.ServerInfoUpdate, [this.players.count, this.gameProgress]);
|
||||||
}
|
}
|
||||||
|
|
||||||
public start() {
|
public start() {
|
||||||
this.handlePhysics();
|
this.handlePhysics();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private updatePoints() {
|
||||||
|
if (this.isInEndGame) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const { decla, red } = this.objects.getPointsGenerated();
|
||||||
|
this.declaPoints += decla;
|
||||||
|
this.redPoints += red;
|
||||||
|
if (this.declaPoints >= this.options.scoreLimit) {
|
||||||
|
this.endGame(CharacterTeam.decla);
|
||||||
|
} else if (this.redPoints >= this.options.scoreLimit) {
|
||||||
|
this.endGame(CharacterTeam.red);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private endGame(winningTeam: CharacterTeam) {
|
||||||
|
this.isInEndGame = true;
|
||||||
|
const endTitleLength = 6000;
|
||||||
|
this.players.sendOnSocket(
|
||||||
|
serialize(new GameEnd(winningTeam, endTitleLength / 1000, true)),
|
||||||
|
);
|
||||||
|
setTimeout(() => this.destroy(), endTitleLength * 1.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
private destroy() {
|
||||||
|
this.initialize();
|
||||||
|
}
|
||||||
|
|
||||||
private handlePhysics() {
|
private handlePhysics() {
|
||||||
const delta = this.deltaTimeCalculator.getNextDeltaTimeInSeconds();
|
let delta = this.deltaTimeCalculator.getNextDeltaTimeInSeconds();
|
||||||
const framesBetweenDeltaTimeCalculation = 1000;
|
const framesBetweenDeltaTimeCalculation = 1000;
|
||||||
|
|
||||||
if (this.deltaTimes.length > framesBetweenDeltaTimeCalculation) {
|
if (this.deltaTimes.length > framesBetweenDeltaTimeCalculation) {
|
||||||
|
|
@ -82,8 +134,25 @@ export class GameServer {
|
||||||
this.deltaTimes = [];
|
this.deltaTimes = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ((this.timeSinceLastServerStateUpdate += delta) > 5) {
|
||||||
|
this.timeSinceLastServerStateUpdate = 0;
|
||||||
|
this.sendServerStateUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.isInEndGame) {
|
||||||
|
this.timeScaling *= Math.pow(settings.endGameDeltaScaling, delta);
|
||||||
|
delta /= this.timeScaling;
|
||||||
|
}
|
||||||
|
|
||||||
this.objects.stepObjects(delta);
|
this.objects.stepObjects(delta);
|
||||||
|
this.updatePoints();
|
||||||
|
this.players.sendOnSocket(
|
||||||
|
serialize(
|
||||||
|
new UpdateGameState(this.declaPoints, this.redPoints, this.options.scoreLimit),
|
||||||
|
),
|
||||||
|
);
|
||||||
this.players.step(delta);
|
this.players.step(delta);
|
||||||
|
this.objects.resetRemoteCalls();
|
||||||
|
|
||||||
const physicsDelta = this.deltaTimeCalculator.getDeltaTimeInSeconds() * 1000;
|
const physicsDelta = this.deltaTimeCalculator.getDeltaTimeInSeconds() * 1000;
|
||||||
this.deltaTimes.push(physicsDelta);
|
this.deltaTimes.push(physicsDelta);
|
||||||
|
|
@ -96,11 +165,16 @@ export class GameServer {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private get gameProgress(): number {
|
||||||
|
return (Math.max(this.declaPoints, this.redPoints) / this.options.scoreLimit) * 100;
|
||||||
|
}
|
||||||
|
|
||||||
public get serverInfo(): ServerInformation {
|
public get serverInfo(): ServerInformation {
|
||||||
return {
|
return {
|
||||||
serverName: this.serverName,
|
serverName: this.serverName,
|
||||||
playerCount: this.players.count,
|
playerCount: this.players.count,
|
||||||
playerLimit: this.playerLimit,
|
playerLimit: this.playerLimit,
|
||||||
|
gameStatePercent: this.gameProgress,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,11 +4,8 @@ import { Server } from 'http';
|
||||||
import cors from 'cors';
|
import cors from 'cors';
|
||||||
import { applyArrayPlugins, Random, serverInformationEndpoint } from 'shared';
|
import { applyArrayPlugins, Random, serverInformationEndpoint } from 'shared';
|
||||||
import minimist from 'minimist';
|
import minimist from 'minimist';
|
||||||
|
|
||||||
import { glMatrix } from 'gl-matrix';
|
import { glMatrix } from 'gl-matrix';
|
||||||
|
|
||||||
import { GameServer } from './game-server';
|
import { GameServer } from './game-server';
|
||||||
|
|
||||||
import { defaultOptions } from './default-options';
|
import { defaultOptions } from './default-options';
|
||||||
|
|
||||||
glMatrix.setMatrixArrayType(Array);
|
glMatrix.setMatrixArrayType(Array);
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { vec2 } from 'gl-matrix';
|
import { vec2 } from 'gl-matrix';
|
||||||
import { Circle, GameObject, serializesTo, settings, UpdateObjectMessage } from 'shared';
|
import { Circle, GameObject, serializesTo, settings } from 'shared';
|
||||||
import { PhysicalBase } from '../physics/physicals/physical-base';
|
import { PhysicalBase } from '../physics/physicals/physical-base';
|
||||||
import { BoundingBox } from '../physics/bounding-boxes/bounding-box';
|
import { BoundingBox } from '../physics/bounding-boxes/bounding-box';
|
||||||
import { BoundingBoxBase } from '../physics/bounding-boxes/bounding-box-base';
|
import { BoundingBoxBase } from '../physics/bounding-boxes/bounding-box-base';
|
||||||
|
|
|
||||||
8
backend/src/objects/generates-points.ts
Normal file
8
backend/src/objects/generates-points.ts
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
export interface GeneratesPoints {
|
||||||
|
getPoints(): {
|
||||||
|
decla: number;
|
||||||
|
red: number;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const generatesPoints = (a: any): a is GeneratesPoints => a && 'getPoints' in a;
|
||||||
|
|
@ -36,6 +36,8 @@ export class LampPhysical extends LampBase implements StaticPhysical {
|
||||||
return vec2.create();
|
return vec2.create();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public step(deltaTime: number): void {}
|
||||||
|
|
||||||
public distance(target: vec2): number {
|
public distance(target: vec2): number {
|
||||||
return vec2.distance(this.center, target);
|
return vec2.distance(this.center, target);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,15 +10,16 @@ import {
|
||||||
settings,
|
settings,
|
||||||
PlanetBase,
|
PlanetBase,
|
||||||
CharacterTeam,
|
CharacterTeam,
|
||||||
UpdateObjectMessage,
|
|
||||||
} from 'shared';
|
} from 'shared';
|
||||||
|
|
||||||
import { ImmutableBoundingBox } from '../physics/bounding-boxes/immutable-bounding-box';
|
import { ImmutableBoundingBox } from '../physics/bounding-boxes/immutable-bounding-box';
|
||||||
import { StaticPhysical } from '../physics/physicals/static-physical';
|
import { StaticPhysical } from '../physics/physicals/static-physical';
|
||||||
import { UpdateGameObjectMessage } from '../update-game-object-message';
|
import { GeneratesPoints } from './generates-points';
|
||||||
|
|
||||||
@serializesTo(PlanetBase)
|
@serializesTo(PlanetBase)
|
||||||
export class PlanetPhysical extends PlanetBase implements StaticPhysical {
|
export class PlanetPhysical
|
||||||
|
extends PlanetBase
|
||||||
|
implements StaticPhysical, GeneratesPoints {
|
||||||
public readonly canCollide = true;
|
public readonly canCollide = true;
|
||||||
public readonly canMove = false;
|
public readonly canMove = false;
|
||||||
|
|
||||||
|
|
@ -67,41 +68,62 @@ export class PlanetPhysical extends PlanetBase implements StaticPhysical {
|
||||||
return sign * d;
|
return sign * d;
|
||||||
}
|
}
|
||||||
|
|
||||||
public calculateUpdates(): UpdateObjectMessage {
|
public get team(): CharacterTeam {
|
||||||
return new UpdateGameObjectMessage(this, ['ownership']);
|
return this.ownership === 0.5
|
||||||
|
? CharacterTeam.neutral
|
||||||
|
: this.ownership < 0.5
|
||||||
|
? CharacterTeam.decla
|
||||||
|
: CharacterTeam.red;
|
||||||
|
}
|
||||||
|
|
||||||
|
private timeSinceLastPointGeneration = 0;
|
||||||
|
public getPoints(): {
|
||||||
|
decla: number;
|
||||||
|
red: number;
|
||||||
|
} {
|
||||||
|
if (this.timeSinceLastPointGeneration > settings.planetPointGenerationInterval) {
|
||||||
|
this.timeSinceLastPointGeneration = 0;
|
||||||
|
if (this.team !== CharacterTeam.neutral) {
|
||||||
|
this.remoteCall('generatedPoints', settings.planetPointGenerationValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
decla:
|
||||||
|
this.team === CharacterTeam.decla ? settings.planetPointGenerationValue : 0,
|
||||||
|
red: this.team === CharacterTeam.red ? settings.planetPointGenerationValue : 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
decla: 0,
|
||||||
|
red: 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public step(deltaTime: number): void {
|
||||||
|
this.timeSinceLastPointGeneration += deltaTime;
|
||||||
|
|
||||||
|
// In reverse order, so that teams can achieve a 100% control.
|
||||||
|
this.remoteCall('setOwnership', this.ownership);
|
||||||
|
this.takeControl(CharacterTeam.neutral, deltaTime);
|
||||||
}
|
}
|
||||||
|
|
||||||
public takeControl(team: CharacterTeam, deltaTime: number) {
|
public takeControl(team: CharacterTeam, deltaTime: number) {
|
||||||
const previousOwnership = this.ownership;
|
|
||||||
if (team === CharacterTeam.decla) {
|
if (team === CharacterTeam.decla) {
|
||||||
this.ownership -= (0.5 / settings.takeControlTimeInSeconds) * deltaTime;
|
this.ownership -= (0.5 / settings.takeControlTimeInSeconds) * deltaTime;
|
||||||
if (
|
} else if (team === CharacterTeam.red) {
|
||||||
previousOwnership >= 0.5 - settings.planetControlThreshold &&
|
|
||||||
this.ownership < 0.5 - settings.planetControlThreshold
|
|
||||||
) {
|
|
||||||
PlanetPhysical.declaPlanetCount++;
|
|
||||||
PlanetPhysical.neutralPlanetCount--;
|
|
||||||
} else if (
|
|
||||||
previousOwnership > 0.5 + settings.planetControlThreshold &&
|
|
||||||
previousOwnership <= 0.5 + settings.planetControlThreshold
|
|
||||||
) {
|
|
||||||
PlanetPhysical.redPlanetCount--;
|
|
||||||
PlanetPhysical.neutralPlanetCount++;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
this.ownership += (0.5 / settings.takeControlTimeInSeconds) * deltaTime;
|
this.ownership += (0.5 / settings.takeControlTimeInSeconds) * deltaTime;
|
||||||
|
} else {
|
||||||
|
const previous = this.ownership;
|
||||||
|
this.ownership +=
|
||||||
|
-Math.sign(this.ownership - 0.5) *
|
||||||
|
(0.5 / settings.loseControlTimeInSeconds) *
|
||||||
|
deltaTime;
|
||||||
if (
|
if (
|
||||||
previousOwnership < 0.5 + settings.planetControlThreshold &&
|
(previous < 0.5 && this.ownership > 0.5) ||
|
||||||
this.ownership >= 0.5 + settings.planetControlThreshold
|
(previous > 0.5 && this.ownership < 0.5)
|
||||||
) {
|
) {
|
||||||
PlanetPhysical.redPlanetCount++;
|
this.ownership = 0.5;
|
||||||
PlanetPhysical.neutralPlanetCount--;
|
|
||||||
} else if (
|
|
||||||
previousOwnership <= 0.5 - settings.planetControlThreshold &&
|
|
||||||
previousOwnership > 0.5 + settings.planetControlThreshold
|
|
||||||
) {
|
|
||||||
PlanetPhysical.declaPlanetCount--;
|
|
||||||
PlanetPhysical.neutralPlanetCount++;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -21,8 +21,6 @@ import { forceAtPosition } from '../physics/functions/force-at-position';
|
||||||
import { getBoundingBoxOfCircle } from '../physics/functions/get-bounding-box-of-circle';
|
import { getBoundingBoxOfCircle } from '../physics/functions/get-bounding-box-of-circle';
|
||||||
import { PlanetPhysical } from './planet-physical';
|
import { PlanetPhysical } from './planet-physical';
|
||||||
import { ReactsToCollision } from '../physics/physicals/reacts-to-collision';
|
import { ReactsToCollision } from '../physics/physicals/reacts-to-collision';
|
||||||
import { UpdateObjectMessage } from 'shared/lib/src/objects/update-object-message';
|
|
||||||
import { UpdateGameObjectMessage } from '../update-game-object-message';
|
|
||||||
|
|
||||||
@serializesTo(PlayerCharacterBase)
|
@serializesTo(PlayerCharacterBase)
|
||||||
export class PlayerCharacterPhysical
|
export class PlayerCharacterPhysical
|
||||||
|
|
@ -133,20 +131,15 @@ export class PlayerCharacterPhysical
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public calculateUpdates(): UpdateObjectMessage {
|
|
||||||
return new UpdateGameObjectMessage(this, [
|
|
||||||
'head',
|
|
||||||
'leftFoot',
|
|
||||||
'rightFoot',
|
|
||||||
'health',
|
|
||||||
'killCount',
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
public handleMovementAction(c: MoveActionCommand) {
|
public handleMovementAction(c: MoveActionCommand) {
|
||||||
this.movementActions.push(c);
|
this.movementActions.push(c);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private addKill() {
|
||||||
|
this.killCount++;
|
||||||
|
this.remoteCall('setKillCount', this.killCount);
|
||||||
|
}
|
||||||
|
|
||||||
public onCollision(other: GameObject) {
|
public onCollision(other: GameObject) {
|
||||||
if (
|
if (
|
||||||
other instanceof ProjectilePhysical &&
|
other instanceof ProjectilePhysical &&
|
||||||
|
|
@ -155,9 +148,10 @@ export class PlayerCharacterPhysical
|
||||||
) {
|
) {
|
||||||
other.destroy();
|
other.destroy();
|
||||||
this.health -= other.strength;
|
this.health -= other.strength;
|
||||||
|
this.remoteCall('setHealth', this.health);
|
||||||
if (this.health <= 0) {
|
if (this.health <= 0) {
|
||||||
this.destroy();
|
this.destroy();
|
||||||
other.originator.killCount++;
|
other.originator.addKill();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -182,6 +176,8 @@ export class PlayerCharacterPhysical
|
||||||
this.container,
|
this.container,
|
||||||
);
|
);
|
||||||
this.container.addObject(projectile);
|
this.container.addObject(projectile);
|
||||||
|
|
||||||
|
this.remoteCall('onShoot', strength);
|
||||||
}
|
}
|
||||||
|
|
||||||
public get boundingBox(): BoundingBoxBase {
|
public get boundingBox(): BoundingBoxBase {
|
||||||
|
|
@ -299,6 +295,7 @@ export class PlayerCharacterPhysical
|
||||||
this.stepBodyPart(this.leftFoot, deltaTime);
|
this.stepBodyPart(this.leftFoot, deltaTime);
|
||||||
this.stepBodyPart(this.rightFoot, deltaTime);
|
this.stepBodyPart(this.rightFoot, deltaTime);
|
||||||
this.keepPosture();
|
this.keepPosture();
|
||||||
|
this.remoteCall('updateCircles', this.head, this.leftFoot, this.rightFoot);
|
||||||
}
|
}
|
||||||
|
|
||||||
private setDirection(direction: vec2, deltaTime: number) {
|
private setDirection(direction: vec2, deltaTime: number) {
|
||||||
|
|
|
||||||
|
|
@ -13,8 +13,6 @@ import { DynamicPhysical } from '../physics/physicals/dynamic-physical';
|
||||||
import { PhysicalContainer } from '../physics/containers/physical-container';
|
import { PhysicalContainer } from '../physics/containers/physical-container';
|
||||||
import { PlanetPhysical } from './planet-physical';
|
import { PlanetPhysical } from './planet-physical';
|
||||||
import { ReactsToCollision } from '../physics/physicals/reacts-to-collision';
|
import { ReactsToCollision } from '../physics/physicals/reacts-to-collision';
|
||||||
import { UpdateObjectMessage } from 'shared/lib/src/objects/update-object-message';
|
|
||||||
import { UpdateGameObjectMessage } from '../update-game-object-message';
|
|
||||||
import { PlayerCharacterPhysical } from './player-character-physical';
|
import { PlayerCharacterPhysical } from './player-character-physical';
|
||||||
import { moveCircle } from '../physics/functions/move-circle';
|
import { moveCircle } from '../physics/functions/move-circle';
|
||||||
|
|
||||||
|
|
@ -68,10 +66,6 @@ export class ProjectilePhysical
|
||||||
vec2.add(this.center, this.center, delta);
|
vec2.add(this.center, this.center, delta);
|
||||||
}
|
}
|
||||||
|
|
||||||
public calculateUpdates(): UpdateObjectMessage {
|
|
||||||
return new UpdateGameObjectMessage(this, ['center', 'strength']);
|
|
||||||
}
|
|
||||||
|
|
||||||
public get boundingBox(): ImmutableBoundingBox {
|
public get boundingBox(): ImmutableBoundingBox {
|
||||||
if (!this._boundingBox) {
|
if (!this._boundingBox) {
|
||||||
this._boundingBox = (this.object as CirclePhysical).boundingBox;
|
this._boundingBox = (this.object as CirclePhysical).boundingBox;
|
||||||
|
|
@ -105,7 +99,9 @@ export class ProjectilePhysical
|
||||||
}
|
}
|
||||||
|
|
||||||
public step(deltaTime: number) {
|
public step(deltaTime: number) {
|
||||||
if ((this.strength -= settings.projectileFadeSpeed * deltaTime) < 0) {
|
super.step(deltaTime);
|
||||||
|
|
||||||
|
if (this.strength <= 0) {
|
||||||
this.destroy();
|
this.destroy();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -122,5 +118,7 @@ export class ProjectilePhysical
|
||||||
|
|
||||||
this.object.velocity = this.velocity;
|
this.object.velocity = this.velocity;
|
||||||
this.object.step2(deltaTime);
|
this.object.step2(deltaTime);
|
||||||
|
|
||||||
|
this.remoteCall('setCenter', this.center);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ export interface Options {
|
||||||
port: number;
|
port: number;
|
||||||
name: string;
|
name: string;
|
||||||
playerLimit: number;
|
playerLimit: number;
|
||||||
|
scoreLimit: number;
|
||||||
seed: number;
|
seed: number;
|
||||||
worldSize: number;
|
worldSize: number;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,12 +6,14 @@ import { BoundingBoxTree } from './bounding-box-tree';
|
||||||
import { Physical } from '../physicals/physical';
|
import { Physical } from '../physicals/physical';
|
||||||
import { StaticPhysical } from '../physicals/static-physical';
|
import { StaticPhysical } from '../physicals/static-physical';
|
||||||
import { DynamicPhysical } from '../physicals/dynamic-physical';
|
import { DynamicPhysical } from '../physicals/dynamic-physical';
|
||||||
|
import { GeneratesPoints, generatesPoints } from '../../objects/generates-points';
|
||||||
|
|
||||||
export class PhysicalContainer {
|
export class PhysicalContainer {
|
||||||
private isTreeInitialized = false;
|
private isTreeInitialized = false;
|
||||||
private staticBoundingBoxesWaitList: Array<StaticPhysical> = [];
|
private staticBoundingBoxesWaitList: Array<StaticPhysical> = [];
|
||||||
private staticBoundingBoxes = new BoundingBoxTree();
|
private staticBoundingBoxes = new BoundingBoxTree();
|
||||||
private dynamicBoundingBoxes = new BoundingBoxList();
|
private dynamicBoundingBoxes = new BoundingBoxList();
|
||||||
|
private objects: Array<Physical> = [];
|
||||||
|
|
||||||
public initialize() {
|
public initialize() {
|
||||||
this.staticBoundingBoxes.build(this.staticBoundingBoxesWaitList);
|
this.staticBoundingBoxes.build(this.staticBoundingBoxesWaitList);
|
||||||
|
|
@ -19,6 +21,8 @@ export class PhysicalContainer {
|
||||||
}
|
}
|
||||||
|
|
||||||
public addObject(physical: Physical) {
|
public addObject(physical: Physical) {
|
||||||
|
this.objects.push(physical);
|
||||||
|
|
||||||
if (physical.canMove) {
|
if (physical.canMove) {
|
||||||
this.dynamicBoundingBoxes.insert(physical);
|
this.dynamicBoundingBoxes.insert(physical);
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -31,11 +35,31 @@ export class PhysicalContainer {
|
||||||
}
|
}
|
||||||
|
|
||||||
public removeObject(object: DynamicPhysical) {
|
public removeObject(object: DynamicPhysical) {
|
||||||
|
this.objects = this.objects.filter((p) => p !== object);
|
||||||
this.dynamicBoundingBoxes.remove(object);
|
this.dynamicBoundingBoxes.remove(object);
|
||||||
}
|
}
|
||||||
|
|
||||||
public stepObjects(deltaTime: number) {
|
public stepObjects(deltaTime: number) {
|
||||||
this.dynamicBoundingBoxes.forEach((o) => o.step(deltaTime));
|
this.objects.forEach((o) => o.step(deltaTime));
|
||||||
|
}
|
||||||
|
|
||||||
|
public resetRemoteCalls() {
|
||||||
|
this.objects.forEach((o) => o.gameObject.resetRemoteCalls());
|
||||||
|
}
|
||||||
|
|
||||||
|
public getPointsGenerated(): { decla: number; red: number } {
|
||||||
|
return this.objects
|
||||||
|
.filter((o) => generatesPoints(o))
|
||||||
|
.reduce(
|
||||||
|
(sum, o) => {
|
||||||
|
const { decla, red } = ((o as unknown) as GeneratesPoints).getPoints();
|
||||||
|
return {
|
||||||
|
decla: sum.decla + decla,
|
||||||
|
red: sum.red + red,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
{ decla: 0, red: 0 },
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public findIntersecting(box: BoundingBoxBase): Array<Physical> {
|
public findIntersecting(box: BoundingBoxBase): Array<Physical> {
|
||||||
|
|
|
||||||
|
|
@ -2,5 +2,4 @@ import { PhysicalBase } from './physical-base';
|
||||||
|
|
||||||
export interface DynamicPhysical extends PhysicalBase {
|
export interface DynamicPhysical extends PhysicalBase {
|
||||||
readonly canMove: true;
|
readonly canMove: true;
|
||||||
step(deltaTimeInMilliseconds: number): void;
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,5 +9,5 @@ export interface PhysicalBase {
|
||||||
readonly gameObject: GameObject;
|
readonly gameObject: GameObject;
|
||||||
|
|
||||||
distance(target: vec2): number;
|
distance(target: vec2): number;
|
||||||
step(deltaTimeInMilliseconds: number): void;
|
step(deltaTime: number): void;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { CharacterTeam, PlayerInformation, Random } from 'shared';
|
import { CharacterTeam, PlayerInformation, Random, TransportEvents } from 'shared';
|
||||||
import { PhysicalContainer } from '../physics/containers/physical-container';
|
import { PhysicalContainer } from '../physics/containers/physical-container';
|
||||||
import { Player } from './player';
|
import { Player } from './player';
|
||||||
|
|
||||||
|
|
@ -32,6 +32,10 @@ export class PlayerContainer {
|
||||||
this.players.forEach((p) => p.step(deltaTimeInSeconds));
|
this.players.forEach((p) => p.step(deltaTimeInSeconds));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public sendOnSocket(message: any) {
|
||||||
|
this.players.forEach((p) => p.socket.emit(TransportEvents.ServerToPlayer, message));
|
||||||
|
}
|
||||||
|
|
||||||
private getTeamOfNextPlayer(): CharacterTeam {
|
private getTeamOfNextPlayer(): CharacterTeam {
|
||||||
const declaCount = this._players.filter((p) => p.team === CharacterTeam.decla).length;
|
const declaCount = this._players.filter((p) => p.team === CharacterTeam.decla).length;
|
||||||
const redCount = this._players.filter((p) => p.team === CharacterTeam.red).length;
|
const redCount = this._players.filter((p) => p.team === CharacterTeam.red).length;
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,6 @@ import {
|
||||||
MoveActionCommand,
|
MoveActionCommand,
|
||||||
serialize,
|
serialize,
|
||||||
TransportEvents,
|
TransportEvents,
|
||||||
UpdateObjectsCommand,
|
|
||||||
SetAspectRatioActionCommand,
|
SetAspectRatioActionCommand,
|
||||||
calculateViewArea,
|
calculateViewArea,
|
||||||
SecondaryActionCommand,
|
SecondaryActionCommand,
|
||||||
|
|
@ -17,11 +16,13 @@ import {
|
||||||
Circle,
|
Circle,
|
||||||
PlayerInformation,
|
PlayerInformation,
|
||||||
CharacterTeam,
|
CharacterTeam,
|
||||||
UpdateGameState,
|
UpdateOtherPlayerDirections,
|
||||||
GameObject,
|
GameObject,
|
||||||
Command,
|
Command,
|
||||||
UpdateObjectMessage,
|
|
||||||
OtherPlayerDirection,
|
OtherPlayerDirection,
|
||||||
|
RemoteCallsForObject,
|
||||||
|
RemoteCallsForObjects,
|
||||||
|
ServerAnnouncement,
|
||||||
} from 'shared';
|
} from 'shared';
|
||||||
import { getTimeInMilliseconds } from '../helper/get-time-in-milliseconds';
|
import { getTimeInMilliseconds } from '../helper/get-time-in-milliseconds';
|
||||||
import { BoundingBox } from '../physics/bounding-boxes/bounding-box';
|
import { BoundingBox } from '../physics/bounding-boxes/bounding-box';
|
||||||
|
|
@ -70,7 +71,7 @@ export class Player extends CommandReceiver {
|
||||||
private readonly playerInfo: PlayerInformation,
|
private readonly playerInfo: PlayerInformation,
|
||||||
private readonly playerContainer: PlayerContainer,
|
private readonly playerContainer: PlayerContainer,
|
||||||
private readonly objectContainer: PhysicalContainer,
|
private readonly objectContainer: PhysicalContainer,
|
||||||
private readonly socket: SocketIO.Socket,
|
public readonly socket: SocketIO.Socket,
|
||||||
public readonly team: CharacterTeam,
|
public readonly team: CharacterTeam,
|
||||||
) {
|
) {
|
||||||
super();
|
super();
|
||||||
|
|
@ -123,8 +124,15 @@ export class Player extends CommandReceiver {
|
||||||
this.character = null;
|
this.character = null;
|
||||||
this.timeUntilRespawn = settings.playerDiedTimeout;
|
this.timeUntilRespawn = settings.playerDiedTimeout;
|
||||||
}
|
}
|
||||||
} else if ((this.timeUntilRespawn -= deltaTimeInSeconds) < 0) {
|
} else {
|
||||||
|
this.sendToPlayer(
|
||||||
|
new ServerAnnouncement(`Reviving in ${Math.round(this.timeUntilRespawn)}…`),
|
||||||
|
);
|
||||||
|
if ((this.timeUntilRespawn -= deltaTimeInSeconds) < 0) {
|
||||||
this.createCharacter();
|
this.createCharacter();
|
||||||
|
this.center = this.character!.center;
|
||||||
|
this.sendToPlayer(new ServerAnnouncement(''));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const viewArea = calculateViewArea(this.center, this.aspectRatio, 1.5);
|
const viewArea = calculateViewArea(this.center, this.aspectRatio, 1.5);
|
||||||
|
|
@ -155,21 +163,14 @@ export class Player extends CommandReceiver {
|
||||||
}
|
}
|
||||||
|
|
||||||
this.sendToPlayer(
|
this.sendToPlayer(
|
||||||
new UpdateObjectsCommand(
|
new RemoteCallsForObjects(
|
||||||
this.objectsPreviouslyInViewArea
|
this.objectsPreviouslyInViewArea.map(
|
||||||
.map((g) => g.calculateUpdates())
|
(g) => new RemoteCallsForObject(g.id, g.getRemoteCalls()),
|
||||||
.filter((u) => u) as Array<UpdateObjectMessage>,
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
this.sendToPlayer(
|
this.sendToPlayer(new UpdateOtherPlayerDirections(this.getOtherPlayers()));
|
||||||
new UpdateGameState(
|
|
||||||
PlanetPhysical.declaPlanetCount,
|
|
||||||
PlanetPhysical.redPlanetCount,
|
|
||||||
PlanetPhysical.neutralPlanetCount,
|
|
||||||
this.getOtherPlayers(),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private findEmptyPositionForPlayer(): vec2 {
|
private findEmptyPositionForPlayer(): vec2 {
|
||||||
|
|
@ -223,7 +224,7 @@ export class Player extends CommandReceiver {
|
||||||
.filter((g) => g instanceof PlayerCharacterPhysical);
|
.filter((g) => g instanceof PlayerCharacterPhysical);
|
||||||
|
|
||||||
const otherPlayers = this.playerContainer.players.filter(
|
const otherPlayers = this.playerContainer.players.filter(
|
||||||
(p) => playersInViewArea.indexOf(p.character!) < 0,
|
(p) => p.character?.isAlive && playersInViewArea.indexOf(p.character!) < 0,
|
||||||
);
|
);
|
||||||
|
|
||||||
return otherPlayers.map(
|
return otherPlayers.map(
|
||||||
|
|
@ -244,7 +245,6 @@ export class Player extends CommandReceiver {
|
||||||
|
|
||||||
public destroy() {
|
public destroy() {
|
||||||
this.isActive = false;
|
this.isActive = false;
|
||||||
|
|
||||||
this.character?.destroy();
|
this.character?.destroy();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,11 +0,0 @@
|
||||||
import { GameObject, serializesTo, UpdateMessage, UpdateObjectMessage } from 'shared';
|
|
||||||
|
|
||||||
@serializesTo(UpdateObjectMessage)
|
|
||||||
export class UpdateGameObjectMessage<T extends GameObject> extends UpdateObjectMessage {
|
|
||||||
constructor(object: T, keys: Array<string & keyof T>) {
|
|
||||||
super(
|
|
||||||
object.id,
|
|
||||||
keys.map((k) => new UpdateMessage(k, object[k])),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -108,7 +108,12 @@ const applyServerContainerShadows = () => {
|
||||||
const main = async () => {
|
const main = async () => {
|
||||||
try {
|
try {
|
||||||
let game: Game;
|
let game: Game;
|
||||||
|
|
||||||
|
const firstClickListener = () => {
|
||||||
SoundHandler.initialize();
|
SoundHandler.initialize();
|
||||||
|
document.removeEventListener('click', firstClickListener);
|
||||||
|
};
|
||||||
|
document.addEventListener('click', firstClickListener);
|
||||||
|
|
||||||
handleFullScreen(minimize, maximize);
|
handleFullScreen(minimize, maximize);
|
||||||
toggleSettingsButton.addEventListener('click', toggleSettings);
|
toggleSettingsButton.addEventListener('click', toggleSettings);
|
||||||
|
|
@ -134,7 +139,7 @@ const main = async () => {
|
||||||
|
|
||||||
for (;;) {
|
for (;;) {
|
||||||
show(spinner);
|
show(spinner);
|
||||||
hide(logoutButton);
|
hide(logoutButton, true);
|
||||||
show(landingUI, true, 'flex');
|
show(landingUI, true, 'flex');
|
||||||
|
|
||||||
const background = new LandingPageBackground(canvas);
|
const background = new LandingPageBackground(canvas);
|
||||||
|
|
@ -156,7 +161,7 @@ const main = async () => {
|
||||||
await game.started;
|
await game.started;
|
||||||
isInGame = true;
|
isInGame = true;
|
||||||
hide(spinner);
|
hide(spinner);
|
||||||
show(logoutButton);
|
show(logoutButton, true, 'block');
|
||||||
await gameOver;
|
await gameOver;
|
||||||
isInGame = false;
|
isInGame = false;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -37,7 +37,9 @@ h1 {
|
||||||
font-size: 6rem;
|
font-size: 6rem;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
padding-bottom: $medium-padding;
|
padding-bottom: $medium-padding;
|
||||||
|
@media (max-width: $height-breakpoint) {
|
||||||
|
font-size: 4.5rem;
|
||||||
|
}
|
||||||
@media (max-height: $height-breakpoint) {
|
@media (max-height: $height-breakpoint) {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
@ -163,11 +165,23 @@ body {
|
||||||
font-size: 0;
|
font-size: 0;
|
||||||
transform: translateX(-50%) translateY(-50%);
|
transform: translateX(-50%) translateY(-50%);
|
||||||
|
|
||||||
|
box-shadow: inset 0 0 3px 0px rgba(0, 0, 0, 0.2);
|
||||||
|
|
||||||
@include square(50px);
|
@include square(50px);
|
||||||
border-radius: 1000px;
|
border-radius: 1000px;
|
||||||
mask-image: url('../static/mask.svg');
|
mask-image: url('../static/mask.svg');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.falling-point {
|
||||||
|
font-size: 1.2em;
|
||||||
|
&.decla {
|
||||||
|
color: $bright-decla;
|
||||||
|
}
|
||||||
|
&.red {
|
||||||
|
color: $bright-red;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.other-player-arrow {
|
.other-player-arrow {
|
||||||
@include square($large-icon);
|
@include square($large-icon);
|
||||||
@media (max-width: $breakpoint) {
|
@media (max-width: $breakpoint) {
|
||||||
|
|
@ -205,18 +219,29 @@ body {
|
||||||
}
|
}
|
||||||
|
|
||||||
.announcement {
|
.announcement {
|
||||||
top: 50%;
|
top: 25%;
|
||||||
left: 50%;
|
transform: translateX(calc(-50% + 50vw)) translateY(-50%);
|
||||||
transform: translateX(-50%) translateY(-50%);
|
|
||||||
font-size: 3rem;
|
font-size: 3rem;
|
||||||
@include background;
|
@include background;
|
||||||
z-index: 1000;
|
z-index: 1000;
|
||||||
padding: $medium-padding;
|
padding: $medium-padding;
|
||||||
border-radius: 16px;
|
border-radius: 16px;
|
||||||
|
|
||||||
|
&:empty {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.decla {
|
||||||
|
color: $bright-decla;
|
||||||
|
}
|
||||||
|
|
||||||
|
.red {
|
||||||
|
color: $bright-red;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.planet-progress {
|
.planet-progress {
|
||||||
position: absolute;
|
|
||||||
top: $small-padding;
|
top: $small-padding;
|
||||||
left: 50%;
|
left: 50%;
|
||||||
transform: translateX(-50%);
|
transform: translateX(-50%);
|
||||||
|
|
@ -225,26 +250,50 @@ body {
|
||||||
$height: 8px;
|
$height: 8px;
|
||||||
height: $height;
|
height: $height;
|
||||||
|
|
||||||
|
justify-content: space-between;
|
||||||
|
box-shadow: inset 0 0 3px 0px rgba(0, 0, 0, 0.2);
|
||||||
|
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
z-index: 100;
|
z-index: 100;
|
||||||
|
|
||||||
div {
|
&::before {
|
||||||
height: $height;
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
left: 50%;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateX(-50%) translateY(-50%);
|
||||||
|
background-color: #888;
|
||||||
|
height: 24px;
|
||||||
|
width: 4px;
|
||||||
|
border-radius: 1000px;
|
||||||
}
|
}
|
||||||
|
|
||||||
border-radius: 100px;
|
&::after {
|
||||||
overflow: hidden;
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
left: 50%;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateX(-50%) translateY(50%);
|
||||||
|
|
||||||
|
@include square(24px);
|
||||||
|
|
||||||
|
background-image: url('../static/flag.svg');
|
||||||
|
background-size: contain;
|
||||||
|
}
|
||||||
|
|
||||||
|
div {
|
||||||
|
height: $height;
|
||||||
|
box-shadow: inset 0 0 3px 0px rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
div:nth-child(1) {
|
div:nth-child(1) {
|
||||||
background: $bright-decla;
|
background: $bright-decla;
|
||||||
|
border-radius: 100px 0 0 100px;
|
||||||
}
|
}
|
||||||
|
|
||||||
div:nth-child(2) {
|
div:nth-child(2) {
|
||||||
background: $bright-neutral;
|
|
||||||
}
|
|
||||||
|
|
||||||
div:nth-child(3) {
|
|
||||||
background: $bright-red;
|
background: $bright-red;
|
||||||
|
border-radius: 0 100px 100px 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,100 +1,140 @@
|
||||||
import { vec2 } from 'gl-matrix';
|
import { vec2 } from 'gl-matrix';
|
||||||
import {
|
import { Renderer, renderNoise } from 'sdf-2d';
|
||||||
CircleLight,
|
|
||||||
ColorfulCircle,
|
|
||||||
FilteringOptions,
|
|
||||||
Flashlight,
|
|
||||||
Renderer,
|
|
||||||
renderNoise,
|
|
||||||
runAnimation,
|
|
||||||
WrapOptions,
|
|
||||||
} from 'sdf-2d';
|
|
||||||
import {
|
import {
|
||||||
broadcastCommands,
|
broadcastCommands,
|
||||||
deserialize,
|
deserialize,
|
||||||
serialize,
|
serialize,
|
||||||
settings,
|
|
||||||
TransportEvents,
|
TransportEvents,
|
||||||
SetAspectRatioActionCommand,
|
SetAspectRatioActionCommand,
|
||||||
PlayerInformation,
|
PlayerInformation,
|
||||||
PlayerDiedCommand,
|
PlayerDiedCommand,
|
||||||
UpdateGameState,
|
UpdateOtherPlayerDirections,
|
||||||
clamp,
|
clamp,
|
||||||
|
UpdateGameState,
|
||||||
|
GameEnd,
|
||||||
|
CharacterTeam,
|
||||||
|
ServerAnnouncement,
|
||||||
|
GameStart,
|
||||||
|
CommandReceiver,
|
||||||
|
CommandExecutors,
|
||||||
|
Command,
|
||||||
} from 'shared';
|
} from 'shared';
|
||||||
import io from 'socket.io-client';
|
import io from 'socket.io-client';
|
||||||
import { KeyboardListener } from './commands/generators/keyboard-listener';
|
import { KeyboardListener } from './commands/generators/keyboard-listener';
|
||||||
import { MouseListener } from './commands/generators/mouse-listener';
|
import { MouseListener } from './commands/generators/mouse-listener';
|
||||||
import { TouchJoystickListener } from './commands/generators/touch-joystick-listener';
|
import { TouchJoystickListener } from './commands/generators/touch-joystick-listener';
|
||||||
import { CommandReceiverSocket } from './commands/receivers/command-receiver-socket';
|
import { CommandReceiverSocket } from './commands/receivers/command-receiver-socket';
|
||||||
|
import { startAnimation } from './start-animation';
|
||||||
import { PlayerDecision } from './join-form-handler';
|
import { PlayerDecision } from './join-form-handler';
|
||||||
import { GameObjectContainer } from './objects/game-object-container';
|
import { GameObjectContainer } from './objects/game-object-container';
|
||||||
import { OptionsHandler } from './options-handler';
|
import { OptionsHandler } from './options-handler';
|
||||||
import { BlobShape } from './shapes/blob-shape';
|
|
||||||
import { PlanetShape } from './shapes/planet-shape';
|
|
||||||
|
|
||||||
export class Game {
|
export class Game extends CommandReceiver {
|
||||||
public readonly gameObjects = new GameObjectContainer(this);
|
public gameObjects = new GameObjectContainer(this);
|
||||||
public renderer?: Renderer;
|
public renderer?: Renderer;
|
||||||
private socket!: SocketIOClient.Socket;
|
private socket!: SocketIOClient.Socket;
|
||||||
private deadTimeout = 0;
|
private isBetweenGames = false;
|
||||||
|
|
||||||
|
public started: Promise<void>;
|
||||||
|
private resolveStarted!: () => unknown;
|
||||||
|
|
||||||
private declaPlanetCountElement = document.createElement('div');
|
private declaPlanetCountElement = document.createElement('div');
|
||||||
private redPlanetCountElement = document.createElement('div');
|
private redPlanetCountElement = document.createElement('div');
|
||||||
private neutralPlanetCountElement = document.createElement('div');
|
|
||||||
private announcementText = document.createElement('h2');
|
private announcementText = document.createElement('h2');
|
||||||
|
private progressBar = document.createElement('div');
|
||||||
|
private arrowElements: Array<HTMLElement> = [];
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly playerDecision: PlayerDecision,
|
private readonly playerDecision: PlayerDecision,
|
||||||
private readonly canvas: HTMLCanvasElement,
|
private readonly canvas: HTMLCanvasElement,
|
||||||
private readonly overlay: HTMLElement,
|
private readonly overlay: HTMLElement,
|
||||||
) {
|
) {
|
||||||
|
super();
|
||||||
|
this.started = new Promise((r) => (this.resolveStarted = r));
|
||||||
this.announcementText.className = 'announcement';
|
this.announcementText.className = 'announcement';
|
||||||
const progressBar = document.createElement('div');
|
this.progressBar.className = 'planet-progress';
|
||||||
progressBar.className = 'planet-progress';
|
this.progressBar.appendChild(this.declaPlanetCountElement);
|
||||||
overlay.appendChild(progressBar);
|
this.progressBar.appendChild(this.redPlanetCountElement);
|
||||||
progressBar.appendChild(this.declaPlanetCountElement);
|
|
||||||
progressBar.appendChild(this.neutralPlanetCountElement);
|
|
||||||
progressBar.appendChild(this.redPlanetCountElement);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private arrowElements: Array<HTMLElement> = [];
|
private initialize() {
|
||||||
private async setupCommunication(serverUrl: string): Promise<void> {
|
this.isBetweenGames = true;
|
||||||
this.socket = io(serverUrl, {
|
|
||||||
|
this.socket?.close();
|
||||||
|
this.gameObjects = new GameObjectContainer(this);
|
||||||
|
this.overlay.innerHTML = '';
|
||||||
|
this.overlay.appendChild(this.progressBar);
|
||||||
|
this.announcementText.innerText = '';
|
||||||
|
this.overlay.appendChild(this.announcementText);
|
||||||
|
|
||||||
|
this.socket = io(this.playerDecision.server, {
|
||||||
reconnectionDelayMax: 10000,
|
reconnectionDelayMax: 10000,
|
||||||
transports: ['websocket'],
|
transports: ['websocket'],
|
||||||
|
forceNew: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
this.socket.on('reconnect_attempt', () => {
|
this.socket.on('reconnect_attempt', () => {
|
||||||
this.socket.io.opts.transports = ['polling', 'websocket'];
|
this.socket.io.opts.transports = ['polling', 'websocket'];
|
||||||
});
|
});
|
||||||
|
|
||||||
this.socket.on('disconnect', this.destroy.bind(this));
|
this.socket.on('disconnect', () => {
|
||||||
|
if (!this.isBetweenGames) {
|
||||||
|
this.destroy();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
this.socket.on(TransportEvents.ServerToPlayer, (serialized: string) => {
|
this.socket.on(TransportEvents.Ping, () => {
|
||||||
const command = deserialize(serialized);
|
this.socket.emit(TransportEvents.Pong);
|
||||||
if (command instanceof PlayerDiedCommand) {
|
});
|
||||||
this.deadTimeout = command.timeout;
|
|
||||||
|
this.socket.on(TransportEvents.ServerToPlayer, (serialized: string) =>
|
||||||
|
this.sendCommand(deserialize(serialized)),
|
||||||
|
);
|
||||||
|
|
||||||
|
broadcastCommands(
|
||||||
|
[
|
||||||
|
new KeyboardListener(document.body),
|
||||||
|
new MouseListener(this.canvas, this),
|
||||||
|
new TouchJoystickListener(this.canvas, this.overlay, this),
|
||||||
|
],
|
||||||
|
[this.gameObjects, new CommandReceiverSocket(this.socket)],
|
||||||
|
);
|
||||||
|
|
||||||
|
this.isBetweenGames = false;
|
||||||
|
|
||||||
|
this.socket.emit(TransportEvents.PlayerJoining, {
|
||||||
|
name: this.playerDecision.playerName,
|
||||||
|
} as PlayerInformation);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected defaultCommandExecutor(c: Command) {
|
||||||
|
this.gameObjects.sendCommand(c);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected commandExecutors: CommandExecutors = {
|
||||||
|
[ServerAnnouncement.type]: (c: ServerAnnouncement) =>
|
||||||
|
(this.announcementText.innerText = c.text),
|
||||||
|
[PlayerDiedCommand.type]: (c: PlayerDiedCommand) => {
|
||||||
if (OptionsHandler.options.vibrationEnabled) {
|
if (OptionsHandler.options.vibrationEnabled) {
|
||||||
navigator.vibrate(150);
|
navigator.vibrate(150);
|
||||||
}
|
}
|
||||||
this.overlay.appendChild(this.announcementText);
|
},
|
||||||
} else if (command instanceof UpdateGameState) {
|
[UpdateGameState.type]: (c: UpdateGameState) => {
|
||||||
const all = command.declaCount + command.redCount + command.neutralCount;
|
this.declaPlanetCountElement.style.width = (c.declaCount / c.limit) * 50 + '%';
|
||||||
this.declaPlanetCountElement.style.width = (command.declaCount / all) * 100 + '%';
|
this.redPlanetCountElement.style.width = (c.redCount / c.limit) * 50 + '%';
|
||||||
this.neutralPlanetCountElement.style.width =
|
},
|
||||||
(command.neutralCount / all) * 100 + '%';
|
[GameEnd.type]: (c: GameEnd) => {
|
||||||
this.redPlanetCountElement.style.width = (command.redCount / all) * 100 + '%';
|
const team =
|
||||||
|
c.winningTeam === CharacterTeam.decla
|
||||||
if (command.declaCount > all * 0.5) {
|
? '<span class="decla">decla</span>'
|
||||||
this.overlay.appendChild(this.announcementText);
|
: '<span class="red">red</span>';
|
||||||
this.announcementText.innerText = 'Decla team won 🎉';
|
this.announcementText.innerHTML = `Team ${team} won 🎉`;
|
||||||
}
|
},
|
||||||
|
[UpdateOtherPlayerDirections.type]: this.handleOtherPlayerDirections.bind(this),
|
||||||
if (command.redCount > all * 0.5) {
|
[GameStart.type]: this.initialize.bind(this),
|
||||||
this.overlay.appendChild(this.announcementText);
|
};
|
||||||
this.announcementText.innerText = 'Red team won 🎉';
|
|
||||||
}
|
|
||||||
|
|
||||||
|
private handleOtherPlayerDirections(command: UpdateOtherPlayerDirections) {
|
||||||
this.arrowElements
|
this.arrowElements
|
||||||
.splice(command.otherPlayerDirections.length, this.arrowElements.length)
|
.splice(command.otherPlayerDirections.length, this.arrowElements.length)
|
||||||
.forEach((e) => e.parentElement?.removeChild(e));
|
.forEach((e) => e.parentElement?.removeChild(e));
|
||||||
|
|
@ -141,76 +181,12 @@ export class Game {
|
||||||
p.y
|
p.y
|
||||||
}px) translateX(-50%) translateY(-50%) rotate(${-angle + Math.PI / 2}rad) `;
|
}px) translateX(-50%) translateY(-50%) rotate(${-angle + Math.PI / 2}rad) `;
|
||||||
});
|
});
|
||||||
} else this.gameObjects.sendCommand(command);
|
|
||||||
});
|
|
||||||
|
|
||||||
this.socket.on(TransportEvents.Ping, () => {
|
|
||||||
this.socket.emit(TransportEvents.Pong);
|
|
||||||
});
|
|
||||||
|
|
||||||
this.socket.emit(TransportEvents.PlayerJoining, {
|
|
||||||
name: this.playerDecision.playerName,
|
|
||||||
} as PlayerInformation);
|
|
||||||
|
|
||||||
broadcastCommands(
|
|
||||||
[
|
|
||||||
new KeyboardListener(document.body),
|
|
||||||
new MouseListener(this.canvas, this),
|
|
||||||
new TouchJoystickListener(this.canvas, this.overlay, this),
|
|
||||||
],
|
|
||||||
[this.gameObjects, new CommandReceiverSocket(this.socket)],
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async start(): Promise<void> {
|
public async start(): Promise<void> {
|
||||||
const noiseTexture = await renderNoise([256, 256], 2, 1);
|
const noiseTexture = await renderNoise([256, 256], 2, 1);
|
||||||
this.setupCommunication(this.playerDecision.server);
|
this.initialize();
|
||||||
await runAnimation(
|
await startAnimation(this.canvas, this.gameLoop.bind(this), noiseTexture);
|
||||||
this.canvas,
|
|
||||||
[
|
|
||||||
{
|
|
||||||
...PlanetShape.descriptor,
|
|
||||||
shaderCombinationSteps: [0, 1, 2, 3],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
...BlobShape.descriptor,
|
|
||||||
shaderCombinationSteps: [0, 1, 2, 8],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
...ColorfulCircle.descriptor,
|
|
||||||
shaderCombinationSteps: [0, 2, 16],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
...CircleLight.descriptor,
|
|
||||||
shaderCombinationSteps: [0, 1, 2, 4, 8, 16],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
...Flashlight.descriptor,
|
|
||||||
shaderCombinationSteps: [0],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
this.gameLoop.bind(this),
|
|
||||||
{
|
|
||||||
shadowTraceCount: 16,
|
|
||||||
paletteSize: settings.palette.length,
|
|
||||||
//enableStopwatch: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
colorPalette: settings.palette,
|
|
||||||
enableHighDpiRendering: true,
|
|
||||||
lightCutoffDistance: settings.lightCutoffDistance,
|
|
||||||
textures: {
|
|
||||||
noiseTexture: {
|
|
||||||
source: noiseTexture,
|
|
||||||
overrides: {
|
|
||||||
maxFilter: FilteringOptions.LINEAR,
|
|
||||||
wrapS: WrapOptions.MIRRORED_REPEAT,
|
|
||||||
wrapT: WrapOptions.MIRRORED_REPEAT,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
);
|
|
||||||
this.socket.close();
|
this.socket.close();
|
||||||
this.overlay.innerHTML = '';
|
this.overlay.innerHTML = '';
|
||||||
}
|
}
|
||||||
|
|
@ -237,17 +213,13 @@ export class Game {
|
||||||
|
|
||||||
private gameLoop(
|
private gameLoop(
|
||||||
renderer: Renderer,
|
renderer: Renderer,
|
||||||
currentTime: DOMHighResTimeStamp,
|
_: DOMHighResTimeStamp,
|
||||||
deltaTime: DOMHighResTimeStamp,
|
deltaTime: DOMHighResTimeStamp,
|
||||||
): boolean {
|
): boolean {
|
||||||
|
this.resolveStarted();
|
||||||
|
deltaTime /= 1000;
|
||||||
|
|
||||||
this.renderer = renderer;
|
this.renderer = renderer;
|
||||||
|
|
||||||
if ((this.deadTimeout -= deltaTime / 1000) > 0) {
|
|
||||||
this.announcementText.innerText = `Reviving in ${Math.floor(this.deadTimeout)}…`;
|
|
||||||
} else {
|
|
||||||
this.announcementText.parentElement?.removeChild(this.announcementText);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.gameObjects.stepObjects(deltaTime);
|
this.gameObjects.stepObjects(deltaTime);
|
||||||
this.gameObjects.drawObjects(this.renderer, this.overlay);
|
this.gameObjects.drawObjects(this.renderer, this.overlay);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -95,6 +95,9 @@ class ServerChooserOption {
|
||||||
private divElement = document.createElement('div');
|
private divElement = document.createElement('div');
|
||||||
private inputElement = document.createElement('input');
|
private inputElement = document.createElement('input');
|
||||||
private labelElement = document.createElement('label');
|
private labelElement = document.createElement('label');
|
||||||
|
private serverNameElement = document.createElement('span');
|
||||||
|
private completionElement = document.createElement('span');
|
||||||
|
|
||||||
private socket: SocketIOClient.Socket;
|
private socket: SocketIOClient.Socket;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
|
|
@ -111,7 +114,11 @@ class ServerChooserOption {
|
||||||
this.labelElement.htmlFor = url;
|
this.labelElement.htmlFor = url;
|
||||||
this.divElement.appendChild(this.inputElement);
|
this.divElement.appendChild(this.inputElement);
|
||||||
this.divElement.appendChild(this.labelElement);
|
this.divElement.appendChild(this.labelElement);
|
||||||
this.setPlayerLabelText();
|
this.labelElement.appendChild(this.serverNameElement);
|
||||||
|
this.labelElement.appendChild(document.createElement('br'));
|
||||||
|
this.labelElement.appendChild(this.completionElement);
|
||||||
|
this.completionElement.className = 'completion';
|
||||||
|
this.setServerInfoLabelText();
|
||||||
|
|
||||||
this.socket = io(url, {
|
this.socket = io(url, {
|
||||||
reconnection: false,
|
reconnection: false,
|
||||||
|
|
@ -121,11 +128,15 @@ class ServerChooserOption {
|
||||||
this.socket.on('connect_error', this.destroy.bind(this));
|
this.socket.on('connect_error', this.destroy.bind(this));
|
||||||
this.socket.on('connect_timeout', this.destroy.bind(this));
|
this.socket.on('connect_timeout', this.destroy.bind(this));
|
||||||
this.socket.on('disconnect', this.destroy.bind(this));
|
this.socket.on('disconnect', this.destroy.bind(this));
|
||||||
this.socket.emit(TransportEvents.SubscribeForPlayerCount);
|
this.socket.emit(TransportEvents.SubscribeForServerInfoUpdates);
|
||||||
this.socket.on(TransportEvents.PlayerCountUpdate, (v: number) => {
|
this.socket.on(
|
||||||
this.content.playerCount = v;
|
TransportEvents.ServerInfoUpdate,
|
||||||
this.setPlayerLabelText();
|
([playerCount, gameState]: [number, number]) => {
|
||||||
});
|
this.content.playerCount = playerCount;
|
||||||
|
this.content.gameStatePercent = gameState;
|
||||||
|
this.setServerInfoLabelText();
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public destroy() {
|
public destroy() {
|
||||||
|
|
@ -134,8 +145,25 @@ class ServerChooserOption {
|
||||||
this.onDestroy(this);
|
this.onDestroy(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
private setPlayerLabelText() {
|
private getRoundCompletionText(percent: number): string {
|
||||||
this.labelElement.innerText = `${this.content.serverName} - ${this.content.playerCount}/${this.content.playerLimit} players`;
|
const texts = [
|
||||||
|
'Just started',
|
||||||
|
'Just started',
|
||||||
|
'Ongoing',
|
||||||
|
'Halfway through',
|
||||||
|
'Nearly over',
|
||||||
|
'About to finish',
|
||||||
|
'Game is over',
|
||||||
|
];
|
||||||
|
|
||||||
|
return texts[Math.floor((percent / 100) * (texts.length - 1))];
|
||||||
|
}
|
||||||
|
|
||||||
|
private setServerInfoLabelText() {
|
||||||
|
this.serverNameElement.innerText = `${this.content.serverName} - ${this.content.playerCount}/${this.content.playerLimit} players`;
|
||||||
|
this.completionElement.innerText = this.getRoundCompletionText(
|
||||||
|
this.content.gameStatePercent,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public get element(): HTMLElement {
|
public get element(): HTMLElement {
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ export class Camera extends GameObject implements ViewObject {
|
||||||
|
|
||||||
public beforeDestroy(): void {}
|
public beforeDestroy(): void {}
|
||||||
|
|
||||||
public step(deltaTimeInMilliseconds: number): void {}
|
public step(deltaTimeInSeconds: number): void {}
|
||||||
|
|
||||||
public draw(renderer: Renderer, overlay: HTMLElement) {
|
public draw(renderer: Renderer, overlay: HTMLElement) {
|
||||||
const canvasAspectRatio = renderer.canvasSize.x / renderer.canvasSize.y;
|
const canvasAspectRatio = renderer.canvasSize.x / renderer.canvasSize.y;
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,7 @@ export class CharacterView extends CharacterBase implements ViewObject {
|
||||||
|
|
||||||
public beforeDestroy(): void {}
|
public beforeDestroy(): void {}
|
||||||
|
|
||||||
public step(deltaTimeInMilliseconds: number): void {}
|
public step(deltaTimeInSeconds: number): void {}
|
||||||
|
|
||||||
public draw(renderer: Renderer, overlay: HTMLElement): void {
|
public draw(renderer: Renderer, overlay: HTMLElement): void {
|
||||||
this.shape.setCircles([this.head!, this.leftFoot!, this.rightFoot!]);
|
this.shape.setCircles([this.head!, this.leftFoot!, this.rightFoot!]);
|
||||||
|
|
|
||||||
|
|
@ -5,9 +5,8 @@ import {
|
||||||
CreateObjectsCommand,
|
CreateObjectsCommand,
|
||||||
CreatePlayerCommand,
|
CreatePlayerCommand,
|
||||||
DeleteObjectsCommand,
|
DeleteObjectsCommand,
|
||||||
GameObject,
|
|
||||||
Id,
|
Id,
|
||||||
UpdateObjectsCommand,
|
RemoteCallsForObjects,
|
||||||
} from 'shared';
|
} from 'shared';
|
||||||
import { Game } from '../game';
|
import { Game } from '../game';
|
||||||
import { Camera } from './camera';
|
import { Camera } from './camera';
|
||||||
|
|
@ -24,8 +23,11 @@ export class GameObjectContainer extends CommandReceiver {
|
||||||
if (this.camera) {
|
if (this.camera) {
|
||||||
this.deleteObject(this.camera.id);
|
this.deleteObject(this.camera.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.player = c.character as PlayerCharacterView;
|
this.player = c.character as PlayerCharacterView;
|
||||||
|
|
||||||
this.camera = new Camera(this.game);
|
this.camera = new Camera(this.game);
|
||||||
|
|
||||||
this.addObject(this.player);
|
this.addObject(this.player);
|
||||||
this.addObject(this.camera);
|
this.addObject(this.camera);
|
||||||
},
|
},
|
||||||
|
|
@ -33,24 +35,25 @@ export class GameObjectContainer extends CommandReceiver {
|
||||||
[CreateObjectsCommand.type]: (c: CreateObjectsCommand) =>
|
[CreateObjectsCommand.type]: (c: CreateObjectsCommand) =>
|
||||||
c.objects.forEach((o) => this.addObject(o as ViewObject)),
|
c.objects.forEach((o) => this.addObject(o as ViewObject)),
|
||||||
|
|
||||||
|
[RemoteCallsForObjects.type]: (c: RemoteCallsForObjects) =>
|
||||||
|
c.callsForObjects.forEach((c) =>
|
||||||
|
this.objects.get(c.id)?.processRemoteCalls(c.calls),
|
||||||
|
),
|
||||||
|
|
||||||
[DeleteObjectsCommand.type]: (c: DeleteObjectsCommand) =>
|
[DeleteObjectsCommand.type]: (c: DeleteObjectsCommand) =>
|
||||||
c.ids.forEach((id: Id) => this.deleteObject(id)),
|
c.ids.forEach((id: Id) => this.deleteObject(id)),
|
||||||
|
|
||||||
[UpdateObjectsCommand.type]: (c: UpdateObjectsCommand) => {
|
|
||||||
c.updates.forEach((u) => this.objects.get(u.id)?.update(u.updates));
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
constructor(private game: Game) {
|
constructor(private game: Game) {
|
||||||
super();
|
super();
|
||||||
}
|
}
|
||||||
|
|
||||||
public stepObjects(delta: number) {
|
public stepObjects(deltaTimeInSeconds: number) {
|
||||||
if (this.player) {
|
if (this.player) {
|
||||||
this.camera.center = this.player.position;
|
this.camera.center = this.player.position;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.objects.forEach((o) => o.step(delta));
|
this.objects.forEach((o) => o.step(deltaTimeInSeconds));
|
||||||
}
|
}
|
||||||
|
|
||||||
public drawObjects(renderer: Renderer, overlay: HTMLElement) {
|
public drawObjects(renderer: Renderer, overlay: HTMLElement) {
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ export class LampView extends LampBase implements ViewObject {
|
||||||
this.light = new CircleLight(center, color, lightness);
|
this.light = new CircleLight(center, color, lightness);
|
||||||
}
|
}
|
||||||
|
|
||||||
public step(deltaTimeInMilliseconds: number): void {}
|
public step(deltaTimeInSeconds: number): void {}
|
||||||
|
|
||||||
public beforeDestroy(): void {}
|
public beforeDestroy(): void {}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,14 @@ import { RenderCommand } from '../commands/types/render';
|
||||||
import { PlanetShape } from '../shapes/planet-shape';
|
import { PlanetShape } from '../shapes/planet-shape';
|
||||||
import { ViewObject } from './view-object';
|
import { ViewObject } from './view-object';
|
||||||
|
|
||||||
|
type FallingPoint = {
|
||||||
|
velocity: vec2;
|
||||||
|
position: vec2;
|
||||||
|
element: HTMLElement;
|
||||||
|
addedToOverlay: boolean;
|
||||||
|
timeToLive: number;
|
||||||
|
};
|
||||||
|
|
||||||
export class PlanetView extends PlanetBase implements ViewObject {
|
export class PlanetView extends PlanetBase implements ViewObject {
|
||||||
private shape: PlanetShape;
|
private shape: PlanetShape;
|
||||||
private ownershipProgess: HTMLElement;
|
private ownershipProgess: HTMLElement;
|
||||||
|
|
@ -22,9 +30,48 @@ export class PlanetView extends PlanetBase implements ViewObject {
|
||||||
this.ownershipProgess.className = 'ownership';
|
this.ownershipProgess.className = 'ownership';
|
||||||
}
|
}
|
||||||
|
|
||||||
public step(deltaTimeInMilliseconds: number): void {
|
public step(deltaTimeInSeconds: number): void {
|
||||||
this.shape.randomOffset += deltaTimeInMilliseconds / 4000;
|
this.shape.randomOffset += deltaTimeInSeconds / 4;
|
||||||
this.shape.colorMixQ = this.ownership;
|
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),
|
||||||
|
);
|
||||||
|
|
||||||
|
if ((p.timeToLive -= deltaTimeInSeconds) <= 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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private generatedPointElements: Array<FallingPoint> = [];
|
||||||
|
|
||||||
|
public generatedPoints(value: number) {
|
||||||
|
const element = document.createElement('div');
|
||||||
|
element.className = 'falling-point ' + (this.ownership < 0.5 ? 'decla' : 'red');
|
||||||
|
element.innerText = '+' + value;
|
||||||
|
this.generatedPointElements.push({
|
||||||
|
element,
|
||||||
|
addedToOverlay: false,
|
||||||
|
timeToLive: Random.getRandomInRange(2, 3),
|
||||||
|
position: vec2.create(),
|
||||||
|
velocity: vec2.fromValues(Random.getRandomInRange(-30, 30), 0),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private getGradient(): string {
|
private getGradient(): string {
|
||||||
|
|
@ -34,18 +81,21 @@ export class PlanetView extends PlanetBase implements ViewObject {
|
||||||
? `conic-gradient(
|
? `conic-gradient(
|
||||||
var(--bright-decla) ${sidePercent}%,
|
var(--bright-decla) ${sidePercent}%,
|
||||||
var(--bright-decla) ${sidePercent}%,
|
var(--bright-decla) ${sidePercent}%,
|
||||||
var(--bright-neutral) ${sidePercent}%,
|
rgba(0, 0, 0, 0) ${sidePercent}%,
|
||||||
var(--bright-neutral) 100%
|
rgba(0, 0, 0, 0) 100%
|
||||||
)`
|
)`
|
||||||
: `conic-gradient(
|
: `conic-gradient(
|
||||||
var(--bright-neutral) 0%,
|
rgba(0, 0, 0, 0) 0%,
|
||||||
var(--bright-neutral) ${100 - sidePercent}%,
|
rgba(0, 0, 0, 0) ${100 - sidePercent}%,
|
||||||
var(--bright-red) ${100 - sidePercent}%,
|
var(--bright-red) ${100 - sidePercent}%,
|
||||||
var(--bright-red) 100%
|
var(--bright-red) 100%
|
||||||
)`;
|
)`;
|
||||||
}
|
}
|
||||||
public beforeDestroy(): void {
|
public beforeDestroy(): void {
|
||||||
this.ownershipProgess.parentElement?.removeChild(this.ownershipProgess);
|
this.ownershipProgess.parentElement?.removeChild(this.ownershipProgess);
|
||||||
|
this.generatedPointElements.forEach((p) =>
|
||||||
|
p.element.parentElement?.removeChild(p.element),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public draw(renderer: Renderer, overlay: HTMLElement): void {
|
public draw(renderer: Renderer, overlay: HTMLElement): void {
|
||||||
|
|
@ -54,6 +104,16 @@ export class PlanetView extends PlanetBase implements ViewObject {
|
||||||
}
|
}
|
||||||
|
|
||||||
const screenPosition = renderer.worldToDisplayCoordinates(this.center);
|
const screenPosition = renderer.worldToDisplayCoordinates(this.center);
|
||||||
|
|
||||||
|
this.generatedPointElements.forEach((p) => {
|
||||||
|
if (!p.addedToOverlay) {
|
||||||
|
overlay.appendChild(p.element);
|
||||||
|
}
|
||||||
|
|
||||||
|
p.element.style.left = screenPosition.x + p.position.x + 'px';
|
||||||
|
p.element.style.top = screenPosition.y + p.position.y + 'px';
|
||||||
|
});
|
||||||
|
|
||||||
this.ownershipProgess.style.left = screenPosition.x + 'px';
|
this.ownershipProgess.style.left = screenPosition.x + 'px';
|
||||||
this.ownershipProgess.style.top = screenPosition.y + 'px';
|
this.ownershipProgess.style.top = screenPosition.y + 'px';
|
||||||
this.ownershipProgess.style.background = this.getGradient();
|
this.ownershipProgess.style.background = this.getGradient();
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import { Circle, Id, PlayerCharacterBase, CharacterTeam, settings } from 'shared
|
||||||
import { OptionsHandler } from '../options-handler';
|
import { OptionsHandler } from '../options-handler';
|
||||||
|
|
||||||
import { BlobShape } from '../shapes/blob-shape';
|
import { BlobShape } from '../shapes/blob-shape';
|
||||||
|
import { SoundHandler, Sounds } from '../sound-handler';
|
||||||
import { ViewObject } from './view-object';
|
import { ViewObject } from './view-object';
|
||||||
|
|
||||||
export class PlayerCharacterView extends PlayerCharacterBase implements ViewObject {
|
export class PlayerCharacterView extends PlayerCharacterBase implements ViewObject {
|
||||||
|
|
@ -39,8 +40,17 @@ export class PlayerCharacterView extends PlayerCharacterBase implements ViewObje
|
||||||
return this.head!.center;
|
return this.head!.center;
|
||||||
}
|
}
|
||||||
|
|
||||||
public step(deltaTimeInMilliseconds: number): void {
|
public setHealth(health: number) {
|
||||||
this.timeSinceLastNameElementUpdate += deltaTimeInMilliseconds;
|
const previousHealth = this.health;
|
||||||
|
super.setHealth(health);
|
||||||
|
SoundHandler.play(
|
||||||
|
Sounds.hit,
|
||||||
|
(0.4 * 2 * (previousHealth - health)) / settings.playerMaxStrength,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public step(deltaTimeInSeconds: number): void {
|
||||||
|
this.timeSinceLastNameElementUpdate += deltaTimeInSeconds;
|
||||||
this.healthElement.style.width = (50 * this.health) / settings.playerMaxHealth + 'px';
|
this.healthElement.style.width = (50 * this.health) / settings.playerMaxHealth + 'px';
|
||||||
this.statsElement.innerText = this.getStatsText();
|
this.statsElement.innerText = this.getStatsText();
|
||||||
if (this.previousHealth > this.health) {
|
if (this.previousHealth > this.health) {
|
||||||
|
|
@ -52,6 +62,10 @@ export class PlayerCharacterView extends PlayerCharacterBase implements ViewObje
|
||||||
this.previousHealth = this.health;
|
this.previousHealth = this.health;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public onShoot(strength: number) {
|
||||||
|
SoundHandler.play(Sounds.shoot, (0.3 * 2 * strength) / settings.playerMaxStrength);
|
||||||
|
}
|
||||||
|
|
||||||
public beforeDestroy(): void {
|
public beforeDestroy(): void {
|
||||||
this.nameElement.parentElement?.removeChild(this.nameElement);
|
this.nameElement.parentElement?.removeChild(this.nameElement);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,9 @@ export class ProjectileView extends ProjectileBase implements ViewObject {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public step(deltaTimeInMilliseconds: number): void {
|
public step(deltaTimeInSeconds: number): void {
|
||||||
|
super.step(deltaTimeInSeconds);
|
||||||
|
|
||||||
this.circle.center = this.center;
|
this.circle.center = this.center;
|
||||||
this.light.center = this.center;
|
this.light.center = this.center;
|
||||||
this.light.intensity = (0.15 * this.strength) / settings.projectileMaxStrength;
|
this.light.intensity = (0.15 * this.strength) / settings.projectileMaxStrength;
|
||||||
|
|
|
||||||
|
|
@ -29,11 +29,7 @@ export abstract class OptionsHandler {
|
||||||
};
|
};
|
||||||
|
|
||||||
if (this._options.musicEnabled) {
|
if (this._options.musicEnabled) {
|
||||||
const firstClickListener = () => {
|
|
||||||
document.removeEventListener('click', firstClickListener);
|
|
||||||
SoundHandler.playAmbient();
|
SoundHandler.playAmbient();
|
||||||
};
|
|
||||||
document.addEventListener('click', firstClickListener);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,37 +8,71 @@ export enum Sounds {
|
||||||
hit = 'hit',
|
hit = 'hit',
|
||||||
shoot = 'shoot',
|
shoot = 'shoot',
|
||||||
click = 'click',
|
click = 'click',
|
||||||
ambient = 'ambient',
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const concurrencyScale = 5;
|
||||||
|
|
||||||
export abstract class SoundHandler {
|
export abstract class SoundHandler {
|
||||||
private static sounds: { [key in Sounds]: HTMLAudioElement };
|
private static sounds: { [key in Sounds]: HTMLAudioElement };
|
||||||
|
private static isAmbientPlaying = false;
|
||||||
|
|
||||||
public static initialize() {
|
private static ambientSound = new Audio(ambientSound);
|
||||||
|
|
||||||
|
private static initialized = false;
|
||||||
|
public static async initialize() {
|
||||||
this.sounds = {
|
this.sounds = {
|
||||||
[Sounds.hit]: new Audio(hitSound),
|
[Sounds.hit]: await this.initializeSound(hitSound),
|
||||||
[Sounds.shoot]: new Audio(shootSound),
|
[Sounds.shoot]: await this.initializeSound(shootSound),
|
||||||
[Sounds.click]: new Audio(clickSound),
|
[Sounds.click]: await this.initializeSound(clickSound),
|
||||||
[Sounds.ambient]: new Audio(ambientSound),
|
|
||||||
};
|
};
|
||||||
this.sounds.ambient.volume = 0.5;
|
|
||||||
|
this.ambientSound.play();
|
||||||
|
this.ambientSound.muted = true;
|
||||||
|
this.initialized = true;
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
this.ambientSound.muted = false;
|
||||||
|
this.ambientSound.volume = 0.5;
|
||||||
|
if (!this.isAmbientPlaying) {
|
||||||
|
this.ambientSound.pause();
|
||||||
|
}
|
||||||
|
}, 100);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static play(sound: Sounds) {
|
private static async initializeSound(hitSound: string): Promise<HTMLAudioElement> {
|
||||||
if (OptionsHandler.options.soundsEnabled) {
|
const sound = new Audio(hitSound);
|
||||||
if (this.sounds[sound].currentTime > 0) {
|
sound.muted = true;
|
||||||
(this.sounds[sound].cloneNode() as HTMLAudioElement).play();
|
await sound.play();
|
||||||
} else {
|
sound.pause();
|
||||||
this.sounds[sound].play();
|
sound.muted = false;
|
||||||
|
sound.currentTime = 0;
|
||||||
|
return sound;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static play(sound: Sounds, volume: number = 1) {
|
||||||
|
if (!this.initialized || !OptionsHandler.options.soundsEnabled) {
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const audio =
|
||||||
|
this.sounds[sound].currentTime > 0
|
||||||
|
? (this.sounds[sound].cloneNode(true) as HTMLAudioElement)
|
||||||
|
: this.sounds[sound];
|
||||||
|
audio.volume = volume;
|
||||||
|
audio.play();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static playAmbient() {
|
public static playAmbient() {
|
||||||
this.sounds.ambient.play();
|
this.isAmbientPlaying = true;
|
||||||
|
if (this.initialized) {
|
||||||
|
this.ambientSound.play();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static stopAmbient() {
|
public static stopAmbient() {
|
||||||
this.sounds.ambient.pause();
|
this.isAmbientPlaying = false;
|
||||||
|
if (this.initialized) {
|
||||||
|
this.ambientSound.pause();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
64
frontend/src/scripts/start-animation.ts
Normal file
64
frontend/src/scripts/start-animation.ts
Normal file
|
|
@ -0,0 +1,64 @@
|
||||||
|
import {
|
||||||
|
CircleLight,
|
||||||
|
ColorfulCircle,
|
||||||
|
FilteringOptions,
|
||||||
|
Flashlight,
|
||||||
|
Renderer,
|
||||||
|
runAnimation,
|
||||||
|
WrapOptions,
|
||||||
|
} from 'sdf-2d';
|
||||||
|
import { settings } from 'shared';
|
||||||
|
import { BlobShape } from './shapes/blob-shape';
|
||||||
|
import { PlanetShape } from './shapes/planet-shape';
|
||||||
|
|
||||||
|
export const startAnimation = async (
|
||||||
|
canvas: HTMLCanvasElement,
|
||||||
|
draw: (r: Renderer, current: number, delta: number) => boolean,
|
||||||
|
noiseTexture: TexImageSource,
|
||||||
|
): Promise<void> =>
|
||||||
|
await runAnimation(
|
||||||
|
canvas,
|
||||||
|
[
|
||||||
|
{
|
||||||
|
...PlanetShape.descriptor,
|
||||||
|
shaderCombinationSteps: [0, 1, 2, 3],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
...BlobShape.descriptor,
|
||||||
|
shaderCombinationSteps: [0, 1, 2, 8],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
...ColorfulCircle.descriptor,
|
||||||
|
shaderCombinationSteps: [0, 2, 16],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
...CircleLight.descriptor,
|
||||||
|
shaderCombinationSteps: [0, 1, 2, 4, 8, 16],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
...Flashlight.descriptor,
|
||||||
|
shaderCombinationSteps: [0],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
draw,
|
||||||
|
{
|
||||||
|
shadowTraceCount: 16,
|
||||||
|
paletteSize: settings.palette.length,
|
||||||
|
//enableStopwatch: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
colorPalette: settings.palette,
|
||||||
|
enableHighDpiRendering: true,
|
||||||
|
lightCutoffDistance: settings.lightCutoffDistance,
|
||||||
|
textures: {
|
||||||
|
noiseTexture: {
|
||||||
|
source: noiseTexture,
|
||||||
|
overrides: {
|
||||||
|
maxFilter: FilteringOptions.LINEAR,
|
||||||
|
wrapS: WrapOptions.MIRRORED_REPEAT,
|
||||||
|
wrapT: WrapOptions.MIRRORED_REPEAT,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
@ -13,7 +13,7 @@ $large-icon: 48px;
|
||||||
$small-icon: 32px;
|
$small-icon: 32px;
|
||||||
$bright-decla: #4069a5;
|
$bright-decla: #4069a5;
|
||||||
$bright-red: #d15652;
|
$bright-red: #d15652;
|
||||||
$bright-neutral: #88888877;
|
$bright-neutral: #ccc;
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
--bright-decla: #{$bright-decla};
|
--bright-decla: #{$bright-decla};
|
||||||
|
|
|
||||||
|
|
@ -96,9 +96,15 @@ form {
|
||||||
border-radius: $border-radius;
|
border-radius: $border-radius;
|
||||||
padding: $small-padding;
|
padding: $small-padding;
|
||||||
border: $border;
|
border: $border;
|
||||||
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
margin: $border-width-focused - $border-width $border-width-focused -
|
margin: $border-width-focused - $border-width $border-width-focused -
|
||||||
$border-width + $small-padding / 2;
|
$border-width + $small-padding / 2;
|
||||||
|
|
||||||
|
.completion {
|
||||||
|
font-size: 0.7em;
|
||||||
|
color: $bright-neutral;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&:focus {
|
&:focus {
|
||||||
|
|
|
||||||
|
|
@ -50,13 +50,18 @@
|
||||||
#settings {
|
#settings {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
padding: 0 $medium-padding $medium-padding $medium-padding;
|
@include background;
|
||||||
|
border-radius: 12px;
|
||||||
|
z-index: 100;
|
||||||
|
margin-right: $medium-padding - $small-padding;
|
||||||
|
padding: $small-padding;
|
||||||
|
|
||||||
transform: translateY(-100%);
|
transform: translateY(-100%);
|
||||||
@media (max-width: $breakpoint) {
|
@media (max-width: $breakpoint) {
|
||||||
flex-direction: row-reverse;
|
flex-direction: row-reverse;
|
||||||
transform: translateX(100%);
|
transform: translateX(100%);
|
||||||
padding: $medium-padding 0 $medium-padding $medium-padding;
|
margin-top: $medium-padding - $small-padding;
|
||||||
|
margin-right: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
|
|
@ -72,13 +77,9 @@
|
||||||
svg {
|
svg {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: opacity $animation-time;
|
transition: opacity $animation-time;
|
||||||
margin-bottom: $small-padding;
|
|
||||||
margin-left: 0;
|
|
||||||
@include square($large-icon);
|
@include square($large-icon);
|
||||||
@media (max-width: $breakpoint) {
|
@media (max-width: $breakpoint) {
|
||||||
@include square($small-icon);
|
@include square($small-icon);
|
||||||
margin-bottom: 0;
|
|
||||||
margin-left: $small-padding;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -130,4 +131,33 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
img,
|
||||||
|
svg {
|
||||||
|
margin-top: $small-padding;
|
||||||
|
@media (max-width: $breakpoint) {
|
||||||
|
margin-top: 0;
|
||||||
|
margin-right: $small-padding;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
label:first-child {
|
||||||
|
img,
|
||||||
|
svg {
|
||||||
|
margin-top: 0;
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
label:not(:first-child) {
|
||||||
|
input[type='checkbox']:after {
|
||||||
|
$height: 4px;
|
||||||
|
top: $height / 2 + $small-padding;
|
||||||
|
|
||||||
|
@media (max-width: $breakpoint) {
|
||||||
|
right: $small-padding;
|
||||||
|
top: $height / 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
7
frontend/static/flag.svg
Normal file
7
frontend/static/flag.svg
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="60" height="60" viewBox="0 0 24 24" stroke-width="1.5" stroke="#ffffff" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
||||||
|
<line x1="5" y1="5" x2="5" y2="21" />
|
||||||
|
<line x1="19" y1="5" x2="19" y2="14" />
|
||||||
|
<path d="M5 5a5 5 0 0 1 7 0a5 5 0 0 0 7 0" />
|
||||||
|
<path d="M5 14a5 5 0 0 1 7 0a5 5 0 0 0 7 0" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 418 B |
BIN
frontend/static/shoot2.mp3
Normal file
BIN
frontend/static/shoot2.mp3
Normal file
Binary file not shown.
BIN
frontend/static/shoot3.mp3
Normal file
BIN
frontend/static/shoot3.mp3
Normal file
Binary file not shown.
BIN
frontend/static/shoot4.mp3
Normal file
BIN
frontend/static/shoot4.mp3
Normal file
Binary file not shown.
BIN
frontend/static/shoot5.mp3
Normal file
BIN
frontend/static/shoot5.mp3
Normal file
Binary file not shown.
BIN
frontend/static/shoot6.mp3
Normal file
BIN
frontend/static/shoot6.mp3
Normal file
Binary file not shown.
18
shared/src/commands/types/game-end.ts
Normal file
18
shared/src/commands/types/game-end.ts
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
import { CharacterTeam } from '../../objects/types/character-team';
|
||||||
|
import { serializable } from '../../transport/serialization/serializable';
|
||||||
|
import { Command } from '../command';
|
||||||
|
|
||||||
|
@serializable
|
||||||
|
export class GameEnd extends Command {
|
||||||
|
constructor(
|
||||||
|
public readonly winningTeam: CharacterTeam,
|
||||||
|
public readonly endCardLengthInSeconds: number,
|
||||||
|
public readonly shouldReconnect: boolean,
|
||||||
|
) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
public toArray(): Array<any> {
|
||||||
|
return [this.winningTeam, this.endCardLengthInSeconds, this.shouldReconnect];
|
||||||
|
}
|
||||||
|
}
|
||||||
13
shared/src/commands/types/game-start.ts
Normal file
13
shared/src/commands/types/game-start.ts
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
import { serializable } from '../../transport/serialization/serializable';
|
||||||
|
import { Command } from '../command';
|
||||||
|
|
||||||
|
@serializable
|
||||||
|
export class GameStart extends Command {
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
public toArray(): Array<any> {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
14
shared/src/commands/types/remote-calls-for-object.ts
Normal file
14
shared/src/commands/types/remote-calls-for-object.ts
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
import { serializable } from '../../transport/serialization/serializable';
|
||||||
|
import { Command } from '../command';
|
||||||
|
import { RemoteCallsForObject } from './remote-calls-for-objects';
|
||||||
|
|
||||||
|
@serializable
|
||||||
|
export class RemoteCallsForObjects extends Command {
|
||||||
|
constructor(public readonly callsForObjects: Array<RemoteCallsForObject>) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
public toArray(): Array<any> {
|
||||||
|
return [this.callsForObjects];
|
||||||
|
}
|
||||||
|
}
|
||||||
15
shared/src/commands/types/remote-calls-for-objects.ts
Normal file
15
shared/src/commands/types/remote-calls-for-objects.ts
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
import { Id } from '../../main';
|
||||||
|
import { RemoteCall } from '../../objects/game-object';
|
||||||
|
import { serializable } from '../../transport/serialization/serializable';
|
||||||
|
import { Command } from '../command';
|
||||||
|
|
||||||
|
@serializable
|
||||||
|
export class RemoteCallsForObject extends Command {
|
||||||
|
constructor(public readonly id: Id, public readonly calls: Array<RemoteCall>) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
public toArray(): Array<any> {
|
||||||
|
return [this.id, this.calls];
|
||||||
|
}
|
||||||
|
}
|
||||||
13
shared/src/commands/types/server-announcement.ts
Normal file
13
shared/src/commands/types/server-announcement.ts
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
import { serializable } from '../../transport/serialization/serializable';
|
||||||
|
import { Command } from '../command';
|
||||||
|
|
||||||
|
@serializable
|
||||||
|
export class ServerAnnouncement extends Command {
|
||||||
|
constructor(public readonly text: string) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
public toArray(): Array<any> {
|
||||||
|
return [this.text];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,37 +1,17 @@
|
||||||
import { vec2 } from 'gl-matrix';
|
|
||||||
import { CharacterTeam } from '../../objects/types/character-team';
|
|
||||||
import { serializable } from '../../transport/serialization/serializable';
|
import { serializable } from '../../transport/serialization/serializable';
|
||||||
import { Command } from '../command';
|
import { Command } from '../command';
|
||||||
|
|
||||||
@serializable
|
|
||||||
export class OtherPlayerDirection {
|
|
||||||
public constructor(
|
|
||||||
public readonly direction: vec2,
|
|
||||||
public readonly team: CharacterTeam,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
public toArray(): Array<any> {
|
|
||||||
return [this.direction, this.team];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@serializable
|
@serializable
|
||||||
export class UpdateGameState extends Command {
|
export class UpdateGameState extends Command {
|
||||||
public constructor(
|
public constructor(
|
||||||
public readonly declaCount: number,
|
public readonly declaCount: number,
|
||||||
public readonly redCount: number,
|
public readonly redCount: number,
|
||||||
public readonly neutralCount: number,
|
public readonly limit: number,
|
||||||
public readonly otherPlayerDirections: Array<OtherPlayerDirection>,
|
|
||||||
) {
|
) {
|
||||||
super();
|
super();
|
||||||
}
|
}
|
||||||
|
|
||||||
public toArray(): Array<any> {
|
public toArray(): Array<any> {
|
||||||
return [
|
return [this.declaCount, this.redCount, this.limit];
|
||||||
this.declaCount,
|
|
||||||
this.redCount,
|
|
||||||
this.neutralCount,
|
|
||||||
this.otherPlayerDirections,
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,15 +0,0 @@
|
||||||
import { vec2 } from 'gl-matrix';
|
|
||||||
import { UpdateObjectMessage } from '../../objects/update-object-message';
|
|
||||||
import { serializable } from '../../transport/serialization/serializable';
|
|
||||||
import { Command } from '../command';
|
|
||||||
|
|
||||||
@serializable
|
|
||||||
export class UpdateObjectsCommand extends Command {
|
|
||||||
public constructor(public readonly updates: Array<UpdateObjectMessage>) {
|
|
||||||
super();
|
|
||||||
}
|
|
||||||
|
|
||||||
public toArray(): Array<any> {
|
|
||||||
return [this.updates];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
27
shared/src/commands/types/update-other-player-directions.ts
Normal file
27
shared/src/commands/types/update-other-player-directions.ts
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
import { vec2 } from 'gl-matrix';
|
||||||
|
import { CharacterTeam } from '../../objects/types/character-team';
|
||||||
|
import { serializable } from '../../transport/serialization/serializable';
|
||||||
|
import { Command } from '../command';
|
||||||
|
|
||||||
|
@serializable
|
||||||
|
export class OtherPlayerDirection {
|
||||||
|
public constructor(
|
||||||
|
public readonly direction: vec2,
|
||||||
|
public readonly team: CharacterTeam,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public toArray(): Array<any> {
|
||||||
|
return [this.direction, this.team];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@serializable
|
||||||
|
export class UpdateOtherPlayerDirections extends Command {
|
||||||
|
public constructor(public readonly otherPlayerDirections: Array<OtherPlayerDirection>) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
public toArray(): Array<any> {
|
||||||
|
return [this.otherPlayerDirections];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -2,6 +2,7 @@ export interface ServerInformation {
|
||||||
playerLimit: number;
|
playerLimit: number;
|
||||||
playerCount: number;
|
playerCount: number;
|
||||||
serverName: string;
|
serverName: string;
|
||||||
|
gameStatePercent: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const serverInformationEndpoint = '/stats';
|
export const serverInformationEndpoint = '/state';
|
||||||
|
|
|
||||||
|
|
@ -5,12 +5,17 @@ export * from './commands/types/delete-objects';
|
||||||
export * from './commands/types/ternary-action';
|
export * from './commands/types/ternary-action';
|
||||||
export * from './commands/types/move-action';
|
export * from './commands/types/move-action';
|
||||||
export * from './commands/types/primary-action';
|
export * from './commands/types/primary-action';
|
||||||
|
export * from './commands/types/remote-calls-for-objects';
|
||||||
|
export * from './commands/types/remote-calls-for-object';
|
||||||
export * from './commands/types/player-died';
|
export * from './commands/types/player-died';
|
||||||
export * from './commands/types/update-objects';
|
export * from './commands/types/server-announcement';
|
||||||
export * from './commands/types/update-game-state';
|
export * from './commands/types/game-end';
|
||||||
|
export * from './commands/types/game-start';
|
||||||
|
export * from './commands/types/update-other-player-directions';
|
||||||
export * from './commands/types/secondary-action';
|
export * from './commands/types/secondary-action';
|
||||||
export * from './commands/command-receiver';
|
|
||||||
export * from './commands/types/update-game-state';
|
export * from './commands/types/update-game-state';
|
||||||
|
export * from './commands/command-receiver';
|
||||||
|
export * from './commands/types/update-other-player-directions';
|
||||||
export * from './commands/command-executors';
|
export * from './commands/command-executors';
|
||||||
export * from './commands/command-generator';
|
export * from './commands/command-generator';
|
||||||
export * from './commands/broadcast-commands';
|
export * from './commands/broadcast-commands';
|
||||||
|
|
@ -46,7 +51,6 @@ export * from './objects/types/player-character-base';
|
||||||
export * from './objects/types/lamp-base';
|
export * from './objects/types/lamp-base';
|
||||||
export * from './objects/types/planet-base';
|
export * from './objects/types/planet-base';
|
||||||
export * from './objects/types/projectile-base';
|
export * from './objects/types/projectile-base';
|
||||||
export * from './objects/update-object-message';
|
|
||||||
export * from './settings';
|
export * from './settings';
|
||||||
export * from './transport/transport-events';
|
export * from './transport/transport-events';
|
||||||
export * from './transport/identity';
|
export * from './transport/identity';
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,37 @@
|
||||||
import { UpdateMessage, UpdateObjectMessage } from '../main';
|
|
||||||
import { Id } from '../transport/identity';
|
import { Id } from '../transport/identity';
|
||||||
|
import { serializable } from '../transport/serialization/serializable';
|
||||||
|
|
||||||
export abstract class GameObject {
|
@serializable
|
||||||
constructor(public readonly id: Id) {}
|
export class RemoteCall {
|
||||||
|
constructor(public readonly functionName: string, public readonly args: Array<any>) {}
|
||||||
|
|
||||||
public calculateUpdates(): UpdateObjectMessage | undefined {
|
public toArray(): Array<any> {
|
||||||
return;
|
return [this.functionName, this.args];
|
||||||
}
|
}
|
||||||
|
}
|
||||||
update(updates: Array<UpdateMessage>): void {
|
|
||||||
updates.forEach((u) => ((this as any)[u.key] = u.value));
|
export abstract class GameObject {
|
||||||
|
private remoteCalls: Array<RemoteCall> = [];
|
||||||
|
|
||||||
|
constructor(public readonly id: Id) {}
|
||||||
|
|
||||||
|
public processRemoteCalls(remoteCalls: Array<RemoteCall>) {
|
||||||
|
remoteCalls.forEach((r) =>
|
||||||
|
((this[r.functionName as keyof this] as unknown) as (
|
||||||
|
...args: Array<any>
|
||||||
|
) => unknown)(...r.args),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public getRemoteCalls(): Array<RemoteCall> {
|
||||||
|
return this.remoteCalls;
|
||||||
|
}
|
||||||
|
|
||||||
|
public resetRemoteCalls() {
|
||||||
|
this.remoteCalls = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
protected remoteCall(name: string & keyof this, ...args: Array<any>) {
|
||||||
|
this.remoteCalls.push(new RemoteCall(name, args));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,12 @@ export class PlanetBase extends GameObject {
|
||||||
vec2.scale(this.center, this.center, 1 / vertices.length);
|
vec2.scale(this.center, this.center, 1 / vertices.length);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public setOwnership(value: number) {
|
||||||
|
this.ownership = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public generatedPoints(value: number) {}
|
||||||
|
|
||||||
public static createPlanetVertices(
|
public static createPlanetVertices(
|
||||||
center: vec2,
|
center: vec2,
|
||||||
width: number,
|
width: number,
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,25 @@ export class PlayerCharacterBase extends CharacterBase {
|
||||||
super(id, team, health, head, leftFoot, rightFoot);
|
super(id, team, health, head, leftFoot, rightFoot);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public onShoot(strength: number) {}
|
||||||
|
|
||||||
|
public updateCircles(head: Circle, leftFoot: Circle, rightFoot: Circle) {
|
||||||
|
this.head!.center = head.center;
|
||||||
|
this.head!.radius = head.radius;
|
||||||
|
this.leftFoot!.center = leftFoot.center;
|
||||||
|
this.leftFoot!.radius = leftFoot.radius;
|
||||||
|
this.rightFoot!.center = rightFoot.center;
|
||||||
|
this.rightFoot!.radius = rightFoot.radius;
|
||||||
|
}
|
||||||
|
|
||||||
|
public setHealth(health: number) {
|
||||||
|
this.health = health;
|
||||||
|
}
|
||||||
|
|
||||||
|
public setKillCount(killCount: number) {
|
||||||
|
this.killCount = killCount;
|
||||||
|
}
|
||||||
|
|
||||||
public toArray(): Array<any> {
|
public toArray(): Array<any> {
|
||||||
return [
|
return [
|
||||||
this.id,
|
this.id,
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import { vec2 } from 'gl-matrix';
|
import { vec2 } from 'gl-matrix';
|
||||||
|
import { settings } from '../../settings';
|
||||||
import { Id } from '../../transport/identity';
|
import { Id } from '../../transport/identity';
|
||||||
import { serializable } from '../../transport/serialization/serializable';
|
import { serializable } from '../../transport/serialization/serializable';
|
||||||
import { GameObject } from '../game-object';
|
import { GameObject } from '../game-object';
|
||||||
|
|
@ -16,6 +17,15 @@ export class ProjectileBase extends GameObject {
|
||||||
super(id);
|
super(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public setCenter(center: vec2) {
|
||||||
|
this.center = center;
|
||||||
|
}
|
||||||
|
|
||||||
|
public step(deltaTimeInSeconds: number) {
|
||||||
|
this.strength -= settings.projectileFadeSpeed * deltaTimeInSeconds;
|
||||||
|
this.strength = Math.max(0, this.strength);
|
||||||
|
}
|
||||||
|
|
||||||
public toArray(): Array<any> {
|
public toArray(): Array<any> {
|
||||||
return [this.id, this.center, this.radius, this.team, this.strength];
|
return [this.id, this.center, this.radius, this.team, this.strength];
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,12 +0,0 @@
|
||||||
import { Id } from '../main';
|
|
||||||
import { serializable } from '../transport/serialization/serializable';
|
|
||||||
import { UpdateMessage } from './update-message';
|
|
||||||
|
|
||||||
@serializable
|
|
||||||
export class UpdateObjectMessage {
|
|
||||||
constructor(public id: Id, public updates: Array<UpdateMessage>) {}
|
|
||||||
|
|
||||||
public toArray(): Array<any> {
|
|
||||||
return [this.id, this.updates];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -21,6 +21,9 @@ export const settings = {
|
||||||
objectsOnCircleLength: 0.002,
|
objectsOnCircleLength: 0.002,
|
||||||
planetEdgeCount: 7,
|
planetEdgeCount: 7,
|
||||||
takeControlTimeInSeconds: 4,
|
takeControlTimeInSeconds: 4,
|
||||||
|
loseControlTimeInSeconds: 24,
|
||||||
|
planetPointGenerationInterval: 1.5,
|
||||||
|
planetPointGenerationValue: 1,
|
||||||
maxGravityDistance: 700,
|
maxGravityDistance: 700,
|
||||||
maxGravityQ: 180,
|
maxGravityQ: 180,
|
||||||
planetControlThreshold: 0.2,
|
planetControlThreshold: 0.2,
|
||||||
|
|
@ -28,6 +31,7 @@ export const settings = {
|
||||||
maxGravityStrength: 10000,
|
maxGravityStrength: 10000,
|
||||||
maxAcceleration: 10000,
|
maxAcceleration: 10000,
|
||||||
playerMaxStrength: 80,
|
playerMaxStrength: 80,
|
||||||
|
endGameDeltaScaling: 4,
|
||||||
playerDiedTimeout: 5,
|
playerDiedTimeout: 5,
|
||||||
playerStrengthRegenerationPerSeconds: 40,
|
playerStrengthRegenerationPerSeconds: 40,
|
||||||
projectileMaxStrength: 40,
|
projectileMaxStrength: 40,
|
||||||
|
|
@ -37,7 +41,7 @@ export const settings = {
|
||||||
projectileFadeSpeed: 20,
|
projectileFadeSpeed: 20,
|
||||||
projectileCreationInterval: 0.1,
|
projectileCreationInterval: 0.1,
|
||||||
playerColorIndexOffset: 3,
|
playerColorIndexOffset: 3,
|
||||||
backgroundGradient: [rgb255(90, 38, 43), rgb255(0, 0, 0), rgb255(43, 39, 73)],
|
backgroundGradient: [rgb255(90, 38, 43), rgb255(43, 39, 73)],
|
||||||
declaColor,
|
declaColor,
|
||||||
declaPlanetColor,
|
declaPlanetColor,
|
||||||
redColor,
|
redColor,
|
||||||
|
|
|
||||||
|
|
@ -2,8 +2,8 @@ export enum TransportEvents {
|
||||||
PlayerJoining = 'PlayerJoining',
|
PlayerJoining = 'PlayerJoining',
|
||||||
PlayerToServer = 'PlayerToServer',
|
PlayerToServer = 'PlayerToServer',
|
||||||
ServerToPlayer = 'ServerToPlayer',
|
ServerToPlayer = 'ServerToPlayer',
|
||||||
SubscribeForPlayerCount = 'SubscribeForPlayerCount',
|
SubscribeForServerInfoUpdates = 'SubscribeForServerInfoUpdates',
|
||||||
PlayerCountUpdate = 'PlayerCountUpdate',
|
ServerInfoUpdate = 'ServerInfoUpdate',
|
||||||
Ping = 'Ping',
|
Ping = 'Ping',
|
||||||
Pong = 'Pong',
|
Pong = 'Pong',
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue