Change gameplay
This commit is contained in:
parent
d79900e3ea
commit
34dae300da
56 changed files with 901 additions and 395 deletions
|
|
@ -4,6 +4,7 @@ export const defaultOptions: Options = {
|
|||
port: 3000,
|
||||
name: 'Localhost',
|
||||
playerLimit: 16,
|
||||
seed: 51,
|
||||
seed: Math.random(),
|
||||
scoreLimit: 500,
|
||||
worldSize: 10000,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -6,6 +6,12 @@ import {
|
|||
settings,
|
||||
ServerInformation,
|
||||
PlayerInformation,
|
||||
serializable,
|
||||
UpdateGameState,
|
||||
serialize,
|
||||
CharacterTeam,
|
||||
GameEnd,
|
||||
GameStart,
|
||||
} from 'shared';
|
||||
import { createWorld } from './map/create-world';
|
||||
import { DeltaTimeCalculator } from './helper/delta-time-calculator';
|
||||
|
|
@ -15,22 +21,40 @@ import { PlayerContainer } from './players/player-container';
|
|||
const playerCountSubscribedRoom = 'playerCountUpdates';
|
||||
|
||||
export class GameServer {
|
||||
private objects = new PhysicalContainer();
|
||||
private players: PlayerContainer;
|
||||
private deltaTimes: Array<number> = [];
|
||||
private deltaTimeCalculator = new DeltaTimeCalculator();
|
||||
private objects!: PhysicalContainer;
|
||||
private players!: PlayerContainer;
|
||||
private deltaTimes!: Array<number>;
|
||||
private deltaTimeCalculator!: DeltaTimeCalculator;
|
||||
|
||||
private declaPoints = 0;
|
||||
private redPoints = 0;
|
||||
|
||||
private isInEndGame = false;
|
||||
private timeScaling = 1;
|
||||
|
||||
private serverName: string;
|
||||
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.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.playerLimit = options.playerLimit;
|
||||
|
||||
createWorld(this.objects, options.worldSize);
|
||||
this.objects.initialize();
|
||||
this.initialize();
|
||||
|
||||
io.on('connection', (socket: SocketIO.Socket) => {
|
||||
socket.on(TransportEvents.PlayerJoining, (playerInfo: PlayerInformation) => {
|
||||
|
|
@ -40,33 +64,61 @@ export class GameServer {
|
|||
player.sendCommand(command);
|
||||
});
|
||||
|
||||
this.sendPlayerCountUpdate();
|
||||
this.sendServerStateUpdate();
|
||||
|
||||
socket.on('disconnect', () => {
|
||||
player.destroy();
|
||||
this.players.deletePlayer(player);
|
||||
this.sendPlayerCountUpdate();
|
||||
this.sendServerStateUpdate();
|
||||
});
|
||||
});
|
||||
|
||||
socket.on(TransportEvents.SubscribeForPlayerCount, () => {
|
||||
socket.on(TransportEvents.SubscribeForServerInfoUpdates, () => {
|
||||
socket.join(playerCountSubscribedRoom);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public sendPlayerCountUpdate() {
|
||||
private timeSinceLastServerStateUpdate = 0;
|
||||
public sendServerStateUpdate() {
|
||||
this.io
|
||||
.to(playerCountSubscribedRoom)
|
||||
.emit(TransportEvents.PlayerCountUpdate, this.players.count);
|
||||
.emit(TransportEvents.ServerInfoUpdate, [this.players.count, this.gameProgress]);
|
||||
}
|
||||
|
||||
public start() {
|
||||
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() {
|
||||
const delta = this.deltaTimeCalculator.getNextDeltaTimeInSeconds();
|
||||
let delta = this.deltaTimeCalculator.getNextDeltaTimeInSeconds();
|
||||
const framesBetweenDeltaTimeCalculation = 1000;
|
||||
|
||||
if (this.deltaTimes.length > framesBetweenDeltaTimeCalculation) {
|
||||
|
|
@ -82,8 +134,25 @@ export class GameServer {
|
|||
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.updatePoints();
|
||||
this.players.sendOnSocket(
|
||||
serialize(
|
||||
new UpdateGameState(this.declaPoints, this.redPoints, this.options.scoreLimit),
|
||||
),
|
||||
);
|
||||
this.players.step(delta);
|
||||
this.objects.resetRemoteCalls();
|
||||
|
||||
const physicsDelta = this.deltaTimeCalculator.getDeltaTimeInSeconds() * 1000;
|
||||
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 {
|
||||
return {
|
||||
serverName: this.serverName,
|
||||
playerCount: this.players.count,
|
||||
playerLimit: this.playerLimit,
|
||||
gameStatePercent: this.gameProgress,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,11 +4,8 @@ import { Server } from 'http';
|
|||
import cors from 'cors';
|
||||
import { applyArrayPlugins, Random, serverInformationEndpoint } from 'shared';
|
||||
import minimist from 'minimist';
|
||||
|
||||
import { glMatrix } from 'gl-matrix';
|
||||
|
||||
import { GameServer } from './game-server';
|
||||
|
||||
import { defaultOptions } from './default-options';
|
||||
|
||||
glMatrix.setMatrixArrayType(Array);
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
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 { BoundingBox } from '../physics/bounding-boxes/bounding-box';
|
||||
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();
|
||||
}
|
||||
|
||||
public step(deltaTime: number): void {}
|
||||
|
||||
public distance(target: vec2): number {
|
||||
return vec2.distance(this.center, target);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,15 +10,16 @@ import {
|
|||
settings,
|
||||
PlanetBase,
|
||||
CharacterTeam,
|
||||
UpdateObjectMessage,
|
||||
} from 'shared';
|
||||
|
||||
import { ImmutableBoundingBox } from '../physics/bounding-boxes/immutable-bounding-box';
|
||||
import { StaticPhysical } from '../physics/physicals/static-physical';
|
||||
import { UpdateGameObjectMessage } from '../update-game-object-message';
|
||||
import { GeneratesPoints } from './generates-points';
|
||||
|
||||
@serializesTo(PlanetBase)
|
||||
export class PlanetPhysical extends PlanetBase implements StaticPhysical {
|
||||
export class PlanetPhysical
|
||||
extends PlanetBase
|
||||
implements StaticPhysical, GeneratesPoints {
|
||||
public readonly canCollide = true;
|
||||
public readonly canMove = false;
|
||||
|
||||
|
|
@ -67,41 +68,62 @@ export class PlanetPhysical extends PlanetBase implements StaticPhysical {
|
|||
return sign * d;
|
||||
}
|
||||
|
||||
public calculateUpdates(): UpdateObjectMessage {
|
||||
return new UpdateGameObjectMessage(this, ['ownership']);
|
||||
public get team(): CharacterTeam {
|
||||
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) {
|
||||
const previousOwnership = this.ownership;
|
||||
if (team === CharacterTeam.decla) {
|
||||
this.ownership -= (0.5 / settings.takeControlTimeInSeconds) * deltaTime;
|
||||
if (
|
||||
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 {
|
||||
} else if (team === CharacterTeam.red) {
|
||||
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 (
|
||||
previousOwnership < 0.5 + settings.planetControlThreshold &&
|
||||
this.ownership >= 0.5 + settings.planetControlThreshold
|
||||
(previous < 0.5 && this.ownership > 0.5) ||
|
||||
(previous > 0.5 && this.ownership < 0.5)
|
||||
) {
|
||||
PlanetPhysical.redPlanetCount++;
|
||||
PlanetPhysical.neutralPlanetCount--;
|
||||
} else if (
|
||||
previousOwnership <= 0.5 - settings.planetControlThreshold &&
|
||||
previousOwnership > 0.5 + settings.planetControlThreshold
|
||||
) {
|
||||
PlanetPhysical.declaPlanetCount--;
|
||||
PlanetPhysical.neutralPlanetCount++;
|
||||
this.ownership = 0.5;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -21,8 +21,6 @@ import { forceAtPosition } from '../physics/functions/force-at-position';
|
|||
import { getBoundingBoxOfCircle } from '../physics/functions/get-bounding-box-of-circle';
|
||||
import { PlanetPhysical } from './planet-physical';
|
||||
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)
|
||||
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) {
|
||||
this.movementActions.push(c);
|
||||
}
|
||||
|
||||
private addKill() {
|
||||
this.killCount++;
|
||||
this.remoteCall('setKillCount', this.killCount);
|
||||
}
|
||||
|
||||
public onCollision(other: GameObject) {
|
||||
if (
|
||||
other instanceof ProjectilePhysical &&
|
||||
|
|
@ -155,9 +148,10 @@ export class PlayerCharacterPhysical
|
|||
) {
|
||||
other.destroy();
|
||||
this.health -= other.strength;
|
||||
this.remoteCall('setHealth', this.health);
|
||||
if (this.health <= 0) {
|
||||
this.destroy();
|
||||
other.originator.killCount++;
|
||||
other.originator.addKill();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -182,6 +176,8 @@ export class PlayerCharacterPhysical
|
|||
this.container,
|
||||
);
|
||||
this.container.addObject(projectile);
|
||||
|
||||
this.remoteCall('onShoot', strength);
|
||||
}
|
||||
|
||||
public get boundingBox(): BoundingBoxBase {
|
||||
|
|
@ -299,6 +295,7 @@ export class PlayerCharacterPhysical
|
|||
this.stepBodyPart(this.leftFoot, deltaTime);
|
||||
this.stepBodyPart(this.rightFoot, deltaTime);
|
||||
this.keepPosture();
|
||||
this.remoteCall('updateCircles', this.head, this.leftFoot, this.rightFoot);
|
||||
}
|
||||
|
||||
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 { PlanetPhysical } from './planet-physical';
|
||||
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 { moveCircle } from '../physics/functions/move-circle';
|
||||
|
||||
|
|
@ -68,10 +66,6 @@ export class ProjectilePhysical
|
|||
vec2.add(this.center, this.center, delta);
|
||||
}
|
||||
|
||||
public calculateUpdates(): UpdateObjectMessage {
|
||||
return new UpdateGameObjectMessage(this, ['center', 'strength']);
|
||||
}
|
||||
|
||||
public get boundingBox(): ImmutableBoundingBox {
|
||||
if (!this._boundingBox) {
|
||||
this._boundingBox = (this.object as CirclePhysical).boundingBox;
|
||||
|
|
@ -105,7 +99,9 @@ export class ProjectilePhysical
|
|||
}
|
||||
|
||||
public step(deltaTime: number) {
|
||||
if ((this.strength -= settings.projectileFadeSpeed * deltaTime) < 0) {
|
||||
super.step(deltaTime);
|
||||
|
||||
if (this.strength <= 0) {
|
||||
this.destroy();
|
||||
return;
|
||||
}
|
||||
|
|
@ -122,5 +118,7 @@ export class ProjectilePhysical
|
|||
|
||||
this.object.velocity = this.velocity;
|
||||
this.object.step2(deltaTime);
|
||||
|
||||
this.remoteCall('setCenter', this.center);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ export interface Options {
|
|||
port: number;
|
||||
name: string;
|
||||
playerLimit: number;
|
||||
scoreLimit: number;
|
||||
seed: number;
|
||||
worldSize: number;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,12 +6,14 @@ import { BoundingBoxTree } from './bounding-box-tree';
|
|||
import { Physical } from '../physicals/physical';
|
||||
import { StaticPhysical } from '../physicals/static-physical';
|
||||
import { DynamicPhysical } from '../physicals/dynamic-physical';
|
||||
import { GeneratesPoints, generatesPoints } from '../../objects/generates-points';
|
||||
|
||||
export class PhysicalContainer {
|
||||
private isTreeInitialized = false;
|
||||
private staticBoundingBoxesWaitList: Array<StaticPhysical> = [];
|
||||
private staticBoundingBoxes = new BoundingBoxTree();
|
||||
private dynamicBoundingBoxes = new BoundingBoxList();
|
||||
private objects: Array<Physical> = [];
|
||||
|
||||
public initialize() {
|
||||
this.staticBoundingBoxes.build(this.staticBoundingBoxesWaitList);
|
||||
|
|
@ -19,6 +21,8 @@ export class PhysicalContainer {
|
|||
}
|
||||
|
||||
public addObject(physical: Physical) {
|
||||
this.objects.push(physical);
|
||||
|
||||
if (physical.canMove) {
|
||||
this.dynamicBoundingBoxes.insert(physical);
|
||||
} else {
|
||||
|
|
@ -31,11 +35,31 @@ export class PhysicalContainer {
|
|||
}
|
||||
|
||||
public removeObject(object: DynamicPhysical) {
|
||||
this.objects = this.objects.filter((p) => p !== object);
|
||||
this.dynamicBoundingBoxes.remove(object);
|
||||
}
|
||||
|
||||
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> {
|
||||
|
|
|
|||
|
|
@ -2,5 +2,4 @@ import { PhysicalBase } from './physical-base';
|
|||
|
||||
export interface DynamicPhysical extends PhysicalBase {
|
||||
readonly canMove: true;
|
||||
step(deltaTimeInMilliseconds: number): void;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,5 +9,5 @@ export interface PhysicalBase {
|
|||
readonly gameObject: GameObject;
|
||||
|
||||
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 { Player } from './player';
|
||||
|
||||
|
|
@ -32,6 +32,10 @@ export class PlayerContainer {
|
|||
this.players.forEach((p) => p.step(deltaTimeInSeconds));
|
||||
}
|
||||
|
||||
public sendOnSocket(message: any) {
|
||||
this.players.forEach((p) => p.socket.emit(TransportEvents.ServerToPlayer, message));
|
||||
}
|
||||
|
||||
private getTeamOfNextPlayer(): CharacterTeam {
|
||||
const declaCount = this._players.filter((p) => p.team === CharacterTeam.decla).length;
|
||||
const redCount = this._players.filter((p) => p.team === CharacterTeam.red).length;
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@ import {
|
|||
MoveActionCommand,
|
||||
serialize,
|
||||
TransportEvents,
|
||||
UpdateObjectsCommand,
|
||||
SetAspectRatioActionCommand,
|
||||
calculateViewArea,
|
||||
SecondaryActionCommand,
|
||||
|
|
@ -17,11 +16,13 @@ import {
|
|||
Circle,
|
||||
PlayerInformation,
|
||||
CharacterTeam,
|
||||
UpdateGameState,
|
||||
UpdateOtherPlayerDirections,
|
||||
GameObject,
|
||||
Command,
|
||||
UpdateObjectMessage,
|
||||
OtherPlayerDirection,
|
||||
RemoteCallsForObject,
|
||||
RemoteCallsForObjects,
|
||||
ServerAnnouncement,
|
||||
} from 'shared';
|
||||
import { getTimeInMilliseconds } from '../helper/get-time-in-milliseconds';
|
||||
import { BoundingBox } from '../physics/bounding-boxes/bounding-box';
|
||||
|
|
@ -70,7 +71,7 @@ export class Player extends CommandReceiver {
|
|||
private readonly playerInfo: PlayerInformation,
|
||||
private readonly playerContainer: PlayerContainer,
|
||||
private readonly objectContainer: PhysicalContainer,
|
||||
private readonly socket: SocketIO.Socket,
|
||||
public readonly socket: SocketIO.Socket,
|
||||
public readonly team: CharacterTeam,
|
||||
) {
|
||||
super();
|
||||
|
|
@ -123,8 +124,15 @@ export class Player extends CommandReceiver {
|
|||
this.character = null;
|
||||
this.timeUntilRespawn = settings.playerDiedTimeout;
|
||||
}
|
||||
} else if ((this.timeUntilRespawn -= deltaTimeInSeconds) < 0) {
|
||||
this.createCharacter();
|
||||
} else {
|
||||
this.sendToPlayer(
|
||||
new ServerAnnouncement(`Reviving in ${Math.round(this.timeUntilRespawn)}…`),
|
||||
);
|
||||
if ((this.timeUntilRespawn -= deltaTimeInSeconds) < 0) {
|
||||
this.createCharacter();
|
||||
this.center = this.character!.center;
|
||||
this.sendToPlayer(new ServerAnnouncement(''));
|
||||
}
|
||||
}
|
||||
|
||||
const viewArea = calculateViewArea(this.center, this.aspectRatio, 1.5);
|
||||
|
|
@ -155,21 +163,14 @@ export class Player extends CommandReceiver {
|
|||
}
|
||||
|
||||
this.sendToPlayer(
|
||||
new UpdateObjectsCommand(
|
||||
this.objectsPreviouslyInViewArea
|
||||
.map((g) => g.calculateUpdates())
|
||||
.filter((u) => u) as Array<UpdateObjectMessage>,
|
||||
new RemoteCallsForObjects(
|
||||
this.objectsPreviouslyInViewArea.map(
|
||||
(g) => new RemoteCallsForObject(g.id, g.getRemoteCalls()),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
this.sendToPlayer(
|
||||
new UpdateGameState(
|
||||
PlanetPhysical.declaPlanetCount,
|
||||
PlanetPhysical.redPlanetCount,
|
||||
PlanetPhysical.neutralPlanetCount,
|
||||
this.getOtherPlayers(),
|
||||
),
|
||||
);
|
||||
this.sendToPlayer(new UpdateOtherPlayerDirections(this.getOtherPlayers()));
|
||||
}
|
||||
|
||||
private findEmptyPositionForPlayer(): vec2 {
|
||||
|
|
@ -223,7 +224,7 @@ export class Player extends CommandReceiver {
|
|||
.filter((g) => g instanceof PlayerCharacterPhysical);
|
||||
|
||||
const otherPlayers = this.playerContainer.players.filter(
|
||||
(p) => playersInViewArea.indexOf(p.character!) < 0,
|
||||
(p) => p.character?.isAlive && playersInViewArea.indexOf(p.character!) < 0,
|
||||
);
|
||||
|
||||
return otherPlayers.map(
|
||||
|
|
@ -244,7 +245,6 @@ export class Player extends CommandReceiver {
|
|||
|
||||
public destroy() {
|
||||
this.isActive = false;
|
||||
|
||||
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 () => {
|
||||
try {
|
||||
let game: Game;
|
||||
SoundHandler.initialize();
|
||||
|
||||
const firstClickListener = () => {
|
||||
SoundHandler.initialize();
|
||||
document.removeEventListener('click', firstClickListener);
|
||||
};
|
||||
document.addEventListener('click', firstClickListener);
|
||||
|
||||
handleFullScreen(minimize, maximize);
|
||||
toggleSettingsButton.addEventListener('click', toggleSettings);
|
||||
|
|
@ -134,7 +139,7 @@ const main = async () => {
|
|||
|
||||
for (;;) {
|
||||
show(spinner);
|
||||
hide(logoutButton);
|
||||
hide(logoutButton, true);
|
||||
show(landingUI, true, 'flex');
|
||||
|
||||
const background = new LandingPageBackground(canvas);
|
||||
|
|
@ -156,7 +161,7 @@ const main = async () => {
|
|||
await game.started;
|
||||
isInGame = true;
|
||||
hide(spinner);
|
||||
show(logoutButton);
|
||||
show(logoutButton, true, 'block');
|
||||
await gameOver;
|
||||
isInGame = false;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -37,7 +37,9 @@ h1 {
|
|||
font-size: 6rem;
|
||||
text-align: center;
|
||||
padding-bottom: $medium-padding;
|
||||
|
||||
@media (max-width: $height-breakpoint) {
|
||||
font-size: 4.5rem;
|
||||
}
|
||||
@media (max-height: $height-breakpoint) {
|
||||
display: none;
|
||||
}
|
||||
|
|
@ -163,11 +165,23 @@ body {
|
|||
font-size: 0;
|
||||
transform: translateX(-50%) translateY(-50%);
|
||||
|
||||
box-shadow: inset 0 0 3px 0px rgba(0, 0, 0, 0.2);
|
||||
|
||||
@include square(50px);
|
||||
border-radius: 1000px;
|
||||
mask-image: url('../static/mask.svg');
|
||||
}
|
||||
|
||||
.falling-point {
|
||||
font-size: 1.2em;
|
||||
&.decla {
|
||||
color: $bright-decla;
|
||||
}
|
||||
&.red {
|
||||
color: $bright-red;
|
||||
}
|
||||
}
|
||||
|
||||
.other-player-arrow {
|
||||
@include square($large-icon);
|
||||
@media (max-width: $breakpoint) {
|
||||
|
|
@ -205,18 +219,29 @@ body {
|
|||
}
|
||||
|
||||
.announcement {
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translateX(-50%) translateY(-50%);
|
||||
top: 25%;
|
||||
transform: translateX(calc(-50% + 50vw)) translateY(-50%);
|
||||
|
||||
font-size: 3rem;
|
||||
@include background;
|
||||
z-index: 1000;
|
||||
padding: $medium-padding;
|
||||
border-radius: 16px;
|
||||
|
||||
&:empty {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.decla {
|
||||
color: $bright-decla;
|
||||
}
|
||||
|
||||
.red {
|
||||
color: $bright-red;
|
||||
}
|
||||
}
|
||||
|
||||
.planet-progress {
|
||||
position: absolute;
|
||||
top: $small-padding;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
|
|
@ -225,26 +250,50 @@ body {
|
|||
$height: 8px;
|
||||
height: $height;
|
||||
|
||||
justify-content: space-between;
|
||||
box-shadow: inset 0 0 3px 0px rgba(0, 0, 0, 0.2);
|
||||
|
||||
border-radius: 4px;
|
||||
z-index: 100;
|
||||
|
||||
div {
|
||||
height: $height;
|
||||
&::before {
|
||||
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;
|
||||
overflow: hidden;
|
||||
&::after {
|
||||
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) {
|
||||
background: $bright-decla;
|
||||
border-radius: 100px 0 0 100px;
|
||||
}
|
||||
|
||||
div:nth-child(2) {
|
||||
background: $bright-neutral;
|
||||
}
|
||||
|
||||
div:nth-child(3) {
|
||||
background: $bright-red;
|
||||
border-radius: 0 100px 100px 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,156 +1,95 @@
|
|||
import { vec2 } from 'gl-matrix';
|
||||
import {
|
||||
CircleLight,
|
||||
ColorfulCircle,
|
||||
FilteringOptions,
|
||||
Flashlight,
|
||||
Renderer,
|
||||
renderNoise,
|
||||
runAnimation,
|
||||
WrapOptions,
|
||||
} from 'sdf-2d';
|
||||
import { Renderer, renderNoise } from 'sdf-2d';
|
||||
import {
|
||||
broadcastCommands,
|
||||
deserialize,
|
||||
serialize,
|
||||
settings,
|
||||
TransportEvents,
|
||||
SetAspectRatioActionCommand,
|
||||
PlayerInformation,
|
||||
PlayerDiedCommand,
|
||||
UpdateGameState,
|
||||
UpdateOtherPlayerDirections,
|
||||
clamp,
|
||||
UpdateGameState,
|
||||
GameEnd,
|
||||
CharacterTeam,
|
||||
ServerAnnouncement,
|
||||
GameStart,
|
||||
CommandReceiver,
|
||||
CommandExecutors,
|
||||
Command,
|
||||
} from 'shared';
|
||||
import io from 'socket.io-client';
|
||||
import { KeyboardListener } from './commands/generators/keyboard-listener';
|
||||
import { MouseListener } from './commands/generators/mouse-listener';
|
||||
import { TouchJoystickListener } from './commands/generators/touch-joystick-listener';
|
||||
import { CommandReceiverSocket } from './commands/receivers/command-receiver-socket';
|
||||
import { startAnimation } from './start-animation';
|
||||
import { PlayerDecision } from './join-form-handler';
|
||||
import { GameObjectContainer } from './objects/game-object-container';
|
||||
import { OptionsHandler } from './options-handler';
|
||||
import { BlobShape } from './shapes/blob-shape';
|
||||
import { PlanetShape } from './shapes/planet-shape';
|
||||
|
||||
export class Game {
|
||||
public readonly gameObjects = new GameObjectContainer(this);
|
||||
export class Game extends CommandReceiver {
|
||||
public gameObjects = new GameObjectContainer(this);
|
||||
public renderer?: Renderer;
|
||||
private socket!: SocketIOClient.Socket;
|
||||
private deadTimeout = 0;
|
||||
private isBetweenGames = false;
|
||||
|
||||
public started: Promise<void>;
|
||||
private resolveStarted!: () => unknown;
|
||||
|
||||
private declaPlanetCountElement = document.createElement('div');
|
||||
private redPlanetCountElement = document.createElement('div');
|
||||
private neutralPlanetCountElement = document.createElement('div');
|
||||
private announcementText = document.createElement('h2');
|
||||
private progressBar = document.createElement('div');
|
||||
private arrowElements: Array<HTMLElement> = [];
|
||||
|
||||
constructor(
|
||||
private readonly playerDecision: PlayerDecision,
|
||||
private readonly canvas: HTMLCanvasElement,
|
||||
private readonly overlay: HTMLElement,
|
||||
) {
|
||||
super();
|
||||
this.started = new Promise((r) => (this.resolveStarted = r));
|
||||
this.announcementText.className = 'announcement';
|
||||
const progressBar = document.createElement('div');
|
||||
progressBar.className = 'planet-progress';
|
||||
overlay.appendChild(progressBar);
|
||||
progressBar.appendChild(this.declaPlanetCountElement);
|
||||
progressBar.appendChild(this.neutralPlanetCountElement);
|
||||
progressBar.appendChild(this.redPlanetCountElement);
|
||||
this.progressBar.className = 'planet-progress';
|
||||
this.progressBar.appendChild(this.declaPlanetCountElement);
|
||||
this.progressBar.appendChild(this.redPlanetCountElement);
|
||||
}
|
||||
|
||||
private arrowElements: Array<HTMLElement> = [];
|
||||
private async setupCommunication(serverUrl: string): Promise<void> {
|
||||
this.socket = io(serverUrl, {
|
||||
private initialize() {
|
||||
this.isBetweenGames = true;
|
||||
|
||||
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,
|
||||
transports: ['websocket'],
|
||||
forceNew: true,
|
||||
});
|
||||
|
||||
this.socket.on('reconnect_attempt', () => {
|
||||
this.socket.io.opts.transports = ['polling', 'websocket'];
|
||||
});
|
||||
|
||||
this.socket.on('disconnect', this.destroy.bind(this));
|
||||
|
||||
this.socket.on(TransportEvents.ServerToPlayer, (serialized: string) => {
|
||||
const command = deserialize(serialized);
|
||||
if (command instanceof PlayerDiedCommand) {
|
||||
this.deadTimeout = command.timeout;
|
||||
if (OptionsHandler.options.vibrationEnabled) {
|
||||
navigator.vibrate(150);
|
||||
}
|
||||
this.overlay.appendChild(this.announcementText);
|
||||
} else if (command instanceof UpdateGameState) {
|
||||
const all = command.declaCount + command.redCount + command.neutralCount;
|
||||
this.declaPlanetCountElement.style.width = (command.declaCount / all) * 100 + '%';
|
||||
this.neutralPlanetCountElement.style.width =
|
||||
(command.neutralCount / all) * 100 + '%';
|
||||
this.redPlanetCountElement.style.width = (command.redCount / all) * 100 + '%';
|
||||
|
||||
if (command.declaCount > all * 0.5) {
|
||||
this.overlay.appendChild(this.announcementText);
|
||||
this.announcementText.innerText = 'Decla team won 🎉';
|
||||
}
|
||||
|
||||
if (command.redCount > all * 0.5) {
|
||||
this.overlay.appendChild(this.announcementText);
|
||||
this.announcementText.innerText = 'Red team won 🎉';
|
||||
}
|
||||
|
||||
this.arrowElements
|
||||
.splice(command.otherPlayerDirections.length, this.arrowElements.length)
|
||||
.forEach((e) => e.parentElement?.removeChild(e));
|
||||
|
||||
for (
|
||||
let i = this.arrowElements.length;
|
||||
i < command.otherPlayerDirections.length;
|
||||
i++
|
||||
) {
|
||||
const element = document.createElement('div');
|
||||
this.arrowElements.push(element);
|
||||
this.overlay.appendChild(element);
|
||||
}
|
||||
|
||||
this.arrowElements.forEach((e, i) => {
|
||||
const direction = command.otherPlayerDirections[i].direction;
|
||||
const team = command.otherPlayerDirections[i].team;
|
||||
const angle = Math.atan2(direction.y, direction.x);
|
||||
e.className = 'other-player-arrow ' + team;
|
||||
|
||||
const { width, height } = this.overlay.getBoundingClientRect();
|
||||
const aspectRatio = width / height;
|
||||
const directionRatio = direction.x / direction.y;
|
||||
|
||||
let deltaX: number, deltaY: number;
|
||||
if (aspectRatio < Math.abs(directionRatio)) {
|
||||
deltaX = (width / 2) * Math.sign(direction.x);
|
||||
deltaY = deltaX / directionRatio;
|
||||
} else {
|
||||
deltaY = (height / 2) * Math.sign(direction.y);
|
||||
deltaX = deltaY * directionRatio;
|
||||
}
|
||||
|
||||
const delta = vec2.fromValues(deltaX, deltaY);
|
||||
const center = vec2.fromValues(width / 2, height / 2);
|
||||
const p = vec2.add(center, center, delta);
|
||||
const arrowPadding = 24;
|
||||
vec2.set(
|
||||
p,
|
||||
clamp(p.x, arrowPadding, width - arrowPadding),
|
||||
clamp(height - p.y, arrowPadding, height - arrowPadding),
|
||||
);
|
||||
e.style.transform = `translateX(${p.x}px) translateY(${
|
||||
p.y
|
||||
}px) translateX(-50%) translateY(-50%) rotate(${-angle + Math.PI / 2}rad) `;
|
||||
});
|
||||
} else this.gameObjects.sendCommand(command);
|
||||
this.socket.on('disconnect', () => {
|
||||
if (!this.isBetweenGames) {
|
||||
this.destroy();
|
||||
}
|
||||
});
|
||||
|
||||
this.socket.on(TransportEvents.Ping, () => {
|
||||
this.socket.emit(TransportEvents.Pong);
|
||||
});
|
||||
|
||||
this.socket.emit(TransportEvents.PlayerJoining, {
|
||||
name: this.playerDecision.playerName,
|
||||
} as PlayerInformation);
|
||||
this.socket.on(TransportEvents.ServerToPlayer, (serialized: string) =>
|
||||
this.sendCommand(deserialize(serialized)),
|
||||
);
|
||||
|
||||
broadcastCommands(
|
||||
[
|
||||
|
|
@ -160,57 +99,94 @@ export class Game {
|
|||
],
|
||||
[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) {
|
||||
navigator.vibrate(150);
|
||||
}
|
||||
},
|
||||
[UpdateGameState.type]: (c: UpdateGameState) => {
|
||||
this.declaPlanetCountElement.style.width = (c.declaCount / c.limit) * 50 + '%';
|
||||
this.redPlanetCountElement.style.width = (c.redCount / c.limit) * 50 + '%';
|
||||
},
|
||||
[GameEnd.type]: (c: GameEnd) => {
|
||||
const team =
|
||||
c.winningTeam === CharacterTeam.decla
|
||||
? '<span class="decla">decla</span>'
|
||||
: '<span class="red">red</span>';
|
||||
this.announcementText.innerHTML = `Team ${team} won 🎉`;
|
||||
},
|
||||
[UpdateOtherPlayerDirections.type]: this.handleOtherPlayerDirections.bind(this),
|
||||
[GameStart.type]: this.initialize.bind(this),
|
||||
};
|
||||
|
||||
private handleOtherPlayerDirections(command: UpdateOtherPlayerDirections) {
|
||||
this.arrowElements
|
||||
.splice(command.otherPlayerDirections.length, this.arrowElements.length)
|
||||
.forEach((e) => e.parentElement?.removeChild(e));
|
||||
|
||||
for (
|
||||
let i = this.arrowElements.length;
|
||||
i < command.otherPlayerDirections.length;
|
||||
i++
|
||||
) {
|
||||
const element = document.createElement('div');
|
||||
this.arrowElements.push(element);
|
||||
this.overlay.appendChild(element);
|
||||
}
|
||||
|
||||
this.arrowElements.forEach((e, i) => {
|
||||
const direction = command.otherPlayerDirections[i].direction;
|
||||
const team = command.otherPlayerDirections[i].team;
|
||||
const angle = Math.atan2(direction.y, direction.x);
|
||||
e.className = 'other-player-arrow ' + team;
|
||||
|
||||
const { width, height } = this.overlay.getBoundingClientRect();
|
||||
const aspectRatio = width / height;
|
||||
const directionRatio = direction.x / direction.y;
|
||||
|
||||
let deltaX: number, deltaY: number;
|
||||
if (aspectRatio < Math.abs(directionRatio)) {
|
||||
deltaX = (width / 2) * Math.sign(direction.x);
|
||||
deltaY = deltaX / directionRatio;
|
||||
} else {
|
||||
deltaY = (height / 2) * Math.sign(direction.y);
|
||||
deltaX = deltaY * directionRatio;
|
||||
}
|
||||
|
||||
const delta = vec2.fromValues(deltaX, deltaY);
|
||||
const center = vec2.fromValues(width / 2, height / 2);
|
||||
const p = vec2.add(center, center, delta);
|
||||
const arrowPadding = 24;
|
||||
vec2.set(
|
||||
p,
|
||||
clamp(p.x, arrowPadding, width - arrowPadding),
|
||||
clamp(height - p.y, arrowPadding, height - arrowPadding),
|
||||
);
|
||||
e.style.transform = `translateX(${p.x}px) translateY(${
|
||||
p.y
|
||||
}px) translateX(-50%) translateY(-50%) rotate(${-angle + Math.PI / 2}rad) `;
|
||||
});
|
||||
}
|
||||
|
||||
public async start(): Promise<void> {
|
||||
const noiseTexture = await renderNoise([256, 256], 2, 1);
|
||||
this.setupCommunication(this.playerDecision.server);
|
||||
await runAnimation(
|
||||
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.initialize();
|
||||
await startAnimation(this.canvas, this.gameLoop.bind(this), noiseTexture);
|
||||
this.socket.close();
|
||||
this.overlay.innerHTML = '';
|
||||
}
|
||||
|
|
@ -237,17 +213,13 @@ export class Game {
|
|||
|
||||
private gameLoop(
|
||||
renderer: Renderer,
|
||||
currentTime: DOMHighResTimeStamp,
|
||||
_: DOMHighResTimeStamp,
|
||||
deltaTime: DOMHighResTimeStamp,
|
||||
): boolean {
|
||||
this.resolveStarted();
|
||||
deltaTime /= 1000;
|
||||
|
||||
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.drawObjects(this.renderer, this.overlay);
|
||||
|
||||
|
|
|
|||
|
|
@ -95,6 +95,9 @@ class ServerChooserOption {
|
|||
private divElement = document.createElement('div');
|
||||
private inputElement = document.createElement('input');
|
||||
private labelElement = document.createElement('label');
|
||||
private serverNameElement = document.createElement('span');
|
||||
private completionElement = document.createElement('span');
|
||||
|
||||
private socket: SocketIOClient.Socket;
|
||||
|
||||
constructor(
|
||||
|
|
@ -111,7 +114,11 @@ class ServerChooserOption {
|
|||
this.labelElement.htmlFor = url;
|
||||
this.divElement.appendChild(this.inputElement);
|
||||
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, {
|
||||
reconnection: false,
|
||||
|
|
@ -121,11 +128,15 @@ class ServerChooserOption {
|
|||
this.socket.on('connect_error', this.destroy.bind(this));
|
||||
this.socket.on('connect_timeout', this.destroy.bind(this));
|
||||
this.socket.on('disconnect', this.destroy.bind(this));
|
||||
this.socket.emit(TransportEvents.SubscribeForPlayerCount);
|
||||
this.socket.on(TransportEvents.PlayerCountUpdate, (v: number) => {
|
||||
this.content.playerCount = v;
|
||||
this.setPlayerLabelText();
|
||||
});
|
||||
this.socket.emit(TransportEvents.SubscribeForServerInfoUpdates);
|
||||
this.socket.on(
|
||||
TransportEvents.ServerInfoUpdate,
|
||||
([playerCount, gameState]: [number, number]) => {
|
||||
this.content.playerCount = playerCount;
|
||||
this.content.gameStatePercent = gameState;
|
||||
this.setServerInfoLabelText();
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
public destroy() {
|
||||
|
|
@ -134,8 +145,25 @@ class ServerChooserOption {
|
|||
this.onDestroy(this);
|
||||
}
|
||||
|
||||
private setPlayerLabelText() {
|
||||
this.labelElement.innerText = `${this.content.serverName} - ${this.content.playerCount}/${this.content.playerLimit} players`;
|
||||
private getRoundCompletionText(percent: number): string {
|
||||
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 {
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue