0ad/binaries/data/mods/public/simulation/ai/common-api/entity.js
mehmed-faheim-arslan 39b1311fac Allow players to set rally points on allied buildings
Instead of storing a single flat list of positions and data per
building, RallyPoint now stores them keyed by player ID. This lets
mutual allies independently set and display rally points on each
other's structures.

The GUI now allows selecting allied buildings with a rally point
and only shows the viewing player's own rally point data.
GuiInterface gets an OnUpdate handler to keep displayed positions
in sync when rally point targets move.

GetRallyPointCommands now takes raw position and data arrays instead
of a component reference. The network command field is also renamed
from "entities" to "structures".

Fixes #3115
2026-06-10 00:09:48 +02:00

1094 lines
30 KiB
JavaScript

import { Class } from "simulation/ai/common-api/class.js";
import { VectorDistance } from "simulation/ai/common-api/utils.js";
// defines a template.
export const Template = Class({
"_init": function(sharedAI, templateName, template)
{
this._templateName = templateName;
this._template = template;
// save a reference to the template tech modifications
if (!sharedAI._templatesModifications[this._templateName])
sharedAI._templatesModifications[this._templateName] = {};
this._templateModif = sharedAI._templatesModifications[this._templateName];
this._tpCache = new Map();
},
// Helper function to return a template value, adjusting for tech.
"get": function(string)
{
if (this._entityModif && this._entityModif.has(string))
return this._entityModif.get(string);
else if (this._templateModif)
{
const owner = this._entity ? this._entity.owner : PlayerID;
if (this._templateModif[owner] && this._templateModif[owner].has(string))
return this._templateModif[owner].get(string);
}
if (!this._tpCache.has(string))
{
let value = this._template;
const args = string.split("/");
for (const arg of args)
{
value = value[arg];
if (value == undefined)
break;
}
this._tpCache.set(string, value);
}
return this._tpCache.get(string);
},
"templateName": function() { return this._templateName; },
"genericName": function() { return this.get("Identity/GenericName"); },
"civ": function() { return this.get("Identity/Civ"); },
"matchLimit": function()
{
if (!this.get("TrainingRestrictions"))
return undefined;
return this.get("TrainingRestrictions/MatchLimit");
},
"classes": function()
{
const template = this.get("Identity");
if (!template)
return undefined;
return GetIdentityClasses(template);
},
"hasClass": function(name)
{
if (!this._classes)
this._classes = this.classes();
return this._classes && this._classes.indexOf(name) != -1;
},
"hasClasses": function(array)
{
if (!this._classes)
this._classes = this.classes();
return this._classes && MatchesClassList(this._classes, array);
},
"requirements": function()
{
return this.get("Identity/Requirements");
},
"available": function(gameState)
{
const requirements = this.requirements();
return !requirements || Sim.RequirementsHelper.AreRequirementsMet(requirements, PlayerID);
},
"cost": function(productionQueue)
{
if (!this.get("Cost"))
return {};
const ret = {};
for (const type in this.get("Cost/Resources"))
ret[type] = +this.get("Cost/Resources/" + type);
return ret;
},
"costSum": function(productionQueue)
{
const cost = this.cost(productionQueue);
if (!cost)
return 0;
let ret = 0;
for (const type in cost)
ret += cost[type];
return ret;
},
"techCostMultiplier": function(type)
{
return +(this.get("Researcher/TechCostMultiplier/"+type) || 1);
},
/**
* Returns { "max": max, "min": min } or undefined if no obstruction.
* max: radius of the outer circle surrounding this entity's obstruction shape
* min: radius of the inner circle
*/
"obstructionRadius": function()
{
if (!this.get("Obstruction"))
return undefined;
if (this.get("Obstruction/Static"))
{
const w = +this.get("Obstruction/Static/@width");
const h = +this.get("Obstruction/Static/@depth");
return { "max": Math.sqrt(w * w + h * h) / 2, "min": Math.min(h, w) / 2 };
}
if (this.get("Obstruction/Unit"))
{
const r = +this.get("Obstruction/Unit/@radius");
return { "max": r, "min": r };
}
const right = this.get("Obstruction/Obstructions/Right");
const left = this.get("Obstruction/Obstructions/Left");
if (left && right)
{
const w = +right["@x"] + right["@width"] / 2 - left["@x"] + left["@width"] / 2;
const h = Math.max(+right["@z"] + right["@depth"] / 2, +left["@z"] + left["@depth"] / 2) -
Math.min(+right["@z"] - right["@depth"] / 2, +left["@z"] - left["@depth"] / 2);
return { "max": Math.sqrt(w * w + h * h) / 2, "min": Math.min(h, w) / 2 };
}
return { "max": 0, "min": 0 }; // Units have currently no obstructions
},
/**
* Returns the radius of a circle surrounding this entity's footprint.
*/
"footprintRadius": function()
{
if (!this.get("Footprint"))
return undefined;
if (this.get("Footprint/Square"))
{
const w = +this.get("Footprint/Square/@width");
const h = +this.get("Footprint/Square/@depth");
return Math.sqrt(w * w + h * h) / 2;
}
if (this.get("Footprint/Circle"))
return +this.get("Footprint/Circle/@radius");
return 0; // this should never happen
},
"maxHitpoints": function() { return +(this.get("Health/Max") || 0); },
"isHealable": function()
{
if (this.get("Health") !== undefined)
return this.get("Health/Unhealable") !== "true";
return false;
},
"isRepairable": function() { return this.get("Repairable") !== undefined; },
"getPopulationBonus": function()
{
if (!this.get("Population"))
return 0;
return +this.get("Population/Bonus");
},
"resistanceStrengths": function()
{
const resistanceTypes = this.get("Resistance");
if (!resistanceTypes || !resistanceTypes.Entity)
return undefined;
const resistance = {};
if (resistanceTypes.Entity.Capture)
resistance.Capture = +this.get("Resistance/Entity/Capture");
if (resistanceTypes.Entity.Damage)
{
resistance.Damage = {};
for (const damageType in resistanceTypes.Entity.Damage)
resistance.Damage[damageType] = +this.get("Resistance/Entity/Damage/" + damageType);
}
// ToDo: Resistance to StatusEffects.
return resistance;
},
"attackTypes": function()
{
const attack = this.get("Attack");
if (!attack)
return undefined;
const ret = [];
for (const type in attack)
ret.push(type);
return ret;
},
"attackRange": function(type)
{
if (!this.get("Attack/" + type))
return undefined;
return {
"max": +this.get("Attack/" + type +"/MaxRange"),
"min": +(this.get("Attack/" + type +"/MinRange") || 0)
};
},
"attackStrengths": function(type)
{
const attackDamageTypes = this.get("Attack/" + type + "/Damage");
if (!attackDamageTypes)
return undefined;
const damage = {};
for (const damageType in attackDamageTypes)
damage[damageType] = +attackDamageTypes[damageType];
return damage;
},
"captureStrength": function()
{
if (!this.get("Attack/Capture"))
return undefined;
return +this.get("Attack/Capture/Capture") || 0;
},
"attackTimes": function(type)
{
if (!this.get("Attack/" + type))
return undefined;
return {
"prepare": +(this.get("Attack/" + type + "/PrepareTime") || 0),
"repeat": +(this.get("Attack/" + type + "/RepeatTime") || 1000)
};
},
// returns the classes this templates counters:
// Return type is [ [-neededClasses- , multiplier], … ].
"getCounteredClasses": function()
{
const attack = this.get("Attack");
if (!attack)
return undefined;
const Classes = [];
for (const type in attack)
{
const bonuses = this.get("Attack/" + type + "/Bonuses");
if (!bonuses)
continue;
for (const b in bonuses)
{
const bonusClasses = this.get("Attack/" + type + "/Bonuses/" + b + "/Classes");
if (bonusClasses)
Classes.push([bonusClasses.split(" "), +this.get("Attack/" + type +"/Bonuses/" + b +"/Multiplier")]);
}
}
return Classes;
},
// returns true if the entity counters the target entity.
// TODO: refine using the multiplier
"counters": function(target)
{
const attack = this.get("Attack");
if (!attack)
return false;
const mcounter = [];
for (const type in attack)
{
const bonuses = this.get("Attack/" + type + "/Bonuses");
if (!bonuses)
continue;
for (const b in bonuses)
{
const bonusClasses = this.get("Attack/" + type + "/Bonuses/" + b + "/Classes");
if (bonusClasses)
mcounter.concat(bonusClasses.split(" "));
}
}
return target.hasClasses(mcounter);
},
// returns, if it exists, the multiplier from each attack against a given class
"getMultiplierAgainst": function(type, againstClass)
{
if (!this.get("Attack/" + type +""))
return undefined;
const bonuses = this.get("Attack/" + type + "/Bonuses");
if (bonuses)
{
for (const b in bonuses)
{
const bonusClasses = this.get("Attack/" + type + "/Bonuses/" + b + "/Classes");
if (!bonusClasses)
continue;
for (const bcl of bonusClasses.split(" "))
if (bcl == againstClass)
return +this.get("Attack/" + type + "/Bonuses/" + b + "/Multiplier");
}
}
return 1;
},
"buildableEntities": function(civ)
{
const templates = this.get("Builder/Entities/_string");
if (!templates)
return [];
return templates.replace(/\{native\}/g, this.civ()).replace(/\{civ\}/g, civ).split(/\s+/);
},
"trainableEntities": function(civ)
{
const templates = this.get("Trainer/Entities/_string");
if (!templates)
return undefined;
return templates.replace(/\{native\}/g, this.civ()).replace(/\{civ\}/g, civ).split(/\s+/);
},
"researchableTechs": function(gameState, civ)
{
const templates = this.get("Researcher/Technologies/_string");
if (!templates)
return undefined;
const techs = templates.split(/\s+/);
for (let i = 0; i < techs.length; ++i)
{
const tech = techs[i];
if (tech.indexOf("{civ}") == -1)
continue;
const civTech = tech.replace("{civ}", civ);
techs[i] = TechnologyTemplates.Has(civTech) ?
civTech : tech.replace("{civ}", "generic");
}
return techs;
},
"resourceSupplyType": function()
{
if (!this.get("ResourceSupply"))
return undefined;
const [type, subtype] = this.get("ResourceSupply/Type").split('.');
return { "generic": type, "specific": subtype };
},
"getResourceType": function()
{
if (!this.get("ResourceSupply"))
return undefined;
return this.get("ResourceSupply/Type").split('.')[0];
},
"getDiminishingReturns": function() { return +(this.get("ResourceSupply/DiminishingReturns") || 1); },
"resourceSupplyMax": function() { return +this.get("ResourceSupply/Max"); },
"maxGatherers": function() { return +(this.get("ResourceSupply/MaxGatherers") || 0); },
"resourceGatherRates": function()
{
if (!this.get("ResourceGatherer"))
return undefined;
const ret = {};
const baseSpeed = +this.get("ResourceGatherer/BaseSpeed");
for (const r in this.get("ResourceGatherer/Rates"))
ret[r] = +this.get("ResourceGatherer/Rates/" + r) * baseSpeed;
return ret;
},
"resourceDropsiteTypes": function()
{
if (!this.get("ResourceDropsite"))
return undefined;
const types = this.get("ResourceDropsite/Types");
return types ? types.split(/\s+/) : [];
},
"isResourceDropsite": function(resourceType)
{
const types = this.resourceDropsiteTypes();
return types && (!resourceType || types.indexOf(resourceType) !== -1);
},
"isTreasure": function() { return this.get("Treasure") !== undefined; },
"treasureResources": function()
{
if (!this.get("Treasure"))
return undefined;
const ret = {};
for (const r in this.get("Treasure/Resources"))
ret[r] = +this.get("Treasure/Resources/" + r);
return ret;
},
"garrisonableClasses": function() { return this.get("GarrisonHolder/List/_string"); },
"garrisonMax": function() { return this.get("GarrisonHolder/Max"); },
"garrisonSize": function() { return this.get("Garrisonable/Size"); },
"garrisonEjectHealth": function() { return +this.get("GarrisonHolder/EjectHealth"); },
"getDefaultArrow": function() { return +this.get("BuildingAI/DefaultArrowCount"); },
"getArrowMultiplier": function() { return +this.get("BuildingAI/GarrisonArrowMultiplier"); },
"getGarrisonArrowClasses": function()
{
if (!this.get("BuildingAI"))
return undefined;
return this.get("BuildingAI/GarrisonArrowClasses").split(/\s+/);
},
"buffHeal": function() { return +this.get("GarrisonHolder/BuffHeal"); },
"promotion": function() { return this.get("Promotion/Entity"); },
"isPackable": function() { return this.get("Pack") != undefined; },
"isHuntable": function()
{
// Do not hunt retaliating animals (dead animals can be used).
// Assume entities which can attack, will attack.
return this.get("ResourceSupply/KillBeforeGather") &&
(!this.get("Health") || !this.get("Attack"));
},
"walkSpeed": function() { return +this.get("UnitMotion/WalkSpeed"); },
"trainingCategory": function() { return this.get("TrainingRestrictions/Category"); },
"buildTime": function(researcher)
{
let time = +this.get("Cost/BuildTime");
if (researcher)
time *= researcher.techCostMultiplier("time");
return time;
},
"buildCategory": function() { return this.get("BuildRestrictions/Category"); },
"buildDistance": function()
{
const distance = this.get("BuildRestrictions/Distance");
if (!distance)
return undefined;
const ret = {};
for (const key in distance)
ret[key] = this.get("BuildRestrictions/Distance/" + key);
return ret;
},
"buildPlacementType": function() { return this.get("BuildRestrictions/PlacementType"); },
"buildTerritories": function()
{
if (!this.get("BuildRestrictions"))
return undefined;
const territory = this.get("BuildRestrictions/Territory");
return !territory ? undefined : territory.split(/\s+/);
},
"hasBuildTerritory": function(territory)
{
const territories = this.buildTerritories();
return territories && territories.indexOf(territory) != -1;
},
"hasTerritoryInfluence": function()
{
return this.get("TerritoryInfluence") !== undefined;
},
"hasDefensiveFire": function()
{
if (!this.get("Attack/Ranged"))
return false;
return this.getDefaultArrow() || this.getArrowMultiplier();
},
"territoryInfluenceRadius": function()
{
if (this.get("TerritoryInfluence") !== undefined)
return +this.get("TerritoryInfluence/Radius");
return -1;
},
"territoryInfluenceWeight": function()
{
if (this.get("TerritoryInfluence") !== undefined)
return +this.get("TerritoryInfluence/Weight");
return -1;
},
"territoryDecayRate": function()
{
return +(this.get("TerritoryDecay/DecayRate") || 0);
},
"defaultRegenRate": function()
{
return +(this.get("Capturable/RegenRate") || 0);
},
"garrisonRegenRate": function()
{
return +(this.get("Capturable/GarrisonRegenRate") || 0);
},
"visionRange": function() { return +this.get("Vision/Range"); },
"gainMultiplier": function() { return +this.get("Trader/GainMultiplier"); },
"isBuilder": function() { return this.get("Builder") !== undefined; },
"isGatherer": function() { return this.get("ResourceGatherer") !== undefined; },
"canGather": function(type)
{
const gatherRates = this.get("ResourceGatherer/Rates");
if (!gatherRates)
return false;
for (const r in gatherRates)
if (r.split('.')[0] === type)
return true;
return false;
},
"isGarrisonHolder": function() { return this.get("GarrisonHolder") !== undefined; },
"isTurretHolder": function() { return this.get("TurretHolder") !== undefined; },
/**
* returns true if the tempalte can capture the given target entity
* if no target is given, returns true if the template has the Capture attack
*/
"canCapture": function(target)
{
if (!this.get("Attack/Capture"))
return false;
if (!target)
return true;
if (!target.get("Capturable"))
return false;
const restrictedClasses = this.get("Attack/Capture/RestrictedClasses/_string");
return !restrictedClasses || !target.hasClasses(restrictedClasses);
},
"isCapturable": function() { return this.get("Capturable") !== undefined; },
"canGuard": function() { return this.get("UnitAI/CanGuard") === "true"; },
"canGarrison": function() { return "Garrisonable" in this._template; },
"canOccupyTurret": function() { return "Turretable" in this._template; },
"isTreasureCollector": function() { return this.get("TreasureCollector") !== undefined; },
"hasUnitAI": function() { return this.get("UnitAI") !== undefined; },
});
// defines an entity, with a super Template.
// also redefines several of the template functions where the only change is applying aura and tech modifications.
export const Entity = Class({
"_super": Template,
"_init": function(sharedAI, entity)
{
this._super.call(this, sharedAI, entity.template, sharedAI.GetTemplate(entity.template));
this._entity = entity;
this._ai = sharedAI;
// save a reference to the template tech modifications
if (!sharedAI._templatesModifications[this._templateName])
sharedAI._templatesModifications[this._templateName] = {};
this._templateModif = sharedAI._templatesModifications[this._templateName];
// save a reference to the entity tech/aura modifications
if (!sharedAI._entitiesModifications.has(entity.id))
sharedAI._entitiesModifications.set(entity.id, new Map());
this._entityModif = sharedAI._entitiesModifications.get(entity.id);
},
"queryInterface": function(iid) { return SimEngine.QueryInterface(this.id(), iid); },
"toString": function() { return "[Entity " + this.id() + " " + this.templateName() + "]"; },
"id": function() { return this._entity.id; },
/**
* Returns extra data that the AI scripts have associated with this entity,
* for arbitrary local annotations.
* (This data should not be shared with any other AI scripts.)
*/
"getMetadata": function(player, key) { return this._ai.getMetadata(player, this, key); },
/**
* Sets extra data to be associated with this entity.
*/
"setMetadata": function(player, key, value) { this._ai.setMetadata(player, this, key, value); },
"deleteAllMetadata": function(player) { delete this._ai._entityMetadata[player][this.id()]; },
"deleteMetadata": function(player, key) { this._ai.deleteMetadata(player, this, key); },
"position": function() { return this._entity.position; },
"angle": function() { return this._entity.angle; },
"isIdle": function() { return this._entity.idle; },
"getStance": function() { return this._entity.stance; },
"unitAIState": function() { return this._entity.unitAIState; },
"unitAIOrderData": function() { return SimEngine.QueryInterface(this.id(), Sim.IID_UnitAI).GetOrders(); },
"hitpoints": function() { return this._entity.hitpoints; },
"isHurt": function() { return this.hitpoints() < this.maxHitpoints(); },
"healthLevel": function() { return this.hitpoints() / this.maxHitpoints(); },
"needsHeal": function() { return this.isHurt() && this.isHealable(); },
"needsRepair": function() { return this.isHurt() && this.isRepairable(); },
"decaying": function() { return this._entity.decaying; },
"capturePoints": function() {return this._entity.capturePoints; },
"isInvulnerable": function() { return this._entity.invulnerability || false; },
"isSharedDropsite": function() { return this._entity.sharedDropsite === true; },
/**
* Returns the current training queue state, of the form
* [ { "id": 0, "template": "...", "count": 1, "progress": 0.5, "metadata": ... }, ... ]
*/
"trainingQueue": function()
{
return this._entity.trainingQueue;
},
"trainingQueueTime": function()
{
const queue = this._entity.trainingQueue;
if (!queue)
return undefined;
let time = 0;
for (const item of queue)
time += item.timeRemaining;
return time / 1000;
},
"foundationProgress": function()
{
return this._entity.foundationProgress;
},
"getBuilders": function()
{
if (this._entity.foundationProgress === undefined)
return undefined;
if (this._entity.foundationBuilders === undefined)
return [];
return this._entity.foundationBuilders;
},
"getBuildersNb": function()
{
if (this._entity.foundationProgress === undefined)
return undefined;
if (this._entity.foundationBuilders === undefined)
return 0;
return this._entity.foundationBuilders.length;
},
"owner": function()
{
return this._entity.owner;
},
"isOwn": function(player)
{
if (typeof this._entity.owner === "undefined")
return false;
return this._entity.owner === player;
},
"resourceSupplyAmount": function()
{
return this.queryInterface(Sim.IID_ResourceSupply)?.GetCurrentAmount();
},
"resourceSupplyNumGatherers": function()
{
return this.queryInterface(Sim.IID_ResourceSupply)?.GetNumGatherers();
},
"isFull": function()
{
const numGatherers = this.resourceSupplyNumGatherers();
if (numGatherers)
return this.maxGatherers() === numGatherers;
return undefined;
},
"resourceCarrying": function()
{
return this.queryInterface(Sim.IID_ResourceGatherer)?.GetCarryingStatus();
},
"currentGatherRate": function()
{
// returns the gather rate for the current target if applicable.
if (!this.get("ResourceGatherer"))
return undefined;
if (this.unitAIOrderData().length &&
this.unitAIState().split(".")[1] == "GATHER")
{
let res;
// this is an abuse of "_ai" but it works.
if (this.unitAIState().split(".")[1] == "GATHER" && this.unitAIOrderData()[0].target !== undefined)
res = this._ai._entities.get(this.unitAIOrderData()[0].target);
else if (this.unitAIOrderData()[1] !== undefined && this.unitAIOrderData()[1].target !== undefined)
res = this._ai._entities.get(this.unitAIOrderData()[1].target);
if (!res)
return 0;
const type = res.resourceSupplyType();
if (!type)
return 0;
const tstring = type.generic + "." + type.specific;
let rate = +this.get("ResourceGatherer/BaseSpeed");
rate *= +this.get("ResourceGatherer/Rates/" +tstring);
if (rate)
return rate;
return 0;
}
return undefined;
},
"garrisonHolderID": function()
{
return this._entity.garrisonHolderID;
},
"garrisoned": function() { return this._entity.garrisoned; },
"garrisonedSlots": function()
{
let count = 0;
if (this._entity.garrisoned)
for (const ent of this._entity.garrisoned)
count += +this._ai._entities.get(ent).garrisonSize();
return count;
},
"canGarrisonInside": function()
{
return this.garrisonedSlots() < this.garrisonMax();
},
/**
* returns true if the entity can attack (including capture) the given class.
*/
"canAttackClass": function(aClass)
{
const attack = this.get("Attack");
if (!attack)
return false;
for (const type in attack)
{
if (type == "Slaughter")
continue;
const restrictedClasses = this.get("Attack/" + type + "/RestrictedClasses/_string");
if (!restrictedClasses || !MatchesClassList([aClass], restrictedClasses))
return true;
}
return false;
},
/**
* Derived from Attack.js' similary named function.
* @return {boolean} - Whether an entity can attack a given target.
*/
"canAttackTarget": function(target, allowCapture)
{
const attackTypes = this.get("Attack");
if (!attackTypes)
return false;
const canCapture = allowCapture && this.canCapture(target);
const health = target.get("Health");
if (!health)
return canCapture;
for (const type in attackTypes)
{
if (type == "Capture" ? !canCapture : target.isInvulnerable())
continue;
const restrictedClasses = this.get("Attack/" + type + "/RestrictedClasses/_string");
if (!restrictedClasses || !target.hasClasses(restrictedClasses))
return true;
}
return false;
},
"move": function(x, z, queued = false, pushFront = false)
{
Engine.PostCommand(PlayerID, { "type": "walk", "entities": [this.id()], "x": x, "z": z, "queued": queued, "pushFront": pushFront });
return this;
},
"moveToRange": function(x, z, min, max, queued = false, pushFront = false)
{
Engine.PostCommand(PlayerID, { "type": "walk-to-range", "entities": [this.id()], "x": x, "z": z, "min": min, "max": max, "queued": queued, "pushFront": pushFront });
return this;
},
"attackMove": function(x, z, targetClasses, allowCapture = true, queued = false, pushFront = false)
{
Engine.PostCommand(PlayerID, { "type": "attack-walk", "entities": [this.id()], "x": x, "z": z, "targetClasses": targetClasses, "allowCapture": allowCapture, "queued": queued, "pushFront": pushFront });
return this;
},
// violent, aggressive, defensive, passive, standground
"setStance": function(stance)
{
if (this.getStance() === undefined)
return undefined;
Engine.PostCommand(PlayerID, { "type": "stance", "entities": [this.id()], "name": stance });
return this;
},
"stopMoving": function()
{
Engine.PostCommand(PlayerID, { "type": "stop", "entities": [this.id()], "queued": false, "pushFront": false });
},
"unload": function(id)
{
if (!this.get("GarrisonHolder"))
return undefined;
Engine.PostCommand(PlayerID, { "type": "unload", "garrisonHolder": this.id(), "entities": [id] });
return this;
},
// Unloads all owned units, don't unload allies
"unloadAll": function()
{
if (!this.get("GarrisonHolder"))
return undefined;
Engine.PostCommand(PlayerID, { "type": "unload-all-by-owner", "garrisonHolders": [this.id()] });
return this;
},
"garrison": function(target, queued = false, pushFront = false)
{
Engine.PostCommand(PlayerID, { "type": "garrison", "entities": [this.id()], "target": target.id(), "queued": queued, "pushFront": pushFront });
return this;
},
"occupy-turret": function(target, queued = false, pushFront = false)
{
Engine.PostCommand(PlayerID, { "type": "occupy-turret", "entities": [this.id()], "target": target.id(), "queued": queued, "pushFront": pushFront });
return this;
},
"attack": function(unitId, allowCapture = true, queued = false, pushFront = false)
{
Engine.PostCommand(PlayerID, { "type": "attack", "entities": [this.id()], "target": unitId, "allowCapture": allowCapture, "queued": queued, "pushFront": pushFront });
return this;
},
"collectTreasure": function(target, queued = false, pushFront = false)
{
Engine.PostCommand(PlayerID, {
"type": "collect-treasure",
"entities": [this.id()],
"target": target.id(),
"queued": queued,
"pushFront": pushFront
});
return this;
},
// moveApart from a point in the opposite direction with a distance dist
"moveApart": function(point, dist)
{
if (this.position() !== undefined)
{
let direction = [this.position()[0] - point[0], this.position()[1] - point[1]];
const norm = VectorDistance(point, this.position());
if (norm === 0)
direction = [1, 0];
else
{
direction[0] /= norm;
direction[1] /= norm;
}
Engine.PostCommand(PlayerID, { "type": "walk", "entities": [this.id()], "x": this.position()[0] + direction[0]*dist, "z": this.position()[1] + direction[1]*dist, "queued": false, "pushFront": false });
}
return this;
},
// Flees from a unit in the opposite direction.
"flee": function(unitToFleeFrom)
{
if (this.position() !== undefined && unitToFleeFrom.position() !== undefined)
{
const FleeDirection = [this.position()[0] - unitToFleeFrom.position()[0],
this.position()[1] - unitToFleeFrom.position()[1]];
const dist = m.VectorDistance(unitToFleeFrom.position(), this.position());
FleeDirection[0] = 40 * FleeDirection[0] / dist;
FleeDirection[1] = 40 * FleeDirection[1] / dist;
Engine.PostCommand(PlayerID, { "type": "walk", "entities": [this.id()], "x": this.position()[0] + FleeDirection[0], "z": this.position()[1] + FleeDirection[1], "queued": false, "pushFront": false });
}
return this;
},
"gather": function(target, queued = false, pushFront = false)
{
Engine.PostCommand(PlayerID, { "type": "gather", "entities": [this.id()], "target": target.id(), "queued": queued, "pushFront": pushFront });
return this;
},
"repair": function(target, autocontinue = false, queued = false, pushFront = false)
{
Engine.PostCommand(PlayerID, { "type": "repair", "entities": [this.id()], "target": target.id(), "autocontinue": autocontinue, "queued": queued, "pushFront": pushFront });
return this;
},
"returnResources": function(target, queued = false, pushFront = false)
{
Engine.PostCommand(PlayerID, { "type": "returnresource", "entities": [this.id()], "target": target.id(), "queued": queued, "pushFront": pushFront });
return this;
},
"destroy": function()
{
Engine.PostCommand(PlayerID, { "type": "delete-entities", "entities": [this.id()] });
return this;
},
"barter": function(buyType, sellType, amount)
{
Engine.PostCommand(PlayerID, { "type": "barter", "sell": sellType, "buy": buyType, "amount": amount });
return this;
},
"tradeRoute": function(target, source)
{
Engine.PostCommand(PlayerID, { "type": "setup-trade-route", "entities": [this.id()], "target": target.id(), "source": source.id(), "route": undefined, "queued": false, "pushFront": false });
return this;
},
"setRallyPoint": function(target, command)
{
const data = { "command": command, "target": target.id() };
Engine.PostCommand(PlayerID, { "type": "set-rallypoint", "structures": [this.id()], "x": target.position()[0], "z": target.position()[1], "data": data });
return this;
},
"unsetRallyPoint": function()
{
Engine.PostCommand(PlayerID, { "type": "unset-rallypoint", "structures": [this.id()] });
return this;
},
"train": function(civ, type, count, metadata, pushFront = false)
{
const trainable = this.trainableEntities(civ);
if (!trainable)
{
error("Called train("+type+", "+count+") on non-training entity "+this);
return this;
}
if (trainable.indexOf(type) == -1)
{
error("Called train("+type+", "+count+") on entity "+this+" which can't train that");
return this;
}
Engine.PostCommand(PlayerID, {
"type": "train",
"entities": [this.id()],
"template": type,
"count": count,
"metadata": metadata,
"pushFront": pushFront
});
return this;
},
"construct": function(template, x, z, angle, metadata)
{
// TODO: verify this unit can construct this, just for internal
// sanity-checking and error reporting
Engine.PostCommand(PlayerID, {
"type": "construct",
"entities": [this.id()],
"template": template,
"x": x,
"z": z,
"angle": angle,
"autorepair": false,
"autocontinue": false,
"queued": false,
"pushFront": false,
"metadata": metadata // can be undefined
});
return this;
},
"research": function(template, pushFront = false)
{
Engine.PostCommand(PlayerID, {
"type": "research",
"entity": this.id(),
"template": template,
"pushFront": pushFront
});
return this;
},
"stopProduction": function(id)
{
Engine.PostCommand(PlayerID, { "type": "stop-production", "entity": this.id(), "id": id });
return this;
},
"stopAllProduction": function(percentToStopAt)
{
const queue = this._entity.trainingQueue;
if (!queue)
return true; // no queue, so technically we stopped all production.
for (const item of queue)
if (item.progress < percentToStopAt)
Engine.PostCommand(PlayerID, { "type": "stop-production", "entity": this.id(), "id": item.id });
return this;
},
"guard": function(target, queued = false, pushFront = false)
{
Engine.PostCommand(PlayerID, { "type": "guard", "entities": [this.id()], "target": target.id(), "queued": queued, "pushFront": pushFront });
return this;
},
"removeGuard": function()
{
Engine.PostCommand(PlayerID, { "type": "remove-guard", "entities": [this.id()] });
return this;
}
});