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

1093 lines
28 KiB
JavaScript
Raw Normal View History

import { VectorDistance } from "simulation/ai/common-api/utils.js";
// defines a template.
export class Template
{
constructor(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(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() { return this._templateName; }
genericName() { return this.get("Identity/GenericName"); }
civ() { return this.get("Identity/Civ"); }
matchLimit()
{
if (!this.get("TrainingRestrictions"))
return undefined;
return this.get("TrainingRestrictions/MatchLimit");
}
classes()
{
const template = this.get("Identity");
if (!template)
return undefined;
return GetIdentityClasses(template);
}
hasClass(name)
{
if (!this._classes)
this._classes = this.classes();
return this._classes && this._classes.indexOf(name) != -1;
}
hasClasses(array)
{
if (!this._classes)
this._classes = this.classes();
return this._classes && MatchesClassList(this._classes, array);
}
requirements()
{
return this.get("Identity/Requirements");
}
available(gameState)
{
const requirements = this.requirements();
return !requirements || Sim.RequirementsHelper.AreRequirementsMet(requirements, PlayerID);
}
cost(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(productionQueue)
{
const cost = this.cost(productionQueue);
if (!cost)
return 0;
let ret = 0;
for (const type in cost)
ret += cost[type];
return ret;
}
techCostMultiplier(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()
{
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()
{
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() { return +(this.get("Health/Max") || 0); }
isHealable()
{
if (this.get("Health") !== undefined)
return this.get("Health/Unhealable") !== "true";
return false;
}
isRepairable() { return this.get("Repairable") !== undefined; }
getPopulationBonus()
{
if (!this.get("Population"))
return 0;
return +this.get("Population/Bonus");
}
resistanceStrengths()
{
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()
{
const attack = this.get("Attack");
if (!attack)
return undefined;
const ret = [];
for (const type in attack)
ret.push(type);
return ret;
}
attackRange(type)
{
if (!this.get("Attack/" + type))
return undefined;
return {
"max": +this.get("Attack/" + type +"/MaxRange"),
"min": +(this.get("Attack/" + type +"/MinRange") || 0)
};
}
attackStrengths(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()
{
if (!this.get("Attack/Capture"))
return undefined;
return +this.get("Attack/Capture/Capture") || 0;
}
attackTimes(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()
{
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(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(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(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(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(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()
{
if (!this.get("ResourceSupply"))
return undefined;
const [type, subtype] = this.get("ResourceSupply/Type").split('.');
return { "generic": type, "specific": subtype };
}
getResourceType()
{
if (!this.get("ResourceSupply"))
return undefined;
return this.get("ResourceSupply/Type").split('.')[0];
}
getDiminishingReturns() { return +(this.get("ResourceSupply/DiminishingReturns") || 1); }
resourceSupplyMax() { return +this.get("ResourceSupply/Max"); }
maxGatherers() { return +(this.get("ResourceSupply/MaxGatherers") || 0); }
resourceGatherRates()
{
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()
{
if (!this.get("ResourceDropsite"))
return undefined;
const types = this.get("ResourceDropsite/Types");
return types ? types.split(/\s+/) : [];
}
isResourceDropsite(resourceType)
{
const types = this.resourceDropsiteTypes();
return types && (!resourceType || types.indexOf(resourceType) !== -1);
}
isTreasure() { return this.get("Treasure") !== undefined; }
treasureResources()
{
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() { return this.get("GarrisonHolder/List/_string"); }
garrisonMax() { return this.get("GarrisonHolder/Max"); }
garrisonSize() { return this.get("Garrisonable/Size"); }
garrisonEjectHealth() { return +this.get("GarrisonHolder/EjectHealth"); }
getDefaultArrow() { return +this.get("BuildingAI/DefaultArrowCount"); }
getArrowMultiplier() { return +this.get("BuildingAI/GarrisonArrowMultiplier"); }
getGarrisonArrowClasses()
{
if (!this.get("BuildingAI"))
return undefined;
return this.get("BuildingAI/GarrisonArrowClasses").split(/\s+/);
}
buffHeal() { return +this.get("GarrisonHolder/BuffHeal"); }
2014-07-22 14:23:25 -07:00
promotion() { return this.get("Promotion/Entity"); }
isPackable() { return this.get("Pack") != undefined; }
isHuntable()
{
// 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() { return +this.get("UnitMotion/WalkSpeed"); }
trainingCategory() { return this.get("TrainingRestrictions/Category"); }
buildTime(researcher)
{
let time = +this.get("Cost/BuildTime");
if (researcher)
time *= researcher.techCostMultiplier("time");
return time;
}
buildCategory() { return this.get("BuildRestrictions/Category"); }
buildDistance()
{
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() { return this.get("BuildRestrictions/PlacementType"); }
buildTerritories()
{
if (!this.get("BuildRestrictions"))
return undefined;
const territory = this.get("BuildRestrictions/Territory");
return !territory ? undefined : territory.split(/\s+/);
}
hasBuildTerritory(territory)
{
const territories = this.buildTerritories();
return territories && territories.indexOf(territory) != -1;
}
hasTerritoryInfluence()
{
return this.get("TerritoryInfluence") !== undefined;
}
hasDefensiveFire()
{
if (!this.get("Attack/Ranged"))
return false;
return this.getDefaultArrow() || this.getArrowMultiplier();
}
territoryInfluenceRadius()
{
if (this.get("TerritoryInfluence") !== undefined)
return +this.get("TerritoryInfluence/Radius");
return -1;
}
territoryInfluenceWeight()
{
if (this.get("TerritoryInfluence") !== undefined)
return +this.get("TerritoryInfluence/Weight");
return -1;
}
territoryDecayRate()
{
return +(this.get("TerritoryDecay/DecayRate") || 0);
}
defaultRegenRate()
{
return +(this.get("Capturable/RegenRate") || 0);
}
garrisonRegenRate()
{
return +(this.get("Capturable/GarrisonRegenRate") || 0);
}
visionRange() { return +this.get("Vision/Range"); }
gainMultiplier() { return +this.get("Trader/GainMultiplier"); }
isBuilder() { return this.get("Builder") !== undefined; }
isGatherer() { return this.get("ResourceGatherer") !== undefined; }
canGather(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() { return this.get("GarrisonHolder") !== undefined; }
isTurretHolder() { 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(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() { return this.get("Capturable") !== undefined; }
canGuard() { return this.get("UnitAI/CanGuard") === "true"; }
canGarrison() { return "Garrisonable" in this._template; }
canOccupyTurret() { return "Turretable" in this._template; }
isTreasureCollector() { return this.get("TreasureCollector") !== undefined; }
hasUnitAI() { 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 class Entity extends Template
{
constructor(sharedAI, entity)
{
super(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(iid) { return SimEngine.QueryInterface(this.id(), iid); }
Run the AI in the same Compartment as the simulation. Let the AI access Sim data. This is a paradigm change for AI computation. Historically, the AI was intended to be run in a separate thread from the simulation. The idea was that slow AI wouldn't stop the renderer from being smooth. In that original design, the AI received a copy of the game world and used that to run its logic. This meant the simulation could safely do whatever it wanted in the meantime. This copy was done via AIProxy & AIInterface. This design ended up having significant flaws: - The copying impacts the simulation negatively, particularly because AIProxy subscribes to a lot of messages (sometimes sent exclusively to it). This time cannot be threaded, and impacts MP games without AIs. - Copying the data is increasingly difficult. Modifiers are a headache, LOS is not implemented. Lots of logic is duplicated. The intended benefits of the design also failed to realise somewhat: - The AI was never threaded, and in fact, it is probably better to try and thread Sim + AI from the renderer than just the AI, at which point threading the AI specifically brings little benefit. The new design is much simpler and straighforward, but this has some side-effects: - The AI can now change the simulation. This can be used for cheating, or possibly for a tutorial AI. - The AI runs in the same GC zone as the simulation, which may lead to more frequent Sim GCs (but overall we might expect a reduction in temporary objects). - The AI state was essentially cached, so replacing some functions with Engine.QueryInterface might be slower. The tradeoff should be balanced by lower AIProxy computation times. Future work: - Threading some specific AI tasks could still be worthwhile, but should be done in specific worker threads, allowed to run over several turns if needed. Technical note: the AI 'global' is in its own Realm, which means name collisions with the same are not possible. Other notes: - The RL Interface uses the AI Interface and thus will gradually lose some data there. Given that the RL Interface can now request data however, this should be dine. Refs #5962, #2370 Differential Revision: https://code.wildfiregames.com/D3769 This was SVN commit r26274.
2022-01-30 05:33:34 -08:00
toString() { return "[Entity " + this.id() + " " + this.templateName() + "]"; }
id() { 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(player, key) { return this._ai.getMetadata(player, this, key); }
/**
* Sets extra data to be associated with this entity.
*/
setMetadata(player, key, value) { this._ai.setMetadata(player, this, key, value); }
deleteAllMetadata(player) { delete this._ai._entityMetadata[player][this.id()]; }
deleteMetadata(player, key) { this._ai.deleteMetadata(player, this, key); }
position() { return this._entity.position; }
angle() { return this._entity.angle; }
isIdle() { return this._entity.idle; }
getStance() { return this._entity.stance; }
unitAIState() { return this._entity.unitAIState; }
unitAIOrderData() { return SimEngine.QueryInterface(this.id(), Sim.IID_UnitAI).GetOrders(); }
hitpoints() { return this._entity.hitpoints; }
isHurt() { return this.hitpoints() < this.maxHitpoints(); }
healthLevel() { return this.hitpoints() / this.maxHitpoints(); }
needsHeal() { return this.isHurt() && this.isHealable(); }
needsRepair() { return this.isHurt() && this.isRepairable(); }
decaying() { return this._entity.decaying; }
capturePoints() {return this._entity.capturePoints; }
isInvulnerable() { return this._entity.invulnerability || false; }
isSharedDropsite() { return this._entity.sharedDropsite === true; }
/**
* Returns the current training queue state, of the form
* [ { "id": 0, "template": "...", "count": 1, "progress": 0.5, "metadata": ... }, ... ]
*/
trainingQueue()
{
return this._entity.trainingQueue;
}
trainingQueueTime()
{
const queue = this._entity.trainingQueue;
if (!queue)
return undefined;
let time = 0;
for (const item of queue)
time += item.timeRemaining;
return time / 1000;
}
foundationProgress()
{
return this._entity.foundationProgress;
}
getBuilders()
{
if (this._entity.foundationProgress === undefined)
return undefined;
if (this._entity.foundationBuilders === undefined)
return [];
return this._entity.foundationBuilders;
}
getBuildersNb()
{
if (this._entity.foundationProgress === undefined)
return undefined;
if (this._entity.foundationBuilders === undefined)
return 0;
return this._entity.foundationBuilders.length;
}
owner()
{
return this._entity.owner;
}
isOwn(player)
{
if (typeof this._entity.owner === "undefined")
return false;
return this._entity.owner === player;
}
resourceSupplyAmount()
{
Run the AI in the same Compartment as the simulation. Let the AI access Sim data. This is a paradigm change for AI computation. Historically, the AI was intended to be run in a separate thread from the simulation. The idea was that slow AI wouldn't stop the renderer from being smooth. In that original design, the AI received a copy of the game world and used that to run its logic. This meant the simulation could safely do whatever it wanted in the meantime. This copy was done via AIProxy & AIInterface. This design ended up having significant flaws: - The copying impacts the simulation negatively, particularly because AIProxy subscribes to a lot of messages (sometimes sent exclusively to it). This time cannot be threaded, and impacts MP games without AIs. - Copying the data is increasingly difficult. Modifiers are a headache, LOS is not implemented. Lots of logic is duplicated. The intended benefits of the design also failed to realise somewhat: - The AI was never threaded, and in fact, it is probably better to try and thread Sim + AI from the renderer than just the AI, at which point threading the AI specifically brings little benefit. The new design is much simpler and straighforward, but this has some side-effects: - The AI can now change the simulation. This can be used for cheating, or possibly for a tutorial AI. - The AI runs in the same GC zone as the simulation, which may lead to more frequent Sim GCs (but overall we might expect a reduction in temporary objects). - The AI state was essentially cached, so replacing some functions with Engine.QueryInterface might be slower. The tradeoff should be balanced by lower AIProxy computation times. Future work: - Threading some specific AI tasks could still be worthwhile, but should be done in specific worker threads, allowed to run over several turns if needed. Technical note: the AI 'global' is in its own Realm, which means name collisions with the same are not possible. Other notes: - The RL Interface uses the AI Interface and thus will gradually lose some data there. Given that the RL Interface can now request data however, this should be dine. Refs #5962, #2370 Differential Revision: https://code.wildfiregames.com/D3769 This was SVN commit r26274.
2022-01-30 05:33:34 -08:00
return this.queryInterface(Sim.IID_ResourceSupply)?.GetCurrentAmount();
}
resourceSupplyNumGatherers()
{
Run the AI in the same Compartment as the simulation. Let the AI access Sim data. This is a paradigm change for AI computation. Historically, the AI was intended to be run in a separate thread from the simulation. The idea was that slow AI wouldn't stop the renderer from being smooth. In that original design, the AI received a copy of the game world and used that to run its logic. This meant the simulation could safely do whatever it wanted in the meantime. This copy was done via AIProxy & AIInterface. This design ended up having significant flaws: - The copying impacts the simulation negatively, particularly because AIProxy subscribes to a lot of messages (sometimes sent exclusively to it). This time cannot be threaded, and impacts MP games without AIs. - Copying the data is increasingly difficult. Modifiers are a headache, LOS is not implemented. Lots of logic is duplicated. The intended benefits of the design also failed to realise somewhat: - The AI was never threaded, and in fact, it is probably better to try and thread Sim + AI from the renderer than just the AI, at which point threading the AI specifically brings little benefit. The new design is much simpler and straighforward, but this has some side-effects: - The AI can now change the simulation. This can be used for cheating, or possibly for a tutorial AI. - The AI runs in the same GC zone as the simulation, which may lead to more frequent Sim GCs (but overall we might expect a reduction in temporary objects). - The AI state was essentially cached, so replacing some functions with Engine.QueryInterface might be slower. The tradeoff should be balanced by lower AIProxy computation times. Future work: - Threading some specific AI tasks could still be worthwhile, but should be done in specific worker threads, allowed to run over several turns if needed. Technical note: the AI 'global' is in its own Realm, which means name collisions with the same are not possible. Other notes: - The RL Interface uses the AI Interface and thus will gradually lose some data there. Given that the RL Interface can now request data however, this should be dine. Refs #5962, #2370 Differential Revision: https://code.wildfiregames.com/D3769 This was SVN commit r26274.
2022-01-30 05:33:34 -08:00
return this.queryInterface(Sim.IID_ResourceSupply)?.GetNumGatherers();
}
isFull()
{
const numGatherers = this.resourceSupplyNumGatherers();
Run the AI in the same Compartment as the simulation. Let the AI access Sim data. This is a paradigm change for AI computation. Historically, the AI was intended to be run in a separate thread from the simulation. The idea was that slow AI wouldn't stop the renderer from being smooth. In that original design, the AI received a copy of the game world and used that to run its logic. This meant the simulation could safely do whatever it wanted in the meantime. This copy was done via AIProxy & AIInterface. This design ended up having significant flaws: - The copying impacts the simulation negatively, particularly because AIProxy subscribes to a lot of messages (sometimes sent exclusively to it). This time cannot be threaded, and impacts MP games without AIs. - Copying the data is increasingly difficult. Modifiers are a headache, LOS is not implemented. Lots of logic is duplicated. The intended benefits of the design also failed to realise somewhat: - The AI was never threaded, and in fact, it is probably better to try and thread Sim + AI from the renderer than just the AI, at which point threading the AI specifically brings little benefit. The new design is much simpler and straighforward, but this has some side-effects: - The AI can now change the simulation. This can be used for cheating, or possibly for a tutorial AI. - The AI runs in the same GC zone as the simulation, which may lead to more frequent Sim GCs (but overall we might expect a reduction in temporary objects). - The AI state was essentially cached, so replacing some functions with Engine.QueryInterface might be slower. The tradeoff should be balanced by lower AIProxy computation times. Future work: - Threading some specific AI tasks could still be worthwhile, but should be done in specific worker threads, allowed to run over several turns if needed. Technical note: the AI 'global' is in its own Realm, which means name collisions with the same are not possible. Other notes: - The RL Interface uses the AI Interface and thus will gradually lose some data there. Given that the RL Interface can now request data however, this should be dine. Refs #5962, #2370 Differential Revision: https://code.wildfiregames.com/D3769 This was SVN commit r26274.
2022-01-30 05:33:34 -08:00
if (numGatherers)
return this.maxGatherers() === numGatherers;
return undefined;
}
resourceCarrying()
{
Run the AI in the same Compartment as the simulation. Let the AI access Sim data. This is a paradigm change for AI computation. Historically, the AI was intended to be run in a separate thread from the simulation. The idea was that slow AI wouldn't stop the renderer from being smooth. In that original design, the AI received a copy of the game world and used that to run its logic. This meant the simulation could safely do whatever it wanted in the meantime. This copy was done via AIProxy & AIInterface. This design ended up having significant flaws: - The copying impacts the simulation negatively, particularly because AIProxy subscribes to a lot of messages (sometimes sent exclusively to it). This time cannot be threaded, and impacts MP games without AIs. - Copying the data is increasingly difficult. Modifiers are a headache, LOS is not implemented. Lots of logic is duplicated. The intended benefits of the design also failed to realise somewhat: - The AI was never threaded, and in fact, it is probably better to try and thread Sim + AI from the renderer than just the AI, at which point threading the AI specifically brings little benefit. The new design is much simpler and straighforward, but this has some side-effects: - The AI can now change the simulation. This can be used for cheating, or possibly for a tutorial AI. - The AI runs in the same GC zone as the simulation, which may lead to more frequent Sim GCs (but overall we might expect a reduction in temporary objects). - The AI state was essentially cached, so replacing some functions with Engine.QueryInterface might be slower. The tradeoff should be balanced by lower AIProxy computation times. Future work: - Threading some specific AI tasks could still be worthwhile, but should be done in specific worker threads, allowed to run over several turns if needed. Technical note: the AI 'global' is in its own Realm, which means name collisions with the same are not possible. Other notes: - The RL Interface uses the AI Interface and thus will gradually lose some data there. Given that the RL Interface can now request data however, this should be dine. Refs #5962, #2370 Differential Revision: https://code.wildfiregames.com/D3769 This was SVN commit r26274.
2022-01-30 05:33:34 -08:00
return this.queryInterface(Sim.IID_ResourceGatherer)?.GetCarryingStatus();
}
currentGatherRate()
{
// 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()
{
return this._entity.garrisonHolderID;
}
garrisoned() { return this._entity.garrisoned; }
garrisonedSlots()
{
let count = 0;
if (this._entity.garrisoned)
for (const ent of this._entity.garrisoned)
count += +this._ai._entities.get(ent).garrisonSize();
return count;
}
canGarrisonInside()
{
return this.garrisonedSlots() < this.garrisonMax();
}
/**
* returns true if the entity can attack (including capture) the given class.
*/
canAttackClass(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(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(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(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(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(stance)
{
if (this.getStance() === undefined)
return undefined;
Engine.PostCommand(PlayerID, { "type": "stance", "entities": [this.id()], "name": stance });
return this;
}
stopMoving()
{
Engine.PostCommand(PlayerID, { "type": "stop", "entities": [this.id()], "queued": false, "pushFront": false });
}
unload(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()
{
if (!this.get("GarrisonHolder"))
return undefined;
Engine.PostCommand(PlayerID, { "type": "unload-all-by-owner", "garrisonHolders": [this.id()] });
return this;
}
garrison(target, queued = false, pushFront = false)
{
Engine.PostCommand(PlayerID, { "type": "garrison", "entities": [this.id()], "target": target.id(), "queued": queued, "pushFront": pushFront });
return this;
}
["occupy-turret"](target, queued = false, pushFront = false)
{
Engine.PostCommand(PlayerID, { "type": "occupy-turret", "entities": [this.id()], "target": target.id(), "queued": queued, "pushFront": pushFront });
return this;
}
attack(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(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(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(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(target, queued = false, pushFront = false)
{
Engine.PostCommand(PlayerID, { "type": "gather", "entities": [this.id()], "target": target.id(), "queued": queued, "pushFront": pushFront });
return this;
}
repair(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(target, queued = false, pushFront = false)
{
Engine.PostCommand(PlayerID, { "type": "returnresource", "entities": [this.id()], "target": target.id(), "queued": queued, "pushFront": pushFront });
return this;
}
destroy()
{
Engine.PostCommand(PlayerID, { "type": "delete-entities", "entities": [this.id()] });
return this;
}
barter(buyType, sellType, amount)
{
Engine.PostCommand(PlayerID, { "type": "barter", "sell": sellType, "buy": buyType, "amount": amount });
return this;
}
tradeRoute(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(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()
{
Engine.PostCommand(PlayerID, { "type": "unset-rallypoint", "structures": [this.id()] });
return this;
}
train(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,
2014-07-22 14:23:25 -07:00
"metadata": metadata,
"pushFront": pushFront
});
return this;
}
construct(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(template, pushFront = false)
{
Engine.PostCommand(PlayerID, {
"type": "research",
"entity": this.id(),
"template": template,
"pushFront": pushFront
});
return this;
}
stopProduction(id)
{
Engine.PostCommand(PlayerID, { "type": "stop-production", "entity": this.id(), "id": id });
return this;
}
stopAllProduction(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(target, queued = false, pushFront = false)
{
Engine.PostCommand(PlayerID, { "type": "guard", "entities": [this.id()], "target": target.id(), "queued": queued, "pushFront": pushFront });
return this;
}
removeGuard()
{
Engine.PostCommand(PlayerID, { "type": "remove-guard", "entities": [this.id()] });
return this;
}
}