Add npcs
This commit is contained in:
parent
a66fa63b4b
commit
efa838a2ad
4 changed files with 1688 additions and 365 deletions
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(),
|
||||
);
|
||||
public measureLatency() {
|
||||
this.pingTime = getTimeInMilliseconds();
|
||||
this.socket.emit(TransportEvents.Ping);
|
||||
if (this.isActive) {
|
||||
setTimeout(this.measureLatency.bind(this), 10000);
|
||||
}
|
||||
}
|
||||
|
||||
this.objectContainer.addObject(this.character);
|
||||
this.objectsPreviouslyInViewArea.push(this.character);
|
||||
public get latency(): number | undefined {
|
||||
return this._latency;
|
||||
}
|
||||
|
||||
this.socket.emit(
|
||||
TransportEvents.ServerToPlayer,
|
||||
serialize(new CreatePlayerCommand(this.character)),
|
||||
);
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue