269 lines
7.6 KiB
TypeScript
269 lines
7.6 KiB
TypeScript
![]() |
// SPDX-License-Identifier: GPL-3.0-or-later
|
||
|
// Author: Sascha Nitsch https://contentnation.net/@grumpydevelop
|
||
|
|
||
|
import { MonitorWS } from "./websockets/monitor";
|
||
|
import { RobotWS } from "./websockets/robot";
|
||
|
import { Robot } from "./robot";
|
||
|
import { RobotCmd } from "./robotcmd";
|
||
|
|
||
|
class Bullet {
|
||
|
private source: string;
|
||
|
public x: number;
|
||
|
public y: number;
|
||
|
private direction: number
|
||
|
private closest: number;
|
||
|
constructor(uuid: string, x: number, y: number, direction: number) {
|
||
|
this.source = uuid;
|
||
|
this.x = x;
|
||
|
this.y = y;
|
||
|
this.direction = direction * Math.PI / 180;
|
||
|
this.closest = 100;
|
||
|
}
|
||
|
advance(dt: number, robots: Map<String, Robot>, width: number, height: number) {
|
||
|
this.x += 100*Math.sin(this.direction) * dt;
|
||
|
this.y -= 100*Math.cos(this.direction) * dt;
|
||
|
if (this.x > width || this.x < 0 || this.y > height || this.y < 0) {
|
||
|
return true;
|
||
|
}
|
||
|
let damage = false;
|
||
|
robots.forEach((robot, uuid) => {
|
||
|
if (uuid === this.source) {
|
||
|
return;
|
||
|
}
|
||
|
const rX = robot.getX();
|
||
|
const rY = robot.getY();
|
||
|
if (Math.abs(rX - this.x) < 20 && Math.abs(rY - this.y) < 20) { // quick check
|
||
|
const dist = Math.sqrt((rX - this.x)*(rX - this.x) + (rY - this.y) * (rY - this.y));
|
||
|
//console.log("potential hit on", uuid, "from ",this.source, dist);
|
||
|
if (dist<10) {
|
||
|
if (dist > this.closest) {
|
||
|
const damageAmount = Math.pow(1.6, 10-this.closest)
|
||
|
robot.damage(damageAmount);
|
||
|
const sourceRobot = robots.get(this.source);
|
||
|
sourceRobot?.causedDamage(damageAmount);
|
||
|
damage = true;
|
||
|
} else {
|
||
|
this.closest = dist;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
});
|
||
|
return damage;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
class WorldSim {
|
||
|
private monitors: MonitorWS;
|
||
|
private simulationTime: number;
|
||
|
private worldTickTimer: NodeJS.Timeout;
|
||
|
private robotsWS: RobotWS;
|
||
|
public robots: Map<string, Robot>
|
||
|
private staged: Set<string>;
|
||
|
private ready: Set<string>
|
||
|
private numRobots: number;
|
||
|
private numRunsLeft: number;
|
||
|
private resolvePromise?: Function;
|
||
|
private winner: string;
|
||
|
private width: number;
|
||
|
private height: number;
|
||
|
private bullets: Array<Bullet>
|
||
|
|
||
|
constructor() {
|
||
|
this.simulationTime = 0;
|
||
|
this.robots = new Map();
|
||
|
this.staged = new Set();
|
||
|
this.ready = new Set();
|
||
|
this.numRobots = 0;
|
||
|
this.numRunsLeft = 99999;
|
||
|
this.winner = "";
|
||
|
this.width = 500;
|
||
|
this.height = 500;
|
||
|
this.bullets = [];
|
||
|
}
|
||
|
|
||
|
cmd(uuid: string, cmd: RobotCmd) {
|
||
|
const robot = this.robots.get(uuid);
|
||
|
if (robot) {
|
||
|
robot.updateCmd(cmd);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
fire(uuid: string, posX, posY, direction) : void {
|
||
|
const bullet = new Bullet(uuid, posX, posY, direction);
|
||
|
this.bullets.push(bullet);
|
||
|
}
|
||
|
|
||
|
getTime() : number {
|
||
|
return this.simulationTime;
|
||
|
}
|
||
|
register(uuid: string) {
|
||
|
const robot = new Robot(this, uuid, this.width, this.height);
|
||
|
this.robots.set(uuid, robot);
|
||
|
this.monitors.broadcast("connected " + uuid);
|
||
|
this.staged.add(uuid);
|
||
|
}
|
||
|
|
||
|
robotReady(uuid: string) {
|
||
|
this.monitors.broadcast("ready " + uuid);
|
||
|
this.robotsWS.send(uuid, "ready");
|
||
|
this.staged.delete(uuid);
|
||
|
this.ready.add(uuid);
|
||
|
if (this.staged.size == 0 && this.ready.size == this.numRobots) {
|
||
|
--this.numRunsLeft;
|
||
|
this.monitors.broadcast("start");
|
||
|
this.ready.forEach((uuid: string) => {
|
||
|
this.robotsWS.send(uuid, "start");
|
||
|
});
|
||
|
this.worldTickTimer = setInterval(this.worldTick, 50);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
setModel(uuid: string, model: string){
|
||
|
const robot = this.robots.get(uuid);
|
||
|
if (robot === undefined) {
|
||
|
return;
|
||
|
}
|
||
|
robot.setModel(model);
|
||
|
this.monitors.broadcast("model " + uuid + " " + model);
|
||
|
this.robotsWS.send(uuid, "specs " + JSON.stringify({width: this.width, height: this.height}));
|
||
|
const status = robot.getStatus();
|
||
|
const statusString = status.toJSON();
|
||
|
this.robotsWS.send(uuid, "status " + statusString);
|
||
|
this.monitors.broadcast('status ' + uuid + ' ' + statusString);
|
||
|
if(robot.isReady()) {
|
||
|
this.robotReady(uuid);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
setMonitors(monitors: MonitorWS) {
|
||
|
this.monitors = monitors;
|
||
|
}
|
||
|
|
||
|
setName(uuid: string, name: string) {
|
||
|
//console.log("set name for ", uuid, "to", name);
|
||
|
const robot = this.robots.get(uuid);
|
||
|
if (robot === undefined) {
|
||
|
return;
|
||
|
}
|
||
|
robot.setName(name);
|
||
|
this.monitors.broadcast("name " + uuid + " " + name);
|
||
|
|
||
|
if(robot.isReady()) {
|
||
|
this.robotReady(uuid);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
setRobots(robots: RobotWS) {
|
||
|
this.robotsWS = robots;
|
||
|
}
|
||
|
|
||
|
start(numRobots: number) : Promise<string> {
|
||
|
this.numRobots = numRobots;
|
||
|
return new Promise((resolve) => {
|
||
|
this.resolvePromise = resolve;
|
||
|
});
|
||
|
}
|
||
|
|
||
|
unregister(uuid: string) {
|
||
|
this.monitors.broadcast("disconnected " + uuid);
|
||
|
this.robots.delete(uuid);
|
||
|
this.ready.delete(uuid);
|
||
|
this.staged.delete(uuid);
|
||
|
if (this.numRunsLeft == 0 && this.staged.size == 0 && this.ready.size == 0) {
|
||
|
console.log("shut down");
|
||
|
if (this.resolvePromise) {
|
||
|
this.resolvePromise(this.winner);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
lastTick = 0;
|
||
|
worldTick = () => {
|
||
|
const now = Date.now();
|
||
|
const dt = this.lastTick ? ((now - this.lastTick) / 1000) : 0.1;
|
||
|
this.lastTick = now;
|
||
|
this.simulationTime += dt;
|
||
|
|
||
|
this.ready.forEach((uuid: string) => {
|
||
|
const robot = this.robots.get(uuid);
|
||
|
robot?.setSimulationTime(this.simulationTime);
|
||
|
robot?.causedDamage(0);
|
||
|
});
|
||
|
// update bullets and check for hits
|
||
|
// run twice with half dt
|
||
|
for (let j = 0; j < 2; ++j) {
|
||
|
const toDelete:Array<number> = [];
|
||
|
const bulletPos: Array<Array<number>> = [];
|
||
|
this.bullets.forEach((bullet, i) => {
|
||
|
if (bullet.advance(dt / 2, this.robots, this.width, this.height)) {
|
||
|
toDelete.push(i);
|
||
|
} else {
|
||
|
bulletPos.push([bullet.x, bullet.y]);
|
||
|
}
|
||
|
});
|
||
|
if (toDelete.length) {
|
||
|
toDelete.reverse();
|
||
|
toDelete.forEach((index) => {
|
||
|
this.bullets.splice(index, 1);
|
||
|
});
|
||
|
}
|
||
|
if (this.bullets.length > 0 || toDelete.length > 0) {
|
||
|
this.monitors.broadcast('bullet ' + JSON.stringify(bulletPos));
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// move robots according to "physics"
|
||
|
this.ready.forEach((uuid: string) => {
|
||
|
const robot = this.robots.get(uuid);
|
||
|
robot?.moveRobot(dt);
|
||
|
});
|
||
|
|
||
|
// update radar
|
||
|
this.ready.forEach((uuid: string) => {
|
||
|
const robot = this.robots.get(uuid);
|
||
|
robot?.updateRadar();
|
||
|
});
|
||
|
|
||
|
|
||
|
const killed: Array<string> = [];
|
||
|
this.ready.forEach((uuid: string) => {
|
||
|
const robot = this.robots.get(uuid);
|
||
|
if (robot !== undefined) {
|
||
|
const status = robot.getStatus();
|
||
|
if (status.health <= 0) {
|
||
|
// robot got killed
|
||
|
killed.push(uuid);
|
||
|
} else {
|
||
|
const statusString = status.toJSON();
|
||
|
/*if (status.contactPoints.length > 0) {
|
||
|
console.log(statusString);
|
||
|
}*/
|
||
|
this.robotsWS.send(uuid, "status " + statusString);
|
||
|
this.monitors.broadcast('status ' + uuid + ' ' + statusString);
|
||
|
}
|
||
|
}
|
||
|
});
|
||
|
|
||
|
killed.forEach((uuid) => {
|
||
|
this.monitors.broadcast("lost " + uuid);
|
||
|
this.robotsWS.send(uuid, "lost");
|
||
|
this.staged.add(uuid);
|
||
|
this.ready.delete(uuid);
|
||
|
});
|
||
|
|
||
|
if (this.ready.size == 1) {
|
||
|
this.ready.forEach((uuid) => {
|
||
|
this.monitors.broadcast("won " + uuid);
|
||
|
this.robotsWS.send(uuid, "won");
|
||
|
this.staged.add(uuid);
|
||
|
this.ready.delete(uuid);
|
||
|
this.winner = uuid;
|
||
|
});
|
||
|
clearInterval(this.worldTickTimer);
|
||
|
}
|
||
|
if (this.ready.size == 0) {
|
||
|
clearInterval(this.worldTickTimer);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
export { WorldSim }
|