alclient
Version:
A node client for interacting with Adventure Land - The Code MMORPG. This package extends the functionality of 'alclient' by managing a mongo database.
887 lines • 40.4 kB
JavaScript
import socketio from "socket.io-client";
import { Database, EntityModel, NPCModel, PlayerModel } from "./database/Database.js";
import { Constants } from "./Constants.js";
import { Entity } from "./Entity.js";
import { Player } from "./Player.js";
import { Tools } from "./Tools.js";
import { RespawnModel } from "./database/respawns/respawns.model.js";
import isNumber from "is-number";
import { ServerModel } from "./database/servers/servers.model.js";
import { InstanceModel } from "./database/instances/instances.model.js";
export class Observer {
socket;
lastAllEntities;
lastPositionUpdate;
G;
entities = new Map();
secret;
pingIndex = 0;
pingMap = new Map();
pingNum = 1;
static pingsPerServer = new Map();
pings = [];
players = new Map();
projectiles = new Map();
S = {};
server;
serverData;
map;
in;
x;
y;
get ping() {
return this.pings.length == 0 ? 0 : Math.min(...this.pings);
}
get timeout() {
return Math.min(this.ping * 2, Constants.TIMEOUT);
}
constructor(serverData, g, secret) {
this.serverData = serverData;
this.G = g;
this.secret = secret;
if (serverData) {
// Retrieve the cached pings data
const key = `${serverData.region}${serverData.name}`;
const pings = Observer.pingsPerServer.get(key);
if (pings) {
this.pings = pings;
}
}
}
async connect(reconnect = false, start = true) {
this.socket = socketio(`ws${this.serverData.secure ? "s" : ""}://${this.serverData.addr}:${this.serverData.port}`, {
autoConnect: false,
query: this.secret ? { secret: this.secret } : {},
reconnection: reconnect,
transports: ["websocket"],
});
this.socket.on("action", (data) => {
if (data.instant)
return; // It's instant, don't add a projectile
// Fix the ETA
// TODO: We should fix this in Adventureland itself
const attacker = this.players.get(data.attacker) ?? this.entities.get(data.attacker);
const target = this.entities.get(data.target) ?? this.players.get(data.target);
const projectileSpeed = this.G.projectiles[data.projectile]?.speed;
if (attacker && target && projectileSpeed) {
const distance = Tools.distance(attacker, target);
const fixedETA = (distance / projectileSpeed) * 1000;
data.eta = fixedETA;
}
this.projectiles.set(data.pid, { ...data, date: new Date() });
});
this.socket.on("death", (data) => {
this.deleteEntity(data.id, true);
});
// Update database when characters move around the map by transporting
this.socket.on("disappear", (data) => {
// Remove them from their list
this.players.delete(data.id) || this.deleteEntity(data.id);
this.updatePositions();
if (!Database.connection || data.reason == "disconnect" || data.reason == "invis")
return; // We don't track these
if ((data.effect == "blink" || data.effect == "magiport") &&
data.to !== undefined &&
this.G.maps[data.to] &&
data.s !== undefined &&
// NOTE: entity IDs are numbers, so the isNumber check filters out entities
!isNumber(data.id)) {
// They used "blink" or "magiport" and don't have a stealth cape
const updateData = {
lastSeen: Date.now(),
map: data.to,
serverIdentifier: this.serverData.name,
serverRegion: this.serverData.region,
x: data.s[0],
y: data.s[1],
};
const nextUpdate = Database.nextUpdate.get(`${this.serverData.name}${this.serverData.region}${data.id}`);
if (!nextUpdate || Date.now() >= nextUpdate) {
PlayerModel.updateOne({ name: data.id }, updateData, { upsert: true })
.lean()
.exec()
.catch(console.error);
Database.nextUpdate.set(`${this.serverData.name}${this.serverData.region}${data.id}`, Date.now() + Constants.MONGO_UPDATE_MS);
}
}
else if (data.to !== undefined && data.effect == 1) {
let s = 0;
if (data.s !== undefined)
s = data.s;
// They used a "home" teleport and don't have a stealth cape
const spawnLocation = this.G.maps[data.to]?.spawns[s];
if (!spawnLocation)
return; // They are wearing a stealth cape
const updateData = {
lastSeen: Date.now(),
map: data.to,
serverIdentifier: this.serverData.name,
serverRegion: this.serverData.region,
x: spawnLocation[0],
y: spawnLocation[1],
};
const nextUpdate = Database.nextUpdate.get(`${this.serverData.name}${this.serverData.region}${data.id}`);
if (!nextUpdate || Date.now() >= nextUpdate) {
PlayerModel.updateOne({ name: data.id }, updateData, { upsert: true })
.lean()
.exec()
.catch(console.error);
Database.nextUpdate.set(`${this.serverData.name}${this.serverData.region}${data.id}`, Date.now() + Constants.MONGO_UPDATE_MS);
}
}
});
this.socket.on("disconnect", () => {
// Save the ping data if we reconnect to the same server
if (!this.serverData || !this.pings || this.pings.length == 0)
return;
const key = `${this.serverData.region}${this.serverData.name}`;
Observer.pingsPerServer.set(key, this.pings);
});
this.socket.on("entities", (data) => {
this.parseEntities(data);
});
this.socket.on("game_event", (data) => {
if (this.G.monsters[data.name]) {
// The event is a monster
const monsterData = {
hp: this.G.monsters[data.name].hp,
lastSeen: Date.now(),
level: 1,
map: data.map,
s: this.G.monsters[data.name].s,
x: data.x,
y: data.y,
};
this.S[data.name] = {
...monsterData,
live: true,
max_hp: monsterData.hp,
};
if (Database.connection) {
EntityModel.updateOne({
serverIdentifier: this.serverData.name,
serverRegion: this.serverData.region,
type: data.name,
}, monsterData, {
upsert: true,
})
.lean()
.exec()
.catch(console.error);
}
}
});
this.socket.on("hit", (data) => {
if (data.miss || data.evade) {
this.projectiles.delete(data.pid);
return;
}
if (data.reflect) {
// Reflect the projectile towards the attacker
const p = this.projectiles.get(data.pid);
if (p) {
p.damage = data.reflect;
p.target = data.hid;
// TODO: Is this correct!? Shouldn't we check the `data.hid` position?
p.x = this.x;
p.y = this.y;
}
}
if (data.kill) {
this.projectiles.delete(data.pid);
this.deleteEntity(data.id, true);
}
else if (data.damage) {
this.projectiles.delete(data.pid);
const e = this.entities.get(data.id) ?? this.players.get(data.id);
if (e)
e.hp = e.hp - data.damage;
}
else {
// NOTE: Priest's `curse` doesn't do damage, and is one example of a projectile that would be caught in this else block.
this.projectiles.delete(data.pid);
}
});
this.socket.on("new_map", (data) => {
this.parseNewMap(data);
});
this.socket.on("ping_ack", (data) => {
const ping = this.pingMap.get(data.id);
if (ping) {
// Add the new ping
const time = Date.now() - ping.time;
this.pings[this.pingIndex++] = time;
this.pingIndex = this.pingIndex % Constants.MAX_PINGS;
if (ping.log)
console.log(`Ping: ${time}`);
// Remove the ping from the map
this.pingMap.delete(data.id);
}
});
this.socket.on("server_info", (data) => {
const databaseEntityUpdates = [];
const databaseRespawnUpdates = [];
const databaseDeletes = new Set();
const now = Date.now();
if (Database.connection) {
for (const type of Constants.SERVER_INFO_MONSTERS) {
if (!data[type] || !data[type].live)
databaseDeletes.add(type);
}
}
for (const mtype in data) {
const mData = data[mtype];
if (typeof mData !== "object")
continue; // Event information, not monster information
if (mData.live == false) {
if (Database.connection) {
databaseDeletes.add(mtype);
// The next respawn date is known
const nextSpawn = new Date(data[mtype].spawn);
databaseRespawnUpdates.push({
updateOne: {
filter: {
serverIdentifier: this.serverData.name,
serverRegion: this.serverData.region,
type: mtype,
},
update: { estimatedRespawn: nextSpawn.getTime() },
upsert: true,
},
});
}
continue;
}
if (data[mtype]["x"] == undefined || data[mtype]["y"] == undefined)
continue; // No location data (e.g.: Slenderman)
// Add soft properties to monster
const mN = mtype;
const goodData = data[mN];
if (goodData.hp == undefined)
goodData.hp = this.G.monsters[mN].hp;
if (goodData.max_hp == undefined)
goodData.max_hp = this.G.monsters[mN].hp;
data[mN] = goodData;
if (Database.connection && Constants.SPECIAL_MONSTERS.includes(mN)) {
const updateKey = `${this.serverData.name}${this.serverData.region}${mN}`;
const nextUpdate = Database.nextUpdate.get(updateKey);
if (nextUpdate && Date.now() < nextUpdate)
continue; // We've updated this monster recently
const filter = {
serverIdentifier: this.serverData.name,
serverRegion: this.serverData.region,
type: mN,
};
const update = {
hp: goodData.hp,
lastSeen: now,
map: goodData.map,
target: goodData.target,
x: goodData.x,
y: goodData.y,
};
databaseEntityUpdates.push({
updateOne: {
filter: filter,
update: update,
upsert: true,
},
});
databaseRespawnUpdates.push({
deleteOne: {
filter: filter,
},
});
Database.nextUpdate.set(updateKey, Date.now() + Constants.MONGO_UPDATE_MS);
}
}
if (Database.connection) {
const nextUpdate = Database.nextUpdate.get(`${this.serverData.name}${this.serverData.region}*server_info*`);
if (!nextUpdate || Date.now() >= nextUpdate) {
for (const type in Constants.MONSTER_RESPAWN_TIMES) {
const mtype = type;
if (data[mtype])
continue; // It's still alive
if (!this.S[mtype])
continue; // It wasn't alive before
// This special monster just died
const nextSpawn = Date.now() + Constants.MONSTER_RESPAWN_TIMES[mtype];
databaseRespawnUpdates.push({
updateOne: {
filter: {
serverIdentifier: this.serverData.name,
serverRegion: this.serverData.region,
type: type,
},
update: { estimatedRespawn: nextSpawn },
upsert: true,
},
});
}
if (databaseDeletes.size)
EntityModel.deleteMany({
serverIdentifier: this.serverData.name,
serverRegion: this.serverData.region,
type: { $in: [...databaseDeletes] },
})
.lean()
.exec()
.catch(console.error);
if (databaseEntityUpdates.length)
EntityModel.bulkWrite(databaseEntityUpdates).catch(console.error);
if (databaseRespawnUpdates.length)
RespawnModel.bulkWrite(databaseRespawnUpdates).catch(console.error);
const updateData = {
S: data,
lastUpdate: now,
serverIdentifier: this.serverData.name,
serverRegion: this.serverData.region,
};
ServerModel.updateOne({ serverIdentifier: this.serverData.name, serverRegion: this.serverData.region }, updateData, { upsert: true })
.lean()
.exec()
.catch(console.error);
Database.nextUpdate.set(`${this.serverData.name}${this.serverData.region}*server_info*`, Date.now() + Constants.MONGO_UPDATE_MS);
}
}
this.S = data;
});
this.socket.on("welcome", (data) => {
this.server = data;
if (data.S)
this.S = data.S;
});
if (start) {
console.debug(`Connecting to ${this.serverData.region} ${this.serverData.name}...`);
const connected = new Promise((resolve, reject) => {
this.socket.on("welcome", (data) => {
if (data.region !== this.serverData.region || data.name !== this.serverData.name) {
reject(new Error(`We wanted the server ${this.serverData.region}${this.serverData.name}, but we are on ${data.region}${data.name}.`));
}
else {
this.socket.emit("loaded", {
height: 1080,
scale: 2,
success: 1,
width: 1920,
});
resolve();
}
});
setTimeout(() => {
reject(new Error(`Failed to start within ${Constants.CONNECT_TIMEOUT_MS / 1000}s.`));
}, Constants.CONNECT_TIMEOUT_MS);
});
this.socket.open();
return connected;
}
}
/**
* Removes an entity from the entity map, as well as potentially handling database updates
* for special monsters
* @param id The ID of the entity to remove
* @param death Are we deleting it because it died?
* @returns If the entity was successfully deleted from the entities map
*/
deleteEntity(id, death = false) {
const entity = this.entities.get(id);
if (entity) {
// If it was a special monster in 'S', delete it from 'S'.
if (this.S[entity.type] && death)
delete this.S[entity.type];
// Delete the entity from the database on death
if (Database.connection && Constants.SPECIAL_MONSTERS.includes(entity.type)) {
const nextUpdate = Database.nextUpdate.get(`${this.serverData.name}${this.serverData.region}${entity.id}`);
if (death && nextUpdate !== Number.MAX_VALUE) {
if (entity.in !== entity.map) {
// It's an instanced monster
const now = Date.now();
InstanceModel.updateOne({
in: entity.in,
map: entity.map,
serverIdentifier: this.serverData.name,
serverRegion: this.serverData.region,
}, {
$inc: {
[`killed.${entity.type}`]: 1,
},
$max: {
lastEntered: now,
},
$min: {
firstEntered: now,
},
}, { upsert: true })
.lean()
.exec()
.catch(console.error);
}
if (Constants.ONE_SPAWN_MONSTERS.includes(entity.type) &&
this.G.monsters[entity.type].respawn > 10) {
// G.monsters time is in seconds. We need the time in ms.
let monsterRespawn = this.G.monsters[entity.type].respawn;
if (monsterRespawn > 200) {
// Long respawns have a random chance to spawn from 28% early to 10% late
monsterRespawn *= 720;
}
else {
monsterRespawn *= 1000;
}
RespawnModel.updateOne({
type: entity.type,
serverIdentifier: this.serverData.name,
serverRegion: this.serverData.region,
}, {
estimatedRespawn: Date.now() + monsterRespawn,
}, { upsert: true })
.lean()
.exec()
.catch(console.error);
}
EntityModel.deleteOne({
name: id,
serverIdentifier: this.serverData.name,
serverRegion: this.serverData.region,
})
.lean()
.exec()
.catch(console.error);
Database.nextUpdate.set(`${this.serverData.name}${this.serverData.region}${entity.id}`, Number.MAX_VALUE);
}
}
return this.entities.delete(id);
}
return false;
}
parseEntities(data) {
if (data.type == "all") {
this.lastAllEntities = Date.now();
// Erase all of the entities
this.entities.clear();
this.players.clear();
this.lastPositionUpdate = Date.now();
}
else {
// Update all positions
this.updatePositions();
}
const visibleIDs = [];
const entityUpdates = [];
const npcUpdates = [];
const playerUpdates = [];
for (const monster of data.monsters) {
let e;
if (!this.entities.has(monster.id)) {
// Create the entity and add it to our list
e = new Entity(monster, data.map, data.in, this.G);
this.entities.set(monster.id, e);
}
else {
// Update everything
e = this.entities.get(monster.id);
e.updateData(monster);
}
visibleIDs.push(e.id);
// Update our database
if (Database.connection && Constants.SPECIAL_MONSTERS.includes(e.type)) {
const now = Date.now();
const nextUpdate = Database.nextUpdate.get(`${this.serverData.name}${this.serverData.region}${e.id}`);
if (!nextUpdate || now > nextUpdate) {
const firstSeen = Tools.estimateSpawnedDate(e.level, this.G.monsters[e.type].hp);
if (Constants.ONE_SPAWN_MONSTERS.includes(e.type)) {
// Don't include the id in the filter, so it overwrites the last one
entityUpdates.push({
updateOne: {
filter: {
serverIdentifier: this.serverData.name,
serverRegion: this.serverData.region,
type: e.type,
},
update: {
$min: { firstSeen: firstSeen },
hp: e.hp,
in: e.in,
lastSeen: now,
level: e.level,
map: e.map,
name: e.id,
s: e.s,
target: e.target,
x: e.x,
y: e.y,
},
upsert: true,
},
});
Database.nextUpdate.set(`${this.serverData.name}${this.serverData.region}${e.type}`, now + Constants.MONGO_UPDATE_MS);
}
else {
// Include the id in the filter
entityUpdates.push({
updateOne: {
filter: {
name: e.id,
serverIdentifier: this.serverData.name,
serverRegion: this.serverData.region,
type: e.type,
},
update: {
$min: { firstSeen: firstSeen },
hp: e.hp,
in: e.in,
lastSeen: now,
level: e.level,
map: e.map,
name: e.id,
s: e.s,
target: e.target,
x: e.x,
y: e.y,
},
upsert: true,
},
});
}
Database.nextUpdate.set(`${this.serverData.name}${this.serverData.region}${e.id}`, now + Constants.MONGO_UPDATE_MS);
}
}
}
for (const player of data.players) {
let p;
if (!this.players.has(player.id)) {
// Create the player and add it to our list
p = new Player(player, data.map, data.in, this.G);
this.players.set(player.id, p);
}
else {
// Update everything
p = this.players.get(player.id);
p.updateData(player);
}
// Update our database
if (Database.connection) {
const now = Date.now();
const nextUpdate = Database.nextUpdate.get(`${this.serverData.name}${this.serverData.region}${p.id}`);
if (!nextUpdate || now > nextUpdate) {
if (p.isNPC()) {
npcUpdates.push({
updateOne: {
filter: {
name: p.name,
serverIdentifier: this.serverData.name,
serverRegion: this.serverData.region,
},
update: { $min: { firstSeen: now }, lastSeen: now, map: p.map, x: p.x, y: p.y },
upsert: true,
},
});
}
else {
const updateData = {
$min: { firstSeen: now },
in: p.in,
lastSeen: now,
map: p.map,
party: p.party,
rip: p.rip,
s: p.s,
serverIdentifier: this.serverData.name,
serverRegion: this.serverData.region,
"slots.amulet": p.slots.amulet,
"slots.belt": p.slots.belt,
"slots.cape": p.slots.cape,
"slots.chest": p.slots.chest,
"slots.earring1": p.slots.earring1,
"slots.earring2": p.slots.earring2,
"slots.elixir": p.slots.elixir,
"slots.gloves": p.slots.gloves,
"slots.helmet": p.slots.helmet,
"slots.mainhand": p.slots.mainhand,
"slots.offhand": p.slots.offhand,
"slots.orb": p.slots.orb,
"slots.pants": p.slots.pants,
"slots.ring1": p.slots.ring1,
"slots.ring2": p.slots.ring2,
"slots.shoes": p.slots.shoes,
"slots.trade1": p.slots.trade1,
"slots.trade2": p.slots.trade2,
"slots.trade3": p.slots.trade3,
"slots.trade4": p.slots.trade4,
type: p.ctype,
x: p.x,
y: p.y,
};
if (p.stand) {
updateData["slots.trade5"] = p.slots.trade5;
updateData["slots.trade6"] = p.slots.trade6;
updateData["slots.trade7"] = p.slots.trade7;
updateData["slots.trade8"] = p.slots.trade8;
updateData["slots.trade9"] = p.slots.trade9;
updateData["slots.trade10"] = p.slots.trade10;
updateData["slots.trade11"] = p.slots.trade11;
updateData["slots.trade12"] = p.slots.trade12;
updateData["slots.trade13"] = p.slots.trade13;
updateData["slots.trade14"] = p.slots.trade14;
updateData["slots.trade15"] = p.slots.trade15;
updateData["slots.trade16"] = p.slots.trade16;
updateData["slots.trade17"] = p.slots.trade17;
updateData["slots.trade18"] = p.slots.trade18;
updateData["slots.trade19"] = p.slots.trade19;
updateData["slots.trade20"] = p.slots.trade20;
updateData["slots.trade21"] = p.slots.trade21;
updateData["slots.trade22"] = p.slots.trade22;
updateData["slots.trade23"] = p.slots.trade23;
updateData["slots.trade24"] = p.slots.trade24;
updateData["slots.trade25"] = p.slots.trade25;
updateData["slots.trade26"] = p.slots.trade26;
updateData["slots.trade27"] = p.slots.trade27;
updateData["slots.trade28"] = p.slots.trade28;
updateData["slots.trade29"] = p.slots.trade29;
updateData["slots.trade30"] = p.slots.trade30;
}
if (p.owner)
updateData.owner = p.owner;
playerUpdates.push({
updateOne: {
filter: { name: p.id },
update: updateData,
upsert: true,
},
});
}
Database.nextUpdate.set(`${this.serverData.name}${this.serverData.region}${p.id}`, Date.now() + Constants.MONGO_UPDATE_MS);
}
}
}
if (Database.connection) {
if (entityUpdates.length)
EntityModel.bulkWrite(entityUpdates).catch(console.error);
if (npcUpdates.length)
NPCModel.bulkWrite(npcUpdates).catch(console.error);
if (playerUpdates.length)
PlayerModel.bulkWrite(playerUpdates).catch(console.error);
if (data.type == "all") {
// Get live special monsters so we can exclude deleting them
const liveSpecialMonsters = [];
for (const key in this.S) {
const data = this.S[key];
if (data.live)
liveSpecialMonsters.push(key);
}
// Delete monsters that we should be able to see
EntityModel.aggregate([
{
$match: {
in: this.in,
map: this.map,
name: { $nin: visibleIDs },
serverIdentifier: this.serverData.name,
serverRegion: this.serverData.region,
type: { $nin: liveSpecialMonsters },
},
},
{
$project: {
distance: {
$sqrt: {
$add: [
{ $pow: [{ $subtract: [this.y, "$y"] }, 2] },
{ $pow: [{ $subtract: [this.x, "$x"] }, 2] },
],
},
},
},
},
{
$match: {
distance: {
$lt: Constants.MAX_VISIBLE_RANGE / 2,
},
},
},
])
.exec()
.then((toDeletes) => {
try {
const ids = [];
for (const toDelete of toDeletes)
ids.push(toDelete._id);
if (ids.length)
EntityModel.deleteMany({
_id: { $in: ids },
serverIdentifier: this.serverData.name,
serverRegion: this.serverData.region,
})
.lean()
.exec()
.catch(console.error);
}
catch (e) {
console.error(e);
}
})
.catch(console.error);
}
}
}
parseNewMap(data) {
this.projectiles.clear();
this.x = data.x;
this.y = data.y;
this.map = data.name;
this.in = data.in;
if (data.in !== data.name && // We're in an instance
data.effect !== "magiport" && // Don't update for magiports
data.effect !== "blink" && // Don't update for blinks
data.effect !== 1 // Don't update for town warps
) {
const now = Date.now();
InstanceModel.updateOne({
in: data.in,
map: data.name,
serverIdentifier: this.serverData.name,
serverRegion: this.serverData.region,
}, {
$max: {
lastEntered: now,
},
$min: {
firstEntered: now,
},
}, { upsert: true })
.lean()
.exec()
.catch(() => {
/* Suppress errors */
});
}
this.parseEntities(data.entities);
}
updatePositions() {
if (this.lastPositionUpdate) {
const msSinceLastUpdate = Date.now() - this.lastPositionUpdate;
if (msSinceLastUpdate == 0)
return;
// Update entities
for (const [, entity] of this.entities) {
if (!entity.moving)
continue;
const speed = entity.speed;
const distanceTraveled = (speed * msSinceLastUpdate) / 1000;
const angle = Math.atan2(entity.going_y - entity.y, entity.going_x - entity.x);
const distanceToGoal = Tools.distance({ x: entity.x, y: entity.y }, { x: entity.going_x, y: entity.going_y });
if (distanceTraveled > distanceToGoal) {
entity.moving = false;
entity.x = entity.going_x;
entity.y = entity.going_y;
}
else {
entity.x = entity.x + Math.cos(angle) * distanceTraveled;
entity.y = entity.y + Math.sin(angle) * distanceTraveled;
}
// Update conditions
for (const condition in entity.s) {
const newCooldown = entity.s[condition].ms - msSinceLastUpdate;
if (newCooldown <= 0)
delete entity.s[condition];
else
entity.s[condition].ms = newCooldown;
}
}
// Update players
for (const [, player] of this.players) {
if (!player.moving)
continue;
const distanceTraveled = (player.speed * msSinceLastUpdate) / 1000;
const angle = Math.atan2(player.going_y - player.y, player.going_x - player.x);
const distanceToGoal = Tools.distance({ x: player.x, y: player.y }, { x: player.going_x, y: player.going_y });
if (distanceTraveled > distanceToGoal) {
player.moving = false;
player.x = player.going_x;
player.y = player.going_y;
}
else {
player.x = player.x + Math.cos(angle) * distanceTraveled;
player.y = player.y + Math.sin(angle) * distanceTraveled;
}
// Update conditions
for (const condition in player.s) {
const newCooldown = player.s[condition].ms - msSinceLastUpdate;
if (newCooldown <= 0)
delete player.s[condition];
else
player.s[condition].ms = newCooldown;
}
// Update processes
for (const process in player.q) {
const newCooldown = player.q[process].ms - msSinceLastUpdate;
if (newCooldown <= 0)
delete player.q[process];
else
player.q[process].ms = newCooldown;
}
// Update channels
for (const channel in player.c) {
const newCooldown = player.c[channel].ms - msSinceLastUpdate;
if (newCooldown <= 0)
delete player.c[channel];
else
player.c[channel].ms = newCooldown;
}
}
}
// Erase all entities that are far away
let toDelete = [];
for (const [id, entity] of this.entities) {
if (Tools.squaredDistance(this, entity) < Constants.MAX_VISIBLE_RANGE_SQUARED)
continue;
toDelete.push(id);
}
for (const id of toDelete)
this.deleteEntity(id);
// Erase all players that are far away
toDelete = [];
for (const [id, player] of this.players) {
if (Tools.squaredDistance(this, player) < Constants.MAX_VISIBLE_RANGE_SQUARED)
continue;
toDelete.push(id);
}
for (const id of toDelete)
this.players.delete(id);
// Erase all stale projectiles
for (const [id, projectile] of this.projectiles) {
if (Date.now() - projectile.date.getTime() > Constants.STALE_PROJECTILE_MS)
this.projectiles.delete(id);
}
this.lastPositionUpdate = Date.now();
}
async sendPing(log = true) {
// Get the next pingID
const pingID = this.pingNum.toString();
this.pingNum++;
// Set the pingID in the map
this.pingMap.set(pingID, { log: log, time: Date.now() });
const promise = new Promise((resolve, reject) => {
const cleanup = () => {
this.socket.off("ping_ack", pingListener);
clearTimeout(timeout);
};
const pingListener = (data) => {
if (data.id == pingID) {
const time = Date.now() - this.pingMap.get(pingID)?.time;
cleanup();
resolve(time);
}
};
const timeout = setTimeout(() => {
cleanup();
reject(new Error("ping timeout (5000ms)"));
}, 5000);
this.socket.on("ping_ack", pingListener);
});
// Get the ping
this.socket.emit("ping_trig", { id: pingID });
return promise;
}
}
//# sourceMappingURL=Observer.js.map