import { SquareVectorDistance } from "simulation/ai/common-api/utils.js"; export class EntityCollection { constructor(sharedAI, entities = new Map(), filters = []) { this._ai = sharedAI; this._entities = entities; this._filters = filters; this.dynamicProp = []; for (const filter of this._filters) if (filter.dynamicProperties.length) this.dynamicProp = this.dynamicProp.concat(filter.dynamicProperties); Object.defineProperty(this, "length", { "get": () => this._entities.size }); this.frozen = false; } Serialize() { const filters = []; for (const f of this._filters) filters.push(uneval(f)); return { "ents": this.toIdArray(), "frozen": this.frozen, "filters": filters }; } Deserialize(data, sharedAI) { this._ai = sharedAI; for (const id of data.ents) this._entities.set(id, sharedAI._entities.get(id)); for (const f of data.filters) this._filters.push(eval(f)); if (data.frozen) this.freeze(); else this.defreeze(); } /** * If an entitycollection is frozen, it will never automatically add a unit. * But can remove one. * this makes it easy to create entity collection that will auto-remove dead units * but never add new ones. */ freeze() { this.frozen = true; } defreeze() { this.frozen = false; } toIdArray() { return Array.from(this._entities.keys()); } toEntityArray() { return Array.from(this._entities.values()); } values() { return this._entities.values(); } toString() { return "[EntityCollection " + this.toEntityArray().join(" ") + "]"; } filter(filter, thisp) { if (typeof filter === "function") filter = { "func": filter, "dynamicProperties": [] }; const ret = new Map(); for (const [id, ent] of this._entities) if (filter.func.call(thisp, ent, id, this)) ret.set(id, ent); return new EntityCollection(this._ai, ret, this._filters.concat([filter])); } /** * Returns the (at most) n entities nearest to targetPos. */ filterNearest(targetPos, n) { // Compute the distance of each entity const data = []; // [ [id, ent, distance], ... ] for (const [id, ent] of this._entities) if (ent.position()) data.push([id, ent, SquareVectorDistance(targetPos, ent.position())]); // Sort by increasing distance data.sort((a, b) => a[2] - b[2]); if (n === undefined) n = data.length; else n = Math.min(n, data.length); // Extract the first n const ret = new Map(); for (let i = 0; i < n; ++i) ret.set(data[i][0], data[i][1]); return new EntityCollection(this._ai, ret); } filter_raw(callback, thisp) { const ret = new Map(); for (const [id, ent] of this._entities) { const val = ent._entity; if (callback.call(thisp, val, id, this)) ret.set(id, ent); } return new EntityCollection(this._ai, ret); } forEach(callback) { for (const ent of this._entities.values()) callback(ent); return this; } hasEntities() { return this._entities.size !== 0; } move(x, z, queued = false, pushFront = false) { Engine.PostCommand(PlayerID, { "type": "walk", "entities": this.toIdArray(), "x": x, "z": z, "queued": queued, "pushFront": pushFront }); return this; } moveToRange(x, z, min, max, queued = false, pushFront = false) { Engine.PostCommand(PlayerID, { "type": "walk-to-range", "entities": this.toIdArray(), "x": x, "z": z, "min": min, "max": max, "queued": queued, "pushFront": pushFront }); return this; } attackMove(x, z, targetClasses, allowCapture = true, queued = false, pushFront = false) { Engine.PostCommand(PlayerID, { "type": "attack-walk", "entities": this.toIdArray(), "x": x, "z": z, "targetClasses": targetClasses, "allowCapture": allowCapture, "queued": queued, "pushFront": pushFront }); return this; } moveIndiv(x, z, queued = false, pushFront = false) { for (const id of this._entities.keys()) Engine.PostCommand(PlayerID, { "type": "walk", "entities": [id], "x": x, "z": z, "queued": queued, "pushFront": pushFront }); return this; } garrison(target, queued = false, pushFront = false) { Engine.PostCommand(PlayerID, { "type": "garrison", "entities": this.toIdArray(), "target": target.id(), "queued": queued, "pushFront": pushFront }); return this; } occupyTurret(target, queued = false, pushFront = false) { Engine.PostCommand(PlayerID, { "type": "occupy-turret", "entities": this.toIdArray(), "target": target.id(), "queued": queued, "pushFront": pushFront }); return this; } destroy() { Engine.PostCommand(PlayerID, { "type": "delete-entities", "entities": this.toIdArray() }); return this; } attack(unitId, queued = false, pushFront = false) { Engine.PostCommand(PlayerID, { "type": "attack", "entities": this.toIdArray(), "target": unitId, "queued": queued, "pushFront": pushFront }); return this; } /** violent, aggressive, defensive, passive, standground */ setStance(stance) { Engine.PostCommand(PlayerID, { "type": "stance", "entities": this.toIdArray(), "name": stance }); return this; } /** Returns the average position of all units */ getCentrePosition() { const sumPos = [0, 0]; let count = 0; for (const ent of this._entities.values()) { if (!ent.position()) continue; sumPos[0] += ent.position()[0]; sumPos[1] += ent.position()[1]; count++; } return count ? [sumPos[0]/count, sumPos[1]/count] : undefined; } /** * returns the average position from the sample first units. * This might be faster for huge collections, but there's * always a risk that it'll be unprecise. */ getApproximatePosition(sample) { const sumPos = [0, 0]; let i = 0; for (const ent of this._entities.values()) { if (!ent.position()) continue; sumPos[0] += ent.position()[0]; sumPos[1] += ent.position()[1]; i++; if (i === sample) break; } return i ? [sumPos[0]/i, sumPos[1]/i] : undefined; } hasEntId(id) { return this._entities.has(id); } /** Removes an entity from the collection, returns true if the entity was a member, false otherwise */ removeEnt(ent) { if (!this._entities.has(ent.id())) return false; this._entities.delete(ent.id()); return true; } /** Adds an entity to the collection, returns true if the entity was not member, false otherwise */ addEnt(ent) { if (this._entities.has(ent.id())) return false; this._entities.set(ent.id(), ent); const temp = this.toEntityArray(); temp.sort((a, b) => a.id() - b.id()); this._entities.clear(); for (const e of temp) this._entities.set(e.id(), e); return true; } /** * Checks the entity against the filters, and adds or removes it appropriately, returns true if the * entity collection was modified. * Force can add a unit despite a freezing. * If an entitycollection is frozen, it will never automatically add a unit. * But can remove one. */ updateEnt(ent, force) { let passesFilters = true; for (const filter of this._filters) passesFilters = passesFilters && filter.func(ent); if (passesFilters) { if (!force && this.frozen) return false; return this.addEnt(ent); } return this.removeEnt(ent); } registerUpdates() { this._ai.registerUpdatingEntityCollection(this); } unregister() { this._ai.removeUpdatingEntityCollection(this); } dynamicProperties() { return this.dynamicProp; } setUID(id) { this._UID = id; } getUID() { return this._UID; } }