This commit is contained in:
schmelczerandras 2020-10-25 17:54:45 +01:00
parent a66fa63b4b
commit efa838a2ad
20 changed files with 1691 additions and 368 deletions

View file

@ -3,8 +3,8 @@ import { Options } from './options';
export const defaultOptions: Options = { export const defaultOptions: Options = {
port: 3000, port: 3000,
name: 'Test server', name: 'Test server',
playerLimit: 16, playerLimit: 8,
seed: Math.random(), seed: 0,
scoreLimit: 500, scoreLimit: 500,
worldSize: 15000, worldSize: 15000,
}; };

View file

@ -6,7 +6,6 @@ import {
settings, settings,
ServerInformation, ServerInformation,
PlayerInformation, PlayerInformation,
serializable,
UpdateGameState, UpdateGameState,
serialize, serialize,
CharacterTeam, CharacterTeam,
@ -18,7 +17,7 @@ import { DeltaTimeCalculator } from './helper/delta-time-calculator';
import { Options } from './options'; import { Options } from './options';
import { PlayerContainer } from './players/player-container'; import { PlayerContainer } from './players/player-container';
const playerCountSubscribedRoom = 'playerCountUpdates'; const gameStateSubscribedRoom = 'gameStateSubscribedRoom';
export class GameServer { export class GameServer {
private objects!: PhysicalContainer; private objects!: PhysicalContainer;
@ -38,15 +37,15 @@ export class GameServer {
private initialize() { private initialize() {
const previousPlayers = this.players; const previousPlayers = this.players;
this.objects = new PhysicalContainer(); this.objects = new PhysicalContainer();
this.players = new PlayerContainer(this.objects); createWorld(this.objects, this.options.worldSize);
this.objects.initialize();
this.players = new PlayerContainer(this.objects, this.options.playerLimit);
this.deltaTimeCalculator = new DeltaTimeCalculator(); this.deltaTimeCalculator = new DeltaTimeCalculator();
this.deltaTimes = []; this.deltaTimes = [];
this.declaPoints = 0; this.declaPoints = 0;
this.redPoints = 0; this.redPoints = 0;
this.isInEndGame = false; this.isInEndGame = false;
this.timeScaling = 1; this.timeScaling = 1;
createWorld(this.objects, this.options.worldSize);
this.objects.initialize();
previousPlayers?.sendOnSocket(serialize(new GameStart())); previousPlayers?.sendOnSocket(serialize(new GameStart()));
} }
@ -58,23 +57,27 @@ export class GameServer {
io.on('connection', (socket: SocketIO.Socket) => { io.on('connection', (socket: SocketIO.Socket) => {
socket.on(TransportEvents.PlayerJoining, (playerInfo: PlayerInformation) => { socket.on(TransportEvents.PlayerJoining, (playerInfo: PlayerInformation) => {
const player = this.players.createPlayer(playerInfo, socket); try {
socket.on(TransportEvents.PlayerToServer, (json: string) => { const player = this.players.createPlayer(playerInfo, socket);
const command = deserialize(json); socket.on(TransportEvents.PlayerToServer, (json: string) => {
player.sendCommand(command); const command = deserialize(json);
}); player.sendCommand(command);
});
this.sendServerStateUpdate();
socket.on('disconnect', () => {
player.destroy();
this.players.deletePlayer(player);
this.sendServerStateUpdate(); this.sendServerStateUpdate();
});
socket.on('disconnect', () => {
player.destroy();
this.players.deletePlayer(player);
this.sendServerStateUpdate();
});
} catch {
socket.disconnect();
}
}); });
socket.on(TransportEvents.SubscribeForServerInfoUpdates, () => { socket.on(TransportEvents.SubscribeForServerInfoUpdates, () => {
socket.join(playerCountSubscribedRoom); socket.join(gameStateSubscribedRoom);
}); });
}); });
} }
@ -82,7 +85,7 @@ export class GameServer {
private timeSinceLastServerStateUpdate = 0; private timeSinceLastServerStateUpdate = 0;
public sendServerStateUpdate() { public sendServerStateUpdate() {
this.io this.io
.to(playerCountSubscribedRoom) .to(gameStateSubscribedRoom)
.emit(TransportEvents.ServerInfoUpdate, [this.players.count, this.gameProgress]); .emit(TransportEvents.ServerInfoUpdate, [this.players.count, this.gameProgress]);
} }

View file

@ -69,7 +69,7 @@ export class PlanetPhysical
} }
public get team(): CharacterTeam { public get team(): CharacterTeam {
return this.ownership === 0.5 return Math.abs(this.ownership - 0.5) < 0.1
? CharacterTeam.neutral ? CharacterTeam.neutral
: this.ownership < 0.5 : this.ownership < 0.5
? CharacterTeam.decla ? CharacterTeam.decla

View file

@ -190,7 +190,9 @@ export class PlayerCharacterPhysical
} }
public get center(): vec2 { public get center(): vec2 {
return this.head.center; const bodyCenter = vec2.add(vec2.create(), this.head.center, this.leftFoot.center);
vec2.add(bodyCenter, bodyCenter, this.rightFoot.center);
return vec2.scale(bodyCenter, bodyCenter, 1 / 3);
} }
public distance(target: vec2): number { public distance(target: vec2): number {
@ -199,7 +201,7 @@ export class PlayerCharacterPhysical
this.head.distance(target), this.head.distance(target),
this.leftFoot.distance(target), this.leftFoot.distance(target),
this.rightFoot.distance(target), this.rightFoot.distance(target),
) - 20 ) - 5
); );
} }
@ -222,7 +224,7 @@ export class PlayerCharacterPhysical
this.movementActions = []; this.movementActions = [];
} }
return direction; return vec2.normalize(direction, direction);
} }
public step(deltaTime: number) { public step(deltaTime: number) {
@ -251,16 +253,28 @@ export class PlayerCharacterPhysical
this.rightFoot.center, this.rightFoot.center,
); );
vec2.scale(feetCenter, feetCenter, 0.5); vec2.scale(feetCenter, feetCenter, 0.5);
const actualGravity = forceAtPosition(feetCenter, intersectingWithForcefield); const leftFootGravity = forceAtPosition(
this.leftFoot.center,
intersectingWithForcefield,
);
const rightFootGravity = forceAtPosition(
this.rightFoot.center,
intersectingWithForcefield,
);
const direction = this.averageAndResetMovementActions(); const direction = this.averageAndResetMovementActions();
const movementForce = vec2.scale(direction, direction, settings.maxAcceleration); const movementForce = vec2.scale(direction, direction, settings.maxAcceleration);
if (!this.currentPlanet) { if (!this.currentPlanet) {
this.applyForce(this.leftFoot, actualGravity, deltaTime); this.applyForce(this.leftFoot, leftFootGravity, deltaTime);
this.applyForce(this.rightFoot, actualGravity, deltaTime); this.applyForce(this.rightFoot, rightFootGravity, deltaTime);
const sumForce = vec2.subtract(vec2.create(), actualGravity, movementForce); const sumForce = vec2.subtract(
vec2.create(),
// the next line is intentional
vec2.add(vec2.create(), leftFootGravity, rightFootGravity),
movementForce,
);
this.setDirection( this.setDirection(
vec2.length(sumForce) === 0 ? vec2.fromValues(0, -1) : sumForce, vec2.length(sumForce) === 0 ? vec2.fromValues(0, -1) : sumForce,
@ -274,7 +288,7 @@ export class PlayerCharacterPhysical
vec2.rotate(movementForce, movementForce, vec2.create(), this.direction); vec2.rotate(movementForce, movementForce, vec2.create(), this.direction);
} }
if (vec2.dot(movementForce, actualGravity) < -vec2.length(movementForce) * 0.8) { if (vec2.dot(movementForce, leftFootGravity) < -vec2.length(movementForce) * 0.8) {
vec2.scale(leftFootGravity, leftFootGravity, 0.35); vec2.scale(leftFootGravity, leftFootGravity, 0.35);
vec2.scale(rightFootGravity, rightFootGravity, 0.35); vec2.scale(rightFootGravity, rightFootGravity, 0.35);
} }
@ -283,7 +297,7 @@ export class PlayerCharacterPhysical
const headGravity = this.currentPlanet!.getForce(this.head.center); const headGravity = this.currentPlanet!.getForce(this.head.center);
if (vec2.length(headGravity) < vec2.length(actualGravity) / 2) { if (vec2.length(headGravity) < vec2.length(leftFootGravity) / 2) {
this.currentPlanet = undefined; this.currentPlanet = undefined;
} }
this.setDirection(headGravity, deltaTime); this.setDirection(headGravity, deltaTime);
@ -294,7 +308,9 @@ 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(deltaTime);
this.remoteCall('updateCircles', this.head, this.leftFoot, this.rightFoot); this.remoteCall('updateCircles', this.head, this.leftFoot, this.rightFoot);
} }
@ -302,27 +318,68 @@ export class PlayerCharacterPhysical
this.direction = interpolateAngles( this.direction = interpolateAngles(
this.direction, this.direction,
Math.atan2(direction.y, direction.x) + Math.PI / 2, Math.atan2(direction.y, direction.x) + Math.PI / 2,
Math.pow(4, deltaTime), Math.pow(2, deltaTime),
); );
} }
private keepPosture() { private keepPosture(deltaTime: number) {
const bodyCenter = vec2.add(vec2.create(), this.head.center, this.leftFoot.center); const center = this.center;
vec2.add(bodyCenter, bodyCenter, this.rightFoot.center); this.springMove(
vec2.scale(bodyCenter, bodyCenter, 1 / 3); this.leftFoot,
this.springMove(this.leftFoot, bodyCenter, PlayerCharacterPhysical.leftFootOffset); center,
this.springMove(this.rightFoot, bodyCenter, PlayerCharacterPhysical.rightFootOffset); PlayerCharacterPhysical.leftFootOffset,
this.springMove(this.head, bodyCenter, PlayerCharacterPhysical.headOffset); deltaTime,
);
this.springMove(
this.rightFoot,
center,
PlayerCharacterPhysical.rightFootOffset,
deltaTime,
);
/*
const feetDelta = vec2.subtract(
vec2.create(),
this.leftFoot.center,
this.rightFoot.center,
);
const desiredDistance = vec2.dist(
PlayerCharacterPhysical.desiredLeftFootOffset,
PlayerCharacterPhysical.desiredRightFootOffset,
);
const actualDistance = vec2.length(feetDelta);
const delta = vec2.normalize(feetDelta, feetDelta);
vec2.scale(delta, delta, Math.min(actualDistance / 2, deltaTime * 200));
let hitObject = this.rightFoot.tryMove(delta);
if (hitObject instanceof PlanetPhysical) {
this.secondsSinceOnSurface = 0;
this.currentPlanet = hitObject;
}
vec2.scale(delta, delta, -1);
hitObject = this.leftFoot.tryMove(delta);
if (hitObject instanceof PlanetPhysical) {
this.secondsSinceOnSurface = 0;
this.currentPlanet = hitObject;
}*/
this.springMove(this.head, center, PlayerCharacterPhysical.headOffset, deltaTime);
} }
private springMove(object: CirclePhysical, center: vec2, offset: vec2) { private springMove(
// todo: make time-independent object: CirclePhysical,
const springConstant = 0.55; center: vec2,
offset: vec2,
deltaTime: number,
) {
const desiredPosition = vec2.add(vec2.create(), center, offset); const desiredPosition = vec2.add(vec2.create(), center, offset);
vec2.rotate(desiredPosition, desiredPosition, center, this.direction); vec2.rotate(desiredPosition, desiredPosition, center, this.direction);
const positionDelta = vec2.subtract(desiredPosition, desiredPosition, object.center); const positionDelta = vec2.subtract(desiredPosition, desiredPosition, object.center);
vec2.scale(positionDelta, positionDelta, springConstant); const positionDeltaDirection = vec2.normalize(vec2.create(), positionDelta);
const positionDeltaLength = vec2.length(positionDelta);
vec2.scale(
positionDelta,
positionDeltaDirection,
Math.min(positionDeltaLength, (positionDeltaLength / 50) * deltaTime * 800),
);
const hitObject = object.tryMove(positionDelta); const hitObject = object.tryMove(positionDelta);
if (hitObject instanceof PlanetPhysical) { if (hitObject instanceof PlanetPhysical) {

132
backend/src/players/npc.ts Normal file
View file

@ -0,0 +1,132 @@
import { vec2 } from 'gl-matrix';
import {
PlayerInformation,
CharacterTeam,
settings,
Circle,
Random,
MoveActionCommand,
} from 'shared';
import { PhysicalContainer } from '../physics/containers/physical-container';
import { PlayerContainer } from './player-container';
import { PlayerBase } from './player-base';
import { getBoundingBoxOfCircle } from '../physics/functions/get-bounding-box-of-circle';
import { PlayerCharacterPhysical } from '../objects/player-character-physical';
import { PlanetPhysical } from '../objects/planet-physical';
export class NPC extends PlayerBase {
private moveTarget: vec2 = vec2.create();
private planet?: PlanetPhysical;
private timeSinceLastFindTarget = 10000;
private timeSinceLastFindShootTarget = 10000;
constructor(
playerInfo: PlayerInformation,
playerContainer: PlayerContainer,
objectContainer: PhysicalContainer,
team: CharacterTeam,
) {
super(playerInfo, playerContainer, objectContainer, team);
this.createCharacter();
this.step(0);
}
private findTarget() {
const observableArea = getBoundingBoxOfCircle(new Circle(this.center, 100));
const nearObjects = this.objectContainer.findIntersecting(observableArea);
const enemies = nearObjects.filter(
(o) =>
o.gameObject instanceof PlayerCharacterPhysical &&
o.gameObject.team !== this.team,
);
const allies = nearObjects.filter(
(o) =>
o.gameObject instanceof PlayerCharacterPhysical &&
o.gameObject.team === this.team,
);
const notControlledPlanets = nearObjects.filter(
(o) => o.gameObject instanceof PlanetPhysical && o.gameObject.team !== this.team,
);
if (enemies.length > allies.length) {
console.log('fleeing');
const enemiesCenter = enemies.reduce(
(sum, e) => vec2.add(sum, sum, (e.gameObject as PlayerCharacterPhysical).center),
vec2.create(),
);
vec2.scale(enemiesCenter, enemiesCenter, 1 / enemies.length);
const enemiesDelta = vec2.subtract(enemiesCenter, this.center, enemiesCenter);
if (vec2.length(enemiesDelta) > 0) {
vec2.scale(enemiesDelta, enemiesDelta, 200 / vec2.length(enemiesDelta));
}
vec2.add(this.moveTarget, this.center, enemiesDelta);
} else if (!this.planet) {
if (notControlledPlanets.length > 0) {
this.planet = notControlledPlanets[0] as PlanetPhysical;
this.moveTarget = this.planet.center;
} else {
this.moveTarget = vec2.fromValues(
Random.getRandomInRange(-5000, 5000),
Random.getRandomInRange(-5000, 5000),
);
}
}
}
private findShootTarget(): vec2 | undefined {
const observableArea = getBoundingBoxOfCircle(new Circle(this.center, 1000));
const nearObjects = this.objectContainer.findIntersecting(observableArea);
const enemies = nearObjects.filter(
(o) =>
o.gameObject instanceof PlayerCharacterPhysical &&
o.gameObject.team !== this.team,
);
if (enemies.length > 0) {
return (enemies[0].gameObject as PlayerCharacterPhysical).center;
}
}
private timeUntilRespawn = 0;
public step(deltaTimeInSeconds: number) {
if (this.character) {
this.center = this.character?.center;
if (!this.character.isAlive) {
this.sumDeaths++;
this.sumKills = this.character.killCount;
this.character = null;
this.timeUntilRespawn = settings.playerDiedTimeout;
}
} else {
if ((this.timeUntilRespawn -= deltaTimeInSeconds) < 0) {
this.createCharacter();
this.center = this.character!.center;
}
}
if (this.planet && Math.abs(this.planet.ownership - 0.5) > 0.3) {
this.planet = undefined;
this.findTarget();
} else {
if ((this.timeSinceLastFindTarget += deltaTimeInSeconds) > 1) {
this.timeSinceLastFindTarget = 0;
this.findTarget();
}
}
if ((this.timeSinceLastFindShootTarget += deltaTimeInSeconds) > 0.5) {
const shootTarget = this.findShootTarget();
if (shootTarget) {
this.character?.shootTowards(shootTarget);
}
this.timeSinceLastFindShootTarget = 0;
}
const targetDelta = vec2.subtract(vec2.create(), this.moveTarget, this.center);
vec2.normalize(targetDelta, targetDelta);
this.character?.handleMovementAction(new MoveActionCommand(targetDelta, false));
}
}

View file

@ -0,0 +1,78 @@
import { vec2 } from 'gl-matrix';
import { CommandReceiver, Circle, PlayerInformation, CharacterTeam } from 'shared';
import { PhysicalContainer } from '../physics/containers/physical-container';
import { getBoundingBoxOfCircle } from '../physics/functions/get-bounding-box-of-circle';
import { isCircleIntersecting } from '../physics/functions/is-circle-intersecting';
import { PlayerCharacterPhysical } from '../objects/player-character-physical';
import { PlayerContainer } from './player-container';
export abstract class PlayerBase extends CommandReceiver {
public character?: PlayerCharacterPhysical | null;
public center: vec2 = vec2.create();
protected sumKills = 0;
protected sumDeaths = 0;
constructor(
protected readonly playerInfo: PlayerInformation,
protected readonly playerContainer: PlayerContainer,
protected readonly objectContainer: PhysicalContainer,
public readonly team: CharacterTeam,
) {
super();
}
protected createCharacter() {
this.character = new PlayerCharacterPhysical(
this.playerInfo.name.slice(0, 20),
this.sumKills,
this.sumDeaths,
this.team,
this.objectContainer,
this.findEmptyPositionForPlayer(),
);
this.objectContainer.addObject(this.character);
}
public abstract step(deltaTimeInSeconds: number): void;
protected findEmptyPositionForPlayer(): vec2 {
let possibleCenter = this.playerContainer.players.find(
(p) => p.character?.isAlive && p.team === this.team,
)?.center;
if (!possibleCenter) {
possibleCenter = vec2.create();
}
let rotation = 0;
let radius = 0;
for (;;) {
const playerPosition = vec2.fromValues(
radius * Math.cos(rotation) + possibleCenter.x,
radius * Math.sin(rotation) + possibleCenter.y,
);
const playerBoundingCircle = new Circle(
playerPosition,
PlayerCharacterPhysical.boundRadius,
);
const playerBoundingBox = getBoundingBoxOfCircle(playerBoundingCircle);
const possibleIntersectors = this.objectContainer.findIntersecting(
playerBoundingBox,
);
if (!isCircleIntersecting(playerBoundingCircle, possibleIntersectors)) {
return playerPosition;
}
rotation += Math.PI / 8;
radius += 30;
}
}
public destroy() {
this.character?.destroy();
}
}

View file

@ -1,31 +1,61 @@
import { CharacterTeam, PlayerInformation, Random, TransportEvents } from 'shared'; import {
CharacterTeam,
PlayerInformation,
Random,
TransportEvents,
settings,
} from 'shared';
import { PhysicalContainer } from '../physics/containers/physical-container'; import { PhysicalContainer } from '../physics/containers/physical-container';
import { NPC } from './npc';
import { Player } from './player'; import { Player } from './player';
import { PlayerBase } from './player-base';
export class PlayerContainer { export class PlayerContainer {
private _players: Array<Player> = []; private _players: Array<Player> = [];
private _npcs: Array<NPC> = [];
constructor(private readonly objects: PhysicalContainer) {} constructor(
private readonly objects: PhysicalContainer,
private readonly playerMaxCount: number,
) {
this.createNPCs();
}
public createNPCs() {
const newNpcCount = this.playerMaxCount - this._players.length - this._npcs.length;
for (let i = 0; i < newNpcCount; i++) {
const name = `BOT ${Random.choose(settings.npcNames)}`;
this._npcs.push(
new NPC({ name }, this, this.objects, this.getTeamOfNextPlayer(true)),
);
}
}
public createPlayer(playerInfo: PlayerInformation, socket: SocketIO.Socket): Player { public createPlayer(playerInfo: PlayerInformation, socket: SocketIO.Socket): Player {
const player = new Player( if (this._players.length === this.playerMaxCount) {
playerInfo, throw new Error('Too many players');
this, }
this.objects,
socket, const team = this.getTeamOfNextPlayer();
this.getTeamOfNextPlayer(), let npcToReplace = this._npcs.find((n) => n.team === team);
); if (!npcToReplace) {
npcToReplace = this._npcs.find((n) => n.team !== team);
}
npcToReplace?.destroy();
this._npcs = this._npcs.filter((n) => n !== npcToReplace);
const player = new Player(playerInfo, this, this.objects, team, socket);
this._players.push(player); this._players.push(player);
return player; return player;
} }
public get players(): Array<Player> { public get players(): Array<PlayerBase> {
return this._players; return [...this._players, ...this._npcs];
} }
public get count(): number { public get count(): number {
return this.players.length; return this._players.length;
} }
public step(deltaTimeInSeconds: number) { public step(deltaTimeInSeconds: number) {
@ -33,12 +63,13 @@ export class PlayerContainer {
} }
public sendOnSocket(message: any) { public sendOnSocket(message: any) {
this.players.forEach((p) => p.socket.emit(TransportEvents.ServerToPlayer, message)); this._players.forEach((p) => p.socket.emit(TransportEvents.ServerToPlayer, message));
} }
private getTeamOfNextPlayer(): CharacterTeam { private getTeamOfNextPlayer(isNpc = false): CharacterTeam {
const declaCount = this._players.filter((p) => p.team === CharacterTeam.decla).length; const players = isNpc ? this.players : this._players;
const redCount = this._players.filter((p) => p.team === CharacterTeam.red).length; const declaCount = players.filter((p) => p.team === CharacterTeam.decla).length;
const redCount = players.filter((p) => p.team === CharacterTeam.red).length;
if ((declaCount === redCount && Random.getRandom() >= 0.5) || declaCount < redCount) { if ((declaCount === redCount && Random.getRandom() >= 0.5) || declaCount < redCount) {
return CharacterTeam.decla; return CharacterTeam.decla;
@ -49,5 +80,6 @@ export class PlayerContainer {
public deletePlayer(player: Player) { public deletePlayer(player: Player) {
this._players = this._players.filter((p) => p !== player); this._players = this._players.filter((p) => p !== player);
this.createNPCs();
} }
} }

View file

@ -27,35 +27,18 @@ import {
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';
import { PhysicalContainer } from '../physics/containers/physical-container'; import { PhysicalContainer } from '../physics/containers/physical-container';
import { getBoundingBoxOfCircle } from '../physics/functions/get-bounding-box-of-circle';
import { isCircleIntersecting } from '../physics/functions/is-circle-intersecting';
import { PlayerCharacterPhysical } from '../objects/player-character-physical'; import { PlayerCharacterPhysical } from '../objects/player-character-physical';
import { PlanetPhysical } from '../objects/planet-physical';
import { PlayerContainer } from './player-container'; import { PlayerContainer } from './player-container';
import { PlayerBase } from './player-base';
export class Player extends CommandReceiver { export class Player extends PlayerBase {
private character?: PlayerCharacterPhysical | null;
private aspectRatio: number = 16 / 9; private aspectRatio: number = 16 / 9;
private isActive = true; private isActive = true;
private sumKills = 0;
private sumDeaths = 0;
private objectsPreviouslyInViewArea: Array<GameObject> = []; private objectsPreviouslyInViewArea: Array<GameObject> = [];
private pingTime?: number; private pingTime?: number;
private _latency?: number; private _latency?: number;
public measureLatency() {
this.pingTime = getTimeInMilliseconds();
this.socket.emit(TransportEvents.Ping);
if (this.isActive) {
setTimeout(this.measureLatency.bind(this), 10000);
}
}
public get latency(): number | undefined {
return this._latency;
}
protected commandExecutors: CommandExecutors = { protected commandExecutors: CommandExecutors = {
[SetAspectRatioActionCommand.type]: (v: SetAspectRatioActionCommand) => [SetAspectRatioActionCommand.type]: (v: SetAspectRatioActionCommand) =>
@ -68,14 +51,13 @@ export class Player extends CommandReceiver {
}; };
constructor( constructor(
private readonly playerInfo: PlayerInformation, playerInfo: PlayerInformation,
private readonly playerContainer: PlayerContainer, playerContainer: PlayerContainer,
private readonly objectContainer: PhysicalContainer, objectContainer: PhysicalContainer,
team: CharacterTeam,
public readonly socket: SocketIO.Socket, public readonly socket: SocketIO.Socket,
public readonly team: CharacterTeam,
) { ) {
super(); super(playerInfo, playerContainer, objectContainer, team);
this.team = team;
this.createCharacter(); this.createCharacter();
@ -88,26 +70,25 @@ export class Player extends CommandReceiver {
this.step(0); this.step(0);
} }
private createCharacter() { public measureLatency() {
this.character = new PlayerCharacterPhysical( this.pingTime = getTimeInMilliseconds();
this.playerInfo.name.slice(0, 20), this.socket.emit(TransportEvents.Ping);
this.sumKills, if (this.isActive) {
this.sumDeaths, setTimeout(this.measureLatency.bind(this), 10000);
this.team, }
this.objectContainer, }
this.findEmptyPositionForPlayer(),
); public get latency(): number | undefined {
return this._latency;
this.objectContainer.addObject(this.character); }
this.objectsPreviouslyInViewArea.push(this.character);
protected createCharacter() {
this.socket.emit( super.createCharacter();
TransportEvents.ServerToPlayer,
serialize(new CreatePlayerCommand(this.character)), this.objectsPreviouslyInViewArea.push(this.character!);
); this.sendToPlayer(new CreatePlayerCommand(this.character!));
} }
private center: vec2 = vec2.create();
private timeUntilRespawn = 0; private timeUntilRespawn = 0;
public step(deltaTimeInSeconds: number) { public step(deltaTimeInSeconds: number) {
if (this.character) { if (this.character) {
@ -173,41 +154,6 @@ export class Player extends CommandReceiver {
this.sendToPlayer(new UpdateOtherPlayerDirections(this.getOtherPlayers())); this.sendToPlayer(new UpdateOtherPlayerDirections(this.getOtherPlayers()));
} }
private findEmptyPositionForPlayer(): vec2 {
let possibleCenter = this.playerContainer.players.find(
(p) => p.character?.isAlive && p.team === this.team,
)?.center;
if (!possibleCenter) {
possibleCenter = vec2.create();
}
let rotation = 0;
let radius = 0;
for (;;) {
const playerPosition = vec2.fromValues(
radius * Math.cos(rotation) + possibleCenter.x,
radius * Math.sin(rotation) + possibleCenter.y,
);
const playerBoundingCircle = new Circle(
playerPosition,
PlayerCharacterPhysical.boundRadius,
);
const playerBoundingBox = getBoundingBoxOfCircle(playerBoundingCircle);
const possibleIntersectors = this.objectContainer.findIntersecting(
playerBoundingBox,
);
if (!isCircleIntersecting(playerBoundingCircle, possibleIntersectors)) {
return playerPosition;
}
rotation += Math.PI / 8;
radius += 30;
}
}
private getOtherPlayers(): Array<OtherPlayerDirection> { private getOtherPlayers(): Array<OtherPlayerDirection> {
if (!this.character) { if (!this.character) {
return []; return [];
@ -244,7 +190,7 @@ export class Player extends CommandReceiver {
} }
public destroy() { public destroy() {
super.destroy();
this.isActive = false; this.isActive = false;
this.character?.destroy();
} }
} }

File diff suppressed because it is too large Load diff

View file

@ -47,7 +47,7 @@
"resolve-url-loader": "^3.1.1", "resolve-url-loader": "^3.1.1",
"sass": "^1.27.0", "sass": "^1.27.0",
"sass-loader": "^8.0.2", "sass-loader": "^8.0.2",
"sdf-2d": "^0.6.0", "sdf-2d": "^0.6.2",
"shared": "file:../shared", "shared": "file:../shared",
"socket.io-client": "^2.3.1", "socket.io-client": "^2.3.1",
"source-map-loader": "^1.1.1", "source-map-loader": "^1.1.1",

View file

@ -1,13 +1,11 @@
import { glMatrix } from 'gl-matrix'; import { glMatrix } from 'gl-matrix';
import { import {
CharacterBase,
LampBase, LampBase,
overrideDeserialization, overrideDeserialization,
PlanetBase, PlanetBase,
PlayerCharacterBase, PlayerCharacterBase,
ProjectileBase, ProjectileBase,
} from 'shared'; } from 'shared';
import { CharacterView } from './scripts/objects/character-view';
import { LampView } from './scripts/objects/lamp-view'; import { LampView } from './scripts/objects/lamp-view';
import { ProjectileView } from './scripts/objects/projectile-view'; import { ProjectileView } from './scripts/objects/projectile-view';
import { PlanetView } from './scripts/objects/planet-view'; import { PlanetView } from './scripts/objects/planet-view';
@ -33,7 +31,6 @@ import { SoundHandler, Sounds } from './scripts/sound-handler';
glMatrix.setMatrixArrayType(Array); glMatrix.setMatrixArrayType(Array);
overrideDeserialization(CharacterBase, CharacterView);
overrideDeserialization(PlayerCharacterBase, PlayerCharacterView); overrideDeserialization(PlayerCharacterBase, PlayerCharacterView);
overrideDeserialization(PlanetBase, PlanetView); overrideDeserialization(PlanetBase, PlanetView);
overrideDeserialization(LampBase, LampView); overrideDeserialization(LampBase, LampView);

View file

@ -115,7 +115,7 @@ body {
.player-tag { .player-tag {
transform: translateX(-50%) translateY(-50%) rotate(-15deg); transform: translateX(-50%) translateY(-50%) rotate(-15deg);
transition: left 200ms, top 200ms; transition: left 150ms, top 150ms;
border-radius: 1000px; border-radius: 1000px;
&.decla { &.decla {

View file

@ -155,7 +155,12 @@ export class Game extends CommandReceiver {
const angle = Math.atan2(direction.y, direction.x); const angle = Math.atan2(direction.y, direction.x);
e.className = 'other-player-arrow ' + team; e.className = 'other-player-arrow ' + team;
const { width, height } = this.overlay.getBoundingClientRect(); if (!this.renderer) {
return;
}
const width = this.renderer.canvasSize.x;
const height = this.renderer.canvasSize.y;
const aspectRatio = width / height; const aspectRatio = width / height;
const directionRatio = direction.x / direction.y; const directionRatio = direction.x / direction.y;

View file

@ -1,35 +0,0 @@
import { vec2 } from 'gl-matrix';
import { Renderer } from 'sdf-2d';
import { CharacterBase, CharacterTeam, Circle, Id, settings } from 'shared';
import { BlobShape } from '../shapes/blob-shape';
import { ViewObject } from './view-object';
export class CharacterView extends CharacterBase implements ViewObject {
private shape: BlobShape;
constructor(
id: Id,
team: CharacterTeam,
health: number,
head?: Circle,
leftFoot?: Circle,
rightFoot?: Circle,
) {
super(id, team, health, head, leftFoot, rightFoot);
this.shape = new BlobShape(settings.colorIndices[team]);
}
public get position(): vec2 {
return this.head!.center;
}
public beforeDestroy(): void {}
public step(deltaTimeInSeconds: number): void {}
public draw(renderer: Renderer, overlay: HTMLElement): void {
this.shape.setCircles([this.head!, this.leftFoot!, this.rightFoot!]);
renderer.addDrawable(this.shape);
}
}

View file

@ -12,7 +12,6 @@ export class PlayerCharacterView extends PlayerCharacterBase implements ViewObje
private nameElement: HTMLElement = document.createElement('div'); private nameElement: HTMLElement = document.createElement('div');
private statsElement: HTMLElement = document.createElement('div'); private statsElement: HTMLElement = document.createElement('div');
private healthElement: HTMLElement = document.createElement('div'); private healthElement: HTMLElement = document.createElement('div');
private timeSinceLastNameElementUpdate = 0;
private previousHealth; private previousHealth;
constructor( constructor(
@ -50,7 +49,6 @@ export class PlayerCharacterView extends PlayerCharacterBase implements ViewObje
} }
public step(deltaTimeInSeconds: number): void { 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) {
@ -75,14 +73,11 @@ export class PlayerCharacterView extends PlayerCharacterBase implements ViewObje
overlay.appendChild(this.nameElement); overlay.appendChild(this.nameElement);
} }
if (this.timeSinceLastNameElementUpdate > 0.15) { const screenPosition = renderer.worldToDisplayCoordinates(
const screenPosition = renderer.worldToDisplayCoordinates( this.calculateTextPosition(),
this.calculateTextPosition(), );
); this.nameElement.style.left = screenPosition.x + 'px';
this.nameElement.style.left = screenPosition.x + 'px'; this.nameElement.style.top = screenPosition.y + 'px';
this.nameElement.style.top = screenPosition.y + 'px';
this.timeSinceLastNameElementUpdate = 0;
}
this.shape.setCircles([this.head!, this.leftFoot!, this.rightFoot!]); this.shape.setCircles([this.head!, this.leftFoot!, this.rightFoot!]);
renderer.addDrawable(this.shape); renderer.addDrawable(this.shape);

View file

@ -33,6 +33,7 @@ export abstract class SoundHandler {
setTimeout(() => { setTimeout(() => {
this.ambientSound.muted = false; this.ambientSound.muted = false;
this.ambientSound.volume = 0.5; this.ambientSound.volume = 0.5;
this.ambientSound.loop = true;
if (!this.isAmbientPlaying) { if (!this.isAmbientPlaying) {
this.ambientSound.pause(); this.ambientSound.pause();
} }

View file

@ -44,7 +44,6 @@ export * from './transport/serialization/serialize';
export * from './transport/serialization/serializes-to'; export * from './transport/serialization/serializes-to';
export * from './transport/serialization/serializable'; export * from './transport/serialization/serializable';
export * from './transport/serialization/override-deserialization'; export * from './transport/serialization/override-deserialization';
export * from './objects/types/character-base';
export * from './objects/types/character-team'; export * from './objects/types/character-team';
export * from './objects/types/player-character-base'; export * from './objects/types/player-character-base';
export * from './objects/types/lamp-base'; export * from './objects/types/lamp-base';

View file

@ -1,26 +0,0 @@
import { Circle } from '../../helper/circle';
import { Id } from '../../transport/identity';
import { serializable } from '../../transport/serialization/serializable';
import { GameObject } from '../game-object';
import { CharacterTeam } from './character-team';
@serializable
export class CharacterBase extends GameObject {
constructor(
id: Id,
public team: CharacterTeam,
public health: number,
public head?: Circle,
public leftFoot?: Circle,
public rightFoot?: Circle,
) {
super(id);
}
public onShoot(strength: number) {}
public toArray(): Array<any> {
const { id, team, health, head, leftFoot, rightFoot } = this;
return [id, team, health, head, leftFoot, rightFoot];
}
}

View file

@ -1,23 +1,23 @@
import { CharacterBase } from './character-base';
import { Circle } from '../../helper/circle'; import { Circle } from '../../helper/circle';
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 { CharacterTeam } from './character-team'; import { CharacterTeam } from './character-team';
@serializable @serializable
export class PlayerCharacterBase extends CharacterBase { export class PlayerCharacterBase extends GameObject {
constructor( constructor(
id: Id, id: Id,
public name: string, public name: string,
public killCount: number, public killCount: number,
public deathCount: number, public deathCount: number,
team: CharacterTeam, public team: CharacterTeam,
health: number, public health: number,
head?: Circle, public head?: Circle,
leftFoot?: Circle, public leftFoot?: Circle,
rightFoot?: Circle, public rightFoot?: Circle,
) { ) {
super(id, team, health, head, leftFoot, rightFoot); super(id);
} }
public onShoot(strength: number) {} public onShoot(strength: number) {}

View file

@ -44,6 +44,64 @@ export const settings = {
backgroundGradient: [rgb255(90, 38, 43), rgb255(43, 39, 73)], backgroundGradient: [rgb255(90, 38, 43), rgb255(43, 39, 73)],
declaColor, declaColor,
declaPlanetColor, declaPlanetColor,
npcNames: [
'Adam',
'Clarence',
'Elliot',
'Elmer',
'Ernie',
'Eugene',
'Fergus',
'Ferris',
'Frank',
'Frasier',
'Fred',
'George',
'Graham',
'Harvey',
'Irwin',
'Larry',
'Lester',
'Marvin',
'Neil',
'Niles',
'Oliver',
'Blaise',
'Andrew',
'Opie',
'Ryan',
'Toby',
'Ulric',
'Ulysses',
'Uri',
'Waldo',
'Wally',
'Walt',
'Wesley',
'Yanni',
'Yogi',
'Yuri',
'Dean',
'Dustin',
'Ethan',
'Harold',
'Henry',
'Irving',
'Jason',
'Jenssen',
'Josh',
'Martin',
'Nick',
'Norm',
'Orin',
'Pat',
'Perry',
'Ron',
'Shawn',
'Tim',
'Will',
'Wyatt',
],
redColor, redColor,
redPlanetColor, redPlanetColor,
colorIndices: { colorIndices: {