Source: entitycollection.js

/**
 * @class
 * @summary A sequence of entities with filtering and tasking
 *          capabilities.
 * @param sharedAI {API3.SharedScript} The shared data container which
 *                                     holds all of the entities.
 */
API3.EntityCollection = function(sharedAI, entities, filters)
{
	this._ai = sharedAI;
	this._entities = entities || new Map();
	this._filters = filters || [];
	this.dynamicProp = [];
	for each (var filter in this._filters)
		this.dynamicProp = this.dynamicProp.concat(filter.dynamicProperties);
	
	Object.defineProperty(this, "length", {
		get: function () {
			return this._entities.size;
		}
	});
	this.frozen = false;
	var themap = this._entities;
	this._entities.forEach =
		function (callback) {
			try {
				var keys = themap.keys();

				do {
					var key = keys.next();
					callback(themap.get(key), key);
				} while(1);
			}
			catch (e) {
			}
		};
};

API3.EntityCollection.prototype.Serialize = function()
{
	var filters = [];
	for each (var f in this._filters)
		filters.push(uneval(f));
	return {
		"ents": this.toIdArray(),
		"frozen": this.frozen,
		"filters": filters
	};
};

API3.EntityCollection.prototype.Deserialize = function(data, sharedAI)
{
	this._ai = sharedAI;
	for (var i = 0; i < data.ents.length; i++) {
		let id = data.ents[i];
		this._entities.set(id, sharedAI._entities.get(id));
	}

	for (var i = 0; i <  data.filters.length; i++) {
		this._filters.push(eval(data.filters[i]));
	}

	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.
API3.EntityCollection.prototype.freeze = function()
{
	this.frozen = true;
};
API3.EntityCollection.prototype.defreeze = function()
{
	this.frozen = false;
};

API3.EntityCollection.prototype.toIdArray = function()
{
	let ret = [];
	this._entities.forEach(
		function (value, key) {
			ret.push(key);
		});
	return ret;
};

API3.EntityCollection.prototype.toEntityArray = function()
{
	let ret = [];
	this._entities.forEach(
		function (value, key) {
			ret.push(value);
		});
	return ret;
};

API3.EntityCollection.prototype.values = function()
{
	return this._entities.values();
};

API3.EntityCollection.prototype.toString = function()
{
	return "[EntityCollection " + this.toEntityArray().join(" ") + "]";
};

/**
 * @summary Derives a new collection which contains only selected
 *          entities.
 * @description The current collection is not affected by the
 *              filtering.
 * @param filter {API3.Filter} A filter object which chooses the
 *                             entities to go into the derived
 *                             collection. Only entities found in the
 *                             current collection will be filtered
 *                             into the derived one. It is possible
 *                             to supply a "naked" decision function
 *                             as well (see {@link API3.Filter.func}).
 * @return {API3.EntityCollection} A new collection which contains
 *                                 only those entities of the current
 *                                 collection which match the filter
 *                                 criterion. The new collection is
 *                                 updated as the current collection
 *                                 changes.
 * @todo Describe the thisp parameter.
 * @example
 * var wonders = gameState.getEnemyStructures().filter(
 *                   API3.Filters.byClass("Wonder"));
 */
API3.EntityCollection.prototype.filter = function(filter, thisp)
{
	if (typeof(filter) == "function")
		filter = {"func": filter, "dynamicProperties": []};
	
	var ret = new Map();
	this._entities.forEach(
		function (ent, id) {
			if (filter.func.call(thisp, ent, id, this))
				ret.set(id, ent);
		});
	
	return new API3.EntityCollection(this._ai, ret, this._filters.concat([filter]));
};

/**
 * @summary Returns the (at most) n entities nearest to targetPos.
 * @description Entities are selected by their euclidian distance to
 *              the target pos.<br/>
 *
 *              <strong>Warning:</strong> As the function considers
 *              the euclidian distance only, it is dangerous to use
 *              it for selection of unit around a target spot, as it
 *              toes not take into account obstructions on the path
 *              between unit and target.
 * @param targetPos {Array.<Number>} Describes the position to start
 *                                   searching for entities, where
 *                                   <code>targetPos[0]</code> gives
 *                                   the x-position and
 *                                   <code>targetPos[1]</code> gives
 *                                   the z-position.
 * @param n {Number} Maximum number of entities to return.
 * @return {API3.EntityCollection} A new collection which holds only
 *                                 the nearest entities to the target
 *                                 spot. The collection will
 *                                 <strong>not</strong> auto-update
 *                                 when entities change position.
 */
API3.EntityCollection.prototype.filterNearest = function(targetPos, n)
{
	// Compute the distance of each entity
	var data = []; // [ [id, ent, distance], ... ]
	this._entities.forEach(
		function (ent, id) {
			if (ent.position())
				data.push([id, ent, API3.SquareVectorDistance(targetPos, ent.position())]);
		});

	// Sort by increasing distance
	data.sort(function (a, b) { return (a[2] - b[2]); });
	if (n === undefined)
		n = data.length;
	else
		n = Math.min(n, data.length);

	// Extract the first n
	let ret = new Map();
	for (let i = 0; i < n; ++i)
		ret.set(data[i][0], data[i][1]);

	return new API3.EntityCollection(this._ai, ret);
};

API3.EntityCollection.prototype.filter_raw = function(callback, thisp)
{
	var ret = new Map();
	this._entities.forEach(
		function (ent, id) {
			let val = ent._entity;
			if (callback.call(thisp, val, id, this))
				ret.set(id, ent);
		});
	return new API3.EntityCollection(this._ai, ret);
};

API3.EntityCollection.prototype.forEach = function(callback)
{
	this._entities.forEach(
		function (value, key) {
			callback(value);
		});
	return this;
};

API3.EntityCollection.prototype.move = function(x, z, queued)
{
	queued = queued || false;
	Engine.PostCommand(PlayerID,{"type": "walk", "entities": this.toIdArray(), "x": x, "z": z, "queued": queued});
	return this;
};

API3.EntityCollection.prototype.attackMove = function(x, z, targetClasses, queued)
{
	queued = queued || false;
	Engine.PostCommand(PlayerID,{"type": "attack-walk", "entities": this.toIdArray(), "x": x, "z": z, "targetClasses": targetClasses, "queued": queued});
	return this;
};

API3.EntityCollection.prototype.moveIndiv = function(x, z, queued)
{
	queued = queued || false;
	this._entities.forEach(
		function (entity, id) {
			// The following try {} finally {} block is a workaround for OOS problems in multiplayer games with AI players (check ticket #2000).
			// It disables JIT compiling of this loop. Don't remove it unless you are sure that the underlying issue has been resolved!
			// TODO: Check this again after the SpiderMonkey upgrade.
			try {} finally {}
			Engine.PostCommand(PlayerID,{"type": "walk", "entities": [id], "x": x, "z": z, "queued": queued});
		});
	return this;
};

API3.EntityCollection.prototype.garrison = function(target)
{
	Engine.PostCommand(PlayerID,{"type": "garrison", "entities": this.toIdArray(), "target": target.id()});
	return this;
};

API3.EntityCollection.prototype.destroy = function()
{
	Engine.PostCommand(PlayerID,{"type": "delete-entities", "entities": this.toIdArray()});
	return this;
};

API3.EntityCollection.prototype.attack = function(unit)
{
	var unitId;
	if (typeof(unit) === "Entity")
		unitId = unit.id();
	else
		unitId = unit;
	Engine.PostCommand(PlayerID,{"type": "attack", "entities": this.toIdArray(), "target": unitId, "queued": false});
	return this;
};

// violent, aggressive, defensive, passive, standground
API3.EntityCollection.prototype.setStance = function(stance)
{
	Engine.PostCommand(PlayerID,{"type": "stance", "entities": this.toIdArray(), "name" : stance, "queued": false});
	return this;
};

// Returns the average position of all units
API3.EntityCollection.prototype.getCentrePosition = function()
{
	var sumPos = [0, 0];
	var count = 0;
	this._entities.forEach(
		function (ent, id) {
			if (ent.position()) {
				sumPos[0] += ent.position()[0];
				sumPos[1] += ent.position()[1];
				count ++;
			}
		});

	if (count === 0)
		return undefined;
	else
		return [sumPos[0]/count, sumPos[1]/count];
};

// 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.
API3.EntityCollection.prototype.getApproximatePosition = function(sample)
{
	var sumPos = [0, 0];
	var i = 0;
	var loopActive = true;
	this._entities.forEach(
		function (ent, id) {
			if (loopActive && ent.position()) {
				sumPos[0] += ent.position()[0];
				sumPos[1] += ent.position()[1];
				i++;
				if (i === sample)
					loopActive = false;
			}
		});
	if (i === 0)
		return undefined;
	else
		return [sumPos[0]/i, sumPos[1]/i];
};


// Removes an entity from the collection, returns true if the entity was a member, false otherwise
API3.EntityCollection.prototype.removeEnt = function(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
API3.EntityCollection.prototype.addEnt = function(ent)
{
	if (this._entities.has(ent.id()))
		return false;
	this._entities.set(ent.id(), ent);
	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.
API3.EntityCollection.prototype.updateEnt = function(ent, force)
{	
	var passesFilters = true;
	for each (let filter in this._filters)
		passesFilters = passesFilters && filter.func(ent);

	if (passesFilters)
	{
		if (!force && this.frozen)
			return false;
		return this.addEnt(ent);
	}
	else
		return this.removeEnt(ent);
};

API3.EntityCollection.prototype.registerUpdates = function()
{
	this._ai.registerUpdatingEntityCollection(this);
};

API3.EntityCollection.prototype.unregister = function()
{
	this._ai.removeUpdatingEntityCollection(this);
};

API3.EntityCollection.prototype.dynamicProperties = function()
{
	return this.dynamicProp;
};

API3.EntityCollection.prototype.setUID = function(id)
{
	this._UID = id;
};

API3.EntityCollection.prototype.getUID = function()
{
	return this._UID;
};

// -------------------------------------------------------------------
//  End of file
// -------------------------------------------------------------------