decla-red/backend/src/players/player-base.ts

104 lines
3.2 KiB
TypeScript

import { vec2 } from 'gl-matrix';
import {
CommandReceiver,
Circle,
PlayerInformation,
CharacterTeam,
Random,
settings,
} 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 { CharacterPhysical } from '../objects/character-physical';
import { PlanetPhysical } from '../objects/planet-physical';
import { PlayerContainer } from './player-container';
export abstract class PlayerBase extends CommandReceiver {
public character?: CharacterPhysical | 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 CharacterPhysical(
this.playerInfo.name.slice(0, 20),
this.sumKills,
this.sumDeaths,
this.team,
this.objectContainer,
this.findEmptyPositionForPlayer(this.findSpawnCenter()),
);
this.objectContainer.addObject(this.character);
}
public abstract step(deltaTimeInSeconds: number): void;
private findSpawnCenter(): vec2 {
const planets = this.objectContainer
.findIntersecting(
getBoundingBoxOfCircle(new Circle(vec2.create(), settings.worldRadius * 2)),
)
.filter((o): o is PlanetPhysical => o instanceof PlanetPhysical);
const friendly = planets.filter((p) => p.team === this.team);
const neutral = planets.filter((p) => p.team === CharacterTeam.neutral);
const candidates = friendly.length ? friendly : neutral.length ? neutral : planets;
if (candidates.length === 0) {
return vec2.create();
}
const isContested = (planet: PlanetPhysical) =>
this.playerContainer.players.some(
(p) =>
p.team !== this.team &&
p.character?.isAlive &&
vec2.distance(p.center, planet.center) < settings.spawnSafetyDistance,
);
const safe = candidates.filter((p) => !isContested(p));
// candidates is non-empty here, so choose() always returns a planet.
return vec2.clone(Random.choose(safe.length ? safe : candidates)!.center);
}
protected findEmptyPositionForPlayer(preferredCenter: vec2): vec2 {
let rotation = 0;
let radius = 0;
for (; ;) {
const playerPosition = vec2.fromValues(
radius * Math.cos(rotation) + preferredCenter.x,
radius * Math.sin(rotation) + preferredCenter.y,
);
const playerBoundingCircle = new Circle(
playerPosition,
CharacterPhysical.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?.onDie();
}
}