initial im
This commit is contained in:
commit
51f0415617
23 changed files with 1512 additions and 0 deletions
3
client-hodejs/.gitignore
vendored
Normal file
3
client-hodejs/.gitignore
vendored
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
node_modules
|
||||||
|
package-lock.json
|
||||||
|
*.js
|
13
client-hodejs/esbuild.mjs
Normal file
13
client-hodejs/esbuild.mjs
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
// Author: Sascha Nitsch https://contentnation.net/@grumpydevelop
|
||||||
|
|
||||||
|
import * as esbuild from 'esbuild'
|
||||||
|
|
||||||
|
await esbuild.build({
|
||||||
|
entryPoints: ['rabbit.ts', 'targetpractice.ts', 'simplehunter.ts'],
|
||||||
|
bundle: true,
|
||||||
|
platform: 'node',
|
||||||
|
target: ['node22'],
|
||||||
|
packages: 'external',
|
||||||
|
outdir: './'
|
||||||
|
})
|
21
client-hodejs/package.json
Normal file
21
client-hodejs/package.json
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
{
|
||||||
|
"name": "robotank-base",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"description": "base robot functions for the robotank simulation",
|
||||||
|
"main": "index.js",
|
||||||
|
"scripts": {
|
||||||
|
"test": "echo \"Error: no test specified\" && exit 1",
|
||||||
|
"build": "node esbuild.mjs"
|
||||||
|
},
|
||||||
|
"author": "",
|
||||||
|
"license": "GPL-3.0-or-later",
|
||||||
|
"dependencies": {
|
||||||
|
"ws": "^8.18.1"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"bufferutil": "^4.0.9"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"esbuild": "^0.25.0"
|
||||||
|
}
|
||||||
|
}
|
134
client-hodejs/rabbit.ts
Normal file
134
client-hodejs/rabbit.ts
Normal file
|
@ -0,0 +1,134 @@
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
// Author: Sascha Nitsch https://contentnation.net/@grumpydevelop
|
||||||
|
|
||||||
|
import { RobotStatus } from './robotstatus';
|
||||||
|
import { RoboBase } from './robobase';
|
||||||
|
|
||||||
|
class Rabbit extends RoboBase {
|
||||||
|
carrotX: number = 0;
|
||||||
|
carrotY: number = 0;
|
||||||
|
running = false;
|
||||||
|
mode = 'MUNCH';
|
||||||
|
munchtime = 0;
|
||||||
|
ready() {
|
||||||
|
console.log('rabbit is ready');
|
||||||
|
}
|
||||||
|
|
||||||
|
start() {
|
||||||
|
console.log('simulation started');
|
||||||
|
this.cmd.powerRight = 0;
|
||||||
|
this.cmd.powerLeft = 0;
|
||||||
|
this.cmd.radarMin = 270;
|
||||||
|
this.cmd.radarMax = 90;
|
||||||
|
this.sendCmd();
|
||||||
|
this.running = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
statusReady(status: RobotStatus) {
|
||||||
|
if (!this.running) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
switch (this.mode) {
|
||||||
|
case 'RUN':
|
||||||
|
const dx = this.carrotX - status.posX;
|
||||||
|
const dy = this.carrotY - status.posY;
|
||||||
|
const dir = Math.atan2(dx, -dy) * 180 / Math.PI;
|
||||||
|
const dst = Math.sqrt(dx * dx + dy * dy);
|
||||||
|
let rot = dir - status.orientation;
|
||||||
|
if (rot < -180) {
|
||||||
|
rot += 360;
|
||||||
|
}
|
||||||
|
if (rot > 180) {
|
||||||
|
rot -= 360;
|
||||||
|
}
|
||||||
|
rot = Math.min(Math.max(-90, rot), 90);
|
||||||
|
let desiredSpeed = 1;
|
||||||
|
if (dst < 30) {
|
||||||
|
desiredSpeed = dst / 40;
|
||||||
|
}
|
||||||
|
const speedDiff = Math.sin(rot * Math.PI / 180.0);
|
||||||
|
// desired speed of left chain
|
||||||
|
let dsl = desiredSpeed + speedDiff;
|
||||||
|
// desired speed of right chain
|
||||||
|
let dsr = desiredSpeed - speedDiff;
|
||||||
|
// prevent overflows, reduce to limits but keep relative difference if possible
|
||||||
|
if (dsl > 1) {
|
||||||
|
dsr -= Math.max(dsl - 1, -1);
|
||||||
|
dsl = 1;
|
||||||
|
} else if (dsr > 1) {
|
||||||
|
dsl -= Math.max(dsr - 1, -1);
|
||||||
|
dsr = 1;
|
||||||
|
}
|
||||||
|
this.cmd.powerLeft = dsl;
|
||||||
|
this.cmd.powerRight = dsr;
|
||||||
|
// close and slow enough?
|
||||||
|
if (dst < 5 && Math.abs(status.chainSpeedLeft) < .1 && Math.abs(status.chainSpeedRight) < .1) {
|
||||||
|
this.mode = 'MUNCH';
|
||||||
|
this.cmd.powerLeft = 0;
|
||||||
|
this.cmd.powerRight = 0;
|
||||||
|
this.munchtime = 5 * 20; // 5 sec
|
||||||
|
console.log("munch");
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "MUNCH":
|
||||||
|
if (this.munchtime) {
|
||||||
|
--this.munchtime;
|
||||||
|
} else {
|
||||||
|
this.mode = 'RUN';
|
||||||
|
this.carrotX = Math.random() * (this.width - 20) + 10;
|
||||||
|
this.carrotY = Math.random() * (this.height - 20) + 10;
|
||||||
|
console.log("target reached, new target", this.carrotX, this.carrotY);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (status.contactPoints && status.contactPoints.length > 0) {
|
||||||
|
status.contactPoints.forEach((cp) => {
|
||||||
|
// adjust angle by radar rotation
|
||||||
|
const angle = cp.angle + status.radarPos;
|
||||||
|
// global angle from position
|
||||||
|
const globalAngle = angle + status.orientation;
|
||||||
|
const targetX = Math.sin(globalAngle * Math.PI / 180)* cp.dist + status.posX;
|
||||||
|
const targetY = -Math.cos(globalAngle * Math.PI / 180)* cp.dist + status.posY;
|
||||||
|
// run the other way +- 45 degree
|
||||||
|
let newTargetAngle = globalAngle + 180 + Math.random()*90 - 45
|
||||||
|
if (newTargetAngle >= 360) {
|
||||||
|
newTargetAngle -= 360;
|
||||||
|
}
|
||||||
|
if (newTargetAngle < 0) {
|
||||||
|
newTargetAngle += 360;
|
||||||
|
}
|
||||||
|
// target is 50-100 units away
|
||||||
|
const newTargetDistance = Math.random() * 50 + 50;
|
||||||
|
this.carrotX = status.posX + Math.sin(newTargetAngle * Math.PI / 180) * newTargetDistance;
|
||||||
|
this.carrotY = status.posY - Math.cos(newTargetAngle * Math.PI / 180) * newTargetDistance;
|
||||||
|
if (this.carrotX > this.width - 10) {
|
||||||
|
this.carrotX = this.width - 10;
|
||||||
|
}
|
||||||
|
if (this.carrotY > this.height - 10) {
|
||||||
|
this.carrotY = this.height - 10;
|
||||||
|
}
|
||||||
|
if (this.carrotX < 0) {
|
||||||
|
this.carrotX = 10;
|
||||||
|
}
|
||||||
|
if (this.carrotY < 0) {
|
||||||
|
this.carrotY = 10;
|
||||||
|
}
|
||||||
|
console.log("enemy at", targetX, targetY, "run to", this.carrotX, this.carrotY);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
this.sendCmd();
|
||||||
|
}
|
||||||
|
|
||||||
|
lost() {
|
||||||
|
console.log('we lost');
|
||||||
|
this.disconnect();
|
||||||
|
}
|
||||||
|
|
||||||
|
won() {
|
||||||
|
console.log('we won');
|
||||||
|
this.disconnect();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var rabbit = new Rabbit('rabbit', 'scout');
|
||||||
|
rabbit.connect();
|
120
client-hodejs/robobase.ts
Normal file
120
client-hodejs/robobase.ts
Normal file
|
@ -0,0 +1,120 @@
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
// Author: Sascha Nitsch https://contentnation.net/@grumpydevelop
|
||||||
|
|
||||||
|
import WebSocket from 'ws';
|
||||||
|
import { RobotCmd } from './robotcmd';
|
||||||
|
import { RobotStatus } from './robotstatus';
|
||||||
|
|
||||||
|
abstract class RoboBase {
|
||||||
|
private pingTimeout: NodeJS.Timeout;
|
||||||
|
private socket: WebSocket;
|
||||||
|
private name: string;
|
||||||
|
private model: string;
|
||||||
|
protected cmd: RobotCmd;
|
||||||
|
protected width: number;
|
||||||
|
protected height: number;
|
||||||
|
private lastUpdate: number;
|
||||||
|
protected deltaTime: number;
|
||||||
|
|
||||||
|
constructor(name: string, model:string) {
|
||||||
|
this.name = name;
|
||||||
|
this.model = model;
|
||||||
|
this.cmd = new RobotCmd();
|
||||||
|
this.width = 0;
|
||||||
|
this.height = 0;
|
||||||
|
this.lastUpdate = 0;
|
||||||
|
this.deltaTime = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
connect() {
|
||||||
|
this.socket = new WebSocket('ws://127.0.0.1:3000/robot', undefined, {
|
||||||
|
perMessageDeflate: false,
|
||||||
|
autoPong: true
|
||||||
|
});
|
||||||
|
this.socket.addEventListener('error', this.error);
|
||||||
|
this.socket.addEventListener('open', () => {
|
||||||
|
this.open();
|
||||||
|
this.heartbeat();
|
||||||
|
});
|
||||||
|
this.socket.addEventListener('ping', this.heartbeat);
|
||||||
|
this.socket.addEventListener('message', this.message);
|
||||||
|
this.socket.addEventListener('close', () => {
|
||||||
|
clearTimeout(this.pingTimeout);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnect() {
|
||||||
|
this.socket.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
error(...param) {
|
||||||
|
console.error(param);
|
||||||
|
}
|
||||||
|
|
||||||
|
private heartbeat = () => {
|
||||||
|
clearTimeout(this.pingTimeout);
|
||||||
|
this.pingTimeout = setTimeout(() => {
|
||||||
|
this.socket.terminate();
|
||||||
|
}, 30000 + 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
message = (event: MessageEvent) => {
|
||||||
|
this.heartbeat();
|
||||||
|
const msgtext = event.data.toString();
|
||||||
|
const tokens = msgtext.split(' ');
|
||||||
|
if (tokens.length == 1) {
|
||||||
|
switch (tokens[0]) {
|
||||||
|
case 'lost':
|
||||||
|
this.lost();
|
||||||
|
break;
|
||||||
|
case 'ready':
|
||||||
|
this.ready();
|
||||||
|
break;
|
||||||
|
case 'start':
|
||||||
|
this.start();
|
||||||
|
break;
|
||||||
|
case 'won':
|
||||||
|
this.won();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
console.log('msg', tokens[0])
|
||||||
|
}
|
||||||
|
} else if (tokens.length == 2) {
|
||||||
|
switch (tokens[0]) {
|
||||||
|
case 'specs':
|
||||||
|
const specs = JSON.parse(tokens[1]);
|
||||||
|
this.width = specs['width'];
|
||||||
|
this.height = specs['height'];
|
||||||
|
break;
|
||||||
|
case 'status':
|
||||||
|
const status:RobotStatus = JSON.parse(tokens[1]);
|
||||||
|
this.deltaTime = status.simulationTime - this.lastUpdate;
|
||||||
|
this.lastUpdate = status.simulationTime;
|
||||||
|
this.statusReady(status);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
console.log('unknown command tk2 ', msgtext);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log('unknown command tk3', msgtext);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
open() {
|
||||||
|
this.socket.send('name ' + this.name);
|
||||||
|
this.socket.send('model ' + this.model);
|
||||||
|
}
|
||||||
|
|
||||||
|
sendCmd() {
|
||||||
|
this.socket.send('cmd ' + JSON.stringify(this.cmd.toObject()));
|
||||||
|
this.cmd.fire = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract lost(): void;
|
||||||
|
abstract ready(): void;
|
||||||
|
abstract start(): void;
|
||||||
|
abstract statusReady(status: RobotStatus): void;
|
||||||
|
abstract won(): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export { RoboBase }
|
1
client-hodejs/robotcmd.ts
Symbolic link
1
client-hodejs/robotcmd.ts
Symbolic link
|
@ -0,0 +1 @@
|
||||||
|
../server-nodejs/robotcmd.ts
|
1
client-hodejs/robotstatus.ts
Symbolic link
1
client-hodejs/robotstatus.ts
Symbolic link
|
@ -0,0 +1 @@
|
||||||
|
../server-nodejs/robotstatus.ts
|
132
client-hodejs/simplehunter.ts
Normal file
132
client-hodejs/simplehunter.ts
Normal file
|
@ -0,0 +1,132 @@
|
||||||
|
import { RobotStatus } from "./robotstatus";
|
||||||
|
import { RoboBase } from "./robobase";
|
||||||
|
|
||||||
|
class SimpleHunter extends RoboBase {
|
||||||
|
targetX: number = 0;
|
||||||
|
targetY: number = 0;
|
||||||
|
fireCoolDown: number = 0;
|
||||||
|
mode: string = "SCAN";
|
||||||
|
ready() {
|
||||||
|
console.log("simpleHunter is ready");
|
||||||
|
this.cmd.radarMin = -1;
|
||||||
|
this.cmd.radarMax = -1;
|
||||||
|
this.sendCmd();
|
||||||
|
}
|
||||||
|
start() {
|
||||||
|
console.log("simulation started");
|
||||||
|
this.targetX = Math.random() * (this.width - 20) + 10;
|
||||||
|
this.targetY = Math.random() * (this.height - 20) + 10;
|
||||||
|
console.log("target", this.targetX, this.targetY);
|
||||||
|
}
|
||||||
|
driveToTarget(status: RobotStatus) {
|
||||||
|
const dx = this.targetX - status.posX;
|
||||||
|
const dy = this.targetY - status.posY;
|
||||||
|
const dir = Math.atan2(dx, -dy) * 180 / Math.PI;
|
||||||
|
const dst = Math.sqrt(dx * dx + dy * dy);
|
||||||
|
let rot = dir - status.orientation;
|
||||||
|
if (rot < -180) {
|
||||||
|
rot += 360;
|
||||||
|
}
|
||||||
|
if (rot > 180) {
|
||||||
|
rot -= 360;
|
||||||
|
}
|
||||||
|
rot = Math.min(Math.max(-90, rot), 90);
|
||||||
|
let desiredSpeed = 1;
|
||||||
|
if (this.mode === 'SCAN') {
|
||||||
|
if (dst < 30) {
|
||||||
|
desiredSpeed = dst / 40;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (dst < 75) {
|
||||||
|
desiredSpeed = dst - 50 / 25;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const speedDiff = Math.sin(rot * Math.PI / 180.0);
|
||||||
|
// desired speed of left chain
|
||||||
|
let dsl = desiredSpeed + speedDiff;
|
||||||
|
// desired speed of right chain
|
||||||
|
let dsr = desiredSpeed - speedDiff;
|
||||||
|
// prevent overflows, reduce to limits but keep relative difference if possible
|
||||||
|
if (dsl > 1) {
|
||||||
|
dsr -= Math.max(dsl - 1, -1);
|
||||||
|
dsl = 1;
|
||||||
|
} else if (dsr > 1) {
|
||||||
|
dsl -= Math.max(dsr - 1, -1);
|
||||||
|
dsr = 1;
|
||||||
|
}
|
||||||
|
this.cmd.powerLeft = dsl;
|
||||||
|
this.cmd.powerRight = dsr;
|
||||||
|
// close and slow enough?
|
||||||
|
if (dst < 5 && Math.abs(status.chainSpeedLeft) < .1 && Math.abs(status.chainSpeedRight) < .1 && this.mode === 'SCAN') {
|
||||||
|
this.targetX = Math.random() * (this.width - 20) + 10;
|
||||||
|
this.targetY = Math.random() * (this.height - 20) + 10;
|
||||||
|
console.log("target reached, new target", this.targetX, this.targetY);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
statusReady(status: RobotStatus) {
|
||||||
|
this.driveToTarget(status);
|
||||||
|
if (this.fireCoolDown > 0) {
|
||||||
|
this.fireCoolDown -= this.deltaTime;
|
||||||
|
}
|
||||||
|
if (status.contactPoints && status.contactPoints.length > 0) {
|
||||||
|
status.contactPoints.forEach((cp) => {
|
||||||
|
// adjust angle by radar rotation
|
||||||
|
const angle = (cp.angle + status.radarPos) % 360;
|
||||||
|
// global angle from position
|
||||||
|
let newTargetAngle = angle + status.orientation;
|
||||||
|
if (newTargetAngle >= 360) {
|
||||||
|
newTargetAngle -= 360;
|
||||||
|
}
|
||||||
|
if (newTargetAngle < 0) {
|
||||||
|
newTargetAngle += 360;
|
||||||
|
}
|
||||||
|
this.targetX = Math.sin(newTargetAngle * Math.PI / 180)* (cp.dist - 50) + status.posX;
|
||||||
|
this.targetY = -Math.cos(newTargetAngle * Math.PI / 180)* (cp.dist - 50) + status.posY;
|
||||||
|
this.cmd.gunTarget = angle;
|
||||||
|
console.log("enemy at", this.targetX, this.targetY, "dist", cp.dist, "angle", angle);
|
||||||
|
this.mode = 'SEEK';
|
||||||
|
this.cmd.radarMin = angle - 20;
|
||||||
|
this.cmd.radarMax = angle + 20;
|
||||||
|
// shoot if aimed close enough
|
||||||
|
if (cp.dist < 100 && cp.dist > 25) {
|
||||||
|
console.log("diff", angle, status.gunOrient, Math.abs((angle-status.gunOrient)%360));
|
||||||
|
if (this.fireCoolDown <= 0 && (Math.abs(angle-status.gunOrient) < 2)) {
|
||||||
|
this.cmd.fire = true;
|
||||||
|
console.log("fire", cp.dist);
|
||||||
|
this.fireCoolDown = 10;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
if (this.mode === 'SEEK') { // lost track
|
||||||
|
this.cmd.radarMin = (this.cmd.radarMin + 359.5) % 360;
|
||||||
|
this.cmd.radarMax = (this.cmd.radarMax + .5) % 360;
|
||||||
|
//console.log("seek radar", this.cmd.radarMin, this.cmd.radarMax);
|
||||||
|
if (this.cmd.radarMax - this.cmd.radarMin > 180) {
|
||||||
|
this.cmd.radarMin = -1;
|
||||||
|
this.cmd.radarMax = -1;
|
||||||
|
this.mode = 'SCAN';
|
||||||
|
console.log("scan");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (status.causedDamage > 0) {
|
||||||
|
console.log("we hit with damage", status.causedDamage);
|
||||||
|
}
|
||||||
|
this.sendCmd();
|
||||||
|
}
|
||||||
|
|
||||||
|
lost() {
|
||||||
|
console.log("we lost");
|
||||||
|
this.disconnect();
|
||||||
|
}
|
||||||
|
won() {
|
||||||
|
console.log("we won");
|
||||||
|
this.disconnect();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
var simpleHunter = new SimpleHunter("targetPractise", "scout");
|
||||||
|
simpleHunter.connect();
|
39
client-hodejs/targetpractice.ts
Normal file
39
client-hodejs/targetpractice.ts
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
import { RobotStatus } from "./robotstatus";
|
||||||
|
import { RoboBase } from "./robobase";
|
||||||
|
|
||||||
|
class TargetPractice extends RoboBase {
|
||||||
|
lastHealth = 100;
|
||||||
|
ready() {
|
||||||
|
console.log("targetPractice is ready");
|
||||||
|
this.cmd.radarMin = -1;
|
||||||
|
this.cmd.radarMax = -1;
|
||||||
|
this.cmd.gunTarget = 0;
|
||||||
|
this.sendCmd();
|
||||||
|
}
|
||||||
|
start() {
|
||||||
|
console.log("simulation started");
|
||||||
|
this.cmd.radarMin = -1;
|
||||||
|
this.cmd.radarMax = -1;
|
||||||
|
this.sendCmd();
|
||||||
|
}
|
||||||
|
statusReady(status: RobotStatus) {
|
||||||
|
if (status.health != this.lastHealth) {
|
||||||
|
console.log("got hit life left:", status.health);
|
||||||
|
this.lastHealth = status.health;
|
||||||
|
}
|
||||||
|
// no actions, no send
|
||||||
|
// this.sendCmd();
|
||||||
|
}
|
||||||
|
lost() {
|
||||||
|
console.log("we lost");
|
||||||
|
this.disconnect();
|
||||||
|
}
|
||||||
|
won() {
|
||||||
|
console.log("we won, how?");
|
||||||
|
this.disconnect();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
var targetPractice = new TargetPractice("targetPractice", "scout");
|
||||||
|
targetPractice.connect();
|
3
server-nodejs/.gitignore
vendored
Normal file
3
server-nodejs/.gitignore
vendored
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
node_modules
|
||||||
|
*.js
|
||||||
|
package-lock.json
|
13
server-nodejs/esbuild.mjs
Normal file
13
server-nodejs/esbuild.mjs
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
// Author: Sascha Nitsch https://contentnation.net/@grumpydevelop
|
||||||
|
|
||||||
|
import * as esbuild from 'esbuild'
|
||||||
|
|
||||||
|
await esbuild.build({
|
||||||
|
entryPoints: ['index.ts'],
|
||||||
|
bundle: true,
|
||||||
|
platform: 'node',
|
||||||
|
target: ['node10.4'],
|
||||||
|
packages: 'external',
|
||||||
|
outfile: 'index.js',
|
||||||
|
})
|
186
server-nodejs/htdocs/index.html
Normal file
186
server-nodejs/htdocs/index.html
Normal file
|
@ -0,0 +1,186 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>WebSocket Chat</title>
|
||||||
|
<style type="text/css">
|
||||||
|
.hidden {
|
||||||
|
display:none;
|
||||||
|
}
|
||||||
|
#log {
|
||||||
|
display:inline-block; width:500px; height:500px; box-sizing: border-box;
|
||||||
|
vertical-align:top;
|
||||||
|
}
|
||||||
|
#arena {
|
||||||
|
display:inline-block; width: 1000px; height: 1000px; border:1px solid #000;
|
||||||
|
position:relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
#input {
|
||||||
|
display:block; width:600px; box-sizing: border-box;
|
||||||
|
}
|
||||||
|
#arena .robot {
|
||||||
|
position:absolute;
|
||||||
|
width: 8px;
|
||||||
|
height: 20px;
|
||||||
|
}
|
||||||
|
#arena .robot .model {
|
||||||
|
width: 8px;
|
||||||
|
height: 20px;
|
||||||
|
}
|
||||||
|
#arena .robot .cannon {
|
||||||
|
width:2px;
|
||||||
|
left:3px;
|
||||||
|
top: -31px;
|
||||||
|
height:15px;
|
||||||
|
background:#f00;
|
||||||
|
display:block;
|
||||||
|
position:relative;
|
||||||
|
transform-origin: bottom center;
|
||||||
|
}
|
||||||
|
#arena .robot .radar {
|
||||||
|
transform-origin: bottom left;
|
||||||
|
left: 4px;
|
||||||
|
top: -433px;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
#arena .bullet {
|
||||||
|
position: absolute;
|
||||||
|
background: #f00;
|
||||||
|
width:4px;
|
||||||
|
height:4px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="arena"></div>
|
||||||
|
<textarea id="log" cols="30" rows="10"></textarea>
|
||||||
|
<button id="clear" type="button">Clear Watch</button><br />
|
||||||
|
|
||||||
|
<script>
|
||||||
|
window.robotLookup = {};
|
||||||
|
function getRobot(uuid) {
|
||||||
|
if (!window.robotLookup[uuid]) {
|
||||||
|
const div = document.createElement("div");
|
||||||
|
div.setAttribute("id", uuid);
|
||||||
|
div.setAttribute("class", "robot");
|
||||||
|
const img = document.createElement("img");
|
||||||
|
img.setAttribute("src", "tank.png");
|
||||||
|
img.setAttribute("class", "model");
|
||||||
|
div.appendChild(img);
|
||||||
|
const span = document.createElement("span");
|
||||||
|
span.setAttribute("class", "cannon");
|
||||||
|
div.appendChild(span);
|
||||||
|
const radar = document.createElement("img");
|
||||||
|
radar.setAttribute("src", "radar.png");
|
||||||
|
radar.setAttribute("class", "radar");
|
||||||
|
div.appendChild(radar);
|
||||||
|
const arena = document.getElementById('arena');
|
||||||
|
arena.appendChild(div);
|
||||||
|
|
||||||
|
window.robotLookup[uuid] = {
|
||||||
|
"cannonElem": span,
|
||||||
|
'imgElem': img,
|
||||||
|
'modelElem': div,
|
||||||
|
'radarElem': radar,
|
||||||
|
'lastRadar': 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return window.robotLookup[uuid];
|
||||||
|
}
|
||||||
|
const textarea = document.getElementById("log");
|
||||||
|
const clear = document.getElementById('clear');
|
||||||
|
const arena = document.getElementById('arena');
|
||||||
|
const bullets = [];
|
||||||
|
for(let i = 0; i < 10; ++i) {
|
||||||
|
const bullet = document.createElement('div');
|
||||||
|
bullet.setAttribute("class", "bullet hidden");
|
||||||
|
bullets.push(bullet);
|
||||||
|
arena.appendChild(bullet);
|
||||||
|
}
|
||||||
|
function openWebSocket() {
|
||||||
|
const websocket = new WebSocket("ws://localhost:3000/monitor");
|
||||||
|
websocket.onopen = function() {
|
||||||
|
console.log("connection opened");
|
||||||
|
}
|
||||||
|
websocket.onclose = function() {
|
||||||
|
console.log("connection closed");
|
||||||
|
for (uuid in window.robotLookup) {
|
||||||
|
window.robotLookup[uuid]['modelElem'].remove();
|
||||||
|
};
|
||||||
|
window.robotLookup = [];
|
||||||
|
setTimeout(function(){
|
||||||
|
openWebSocket();
|
||||||
|
}, 10000);
|
||||||
|
}
|
||||||
|
|
||||||
|
websocket.onmessage = handleMessage;
|
||||||
|
}
|
||||||
|
function handleMessage(e) {
|
||||||
|
const tokens = e.data.split(" ");
|
||||||
|
let robot;
|
||||||
|
switch (tokens[0]) {
|
||||||
|
case 'ready':
|
||||||
|
case 'connected':
|
||||||
|
break;
|
||||||
|
case 'disconnected':
|
||||||
|
robot = getRobot(tokens[1]);
|
||||||
|
robot['modelElem'].remove();
|
||||||
|
delete window.robotLookup[tokens[1]]
|
||||||
|
break;
|
||||||
|
case 'name':
|
||||||
|
robot = getRobot(tokens[1]);
|
||||||
|
robot["name"] = tokens[2];
|
||||||
|
break;
|
||||||
|
case 'model':
|
||||||
|
robot = getRobot(tokens[1]);
|
||||||
|
robot["model"] = tokens[2];
|
||||||
|
break;
|
||||||
|
case 'bullet':
|
||||||
|
const activeBullets = JSON.parse(tokens[1]);
|
||||||
|
for (let i = 0; i < 10; ++i) {
|
||||||
|
if (i >= activeBullets.length) {
|
||||||
|
bullets[i].classList.add("hidden");
|
||||||
|
} else {
|
||||||
|
bullets[i].classList.remove("hidden");
|
||||||
|
bullets[i].style.left = (2*activeBullets[i][0]-2) + 'px';
|
||||||
|
bullets[i].style.top = (2*activeBullets[i][1]-2) + 'px';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'status':
|
||||||
|
robot = getRobot(tokens[1]);
|
||||||
|
const status = JSON.parse(tokens[2]);
|
||||||
|
robot["status"] = status;
|
||||||
|
const model = robot['modelElem'];
|
||||||
|
model.style.left = (Math.floor(status["posX"]*2) - 5) + 'px';
|
||||||
|
model.style.top = (Math.floor(status["posY"]*2) - 12.5) + 'px';
|
||||||
|
model.style.transform = "rotate(" + Math.floor(status["orientation"])+ "deg)";
|
||||||
|
const cannon = robot['cannonElem'];
|
||||||
|
cannon.style.transform = "rotate(" + Math.floor(status["gunOrient"])+ "deg)";
|
||||||
|
const radar = robot['radarElem'];
|
||||||
|
let pos = Math.floor(status["radarPos"]);
|
||||||
|
let diff = (pos - robot['lastRadar']);
|
||||||
|
if (diff < -180) diff +=360;
|
||||||
|
if (diff > 180) diff -=360;
|
||||||
|
// pos = 0;
|
||||||
|
//console.log(diff);
|
||||||
|
if (diff < 0) { // swiping clock wise
|
||||||
|
radar.style.transform = "rotate(" + pos + "deg)";
|
||||||
|
} else {
|
||||||
|
radar.style.transform = "rotate(" + pos + "deg) scaleX(-1)";
|
||||||
|
}
|
||||||
|
robot['lastRadar'] = Math.floor(status["radarPos"]);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
console.log("received message:", tokens);
|
||||||
|
textarea.value += e.data+"\r\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
openWebSocket();
|
||||||
|
clear.addEventListener('click', function() {
|
||||||
|
textarea.value = '';
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
BIN
server-nodejs/htdocs/radar.png
Normal file
BIN
server-nodejs/htdocs/radar.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 4 KiB |
BIN
server-nodejs/htdocs/tank.png
Normal file
BIN
server-nodejs/htdocs/tank.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 730 B |
17
server-nodejs/index.ts
Normal file
17
server-nodejs/index.ts
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
// Author: Sascha Nitsch https://contentnation.net/@grumpydevelop
|
||||||
|
|
||||||
|
import { MyWebSocketServer } from './websockets/server';
|
||||||
|
import { WorldSim } from './worldsim';
|
||||||
|
|
||||||
|
const worldSim = new WorldSim();
|
||||||
|
const webserver = new MyWebSocketServer(worldSim);
|
||||||
|
webserver.start();
|
||||||
|
worldSim.start(2)
|
||||||
|
.then( () => {
|
||||||
|
webserver.stop().then( () => {
|
||||||
|
//console.log("stopped", process._getActiveRequests());//,process._getActiveHandles(), );
|
||||||
|
//console.log("stopped", process._getActiveHandles());
|
||||||
|
process.exit(0);
|
||||||
|
});
|
||||||
|
});
|
19
server-nodejs/package.json
Normal file
19
server-nodejs/package.json
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
{
|
||||||
|
"name": "server-nodejs",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"description": "Server for robotank",
|
||||||
|
"main": "index.js",
|
||||||
|
"scripts": {
|
||||||
|
"test": "echo \"Error: no test specified\" && exit 1",
|
||||||
|
"build": "node esbuild.mjs"
|
||||||
|
},
|
||||||
|
"author": "",
|
||||||
|
"license": "ISC",
|
||||||
|
"devDependencies": {
|
||||||
|
"esbuild": "^0.25.0",
|
||||||
|
"ws": "^8.18.1"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"bufferutil": "^4.0.9"
|
||||||
|
}
|
||||||
|
}
|
182
server-nodejs/robot.ts
Normal file
182
server-nodejs/robot.ts
Normal file
|
@ -0,0 +1,182 @@
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
// Author: Sascha Nitsch https://contentnation.net/@grumpydevelop
|
||||||
|
|
||||||
|
import { WorldSim } from './worldsim';
|
||||||
|
import { RobotCmd } from './robotcmd';
|
||||||
|
import { RobotStatusExt } from './robotstatus';
|
||||||
|
|
||||||
|
class Robot {
|
||||||
|
private name: string;
|
||||||
|
private model: string;
|
||||||
|
private status: RobotStatusExt;
|
||||||
|
private cmd: RobotCmd;
|
||||||
|
private world: WorldSim;
|
||||||
|
private uuid: string;
|
||||||
|
private width: number;
|
||||||
|
private height: number;
|
||||||
|
private fireCooldown: number;
|
||||||
|
private topSpeed: number = 20;
|
||||||
|
private radarRange: number = 200;
|
||||||
|
|
||||||
|
constructor(worldsim: WorldSim, uuid: string, width: number, height: number) {
|
||||||
|
this.world = worldsim;
|
||||||
|
this.uuid = uuid;
|
||||||
|
this.name = '';
|
||||||
|
this.model = '';
|
||||||
|
this.status = new RobotStatusExt();
|
||||||
|
this.width = width;
|
||||||
|
this.height = height;
|
||||||
|
this.status.posX = Math.random()*(width - 20) + 10,
|
||||||
|
this.status.posY = Math.random()*(height - 20) + 10,
|
||||||
|
this.status.orientation = Math.random()*360;
|
||||||
|
this.fireCooldown = 0;
|
||||||
|
this.cmd = new RobotCmd;
|
||||||
|
this.cmd.mergeObject({
|
||||||
|
powerLeft: 0,
|
||||||
|
powerRight: 0,
|
||||||
|
radarMin: 0,
|
||||||
|
radarMax: 360,
|
||||||
|
gunTarget: 0,
|
||||||
|
fire: false
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
causedDamage(amount: number) {
|
||||||
|
this.status.causedDamage = amount;
|
||||||
|
}
|
||||||
|
damage(amount: number) {
|
||||||
|
this.status.health -= amount;
|
||||||
|
}
|
||||||
|
|
||||||
|
getStatus() : RobotStatusExt {
|
||||||
|
return this.status;
|
||||||
|
}
|
||||||
|
|
||||||
|
getX() : number {
|
||||||
|
return this.status.posX;
|
||||||
|
}
|
||||||
|
|
||||||
|
getY() : number {
|
||||||
|
return this.status.posY;
|
||||||
|
}
|
||||||
|
|
||||||
|
isReady() : boolean {
|
||||||
|
return this.name !== '' && this.model !== '';
|
||||||
|
}
|
||||||
|
|
||||||
|
moveRobot(dt: number) {
|
||||||
|
if (this.cmd.fire && this.fireCooldown <= 0) {
|
||||||
|
this.world.fire(this.uuid, this.status.posX, this.status.posY, this.status.orientation + this.status.gunOrient);
|
||||||
|
this.fireCooldown = 10; // 10 seconds to reload
|
||||||
|
}
|
||||||
|
if (this.fireCooldown > 0) {
|
||||||
|
this.fireCooldown -= dt;
|
||||||
|
}
|
||||||
|
this.status.chainSpeedLeft += (0.3*(this.cmd.powerLeft-this.status.chainSpeedLeft));
|
||||||
|
this.status.chainSpeedRight += (0.3*(this.cmd.powerRight-this.status.chainSpeedRight));
|
||||||
|
const speed = (this.status.chainSpeedLeft + this.status.chainSpeedRight)/2.0;
|
||||||
|
this.status.posX += speed * Math.sin(this.status.orientation * Math.PI / 180.0) * dt * this.topSpeed;
|
||||||
|
this.status.posY -= speed * Math.cos(this.status.orientation * Math.PI / 180.0) * dt * this.topSpeed;
|
||||||
|
if (this.status.posX < 0) {
|
||||||
|
this.status.posX = 0;
|
||||||
|
this.damage(1);
|
||||||
|
}
|
||||||
|
if (this.status.posY < 0) {
|
||||||
|
this.status.posY = 0;
|
||||||
|
this.damage(1);
|
||||||
|
}
|
||||||
|
if (this.status.posX > this.width) {
|
||||||
|
this.status.posX = this.width;
|
||||||
|
this.damage(1);
|
||||||
|
}
|
||||||
|
if (this.status.posY > this.height) {
|
||||||
|
this.status.posY = this.height;
|
||||||
|
this.damage(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.status.orientation += (this.status.chainSpeedLeft - this.status.chainSpeedRight) * 30 * dt;
|
||||||
|
if (this.status.orientation >= 360.0) this.status.orientation -= 360.0;
|
||||||
|
if (this.status.orientation < 0.0) this.status.orientation += 360.0;
|
||||||
|
if (this.cmd.radarMax == this.cmd.radarMin && this.cmd.radarMax == -1) { // full 360
|
||||||
|
this.status.radarPos += 60 * dt; // 60 degree per sec
|
||||||
|
if (this.status.radarPos > 360) this.status.radarPos -= 360;
|
||||||
|
} else if (this.status.radarInc) {
|
||||||
|
this.status.radarPos = (this.status.radarPos + 60 * dt) % 360;
|
||||||
|
if (Math.abs(this.cmd.radarMax - this.status.radarPos) < 30 && this.cmd.radarMax <= this.status.radarPos) {
|
||||||
|
this.status.radarInc = false;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.status.radarPos = (360 + this.status.radarPos - 60 * dt) % 360;
|
||||||
|
if (Math.abs(this.cmd.radarMin - this.status.radarPos)< 30 && this.cmd.radarMin < this.status.radarPos) {
|
||||||
|
this.status.radarInc = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (Math.abs(this.status.gunOrient - this.cmd.gunTarget) > 0.1) {
|
||||||
|
const relTarget = this.cmd.gunTarget;
|
||||||
|
const relPos = this.status.gunOrient;
|
||||||
|
const diffcw = ((relPos - relTarget)+360)%360;
|
||||||
|
const diffccw = ((relTarget-relPos)+360)%360;
|
||||||
|
//console.log(relTarget, relPos, diffcw, diffccw, diffcw > diffccw);
|
||||||
|
const diff = Math.min(Math.abs((relTarget - relPos)%360), 2.5) * (diffcw > diffccw ? 1 : -1);
|
||||||
|
this.status.gunOrient = (this.status.gunOrient + diff + 360) %360;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setName(name: string) {
|
||||||
|
this.name = name;
|
||||||
|
}
|
||||||
|
|
||||||
|
setModel(model: string) {
|
||||||
|
// TODO, read from file
|
||||||
|
this.model = model;
|
||||||
|
}
|
||||||
|
setSimulationTime(time: number) {
|
||||||
|
this.status.simulationTime = time;
|
||||||
|
}
|
||||||
|
|
||||||
|
updateCmd(newCmd: RobotCmd) {
|
||||||
|
this.cmd.mergeObject(newCmd);
|
||||||
|
if (this.cmd.powerLeft > 1) {
|
||||||
|
this.cmd.powerLeft = 1;
|
||||||
|
}
|
||||||
|
if (this.cmd.powerLeft < -1) {
|
||||||
|
this.cmd.powerLeft = -1;
|
||||||
|
}
|
||||||
|
if (this.cmd.powerRight > 1) {
|
||||||
|
this.cmd.powerRight = 1;
|
||||||
|
}
|
||||||
|
if (this.cmd.powerRight < -1) {
|
||||||
|
this.cmd.powerRight = -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateRadar() {
|
||||||
|
this.status.contactPoints = [];
|
||||||
|
this.world.robots.forEach((robot) => {
|
||||||
|
if (robot != this) {
|
||||||
|
// x distance to other robot
|
||||||
|
const dx = robot.status.posX - this.status.posX;
|
||||||
|
// y distance to other robot
|
||||||
|
const dy = robot.status.posY - this.status.posY;
|
||||||
|
// direction to other robot
|
||||||
|
const dir = Math.atan2(dx, -dy) / Math.PI * 180;
|
||||||
|
// distance to other robot
|
||||||
|
const dst = Math.sqrt(dx * dx + dy * dy);
|
||||||
|
// relative rotation to other robot
|
||||||
|
let rot = dir - this.status.orientation - this.status.radarPos;
|
||||||
|
// ensure -180 > rot < 180
|
||||||
|
while (rot > 180) {
|
||||||
|
rot -= 360;
|
||||||
|
}
|
||||||
|
while (rot < -180) {
|
||||||
|
rot += 360;
|
||||||
|
}
|
||||||
|
if (((this.status.radarInc && rot>=-6 && rot <=0) || (!this.status.radarInc && rot>=0 && rot <=6)) && dst < this.radarRange) {
|
||||||
|
this.status.contactPoints.push({angle: rot, dist: dst});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Robot };
|
51
server-nodejs/robotcmd.ts
Normal file
51
server-nodejs/robotcmd.ts
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
// Author: Sascha Nitsch https://contentnation.net/@grumpydevelop
|
||||||
|
|
||||||
|
class RobotCmd {
|
||||||
|
powerLeft: number = 0;
|
||||||
|
powerRight: number = 0;
|
||||||
|
radarMin: number = -1;
|
||||||
|
radarMax: number = -1;
|
||||||
|
gunTarget: number = -1;
|
||||||
|
fire?: boolean;
|
||||||
|
|
||||||
|
toObject() {
|
||||||
|
const o: Object = {};
|
||||||
|
if (this.powerLeft !== -9999) {
|
||||||
|
o['powerLeft'] = this.powerLeft;
|
||||||
|
}
|
||||||
|
if (this.powerRight !== -9999) {
|
||||||
|
o['powerRight'] = this.powerRight;
|
||||||
|
}
|
||||||
|
o['radarMin'] = this.radarMin;
|
||||||
|
o['radarMax'] = this.radarMax;
|
||||||
|
if (this.gunTarget !== -1) {
|
||||||
|
o['gunTarget'] = this.gunTarget;
|
||||||
|
}
|
||||||
|
if (this.fire === true) {
|
||||||
|
o['fire'] = true;
|
||||||
|
}
|
||||||
|
return o;
|
||||||
|
}
|
||||||
|
|
||||||
|
mergeObject(o: Object) {
|
||||||
|
if (o['powerLeft'] !== undefined) {
|
||||||
|
this.powerLeft = o['powerLeft'];
|
||||||
|
}
|
||||||
|
if (o['powerRight'] !== undefined) {
|
||||||
|
this.powerRight = o['powerRight'];
|
||||||
|
}
|
||||||
|
if (o['radarMin'] !== undefined) {
|
||||||
|
this.radarMin = o['radarMin'];
|
||||||
|
}
|
||||||
|
if (o['radarMax'] !== undefined) {
|
||||||
|
this.radarMax = o['radarMax'];
|
||||||
|
}
|
||||||
|
if (o['gunTarget'] !== undefined) {
|
||||||
|
this.gunTarget = o['gunTarget'];
|
||||||
|
}
|
||||||
|
this.fire = o['fire'] === true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export { RobotCmd }
|
83
server-nodejs/robotstatus.ts
Normal file
83
server-nodejs/robotstatus.ts
Normal file
|
@ -0,0 +1,83 @@
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
// Author: Sascha Nitsch https://contentnation.net/@grumpydevelop
|
||||||
|
|
||||||
|
interface ContactPoint {
|
||||||
|
dist: number;
|
||||||
|
angle: number;
|
||||||
|
}
|
||||||
|
class RobotStatus {
|
||||||
|
/**
|
||||||
|
* current health 0 = death, max depends on robot
|
||||||
|
*/
|
||||||
|
health: number;
|
||||||
|
/**
|
||||||
|
* x position, range 0..1000
|
||||||
|
*/
|
||||||
|
posX: number;
|
||||||
|
/**
|
||||||
|
* x position, range 0..1000
|
||||||
|
*/
|
||||||
|
posY: number;
|
||||||
|
/**
|
||||||
|
* rotation, range 0..360, 0: north, 90: east
|
||||||
|
*/
|
||||||
|
orientation: number;
|
||||||
|
/**
|
||||||
|
* gun orientation (relative to tank), range 0..360, 0 front, 90 right
|
||||||
|
*/
|
||||||
|
gunOrient: number
|
||||||
|
/**
|
||||||
|
* radar_position 0..360, 0 front, 90 right
|
||||||
|
*/
|
||||||
|
radarPos: number
|
||||||
|
contactPoints: ContactPoint[];
|
||||||
|
chainSpeedLeft: number;
|
||||||
|
chainSpeedRight: number;
|
||||||
|
simulationTime: number;
|
||||||
|
causedDamage: number;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.health = 100;
|
||||||
|
this.posX = 0;
|
||||||
|
this.posY = 0;
|
||||||
|
this.orientation = 0;
|
||||||
|
this.gunOrient = 0;
|
||||||
|
this.radarPos = 0;
|
||||||
|
this.contactPoints = [];
|
||||||
|
this.chainSpeedLeft = 0;
|
||||||
|
this.chainSpeedRight = 0;
|
||||||
|
this.simulationTime = 0;
|
||||||
|
this.causedDamage = 0;
|
||||||
|
}
|
||||||
|
private prec(input: number): number {
|
||||||
|
return Math.round(input * 100) / 100;
|
||||||
|
}
|
||||||
|
toJSON() : string {
|
||||||
|
return JSON.stringify({
|
||||||
|
health: this.health,
|
||||||
|
posX: this.prec(this.posX),
|
||||||
|
posY: this.prec(this.posY),
|
||||||
|
orientation: this.prec(this.orientation),
|
||||||
|
gunOrient: this.prec(this.gunOrient),
|
||||||
|
radarPos: this.prec(this.radarPos),
|
||||||
|
contactPoints: this.contactPoints,
|
||||||
|
chainSpeedLeft: this.prec(this.chainSpeedLeft),
|
||||||
|
chainSpeedRight: this.prec(this.chainSpeedRight),
|
||||||
|
simulationTime: this.prec(this.simulationTime),
|
||||||
|
causedDamage: this.prec(this.causedDamage),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class RobotStatusExt extends RobotStatus {
|
||||||
|
radarInc: boolean;
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.radarInc = true;
|
||||||
|
}
|
||||||
|
toJSON() : string {
|
||||||
|
return super.toJSON();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export {RobotStatus, RobotStatusExt};
|
42
server-nodejs/websockets/monitor.ts
Normal file
42
server-nodejs/websockets/monitor.ts
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
// Author: Sascha Nitsch https://contentnation.net/@grumpydevelop
|
||||||
|
|
||||||
|
import { WebSocketServer } from "ws";
|
||||||
|
|
||||||
|
class MonitorWS extends WebSocketServer {
|
||||||
|
constructor(options: any) {
|
||||||
|
super(options);
|
||||||
|
const that = this;
|
||||||
|
this.on('connection', (ws: any) => {
|
||||||
|
ws.on('error', this.errorHandler);
|
||||||
|
ws.on('disconnect', this.errorHandler);
|
||||||
|
ws.on('message', function (this: WebSocket, data: Buffer, isBinary: boolean ){
|
||||||
|
that.message(this, data, isBinary);
|
||||||
|
});
|
||||||
|
// ...
|
||||||
|
ws.send("Connected");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
errorHandler = (a: any,b: any, c:any) => {
|
||||||
|
console.log(a,b,c);
|
||||||
|
}
|
||||||
|
|
||||||
|
broadcast = (msg: string) => {
|
||||||
|
if (this.clients === undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.clients.forEach((client) => {
|
||||||
|
if (client.readyState === WebSocket.OPEN) {;
|
||||||
|
client.send(msg);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
message = (ws: WebSocket, data: Buffer, isBinary:boolean) => {
|
||||||
|
//const message = data.toString();
|
||||||
|
//console.log('received monitor: ', message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export { MonitorWS }
|
106
server-nodejs/websockets/robot.ts
Normal file
106
server-nodejs/websockets/robot.ts
Normal file
|
@ -0,0 +1,106 @@
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
// Author: Sascha Nitsch https://contentnation.net/@grumpydevelop
|
||||||
|
|
||||||
|
import { randomUUID, UUID } from 'crypto';
|
||||||
|
import { WebSocketServer } from 'ws';
|
||||||
|
import { WorldSim } from '../worldsim';
|
||||||
|
import WebSocket from 'ws';
|
||||||
|
|
||||||
|
interface MyWebSocket extends WebSocket {
|
||||||
|
isAlive: boolean;
|
||||||
|
websocketid: string;
|
||||||
|
heartbeat(): void;
|
||||||
|
closeHandler(this: WebSocket): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
class RobotWS extends WebSocketServer {
|
||||||
|
private idLookup: Map<String, WebSocket>;
|
||||||
|
private worldSim: WorldSim;
|
||||||
|
constructor(options: any, worldsim: WorldSim) {
|
||||||
|
super(options);
|
||||||
|
this.worldSim = worldsim;
|
||||||
|
this.idLookup = new Map();
|
||||||
|
const that = this;
|
||||||
|
this.on('connection', function connection(ws: MyWebSocket) {
|
||||||
|
ws.isAlive = true;
|
||||||
|
ws.websocketid = randomUUID().toString();
|
||||||
|
worldsim.register(ws.websocketid);
|
||||||
|
that.idLookup.set(ws.websocketid, ws);
|
||||||
|
ws.heartbeat = function() {
|
||||||
|
this.isAlive = true;
|
||||||
|
}
|
||||||
|
ws.closeHandler = function(this: WebSocket) {
|
||||||
|
that.closeHandler(<MyWebSocket>this);
|
||||||
|
}
|
||||||
|
ws.on('close', ws.closeHandler);
|
||||||
|
ws.on('pong', ws.heartbeat);
|
||||||
|
ws.on('message', function (this: WebSocket, data: Buffer, isBinary: boolean) {
|
||||||
|
const t = <MyWebSocket>this;
|
||||||
|
t.heartbeat();
|
||||||
|
that.message(t, data, isBinary);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
const interval = setInterval(this.ping, 10000);
|
||||||
|
this.on('close', function close() {
|
||||||
|
//console.log("close", a);
|
||||||
|
clearInterval(interval);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
broadcast = (msg: string) => {
|
||||||
|
if (this.clients === undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.clients.forEach((client) => {
|
||||||
|
if (client.readyState === WebSocket.OPEN) {
|
||||||
|
client.send(msg);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
closeHandler = (ws: MyWebSocket) => {
|
||||||
|
this.worldSim.unregister(ws.websocketid);
|
||||||
|
this.idLookup.delete(ws.websocketid);
|
||||||
|
}
|
||||||
|
|
||||||
|
message = (ws: MyWebSocket, data: Buffer, isBinary:boolean) => {
|
||||||
|
const message = data.toString();
|
||||||
|
const uuid = ws.websocketid;
|
||||||
|
const tokens = message.split(" ");
|
||||||
|
if (tokens.length == 2) {
|
||||||
|
switch(tokens[0]) {
|
||||||
|
case 'cmd':
|
||||||
|
this.worldSim.cmd(uuid, JSON.parse(tokens[1]));
|
||||||
|
break;
|
||||||
|
case 'model':
|
||||||
|
this.worldSim.setModel(uuid, tokens[1])
|
||||||
|
break;
|
||||||
|
case 'name':
|
||||||
|
this.worldSim.setName(uuid, tokens[1]);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
console.log("unknown command", message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ping = () => {
|
||||||
|
if (this.clients === undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.clients.forEach(function each(ws: WebSocket) {
|
||||||
|
const mws = <MyWebSocket>ws;
|
||||||
|
if (mws.isAlive === false) {
|
||||||
|
return mws.terminate();
|
||||||
|
}
|
||||||
|
mws.isAlive = false;
|
||||||
|
mws.ping();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
send(uuid: string, msg: string) {
|
||||||
|
const ws = this.idLookup.get(uuid);
|
||||||
|
ws?.send(msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export { RobotWS }
|
77
server-nodejs/websockets/server.ts
Normal file
77
server-nodejs/websockets/server.ts
Normal file
|
@ -0,0 +1,77 @@
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
// Author: Sascha Nitsch https://contentnation.net/@grumpydevelop
|
||||||
|
|
||||||
|
import { createServer, Server } from 'http';
|
||||||
|
import { MonitorWS } from './monitor';
|
||||||
|
import { RobotWS } from './robot';
|
||||||
|
import { WorldSim } from '../worldsim';
|
||||||
|
import fs from 'fs';
|
||||||
|
|
||||||
|
class MyWebSocketServer {
|
||||||
|
private server: Server;
|
||||||
|
private monitor: MonitorWS;
|
||||||
|
private robot: RobotWS;
|
||||||
|
constructor(worldSim: WorldSim) {
|
||||||
|
this.server = createServer();
|
||||||
|
this.monitor = new MonitorWS({ noServer: true });
|
||||||
|
this.robot = new RobotWS({ noServer: true }, worldSim);
|
||||||
|
worldSim.setMonitors(this.monitor);
|
||||||
|
worldSim.setRobots(this.robot);
|
||||||
|
}
|
||||||
|
start() {
|
||||||
|
this.server.on('request', (request, res) => {
|
||||||
|
const { pathname } = new URL(request.url || '', 'ws://base.url');
|
||||||
|
let filename = 'htdocs' + pathname;
|
||||||
|
if (pathname.endsWith('/')) {
|
||||||
|
filename += 'index.html';
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const stream = fs.createReadStream(filename);
|
||||||
|
stream.on("error", () => {
|
||||||
|
res.statusCode = 404;
|
||||||
|
res.end("Not found");
|
||||||
|
console.log("404", filename);
|
||||||
|
});
|
||||||
|
stream.on("open", () => {
|
||||||
|
if (filename.endsWith('.html')) {
|
||||||
|
res.setHeader("Content-Type", "text/html");
|
||||||
|
} else if (filename.endsWith('.css')) {
|
||||||
|
res.setHeader("Content-Type", "text/css");
|
||||||
|
} else if (filename.endsWith('.js')) {
|
||||||
|
res.setHeader("Content-Type", "text/javascript");
|
||||||
|
}
|
||||||
|
stream.pipe(res);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
request.statusCode = 404;
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
this.server.on('upgrade', (request, socket, head) => {
|
||||||
|
const { pathname } = new URL(request.url || '', 'ws://base.url');
|
||||||
|
if (pathname === '/monitor') {
|
||||||
|
this.monitor.handleUpgrade(request, socket, head, (ws) =>{
|
||||||
|
this.monitor.emit('connection', ws, request);
|
||||||
|
});
|
||||||
|
} else if (pathname === '/robot') {
|
||||||
|
this.robot.handleUpgrade(request, socket, head, (ws) => {
|
||||||
|
this.robot.emit('connection', ws, request);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
console.log("nope");
|
||||||
|
socket.destroy();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.server.listen(3000);
|
||||||
|
}
|
||||||
|
|
||||||
|
async stop() {
|
||||||
|
this.server.closeAllConnections();
|
||||||
|
// await this.httpTerminator.terminate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export { MyWebSocketServer }
|
269
server-nodejs/worldsim.ts
Normal file
269
server-nodejs/worldsim.ts
Normal file
|
@ -0,0 +1,269 @@
|
||||||
|
// 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 }
|
Loading…
Reference in a new issue