Index: ps/trunk/binaries/data/mods/public/gui/reference/common/TemplateParser.js
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/reference/common/TemplateParser.js (revision 24962)
+++ ps/trunk/binaries/data/mods/public/gui/reference/common/TemplateParser.js (revision 24963)
@@ -1,342 +1,342 @@
/**
* This class parses and stores parsed template data.
*/
class TemplateParser
{
constructor(TemplateLoader)
{
this.TemplateLoader = TemplateLoader;
/**
* Parsed Data Stores
*/
this.auras = {};
this.entities = {};
this.techs = {};
this.phases = {};
this.modifiers = {};
this.phaseList = [];
}
getAura(auraName)
{
if (auraName in this.auras)
return this.auras[auraName];
if (!AuraTemplateExists(auraName))
return null;
let template = this.TemplateLoader.loadAuraTemplate(auraName);
let parsed = GetAuraDataHelper(template);
if (template.civ)
parsed.civ = template.civ;
this.auras[auraName] = parsed;
return this.auras[auraName];
}
/**
* Load and parse a structure, unit, resource, etc from its entity template file.
*
* @param {string} templateName
* @param {string} civCode
* @return {(object|null)} Sanitized object about the requested template or null if entity template doesn't exist.
*/
getEntity(templateName, civCode)
{
if (!(civCode in this.entities))
this.entities[civCode] = {};
else if (templateName in this.entities[civCode])
return this.entities[civCode][templateName];
if (!Engine.TemplateExists(templateName))
return null;
let template = this.TemplateLoader.loadEntityTemplate(templateName, civCode);
let parsed = GetTemplateDataHelper(template, null, this.TemplateLoader.auraData, this.modifiers[civCode] || {});
parsed.name.internal = templateName;
parsed.history = template.Identity.History;
parsed.production = this.TemplateLoader.deriveProductionQueue(template, civCode);
if (template.Builder)
parsed.builder = this.TemplateLoader.deriveBuildQueue(template, civCode);
// Set the minimum phase that this entity is available.
// For gaia objects, this is meaningless.
if (!parsed.requiredTechnology)
parsed.phase = this.phaseList[0];
else if (this.TemplateLoader.isPhaseTech(parsed.requiredTechnology))
parsed.phase = this.getActualPhase(parsed.requiredTechnology);
else
parsed.phase = this.getPhaseOfTechnology(parsed.requiredTechnology, civCode);
if (template.Identity.Rank)
parsed.promotion = {
"current_rank": template.Identity.Rank,
"entity": template.Promotion && template.Promotion.Entity
};
if (template.ResourceSupply)
parsed.supply = {
"type": template.ResourceSupply.Type.split("."),
- "amount": template.ResourceSupply.Amount,
+ "amount": template.ResourceSupply.Max,
};
if (parsed.upgrades)
parsed.upgrades = this.getActualUpgradeData(parsed.upgrades, civCode);
if (parsed.wallSet)
{
parsed.wallset = {};
if (!parsed.upgrades)
parsed.upgrades = [];
// Note: An assumption is made here that wall segments all have the same resistance and auras
let struct = this.getEntity(parsed.wallSet.templates.long, civCode);
parsed.resistance = struct.resistance;
parsed.auras = struct.auras;
// For technology cost multiplier, we need to use the tower
struct = this.getEntity(parsed.wallSet.templates.tower, civCode);
parsed.techCostMultiplier = struct.techCostMultiplier;
let health;
for (let wSegm in parsed.wallSet.templates)
{
if (wSegm == "fort" || wSegm == "curves")
continue;
let wPart = this.getEntity(parsed.wallSet.templates[wSegm], civCode);
parsed.wallset[wSegm] = wPart;
for (let research of wPart.production.techs)
parsed.production.techs.push(research);
if (wPart.upgrades)
Array.prototype.push.apply(parsed.upgrades, wPart.upgrades);
if (["gate", "tower"].indexOf(wSegm) != -1)
continue;
if (!health)
{
health = { "min": wPart.health, "max": wPart.health };
continue;
}
health.min = Math.min(health.min, wPart.health);
health.max = Math.max(health.max, wPart.health);
}
if (parsed.wallSet.templates.curves)
for (let curve of parsed.wallSet.templates.curves)
{
let wPart = this.getEntity(curve, civCode);
health.min = Math.min(health.min, wPart.health);
health.max = Math.max(health.max, wPart.health);
}
if (health.min == health.max)
parsed.health = health.min;
else
parsed.health = sprintf(translate("%(health_min)s to %(health_max)s"), {
"health_min": health.min,
"health_max": health.max
});
}
this.entities[civCode][templateName] = parsed;
return parsed;
}
/**
* Load and parse technology from json template.
*
* @param {string} technologyName
* @param {string} civCode
* @return {Object} Sanitized data about the requested technology.
*/
getTechnology(technologyName, civCode)
{
if (!TechnologyTemplateExists(technologyName))
return null;
if (this.TemplateLoader.isPhaseTech(technologyName) && technologyName in this.phases)
return this.phases[technologyName];
if (!(civCode in this.techs))
this.techs[civCode] = {};
else if (technologyName in this.techs[civCode])
return this.techs[civCode][technologyName];
let template = this.TemplateLoader.loadTechnologyTemplate(technologyName);
let tech = GetTechnologyDataHelper(template, civCode, g_ResourceData);
tech.name.internal = technologyName;
if (template.pair !== undefined)
{
tech.pair = template.pair;
tech.reqs = this.mergeRequirements(tech.reqs, this.TemplateLoader.loadTechnologyPairTemplate(template.pair).reqs);
}
if (this.TemplateLoader.isPhaseTech(technologyName))
{
tech.actualPhase = technologyName;
if (tech.replaces !== undefined)
tech.actualPhase = tech.replaces[0];
this.phases[technologyName] = tech;
}
else
this.techs[civCode][technologyName] = tech;
return tech;
}
/**
* @param {string} phaseCode
* @param {string} civCode
* @return {Object} Sanitized object containing phase data
*/
getPhase(phaseCode, civCode)
{
return this.getTechnology(phaseCode, civCode);
}
/**
* Provided with an array containing basic information about possible
* upgrades, such as that generated by globalscript's GetTemplateDataHelper,
* this function loads the actual template data of the upgrades, overwrites
* certain values within, then passes an array containing the template data
* back to caller.
*/
getActualUpgradeData(upgradesInfo, civCode)
{
let newUpgrades = [];
for (let upgrade of upgradesInfo)
{
upgrade.entity = upgrade.entity.replace(/\{(civ|native)\}/g, civCode);
let data = GetTemplateDataHelper(this.TemplateLoader.loadEntityTemplate(upgrade.entity, civCode), null, this.TemplateLoader.auraData, this.modifiers[civCode] || {});
data.name.internal = upgrade.entity;
data.cost = upgrade.cost;
data.icon = upgrade.icon || data.icon;
data.tooltip = upgrade.tooltip || data.tooltip;
data.requiredTechnology = upgrade.requiredTechnology || data.requiredTechnology;
if (!data.requiredTechnology)
data.phase = this.phaseList[0];
else if (this.TemplateLoader.isPhaseTech(data.requiredTechnology))
data.phase = this.getActualPhase(data.requiredTechnology);
else
data.phase = this.getPhaseOfTechnology(data.requiredTechnology, civCode);
newUpgrades.push(data);
}
return newUpgrades;
}
/**
* Determines and returns the phase in which a given technology can be
* first researched. Works recursively through the given tech's
* pre-requisite and superseded techs if necessary.
*
* @param {string} techName - The Technology's name
* @param {string} civCode
* @return The name of the phase the technology belongs to, or false if
* the current civ can't research this tech
*/
getPhaseOfTechnology(techName, civCode)
{
let phaseIdx = -1;
if (basename(techName).startsWith("phase"))
{
if (!this.phases[techName].reqs)
return false;
phaseIdx = this.phaseList.indexOf(this.getActualPhase(techName));
if (phaseIdx > 0)
return this.phaseList[phaseIdx - 1];
}
let techReqs = this.getTechnology(techName, civCode).reqs;
if (!techReqs)
return false;
for (let option of techReqs)
if (option.techs)
for (let tech of option.techs)
{
if (basename(tech).startsWith("phase"))
return tech;
if (basename(tech).startsWith("pair"))
continue;
phaseIdx = Math.max(phaseIdx, this.phaseList.indexOf(this.getPhaseOfTechnology(tech, civCode)));
}
return this.phaseList[phaseIdx] || false;
}
/**
* Returns the actual phase a certain phase tech represents or stands in for.
*
* For example, passing `phase_city_athen` would result in `phase_city`.
*
* @param {string} phaseName
* @return {string}
*/
getActualPhase(phaseName)
{
if (this.phases[phaseName])
return this.phases[phaseName].actualPhase;
warn("Unrecognized phase (" + phaseName + ")");
return this.phaseList[0];
}
getModifiers(civCode)
{
return this.modifiers[civCode];
}
deriveModifications(civCode)
{
this.modifiers[civCode] = this.TemplateLoader.deriveModifications(civCode);
}
derivePhaseList(technologyList, civCode)
{
// Load all of a civ's specific phase technologies
for (let techcode of technologyList)
if (this.TemplateLoader.isPhaseTech(techcode))
this.getTechnology(techcode, civCode);
this.phaseList = UnravelPhases(this.phases);
// Make sure all required generic phases are loaded and parsed
for (let phasecode of this.phaseList)
this.getTechnology(phasecode, civCode);
}
mergeRequirements(reqsA, reqsB)
{
if (!reqsA || !reqsB)
return false;
let finalReqs = clone(reqsA);
for (let option of reqsB)
for (let type in option)
for (let opt in finalReqs)
{
if (!finalReqs[opt][type])
finalReqs[opt][type] = [];
Array.prototype.push.apply(finalReqs[opt][type], option[type]);
}
return finalReqs;
}
}
Index: ps/trunk/binaries/data/mods/public/simulation/ai/common-api/entity.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/ai/common-api/entity.js (revision 24962)
+++ ps/trunk/binaries/data/mods/public/simulation/ai/common-api/entity.js (revision 24963)
@@ -1,1004 +1,1004 @@
var API3 = function(m)
{
// defines a template.
m.Template = m.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)
{
let value = this._template;
if (this._entityModif && this._entityModif.has(string))
return this._entityModif.get(string);
else if (this._templateModif)
{
let 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 args = string.split("/");
for (let arg of args)
{
if (value[arg] != undefined)
value = value[arg];
else
{
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() {
let template = this.get("Identity");
if (!template)
return undefined;
return GetIdentityClasses(template);
},
"hasClass": function(name) {
if (!this._classes)
this._classes = this.classes();
let classes = this._classes;
return classes && classes.indexOf(name) != -1;
},
"hasClasses": function(array) {
if (!this._classes)
this._classes = this.classes();
let classes = this._classes;
if (!classes)
return false;
for (let cls of array)
if (classes.indexOf(cls) == -1)
return false;
return true;
},
"requiredTech": function() { return this.get("Identity/RequiredTechnology"); },
"available": function(gameState) {
let techRequired = this.requiredTech();
if (!techRequired)
return true;
return gameState.isResearched(techRequired);
},
// specifically
"phase": function() {
let techRequired = this.requiredTech();
if (!techRequired)
return 0;
if (techRequired == "phase_village")
return 1;
if (techRequired == "phase_town")
return 2;
if (techRequired == "phase_city")
return 3;
if (techRequired.startsWith("phase_"))
return 4;
return 0;
},
"cost": function(productionQueue) {
if (!this.get("Cost"))
return {};
let ret = {};
for (let type in this.get("Cost/Resources"))
ret[type] = +this.get("Cost/Resources/" + type);
return ret;
},
"costSum": function(productionQueue) {
let cost = this.cost(productionQueue);
if (!cost)
return 0;
let ret = 0;
for (let type in cost)
ret += cost[type];
return ret;
},
"techCostMultiplier": function(type) {
return +(this.get("ProductionQueue/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"))
{
let w = +this.get("Obstruction/Static/@width");
let 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"))
{
let r = +this.get("Obstruction/Unit/@radius");
return { "max": r, "min": r };
}
let right = this.get("Obstruction/Obstructions/Right");
let left = this.get("Obstruction/Obstructions/Left");
if (left && right)
{
let w = +right["@x"] + right["@width"]/2 - left["@x"] + left["@width"]/2;
let 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"))
{
let w = +this.get("Footprint/Square/@width");
let 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() {
let resistanceTypes = this.get("Resistance");
if (!resistanceTypes || !resistanceTypes.Entity)
return undefined;
let resistance = {};
if (resistanceTypes.Entity.Capture)
resistance.Capture = +this.get("Resistance/Entity/Capture");
if (resistanceTypes.Entity.Damage)
{
resistance.Damage = {};
for (let damageType in resistanceTypes.Entity.Damage)
resistance.Damage[damageType] = +this.get("Resistance/Entity/Damage/" + damageType);
}
// ToDo: Resistance to StatusEffects.
return resistance;
},
"attackTypes": function() {
if (!this.get("Attack"))
return undefined;
let ret = [];
for (let type in this.get("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) {
let attackDamageTypes = this.get("Attack/" + type + "/Damage");
if (!attackDamageTypes)
return undefined;
let damage = {};
for (let 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() {
if (!this.get("Attack"))
return undefined;
let Classes = [];
for (let type in this.get("Attack"))
{
let bonuses = this.get("Attack/" + type + "/Bonuses");
if (!bonuses)
continue;
for (let b in bonuses)
{
let 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 those classes.
// TODO: refine using the multiplier
"countersClasses": function(classes) {
if (!this.get("Attack"))
return false;
let mcounter = [];
for (let type in this.get("Attack"))
{
let bonuses = this.get("Attack/" + type + "/Bonuses");
if (!bonuses)
continue;
for (let b in bonuses)
{
let bonusClasses = this.get("Attack/" + type + "/Bonuses/" + b + "/Classes");
if (bonusClasses)
mcounter.concat(bonusClasses.split(" "));
}
}
for (let i in classes)
if (mcounter.indexOf(classes[i]) != -1)
return true;
return false;
},
// returns, if it exists, the multiplier from each attack against a given class
"getMultiplierAgainst": function(type, againstClass) {
if (!this.get("Attack/" + type +""))
return undefined;
if (this.get("Attack/" + type + "/Bonuses"))
{
for (let b in this.get("Attack/" + type + "/Bonuses"))
{
let bonusClasses = this.get("Attack/" + type + "/Bonuses/" + b + "/Classes");
if (!bonusClasses)
continue;
for (let bcl of bonusClasses.split(" "))
if (bcl == againstClass)
return +this.get("Attack/" + type + "/Bonuses/" + b + "/Multiplier");
}
}
return 1;
},
"buildableEntities": function(civ) {
let 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) {
let templates = this.get("ProductionQueue/Entities/_string");
if (!templates)
return undefined;
return templates.replace(/\{native\}/g, this.civ()).replace(/\{civ\}/g, civ).split(/\s+/);
},
"researchableTechs": function(gameState, civ) {
let templates = this.get("ProductionQueue/Technologies/_string");
if (!templates)
return undefined;
let techs = templates.split(/\s+/);
for (let i = 0; i < techs.length; ++i)
{
let tech = techs[i];
if (tech.indexOf("{civ}") == -1)
continue;
let 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;
let [type, subtype] = this.get("ResourceSupply/Type").split('.');
return { "generic": type, "specific": subtype };
},
// will return either "food", "wood", "stone", "metal" and not treasure.
"getResourceType": function() {
if (!this.get("ResourceSupply"))
return undefined;
let [type, subtype] = this.get("ResourceSupply/Type").split('.');
if (type == "treasure")
return subtype;
return type;
},
"getDiminishingReturns": function() { return +(this.get("ResourceSupply/DiminishingReturns") || 1); },
- "resourceSupplyMax": function() { return +this.get("ResourceSupply/Amount"); },
+ "resourceSupplyMax": function() { return +this.get("ResourceSupply/Max"); },
"maxGatherers": function() { return +(this.get("ResourceSupply/MaxGatherers") || 0); },
"resourceGatherRates": function() {
if (!this.get("ResourceGatherer"))
return undefined;
let ret = {};
let baseSpeed = +this.get("ResourceGatherer/BaseSpeed");
for (let r in this.get("ResourceGatherer/Rates"))
ret[r] = +this.get("ResourceGatherer/Rates/" + r) * baseSpeed;
return ret;
},
"resourceDropsiteTypes": function() {
if (!this.get("ResourceDropsite"))
return undefined;
let types = this.get("ResourceDropsite/Types");
return types ? types.split(/\s+/) : [];
},
"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(productionQueue) {
let time = +this.get("Cost/BuildTime");
if (productionQueue)
time *= productionQueue.techCostMultiplier("time");
return time;
},
"buildCategory": function() { return this.get("BuildRestrictions/Category"); },
"buildDistance": function() {
let distance = this.get("BuildRestrictions/Distance");
if (!distance)
return undefined;
let ret = {};
for (let 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") || !this.get("BuildRestrictions/Territory"))
return undefined;
return this.get("BuildRestrictions/Territory").split(/\s+/);
},
"hasBuildTerritory": function(territory) {
let 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) {
let gatherRates = this.get("ResourceGatherer/Rates");
if (!gatherRates)
return false;
for (let r in gatherRates)
if (r.split('.')[0] === type)
return true;
return false;
},
"isGarrisonHolder": function() { return this.get("GarrisonHolder") !== 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;
let restrictedClasses = this.get("Attack/Capture/RestrictedClasses/_string");
return !restrictedClasses || !MatchesClassList(target.classes(), restrictedClasses);
},
"isCapturable": function() { return this.get("Capturable") !== undefined; },
"canGuard": function() { return this.get("UnitAI/CanGuard") === "true"; },
"canGarrison": function() { return "Garrisonable" in this._template; },
});
// defines an entity, with a super Template.
// also redefines several of the template functions where the only change is applying aura and tech modifications.
m.Entity = m.Class({
"_super": m.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);
},
"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() {
if (typeof this._entity.idle === "undefined")
return undefined;
return this._entity.idle;
},
"getStance": function() { return this._entity.stance !== undefined ? this._entity.stance : undefined; },
"unitAIState": function() { return this._entity.unitAIState !== undefined ? this._entity.unitAIState : undefined; },
"unitAIOrderData": function() { return this._entity.unitAIOrderData !== undefined ? this._entity.unitAIOrderData : undefined; },
"hitpoints": function() { return this._entity.hitpoints !== undefined ? this._entity.hitpoints : undefined; },
"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 !== undefined ? this._entity.decaying : undefined; },
"capturePoints": function() {return this._entity.capturePoints !== undefined ? this._entity.capturePoints : undefined; },
"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() {
let queue = this._entity.trainingQueue;
return queue;
},
"trainingQueueTime": function() {
let queue = this._entity.trainingQueue;
if (!queue)
return undefined;
let time = 0;
for (let item of queue)
time += item.timeRemaining;
return time/1000;
},
"foundationProgress": function() {
if (this._entity.foundationProgress === undefined)
return undefined;
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() {
if (this._entity.resourceSupplyAmount === undefined)
return undefined;
return this._entity.resourceSupplyAmount;
},
"resourceSupplyNumGatherers": function()
{
if (this._entity.resourceSupplyNumGatherers !== undefined)
return this._entity.resourceSupplyNumGatherers;
return undefined;
},
"isFull": function()
{
if (this._entity.resourceSupplyNumGatherers !== undefined)
return this.maxGatherers() === this._entity.resourceSupplyNumGatherers;
return undefined;
},
"resourceCarrying": function() {
if (this._entity.resourceCarrying === undefined)
return undefined;
return this._entity.resourceCarrying;
},
"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" || this.unitAIState().split(".")[1] == "RETURNRESOURCE"))
{
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;
let type = res.resourceSupplyType();
if (!type)
return 0;
if (type.generic == "treasure")
return 1000;
let 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;
},
"garrisoned": function() { return this._entity.garrisoned; },
"garrisonedSlots": function() {
let count = 0;
if (this._entity.garrisoned)
for (let 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)
{
if (!this.get("Attack"))
return false;
for (let type in this.get("Attack"))
{
if (type == "Slaughter")
continue;
let 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)
{
let attackTypes = this.get("Attack");
if (!attackTypes)
return false;
let canCapture = allowCapture && this.canCapture(target);
let health = target.get("Health");
if (!health)
return canCapture;
for (let type in attackTypes)
{
if (type == "Capture" ? !canCapture : target.isInvulnerable())
continue;
let restrictedClasses = this.get("Attack/" + type + "/RestrictedClasses/_string");
if (!restrictedClasses || !MatchesClassList(target.classes(), restrictedClasses))
return true;
};
return false;
},
"move": function(x, z, queued = false) {
Engine.PostCommand(PlayerID, { "type": "walk", "entities": [this.id()], "x": x, "z": z, "queued": queued });
return this;
},
"moveToRange": function(x, z, min, max, queued = false) {
Engine.PostCommand(PlayerID, { "type": "walk-to-range", "entities": [this.id()], "x": x, "z": z, "min": min, "max": max, "queued": queued });
return this;
},
"attackMove": function(x, z, targetClasses, allowCapture = true, queued = false) {
Engine.PostCommand(PlayerID, { "type": "attack-walk", "entities": [this.id()], "x": x, "z": z, "targetClasses": targetClasses, "allowCapture": allowCapture, "queued": queued });
return this;
},
// violent, aggressive, defensive, passive, standground
"setStance": function(stance, queued = false) {
if (this.getStance() === undefined)
return undefined;
Engine.PostCommand(PlayerID, { "type": "stance", "entities": [this.id()], "name": stance, "queued": queued });
return this;
},
"stopMoving": function() {
Engine.PostCommand(PlayerID, { "type": "stop", "entities": [this.id()], "queued": 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) {
Engine.PostCommand(PlayerID, { "type": "garrison", "entities": [this.id()], "target": target.id(), "queued": queued });
return this;
},
"attack": function(unitId, allowCapture = true, queued = false) {
Engine.PostCommand(PlayerID, { "type": "attack", "entities": [this.id()], "target": unitId, "allowCapture": allowCapture, "queued": queued });
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]];
let norm = m.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 });
}
return this;
},
// Flees from a unit in the opposite direction.
"flee": function(unitToFleeFrom) {
if (this.position() !== undefined && unitToFleeFrom.position() !== undefined) {
let FleeDirection = [this.position()[0] - unitToFleeFrom.position()[0],
this.position()[1] - unitToFleeFrom.position()[1]];
let 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 });
}
return this;
},
"gather": function(target, queued = false) {
Engine.PostCommand(PlayerID, { "type": "gather", "entities": [this.id()], "target": target.id(), "queued": queued });
return this;
},
"repair": function(target, autocontinue = false, queued = false) {
Engine.PostCommand(PlayerID, { "type": "repair", "entities": [this.id()], "target": target.id(), "autocontinue": autocontinue, "queued": queued });
return this;
},
"returnResources": function(target, queued = false) {
Engine.PostCommand(PlayerID, { "type": "returnresource", "entities": [this.id()], "target": target.id(), "queued": queued });
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 });
return this;
},
"setRallyPoint": function(target, command) {
let data = { "command": command, "target": target.id() };
Engine.PostCommand(PlayerID, { "type": "set-rallypoint", "entities": [this.id()], "x": target.position()[0], "z": target.position()[1], "data": data });
return this;
},
"unsetRallyPoint": function() {
Engine.PostCommand(PlayerID, { "type": "unset-rallypoint", "entities": [this.id()] });
return this;
},
"train": function(civ, type, count, metadata, promotedTypes)
{
let 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,
"promoted": promotedTypes
});
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,
"metadata": metadata // can be undefined
});
return this;
},
"research": function(template) {
Engine.PostCommand(PlayerID, { "type": "research", "entity": this.id(), "template": template });
return this;
},
"stopProduction": function(id) {
Engine.PostCommand(PlayerID, { "type": "stop-production", "entity": this.id(), "id": id });
return this;
},
"stopAllProduction": function(percentToStopAt) {
let queue = this._entity.trainingQueue;
if (!queue)
return true; // no queue, so technically we stopped all production.
for (let 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) {
Engine.PostCommand(PlayerID, { "type": "guard", "entities": [this.id()], "target": target.id(), "queued": queued });
return this;
},
"removeGuard": function() {
Engine.PostCommand(PlayerID, { "type": "remove-guard", "entities": [this.id()] });
return this;
}
});
return m;
}(API3);
Index: ps/trunk/binaries/data/mods/public/simulation/components/ResourceSupply.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/components/ResourceSupply.js (revision 24962)
+++ ps/trunk/binaries/data/mods/public/simulation/components/ResourceSupply.js (revision 24963)
@@ -1,185 +1,471 @@
function ResourceSupply() {}
ResourceSupply.prototype.Schema =
"Provides a supply of one particular type of resource." +
"" +
- "1000" +
+ "1000" +
+ "1000" +
"food.meat" +
"false" +
"25" +
"0.8" +
+ "" +
+ "" +
+ "2" +
+ "1000" +
+ "" +
+ "" +
+ "alive" +
+ "2" +
+ "1000" +
+ "500" +
+ "" +
+ "" +
+ "dead notGathered" +
+ "-2" +
+ "1000" +
+ "" +
+ "" +
+ "dead" +
+ "-1" +
+ "1000" +
+ "500" +
+ "" +
+ "" +
"" +
"" +
"" +
"" +
- "" +
+ "" +
"Infinity" +
"" +
+ "" +
+ "" +
+ "Infinity" +
+ "" +
+ "" +
"" +
Resources.BuildChoicesSchema(true, true) +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "alive" +
+ "dead" +
+ "gathered" +
+ "notGathered" +
+ "" +
+ "" +
+ "
" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
"";
ResourceSupply.prototype.Init = function()
{
- // Current resource amount (non-negative)
- this.amount = this.GetMaxAmount();
+ this.amount = +(this.template.Initial || this.template.Max);
+ // Includes the ones that are tasked but not here yet, i.e. approaching.
this.gatherers = [];
+ this.activeGatherers = [];
let [type, subtype] = this.template.Type.split('.');
this.cachedType = { "generic": type, "specific": subtype };
+
+ if (this.template.Change)
+ {
+ this.timers = {};
+ this.cachedChanges = {};
+ }
};
ResourceSupply.prototype.IsInfinite = function()
{
- return !isFinite(+this.template.Amount);
+ return !isFinite(+this.template.Max);
};
ResourceSupply.prototype.GetKillBeforeGather = function()
{
return this.template.KillBeforeGather == "true";
};
ResourceSupply.prototype.GetMaxAmount = function()
{
- return +this.template.Amount;
+ return this.maxAmount;
};
ResourceSupply.prototype.GetCurrentAmount = function()
{
return this.amount;
};
ResourceSupply.prototype.GetMaxGatherers = function()
{
return +this.template.MaxGatherers;
};
ResourceSupply.prototype.GetNumGatherers = function()
{
return this.gatherers.length;
};
/**
+ * @return {number} - The number of currently active gatherers.
+ */
+ResourceSupply.prototype.GetNumActiveGatherers = function()
+{
+ return this.activeGatherers.length;
+};
+
+/**
* @return {{ "generic": string, "specific": string }} An object containing the subtype and the generic type. All resources must have both.
*/
ResourceSupply.prototype.GetType = function()
{
return this.cachedType;
};
/**
* @param {number} gathererID - The gatherer's entity id.
* @return {boolean} - Whether the ResourceSupply can have this additional gatherer or it is already gathering.
*/
ResourceSupply.prototype.IsAvailableTo = function(gathererID)
{
return this.IsAvailable() || this.IsGatheringUs(gathererID);
};
/**
* @return {boolean} - Whether this entity can have an additional gatherer.
*/
ResourceSupply.prototype.IsAvailable = function()
{
return this.amount && this.gatherers.length < this.GetMaxGatherers();
};
/**
* @param {number} entity - The entityID to check for.
* @return {boolean} - Whether the given entity is already gathering at us.
*/
ResourceSupply.prototype.IsGatheringUs = function(entity)
{
return this.gatherers.indexOf(entity) !== -1;
};
/**
* Each additional gatherer decreases the rate following a geometric sequence, with diminishingReturns as ratio.
* @return {number} The diminishing return if any, null otherwise.
*/
ResourceSupply.prototype.GetDiminishingReturns = function()
{
if (!this.template.DiminishingReturns)
return null;
let diminishingReturns = ApplyValueModificationsToEntity("ResourceSupply/DiminishingReturns", +this.template.DiminishingReturns, this.entity);
if (!diminishingReturns)
return null;
let numGatherers = this.GetNumGatherers();
if (numGatherers > 1)
return diminishingReturns == 1 ? 1 : (1 - Math.pow(diminishingReturns, numGatherers)) / (1 - diminishingReturns) / numGatherers;
return null;
};
/**
* @param {number} amount The amount of resources that should be taken from the resource supply. The amount must be positive.
* @return {{ "amount": number, "exhausted": boolean }} The current resource amount in the entity and whether it's exhausted or not.
*/
ResourceSupply.prototype.TakeResources = function(amount)
{
+ if (this.IsInfinite())
+ return { "amount": amount, "exhausted": false };
+
+ return {
+ "amount": Math.abs(this.Change(-amount)),
+ "exhausted": this.amount == 0
+ };
+};
+
+/**
+ * @param {number} change - The amount to change the resources with (can be negative).
+ * @return {number} - The actual change in resourceSupply.
+ */
+ResourceSupply.prototype.Change = function(change)
+{
// Before changing the amount, activate Fogging if necessary to hide changes
let cmpFogging = Engine.QueryInterface(this.entity, IID_Fogging);
if (cmpFogging)
cmpFogging.Activate();
- if (this.IsInfinite())
- return { "amount": amount, "exhausted": false };
-
- let oldAmount = this.GetCurrentAmount();
- this.amount = Math.max(0, oldAmount - amount);
+ let oldAmount = this.amount;
+ this.amount = Math.min(Math.max(oldAmount + change, 0), this.maxAmount);
- let isExhausted = this.GetCurrentAmount() == 0;
- // Remove entities that have been exhausted
- if (isExhausted)
+ // Remove entities that have been exhausted.
+ if (this.amount == 0)
Engine.DestroyEntity(this.entity);
- Engine.PostMessage(this.entity, MT_ResourceSupplyChanged, { "from": oldAmount, "to": this.GetCurrentAmount() });
+ let actualChange = this.amount - oldAmount;
+ if (actualChange != 0)
+ {
+ Engine.PostMessage(this.entity, MT_ResourceSupplyChanged, {
+ "from": oldAmount,
+ "to": this.amount
+ });
+ this.CheckTimers();
+ }
+ return actualChange;
+};
- return { "amount": oldAmount - this.GetCurrentAmount(), "exhausted": isExhausted };
+/**
+ * @param {number} newValue - The value to set the current amount to.
+ */
+ResourceSupply.prototype.SetAmount = function(newValue)
+{
+ this.Change(newValue - this.amount);
};
/**
* @param {number} gathererID - The gatherer to add.
* @return {boolean} - Whether the gatherer was successfully added to the entity's gatherers list
* or the entity was already gathering us.
*/
ResourceSupply.prototype.AddGatherer = function(gathererID)
{
if (!this.IsAvailable())
return false;
if (this.IsGatheringUs(gathererID))
return true;
this.gatherers.push(gathererID);
Engine.PostMessage(this.entity, MT_ResourceSupplyNumGatherersChanged, { "to": this.GetNumGatherers() });
return true;
};
/**
+ * @param {number} player - The playerID owning the gatherer.
+ * @param {number} entity - The entityID gathering.
+ *
+ * @return {boolean} - Whether the gatherer was successfully added to the active-gatherers list
+ * or the entity was already in that list.
+ */
+ResourceSupply.prototype.AddActiveGatherer = function(entity)
+{
+ if (!this.AddGatherer(entity))
+ return false;
+
+ if (this.activeGatherers.indexOf(entity) == -1)
+ {
+ this.activeGatherers.push(entity);
+ this.CheckTimers();
+ }
+ return true;
+};
+
+/**
* @param {number} gathererID - The gatherer's entity id.
* @todo: Should this return false if the gatherer didn't gather from said resource?
*/
ResourceSupply.prototype.RemoveGatherer = function(gathererID)
{
let index = this.gatherers.indexOf(gathererID);
+ if (index != -1)
+ {
+ this.gatherers.splice(index, 1);
+ Engine.PostMessage(this.entity, MT_ResourceSupplyNumGatherersChanged, { "to": this.GetNumGatherers() });
+ }
+
+ index = this.activeGatherers.indexOf(gathererID);
if (index == -1)
return;
+ this.activeGatherers.splice(index, 1);
+ this.CheckTimers();
+};
- this.gatherers.splice(index, 1);
- Engine.PostMessage(this.entity, MT_ResourceSupplyNumGatherersChanged, { "to": this.GetNumGatherers() });
+/**
+ * Checks whether a timer ought to be added or removed.
+ */
+ResourceSupply.prototype.CheckTimers = function()
+{
+ if (!this.template.Change || this.IsInfinite())
+ return;
+
+ for (let changeKey in this.template.Change)
+ {
+ if (!this.CheckState(changeKey))
+ {
+ this.StopTimer(changeKey);
+ continue;
+ }
+ let template = this.template.Change[changeKey];
+ if (this.amount < +(template.LowerLimit || -1) ||
+ this.amount > +(template.UpperLimit || this.GetMaxAmount()))
+ {
+ this.StopTimer(changeKey);
+ continue;
+ }
+
+ if (this.cachedChanges[changeKey] == 0)
+ {
+ this.StopTimer(changeKey);
+ continue;
+ }
+
+ if (!this.timers[changeKey])
+ this.StartTimer(changeKey);
+ }
+};
+
+/**
+ * This verifies whether the current state of the supply matches the ones needed
+ * for the specific timer to run.
+ *
+ * @param {string} changeKey - The name of the Change to verify the state for.
+ * @return {boolean} - Whether the timer may run.
+ */
+ResourceSupply.prototype.CheckState = function(changeKey)
+{
+ let template = this.template.Change[changeKey];
+ if (!template.State)
+ return true;
+
+ let states = template.State;
+ let cmpHealth = Engine.QueryInterface(this.entity, IID_Health);
+ if (states.indexOf("alive") != -1 && !cmpHealth && states.indexOf("dead") == -1 ||
+ states.indexOf("dead") != -1 && cmpHealth && states.indexOf("alive") == -1)
+ return false;
+
+ let activeGatherers = this.GetNumActiveGatherers();
+ if (states.indexOf("gathered") != -1 && activeGatherers == 0 && states.indexOf("notGathered") == -1 ||
+ states.indexOf("notGathered") != -1 && activeGatherers > 0 && states.indexOf("gathered") == -1)
+ return false;
+
+ return true;
+};
+
+/**
+ * @param {string} changeKey - The name of the Change to apply to the entity.
+ */
+ResourceSupply.prototype.StartTimer = function(changeKey)
+{
+ if (this.timers[changeKey])
+ return;
+
+ let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
+ let interval = ApplyValueModificationsToEntity("ResourceSupply/Change/" + changeKey + "/Interval", +(this.template.Change[changeKey].Interval || 1000), this.entity);
+ this.timers[changeKey] = cmpTimer.SetInterval(this.entity, IID_ResourceSupply, "TimerTick", interval, interval, changeKey);
+};
+
+/**
+ * @param {string} changeKey - The name of the change to stop the timer for.
+ */
+ResourceSupply.prototype.StopTimer = function(changeKey)
+{
+ if (!this.timers[changeKey])
+ return;
+
+ let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
+ cmpTimer.CancelTimer(this.timers[changeKey]);
+ delete this.timers[changeKey];
+};
+
+/**
+ * @param {string} changeKey - The name of the change to apply to the entity.
+ */
+ResourceSupply.prototype.TimerTick = function(changeKey)
+{
+ let template = this.template.Change[changeKey];
+ if (!template || !this.Change(this.cachedChanges[changeKey]))
+ this.StopTimer(changeKey);
+};
+
+/**
+ * Since the supposed changes can be affected by modifications, and applying those
+ * are slow, do not calculate them every timer tick.
+ */
+ResourceSupply.prototype.RecalculateValues = function()
+{
+ this.maxAmount = ApplyValueModificationsToEntity("ResourceSupply/Max", +this.template.Max, this.entity);
+ if (!this.template.Change || this.IsInfinite())
+ return;
+
+ for (let changeKey in this.template.Change)
+ this.cachedChanges[changeKey] = ApplyValueModificationsToEntity("ResourceSupply/Change/" + changeKey + "/Value", +this.template.Change[changeKey].Value, this.entity);
+
+ this.CheckTimers();
+};
+
+/**
+ * @param {{ "component": string, "valueNames": string[] }} msg - Message containing a list of values that were changed.
+ */
+ResourceSupply.prototype.OnValueModification = function(msg)
+{
+ if (msg.component != "ResourceSupply")
+ return;
+
+ this.RecalculateValues();
+};
+
+/**
+ * @param {{ "from": number, "to": number }} msg - Message containing the old new owner.
+ */
+ResourceSupply.prototype.OnOwnershipChanged = function(msg)
+{
+ if (msg.to == INVALID_PLAYER)
+ {
+ let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
+ for (let changeKey in this.timers)
+ cmpTimer.CancelTimer(this.timers[changeKey]);
+ }
+ else
+ this.RecalculateValues();
+};
+
+/**
+ * @param {{ "entity": number, "newentity": number }} msg - Message to what the entity has been renamed.
+ */
+ResourceSupply.prototype.OnEntityRenamed = function(msg)
+{
+ let cmpResourceSupplyNew = Engine.QueryInterface(msg.newentity, IID_ResourceSupply);
+ if (cmpResourceSupplyNew)
+ cmpResourceSupplyNew.SetAmount(this.GetCurrentAmount());
};
Engine.RegisterComponentType(IID_ResourceSupply, "ResourceSupply", ResourceSupply);
Index: ps/trunk/binaries/data/mods/public/simulation/components/UnitAI.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/components/UnitAI.js (revision 24962)
+++ ps/trunk/binaries/data/mods/public/simulation/components/UnitAI.js (revision 24963)
@@ -1,6527 +1,6527 @@
function UnitAI() {}
UnitAI.prototype.Schema =
"Controls the unit's movement, attacks, etc, in response to commands from the player." +
"" +
"" +
"" +
"violent" +
"aggressive" +
"defensive" +
"passive" +
"standground" +
"skittish" +
"passive-defensive" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
""+
"" +
"";
// Unit stances.
// There some targeting options:
// targetVisibleEnemies: anything in vision range is a viable target
// targetAttackersAlways: anything that hurts us is a viable target,
// possibly overriding user orders!
// There are some response options, triggered when targets are detected:
// respondFlee: run away
// respondFleeOnSight: run away when an enemy is sighted
// respondChase: start chasing after the enemy
// respondChaseBeyondVision: start chasing, and don't stop even if it's out
// of this unit's vision range (though still visible to the player)
// respondStandGround: attack enemy but don't move at all
// respondHoldGround: attack enemy but don't move far from current position
// TODO: maybe add targetAggressiveEnemies (don't worry about lone scouts,
// do worry around armies slaughtering the guy standing next to you), etc.
var g_Stances = {
"violent": {
"targetVisibleEnemies": true,
"targetAttackersAlways": true,
"respondFlee": false,
"respondFleeOnSight": false,
"respondChase": true,
"respondChaseBeyondVision": true,
"respondStandGround": false,
"respondHoldGround": false,
"selectable": true
},
"aggressive": {
"targetVisibleEnemies": true,
"targetAttackersAlways": false,
"respondFlee": false,
"respondFleeOnSight": false,
"respondChase": true,
"respondChaseBeyondVision": false,
"respondStandGround": false,
"respondHoldGround": false,
"selectable": true
},
"defensive": {
"targetVisibleEnemies": true,
"targetAttackersAlways": false,
"respondFlee": false,
"respondFleeOnSight": false,
"respondChase": false,
"respondChaseBeyondVision": false,
"respondStandGround": false,
"respondHoldGround": true,
"selectable": true
},
"passive": {
"targetVisibleEnemies": false,
"targetAttackersAlways": false,
"respondFlee": true,
"respondFleeOnSight": false,
"respondChase": false,
"respondChaseBeyondVision": false,
"respondStandGround": false,
"respondHoldGround": false,
"selectable": true
},
"standground": {
"targetVisibleEnemies": true,
"targetAttackersAlways": false,
"respondFlee": false,
"respondFleeOnSight": false,
"respondChase": false,
"respondChaseBeyondVision": false,
"respondStandGround": true,
"respondHoldGround": false,
"selectable": true
},
"skittish": {
"targetVisibleEnemies": false,
"targetAttackersAlways": false,
"respondFlee": true,
"respondFleeOnSight": true,
"respondChase": false,
"respondChaseBeyondVision": false,
"respondStandGround": false,
"respondHoldGround": false,
"selectable": false
},
"passive-defensive": {
"targetVisibleEnemies": false,
"targetAttackersAlways": false,
"respondFlee": false,
"respondFleeOnSight": false,
"respondChase": false,
"respondChaseBeyondVision": false,
"respondStandGround": false,
"respondHoldGround": true,
"selectable": false
},
"none": {
// Only to be used by AI or trigger scripts
"targetVisibleEnemies": false,
"targetAttackersAlways": false,
"respondFlee": false,
"respondFleeOnSight": false,
"respondChase": false,
"respondChaseBeyondVision": false,
"respondStandGround": false,
"respondHoldGround": false,
"selectable": false
}
};
// These orders always require a packed unit, so if a unit that is unpacking is given one of these orders,
// it will immediately cancel unpacking.
var g_OrdersCancelUnpacking = new Set([
"FormationWalk",
"Walk",
"WalkAndFight",
"WalkToTarget",
"Patrol",
"Garrison"
]);
// When leaving a foundation, we want to be clear of it by this distance.
var g_LeaveFoundationRange = 4;
UnitAI.prototype.notifyToCheerInRange = 30;
const ACCEPT_ORDER = true;
const REJECT_ORDER = false;
// See ../helpers/FSM.js for some documentation of this FSM specification syntax
UnitAI.prototype.UnitFsmSpec = {
// Default event handlers:
"MovementUpdate": function(msg) {
// ignore spurious movement messages
// (these can happen when stopping moving at the same time
// as switching states)
},
"ConstructionFinished": function(msg) {
// ignore uninteresting construction messages
},
"LosRangeUpdate": function(msg) {
// Ignore newly-seen units by default.
},
"LosHealRangeUpdate": function(msg) {
// Ignore newly-seen injured units by default.
},
"LosAttackRangeUpdate": function(msg) {
// Ignore newly-seen enemy units by default.
},
"Attacked": function(msg) {
// ignore attacker
},
"PackFinished": function(msg) {
// ignore
},
"PickupCanceled": function(msg) {
// ignore
},
"TradingCanceled": function(msg) {
// ignore
},
"GuardedAttacked": function(msg) {
// ignore
},
"OrderTargetRenamed": function() {
// By default, trigger an exit-reenter
// so that state preconditions are checked against the new entity
// (there is no reason to assume the target is still valid).
this.SetNextState(this.GetCurrentState());
},
// Formation handlers:
"FormationLeave": function(msg) {
// Overloaded by FORMATIONMEMBER
// We end up here if LeaveFormation was called when the entity
// was executing an order in an individual state, so we must
// discard the order now that it has been executed.
if (this.order && this.order.type === "LeaveFormation")
this.FinishOrder();
},
// Called when being told to walk as part of a formation
"Order.FormationWalk": function(msg) {
if (!this.AbleToMove())
return REJECT_ORDER;
if (this.CanPack())
{
// If the controller is IDLE, this is just the regular reformation timer.
// In that case we don't actually want to move, as that would unpack us.
let cmpControllerAI = Engine.QueryInterface(this.GetFormationController(), IID_UnitAI);
if (cmpControllerAI.IsIdle())
return REJECT_ORDER;
this.PushOrderFront("Pack", { "force": true });
}
else
this.SetNextState("FORMATIONMEMBER.WALKING");
return ACCEPT_ORDER;
},
// Special orders:
// (these will be overridden by various states)
"Order.LeaveFoundation": function(msg) {
if (!this.WillMoveFromFoundation(msg.data.target))
return REJECT_ORDER;
this.order.data.min = g_LeaveFoundationRange;
this.SetNextState("INDIVIDUAL.WALKING");
return ACCEPT_ORDER;
},
// Individual orders:
"Order.LeaveFormation": function() {
if (!this.IsFormationMember())
return REJECT_ORDER;
let cmpFormation = Engine.QueryInterface(this.formationController, IID_Formation);
if (cmpFormation)
{
cmpFormation.SetRearrange(false);
// Triggers FormationLeave, which ultimately will FinishOrder,
// discarding this order.
cmpFormation.RemoveMembers([this.entity]);
cmpFormation.SetRearrange(true);
}
return ACCEPT_ORDER;
},
"Order.Stop": function(msg) {
this.FinishOrder();
return ACCEPT_ORDER;
},
"Order.Walk": function(msg) {
if (!this.AbleToMove())
return REJECT_ORDER;
if (this.CanPack())
{
this.PushOrderFront("Pack", { "force": true });
return ACCEPT_ORDER;
}
this.SetHeldPosition(this.order.data.x, this.order.data.z);
this.order.data.relaxed = true;
this.SetNextState("INDIVIDUAL.WALKING");
return ACCEPT_ORDER;
},
"Order.WalkAndFight": function(msg) {
if (!this.AbleToMove())
return REJECT_ORDER;
if (this.CanPack())
{
this.PushOrderFront("Pack", { "force": true });
return ACCEPT_ORDER;
}
this.SetHeldPosition(this.order.data.x, this.order.data.z);
this.order.data.relaxed = true;
this.SetNextState("INDIVIDUAL.WALKINGANDFIGHTING");
return ACCEPT_ORDER;
},
"Order.WalkToTarget": function(msg) {
if (!this.AbleToMove())
return REJECT_ORDER;
if (this.CanPack())
{
this.PushOrderFront("Pack", { "force": true });
return ACCEPT_ORDER;
}
if (this.CheckRange(this.order.data))
return REJECT_ORDER;
this.order.data.relaxed = true;
this.SetNextState("INDIVIDUAL.WALKING");
return ACCEPT_ORDER;
},
"Order.PickupUnit": function(msg) {
let cmpGarrisonHolder = Engine.QueryInterface(this.entity, IID_GarrisonHolder);
if (!cmpGarrisonHolder || cmpGarrisonHolder.IsFull())
return REJECT_ORDER;
let range = cmpGarrisonHolder.GetLoadingRange();
this.order.data.min = range.min;
this.order.data.max = range.max;
if (this.CheckRange(this.order.data))
return REJECT_ORDER;
// Check if we need to move
// If the target can reach us and we are reasonably close, don't move.
// TODO: it would be slightly more optimal to check for real, not bird-flight distance.
let cmpPassengerMotion = Engine.QueryInterface(this.order.data.target, IID_UnitMotion);
if (cmpPassengerMotion &&
cmpPassengerMotion.IsTargetRangeReachable(this.entity, range.min, range.max) &&
PositionHelper.DistanceBetweenEntities(this.entity, this.order.data.target) < 200)
this.SetNextState("INDIVIDUAL.PICKUP.LOADING");
else
this.SetNextState("INDIVIDUAL.PICKUP.APPROACHING");
return ACCEPT_ORDER;
},
"Order.Guard": function(msg) {
if (!this.AddGuard(this.order.data.target))
return REJECT_ORDER;
if (!this.CheckTargetRangeExplicit(this.isGuardOf, 0, this.guardRange))
this.SetNextState("INDIVIDUAL.GUARD.ESCORTING");
else
this.SetNextState("INDIVIDUAL.GUARD.GUARDING");
return ACCEPT_ORDER;
},
"Order.Flee": function(msg) {
this.SetNextState("INDIVIDUAL.FLEEING");
return ACCEPT_ORDER;
},
"Order.Attack": function(msg) {
let type = this.GetBestAttackAgainst(this.order.data.target, this.order.data.allowCapture);
if (!type)
return REJECT_ORDER;
this.order.data.attackType = type;
this.RememberTargetPosition();
if (this.order.data.hunting && this.orderQueue.length > 1 && this.orderQueue[1].type === "Gather")
this.RememberTargetPosition(this.orderQueue[1].data);
if (this.CheckTargetAttackRange(this.order.data.target, this.order.data.attackType))
{
if (this.CanUnpack())
{
this.PushOrderFront("Unpack", { "force": true });
return ACCEPT_ORDER;
}
// Cancel any current packing order.
if (this.EnsureCorrectPackStateForAttack(false))
this.SetNextState("INDIVIDUAL.COMBAT.ATTACKING");
return ACCEPT_ORDER;
}
// If we're hunting, that's a special case where we should continue attacking our target.
if (this.GetStance().respondStandGround && !this.order.data.force && !this.order.data.hunting || !this.AbleToMove())
return REJECT_ORDER;
if (this.CanPack())
{
this.PushOrderFront("Pack", { "force": true });
return ACCEPT_ORDER;
}
// If we're currently packing/unpacking, make sure we are packed, so we can move.
if (this.EnsureCorrectPackStateForAttack(true))
this.SetNextState("INDIVIDUAL.COMBAT.APPROACHING");
return ACCEPT_ORDER;
},
"Order.Patrol": function(msg) {
if (!this.AbleToMove())
return REJECT_ORDER;
if (this.CanPack())
{
this.PushOrderFront("Pack", { "force": true });
return ACCEPT_ORDER;
}
this.order.data.relaxed = true;
this.SetNextState("INDIVIDUAL.PATROL.PATROLLING");
return ACCEPT_ORDER;
},
"Order.Heal": function(msg) {
if (!this.TargetIsAlive(this.order.data.target))
return REJECT_ORDER;
// Healers can't heal themselves.
if (this.order.data.target == this.entity)
return REJECT_ORDER;
if (this.CheckTargetRange(this.order.data.target, IID_Heal))
{
this.SetNextState("INDIVIDUAL.HEAL.HEALING");
return ACCEPT_ORDER;
}
if (this.GetStance().respondStandGround && !this.order.data.force)
return REJECT_ORDER;
this.SetNextState("INDIVIDUAL.HEAL.APPROACHING");
return ACCEPT_ORDER;
},
"Order.Gather": function(msg) {
if (!this.CanGather(this.order.data.target))
{
this.SetNextState("INDIVIDUAL.GATHER.FINDINGNEWTARGET");
return ACCEPT_ORDER;
}
// If the unit is full go to the nearest dropsite instead of trying to gather.
// Unless our target is a treasure which we cannot be full enough with (we can't carry treasures).
let cmpResourceGatherer = Engine.QueryInterface(this.entity, IID_ResourceGatherer);
if (msg.data.type.generic !== "treasure" && cmpResourceGatherer && !cmpResourceGatherer.CanCarryMore(msg.data.type.generic))
{
let nearestDropsite = this.FindNearestDropsite(msg.data.type.generic);
if (nearestDropsite)
this.PushOrderFront("ReturnResource", {
"target": nearestDropsite,
"force": false,
"type": msg.data.type
});
// Players expect the unit to move, so walk to the target instead of trying to gather.
else if (!this.FinishOrder())
this.WalkToTarget(msg.data.target, false);
return ACCEPT_ORDER;
}
if (this.MustKillGatherTarget(this.order.data.target))
{
// Make sure we can attack the target, else we'll get very stuck
if (!this.GetBestAttackAgainst(this.order.data.target, false))
{
// Oops, we can't attack at all - give up
// TODO: should do something so the player knows why this failed
return REJECT_ORDER;
}
// The target was visible when this order was issued,
// but could now be invisible again.
if (!this.CheckTargetVisible(this.order.data.target))
{
if (this.order.data.secondTry === undefined)
{
this.order.data.secondTry = true;
this.PushOrderFront("Walk", this.order.data.lastPos);
}
// We couldn't move there, or the target moved away
else
{
let data = this.order.data;
if (!this.FinishOrder())
this.PushOrderFront("GatherNearPosition", {
"x": data.lastPos.x,
"z": data.lastPos.z,
"type": data.type,
"template": data.template
});
}
return ACCEPT_ORDER;
}
this.PushOrderFront("Attack", { "target": this.order.data.target, "force": !!this.order.data.force, "hunting": true, "allowCapture": false });
return ACCEPT_ORDER;
}
this.RememberTargetPosition();
if (!this.order.data.initPos)
this.order.data.initPos = this.order.data.lastPos;
if (this.CheckTargetRange(this.order.data.target, IID_ResourceGatherer))
this.SetNextState("INDIVIDUAL.GATHER.GATHERING");
else
this.SetNextState("INDIVIDUAL.GATHER.APPROACHING");
return ACCEPT_ORDER;
},
"Order.GatherNearPosition": function(msg) {
this.SetNextState("INDIVIDUAL.GATHER.WALKING");
this.order.data.initPos = { 'x': this.order.data.x, 'z': this.order.data.z };
this.order.data.relaxed = true;
return ACCEPT_ORDER;
},
"Order.ReturnResource": function(msg) {
let cmpResourceGatherer = Engine.QueryInterface(this.entity, IID_ResourceGatherer);
if (this.CheckTargetRange(this.order.data.target, IID_ResourceGatherer) &&
this.CanReturnResource(this.order.data.target, true, cmpResourceGatherer))
{
cmpResourceGatherer.CommitResources(this.order.data.target);
this.SetDefaultAnimationVariant();
// Our next order should always be a Gather,
// so just switch back to that order.
this.FinishOrder();
}
else
this.SetNextState("INDIVIDUAL.RETURNRESOURCE.APPROACHING");
return ACCEPT_ORDER;
},
"Order.Trade": function(msg) {
// We must check if this trader has both markets in case it was a back-to-work order.
let cmpTrader = Engine.QueryInterface(this.entity, IID_Trader);
if (!cmpTrader || !cmpTrader.HasBothMarkets())
return REJECT_ORDER;
this.waypoints = [];
this.SetNextState("TRADE.APPROACHINGMARKET");
return ACCEPT_ORDER;
},
"Order.Repair": function(msg) {
if (this.CheckTargetRange(this.order.data.target, IID_Builder))
this.SetNextState("INDIVIDUAL.REPAIR.REPAIRING");
else
this.SetNextState("INDIVIDUAL.REPAIR.APPROACHING");
return ACCEPT_ORDER;
},
"Order.Garrison": function(msg) {
if (!this.AbleToMove())
{
// Garrisoned turrets (unable to move) go IDLE.
this.SetNextState("IDLE");
return ACCEPT_ORDER;
}
if (this.IsGarrisoned())
{
this.SetNextState("INDIVIDUAL.GARRISON.GARRISONED");
return ACCEPT_ORDER;
}
// Also pack when we are in range.
if (this.CanPack())
{
this.PushOrderFront("Pack", { "force": true });
return ACCEPT_ORDER;
}
if (this.CheckGarrisonRange(this.order.data.target))
this.SetNextState("INDIVIDUAL.GARRISON.GARRISONED");
else
this.SetNextState("INDIVIDUAL.GARRISON.APPROACHING");
return ACCEPT_ORDER;
},
"Order.Ungarrison": function() {
this.FinishOrder();
this.isGarrisoned = false;
return ACCEPT_ORDER;
},
"Order.Cheer": function(msg) {
return REJECT_ORDER;
},
"Order.Pack": function(msg) {
if (!this.CanPack())
return REJECT_ORDER;
this.SetNextState("INDIVIDUAL.PACKING");
return ACCEPT_ORDER;
},
"Order.Unpack": function(msg) {
if (!this.CanUnpack())
return REJECT_ORDER;
this.SetNextState("INDIVIDUAL.UNPACKING");
return ACCEPT_ORDER;
},
"Order.MoveToChasingPoint": function(msg) {
// Overriden by the CHASING state.
// Can however happen outside of it when renaming...
// TODO: don't use an order for that behaviour.
return REJECT_ORDER;
},
// States for the special entity representing a group of units moving in formation:
"FORMATIONCONTROLLER": {
"Order.Walk": function(msg) {
this.CallMemberFunction("SetHeldPosition", [msg.data.x, msg.data.z]);
this.SetNextState("WALKING");
return ACCEPT_ORDER;
},
"Order.WalkAndFight": function(msg) {
this.CallMemberFunction("SetHeldPosition", [msg.data.x, msg.data.z]);
this.SetNextState("WALKINGANDFIGHTING");
return ACCEPT_ORDER;
},
"Order.MoveIntoFormation": function(msg) {
this.CallMemberFunction("SetHeldPosition", [msg.data.x, msg.data.z]);
this.SetNextState("FORMING");
return ACCEPT_ORDER;
},
// Only used by other orders to walk there in formation.
"Order.WalkToTargetRange": function(msg) {
if (this.CheckRange(this.order.data))
return REJECT_ORDER;
this.SetNextState("WALKING");
return ACCEPT_ORDER;
},
"Order.WalkToTarget": function(msg) {
if (this.CheckRange(this.order.data))
return REJECT_ORDER;
this.SetNextState("WALKING");
return ACCEPT_ORDER;
},
"Order.WalkToPointRange": function(msg) {
if (this.CheckRange(this.order.data))
return REJECT_ORDER;
this.SetNextState("WALKING");
return ACCEPT_ORDER;
},
"Order.Patrol": function(msg) {
this.CallMemberFunction("SetHeldPosition", [msg.data.x, msg.data.z]);
this.SetNextState("PATROL.PATROLLING");
return ACCEPT_ORDER;
},
"Order.Guard": function(msg) {
this.CallMemberFunction("Guard", [msg.data.target, false]);
Engine.QueryInterface(this.entity, IID_Formation).Disband();
return ACCEPT_ORDER;
},
"Order.Stop": function(msg) {
let cmpFormation = Engine.QueryInterface(this.entity, IID_Formation);
cmpFormation.ResetOrderVariant();
if (!this.IsAttackingAsFormation())
this.CallMemberFunction("Stop", [false]);
this.FinishOrder();
return ACCEPT_ORDER;
// Don't move the members back into formation,
// as the formation then resets and it looks odd when walk-stopping.
// TODO: this should be improved in the formation reshaping code.
},
"Order.Attack": function(msg) {
let target = msg.data.target;
let allowCapture = msg.data.allowCapture;
let cmpTargetUnitAI = Engine.QueryInterface(target, IID_UnitAI);
if (cmpTargetUnitAI && cmpTargetUnitAI.IsFormationMember())
target = cmpTargetUnitAI.GetFormationController();
if (!this.CheckFormationTargetAttackRange(target))
{
if (this.CanAttack(target) && this.CheckTargetVisible(target))
{
this.SetNextState("COMBAT.APPROACHING");
return ACCEPT_ORDER;
}
return REJECT_ORDER;
}
this.CallMemberFunction("Attack", [target, allowCapture, false]);
let cmpAttack = Engine.QueryInterface(this.entity, IID_Attack);
if (cmpAttack && cmpAttack.CanAttackAsFormation())
this.SetNextState("COMBAT.ATTACKING");
else
this.SetNextState("MEMBER");
return ACCEPT_ORDER;
},
"Order.Garrison": function(msg) {
if (!Engine.QueryInterface(msg.data.target, IID_GarrisonHolder))
return REJECT_ORDER;
if (!this.CheckGarrisonRange(msg.data.target))
{
if (!this.CheckTargetVisible(msg.data.target))
return REJECT_ORDER;
this.SetNextState("GARRISON.APPROACHING");
}
else
this.SetNextState("GARRISON.GARRISONING");
return ACCEPT_ORDER;
},
"Order.Gather": function(msg) {
if (this.MustKillGatherTarget(msg.data.target))
{
// The target was visible when this order was given,
// but could now be invisible.
if (!this.CheckTargetVisible(msg.data.target))
{
if (msg.data.secondTry === undefined)
{
msg.data.secondTry = true;
this.PushOrderFront("Walk", msg.data.lastPos);
}
// We couldn't move there, or the target moved away
else
{
let data = msg.data;
if (!this.FinishOrder())
this.PushOrderFront("GatherNearPosition", {
"x": data.lastPos.x,
"z": data.lastPos.z,
"type": data.type,
"template": data.template
});
}
return ACCEPT_ORDER;
}
this.PushOrderFront("Attack", { "target": msg.data.target, "force": !!msg.data.force, "hunting": true, "allowCapture": false, "min": 0, "max": 10 });
return ACCEPT_ORDER;
}
// TODO: on what should we base this range?
if (!this.CheckTargetRangeExplicit(msg.data.target, 0, 10))
{
if (!this.CanGather(msg.data.target) || !this.CheckTargetVisible(msg.data.target))
return REJECT_ORDER;
// TODO: Should we issue a gather-near-position order
// if the target isn't gatherable/doesn't exist anymore?
if (!msg.data.secondTry)
{
msg.data.secondTry = true;
this.PushOrderFront("WalkToTargetRange", { "target": msg.data.target, "min": 0, "max": 10 });
return ACCEPT_ORDER;
}
return REJECT_ORDER;
}
this.CallMemberFunction("Gather", [msg.data.target, false]);
this.SetNextState("MEMBER");
return ACCEPT_ORDER;
},
"Order.GatherNearPosition": function(msg) {
// TODO: on what should we base this range?
if (!this.CheckPointRangeExplicit(msg.data.x, msg.data.z, 0, 20))
{
// Out of range; move there in formation
this.PushOrderFront("WalkToPointRange", { "x": msg.data.x, "z": msg.data.z, "min": 0, "max": 20 });
return ACCEPT_ORDER;
}
this.CallMemberFunction("GatherNearPosition", [msg.data.x, msg.data.z, msg.data.type, msg.data.template, false]);
this.SetNextState("MEMBER");
return ACCEPT_ORDER;
},
"Order.Heal": function(msg) {
// TODO: on what should we base this range?
if (!this.CheckTargetRangeExplicit(msg.data.target, 0, 10))
{
if (!this.TargetIsAlive(msg.data.target) || !this.CheckTargetVisible(msg.data.target))
return REJECT_ORDER;
if (!msg.data.secondTry)
{
msg.data.secondTry = true;
this.PushOrderFront("WalkToTargetRange", { "target": msg.data.target, "min": 0, "max": 10 });
return ACCEPT_ORDER;
}
return REJECT_ORDER;
}
this.CallMemberFunction("Heal", [msg.data.target, false]);
this.SetNextState("MEMBER");
return ACCEPT_ORDER;
},
"Order.Repair": function(msg) {
// TODO: on what should we base this range?
if (!this.CheckTargetRangeExplicit(msg.data.target, 0, 10))
{
if (!this.TargetIsAlive(msg.data.target) || !this.CheckTargetVisible(msg.data.target))
return REJECT_ORDER;
if (!msg.data.secondTry)
{
msg.data.secondTry = true;
this.PushOrderFront("WalkToTargetRange", { "target": msg.data.target, "min": 0, "max": 10 });
return ACCEPT_ORDER;
}
return REJECT_ORDER;
}
this.CallMemberFunction("Repair", [msg.data.target, msg.data.autocontinue, false]);
this.SetNextState("MEMBER");
return ACCEPT_ORDER;
},
"Order.ReturnResource": function(msg) {
// TODO: on what should we base this range?
if (!this.CheckTargetRangeExplicit(msg.data.target, 0, 10))
{
if (!this.CheckTargetVisible(msg.data.target))
return REJECT_ORDER;
if (!msg.data.secondTry)
{
msg.data.secondTry = true;
this.PushOrderFront("WalkToTargetRange", { "target": msg.data.target, "min": 0, "max": 10 });
return ACCEPT_ORDER;
}
return REJECT_ORDER;
}
this.CallMemberFunction("ReturnResource", [msg.data.target, false]);
this.SetNextState("MEMBER");
return ACCEPT_ORDER;
},
"Order.Pack": function(msg) {
this.CallMemberFunction("Pack", [false]);
this.SetNextState("MEMBER");
return ACCEPT_ORDER;
},
"Order.Unpack": function(msg) {
this.CallMemberFunction("Unpack", [false]);
this.SetNextState("MEMBER");
return ACCEPT_ORDER;
},
"IDLE": {
"enter": function(msg) {
// Turn rearrange off. Otherwise, if the formation is idle
// but individual units go off to fight,
// any death will rearrange the formation, which looks odd.
// Instead, move idle units in formation on a timer.
let cmpFormation = Engine.QueryInterface(this.entity, IID_Formation);
cmpFormation.SetRearrange(false);
// Start the timer on the next turn to catch up with potential stragglers.
this.StartTimer(100, 2000);
this.isIdle = true;
this.CallMemberFunction("ResetIdle");
return false;
},
"leave": function() {
this.isIdle = false;
this.StopTimer();
},
"Timer": function(msg) {
let cmpFormation = Engine.QueryInterface(this.entity, IID_Formation);
if (!cmpFormation)
return;
if (this.TestAllMemberFunction("IsIdle"))
cmpFormation.MoveMembersIntoFormation(false, false);
},
},
"WALKING": {
"enter": function() {
let cmpFormation = Engine.QueryInterface(this.entity, IID_Formation);
cmpFormation.SetRearrange(true);
cmpFormation.MoveMembersIntoFormation(true, true);
if (!this.MoveTo(this.order.data))
{
this.FinishOrder();
return true;
}
return false;
},
"leave": function() {
this.StopTimer();
this.StopMoving();
},
"MovementUpdate": function(msg) {
if (msg.veryObstructed && !this.timer)
{
// It's possible that the controller (with large clearance)
// is stuck, but not the individual units.
// Ask them to move individually for a little while.
this.CallMemberFunction("MoveTo", [this.order.data]);
this.StartTimer(3000);
return;
}
else if (this.timer)
return;
if (msg.likelyFailure || this.CheckRange(this.order.data))
this.FinishOrder();
},
"Timer": function() {
// Reenter to reset the pathfinder state.
this.SetNextState("WALKING");
}
},
"WALKINGANDFIGHTING": {
"enter": function(msg) {
let cmpFormation = Engine.QueryInterface(this.entity, IID_Formation);
cmpFormation.SetRearrange(true);
cmpFormation.MoveMembersIntoFormation(true, true, "combat");
if (!this.MoveTo(this.order.data))
{
this.FinishOrder();
return true;
}
this.StartTimer(0, 1000);
return false;
},
"leave": function() {
this.StopMoving();
this.StopTimer();
},
"Timer": function(msg) {
this.FindWalkAndFightTargets();
},
"MovementUpdate": function(msg) {
if (msg.likelyFailure || this.CheckRange(this.order.data))
this.FinishOrder();
},
},
"PATROL": {
"enter": function() {
let cmpPosition = Engine.QueryInterface(this.entity, IID_Position);
if (!cmpPosition || !cmpPosition.IsInWorld())
{
this.FinishOrder();
return true;
}
// Memorize the origin position in case that we want to go back.
if (!this.patrolStartPosOrder)
{
this.patrolStartPosOrder = cmpPosition.GetPosition();
this.patrolStartPosOrder.targetClasses = this.order.data.targetClasses;
this.patrolStartPosOrder.allowCapture = this.order.data.allowCapture;
}
this.SetAnimationVariant("combat");
return false;
},
"leave": function() {
delete this.patrolStartPosOrder;
this.SetDefaultAnimationVariant();
},
"PATROLLING": {
"enter": function() {
let cmpFormation = Engine.QueryInterface(this.entity, IID_Formation);
cmpFormation.SetRearrange(true);
cmpFormation.MoveMembersIntoFormation(true, true, "combat");
let cmpPosition = Engine.QueryInterface(this.entity, IID_Position);
if (!cmpPosition || !cmpPosition.IsInWorld() ||
!this.MoveTo(this.order.data))
{
this.FinishOrder();
return true;
}
this.StartTimer(0, 1000);
return false;
},
"leave": function() {
this.StopMoving();
this.StopTimer();
},
"Timer": function(msg) {
this.FindWalkAndFightTargets();
},
"MovementUpdate": function(msg) {
if (!msg.likelyFailure && !msg.likelySuccess && !this.RelaxedMaxRangeCheck(this.order.data, this.DefaultRelaxedMaxRange))
return;
if (this.orderQueue.length == 1)
this.PushOrder("Patrol", this.patrolStartPosOrder);
this.PushOrder(this.order.type, this.order.data);
this.SetNextState("CHECKINGWAYPOINT");
},
},
"CHECKINGWAYPOINT": {
"enter": function() {
this.StartTimer(0, 1000);
this.stopSurveying = 0;
// TODO: pick a proper animation
return false;
},
"leave": function() {
this.StopTimer();
delete this.stopSurveying;
},
"Timer": function(msg) {
if (this.stopSurveying >= +this.template.PatrolWaitTime)
{
this.FinishOrder();
return;
}
this.FindWalkAndFightTargets();
++this.stopSurveying;
}
}
},
"GARRISON": {
"APPROACHING": {
"enter": function() {
let cmpFormation = Engine.QueryInterface(this.entity, IID_Formation);
cmpFormation.SetRearrange(true);
cmpFormation.MoveMembersIntoFormation(true, true);
if (!this.MoveToGarrisonRange(this.order.data.target))
{
this.FinishOrder();
return true;
}
// If the garrisonholder should pickup, warn it so it can take needed action.
let cmpGarrisonHolder = Engine.QueryInterface(this.order.data.target, IID_GarrisonHolder);
if (cmpGarrisonHolder && cmpGarrisonHolder.CanPickup(this.entity))
{
this.pickup = this.order.data.target; // temporary, deleted in "leave"
Engine.PostMessage(this.pickup, MT_PickupRequested, { "entity": this.entity });
}
return false;
},
"leave": function() {
this.StopMoving();
if (this.pickup)
{
Engine.PostMessage(this.pickup, MT_PickupCanceled, { "entity": this.entity });
delete this.pickup;
}
},
"MovementUpdate": function(msg) {
if (msg.likelyFailure || msg.likelySuccess)
this.SetNextState("GARRISONING");
},
},
"GARRISONING": {
"enter": function() {
this.CallMemberFunction("Garrison", [this.order.data.target, false]);
// We might have been disbanded due to the lack of members.
if (Engine.QueryInterface(this.entity, IID_Formation).GetMemberCount())
this.SetNextState("MEMBER");
return true;
},
},
},
"FORMING": {
"enter": function() {
let cmpFormation = Engine.QueryInterface(this.entity, IID_Formation);
cmpFormation.SetRearrange(true);
cmpFormation.MoveMembersIntoFormation(true, true);
if (!this.MoveTo(this.order.data))
{
this.FinishOrder();
return true;
}
return false;
},
"leave": function() {
this.StopMoving();
},
"MovementUpdate": function(msg) {
if (!msg.likelyFailure && !this.CheckRange(this.order.data))
return;
this.FinishOrder();
}
},
"COMBAT": {
"APPROACHING": {
"enter": function() {
let cmpFormation = Engine.QueryInterface(this.entity, IID_Formation);
cmpFormation.SetRearrange(true);
cmpFormation.MoveMembersIntoFormation(true, true, "combat");
if (!this.MoveFormationToTargetAttackRange(this.order.data.target))
{
this.FinishOrder();
return true;
}
return false;
},
"leave": function() {
this.StopMoving();
},
"MovementUpdate": function(msg) {
let target = this.order.data.target;
let cmpTargetUnitAI = Engine.QueryInterface(target, IID_UnitAI);
if (cmpTargetUnitAI && cmpTargetUnitAI.IsFormationMember())
target = cmpTargetUnitAI.GetFormationController();
let cmpAttack = Engine.QueryInterface(this.entity, IID_Attack);
this.CallMemberFunction("Attack", [target, this.order.data.allowCapture, false]);
if (cmpAttack.CanAttackAsFormation())
this.SetNextState("COMBAT.ATTACKING");
else
this.SetNextState("MEMBER");
},
},
"ATTACKING": {
// Wait for individual members to finish
"enter": function(msg) {
let target = this.order.data.target;
let allowCapture = this.order.data.allowCapture;
if (!this.CheckFormationTargetAttackRange(target))
{
if (this.CanAttack(target) && this.CheckTargetVisible(target))
{
this.SetNextState("COMBAT.APPROACHING");
return true;
}
this.FinishOrder();
return true;
}
let cmpFormation = Engine.QueryInterface(this.entity, IID_Formation);
// TODO fix the rearranging while attacking as formation
cmpFormation.SetRearrange(!this.IsAttackingAsFormation());
cmpFormation.MoveMembersIntoFormation(false, false, "combat");
this.StartTimer(200, 200);
return false;
},
"Timer": function(msg) {
let target = this.order.data.target;
let allowCapture = this.order.data.allowCapture;
if (!this.CheckFormationTargetAttackRange(target))
{
if (this.CanAttack(target) && this.CheckTargetVisible(target))
{
this.SetNextState("COMBAT.APPROACHING");
return;
}
this.FinishOrder();
return;
}
},
"leave": function(msg) {
this.StopTimer();
var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation);
if (cmpFormation)
cmpFormation.SetRearrange(true);
},
},
},
// Wait for individual members to finish
"MEMBER": {
"OrderTargetRenamed": function(msg) {
// In general, don't react - we don't want to send spurious messages to members.
// This looks odd for hunting however because we wait for all
// entities to have clumped around the dead resource before proceeding
// so explicitly handle this case.
if (this.order && this.order.data && this.order.data.hunting &&
this.order.data.target == msg.data.newentity &&
this.orderQueue.length > 1)
this.FinishOrder();
},
"enter": function(msg) {
// Don't rearrange the formation, as that forces all units to stop
// what they're doing.
let cmpFormation = Engine.QueryInterface(this.entity, IID_Formation);
if (cmpFormation)
cmpFormation.SetRearrange(false);
// While waiting on members, the formation is more like
// a group of unit and does not have a well-defined position,
// so move the controller out of the world to enforce that.
let cmpPosition = Engine.QueryInterface(this.entity, IID_Position);
if (cmpPosition && cmpPosition.IsInWorld())
cmpPosition.MoveOutOfWorld();
this.StartTimer(1000, 1000);
return false;
},
"Timer": function(msg) {
let cmpFormation = Engine.QueryInterface(this.entity, IID_Formation);
if (cmpFormation && !cmpFormation.AreAllMembersWaiting())
return;
if (this.FinishOrder())
{
if (this.IsWalkingAndFighting())
this.FindWalkAndFightTargets();
return;
}
return;
},
"leave": function(msg) {
this.StopTimer();
// Reform entirely as members might be all over the place now.
let cmpFormation = Engine.QueryInterface(this.entity, IID_Formation);
if (cmpFormation)
cmpFormation.MoveMembersIntoFormation(true);
// Update the held position so entities respond to orders.
let cmpPosition = Engine.QueryInterface(this.entity, IID_Position);
if (cmpPosition && cmpPosition.IsInWorld())
{
let pos = cmpPosition.GetPosition2D();
this.CallMemberFunction("SetHeldPosition", [pos.x, pos.y]);
}
},
},
},
// States for entities moving as part of a formation:
"FORMATIONMEMBER": {
"FormationLeave": function(msg) {
// Stop moving as soon as the formation disbands
// Keep current rotation
let facePointAfterMove = this.GetFacePointAfterMove();
this.SetFacePointAfterMove(false);
this.StopMoving();
this.SetFacePointAfterMove(facePointAfterMove);
// If the controller handled an order but some members rejected it,
// they will have no orders and be in the FORMATIONMEMBER.IDLE state.
if (this.orderQueue.length)
{
// We're leaving the formation, so stop our FormationWalk order
if (this.FinishOrder())
return;
}
this.formationAnimationVariant = undefined;
this.SetNextState("INDIVIDUAL.IDLE");
},
// Override the LeaveFoundation order since we're not doing
// anything more important (and we might be stuck in the WALKING
// state forever and need to get out of foundations in that case)
"Order.LeaveFoundation": function(msg) {
if (!this.WillMoveFromFoundation(msg.data.target))
return REJECT_ORDER;
this.order.data.min = g_LeaveFoundationRange;
this.SetNextState("WALKINGTOPOINT");
return ACCEPT_ORDER;
},
"enter": function() {
let cmpFormation = Engine.QueryInterface(this.formationController, IID_Formation);
if (cmpFormation)
{
this.formationAnimationVariant = cmpFormation.GetFormationAnimationVariant(this.entity);
if (this.formationAnimationVariant)
this.SetAnimationVariant(this.formationAnimationVariant);
else
this.SetDefaultAnimationVariant();
}
return false;
},
"leave": function() {
this.SetDefaultAnimationVariant();
this.formationAnimationVariant = undefined;
},
"IDLE": "INDIVIDUAL.IDLE",
"CHEERING": "INDIVIDUAL.CHEERING",
"WALKING": {
"enter": function() {
let cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion);
cmpUnitMotion.MoveToFormationOffset(this.order.data.target, this.order.data.x, this.order.data.z);
if (this.order.data.offsetsChanged)
{
let cmpFormation = Engine.QueryInterface(this.formationController, IID_Formation);
if (cmpFormation)
this.formationAnimationVariant = cmpFormation.GetFormationAnimationVariant(this.entity);
}
if (this.formationAnimationVariant)
this.SetAnimationVariant(this.formationAnimationVariant);
else if (this.order.data.variant)
this.SetAnimationVariant(this.order.data.variant);
else
this.SetDefaultAnimationVariant();
return false;
},
"leave": function() {
// Don't use the logic from unitMotion, as SetInPosition
// has already given us a custom rotation
// (or we failed to move and thus don't care.)
let facePointAfterMove = this.GetFacePointAfterMove();
this.SetFacePointAfterMove(false);
this.StopMoving();
this.SetFacePointAfterMove(facePointAfterMove);
},
// Occurs when the unit has reached its destination and the controller
// is done moving. The controller is notified.
"MovementUpdate": function(msg) {
// When walking in formation, we'll only get notified in case of failure
// if the formation controller has stopped walking.
// Formations can start lagging a lot if many entities request short path
// so prefer to finish order early than retry pathing.
// (see https://code.wildfiregames.com/rP23806)
// (if the message is likelyFailure of likelySuccess, we also want to stop).
this.FinishOrder();
},
},
// Special case used by Order.LeaveFoundation
"WALKINGTOPOINT": {
"enter": function() {
if (!this.MoveTo(this.order.data))
{
this.FinishOrder();
return true;
}
return false;
},
"leave": function() {
this.StopMoving();
},
"MovementUpdate": function() {
if (!this.CheckRange(this.order.data))
return;
this.FinishOrder();
},
},
},
// States for entities not part of a formation:
"INDIVIDUAL": {
"Attacked": function(msg) {
if (this.GetStance().targetAttackersAlways || !this.order || !this.order.data || !this.order.data.force)
this.RespondToTargetedEntities([msg.data.attacker]);
},
"GuardedAttacked": function(msg) {
// do nothing if we have a forced order in queue before the guard order
for (var i = 0; i < this.orderQueue.length; ++i)
{
if (this.orderQueue[i].type == "Guard")
break;
if (this.orderQueue[i].data && this.orderQueue[i].data.force)
return;
}
// if we already are targeting another unit still alive, finish with it first
if (this.order && (this.order.type == "WalkAndFight" || this.order.type == "Attack"))
if (this.order.data.target != msg.data.attacker && this.CanAttack(msg.data.attacker))
return;
var cmpIdentity = Engine.QueryInterface(this.entity, IID_Identity);
var cmpHealth = Engine.QueryInterface(this.isGuardOf, IID_Health);
if (cmpIdentity && cmpIdentity.HasClass("Support") &&
cmpHealth && cmpHealth.IsInjured())
{
if (this.CanHeal(this.isGuardOf))
this.PushOrderFront("Heal", { "target": this.isGuardOf, "force": false });
else if (this.CanRepair(this.isGuardOf))
this.PushOrderFront("Repair", { "target": this.isGuardOf, "autocontinue": false, "force": false });
return;
}
var cmpBuildingAI = Engine.QueryInterface(msg.data.attacker, IID_BuildingAI);
if (cmpBuildingAI && this.CanRepair(this.isGuardOf))
{
this.PushOrderFront("Repair", { "target": this.isGuardOf, "autocontinue": false, "force": false });
return;
}
if (this.CheckTargetVisible(msg.data.attacker))
this.PushOrderFront("Attack", { "target": msg.data.attacker, "force": false, "allowCapture": true });
else
{
var cmpPosition = Engine.QueryInterface(msg.data.attacker, IID_Position);
if (!cmpPosition || !cmpPosition.IsInWorld())
return;
var pos = cmpPosition.GetPosition();
this.PushOrderFront("WalkAndFight", { "x": pos.x, "z": pos.z, "target": msg.data.attacker, "force": false });
// if we already had a WalkAndFight, keep only the most recent one in case the target has moved
if (this.orderQueue[1] && this.orderQueue[1].type == "WalkAndFight")
{
this.orderQueue.splice(1, 1);
Engine.PostMessage(this.entity, MT_UnitAIOrderDataChanged, { "to": this.GetOrderData() });
}
}
},
"IDLE": {
"Order.Cheer": function() {
// Do not cheer if there is no cheering time and we are not idle yet.
if (!this.cheeringTime || !this.isIdle)
return REJECT_ORDER;
this.SetNextState("CHEERING");
return ACCEPT_ORDER;
},
"enter": function() {
// Switch back to idle animation to guarantee we won't
// get stuck with an incorrect animation
this.SelectAnimation("idle");
// Idle is the default state. If units try, from the IDLE.enter sub-state, to
// begin another order, and that order fails (calling FinishOrder), they might
// end up in an infinite loop. To avoid this, all methods that could put the unit in
// a new state are done on the next turn.
// This wastes a turn but avoids infinite loops.
// Further, the GUI and AI want to know when a unit is idle,
// but sending this info in Idle.enter will send spurious messages.
// Pick 100 to execute on the next turn in SP and MP.
this.StartTimer(100);
return false;
},
"leave": function() {
let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
if (this.losRangeQuery)
cmpRangeManager.DisableActiveQuery(this.losRangeQuery);
if (this.losHealRangeQuery)
cmpRangeManager.DisableActiveQuery(this.losHealRangeQuery);
if (this.losAttackRangeQuery)
cmpRangeManager.DisableActiveQuery(this.losAttackRangeQuery);
this.StopTimer();
if (this.isIdle)
{
this.isIdle = false;
Engine.PostMessage(this.entity, MT_UnitIdleChanged, { "idle": this.isIdle });
}
},
"Attacked": function(msg) {
if (this.isIdle && (this.GetStance().targetAttackersAlways || !this.order || !this.order.data || !this.order.data.force))
this.RespondToTargetedEntities([msg.data.attacker]);
},
// On the range updates:
// We check for idleness to prevent an entity to react only to newly seen entities
// when receiving a Los*RangeUpdate on the same turn as the entity becomes idle
// since this.FindNew*Targets is called in the timer.
"LosRangeUpdate": function(msg) {
if (this.isIdle && msg && msg.data && msg.data.added && msg.data.added.length)
this.RespondToSightedEntities(msg.data.added);
},
"LosHealRangeUpdate": function(msg) {
if (this.isIdle && msg && msg.data && msg.data.added && msg.data.added.length)
this.RespondToHealableEntities(msg.data.added);
},
"LosAttackRangeUpdate": function(msg) {
if (this.isIdle && msg && msg.data && msg.data.added && msg.data.added.length && this.GetStance().targetVisibleEnemies)
this.AttackEntitiesByPreference(msg.data.added);
},
"Timer": function(msg) {
if (this.isGuardOf)
{
this.Guard(this.isGuardOf, false);
return;
}
// If a unit can heal and attack we first want to heal wounded units,
// so check if we are a healer and find whether there's anybody nearby to heal.
// (If anyone approaches later it'll be handled via LosHealRangeUpdate.)
// If anyone in sight gets hurt that will be handled via LosHealRangeUpdate.
if (this.IsHealer() && this.FindNewHealTargets())
return;
// If we entered the idle state we must have nothing better to do,
// so immediately check whether there's anybody nearby to attack.
// (If anyone approaches later, it'll be handled via LosAttackRangeUpdate.)
if (this.FindNewTargets())
return;
if (this.FindSightedEnemies())
return;
if (!this.isIdle)
{
// Move back to the held position if we drifted away.
// (only if not a formation member).
if (!this.IsFormationMember() &&
this.GetStance().respondHoldGround && this.heldPosition &&
!this.CheckPointRangeExplicit(this.heldPosition.x, this.heldPosition.z, 0, 10) &&
this.WalkToHeldPosition())
return;
if (this.IsFormationMember())
{
let cmpFormationAI = Engine.QueryInterface(this.formationController, IID_UnitAI);
if (!cmpFormationAI || !cmpFormationAI.IsIdle())
return;
}
this.isIdle = true;
Engine.PostMessage(this.entity, MT_UnitIdleChanged, { "idle": this.isIdle });
}
// Go linger first to prevent all roaming entities
// to move all at the same time on map init.
if (this.template.RoamDistance)
this.SetNextState("LINGERING");
},
"ROAMING": {
"enter": function() {
this.SetFacePointAfterMove(false);
this.MoveRandomly(+this.template.RoamDistance);
this.StartTimer(randIntInclusive(+this.template.RoamTimeMin, +this.template.RoamTimeMax));
return false;
},
"leave": function() {
this.StopMoving();
this.StopTimer();
this.SetFacePointAfterMove(true);
},
"Timer": function(msg) {
this.SetNextState("LINGERING");
},
"MovementUpdate": function() {
this.MoveRandomly(+this.template.RoamDistance);
},
},
"LINGERING": {
"enter": function() {
// ToDo: rename animations?
this.SelectAnimation("feeding");
this.StartTimer(randIntInclusive(+this.template.FeedTimeMin, +this.template.FeedTimeMax));
return false;
},
"leave": function() {
this.ResetAnimation();
this.StopTimer();
},
"Timer": function(msg) {
this.SetNextState("ROAMING");
},
},
},
"WALKING": {
"enter": function() {
if (!this.MoveTo(this.order.data))
{
this.FinishOrder();
return true;
}
return false;
},
"leave": function() {
this.StopMoving();
},
"MovementUpdate": function(msg) {
// If it looks like the path is failing, and we are close enough stop anyways.
// This avoids pathing for an unreachable goal and reduces lag considerably.
if (msg.likelyFailure || msg.obstructed && this.RelaxedMaxRangeCheck(this.order.data, this.DefaultRelaxedMaxRange) ||
this.CheckRange(this.order.data))
this.FinishOrder();
},
},
"WALKINGANDFIGHTING": {
"enter": function() {
if (!this.MoveTo(this.order.data))
{
this.FinishOrder();
return true;
}
// Show weapons rather than carried resources.
this.SetAnimationVariant("combat");
this.StartTimer(0, 1000);
return false;
},
"Timer": function(msg) {
this.FindWalkAndFightTargets();
},
"leave": function(msg) {
this.StopMoving();
this.StopTimer();
this.SetDefaultAnimationVariant();
},
"MovementUpdate": function(msg) {
// If it looks like the path is failing, and we are close enough stop anyways.
// This avoids pathing for an unreachable goal and reduces lag considerably.
if (msg.likelyFailure || msg.obstructed && this.RelaxedMaxRangeCheck(this.order.data, this.DefaultRelaxedMaxRange) ||
this.CheckRange(this.order.data))
this.FinishOrder();
},
},
"PATROL": {
"enter": function() {
let cmpPosition = Engine.QueryInterface(this.entity, IID_Position);
if (!cmpPosition || !cmpPosition.IsInWorld())
{
this.FinishOrder();
return true;
}
// Memorize the origin position in case that we want to go back.
if (!this.patrolStartPosOrder)
{
this.patrolStartPosOrder = cmpPosition.GetPosition();
this.patrolStartPosOrder.targetClasses = this.order.data.targetClasses;
this.patrolStartPosOrder.allowCapture = this.order.data.allowCapture;
}
this.SetAnimationVariant("combat");
return false;
},
"leave": function() {
delete this.patrolStartPosOrder;
this.SetDefaultAnimationVariant();
},
"PATROLLING": {
"enter": function() {
let cmpPosition = Engine.QueryInterface(this.entity, IID_Position);
if (!cmpPosition || !cmpPosition.IsInWorld() ||
!this.MoveTo(this.order.data))
{
this.FinishOrder();
return true;
}
this.StartTimer(0, 1000);
return false;
},
"leave": function() {
this.StopMoving();
this.StopTimer();
},
"Timer": function(msg) {
this.FindWalkAndFightTargets();
},
"MovementUpdate": function(msg) {
if (!msg.likelyFailure && !msg.likelySuccess && !this.RelaxedMaxRangeCheck(this.order.data, this.DefaultRelaxedMaxRange))
return;
if (this.orderQueue.length == 1)
this.PushOrder("Patrol", this.patrolStartPosOrder);
this.PushOrder(this.order.type, this.order.data);
this.SetNextState("CHECKINGWAYPOINT");
},
},
"CHECKINGWAYPOINT": {
"enter": function() {
this.StartTimer(0, 1000);
this.stopSurveying = 0;
// TODO: pick a proper animation
return false;
},
"leave": function() {
this.StopTimer();
delete this.stopSurveying;
},
"Timer": function(msg) {
if (this.stopSurveying >= +this.template.PatrolWaitTime)
{
this.FinishOrder();
return;
}
this.FindWalkAndFightTargets();
++this.stopSurveying;
}
}
},
"GUARD": {
"RemoveGuard": function() {
this.FinishOrder();
},
"ESCORTING": {
"enter": function() {
if (!this.MoveToTargetRangeExplicit(this.isGuardOf, 0, this.guardRange))
{
this.FinishOrder();
return true;
}
// Show weapons rather than carried resources.
this.SetAnimationVariant("combat");
this.StartTimer(0, 1000);
this.SetHeldPositionOnEntity(this.isGuardOf);
return false;
},
"Timer": function(msg) {
if (!this.ShouldGuard(this.isGuardOf))
{
this.FinishOrder();
return;
}
let cmpObstructionManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ObstructionManager);
if (cmpObstructionManager.IsInTargetRange(this.entity, this.isGuardOf, 0, 3 * this.guardRange, false))
this.TryMatchTargetSpeed(this.isGuardOf, false);
this.SetHeldPositionOnEntity(this.isGuardOf);
},
"leave": function(msg) {
this.StopMoving();
this.ResetSpeedMultiplier();
this.StopTimer();
this.SetDefaultAnimationVariant();
},
"MovementUpdate": function(msg) {
if (msg.likelyFailure || this.CheckTargetRangeExplicit(this.isGuardOf, 0, this.guardRange))
this.SetNextState("GUARDING");
},
},
"GUARDING": {
"enter": function() {
this.StartTimer(1000, 1000);
this.SetHeldPositionOnEntity(this.entity);
this.SetAnimationVariant("combat");
this.FaceTowardsTarget(this.order.data.target);
return false;
},
"LosAttackRangeUpdate": function(msg) {
if (this.GetStance().targetVisibleEnemies)
this.AttackEntitiesByPreference(msg.data.added);
},
"Timer": function(msg) {
if (!this.ShouldGuard(this.isGuardOf))
{
this.FinishOrder();
return;
}
// TODO: find out what to do if we cannot move.
if (!this.CheckTargetRangeExplicit(this.isGuardOf, 0, this.guardRange) &&
this.MoveToTargetRangeExplicit(this.isGuardOf, 0, this.guardRange))
this.SetNextState("ESCORTING");
else
{
this.FaceTowardsTarget(this.order.data.target);
var cmpHealth = Engine.QueryInterface(this.isGuardOf, IID_Health);
if (cmpHealth && cmpHealth.IsInjured())
{
if (this.CanHeal(this.isGuardOf))
this.PushOrderFront("Heal", { "target": this.isGuardOf, "force": false });
else if (this.CanRepair(this.isGuardOf))
this.PushOrderFront("Repair", { "target": this.isGuardOf, "autocontinue": false, "force": false });
}
}
},
"leave": function(msg) {
this.StopTimer();
this.SetDefaultAnimationVariant();
},
},
},
"FLEEING": {
"enter": function() {
// We use the distance between the entities to account for ranged attacks
this.order.data.distanceToFlee = PositionHelper.DistanceBetweenEntities(this.entity, this.order.data.target) + (+this.template.FleeDistance);
let cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion);
// Use unit motion directly to ignore the visibility check. TODO: change this if we add LOS to fauna.
if (this.CheckTargetRangeExplicit(this.order.data.target, this.order.data.distanceToFlee, -1) ||
!cmpUnitMotion || !cmpUnitMotion.MoveToTargetRange(this.order.data.target, this.order.data.distanceToFlee, -1))
{
this.FinishOrder();
return true;
}
this.PlaySound("panic");
this.SetSpeedMultiplier(this.GetRunMultiplier());
return false;
},
"OrderTargetRenamed": function(msg) {
// To avoid replaying the panic sound, handle this explicitly.
let cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion);
if (this.CheckTargetRangeExplicit(this.order.data.target, this.order.data.distanceToFlee, -1) ||
!cmpUnitMotion || !cmpUnitMotion.MoveToTargetRange(this.order.data.target, this.order.data.distanceToFlee, -1))
this.FinishOrder();
},
"Attacked": function(msg) {
if (msg.data.attacker == this.order.data.target)
return;
let cmpObstructionManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ObstructionManager);
if (cmpObstructionManager.DistanceToTarget(this.entity, msg.data.target) > cmpObstructionManager.DistanceToTarget(this.entity, this.order.data.target))
return;
if (this.GetStance().targetAttackersAlways || !this.order || !this.order.data || !this.order.data.force)
this.RespondToTargetedEntities([msg.data.attacker]);
},
"leave": function() {
this.ResetSpeedMultiplier();
this.StopMoving();
},
"MovementUpdate": function(msg) {
if (msg.likelyFailure || this.CheckTargetRangeExplicit(this.order.data.target, this.order.data.distanceToFlee, -1))
this.FinishOrder();
},
},
"COMBAT": {
"Order.LeaveFoundation": function(msg) {
// Ignore the order as we're busy.
return REJECT_ORDER;
},
"Attacked": function(msg) {
// If we're already in combat mode, ignore anyone else who's attacking us
// unless it's a melee attack since they may be blocking our way to the target
if (msg.data.type == "Melee" && (this.GetStance().targetAttackersAlways || !this.order.data.force))
this.RespondToTargetedEntities([msg.data.attacker]);
},
"leave": function() {
if (!this.formationAnimationVariant)
this.SetDefaultAnimationVariant();
},
"APPROACHING": {
"enter": function() {
if (!this.MoveToTargetAttackRange(this.order.data.target, this.order.data.attackType))
{
this.FinishOrder();
return true;
}
if (!this.formationAnimationVariant)
this.SetAnimationVariant("combat");
this.StartTimer(1000, 1000);
return false;
},
"leave": function() {
this.StopMoving();
this.StopTimer();
},
"Timer": function(msg) {
if (this.ShouldAbandonChase(this.order.data.target, this.order.data.force, IID_Attack, this.order.data.attackType))
{
this.FinishOrder();
if (this.GetStance().respondHoldGround)
this.WalkToHeldPosition();
}
else
{
this.RememberTargetPosition();
if (this.order.data.hunting && this.orderQueue.length > 1 &&
this.orderQueue[1].type === "Gather")
this.RememberTargetPosition(this.orderQueue[1].data);
}
},
"MovementUpdate": function(msg) {
if (msg.likelyFailure)
{
// This also handles hunting.
if (this.orderQueue.length > 1)
{
this.FinishOrder();
return;
}
else if (!this.order.data.force || !this.order.data.lastPos)
{
this.SetNextState("COMBAT.FINDINGNEWTARGET");
return;
}
// If the order was forced, try moving to the target position,
// under the assumption that this is desirable if the target
// was somewhat far away - we'll likely end up closer to where
// the player hoped we would.
let lastPos = this.order.data.lastPos;
this.PushOrder("WalkAndFight", {
"x": lastPos.x, "z": lastPos.z,
"force": false,
// Force to true - otherwise structures might be attacked instead of captured,
// which is generally not expected (attacking units usually has allowCapture false).
"allowCapture": true
});
return;
}
if (this.CheckTargetAttackRange(this.order.data.target, this.order.data.attackType))
{
if (this.CanUnpack())
{
this.PushOrderFront("Unpack", { "force": true });
return;
}
this.SetNextState("ATTACKING");
}
else if (msg.likelySuccess)
// Try moving again,
// attack range uses a height-related formula and our actual max range might have changed.
if (!this.MoveToTargetAttackRange(this.order.data.target, this.order.data.attackType))
this.FinishOrder();
},
},
"ATTACKING": {
"enter": function() {
let target = this.order.data.target;
let cmpFormation = Engine.QueryInterface(target, IID_Formation);
if (cmpFormation)
{
this.order.data.formationTarget = target;
target = cmpFormation.GetClosestMember(this.entity);
this.order.data.target = target;
}
this.shouldCheer = false;
if (!this.CanAttack(target))
{
this.SetNextState("COMBAT.FINDINGNEWTARGET");
return true;
}
if (!this.CheckTargetAttackRange(target, this.order.data.attackType))
{
if (this.CanPack())
{
this.PushOrderFront("Pack", { "force": true });
return true;
}
this.SetNextState("COMBAT.APPROACHING");
return true;
}
let cmpAttack = Engine.QueryInterface(this.entity, IID_Attack);
this.attackTimers = cmpAttack.GetTimers(this.order.data.attackType);
// If the repeat time since the last attack hasn't elapsed,
// delay this attack to avoid attacking too fast.
let prepare = this.attackTimers.prepare;
if (this.lastAttacked)
{
let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
let repeatLeft = this.lastAttacked + this.attackTimers.repeat - cmpTimer.GetTime();
prepare = Math.max(prepare, repeatLeft);
}
if (!this.formationAnimationVariant)
this.SetAnimationVariant("combat");
this.oldAttackType = this.order.data.attackType;
this.SelectAnimation("attack_" + this.order.data.attackType.toLowerCase());
this.SetAnimationSync(prepare, this.attackTimers.repeat);
this.StartTimer(prepare, this.attackTimers.repeat);
// TODO: we should probably only bother syncing projectile attacks, not melee
// If using a non-default prepare time, re-sync the animation when the timer runs.
this.resyncAnimation = prepare != this.attackTimers.prepare;
this.FaceTowardsTarget(this.order.data.target);
let cmpBuildingAI = Engine.QueryInterface(this.entity, IID_BuildingAI);
if (cmpBuildingAI)
{
cmpBuildingAI.SetUnitAITarget(this.order.data.target);
return false;
}
let cmpUnitAI = Engine.QueryInterface(this.order.data.target, IID_UnitAI);
// Units with no cheering time do not cheer.
this.shouldCheer = cmpUnitAI && (!cmpUnitAI.IsAnimal() || cmpUnitAI.IsDangerousAnimal()) && this.cheeringTime > 0;
return false;
},
"leave": function() {
let cmpBuildingAI = Engine.QueryInterface(this.entity, IID_BuildingAI);
if (cmpBuildingAI)
cmpBuildingAI.SetUnitAITarget(0);
this.StopTimer();
this.ResetAnimation();
},
"Timer": function(msg) {
let target = this.order.data.target;
let attackType = this.order.data.attackType;
if (!this.CanAttack(target))
{
this.SetNextState("COMBAT.FINDINGNEWTARGET");
return;
}
this.RememberTargetPosition();
if (this.order.data.hunting && this.orderQueue.length > 1 && this.orderQueue[1].type === "Gather")
this.RememberTargetPosition(this.orderQueue[1].data);
let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
this.lastAttacked = cmpTimer.GetTime() - msg.lateness;
this.FaceTowardsTarget(target);
// BuildingAI has it's own attack-routine
let cmpBuildingAI = Engine.QueryInterface(this.entity, IID_BuildingAI);
if (!cmpBuildingAI)
{
let cmpAttack = Engine.QueryInterface(this.entity, IID_Attack);
cmpAttack.PerformAttack(attackType, target);
}
// PerformAttack might have triggered messages that moved us to another state.
// (use 'ends with' to handle formation members copying our state).
if (!this.GetCurrentState().endsWith("COMBAT.ATTACKING"))
return;
// Check we can still reach the target for the next attack
if (this.CheckTargetAttackRange(target, attackType))
{
if (this.resyncAnimation)
{
this.SetAnimationSync(this.attackTimers.repeat, this.attackTimers.repeat);
this.resyncAnimation = false;
}
return;
}
if (this.ShouldChaseTargetedEntity(target, this.order.data.force))
{
if (this.CanPack())
{
this.PushOrderFront("Pack", { "force": true });
return;
}
this.SetNextState("COMBAT.CHASING");
return;
}
this.SetNextState("FINDINGNEWTARGET");
},
// TODO: respond to target deaths immediately, rather than waiting
// until the next Timer event
"Attacked": function(msg) {
if (this.order.data.attackType == "Capture" && (this.GetStance().targetAttackersAlways || !this.order.data.force)
&& this.order.data.target != msg.data.attacker && this.GetBestAttackAgainst(msg.data.attacker, true) != "Capture")
this.RespondToTargetedEntities([msg.data.attacker]);
},
},
"FINDINGNEWTARGET": {
"Order.Cheer": function() {
if (!this.cheeringTime)
return REJECT_ORDER;
this.SetNextState("CHEERING");
return ACCEPT_ORDER;
},
"enter": function() {
// Try to find the formation the target was a part of.
let cmpFormation = Engine.QueryInterface(this.order.data.target, IID_Formation);
if (!cmpFormation)
cmpFormation = Engine.QueryInterface(this.order.data.formationTarget || INVALID_ENTITY, IID_Formation);
// If the target is a formation, pick closest member.
if (cmpFormation)
{
let filter = (t) => this.CanAttack(t);
this.order.data.formationTarget = this.order.data.target;
let target = cmpFormation.GetClosestMember(this.entity, filter);
this.order.data.target = target;
this.SetNextState("COMBAT.ATTACKING");
return true;
}
// Can't reach it, no longer owned by enemy, or it doesn't exist any more - give up
// except if in WalkAndFight mode where we look for more enemies around before moving again.
if (this.FinishOrder())
{
if (this.IsWalkingAndFighting())
this.FindWalkAndFightTargets();
return true;
}
if (this.FindNewTargets())
return true;
if (this.GetStance().respondHoldGround)
this.WalkToHeldPosition();
if (this.shouldCheer)
{
this.Cheer();
this.CallPlayerOwnedEntitiesFunctionInRange("Cheer", [], this.notifyToCheerInRange);
}
return true;
},
},
"CHASING": {
"Order.MoveToChasingPoint": function(msg) {
if (this.CheckPointRangeExplicit(msg.data.x, msg.data.z, 0, msg.data.max))
return REJECT_ORDER;
this.order.data.relaxed = true;
this.StopTimer();
this.SetNextState("MOVINGTOPOINT");
return ACCEPT_ORDER;
},
"enter": function() {
if (!this.MoveToTargetAttackRange(this.order.data.target, this.order.data.attackType))
{
this.FinishOrder();
return true;
}
if (!this.formationAnimationVariant)
this.SetAnimationVariant("combat");
var cmpUnitAI = Engine.QueryInterface(this.order.data.target, IID_UnitAI);
if (cmpUnitAI && cmpUnitAI.IsFleeing())
this.SetSpeedMultiplier(this.GetRunMultiplier());
this.StartTimer(1000, 1000);
return false;
},
"leave": function() {
this.ResetSpeedMultiplier();
this.StopMoving();
this.StopTimer();
},
"Timer": function(msg) {
if (this.ShouldAbandonChase(this.order.data.target, this.order.data.force, IID_Attack, this.order.data.attackType))
{
this.FinishOrder();
if (this.GetStance().respondHoldGround)
this.WalkToHeldPosition();
}
else
{
this.RememberTargetPosition();
if (this.order.data.hunting && this.orderQueue.length > 1 &&
this.orderQueue[1].type === "Gather")
this.RememberTargetPosition(this.orderQueue[1].data);
}
},
"MovementUpdate": function(msg) {
if (msg.likelyFailure)
{
// This also handles hunting.
if (this.orderQueue.length > 1)
{
this.FinishOrder();
return;
}
else if (!this.order.data.force)
{
this.SetNextState("COMBAT.FINDINGNEWTARGET");
return;
}
else if (this.order.data.lastPos)
{
let lastPos = this.order.data.lastPos;
let cmpAttack = Engine.QueryInterface(this.entity, IID_Attack);
this.PushOrder("MoveToChasingPoint", {
"x": lastPos.x,
"z": lastPos.z,
"max": cmpAttack.GetRange(this.order.data.attackType).max,
"force": true
});
return;
}
}
if (this.CheckTargetAttackRange(this.order.data.target, this.order.data.attackType))
{
if (this.CanUnpack())
{
this.PushOrderFront("Unpack", { "force": true });
return;
}
this.SetNextState("ATTACKING");
}
else if (msg.likelySuccess)
// Try moving again,
// attack range uses a height-related formula and our actual max range might have changed.
if (!this.MoveToTargetAttackRange(this.order.data.target, this.order.data.attackType))
this.FinishOrder();
},
"MOVINGTOPOINT": {
"enter": function() {
if (!this.MoveTo(this.order.data))
{
this.FinishOrder();
return true;
}
return false;
},
"leave": function() {
this.StopMoving();
},
"MovementUpdate": function(msg) {
// If it looks like the path is failing, and we are close enough from wanted range
// stop anyways. This avoids pathing for an unreachable goal and reduces lag considerably.
if (msg.likelyFailure ||
msg.obstructed && this.RelaxedMaxRangeCheck(this.order.data, this.order.data.max + this.DefaultRelaxedMaxRange) ||
!msg.obstructed && this.CheckRange(this.order.data))
this.FinishOrder();
},
},
},
},
"GATHER": {
"leave": function() {
// Show the carried resource, if we've gathered anything.
this.SetDefaultAnimationVariant();
},
"APPROACHING": {
"enter": function() {
this.gatheringTarget = this.order.data.target; // temporary, deleted in "leave".
let cmpSupply = Engine.QueryInterface(this.gatheringTarget, IID_ResourceSupply);
let cmpMirage = Engine.QueryInterface(this.gatheringTarget, IID_Mirage);
if ((!cmpMirage || !cmpMirage.Mirages(IID_ResourceSupply)) &&
(!cmpSupply || !cmpSupply.AddGatherer(this.entity)) ||
!this.MoveTo(this.order.data, IID_ResourceGatherer))
{
// If the target's last known position is in FOW, try going there
// and hope that we might find it then.
let lastPos = this.order.data.lastPos;
if (this.gatheringTarget != INVALID_ENTITY &&
lastPos && !this.CheckPositionVisible(lastPos.x, lastPos.z))
{
this.PushOrderFront("Walk", {
"x": lastPos.x, "z": lastPos.z,
"force": this.order.data.force
});
return true;
}
this.SetNextState("FINDINGNEWTARGET");
return true;
}
this.SetAnimationVariant("approach_" + this.order.data.type.specific);
let cmpResourceGatherer = Engine.QueryInterface(this.entity, IID_ResourceGatherer);
if (cmpResourceGatherer)
cmpResourceGatherer.AddToPlayerCounter(this.order.data.type.generic);
return false;
},
"MovementUpdate": function(msg) {
// The GATHERING timer will handle finding a valid resource.
if (msg.likelyFailure)
this.SetNextState("FINDINGNEWTARGET");
else if (this.CheckRange(this.order.data, IID_ResourceGatherer))
this.SetNextState("GATHERING");
},
"leave": function() {
this.StopMoving();
if (!this.gatheringTarget)
return;
// don't use ownership because this is called after a conversion/resignation
// and the ownership would be invalid then.
let cmpSupply = Engine.QueryInterface(this.gatheringTarget, IID_ResourceSupply);
if (cmpSupply)
cmpSupply.RemoveGatherer(this.entity);
let cmpResourceGatherer = Engine.QueryInterface(this.entity, IID_ResourceGatherer);
if (cmpResourceGatherer)
cmpResourceGatherer.RemoveFromPlayerCounter();
delete this.gatheringTarget;
},
},
// Walking to a good place to gather resources near, used by GatherNearPosition
"WALKING": {
"enter": function() {
if (!this.MoveTo(this.order.data))
{
this.FinishOrder();
return true;
}
this.SetAnimationVariant("approach_" + this.order.data.type.specific);
return false;
},
"leave": function() {
this.StopMoving();
},
"MovementUpdate": function(msg) {
// If we failed, the GATHERING timer will handle finding a valid resource.
if (msg.likelyFailure || msg.obstructed && this.RelaxedMaxRangeCheck(this.order.data, this.DefaultRelaxedMaxRange) ||
this.CheckRange(this.order.data))
this.SetNextState("GATHERING");
},
},
"GATHERING": {
"enter": function() {
this.gatheringTarget = this.order.data.target || INVALID_ENTITY; // deleted in "leave".
// Check if the resource is full.
// Will only be added if we're not already in.
let cmpSupply = Engine.QueryInterface(this.gatheringTarget, IID_ResourceSupply);
- if (!cmpSupply || !cmpSupply.AddGatherer(this.entity))
+ if (!cmpSupply || !cmpSupply.AddActiveGatherer(this.entity))
{
this.SetNextState("FINDINGNEWTARGET");
return true;
}
// If this order was forced, the player probably gave it, but now we've reached the target
// switch to an unforced order (can be interrupted by attacks)
this.order.data.force = false;
this.order.data.autoharvest = true;
// Calculate timing based on gather rates
// This allows the gather rate to control how often we gather, instead of how much.
let cmpResourceGatherer = Engine.QueryInterface(this.entity, IID_ResourceGatherer);
let rate = cmpResourceGatherer.GetTargetGatherRate(this.gatheringTarget);
if (!rate)
{
// Try to find another target if the current one stopped existing
if (!Engine.QueryInterface(this.gatheringTarget, IID_Identity))
{
this.SetNextState("FINDINGNEWTARGET");
return true;
}
// No rate, give up on gathering
this.FinishOrder();
return true;
}
// Scale timing interval based on rate, and start timer
// The offset should be at least as long as the repeat time so we use the same value for both.
let offset = 1000 / rate;
this.StartTimer(offset, offset);
// We want to start the gather animation as soon as possible,
// but only if we're actually at the target and it's still alive
// (else it'll look like we're chopping empty air).
// (If it's not alive, the Timer handler will deal with sending us
// off to a different target.)
if (this.CheckTargetRange(this.gatheringTarget, IID_ResourceGatherer))
{
this.SetDefaultAnimationVariant();
this.FaceTowardsTarget(this.order.data.target);
this.SelectAnimation("gather_" + this.order.data.type.specific);
cmpResourceGatherer.AddToPlayerCounter(this.order.data.type.generic);
}
return false;
},
"leave": function() {
this.StopTimer();
// Don't use ownership because this is called after a conversion/resignation
// and the ownership would be invalid then.
let cmpSupply = Engine.QueryInterface(this.gatheringTarget, IID_ResourceSupply);
if (cmpSupply)
cmpSupply.RemoveGatherer(this.entity);
let cmpResourceGatherer = Engine.QueryInterface(this.entity, IID_ResourceGatherer);
if (cmpResourceGatherer)
cmpResourceGatherer.RemoveFromPlayerCounter();
delete this.gatheringTarget;
this.ResetAnimation();
},
"Timer": function(msg) {
let resourceTemplate = this.order.data.template;
let resourceType = this.order.data.type;
// TODO: we are leaking information here - if the target died in FOW, we'll know it's dead
// straight away.
// Seems one would have to listen to ownership changed messages to make it work correctly
// but that's likely prohibitively expansive performance wise.
let cmpSupply = Engine.QueryInterface(this.gatheringTarget, IID_ResourceSupply);
// If we can't gather from the target, find a new one.
if (!cmpSupply || !cmpSupply.IsAvailableTo(this.entity) ||
!this.CanGather(this.gatheringTarget))
{
this.SetNextState("FINDINGNEWTARGET");
return;
}
if (!this.CheckTargetRange(this.gatheringTarget, IID_ResourceGatherer))
{
// Try to follow the target
if (this.MoveToTargetRange(this.gatheringTarget, IID_ResourceGatherer))
this.SetNextState("APPROACHING");
// Our target is no longer visible - go to its last known position first
// and then hopefully it will become visible.
else if (!this.CheckTargetVisible(this.gatheringTarget) && this.order.data.lastPos)
this.PushOrderFront("Walk", {
"x": this.order.data.lastPos.x,
"z": this.order.data.lastPos.z,
"force": this.order.data.force
});
else
this.SetNextState("FINDINGNEWTARGET");
return;
}
// Gather the resources:
let cmpResourceGatherer = Engine.QueryInterface(this.entity, IID_ResourceGatherer);
// Try to gather treasure
if (cmpResourceGatherer.TryInstantGather(this.gatheringTarget))
return;
// If we've already got some resources but they're the wrong type,
// drop them first to ensure we're only ever carrying one type
if (cmpResourceGatherer.IsCarryingAnythingExcept(resourceType.generic))
cmpResourceGatherer.DropResources();
this.FaceTowardsTarget(this.order.data.target);
// Collect from the target
let status = cmpResourceGatherer.PerformGather(this.gatheringTarget);
// If we've collected as many resources as possible,
// return to the nearest dropsite
if (status.filled)
{
let nearestDropsite = this.FindNearestDropsite(resourceType.generic);
if (nearestDropsite)
{
// (Keep this Gather order on the stack so we'll
// continue gathering after returning)
// However mark our target as invalid if it's exhausted, so we don't waste time
// trying to gather from it.
if (status.exhausted)
this.order.data.target = INVALID_ENTITY;
this.PushOrderFront("ReturnResource", { "target": nearestDropsite, "force": false });
return;
}
// Oh no, couldn't find any drop sites. Give up on gathering.
this.FinishOrder();
return;
}
if (status.exhausted)
this.SetNextState("FINDINGNEWTARGET");
},
},
"FINDINGNEWTARGET": {
"enter": function() {
let previousTarget = this.order.data.target;
let resourceTemplate = this.order.data.template;
let resourceType = this.order.data.type;
// Give up on this order and try our next queued order
// but first check what is our next order and, if needed, insert a returnResource order
let cmpResourceGatherer = Engine.QueryInterface(this.entity, IID_ResourceGatherer);
if (cmpResourceGatherer.IsCarrying(resourceType.generic) &&
this.orderQueue.length > 1 && this.orderQueue[1] !== "ReturnResource" &&
(this.orderQueue[1].type !== "Gather" || this.orderQueue[1].data.type.generic !== resourceType.generic))
{
let nearestDropsite = this.FindNearestDropsite(resourceType.generic);
if (nearestDropsite)
this.orderQueue.splice(1, 0, { "type": "ReturnResource", "data": { "target": nearestDropsite, "force": false } });
}
// Must go before FinishOrder or this.order will be undefined.
let initPos = this.order.data.initPos;
if (this.FinishOrder())
return true;
// No remaining orders - pick a useful default behaviour
// Give up if we're not in the world right now.
let cmpPosition = Engine.QueryInterface(this.entity, IID_Position);
if (!cmpPosition || !cmpPosition.IsInWorld())
return true;
// If we have no known initial position of our target, look around our own position
// as a fallback.
if (!initPos)
{
let pos = cmpPosition.GetPosition();
initPos = { 'x': pos.X, 'z': pos.Z };
}
// Try to find a new resource of the same specific type near the initial resource position:
// Also don't switch to a different type of huntable animal
let nearbyResource = this.FindNearbyResource(new Vector2D(initPos.x, initPos.z),
(ent, type, template) => {
if (previousTarget == ent)
return false;
if (type.generic == "treasure" && resourceType.generic == "treasure")
return true;
return type.specific == resourceType.specific &&
(type.specific != "meat" || resourceTemplate == template);
});
if (nearbyResource)
{
this.PerformGather(nearbyResource, false, false);
return true;
}
// Failing that, try to move there and se if we are more lucky: maybe there are resources in FOW.
// Only move if we are some distance away (TODO: pick the distance better?)
if (!this.CheckPointRangeExplicit(initPos.x, initPos.z, 0, 10))
{
this.GatherNearPosition(initPos.x, initPos.z, resourceType, resourceTemplate);
return true;
}
// Nothing else to gather - if we're carrying anything then we should
// drop it off, and if not then we might as well head to the dropsite
// anyway because that's a nice enough place to congregate and idle
let nearestDropsite = this.FindNearestDropsite(resourceType.generic);
if (nearestDropsite)
{
this.PushOrderFront("ReturnResource", { "target": nearestDropsite, "force": false });
return true;
}
// No dropsites - just give up.
return true;
},
},
},
"HEAL": {
"Attacked": function(msg) {
if (!this.GetStance().respondStandGround && !this.order.data.force)
this.Flee(msg.data.attacker, false);
},
"APPROACHING": {
"enter": function() {
if (this.CheckRange(this.order.data, IID_Heal))
{
this.SetNextState("HEALING");
return true;
}
if (!this.MoveTo(this.order.data, IID_Heal))
{
this.SetNextState("FINDINGNEWTARGET");
return true;
}
this.StartTimer(1000, 1000);
return false;
},
"leave": function() {
this.StopMoving();
this.StopTimer();
},
"Timer": function(msg) {
if (this.ShouldAbandonChase(this.order.data.target, this.order.data.force, IID_Heal, null))
this.SetNextState("FINDINGNEWTARGET");
},
"MovementUpdate": function(msg) {
if (msg.likelyFailure || this.CheckRange(this.order.data, IID_Heal))
this.SetNextState("HEALING");
},
},
"HEALING": {
"enter": function() {
if (!this.CheckRange(this.order.data, IID_Heal))
{
this.SetNextState("APPROACHING");
return true;
}
if (!this.TargetIsAlive(this.order.data.target) ||
!this.CanHeal(this.order.data.target))
{
this.SetNextState("FINDINGNEWTARGET");
return true;
}
let cmpHeal = Engine.QueryInterface(this.entity, IID_Heal);
this.healTimers = cmpHeal.GetTimers();
// If the repeat time since the last heal hasn't elapsed,
// delay the action to avoid healing too fast.
var prepare = this.healTimers.prepare;
if (this.lastHealed)
{
var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
var repeatLeft = this.lastHealed + this.healTimers.repeat - cmpTimer.GetTime();
prepare = Math.max(prepare, repeatLeft);
}
this.SelectAnimation("heal");
this.SetAnimationSync(prepare, this.healTimers.repeat);
this.StartTimer(prepare, this.healTimers.repeat);
// If using a non-default prepare time, re-sync the animation when the timer runs.
this.resyncAnimation = prepare != this.healTimers.prepare;
this.FaceTowardsTarget(this.order.data.target);
return false;
},
"leave": function() {
this.ResetAnimation();
this.StopTimer();
},
"Timer": function(msg) {
let target = this.order.data.target;
if (!this.TargetIsAlive(target) || !this.CanHeal(target))
{
this.SetNextState("FINDINGNEWTARGET");
return;
}
if (!this.CheckRange(this.order.data, IID_Heal))
{
if (this.ShouldChaseTargetedEntity(target, this.order.data.force))
{
if (this.CanPack())
{
this.PushOrderFront("Pack", { "force": true });
return;
}
this.SetNextState("HEAL.APPROACHING");
}
else
this.SetNextState("FINDINGNEWTARGET");
return;
}
let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
this.lastHealed = cmpTimer.GetTime() - msg.lateness;
this.FaceTowardsTarget(target);
let cmpHeal = Engine.QueryInterface(this.entity, IID_Heal);
cmpHeal.PerformHeal(target);
if (this.resyncAnimation)
{
this.SetAnimationSync(this.healTimers.repeat, this.healTimers.repeat);
this.resyncAnimation = false;
}
},
},
"FINDINGNEWTARGET": {
"enter": function() {
// If we have another order, do that instead.
if (this.FinishOrder())
return true;
if (this.FindNewHealTargets())
return true;
if (this.GetStance().respondHoldGround)
this.WalkToHeldPosition();
// We quit this state right away.
return true;
},
},
},
// Returning to dropsite
"RETURNRESOURCE": {
"APPROACHING": {
"enter": function() {
if (!this.MoveTo(this.order.data, IID_ResourceGatherer))
{
this.FinishOrder();
return true;
}
return false;
},
"leave": function() {
this.StopMoving();
},
"MovementUpdate": function(msg) {
// Check the dropsite is in range and we can return our resource there
// (we didn't get stopped before reaching it)
let cmpResourceGatherer = Engine.QueryInterface(this.entity, IID_ResourceGatherer);
if (this.CheckTargetRange(this.order.data.target, IID_ResourceGatherer) &&
this.CanReturnResource(this.order.data.target, true, cmpResourceGatherer))
{
cmpResourceGatherer.CommitResources(this.order.data.target);
// Stop showing the carried resource animation.
this.SetDefaultAnimationVariant();
// Our next order should always be a Gather,
// so just switch back to that order.
this.FinishOrder();
return;
}
if (msg.obstructed)
return;
// If we are here: we are in range but not carrying the right resources (or resources at all),
// the dropsite was destroyed, or we couldn't reach it, or ownership changed.
// Look for a new one.
let genericType = cmpResourceGatherer.GetMainCarryingType();
let nearby = this.FindNearestDropsite(genericType);
if (nearby)
{
this.FinishOrder();
this.PushOrderFront("ReturnResource", { "target": nearby, "force": false });
return;
}
// Oh no, couldn't find any drop sites. Give up on returning.
this.FinishOrder();
},
},
},
"TRADE": {
"Attacked": function(msg) {
// Ignore attack
// TODO: Inform player
},
"APPROACHINGMARKET": {
"enter": function() {
if (!this.MoveToMarket(this.order.data.target))
{
this.FinishOrder();
return true;
}
return false;
},
"leave": function() {
this.StopMoving();
},
"MovementUpdate": function(msg) {
if (!msg.likelyFailure && !this.CheckRange(this.order.data.nextTarget, IID_Trader))
return;
if (this.waypoints && this.waypoints.length)
{
if (!this.MoveToMarket(this.order.data.target))
this.StopTrading();
}
else
this.PerformTradeAndMoveToNextMarket(this.order.data.target);
},
},
"TradingCanceled": function(msg) {
if (msg.market != this.order.data.target)
return;
let cmpTrader = Engine.QueryInterface(this.entity, IID_Trader);
let otherMarket = cmpTrader && cmpTrader.GetFirstMarket();
this.StopTrading();
if (otherMarket)
this.WalkToTarget(otherMarket);
},
},
"REPAIR": {
"APPROACHING": {
"enter": function() {
if (!this.MoveTo(this.order.data, IID_Builder))
{
this.FinishOrder();
return true;
}
return false;
},
"leave": function() {
this.StopMoving();
},
"MovementUpdate": function(msg) {
if (msg.likelyFailure || msg.likelySuccess)
this.SetNextState("REPAIRING");
},
},
"REPAIRING": {
"enter": function() {
// If this order was forced, the player probably gave it, but now we've reached the target
// switch to an unforced order (can be interrupted by attacks)
if (this.order.data.force)
this.order.data.autoharvest = true;
this.order.data.force = false;
// Needed to remove the entity from the builder list when leaving this state.
this.repairTarget = this.order.data.target;
if (!this.CanRepair(this.repairTarget))
{
this.FinishOrder();
return true;
}
if (!this.CheckTargetRange(this.repairTarget, IID_Builder))
{
this.SetNextState("APPROACHING");
return true;
}
let cmpHealth = Engine.QueryInterface(this.repairTarget, IID_Health);
if (cmpHealth && cmpHealth.GetHitpoints() >= cmpHealth.GetMaxHitpoints())
{
// The building was already finished/fully repaired before we arrived;
// let the ConstructionFinished handler handle this.
this.ConstructionFinished({ "entity": this.repairTarget, "newentity": this.repairTarget });
return true;
}
let cmpBuilderList = QueryBuilderListInterface(this.repairTarget);
if (cmpBuilderList)
cmpBuilderList.AddBuilder(this.entity);
this.FaceTowardsTarget(this.repairTarget);
this.SelectAnimation("build");
this.StartTimer(1000, 1000);
return false;
},
"leave": function() {
let cmpBuilderList = QueryBuilderListInterface(this.repairTarget);
if (cmpBuilderList)
cmpBuilderList.RemoveBuilder(this.entity);
delete this.repairTarget;
this.StopTimer();
this.ResetAnimation();
},
"Timer": function(msg) {
if (!this.CanRepair(this.repairTarget))
{
this.FinishOrder();
return;
}
this.FaceTowardsTarget(this.repairTarget);
let cmpBuilder = Engine.QueryInterface(this.entity, IID_Builder);
cmpBuilder.PerformBuilding(this.repairTarget);
// If the building is completed, the leave() function will be called
// by the ConstructionFinished message.
// In that case, the repairTarget is deleted, and we can just return.
if (!this.repairTarget)
return;
if (!this.CheckTargetRange(this.repairTarget, IID_Builder))
this.SetNextState("APPROACHING");
},
},
"ConstructionFinished": function(msg) {
if (msg.data.entity != this.order.data.target)
return; // ignore other buildings
let oldData = this.order.data;
// Save the current state so we can continue walking if necessary
// FinishOrder() below will switch to IDLE if there's no order, which sets the idle animation.
// Idle animation while moving towards finished construction looks weird (ghosty).
let oldState = this.GetCurrentState();
let cmpResourceGatherer = Engine.QueryInterface(this.entity, IID_ResourceGatherer);
let canReturnResources = this.CanReturnResource(msg.data.newentity, true, cmpResourceGatherer);
if (this.CheckTargetRange(msg.data.newentity, IID_Builder) && canReturnResources)
{
cmpResourceGatherer.CommitResources(msg.data.newentity);
this.SetDefaultAnimationVariant();
}
// Switch to the next order (if any)
if (this.FinishOrder())
{
if (canReturnResources)
{
// We aren't in range, but we can still return resources there: always do so.
this.SetDefaultAnimationVariant();
this.PushOrderFront("ReturnResource", { "target": msg.data.newentity, "force": false });
}
return;
}
if (canReturnResources)
{
// We aren't in range, but we can still return resources there: always do so.
this.SetDefaultAnimationVariant();
this.PushOrderFront("ReturnResource", { "target": msg.data.newentity, "force": false });
}
// No remaining orders - pick a useful default behaviour
// If autocontinue explicitly disabled (e.g. by AI) then
// do nothing automatically
if (!oldData.autocontinue)
return;
// If this building was e.g. a farm of ours, the entities that received
// the build command should start gathering from it
if ((oldData.force || oldData.autoharvest) && this.CanGather(msg.data.newentity))
{
this.PerformGather(msg.data.newentity, true, false);
return;
}
// If this building was e.g. a farmstead of ours, entities that received
// the build command should look for nearby resources to gather
if ((oldData.force || oldData.autoharvest) &&
this.CanReturnResource(msg.data.newentity, false, cmpResourceGatherer))
{
let cmpResourceDropsite = Engine.QueryInterface(msg.data.newentity, IID_ResourceDropsite);
let types = cmpResourceDropsite.GetTypes();
// TODO: Slightly undefined behavior here, we don't know what type of resource will be collected,
// may cause problems for AIs (especially hunting fast animals), but avoid ugly hacks to fix that!
let nearby = this.FindNearbyResource(this.TargetPosOrEntPos(msg.data.newentity),
(ent, type, template) => types.indexOf(type.generic) != -1);
if (nearby)
{
this.PerformGather(nearby, true, false);
return;
}
}
let nearbyFoundation = this.FindNearbyFoundation(this.TargetPosOrEntPos(msg.data.newentity));
if (nearbyFoundation)
{
this.AddOrder("Repair", { "target": nearbyFoundation, "autocontinue": oldData.autocontinue, "force": false }, true);
return;
}
// Unit was approaching and there's nothing to do now, so switch to walking
if (oldState.endsWith("REPAIR.APPROACHING"))
// We're already walking to the given point, so add this as a order.
this.WalkToTarget(msg.data.newentity, true);
},
},
"GARRISON": {
"leave": function() {
if (this.pickup)
{
Engine.PostMessage(this.pickup, MT_PickupCanceled, { "entity": this.entity });
delete this.pickup;
}
return false;
},
"APPROACHING": {
"enter": function() {
if (!this.MoveToGarrisonRange(this.order.data.target))
{
this.FinishOrder();
return true;
}
if (this.pickup)
Engine.PostMessage(this.pickup, MT_PickupCanceled, { "entity": this.entity });
let cmpGarrisonHolder = Engine.QueryInterface(this.order.data.target, IID_GarrisonHolder);
if (cmpGarrisonHolder && cmpGarrisonHolder.CanPickup(this.entity))
{
this.pickup = this.order.data.target; // temporary, deleted in "leave"
Engine.PostMessage(this.pickup, MT_PickupRequested, { "entity": this.entity });
}
return false;
},
"leave": function() {
this.StopMoving();
},
"MovementUpdate": function(msg) {
if (msg.likelyFailure || msg.likelySuccess)
this.SetNextState("GARRISONED");
},
},
"GARRISONED": {
"enter": function() {
let target = this.order.data.target;
if (!target)
{
this.FinishOrder();
return true;
}
// Called when autogarrisoning.
if (this.isGarrisoned)
{
this.SetImmobile(true);
if (this.IsTurret())
{
this.SetNextState("IDLE");
return true;
}
return false;
}
if (this.CanGarrison(target))
if (this.CheckGarrisonRange(target))
{
var cmpGarrisonHolder = Engine.QueryInterface(target, IID_GarrisonHolder);
if (cmpGarrisonHolder.Garrison(this.entity))
{
this.isGarrisoned = true;
this.SetImmobile(true);
if (this.formationController)
{
var cmpFormation = Engine.QueryInterface(this.formationController, IID_Formation);
if (cmpFormation)
{
var rearrange = cmpFormation.rearrange;
cmpFormation.SetRearrange(false);
cmpFormation.RemoveMembers([this.entity]);
cmpFormation.SetRearrange(rearrange);
}
}
let cmpResourceGatherer = Engine.QueryInterface(this.entity, IID_ResourceGatherer);
if (this.CanReturnResource(target, true, cmpResourceGatherer))
{
cmpResourceGatherer.CommitResources(target);
this.SetDefaultAnimationVariant();
}
if (this.pickup)
{
Engine.PostMessage(this.pickup, MT_PickupCanceled, { "entity": this.entity });
delete this.pickup;
}
if (this.IsTurret())
{
this.SetNextState("IDLE");
return true;
}
return false;
}
}
else
{
// Unable to reach the target, try again (or follow if it is a moving target)
// except if the does not exits anymore or its orders have changed
if (this.pickup)
{
var cmpUnitAI = Engine.QueryInterface(this.pickup, IID_UnitAI);
if (!cmpUnitAI || (!cmpUnitAI.HasPickupOrder(this.entity) &&
!cmpUnitAI.IsIdle()))
{
this.FinishOrder();
return true;
}
}
this.SetNextState("APPROACHING");
return true;
}
this.FinishOrder();
return true;
},
"leave": function() {
}
},
},
"CHEERING": {
"enter": function() {
this.SelectAnimation("promotion");
this.StartTimer(this.cheeringTime);
return false;
},
"leave": function() {
// PushOrderFront preserves the cheering order,
// which can lead to very bad behaviour, so make
// sure to delete any queued ones.
for (let i = 1; i < this.orderQueue.length; ++i)
if (this.orderQueue[i].type == "Cheer")
this.orderQueue.splice(i--, 1);
this.StopTimer();
this.ResetAnimation();
},
"LosRangeUpdate": function(msg) {
if (msg && msg.data && msg.data.added && msg.data.added.length)
this.RespondToSightedEntities(msg.data.added);
},
"LosHealRangeUpdate": function(msg) {
if (msg && msg.data && msg.data.added && msg.data.added.length)
this.RespondToHealableEntities(msg.data.added);
},
"LosAttackRangeUpdate": function(msg) {
if (msg && msg.data && msg.data.added && msg.data.added.length && this.GetStance().targetVisibleEnemies)
this.AttackEntitiesByPreference(msg.data.added);
},
"Timer": function(msg) {
this.FinishOrder();
},
},
"PACKING": {
"enter": function() {
let cmpPack = Engine.QueryInterface(this.entity, IID_Pack);
cmpPack.Pack();
return false;
},
"Order.CancelPack": function(msg) {
this.FinishOrder();
return ACCEPT_ORDER;
},
"PackFinished": function(msg) {
this.FinishOrder();
},
"leave": function() {
let cmpPack = Engine.QueryInterface(this.entity, IID_Pack);
cmpPack.CancelPack();
},
"Attacked": function(msg) {
// Ignore attacks while packing
},
},
"UNPACKING": {
"enter": function() {
let cmpPack = Engine.QueryInterface(this.entity, IID_Pack);
cmpPack.Unpack();
return false;
},
"Order.CancelUnpack": function(msg) {
this.FinishOrder();
return ACCEPT_ORDER;
},
"PackFinished": function(msg) {
this.FinishOrder();
},
"leave": function() {
let cmpPack = Engine.QueryInterface(this.entity, IID_Pack);
cmpPack.CancelPack();
},
"Attacked": function(msg) {
// Ignore attacks while unpacking
},
},
"PICKUP": {
"APPROACHING": {
"enter": function() {
if (!this.MoveTo(this.order.data))
{
this.FinishOrder();
return true;
}
return false;
},
"leave": function() {
this.StopMoving();
},
"MovementUpdate": function(msg) {
if (msg.likelyFailure || msg.likelySuccess)
this.SetNextState("LOADING");
},
"PickupCanceled": function() {
this.FinishOrder();
},
},
"LOADING": {
"enter": function() {
let cmpGarrisonHolder = Engine.QueryInterface(this.entity, IID_GarrisonHolder);
if (!cmpGarrisonHolder || cmpGarrisonHolder.IsFull())
{
this.FinishOrder();
return true;
}
return false;
},
"PickupCanceled": function() {
this.FinishOrder();
},
},
},
},
};
UnitAI.prototype.Init = function()
{
this.orderQueue = []; // current order is at the front of the list
this.order = undefined; // always == this.orderQueue[0]
this.formationController = INVALID_ENTITY; // entity with IID_Formation that we belong to
this.isGarrisoned = false;
this.isIdle = false;
this.isImmobile = false; // True if the unit is currently unable to move (garrisoned,...)
this.heldPosition = undefined;
// Queue of remembered works
this.workOrders = [];
this.isGuardOf = undefined;
// For preventing increased action rate due to Stop orders or target death.
this.lastAttacked = undefined;
this.lastHealed = undefined;
this.formationAnimationVariant = undefined;
this.cheeringTime = +(this.template.CheeringTime || 0);
this.SetStance(this.template.DefaultStance);
};
UnitAI.prototype.IsTurret = function()
{
let cmpPosition = Engine.QueryInterface(this.entity, IID_Position);
return cmpPosition && cmpPosition.GetTurretParent() != INVALID_ENTITY;
};
UnitAI.prototype.IsFormationController = function()
{
return (this.template.FormationController == "true");
};
UnitAI.prototype.IsFormationMember = function()
{
return (this.formationController != INVALID_ENTITY);
};
/**
* For now, entities with a RoamDistance are animals.
*/
UnitAI.prototype.IsAnimal = function()
{
return !!this.template.RoamDistance;
};
/**
* ToDo: Make this not needed by fixing gaia
* range queries in BuildingAI and UnitAI regarding
* animals and other gaia entities.
*/
UnitAI.prototype.IsDangerousAnimal = function()
{
return this.IsAnimal() && this.GetStance().targetVisibleEnemies && !!Engine.QueryInterface(this.entity, IID_Attack);
};
UnitAI.prototype.IsHealer = function()
{
return Engine.QueryInterface(this.entity, IID_Heal);
};
UnitAI.prototype.IsIdle = function()
{
return this.isIdle;
};
/**
* Used by formation controllers to toggle the idleness of their members.
*/
UnitAI.prototype.ResetIdle = function()
{
let shouldBeIdle = this.GetCurrentState().endsWith(".IDLE");
if (this.isIdle == shouldBeIdle)
return;
this.isIdle = shouldBeIdle;
Engine.PostMessage(this.entity, MT_UnitIdleChanged, { "idle": this.isIdle });
};
UnitAI.prototype.IsGarrisoned = function()
{
return this.isGarrisoned;
};
UnitAI.prototype.SetGarrisoned = function()
{
this.isGarrisoned = true;
};
UnitAI.prototype.GetGarrisonHolder = function()
{
if (!this.isGarrisoned)
return INVALID_ENTITY;
let cmpGarrisonable = Engine.QueryInterface(this.entity, IID_Garrisonable);
return cmpGarrisonable ? cmpGarrisonable.HolderID() : INVALID_ENTITY;
};
UnitAI.prototype.ShouldRespondToEndOfAlert = function()
{
return !this.orderQueue.length || this.orderQueue[0].type == "Garrison";
};
UnitAI.prototype.SetImmobile = function(immobile)
{
this.isImmobile = immobile;
Engine.PostMessage(this.entity, MT_UnitAbleToMoveChanged, {
"entity": this.entity,
"ableToMove": this.AbleToMove()
});
};
/**
* @param cmpUnitMotion - optionally pass unitMotion to avoid querying it here
* @returns true if the entity can move, i.e. has UnitMotion and isn't immobile.
*/
UnitAI.prototype.AbleToMove = function(cmpUnitMotion)
{
if (this.isImmobile || this.IsTurret())
return false;
if (!cmpUnitMotion)
cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion);
return !!cmpUnitMotion;
};
UnitAI.prototype.IsFleeing = function()
{
var state = this.GetCurrentState().split(".").pop();
return (state == "FLEEING");
};
UnitAI.prototype.IsWalking = function()
{
var state = this.GetCurrentState().split(".").pop();
return (state == "WALKING");
};
/**
* Return true if the current order is WalkAndFight or Patrol.
*/
UnitAI.prototype.IsWalkingAndFighting = function()
{
if (this.IsFormationMember())
return false;
return this.orderQueue.length > 0 && (this.orderQueue[0].type == "WalkAndFight" || this.orderQueue[0].type == "Patrol");
};
UnitAI.prototype.OnCreate = function()
{
if (this.IsFormationController())
this.UnitFsm.Init(this, "FORMATIONCONTROLLER.IDLE");
else
this.UnitFsm.Init(this, "INDIVIDUAL.IDLE");
this.isIdle = true;
};
UnitAI.prototype.OnDiplomacyChanged = function(msg)
{
let cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership);
if (cmpOwnership && cmpOwnership.GetOwner() == msg.player)
this.SetupRangeQueries();
if (this.isGuardOf && !IsOwnedByMutualAllyOfEntity(this.entity, this.isGuardOf))
this.RemoveGuard();
};
UnitAI.prototype.OnOwnershipChanged = function(msg)
{
this.SetupRangeQueries();
if (this.isGuardOf && (msg.to == INVALID_PLAYER || !IsOwnedByMutualAllyOfEntity(this.entity, this.isGuardOf)))
this.RemoveGuard();
// If the unit isn't being created or dying, reset stance and clear orders
if (msg.to != INVALID_PLAYER && msg.from != INVALID_PLAYER)
{
// Switch to a virgin state to let states execute their leave handlers.
// Except if garrisoned or (un)packing, in which case we only clear the order queue.
if (this.isGarrisoned || this.IsPacking())
{
this.orderQueue.length = Math.min(this.orderQueue.length, 1);
Engine.PostMessage(this.entity, MT_UnitAIOrderDataChanged, { "to": this.GetOrderData() });
}
else
{
let index = this.GetCurrentState().indexOf(".");
if (index != -1)
this.UnitFsm.SwitchToNextState(this, this.GetCurrentState().slice(0,index));
this.Stop(false);
}
this.workOrders = [];
let cmpTrader = Engine.QueryInterface(this.entity, IID_Trader);
if (cmpTrader)
cmpTrader.StopTrading();
this.SetStance(this.template.DefaultStance);
if (this.IsTurret())
this.SetTurretStance();
}
};
UnitAI.prototype.OnDestroy = function()
{
// Switch to an empty state to let states execute their leave handlers.
this.UnitFsm.SwitchToNextState(this, "");
let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
if (this.losRangeQuery)
cmpRangeManager.DestroyActiveQuery(this.losRangeQuery);
if (this.losHealRangeQuery)
cmpRangeManager.DestroyActiveQuery(this.losHealRangeQuery);
if (this.losAttackRangeQuery)
cmpRangeManager.DestroyActiveQuery(this.losAttackRangeQuery);
};
UnitAI.prototype.OnVisionRangeChanged = function(msg)
{
if (this.entity == msg.entity)
this.SetupRangeQueries();
};
UnitAI.prototype.HasPickupOrder = function(entity)
{
return this.orderQueue.some(order => order.type == "PickupUnit" && order.data.target == entity);
};
UnitAI.prototype.OnPickupRequested = function(msg)
{
if (this.HasPickupOrder(msg.entity))
return;
this.PushOrderAfterForced("PickupUnit", { "target": msg.entity });
};
UnitAI.prototype.OnPickupCanceled = function(msg)
{
for (let i = 0; i < this.orderQueue.length; ++i)
{
if (this.orderQueue[i].type != "PickupUnit" || this.orderQueue[i].data.target != msg.entity)
continue;
if (i == 0)
this.UnitFsm.ProcessMessage(this, {"type": "PickupCanceled", "data": msg});
else
this.orderQueue.splice(i, 1);
Engine.PostMessage(this.entity, MT_UnitAIOrderDataChanged, { "to": this.GetOrderData() });
break;
}
};
/**
* Wrapper function that sets up the LOS, healer and attack range queries.
* This should be called whenever our ownership changes.
*/
UnitAI.prototype.SetupRangeQueries = function()
{
if (this.GetStance().respondFleeOnSight)
this.SetupLOSRangeQuery();
if (this.IsHealer())
this.SetupHealRangeQuery();
if (Engine.QueryInterface(this.entity, IID_Attack))
this.SetupAttackRangeQuery();
};
UnitAI.prototype.UpdateRangeQueries = function()
{
let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
if (this.losRangeQuery)
this.SetupLOSRangeQuery(cmpRangeManager.IsActiveQueryEnabled(this.losRangeQuery));
if (this.losHealRangeQuery)
this.SetupHealRangeQuery(cmpRangeManager.IsActiveQueryEnabled(this.losHealRangeQuery));
if (this.losAttackRangeQuery)
this.SetupAttackRangeQuery(cmpRangeManager.IsActiveQueryEnabled(this.losAttackRangeQuery));
};
/**
* Set up a range query for all enemy units within LOS range.
* @param {boolean} enable - Optional parameter whether to enable the query.
*/
UnitAI.prototype.SetupLOSRangeQuery = function(enable = true)
{
let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
if (this.losRangeQuery)
{
cmpRangeManager.DestroyActiveQuery(this.losRangeQuery);
this.losRangeQuery = undefined;
}
let cmpPlayer = QueryOwnerInterface(this.entity);
// If we are being destructed (owner == -1), creating a range query is pointless.
if (!cmpPlayer)
return;
let players = cmpPlayer.GetEnemies();
if (!players.length)
return;
let range = this.GetQueryRange(IID_Vision);
// Do not compensate for entity sizes: LOS doesn't, and UnitAI relies on that.
this.losRangeQuery = cmpRangeManager.CreateActiveQuery(this.entity,
range.min, range.max, players, IID_Identity,
cmpRangeManager.GetEntityFlagMask("normal"), false);
if (enable)
cmpRangeManager.EnableActiveQuery(this.losRangeQuery);
};
/**
* Set up a range query for all own or ally units within LOS range
* which can be healed.
* @param {boolean} enable - Optional parameter whether to enable the query.
*/
UnitAI.prototype.SetupHealRangeQuery = function(enable = true)
{
let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
if (this.losHealRangeQuery)
{
cmpRangeManager.DestroyActiveQuery(this.losHealRangeQuery);
this.losHealRangeQuery = undefined;
}
let cmpPlayer = QueryOwnerInterface(this.entity);
// If we are being destructed (owner == -1), creating a range query is pointless.
if (!cmpPlayer)
return;
let players = cmpPlayer.GetAllies();
let range = this.GetQueryRange(IID_Heal);
// Do not compensate for entity sizes: LOS doesn't, and UnitAI relies on that.
this.losHealRangeQuery = cmpRangeManager.CreateActiveQuery(this.entity,
range.min, range.max, players, IID_Health,
cmpRangeManager.GetEntityFlagMask("injured"), false);
if (enable)
cmpRangeManager.EnableActiveQuery(this.losHealRangeQuery);
};
/**
* Set up a range query for all enemy and gaia units within range
* which can be attacked.
* @param {boolean} enable - Optional parameter whether to enable the query.
*/
UnitAI.prototype.SetupAttackRangeQuery = function(enable = true)
{
let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
if (this.losAttackRangeQuery)
{
cmpRangeManager.DestroyActiveQuery(this.losAttackRangeQuery);
this.losAttackRangeQuery = undefined;
}
let cmpPlayer = QueryOwnerInterface(this.entity);
// If we are being destructed (owner == -1), creating a range query is pointless.
if (!cmpPlayer)
return;
// TODO: How to handle neutral players - Special query to attack military only?
let players = cmpPlayer.GetEnemies();
if (!players.length)
return;
let range = this.GetQueryRange(IID_Attack);
// Do not compensate for entity sizes: LOS doesn't, and UnitAI relies on that.
this.losAttackRangeQuery = cmpRangeManager.CreateActiveQuery(this.entity,
range.min, range.max, players, IID_Resistance,
cmpRangeManager.GetEntityFlagMask("normal"), false);
if (enable)
cmpRangeManager.EnableActiveQuery(this.losAttackRangeQuery);
};
//// FSM linkage functions ////
// Setting the next state to the current state will leave/re-enter the top-most substate.
UnitAI.prototype.SetNextState = function(state)
{
this.UnitFsm.SetNextState(this, state);
};
UnitAI.prototype.DeferMessage = function(msg)
{
this.UnitFsm.DeferMessage(this, msg);
};
UnitAI.prototype.GetCurrentState = function()
{
return this.UnitFsm.GetCurrentState(this);
};
UnitAI.prototype.FsmStateNameChanged = function(state)
{
Engine.PostMessage(this.entity, MT_UnitAIStateChanged, { "to": state });
};
/**
* Call when the current order has been completed (or failed).
* Removes the current order from the queue, and processes the
* next one (if any). Returns false and defaults to IDLE
* if there are no remaining orders or if the unit is not
* inWorld and not garrisoned (thus usually waiting to be destroyed).
*/
UnitAI.prototype.FinishOrder = function()
{
if (!this.orderQueue.length)
{
let stack = new Error().stack.trimRight().replace(/^/mg, ' '); // indent each line
let cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager);
let template = cmpTemplateManager.GetCurrentTemplateName(this.entity);
error("FinishOrder called for entity " + this.entity + " (" + template + ") when order queue is empty\n" + stack);
}
this.orderQueue.shift();
this.order = this.orderQueue[0];
let cmpPosition = Engine.QueryInterface(this.entity, IID_Position);
if (this.orderQueue.length && (this.IsGarrisoned() || this.IsFormationController() ||
cmpPosition && cmpPosition.IsInWorld()))
{
let ret = this.UnitFsm.ProcessMessage(this,
{ "type": "Order."+this.order.type, "data": this.order.data }
);
Engine.PostMessage(this.entity, MT_UnitAIOrderDataChanged, { "to": this.GetOrderData() });
// If the order was rejected then immediately take it off
// and process the remaining queue
if (ret === REJECT_ORDER)
return this.FinishOrder();
// Otherwise we've successfully processed a new order
return true;
}
this.orderQueue = [];
this.order = undefined;
// Switch to IDLE as a default state.
this.SetNextState("IDLE");
Engine.PostMessage(this.entity, MT_UnitAIOrderDataChanged, { "to": this.GetOrderData() });
// Check if there are queued formation orders
if (this.IsFormationMember())
{
this.SetNextState("FORMATIONMEMBER.IDLE");
let cmpUnitAI = Engine.QueryInterface(this.formationController, IID_UnitAI);
if (cmpUnitAI)
{
// Inform the formation controller that we finished this task
let cmpFormation = Engine.QueryInterface(this.formationController, IID_Formation);
cmpFormation.SetWaitingOnController(this.entity);
// We don't want to carry out the default order
// if there are still queued formation orders left
if (cmpUnitAI.GetOrders().length > 1)
return true;
}
}
return false;
};
/**
* Add an order onto the back of the queue,
* and execute it if we didn't already have an order.
*/
UnitAI.prototype.PushOrder = function(type, data)
{
var order = { "type": type, "data": data };
this.orderQueue.push(order);
if (this.orderQueue.length == 1)
{
this.order = order;
let ret = this.UnitFsm.ProcessMessage(this,
{ "type": "Order."+this.order.type, "data": this.order.data }
);
// If the order was rejected then immediately take it off
// and process the remaining queue
if (ret === REJECT_ORDER)
this.FinishOrder();
}
Engine.PostMessage(this.entity, MT_UnitAIOrderDataChanged, { "to": this.GetOrderData() });
};
/**
* Add an order onto the front of the queue,
* and execute it immediately.
*/
UnitAI.prototype.PushOrderFront = function(type, data, ignorePacking = false)
{
var order = { "type": type, "data": data };
// If current order is packing/unpacking then add new order after it.
if (!ignorePacking && this.order && this.IsPacking())
{
var packingOrder = this.orderQueue.shift();
this.orderQueue.unshift(packingOrder, order);
}
else
{
this.orderQueue.unshift(order);
this.order = order;
let ret = this.UnitFsm.ProcessMessage(this,
{ "type": "Order."+this.order.type, "data": this.order.data }
);
// If the order was rejected then immediately take it off again;
// assume the previous active order is still valid (the short-lived
// new order hasn't changed state or anything) so we can carry on
// as if nothing had happened
if (ret === REJECT_ORDER)
{
this.orderQueue.shift();
this.order = this.orderQueue[0];
}
}
Engine.PostMessage(this.entity, MT_UnitAIOrderDataChanged, { "to": this.GetOrderData() });
};
/**
* Insert an order after the last forced order onto the queue
* and after the other orders of the same type
*/
UnitAI.prototype.PushOrderAfterForced = function(type, data)
{
if (!this.order || ((!this.order.data || !this.order.data.force) && this.order.type != type))
this.PushOrderFront(type, data);
else
{
for (let i = 1; i < this.orderQueue.length; ++i)
{
if (this.orderQueue[i].data && this.orderQueue[i].data.force)
continue;
if (this.orderQueue[i].type == type)
continue;
this.orderQueue.splice(i, 0, {"type": type, "data": data});
Engine.PostMessage(this.entity, MT_UnitAIOrderDataChanged, { "to": this.GetOrderData() });
return;
}
this.PushOrder(type, data);
}
Engine.PostMessage(this.entity, MT_UnitAIOrderDataChanged, { "to": this.GetOrderData() });
};
/**
* For a unit that is packing and trying to attack something,
* either cancel packing or continue with packing, as appropriate.
* Precondition: if the unit is packing/unpacking, then orderQueue
* should have the Attack order at index 0,
* and the Pack/Unpack order at index 1.
* This precondition holds because if we are packing while processing "Order.Attack",
* then we must have come from ReplaceOrder, which guarantees it.
*
* @param {boolean} requirePacked - true if the unit needs to be packed to continue attacking,
* false if it needs to be unpacked.
* @return {boolean} true if the unit can attack now, false if it must continue packing (or unpacking) first.
*/
UnitAI.prototype.EnsureCorrectPackStateForAttack = function(requirePacked)
{
let cmpPack = Engine.QueryInterface(this.entity, IID_Pack);
if (!cmpPack ||
!cmpPack.IsPacking() ||
this.orderQueue.length != 2 ||
this.orderQueue[0].type != "Attack" ||
this.orderQueue[1].type != "Pack" &&
this.orderQueue[1].type != "Unpack")
return true;
if (cmpPack.IsPacked() == requirePacked)
{
// The unit is already in the packed/unpacked state we want.
// Delete the packing order.
this.orderQueue.splice(1, 1);
cmpPack.CancelPack();
Engine.PostMessage(this.entity, MT_UnitAIOrderDataChanged, { "to": this.GetOrderData() });
// Continue with the attack order.
return true;
}
// Move the attack order behind the unpacking order, to continue unpacking.
let tmp = this.orderQueue[0];
this.orderQueue[0] = this.orderQueue[1];
this.orderQueue[1] = tmp;
Engine.PostMessage(this.entity, MT_UnitAIOrderDataChanged, { "to": this.GetOrderData() });
return false;
};
UnitAI.prototype.WillMoveFromFoundation = function(target, checkPacking = true)
{
let cmpUnitAI = Engine.QueryInterface(target, IID_UnitAI);
if (!IsOwnedByAllyOfEntity(this.entity, target) && cmpUnitAI && !cmpUnitAI.IsAnimal() &&
!Engine.QueryInterface(SYSTEM_ENTITY, IID_CeasefireManager).IsCeasefireActive() ||
checkPacking && this.IsPacking() || this.CanPack() || !this.AbleToMove())
return false;
return !this.CheckTargetRangeExplicit(target, g_LeaveFoundationRange, -1);
};
UnitAI.prototype.ReplaceOrder = function(type, data)
{
// Remember the previous work orders to be able to go back to them later if required
if (data && data.force)
{
if (this.IsFormationController())
this.CallMemberFunction("UpdateWorkOrders", [type]);
else
this.UpdateWorkOrders(type);
}
let garrisonHolder = this.IsGarrisoned() && type != "Ungarrison" ? this.GetGarrisonHolder() : null;
// Do not replace packing/unpacking unless it is cancel order.
// TODO: maybe a better way of doing this would be to use priority levels
if (this.IsPacking() && type != "CancelPack" && type != "CancelUnpack" && type != "Stop")
{
var order = { "type": type, "data": data };
var packingOrder = this.orderQueue.shift();
if (type == "Attack")
{
// The Attack order is able to handle a packing unit, while other orders can't.
this.orderQueue = [packingOrder];
this.PushOrderFront(type, data, true);
}
else if (packingOrder.type == "Unpack" && g_OrdersCancelUnpacking.has(type))
{
// Immediately cancel unpacking before processing an order that demands a packed unit.
let cmpPack = Engine.QueryInterface(this.entity, IID_Pack);
cmpPack.CancelPack();
this.orderQueue = [];
this.PushOrder(type, data);
}
else
this.orderQueue = [packingOrder, order];
}
else if (this.IsFormationMember())
{
// Don't replace orders after a LeaveFormation order
// (this is needed to support queued no-formation orders).
let idx = this.orderQueue.findIndex(o => o.type == "LeaveFormation");
if (idx === -1)
{
this.orderQueue = [];
this.order = undefined;
}
else
this.orderQueue.splice(0, idx);
this.PushOrderFront(type, data);
}
else
{
this.orderQueue = [];
this.PushOrder(type, data);
}
if (garrisonHolder)
this.PushOrder("Garrison", { "target": garrisonHolder });
Engine.PostMessage(this.entity, MT_UnitAIOrderDataChanged, { "to": this.GetOrderData() });
};
UnitAI.prototype.GetOrders = function()
{
return this.orderQueue.slice();
};
UnitAI.prototype.AddOrders = function(orders)
{
orders.forEach(order => this.PushOrder(order.type, order.data));
};
UnitAI.prototype.GetOrderData = function()
{
var orders = [];
for (let order of this.orderQueue)
if (order.data)
orders.push(clone(order.data));
return orders;
};
UnitAI.prototype.UpdateWorkOrders = function(type)
{
var isWorkType = type => type == "Gather" || type == "Trade" || type == "Repair" || type == "ReturnResource";
if (isWorkType(type))
{
this.workOrders = [];
return;
}
if (this.workOrders.length)
return;
if (this.IsFormationMember())
{
var cmpUnitAI = Engine.QueryInterface(this.formationController, IID_UnitAI);
if (cmpUnitAI)
{
for (var i = 0; i < cmpUnitAI.orderQueue.length; ++i)
{
if (isWorkType(cmpUnitAI.orderQueue[i].type))
{
this.workOrders = cmpUnitAI.orderQueue.slice(i);
return;
}
}
}
}
// If nothing found, take the unit orders
for (var i = 0; i < this.orderQueue.length; ++i)
{
if (isWorkType(this.orderQueue[i].type))
{
this.workOrders = this.orderQueue.slice(i);
return;
}
}
};
UnitAI.prototype.BackToWork = function()
{
if (this.workOrders.length == 0)
return false;
if (this.IsGarrisoned())
{
let cmpGarrisonHolder = Engine.QueryInterface(this.GetGarrisonHolder(), IID_GarrisonHolder);
if (!cmpGarrisonHolder || !cmpGarrisonHolder.PerformEject([this.entity], false))
return false;
}
this.orderQueue = [];
this.AddOrders(this.workOrders);
Engine.PostMessage(this.entity, MT_UnitAIOrderDataChanged, { "to": this.GetOrderData() });
if (this.IsFormationMember())
{
var cmpFormation = Engine.QueryInterface(this.formationController, IID_Formation);
if (cmpFormation)
cmpFormation.RemoveMembers([this.entity]);
}
this.workOrders = [];
return true;
};
UnitAI.prototype.HasWorkOrders = function()
{
return this.workOrders.length > 0;
};
UnitAI.prototype.GetWorkOrders = function()
{
return this.workOrders;
};
UnitAI.prototype.SetWorkOrders = function(orders)
{
this.workOrders = orders;
};
UnitAI.prototype.TimerHandler = function(data, lateness)
{
// Reset the timer
if (data.timerRepeat === undefined)
this.timer = undefined;
this.UnitFsm.ProcessMessage(this, {"type": "Timer", "data": data, "lateness": lateness});
};
/**
* Set up the UnitAI timer to run after 'offset' msecs, and then
* every 'repeat' msecs until StopTimer is called. A "Timer" message
* will be sent each time the timer runs.
*/
UnitAI.prototype.StartTimer = function(offset, repeat)
{
if (this.timer)
error("Called StartTimer when there's already an active timer");
var data = { "timerRepeat": repeat };
var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
if (repeat === undefined)
this.timer = cmpTimer.SetTimeout(this.entity, IID_UnitAI, "TimerHandler", offset, data);
else
this.timer = cmpTimer.SetInterval(this.entity, IID_UnitAI, "TimerHandler", offset, repeat, data);
};
/**
* Stop the current UnitAI timer.
*/
UnitAI.prototype.StopTimer = function()
{
if (!this.timer)
return;
var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
cmpTimer.CancelTimer(this.timer);
this.timer = undefined;
};
UnitAI.prototype.OnMotionUpdate = function(msg)
{
if (msg.veryObstructed)
msg.obstructed = true;
this.UnitFsm.ProcessMessage(this, Object.assign({ "type": "MovementUpdate" }, msg));
};
/**
* Called directly by cmpFoundation and cmpRepairable to
* inform builders that repairing has finished.
* This not done by listening to a global message due to performance.
*/
UnitAI.prototype.ConstructionFinished = function(msg)
{
this.UnitFsm.ProcessMessage(this, { "type": "ConstructionFinished", "data": msg });
};
UnitAI.prototype.OnGlobalEntityRenamed = function(msg)
{
let changed = false;
let currentOrderChanged = false;
for (let i = 0; i < this.orderQueue.length; ++i)
{
let order = this.orderQueue[i];
if (order.data && order.data.target && order.data.target == msg.entity)
{
changed = true;
if (i == 0)
currentOrderChanged = true;
order.data.target = msg.newentity;
}
if (order.data && order.data.formationTarget && order.data.formationTarget == msg.entity)
{
changed = true;
if (i == 0)
currentOrderChanged = true;
order.data.formationTarget = msg.newentity;
}
}
if (!changed)
return;
if (currentOrderChanged)
this.UnitFsm.ProcessMessage(this, { "type": "OrderTargetRenamed", "data": msg });
Engine.PostMessage(this.entity, MT_UnitAIOrderDataChanged, { "to": this.GetOrderData() });
};
UnitAI.prototype.OnAttacked = function(msg)
{
if (msg.fromStatusEffect)
return;
this.UnitFsm.ProcessMessage(this, {"type": "Attacked", "data": msg});
};
UnitAI.prototype.OnGuardedAttacked = function(msg)
{
this.UnitFsm.ProcessMessage(this, {"type": "GuardedAttacked", "data": msg.data});
};
UnitAI.prototype.OnRangeUpdate = function(msg)
{
if (msg.tag == this.losRangeQuery)
this.UnitFsm.ProcessMessage(this, { "type": "LosRangeUpdate", "data": msg });
else if (msg.tag == this.losHealRangeQuery)
this.UnitFsm.ProcessMessage(this, { "type": "LosHealRangeUpdate", "data": msg });
else if (msg.tag == this.losAttackRangeQuery)
this.UnitFsm.ProcessMessage(this, { "type": "LosAttackRangeUpdate", "data": msg });
};
UnitAI.prototype.OnPackFinished = function(msg)
{
this.UnitFsm.ProcessMessage(this, {"type": "PackFinished", "packed": msg.packed});
};
//// Helper functions to be called by the FSM ////
UnitAI.prototype.GetWalkSpeed = function()
{
let cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion);
if (!cmpUnitMotion)
return 0;
return cmpUnitMotion.GetWalkSpeed();
};
UnitAI.prototype.GetRunMultiplier = function()
{
var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion);
if (!cmpUnitMotion)
return 0;
return cmpUnitMotion.GetRunMultiplier();
};
/**
* Returns true if the target exists and has non-zero hitpoints.
*/
UnitAI.prototype.TargetIsAlive = function(ent)
{
var cmpFormation = Engine.QueryInterface(ent, IID_Formation);
if (cmpFormation)
return true;
var cmpHealth = QueryMiragedInterface(ent, IID_Health);
return cmpHealth && cmpHealth.GetHitpoints() != 0;
};
/**
* Returns true if the target exists and needs to be killed before
* beginning to gather resources from it.
*/
UnitAI.prototype.MustKillGatherTarget = function(ent)
{
var cmpResourceSupply = Engine.QueryInterface(ent, IID_ResourceSupply);
if (!cmpResourceSupply)
return false;
if (!cmpResourceSupply.GetKillBeforeGather())
return false;
return this.TargetIsAlive(ent);
};
/**
* Returns the position of target or, if there is none,
* the entity's position, or undefined.
*/
UnitAI.prototype.TargetPosOrEntPos = function(target)
{
let cmpTargetPosition = Engine.QueryInterface(target, IID_Position);
if (cmpTargetPosition && cmpTargetPosition.IsInWorld())
return cmpTargetPosition.GetPosition2D();
let cmpPosition = Engine.QueryInterface(this.entity, IID_Position);
if (cmpPosition && cmpPosition.IsInWorld())
return cmpPosition.GetPosition2D();
return undefined;
};
/**
* Returns the entity ID of the nearest resource supply where the given
* filter returns true, or undefined if none can be found.
* "Nearest" is nearest from @param position.
* TODO: extend this to exclude resources that already have lots of gatherers.
*/
UnitAI.prototype.FindNearbyResource = function(position, filter)
{
if (!position)
return undefined;
// We accept resources owned by Gaia or any player
let players = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager).GetAllPlayers();
let range = 64; // TODO: what's a sensible number?
let cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager);
let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
// Don't account for entity size, we need to match LOS visibility.
let nearby = cmpRangeManager.ExecuteQueryAroundPos(position, 0, range, players, IID_ResourceSupply, false);
return nearby.find(ent => {
if (!this.CanGather(ent) || !this.CheckTargetVisible(ent))
return false;
let cmpResourceSupply = Engine.QueryInterface(ent, IID_ResourceSupply);
let type = cmpResourceSupply.GetType();
let amount = cmpResourceSupply.GetCurrentAmount();
let template = cmpTemplateManager.GetCurrentTemplateName(ent);
if (template.indexOf("resource|") != -1)
template = template.slice(9);
return amount > 0 && cmpResourceSupply.IsAvailableTo(this.entity) && filter(ent, type, template);
});
};
/**
* Returns the entity ID of the nearest resource dropsite that accepts
* the given type, or undefined if none can be found.
*/
UnitAI.prototype.FindNearestDropsite = function(genericType)
{
let cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership);
if (!cmpOwnership || cmpOwnership.GetOwner() == INVALID_PLAYER)
return undefined;
let cmpPosition = Engine.QueryInterface(this.entity, IID_Position);
if (!cmpPosition || !cmpPosition.IsInWorld())
return undefined;
let pos = cmpPosition.GetPosition2D();
let bestDropsite;
let bestDist = Infinity;
// Maximum distance a point on an obstruction can be from the center of the obstruction.
let maxDifference = 40;
let owner = cmpOwnership.GetOwner();
let cmpPlayer = QueryOwnerInterface(this.entity);
let players = cmpPlayer && cmpPlayer.HasSharedDropsites() ? cmpPlayer.GetMutualAllies() : [owner];
let nearestDropsites = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager).ExecuteQuery(this.entity, 0, -1, players, IID_ResourceDropsite, false);
let isShip = Engine.QueryInterface(this.entity, IID_Identity).HasClass("Ship");
let cmpObstructionManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ObstructionManager);
for (let dropsite of nearestDropsites)
{
// Ships are unable to reach land dropsites and shouldn't attempt to do so.
if (isShip && !Engine.QueryInterface(dropsite, IID_Identity).HasClass("Naval"))
continue;
let cmpResourceDropsite = Engine.QueryInterface(dropsite, IID_ResourceDropsite);
if (!cmpResourceDropsite.AcceptsType(genericType) || !this.CheckTargetVisible(dropsite))
continue;
if (Engine.QueryInterface(dropsite, IID_Ownership).GetOwner() != owner && !cmpResourceDropsite.IsShared())
continue;
// The range manager sorts entities by the distance to their center,
// but we want the distance to the point where resources will be dropped off.
let dist = cmpObstructionManager.DistanceToPoint(dropsite, pos.x, pos.y);
if (dist == -1)
continue;
if (dist < bestDist)
{
bestDropsite = dropsite;
bestDist = dist;
}
else if (dist > bestDist + maxDifference)
break;
}
return bestDropsite;
};
/**
* Returns the entity ID of the nearest building that needs to be constructed.
* "Nearest" is nearest from @param position.
*/
UnitAI.prototype.FindNearbyFoundation = function(position)
{
if (!position)
return undefined;
let cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership);
if (!cmpOwnership || cmpOwnership.GetOwner() == INVALID_PLAYER)
return undefined;
let players = [cmpOwnership.GetOwner()];
let range = 64; // TODO: what's a sensible number?
let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
// Don't account for entity size, we need to match LOS visibility.
let nearby = cmpRangeManager.ExecuteQueryAroundPos(position, 0, range, players, IID_Foundation, false);
// Skip foundations that are already complete. (This matters since
// we process the ConstructionFinished message before the foundation
// we're working on has been deleted.)
return nearby.find(ent => !Engine.QueryInterface(ent, IID_Foundation).IsFinished());
};
/**
* Play a sound appropriate to the current entity.
*/
UnitAI.prototype.PlaySound = function(name)
{
if (this.IsFormationController())
{
var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation);
var member = cmpFormation.GetPrimaryMember();
if (member)
PlaySound(name, member);
}
else
{
PlaySound(name, this.entity);
}
};
/*
* Set a visualActor animation variant.
* By changing the animation variant, you can change animations based on unitAI state.
* If there are no specific variants or the variant doesn't exist in the actor,
* the actor fallbacks to any existing animation.
* @param type if present, switch to a specific animation variant.
*/
UnitAI.prototype.SetAnimationVariant = function(type)
{
let cmpVisual = Engine.QueryInterface(this.entity, IID_Visual);
if (!cmpVisual)
return;
cmpVisual.SetVariant("animationVariant", type);
};
/*
* Reset the animation variant to default behavior.
* Default behavior is to pick a resource-carrying variant if resources are being carried.
* Otherwise pick nothing in particular.
*/
UnitAI.prototype.SetDefaultAnimationVariant = function()
{
let cmpResourceGatherer = Engine.QueryInterface(this.entity, IID_ResourceGatherer);
if (cmpResourceGatherer)
{
let type = cmpResourceGatherer.GetLastCarriedType();
if (type)
{
let typename = "carry_" + type.generic;
if (type.specific == "meat")
typename = "carry_" + type.specific;
this.SetAnimationVariant(typename);
return;
}
}
this.SetAnimationVariant("");
};
UnitAI.prototype.ResetAnimation = function()
{
let cmpVisual = Engine.QueryInterface(this.entity, IID_Visual);
if (!cmpVisual)
return;
cmpVisual.SelectAnimation("idle", false, 1.0);
};
UnitAI.prototype.SelectAnimation = function(name, once = false, speed = 1.0)
{
let cmpVisual = Engine.QueryInterface(this.entity, IID_Visual);
if (!cmpVisual)
return;
cmpVisual.SelectAnimation(name, once, speed);
};
UnitAI.prototype.SetAnimationSync = function(actiontime, repeattime)
{
var cmpVisual = Engine.QueryInterface(this.entity, IID_Visual);
if (!cmpVisual)
return;
cmpVisual.SetAnimationSyncRepeat(repeattime);
cmpVisual.SetAnimationSyncOffset(actiontime);
};
UnitAI.prototype.StopMoving = function()
{
let cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion);
if (cmpUnitMotion)
cmpUnitMotion.StopMoving();
};
/**
* Generic dispatcher for other MoveTo functions.
* @param iid - Interface ID (optional) implementing GetRange
* @param type - Range type for the interface call
* @returns whether the move succeeded or failed.
*/
UnitAI.prototype.MoveTo = function(data, iid, type)
{
if (data.target)
{
if (data.min || data.max)
return this.MoveToTargetRangeExplicit(data.target, data.min || -1, data.max || -1);
else if (!iid)
return this.MoveToTarget(data.target);
return this.MoveToTargetRange(data.target, iid, type);
}
else if (data.min || data.max)
return this.MoveToPointRange(data.x, data.z, data.min || -1, data.max || -1);
return this.MoveToPoint(data.x, data.z);
};
UnitAI.prototype.MoveToPoint = function(x, z)
{
let cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion);
return this.AbleToMove(cmpUnitMotion) && cmpUnitMotion.MoveToPointRange(x, z, 0, 0); // For point goals, allow a max range of 0.
};
UnitAI.prototype.MoveToPointRange = function(x, z, rangeMin, rangeMax)
{
let cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion);
return this.AbleToMove(cmpUnitMotion) && cmpUnitMotion.MoveToPointRange(x, z, rangeMin, rangeMax);
};
UnitAI.prototype.MoveToTarget = function(target)
{
if (!this.CheckTargetVisible(target))
return false;
let cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion);
return this.AbleToMove(cmpUnitMotion) && cmpUnitMotion.MoveToTargetRange(target, 0, 1);
};
UnitAI.prototype.MoveToTargetRange = function(target, iid, type)
{
if (!this.CheckTargetVisible(target))
return false;
let range = this.GetRange(iid, type);
if (!range)
return false;
let cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion);
return this.AbleToMove(cmpUnitMotion) && cmpUnitMotion.MoveToTargetRange(target, range.min, range.max);
};
/**
* Move unit so we hope the target is in the attack range
* for melee attacks, this goes straight to the default range checks
* for ranged attacks, the parabolic range is used
*/
UnitAI.prototype.MoveToTargetAttackRange = function(target, type)
{
// for formation members, the formation will take care of the range check
if (this.IsFormationMember())
{
let cmpFormationUnitAI = Engine.QueryInterface(this.formationController, IID_UnitAI);
if (cmpFormationUnitAI && cmpFormationUnitAI.IsAttackingAsFormation())
return false;
}
let cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion);
if (!this.AbleToMove(cmpUnitMotion))
return false;
let cmpFormation = Engine.QueryInterface(target, IID_Formation);
if (cmpFormation)
target = cmpFormation.GetClosestMember(this.entity);
if (type != "Ranged")
return this.MoveToTargetRange(target, IID_Attack, type);
if (!this.CheckTargetVisible(target))
return false;
let range = this.GetRange(IID_Attack, type);
if (!range)
return false;
let thisCmpPosition = Engine.QueryInterface(this.entity, IID_Position);
if (!thisCmpPosition.IsInWorld())
return false;
let s = thisCmpPosition.GetPosition();
let targetCmpPosition = Engine.QueryInterface(target, IID_Position);
if (!targetCmpPosition || !targetCmpPosition.IsInWorld())
return false;
// Parabolic range compuation is the same as in BuildingAI's FireArrows.
let t = targetCmpPosition.GetPosition();
// h is positive when I'm higher than the target
let h = s.y - t.y + range.elevationBonus;
let parabolicMaxRange = Math.sqrt(Math.square(range.max) + 2 * range.max * h);
// No negative roots please
if (h <= -range.max / 2)
// return false? Or hope you come close enough?
parabolicMaxRange = 0;
// The parabole changes while walking so be cautious:
let guessedMaxRange = parabolicMaxRange > range.max ? (range.max + parabolicMaxRange) / 2 : parabolicMaxRange;
return cmpUnitMotion && cmpUnitMotion.MoveToTargetRange(target, range.min, guessedMaxRange);
};
UnitAI.prototype.MoveToTargetRangeExplicit = function(target, min, max)
{
if (!this.CheckTargetVisible(target))
return false;
let cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion);
return this.AbleToMove(cmpUnitMotion) && cmpUnitMotion.MoveToTargetRange(target, min, max);
};
/**
* Move unit so we hope the target is in the attack range of the formation.
*
* @param {number} target - The target entity ID to attack.
* @return {boolean} - Whether the order to move has succeeded.
*/
UnitAI.prototype.MoveFormationToTargetAttackRange = function(target)
{
let cmpTargetFormation = Engine.QueryInterface(target, IID_Formation);
if (cmpTargetFormation)
target = cmpTargetFormation.GetClosestMember(this.entity);
if (!this.CheckTargetVisible(target))
return false;
let cmpFormationAttack = Engine.QueryInterface(this.entity, IID_Attack);
if (!cmpFormationAttack)
return false;
let range = cmpFormationAttack.GetRange(target);
let cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion);
return this.AbleToMove(cmpUnitMotion) && cmpUnitMotion.MoveToTargetRange(target, range.min, range.max);
};
UnitAI.prototype.MoveToGarrisonRange = function(target)
{
if (!this.CheckTargetVisible(target))
return false;
var cmpGarrisonHolder = Engine.QueryInterface(target, IID_GarrisonHolder);
if (!cmpGarrisonHolder)
return false;
var range = cmpGarrisonHolder.GetLoadingRange();
let cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion);
return this.AbleToMove(cmpUnitMotion) && cmpUnitMotion.MoveToTargetRange(target, range.min, range.max);
};
/**
* Generic dispatcher for other Check...Range functions.
* @param iid - Interface ID (optional) implementing GetRange
* @param type - Range type for the interface call
*/
UnitAI.prototype.CheckRange = function(data, iid, type)
{
if (data.target)
{
if (data.min || data.max)
return this.CheckTargetRangeExplicit(data.target, data.min || -1, data.max || -1);
else if (!iid)
return this.CheckTargetRangeExplicit(data.target, 0, 1);
return this.CheckTargetRange(data.target, iid, type);
}
else if (data.min || data.max)
return this.CheckPointRangeExplicit(data.x, data.z, data.min || -1, data.max || -1);
return this.CheckPointRangeExplicit(data.x, data.z, 0, 0);
};
UnitAI.prototype.CheckPointRangeExplicit = function(x, z, min, max)
{
let cmpObstructionManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ObstructionManager);
return cmpObstructionManager.IsInPointRange(this.entity, x, z, min, max, false);
};
UnitAI.prototype.CheckTargetRange = function(target, iid, type)
{
let range = this.GetRange(iid, type);
if (!range)
return false;
let cmpObstructionManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ObstructionManager);
return cmpObstructionManager.IsInTargetRange(this.entity, target, range.min, range.max, false);
};
/**
* Check if the target is inside the attack range
* For melee attacks, this goes straigt to the regular range calculation
* For ranged attacks, the parabolic formula is used to accout for bigger ranges
* when the target is lower, and smaller ranges when the target is higher
*/
UnitAI.prototype.CheckTargetAttackRange = function(target, type)
{
// for formation members, the formation will take care of the range check
if (this.IsFormationMember())
{
let cmpFormationUnitAI = Engine.QueryInterface(this.formationController, IID_UnitAI);
if (cmpFormationUnitAI && cmpFormationUnitAI.IsAttackingAsFormation() &&
cmpFormationUnitAI.order.data.target == target)
return true;
}
let cmpFormation = Engine.QueryInterface(target, IID_Formation);
if (cmpFormation)
target = cmpFormation.GetClosestMember(this.entity);
if (type != "Ranged")
return this.CheckTargetRange(target, IID_Attack, type);
let targetCmpPosition = Engine.QueryInterface(target, IID_Position);
if (!targetCmpPosition || !targetCmpPosition.IsInWorld())
return false;
let range = this.GetRange(IID_Attack, type);
if (!range)
return false;
let thisCmpPosition = Engine.QueryInterface(this.entity, IID_Position);
if (!thisCmpPosition.IsInWorld())
return false;
let s = thisCmpPosition.GetPosition();
let t = targetCmpPosition.GetPosition();
let h = s.y - t.y + range.elevationBonus;
let maxRange = Math.sqrt(Math.square(range.max) + 2 * range.max * h);
if (maxRange < 0)
return false;
let cmpObstructionManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ObstructionManager);
return cmpObstructionManager.IsInTargetRange(this.entity, target, range.min, maxRange, false);
};
UnitAI.prototype.CheckTargetRangeExplicit = function(target, min, max)
{
let cmpObstructionManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ObstructionManager);
return cmpObstructionManager.IsInTargetRange(this.entity, target, min, max, false);
};
/**
* Check if the target is inside the attack range of the formation.
*
* @param {number} target - The target entity ID to attack.
* @return {boolean} - Whether the entity is within attacking distance.
*/
UnitAI.prototype.CheckFormationTargetAttackRange = function(target)
{
let cmpTargetFormation = Engine.QueryInterface(target, IID_Formation);
if (cmpTargetFormation)
target = cmpTargetFormation.GetClosestMember(this.entity);
let cmpFormationAttack = Engine.QueryInterface(this.entity, IID_Attack);
if (!cmpFormationAttack)
return false;
let range = cmpFormationAttack.GetRange(target);
let cmpObstructionManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ObstructionManager);
return cmpObstructionManager.IsInTargetRange(this.entity, target, range.min, range.max, false);
};
UnitAI.prototype.CheckGarrisonRange = function(target)
{
let cmpGarrisonHolder = Engine.QueryInterface(target, IID_GarrisonHolder);
if (!cmpGarrisonHolder)
return false;
let range = cmpGarrisonHolder.GetLoadingRange();
return this.CheckTargetRangeExplicit(target, range.min, range.max);
};
/**
* Returns true if the target entity is visible through the FoW/SoD.
*/
UnitAI.prototype.CheckTargetVisible = function(target)
{
var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership);
if (!cmpOwnership)
return false;
var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
if (!cmpRangeManager)
return false;
// Entities that are hidden and miraged are considered visible
var cmpFogging = Engine.QueryInterface(target, IID_Fogging);
if (cmpFogging && cmpFogging.IsMiraged(cmpOwnership.GetOwner()))
return true;
if (cmpRangeManager.GetLosVisibility(target, cmpOwnership.GetOwner()) == "hidden")
return false;
// Either visible directly, or visible in fog
return true;
};
/**
* Returns true if the given position is currentl visible (not in FoW/SoD).
*/
UnitAI.prototype.CheckPositionVisible = function(x, z)
{
let cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership);
if (!cmpOwnership)
return false;
let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
if (!cmpRangeManager)
return false;
return cmpRangeManager.GetLosVisibilityPosition(x, z, cmpOwnership.GetOwner()) == "visible";
};
/**
* How close to our goal do we consider it's OK to stop if the goal appears unreachable.
* Currently 3 terrain tiles as that's relatively close but helps pathfinding.
*/
UnitAI.prototype.DefaultRelaxedMaxRange = 12;
/**
* @returns true if the unit is in the relaxed-range from the target.
*/
UnitAI.prototype.RelaxedMaxRangeCheck = function(data, relaxedRange)
{
if (!data.relaxed)
return false;
let ndata = data;
ndata.min = 0;
ndata.max = relaxedRange;
return this.CheckRange(ndata);
};
/**
* Let an entity face its target.
* @param {number} target - The entity-ID of the target.
*/
UnitAI.prototype.FaceTowardsTarget = function(target)
{
let cmpTargetPosition = Engine.QueryInterface(target, IID_Position);
if (!cmpTargetPosition || !cmpTargetPosition.IsInWorld())
return;
let targetPosition = cmpTargetPosition.GetPosition2D();
// Use cmpUnitMotion for units that support that, otherwise try cmpPosition (e.g. turrets)
let cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion);
if (cmpUnitMotion)
{
cmpUnitMotion.FaceTowardsPoint(targetPosition.x, targetPosition.y);
return;
}
let cmpPosition = Engine.QueryInterface(this.entity, IID_Position);
if (cmpPosition && cmpPosition.IsInWorld())
cmpPosition.TurnTo(cmpPosition.GetPosition2D().angleTo(targetPosition));
};
UnitAI.prototype.CheckTargetDistanceFromHeldPosition = function(target, iid, type)
{
let range = this.GetRange(iid, type);
if (!range)
return false;
let cmpPosition = Engine.QueryInterface(target, IID_Position);
if (!cmpPosition || !cmpPosition.IsInWorld())
return false;
let cmpVision = Engine.QueryInterface(this.entity, IID_Vision);
if (!cmpVision)
return false;
let halfvision = cmpVision.GetRange() / 2;
let pos = cmpPosition.GetPosition();
let heldPosition = this.heldPosition;
if (heldPosition === undefined)
heldPosition = { "x": pos.x, "z": pos.z };
return Math.euclidDistance2D(pos.x, pos.z, heldPosition.x, heldPosition.z) < halfvision + range.max;
};
UnitAI.prototype.CheckTargetIsInVisionRange = function(target)
{
let cmpVision = Engine.QueryInterface(this.entity, IID_Vision);
if (!cmpVision)
return false;
let range = cmpVision.GetRange();
let distance = PositionHelper.DistanceBetweenEntities(this.entity, target);
return distance < range;
};
UnitAI.prototype.GetBestAttackAgainst = function(target, allowCapture)
{
var cmpAttack = Engine.QueryInterface(this.entity, IID_Attack);
if (!cmpAttack)
return undefined;
return cmpAttack.GetBestAttackAgainst(target, allowCapture);
};
/**
* Try to find one of the given entities which can be attacked,
* and start attacking it.
* Returns true if it found something to attack.
*/
UnitAI.prototype.AttackVisibleEntity = function(ents)
{
var target = ents.find(target => this.CanAttack(target));
if (!target)
return false;
this.PushOrderFront("Attack", { "target": target, "force": false, "allowCapture": true });
return true;
};
/**
* Try to find one of the given entities which can be attacked
* and which is close to the hold position, and start attacking it.
* Returns true if it found something to attack.
*/
UnitAI.prototype.AttackEntityInZone = function(ents)
{
var target = ents.find(target =>
this.CanAttack(target)
&& this.CheckTargetDistanceFromHeldPosition(target, IID_Attack, this.GetBestAttackAgainst(target, true))
&& (this.GetStance().respondChaseBeyondVision || this.CheckTargetIsInVisionRange(target))
);
if (!target)
return false;
this.PushOrderFront("Attack", { "target": target, "force": false, "allowCapture": true });
return true;
};
/**
* Try to respond appropriately given our current stance,
* given a list of entities that match our stance's target criteria.
* Returns true if it responded.
*/
UnitAI.prototype.RespondToTargetedEntities = function(ents)
{
if (!ents.length)
return false;
if (this.GetStance().respondChase)
return this.AttackVisibleEntity(ents);
if (this.GetStance().respondStandGround)
return this.AttackVisibleEntity(ents);
if (this.GetStance().respondHoldGround)
return this.AttackEntityInZone(ents);
if (this.GetStance().respondFlee)
{
if (this.order && this.order.type == "Flee")
this.orderQueue.shift();
this.PushOrderFront("Flee", { "target": ents[0], "force": false });
return true;
}
return false;
};
/**
* @param {number} ents - An array of the IDs of the spotted entities.
* @return {boolean} - Whether we responded.
*/
UnitAI.prototype.RespondToSightedEntities = function(ents)
{
if (!ents || !ents.length)
return false;
if (this.GetStance().respondFleeOnSight)
{
this.Flee(ents[0], false);
return true;
}
return false;
};
/**
* Try to respond to healable entities.
* Returns true if it responded.
*/
UnitAI.prototype.RespondToHealableEntities = function(ents)
{
let ent = ents.find(ent => this.CanHeal(ent));
if (!ent)
return false;
this.PushOrderFront("Heal", { "target": ent, "force": false });
return true;
};
/**
* Returns true if we should stop following the target entity.
*/
UnitAI.prototype.ShouldAbandonChase = function(target, force, iid, type)
{
if (!this.CheckTargetVisible(target))
return true;
// Forced orders shouldn't be interrupted.
if (force)
return false;
// If we are guarding/escorting, don't abandon as long as the guarded unit is in target range of the attacker
if (this.isGuardOf)
{
let cmpUnitAI = Engine.QueryInterface(target, IID_UnitAI);
let cmpAttack = Engine.QueryInterface(target, IID_Attack);
if (cmpUnitAI && cmpAttack &&
cmpAttack.GetAttackTypes().some(type => cmpUnitAI.CheckTargetAttackRange(this.isGuardOf, type)))
return false;
}
if (this.GetStance().respondHoldGround)
if (!this.CheckTargetDistanceFromHeldPosition(target, iid, type))
return true;
// Stop if it's left our vision range, unless we're especially persistent.
if (!this.GetStance().respondChaseBeyondVision)
if (!this.CheckTargetIsInVisionRange(target))
return true;
return false;
};
/*
* Returns whether we should chase the targeted entity,
* given our current stance.
*/
UnitAI.prototype.ShouldChaseTargetedEntity = function(target, force)
{
if (!this.AbleToMove())
return false;
if (this.GetStance().respondChase)
return true;
// If we are guarding/escorting, chase at least as long as the guarded unit is in target range of the attacker
if (this.isGuardOf)
{
let cmpUnitAI = Engine.QueryInterface(target, IID_UnitAI);
let cmpAttack = Engine.QueryInterface(target, IID_Attack);
if (cmpUnitAI && cmpAttack &&
cmpAttack.GetAttackTypes().some(type => cmpUnitAI.CheckTargetAttackRange(this.isGuardOf, type)))
return true;
}
return force;
};
//// External interface functions ////
/**
* Order a unit to leave the formation it is in.
* Used to handle queued no-formation orders for units in formation.
*/
UnitAI.prototype.LeaveFormation = function(queued = true)
{
// If queued, add the order even if we're not in formation,
// maybe we will be later.
if (!queued && !this.IsFormationMember())
return;
if (queued)
this.AddOrder("LeaveFormation", { "force": true }, queued);
else
this.PushOrderFront("LeaveFormation", { "force": true });
};
UnitAI.prototype.SetFormationController = function(ent)
{
this.formationController = ent;
// Set obstruction group, so we can walk through members
// of our own formation (or ourself if not in formation)
var cmpObstruction = Engine.QueryInterface(this.entity, IID_Obstruction);
if (cmpObstruction)
{
if (ent == INVALID_ENTITY)
cmpObstruction.SetControlGroup(this.entity);
else
cmpObstruction.SetControlGroup(ent);
}
// If we were removed from a formation, let the FSM switch back to INDIVIDUAL
if (ent == INVALID_ENTITY)
this.UnitFsm.ProcessMessage(this, { "type": "FormationLeave" });
};
UnitAI.prototype.GetFormationController = function()
{
return this.formationController;
};
UnitAI.prototype.GetFormationTemplate = function()
{
return Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager).GetCurrentTemplateName(this.formationController) || NULL_FORMATION;
};
UnitAI.prototype.MoveIntoFormation = function(cmd)
{
var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation);
if (!cmpFormation)
return;
var cmpPosition = Engine.QueryInterface(this.entity, IID_Position);
if (!cmpPosition || !cmpPosition.IsInWorld())
return;
var pos = cmpPosition.GetPosition();
this.PushOrderFront("MoveIntoFormation", { "x": pos.x, "z": pos.z, "force": true });
};
UnitAI.prototype.GetTargetPositions = function()
{
var targetPositions = [];
for (var i = 0; i < this.orderQueue.length; ++i)
{
var order = this.orderQueue[i];
switch (order.type)
{
case "Walk":
case "WalkAndFight":
case "WalkToPointRange":
case "MoveIntoFormation":
case "GatherNearPosition":
case "Patrol":
targetPositions.push(new Vector2D(order.data.x, order.data.z));
break; // and continue the loop
case "WalkToTarget":
case "WalkToTargetRange": // This doesn't move to the target (just into range), but a later order will.
case "Guard":
case "Flee":
case "LeaveFoundation":
case "Attack":
case "Heal":
case "Gather":
case "ReturnResource":
case "Repair":
case "Garrison":
var cmpTargetPosition = Engine.QueryInterface(order.data.target, IID_Position);
if (!cmpTargetPosition || !cmpTargetPosition.IsInWorld())
return targetPositions;
targetPositions.push(cmpTargetPosition.GetPosition2D());
return targetPositions;
case "Stop":
return [];
default:
error("GetTargetPositions: Unrecognised order type '"+order.type+"'");
return [];
}
}
return targetPositions;
};
/**
* Returns the estimated distance that this unit will travel before either
* finishing all of its orders, or reaching a non-walk target (attack, gather, etc).
* Intended for Formation to switch to column layout on long walks.
*/
UnitAI.prototype.ComputeWalkingDistance = function()
{
var distance = 0;
var cmpPosition = Engine.QueryInterface(this.entity, IID_Position);
if (!cmpPosition || !cmpPosition.IsInWorld())
return 0;
// Keep track of the position at the start of each order
var pos = cmpPosition.GetPosition2D();
var targetPositions = this.GetTargetPositions();
for (var i = 0; i < targetPositions.length; ++i)
{
distance += pos.distanceTo(targetPositions[i]);
// Remember this as the start position for the next order
pos = targetPositions[i];
}
return distance;
};
UnitAI.prototype.AddOrder = function(type, data, queued)
{
if (this.expectedRoute)
this.expectedRoute = undefined;
if (queued)
this.PushOrder(type, data);
else
{
// May happen if an order arrives on the same turn the unit is garrisoned
// in that case, just forget the order as this will lead to an infinite loop
if (this.IsGarrisoned() && !this.IsTurret() && type != "Ungarrison")
return;
this.ReplaceOrder(type, data);
}
};
/**
* Adds guard/escort order to the queue, forced by the player.
*/
UnitAI.prototype.Guard = function(target, queued)
{
if (!this.CanGuard())
{
this.WalkToTarget(target, queued);
return;
}
if (target === this.entity)
return;
if (this.isGuardOf)
{
if (this.isGuardOf == target && this.order && this.order.type == "Guard")
return;
else
this.RemoveGuard();
}
this.AddOrder("Guard", { "target": target, "force": false }, queued);
};
/**
* @return {boolean} - Whether it makes sense to guard the given entity.
*/
UnitAI.prototype.ShouldGuard = function(target)
{
return this.TargetIsAlive(target) ||
Engine.QueryInterface(target, IID_Capturable) ||
Engine.QueryInterface(target, IID_StatusEffectsReceiver);
};
UnitAI.prototype.AddGuard = function(target)
{
if (!this.CanGuard())
return false;
var cmpGuard = Engine.QueryInterface(target, IID_Guard);
if (!cmpGuard)
return false;
this.isGuardOf = target;
this.guardRange = cmpGuard.GetRange(this.entity);
cmpGuard.AddGuard(this.entity);
return true;
};
UnitAI.prototype.RemoveGuard = function()
{
if (!this.isGuardOf)
return;
let cmpGuard = Engine.QueryInterface(this.isGuardOf, IID_Guard);
if (cmpGuard)
cmpGuard.RemoveGuard(this.entity);
this.guardRange = undefined;
this.isGuardOf = undefined;
if (!this.order)
return;
if (this.order.type == "Guard")
this.UnitFsm.ProcessMessage(this, { "type": "RemoveGuard" });
else
for (let i = 1; i < this.orderQueue.length; ++i)
if (this.orderQueue[i].type == "Guard")
this.orderQueue.splice(i, 1);
Engine.PostMessage(this.entity, MT_UnitAIOrderDataChanged, { "to": this.GetOrderData() });
};
UnitAI.prototype.IsGuardOf = function()
{
return this.isGuardOf;
};
UnitAI.prototype.SetGuardOf = function(entity)
{
// entity may be undefined
this.isGuardOf = entity;
};
UnitAI.prototype.CanGuard = function()
{
// Formation controllers should always respond to commands
// (then the individual units can make up their own minds)
if (this.IsFormationController())
return true;
return this.template.CanGuard == "true";
};
UnitAI.prototype.CanPatrol = function()
{
// Formation controllers should always respond to commands
// (then the individual units can make up their own minds)
return this.IsFormationController() || this.template.CanPatrol == "true";
};
/**
* Adds walk order to queue, forced by the player.
*/
UnitAI.prototype.Walk = function(x, z, queued)
{
if (this.expectedRoute && queued)
this.expectedRoute.push({ "x": x, "z": z });
else
this.AddOrder("Walk", { "x": x, "z": z, "force": true }, queued);
};
/**
* Adds walk to point range order to queue, forced by the player.
*/
UnitAI.prototype.WalkToPointRange = function(x, z, min, max, queued)
{
this.AddOrder("Walk", { "x": x, "z": z, "min": min, "max": max, "force": true }, queued);
};
/**
* Adds stop order to queue, forced by the player.
*/
UnitAI.prototype.Stop = function(queued)
{
this.AddOrder("Stop", { "force": true }, queued);
};
/**
* Adds walk-to-target order to queue, this only occurs in response
* to a player order, and so is forced.
*/
UnitAI.prototype.WalkToTarget = function(target, queued)
{
this.AddOrder("WalkToTarget", { "target": target, "force": true }, queued);
};
/**
* Adds walk-and-fight order to queue, this only occurs in response
* to a player order, and so is forced.
* If targetClasses is given, only entities matching the targetClasses can be attacked.
*/
UnitAI.prototype.WalkAndFight = function(x, z, targetClasses, allowCapture = true, queued = false)
{
this.AddOrder("WalkAndFight", { "x": x, "z": z, "targetClasses": targetClasses, "allowCapture": allowCapture, "force": true }, queued);
};
UnitAI.prototype.Patrol = function(x, z, targetClasses, allowCapture = true, queued = false)
{
if (!this.CanPatrol())
{
this.Walk(x, z, queued);
return;
}
this.AddOrder("Patrol", { "x": x, "z": z, "targetClasses": targetClasses, "allowCapture": allowCapture, "force": true }, queued);
};
/**
* Adds leave foundation order to queue, treated as forced.
*/
UnitAI.prototype.LeaveFoundation = function(target)
{
// If we're already being told to leave a foundation, then
// ignore this new request so we don't end up being too indecisive
// to ever actually move anywhere.
if (this.order && (this.order.type == "LeaveFoundation" || (this.order.type == "Flee" && this.order.data.target == target)))
return;
if (this.orderQueue.length && this.orderQueue[0].type == "Unpack" && this.WillMoveFromFoundation(target, false))
{
let cmpPack = Engine.QueryInterface(this.entity, IID_Pack);
if (cmpPack)
cmpPack.CancelPack();
}
if (this.IsPacking())
return;
this.PushOrderFront("LeaveFoundation", { "target": target, "force": true });
};
/**
* Adds attack order to the queue, forced by the player.
*/
UnitAI.prototype.Attack = function(target, allowCapture = true, queued = false)
{
if (!this.CanAttack(target))
{
// We don't want to let healers walk to the target unit so they can be easily killed.
// Instead we just let them get into healing range.
if (this.IsHealer())
this.MoveToTargetRange(target, IID_Heal);
else
this.WalkToTarget(target, queued);
return;
}
let order = {
"target": target,
"force": true,
"allowCapture": allowCapture,
};
this.RememberTargetPosition(order);
if (this.order && this.order.type == "Attack" &&
this.order.data &&
this.order.data.target === order.target &&
this.order.data.allowCapture === order.allowCapture)
{
this.order.data.lastPos = order.lastPos;
this.order.data.force = order.force;
return;
}
this.AddOrder("Attack", order, queued);
};
/**
* Adds garrison order to the queue, forced by the player.
*/
UnitAI.prototype.Garrison = function(target, queued)
{
if (target == this.entity)
return;
if (!this.CanGarrison(target))
{
this.WalkToTarget(target, queued);
return;
}
this.AddOrder("Garrison", { "target": target, "force": true }, queued);
};
/**
* Adds ungarrison order to the queue.
*/
UnitAI.prototype.Ungarrison = function()
{
if (this.IsGarrisoned())
{
this.SetImmobile(false);
this.AddOrder("Ungarrison", null, false);
}
};
/**
* Adds a garrison order for units that are already garrisoned in the garrison holder.
*/
UnitAI.prototype.Autogarrison = function(target)
{
this.isGarrisoned = true;
this.PushOrderFront("Garrison", { "target": target });
};
/**
* Adds gather order to the queue, forced by the player
* until the target is reached
*/
UnitAI.prototype.Gather = function(target, queued)
{
this.PerformGather(target, queued, true);
};
/**
* Internal function to abstract the force parameter.
*/
UnitAI.prototype.PerformGather = function(target, queued, force)
{
if (!this.CanGather(target))
{
this.WalkToTarget(target, queued);
return;
}
// Save the resource type now, so if the resource gets destroyed
// before we process the order then we still know what resource
// type to look for more of
var type;
var cmpResourceSupply = QueryMiragedInterface(target, IID_ResourceSupply);
if (cmpResourceSupply)
type = cmpResourceSupply.GetType();
else
error("CanGather allowed gathering from invalid entity");
// Also save the target entity's template, so that if it's an animal,
// we won't go from hunting slow safe animals to dangerous fast ones
var cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager);
var template = cmpTemplateManager.GetCurrentTemplateName(target);
if (template.indexOf("resource|") != -1)
template = template.slice(9);
let order = {
"target": target,
"type": type,
"template": template,
"force": force,
};
this.RememberTargetPosition(order);
order.initPos = order.lastPos;
if (this.order &&
(this.order.type == "Gather" || this.order.type == "Attack") &&
this.order.data &&
this.order.data.target === order.target)
{
this.order.data.lastPos = order.lastPos;
this.order.data.force = order.force;
return;
}
this.AddOrder("Gather", order, queued);
};
/**
* Adds gather-near-position order to the queue, not forced, so it can be
* interrupted by attacks.
*/
UnitAI.prototype.GatherNearPosition = function(x, z, type, template, queued)
{
if (template.indexOf("resource|") != -1)
template = template.slice(9);
if (this.IsFormationController() || Engine.QueryInterface(this.entity, IID_ResourceGatherer))
this.AddOrder("GatherNearPosition", { "type": type, "template": template, "x": x, "z": z, "force": false }, queued);
else
this.AddOrder("Walk", { "x": x, "z": z, "force": false }, queued);
};
/**
* Adds heal order to the queue, forced by the player.
*/
UnitAI.prototype.Heal = function(target, queued)
{
if (!this.CanHeal(target))
{
this.WalkToTarget(target, queued);
return;
}
if (this.order && this.order.type == "Heal" &&
this.order.data &&
this.order.data.target === target)
{
this.order.data.force = true;
return;
}
this.AddOrder("Heal", { "target": target, "force": true }, queued);
};
/**
* Adds return resource order to the queue, forced by the player.
*/
UnitAI.prototype.ReturnResource = function(target, queued)
{
if (!this.CanReturnResource(target, true))
{
this.WalkToTarget(target, queued);
return;
}
this.AddOrder("ReturnResource", { "target": target, "force": true }, queued);
};
UnitAI.prototype.CancelSetupTradeRoute = function(target)
{
let cmpTrader = Engine.QueryInterface(this.entity, IID_Trader);
if (!cmpTrader)
return;
cmpTrader.RemoveTargetMarket(target);
if (this.IsFormationController())
this.CallMemberFunction("CancelSetupTradeRoute", [target]);
};
/**
* Adds trade order to the queue. Either walk to the first market, or
* start a new route. Not forced, so it can be interrupted by attacks.
* The possible route may be given directly as a SetupTradeRoute argument
* if coming from a RallyPoint, or through this.expectedRoute if a user command.
*/
UnitAI.prototype.SetupTradeRoute = function(target, source, route, queued)
{
if (!this.CanTrade(target))
{
this.WalkToTarget(target, queued);
return;
}
// AI has currently no access to BackToWork
let cmpPlayer = QueryOwnerInterface(this.entity);
if (cmpPlayer && cmpPlayer.IsAI() && !this.IsFormationController() &&
this.workOrders.length && this.workOrders[0].type == "Trade")
{
let cmpTrader = Engine.QueryInterface(this.entity, IID_Trader);
if (cmpTrader.HasBothMarkets() &&
(cmpTrader.GetFirstMarket() == target && cmpTrader.GetSecondMarket() == source ||
cmpTrader.GetFirstMarket() == source && cmpTrader.GetSecondMarket() == target))
{
this.BackToWork();
return;
}
}
var marketsChanged = this.SetTargetMarket(target, source);
if (!marketsChanged)
return;
var cmpTrader = Engine.QueryInterface(this.entity, IID_Trader);
if (cmpTrader.HasBothMarkets())
{
let data = {
"target": cmpTrader.GetFirstMarket(),
"route": route,
"force": false
};
if (this.expectedRoute)
{
if (!route && this.expectedRoute.length)
data.route = this.expectedRoute.slice();
this.expectedRoute = undefined;
}
if (this.IsFormationController())
{
this.CallMemberFunction("AddOrder", ["Trade", data, queued]);
let cmpFormation = Engine.QueryInterface(this.entity, IID_Formation);
if (cmpFormation)
cmpFormation.Disband();
}
else
this.AddOrder("Trade", data, queued);
}
else
{
if (this.IsFormationController())
this.CallMemberFunction("WalkToTarget", [cmpTrader.GetFirstMarket(), queued]);
else
this.WalkToTarget(cmpTrader.GetFirstMarket(), queued);
this.expectedRoute = [];
}
};
UnitAI.prototype.SetTargetMarket = function(target, source)
{
var cmpTrader = Engine.QueryInterface(this.entity, IID_Trader);
if (!cmpTrader)
return false;
var marketsChanged = cmpTrader.SetTargetMarket(target, source);
if (this.IsFormationController())
this.CallMemberFunction("SetTargetMarket", [target, source]);
return marketsChanged;
};
UnitAI.prototype.SwitchMarketOrder = function(oldMarket, newMarket)
{
if (this.order && this.order.data && this.order.data.target && this.order.data.target == oldMarket)
this.order.data.target = newMarket;
};
UnitAI.prototype.MoveToMarket = function(targetMarket)
{
let nextTarget;
if (this.waypoints && this.waypoints.length >= 1)
nextTarget = this.waypoints.pop();
else
nextTarget = { "target": targetMarket };
this.order.data.nextTarget = nextTarget;
return this.MoveTo(this.order.data.nextTarget, IID_Trader);
};
UnitAI.prototype.PerformTradeAndMoveToNextMarket = function(currentMarket)
{
if (!this.CanTrade(currentMarket))
{
this.StopTrading();
return;
}
if (!this.CheckTargetRange(currentMarket, IID_Trader))
{
if (!this.MoveToMarket(currentMarket)) // If the current market is not reached try again
this.StopTrading();
return;
}
let cmpTrader = Engine.QueryInterface(this.entity, IID_Trader);
let nextMarket = cmpTrader.PerformTrade(currentMarket);
let amount = cmpTrader.GetGoods().amount;
if (!nextMarket || !amount || !amount.traderGain)
{
this.StopTrading();
return;
}
this.order.data.target = nextMarket;
if (this.order.data.route && this.order.data.route.length)
{
this.waypoints = this.order.data.route.slice();
if (this.order.data.target == cmpTrader.GetSecondMarket())
this.waypoints.reverse();
}
this.SetNextState("APPROACHINGMARKET");
};
UnitAI.prototype.MarketRemoved = function(market)
{
if (this.order && this.order.data && this.order.data.target && this.order.data.target == market)
this.UnitFsm.ProcessMessage(this, { "type": "TradingCanceled", "market": market });
};
UnitAI.prototype.StopTrading = function()
{
this.FinishOrder();
var cmpTrader = Engine.QueryInterface(this.entity, IID_Trader);
cmpTrader.StopTrading();
};
/**
* Adds repair/build order to the queue, forced by the player
* until the target is reached
*/
UnitAI.prototype.Repair = function(target, autocontinue, queued)
{
if (!this.CanRepair(target))
{
this.WalkToTarget(target, queued);
return;
}
if (this.order && this.order.type == "Repair" &&
this.order.data &&
this.order.data.target === target &&
this.order.data.autocontinue === autocontinue)
{
this.order.data.force = true;
return;
}
this.AddOrder("Repair", { "target": target, "autocontinue": autocontinue, "force": true }, queued);
};
/**
* Adds flee order to the queue, not forced, so it can be
* interrupted by attacks.
*/
UnitAI.prototype.Flee = function(target, queued)
{
this.AddOrder("Flee", { "target": target, "force": false }, queued);
};
UnitAI.prototype.Cheer = function()
{
this.PushOrderFront("Cheer", { "force": false });
};
UnitAI.prototype.Pack = function(queued)
{
if (this.CanPack())
this.AddOrder("Pack", { "force": true }, queued);
};
UnitAI.prototype.Unpack = function(queued)
{
if (this.CanUnpack())
this.AddOrder("Unpack", { "force": true }, queued);
};
UnitAI.prototype.CancelPack = function(queued)
{
var cmpPack = Engine.QueryInterface(this.entity, IID_Pack);
if (cmpPack && cmpPack.IsPacking() && !cmpPack.IsPacked())
this.AddOrder("CancelPack", { "force": true }, queued);
};
UnitAI.prototype.CancelUnpack = function(queued)
{
var cmpPack = Engine.QueryInterface(this.entity, IID_Pack);
if (cmpPack && cmpPack.IsPacking() && cmpPack.IsPacked())
this.AddOrder("CancelUnpack", { "force": true }, queued);
};
UnitAI.prototype.SetStance = function(stance)
{
if (g_Stances[stance])
{
this.stance = stance;
Engine.PostMessage(this.entity, MT_UnitStanceChanged, { "to": this.stance });
}
else
error("UnitAI: Setting to invalid stance '"+stance+"'");
};
UnitAI.prototype.SwitchToStance = function(stance)
{
var cmpPosition = Engine.QueryInterface(this.entity, IID_Position);
if (!cmpPosition || !cmpPosition.IsInWorld())
return;
var pos = cmpPosition.GetPosition();
this.SetHeldPosition(pos.x, pos.z);
this.SetStance(stance);
// Reset the range queries, since the range depends on stance.
this.SetupRangeQueries();
};
UnitAI.prototype.SetTurretStance = function()
{
this.previousStance = undefined;
if (this.GetStance().respondStandGround)
return;
for (let stance in g_Stances)
{
if (!g_Stances[stance].respondStandGround)
continue;
this.previousStance = this.GetStanceName();
this.SwitchToStance(stance);
return;
}
};
UnitAI.prototype.ResetTurretStance = function()
{
if (!this.previousStance)
return;
this.SwitchToStance(this.previousStance);
this.previousStance = undefined;
};
/**
* Resets the losRangeQuery.
* @return {boolean} - Whether there are targets in range that we ought to react upon.
*/
UnitAI.prototype.FindSightedEnemies = function()
{
if (!this.losRangeQuery)
return false;
let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
return this.RespondToSightedEntities(cmpRangeManager.ResetActiveQuery(this.losRangeQuery));
};
/**
* Resets losHealRangeQuery, and if there are some targets in range that we can heal
* then we start healing and this returns true; otherwise, returns false.
*/
UnitAI.prototype.FindNewHealTargets = function()
{
if (!this.losHealRangeQuery)
return false;
let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
return this.RespondToHealableEntities(cmpRangeManager.ResetActiveQuery(this.losHealRangeQuery));
};
/**
* Resets losAttackRangeQuery, and if there are some targets in range that we can
* attack then we start attacking and this returns true; otherwise, returns false.
*/
UnitAI.prototype.FindNewTargets = function()
{
if (!this.losAttackRangeQuery)
return false;
if (!this.GetStance().targetVisibleEnemies)
return false;
let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
return this.AttackEntitiesByPreference(cmpRangeManager.ResetActiveQuery(this.losAttackRangeQuery));
};
UnitAI.prototype.FindWalkAndFightTargets = function()
{
if (this.IsFormationController())
{
var cmpUnitAI;
var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation);
for (var ent of cmpFormation.members)
{
if (!(cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI)))
continue;
var targets = cmpUnitAI.GetTargetsFromUnit();
for (var targ of targets)
{
if (!cmpUnitAI.CanAttack(targ))
continue;
if (this.order.data.targetClasses)
{
var cmpIdentity = Engine.QueryInterface(targ, IID_Identity);
var targetClasses = this.order.data.targetClasses;
if (targetClasses.attack && cmpIdentity
&& !MatchesClassList(cmpIdentity.GetClassesList(), targetClasses.attack))
continue;
if (targetClasses.avoid && cmpIdentity
&& MatchesClassList(cmpIdentity.GetClassesList(), targetClasses.avoid))
continue;
// Only used by the AIs to prevent some choices of targets
if (targetClasses.vetoEntities && targetClasses.vetoEntities[targ])
continue;
}
this.PushOrderFront("Attack", { "target": targ, "force": false, "allowCapture": this.order.data.allowCapture });
return true;
}
}
return false;
}
var targets = this.GetTargetsFromUnit();
for (var targ of targets)
{
if (!this.CanAttack(targ))
continue;
if (this.order.data.targetClasses)
{
var cmpIdentity = Engine.QueryInterface(targ, IID_Identity);
var targetClasses = this.order.data.targetClasses;
if (cmpIdentity && targetClasses.attack
&& !MatchesClassList(cmpIdentity.GetClassesList(), targetClasses.attack))
continue;
if (cmpIdentity && targetClasses.avoid
&& MatchesClassList(cmpIdentity.GetClassesList(), targetClasses.avoid))
continue;
// Only used by the AIs to prevent some choices of targets
if (targetClasses.vetoEntities && targetClasses.vetoEntities[targ])
continue;
}
this.PushOrderFront("Attack", { "target": targ, "force": false, "allowCapture": this.order.data.allowCapture });
return true;
}
// healers on a walk-and-fight order should heal injured units
if (this.IsHealer())
return this.FindNewHealTargets();
return false;
};
UnitAI.prototype.GetTargetsFromUnit = function()
{
if (!this.losAttackRangeQuery)
return [];
if (!this.GetStance().targetVisibleEnemies)
return [];
let cmpAttack = Engine.QueryInterface(this.entity, IID_Attack);
if (!cmpAttack)
return [];
let attackfilter = function(e) {
if (!cmpAttack.CanAttack(e))
return false;
let cmpOwnership = Engine.QueryInterface(e, IID_Ownership);
if (cmpOwnership && cmpOwnership.GetOwner() > 0)
return true;
let cmpUnitAI = Engine.QueryInterface(e, IID_UnitAI);
return cmpUnitAI && (!cmpUnitAI.IsAnimal() || cmpUnitAI.IsDangerousAnimal());
};
let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
let entities = cmpRangeManager.ResetActiveQuery(this.losAttackRangeQuery);
let targets = entities.filter(attackfilter).sort(function(a, b) {
return cmpAttack.CompareEntitiesByPreference(a, b);
});
return targets;
};
UnitAI.prototype.GetQueryRange = function(iid)
{
let ret = { "min": 0, "max": 0 };
let cmpVision = Engine.QueryInterface(this.entity, IID_Vision);
if (!cmpVision)
return ret;
let visionRange = cmpVision.GetRange();
if (iid === IID_Vision)
{
ret.max = visionRange;
return ret;
}
if (this.GetStance().respondStandGround)
{
let range = this.GetRange(iid);
if (!range)
return ret;
ret.min = range.min;
ret.max = Math.min(range.max, visionRange);
}
else if (this.GetStance().respondChase)
ret.max = visionRange;
else if (this.GetStance().respondHoldGround)
{
let range = this.GetRange(iid);
if (!range)
return ret;
ret.max = Math.min(range.max + visionRange / 2, visionRange);
}
// We probably have stance 'passive' and we wouldn't have a range,
// but as it is the default for healers we need to set it to something sane.
else if (iid === IID_Heal)
ret.max = visionRange;
return ret;
};
UnitAI.prototype.GetStance = function()
{
return g_Stances[this.stance];
};
UnitAI.prototype.GetSelectableStances = function()
{
if (this.IsTurret())
return [];
return Object.keys(g_Stances).filter(key => g_Stances[key].selectable);
};
UnitAI.prototype.GetStanceName = function()
{
return this.stance;
};
/*
* Make the unit walk at its normal pace.
*/
UnitAI.prototype.ResetSpeedMultiplier = function()
{
let cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion);
if (cmpUnitMotion)
cmpUnitMotion.SetSpeedMultiplier(1);
};
UnitAI.prototype.SetSpeedMultiplier = function(speed)
{
let cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion);
if (cmpUnitMotion)
cmpUnitMotion.SetSpeedMultiplier(speed);
};
/**
* Try to match the targets current movement speed.
*
* @param {number} target - The entity ID of the target to match.
* @param {boolean} mayRun - Whether the entity is allowed to run to match the speed.
*/
UnitAI.prototype.TryMatchTargetSpeed = function(target, mayRun = true)
{
let cmpUnitMotionTarget = Engine.QueryInterface(target, IID_UnitMotion);
if (cmpUnitMotionTarget)
{
let targetSpeed = cmpUnitMotionTarget.GetCurrentSpeed();
if (targetSpeed)
this.SetSpeedMultiplier(Math.min(mayRun ? this.GetRunMultiplier() : 1, targetSpeed / this.GetWalkSpeed()));
}
};
/*
* Remember the position of the target (in lastPos), if any, in case it disappears later
* and we want to head to its last known position.
* @param orderData - The order data to set this on. Defaults to this.order.data
*/
UnitAI.prototype.RememberTargetPosition = function(orderData)
{
if (!orderData)
orderData = this.order.data;
let cmpPosition = Engine.QueryInterface(orderData.target, IID_Position);
if (cmpPosition && cmpPosition.IsInWorld())
orderData.lastPos = cmpPosition.GetPosition();
};
UnitAI.prototype.SetHeldPosition = function(x, z)
{
this.heldPosition = {"x": x, "z": z};
};
UnitAI.prototype.SetHeldPositionOnEntity = function(entity)
{
var cmpPosition = Engine.QueryInterface(this.entity, IID_Position);
if (!cmpPosition || !cmpPosition.IsInWorld())
return;
var pos = cmpPosition.GetPosition();
this.SetHeldPosition(pos.x, pos.z);
};
UnitAI.prototype.GetHeldPosition = function()
{
return this.heldPosition;
};
UnitAI.prototype.WalkToHeldPosition = function()
{
if (this.heldPosition)
{
this.AddOrder("Walk", { "x": this.heldPosition.x, "z": this.heldPosition.z, "force": false }, false);
return true;
}
return false;
};
//// Helper functions ////
/**
* General getter for ranges.
*
* @param {number} iid
* @param {string} type - [Optional]
* @return {Object | undefined} - The range in the form
* { "min": number, "max": number }
* Object."elevationBonus": number may be present when iid == IID_Attack.
* Returns undefined when the entity does not have the requested component.
*/
UnitAI.prototype.GetRange = function(iid, type)
{
let component = Engine.QueryInterface(this.entity, iid);
if (!component)
return undefined;
return component.GetRange(type);
}
UnitAI.prototype.CanAttack = function(target)
{
// Formation controllers should always respond to commands
// (then the individual units can make up their own minds)
if (this.IsFormationController())
return true;
let cmpAttack = Engine.QueryInterface(this.entity, IID_Attack);
return cmpAttack && cmpAttack.CanAttack(target);
};
UnitAI.prototype.CanGarrison = function(target)
{
// Formation controllers should always respond to commands
// (then the individual units can make up their own minds)
if (this.IsFormationController())
return true;
let cmpGarrisonHolder = Engine.QueryInterface(target, IID_GarrisonHolder);
if (!cmpGarrisonHolder)
return false;
let cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership);
if (!cmpOwnership || !(IsOwnedByPlayer(cmpOwnership.GetOwner(), target) || IsOwnedByMutualAllyOfPlayer(cmpOwnership.GetOwner(), target)))
return false;
return true;
};
UnitAI.prototype.CanGather = function(target)
{
if (this.IsTurret())
return false;
var cmpResourceSupply = QueryMiragedInterface(target, IID_ResourceSupply);
if (!cmpResourceSupply)
return false;
// Formation controllers should always respond to commands
// (then the individual units can make up their own minds)
if (this.IsFormationController())
return true;
var cmpResourceGatherer = Engine.QueryInterface(this.entity, IID_ResourceGatherer);
if (!cmpResourceGatherer)
return false;
if (!cmpResourceGatherer.GetTargetGatherRate(target))
return false;
// No need to verify ownership as we should be able to gather from
// a target regardless of ownership.
// No need to call "cmpResourceSupply.IsAvailable()" either because that
// would cause units to walk to full entities instead of choosing another one
// nearby to gather from, which is undesirable.
return true;
};
UnitAI.prototype.CanHeal = function(target)
{
// Formation controllers should always respond to commands
// (then the individual units can make up their own minds)
if (this.IsFormationController())
return true;
let cmpHeal = Engine.QueryInterface(this.entity, IID_Heal);
return cmpHeal && cmpHeal.CanHeal(target);
};
/**
* Check if the entity can return carried resources at @param target
* @param checkCarriedResource check we are carrying resources
* @param cmpResourceGatherer if present, use this directly instead of re-querying.
*/
UnitAI.prototype.CanReturnResource = function(target, checkCarriedResource, cmpResourceGatherer = undefined)
{
if (this.IsTurret())
return false;
// Formation controllers should always respond to commands
// (then the individual units can make up their own minds)
if (this.IsFormationController())
return true;
if (!cmpResourceGatherer)
{
cmpResourceGatherer = Engine.QueryInterface(this.entity, IID_ResourceGatherer);
if (!cmpResourceGatherer)
return false;
}
let cmpResourceDropsite = Engine.QueryInterface(target, IID_ResourceDropsite);
if (!cmpResourceDropsite)
return false;
if (checkCarriedResource)
{
let type = cmpResourceGatherer.GetMainCarryingType();
if (!type || !cmpResourceDropsite.AcceptsType(type))
return false;
}
let cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership);
if (cmpOwnership && IsOwnedByPlayer(cmpOwnership.GetOwner(), target))
return true;
let cmpPlayer = QueryOwnerInterface(this.entity);
return cmpPlayer && cmpPlayer.HasSharedDropsites() && cmpResourceDropsite.IsShared() &&
cmpOwnership && IsOwnedByMutualAllyOfPlayer(cmpOwnership.GetOwner(), target);
};
UnitAI.prototype.CanTrade = function(target)
{
if (this.IsTurret())
return false;
// Formation controllers should always respond to commands
// (then the individual units can make up their own minds)
if (this.IsFormationController())
return true;
var cmpTrader = Engine.QueryInterface(this.entity, IID_Trader);
return cmpTrader && cmpTrader.CanTrade(target);
};
UnitAI.prototype.CanRepair = function(target)
{
if (this.IsTurret())
return false;
// Formation controllers should always respond to commands
// (then the individual units can make up their own minds)
if (this.IsFormationController())
return true;
// Verify that we're able to respond to Repair (Builder) commands
var cmpBuilder = Engine.QueryInterface(this.entity, IID_Builder);
if (!cmpBuilder)
return false;
var cmpFoundation = QueryMiragedInterface(target, IID_Foundation);
var cmpRepairable = Engine.QueryInterface(target, IID_Repairable);
if (!cmpFoundation && !cmpRepairable)
return false;
var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership);
return cmpOwnership && IsOwnedByAllyOfPlayer(cmpOwnership.GetOwner(), target);
};
UnitAI.prototype.CanPack = function()
{
var cmpPack = Engine.QueryInterface(this.entity, IID_Pack);
return cmpPack && !cmpPack.IsPacking() && !cmpPack.IsPacked();
};
UnitAI.prototype.CanUnpack = function()
{
var cmpPack = Engine.QueryInterface(this.entity, IID_Pack);
return cmpPack && !cmpPack.IsPacking() && cmpPack.IsPacked();
};
UnitAI.prototype.IsPacking = function()
{
var cmpPack = Engine.QueryInterface(this.entity, IID_Pack);
return cmpPack && cmpPack.IsPacking();
};
//// Formation specific functions ////
UnitAI.prototype.IsAttackingAsFormation = function()
{
var cmpAttack = Engine.QueryInterface(this.entity, IID_Attack);
return cmpAttack && cmpAttack.CanAttackAsFormation()
&& this.GetCurrentState() == "FORMATIONCONTROLLER.COMBAT.ATTACKING";
};
UnitAI.prototype.MoveRandomly = function(distance)
{
// To minimize drift all across the map, describe circles
// approximated by polygons.
// And to avoid getting stuck in obstacles or narrow spaces, each side
// of the polygon is obtained by trying to go away from a point situated
// half a meter backwards of the current position, after rotation.
// We also add a fluctuation on the length of each side of the polygon (dist)
// which, in addition to making the move more random, helps escaping narrow spaces
// with bigger values of dist.
let cmpPosition = Engine.QueryInterface(this.entity, IID_Position);
let cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion);
if (!cmpPosition || !cmpPosition.IsInWorld() || !cmpUnitMotion)
return;
let pos = cmpPosition.GetPosition();
let ang = cmpPosition.GetRotation().y;
if (!this.roamAngle)
{
this.roamAngle = (randBool() ? 1 : -1) * Math.PI / 6;
ang -= this.roamAngle / 2;
this.startAngle = ang;
}
else if (Math.abs((ang - this.startAngle + Math.PI) % (2 * Math.PI) - Math.PI) < Math.abs(this.roamAngle / 2))
this.roamAngle *= randBool() ? 1 : -1;
let halfDelta = randFloat(this.roamAngle / 4, this.roamAngle * 3 / 4);
// First half rotation to decrease the impression of immediate rotation
ang += halfDelta;
cmpUnitMotion.FaceTowardsPoint(pos.x + 0.5 * Math.sin(ang), pos.z + 0.5 * Math.cos(ang));
// Then second half of the rotation
ang += halfDelta;
let dist = randFloat(0.5, 1.5) * distance;
cmpUnitMotion.MoveToPointRange(pos.x - 0.5 * Math.sin(ang), pos.z - 0.5 * Math.cos(ang), dist, -1);
};
UnitAI.prototype.SetFacePointAfterMove = function(val)
{
var cmpMotion = Engine.QueryInterface(this.entity, IID_UnitMotion);
if (cmpMotion)
cmpMotion.SetFacePointAfterMove(val);
};
UnitAI.prototype.GetFacePointAfterMove = function()
{
let cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion);
return cmpUnitMotion && cmpUnitMotion.GetFacePointAfterMove();
}
UnitAI.prototype.AttackEntitiesByPreference = function(ents)
{
if (!ents.length)
return false;
let cmpAttack = Engine.QueryInterface(this.entity, IID_Attack);
if (!cmpAttack)
return false;
let attackfilter = function(e) {
if (!cmpAttack.CanAttack(e))
return false;
let cmpOwnership = Engine.QueryInterface(e, IID_Ownership);
if (cmpOwnership && cmpOwnership.GetOwner() > 0)
return true;
let cmpUnitAI = Engine.QueryInterface(e, IID_UnitAI);
return cmpUnitAI && (!cmpUnitAI.IsAnimal() || cmpUnitAI.IsDangerousAnimal());
};
let entsByPreferences = {};
let preferences = [];
let entsWithoutPref = [];
for (let ent of ents)
{
if (!attackfilter(ent))
continue;
let pref = cmpAttack.GetPreference(ent);
if (pref === null || pref === undefined)
entsWithoutPref.push(ent);
else if (!entsByPreferences[pref])
{
preferences.push(pref);
entsByPreferences[pref] = [ent];
}
else
entsByPreferences[pref].push(ent);
}
if (preferences.length)
{
preferences.sort((a, b) => a - b);
for (let pref of preferences)
if (this.RespondToTargetedEntities(entsByPreferences[pref]))
return true;
}
return this.RespondToTargetedEntities(entsWithoutPref);
};
/**
* Call UnitAI.funcname(args) on all formation members.
* @param resetWaitingEntities - If true, call ResetWaitingEntities first.
* If the controller wants to wait on its members to finish their order,
* this needs to be reset before sending new orders (in case they instafail)
* so it makes sense to do it here.
* Only set this to false if you're sure it's safe.
*/
UnitAI.prototype.CallMemberFunction = function(funcname, args, resetWaitingEntities = true)
{
var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation);
if (!cmpFormation)
return;
if (resetWaitingEntities)
cmpFormation.ResetWaitingEntities();
cmpFormation.GetMembers().forEach(ent => {
let cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
cmpUnitAI[funcname].apply(cmpUnitAI, args);
});
};
/**
* Call obj.funcname(args) on UnitAI components owned by player in given range.
*/
UnitAI.prototype.CallPlayerOwnedEntitiesFunctionInRange = function(funcname, args, range)
{
let cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership);
if (!cmpOwnership)
return;
let owner = cmpOwnership.GetOwner();
if (owner == INVALID_PLAYER)
return;
let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
let nearby = cmpRangeManager.ExecuteQuery(this.entity, 0, range, [owner], IID_UnitAI, true);
for (let i = 0; i < nearby.length; ++i)
{
let cmpUnitAI = Engine.QueryInterface(nearby[i], IID_UnitAI);
cmpUnitAI[funcname].apply(cmpUnitAI, args);
}
};
/**
* Call obj.functname(args) on UnitAI components of all formation members,
* and return true if all calls return true.
*/
UnitAI.prototype.TestAllMemberFunction = function(funcname, args)
{
let cmpFormation = Engine.QueryInterface(this.entity, IID_Formation);
return cmpFormation && cmpFormation.GetMembers().every(ent => {
let cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
return cmpUnitAI[funcname].apply(cmpUnitAI, args);
});
};
UnitAI.prototype.UnitFsm = new FSM(UnitAI.prototype.UnitFsmSpec);
Engine.RegisterComponentType(IID_UnitAI, "UnitAI", UnitAI);
Index: ps/trunk/binaries/data/mods/public/simulation/components/tests/test_ResourceSupply.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/components/tests/test_ResourceSupply.js (revision 24962)
+++ ps/trunk/binaries/data/mods/public/simulation/components/tests/test_ResourceSupply.js (revision 24963)
@@ -1,74 +1,748 @@
Resources = {
"BuildChoicesSchema": () => {
let schema = "";
for (let res of ["food", "metal"])
{
for (let subtype in ["meat", "grain"])
schema += "" + res + "." + subtype + "";
schema += " treasure." + res + "";
}
return "" + schema + "";
}
};
+Engine.LoadHelperScript("ValueModification.js");
+Engine.LoadComponentScript("interfaces/Health.js");
+Engine.LoadComponentScript("interfaces/ModifiersManager.js");
Engine.LoadComponentScript("interfaces/ResourceSupply.js");
+Engine.LoadComponentScript("interfaces/Timer.js");
Engine.LoadComponentScript("ResourceSupply.js");
+Engine.LoadComponentScript("Timer.js");
-const entity = 60;
+let entity = 60;
AddMock(SYSTEM_ENTITY, IID_PlayerManager, {
"GetNumPlayers": () => 3
});
AddMock(entity, IID_Fogging, {
"Activate": () => {}
});
let template = {
- "Amount": 1000,
+ "Max": "1001",
+ "Initial": "1000",
"Type": "food.meat",
- "KillBeforeGather": false,
- "MaxGatherers": 2
+ "KillBeforeGather": "false",
+ "MaxGatherers": "2"
};
let cmpResourceSupply = ConstructComponent(entity, "ResourceSupply", template);
+cmpResourceSupply.OnOwnershipChanged({ "to": 1 });
TS_ASSERT(!cmpResourceSupply.IsInfinite());
TS_ASSERT(!cmpResourceSupply.GetKillBeforeGather());
-TS_ASSERT_EQUALS(cmpResourceSupply.GetMaxAmount(), 1000);
+TS_ASSERT_EQUALS(cmpResourceSupply.GetMaxAmount(), 1001);
TS_ASSERT_EQUALS(cmpResourceSupply.GetMaxGatherers(), 2);
TS_ASSERT_EQUALS(cmpResourceSupply.GetDiminishingReturns(), null);
TS_ASSERT_EQUALS(cmpResourceSupply.GetNumGatherers(), 0);
TS_ASSERT(cmpResourceSupply.IsAvailableTo(70));
TS_ASSERT(cmpResourceSupply.AddGatherer(70));
TS_ASSERT_EQUALS(cmpResourceSupply.GetNumGatherers(), 1);
TS_ASSERT(cmpResourceSupply.AddGatherer(71));
TS_ASSERT_EQUALS(cmpResourceSupply.GetNumGatherers(), 2);
TS_ASSERT(!cmpResourceSupply.AddGatherer(72));
TS_ASSERT_EQUALS(cmpResourceSupply.GetNumGatherers(), 2);
TS_ASSERT(cmpResourceSupply.IsAvailableTo(70));
TS_ASSERT(!cmpResourceSupply.IsAvailableTo(73));
TS_ASSERT(!cmpResourceSupply.AddGatherer(73));
TS_ASSERT_EQUALS(cmpResourceSupply.GetNumGatherers(), 2);
cmpResourceSupply.RemoveGatherer(70);
TS_ASSERT_EQUALS(cmpResourceSupply.GetNumGatherers(), 1);
+TS_ASSERT(cmpResourceSupply.AddActiveGatherer(70));
+TS_ASSERT_EQUALS(cmpResourceSupply.GetNumGatherers(), 2);
+
+cmpResourceSupply.RemoveGatherer(70);
+TS_ASSERT_EQUALS(cmpResourceSupply.GetNumGatherers(), 1);
+
+TS_ASSERT(cmpResourceSupply.AddActiveGatherer(70));
+TS_ASSERT_EQUALS(cmpResourceSupply.GetNumGatherers(), 2);
+
+cmpResourceSupply.RemoveGatherer(70);
+TS_ASSERT_EQUALS(cmpResourceSupply.GetNumGatherers(), 1);
+
TS_ASSERT_UNEVAL_EQUALS(cmpResourceSupply.GetCurrentAmount(), 1000);
TS_ASSERT_UNEVAL_EQUALS(cmpResourceSupply.TakeResources(300), { "amount": 300, "exhausted": false });
TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 700);
TS_ASSERT(cmpResourceSupply.IsAvailableTo(70));
TS_ASSERT_UNEVAL_EQUALS(cmpResourceSupply.TakeResources(800), { "amount": 700, "exhausted": true });
TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 0);
// The resource is not available when exhausted
TS_ASSERT(!cmpResourceSupply.IsAvailableTo(70));
+
+cmpResourceSupply.RemoveGatherer(71);
+TS_ASSERT_EQUALS(cmpResourceSupply.GetNumGatherers(), 0);
+
+
+// Test Changes.
+
+let cmpTimer;
+function reset(newTemplate)
+{
+ cmpTimer = ConstructComponent(SYSTEM_ENTITY, "Timer");
+ cmpResourceSupply = ConstructComponent(entity, "ResourceSupply", newTemplate);
+ cmpResourceSupply.OnOwnershipChanged({ "to": 1 });
+}
+
+// Decay.
+template = {
+ "Max": "1000",
+ "Type": "food.meat",
+ "KillBeforeGather": "false",
+ "Change": {
+ "Rotting": {
+ "Value": "-1",
+ "Interval": "1000"
+ }
+ },
+ "MaxGatherers": "2"
+};
+reset(template);
+TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 1000);
+cmpTimer.OnUpdate({ "turnLength": 1 });
+TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 999);
+cmpTimer.OnUpdate({ "turnLength": 5 });
+TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 994);
+
+// Decay with minimum.
+template = {
+ "Max": "1000",
+ "Type": "food.meat",
+ "KillBeforeGather": "false",
+ "Change": {
+ "Rotting": {
+ "Value": "-1",
+ "Interval": "1000",
+ "LowerLimit": "997"
+ }
+ },
+ "MaxGatherers": "2"
+};
+reset(template);
+TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 1000);
+cmpTimer.OnUpdate({ "turnLength": 3 });
+TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 997);
+cmpTimer.OnUpdate({ "turnLength": 1 });
+TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 996);
+cmpTimer.OnUpdate({ "turnLength": 1 });
+TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 996);
+
+// Decay with maximum.
+template = {
+ "Max": "1000",
+ "Type": "food.meat",
+ "KillBeforeGather": "false",
+ "Change": {
+ "Rotting": {
+ "Value": "-1",
+ "Interval": "1000",
+ "UpperLimit": "995"
+ }
+ },
+ "MaxGatherers": "2"
+};
+reset(template);
+TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 1000);
+cmpTimer.OnUpdate({ "turnLength": 1 });
+TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 1000);
+
+// Decay with minimum and maximum.
+template = {
+ "Max": "1000",
+ "Type": "food.meat",
+ "KillBeforeGather": "false",
+ "Change": {
+ "Rotting": {
+ "Value": "-1",
+ "Interval": "1000",
+ "UpperLimit": "995",
+ "LowerLimit": "990"
+ }
+ },
+ "MaxGatherers": "2"
+};
+reset(template);
+TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 1000);
+cmpTimer.OnUpdate({ "turnLength": 1 });
+TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 1000);
+cmpResourceSupply.TakeResources(6);
+TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 994);
+cmpTimer.OnUpdate({ "turnLength": 10 });
+TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 989);
+
+// Growth.
+template = {
+ "Initial": "995",
+ "Max": "1000",
+ "Type": "food.meat",
+ "KillBeforeGather": "false",
+ "Change": {
+ "Growth": {
+ "Value": "1",
+ "Interval": "1000"
+ }
+ },
+ "MaxGatherers": "2"
+};
+reset(template);
+TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 995);
+cmpTimer.OnUpdate({ "turnLength": 1 });
+TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 996);
+cmpTimer.OnUpdate({ "turnLength": 5 });
+TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 1000);
+
+// Growth with minimum.
+template = {
+ "Initial": "995",
+ "Max": "1000",
+ "Type": "food.meat",
+ "KillBeforeGather": "false",
+ "Change": {
+ "Growth": {
+ "Value": "1",
+ "Interval": "1000",
+ "LowerLimit": "997"
+ }
+ },
+ "MaxGatherers": "2"
+};
+reset(template);
+TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 995);
+cmpTimer.OnUpdate({ "turnLength": 1 });
+TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 995);
+
+// Growth with maximum.
+template = {
+ "Initial": "994",
+ "Max": "1000",
+ "Type": "food.meat",
+ "KillBeforeGather": "false",
+ "Change": {
+ "Growth": {
+ "Value": "1",
+ "Interval": "1000",
+ "UpperLimit": 995
+ }
+ },
+ "MaxGatherers": "2"
+};
+reset(template);
+TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 994);
+cmpTimer.OnUpdate({ "turnLength": 1 });
+TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 995);
+cmpTimer.OnUpdate({ "turnLength": 1 });
+TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 996);
+cmpTimer.OnUpdate({ "turnLength": 1 });
+TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 996);
+
+// Growth with minimum and maximum.
+template = {
+ "Initial": "990",
+ "Max": "1000",
+ "Type": "food.meat",
+ "KillBeforeGather": "false",
+ "Change": {
+ "Growth": {
+ "Value": "1",
+ "Interval": "1000",
+ "UpperLimit": "995",
+ "LowerLimit": "990"
+ }
+ },
+ "MaxGatherers": "2"
+};
+reset(template);
+TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 990);
+cmpTimer.OnUpdate({ "turnLength": 1 });
+TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 991);
+cmpTimer.OnUpdate({ "turnLength": 8 });
+TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 996);
+
+// Growth when resources are taken again.
+template = {
+ "Initial": "995",
+ "Max": "1000",
+ "Type": "food.meat",
+ "KillBeforeGather": "false",
+ "Change": {
+ "Growth": {
+ "Value": "1",
+ "Interval": "1000"
+ }
+ },
+ "MaxGatherers": "2"
+};
+reset(template);
+TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 995);
+cmpTimer.OnUpdate({ "turnLength": 1 });
+TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 996);
+cmpResourceSupply.TakeResources(6);
+TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 990);
+cmpTimer.OnUpdate({ "turnLength": 5 });
+TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 995);
+
+// Decay when dead.
+template = {
+ "Max": "10",
+ "Type": "food.meat",
+ "KillBeforeGather": "false",
+ "Change": {
+ "Rotting": {
+ "Value": "-1",
+ "Interval": "1000",
+ "State": "dead"
+ }
+ },
+ "MaxGatherers": "2"
+};
+reset(template);
+TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 10);
+cmpTimer.OnUpdate({ "turnLength": 1 });
+TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 9);
+
+// No growth when dead.
+template = {
+ "Max": "10",
+ "Initial": "5",
+ "Type": "food.meat",
+ "KillBeforeGather": "false",
+ "Change": {
+ "Growth": {
+ "Value": "1",
+ "Interval": "1000",
+ "State": "alive"
+ }
+ },
+ "MaxGatherers": "2"
+};
+reset(template);
+
+TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 5);
+cmpTimer.OnUpdate({ "turnLength": 1 });
+TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 5);
+
+// Decay when dead or alive.
+template = {
+ "Max": "10",
+ "Type": "food.meat",
+ "KillBeforeGather": "false",
+ "Change": {
+ "Rotting": {
+ "Value": "-1",
+ "Interval": "1000",
+ "State": "dead alive"
+ }
+ },
+ "MaxGatherers": "2"
+};
+reset(template);
+TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 10);
+cmpTimer.OnUpdate({ "turnLength": 1 });
+TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 9);
+
+AddMock(entity, IID_Health, {}); // Bring the entity to life.
+
+cmpTimer.OnUpdate({ "turnLength": 1 });
+TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 8);
+
+// No decay when alive.
+template = {
+ "Max": "10",
+ "Type": "food.meat",
+ "KillBeforeGather": "false",
+ "Change": {
+ "Rotting": {
+ "Value": "-1",
+ "Interval": "1000",
+ "State": "dead"
+ }
+ },
+ "MaxGatherers": "2"
+};
+reset(template);
+
+TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 10);
+cmpTimer.OnUpdate({ "turnLength": 1 });
+TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 10);
+
+// Growth when alive.
+template = {
+ "Max": "10",
+ "Initial": "5",
+ "Type": "food.meat",
+ "KillBeforeGather": "false",
+ "Change": {
+ "Growth": {
+ "Value": "1",
+ "Interval": "1000",
+ "State": "alive"
+ }
+ },
+ "MaxGatherers": "2"
+};
+reset(template);
+
+TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 5);
+cmpTimer.OnUpdate({ "turnLength": 1 });
+TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 6);
+
+// Growth when dead or alive.
+template = {
+ "Max": "10",
+ "Initial": "5",
+ "Type": "food.meat",
+ "KillBeforeGather": "false",
+ "Change": {
+ "Growth": {
+ "Value": "1",
+ "Interval": "1000",
+ "State": "dead alive"
+ }
+ },
+ "MaxGatherers": "2"
+};
+reset(template);
+TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 5);
+cmpTimer.OnUpdate({ "turnLength": 1 });
+TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 6);
+
+DeleteMock(entity, IID_Health); // "Kill" the entity.
+
+cmpTimer.OnUpdate({ "turnLength": 1 });
+TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 7);
+
+// Decay *and* growth.
+template = {
+ "Max": "10",
+ "Type": "food.meat",
+ "KillBeforeGather": "false",
+ "Change": {
+ "Rotting": {
+ "Value": "-1",
+ "Interval": "1000"
+ },
+ "Growth": {
+ "Value": "1",
+ "Interval": "1000"
+ }
+ },
+ "MaxGatherers": "2"
+};
+reset(template);
+TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 10);
+cmpTimer.OnUpdate({ "turnLength": 1 });
+TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 10);
+
+// Decay *and* growth with different health states.
+template = {
+ "Max": "10",
+ "Initial": "5",
+ "Type": "food.meat",
+ "KillBeforeGather": "false",
+ "Change": {
+ "Rotting": {
+ "Value": "-1",
+ "Interval": "1000",
+ "State": "dead"
+ },
+ "Growth": {
+ "Value": "1",
+ "Interval": "1000",
+ "State": "alive"
+ }
+ },
+ "MaxGatherers": "2"
+};
+AddMock(entity, IID_Health, { }); // Bring the entity to life.
+reset(template);
+TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 5);
+cmpTimer.OnUpdate({ "turnLength": 1 });
+TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 6);
+DeleteMock(entity, IID_Health); // "Kill" the entity.
+// We overshoot one due to lateness.
+cmpTimer.OnUpdate({ "turnLength": 1 });
+TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 7);
+
+cmpTimer.OnUpdate({ "turnLength": 1 });
+TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 6);
+cmpTimer.OnUpdate({ "turnLength": 1 });
+TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 5);
+
+// Two effects with different limits.
+template = {
+ "Max": "20",
+ "Initial": "5",
+ "Type": "food.meat",
+ "KillBeforeGather": "false",
+ "Change": {
+ "SuperGrowth": {
+ "Value": "2",
+ "Interval": "1000",
+ "UpperLimit": "8"
+ },
+ "Growth": {
+ "Value": "1",
+ "Interval": "1000",
+ "UpperLimit": "12"
+ }
+ },
+ "MaxGatherers": "2"
+};
+reset(template);
+TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 5);
+cmpTimer.OnUpdate({ "turnLength": 1 });
+TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 8);
+cmpTimer.OnUpdate({ "turnLength": 1 });
+TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 11);
+cmpTimer.OnUpdate({ "turnLength": 1 });
+TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 12);
+cmpTimer.OnUpdate({ "turnLength": 1 });
+TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 13);
+cmpTimer.OnUpdate({ "turnLength": 1 });
+TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 13);
+
+// Two effects with different limits.
+// This in an interesting case, where the order of the changes matters.
+template = {
+ "Max": "20",
+ "Initial": "5",
+ "Type": "food.meat",
+ "KillBeforeGather": "false",
+ "Change": {
+ "Growth": {
+ "Value": "1",
+ "Interval": "1000",
+ "UpperLimit": "12"
+ },
+ "SuperGrowth": {
+ "Value": "2",
+ "Interval": "1000",
+ "UpperLimit": "8"
+ }
+ },
+ "MaxGatherers": "2"
+};
+reset(template);
+TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 5);
+cmpTimer.OnUpdate({ "turnLength": 1 });
+TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 8);
+cmpTimer.OnUpdate({ "turnLength": 1 });
+TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 9);
+cmpTimer.OnUpdate({ "turnLength": 5 });
+TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 13);
+
+// Infinity with growth.
+template = {
+ "Max": "Infinity",
+ "Type": "food.meat",
+ "KillBeforeGather": "false",
+ "Change": {
+ "Growth": {
+ "Value": "1",
+ "Interval": "1000"
+ }
+ },
+ "MaxGatherers": "2"
+};
+reset(template);
+TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), Infinity);
+cmpTimer.OnUpdate({ "turnLength": 1 });
+TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), Infinity);
+
+// Infinity with decay.
+template = {
+ "Max": "Infinity",
+ "Type": "food.meat",
+ "KillBeforeGather": "false",
+ "Change": {
+ "Decay": {
+ "Value": "-1",
+ "Interval": "1000"
+ }
+ },
+ "MaxGatherers": "2"
+};
+reset(template);
+TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), Infinity);
+cmpTimer.OnUpdate({ "turnLength": 1 });
+TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), Infinity);
+
+// Decay when not gathered.
+template = {
+ "Max": "10",
+ "Type": "food.meat",
+ "KillBeforeGather": "false",
+ "Change": {
+ "Decay": {
+ "Value": "-1",
+ "Interval": "1000",
+ "State": "notGathered"
+ }
+ },
+ "MaxGatherers": "2"
+};
+reset(template);
+TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 10);
+cmpTimer.OnUpdate({ "turnLength": 1 });
+TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 9);
+TS_ASSERT(cmpResourceSupply.AddActiveGatherer(70));
+cmpTimer.OnUpdate({ "turnLength": 1 });
+TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 9);
+cmpResourceSupply.RemoveGatherer(70);
+cmpTimer.OnUpdate({ "turnLength": 1 });
+TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 8);
+cmpTimer.OnUpdate({ "turnLength": 1 });
+TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 7);
+
+// Grow when gathered.
+template = {
+ "Max": "10",
+ "Initial": "5",
+ "Type": "food.meat",
+ "KillBeforeGather": "false",
+ "Change": {
+ "Growth": {
+ "Value": "1",
+ "Interval": "1000",
+ "State": "gathered"
+ }
+ },
+ "MaxGatherers": "2"
+};
+reset(template);
+TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 5);
+cmpTimer.OnUpdate({ "turnLength": 1 });
+TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 5);
+TS_ASSERT(cmpResourceSupply.AddActiveGatherer(70));
+cmpTimer.OnUpdate({ "turnLength": 1 });
+TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 6);
+cmpTimer.OnUpdate({ "turnLength": 1 });
+TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 7);
+cmpResourceSupply.RemoveGatherer(70, 1);
+cmpTimer.OnUpdate({ "turnLength": 1 });
+TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 7);
+cmpTimer.OnUpdate({ "turnLength": 1 });
+TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 7);
+
+// Grow when gathered or not.
+template = {
+ "Max": "10",
+ "Initial": "5",
+ "Type": "food.meat",
+ "KillBeforeGather": "false",
+ "Change": {
+ "Growth": {
+ "Value": "1",
+ "Interval": "1000",
+ "State": "notGathered gathered"
+ }
+ },
+ "MaxGatherers": "2"
+};
+reset(template);
+TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 5);
+cmpTimer.OnUpdate({ "turnLength": 1 });
+TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 6);
+TS_ASSERT(cmpResourceSupply.AddActiveGatherer(70));
+cmpTimer.OnUpdate({ "turnLength": 1 });
+TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 7);
+cmpTimer.OnUpdate({ "turnLength": 1 });
+TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 8);
+cmpResourceSupply.RemoveGatherer(70);
+cmpTimer.OnUpdate({ "turnLength": 1 });
+TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 9);
+
+// Grow when gathered and alive.
+template = {
+ "Max": "10",
+ "Initial": "5",
+ "Type": "food.meat",
+ "KillBeforeGather": "false",
+ "Change": {
+ "Growth": {
+ "Value": "1",
+ "Interval": "1000",
+ "State": "alive gathered"
+ }
+ },
+ "MaxGatherers": "2"
+};
+reset(template);
+TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 5);
+cmpTimer.OnUpdate({ "turnLength": 1 });
+TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 5);
+TS_ASSERT(cmpResourceSupply.AddActiveGatherer(70));
+cmpTimer.OnUpdate({ "turnLength": 1 });
+TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 5);
+AddMock(entity, IID_Health, { }); // Bring the entity to life.
+cmpResourceSupply.CheckTimers(); // No other way to tell we've come to life.
+cmpTimer.OnUpdate({ "turnLength": 1 });
+TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 6);
+cmpTimer.OnUpdate({ "turnLength": 1 });
+TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 7);
+cmpResourceSupply.RemoveGatherer(70);
+cmpTimer.OnUpdate({ "turnLength": 1 });
+TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 7);
+DeleteMock(entity, IID_Health); // "Kill" the entity.
+cmpTimer.OnUpdate({ "turnLength": 1 });
+TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 7);
+
+// Decay when dead and not gathered.
+template = {
+ "Max": "10",
+ "Initial": "5",
+ "Type": "food.meat",
+ "KillBeforeGather": "false",
+ "Change": {
+ "Decay": {
+ "Value": "-1",
+ "Interval": "1000",
+ "State": "dead notGathered"
+ }
+ },
+ "MaxGatherers": "2"
+};
+reset(template);
+TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 5);
+cmpTimer.OnUpdate({ "turnLength": 1 });
+TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 4);
+TS_ASSERT(cmpResourceSupply.AddActiveGatherer(70));
+cmpTimer.OnUpdate({ "turnLength": 1 });
+TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 4);
+AddMock(entity, IID_Health, {}); // Bring the entity to life.
+cmpResourceSupply.CheckTimers(); // No other way to tell we've come to life.
+cmpTimer.OnUpdate({ "turnLength": 1 });
+TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 4);
+cmpTimer.OnUpdate({ "turnLength": 1 });
+TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 4);
+cmpResourceSupply.RemoveGatherer(70);
+cmpTimer.OnUpdate({ "turnLength": 1 });
+TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 4);
+DeleteMock(entity, IID_Health); // "Kill" the entity.
+cmpResourceSupply.CheckTimers(); // No other way to tell we've died.
+cmpTimer.OnUpdate({ "turnLength": 1 });
+TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 3);
+cmpTimer.OnUpdate({ "turnLength": 1 });
+TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 2);
Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fauna_boar.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fauna_boar.xml (revision 24962)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fauna_boar.xml (revision 24963)
@@ -1,36 +1,36 @@
Tusks
12.0
0.0
0.0
1.0
1000
2.0
50
Wild Boar
Sus scrofa
gaia/fauna_boar.png
- 150
+ 150
3.0
fauna/boar.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fruit/berry_03.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fruit/berry_03.xml (revision 24962)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fruit/berry_03.xml (revision 24963)
@@ -1,12 +1,12 @@
Berries
- 200
+ 200
props/flora/berry_bush_03.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fruit/date.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fruit/date.xml (revision 24962)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fruit/date.xml (revision 24963)
@@ -1,12 +1,12 @@
Date Palm
- 400
+ 400
flora/trees/palm_date_new_fruit.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fruit/olive.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fruit/olive.xml (revision 24962)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fruit/olive.xml (revision 24963)
@@ -1,12 +1,12 @@
Olive
- 400
+ 400
flora/trees/olive.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/ruins/pyramid_minor.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/ruins/pyramid_minor.xml (revision 24962)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/ruins/pyramid_minor.xml (revision 24963)
@@ -1,34 +1,34 @@
10.0
Minor Pyramid
gaia/special_pyramid.png
- 5000
+ 5000
90
6.0
0.6
12.0
40
special/pyramid_minor.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/ruins/stone_statues_kushite.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/ruins/stone_statues_kushite.xml (revision 24962)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/ruins/stone_statues_kushite.xml (revision 24963)
@@ -1,25 +1,25 @@
12.0
Kushite Statue
structures/statue.png
- 300
+ 300
props/special/eyecandy/statues_kush.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fauna_rhinoceros_white.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fauna_rhinoceros_white.xml (revision 24962)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fauna_rhinoceros_white.xml (revision 24963)
@@ -1,43 +1,43 @@
Horn
20
0
20
4
2000
3.0
160
White Rhinoceros
Ceratotherium simum
gaia/fauna_rhino.png
- 300
+ 300
actor/fauna/animal/lion_attack.xml
actor/fauna/animal/lion_death.xml
4.0
fauna/rhino.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fauna_walrus.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fauna_walrus.xml (revision 24962)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fauna_walrus.xml (revision 24963)
@@ -1,46 +1,46 @@
Tusks
15
10
0
5
1000
2000
Structure Ship Siege
3.0
120
Walrus
Odobenus rosmarus
gaia/fauna_walrus.png
- 300
+ 300
256x256/ellipse.png
256x256/ellipse_mask.png
4.0
fauna/walrus.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fauna_zebra.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fauna_zebra.xml (revision 24962)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fauna_zebra.xml (revision 24963)
@@ -1,27 +1,27 @@
2.5
50
Common Zebra
Equus quagga
gaia/fauna_zebra.png
- 150
+ 150
3.5
0.9
fauna/zebra.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fruit/banana.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fruit/banana.xml (revision 24962)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fruit/banana.xml (revision 24963)
@@ -1,12 +1,12 @@
Banana
- 400
+ 400
flora/trees/banana.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fruit/berry_02.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fruit/berry_02.xml (revision 24962)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fruit/berry_02.xml (revision 24963)
@@ -1,12 +1,12 @@
Berries
- 200
+ 200
props/flora/berry_bush_02.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fruit/berry_05.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fruit/berry_05.xml (revision 24962)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fruit/berry_05.xml (revision 24963)
@@ -1,12 +1,12 @@
Berries
- 200
+ 200
props/flora/bush_berries_large.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fruit/grapes.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fruit/grapes.xml (revision 24962)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fruit/grapes.xml (revision 24963)
@@ -1,13 +1,13 @@
Grapes
gaia/flora_bush_grapes.png
- 200
+ 200
props/flora/forage_grapes.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/ruins/pyramid_great.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/ruins/pyramid_great.xml (revision 24962)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/ruins/pyramid_great.xml (revision 24963)
@@ -1,37 +1,37 @@
10.0
Great Pyramid
gaia/special_pyramid.png
-2.0
- 10000
+ 10000
120
6.0
0.6
12.0
72
special/pyramid.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/ruins/stone_statues_egyptian.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/ruins/stone_statues_egyptian.xml (revision 24962)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/ruins/stone_statues_egyptian.xml (revision 24963)
@@ -1,25 +1,25 @@
12.0
Ptolemaic Egyptian Statues
structures/statue.png
- 300
+ 300
props/special/eyecandy/statues_ptol.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fauna_camel.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fauna_camel.xml (revision 24962)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fauna_camel.xml (revision 24963)
@@ -1,34 +1,34 @@
5.5
50
Dromedary
Camelus dromedarius
gaia/fauna_camel.png
- 200
+ 200
actor/fauna/movement/camel_order.xml
actor/fauna/death/death_camel.xml
6.5
0.45
fauna/camel.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fauna_cattle_cow.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fauna_cattle_cow.xml (revision 24962)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fauna_cattle_cow.xml (revision 24963)
@@ -1,31 +1,31 @@
60
150
3.5
150
Cow
Bos taurus taurus
gaia/fauna_cow.png
- 300
+ 300
5
4.5
fauna/cow.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fauna_cattle_sanga.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fauna_cattle_sanga.xml (revision 24962)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fauna_cattle_sanga.xml (revision 24963)
@@ -1,31 +1,31 @@
60
150
3.5
150
Sanga Cattle
Bos taurus africanus
gaia/fauna_sanga.png
- 300
+ 300
5
4.5
fauna/sanga_cattle.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fauna_cattle_zebu.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fauna_cattle_zebu.xml (revision 24962)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fauna_cattle_zebu.xml (revision 24963)
@@ -1,31 +1,31 @@
60
150
3.5
150
Zebu
Bos taurus indicus
gaia/fauna_zebu.png
- 300
+ 300
5
4.5
fauna/zebu_wild.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fauna_chicken.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fauna_chicken.xml (revision 24962)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fauna_chicken.xml (revision 24963)
@@ -1,49 +1,49 @@
1.5
5
Chicken
Gallus gallus domesticus
gaia/fauna_chicken.png
false
upright
- 40
+ 40
5
actor/fauna/animal/chickens.xml
actor/fauna/animal/chickens.xml
2.5
4.0
12.0
2000
8000
10000
40000
0.15
fauna/chicken.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fauna_donkey.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fauna_donkey.xml (revision 24962)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fauna_donkey.xml (revision 24963)
@@ -1,35 +1,35 @@
2.5
50
Donkey
Equus africanus asinus
gaia/fauna_donkey.png
- 200
+ 200
actor/fauna/animal/horse_order.xml
actor/fauna/animal/horse_death.xml
actor/fauna/animal/horse_trained.xml
3.5
0.8
fauna/donkey.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fauna_elephant_african_bush.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fauna_elephant_african_bush.xml (revision 24962)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fauna_elephant_african_bush.xml (revision 24963)
@@ -1,27 +1,27 @@
9.0
300
African Bush Elephant
Loxodonta africana
gaia/fauna_elephant_african_bush.png
75
- 800
+ 800
10.0
fauna/elephant_african_bush.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fauna_elephant_asian.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fauna_elephant_asian.xml (revision 24962)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fauna_elephant_asian.xml (revision 24963)
@@ -1,27 +1,27 @@
7.0
250
Asian Elephant
Elephas maximus
gaia/fauna_elephant_asian.png
60
- 650
+ 650
8.0
fauna/elephant_asian.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fauna_elephant_north_african.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fauna_elephant_north_african.xml (revision 24962)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fauna_elephant_north_african.xml (revision 24963)
@@ -1,27 +1,27 @@
6.5
200
African Forest Elephant
Loxodonta cyclotis
gaia/fauna_elephant_north_african.png
50
- 500
+ 500
7.5
fauna/elephant_african_forest.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fauna_giraffe.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fauna_giraffe.xml (revision 24962)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fauna_giraffe.xml (revision 24963)
@@ -1,27 +1,27 @@
13.0
80
Giraffe
Giraffa camelopardalis
gaia/fauna_giraffe.png
- 350
+ 350
14.0
0.6
fauna/giraffe_adult.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fauna_giraffe_infant.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fauna_giraffe_infant.xml (revision 24962)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fauna_giraffe_infant.xml (revision 24963)
@@ -1,27 +1,27 @@
7.5
40
Juvenile Giraffe
Giraffa camelopardalis
gaia/fauna_giraffe.png
- 150
+ 150
8.5
0.6
fauna/giraffe_baby.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fauna_goat.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fauna_goat.xml (revision 24962)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fauna_goat.xml (revision 24963)
@@ -1,42 +1,42 @@
30
35
2.0
35
Goat
Capra aegagrus hircus
gaia/fauna_goat.png
- 70
+ 70
2
actor/fauna/animal/goat_order.xml
actor/fauna/death/goat.xml
actor/fauna/animal/goat_trained.xml
3.0
0.45
fauna/goat.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fauna_hippopotamus.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fauna_hippopotamus.xml (revision 24962)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fauna_hippopotamus.xml (revision 24963)
@@ -1,44 +1,44 @@
Tusks
12
12
3
3.5
200
Hippopotamus
Hippopotamus amphibius
gaia/fauna_hippopotamus.png
50
- 400
+ 400
actor/fauna/animal/lion_attack.xml
actor/fauna/animal/lion_death.xml
4.5
fauna/hippopotamus.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fauna_horse.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fauna_horse.xml (revision 24962)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fauna_horse.xml (revision 24963)
@@ -1,35 +1,35 @@
4.0
50
Horse
Equus ferus caballus
gaia/fauna_horse.png
- 200
+ 200
actor/fauna/animal/horse_order.xml
actor/fauna/animal/horse_death.xml
actor/fauna/animal/horse_trained.xml
5.0
0.8
fauna/horse.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fauna_muskox.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fauna_muskox.xml (revision 24962)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fauna_muskox.xml (revision 24963)
@@ -1,24 +1,24 @@
3.5
50
Muskox
Ovibos moschatus
gaia/fauna_muskox.png
- 200
+ 200
4.5
fauna/muskox.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fauna_peacock.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fauna_peacock.xml (revision 24962)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fauna_peacock.xml (revision 24963)
@@ -1,47 +1,47 @@
1.5
10
Peacock
Pavo cristatus
gaia/fauna_peacock.png
upright
- 50
+ 50
5
actor/fauna/animal/peacock_order.xml
actor/fauna/animal/peacock_call.xml
actor/fauna/animal/peacock_trained.xml
2.5
4.0
12.0
2000
8000
10000
40000
0.3
fauna/peacock.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fauna_pig.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fauna_pig.xml (revision 24962)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fauna_pig.xml (revision 24963)
@@ -1,42 +1,42 @@
50
75
1.5
75
Pig
Sus scrofa domesticus
gaia/fauna_pig.png
- 150
+ 150
4
actor/fauna/animal/pig_order.xml
actor/fauna/animal/pig.xml
actor/fauna/animal/pig_trained.xml
2.5
0.45
fauna/pig1.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fauna_piglet.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fauna_piglet.xml (revision 24962)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fauna_piglet.xml (revision 24963)
@@ -1,26 +1,26 @@
0.5
15
Piglet
- 10
+ 10
1
1.5
0.25
fauna/piglet.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fauna_rabbit.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fauna_rabbit.xml (revision 24962)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fauna_rabbit.xml (revision 24963)
@@ -1,27 +1,27 @@
1.0
5
Rabbit
Oryctolagus cuniculus
gaia/fauna_rabbit.png
false
- 50
+ 50
2.0
fauna/rabbit1.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fauna_sheep.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fauna_sheep.xml (revision 24962)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fauna_sheep.xml (revision 24963)
@@ -1,42 +1,42 @@
40
50
2.0
50
Sheep
Ovis aries
gaia/fauna_sheep.png
- 100
+ 100
3
actor/fauna/animal/sheep_order.xml
actor/fauna/animal/sheep.xml
actor/fauna/animal/sheep_trained.xml
3.0
0.45
fauna/sheep3.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fauna_wildebeest.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fauna_wildebeest.xml (revision 24962)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fauna_wildebeest.xml (revision 24963)
@@ -1,27 +1,27 @@
3.0
50
Blue Wildebeest
Connochaetes taurinus
gaia/fauna_wildebeest.png
- 150
+ 150
4.0
0.9
fauna/wildebeest.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fruit/apple.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fruit/apple.xml (revision 24962)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fruit/apple.xml (revision 24963)
@@ -1,12 +1,12 @@
Apple
- 400
+ 400
flora/trees/apple_bloom.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fruit/berry_01.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fruit/berry_01.xml (revision 24962)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fruit/berry_01.xml (revision 24963)
@@ -1,12 +1,12 @@
Berries
- 200
+ 200
props/flora/berry_bush.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fruit/berry_04.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fruit/berry_04.xml (revision 24962)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fruit/berry_04.xml (revision 24963)
@@ -1,12 +1,12 @@
Berries
- 200
+ 200
props/flora/berry_bush_autumn_01.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fruit/fig.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fruit/fig.xml (revision 24962)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fruit/fig.xml (revision 24963)
@@ -1,18 +1,18 @@
Fig
- 500
+ 500
flora/trees/fig.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/ruins/column_doric.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/ruins/column_doric.xml (revision 24962)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/ruins/column_doric.xml (revision 24963)
@@ -1,17 +1,17 @@
2.5
Ancient Ruins
gaia/special_fence.png
- 500
+ 500
props/special/eyecandy/column_doric_fallen.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/ruins/standing_stone.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/ruins/standing_stone.xml (revision 24962)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/ruins/standing_stone.xml (revision 24963)
@@ -1,20 +1,20 @@
2.0
Celtic Standing Stone
gaia/special_treasure.png
- 300
+ 300
props/special/eyecandy/standing_stones.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/treasure/food_barrel.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/treasure/food_barrel.xml (revision 24962)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/treasure/food_barrel.xml (revision 24963)
@@ -1,26 +1,26 @@
2.5
Food Treasure
gaia/special_treasure_food.png
- 100
+ 100
treasure.food
128x128/ellipse.png
128x128/ellipse_mask.png
props/special/eyecandy/barrel_a.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/ruins/unfinished_greek_temple.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/ruins/unfinished_greek_temple.xml (revision 24962)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/ruins/unfinished_greek_temple.xml (revision 24963)
@@ -1,29 +1,29 @@
12.0
Unfinished Greek Temple
structures/temple.png
- 2000
+ 2000
30
6.0
0.6
12.0
40
props/special/eyecandy/greek_temple_unfinished.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/treasure/food_crate.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/treasure/food_crate.xml (revision 24962)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/treasure/food_crate.xml (revision 24963)
@@ -1,21 +1,21 @@
12.0
Food Treasure
gaia/special_treasure_food.png
- 200
+ 200
treasure.food
props/special/eyecandy/crate_a.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/ruins/stone_statues_roman.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/ruins/stone_statues_roman.xml (revision 24962)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/ruins/stone_statues_roman.xml (revision 24963)
@@ -1,25 +1,25 @@
12.0
Roman Statues
structures/statue.png
- 300
+ 300
props/special/eyecandy/statues_roman.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/treasure/food_bin.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/treasure/food_bin.xml (revision 24962)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/treasure/food_bin.xml (revision 24963)
@@ -1,21 +1,21 @@
12.0
Food Treasure
gaia/special_treasure_food.png
- 300
+ 300
treasure.food
props/special/eyecandy/produce_bin_a.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/treasure/food_barrels_buried.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/treasure/food_barrels_buried.xml (revision 24962)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/treasure/food_barrels_buried.xml (revision 24963)
@@ -1,25 +1,25 @@
2.5
Half-buried Barrels
gaia/special_treasure_food.png
false
0.0
- 200
+ 200
treasure.food
props/special/eyecandy/barrels_buried.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/treasure/food_jars.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/treasure/food_jars.xml (revision 24962)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/treasure/food_jars.xml (revision 24963)
@@ -1,21 +1,21 @@
2.0
Food Treasure
gaia/special_treasure_food.png
- 300
+ 300
treasure.food
props/special/eyecandy/amphorae.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/treasure/metal.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/treasure/metal.xml (revision 24962)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/treasure/metal.xml (revision 24963)
@@ -1,22 +1,22 @@
2.5
Secret Box
- 300
+ 300
treasure.metal
props/special/eyecandy/barrel_a.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/treasure/shipwreck.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/treasure/shipwreck.xml (revision 24962)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/treasure/shipwreck.xml (revision 24963)
@@ -1,24 +1,24 @@
9.0
Shipwreck
true
0.0
- 500
+ 500
treasure.wood
props/special/eyecandy/shipwreck_ram_side.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/treasure/shipwreck_sail_boat_cut.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/treasure/shipwreck_sail_boat_cut.xml (revision 24962)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/treasure/shipwreck_sail_boat_cut.xml (revision 24963)
@@ -1,24 +1,24 @@
9.0
Shipwreck
true
0.0
- 450
+ 450
treasure.wood
props/special/eyecandy/shipwreck_sail_boat_cut.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/acacia.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/acacia.xml (revision 24962)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/acacia.xml (revision 24963)
@@ -1,12 +1,12 @@
Acacia
- 200
+ 200
flora/trees/acacia.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/bamboo_single.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/bamboo_single.xml (revision 24962)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/bamboo_single.xml (revision 24963)
@@ -1,13 +1,13 @@
Bamboo
- 100
+ 100
1
flora/trees/bamboo_single.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/baobab_2_young.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/baobab_2_young.xml (revision 24962)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/baobab_2_young.xml (revision 24963)
@@ -1,12 +1,12 @@
Young Baobab
- 200
+ 200
flora/trees/baobab_new_young.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/bush_temperate.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/bush_temperate.xml (revision 24962)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/bush_temperate.xml (revision 24963)
@@ -1,12 +1,12 @@
Bush
- 50
+ 50
flora/trees/temperate_bush_biome.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/cedar_atlas_1_sapling.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/cedar_atlas_1_sapling.xml (revision 24962)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/cedar_atlas_1_sapling.xml (revision 24963)
@@ -1,12 +1,12 @@
Atlas Cedar Sapling
- 50
+ 50
flora/trees/cedar_atlas_sapling.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/treasure/golden_fleece.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/treasure/golden_fleece.xml (revision 24962)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/treasure/golden_fleece.xml (revision 24963)
@@ -1,17 +1,17 @@
2.5
Golden Fleece
- 1000
+ 1000
treasure.metal
special/golden_fleece.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/treasure/pegasus.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/treasure/pegasus.xml (revision 24962)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/treasure/pegasus.xml (revision 24963)
@@ -1,17 +1,17 @@
2.0
Pegasus
- 1000
+ 1000
treasure.metal
special/pegasus.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/treasure/shipwreck_sail_boat.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/treasure/shipwreck_sail_boat.xml (revision 24962)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/treasure/shipwreck_sail_boat.xml (revision 24963)
@@ -1,24 +1,24 @@
9.0
Shipwreck
true
0.0
- 400
+ 400
treasure.wood
props/special/eyecandy/shipwreck_sail_boat.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/treasure/wood.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/treasure/wood.xml (revision 24962)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/treasure/wood.xml (revision 24963)
@@ -1,25 +1,25 @@
12.0
Wood Treasure
- 300
+ 300
treasure.wood
props/special/eyecandy/wood_pile.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/bamboo_dragon.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/bamboo_dragon.xml (revision 24962)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/bamboo_dragon.xml (revision 24963)
@@ -1,20 +1,20 @@
15.0
Dragon Bamboo
- 1000
+ 1000
12
flora/trees/bamboo_dragon.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/baobab_1_sapling.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/baobab_1_sapling.xml (revision 24962)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/baobab_1_sapling.xml (revision 24963)
@@ -1,13 +1,13 @@
Baobab Sapling
- 50
+ 50
2
flora/trees/baobab_new_sapling.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/bush_badlands.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/bush_badlands.xml (revision 24962)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/bush_badlands.xml (revision 24963)
@@ -1,12 +1,12 @@
Hardy Bush
- 75
+ 75
props/flora/bush_tempe_a.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/carob.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/carob.xml (revision 24962)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/carob.xml (revision 24963)
@@ -1,12 +1,12 @@
Carob
- 200
+ 200
flora/trees/carob.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/cedar_atlas_4_dead.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/cedar_atlas_4_dead.xml (revision 24962)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/cedar_atlas_4_dead.xml (revision 24963)
@@ -1,12 +1,12 @@
Atlas Cedar
- 300
+ 300
flora/trees/cedar_atlas_dead.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/treasure/food_persian_small.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/treasure/food_persian_small.xml (revision 24962)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/treasure/food_persian_small.xml (revision 24963)
@@ -1,21 +1,21 @@
12.0
Persian Food Treasure
gaia/special_treasure_food.png
- 400
+ 400
treasure.food
props/special/eyecandy/treasure_persian_food_small.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/treasure/metal_persian_small.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/treasure/metal_persian_small.xml (revision 24962)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/treasure/metal_persian_small.xml (revision 24963)
@@ -1,25 +1,25 @@
12.0
Persian Rugs
- 300
+ 300
treasure.metal
props/special/eyecandy/treasure_persian_metal_small.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/treasure/shipwreck_ram_bow.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/treasure/shipwreck_ram_bow.xml (revision 24962)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/treasure/shipwreck_ram_bow.xml (revision 24963)
@@ -1,24 +1,24 @@
9.0
Shipwreck
true
0.0
- 550
+ 550
treasure.wood
props/special/eyecandy/shipwreck_ram_bow.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/treasure/stone.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/treasure/stone.xml (revision 24962)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/treasure/stone.xml (revision 24963)
@@ -1,25 +1,25 @@
2.0
Stone Treasure
- 300
+ 300
treasure.stone
props/special/eyecandy/stone_pile.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/bamboo.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/bamboo.xml (revision 24962)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/bamboo.xml (revision 24963)
@@ -1,12 +1,12 @@
Bamboo
- 200
+ 200
flora/trees/bamboo.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/baobab.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/baobab.xml (revision 24962)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/baobab.xml (revision 24963)
@@ -1,20 +1,20 @@
12.0
Baobab
- 400
+ 400
9
flora/trees/baobab.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/baobab_4_dead.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/baobab_4_dead.xml (revision 24962)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/baobab_4_dead.xml (revision 24963)
@@ -1,20 +1,20 @@
15.0
Baobab
- 550
+ 550
12
flora/trees/baobab_new_dead.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/bush_tropic.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/bush_tropic.xml (revision 24962)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/bush_tropic.xml (revision 24963)
@@ -1,12 +1,12 @@
Bush
- 50
+ 50
flora/trees/tropic_bush_biome.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/cedar_atlas_3_mature.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/cedar_atlas_3_mature.xml (revision 24962)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/cedar_atlas_3_mature.xml (revision 24963)
@@ -1,12 +1,12 @@
Atlas Cedar
- 350
+ 350
flora/trees/cedar_atlas.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/treasure/food_persian_big.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/treasure/food_persian_big.xml (revision 24962)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/treasure/food_persian_big.xml (revision 24963)
@@ -1,21 +1,21 @@
2.0
Persian Food Stores
gaia/special_treasure_food.png
- 600
+ 600
treasure.food
props/special/eyecandy/treasure_persian_food_big.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/treasure/metal_persian_big.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/treasure/metal_persian_big.xml (revision 24962)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/treasure/metal_persian_big.xml (revision 24963)
@@ -1,25 +1,25 @@
12.0
Persian Wares
- 500
+ 500
treasure.metal
props/special/eyecandy/treasure_persian_metal_big.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/treasure/shipwreck_debris.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/treasure/shipwreck_debris.xml (revision 24962)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/treasure/shipwreck_debris.xml (revision 24963)
@@ -1,27 +1,27 @@
2.5
Shipwreck Cargo
false
false
-0.1
true
0.0
- 200
+ 200
treasure.food
props/special/eyecandy/barrels_floating.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/treasure/standing_stone.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/treasure/standing_stone.xml (revision 24962)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/treasure/standing_stone.xml (revision 24963)
@@ -1,25 +1,25 @@
2.0
Celtic Standing Stone
- 300
+ 300
treasure.stone
props/special/eyecandy/standing_stones.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/aleppo_pine.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/aleppo_pine.xml (revision 24962)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/aleppo_pine.xml (revision 24963)
@@ -1,12 +1,12 @@
Aleppo Pine
- 200
+ 200
flora/trees/aleppo_pine.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/banyan.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/banyan.xml (revision 24962)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/banyan.xml (revision 24963)
@@ -1,20 +1,20 @@
15.0
Banyan
- 600
+ 600
12
flora/trees/banyan.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/baobab_3_mature.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/baobab_3_mature.xml (revision 24962)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/baobab_3_mature.xml (revision 24963)
@@ -1,20 +1,20 @@
15.0
Baobab
- 600
+ 600
12
flora/trees/baobab_new.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/bush_temperate_winter.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/bush_temperate_winter.xml (revision 24962)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/bush_temperate_winter.xml (revision 24963)
@@ -1,12 +1,12 @@
Bush
- 50
+ 50
flora/trees/temperate_bush_biome_winter.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/cedar_atlas_2_young.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/cedar_atlas_2_young.xml (revision 24962)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/cedar_atlas_2_young.xml (revision 24963)
@@ -1,12 +1,12 @@
Atlas Cedar
- 200
+ 200
flora/trees/cedar_atlas_young.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/cretan_date_palm_patch.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/cretan_date_palm_patch.xml (revision 24962)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/cretan_date_palm_patch.xml (revision 24963)
@@ -1,20 +1,20 @@
12.0
Cretan Date Palm
- 300
+ 300
12
flora/trees/palm_cretan_date_patch.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/cypress_wild.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/cypress_wild.xml (revision 24962)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/cypress_wild.xml (revision 24963)
@@ -1,12 +1,12 @@
Cypress
- 200
+ 200
flora/trees/cypress_mediterranean_wild.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/cypress.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/cypress.xml (revision 24962)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/cypress.xml (revision 24963)
@@ -1,12 +1,12 @@
Cypress
- 200
+ 200
flora/trees/mediterranean_cypress.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/cretan_date_palm_tall.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/cretan_date_palm_tall.xml (revision 24962)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/cretan_date_palm_tall.xml (revision 24963)
@@ -1,12 +1,12 @@
Cretan Date Palm
- 200
+ 200
flora/trees/palm_cretan_date_tall.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/cretan_date_palm_short.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/cretan_date_palm_short.xml (revision 24962)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/cretan_date_palm_short.xml (revision 24963)
@@ -1,12 +1,12 @@
Cretan Date Palm
- 100
+ 100
flora/trees/palm_cretan_date_short.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/cypress_windswept.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/cypress_windswept.xml (revision 24962)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/cypress_windswept.xml (revision 24963)
@@ -1,12 +1,12 @@
Cypress
- 200
+ 200
flora/trees/cypress_mediterranean_windswept.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/dead.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/dead.xml (revision 24962)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/dead.xml (revision 24963)
@@ -1,12 +1,12 @@
Deciduous Tree
- 200
+ 200
flora/trees/temperate_dead_forest.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/euro_beech_aut.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/euro_beech_aut.xml (revision 24962)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/euro_beech_aut.xml (revision 24963)
@@ -1,12 +1,12 @@
European Beech
- 200
+ 200
flora/trees/european_beech_aut.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/fir_winter.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/fir_winter.xml (revision 24962)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/fir_winter.xml (revision 24963)
@@ -1,12 +1,12 @@
Fir
- 200
+ 200
flora/trees/fir_tree_winter.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/medit_fan_palm.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/medit_fan_palm.xml (revision 24962)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/medit_fan_palm.xml (revision 24963)
@@ -1,12 +1,12 @@
Mediterranean Fan Palm
- 200
+ 200
flora/trees/palm_medit_fan_new.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/oak_dead.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/oak_dead.xml (revision 24962)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/oak_dead.xml (revision 24963)
@@ -1,12 +1,12 @@
Oak
- 200
+ 200
flora/trees/oak_dead.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/oak_large.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/oak_large.xml (revision 24962)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/oak_large.xml (revision 24963)
@@ -1,12 +1,12 @@
Large Oak
- 300
+ 300
flora/trees/oak_large.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/palm_palmyra.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/palm_palmyra.xml (revision 24962)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/palm_palmyra.xml (revision 24963)
@@ -1,12 +1,12 @@
Palmyra Palm
- 200
+ 200
flora/trees/palm_palmyra.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/pine_black.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/pine_black.xml (revision 24962)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/pine_black.xml (revision 24963)
@@ -1,12 +1,12 @@
Black Pine
- 200
+ 200
flora/trees/pine_black.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/pine_w.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/pine_w.xml (revision 24962)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/pine_w.xml (revision 24963)
@@ -1,12 +1,12 @@
Pine
- 200
+ 200
flora/trees/pine_w.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/date_palm_dead.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/date_palm_dead.xml (revision 24962)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/date_palm_dead.xml (revision 24963)
@@ -1,12 +1,12 @@
Date Palm
- 200
+ 200
flora/trees/palm_date_dead.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/euro_beech.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/euro_beech.xml (revision 24962)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/euro_beech.xml (revision 24963)
@@ -1,12 +1,12 @@
European Beech
- 200
+ 200
flora/trees/european_beech.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/fir_sapling.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/fir_sapling.xml (revision 24962)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/fir_sapling.xml (revision 24963)
@@ -1,12 +1,12 @@
Fir Sapling
- 50
+ 50
flora/trees/fir_sapling.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/maple.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/maple.xml (revision 24962)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/maple.xml (revision 24963)
@@ -1,12 +1,12 @@
Maple
- 300
+ 300
flora/trees/temperate_maple_trees.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/oak_aut_new.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/oak_aut_new.xml (revision 24962)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/oak_aut_new.xml (revision 24963)
@@ -1,12 +1,12 @@
Oak
- 200
+ 200
flora/trees/oak_new_aut.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/oak_hungarian_autumn.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/oak_hungarian_autumn.xml (revision 24962)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/oak_hungarian_autumn.xml (revision 24963)
@@ -1,12 +1,12 @@
Hungarian Oak
- 200
+ 200
flora/trees/oak_hungarian_autumn.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/palm_doum.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/palm_doum.xml (revision 24962)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/palm_doum.xml (revision 24963)
@@ -1,12 +1,12 @@
Doum Palm
- 200
+ 200
flora/trees/palm_doum.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/pine.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/pine.xml (revision 24962)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/pine.xml (revision 24963)
@@ -1,12 +1,12 @@
Pine
- 200
+ 200
flora/trees/pine.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/pine_maritime_short.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/pine_maritime_short.xml (revision 24962)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/pine_maritime_short.xml (revision 24963)
@@ -1,12 +1,12 @@
Maritime Pine
- 200
+ 200
flora/trees/pine_maritime_short.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/poplar_lombardy.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/poplar_lombardy.xml (revision 24962)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/poplar_lombardy.xml (revision 24963)
@@ -1,12 +1,12 @@
Black Poplar
- 200
+ 200
flora/trees/poplar_lombardy.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/date_palm.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/date_palm.xml (revision 24962)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/date_palm.xml (revision 24963)
@@ -1,12 +1,12 @@
Date Palm
- 200
+ 200
flora/trees/palm_date_new.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/elm_dead.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/elm_dead.xml (revision 24962)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/elm_dead.xml (revision 24963)
@@ -1,12 +1,12 @@
Elm
- 200
+ 200
flora/trees/elm_dead.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/fir.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/fir.xml (revision 24962)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/fir.xml (revision 24963)
@@ -1,12 +1,12 @@
Fir
- 200
+ 200
flora/trees/fir_tree.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/mangrove.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/mangrove.xml (revision 24962)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/mangrove.xml (revision 24963)
@@ -1,12 +1,12 @@
Mangrove
- 200
+ 200
flora/trees/mangrove.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/oak_aut.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/oak_aut.xml (revision 24962)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/oak_aut.xml (revision 24963)
@@ -1,12 +1,12 @@
Oak
- 200
+ 200
flora/trees/oak_aut.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/oak_hungarian.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/oak_hungarian.xml (revision 24962)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/oak_hungarian.xml (revision 24963)
@@ -1,12 +1,12 @@
Hungarian Oak
- 200
+ 200
flora/trees/oak_hungarian.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/palm_areca.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/palm_areca.xml (revision 24962)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/palm_areca.xml (revision 24963)
@@ -1,12 +1,12 @@
Areca Palm
- 200
+ 200
flora/trees/palm_areca.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/palm_tropical.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/palm_tropical.xml (revision 24962)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/palm_tropical.xml (revision 24963)
@@ -1,12 +1,12 @@
Tropical Palm
- 200
+ 200
flora/trees/palm_tropical.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/pine_maritime.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/pine_maritime.xml (revision 24962)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/pine_maritime.xml (revision 24963)
@@ -1,12 +1,12 @@
Maritime Pine
- 200
+ 200
flora/trees/pine_maritime.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/poplar_dead.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/poplar_dead.xml (revision 24962)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/poplar_dead.xml (revision 24963)
@@ -1,12 +1,12 @@
Poplar
- 200
+ 200
flora/trees/poplar_dead.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/elm.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/elm.xml (revision 24962)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/elm.xml (revision 24963)
@@ -1,12 +1,12 @@
Elm
- 200
+ 200
flora/trees/elm.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/euro_birch.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/euro_birch.xml (revision 24962)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/euro_birch.xml (revision 24963)
@@ -1,12 +1,12 @@
Silver Birch
- 300
+ 300
flora/trees/euro_birch_tree.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/juniper_prickly.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/juniper_prickly.xml (revision 24962)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/juniper_prickly.xml (revision 24963)
@@ -1,12 +1,12 @@
Prickly Juniper
- 200
+ 200
flora/trees/juniper_prickly.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/oak.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/oak.xml (revision 24962)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/oak.xml (revision 24963)
@@ -1,12 +1,12 @@
Oak
- 200
+ 200
flora/trees/oak.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/oak_holly.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/oak_holly.xml (revision 24962)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/oak_holly.xml (revision 24963)
@@ -1,12 +1,12 @@
Holly Oak
- 200
+ 200
flora/trees/oak_holly.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/oak_new.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/oak_new.xml (revision 24962)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/oak_new.xml (revision 24963)
@@ -1,12 +1,12 @@
Oak
- 200
+ 200
flora/trees/oak_new.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/palm_tropic.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/palm_tropic.xml (revision 24962)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/palm_tropic.xml (revision 24963)
@@ -1,12 +1,12 @@
Palm
- 200
+ 200
flora/trees/palm_tropical_tall.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/pine_black_dead.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/pine_black_dead.xml (revision 24962)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/pine_black_dead.xml (revision 24963)
@@ -1,12 +1,12 @@
Black Pine
- 200
+ 200
flora/trees/pine_black_dead.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/poplar.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/poplar.xml (revision 24962)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/poplar.xml (revision 24963)
@@ -1,12 +1,12 @@
Poplar
- 200
+ 200
flora/trees/poplar.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/poplar_lombardy_autumn.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/poplar_lombardy_autumn.xml (revision 24962)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/poplar_lombardy_autumn.xml (revision 24963)
@@ -1,12 +1,12 @@
Poplar
- 200
+ 200
flora/trees/poplar_autumn.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/tamarix.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/tamarix.xml (revision 24962)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/tamarix.xml (revision 24963)
@@ -1,12 +1,12 @@
Tamarix
- 200
+ 200
flora/trees/tamarix.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/strangler.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/strangler.xml (revision 24962)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/strangler.xml (revision 24963)
@@ -1,20 +1,20 @@
15.0
Strangler Fig
- 500
+ 500
10
flora/trees/strangler.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/senegal_date_palm.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/senegal_date_palm.xml (revision 24962)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/senegal_date_palm.xml (revision 24963)
@@ -1,12 +1,12 @@
Senegal Date Palm
- 200
+ 200
flora/trees/palm_senegal_date.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/poplar_lombardy_dead.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/poplar_lombardy_dead.xml (revision 24962)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/poplar_lombardy_dead.xml (revision 24963)
@@ -1,12 +1,12 @@
Black Poplar
- 200
+ 200
flora/trees/poplar_lombardy_dead.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/teak.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/teak.xml (revision 24962)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/teak.xml (revision 24963)
@@ -1,19 +1,19 @@
15.0
Teak
- 500
+ 500
flora/trees/teak.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/temperate_winter.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/temperate_winter.xml (revision 24962)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/temperate_winter.xml (revision 24963)
@@ -1,12 +1,12 @@
Deciduous Tree
- 200
+ 200
flora/trees/temperate_forest_biome_tree_winter.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_gaia_fruit.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/template_gaia_fruit.xml (revision 24962)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/template_gaia_fruit.xml (revision 24963)
@@ -1,42 +1,42 @@
3.0
Fruit
Pick fruit for food.
gaia/flora_bush_berry.png
food
true
false
- 1
+ 1
food.fruit
8
3.0
0.5
4.0
Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_gaia_rock_large.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/template_gaia_rock_large.xml (revision 24962)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/template_gaia_rock_large.xml (revision 24963)
@@ -1,18 +1,18 @@
3.5
- 5000
+ 5000
24
5.0
0.5
Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_resource_corral.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_resource_corral.xml (revision 24962)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_resource_corral.xml (revision 24963)
@@ -1,60 +1,63 @@
+
+ structures/corral
+
50
100
5.0
500
decay|rubble/rubble_stone_3x3
Corral
template_structure_resource_corral
Raise Domestic Animals for food.
Economic Village Corral
structures/corral.png
phase_village
20
0.7
gaia/fauna_goat_trainable
gaia/fauna_sheep_trainable
gaia/fauna_pig_trainable
gaia/fauna_cattle_cow_trainable
gather_animals_stockbreeding
interface/complete/building/complete_corral.xml
false
20
30000
20
structures/fndn_3x3.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_fauna_hunt_aggressive_bear.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_fauna_hunt_aggressive_bear.xml (revision 24962)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_fauna_hunt_aggressive_bear.xml (revision 24963)
@@ -1,25 +1,25 @@
Paws
20
0
20
6
2000
- 300
+ 300
actor/fauna/animal/lion_attack.xml
actor/fauna/animal/lion_death.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/temperate_autumn.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/temperate_autumn.xml (revision 24962)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/temperate_autumn.xml (revision 24963)
@@ -1,12 +1,12 @@
Deciduous Tree
- 200
+ 200
flora/trees/temperate_forest_biome_tree_autumn.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_gaia_fish.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/template_gaia_fish.xml (revision 24962)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/template_gaia_fish.xml (revision 24963)
@@ -1,45 +1,45 @@
2.5
Fish
SeaCreature
Catch fish for food.
food
false
false
false
true
-2.0
true
false
- 1000
+ 1000
food.fish
4
0.2
4.0
0.666
5.0
Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_gaia_rock.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/template_gaia_rock.xml (revision 24962)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/template_gaia_rock.xml (revision 24963)
@@ -1,33 +1,33 @@
3.5
Stone Quarry
Quarry rock for stone.
gaia/geology_stone_1.png
stone
pitch-roll
false
- 1000
+ 1000
stone.rock
12
Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_gaia_tree.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/template_gaia_tree.xml (revision 24962)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/template_gaia_tree.xml (revision 24963)
@@ -1,51 +1,51 @@
15.0
Tree
Chop trees for wood.
gaia/flora_tree_generic.png
wood
true
false
- 1
+ 1
wood.tree
8
128x128/ellipse.png
128x128/ellipse_mask.png
3.0
0.5
10.0
Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_fauna_hunt.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_fauna_hunt.xml (revision 24962)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_fauna_hunt.xml (revision 24963)
@@ -1,15 +1,15 @@
Kill to gather meat for food.
10
true
- 100
+ 100
food.meat
8
Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/temperate.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/temperate.xml (revision 24962)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/temperate.xml (revision 24963)
@@ -1,12 +1,12 @@
Deciduous Tree
- 200
+ 200
flora/trees/temperate_forest_biome_tree.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/tropic_rainforest.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/tropic_rainforest.xml (revision 24962)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/tropic_rainforest.xml (revision 24963)
@@ -1,12 +1,12 @@
Rainforest Tree
- 200
+ 200
flora/trees/tropic_forest_biome_tree.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_gaia_ore_large.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/template_gaia_ore_large.xml (revision 24962)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/template_gaia_ore_large.xml (revision 24963)
@@ -1,18 +1,18 @@
3.5
- 5000
+ 5000
24
5.0
0.5
Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_gaia_treasure.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/template_gaia_treasure.xml (revision 24962)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/template_gaia_treasure.xml (revision 24963)
@@ -1,39 +1,39 @@
12.0
Treasure
Collect treasures for resources.
gaia/special_treasure.png
metal
false
- 300
+ 300
1
0.3
3.75
Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_fauna_herd.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_fauna_herd.xml (revision 24962)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_fauna_herd.xml (revision 24963)
@@ -1,15 +1,15 @@
Kill to gather meat for food.
true
- 100
+ 100
food.meat
8
20
Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_fauna_hunt_whale.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_fauna_hunt_whale.xml (revision 24962)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_fauna_hunt_whale.xml (revision 24963)
@@ -1,57 +1,57 @@
true
Kill to butcher for food.
10
false
false
upright
true
0.0
true
- 2000
+ 2000
food.fish
5
128x512/ellipse.png
128x512/ellipse_mask.png
4.0
0.666
5.0
skittish
60.0
60.0
100000
300000
1
2
0
ship-small
1.8
1
Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/toona.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/toona.xml (revision 24962)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/toona.xml (revision 24963)
@@ -1,12 +1,12 @@
Toona
- 200
+ 200
flora/trees/tree_tropic.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_gaia_ore.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/template_gaia_ore.xml (revision 24962)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/template_gaia_ore.xml (revision 24963)
@@ -1,33 +1,33 @@
3.5
Metal Mine
Mine ore for metal.
gaia/geology_metal.png
metal
pitch-roll
false
- 1000
+ 1000
metal.ore
12
Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_gaia_ruins.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/template_gaia_ruins.xml (revision 24962)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/template_gaia_ruins.xml (revision 24963)
@@ -1,32 +1,32 @@
2.5
Ruins
Demolish ruins for stone.
gaia/geology_stone_1.png
stone
false
- 500
+ 500
stone.ruins
1
interface/complete/building/complete_universal.xml
attack/destruction/building_collapse_large.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_resource_field.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_resource_field.xml (revision 24962)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_resource_field.xml (revision 24963)
@@ -1,74 +1,74 @@
50
100
2.0
250
decay|rubble/rubble_field
Field
template_structure_resource_field
Field
Harvest grain for food. Each subsequent gatherer works less efficiently.
structures/field.png
50
false
false
15
40
5
false
- Infinity
+ Infinity
food.grain
5
0.90
interface/complete/building/complete_field.xml
8.0
5.0
0
structures/plot_field_found.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_fauna_hunt_skittish_elephant_infant.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_fauna_hunt_skittish_elephant_infant.xml (revision 24962)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_fauna_hunt_skittish_elephant_infant.xml (revision 24963)
@@ -1,17 +1,17 @@
Elephant
- 100
+ 100
actor/fauna/animal/elephant_order.xml
actor/fauna/animal/elephant_death.xml
actor/fauna/animal/elephant_trained.xml