Index: ps/trunk/binaries/data/mods/public/art/textures/cursors/action-gather-treasure.png
===================================================================
Cannot display: file marked as a binary type.
svn:mime-type = application/octet-stream
Index: ps/trunk/binaries/data/mods/public/art/textures/cursors/action-gather-treasure.png
===================================================================
--- ps/trunk/binaries/data/mods/public/art/textures/cursors/action-gather-treasure.png (revision 24988)
+++ ps/trunk/binaries/data/mods/public/art/textures/cursors/action-gather-treasure.png (nonexistent)
Property changes on: ps/trunk/binaries/data/mods/public/art/textures/cursors/action-gather-treasure.png
___________________________________________________________________
Deleted: svn:mime-type
## -1 +0,0 ##
-application/octet-stream
\ No newline at end of property
Index: ps/trunk/binaries/data/mods/public/art/textures/cursors/action-gather-treasure.txt
===================================================================
--- ps/trunk/binaries/data/mods/public/art/textures/cursors/action-gather-treasure.txt (revision 24988)
+++ ps/trunk/binaries/data/mods/public/art/textures/cursors/action-gather-treasure.txt (nonexistent)
@@ -1 +0,0 @@
-1 1
Index: ps/trunk/binaries/data/mods/public/art/textures/cursors/action-collect-treasure.png
===================================================================
Cannot display: file marked as a binary type.
svn:mime-type = image/png
Index: ps/trunk/binaries/data/mods/public/art/textures/cursors/action-collect-treasure.png
===================================================================
--- ps/trunk/binaries/data/mods/public/art/textures/cursors/action-collect-treasure.png (nonexistent)
+++ ps/trunk/binaries/data/mods/public/art/textures/cursors/action-collect-treasure.png (revision 24989)
Property changes on: ps/trunk/binaries/data/mods/public/art/textures/cursors/action-collect-treasure.png
___________________________________________________________________
Added: svn:mime-type
## -0,0 +1 ##
+image/png
\ No newline at end of property
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 24988)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/treasure/food_barrels_buried.xml (revision 24989)
@@ -1,25 +1,26 @@
2.5Half-buried Barrelsgaia/special_treasure_food.pngfalse0.0
-
- 200
- treasure.food
-
+
+
+ 200
+
+ props/special/eyecandy/barrels_buried.xml
Index: ps/trunk/binaries/data/mods/public/art/textures/cursors/action-collect-treasure.txt
===================================================================
--- ps/trunk/binaries/data/mods/public/art/textures/cursors/action-collect-treasure.txt (nonexistent)
+++ ps/trunk/binaries/data/mods/public/art/textures/cursors/action-collect-treasure.txt (revision 24989)
@@ -0,0 +1 @@
+1 1
Property changes on: ps/trunk/binaries/data/mods/public/art/textures/cursors/action-collect-treasure.txt
___________________________________________________________________
Added: svn:eol-style
## -0,0 +1 ##
+native
\ No newline at end of property
Added: svn:mime-type
## -0,0 +1 ##
+text/plain
\ No newline at end of property
Index: ps/trunk/binaries/data/mods/public/globalscripts/Resources.js
===================================================================
--- ps/trunk/binaries/data/mods/public/globalscripts/Resources.js (revision 24988)
+++ ps/trunk/binaries/data/mods/public/globalscripts/Resources.js (revision 24989)
@@ -1,122 +1,115 @@
/**
* This class provides a cache to all resource names and properties defined by the JSON files.
*/
function Resources()
{
this.resourceData = [];
this.resourceDataObj = {};
this.resourceCodes = [];
this.resourceNames = {};
this.resourceCodesByProperty = {};
for (let filename of Engine.ListDirectoryFiles("simulation/data/resources/", "*.json", false))
{
let data = Engine.ReadJSONFile(filename);
if (!data)
continue;
if (data.code != data.code.toLowerCase())
warn("Resource codes should use lower case: " + data.code);
- // Treasures are supported for every specified resource
- if (data.code == "treasure")
- {
- error("Encountered resource with reserved keyword: " + data.code);
- continue;
- }
-
this.resourceData.push(data);
this.resourceDataObj[data.code] = data;
this.resourceCodes.push(data.code);
this.resourceNames[data.code] = data.name;
for (let subres in data.subtypes)
this.resourceNames[subres] = data.subtypes[subres];
for (let property in data.properties)
{
if (!this.resourceCodesByProperty[data.properties[property]])
this.resourceCodesByProperty[data.properties[property]] = [];
this.resourceCodesByProperty[data.properties[property]].push(data.code);
}
}
// Sort arrays by specified order
let resDataSort = (a, b) => a.order < b.order ? -1 : a.order > b.order ? +1 : 0;
let resSort = (a, b) => resDataSort(
this.resourceData.find(resource => resource.code == a),
this.resourceData.find(resource => resource.code == b)
);
this.resourceData.sort(resDataSort);
this.resourceCodes.sort(resSort);
for (let property in this.resourceCodesByProperty)
this.resourceCodesByProperty[property].sort(resSort);
deepfreeze(this.resourceData);
deepfreeze(this.resourceDataObj);
deepfreeze(this.resourceCodes);
deepfreeze(this.resourceNames);
deepfreeze(this.resourceCodesByProperty);
}
/**
* Returns the objects defined in the JSON files for all available resources,
* ordered as defined in these files.
*/
Resources.prototype.GetResources = function()
{
return this.resourceData;
};
/**
* Returns the object defined in the JSON file for the given resource.
*/
Resources.prototype.GetResource = function(type)
{
return this.resourceDataObj[type];
};
/**
* Returns an array containing all resource codes ordered as defined in the resource files.
* @return {string[]} - Data of the form [ "food", "wood", ... ].
*/
Resources.prototype.GetCodes = function()
{
return this.resourceCodes;
};
/**
* Returns an array containing all barterable resource codes ordered as defined in the resource files.
* @return {string[]} - Data of the form [ "food", "wood", ... ].
*/
Resources.prototype.GetBarterableCodes = function()
{
return this.resourceCodesByProperty.barterable || [];
};
/**
* Returns an array containing all tradable resource codes ordered as defined in the resource files.
* @return {string[]} - Data of the form [ "food", "wood", ... ].
*/
Resources.prototype.GetTradableCodes = function()
{
return this.resourceCodesByProperty.tradable || [];
};
/**
* Returns an array containing all tributable resource codes ordered as defined in the resource files.
* @return {string[]} - Data of the form [ "food", "wood", ... ].
*/
Resources.prototype.GetTributableCodes = function()
{
return this.resourceCodesByProperty.tributable || [];
};
/**
* Returns an object mapping resource codes to translatable resource names. Includes subtypes.
* For example { "food": "Food", "fish": "Fish", "fruit": "Fruit", "metal": "Metal", ... }
*/
Resources.prototype.GetNames = function()
{
return this.resourceNames;
};
Index: ps/trunk/binaries/data/mods/public/globalscripts/Templates.js
===================================================================
--- ps/trunk/binaries/data/mods/public/globalscripts/Templates.js (revision 24988)
+++ ps/trunk/binaries/data/mods/public/globalscripts/Templates.js (revision 24989)
@@ -1,599 +1,609 @@
/**
* Loads history and gameplay data of all civs.
*
* @param selectableOnly {boolean} - Only load civs that can be selected
* in the gamesetup. Scenario maps might set non-selectable civs.
*/
function loadCivFiles(selectableOnly)
{
let propertyNames = [
"Code", "Culture", "Name", "Emblem", "History", "Music", "CivBonuses", "StartEntities",
"Formations", "AINames", "SkirmishReplacements", "SelectableInGameSetup"];
let civData = {};
for (let filename of Engine.ListDirectoryFiles("simulation/data/civs/", "*.json", false))
{
let data = Engine.ReadJSONFile(filename);
for (let prop of propertyNames)
if (data[prop] === undefined)
throw new Error(filename + " doesn't contain " + prop);
if (!selectableOnly || data.SelectableInGameSetup)
civData[data.Code] = data;
}
return civData;
}
/**
* @return {string[]} - All the classes for this identity template.
*/
function GetIdentityClasses(template)
{
let classString = "";
if (template.Classes && template.Classes._string)
classString += " " + template.Classes._string;
if (template.VisibleClasses && template.VisibleClasses._string)
classString += " " + template.VisibleClasses._string;
if (template.Rank)
classString += " " + template.Rank;
return classString.length > 1 ? classString.substring(1).split(" ") : [];
}
/**
* Gets an array with all classes for this identity template
* that should be shown in the GUI
*/
function GetVisibleIdentityClasses(template)
{
return template.VisibleClasses && template.VisibleClasses._string ? template.VisibleClasses._string.split(" ") : [];
}
/**
* Check if a given list of classes matches another list of classes.
* Useful f.e. for checking identity classes.
*
* @param classes - List of the classes to check against.
* @param match - Either a string in the form
* "Class1 Class2+Class3"
* where spaces are handled as OR and '+'-signs as AND,
* and ! is handled as NOT, thus Class1+!Class2 = Class1 AND NOT Class2.
* Or a list in the form
* [["Class1"], ["Class2", "Class3"]]
* where the outer list is combined as OR, and the inner lists are AND-ed.
* Or a hybrid format containing a list of strings, where the list is
* combined as OR, and the strings are split by space and '+' and AND-ed.
*
* @return undefined if there are no classes or no match object
* true if the the logical combination in the match object matches the classes
* false otherwise.
*/
function MatchesClassList(classes, match)
{
if (!match || !classes)
return undefined;
// Transform the string to an array
if (typeof match == "string")
match = match.split(/\s+/);
for (let sublist of match)
{
// If the elements are still strings, split them by space or by '+'
if (typeof sublist == "string")
sublist = sublist.split(/[+\s]+/);
if (sublist.every(c => (c[0] == "!" && classes.indexOf(c.substr(1)) == -1) ||
(c[0] != "!" && classes.indexOf(c) != -1)))
return true;
}
return false;
}
/**
* Gets the value originating at the value_path as-is, with no modifiers applied.
*
* @param {Object} template - A valid template as returned from a template loader.
* @param {string} value_path - Route to value within the xml template structure.
* @return {number}
*/
function GetBaseTemplateDataValue(template, value_path)
{
let current_value = template;
for (let property of value_path.split("/"))
current_value = current_value[property] || 0;
return +current_value;
}
/**
* Gets the value originating at the value_path with the modifiers dictated by the mod_key applied.
*
* @param {Object} template - A valid template as returned from a template loader.
* @param {string} value_path - Route to value within the xml template structure.
* @param {string} mod_key - Tech modification key, if different from value_path.
* @param {number} player - Optional player id.
* @param {Object} modifiers - Value modifiers from auto-researched techs, unit upgrades,
* etc. Optional as only used if no player id provided.
* @return {number} Modifier altered value.
*/
function GetModifiedTemplateDataValue(template, value_path, mod_key, player, modifiers={})
{
let current_value = GetBaseTemplateDataValue(template, value_path);
mod_key = mod_key || value_path;
if (player)
current_value = ApplyValueModificationsToTemplate(mod_key, current_value, player, template);
else if (modifiers && modifiers[mod_key])
current_value = GetTechModifiedProperty(modifiers[mod_key], GetIdentityClasses(template.Identity), current_value);
// Using .toFixed() to get around spidermonkey's treatment of numbers (3 * 1.1 = 3.3000000000000003 for instance).
return +current_value.toFixed(8);
}
/**
* Get information about a template with or without technology modifications.
*
* NOTICE: The data returned here should have the same structure as
* the object returned by GetEntityState and GetExtendedEntityState!
*
* @param {Object} template - A valid template as returned by the template loader.
* @param {number} player - An optional player id to get the technology modifications
* of properties.
* @param {Object} auraTemplates - In the form of { key: { "auraName": "", "auraDescription": "" } }.
* @param {Object} modifiers - Modifications from auto-researched techs, unit upgrades
* etc. Optional as only used if there's no player
* id provided.
*/
function GetTemplateDataHelper(template, player, auraTemplates, modifiers = {})
{
// Return data either from template (in tech tree) or sim state (ingame).
// @param {string} value_path - Route to the value within the template.
// @param {string} mod_key - Modification key, if not the same as the value_path.
let getEntityValue = function(value_path, mod_key) {
return GetModifiedTemplateDataValue(template, value_path, mod_key, player, modifiers);
};
let ret = {};
if (template.Resistance)
{
// Don't show Foundation resistance.
ret.resistance = {};
if (template.Resistance.Entity)
{
if (template.Resistance.Entity.Damage)
{
ret.resistance.Damage = {};
for (let damageType in template.Resistance.Entity.Damage)
ret.resistance.Damage[damageType] = getEntityValue("Resistance/Entity/Damage/" + damageType);
}
if (template.Resistance.Entity.Capture)
ret.resistance.Capture = getEntityValue("Resistance/Entity/Capture");
if (template.Resistance.Entity.ApplyStatus)
{
ret.resistance.ApplyStatus = {};
for (let statusEffect in template.Resistance.Entity.ApplyStatus)
ret.resistance.ApplyStatus[statusEffect] = {
"blockChance": getEntityValue("Resistance/Entity/ApplyStatus/" + statusEffect + "/BlockChance"),
"duration": getEntityValue("Resistance/Entity/ApplyStatus/" + statusEffect + "/Duration")
};
}
}
}
let getAttackEffects = (temp, path) => {
let effects = {};
if (temp.Capture)
effects.Capture = getEntityValue(path + "/Capture");
if (temp.Damage)
{
effects.Damage = {};
for (let damageType in temp.Damage)
effects.Damage[damageType] = getEntityValue(path + "/Damage/" + damageType);
}
if (temp.ApplyStatus)
effects.ApplyStatus = temp.ApplyStatus;
return effects;
};
if (template.Attack)
{
ret.attack = {};
for (let type in template.Attack)
{
let getAttackStat = function(stat) {
return getEntityValue("Attack/" + type + "/" + stat);
};
ret.attack[type] = {
"attackName": {
"name": template.Attack[type].AttackName._string || template.Attack[type].AttackName,
"context": template.Attack[type].AttackName["@context"]
},
"minRange": getAttackStat("MinRange"),
"maxRange": getAttackStat("MaxRange"),
"elevationBonus": getAttackStat("ElevationBonus")
};
ret.attack[type].elevationAdaptedRange = Math.sqrt(ret.attack[type].maxRange *
(2 * ret.attack[type].elevationBonus + ret.attack[type].maxRange));
ret.attack[type].repeatTime = getAttackStat("RepeatTime");
if (template.Attack[type].Projectile)
ret.attack[type].friendlyFire = template.Attack[type].Projectile.FriendlyFire == "true";
Object.assign(ret.attack[type], getAttackEffects(template.Attack[type], "Attack/" + type));
if (template.Attack[type].Splash)
{
ret.attack[type].splash = {
"friendlyFire": template.Attack[type].Splash.FriendlyFire != "false",
"shape": template.Attack[type].Splash.Shape,
};
Object.assign(ret.attack[type].splash, getAttackEffects(template.Attack[type].Splash, "Attack/" + type + "/Splash"));
}
}
}
if (template.DeathDamage)
{
ret.deathDamage = {
"friendlyFire": template.DeathDamage.FriendlyFire != "false",
};
Object.assign(ret.deathDamage, getAttackEffects(template.DeathDamage, "DeathDamage"));
}
if (template.Auras && auraTemplates)
{
ret.auras = {};
for (let auraID of template.Auras._string.split(/\s+/))
ret.auras[auraID] = GetAuraDataHelper(auraTemplates[auraID]);
}
if (template.BuildingAI)
ret.buildingAI = {
"defaultArrowCount": Math.round(getEntityValue("BuildingAI/DefaultArrowCount")),
"garrisonArrowMultiplier": getEntityValue("BuildingAI/GarrisonArrowMultiplier"),
"maxArrowCount": Math.round(getEntityValue("BuildingAI/MaxArrowCount"))
};
if (template.BuildRestrictions)
{
// required properties
ret.buildRestrictions = {
"placementType": template.BuildRestrictions.PlacementType,
"territory": template.BuildRestrictions.Territory,
"category": template.BuildRestrictions.Category,
};
// optional properties
if (template.BuildRestrictions.Distance)
{
ret.buildRestrictions.distance = {
"fromClass": template.BuildRestrictions.Distance.FromClass,
};
if (template.BuildRestrictions.Distance.MinDistance)
ret.buildRestrictions.distance.min = getEntityValue("BuildRestrictions/Distance/MinDistance");
if (template.BuildRestrictions.Distance.MaxDistance)
ret.buildRestrictions.distance.max = getEntityValue("BuildRestrictions/Distance/MaxDistance");
}
}
if (template.TrainingRestrictions)
{
ret.trainingRestrictions = {
"category": template.TrainingRestrictions.Category
};
if (template.TrainingRestrictions.MatchLimit)
ret.trainingRestrictions.matchLimit = +template.TrainingRestrictions.MatchLimit;
}
if (template.Cost)
{
ret.cost = {};
for (let resCode in template.Cost.Resources)
ret.cost[resCode] = getEntityValue("Cost/Resources/" + resCode);
if (template.Cost.Population)
ret.cost.population = getEntityValue("Cost/Population");
if (template.Cost.BuildTime)
ret.cost.time = getEntityValue("Cost/BuildTime");
}
if (template.Footprint)
{
ret.footprint = { "height": template.Footprint.Height };
if (template.Footprint.Square)
ret.footprint.square = {
"width": +template.Footprint.Square["@width"],
"depth": +template.Footprint.Square["@depth"]
};
else if (template.Footprint.Circle)
ret.footprint.circle = { "radius": +template.Footprint.Circle["@radius"] };
else
warn("GetTemplateDataHelper(): Unrecognized Footprint type");
}
if (template.Garrisonable)
ret.garrisonable = {
"size": getEntityValue("Garrisonable/Size")
};
if (template.GarrisonHolder)
{
ret.garrisonHolder = {
"buffHeal": getEntityValue("GarrisonHolder/BuffHeal")
};
if (template.GarrisonHolder.Max)
ret.garrisonHolder.capacity = getEntityValue("GarrisonHolder/Max");
}
if (template.Heal)
ret.heal = {
"health": getEntityValue("Heal/Health"),
"range": getEntityValue("Heal/Range"),
"interval": getEntityValue("Heal/Interval")
};
if (template.ResourceGatherer)
{
ret.resourceGatherRates = {};
let baseSpeed = getEntityValue("ResourceGatherer/BaseSpeed");
for (let type in template.ResourceGatherer.Rates)
ret.resourceGatherRates[type] = getEntityValue("ResourceGatherer/Rates/"+ type) * baseSpeed;
}
if (template.ResourceDropsite)
ret.resourceDropsite = {
"types": template.ResourceDropsite.Types.split(" ")
};
if (template.ResourceTrickle)
{
ret.resourceTrickle = {
"interval": +template.ResourceTrickle.Interval,
"rates": {}
};
for (let type in template.ResourceTrickle.Rates)
ret.resourceTrickle.rates[type] = getEntityValue("ResourceTrickle/Rates/" + type);
}
if (template.Loot)
{
ret.loot = {};
for (let type in template.Loot)
ret.loot[type] = getEntityValue("Loot/"+ type);
}
if (template.Obstruction)
{
ret.obstruction = {
"active": ("" + template.Obstruction.Active == "true"),
"blockMovement": ("" + template.Obstruction.BlockMovement == "true"),
"blockPathfinding": ("" + template.Obstruction.BlockPathfinding == "true"),
"blockFoundation": ("" + template.Obstruction.BlockFoundation == "true"),
"blockConstruction": ("" + template.Obstruction.BlockConstruction == "true"),
"disableBlockMovement": ("" + template.Obstruction.DisableBlockMovement == "true"),
"disableBlockPathfinding": ("" + template.Obstruction.DisableBlockPathfinding == "true"),
"shape": {}
};
if (template.Obstruction.Static)
{
ret.obstruction.shape.type = "static";
ret.obstruction.shape.width = +template.Obstruction.Static["@width"];
ret.obstruction.shape.depth = +template.Obstruction.Static["@depth"];
}
else if (template.Obstruction.Unit)
{
ret.obstruction.shape.type = "unit";
ret.obstruction.shape.radius = +template.Obstruction.Unit["@radius"];
}
else
ret.obstruction.shape.type = "cluster";
}
if (template.Pack)
ret.pack = {
"state": template.Pack.State,
"time": getEntityValue("Pack/Time"),
};
if (template.Population && template.Population.Bonus)
ret.population = {
"bonus": getEntityValue("Population/Bonus")
};
if (template.Health)
ret.health = Math.round(getEntityValue("Health/Max"));
if (template.Identity)
{
ret.selectionGroupName = template.Identity.SelectionGroupName;
ret.name = {
"specific": (template.Identity.SpecificName || template.Identity.GenericName),
"generic": template.Identity.GenericName
};
ret.icon = template.Identity.Icon;
ret.tooltip = template.Identity.Tooltip;
ret.requiredTechnology = template.Identity.RequiredTechnology;
ret.visibleIdentityClasses = GetVisibleIdentityClasses(template.Identity);
ret.nativeCiv = template.Identity.Civ;
}
if (template.UnitMotion)
{
ret.speed = {
"walk": getEntityValue("UnitMotion/WalkSpeed"),
};
ret.speed.run = getEntityValue("UnitMotion/WalkSpeed");
if (template.UnitMotion.RunMultiplier)
ret.speed.run *= getEntityValue("UnitMotion/RunMultiplier");
}
if (template.Upgrade)
{
ret.upgrades = [];
for (let upgradeName in template.Upgrade)
{
let upgrade = template.Upgrade[upgradeName];
let cost = {};
if (upgrade.Cost)
for (let res in upgrade.Cost)
cost[res] = getEntityValue("Upgrade/" + upgradeName + "/Cost/" + res, "Upgrade/Cost/" + res);
if (upgrade.Time)
cost.time = getEntityValue("Upgrade/" + upgradeName + "/Time", "Upgrade/Time");
ret.upgrades.push({
"entity": upgrade.Entity,
"tooltip": upgrade.Tooltip,
"cost": cost,
"icon": upgrade.Icon || undefined,
"requiredTechnology": upgrade.RequiredTechnology || undefined
});
}
}
if (template.ProductionQueue)
{
ret.techCostMultiplier = {};
for (let res in template.ProductionQueue.TechCostMultiplier)
ret.techCostMultiplier[res] = getEntityValue("ProductionQueue/TechCostMultiplier/" + res);
}
if (template.Trader)
ret.trader = {
"GainMultiplier": getEntityValue("Trader/GainMultiplier")
};
+ if (template.Treasure)
+ {
+ ret.treasure = {
+ "collectTime": getEntityValue("Treasure/CollectTime"),
+ "resources": {}
+ };
+ for (let resource in template.Treasure.Resources)
+ ret.treasure.resources[resource] = getEntityValue("Treasure/Resources/" + resource);
+ }
+
if (template.WallSet)
{
ret.wallSet = {
"templates": {
"tower": template.WallSet.Templates.Tower,
"gate": template.WallSet.Templates.Gate,
"fort": template.WallSet.Templates.Fort || "structures/" + template.Identity.Civ + "/fortress",
"long": template.WallSet.Templates.WallLong,
"medium": template.WallSet.Templates.WallMedium,
"short": template.WallSet.Templates.WallShort
},
"maxTowerOverlap": +template.WallSet.MaxTowerOverlap,
"minTowerOverlap": +template.WallSet.MinTowerOverlap
};
if (template.WallSet.Templates.WallEnd)
ret.wallSet.templates.end = template.WallSet.Templates.WallEnd;
if (template.WallSet.Templates.WallCurves)
ret.wallSet.templates.curves = template.WallSet.Templates.WallCurves.split(/\s+/);
}
if (template.WallPiece)
ret.wallPiece = {
"length": +template.WallPiece.Length,
"angle": +(template.WallPiece.Orientation || 1) * Math.PI,
"indent": +(template.WallPiece.Indent || 0),
"bend": +(template.WallPiece.Bend || 0) * Math.PI
};
return ret;
}
/**
* Get basic information about a technology template.
* @param {Object} template - A valid template as obtained by loading the tech JSON file.
* @param {string} civ - Civilization for which the tech requirements should be calculated.
*/
function GetTechnologyBasicDataHelper(template, civ)
{
return {
"name": {
"generic": template.genericName
},
"icon": template.icon ? "technologies/" + template.icon : undefined,
"description": template.description,
"reqs": DeriveTechnologyRequirements(template, civ),
"modifications": template.modifications,
"affects": template.affects,
"replaces": template.replaces
};
}
/**
* Get information about a technology template.
* @param {Object} template - A valid template as obtained by loading the tech JSON file.
* @param {string} civ - Civilization for which the specific name and tech requirements should be returned.
*/
function GetTechnologyDataHelper(template, civ, resources)
{
let ret = GetTechnologyBasicDataHelper(template, civ);
if (template.specificName)
ret.name.specific = template.specificName[civ] || template.specificName.generic;
ret.cost = { "time": template.researchTime ? +template.researchTime : 0 };
for (let type of resources.GetCodes())
ret.cost[type] = +(template.cost && template.cost[type] || 0);
ret.tooltip = template.tooltip;
ret.requirementsTooltip = template.requirementsTooltip || "";
return ret;
}
/**
* Get information about an aura template.
* @param {object} template - A valid template as obtained by loading the aura JSON file.
*/
function GetAuraDataHelper(template)
{
return {
"name": {
"generic": template.auraName,
},
"description": template.auraDescription || null,
"modifications": template.modifications,
"radius": template.radius || null,
};
}
function calculateCarriedResources(carriedResources, tradingGoods)
{
var resources = {};
if (carriedResources)
for (let resource of carriedResources)
resources[resource.type] = (resources[resource.type] || 0) + resource.amount;
if (tradingGoods && tradingGoods.amount)
resources[tradingGoods.type] =
(resources[tradingGoods.type] || 0) +
(tradingGoods.amount.traderGain || 0) +
(tradingGoods.amount.market1Gain || 0) +
(tradingGoods.amount.market2Gain || 0);
return resources;
}
/**
* Remove filter prefix (mirage, corpse, etc) from template name.
*
* ie. filter|dir/to/template -> dir/to/template
*/
function removeFiltersFromTemplateName(templateName)
{
return templateName.split("|").pop();
}
Index: ps/trunk/binaries/data/mods/public/gui/common/tooltips.js
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/common/tooltips.js (revision 24988)
+++ ps/trunk/binaries/data/mods/public/gui/common/tooltips.js (revision 24989)
@@ -1,1116 +1,1147 @@
var g_TooltipTextFormats = {
"unit": { "font": "sans-10", "color": "orange" },
"header": { "font": "sans-bold-13" },
"body": { "font": "sans-13" },
"comma": { "font": "sans-12" },
"nameSpecificBig": { "font": "sans-bold-16" },
"nameSpecificSmall": { "font": "sans-bold-12" },
"nameGeneric": { "font": "sans-bold-16" }
};
/**
* String of four spaces to be used as indentation in gui strings.
*/
var g_Indent = " ";
var g_DamageTypesMetadata = new DamageTypesMetadata();
var g_StatusEffectsMetadata = new StatusEffectsMetadata();
/**
* If true, always shows whether the splash damage deals friendly fire.
* Otherwise display the friendly fire tooltip only if it does.
*/
var g_AlwaysDisplayFriendlyFire = false;
function getCostTypes()
{
return g_ResourceData.GetCodes().concat(["population", "time"]);
}
function resourceIcon(resource)
{
return '[icon="icon_' + resource + '"]';
}
function resourceNameFirstWord(type)
{
return translateWithContext("firstWord", g_ResourceData.GetNames()[type]);
}
function resourceNameWithinSentence(type)
{
return translateWithContext("withinSentence", g_ResourceData.GetNames()[type]);
}
/**
* Format resource amounts to proper english and translate (for example: "200 food, 100 wood and 300 metal").
*/
function getLocalizedResourceAmounts(resources)
{
let amounts = g_ResourceData.GetCodes()
.filter(type => !!resources[type])
.map(type => sprintf(translate("%(amount)s %(resourceType)s"), {
"amount": resources[type],
"resourceType": resourceNameWithinSentence(type)
}));
if (amounts.length < 2)
return amounts.join();
let lastAmount = amounts.pop();
return sprintf(translate("%(previousAmounts)s and %(lastAmount)s"), {
// Translation: This comma is used for separating first to penultimate elements in an enumeration.
"previousAmounts": amounts.join(translate(", ")),
"lastAmount": lastAmount
});
}
function bodyFont(text)
{
return setStringTags(text, g_TooltipTextFormats.body);
}
function headerFont(text)
{
return setStringTags(text, g_TooltipTextFormats.header);
}
function unitFont(text)
{
return setStringTags(text, g_TooltipTextFormats.unit);
}
function commaFont(text)
{
return setStringTags(text, g_TooltipTextFormats.comma);
}
function getSecondsString(seconds)
{
return sprintf(translatePlural("%(time)s %(second)s", "%(time)s %(second)s", seconds), {
"time": seconds,
"second": unitFont(translatePlural("second", "seconds", seconds))
});
}
/**
* Entity templates have a `Tooltip` tag in the Identity component.
* (The contents of which are copied to a `tooltip` attribute in globalscripts.)
*
* Technologies have a `tooltip` attribute.
*/
function getEntityTooltip(template)
{
if (!template.tooltip)
return "";
return bodyFont(template.tooltip);
}
/**
* Technologies have a `description` attribute, and Auras have an `auraDescription`
* attribute, which becomes `description`.
*
* (For technologies, this happens in globalscripts.)
*
* (For auras, this happens either in the Auras component (for session gui) or
* reference/common/load.js (for Reference Suite gui))
*/
function getDescriptionTooltip(template)
{
if (!template.description)
return "";
return bodyFont(template.description);
}
/**
* Entity templates have a `History` tag in the Identity component.
* (The contents of which are copied to a `history` attribute in globalscripts.)
*/
function getHistoryTooltip(template)
{
if (!template.history)
return "";
return bodyFont(template.history);
}
function getHealthTooltip(template)
{
if (!template.health)
return "";
return sprintf(translate("%(label)s %(details)s"), {
"label": headerFont(translate("Health:")),
"details": template.health
});
}
function getCurrentHealthTooltip(entState, label)
{
if (!entState.maxHitpoints)
return "";
return sprintf(translate("%(healthLabel)s %(current)s / %(max)s"), {
"healthLabel": headerFont(label || translate("Health:")),
"current": Math.round(entState.hitpoints),
"max": Math.round(entState.maxHitpoints)
});
}
function getCurrentCaptureTooltip(entState, label)
{
if (!entState.maxCapturePoints)
return "";
return sprintf(translate("%(captureLabel)s %(current)s / %(max)s"), {
"captureLabel": headerFont(label || translate("Capture points:")),
"current": Math.round(entState.capturePoints[entState.player]),
"max": Math.round(entState.maxCapturePoints)
});
}
/**
* Converts an resistance level into the actual reduction percentage.
*/
function resistanceLevelToPercentageString(level)
{
return sprintf(translate("%(percentage)s%%"), {
"percentage": (100 - Math.round(Math.pow(0.9, level) * 100))
});
}
function getResistanceTooltip(template)
{
if (!template.resistance)
return "";
let details = [];
if (template.resistance.Damage)
details.push(getDamageResistanceTooltip(template.resistance.Damage));
if (template.resistance.Capture)
details.push(getCaptureResistanceTooltip(template.resistance.Capture));
if (template.resistance.ApplyStatus)
details.push(getStatusEffectsResistanceTooltip(template.resistance.ApplyStatus));
return details.length ? sprintf(translate("%(label)s\n%(details)s"), {
"label": headerFont(translate("Resistance:")),
"details": g_Indent + details.join("\n" + g_Indent)
}) : "";
}
function getDamageResistanceTooltip(resistanceTypeTemplate)
{
if (!resistanceTypeTemplate)
return "";
return sprintf(translate("%(label)s %(details)s"), {
"label": headerFont(translate("Damage:")),
"details":
g_DamageTypesMetadata.sort(Object.keys(resistanceTypeTemplate)).map(
dmgType => sprintf(translate("%(damage)s %(damageType)s %(resistancePercentage)s"), {
"damage": resistanceTypeTemplate[dmgType].toFixed(1),
"damageType": unitFont(translateWithContext("damage type", g_DamageTypesMetadata.getName(dmgType))),
"resistancePercentage":
'[font="sans-10"]' +
sprintf(translate("(%(resistancePercentage)s)"), {
"resistancePercentage": resistanceLevelToPercentageString(resistanceTypeTemplate[dmgType])
}) + '[/font]'
})
).join(commaFont(translate(", ")))
});
}
function getCaptureResistanceTooltip(resistanceTypeTemplate)
{
if (!resistanceTypeTemplate)
return "";
return sprintf(translate("%(label)s %(details)s"), {
"label": headerFont(translate("Capture:")),
"details":
sprintf(translate("%(damage)s %(damageType)s %(resistancePercentage)s"), {
"damage": resistanceTypeTemplate.toFixed(1),
"damageType": unitFont(translateWithContext("damage type", "Capture")),
"resistancePercentage":
'[font="sans-10"]' +
sprintf(translate("(%(resistancePercentage)s)"), {
"resistancePercentage": resistanceLevelToPercentageString(resistanceTypeTemplate)
}) + '[/font]'
})
});
}
function getStatusEffectsResistanceTooltip(resistanceTypeTemplate)
{
if (!resistanceTypeTemplate)
return "";
return sprintf(translate("%(label)s %(details)s"), {
"label": headerFont(translate("Status Effects:")),
"details":
Object.keys(resistanceTypeTemplate).map(
statusEffect => {
if (resistanceTypeTemplate[statusEffect].blockChance == 1)
return sprintf(translate("Blocks %(name)s"), {
"name": unitFont(translateWithContext("status effect", g_StatusEffectsMetadata.getName(statusEffect)))
});
if (resistanceTypeTemplate[statusEffect].blockChance == 0)
return sprintf(translate("%(name)s %(details)s"), {
"name": unitFont(translateWithContext("status effect", g_StatusEffectsMetadata.getName(statusEffect))),
"details": sprintf(translate("Duration reduction: %(durationReduction)s%%"), {
"durationReduction": (100 - resistanceTypeTemplate[statusEffect].duration * 100)
})
});
if (resistanceTypeTemplate[statusEffect].duration == 1)
return sprintf(translate("%(name)s %(details)s"), {
"name": unitFont(translateWithContext("status effect", g_StatusEffectsMetadata.getName(statusEffect))),
"details": sprintf(translate("Blocks: %(blockPercentage)s%%"), {
"blockPercentage": resistanceTypeTemplate[statusEffect].blockChance * 100
})
});
return sprintf(translate("%(name)s %(details)s"), {
"name": unitFont(translateWithContext("status effect", g_StatusEffectsMetadata.getName(statusEffect))),
"details": sprintf(translate("Blocks: %(blockPercentage)s%%, Duration reduction: %(durationReduction)s%%"), {
"blockPercentage": resistanceTypeTemplate[statusEffect].blockChance * 100,
"durationReduction": (100 - resistanceTypeTemplate[statusEffect].duration * 100)
})
});
}
).join(commaFont(translate(", ")))
});
}
function attackRateDetails(interval, projectiles)
{
if (!interval)
return "";
if (projectiles === 0)
return translate("Garrison to fire arrows");
let attackRateString = getSecondsString(interval / 1000);
let header = headerFont(translate("Interval:"));
if (projectiles && +projectiles > 1)
{
header = headerFont(translate("Rate:"));
let projectileString = sprintf(translatePlural("%(projectileCount)s %(projectileName)s", "%(projectileCount)s %(projectileName)s", projectiles), {
"projectileCount": projectiles,
"projectileName": unitFont(translatePlural("arrow", "arrows", projectiles))
});
attackRateString = sprintf(translate("%(projectileString)s / %(attackRateString)s"), {
"projectileString": projectileString,
"attackRateString": attackRateString
});
}
return sprintf(translate("%(label)s %(details)s"), {
"label": header,
"details": attackRateString
});
}
function rangeDetails(attackTypeTemplate)
{
if (!attackTypeTemplate.maxRange)
return "";
let rangeTooltipString = {
"relative": {
// Translation: For example: Range: 2 to 10 (+2) meters
"minRange": translate("%(rangeLabel)s %(minRange)s to %(maxRange)s (%(relativeRange)s) %(rangeUnit)s"),
// Translation: For example: Range: 10 (+2) meters
"no-minRange": translate("%(rangeLabel)s %(maxRange)s (%(relativeRange)s) %(rangeUnit)s"),
},
"non-relative": {
// Translation: For example: Range: 2 to 10 meters
"minRange": translate("%(rangeLabel)s %(minRange)s to %(maxRange)s %(rangeUnit)s"),
// Translation: For example: Range: 10 meters
"no-minRange": translate("%(rangeLabel)s %(maxRange)s %(rangeUnit)s"),
}
};
let minRange = Math.round(attackTypeTemplate.minRange);
let maxRange = Math.round(attackTypeTemplate.maxRange);
let realRange = attackTypeTemplate.elevationAdaptedRange;
let relativeRange = realRange ? Math.round(realRange - maxRange) : 0;
return sprintf(rangeTooltipString[relativeRange ? "relative" : "non-relative"][minRange ? "minRange" : "no-minRange"], {
"rangeLabel": headerFont(translate("Range:")),
"minRange": minRange,
"maxRange": maxRange,
"relativeRange": relativeRange > 0 ? sprintf(translate("+%(number)s"), { "number": relativeRange }) : relativeRange,
"rangeUnit":
unitFont(minRange || relativeRange ?
// Translation: For example "0.5 to 1 meters", "1 (+1) meters" or "1 to 2 (+3) meters"
translate("meters") :
translatePlural("meter", "meters", maxRange))
});
}
function damageDetails(damageTemplate)
{
if (!damageTemplate)
return "";
return g_DamageTypesMetadata.sort(Object.keys(damageTemplate).filter(dmgType => damageTemplate[dmgType])).map(
dmgType => sprintf(translate("%(damage)s %(damageType)s"), {
"damage": (+damageTemplate[dmgType]).toFixed(1),
"damageType": unitFont(translateWithContext("damage type", g_DamageTypesMetadata.getName(dmgType)))
})).join(commaFont(translate(", ")));
}
function captureDetails(captureTemplate)
{
if (!captureTemplate)
return "";
return sprintf(translate("%(amount)s %(name)s"), {
"amount": (+captureTemplate).toFixed(1),
"name": unitFont(translateWithContext("damage type", "Capture"))
});
}
function splashDetails(splashTemplate)
{
let splashLabel = sprintf(headerFont(translate("%(splashShape)s Splash")), {
"splashShape": splashTemplate.shape
});
let splashDamageTooltip = sprintf(translate("%(label)s: %(effects)s"), {
"label": splashLabel,
"effects": attackEffectsDetails(splashTemplate)
});
if (g_AlwaysDisplayFriendlyFire || splashTemplate.friendlyFire)
splashDamageTooltip += commaFont(translate(", ")) + sprintf(translate("Friendly Fire: %(enabled)s"), {
"enabled": splashTemplate.friendlyFire ? translate("Yes") : translate("No")
});
return splashDamageTooltip;
}
function applyStatusDetails(applyStatusTemplate)
{
if (!applyStatusTemplate)
return "";
return sprintf(translate("gives %(name)s"), {
"name": Object.keys(applyStatusTemplate).map(x =>
unitFont(translateWithContext("status effect", g_StatusEffectsMetadata.getName(x)))
).join(commaFont(translate(", "))),
});
}
function attackEffectsDetails(attackTypeTemplate)
{
if (!attackTypeTemplate)
return "";
let effects = [
captureDetails(attackTypeTemplate.Capture || undefined),
damageDetails(attackTypeTemplate.Damage || undefined),
applyStatusDetails(attackTypeTemplate.ApplyStatus || undefined)
];
return effects.filter(effect => effect).join(commaFont(translate(", ")));
}
function getAttackTooltip(template)
{
if (!template.attack)
return "";
let tooltips = [];
for (let attackType in template.attack)
{
// Slaughter is used to kill animals, so do not show it.
if (attackType == "Slaughter")
continue;
let attackTypeTemplate = template.attack[attackType];
let attackLabel = sprintf(headerFont(translate("%(attackType)s")), {
"attackType": attackTypeTemplate.attackName.context ?
translateWithContext(attackTypeTemplate.attackName.context || "Name of an attack, usually the weapon.", attackTypeTemplate.attackName.name) :
translate(attackTypeTemplate.attackName.name)
});
let projectiles;
// Use either current rate from simulation or default count if the sim is not running.
// TODO: This ought to be extended to include units which fire multiple projectiles.
if (template.buildingAI)
projectiles = template.buildingAI.arrowCount || template.buildingAI.defaultArrowCount;
let splashTemplate = attackTypeTemplate.splash;
// Show the effects of status effects below.
let statusEffectsDetails = [];
if (attackTypeTemplate.ApplyStatus)
for (let status in attackTypeTemplate.ApplyStatus)
statusEffectsDetails.push("\n" + g_Indent + g_Indent + getStatusEffectsTooltip(status, attackTypeTemplate.ApplyStatus[status], true));
statusEffectsDetails = statusEffectsDetails.join("");
tooltips.push(sprintf(translate("%(attackLabel)s: %(effects)s, %(range)s, %(rate)s%(statusEffects)s%(splash)s"), {
"attackLabel": attackLabel,
"effects": attackEffectsDetails(attackTypeTemplate),
"range": rangeDetails(attackTypeTemplate),
"rate": attackRateDetails(attackTypeTemplate.repeatTime, projectiles),
"splash": splashTemplate ? "\n" + g_Indent + g_Indent + splashDetails(splashTemplate) : "",
"statusEffects": statusEffectsDetails
}));
}
return sprintf(translate("%(label)s\n%(details)s"), {
"label": headerFont(translate("Attack:")),
"details": g_Indent + tooltips.join("\n" + g_Indent)
});
}
/**
* @param applier - if true, return the tooltip for the Applier. If false, Receiver is returned.
*/
function getStatusEffectsTooltip(statusCode, template, applier)
{
let tooltipAttributes = [];
let statusData = g_StatusEffectsMetadata.getData(statusCode);
if (template.Damage || template.Capture)
tooltipAttributes.push(attackEffectsDetails(template));
if (template.Interval)
tooltipAttributes.push(attackRateDetails(+template.Interval));
if (template.Duration)
tooltipAttributes.push(getStatusEffectDurationTooltip(template));
if (applier && statusData.applierTooltip)
tooltipAttributes.push(translate(statusData.applierTooltip));
else if (!applier && statusData.receiverTooltip)
tooltipAttributes.push(translate(statusData.receiverTooltip));
if (applier)
return sprintf(translate("%(statusName)s: %(statusInfo)s %(stackability)s"), {
"statusName": headerFont(translateWithContext("status effect", statusData.statusName)),
"statusInfo": tooltipAttributes.join(commaFont(translate(", "))),
"stackability": getStatusEffectStackabilityTooltip(template)
});
return sprintf(translate("%(statusName)s: %(statusInfo)s"), {
"statusName": headerFont(translateWithContext("status effect", statusData.statusName)),
"statusInfo": tooltipAttributes.join(commaFont(translate(", ")))
});
}
function getStatusEffectDurationTooltip(template)
{
if (!template.Duration)
return "";
return sprintf(translate("%(durName)s: %(duration)s"), {
"durName": headerFont(translate("Duration")),
"duration": getSecondsString((template._timeElapsed ?
+template.Duration - template._timeElapsed :
+template.Duration) / 1000)
});
}
function getStatusEffectStackabilityTooltip(template)
{
if (!template.Stackability || template.Stackability == "Ignore")
return "";
let stackabilityString = "";
if (template.Stackability === "Extend")
stackabilityString = translateWithContext("status effect stackability", "(extends)");
else if (template.Stackability === "Replace")
stackabilityString = translateWithContext("status effect stackability", "(replaces)");
else if (template.Stackability === "Stack")
stackabilityString = translateWithContext("status effect stackability", "(stacks)");
return sprintf(translate("%(stackability)s"), {
"stackability": stackabilityString
});
}
function getGarrisonTooltip(template)
{
let tooltips = [];
if (template.garrisonHolder)
{
tooltips.push (
sprintf(translate("%(label)s: %(garrisonLimit)s"), {
"label": headerFont(translate("Garrison Limit")),
"garrisonLimit": template.garrisonHolder.capacity
})
);
if (template.garrisonHolder.buffHeal)
tooltips.push(
sprintf(translate("%(healRateLabel)s %(value)s %(health)s / %(second)s"), {
"healRateLabel": headerFont(translate("Heal:")),
"value": Math.round(template.garrisonHolder.buffHeal),
"health": unitFont(translate("Health")),
"second": unitFont(translate("second")),
})
);
tooltips.join(commaFont(translate(", ")));
}
if (template.garrisonable)
{
let extraSize;
if (template.garrisonHolder)
extraSize = template.garrisonHolder.occupiedSlots;
if (template.garrisonable.size > 1 || extraSize)
tooltips.push (
sprintf(translate("%(label)s: %(garrisonSize)s %(extraSize)s"), {
"label": headerFont(translate("Garrison Size")),
"garrisonSize": template.garrisonable.size,
"extraSize": extraSize ?
translateWithContext("nested garrison", "+ ") + extraSize : ""
})
);
}
return tooltips.join("\n");
}
function getProjectilesTooltip(template)
{
if (!template.garrisonHolder || !template.buildingAI)
return "";
let limit = Math.min(
template.buildingAI.maxArrowCount || Infinity,
template.buildingAI.defaultArrowCount +
Math.round(template.buildingAI.garrisonArrowMultiplier *
template.garrisonHolder.capacity)
);
if (!limit)
return "";
return [
sprintf(translate("%(label)s: %(value)s"), {
"label": headerFont(translate("Projectile Limit")),
"value": limit
}),
sprintf(translate("%(label)s: %(value)s"), {
"label": headerFont(translateWithContext("projectiles", "Default")),
"value": template.buildingAI.defaultArrowCount
}),
sprintf(translate("%(label)s: %(value)s"), {
"label": headerFont(translateWithContext("projectiles", "Per Unit")),
"value": +template.buildingAI.garrisonArrowMultiplier.toFixed(2)
})
].join(commaFont(translate(", ")));
}
function getRepairTimeTooltip(entState)
{
let result = [];
result.push(sprintf(translate("%(label)s %(details)s"), {
"label": headerFont(translate("Number of repairers:")),
"details": entState.repairable.numBuilders
}));
if (entState.repairable.numBuilders)
{
result.push(sprintf(translate("%(label)s %(details)s"), {
"label": headerFont(translate("Remaining repair time:")),
"details": getSecondsString(Math.floor(entState.repairable.buildTime.timeRemaining))
}));
let timeReduction = Math.round(entState.repairable.buildTime.timeRemaining - entState.repairable.buildTime.timeRemainingNew);
result.push(sprintf(translatePlural(
"Add another worker to speed up the repairs by %(second)s second.",
"Add another worker to speed up the repairs by %(second)s seconds.",
timeReduction),
{
"second": timeReduction
}));
}
else
result.push(sprintf(translatePlural(
"Add a worker to finish the repairs in %(second)s second.",
"Add a worker to finish the repairs in %(second)s seconds.",
Math.round(entState.repairable.buildTime.timeRemainingNew)),
{
"second": Math.round(entState.repairable.buildTime.timeRemainingNew)
}));
return result.join("\n");
}
function getBuildTimeTooltip(entState)
{
let result = [];
result.push(sprintf(translate("%(label)s %(details)s"), {
"label": headerFont(translate("Number of builders:")),
"details": entState.foundation.numBuilders
}));
if (entState.foundation.numBuilders)
{
result.push(sprintf(translate("%(label)s %(details)s"), {
"label": headerFont(translate("Remaining build time:")),
"details": getSecondsString(Math.floor(entState.foundation.buildTime.timeRemaining))
}));
let timeReduction = Math.round(entState.foundation.buildTime.timeRemaining - entState.foundation.buildTime.timeRemainingNew);
result.push(sprintf(translatePlural(
"Add another worker to speed up the construction by %(second)s second.",
"Add another worker to speed up the construction by %(second)s seconds.",
timeReduction),
{
"second": timeReduction
}));
}
else
result.push(sprintf(translatePlural(
"Add a worker to finish the construction in %(second)s second.",
"Add a worker to finish the construction in %(second)s seconds.",
Math.round(entState.foundation.buildTime.timeRemainingNew)),
{
"second": Math.round(entState.foundation.buildTime.timeRemainingNew)
}));
return result.join("\n");
}
/**
* Multiplies the costs for a template by a given batch size.
*/
function multiplyEntityCosts(template, trainNum)
{
let totalCosts = {};
for (let r of getCostTypes())
if (template.cost[r])
totalCosts[r] = Math.floor(template.cost[r] * trainNum);
return totalCosts;
}
/**
* Helper function for getEntityCostTooltip.
*/
function getEntityCostComponentsTooltipString(template, entity, buildingsCountToTrainFullBatch = 1, fullBatchSize = 1, remainderBatch = 0)
{
if (!template.cost)
return [];
let totalCosts = multiplyEntityCosts(template, buildingsCountToTrainFullBatch * fullBatchSize + remainderBatch);
if (template.cost.time)
totalCosts.time = Math.ceil(template.cost.time * (entity ? Engine.GuiInterfaceCall("GetBatchTime", {
"entity": entity,
"batchSize": buildingsCountToTrainFullBatch > 0 ? fullBatchSize : remainderBatch
}) : 1));
let costs = [];
for (let type of getCostTypes())
if (totalCosts[type])
costs.push(sprintf(translate("%(component)s %(cost)s"), {
"component": resourceIcon(type),
"cost": totalCosts[type]
}));
return costs;
}
function getGatherTooltip(template)
{
if (!template.resourceGatherRates)
return "";
let rates = {};
for (let resource of g_ResourceData.GetResources())
{
let types = [resource.code];
for (let subtype in resource.subtypes)
{
// We ignore ruins as those are not that common
if (subtype == "ruins")
continue;
let rate = template.resourceGatherRates[resource.code + "." + subtype];
if (rate > 0)
rates[resource.code + "_" + subtype] = rate;
}
}
if (!Object.keys(rates).length)
return "";
return sprintf(translate("%(label)s %(details)s"), {
"label": headerFont(translate("Gather Rates:")),
"details":
Object.keys(rates).map(
type => sprintf(translate("%(resourceIcon)s %(rate)s"), {
"resourceIcon": resourceIcon(type),
"rate": rates[type].toFixed(2)
})
).join(" ")
});
}
/**
* Returns the resources this entity supplies in the specified entity's tooltip
*/
function getResourceSupplyTooltip(template)
{
if (!template.supply)
return "";
let supply = template.supply;
- let type = supply.type[0] == "treasure" ? supply.type[1] : supply.type[0];
-
// Translation: Label in tooltip showing the resource type and quantity of a given resource supply.
return sprintf(translate("%(label)s %(component)s %(amount)s"), {
"label": headerFont(translate("Resource Supply:")),
- "component": resourceIcon(type),
+ "component": resourceIcon(supply.type[0]),
// Translation: Marks that a resource supply entity has an unending, infinite, supply of its resource.
"amount": Number.isFinite(+supply.amount) ? supply.amount : translate("∞")
});
}
+/**
+ * @param {Object} template - The entity's template.
+ * @return {string} - The resources this entity rewards to a collecter.
+ */
+function getTreasureTooltip(template)
+{
+ if (!template.treasure)
+ return "";
+
+ let resources = {};
+ for (let resource of g_ResourceData.GetResources())
+ {
+ let type = resource.code;
+ if (template.treasure.resources[type])
+ resources[type] = template.treasure.resources[type];
+ }
+
+ let resourceNames = Object.keys(resources);
+ if (!resourceNames.length)
+ return "";
+
+ return sprintf(translate("%(label)s %(details)s"), {
+ "label": headerFont(translate("Reward:")),
+ "details":
+ resourceNames.map(
+ type => sprintf(translate("%(resourceIcon)s %(reward)s"), {
+ "resourceIcon": resourceIcon(type),
+ "reward": resources[type]
+ })
+ ).join(" ")
+ });
+}
+
function getResourceTrickleTooltip(template)
{
if (!template.resourceTrickle)
return "";
let resCodes = g_ResourceData.GetCodes().filter(res => !!template.resourceTrickle.rates[res]);
if (!resCodes.length)
return "";
return sprintf(translate("%(label)s %(details)s"), {
"label": headerFont(translate("Resource Trickle:")),
"details": sprintf(translate("%(resources)s / %(time)s"), {
"resources":
resCodes.map(
res => sprintf(translate("%(resourceIcon)s %(rate)s"), {
"resourceIcon": resourceIcon(res),
"rate": template.resourceTrickle.rates[res]
})
).join(" "),
"time": getSecondsString(template.resourceTrickle.interval / 1000)
})
});
}
/**
* Returns an array of strings for a set of wall pieces. If the pieces share
* resource type requirements, output will be of the form '10 to 30 Stone',
* otherwise output will be, e.g. '10 Stone, 20 Stone, 30 Stone'.
*/
function getWallPieceTooltip(wallTypes)
{
let out = [];
let resourceCount = {};
for (let resource of getCostTypes())
if (wallTypes[0].cost[resource])
resourceCount[resource] = [wallTypes[0].cost[resource]];
let sameTypes = true;
for (let i = 1; i < wallTypes.length; ++i)
{
for (let resource in wallTypes[i].cost)
// Break out of the same-type mode if this wall requires
// resource types that the first didn't.
if (wallTypes[i].cost[resource] && !resourceCount[resource])
{
sameTypes = false;
break;
}
for (let resource in resourceCount)
if (wallTypes[i].cost[resource])
resourceCount[resource].push(wallTypes[i].cost[resource]);
else
{
sameTypes = false;
break;
}
}
if (sameTypes)
for (let resource in resourceCount)
// Translation: This string is part of the resources cost string on
// the tooltip for wall structures.
out.push(sprintf(translate("%(resourceIcon)s %(minimum)s to %(resourceIcon)s %(maximum)s"), {
"resourceIcon": resourceIcon(resource),
"minimum": Math.min.apply(Math, resourceCount[resource]),
"maximum": Math.max.apply(Math, resourceCount[resource])
}));
else
for (let i = 0; i < wallTypes.length; ++i)
out.push(getEntityCostComponentsTooltipString(wallTypes[i]).join(", "));
return out;
}
/**
* Returns the cost information to display in the specified entity's construction button tooltip.
*/
function getEntityCostTooltip(template, player, entity, buildingsCountToTrainFullBatch, fullBatchSize, remainderBatch)
{
// Entities with a wallset component are proxies for initiating wall placement and as such do not have a cost of
// their own; the individual wall pieces within it do.
if (template.wallSet)
{
let templateLong = GetTemplateData(template.wallSet.templates.long, player);
let templateMedium = GetTemplateData(template.wallSet.templates.medium, player);
let templateShort = GetTemplateData(template.wallSet.templates.short, player);
let templateTower = GetTemplateData(template.wallSet.templates.tower, player);
let wallCosts = getWallPieceTooltip([templateShort, templateMedium, templateLong]);
let towerCosts = getEntityCostComponentsTooltipString(templateTower);
return sprintf(translate("Walls: %(costs)s"), { "costs": wallCosts.join(" ") }) + "\n" +
sprintf(translate("Towers: %(costs)s"), { "costs": towerCosts.join(" ") });
}
if (template.cost)
{
let costs = getEntityCostComponentsTooltipString(template, entity, buildingsCountToTrainFullBatch, fullBatchSize, remainderBatch).join(" ");
if (costs)
// Translation: Label in tooltip showing cost of a unit, structure or technology.
return sprintf(translate("%(label)s %(costs)s"), {
"label": headerFont(translate("Cost:")),
"costs": costs
});
}
return "";
}
function getRequiredTechnologyTooltip(technologyEnabled, requiredTechnology, civ)
{
if (technologyEnabled)
return "";
return sprintf(translate("Requires %(technology)s"), {
"technology": getEntityNames(GetTechnologyData(requiredTechnology, civ))
});
}
/**
* Returns the population bonus information to display in the specified entity's construction button tooltip.
*/
function getPopulationBonusTooltip(template)
{
if (!template.population || !template.population.bonus)
return "";
return sprintf(translate("%(label)s %(bonus)s"), {
"label": headerFont(translate("Population Bonus:")),
"bonus": template.population.bonus
});
}
/**
* Returns a message with the amount of each resource needed to create an entity.
*/
function getNeededResourcesTooltip(resources)
{
if (!resources)
return "";
let formatted = [];
for (let resource in resources)
formatted.push(sprintf(translate("%(component)s %(cost)s"), {
"component": '[font="sans-12"]' + resourceIcon(resource) + '[/font]',
"cost": resources[resource]
}));
return coloredText(
'[font="sans-bold-13"]' + translate("Insufficient resources:") + '[/font]',
"red") + " " +
formatted.join(" ");
}
function getSpeedTooltip(template)
{
if (!template.speed)
return "";
let walk = template.speed.walk.toFixed(1);
let run = template.speed.run.toFixed(1);
if (walk == 0 && run == 0)
return "";
return sprintf(translate("%(label)s %(speeds)s"), {
"label": headerFont(translate("Speed:")),
"speeds":
sprintf(translate("%(speed)s %(movementType)s"), {
"speed": walk,
"movementType": unitFont(translate("Walk"))
}) +
commaFont(translate(", ")) +
sprintf(translate("%(speed)s %(movementType)s"), {
"speed": run,
"movementType": unitFont(translate("Run"))
})
});
}
function getHealerTooltip(template)
{
if (!template.heal)
return "";
let health = +(template.heal.health.toFixed(1));
let range = +(template.heal.range.toFixed(0));
let interval = +((template.heal.interval / 1000).toFixed(1));
return [
sprintf(translatePlural("%(label)s %(val)s %(unit)s", "%(label)s %(val)s %(unit)s", health), {
"label": headerFont(translate("Heal:")),
"val": health,
"unit": unitFont(translatePlural("Health", "Health", health))
}),
sprintf(translatePlural("%(label)s %(val)s %(unit)s", "%(label)s %(val)s %(unit)s", range), {
"label": headerFont(translate("Range:")),
"val": range,
"unit": unitFont(translatePlural("meter", "meters", range))
}),
sprintf(translatePlural("%(label)s %(val)s %(unit)s", "%(label)s %(val)s %(unit)s", interval), {
"label": headerFont(translate("Interval:")),
"val": interval,
"unit": unitFont(translatePlural("second", "seconds", interval))
})
].join(translate(", "));
}
function getAurasTooltip(template)
{
let auras = template.auras || template.wallSet && GetTemplateData(template.wallSet.templates.long).auras;
if (!auras)
return "";
let tooltips = [];
for (let auraID in auras)
{
let tooltip = sprintf(translate("%(auralabel)s %(aurainfo)s"), {
"auralabel": headerFont(sprintf(translate("%(auraname)s:"), {
"auraname": getEntityNames(auras[auraID])
})),
"aurainfo": bodyFont(translate(auras[auraID].description))
});
let radius = +auras[auraID].radius;
if (radius)
tooltip += " " + sprintf(translatePlural("%(label)s %(val)s %(unit)s", "%(label)s %(val)s %(unit)s", radius), {
"label": translateWithContext("aura", "Range:"),
"val": radius,
"unit": unitFont(translatePlural("meter", "meters", radius))
});
tooltips.push(tooltip);
}
return tooltips.join("\n");
}
function getEntityNames(template)
{
if (!template.name.specific)
return template.name.generic;
if (template.name.specific == template.name.generic)
return template.name.specific;
return sprintf(translate("%(specificName)s (%(genericName)s)"), {
"specificName": template.name.specific,
"genericName": template.name.generic
});
}
function getEntityNamesFormatted(template)
{
if (!template.name.specific)
return setStringTags(template.name.generic, g_TooltipTextFormats.nameSpecificBig);
// Translation: Example: "Epibátēs Athēnaîos [font="sans-bold-16"](Athenian Marine)[/font]"
return sprintf(translate("%(specificName)s %(fontStart)s(%(genericName)s)%(fontEnd)s"), {
"specificName": getEntitySpecificNameFormatted(template),
"genericName": template.name.generic,
"fontStart": '[font="' + g_TooltipTextFormats.nameGeneric.font + '"]',
"fontEnd": '[/font]'
});
}
function getEntitySpecificNameFormatted(template)
{
if (!template.name.specific)
return setStringTags(template.name.generic, g_TooltipTextFormats.nameSpecificBig);
return setStringTags(template.name.specific[0], g_TooltipTextFormats.nameSpecificBig) +
setStringTags(template.name.specific.slice(1).toUpperCase(), g_TooltipTextFormats.nameSpecificSmall);
}
function getVisibleEntityClassesFormatted(template)
{
if (!template.visibleIdentityClasses || !template.visibleIdentityClasses.length)
return "";
return headerFont(translate("Classes:")) + ' ' +
bodyFont(template.visibleIdentityClasses.map(c => translate(c)).join(translate(", ")));
}
function getLootTooltip(template)
{
if (!template.loot && !template.resourceCarrying)
return "";
let resourcesCarried = [];
if (template.resourceCarrying)
resourcesCarried = calculateCarriedResources(
template.resourceCarrying,
template.trader && template.trader.goods
);
let lootLabels = [];
for (let type of g_ResourceData.GetCodes().concat(["xp"]))
{
let loot =
(template.loot && template.loot[type] || 0) +
(resourcesCarried[type] || 0);
if (!loot)
continue;
// Translation: %(component) will be the icon for the loot type and %(loot) will be the value.
lootLabels.push(sprintf(translate("%(component)s %(loot)s"), {
"component": resourceIcon(type),
"loot": loot
}));
}
if (!lootLabels.length)
return "";
return sprintf(translate("%(label)s %(details)s"), {
"label": headerFont(translate("Loot:")),
"details": lootLabels.join(" ")
});
}
function getResourceDropsiteTooltip(template)
{
if (!template || !template.resourceDropsite || !template.resourceDropsite.types)
return "";
return sprintf(translate("%(label)s %(icons)s"), {
"label": headerFont(translate("Dropsite for:")),
"icons": template.resourceDropsite.types.map(type => resourceIcon(type)).join(" ")
});
}
function showTemplateViewerOnRightClickTooltip()
{
// Translation: Appears in a tooltip to indicate that right-clicking the corresponding GUI element will open the Template Details GUI page.
return translate("Right-click to view more information.");
}
function showTemplateViewerOnClickTooltip()
{
// Translation: Appears in a tooltip to indicate that clicking the corresponding GUI element will open the Template Details GUI page.
return translate("Click to view more information.");
}
Index: ps/trunk/binaries/data/mods/public/gui/session/selection_details.js
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/session/selection_details.js (revision 24988)
+++ ps/trunk/binaries/data/mods/public/gui/session/selection_details.js (revision 24989)
@@ -1,571 +1,564 @@
function layoutSelectionSingle()
{
Engine.GetGUIObjectByName("detailsAreaSingle").hidden = false;
Engine.GetGUIObjectByName("detailsAreaMultiple").hidden = true;
}
function layoutSelectionMultiple()
{
Engine.GetGUIObjectByName("detailsAreaMultiple").hidden = false;
Engine.GetGUIObjectByName("detailsAreaSingle").hidden = true;
}
-function getResourceTypeDisplayName(resourceType)
-{
- return resourceNameFirstWord(
- resourceType.generic == "treasure" ?
- resourceType.specific :
- resourceType.generic);
-}
-
// Updates the health bar of garrisoned units
function updateGarrisonHealthBar(entState, selection)
{
if (!entState.garrisonHolder)
return;
// Summing up the Health of every single unit
let totalGarrisonHealth = 0;
let maxGarrisonHealth = 0;
for (let selEnt of selection)
{
let selEntState = GetEntityState(selEnt);
if (selEntState.garrisonHolder)
for (let ent of selEntState.garrisonHolder.entities)
{
let state = GetEntityState(ent);
totalGarrisonHealth += state.hitpoints || 0;
maxGarrisonHealth += state.maxHitpoints || 0;
}
}
// Configuring the health bar
let healthGarrison = Engine.GetGUIObjectByName("healthGarrison");
healthGarrison.hidden = totalGarrisonHealth <= 0;
if (totalGarrisonHealth > 0)
{
let healthBarGarrison = Engine.GetGUIObjectByName("healthBarGarrison");
let healthSize = healthBarGarrison.size;
healthSize.rtop = 100 - 100 * Math.max(0, Math.min(1, totalGarrisonHealth / maxGarrisonHealth));
healthBarGarrison.size = healthSize;
healthGarrison.tooltip = getCurrentHealthTooltip({
"hitpoints": totalGarrisonHealth,
"maxHitpoints": maxGarrisonHealth
});
}
}
// Fills out information that most entities have
function displaySingle(entState)
{
// Get general unit and player data
let template = GetTemplateData(entState.template);
let specificName = template.name.specific;
let genericName = template.name.generic;
// If packed, add that to the generic name (reduces template clutter)
if (genericName && template.pack && template.pack.state == "packed")
genericName = sprintf(translate("%(genericName)s — Packed"), { "genericName": genericName });
let playerState = g_Players[entState.player];
let civName = g_CivData[playerState.civ].Name;
let civEmblem = g_CivData[playerState.civ].Emblem;
let playerName = playerState.name;
// Indicate disconnected players by prefixing their name
if (g_Players[entState.player].offline)
playerName = sprintf(translate("\\[OFFLINE] %(player)s"), { "player": playerName });
// Rank
if (entState.identity && entState.identity.rank && entState.identity.classes)
{
Engine.GetGUIObjectByName("rankIcon").tooltip = sprintf(translate("%(rank)s Rank"), {
"rank": translateWithContext("Rank", entState.identity.rank)
});
Engine.GetGUIObjectByName("rankIcon").sprite = "stretched:session/icons/ranks/" + entState.identity.rank + ".png";
Engine.GetGUIObjectByName("rankIcon").hidden = false;
}
else
{
Engine.GetGUIObjectByName("rankIcon").hidden = true;
Engine.GetGUIObjectByName("rankIcon").tooltip = "";
}
if (entState.statusEffects)
{
let statusEffectsSection = Engine.GetGUIObjectByName("statusEffectsIcons");
statusEffectsSection.hidden = false;
let statusIcons = statusEffectsSection.children;
let i = 0;
for (let effectCode in entState.statusEffects)
{
let effect = entState.statusEffects[effectCode];
statusIcons[i].hidden = false;
statusIcons[i].sprite = "stretched:session/icons/status_effects/" + g_StatusEffectsMetadata.getIcon(effect.baseCode) + ".png";
statusIcons[i].tooltip = getStatusEffectsTooltip(effect.baseCode, effect, false);
let size = statusIcons[i].size;
size.top = i * 18;
size.bottom = i * 18 + 16;
statusIcons[i].size = size;
if (++i >= statusIcons.length)
break;
}
for (; i < statusIcons.length; ++i)
statusIcons[i].hidden = true;
}
else
Engine.GetGUIObjectByName("statusEffectsIcons").hidden = true;
let showHealth = entState.hitpoints;
let showResource = entState.resourceSupply;
let showCapture = entState.capturePoints;
let healthSection = Engine.GetGUIObjectByName("healthSection");
let captureSection = Engine.GetGUIObjectByName("captureSection");
let resourceSection = Engine.GetGUIObjectByName("resourceSection");
let sectionPosTop = Engine.GetGUIObjectByName("sectionPosTop");
let sectionPosMiddle = Engine.GetGUIObjectByName("sectionPosMiddle");
let sectionPosBottom = Engine.GetGUIObjectByName("sectionPosBottom");
// Hitpoints
healthSection.hidden = !showHealth;
if (showHealth)
{
let unitHealthBar = Engine.GetGUIObjectByName("healthBar");
let healthSize = unitHealthBar.size;
healthSize.rright = 100 * Math.max(0, Math.min(1, entState.hitpoints / entState.maxHitpoints));
unitHealthBar.size = healthSize;
Engine.GetGUIObjectByName("healthStats").caption = sprintf(translate("%(hitpoints)s / %(maxHitpoints)s"), {
"hitpoints": Math.ceil(entState.hitpoints),
"maxHitpoints": Math.ceil(entState.maxHitpoints)
});
healthSection.size = sectionPosTop.size;
captureSection.size = showResource ? sectionPosMiddle.size : sectionPosBottom.size;
resourceSection.size = showResource ? sectionPosBottom.size : sectionPosMiddle.size;
}
else if (showResource)
{
captureSection.size = sectionPosBottom.size;
resourceSection.size = sectionPosTop.size;
}
else if (showCapture)
captureSection.size = sectionPosTop.size;
// CapturePoints
captureSection.hidden = !entState.capturePoints;
if (entState.capturePoints)
{
let setCaptureBarPart = function(playerID, startSize) {
let unitCaptureBar = Engine.GetGUIObjectByName("captureBar[" + playerID + "]");
let sizeObj = unitCaptureBar.size;
sizeObj.rleft = startSize;
let size = 100 * Math.max(0, Math.min(1, entState.capturePoints[playerID] / entState.maxCapturePoints));
sizeObj.rright = startSize + size;
unitCaptureBar.size = sizeObj;
unitCaptureBar.sprite = "color:" + g_DiplomacyColors.getPlayerColor(playerID, 128);
unitCaptureBar.hidden = false;
return startSize + size;
};
// first handle the owner's points, to keep those points on the left for clarity
let size = setCaptureBarPart(entState.player, 0);
for (let i in entState.capturePoints)
if (i != entState.player)
size = setCaptureBarPart(i, size);
let captureText = sprintf(translate("%(capturePoints)s / %(maxCapturePoints)s"), {
"capturePoints": Math.ceil(entState.capturePoints[entState.player]),
"maxCapturePoints": Math.ceil(entState.maxCapturePoints)
});
let showSmallCapture = showResource && showHealth;
Engine.GetGUIObjectByName("captureStats").caption = showSmallCapture ? "" : captureText;
Engine.GetGUIObjectByName("capture").tooltip = showSmallCapture ? captureText : "";
}
// Experience
Engine.GetGUIObjectByName("experience").hidden = !entState.promotion;
if (entState.promotion)
{
let experienceBar = Engine.GetGUIObjectByName("experienceBar");
let experienceSize = experienceBar.size;
experienceSize.rtop = 100 - (100 * Math.max(0, Math.min(1, 1.0 * +entState.promotion.curr / +entState.promotion.req)));
experienceBar.size = experienceSize;
if (entState.promotion.curr < entState.promotion.req)
Engine.GetGUIObjectByName("experience").tooltip = sprintf(translate("%(experience)s %(current)s / %(required)s"), {
"experience": "[font=\"sans-bold-13\"]" + translate("Experience:") + "[/font]",
"current": Math.floor(entState.promotion.curr),
"required": entState.promotion.req
});
else
Engine.GetGUIObjectByName("experience").tooltip = sprintf(translate("%(experience)s %(current)s"), {
"experience": "[font=\"sans-bold-13\"]" + translate("Experience:") + "[/font]",
"current": Math.floor(entState.promotion.curr)
});
}
// Resource stats
resourceSection.hidden = !showResource;
if (entState.resourceSupply)
{
let resources = entState.resourceSupply.isInfinite ? translate("∞") : // Infinity symbol
sprintf(translate("%(amount)s / %(max)s"), {
"amount": Math.ceil(+entState.resourceSupply.amount),
"max": entState.resourceSupply.max
});
let unitResourceBar = Engine.GetGUIObjectByName("resourceBar");
let resourceSize = unitResourceBar.size;
resourceSize.rright = entState.resourceSupply.isInfinite ? 100 :
100 * Math.max(0, Math.min(1, +entState.resourceSupply.amount / +entState.resourceSupply.max));
unitResourceBar.size = resourceSize;
Engine.GetGUIObjectByName("resourceLabel").caption = sprintf(translate("%(resource)s:"), {
- "resource": getResourceTypeDisplayName(entState.resourceSupply.type)
+ "resource": resourceNameFirstWord(entState.resourceSupply.type.generic)
});
Engine.GetGUIObjectByName("resourceStats").caption = resources;
}
let resourceCarryingIcon = Engine.GetGUIObjectByName("resourceCarryingIcon");
let resourceCarryingText = Engine.GetGUIObjectByName("resourceCarryingText");
resourceCarryingIcon.hidden = false;
resourceCarryingText.hidden = false;
// Resource carrying
if (entState.resourceCarrying && entState.resourceCarrying.length)
{
// We should only be carrying one resource type at once, so just display the first
let carried = entState.resourceCarrying[0];
resourceCarryingIcon.sprite = "stretched:session/icons/resources/" + carried.type + ".png";
resourceCarryingText.caption = sprintf(translate("%(amount)s / %(max)s"), { "amount": carried.amount, "max": carried.max });
resourceCarryingIcon.tooltip = "";
}
// Use the same indicators for traders
else if (entState.trader && entState.trader.goods.amount)
{
resourceCarryingIcon.sprite = "stretched:session/icons/resources/" + entState.trader.goods.type + ".png";
let totalGain = entState.trader.goods.amount.traderGain;
if (entState.trader.goods.amount.market1Gain)
totalGain += entState.trader.goods.amount.market1Gain;
if (entState.trader.goods.amount.market2Gain)
totalGain += entState.trader.goods.amount.market2Gain;
resourceCarryingText.caption = totalGain;
resourceCarryingIcon.tooltip = sprintf(translate("Gain: %(gain)s"), {
"gain": getTradingTooltip(entState.trader.goods.amount)
});
}
// And for number of workers
else if (entState.foundation)
{
resourceCarryingIcon.sprite = "stretched:session/icons/repair.png";
resourceCarryingIcon.tooltip = getBuildTimeTooltip(entState);
resourceCarryingText.caption = entState.foundation.numBuilders ? sprintf(translate("(%(number)s)\n%(time)s"), {
"number": entState.foundation.numBuilders,
"time": Engine.FormatMillisecondsIntoDateStringGMT(entState.foundation.buildTime.timeRemaining * 1000, translateWithContext("countdown format", "m:ss"))
}) : "";
}
else if (entState.resourceSupply && (!entState.resourceSupply.killBeforeGather || !entState.hitpoints))
{
resourceCarryingIcon.sprite = "stretched:session/icons/repair.png";
resourceCarryingText.caption = sprintf(translate("%(amount)s / %(max)s"), {
"amount": entState.resourceSupply.numGatherers,
"max": entState.resourceSupply.maxGatherers
});
Engine.GetGUIObjectByName("resourceCarryingIcon").tooltip = translate("Current/max gatherers");
}
else if (entState.repairable && entState.needsRepair)
{
resourceCarryingIcon.sprite = "stretched:session/icons/repair.png";
resourceCarryingIcon.tooltip = getRepairTimeTooltip(entState);
resourceCarryingText.caption = entState.repairable.numBuilders ? sprintf(translate("(%(number)s)\n%(time)s"), {
"number": entState.repairable.numBuilders,
"time": Engine.FormatMillisecondsIntoDateStringGMT(entState.repairable.buildTime.timeRemaining * 1000, translateWithContext("countdown format", "m:ss"))
}) : "";
}
else
{
resourceCarryingIcon.hidden = true;
resourceCarryingText.hidden = true;
}
Engine.GetGUIObjectByName("specific").caption = specificName;
Engine.GetGUIObjectByName("player").caption = playerName;
Engine.GetGUIObjectByName("playerColorBackground").sprite =
"color:" + g_DiplomacyColors.getPlayerColor(entState.player, 128);
Engine.GetGUIObjectByName("generic").caption = genericName == specificName ? "" :
sprintf(translate("(%(genericName)s)"), {
"genericName": genericName
});
let isGaia = playerState.civ == "gaia";
Engine.GetGUIObjectByName("playerCivIcon").sprite = isGaia ? "" : "stretched:grayscale:" + civEmblem;
Engine.GetGUIObjectByName("player").tooltip = isGaia ? "" : civName;
// TODO: we should require all entities to have icons
Engine.GetGUIObjectByName("icon").sprite = template.icon ? ("stretched:session/portraits/" + template.icon) : "BackgroundBlack";
if (template.icon)
Engine.GetGUIObjectByName("iconBorder").onPressRight = () => {
showTemplateDetails(entState.template);
};
let detailedTooltip = [
getAttackTooltip,
getHealerTooltip,
getResistanceTooltip,
getGatherTooltip,
getSpeedTooltip,
getGarrisonTooltip,
getPopulationBonusTooltip,
getProjectilesTooltip,
getResourceTrickleTooltip,
getLootTooltip
].map(func => func(entState)).filter(tip => tip).join("\n");
if (detailedTooltip)
{
Engine.GetGUIObjectByName("attackAndResistanceStats").hidden = false;
Engine.GetGUIObjectByName("attackAndResistanceStats").tooltip = detailedTooltip;
}
else
Engine.GetGUIObjectByName("attackAndResistanceStats").hidden = true;
let iconTooltips = [];
if (genericName)
iconTooltips.push("[font=\"sans-bold-16\"]" + genericName + "[/font]");
iconTooltips = iconTooltips.concat([
getVisibleEntityClassesFormatted,
getAurasTooltip,
getEntityTooltip,
+ getTreasureTooltip,
showTemplateViewerOnRightClickTooltip
].map(func => func(template)));
Engine.GetGUIObjectByName("iconBorder").tooltip = iconTooltips.filter(tip => tip).join("\n");
Engine.GetGUIObjectByName("detailsAreaSingle").hidden = false;
Engine.GetGUIObjectByName("detailsAreaMultiple").hidden = true;
}
// Fills out information for multiple entities
function displayMultiple(entStates)
{
let averageHealth = 0;
let maxHealth = 0;
let maxCapturePoints = 0;
let capturePoints = (new Array(g_MaxPlayers + 1)).fill(0);
let playerID = 0;
let totalCarrying = {};
let totalLoot = {};
let garrisonSize = 0;
for (let entState of entStates)
{
playerID = entState.player; // trust that all selected entities have the same owner
if (entState.hitpoints)
{
averageHealth += entState.hitpoints;
maxHealth += entState.maxHitpoints;
}
if (entState.capturePoints)
{
maxCapturePoints += entState.maxCapturePoints;
capturePoints = entState.capturePoints.map((v, i) => v + capturePoints[i]);
}
let carrying = calculateCarriedResources(
entState.resourceCarrying || null,
entState.trader && entState.trader.goods
);
if (entState.loot)
for (let type in entState.loot)
totalLoot[type] = (totalLoot[type] || 0) + entState.loot[type];
for (let type in carrying)
{
totalCarrying[type] = (totalCarrying[type] || 0) + carrying[type];
totalLoot[type] = (totalLoot[type] || 0) + carrying[type];
}
if (entState.garrisonable)
garrisonSize += entState.garrisonable.size;
if (entState.garrisonHolder)
garrisonSize += entState.garrisonHolder.occupiedSlots;
}
Engine.GetGUIObjectByName("healthMultiple").hidden = averageHealth <= 0;
if (averageHealth > 0)
{
let unitHealthBar = Engine.GetGUIObjectByName("healthBarMultiple");
let healthSize = unitHealthBar.size;
healthSize.rtop = 100 - 100 * Math.max(0, Math.min(1, averageHealth / maxHealth));
unitHealthBar.size = healthSize;
Engine.GetGUIObjectByName("healthMultiple").tooltip = getCurrentHealthTooltip({
"hitpoints": averageHealth,
"maxHitpoints": maxHealth
});
}
Engine.GetGUIObjectByName("captureMultiple").hidden = maxCapturePoints <= 0;
if (maxCapturePoints > 0)
{
let setCaptureBarPart = function(pID, startSize)
{
let unitCaptureBar = Engine.GetGUIObjectByName("captureBarMultiple[" + pID + "]");
let sizeObj = unitCaptureBar.size;
sizeObj.rtop = startSize;
let size = 100 * Math.max(0, Math.min(1, capturePoints[pID] / maxCapturePoints));
sizeObj.rbottom = startSize + size;
unitCaptureBar.size = sizeObj;
unitCaptureBar.sprite = "color:" + g_DiplomacyColors.getPlayerColor(pID, 128);
unitCaptureBar.hidden = false;
return startSize + size;
};
let size = 0;
for (let i in capturePoints)
if (i != playerID)
size = setCaptureBarPart(i, size);
// last handle the owner's points, to keep those points on the bottom for clarity
setCaptureBarPart(playerID, size);
Engine.GetGUIObjectByName("captureMultiple").tooltip = getCurrentHealthTooltip(
{
"hitpoints": capturePoints[playerID],
"maxHitpoints": maxCapturePoints
},
translate("Capture Points:"));
}
let numberOfUnits = Engine.GetGUIObjectByName("numberOfUnits");
numberOfUnits.caption = entStates.length;
numberOfUnits.tooltip = "";
if (garrisonSize)
numberOfUnits.tooltip = sprintf(translate("%(label)s: %(details)s\n"), {
"label": headerFont(translate("Garrison Size")),
"details": bodyFont(garrisonSize)
});
if (Object.keys(totalCarrying).length)
numberOfUnits.tooltip = sprintf(translate("%(label)s %(details)s\n"), {
"label": headerFont(translate("Carrying:")),
"details": bodyFont(Object.keys(totalCarrying).filter(
res => totalCarrying[res] != 0).map(
res => sprintf(translate("%(type)s %(amount)s"),
{ "type": resourceIcon(res), "amount": totalCarrying[res] })).join(" "))
});
if (Object.keys(totalLoot).length)
numberOfUnits.tooltip += sprintf(translate("%(label)s %(details)s"), {
"label": headerFont(translate("Loot:")),
"details": bodyFont(Object.keys(totalLoot).filter(
res => totalLoot[res] != 0).map(
res => sprintf(translate("%(type)s %(amount)s"),
{ "type": resourceIcon(res), "amount": totalLoot[res] })).join(" "))
});
// Unhide Details Area
Engine.GetGUIObjectByName("detailsAreaMultiple").hidden = false;
Engine.GetGUIObjectByName("detailsAreaSingle").hidden = true;
}
// Updates middle entity Selection Details Panel and left Unit Commands Panel
function updateSelectionDetails()
{
let supplementalDetailsPanel = Engine.GetGUIObjectByName("supplementalSelectionDetails");
let detailsPanel = Engine.GetGUIObjectByName("selectionDetails");
let commandsPanel = Engine.GetGUIObjectByName("unitCommands");
let entStates = [];
for (let sel of g_Selection.toList())
{
let entState = GetEntityState(sel);
if (!entState)
continue;
entStates.push(entState);
}
if (entStates.length == 0)
{
Engine.GetGUIObjectByName("detailsAreaMultiple").hidden = true;
Engine.GetGUIObjectByName("detailsAreaSingle").hidden = true;
hideUnitCommands();
supplementalDetailsPanel.hidden = true;
detailsPanel.hidden = true;
commandsPanel.hidden = true;
return;
}
// Fill out general info and display it
if (entStates.length == 1)
displaySingle(entStates[0]);
else
displayMultiple(entStates);
// Show basic details.
detailsPanel.hidden = false;
// Fill out commands panel for specific unit selected (or first unit of primary group)
updateUnitCommands(entStates, supplementalDetailsPanel, commandsPanel);
// Show health bar for garrisoned units if the garrison panel is visible
if (Engine.GetGUIObjectByName("unitGarrisonPanel") && !Engine.GetGUIObjectByName("unitGarrisonPanel").hidden)
updateGarrisonHealthBar(entStates[0], g_Selection.toList());
}
function tradingGainString(gain, owner)
{
// Translation: Used in the trading gain tooltip
return sprintf(translate("%(gain)s (%(player)s)"), {
"gain": gain,
"player": GetSimState().players[owner].name
});
}
/**
* Returns a message with the details of the trade gain.
*/
function getTradingTooltip(gain)
{
if (!gain)
return "";
let markets = [
{ "gain": gain.market1Gain, "owner": gain.market1Owner },
{ "gain": gain.market2Gain, "owner": gain.market2Owner }
];
let primaryGain = gain.traderGain;
for (let market of markets)
if (market.gain && market.owner == gain.traderOwner)
// Translation: Used in the trading gain tooltip to concatenate profits of different players
primaryGain += translate("+") + market.gain;
let tooltip = tradingGainString(primaryGain, gain.traderOwner);
for (let market of markets)
if (market.gain && market.owner != gain.traderOwner)
tooltip +=
translateWithContext("Separation mark in an enumeration", ", ") +
tradingGainString(market.gain, market.owner);
return tooltip;
}
Index: ps/trunk/binaries/data/mods/public/gui/session/unit_actions.js
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/session/unit_actions.js (revision 24988)
+++ ps/trunk/binaries/data/mods/public/gui/session/unit_actions.js (revision 24989)
@@ -1,1629 +1,1675 @@
/**
* Specifies which template should indicate the target location of a player command,
* given a command type.
*/
var g_TargetMarker = {
"move": "special/target_marker"
};
/**
* Which enemy entity types will be attacked on sight when patroling.
*/
var g_PatrolTargets = ["Unit"];
const g_DisabledTags = { "color": "255 140 0" };
/**
* List of different actions units can execute,
* this is mostly used to determine which actions can be executed
*
* "execute" is meant to send the command to the engine
*
* The next functions will always return false
* in case you have to continue to seek
* (i.e. look at the next entity for getActionInfo, the next
* possible action for the actionCheck ...)
* They will return an object when the searching is finished
*
* "getActionInfo" is used to determine if the action is possible,
* and also give visual feedback to the user (tooltips, cursors, ...)
*
* "preSelectedActionCheck" is used to select actions when the gui buttons
* were used to set them, but still require a target (like the guard button)
*
* "hotkeyActionCheck" is used to check the possibility of actions when
* a hotkey is pressed
*
* "actionCheck" is used to check the possibilty of actions without specific
* command. For that, the specificness variable is used
*
* "specificness" is used to determine how specific an action is,
* The lower the number, the more specific an action is, and the bigger
* the chance of selecting that action when multiple actions are possible
*/
var g_UnitActions =
{
"move":
{
"execute": function(target, action, selection, queued)
{
Engine.PostNetworkCommand({
"type": "walk",
"entities": selection,
"x": target.x,
"z": target.z,
"queued": queued,
"formation": g_AutoFormation.getDefault()
});
DrawTargetMarker(target);
Engine.GuiInterfaceCall("PlaySound", {
"name": "order_walk",
"entity": action.firstAbleEntity
});
return true;
},
"getActionInfo": function(entState, targetState)
{
if (!entState.unitAI)
return false;
return { "possible": true };
},
"hotkeyActionCheck": function(target, selection)
{
return Engine.HotkeyIsPressed("session.move") &&
this.actionCheck(target, selection);
},
"actionCheck": function(target, selection)
{
let actionInfo = getActionInfo("move", target, selection);
return actionInfo.possible && {
"type": "move",
"firstAbleEntity": actionInfo.entity
};
},
"specificness": 12,
},
"attack-move":
{
"execute": function(target, action, selection, queued)
{
let targetClasses;
if (Engine.HotkeyIsPressed("session.attackmoveUnit"))
targetClasses = { "attack": ["Unit"] };
else
targetClasses = { "attack": ["Unit", "Structure"] };
Engine.PostNetworkCommand({
"type": "attack-walk",
"entities": selection,
"x": target.x,
"z": target.z,
"targetClasses": targetClasses,
"queued": queued,
"formation": g_AutoFormation.getNull()
});
DrawTargetMarker(target);
Engine.GuiInterfaceCall("PlaySound", {
"name": "order_walk",
"entity": action.firstAbleEntity
});
return true;
},
"getActionInfo": function(entState, targetState)
{
if (!entState.unitAI)
return false;
return { "possible": true };
},
"hotkeyActionCheck": function(target, selection)
{
return isAttackMovePressed() &&
this.actionCheck(target, selection);
},
"actionCheck": function(target, selection)
{
let actionInfo = getActionInfo("attack-move", target, selection);
return actionInfo.possible && {
"type": "attack-move",
"cursor": "action-attack-move",
"firstAbleEntity": actionInfo.entity
};
},
"specificness": 30,
},
"capture":
{
"execute": function(target, action, selection, queued)
{
Engine.PostNetworkCommand({
"type": "attack",
"entities": selection,
"target": action.target,
"allowCapture": true,
"queued": queued,
"formation": g_AutoFormation.getNull()
});
Engine.GuiInterfaceCall("PlaySound", {
"name": "order_attack",
"entity": action.firstAbleEntity
});
return true;
},
"getActionInfo": function(entState, targetState)
{
if (!entState.attack || !targetState || !targetState.capturePoints)
return false;
return {
"possible": Engine.GuiInterfaceCall("CanAttack", {
"entity": entState.id,
"target": targetState.id,
"types": ["Capture"]
})
};
},
"actionCheck": function(target, selection)
{
let actionInfo = getActionInfo("capture", target, selection);
return actionInfo.possible && {
"type": "capture",
"cursor": "action-capture",
"target": target,
"firstAbleEntity": actionInfo.entity
};
},
"specificness": 9,
},
"attack":
{
"execute": function(target, action, selection, queued)
{
Engine.PostNetworkCommand({
"type": "attack",
"entities": selection,
"target": action.target,
"queued": queued,
"allowCapture": false,
"formation": g_AutoFormation.getNull()
});
Engine.GuiInterfaceCall("PlaySound", {
"name": "order_attack",
"entity": action.firstAbleEntity
});
return true;
},
"getActionInfo": function(entState, targetState)
{
if (!entState.attack || !targetState || !targetState.hitpoints)
return false;
return {
"possible": Engine.GuiInterfaceCall("CanAttack", {
"entity": entState.id,
"target": targetState.id,
"types": ["!Capture"]
})
};
},
"hotkeyActionCheck": function(target, selection)
{
return Engine.HotkeyIsPressed("session.attack") &&
this.actionCheck(target, selection);
},
"actionCheck": function(target, selection)
{
let actionInfo = getActionInfo("attack", target, selection);
return actionInfo.possible && {
"type": "attack",
"cursor": "action-attack",
"target": target,
"firstAbleEntity": actionInfo.entity
};
},
"specificness": 10,
},
"patrol":
{
"execute": function(target, action, selection, queued)
{
Engine.PostNetworkCommand({
"type": "patrol",
"entities": selection,
"x": target.x,
"z": target.z,
"target": action.target,
"targetClasses": { "attack": g_PatrolTargets },
"queued": queued,
"allowCapture": false,
"formation": g_AutoFormation.getDefault()
});
DrawTargetMarker(target);
Engine.GuiInterfaceCall("PlaySound", {
"name": "order_patrol",
"entity": action.firstAbleEntity
});
return true;
},
"getActionInfo": function(entState, targetState)
{
if (!entState.unitAI || !entState.unitAI.canPatrol)
return false;
return { "possible": true };
},
"hotkeyActionCheck": function(target, selection)
{
return Engine.HotkeyIsPressed("session.patrol") &&
this.actionCheck(target, selection);
},
"preSelectedActionCheck": function(target, selection)
{
return preSelectedAction == ACTION_PATROL &&
this.actionCheck(target, selection);
},
"actionCheck": function(target, selection)
{
let actionInfo = getActionInfo("patrol", target, selection);
return actionInfo.possible && {
"type": "patrol",
"cursor": "action-patrol",
"target": target,
"firstAbleEntity": actionInfo.entity
};
},
"specificness": 37,
},
"heal":
{
"execute": function(target, action, selection, queued)
{
Engine.PostNetworkCommand({
"type": "heal",
"entities": selection,
"target": action.target,
"queued": queued,
"formation": g_AutoFormation.getNull()
});
Engine.GuiInterfaceCall("PlaySound", {
"name": "order_heal",
"entity": action.firstAbleEntity
});
return true;
},
"getActionInfo": function(entState, targetState)
{
if (!entState.heal || !targetState ||
!hasClass(targetState, "Unit") || !targetState.needsHeal ||
!playerCheck(entState, targetState, ["Player", "Ally"]) ||
entState.id == targetState.id) // Healers can't heal themselves.
return false;
let unhealableClasses = entState.heal.unhealableClasses;
if (MatchesClassList(targetState.identity.classes, unhealableClasses))
return false;
let healableClasses = entState.heal.healableClasses;
if (!MatchesClassList(targetState.identity.classes, healableClasses))
return false;
return { "possible": true };
},
"actionCheck": function(target, selection)
{
let actionInfo = getActionInfo("heal", target, selection);
return actionInfo.possible && {
"type": "heal",
"cursor": "action-heal",
"target": target,
"firstAbleEntity": actionInfo.entity
};
},
"specificness": 7,
},
// "Fake" action to check if an entity can be ordered to "construct"
// which is handled differently from repair as the target does not exist.
"construct":
{
"preSelectedActionCheck": function(target, selection)
{
let state = GetEntityState(selection[0]);
if (state && state.builder &&
target && target.constructor && target.constructor.name == "PlacementSupport")
return { "type": "construct" };
return false;
},
"specificness": 0,
},
"repair":
{
"execute": function(target, action, selection, queued)
{
Engine.PostNetworkCommand({
"type": "repair",
"entities": selection,
"target": action.target,
"autocontinue": true,
"queued": queued,
"formation": g_AutoFormation.getNull()
});
Engine.GuiInterfaceCall("PlaySound", {
"name": action.foundation ? "order_build" : "order_repair",
"entity": action.firstAbleEntity
});
return true;
},
"getActionInfo": function(entState, targetState)
{
if (!entState.builder || !targetState ||
!targetState.needsRepair && !targetState.foundation ||
!playerCheck(entState, targetState, ["Player", "Ally"]))
return false;
return {
"possible": true,
"foundation": targetState.foundation
};
},
"preSelectedActionCheck": function(target, selection)
{
return preSelectedAction == ACTION_REPAIR && (this.actionCheck(target, selection) || {
"type": "none",
"cursor": "action-repair-disabled",
"target": null
});
},
"hotkeyActionCheck": function(target, selection)
{
return Engine.HotkeyIsPressed("session.repair") &&
this.actionCheck(target, selection);
},
"actionCheck": function(target, selection)
{
let actionInfo = getActionInfo("repair", target, selection);
return actionInfo.possible && {
"type": "repair",
"cursor": "action-repair",
"target": target,
"foundation": actionInfo.foundation,
"firstAbleEntity": actionInfo.entity
};
},
"specificness": 11,
},
"gather":
{
"execute": function(target, action, selection, queued)
{
Engine.PostNetworkCommand({
"type": "gather",
"entities": selection,
"target": action.target,
"queued": queued,
"formation": g_AutoFormation.getNull()
});
Engine.GuiInterfaceCall("PlaySound", {
"name": "order_gather",
"entity": action.firstAbleEntity
});
return true;
},
"getActionInfo": function(entState, targetState)
{
if (!entState.resourceGatherRates ||
!targetState || !targetState.resourceSupply)
return false;
let resource;
if (entState.resourceGatherRates[targetState.resourceSupply.type.generic + "." + targetState.resourceSupply.type.specific])
resource = targetState.resourceSupply.type.specific;
else if (entState.resourceGatherRates[targetState.resourceSupply.type.generic])
resource = targetState.resourceSupply.type.generic;
if (!resource)
return false;
return {
"possible": true,
"cursor": "action-gather-" + resource
};
},
"actionCheck": function(target, selection)
{
let actionInfo = getActionInfo("gather", target, selection);
return actionInfo.possible && {
"type": "gather",
"cursor": actionInfo.cursor,
"target": target,
"firstAbleEntity": actionInfo.entity
};
},
"specificness": 1,
},
"returnresource":
{
"execute": function(target, action, selection, queued)
{
Engine.PostNetworkCommand({
"type": "returnresource",
"entities": selection,
"target": action.target,
"queued": queued,
"formation": g_AutoFormation.getNull()
});
Engine.GuiInterfaceCall("PlaySound", {
"name": "order_gather",
"entity": action.firstAbleEntity
});
return true;
},
"getActionInfo": function(entState, targetState)
{
if (!targetState || !targetState.resourceDropsite)
return false;
let playerState = GetSimState().players[entState.player];
if (playerState.hasSharedDropsites && targetState.resourceDropsite.shared)
{
if (!playerCheck(entState, targetState, ["Player", "MutualAlly"]))
return false;
}
else if (!playerCheck(entState, targetState, ["Player"]))
return false;
if (!entState.resourceCarrying || !entState.resourceCarrying.length)
return false;
let carriedType = entState.resourceCarrying[0].type;
if (targetState.resourceDropsite.types.indexOf(carriedType) == -1)
return false;
return {
"possible": true,
"cursor": "action-return-" + carriedType
};
},
"actionCheck": function(target, selection)
{
let actionInfo = getActionInfo("returnresource", target, selection);
return actionInfo.possible && {
"type": "returnresource",
"cursor": actionInfo.cursor,
"target": target,
"firstAbleEntity": actionInfo.entity
};
},
"specificness": 2,
},
"cancel-setup-trade-route":
{
"execute": function(target, action, selection, queued)
{
Engine.PostNetworkCommand({
"type": "cancel-setup-trade-route",
"entities": selection,
"target": action.target,
"queued": queued
});
return true;
},
"getActionInfo": function(entState, targetState)
{
if (!targetState || targetState.foundation || !entState.trader || !targetState.market ||
playerCheck(entState, targetState, ["Enemy"]) ||
!(targetState.market.land && hasClass(entState, "Organic") ||
targetState.market.naval && hasClass(entState, "Ship")))
return false;
let tradingDetails = Engine.GuiInterfaceCall("GetTradingDetails", {
"trader": entState.id,
"target": targetState.id
});
if (!tradingDetails || !tradingDetails.type)
return false;
if (tradingDetails.type == "is first" && !tradingDetails.hasBothMarkets)
return {
"possible": true,
"tooltip": translate("This is the origin trade market.\nRight-click to cancel trade route.")
};
return false;
},
"actionCheck": function(target, selection)
{
let actionInfo = getActionInfo("cancel-setup-trade-route", target, selection);
return actionInfo.possible && {
"type": "cancel-setup-trade-route",
"cursor": "action-cancel-setup-trade-route",
"tooltip": actionInfo.tooltip,
"target": target,
"firstAbleEntity": actionInfo.entity
};
},
"specificness": 2,
},
"setup-trade-route":
{
"execute": function(target, action, selection, queued)
{
Engine.PostNetworkCommand({
"type": "setup-trade-route",
"entities": selection,
"target": action.target,
"source": null,
"route": null,
"queued": queued,
"formation": g_AutoFormation.getNull()
});
Engine.GuiInterfaceCall("PlaySound", {
"name": "order_trade",
"entity": action.firstAbleEntity
});
return true;
},
"getActionInfo": function(entState, targetState)
{
if (!targetState || targetState.foundation || !entState.trader || !targetState.market ||
playerCheck(entState, targetState, ["Enemy"]) ||
!(targetState.market.land && hasClass(entState, "Organic") ||
targetState.market.naval && hasClass(entState, "Ship")))
return false;
let tradingDetails = Engine.GuiInterfaceCall("GetTradingDetails", {
"trader": entState.id,
"target": targetState.id
});
if (!tradingDetails)
return false;
let tooltip;
switch (tradingDetails.type)
{
case "is first":
tooltip = translate("Origin trade market.") + "\n";
if (tradingDetails.hasBothMarkets)
tooltip += sprintf(translate("Gain: %(gain)s"), {
"gain": getTradingTooltip(tradingDetails.gain)
});
else
return false;
break;
case "is second":
tooltip = translate("Destination trade market.") + "\n" +
sprintf(translate("Gain: %(gain)s"), {
"gain": getTradingTooltip(tradingDetails.gain)
});
break;
case "set first":
tooltip = translate("Right-click to set as origin trade market");
break;
case "set second":
if (tradingDetails.gain.traderGain == 0)
return {
"possible": true,
"tooltip": setStringTags(translate("This market is too close to the origin market."), g_DisabledTags),
"disabled": true
};
tooltip = translate("Right-click to set as destination trade market.") + "\n" +
sprintf(translate("Gain: %(gain)s"), {
"gain": getTradingTooltip(tradingDetails.gain)
});
break;
}
return {
"possible": true,
"tooltip": tooltip
};
},
"actionCheck": function(target, selection)
{
let actionInfo = getActionInfo("setup-trade-route", target, selection);
if (actionInfo.disabled)
return {
"type": "none",
"cursor": "action-setup-trade-route-disabled",
"target": null,
"tooltip": actionInfo.tooltip
};
return actionInfo.possible && {
"type": "setup-trade-route",
"cursor": "action-setup-trade-route",
"tooltip": actionInfo.tooltip,
"target": target,
"firstAbleEntity": actionInfo.entity
};
},
"specificness": 0,
},
"garrison":
{
"execute": function(target, action, selection, queued)
{
Engine.PostNetworkCommand({
"type": "garrison",
"entities": selection,
"target": action.target,
"queued": queued,
"formation": g_AutoFormation.getNull()
});
Engine.GuiInterfaceCall("PlaySound", {
"name": "order_garrison",
"entity": action.firstAbleEntity
});
return true;
},
"getActionInfo": function(entState, targetState)
{
if (!entState.garrisonable || !targetState || !targetState.garrisonHolder ||
!playerCheck(entState, targetState, ["Player", "MutualAlly"]))
return false;
let tooltip = sprintf(translate("Current garrison: %(garrisoned)s/%(capacity)s"), {
"garrisoned": targetState.garrisonHolder.occupiedSlots,
"capacity": targetState.garrisonHolder.capacity
});
let extraCount = entState.garrisonable.size;
if (entState.garrisonHolder)
extraCount += entState.garrisonHolder.occupiedSlots;
if (targetState.garrisonHolder.occupiedSlots + extraCount > targetState.garrisonHolder.capacity)
tooltip = coloredText(tooltip, "orange");
if (!MatchesClassList(entState.identity.classes, targetState.garrisonHolder.allowedClasses))
return false;
return {
"possible": true,
"tooltip": tooltip
};
},
"preSelectedActionCheck": function(target, selection)
{
return preSelectedAction == ACTION_GARRISON && (this.actionCheck(target, selection) || {
"type": "none",
"cursor": "action-garrison-disabled",
"target": null
});
},
"hotkeyActionCheck": function(target, selection)
{
return Engine.HotkeyIsPressed("session.garrison") &&
this.actionCheck(target, selection);
},
"actionCheck": function(target, selection)
{
let actionInfo = getActionInfo("garrison", target, selection);
return actionInfo.possible && {
"type": "garrison",
"cursor": "action-garrison",
"tooltip": actionInfo.tooltip,
"target": target,
"firstAbleEntity": actionInfo.entity
};
},
"specificness": 20,
},
"guard":
{
"execute": function(target, action, selection, queued)
{
Engine.PostNetworkCommand({
"type": "guard",
"entities": selection,
"target": action.target,
"queued": queued,
"formation": g_AutoFormation.getNull()
});
Engine.GuiInterfaceCall("PlaySound", {
"name": "order_guard",
"entity": action.firstAbleEntity
});
return true;
},
"getActionInfo": function(entState, targetState)
{
if (!targetState || !targetState.guard || entState.id == targetState.id ||
!playerCheck(entState, targetState, ["Player", "Ally"]) ||
!entState.unitAI || !entState.unitAI.canGuard)
return false;
return { "possible": true };
},
"preSelectedActionCheck": function(target, selection)
{
return preSelectedAction == ACTION_GUARD && (this.actionCheck(target, selection) || {
"type": "none",
"cursor": "action-guard-disabled",
"target": null
});
},
"hotkeyActionCheck": function(target, selection)
{
return Engine.HotkeyIsPressed("session.guard") &&
this.actionCheck(target, selection);
},
"actionCheck": function(target, selection)
{
let actionInfo = getActionInfo("guard", target, selection);
return actionInfo.possible && {
"type": "guard",
"cursor": "action-guard",
"target": target,
"firstAbleEntity": actionInfo.entity
};
},
"specificness": 40,
},
+ "collect-treasure":
+ {
+ "execute": function(target, action, selection, queued)
+ {
+ Engine.PostNetworkCommand({
+ "type": "collect-treasure",
+ "entities": selection,
+ "target": action.target,
+ "queued": queued,
+ "formation": g_AutoFormation.getNull()
+ });
+
+ Engine.GuiInterfaceCall("PlaySound", {
+ "name": "order_collect_treasure",
+ "entity": action.firstAbleEntity
+ });
+
+ return true;
+ },
+ "getActionInfo": function(entState, targetState)
+ {
+ if (!entState.treasureCollecter ||
+ !targetState || !targetState.treasure)
+ return false;
+
+ return {
+ "possible": true,
+ "cursor": "action-collect-treasure"
+ };
+ },
+ "actionCheck": function(target, selection)
+ {
+ let actionInfo = getActionInfo("collect-treasure", target, selection);
+ return actionInfo.possible && {
+ "type": "collect-treasure",
+ "cursor": actionInfo.cursor,
+ "target": target,
+ "firstAbleEntity": actionInfo.entity
+ };
+ },
+ "specificness": 1,
+ },
+
"remove-guard":
{
"execute": function(target, action, selection, queued)
{
Engine.PostNetworkCommand({
"type": "remove-guard",
"entities": selection,
"target": action.target,
"queued": queued
});
Engine.GuiInterfaceCall("PlaySound", {
"name": "order_guard",
"entity": action.firstAbleEntity
});
return true;
},
"getActionInfo": function(entState, targetState)
{
if (!entState.unitAI || !entState.unitAI.isGuarding)
return false;
return { "possible": true };
},
"hotkeyActionCheck": function(target, selection)
{
return Engine.HotkeyIsPressed("session.guard") &&
this.actionCheck(target, selection);
},
"actionCheck": function(target, selection)
{
let actionInfo = getActionInfo("remove-guard", target, selection);
return actionInfo.possible && {
"type": "remove-guard",
"cursor": "action-remove-guard",
"firstAbleEntity": actionInfo.entity
};
},
"specificness": 41,
},
"set-rallypoint":
{
"execute": function(target, action, selection, queued)
{
// if there is a position set in the action then use this so that when setting a
// rally point on an entity it is centered on that entity
if (action.position)
target = action.position;
Engine.PostNetworkCommand({
"type": "set-rallypoint",
"entities": selection,
"x": target.x,
"z": target.z,
"data": action.data,
"queued": queued
});
// Display rally point at the new coordinates, to avoid display lag
Engine.GuiInterfaceCall("DisplayRallyPoint", {
"entities": selection,
"x": target.x,
"z": target.z,
"queued": queued
});
return true;
},
"getActionInfo": function(entState, targetState)
{
if (!entState.rallyPoint)
return false;
// Don't allow the rally point to be set on any of the currently selected entities (used for unset)
// except if the autorallypoint hotkey is pressed and the target can produce entities.
if (targetState && (!Engine.HotkeyIsPressed("session.autorallypoint") ||
!targetState.production ||
!targetState.production.entities.length))
for (let ent in g_Selection.selected)
if (targetState.id == +ent)
return false;
let tooltip;
let disabled = false;
// default to walking there (or attack-walking if hotkey pressed)
let data = { "command": "walk" };
let cursor = "";
if (isAttackMovePressed())
{
let targetClasses;
if (Engine.HotkeyIsPressed("session.attackmoveUnit"))
targetClasses = { "attack": ["Unit"] };
else
targetClasses = { "attack": ["Unit", "Structure"] };
data.command = "attack-walk";
data.targetClasses = targetClasses;
cursor = "action-attack-move";
}
if (Engine.HotkeyIsPressed("session.repair") && targetState &&
(targetState.needsRepair || targetState.foundation) &&
playerCheck(entState, targetState, ["Player", "Ally"]))
{
data.command = "repair";
data.target = targetState.id;
cursor = "action-repair";
}
else if (targetState && targetState.garrisonHolder &&
playerCheck(entState, targetState, ["Player", "MutualAlly"]))
{
data.command = "garrison";
data.target = targetState.id;
cursor = "action-garrison";
tooltip = sprintf(translate("Current garrison: %(garrisoned)s/%(capacity)s"), {
"garrisoned": targetState.garrisonHolder.occupiedSlots,
"capacity": targetState.garrisonHolder.capacity
});
if (targetState.garrisonHolder.occupiedSlots >=
targetState.garrisonHolder.capacity)
tooltip = coloredText(tooltip, "orange");
}
else if (targetState && targetState.resourceSupply)
{
let resourceType = targetState.resourceSupply.type;
- if (resourceType.generic == "treasure")
- cursor = "action-gather-" + resourceType.generic;
- else
- cursor = "action-gather-" + resourceType.specific;
+ cursor = "action-gather-" + resourceType.specific;
data.command = "gather-near-position";
data.resourceType = resourceType;
data.resourceTemplate = targetState.template;
if (!targetState.speed)
{
data.command = "gather";
data.target = targetState.id;
}
}
+ else if (targetState && targetState.treasure)
+ {
+ cursor = "action-collect-treasure";
+ data.command = "collect-treasure";
+ data.target = targetState.id;
+ }
else if (entState.market && targetState && targetState.market &&
entState.id != targetState.id &&
(!entState.market.naval || targetState.market.naval) &&
!playerCheck(entState, targetState, ["Enemy"]))
{
// Find a trader (if any) that this structure can train.
let trader;
if (entState.production && entState.production.entities.length)
for (let i = 0; i < entState.production.entities.length; ++i)
if ((trader = GetTemplateData(entState.production.entities[i]).trader))
break;
let traderData = {
"firstMarket": entState.id,
"secondMarket": targetState.id,
"template": trader
};
let gain = Engine.GuiInterfaceCall("GetTradingRouteGain", traderData);
if (gain)
{
data.command = "trade";
data.target = traderData.secondMarket;
data.source = traderData.firstMarket;
cursor = "action-setup-trade-route";
if (gain.traderGain)
tooltip = translate("Right-click to establish a default route for new traders.") + "\n" +
sprintf(
trader ?
translate("Gain: %(gain)s") :
translate("Expected gain: %(gain)s"),
{ "gain": getTradingTooltip(gain) });
else
{
disabled = true;
tooltip = setStringTags(translate("This market is too close to the origin market."), g_DisabledTags);
cursor = "action-setup-trade-route-disabled";
}
}
}
else if (targetState && (targetState.needsRepair || targetState.foundation) && playerCheck(entState, targetState, ["Ally"]))
{
data.command = "repair";
data.target = targetState.id;
cursor = "action-repair";
}
else if (targetState && playerCheck(entState, targetState, ["Enemy"]))
{
data.target = targetState.id;
data.command = "attack";
cursor = "action-attack";
}
return {
"possible": true,
"data": data,
"position": targetState && targetState.position,
"cursor": cursor,
"disabled": disabled,
"tooltip": tooltip
};
},
"hotkeyActionCheck": function(target, selection)
{
// Hotkeys are checked in the actionInfo.
return this.actionCheck(target, selection);
},
"actionCheck": function(target, selection)
{
// We want commands to units take precedence.
if (selection.some(ent => {
let entState = GetEntityState(ent);
return entState && !!entState.unitAI;
}))
return false;
let actionInfo = getActionInfo("set-rallypoint", target, selection);
if (actionInfo.disabled)
return {
"type": "none",
"cursor": actionInfo.cursor,
"target": null,
"tooltip": actionInfo.tooltip
};
return actionInfo.possible && {
"type": "set-rallypoint",
"cursor": actionInfo.cursor,
"data": actionInfo.data,
"tooltip": actionInfo.tooltip,
"position": actionInfo.position,
"firstAbleEntity": actionInfo.entity
};
},
"specificness": 6,
},
"unset-rallypoint":
{
"execute": function(target, action, selection, queued)
{
Engine.PostNetworkCommand({
"type": "unset-rallypoint",
"entities": selection
});
// Remove displayed rally point
Engine.GuiInterfaceCall("DisplayRallyPoint", {
"entities": []
});
return true;
},
"getActionInfo": function(entState, targetState)
{
if (!targetState ||
entState.id != targetState.id || entState.unitAI ||
!entState.rallyPoint || !entState.rallyPoint.position)
return false;
return { "possible": true };
},
"actionCheck": function(target, selection)
{
let actionInfo = getActionInfo("unset-rallypoint", target, selection);
return actionInfo.possible && {
"type": "unset-rallypoint",
"cursor": "action-unset-rally",
"firstAbleEntity": actionInfo.entity
};
},
"specificness": 11,
},
// This is a "fake" action to show a failure cursor
// when only uncontrollable entities are selected.
"uncontrollable":
{
"execute": function(target, action, selection, queued)
{
return true;
},
"actionCheck": function(target, selection)
{
// Only show this action if all entities are marked uncontrollable.
let playerState = g_SimState.players[g_ViewedPlayer];
if (playerState && playerState.controlsAll || selection.some(ent => {
let entState = GetEntityState(ent);
return entState && entState.identity && entState.identity.controllable;
}))
return false;
return {
"type": "none",
"cursor": "cursor-no",
"tooltip": translatePlural("This entity cannot be controlled.", "These entities cannot be controlled.", selection.length)
};
},
"specificness": 100,
},
"none":
{
"execute": function(target, action, selection, queued)
{
return true;
},
"specificness": 100,
},
};
var g_UnitActionsSortedKeys = Object.keys(g_UnitActions).sort((a, b) => g_UnitActions[a].specificness - g_UnitActions[b].specificness);
/**
* Info and actions for the entity commands
* Currently displayed in the bottom of the central panel
*/
var g_EntityCommands =
{
"unload-all": {
"getInfo": function(entStates)
{
let count = 0;
for (let entState of entStates)
{
if (!entState.garrisonHolder)
continue;
if (allowedPlayersCheck([entState], ["Player"]))
count += entState.garrisonHolder.entities.length;
else
for (let entity of entState.garrisonHolder.entities)
if (allowedPlayersCheck([GetEntityState(entity)], ["Player"]))
++count;
}
if (!count)
return false;
return {
"tooltip": colorizeHotkey("%(hotkey)s" + " ", "session.unload") +
translate("Unload All."),
"icon": "garrison-out.png",
"count": count,
"enabled": true
};
},
"execute": function()
{
unloadAll();
},
"allowedPlayers": ["Player", "Ally"]
},
"delete": {
"getInfo": function(entStates)
{
return entStates.some(entState => !isUndeletable(entState)) ?
{
"tooltip":
colorizeHotkey("%(hotkey)s" + " ", "session.kill") +
translate("Destroy the selected units or structures.") + "\n" +
colorizeHotkey(
translate("Use %(hotkey)s to avoid the confirmation dialog."),
"session.noconfirmation"
),
"icon": "kill_small.png",
"enabled": true
} :
{
// Get all delete reasons and remove duplications
"tooltip": entStates.map(entState => isUndeletable(entState))
.filter((reason, pos, self) =>
self.indexOf(reason) == pos && reason
).join("\n"),
"icon": "kill_small_disabled.png",
"enabled": false
};
},
"execute": function(entStates)
{
let entityIDs = entStates.reduce(
(ids, entState) => {
if (!isUndeletable(entState))
ids.push(entState.id);
return ids;
},
[]);
if (!entityIDs.length)
return;
let deleteSelection = () => Engine.PostNetworkCommand({
"type": "delete-entities",
"entities": entityIDs
});
if (Engine.HotkeyIsPressed("session.noconfirmation"))
deleteSelection();
else
(new DeleteSelectionConfirmation(deleteSelection)).display();
},
"allowedPlayers": ["Player"]
},
"stop": {
"getInfo": function(entStates)
{
if (entStates.every(entState => !entState.unitAI))
return false;
return {
"tooltip": colorizeHotkey("%(hotkey)s" + " ", "session.stop") +
translate("Abort the current order."),
"icon": "stop.png",
"enabled": true
};
},
"execute": function(entStates)
{
if (entStates.length)
stopUnits(entStates.map(entState => entState.id));
},
"allowedPlayers": ["Player"]
},
"garrison": {
"getInfo": function(entStates)
{
if (entStates.every(entState => !entState.unitAI || entState.turretParent || false))
return false;
return {
"tooltip": colorizeHotkey("%(hotkey)s" + " ", "session.garrison") +
translate("Order the selected units to garrison in a structure or unit."),
"icon": "garrison.png",
"enabled": true
};
},
"execute": function()
{
inputState = INPUT_PRESELECTEDACTION;
preSelectedAction = ACTION_GARRISON;
},
"allowedPlayers": ["Player"]
},
"unload": {
"getInfo": function(entStates)
{
if (entStates.every(entState => {
if (!entState.unitAI || !entState.turretParent)
return true;
let parent = GetEntityState(entState.turretParent);
return !parent || !parent.garrisonHolder || parent.garrisonHolder.entities.indexOf(entState.id) == -1;
}))
return false;
return {
"tooltip": translate("Unload"),
"icon": "garrison-out.png",
"enabled": true
};
},
"execute": function()
{
unloadSelection();
},
"allowedPlayers": ["Player"]
},
"repair": {
"getInfo": function(entStates)
{
if (entStates.every(entState => !entState.builder))
return false;
return {
"tooltip": colorizeHotkey("%(hotkey)s" + " ", "session.repair") +
translate("Order the selected units to repair a structure, ship, or siege engine."),
"icon": "repair.png",
"enabled": true
};
},
"execute": function()
{
inputState = INPUT_PRESELECTEDACTION;
preSelectedAction = ACTION_REPAIR;
},
"allowedPlayers": ["Player"]
},
"focus-rally": {
"getInfo": function(entStates)
{
if (entStates.every(entState => !entState.rallyPoint))
return false;
return {
"tooltip": colorizeHotkey("%(hotkey)s" + " ", "camera.rallypointfocus") +
translate("Focus on Rally Point."),
"icon": "focus-rally.png",
"enabled": true
};
},
"execute": function(entStates)
{
// TODO: Would be nicer to cycle between the rallypoints of multiple entities instead of just using the first
let focusTarget;
for (let entState of entStates)
if (entState.rallyPoint && entState.rallyPoint.position)
{
focusTarget = entState.rallyPoint.position;
break;
}
if (!focusTarget)
for (let entState of entStates)
if (entState.position)
{
focusTarget = entState.position;
break;
}
if (focusTarget)
Engine.CameraMoveTo(focusTarget.x, focusTarget.z);
},
"allowedPlayers": ["Player", "Observer"]
},
"back-to-work": {
"getInfo": function(entStates)
{
if (entStates.every(entState => !entState.unitAI || !entState.unitAI.hasWorkOrders))
return false;
return {
"tooltip": colorizeHotkey("%(hotkey)s" + " ", "session.backtowork") +
translate("Back to Work"),
"icon": "back-to-work.png",
"enabled": true
};
},
"execute": function()
{
backToWork();
},
"allowedPlayers": ["Player"]
},
"add-guard": {
"getInfo": function(entStates)
{
if (entStates.every(entState =>
!entState.unitAI || !entState.unitAI.canGuard || entState.unitAI.isGuarding))
return false;
return {
"tooltip": colorizeHotkey("%(hotkey)s" + " ", "session.guard") +
translate("Order the selected units to guard a structure or unit."),
"icon": "add-guard.png",
"enabled": true
};
},
"execute": function()
{
inputState = INPUT_PRESELECTEDACTION;
preSelectedAction = ACTION_GUARD;
},
"allowedPlayers": ["Player"]
},
"remove-guard": {
"getInfo": function(entStates)
{
if (entStates.every(entState => !entState.unitAI || !entState.unitAI.isGuarding))
return false;
return {
"tooltip": translate("Remove guard"),
"icon": "remove-guard.png",
"enabled": true
};
},
"execute": function()
{
removeGuard();
},
"allowedPlayers": ["Player"]
},
"select-trading-goods": {
"getInfo": function(entStates)
{
if (entStates.every(entState => !entState.market))
return false;
return {
"tooltip": translate("Barter & Trade"),
"icon": "economics.png",
"enabled": true
};
},
"execute": function()
{
g_TradeDialog.toggle();
},
"allowedPlayers": ["Player"]
},
"patrol": {
"getInfo": function(entStates)
{
if (!entStates.some(entState => entState.unitAI && entState.unitAI.canPatrol))
return false;
return {
"tooltip": colorizeHotkey("%(hotkey)s" + " ", "session.patrol") +
translate("Patrol") + "\n" +
translate("Attack all encountered enemy units while avoiding structures."),
"icon": "patrol.png",
"enabled": true
};
},
"execute": function()
{
inputState = INPUT_PRESELECTEDACTION;
preSelectedAction = ACTION_PATROL;
},
"allowedPlayers": ["Player"]
},
"share-dropsite": {
"getInfo": function(entStates)
{
let sharableEntities = entStates.filter(
entState => entState.resourceDropsite && entState.resourceDropsite.sharable);
if (!sharableEntities.length)
return false;
// Returns if none of the entities belong to a player with a mutual ally.
if (entStates.every(entState => !GetSimState().players[entState.player].isMutualAlly.some(
(isAlly, playerId) => isAlly && playerId != entState.player)))
return false;
return sharableEntities.some(entState => !entState.resourceDropsite.shared) ?
{
"tooltip": translate("Press to allow allies to use this dropsite"),
"icon": "locked_small.png",
"enabled": true
} :
{
"tooltip": translate("Press to prevent allies from using this dropsite"),
"icon": "unlocked_small.png",
"enabled": true
};
},
"execute": function(entStates)
{
let sharableEntities = entStates.filter(
entState => entState.resourceDropsite && entState.resourceDropsite.sharable);
if (sharableEntities)
Engine.PostNetworkCommand({
"type": "set-dropsite-sharing",
"entities": sharableEntities.map(entState => entState.id),
"shared": sharableEntities.some(entState => !entState.resourceDropsite.shared)
});
},
"allowedPlayers": ["Player"]
},
"is-dropsite-shared": {
"getInfo": function(entStates)
{
let shareableEntities = entStates.filter(
entState => entState.resourceDropsite && entState.resourceDropsite.sharable);
if (!shareableEntities.length)
return false;
let player = Engine.GetPlayerID();
let simState = GetSimState();
if (!g_IsObserver && !simState.players[player].hasSharedDropsites ||
shareableEntities.every(entState => controlsPlayer(entState.player)))
return false;
if (!shareableEntities.every(entState => entState.resourceDropsite.shared))
return {
"tooltip": translate("The use of this dropsite is prohibited"),
"icon": "locked_small.png",
"enabled": false
};
return {
"tooltip": g_IsObserver ? translate("Allies are allowed to use this dropsite.") :
translate("You are allowed to use this dropsite"),
"icon": "unlocked_small.png",
"enabled": false
};
},
"execute": function(entState)
{
// This command button is always disabled.
},
"allowedPlayers": ["Ally", "Observer"]
}
};
function playerCheck(entState, targetState, validPlayers)
{
let playerState = GetSimState().players[entState.player];
for (let player of validPlayers)
if (player == "Gaia" && targetState.player == 0 ||
player == "Player" && targetState.player == entState.player ||
playerState["is" + player] && playerState["is" + player][targetState.player])
return true;
return false;
}
/**
* Checks whether the entities have the right diplomatic status
* with respect to the currently active player.
* Also "Observer" can be used.
*
* @param {Object[]} entStates - An array containing the entity states to check.
* @param {string[]} validPlayers - An array containing the diplomatic statuses.
*
* @return {boolean} - Whether the currently active player is allowed.
*/
function allowedPlayersCheck(entStates, validPlayers)
{
// Assume we can only select entities from one player,
// or it does not matter (e.g. observer).
let targetState = entStates[0];
let playerState = GetSimState().players[Engine.GetPlayerID()];
return validPlayers.some(player =>
player == "Observer" && g_IsObserver ||
player == "Player" && controlsPlayer(targetState.player) ||
playerState && playerState["is" + player] && playerState["is" + player][targetState.player]);
}
function hasClass(entState, className)
{
// note: use the functions in globalscripts/Templates.js for more versatile matching
return entState.identity && entState.identity.classes.indexOf(className) != -1;
}
/**
* Keep in sync with Commands.js.
*/
function isUndeletable(entState)
{
let playerState = g_SimState.players[entState.player];
if (playerState && playerState.controlsAll)
return false;
if (entState.resourceSupply && entState.resourceSupply.killBeforeGather)
return translate("The entity has to be killed before it can be gathered from");
if (entState.capturePoints && entState.capturePoints[entState.player] < entState.maxCapturePoints / 2)
return translate("You cannot destroy this entity as you own less than half the capture points");
if (!entState.identity.canDelete)
return translate("This entity is undeletable");
return false;
}
function DrawTargetMarker(target)
{
Engine.GuiInterfaceCall("AddTargetMarker", {
"template": g_TargetMarker.move,
"x": target.x,
"z": target.z
});
}
function getCommandInfo(command, entStates)
{
return entStates && g_EntityCommands[command] &&
allowedPlayersCheck(entStates, g_EntityCommands[command].allowedPlayers) &&
g_EntityCommands[command].getInfo(entStates);
}
function getActionInfo(action, target, selection)
{
if (!selection || !selection.length || !GetEntityState(selection[0]))
return { "possible": false };
// Look at the first targeted entity
// (TODO: maybe we eventually want to look at more, and be more context-sensitive?
// e.g. prefer to attack an enemy unit, even if some friendly units are closer to the mouse)
let targetState = GetEntityState(target);
let simState = GetSimState();
let playerState = g_SimState.players[g_ViewedPlayer];
// Check if any entities in the selection can do some of the available actions.
for (let entityID of selection)
{
let entState = GetEntityState(entityID);
if (!entState)
continue;
if (playerState && !playerState.controlsAll && !entState.identity.controllable)
continue;
if (g_UnitActions[action] && g_UnitActions[action].getActionInfo)
{
let r = g_UnitActions[action].getActionInfo(entState, targetState, simState);
if (r && r.possible)
{
r.entity = entityID;
return r;
}
}
}
return { "possible": false };
}
Index: ps/trunk/binaries/data/mods/public/maps/random/rmgen-common/player.js
===================================================================
--- ps/trunk/binaries/data/mods/public/maps/random/rmgen-common/player.js (revision 24988)
+++ ps/trunk/binaries/data/mods/public/maps/random/rmgen-common/player.js (revision 24989)
@@ -1,843 +1,843 @@
/**
* @file These functions locate and place the starting entities of players.
*/
var g_NomadTreasureTemplates = {
"food": "gaia/treasure/food_jars",
"wood": "gaia/treasure/wood",
"stone": "gaia/treasure/stone",
"metal": "gaia/treasure/metal"
};
/**
* These are identifiers of functions that can generate parts of a player base.
* There must be a function starting with placePlayerBase and ending with this name.
* This is a global so mods can extend this from external files.
*/
var g_PlayerBaseFunctions = [
// Possibly mark player class first here and use it afterwards
"CityPatch",
// Create the largest and most important entities first
"Trees",
"Mines",
"Treasures",
"Berries",
"Chicken",
"Decoratives"
];
function isNomad()
{
return !!g_MapSettings.Nomad;
}
function getNumPlayers()
{
return g_MapSettings.PlayerData.length - 1;
}
function getCivCode(playerID)
{
return g_MapSettings.PlayerData[playerID].Civ;
}
function areAllies(playerID1, playerID2)
{
return g_MapSettings.PlayerData[playerID1].Team !== undefined &&
g_MapSettings.PlayerData[playerID2].Team !== undefined &&
g_MapSettings.PlayerData[playerID1].Team != -1 &&
g_MapSettings.PlayerData[playerID2].Team != -1 &&
g_MapSettings.PlayerData[playerID1].Team === g_MapSettings.PlayerData[playerID2].Team;
}
function getPlayerTeam(playerID)
{
if (g_MapSettings.PlayerData[playerID].Team === undefined)
return -1;
return g_MapSettings.PlayerData[playerID].Team;
}
/**
* Gets the default starting entities for the civ of the given player, as defined by the civ file.
*/
function getStartingEntities(playerID)
{
return g_CivData[getCivCode(playerID)].StartEntities;
}
/**
* Places the given entities at the given location (typically a civic center and starting units).
* @param location - A Vector2D specifying tile coordinates.
* @param civEntities - An array of objects with the Template property and optionally a Count property.
* The first entity is placed in the center, the other ones surround it.
*/
function placeStartingEntities(location, playerID, civEntities, dist = 6, orientation = BUILDING_ORIENTATION)
{
// Place the central structure
let i = 0;
let firstTemplate = civEntities[i].Template;
if (firstTemplate.startsWith("structures/"))
{
g_Map.placeEntityPassable(firstTemplate, playerID, location, orientation);
++i;
}
// Place entities surrounding it
let space = 2;
for (let j = i; j < civEntities.length; ++j)
{
let angle = orientation - Math.PI * (1 - j / 2);
let count = civEntities[j].Count || 1;
for (let num = 0; num < count; ++num)
{
let position = Vector2D.sum([
location,
new Vector2D(dist, 0).rotate(-angle),
new Vector2D(space * (-num + (count - 1) / 2), 0).rotate(angle)
]);
g_Map.placeEntityPassable(civEntities[j].Template, playerID, position, angle);
}
}
}
/**
* Places the default starting entities as defined by the civilization definition, optionally including city walls.
*/
function placeCivDefaultStartingEntities(position, playerID, wallType, dist = 6, orientation = BUILDING_ORIENTATION)
{
placeStartingEntities(position, playerID, getStartingEntities(playerID), dist, orientation);
placeStartingWalls(position, playerID, wallType, orientation);
}
/**
* If the map is large enough and the civilization defines them, places the initial city walls or towers.
* @param {string|boolean} wallType - Either "towers" to only place the wall turrets or a boolean indicating enclosing city walls.
*/
function placeStartingWalls(position, playerID, wallType, orientation = BUILDING_ORIENTATION)
{
let civ = getCivCode(playerID);
if (civ != "iber" || g_Map.getSize() <= 128)
return;
// TODO: should prevent trees inside walls
// When fixing, remove the DeleteUponConstruction flag from template_gaia_flora.xml
if (wallType == "towers")
placePolygonalWall(position, 15, ["entry"], "tower", civ, playerID, orientation, 7);
else if (wallType)
placeGenericFortress(position, 20, playerID);
}
/**
* Places the civic center and starting resources for all given players.
*/
function placePlayerBases(playerBaseArgs)
{
g_Map.log("Creating playerbases");
let [playerIDs, playerPosition] = playerBaseArgs.PlayerPlacement;
for (let i = 0; i < getNumPlayers(); ++i)
{
playerBaseArgs.playerID = playerIDs[i];
playerBaseArgs.playerPosition = playerPosition[i];
placePlayerBase(playerBaseArgs);
}
}
/**
* Places the civic center and starting resources.
*/
function placePlayerBase(playerBaseArgs)
{
if (isNomad())
return;
placeCivDefaultStartingEntities(playerBaseArgs.playerPosition, playerBaseArgs.playerID, playerBaseArgs.Walls !== undefined ? playerBaseArgs.Walls : true);
if (playerBaseArgs.PlayerTileClass)
addCivicCenterAreaToClass(playerBaseArgs.playerPosition, playerBaseArgs.PlayerTileClass);
for (let functionID of g_PlayerBaseFunctions)
{
let funcName = "placePlayerBase" + functionID;
let func = global[funcName];
if (!func)
throw new Error("Could not find " + funcName);
if (!playerBaseArgs[functionID])
continue;
let args = playerBaseArgs[functionID];
// Copy some global arguments to the arguments for each function
for (let prop of ["playerID", "playerPosition", "BaseResourceClass", "baseResourceConstraint"])
args[prop] = playerBaseArgs[prop];
func(args);
}
}
function defaultPlayerBaseRadius()
{
return scaleByMapSize(15, 25);
}
/**
* Marks the corner and center tiles of an area that is about the size of a Civic Center with the given TileClass.
* Used to prevent resource collisions with the Civic Center.
*/
function addCivicCenterAreaToClass(position, tileClass)
{
createArea(
new DiskPlacer(5, position),
new TileClassPainter(tileClass));
}
/**
* Helper function.
*/
function getPlayerBaseArgs(playerBaseArgs)
{
let baseResourceConstraint = playerBaseArgs.BaseResourceClass && avoidClasses(playerBaseArgs.BaseResourceClass, 4);
if (playerBaseArgs.baseResourceConstraint)
baseResourceConstraint = new AndConstraint([baseResourceConstraint, playerBaseArgs.baseResourceConstraint]);
return [
(property, defaultVal) => playerBaseArgs[property] === undefined ? defaultVal : playerBaseArgs[property],
playerBaseArgs.playerPosition,
baseResourceConstraint
];
}
function placePlayerBaseCityPatch(args)
{
let [get, basePosition, baseResourceConstraint] = getPlayerBaseArgs(args);
let painters = [];
if (args.outerTerrain && args.innerTerrain)
painters.push(new LayeredPainter([args.outerTerrain, args.innerTerrain], [get("width", 1)]));
if (args.painters)
painters = painters.concat(args.painters);
createArea(
new ClumpPlacer(
Math.floor(diskArea(get("radius", defaultPlayerBaseRadius() / 3))),
get("coherence", 0.6),
get("smoothness", 0.3),
get("failFraction", Infinity),
basePosition),
painters);
}
function placePlayerBaseChicken(args)
{
let [get, basePosition, baseResourceConstraint] = getPlayerBaseArgs(args);
for (let i = 0; i < get("groupCount", 2); ++i)
{
let success = false;
for (let tries = 0; tries < get("maxTries", 30); ++tries)
{
let position = new Vector2D(0, get("distance", 9)).rotate(randomAngle()).add(basePosition);
if (createObjectGroup(
new SimpleGroup(
[
new SimpleObject(
get("template", "gaia/fauna_chicken"),
get("minGroupCount", 5),
get("maxGroupCount", 5),
get("minGroupDistance", 0),
get("maxGroupDistance", 2))
],
true,
args.BaseResourceClass,
position),
0,
baseResourceConstraint))
{
success = true;
break;
}
}
if (!success)
{
error("Could not place chicken for player " + args.playerID);
return;
}
}
}
function placePlayerBaseBerries(args)
{
let [get, basePosition, baseResourceConstraint] = getPlayerBaseArgs(args);
for (let tries = 0; tries < get("maxTries", 30); ++tries)
{
let position = new Vector2D(0, get("distance", 12)).rotate(randomAngle()).add(basePosition);
if (createObjectGroup(
new SimpleGroup(
[new SimpleObject(args.template, get("minCount", 5), get("maxCount", 5), get("maxDist", 1), get("maxDist", 3))],
true,
args.BaseResourceClass,
position),
0,
baseResourceConstraint))
return;
}
error("Could not place berries for player " + args.playerID);
}
function placePlayerBaseMines(args)
{
let [get, basePosition, baseResourceConstraint] = getPlayerBaseArgs(args);
let angleBetweenMines = randFloat(get("minAngle", Math.PI / 6), get("maxAngle", Math.PI / 3));
let mineCount = args.types.length;
let groupElements = [];
if (args.groupElements)
groupElements = groupElements.concat(args.groupElements);
for (let tries = 0; tries < get("maxTries", 75); ++tries)
{
// First find a place where all mines can be placed
let pos = [];
let startAngle = randomAngle();
for (let i = 0; i < mineCount; ++i)
{
let angle = startAngle + angleBetweenMines * (i + (mineCount - 1) / 2);
pos[i] = new Vector2D(0, get("distance", 12)).rotate(angle).add(basePosition).round();
if (!g_Map.validTilePassable(pos[i]) || !baseResourceConstraint.allows(pos[i]))
{
pos = undefined;
break;
}
}
if (!pos)
continue;
// Place the mines
for (let i = 0; i < mineCount; ++i)
{
if (args.types[i].type && args.types[i].type == "stone_formation")
{
createStoneMineFormation(pos[i], args.types[i].template, args.types[i].terrain);
args.BaseResourceClass.add(pos[i]);
continue;
}
createObjectGroup(
new SimpleGroup(
[new SimpleObject(args.types[i].template, 1, 1, 0, 0)].concat(groupElements),
true,
args.BaseResourceClass,
pos[i]),
0);
}
return;
}
error("Could not place mines for player " + args.playerID);
}
function placePlayerBaseTrees(args)
{
let [get, basePosition, baseResourceConstraint] = getPlayerBaseArgs(args);
let num = Math.floor(get("count", scaleByMapSize(7, 20)));
for (let x = 0; x < get("maxTries", 30); ++x)
{
let position = new Vector2D(0, randFloat(get("minDist", 11), get("maxDist", 13))).rotate(randomAngle()).add(basePosition).round();
if (createObjectGroup(
new SimpleGroup(
[new SimpleObject(args.template, num, num, get("minDistGroup", 0), get("maxDistGroup", 5))],
false,
args.BaseResourceClass,
position),
0,
baseResourceConstraint))
return;
}
error("Could not place starting trees for player " + args.playerID);
}
function placePlayerBaseTreasures(args)
{
let [get, basePosition, baseResourceConstraint] = getPlayerBaseArgs(args);
for (let resourceTypeArgs of args.types)
{
get = (property, defaultVal) => resourceTypeArgs[property] === undefined ? defaultVal : resourceTypeArgs[property];
let success = false;
for (let tries = 0; tries < get("maxTries", 30); ++tries)
{
let position = new Vector2D(0, randFloat(get("minDist", 11), get("maxDist", 13))).rotate(randomAngle()).add(basePosition).round();
if (createObjectGroup(
new SimpleGroup(
[new SimpleObject(resourceTypeArgs.template, get("count", 14), get("count", 14), get("minDistGroup", 1), get("maxDistGroup", 3))],
false,
args.BaseResourceClass,
position),
0,
baseResourceConstraint))
{
success = true;
break;
}
}
if (!success)
{
error("Could not place treasure " + resourceTypeArgs.template + " for player " + args.playerID);
return;
}
}
}
/**
* Typically used for placing grass tufts around the civic centers.
*/
function placePlayerBaseDecoratives(args)
{
let [get, basePosition, baseResourceConstraint] = getPlayerBaseArgs(args);
for (let i = 0; i < get("count", scaleByMapSize(2, 5)); ++i)
{
let success = false;
for (let x = 0; x < get("maxTries", 30); ++x)
{
let position = new Vector2D(0, randIntInclusive(get("minDist", 8), get("maxDist", 11))).rotate(randomAngle()).add(basePosition).round();
if (createObjectGroup(
new SimpleGroup(
[new SimpleObject(args.template, get("minCount", 2), get("maxCount", 5), 0, 1)],
false,
args.BaseResourceClass,
position),
0,
baseResourceConstraint))
{
success = true;
break;
}
}
if (!success)
// Don't warn since the decoratives are not important
return;
}
}
function placePlayersNomad(playerClass, constraints)
{
if (!isNomad())
return undefined;
g_Map.log("Placing nomad starting units");
let distance = scaleByMapSize(60, 240);
let constraint = new StaticConstraint(constraints);
let numPlayers = getNumPlayers();
let playerIDs = shuffleArray(sortAllPlayers());
let playerPosition = [];
for (let i = 0; i < numPlayers; ++i)
{
let objects = getStartingEntities(playerIDs[i]).filter(ents => ents.Template.startsWith("units/")).map(
ents => new SimpleObject(ents.Template, ents.Count || 1, ents.Count || 1, 1, 3));
// Add treasure if too few resources for a civic center
let ccCost = Engine.GetTemplate("structures/" + getCivCode(playerIDs[i]) + "/civil_centre").Cost.Resources;
for (let resourceType in ccCost)
{
let treasureTemplate = g_NomadTreasureTemplates[resourceType];
let count = Math.max(0, Math.ceil(
(ccCost[resourceType] - (g_MapSettings.StartingResources || 0)) /
- Engine.GetTemplate(treasureTemplate).ResourceSupply.Amount));
+ Engine.GetTemplate(treasureTemplate).Treasure.Resources[resourceType]));
objects.push(new SimpleObject(treasureTemplate, count, count, 3, 5));
}
// Try place these entities at a random location
let group = new SimpleGroup(objects, true, playerClass);
let success = false;
for (let distanceFactor of [1, 1/2, 1/4, 0])
if (createObjectGroups(group, playerIDs[i], new AndConstraint([constraint, avoidClasses(playerClass, distance * distanceFactor)]), 1, 200, false).length)
{
success = true;
playerPosition[i] = group.centerPosition;
break;
}
if (!success)
throw new Error("Could not place starting units for player " + playerIDs[i] + "!");
}
return [playerIDs, playerPosition];
}
/**
* Sorts an array of player IDs by team index. Players without teams come first.
* Randomize order for players of the same team.
*/
function sortPlayers(playerIDs)
{
return shuffleArray(playerIDs).sort((playerID1, playerID2) => getPlayerTeam(playerID1) - getPlayerTeam(playerID2));
}
/**
* Randomize playerIDs but sort by team.
*
* @returns {Array} - every item is an array of player indices
*/
function sortAllPlayers()
{
let playerIDs = [];
for (let i = 0; i < getNumPlayers(); ++i)
playerIDs.push(i+1);
return sortPlayers(playerIDs);
}
/**
* Rearrange order so that teams of neighboring players alternate (if the given IDs are sorted by team).
*/
function primeSortPlayers(playerIDs)
{
let prime = [];
for (let i = 0; i < Math.floor(playerIDs.length / 2); ++i)
{
prime.push(playerIDs[i]);
prime.push(playerIDs[playerIDs.length - 1 - i]);
}
if (playerIDs.length % 2)
prime.push(playerIDs[Math.floor(playerIDs.length / 2)]);
return prime;
}
function primeSortAllPlayers()
{
return primeSortPlayers(sortAllPlayers());
}
/*
* Separates playerIDs into two arrays such that teammates are in the same array,
* unless everyone's on the same team in which case they'll be split in half.
*/
function partitionPlayers(playerIDs)
{
let teamIDs = Array.from(new Set(playerIDs.map(getPlayerTeam)));
let teams = teamIDs.map(teamID => playerIDs.filter(playerID => getPlayerTeam(playerID) == teamID));
if (teamIDs.indexOf(-1) != -1)
teams = teams.concat(teams.splice(teamIDs.indexOf(-1), 1)[0].map(playerID => [playerID]));
if (teams.length == 1)
{
let idx = Math.floor(teams[0].length / 2);
teams = [teams[0].slice(idx), teams[0].slice(0, idx)];
}
teams.sort((a, b) => b.length - a.length);
// Use the greedy algorithm: add the next team to the side with fewer players
return teams.reduce(([east, west], team) =>
east.length > west.length ?
[east, west.concat(team)] :
[east.concat(team), west],
[[], []]);
}
/**
* Determine player starting positions on a circular pattern.
*/
function playerPlacementCircle(radius, startingAngle = undefined, center = undefined)
{
let startAngle = startingAngle !== undefined ? startingAngle : randomAngle();
let [playerPosition, playerAngle] = distributePointsOnCircle(getNumPlayers(), startAngle, radius, center || g_Map.getCenter());
return [sortAllPlayers(), playerPosition.map(p => p.round()), playerAngle, startAngle];
}
/**
* Determine player starting positions on a circular pattern, with a custom angle for each player.
* Commonly used for gulf terrains.
*/
function playerPlacementCustomAngle(radius, center, playerAngleFunc)
{
let playerPosition = [];
let playerAngle = [];
let numPlayers = getNumPlayers();
for (let i = 0; i < numPlayers; ++i)
{
playerAngle[i] = playerAngleFunc(i);
playerPosition[i] = Vector2D.add(center, new Vector2D(radius, 0).rotate(-playerAngle[i])).round();
}
return [playerPosition, playerAngle];
}
/**
* Returns player starting positions equally spaced along an arc.
*/
function playerPlacementArc(playerIDs, center, radius, startAngle, endAngle)
{
return distributePointsOnCircularSegment(
playerIDs.length + 2,
endAngle - startAngle,
startAngle,
radius,
center
)[0].slice(1, -1).map(p => p.round());
}
/**
* Returns player starting positions located on two symmetrically placed arcs, with teammates placed on the same arc.
*/
function playerPlacementArcs(playerIDs, center, radius, mapAngle, startAngle, endAngle)
{
let [east, west] = partitionPlayers(playerIDs);
let eastPosition = playerPlacementArc(east, center, radius, mapAngle + startAngle, mapAngle + endAngle);
let westPosition = playerPlacementArc(west, center, radius, mapAngle - startAngle, mapAngle - endAngle);
return playerIDs.map(playerID => east.indexOf(playerID) != -1 ?
eastPosition[east.indexOf(playerID)] :
westPosition[west.indexOf(playerID)]);
}
/**
* Returns player starting positions located on two parallel lines, typically used by central river maps.
* If there are two teams with an equal number of players, each team will occupy exactly one line.
* Angle 0 means the players are placed in north to south direction, i.e. along the Z axis.
*/
function playerPlacementRiver(angle, width, center = undefined)
{
let numPlayers = getNumPlayers();
let numPlayersEven = numPlayers % 2 == 0;
let mapSize = g_Map.getSize();
let centerPosition = center || g_Map.getCenter();
let playerPosition = [];
for (let i = 0; i < numPlayers; ++i)
{
let currentPlayerEven = i % 2 == 0;
let offsetDivident = numPlayersEven || currentPlayerEven ? (i + 1) % 2 : 0;
let offsetDivisor = numPlayersEven ? 0 : currentPlayerEven ? +1 : -1;
playerPosition[i] = new Vector2D(
width * (i % 2) + (mapSize - width) / 2,
fractionToTiles(((i - 1 + offsetDivident) / 2 + 1) / ((numPlayers + offsetDivisor) / 2 + 1))
).rotateAround(angle, centerPosition).round();
}
return [primeSortAllPlayers(), playerPosition];
}
/**
* Returns starting positions located on two parallel lines.
* The locations on the first line are shifted in comparison to the other line.
*/
function playerPlacementLine(angle, center, width)
{
let playerPosition = [];
let numPlayers = getNumPlayers();
for (let i = 0; i < numPlayers; ++i)
playerPosition[i] = Vector2D.add(
center,
new Vector2D(
fractionToTiles((i + 1) / (numPlayers + 1) - 0.5),
width * (i % 2 - 1/2)
).rotate(angle)
).round();
return playerPosition;
}
/**
* Returns a random location for each player that meets the given constraints and
* orders the playerIDs so that players become grouped by team.
*/
function playerPlacementRandom(playerIDs, constraints = undefined)
{
let locations = [];
let attempts = 0;
let resets = 0;
let mapCenter = g_Map.getCenter();
let playerMinDistSquared = Math.square(fractionToTiles(0.25));
let borderDistance = fractionToTiles(0.08);
let area = createArea(new MapBoundsPlacer(), undefined, new AndConstraint(constraints));
for (let i = 0; i < getNumPlayers(); ++i)
{
let position = pickRandom(area.getPoints());
// Minimum distance between initial bases must be a quarter of the map diameter
if (locations.some(loc => loc.distanceToSquared(position) < playerMinDistSquared) ||
position.distanceToSquared(mapCenter) > Math.square(mapCenter.x - borderDistance))
{
--i;
++attempts;
// Reset if we're in what looks like an infinite loop
if (attempts > 500)
{
locations = [];
i = -1;
attempts = 0;
++resets;
// Reduce minimum player distance progressively
if (resets % 25 == 0)
playerMinDistSquared *= 0.95;
// If we only pick bad locations, stop trying to place randomly
if (resets == 500)
return undefined;
}
continue;
}
locations[i] = position;
}
return groupPlayersByArea(playerIDs, locations);
}
/**
* Pick locations from the given set so that teams end up grouped.
*/
function groupPlayersByArea(playerIDs, locations)
{
playerIDs = sortPlayers(playerIDs);
let minDist = Infinity;
let minLocations;
// Of all permutations of starting locations, find the one where
// the sum of the distances between allies is minimal, weighted by teamsize.
heapsPermute(shuffleArray(locations).slice(0, playerIDs.length), v => v.clone(), permutation => {
let dist = 0;
let teamDist = 0;
let teamSize = 0;
for (let i = 1; i < playerIDs.length; ++i)
{
let team1 = getPlayerTeam(playerIDs[i - 1]);
let team2 = getPlayerTeam(playerIDs[i]);
++teamSize;
if (team1 != -1 && team1 == team2)
teamDist += permutation[i - 1].distanceTo(permutation[i]);
else
{
dist += teamDist / teamSize;
teamDist = 0;
teamSize = 0;
}
}
if (teamSize)
dist += teamDist / teamSize;
if (dist < minDist)
{
minDist = dist;
minLocations = permutation;
}
});
return [playerIDs, minLocations];
}
/**
* Sorts the playerIDs so that team members are as close as possible on a ring.
*/
function groupPlayersCycle(startLocations)
{
let startLocationOrder = sortPointsShortestCycle(startLocations);
let newStartLocations = [];
for (let i = 0; i < startLocations.length; ++i)
newStartLocations.push(startLocations[startLocationOrder[i]]);
startLocations = newStartLocations;
// Sort players by team
let playerIDs = [];
let teams = [];
for (let i = 0; i < g_MapSettings.PlayerData.length - 1; ++i)
{
playerIDs.push(i+1);
let t = g_MapSettings.PlayerData[i + 1].Team;
if (teams.indexOf(t) == -1 && t !== undefined)
teams.push(t);
}
playerIDs = sortPlayers(playerIDs);
if (!teams.length)
return [playerIDs, startLocations];
// Minimize maximum distance between players within a team
let minDistance = Infinity;
let bestShift;
for (let s = 0; s < playerIDs.length; ++s)
{
let maxTeamDist = 0;
for (let pi = 0; pi < playerIDs.length - 1; ++pi)
{
let t1 = getPlayerTeam(playerIDs[(pi + s) % playerIDs.length]);
if (teams.indexOf(t1) === -1)
continue;
for (let pj = pi + 1; pj < playerIDs.length; ++pj)
{
if (t1 != getPlayerTeam(playerIDs[(pj + s) % playerIDs.length]))
continue;
maxTeamDist = Math.max(
maxTeamDist,
Math.euclidDistance2D(
startLocations[pi].x,
startLocations[pi].y,
startLocations[pj].x,
startLocations[pj].y));
}
}
if (maxTeamDist < minDistance)
{
minDistance = maxTeamDist;
bestShift = s;
}
}
if (bestShift)
{
let newPlayerIDs = [];
for (let i = 0; i < playerIDs.length; ++i)
newPlayerIDs.push(playerIDs[(i + bestShift) % playerIDs.length]);
playerIDs = newPlayerIDs;
}
return [playerIDs, startLocations];
}
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 24988)
+++ ps/trunk/binaries/data/mods/public/simulation/ai/common-api/entity.js (revision 24989)
@@ -1,1004 +1,1021 @@
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;
+ return this.get("ResourceSupply/Type").split('.')[0];
},
"getDiminishingReturns": function() { return +(this.get("ResourceSupply/DiminishingReturns") || 1); },
"resourceSupplyMax": function() { return +this.get("ResourceSupply/Max"); },
"maxGatherers": function() { return +(this.get("ResourceSupply/MaxGatherers") || 0); },
"resourceGatherRates": function() {
if (!this.get("ResourceGatherer"))
return undefined;
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+/) : [];
},
+ "isTreasure": function() { return this.get("Treasure") !== undefined; },
+
+ "treasureResources": function() {
+ if (!this.get("Treasure"))
+ return undefined;
+ let ret = {};
+ for (let r in this.get("Treasure/Resources"))
+ ret[r] = +this.get("Treasure/Resources/" + r);
+ return ret;
+ },
+
"garrisonableClasses": function() { return this.get("GarrisonHolder/List/_string"); },
"garrisonMax": function() { return this.get("GarrisonHolder/Max"); },
"garrisonSize": function() { return this.get("Garrisonable/Size"); },
"garrisonEjectHealth": function() { return +this.get("GarrisonHolder/EjectHealth"); },
"getDefaultArrow": function() { return +this.get("BuildingAI/DefaultArrowCount"); },
"getArrowMultiplier": function() { return +this.get("BuildingAI/GarrisonArrowMultiplier"); },
"getGarrisonArrowClasses": function() {
if (!this.get("BuildingAI"))
return undefined;
return this.get("BuildingAI/GarrisonArrowClasses").split(/\s+/);
},
"buffHeal": function() { return +this.get("GarrisonHolder/BuffHeal"); },
"promotion": function() { return this.get("Promotion/Entity"); },
"isPackable": function() { return this.get("Pack") != undefined; },
"isHuntable": function() {
// Do not hunt retaliating animals (dead animals can be used).
// Assume entities which can attack, will attack.
return this.get("ResourceSupply/KillBeforeGather") &&
(!this.get("Health") || !this.get("Attack"));
},
"walkSpeed": function() { return +this.get("UnitMotion/WalkSpeed"); },
"trainingCategory": function() { return this.get("TrainingRestrictions/Category"); },
"buildTime": function(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; },
+
+ "isTreasureCollecter": function() { return this.get("TreasureCollecter") !== undefined; },
});
// defines an entity, with a super Template.
// also redefines several of the template functions where the only change is applying aura and tech modifications.
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;
},
+ "collectTreasure": function(target, queued = false) {
+ Engine.PostCommand(PlayerID, {
+ "type": "collect-treasure",
+ "entities": [this.id()],
+ "target": target.id(),
+ "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/ai/common-api/filters.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/ai/common-api/filters.js (revision 24988)
+++ ps/trunk/binaries/data/mods/public/simulation/ai/common-api/filters.js (revision 24989)
@@ -1,163 +1,169 @@
var API3 = function(m)
{
m.Filters = {
"byType": type => ({
"func": ent => ent.templateName() == type,
"dynamicProperties": []
}),
"byClass": cls => ({
"func": ent => ent.hasClass(cls),
"dynamicProperties": []
}),
"byClassesAnd": clsList => ({
"func": ent => clsList.every(cls => ent.hasClass(cls)),
"dynamicProperties": []
}),
"byClassesOr": clsList => ({
"func": ent => clsList.some(cls => ent.hasClass(cls)),
"dynamicProperties": []
}),
"byMetadata": (player, key, value) => ({
"func": ent => ent.getMetadata(player, key) == value,
"dynamicProperties": ['metadata.' + key]
}),
"byHasMetadata": (player, key) => ({
"func": ent => ent.getMetadata(player, key) !== undefined,
"dynamicProperties": ['metadata.' + key]
}),
"and": (filter1, filter2) => ({
"func": ent => filter1.func(ent) && filter2.func(ent),
"dynamicProperties": filter1.dynamicProperties.concat(filter2.dynamicProperties)
}),
"or": (filter1, filter2) => ({
"func": ent => filter1.func(ent) || filter2.func(ent),
"dynamicProperties": filter1.dynamicProperties.concat(filter2.dynamicProperties)
}),
"not": (filter) => ({
"func": ent => !filter.func(ent),
"dynamicProperties": filter.dynamicProperties
}),
"byOwner": owner => ({
"func": ent => ent.owner() == owner,
"dynamicProperties": ['owner']
}),
"byNotOwner": owner => ({
"func": ent => ent.owner() != owner,
"dynamicProperties": ['owner']
}),
"byOwners": owners => ({
"func": ent => owners.some(owner => owner == ent.owner()),
"dynamicProperties": ['owner']
}),
"byCanGarrison": () => ({
"func": ent => ent.garrisonMax() > 0,
"dynamicProperties": []
}),
"byTrainingQueue": () => ({
"func": ent => ent.trainingQueue(),
"dynamicProperties": ['trainingQueue']
}),
"byResearchAvailable": (gameState, civ) => ({
"func": ent => ent.researchableTechs(gameState, civ) !== undefined,
"dynamicProperties": []
}),
"byCanAttackClass": aClass => ({
"func": ent => ent.canAttackClass(aClass),
"dynamicProperties": []
}),
"byCanAttackTarget": target => ({
"func": ent => ent.canAttackTarget(target),
"dynamicProperties": []
}),
"isGarrisoned": () => ({
"func": ent => ent.position() === undefined,
"dynamicProperties": []
}),
"isIdle": () => ({
"func": ent => ent.isIdle(),
"dynamicProperties": ['idle']
}),
"isFoundation": () => ({
"func": ent => ent.foundationProgress() !== undefined,
"dynamicProperties": []
}),
"isBuilt": () => ({
"func": ent => ent.foundationProgress() === undefined,
"dynamicProperties": []
}),
"hasDefensiveFire": () => ({
"func": ent => ent.hasDefensiveFire(),
"dynamicProperties": []
}),
"isDropsite": resourceType => ({
"func": ent => ent.resourceDropsiteTypes() && (resourceType === undefined || ent.resourceDropsiteTypes().indexOf(resourceType) != -1),
"dynamicProperties": []
}),
+ "isTreasure": () => ({
+ "func": ent => {
+ if (!ent.isTreasure())
+ return false;
+
+ // Don't go for floating treasures since we might not be able
+ // to reach them and that kills the pathfinder.
+ let template = ent.templateName();
+ return template != "gaia/treasure/shipwreck_debris" &&
+ template != "gaia/treasure/shipwreck";
+ },
+ "dynamicProperties": []
+ }),
+
"byResource": resourceType => ({
"func": ent => {
if (!ent.resourceSupplyMax())
return false;
let type = ent.resourceSupplyType();
if (!type)
return false;
// Skip targets that are too hard to hunt
if (!ent.isHuntable() || ent.hasClass("SeaCreature"))
return false;
- // Don't go for floating treasures since we won't be able to reach them and it kills the pathfinder.
- if (ent.templateName() == "gaia/treasure/shipwreck_debris" ||
- ent.templateName() == "gaia/treasure/shipwreck")
- return false;
-
- if (type.generic == "treasure")
- return resourceType == type.specific;
-
return resourceType == type.generic;
},
"dynamicProperties": []
}),
"isHuntable": () => ({
// Skip targets that are too hard to hunt and don't go for the fish! TODO: better accessibility checks
"func": ent => ent.hasClass("Animal") && ent.resourceSupplyMax() &&
ent.isHuntable() && !ent.hasClass("SeaCreature"),
"dynamicProperties": []
}),
"isFishable": () => ({
// temporarily do not fish moving fish (i.e. whales)
"func": ent => !ent.get("UnitMotion") && ent.hasClass("SeaCreature") && ent.resourceSupplyMax(),
"dynamicProperties": []
})
};
return m;
}(API3);
Index: ps/trunk/binaries/data/mods/public/simulation/ai/common-api/terrain-analysis.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/ai/common-api/terrain-analysis.js (revision 24988)
+++ ps/trunk/binaries/data/mods/public/simulation/ai/common-api/terrain-analysis.js (revision 24989)
@@ -1,479 +1,479 @@
var API3 = function(m)
{
/**
* TerrainAnalysis, inheriting from the Map Component.
*
* This creates a suitable passability map.
* This is part of the Shared Script, and thus should only be used for things that are non-player specific.
* This.map is a map of the world, where particular stuffs are pointed with a value
* For example, impassable land is 0, water is 200, areas near tree (ie forest grounds) are 41…
* This is intended for use with 8 bit maps for reduced memory usage.
* Upgraded from QuantumState's original TerrainAnalysis for qBot.
*/
m.TerrainAnalysis = function()
{
};
m.copyPrototype(m.TerrainAnalysis, m.Map);
m.TerrainAnalysis.prototype.init = function(sharedScript, rawState)
{
let passabilityMap = rawState.passabilityMap;
this.width = passabilityMap.width;
this.height = passabilityMap.height;
this.cellSize = passabilityMap.cellSize;
let obstructionMaskLand = rawState.passabilityClasses["default-terrain-only"];
let obstructionMaskWater = rawState.passabilityClasses["ship-terrain-only"];
let obstructionTiles = new Uint8Array(passabilityMap.data.length);
/* Generated map legend:
0 is impassable
200 is deep water (ie non-passable by land units)
201 is shallow water (passable by land units and water units)
255 is land (or extremely shallow water where ships can't go).
*/
for (let i = 0; i < passabilityMap.data.length; ++i)
{
// If impassable for land units, set to 0, else to 255.
obstructionTiles[i] = (passabilityMap.data[i] & obstructionMaskLand) ? 0 : 255;
if (!(passabilityMap.data[i] & obstructionMaskWater) && obstructionTiles[i] === 0)
obstructionTiles[i] = 200; // if navigable and not walkable (ie basic water), set to 200.
else if (!(passabilityMap.data[i] & obstructionMaskWater) && obstructionTiles[i] === 255)
obstructionTiles[i] = 201; // navigable and walkable.
}
this.Map(rawState, "passability", obstructionTiles);
};
/**
* Accessibility inherits from TerrainAnalysis
*
* This can easily and efficiently determine if any two points are connected.
* it can also determine if any point is "probably" reachable, assuming the unit can get close enough
* for optimizations it's called after the TerrainAnalyser has finished initializing his map
* so this can use the land regions already.
*/
m.Accessibility = function()
{
};
m.copyPrototype(m.Accessibility, m.TerrainAnalysis);
m.Accessibility.prototype.init = function(rawState, terrainAnalyser)
{
this.Map(rawState, "passability", terrainAnalyser.map);
this.landPassMap = new Uint16Array(terrainAnalyser.length);
this.navalPassMap = new Uint16Array(terrainAnalyser.length);
this.maxRegions = 65535;
this.regionSize = [];
this.regionType = []; // "inaccessible", "land" or "water";
// ID of the region associated with an array of region IDs.
this.regionLinks = [];
// initialized to 0, it's more optimized to start at 1 (I'm checking that if it's not 0, then it's already aprt of a region, don't touch);
// However I actually store all unpassable as region 1 (because if I don't, on some maps the toal nb of region is over 256, and it crashes as the mapis 8bit.)
// So start at 2.
this.regionID = 2;
for (let i = 0; i < this.landPassMap.length; ++i)
{
if (this.map[i] !== 0)
{ // any non-painted, non-inacessible area.
if (this.landPassMap[i] == 0 && this.floodFill(i, this.regionID, false))
this.regionType[this.regionID++] = "land";
if (this.navalPassMap[i] == 0 && this.floodFill(i, this.regionID, true))
this.regionType[this.regionID++] = "water";
}
else if (this.landPassMap[i] == 0)
{ // any non-painted, inacessible area.
this.floodFill(i, 1, false);
this.floodFill(i, 1, true);
}
}
// calculating region links. Regions only touching diagonaly are not linked.
// since we're checking all of them, we'll check from the top left to the bottom right
let w = this.width;
for (let x = 0; x < this.width-1; ++x)
{
for (let y = 0; y < this.height-1; ++y)
{
// checking right.
let thisLID = this.landPassMap[x+y*w];
let thisNID = this.navalPassMap[x+y*w];
let rightLID = this.landPassMap[x+1+y*w];
let rightNID = this.navalPassMap[x+1+y*w];
let bottomLID = this.landPassMap[x+y*w+w];
let bottomNID = this.navalPassMap[x+y*w+w];
if (thisLID > 1)
{
if (rightNID > 1)
if (this.regionLinks[thisLID].indexOf(rightNID) === -1)
this.regionLinks[thisLID].push(rightNID);
if (bottomNID > 1)
if (this.regionLinks[thisLID].indexOf(bottomNID) === -1)
this.regionLinks[thisLID].push(bottomNID);
}
if (thisNID > 1)
{
if (rightLID > 1)
if (this.regionLinks[thisNID].indexOf(rightLID) === -1)
this.regionLinks[thisNID].push(rightLID);
if (bottomLID > 1)
if (this.regionLinks[thisNID].indexOf(bottomLID) === -1)
this.regionLinks[thisNID].push(bottomLID);
if (thisLID > 1)
if (this.regionLinks[thisNID].indexOf(thisLID) === -1)
this.regionLinks[thisNID].push(thisLID);
}
}
}
// Engine.DumpImage("LandPassMap.png", this.landPassMap, this.width, this.height, 255);
// Engine.DumpImage("NavalPassMap.png", this.navalPassMap, this.width, this.height, 255);
};
m.Accessibility.prototype.getAccessValue = function(position, onWater)
{
let gamePos = this.gamePosToMapPos(position);
if (onWater)
return this.navalPassMap[gamePos[0] + this.width*gamePos[1]];
let ret = this.landPassMap[gamePos[0] + this.width*gamePos[1]];
if (ret === 1)
{
// quick spiral search.
let indx = [ [-1, -1], [-1, 0], [-1, 1], [0, 1], [1, 1], [1, 0], [1, -1], [0, -1]];
for (let i of indx)
{
let id0 = gamePos[0] + i[0];
let id1 = gamePos[1] + i[1];
if (id0 < 0 || id0 >= this.width || id1 < 0 || id1 >= this.width)
continue;
ret = this.landPassMap[id0 + this.width*id1];
if (ret !== 1)
return ret;
}
}
return ret;
};
m.Accessibility.prototype.getTrajectTo = function(start, end)
{
let pstart = this.gamePosToMapPos(start);
let istart = pstart[0] + pstart[1]*this.width;
let pend = this.gamePosToMapPos(end);
let iend = pend[0] + pend[1]*this.width;
let onLand = true;
if (this.landPassMap[istart] <= 1 && this.navalPassMap[istart] > 1)
onLand = false;
if (this.landPassMap[istart] <= 1 && this.navalPassMap[istart] <= 1)
return false;
let endRegion = this.landPassMap[iend];
if (endRegion <= 1 && this.navalPassMap[iend] > 1)
endRegion = this.navalPassMap[iend];
else if (endRegion <= 1)
return false;
let startRegion = onLand ? this.landPassMap[istart] : this.navalPassMap[istart];
return this.getTrajectToIndex(startRegion, endRegion);
};
/**
* Return a "path" of accessibility indexes from one point to another, including the start and the end indexes
* this can tell you what sea zone you need to have a dock on, for example.
* assumes a land unit unless start point is over deep water.
*/
m.Accessibility.prototype.getTrajectToIndex = function(istart, iend)
{
if (istart === iend)
return [istart];
let trajects = new Set();
let explored = new Set();
trajects.add([istart]);
explored.add(istart);
while (trajects.size)
{
for (let traj of trajects)
{
let ilast = traj[traj.length-1];
for (let inew of this.regionLinks[ilast])
{
if (inew === iend)
return traj.concat(iend);
if (explored.has(inew))
continue;
trajects.add(traj.concat(inew));
explored.add(inew);
}
trajects.delete(traj);
}
}
return undefined;
};
m.Accessibility.prototype.getRegionSize = function(position, onWater)
{
let pos = this.gamePosToMapPos(position);
let index = pos[0] + pos[1]*this.width;
let ID = onWater === true ? this.navalPassMap[index] : this.landPassMap[index];
if (this.regionSize[ID] === undefined)
return 0;
return this.regionSize[ID];
};
m.Accessibility.prototype.getRegionSizei = function(index, onWater)
{
if (this.regionSize[this.landPassMap[index]] === undefined && (!onWater || this.regionSize[this.navalPassMap[index]] === undefined))
return 0;
if (onWater && this.regionSize[this.navalPassMap[index]] > this.regionSize[this.landPassMap[index]])
return this.regionSize[this.navalPassMap[index]];
return this.regionSize[this.landPassMap[index]];
};
/** Implementation of a fast flood fill. Reasonably good performances for JS. */
m.Accessibility.prototype.floodFill = function(startIndex, value, onWater)
{
if (value > this.maxRegions)
{
error("AI accessibility map: too many regions.");
this.landPassMap[startIndex] = 1;
this.navalPassMap[startIndex] = 1;
return false;
}
if (!onWater && this.landPassMap[startIndex] != 0 || onWater && this.navalPassMap[startIndex] != 0)
return false; // already painted.
let floodFor = "land";
if (this.map[startIndex] === 0)
{
this.landPassMap[startIndex] = 1;
this.navalPassMap[startIndex] = 1;
return false;
}
if (onWater === true)
{
if (this.map[startIndex] !== 200 && this.map[startIndex] !== 201)
{
this.navalPassMap[startIndex] = 1; // impassable for naval
return false; // do nothing
}
floodFor = "water";
}
else if (this.map[startIndex] === 200)
{
this.landPassMap[startIndex] = 1; // impassable for land
return false;
}
// here we'll be able to start.
for (let i = this.regionSize.length; i <= value; ++i)
{
this.regionLinks.push([]);
this.regionSize.push(0);
this.regionType.push("inaccessible");
}
let w = this.width;
let h = this.height;
let y = 0;
// Get x and y from index
let IndexArray = [startIndex];
let newIndex;
while(IndexArray.length)
{
newIndex = IndexArray.pop();
y = 0;
let loop = false;
// vertical iteration
do {
--y;
loop = false;
let index = newIndex + w*y;
if (index < 0)
break;
if (floodFor === "land" && this.landPassMap[index] === 0 && this.map[index] !== 0 && this.map[index] !== 200)
loop = true;
else if (floodFor === "water" && this.navalPassMap[index] === 0 && (this.map[index] === 200 || this.map[index] === 201))
loop = true;
else
break;
} while (loop === true); // should actually break
++y;
let reachLeft = false;
let reachRight = false;
let index;
do {
index = newIndex + w*y;
if (floodFor === "land" && this.landPassMap[index] === 0 && this.map[index] !== 0 && this.map[index] !== 200)
{
this.landPassMap[index] = value;
this.regionSize[value]++;
}
else if (floodFor === "water" && this.navalPassMap[index] === 0 && (this.map[index] === 200 || this.map[index] === 201))
{
this.navalPassMap[index] = value;
this.regionSize[value]++;
}
else
break;
if (index%w > 0)
{
if (floodFor === "land" && this.landPassMap[index -1] === 0 && this.map[index -1] !== 0 && this.map[index -1] !== 200)
{
if (!reachLeft)
{
IndexArray.push(index -1);
reachLeft = true;
}
}
else if (floodFor === "water" && this.navalPassMap[index -1] === 0 && (this.map[index -1] === 200 || this.map[index -1] === 201))
{
if (!reachLeft)
{
IndexArray.push(index -1);
reachLeft = true;
}
}
else if (reachLeft)
reachLeft = false;
}
if (index%w < w - 1)
{
if (floodFor === "land" && this.landPassMap[index +1] === 0 && this.map[index +1] !== 0 && this.map[index +1] !== 200)
{
if (!reachRight)
{
IndexArray.push(index +1);
reachRight = true;
}
}
else if (floodFor === "water" && this.navalPassMap[index +1] === 0 && (this.map[index +1] === 200 || this.map[index +1] === 201))
{
if (!reachRight)
{
IndexArray.push(index +1);
reachRight = true;
}
}
else if (reachRight)
reachRight = false;
}
++y;
} while (index/w < h-1); // should actually break
}
return true;
};
/** creates a map of resource density */
m.SharedScript.prototype.createResourceMaps = function()
{
for (let resource of Resources.GetCodes())
{
if (!(Resources.GetResource(resource).aiAnalysisInfluenceGroup in this.normalizationFactor))
continue;
// if there is no resourceMap create one with an influence for everything with that resource
if (this.resourceMaps[resource])
continue;
// We're creating them 8-bit. Things could go above 255 if there are really tons of resources
// But at that point the precision is not really important anyway. And it saves memory.
this.resourceMaps[resource] = new m.Map(this, "resource");
this.ccResourceMaps[resource] = new m.Map(this, "resource");
}
for (let ent of this._entities.values())
{
- if (!ent || !ent.position() || !ent.resourceSupplyType() || ent.resourceSupplyType().generic === "treasure")
+ if (!ent || !ent.position() || !ent.resourceSupplyType())
continue;
let resource = ent.resourceSupplyType().generic;
if (!this.resourceMaps[resource])
continue;
let cellSize = this.resourceMaps[resource].cellSize;
let x = Math.floor(ent.position()[0] / cellSize);
let z = Math.floor(ent.position()[1] / cellSize);
let grp = Resources.GetResource(resource).aiAnalysisInfluenceGroup;
let strength = Math.floor(ent.resourceSupplyMax() / this.normalizationFactor[grp]);
this.resourceMaps[resource].addInfluence(x, z, this.influenceRadius[grp] / cellSize, strength/2, "constant");
this.resourceMaps[resource].addInfluence(x, z, this.influenceRadius[grp] / cellSize, strength/2);
this.ccResourceMaps[resource].addInfluence(x, z, this.ccInfluenceRadius[grp] / cellSize, strength, "constant");
}
};
/**
* creates and maintains a map of unused resource density
* this also takes dropsites into account.
* resources that are "part" of a dropsite are not counted.
*/
m.SharedScript.prototype.updateResourceMaps = function(events)
{
for (let resource of Resources.GetCodes())
{
if (!(Resources.GetResource(resource).aiAnalysisInfluenceGroup in this.normalizationFactor))
continue;
// if there is no resourceMap create one with an influence for everything with that resource
if (this.resourceMaps[resource])
continue;
// We're creating them 8-bit. Things could go above 255 if there are really tons of resources
// But at that point the precision is not really important anyway. And it saves memory.
this.resourceMaps[resource] = new m.Map(this, "resource");
this.ccResourceMaps[resource] = new m.Map(this, "resource");
}
// Look for destroy (or create) events and subtract (or add) the entities original influence from the resourceMap
for (let e of events.Destroy)
{
if (!e.entityObj)
continue;
let ent = e.entityObj;
- if (!ent || !ent.position() || !ent.resourceSupplyType() || ent.resourceSupplyType().generic === "treasure")
+ if (!ent || !ent.position() || !ent.resourceSupplyType())
continue;
let resource = ent.resourceSupplyType().generic;
if (!this.resourceMaps[resource])
continue;
let cellSize = this.resourceMaps[resource].cellSize;
let x = Math.floor(ent.position()[0] / cellSize);
let z = Math.floor(ent.position()[1] / cellSize);
let grp = Resources.GetResource(resource).aiAnalysisInfluenceGroup;
let strength = -Math.floor(ent.resourceSupplyMax() / this.normalizationFactor[grp]);
this.resourceMaps[resource].addInfluence(x, z, this.influenceRadius[grp] / cellSize, strength/2, "constant");
this.resourceMaps[resource].addInfluence(x, z, this.influenceRadius[grp] / cellSize, strength/2);
this.ccResourceMaps[resource].addInfluence(x, z, this.ccInfluenceRadius[grp] / cellSize, strength, "constant");
}
for (let e of events.Create)
{
if (!e.entity || !this._entities.has(e.entity))
continue;
let ent = this._entities.get(e.entity);
- if (!ent || !ent.position() || !ent.resourceSupplyType() || ent.resourceSupplyType().generic === "treasure")
+ if (!ent || !ent.position() || !ent.resourceSupplyType())
continue;
let resource = ent.resourceSupplyType().generic;
if (!this.resourceMaps[resource])
continue;
let cellSize = this.resourceMaps[resource].cellSize;
let x = Math.floor(ent.position()[0] / cellSize);
let z = Math.floor(ent.position()[1] / cellSize);
let grp = Resources.GetResource(resource).aiAnalysisInfluenceGroup;
let strength = Math.floor(ent.resourceSupplyMax() / this.normalizationFactor[grp]);
this.resourceMaps[resource].addInfluence(x, z, this.influenceRadius[grp] / cellSize, strength/2, "constant");
this.resourceMaps[resource].addInfluence(x, z, this.influenceRadius[grp] / cellSize, strength/2);
this.ccResourceMaps[resource].addInfluence(x, z, this.ccInfluenceRadius[grp] / cellSize, strength, "constant");
}
};
return m;
}(API3);
Index: ps/trunk/binaries/data/mods/public/simulation/ai/petra/baseManager.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/ai/petra/baseManager.js (revision 24988)
+++ ps/trunk/binaries/data/mods/public/simulation/ai/petra/baseManager.js (revision 24989)
@@ -1,1093 +1,1091 @@
/**
* Base Manager
* Handles lower level economic stuffs.
* Some tasks:
* -tasking workers: gathering/hunting/building/repairing?/scouting/plans.
* -giving feedback/estimates on GR
* -achieving building stuff plans (scouting/getting ressource/building) or other long-staying plans.
* -getting good spots for dropsites
* -managing dropsite use in the base
* -updating whatever needs updating, keeping track of stuffs (rebuilding needs…)
*/
PETRA.BaseManager = function(gameState, Config)
{
this.Config = Config;
this.ID = gameState.ai.uniqueIDs.bases++;
// anchor building: seen as the main building of the base. Needs to have territorial influence
this.anchor = undefined;
this.anchorId = undefined;
this.accessIndex = undefined;
// Maximum distance (from any dropsite) to look for resources
// 3 areas are used: from 0 to max/4, from max/4 to max/2 and from max/2 to max
this.maxDistResourceSquare = 360*360;
this.constructing = false;
// Defenders to train in this cc when its construction is finished
this.neededDefenders = this.Config.difficulty > 2 ? 3 + 2*(this.Config.difficulty - 3) : 0;
// vector for iterating, to check one use the HQ map.
this.territoryIndices = [];
this.timeNextIdleCheck = 0;
};
PETRA.BaseManager.prototype.init = function(gameState, state)
{
if (state == "unconstructed")
this.constructing = true;
else if (state != "captured")
this.neededDefenders = 0;
this.workerObject = new PETRA.Worker(this);
// entitycollections
this.units = gameState.getOwnUnits().filter(API3.Filters.byMetadata(PlayerID, "base", this.ID));
this.workers = this.units.filter(API3.Filters.byMetadata(PlayerID, "role", "worker"));
this.buildings = gameState.getOwnStructures().filter(API3.Filters.byMetadata(PlayerID, "base", this.ID));
this.mobileDropsites = this.units.filter(API3.Filters.isDropsite());
this.units.registerUpdates();
this.workers.registerUpdates();
this.buildings.registerUpdates();
this.mobileDropsites.registerUpdates();
// array of entity IDs, with each being
this.dropsites = {};
this.dropsiteSupplies = {};
this.gatherers = {};
for (let res of Resources.GetCodes())
{
this.dropsiteSupplies[res] = { "nearby": [], "medium": [], "faraway": [] };
this.gatherers[res] = { "nextCheck": 0, "used": 0, "lost": 0 };
}
};
PETRA.BaseManager.prototype.reset = function(gameState, state)
{
if (state == "unconstructed")
this.constructing = true;
else
this.constructing = false;
if (state != "captured" || this.Config.difficulty < 3)
this.neededDefenders = 0;
else
this.neededDefenders = 3 + 2 * (this.Config.difficulty - 3);
};
PETRA.BaseManager.prototype.assignEntity = function(gameState, ent)
{
ent.setMetadata(PlayerID, "base", this.ID);
this.units.updateEnt(ent);
this.workers.updateEnt(ent);
this.buildings.updateEnt(ent);
if (ent.resourceDropsiteTypes() && !ent.hasClass("Unit"))
this.assignResourceToDropsite(gameState, ent);
};
PETRA.BaseManager.prototype.setAnchor = function(gameState, anchorEntity)
{
if (!anchorEntity.hasClass("CivCentre"))
API3.warn("Error: Petra base " + this.ID + " has been assigned " + ent.templateName() + " as anchor.");
else
{
this.anchor = anchorEntity;
this.anchorId = anchorEntity.id();
this.anchor.setMetadata(PlayerID, "baseAnchor", true);
gameState.ai.HQ.resetBaseCache();
}
anchorEntity.setMetadata(PlayerID, "base", this.ID);
this.buildings.updateEnt(anchorEntity);
this.accessIndex = PETRA.getLandAccess(gameState, anchorEntity);
return true;
};
/* we lost our anchor. Let's reassign our units and buildings */
PETRA.BaseManager.prototype.anchorLost = function(gameState, ent)
{
this.anchor = undefined;
this.anchorId = undefined;
this.neededDefenders = 0;
gameState.ai.HQ.resetBaseCache();
};
/** Set a building of an anchorless base */
PETRA.BaseManager.prototype.setAnchorlessEntity = function(gameState, ent)
{
if (!this.buildings.hasEntities())
{
if (!PETRA.getBuiltEntity(gameState, ent).resourceDropsiteTypes())
API3.warn("Error: Petra base " + this.ID + " has been assigned " + ent.templateName() + " as origin.");
this.accessIndex = PETRA.getLandAccess(gameState, ent);
}
else if (this.accessIndex != PETRA.getLandAccess(gameState, ent))
API3.warn(" Error: Petra base " + this.ID + " with access " + this.accessIndex +
" has been assigned " + ent.templateName() + " with access" + PETRA.getLandAccess(gameState, ent));
ent.setMetadata(PlayerID, "base", this.ID);
this.buildings.updateEnt(ent);
return true;
};
/**
* Assign the resources around the dropsites of this basis in three areas according to distance, and sort them in each area.
* Moving resources (animals) and buildable resources (fields) are treated elsewhere.
*/
PETRA.BaseManager.prototype.assignResourceToDropsite = function(gameState, dropsite)
{
if (this.dropsites[dropsite.id()])
{
if (this.Config.debug > 0)
warn("assignResourceToDropsite: dropsite already in the list. Should never happen");
return;
}
let accessIndex = this.accessIndex;
let dropsitePos = dropsite.position();
let dropsiteId = dropsite.id();
this.dropsites[dropsiteId] = true;
if (this.ID == gameState.ai.HQ.baseManagers[0].ID)
accessIndex = PETRA.getLandAccess(gameState, dropsite);
let maxDistResourceSquare = this.maxDistResourceSquare;
for (let type of dropsite.resourceDropsiteTypes())
{
let resources = gameState.getResourceSupplies(type);
if (!resources.length)
continue;
let nearby = this.dropsiteSupplies[type].nearby;
let medium = this.dropsiteSupplies[type].medium;
let faraway = this.dropsiteSupplies[type].faraway;
resources.forEach(function(supply)
{
if (!supply.position())
return;
if (supply.hasClass("Animal")) // moving resources are treated differently
return;
if (supply.hasClass("Field")) // fields are treated separately
return;
- if (supply.resourceSupplyType().generic == "treasure") // treasures are treated separately
- return;
// quick accessibility check
if (PETRA.getLandAccess(gameState, supply) != accessIndex)
return;
let dist = API3.SquareVectorDistance(supply.position(), dropsitePos);
if (dist < maxDistResourceSquare)
{
if (dist < maxDistResourceSquare/16) // distmax/4
nearby.push({ "dropsite": dropsiteId, "id": supply.id(), "ent": supply, "dist": dist });
else if (dist < maxDistResourceSquare/4) // distmax/2
medium.push({ "dropsite": dropsiteId, "id": supply.id(), "ent": supply, "dist": dist });
else
faraway.push({ "dropsite": dropsiteId, "id": supply.id(), "ent": supply, "dist": dist });
}
});
nearby.sort((r1, r2) => r1.dist - r2.dist);
medium.sort((r1, r2) => r1.dist - r2.dist);
faraway.sort((r1, r2) => r1.dist - r2.dist);
/*
let debug = false;
if (debug)
{
faraway.forEach(function(res){
Engine.PostCommand(PlayerID,{"type": "set-shading-color", "entities": [res.ent.id()], "rgb": [2,0,0]});
});
medium.forEach(function(res){
Engine.PostCommand(PlayerID,{"type": "set-shading-color", "entities": [res.ent.id()], "rgb": [0,2,0]});
});
nearby.forEach(function(res){
Engine.PostCommand(PlayerID,{"type": "set-shading-color", "entities": [res.ent.id()], "rgb": [0,0,2]});
});
}
*/
}
// Allows all allies to use this dropsite except if base anchor to be sure to keep
// a minimum of resources for this base
Engine.PostCommand(PlayerID, {
"type": "set-dropsite-sharing",
"entities": [dropsiteId],
"shared": dropsiteId != this.anchorId
});
};
// completely remove the dropsite resources from our list.
PETRA.BaseManager.prototype.removeDropsite = function(gameState, ent)
{
if (!ent.id())
return;
let removeSupply = function(entId, supply){
for (let i = 0; i < supply.length; ++i)
{
// exhausted resource, remove it from this list
if (!supply[i].ent || !gameState.getEntityById(supply[i].id))
supply.splice(i--, 1);
// resource assigned to the removed dropsite, remove it
else if (supply[i].dropsite == entId)
supply.splice(i--, 1);
}
};
for (let type in this.dropsiteSupplies)
{
removeSupply(ent.id(), this.dropsiteSupplies[type].nearby);
removeSupply(ent.id(), this.dropsiteSupplies[type].medium);
removeSupply(ent.id(), this.dropsiteSupplies[type].faraway);
}
this.dropsites[ent.id()] = undefined;
};
/**
* Returns the position of the best place to build a new dropsite for the specified resource
*/
PETRA.BaseManager.prototype.findBestDropsiteLocation = function(gameState, resource)
{
let template = gameState.getTemplate(gameState.applyCiv("structures/{civ}/storehouse"));
let halfSize = 0;
if (template.get("Footprint/Square"))
halfSize = Math.max(+template.get("Footprint/Square/@depth"), +template.get("Footprint/Square/@width")) / 2;
else if (template.get("Footprint/Circle"))
halfSize = +template.get("Footprint/Circle/@radius");
// This builds a map. The procedure is fairly simple. It adds the resource maps
// (which are dynamically updated and are made so that they will facilitate DP placement)
// Then checks for a good spot in the territory. If none, and town/city phase, checks outside
// The AI will currently not build a CC if it wouldn't connect with an existing CC.
let obstructions = PETRA.createObstructionMap(gameState, this.accessIndex, template);
let ccEnts = gameState.getOwnStructures().filter(API3.Filters.byClass("CivCentre")).toEntityArray();
let dpEnts = gameState.getOwnStructures().filter(API3.Filters.byClassesOr(["Storehouse", "Dock"])).toEntityArray();
let bestIdx;
let bestVal = 0;
let radius = Math.ceil(template.obstructionRadius().max / obstructions.cellSize);
let territoryMap = gameState.ai.HQ.territoryMap;
let width = territoryMap.width;
let cellSize = territoryMap.cellSize;
for (let j of this.territoryIndices)
{
let i = territoryMap.getNonObstructedTile(j, radius, obstructions);
if (i < 0) // no room around
continue;
// we add 3 times the needed resource and once the others (except food)
let total = 2*gameState.sharedScript.resourceMaps[resource].map[j];
for (let res in gameState.sharedScript.resourceMaps)
if (res != "food")
total += gameState.sharedScript.resourceMaps[res].map[j];
total *= 0.7; // Just a normalisation factor as the locateMap is limited to 255
if (total <= bestVal)
continue;
let pos = [cellSize * (j%width+0.5), cellSize * (Math.floor(j/width)+0.5)];
for (let dp of dpEnts)
{
let dpPos = dp.position();
if (!dpPos)
continue;
let dist = API3.SquareVectorDistance(dpPos, pos);
if (dist < 3600)
{
total = 0;
break;
}
else if (dist < 6400)
total *= (Math.sqrt(dist)-60)/20;
}
if (total <= bestVal)
continue;
for (let cc of ccEnts)
{
let ccPos = cc.position();
if (!ccPos)
continue;
let dist = API3.SquareVectorDistance(ccPos, pos);
if (dist < 3600)
{
total = 0;
break;
}
else if (dist < 6400)
total *= (Math.sqrt(dist)-60)/20;
}
if (total <= bestVal)
continue;
if (gameState.ai.HQ.isDangerousLocation(gameState, pos, halfSize))
continue;
bestVal = total;
bestIdx = i;
}
if (this.Config.debug > 2)
warn(" for dropsite best is " + bestVal);
if (bestVal <= 0)
return { "quality": bestVal, "pos": [0, 0] };
let x = (bestIdx % obstructions.width + 0.5) * obstructions.cellSize;
let z = (Math.floor(bestIdx / obstructions.width) + 0.5) * obstructions.cellSize;
return { "quality": bestVal, "pos": [x, z] };
};
PETRA.BaseManager.prototype.getResourceLevel = function(gameState, type, nearbyOnly = false)
{
let count = 0;
let check = {};
for (let supply of this.dropsiteSupplies[type].nearby)
{
if (check[supply.id]) // avoid double counting as same resource can appear several time
continue;
check[supply.id] = true;
count += supply.ent.resourceSupplyAmount();
}
if (nearbyOnly)
return count;
for (let supply of this.dropsiteSupplies[type].medium)
{
if (check[supply.id])
continue;
check[supply.id] = true;
count += 0.6*supply.ent.resourceSupplyAmount();
}
return count;
};
/** check our resource levels and react accordingly */
PETRA.BaseManager.prototype.checkResourceLevels = function(gameState, queues)
{
for (let type of Resources.GetCodes())
{
if (type == "food")
{
if (gameState.ai.HQ.canBuild(gameState, "structures/{civ}/field")) // let's see if we need to add new farms.
{
let count = this.getResourceLevel(gameState, type, gameState.currentPhase() > 1); // animals are not accounted
let numFarms = gameState.getOwnStructures().filter(API3.Filters.byClass("Field")).length; // including foundations
let numQueue = queues.field.countQueuedUnits();
// TODO if not yet farms, add a check on time used/lost and build farmstead if needed
if (numFarms + numQueue == 0) // starting game, rely on fruits as long as we have enough of them
{
if (count < 600)
{
queues.field.addPlan(new PETRA.ConstructionPlan(gameState, "structures/{civ}/field", { "favoredBase": this.ID }));
gameState.ai.HQ.needFarm = true;
}
}
else if (!gameState.ai.HQ.maxFields || numFarms + numQueue < gameState.ai.HQ.maxFields)
{
let numFound = gameState.getOwnFoundations().filter(API3.Filters.byClass("Field")).length;
let goal = this.Config.Economy.provisionFields;
if (gameState.ai.HQ.saveResources || gameState.ai.HQ.saveSpace || count > 300 || numFarms > 5)
goal = Math.max(goal-1, 1);
if (numFound + numQueue < goal)
queues.field.addPlan(new PETRA.ConstructionPlan(gameState, "structures/{civ}/field", { "favoredBase": this.ID }));
}
else if (gameState.ai.HQ.needCorral && !gameState.getOwnEntitiesByClass("Corral", true).hasEntities() &&
!queues.corral.hasQueuedUnits() && gameState.ai.HQ.canBuild(gameState, "structures/{civ}/corral"))
queues.corral.addPlan(new PETRA.ConstructionPlan(gameState, "structures/{civ}/corral", { "favoredBase": this.ID }));
continue;
}
if (!gameState.getOwnEntitiesByClass("Corral", true).hasEntities() &&
!queues.corral.hasQueuedUnits() && gameState.ai.HQ.canBuild(gameState, "structures/{civ}/corral"))
{
let count = this.getResourceLevel(gameState, type, gameState.currentPhase() > 1); // animals are not accounted
if (count < 900)
{
queues.corral.addPlan(new PETRA.ConstructionPlan(gameState, "structures/{civ}/corral", { "favoredBase": this.ID }));
gameState.ai.HQ.needCorral = true;
}
}
continue;
}
// Non food stuff
if (!gameState.sharedScript.resourceMaps[type] || queues.dropsites.hasQueuedUnits() ||
gameState.getOwnFoundations().filter(API3.Filters.byClass("Storehouse")).hasEntities())
{
this.gatherers[type].nextCheck = gameState.ai.playedTurn;
this.gatherers[type].used = 0;
this.gatherers[type].lost = 0;
continue;
}
if (gameState.ai.playedTurn < this.gatherers[type].nextCheck)
continue;
for (let ent of this.gatherersByType(gameState, type).values())
{
if (ent.unitAIState() == "INDIVIDUAL.GATHER.GATHERING")
++this.gatherers[type].used;
else if (ent.unitAIState() == "INDIVIDUAL.RETURNRESOURCE.APPROACHING")
++this.gatherers[type].lost;
}
// TODO add also a test on remaining resources.
let total = this.gatherers[type].used + this.gatherers[type].lost;
if (total > 150 || total > 60 && type != "wood")
{
let ratio = this.gatherers[type].lost / total;
if (ratio > 0.15)
{
let newDP = this.findBestDropsiteLocation(gameState, type);
if (newDP.quality > 50 && gameState.ai.HQ.canBuild(gameState, "structures/{civ}/storehouse"))
queues.dropsites.addPlan(new PETRA.ConstructionPlan(gameState, "structures/{civ}/storehouse", { "base": this.ID, "type": type }, newDP.pos));
else if (!gameState.getOwnFoundations().filter(API3.Filters.byClass("CivCentre")).hasEntities() && !queues.civilCentre.hasQueuedUnits())
{
// No good dropsite, try to build a new base if no base already planned,
// and if not possible, be less strict on dropsite quality.
if ((!gameState.ai.HQ.canExpand || !gameState.ai.HQ.buildNewBase(gameState, queues, type)) &&
newDP.quality > Math.min(25, 50*0.15/ratio) &&
gameState.ai.HQ.canBuild(gameState, "structures/{civ}/storehouse"))
queues.dropsites.addPlan(new PETRA.ConstructionPlan(gameState, "structures/{civ}/storehouse", { "base": this.ID, "type": type }, newDP.pos));
}
}
this.gatherers[type].nextCheck = gameState.ai.playedTurn + 20;
this.gatherers[type].used = 0;
this.gatherers[type].lost = 0;
}
else if (total == 0)
this.gatherers[type].nextCheck = gameState.ai.playedTurn + 10;
}
};
/** Adds the estimated gather rates from this base to the currentRates */
PETRA.BaseManager.prototype.addGatherRates = function(gameState, currentRates)
{
for (let res in currentRates)
{
// I calculate the exact gathering rate for each unit.
// I must then lower that to account for travel time.
// Given that the faster you gather, the more travel time matters,
// I use some logarithms.
// TODO: this should take into account for unit speed and/or distance to target
this.gatherersByType(gameState, res).forEach(ent => {
if (ent.isIdle() || !ent.position())
return;
let gRate = ent.currentGatherRate();
if (gRate)
currentRates[res] += Math.log(1+gRate)/1.1;
});
if (res == "food")
{
this.workersBySubrole(gameState, "hunter").forEach(ent => {
if (ent.isIdle() || !ent.position())
return;
let gRate = ent.currentGatherRate();
if (gRate)
currentRates[res] += Math.log(1+gRate)/1.1;
});
this.workersBySubrole(gameState, "fisher").forEach(ent => {
if (ent.isIdle() || !ent.position())
return;
let gRate = ent.currentGatherRate();
if (gRate)
currentRates[res] += Math.log(1+gRate)/1.1;
});
}
}
};
PETRA.BaseManager.prototype.assignRolelessUnits = function(gameState, roleless)
{
if (!roleless)
roleless = this.units.filter(API3.Filters.not(API3.Filters.byHasMetadata(PlayerID, "role"))).values();
for (let ent of roleless)
{
if (ent.hasClass("Worker") || ent.hasClass("CitizenSoldier") || ent.hasClass("FishingBoat"))
ent.setMetadata(PlayerID, "role", "worker");
}
};
/**
* If the numbers of workers on the resources is unbalanced then set some of workers to idle so
* they can be reassigned by reassignIdleWorkers.
* TODO: actually this probably should be in the HQ.
*/
PETRA.BaseManager.prototype.setWorkersIdleByPriority = function(gameState)
{
this.timeNextIdleCheck = gameState.ai.elapsedTime + 8;
// change resource only towards one which is more needed, and if changing will not change this order
let nb = 1; // no more than 1 change per turn (otherwise we should update the rates)
let mostNeeded = gameState.ai.HQ.pickMostNeededResources(gameState);
let sumWanted = 0;
let sumCurrent = 0;
for (let need of mostNeeded)
{
sumWanted += need.wanted;
sumCurrent += need.current;
}
let scale = 1;
if (sumWanted > 0)
scale = sumCurrent / sumWanted;
for (let i = mostNeeded.length-1; i > 0; --i)
{
let lessNeed = mostNeeded[i];
for (let j = 0; j < i; ++j)
{
let moreNeed = mostNeeded[j];
let lastFailed = gameState.ai.HQ.lastFailedGather[moreNeed.type];
if (lastFailed && gameState.ai.elapsedTime - lastFailed < 20)
continue;
// Ensure that the most wanted resource is not exhausted
if (moreNeed.type != "food" && gameState.ai.HQ.isResourceExhausted(moreNeed.type))
{
if (lessNeed.type != "food" && gameState.ai.HQ.isResourceExhausted(lessNeed.type))
continue;
// And if so, move the gatherer to the less wanted one.
nb = this.switchGatherer(gameState, moreNeed.type, lessNeed.type, nb);
if (nb == 0)
return;
}
// If we assume a mean rate of 0.5 per gatherer, this diff should be > 1
// but we require a bit more to avoid too frequent changes
if (scale*moreNeed.wanted - moreNeed.current - scale*lessNeed.wanted + lessNeed.current > 1.5 ||
lessNeed.type != "food" && gameState.ai.HQ.isResourceExhausted(lessNeed.type))
{
nb = this.switchGatherer(gameState, lessNeed.type, moreNeed.type, nb);
if (nb == 0)
return;
}
}
}
};
/**
* Switch some gatherers (limited to number) from resource "from" to resource "to"
* and return remaining number of possible switches.
* Prefer FemaleCitizen for food and CitizenSoldier for other resources.
*/
PETRA.BaseManager.prototype.switchGatherer = function(gameState, from, to, number)
{
let num = number;
let only;
let gatherers = this.gatherersByType(gameState, from);
if (from == "food" && gatherers.filter(API3.Filters.byClass("CitizenSoldier")).hasEntities())
only = "CitizenSoldier";
else if (to == "food" && gatherers.filter(API3.Filters.byClass("FemaleCitizen")).hasEntities())
only = "FemaleCitizen";
for (let ent of gatherers.values())
{
if (num == 0)
return num;
if (!ent.canGather(to))
continue;
if (only && !ent.hasClass(only))
continue;
--num;
ent.stopMoving();
ent.setMetadata(PlayerID, "gather-type", to);
gameState.ai.HQ.AddTCResGatherer(to);
}
return num;
};
PETRA.BaseManager.prototype.reassignIdleWorkers = function(gameState, idleWorkers)
{
// Search for idle workers, and tell them to gather resources based on demand
if (!idleWorkers)
{
let filter = API3.Filters.byMetadata(PlayerID, "subrole", "idle");
idleWorkers = gameState.updatingCollection("idle-workers-base-" + this.ID, filter, this.workers).values();
}
for (let ent of idleWorkers)
{
// Check that the worker isn't garrisoned
if (!ent.position())
continue;
if (ent.hasClass("Worker"))
{
// Just emergency repairing here. It is better managed in assignToFoundations
if (ent.isBuilder() && this.anchor && this.anchor.needsRepair() &&
gameState.getOwnEntitiesByMetadata("target-foundation", this.anchor.id()).length < 2)
ent.repair(this.anchor);
else if (ent.isGatherer())
{
let mostNeeded = gameState.ai.HQ.pickMostNeededResources(gameState);
for (let needed of mostNeeded)
{
if (!ent.canGather(needed.type))
continue;
let lastFailed = gameState.ai.HQ.lastFailedGather[needed.type];
if (lastFailed && gameState.ai.elapsedTime - lastFailed < 20)
continue;
if (needed.type != "food" && gameState.ai.HQ.isResourceExhausted(needed.type))
continue;
ent.setMetadata(PlayerID, "subrole", "gatherer");
ent.setMetadata(PlayerID, "gather-type", needed.type);
gameState.ai.HQ.AddTCResGatherer(needed.type);
break;
}
}
}
else if (PETRA.isFastMoving(ent) && ent.canGather("food") && ent.canAttackClass("Animal"))
ent.setMetadata(PlayerID, "subrole", "hunter");
else if (ent.hasClass("FishingBoat"))
ent.setMetadata(PlayerID, "subrole", "fisher");
}
};
PETRA.BaseManager.prototype.workersBySubrole = function(gameState, subrole)
{
return gameState.updatingCollection("subrole-" + subrole +"-base-" + this.ID, API3.Filters.byMetadata(PlayerID, "subrole", subrole), this.workers);
};
PETRA.BaseManager.prototype.gatherersByType = function(gameState, type)
{
return gameState.updatingCollection("workers-gathering-" + type +"-base-" + this.ID, API3.Filters.byMetadata(PlayerID, "gather-type", type), this.workersBySubrole(gameState, "gatherer"));
};
/**
* returns an entity collection of workers.
* They are idled immediatly and their subrole set to idle.
*/
PETRA.BaseManager.prototype.pickBuilders = function(gameState, workers, number)
{
let availableWorkers = this.workers.filter(ent => {
if (!ent.position() || !ent.isBuilder())
return false;
if (ent.getMetadata(PlayerID, "plan") == -2 || ent.getMetadata(PlayerID, "plan") == -3)
return false;
if (ent.getMetadata(PlayerID, "transport"))
return false;
return true;
}).toEntityArray();
availableWorkers.sort((a, b) => {
let vala = 0;
let valb = 0;
if (a.getMetadata(PlayerID, "subrole") == "builder")
vala = 100;
if (b.getMetadata(PlayerID, "subrole") == "builder")
valb = 100;
if (a.getMetadata(PlayerID, "subrole") == "idle")
vala = -50;
if (b.getMetadata(PlayerID, "subrole") == "idle")
valb = -50;
if (a.getMetadata(PlayerID, "plan") === undefined)
vala = -20;
if (b.getMetadata(PlayerID, "plan") === undefined)
valb = -20;
return vala - valb;
});
let needed = Math.min(number, availableWorkers.length - 3);
for (let i = 0; i < needed; ++i)
{
availableWorkers[i].stopMoving();
availableWorkers[i].setMetadata(PlayerID, "subrole", "idle");
workers.addEnt(availableWorkers[i]);
}
return;
};
/**
* If we have some foundations, and we don't have enough builder-workers,
* try reassigning some other workers who are nearby
* AI tries to use builders sensibly, not completely stopping its econ.
*/
PETRA.BaseManager.prototype.assignToFoundations = function(gameState, noRepair)
{
let foundations = this.buildings.filter(API3.Filters.and(API3.Filters.isFoundation(), API3.Filters.not(API3.Filters.byClass("Field"))));
let damagedBuildings = this.buildings.filter(ent => ent.foundationProgress() === undefined && ent.needsRepair());
// Check if nothing to build
if (!foundations.length && !damagedBuildings.length)
return;
let workers = this.workers.filter(ent => ent.isBuilder());
let builderWorkers = this.workersBySubrole(gameState, "builder");
let idleBuilderWorkers = builderWorkers.filter(API3.Filters.isIdle());
// if we're constructing and we have the foundations to our base anchor, only try building that.
if (this.constructing && foundations.filter(API3.Filters.byMetadata(PlayerID, "baseAnchor", true)).hasEntities())
{
foundations = foundations.filter(API3.Filters.byMetadata(PlayerID, "baseAnchor", true));
let tID = foundations.toEntityArray()[0].id();
workers.forEach(ent => {
let target = ent.getMetadata(PlayerID, "target-foundation");
if (target && target != tID)
{
ent.stopMoving();
ent.setMetadata(PlayerID, "target-foundation", tID);
}
});
}
if (workers.length < 3)
{
let fromOtherBase = gameState.ai.HQ.bulkPickWorkers(gameState, this, 2);
if (fromOtherBase)
{
let baseID = this.ID;
fromOtherBase.forEach(worker => {
worker.setMetadata(PlayerID, "base", baseID);
worker.setMetadata(PlayerID, "subrole", "builder");
workers.updateEnt(worker);
builderWorkers.updateEnt(worker);
idleBuilderWorkers.updateEnt(worker);
});
}
}
let builderTot = builderWorkers.length - idleBuilderWorkers.length;
// Make the limit on number of builders depends on the available resources
let availableResources = gameState.ai.queueManager.getAvailableResources(gameState);
let builderRatio = 1;
for (let res of Resources.GetCodes())
{
if (availableResources[res] < 200)
{
builderRatio = 0.2;
break;
}
else if (availableResources[res] < 1000)
builderRatio = Math.min(builderRatio, availableResources[res] / 1000);
}
for (let target of foundations.values())
{
if (target.hasClass("Field"))
continue; // we do not build fields
if (gameState.ai.HQ.isNearInvadingArmy(target.position()))
if (!target.hasClass("CivCentre") && !target.hasClass("Wall") &&
(!target.hasClass("Wonder") || !gameState.getVictoryConditions().has("wonder")))
continue;
// if our territory has shrinked since this foundation was positioned, do not build it
if (PETRA.isNotWorthBuilding(gameState, target))
continue;
let assigned = gameState.getOwnEntitiesByMetadata("target-foundation", target.id()).length;
let maxTotalBuilders = Math.ceil(workers.length * builderRatio);
if (maxTotalBuilders < 2 && workers.length > 1)
maxTotalBuilders = 2;
if (target.hasClass("House") && gameState.getPopulationLimit() < gameState.getPopulation() + 5 &&
gameState.getPopulationLimit() < gameState.getPopulationMax())
maxTotalBuilders += 2;
let targetNB = 2;
if (target.hasClass("Fortress") || target.hasClass("Wonder") ||
target.getMetadata(PlayerID, "phaseUp") == true)
targetNB = 7;
else if (target.hasClass("Barracks") || target.hasClass("Range") || target.hasClass("Stable") ||
target.hasClass("Tower") || target.hasClass("Market"))
targetNB = 4;
else if (target.hasClass("House") || target.hasClass("DropsiteWood"))
targetNB = 3;
if (target.getMetadata(PlayerID, "baseAnchor") == true ||
target.hasClass("Wonder") && gameState.getVictoryConditions().has("wonder"))
{
targetNB = 15;
maxTotalBuilders = Math.max(maxTotalBuilders, 15);
}
// if no base yet, everybody should build
if (gameState.ai.HQ.numActiveBases() == 0)
{
targetNB = workers.length;
maxTotalBuilders = targetNB;
}
if (assigned >= targetNB)
continue;
idleBuilderWorkers.forEach(function(ent) {
if (ent.getMetadata(PlayerID, "target-foundation") !== undefined)
return;
if (assigned >= targetNB || !ent.position() ||
API3.SquareVectorDistance(ent.position(), target.position()) > 40000)
return;
++assigned;
++builderTot;
ent.setMetadata(PlayerID, "target-foundation", target.id());
});
if (assigned >= targetNB || builderTot >= maxTotalBuilders)
continue;
let nonBuilderWorkers = workers.filter(function(ent) {
if (ent.getMetadata(PlayerID, "subrole") == "builder")
return false;
if (!ent.position())
return false;
if (ent.getMetadata(PlayerID, "plan") == -2 || ent.getMetadata(PlayerID, "plan") == -3)
return false;
if (ent.getMetadata(PlayerID, "transport"))
return false;
return true;
}).toEntityArray();
let time = target.buildTime();
nonBuilderWorkers.sort((workerA, workerB) => {
let coeffA = API3.SquareVectorDistance(target.position(), workerA.position());
if (workerA.getMetadata(PlayerID, "gather-type") == "food")
coeffA *= 3;
let coeffB = API3.SquareVectorDistance(target.position(), workerB.position());
if (workerB.getMetadata(PlayerID, "gather-type") == "food")
coeffB *= 3;
return coeffA - coeffB;
});
let current = 0;
let nonBuilderTot = nonBuilderWorkers.length;
while (assigned < targetNB && builderTot < maxTotalBuilders && current < nonBuilderTot)
{
++assigned;
++builderTot;
let ent = nonBuilderWorkers[current++];
ent.stopMoving();
ent.setMetadata(PlayerID, "subrole", "builder");
ent.setMetadata(PlayerID, "target-foundation", target.id());
}
}
for (let target of damagedBuildings.values())
{
// Don't repair if we're still under attack, unless it's a vital (civcentre or wall) building
// that's being destroyed.
if (gameState.ai.HQ.isNearInvadingArmy(target.position()))
{
if (target.healthLevel() > 0.5 ||
!target.hasClass("CivCentre") && !target.hasClass("Wall") &&
(!target.hasClass("Wonder") || !gameState.getVictoryConditions().has("wonder")))
continue;
}
else if (noRepair && !target.hasClass("CivCentre"))
continue;
if (target.decaying())
continue;
let assigned = gameState.getOwnEntitiesByMetadata("target-foundation", target.id()).length;
let maxTotalBuilders = Math.ceil(workers.length * builderRatio);
let targetNB = 1;
if (target.hasClass("Fortress") || target.hasClass("Wonder"))
targetNB = 3;
if (target.getMetadata(PlayerID, "baseAnchor") == true ||
target.hasClass("Wonder") && gameState.getVictoryConditions().has("wonder"))
{
maxTotalBuilders = Math.ceil(workers.length * Math.max(0.3, builderRatio));
targetNB = 5;
if (target.healthLevel() < 0.3)
{
maxTotalBuilders = Math.ceil(workers.length * Math.max(0.6, builderRatio));
targetNB = 7;
}
}
if (assigned >= targetNB)
continue;
idleBuilderWorkers.forEach(function(ent) {
if (ent.getMetadata(PlayerID, "target-foundation") !== undefined)
return;
if (assigned >= targetNB || !ent.position() ||
API3.SquareVectorDistance(ent.position(), target.position()) > 40000)
return;
++assigned;
++builderTot;
ent.setMetadata(PlayerID, "target-foundation", target.id());
});
if (assigned >= targetNB || builderTot >= maxTotalBuilders)
continue;
let nonBuilderWorkers = workers.filter(function(ent) {
if (ent.getMetadata(PlayerID, "subrole") == "builder")
return false;
if (!ent.position())
return false;
if (ent.getMetadata(PlayerID, "plan") == -2 || ent.getMetadata(PlayerID, "plan") == -3)
return false;
if (ent.getMetadata(PlayerID, "transport"))
return false;
return true;
});
let num = Math.min(nonBuilderWorkers.length, targetNB-assigned);
let nearestNonBuilders = nonBuilderWorkers.filterNearest(target.position(), num);
nearestNonBuilders.forEach(function(ent) {
++assigned;
++builderTot;
ent.stopMoving();
ent.setMetadata(PlayerID, "subrole", "builder");
ent.setMetadata(PlayerID, "target-foundation", target.id());
});
}
};
/** Return false when the base is not active (no workers on it) */
PETRA.BaseManager.prototype.update = function(gameState, queues, events)
{
if (this.ID == gameState.ai.HQ.baseManagers[0].ID) // base for unaffected units
{
// if some active base, reassigns the workers/buildings
// otherwise look for anything useful to do, i.e. treasures to gather
if (gameState.ai.HQ.numActiveBases() > 0)
{
for (let ent of this.units.values())
{
let bestBase = PETRA.getBestBase(gameState, ent);
if (bestBase.ID != this.ID)
bestBase.assignEntity(gameState, ent);
}
for (let ent of this.buildings.values())
{
let bestBase = PETRA.getBestBase(gameState, ent);
if (!bestBase)
{
if (ent.hasClass("Dock"))
API3.warn("Petra: dock in baseManager[0]. It may be useful to do an anchorless base for " + ent.templateName());
continue;
}
if (ent.resourceDropsiteTypes())
this.removeDropsite(gameState, ent);
bestBase.assignEntity(gameState, ent);
}
}
else if (gameState.ai.HQ.canBuildUnits)
{
this.assignToFoundations(gameState);
if (gameState.ai.elapsedTime > this.timeNextIdleCheck)
this.setWorkersIdleByPriority(gameState);
this.assignRolelessUnits(gameState);
this.reassignIdleWorkers(gameState);
for (let ent of this.workers.values())
this.workerObject.update(gameState, ent);
for (let ent of this.mobileDropsites.values())
this.workerObject.moveToGatherer(gameState, ent, false);
}
return false;
}
if (!this.anchor) // This anchor has been destroyed, but the base may still be usable
{
if (!this.buildings.hasEntities())
{
// Reassign all remaining entities to its nearest base
for (let ent of this.units.values())
{
let base = PETRA.getBestBase(gameState, ent, false, this.ID);
base.assignEntity(gameState, ent);
}
return false;
}
// If we have a base with anchor on the same land, reassign everything to it
let reassignedBase;
for (let ent of this.buildings.values())
{
if (!ent.position())
continue;
let base = PETRA.getBestBase(gameState, ent);
if (base.anchor)
reassignedBase = base;
break;
}
if (reassignedBase)
{
for (let ent of this.units.values())
reassignedBase.assignEntity(gameState, ent);
for (let ent of this.buildings.values())
{
if (ent.resourceDropsiteTypes())
this.removeDropsite(gameState, ent);
reassignedBase.assignEntity(gameState, ent);
}
return false;
}
this.assignToFoundations(gameState);
if (gameState.ai.elapsedTime > this.timeNextIdleCheck)
this.setWorkersIdleByPriority(gameState);
this.assignRolelessUnits(gameState);
this.reassignIdleWorkers(gameState);
for (let ent of this.workers.values())
this.workerObject.update(gameState, ent);
for (let ent of this.mobileDropsites.values())
this.workerObject.moveToGatherer(gameState, ent, false);
return true;
}
Engine.ProfileStart("Base update - base " + this.ID);
this.checkResourceLevels(gameState, queues);
this.assignToFoundations(gameState);
if (this.constructing)
{
let owner = gameState.ai.HQ.territoryMap.getOwner(this.anchor.position());
if(owner != 0 && !gameState.isPlayerAlly(owner))
{
// we're in enemy territory. If we're too close from the enemy, destroy us.
let ccEnts = gameState.updatingGlobalCollection("allCCs", API3.Filters.byClass("CivCentre"));
for (let cc of ccEnts.values())
{
if (cc.owner() != owner)
continue;
if (API3.SquareVectorDistance(cc.position(), this.anchor.position()) > 8000)
continue;
this.anchor.destroy();
gameState.ai.HQ.resetBaseCache();
break;
}
}
}
else if (this.neededDefenders && gameState.ai.HQ.trainEmergencyUnits(gameState, [this.anchor.position()]))
--this.neededDefenders;
if (gameState.ai.elapsedTime > this.timeNextIdleCheck &&
(gameState.currentPhase() > 1 || gameState.ai.HQ.phasing == 2))
this.setWorkersIdleByPriority(gameState);
this.assignRolelessUnits(gameState);
this.reassignIdleWorkers(gameState);
// check if workers can find something useful to do
for (let ent of this.workers.values())
this.workerObject.update(gameState, ent);
for (let ent of this.mobileDropsites.values())
this.workerObject.moveToGatherer(gameState, ent, false);
Engine.ProfileStop();
return true;
};
PETRA.BaseManager.prototype.Serialize = function()
{
return {
"ID": this.ID,
"anchorId": this.anchorId,
"accessIndex": this.accessIndex,
"maxDistResourceSquare": this.maxDistResourceSquare,
"constructing": this.constructing,
"gatherers": this.gatherers,
"neededDefenders": this.neededDefenders,
"territoryIndices": this.territoryIndices,
"timeNextIdleCheck": this.timeNextIdleCheck
};
};
PETRA.BaseManager.prototype.Deserialize = function(gameState, data)
{
for (let key in data)
this[key] = data[key];
this.anchor = this.anchorId !== undefined ? gameState.getEntityById(this.anchorId) : undefined;
};
Index: ps/trunk/binaries/data/mods/public/simulation/ai/petra/entityExtend.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/ai/petra/entityExtend.js (revision 24988)
+++ ps/trunk/binaries/data/mods/public/simulation/ai/petra/entityExtend.js (revision 24989)
@@ -1,440 +1,436 @@
/** returns true if this unit should be considered as a siege unit */
PETRA.isSiegeUnit = function(ent)
{
return ent.hasClass("Siege") || ent.hasClass("Elephant") && ent.hasClass("Melee");
};
/** returns true if this unit should be considered as "fast". */
PETRA.isFastMoving = function(ent)
{
// TODO: use clever logic based on walkspeed comparisons.
return ent.hasClass("FastMoving");
};
/** returns some sort of DPS * health factor. If you specify a class, it'll use the modifiers against that class too. */
PETRA.getMaxStrength = function(ent, debugLevel, DamageTypeImportance, againstClass)
{
let strength = 0;
let attackTypes = ent.attackTypes();
let damageTypes = Object.keys(DamageTypeImportance);
if (!attackTypes)
return strength;
for (let type of attackTypes)
{
if (type == "Slaughter")
continue;
let attackStrength = ent.attackStrengths(type);
for (let str in attackStrength)
{
let val = parseFloat(attackStrength[str]);
if (againstClass)
val *= ent.getMultiplierAgainst(type, againstClass);
if (DamageTypeImportance[str])
strength += DamageTypeImportance[str] * val / damageTypes.length;
else if (debugLevel > 0)
API3.warn("Petra: " + str + " unknown attackStrength in getMaxStrength (please add " + str + " to config.js).");
}
let attackRange = ent.attackRange(type);
if (attackRange)
strength += attackRange.max * 0.0125;
let attackTimes = ent.attackTimes(type);
for (let str in attackTimes)
{
let val = parseFloat(attackTimes[str]);
switch (str)
{
case "repeat":
strength += val / 100000;
break;
case "prepare":
strength -= val / 100000;
break;
default:
API3.warn("Petra: " + str + " unknown attackTimes in getMaxStrength");
}
}
}
let resistanceStrength = ent.resistanceStrengths();
if (resistanceStrength.Damage)
for (let str in resistanceStrength.Damage)
{
let val = +resistanceStrength.Damage[str];
if (DamageTypeImportance[str])
strength += DamageTypeImportance[str] * val / damageTypes.length;
else if (debugLevel > 0)
API3.warn("Petra: " + str + " unknown resistanceStrength in getMaxStrength (please add " + str + " to config.js).");
}
// ToDo: Add support for StatusEffects and Capture.
return strength * ent.maxHitpoints() / 100.0;
};
/** Get access and cache it (except for units as it can change) in metadata if not already done */
PETRA.getLandAccess = function(gameState, ent)
{
if (ent.hasClass("Unit"))
{
let pos = ent.position();
if (!pos)
{
let holder = PETRA.getHolder(gameState, ent);
if (holder)
return PETRA.getLandAccess(gameState, holder);
API3.warn("Petra error: entity without position, but not garrisoned");
PETRA.dumpEntity(ent);
return undefined;
}
return gameState.ai.accessibility.getAccessValue(pos);
}
let access = ent.getMetadata(PlayerID, "access");
if (!access)
{
access = gameState.ai.accessibility.getAccessValue(ent.position());
// Docks are sometimes not as expected
if (access < 2 && ent.buildPlacementType() == "shore")
{
let halfDepth = 0;
if (ent.get("Footprint/Square"))
halfDepth = +ent.get("Footprint/Square/@depth") / 2;
else if (ent.get("Footprint/Circle"))
halfDepth = +ent.get("Footprint/Circle/@radius");
let entPos = ent.position();
let cosa = Math.cos(ent.angle());
let sina = Math.sin(ent.angle());
for (let d = 3; d < halfDepth; d += 3)
{
let pos = [ entPos[0] - d * sina,
entPos[1] - d * cosa];
access = gameState.ai.accessibility.getAccessValue(pos);
if (access > 1)
break;
}
}
ent.setMetadata(PlayerID, "access", access);
}
return access;
};
/** Sea access always cached as it never changes */
PETRA.getSeaAccess = function(gameState, ent)
{
let sea = ent.getMetadata(PlayerID, "sea");
if (!sea)
{
sea = gameState.ai.accessibility.getAccessValue(ent.position(), true);
// Docks are sometimes not as expected
if (sea < 2 && ent.buildPlacementType() == "shore")
{
let entPos = ent.position();
let cosa = Math.cos(ent.angle());
let sina = Math.sin(ent.angle());
for (let d = 3; d < 15; d += 3)
{
let pos = [ entPos[0] + d * sina,
entPos[1] + d * cosa];
sea = gameState.ai.accessibility.getAccessValue(pos, true);
if (sea > 1)
break;
}
}
ent.setMetadata(PlayerID, "sea", sea);
}
return sea;
};
PETRA.setSeaAccess = function(gameState, ent)
{
PETRA.getSeaAccess(gameState, ent);
};
/** Decide if we should try to capture (returns true) or destroy (return false) */
PETRA.allowCapture = function(gameState, ent, target)
{
if (!target.isCapturable() || !ent.canCapture(target))
return false;
if (target.isInvulnerable())
return true;
// always try to recapture capture points from an allied, except if it's decaying
if (gameState.isPlayerAlly(target.owner()))
return !target.decaying();
let antiCapture = target.defaultRegenRate();
if (target.isGarrisonHolder() && target.garrisoned())
antiCapture += target.garrisonRegenRate() * target.garrisoned().length;
if (target.decaying())
antiCapture -= target.territoryDecayRate();
let capture;
let capturableTargets = gameState.ai.HQ.capturableTargets;
if (!capturableTargets.has(target.id()))
{
capture = ent.captureStrength() * PETRA.getAttackBonus(ent, target, "Capture");
capturableTargets.set(target.id(), { "strength": capture, "ents": new Set([ent.id()]) });
}
else
{
let capturable = capturableTargets.get(target.id());
if (!capturable.ents.has(ent.id()))
{
capturable.strength += ent.captureStrength() * PETRA.getAttackBonus(ent, target, "Capture");
capturable.ents.add(ent.id());
}
capture = capturable.strength;
}
capture *= 1 / (0.1 + 0.9*target.healthLevel());
let sumCapturePoints = target.capturePoints().reduce((a, b) => a + b);
if (target.hasDefensiveFire() && target.isGarrisonHolder() && target.garrisoned())
return capture > antiCapture + sumCapturePoints/50;
return capture > antiCapture + sumCapturePoints/80;
};
PETRA.getAttackBonus = function(ent, target, type)
{
let attackBonus = 1;
if (!ent.get("Attack/" + type) || !ent.get("Attack/" + type + "/Bonuses"))
return attackBonus;
let bonuses = ent.get("Attack/" + type + "/Bonuses");
for (let key in bonuses)
{
let bonus = bonuses[key];
if (bonus.Civ && bonus.Civ !== target.civ())
continue;
if (bonus.Classes && bonus.Classes.split(/\s+/).some(cls => !target.hasClass(cls)))
continue;
attackBonus *= bonus.Multiplier;
}
return attackBonus;
};
/** Makes the worker deposit the currently carried resources at the closest accessible dropsite */
PETRA.returnResources = function(gameState, ent)
{
if (!ent.resourceCarrying() || !ent.resourceCarrying().length || !ent.position())
return false;
let resource = ent.resourceCarrying()[0].type;
let closestDropsite;
let distmin = Math.min();
let access = PETRA.getLandAccess(gameState, ent);
let dropsiteCollection = gameState.playerData.hasSharedDropsites ?
gameState.getAnyDropsites(resource) : gameState.getOwnDropsites(resource);
for (let dropsite of dropsiteCollection.values())
{
if (!dropsite.position())
continue;
let owner = dropsite.owner();
// owner !== PlayerID can only happen when hasSharedDropsites === true, so no need to test it again
if (owner !== PlayerID && (!dropsite.isSharedDropsite() || !gameState.isPlayerMutualAlly(owner)))
continue;
if (PETRA.getLandAccess(gameState, dropsite) != access)
continue;
let dist = API3.SquareVectorDistance(ent.position(), dropsite.position());
if (dist > distmin)
continue;
distmin = dist;
closestDropsite = dropsite;
}
if (!closestDropsite)
return false;
ent.returnResources(closestDropsite);
return true;
};
/** is supply full taking into account gatherers affected during this turn */
PETRA.IsSupplyFull = function(gameState, ent)
{
return ent.isFull() === true ||
ent.resourceSupplyNumGatherers() + gameState.ai.HQ.GetTCGatherer(ent.id()) >= ent.maxGatherers();
};
/**
* Get the best base (in terms of distance and accessIndex) for an entity.
* It should be on the same accessIndex for structures.
* If nothing found, return the base[0] for units and undefined for structures.
* If exclude is given, we exclude the base with ID = exclude.
*/
PETRA.getBestBase = function(gameState, ent, onlyConstructedBase = false, exclude = false)
{
let pos = ent.position();
let accessIndex;
if (!pos)
{
let holder = PETRA.getHolder(gameState, ent);
if (!holder || !holder.position())
{
API3.warn("Petra error: entity without position, but not garrisoned");
PETRA.dumpEntity(ent);
return gameState.ai.HQ.baseManagers[0];
}
pos = holder.position();
accessIndex = PETRA.getLandAccess(gameState, holder);
}
else
accessIndex = PETRA.getLandAccess(gameState, ent);
let distmin = Math.min();
let dist;
let bestbase;
for (let base of gameState.ai.HQ.baseManagers)
{
if (base.ID == gameState.ai.HQ.baseManagers[0].ID || exclude && base.ID == exclude)
continue;
if (onlyConstructedBase && (!base.anchor || base.anchor.foundationProgress() !== undefined))
continue;
if (ent.hasClass("Structure") && base.accessIndex != accessIndex)
continue;
if (base.anchor && base.anchor.position())
dist = API3.SquareVectorDistance(base.anchor.position(), pos);
else
{
let found = false;
for (let structure of base.buildings.values())
{
if (!structure.position())
continue;
dist = API3.SquareVectorDistance(structure.position(), pos);
found = true;
break;
}
if (!found)
continue;
}
if (base.accessIndex != accessIndex)
dist += 50000000;
if (!base.anchor)
dist += 50000000;
if (dist > distmin)
continue;
distmin = dist;
bestbase = base;
}
if (!bestbase && !ent.hasClass("Structure"))
bestbase = gameState.ai.HQ.baseManagers[0];
return bestbase;
};
PETRA.getHolder = function(gameState, ent)
{
for (let holder of gameState.getEntities().values())
{
if (holder.isGarrisonHolder() && holder.garrisoned().indexOf(ent.id()) !== -1)
return holder;
}
return undefined;
};
/** return the template of the built foundation if a foundation, otherwise return the entity itself */
PETRA.getBuiltEntity = function(gameState, ent)
{
if (ent.foundationProgress() !== undefined)
return gameState.getBuiltTemplate(ent.templateName());
return ent;
};
/**
* return true if it is not worth finishing this building (it would surely decay)
* TODO implement the other conditions
*/
PETRA.isNotWorthBuilding = function(gameState, ent)
{
if (gameState.ai.HQ.territoryMap.getOwner(ent.position()) !== PlayerID)
{
let buildTerritories = ent.buildTerritories();
if (buildTerritories && (!buildTerritories.length || buildTerritories.length === 1 && buildTerritories[0] === "own"))
return true;
}
return false;
};
/**
* Check if the straight line between the two positions crosses an enemy territory
*/
PETRA.isLineInsideEnemyTerritory = function(gameState, pos1, pos2, step=70)
{
let n = Math.floor(Math.sqrt(API3.SquareVectorDistance(pos1, pos2))/step) + 1;
let stepx = (pos2[0] - pos1[0]) / n;
let stepy = (pos2[1] - pos1[1]) / n;
for (let i = 1; i < n; ++i)
{
let pos = [pos1[0]+i*stepx, pos1[1]+i*stepy];
let owner = gameState.ai.HQ.territoryMap.getOwner(pos);
if (owner && gameState.isPlayerEnemy(owner))
return true;
}
return false;
};
PETRA.gatherTreasure = function(gameState, ent, water = false)
{
if (!gameState.ai.HQ.treasures.hasEntities())
return false;
if (!ent || !ent.position())
return false;
- let rates = ent.resourceGatherRates();
- if (!rates || !rates.treasure || rates.treasure <= 0)
+ if (!ent.isTreasureCollecter)
return false;
let treasureFound;
let distmin = Math.min();
let access = water ? PETRA.getSeaAccess(gameState, ent) : PETRA.getLandAccess(gameState, ent);
for (let treasure of gameState.ai.HQ.treasures.values())
{
- if (PETRA.IsSupplyFull(gameState, treasure))
- continue;
// let some time for the previous gatherer to reach the treasure before trying again
let lastGathered = treasure.getMetadata(PlayerID, "lastGathered");
if (lastGathered && gameState.ai.elapsedTime - lastGathered < 20)
continue;
if (!water && access != PETRA.getLandAccess(gameState, treasure))
continue;
if (water && access != PETRA.getSeaAccess(gameState, treasure))
continue;
let territoryOwner = gameState.ai.HQ.territoryMap.getOwner(treasure.position());
if (territoryOwner != 0 && !gameState.isPlayerAlly(territoryOwner))
continue;
let dist = API3.SquareVectorDistance(ent.position(), treasure.position());
if (dist > 120000 || territoryOwner != PlayerID && dist > 14000) // AI has no LOS, so restrict it a bit
continue;
if (dist > distmin)
continue;
distmin = dist;
treasureFound = treasure;
}
if (!treasureFound)
return false;
treasureFound.setMetadata(PlayerID, "lastGathered", gameState.ai.elapsedTime);
- ent.gather(treasureFound);
- gameState.ai.HQ.AddTCGatherer(treasureFound.id());
- ent.setMetadata(PlayerID, "supply", treasureFound.id());
+ ent.collectTreasure(treasureFound);
+ ent.setMetadata(PlayerID, "treasure", treasureFound.id());
return true;
};
PETRA.dumpEntity = function(ent)
{
if (!ent)
return;
API3.warn(" >>> id " + ent.id() + " name " + ent.genericName() + " pos " + ent.position() +
" state " + ent.unitAIState());
API3.warn(" base " + ent.getMetadata(PlayerID, "base") + " >>> role " + ent.getMetadata(PlayerID, "role") +
" subrole " + ent.getMetadata(PlayerID, "subrole"));
API3.warn("owner " + ent.owner() + " health " + ent.hitpoints() + " healthMax " + ent.maxHitpoints() +
" foundationProgress " + ent.foundationProgress());
API3.warn(" garrisoning " + ent.getMetadata(PlayerID, "garrisoning") +
" garrisonHolder " + ent.getMetadata(PlayerID, "garrisonHolder") +
" plan " + ent.getMetadata(PlayerID, "plan") + " transport " + ent.getMetadata(PlayerID, "transport"));
API3.warn(" stance " + ent.getStance() + " transporter " + ent.getMetadata(PlayerID, "transporter") +
" gather-type " + ent.getMetadata(PlayerID, "gather-type") +
" target-foundation " + ent.getMetadata(PlayerID, "target-foundation") +
" PartOfArmy " + ent.getMetadata(PlayerID, "PartOfArmy"));
};
Index: ps/trunk/binaries/data/mods/public/simulation/ai/petra/headquarters.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/ai/petra/headquarters.js (revision 24988)
+++ ps/trunk/binaries/data/mods/public/simulation/ai/petra/headquarters.js (revision 24989)
@@ -1,2897 +1,2894 @@
/**
* Headquarters
* Deal with high level logic for the AI. Most of the interesting stuff gets done here.
* Some tasks:
* -defining RESS needs
* -BO decisions.
* > training workers
* > building stuff (though we'll send that to bases)
* -picking strategy (specific manager?)
* -diplomacy -> diplomacyManager
* -planning attacks -> attackManager
* -picking new CC locations.
*/
PETRA.HQ = function(Config)
{
this.Config = Config;
this.phasing = 0; // existing values: 0 means no, i > 0 means phasing towards phase i
// Cache various quantities.
this.turnCache = {};
this.lastFailedGather = {};
this.firstBaseConfig = false;
this.currentBase = 0; // Only one base (from baseManager) is run every turn.
// Workers configuration.
this.targetNumWorkers = this.Config.Economy.targetNumWorkers;
this.supportRatio = this.Config.Economy.supportRatio;
this.fortStartTime = 180; // Sentry towers, will start at fortStartTime + towerLapseTime.
this.towerStartTime = 0; // Stone towers, will start as soon as available (town phase).
this.towerLapseTime = this.Config.Military.towerLapseTime;
this.fortressStartTime = 0; // Fortresses, will start as soon as available (city phase).
this.fortressLapseTime = this.Config.Military.fortressLapseTime;
this.extraTowers = Math.round(Math.min(this.Config.difficulty, 3) * this.Config.personality.defensive);
this.extraFortresses = Math.round(Math.max(Math.min(this.Config.difficulty - 1, 2), 0) * this.Config.personality.defensive);
this.baseManagers = [];
this.attackManager = new PETRA.AttackManager(this.Config);
this.buildManager = new PETRA.BuildManager();
this.defenseManager = new PETRA.DefenseManager(this.Config);
this.tradeManager = new PETRA.TradeManager(this.Config);
this.navalManager = new PETRA.NavalManager(this.Config);
this.researchManager = new PETRA.ResearchManager(this.Config);
this.diplomacyManager = new PETRA.DiplomacyManager(this.Config);
this.garrisonManager = new PETRA.GarrisonManager(this.Config);
this.victoryManager = new PETRA.VictoryManager(this.Config);
this.capturableTargets = new Map();
this.capturableTargetsTime = 0;
};
/** More initialisation for stuff that needs the gameState */
PETRA.HQ.prototype.init = function(gameState, queues)
{
this.territoryMap = PETRA.createTerritoryMap(gameState);
// initialize base map. Each pixel is a base ID, or 0 if not or not accessible
this.basesMap = new API3.Map(gameState.sharedScript, "territory");
// create borderMap: flag cells on the border of the map
// then this map will be completed with our frontier in updateTerritories
this.borderMap = PETRA.createBorderMap(gameState);
// list of allowed regions
this.landRegions = {};
// try to determine if we have a water map
this.navalMap = false;
this.navalRegions = {};
- this.treasures = gameState.getEntities().filter(ent => {
- let type = ent.resourceSupplyType();
- return type && type.generic == "treasure";
- });
+ this.treasures = gameState.getEntities().filter(ent => ent.isTreasure());
this.treasures.registerUpdates();
this.currentPhase = gameState.currentPhase();
this.decayingStructures = new Set();
};
/**
* initialization needed after deserialization (only called when deserialization)
*/
PETRA.HQ.prototype.postinit = function(gameState)
{
// Rebuild the base maps from the territory indices of each base
this.basesMap = new API3.Map(gameState.sharedScript, "territory");
for (let base of this.baseManagers)
for (let j of base.territoryIndices)
this.basesMap.map[j] = base.ID;
for (let ent of gameState.getOwnEntities().values())
{
if (!ent.resourceDropsiteTypes() || !ent.hasClass("Structure"))
continue;
// Entities which have been built or have changed ownership after the last AI turn have no base.
// they will be dealt with in the next checkEvents
let baseID = ent.getMetadata(PlayerID, "base");
if (baseID === undefined)
continue;
let base = this.getBaseByID(baseID);
base.assignResourceToDropsite(gameState, ent);
}
this.updateTerritories(gameState);
};
/**
* Create a new base in the baseManager:
* If an existing one without anchor already exist, use it.
* Otherwise create a new one.
* TODO when buildings, criteria should depend on distance
* allowedType: undefined => new base with an anchor
* "unconstructed" => new base with a foundation anchor
* "captured" => captured base with an anchor
* "anchorless" => anchorless base, currently with dock
*/
PETRA.HQ.prototype.createBase = function(gameState, ent, type)
{
let access = PETRA.getLandAccess(gameState, ent);
let newbase;
for (let base of this.baseManagers)
{
if (base.accessIndex != access)
continue;
if (type != "anchorless" && base.anchor)
continue;
if (type != "anchorless")
{
// TODO we keep the fisrt one, we should rather use the nearest if buildings
// and possibly also cut on distance
newbase = base;
break;
}
else
{
// TODO here also test on distance instead of first
if (newbase && !base.anchor)
continue;
newbase = base;
if (newbase.anchor)
break;
}
}
if (this.Config.debug > 0)
{
API3.warn(" ----------------------------------------------------------");
API3.warn(" HQ createBase entrance avec access " + access + " and type " + type);
API3.warn(" with access " + uneval(this.baseManagers.map(base => base.accessIndex)) +
" and base nbr " + uneval(this.baseManagers.map(base => base.ID)) +
" and anchor " + uneval(this.baseManagers.map(base => !!base.anchor)));
}
if (!newbase)
{
newbase = new PETRA.BaseManager(gameState, this.Config);
newbase.init(gameState, type);
this.baseManagers.push(newbase);
}
else
newbase.reset(type);
if (type != "anchorless")
newbase.setAnchor(gameState, ent);
else
newbase.setAnchorlessEntity(gameState, ent);
return newbase;
};
/**
* returns the sea index linking regions 1 and region 2 (supposed to be different land region)
* otherwise return undefined
* for the moment, only the case land-sea-land is supported
*/
PETRA.HQ.prototype.getSeaBetweenIndices = function(gameState, index1, index2)
{
let path = gameState.ai.accessibility.getTrajectToIndex(index1, index2);
if (path && path.length == 3 && gameState.ai.accessibility.regionType[path[1]] == "water")
return path[1];
if (this.Config.debug > 1)
{
API3.warn("bad path from " + index1 + " to " + index2 + " ??? " + uneval(path));
API3.warn(" regionLinks start " + uneval(gameState.ai.accessibility.regionLinks[index1]));
API3.warn(" regionLinks end " + uneval(gameState.ai.accessibility.regionLinks[index2]));
}
return undefined;
};
/** TODO check if the new anchorless bases should be added to addBase */
PETRA.HQ.prototype.checkEvents = function(gameState, events)
{
let addBase = false;
this.buildManager.checkEvents(gameState, events);
if (events.TerritoriesChanged.length || events.DiplomacyChanged.length)
this.updateTerritories(gameState);
for (let evt of events.DiplomacyChanged)
{
if (evt.player != PlayerID && evt.otherPlayer != PlayerID)
continue;
// Reset the entities collections which depend on diplomacy
gameState.resetOnDiplomacyChanged();
break;
}
for (let evt of events.Destroy)
{
// Let's check we haven't lost an important building here.
if (evt && !evt.SuccessfulFoundation && evt.entityObj && evt.metadata && evt.metadata[PlayerID] &&
evt.metadata[PlayerID].base)
{
let ent = evt.entityObj;
if (ent.owner() != PlayerID)
continue;
// A new base foundation was created and destroyed on the same (AI) turn
if (evt.metadata[PlayerID].base == -1 || evt.metadata[PlayerID].base == -2)
continue;
let base = this.getBaseByID(evt.metadata[PlayerID].base);
if (ent.resourceDropsiteTypes() && ent.hasClass("Structure"))
base.removeDropsite(gameState, ent);
if (evt.metadata[PlayerID].baseAnchor && evt.metadata[PlayerID].baseAnchor === true)
base.anchorLost(gameState, ent);
}
}
for (let evt of events.EntityRenamed)
{
let ent = gameState.getEntityById(evt.newentity);
if (!ent || ent.owner() != PlayerID || ent.getMetadata(PlayerID, "base") === undefined)
continue;
let base = this.getBaseByID(ent.getMetadata(PlayerID, "base"));
if (!base.anchorId || base.anchorId != evt.entity)
continue;
base.anchorId = evt.newentity;
base.anchor = ent;
}
for (let evt of events.Create)
{
// Let's check if we have a valuable foundation needing builders quickly
// (normal foundations are taken care in baseManager.assignToFoundations)
let ent = gameState.getEntityById(evt.entity);
if (!ent || ent.owner() != PlayerID || ent.foundationProgress() === undefined)
continue;
if (ent.getMetadata(PlayerID, "base") == -1) // Standard base around a cc
{
// Okay so let's try to create a new base around this.
let newbase = this.createBase(gameState, ent, "unconstructed");
// Let's get a few units from other bases there to build this.
let builders = this.bulkPickWorkers(gameState, newbase, 10);
if (builders !== false)
{
builders.forEach(worker => {
worker.setMetadata(PlayerID, "base", newbase.ID);
worker.setMetadata(PlayerID, "subrole", "builder");
worker.setMetadata(PlayerID, "target-foundation", ent.id());
});
}
}
else if (ent.getMetadata(PlayerID, "base") == -2) // anchorless base around a dock
{
let newbase = this.createBase(gameState, ent, "anchorless");
// Let's get a few units from other bases there to build this.
let builders = this.bulkPickWorkers(gameState, newbase, 4);
if (builders != false)
{
builders.forEach(worker => {
worker.setMetadata(PlayerID, "base", newbase.ID);
worker.setMetadata(PlayerID, "subrole", "builder");
worker.setMetadata(PlayerID, "target-foundation", ent.id());
});
}
}
}
for (let evt of events.ConstructionFinished)
{
if (evt.newentity == evt.entity) // repaired building
continue;
let ent = gameState.getEntityById(evt.newentity);
if (!ent || ent.owner() != PlayerID)
continue;
if (ent.hasClass("Market") && this.maxFields)
this.maxFields = false;
if (ent.getMetadata(PlayerID, "base") === undefined)
continue;
let base = this.getBaseByID(ent.getMetadata(PlayerID, "base"));
base.buildings.updateEnt(ent);
if (ent.resourceDropsiteTypes())
base.assignResourceToDropsite(gameState, ent);
if (ent.getMetadata(PlayerID, "baseAnchor") === true)
{
if (base.constructing)
base.constructing = false;
addBase = true;
}
}
for (let evt of events.OwnershipChanged) // capture events
{
if (evt.from == PlayerID)
{
let ent = gameState.getEntityById(evt.entity);
if (!ent || ent.getMetadata(PlayerID, "base") === undefined)
continue;
let base = this.getBaseByID(ent.getMetadata(PlayerID, "base"));
if (ent.resourceDropsiteTypes() && ent.hasClass("Structure"))
base.removeDropsite(gameState, ent);
if (ent.getMetadata(PlayerID, "baseAnchor") === true)
base.anchorLost(gameState, ent);
}
if (evt.to != PlayerID)
continue;
let ent = gameState.getEntityById(evt.entity);
if (!ent)
continue;
if (ent.hasClass("Unit"))
{
PETRA.getBestBase(gameState, ent).assignEntity(gameState, ent);
ent.setMetadata(PlayerID, "role", undefined);
ent.setMetadata(PlayerID, "subrole", undefined);
ent.setMetadata(PlayerID, "plan", undefined);
ent.setMetadata(PlayerID, "PartOfArmy", undefined);
if (ent.hasClass("Trader"))
{
ent.setMetadata(PlayerID, "role", "trader");
ent.setMetadata(PlayerID, "route", undefined);
}
if (ent.hasClass("Worker"))
{
ent.setMetadata(PlayerID, "role", "worker");
ent.setMetadata(PlayerID, "subrole", "idle");
}
if (ent.hasClass("Ship"))
PETRA.setSeaAccess(gameState, ent);
if (!ent.hasClass("Support") && !ent.hasClass("Ship") && ent.attackTypes() !== undefined)
ent.setMetadata(PlayerID, "plan", -1);
continue;
}
if (ent.hasClass("CivCentre")) // build a new base around it
{
let newbase;
if (ent.foundationProgress() !== undefined)
newbase = this.createBase(gameState, ent, "unconstructed");
else
{
newbase = this.createBase(gameState, ent, "captured");
addBase = true;
}
newbase.assignEntity(gameState, ent);
}
else
{
let base;
// If dropsite on new island, create a base around it
if (!ent.decaying() && ent.resourceDropsiteTypes())
base = this.createBase(gameState, ent, "anchorless");
else
base = PETRA.getBestBase(gameState, ent) || this.baseManagers[0];
base.assignEntity(gameState, ent);
if (ent.decaying())
{
if (ent.isGarrisonHolder() && this.garrisonManager.addDecayingStructure(gameState, evt.entity, true))
continue;
if (!this.decayingStructures.has(evt.entity))
this.decayingStructures.add(evt.entity);
}
}
}
// deal with the different rally points of training units: the rally point is set when the training starts
// for the time being, only autogarrison is used
for (let evt of events.TrainingStarted)
{
let ent = gameState.getEntityById(evt.entity);
if (!ent || !ent.isOwn(PlayerID))
continue;
if (!ent._entity.trainingQueue || !ent._entity.trainingQueue.length)
continue;
let metadata = ent._entity.trainingQueue[0].metadata;
if (metadata && metadata.garrisonType)
ent.setRallyPoint(ent, "garrison"); // trained units will autogarrison
else
ent.unsetRallyPoint();
}
for (let evt of events.TrainingFinished)
{
for (let entId of evt.entities)
{
let ent = gameState.getEntityById(entId);
if (!ent || !ent.isOwn(PlayerID))
continue;
if (!ent.position())
{
// we are autogarrisoned, check that the holder is registered in the garrisonManager
let holderId = ent.unitAIOrderData()[0].target;
let holder = gameState.getEntityById(holderId);
if (holder)
this.garrisonManager.registerHolder(gameState, holder);
}
else if (ent.getMetadata(PlayerID, "garrisonType"))
{
// we were supposed to be autogarrisoned, but this has failed (may-be full)
ent.setMetadata(PlayerID, "garrisonType", undefined);
}
// Check if this unit is no more needed in its attack plan
// (happen when the training ends after the attack is started or aborted)
let plan = ent.getMetadata(PlayerID, "plan");
if (plan !== undefined && plan >= 0)
{
let attack = this.attackManager.getPlan(plan);
if (!attack || attack.state != "unexecuted")
ent.setMetadata(PlayerID, "plan", -1);
}
// Assign it immediately to something useful to do
if (ent.getMetadata(PlayerID, "role") == "worker")
{
let base;
if (ent.getMetadata(PlayerID, "base") === undefined)
{
base = PETRA.getBestBase(gameState, ent);
base.assignEntity(gameState, ent);
}
else
base = this.getBaseByID(ent.getMetadata(PlayerID, "base"));
base.reassignIdleWorkers(gameState, [ent]);
base.workerObject.update(gameState, ent);
}
else if (ent.resourceSupplyType() && ent.position())
{
let type = ent.resourceSupplyType();
if (!type.generic)
continue;
let dropsites = gameState.getOwnDropsites(type.generic);
let pos = ent.position();
let access = PETRA.getLandAccess(gameState, ent);
let distmin = Math.min();
let goal;
for (let dropsite of dropsites.values())
{
if (!dropsite.position() || PETRA.getLandAccess(gameState, dropsite) != access)
continue;
let dist = API3.SquareVectorDistance(pos, dropsite.position());
if (dist > distmin)
continue;
distmin = dist;
goal = dropsite.position();
}
if (goal)
ent.moveToRange(goal[0], goal[1]);
}
}
}
for (let evt of events.TerritoryDecayChanged)
{
let ent = gameState.getEntityById(evt.entity);
if (!ent || !ent.isOwn(PlayerID) || ent.foundationProgress() !== undefined)
continue;
if (evt.to)
{
if (ent.isGarrisonHolder() && this.garrisonManager.addDecayingStructure(gameState, evt.entity))
continue;
if (!this.decayingStructures.has(evt.entity))
this.decayingStructures.add(evt.entity);
}
else if (ent.isGarrisonHolder())
this.garrisonManager.removeDecayingStructure(evt.entity);
}
if (addBase)
{
if (!this.firstBaseConfig)
{
// This is our first base, let us configure our starting resources
this.configFirstBase(gameState);
}
else
{
// Let us hope this new base will fix our possible resource shortage
this.saveResources = undefined;
this.saveSpace = undefined;
this.maxFields = false;
}
}
// Then deals with decaying structures: destroy them if being lost to enemy (except in easier difficulties)
if (this.Config.difficulty < 2)
return;
for (let entId of this.decayingStructures)
{
let ent = gameState.getEntityById(entId);
if (ent && ent.decaying() && ent.isOwn(PlayerID))
{
let capture = ent.capturePoints();
if (!capture)
continue;
let captureRatio = capture[PlayerID] / capture.reduce((a, b) => a + b);
if (captureRatio < 0.50)
continue;
let decayToGaia = true;
for (let i = 1; i < capture.length; ++i)
{
if (gameState.isPlayerAlly(i) || !capture[i])
continue;
decayToGaia = false;
break;
}
if (decayToGaia)
continue;
let ratioMax = 0.7 + randFloat(0, 0.1);
for (let evt of events.Attacked)
{
if (ent.id() != evt.target)
continue;
ratioMax = 0.85 + randFloat(0, 0.1);
break;
}
if (captureRatio > ratioMax)
continue;
ent.destroy();
}
this.decayingStructures.delete(entId);
}
};
/** Ensure that all requirements are met when phasing up*/
PETRA.HQ.prototype.checkPhaseRequirements = function(gameState, queues)
{
if (gameState.getNumberOfPhases() == this.currentPhase)
return;
let requirements = gameState.getPhaseEntityRequirements(this.currentPhase + 1);
let plan;
let queue;
for (let entityReq of requirements)
{
// Village requirements are met elsewhere by constructing more houses
if (entityReq.class == "Village" || entityReq.class == "NotField")
continue;
if (gameState.getOwnEntitiesByClass(entityReq.class, true).length >= entityReq.count)
continue;
switch (entityReq.class)
{
case "Town":
if (!queues.economicBuilding.hasQueuedUnits() &&
!queues.militaryBuilding.hasQueuedUnits() &&
!queues.defenseBuilding.hasQueuedUnits())
{
if (!gameState.getOwnEntitiesByClass("Market", true).hasEntities() &&
this.canBuild(gameState, "structures/{civ}/market"))
{
plan = new PETRA.ConstructionPlan(gameState, "structures/{civ}/market", { "phaseUp": true });
queue = "economicBuilding";
break;
}
if (!gameState.getOwnEntitiesByClass("Temple", true).hasEntities() &&
this.canBuild(gameState, "structures/{civ}/temple"))
{
plan = new PETRA.ConstructionPlan(gameState, "structures/{civ}/temple", { "phaseUp": true });
queue = "economicBuilding";
break;
}
if (!gameState.getOwnEntitiesByClass("Forge", true).hasEntities() &&
this.canBuild(gameState, "structures/{civ}/forge"))
{
plan = new PETRA.ConstructionPlan(gameState, "structures/{civ}/forge", { "phaseUp": true });
queue = "militaryBuilding";
break;
}
if (this.canBuild(gameState, "structures/{civ}/defense_tower"))
{
plan = new PETRA.ConstructionPlan(gameState, "structures/{civ}/defense_tower", { "phaseUp": true });
queue = "defenseBuilding";
break;
}
}
break;
default:
// All classes not dealt with inside vanilla game.
// We put them for the time being on the economic queue, except if wonder
queue = entityReq.class == "Wonder" ? "wonder" : "economicBuilding";
if (!queues[queue].hasQueuedUnits())
{
let structure = this.buildManager.findStructureWithClass(gameState, [entityReq.class]);
if (structure && this.canBuild(gameState, structure))
plan = new PETRA.ConstructionPlan(gameState, structure, { "phaseUp": true });
}
}
if (plan)
{
if (queue == "wonder")
{
gameState.ai.queueManager.changePriority("majorTech", 400, { "phaseUp": true });
plan.queueToReset = "majorTech";
}
else
{
gameState.ai.queueManager.changePriority(queue, 1000, { "phaseUp": true });
plan.queueToReset = queue;
}
queues[queue].addPlan(plan);
return;
}
}
};
/** Called by any "phase" research plan once it's started */
PETRA.HQ.prototype.OnPhaseUp = function(gameState, phase)
{
};
/** This code trains citizen workers, trying to keep close to a ratio of worker/soldiers */
PETRA.HQ.prototype.trainMoreWorkers = function(gameState, queues)
{
// default template
let requirementsDef = [ ["costsResource", 1, "food"] ];
let classesDef = ["Support", "Worker"];
let templateDef = this.findBestTrainableUnit(gameState, classesDef, requirementsDef);
// counting the workers that aren't part of a plan
let numberOfWorkers = 0; // all workers
let numberOfSupports = 0; // only support workers (i.e. non fighting)
gameState.getOwnUnits().forEach(ent => {
if (ent.getMetadata(PlayerID, "role") == "worker" && ent.getMetadata(PlayerID, "plan") === undefined)
{
++numberOfWorkers;
if (ent.hasClass("Support"))
++numberOfSupports;
}
});
let numberInTraining = 0;
gameState.getOwnTrainingFacilities().forEach(function(ent) {
for (let item of ent.trainingQueue())
{
numberInTraining += item.count;
if (item.metadata && item.metadata.role && item.metadata.role == "worker" &&
item.metadata.plan === undefined)
{
numberOfWorkers += item.count;
if (item.metadata.support)
numberOfSupports += item.count;
}
}
});
// Anticipate the optimal batch size when this queue will start
// and adapt the batch size of the first and second queued workers to the present population
// to ease a possible recovery if our population was drastically reduced by an attack
// (need to go up to second queued as it is accounted in queueManager)
let size = numberOfWorkers < 12 ? 1 : Math.min(5, Math.ceil(numberOfWorkers / 10));
if (queues.villager.plans[0])
{
queues.villager.plans[0].number = Math.min(queues.villager.plans[0].number, size);
if (queues.villager.plans[1])
queues.villager.plans[1].number = Math.min(queues.villager.plans[1].number, size);
}
if (queues.citizenSoldier.plans[0])
{
queues.citizenSoldier.plans[0].number = Math.min(queues.citizenSoldier.plans[0].number, size);
if (queues.citizenSoldier.plans[1])
queues.citizenSoldier.plans[1].number = Math.min(queues.citizenSoldier.plans[1].number, size);
}
let numberOfQueuedSupports = queues.villager.countQueuedUnits();
let numberOfQueuedSoldiers = queues.citizenSoldier.countQueuedUnits();
let numberQueued = numberOfQueuedSupports + numberOfQueuedSoldiers;
let numberTotal = numberOfWorkers + numberQueued;
if (this.saveResources && numberTotal > this.Config.Economy.popPhase2 + 10)
return;
if (numberTotal > this.targetNumWorkers || (numberTotal >= this.Config.Economy.popPhase2 &&
this.currentPhase == 1 && !gameState.isResearching(gameState.getPhaseName(2))))
return;
if (numberQueued > 50 || (numberOfQueuedSupports > 20 && numberOfQueuedSoldiers > 20) || numberInTraining > 15)
return;
// Choose whether we want soldiers or support units: when full pop, we aim at targetNumWorkers workers
// with supportRatio fraction of support units. But we want to have more support (less cost) at startup.
// So we take: supportRatio*targetNumWorkers*(1 - exp(-alfa*currentWorkers/supportRatio/targetNumWorkers))
// This gives back supportRatio*targetNumWorkers when currentWorkers >> supportRatio*targetNumWorkers
// and gives a ratio alfa at startup.
let supportRatio = this.supportRatio;
let alpha = 0.85;
if (!gameState.isTemplateAvailable(gameState.applyCiv("structures/{civ}/field")))
supportRatio = Math.min(this.supportRatio, 0.1);
if (this.attackManager.rushNumber < this.attackManager.maxRushes || this.attackManager.upcomingAttacks.Rush.length)
alpha = 0.7;
if (gameState.isCeasefireActive())
alpha += (1 - alpha) * Math.min(Math.max(gameState.ceasefireTimeRemaining - 120, 0), 180) / 180;
let supportMax = supportRatio * this.targetNumWorkers;
let supportNum = supportMax * (1 - Math.exp(-alpha*numberTotal/supportMax));
let template;
if (!templateDef || numberOfSupports + numberOfQueuedSupports > supportNum)
{
let requirements;
if (numberTotal < 45)
requirements = [ ["speed", 0.5], ["costsResource", 0.5, "stone"], ["costsResource", 0.5, "metal"] ];
else
requirements = [ ["strength", 1] ];
let classes = ["CitizenSoldier", "Infantry"];
// We want at least 33% ranged and 33% melee
classes.push(pickRandom(["Ranged", "Melee", "Infantry"]));
template = this.findBestTrainableUnit(gameState, classes, requirements);
}
// If the template variable is empty, the default unit (Support unit) will be used
// base "0" means automatic choice of base
if (!template && templateDef)
queues.villager.addPlan(new PETRA.TrainingPlan(gameState, templateDef, { "role": "worker", "base": 0, "support": true }, size, size));
else if (template)
queues.citizenSoldier.addPlan(new PETRA.TrainingPlan(gameState, template, { "role": "worker", "base": 0 }, size, size));
};
/** picks the best template based on parameters and classes */
PETRA.HQ.prototype.findBestTrainableUnit = function(gameState, classes, requirements)
{
let units;
if (classes.indexOf("Hero") != -1)
units = gameState.findTrainableUnits(classes, []);
else if (classes.indexOf("Siege") != -1) // We do not want siege tower as AI does not know how to use it
units = gameState.findTrainableUnits(classes, ["SiegeTower"]);
else // We do not want hero when not explicitely specified
units = gameState.findTrainableUnits(classes, ["Hero"]);
if (!units.length)
return undefined;
let parameters = requirements.slice();
let remainingResources = this.getTotalResourceLevel(gameState); // resources (estimation) still gatherable in our territory
let availableResources = gameState.ai.queueManager.getAvailableResources(gameState); // available (gathered) resources
for (let type in remainingResources)
{
if (availableResources[type] > 800)
continue;
if (remainingResources[type] > 800)
continue;
let costsResource = remainingResources[type] > 400 ? 0.6 : 0.2;
let toAdd = true;
for (let param of parameters)
{
if (param[0] != "costsResource" || param[2] != type)
continue;
param[1] = Math.min(param[1], costsResource);
toAdd = false;
break;
}
if (toAdd)
parameters.push(["costsResource", costsResource, type]);
}
units.sort((a, b) => {
let aCost = 1 + a[1].costSum();
let bCost = 1 + b[1].costSum();
let aValue = 0.1;
let bValue = 0.1;
for (let param of parameters)
{
if (param[0] == "strength")
{
aValue += PETRA.getMaxStrength(a[1], gameState.ai.Config.debug, gameState.ai.Config.DamageTypeImportance) * param[1];
bValue += PETRA.getMaxStrength(b[1], gameState.ai.Config.debug, gameState.ai.Config.DamageTypeImportance) * param[1];
}
else if (param[0] == "siegeStrength")
{
aValue += PETRA.getMaxStrength(a[1], gameState.ai.Config.debug, gameState.ai.Config.DamageTypeImportance, "Structure") * param[1];
bValue += PETRA.getMaxStrength(b[1], gameState.ai.Config.debug, gameState.ai.Config.DamageTypeImportance, "Structure") * param[1];
}
else if (param[0] == "speed")
{
aValue += a[1].walkSpeed() * param[1];
bValue += b[1].walkSpeed() * param[1];
}
else if (param[0] == "costsResource")
{
// requires a third parameter which is the resource
if (a[1].cost()[param[2]])
aValue *= param[1];
if (b[1].cost()[param[2]])
bValue *= param[1];
}
else if (param[0] == "canGather")
{
// checking against wood, could be anything else really.
if (a[1].resourceGatherRates() && a[1].resourceGatherRates()["wood.tree"])
aValue *= param[1];
if (b[1].resourceGatherRates() && b[1].resourceGatherRates()["wood.tree"])
bValue *= param[1];
}
else
API3.warn(" trainMoreUnits avec non prevu " + uneval(param));
}
return -aValue/aCost + bValue/bCost;
});
return units[0][0];
};
/**
* returns an entity collection of workers through BaseManager.pickBuilders
* TODO: when same accessIndex, sort by distance
*/
PETRA.HQ.prototype.bulkPickWorkers = function(gameState, baseRef, number)
{
let accessIndex = baseRef.accessIndex;
if (!accessIndex)
return false;
// sorting bases by whether they are on the same accessindex or not.
let baseBest = this.baseManagers.slice().sort((a, b) => {
if (a.accessIndex == accessIndex && b.accessIndex != accessIndex)
return -1;
else if (b.accessIndex == accessIndex && a.accessIndex != accessIndex)
return 1;
return 0;
});
let needed = number;
let workers = new API3.EntityCollection(gameState.sharedScript);
for (let base of baseBest)
{
if (base.ID == baseRef.ID)
continue;
base.pickBuilders(gameState, workers, needed);
if (workers.length >= number)
break;
needed = number - workers.length;
}
if (!workers.length)
return false;
return workers;
};
PETRA.HQ.prototype.getTotalResourceLevel = function(gameState)
{
let total = {};
for (let res of Resources.GetCodes())
total[res] = 0;
for (let base of this.baseManagers)
for (let res in total)
total[res] += base.getResourceLevel(gameState, res);
return total;
};
/**
* Returns the current gather rate
* This is not per-se exact, it performs a few adjustments ad-hoc to account for travel distance, stuffs like that.
*/
PETRA.HQ.prototype.GetCurrentGatherRates = function(gameState)
{
if (!this.turnCache.currentRates)
{
let currentRates = {};
for (let res of Resources.GetCodes())
currentRates[res] = 0.5 * this.GetTCResGatherer(res);
for (let base of this.baseManagers)
base.addGatherRates(gameState, currentRates);
for (let res of Resources.GetCodes())
currentRates[res] = Math.max(currentRates[res], 0);
this.turnCache.currentRates = currentRates;
}
return this.turnCache.currentRates;
};
/**
* Returns the wanted gather rate.
*/
PETRA.HQ.prototype.GetWantedGatherRates = function(gameState)
{
if (!this.turnCache.wantedRates)
this.turnCache.wantedRates = gameState.ai.queueManager.wantedGatherRates(gameState);
return this.turnCache.wantedRates;
};
/**
* Pick the resource which most needs another worker
* How this works:
* We get the rates we would want to have to be able to deal with our plans
* We get our current rates
* We compare; we pick the one where the discrepancy is highest.
* Need to balance long-term needs and possible short-term needs.
*/
PETRA.HQ.prototype.pickMostNeededResources = function(gameState, allowedResources = [])
{
let wantedRates = this.GetWantedGatherRates(gameState);
let currentRates = this.GetCurrentGatherRates(gameState);
if (!allowedResources.length)
allowedResources = Resources.GetCodes();
let needed = [];
for (let res of allowedResources)
needed.push({ "type": res, "wanted": wantedRates[res], "current": currentRates[res] });
needed.sort((a, b) => {
if (a.current < a.wanted && b.current < b.wanted)
{
if (a.current && b.current)
return b.wanted / b.current - a.wanted / a.current;
if (a.current)
return 1;
if (b.current)
return -1;
return b.wanted - a.wanted;
}
if (a.current < a.wanted || a.wanted && !b.wanted)
return -1;
if (b.current < b.wanted || b.wanted && !a.wanted)
return 1;
return a.current - a.wanted - b.current + b.wanted;
});
return needed;
};
/**
* Returns the best position to build a new Civil Center
* Whose primary function would be to reach new resources of type "resource".
*/
PETRA.HQ.prototype.findEconomicCCLocation = function(gameState, template, resource, proximity, fromStrategic)
{
// This builds a map. The procedure is fairly simple. It adds the resource maps
// (which are dynamically updated and are made so that they will facilitate DP placement)
// Then look for a good spot.
Engine.ProfileStart("findEconomicCCLocation");
// obstruction map
let obstructions = PETRA.createObstructionMap(gameState, 0, template);
let halfSize = 0;
if (template.get("Footprint/Square"))
halfSize = Math.max(+template.get("Footprint/Square/@depth"), +template.get("Footprint/Square/@width")) / 2;
else if (template.get("Footprint/Circle"))
halfSize = +template.get("Footprint/Circle/@radius");
let ccEnts = gameState.updatingGlobalCollection("allCCs", API3.Filters.byClass("CivCentre"));
let dpEnts = gameState.getOwnDropsites().filter(API3.Filters.not(API3.Filters.byClassesOr(["CivCentre", "Unit"])));
let ccList = [];
for (let cc of ccEnts.values())
ccList.push({ "ent": cc, "pos": cc.position(), "ally": gameState.isPlayerAlly(cc.owner()) });
let dpList = [];
for (let dp of dpEnts.values())
dpList.push({ "ent": dp, "pos": dp.position(), "territory": this.territoryMap.getOwner(dp.position()) });
let bestIdx;
let bestVal;
let radius = Math.ceil(template.obstructionRadius().max / obstructions.cellSize);
let scale = 250 * 250;
let proxyAccess;
let nbShips = this.navalManager.transportShips.length;
if (proximity) // this is our first base
{
// if our first base, ensure room around
radius = Math.ceil((template.obstructionRadius().max + 8) / obstructions.cellSize);
// scale is the typical scale at which we want to find a location for our first base
// look for bigger scale if we start from a ship (access < 2) or from a small island
let cellArea = gameState.getPassabilityMap().cellSize * gameState.getPassabilityMap().cellSize;
proxyAccess = gameState.ai.accessibility.getAccessValue(proximity);
if (proxyAccess < 2 || cellArea*gameState.ai.accessibility.regionSize[proxyAccess] < 24000)
scale = 400 * 400;
}
let width = this.territoryMap.width;
let cellSize = this.territoryMap.cellSize;
// DistanceSquare cuts to other ccs (bigger or no cuts on inaccessible ccs to allow colonizing other islands).
let reduce = (template.hasClass("Colony") ? 30 : 0) + 30 * this.Config.personality.defensive;
let nearbyRejected = Math.square(120); // Reject if too near from any cc
let nearbyAllyRejected = Math.square(200); // Reject if too near from an allied cc
let nearbyAllyDisfavored = Math.square(250); // Disfavor if quite near an allied cc
let maxAccessRejected = Math.square(410); // Reject if too far from an accessible ally cc
let maxAccessDisfavored = Math.square(360 - reduce); // Disfavor if quite far from an accessible ally cc
let maxNoAccessDisfavored = Math.square(500); // Disfavor if quite far from an inaccessible ally cc
let cut = 60;
if (fromStrategic || proximity) // be less restrictive
cut = 30;
for (let j = 0; j < this.territoryMap.length; ++j)
{
if (this.territoryMap.getOwnerIndex(j) != 0)
continue;
// With enough room around to build the cc
let i = this.territoryMap.getNonObstructedTile(j, radius, obstructions);
if (i < 0)
continue;
// We require that it is accessible
let index = gameState.ai.accessibility.landPassMap[i];
if (!this.landRegions[index])
continue;
if (proxyAccess && nbShips == 0 && proxyAccess != index)
continue;
let norm = 0.5; // TODO adjust it, knowing that we will sum 5 maps
// Checking distance to other cc
let pos = [cellSize * (j%width+0.5), cellSize * (Math.floor(j/width)+0.5)];
// We will be more tolerant for cc around our oversea docks
let oversea = false;
if (proximity) // This is our first cc, let's do it near our units
norm /= 1 + API3.SquareVectorDistance(proximity, pos) / scale;
else
{
let minDist = Math.min();
let accessible = false;
for (let cc of ccList)
{
let dist = API3.SquareVectorDistance(cc.pos, pos);
if (dist < nearbyRejected)
{
norm = 0;
break;
}
if (!cc.ally)
continue;
if (dist < nearbyAllyRejected)
{
norm = 0;
break;
}
if (dist < nearbyAllyDisfavored)
norm *= 0.5;
if (dist < minDist)
minDist = dist;
accessible = accessible || index == PETRA.getLandAccess(gameState, cc.ent);
}
if (norm == 0)
continue;
if (accessible && minDist > maxAccessRejected)
continue;
if (minDist > maxAccessDisfavored) // Disfavor if quite far from any allied cc
{
if (!accessible)
{
if (minDist > maxNoAccessDisfavored)
norm *= 0.5;
else
norm *= 0.8;
}
else
norm *= 0.5;
}
// Not near any of our dropsite, except for oversea docks
oversea = !accessible && dpList.some(dp => PETRA.getLandAccess(gameState, dp.ent) == index);
if (!oversea)
{
for (let dp of dpList)
{
let dist = API3.SquareVectorDistance(dp.pos, pos);
if (dist < 3600)
{
norm = 0;
break;
}
else if (dist < 6400)
norm *= 0.5;
}
}
if (norm == 0)
continue;
}
if (this.borderMap.map[j] & PETRA.fullBorder_Mask) // disfavor the borders of the map
norm *= 0.5;
let val = 2 * gameState.sharedScript.ccResourceMaps[resource].map[j];
for (let res in gameState.sharedScript.resourceMaps)
if (res != "food")
val += gameState.sharedScript.ccResourceMaps[res].map[j];
val *= norm;
// If oversea, be just above threshold to be accepted if nothing else
if (oversea)
val = Math.max(val, cut + 0.1);
if (bestVal !== undefined && val < bestVal)
continue;
if (this.isDangerousLocation(gameState, pos, halfSize))
continue;
bestVal = val;
bestIdx = i;
}
Engine.ProfileStop();
if (bestVal === undefined)
return false;
if (this.Config.debug > 1)
API3.warn("we have found a base for " + resource + " with best (cut=" + cut + ") = " + bestVal);
// not good enough.
if (bestVal < cut)
return false;
let x = (bestIdx % obstructions.width + 0.5) * obstructions.cellSize;
let z = (Math.floor(bestIdx / obstructions.width) + 0.5) * obstructions.cellSize;
// Define a minimal number of wanted ships in the seas reaching this new base
let indexIdx = gameState.ai.accessibility.landPassMap[bestIdx];
for (let base of this.baseManagers)
{
if (!base.anchor || base.accessIndex == indexIdx)
continue;
let sea = this.getSeaBetweenIndices(gameState, base.accessIndex, indexIdx);
if (sea !== undefined)
this.navalManager.setMinimalTransportShips(gameState, sea, 1);
}
return [x, z];
};
/**
* Returns the best position to build a new Civil Center
* Whose primary function would be to assure territorial continuity with our allies
*/
PETRA.HQ.prototype.findStrategicCCLocation = function(gameState, template)
{
// This builds a map. The procedure is fairly simple.
// We minimize the Sum((dist - 300)^2) where the sum is on the three nearest allied CC
// with the constraints that all CC have dist > 200 and at least one have dist < 400
// This needs at least 2 CC. Otherwise, go back to economic CC.
let ccEnts = gameState.updatingGlobalCollection("allCCs", API3.Filters.byClass("CivCentre"));
let ccList = [];
let numAllyCC = 0;
for (let cc of ccEnts.values())
{
let ally = gameState.isPlayerAlly(cc.owner());
ccList.push({ "pos": cc.position(), "ally": ally });
if (ally)
++numAllyCC;
}
if (numAllyCC < 2)
return this.findEconomicCCLocation(gameState, template, "wood", undefined, true);
Engine.ProfileStart("findStrategicCCLocation");
// obstruction map
let obstructions = PETRA.createObstructionMap(gameState, 0, template);
let halfSize = 0;
if (template.get("Footprint/Square"))
halfSize = Math.max(+template.get("Footprint/Square/@depth"), +template.get("Footprint/Square/@width")) / 2;
else if (template.get("Footprint/Circle"))
halfSize = +template.get("Footprint/Circle/@radius");
let bestIdx;
let bestVal;
let radius = Math.ceil(template.obstructionRadius().max / obstructions.cellSize);
let width = this.territoryMap.width;
let cellSize = this.territoryMap.cellSize;
let currentVal, delta;
let distcc0, distcc1, distcc2;
let favoredDistance = (template.hasClass("Colony") ? 220 : 280) - 40 * this.Config.personality.defensive;
for (let j = 0; j < this.territoryMap.length; ++j)
{
if (this.territoryMap.getOwnerIndex(j) != 0)
continue;
// with enough room around to build the cc
let i = this.territoryMap.getNonObstructedTile(j, radius, obstructions);
if (i < 0)
continue;
// we require that it is accessible
let index = gameState.ai.accessibility.landPassMap[i];
if (!this.landRegions[index])
continue;
// checking distances to other cc
let pos = [cellSize * (j%width+0.5), cellSize * (Math.floor(j/width)+0.5)];
let minDist = Math.min();
distcc0 = undefined;
for (let cc of ccList)
{
let dist = API3.SquareVectorDistance(cc.pos, pos);
if (dist < 14000) // Reject if too near from any cc
{
minDist = 0;
break;
}
if (!cc.ally)
continue;
if (dist < 62000) // Reject if quite near from ally cc
{
minDist = 0;
break;
}
if (dist < minDist)
minDist = dist;
if (!distcc0 || dist < distcc0)
{
distcc2 = distcc1;
distcc1 = distcc0;
distcc0 = dist;
}
else if (!distcc1 || dist < distcc1)
{
distcc2 = distcc1;
distcc1 = dist;
}
else if (!distcc2 || dist < distcc2)
distcc2 = dist;
}
if (minDist < 1 || minDist > 170000 && !this.navalMap)
continue;
delta = Math.sqrt(distcc0) - favoredDistance;
currentVal = delta*delta;
delta = Math.sqrt(distcc1) - favoredDistance;
currentVal += delta*delta;
if (distcc2)
{
delta = Math.sqrt(distcc2) - favoredDistance;
currentVal += delta*delta;
}
// disfavor border of the map
if (this.borderMap.map[j] & PETRA.fullBorder_Mask)
currentVal += 10000;
if (bestVal !== undefined && currentVal > bestVal)
continue;
if (this.isDangerousLocation(gameState, pos, halfSize))
continue;
bestVal = currentVal;
bestIdx = i;
}
if (this.Config.debug > 1)
API3.warn("We've found a strategic base with bestVal = " + bestVal);
Engine.ProfileStop();
if (bestVal === undefined)
return undefined;
let x = (bestIdx % obstructions.width + 0.5) * obstructions.cellSize;
let z = (Math.floor(bestIdx / obstructions.width) + 0.5) * obstructions.cellSize;
// Define a minimal number of wanted ships in the seas reaching this new base
let indexIdx = gameState.ai.accessibility.landPassMap[bestIdx];
for (let base of this.baseManagers)
{
if (!base.anchor || base.accessIndex == indexIdx)
continue;
let sea = this.getSeaBetweenIndices(gameState, base.accessIndex, indexIdx);
if (sea !== undefined)
this.navalManager.setMinimalTransportShips(gameState, sea, 1);
}
return [x, z];
};
/**
* Returns the best position to build a new market: if the allies already have a market, build it as far as possible
* from it, although not in our border to be able to defend it easily. If no allied market, our second market will
* follow the same logic.
* To do so, we suppose that the gain/distance is an increasing function of distance and look for the max distance
* for performance reasons.
*/
PETRA.HQ.prototype.findMarketLocation = function(gameState, template)
{
let markets = gameState.updatingCollection("diplo-ExclusiveAllyMarkets", API3.Filters.byClass("Trade"), gameState.getExclusiveAllyEntities()).toEntityArray();
if (!markets.length)
markets = gameState.updatingCollection("OwnMarkets", API3.Filters.byClass("Trade"), gameState.getOwnStructures()).toEntityArray();
if (!markets.length) // this is the first market. For the time being, place it arbitrarily by the ConstructionPlan
return [-1, -1, -1, 0];
// No need for more than one market when we cannot trade.
if (!Resources.GetTradableCodes().length)
return false;
// obstruction map
let obstructions = PETRA.createObstructionMap(gameState, 0, template);
let halfSize = 0;
if (template.get("Footprint/Square"))
halfSize = Math.max(+template.get("Footprint/Square/@depth"), +template.get("Footprint/Square/@width")) / 2;
else if (template.get("Footprint/Circle"))
halfSize = +template.get("Footprint/Circle/@radius");
let bestIdx;
let bestJdx;
let bestVal;
let bestDistSq;
let bestGainMult;
let radius = Math.ceil(template.obstructionRadius().max / obstructions.cellSize);
let isNavalMarket = template.hasClass("Naval") && template.hasClass("Trade");
let width = this.territoryMap.width;
let cellSize = this.territoryMap.cellSize;
let traderTemplatesGains = gameState.getTraderTemplatesGains();
for (let j = 0; j < this.territoryMap.length; ++j)
{
// do not try on the narrow border of our territory
if (this.borderMap.map[j] & PETRA.narrowFrontier_Mask)
continue;
if (this.basesMap.map[j] == 0) // only in our territory
continue;
// with enough room around to build the market
let i = this.territoryMap.getNonObstructedTile(j, radius, obstructions);
if (i < 0)
continue;
let index = gameState.ai.accessibility.landPassMap[i];
if (!this.landRegions[index])
continue;
let pos = [cellSize * (j%width+0.5), cellSize * (Math.floor(j/width)+0.5)];
// checking distances to other markets
let maxVal = 0;
let maxDistSq;
let maxGainMult;
let gainMultiplier;
for (let market of markets)
{
if (isNavalMarket && template.hasClass("Naval") && template.hasClass("Trade"))
{
if (PETRA.getSeaAccess(gameState, market) != gameState.ai.accessibility.getAccessValue(pos, true))
continue;
gainMultiplier = traderTemplatesGains.navalGainMultiplier;
}
else if (PETRA.getLandAccess(gameState, market) == index &&
!PETRA.isLineInsideEnemyTerritory(gameState, market.position(), pos))
gainMultiplier = traderTemplatesGains.landGainMultiplier;
else
continue;
if (!gainMultiplier)
continue;
let distSq = API3.SquareVectorDistance(market.position(), pos);
if (gainMultiplier * distSq > maxVal)
{
maxVal = gainMultiplier * distSq;
maxDistSq = distSq;
maxGainMult = gainMultiplier;
}
}
if (maxVal == 0)
continue;
if (bestVal !== undefined && maxVal < bestVal)
continue;
if (this.isDangerousLocation(gameState, pos, halfSize))
continue;
bestVal = maxVal;
bestDistSq = maxDistSq;
bestGainMult = maxGainMult;
bestIdx = i;
bestJdx = j;
}
if (this.Config.debug > 1)
API3.warn("We found a market position with bestVal = " + bestVal);
if (bestVal === undefined) // no constraints. For the time being, place it arbitrarily by the ConstructionPlan
return [-1, -1, -1, 0];
let expectedGain = Math.round(bestGainMult * TradeGain(bestDistSq, gameState.sharedScript.mapSize));
if (this.Config.debug > 1)
API3.warn("this would give a trading gain of " + expectedGain);
// Do not keep it if gain is too small, except if this is our first Market.
let idx;
if (expectedGain < this.tradeManager.minimalGain)
{
if (template.hasClass("Market") &&
!gameState.getOwnEntitiesByClass("Market", true).hasEntities())
idx = -1; // Needed by queueplanBuilding manager to keep that Market.
else
return false;
}
else
idx = this.basesMap.map[bestJdx];
let x = (bestIdx % obstructions.width + 0.5) * obstructions.cellSize;
let z = (Math.floor(bestIdx / obstructions.width) + 0.5) * obstructions.cellSize;
return [x, z, idx, expectedGain];
};
/**
* Returns the best position to build defensive buildings (fortress and towers)
* Whose primary function is to defend our borders
*/
PETRA.HQ.prototype.findDefensiveLocation = function(gameState, template)
{
// We take the point in our territory which is the nearest to any enemy cc
// but requiring a minimal distance with our other defensive structures
// and not in range of any enemy defensive structure to avoid building under fire.
let ownStructures = gameState.getOwnStructures().filter(API3.Filters.byClassesOr(["Fortress", "Tower"])).toEntityArray();
let enemyStructures = gameState.getEnemyStructures().filter(API3.Filters.not(API3.Filters.byOwner(0))).
filter(API3.Filters.byClassesOr(["CivCentre", "Fortress", "Tower"]));
if (!enemyStructures.hasEntities()) // we may be in cease fire mode, build defense against neutrals
{
enemyStructures = gameState.getNeutralStructures().filter(API3.Filters.not(API3.Filters.byOwner(0))).
filter(API3.Filters.byClassesOr(["CivCentre", "Fortress", "Tower"]));
if (!enemyStructures.hasEntities() && !gameState.getAlliedVictory())
enemyStructures = gameState.getAllyStructures().filter(API3.Filters.not(API3.Filters.byOwner(PlayerID))).
filter(API3.Filters.byClassesOr(["CivCentre", "Fortress", "Tower"]));
if (!enemyStructures.hasEntities())
return undefined;
}
enemyStructures = enemyStructures.toEntityArray();
let wonderMode = gameState.getVictoryConditions().has("wonder");
let wonderDistmin;
let wonders;
if (wonderMode)
{
wonders = gameState.getOwnStructures().filter(API3.Filters.byClass("Wonder")).toEntityArray();
wonderMode = wonders.length != 0;
if (wonderMode)
wonderDistmin = (50 + wonders[0].footprintRadius()) * (50 + wonders[0].footprintRadius());
}
// obstruction map
let obstructions = PETRA.createObstructionMap(gameState, 0, template);
let halfSize = 0;
if (template.get("Footprint/Square"))
halfSize = Math.max(+template.get("Footprint/Square/@depth"), +template.get("Footprint/Square/@width")) / 2;
else if (template.get("Footprint/Circle"))
halfSize = +template.get("Footprint/Circle/@radius");
let bestIdx;
let bestJdx;
let bestVal;
let width = this.territoryMap.width;
let cellSize = this.territoryMap.cellSize;
let isTower = template.hasClass("Tower");
let isFortress = template.hasClass("Fortress");
let radius;
if (isFortress)
radius = Math.floor((template.obstructionRadius().max + 8) / obstructions.cellSize);
else
radius = Math.ceil(template.obstructionRadius().max / obstructions.cellSize);
for (let j = 0; j < this.territoryMap.length; ++j)
{
if (!wonderMode)
{
// do not try if well inside or outside territory
if (!(this.borderMap.map[j] & PETRA.fullFrontier_Mask))
continue;
if (this.borderMap.map[j] & PETRA.largeFrontier_Mask && isTower)
continue;
}
if (this.basesMap.map[j] == 0) // inaccessible cell
continue;
// with enough room around to build the cc
let i = this.territoryMap.getNonObstructedTile(j, radius, obstructions);
if (i < 0)
continue;
let pos = [cellSize * (j%width+0.5), cellSize * (Math.floor(j/width)+0.5)];
// checking distances to other structures
let minDist = Math.min();
let dista = 0;
if (wonderMode)
{
dista = API3.SquareVectorDistance(wonders[0].position(), pos);
if (dista < wonderDistmin)
continue;
dista *= 200; // empirical factor (TODO should depend on map size) to stay near the wonder
}
for (let str of enemyStructures)
{
if (str.foundationProgress() !== undefined)
continue;
let strPos = str.position();
if (!strPos)
continue;
let dist = API3.SquareVectorDistance(strPos, pos);
if (dist < 6400) // TODO check on true attack range instead of this 80×80
{
minDist = -1;
break;
}
if (str.hasClass("CivCentre") && dist + dista < minDist)
minDist = dist + dista;
}
if (minDist < 0)
continue;
let cutDist = 900; // 30×30 TODO maybe increase it
for (let str of ownStructures)
{
let strPos = str.position();
if (!strPos)
continue;
if (API3.SquareVectorDistance(strPos, pos) < cutDist)
{
minDist = -1;
break;
}
}
if (minDist < 0 || minDist == Math.min())
continue;
if (bestVal !== undefined && minDist > bestVal)
continue;
if (this.isDangerousLocation(gameState, pos, halfSize))
continue;
bestVal = minDist;
bestIdx = i;
bestJdx = j;
}
if (bestVal === undefined)
return undefined;
let x = (bestIdx % obstructions.width + 0.5) * obstructions.cellSize;
let z = (Math.floor(bestIdx / obstructions.width) + 0.5) * obstructions.cellSize;
return [x, z, this.basesMap.map[bestJdx]];
};
PETRA.HQ.prototype.buildTemple = function(gameState, queues)
{
// at least one market (which have the same queue) should be build before any temple
if (queues.economicBuilding.hasQueuedUnits() ||
gameState.getOwnEntitiesByClass("Temple", true).hasEntities() ||
!gameState.getOwnEntitiesByClass("Market", true).hasEntities())
return;
// Try to build a temple earlier if in regicide to recruit healer guards
if (this.currentPhase < 3 && !gameState.getVictoryConditions().has("regicide"))
return;
let templateName = "structures/{civ}/temple";
if (this.canBuild(gameState, "structures/{civ}/temple_vesta"))
templateName = "structures/{civ}/temple_vesta";
else if (!this.canBuild(gameState, templateName))
return;
queues.economicBuilding.addPlan(new PETRA.ConstructionPlan(gameState, templateName));
};
PETRA.HQ.prototype.buildMarket = function(gameState, queues)
{
if (gameState.getOwnEntitiesByClass("Market", true).hasEntities() ||
!this.canBuild(gameState, "structures/{civ}/market"))
return;
if (queues.economicBuilding.hasQueuedUnitsWithClass("Market"))
{
if (!queues.economicBuilding.paused)
{
// Put available resources in this market
let queueManager = gameState.ai.queueManager;
let cost = queues.economicBuilding.plans[0].getCost();
queueManager.setAccounts(gameState, cost, "economicBuilding");
if (!queueManager.canAfford("economicBuilding", cost))
{
for (let q in queueManager.queues)
{
if (q == "economicBuilding")
continue;
queueManager.transferAccounts(cost, q, "economicBuilding");
if (queueManager.canAfford("economicBuilding", cost))
break;
}
}
}
return;
}
gameState.ai.queueManager.changePriority("economicBuilding", 3 * this.Config.priorities.economicBuilding);
let plan = new PETRA.ConstructionPlan(gameState, "structures/{civ}/market");
plan.queueToReset = "economicBuilding";
queues.economicBuilding.addPlan(plan);
};
/** Build a farmstead */
PETRA.HQ.prototype.buildFarmstead = function(gameState, queues)
{
// Only build one farmstead for the time being ("DropsiteFood" does not refer to CCs)
if (gameState.getOwnEntitiesByClass("Farmstead", true).hasEntities())
return;
// Wait to have at least one dropsite and house before the farmstead
if (!gameState.getOwnEntitiesByClass("Storehouse", true).hasEntities())
return;
if (!gameState.getOwnEntitiesByClass("House", true).hasEntities())
return;
if (queues.economicBuilding.hasQueuedUnitsWithClass("DropsiteFood"))
return;
if (!this.canBuild(gameState, "structures/{civ}/farmstead"))
return;
queues.economicBuilding.addPlan(new PETRA.ConstructionPlan(gameState, "structures/{civ}/farmstead"));
};
/**
* Try to build a wonder when required
* force = true when called from the victoryManager in case of Wonder victory condition.
*/
PETRA.HQ.prototype.buildWonder = function(gameState, queues, force = false)
{
if (queues.wonder && queues.wonder.hasQueuedUnits() ||
gameState.getOwnEntitiesByClass("Wonder", true).hasEntities() ||
!this.canBuild(gameState, "structures/{civ}/wonder"))
return;
if (!force)
{
let template = gameState.getTemplate(gameState.applyCiv("structures/{civ}/wonder"));
// Check that we have enough resources to start thinking to build a wonder
let cost = template.cost();
let resources = gameState.getResources();
let highLevel = 0;
let lowLevel = 0;
for (let res in cost)
{
if (resources[res] && resources[res] > 0.7 * cost[res])
++highLevel;
else if (!resources[res] || resources[res] < 0.3 * cost[res])
++lowLevel;
}
if (highLevel == 0 || lowLevel > 1)
return;
}
queues.wonder.addPlan(new PETRA.ConstructionPlan(gameState, "structures/{civ}/wonder"));
};
/** Build a corral, and train animals there */
PETRA.HQ.prototype.manageCorral = function(gameState, queues)
{
if (queues.corral.hasQueuedUnits())
return;
let nCorral = gameState.getOwnEntitiesByClass("Corral", true).length;
if (!nCorral || !gameState.isTemplateAvailable(gameState.applyCiv("structures/{civ}/field")) &&
nCorral < this.currentPhase && gameState.getPopulation() > 30 * nCorral)
{
if (this.canBuild(gameState, "structures/{civ}/corral"))
{
queues.corral.addPlan(new PETRA.ConstructionPlan(gameState, "structures/{civ}/corral"));
return;
}
if (!nCorral)
return;
}
// And train some animals
let civ = gameState.getPlayerCiv();
for (let corral of gameState.getOwnEntitiesByClass("Corral", true).values())
{
if (corral.foundationProgress() !== undefined)
continue;
let trainables = corral.trainableEntities(civ);
for (let trainable of trainables)
{
if (gameState.isTemplateDisabled(trainable))
continue;
let template = gameState.getTemplate(trainable);
if (!template || !template.isHuntable())
continue;
let count = gameState.countEntitiesByType(trainable, true);
for (let item of corral.trainingQueue())
count += item.count;
if (count > nCorral)
continue;
queues.corral.addPlan(new PETRA.TrainingPlan(gameState, trainable, { "trainer": corral.id() }));
return;
}
}
};
/**
* build more houses if needed.
* kinda ugly, lots of special cases to both build enough houses but not tooo many…
*/
PETRA.HQ.prototype.buildMoreHouses = function(gameState, queues)
{
if (!gameState.isTemplateAvailable(gameState.applyCiv("structures/{civ}/house")) ||
gameState.getPopulationMax() <= gameState.getPopulationLimit())
return;
let numPlanned = queues.house.length();
if (numPlanned < 3 || numPlanned < 5 && gameState.getPopulation() > 80)
{
let plan = new PETRA.ConstructionPlan(gameState, "structures/{civ}/house");
// change the starting condition according to the situation.
plan.goRequirement = "houseNeeded";
queues.house.addPlan(plan);
}
if (numPlanned > 0 && this.phasing && gameState.getPhaseEntityRequirements(this.phasing).length)
{
let houseTemplateName = gameState.applyCiv("structures/{civ}/house");
let houseTemplate = gameState.getTemplate(houseTemplateName);
let needed = 0;
for (let entityReq of gameState.getPhaseEntityRequirements(this.phasing))
{
if (!houseTemplate.hasClass(entityReq.class))
continue;
let count = gameState.getOwnStructures().filter(API3.Filters.byClass(entityReq.class)).length;
if (count < entityReq.count && this.buildManager.isUnbuildable(gameState, houseTemplateName))
{
if (this.Config.debug > 1)
API3.warn("no room to place a house ... try to be less restrictive");
this.buildManager.setBuildable(houseTemplateName);
this.requireHouses = true;
}
needed = Math.max(needed, entityReq.count - count);
}
let houseQueue = queues.house.plans;
for (let i = 0; i < numPlanned; ++i)
if (houseQueue[i].isGo(gameState))
--needed;
else if (needed > 0)
{
houseQueue[i].goRequirement = undefined;
--needed;
}
}
if (this.requireHouses)
{
let houseTemplate = gameState.getTemplate(gameState.applyCiv("structures/{civ}/house"));
if (!this.phasing || gameState.getPhaseEntityRequirements(this.phasing).every(req =>
!houseTemplate.hasClass(req.class) || gameState.getOwnStructures().filter(API3.Filters.byClass(req.class)).length >= req.count))
this.requireHouses = undefined;
}
// When population limit too tight
// - if no room to build, try to improve with technology
// - otherwise increase temporarily the priority of houses
let house = gameState.applyCiv("structures/{civ}/house");
let HouseNb = gameState.getOwnFoundations().filter(API3.Filters.byClass("House")).length;
let popBonus = gameState.getTemplate(house).getPopulationBonus();
let freeSlots = gameState.getPopulationLimit() + HouseNb*popBonus - this.getAccountedPopulation(gameState);
let priority;
if (freeSlots < 5)
{
if (this.buildManager.isUnbuildable(gameState, house))
{
if (this.Config.debug > 1)
API3.warn("no room to place a house ... try to improve with technology");
this.researchManager.researchPopulationBonus(gameState, queues);
}
else
priority = 2 * this.Config.priorities.house;
}
else
priority = this.Config.priorities.house;
if (priority && priority != gameState.ai.queueManager.getPriority("house"))
gameState.ai.queueManager.changePriority("house", priority);
};
/** Checks the status of the territory expansion. If no new economic bases created, build some strategic ones. */
PETRA.HQ.prototype.checkBaseExpansion = function(gameState, queues)
{
if (queues.civilCentre.hasQueuedUnits())
return;
// First build one cc if all have been destroyed
if (this.numPotentialBases() == 0)
{
this.buildFirstBase(gameState);
return;
}
// Then expand if we have not enough room available for buildings
if (this.buildManager.numberMissingRoom(gameState) > 1)
{
if (this.Config.debug > 2)
API3.warn("try to build a new base because not enough room to build ");
this.buildNewBase(gameState, queues);
return;
}
// If we've already planned to phase up, wait a bit before trying to expand
if (this.phasing)
return;
// Finally expand if we have lots of units (threshold depending on the aggressivity value)
let activeBases = this.numActiveBases();
let numUnits = gameState.getOwnUnits().length;
let numvar = 10 * (1 - this.Config.personality.aggressive);
if (numUnits > activeBases * (65 + numvar + (10 + numvar)*(activeBases-1)) || this.saveResources && numUnits > 50)
{
if (this.Config.debug > 2)
API3.warn("try to build a new base because of population " + numUnits + " for " + activeBases + " CCs");
this.buildNewBase(gameState, queues);
}
};
PETRA.HQ.prototype.buildNewBase = function(gameState, queues, resource)
{
if (this.numPotentialBases() > 0 && this.currentPhase == 1 && !gameState.isResearching(gameState.getPhaseName(2)))
return false;
if (gameState.getOwnFoundations().filter(API3.Filters.byClass("CivCentre")).hasEntities() || queues.civilCentre.hasQueuedUnits())
return false;
let template;
// We require at least one of this civ civCentre as they may allow specific units or techs
let hasOwnCC = false;
for (let ent of gameState.updatingGlobalCollection("allCCs", API3.Filters.byClass("CivCentre")).values())
{
if (ent.owner() != PlayerID || ent.templateName() != gameState.applyCiv("structures/{civ}/civil_centre"))
continue;
hasOwnCC = true;
break;
}
if (hasOwnCC && this.canBuild(gameState, "structures/{civ}/military_colony"))
template = "structures/{civ}/military_colony";
else if (this.canBuild(gameState, "structures/{civ}/civil_centre"))
template = "structures/{civ}/civil_centre";
else if (!hasOwnCC && this.canBuild(gameState, "structures/{civ}/military_colony"))
template = "structures/{civ}/military_colony";
else
return false;
// base "-1" means new base.
if (this.Config.debug > 1)
API3.warn("new base " + gameState.applyCiv(template) + " planned with resource " + resource);
queues.civilCentre.addPlan(new PETRA.ConstructionPlan(gameState, template, { "base": -1, "resource": resource }));
return true;
};
/** Deals with building fortresses and towers along our border with enemies. */
PETRA.HQ.prototype.buildDefenses = function(gameState, queues)
{
if (this.saveResources && !this.canBarter || queues.defenseBuilding.hasQueuedUnits())
return;
if (!this.saveResources && (this.currentPhase > 2 || gameState.isResearching(gameState.getPhaseName(3))))
{
// Try to build fortresses.
if (this.canBuild(gameState, "structures/{civ}/fortress"))
{
let numFortresses = gameState.getOwnEntitiesByClass("Fortress", true).length;
if ((!numFortresses || gameState.ai.elapsedTime > (1 + 0.10 * numFortresses) * this.fortressLapseTime + this.fortressStartTime) &&
numFortresses < this.numActiveBases() + 1 + this.extraFortresses &&
numFortresses < Math.floor(gameState.getPopulation() / 25) &&
gameState.getOwnFoundationsByClass("Fortress").length < 2)
{
this.fortressStartTime = gameState.ai.elapsedTime;
if (!numFortresses)
gameState.ai.queueManager.changePriority("defenseBuilding", 2 * this.Config.priorities.defenseBuilding);
let plan = new PETRA.ConstructionPlan(gameState, "structures/{civ}/fortress");
plan.queueToReset = "defenseBuilding";
queues.defenseBuilding.addPlan(plan);
return;
}
}
}
if (this.Config.Military.numSentryTowers && this.currentPhase < 2 && this.canBuild(gameState, "structures/{civ}/sentry_tower"))
{
// Count all towers + wall towers.
let numTowers = gameState.getOwnEntitiesByClass("Tower", true).length + gameState.getOwnEntitiesByClass("WallTower", true).length;
let towerLapseTime = this.saveResource ? (1 + 0.5 * numTowers) * this.towerLapseTime : this.towerLapseTime;
if (numTowers < this.Config.Military.numSentryTowers && gameState.ai.elapsedTime > towerLapseTime + this.fortStartTime)
{
this.fortStartTime = gameState.ai.elapsedTime;
queues.defenseBuilding.addPlan(new PETRA.ConstructionPlan(gameState, "structures/{civ}/sentry_tower"));
}
return;
}
if (this.currentPhase < 2 || !this.canBuild(gameState, "structures/{civ}/defense_tower"))
return;
let numTowers = gameState.getOwnEntitiesByClass("StoneTower", true).length;
let towerLapseTime = this.saveResource ? (1 + numTowers) * this.towerLapseTime : this.towerLapseTime;
if ((!numTowers || gameState.ai.elapsedTime > (1 + 0.1 * numTowers) * towerLapseTime + this.towerStartTime) &&
numTowers < 2 * this.numActiveBases() + 3 + this.extraTowers &&
numTowers < Math.floor(gameState.getPopulation() / 8) &&
gameState.getOwnFoundationsByClass("Tower").length < 3)
{
this.towerStartTime = gameState.ai.elapsedTime;
if (numTowers > 2 * this.numActiveBases() + 3)
gameState.ai.queueManager.changePriority("defenseBuilding", Math.round(0.7 * this.Config.priorities.defenseBuilding));
let plan = new PETRA.ConstructionPlan(gameState, "structures/{civ}/defense_tower");
plan.queueToReset = "defenseBuilding";
queues.defenseBuilding.addPlan(plan);
}
};
PETRA.HQ.prototype.buildForge = function(gameState, queues)
{
if (this.getAccountedPopulation(gameState) < this.Config.Military.popForForge ||
queues.militaryBuilding.hasQueuedUnits() || gameState.getOwnEntitiesByClass("Forge", true).length)
return;
// Build a Market before the Forge.
if (!gameState.getOwnEntitiesByClass("Market", true).hasEntities())
return;
if (this.canBuild(gameState, "structures/{civ}/forge"))
queues.militaryBuilding.addPlan(new PETRA.ConstructionPlan(gameState, "structures/{civ}/forge"));
};
/**
* Deals with constructing military buildings (barracks, stables…)
* They are mostly defined by Config.js. This is unreliable since changes could be done easily.
*/
PETRA.HQ.prototype.constructTrainingBuildings = function(gameState, queues)
{
if (this.saveResources && !this.canBarter || queues.militaryBuilding.hasQueuedUnits())
return;
let numBarracks = gameState.getOwnEntitiesByClass("Barracks", true).length;
if (this.saveResources && numBarracks != 0)
return;
let barracksTemplate = this.canBuild(gameState, "structures/{civ}/barracks") ? "structures/{civ}/barracks" : undefined;
let rangeTemplate = this.canBuild(gameState, "structures/{civ}/range") ? "structures/{civ}/range" : undefined;
let numRanges = gameState.getOwnEntitiesByClass("Range", true).length;
let stableTemplate = this.canBuild(gameState, "structures/{civ}/stable") ? "structures/{civ}/stable" : undefined;
let numStables = gameState.getOwnEntitiesByClass("Stable", true).length;
if (this.getAccountedPopulation(gameState) > this.Config.Military.popForBarracks1 ||
this.phasing == 2 && gameState.getOwnStructures().filter(API3.Filters.byClass("Village")).length < 5)
{
// first barracks/range and stables.
if (numBarracks + numRanges == 0)
{
let template = barracksTemplate || rangeTemplate;
if (template)
{
gameState.ai.queueManager.changePriority("militaryBuilding", 2 * this.Config.priorities.militaryBuilding);
let plan = new PETRA.ConstructionPlan(gameState, template, { "militaryBase": true });
plan.queueToReset = "militaryBuilding";
queues.militaryBuilding.addPlan(plan);
return;
}
}
if (numStables == 0 && stableTemplate)
{
queues.militaryBuilding.addPlan(new PETRA.ConstructionPlan(gameState, stableTemplate, { "militaryBase": true }));
return;
}
// Second range/barracks and stables
if (numBarracks + numRanges == 1 && this.getAccountedPopulation(gameState) > this.Config.Military.popForBarracks2)
{
let template = numBarracks == 0 ? (barracksTemplate || rangeTemplate) : (rangeTemplate || barracksTemplate);
if (template)
{
queues.militaryBuilding.addPlan(new PETRA.ConstructionPlan(gameState, template, { "militaryBase": true }));
return;
}
}
if (numStables == 1 && stableTemplate && this.getAccountedPopulation(gameState) > this.Config.Military.popForBarracks2)
{
queues.militaryBuilding.addPlan(new PETRA.ConstructionPlan(gameState, stableTemplate, { "militaryBase": true }));
return;
}
// Then 3rd barracks/range/stables if needed
if (numBarracks + numRanges + numStables == 2 && this.getAccountedPopulation(gameState) > this.Config.Military.popForBarracks2 + 30)
{
let template = barracksTemplate || stableTemplate || rangeTemplate;
if (template)
{
queues.militaryBuilding.addPlan(new PETRA.ConstructionPlan(gameState, template, { "militaryBase": true }));
return;
}
}
}
if (this.saveResources)
return;
if (this.currentPhase < 3)
return;
if (this.canBuild(gameState, "structures/{civ}/elephant_stables") && !gameState.getOwnEntitiesByClass("ElephantStable", true).hasEntities())
{
queues.militaryBuilding.addPlan(new PETRA.ConstructionPlan(gameState, "structures/{civ}/elephant_stables", { "militaryBase": true }));
return;
}
if (this.canBuild(gameState, "structures/{civ}/arsenal") && !gameState.getOwnEntitiesByClass("Arsenal", true).hasEntities())
{
queues.militaryBuilding.addPlan(new PETRA.ConstructionPlan(gameState, "structures/{civ}/arsenal", { "militaryBase": true }));
return;
}
if (this.getAccountedPopulation(gameState) < 80 || !this.bAdvanced.length)
return;
// Build advanced military buildings
let nAdvanced = 0;
for (let advanced of this.bAdvanced)
nAdvanced += gameState.countEntitiesAndQueuedByType(advanced, true);
if (!nAdvanced || nAdvanced < this.bAdvanced.length && this.getAccountedPopulation(gameState) > 110)
{
for (let advanced of this.bAdvanced)
{
if (gameState.countEntitiesAndQueuedByType(advanced, true) > 0 || !this.canBuild(gameState, advanced))
continue;
let template = gameState.getTemplate(advanced);
if (!template)
continue;
let civ = gameState.getPlayerCiv();
if (template.hasDefensiveFire() || template.trainableEntities(civ))
queues.militaryBuilding.addPlan(new PETRA.ConstructionPlan(gameState, advanced, { "militaryBase": true }));
else // not a military building, but still use this queue
queues.militaryBuilding.addPlan(new PETRA.ConstructionPlan(gameState, advanced));
return;
}
}
};
/**
* Find base nearest to ennemies for military buildings.
*/
PETRA.HQ.prototype.findBestBaseForMilitary = function(gameState)
{
let ccEnts = gameState.updatingGlobalCollection("allCCs", API3.Filters.byClass("CivCentre")).toEntityArray();
let bestBase;
let enemyFound = false;
let distMin = Math.min();
for (let cce of ccEnts)
{
if (gameState.isPlayerAlly(cce.owner()))
continue;
if (enemyFound && !gameState.isPlayerEnemy(cce.owner()))
continue;
let access = PETRA.getLandAccess(gameState, cce);
let isEnemy = gameState.isPlayerEnemy(cce.owner());
for (let cc of ccEnts)
{
if (cc.owner() != PlayerID)
continue;
if (PETRA.getLandAccess(gameState, cc) != access)
continue;
let dist = API3.SquareVectorDistance(cc.position(), cce.position());
if (!enemyFound && isEnemy)
enemyFound = true;
else if (dist > distMin)
continue;
bestBase = cc.getMetadata(PlayerID, "base");
distMin = dist;
}
}
return bestBase;
};
/**
* train with highest priority ranged infantry in the nearest civil center from a given set of positions
* and garrison them there for defense
*/
PETRA.HQ.prototype.trainEmergencyUnits = function(gameState, positions)
{
if (gameState.ai.queues.emergency.hasQueuedUnits())
return false;
let civ = gameState.getPlayerCiv();
// find nearest base anchor
let distcut = 20000;
let nearestAnchor;
let distmin;
for (let pos of positions)
{
let access = gameState.ai.accessibility.getAccessValue(pos);
// check nearest base anchor
for (let base of this.baseManagers)
{
if (!base.anchor || !base.anchor.position())
continue;
if (PETRA.getLandAccess(gameState, base.anchor) != access)
continue;
if (!base.anchor.trainableEntities(civ)) // base still in construction
continue;
let queue = base.anchor._entity.trainingQueue;
if (queue)
{
let time = 0;
for (let item of queue)
if (item.progress > 0 || item.metadata && item.metadata.garrisonType)
time += item.timeRemaining;
if (time/1000 > 5)
continue;
}
let dist = API3.SquareVectorDistance(base.anchor.position(), pos);
if (nearestAnchor && dist > distmin)
continue;
distmin = dist;
nearestAnchor = base.anchor;
}
}
if (!nearestAnchor || distmin > distcut)
return false;
// We will choose randomly ranged and melee units, except when garrisonHolder is full
// in which case we prefer melee units
let numGarrisoned = this.garrisonManager.numberOfGarrisonedSlots(nearestAnchor);
if (nearestAnchor._entity.trainingQueue)
{
for (let item of nearestAnchor._entity.trainingQueue)
{
if (item.metadata && item.metadata.garrisonType)
numGarrisoned += item.count;
else if (!item.progress && (!item.metadata || !item.metadata.trainer))
nearestAnchor.stopProduction(item.id);
}
}
let autogarrison = numGarrisoned < nearestAnchor.garrisonMax() &&
nearestAnchor.hitpoints() > nearestAnchor.garrisonEjectHealth() * nearestAnchor.maxHitpoints();
let rangedWanted = randBool() && autogarrison;
let total = gameState.getResources();
let templateFound;
let trainables = nearestAnchor.trainableEntities(civ);
let garrisonArrowClasses = nearestAnchor.getGarrisonArrowClasses();
for (let trainable of trainables)
{
if (gameState.isTemplateDisabled(trainable))
continue;
let template = gameState.getTemplate(trainable);
if (!template || !template.hasClass("Infantry") || !template.hasClass("CitizenSoldier"))
continue;
if (autogarrison && !MatchesClassList(template.classes(), garrisonArrowClasses))
continue;
if (!total.canAfford(new API3.Resources(template.cost())))
continue;
templateFound = [trainable, template];
if (template.hasClass("Ranged") == rangedWanted)
break;
}
if (!templateFound)
return false;
// Check first if we can afford it without touching the other accounts
// and if not, take some of other accounted resources
// TODO sort the queues to be substracted
let queueManager = gameState.ai.queueManager;
let cost = new API3.Resources(templateFound[1].cost());
queueManager.setAccounts(gameState, cost, "emergency");
if (!queueManager.canAfford("emergency", cost))
{
for (let q in queueManager.queues)
{
if (q == "emergency")
continue;
queueManager.transferAccounts(cost, q, "emergency");
if (queueManager.canAfford("emergency", cost))
break;
}
}
let metadata = { "role": "worker", "base": nearestAnchor.getMetadata(PlayerID, "base"), "plan": -1, "trainer": nearestAnchor.id() };
if (autogarrison)
metadata.garrisonType = "protection";
gameState.ai.queues.emergency.addPlan(new PETRA.TrainingPlan(gameState, templateFound[0], metadata, 1, 1));
return true;
};
PETRA.HQ.prototype.canBuild = function(gameState, structure)
{
let type = gameState.applyCiv(structure);
if (this.buildManager.isUnbuildable(gameState, type))
return false;
if (gameState.isTemplateDisabled(type))
{
this.buildManager.setUnbuildable(gameState, type, Infinity, "disabled");
return false;
}
let template = gameState.getTemplate(type);
if (!template)
{
this.buildManager.setUnbuildable(gameState, type, Infinity, "notemplate");
return false;
}
if (!template.available(gameState))
{
this.buildManager.setUnbuildable(gameState, type, 30, "tech");
return false;
}
if (!this.buildManager.hasBuilder(type))
{
this.buildManager.setUnbuildable(gameState, type, 120, "nobuilder");
return false;
}
if (this.numActiveBases() < 1)
{
// if no base, check that we can build outside our territory
let buildTerritories = template.buildTerritories();
if (buildTerritories && (!buildTerritories.length || buildTerritories.length == 1 && buildTerritories[0] == "own"))
{
this.buildManager.setUnbuildable(gameState, type, 180, "room");
return false;
}
}
// build limits
let limits = gameState.getEntityLimits();
let category = template.buildCategory();
if (category && limits[category] !== undefined && gameState.getEntityCounts()[category] >= limits[category])
{
this.buildManager.setUnbuildable(gameState, type, 90, "limit");
return false;
}
return true;
};
PETRA.HQ.prototype.updateTerritories = function(gameState)
{
const around = [ [-0.7, 0.7], [0, 1], [0.7, 0.7], [1, 0], [0.7, -0.7], [0, -1], [-0.7, -0.7], [-1, 0] ];
let alliedVictory = gameState.getAlliedVictory();
let passabilityMap = gameState.getPassabilityMap();
let width = this.territoryMap.width;
let cellSize = this.territoryMap.cellSize;
let insideSmall = Math.round(45 / cellSize);
let insideLarge = Math.round(80 / cellSize); // should be about the range of towers
let expansion = 0;
for (let j = 0; j < this.territoryMap.length; ++j)
{
if (this.borderMap.map[j] & PETRA.outside_Mask)
continue;
if (this.borderMap.map[j] & PETRA.fullFrontier_Mask)
this.borderMap.map[j] &= ~PETRA.fullFrontier_Mask; // reset the frontier
if (this.territoryMap.getOwnerIndex(j) != PlayerID)
{
// If this tile was already accounted, remove it
if (this.basesMap.map[j] == 0)
continue;
let base = this.getBaseByID(this.basesMap.map[j]);
if (base)
{
let index = base.territoryIndices.indexOf(j);
if (index != -1)
base.territoryIndices.splice(index, 1);
else
API3.warn(" problem in headquarters::updateTerritories for base " + this.basesMap.map[j]);
}
else
API3.warn(" problem in headquarters::updateTerritories without base " + this.basesMap.map[j]);
this.basesMap.map[j] = 0;
}
else
{
// Update the frontier
let ix = j%width;
let iz = Math.floor(j/width);
let onFrontier = false;
for (let a of around)
{
let jx = ix + Math.round(insideSmall*a[0]);
if (jx < 0 || jx >= width)
continue;
let jz = iz + Math.round(insideSmall*a[1]);
if (jz < 0 || jz >= width)
continue;
if (this.borderMap.map[jx+width*jz] & PETRA.outside_Mask)
continue;
let territoryOwner = this.territoryMap.getOwnerIndex(jx+width*jz);
if (territoryOwner != PlayerID && !(alliedVictory && gameState.isPlayerAlly(territoryOwner)))
{
this.borderMap.map[j] |= PETRA.narrowFrontier_Mask;
break;
}
jx = ix + Math.round(insideLarge*a[0]);
if (jx < 0 || jx >= width)
continue;
jz = iz + Math.round(insideLarge*a[1]);
if (jz < 0 || jz >= width)
continue;
if (this.borderMap.map[jx+width*jz] & PETRA.outside_Mask)
continue;
territoryOwner = this.territoryMap.getOwnerIndex(jx+width*jz);
if (territoryOwner != PlayerID && !(alliedVictory && gameState.isPlayerAlly(territoryOwner)))
onFrontier = true;
}
if (onFrontier && !(this.borderMap.map[j] & PETRA.narrowFrontier_Mask))
this.borderMap.map[j] |= PETRA.largeFrontier_Mask;
// If this tile was not already accounted, add it.
if (this.basesMap.map[j] != 0)
continue;
let landPassable = false;
let ind = API3.getMapIndices(j, this.territoryMap, passabilityMap);
let access;
for (let k of ind)
{
if (!this.landRegions[gameState.ai.accessibility.landPassMap[k]])
continue;
landPassable = true;
access = gameState.ai.accessibility.landPassMap[k];
break;
}
if (!landPassable)
continue;
let distmin = Math.min();
let baseID;
let pos = [cellSize * (j%width+0.5), cellSize * (Math.floor(j/width)+0.5)];
for (let base of this.baseManagers)
{
if (!base.anchor || !base.anchor.position())
continue;
if (base.accessIndex != access)
continue;
let dist = API3.SquareVectorDistance(base.anchor.position(), pos);
if (dist >= distmin)
continue;
distmin = dist;
baseID = base.ID;
}
if (!baseID)
continue;
this.getBaseByID(baseID).territoryIndices.push(j);
this.basesMap.map[j] = baseID;
expansion++;
}
}
if (!expansion)
return;
// We've increased our territory, so we may have some new room to build
this.buildManager.resetMissingRoom(gameState);
// And if sufficient expansion, check if building a new market would improve our present trade routes
let cellArea = this.territoryMap.cellSize * this.territoryMap.cellSize;
if (expansion * cellArea > 960)
this.tradeManager.routeProspection = true;
};
/** Reassign territories when a base is going to be deleted */
PETRA.HQ.prototype.reassignTerritories = function(deletedBase)
{
let cellSize = this.territoryMap.cellSize;
let width = this.territoryMap.width;
for (let j = 0; j < this.territoryMap.length; ++j)
{
if (this.basesMap.map[j] != deletedBase.ID)
continue;
if (this.territoryMap.getOwnerIndex(j) != PlayerID)
{
API3.warn("Petra reassignTerritories: should never happen");
this.basesMap.map[j] = 0;
continue;
}
let distmin = Math.min();
let baseID;
let pos = [cellSize * (j%width+0.5), cellSize * (Math.floor(j/width)+0.5)];
for (let base of this.baseManagers)
{
if (!base.anchor || !base.anchor.position())
continue;
if (base.accessIndex != deletedBase.accessIndex)
continue;
let dist = API3.SquareVectorDistance(base.anchor.position(), pos);
if (dist >= distmin)
continue;
distmin = dist;
baseID = base.ID;
}
if (baseID)
{
this.getBaseByID(baseID).territoryIndices.push(j);
this.basesMap.map[j] = baseID;
}
else
this.basesMap.map[j] = 0;
}
};
/**
* returns the base corresponding to baseID
*/
PETRA.HQ.prototype.getBaseByID = function(baseID)
{
for (let base of this.baseManagers)
if (base.ID == baseID)
return base;
return undefined;
};
/**
* returns the number of bases with a cc
* ActiveBases includes only those with a built cc
* PotentialBases includes also those with a cc in construction
*/
PETRA.HQ.prototype.numActiveBases = function()
{
if (!this.turnCache.base)
this.updateBaseCache();
return this.turnCache.base.active;
};
PETRA.HQ.prototype.numPotentialBases = function()
{
if (!this.turnCache.base)
this.updateBaseCache();
return this.turnCache.base.potential;
};
PETRA.HQ.prototype.updateBaseCache = function()
{
this.turnCache.base = { "active": 0, "potential": 0 };
for (let base of this.baseManagers)
{
if (!base.anchor)
continue;
++this.turnCache.base.potential;
if (base.anchor.foundationProgress() === undefined)
++this.turnCache.base.active;
}
};
PETRA.HQ.prototype.resetBaseCache = function()
{
this.turnCache.base = undefined;
};
/**
* Count gatherers returning resources in the number of gatherers of resourceSupplies
* to prevent the AI always reassigning idle workers to these resourceSupplies (specially in naval maps).
*/
PETRA.HQ.prototype.assignGatherers = function()
{
for (let base of this.baseManagers)
{
for (let worker of base.workers.values())
{
if (worker.unitAIState().split(".")[1] != "RETURNRESOURCE")
continue;
let orders = worker.unitAIOrderData();
if (orders.length < 2 || !orders[1].target || orders[1].target != worker.getMetadata(PlayerID, "supply"))
continue;
this.AddTCGatherer(orders[1].target);
}
}
};
PETRA.HQ.prototype.isDangerousLocation = function(gameState, pos, radius)
{
return this.isNearInvadingArmy(pos) || this.isUnderEnemyFire(gameState, pos, radius);
};
/** Check that the chosen position is not too near from an invading army */
PETRA.HQ.prototype.isNearInvadingArmy = function(pos)
{
for (let army of this.defenseManager.armies)
if (army.foePosition && API3.SquareVectorDistance(army.foePosition, pos) < 12000)
return true;
return false;
};
PETRA.HQ.prototype.isUnderEnemyFire = function(gameState, pos, radius = 0)
{
if (!this.turnCache.firingStructures)
this.turnCache.firingStructures = gameState.updatingCollection("diplo-FiringStructures", API3.Filters.hasDefensiveFire(), gameState.getEnemyStructures());
for (let ent of this.turnCache.firingStructures.values())
{
let range = radius + ent.attackRange("Ranged").max;
if (API3.SquareVectorDistance(ent.position(), pos) < range*range)
return true;
}
return false;
};
/** Compute the capture strength of all units attacking a capturable target */
PETRA.HQ.prototype.updateCaptureStrength = function(gameState)
{
this.capturableTargets.clear();
for (let ent of gameState.getOwnUnits().values())
{
if (!ent.canCapture())
continue;
let state = ent.unitAIState();
if (!state || !state.split(".")[1] || state.split(".")[1] != "COMBAT")
continue;
let orderData = ent.unitAIOrderData();
if (!orderData || !orderData.length || !orderData[0].target)
continue;
let targetId = orderData[0].target;
let target = gameState.getEntityById(targetId);
if (!target || !target.isCapturable() || !ent.canCapture(target))
continue;
if (!this.capturableTargets.has(targetId))
this.capturableTargets.set(targetId, {
"strength": ent.captureStrength() * PETRA.getAttackBonus(ent, target, "Capture"),
"ents": new Set([ent.id()])
});
else
{
let capturableTarget = this.capturableTargets.get(target.id());
capturableTarget.strength += ent.captureStrength() * PETRA.getAttackBonus(ent, target, "Capture");
capturableTarget.ents.add(ent.id());
}
}
for (let [targetId, capturableTarget] of this.capturableTargets)
{
let target = gameState.getEntityById(targetId);
let allowCapture;
for (let entId of capturableTarget.ents)
{
let ent = gameState.getEntityById(entId);
if (allowCapture === undefined)
allowCapture = PETRA.allowCapture(gameState, ent, target);
let orderData = ent.unitAIOrderData();
if (!orderData || !orderData.length || !orderData[0].attackType)
continue;
if ((orderData[0].attackType == "Capture") !== allowCapture)
ent.attack(targetId, allowCapture);
}
}
this.capturableTargetsTime = gameState.ai.elapsedTime;
};
/** Some functions that register that we assigned a gatherer to a resource this turn */
/** add a gatherer to the turn cache for this supply. */
PETRA.HQ.prototype.AddTCGatherer = function(supplyID)
{
if (this.turnCache.resourceGatherer && this.turnCache.resourceGatherer[supplyID] !== undefined)
++this.turnCache.resourceGatherer[supplyID];
else
{
if (!this.turnCache.resourceGatherer)
this.turnCache.resourceGatherer = {};
this.turnCache.resourceGatherer[supplyID] = 1;
}
};
/** remove a gatherer to the turn cache for this supply. */
PETRA.HQ.prototype.RemoveTCGatherer = function(supplyID)
{
if (this.turnCache.resourceGatherer && this.turnCache.resourceGatherer[supplyID])
--this.turnCache.resourceGatherer[supplyID];
else
{
if (!this.turnCache.resourceGatherer)
this.turnCache.resourceGatherer = {};
this.turnCache.resourceGatherer[supplyID] = -1;
}
};
PETRA.HQ.prototype.GetTCGatherer = function(supplyID)
{
if (this.turnCache.resourceGatherer && this.turnCache.resourceGatherer[supplyID])
return this.turnCache.resourceGatherer[supplyID];
return 0;
};
/** The next two are to register that we assigned a gatherer to a resource this turn. */
PETRA.HQ.prototype.AddTCResGatherer = function(resource)
{
if (this.turnCache["resourceGatherer-" + resource])
++this.turnCache["resourceGatherer-" + resource];
else
this.turnCache["resourceGatherer-" + resource] = 1;
if (this.turnCache.currentRates)
this.turnCache.currentRates[resource] += 0.5;
};
PETRA.HQ.prototype.GetTCResGatherer = function(resource)
{
if (this.turnCache["resourceGatherer-" + resource])
return this.turnCache["resourceGatherer-" + resource];
return 0;
};
/**
* flag a resource as exhausted
*/
PETRA.HQ.prototype.isResourceExhausted = function(resource)
{
if (this.turnCache["exhausted-" + resource] == undefined)
this.turnCache["exhausted-" + resource] = this.baseManagers.every(base =>
!base.dropsiteSupplies[resource].nearby.length &&
!base.dropsiteSupplies[resource].medium.length &&
!base.dropsiteSupplies[resource].faraway.length);
return this.turnCache["exhausted-" + resource];
};
/**
* Check if a structure in blinking territory should/can be defended (currently if it has some attacking armies around)
*/
PETRA.HQ.prototype.isDefendable = function(ent)
{
if (!this.turnCache.numAround)
this.turnCache.numAround = {};
if (this.turnCache.numAround[ent.id()] === undefined)
this.turnCache.numAround[ent.id()] = this.attackManager.numAttackingUnitsAround(ent.position(), 130);
return +this.turnCache.numAround[ent.id()] > 8;
};
/**
* Get the number of population already accounted for
*/
PETRA.HQ.prototype.getAccountedPopulation = function(gameState)
{
if (this.turnCache.accountedPopulation == undefined)
{
let pop = gameState.getPopulation();
for (let ent of gameState.getOwnTrainingFacilities().values())
{
for (let item of ent.trainingQueue())
{
if (!item.unitTemplate)
continue;
let unitPop = gameState.getTemplate(item.unitTemplate).get("Cost/Population");
if (unitPop)
pop += item.count * unitPop;
}
}
this.turnCache.accountedPopulation = pop;
}
return this.turnCache.accountedPopulation;
};
/**
* Get the number of workers already accounted for
*/
PETRA.HQ.prototype.getAccountedWorkers = function(gameState)
{
if (this.turnCache.accountedWorkers == undefined)
{
let workers = gameState.getOwnEntitiesByRole("worker", true).length;
for (let ent of gameState.getOwnTrainingFacilities().values())
{
for (let item of ent.trainingQueue())
{
if (!item.metadata || !item.metadata.role || item.metadata.role != "worker")
continue;
workers += item.count;
}
}
this.turnCache.accountedWorkers = workers;
}
return this.turnCache.accountedWorkers;
};
/**
* Some functions are run every turn
* Others once in a while
*/
PETRA.HQ.prototype.update = function(gameState, queues, events)
{
Engine.ProfileStart("Headquarters update");
this.turnCache = {};
this.territoryMap = PETRA.createTerritoryMap(gameState);
this.canBarter = gameState.getOwnEntitiesByClass("Market", true).filter(API3.Filters.isBuilt()).hasEntities();
// TODO find a better way to update
if (this.currentPhase != gameState.currentPhase())
{
if (this.Config.debug > 0)
API3.warn(" civ " + gameState.getPlayerCiv() + " has phasedUp from " + this.currentPhase +
" to " + gameState.currentPhase() + " at time " + gameState.ai.elapsedTime +
" phasing " + this.phasing);
this.currentPhase = gameState.currentPhase();
// In principle, this.phasing should be already reset to 0 when starting the research
// but this does not work in case of an autoResearch tech
if (this.phasing)
this.phasing = 0;
}
/*
if (this.Config.debug > 1)
{
gameState.getOwnUnits().forEach (function (ent) {
if (!ent.position())
return;
PETRA.dumpEntity(ent);
});
}
*/
this.checkEvents(gameState, events);
this.navalManager.checkEvents(gameState, queues, events);
if (this.phasing)
this.checkPhaseRequirements(gameState, queues);
else
this.researchManager.checkPhase(gameState, queues);
if (this.numActiveBases() > 0)
{
if (gameState.ai.playedTurn % 4 == 0)
this.trainMoreWorkers(gameState, queues);
if (gameState.ai.playedTurn % 4 == 1)
this.buildMoreHouses(gameState, queues);
if ((!this.saveResources || this.canBarter) && gameState.ai.playedTurn % 4 == 2)
this.buildFarmstead(gameState, queues);
if (this.needCorral && gameState.ai.playedTurn % 4 == 3)
this.manageCorral(gameState, queues);
if (gameState.ai.playedTurn % 5 == 1)
this.researchManager.update(gameState, queues);
}
if (this.numPotentialBases() < 1 ||
this.canExpand && gameState.ai.playedTurn % 10 == 7 && this.currentPhase > 1)
this.checkBaseExpansion(gameState, queues);
if (this.currentPhase > 1 && gameState.ai.playedTurn % 3 == 0)
{
if (!this.canBarter)
this.buildMarket(gameState, queues);
if (!this.saveResources)
{
this.buildForge(gameState, queues);
this.buildTemple(gameState, queues);
}
if (gameState.ai.playedTurn % 30 == 0 &&
gameState.getPopulation() > 0.9 * gameState.getPopulationMax())
this.buildWonder(gameState, queues, false);
}
this.tradeManager.update(gameState, events, queues);
this.garrisonManager.update(gameState, events);
this.defenseManager.update(gameState, events);
if (gameState.ai.playedTurn % 3 == 0)
{
this.constructTrainingBuildings(gameState, queues);
if (this.Config.difficulty > 0)
this.buildDefenses(gameState, queues);
}
this.assignGatherers();
let nbBases = this.baseManagers.length;
let activeBase; // We will loop only on 1 active base per turn
do
{
this.currentBase %= this.baseManagers.length;
activeBase = this.baseManagers[this.currentBase++].update(gameState, queues, events);
--nbBases;
// TODO what to do with this.reassignTerritories(this.baseManagers[this.currentBase]);
}
while (!activeBase && nbBases != 0);
this.navalManager.update(gameState, queues, events);
if (this.Config.difficulty > 0 && (this.numActiveBases() > 0 || !this.canBuildUnits))
this.attackManager.update(gameState, queues, events);
this.diplomacyManager.update(gameState, events);
this.victoryManager.update(gameState, events, queues);
// We update the capture strength at the end as it can change attack orders
if (gameState.ai.elapsedTime - this.capturableTargetsTime > 3)
this.updateCaptureStrength(gameState);
Engine.ProfileStop();
};
PETRA.HQ.prototype.Serialize = function()
{
let properties = {
"phasing": this.phasing,
"currentBase": this.currentBase,
"lastFailedGather": this.lastFailedGather,
"firstBaseConfig": this.firstBaseConfig,
"supportRatio": this.supportRatio,
"targetNumWorkers": this.targetNumWorkers,
"fortStartTime": this.fortStartTime,
"towerStartTime": this.towerStartTime,
"fortressStartTime": this.fortressStartTime,
"bAdvanced": this.bAdvanced,
"saveResources": this.saveResources,
"saveSpace": this.saveSpace,
"needCorral": this.needCorral,
"needFarm": this.needFarm,
"needFish": this.needFish,
"maxFields": this.maxFields,
"canExpand": this.canExpand,
"canBuildUnits": this.canBuildUnits,
"navalMap": this.navalMap,
"landRegions": this.landRegions,
"navalRegions": this.navalRegions,
"decayingStructures": this.decayingStructures,
"capturableTargets": this.capturableTargets,
"capturableTargetsTime": this.capturableTargetsTime
};
let baseManagers = [];
for (let base of this.baseManagers)
baseManagers.push(base.Serialize());
if (this.Config.debug == -100)
{
API3.warn(" HQ serialization ---------------------");
API3.warn(" properties " + uneval(properties));
API3.warn(" baseManagers " + uneval(baseManagers));
API3.warn(" attackManager " + uneval(this.attackManager.Serialize()));
API3.warn(" buildManager " + uneval(this.buildManager.Serialize()));
API3.warn(" defenseManager " + uneval(this.defenseManager.Serialize()));
API3.warn(" tradeManager " + uneval(this.tradeManager.Serialize()));
API3.warn(" navalManager " + uneval(this.navalManager.Serialize()));
API3.warn(" researchManager " + uneval(this.researchManager.Serialize()));
API3.warn(" diplomacyManager " + uneval(this.diplomacyManager.Serialize()));
API3.warn(" garrisonManager " + uneval(this.garrisonManager.Serialize()));
API3.warn(" victoryManager " + uneval(this.victoryManager.Serialize()));
}
return {
"properties": properties,
"baseManagers": baseManagers,
"attackManager": this.attackManager.Serialize(),
"buildManager": this.buildManager.Serialize(),
"defenseManager": this.defenseManager.Serialize(),
"tradeManager": this.tradeManager.Serialize(),
"navalManager": this.navalManager.Serialize(),
"researchManager": this.researchManager.Serialize(),
"diplomacyManager": this.diplomacyManager.Serialize(),
"garrisonManager": this.garrisonManager.Serialize(),
"victoryManager": this.victoryManager.Serialize(),
};
};
PETRA.HQ.prototype.Deserialize = function(gameState, data)
{
for (let key in data.properties)
this[key] = data.properties[key];
this.baseManagers = [];
for (let base of data.baseManagers)
{
// the first call to deserialize set the ID base needed by entitycollections
let newbase = new PETRA.BaseManager(gameState, this.Config);
newbase.Deserialize(gameState, base);
newbase.init(gameState);
newbase.Deserialize(gameState, base);
this.baseManagers.push(newbase);
}
this.navalManager = new PETRA.NavalManager(this.Config);
this.navalManager.init(gameState, true);
this.navalManager.Deserialize(gameState, data.navalManager);
this.attackManager = new PETRA.AttackManager(this.Config);
this.attackManager.Deserialize(gameState, data.attackManager);
this.attackManager.init(gameState);
this.attackManager.Deserialize(gameState, data.attackManager);
this.buildManager = new PETRA.BuildManager();
this.buildManager.Deserialize(data.buildManager);
this.defenseManager = new PETRA.DefenseManager(this.Config);
this.defenseManager.Deserialize(gameState, data.defenseManager);
this.tradeManager = new PETRA.TradeManager(this.Config);
this.tradeManager.init(gameState);
this.tradeManager.Deserialize(gameState, data.tradeManager);
this.researchManager = new PETRA.ResearchManager(this.Config);
this.researchManager.Deserialize(data.researchManager);
this.diplomacyManager = new PETRA.DiplomacyManager(this.Config);
this.diplomacyManager.Deserialize(data.diplomacyManager);
this.garrisonManager = new PETRA.GarrisonManager(this.Config);
this.garrisonManager.Deserialize(data.garrisonManager);
this.victoryManager = new PETRA.VictoryManager(this.Config);
this.victoryManager.Deserialize(data.victoryManager);
};
Index: ps/trunk/binaries/data/mods/public/simulation/ai/petra/startingStrategy.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/ai/petra/startingStrategy.js (revision 24988)
+++ ps/trunk/binaries/data/mods/public/simulation/ai/petra/startingStrategy.js (revision 24989)
@@ -1,573 +1,573 @@
/**
* Determines the strategy to adopt when starting a new game,
* depending on the initial conditions
*/
PETRA.HQ.prototype.gameAnalysis = function(gameState)
{
// Analysis of the terrain and the different access regions
if (!this.regionAnalysis(gameState))
return;
this.attackManager.init(gameState);
this.buildManager.init(gameState);
this.navalManager.init(gameState);
this.tradeManager.init(gameState);
this.diplomacyManager.init(gameState);
// Make a list of buildable structures from the config file
this.structureAnalysis(gameState);
// Let's get our initial situation here.
let nobase = new PETRA.BaseManager(gameState, this.Config);
nobase.init(gameState);
nobase.accessIndex = 0;
this.baseManagers.push(nobase); // baseManagers[0] will deal with unit/structure without base
let ccEnts = gameState.getOwnStructures().filter(API3.Filters.byClass("CivCentre"));
for (let cc of ccEnts.values())
if (cc.foundationProgress() === undefined)
this.createBase(gameState, cc);
else
this.createBase(gameState, cc, "unconstructed");
this.updateTerritories(gameState);
// Assign entities and resources in the different bases
this.assignStartingEntities(gameState);
// Sandbox difficulty should not try to expand
this.canExpand = this.Config.difficulty != 0;
// If no base yet, check if we can construct one. If not, dispatch our units to possible tasks/attacks
this.canBuildUnits = true;
if (!gameState.getOwnStructures().filter(API3.Filters.byClass("CivCentre")).hasEntities())
{
let template = gameState.applyCiv("structures/{civ}/civil_centre");
if (!gameState.isTemplateAvailable(template) || !gameState.getTemplate(template).available(gameState))
{
if (this.Config.debug > 1)
API3.warn(" this AI is unable to produce any units");
this.canBuildUnits = false;
this.dispatchUnits(gameState);
}
else
this.buildFirstBase(gameState);
}
// configure our first base strategy
if (this.baseManagers.length > 1)
this.configFirstBase(gameState);
};
/**
* Assign the starting entities to the different bases
*/
PETRA.HQ.prototype.assignStartingEntities = function(gameState)
{
for (let ent of gameState.getOwnEntities().values())
{
// do not affect merchant ship immediately to trade as they may-be useful for transport
if (ent.hasClass("Trader") && !ent.hasClass("Ship"))
this.tradeManager.assignTrader(ent);
let pos = ent.position();
if (!pos)
{
// TODO should support recursive garrisoning. Make a warning for now
if (ent.isGarrisonHolder() && ent.garrisoned().length)
API3.warn("Petra warning: support for garrisoned units inside garrisoned holders not yet implemented");
continue;
}
// make sure we have not rejected small regions with units (TODO should probably also check with other non-gaia units)
let gamepos = gameState.ai.accessibility.gamePosToMapPos(pos);
let index = gamepos[0] + gamepos[1]*gameState.ai.accessibility.width;
let land = gameState.ai.accessibility.landPassMap[index];
if (land > 1 && !this.landRegions[land])
this.landRegions[land] = true;
let sea = gameState.ai.accessibility.navalPassMap[index];
if (sea > 1 && !this.navalRegions[sea])
this.navalRegions[sea] = true;
// if garrisoned units inside, ungarrison them except if a ship in which case we will make a transport
// when a construction will start (see createTransportIfNeeded)
if (ent.isGarrisonHolder() && ent.garrisoned().length && !ent.hasClass("Ship"))
for (let id of ent.garrisoned())
ent.unload(id);
let bestbase;
let territorypos = this.territoryMap.gamePosToMapPos(pos);
let territoryIndex = territorypos[0] + territorypos[1]*this.territoryMap.width;
for (let i = 1; i < this.baseManagers.length; ++i)
{
let base = this.baseManagers[i];
if ((!ent.getMetadata(PlayerID, "base") || ent.getMetadata(PlayerID, "base") != base.ID) &&
base.territoryIndices.indexOf(territoryIndex) == -1)
continue;
base.assignEntity(gameState, ent);
bestbase = base;
break;
}
if (!bestbase) // entity outside our territory
{
if (ent.hasClass("Structure") && !ent.decaying() && ent.resourceDropsiteTypes())
bestbase = this.createBase(gameState, ent, "anchorless");
else
bestbase = PETRA.getBestBase(gameState, ent) || this.baseManagers[0];
bestbase.assignEntity(gameState, ent);
}
// now assign entities garrisoned inside this entity
if (ent.isGarrisonHolder() && ent.garrisoned().length)
for (let id of ent.garrisoned())
bestbase.assignEntity(gameState, gameState.getEntityById(id));
// and find something useful to do if we already have a base
if (pos && bestbase.ID !== this.baseManagers[0].ID)
{
bestbase.assignRolelessUnits(gameState, [ent]);
if (ent.getMetadata(PlayerID, "role") === "worker")
{
bestbase.reassignIdleWorkers(gameState, [ent]);
bestbase.workerObject.update(gameState, ent);
}
}
}
};
/**
* determine the main land Index (or water index if none)
* as well as the list of allowed (land andf water) regions
*/
PETRA.HQ.prototype.regionAnalysis = function(gameState)
{
let accessibility = gameState.ai.accessibility;
let landIndex;
let seaIndex;
let ccEnts = gameState.getOwnStructures().filter(API3.Filters.byClass("CivCentre"));
for (let cc of ccEnts.values())
{
let land = accessibility.getAccessValue(cc.position());
if (land > 1)
{
landIndex = land;
break;
}
}
if (!landIndex)
{
let civ = gameState.getPlayerCiv();
for (let ent of gameState.getOwnEntities().values())
{
if (!ent.position() || !ent.hasClass("Unit") && !ent.trainableEntities(civ))
continue;
let land = accessibility.getAccessValue(ent.position());
if (land > 1)
{
landIndex = land;
break;
}
let sea = accessibility.getAccessValue(ent.position(), true);
if (!seaIndex && sea > 1)
seaIndex = sea;
}
}
if (!landIndex && !seaIndex)
{
API3.warn("Petra error: it does not know how to interpret this map");
return false;
}
let passabilityMap = gameState.getPassabilityMap();
let totalSize = passabilityMap.width * passabilityMap.width;
let minLandSize = Math.floor(0.1*totalSize);
let minWaterSize = Math.floor(0.2*totalSize);
let cellArea = passabilityMap.cellSize * passabilityMap.cellSize;
for (let i = 0; i < accessibility.regionSize.length; ++i)
{
if (landIndex && i == landIndex)
this.landRegions[i] = true;
else if (accessibility.regionType[i] === "land" && cellArea*accessibility.regionSize[i] > 320)
{
if (landIndex)
{
let sea = this.getSeaBetweenIndices(gameState, landIndex, i);
if (sea && (accessibility.regionSize[i] > minLandSize || accessibility.regionSize[sea] > minWaterSize))
{
this.navalMap = true;
this.landRegions[i] = true;
this.navalRegions[sea] = true;
}
}
else
{
let traject = accessibility.getTrajectToIndex(seaIndex, i);
if (traject && traject.length === 2)
{
this.navalMap = true;
this.landRegions[i] = true;
this.navalRegions[seaIndex] = true;
}
}
}
else if (accessibility.regionType[i] === "water" && accessibility.regionSize[i] > minWaterSize)
{
this.navalMap = true;
this.navalRegions[i] = true;
}
else if (accessibility.regionType[i] === "water" && cellArea*accessibility.regionSize[i] > 3600)
this.navalRegions[i] = true;
}
if (this.Config.debug < 3)
return true;
for (let region in this.landRegions)
API3.warn(" >>> zone " + region + " taille " + cellArea*gameState.ai.accessibility.regionSize[region]);
API3.warn(" navalMap " + this.navalMap);
API3.warn(" landRegions " + uneval(this.landRegions));
API3.warn(" navalRegions " + uneval(this.navalRegions));
return true;
};
/**
* load units and buildings from the config files
* TODO: change that to something dynamic
*/
PETRA.HQ.prototype.structureAnalysis = function(gameState)
{
let civref = gameState.playerData.civ;
let civ = civref in this.Config.buildings ? civref : 'default';
this.bAdvanced = [];
for (let building of this.Config.buildings[civ])
if (gameState.isTemplateAvailable(gameState.applyCiv(building)))
this.bAdvanced.push(gameState.applyCiv(building));
};
/**
* build our first base
* if not enough resource, try first to do a dock
*/
PETRA.HQ.prototype.buildFirstBase = function(gameState)
{
if (gameState.ai.queues.civilCentre.hasQueuedUnits())
return;
let templateName = gameState.applyCiv("structures/{civ}/civil_centre");
if (gameState.isTemplateDisabled(templateName))
return;
let template = gameState.getTemplate(templateName);
if (!template)
return;
let total = gameState.getResources();
let goal = "civil_centre";
if (!total.canAfford(new API3.Resources(template.cost())))
{
let totalExpected = gameState.getResources();
// Check for treasures around available in some maps at startup
for (let ent of gameState.getOwnUnits().values())
{
if (!ent.position())
continue;
// If we can get a treasure around, just do it
if (ent.isIdle())
PETRA.gatherTreasure(gameState, ent);
// Then count the resources from the treasures being collected
- let supplyId = ent.getMetadata(PlayerID, "supply");
- if (!supplyId)
+ let treasureId = ent.getMetadata(PlayerID, "treasure");
+ if (!treasureId)
continue;
- let supply = gameState.getEntityById(supplyId);
- if (!supply || supply.resourceSupplyType().generic != "treasure")
+ let treasure = gameState.getEntityById(treasureId);
+ if (!treasure)
continue;
- let type = supply.resourceSupplyType().specific;
- if (!(type in totalExpected))
- continue;
- totalExpected[type] += supply.resourceSupplyMax();
- // If we can collect enough resources from these treasures, wait for them
+ let types = treasure.treasureResources();
+ for (let type in types)
+ if (type in totalExpected)
+ totalExpected[type] += types[type];
+ // If we can collect enough resources from these treasures, wait for them.
if (totalExpected.canAfford(new API3.Resources(template.cost())))
return;
}
// not enough resource to build a cc, try with a dock to accumulate resources if none yet
if (!this.navalManager.docks.filter(API3.Filters.byClass("Dock")).hasEntities())
{
if (gameState.ai.queues.dock.hasQueuedUnits())
return;
templateName = gameState.applyCiv("structures/{civ}/dock");
if (gameState.isTemplateDisabled(templateName))
return;
template = gameState.getTemplate(templateName);
if (!template || !total.canAfford(new API3.Resources(template.cost())))
return;
goal = "dock";
}
}
if (!this.canBuild(gameState, templateName))
return;
// We first choose as startingPoint the point where we have the more units
let startingPoint = [];
for (let ent of gameState.getOwnUnits().values())
{
if (!ent.hasClass("Worker"))
continue;
if (PETRA.isFastMoving(ent))
continue;
let pos = ent.position();
if (!pos)
{
let holder = PETRA.getHolder(gameState, ent);
if (!holder || !holder.position())
continue;
pos = holder.position();
}
let gamepos = gameState.ai.accessibility.gamePosToMapPos(pos);
let index = gamepos[0] + gamepos[1] * gameState.ai.accessibility.width;
let land = gameState.ai.accessibility.landPassMap[index];
let sea = gameState.ai.accessibility.navalPassMap[index];
let found = false;
for (let point of startingPoint)
{
if (land !== point.land || sea !== point.sea)
continue;
if (API3.SquareVectorDistance(point.pos, pos) > 2500)
continue;
point.weight += 1;
found = true;
break;
}
if (!found)
startingPoint.push({ "pos": pos, "land": land, "sea": sea, "weight": 1 });
}
if (!startingPoint.length)
return;
let imax = 0;
for (let i = 1; i < startingPoint.length; ++i)
if (startingPoint[i].weight > startingPoint[imax].weight)
imax = i;
if (goal == "dock")
{
let sea = startingPoint[imax].sea > 1 ? startingPoint[imax].sea : undefined;
gameState.ai.queues.dock.addPlan(new PETRA.ConstructionPlan(gameState, "structures/{civ}/dock", { "sea": sea, "proximity": startingPoint[imax].pos }));
}
else
gameState.ai.queues.civilCentre.addPlan(new PETRA.ConstructionPlan(gameState, "structures/{civ}/civil_centre", { "base": -1, "resource": "wood", "proximity": startingPoint[imax].pos }));
};
/**
* set strategy if game without construction:
* - if one of our allies has a cc, affect a small fraction of our army for his defense, the rest will attack
* - otherwise all units will attack
*/
PETRA.HQ.prototype.dispatchUnits = function(gameState)
{
let allycc = gameState.getExclusiveAllyEntities().filter(API3.Filters.byClass("CivCentre")).toEntityArray();
if (allycc.length)
{
if (this.Config.debug > 1)
API3.warn(" We have allied cc " + allycc.length + " and " + gameState.getOwnUnits().length + " units ");
let units = gameState.getOwnUnits();
let num = Math.max(Math.min(Math.round(0.08*(1+this.Config.personality.cooperative)*units.length), 20), 5);
let num1 = Math.floor(num / 2);
let num2 = num1;
// first pass to affect ranged infantry
units.filter(API3.Filters.byClassesAnd(["Infantry", "Ranged"])).forEach(ent => {
if (!num || !num1)
return;
if (ent.getMetadata(PlayerID, "allied"))
return;
let access = PETRA.getLandAccess(gameState, ent);
for (let cc of allycc)
{
if (!cc.position() || PETRA.getLandAccess(gameState, cc) != access)
continue;
--num;
--num1;
ent.setMetadata(PlayerID, "allied", true);
let range = 1.5 * cc.footprintRadius();
ent.moveToRange(cc.position()[0], cc.position()[1], range, range + 5);
break;
}
});
// second pass to affect melee infantry
units.filter(API3.Filters.byClassesAnd(["Infantry", "Melee"])).forEach(ent => {
if (!num || !num2)
return;
if (ent.getMetadata(PlayerID, "allied"))
return;
let access = PETRA.getLandAccess(gameState, ent);
for (let cc of allycc)
{
if (!cc.position() || PETRA.getLandAccess(gameState, cc) != access)
continue;
--num;
--num2;
ent.setMetadata(PlayerID, "allied", true);
let range = 1.5 * cc.footprintRadius();
ent.moveToRange(cc.position()[0], cc.position()[1], range, range + 5);
break;
}
});
// and now complete the affectation, including all support units
units.forEach(ent => {
if (!num && !ent.hasClass("Support"))
return;
if (ent.getMetadata(PlayerID, "allied"))
return;
let access = PETRA.getLandAccess(gameState, ent);
for (let cc of allycc)
{
if (!cc.position() || PETRA.getLandAccess(gameState, cc) != access)
continue;
if (!ent.hasClass("Support"))
--num;
ent.setMetadata(PlayerID, "allied", true);
let range = 1.5 * cc.footprintRadius();
ent.moveToRange(cc.position()[0], cc.position()[1], range, range + 5);
break;
}
});
}
};
/**
* configure our first base expansion
* - if on a small island, favor fishing
* - count the available wood resource, and allow rushes only if enough (we should otherwise favor expansion)
*/
PETRA.HQ.prototype.configFirstBase = function(gameState)
{
if (this.baseManagers.length < 2)
return;
this.firstBaseConfig = true;
let startingSize = 0;
let startingLand = [];
for (let region in this.landRegions)
{
for (let base of this.baseManagers)
{
if (!base.anchor || base.accessIndex != +region)
continue;
startingSize += gameState.ai.accessibility.regionSize[region];
startingLand.push(base.accessIndex);
break;
}
}
let cell = gameState.getPassabilityMap().cellSize;
startingSize = startingSize * cell * cell;
if (this.Config.debug > 1)
API3.warn("starting size " + startingSize + "(cut at 24000 for fish pushing)");
if (startingSize < 25000)
{
this.saveSpace = true;
this.Config.Economy.popForDock = Math.min(this.Config.Economy.popForDock, 16);
let num = Math.max(this.Config.Economy.targetNumFishers, 2);
for (let land of startingLand)
{
for (let sea of gameState.ai.accessibility.regionLinks[land])
if (gameState.ai.HQ.navalRegions[sea])
this.navalManager.updateFishingBoats(sea, num);
}
this.maxFields = 1;
this.needCorral = true;
}
else if (startingSize < 60000)
this.maxFields = 2;
else
this.maxFields = false;
// - count the available food resource, and react accordingly
let startingFood = gameState.getResources().food;
let check = {};
for (let proxim of ["nearby", "medium", "faraway"])
{
for (let base of this.baseManagers)
{
for (let supply of base.dropsiteSupplies.food[proxim])
{
if (check[supply.id]) // avoid double counting as same resource can appear several time
continue;
check[supply.id] = true;
startingFood += supply.ent.resourceSupplyAmount();
}
}
}
if (startingFood < 800)
{
if (startingSize < 25000)
{
this.needFish = true;
this.Config.Economy.popForDock = 1;
}
else
this.needFarm = true;
}
// - count the available wood resource, and allow rushes only if enough (we should otherwise favor expansion)
let startingWood = gameState.getResources().wood;
check = {};
for (let proxim of ["nearby", "medium", "faraway"])
{
for (let base of this.baseManagers)
{
for (let supply of base.dropsiteSupplies.wood[proxim])
{
if (check[supply.id]) // avoid double counting as same resource can appear several time
continue;
check[supply.id] = true;
startingWood += supply.ent.resourceSupplyAmount();
}
}
}
if (this.Config.debug > 1)
API3.warn("startingWood: " + startingWood + " (cut at 8500 for no rush and 6000 for saveResources)");
if (startingWood < 6000)
{
this.saveResources = true;
this.Config.Economy.popPhase2 = Math.floor(0.75 * this.Config.Economy.popPhase2); // Switch to town phase sooner to be able to expand
if (startingWood < 2000 && this.needFarm)
{
this.needCorral = true;
this.needFarm = false;
}
}
if (startingWood > 8500 && this.canBuildUnits)
{
let allowed = Math.ceil((startingWood - 8500) / 3000);
// Not useful to prepare rushing if too long ceasefire
if (gameState.isCeasefireActive())
{
if (gameState.ceasefireTimeRemaining > 900)
allowed = 0;
else if (gameState.ceasefireTimeRemaining > 600 && allowed > 1)
allowed = 1;
}
this.attackManager.setRushes(allowed);
}
// immediatly build a wood dropsite if possible.
let template = gameState.applyCiv("structures/{civ}/storehouse");
if (!gameState.getOwnEntitiesByClass("Storehouse", true).hasEntities() && this.canBuild(gameState, template))
{
let newDP = this.baseManagers[1].findBestDropsiteLocation(gameState, "wood");
if (newDP.quality > 40)
{
// if we start with enough workers, put our available resources in this first dropsite
// same thing if our pop exceed the allowed one, as we will need several houses
let numWorkers = gameState.getOwnUnits().filter(API3.Filters.byClass("Worker")).length;
if (numWorkers > 12 && newDP.quality > 60 ||
gameState.getPopulation() > gameState.getPopulationLimit() + 20)
{
let cost = new API3.Resources(gameState.getTemplate(template).cost());
gameState.ai.queueManager.setAccounts(gameState, cost, "dropsites");
}
gameState.ai.queues.dropsites.addPlan(new PETRA.ConstructionPlan(gameState, template, { "base": this.baseManagers[1].ID }, newDP.pos));
}
}
// and build immediately a corral if needed
if (this.needCorral)
{
template = gameState.applyCiv("structures/{civ}/corral");
if (!gameState.getOwnEntitiesByClass("Corral", true).hasEntities() && this.canBuild(gameState, template))
gameState.ai.queues.corral.addPlan(new PETRA.ConstructionPlan(gameState, template, { "base": this.baseManagers[1].ID }));
}
};
Index: ps/trunk/binaries/data/mods/public/simulation/ai/petra/worker.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/ai/petra/worker.js (revision 24988)
+++ ps/trunk/binaries/data/mods/public/simulation/ai/petra/worker.js (revision 24989)
@@ -1,1104 +1,1103 @@
/**
* This class makes a worker do as instructed by the economy manager
*/
PETRA.Worker = function(base)
{
this.ent = undefined;
this.base = base;
this.baseID = base.ID;
};
PETRA.Worker.prototype.update = function(gameState, ent)
{
if (!ent.position() || ent.getMetadata(PlayerID, "plan") == -2 || ent.getMetadata(PlayerID, "plan") == -3)
return;
let subrole = ent.getMetadata(PlayerID, "subrole");
// If we are waiting for a transport or we are sailing, just wait
if (ent.getMetadata(PlayerID, "transport") !== undefined)
{
// Except if builder with their foundation destroyed, in which case cancel the transport if not yet on board
if (subrole == "builder" && ent.getMetadata(PlayerID, "target-foundation") !== undefined)
{
let plan = gameState.ai.HQ.navalManager.getPlan(ent.getMetadata(PlayerID, "transport"));
let target = gameState.getEntityById(ent.getMetadata(PlayerID, "target-foundation"));
if (!target && plan && plan.state == "boarding" && ent.position())
plan.removeUnit(gameState, ent);
}
// and gatherer if there are no more dropsite accessible in the base the ent is going to
if (subrole == "gatherer" || subrole == "hunter")
{
let plan = gameState.ai.HQ.navalManager.getPlan(ent.getMetadata(PlayerID, "transport"));
if (plan.state == "boarding" && ent.position())
{
let hasDropsite = false;
let gatherType = ent.getMetadata(PlayerID, "gather-type") || "food";
for (let structure of gameState.getOwnStructures().values())
{
if (PETRA.getLandAccess(gameState, structure) != plan.endIndex)
continue;
let resourceDropsiteTypes = PETRA.getBuiltEntity(gameState, structure).resourceDropsiteTypes();
if (!resourceDropsiteTypes || resourceDropsiteTypes.indexOf(gatherType) == -1)
continue;
hasDropsite = true;
break;
}
if (!hasDropsite)
{
for (let unit of gameState.getOwnUnits().filter(API3.Filters.byClass("Support")).values())
{
if (!unit.position() || PETRA.getLandAccess(gameState, unit) != plan.endIndex)
continue;
let resourceDropsiteTypes = unit.resourceDropsiteTypes();
if (!resourceDropsiteTypes || resourceDropsiteTypes.indexOf(gatherType) == -1)
continue;
hasDropsite = true;
break;
}
}
if (!hasDropsite)
plan.removeUnit(gameState, ent);
}
}
if (ent.getMetadata(PlayerID, "transport") !== undefined)
return;
}
this.entAccess = PETRA.getLandAccess(gameState, ent);
// base 0 for unassigned entities has no accessIndex, so take the one from the entity
if (this.baseID == gameState.ai.HQ.baseManagers[0].ID)
this.baseAccess = this.entAccess;
else
this.baseAccess = this.base.accessIndex;
if (!subrole) // subrole may-be undefined after a transport, garrisoning, army, ...
{
ent.setMetadata(PlayerID, "subrole", "idle");
this.base.reassignIdleWorkers(gameState, [ent]);
this.update(gameState, ent);
return;
}
this.ent = ent;
let unitAIState = ent.unitAIState();
if ((subrole == "hunter" || subrole == "gatherer") &&
(unitAIState == "INDIVIDUAL.GATHER.GATHERING" || unitAIState == "INDIVIDUAL.GATHER.APPROACHING" ||
unitAIState == "INDIVIDUAL.COMBAT.APPROACHING"))
{
if (this.isInaccessibleSupply(gameState))
{
if (this.retryWorking(gameState, subrole))
return;
ent.stopMoving();
}
if (unitAIState == "INDIVIDUAL.COMBAT.APPROACHING" && ent.unitAIOrderData().length)
{
let orderData = ent.unitAIOrderData()[0];
if (orderData && orderData.target)
{
// Check that we have not drifted too far when hunting
let target = gameState.getEntityById(orderData.target);
if (target && target.resourceSupplyType() && target.resourceSupplyType().generic == "food")
{
let territoryOwner = gameState.ai.HQ.territoryMap.getOwner(target.position());
if (gameState.isPlayerEnemy(territoryOwner))
{
if (this.retryWorking(gameState, subrole))
return;
ent.stopMoving();
}
else if (!gameState.isPlayerAlly(territoryOwner))
{
let distanceSquare = PETRA.isFastMoving(ent) ? 90000 : 30000;
let targetAccess = PETRA.getLandAccess(gameState, target);
let foodDropsites = gameState.playerData.hasSharedDropsites ?
gameState.getAnyDropsites("food") : gameState.getOwnDropsites("food");
let hasFoodDropsiteWithinDistance = false;
for (let dropsite of foodDropsites.values())
{
if (!dropsite.position())
continue;
let owner = dropsite.owner();
// owner != PlayerID can only happen when hasSharedDropsites == true, so no need to test it again
if (owner != PlayerID && (!dropsite.isSharedDropsite() || !gameState.isPlayerMutualAlly(owner)))
continue;
if (targetAccess != PETRA.getLandAccess(gameState, dropsite))
continue;
if (API3.SquareVectorDistance(target.position(), dropsite.position()) < distanceSquare)
{
hasFoodDropsiteWithinDistance = true;
break;
}
}
if (!hasFoodDropsiteWithinDistance)
{
if (this.retryWorking(gameState, subrole))
return;
ent.stopMoving();
}
}
}
}
}
}
else if (ent.getMetadata(PlayerID, "approachingTarget"))
{
ent.setMetadata(PlayerID, "approachingTarget", undefined);
ent.setMetadata(PlayerID, "alreadyTried", undefined);
}
let unitAIStateOrder = unitAIState.split(".")[1];
// If we're fighting or hunting, let's not start gathering except if inaccessible target
// but for fishers where UnitAI must have made us target a moving whale.
// Also, if we are attacking, do not capture
if (unitAIStateOrder == "COMBAT")
{
if (subrole == "fisher")
this.startFishing(gameState);
else if (unitAIState == "INDIVIDUAL.COMBAT.APPROACHING" && ent.unitAIOrderData().length &&
!ent.getMetadata(PlayerID, "PartOfArmy"))
{
let orderData = ent.unitAIOrderData()[0];
if (orderData && orderData.target)
{
let target = gameState.getEntityById(orderData.target);
if (target && (!target.position() || PETRA.getLandAccess(gameState, target) != this.entAccess))
{
if (this.retryWorking(gameState, subrole))
return;
ent.stopMoving();
}
}
}
else if (unitAIState == "INDIVIDUAL.COMBAT.ATTACKING" && ent.unitAIOrderData().length &&
!ent.getMetadata(PlayerID, "PartOfArmy"))
{
let orderData = ent.unitAIOrderData()[0];
if (orderData && orderData.target && orderData.attackType && orderData.attackType == "Capture")
{
// If we are here, an enemy structure must have targeted one of our workers
// and UnitAI sent it fight back with allowCapture=true
let target = gameState.getEntityById(orderData.target);
if (target && target.owner() > 0 && !gameState.isPlayerAlly(target.owner()))
ent.attack(orderData.target, PETRA.allowCapture(gameState, ent, target));
}
}
return;
}
// Okay so we have a few tasks.
// If we're gathering, we'll check that we haven't run idle.
// And we'll also check that we're gathering a resource we want to gather.
if (subrole == "gatherer")
{
if (ent.isIdle())
{
// if we aren't storing resources or it's the same type as what we're about to gather,
// let's just pick a new resource.
// TODO if we already carry the max we can -> returnresources
if (!ent.resourceCarrying() || !ent.resourceCarrying().length ||
ent.resourceCarrying()[0].type == ent.getMetadata(PlayerID, "gather-type"))
{
this.startGathering(gameState);
}
else if (!PETRA.returnResources(gameState, ent)) // try to deposit resources
{
// no dropsite, abandon old resources and start gathering new ones
this.startGathering(gameState);
}
}
else if (unitAIStateOrder == "GATHER")
{
// we're already gathering. But let's check if there is nothing better
// in case UnitAI did something bad
if (ent.unitAIOrderData().length)
{
let supplyId = ent.unitAIOrderData()[0].target;
let supply = gameState.getEntityById(supplyId);
if (supply && !supply.hasClass("Field") && !supply.hasClass("Animal") &&
- supply.resourceSupplyType().generic != "treasure" &&
supplyId != ent.getMetadata(PlayerID, "supply"))
{
let nbGatherers = supply.resourceSupplyNumGatherers() + gameState.ai.HQ.GetTCGatherer(supplyId);
if (nbGatherers > 1 && supply.resourceSupplyAmount()/nbGatherers < 30)
{
gameState.ai.HQ.RemoveTCGatherer(supplyId);
this.startGathering(gameState);
}
else
{
let gatherType = ent.getMetadata(PlayerID, "gather-type");
let nearby = this.base.dropsiteSupplies[gatherType].nearby;
if (nearby.some(sup => sup.id == supplyId))
ent.setMetadata(PlayerID, "supply", supplyId);
else if (nearby.length)
{
gameState.ai.HQ.RemoveTCGatherer(supplyId);
this.startGathering(gameState);
}
else
{
let medium = this.base.dropsiteSupplies[gatherType].medium;
if (medium.length && !medium.some(sup => sup.id == supplyId))
{
gameState.ai.HQ.RemoveTCGatherer(supplyId);
this.startGathering(gameState);
}
else
ent.setMetadata(PlayerID, "supply", supplyId);
}
}
}
}
}
else if (unitAIState == "INDIVIDUAL.RETURNRESOURCE.APPROACHING")
{
if (gameState.ai.playedTurn % 10 == 0)
{
// Check from time to time that UnitAI does not send us to an inaccessible dropsite
let dropsite = gameState.getEntityById(ent.unitAIOrderData()[0].target);
if (dropsite && dropsite.position() && this.entAccess != PETRA.getLandAccess(gameState, dropsite))
PETRA.returnResources(gameState, this.ent);
}
// If gathering a sparse resource, we may have been sent to a faraway resource if the one nearby was full.
// Let's check if it is still the case. If so, we reset its metadata supplyId so that the unit will be
// reordered to gather after having returned the resources (when comparing its supplyId with the UnitAI one).
let gatherType = ent.getMetadata(PlayerID, "gather-type");
let influenceGroup = Resources.GetResource(gatherType).aiAnalysisInfluenceGroup;
if (influenceGroup && influenceGroup == "sparse")
{
let supplyId = ent.getMetadata(PlayerID, "supply");
if (supplyId)
{
let nearby = this.base.dropsiteSupplies[gatherType].nearby;
if (!nearby.some(sup => sup.id == supplyId))
{
if (nearby.length)
ent.setMetadata(PlayerID, "supply", undefined);
else
{
let medium = this.base.dropsiteSupplies[gatherType].medium;
if (!medium.some(sup => sup.id == supplyId) && medium.length)
ent.setMetadata(PlayerID, "supply", undefined);
}
}
}
}
}
}
else if (subrole == "builder")
{
if (unitAIStateOrder == "REPAIR")
{
// Update our target in case UnitAI sent us to a different foundation because of autocontinue
// and abandon it if UnitAI has sent us to build a field (as we build them only when needed)
if (ent.unitAIOrderData()[0] && ent.unitAIOrderData()[0].target &&
ent.getMetadata(PlayerID, "target-foundation") != ent.unitAIOrderData()[0].target)
{
let targetId = ent.unitAIOrderData()[0].target;
let target = gameState.getEntityById(targetId);
if (target && !target.hasClass("Field"))
{
ent.setMetadata(PlayerID, "target-foundation", targetId);
return;
}
ent.setMetadata(PlayerID, "target-foundation", undefined);
ent.setMetadata(PlayerID, "subrole", "idle");
ent.stopMoving();
if (this.baseID != gameState.ai.HQ.baseManagers[0].ID)
{
// reassign it to something useful
this.base.reassignIdleWorkers(gameState, [ent]);
this.update(gameState, ent);
return;
}
}
// Otherwise check that the target still exists (useful in REPAIR.APPROACHING)
let targetId = ent.getMetadata(PlayerID, "target-foundation");
if (targetId && gameState.getEntityById(targetId))
return;
ent.stopMoving();
}
// okay so apparently we aren't working.
// Unless we've been explicitely told to keep our role, make us idle.
let target = gameState.getEntityById(ent.getMetadata(PlayerID, "target-foundation"));
if (!target || target.foundationProgress() === undefined && target.needsRepair() === false)
{
ent.setMetadata(PlayerID, "subrole", "idle");
ent.setMetadata(PlayerID, "target-foundation", undefined);
if (this.baseID != gameState.ai.HQ.baseManagers[0].ID)
{
// reassign it to something useful
this.base.reassignIdleWorkers(gameState, [ent]);
this.update(gameState, ent);
return;
}
}
else
{
let goalAccess = PETRA.getLandAccess(gameState, target);
let queued = PETRA.returnResources(gameState, ent);
if (this.entAccess == goalAccess)
ent.repair(target, target.hasClass("House"), queued); // autocontinue=true for houses
else
gameState.ai.HQ.navalManager.requireTransport(gameState, ent, this.entAccess, goalAccess, target.position());
}
}
else if (subrole == "hunter")
{
let lastHuntSearch = ent.getMetadata(PlayerID, "lastHuntSearch");
if (ent.isIdle() && (!lastHuntSearch || gameState.ai.elapsedTime - lastHuntSearch > 20))
{
if (!this.startHunting(gameState))
{
// nothing to hunt around. Try another region if any
let nowhereToHunt = true;
for (let base of gameState.ai.HQ.baseManagers)
{
if (!base.anchor || !base.anchor.position())
continue;
let basePos = base.anchor.position();
if (this.startHunting(gameState, basePos))
{
ent.setMetadata(PlayerID, "base", base.ID);
if (base.accessIndex == this.entAccess)
ent.move(basePos[0], basePos[1]);
else
gameState.ai.HQ.navalManager.requireTransport(gameState, ent, this.entAccess, base.accessIndex, basePos);
nowhereToHunt = false;
break;
}
}
if (nowhereToHunt)
ent.setMetadata(PlayerID, "lastHuntSearch", gameState.ai.elapsedTime);
}
}
else // Perform some sanity checks
{
if (unitAIStateOrder == "GATHER" || unitAIStateOrder == "RETURNRESOURCE")
{
// we may have drifted towards ennemy territory during the hunt, if yes go home
let territoryOwner = gameState.ai.HQ.territoryMap.getOwner(ent.position());
if (territoryOwner != 0 && !gameState.isPlayerAlly(territoryOwner)) // player is its own ally
this.startHunting(gameState);
else if (unitAIState == "INDIVIDUAL.RETURNRESOURCE.APPROACHING")
{
// Check that UnitAI does not send us to an inaccessible dropsite
let dropsite = gameState.getEntityById(ent.unitAIOrderData()[0].target);
if (dropsite && dropsite.position() && this.entAccess != PETRA.getLandAccess(gameState, dropsite))
PETRA.returnResources(gameState, ent);
}
}
}
}
else if (subrole == "fisher")
{
if (ent.isIdle())
this.startFishing(gameState);
else // if we have drifted towards ennemy territory during the fishing, go home
{
let territoryOwner = gameState.ai.HQ.territoryMap.getOwner(ent.position());
if (territoryOwner != 0 && !gameState.isPlayerAlly(territoryOwner)) // player is its own ally
this.startFishing(gameState);
}
}
};
PETRA.Worker.prototype.retryWorking = function(gameState, subrole)
{
switch (subrole)
{
case "gatherer":
return this.startGathering(gameState);
case "hunter":
return this.startHunting(gameState);
case "fisher":
return this.startFishing(gameState);
case "builder":
return this.startBuilding(gameState);
default:
return false;
}
};
PETRA.Worker.prototype.startBuilding = function(gameState)
{
let target = gameState.getEntityById(this.ent.getMetadata(PlayerID, "target-foundation"));
if (!target || target.foundationProgress() === undefined && target.needsRepair() == false)
return false;
if (PETRA.getLandAccess(gameState, target) != this.entAccess)
return false;
this.ent.repair(target, target.hasClass("House")); // autocontinue=true for houses
return true;
};
PETRA.Worker.prototype.startGathering = function(gameState)
{
// First look for possible treasure if any
if (PETRA.gatherTreasure(gameState, this.ent))
return true;
let resource = this.ent.getMetadata(PlayerID, "gather-type");
// If we are gathering food, try to hunt first
if (resource == "food" && this.startHunting(gameState))
return true;
let findSupply = function(ent, supplies) {
let ret = false;
let gatherRates = ent.resourceGatherRates();
for (let i = 0; i < supplies.length; ++i)
{
// exhausted resource, remove it from this list
if (!supplies[i].ent || !gameState.getEntityById(supplies[i].id))
{
supplies.splice(i--, 1);
continue;
}
if (PETRA.IsSupplyFull(gameState, supplies[i].ent))
continue;
let inaccessibleTime = supplies[i].ent.getMetadata(PlayerID, "inaccessibleTime");
if (inaccessibleTime && gameState.ai.elapsedTime < inaccessibleTime)
continue;
let supplyType = supplies[i].ent.get("ResourceSupply/Type");
if (!gatherRates[supplyType])
continue;
// check if available resource is worth one additionnal gatherer (except for farms)
let nbGatherers = supplies[i].ent.resourceSupplyNumGatherers() + gameState.ai.HQ.GetTCGatherer(supplies[i].id);
if (supplies[i].ent.resourceSupplyType().specific != "grain" && nbGatherers > 0 &&
supplies[i].ent.resourceSupplyAmount()/(1+nbGatherers) < 30)
continue;
// not in ennemy territory
let territoryOwner = gameState.ai.HQ.territoryMap.getOwner(supplies[i].ent.position());
if (territoryOwner != 0 && !gameState.isPlayerAlly(territoryOwner)) // player is its own ally
continue;
gameState.ai.HQ.AddTCGatherer(supplies[i].id);
ent.setMetadata(PlayerID, "supply", supplies[i].id);
ret = supplies[i].ent;
break;
}
return ret;
};
let navalManager = gameState.ai.HQ.navalManager;
let supply;
// first look in our own base if accessible from our present position
if (this.baseAccess == this.entAccess)
{
supply = findSupply(this.ent, this.base.dropsiteSupplies[resource].nearby);
if (supply)
{
this.ent.gather(supply);
return true;
}
// --> for food, try to gather from fields if any, otherwise build one if any
if (resource == "food")
{
supply = this.gatherNearestField(gameState, this.baseID);
if (supply)
{
this.ent.gather(supply);
return true;
}
supply = this.buildAnyField(gameState, this.baseID);
if (supply)
{
this.ent.repair(supply);
return true;
}
}
supply = findSupply(this.ent, this.base.dropsiteSupplies[resource].medium);
if (supply)
{
this.ent.gather(supply);
return true;
}
}
// So if we're here we have checked our whole base for a proper resource (or it was not accessible)
// --> check other bases directly accessible
for (let base of gameState.ai.HQ.baseManagers)
{
if (base.ID == this.baseID)
continue;
if (base.accessIndex != this.entAccess)
continue;
supply = findSupply(this.ent, base.dropsiteSupplies[resource].nearby);
if (supply)
{
this.ent.setMetadata(PlayerID, "base", base.ID);
this.ent.gather(supply);
return true;
}
}
if (resource == "food") // --> for food, try to gather from fields if any, otherwise build one if any
{
for (let base of gameState.ai.HQ.baseManagers)
{
if (base.ID == this.baseID)
continue;
if (base.accessIndex != this.entAccess)
continue;
supply = this.gatherNearestField(gameState, base.ID);
if (supply)
{
this.ent.setMetadata(PlayerID, "base", base.ID);
this.ent.gather(supply);
return true;
}
supply = this.buildAnyField(gameState, base.ID);
if (supply)
{
this.ent.setMetadata(PlayerID, "base", base.ID);
this.ent.repair(supply);
return true;
}
}
}
for (let base of gameState.ai.HQ.baseManagers)
{
if (base.ID == this.baseID)
continue;
if (base.accessIndex != this.entAccess)
continue;
supply = findSupply(this.ent, base.dropsiteSupplies[resource].medium);
if (supply)
{
this.ent.setMetadata(PlayerID, "base", base.ID);
this.ent.gather(supply);
return true;
}
}
// Okay may-be we haven't found any appropriate dropsite anywhere.
// Try to help building one if any accessible foundation available
let foundations = gameState.getOwnFoundations().toEntityArray();
let shouldBuild = this.ent.isBuilder() && foundations.some(function(foundation) {
if (!foundation || PETRA.getLandAccess(gameState, foundation) != this.entAccess)
return false;
let structure = gameState.getBuiltTemplate(foundation.templateName());
if (structure.resourceDropsiteTypes() && structure.resourceDropsiteTypes().indexOf(resource) != -1)
{
if (foundation.getMetadata(PlayerID, "base") != this.baseID)
this.ent.setMetadata(PlayerID, "base", foundation.getMetadata(PlayerID, "base"));
this.ent.setMetadata(PlayerID, "target-foundation", foundation.id());
this.ent.setMetadata(PlayerID, "subrole", "builder");
this.ent.repair(foundation);
return true;
}
return false;
}, this);
if (shouldBuild)
return true;
// Still nothing ... try bases which need a transport
for (let base of gameState.ai.HQ.baseManagers)
{
if (base.accessIndex == this.entAccess)
continue;
supply = findSupply(this.ent, base.dropsiteSupplies[resource].nearby);
if (supply && navalManager.requireTransport(gameState, this.ent, this.entAccess, base.accessIndex, supply.position()))
{
if (base.ID != this.baseID)
this.ent.setMetadata(PlayerID, "base", base.ID);
return true;
}
}
if (resource == "food") // --> for food, try to gather from fields if any, otherwise build one if any
{
for (let base of gameState.ai.HQ.baseManagers)
{
if (base.accessIndex == this.entAccess)
continue;
supply = this.gatherNearestField(gameState, base.ID);
if (supply && navalManager.requireTransport(gameState, this.ent, this.entAccess, base.accessIndex, supply.position()))
{
if (base.ID != this.baseID)
this.ent.setMetadata(PlayerID, "base", base.ID);
return true;
}
supply = this.buildAnyField(gameState, base.ID);
if (supply && navalManager.requireTransport(gameState, this.ent, this.entAccess, base.accessIndex, supply.position()))
{
if (base.ID != this.baseID)
this.ent.setMetadata(PlayerID, "base", base.ID);
return true;
}
}
}
for (let base of gameState.ai.HQ.baseManagers)
{
if (base.accessIndex == this.entAccess)
continue;
supply = findSupply(this.ent, base.dropsiteSupplies[resource].medium);
if (supply && navalManager.requireTransport(gameState, this.ent, this.entAccess, base.accessIndex, supply.position()))
{
if (base.ID != this.baseID)
this.ent.setMetadata(PlayerID, "base", base.ID);
return true;
}
}
// Okay so we haven't found any appropriate dropsite anywhere.
// Try to help building one if any non-accessible foundation available
shouldBuild = this.ent.isBuilder() && foundations.some(function(foundation) {
if (!foundation || PETRA.getLandAccess(gameState, foundation) == this.entAccess)
return false;
let structure = gameState.getBuiltTemplate(foundation.templateName());
if (structure.resourceDropsiteTypes() && structure.resourceDropsiteTypes().indexOf(resource) != -1)
{
let foundationAccess = PETRA.getLandAccess(gameState, foundation);
if (navalManager.requireTransport(gameState, this.ent, this.entAccess, foundationAccess, foundation.position()))
{
if (foundation.getMetadata(PlayerID, "base") != this.baseID)
this.ent.setMetadata(PlayerID, "base", foundation.getMetadata(PlayerID, "base"));
this.ent.setMetadata(PlayerID, "target-foundation", foundation.id());
this.ent.setMetadata(PlayerID, "subrole", "builder");
return true;
}
}
return false;
}, this);
if (shouldBuild)
return true;
// Still nothing, we look now for faraway resources, first in the accessible ones, then in the others
// except for food when farms or corrals can be used
let allowDistant = true;
if (resource == "food")
{
if (gameState.ai.HQ.turnCache.allowDistantFood === undefined)
gameState.ai.HQ.turnCache.allowDistantFood =
!gameState.ai.HQ.canBuild(gameState, "structures/{civ}/field") &&
!gameState.ai.HQ.canBuild(gameState, "structures/{civ}/corral");
allowDistant = gameState.ai.HQ.turnCache.allowDistantFood;
}
if (allowDistant)
{
if (this.baseAccess == this.entAccess)
{
supply = findSupply(this.ent, this.base.dropsiteSupplies[resource].faraway);
if (supply)
{
this.ent.gather(supply);
return true;
}
}
for (let base of gameState.ai.HQ.baseManagers)
{
if (base.ID == this.baseID)
continue;
if (base.accessIndex != this.entAccess)
continue;
supply = findSupply(this.ent, base.dropsiteSupplies[resource].faraway);
if (supply)
{
this.ent.setMetadata(PlayerID, "base", base.ID);
this.ent.gather(supply);
return true;
}
}
for (let base of gameState.ai.HQ.baseManagers)
{
if (base.accessIndex == this.entAccess)
continue;
supply = findSupply(this.ent, base.dropsiteSupplies[resource].faraway);
if (supply && navalManager.requireTransport(gameState, this.ent, this.entAccess, base.accessIndex, supply.position()))
{
if (base.ID != this.baseID)
this.ent.setMetadata(PlayerID, "base", base.ID);
return true;
}
}
}
// If we are here, we have nothing left to gather ... certainly no more resources of this type
gameState.ai.HQ.lastFailedGather[resource] = gameState.ai.elapsedTime;
if (gameState.ai.Config.debug > 2)
API3.warn(" >>>>> worker with gather-type " + resource + " with nothing to gather ");
this.ent.setMetadata(PlayerID, "subrole", "idle");
return false;
};
/**
* if position is given, we only check if we could hunt from this position but do nothing
* otherwise the position of the entity is taken, and if something is found, we directly start the hunt
*/
PETRA.Worker.prototype.startHunting = function(gameState, position)
{
// First look for possible treasure if any
if (!position && PETRA.gatherTreasure(gameState, this.ent))
return true;
let resources = gameState.getHuntableSupplies();
if (!resources.hasEntities())
return false;
let nearestSupplyDist = Math.min();
let nearestSupply;
let isFastMoving = PETRA.isFastMoving(this.ent);
let isRanged = this.ent.hasClass("Ranged");
let entPosition = position ? position : this.ent.position();
let foodDropsites = gameState.playerData.hasSharedDropsites ?
gameState.getAnyDropsites("food") : gameState.getOwnDropsites("food");
let hasFoodDropsiteWithinDistance = function(supplyPosition, supplyAccess, distSquare)
{
for (let dropsite of foodDropsites.values())
{
if (!dropsite.position())
continue;
let owner = dropsite.owner();
// owner != PlayerID can only happen when hasSharedDropsites == true, so no need to test it again
if (owner != PlayerID && (!dropsite.isSharedDropsite() || !gameState.isPlayerMutualAlly(owner)))
continue;
if (supplyAccess != PETRA.getLandAccess(gameState, dropsite))
continue;
if (API3.SquareVectorDistance(supplyPosition, dropsite.position()) < distSquare)
return true;
}
return false;
};
let gatherRates = this.ent.resourceGatherRates();
for (let supply of resources.values())
{
if (!supply.position())
continue;
let inaccessibleTime = supply.getMetadata(PlayerID, "inaccessibleTime");
if (inaccessibleTime && gameState.ai.elapsedTime < inaccessibleTime)
continue;
let supplyType = supply.get("ResourceSupply/Type");
if (!gatherRates[supplyType])
continue;
if (PETRA.IsSupplyFull(gameState, supply))
continue;
// Check if available resource is worth one additionnal gatherer (except for farms).
let nbGatherers = supply.resourceSupplyNumGatherers() + gameState.ai.HQ.GetTCGatherer(supply.id());
if (nbGatherers > 0 && supply.resourceSupplyAmount()/(1+nbGatherers) < 30)
continue;
let canFlee = !supply.hasClass("Domestic") && supply.templateName().indexOf("resource|") == -1;
// Only FastMoving and Ranged units should hunt fleeing animals.
if (canFlee && !isFastMoving && !isRanged)
continue;
let supplyAccess = PETRA.getLandAccess(gameState, supply);
if (supplyAccess != this.entAccess)
continue;
// measure the distance to the resource.
let dist = API3.SquareVectorDistance(entPosition, supply.position());
if (dist > nearestSupplyDist)
continue;
// Only FastMoving should hunt faraway.
if (!isFastMoving && dist > 25000)
continue;
// Avoid enemy territory.
let territoryOwner = gameState.ai.HQ.territoryMap.getOwner(supply.position());
if (territoryOwner != 0 && !gameState.isPlayerAlly(territoryOwner)) // Player is its own ally.
continue;
// And if in ally territory, don't hunt this ally's cattle.
if (territoryOwner != 0 && territoryOwner != PlayerID && supply.owner() == territoryOwner)
continue;
// Only FastMoving should hunt far from dropsite (specially for non-Domestic animals which flee).
if (!isFastMoving && canFlee && territoryOwner == 0)
continue;
let distanceSquare = isFastMoving ? 35000 : (canFlee ? 7000 : 12000);
if (!hasFoodDropsiteWithinDistance(supply.position(), supplyAccess, distanceSquare))
continue;
nearestSupplyDist = dist;
nearestSupply = supply;
}
if (nearestSupply)
{
if (position)
return true;
gameState.ai.HQ.AddTCGatherer(nearestSupply.id());
this.ent.gather(nearestSupply);
this.ent.setMetadata(PlayerID, "supply", nearestSupply.id());
this.ent.setMetadata(PlayerID, "target-foundation", undefined);
return true;
}
return false;
};
PETRA.Worker.prototype.startFishing = function(gameState)
{
if (!this.ent.position())
return false;
let resources = gameState.getFishableSupplies();
if (!resources.hasEntities())
{
gameState.ai.HQ.navalManager.resetFishingBoats(gameState);
this.ent.destroy();
return false;
}
let nearestSupplyDist = Math.min();
let nearestSupply;
let fisherSea = PETRA.getSeaAccess(gameState, this.ent);
let fishDropsites = (gameState.playerData.hasSharedDropsites ? gameState.getAnyDropsites("food") : gameState.getOwnDropsites("food")).
filter(API3.Filters.byClass("Dock")).toEntityArray();
let nearestDropsiteDist = function(supply) {
let distMin = 1000000;
let pos = supply.position();
for (let dropsite of fishDropsites)
{
if (!dropsite.position())
continue;
let owner = dropsite.owner();
// owner != PlayerID can only happen when hasSharedDropsites == true, so no need to test it again
if (owner != PlayerID && (!dropsite.isSharedDropsite() || !gameState.isPlayerMutualAlly(owner)))
continue;
if (fisherSea != PETRA.getSeaAccess(gameState, dropsite))
continue;
distMin = Math.min(distMin, API3.SquareVectorDistance(pos, dropsite.position()));
}
return distMin;
};
let exhausted = true;
let gatherRates = this.ent.resourceGatherRates();
resources.forEach(function(supply)
{
if (!supply.position())
return;
// check that it is accessible
if (gameState.ai.HQ.navalManager.getFishSea(gameState, supply) != fisherSea)
return;
exhausted = false;
let supplyType = supply.get("ResourceSupply/Type");
if (!gatherRates[supplyType])
return;
if (PETRA.IsSupplyFull(gameState, supply))
return;
// check if available resource is worth one additionnal gatherer (except for farms)
let nbGatherers = supply.resourceSupplyNumGatherers() + gameState.ai.HQ.GetTCGatherer(supply.id());
if (nbGatherers > 0 && supply.resourceSupplyAmount()/(1+nbGatherers) < 30)
return;
// Avoid ennemy territory
if (!gameState.ai.HQ.navalManager.canFishSafely(gameState, supply))
return;
// measure the distance from the resource to the nearest dropsite
let dist = nearestDropsiteDist(supply);
if (dist > nearestSupplyDist)
return;
nearestSupplyDist = dist;
nearestSupply = supply;
});
if (exhausted)
{
gameState.ai.HQ.navalManager.resetFishingBoats(gameState, fisherSea);
this.ent.destroy();
return false;
}
if (nearestSupply)
{
gameState.ai.HQ.AddTCGatherer(nearestSupply.id());
this.ent.gather(nearestSupply);
this.ent.setMetadata(PlayerID, "supply", nearestSupply.id());
this.ent.setMetadata(PlayerID, "target-foundation", undefined);
return true;
}
if (this.ent.getMetadata(PlayerID, "subrole") == "fisher")
this.ent.setMetadata(PlayerID, "subrole", "idle");
return false;
};
PETRA.Worker.prototype.gatherNearestField = function(gameState, baseID)
{
let ownFields = gameState.getOwnEntitiesByClass("Field", true).filter(API3.Filters.isBuilt()).filter(API3.Filters.byMetadata(PlayerID, "base", baseID));
let bestFarm;
let gatherRates = this.ent.resourceGatherRates();
for (let field of ownFields.values())
{
if (PETRA.IsSupplyFull(gameState, field))
continue;
let supplyType = field.get("ResourceSupply/Type");
if (!gatherRates[supplyType])
continue;
let rate = 1;
let diminishing = field.getDiminishingReturns();
if (diminishing < 1)
{
let num = field.resourceSupplyNumGatherers() + gameState.ai.HQ.GetTCGatherer(field.id());
if (num > 0)
rate = Math.pow(diminishing, num);
}
// Add a penalty distance depending on rate
let dist = API3.SquareVectorDistance(field.position(), this.ent.position()) + (1 - rate) * 160000;
if (!bestFarm || dist < bestFarm.dist)
bestFarm = { "ent": field, "dist": dist, "rate": rate };
}
// If other field foundations available, better build them when rate becomes too small
if (!bestFarm || bestFarm.rate < 0.70 &&
gameState.getOwnFoundations().filter(API3.Filters.byClass("Field")).filter(API3.Filters.byMetadata(PlayerID, "base", baseID)).hasEntities())
return false;
gameState.ai.HQ.AddTCGatherer(bestFarm.ent.id());
this.ent.setMetadata(PlayerID, "supply", bestFarm.ent.id());
return bestFarm.ent;
};
/**
* WARNING with the present options of AI orders, the unit will not gather after building the farm.
* This is done by calling the gatherNearestField function when construction is completed.
*/
PETRA.Worker.prototype.buildAnyField = function(gameState, baseID)
{
if (!this.ent.isBuilder())
return false;
let bestFarmEnt = false;
let bestFarmDist = 10000000;
let pos = this.ent.position();
for (let found of gameState.getOwnFoundations().values())
{
if (found.getMetadata(PlayerID, "base") != baseID || !found.hasClass("Field"))
continue;
let current = found.getBuildersNb();
if (current === undefined ||
current >= gameState.getBuiltTemplate(found.templateName()).maxGatherers())
continue;
let dist = API3.SquareVectorDistance(found.position(), pos);
if (dist > bestFarmDist)
continue;
bestFarmEnt = found;
bestFarmDist = dist;
}
return bestFarmEnt;
};
/**
* Workers elephant should move away from the buildings they've built to avoid being trapped in between constructions.
* For the time being, we move towards the nearest gatherer (providing him a dropsite).
* BaseManager does also use that function to deal with its mobile dropsites.
*/
PETRA.Worker.prototype.moveToGatherer = function(gameState, ent, forced)
{
let pos = ent.position();
if (!pos || ent.getMetadata(PlayerID, "target-foundation") !== undefined)
return;
if (!forced && gameState.ai.elapsedTime < (ent.getMetadata(PlayerID, "nextMoveToGatherer") || 5))
return;
let gatherers = this.base.workersBySubrole(gameState, "gatherer");
let dist = Math.min();
let destination;
let access = PETRA.getLandAccess(gameState, ent);
let types = ent.resourceDropsiteTypes();
for (let gatherer of gatherers.values())
{
let gathererType = gatherer.getMetadata(PlayerID, "gather-type");
if (!gathererType || types.indexOf(gathererType) == -1)
continue;
if (!gatherer.position() || gatherer.getMetadata(PlayerID, "transport") !== undefined ||
PETRA.getLandAccess(gameState, gatherer) != access || gatherer.isIdle())
continue;
let distance = API3.SquareVectorDistance(pos, gatherer.position());
if (distance > dist)
continue;
dist = distance;
destination = gatherer.position();
}
ent.setMetadata(PlayerID, "nextMoveToGatherer", gameState.ai.elapsedTime + (destination ? 12 : 5));
if (destination && dist > 10)
ent.move(destination[0], destination[1]);
};
/**
* Check accessibility of the target when in approach (in RMS maps, we quite often have chicken or bushes
* inside obstruction of other entities). The resource will be flagged as inaccessible during 10 mn (in case
* it will be cleared later).
*/
PETRA.Worker.prototype.isInaccessibleSupply = function(gameState)
{
if (!this.ent.unitAIOrderData()[0] || !this.ent.unitAIOrderData()[0].target)
return false;
let targetId = this.ent.unitAIOrderData()[0].target;
let target = gameState.getEntityById(targetId);
if (!target)
return true;
if (!target.resourceSupplyType())
return false;
let approachingTarget = this.ent.getMetadata(PlayerID, "approachingTarget");
let carriedAmount = this.ent.resourceCarrying().length ? this.ent.resourceCarrying()[0].amount : 0;
if (!approachingTarget || approachingTarget != targetId)
{
this.ent.setMetadata(PlayerID, "approachingTarget", targetId);
this.ent.setMetadata(PlayerID, "approachingTime", undefined);
this.ent.setMetadata(PlayerID, "approachingPos", undefined);
this.ent.setMetadata(PlayerID, "carriedBefore", carriedAmount);
let alreadyTried = this.ent.getMetadata(PlayerID, "alreadyTried");
if (alreadyTried && alreadyTried != targetId)
this.ent.setMetadata(PlayerID, "alreadyTried", undefined);
}
let carriedBefore = this.ent.getMetadata(PlayerID, "carriedBefore");
if (carriedBefore != carriedAmount)
{
this.ent.setMetadata(PlayerID, "approachingTarget", undefined);
this.ent.setMetadata(PlayerID, "alreadyTried", undefined);
if (target.getMetadata(PlayerID, "inaccessibleTime"))
target.setMetadata(PlayerID, "inaccessibleTime", 0);
return false;
}
let inaccessibleTime = target.getMetadata(PlayerID, "inaccessibleTime");
if (inaccessibleTime && gameState.ai.elapsedTime < inaccessibleTime)
return true;
let approachingTime = this.ent.getMetadata(PlayerID, "approachingTime");
if (!approachingTime || gameState.ai.elapsedTime - approachingTime > 3)
{
let presentPos = this.ent.position();
let approachingPos = this.ent.getMetadata(PlayerID, "approachingPos");
if (!approachingPos || approachingPos[0] != presentPos[0] || approachingPos[1] != presentPos[1])
{
this.ent.setMetadata(PlayerID, "approachingTime", gameState.ai.elapsedTime);
this.ent.setMetadata(PlayerID, "approachingPos", presentPos);
return false;
}
if (gameState.ai.elapsedTime - approachingTime > 10)
{
if (this.ent.getMetadata(PlayerID, "alreadyTried"))
{
target.setMetadata(PlayerID, "inaccessibleTime", gameState.ai.elapsedTime + 600);
return true;
}
// let's try again to reach it
this.ent.setMetadata(PlayerID, "alreadyTried", targetId);
this.ent.setMetadata(PlayerID, "approachingTarget", undefined);
this.ent.gather(target);
return false;
}
}
return false;
};
Index: ps/trunk/binaries/data/mods/public/simulation/components/GuiInterface.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/components/GuiInterface.js (revision 24988)
+++ ps/trunk/binaries/data/mods/public/simulation/components/GuiInterface.js (revision 24989)
@@ -1,2093 +1,2104 @@
function GuiInterface() {}
GuiInterface.prototype.Schema =
"";
GuiInterface.prototype.Serialize = function()
{
// This component isn't network-synchronized for the biggest part,
// so most of the attributes shouldn't be serialized.
// Return an object with a small selection of deterministic data.
return {
"timeNotifications": this.timeNotifications,
"timeNotificationID": this.timeNotificationID
};
};
GuiInterface.prototype.Deserialize = function(data)
{
this.Init();
this.timeNotifications = data.timeNotifications;
this.timeNotificationID = data.timeNotificationID;
};
GuiInterface.prototype.Init = function()
{
this.placementEntity = undefined; // = undefined or [templateName, entityID]
this.placementWallEntities = undefined;
this.placementWallLastAngle = 0;
this.notifications = [];
this.renamedEntities = [];
this.miragedEntities = [];
this.timeNotificationID = 1;
this.timeNotifications = [];
this.entsRallyPointsDisplayed = [];
this.entsWithAuraAndStatusBars = new Set();
this.enabledVisualRangeOverlayTypes = {};
this.templateModified = {};
this.selectionDirty = {};
this.obstructionSnap = new ObstructionSnap();
};
/*
* All of the functions defined below are called via Engine.GuiInterfaceCall(name, arg)
* from GUI scripts, and executed here with arguments (player, arg).
*
* CAUTION: The input to the functions in this module is not network-synchronised, so it
* mustn't affect the simulation state (i.e. the data that is serialised and can affect
* the behaviour of the rest of the simulation) else it'll cause out-of-sync errors.
*/
/**
* Returns global information about the current game state.
* This is used by the GUI and also by AI scripts.
*/
GuiInterface.prototype.GetSimulationState = function()
{
let ret = {
"players": []
};
let cmpPlayerManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager);
let numPlayers = cmpPlayerManager.GetNumPlayers();
for (let i = 0; i < numPlayers; ++i)
{
let cmpPlayer = QueryPlayerIDInterface(i);
let cmpPlayerEntityLimits = QueryPlayerIDInterface(i, IID_EntityLimits);
// Work out which phase we are in.
let phase = "";
let cmpTechnologyManager = QueryPlayerIDInterface(i, IID_TechnologyManager);
if (cmpTechnologyManager)
{
if (cmpTechnologyManager.IsTechnologyResearched("phase_city"))
phase = "city";
else if (cmpTechnologyManager.IsTechnologyResearched("phase_town"))
phase = "town";
else if (cmpTechnologyManager.IsTechnologyResearched("phase_village"))
phase = "village";
}
let allies = [];
let mutualAllies = [];
let neutrals = [];
let enemies = [];
for (let j = 0; j < numPlayers; ++j)
{
allies[j] = cmpPlayer.IsAlly(j);
mutualAllies[j] = cmpPlayer.IsMutualAlly(j);
neutrals[j] = cmpPlayer.IsNeutral(j);
enemies[j] = cmpPlayer.IsEnemy(j);
}
ret.players.push({
"name": cmpPlayer.GetName(),
"civ": cmpPlayer.GetCiv(),
"color": cmpPlayer.GetColor(),
"controlsAll": cmpPlayer.CanControlAllUnits(),
"popCount": cmpPlayer.GetPopulationCount(),
"popLimit": cmpPlayer.GetPopulationLimit(),
"popMax": cmpPlayer.GetMaxPopulation(),
"panelEntities": cmpPlayer.GetPanelEntities(),
"resourceCounts": cmpPlayer.GetResourceCounts(),
"resourceGatherers": cmpPlayer.GetResourceGatherers(),
"trainingBlocked": cmpPlayer.IsTrainingBlocked(),
"state": cmpPlayer.GetState(),
"team": cmpPlayer.GetTeam(),
"teamsLocked": cmpPlayer.GetLockTeams(),
"cheatsEnabled": cmpPlayer.GetCheatsEnabled(),
"disabledTemplates": cmpPlayer.GetDisabledTemplates(),
"disabledTechnologies": cmpPlayer.GetDisabledTechnologies(),
"hasSharedDropsites": cmpPlayer.HasSharedDropsites(),
"hasSharedLos": cmpPlayer.HasSharedLos(),
"spyCostMultiplier": cmpPlayer.GetSpyCostMultiplier(),
"phase": phase,
"isAlly": allies,
"isMutualAlly": mutualAllies,
"isNeutral": neutrals,
"isEnemy": enemies,
"entityLimits": cmpPlayerEntityLimits ? cmpPlayerEntityLimits.GetLimits() : null,
"entityCounts": cmpPlayerEntityLimits ? cmpPlayerEntityLimits.GetCounts() : null,
"matchEntityCounts": cmpPlayerEntityLimits ? cmpPlayerEntityLimits.GetMatchCounts() : null,
"entityLimitChangers": cmpPlayerEntityLimits ? cmpPlayerEntityLimits.GetLimitChangers() : null,
"researchQueued": cmpTechnologyManager ? cmpTechnologyManager.GetQueuedResearch() : null,
"researchStarted": cmpTechnologyManager ? cmpTechnologyManager.GetStartedTechs() : null,
"researchedTechs": cmpTechnologyManager ? cmpTechnologyManager.GetResearchedTechs() : null,
"classCounts": cmpTechnologyManager ? cmpTechnologyManager.GetClassCounts() : null,
"typeCountsByClass": cmpTechnologyManager ? cmpTechnologyManager.GetTypeCountsByClass() : null,
"canBarter": cmpPlayer.CanBarter(),
"barterPrices": Engine.QueryInterface(SYSTEM_ENTITY, IID_Barter).GetPrices(cmpPlayer)
});
}
let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
if (cmpRangeManager)
ret.circularMap = cmpRangeManager.GetLosCircular();
let cmpTerrain = Engine.QueryInterface(SYSTEM_ENTITY, IID_Terrain);
if (cmpTerrain)
ret.mapSize = cmpTerrain.GetMapSize();
let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
ret.timeElapsed = cmpTimer.GetTime();
let cmpCeasefireManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_CeasefireManager);
if (cmpCeasefireManager)
{
ret.ceasefireActive = cmpCeasefireManager.IsCeasefireActive();
ret.ceasefireTimeRemaining = ret.ceasefireActive ? cmpCeasefireManager.GetCeasefireStartedTime() + cmpCeasefireManager.GetCeasefireTime() - ret.timeElapsed : 0;
}
let cmpCinemaManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_CinemaManager);
if (cmpCinemaManager)
ret.cinemaPlaying = cmpCinemaManager.IsPlaying();
let cmpEndGameManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_EndGameManager);
ret.victoryConditions = cmpEndGameManager.GetVictoryConditions();
ret.alliedVictory = cmpEndGameManager.GetAlliedVictory();
ret.maxWorldPopulation = cmpPlayerManager.GetMaxWorldPopulation();
for (let i = 0; i < numPlayers; ++i)
{
let cmpPlayerStatisticsTracker = QueryPlayerIDInterface(i, IID_StatisticsTracker);
if (cmpPlayerStatisticsTracker)
ret.players[i].statistics = cmpPlayerStatisticsTracker.GetBasicStatistics();
}
return ret;
};
/**
* Returns global information about the current game state, plus statistics.
* This is used by the GUI at the end of a game, in the summary screen.
* Note: Amongst statistics, the team exploration map percentage is computed from
* scratch, so the extended simulation state should not be requested too often.
*/
GuiInterface.prototype.GetExtendedSimulationState = function()
{
let ret = this.GetSimulationState();
let numPlayers = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager).GetNumPlayers();
for (let i = 0; i < numPlayers; ++i)
{
let cmpPlayerStatisticsTracker = QueryPlayerIDInterface(i, IID_StatisticsTracker);
if (cmpPlayerStatisticsTracker)
ret.players[i].sequences = cmpPlayerStatisticsTracker.GetSequences();
}
return ret;
};
/**
* Returns the gamesettings that were chosen at the time the match started.
*/
GuiInterface.prototype.GetInitAttributes = function()
{
return InitAttributes;
};
/**
* This data will be stored in the replay metadata file after a match has been finished recording.
*/
GuiInterface.prototype.GetReplayMetadata = function()
{
let extendedSimState = this.GetExtendedSimulationState();
return {
"timeElapsed": extendedSimState.timeElapsed,
"playerStates": extendedSimState.players,
"mapSettings": InitAttributes.settings
};
};
/**
* Called when the game ends if the current game is part of a campaign run.
*/
GuiInterface.prototype.GetCampaignGameEndData = function(player)
{
let cmpTrigger = Engine.QueryInterface(SYSTEM_ENTITY, IID_Trigger);
if (Trigger.prototype.OnCampaignGameEnd)
return Trigger.prototype.OnCampaignGameEnd();
return {};
};
GuiInterface.prototype.GetRenamedEntities = function(player)
{
if (this.miragedEntities[player])
return this.renamedEntities.concat(this.miragedEntities[player]);
return this.renamedEntities;
};
GuiInterface.prototype.ClearRenamedEntities = function()
{
this.renamedEntities = [];
this.miragedEntities = [];
};
GuiInterface.prototype.AddMiragedEntity = function(player, entity, mirage)
{
if (!this.miragedEntities[player])
this.miragedEntities[player] = [];
this.miragedEntities[player].push({ "entity": entity, "newentity": mirage });
};
/**
* Get common entity info, often used in the gui.
*/
GuiInterface.prototype.GetEntityState = function(player, ent)
{
let cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager);
// All units must have a template; if not then it's a nonexistent entity id.
let template = cmpTemplateManager.GetCurrentTemplateName(ent);
if (!template)
return null;
let ret = {
"id": ent,
"player": INVALID_PLAYER,
"template": template
};
let cmpMirage = Engine.QueryInterface(ent, IID_Mirage);
if (cmpMirage)
ret.mirage = true;
let cmpIdentity = Engine.QueryInterface(ent, IID_Identity);
if (cmpIdentity)
ret.identity = {
"rank": cmpIdentity.GetRank(),
"classes": cmpIdentity.GetClassesList(),
"selectionGroupName": cmpIdentity.GetSelectionGroupName(),
"canDelete": !cmpIdentity.IsUndeletable(),
"hasSomeFormation": cmpIdentity.HasSomeFormation(),
"formations": cmpIdentity.GetFormationsList(),
"controllable": cmpIdentity.IsControllable()
};
let cmpPosition = Engine.QueryInterface(ent, IID_Position);
if (cmpPosition && cmpPosition.IsInWorld())
ret.position = cmpPosition.GetPosition();
let cmpHealth = QueryMiragedInterface(ent, IID_Health);
if (cmpHealth)
{
ret.hitpoints = cmpHealth.GetHitpoints();
ret.maxHitpoints = cmpHealth.GetMaxHitpoints();
ret.needsRepair = cmpHealth.IsRepairable() && cmpHealth.IsInjured();
ret.needsHeal = !cmpHealth.IsUnhealable();
}
let cmpCapturable = QueryMiragedInterface(ent, IID_Capturable);
if (cmpCapturable)
{
ret.capturePoints = cmpCapturable.GetCapturePoints();
ret.maxCapturePoints = cmpCapturable.GetMaxCapturePoints();
}
let cmpBuilder = Engine.QueryInterface(ent, IID_Builder);
if (cmpBuilder)
ret.builder = true;
let cmpMarket = QueryMiragedInterface(ent, IID_Market);
if (cmpMarket)
ret.market = {
"land": cmpMarket.HasType("land"),
"naval": cmpMarket.HasType("naval")
};
let cmpPack = Engine.QueryInterface(ent, IID_Pack);
if (cmpPack)
ret.pack = {
"packed": cmpPack.IsPacked(),
"progress": cmpPack.GetProgress()
};
let cmpPopulation = Engine.QueryInterface(ent, IID_Population);
if (cmpPopulation)
ret.population = {
"bonus": cmpPopulation.GetPopBonus()
};
let cmpUpgrade = Engine.QueryInterface(ent, IID_Upgrade);
if (cmpUpgrade)
ret.upgrade = {
"upgrades": cmpUpgrade.GetUpgrades(),
"progress": cmpUpgrade.GetProgress(),
"template": cmpUpgrade.GetUpgradingTo(),
"isUpgrading": cmpUpgrade.IsUpgrading()
};
let cmpStatusEffects = Engine.QueryInterface(ent, IID_StatusEffectsReceiver);
if (cmpStatusEffects)
ret.statusEffects = cmpStatusEffects.GetActiveStatuses();
let cmpProductionQueue = Engine.QueryInterface(ent, IID_ProductionQueue);
if (cmpProductionQueue)
ret.production = {
"entities": cmpProductionQueue.GetEntitiesList(),
"technologies": cmpProductionQueue.GetTechnologiesList(),
"techCostMultiplier": cmpProductionQueue.GetTechCostMultiplier(),
"queue": cmpProductionQueue.GetQueue()
};
let cmpTrader = Engine.QueryInterface(ent, IID_Trader);
if (cmpTrader)
ret.trader = {
"goods": cmpTrader.GetGoods()
};
let cmpFoundation = QueryMiragedInterface(ent, IID_Foundation);
if (cmpFoundation)
ret.foundation = {
"numBuilders": cmpFoundation.GetNumBuilders(),
"buildTime": cmpFoundation.GetBuildTime()
};
let cmpRepairable = QueryMiragedInterface(ent, IID_Repairable);
if (cmpRepairable)
ret.repairable = {
"numBuilders": cmpRepairable.GetNumBuilders(),
"buildTime": cmpRepairable.GetBuildTime()
};
let cmpOwnership = Engine.QueryInterface(ent, IID_Ownership);
if (cmpOwnership)
ret.player = cmpOwnership.GetOwner();
let cmpRallyPoint = Engine.QueryInterface(ent, IID_RallyPoint);
if (cmpRallyPoint)
ret.rallyPoint = { "position": cmpRallyPoint.GetPositions()[0] }; // undefined or {x,z} object
let cmpGarrisonHolder = Engine.QueryInterface(ent, IID_GarrisonHolder);
if (cmpGarrisonHolder)
ret.garrisonHolder = {
"entities": cmpGarrisonHolder.GetEntities(),
"buffHeal": cmpGarrisonHolder.GetHealRate(),
"allowedClasses": cmpGarrisonHolder.GetAllowedClasses(),
"capacity": cmpGarrisonHolder.GetCapacity(),
"occupiedSlots": cmpGarrisonHolder.OccupiedSlots()
};
let cmpTurretHolder = Engine.QueryInterface(ent, IID_TurretHolder);
if (cmpTurretHolder)
ret.turretHolder = {
"turretPoints": cmpTurretHolder.GetTurretPoints()
};
let cmpGarrisonable = Engine.QueryInterface(ent, IID_Garrisonable);
if (cmpGarrisonable)
ret.garrisonable = {
"holder": cmpGarrisonable.HolderID(),
"size": cmpGarrisonable.UnitSize()
};
let cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
if (cmpUnitAI)
ret.unitAI = {
"state": cmpUnitAI.GetCurrentState(),
"orders": cmpUnitAI.GetOrders(),
"hasWorkOrders": cmpUnitAI.HasWorkOrders(),
"canGuard": cmpUnitAI.CanGuard(),
"isGuarding": cmpUnitAI.IsGuardOf(),
"canPatrol": cmpUnitAI.CanPatrol(),
"selectableStances": cmpUnitAI.GetSelectableStances(),
"isIdle": cmpUnitAI.IsIdle()
};
let cmpGuard = Engine.QueryInterface(ent, IID_Guard);
if (cmpGuard)
ret.guard = {
"entities": cmpGuard.GetEntities()
};
let cmpResourceGatherer = Engine.QueryInterface(ent, IID_ResourceGatherer);
if (cmpResourceGatherer)
{
ret.resourceCarrying = cmpResourceGatherer.GetCarryingStatus();
ret.resourceGatherRates = cmpResourceGatherer.GetGatherRates();
}
let cmpGate = Engine.QueryInterface(ent, IID_Gate);
if (cmpGate)
ret.gate = {
"locked": cmpGate.IsLocked()
};
let cmpAlertRaiser = Engine.QueryInterface(ent, IID_AlertRaiser);
if (cmpAlertRaiser)
ret.alertRaiser = true;
let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
ret.visibility = cmpRangeManager.GetLosVisibility(ent, player);
let cmpAttack = Engine.QueryInterface(ent, IID_Attack);
if (cmpAttack)
{
let types = cmpAttack.GetAttackTypes();
if (types.length)
ret.attack = {};
for (let type of types)
{
ret.attack[type] = {};
Object.assign(ret.attack[type], cmpAttack.GetAttackEffectsData(type));
ret.attack[type].attackName = cmpAttack.GetAttackName(type);
ret.attack[type].splash = cmpAttack.GetSplashData(type);
if (ret.attack[type].splash)
Object.assign(ret.attack[type].splash, cmpAttack.GetAttackEffectsData(type, true));
let range = cmpAttack.GetRange(type);
ret.attack[type].minRange = range.min;
ret.attack[type].maxRange = range.max;
let timers = cmpAttack.GetTimers(type);
ret.attack[type].prepareTime = timers.prepare;
ret.attack[type].repeatTime = timers.repeat;
if (type != "Ranged")
{
// Not a ranged attack, set some defaults.
ret.attack[type].elevationBonus = 0;
ret.attack[type].elevationAdaptedRange = ret.attack.maxRange;
continue;
}
ret.attack[type].elevationBonus = range.elevationBonus;
if (cmpPosition && cmpPosition.IsInWorld())
// For units, take the range in front of it, no spread, so angle = 0,
// else, take the average elevation around it: angle = 2 * pi.
ret.attack[type].elevationAdaptedRange = cmpRangeManager.GetElevationAdaptedRange(cmpPosition.GetPosition(), cmpPosition.GetRotation(), range.max, range.elevationBonus, cmpUnitAI ? 0 : 2 * Math.PI);
else
// Not in world, set a default?
ret.attack[type].elevationAdaptedRange = ret.attack.maxRange;
}
}
let cmpResistance = Engine.QueryInterface(ent, IID_Resistance);
if (cmpResistance)
ret.resistance = cmpResistance.GetResistanceOfForm(cmpFoundation ? "Foundation" : "Entity");
let cmpBuildingAI = Engine.QueryInterface(ent, IID_BuildingAI);
if (cmpBuildingAI)
ret.buildingAI = {
"defaultArrowCount": cmpBuildingAI.GetDefaultArrowCount(),
"maxArrowCount": cmpBuildingAI.GetMaxArrowCount(),
"garrisonArrowMultiplier": cmpBuildingAI.GetGarrisonArrowMultiplier(),
"garrisonArrowClasses": cmpBuildingAI.GetGarrisonArrowClasses(),
"arrowCount": cmpBuildingAI.GetArrowCount()
};
if (cmpPosition && cmpPosition.GetTurretParent() != INVALID_ENTITY)
ret.turretParent = cmpPosition.GetTurretParent();
let cmpResourceSupply = QueryMiragedInterface(ent, IID_ResourceSupply);
if (cmpResourceSupply)
ret.resourceSupply = {
"isInfinite": cmpResourceSupply.IsInfinite(),
"max": cmpResourceSupply.GetMaxAmount(),
"amount": cmpResourceSupply.GetCurrentAmount(),
"type": cmpResourceSupply.GetType(),
"killBeforeGather": cmpResourceSupply.GetKillBeforeGather(),
"maxGatherers": cmpResourceSupply.GetMaxGatherers(),
"numGatherers": cmpResourceSupply.GetNumGatherers()
};
let cmpResourceDropsite = Engine.QueryInterface(ent, IID_ResourceDropsite);
if (cmpResourceDropsite)
ret.resourceDropsite = {
"types": cmpResourceDropsite.GetTypes(),
"sharable": cmpResourceDropsite.IsSharable(),
"shared": cmpResourceDropsite.IsShared()
};
let cmpPromotion = Engine.QueryInterface(ent, IID_Promotion);
if (cmpPromotion)
ret.promotion = {
"curr": cmpPromotion.GetCurrentXp(),
"req": cmpPromotion.GetRequiredXp()
};
if (!cmpFoundation && cmpIdentity && cmpIdentity.HasClass("Barter"))
ret.isBarterMarket = true;
let cmpHeal = Engine.QueryInterface(ent, IID_Heal);
if (cmpHeal)
ret.heal = {
"health": cmpHeal.GetHealth(),
"range": cmpHeal.GetRange().max,
"interval": cmpHeal.GetInterval(),
"unhealableClasses": cmpHeal.GetUnhealableClasses(),
"healableClasses": cmpHeal.GetHealableClasses()
};
let cmpLoot = Engine.QueryInterface(ent, IID_Loot);
if (cmpLoot)
{
ret.loot = cmpLoot.GetResources();
ret.loot.xp = cmpLoot.GetXp();
}
let cmpResourceTrickle = Engine.QueryInterface(ent, IID_ResourceTrickle);
if (cmpResourceTrickle)
ret.resourceTrickle = {
"interval": cmpResourceTrickle.GetInterval(),
"rates": cmpResourceTrickle.GetRates()
};
+ let cmpTreasure = Engine.QueryInterface(ent, IID_Treasure);
+ if (cmpTreasure)
+ ret.treasure = {
+ "collectTime": cmpTreasure.CollectionTime(),
+ "resources": cmpTreasure.Resources()
+ };
+
+ let cmpTreasureCollecter = Engine.QueryInterface(ent, IID_TreasureCollecter);
+ if (cmpTreasureCollecter)
+ ret.treasureCollecter = true;
+
let cmpUnitMotion = Engine.QueryInterface(ent, IID_UnitMotion);
if (cmpUnitMotion)
ret.speed = {
"walk": cmpUnitMotion.GetWalkSpeed(),
"run": cmpUnitMotion.GetWalkSpeed() * cmpUnitMotion.GetRunMultiplier()
};
return ret;
};
GuiInterface.prototype.GetMultipleEntityStates = function(player, ents)
{
return ents.map(ent => ({ "entId": ent, "state": this.GetEntityState(player, ent) }));
};
GuiInterface.prototype.GetAverageRangeForBuildings = function(player, cmd)
{
let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
let cmpTerrain = Engine.QueryInterface(SYSTEM_ENTITY, IID_Terrain);
let rot = { "x": 0, "y": 0, "z": 0 };
let pos = {
"x": cmd.x,
"y": cmpTerrain.GetGroundLevel(cmd.x, cmd.z),
"z": cmd.z
};
let elevationBonus = cmd.elevationBonus || 0;
let range = cmd.range;
return cmpRangeManager.GetElevationAdaptedRange(pos, rot, range, elevationBonus, 2 * Math.PI);
};
GuiInterface.prototype.GetTemplateData = function(player, data)
{
let templateName = data.templateName;
let owner = data.player !== undefined ? data.player : player;
let cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager);
let template = cmpTemplateManager.GetTemplate(templateName);
if (!template)
return null;
let aurasTemplate = {};
if (!template.Auras)
return GetTemplateDataHelper(template, owner, aurasTemplate);
let auraNames = template.Auras._string.split(/\s+/);
for (let name of auraNames)
{
let auraTemplate = AuraTemplates.Get(name);
if (!auraTemplate)
error("Template " + templateName + " has undefined aura " + name);
else
aurasTemplate[name] = auraTemplate;
}
return GetTemplateDataHelper(template, owner, aurasTemplate);
};
GuiInterface.prototype.IsTechnologyResearched = function(player, data)
{
if (!data.tech)
return true;
let cmpTechnologyManager = QueryPlayerIDInterface(data.player !== undefined ? data.player : player, IID_TechnologyManager);
if (!cmpTechnologyManager)
return false;
return cmpTechnologyManager.IsTechnologyResearched(data.tech);
};
/**
* Checks whether the requirements for this technology have been met.
*/
GuiInterface.prototype.CheckTechnologyRequirements = function(player, data)
{
let cmpTechnologyManager = QueryPlayerIDInterface(data.player !== undefined ? data.player : player, IID_TechnologyManager);
if (!cmpTechnologyManager)
return false;
return cmpTechnologyManager.CanResearch(data.tech);
};
/**
* Returns technologies that are being actively researched, along with
* which entity is researching them and how far along the research is.
*/
GuiInterface.prototype.GetStartedResearch = function(player)
{
let cmpTechnologyManager = QueryPlayerIDInterface(player, IID_TechnologyManager);
if (!cmpTechnologyManager)
return {};
let ret = {};
for (let tech of cmpTechnologyManager.GetStartedTechs())
{
ret[tech] = { "researcher": cmpTechnologyManager.GetResearcher(tech) };
let cmpProductionQueue = Engine.QueryInterface(ret[tech].researcher, IID_ProductionQueue);
if (cmpProductionQueue)
{
ret[tech].progress = cmpProductionQueue.GetQueue()[0].progress;
ret[tech].timeRemaining = cmpProductionQueue.GetQueue()[0].timeRemaining;
}
else
{
ret[tech].progress = 0;
ret[tech].timeRemaining = 0;
}
}
return ret;
};
/**
* Returns the battle state of the player.
*/
GuiInterface.prototype.GetBattleState = function(player)
{
let cmpBattleDetection = QueryPlayerIDInterface(player, IID_BattleDetection);
if (!cmpBattleDetection)
return false;
return cmpBattleDetection.GetState();
};
/**
* Returns a list of ongoing attacks against the player.
*/
GuiInterface.prototype.GetIncomingAttacks = function(player)
{
let cmpAttackDetection = QueryPlayerIDInterface(player, IID_AttackDetection);
if (!cmpAttackDetection)
return [];
return cmpAttackDetection.GetIncomingAttacks();
};
/**
* Used to show a red square over GUI elements you can't yet afford.
*/
GuiInterface.prototype.GetNeededResources = function(player, data)
{
let cmpPlayer = QueryPlayerIDInterface(data.player !== undefined ? data.player : player);
return cmpPlayer ? cmpPlayer.GetNeededResources(data.cost) : {};
};
/**
* State of the templateData (player dependent): true when some template values have been modified
* and need to be reloaded by the gui.
*/
GuiInterface.prototype.OnTemplateModification = function(msg)
{
this.templateModified[msg.player] = true;
this.selectionDirty[msg.player] = true;
};
GuiInterface.prototype.IsTemplateModified = function(player)
{
return this.templateModified[player] || false;
};
GuiInterface.prototype.ResetTemplateModified = function()
{
this.templateModified = {};
};
/**
* Some changes may require an update to the selection panel,
* which is cached for efficiency. Inform the GUI it needs reloading.
*/
GuiInterface.prototype.OnDisabledTemplatesChanged = function(msg)
{
this.selectionDirty[msg.player] = true;
};
GuiInterface.prototype.OnDisabledTechnologiesChanged = function(msg)
{
this.selectionDirty[msg.player] = true;
};
GuiInterface.prototype.SetSelectionDirty = function(player)
{
this.selectionDirty[player] = true;
};
GuiInterface.prototype.IsSelectionDirty = function(player)
{
return this.selectionDirty[player] || false;
};
GuiInterface.prototype.ResetSelectionDirty = function()
{
this.selectionDirty = {};
};
/**
* Add a timed notification.
* Warning: timed notifacations are serialised
* (to also display them on saved games or after a rejoin)
* so they should allways be added and deleted in a deterministic way.
*/
GuiInterface.prototype.AddTimeNotification = function(notification, duration = 10000)
{
let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
notification.endTime = duration + cmpTimer.GetTime();
notification.id = ++this.timeNotificationID;
// Let all players and observers receive the notification by default.
if (!notification.players)
{
notification.players = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager).GetAllPlayers();
notification.players[0] = -1;
}
this.timeNotifications.push(notification);
this.timeNotifications.sort((n1, n2) => n2.endTime - n1.endTime);
cmpTimer.SetTimeout(this.entity, IID_GuiInterface, "DeleteTimeNotification", duration, this.timeNotificationID);
return this.timeNotificationID;
};
GuiInterface.prototype.DeleteTimeNotification = function(notificationID)
{
this.timeNotifications = this.timeNotifications.filter(n => n.id != notificationID);
};
GuiInterface.prototype.GetTimeNotifications = function(player)
{
let time = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer).GetTime();
// Filter on players and time, since the delete timer might be executed with a delay.
return this.timeNotifications.filter(n => n.players.indexOf(player) != -1 && n.endTime > time);
};
GuiInterface.prototype.PushNotification = function(notification)
{
if (!notification.type || notification.type == "text")
this.AddTimeNotification(notification);
else
this.notifications.push(notification);
};
GuiInterface.prototype.GetNotifications = function()
{
let n = this.notifications;
this.notifications = [];
return n;
};
GuiInterface.prototype.GetAvailableFormations = function(player, wantedPlayer)
{
let cmpPlayer = QueryPlayerIDInterface(wantedPlayer);
if (!cmpPlayer)
return [];
return cmpPlayer.GetFormations();
};
GuiInterface.prototype.GetFormationRequirements = function(player, data)
{
return GetFormationRequirements(data.formationTemplate);
};
GuiInterface.prototype.CanMoveEntsIntoFormation = function(player, data)
{
return CanMoveEntsIntoFormation(data.ents, data.formationTemplate);
};
GuiInterface.prototype.GetFormationInfoFromTemplate = function(player, data)
{
let cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager);
let template = cmpTemplateManager.GetTemplate(data.templateName);
if (!template || !template.Formation)
return {};
return {
"name": template.Formation.FormationName,
"tooltip": template.Formation.DisabledTooltip || "",
"icon": template.Formation.Icon
};
};
GuiInterface.prototype.IsFormationSelected = function(player, data)
{
return data.ents.some(ent => {
let cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
return cmpUnitAI && cmpUnitAI.GetFormationTemplate() == data.formationTemplate;
});
};
GuiInterface.prototype.IsStanceSelected = function(player, data)
{
for (let ent of data.ents)
{
let cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
if (cmpUnitAI && cmpUnitAI.GetStanceName() == data.stance)
return true;
}
return false;
};
GuiInterface.prototype.GetAllBuildableEntities = function(player, cmd)
{
let buildableEnts = [];
for (let ent of cmd.entities)
{
let cmpBuilder = Engine.QueryInterface(ent, IID_Builder);
if (!cmpBuilder)
continue;
for (let building of cmpBuilder.GetEntitiesList())
if (buildableEnts.indexOf(building) == -1)
buildableEnts.push(building);
}
return buildableEnts;
};
GuiInterface.prototype.UpdateDisplayedPlayerColors = function(player, data)
{
let updateEntityColor = (iids, entities) => {
for (let ent of entities)
for (let iid of iids)
{
let cmp = Engine.QueryInterface(ent, iid);
if (cmp)
cmp.UpdateColor();
}
};
let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
let numPlayers = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager).GetNumPlayers();
for (let i = 1; i < numPlayers; ++i)
{
let cmpPlayer = QueryPlayerIDInterface(i, IID_Player);
if (!cmpPlayer)
continue;
cmpPlayer.SetDisplayDiplomacyColor(data.displayDiplomacyColors);
if (data.displayDiplomacyColors)
cmpPlayer.SetDiplomacyColor(data.displayedPlayerColors[i]);
updateEntityColor(data.showAllStatusBars && (i == player || player == -1) ?
[IID_Minimap, IID_RangeOverlayRenderer, IID_RallyPointRenderer, IID_StatusBars] :
[IID_Minimap, IID_RangeOverlayRenderer, IID_RallyPointRenderer],
cmpRangeManager.GetEntitiesByPlayer(i));
}
updateEntityColor([IID_Selectable, IID_StatusBars], data.selected);
Engine.QueryInterface(SYSTEM_ENTITY, IID_TerritoryManager).UpdateColors();
};
GuiInterface.prototype.SetSelectionHighlight = function(player, cmd)
{
// Cache of owner -> color map
let playerColors = {};
for (let ent of cmd.entities)
{
let cmpSelectable = Engine.QueryInterface(ent, IID_Selectable);
if (!cmpSelectable)
continue;
// Find the entity's owner's color.
let owner = INVALID_PLAYER;
let cmpOwnership = Engine.QueryInterface(ent, IID_Ownership);
if (cmpOwnership)
owner = cmpOwnership.GetOwner();
let color = playerColors[owner];
if (!color)
{
color = { "r": 1, "g": 1, "b": 1 };
let cmpPlayer = QueryPlayerIDInterface(owner);
if (cmpPlayer)
color = cmpPlayer.GetDisplayedColor();
playerColors[owner] = color;
}
cmpSelectable.SetSelectionHighlight({ "r": color.r, "g": color.g, "b": color.b, "a": cmd.alpha }, cmd.selected);
let cmpRangeOverlayManager = Engine.QueryInterface(ent, IID_RangeOverlayManager);
if (!cmpRangeOverlayManager || player != owner && player != INVALID_PLAYER)
continue;
cmpRangeOverlayManager.SetEnabled(cmd.selected, this.enabledVisualRangeOverlayTypes, false);
}
};
GuiInterface.prototype.EnableVisualRangeOverlayType = function(player, data)
{
this.enabledVisualRangeOverlayTypes[data.type] = data.enabled;
};
GuiInterface.prototype.GetEntitiesWithStatusBars = function()
{
return Array.from(this.entsWithAuraAndStatusBars);
};
GuiInterface.prototype.SetStatusBars = function(player, cmd)
{
let affectedEnts = new Set();
for (let ent of cmd.entities)
{
let cmpStatusBars = Engine.QueryInterface(ent, IID_StatusBars);
if (!cmpStatusBars)
continue;
cmpStatusBars.SetEnabled(cmd.enabled, cmd.showRank, cmd.showExperience);
let cmpAuras = Engine.QueryInterface(ent, IID_Auras);
if (!cmpAuras)
continue;
for (let name of cmpAuras.GetAuraNames())
{
if (!cmpAuras.GetOverlayIcon(name))
continue;
for (let e of cmpAuras.GetAffectedEntities(name))
affectedEnts.add(e);
if (cmd.enabled)
this.entsWithAuraAndStatusBars.add(ent);
else
this.entsWithAuraAndStatusBars.delete(ent);
}
}
for (let ent of affectedEnts)
{
let cmpStatusBars = Engine.QueryInterface(ent, IID_StatusBars);
if (cmpStatusBars)
cmpStatusBars.RegenerateSprites();
}
};
GuiInterface.prototype.SetRangeOverlays = function(player, cmd)
{
for (let ent of cmd.entities)
{
let cmpRangeOverlayManager = Engine.QueryInterface(ent, IID_RangeOverlayManager);
if (cmpRangeOverlayManager)
cmpRangeOverlayManager.SetEnabled(cmd.enabled, this.enabledVisualRangeOverlayTypes, true);
}
};
GuiInterface.prototype.GetPlayerEntities = function(player)
{
return Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager).GetEntitiesByPlayer(player);
};
GuiInterface.prototype.GetNonGaiaEntities = function()
{
return Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager).GetNonGaiaEntities();
};
/**
* Displays the rally points of a given list of entities (carried in cmd.entities).
*
* The 'cmd' object may carry its own x/z coordinate pair indicating the location where the rally point should
* be rendered, in order to support instantaneously rendering a rally point marker at a specified location
* instead of incurring a delay while PostNetworkCommand processes the set-rallypoint command (see input.js).
* If cmd doesn't carry a custom location, then the position to render the marker at will be read from the
* RallyPoint component.
*/
GuiInterface.prototype.DisplayRallyPoint = function(player, cmd)
{
let cmpPlayer = QueryPlayerIDInterface(player);
// If there are some rally points already displayed, first hide them.
for (let ent of this.entsRallyPointsDisplayed)
{
let cmpRallyPointRenderer = Engine.QueryInterface(ent, IID_RallyPointRenderer);
if (cmpRallyPointRenderer)
cmpRallyPointRenderer.SetDisplayed(false);
}
this.entsRallyPointsDisplayed = [];
// Show the rally points for the passed entities.
for (let ent of cmd.entities)
{
let cmpRallyPointRenderer = Engine.QueryInterface(ent, IID_RallyPointRenderer);
if (!cmpRallyPointRenderer)
continue;
// Entity must have a rally point component to display a rally point marker
// (regardless of whether cmd specifies a custom location).
let cmpRallyPoint = Engine.QueryInterface(ent, IID_RallyPoint);
if (!cmpRallyPoint)
continue;
// Verify the owner.
let cmpOwnership = Engine.QueryInterface(ent, IID_Ownership);
if (!(cmpPlayer && cmpPlayer.CanControlAllUnits()))
if (!cmpOwnership || cmpOwnership.GetOwner() != player)
continue;
// If the command was passed an explicit position, use that and
// override the real rally point position; otherwise use the real position.
let pos;
if (cmd.x && cmd.z)
pos = cmd;
else
// May return undefined if no rally point is set.
pos = cmpRallyPoint.GetPositions()[0];
if (pos)
{
// Only update the position if we changed it (cmd.queued is set).
// Note that Add-/SetPosition take a CFixedVector2D which has X/Y components, not X/Z.
if ("queued" in cmd)
{
if (cmd.queued == true)
cmpRallyPointRenderer.AddPosition(new Vector2D(pos.x, pos.z));
else
cmpRallyPointRenderer.SetPosition(new Vector2D(pos.x, pos.z));
}
else if (!cmpRallyPointRenderer.IsSet())
// Rebuild the renderer when not set (when reading saved game or in case of building update).
for (let posi of cmpRallyPoint.GetPositions())
cmpRallyPointRenderer.AddPosition(new Vector2D(posi.x, posi.z));
cmpRallyPointRenderer.SetDisplayed(true);
// Remember which entities have their rally points displayed so we can hide them again.
this.entsRallyPointsDisplayed.push(ent);
}
}
};
GuiInterface.prototype.AddTargetMarker = function(player, cmd)
{
let ent = Engine.AddLocalEntity(cmd.template);
if (!ent)
return;
let cmpPosition = Engine.QueryInterface(ent, IID_Position);
cmpPosition.JumpTo(cmd.x, cmd.z);
};
/**
* Display the building placement preview.
* cmd.template is the name of the entity template, or "" to disable the preview.
* cmd.x, cmd.z, cmd.angle give the location.
*
* Returns result object from CheckPlacement:
* {
* "success": true iff the placement is valid, else false
* "message": message to display in UI for invalid placement, else ""
* "parameters": parameters to use in the message
* "translateMessage": localisation info
* "translateParameters": localisation info
* "pluralMessage": we might return a plural translation instead (optional)
* "pluralCount": localisation info (optional)
* }
*/
GuiInterface.prototype.SetBuildingPlacementPreview = function(player, cmd)
{
let result = {
"success": false,
"message": "",
"parameters": {},
"translateMessage": false,
"translateParameters": []
};
if (!this.placementEntity || this.placementEntity[0] != cmd.template)
{
if (this.placementEntity)
Engine.DestroyEntity(this.placementEntity[1]);
if (cmd.template == "")
this.placementEntity = undefined;
else
this.placementEntity = [cmd.template, Engine.AddLocalEntity("preview|" + cmd.template)];
}
if (this.placementEntity)
{
let ent = this.placementEntity[1];
let pos = Engine.QueryInterface(ent, IID_Position);
if (pos)
{
pos.JumpTo(cmd.x, cmd.z);
pos.SetYRotation(cmd.angle);
}
let cmpOwnership = Engine.QueryInterface(ent, IID_Ownership);
cmpOwnership.SetOwner(player);
let cmpBuildRestrictions = Engine.QueryInterface(ent, IID_BuildRestrictions);
if (!cmpBuildRestrictions)
error("cmpBuildRestrictions not defined");
else
result = cmpBuildRestrictions.CheckPlacement();
let cmpRangeOverlayManager = Engine.QueryInterface(ent, IID_RangeOverlayManager);
if (cmpRangeOverlayManager)
cmpRangeOverlayManager.SetEnabled(true, this.enabledVisualRangeOverlayTypes);
// Set it to a red shade if this is an invalid location.
let cmpVisual = Engine.QueryInterface(ent, IID_Visual);
if (cmpVisual)
{
if (cmd.actorSeed !== undefined)
cmpVisual.SetActorSeed(cmd.actorSeed);
if (!result.success)
cmpVisual.SetShadingColor(1.4, 0.4, 0.4, 1);
else
cmpVisual.SetShadingColor(1, 1, 1, 1);
}
}
return result;
};
/**
* Previews the placement of a wall between cmd.start and cmd.end, or just the starting piece of a wall if cmd.end is not
* specified. Returns an object with information about the list of entities that need to be newly constructed to complete
* at least a part of the wall, or false if there are entities required to build at least part of the wall but none of
* them can be validly constructed.
*
* It's important to distinguish between three lists of entities that are at play here, because they may be subsets of one
* another depending on things like snapping and whether some of the entities inside them can be validly positioned.
* We have:
* - The list of entities that previews the wall. This list is usually equal to the entities required to construct the
* entire wall. However, if there is snapping to an incomplete tower (i.e. a foundation), it includes extra entities
* to preview the completed tower on top of its foundation.
*
* - The list of entities that need to be newly constructed to build the entire wall. This list is regardless of whether
* any of them can be validly positioned. The emphasishere here is on 'newly'; this list does not include any existing
* towers at either side of the wall that we snapped to. Or, more generally; it does not include any _entities_ that we
* snapped to; we might still snap to e.g. terrain, in which case the towers on either end will still need to be newly
* constructed.
*
* - The list of entities that need to be newly constructed to build at least a part of the wall. This list is the same
* as the one above, except that it is truncated at the first entity that cannot be validly positioned. This happens
* e.g. if the player tries to build a wall straight through an obstruction. Note that any entities that can be validly
* constructed but come after said first invalid entity are also truncated away.
*
* With this in mind, this method will return false if the second list is not empty, but the third one is. That is, if there
* were entities that are needed to build the wall, but none of them can be validly constructed. False is also returned in
* case of unexpected errors (typically missing components), and when clearing the preview by passing an empty wallset
* argument (see below). Otherwise, it will return an object with the following information:
*
* result: {
* 'startSnappedEnt': ID of the entity that we snapped to at the starting side of the wall. Currently only supports towers.
* 'endSnappedEnt': ID of the entity that we snapped to at the (possibly truncated) ending side of the wall. Note that this
* can only be set if no truncation of the second list occurs; if we snapped to an entity at the ending side
* but the wall construction was truncated before we could reach it, it won't be set here. Currently only
* supports towers.
* 'pieces': Array with the following data for each of the entities in the third list:
* [{
* 'template': Template name of the entity.
* 'x': X coordinate of the entity's position.
* 'z': Z coordinate of the entity's position.
* 'angle': Rotation around the Y axis of the entity (in radians).
* },
* ...]
* 'cost': { The total cost required for constructing all the pieces as listed above.
* 'food': ...,
* 'wood': ...,
* 'stone': ...,
* 'metal': ...,
* 'population': ...,
* }
* }
*
* @param cmd.wallSet Object holding the set of wall piece template names. Set to an empty value to clear the preview.
* @param cmd.start Starting point of the wall segment being created.
* @param cmd.end (Optional) Ending point of the wall segment being created. If not defined, it is understood that only
* the starting point of the wall is available at this time (e.g. while the player is still in the process
* of picking a starting point), and that therefore only the first entity in the wall (a tower) should be
* previewed.
* @param cmd.snapEntities List of candidate entities to snap the start and ending positions to.
*/
GuiInterface.prototype.SetWallPlacementPreview = function(player, cmd)
{
let wallSet = cmd.wallSet;
// Did the start position snap to anything?
// If we snapped, was it to an entity? If yes, hold that entity's ID.
let start = {
"pos": cmd.start,
"angle": 0,
"snapped": false,
"snappedEnt": INVALID_ENTITY
};
// Did the end position snap to anything?
// If we snapped, was it to an entity? If yes, hold that entity's ID.
let end = {
"pos": cmd.end,
"angle": 0,
"snapped": false,
"snappedEnt": INVALID_ENTITY
};
// --------------------------------------------------------------------------------
// Do some entity cache management and check for snapping.
if (!this.placementWallEntities)
this.placementWallEntities = {};
if (!wallSet)
{
// We're clearing the preview, clear the entity cache and bail.
for (let tpl in this.placementWallEntities)
{
for (let ent of this.placementWallEntities[tpl].entities)
Engine.DestroyEntity(ent);
this.placementWallEntities[tpl].numUsed = 0;
this.placementWallEntities[tpl].entities = [];
// Keep template data around.
}
return false;
}
for (let tpl in this.placementWallEntities)
{
for (let ent of this.placementWallEntities[tpl].entities)
{
let pos = Engine.QueryInterface(ent, IID_Position);
if (pos)
pos.MoveOutOfWorld();
}
this.placementWallEntities[tpl].numUsed = 0;
}
// Create cache entries for templates we haven't seen before.
for (let type in wallSet.templates)
{
if (type == "curves")
continue;
let tpl = wallSet.templates[type];
if (!(tpl in this.placementWallEntities))
{
this.placementWallEntities[tpl] = {
"numUsed": 0,
"entities": [],
"templateData": this.GetTemplateData(player, { "templateName": tpl }),
};
if (!this.placementWallEntities[tpl].templateData.wallPiece)
{
error("[SetWallPlacementPreview] No WallPiece component found for wall set template '" + tpl + "'");
return false;
}
}
}
// Prevent division by zero errors further on if the start and end positions are the same.
if (end.pos && (start.pos.x === end.pos.x && start.pos.z === end.pos.z))
end.pos = undefined;
// See if we need to snap the start and/or end coordinates to any of our list of snap entities. Note that, despite the list
// of snapping candidate entities, it might still snap to e.g. terrain features. Use the "ent" key in the returned snapping
// data to determine whether it snapped to an entity (if any), and to which one (see GetFoundationSnapData).
if (cmd.snapEntities)
{
// Value of 0.5 was determined through trial and error.
let snapRadius = this.placementWallEntities[wallSet.templates.tower].templateData.wallPiece.length * 0.5;
let startSnapData = this.GetFoundationSnapData(player, {
"x": start.pos.x,
"z": start.pos.z,
"template": wallSet.templates.tower,
"snapEntities": cmd.snapEntities,
"snapRadius": snapRadius,
});
if (startSnapData)
{
start.pos.x = startSnapData.x;
start.pos.z = startSnapData.z;
start.angle = startSnapData.angle;
start.snapped = true;
if (startSnapData.ent)
start.snappedEnt = startSnapData.ent;
}
if (end.pos)
{
let endSnapData = this.GetFoundationSnapData(player, {
"x": end.pos.x,
"z": end.pos.z,
"template": wallSet.templates.tower,
"snapEntities": cmd.snapEntities,
"snapRadius": snapRadius,
});
if (endSnapData)
{
end.pos.x = endSnapData.x;
end.pos.z = endSnapData.z;
end.angle = endSnapData.angle;
end.snapped = true;
if (endSnapData.ent)
end.snappedEnt = endSnapData.ent;
}
}
}
// Clear the single-building preview entity (we'll be rolling our own).
this.SetBuildingPlacementPreview(player, { "template": "" });
// --------------------------------------------------------------------------------
// Calculate wall placement and position preview entities.
let result = {
"pieces": [],
"cost": { "population": 0, "time": 0 }
};
for (let res of Resources.GetCodes())
result.cost[res] = 0;
let previewEntities = [];
if (end.pos)
// See helpers/Walls.js.
previewEntities = GetWallPlacement(this.placementWallEntities, wallSet, start, end);
// For wall placement, we may (and usually do) need to have wall pieces overlap each other more than would
// otherwise be allowed by their obstruction shapes. However, during this preview phase, this is not so much of
// an issue, because all preview entities have their obstruction components deactivated, meaning that their
// obstruction shapes do not register in the simulation and hence cannot affect it. This implies that the preview
// entities cannot be found to obstruct each other, which largely solves the issue of overlap between wall pieces.
// Note that they will still be obstructed by existing shapes in the simulation (that have the BLOCK_FOUNDATION
// flag set), which is what we want. The only exception to this is when snapping to existing towers (or
// foundations thereof); the wall segments that connect up to these will be found to be obstructed by the
// existing tower/foundation, and be shaded red to indicate that they cannot be placed there. To prevent this,
// we manually set the control group of the outermost wall pieces equal to those of the snapped-to towers, so
// that they are free from mutual obstruction (per definition of obstruction control groups). This is done by
// assigning them an extra "controlGroup" field, which we'll then set during the placement loop below.
// Additionally, in the situation that we're snapping to merely a foundation of a tower instead of a fully
// constructed one, we'll need an extra preview entity for the starting tower, which also must not be obstructed
// by the foundation it snaps to.
if (start.snappedEnt && start.snappedEnt != INVALID_ENTITY)
{
let startEntObstruction = Engine.QueryInterface(start.snappedEnt, IID_Obstruction);
if (previewEntities.length && startEntObstruction)
previewEntities[0].controlGroups = [startEntObstruction.GetControlGroup()];
// If we're snapping to merely a foundation, add an extra preview tower and also set it to the same control group.
let startEntState = this.GetEntityState(player, start.snappedEnt);
if (startEntState.foundation)
{
let cmpPosition = Engine.QueryInterface(start.snappedEnt, IID_Position);
if (cmpPosition)
previewEntities.unshift({
"template": wallSet.templates.tower,
"pos": start.pos,
"angle": cmpPosition.GetRotation().y,
"controlGroups": [startEntObstruction ? startEntObstruction.GetControlGroup() : undefined],
"excludeFromResult": true // Preview only, must not appear in the result.
});
}
}
else
{
// Didn't snap to an existing entity, add the starting tower manually. To prevent odd-looking rotation jumps
// when shift-clicking to build a wall, reuse the placement angle that was last seen on a validly positioned
// wall piece.
// To illustrate the last point, consider what happens if we used some constant instead, say, 0. Issuing the
// build command for a wall is asynchronous, so when the preview updates after shift-clicking, the wall piece
// foundations are not registered yet in the simulation. This means they cannot possibly be picked in the list
// of candidate entities for snapping. In the next preview update, we therefore hit this case, and would rotate
// the preview to 0 radians. Then, after one or two simulation updates or so, the foundations register and
// onSimulationUpdate in session.js updates the preview again. It first grabs a new list of snapping candidates,
// which this time does include the new foundations; so we snap to the entity, and rotate the preview back to
// the foundation's angle.
// The result is a noticeable rotation to 0 and back, which is undesirable. So, for a split second there until
// the simulation updates, we fake it by reusing the last angle and hope the player doesn't notice.
previewEntities.unshift({
"template": wallSet.templates.tower,
"pos": start.pos,
"angle": previewEntities.length ? previewEntities[0].angle : this.placementWallLastAngle
});
}
if (end.pos)
{
// Analogous to the starting side case above.
if (end.snappedEnt && end.snappedEnt != INVALID_ENTITY)
{
let endEntObstruction = Engine.QueryInterface(end.snappedEnt, IID_Obstruction);
// Note that it's possible for the last entity in previewEntities to be the same as the first, i.e. the
// same wall piece snapping to both a starting and an ending tower. And it might be more common than you would
// expect; the allowed overlap between wall segments and towers facilitates this to some degree. To deal with
// the possibility of dual initial control groups, we use a '.controlGroups' array rather than a single
// '.controlGroup' property. Note that this array can only ever have 0, 1 or 2 elements (checked at a later time).
if (previewEntities.length > 0 && endEntObstruction)
{
previewEntities[previewEntities.length - 1].controlGroups = previewEntities[previewEntities.length - 1].controlGroups || [];
previewEntities[previewEntities.length - 1].controlGroups.push(endEntObstruction.GetControlGroup());
}
// If we're snapping to a foundation, add an extra preview tower and also set it to the same control group.
let endEntState = this.GetEntityState(player, end.snappedEnt);
if (endEntState.foundation)
{
let cmpPosition = Engine.QueryInterface(end.snappedEnt, IID_Position);
if (cmpPosition)
previewEntities.push({
"template": wallSet.templates.tower,
"pos": end.pos,
"angle": cmpPosition.GetRotation().y,
"controlGroups": [endEntObstruction ? endEntObstruction.GetControlGroup() : undefined],
"excludeFromResult": true
});
}
}
else
previewEntities.push({
"template": wallSet.templates.tower,
"pos": end.pos,
"angle": previewEntities.length ? previewEntities[previewEntities.length - 1].angle : this.placementWallLastAngle
});
}
let cmpTerrain = Engine.QueryInterface(SYSTEM_ENTITY, IID_Terrain);
if (!cmpTerrain)
{
error("[SetWallPlacementPreview] System Terrain component not found");
return false;
}
let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
if (!cmpRangeManager)
{
error("[SetWallPlacementPreview] System RangeManager component not found");
return false;
}
// Loop through the preview entities, and construct the subset of them that need to be, and can be, validly constructed
// to build at least a part of the wall (meaning that the subset is truncated after the first entity that needs to be,
// but cannot validly be, constructed). See method-level documentation for more details.
let allPiecesValid = true;
// Number of entities that are required to build the entire wall, regardless of validity.
let numRequiredPieces = 0;
for (let i = 0; i < previewEntities.length; ++i)
{
let entInfo = previewEntities[i];
let ent = null;
let tpl = entInfo.template;
let tplData = this.placementWallEntities[tpl].templateData;
let entPool = this.placementWallEntities[tpl];
if (entPool.numUsed >= entPool.entities.length)
{
ent = Engine.AddLocalEntity("preview|" + tpl);
entPool.entities.push(ent);
}
else
ent = entPool.entities[entPool.numUsed];
if (!ent)
{
error("[SetWallPlacementPreview] Failed to allocate or reuse preview entity of template '" + tpl + "'");
continue;
}
// Move piece to right location.
// TODO: Consider reusing SetBuildingPlacementReview for this, enhanced to be able to deal with multiple entities.
let cmpPosition = Engine.QueryInterface(ent, IID_Position);
if (cmpPosition)
{
cmpPosition.JumpTo(entInfo.pos.x, entInfo.pos.z);
cmpPosition.SetYRotation(entInfo.angle);
// If this piece is a tower, then it should have a Y position that is at least as high as its surrounding pieces.
if (tpl === wallSet.templates.tower)
{
let terrainGroundPrev = null;
let terrainGroundNext = null;
if (i > 0)
terrainGroundPrev = cmpTerrain.GetGroundLevel(previewEntities[i - 1].pos.x, previewEntities[i - 1].pos.z);
if (i < previewEntities.length - 1)
terrainGroundNext = cmpTerrain.GetGroundLevel(previewEntities[i + 1].pos.x, previewEntities[i + 1].pos.z);
if (terrainGroundPrev != null || terrainGroundNext != null)
{
let targetY = Math.max(terrainGroundPrev, terrainGroundNext);
cmpPosition.SetHeightFixed(targetY);
}
}
}
let cmpObstruction = Engine.QueryInterface(ent, IID_Obstruction);
if (!cmpObstruction)
{
error("[SetWallPlacementPreview] Preview entity of template '" + tpl + "' does not have an Obstruction component");
continue;
}
// Assign any predefined control groups. Note that there can only be 0, 1 or 2 predefined control groups; if there are
// more, we've made a programming error. The control groups are assigned from the entInfo.controlGroups array on a
// first-come first-served basis; the first value in the array is always assigned as the primary control group, and
// any second value as the secondary control group.
// By default, we reset the control groups to their standard values. Remember that we're reusing entities; if we don't
// reset them, then an ending wall segment that was e.g. at one point snapped to an existing tower, and is subsequently
// reused as a non-snapped ending wall segment, would no longer be capable of being obstructed by the same tower it was
// once snapped to.
let primaryControlGroup = ent;
let secondaryControlGroup = INVALID_ENTITY;
if (entInfo.controlGroups && entInfo.controlGroups.length > 0)
{
if (entInfo.controlGroups.length > 2)
{
error("[SetWallPlacementPreview] Encountered preview entity of template '" + tpl + "' with more than 2 initial control groups");
break;
}
primaryControlGroup = entInfo.controlGroups[0];
if (entInfo.controlGroups.length > 1)
secondaryControlGroup = entInfo.controlGroups[1];
}
cmpObstruction.SetControlGroup(primaryControlGroup);
cmpObstruction.SetControlGroup2(secondaryControlGroup);
let validPlacement = false;
let cmpOwnership = Engine.QueryInterface(ent, IID_Ownership);
cmpOwnership.SetOwner(player);
// Check whether it's in a visible or fogged region.
// TODO: Should definitely reuse SetBuildingPlacementPreview, this is just straight up copy/pasta.
let visible = cmpRangeManager.GetLosVisibility(ent, player) != "hidden";
if (visible)
{
let cmpBuildRestrictions = Engine.QueryInterface(ent, IID_BuildRestrictions);
if (!cmpBuildRestrictions)
{
error("[SetWallPlacementPreview] cmpBuildRestrictions not defined for preview entity of template '" + tpl + "'");
continue;
}
// TODO: Handle results of CheckPlacement.
validPlacement = cmpBuildRestrictions && cmpBuildRestrictions.CheckPlacement().success;
// If a wall piece has two control groups, it's likely a segment that spans
// between two existing towers. To avoid placing a duplicate wall segment,
// check for collisions with entities that share both control groups.
if (validPlacement && entInfo.controlGroups && entInfo.controlGroups.length > 1)
validPlacement = cmpObstruction.CheckDuplicateFoundation();
}
allPiecesValid = allPiecesValid && validPlacement;
// The requirement below that all pieces so far have to have valid positions, rather than only this single one,
// ensures that no more foundations will be placed after a first invalidly-positioned piece. (It is possible
// for pieces past some invalidly-positioned ones to still have valid positions, e.g. if you drag a wall
// through and past an existing building).
// Additionally, the excludeFromResult flag is set for preview entities that were manually added to be placed
// on top of foundations of incompleted towers that we snapped to; they must not be part of the result.
if (!entInfo.excludeFromResult)
++numRequiredPieces;
if (allPiecesValid && !entInfo.excludeFromResult)
{
result.pieces.push({
"template": tpl,
"x": entInfo.pos.x,
"z": entInfo.pos.z,
"angle": entInfo.angle,
});
this.placementWallLastAngle = entInfo.angle;
// Grab the cost of this wall piece and add it up (note; preview entities don't have their Cost components
// copied over, so we need to fetch it from the template instead).
// TODO: We should really use a Cost object or at least some utility functions for this, this is mindless
// boilerplate that's probably duplicated in tons of places.
for (let res of Resources.GetCodes().concat(["population", "time"]))
result.cost[res] += tplData.cost[res];
}
let canAfford = true;
let cmpPlayer = QueryPlayerIDInterface(player, IID_Player);
if (cmpPlayer && cmpPlayer.GetNeededResources(result.cost))
canAfford = false;
let cmpVisual = Engine.QueryInterface(ent, IID_Visual);
if (cmpVisual)
{
if (!allPiecesValid || !canAfford)
cmpVisual.SetShadingColor(1.4, 0.4, 0.4, 1);
else
cmpVisual.SetShadingColor(1, 1, 1, 1);
}
++entPool.numUsed;
}
// If any were entities required to build the wall, but none of them could be validly positioned, return failure
// (see method-level documentation).
if (numRequiredPieces > 0 && result.pieces.length == 0)
return false;
if (start.snappedEnt && start.snappedEnt != INVALID_ENTITY)
result.startSnappedEnt = start.snappedEnt;
// We should only return that we snapped to an entity if all pieces up until that entity can be validly constructed,
// i.e. are included in result.pieces (see docs for the result object).
if (end.pos && end.snappedEnt && end.snappedEnt != INVALID_ENTITY && allPiecesValid)
result.endSnappedEnt = end.snappedEnt;
return result;
};
/**
* Given the current position {data.x, data.z} of an foundation of template data.template, returns the position and angle to snap
* it to (if necessary/useful).
*
* @param data.x The X position of the foundation to snap.
* @param data.z The Z position of the foundation to snap.
* @param data.template The template to get the foundation snapping data for.
* @param data.snapEntities Optional; list of entity IDs to snap to if {data.x, data.z} is within a circle of radius data.snapRadius
* around the entity. Only takes effect when used in conjunction with data.snapRadius.
* When this option is used and the foundation is found to snap to one of the entities passed in this list
* (as opposed to e.g. snapping to terrain features), then the result will contain an additional key "ent",
* holding the ID of the entity that was snapped to.
* @param data.snapRadius Optional; when used in conjunction with data.snapEntities, indicates the circle radius around an entity that
* {data.x, data.z} must be located within to have it snap to that entity.
*/
GuiInterface.prototype.GetFoundationSnapData = function(player, data)
{
let template = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager).GetTemplate(data.template);
if (!template)
{
warn("[GetFoundationSnapData] Failed to load template '" + data.template + "'");
return false;
}
if (data.snapEntities && data.snapRadius && data.snapRadius > 0)
{
// See if {data.x, data.z} is inside the snap radius of any of the snap entities; and if so, to which it is closest.
// (TODO: Break unlikely ties by choosing the lowest entity ID.)
let minDist2 = -1;
let minDistEntitySnapData = null;
let radius2 = data.snapRadius * data.snapRadius;
for (let ent of data.snapEntities)
{
let cmpPosition = Engine.QueryInterface(ent, IID_Position);
if (!cmpPosition || !cmpPosition.IsInWorld())
continue;
let pos = cmpPosition.GetPosition();
let dist2 = (data.x - pos.x) * (data.x - pos.x) + (data.z - pos.z) * (data.z - pos.z);
if (dist2 > radius2)
continue;
if (minDist2 < 0 || dist2 < minDist2)
{
minDist2 = dist2;
minDistEntitySnapData = {
"x": pos.x,
"z": pos.z,
"angle": cmpPosition.GetRotation().y,
"ent": ent
};
}
}
if (minDistEntitySnapData != null)
return minDistEntitySnapData;
}
if (data.snapToEdges)
{
let position = this.obstructionSnap.getPosition(data, template);
if (position)
return position;
}
if (template.BuildRestrictions.PlacementType == "shore")
{
let angle = GetDockAngle(template, data.x, data.z);
if (angle !== undefined)
return {
"x": data.x,
"z": data.z,
"angle": angle
};
}
return false;
};
GuiInterface.prototype.PlaySound = function(player, data)
{
if (!data.entity)
return;
PlaySound(data.name, data.entity);
};
/**
* Find any idle units.
*
* @param data.idleClasses Array of class names to include.
* @param data.prevUnit The previous idle unit, if calling a second time to iterate through units. May be left undefined.
* @param data.limit The number of idle units to return. May be left undefined (will return all idle units).
* @param data.excludeUnits Array of units to exclude.
*
* Returns an array of idle units.
* If multiple classes were supplied, and multiple items will be returned, the items will be sorted by class.
*/
GuiInterface.prototype.FindIdleUnits = function(player, data)
{
let idleUnits = [];
// The general case is that only the 'first' idle unit is required; filtering would examine every unit.
// This loop imitates a grouping/aggregation on the first matching idle class.
let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
for (let entity of cmpRangeManager.GetEntitiesByPlayer(player))
{
let filtered = this.IdleUnitFilter(entity, data.idleClasses, data.excludeUnits);
if (!filtered.idle)
continue;
// If the entity is in the 'current' (first, 0) bucket on a resumed search, it must be after the "previous" unit, if any.
// By adding to the 'end', there is no pause if the series of units loops.
let bucket = filtered.bucket;
if (bucket == 0 && data.prevUnit && entity <= data.prevUnit)
bucket = data.idleClasses.length;
if (!idleUnits[bucket])
idleUnits[bucket] = [];
idleUnits[bucket].push(entity);
// If enough units have been collected in the first bucket, go ahead and return them.
if (data.limit && bucket == 0 && idleUnits[0].length == data.limit)
return idleUnits[0];
}
let reduced = idleUnits.reduce((prev, curr) => prev.concat(curr), []);
if (data.limit && reduced.length > data.limit)
return reduced.slice(0, data.limit);
return reduced;
};
/**
* Discover if the player has idle units.
*
* @param data.idleClasses Array of class names to include.
* @param data.excludeUnits Array of units to exclude.
*
* Returns a boolean of whether the player has any idle units
*/
GuiInterface.prototype.HasIdleUnits = function(player, data)
{
let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
return cmpRangeManager.GetEntitiesByPlayer(player).some(unit => this.IdleUnitFilter(unit, data.idleClasses, data.excludeUnits).idle);
};
/**
* Whether to filter an idle unit
*
* @param unit The unit to filter.
* @param idleclasses Array of class names to include.
* @param excludeUnits Array of units to exclude.
*
* Returns an object with the following fields:
* - idle - true if the unit is considered idle by the filter, false otherwise.
* - bucket - if idle, set to the index of the first matching idle class, undefined otherwise.
*/
GuiInterface.prototype.IdleUnitFilter = function(unit, idleClasses, excludeUnits)
{
let cmpUnitAI = Engine.QueryInterface(unit, IID_UnitAI);
if (!cmpUnitAI || !cmpUnitAI.IsIdle() || cmpUnitAI.IsGarrisoned())
return { "idle": false };
let cmpIdentity = Engine.QueryInterface(unit, IID_Identity);
if (!cmpIdentity)
return { "idle": false };
let bucket = idleClasses.findIndex(elem => MatchesClassList(cmpIdentity.GetClassesList(), elem));
if (bucket == -1 || excludeUnits.indexOf(unit) > -1)
return { "idle": false };
return { "idle": true, "bucket": bucket };
};
GuiInterface.prototype.GetTradingRouteGain = function(player, data)
{
if (!data.firstMarket || !data.secondMarket)
return null;
return CalculateTraderGain(data.firstMarket, data.secondMarket, data.template);
};
GuiInterface.prototype.GetTradingDetails = function(player, data)
{
let cmpEntityTrader = Engine.QueryInterface(data.trader, IID_Trader);
if (!cmpEntityTrader || !cmpEntityTrader.CanTrade(data.target))
return null;
let firstMarket = cmpEntityTrader.GetFirstMarket();
let secondMarket = cmpEntityTrader.GetSecondMarket();
let result = null;
if (data.target === firstMarket)
{
result = {
"type": "is first",
"hasBothMarkets": cmpEntityTrader.HasBothMarkets()
};
if (cmpEntityTrader.HasBothMarkets())
result.gain = cmpEntityTrader.GetGoods().amount;
}
else if (data.target === secondMarket)
result = {
"type": "is second",
"gain": cmpEntityTrader.GetGoods().amount,
};
else if (!firstMarket)
result = { "type": "set first" };
else if (!secondMarket)
result = {
"type": "set second",
"gain": cmpEntityTrader.CalculateGain(firstMarket, data.target),
};
else
result = { "type": "set first" };
return result;
};
GuiInterface.prototype.CanAttack = function(player, data)
{
let cmpAttack = Engine.QueryInterface(data.entity, IID_Attack);
return cmpAttack && cmpAttack.CanAttack(data.target, data.types || undefined);
};
/*
* Returns batch build time.
*/
GuiInterface.prototype.GetBatchTime = function(player, data)
{
let cmpProductionQueue = Engine.QueryInterface(data.entity, IID_ProductionQueue);
if (!cmpProductionQueue)
return 0;
return cmpProductionQueue.GetBatchTime(data.batchSize);
};
GuiInterface.prototype.IsMapRevealed = function(player)
{
return Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager).GetLosRevealAll(player);
};
GuiInterface.prototype.SetPathfinderDebugOverlay = function(player, enabled)
{
Engine.QueryInterface(SYSTEM_ENTITY, IID_Pathfinder).SetDebugOverlay(enabled);
};
GuiInterface.prototype.SetPathfinderHierDebugOverlay = function(player, enabled)
{
Engine.QueryInterface(SYSTEM_ENTITY, IID_Pathfinder).SetHierDebugOverlay(enabled);
};
GuiInterface.prototype.SetObstructionDebugOverlay = function(player, enabled)
{
Engine.QueryInterface(SYSTEM_ENTITY, IID_ObstructionManager).SetDebugOverlay(enabled);
};
GuiInterface.prototype.SetMotionDebugOverlay = function(player, data)
{
for (let ent of data.entities)
{
let cmpUnitMotion = Engine.QueryInterface(ent, IID_UnitMotion);
if (cmpUnitMotion)
cmpUnitMotion.SetDebugOverlay(data.enabled);
}
};
GuiInterface.prototype.SetRangeDebugOverlay = function(player, enabled)
{
Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager).SetDebugOverlay(enabled);
};
GuiInterface.prototype.GetTraderNumber = function(player)
{
let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
let traders = cmpRangeManager.GetEntitiesByPlayer(player).filter(e => Engine.QueryInterface(e, IID_Trader));
let landTrader = { "total": 0, "trading": 0, "garrisoned": 0 };
let shipTrader = { "total": 0, "trading": 0 };
for (let ent of traders)
{
let cmpIdentity = Engine.QueryInterface(ent, IID_Identity);
let cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
if (!cmpIdentity || !cmpUnitAI)
continue;
if (cmpIdentity.HasClass("Ship"))
{
++shipTrader.total;
if (cmpUnitAI.order && cmpUnitAI.order.type == "Trade")
++shipTrader.trading;
}
else
{
++landTrader.total;
if (cmpUnitAI.order && cmpUnitAI.order.type == "Trade")
++landTrader.trading;
if (cmpUnitAI.order && cmpUnitAI.order.type == "Garrison")
{
let holder = cmpUnitAI.order.data.target;
let cmpHolderUnitAI = Engine.QueryInterface(holder, IID_UnitAI);
if (cmpHolderUnitAI && cmpHolderUnitAI.order && cmpHolderUnitAI.order.type == "Trade")
++landTrader.garrisoned;
}
}
}
return { "landTrader": landTrader, "shipTrader": shipTrader };
};
GuiInterface.prototype.GetTradingGoods = function(player)
{
let cmpPlayer = QueryPlayerIDInterface(player);
if (!cmpPlayer)
return [];
return cmpPlayer.GetTradingGoods();
};
GuiInterface.prototype.OnGlobalEntityRenamed = function(msg)
{
this.renamedEntities.push(msg);
};
/**
* List the GuiInterface functions that can be safely called by GUI scripts.
* (GUI scripts are non-deterministic and untrusted, so these functions must be
* appropriately careful. They are called with a first argument "player", which is
* trusted and indicates the player associated with the current client; no data should
* be returned unless this player is meant to be able to see it.)
*/
let exposedFunctions = {
"GetSimulationState": 1,
"GetExtendedSimulationState": 1,
"GetInitAttributes": 1,
"GetReplayMetadata": 1,
"GetCampaignGameEndData": 1,
"GetRenamedEntities": 1,
"ClearRenamedEntities": 1,
"GetEntityState": 1,
"GetMultipleEntityStates": 1,
"GetAverageRangeForBuildings": 1,
"GetTemplateData": 1,
"IsTechnologyResearched": 1,
"CheckTechnologyRequirements": 1,
"GetStartedResearch": 1,
"GetBattleState": 1,
"GetIncomingAttacks": 1,
"GetNeededResources": 1,
"GetNotifications": 1,
"GetTimeNotifications": 1,
"GetAvailableFormations": 1,
"GetFormationRequirements": 1,
"CanMoveEntsIntoFormation": 1,
"IsFormationSelected": 1,
"GetFormationInfoFromTemplate": 1,
"IsStanceSelected": 1,
"UpdateDisplayedPlayerColors": 1,
"SetSelectionHighlight": 1,
"GetAllBuildableEntities": 1,
"SetStatusBars": 1,
"GetPlayerEntities": 1,
"GetNonGaiaEntities": 1,
"DisplayRallyPoint": 1,
"AddTargetMarker": 1,
"SetBuildingPlacementPreview": 1,
"SetWallPlacementPreview": 1,
"GetFoundationSnapData": 1,
"PlaySound": 1,
"FindIdleUnits": 1,
"HasIdleUnits": 1,
"GetTradingRouteGain": 1,
"GetTradingDetails": 1,
"CanAttack": 1,
"GetBatchTime": 1,
"IsMapRevealed": 1,
"SetPathfinderDebugOverlay": 1,
"SetPathfinderHierDebugOverlay": 1,
"SetObstructionDebugOverlay": 1,
"SetMotionDebugOverlay": 1,
"SetRangeDebugOverlay": 1,
"EnableVisualRangeOverlayType": 1,
"SetRangeOverlays": 1,
"GetTraderNumber": 1,
"GetTradingGoods": 1,
"IsTemplateModified": 1,
"ResetTemplateModified": 1,
"IsSelectionDirty": 1,
"ResetSelectionDirty": 1
};
GuiInterface.prototype.ScriptCall = function(player, name, args)
{
if (exposedFunctions[name])
return this[name](player, args);
throw new Error("Invalid GuiInterface Call name \"" + name + "\"");
};
Engine.RegisterSystemComponentType(IID_GuiInterface, "GuiInterface", GuiInterface);
Index: ps/trunk/binaries/data/mods/public/simulation/components/ResourceGatherer.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/components/ResourceGatherer.js (revision 24988)
+++ ps/trunk/binaries/data/mods/public/simulation/components/ResourceGatherer.js (revision 24989)
@@ -1,426 +1,397 @@
function ResourceGatherer() {}
ResourceGatherer.prototype.Schema =
"Lets the unit gather resources from entities that have the ResourceSupply component." +
"" +
"2.0" +
"1.0" +
"" +
"1" +
"3" +
"3" +
"2" +
"" +
"" +
"10" +
"10" +
"10" +
"10" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
- Resources.BuildSchema("positiveDecimal", ["treasure"], true) +
+ Resources.BuildSchema("positiveDecimal", [], true) +
"" +
"" +
Resources.BuildSchema("positiveDecimal") +
"";
ResourceGatherer.prototype.Init = function()
{
this.capacities = {};
this.carrying = {}; // { generic type: integer amount currently carried }
// (Note that this component supports carrying multiple types of resources,
// each with an independent capacity, but the rest of the game currently
// ensures and assumes we'll only be carrying one type at once)
// The last exact type gathered, so we can render appropriate props
this.lastCarriedType = undefined; // { generic, specific }
};
/**
* Returns data about what resources the unit is currently carrying,
* in the form [ {"type":"wood", "amount":7, "max":10} ]
*/
ResourceGatherer.prototype.GetCarryingStatus = function()
{
let ret = [];
for (let type in this.carrying)
{
ret.push({
"type": type,
"amount": this.carrying[type],
"max": +this.GetCapacity(type)
});
}
return ret;
};
/**
* Used to instantly give resources to unit
* @param resources The same structure as returned form GetCarryingStatus
*/
ResourceGatherer.prototype.GiveResources = function(resources)
{
for (let resource of resources)
this.carrying[resource.type] = +resource.amount;
Engine.PostMessage(this.entity, MT_ResourceCarryingChanged, { "to": this.GetCarryingStatus() });
};
/**
* Returns the generic type of one particular resource this unit is
* currently carrying, or undefined if none.
*/
ResourceGatherer.prototype.GetMainCarryingType = function()
{
// Return the first key, if any
for (let type in this.carrying)
return type;
return undefined;
};
/**
* Returns the exact resource type we last picked up, as long as
* we're still carrying something similar enough, in the form
* { generic, specific }
*/
ResourceGatherer.prototype.GetLastCarriedType = function()
{
if (this.lastCarriedType && this.lastCarriedType.generic in this.carrying)
return this.lastCarriedType;
return undefined;
};
ResourceGatherer.prototype.SetLastCarriedType = function(lastCarriedType)
{
this.lastCarriedType = lastCarriedType;
};
// Since this code is very performancecritical and applying technologies quite slow, cache it.
ResourceGatherer.prototype.RecalculateGatherRates = function()
{
this.baseSpeed = ApplyValueModificationsToEntity("ResourceGatherer/BaseSpeed", +this.template.BaseSpeed, this.entity);
this.rates = {};
for (let r in this.template.Rates)
{
let type = r.split(".");
- if (type[0] != "treasure" && type.length > 1 && !Resources.GetResource(type[0]).subtypes[type[1]])
+ if (!Resources.GetResource(type[0]).subtypes[type[1]])
{
error("Resource subtype not found: " + type[0] + "." + type[1]);
continue;
}
let rate = ApplyValueModificationsToEntity("ResourceGatherer/Rates/" + r, +this.template.Rates[r], this.entity);
this.rates[r] = rate * this.baseSpeed;
}
};
ResourceGatherer.prototype.RecalculateCapacities = function()
{
this.capacities = {};
for (let r in this.template.Capacities)
this.capacities[r] = ApplyValueModificationsToEntity("ResourceGatherer/Capacities/" + r, +this.template.Capacities[r], this.entity);
};
ResourceGatherer.prototype.RecalculateCapacity = function(type)
{
if (type in this.capacities)
this.capacities[type] = ApplyValueModificationsToEntity("ResourceGatherer/Capacities/" + type, +this.template.Capacities[type], this.entity);
};
ResourceGatherer.prototype.GetGatherRates = function()
{
return this.rates;
};
ResourceGatherer.prototype.GetGatherRate = function(resourceType)
{
if (!this.template.Rates[resourceType])
return 0;
return this.rates[resourceType];
};
ResourceGatherer.prototype.GetCapacity = function(resourceType)
{
if (!this.template.Capacities[resourceType])
return 0;
return this.capacities[resourceType];
};
ResourceGatherer.prototype.GetRange = function()
{
return { "max": +this.template.MaxDistance, "min": 0 };
// maybe this should depend on the unit or target or something?
};
/**
- * Try to gather treasure
- * @return 'true' if treasure is successfully gathered, otherwise 'false'
- */
-ResourceGatherer.prototype.TryInstantGather = function(target)
-{
- let cmpResourceSupply = Engine.QueryInterface(target, IID_ResourceSupply);
- let type = cmpResourceSupply.GetType();
-
- if (type.generic != "treasure")
- return false;
-
- let status = cmpResourceSupply.TakeResources(cmpResourceSupply.GetCurrentAmount());
-
- let cmpPlayer = QueryOwnerInterface(this.entity, IID_Player);
- if (cmpPlayer)
- cmpPlayer.AddResource(type.specific, status.amount);
-
- let cmpStatisticsTracker = QueryOwnerInterface(this.entity, IID_StatisticsTracker);
- if (cmpStatisticsTracker)
- cmpStatisticsTracker.IncreaseTreasuresCollectedCounter();
-
- let cmpTrigger = Engine.QueryInterface(SYSTEM_ENTITY, IID_Trigger);
- if (cmpTrigger && cmpPlayer)
- cmpTrigger.CallEvent("TreasureCollected", { "player": cmpPlayer.GetPlayerID(), "type": type.specific, "amount": status.amount });
-
- return true;
-};
-
-/**
* Gather from the target entity. This should only be called after a successful range check,
* and if the target has a compatible ResourceSupply.
* Call interval will be determined by gather rate, so always gather 1 amount when called.
*/
ResourceGatherer.prototype.PerformGather = function(target)
{
if (!this.GetTargetGatherRate(target))
return { "exhausted": true };
let gatherAmount = 1;
let cmpResourceSupply = Engine.QueryInterface(target, IID_ResourceSupply);
let type = cmpResourceSupply.GetType();
// Initialise the carried count if necessary
if (!this.carrying[type.generic])
this.carrying[type.generic] = 0;
// Find the maximum so we won't exceed our capacity
let maxGathered = this.GetCapacity(type.generic) - this.carrying[type.generic];
let status = cmpResourceSupply.TakeResources(Math.min(gatherAmount, maxGathered));
this.carrying[type.generic] += status.amount;
this.lastCarriedType = type;
// Update stats of how much the player collected.
// (We have to do it here rather than at the dropsite, because we
// need to know what subtype it was)
let cmpStatisticsTracker = QueryOwnerInterface(this.entity, IID_StatisticsTracker);
if (cmpStatisticsTracker)
cmpStatisticsTracker.IncreaseResourceGatheredCounter(type.generic, status.amount, type.specific);
Engine.PostMessage(this.entity, MT_ResourceCarryingChanged, { "to": this.GetCarryingStatus() });
return {
"amount": status.amount,
"exhausted": status.exhausted,
"filled": this.carrying[type.generic] >= this.GetCapacity(type.generic)
};
};
/**
* Compute the amount of resources collected per second from the target.
* Returns 0 if resources cannot be collected (e.g. the target doesn't
* exist, or is the wrong type).
*/
ResourceGatherer.prototype.GetTargetGatherRate = function(target)
{
let cmpResourceSupply = QueryMiragedInterface(target, IID_ResourceSupply);
if (!cmpResourceSupply)
return 0;
let type = cmpResourceSupply.GetType();
let rate = 0;
if (type.specific)
rate = this.GetGatherRate(type.generic+"."+type.specific);
if (rate == 0 && type.generic)
rate = this.GetGatherRate(type.generic);
if ("Mirages" in cmpResourceSupply)
return rate;
// Apply diminishing returns with more gatherers, for e.g. infinite farms. For most resources this has no effect
// (GetDiminishingReturns will return null). We can assume that for resources that are miraged this is the case
// (else just add the diminishing returns data to the mirage data and remove the early return above)
let diminishingReturns = cmpResourceSupply.GetDiminishingReturns();
if (diminishingReturns)
rate *= diminishingReturns;
return rate;
};
/**
* Returns whether this unit can carry more of the given type of resource.
* (This ignores whether the unit is actually able to gather that
* resource type or not.)
*/
ResourceGatherer.prototype.CanCarryMore = function(type)
{
let amount = this.carrying[type] || 0;
return amount < this.GetCapacity(type);
};
ResourceGatherer.prototype.IsCarrying = function(type)
{
let amount = this.carrying[type] || 0;
return amount > 0;
};
/**
* Returns whether this unit is carrying any resources of a type that is
* not the requested type. (This is to support cases where the unit is
* only meant to be able to carry one type at once.)
*/
ResourceGatherer.prototype.IsCarryingAnythingExcept = function(exceptedType)
{
for (let type in this.carrying)
if (type != exceptedType)
return true;
return false;
};
/**
* Transfer our carried resources to our owner immediately.
* Only resources of the appropriate types will be transferred.
* (This should typically be called after reaching a dropsite.)
*
* @param {number} target - The target entity ID to drop resources at.
*/
ResourceGatherer.prototype.CommitResources = function(target)
{
let cmpResourceDropsite = Engine.QueryInterface(target, IID_ResourceDropsite);
if (!cmpResourceDropsite)
return;
let change = cmpResourceDropsite.ReceiveResources(this.carrying, this.entity);
let changed = false;
for (let type in change)
{
this.carrying[type] -= change[type];
if (this.carrying[type] == 0)
delete this.carrying[type];
changed = true;
}
if (changed)
Engine.PostMessage(this.entity, MT_ResourceCarryingChanged, { "to": this.GetCarryingStatus() });
};
/**
* Drop all currently-carried resources.
* (Currently they just vanish after being dropped - we don't bother depositing
* them onto the ground.)
*/
ResourceGatherer.prototype.DropResources = function()
{
this.carrying = {};
Engine.PostMessage(this.entity, MT_ResourceCarryingChanged, { "to": this.GetCarryingStatus() });
};
/**
* @param {string} type - A generic resource type.
*/
ResourceGatherer.prototype.AddToPlayerCounter = function(type)
{
// We need to be removed from the player counter first.
if (this.lastGathered)
return;
let cmpPlayer = QueryOwnerInterface(this.entity, IID_Player);
if (cmpPlayer)
cmpPlayer.AddResourceGatherer(type);
this.lastGathered = type;
};
/**
* @param {number} playerid - Optionally a player ID.
*/
ResourceGatherer.prototype.RemoveFromPlayerCounter = function(playerid)
{
if (!this.lastGathered)
return;
let cmpPlayer = playerid != undefined ?
QueryPlayerIDInterface(playerid) :
QueryOwnerInterface(this.entity, IID_Player);
if (cmpPlayer)
cmpPlayer.RemoveResourceGatherer(this.lastGathered);
delete this.lastGathered;
};
// Since we cache gather rates, we need to make sure we update them when tech changes.
// and when our owner change because owners can had different techs.
ResourceGatherer.prototype.OnValueModification = function(msg)
{
if (msg.component != "ResourceGatherer")
return;
// NB: at the moment, 0 A.D. always uses the fast path, the other is mod support.
if (msg.valueNames.length === 1)
{
if (msg.valueNames[0].indexOf("Capacities") !== -1)
this.RecalculateCapacity(msg.valueNames[0].substr(28));
else
this.RecalculateGatherRates();
}
else
{
this.RecalculateGatherRates();
this.RecalculateCapacities();
}
};
ResourceGatherer.prototype.OnOwnershipChanged = function(msg)
{
if (msg.to == INVALID_PLAYER)
{
this.RemoveFromPlayerCounter(msg.from);
return;
}
this.RecalculateGatherRates();
this.RecalculateCapacities();
};
ResourceGatherer.prototype.OnGlobalInitGame = function(msg)
{
this.RecalculateGatherRates();
this.RecalculateCapacities();
};
ResourceGatherer.prototype.OnMultiplierChanged = function(msg)
{
let cmpPlayer = QueryOwnerInterface(this.entity, IID_Player);
if (cmpPlayer && msg.player == cmpPlayer.GetPlayerID())
this.RecalculateGatherRates();
};
Engine.RegisterComponentType(IID_ResourceGatherer, "ResourceGatherer", ResourceGatherer);
Index: ps/trunk/binaries/data/mods/public/simulation/components/ResourceSupply.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/components/ResourceSupply.js (revision 24988)
+++ ps/trunk/binaries/data/mods/public/simulation/components/ResourceSupply.js (revision 24989)
@@ -1,471 +1,471 @@
function ResourceSupply() {}
ResourceSupply.prototype.Schema =
"Provides a supply of one particular type of resource." +
"" +
"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) +
+ Resources.BuildChoicesSchema(true) +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"alive" +
"dead" +
"gathered" +
"notGathered" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"";
ResourceSupply.prototype.Init = function()
{
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.Max);
};
ResourceSupply.prototype.GetKillBeforeGather = function()
{
return this.template.KillBeforeGather == "true";
};
ResourceSupply.prototype.GetMaxAmount = function()
{
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();
let oldAmount = this.amount;
this.amount = Math.min(Math.max(oldAmount + change, 0), this.maxAmount);
// Remove entities that have been exhausted.
if (this.amount == 0)
Engine.DestroyEntity(this.entity);
let actualChange = this.amount - oldAmount;
if (actualChange != 0)
{
Engine.PostMessage(this.entity, MT_ResourceSupplyChanged, {
"from": oldAmount,
"to": this.amount
});
this.CheckTimers();
}
return actualChange;
};
/**
* @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();
};
/**
* 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/Treasure.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/components/Treasure.js (nonexistent)
+++ ps/trunk/binaries/data/mods/public/simulation/components/Treasure.js (revision 24989)
@@ -0,0 +1,111 @@
+function Treasure() {}
+
+Treasure.prototype.Schema =
+ "Provides a bonus when taken. E.g. a supply of resources." +
+ "" +
+ "1000" +
+ "" +
+ "1000" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ Resources.BuildSchema("positiveDecimal") +
+ "" +
+ "";
+
+Treasure.prototype.Init = function()
+{
+};
+
+Treasure.prototype.ComputeReward = function()
+{
+ for (let resource in this.template.Resources)
+ {
+ let amount = ApplyValueModificationsToEntity("Treasure/Resources/" + resource, this.template.Resources[resource], this.entity);
+ if (!amount)
+ continue;
+ if (!this.resources)
+ this.resources = {};
+ this.resources[resource] = amount;
+ }
+};
+
+/**
+ * @return {Object} - The resources given by this treasure.
+ */
+Treasure.prototype.Resources = function()
+{
+ return this.resources || {};
+};
+
+/**
+ * @return {number} - The time in miliseconds it takes to collect this treasure.
+ */
+Treasure.prototype.CollectionTime = function()
+{
+ return +this.template.CollectTime;
+};
+
+/**
+ * @param {number} entity - The entity collecting us.
+ * @return {boolean} - Whether the reward was granted.
+ */
+Treasure.prototype.Reward = function(entity)
+{
+ if (this.isTaken)
+ return false;
+
+ let cmpPlayer = QueryOwnerInterface(entity);
+ if (!cmpPlayer)
+ return false;
+
+ let cmpFogging = Engine.QueryInterface(this.entity, IID_Fogging);
+ if (cmpFogging)
+ cmpFogging.Activate();
+
+ if (this.resources)
+ cmpPlayer.AddResources(this.resources);
+
+ let cmpStatisticsTracker = QueryOwnerInterface(this.entity, IID_StatisticsTracker);
+ if (cmpStatisticsTracker)
+ cmpStatisticsTracker.IncreaseTreasuresCollectedCounter();
+
+ let cmpTrigger = Engine.QueryInterface(SYSTEM_ENTITY, IID_Trigger);
+ cmpTrigger.CallEvent("TreasureCollected", {
+ "player": cmpPlayer.GetPlayerID(),
+ "treasure": this.entity
+ });
+
+ this.isTaken = true;
+ Engine.DestroyEntity(this.entity);
+ return true;
+};
+
+/**
+ * We might live long enough for a collecting entity
+ * to find us again after taking us.
+ * @return {boolean} - Whether we are taken already.
+ */
+Treasure.prototype.IsAvailable = function()
+{
+ return !this.isTaken;
+};
+
+Treasure.prototype.OnOwnershipChanged = function(msg)
+{
+ if (msg.to != INVALID_PLAYER)
+ this.ComputeReward();
+};
+
+Treasure.prototype.OnValueModification = function(msg)
+{
+ if (msg.component != "Treasure")
+ return;
+ this.ComputeReward();
+};
+
+Engine.RegisterComponentType(IID_Treasure, "Treasure", Treasure);
Property changes on: ps/trunk/binaries/data/mods/public/simulation/components/Treasure.js
___________________________________________________________________
Added: svn:eol-style
## -0,0 +1 ##
+native
\ No newline at end of property
Added: svn:mime-type
## -0,0 +1 ##
+text/plain
\ No newline at end of property
Index: ps/trunk/binaries/data/mods/public/simulation/components/TreasureCollecter.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/components/TreasureCollecter.js (nonexistent)
+++ ps/trunk/binaries/data/mods/public/simulation/components/TreasureCollecter.js (revision 24989)
@@ -0,0 +1,119 @@
+function TreasureCollecter() {}
+
+TreasureCollecter.prototype.Schema =
+ "Defines the treasure collecting abilities." +
+ "" +
+ "2.0" +
+ "" +
+ "" +
+ "" +
+ "";
+
+TreasureCollecter.prototype.Init = function()
+{
+};
+
+/**
+ * @return {Object} - Min/Max range at which this entity can claim a treasure.
+ */
+TreasureCollecter.prototype.GetRange = function()
+{
+ return { "min": 0, "max": +this.template.MaxDistance };
+};
+
+/**
+ * @param {number} target - Entity ID of the target.
+ * @return {boolean} - Whether we can collect from the target.
+ */
+TreasureCollecter.prototype.CanCollect = function(target)
+{
+ let cmpTreasure = Engine.QueryInterface(target, IID_Treasure);
+ return cmpTreasure && cmpTreasure.IsAvailable();
+};
+
+/**
+ * @param {number} target - The target to collect.
+ * @param {number} callerIID - The IID to notify on specific events.
+ *
+ * @return {boolean} - Whether we started collecting.
+ */
+TreasureCollecter.prototype.StartCollecting = function(target, callerIID)
+{
+ if (this.target)
+ this.StopCollecting();
+
+ let cmpTreasure = Engine.QueryInterface(target, IID_Treasure);
+ if (!cmpTreasure || !cmpTreasure.IsAvailable())
+ return false;
+
+ this.target = target;
+ this.callerIID = callerIID;
+
+ // ToDo: Implement rate modifiers.
+ let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
+ this.timer = cmpTimer.SetTimeout(this.entity, IID_TreasureCollecter, "CollectTreasure", cmpTreasure.CollectionTime(), null);
+
+ return true;
+};
+
+/**
+ * @param {string} reason - The reason why we stopped collecting, used to notify the caller.
+ */
+TreasureCollecter.prototype.StopCollecting = function(reason)
+{
+ if (this.timer)
+ {
+ let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
+ cmpTimer.CancelTimer(this.timer);
+ delete this.timer;
+ }
+ delete this.target;
+
+ // The callerIID component may start gathering again,
+ // replacing the callerIID, which gets deleted after
+ // the callerIID has finished. Hence save the data.
+ let callerIID = this.callerIID;
+ delete this.callerIID;
+
+ if (reason && callerIID)
+ {
+ let component = Engine.QueryInterface(this.entity, callerIID);
+ if (component)
+ component.ProcessMessage(reason, null);
+ }
+};
+
+/**
+ * @params - Data and lateness are unused.
+ */
+TreasureCollecter.prototype.CollectTreasure = function(data, lateness)
+{
+ let cmpTreasure = Engine.QueryInterface(this.target, IID_Treasure);
+ if (!cmpTreasure || !cmpTreasure.IsAvailable())
+ {
+ this.StopCollecting("TargetInvalidated");
+ return;
+ }
+
+ if (!this.IsTargetInRange(this.target))
+ {
+ this.StopCollecting("OutOfRange");
+ return;
+ }
+
+ cmpTreasure.Reward(this.entity);
+ this.StopCollecting("TargetInvalidated");
+};
+
+/**
+ * @param {number} - The entity ID of the target to check.
+ * @return {boolean} - Whether this entity is in range of its target.
+ */
+TreasureCollecter.prototype.IsTargetInRange = function(target)
+{
+ let range = this.GetRange();
+ let cmpObstructionManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ObstructionManager);
+ return cmpObstructionManager.IsInTargetRange(this.entity, target, range.min, range.max, false);
+};
+
+Engine.RegisterComponentType(IID_TreasureCollecter, "TreasureCollecter", TreasureCollecter);
Property changes on: ps/trunk/binaries/data/mods/public/simulation/components/TreasureCollecter.js
___________________________________________________________________
Added: svn:eol-style
## -0,0 +1 ##
+native
\ No newline at end of property
Added: svn:mime-type
## -0,0 +1 ##
+text/plain
\ No newline at end of property
Index: ps/trunk/binaries/data/mods/public/simulation/components/UnitAI.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/components/UnitAI.js (revision 24988)
+++ ps/trunk/binaries/data/mods/public/simulation/components/UnitAI.js (revision 24989)
@@ -1,6509 +1,6594 @@
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;
// To reject an order, use 'return this.FinishOrder();'
const ACCEPT_ORDER = true;
// 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 this.FinishOrder();
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 this.FinishOrder();
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 this.FinishOrder();
this.order.data.min = g_LeaveFoundationRange;
this.SetNextState("INDIVIDUAL.WALKING");
return ACCEPT_ORDER;
},
// Individual orders:
"Order.LeaveFormation": function() {
if (!this.IsFormationMember())
return this.FinishOrder();
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 this.FinishOrder();
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 this.FinishOrder();
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 this.FinishOrder();
if (this.CanPack())
{
this.PushOrderFront("Pack", { "force": true });
return ACCEPT_ORDER;
}
if (this.CheckRange(this.order.data))
return this.FinishOrder();
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 this.FinishOrder();
let range = cmpGarrisonHolder.GetLoadingRange();
this.order.data.min = range.min;
this.order.data.max = range.max;
if (this.CheckRange(this.order.data))
return this.FinishOrder();
// 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 this.FinishOrder();
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 this.FinishOrder();
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 this.FinishOrder();
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 this.FinishOrder();
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 this.FinishOrder();
// Healers can't heal themselves.
if (this.order.data.target == this.entity)
return this.FinishOrder();
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 this.FinishOrder();
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))
+ if (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 this.FinishOrder();
}
// 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 this.FinishOrder();
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 this.FinishOrder();
},
"Order.Pack": function(msg) {
if (!this.CanPack())
return this.FinishOrder();
this.SetNextState("INDIVIDUAL.PACKING");
return ACCEPT_ORDER;
},
"Order.Unpack": function(msg) {
if (!this.CanUnpack())
return this.FinishOrder();
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 this.FinishOrder();
},
+ "Order.CollectTreasure": function(msg) {
+ let cmpTreasureCollecter = Engine.QueryInterface(this.entity, IID_TreasureCollecter);
+ if (!cmpTreasureCollecter || !cmpTreasureCollecter.CanCollect(msg.data.target))
+ return this.FinishOrder();
+
+ this.SetNextState("COLLECTTREASURE");
+ return ACCEPT_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 this.FinishOrder();
this.SetNextState("WALKING");
return ACCEPT_ORDER;
},
"Order.WalkToTarget": function(msg) {
if (this.CheckRange(this.order.data))
return this.FinishOrder();
this.SetNextState("WALKING");
return ACCEPT_ORDER;
},
"Order.WalkToPointRange": function(msg) {
if (this.CheckRange(this.order.data))
return this.FinishOrder();
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 this.FinishOrder();
}
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 this.FinishOrder();
if (!this.CheckGarrisonRange(msg.data.target))
{
if (!this.CheckTargetVisible(msg.data.target))
return this.FinishOrder();
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 this.FinishOrder();
// 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 this.FinishOrder();
}
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 this.FinishOrder();
if (!msg.data.secondTry)
{
msg.data.secondTry = true;
this.PushOrderFront("WalkToTargetRange", { "target": msg.data.target, "min": 0, "max": 10 });
return ACCEPT_ORDER;
}
return this.FinishOrder();
}
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 this.FinishOrder();
if (!msg.data.secondTry)
{
msg.data.secondTry = true;
this.PushOrderFront("WalkToTargetRange", { "target": msg.data.target, "min": 0, "max": 10 });
return ACCEPT_ORDER;
}
return this.FinishOrder();
}
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 this.FinishOrder();
if (!msg.data.secondTry)
{
msg.data.secondTry = true;
this.PushOrderFront("WalkToTargetRange", { "target": msg.data.target, "min": 0, "max": 10 });
return ACCEPT_ORDER;
}
return this.FinishOrder();
}
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 this.FinishOrder();
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 this.FinishOrder();
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 this.FinishOrder();
},
"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 this.FinishOrder();
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 this.FinishOrder();
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.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();
},
},
},
+ "COLLECTTREASURE": {
+ "enter": function() {
+ let cmpTreasureCollecter = Engine.QueryInterface(this.entity, IID_TreasureCollecter);
+ if (!cmpTreasureCollecter || !cmpTreasureCollecter.CanCollect(this.order.data.target))
+ {
+ this.FinishOrder();
+ return true;
+ }
+ if (this.CheckTargetRange(this.order.data.target, IID_TreasureCollecter))
+ this.SetNextState("COLLECTING");
+ else
+ this.SetNextState("APPROACHING");
+ return true;
+ },
+
+ "leave": function() {
+ },
+
+ "APPROACHING": {
+ "enter": function() {
+ if (!this.MoveToTargetRange(this.order.data.target, IID_TreasureCollecter))
+ {
+ this.FinishOrder();
+ return true;
+ }
+ return false;
+ },
+
+ "leave": function() {
+ this.StopMoving();
+ },
+
+ "MovementUpdate": function(msg) {
+ if (this.CheckTargetRange(this.order.data.target, IID_TreasureCollecter))
+ this.SetNextState("COLLECTING");
+ else if (msg.likelyFailure)
+ this.FinishOrder();
+ },
+ },
+
+ "COLLECTING": {
+ "enter": function() {
+ let cmpTreasureCollecter = Engine.QueryInterface(this.entity, IID_TreasureCollecter);
+ if (!cmpTreasureCollecter.StartCollecting(this.order.data.target, IID_UnitAI))
+ {
+ this.ProcessMessage("TargetInvalidated");
+ return true;
+ }
+ this.FaceTowardsTarget(this.order.data.target);
+ this.SelectAnimation("collecting_treasure");
+ return false;
+ },
+
+ "leave": function() {
+ let cmpTreasureCollecter = Engine.QueryInterface(this.entity, IID_TreasureCollecter);
+ if (cmpTreasureCollecter)
+ cmpTreasureCollecter.StopCollecting();
+ this.ResetAnimation();
+ },
+
+ "OutOfRange": function(msg) {
+ this.SetNextState("APPROACHING");
+ },
+
+ "TargetInvalidated": function(msg) {
+ 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))
{
let cmpGarrisonable = Engine.QueryInterface(this.entity, IID_Garrisonable);
if (cmpGarrisonable.Garrison(target))
{
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.
// Must be called from inside the FSM.
UnitAI.prototype.SetNextState = function(state)
{
this.UnitFsm.SetNextState(this, state);
};
// Must be called from inside the FSM.
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).
* Must be called from inside the FSM.
*/
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() });
return ret;
}
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;
this.UnitFsm.ProcessMessage(this,
{ "type": "Order."+this.order.type, "data": this.order.data }
);
}
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;
this.UnitFsm.ProcessMessage(this,
{ "type": "Order."+this.order.type, "data": this.order.data }
);
}
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});
};
+/**
+ * A general function to process messages sent from components.
+ * @param {string} type - The type of message to process.
+ * @param {Object} msg - Optionally extra data to use.
+ */
+UnitAI.prototype.ProcessMessage = function(type, msg)
+{
+ this.UnitFsm.ProcessMessage(this, { "type": type, "data": msg });
+};
+
//// 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);
};
+/**
+ * Adds order to collect a treasure to queue, forced by the player.
+ */
+UnitAI.prototype.CollectTreasure = function(target, queued)
+{
+ this.AddOrder("CollectTreasure", { "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/interfaces/Treasure.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/components/interfaces/Treasure.js (nonexistent)
+++ ps/trunk/binaries/data/mods/public/simulation/components/interfaces/Treasure.js (revision 24989)
@@ -0,0 +1 @@
+Engine.RegisterInterface("Treasure");
Property changes on: ps/trunk/binaries/data/mods/public/simulation/components/interfaces/Treasure.js
___________________________________________________________________
Added: svn:eol-style
## -0,0 +1 ##
+native
\ No newline at end of property
Added: svn:mime-type
## -0,0 +1 ##
+text/plain
\ No newline at end of property
Index: ps/trunk/binaries/data/mods/public/simulation/components/interfaces/TreasureCollecter.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/components/interfaces/TreasureCollecter.js (nonexistent)
+++ ps/trunk/binaries/data/mods/public/simulation/components/interfaces/TreasureCollecter.js (revision 24989)
@@ -0,0 +1 @@
+Engine.RegisterInterface("TreasureCollecter");
Property changes on: ps/trunk/binaries/data/mods/public/simulation/components/interfaces/TreasureCollecter.js
___________________________________________________________________
Added: svn:eol-style
## -0,0 +1 ##
+native
\ No newline at end of property
Added: svn:mime-type
## -0,0 +1 ##
+text/plain
\ No newline at end of property
Index: ps/trunk/binaries/data/mods/public/simulation/components/tests/test_GuiInterface.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/components/tests/test_GuiInterface.js (revision 24988)
+++ ps/trunk/binaries/data/mods/public/simulation/components/tests/test_GuiInterface.js (revision 24989)
@@ -1,612 +1,614 @@
Engine.LoadHelperScript("ObstructionSnap.js");
Engine.LoadHelperScript("Player.js");
Engine.LoadComponentScript("interfaces/AlertRaiser.js");
Engine.LoadComponentScript("interfaces/Auras.js");
Engine.LoadComponentScript("interfaces/Barter.js");
Engine.LoadComponentScript("interfaces/Builder.js");
Engine.LoadComponentScript("interfaces/Capturable.js");
Engine.LoadComponentScript("interfaces/CeasefireManager.js");
Engine.LoadComponentScript("interfaces/Resistance.js");
Engine.LoadComponentScript("interfaces/DeathDamage.js");
Engine.LoadComponentScript("interfaces/EndGameManager.js");
Engine.LoadComponentScript("interfaces/EntityLimits.js");
Engine.LoadComponentScript("interfaces/Foundation.js");
Engine.LoadComponentScript("interfaces/Garrisonable.js");
Engine.LoadComponentScript("interfaces/GarrisonHolder.js");
Engine.LoadComponentScript("interfaces/Gate.js");
Engine.LoadComponentScript("interfaces/Guard.js");
Engine.LoadComponentScript("interfaces/Heal.js");
Engine.LoadComponentScript("interfaces/Health.js");
Engine.LoadComponentScript("interfaces/Loot.js");
Engine.LoadComponentScript("interfaces/Market.js");
Engine.LoadComponentScript("interfaces/Pack.js");
Engine.LoadComponentScript("interfaces/Population.js");
Engine.LoadComponentScript("interfaces/ProductionQueue.js");
Engine.LoadComponentScript("interfaces/Promotion.js");
Engine.LoadComponentScript("interfaces/Repairable.js");
Engine.LoadComponentScript("interfaces/ResourceDropsite.js");
Engine.LoadComponentScript("interfaces/ResourceGatherer.js");
Engine.LoadComponentScript("interfaces/ResourceTrickle.js");
Engine.LoadComponentScript("interfaces/ResourceSupply.js");
Engine.LoadComponentScript("interfaces/TechnologyManager.js");
Engine.LoadComponentScript("interfaces/Trader.js");
Engine.LoadComponentScript("interfaces/TurretHolder.js");
Engine.LoadComponentScript("interfaces/Timer.js");
+Engine.LoadComponentScript("interfaces/Treasure.js");
+Engine.LoadComponentScript("interfaces/TreasureCollecter.js");
Engine.LoadComponentScript("interfaces/StatisticsTracker.js");
Engine.LoadComponentScript("interfaces/StatusEffectsReceiver.js");
Engine.LoadComponentScript("interfaces/UnitAI.js");
Engine.LoadComponentScript("interfaces/Upgrade.js");
Engine.LoadComponentScript("interfaces/BuildingAI.js");
Engine.LoadComponentScript("GuiInterface.js");
Resources = {
"GetCodes": () => ["food", "metal", "stone", "wood"],
"GetNames": () => ({
"food": "Food",
"metal": "Metal",
"stone": "Stone",
"wood": "Wood"
}),
"GetResource": resource => ({
"aiAnalysisInfluenceGroup":
resource == "food" ? "ignore" :
resource == "wood" ? "abundant" : "sparse"
})
};
var cmp = ConstructComponent(SYSTEM_ENTITY, "GuiInterface");
AddMock(SYSTEM_ENTITY, IID_Barter, {
"GetPrices": function() {
return {
"buy": { "food": 150 },
"sell": { "food": 25 }
};
}
});
AddMock(SYSTEM_ENTITY, IID_EndGameManager, {
"GetVictoryConditions": () => ["conquest", "wonder"],
"GetAlliedVictory": function() { return false; }
});
AddMock(SYSTEM_ENTITY, IID_PlayerManager, {
"GetNumPlayers": function() { return 2; },
"GetPlayerByID": function(id) { TS_ASSERT(id === 0 || id === 1); return 100 + id; },
"GetMaxWorldPopulation": function() {}
});
AddMock(SYSTEM_ENTITY, IID_RangeManager, {
"GetLosVisibility": function(ent, player) { return "visible"; },
"GetLosCircular": function() { return false; }
});
AddMock(SYSTEM_ENTITY, IID_TemplateManager, {
"GetCurrentTemplateName": function(ent) { return "example"; },
"GetTemplate": function(name) { return ""; }
});
AddMock(SYSTEM_ENTITY, IID_Timer, {
"GetTime": function() { return 0; },
"SetTimeout": function(ent, iid, funcname, time, data) { return 0; }
});
AddMock(100, IID_Player, {
"GetName": function() { return "Player 1"; },
"GetCiv": function() { return "gaia"; },
"GetColor": function() { return { "r": 1, "g": 1, "b": 1, "a": 1 }; },
"CanControlAllUnits": function() { return false; },
"GetPopulationCount": function() { return 10; },
"GetPopulationLimit": function() { return 20; },
"GetMaxPopulation": function() { return 200; },
"GetResourceCounts": function() { return { "food": 100 }; },
"GetResourceGatherers": function() { return { "food": 1 }; },
"GetPanelEntities": function() { return []; },
"IsTrainingBlocked": function() { return false; },
"GetState": function() { return "active"; },
"GetTeam": function() { return -1; },
"GetLockTeams": function() { return false; },
"GetCheatsEnabled": function() { return false; },
"GetDiplomacy": function() { return [-1, 1]; },
"IsAlly": function() { return false; },
"IsMutualAlly": function() { return false; },
"IsNeutral": function() { return false; },
"IsEnemy": function() { return true; },
"GetDisabledTemplates": function() { return {}; },
"GetDisabledTechnologies": function() { return {}; },
"CanBarter": function() { return false; },
"GetSpyCostMultiplier": function() { return 1; },
"HasSharedDropsites": function() { return false; },
"HasSharedLos": function() { return false; }
});
AddMock(100, IID_EntityLimits, {
"GetLimits": function() { return { "Foo": 10 }; },
"GetCounts": function() { return { "Foo": 5 }; },
"GetLimitChangers": function() { return { "Foo": {} }; },
"GetMatchCounts": function() { return { "Bar": 0 }; }
});
AddMock(100, IID_TechnologyManager, {
"IsTechnologyResearched": tech => tech == "phase_village",
"GetQueuedResearch": () => new Map(),
"GetStartedTechs": () => new Set(),
"GetResearchedTechs": () => new Set(),
"GetClassCounts": () => ({}),
"GetTypeCountsByClass": () => ({})
});
AddMock(100, IID_StatisticsTracker, {
"GetBasicStatistics": function() {
return {
"resourcesGathered": {
"food": 100,
"wood": 0,
"metal": 0,
"stone": 0,
"vegetarianFood": 0
},
"percentMapExplored": 10
};
},
"GetSequences": function() {
return {
"unitsTrained": [0, 10],
"unitsLost": [0, 42],
"buildingsConstructed": [1, 3],
"buildingsCaptured": [3, 7],
"buildingsLost": [3, 10],
"civCentresBuilt": [4, 10],
"resourcesGathered": {
"food": [5, 100],
"wood": [0, 0],
"metal": [0, 0],
"stone": [0, 0],
"vegetarianFood": [0, 0]
},
"treasuresCollected": [1, 20],
"lootCollected": [0, 2],
"percentMapExplored": [0, 10],
"teamPercentMapExplored": [0, 10],
"percentMapControlled": [0, 10],
"teamPercentMapControlled": [0, 10],
"peakPercentOfMapControlled": [0, 10],
"teamPeakPercentOfMapControlled": [0, 10]
};
},
"IncreaseTrainedUnitsCounter": function() { return 1; },
"IncreaseConstructedBuildingsCounter": function() { return 1; },
"IncreaseBuiltCivCentresCounter": function() { return 1; }
});
AddMock(101, IID_Player, {
"GetName": function() { return "Player 2"; },
"GetCiv": function() { return "mace"; },
"GetColor": function() { return { "r": 1, "g": 0, "b": 0, "a": 1 }; },
"CanControlAllUnits": function() { return true; },
"GetPopulationCount": function() { return 40; },
"GetPopulationLimit": function() { return 30; },
"GetMaxPopulation": function() { return 300; },
"GetResourceCounts": function() { return { "food": 200 }; },
"GetResourceGatherers": function() { return { "food": 3 }; },
"GetPanelEntities": function() { return []; },
"IsTrainingBlocked": function() { return false; },
"GetState": function() { return "active"; },
"GetTeam": function() { return -1; },
"GetLockTeams": function() {return false; },
"GetCheatsEnabled": function() { return false; },
"GetDiplomacy": function() { return [-1, 1]; },
"IsAlly": function() { return true; },
"IsMutualAlly": function() {return false; },
"IsNeutral": function() { return false; },
"IsEnemy": function() { return false; },
"GetDisabledTemplates": function() { return {}; },
"GetDisabledTechnologies": function() { return {}; },
"CanBarter": function() { return false; },
"GetSpyCostMultiplier": function() { return 1; },
"HasSharedDropsites": function() { return false; },
"HasSharedLos": function() { return false; }
});
AddMock(101, IID_EntityLimits, {
"GetLimits": function() { return { "Bar": 20 }; },
"GetCounts": function() { return { "Bar": 0 }; },
"GetLimitChangers": function() { return { "Bar": {} }; },
"GetMatchCounts": function() { return { "Foo": 0 }; }
});
AddMock(101, IID_TechnologyManager, {
"IsTechnologyResearched": tech => tech == "phase_village",
"GetQueuedResearch": () => new Map(),
"GetStartedTechs": () => new Set(),
"GetResearchedTechs": () => new Set(),
"GetClassCounts": () => ({}),
"GetTypeCountsByClass": () => ({})
});
AddMock(101, IID_StatisticsTracker, {
"GetBasicStatistics": function() {
return {
"resourcesGathered": {
"food": 100,
"wood": 0,
"metal": 0,
"stone": 0,
"vegetarianFood": 0
},
"percentMapExplored": 10
};
},
"GetSequences": function() {
return {
"unitsTrained": [0, 10],
"unitsLost": [0, 9],
"buildingsConstructed": [0, 5],
"buildingsCaptured": [0, 7],
"buildingsLost": [0, 4],
"civCentresBuilt": [0, 1],
"resourcesGathered": {
"food": [0, 100],
"wood": [0, 0],
"metal": [0, 0],
"stone": [0, 0],
"vegetarianFood": [0, 0]
},
"treasuresCollected": [0, 0],
"lootCollected": [0, 0],
"percentMapExplored": [0, 10],
"teamPercentMapExplored": [0, 10],
"percentMapControlled": [0, 10],
"teamPercentMapControlled": [0, 10],
"peakPercentOfMapControlled": [0, 10],
"teamPeakPercentOfMapControlled": [0, 10]
};
},
"IncreaseTrainedUnitsCounter": function() { return 1; },
"IncreaseConstructedBuildingsCounter": function() { return 1; },
"IncreaseBuiltCivCentresCounter": function() { return 1; }
});
// Note: property order matters when using TS_ASSERT_UNEVAL_EQUALS,
// because uneval preserves property order. So make sure this object
// matches the ordering in GuiInterface.
TS_ASSERT_UNEVAL_EQUALS(cmp.GetSimulationState(), {
"players": [
{
"name": "Player 1",
"civ": "gaia",
"color": { "r": 1, "g": 1, "b": 1, "a": 1 },
"controlsAll": false,
"popCount": 10,
"popLimit": 20,
"popMax": 200,
"panelEntities": [],
"resourceCounts": { "food": 100 },
"resourceGatherers": { "food": 1 },
"trainingBlocked": false,
"state": "active",
"team": -1,
"teamsLocked": false,
"cheatsEnabled": false,
"disabledTemplates": {},
"disabledTechnologies": {},
"hasSharedDropsites": false,
"hasSharedLos": false,
"spyCostMultiplier": 1,
"phase": "village",
"isAlly": [false, false],
"isMutualAlly": [false, false],
"isNeutral": [false, false],
"isEnemy": [true, true],
"entityLimits": { "Foo": 10 },
"entityCounts": { "Foo": 5 },
"matchEntityCounts": { "Bar": 0 },
"entityLimitChangers": { "Foo": {} },
"researchQueued": new Map(),
"researchStarted": new Set(),
"researchedTechs": new Set(),
"classCounts": {},
"typeCountsByClass": {},
"canBarter": false,
"barterPrices": {
"buy": { "food": 150 },
"sell": { "food": 25 }
},
"statistics": {
"resourcesGathered": {
"food": 100,
"wood": 0,
"metal": 0,
"stone": 0,
"vegetarianFood": 0
},
"percentMapExplored": 10
}
},
{
"name": "Player 2",
"civ": "mace",
"color": { "r": 1, "g": 0, "b": 0, "a": 1 },
"controlsAll": true,
"popCount": 40,
"popLimit": 30,
"popMax": 300,
"panelEntities": [],
"resourceCounts": { "food": 200 },
"resourceGatherers": { "food": 3 },
"trainingBlocked": false,
"state": "active",
"team": -1,
"teamsLocked": false,
"cheatsEnabled": false,
"disabledTemplates": {},
"disabledTechnologies": {},
"hasSharedDropsites": false,
"hasSharedLos": false,
"spyCostMultiplier": 1,
"phase": "village",
"isAlly": [true, true],
"isMutualAlly": [false, false],
"isNeutral": [false, false],
"isEnemy": [false, false],
"entityLimits": { "Bar": 20 },
"entityCounts": { "Bar": 0 },
"matchEntityCounts": { "Foo": 0 },
"entityLimitChangers": { "Bar": {} },
"researchQueued": new Map(),
"researchStarted": new Set(),
"researchedTechs": new Set(),
"classCounts": {},
"typeCountsByClass": {},
"canBarter": false,
"barterPrices": {
"buy": { "food": 150 },
"sell": { "food": 25 }
},
"statistics": {
"resourcesGathered": {
"food": 100,
"wood": 0,
"metal": 0,
"stone": 0,
"vegetarianFood": 0
},
"percentMapExplored": 10
}
}
],
"circularMap": false,
"timeElapsed": 0,
"victoryConditions": ["conquest", "wonder"],
"alliedVictory": false,
"maxWorldPopulation": undefined
});
TS_ASSERT_UNEVAL_EQUALS(cmp.GetExtendedSimulationState(), {
"players": [
{
"name": "Player 1",
"civ": "gaia",
"color": { "r": 1, "g": 1, "b": 1, "a": 1 },
"controlsAll": false,
"popCount": 10,
"popLimit": 20,
"popMax": 200,
"panelEntities": [],
"resourceCounts": { "food": 100 },
"resourceGatherers": { "food": 1 },
"trainingBlocked": false,
"state": "active",
"team": -1,
"teamsLocked": false,
"cheatsEnabled": false,
"disabledTemplates": {},
"disabledTechnologies": {},
"hasSharedDropsites": false,
"hasSharedLos": false,
"spyCostMultiplier": 1,
"phase": "village",
"isAlly": [false, false],
"isMutualAlly": [false, false],
"isNeutral": [false, false],
"isEnemy": [true, true],
"entityLimits": { "Foo": 10 },
"entityCounts": { "Foo": 5 },
"matchEntityCounts": { "Bar": 0 },
"entityLimitChangers": { "Foo": {} },
"researchQueued": new Map(),
"researchStarted": new Set(),
"researchedTechs": new Set(),
"classCounts": {},
"typeCountsByClass": {},
"canBarter": false,
"barterPrices": {
"buy": { "food": 150 },
"sell": { "food": 25 }
},
"statistics": {
"resourcesGathered": {
"food": 100,
"wood": 0,
"metal": 0,
"stone": 0,
"vegetarianFood": 0
},
"percentMapExplored": 10
},
"sequences": {
"unitsTrained": [0, 10],
"unitsLost": [0, 42],
"buildingsConstructed": [1, 3],
"buildingsCaptured": [3, 7],
"buildingsLost": [3, 10],
"civCentresBuilt": [4, 10],
"resourcesGathered": {
"food": [5, 100],
"wood": [0, 0],
"metal": [0, 0],
"stone": [0, 0],
"vegetarianFood": [0, 0]
},
"treasuresCollected": [1, 20],
"lootCollected": [0, 2],
"percentMapExplored": [0, 10],
"teamPercentMapExplored": [0, 10],
"percentMapControlled": [0, 10],
"teamPercentMapControlled": [0, 10],
"peakPercentOfMapControlled": [0, 10],
"teamPeakPercentOfMapControlled": [0, 10]
}
},
{
"name": "Player 2",
"civ": "mace",
"color": { "r": 1, "g": 0, "b": 0, "a": 1 },
"controlsAll": true,
"popCount": 40,
"popLimit": 30,
"popMax": 300,
"panelEntities": [],
"resourceCounts": { "food": 200 },
"resourceGatherers": { "food": 3 },
"trainingBlocked": false,
"state": "active",
"team": -1,
"teamsLocked": false,
"cheatsEnabled": false,
"disabledTemplates": {},
"disabledTechnologies": {},
"hasSharedDropsites": false,
"hasSharedLos": false,
"spyCostMultiplier": 1,
"phase": "village",
"isAlly": [true, true],
"isMutualAlly": [false, false],
"isNeutral": [false, false],
"isEnemy": [false, false],
"entityLimits": { "Bar": 20 },
"entityCounts": { "Bar": 0 },
"matchEntityCounts": { "Foo": 0 },
"entityLimitChangers": { "Bar": {} },
"researchQueued": new Map(),
"researchStarted": new Set(),
"researchedTechs": new Set(),
"classCounts": {},
"typeCountsByClass": {},
"canBarter": false,
"barterPrices": {
"buy": { "food": 150 },
"sell": { "food": 25 }
},
"statistics": {
"resourcesGathered": {
"food": 100,
"wood": 0,
"metal": 0,
"stone": 0,
"vegetarianFood": 0
},
"percentMapExplored": 10
},
"sequences": {
"unitsTrained": [0, 10],
"unitsLost": [0, 9],
"buildingsConstructed": [0, 5],
"buildingsCaptured": [0, 7],
"buildingsLost": [0, 4],
"civCentresBuilt": [0, 1],
"resourcesGathered": {
"food": [0, 100],
"wood": [0, 0],
"metal": [0, 0],
"stone": [0, 0],
"vegetarianFood": [0, 0]
},
"treasuresCollected": [0, 0],
"lootCollected": [0, 0],
"percentMapExplored": [0, 10],
"teamPercentMapExplored": [0, 10],
"percentMapControlled": [0, 10],
"teamPercentMapControlled": [0, 10],
"peakPercentOfMapControlled": [0, 10],
"teamPeakPercentOfMapControlled": [0, 10]
}
}
],
"circularMap": false,
"timeElapsed": 0,
"victoryConditions": ["conquest", "wonder"],
"alliedVictory": false,
"maxWorldPopulation": undefined
});
AddMock(10, IID_Builder, {
"GetEntitiesList": function() {
return ["test1", "test2"];
},
});
AddMock(10, IID_Health, {
"GetHitpoints": function() { return 50; },
"GetMaxHitpoints": function() { return 60; },
"IsRepairable": function() { return false; },
"IsUnhealable": function() { return false; }
});
AddMock(10, IID_Identity, {
"GetClassesList": function() { return ["class1", "class2"]; },
"GetRank": function() { return "foo"; },
"GetSelectionGroupName": function() { return "Selection Group Name"; },
"HasClass": function() { return true; },
"IsUndeletable": function() { return false; },
"IsControllable": function() { return true; },
"HasSomeFormation": function() { return false; },
"GetFormationsList": function() { return []; },
});
AddMock(10, IID_Position, {
"GetTurretParent": function() { return INVALID_ENTITY; },
"GetPosition": function() {
return { "x": 1, "y": 2, "z": 3 };
},
"IsInWorld": function() {
return true;
}
});
AddMock(10, IID_ResourceTrickle, {
"GetInterval": () => 1250,
"GetRates": () => ({ "food": 2, "wood": 3, "stone": 5, "metal": 9 })
});
// Note: property order matters when using TS_ASSERT_UNEVAL_EQUALS,
// because uneval preserves property order. So make sure this object
// matches the ordering in GuiInterface.
TS_ASSERT_UNEVAL_EQUALS(cmp.GetEntityState(-1, 10), {
"id": 10,
"player": INVALID_PLAYER,
"template": "example",
"identity": {
"rank": "foo",
"classes": ["class1", "class2"],
"selectionGroupName": "Selection Group Name",
"canDelete": true,
"hasSomeFormation": false,
"formations": [],
"controllable": true,
},
"position": { "x": 1, "y": 2, "z": 3 },
"hitpoints": 50,
"maxHitpoints": 60,
"needsRepair": false,
"needsHeal": true,
"builder": true,
"visibility": "visible",
"isBarterMarket": true,
"resourceTrickle": {
"interval": 1250,
"rates": { "food": 2, "wood": 3, "stone": 5, "metal": 9 }
}
});
Index: ps/trunk/binaries/data/mods/public/simulation/components/tests/test_TreasureCollecter.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/components/tests/test_TreasureCollecter.js (nonexistent)
+++ ps/trunk/binaries/data/mods/public/simulation/components/tests/test_TreasureCollecter.js (revision 24989)
@@ -0,0 +1,47 @@
+Engine.LoadHelperScript("Player.js");
+Engine.LoadComponentScript("interfaces/Timer.js");
+Engine.LoadComponentScript("interfaces/Treasure.js");
+Engine.LoadComponentScript("interfaces/TreasureCollecter.js");
+Engine.LoadComponentScript("interfaces/UnitAI.js");
+Engine.LoadComponentScript("Timer.js");
+Engine.LoadComponentScript("TreasureCollecter.js");
+
+AddMock(SYSTEM_ENTITY, IID_ObstructionManager, {
+ "IsInTargetRange": () => true
+});
+
+const entity = 11;
+let treasure = 12;
+let cmpTimer = ConstructComponent(SYSTEM_ENTITY, "Timer", {});
+
+let cmpTreasurer = ConstructComponent(entity, "TreasureCollecter", {
+ "MaxDistance": "2.0"
+});
+
+TS_ASSERT(!cmpTreasurer.StartCollecting(treasure));
+
+let cmpTreasure = AddMock(treasure, IID_Treasure, {
+ "Reward": (ent) => true,
+ "CollectionTime": () => 1000,
+ "IsAvailable": () => true
+});
+let spyTreasure = new Spy(cmpTreasure, "Reward");
+TS_ASSERT(cmpTreasurer.StartCollecting(treasure));
+cmpTimer.OnUpdate({ "turnLength": 1 });
+TS_ASSERT_EQUALS(spyTreasure._called, 1);
+
+// Test that starting to collect twice merely collects once.
+spyTreasure._called = 0;
+TS_ASSERT(cmpTreasurer.StartCollecting(treasure));
+TS_ASSERT(cmpTreasurer.StartCollecting(treasure));
+cmpTimer.OnUpdate({ "turnLength": 1 });
+TS_ASSERT_EQUALS(spyTreasure._called, 1);
+
+// Test callback is called.
+let cmpUnitAI = AddMock(entity, IID_UnitAI, {
+ "ProcessMessage": (type, data) => TS_ASSERT_EQUALS(type, "TargetInvalidated")
+});
+let spyUnitAI = new Spy(cmpUnitAI, "ProcessMessage");
+TS_ASSERT(cmpTreasurer.StartCollecting(treasure, IID_UnitAI));
+cmpTimer.OnUpdate({ "turnLength": 1 });
+TS_ASSERT_EQUALS(spyUnitAI._called, 1);
Property changes on: ps/trunk/binaries/data/mods/public/simulation/components/tests/test_TreasureCollecter.js
___________________________________________________________________
Added: svn:eol-style
## -0,0 +1 ##
+native
\ No newline at end of property
Added: svn:mime-type
## -0,0 +1 ##
+text/plain
\ No newline at end of property
Index: ps/trunk/binaries/data/mods/public/simulation/data/technologies/unit_mercenary.json
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/data/technologies/unit_mercenary.json (revision 24988)
+++ ps/trunk/binaries/data/mods/public/simulation/data/technologies/unit_mercenary.json (revision 24989)
@@ -1,30 +1,22 @@
{
"autoResearch": true,
"icon": "coins.png",
"tooltip": "Non-Champion Mercenaries −60% food cost, no wood or stone costs, −30% training time, and unable to gather resources; Infantry costs 60 metal, Cavalry 80 metal, Elephants 120 metal.",
"modifications": [
{ "value": "Cost/BuildTime", "multiply": 0.7 },
{ "value": "Cost/Resources/food", "multiply": 0.4 },
{ "value": "Cost/Resources/wood", "replace": 0 },
{ "value": "Cost/Resources/stone", "replace": 0 },
{ "value": "Cost/Resources/metal", "replace": 60, "affects": "Infantry" },
{ "value": "Cost/Resources/metal", "replace": 80, "affects": "Cavalry" },
{ "value": "Cost/Resources/metal", "replace": 120, "affects": "Elephant" },
{ "value": "Loot/food", "multiply": 0.4 },
{ "value": "Loot/wood", "replace": 0 },
{ "value": "Loot/stone", "replace": 0 },
{ "value": "Loot/metal", "replace": 6, "affects": "Infantry" },
{ "value": "Loot/metal", "replace": 8, "affects": "Cavalry" },
{ "value": "Loot/metal", "replace": 12, "affects": "Elephant" },
- { "value": "ResourceGatherer/Rates/food.fish", "replace": 0 },
- { "value": "ResourceGatherer/Rates/food.fruit", "replace": 0 },
- { "value": "ResourceGatherer/Rates/food.grain", "replace": 0 },
- { "value": "ResourceGatherer/Rates/food.meat", "replace": 0 },
- { "value": "ResourceGatherer/Rates/wood.tree", "replace": 0 },
- { "value": "ResourceGatherer/Rates/wood.ruins", "replace": 0 },
- { "value": "ResourceGatherer/Rates/stone.rock", "replace": 0 },
- { "value": "ResourceGatherer/Rates/stone.ruins", "replace": 0 },
- { "value": "ResourceGatherer/Rates/metal.ore", "replace": 0 }
+ { "value": "ResourceGatherer/BaseSpeed", "replace": 0 }
],
"affects": ["Mercenary !Champion"]
}
Index: ps/trunk/binaries/data/mods/public/simulation/helpers/RallyPointCommands.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/helpers/RallyPointCommands.js (revision 24988)
+++ ps/trunk/binaries/data/mods/public/simulation/helpers/RallyPointCommands.js (revision 24989)
@@ -1,139 +1,147 @@
// Returns an array of commands suitable for ProcessCommand() based on the rally point data.
// This assumes that the rally point has a valid position.
function GetRallyPointCommands(cmpRallyPoint, spawnedEnts)
{
let data = cmpRallyPoint.GetData();
let rallyPos = cmpRallyPoint.GetPositions();
let ret = [];
for (let i = 0; i < rallyPos.length; ++i)
{
// Look and see if there is a command in the rally point data, otherwise just walk there.
let command = data[i] && data[i].command ? data[i].command : "walk";
// If a target was set and the target no longer exists, or no longer
// has a valid position, then just walk to the rally point.
if (data[i] && data[i].target)
{
let cmpPosition = Engine.QueryInterface(data[i].target, IID_Position);
if (!cmpPosition || !cmpPosition.IsInWorld())
command = command == "gather" ? "gather-near-position" : "walk";
}
switch (command)
{
case "gather":
ret.push({
"type": "gather",
"entities": spawnedEnts,
"target": data[i].target,
"queued": true
});
break;
case "gather-near-position":
ret.push({
"type": "gather-near-position",
"entities": spawnedEnts,
"x": rallyPos[i].x,
"z": rallyPos[i].z,
"resourceType": data[i].resourceType,
"resourceTemplate": data[i].resourceTemplate,
"queued": true
});
break;
case "repair":
case "build":
ret.push({
"type": "repair",
"entities": spawnedEnts,
"target": data[i].target,
"queued": true,
"autocontinue": i == rallyPos.length - 1
});
break;
case "garrison":
ret.push({
"type": "garrison",
"entities": spawnedEnts,
"target": data[i].target,
"queued": true
});
break;
case "attack-walk":
ret.push({
"type": "attack-walk",
"entities": spawnedEnts,
"x": rallyPos[i].x,
"z": rallyPos[i].z,
"targetClasses": data[i].targetClasses,
"queued": true
});
break;
case "patrol":
ret.push({
"type": "patrol",
"entities": spawnedEnts,
"x": rallyPos[i].x,
"z": rallyPos[i].z,
"target": data[i].target,
"targetClasses": data[i].targetClasses,
"queued": true
});
break;
case "attack":
ret.push({
"type": "attack",
"entities": spawnedEnts,
"target": data[i].target,
"queued": true,
});
break;
case "trade":
ret.push({
"type": "setup-trade-route",
"entities": spawnedEnts,
"source": data[i].source,
"target": data[i].target,
"route": undefined,
"queued": true
});
break;
+ case "collect-treasure":
+ ret.push({
+ "type": "collect-treasure",
+ "entities": spawnedEnts,
+ "target": data[i].target,
+ "queued": true,
+ });
+ break;
default:
ret.push({
"type": "walk",
"entities": spawnedEnts,
"x": rallyPos[i].x,
"z": rallyPos[i].z,
"queued": true
});
break;
}
}
// special case: trade route with waypoints
// (we do not modify the RallyPoint before, as we want it to be displayed with all way-points)
if (ret.length > 1 && ret[ret.length-1].type == "setup-trade-route")
{
let route = [];
let waypoints = ret.length - 1;
for (let i = 0; i < waypoints; ++i)
{
if (ret[i].type != "walk")
{
route = undefined;
break;
}
route.push({ "x": ret[i].x, "z": ret[i].z });
}
if (route && route.length > 0)
{
ret.splice(0, waypoints);
ret[0].route = route;
}
}
return ret;
}
Engine.RegisterGlobal("GetRallyPointCommands", GetRallyPointCommands);
Index: ps/trunk/binaries/data/mods/public/simulation/helpers/Setup.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/helpers/Setup.js (revision 24988)
+++ ps/trunk/binaries/data/mods/public/simulation/helpers/Setup.js (revision 24989)
@@ -1,72 +1,68 @@
/**
* Used to initialize non-player settings relevant to the map, like
* default stance and victory conditions. DO NOT load players here
*/
function LoadMapSettings(settings)
{
if (!settings)
settings = {};
if (settings.DefaultStance)
for (let ent of Engine.GetEntitiesWithInterface(IID_UnitAI))
{
let cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
cmpUnitAI.SwitchToStance(settings.DefaultStance);
}
if (settings.RevealMap)
{
let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
if (cmpRangeManager)
cmpRangeManager.SetLosRevealAll(-1, true);
}
if (settings.DisableTreasures)
- for (let ent of Engine.GetEntitiesWithInterface(IID_ResourceSupply))
- {
- let cmpResourceSupply = Engine.QueryInterface(ent, IID_ResourceSupply);
- if (cmpResourceSupply.GetType().generic == "treasure")
- Engine.DestroyEntity(ent);
- }
+ for (let ent of Engine.GetEntitiesWithInterface(IID_Treasure))
+ Engine.DestroyEntity(ent);
let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
if (cmpRangeManager)
cmpRangeManager.SetLosCircular(!!settings.CircularMap);
let cmpObstructionManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ObstructionManager);
if (cmpObstructionManager)
cmpObstructionManager.SetPassabilityCircular(!!settings.CircularMap);
if (settings.TriggerDifficulty !== undefined)
Engine.QueryInterface(SYSTEM_ENTITY, IID_Trigger).SetDifficulty(settings.TriggerDifficulty);
else if (settings.SupportedTriggerDifficulties) // used by Atlas and autostart games
{
let difficulties = Engine.ReadJSONFile("simulation/data/settings/trigger_difficulties.json").Data;
let defaultDiff = difficulties.find(d => d.Name == settings.SupportedTriggerDifficulties.Default).Difficulty;
Engine.QueryInterface(SYSTEM_ENTITY, IID_Trigger).SetDifficulty(defaultDiff);
}
let cmpEndGameManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_EndGameManager);
let gameSettings = { "victoryConditions": clone(settings.VictoryConditions) };
if (gameSettings.victoryConditions.indexOf("capture_the_relic") != -1)
{
gameSettings.relicCount = settings.RelicCount;
gameSettings.relicDuration = settings.RelicDuration * 60 * 1000;
}
if (gameSettings.victoryConditions.indexOf("wonder") != -1)
gameSettings.wonderDuration = settings.WonderDuration * 60 * 1000;
if (gameSettings.victoryConditions.indexOf("regicide") != -1)
gameSettings.regicideGarrison = settings.RegicideGarrison;
cmpEndGameManager.SetGameSettings(gameSettings);
cmpEndGameManager.SetAlliedVictory(settings.LockTeams || !settings.LastManStanding);
if (settings.LockTeams && settings.LastManStanding)
warn("Last man standing is only available in games with unlocked teams!");
let cmpCeasefireManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_CeasefireManager);
if (settings.Ceasefire)
cmpCeasefireManager.StartCeasefire(settings.Ceasefire * 60 * 1000);
}
Engine.RegisterGlobal("LoadMapSettings", LoadMapSettings);
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 24988)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/treasure/food_bin.xml (revision 24989)
@@ -1,21 +1,22 @@
12.0Food Treasuregaia/special_treasure_food.png
-
- 300
- treasure.food
-
+
+
+ 300
+
+ props/special/eyecandy/produce_bin_a.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 24988)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/treasure/food_persian_big.xml (revision 24989)
@@ -1,21 +1,22 @@
2.0Persian Food Storesgaia/special_treasure_food.png
-
- 600
- treasure.food
-
+
+
+ 600
+
+ props/special/eyecandy/treasure_persian_food_big.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 24988)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/treasure/metal.xml (revision 24989)
@@ -1,22 +1,23 @@
2.5Secret Box
-
- 300
- treasure.metal
-
+
+
+ 300
+
+ props/special/eyecandy/barrel_a.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 24988)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/treasure/pegasus.xml (revision 24989)
@@ -1,17 +1,18 @@
2.0Pegasus
-
- 1000
- treasure.metal
-
+
+
+ 1000
+
+ special/pegasus.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 24988)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/treasure/shipwreck_ram_bow.xml (revision 24989)
@@ -1,24 +1,25 @@
9.0Shipwrecktrue0.0
-
- 550
- treasure.wood
-
+
+
+ 550
+
+ props/special/eyecandy/shipwreck_ram_bow.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 24988)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/treasure/standing_stone.xml (revision 24989)
@@ -1,25 +1,26 @@
2.0Celtic Standing Stone
-
- 300
- treasure.stone
-
+
+
+ 300
+
+ props/special/eyecandy/standing_stones.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 24988)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/treasure/food_jars.xml (revision 24989)
@@ -1,21 +1,22 @@
2.0Food Treasuregaia/special_treasure_food.png
-
- 300
- treasure.food
-
+
+
+ 300
+
+ props/special/eyecandy/amphorae.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 24988)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/treasure/golden_fleece.xml (revision 24989)
@@ -1,17 +1,18 @@
2.5Golden Fleece
-
- 1000
- treasure.metal
-
+
+
+ 1000
+
+ special/golden_fleece.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 24988)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/treasure/metal_persian_small.xml (revision 24989)
@@ -1,25 +1,26 @@
12.0Persian Rugs
-
- 300
- treasure.metal
-
+
+
+ 300
+
+ props/special/eyecandy/treasure_persian_metal_small.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 24988)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/treasure/shipwreck_debris.xml (revision 24989)
@@ -1,27 +1,28 @@
2.5Shipwreck Cargofalsefalse-0.1true0.0
-
- 200
- treasure.food
-
+
+
+ 200
+
+ props/special/eyecandy/barrels_floating.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 24988)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/treasure/shipwreck_sail_boat_cut.xml (revision 24989)
@@ -1,24 +1,25 @@
9.0Shipwrecktrue0.0
-
- 450
- treasure.wood
-
+
+
+ 450
+
+ props/special/eyecandy/shipwreck_sail_boat_cut.xml
Index: ps/trunk/binaries/data/mods/public/simulation/components/tests/test_Treasure.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/components/tests/test_Treasure.js (nonexistent)
+++ ps/trunk/binaries/data/mods/public/simulation/components/tests/test_Treasure.js (revision 24989)
@@ -0,0 +1,56 @@
+Resources = {
+ "BuildSchema": () => {
+ let schema = "";
+ for (let res of ["food", "metal"])
+ {
+ for (let subtype in ["meat", "grain"])
+ schema += "" + res + "." + subtype + "";
+ schema += " treasure." + res + "";
+ }
+ return "" + schema + "";
+ }
+};
+
+Engine.LoadHelperScript("Player.js");
+Engine.LoadComponentScript("interfaces/StatisticsTracker.js");
+Engine.LoadComponentScript("interfaces/Treasure.js");
+Engine.LoadComponentScript("interfaces/Trigger.js");
+Engine.LoadComponentScript("Treasure.js");
+Engine.LoadComponentScript("Trigger.js");
+
+Engine.RegisterGlobal("ApplyValueModificationsToEntity", (prop, oVal, ent) => oVal);
+ConstructComponent(SYSTEM_ENTITY, "Trigger", {});
+
+const entity = 11;
+let treasurer = 12;
+let treasurerOwner = 1;
+
+let cmpTreasure = ConstructComponent(entity, "Treasure", {
+ "CollectTime": "1000",
+ "Resources": {
+ "Food": "10"
+ }
+});
+cmpTreasure.OnOwnershipChanged({ "to": 0 });
+
+TS_ASSERT(!cmpTreasure.Reward(treasurer));
+
+AddMock(treasurer, IID_Ownership, {
+ "GetOwner": () => treasurerOwner
+});
+
+AddMock(SYSTEM_ENTITY, IID_PlayerManager, {
+ "GetPlayerByID": (id) => treasurerOwner
+});
+
+let cmpPlayer = AddMock(treasurerOwner, IID_Player, {
+ "AddResources": (type, amount) => {},
+ "GetPlayerID": () => treasurerOwner
+});
+let spy = new Spy(cmpPlayer, "AddResources");
+TS_ASSERT(cmpTreasure.Reward(treasurer));
+TS_ASSERT_EQUALS(spy._called, 1);
+
+// Don't allow collecting twice.
+TS_ASSERT(!cmpTreasure.Reward(treasurer));
+TS_ASSERT_EQUALS(spy._called, 1);
Property changes on: ps/trunk/binaries/data/mods/public/simulation/components/tests/test_Treasure.js
___________________________________________________________________
Added: svn:eol-style
## -0,0 +1 ##
+native
\ No newline at end of property
Added: svn:mime-type
## -0,0 +1 ##
+text/plain
\ No newline at end of property
Index: ps/trunk/binaries/data/mods/public/simulation/components/tests/test_Treasures.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/components/tests/test_Treasures.js (nonexistent)
+++ ps/trunk/binaries/data/mods/public/simulation/components/tests/test_Treasures.js (revision 24989)
@@ -0,0 +1,79 @@
+Resources = {
+ "GetCodes": () => ["food", "metal", "stone", "wood"],
+ "GetTradableCodes": () => ["food", "metal", "stone", "wood"],
+ "GetBarterableCodes": () => ["food", "metal", "stone", "wood"],
+ "BuildSchema": () => {
+ let schema = "";
+ for (let res of ["food", "metal"])
+ {
+ for (let subtype in ["meat", "grain"])
+ schema += "" + res + "." + subtype + "";
+ schema += " treasure." + res + "";
+ }
+ return "" + schema + "";
+ },
+ "GetResource": (type) => {
+ return {
+ "subtypes": {
+ "meat": "meat",
+ "grain": "grain"
+ }
+ };
+ }
+};
+
+Engine.LoadHelperScript("Player.js");
+Engine.LoadComponentScript("interfaces/Player.js");
+Engine.LoadComponentScript("interfaces/StatisticsTracker.js");
+Engine.LoadComponentScript("interfaces/Treasure.js");
+Engine.LoadComponentScript("interfaces/TreasureCollecter.js");
+Engine.LoadComponentScript("interfaces/Timer.js");
+Engine.LoadComponentScript("interfaces/Trigger.js");
+Engine.LoadComponentScript("interfaces/UnitAI.js");
+Engine.LoadComponentScript("Player.js");
+Engine.LoadComponentScript("Timer.js");
+Engine.LoadComponentScript("Treasure.js");
+Engine.LoadComponentScript("TreasureCollecter.js");
+Engine.LoadComponentScript("Trigger.js");
+
+let cmpTimer = ConstructComponent(SYSTEM_ENTITY, "Timer", {});
+Engine.RegisterGlobal("ApplyValueModificationsToEntity", (prop, oVal, ent) => oVal);
+ConstructComponent(SYSTEM_ENTITY, "Trigger", {});
+
+const treasure = 11;
+const treasurer = 12;
+const owner = 1;
+
+AddMock(treasurer, IID_Ownership, {
+ "GetOwner": () => owner
+});
+
+AddMock(SYSTEM_ENTITY, IID_PlayerManager, {
+ "GetPlayerByID": (id) => owner
+});
+
+AddMock(SYSTEM_ENTITY, IID_ObstructionManager, {
+ "IsInTargetRange": (ent, target, min, max, invert) => true
+});
+
+let cmpPlayer = ConstructComponent(owner, "Player", {
+ "SpyCostMultiplier": 1,
+ "BarterMultiplier": {}
+});
+let playerSpy = new Spy(cmpPlayer, "AddResources");
+
+let cmpTreasure = ConstructComponent(treasure, "Treasure", {
+ "CollectTime": "1000",
+ "Resources": {
+ "Food": "10"
+ }
+});
+cmpTreasure.OnOwnershipChanged({ "to": 0 });
+
+let cmpTreasurer = ConstructComponent(treasurer, "TreasureCollecter", {
+ "MaxDistance": "2.0"
+});
+
+TS_ASSERT(cmpTreasurer.StartCollecting(treasure));
+cmpTimer.OnUpdate({ "turnLength": 1 });
+TS_ASSERT_EQUALS(playerSpy._called, 1);
Property changes on: ps/trunk/binaries/data/mods/public/simulation/components/tests/test_Treasures.js
___________________________________________________________________
Added: svn:eol-style
## -0,0 +1 ##
+native
\ No newline at end of property
Added: svn:mime-type
## -0,0 +1 ##
+text/plain
\ No newline at end of property
Index: ps/trunk/binaries/data/mods/public/simulation/helpers/Commands.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/helpers/Commands.js (revision 24988)
+++ ps/trunk/binaries/data/mods/public/simulation/helpers/Commands.js (revision 24989)
@@ -1,1762 +1,1769 @@
// Setting this to true will display some warnings when commands
// are likely to fail, which may be useful for debugging AIs
var g_DebugCommands = false;
function ProcessCommand(player, cmd)
{
let cmpPlayer = QueryPlayerIDInterface(player);
if (!cmpPlayer)
return;
let data = {
"cmpPlayer": cmpPlayer,
"controlAllUnits": cmpPlayer.CanControlAllUnits()
};
if (cmd.entities)
data.entities = FilterEntityList(cmd.entities, player, data.controlAllUnits);
// TODO: queuing order and forcing formations doesn't really work.
// To play nice, we'll still no-formation queued order if units are in formation
// but the opposite perhaps ought to be implemented.
if (!cmd.queued || cmd.formation == NULL_FORMATION)
data.formation = cmd.formation || undefined;
// Allow focusing the camera on recent commands
let commandData = {
"type": "playercommand",
"players": [player],
"cmd": cmd
};
// Save the position, since the GUI event is received after the unit died
if (cmd.type == "delete-entities")
{
let cmpPosition = cmd.entities[0] && Engine.QueryInterface(cmd.entities[0], IID_Position);
commandData.position = cmpPosition && cmpPosition.IsInWorld() && cmpPosition.GetPosition2D();
}
let cmpGuiInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface);
cmpGuiInterface.PushNotification(commandData);
// Note: checks of UnitAI targets are not robust enough here, as ownership
// can change after the order is issued, they should be checked by UnitAI
// when the specific behavior (e.g. attack, garrison) is performed.
// (Also it's not ideal if a command silently fails, it's nicer if UnitAI
// moves the entities closer to the target before giving up.)
// Now handle various commands
if (g_Commands[cmd.type])
{
var cmpTrigger = Engine.QueryInterface(SYSTEM_ENTITY, IID_Trigger);
cmpTrigger.CallEvent("PlayerCommand", { "player": player, "cmd": cmd });
g_Commands[cmd.type](player, cmd, data);
}
else
error("Invalid command: unknown command type: "+uneval(cmd));
}
var g_Commands = {
"aichat": function(player, cmd, data)
{
var cmpGuiInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface);
var notification = { "players": [player] };
for (var key in cmd)
notification[key] = cmd[key];
cmpGuiInterface.PushNotification(notification);
},
"cheat": function(player, cmd, data)
{
Cheat(cmd);
},
+ "collect-treasure": function(player, cmd, data)
+ {
+ GetFormationUnitAIs(data.entities, player, cmd, data.formation).forEach(cmpUnitAI => {
+ cmpUnitAI.CollectTreasure(cmd.target, cmd.queued);
+ });
+ },
+
"diplomacy": function(player, cmd, data)
{
let cmpCeasefireManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_CeasefireManager);
if (data.cmpPlayer.GetLockTeams() ||
cmpCeasefireManager && cmpCeasefireManager.IsCeasefireActive())
return;
switch(cmd.to)
{
case "ally":
data.cmpPlayer.SetAlly(cmd.player);
break;
case "neutral":
data.cmpPlayer.SetNeutral(cmd.player);
break;
case "enemy":
data.cmpPlayer.SetEnemy(cmd.player);
break;
default:
warn("Invalid command: Could not set "+player+" diplomacy status of player "+cmd.player+" to "+cmd.to);
}
var cmpGuiInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface);
cmpGuiInterface.PushNotification({
"type": "diplomacy",
"players": [player],
"targetPlayer": cmd.player,
"status": cmd.to
});
},
"tribute": function(player, cmd, data)
{
data.cmpPlayer.TributeResource(cmd.player, cmd.amounts);
},
"control-all": function(player, cmd, data)
{
if (!data.cmpPlayer.GetCheatsEnabled())
return;
var cmpGuiInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface);
cmpGuiInterface.PushNotification({
"type": "aichat",
"players": [player],
"message": markForTranslation("(Cheat - control all units)")
});
data.cmpPlayer.SetControlAllUnits(cmd.flag);
},
"reveal-map": function(player, cmd, data)
{
if (!data.cmpPlayer.GetCheatsEnabled())
return;
var cmpGuiInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface);
cmpGuiInterface.PushNotification({
"type": "aichat",
"players": [player],
"message": markForTranslation("(Cheat - reveal map)")
});
// Reveal the map for all players, not just the current player,
// primarily to make it obvious to everyone that the player is cheating
var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
cmpRangeManager.SetLosRevealAll(-1, cmd.enable);
},
"walk": function(player, cmd, data)
{
GetFormationUnitAIs(data.entities, player, cmd, data.formation).forEach(cmpUnitAI => {
cmpUnitAI.Walk(cmd.x, cmd.z, cmd.queued);
});
},
"walk-custom": function(player, cmd, data)
{
for (let ent in data.entities)
GetFormationUnitAIs([data.entities[ent]], player, cmd, data.formation).forEach(cmpUnitAI => {
cmpUnitAI.Walk(cmd.targetPositions[ent].x, cmd.targetPositions[ent].y, cmd.queued);
});
},
"walk-to-range": function(player, cmd, data)
{
// Only used by the AI
for (let ent of data.entities)
{
var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
if (cmpUnitAI)
cmpUnitAI.WalkToPointRange(cmd.x, cmd.z, cmd.min, cmd.max, cmd.queued);
}
},
"attack-walk": function(player, cmd, data)
{
let allowCapture = cmd.allowCapture || cmd.allowCapture == null;
GetFormationUnitAIs(data.entities, player, cmd, data.formation).forEach(cmpUnitAI => {
cmpUnitAI.WalkAndFight(cmd.x, cmd.z, cmd.targetClasses, allowCapture, cmd.queued);
});
},
"attack-walk-custom": function(player, cmd, data)
{
let allowCapture = cmd.allowCapture || cmd.allowCapture == null;
for (let ent in data.entities)
GetFormationUnitAIs([data.entities[ent]], player, cmd, data.formation).forEach(cmpUnitAI => {
cmpUnitAI.WalkAndFight(cmd.targetPositions[ent].x, cmd.targetPositions[ent].y, cmd.targetClasses, allowCapture, cmd.queued);
});
},
"attack": function(player, cmd, data)
{
let allowCapture = cmd.allowCapture || cmd.allowCapture == null;
if (g_DebugCommands && !allowCapture &&
!(IsOwnedByEnemyOfPlayer(player, cmd.target) || IsOwnedByNeutralOfPlayer(player, cmd.target)))
warn("Invalid command: attack target is not owned by enemy of player "+player+": "+uneval(cmd));
GetFormationUnitAIs(data.entities, player, cmd, data.formation).forEach(cmpUnitAI => {
cmpUnitAI.Attack(cmd.target, allowCapture, cmd.queued);
});
},
"patrol": function(player, cmd, data)
{
let allowCapture = cmd.allowCapture || cmd.allowCapture == null;
GetFormationUnitAIs(data.entities, player, cmd, data.formation).forEach(cmpUnitAI =>
cmpUnitAI.Patrol(cmd.x, cmd.z, cmd.targetClasses, allowCapture, cmd.queued)
);
},
"heal": function(player, cmd, data)
{
if (g_DebugCommands && !(IsOwnedByPlayer(player, cmd.target) || IsOwnedByAllyOfPlayer(player, cmd.target)))
warn("Invalid command: heal target is not owned by player "+player+" or their ally: "+uneval(cmd));
GetFormationUnitAIs(data.entities, player, cmd, data.formation).forEach(cmpUnitAI => {
cmpUnitAI.Heal(cmd.target, cmd.queued);
});
},
"repair": function(player, cmd, data)
{
// This covers both repairing damaged buildings, and constructing unfinished foundations
if (g_DebugCommands && !IsOwnedByAllyOfPlayer(player, cmd.target))
warn("Invalid command: repair target is not owned by ally of player "+player+": "+uneval(cmd));
GetFormationUnitAIs(data.entities, player, cmd, data.formation).forEach(cmpUnitAI => {
cmpUnitAI.Repair(cmd.target, cmd.autocontinue, cmd.queued);
});
},
"gather": function(player, cmd, data)
{
if (g_DebugCommands && !(IsOwnedByPlayer(player, cmd.target) || IsOwnedByGaia(cmd.target)))
warn("Invalid command: resource is not owned by gaia or player "+player+": "+uneval(cmd));
GetFormationUnitAIs(data.entities, player, cmd, data.formation).forEach(cmpUnitAI => {
cmpUnitAI.Gather(cmd.target, cmd.queued);
});
},
"gather-near-position": function(player, cmd, data)
{
GetFormationUnitAIs(data.entities, player, cmd, data.formation).forEach(cmpUnitAI => {
cmpUnitAI.GatherNearPosition(cmd.x, cmd.z, cmd.resourceType, cmd.resourceTemplate, cmd.queued);
});
},
"returnresource": function(player, cmd, data)
{
if (g_DebugCommands && !IsOwnedByPlayer(player, cmd.target))
warn("Invalid command: dropsite is not owned by player "+player+": "+uneval(cmd));
GetFormationUnitAIs(data.entities, player, cmd, data.formation).forEach(cmpUnitAI => {
cmpUnitAI.ReturnResource(cmd.target, cmd.queued);
});
},
"back-to-work": function(player, cmd, data)
{
for (let ent of data.entities)
{
var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
if(!cmpUnitAI || !cmpUnitAI.BackToWork())
notifyBackToWorkFailure(player);
}
},
"remove-guard": function(player, cmd, data)
{
for (let ent of data.entities)
{
var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
if (cmpUnitAI)
cmpUnitAI.RemoveGuard();
}
},
"train": function(player, cmd, data)
{
if (!Number.isInteger(cmd.count) || cmd.count <= 0)
{
warn("Invalid command: can't train " + uneval(cmd.count) + " units");
return;
}
// Check entity limits
var template = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager).GetTemplate(cmd.template);
var unitCategory = null;
if (template.TrainingRestrictions)
unitCategory = template.TrainingRestrictions.Category;
// Verify that the building(s) can be controlled by the player
if (data.entities.length <= 0)
{
if (g_DebugCommands)
warn("Invalid command: training building(s) cannot be controlled by player "+player+": "+uneval(cmd));
return;
}
for (let ent of data.entities)
{
if (unitCategory)
{
var cmpPlayerEntityLimits = QueryOwnerInterface(ent, IID_EntityLimits);
if (cmpPlayerEntityLimits && !cmpPlayerEntityLimits.AllowedToTrain(unitCategory, cmd.count, cmd.template, template.TrainingRestrictions.MatchLimit))
{
if (g_DebugCommands)
warn(unitCategory + " train limit is reached: " + uneval(cmd));
continue;
}
}
var cmpTechnologyManager = QueryOwnerInterface(ent, IID_TechnologyManager);
if (cmpTechnologyManager && !cmpTechnologyManager.CanProduce(cmd.template))
{
if (g_DebugCommands)
warn("Invalid command: training requires unresearched technology: " + uneval(cmd));
continue;
}
var queue = Engine.QueryInterface(ent, IID_ProductionQueue);
// Check if the building can train the unit
// TODO: the AI API does not take promotion technologies into account for the list
// of trainable units (taken directly from the unit template). Here is a temporary fix.
if (queue && data.cmpPlayer.IsAI())
{
var list = queue.GetEntitiesList();
if (list.indexOf(cmd.template) === -1 && cmd.promoted)
{
for (var promoted of cmd.promoted)
{
if (list.indexOf(promoted) === -1)
continue;
cmd.template = promoted;
break;
}
}
}
if (queue && queue.GetEntitiesList().indexOf(cmd.template) != -1)
if ("metadata" in cmd)
queue.AddBatch(cmd.template, "unit", +cmd.count, cmd.metadata);
else
queue.AddBatch(cmd.template, "unit", +cmd.count);
}
},
"research": function(player, cmd, data)
{
var cmpTechnologyManager = QueryOwnerInterface(cmd.entity, IID_TechnologyManager);
if (cmpTechnologyManager && !cmpTechnologyManager.CanResearch(cmd.template))
{
if (g_DebugCommands)
warn("Invalid command: Requirements to research technology are not met: " + uneval(cmd));
return;
}
var queue = Engine.QueryInterface(cmd.entity, IID_ProductionQueue);
if (queue)
queue.AddBatch(cmd.template, "technology");
},
"stop-production": function(player, cmd, data)
{
var queue = Engine.QueryInterface(cmd.entity, IID_ProductionQueue);
if (queue)
queue.RemoveBatch(cmd.id);
},
"construct": function(player, cmd, data)
{
TryConstructBuilding(player, data.cmpPlayer, data.controlAllUnits, cmd);
},
"construct-wall": function(player, cmd, data)
{
TryConstructWall(player, data.cmpPlayer, data.controlAllUnits, cmd);
},
"delete-entities": function(player, cmd, data)
{
for (let ent of data.entities)
{
if (!data.controlAllUnits)
{
let cmpIdentity = Engine.QueryInterface(ent, IID_Identity);
if (cmpIdentity && cmpIdentity.IsUndeletable())
continue;
let cmpCapturable = QueryMiragedInterface(ent, IID_Capturable);
if (cmpCapturable &&
cmpCapturable.GetCapturePoints()[player] < cmpCapturable.GetMaxCapturePoints() / 2)
continue;
let cmpResourceSupply = QueryMiragedInterface(ent, IID_ResourceSupply);
if (cmpResourceSupply && cmpResourceSupply.GetKillBeforeGather())
continue;
}
let cmpMirage = Engine.QueryInterface(ent, IID_Mirage);
if (cmpMirage)
{
let cmpMiragedHealth = Engine.QueryInterface(cmpMirage.parent, IID_Health);
if (cmpMiragedHealth)
cmpMiragedHealth.Kill();
else
Engine.DestroyEntity(cmpMirage.parent);
Engine.DestroyEntity(ent);
continue;
}
let cmpHealth = Engine.QueryInterface(ent, IID_Health);
if (cmpHealth)
cmpHealth.Kill();
else
Engine.DestroyEntity(ent);
}
},
"set-rallypoint": function(player, cmd, data)
{
for (let ent of data.entities)
{
var cmpRallyPoint = Engine.QueryInterface(ent, IID_RallyPoint);
if (cmpRallyPoint)
{
if (!cmd.queued)
cmpRallyPoint.Unset();
cmpRallyPoint.AddPosition(cmd.x, cmd.z);
cmpRallyPoint.AddData(clone(cmd.data));
}
}
},
"unset-rallypoint": function(player, cmd, data)
{
for (let ent of data.entities)
{
var cmpRallyPoint = Engine.QueryInterface(ent, IID_RallyPoint);
if (cmpRallyPoint)
cmpRallyPoint.Reset();
}
},
"resign": function(player, cmd, data)
{
data.cmpPlayer.SetState("defeated", markForTranslation("%(player)s has resigned."));
},
"garrison": function(player, cmd, data)
{
if (!CanPlayerOrAllyControlUnit(cmd.target, player, data.controlAllUnits))
{
if (g_DebugCommands)
warn("Invalid command: garrison target cannot be controlled by player "+player+" (or ally): "+uneval(cmd));
return;
}
GetFormationUnitAIs(data.entities, player, cmd, data.formation).forEach(cmpUnitAI => {
cmpUnitAI.Garrison(cmd.target, cmd.queued);
});
},
"guard": function(player, cmd, data)
{
if (!IsOwnedByPlayerOrMutualAlly(cmd.target, player, data.controlAllUnits))
{
if (g_DebugCommands)
warn("Invalid command: Guard/escort target is not owned by player " + player + " or ally thereof: " + uneval(cmd));
return;
}
GetFormationUnitAIs(data.entities, player, cmd, data.formation).forEach(cmpUnitAI => {
cmpUnitAI.Guard(cmd.target, cmd.queued);
});
},
"stop": function(player, cmd, data)
{
GetFormationUnitAIs(data.entities, player, cmd, data.formation).forEach(cmpUnitAI => {
cmpUnitAI.Stop(cmd.queued);
});
},
"unload": function(player, cmd, data)
{
if (!CanPlayerOrAllyControlUnit(cmd.garrisonHolder, player, data.controlAllUnits))
{
if (g_DebugCommands)
warn("Invalid command: unload target cannot be controlled by player "+player+" (or ally): "+uneval(cmd));
return;
}
var cmpGarrisonHolder = Engine.QueryInterface(cmd.garrisonHolder, IID_GarrisonHolder);
var notUngarrisoned = 0;
// The owner can ungarrison every garrisoned unit
if (IsOwnedByPlayer(player, cmd.garrisonHolder))
data.entities = cmd.entities;
for (let ent of data.entities)
if (!cmpGarrisonHolder || !cmpGarrisonHolder.Unload(ent))
++notUngarrisoned;
if (notUngarrisoned != 0)
notifyUnloadFailure(player, cmd.garrisonHolder);
},
"unload-template": function(player, cmd, data)
{
var entities = FilterEntityListWithAllies(cmd.garrisonHolders, player, data.controlAllUnits);
for (let garrisonHolder of entities)
{
var cmpGarrisonHolder = Engine.QueryInterface(garrisonHolder, IID_GarrisonHolder);
if (cmpGarrisonHolder)
{
// Only the owner of the garrisonHolder may unload entities from any owners
if (!IsOwnedByPlayer(player, garrisonHolder) && !data.controlAllUnits
&& player != +cmd.owner)
continue;
if (!cmpGarrisonHolder.UnloadTemplate(cmd.template, cmd.owner, cmd.all))
notifyUnloadFailure(player, garrisonHolder);
}
}
},
"unload-all-by-owner": function(player, cmd, data)
{
var entities = FilterEntityListWithAllies(cmd.garrisonHolders, player, data.controlAllUnits);
for (let garrisonHolder of entities)
{
var cmpGarrisonHolder = Engine.QueryInterface(garrisonHolder, IID_GarrisonHolder);
if (!cmpGarrisonHolder || !cmpGarrisonHolder.UnloadAllByOwner(player))
notifyUnloadFailure(player, garrisonHolder);
}
},
"unload-all": function(player, cmd, data)
{
var entities = FilterEntityList(cmd.garrisonHolders, player, data.controlAllUnits);
for (let garrisonHolder of entities)
{
var cmpGarrisonHolder = Engine.QueryInterface(garrisonHolder, IID_GarrisonHolder);
if (!cmpGarrisonHolder || !cmpGarrisonHolder.UnloadAll())
notifyUnloadFailure(player, garrisonHolder);
}
},
"alert-raise": function(player, cmd, data)
{
for (let ent of data.entities)
{
var cmpAlertRaiser = Engine.QueryInterface(ent, IID_AlertRaiser);
if (cmpAlertRaiser)
cmpAlertRaiser.RaiseAlert();
}
},
"alert-end": function(player, cmd, data)
{
for (let ent of data.entities)
{
var cmpAlertRaiser = Engine.QueryInterface(ent, IID_AlertRaiser);
if (cmpAlertRaiser)
cmpAlertRaiser.EndOfAlert();
}
},
"formation": function(player, cmd, data)
{
GetFormationUnitAIs(data.entities, player, cmd, data.formation, true).forEach(cmpUnitAI => {
cmpUnitAI.MoveIntoFormation(cmd);
});
},
"promote": function(player, cmd, data)
{
if (!data.cmpPlayer.GetCheatsEnabled())
return;
var cmpGuiInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface);
cmpGuiInterface.PushNotification({
"type": "aichat",
"players": [player],
"message": markForTranslation("(Cheat - promoted units)"),
"translateMessage": true
});
for (let ent of cmd.entities)
{
var cmpPromotion = Engine.QueryInterface(ent, IID_Promotion);
if (cmpPromotion)
cmpPromotion.IncreaseXp(cmpPromotion.GetRequiredXp() - cmpPromotion.GetCurrentXp());
}
},
"stance": function(player, cmd, data)
{
for (let ent of data.entities)
{
var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
if (cmpUnitAI && !cmpUnitAI.IsTurret())
cmpUnitAI.SwitchToStance(cmd.name);
}
},
"lock-gate": function(player, cmd, data)
{
for (let ent of data.entities)
{
var cmpGate = Engine.QueryInterface(ent, IID_Gate);
if (!cmpGate)
continue;
if (cmd.lock)
cmpGate.LockGate();
else
cmpGate.UnlockGate();
}
},
"setup-trade-route": function(player, cmd, data)
{
GetFormationUnitAIs(data.entities, player, cmd, data.formation).forEach(cmpUnitAI => {
cmpUnitAI.SetupTradeRoute(cmd.target, cmd.source, cmd.route, cmd.queued);
});
},
"cancel-setup-trade-route": function(player, cmd, data)
{
GetFormationUnitAIs(data.entities, player, cmd, data.formation).forEach(cmpUnitAI => {
cmpUnitAI.CancelSetupTradeRoute(cmd.target);
});
},
"set-trading-goods": function(player, cmd, data)
{
data.cmpPlayer.SetTradingGoods(cmd.tradingGoods);
},
"barter": function(player, cmd, data)
{
var cmpBarter = Engine.QueryInterface(SYSTEM_ENTITY, IID_Barter);
cmpBarter.ExchangeResources(player, cmd.sell, cmd.buy, cmd.amount);
},
"set-shading-color": function(player, cmd, data)
{
// Prevent multiplayer abuse
if (!data.cmpPlayer.IsAI())
return;
// Debug command to make an entity brightly colored
for (let ent of cmd.entities)
{
var cmpVisual = Engine.QueryInterface(ent, IID_Visual);
if (cmpVisual)
cmpVisual.SetShadingColor(cmd.rgb[0], cmd.rgb[1], cmd.rgb[2], 0); // alpha isn't used so just send 0
}
},
"pack": function(player, cmd, data)
{
for (let ent of data.entities)
{
var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
if (!cmpUnitAI)
continue;
if (cmd.pack)
cmpUnitAI.Pack(cmd.queued);
else
cmpUnitAI.Unpack(cmd.queued);
}
},
"cancel-pack": function(player, cmd, data)
{
for (let ent of data.entities)
{
var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
if (!cmpUnitAI)
continue;
if (cmd.pack)
cmpUnitAI.CancelPack(cmd.queued);
else
cmpUnitAI.CancelUnpack(cmd.queued);
}
},
"upgrade": function(player, cmd, data)
{
for (let ent of data.entities)
{
var cmpUpgrade = Engine.QueryInterface(ent, IID_Upgrade);
if (!cmpUpgrade || !cmpUpgrade.CanUpgradeTo(cmd.template))
continue;
if (cmpUpgrade.WillCheckPlacementRestrictions(cmd.template) && ObstructionsBlockingTemplateChange(ent, cmd.template))
{
var cmpGUIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface);
cmpGUIInterface.PushNotification({
"players": [player],
"message": markForTranslation("Cannot upgrade as distance requirements are not verified or terrain is obstructed.")
});
continue;
}
// Check entity limits
var cmpEntityLimits = QueryPlayerIDInterface(player, IID_EntityLimits);
if (cmpEntityLimits && !cmpEntityLimits.AllowedToReplace(ent, cmd.template))
{
if (g_DebugCommands)
warn("Invalid command: build limits check failed for player " + player + ": " + uneval(cmd));
continue;
}
let cmpTechnologyManager = QueryOwnerInterface(ent, IID_TechnologyManager);
let requiredTechnology = cmpUpgrade.GetRequiredTechnology(cmd.template);
if (requiredTechnology && (!cmpTechnologyManager || !cmpTechnologyManager.IsTechnologyResearched(requiredTechnology)))
{
if (g_DebugCommands)
warn("Invalid command: upgrading is not possible for this player or requires unresearched technology: " + uneval(cmd));
continue;
}
cmpUpgrade.Upgrade(cmd.template, data.cmpPlayer);
}
},
"cancel-upgrade": function(player, cmd, data)
{
for (let ent of data.entities)
{
let cmpUpgrade = Engine.QueryInterface(ent, IID_Upgrade);
if (cmpUpgrade)
cmpUpgrade.CancelUpgrade(player);
}
},
"attack-request": function(player, cmd, data)
{
// Send a chat message to human players
var cmpGuiInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface);
cmpGuiInterface.PushNotification({
"type": "aichat",
"players": [player],
"message": "/allies " + markForTranslation("Attack against %(_player_)s requested."),
"translateParameters": ["_player_"],
"parameters": { "_player_": cmd.player }
});
// And send an attackRequest event to the AIs
let cmpAIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_AIInterface);
if (cmpAIInterface)
cmpAIInterface.PushEvent("AttackRequest", cmd);
},
"spy-request": function(player, cmd, data)
{
let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
let ent = pickRandom(cmpRangeManager.GetEntitiesByPlayer(cmd.player).filter(ent => {
let cmpVisionSharing = Engine.QueryInterface(ent, IID_VisionSharing);
return cmpVisionSharing && cmpVisionSharing.IsBribable() && !cmpVisionSharing.ShareVisionWith(player);
}));
let cmpGUIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface);
cmpGUIInterface.PushNotification({
"type": "spy-response",
"players": [player],
"target": cmd.player,
"entity": ent
});
if (ent)
Engine.QueryInterface(ent, IID_VisionSharing).AddSpy(cmd.source);
else
{
let template = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager).GetTemplate("special/spy");
IncurBribeCost(template, player, cmd.player, true);
// update statistics for failed bribes
let cmpBribesStatisticsTracker = QueryPlayerIDInterface(player, IID_StatisticsTracker);
if (cmpBribesStatisticsTracker)
cmpBribesStatisticsTracker.IncreaseFailedBribesCounter();
cmpGUIInterface.PushNotification({
"type": "text",
"players": [player],
"message": markForTranslation("There are no bribable units"),
"translateMessage": true
});
}
},
"diplomacy-request": function(player, cmd, data)
{
let cmpAIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_AIInterface);
if (cmpAIInterface)
cmpAIInterface.PushEvent("DiplomacyRequest", cmd);
},
"tribute-request": function(player, cmd, data)
{
let cmpAIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_AIInterface);
if (cmpAIInterface)
cmpAIInterface.PushEvent("TributeRequest", cmd);
},
"dialog-answer": function(player, cmd, data)
{
// Currently nothing. Triggers can read it anyway, and send this
// message to any component you like.
},
"set-dropsite-sharing": function(player, cmd, data)
{
for (let ent of data.entities)
{
let cmpResourceDropsite = Engine.QueryInterface(ent, IID_ResourceDropsite);
if (cmpResourceDropsite && cmpResourceDropsite.IsSharable())
cmpResourceDropsite.SetSharing(cmd.shared);
}
},
};
/**
* Sends a GUI notification about unit(s) that failed to ungarrison.
*/
function notifyUnloadFailure(player, garrisonHolder)
{
var cmpGUIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface);
cmpGUIInterface.PushNotification({
"type": "text",
"players": [player],
"message": markForTranslation("Unable to ungarrison unit(s)"),
"translateMessage": true
});
}
/**
* Sends a GUI notification about worker(s) that failed to go back to work.
*/
function notifyBackToWorkFailure(player)
{
var cmpGUIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface);
cmpGUIInterface.PushNotification({
"type": "text",
"players": [player],
"message": markForTranslation("Some unit(s) can't go back to work"),
"translateMessage": true
});
}
/**
* Sends a GUI notification about entities that can't be controlled.
* @param {number} player - The player-ID of the player that needs to receive this message.
*/
function notifyOrderFailure(entity, player)
{
let cmpIdentity = Engine.QueryInterface(entity, IID_Identity);
if (!cmpIdentity)
return;
let cmpGUIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface);
cmpGUIInterface.PushNotification({
"type": "text",
"players": [player],
"message": sprintf(markForTranslation("%(unit)s can't be controlled."), {
"unit": cmpIdentity.GetGenericName()
}),
"translateMessage": true
});
}
/**
* Get some information about the formations used by entities.
*/
function ExtractFormations(ents)
{
let entities = []; // Entities with UnitAI.
let members = {}; // { formationentity: [ent, ent, ...], ... }
let templates = {}; // { formationentity: template }
for (let ent of ents)
{
let cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
if (!cmpUnitAI)
continue;
entities.push(ent);
let fid = cmpUnitAI.GetFormationController();
if (fid == INVALID_ENTITY)
continue;
if (!members[fid])
{
members[fid] = [];
templates[fid] = cmpUnitAI.GetFormationTemplate();
}
members[fid].push(ent);
}
return {
"entities": entities,
"members": members,
"templates": templates
};
}
/**
* Tries to find the best angle to put a dock at a given position
* Taken from GuiInterface.js
*/
function GetDockAngle(template, x, z)
{
var cmpTerrain = Engine.QueryInterface(SYSTEM_ENTITY, IID_Terrain);
var cmpWaterManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_WaterManager);
if (!cmpTerrain || !cmpWaterManager)
return undefined;
// Get footprint size
var halfSize = 0;
if (template.Footprint.Square)
halfSize = Math.max(template.Footprint.Square["@depth"], template.Footprint.Square["@width"])/2;
else if (template.Footprint.Circle)
halfSize = template.Footprint.Circle["@radius"];
/* Find direction of most open water, algorithm:
* 1. Pick points in a circle around dock
* 2. If point is in water, add to array
* 3. Scan array looking for consecutive points
* 4. Find longest sequence of consecutive points
* 5. If sequence equals all points, no direction can be determined,
* expand search outward and try (1) again
* 6. Calculate angle using average of sequence
*/
const numPoints = 16;
for (var dist = 0; dist < 4; ++dist)
{
var waterPoints = [];
for (var i = 0; i < numPoints; ++i)
{
var angle = (i/numPoints)*2*Math.PI;
var d = halfSize*(dist+1);
var nx = x - d*Math.sin(angle);
var nz = z + d*Math.cos(angle);
if (cmpTerrain.GetGroundLevel(nx, nz) < cmpWaterManager.GetWaterLevel(nx, nz))
waterPoints.push(i);
}
var consec = [];
var length = waterPoints.length;
if (!length)
continue;
for (var i = 0; i < length; ++i)
{
var count = 0;
for (let j = 0; j < length - 1; ++j)
{
if ((waterPoints[(i + j) % length] + 1) % numPoints == waterPoints[(i + j + 1) % length])
++count;
else
break;
}
consec[i] = count;
}
var start = 0;
var count = 0;
for (var c in consec)
{
if (consec[c] > count)
{
start = c;
count = consec[c];
}
}
// If we've found a shoreline, stop searching
if (count != numPoints-1)
return -((waterPoints[start] + consec[start]/2) % numPoints) / numPoints * 2 * Math.PI;
}
return undefined;
}
/**
* Attempts to construct a building using the specified parameters.
* Returns true on success, false on failure.
*/
function TryConstructBuilding(player, cmpPlayer, controlAllUnits, cmd)
{
// Message structure:
// {
// "type": "construct",
// "entities": [...], // entities that will be ordered to construct the building (if applicable)
// "template": "...", // template name of the entity being constructed
// "x": ...,
// "z": ...,
// "angle": ...,
// "metadata": "...", // AI metadata of the building
// "actorSeed": ...,
// "autorepair": true, // whether to automatically start constructing/repairing the new foundation
// "autocontinue": true, // whether to automatically gather/build/etc after finishing this
// "queued": true, // whether to add the construction/repairing of this foundation to entities' queue (if applicable)
// "obstructionControlGroup": ..., // Optional; the obstruction control group ID that should be set for this building prior to obstruction
// // testing to determine placement validity. If specified, must be a valid control group ID (> 0).
// "obstructionControlGroup2": ..., // Optional; secondary obstruction control group ID that should be set for this building prior to obstruction
// // testing to determine placement validity. May be INVALID_ENTITY.
// }
/*
* Construction process:
* . Take resources away immediately.
* . Create a foundation entity with 1hp, 0% build progress.
* . Increase hp and build progress up to 100% when people work on it.
* . If it's destroyed, an appropriate fraction of the resource cost is refunded.
* . If it's completed, it gets replaced with the real building.
*/
// Check whether we can control these units
var entities = FilterEntityList(cmd.entities, player, controlAllUnits);
if (!entities.length)
return false;
var foundationTemplate = "foundation|" + cmd.template;
// Tentatively create the foundation (we might find later that it's a invalid build command)
var ent = Engine.AddEntity(foundationTemplate);
if (ent == INVALID_ENTITY)
{
// Error (e.g. invalid template names)
error("Error creating foundation entity for '" + cmd.template + "'");
return false;
}
// If it's a dock, get the right angle.
var template = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager).GetTemplate(cmd.template);
var angle = cmd.angle;
if (template.BuildRestrictions.PlacementType === "shore")
{
let angleDock = GetDockAngle(template, cmd.x, cmd.z);
if (angleDock !== undefined)
angle = angleDock;
}
// Move the foundation to the right place
var cmpPosition = Engine.QueryInterface(ent, IID_Position);
cmpPosition.JumpTo(cmd.x, cmd.z);
cmpPosition.SetYRotation(angle);
// Set the obstruction control group if needed
if (cmd.obstructionControlGroup || cmd.obstructionControlGroup2)
{
var cmpObstruction = Engine.QueryInterface(ent, IID_Obstruction);
// primary control group must always be valid
if (cmd.obstructionControlGroup)
{
if (cmd.obstructionControlGroup <= 0)
warn("[TryConstructBuilding] Invalid primary obstruction control group " + cmd.obstructionControlGroup + " received; must be > 0");
cmpObstruction.SetControlGroup(cmd.obstructionControlGroup);
}
if (cmd.obstructionControlGroup2)
cmpObstruction.SetControlGroup2(cmd.obstructionControlGroup2);
}
// Make it owned by the current player
var cmpOwnership = Engine.QueryInterface(ent, IID_Ownership);
cmpOwnership.SetOwner(player);
// Check whether building placement is valid
var cmpBuildRestrictions = Engine.QueryInterface(ent, IID_BuildRestrictions);
if (cmpBuildRestrictions)
{
var ret = cmpBuildRestrictions.CheckPlacement();
if (!ret.success)
{
if (g_DebugCommands)
warn("Invalid command: build restrictions check failed with '"+ret.message+"' for player "+player+": "+uneval(cmd));
var cmpGuiInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface);
ret.players = [player];
cmpGuiInterface.PushNotification(ret);
// Remove the foundation because the construction was aborted
// move it out of world because it's not destroyed immediately.
cmpPosition.MoveOutOfWorld();
Engine.DestroyEntity(ent);
return false;
}
}
else
error("cmpBuildRestrictions not defined");
// Check entity limits
var cmpEntityLimits = QueryPlayerIDInterface(player, IID_EntityLimits);
if (cmpEntityLimits && !cmpEntityLimits.AllowedToBuild(cmpBuildRestrictions.GetCategory()))
{
if (g_DebugCommands)
warn("Invalid command: build limits check failed for player "+player+": "+uneval(cmd));
// Remove the foundation because the construction was aborted
cmpPosition.MoveOutOfWorld();
Engine.DestroyEntity(ent);
return false;
}
var cmpTechnologyManager = QueryPlayerIDInterface(player, IID_TechnologyManager);
if (cmpTechnologyManager && !cmpTechnologyManager.CanProduce(cmd.template))
{
if (g_DebugCommands)
warn("Invalid command: required technology check failed for player "+player+": "+uneval(cmd));
var cmpGuiInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface);
cmpGuiInterface.PushNotification({
"type": "text",
"players": [player],
"message": markForTranslation("The building's technology requirements are not met."),
"translateMessage": true
});
// Remove the foundation because the construction was aborted
cmpPosition.MoveOutOfWorld();
Engine.DestroyEntity(ent);
}
// We need the cost after tech and aura modifications
// To calculate this with an entity requires ownership, so use the template instead
let cmpCost = Engine.QueryInterface(ent, IID_Cost);
let costs = cmpCost.GetResourceCosts(player);
if (!cmpPlayer.TrySubtractResources(costs))
{
if (g_DebugCommands)
warn("Invalid command: building cost check failed for player "+player+": "+uneval(cmd));
Engine.DestroyEntity(ent);
cmpPosition.MoveOutOfWorld();
return false;
}
var cmpVisual = Engine.QueryInterface(ent, IID_Visual);
if (cmpVisual && cmd.actorSeed !== undefined)
cmpVisual.SetActorSeed(cmd.actorSeed);
// Initialise the foundation
var cmpFoundation = Engine.QueryInterface(ent, IID_Foundation);
cmpFoundation.InitialiseConstruction(player, cmd.template);
// send Metadata info if any
if (cmd.metadata)
Engine.PostMessage(ent, MT_AIMetadata, { "id": ent, "metadata" : cmd.metadata, "owner" : player } );
// Tell the units to start building this new entity
if (cmd.autorepair)
{
ProcessCommand(player, {
"type": "repair",
"entities": entities,
"target": ent,
"autocontinue": cmd.autocontinue,
"queued": cmd.queued,
"formation": cmd.formation || undefined
});
}
return ent;
}
function TryConstructWall(player, cmpPlayer, controlAllUnits, cmd)
{
// 'cmd' message structure:
// {
// "type": "construct-wall",
// "entities": [...], // entities that will be ordered to construct the wall (if applicable)
// "pieces": [ // ordered list of information about the pieces making up the wall (towers, wall segments, ...)
// {
// "template": "...", // one of the templates from the wallset
// "x": ...,
// "z": ...,
// "angle": ...,
// },
// ...
// ],
// "wallSet": {
// "templates": {
// "tower": // tower template name
// "long": // long wall segment template name
// ... // etc.
// },
// "maxTowerOverlap": ...,
// "minTowerOverlap": ...,
// },
// "startSnappedEntity": // optional; entity ID of tower being snapped to at the starting side of the wall
// "endSnappedEntity": // optional; entity ID of tower being snapped to at the ending side of the wall
// "autorepair": true, // whether to automatically start constructing/repairing the new foundation
// "autocontinue": true, // whether to automatically gather/build/etc after finishing this
// "queued": true, // whether to add the construction/repairing of this wall's pieces to entities' queue (if applicable)
// }
if (cmd.pieces.length <= 0)
return;
if (cmd.startSnappedEntity && cmd.pieces[0].template == cmd.wallSet.templates.tower)
{
error("[TryConstructWall] Starting wall piece cannot be a tower (" + cmd.wallSet.templates.tower + ") when snapping at the starting side");
return;
}
if (cmd.endSnappedEntity && cmd.pieces[cmd.pieces.length - 1].template == cmd.wallSet.templates.tower)
{
error("[TryConstructWall] Ending wall piece cannot be a tower (" + cmd.wallSet.templates.tower + ") when snapping at the ending side");
return;
}
// Assign obstruction control groups to allow the wall pieces to mutually overlap during foundation placement
// and during construction. The scheme here is that whatever wall pieces are inbetween two towers inherit the control
// groups of both of the towers they are connected to (either newly constructed ones as part of the wall, or existing
// towers in the case of snapping). The towers themselves all keep their default unique control groups.
// To support this, every non-tower piece registers the entity ID of the towers (or foundations thereof) that neighbour
// it on either side. Specifically, each non-tower wall piece has its primary control group set equal to that of the
// first tower encountered towards the starting side of the wall, and its secondary control group set equal to that of
// the first tower encountered towards the ending side of the wall (if any).
// We can't build the whole wall at once by linearly stepping through the wall pieces and build them, because the
// wall segments may/will need the entity IDs of towers that come afterwards. So, build it in two passes:
//
// FIRST PASS:
// - Go from start to end and construct wall piece foundations as far as we can without running into a piece that
// cannot be built (e.g. because it is obstructed). At each non-tower, set the most recently built tower's ID
// as the primary control group, thus allowing it to be built overlapping the previous piece.
// - If we encounter a new tower along the way (which will gain its own control group), do the following:
// o First build it using temporarily the same control group of the previous (non-tower) piece
// o Set the previous piece's secondary control group to the tower's entity ID
// o Restore the primary control group of the constructed tower back its original (unique) value.
// The temporary control group is necessary to allow the newer tower with its unique control group ID to be able
// to be placed while overlapping the previous piece.
//
// SECOND PASS:
// - Go end to start from the last successfully placed wall piece (which might be a tower we backtracked to), this
// time registering the right neighbouring tower in each non-tower piece.
// first pass; L -> R
var lastTowerIndex = -1; // index of the last tower we've encountered in cmd.pieces
var lastTowerControlGroup = null; // control group of the last tower we've encountered, to assign to non-tower pieces
// If we're snapping to an existing entity at the starting end, set lastTowerControlGroup to its control group ID so that
// the first wall piece can be built while overlapping it.
if (cmd.startSnappedEntity)
{
var cmpSnappedStartObstruction = Engine.QueryInterface(cmd.startSnappedEntity, IID_Obstruction);
if (!cmpSnappedStartObstruction)
{
error("[TryConstructWall] Snapped entity on starting side does not have an obstruction component");
return;
}
lastTowerControlGroup = cmpSnappedStartObstruction.GetControlGroup();
//warn("setting lastTowerControlGroup to control group of start snapped entity " + cmd.startSnappedEntity + ": " + lastTowerControlGroup);
}
var i = 0;
var queued = cmd.queued;
var pieces = clone(cmd.pieces);
for (; i < pieces.length; ++i)
{
var piece = pieces[i];
// All wall pieces after the first must be queued.
if (i > 0 && !queued)
queued = true;
// 'lastTowerControlGroup' must always be defined and valid here, except if we're at the first piece and we didn't do
// start position snapping (implying that the first entity we build must be a tower)
if (lastTowerControlGroup === null || lastTowerControlGroup == INVALID_ENTITY)
{
if (!(i == 0 && piece.template == cmd.wallSet.templates.tower && !cmd.startSnappedEntity))
{
error("[TryConstructWall] Expected last tower control group to be available, none found (1st pass, iteration " + i + ")");
break;
}
}
var constructPieceCmd = {
"type": "construct",
"entities": cmd.entities,
"template": piece.template,
"x": piece.x,
"z": piece.z,
"angle": piece.angle,
"autorepair": cmd.autorepair,
"autocontinue": cmd.autocontinue,
"queued": queued,
// Regardless of whether we're building a tower or an intermediate wall piece, it is always (first) constructed
// using the control group of the last tower (see comments above).
"obstructionControlGroup": lastTowerControlGroup,
};
// If we're building the last piece and we're attaching to a snapped entity, we need to add in the snapped entity's
// control group directly at construction time (instead of setting it in the second pass) to allow it to be built
// while overlapping the snapped entity.
if (i == pieces.length - 1 && cmd.endSnappedEntity)
{
var cmpEndSnappedObstruction = Engine.QueryInterface(cmd.endSnappedEntity, IID_Obstruction);
if (cmpEndSnappedObstruction)
constructPieceCmd.obstructionControlGroup2 = cmpEndSnappedObstruction.GetControlGroup();
}
var pieceEntityId = TryConstructBuilding(player, cmpPlayer, controlAllUnits, constructPieceCmd);
if (pieceEntityId)
{
// wall piece foundation successfully built, save the entity ID in the piece info object so we can reference it later
piece.ent = pieceEntityId;
// if we built a tower, do the control group dance (see outline above) and update lastTowerControlGroup and lastTowerIndex
if (piece.template == cmd.wallSet.templates.tower)
{
var cmpTowerObstruction = Engine.QueryInterface(pieceEntityId, IID_Obstruction);
var newTowerControlGroup = pieceEntityId;
if (i > 0)
{
//warn(" updating previous wall piece's secondary control group to " + newTowerControlGroup);
var cmpPreviousObstruction = Engine.QueryInterface(pieces[i-1].ent, IID_Obstruction);
// TODO: ensure that cmpPreviousObstruction exists
// TODO: ensure that the previous obstruction does not yet have a secondary control group set
cmpPreviousObstruction.SetControlGroup2(newTowerControlGroup);
}
// TODO: ensure that cmpTowerObstruction exists
cmpTowerObstruction.SetControlGroup(newTowerControlGroup); // give the tower its own unique control group
lastTowerIndex = i;
lastTowerControlGroup = newTowerControlGroup;
}
}
else // failed to build wall piece, abort
break;
}
var lastBuiltPieceIndex = i - 1;
var wallComplete = (lastBuiltPieceIndex == pieces.length - 1);
// At this point, 'i' is the index of the last wall piece that was successfully constructed (which may or may not be a tower).
// Now do the second pass going right-to-left, registering the control groups of the towers to the right of each piece (if any)
// as their secondary control groups.
lastTowerControlGroup = null; // control group of the last tower we've encountered, to assign to non-tower pieces
// only start off with the ending side's snapped tower's control group if we were able to build the entire wall
if (cmd.endSnappedEntity && wallComplete)
{
var cmpSnappedEndObstruction = Engine.QueryInterface(cmd.endSnappedEntity, IID_Obstruction);
if (!cmpSnappedEndObstruction)
{
error("[TryConstructWall] Snapped entity on ending side does not have an obstruction component");
return;
}
lastTowerControlGroup = cmpSnappedEndObstruction.GetControlGroup();
}
for (var j = lastBuiltPieceIndex; j >= 0; --j)
{
var piece = pieces[j];
if (!piece.ent)
{
error("[TryConstructWall] No entity ID set for constructed entity of template '" + piece.template + "'");
continue;
}
var cmpPieceObstruction = Engine.QueryInterface(piece.ent, IID_Obstruction);
if (!cmpPieceObstruction)
{
error("[TryConstructWall] Wall piece of template '" + piece.template + "' has no Obstruction component");
continue;
}
if (piece.template == cmd.wallSet.templates.tower)
{
// encountered a tower entity, update the last tower control group
lastTowerControlGroup = cmpPieceObstruction.GetControlGroup();
}
else
{
// Encountered a non-tower entity, update its secondary control group to 'lastTowerControlGroup'.
// Note that the wall piece may already have its secondary control group set to the tower's entity ID from a control group
// dance during the first pass, in which case we should validate it against 'lastTowerControlGroup'.
var existingSecondaryControlGroup = cmpPieceObstruction.GetControlGroup2();
if (existingSecondaryControlGroup == INVALID_ENTITY)
{
if (lastTowerControlGroup != null && lastTowerControlGroup != INVALID_ENTITY)
{
cmpPieceObstruction.SetControlGroup2(lastTowerControlGroup);
}
}
else if (existingSecondaryControlGroup != lastTowerControlGroup)
{
error("[TryConstructWall] Existing secondary control group of non-tower entity does not match expected value (2nd pass, iteration " + j + ")");
break;
}
}
}
}
/**
* Remove the given list of entities from their current formations.
*/
function RemoveFromFormation(ents)
{
let formation = ExtractFormations(ents);
for (let fid in formation.members)
{
let cmpFormation = Engine.QueryInterface(+fid, IID_Formation);
if (cmpFormation)
cmpFormation.RemoveMembers(formation.members[fid]);
}
}
/**
* Returns a list of UnitAI components, each belonging either to a
* selected unit or to a formation entity for groups of the selected units.
*/
function GetFormationUnitAIs(ents, player, cmd, formationTemplate, forceTemplate)
{
// If an individual was selected, remove it from any formation
// and command it individually.
if (ents.length == 1)
{
let cmpUnitAI = Engine.QueryInterface(ents[0], IID_UnitAI);
if (!cmpUnitAI)
return [];
RemoveFromFormation(ents);
return [ cmpUnitAI ];
}
let formationUnitAIs = [];
// Find what formations the selected entities are currently in,
// and default to that unless the formation is forced or it's the null formation
// (we want that to reset whatever formations units are in).
if (formationTemplate != NULL_FORMATION)
{
let formation = ExtractFormations(ents);
let formationIds = Object.keys(formation.members);
if (formationIds.length == 1)
{
// Selected units either belong to this formation or have no formation.
let fid = formationIds[0];
let cmpFormation = Engine.QueryInterface(+fid, IID_Formation);
if (cmpFormation && cmpFormation.GetMemberCount() == formation.members[fid].length &&
cmpFormation.GetMemberCount() == formation.entities.length)
{
cmpFormation.DeleteTwinFormations();
// The whole formation was selected, so reuse its controller for this command.
if (!forceTemplate || formationTemplate == formation.templates[fid])
{
formationTemplate = formation.templates[fid];
formationUnitAIs = [Engine.QueryInterface(+fid, IID_UnitAI)];
}
else if (formationTemplate && CanMoveEntsIntoFormation(formation.entities, formationTemplate))
formationUnitAIs = [cmpFormation.LoadFormation(formationTemplate)];
}
else if (cmpFormation && !forceTemplate)
{
// Just reuse the template.
formationTemplate = formation.templates[fid];
}
}
else if (formationIds.length)
{
// Check if all entities share a common formation, if so reuse this template.
let template = formation.templates[formationIds[0]];
for (let i = 1; i < formationIds.length; ++i)
if (formation.templates[formationIds[i]] != template)
{
template = null;
break;
}
if (template && !forceTemplate)
formationTemplate = template;
}
}
// Separate out the units that don't support the chosen formation.
let formedUnits = [];
let nonformedUnitAIs = [];
for (let ent of ents)
{
let cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
let cmpPosition = Engine.QueryInterface(ent, IID_Position);
if (!cmpUnitAI || !cmpPosition || !cmpPosition.IsInWorld())
continue;
let cmpIdentity = Engine.QueryInterface(ent, IID_Identity);
// TODO: We only check if the formation is usable by some units
// if we move them to it. We should check if we can use formations
// for the other cases.
let nullFormation = (formationTemplate || cmpUnitAI.GetFormationTemplate()) == NULL_FORMATION;
if (nullFormation || !cmpIdentity || !cmpIdentity.CanUseFormation(formationTemplate || NULL_FORMATION))
{
if (nullFormation && cmpUnitAI.GetFormationController())
cmpUnitAI.LeaveFormation(cmd.queued || false);
nonformedUnitAIs.push(cmpUnitAI);
}
else
formedUnits.push(ent);
}
if (nonformedUnitAIs.length == ents.length)
{
// No units support the formation.
return nonformedUnitAIs;
}
if (!formationUnitAIs.length)
{
// We need to give the selected units a new formation controller.
// TODO replace the fixed 60 with something sensible, based on vision range f.e.
let formationSeparation = 60;
let clusters = ClusterEntities(formedUnits, formationSeparation);
let formationEnts = [];
for (let cluster of clusters)
{
RemoveFromFormation(cluster);
if (!formationTemplate || !CanMoveEntsIntoFormation(cluster, formationTemplate))
{
for (let ent of cluster)
nonformedUnitAIs.push(Engine.QueryInterface(ent, IID_UnitAI));
continue;
}
// Create the new controller.
let formationEnt = Engine.AddEntity(formationTemplate);
let cmpFormation = Engine.QueryInterface(formationEnt, IID_Formation);
formationUnitAIs.push(Engine.QueryInterface(formationEnt, IID_UnitAI));
cmpFormation.SetFormationSeparation(formationSeparation);
cmpFormation.SetMembers(cluster);
for (let ent of formationEnts)
cmpFormation.RegisterTwinFormation(ent);
formationEnts.push(formationEnt);
let cmpOwnership = Engine.QueryInterface(formationEnt, IID_Ownership);
cmpOwnership.SetOwner(player);
}
}
return nonformedUnitAIs.concat(formationUnitAIs);
}
/**
* Group a list of entities in clusters via single-links
*/
function ClusterEntities(ents, separationDistance)
{
let clusters = [];
if (!ents.length)
return clusters;
let distSq = separationDistance * separationDistance;
let positions = [];
// triangular matrix with the (squared) distances between the different clusters
// the other half is not initialised
let matrix = [];
for (let i = 0; i < ents.length; ++i)
{
matrix[i] = [];
clusters.push([ents[i]]);
let cmpPosition = Engine.QueryInterface(ents[i], IID_Position);
positions.push(cmpPosition.GetPosition2D());
for (let j = 0; j < i; ++j)
matrix[i][j] = positions[i].distanceToSquared(positions[j]);
}
while (clusters.length > 1)
{
// search two clusters that are closer than the required distance
let closeClusters = undefined;
for (let i = matrix.length - 1; i >= 0 && !closeClusters; --i)
for (let j = i - 1; j >= 0 && !closeClusters; --j)
if (matrix[i][j] < distSq)
closeClusters = [i,j];
// if no more close clusters found, just return all found clusters so far
if (!closeClusters)
return clusters;
// make a new cluster with the entities from the two found clusters
let newCluster = clusters[closeClusters[0]].concat(clusters[closeClusters[1]]);
// calculate the minimum distance between the new cluster and all other remaining
// clusters by taking the minimum of the two distances.
let distances = [];
for (let i = 0; i < clusters.length; ++i)
{
let a = closeClusters[1];
let b = closeClusters[0];
if (i == a || i == b)
continue;
let dist1 = matrix[a][i] !== undefined ? matrix[a][i] : matrix[i][a];
let dist2 = matrix[b][i] !== undefined ? matrix[b][i] : matrix[i][b];
distances.push(Math.min(dist1, dist2));
}
// remove the rows and columns in the matrix for the merged clusters,
// and the clusters themselves from the cluster list
clusters.splice(closeClusters[0],1);
clusters.splice(closeClusters[1],1);
matrix.splice(closeClusters[0],1);
matrix.splice(closeClusters[1],1);
for (let i = 0; i < matrix.length; ++i)
{
if (matrix[i].length > closeClusters[0])
matrix[i].splice(closeClusters[0],1);
if (matrix[i].length > closeClusters[1])
matrix[i].splice(closeClusters[1],1);
}
// add a new row of distances to the matrix and the new cluster
clusters.push(newCluster);
matrix.push(distances);
}
return clusters;
}
function GetFormationRequirements(formationTemplate)
{
var template = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager).GetTemplate(formationTemplate);
if (!template.Formation)
return false;
return { "minCount": +template.Formation.RequiredMemberCount };
}
function CanMoveEntsIntoFormation(ents, formationTemplate)
{
// TODO: should check the player's civ is allowed to use this formation
// See simulation/components/Player.js GetFormations() for a list of all allowed formations
var requirements = GetFormationRequirements(formationTemplate);
if (!requirements)
return false;
var count = 0;
for (let ent of ents)
{
var cmpIdentity = Engine.QueryInterface(ent, IID_Identity);
if (!cmpIdentity || !cmpIdentity.CanUseFormation(formationTemplate))
continue;
++count;
}
return count >= requirements.minCount;
}
/**
* Check if player can control this entity
* returns: true if the entity is owned by the player and controllable
* or control all units is activated, else false
*/
function CanControlUnit(entity, player, controlAll)
{
let cmpIdentity = Engine.QueryInterface(entity, IID_Identity);
let canBeControlled = IsOwnedByPlayer(player, entity) &&
(!cmpIdentity || cmpIdentity.IsControllable()) ||
controlAll;
if (!canBeControlled)
notifyOrderFailure(entity, player);
return canBeControlled;
}
/**
* @param {number} entity - The entityID to verify.
* @param {number} player - The playerID to check against.
* @return {boolean}.
*/
function IsOwnedByPlayerOrMutualAlly(entity, player)
{
return IsOwnedByPlayer(player, entity) || IsOwnedByMutualAllyOfPlayer(player, entity);
}
/**
* Check if player can control this entity
* @return {boolean} - True if the entity is valid and controlled by the player
* or the entity is owned by an mutualAlly and can be controlled
* or control all units is activated, else false.
*/
function CanPlayerOrAllyControlUnit(entity, player, controlAll)
{
return CanControlUnit(player, entity, controlAll) ||
IsOwnedByMutualAllyOfPlayer(player, entity) && CanOwnerControlEntity(entity);
}
/**
* @return {boolean} - Whether the owner of this entity can control the entity.
*/
function CanOwnerControlEntity(entity)
{
let cmpOwner = QueryOwnerInterface(entity);
return cmpOwner && CanControlUnit(entity, cmpOwner.GetPlayerID());
}
/**
* Filter entities which the player can control.
*/
function FilterEntityList(entities, player, controlAll)
{
return entities.filter(ent => CanControlUnit(ent, player, controlAll));
}
/**
* Filter entities which the player can control or are mutualAlly
*/
function FilterEntityListWithAllies(entities, player, controlAll)
{
return entities.filter(ent => CanPlayerOrAllyControlUnit(ent, player, controlAll));
}
/**
* Incur the player with the cost of a bribe, optionally multiply the cost with
* the additionalMultiplier
*/
function IncurBribeCost(template, player, playerBribed, failedBribe)
{
let cmpPlayerBribed = QueryPlayerIDInterface(playerBribed);
if (!cmpPlayerBribed)
return false;
let costs = {};
// Additional cost for this owner
let multiplier = cmpPlayerBribed.GetSpyCostMultiplier();
if (failedBribe)
multiplier *= template.VisionSharing.FailureCostRatio;
for (let res in template.Cost.Resources)
costs[res] = Math.floor(multiplier * ApplyValueModificationsToTemplate("Cost/Resources/" + res, +template.Cost.Resources[res], player, template));
let cmpPlayer = QueryPlayerIDInterface(player);
return cmpPlayer && cmpPlayer.TrySubtractResources(costs);
}
Engine.RegisterGlobal("GetFormationRequirements", GetFormationRequirements);
Engine.RegisterGlobal("CanMoveEntsIntoFormation", CanMoveEntsIntoFormation);
Engine.RegisterGlobal("GetDockAngle", GetDockAngle);
Engine.RegisterGlobal("ProcessCommand", ProcessCommand);
Engine.RegisterGlobal("g_Commands", g_Commands);
Engine.RegisterGlobal("IncurBribeCost", IncurBribeCost);
Index: ps/trunk/binaries/data/mods/public/simulation/helpers/Resources.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/helpers/Resources.js (revision 24988)
+++ ps/trunk/binaries/data/mods/public/simulation/helpers/Resources.js (revision 24989)
@@ -1,93 +1,74 @@
/**
* Builds a RelaxRNG schema based on currently valid elements.
*
* To prevent validation errors, disabled resources are included in the schema.
*
* @param datatype - The datatype of the element
- * @param additional - Array of additional data elements. Time, xp, treasure, etc.
+ * @param additional - Array of additional data elements. Time, xp, etc.
* @param subtypes - If true, resource subtypes will be included as well.
* @return RelaxNG schema string
*/
Resources.prototype.BuildSchema = function(datatype, additional = [], subtypes = false)
{
if (!datatype)
return "";
switch (datatype)
{
case "decimal":
case "nonNegativeDecimal":
case "positiveDecimal":
datatype = "";
break;
default:
datatype = "";
}
let resCodes = this.resourceData.map(resource => resource.code);
let schema = "";
for (let res of resCodes.concat(additional))
schema +=
"" +
"" +
datatype +
"" +
"";
if (!subtypes)
return "" + schema + "";
for (let res of this.resourceData)
for (let subtype in res.subtypes)
schema +=
"" +
"" +
datatype +
"" +
"";
- if (additional.indexOf("treasure") !== -1)
- for (let res of resCodes)
- schema +=
- "" +
- "" +
- datatype +
- "" +
- "";
-
return "" + schema + "";
};
/**
* Builds the value choices for a RelaxNG `` object, based on currently valid resources.
*
* @oaram subtypes - If set to true, the choices returned will be resource subtypes, rather than main types
- * @param treasure - If set to true, the pseudo resource 'treasure' (or its subtypes) will be included
* @return String of RelaxNG Schema `` values.
*/
-Resources.prototype.BuildChoicesSchema = function(subtypes = false, treasure = false)
+Resources.prototype.BuildChoicesSchema = function(subtypes = false)
{
let schema = "";
if (!subtypes)
- {
- let resCodes = this.resourceData.map(resource => resource.code);
- if (treasure)
- resCodes.push("treasure");
- for (let res of resCodes)
+ for (let res of this.resourceData.map(resource => resource.code))
schema += "" + res + "";
- }
else
for (let res of this.resourceData)
- {
for (let subtype in res.subtypes)
schema += "" + res.code + "." + subtype + "";
- if (treasure)
- schema += "" + "treasure." + res.code + "";
- }
return "" + schema + "";
};
Resources = new Resources();
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 24988)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/treasure/food_barrel.xml (revision 24989)
@@ -1,26 +1,27 @@
2.5Food Treasuregaia/special_treasure_food.png
-
- 100
- treasure.food
- 128x128/ellipse.png128x128/ellipse_mask.png
+
+
+ 100
+
+ props/special/eyecandy/barrel_a.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 24988)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/treasure/food_crate.xml (revision 24989)
@@ -1,21 +1,22 @@
12.0Food Treasuregaia/special_treasure_food.png
-
- 200
- treasure.food
-
+
+
+ 200
+
+ props/special/eyecandy/crate_a.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 24988)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/treasure/food_persian_small.xml (revision 24989)
@@ -1,21 +1,22 @@
12.0Persian Food Treasuregaia/special_treasure_food.png
-
- 400
- treasure.food
-
+
+
+ 400
+
+ props/special/eyecandy/treasure_persian_food_small.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 24988)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/treasure/metal_persian_big.xml (revision 24989)
@@ -1,25 +1,26 @@
12.0Persian Wares
-
- 500
- treasure.metal
-
+
+
+ 500
+
+ props/special/eyecandy/treasure_persian_metal_big.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 24988)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/treasure/shipwreck.xml (revision 24989)
@@ -1,24 +1,25 @@
9.0Shipwrecktrue0.0
-
- 500
- treasure.wood
-
+
+
+ 500
+
+ props/special/eyecandy/shipwreck_ram_side.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 24988)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/treasure/shipwreck_sail_boat.xml (revision 24989)
@@ -1,24 +1,25 @@
9.0Shipwrecktrue0.0
-
- 400
- treasure.wood
-
+
+
+ 400
+
+ 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 24988)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/treasure/wood.xml (revision 24989)
@@ -1,25 +1,26 @@
12.0Wood Treasure
-
- 300
- treasure.wood
-
+
+
+ 300
+
+ props/special/eyecandy/wood_pile.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_cavalry.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_cavalry.xml (revision 24988)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_cavalry.xml (revision 24989)
@@ -1,106 +1,103 @@
Capture2.541000Field Palisade WallSlaughter10002161006.0CavalryBasicHuman FastMoving CitizenSoldierCitizen Soldier Cavalry
special/formations/wedge
13010500515031152.01.0520
- 20
- 20
- 20128x256/ellipse.png128x256/ellipse_mask.pngattack/weapon/sword_attack.xmlvoice/{lang}/civ/civ_{phenotype}_attack.xmlvoice/{lang}/civ/civ_{phenotype}_garrison.xmlvoice/{lang}/civ/civ_{phenotype}_gather.xmlvoice/{lang}/civ/civ_{phenotype}_walk.xmlactor/mounted/movement/walk.xmlactor/mounted/movement/walk.xmlresource/gathering/gather_meat.xmlactor/fauna/death/death_horse.xmlinterface/alarm/alarm_create_cav.xml7.021.492
Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_ship.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_ship.xml (revision 24988)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_ship.xml (revision 24989)
@@ -1,76 +1,77 @@
true0.00.57.010.01010FemaleCitizen Infantry Healer Dog0FemaleCitizen Infantry Healer Dog010truetrueShip-OrganicShipuprighttrue034.05105128x512/ellipse.png128x512/ellipse_mask.pnginterface/alarm/alarm_create_warship.xmlactor/ship/warship_move_01.xmlactor/ship/warship_move_01.xmlactor/ship/warship_move_01.xmlactor/ship/warship_death.xml6.00.56.0
+ ship
Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_ship_merchant.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_ship_merchant.xml (revision 24988)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_ship_merchant.xml (revision 24989)
@@ -1,59 +1,59 @@
2010015Support Cavalry400Merchantmantemplate_unit_ship_merchantTrade between docks. Garrison a Trader aboard for additional profit (+20% for each garrisoned). Gather profitable aquatic treasures.-ConquestCriticalTrader Bribablephase_town2515252
-
- 12.0
- 0.750.2
+
+ 12
+ passivefalsefalseship-small1.3550true
Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_support_slave.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_support_slave.xml (revision 24988)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_support_slave.xml (revision 24989)
@@ -1,84 +1,91 @@
0.5
structures/{civ}/civil_centre
structures/{civ}/crannog
structures/{civ}/military_colony
structures/{civ}/house
structures/{civ}/apartment
structures/{civ}/storehouse
structures/{civ}/farmstead
structures/{civ}/field
structures/{civ}/corral
structures/{civ}/dock
structures/{civ}/barracks
structures/{civ}/stable
structures/{civ}/elephant_stables
structures/{civ}/arsenal
structures/{civ}/forge
structures/{civ}/temple
structures/{civ}/market
structures/{civ}/outpost
structures/{civ}/sentry_tower
structures/{civ}/defense_tower
structures/{civ}/fortress
structures/wallset_palisade
structures/{civ}/wallset_siege
structures/{civ}/wallset_stone
structures/{civ}/theater
structures/{civ}/wonder
01550-0.25trueSlave WorkerSlavetemplate_unit_support_slaveGatherer with a finite life span. Bonused at mining and lumbering.105
+ 2.01.00.50.50.3511.051.051.0
-
+
+ 10
+ 10
+ 10
+ 10
+
+ resource/construction/con_wood.xmlresource/foraging/forage_leaves.xmlresource/farming/farm.xmlresource/lumbering/lumbering.xmlresource/mining/pickaxe.xmlresource/mining/mining.xmlinterface/alarm/alarm_invalid_building_placement.xmlvoice/{lang}/civ/civ_{phenotype}_build.xmlvoice/{lang}/civ/civ_{phenotype}_repair.xmlactor/singlesteps/steps_gravel_trained.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 24988)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/treasure/stone.xml (revision 24989)
@@ -1,25 +1,26 @@
2.0Stone Treasure
-
- 300
- treasure.stone
-
+
+
+ 300
+
+ props/special/eyecandy/stone_pile.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_catafalque.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_catafalque.xml (revision 24988)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_catafalque.xml (revision 24989)
@@ -1,59 +1,59 @@
2500102.0-Organic -ConquestCriticalRelicCatafalqueunits/catafalque.pngtemplate_unit_catafalqueA catafalque that holds the remains of a great leader.truepitch-roll
- 128x256/cartouche.png128x256/cartouche_mask.pngactor/singlesteps/steps_grass_order.xmlactor/singlesteps/steps_grass.xmlactor/singlesteps/steps_grass.xml
+ standgroundfalselarge0.55units/global/catafalque.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_infantry.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_infantry.xml (revision 24988)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_infantry.xml (revision 24989)
@@ -1,132 +1,138 @@
Capture2.541000Field Palisade WallSlaughter100021.0
structures/{civ}/civil_centre
structures/{civ}/crannog
structures/{civ}/military_colony
structures/{civ}/house
structures/{civ}/apartment
structures/{civ}/storehouse
structures/{civ}/farmstead
structures/{civ}/field
structures/{civ}/corral
structures/{civ}/dock
structures/{civ}/barracks
structures/{civ}/stable
structures/{civ}/elephant_stables
structures/{civ}/arsenal
structures/{civ}/forge
structures/{civ}/temple
structures/{civ}/market
structures/{civ}/outpost
structures/{civ}/sentry_tower
structures/{civ}/defense_tower
structures/{civ}/fortress
structures/wallset_palisade
structures/{civ}/wallset_siege
structures/{civ}/wallset_stone
structures/{civ}/theater
structures/{civ}/wonder
125000080Human CitizenSoldierCitizen Worker Soldier InfantryInfantryBasic1005000upright10024152.01.00.50.2510.7550.520.5
+
+ 10
+ 10
+ 10
+ 10
+ attack/weapon/knife_attack.xmlvoice/{lang}/civ/civ_{phenotype}_attack.xmlvoice/{lang}/civ/civ_{phenotype}_build.xmlvoice/{lang}/civ/civ_{phenotype}_garrison.xmlvoice/{lang}/civ/civ_{phenotype}_gather.xmlvoice/{lang}/civ/civ_{phenotype}_repair.xmlvoice/{lang}/civ/civ_{phenotype}_walk.xmlactor/human/movement/walk.xmlactor/human/movement/run.xmlactor/human/death/{phenotype}_death.xmlresource/construction/con_wood.xmlresource/foraging/forage_leaves.xmlresource/farming/farm.xmlresource/gathering/gather_meat.xmlresource/lumbering/lumbering.xmlresource/mining/pickaxe.xmlresource/mining/mining.xmlresource/mining/mining.xmlinterface/alarm/alarm_create_infantry.xmlinterface/alarm/alarm_invalid_building_placement.xml80
Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_ship_fishing.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_ship_fishing.xml (revision 24988)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_ship_fishing.xml (revision 24989)
@@ -1,90 +1,90 @@
Harpoon10.00.00.05.05001000!SeaCreature20501200Fishing Boattemplate_unit_ship_fishingFish the waters for food.-ConquestCriticalFishingBoat1102526.0
+ 1.01.8
- 40128x256/ellipse.png128x256/ellipse_mask.pngactor/ship/boat_move.xmlactor/ship/boat_move.xml2.00.3335.0passivefalsefalseship-small1.130
Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_support_female_citizen.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_support_female_citizen.xml (revision 24988)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_support_female_citizen.xml (revision 24989)
@@ -1,114 +1,120 @@
Dagger2.000.035001000Slaughter100021.0
structures/{civ}/civil_centre
structures/{civ}/crannog
structures/{civ}/military_colony
structures/{civ}/house
structures/{civ}/apartment
structures/{civ}/storehouse
structures/{civ}/farmstead
structures/{civ}/field
structures/{civ}/corral
structures/{civ}/dock
structures/{civ}/barracks
structures/{civ}/stable
structures/{civ}/elephant_stables
structures/{civ}/arsenal
structures/{civ}/forge
structures/{civ}/temple
structures/{civ}/market
structures/{civ}/outpost
structures/{civ}/sentry_tower
structures/{civ}/defense_tower
structures/{civ}/fortress
structures/wallset_palisade
structures/{civ}/wallset_siege
structures/{civ}/wallset_stone
structures/{civ}/theater
structures/{civ}/wonder
95025FemaleCitizenCitizen WorkerFemale Citizentemplate_unit_support_female_citizenfemale352.01.010.510.750.3520.35
+
+ 10
+ 10
+ 10
+ 10
+ attack/weapon/sword.xmlattack/weapon/knife_attack.xmlresource/construction/con_wood.xmlresource/foraging/forage_leaves.xmlresource/farming/farm.xmlresource/gathering/gather_meat.xmlresource/lumbering/lumbering.xmlresource/mining/pickaxe.xmlresource/mining/mining.xmlresource/mining/mining.xmlinterface/alarm/alarm_invalid_building_placement.xmlvoice/{lang}/civ/civ_{phenotype}_attack.xmlvoice/{lang}/civ/civ_{phenotype}_build.xmlvoice/{lang}/civ/civ_{phenotype}_repair.xmlinterface/alarm/alarm_create_female.xmlfalse32
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 24988)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/template_gaia_treasure.xml (revision 24989)
@@ -1,39 +1,37 @@
12.0TreasureCollect treasures for resources.gaia/special_treasure.pngmetal
-
- false
- 300
- 1
- 0.33.75
+
+ 1000
+
Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_dog.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_dog.xml (revision 24988)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_dog.xml (revision 24989)
@@ -1,75 +1,75 @@
Fangs72035001000Structure Ship Siege1501001.5110War DogCannot attack Structures, Ships, or Siege Engines.Human FastMovingDog Melee10010
- 128x256/ellipse.png128x256/ellipse_mask.pngvoice/global/civ_dog_move.xmlvoice/global/civ_dog_move.xmlactor/mounted/movement/walk.xmlactor/mounted/movement/walk.xmlattack/weapon/sword.xmlactor/fauna/death/death_animal_gen.xmlinterface/complete/building/complete_kennel.xml2.5WarDog
+ 1.5230
Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_ship_bireme.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_ship_bireme.xml (revision 24988)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_ship_bireme.xml (revision 24989)
@@ -1,71 +1,70 @@
Bow0.035.00.045.00.0100020000100.02.050.0falseShip Human2101Infantry Cavalry2201255020Support Cavalry800Light WarshipGarrison units for transport and to increase firepower.Ranged Warship Biremephase_town752515
- attack/impact/arrow_impact.xmlattack/weapon/bow_attack.xml1.5590
Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_ship_quinquereme.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_ship_quinquereme.xml (revision 24988)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_ship_quinquereme.xml (revision 24989)
@@ -1,74 +1,73 @@
Stone00150804020005000040.06.020.0falseShip Structure1101StoneThrower33035020035050Support Soldier Siege2000Heavy WarshipGarrison units for transport and to increase firepower.Ranged Warship Quinqueremephase_city15040302
- attack/siege/ballist_attack.xml1.8110
Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_unit.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/template_unit.xml (revision 24988)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/template_unit.xml (revision 24989)
@@ -1,155 +1,145 @@
110000falsefalse30.00.010.04.01corpse10000falsegaiaUnit Organic ConquestCriticalUnit
special/formations/null
special/formations/box
special/formations/column_closed
special/formations/line_closed
special/formations/column_open
special/formations/line_open
special/formations/flank
special/formations/battle_line
malefalseunittruetruefalsefalsetruefalsefalsefalse0pitchfalse0.08111
-
- 2.0
- 1.0
-
- 1
-
-
- 10
- 10
- 10
- 10
-
- 128x128/ellipse.png128x128/ellipse_mask.pnginterface/alarm/alarm_attackplayer.xmlinterface/alarm/alarm_attacked_gaia.xmlinterface/alarm/alarm_attackplayer.xmlinterface/alarm/alarm_attacked_gaia.xml2.00.3335.0
+
+ 2
+ aggressive12.0falsetruetrue12800falsedefault9.01.67falsefalsefalsefalse12falsetruefalsefalse
Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_fauna.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_fauna.xml (revision 24988)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_fauna.xml (revision 24989)
@@ -1,53 +1,53 @@
04.0-ConquestCritical Animalgaia/fauna_generic.pngfood4
- 128x256/ellipse.png128x256/ellipse_mask.png
+ passivefalsefalse8.024.02000800015000600000.7true60
Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_ship_fire.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_ship_fire.xml (revision 24988)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_ship_fire.xml (revision 24989)
@@ -1,55 +1,54 @@
Fire201250100!Ship30300Circular30true600500-60.850.650.35Fire ShipUnrepairable. Gradually loses health. Can only attack Ships.Melee Warship Fireshipphase_town
- ship-small1.660
Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_ship_trireme.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_ship_trireme.xml (revision 24988)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_ship_trireme.xml (revision 24989)
@@ -1,74 +1,73 @@
Bow0.035.00.055.00.0100020000100.02.050.0falseShip Human3131Infantry Cavalry32515015030Support Soldier Siege1400Medium WarshipGarrison units for transport and to increase firepower.Ranged Warship Triremephase_town10030202
- attack/impact/arrow_impact.xmlattack/weapon/bow_attack.xml1.890