Change gameplay

This commit is contained in:
schmelczerandras 2020-10-24 22:24:27 +02:00
parent d79900e3ea
commit 34dae300da
56 changed files with 906 additions and 400 deletions

View file

@ -4,6 +4,7 @@ export const defaultOptions: Options = {
port: 3000,
name: 'Localhost',
playerLimit: 16,
seed: 51,
seed: Math.random(),
scoreLimit: 500,
worldSize: 10000,
};

View file

@ -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,
};
}
}

View file

@ -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);

View file

@ -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';

View 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;

View file

@ -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);
}

View file

@ -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;
}
}

View file

@ -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) {

View file

@ -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);
}
}

View file

@ -2,6 +2,7 @@ export interface Options {
port: number;
name: string;
playerLimit: number;
scoreLimit: number;
seed: number;
worldSize: number;
}

View file

@ -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> {

View file

@ -2,5 +2,4 @@ import { PhysicalBase } from './physical-base';
export interface DynamicPhysical extends PhysicalBase {
readonly canMove: true;
step(deltaTimeInMilliseconds: number): void;
}

View file

@ -9,5 +9,5 @@ export interface PhysicalBase {
readonly gameObject: GameObject;
distance(target: vec2): number;
step(deltaTimeInMilliseconds: number): void;
step(deltaTime: number): void;
}

View file

@ -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;

View file

@ -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();
}
}

View file

@ -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])),
);
}
}