// 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, 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 private staged: Set; private ready: Set private numRobots: number; private numRunsLeft: number; private resolvePromise?: Function; private winner: string; private width: number; private height: number; private bullets: Array 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 { 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 = []; const bulletPos: Array> = []; 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 = []; 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 }