Add npcs
This commit is contained in:
parent
a66fa63b4b
commit
efa838a2ad
20 changed files with 1691 additions and 368 deletions
|
|
@ -3,8 +3,8 @@ import { Options } from './options';
|
|||
export const defaultOptions: Options = {
|
||||
port: 3000,
|
||||
name: 'Test server',
|
||||
playerLimit: 16,
|
||||
seed: Math.random(),
|
||||
playerLimit: 8,
|
||||
seed: 0,
|
||||
scoreLimit: 500,
|
||||
worldSize: 15000,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ import {
|
|||
settings,
|
||||
ServerInformation,
|
||||
PlayerInformation,
|
||||
serializable,
|
||||
UpdateGameState,
|
||||
serialize,
|
||||
CharacterTeam,
|
||||
|
|
@ -18,7 +17,7 @@ import { DeltaTimeCalculator } from './helper/delta-time-calculator';
|
|||
import { Options } from './options';
|
||||
import { PlayerContainer } from './players/player-container';
|
||||
|
||||
const playerCountSubscribedRoom = 'playerCountUpdates';
|
||||
const gameStateSubscribedRoom = 'gameStateSubscribedRoom';
|
||||
|
||||
export class GameServer {
|
||||
private objects!: PhysicalContainer;
|
||||
|
|
@ -38,15 +37,15 @@ export class GameServer {
|
|||
private initialize() {
|
||||
const previousPlayers = this.players;
|
||||
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.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()));
|
||||
}
|
||||
|
||||
|
|
@ -58,23 +57,27 @@ export class GameServer {
|
|||
|
||||
io.on('connection', (socket: SocketIO.Socket) => {
|
||||
socket.on(TransportEvents.PlayerJoining, (playerInfo: PlayerInformation) => {
|
||||
const player = this.players.createPlayer(playerInfo, socket);
|
||||
socket.on(TransportEvents.PlayerToServer, (json: string) => {
|
||||
const command = deserialize(json);
|
||||
player.sendCommand(command);
|
||||
});
|
||||
try {
|
||||
const player = this.players.createPlayer(playerInfo, socket);
|
||||
socket.on(TransportEvents.PlayerToServer, (json: string) => {
|
||||
const command = deserialize(json);
|
||||
player.sendCommand(command);
|
||||
});
|
||||
|
||||
this.sendServerStateUpdate();
|
||||
|
||||
socket.on('disconnect', () => {
|
||||
player.destroy();
|
||||
this.players.deletePlayer(player);
|
||||
this.sendServerStateUpdate();
|
||||
});
|
||||
|
||||
socket.on('disconnect', () => {
|
||||
player.destroy();
|
||||
this.players.deletePlayer(player);
|
||||
this.sendServerStateUpdate();
|
||||
});
|
||||
} catch {
|
||||
socket.disconnect();
|
||||
}
|
||||
});
|
||||
|
||||
socket.on(TransportEvents.SubscribeForServerInfoUpdates, () => {
|
||||
socket.join(playerCountSubscribedRoom);
|
||||
socket.join(gameStateSubscribedRoom);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
@ -82,7 +85,7 @@ export class GameServer {
|
|||
private timeSinceLastServerStateUpdate = 0;
|
||||
public sendServerStateUpdate() {
|
||||
this.io
|
||||
.to(playerCountSubscribedRoom)
|
||||
.to(gameStateSubscribedRoom)
|
||||
.emit(TransportEvents.ServerInfoUpdate, [this.players.count, this.gameProgress]);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -69,7 +69,7 @@ export class PlanetPhysical
|
|||
}
|
||||
|
||||
public get team(): CharacterTeam {
|
||||
return this.ownership === 0.5
|
||||
return Math.abs(this.ownership - 0.5) < 0.1
|
||||
? CharacterTeam.neutral
|
||||
: this.ownership < 0.5
|
||||
? CharacterTeam.decla
|
||||
|
|
|
|||
|
|
@ -190,7 +190,9 @@ export class PlayerCharacterPhysical
|
|||
}
|
||||
|
||||
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 {
|
||||
|
|
@ -199,7 +201,7 @@ export class PlayerCharacterPhysical
|
|||
this.head.distance(target),
|
||||
this.leftFoot.distance(target),
|
||||
this.rightFoot.distance(target),
|
||||
) - 20
|
||||
) - 5
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -222,7 +224,7 @@ export class PlayerCharacterPhysical
|
|||
this.movementActions = [];
|
||||
}
|
||||
|
||||
return direction;
|
||||
return vec2.normalize(direction, direction);
|
||||
}
|
||||
|
||||
public step(deltaTime: number) {
|
||||
|
|
@ -251,16 +253,28 @@ export class PlayerCharacterPhysical
|
|||
this.rightFoot.center,
|
||||
);
|
||||
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 movementForce = vec2.scale(direction, direction, settings.maxAcceleration);
|
||||
|
||||
if (!this.currentPlanet) {
|
||||
this.applyForce(this.leftFoot, actualGravity, deltaTime);
|
||||
this.applyForce(this.rightFoot, actualGravity, deltaTime);
|
||||
this.applyForce(this.leftFoot, leftFootGravity, 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(
|
||||
vec2.length(sumForce) === 0 ? vec2.fromValues(0, -1) : sumForce,
|
||||
|
|
@ -274,7 +288,7 @@ export class PlayerCharacterPhysical
|
|||
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(rightFootGravity, rightFootGravity, 0.35);
|
||||
}
|
||||
|
|
@ -283,7 +297,7 @@ export class PlayerCharacterPhysical
|
|||
|
||||
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.setDirection(headGravity, deltaTime);
|
||||
|
|
@ -294,7 +308,9 @@ export class PlayerCharacterPhysical
|
|||
|
||||
this.stepBodyPart(this.leftFoot, deltaTime);
|
||||
this.stepBodyPart(this.rightFoot, deltaTime);
|
||||
this.keepPosture();
|
||||
|
||||
this.keepPosture(deltaTime);
|
||||
|
||||
this.remoteCall('updateCircles', this.head, this.leftFoot, this.rightFoot);
|
||||
}
|
||||
|
||||
|
|
@ -302,27 +318,68 @@ export class PlayerCharacterPhysical
|
|||
this.direction = interpolateAngles(
|
||||
this.direction,
|
||||
Math.atan2(direction.y, direction.x) + Math.PI / 2,
|
||||
Math.pow(4, deltaTime),
|
||||
Math.pow(2, deltaTime),
|
||||
);
|
||||
}
|
||||
|
||||
private keepPosture() {
|
||||
const bodyCenter = vec2.add(vec2.create(), this.head.center, this.leftFoot.center);
|
||||
vec2.add(bodyCenter, bodyCenter, this.rightFoot.center);
|
||||
vec2.scale(bodyCenter, bodyCenter, 1 / 3);
|
||||
this.springMove(this.leftFoot, bodyCenter, PlayerCharacterPhysical.leftFootOffset);
|
||||
this.springMove(this.rightFoot, bodyCenter, PlayerCharacterPhysical.rightFootOffset);
|
||||
this.springMove(this.head, bodyCenter, PlayerCharacterPhysical.headOffset);
|
||||
private keepPosture(deltaTime: number) {
|
||||
const center = this.center;
|
||||
this.springMove(
|
||||
this.leftFoot,
|
||||
center,
|
||||
PlayerCharacterPhysical.leftFootOffset,
|
||||
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) {
|
||||
// todo: make time-independent
|
||||
const springConstant = 0.55;
|
||||
|
||||
private springMove(
|
||||
object: CirclePhysical,
|
||||
center: vec2,
|
||||
offset: vec2,
|
||||
deltaTime: number,
|
||||
) {
|
||||
const desiredPosition = vec2.add(vec2.create(), center, offset);
|
||||
vec2.rotate(desiredPosition, desiredPosition, center, this.direction);
|
||||
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);
|
||||
|
||||
if (hitObject instanceof PlanetPhysical) {
|
||||
|
|
|
|||
132
backend/src/players/npc.ts
Normal file
132
backend/src/players/npc.ts
Normal 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));
|
||||
}
|
||||
}
|
||||
78
backend/src/players/player-base.ts
Normal file
78
backend/src/players/player-base.ts
Normal 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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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 { NPC } from './npc';
|
||||
import { Player } from './player';
|
||||
import { PlayerBase } from './player-base';
|
||||
|
||||
export class PlayerContainer {
|
||||
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 {
|
||||
const player = new Player(
|
||||
playerInfo,
|
||||
this,
|
||||
this.objects,
|
||||
socket,
|
||||
this.getTeamOfNextPlayer(),
|
||||
);
|
||||
if (this._players.length === this.playerMaxCount) {
|
||||
throw new Error('Too many players');
|
||||
}
|
||||
|
||||
const team = 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);
|
||||
|
||||
return player;
|
||||
}
|
||||
|
||||
public get players(): Array<Player> {
|
||||
return this._players;
|
||||
public get players(): Array<PlayerBase> {
|
||||
return [...this._players, ...this._npcs];
|
||||
}
|
||||
|
||||
public get count(): number {
|
||||
return this.players.length;
|
||||
return this._players.length;
|
||||
}
|
||||
|
||||
public step(deltaTimeInSeconds: number) {
|
||||
|
|
@ -33,12 +63,13 @@ export class PlayerContainer {
|
|||
}
|
||||
|
||||
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 {
|
||||
const declaCount = this._players.filter((p) => p.team === CharacterTeam.decla).length;
|
||||
const redCount = this._players.filter((p) => p.team === CharacterTeam.red).length;
|
||||
private getTeamOfNextPlayer(isNpc = false): CharacterTeam {
|
||||
const players = isNpc ? this.players : this._players;
|
||||
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) {
|
||||
return CharacterTeam.decla;
|
||||
|
|
@ -49,5 +80,6 @@ export class PlayerContainer {
|
|||
|
||||
public deletePlayer(player: Player) {
|
||||
this._players = this._players.filter((p) => p !== player);
|
||||
this.createNPCs();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,35 +27,18 @@ import {
|
|||
import { getTimeInMilliseconds } from '../helper/get-time-in-milliseconds';
|
||||
import { BoundingBox } from '../physics/bounding-boxes/bounding-box';
|
||||
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 { PlanetPhysical } from '../objects/planet-physical';
|
||||
import { PlayerContainer } from './player-container';
|
||||
import { PlayerBase } from './player-base';
|
||||
|
||||
export class Player extends CommandReceiver {
|
||||
private character?: PlayerCharacterPhysical | null;
|
||||
export class Player extends PlayerBase {
|
||||
private aspectRatio: number = 16 / 9;
|
||||
private isActive = true;
|
||||
|
||||
private sumKills = 0;
|
||||
private sumDeaths = 0;
|
||||
|
||||
private objectsPreviouslyInViewArea: Array<GameObject> = [];
|
||||
|
||||
private pingTime?: 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 = {
|
||||
[SetAspectRatioActionCommand.type]: (v: SetAspectRatioActionCommand) =>
|
||||
|
|
@ -68,14 +51,13 @@ export class Player extends CommandReceiver {
|
|||
};
|
||||
|
||||
constructor(
|
||||
private readonly playerInfo: PlayerInformation,
|
||||
private readonly playerContainer: PlayerContainer,
|
||||
private readonly objectContainer: PhysicalContainer,
|
||||
playerInfo: PlayerInformation,
|
||||
playerContainer: PlayerContainer,
|
||||
objectContainer: PhysicalContainer,
|
||||
team: CharacterTeam,
|
||||
public readonly socket: SocketIO.Socket,
|
||||
public readonly team: CharacterTeam,
|
||||
) {
|
||||
super();
|
||||
this.team = team;
|
||||
super(playerInfo, playerContainer, objectContainer, team);
|
||||
|
||||
this.createCharacter();
|
||||
|
||||
|
|
@ -88,26 +70,25 @@ export class Player extends CommandReceiver {
|
|||
this.step(0);
|
||||
}
|
||||
|
||||
private 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);
|
||||
this.objectsPreviouslyInViewArea.push(this.character);
|
||||
|
||||
this.socket.emit(
|
||||
TransportEvents.ServerToPlayer,
|
||||
serialize(new CreatePlayerCommand(this.character)),
|
||||
);
|
||||
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 createCharacter() {
|
||||
super.createCharacter();
|
||||
|
||||
this.objectsPreviouslyInViewArea.push(this.character!);
|
||||
this.sendToPlayer(new CreatePlayerCommand(this.character!));
|
||||
}
|
||||
|
||||
private center: vec2 = vec2.create();
|
||||
private timeUntilRespawn = 0;
|
||||
public step(deltaTimeInSeconds: number) {
|
||||
if (this.character) {
|
||||
|
|
@ -173,41 +154,6 @@ export class Player extends CommandReceiver {
|
|||
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> {
|
||||
if (!this.character) {
|
||||
return [];
|
||||
|
|
@ -244,7 +190,7 @@ export class Player extends CommandReceiver {
|
|||
}
|
||||
|
||||
public destroy() {
|
||||
super.destroy();
|
||||
this.isActive = false;
|
||||
this.character?.destroy();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
1367
frontend/package-lock.json
generated
1367
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -47,7 +47,7 @@
|
|||
"resolve-url-loader": "^3.1.1",
|
||||
"sass": "^1.27.0",
|
||||
"sass-loader": "^8.0.2",
|
||||
"sdf-2d": "^0.6.0",
|
||||
"sdf-2d": "^0.6.2",
|
||||
"shared": "file:../shared",
|
||||
"socket.io-client": "^2.3.1",
|
||||
"source-map-loader": "^1.1.1",
|
||||
|
|
|
|||
|
|
@ -1,13 +1,11 @@
|
|||
import { glMatrix } from 'gl-matrix';
|
||||
import {
|
||||
CharacterBase,
|
||||
LampBase,
|
||||
overrideDeserialization,
|
||||
PlanetBase,
|
||||
PlayerCharacterBase,
|
||||
ProjectileBase,
|
||||
} from 'shared';
|
||||
import { CharacterView } from './scripts/objects/character-view';
|
||||
import { LampView } from './scripts/objects/lamp-view';
|
||||
import { ProjectileView } from './scripts/objects/projectile-view';
|
||||
import { PlanetView } from './scripts/objects/planet-view';
|
||||
|
|
@ -33,7 +31,6 @@ import { SoundHandler, Sounds } from './scripts/sound-handler';
|
|||
|
||||
glMatrix.setMatrixArrayType(Array);
|
||||
|
||||
overrideDeserialization(CharacterBase, CharacterView);
|
||||
overrideDeserialization(PlayerCharacterBase, PlayerCharacterView);
|
||||
overrideDeserialization(PlanetBase, PlanetView);
|
||||
overrideDeserialization(LampBase, LampView);
|
||||
|
|
|
|||
|
|
@ -115,7 +115,7 @@ body {
|
|||
|
||||
.player-tag {
|
||||
transform: translateX(-50%) translateY(-50%) rotate(-15deg);
|
||||
transition: left 200ms, top 200ms;
|
||||
transition: left 150ms, top 150ms;
|
||||
border-radius: 1000px;
|
||||
|
||||
&.decla {
|
||||
|
|
|
|||
|
|
@ -155,7 +155,12 @@ export class Game extends CommandReceiver {
|
|||
const angle = Math.atan2(direction.y, direction.x);
|
||||
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 directionRatio = direction.x / direction.y;
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -12,7 +12,6 @@ export class PlayerCharacterView extends PlayerCharacterBase implements ViewObje
|
|||
private nameElement: HTMLElement = document.createElement('div');
|
||||
private statsElement: HTMLElement = document.createElement('div');
|
||||
private healthElement: HTMLElement = document.createElement('div');
|
||||
private timeSinceLastNameElementUpdate = 0;
|
||||
private previousHealth;
|
||||
|
||||
constructor(
|
||||
|
|
@ -50,7 +49,6 @@ export class PlayerCharacterView extends PlayerCharacterBase implements ViewObje
|
|||
}
|
||||
|
||||
public step(deltaTimeInSeconds: number): void {
|
||||
this.timeSinceLastNameElementUpdate += deltaTimeInSeconds;
|
||||
this.healthElement.style.width = (50 * this.health) / settings.playerMaxHealth + 'px';
|
||||
this.statsElement.innerText = this.getStatsText();
|
||||
if (this.previousHealth > this.health) {
|
||||
|
|
@ -75,14 +73,11 @@ export class PlayerCharacterView extends PlayerCharacterBase implements ViewObje
|
|||
overlay.appendChild(this.nameElement);
|
||||
}
|
||||
|
||||
if (this.timeSinceLastNameElementUpdate > 0.15) {
|
||||
const screenPosition = renderer.worldToDisplayCoordinates(
|
||||
this.calculateTextPosition(),
|
||||
);
|
||||
this.nameElement.style.left = screenPosition.x + 'px';
|
||||
this.nameElement.style.top = screenPosition.y + 'px';
|
||||
this.timeSinceLastNameElementUpdate = 0;
|
||||
}
|
||||
const screenPosition = renderer.worldToDisplayCoordinates(
|
||||
this.calculateTextPosition(),
|
||||
);
|
||||
this.nameElement.style.left = screenPosition.x + 'px';
|
||||
this.nameElement.style.top = screenPosition.y + 'px';
|
||||
|
||||
this.shape.setCircles([this.head!, this.leftFoot!, this.rightFoot!]);
|
||||
renderer.addDrawable(this.shape);
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@ export abstract class SoundHandler {
|
|||
setTimeout(() => {
|
||||
this.ambientSound.muted = false;
|
||||
this.ambientSound.volume = 0.5;
|
||||
this.ambientSound.loop = true;
|
||||
if (!this.isAmbientPlaying) {
|
||||
this.ambientSound.pause();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -44,7 +44,6 @@ export * from './transport/serialization/serialize';
|
|||
export * from './transport/serialization/serializes-to';
|
||||
export * from './transport/serialization/serializable';
|
||||
export * from './transport/serialization/override-deserialization';
|
||||
export * from './objects/types/character-base';
|
||||
export * from './objects/types/character-team';
|
||||
export * from './objects/types/player-character-base';
|
||||
export * from './objects/types/lamp-base';
|
||||
|
|
|
|||
|
|
@ -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];
|
||||
}
|
||||
}
|
||||
|
|
@ -1,23 +1,23 @@
|
|||
import { CharacterBase } from './character-base';
|
||||
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 PlayerCharacterBase extends CharacterBase {
|
||||
export class PlayerCharacterBase extends GameObject {
|
||||
constructor(
|
||||
id: Id,
|
||||
public name: string,
|
||||
public killCount: number,
|
||||
public deathCount: number,
|
||||
team: CharacterTeam,
|
||||
health: number,
|
||||
head?: Circle,
|
||||
leftFoot?: Circle,
|
||||
rightFoot?: Circle,
|
||||
public team: CharacterTeam,
|
||||
public health: number,
|
||||
public head?: Circle,
|
||||
public leftFoot?: Circle,
|
||||
public rightFoot?: Circle,
|
||||
) {
|
||||
super(id, team, health, head, leftFoot, rightFoot);
|
||||
super(id);
|
||||
}
|
||||
|
||||
public onShoot(strength: number) {}
|
||||
|
|
|
|||
|
|
@ -44,6 +44,64 @@ export const settings = {
|
|||
backgroundGradient: [rgb255(90, 38, 43), rgb255(43, 39, 73)],
|
||||
declaColor,
|
||||
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,
|
||||
redPlanetColor,
|
||||
colorIndices: {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue