0ad/binaries/data/mods/public/simulation/ai/common-api/entitycollection.js

372 lines
7.4 KiB
JavaScript
Raw Normal View History

import { SquareVectorDistance } from "simulation/ai/common-api/utils.js";
2026-07-13 10:47:21 -07:00
export class EntityCollection
{
2026-07-13 10:47:21 -07:00
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;
}
2026-07-13 10:47:21 -07:00
Serialize()
{
const filters = [];
for (const f of this._filters)
filters.push(uneval(f));
return {
"ents": this.toIdArray(),
"frozen": this.frozen,
"filters": filters
};
}
2026-07-13 10:47:21 -07:00
Deserialize(data, sharedAI)
{
this._ai = sharedAI;
for (const id of data.ents)
this._entities.set(id, sharedAI._entities.get(id));
2026-07-13 10:47:21 -07:00
for (const f of data.filters)
this._filters.push(eval(f));
2026-07-13 10:47:21 -07:00
if (data.frozen)
this.freeze();
else
this.defreeze();
}
2026-07-13 10:47:21 -07:00
/**
* 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;
}
2026-07-13 10:47:21 -07:00
defreeze()
{
this.frozen = false;
}
2026-07-13 10:47:21 -07:00
toIdArray()
{
return Array.from(this._entities.keys());
}
2026-07-13 10:47:21 -07:00
toEntityArray()
{
return Array.from(this._entities.values());
}
2026-07-13 10:47:21 -07:00
values()
{
return this._entities.values();
}
2026-07-13 10:47:21 -07:00
toString()
{
return "[EntityCollection " + this.toEntityArray().join(" ") + "]";
}
2026-07-13 10:47:21 -07:00
filter(filter, thisp)
{
if (typeof filter === "function")
filter = { "func": filter, "dynamicProperties": [] };
2026-07-13 10:47:21 -07:00
const ret = new Map();
for (const [id, ent] of this._entities)
if (filter.func.call(thisp, ent, id, this))
ret.set(id, ent);
2026-07-13 10:47:21 -07:00
return new EntityCollection(this._ai, ret, this._filters.concat([filter]));
}
2026-07-13 10:47:21 -07:00
/**
* 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);
}
2026-07-13 10:47:21 -07:00
filter_raw(callback, thisp)
{
2026-07-13 10:47:21 -07:00
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);
}
2026-07-13 10:47:21 -07:00
forEach(callback)
{
for (const ent of this._entities.values())
callback(ent);
return this;
}
2026-07-13 10:47:21 -07:00
hasEntities()
{
return this._entities.size !== 0;
}
2026-07-13 10:47:21 -07:00
move(x, z, queued = false, pushFront = false)
{
Engine.PostCommand(PlayerID, {
"type": "walk",
2026-07-13 10:47:21 -07:00
"entities": this.toIdArray(),
"x": x,
"z": z,
"queued": queued,
"pushFront": pushFront
});
2026-07-13 10:47:21 -07:00
return this;
}
2026-07-13 10:47:21 -07:00
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;
}
2026-07-13 10:47:21 -07:00
attackMove(x, z, targetClasses, allowCapture = true, queued = false, pushFront = false)
{
2026-07-13 10:47:21 -07:00
Engine.PostCommand(PlayerID, {
"type": "attack-walk",
"entities": this.toIdArray(),
"x": x,
"z": z,
"targetClasses": targetClasses,
"allowCapture": allowCapture,
"queued": queued,
"pushFront": pushFront
});
return this;
}
2026-07-13 10:47:21 -07:00
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;
}
2026-07-13 10:47:21 -07:00
garrison(target, queued = false, pushFront = false)
{
Engine.PostCommand(PlayerID, {
"type": "garrison",
"entities": this.toIdArray(),
"target": target.id(),
"queued": queued,
"pushFront": pushFront
});
return this;
}
2026-07-13 10:47:21 -07:00
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;
}
2026-07-13 10:47:21 -07:00
/** violent, aggressive, defensive, passive, standground */
setStance(stance)
{
2026-07-13 10:47:21 -07:00
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;
2026-07-13 10:47:21 -07:00
this._entities.delete(ent.id());
return true;
}
2026-07-13 10:47:21 -07:00
/** 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;
}
2026-07-13 10:47:21 -07:00
/**
* 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);
}
2026-07-13 10:47:21 -07:00
registerUpdates()
{
this._ai.registerUpdatingEntityCollection(this);
}
2026-07-13 10:47:21 -07:00
unregister()
{
this._ai.removeUpdatingEntityCollection(this);
}
2026-07-13 10:47:21 -07:00
dynamicProperties()
{
return this.dynamicProp;
}
2026-07-13 10:47:21 -07:00
setUID(id)
{
this._UID = id;
}
getUID()
{
return this._UID;
}
}