Index: ps/trunk/binaries/data/mods/public/globalscripts/Templates.js
===================================================================
--- ps/trunk/binaries/data/mods/public/globalscripts/Templates.js (revision 27244)
+++ ps/trunk/binaries/data/mods/public/globalscripts/Templates.js (revision 27245)
@@ -1,638 +1,638 @@
/**
* 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", "Music", "CivBonuses", "StartEntities",
"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)
continue;
const template = Engine.GetTemplate("special/players/" + data.Code);
data.Name = template.Identity.GenericName;
data.Emblem = "session/portraits/" + template.Identity.Icon;
data.History = template.Identity.History;
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.
* @param {number} default_value - A value to use if one is not specified in the template.
* @return {number}
*/
function GetBaseTemplateDataValue(template, value_path, default_value)
{
let current_value = template;
for (let property of value_path.split("/"))
current_value = current_value[property] || default_value;
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.
* @param {number} default_value - A value to use if one is not specified in the template.
* @return {number} Modifier altered value.
*/
function GetModifiedTemplateDataValue(template, value_path, mod_key, player, modifiers={}, default_value)
{
let current_value = GetBaseTemplateDataValue(template, value_path, default_value);
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} resources - An instance of the Resources class.
* @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, resources, 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.
// @param {number} default_value - A value to use if one is not specified in the template.
const getEntityValue = function(value_path, mod_key, default_value = 0) {
return GetModifiedTemplateDataValue(template, value_path, mod_key, player, modifiers, default_value);
};
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"),
"yOrigin": getAttackStat("Origin/Y")
};
ret.attack[type].elevationAdaptedRange = Math.sqrt(ret.attack[type].maxRange *
(2 * ret.attack[type].yOrigin + 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.requirements = template.Identity.Requirements;
ret.visibleIdentityClasses = GetVisibleIdentityClasses(template.Identity);
ret.nativeCiv = template.Identity.Civ;
}
if (template.UnitMotion)
{
const walkSpeed = getEntityValue("UnitMotion/WalkSpeed");
ret.speed = {
"walk": walkSpeed,
"run": walkSpeed,
"acceleration": getEntityValue("UnitMotion/Acceleration")
};
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
+ "icon": upgrade.Icon,
+ "requirements": upgrade.Requirements
});
}
}
if (template.Researcher)
{
ret.techCostMultiplier = {};
for (const res of resources.GetCodes().concat(["time"]))
ret.techCostMultiplier[res] = getEntityValue("Researcher/TechCostMultiplier/" + res, null, 1);
}
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.TurretHolder)
ret.turretHolder = {
"turretPoints": template.TurretHolder.TurretPoints
};
if (template.Upkeep)
{
ret.upkeep = {
"interval": +template.Upkeep.Interval,
"rates": {}
};
for (let type in template.Upkeep.Rates)
ret.upkeep.rates[type] = getEntityValue("Upkeep/Rates/" + type);
}
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.
* @param {Object} resources - An instance of the Resources class.
*/
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 27244)
+++ ps/trunk/binaries/data/mods/public/gui/common/tooltips.js (revision 27245)
@@ -1,1235 +1,1238 @@
var g_TooltipTextFormats = {
"unit": { "font": "sans-10", "color": "orange" },
"header": { "font": "sans-bold-13" },
"body": { "font": "sans-13" },
"comma": { "font": "sans-12" },
"namePrimaryBig": { "font": "sans-bold-16" },
"namePrimarySmall": { "font": "sans-bold-12" },
"nameSecondary": { "font": "sans-bold-16" }
};
var g_SpecificNamesPrimary = Engine.ConfigDB_GetValue("user", "gui.session.howtoshownames") == 0 || Engine.ConfigDB_GetValue("user", "gui.session.howtoshownames") == 2;
var g_ShowSecondaryNames = Engine.ConfigDB_GetValue("user", "gui.session.howtoshownames") == 0 || Engine.ConfigDB_GetValue("user", "gui.session.howtoshownames") == 1;
function initDisplayedNames()
{
registerConfigChangeHandler(changes => {
if (changes.has("gui.session.howtoshownames"))
updateDisplayedNames();
});
}
/**
* 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": translate(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": translateWithContext(attackTypeTemplate.attackName.context || "Name of an attack, usually the weapon.", 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(translateWithContext("status effect", statusData.applierTooltip));
else if (!applier && statusData.receiverTooltip)
tooltipAttributes.push(translateWithContext("status effect", 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(translateWithContext("garrison tooltip", "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 getTurretsTooltip(template)
{
if (!template.turretHolder)
return "";
return sprintf(translate("%(label)s: %(turretsLimit)s"), {
"label": headerFont(translate("Turret Positions")),
"turretsLimit": Object.keys(template.turretHolder.turretPoints).length
});
}
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;
// 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(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)
})
});
}
function getUpkeepTooltip(template)
{
if (!template.upkeep)
return "";
let resCodes = g_ResourceData.GetCodes().filter(res => !!template.upkeep.rates[res]);
if (!resCodes.length)
return "";
return sprintf(translate("%(label)s %(details)s"), {
"label": headerFont(translate("Upkeep:")),
"details": sprintf(translate("%(resources)s / %(time)s"), {
"resources":
resCodes.map(
res => sprintf(translate("%(resourceIcon)s %(rate)s"), {
"resourceIcon": resourceIcon(res),
"rate": template.upkeep.rates[res]
})
).join(" "),
"time": getSecondsString(template.upkeep.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)
+function getRequirementsTooltip(enabled, requirements, civ)
{
- if (technologyEnabled)
+ if (enabled)
return "";
- return sprintf(translate("Requires %(technology)s"), {
- "technology": getEntityNames(GetTechnologyData(requiredTechnology, civ))
- });
+ // Simple requirements (one tech) can be translated on the fly.
+ if ("Techs" in requirements && !requirements.Techs.includes(" "))
+ return sprintf(translate("Requires %(technology)s"), {
+ "technology": getEntityNames(GetTechnologyData(requirements.Techs, civ))
+ });
+ return translate(requirements.Tooltip);
}
/**
* 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": Math.ceil(resources[resource])
}));
return coloredText(
'[font="sans-bold-13"]' + translate("Insufficient resources:") + '[/font]',
"red") + " " +
formatted.join(" ");
}
function getSpeedTooltip(template)
{
if (!template.speed)
return "";
const walk = template.speed.walk.toFixed(1);
const run = template.speed.run.toFixed(1);
if (walk == 0 && run == 0)
return "";
const acceleration = template.speed.acceleration.toFixed(1);
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"))
}) +
commaFont(translate(", ")) +
sprintf(translate("%(speed)s %(movementType)s"), {
"speed": acceleration,
"movementType": unitFont(translate("Acceleration"))
})
});
}
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;
let primaryName = g_SpecificNamesPrimary ? template.name.specific : template.name.generic;
let secondaryName;
if (g_ShowSecondaryNames)
secondaryName = g_SpecificNamesPrimary ? template.name.generic : template.name.specific;
if (secondaryName)
return sprintf(translate("%(primaryName)s (%(secondaryName)s)"), {
"primaryName": primaryName,
"secondaryName": secondaryName
});
return sprintf(translate("%(primaryName)s"), {
"primaryName": primaryName
});
}
function getEntityNamesFormatted(template)
{
if (!template.name.specific)
return setStringTags(template.name.generic, g_TooltipTextFormats.namePrimaryBig);
let primaryName = g_SpecificNamesPrimary ? template.name.specific : template.name.generic;
let secondaryName;
if (g_ShowSecondaryNames)
secondaryName = g_SpecificNamesPrimary ? template.name.generic : template.name.specific;
if (!secondaryName || primaryName == secondaryName)
return sprintf(translate("%(primaryName)s"), {
"primaryName":
setStringTags(primaryName[0], g_TooltipTextFormats.namePrimaryBig) +
setStringTags(primaryName.slice(1).toUpperCase(), g_TooltipTextFormats.namePrimarySmall)
});
// Translation: Example: "Epibátēs Athēnaîos [font="sans-bold-16"](Athenian Marine)[/font]"
return sprintf(translate("%(primaryName)s (%(secondaryName)s)"), {
"primaryName":
setStringTags(primaryName[0], g_TooltipTextFormats.namePrimaryBig) +
setStringTags(primaryName.slice(1).toUpperCase(), g_TooltipTextFormats.namePrimarySmall),
"secondaryName": setStringTags(secondaryName, g_TooltipTextFormats.nameSecondary)
});
}
function getEntityPrimaryNameFormatted(template)
{
let primaryName = g_SpecificNamesPrimary ? template.name.specific : template.name.generic;
if (!primaryName)
return setStringTags(g_SpecificNamesPrimary ? template.name.generic : template.name.specific, g_TooltipTextFormats.namePrimaryBig);
return setStringTags(primaryName[0], g_TooltipTextFormats.namePrimaryBig) +
setStringTags(primaryName.slice(1).toUpperCase(), g_TooltipTextFormats.namePrimarySmall);
}
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.");
}
/**
* @param {number} number - A number to shorten using SI prefix.
*/
function abbreviateLargeNumbers(number)
{
if (number >= 1e6)
return Math.floor(number / 1e6) + translateWithContext("One letter abbreviation for million", 'M');
if (number >= 1e5)
return Math.floor(number / 1e3) + translateWithContext("One letter abbreviation for thousand", 'k');
if (number >= 1e4)
return (number / 1e3).toFixed(1).replace(/\.0$/, '') + translateWithContext("One letter abbreviation for thousand", 'k');
return number;
}
Index: ps/trunk/binaries/data/mods/public/gui/reference/common/TemplateLoader.js
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/reference/common/TemplateLoader.js (revision 27244)
+++ ps/trunk/binaries/data/mods/public/gui/reference/common/TemplateLoader.js (revision 27245)
@@ -1,336 +1,336 @@
/**
* This class handles the loading of files.
*/
class TemplateLoader
{
constructor()
{
/**
* Raw Data Caches.
*/
this.auraData = {};
this.playerData = {};
this.technologyData = {};
this.templateData = {};
/**
* Partly-composed data.
*/
this.autoResearchTechList = this.findAllAutoResearchedTechs();
}
/**
* Loads raw aura template.
*
* Loads from local cache if available, else from file system.
*
* @param {string} templateName
* @return {Object} Object containing raw template data.
*/
loadAuraTemplate(templateName)
{
if (!(templateName in this.auraData))
{
let data = Engine.ReadJSONFile(this.AuraPath + templateName + ".json");
translateObjectKeys(data, this.AuraTranslateKeys);
this.auraData[templateName] = data;
}
return this.auraData[templateName];
}
/**
* Loads raw entity template.
*
* Loads from local cache if data present, else from file system.
*
* @param {string} templateName
* @param {string} civCode
* @return {Object} Object containing raw template data.
*/
loadEntityTemplate(templateName, civCode)
{
if (!(templateName in this.templateData))
{
// We need to clone the template because we want to perform some translations.
let data = clone(Engine.GetTemplate(templateName));
translateObjectKeys(data, this.EntityTranslateKeys);
if (data.Auras)
for (let auraID of data.Auras._string.split(/\s+/))
this.loadAuraTemplate(auraID);
if (data.Identity.Civ != this.DefaultCiv && civCode != this.DefaultCiv && data.Identity.Civ != civCode)
warn("The \"" + templateName + "\" template has a defined civ of \"" + data.Identity.Civ + "\". " +
"This does not match the currently selected civ \"" + civCode + "\".");
this.templateData[templateName] = data;
}
return this.templateData[templateName];
}
/**
* Loads raw player template.
*
* Loads from local cache if data present, else from file system.
*
* If a civ doesn't have their own civ-specific template,
* then we return the generic template.
*
* @param {string} civCode
* @return {Object} Object containing raw template data.
*/
loadPlayerTemplate(civCode)
{
if (!(civCode in this.playerData))
{
let templateName = this.buildPlayerTemplateName(civCode);
this.playerData[civCode] = Engine.GetTemplate(templateName);
// No object keys need to be translated
}
return this.playerData[civCode];
}
/**
* Loads raw technology template.
*
* Loads from local cache if available, else from file system.
*
* @param {string} templateName
* @return {Object} Object containing raw template data.
*/
loadTechnologyTemplate(templateName)
{
if (!(templateName in this.technologyData))
{
let data = Engine.ReadJSONFile(this.TechnologyPath + templateName + ".json");
translateObjectKeys(data, this.TechnologyTranslateKeys);
// Translate specificName as in GetTechnologyData() from gui/session/session.js
if (typeof (data.specificName) === 'object')
for (let civ in data.specificName)
data.specificName[civ] = translate(data.specificName[civ]);
else if (data.specificName)
warn("specificName should be an object of civ->name mappings in " + templateName + ".json");
this.technologyData[templateName] = data;
}
return this.technologyData[templateName];
}
/**
* @param {string} templateName
* @param {string} civCode
* @return {Object} Contains a list and the requirements of the techs in the pair
*/
loadTechnologyPairTemplate(templateName, civCode)
{
let template = this.loadTechnologyTemplate(templateName);
return {
"techs": [template.top, template.bottom],
"reqs": DeriveTechnologyRequirements(template, civCode)
};
}
deriveProduction(template, civCode)
{
const production = {
"techs": [],
"units": []
};
if (!template.Researcher && !template.Trainer)
return production;
if (template.Trainer?.Entities?._string)
for (let templateName of template.Trainer.Entities._string.split(" "))
{
templateName = templateName.replace(/\{(civ|native)\}/g, civCode);
if (Engine.TemplateExists(templateName))
production.units.push(templateName);
}
const appendTechnology = (technologyName) => {
const technology = this.loadTechnologyTemplate(technologyName, civCode);
if (DeriveTechnologyRequirements(technology, civCode))
production.techs.push(technologyName);
};
if (template.Researcher?.Technologies?._string)
for (let technologyName of template.Researcher.Technologies._string.split(" "))
{
if (technologyName.indexOf("{civ}") != -1)
{
const civTechName = technologyName.replace("{civ}", civCode);
technologyName = TechnologyTemplateExists(civTechName) ? civTechName : technologyName.replace("{civ}", "generic");
}
if (this.isPairTech(technologyName))
{
let technologyPair = this.loadTechnologyPairTemplate(technologyName, civCode);
if (technologyPair.reqs)
for (technologyName of technologyPair.techs)
appendTechnology(technologyName);
}
else
appendTechnology(technologyName);
}
return production;
}
deriveBuildQueue(template, civCode)
{
let buildQueue = [];
if (!template.Builder || !template.Builder.Entities._string)
return buildQueue;
for (let build of template.Builder.Entities._string.split(" "))
{
build = build.replace(/\{(civ|native)\}/g, civCode);
if (Engine.TemplateExists(build))
buildQueue.push(build);
}
return buildQueue;
}
deriveModifications(civCode, auraList)
{
const modificationData = [];
for (const techName of this.autoResearchTechList)
modificationData.push(GetTechnologyBasicDataHelper(this.loadTechnologyTemplate(techName), civCode));
for (const auraName of auraList)
modificationData.push(this.loadAuraTemplate(auraName));
return DeriveModificationsFromTechnologies(modificationData);
}
/**
* If a civ doesn't have its own civ-specific player template,
* this returns the name of the generic player template.
*
* @see simulation/helpers/Player.js GetPlayerTemplateName()
* (Which can't be combined with this due to different Engine contexts)
*/
buildPlayerTemplateName(civCode)
{
let templateName = this.PlayerPath + civCode;
if (Engine.TemplateExists(templateName))
return templateName;
warn("No template found for civ " + civCode + ".");
return this.PlayerPath + this.DefaultCiv;
}
/**
* Crudely iterates through every tech JSON file and identifies those
* that are auto-researched.
*
* @return {array} List of techs that are researched automatically
*/
findAllAutoResearchedTechs()
{
let techList = [];
for (let templateName of listFiles(this.TechnologyPath, ".json", true))
{
let data = this.loadTechnologyTemplate(templateName);
if (data && data.autoResearch)
techList.push(templateName);
}
return techList;
}
/**
* A template may be a variant of another template,
* eg. `*_house`, `*_trireme`, or a promotion.
*
* This method returns an array containing:
* [0] - The template's basename
* [1] - The variant type
* [2] - Further information (if available)
*
* e.g.:
* units/athen/infantry_swordsman_e
* -> ["units/athen/infantry_swordsman_b", TemplateVariant.promotion, "elite"]
*
* units/brit/support_female_citizen_house
* -> ["units/brit/support_female_citizen", TemplateVariant.unlockedByTechnology, "unlock_female_house"]
*/
getVariantBaseAndType(templateName, civCode)
{
if (!templateName || !Engine.TemplateExists(templateName))
return undefined;
templateName = removeFiltersFromTemplateName(templateName);
let template = this.loadEntityTemplate(templateName, civCode);
if (!dirname(templateName) || dirname(template["@parent"]) != dirname(templateName))
return [templateName, TemplateVariant.base];
let parentTemplate = this.loadEntityTemplate(template["@parent"], civCode);
let inheritedVariance = this.getVariantBaseAndType(template["@parent"], civCode);
if (parentTemplate.Identity)
{
if (parentTemplate.Identity.Civ && parentTemplate.Identity.Civ != template.Identity.Civ)
return [templateName, TemplateVariant.base];
if (parentTemplate.Identity.Rank && parentTemplate.Identity.Rank != template.Identity.Rank)
return [inheritedVariance[0], TemplateVariant.promotion, template.Identity.Rank.toLowerCase()];
}
if (parentTemplate.Upgrade)
for (let upgrade in parentTemplate.Upgrade)
if (parentTemplate.Upgrade[upgrade].Entity)
return [inheritedVariance[0], TemplateVariant.upgrade, upgrade.toLowerCase()];
- if (template.Identity.RequiredTechnology)
- return [inheritedVariance[0], TemplateVariant.unlockedByTechnology, template.Identity.RequiredTechnology];
+ if (template.Identity.Requirements?.Techs)
+ return [inheritedVariance[0], TemplateVariant.unlockedByTechnology, template.Identity.Requirements?.Techs];
if (parentTemplate.Cost)
for (let res in parentTemplate.Cost.Resources)
if (+parentTemplate.Cost.Resources[res])
return [inheritedVariance[0], TemplateVariant.trainable];
warn("Template variance unknown: " + templateName);
return [templateName, TemplateVariant.unknown];
}
isPairTech(technologyCode)
{
return !!this.loadTechnologyTemplate(technologyCode).top;
}
isPhaseTech(technologyCode)
{
return basename(technologyCode).startsWith("phase");
}
}
/**
* Paths to certain files.
*
* It might be nice if we could get these from somewhere, instead of having them hardcoded here.
*/
TemplateLoader.prototype.AuraPath = "simulation/data/auras/";
TemplateLoader.prototype.PlayerPath = "special/players/";
TemplateLoader.prototype.TechnologyPath = "simulation/data/technologies/";
TemplateLoader.prototype.DefaultCiv = "gaia";
/**
* Keys of template values that are to be translated on load.
*/
TemplateLoader.prototype.AuraTranslateKeys = ["auraName", "auraDescription"];
TemplateLoader.prototype.EntityTranslateKeys = ["GenericName", "SpecificName", "Tooltip", "History"];
TemplateLoader.prototype.TechnologyTranslateKeys = ["genericName", "tooltip", "description"];
Index: ps/trunk/binaries/data/mods/public/gui/reference/common/TemplateParser.js
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/reference/common/TemplateParser.js (revision 27244)
+++ ps/trunk/binaries/data/mods/public/gui/reference/common/TemplateParser.js (revision 27245)
@@ -1,394 +1,394 @@
/**
* This class parses and stores parsed template data.
*/
class TemplateParser
{
constructor(TemplateLoader)
{
this.TemplateLoader = TemplateLoader;
/**
* Parsed Data Stores
*/
this.auras = {};
this.entities = {};
this.techs = {};
this.phases = {};
this.modifiers = {};
this.players = {};
this.phaseList = [];
}
getAura(auraName)
{
if (auraName in this.auras)
return this.auras[auraName];
if (!AuraTemplateExists(auraName))
return null;
let template = this.TemplateLoader.loadAuraTemplate(auraName);
let parsed = GetAuraDataHelper(template);
if (template.civ)
parsed.civ = template.civ;
let affectedPlayers = template.affectedPlayers || this.AuraAffectedPlayerDefault;
parsed.affectsTeam = this.AuraTeamIndicators.some(indicator => affectedPlayers.includes(indicator));
parsed.affectsSelf = this.AuraSelfIndicators.some(indicator => affectedPlayers.includes(indicator));
this.auras[auraName] = parsed;
return this.auras[auraName];
}
/**
* Load and parse a structure, unit, resource, etc from its entity template file.
*
* @param {string} templateName
* @param {string} civCode
* @return {(object|null)} Sanitized object about the requested template or null if entity template doesn't exist.
*/
getEntity(templateName, civCode)
{
if (!(civCode in this.entities))
this.entities[civCode] = {};
else if (templateName in this.entities[civCode])
return this.entities[civCode][templateName];
if (!Engine.TemplateExists(templateName))
return null;
let template = this.TemplateLoader.loadEntityTemplate(templateName, civCode);
const parsed = GetTemplateDataHelper(template, null, this.TemplateLoader.auraData, g_ResourceData, this.modifiers[civCode] || {});
parsed.name.internal = templateName;
parsed.history = template.Identity.History;
parsed.production = this.TemplateLoader.deriveProduction(template, civCode);
if (template.Builder)
parsed.builder = this.TemplateLoader.deriveBuildQueue(template, civCode);
// Set the minimum phase that this entity is available.
// For gaia objects, this is meaningless.
- if (!parsed.requiredTechnology)
+ if (!parsed.requirements)
parsed.phase = this.phaseList[0];
- else if (this.TemplateLoader.isPhaseTech(parsed.requiredTechnology))
- parsed.phase = this.getActualPhase(parsed.requiredTechnology);
+ else if (this.TemplateLoader.isPhaseTech(parsed.requirements.Techs))
+ parsed.phase = this.getActualPhase(parsed.requirements.Techs);
else
- parsed.phase = this.getPhaseOfTechnology(parsed.requiredTechnology, civCode);
+ parsed.phase = this.getPhaseOfTechnology(parsed.requirements.Techs, civCode);
if (template.Identity.Rank)
parsed.promotion = {
"current_rank": template.Identity.Rank,
"entity": template.Promotion && template.Promotion.Entity
};
if (template.ResourceSupply)
parsed.supply = {
"type": template.ResourceSupply.Type.split("."),
"amount": template.ResourceSupply.Max,
};
if (parsed.upgrades)
parsed.upgrades = this.getActualUpgradeData(parsed.upgrades, civCode);
if (parsed.wallSet)
{
parsed.wallset = {};
if (!parsed.upgrades)
parsed.upgrades = [];
// Note: An assumption is made here that wall segments all have the same resistance and auras
let struct = this.getEntity(parsed.wallSet.templates.long, civCode);
parsed.resistance = struct.resistance;
parsed.auras = struct.auras;
// For technology cost multiplier, we need to use the tower
struct = this.getEntity(parsed.wallSet.templates.tower, civCode);
parsed.techCostMultiplier = struct.techCostMultiplier;
let health;
for (let wSegm in parsed.wallSet.templates)
{
if (wSegm == "fort" || wSegm == "curves")
continue;
let wPart = this.getEntity(parsed.wallSet.templates[wSegm], civCode);
parsed.wallset[wSegm] = wPart;
for (let research of wPart.production.techs)
parsed.production.techs.push(research);
if (wPart.upgrades)
Array.prototype.push.apply(parsed.upgrades, wPart.upgrades);
if (["gate", "tower"].indexOf(wSegm) != -1)
continue;
if (!health)
{
health = { "min": wPart.health, "max": wPart.health };
continue;
}
health.min = Math.min(health.min, wPart.health);
health.max = Math.max(health.max, wPart.health);
}
if (parsed.wallSet.templates.curves)
for (let curve of parsed.wallSet.templates.curves)
{
let wPart = this.getEntity(curve, civCode);
health.min = Math.min(health.min, wPart.health);
health.max = Math.max(health.max, wPart.health);
}
if (health.min == health.max)
parsed.health = health.min;
else
parsed.health = sprintf(translate("%(health_min)s to %(health_max)s"), {
"health_min": health.min,
"health_max": health.max
});
}
this.entities[civCode][templateName] = parsed;
return parsed;
}
/**
* Load and parse technology from json template.
*
* @param {string} technologyName
* @param {string} civCode
* @return {Object} Sanitized data about the requested technology.
*/
getTechnology(technologyName, civCode)
{
if (!TechnologyTemplateExists(technologyName))
return null;
if (this.TemplateLoader.isPhaseTech(technologyName) && technologyName in this.phases)
return this.phases[technologyName];
if (!(civCode in this.techs))
this.techs[civCode] = {};
else if (technologyName in this.techs[civCode])
return this.techs[civCode][technologyName];
let template = this.TemplateLoader.loadTechnologyTemplate(technologyName);
const tech = GetTechnologyDataHelper(template, civCode, g_ResourceData, this.modifiers[civCode] || {});
tech.name.internal = technologyName;
if (template.pair !== undefined)
{
tech.pair = template.pair;
tech.reqs = this.mergeRequirements(tech.reqs, this.TemplateLoader.loadTechnologyPairTemplate(template.pair).reqs);
}
if (this.TemplateLoader.isPhaseTech(technologyName))
{
tech.actualPhase = technologyName;
if (tech.replaces !== undefined)
tech.actualPhase = tech.replaces[0];
this.phases[technologyName] = tech;
}
else
this.techs[civCode][technologyName] = tech;
return tech;
}
/**
* @param {string} phaseCode
* @param {string} civCode
* @return {Object} Sanitized object containing phase data
*/
getPhase(phaseCode, civCode)
{
return this.getTechnology(phaseCode, civCode);
}
/**
* Load and parse the relevant player_{civ}.xml template.
*/
getPlayer(civCode)
{
if (civCode in this.players)
return this.players[civCode];
let template = this.TemplateLoader.loadPlayerTemplate(civCode);
let parsed = {
"civbonuses": [],
"teambonuses": [],
};
if (template.Auras)
for (let auraTemplateName of template.Auras._string.split(/\s+/))
if (AuraTemplateExists(auraTemplateName))
if (this.getAura(auraTemplateName).affectsTeam)
parsed.teambonuses.push(auraTemplateName);
else
parsed.civbonuses.push(auraTemplateName);
this.players[civCode] = parsed;
return parsed;
}
/**
* Provided with an array containing basic information about possible
* upgrades, such as that generated by globalscript's GetTemplateDataHelper,
* this function loads the actual template data of the upgrades, overwrites
* certain values within, then passes an array containing the template data
* back to caller.
*/
getActualUpgradeData(upgradesInfo, civCode)
{
let newUpgrades = [];
for (let upgrade of upgradesInfo)
{
upgrade.entity = upgrade.entity.replace(/\{(civ|native)\}/g, civCode);
const data = GetTemplateDataHelper(this.TemplateLoader.loadEntityTemplate(upgrade.entity, civCode), null, this.TemplateLoader.auraData, g_ResourceData, this.modifiers[civCode] || {});
data.name.internal = upgrade.entity;
data.cost = upgrade.cost;
data.icon = upgrade.icon || data.icon;
data.tooltip = upgrade.tooltip || data.tooltip;
- data.requiredTechnology = upgrade.requiredTechnology || data.requiredTechnology;
+ data.requirements = upgrade.requirements || data.requirements;
- if (!data.requiredTechnology)
+ if (!data.requirements)
data.phase = this.phaseList[0];
- else if (this.TemplateLoader.isPhaseTech(data.requiredTechnology))
- data.phase = this.getActualPhase(data.requiredTechnology);
+ else if (this.TemplateLoader.isPhaseTech(data.requirements.Techs))
+ data.phase = this.getActualPhase(data.requirements.Techs);
else
- data.phase = this.getPhaseOfTechnology(data.requiredTechnology, civCode);
+ data.phase = this.getPhaseOfTechnology(data.requirements.Techs, civCode);
newUpgrades.push(data);
}
return newUpgrades;
}
/**
* Determines and returns the phase in which a given technology can be
* first researched. Works recursively through the given tech's
* pre-requisite and superseded techs if necessary.
*
* @param {string} techName - The Technology's name
* @param {string} civCode
* @return The name of the phase the technology belongs to, or false if
* the current civ can't research this tech
*/
getPhaseOfTechnology(techName, civCode)
{
let phaseIdx = -1;
if (basename(techName).startsWith("phase"))
{
if (!this.phases[techName].reqs)
return false;
phaseIdx = this.phaseList.indexOf(this.getActualPhase(techName));
if (phaseIdx > 0)
return this.phaseList[phaseIdx - 1];
}
let techReqs = this.getTechnology(techName, civCode).reqs;
if (!techReqs)
return false;
for (let option of techReqs)
if (option.techs)
for (let tech of option.techs)
{
if (basename(tech).startsWith("phase"))
return tech;
if (basename(tech).startsWith("pair"))
continue;
phaseIdx = Math.max(phaseIdx, this.phaseList.indexOf(this.getPhaseOfTechnology(tech, civCode)));
}
return this.phaseList[phaseIdx] || false;
}
/**
* Returns the actual phase a certain phase tech represents or stands in for.
*
* For example, passing `phase_city_athen` would result in `phase_city`.
*
* @param {string} phaseName
* @return {string}
*/
getActualPhase(phaseName)
{
if (this.phases[phaseName])
return this.phases[phaseName].actualPhase;
warn("Unrecognized phase (" + phaseName + ")");
return this.phaseList[0];
}
getModifiers(civCode)
{
return this.modifiers[civCode];
}
deriveModifications(civCode)
{
const player = this.getPlayer(civCode);
const auraList = clone(player.civbonuses);
for (const bonusname of player.teambonuses)
if (this.getAura(bonusname).affectsSelf)
auraList.push(bonusname);
this.modifiers[civCode] = this.TemplateLoader.deriveModifications(civCode, auraList);
}
derivePhaseList(technologyList, civCode)
{
// Load all of a civ's specific phase technologies
for (let techcode of technologyList)
if (this.TemplateLoader.isPhaseTech(techcode))
this.getTechnology(techcode, civCode);
this.phaseList = UnravelPhases(this.phases);
// Make sure all required generic phases are loaded and parsed
for (let phasecode of this.phaseList)
this.getTechnology(phasecode, civCode);
}
mergeRequirements(reqsA, reqsB)
{
if (!reqsA || !reqsB)
return false;
let finalReqs = clone(reqsA);
for (let option of reqsB)
for (let type in option)
for (let opt in finalReqs)
{
if (!finalReqs[opt][type])
finalReqs[opt][type] = [];
Array.prototype.push.apply(finalReqs[opt][type], option[type]);
}
return finalReqs;
}
}
// Default affected player token list to use if an aura doesn't explicitly give one.
// Keep in sync with simulation/components/Auras.js
TemplateParser.prototype.AuraAffectedPlayerDefault =
["Player"];
// List of tokens that, if found in an aura's "affectedPlayers" attribute, indicate
// that the aura applies to team members.
TemplateParser.prototype.AuraTeamIndicators =
["MutualAlly", "ExclusiveMutualAlly"];
// List of tokens that, if found in an aura's "affectedPlayers" attribute, indicate
// that the aura applies to the aura's owning civ.
TemplateParser.prototype.AuraSelfIndicators =
["Player", "Ally", "MutualAlly"];
Index: ps/trunk/binaries/data/mods/public/gui/session/diplomacy/playercontrols/SpyRequestButton.js
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/session/diplomacy/playercontrols/SpyRequestButton.js (revision 27244)
+++ ps/trunk/binaries/data/mods/public/gui/session/diplomacy/playercontrols/SpyRequestButton.js (revision 27245)
@@ -1,142 +1,142 @@
/**
* This class is concerned with providing the spy request button that once pressed will
* attempt to bribe a unit of the selected enemy for temporary vision sharing against resources.
*/
DiplomacyDialogPlayerControl.prototype.SpyRequestButton = class
{
constructor(playerID)
{
this.playerID = playerID;
// Players who requested a spy against this playerID.
this.spyRequests = new Set();
let id = "[" + (playerID - 1) + "]";
this.diplomacySpyRequest = Engine.GetGUIObjectByName("diplomacySpyRequest" + id);
this.diplomacySpyRequestImage = Engine.GetGUIObjectByName("diplomacySpyRequestImage" + id);
this.diplomacySpyRequest.onPress = this.onPress.bind(this);
}
onPress()
{
Engine.PostNetworkCommand({
"type": "spy-request",
"source": g_ViewedPlayer,
"player": this.playerID
});
this.spyRequests.add(g_ViewedPlayer);
this.update(false);
}
/**
* Called from GUIInterface notification.
* @param player is the one who requested a spy.
* @param notification.target is the player who shall be spied upon.
*/
onSpyResponse(notification, player, playerInactive)
{
// Update the state if the response was against the current row (target player)
if (notification.target == this.playerID)
{
this.spyRequests.delete(player);
// Update UI if the currently viewed player sent the request
if (player == g_ViewedPlayer)
this.update(false);
}
}
update(playerInactive)
{
let template = GetTemplateData(this.TemplateName);
let hidden =
playerInactive ||
!template ||
!!GetSimState().players[g_ViewedPlayer].disabledTemplates[this.TemplateName] ||
g_Players[this.playerID].isMutualAlly[g_ViewedPlayer] &&
!GetSimState().players[g_ViewedPlayer].hasSharedLos;
this.diplomacySpyRequest.hidden = hidden;
if (hidden)
return;
let tooltip = translate(this.Tooltip);
- if (template.requiredTechnology &&
- !Engine.GuiInterfaceCall("IsTechnologyResearched", {
- "tech": template.requiredTechnology,
+ if (template.requirements &&
+ !Engine.GuiInterfaceCall("AreRequirementsMet", {
+ "requirements": template.requirements,
"player": g_ViewedPlayer
}))
{
- tooltip += "\n" + getRequiredTechnologyTooltip(
+ tooltip += "\n" + getRequirementsTooltip(
false,
- template.requiredTechnology,
+ template.requirements,
GetSimState().players[g_ViewedPlayer].civ);
this.diplomacySpyRequest.enabled = false;
this.diplomacySpyRequest.tooltip = tooltip;
this.diplomacySpyRequestImage.sprite = this.SpriteModifierDisabled + this.Sprite;
return;
}
if (template.cost)
{
let modifiedTemplate = clone(template);
for (let res in template.cost)
modifiedTemplate.cost[res] =
Math.floor(GetSimState().players[this.playerID].spyCostMultiplier * template.cost[res]);
tooltip += "\n" + getEntityCostTooltip(modifiedTemplate);
let neededResources = Engine.GuiInterfaceCall("GetNeededResources", {
"cost": modifiedTemplate.cost,
"player": g_ViewedPlayer
});
if (neededResources)
{
tooltip += "\n" + getNeededResourcesTooltip(neededResources);
this.diplomacySpyRequest.enabled = false;
this.diplomacySpyRequest.tooltip = tooltip;
this.diplomacySpyRequestImage.sprite =
resourcesToAlphaMask(neededResources) + ":" + this.Sprite;
return;
}
let costRatio = Engine.GetTemplate(this.TemplateName).VisionSharing.FailureCostRatio;
if (costRatio)
{
for (let res in modifiedTemplate.cost)
modifiedTemplate.cost[res] = Math.floor(costRatio * modifiedTemplate.cost[res]);
tooltip +=
"\n" + translate(this.TooltipFailed) +
"\n" + getEntityCostTooltip(modifiedTemplate);
}
}
let enabled = !this.spyRequests.has(g_ViewedPlayer);
this.diplomacySpyRequest.enabled = enabled && controlsPlayer(g_ViewedPlayer);
this.diplomacySpyRequest.tooltip = tooltip;
this.diplomacySpyRequestImage.sprite = (enabled ? "" : this.SpriteModifierDisabled) + this.Sprite;
}
};
DiplomacyDialogPlayerControl.prototype.SpyRequestButton.prototype.TemplateName =
"special/spy";
DiplomacyDialogPlayerControl.prototype.SpyRequestButton.prototype.Sprite =
"stretched:" + "session/icons/bribes.png";
DiplomacyDialogPlayerControl.prototype.SpyRequestButton.prototype.SpriteModifierDisabled =
"color:0 0 0 127:grayscale:";
DiplomacyDialogPlayerControl.prototype.SpyRequestButton.prototype.Tooltip =
markForTranslation("Bribe a random unit from this player and share its vision during a limited period.");
DiplomacyDialogPlayerControl.prototype.SpyRequestButton.prototype.TooltipFailed =
markForTranslation("A failed bribe will cost you:");
Index: ps/trunk/binaries/data/mods/public/gui/session/selection_panels.js
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/session/selection_panels.js (revision 27244)
+++ ps/trunk/binaries/data/mods/public/gui/session/selection_panels.js (revision 27245)
@@ -1,1295 +1,1293 @@
/**
* Contains the layout and button settings per selection panel
*
* getItems returns a list of basic items used to fill the panel.
* This method is obligated. If the items list is empty, the panel
* won't be rendered.
*
* Then there's a loop over all items provided. In the loop,
* the item and some other standard data is added to a data object.
*
* The standard data is
* {
* "i": index
* "item": item coming from the getItems function
* "playerState": playerState
* "unitEntStates": states of the selected entities
* "rowLength": rowLength
* "numberOfItems": number of items that will be processed
* "button": gui Button object
* "icon": gui Icon object
* "guiSelection": gui button Selection overlay
* "countDisplay": gui caption space
* }
*
* Then for every data object, the setupButton function is called which
* sets the view and handlers of the button.
*/
// Cache some formation info
// Available formations per player
var g_AvailableFormations = new Map();
var g_FormationsInfo = new Map();
var g_SelectionPanels = {};
var g_SelectionPanelBarterButtonManager;
g_SelectionPanels.Alert = {
"getMaxNumberOfItems": function()
{
return 2;
},
"getItems": function(unitEntStates)
{
return unitEntStates.some(state => !!state.alertRaiser) ? ["raise", "end"] : [];
},
"setupButton": function(data)
{
data.button.onPress = function() {
switch (data.item)
{
case "raise":
raiseAlert();
return;
case "end":
endOfAlert();
return;
}
};
switch (data.item)
{
case "raise":
data.icon.sprite = "stretched:session/icons/bell_level1.png";
data.button.tooltip = translate("Raise an alert!");
break;
case "end":
data.button.tooltip = translate("End of alert.");
data.icon.sprite = "stretched:session/icons/bell_level0.png";
break;
}
data.button.enabled = controlsPlayer(data.player);
setPanelObjectPosition(data.button, this.getMaxNumberOfItems() - data.i, data.rowLength);
return true;
}
};
g_SelectionPanels.Barter = {
"getMaxNumberOfItems": function()
{
return 5;
},
"rowLength": 5,
"conflictsWith": ["Garrison"],
"getItems": function(unitEntStates)
{
// If more than `rowLength` resources, don't display icons.
if (unitEntStates.every(state => !state.isBarterMarket) || g_ResourceData.GetBarterableCodes().length > this.rowLength)
return [];
return g_ResourceData.GetBarterableCodes();
},
"setupButton": function(data)
{
if (g_SelectionPanelBarterButtonManager)
{
g_SelectionPanelBarterButtonManager.setViewedPlayer(data.player);
g_SelectionPanelBarterButtonManager.update();
}
return true;
}
};
g_SelectionPanels.Command = {
"getMaxNumberOfItems": function()
{
return 6;
},
"getItems": function(unitEntStates)
{
let commands = [];
for (let command in g_EntityCommands)
{
let info = getCommandInfo(command, unitEntStates);
if (info)
{
info.name = command;
commands.push(info);
}
}
return commands;
},
"setupButton": function(data)
{
data.button.tooltip = data.item.tooltip;
data.button.onPress = function() {
if (data.item.callback)
data.item.callback(data.item);
else
performCommand(data.unitEntStates, data.item.name);
};
data.countDisplay.caption = data.item.count || "";
data.button.enabled = data.item.enabled == true;
data.icon.sprite = "stretched:session/icons/" + data.item.icon;
let size = data.button.size;
// relative to the center ( = 50%)
size.rleft = 50;
size.rright = 50;
// offset from the center calculation, count on square buttons, so size.bottom is the width too
size.left = (data.i - data.numberOfItems / 2) * (size.bottom + 1);
size.right = size.left + size.bottom;
data.button.size = size;
return true;
}
};
g_SelectionPanels.Construction = {
"getMaxNumberOfItems": function()
{
return 40 - getNumberOfRightPanelButtons();
},
"rowLength": 10,
"getItems": function()
{
return getAllBuildableEntitiesFromSelection();
},
"setupButton": function(data)
{
let template = GetTemplateData(data.item, data.player);
if (!template)
return false;
- let technologyEnabled = Engine.GuiInterfaceCall("IsTechnologyResearched", {
- "tech": template.requiredTechnology,
+ const requirementsMet = Engine.GuiInterfaceCall("AreRequirementsMet", {
+ "requirements": template.requirements,
"player": data.player
});
let neededResources;
if (template.cost)
neededResources = Engine.GuiInterfaceCall("GetNeededResources", {
"cost": multiplyEntityCosts(template, 1),
"player": data.player
});
data.button.onPress = function() { startBuildingPlacement(data.item, data.playerState); };
let showTemplateFunc = () => { showTemplateDetails(data.item, data.playerState.civ); };
data.button.onPressRight = showTemplateFunc;
data.button.onPressRightDisabled = showTemplateFunc;
let tooltips = [
getEntityNamesFormatted,
getVisibleEntityClassesFormatted,
getAurasTooltip,
getEntityTooltip
].map(func => func(template));
tooltips.push(
getEntityCostTooltip(template, data.player),
getResourceDropsiteTooltip(template),
getGarrisonTooltip(template),
getTurretsTooltip(template),
getPopulationBonusTooltip(template),
showTemplateViewerOnRightClickTooltip(template)
);
let limits = getEntityLimitAndCount(data.playerState, data.item);
tooltips.push(
formatLimitString(limits.entLimit, limits.entCount, limits.entLimitChangers),
formatMatchLimitString(limits.matchLimit, limits.matchCount, limits.type),
- getRequiredTechnologyTooltip(technologyEnabled, template.requiredTechnology, GetSimState().players[data.player].civ),
+ getRequirementsTooltip(requirementsMet, template.requirements, GetSimState().players[data.player].civ),
getNeededResourcesTooltip(neededResources));
data.button.tooltip = tooltips.filter(tip => tip).join("\n");
let modifier = "";
- if (!technologyEnabled || limits.canBeAddedCount == 0)
+ if (!requirementsMet || limits.canBeAddedCount == 0)
{
data.button.enabled = false;
modifier += "color:0 0 0 127:grayscale:";
}
else if (neededResources)
{
data.button.enabled = false;
modifier += resourcesToAlphaMask(neededResources) + ":";
}
else
data.button.enabled = controlsPlayer(data.player);
if (template.icon)
data.icon.sprite = modifier + "stretched:session/portraits/" + template.icon;
setPanelObjectPosition(data.button, data.i + getNumberOfRightPanelButtons(), data.rowLength);
return true;
}
};
g_SelectionPanels.Formation = {
"getMaxNumberOfItems": function()
{
return 15;
},
"rowLength": 5,
"conflictsWith": ["Garrison"],
"getItems": function(unitEntStates)
{
if (unitEntStates.some(state => !hasClass(state, "Unit")))
return [];
if (unitEntStates.every(state => !state.unitAI || !state.unitAI.formations.length))
return [];
if (!g_AvailableFormations.has(unitEntStates[0].player))
g_AvailableFormations.set(unitEntStates[0].player, Engine.GuiInterfaceCall("GetAvailableFormations", unitEntStates[0].player));
return g_AvailableFormations.get(unitEntStates[0].player).filter(formation => unitEntStates.some(state => !!state.unitAI && state.unitAI.formations.includes(formation)));
},
"setupButton": function(data)
{
if (!g_FormationsInfo.has(data.item))
g_FormationsInfo.set(data.item, Engine.GuiInterfaceCall("GetFormationInfoFromTemplate", { "templateName": data.item }));
let formationOk = canMoveSelectionIntoFormation(data.item);
let unitIds = data.unitEntStates.map(state => state.id);
let formationSelected = Engine.GuiInterfaceCall("IsFormationSelected", {
"ents": unitIds,
"formationTemplate": data.item
});
data.button.onPress = function() {
performFormation(unitIds, data.item);
};
data.button.onMouseRightPress = () => g_AutoFormation.setDefault(data.item);
let formationInfo = g_FormationsInfo.get(data.item);
let tooltip = translate(formationInfo.name);
let isDefaultFormation = g_AutoFormation.isDefault(data.item);
if (data.item === NULL_FORMATION)
tooltip += "\n" + (isDefaultFormation ?
translate("Default formation is disabled.") :
translate("Right-click to disable the default formation feature."));
else
tooltip += "\n" + (isDefaultFormation ?
translate("This is the default formation, used for movement orders.") :
translate("Right-click to set this as the default formation."));
if (!formationOk && formationInfo.tooltip)
tooltip += "\n" + coloredText(translate(formationInfo.tooltip), "red");
data.button.tooltip = tooltip;
data.button.enabled = formationOk && controlsPlayer(data.player);
let grayscale = formationOk ? "" : "grayscale:";
data.guiSelection.hidden = !formationSelected;
data.countDisplay.hidden = !isDefaultFormation;
data.icon.sprite = "stretched:" + grayscale + "session/icons/" + formationInfo.icon;
setPanelObjectPosition(data.button, data.i, data.rowLength);
return true;
}
};
g_SelectionPanels.Garrison = {
"getMaxNumberOfItems": function()
{
return 12;
},
"rowLength": 4,
"conflictsWith": ["Barter"],
"getItems": function(unitEntStates)
{
if (unitEntStates.every(state => !state.garrisonHolder))
return [];
let groups = new EntityGroups();
for (let state of unitEntStates)
if (state.garrisonHolder)
groups.add(state.garrisonHolder.entities);
return groups.getEntsGrouped();
},
"setupButton": function(data)
{
let entState = GetEntityState(data.item.ents[0]);
let template = GetTemplateData(entState.template);
if (!template)
return false;
data.button.onPress = function() {
unloadTemplate(template.selectionGroupName || entState.template, entState.player);
};
data.countDisplay.caption = data.item.ents.length || "";
let canUngarrison = controlsPlayer(data.player) || controlsPlayer(entState.player);
data.button.enabled = canUngarrison;
data.button.tooltip = (canUngarrison ?
sprintf(translate("Unload %(name)s"), { "name": getEntityNames(template) }) + "\n" +
translate("Single-click to unload 1. Shift-click to unload all of this type.") :
getEntityNames(template)) + "\n" +
sprintf(translate("Player: %(playername)s"), {
"playername": g_Players[entState.player].name
});
data.guiSelection.sprite = "color:" + g_DiplomacyColors.getPlayerColor(entState.player, 160);
data.button.sprite_disabled = data.button.sprite;
// Selection panel buttons only appear disabled if they
// also appear disabled to the owner of the structure.
data.icon.sprite =
(canUngarrison || g_IsObserver ? "" : "grayscale:") +
"stretched:session/portraits/" + template.icon;
setPanelObjectPosition(data.button, data.i, data.rowLength);
return true;
}
};
g_SelectionPanels.Gate = {
"getMaxNumberOfItems": function()
{
return 40 - getNumberOfRightPanelButtons();
},
"rowLength": 10,
"getItems": function(unitEntStates)
{
let hideLocked = unitEntStates.every(state => !state.gate || !state.gate.locked);
let hideUnlocked = unitEntStates.every(state => !state.gate || state.gate.locked);
if (hideLocked && hideUnlocked)
return [];
return [
{
"hidden": hideLocked,
"tooltip": translate("Lock Gate"),
"icon": "session/icons/lock_locked.png",
"locked": true
},
{
"hidden": hideUnlocked,
"tooltip": translate("Unlock Gate"),
"icon": "session/icons/lock_unlocked.png",
"locked": false
}
];
},
"setupButton": function(data)
{
data.button.onPress = function() { lockGate(data.item.locked); };
data.button.tooltip = data.item.tooltip;
data.button.enabled = controlsPlayer(data.player);
data.guiSelection.hidden = data.item.hidden;
data.icon.sprite = "stretched:" + data.item.icon;
setPanelObjectPosition(data.button, data.i + getNumberOfRightPanelButtons(), data.rowLength);
return true;
}
};
g_SelectionPanels.Pack = {
"getMaxNumberOfItems": function()
{
return 40 - getNumberOfRightPanelButtons();
},
"rowLength": 10,
"getItems": function(unitEntStates)
{
let checks = {};
for (let state of unitEntStates)
{
if (!state.pack)
continue;
if (state.pack.progress == 0)
{
if (state.pack.packed)
checks.unpackButton = true;
else
checks.packButton = true;
}
else if (state.pack.packed)
checks.unpackCancelButton = true;
else
checks.packCancelButton = true;
}
let items = [];
if (checks.packButton)
items.push({
"packing": false,
"packed": false,
"tooltip": translate("Pack"),
"callback": function() { packUnit(true); }
});
if (checks.unpackButton)
items.push({
"packing": false,
"packed": true,
"tooltip": translate("Unpack"),
"callback": function() { packUnit(false); }
});
if (checks.packCancelButton)
items.push({
"packing": true,
"packed": false,
"tooltip": translate("Cancel Packing"),
"callback": function() { cancelPackUnit(true); }
});
if (checks.unpackCancelButton)
items.push({
"packing": true,
"packed": true,
"tooltip": translate("Cancel Unpacking"),
"callback": function() { cancelPackUnit(false); }
});
return items;
},
"setupButton": function(data)
{
data.button.onPress = function() {data.item.callback(data.item); };
data.button.tooltip = data.item.tooltip;
if (data.item.packing)
data.icon.sprite = "stretched:session/icons/cancel.png";
else if (data.item.packed)
data.icon.sprite = "stretched:session/icons/unpack.png";
else
data.icon.sprite = "stretched:session/icons/pack.png";
data.button.enabled = controlsPlayer(data.player);
setPanelObjectPosition(data.button, data.i + getNumberOfRightPanelButtons(), data.rowLength);
return true;
}
};
g_SelectionPanels.Queue = {
"getMaxNumberOfItems": function()
{
return 16;
},
/**
* Returns a list of all items in the productionqueue of the selection
* The first entry of every entity's production queue will come before
* the second entry of every entity's production queue
*/
"getItems": function(unitEntStates)
{
const queue = [];
let foundNew = true;
for (let i = 0; foundNew; ++i)
{
foundNew = false;
for (const state of unitEntStates)
{
if (!state.production || !state.production.queue[i])
continue;
queue.push({
"producingEnt": state.id,
"queuedItem": state.production.queue[i],
"autoqueue": state.production.autoqueue && state.production.queue[i].unitTemplate,
});
foundNew = true;
}
}
if (!queue.length)
return queue;
// Add 'ghost' items to show autoqueues.
const repeat = [];
for (const item of queue)
if (item.autoqueue)
{
const ghostItem = clone(item);
ghostItem.ghost = true;
repeat.push(ghostItem);
}
if (repeat.length)
for (let i = 0; queue.length < g_SelectionPanels.Queue.getMaxNumberOfItems(); ++i)
queue.push(repeat[i % repeat.length]);
return queue;
},
"resizePanel": function(numberOfItems, rowLength)
{
let numRows = Math.ceil(numberOfItems / rowLength);
let panel = Engine.GetGUIObjectByName("unitQueuePanel");
let size = panel.size;
let buttonSize = Engine.GetGUIObjectByName("unitQueueButton[0]").size.bottom;
let margin = 4;
size.top = size.bottom - numRows * buttonSize - (numRows + 2) * margin;
panel.size = size;
},
"setupButton": function(data)
{
let queuedItem = data.item.queuedItem;
// Differentiate between units and techs
let template;
if (queuedItem.unitTemplate)
template = GetTemplateData(queuedItem.unitTemplate);
else if (queuedItem.technologyTemplate)
template = GetTechnologyData(queuedItem.technologyTemplate, GetSimState().players[data.player].civ);
else
{
warning("Unknown production queue template " + uneval(queuedItem));
return false;
}
data.button.onPress = function() { removeFromProductionQueue(data.item.producingEnt, queuedItem.id); };
const tooltips = [getEntityNames(template)];
if (data.item.ghost)
tooltips.push(translate("The auto-queue will try to train this item later."));
if (queuedItem.neededSlots)
{
tooltips.push(coloredText(translate("Insufficient population capacity:"), "red"));
tooltips.push(sprintf(translate("%(population)s %(neededSlots)s"), {
"population": resourceIcon("population"),
"neededSlots": queuedItem.neededSlots
}));
}
tooltips.push(showTemplateViewerOnRightClickTooltip(template));
data.button.tooltip = tooltips.join("\n");
data.countDisplay.caption = queuedItem.count > 1 ? queuedItem.count : "";
const progressSlider = Engine.GetGUIObjectByName("unitQueueProgressSlider[" + data.i + "]");
if (data.item.ghost)
{
data.button.enabled = false;
progressSlider.sprite="color:0 150 250 50";
const size = progressSlider.size;
// Buttons are assumed to be square, so left/right offsets can be used for top/bottom.
size.top = size.left;
progressSlider.size = size;
}
else
{
// Show the time remaining to finish the first item
if (data.i == 0)
Engine.GetGUIObjectByName("queueTimeRemaining").caption =
Engine.FormatMillisecondsIntoDateStringGMT(queuedItem.timeRemaining, translateWithContext("countdown format", "m:ss"));
progressSlider.sprite = "queueProgressSlider";
const size = progressSlider.size;
// Buttons are assumed to be square, so left/right offsets can be used for top/bottom.
size.top = size.left + Math.round(queuedItem.progress * (size.right - size.left));
progressSlider.size = size;
data.button.enabled = controlsPlayer(data.player);
Engine.GetGUIObjectByName("unitQueuePausedIcon[" + data.i + "]").hidden = !queuedItem.paused;
if (queuedItem.paused)
// Translation: String displayed when the research is paused. E.g. by being garrisoned or when not the first item in the queue.
data.button.tooltip += "\n" + translate("This item is paused.");
}
if (template.icon)
{
let modifier = "stretched:";
if (queuedItem.paused)
modifier += "color:0 0 0 127:grayscale:";
else if (data.item.ghost)
modifier += "grayscale:";
data.icon.sprite = modifier + "session/portraits/" + template.icon;
}
const showTemplateFunc = () => { showTemplateDetails(data.item.queuedItem.unitTemplate || data.item.queuedItem.technologyTemplate, data.playerState.civ); };
data.button.onPressRight = showTemplateFunc;
data.button.onPressRightDisabled = showTemplateFunc;
setPanelObjectPosition(data.button, data.i, data.rowLength);
return true;
}
};
g_SelectionPanels.Research = {
"getMaxNumberOfItems": function()
{
return 10;
},
"rowLength": 10,
"getItems": function(unitEntStates)
{
let ret = [];
if (unitEntStates.length == 1)
{
const entState = unitEntStates[0];
if (!entState?.researcher?.technologies)
return ret;
if (!entState.production)
warn("Researcher without ProductionQueue found: " + entState.id + ".");
return entState.researcher.technologies.map(tech => ({
"tech": tech,
"techCostMultiplier": entState.researcher.techCostMultiplier,
"researchFacilityId": entState.id,
"isUpgrading": !!entState.upgrade && entState.upgrade.isUpgrading
}));
}
let sortedEntStates = unitEntStates.sort((a, b) =>
(!b.upgrade || !b.upgrade.isUpgrading) - (!a.upgrade || !a.upgrade.isUpgrading) ||
(!a.production ? 0 : a.production.queue.length) - (!b.production ? 0 : b.production.queue.length)
);
for (let state of sortedEntStates)
{
if (!state.researcher || !state.researcher.technologies)
continue;
if (!state.production)
warn("Researcher without ProductionQueue found: " + state.id + ".");
// Remove the techs we already have in ret (with the same name and techCostMultiplier)
const filteredTechs = state.researcher.technologies.filter(
tech => tech != null && !ret.some(
item =>
(item.tech == tech ||
item.tech.pair &&
tech.pair &&
item.tech.bottom == tech.bottom &&
item.tech.top == tech.top) &&
Object.keys(item.techCostMultiplier).every(
k => item.techCostMultiplier[k] == state.researcher.techCostMultiplier[k])
));
if (filteredTechs.length + ret.length <= this.getMaxNumberOfItems() &&
getNumberOfRightPanelButtons() <= this.getMaxNumberOfItems() * (filteredTechs.some(tech => !!tech.pair) ? 1 : 2))
ret = ret.concat(filteredTechs.map(tech => ({
"tech": tech,
"techCostMultiplier": state.researcher.techCostMultiplier,
"researchFacilityId": state.id,
"isUpgrading": !!state.upgrade && state.upgrade.isUpgrading
})));
}
return ret;
},
"hideItem": function(i, rowLength) // Called when no item is found
{
Engine.GetGUIObjectByName("unitResearchButton[" + i + "]").hidden = true;
// We also remove the paired tech and the pair symbol
Engine.GetGUIObjectByName("unitResearchButton[" + (i + rowLength) + "]").hidden = true;
Engine.GetGUIObjectByName("unitResearchPair[" + i + "]").hidden = true;
},
"setupButton": function(data)
{
if (!data.item.tech)
{
g_SelectionPanels.Research.hideItem(data.i, data.rowLength);
return false;
}
// Start position (start at the bottom)
let position = data.i + data.rowLength;
// Only show the top button for pairs
if (!data.item.tech.pair)
Engine.GetGUIObjectByName("unitResearchButton[" + data.i + "]").hidden = true;
// Set up the tech connector
let pair = Engine.GetGUIObjectByName("unitResearchPair[" + data.i + "]");
pair.hidden = data.item.tech.pair == null;
setPanelObjectPosition(pair, data.i, data.rowLength);
// Handle one or two techs (tech pair)
let player = data.player;
let playerState = GetSimState().players[player];
for (let tech of data.item.tech.pair ? [data.item.tech.bottom, data.item.tech.top] : [data.item.tech])
{
// Don't change the object returned by GetTechnologyData
let template = clone(GetTechnologyData(tech, playerState.civ));
if (!template)
return false;
// Not allowed by civ.
if (!template.reqs)
{
// One of the pair may still be researchable by the current civ,
// hence don't hide everything.
Engine.GetGUIObjectByName("unitResearchButton[" + data.i + "]").hidden = true;
pair.hidden = true;
continue;
}
for (let res in template.cost)
template.cost[res] *= data.item.techCostMultiplier[res] !== undefined ? data.item.techCostMultiplier[res] : 1;
let neededResources = Engine.GuiInterfaceCall("GetNeededResources", {
"cost": template.cost,
"player": player
});
let requirementsPassed = Engine.GuiInterfaceCall("CheckTechnologyRequirements", {
"tech": tech,
"player": player
});
let button = Engine.GetGUIObjectByName("unitResearchButton[" + position + "]");
let icon = Engine.GetGUIObjectByName("unitResearchIcon[" + position + "]");
let tooltips = [
getEntityNamesFormatted,
getEntityTooltip,
getEntityCostTooltip,
showTemplateViewerOnRightClickTooltip
].map(func => func(template));
if (!requirementsPassed)
{
let tip = template.requirementsTooltip;
let reqs = template.reqs;
for (let req of reqs)
{
if (!req.entities)
continue;
let entityCounts = [];
for (let entity of req.entities)
{
let current = 0;
switch (entity.check)
{
case "count":
current = playerState.classCounts[entity.class] || 0;
break;
case "variants":
current = playerState.typeCountsByClass[entity.class] ?
Object.keys(playerState.typeCountsByClass[entity.class]).length : 0;
break;
}
let remaining = entity.number - current;
if (remaining < 1)
continue;
entityCounts.push(sprintf(translatePlural("%(number)s entity of class %(class)s", "%(number)s entities of class %(class)s", remaining), {
"number": remaining,
"class": translate(entity.class)
}));
}
tip += " " + sprintf(translate("Remaining: %(entityCounts)s"), {
"entityCounts": entityCounts.join(translateWithContext("Separator for a list of entity counts", ", "))
});
}
tooltips.push(tip);
}
tooltips.push(getNeededResourcesTooltip(neededResources));
button.tooltip = tooltips.filter(tip => tip).join("\n");
button.onPress = (t => function() {
addResearchToQueue(data.item.researchFacilityId, t);
})(tech);
let showTemplateFunc = (t => function() {
showTemplateDetails(
t,
GetTemplateData(data.unitEntStates.find(state => state.id == data.item.researchFacilityId).template).nativeCiv);
});
button.onPressRight = showTemplateFunc(tech);
button.onPressRightDisabled = showTemplateFunc(tech);
if (data.item.tech.pair)
{
// On mouse enter, show a cross over the other icon
let unchosenIcon = Engine.GetGUIObjectByName("unitResearchUnchosenIcon[" + (position + data.rowLength) % (2 * data.rowLength) + "]");
button.onMouseEnter = function() {
unchosenIcon.hidden = false;
};
button.onMouseLeave = function() {
unchosenIcon.hidden = true;
};
}
button.hidden = false;
let modifier = "";
if (!requirementsPassed)
{
button.enabled = false;
modifier += "color:0 0 0 127:grayscale:";
}
else if (neededResources)
{
button.enabled = false;
modifier += resourcesToAlphaMask(neededResources) + ":";
}
else
button.enabled = controlsPlayer(data.player);
if (data.item.isUpgrading)
{
button.enabled = false;
modifier += "color:0 0 0 127:grayscale:";
button.tooltip += "\n" + coloredText(translate("Cannot research while upgrading."), "red");
}
if (template.icon)
icon.sprite = modifier + "stretched:session/portraits/" + template.icon;
setPanelObjectPosition(button, position, data.rowLength);
// Prepare to handle the top button (if any)
position -= data.rowLength;
}
return true;
}
};
g_SelectionPanels.Selection = {
"getMaxNumberOfItems": function()
{
return 16;
},
"rowLength": 4,
"getItems": function(unitEntStates)
{
if (unitEntStates.length < 2)
return [];
return g_Selection.groups.getEntsGrouped();
},
"setupButton": function(data)
{
let entState = GetEntityState(data.item.ents[0]);
let template = GetTemplateData(entState.template);
if (!template)
return false;
for (let ent of data.item.ents)
{
let state = GetEntityState(ent);
if (state.resourceCarrying && state.resourceCarrying.length !== 0)
{
if (!data.carried)
data.carried = {};
let carrying = state.resourceCarrying[0];
if (data.carried[carrying.type])
data.carried[carrying.type] += carrying.amount;
else
data.carried[carrying.type] = carrying.amount;
}
if (state.trader && state.trader.goods && state.trader.goods.amount)
{
if (!data.carried)
data.carried = {};
let amount = state.trader.goods.amount;
let type = state.trader.goods.type;
let totalGain = amount.traderGain;
if (amount.market1Gain)
totalGain += amount.market1Gain;
if (amount.market2Gain)
totalGain += amount.market2Gain;
if (data.carried[type])
data.carried[type] += totalGain;
else
data.carried[type] = totalGain;
}
}
let unitOwner = GetEntityState(data.item.ents[0]).player;
let tooltip = getEntityNames(template);
if (data.carried)
tooltip += "\n" + Object.keys(data.carried).map(res =>
resourceIcon(res) + data.carried[res]
).join(" ");
if (g_IsObserver)
tooltip += "\n" + sprintf(translate("Player: %(playername)s"), {
"playername": g_Players[unitOwner].name
});
data.button.tooltip = tooltip;
data.guiSelection.sprite = "color:" + g_DiplomacyColors.getPlayerColor(unitOwner, 160);
data.guiSelection.hidden = !g_IsObserver;
data.countDisplay.caption = data.item.ents.length || "";
data.button.onPress = function() {
if (Engine.HotkeyIsPressed("session.deselectgroup"))
removeFromSelectionGroup(data.item.key);
else
makePrimarySelectionGroup(data.item.key);
};
data.button.onPressRight = function() { removeFromSelectionGroup(data.item.key); };
if (template.icon)
data.icon.sprite = "stretched:session/portraits/" + template.icon;
setPanelObjectPosition(data.button, data.i, data.rowLength);
return true;
}
};
g_SelectionPanels.Stance = {
"getMaxNumberOfItems": function()
{
return 5;
},
"getItems": function(unitEntStates)
{
if (unitEntStates.some(state => !state.unitAI || !hasClass(state, "Unit") || hasClass(state, "Animal")))
return [];
return unitEntStates[0].unitAI.selectableStances;
},
"setupButton": function(data)
{
let unitIds = data.unitEntStates.map(state => state.id);
data.button.onPress = function() { performStance(unitIds, data.item); };
data.button.tooltip = getStanceDisplayName(data.item) + "\n" +
"[font=\"sans-13\"]" + getStanceTooltip(data.item) + "[/font]";
data.guiSelection.hidden = !Engine.GuiInterfaceCall("IsStanceSelected", {
"ents": unitIds,
"stance": data.item
});
data.icon.sprite = "stretched:session/icons/stances/" + data.item + ".png";
data.button.enabled = controlsPlayer(data.player);
setPanelObjectPosition(data.button, data.i, data.rowLength);
return true;
}
};
g_SelectionPanels.Training = {
"getMaxNumberOfItems": function()
{
return 40 - getNumberOfRightPanelButtons();
},
"rowLength": 10,
"getItems": function()
{
return getAllTrainableEntitiesFromSelection();
},
"setupButton": function(data)
{
let template = GetTemplateData(data.item, data.player);
if (!template)
return false;
- let technologyEnabled = Engine.GuiInterfaceCall("IsTechnologyResearched", {
- "tech": template.requiredTechnology,
+ const requirementsMet = Engine.GuiInterfaceCall("AreRequirementsMet", {
+ "requirements": template.requirements,
"player": data.player
});
let unitIds = data.unitEntStates.map(status => status.id);
let [buildingsCountToTrainFullBatch, fullBatchSize, remainderBatch] =
getTrainingStatus(unitIds, data.item, data.playerState);
let trainNum = buildingsCountToTrainFullBatch * fullBatchSize + remainderBatch;
let neededResources;
if (template.cost)
neededResources = Engine.GuiInterfaceCall("GetNeededResources", {
"cost": multiplyEntityCosts(template, trainNum),
"player": data.player
});
data.button.onPress = function() {
if (!neededResources)
addTrainingToQueue(unitIds, data.item, data.playerState);
};
let showTemplateFunc = () => { showTemplateDetails(data.item, data.playerState.civ); };
data.button.onPressRight = showTemplateFunc;
data.button.onPressRightDisabled = showTemplateFunc;
data.countDisplay.caption = trainNum > 1 ? trainNum : "";
let tooltips = [
"[font=\"sans-bold-16\"]" +
colorizeHotkey("%(hotkey)s", "session.queueunit." + (data.i + 1)) +
"[/font]" + " " + getEntityNamesFormatted(template),
getVisibleEntityClassesFormatted(template),
getAurasTooltip(template),
getEntityTooltip(template),
getEntityCostTooltip(template, data.player, unitIds[0], buildingsCountToTrainFullBatch, fullBatchSize, remainderBatch)
];
let limits = getEntityLimitAndCount(data.playerState, data.item);
tooltips.push(formatLimitString(limits.entLimit, limits.entCount, limits.entLimitChangers),
formatMatchLimitString(limits.matchLimit, limits.matchCount, limits.type));
if (Engine.ConfigDB_GetValue("user", "showdetailedtooltips") === "true")
tooltips = tooltips.concat([
getHealthTooltip,
getAttackTooltip,
getHealerTooltip,
getResistanceTooltip,
getGarrisonTooltip,
getTurretsTooltip,
getProjectilesTooltip,
getSpeedTooltip,
getResourceDropsiteTooltip
].map(func => func(template)));
tooltips.push(showTemplateViewerOnRightClickTooltip());
tooltips.push(
formatBatchTrainingString(buildingsCountToTrainFullBatch, fullBatchSize, remainderBatch),
- getRequiredTechnologyTooltip(technologyEnabled, template.requiredTechnology, GetSimState().players[data.player].civ),
+ getRequirementsTooltip(requirementsMet, template.requirements, GetSimState().players[data.player].civ),
getNeededResourcesTooltip(neededResources));
data.button.tooltip = tooltips.filter(tip => tip).join("\n");
let modifier = "";
- if (!technologyEnabled || limits.canBeAddedCount == 0)
+ if (!requirementsMet || limits.canBeAddedCount == 0)
{
data.button.enabled = false;
modifier = "color:0 0 0 127:grayscale:";
}
else
{
data.button.enabled = controlsPlayer(data.player);
if (neededResources)
modifier = resourcesToAlphaMask(neededResources) + ":";
}
if (data.unitEntStates.every(state => state.upgrade && state.upgrade.isUpgrading))
{
data.button.enabled = false;
modifier = "color:0 0 0 127:grayscale:";
data.button.tooltip += "\n" + coloredText(translate("Cannot train while upgrading."), "red");
}
if (template.icon)
data.icon.sprite = modifier + "stretched:session/portraits/" + template.icon;
let index = data.i + getNumberOfRightPanelButtons();
setPanelObjectPosition(data.button, index, data.rowLength);
return true;
}
};
g_SelectionPanels.Upgrade = {
"getMaxNumberOfItems": function()
{
return 40 - getNumberOfRightPanelButtons();
},
"rowLength": 10,
"getItems": function(unitEntStates)
{
// Interface becomes complicated with multiple different units and this is meant per-entity, so prevent it if the selection has multiple different units.
if (unitEntStates.some(state => state.template != unitEntStates[0].template))
return false;
return unitEntStates[0].upgrade && unitEntStates[0].upgrade.upgrades;
},
"setupButton": function(data)
{
let template = GetTemplateData(data.item.entity);
if (!template)
return false;
let progressOverlay = Engine.GetGUIObjectByName("unitUpgradeProgressSlider[" + data.i + "]");
progressOverlay.hidden = true;
- let technologyEnabled = true;
-
- if (data.item.requiredTechnology)
- technologyEnabled = Engine.GuiInterfaceCall("IsTechnologyResearched", {
- "tech": data.item.requiredTechnology,
+ const requirementsMet = !data.item.requirements ||
+ Engine.GuiInterfaceCall("AreRequirementsMet", {
+ "requirements": data.item.requirements,
"player": data.player
});
let limits = getEntityLimitAndCount(data.playerState, data.item.entity);
let upgradingEntStates = data.unitEntStates.filter(state => state.upgrade.template == data.item.entity);
let upgradableEntStates = data.unitEntStates.filter(state =>
!state.upgrade.progress &&
(!state.production || !state.production.queue || !state.production.queue.length));
let neededResources = data.item.cost && Engine.GuiInterfaceCall("GetNeededResources", {
"cost": multiplyEntityCosts(data.item, upgradableEntStates.length),
"player": data.player
});
let tooltip;
let modifier = "";
if (!upgradingEntStates.length && upgradableEntStates.length)
{
let primaryName = g_SpecificNamesPrimary ? template.name.specific : template.name.generic;
let secondaryName;
if (g_ShowSecondaryNames)
secondaryName = g_SpecificNamesPrimary ? template.name.generic : template.name.specific;
let tooltips = [];
if (g_ShowSecondaryNames)
{
if (data.item.tooltip)
tooltips.push(sprintf(translate("Upgrade to a %(primaryName)s (%(secondaryName)s). %(tooltip)s"), {
"primaryName": primaryName,
"secondaryName": secondaryName,
"tooltip": translate(data.item.tooltip)
}));
else
tooltips.push(sprintf(translate("Upgrade to a %(primaryName)s (%(secondaryName)s)."), {
"primaryName": primaryName,
"secondaryName": secondaryName
}));
}
else
{
if (data.item.tooltip)
tooltips.push(sprintf(translate("Upgrade to a %(primaryName)s. %(tooltip)s"), {
"primaryName": primaryName,
"tooltip": translate(data.item.tooltip)
}));
else
tooltips.push(sprintf(translate("Upgrade to a %(primaryName)s."), {
"primaryName": primaryName
}));
}
tooltips.push(
getEntityCostTooltip(data.item, undefined, undefined, data.unitEntStates.length),
formatLimitString(limits.entLimit, limits.entCount, limits.entLimitChangers),
formatMatchLimitString(limits.matchLimit, limits.matchCount, limits.type),
- getRequiredTechnologyTooltip(technologyEnabled, data.item.requiredTechnology, GetSimState().players[data.player].civ),
+ getRequirementsTooltip(requirementsMet, data.item.requirements, GetSimState().players[data.player].civ),
getNeededResourcesTooltip(neededResources),
showTemplateViewerOnRightClickTooltip());
tooltip = tooltips.filter(tip => tip).join("\n");
data.button.onPress = function() {
upgradeEntity(
data.item.entity,
upgradableEntStates.map(state => state.id));
};
- if (!technologyEnabled || limits.canBeAddedCount == 0 &&
+ if (!requirementsMet || limits.canBeAddedCount == 0 &&
!upgradableEntStates.some(state => hasSameRestrictionCategory(data.item.entity, state.template)))
{
data.button.enabled = false;
modifier = "color:0 0 0 127:grayscale:";
}
else if (neededResources)
{
data.button.enabled = false;
modifier = resourcesToAlphaMask(neededResources) + ":";
}
data.countDisplay.caption = upgradableEntStates.length > 1 ? upgradableEntStates.length : "";
}
else if (upgradingEntStates.length)
{
tooltip = translate("Cancel Upgrading");
data.button.onPress = function() { cancelUpgradeEntity(); };
data.countDisplay.caption = upgradingEntStates.length > 1 ? upgradingEntStates.length : "";
let progress = 0;
for (let state of upgradingEntStates)
progress = Math.max(progress, state.upgrade.progress || 1);
let progressOverlaySize = progressOverlay.size;
// TODO This is bad: we assume the progressOverlay is square
progressOverlaySize.top = progressOverlaySize.bottom + Math.round((1 - progress) * (progressOverlaySize.left - progressOverlaySize.right));
progressOverlay.size = progressOverlaySize;
progressOverlay.hidden = false;
}
else
{
tooltip = coloredText(translatePlural(
"Cannot upgrade when the entity is training, researching or already upgrading.",
"Cannot upgrade when all entities are training, researching or already upgrading.",
data.unitEntStates.length), "red");
data.button.onPress = function() {};
data.button.enabled = false;
modifier = "color:0 0 0 127:grayscale:";
}
data.button.enabled = controlsPlayer(data.player);
data.button.tooltip = tooltip;
let showTemplateFunc = () => { showTemplateDetails(data.item.entity, data.playerState.civ); };
data.button.onPressRight = showTemplateFunc;
data.button.onPressRightDisabled = showTemplateFunc;
data.icon.sprite = modifier + "stretched:session/" +
(data.item.icon || "portraits/" + template.icon);
setPanelObjectPosition(data.button, data.i + getNumberOfRightPanelButtons(), data.rowLength);
return true;
}
};
function initSelectionPanels()
{
let unitBarterPanel = Engine.GetGUIObjectByName("unitBarterPanel");
if (BarterButtonManager.IsAvailable(unitBarterPanel))
g_SelectionPanelBarterButtonManager = new BarterButtonManager(unitBarterPanel);
}
/**
* Pauses game and opens the template details viewer for a selected entity or technology.
*
* Technologies don't have a set civ, so we pass along the native civ of
* the template of the entity that's researching it.
*
* @param {string} [civCode] - The template name of the entity that researches the selected technology.
*/
function showTemplateDetails(templateName, civCode)
{
if (inputState != INPUT_NORMAL)
return;
g_PauseControl.implicitPause();
Engine.PushGuiPage(
"page_viewer.xml",
{
"templateName": templateName,
"civ": civCode
},
resumeGame);
}
/**
* If two panels need the same space, so they collide,
* the one appearing first in the order is rendered.
*
* Note that the panel needs to appear in the list to get rendered.
*/
let g_PanelsOrder = [
// LEFT PANE
"Barter", // Must always be visible on markets
"Garrison", // More important than Formation, as you want to see the garrisoned units in ships
"Alert",
"Formation",
"Stance", // Normal together with formation
// RIGHT PANE
"Gate", // Must always be shown on gates
"Pack", // Must always be shown on packable entities
"Upgrade", // Must always be shown on upgradable entities
"Training",
"Construction",
"Research", // Normal together with training
// UNIQUE PANES (importance doesn't matter)
"Command",
"Queue",
"Selection",
];
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 27244)
+++ ps/trunk/binaries/data/mods/public/simulation/ai/common-api/entity.js (revision 27245)
@@ -1,1029 +1,1013 @@
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)
{
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 value = this._template;
let args = string.split("/");
for (let arg of args)
{
value = value[arg];
if (value == undefined)
break;
}
this._tpCache.set(string, value);
}
return this._tpCache.get(string);
},
"templateName": function() { return this._templateName; },
"genericName": function() { return this.get("Identity/GenericName"); },
"civ": function() { return this.get("Identity/Civ"); },
"matchLimit": function() {
if (!this.get("TrainingRestrictions"))
return undefined;
return this.get("TrainingRestrictions/MatchLimit");
},
"classes": function() {
let template = this.get("Identity");
if (!template)
return undefined;
return GetIdentityClasses(template);
},
"hasClass": function(name) {
if (!this._classes)
this._classes = this.classes();
return this._classes && this._classes.indexOf(name) != -1;
},
"hasClasses": function(array) {
if (!this._classes)
this._classes = this.classes();
return this._classes && MatchesClassList(this._classes, array);
},
- "requiredTech": function() { return this.get("Identity/RequiredTechnology"); },
-
- "available": function(gameState) {
- let techRequired = this.requiredTech();
- if (!techRequired)
- return true;
- return gameState.isResearched(techRequired);
+ "requirements": function() {
+ return this.get("Identity/Requirements");
},
- // 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;
+ "available": function(gameState) {
+ const requirements = this.requirements();
+ return !requirements || Sim.RequirementsHelper.AreRequirementsMet(requirements, PlayerID);
},
"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("Researcher/TechCostMultiplier/"+type) || 1);
},
/**
* Returns { "max": max, "min": min } or undefined if no obstruction.
* max: radius of the outer circle surrounding this entity's obstruction shape
* min: radius of the inner circle
*/
"obstructionRadius": function() {
if (!this.get("Obstruction"))
return undefined;
if (this.get("Obstruction/Static"))
{
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() {
let attack = this.get("Attack");
if (!attack)
return undefined;
let ret = [];
for (let type in attack)
ret.push(type);
return ret;
},
"attackRange": function(type) {
if (!this.get("Attack/" + type))
return undefined;
return {
"max": +this.get("Attack/" + type +"/MaxRange"),
"min": +(this.get("Attack/" + type +"/MinRange") || 0)
};
},
"attackStrengths": function(type) {
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() {
let attack = this.get("Attack");
if (!attack)
return undefined;
let Classes = [];
for (let type in 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 the target entity.
// TODO: refine using the multiplier
"counters": function(target) {
let attack = this.get("Attack");
if (!attack)
return false;
let mcounter = [];
for (let type in 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(" "));
}
}
return target.hasClasses(mcounter);
},
// returns, if it exists, the multiplier from each attack against a given class
"getMultiplierAgainst": function(type, againstClass) {
if (!this.get("Attack/" + type +""))
return undefined;
let bonuses = this.get("Attack/" + type + "/Bonuses");
if (bonuses)
{
for (let b in 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) {
const templates = this.get("Trainer/Entities/_string");
if (!templates)
return undefined;
return templates.replace(/\{native\}/g, this.civ()).replace(/\{civ\}/g, civ).split(/\s+/);
},
"researchableTechs": function(gameState, civ) {
const templates = this.get("Researcher/Technologies/_string");
if (!templates)
return undefined;
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 };
},
"getResourceType": function() {
if (!this.get("ResourceSupply"))
return undefined;
return this.get("ResourceSupply/Type").split('.')[0];
},
"getDiminishingReturns": function() { return +(this.get("ResourceSupply/DiminishingReturns") || 1); },
"resourceSupplyMax": function() { return +this.get("ResourceSupply/Max"); },
"maxGatherers": function() { return +(this.get("ResourceSupply/MaxGatherers") || 0); },
"resourceGatherRates": function() {
if (!this.get("ResourceGatherer"))
return undefined;
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+/) : [];
},
"isResourceDropsite": function(resourceType) {
const types = this.resourceDropsiteTypes();
return types && (!resourceType || types.indexOf(resourceType) !== -1);
},
"isTreasure": function() { return this.get("Treasure") !== undefined; },
"treasureResources": function() {
if (!this.get("Treasure"))
return undefined;
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(researcher) {
let time = +this.get("Cost/BuildTime");
if (researcher)
time *= researcher.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"))
return undefined;
let territory = this.get("BuildRestrictions/Territory");
return !territory ? undefined : 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; },
"isTurretHolder": function() { return this.get("TurretHolder") !== undefined; },
/**
* returns true if the tempalte can capture the given target entity
* if no target is given, returns true if the template has the Capture attack
*/
"canCapture": function(target)
{
if (!this.get("Attack/Capture"))
return false;
if (!target)
return true;
if (!target.get("Capturable"))
return false;
let restrictedClasses = this.get("Attack/Capture/RestrictedClasses/_string");
return !restrictedClasses || !target.hasClasses(restrictedClasses);
},
"isCapturable": function() { return this.get("Capturable") !== undefined; },
"canGuard": function() { return this.get("UnitAI/CanGuard") === "true"; },
"canGarrison": function() { return "Garrisonable" in this._template; },
"canOccupyTurret": function() { return "Turretable" in this._template; },
"isTreasureCollector": function() { return this.get("TreasureCollector") !== undefined; },
});
// 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);
},
"queryInterface": function(iid) { return SimEngine.QueryInterface(this.id(), iid) },
"toString": function() { return "[Entity " + this.id() + " " + this.templateName() + "]"; },
"id": function() { return this._entity.id; },
/**
* Returns extra data that the AI scripts have associated with this entity,
* for arbitrary local annotations.
* (This data should not be shared with any other AI scripts.)
*/
"getMetadata": function(player, key) { return this._ai.getMetadata(player, this, key); },
/**
* Sets extra data to be associated with this entity.
*/
"setMetadata": function(player, key, value) { this._ai.setMetadata(player, this, key, value); },
"deleteAllMetadata": function(player) { delete this._ai._entityMetadata[player][this.id()]; },
"deleteMetadata": function(player, key) { this._ai.deleteMetadata(player, this, key); },
"position": function() { return this._entity.position; },
"angle": function() { return this._entity.angle; },
"isIdle": function() { return this._entity.idle; },
"getStance": function() { return this._entity.stance; },
"unitAIState": function() { return this._entity.unitAIState; },
"unitAIOrderData": function() { return this._entity.unitAIOrderData; },
"hitpoints": function() { return this._entity.hitpoints; },
"isHurt": function() { return this.hitpoints() < this.maxHitpoints(); },
"healthLevel": function() { return this.hitpoints() / this.maxHitpoints(); },
"needsHeal": function() { return this.isHurt() && this.isHealable(); },
"needsRepair": function() { return this.isHurt() && this.isRepairable(); },
"decaying": function() { return this._entity.decaying; },
"capturePoints": function() {return this._entity.capturePoints; },
"isInvulnerable": function() { return this._entity.invulnerability || false; },
"isSharedDropsite": function() { return this._entity.sharedDropsite === true; },
/**
* Returns the current training queue state, of the form
* [ { "id": 0, "template": "...", "count": 1, "progress": 0.5, "metadata": ... }, ... ]
*/
"trainingQueue": function() {
return this._entity.trainingQueue;
},
"trainingQueueTime": function() {
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() {
return this._entity.foundationProgress;
},
"getBuilders": function() {
if (this._entity.foundationProgress === undefined)
return undefined;
if (this._entity.foundationBuilders === undefined)
return [];
return this._entity.foundationBuilders;
},
"getBuildersNb": function() {
if (this._entity.foundationProgress === undefined)
return undefined;
if (this._entity.foundationBuilders === undefined)
return 0;
return this._entity.foundationBuilders.length;
},
"owner": function() {
return this._entity.owner;
},
"isOwn": function(player) {
if (typeof this._entity.owner === "undefined")
return false;
return this._entity.owner === player;
},
"resourceSupplyAmount": function() {
return this.queryInterface(Sim.IID_ResourceSupply)?.GetCurrentAmount();
},
"resourceSupplyNumGatherers": function()
{
return this.queryInterface(Sim.IID_ResourceSupply)?.GetNumGatherers();
},
"isFull": function()
{
let numGatherers = this.resourceSupplyNumGatherers();
if (numGatherers)
return this.maxGatherers() === numGatherers;
return undefined;
},
"resourceCarrying": function() {
return this.queryInterface(Sim.IID_ResourceGatherer)?.GetCarryingStatus();
},
"currentGatherRate": function() {
// returns the gather rate for the current target if applicable.
if (!this.get("ResourceGatherer"))
return undefined;
if (this.unitAIOrderData().length &&
this.unitAIState().split(".")[1] == "GATHER")
{
let res;
// this is an abuse of "_ai" but it works.
if (this.unitAIState().split(".")[1] == "GATHER" && this.unitAIOrderData()[0].target !== undefined)
res = this._ai._entities.get(this.unitAIOrderData()[0].target);
else if (this.unitAIOrderData()[1] !== undefined && this.unitAIOrderData()[1].target !== undefined)
res = this._ai._entities.get(this.unitAIOrderData()[1].target);
if (!res)
return 0;
let type = res.resourceSupplyType();
if (!type)
return 0;
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;
},
"garrisonHolderID": function() {
return this._entity.garrisonHolderID;
},
"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)
{
let attack = this.get("Attack");
if (!attack)
return false;
for (let type in 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 || !target.hasClasses(restrictedClasses))
return true;
}
return false;
},
"move": function(x, z, queued = false, pushFront = false) {
Engine.PostCommand(PlayerID, { "type": "walk", "entities": [this.id()], "x": x, "z": z, "queued": queued, "pushFront": pushFront });
return this;
},
"moveToRange": function(x, z, min, max, queued = false, pushFront = false) {
Engine.PostCommand(PlayerID, { "type": "walk-to-range", "entities": [this.id()], "x": x, "z": z, "min": min, "max": max, "queued": queued, "pushFront": pushFront });
return this;
},
"attackMove": function(x, z, targetClasses, allowCapture = true, queued = false, pushFront = false) {
Engine.PostCommand(PlayerID, { "type": "attack-walk", "entities": [this.id()], "x": x, "z": z, "targetClasses": targetClasses, "allowCapture": allowCapture, "queued": queued, "pushFront": pushFront });
return this;
},
// violent, aggressive, defensive, passive, standground
"setStance": function(stance) {
if (this.getStance() === undefined)
return undefined;
Engine.PostCommand(PlayerID, { "type": "stance", "entities": [this.id()], "name": stance});
return this;
},
"stopMoving": function() {
Engine.PostCommand(PlayerID, { "type": "stop", "entities": [this.id()], "queued": false, "pushFront": false });
},
"unload": function(id) {
if (!this.get("GarrisonHolder"))
return undefined;
Engine.PostCommand(PlayerID, { "type": "unload", "garrisonHolder": this.id(), "entities": [id] });
return this;
},
// Unloads all owned units, don't unload allies
"unloadAll": function() {
if (!this.get("GarrisonHolder"))
return undefined;
Engine.PostCommand(PlayerID, { "type": "unload-all-by-owner", "garrisonHolders": [this.id()] });
return this;
},
"garrison": function(target, queued = false, pushFront = false) {
Engine.PostCommand(PlayerID, { "type": "garrison", "entities": [this.id()], "target": target.id(), "queued": queued, "pushFront": pushFront });
return this;
},
"occupy-turret": function(target, queued = false, pushFront = false) {
Engine.PostCommand(PlayerID, { "type": "occupy-turret", "entities": [this.id()], "target": target.id(), "queued": queued, "pushFront": pushFront });
return this;
},
"attack": function(unitId, allowCapture = true, queued = false, pushFront = false) {
Engine.PostCommand(PlayerID, { "type": "attack", "entities": [this.id()], "target": unitId, "allowCapture": allowCapture, "queued": queued, "pushFront": pushFront });
return this;
},
"collectTreasure": function(target, queued = false, pushFront = false) {
Engine.PostCommand(PlayerID, {
"type": "collect-treasure",
"entities": [this.id()],
"target": target.id(),
"queued": queued,
"pushFront": pushFront
});
return this;
},
// moveApart from a point in the opposite direction with a distance dist
"moveApart": function(point, dist) {
if (this.position() !== undefined)
{
let direction = [this.position()[0] - point[0], this.position()[1] - point[1]];
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, "pushFront": 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, "pushFront": false });
}
return this;
},
"gather": function(target, queued = false, pushFront = false) {
Engine.PostCommand(PlayerID, { "type": "gather", "entities": [this.id()], "target": target.id(), "queued": queued, "pushFront": pushFront });
return this;
},
"repair": function(target, autocontinue = false, queued = false, pushFront = false) {
Engine.PostCommand(PlayerID, { "type": "repair", "entities": [this.id()], "target": target.id(), "autocontinue": autocontinue, "queued": queued, "pushFront": pushFront });
return this;
},
"returnResources": function(target, queued = false, pushFront = false) {
Engine.PostCommand(PlayerID, { "type": "returnresource", "entities": [this.id()], "target": target.id(), "queued": queued, "pushFront": pushFront });
return this;
},
"destroy": function() {
Engine.PostCommand(PlayerID, { "type": "delete-entities", "entities": [this.id()] });
return this;
},
"barter": function(buyType, sellType, amount) {
Engine.PostCommand(PlayerID, { "type": "barter", "sell": sellType, "buy": buyType, "amount": amount });
return this;
},
"tradeRoute": function(target, source) {
Engine.PostCommand(PlayerID, { "type": "setup-trade-route", "entities": [this.id()], "target": target.id(), "source": source.id(), "route": undefined, "queued": false, "pushFront": false });
return this;
},
"setRallyPoint": function(target, command) {
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, pushFront = false)
{
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,
"pushFront": pushFront
});
return this;
},
"construct": function(template, x, z, angle, metadata) {
// TODO: verify this unit can construct this, just for internal
// sanity-checking and error reporting
Engine.PostCommand(PlayerID, {
"type": "construct",
"entities": [this.id()],
"template": template,
"x": x,
"z": z,
"angle": angle,
"autorepair": false,
"autocontinue": false,
"queued": false,
"pushFront": false,
"metadata": metadata // can be undefined
});
return this;
},
"research": function(template, pushFront = false) {
Engine.PostCommand(PlayerID, {
"type": "research",
"entity": this.id(),
"template": template,
"pushFront": pushFront
});
return this;
},
"stopProduction": function(id) {
Engine.PostCommand(PlayerID, { "type": "stop-production", "entity": this.id(), "id": id });
return this;
},
"stopAllProduction": function(percentToStopAt) {
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, pushFront = false) {
Engine.PostCommand(PlayerID, { "type": "guard", "entities": [this.id()], "target": target.id(), "queued": queued, "pushFront": pushFront });
return this;
},
"removeGuard": function() {
Engine.PostCommand(PlayerID, { "type": "remove-guard", "entities": [this.id()] });
return this;
}
});
return m;
}(API3);
Index: ps/trunk/binaries/data/mods/public/simulation/ai/petra/headquarters.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/ai/petra/headquarters.js (revision 27244)
+++ ps/trunk/binaries/data/mods/public/simulation/ai/petra/headquarters.js (revision 27245)
@@ -1,2406 +1,2406 @@
/**
* 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;
// 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.basesManager = new PETRA.BasesManager(this.Config);
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.emergencyManager = new PETRA.EmergencyManager(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);
// 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 => ent.isTreasure());
this.treasures.registerUpdates();
this.currentPhase = gameState.currentPhase();
this.decayingStructures = new Set();
this.emergencyManager.init(gameState);
};
/**
* initialization needed after deserialization (only called when deserialization)
*/
PETRA.HQ.prototype.postinit = function(gameState)
{
this.basesManager.postinit(gameState);
this.updateTerritories(gameState);
};
/**
* 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;
};
PETRA.HQ.prototype.checkEvents = function(gameState, events)
{
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;
}
this.basesManager.checkEvents(gameState, events);
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;
}
for (let evt of events.OwnershipChanged) // capture events
{
if (evt.to != PlayerID)
continue;
let ent = gameState.getEntityById(evt.entity);
if (!ent)
continue;
if (!ent.hasClass("Unit"))
{
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);
}
continue;
}
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", PETRA.Worker.ROLE_TRADER);
ent.setMetadata(PlayerID, "route", undefined);
}
if (ent.hasClass("Worker"))
{
ent.setMetadata(PlayerID, "role", PETRA.Worker.ROLE_WORKER);
ent.setMetadata(PlayerID, "subrole", PETRA.Worker.SUBROLE_IDLE);
}
if (ent.hasClass("Ship"))
PETRA.setSeaAccess(gameState, ent);
if (!ent.hasClasses(["Support", "Ship"]) && ent.attackTypes() !== undefined)
ent.setMetadata(PlayerID, "plan", -1);
}
// 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 holder = gameState.getEntityById(ent.garrisonHolderID());
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 !== PETRA.AttackPlan.STATE_UNEXECUTED)
ent.setMetadata(PlayerID, "plan", -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);
}
// Then deals with decaying structures: destroy them if being lost to enemy (except in easier difficulties)
if (this.Config.difficulty < PETRA.DIFFICULTY_EASY)
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);
}
};
PETRA.HQ.prototype.handleNewBase = function(gameState)
{
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;
}
};
/** 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())
{
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;
}
}
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"] ];
const 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") === PETRA.Worker.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 === PETRA.Worker.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[PETRA.AttackPlan.TYPE_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] ];
const classes = [["CitizenSoldier", "Infantry"]];
// We want at least 33% ranged and 33% melee.
classes[0].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": PETRA.Worker.ROLE_WORKER, "base": 0, "support": true }, size, size));
else if (template)
queues.citizenSoldier.addPlan(new PETRA.TrainingPlan(gameState, template, { "role": PETRA.Worker.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, []);
// We do not want siege tower as AI does not know how to use it nor hero when not explicitely specified.
else
units = gameState.findTrainableUnits(classes, ["Hero", "SiegeTower"]);
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)
{
return this.basesManager.bulkPickWorkers(gameState, baseRef, number);
};
PETRA.HQ.prototype.getTotalResourceLevel = function(gameState, resources, proximity)
{
return this.basesManager.getTotalResourceLevel(gameState, resources, proximity);
};
/**
* 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)
{
return this.basesManager.GetCurrentGatherRates(gameState);
};
/**
* 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"));
const dpEnts = gameState.getOwnDropsites().filter(API3.Filters.not(API3.Filters.byClasses(["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 (const 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 (const 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);
const isNavalMarket = template.hasClasses(["Naval+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.baseAtIndex(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.hasClasses(["Naval+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.baseAtIndex(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.
const ownStructures = gameState.getOwnStructures().filter(API3.Filters.byClasses(["Fortress", "Tower"])).toEntityArray();
let enemyStructures = gameState.getEnemyStructures().filter(API3.Filters.not(API3.Filters.byOwner(0))).
filter(API3.Filters.byClasses(["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.byClasses(["CivCentre", "Fortress", "Tower"]));
if (!enemyStructures.hasEntities() && !gameState.getAlliedVictory())
enemyStructures = gameState.getAllyStructures().filter(API3.Filters.not(API3.Filters.byOwner(PlayerID))).
filter(API3.Filters.byClasses(["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.baseAtIndex(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.baseAtIndex(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)
{
let houseTemplateString = "structures/{civ}/apartment";
if (!gameState.isTemplateAvailable(gameState.applyCiv(houseTemplateString)) ||
!this.canBuild(gameState, houseTemplateString))
{
houseTemplateString = "structures/{civ}/house";
if (!gameState.isTemplateAvailable(gameState.applyCiv(houseTemplateString)))
return;
}
if (gameState.getPopulationMax() <= gameState.getPopulationLimit())
return;
let numPlanned = queues.house.length();
if (numPlanned < 3 || numPlanned < 5 && gameState.getPopulation() > 80)
{
let plan = new PETRA.ConstructionPlan(gameState, houseTemplateString);
// 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(houseTemplateString);
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(houseTemplateString));
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(houseTemplateString);
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.hasPotentialBase())
{
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.hasPotentialBase() && 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 (e.g. barracks, stable).
* 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 stable.
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 barracks/range and stable.
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;
}
// Third barracks/range and stable, 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_stable") && !gameState.getOwnEntitiesByClass("ElephantStable", true).hasEntities())
{
queues.militaryBuilding.addPlan(new PETRA.ConstructionPlan(gameState, "structures/{civ}/elephant_stable", { "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 (const 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.hasClasses(["Infantry+CitizenSoldier"]))
continue;
if (autogarrison && !template.hasClasses(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;
}
}
const metadata = { "role": PETRA.Worker.ROLE_WORKER, "base": nearestAnchor.getMetadata(PlayerID, "base"), "plan": -1, "trainer": nearestAnchor.id() };
if (autogarrison)
metadata.garrisonType = PETRA.GarrisonManager.TYPE_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");
+ this.buildManager.setUnbuildable(gameState, type, 30, "requirements");
return false;
}
if (!this.buildManager.hasBuilder(type))
{
this.buildManager.setUnbuildable(gameState, type, 120, "nobuilder");
return false;
}
if (!this.hasActiveBase())
{
// 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)
this.basesManager.removeBaseFromTerritoryIndex(j);
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.basesManager.addTerritoryIndexToBase(gameState, j, passabilityMap))
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;
};
/**
* returns the base corresponding to baseID
*/
PETRA.HQ.prototype.getBaseByID = function(baseID)
{
return this.basesManager.getBaseByID(baseID);
};
/**
* 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()
{
return this.basesManager.numActiveBases();
};
PETRA.HQ.prototype.hasActiveBase = function()
{
return this.basesManager.hasActiveBase();
};
PETRA.HQ.prototype.numPotentialBases = function()
{
return this.basesManager.numPotentialBases();
};
PETRA.HQ.prototype.hasPotentialBase = function()
{
return this.basesManager.hasPotentialBase();
};
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;
};
/**
* 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(PETRA.Worker.ROLE_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 !== PETRA.Worker.ROLE_WORKER)
continue;
workers += item.count;
}
}
this.turnCache.accountedWorkers = workers;
}
return this.turnCache.accountedWorkers;
};
PETRA.HQ.prototype.baseManagers = function()
{
return this.basesManager.baseManagers;
};
/**
* @param {number} territoryIndex - The index to get the map for.
* @return {number} - The ID of the base at the given territory index.
*/
PETRA.HQ.prototype.baseAtIndex = function(territoryIndex)
{
return this.basesManager.baseAtIndex(territoryIndex);
};
/**
* Some functions are run every turn
* Others once in a while
*/
PETRA.HQ.prototype.update = function(gameState, queues, events)
{
Engine.ProfileStart("Headquarters update");
this.emergencyManager.update(gameState);
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.hasActiveBase())
{
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.hasPotentialBase() ||
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 > PETRA.DIFFICULTY_SANDBOX)
this.buildDefenses(gameState, queues);
}
this.basesManager.update(gameState, queues, events);
this.navalManager.update(gameState, queues, events);
if (this.Config.difficulty > PETRA.DIFFICULTY_SANDBOX && (this.hasActiveBase() || !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,
"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
};
if (this.Config.debug == -100)
{
API3.warn(" HQ serialization ---------------------");
API3.warn(" properties " + uneval(properties));
API3.warn(" basesManager " + uneval(this.basesManager.Serialize()));
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()));
API3.warn(" emergencyManager " + uneval(this.emergencyManager.Serialize()));
}
return {
"properties": properties,
"basesManager": this.basesManager.Serialize(),
"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(),
"emergencyManager": this.emergencyManager.Serialize(),
};
};
PETRA.HQ.prototype.Deserialize = function(gameState, data)
{
for (let key in data.properties)
this[key] = data.properties[key];
this.basesManager = new PETRA.BasesManager(this.Config);
this.basesManager.init(gameState);
this.basesManager.Deserialize(gameState, data.basesManager);
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);
this.emergencyManager = new PETRA.EmergencyManager(this.Config);
this.emergencyManager.Deserialize(data.emergencyManager);
};
Index: ps/trunk/binaries/data/mods/public/simulation/ai/petra/queueplanBuilding.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/ai/petra/queueplanBuilding.js (revision 27244)
+++ ps/trunk/binaries/data/mods/public/simulation/ai/petra/queueplanBuilding.js (revision 27245)
@@ -1,945 +1,945 @@
/**
* Defines a construction plan, ie a building.
* We'll try to fing a good position if non has been provided
*/
PETRA.ConstructionPlan = function(gameState, type, metadata, position)
{
if (!PETRA.QueuePlan.call(this, gameState, type, metadata))
return false;
this.position = position ? position : 0;
this.category = "building";
return true;
};
PETRA.ConstructionPlan.prototype = Object.create(PETRA.QueuePlan.prototype);
PETRA.ConstructionPlan.prototype.canStart = function(gameState)
{
if (gameState.ai.HQ.turnCache.buildingBuilt) // do not start another building if already one this turn
return false;
if (!this.isGo(gameState))
return false;
- if (this.template.requiredTech() && !gameState.isResearched(this.template.requiredTech()))
+ if (!this.template.available(gameState))
return false;
return gameState.ai.HQ.buildManager.hasBuilder(this.type);
};
PETRA.ConstructionPlan.prototype.start = function(gameState)
{
Engine.ProfileStart("Building construction start");
// We don't care which builder we assign, since they won't actually do
// the building themselves - all we care about is that there is at least
// one unit that can start the foundation (should always be the case here).
let builder = gameState.findBuilder(this.type);
if (!builder)
{
API3.warn("petra error: builder not found when starting construction.");
Engine.ProfileStop();
return;
}
let pos = this.findGoodPosition(gameState);
if (!pos)
{
gameState.ai.HQ.buildManager.setUnbuildable(gameState, this.type, 90, "room");
Engine.ProfileStop();
return;
}
if (this.metadata && this.metadata.expectedGain && (!this.template.hasClass("Market") ||
gameState.getOwnEntitiesByClass("Market", true).hasEntities()))
{
// Check if this Market is still worth building (others may have been built making it useless).
let tradeManager = gameState.ai.HQ.tradeManager;
tradeManager.checkRoutes(gameState);
if (!tradeManager.isNewMarketWorth(this.metadata.expectedGain))
{
Engine.ProfileStop();
return;
}
}
gameState.ai.HQ.turnCache.buildingBuilt = true;
if (this.metadata === undefined)
this.metadata = { "base": pos.base };
else if (this.metadata.base === undefined)
this.metadata.base = pos.base;
if (pos.access)
this.metadata.access = pos.access; // needed for Docks whose position is on water
else
this.metadata.access = gameState.ai.accessibility.getAccessValue([pos.x, pos.z]);
if (this.template.buildPlacementType() == "shore")
{
// adjust a bit the position if needed
let cosa = Math.cos(pos.angle);
let sina = Math.sin(pos.angle);
let shiftMax = gameState.ai.HQ.territoryMap.cellSize;
for (let shift = 0; shift <= shiftMax; shift += 2)
{
builder.construct(this.type, pos.x-shift*sina, pos.z-shift*cosa, pos.angle, this.metadata);
if (shift > 0)
builder.construct(this.type, pos.x+shift*sina, pos.z+shift*cosa, pos.angle, this.metadata);
}
}
else if (pos.xx === undefined || pos.x == pos.xx && pos.z == pos.zz)
builder.construct(this.type, pos.x, pos.z, pos.angle, this.metadata);
else // try with the lowest, move towards us unless we're same
{
for (let step = 0; step <= 1; step += 0.2)
builder.construct(this.type, step*pos.x + (1-step)*pos.xx, step*pos.z + (1-step)*pos.zz,
pos.angle, this.metadata);
}
this.onStart(gameState);
Engine.ProfileStop();
if (this.metadata && this.metadata.proximity)
gameState.ai.HQ.navalManager.createTransportIfNeeded(gameState, this.metadata.proximity, [pos.x, pos.z], this.metadata.access);
};
PETRA.ConstructionPlan.prototype.findGoodPosition = function(gameState)
{
let template = this.template;
if (template.buildPlacementType() == "shore")
return this.findDockPosition(gameState);
let HQ = gameState.ai.HQ;
if (template.hasClass("Storehouse") && this.metadata && this.metadata.base)
{
// recompute the best dropsite location in case some conditions have changed
let base = HQ.getBaseByID(this.metadata.base);
let type = this.metadata.type ? this.metadata.type : "wood";
const newpos = base.findBestDropsiteLocation(gameState, type, template._templateName);
if (newpos && newpos.quality > 0)
{
let pos = newpos.pos;
return { "x": pos[0], "z": pos[1], "angle": 3*Math.PI/4, "base": this.metadata.base };
}
}
if (!this.position)
{
if (template.hasClass("CivCentre"))
{
let pos;
if (this.metadata && this.metadata.resource)
{
let proximity = this.metadata.proximity ? this.metadata.proximity : undefined;
pos = HQ.findEconomicCCLocation(gameState, template, this.metadata.resource, proximity);
}
else
pos = HQ.findStrategicCCLocation(gameState, template);
if (pos)
return { "x": pos[0], "z": pos[1], "angle": 3*Math.PI/4, "base": 0 };
// No possible location, try to build instead a dock in a not-enemy island
let templateName = gameState.applyCiv("structures/{civ}/dock");
if (gameState.ai.HQ.canBuild(gameState, templateName) && !gameState.isTemplateDisabled(templateName))
{
template = gameState.getTemplate(templateName);
if (template && gameState.getResources().canAfford(new API3.Resources(template.cost())))
this.buildOverseaDock(gameState, template);
}
return false;
}
else if (template.hasClasses(["Tower", "Fortress", "ArmyCamp"]))
{
let pos = HQ.findDefensiveLocation(gameState, template);
if (pos)
return { "x": pos[0], "z": pos[1], "angle": 3*Math.PI/4, "base": pos[2] };
// if this fortress is our first one, just try the standard placement
if (!template.hasClass("Fortress") || gameState.getOwnEntitiesByClass("Fortress", true).hasEntities())
return false;
}
else if (template.hasClass("Market")) // Docks are done before.
{
let pos = HQ.findMarketLocation(gameState, template);
if (pos && pos[2] > 0)
{
if (!this.metadata)
this.metadata = {};
this.metadata.expectedGain = pos[3];
return { "x": pos[0], "z": pos[1], "angle": 3*Math.PI/4, "base": pos[2] };
}
else if (!pos)
return false;
}
}
// Compute each tile's closeness to friendly structures:
let placement = new API3.Map(gameState.sharedScript, "territory");
let cellSize = placement.cellSize; // size of each tile
let alreadyHasHouses = false;
if (this.position) // If a position was specified then place the building as close to it as possible
{
let x = Math.floor(this.position[0] / cellSize);
let z = Math.floor(this.position[1] / cellSize);
placement.addInfluence(x, z, 255);
}
else // No position was specified so try and find a sensible place to build
{
// give a small > 0 level as the result of addInfluence is constrained to be > 0
// if we really need houses (i.e. Phasing without enough village building), do not apply these constraints
if (this.metadata && this.metadata.base !== undefined)
{
let base = this.metadata.base;
for (let j = 0; j < placement.map.length; ++j)
if (HQ.baseAtIndex(j) == base)
placement.set(j, 45);
}
else
{
for (let j = 0; j < placement.map.length; ++j)
if (HQ.baseAtIndex(j) != 0)
placement.set(j, 45);
}
if (!HQ.requireHouses || !template.hasClass("House"))
{
gameState.getOwnStructures().forEach(function(ent) {
let pos = ent.position();
let x = Math.round(pos[0] / cellSize);
let z = Math.round(pos[1] / cellSize);
let struct = PETRA.getBuiltEntity(gameState, ent);
if (struct.resourceDropsiteTypes() && struct.resourceDropsiteTypes().indexOf("food") != -1)
{
if (template.hasClasses(["Field", "Corral"]))
placement.addInfluence(x, z, 80 / cellSize, 50);
else // If this is not a field add a negative influence because we want to leave this area for fields
placement.addInfluence(x, z, 80 / cellSize, -20);
}
else if (template.hasClass("House"))
{
if (ent.hasClass("House"))
{
placement.addInfluence(x, z, 60 / cellSize, 40); // houses are close to other houses
alreadyHasHouses = true;
}
else if (ent.hasClasses(["Gate", "!Wall"]))
placement.addInfluence(x, z, 60 / cellSize, -40); // and further away from other stuffs
}
else if (template.hasClass("Farmstead") && !ent.hasClasses(["Field", "Corral"]) &&
ent.hasClasses(["Gate", "!Wall"]))
placement.addInfluence(x, z, 100 / cellSize, -25); // move farmsteads away to make room (Wall test needed for iber)
else if (template.hasClass("GarrisonFortress") && ent.hasClass("House"))
placement.addInfluence(x, z, 120 / cellSize, -50);
else if (template.hasClass("Military"))
placement.addInfluence(x, z, 40 / cellSize, -40);
else if (template.genericName() == "Rotary Mill" && ent.hasClass("Field"))
placement.addInfluence(x, z, 60 / cellSize, 40);
});
}
if (template.hasClass("Farmstead"))
{
for (let j = 0; j < placement.map.length; ++j)
{
let value = placement.map[j] - gameState.sharedScript.resourceMaps.wood.map[j]/3;
if (HQ.borderMap.map[j] & PETRA.fullBorder_Mask)
value /= 2; // we need space around farmstead, so disfavor map border
placement.set(j, value);
}
}
}
// Requires to be inside our territory, and inside our base territory if required
// and if our first market, put it on border if possible to maximize distance with next Market.
let favorBorder = template.hasClass("Market");
let disfavorBorder = gameState.currentPhase() > 1 && !template.hasDefensiveFire();
let favoredBase = this.metadata && (this.metadata.favoredBase ||
(this.metadata.militaryBase ? HQ.findBestBaseForMilitary(gameState) : undefined));
if (this.metadata && this.metadata.base !== undefined)
{
let base = this.metadata.base;
for (let j = 0; j < placement.map.length; ++j)
{
if (HQ.baseAtIndex(j) != base)
placement.map[j] = 0;
else if (placement.map[j] > 0)
{
if (favorBorder && HQ.borderMap.map[j] & PETRA.border_Mask)
placement.set(j, placement.map[j] + 50);
else if (disfavorBorder && !(HQ.borderMap.map[j] & PETRA.fullBorder_Mask))
placement.set(j, placement.map[j] + 10);
let x = (j % placement.width + 0.5) * cellSize;
let z = (Math.floor(j / placement.width) + 0.5) * cellSize;
if (HQ.isNearInvadingArmy([x, z]))
placement.map[j] = 0;
}
}
}
else
{
for (let j = 0; j < placement.map.length; ++j)
{
if (HQ.baseAtIndex(j) == 0)
placement.map[j] = 0;
else if (placement.map[j] > 0)
{
if (favorBorder && HQ.borderMap.map[j] & PETRA.border_Mask)
placement.set(j, placement.map[j] + 50);
else if (disfavorBorder && !(HQ.borderMap.map[j] & PETRA.fullBorder_Mask))
placement.set(j, placement.map[j] + 10);
let x = (j % placement.width + 0.5) * cellSize;
let z = (Math.floor(j / placement.width) + 0.5) * cellSize;
if (HQ.isNearInvadingArmy([x, z]))
placement.map[j] = 0;
else if (favoredBase && HQ.baseAtIndex(j) == favoredBase)
placement.set(j, placement.map[j] + 100);
}
}
}
// Find the best non-obstructed:
// Find target building's approximate obstruction radius, and expand by a bit to make sure we're not too close,
// this allows room for units to walk between buildings.
// note: not for houses and dropsites who ought to be closer to either each other or a resource.
// also not for fields who can be stacked quite a bit
let obstructions = PETRA.createObstructionMap(gameState, 0, template);
// obstructions.dumpIm(template.buildPlacementType() + "_obstructions.png");
let radius = 0;
if (template.hasClasses(["Fortress", "Arsenal"]) ||
this.type == gameState.applyCiv("structures/{civ}/elephant_stable"))
radius = Math.floor((template.obstructionRadius().max + 8) / obstructions.cellSize);
else if (template.resourceDropsiteTypes() === undefined && !template.hasClasses(["House", "Field", "Market"]))
radius = Math.ceil((template.obstructionRadius().max + 4) / obstructions.cellSize);
else
radius = Math.ceil(template.obstructionRadius().max / obstructions.cellSize);
let bestTile;
if (template.hasClass("House") && !alreadyHasHouses)
{
// try to get some space to place several houses first
bestTile = placement.findBestTile(3*radius, obstructions);
if (!bestTile.val)
bestTile = undefined;
}
if (!bestTile)
bestTile = placement.findBestTile(radius, obstructions);
if (!bestTile.val)
return false;
let bestIdx = bestTile.idx;
let x = (bestIdx % obstructions.width + 0.5) * obstructions.cellSize;
let z = (Math.floor(bestIdx / obstructions.width) + 0.5) * obstructions.cellSize;
let territorypos = placement.gamePosToMapPos([x, z]);
let territoryIndex = territorypos[0] + territorypos[1]*placement.width;
// default angle = 3*Math.PI/4;
return { "x": x, "z": z, "angle": 3*Math.PI/4, "base": HQ.baseAtIndex(territoryIndex) };
};
/**
* Placement of buildings with Dock build category
* metadata.proximity is defined when first dock without any territory
* => we try to minimize distance from our current point
* metadata.oversea is defined for dock in oversea islands
* => we try to maximize distance to our current docks (for trade)
* otherwise standard dock on an island where we already have a cc
* => we try not to be too far from our territory
* In all cases, we add a bonus for nearby resources, and when a large extend of water in front ot it.
*/
PETRA.ConstructionPlan.prototype.findDockPosition = function(gameState)
{
let template = this.template;
let territoryMap = gameState.ai.HQ.territoryMap;
let obstructions = PETRA.createObstructionMap(gameState, 0, template);
// obstructions.dumpIm(template.buildPlacementType() + "_obstructions.png");
let bestIdx;
let bestJdx;
let bestAngle;
let bestLand;
let bestWater;
let bestVal = -1;
let navalPassMap = gameState.ai.accessibility.navalPassMap;
let width = gameState.ai.HQ.territoryMap.width;
let cellSize = gameState.ai.HQ.territoryMap.cellSize;
let nbShips = gameState.ai.HQ.navalManager.transportShips.length;
let wantedLand = this.metadata && this.metadata.land ? this.metadata.land : null;
let wantedSea = this.metadata && this.metadata.sea ? this.metadata.sea : null;
let proxyAccess = this.metadata && this.metadata.proximity ? gameState.ai.accessibility.getAccessValue(this.metadata.proximity) : null;
let oversea = this.metadata && this.metadata.oversea ? this.metadata.oversea : null;
if (nbShips == 0 && proxyAccess && proxyAccess > 1)
{
wantedLand = {};
wantedLand[proxyAccess] = true;
}
let dropsiteTypes = template.resourceDropsiteTypes();
let radius = Math.ceil(template.obstructionRadius().max / obstructions.cellSize);
let halfSize = 0; // used for dock angle
let halfDepth = 0; // used by checkPlacement
let halfWidth = 0; // used by checkPlacement
if (template.get("Footprint/Square"))
{
halfSize = Math.max(+template.get("Footprint/Square/@depth"), +template.get("Footprint/Square/@width")) / 2;
halfDepth = +template.get("Footprint/Square/@depth") / 2;
halfWidth = +template.get("Footprint/Square/@width") / 2;
}
else if (template.get("Footprint/Circle"))
{
halfSize = +template.get("Footprint/Circle/@radius");
halfDepth = halfSize;
halfWidth = halfSize;
}
// res is a measure of the amount of resources around, and maxRes is the max value taken into account
// water is a measure of the water space around, and maxWater is the max value that can be returned by checkDockPlacement
const maxRes = 10;
const maxWater = 16;
let ccEnts = oversea ? gameState.updatingGlobalCollection("allCCs", API3.Filters.byClass("CivCentre")) : null;
let docks = oversea ? gameState.getOwnStructures().filter(API3.Filters.byClass("Dock")) : null;
// Normalisation factors (only guessed, no attempt to optimize them)
let factor = proxyAccess ? 1 : oversea ? 0.2 : 40;
for (let j = 0; j < territoryMap.length; ++j)
{
if (!this.isDockLocation(gameState, j, halfDepth, wantedLand, wantedSea))
continue;
let score = 0;
if (!proxyAccess && !oversea)
{
// if not in our (or allied) territory, we do not want it too far to be able to defend it
score = this.getFrontierProximity(gameState, j);
if (score > 4)
continue;
score *= factor;
}
let i = territoryMap.getNonObstructedTile(j, radius, obstructions);
if (i < 0)
continue;
if (wantedSea && navalPassMap[i] != wantedSea)
continue;
let res = dropsiteTypes ? Math.min(maxRes, this.getResourcesAround(gameState, dropsiteTypes, j, 80)) : maxRes;
let pos = [cellSize * (j%width+0.5), cellSize * (Math.floor(j/width)+0.5)];
// If proximity is given, we look for the nearest point
if (proxyAccess)
score = API3.VectorDistance(this.metadata.proximity, pos);
// Bonus for resources
score += 20 * (maxRes - res);
if (oversea)
{
// Not much farther to one of our cc than to enemy ones
let enemyDist;
let ownDist;
for (let cc of ccEnts.values())
{
let owner = cc.owner();
if (owner != PlayerID && !gameState.isPlayerEnemy(owner))
continue;
let dist = API3.SquareVectorDistance(pos, cc.position());
if (owner == PlayerID && (!ownDist || dist < ownDist))
ownDist = dist;
if (gameState.isPlayerEnemy(owner) && (!enemyDist || dist < enemyDist))
enemyDist = dist;
}
if (ownDist && enemyDist && enemyDist < 0.5 * ownDist)
continue;
// And maximize distance for trade.
let dockDist = 0;
for (let dock of docks.values())
{
if (PETRA.getSeaAccess(gameState, dock) != navalPassMap[i])
continue;
let dist = API3.SquareVectorDistance(pos, dock.position());
if (dist > dockDist)
dockDist = dist;
}
if (dockDist > 0)
{
dockDist = Math.sqrt(dockDist);
if (dockDist > width * cellSize) // Could happen only on square maps, but anyway we don't want to be too far away
continue;
score += factor * (width * cellSize - dockDist);
}
}
// Add a penalty if on the map border as ship movement will be difficult
if (gameState.ai.HQ.borderMap.map[j] & PETRA.fullBorder_Mask)
score += 20;
// Do a pre-selection, supposing we will have the best possible water
if (bestIdx !== undefined && score > bestVal + 5 * maxWater)
continue;
let x = (i % obstructions.width + 0.5) * obstructions.cellSize;
let z = (Math.floor(i / obstructions.width) + 0.5) * obstructions.cellSize;
let angle = this.getDockAngle(gameState, x, z, halfSize);
if (angle == false)
continue;
let ret = this.checkDockPlacement(gameState, x, z, halfDepth, halfWidth, angle);
if (!ret || !gameState.ai.HQ.landRegions[ret.land] || wantedLand && !wantedLand[ret.land])
continue;
// Final selection now that the checkDockPlacement water is known
if (bestIdx !== undefined && score + 5 * (maxWater - ret.water) > bestVal)
continue;
if (this.metadata.proximity && gameState.ai.accessibility.regionSize[ret.land] < 4000)
continue;
if (gameState.ai.HQ.isDangerousLocation(gameState, pos, halfSize))
continue;
bestVal = score + maxWater - ret.water;
bestIdx = i;
bestJdx = j;
bestAngle = angle;
bestLand = ret.land;
bestWater = ret.water;
}
if (bestVal < 0)
return false;
// if no good place with enough water around and still in first phase, wait for expansion at the next phase
if (!this.metadata.proximity && bestWater < 10 && gameState.currentPhase() == 1)
return false;
let x = (bestIdx % obstructions.width + 0.5) * obstructions.cellSize;
let z = (Math.floor(bestIdx / obstructions.width) + 0.5) * obstructions.cellSize;
// Assign this dock to a base
let baseIndex = gameState.ai.HQ.baseAtIndex(bestJdx);
if (!baseIndex)
baseIndex = -2; // We'll do an anchorless base around it
return { "x": x, "z": z, "angle": bestAngle, "base": baseIndex, "access": bestLand };
};
/**
* Find a good island to build a dock.
*/
PETRA.ConstructionPlan.prototype.buildOverseaDock = function(gameState, template)
{
let docks = gameState.getOwnStructures().filter(API3.Filters.byClass("Dock"));
if (!docks.hasEntities())
return;
let passabilityMap = gameState.getPassabilityMap();
let cellArea = passabilityMap.cellSize * passabilityMap.cellSize;
let ccEnts = gameState.updatingGlobalCollection("allCCs", API3.Filters.byClass("CivCentre"));
let land = {};
let found;
for (let i = 0; i < gameState.ai.accessibility.regionSize.length; ++i)
{
if (gameState.ai.accessibility.regionType[i] != "land" ||
cellArea * gameState.ai.accessibility.regionSize[i] < 3600)
continue;
let keep = true;
for (let dock of docks.values())
{
if (PETRA.getLandAccess(gameState, dock) != i)
continue;
keep = false;
break;
}
if (!keep)
continue;
let sea;
for (let cc of ccEnts.values())
{
let ccAccess = PETRA.getLandAccess(gameState, cc);
if (ccAccess != i)
{
if (cc.owner() == PlayerID && !sea)
sea = gameState.ai.HQ.getSeaBetweenIndices(gameState, ccAccess, i);
continue;
}
// Docks on island where we have a cc are already done elsewhere
if (cc.owner() == PlayerID || gameState.isPlayerEnemy(cc.owner()))
{
keep = false;
break;
}
}
if (!keep || !sea)
continue;
land[i] = true;
found = true;
}
if (!found)
return;
if (!gameState.ai.HQ.navalMap)
API3.warn("petra.findOverseaLand on a non-naval map??? we should never go there ");
let oldTemplate = this.template;
let oldMetadata = this.metadata;
this.template = template;
let pos;
this.metadata = { "land": land, "oversea": true };
pos = this.findDockPosition(gameState);
if (pos)
{
let type = template.templateName();
let builder = gameState.findBuilder(type);
this.metadata.base = pos.base;
// Adjust a bit the position if needed
let cosa = Math.cos(pos.angle);
let sina = Math.sin(pos.angle);
let shiftMax = gameState.ai.HQ.territoryMap.cellSize;
for (let shift = 0; shift <= shiftMax; shift += 2)
{
builder.construct(type, pos.x-shift*sina, pos.z-shift*cosa, pos.angle, this.metadata);
if (shift > 0)
builder.construct(type, pos.x+shift*sina, pos.z+shift*cosa, pos.angle, this.metadata);
}
}
this.template = oldTemplate;
this.metadata = oldMetadata;
};
/** Algorithm taken from the function GetDockAngle in simulation/helpers/Commands.js */
PETRA.ConstructionPlan.prototype.getDockAngle = function(gameState, x, z, size)
{
let pos = gameState.ai.accessibility.gamePosToMapPos([x, z]);
let k = pos[0] + pos[1]*gameState.ai.accessibility.width;
let seaRef = gameState.ai.accessibility.navalPassMap[k];
if (seaRef < 2)
return false;
const numPoints = 16;
for (let dist = 0; dist < 4; ++dist)
{
let waterPoints = [];
for (let i = 0; i < numPoints; ++i)
{
let angle = 2 * Math.PI * i / numPoints;
pos = [x - (1+dist)*size*Math.sin(angle), z + (1+dist)*size*Math.cos(angle)];
pos = gameState.ai.accessibility.gamePosToMapPos(pos);
if (pos[0] < 0 || pos[0] >= gameState.ai.accessibility.width ||
pos[1] < 0 || pos[1] >= gameState.ai.accessibility.height)
continue;
let j = pos[0] + pos[1]*gameState.ai.accessibility.width;
if (gameState.ai.accessibility.navalPassMap[j] == seaRef)
waterPoints.push(i);
}
let length = waterPoints.length;
if (!length)
continue;
let consec = [];
for (let i = 0; i < length; ++i)
{
let 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;
}
let start = 0;
let count = 0;
for (let 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 false;
};
/**
* Algorithm taken from checkPlacement in simulation/components/BuildRestriction.js
* to determine the special dock requirements
* returns {"land": land index for this dock, "water": amount of water around this spot}
*/
PETRA.ConstructionPlan.prototype.checkDockPlacement = function(gameState, x, z, halfDepth, halfWidth, angle)
{
let sz = halfDepth * Math.sin(angle);
let cz = halfDepth * Math.cos(angle);
// center back position
let pos = gameState.ai.accessibility.gamePosToMapPos([x - sz, z - cz]);
let j = pos[0] + pos[1]*gameState.ai.accessibility.width;
let land = gameState.ai.accessibility.landPassMap[j];
if (land < 2)
return null;
// center front position
pos = gameState.ai.accessibility.gamePosToMapPos([x + sz, z + cz]);
j = pos[0] + pos[1]*gameState.ai.accessibility.width;
if (gameState.ai.accessibility.landPassMap[j] > 1 || gameState.ai.accessibility.navalPassMap[j] < 2)
return null;
// additional constraints compared to BuildRestriction.js to assure we have enough place to build
let sw = halfWidth * Math.cos(angle) * 3 / 4;
let cw = halfWidth * Math.sin(angle) * 3 / 4;
pos = gameState.ai.accessibility.gamePosToMapPos([x - sz + sw, z - cz - cw]);
j = pos[0] + pos[1]*gameState.ai.accessibility.width;
if (gameState.ai.accessibility.landPassMap[j] != land)
return null;
pos = gameState.ai.accessibility.gamePosToMapPos([x - sz - sw, z - cz + cw]);
j = pos[0] + pos[1]*gameState.ai.accessibility.width;
if (gameState.ai.accessibility.landPassMap[j] != land)
return null;
let water = 0;
let sp = 15 * Math.sin(angle);
let cp = 15 * Math.cos(angle);
for (let i = 1; i < 5; ++i)
{
pos = gameState.ai.accessibility.gamePosToMapPos([x + sz + i*(sp+sw), z + cz + i*(cp-cw)]);
if (pos[0] < 0 || pos[0] >= gameState.ai.accessibility.width ||
pos[1] < 0 || pos[1] >= gameState.ai.accessibility.height)
break;
j = pos[0] + pos[1]*gameState.ai.accessibility.width;
if (gameState.ai.accessibility.landPassMap[j] > 1 || gameState.ai.accessibility.navalPassMap[j] < 2)
break;
pos = gameState.ai.accessibility.gamePosToMapPos([x + sz + i*sp, z + cz + i*cp]);
if (pos[0] < 0 || pos[0] >= gameState.ai.accessibility.width ||
pos[1] < 0 || pos[1] >= gameState.ai.accessibility.height)
break;
j = pos[0] + pos[1]*gameState.ai.accessibility.width;
if (gameState.ai.accessibility.landPassMap[j] > 1 || gameState.ai.accessibility.navalPassMap[j] < 2)
break;
pos = gameState.ai.accessibility.gamePosToMapPos([x + sz + i*(sp-sw), z + cz + i*(cp+cw)]);
if (pos[0] < 0 || pos[0] >= gameState.ai.accessibility.width ||
pos[1] < 0 || pos[1] >= gameState.ai.accessibility.height)
break;
j = pos[0] + pos[1]*gameState.ai.accessibility.width;
if (gameState.ai.accessibility.landPassMap[j] > 1 || gameState.ai.accessibility.navalPassMap[j] < 2)
break;
water += 4;
}
return { "land": land, "water": water };
};
/**
* fast check if we can build a dock: returns false if nearest land is farther than the dock dimension
* if the (object) wantedLand is given, this nearest land should have one of these accessibility
* if wantedSea is given, this tile should be inside this sea
*/
PETRA.ConstructionPlan.prototype.around = [[ 1.0, 0.0], [ 0.87, 0.50], [ 0.50, 0.87], [ 0.0, 1.0], [-0.50, 0.87], [-0.87, 0.50],
[-1.0, 0.0], [-0.87, -0.50], [-0.50, -0.87], [ 0.0, -1.0], [ 0.50, -0.87], [ 0.87, -0.50]];
PETRA.ConstructionPlan.prototype.isDockLocation = function(gameState, j, dimension, wantedLand, wantedSea)
{
let width = gameState.ai.HQ.territoryMap.width;
let cellSize = gameState.ai.HQ.territoryMap.cellSize;
let dimLand = dimension + 1.5 * cellSize;
let dimSea = dimension + 2 * cellSize;
let accessibility = gameState.ai.accessibility;
let x = (j%width + 0.5) * cellSize;
let z = (Math.floor(j/width) + 0.5) * cellSize;
let pos = accessibility.gamePosToMapPos([x, z]);
let k = pos[0] + pos[1]*accessibility.width;
let landPass = accessibility.landPassMap[k];
if (landPass > 1 && wantedLand && !wantedLand[landPass] ||
landPass < 2 && accessibility.navalPassMap[k] < 2)
return false;
for (let a of this.around)
{
pos = accessibility.gamePosToMapPos([x + dimLand*a[0], z + dimLand*a[1]]);
if (pos[0] < 0 || pos[0] >= accessibility.width)
continue;
if (pos[1] < 0 || pos[1] >= accessibility.height)
continue;
k = pos[0] + pos[1]*accessibility.width;
landPass = accessibility.landPassMap[k];
if (landPass < 2 || wantedLand && !wantedLand[landPass])
continue;
pos = accessibility.gamePosToMapPos([x - dimSea*a[0], z - dimSea*a[1]]);
if (pos[0] < 0 || pos[0] >= accessibility.width)
continue;
if (pos[1] < 0 || pos[1] >= accessibility.height)
continue;
k = pos[0] + pos[1]*accessibility.width;
if (wantedSea && accessibility.navalPassMap[k] != wantedSea ||
!wantedSea && accessibility.navalPassMap[k] < 2)
continue;
return true;
}
return false;
};
/**
* return a measure of the proximity to our frontier (including our allies)
* 0=inside, 1=less than 24m, 2= less than 48m, 3= less than 72m, 4=less than 96m, 5=above 96m
*/
PETRA.ConstructionPlan.prototype.getFrontierProximity = function(gameState, j)
{
let alliedVictory = gameState.getAlliedVictory();
let territoryMap = gameState.ai.HQ.territoryMap;
let territoryOwner = territoryMap.getOwnerIndex(j);
if (territoryOwner == PlayerID || alliedVictory && gameState.isPlayerAlly(territoryOwner))
return 0;
let borderMap = gameState.ai.HQ.borderMap;
let width = territoryMap.width;
let step = Math.round(24 / territoryMap.cellSize);
let ix = j % width;
let iz = Math.floor(j / width);
let best = 5;
for (let a of this.around)
{
for (let i = 1; i < 5; ++i)
{
let jx = ix + Math.round(i*step*a[0]);
if (jx < 0 || jx >= width)
continue;
let jz = iz + Math.round(i*step*a[1]);
if (jz < 0 || jz >= width)
continue;
if (borderMap.map[jx+width*jz] & PETRA.outside_Mask)
continue;
territoryOwner = territoryMap.getOwnerIndex(jx+width*jz);
if (alliedVictory && gameState.isPlayerAlly(territoryOwner) || territoryOwner == PlayerID)
{
best = Math.min(best, i);
break;
}
}
if (best == 1)
break;
}
return best;
};
/**
* get the sum of the resources (except food) around, inside a given radius
* resources have a weight (1 if dist=0 and 0 if dist=size) doubled for wood
*/
PETRA.ConstructionPlan.prototype.getResourcesAround = function(gameState, types, i, radius)
{
let resourceMaps = gameState.sharedScript.resourceMaps;
let w = resourceMaps.wood.width;
let cellSize = resourceMaps.wood.cellSize;
let size = Math.floor(radius / cellSize);
let ix = i % w;
let iy = Math.floor(i / w);
let total = 0;
let nbcell = 0;
for (let k of types)
{
if (k == "food" || !resourceMaps[k])
continue;
let weigh0 = k == "wood" ? 2 : 1;
for (let dy = 0; dy <= size; ++dy)
{
let dxmax = size - dy;
let ky = iy + dy;
if (ky >= 0 && ky < w)
{
for (let dx = -dxmax; dx <= dxmax; ++dx)
{
let kx = ix + dx;
if (kx < 0 || kx >= w)
continue;
let ddx = dx > 0 ? dx : -dx;
let weight = weigh0 * (dxmax - ddx) / size;
total += weight * resourceMaps[k].map[kx + w * ky];
nbcell += weight;
}
}
if (dy == 0)
continue;
ky = iy - dy;
if (ky >= 0 && ky < w)
{
for (let dx = -dxmax; dx <= dxmax; ++dx)
{
let kx = ix + dx;
if (kx < 0 || kx >= w)
continue;
let ddx = dx > 0 ? dx : -dx;
let weight = weigh0 * (dxmax - ddx) / size;
total += weight * resourceMaps[k].map[kx + w * ky];
nbcell += weight;
}
}
}
}
return nbcell ? total / nbcell : 0;
};
PETRA.ConstructionPlan.prototype.isGo = function(gameState)
{
if (this.goRequirement && this.goRequirement == "houseNeeded")
{
if (!gameState.ai.HQ.canBuild(gameState, "structures/{civ}/house") &&
!gameState.ai.HQ.canBuild(gameState, "structures/{civ}/apartment"))
return false;
if (gameState.getPopulationMax() <= gameState.getPopulationLimit())
return false;
let freeSlots = gameState.getPopulationLimit() - gameState.getPopulation();
for (let ent of gameState.getOwnFoundations().values())
{
let template = gameState.getBuiltTemplate(ent.templateName());
if (template)
freeSlots += template.getPopulationBonus();
}
if (gameState.ai.HQ.saveResources)
return freeSlots <= 10;
if (gameState.getPopulation() > 55)
return freeSlots <= 21;
if (gameState.getPopulation() > 30)
return freeSlots <= 15;
return freeSlots <= 10;
}
return true;
};
PETRA.ConstructionPlan.prototype.onStart = function(gameState)
{
if (this.queueToReset)
gameState.ai.queueManager.changePriority(this.queueToReset, gameState.ai.Config.priorities[this.queueToReset]);
};
PETRA.ConstructionPlan.prototype.Serialize = function()
{
return {
"category": this.category,
"type": this.type,
"ID": this.ID,
"metadata": this.metadata,
"cost": this.cost.Serialize(),
"number": this.number,
"position": this.position,
"goRequirement": this.goRequirement || undefined,
"queueToReset": this.queueToReset || undefined
};
};
PETRA.ConstructionPlan.prototype.Deserialize = function(gameState, data)
{
for (let key in data)
this[key] = data[key];
this.cost = new API3.Resources();
this.cost.Deserialize(data.cost);
};
Index: ps/trunk/binaries/data/mods/public/simulation/components/GuiInterface.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/components/GuiInterface.js (revision 27244)
+++ ps/trunk/binaries/data/mods/public/simulation/components/GuiInterface.js (revision 27245)
@@ -1,2140 +1,2134 @@
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)
{
const playerEnt = cmpPlayerManager.GetPlayerByID(i);
const cmpPlayer = Engine.QueryInterface(playerEnt, IID_Player);
const cmpPlayerEntityLimits = Engine.QueryInterface(playerEnt, IID_EntityLimits);
const cmpIdentity = Engine.QueryInterface(playerEnt, IID_Identity);
// Work out which phase we are in.
let phase = "";
const cmpTechnologyManager = Engine.QueryInterface(playerEnt, 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": cmpIdentity.GetName(),
"civ": cmpIdentity.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,
"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)
{
if (!ent)
return null;
// All units must have a template; if not then it's a nonexistent entity id.
const template = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager).GetCurrentTemplateName(ent);
if (!template)
return null;
const ret = {
"id": ent,
"player": INVALID_PLAYER,
"template": template
};
const cmpAuras = Engine.QueryInterface(ent, IID_Auras);
if (cmpAuras)
ret.auras = cmpAuras.GetDescriptions();
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(),
"rankTechName": cmpIdentity.GetRankTechName(),
"classes": cmpIdentity.GetClassesList(),
"selectionGroupName": cmpIdentity.GetSelectionGroupName(),
"canDelete": !cmpIdentity.IsUndeletable(),
"controllable": cmpIdentity.IsControllable()
};
const cmpFormation = Engine.QueryInterface(ent, IID_Formation);
if (cmpFormation)
ret.formation = {
"members": cmpFormation.GetMembers()
};
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()
};
const cmpResearcher = Engine.QueryInterface(ent, IID_Researcher);
if (cmpResearcher)
ret.researcher = {
"technologies": cmpResearcher.GetTechnologiesList(),
"techCostMultiplier": cmpResearcher.GetTechCostMultiplier()
};
let cmpStatusEffects = Engine.QueryInterface(ent, IID_StatusEffectsReceiver);
if (cmpStatusEffects)
ret.statusEffects = cmpStatusEffects.GetActiveStatuses();
let cmpProductionQueue = Engine.QueryInterface(ent, IID_ProductionQueue);
if (cmpProductionQueue)
ret.production = {
"queue": cmpProductionQueue.GetQueue(),
"autoqueue": cmpProductionQueue.IsAutoQueueing()
};
const cmpTrainer = Engine.QueryInterface(ent, IID_Trainer);
if (cmpTrainer)
ret.trainer = {
"entities": cmpTrainer.GetEntitiesList()
};
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 cmpTurretable = Engine.QueryInterface(ent, IID_Turretable);
if (cmpTurretable)
ret.turretable = {
"ejectable": cmpTurretable.IsEjectable(),
"holder": cmpTurretable.HolderID()
};
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(),
"formations": cmpUnitAI.GetFormationsList(),
"formation": cmpUnitAI.GetFormationController()
};
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;
ret.attack[type].yOrigin = cmpAttack.GetAttackYOrigin(type);
let timers = cmpAttack.GetTimers(type);
ret.attack[type].prepareTime = timers.prepare;
ret.attack[type].repeatTime = timers.repeat;
if (type != "Ranged")
{
ret.attack[type].elevationAdaptedRange = ret.attack.maxRange;
continue;
}
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, ret.attack[type].yOrigin, cmpUnitAI ? 0 : 2 * Math.PI);
else
// Not in world, set a default?
ret.attack[type].elevationAdaptedRange = ret.attack.maxRange;
}
}
let cmpResistance = QueryMiragedInterface(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 cmpTreasureCollector = Engine.QueryInterface(ent, IID_TreasureCollector);
if (cmpTreasureCollector)
ret.treasureCollector = true;
let cmpUnitMotion = Engine.QueryInterface(ent, IID_UnitMotion);
if (cmpUnitMotion)
ret.speed = {
"walk": cmpUnitMotion.GetWalkSpeed(),
"run": cmpUnitMotion.GetWalkSpeed() * cmpUnitMotion.GetRunMultiplier(),
"acceleration": cmpUnitMotion.GetAcceleration()
};
let cmpUpkeep = Engine.QueryInterface(ent, IID_Upkeep);
if (cmpUpkeep)
ret.upkeep = {
"interval": cmpUpkeep.GetInterval(),
"rates": cmpUpkeep.GetRates()
};
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
};
const yOrigin = cmd.yOrigin || 0;
let range = cmd.range;
return cmpRangeManager.GetElevationAdaptedRange(pos, rot, range, yOrigin, 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, Resources);
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, Resources);
};
-GuiInterface.prototype.IsTechnologyResearched = function(player, data)
+GuiInterface.prototype.AreRequirementsMet = 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);
+ return !data.requirements || RequirementsHelper.AreRequirementsMet(data.requirements,
+ data.player !== undefined ? data.player : player);
};
/**
* 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)
{
return QueryPlayerIDInterface(player, IID_TechnologyManager)?.GetBasicInfoOfStartedTechs() || {};
};
/**
* 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.Identity.GenericName,
"tooltip": template.Formation.DisabledTooltip || "",
"icon": template.Identity.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 cmpOwnership = Engine.QueryInterface(ent, IID_Ownership);
if (cmpOwnership)
cmpOwnership.SetOwner(cmd.owner);
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.PlaySoundForPlayer = function(player, data)
{
let playerEntityID = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager).GetPlayerByID(player);
let cmpSound = Engine.QueryInterface(playerEntityID, IID_Sound);
if (!cmpSound)
return;
let soundGroup = cmpSound.GetSoundGroup(data.name);
if (soundGroup)
Engine.QueryInterface(SYSTEM_ENTITY, IID_SoundManager).PlaySoundGroupForPlayer(soundGroup, player);
};
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())
return { "idle": false };
let cmpGarrisonable = Engine.QueryInterface(unit, IID_Garrisonable);
if (cmpGarrisonable && cmpGarrisonable.IsGarrisoned())
return { "idle": false };
const cmpTurretable = Engine.QueryInterface(unit, IID_Turretable);
if (cmpTurretable && cmpTurretable.IsTurreted())
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;
let cmpMarket = QueryMiragedInterface(data.firstMarket, IID_Market);
return cmpMarket && cmpMarket.CalculateTraderGain(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)
{
return Engine.QueryInterface(data.entity, IID_Trainer)?.GetBatchTime(data.batchSize) || 0;
};
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,
+ "AreRequirementsMet": 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,
"PlaySoundForPlayer": 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/Identity.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/components/Identity.js (revision 27244)
+++ ps/trunk/binaries/data/mods/public/simulation/components/Identity.js (revision 27245)
@@ -1,223 +1,221 @@
function Identity() {}
Identity.prototype.Schema =
"Specifies various names and values associated with the entity, typically for GUI display to users." +
"" +
"athen" +
"Athenian Hoplite" +
"Hoplī́tēs Athēnaïkós" +
"units/athen_infantry_spearman.png" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"tokens" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"Basic" +
"Advanced" +
"Elite" +
"" +
"" +
"" +
"" +
"" +
"" +
"tokens" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"tokens" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
- "" +
- "" +
- "" +
+ RequirementsHelper.BuildSchema() +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"";
Identity.prototype.Init = function()
{
this.classesList = GetIdentityClasses(this.template);
this.visibleClassesList = GetVisibleIdentityClasses(this.template);
if (this.template.Phenotype)
this.phenotype = pickRandom(this.GetPossiblePhenotypes());
else
this.phenotype = "default";
this.controllable = this.template.Controllable ? this.template.Controllable == "true" : true;
};
Identity.prototype.GetCiv = function()
{
return this.template.Civ;
};
Identity.prototype.GetLang = function()
{
return this.template.Lang || "greek"; // ugly default
};
/**
* Get a list of possible Phenotypes.
* @return {string[]} A list of possible phenotypes.
*/
Identity.prototype.GetPossiblePhenotypes = function()
{
return this.template.Phenotype._string.split(/\s+/);
};
/**
* Get the current Phenotype.
* @return {string} The current phenotype.
*/
Identity.prototype.GetPhenotype = function()
{
return this.phenotype;
};
Identity.prototype.GetRank = function()
{
return this.template.Rank || "";
};
Identity.prototype.GetRankTechName = function()
{
return this.template.Rank ? "unit_" + this.template.Rank.toLowerCase() : "";
};
Identity.prototype.GetClassesList = function()
{
return this.classesList;
};
Identity.prototype.GetVisibleClassesList = function()
{
return this.visibleClassesList;
};
Identity.prototype.HasClass = function(name)
{
return this.GetClassesList().indexOf(name) != -1;
};
Identity.prototype.GetSelectionGroupName = function()
{
return this.template.SelectionGroupName || "";
};
Identity.prototype.GetGenericName = function()
{
return this.template.GenericName;
};
Identity.prototype.IsUndeletable = function()
{
return this.template.Undeletable == "true";
};
Identity.prototype.IsControllable = function()
{
return this.controllable;
};
Identity.prototype.SetControllable = function(controllability)
{
this.controllable = controllability;
};
Identity.prototype.SetPhenotype = function(phenotype)
{
this.phenotype = phenotype;
};
/**
* @param {string} newName -
*/
Identity.prototype.SetName = function(newName)
{
this.name = newName;
};
/**
* @return {string} -
*/
Identity.prototype.GetName = function()
{
return this.name || this.template.GenericName;
};
function IdentityMirage() {}
IdentityMirage.prototype.Init = function(cmpIdentity)
{
// Mirages don't get identity classes via the template-filter, so that code can query
// identity components via Engine.QueryInterface without having to explicitly check for mirages.
// This is cloned as otherwise we get a reference to Identity's property,
// and that array is deleted when serializing (as it's not seralized), which ends in OOS.
this.classes = clone(cmpIdentity.GetClassesList());
};
IdentityMirage.prototype.GetClassesList = function() { return this.classes; };
Engine.RegisterGlobal("IdentityMirage", IdentityMirage);
Identity.prototype.Mirage = function()
{
let mirage = new IdentityMirage();
mirage.Init(this);
return mirage;
};
Engine.RegisterComponentType(IID_Identity, "Identity", Identity);
Index: ps/trunk/binaries/data/mods/public/simulation/components/TechnologyManager.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/components/TechnologyManager.js (revision 27244)
+++ ps/trunk/binaries/data/mods/public/simulation/components/TechnologyManager.js (revision 27245)
@@ -1,552 +1,552 @@
function TechnologyManager() {}
TechnologyManager.prototype.Schema =
"";
/**
* This object represents a technology under research.
* @param {string} templateName - The name of the template to research.
* @param {number} player - The player ID researching.
* @param {number} researcher - The entity ID researching.
*/
TechnologyManager.prototype.Technology = function(templateName, player, researcher)
{
this.player = player;
this.researcher = researcher;
this.templateName = templateName;
};
/**
* Prepare for the queue.
* @param {Object} techCostMultiplier - The multipliers to use when calculating costs.
* @return {boolean} - Whether the technology was successfully initiated.
*/
TechnologyManager.prototype.Technology.prototype.Queue = function(techCostMultiplier)
{
const template = TechnologyTemplates.Get(this.templateName);
if (!template)
return false;
this.resources = {};
if (template.cost)
for (const res in template.cost)
this.resources[res] = Math.floor(techCostMultiplier[res] * template.cost[res]);
// ToDo: Subtract resources here or in cmpResearcher?
const cmpPlayer = Engine.QueryInterface(this.player, IID_Player);
// TrySubtractResources should report error to player (they ran out of resources).
if (!cmpPlayer?.TrySubtractResources(this.resources))
return false;
const time = techCostMultiplier.time * (template.researchTime || 0) * 1000;
this.timeRemaining = time;
this.timeTotal = time;
const playerID = cmpPlayer.GetPlayerID();
Engine.QueryInterface(SYSTEM_ENTITY, IID_Trigger).CallEvent("OnResearchQueued", {
"playerid": playerID,
"technologyTemplate": this.templateName,
"researcherEntity": this.researcher
});
return true;
};
TechnologyManager.prototype.Technology.prototype.Stop = function()
{
const cmpPlayer = Engine.QueryInterface(this.player, IID_Player);
cmpPlayer?.RefundResources(this.resources);
delete this.resources;
if (this.started && this.templateName.startsWith("phase"))
Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface).PushNotification({
"type": "phase",
"players": [cmpPlayer.GetPlayerID()],
"phaseName": this.templateName,
"phaseState": "aborted"
});
};
/**
* Called when the first work is performed.
*/
TechnologyManager.prototype.Technology.prototype.Start = function()
{
this.started = true;
if (!this.templateName.startsWith("phase"))
return;
const cmpPlayer = Engine.QueryInterface(this.player, IID_Player);
Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface).PushNotification({
"type": "phase",
"players": [cmpPlayer.GetPlayerID()],
"phaseName": this.templateName,
"phaseState": "started"
});
};
TechnologyManager.prototype.Technology.prototype.Finish = function()
{
this.finished = true;
const template = TechnologyTemplates.Get(this.templateName);
if (template.soundComplete)
Engine.QueryInterface(SYSTEM_ENTITY, IID_SoundManager)?.PlaySoundGroup(template.soundComplete, this.researcher);
if (template.modifications)
{
const cmpModifiersManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ModifiersManager);
cmpModifiersManager.AddModifiers("tech/" + this.templateName, DeriveModificationsFromTech(template), this.player);
}
const cmpEntityLimits = Engine.QueryInterface(this.player, IID_EntityLimits);
const cmpTechnologyManager = Engine.QueryInterface(this.player, IID_TechnologyManager);
if (template.replaces && template.replaces.length > 0)
for (const i of template.replaces)
{
cmpTechnologyManager.MarkTechnologyAsResearched(i);
cmpEntityLimits?.UpdateLimitsFromTech(i);
}
cmpTechnologyManager.MarkTechnologyAsResearched(this.templateName);
// ToDo: Move to EntityLimits.js.
cmpEntityLimits?.UpdateLimitsFromTech(this.templateName);
const playerID = Engine.QueryInterface(this.player, IID_Player).GetPlayerID();
Engine.PostMessage(this.player, MT_ResearchFinished, { "player": playerID, "tech": this.templateName });
if (this.templateName.startsWith("phase") && !template.autoResearch)
Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface).PushNotification({
"type": "phase",
"players": [playerID],
"phaseName": this.templateName,
"phaseState": "completed"
});
};
/**
* @param {number} allocatedTime - The time allocated to this item.
* @return {number} - The time used for this item.
*/
TechnologyManager.prototype.Technology.prototype.Progress = function(allocatedTime)
{
if (!this.started)
this.Start();
if (this.paused)
this.Unpause();
if (this.timeRemaining > allocatedTime)
{
this.timeRemaining -= allocatedTime;
return allocatedTime;
}
this.Finish();
return this.timeRemaining;
};
TechnologyManager.prototype.Technology.prototype.Pause = function()
{
this.paused = true;
};
TechnologyManager.prototype.Technology.prototype.Unpause = function()
{
delete this.paused;
};
TechnologyManager.prototype.Technology.prototype.GetBasicInfo = function()
{
return {
"paused": this.paused,
"progress": 1 - (this.timeRemaining / (this.timeTotal || 1)),
"researcher": this.researcher,
"templateName": this.templateName,
"timeRemaining": this.timeRemaining
};
};
TechnologyManager.prototype.Technology.prototype.SerializableAttributes = [
"paused",
"player",
"researcher",
"resources",
"started",
"templateName",
"timeRemaining",
"timeTotal"
];
TechnologyManager.prototype.Technology.prototype.Serialize = function()
{
const result = {};
for (const att of this.SerializableAttributes)
if (this.hasOwnProperty(att))
result[att] = this[att];
return result;
};
TechnologyManager.prototype.Technology.prototype.Deserialize = function(data)
{
for (const att of this.SerializableAttributes)
if (att in data)
this[att] = data[att];
};
TechnologyManager.prototype.Init = function()
{
// Holds names of technologies that have been researched.
this.researchedTechs = new Set();
// Maps from technolgy name to the technology object.
this.researchQueued = new Map();
this.classCounts = {}; // stores the number of entities of each Class
this.typeCountsByClass = {}; // stores the number of entities of each type for each class i.e.
// {"someClass": {"unit/spearman": 2, "unit/cav": 5} "someOtherClass":...}
// Some technologies are automatically researched when their conditions are met. They have no cost and are
// researched instantly. This allows civ bonuses and more complicated technologies.
this.unresearchedAutoResearchTechs = new Set();
let allTechs = TechnologyTemplates.GetAll();
for (let key in allTechs)
if (allTechs[key].autoResearch || allTechs[key].top)
this.unresearchedAutoResearchTechs.add(key);
};
TechnologyManager.prototype.SerializableAttributes = [
"researchedTechs",
"classCounts",
"typeCountsByClass",
"unresearchedAutoResearchTechs"
];
TechnologyManager.prototype.Serialize = function()
{
const result = {};
for (const att of this.SerializableAttributes)
if (this.hasOwnProperty(att))
result[att] = this[att];
result.researchQueued = [];
for (const [techName, techObject] of this.researchQueued)
result.researchQueued.push(techObject.Serialize());
return result;
};
TechnologyManager.prototype.Deserialize = function(data)
{
for (const att of this.SerializableAttributes)
if (att in data)
this[att] = data[att];
this.researchQueued = new Map();
for (const tech of data.researchQueued)
{
const newTech = new this.Technology();
newTech.Deserialize(tech);
this.researchQueued.set(tech.templateName, newTech);
}
};
TechnologyManager.prototype.OnUpdate = function()
{
this.UpdateAutoResearch();
};
// This function checks if the requirements of any autoresearch techs are met and if they are it researches them
TechnologyManager.prototype.UpdateAutoResearch = function()
{
for (let key of this.unresearchedAutoResearchTechs)
{
let tech = TechnologyTemplates.Get(key);
if ((tech.autoResearch && this.CanResearch(key)) ||
(tech.top && (this.IsTechnologyResearched(tech.top) || this.IsTechnologyResearched(tech.bottom))))
{
this.unresearchedAutoResearchTechs.delete(key);
this.ResearchTechnology(key);
return; // We will have recursively handled any knock-on effects so can just return
}
}
};
// Checks an entity template to see if its technology requirements have been met
TechnologyManager.prototype.CanProduce = function(templateName)
{
var cmpTempManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager);
var template = cmpTempManager.GetTemplate(templateName);
- if (template.Identity && template.Identity.RequiredTechnology)
- return this.IsTechnologyResearched(template.Identity.RequiredTechnology);
+ if (template.Identity?.Requirements)
+ return RequirementsHelper.AreRequirementsMet(template.Identity.Requirements, Engine.QueryInterface(this.entity, IID_Player).GetPlayerID());
// If there is no required technology then this entity can be produced
return true;
};
TechnologyManager.prototype.IsTechnologyQueued = function(tech)
{
return this.researchQueued.has(tech);
};
TechnologyManager.prototype.IsTechnologyResearched = function(tech)
{
return this.researchedTechs.has(tech);
};
// Checks the requirements for a technology to see if it can be researched at the current time
TechnologyManager.prototype.CanResearch = function(tech)
{
let template = TechnologyTemplates.Get(tech);
if (!template)
{
warn("Technology \"" + tech + "\" does not exist");
return false;
}
if (template.top && this.IsInProgress(template.top) ||
template.bottom && this.IsInProgress(template.bottom))
return false;
if (template.pair && !this.CanResearch(template.pair))
return false;
if (this.IsInProgress(tech))
return false;
if (this.IsTechnologyResearched(tech))
return false;
return this.CheckTechnologyRequirements(DeriveTechnologyRequirements(template, Engine.QueryInterface(this.entity, IID_Identity).GetCiv()));
};
/**
* Private function for checking a set of requirements is met
* @param {Object} reqs - Technology requirements as derived from the technology template by globalscripts
* @param {boolean} civonly - True if only the civ requirement is to be checked
*
* @return true if the requirements pass, false otherwise
*/
TechnologyManager.prototype.CheckTechnologyRequirements = function(reqs, civonly = false)
{
let cmpPlayer = Engine.QueryInterface(this.entity, IID_Player);
if (!reqs)
return false;
if (civonly || !reqs.length)
return true;
return reqs.some(req => {
return Object.keys(req).every(type => {
switch (type)
{
case "techs":
return req[type].every(this.IsTechnologyResearched, this);
case "entities":
return req[type].every(this.DoesEntitySpecPass, this);
}
return false;
});
});
};
TechnologyManager.prototype.DoesEntitySpecPass = function(entity)
{
switch (entity.check)
{
case "count":
if (!this.classCounts[entity.class] || this.classCounts[entity.class] < entity.number)
return false;
break;
case "variants":
if (!this.typeCountsByClass[entity.class] || Object.keys(this.typeCountsByClass[entity.class]).length < entity.number)
return false;
break;
}
return true;
};
TechnologyManager.prototype.OnGlobalOwnershipChanged = function(msg)
{
// This automatically updates classCounts and typeCountsByClass
var playerID = (Engine.QueryInterface(this.entity, IID_Player)).GetPlayerID();
if (msg.to == playerID)
{
var cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager);
var template = cmpTemplateManager.GetCurrentTemplateName(msg.entity);
var cmpIdentity = Engine.QueryInterface(msg.entity, IID_Identity);
if (!cmpIdentity)
return;
var classes = cmpIdentity.GetClassesList();
// don't use foundations for the class counts but check if techs apply (e.g. health increase)
if (!Engine.QueryInterface(msg.entity, IID_Foundation))
{
for (let cls of classes)
{
this.classCounts[cls] = this.classCounts[cls] || 0;
this.classCounts[cls] += 1;
this.typeCountsByClass[cls] = this.typeCountsByClass[cls] || {};
this.typeCountsByClass[cls][template] = this.typeCountsByClass[cls][template] || 0;
this.typeCountsByClass[cls][template] += 1;
}
}
}
if (msg.from == playerID)
{
var cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager);
var template = cmpTemplateManager.GetCurrentTemplateName(msg.entity);
// don't use foundations for the class counts
if (!Engine.QueryInterface(msg.entity, IID_Foundation))
{
var cmpIdentity = Engine.QueryInterface(msg.entity, IID_Identity);
if (cmpIdentity)
{
var classes = cmpIdentity.GetClassesList();
for (let cls of classes)
{
this.classCounts[cls] -= 1;
if (this.classCounts[cls] <= 0)
delete this.classCounts[cls];
this.typeCountsByClass[cls][template] -= 1;
if (this.typeCountsByClass[cls][template] <= 0)
delete this.typeCountsByClass[cls][template];
}
}
}
}
};
/**
* This does neither apply effects nor verify requirements.
* @param {string} tech - The name of the technology to mark as researched.
*/
TechnologyManager.prototype.MarkTechnologyAsResearched = function(tech)
{
this.researchedTechs.add(tech);
this.UpdateAutoResearch();
};
/**
* Note that this does not verify whether the requirements are met.
* @param {string} tech - The technology to research.
* @param {number} researcher - Optionally the entity to couple with the research.
*/
TechnologyManager.prototype.ResearchTechnology = function(tech, researcher = INVALID_ENTITY)
{
if (this.IsTechnologyQueued(tech) || this.IsTechnologyResearched(tech))
return;
const technology = new this.Technology(tech, this.entity, researcher);
technology.Finish();
};
/**
* Marks a technology as being queued for research at the given entityID.
* @param {string} tech - The technology to queue.
* @param {number} researcher - The entity ID of the entity researching this technology.
* @param {Object} techCostMultiplier - The multipliers used when calculating the costs.
*
* @return {boolean} - Whether we successfully have queued the technology.
*/
TechnologyManager.prototype.QueuedResearch = function(tech, researcher, techCostMultiplier)
{
// ToDo: Check whether the technology is researched already?
const technology = new this.Technology(tech, this.entity, researcher);
if (!technology.Queue(techCostMultiplier))
return false;
this.researchQueued.set(tech, technology);
return true;
};
/**
* Marks a technology as not being currently researched and optionally sends a GUI notification.
* @param {string} tech - The name of the technology to stop.
* @param {boolean} notification - Whether a GUI notification ought to be sent.
*/
TechnologyManager.prototype.StoppedResearch = function(tech)
{
this.researchQueued.get(tech).Stop();
this.researchQueued.delete(tech);
};
/**
* @param {string} tech -
*/
TechnologyManager.prototype.Pause = function(tech)
{
this.researchQueued.get(tech).Pause();
};
/**
* @param {string} tech - The technology to advance.
* @param {number} allocatedTime - The time allocated to the technology.
* @return {number} - The time we've actually used.
*/
TechnologyManager.prototype.Progress = function(techName, allocatedTime)
{
const technology = this.researchQueued.get(techName);
const usedTime = technology.Progress(allocatedTime);
if (technology.finished)
this.researchQueued.delete(techName);
return usedTime;
};
/**
* @param {string} tech - The technology name to retreive some basic information for.
* @return {Object} - Some basic information about the technology under research.
*/
TechnologyManager.prototype.GetBasicInfo = function(tech)
{
return this.researchQueued.get(tech).GetBasicInfo();
};
/**
* Checks whether a technology is set to be researched.
*/
TechnologyManager.prototype.IsInProgress = function(tech)
{
return this.researchQueued.has(tech);
};
TechnologyManager.prototype.GetBasicInfoOfStartedTechs = function()
{
const result = {};
for (const [techName, tech] of this.researchQueued)
if (tech.started)
result[techName] = tech.GetBasicInfo();
return result;
};
/**
* Called by GUIInterface for PlayerData. AI use.
*/
TechnologyManager.prototype.GetQueuedResearch = function()
{
return this.researchQueued;
};
/**
* Returns the names of technologies that have already been researched.
*/
TechnologyManager.prototype.GetResearchedTechs = function()
{
return this.researchedTechs;
};
TechnologyManager.prototype.GetClassCounts = function()
{
return this.classCounts;
};
TechnologyManager.prototype.GetTypeCountsByClass = function()
{
return this.typeCountsByClass;
};
Engine.RegisterComponentType(IID_TechnologyManager, "TechnologyManager", TechnologyManager);
Index: ps/trunk/binaries/data/mods/public/simulation/components/Upgrade.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/components/Upgrade.js (revision 27244)
+++ ps/trunk/binaries/data/mods/public/simulation/components/Upgrade.js (revision 27245)
@@ -1,369 +1,364 @@
function Upgrade() {}
const UPGRADING_PROGRESS_INTERVAL = 250;
Upgrade.prototype.Schema =
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
Resources.BuildSchema("nonNegativeInteger") +
"" +
"" +
"" +
"" +
"" +
- "" +
- "" +
- "" +
- "" +
- "" +
- "" +
+ RequirementsHelper.BuildSchema() +
"" +
"" +
"" +
"" +
"" +
"" +
"";
Upgrade.prototype.Init = function()
{
this.elapsedTime = 0;
this.expendedResources = {};
};
// This will also deal with the "OnDestroy" case.
Upgrade.prototype.OnOwnershipChanged = function(msg)
{
if (!this.completed)
this.CancelUpgrade(msg.from);
if (msg.to != INVALID_PLAYER)
{
this.owner = msg.to;
this.DetermineUpgrades();
}
};
Upgrade.prototype.DetermineUpgrades = function()
{
this.upgradeTemplates = {};
for (const choice in this.template)
{
const nativeCiv = Engine.QueryInterface(this.entity, IID_Identity).GetCiv();
const playerCiv = QueryPlayerIDInterface(this.owner, IID_Identity).GetCiv();
const name = this.template[choice].Entity.
replace(/\{native\}/g, nativeCiv).
replace(/\{civ\}/g, playerCiv);
if (!Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager).TemplateExists(name))
continue;
if (this.upgradeTemplates[name])
warn("Upgrade Component: entity " + this.entity + " has two upgrades to the same entity, only the last will be used.");
this.upgradeTemplates[name] = choice;
}
};
Upgrade.prototype.ChangeUpgradedEntityCount = function(amount)
{
if (!this.IsUpgrading())
return;
let cmpTempMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager);
let template = cmpTempMan.GetTemplate(this.upgrading);
let categoryTo;
if (template.TrainingRestrictions)
categoryTo = template.TrainingRestrictions.Category;
else if (template.BuildRestrictions)
categoryTo = template.BuildRestrictions.Category;
if (!categoryTo)
return;
let categoryFrom;
let cmpTrainingRestrictions = Engine.QueryInterface(this.entity, IID_TrainingRestrictions);
let cmpBuildRestrictions = Engine.QueryInterface(this.entity, IID_BuildRestrictions);
if (cmpTrainingRestrictions)
categoryFrom = cmpTrainingRestrictions.GetCategory();
else if (cmpBuildRestrictions)
categoryFrom = cmpBuildRestrictions.GetCategory();
if (categoryTo == categoryFrom)
return;
let cmpEntityLimits = QueryPlayerIDInterface(this.owner, IID_EntityLimits);
if (cmpEntityLimits)
cmpEntityLimits.ChangeCount(categoryTo, amount);
};
Upgrade.prototype.CanUpgradeTo = function(template)
{
return this.upgradeTemplates[template] !== undefined;
};
Upgrade.prototype.GetUpgrades = function()
{
let ret = [];
for (const option in this.upgradeTemplates)
{
const choice = this.template[this.upgradeTemplates[option]];
let cost = {};
if (choice.Cost)
cost = this.GetResourceCosts(option);
if (choice.Time)
cost.time = this.GetUpgradeTime(option);
let hasCost = choice.Cost || choice.Time;
ret.push({
"entity": option,
"icon": choice.Icon || undefined,
"cost": hasCost ? cost : undefined,
"tooltip": choice.Tooltip || undefined,
- "requiredTechnology": this.GetRequiredTechnology(option),
+ "requirements": this.GetRequirements(option),
});
}
return ret;
};
Upgrade.prototype.CancelTimer = function()
{
if (!this.timer)
return;
let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
cmpTimer.CancelTimer(this.timer);
delete this.timer;
};
Upgrade.prototype.IsUpgrading = function()
{
return !!this.upgrading;
};
Upgrade.prototype.GetUpgradingTo = function()
{
return this.upgrading;
};
Upgrade.prototype.WillCheckPlacementRestrictions = function(template)
{
if (!this.upgradeTemplates[template])
return undefined;
// is undefined by default so use X in Y
return "CheckPlacementRestrictions" in this.template[this.upgradeTemplates[template]];
};
-Upgrade.prototype.GetRequiredTechnology = function(templateArg)
+Upgrade.prototype.GetRequirements = function(templateArg)
{
let choice = this.upgradeTemplates[templateArg] || templateArg;
- if (this.template[choice].RequiredTechnology)
- return this.template[choice].RequiredTechnology;
+ if (this.template[choice].Requirements)
+ return this.template[choice].Requirements;
- if (!("RequiredTechnology" in this.template[choice]))
+ if (!("Requirements" in this.template[choice]))
return undefined;
let cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager);
let cmpIdentity = Engine.QueryInterface(this.entity, IID_Identity);
let entType = this.template[choice].Entity;
if (cmpIdentity)
entType = entType.replace(/\{civ\}/g, cmpIdentity.GetCiv());
let template = cmpTemplateManager.GetTemplate(entType);
- return template.Identity.RequiredTechnology || undefined;
+ return template.Identity.Requirements || undefined;
};
Upgrade.prototype.GetResourceCosts = function(template)
{
if (!this.upgradeTemplates[template])
return undefined;
if (this.IsUpgrading() && template == this.GetUpgradingTo())
return clone(this.expendedResources);
let choice = this.upgradeTemplates[template];
if (!this.template[choice].Cost)
return {};
let costs = {};
for (let r in this.template[choice].Cost)
costs[r] = ApplyValueModificationsToEntity("Upgrade/Cost/"+r, +this.template[choice].Cost[r], this.entity);
return costs;
};
Upgrade.prototype.Upgrade = function(template)
{
if (this.IsUpgrading() || !this.upgradeTemplates[template])
return false;
let cmpPlayer = QueryOwnerInterface(this.entity, IID_Player);
if (!cmpPlayer)
return false;
let cmpProductionQueue = Engine.QueryInterface(this.entity, IID_ProductionQueue);
if (cmpProductionQueue && cmpProductionQueue.HasQueuedProduction())
{
let cmpGUIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface);
cmpGUIInterface.PushNotification({
"players": [cmpPlayer.GetPlayerID()],
"message": markForTranslation("Entity is producing. Cannot start upgrading."),
"translateMessage": true
});
return false;
}
this.expendedResources = this.GetResourceCosts(template);
if (!cmpPlayer || !cmpPlayer.TrySubtractResources(this.expendedResources))
{
this.expendedResources = {};
return false;
}
this.upgrading = template;
this.SetUpgradeAnimationVariant();
// Prevent cheating
this.ChangeUpgradedEntityCount(1);
if (this.GetUpgradeTime(template) !== 0)
{
let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
this.timer = cmpTimer.SetInterval(this.entity, IID_Upgrade, "UpgradeProgress", 0, UPGRADING_PROGRESS_INTERVAL, { "upgrading": template });
}
else
this.UpgradeProgress();
return true;
};
Upgrade.prototype.CancelUpgrade = function(owner)
{
if (!this.IsUpgrading())
return;
let cmpPlayer = QueryPlayerIDInterface(owner, IID_Player);
if (cmpPlayer)
cmpPlayer.AddResources(this.expendedResources);
this.expendedResources = {};
this.ChangeUpgradedEntityCount(-1);
// Do not update visual actor if the animation didn't change.
let choice = this.upgradeTemplates[this.upgrading];
if (choice && this.template[choice].Variant)
{
let cmpVisual = Engine.QueryInterface(this.entity, IID_Visual);
if (cmpVisual)
cmpVisual.SelectAnimation("idle", false, 1.0);
}
delete this.upgrading;
this.CancelTimer();
this.SetElapsedTime(0);
};
Upgrade.prototype.GetUpgradeTime = function(templateArg)
{
let template = this.upgrading || templateArg;
let choice = this.upgradeTemplates[template];
if (!choice)
return undefined;
if (!this.template[choice].Time)
return 0;
return ApplyValueModificationsToEntity("Upgrade/Time", +this.template[choice].Time, this.entity);
};
Upgrade.prototype.GetElapsedTime = function()
{
return this.elapsedTime;
};
Upgrade.prototype.GetProgress = function()
{
if (!this.IsUpgrading())
return undefined;
return this.GetUpgradeTime() == 0 ? 1 : Math.min(this.elapsedTime / 1000.0 / this.GetUpgradeTime(), 1.0);
};
Upgrade.prototype.SetElapsedTime = function(time)
{
this.elapsedTime = time;
Engine.PostMessage(this.entity, MT_UpgradeProgressUpdate, null);
};
Upgrade.prototype.SetUpgradeAnimationVariant = function()
{
let choice = this.upgradeTemplates[this.upgrading];
if (!choice || !this.template[choice].Variant)
return;
let cmpVisual = Engine.QueryInterface(this.entity, IID_Visual);
if (!cmpVisual)
return;
cmpVisual.SelectAnimation(this.template[choice].Variant, false, 1.0);
};
Upgrade.prototype.UpgradeProgress = function(data, lateness)
{
if (this.elapsedTime/1000.0 < this.GetUpgradeTime())
{
this.SetElapsedTime(this.GetElapsedTime() + UPGRADING_PROGRESS_INTERVAL + lateness);
return;
}
this.CancelTimer();
this.completed = true;
this.ChangeUpgradedEntityCount(-1);
this.expendedResources = {};
let newEntity = ChangeEntityTemplate(this.entity, this.upgrading);
if (newEntity)
PlaySound("upgraded", newEntity);
};
Engine.RegisterComponentType(IID_Upgrade, "Upgrade", Upgrade);
Index: ps/trunk/binaries/data/mods/public/simulation/components/tests/test_Identity.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/components/tests/test_Identity.js (revision 27244)
+++ ps/trunk/binaries/data/mods/public/simulation/components/tests/test_Identity.js (revision 27245)
@@ -1,56 +1,59 @@
+Engine.LoadHelperScript("Requirements.js");
Engine.LoadComponentScript("Identity.js");
let cmpIdentity = ConstructComponent(5, "Identity", {
"Civ": "iber",
"GenericName": "Iberian Skirmisher",
"Phenotype": { "_string": "male" },
});
TS_ASSERT_EQUALS(cmpIdentity.GetCiv(), "iber");
TS_ASSERT_EQUALS(cmpIdentity.GetLang(), "greek");
TS_ASSERT_EQUALS(cmpIdentity.GetPhenotype(), "male");
TS_ASSERT_EQUALS(cmpIdentity.GetRank(), "");
TS_ASSERT_UNEVAL_EQUALS(cmpIdentity.GetClassesList(), []);
TS_ASSERT_UNEVAL_EQUALS(cmpIdentity.GetVisibleClassesList(), []);
TS_ASSERT_EQUALS(cmpIdentity.HasClass("CitizenSoldier"), false);
TS_ASSERT_EQUALS(cmpIdentity.GetSelectionGroupName(), "");
TS_ASSERT_EQUALS(cmpIdentity.GetGenericName(), "Iberian Skirmisher");
cmpIdentity = ConstructComponent(6, "Identity", {
"Civ": "iber",
"Lang": "iberian",
"Phenotype": { "_string": "female" },
"GenericName": "Iberian Skirmisher",
"SpecificName": "Lusitano Ezpatari",
"SelectionGroupName": "units/iber/infantry_javelineer_b",
"Tooltip": "Basic ranged infantry",
"History": "Iberians, especially the Lusitanians, were good at" +
" ranged combat and ambushing enemy columns. They throw heavy iron" +
" javelins and sometimes even add burning pitch to them, making them" +
" good as a cheap siege weapon.",
"Rank": "Basic",
"Classes": { "_string": "CitizenSoldier Human Organic" },
"VisibleClasses": { "_string": "Javelineer" },
"Icon": "units/iber_infantry_javelineer.png",
- "RequiredTechnology": "phase_town"
+ "Requirements": {
+ "Techs": "phase_town"
+ }
});
TS_ASSERT_EQUALS(cmpIdentity.GetCiv(), "iber");
TS_ASSERT_EQUALS(cmpIdentity.GetLang(), "iberian");
TS_ASSERT_EQUALS(cmpIdentity.GetPhenotype(), "female");
TS_ASSERT_EQUALS(cmpIdentity.GetRank(), "Basic");
TS_ASSERT_UNEVAL_EQUALS(cmpIdentity.GetClassesList(), ["CitizenSoldier", "Human", "Organic", "Javelineer", "Basic"]);
TS_ASSERT_UNEVAL_EQUALS(cmpIdentity.GetVisibleClassesList(), ["Javelineer"]);
TS_ASSERT_EQUALS(cmpIdentity.HasClass("CitizenSoldier"), true);
TS_ASSERT_EQUALS(cmpIdentity.HasClass("Female"), false);
TS_ASSERT_EQUALS(cmpIdentity.GetSelectionGroupName(), "units/iber/infantry_javelineer_b");
TS_ASSERT_EQUALS(cmpIdentity.GetGenericName(), "Iberian Skirmisher");
cmpIdentity = ConstructComponent(7, "Identity", {
"Phenotype": { "_string": "First Second" },
});
TS_ASSERT_UNEVAL_EQUALS(cmpIdentity.GetPossiblePhenotypes(), ["First", "Second"]);
TS_ASSERT(["First", "Second"].indexOf(cmpIdentity.GetPhenotype()) !== -1);
cmpIdentity = ConstructComponent(8, "Identity", {});
TS_ASSERT_EQUALS(cmpIdentity.GetPhenotype(), "default");
Index: ps/trunk/binaries/data/mods/public/simulation/components/tests/test_UpgradeModification.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/components/tests/test_UpgradeModification.js (revision 27244)
+++ ps/trunk/binaries/data/mods/public/simulation/components/tests/test_UpgradeModification.js (revision 27245)
@@ -1,170 +1,171 @@
Engine.LoadHelperScript("Player.js");
+Engine.LoadHelperScript("Requirements.js");
Engine.LoadHelperScript("ValueModification.js");
Resources = {
"BuildSchema": type => {
let schema = "";
for (let res of ["food", "metal", "stone", "wood"])
schema +=
"" +
"" +
"" +
"" +
"";
return "" + schema + "";
}
};
Engine.LoadComponentScript("interfaces/ProductionQueue.js");
Engine.LoadComponentScript("interfaces/ModifiersManager.js"); // Provides `IID_ModifiersManager`, used below.
Engine.LoadComponentScript("interfaces/Timer.js"); // Provides `IID_Timer`, used below.
// What we're testing:
Engine.LoadComponentScript("interfaces/Upgrade.js");
Engine.LoadComponentScript("Upgrade.js");
// Input (bare minimum needed for tests):
let techs = {
"alter_tower_upgrade_cost": {
"modifications": [
{ "value": "Upgrade/Cost/stone", "add": 60.0 },
{ "value": "Upgrade/Cost/wood", "multiply": 0.5 },
{ "value": "Upgrade/Time", "replace": 90 }
],
"affects": ["Tower"]
}
};
let template = {
"Identity": {
"Classes": { '@datatype': "tokens", "_string": "Tower" },
"VisibleClasses": { '@datatype': "tokens", "_string": "" }
},
"Upgrade": {
"Tower": {
"Cost": { "stone": "100", "wood": "50" },
"Entity": "structures/{civ}/defense_tower",
"Time": "100"
}
}
};
let civCode = "pony";
let playerID = 1;
// Usually, the tech modifications would be worked out by the TechnologyManager
// with assistance from globalscripts. This test is not about testing the
// TechnologyManager, so the modifications (both with and without the technology
// researched) are worked out before hand and placed here.
let isResearched = false;
let templateTechModifications = {
"without": {},
"with": {
"Upgrade/Cost/stone": [{ "affects": [["Tower"]], "add": 60 }],
"Upgrade/Cost/wood": [{ "affects": [["Tower"]], "multiply": 0.5 }],
"Upgrade/Time": [{ "affects": [["Tower"]], "replace": 90 }]
}
};
let entityTechModifications = {
"without": {
'Upgrade/Cost/stone': { "20": { "origValue": 100, "newValue": 100 } },
'Upgrade/Cost/wood': { "20": { "origValue": 50, "newValue": 50 } },
'Upgrade/Time': { "20": { "origValue": 100, "newValue": 100 } }
},
"with": {
'Upgrade/Cost/stone': { "20": { "origValue": 100, "newValue": 160 } },
'Upgrade/Cost/wood': { "20": { "origValue": 50, "newValue": 25 } },
'Upgrade/Time': { "20": { "origValue": 100, "newValue": 90 } }
}
};
/**
* Initialise various bits.
*/
// System Entities:
AddMock(SYSTEM_ENTITY, IID_PlayerManager, {
"GetPlayerByID": pID => 10 // Called in helpers/player.js::QueryPlayerIDInterface(), as part of Tests T2 and T5.
});
AddMock(SYSTEM_ENTITY, IID_TemplateManager, {
"GetTemplate": () => template, // Called in components/Upgrade.js::ChangeUpgradedEntityCount().
"TemplateExists": (templ) => true
});
AddMock(SYSTEM_ENTITY, IID_Timer, {
"SetInterval": () => 1, // Called in components/Upgrade.js::Upgrade().
"CancelTimer": () => {} // Called in components/Upgrade.js::CancelUpgrade().
});
AddMock(SYSTEM_ENTITY, IID_ModifiersManager, {
"ApplyTemplateModifiers": (valueName, curValue, template, player) => {
// Called in helpers/ValueModification.js::ApplyValueModificationsToTemplate()
// as part of Tests T2 and T5 below.
let mods = isResearched ? templateTechModifications.with : templateTechModifications.without;
if (mods[valueName])
return GetTechModifiedProperty(mods[valueName], GetIdentityClasses(template.Identity), curValue);
return curValue;
},
"ApplyModifiers": (valueName, curValue, ent) => {
// Called in helpers/ValueModification.js::ApplyValueModificationsToEntity()
// as part of Tests T3, T6 and T7 below.
let mods = isResearched ? entityTechModifications.with : entityTechModifications.without;
return mods[valueName][ent].newValue;
}
});
// Init Player:
AddMock(10, IID_Player, {
"AddResources": () => {}, // Called in components/Upgrade.js::CancelUpgrade().
"GetPlayerID": () => playerID, // Called in helpers/Player.js::QueryOwnerInterface() (and several times below).
"TrySubtractResources": () => true // Called in components/Upgrade.js::Upgrade().
});
AddMock(10, IID_Identity, {
"GetCiv": () => civCode
});
// Create an entity with an Upgrade component:
AddMock(20, IID_Ownership, {
"GetOwner": () => playerID // Called in helpers/Player.js::QueryOwnerInterface().
});
AddMock(20, IID_Identity, {
"GetCiv": () => civCode // Called in components/Upgrade.js::init().
});
AddMock(20, IID_ProductionQueue, {
"HasQueuedProduction": () => false
});
let cmpUpgrade = ConstructComponent(20, "Upgrade", template.Upgrade);
cmpUpgrade.owner = playerID;
cmpUpgrade.OnOwnershipChanged({ "to": playerID });
/**
* Now to start the test proper
* To start with, no techs are researched...
*/
// T1: Check the cost of the upgrade without a player value being passed (as it would be in the structree).
let parsed_template = GetTemplateDataHelper(template, null, {}, Resources);
TS_ASSERT_UNEVAL_EQUALS(parsed_template.upgrades[0].cost, { "stone": 100, "wood": 50, "time": 100 });
// T2: Check the value, with a player ID (as it would be in-session).
parsed_template = GetTemplateDataHelper(template, playerID, {}, Resources);
TS_ASSERT_UNEVAL_EQUALS(parsed_template.upgrades[0].cost, { "stone": 100, "wood": 50, "time": 100 });
// T3: Check that the value is correct within the Update Component.
TS_ASSERT_UNEVAL_EQUALS(cmpUpgrade.GetUpgrades()[0].cost, { "stone": 100, "wood": 50, "time": 100 });
/**
* Tell the Upgrade component to start the Upgrade,
* then mark the technology that alters the upgrade cost as researched.
*/
cmpUpgrade.Upgrade("structures/" + civCode + "/defense_tower");
isResearched = true;
// T4: Check that the player-less value hasn't increased...
parsed_template = GetTemplateDataHelper(template, null, {}, Resources);
TS_ASSERT_UNEVAL_EQUALS(parsed_template.upgrades[0].cost, { "stone": 100, "wood": 50, "time": 100 });
// T5: ...but the player-backed value has.
parsed_template = GetTemplateDataHelper(template, playerID, {}, Resources);
TS_ASSERT_UNEVAL_EQUALS(parsed_template.upgrades[0].cost, { "stone": 160, "wood": 25, "time": 90 });
// T6: The upgrade component should still be using the old resource cost (but new time cost) for the upgrade in progress...
TS_ASSERT_UNEVAL_EQUALS(cmpUpgrade.GetUpgrades()[0].cost, { "stone": 100, "wood": 50, "time": 90 });
// T7: ...but with the upgrade cancelled, it now uses the modified value.
cmpUpgrade.CancelUpgrade(playerID);
TS_ASSERT_UNEVAL_EQUALS(cmpUpgrade.GetUpgrades()[0].cost, { "stone": 160, "wood": 25, "time": 90 });
Index: ps/trunk/binaries/data/mods/public/simulation/helpers/Commands.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/helpers/Commands.js (revision 27244)
+++ ps/trunk/binaries/data/mods/public/simulation/helpers/Commands.js (revision 27245)
@@ -1,1851 +1,1848 @@
// 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("OnPlayerCommand", { "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);
});
},
"collect-treasure-near-position": function(player, cmd, data)
{
GetFormationUnitAIs(data.entities, player, cmd, data.formation).forEach(cmpUnitAI => {
cmpUnitAI.CollectTreasureNearPosition(cmd.x, cmd.z, 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, cmd.pushFront);
});
},
"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, cmd.pushFront);
});
},
"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, cmd.pushFront);
}
},
"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, cmd.pushFront);
});
},
"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, cmd.pushFront);
});
},
"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, cmd.pushFront);
});
},
"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, cmd.pushFront);
});
},
"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, cmd.pushFront);
});
},
"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, cmd.pushFront);
});
},
"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, cmd.pushFront);
});
},
"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, cmd.pushFront);
});
},
"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);
}
},
"call-to-arms": function(player, cmd, data)
{
const unitsToMove = data.entities.filter(ent =>
MatchesClassList(Engine.QueryInterface(ent, IID_Identity).GetClassesList(),
["Soldier", "Warship", "Siege", "Healer"])
);
GetFormationUnitAIs(unitsToMove, player, cmd, data.formation).forEach(cmpUnitAI => {
const target = cmd.target;
if (cmd.pushFront)
{
cmpUnitAI.WalkAndFight(target.x, target.z, cmd.targetClasses, cmd.allowCapture, false, cmd.pushFront);
cmpUnitAI.DropAtNearestDropSite(false, cmd.pushFront);
}
else
{
cmpUnitAI.DropAtNearestDropSite(cmd.queued, false)
cmpUnitAI.WalkAndFight(target.x, target.z, cmd.targetClasses, cmd.allowCapture, true, false);
}
});
},
"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;
}
const cmpTrainer = Engine.QueryInterface(ent, IID_Trainer);
if (!cmpTrainer)
continue;
let templateName = cmd.template;
// 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 (data.cmpPlayer.IsAI())
templateName = cmpTrainer.GetUpgradedTemplate(cmd.template);
if (cmpTrainer.CanTrain(templateName))
Engine.QueryInterface(ent, IID_ProductionQueue)?.AddItem(templateName, "unit", +cmd.count, cmd.metadata, cmd.pushFront);
}
},
"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.AddItem(cmd.template, "technology", undefined, cmd.metadata, cmd.pushFront);
},
"stop-production": function(player, cmd, data)
{
let cmpProductionQueue = Engine.QueryInterface(cmd.entity, IID_ProductionQueue);
if (cmpProductionQueue)
cmpProductionQueue.RemoveItem(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.Defeat(markForTranslation("%(player)s has resigned."));
},
"occupy-turret": function(player, cmd, data)
{
GetFormationUnitAIs(data.entities, player).forEach(cmpUnitAI => {
cmpUnitAI.OccupyTurret(cmd.target, cmd.queued);
});
},
"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, cmd.pushFront);
});
},
"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, cmd.pushFront);
});
},
"stop": function(player, cmd, data)
{
GetFormationUnitAIs(data.entities, player, cmd, data.formation).forEach(cmpUnitAI => {
cmpUnitAI.Stop(cmd.queued);
});
},
"leave-turret": function(player, cmd, data)
{
let notUnloaded = 0;
for (let ent of data.entities)
{
let cmpTurretable = Engine.QueryInterface(ent, IID_Turretable);
if (!cmpTurretable || !cmpTurretable.LeaveTurret())
++notUnloaded;
}
if (notUnloaded)
notifyUnloadFailure(player);
},
"unload-turrets": function(player, cmd, data)
{
let notUnloaded = 0;
for (let ent of data.entities)
{
let cmpTurretHolder = Engine.QueryInterface(ent, IID_TurretHolder);
for (let turret of cmpTurretHolder.GetEntities())
{
let cmpTurretable = Engine.QueryInterface(turret, IID_Turretable);
if (!cmpTurretable || !cmpTurretable.LeaveTurret())
++notUnloaded;
}
}
if (notUnloaded)
notifyUnloadFailure(player);
},
"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, cmd.pushFront);
else
cmpUnitAI.Unpack(cmd.queued, cmd.pushFront);
}
},
"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, cmd.pushFront);
else
cmpUnitAI.CancelUnpack(cmd.queued, cmd.pushFront);
}
},
"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 (!RequirementsHelper.AreRequirementsMet(cmpUpgrade.GetRequirements(cmd.template), player))
{
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);
}
},
"map-flare": function(player, cmd, data)
{
let cmpGuiInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface);
cmpGuiInterface.PushNotification({
"type": "map-flare",
"players": [player],
"target": cmd.target
});
},
"autoqueue-on": function(player, cmd, data)
{
for (let ent of data.entities)
{
let cmpProductionQueue = Engine.QueryInterface(ent, IID_ProductionQueue);
if (cmpProductionQueue)
cmpProductionQueue.EnableAutoQueue();
}
},
"autoqueue-off": function(player, cmd, data)
{
for (let ent of data.entities)
{
let cmpProductionQueue = Engine.QueryInterface(ent, IID_ProductionQueue);
if (cmpProductionQueue)
cmpProductionQueue.DisableAutoQueue();
}
},
};
/**
* Sends a GUI notification about unit(s) that failed to ungarrison.
*/
function notifyUnloadFailure(player)
{
let cmpGUIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface);
cmpGUIInterface.PushNotification({
"type": "text",
"players": [player],
"message": markForTranslation("Unable to unload 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": markForTranslation("%(unit)s can't be controlled."),
"parameters": { "unit": cmpIdentity.GetGenericName() },
"translateParameters": ["unit"],
"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.
let cmpCost = Engine.QueryInterface(ent, IID_Cost);
let costs = cmpCost.GetResourceCosts();
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(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,
"pushFront": cmd.pushFront,
"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;
// 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 || !cmpUnitAI.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
const requirements = GetFormationRequirements(formationTemplate);
if (!requirements)
return false;
let count = 0;
for (const ent of ents)
if (Engine.QueryInterface(ent, IID_UnitAI)?.CanUseFormation(formationTemplate))
++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/Requirements.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/helpers/Requirements.js (nonexistent)
+++ ps/trunk/binaries/data/mods/public/simulation/helpers/Requirements.js (revision 27245)
@@ -0,0 +1,164 @@
+function RequirementsHelper() {}
+
+RequirementsHelper.prototype.DEFAULT_RECURSION_DEPTH = 1;
+
+RequirementsHelper.prototype.EntityRequirementsSchema =
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "";
+
+RequirementsHelper.prototype.TechnologyRequirementsSchema =
+ "" +
+ "" +
+ "";
+
+/**
+ * @param {number} recursionDepth - How deep we recurse.
+ * @return {string} - A RelaxRNG schema for requirements.
+ */
+RequirementsHelper.prototype.RequirementsSchema = function(recursionDepth)
+{
+ return "" +
+ "" +
+ this.ChoicesSchema(--recursionDepth) +
+ "";
+};
+
+/**
+ * @param {number} recursionDepth - How deep we recurse.
+ * @return {string} - A RelaxRNG schema for chosing requirements.
+ */
+RequirementsHelper.prototype.ChoicesSchema = function(recursionDepth)
+{
+ const allAnySchema = recursionDepth > 0 ? "" +
+ "" +
+ this.RequirementsSchema(recursionDepth) +
+ "" +
+ "" +
+ this.RequirementsSchema(recursionDepth) +
+ "" : "";
+
+ return "" +
+ "" +
+ allAnySchema +
+ this.EntityRequirementsSchema +
+ this.TechnologyRequirementsSchema +
+ "";
+};
+
+/**
+ * @param {number} recursionDepth - How deeply recursive we build the schema.
+ * @return {string} - A RelaxRNG schema for requirements.
+ */
+RequirementsHelper.prototype.BuildSchema = function(recursionDepth = this.DEFAULT_RECURSION_DEPTH)
+{
+ return "" +
+ "" +
+ "" +
+ this.ChoicesSchema(recursionDepth) +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "";
+};
+
+/**
+ * @param {Object} template - The requirements template as defined above.
+ * @param {number} playerID -
+ * @return {boolean} -
+ */
+RequirementsHelper.prototype.AreRequirementsMet = function(template, playerID)
+{
+ if (!template || !Object.keys(template).length)
+ return true;
+
+ const cmpTechManager = QueryPlayerIDInterface(playerID, IID_TechnologyManager);
+ return this.AllRequirementsMet(template, cmpTechManager);
+};
+
+/**
+ * @param {Object} template - The requirements template for "all".
+ * @param {component} cmpTechManager -
+ * @return {boolean} -
+ */
+RequirementsHelper.prototype.AllRequirementsMet = function(template, cmpTechManager)
+{
+ for (const requirementType in template)
+ {
+ const requirement = template[requirementType];
+ if (requirementType === "All" && !this.AllRequirementsMet(requirement, cmpTechManager))
+ return false;
+ if (requirementType === "Any" && !this.AnyRequirementsMet(requirement, cmpTechManager))
+ return false;
+ if (requirementType === "Entities")
+ {
+ for (const className in requirement)
+ {
+ const entReq = requirement[className];
+ if ("Count" in entReq && (!(className in cmpTechManager.classCounts) || cmpTechManager.classCounts[className] < entReq.Count))
+ return false;
+ if ("Variants" in entReq && (!(className in cmpTechManager.typeCountsByClass) || Object.keys(cmpTechManager.typeCountsByClass[className]).length < entReq.Variants))
+ return false;
+ }
+ }
+ if (requirementType === "Techs")
+ for (const tech of requirement.split(" "))
+ if (tech[0] === "!" ? cmpTechManager.IsTechnologyResearched(tech.substring(1)) :
+ !cmpTechManager.IsTechnologyResearched(tech))
+ return false;
+ }
+ return true;
+};
+
+/**
+ * @param {Object} template - The requirements template for "any".
+ * @param {component} cmpTechManager -
+ * @return {boolean} -
+ */
+RequirementsHelper.prototype.AnyRequirementsMet = function(template, cmpTechManager)
+{
+ for (const requirementType in template)
+ {
+ const requirement = template[requirementType];
+ if (requirementType === "All" && this.AllRequirementsMet(requirement, cmpTechManager))
+ return true;
+ if (requirementType === "Any" && this.AnyRequirementsMet(requirement, cmpTechManager))
+ return true;
+ if (requirementType === "Entities")
+ {
+ for (const className in requirement)
+ {
+ const entReq = requirement[className];
+ if ("Count" in entReq && className in cmpTechManager.classCounts && cmpTechManager.classCounts[className] >= entReq.Count)
+ return true;
+ if ("Variants" in entReq && className in cmpTechManager.typeCountsByClass && Object.keys(cmpTechManager.typeCountsByClass[className]).length >= entReq.Variants)
+ return true;
+ }
+ }
+ if (requirementType === "Techs")
+ for (const tech of requirement.split(" "))
+ if (tech[0] === "!" ? !cmpTechManager.IsTechnologyResearched(tech.substring(1)) :
+ cmpTechManager.IsTechnologyResearched(tech))
+ return true;
+ }
+ return false;
+};
+
+Engine.RegisterGlobal("RequirementsHelper", new RequirementsHelper());
Property changes on: ps/trunk/binaries/data/mods/public/simulation/helpers/Requirements.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/tests/test_Requirements.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/helpers/tests/test_Requirements.js (nonexistent)
+++ ps/trunk/binaries/data/mods/public/simulation/helpers/tests/test_Requirements.js (revision 27245)
@@ -0,0 +1,718 @@
+Engine.LoadComponentScript("interfaces/PlayerManager.js");
+Engine.LoadComponentScript("interfaces/TechnologyManager.js");
+Engine.LoadHelperScript("Player.js");
+Engine.LoadHelperScript("Requirements.js");
+
+const playerID = 1;
+const playerEnt = 11;
+
+AddMock(SYSTEM_ENTITY, IID_PlayerManager, {
+ "GetPlayerByID": () => playerEnt
+});
+
+// First test no requirements.
+let template = {
+};
+
+const met = () => TS_ASSERT(RequirementsHelper.AreRequirementsMet(template, playerID));
+const notMet = () => TS_ASSERT(!RequirementsHelper.AreRequirementsMet(template, playerID));
+
+met();
+
+// Simple requirements are assumed to be additive.
+template = {
+ "Techs": "phase_city"
+};
+
+AddMock(playerEnt, IID_TechnologyManager, {
+ "IsTechnologyResearched": (tech) => false
+});
+notMet();
+
+AddMock(playerEnt, IID_TechnologyManager, {
+ "IsTechnologyResearched": (tech) => tech === "phase_town"
+});
+notMet();
+
+AddMock(playerEnt, IID_TechnologyManager, {
+ "IsTechnologyResearched": (tech) => tech === "phase_town" || tech === "phase_city"
+});
+met();
+
+template = {
+ "Techs": "cartography phase_city"
+};
+
+AddMock(playerEnt, IID_TechnologyManager, {
+ "IsTechnologyResearched": (tech) => false
+});
+notMet();
+
+AddMock(playerEnt, IID_TechnologyManager, {
+ "IsTechnologyResearched": (tech) => tech === "phase_town"
+});
+notMet();
+
+AddMock(playerEnt, IID_TechnologyManager, {
+ "IsTechnologyResearched": (tech) => tech === "phase_town" || tech === "phase_city"
+});
+notMet();
+
+AddMock(playerEnt, IID_TechnologyManager, {
+ "IsTechnologyResearched": (tech) => tech === "cartography" || tech === "phase_town"
+});
+notMet();
+
+AddMock(playerEnt, IID_TechnologyManager, {
+ "IsTechnologyResearched": (tech) => tech === "cartography" || tech === "phase_city"
+});
+met();
+
+
+// Additive requirements (all should to be met).
+// Entity requirements.
+template = {
+ "All": {
+ "Entities": {
+ "class_1": {
+ "Count": 1
+ }
+ }
+ }
+};
+
+AddMock(playerEnt, IID_TechnologyManager, {
+ "classCounts": {},
+ "typeCountsByClass": {}
+});
+notMet();
+
+AddMock(playerEnt, IID_TechnologyManager, {
+ "classCounts": {
+ "class_1": 0
+ },
+ "typeCountsByClass": {}
+});
+notMet();
+
+AddMock(playerEnt, IID_TechnologyManager, {
+ "classCounts": {
+ "class_2": 1
+ },
+ "typeCountsByClass": {}
+});
+notMet();
+
+AddMock(playerEnt, IID_TechnologyManager, {
+ "classCounts": {
+ "class_1": 1
+ },
+ "typeCountsByClass": {}
+});
+met();
+
+AddMock(playerEnt, IID_TechnologyManager, {
+ "classCounts": {
+ "class_1": 1
+ },
+ "typeCountsByClass": {
+ "class_1": {
+ "template_1": 1
+ }
+ }
+});
+met();
+
+
+template = {
+ "All": {
+ "Entities": {
+ "class_1": {
+ "Variants": 2
+ }
+ }
+ }
+};
+
+AddMock(playerEnt, IID_TechnologyManager, {
+ "classCounts": {
+ "class_1": 2
+ },
+ "typeCountsByClass": {
+ "class_1": {
+ "template_1": 2
+ }
+ }
+});
+notMet();
+
+AddMock(playerEnt, IID_TechnologyManager, {
+ "classCounts": {
+ "class_1": 2
+ },
+ "typeCountsByClass": {
+ "class_1": {
+ "template_1": 1,
+ "template_2": 1
+ }
+ }
+});
+met();
+
+AddMock(playerEnt, IID_TechnologyManager, {
+ "classCounts": {
+ "class_1": 1
+ },
+ "typeCountsByClass": {
+ "class_1": {
+ "template_1": 1
+ }
+ }
+});
+notMet();
+
+template = {
+ "All": {
+ "Entities": {
+ "class_1": {
+ "Count": 1,
+ "Variants": 2
+ }
+ }
+ }
+};
+
+AddMock(playerEnt, IID_TechnologyManager, {
+ "classCounts": {
+ "class_1": 1
+ },
+ "typeCountsByClass": {
+ "class_1": {
+ "template_1": 1
+ }
+ }
+});
+notMet();
+
+AddMock(playerEnt, IID_TechnologyManager, {
+ "classCounts": {
+ "class_1": 2
+ },
+ "typeCountsByClass": {
+ "class_1": {
+ "template_1": 1,
+ "template_2": 1
+ }
+ }
+});
+met();
+
+template = {
+ "All": {
+ "Entities": {
+ "class_1": {
+ "Count": 3,
+ "Variants": 2
+ }
+ }
+ }
+};
+
+AddMock(playerEnt, IID_TechnologyManager, {
+ "classCounts": {
+ "class_1": 2
+ },
+ "typeCountsByClass": {
+ "class_1": {
+ "template_1": 1,
+ "template_2": 1
+ }
+ }
+});
+notMet();
+
+AddMock(playerEnt, IID_TechnologyManager, {
+ "classCounts": {
+ "class_1": 3
+ },
+ "typeCountsByClass": {
+ "class_1": {
+ "template_1": 2,
+ "template_2": 1
+ }
+ }
+});
+met();
+
+
+// Technology requirements.
+template = {
+ "All": {
+ "Techs": "phase_town"
+ }
+};
+
+AddMock(playerEnt, IID_TechnologyManager, {
+ "IsTechnologyResearched": (tech) => false
+});
+notMet();
+
+AddMock(playerEnt, IID_TechnologyManager, {
+ "IsTechnologyResearched": (tech) => tech === "phase_town"
+});
+met();
+
+template = {
+ "All": {
+ "Techs": "phase_city"
+ }
+};
+notMet();
+
+template = {
+ "All": {
+ "Techs": "phase_town phase_city"
+ }
+};
+notMet();
+
+AddMock(playerEnt, IID_TechnologyManager, {
+ "IsTechnologyResearched": (tech) => tech === "phase_town" || tech === "phase_city"
+});
+met();
+
+template = {
+ "All": {
+ "Techs": "!phase_city"
+ }
+};
+notMet();
+
+template = {
+ "All": {
+ "Techs": "!phase_town phase_city"
+ }
+};
+notMet();
+
+AddMock(playerEnt, IID_TechnologyManager, {
+ "IsTechnologyResearched": (tech) => tech === "phase_city"
+});
+met();
+
+
+// Combination of Entity and Technology requirements.
+template = {
+ "All": {
+ "Entities": {
+ "class_1": {
+ "Count": 3,
+ "Variants": 2
+ }
+ },
+ "Techs": "phase_town"
+ }
+};
+
+AddMock(playerEnt, IID_TechnologyManager, {
+ "classCounts": {
+ "class_1": 3
+ },
+ "IsTechnologyResearched": (tech) => false,
+ "typeCountsByClass": {
+ "class_1": {
+ "template_1": 2,
+ "template_2": 1
+ }
+ }
+});
+notMet();
+
+AddMock(playerEnt, IID_TechnologyManager, {
+ "classCounts": {
+ "class_1": 3
+ },
+ "IsTechnologyResearched": (tech) => tech === "phase_town",
+ "typeCountsByClass": {
+ "class_1": {
+ "template_1": 2,
+ "template_2": 1
+ }
+ }
+});
+met();
+
+AddMock(playerEnt, IID_TechnologyManager, {
+ "classCounts": {
+ "class_1": 3
+ },
+ "IsTechnologyResearched": (tech) => tech === "phase_city",
+ "typeCountsByClass": {
+ "class_1": {
+ "template_1": 2,
+ "template_2": 1
+ }
+ }
+});
+notMet();
+
+
+// Choice requirements (at least one needs to be met).
+// Entity requirements.
+template = {
+ "Any": {
+ "Entities": {
+ "class_1": {
+ "Count": 1,
+ }
+ },
+ }
+};
+
+AddMock(playerEnt, IID_TechnologyManager, {
+ "classCounts": {
+ "class_1": 0
+ }
+});
+notMet();
+
+AddMock(playerEnt, IID_TechnologyManager, {
+ "classCounts": {
+ "class_1": 1
+ }
+});
+met();
+
+template = {
+ "Any": {
+ "Entities": {
+ "class_1": {
+ "Count": 5,
+ "Variants": 2
+ }
+ },
+ }
+};
+
+AddMock(playerEnt, IID_TechnologyManager, {
+ "classCounts": {
+ "class_1": 3
+ },
+ "typeCountsByClass": {
+ "class_1": {
+ "template_1": 3,
+ }
+ }
+});
+notMet();
+
+AddMock(playerEnt, IID_TechnologyManager, {
+ "classCounts": {
+ "class_1": 3
+ },
+ "typeCountsByClass": {
+ "class_1": {
+ "template_1": 2,
+ "template_2": 1
+ }
+ }
+});
+met();
+
+
+// Technology requirements.
+template = {
+ "Any": {
+ "Techs": "phase_town"
+ }
+};
+
+AddMock(playerEnt, IID_TechnologyManager, {
+ "IsTechnologyResearched": (tech) => tech === "phase_city"
+});
+notMet();
+
+AddMock(playerEnt, IID_TechnologyManager, {
+ "IsTechnologyResearched": (tech) => tech === "phase_town"
+});
+met();
+
+template = {
+ "Any": {
+ "Techs": "phase_town phase_city"
+ }
+};
+
+AddMock(playerEnt, IID_TechnologyManager, {
+ "IsTechnologyResearched": (tech) => tech === "phase_city"
+});
+met();
+
+template = {
+ "Any": {
+ "Techs": "!phase_town"
+ }
+};
+
+AddMock(playerEnt, IID_TechnologyManager, {
+ "IsTechnologyResearched": (tech) => tech === "phase_town"
+});
+notMet();
+
+AddMock(playerEnt, IID_TechnologyManager, {
+ "IsTechnologyResearched": (tech) => tech === "phase_city"
+});
+met();
+
+template = {
+ "Any": {
+ "Techs": "!phase_town phase_city"
+ }
+};
+
+AddMock(playerEnt, IID_TechnologyManager, {
+ "IsTechnologyResearched": (tech) => tech === "phase_town" || tech === "phase_city"
+});
+met();
+
+
+// Combinational requirements of entities and technologies.
+template = {
+ "Any": {
+ "Entities": {
+ "class_1": {
+ "Count": 3,
+ "Variants": 2
+ }
+ },
+ "Techs": "!phase_town"
+ }
+};
+
+AddMock(playerEnt, IID_TechnologyManager, {
+ "classCounts": {
+ "class_1": 3
+ },
+ "IsTechnologyResearched": (tech) => tech === "phase_town",
+ "typeCountsByClass": {
+ "class_1": {
+ "template_1": 3
+ }
+ }
+});
+met();
+
+
+// Nested requirements.
+template = {
+ "All": {
+ "All": {
+ "Techs": "!phase_town"
+ },
+ "Any": {
+ "Entities": {
+ "class_1": {
+ "Count": 3,
+ "Variants": 2
+ }
+ },
+ "Techs": "phase_city"
+ }
+ }
+};
+
+AddMock(playerEnt, IID_TechnologyManager, {
+ "classCounts": {
+ "class_1": 3
+ },
+ "IsTechnologyResearched": (tech) => tech === "phase_town",
+ "typeCountsByClass": {
+ "class_1": {
+ "template_1": 3
+ }
+ }
+});
+notMet();
+
+AddMock(playerEnt, IID_TechnologyManager, {
+ "classCounts": {
+ "class_1": 3
+ },
+ "IsTechnologyResearched": (tech) => tech === "phase_town" || tech === "phase_city",
+ "typeCountsByClass": {
+ "class_1": {
+ "template_1": 3
+ }
+ }
+});
+notMet();
+
+AddMock(playerEnt, IID_TechnologyManager, {
+ "classCounts": {
+ "class_1": 2
+ },
+ "IsTechnologyResearched": (tech) => tech === "phase_city",
+ "typeCountsByClass": {
+ "class_1": {
+ "template_1": 2
+ }
+ }
+});
+met();
+
+AddMock(playerEnt, IID_TechnologyManager, {
+ "classCounts": {
+ "class_1": 2
+ },
+ "IsTechnologyResearched": (tech) => false,
+ "typeCountsByClass": {
+ "class_1": {
+ "template_1": 1,
+ "template_2": 1
+ }
+ }
+});
+met();
+
+
+template = {
+ "Any": {
+ "All": {
+ "Techs": "!phase_town"
+ },
+ "Any": {
+ "Entities": {
+ "class_1": {
+ "Count": 3,
+ "Variants": 2
+ }
+ },
+ "Techs": "phase_city"
+ }
+ }
+};
+
+AddMock(playerEnt, IID_TechnologyManager, {
+ "classCounts": {
+ "class_1": 2
+ },
+ "IsTechnologyResearched": (tech) => tech === "phase_town",
+ "typeCountsByClass": {
+ "class_1": {
+ "template_1": 2
+ }
+ }
+});
+notMet();
+
+AddMock(playerEnt, IID_TechnologyManager, {
+ "classCounts": {
+ "class_1": 3
+ },
+ "IsTechnologyResearched": (tech) => tech === "phase_town" || tech === "phase_city",
+ "typeCountsByClass": {
+ "class_1": {
+ "template_1": 3
+ }
+ }
+});
+met();
+
+AddMock(playerEnt, IID_TechnologyManager, {
+ "classCounts": {
+ "class_1": 2
+ },
+ "IsTechnologyResearched": (tech) => tech === "phase_city",
+ "typeCountsByClass": {
+ "class_1": {
+ "template_1": 2
+ }
+ }
+});
+met();
+
+AddMock(playerEnt, IID_TechnologyManager, {
+ "classCounts": {
+ "class_1": 2
+ },
+ "IsTechnologyResearched": (tech) => false,
+ "typeCountsByClass": {
+ "class_1": {
+ "template_1": 1,
+ "template_2": 1
+ }
+ }
+});
+met();
+
+
+// Two levels deep nested.
+template = {
+ "All": {
+ "Any": {
+ "All": {
+ "Techs": "cartography phase_imperial",
+ },
+ "Entities": {
+ "class_1": {
+ "Count": 3,
+ "Variants": 2
+ }
+ },
+ "Techs": "phase_city"
+ },
+ "Techs": "!phase_town"
+ }
+};
+
+AddMock(playerEnt, IID_TechnologyManager, {
+ "classCounts": {
+ "class_1": 2
+ },
+ "IsTechnologyResearched": (tech) => tech === "phase_town",
+ "typeCountsByClass": {
+ "class_1": {
+ "template_1": 2
+ }
+ }
+});
+notMet();
+
+AddMock(playerEnt, IID_TechnologyManager, {
+ "classCounts": {
+ "class_1": 2
+ },
+ "IsTechnologyResearched": (tech) => tech === "phase_city",
+ "typeCountsByClass": {
+ "class_1": {
+ "template_1": 2
+ }
+ }
+});
+met();
+
+AddMock(playerEnt, IID_TechnologyManager, {
+ "classCounts": {
+ "class_1": 2
+ },
+ "IsTechnologyResearched": (tech) => tech === "phase_imperial",
+ "typeCountsByClass": {
+ "class_1": {
+ "template_1": 2
+ }
+ }
+});
+notMet();
+
+AddMock(playerEnt, IID_TechnologyManager, {
+ "classCounts": {
+ "class_1": 2
+ },
+ "IsTechnologyResearched": (tech) => tech === "cartography" || tech === "phase_imperial",
+ "typeCountsByClass": {
+ "class_1": {
+ "template_1": 2
+ }
+ }
+});
+met();
Property changes on: ps/trunk/binaries/data/mods/public/simulation/helpers/tests/test_Requirements.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/templates/gaia/fauna_pig_trainable.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fauna_pig_trainable.xml (revision 27244)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fauna_pig_trainable.xml (revision 27245)
@@ -1,10 +1,12 @@
-
- phase_town
+
+ phase_town
+
+ false
Index: ps/trunk/binaries/data/mods/public/simulation/templates/special/spy.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/special/spy.xml (revision 27244)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/special/spy.xml (revision 27245)
@@ -1,26 +1,28 @@
00000500gaiaSpySpytechnologies/spy_trader.png
- unlock_spiestrue
+
+ unlock_spies
+ false150.25
Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/athen/gymnasium.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/structures/athen/gymnasium.xml (revision 27244)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/athen/gymnasium.xml (revision 27245)
@@ -1,70 +1,72 @@
2001501008.0102000athenGymnasiumGymnasionTrain Champions.ConquestCritical CivSpecificGymnasium -City Townstructures/gymnasium.png
- phase_town
+
+ phase_town
+ 4040
iphicratean_reforms
interface/complete/building/complete_gymnasium.xmlfalse38400000.7
units/{civ}/infantry_marine_archer_b
units/{civ}/champion_marine
units/{civ}/champion_infantry
units/{civ}/champion_ranged
40structures/athenians/gymnasium.xmlstructures/fndn_8x9.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/brit/crannog.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/structures/brit/crannog.xml (revision 27244)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/brit/crannog.xml (revision 27245)
@@ -1,52 +1,54 @@
own ally neutralshore8.0britIsland SettlementCranogionBuild upon a shoreline in own, neutral, or allied territory. Acquire large tracts of territory. Territory root. Train Citizens, construct Ships, and research technologies. Garrison Soldiers for additional arrows.CivSpecificNavalstructures/crannog.png
- phase_town
+
+ phase_town
+ true0.0ship
-phase_town_{civ}
-hellenistic_metropolis
units/{civ}/infantry_spearman_b
units/{civ}/infantry_slinger_b
units/{civ}/cavalry_javelineer_b
units/{civ}/ship_fishing
units/{civ}/ship_merchant
units/{civ}/ship_bireme
units/{civ}/ship_trireme
structures/britons/crannog.xmlstructures/fndn_8x8.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/cart/apartment.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/structures/cart/apartment.xml (revision 27244)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/cart/apartment.xml (revision 27245)
@@ -1,47 +1,49 @@
21.5505021.5Apartment BuildingBetCivSpecific-Village Townstructures/apartment.png
- phase_town
+
+ phase_town
+ 101010
+
+ 2
+ structures/carthaginians/fndn_house.xmlstructures/carthaginians/apartment.xml
-
- 2
-
Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/cart/wallset_short.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/structures/cart/wallset_short.xml (revision 27244)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/cart/wallset_short.xml (revision 27245)
@@ -1,18 +1,20 @@
cartLow Wall
- phase_villagestructures/palisade_wall.png
+
+ phase_village
+ structures/cart/s_wall_towerstructures/cart/s_wall_gatestructures/cart/s_wall_longstructures/cart/s_wall_mediumstructures/cart/s_wall_short
Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/gaul/assembly.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/structures/gaul/assembly.xml (revision 27244)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/gaul/assembly.xml (revision 27245)
@@ -1,78 +1,80 @@
20040010.0200.1UnitSupport Infantry Cavalry022000decay|rubble/rubble_stone_6x6gaulAssembly of PrincesRemogantionTrain Champion Trumpeters and Heroes.ConquestCritical CivSpecificCity Councilstructures/tholos.png
- phase_city
+
+ phase_city
+ 8020303interface/complete/building/complete_iber_monument.xmlfalse40400000.7
units/{civ}/champion_infantry_trumpeter
units/{civ}/hero_brennus
units/{civ}/hero_viridomarus
units/{civ}/hero_vercingetorix
40structures/gauls/theater.xmlstructures/fndn_6x6.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/han/academy.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/structures/han/academy.xml (revision 27244)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/han/academy.xml (revision 27245)
@@ -1,64 +1,66 @@
Academy200030030010.0Minister2000decay|rubble/rubble_stone_5x5hanImperial AcademyDìguó Xuéyuànstructures/han/academyTrain Champions and research their technologies.-Village City Academystructures/embassy_italic.png
- phase_city
+
+ phase_city
+
-barracks_batch_training
-unlock_champion_infantry
interface/complete/building/complete_tholos.xmlinterface/complete/building/complete_tholos.xml0.8
units/{civ}/champion_infantry_spearman_academy
units/{civ}/champion_infantry_crossbowman_academy
units/{civ}/champion_cavalry_spearman_academy
units/{civ}/champion_chariot_academy
structures/han/academy.xmlstructures/fndn_6x6.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/han/civil_centre_court.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/structures/han/civil_centre_court.xml (revision 27244)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/han/civil_centre_court.xml (revision 27245)
@@ -1,44 +1,46 @@
ImperialCourt1.51.51.5hanImperial CourtCháotíngDefensive ImperialCourt CityCivCentre CivSpecific
- phase_citystructures/military_settlement.png
+
+ phase_city
+ 30
-phase_town_{civ}
0.5
units/{civ}/hero_han_xin_horse
units/{civ}/hero_liu_bang_horse
units/{civ}/hero_wei_qing_chariot
structures/han/imperial_court.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/han/laozigate.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/structures/han/laozigate.xml (revision 27244)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/han/laozigate.xml (revision 27245)
@@ -1,61 +1,63 @@
12010020012.01200decay|rubble/rubble_stone_4x2hanLaoziGateLǎozǐ MénCivSpecificLaoziGate Townstructures/paifang.png
- phase_town
+
+ phase_town
+ 2020falsefalse20303interface/complete/building/complete_iber_monument.xmlstructures/han/shrine.xmlstructures/fndn_6x2.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/han/wallset_palisade.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/structures/han/wallset_palisade.xml (revision 27244)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/han/wallset_palisade.xml (revision 27245)
@@ -1,24 +1,26 @@
hanBamboo StockadeWall off an area. Build in own or neutral territory.-Wall Palisadestructures/palisade_wall.png
- phase_village
+
+ phase_village
+ 0.950.05structures/han/palisades_towerstructures/han/palisades_gatestructures/han/palisades_towerstructures/han/palisades_longstructures/han/palisades_mediumstructures/han/palisades_shortstructures/han/palisades_curve
Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/kush/pyramid_small.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/structures/kush/pyramid_small.xml (revision 27244)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/kush/pyramid_small.xml (revision 27245)
@@ -1,58 +1,60 @@
structures/kush_pyramids_economic
1201507515.02000decay|rubble/rubble_stone_4x4kushSmall Pyramidmr-ConquestCritical CivSpecificVillage Pyramid
- phase_villagestructures/kush_pyramid_small.png
+
+ phase_village
+ 3015interface/complete/building/complete_iber_monument.xmlfalse303000030structures/kushites/pyramid_small.xmlstructures/fndn_4x5.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/maur/tower_double.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/structures/maur/tower_double.xml (revision 27244)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/maur/tower_double.xml (revision 27245)
@@ -1,84 +1,118 @@
272001200Rampart Towerstructures/maur/tower_doubleHigher health tower with ramparts for up to 16 archers. Visibly garrisoned archers recieve a range and armor bonus. Only archers can garrison. Needs the murder holes tech to protect its foot.maurUdarka
- phase_city
+
+ phase_city
+ 4019.0
- 212.50
+ 2
+ 12.5
+ 0
- 212.52
+ 2
+ 12.5
+ 2
- 212.5-2
+ 2
+ 12.5
+ -2
- 012.52
+ 0
+ 12.5
+ 2
- 012.5-2
+ 0
+ 12.5
+ -2
- -212.50
+ -2
+ 12.5
+ 0
- -212.52
+ -2
+ 12.5
+ 2
- -212.5-2
+ -2
+ 12.5
+ -2
- 2.118.00
+ 2.1
+ 18.0
+ 0
- 2.118.02.1
+ 2.1
+ 18.0
+ 2.1
- 2.118.0-2.1
+ 2.1
+ 18.0
+ -2.1
- 018.02.1
+ 0
+ 18.0
+ 2.1
- 018.0-2.1
+ 0
+ 18.0
+ -2.1
- -2.118.00
+ -2.1
+ 18.0
+ 0
- -2.118.02.1
+ -2.1
+ 18.0
+ 2.1
- -2.118.0-2.1
+ -2.1
+ 18.0
+ -2.1structures/mauryas/tower_double.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/ptol/mercenary_camp.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/structures/ptol/mercenary_camp.xml (revision 27244)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/ptol/mercenary_camp.xml (revision 27245)
@@ -1,64 +1,66 @@
own neutralMercenaryCamp10030010010012.01200decay|rubble/rubble_stone_5x5ptolMercenary CampStratopedeia MisthophorōnMercenaryCampCheap Barracks-like structure that is buildable in neutral territory, but casts no territory influence. Train Mercenaries.structures/mercenary_camp.png
- phase_town
+
+ phase_town
+ 20020interface/complete/building/complete_gymnasium.xml1
units/{civ}/infantry_spearman_merc_b
units/{civ}/infantry_swordsman_merc_b
units/{civ}/cavalry_spearman_merc_b
units/{civ}/cavalry_javelineer_merc_b
structures/ptolemies/settlement.xmlstructures/fndn_7x7.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/rome/wallset_siege.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/structures/rome/wallset_siege.xml (revision 27244)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/rome/wallset_siege.xml (revision 27245)
@@ -1,25 +1,27 @@
romeSiege WallMūrus CircummūnītiōnisWall off an area. Build in own, neutral, or enemy territory.CivSpecificSiegeWallstructures/siege_wall.png
- phase_city
+
+ phase_city
+ structures/rome/siege_wall_towerstructures/rome/siege_wall_gatestructures/rome/army_campstructures/rome/siege_wall_longstructures/rome/siege_wall_mediumstructures/rome/siege_wall_short1.000.05
Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/kush/pyramid_large.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/structures/kush/pyramid_large.xml (revision 27244)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/kush/pyramid_large.xml (revision 27245)
@@ -1,64 +1,66 @@
structures/kush_pyramids_military
PyramidLarge30045015020.03000decay|rubble/rubble_stone_6x6kushLarge Pyramidmr ʿȝ-ConquestCritical CivSpecificCity Pyramid
- phase_citystructures/kush_pyramid_big.png
+
+ phase_city
+ 9030interface/complete/building/complete_iber_monument.xml15.0false404000040structures/kushites/pyramid_large.xmlstructures/fndn_5x7.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/maur/elephant_stable.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/structures/maur/elephant_stable.xml (revision 27244)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/maur/elephant_stable.xml (revision 27245)
@@ -1,13 +1,15 @@
maurVāraṇaśālā
- phase_townCivSpecific-City Town
+
+ phase_town
+ structures/mauryas/stable_elephant.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/pers/ice_house.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/structures/pers/ice_house.xml (revision 27244)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/pers/ice_house.xml (revision 27245)
@@ -1,72 +1,74 @@
-
-
-
- own
- Yakhchal
-
-
- 60
-
- 100
- 100
-
-
-
-
- 12.0
-
-
-
- 800
- decay|rubble/rubble_stone_3x3
-
-
- pers
- Ice House
- Yakhchāl
- -City Village IceHouse
-
- CivSpecific
-
- phase_village
- structures/yakhchal.png
-
-
- 20
- 20
-
-
-
-
-
-
-
- subterranean_aqueducts
-
-
-
-
- 1.0
-
- 2000
-
-
-
-
- interface/complete/building/complete_farmstead.xml
- attack/destruction/building_collapse_large.xml
-
-
-
- false
- 20
- 30000
-
-
- 20
-
-
- structures/persians/ice_house.xml
- structures/fndn_4x4.xml
-
-
+
+
+
+ own
+ Yakhchal
+
+
+ 60
+
+ 100
+ 100
+
+
+
+
+ 12.0
+
+
+
+ 800
+ decay|rubble/rubble_stone_3x3
+
+
+ pers
+ Ice House
+ Yakhchāl
+ -City Village IceHouse
+
+ CivSpecific
+
+ structures/yakhchal.png
+
+ phase_village
+
+
+
+ 20
+ 20
+
+
+
+
+
+
+
+ subterranean_aqueducts
+
+
+
+
+ 1.0
+
+ 2000
+
+
+
+
+ interface/complete/building/complete_farmstead.xml
+ attack/destruction/building_collapse_large.xml
+
+
+
+ false
+ 20
+ 30000
+
+
+ 20
+
+
+ structures/persians/ice_house.xml
+ structures/fndn_4x4.xml
+
+
Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/rome/temple_mars.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/structures/rome/temple_mars.xml (revision 27244)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/rome/temple_mars.xml (revision 27245)
@@ -1,26 +1,28 @@
12.010romeTemple of MarsAedēs Mārtiālis-Town City TempleOfMars
- phase_city
+
+ phase_city
+ 12structures/romans/temple_mars.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/gaul/tavern.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/structures/gaul/tavern.xml (revision 27244)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/gaul/tavern.xml (revision 27245)
@@ -1,49 +1,51 @@
2001001005.01500decay|rubble/rubble_stone_4x4gaulTavernTaberna-Village Townstructures/embassy_celtic.png
- phase_town
+
+ phase_town
+ 202010interface/complete/building/complete_broch.xml30structures/celts/tavern.xmlstructures/fndn_6x6.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/han/civil_centre.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/structures/han/civil_centre.xml (revision 27244)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/han/civil_centre.xml (revision 27245)
@@ -1,47 +1,49 @@
8.0MinisterhanGuān Shǔ
-unlock_spies
-spy_counter
units/{civ}/infantry_spearman_b
units/{civ}/infantry_crossbowman_b
units/{civ}/cavalry_swordsman_b
structures/{civ}/civil_centre_courtThis greatly increases the health, capture resistance, and garrison capacity of this specific Civic Center. Unlock training of Heroes here and reduce its research and batch training times by half.
- phase_city300300upgrading
+
+ phase_city
+ structures/fndn_8x8.xmlstructures/han/civil_centre.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/han/defense_tower.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/structures/han/defense_tower.xml (revision 27244)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/han/defense_tower.xml (revision 27245)
@@ -1,35 +1,37 @@
15.0MinisterhanFángyù Tǎ
+
+
+
+
+ 22.0
+ structures/{civ}/defense_tower_greatThis tower has greater range, greater attack, greater health, and is twice as difficult to capture.
- phase_city200upgrading
+
+ phase_city
+
-
-
-
-
- 22.0
- structures/han/tower_large.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/han/ministry.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/structures/han/ministry.xml (revision 27244)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/han/ministry.xml (revision 27245)
@@ -1,91 +1,93 @@
ImperialMinistry30005.020002002008.090UnitSupport Minister Infantry113000decay|rubble/rubble_stone_6x6hanImperial MinistryGōngdiànCivSpecificImperialMinistry TownTrain the Nine Ministers. Territory root. Research a powerful suite of Administrative technologies.
- phase_townstructures/imperial_ministry.png
+
+ phase_town
+ 200404020
pair_unlock_civil_engineering_han
pair_unlock_civil_service_han
unlock_spies
spy_counter
0.00.10.10.12000true60300000.8
units/{civ}/support_minister
80structures/fndn_8x8.xmlstructures/han/imperial_ministry.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/iber/monument.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/structures/iber/monument.xml (revision 27244)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/iber/monument.xml (revision 27245)
@@ -1,69 +1,71 @@
structures/iber_monument
MonumentMonument1501201001008.01200decay|rubble/rubble_stone_2x2iberRevered MonumentGur OroigarriCivSpecificMonument Townstructures/iberian_bull.png
- phase_town
+
+ phase_town
+ 202020303interface/complete/building/complete_iber_monument.xmlstructures/iberians/sb_1.xmlstructures/fndn_2x2.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/kush/temple_amun.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/structures/kush/temple_amun.xml (revision 27244)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/kush/temple_amun.xml (revision 27245)
@@ -1,55 +1,57 @@
structures/kush_temple_amun
TempleOfAmun2222decay|rubble/rubble_stone_6x6kushGrand Temple of AmunPr-ʿImnTrain Amun Champions and Elite Healers. Research healing technologies.CivSpecific-Town City TempleOfAmunstructures/temple_epic.png
- phase_city
+
+ phase_city
+ 22
-units/{civ}/support_healer_b
units/{civ}/support_healer_e
units/{civ}/champion_infantry_amun
structures/kushites/temple_amun.xmlstructures/fndn_9x9.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/merc_camp_egyptian.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/structures/merc_camp_egyptian.xml (revision 27244)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/merc_camp_egyptian.xml (revision 27245)
@@ -1,65 +1,67 @@
own neutralMercenaryCamp100300100010012.01200decay|rubble/rubble_stone_5x5ptolEgyptian Mercenary CampStratopedeia MisthophorōnMercenaryCampCapture this structure to train mercenaries from Hellenistic Egypt.structures/military_settlement.png
- phase_town
+
+ phase_town
+ 20020interface/complete/building/complete_gymnasium.xml1
units/{civ}/infantry_spearman_merc_b
units/{civ}/infantry_swordsman_merc_b
units/{civ}/cavalry_spearman_merc_b
units/{civ}/cavalry_javelineer_merc_b
structures/mercenaries/camp_egyptian.xmlstructures/fndn_7x7.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/rome/army_camp.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/structures/rome/army_camp.xml (revision 27244)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/rome/army_camp.xml (revision 27245)
@@ -1,128 +1,130 @@
Bow1060120020001001.550falseHumanoutline_border.pngoutline_border_mask.png0.175neutral enemyArmyCampArmyCamp803151Soldier310.0125050010010012.0200.1UnitSupport Infantry Cavalry Siege062250decay|rubble/rubble_rome_sbromeArmy CampCastraBuild in neutral or enemy territory. Train Advanced Melee Infantry. Construct Rams. Garrison Soldiers for additional arrows.ConquestCritical CivSpecificCity ArmyCampstructures/roman_camp.png
- phase_city
+
+ phase_city
+ 10015353interface/complete/building/complete_broch.xmlattack/weapon/bow_attack.xmlattack/impact/arrow_impact.xml20.7
units/{civ}/infantry_axeman_a
units/{civ}/infantry_swordsman_a
units/{civ}/infantry_spearman_a
units/{civ}/infantry_pikeman_a
units/{civ}/siege_ram
90structures/romans/camp.xmlstructures/fndn_8x8.xml29.58
Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_civic_civil_centre_military_colony.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_civic_civil_centre_military_colony.xml (revision 27244)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_civic_civil_centre_military_colony.xml (revision 27245)
@@ -1,54 +1,56 @@
own neutralColonyCivilCentre12013002002002002000decay|rubble/rubble_stone_5x5Military Colonytemplate_structure_civic_civil_centre_military_colonyColonystructures/military_settlement.png
- phase_town
+
+ phase_town
+ 404040
-phase_town_{civ}
-phase_city_{civ}
-hellenistic_metropolis
interface/complete/building/complete_gymnasium.xml80
Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_defensive_tower_artillery.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_defensive_tower_artillery.xml (revision 27244)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_defensive_tower_artillery.xml (revision 27245)
@@ -1,84 +1,86 @@
Stone90080400150450050004069.81falseprops/units/weapons/tower_artillery_projectile.xmlprops/units/weapons/tower_artillery_projectile_impact.xml0.3-Human !Organic1020020020015.051400Artillery Towertemplate_structure_defensive_tower_artilleryArtilleryTowerstructures/tower_artillery.png
- phase_city
+
+ phase_city
+ 4040
tower_health
attack/impact/siegeprojectilehit.xmlattack/siege/ballist_attack.xmlfalse3230000
Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_defensive_wall.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_defensive_wall.xml (revision 27244)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_defensive_wall.xml (revision 27245)
@@ -1,36 +1,38 @@
land-shoreWall8.0Walltemplate_structure_defensive_wallWall off your town for a stout defense.Wallstructures/wall.png
- phase_town
+
+ phase_town
+ 4.5interface/complete/building/complete_wall.xmlfalse2065535
Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_military_arsenal.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_military_arsenal.xml (revision 27244)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_military_arsenal.xml (revision 27245)
@@ -1,76 +1,78 @@
structures/arsenal_repair
18030012.05Siege2000decay|rubble/rubble_stone_5x5Arsenaltemplate_structure_military_arsenalTrain Champion Infantry Crossbowmen, construct Siege Engines, and research Siege Engine technologies.City Arsenalstructures/siege_workshop.png
- phase_city
+
+ phase_city
+ 60
siege_attack
siege_cost_time
siege_health
siege_pack_unpack
siege_bolt_accuracy
interface/complete/building/complete_barracks.xml380.7
units/{civ}/champion_infantry_crossbowman
units/{civ}/siege_scorpio_packed
units/{civ}/siege_polybolos_packed
units/{civ}/siege_oxybeles_packed
units/{civ}/siege_lithobolos_packed
units/{civ}/siege_ballista_packed
units/{civ}/siege_ram
units/{civ}/siege_tower
40structures/fndn_8x8.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_military_forge.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_military_forge.xml (revision 27244)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_military_forge.xml (revision 27245)
@@ -1,71 +1,73 @@
12020012.01Infantry Healer2000decay|rubble/rubble_stone_4x4Forgetemplate_structure_military_forgeResearch attack damage and damage resistance technologies.-ConquestCriticalTown Forgestructures/blacksmith.png
- phase_town
+
+ phase_town
+ 40
soldier_attack_melee_01
soldier_attack_melee_02
soldier_attack_melee_03
soldier_attack_melee_03_variant
soldier_attack_ranged_01
soldier_attack_ranged_02
soldier_attack_ranged_03
soldier_resistance_hack_01
soldier_resistance_hack_02
soldier_resistance_hack_03
soldier_resistance_pierce_01
soldier_resistance_pierce_02
soldier_resistance_pierce_03
archer_attack_spread
interface/complete/building/complete_forge.xml383000032structures/fndn_5x5.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_resource_corral.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_resource_corral.xml (revision 27244)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_resource_corral.xml (revision 27245)
@@ -1,81 +1,83 @@
structures/corral_garrison
501005.080.5AnimalAnimal14500decay|rubble/rubble_stone_3x3Corraltemplate_structure_resource_corralRaise Domestic Animals for immediate slaughter, or garrison them instead to gain a free trickle of food.Economic Village Corralstructures/corral.png
- phase_village
+
+ phase_village
+ 20
gather_animals_stockbreeding
20interface/complete/building/complete_corral.xmlfalse20300000.7
gaia/fauna_goat_trainable
gaia/fauna_sheep_trainable
gaia/fauna_pig_trainable
gaia/fauna_cattle_cow_trainable
20structures/fndn_3x3.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_champion_cavalry.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_champion_cavalry.xml (revision 27244)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_champion_cavalry.xml (revision 27245)
@@ -1,65 +1,67 @@
25150801006.0240FastMovingCavalryChampion Cavalry
- unlock_champion_cavalry
+
+ unlock_champion_cavalry
+ 2001581057520actor/mounted/movement/walk.xmlactor/mounted/movement/walk.xmlactor/fauna/death/death_horse.xmlinterface/alarm/alarm_create_cav.xml7.0
special/formations/wedge
21.480
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 27244)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_ship_fire.xml (revision 27245)
@@ -1,63 +1,65 @@
Fire201250100!Ship30300Circular30true600500-60.850.650.35Fire ShipUnrepairable. Gradually loses health. Can only attack Ships.Melee Warship Fireship
- phase_town
+
+ phase_town
+ 128x256/cartouche.png128x256/cartouche_mask.pngship-small1.61.660
Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/wallset_palisade.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/structures/wallset_palisade.xml (revision 27244)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/wallset_palisade.xml (revision 27245)
@@ -1,22 +1,24 @@
PalisadeWall off an area. Build in own or neutral territory.-Wall Palisadestructures/palisade_wall.png
- phase_village
+
+ phase_village
+ structures/palisades_towerstructures/palisades_gatestructures/palisades_fortstructures/palisades_longstructures/palisades_mediumstructures/palisades_shortstructures/palisades_curvestructures/palisades_end
Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_civic_temple.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_civic_temple.xml (revision 27244)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_civic_temple.xml (revision 27245)
@@ -1,76 +1,78 @@
structures/temple_heal
20030012.0200.1UnitSupport Infantry Cavalry322000decay|rubble/rubble_stone_4x6Templetemplate_structure_civic_templeTrain Healers and research healing technologies.Town Templestructures/temple.png
- phase_town
+
+ phase_town
+ 60
heal_range
heal_range_2
heal_rate
heal_rate_2
garrison_heal
health_regen_units
interface/complete/building/complete_temple.xmlfalse40300000.8
units/{civ}/support_healer_b
40structures/fndn_4x6.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_defensive_tower_stone.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_defensive_tower_stone.xml (revision 27244)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_defensive_tower_stone.xml (revision 27245)
@@ -1,59 +1,61 @@
015015010010015.051000Stone Towertemplate_structure_defensive_tower_stoneGarrison Infantry for additional arrows. Needs the “Murder Holes” technology to protect its foot.StoneTowerstructures/defense_tower.png
- phase_town
+
+ phase_town
+ 2020
tower_watch
tower_crenellations
tower_range
tower_murderholes
tower_health
false3230000
Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_economic_storehouse.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_economic_storehouse.xml (revision 27244)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_economic_storehouse.xml (revision 27245)
@@ -1,79 +1,81 @@
FemaleCitizen50100100401008.0800decay|rubble/rubble_stone_3x3Storehousetemplate_structure_economic_storehouseResearch gathering technologies.DropsiteWood DropsiteMetal DropsiteStoneVillage Storehousestructures/storehouse.png
- phase_village
+
+ phase_village
+ 20
gather_lumbering_ironaxes
gather_lumbering_strongeraxes
gather_lumbering_sharpaxes
gather_mining_servants
gather_mining_serfs
gather_mining_slaves
gather_mining_wedgemallet
gather_mining_shaftmining
gather_mining_silvermining
gather_capacity_basket
gather_capacity_wheelbarrow
gather_capacity_carts
wood stone metaltrueinterface/complete/building/complete_storehouse.xmlinterface/alarm/alarm_alert_0.xmlinterface/alarm/alarm_alert_1.xmlfalse203000020structures/fndn_3x3.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_military_embassy.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_military_embassy.xml (revision 27244)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_military_embassy.xml (revision 27245)
@@ -1,56 +1,58 @@
Embassy15012.06Support Infantry Cavalry2000decay|rubble/rubble_stone_3x3Embassytemplate_structure_military_embassyTown Embassy
- phase_town
+
+ phase_town
+ 30interface/complete/building/complete_gymnasium.xml250.824structures/fndn_4x4.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_military_stable.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_military_stable.xml (revision 27244)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_military_stable.xml (revision 27245)
@@ -1,77 +1,79 @@
structures/xp_trickle
1202005012.010Cavalry2000decay|rubble/rubble_stone_5x5Stabletemplate_structure_military_stableTrain Cavalry and research Cavalry technologies.Village Stablestructures/stable_01.png
- phase_village
+
+ phase_village
+ 4010
stable_batch_training
cavalry_movement_speed
cavalry_health
nisean_horses
unlock_champion_cavalry
unlock_champion_chariots
interface/complete/building/complete_stable.xml0.8
units/{civ}/cavalry_axeman_b
units/{civ}/cavalry_swordsman_b
units/{civ}/cavalry_spearman_b
units/{civ}/cavalry_javelineer_b
units/{civ}/cavalry_archer_b
units/{civ}/champion_cavalry
units/{civ}/champion_cavalry_spearman
units/{civ}/champion_cavalry_swordsman
units/{civ}/champion_cavalry_javelineer
units/{civ}/champion_cavalry_archer
units/{civ}/champion_chariot
units/{civ}/war_dog
32
Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_champion.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_champion.xml (revision 27244)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_champion.xml (revision 27245)
@@ -1,39 +1,41 @@
Capture541000Field Palisade WallHumanSoldier ChampionChampion Unit
- phase_city
+
+ phase_city
+ 8256x256/arrow.png256x256/arrow_mask.pngvoice/{lang}/civ/civ_{phenotype}_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.xml
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 27244)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_ship_bireme.xml (revision 27245)
@@ -1,81 +1,83 @@
Bow124510002000Ship3.0100250falseShip Human2101Infantry Cavalry2201206020Support Cavalry800Light WarshipGarrison units for transport and to increase firepower. Deals triple damage against Ships.Ranged Warship Bireme
- phase_town
+
+ phase_town
+ 802412128x512/cartouche.png128x512/cartouche_mask.pngattack/impact/arrow_impact.xmlattack/weapon/bow_attack.xml1.551.5590
Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_civic_stoa.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_civic_stoa.xml (revision 27244)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_civic_stoa.xml (revision 27245)
@@ -1,52 +1,54 @@
15010015010.0100.1UnitSupport Infantry Cavalry022500decay|rubble/rubble_stone_6x4Stoatemplate_structure_civic_stoa-ConquestCriticalTown Stoastructures/stoa.png
- phase_town
+
+ phase_town
+ 2030false406553540structures/fndn_8x4.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_defensive_tower_sentry.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_defensive_tower_sentry.xml (revision 27244)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_defensive_tower_sentry.xml (revision 27245)
@@ -1,69 +1,73 @@
0903401009.03400Sentry Towertemplate_structure_defensive_tower_sentryGarrison Infantry for additional arrows. Needs the “Murder Holes” technology to protect its foot.SentryTowerstructures/sentry_tower.png
- phase_village
+
+ phase_village
+ 20
tower_watch
false1630000structures/{civ}/defense_towerReinforce with stone and upgrade to a defense tower.
- phase_town50100upgrading
+
+ phase_town
+
Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_economic_market.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_economic_market.xml (revision 27244)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_economic_market.xml (revision 27245)
@@ -1,76 +1,78 @@
Trader+!Ship-1-11001503008.01500decay|rubble/rubble_stone_5x5Markettemplate_structure_economic_marketBarter resources. Establish trade routes. Train Traders and research trade and barter technologies.BarterTrade Town Marketstructures/market.png
- phase_town
+
+ phase_town
+ 60land0.2
trader_health
trade_gain_01
trade_gain_02
trade_commercial_treaty
interface/complete/building/complete_market.xmlinterface/alarm/alarm_alert_0.xmlinterface/alarm/alarm_alert_1.xmlfalse40300000.7
units/{civ}/support_trader
32structures/fndn_8x8.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_military_elephant_stable.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_military_elephant_stable.xml (revision 27244)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_military_elephant_stable.xml (revision 27245)
@@ -1,64 +1,66 @@
structures/xp_trickle
1802002008.05Elephant3000decay|rubble/rubble_stone_6x6Elephant Stabletemplate_structure_military_elephant_stableTrain Elephants and research Elephant technologies.City ElephantStable
- phase_citystructures/stable_elephant.png
+
+ phase_city
+ 4040interface/complete/building/complete_elephant_stable.xml380.7
units/{civ}/support_elephant
units/{civ}/elephant_archer_b
units/{civ}/champion_elephant
40structures/fndn_9x8.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_military_range.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_military_range.xml (revision 27244)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_military_range.xml (revision 27245)
@@ -1,61 +1,63 @@
12020012.010Infantry2000decay|rubble/rubble_stone_5x5Practice Rangetemplate_structure_military_rangeTrain Ranged Infantry and research technologies.Village Rangestructures/range.png
- phase_village
+
+ phase_village
+ 40interface/complete/building/complete_range.xml0.8
units/{civ}/infantry_javelineer_b
units/{civ}/infantry_slinger_b
units/{civ}/infantry_archer_b
units/{civ}/infantry_crossbowman_b
units/{civ}/champion_infantry_javelineer
units/{civ}/champion_infantry_slinger
units/{civ}/champion_infantry_archer
units/{civ}/champion_infantry_crossbowman
32structures/fndn_7x7.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_wonder.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_wonder.xml (revision 27244)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_wonder.xml (revision 27245)
@@ -1,99 +1,101 @@
structures/wonder_population_cap
Wonder4100010001500100010.0500.1UnitSupport Soldier525000decay|rubble/rubble_stone_6x6Wondertemplate_structure_wonderBring glory to your civilization and add large tracts of land to your empire.ConquestCriticalCity Wonderstructures/wonder.png
- phase_city
+
+ phase_city
+ 200300200structurewonder.png
wonder_population_cap
152531.01.01.01.02000interface/complete/building/complete_wonder.xmltrue1006553572structures/fndn_9x9.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_hero.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_hero.xml (revision 27244)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_hero.xml (revision 27245)
@@ -1,70 +1,72 @@
Capture1041000Field Palisade Wall
units/heroes/hero_garrison
050100250HumanSoldier HeroHerotechnologies/laurel_wreath.png
- phase_city
+
+ phase_city
+ 400100025hero256x256/star.png256x256/star_mask.pngvoice/{lang}/civ/civ_{phenotype}_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.xmlinterface/alarm/alarm_create_infantry.xmlactor/human/movement/walk.xmlactor/human/movement/walk.xmlactor/human/death/{phenotype}_death.xmlHero1100
Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_civic_house.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_civic_house.xml (revision 27244)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_civic_house.xml (revision 27245)
@@ -1,75 +1,77 @@
30755.0300.1UnitSupport+!Elephant1800decay|rubble/rubble_stone_2x2Housetemplate_structure_civic_houseVillage Housestructures/house.png
- phase_village
+
+ phase_village
+ 155
health_females_01
pop_house_01
pop_house_02
unlock_females_house
interface/complete/building/complete_house.xml8.0false1665535
units/{civ}/support_female_citizen_house
20structures/fndn_3x3.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_defensive_tower_bolt.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_defensive_tower_bolt.xml (revision 27244)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_defensive_tower_bolt.xml (revision 27245)
@@ -1,81 +1,83 @@
Bolt10090300150500400015019.81falseprops/units/weapons/tower_artillery_projectile_impact.xml0.11020020010015.051400Bolt Towertemplate_structure_defensive_tower_boltBoltTowerstructures/tower_bolt.png
- phase_city
+
+ phase_city
+ 4020
tower_health
attack/weapon/arrowfly.xmlattack/impact/arrow_metal.xmlfalse3230000
Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_economic_farmstead.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_economic_farmstead.xml (revision 27244)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_economic_farmstead.xml (revision 27245)
@@ -1,74 +1,76 @@
FemaleCitizen50100100300451008.0900decay|rubble/rubble_stone_4x4Farmsteadtemplate_structure_economic_farmsteadResearch food gathering technologies.DropsiteFoodVillage Farmsteadstructures/farmstead.png
- phase_village
+
+ phase_village
+ 20
gather_wicker_baskets
gather_farming_plows
gather_farming_training
gather_farming_fertilizer
gather_farming_seed_drill
gather_farming_water_weeding
gather_farming_chain_pump
gather_farming_harvester
foodtrueinterface/complete/building/complete_farmstead.xmlinterface/alarm/alarm_alert_0.xmlinterface/alarm/alarm_alert_1.xmlfalse203000020structures/fndn_5x5.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_military_barracks.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_military_barracks.xml (revision 27244)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_military_barracks.xml (revision 27245)
@@ -1,81 +1,83 @@
structures/xp_trickle
15020010012.010Infantry2000decay|rubble/rubble_stone_4x4Barrackstemplate_structure_military_barracksTrain Infantry and research Infantry technologies.Village Barracksstructures/barracks.png
- phase_village
+
+ phase_village
+ 4020
barracks_batch_training
unlock_champion_infantry
pair_unlock_champions_sele
interface/complete/building/complete_barracks.xml0.8
units/{civ}/infantry_spearman_b
units/{civ}/infantry_pikeman_b
units/{civ}/infantry_maceman_b
units/{civ}/infantry_axeman_b
units/{civ}/infantry_swordsman_b
units/{civ}/infantry_javelineer_b
units/{civ}/infantry_slinger_b
units/{civ}/infantry_archer_b
units/{civ}/champion_infantry_spearman
units/{civ}/champion_infantry_pikeman
units/{civ}/champion_infantry_maceman
units/{civ}/champion_infantry_axeman
units/{civ}/champion_infantry_swordsman
units/{civ}/champion_infantry_javelineer
units/{civ}/champion_infantry_slinger
units/{civ}/champion_infantry_archer
32structures/fndn_6x6.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_military_fortress.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_military_fortress.xml (revision 27244)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_military_fortress.xml (revision 27245)
@@ -1,112 +1,114 @@
Bow1060120020001001.550falseHumanoutline_border.pngoutline_border_mask.png0.175FortressFortress8041Soldier810.04503006008.0200.075Support Infantry Cavalry Siege65200decay|rubble/rubble_stone_6x6Fortresstemplate_structure_military_fortressGarrison Soldiers for additional arrows.GarrisonFortressDefensive Fortressstructures/fortress.png
- phase_city
+
+ phase_city
+ 60120
attack_soldiers_will
art_of_war
poison_arrows
poison_blades
interface/complete/building/complete_fortress.xmlattack/weapon/bow_attack.xmlattack/impact/arrow_impact.xml2800.890structures/fndn_8x8.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_special.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_special.xml (revision 27244)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_special.xml (revision 27245)
@@ -1,38 +1,40 @@
8.050.1UnitSupport Infantry Cavalry02decay|rubble/rubble_stone_6x6Special StructureCity
- phase_city
+
+ phase_city
+ 20303structures/fndn_5x5.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_fauna_herd_domestic_cattle.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_fauna_herd_domestic_cattle.xml (revision 27244)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_fauna_herd_domestic_cattle.xml (revision 27245)
@@ -1,22 +1,24 @@
- phase_city
+
+ phase_city
+ 2000actor/fauna/animal/cattle_order.xmlactor/fauna/animal/cattle_death.xmlactor/fauna/animal/cattle_trained.xml0.41.40.4
Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_siege.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_siege.xml (revision 27244)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_siege.xml (revision 27245)
@@ -1,74 +1,76 @@
30.00.02.0true-OrganicSiegeSiege
- phase_city
+
+ phase_city
+ pitch-roll44.00.011255128x256/rounded_rectangle.png128x256/rounded_rectangle_mask.pngattack/siege/ram_move.xmlattack/siege/ram_move.xmlattack/siege/ram_move.xmlattack/siege/ram_trained.xml4.00.5falselarge10.750.155.0
Index: ps/trunk/binaries/data/mods/public/simulation/templates/units/athen/champion_marine.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/units/athen/champion_marine.xml (revision 27244)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/units/athen/champion_marine.xml (revision 27245)
@@ -1,12 +1,14 @@
Athenian MarineEpibátēs Athēnaîosunits/athen/champion_marine.png
- iphicratean_reforms
+
+ iphicratean_reforms
+ units/athenians/infantry_swordsman_c.xml
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 27244)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_ship_trireme.xml (revision 27245)
@@ -1,84 +1,86 @@
Bow125510002000Ship3.0100250falseShip Human3131Infantry Cavalry32520010030Support Soldier Siege1400Medium WarshipGarrison units for transport and to increase firepower. Deals triple damage against Ships.Ranged Warship Trireme
- phase_town
+
+ phase_town
+ 14040204128x512/cartouche.png128x512/cartouche_mask.pngattack/impact/arrow_impact.xmlattack/weapon/bow_attack.xml1.81.890
Index: ps/trunk/binaries/data/mods/public/simulation/templates/units/athen/cavalry_swordsman_b.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/units/athen/cavalry_swordsman_b.xml (revision 27244)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/units/athen/cavalry_swordsman_b.xml (revision 27245)
@@ -1,21 +1,23 @@
Swordunits/athen/cavalry_swordsman_bGreek CavalryHippeúsunits/athen/cavalry_swordsman.png
- phase_town
+
+ phase_town
+ units/athen/cavalry_swordsman_aunits/athenians/cavalry_swordsman_b_m.xml
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 27244)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_ship_quinquereme.xml (revision 27245)
@@ -1,87 +1,89 @@
Stone150100402000500040620falseShip Structureoutline_border.pngoutline_border_mask.png0.1751101StoneThrower53060030010.050Support Soldier Siege2000Heavy WarshipGarrison units for transport and to increase firepower.Ranged Warship Quinquereme
- phase_city
+
+ phase_city
+ 200120604128x512/cartouche.png128x512/cartouche_mask.pngattack/siege/ballist_attack.xml1.81.8110
Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_wallset.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/template_wallset.xml (revision 27244)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/template_wallset.xml (revision 27245)
@@ -1,26 +1,28 @@
gaiaWallWall off an area.Wallstructures/wall.png
- phase_towntrue
+
+ phase_town
+ falsefalsefalsefalse0.850.05
Index: ps/trunk/binaries/data/mods/public/simulation/templates/units/athen/infantry_marine_archer_b.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/units/athen/infantry_marine_archer_b.xml (revision 27244)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/units/athen/infantry_marine_archer_b.xml (revision 27245)
@@ -1,16 +1,18 @@
units/athen/infantry_marine_archer_bCretan Mercenary ArcherToxótēs Krētikósunits/mace/infantry_archer.png
- iphicratean_reforms
+
+ iphicratean_reforms
+ units/athen/infantry_marine_archer_aunits/athenians/infantry_archer_b.xml
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 27244)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_ship_merchant.xml (revision 27245)
@@ -1,58 +1,60 @@
2010015Support Cavalry240Merchantmantemplate_unit_ship_merchantTrade between docks. Garrison a Trader aboard for additional profit (+20% for each garrisoned). Gather profitable aquatic treasures.-ConquestCriticalTrader Bribable
- phase_town
+
+ phase_town
+ 20128x256/ellipse.png128x256/ellipse_mask.png0.750.212passivefalsefalseship-small1.351.650true
Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_support_healer.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_support_healer.xml (revision 27244)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_support_healer.xml (revision 27245)
@@ -1,57 +1,59 @@
1525012heal_overlay_range.pngheal_overlay_range_mask.png0.3552000Human85-ConquestCriticalHealerHealertemplate_unit_support_healerBasic
- phase_townHeal units.
+
+ phase_town
+ 825150128x128/plus.png128x128/plus_mask.pngvoice/{lang}/civ/civ_{phenotype}_heal.xmlinterface/alarm/alarm_create_priest.xml30
Index: ps/trunk/binaries/data/mods/public/simulation/templates/units/athen/infantry_javelineer_b.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/units/athen/infantry_javelineer_b.xml (revision 27244)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/units/athen/infantry_javelineer_b.xml (revision 27245)
@@ -1,16 +1,18 @@
units/athen/infantry_javelineer_bThracian PeltastPeltastḗs Thrâxunits/athen/infantry_javelinist.png
- phase_town
+
+ phase_town
+ units/athen/infantry_javelineer_aunits/athenians/infantry_javelinist_b.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/units/brit/cavalry_swordsman_b.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/units/brit/cavalry_swordsman_b.xml (revision 27244)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/units/brit/cavalry_swordsman_b.xml (revision 27245)
@@ -1,17 +1,19 @@
britunits/brit/cavalry_swordsman_bEporedosCeltic Cavalryunits/brit/cavalry_swordsman.png
- phase_town
+
+ phase_town
+ units/brit/cavalry_swordsman_aunits/britons/cavalry_swordsman_b_m.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/units/brit/support_female_citizen_house.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/units/brit/support_female_citizen_house.xml (revision 27244)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/units/brit/support_female_citizen_house.xml (revision 27245)
@@ -1,9 +1,11 @@
30
- unlock_females_house
+
+ unlock_females_house
+
Index: ps/trunk/binaries/data/mods/public/simulation/templates/units/cart/infantry_javelineer_iber_b.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/units/cart/infantry_javelineer_iber_b.xml (revision 27244)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/units/cart/infantry_javelineer_iber_b.xml (revision 27245)
@@ -1,25 +1,27 @@
structures/{civ}/super_dock
structures/{civ}/embassy_celtic
structures/{civ}/embassy_iberian
structures/{civ}/embassy_italic
cartIberian Mercenary SkirmisherSǝḫīr Kidōnunits/cart/infantry_javelineer_iber_bunits/cart/infantry_javelinist.png
- phase_town
+
+ phase_town
+ units/cart/infantry_javelineer_iber_aunits/iberians/infantry_javelinist_b.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/units/cart/ship_merchant.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/units/cart/ship_merchant.xml (revision 27244)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/units/cart/ship_merchant.xml (revision 27245)
@@ -1,28 +1,30 @@
8.0cartSeḥerTrade between docks. Garrison a Trader aboard for additional profit (+20% for each garrisoned). Gather profitable aquatic treasures. Carthaginians have +25% sea trading bonus.units/cart/ship_merchant.png
- phase_village
+
+ phase_village
+ 128x512/ellipse.png128x512/ellipse_mask.png1.25structures/carthaginians/merchant_ship.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/units/gaul/champion_infantry_swordsman.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/units/gaul/champion_infantry_swordsman.xml (revision 27244)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/units/gaul/champion_infantry_swordsman.xml (revision 27245)
@@ -1,12 +1,14 @@
gaulSolidurosunits/gaul/champion_infantry.png
- unlock_champion_infantry
+
+ unlock_champion_infantry
+ units/gauls/infantry_swordsman_c.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/units/han/cavalry_crossbowman_b.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/units/han/cavalry_crossbowman_b.xml (revision 27244)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/units/han/cavalry_crossbowman_b.xml (revision 27245)
@@ -1,16 +1,18 @@
hanunits/han/cavalry_crossbowman_bHan Cavalry Crossbowmanunits/han/cavalry_crossbowman.png
- phase_town
+
+ phase_town
+ units/han/cavalry_crossbowman_aunits/han/cavalry_crossbowman_b_m.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/units/han/champion_infantry_archer_academy.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/units/han/champion_infantry_archer_academy.xml (revision 27244)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/units/han/champion_infantry_archer_academy.xml (revision 27245)
@@ -1,14 +1,16 @@
hanPalace Guard ArcherYǔ Línunits/han/champion_infantry_archerunits/han/champion_infantry_archer.png
- phase_city
+
+ phase_city
+ units/han/infantry_archer_c.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/units/han/infantry_pikeman_b.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/units/han/infantry_pikeman_b.xml (revision 27244)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/units/han/infantry_pikeman_b.xml (revision 27245)
@@ -1,44 +1,46 @@
Ji11
-structures/wallset_palisade
hanunits/han/infantry_pikeman_bHalberdierJǐ Bīngunits/han/infantry_halberdman.png
- phase_town
+
+ phase_town
+ units/han/infantry_pikeman_a
-
- units/han/infantry_halberdman_b.xml
- -2-2
special/formations/anti_cavalry
+
+ units/han/infantry_halberdman_b.xml
+
Index: ps/trunk/binaries/data/mods/public/simulation/templates/units/athen/support_female_citizen_house.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/units/athen/support_female_citizen_house.xml (revision 27244)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/units/athen/support_female_citizen_house.xml (revision 27245)
@@ -1,9 +1,11 @@
30
- unlock_females_house
+
+ unlock_females_house
+
Index: ps/trunk/binaries/data/mods/public/simulation/templates/units/brit/infantry_javelineer_b.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/units/brit/infantry_javelineer_b.xml (revision 27244)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/units/brit/infantry_javelineer_b.xml (revision 27245)
@@ -1,23 +1,25 @@
0.800.500.20britunits/brit/infantry_javelineer_bAdretosunits/brit/infantry_javelinist.png
- phase_town
+
+ phase_town
+ units/brit/infantry_javelineer_aunits/britons/infantry_javelinist_b.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/units/cart/cavalry_swordsman_iber_b.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/units/cart/cavalry_swordsman_iber_b.xml (revision 27244)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/units/cart/cavalry_swordsman_iber_b.xml (revision 27245)
@@ -1,22 +1,24 @@
SwordcartIberian Heavy CavalryḤayyāl Ḥerev Raḫūvunits/cart/cavalry_swordsman_iber_bunits/cart/cavalry_swordsman.png
- phase_town
+
+ phase_town
+ units/cart/cavalry_swordsman_iber_aunits/iberians/cavalry_swordsman_b_m.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/units/cart/infantry_swordsman_ital_b.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/units/cart/infantry_swordsman_ital_b.xml (revision 27244)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/units/cart/infantry_swordsman_ital_b.xml (revision 27245)
@@ -1,25 +1,27 @@
structures/{civ}/super_dock
structures/{civ}/embassy_celtic
structures/{civ}/embassy_iberian
structures/{civ}/embassy_italic
cartSamnite SwordsmanSeḫīr Romaḥunits/cart/infantry_swordsman_ital_bunits/cart/infantry_swordsman_2.png
- phase_town
+
+ phase_town
+ units/cart/infantry_swordsman_ital_aunits/carthaginians/infantry_swordsman_b.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/units/gaul/champion_fanatic.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/units/gaul/champion_fanatic.xml (revision 27244)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/units/gaul/champion_fanatic.xml (revision 27245)
@@ -1,37 +1,39 @@
1201000gaulNaked FanaticBariogaisatosunits/gaul/champion_fanatic.png
- phase_town
+
+ phase_town
+ 12100-3-41.41.4units/gauls/infantry_spearman_c.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/units/han/cavalry_archer_b.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/units/han/cavalry_archer_b.xml (revision 27244)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/units/han/cavalry_archer_b.xml (revision 27245)
@@ -1,16 +1,18 @@
hanunits/han/cavalry_archer_bGōng Qíbīngunits/han/cavalry_archer.png
- phase_town
+
+ phase_town
+ units/han/cavalry_archer_aunits/han/cavalry_archer_b_m.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/units/han/champion_chariot_academy.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/units/han/champion_chariot_academy.xml (revision 27244)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/units/han/champion_chariot_academy.xml (revision 27245)
@@ -1,18 +1,20 @@
5.0hanHan War ChariotZhancheChariot
- phase_cityunits/han/chariot.png
+
+ phase_city
+ units/han/chariot_archer_m.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/units/han/infantry_archer_b.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/units/han/infantry_archer_b.xml (revision 27244)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/units/han/infantry_archer_b.xml (revision 27245)
@@ -1,21 +1,23 @@
-structures/wallset_palisade
hanunits/han/infantry_archer_bShè Shǒuunits/han/infantry_archer.png
- phase_town
+
+ phase_town
+ units/han/infantry_archer_aunits/han/infantry_archer_b.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/units/han/support_minister.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/units/han/support_minister.xml (revision 27244)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/units/han/support_minister.xml (revision 27245)
@@ -1,107 +1,109 @@
Capture841000Field Palisade WallSword1035001000Unit+!Ship
units/han_minister_garrison
units/han_minister_gathering
units/han_minister_building
units/han_minister_garrison_ministry
215100100200hanImperial MinisterGuānlìUse to boost the efficiency of nearby units and buildings. Garrison within a building to boost the efficiency of its production queue. Only Han buildings can garrison ministers.Organic HumanMinisterunits/han/minister.png
- phase_town
+
+ phase_town
+ 1501010hero8246128x128/octagram.png128x128/octagram_mask.pngattack/weapon/sword_attack.xmlresource/construction/con_wood.xmlactor/human/death/{phenotype}_death.xmlvoice/{lang}/civ/civ_{phenotype}_attack.xmlvoice/{lang}/civ/civ_{phenotype}_attack.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/run.xmlinterface/alarm/alarm_create_infantry.xmlactor/human/movement/walk.xmlMinister40units/han/minister.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/units/brit/champion_infantry_swordsman.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/units/brit/champion_infantry_swordsman.xml (revision 27244)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/units/brit/champion_infantry_swordsman.xml (revision 27245)
@@ -1,13 +1,15 @@
britBrythonic ChampionArgosunits/brit/champion_infantry.png
- unlock_champion_infantry
+
+ unlock_champion_infantry
+ units/britons/infantry_swordsman_c.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/units/cart/cavalry_swordsman_gaul_b.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/units/cart/cavalry_swordsman_gaul_b.xml (revision 27244)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/units/cart/cavalry_swordsman_gaul_b.xml (revision 27245)
@@ -1,17 +1,19 @@
cartGallic Mercenary CavalryḤayyāl Ḥerev Raḫūvunits/cart/cavalry_swordsman_gaul_bunits/cart/cavalry_swordsman_2.png
- phase_town
+
+ phase_town
+ units/cart/cavalry_swordsman_gaul_aunits/gauls/cavalry_swordsman_b_m.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/units/cart/infantry_swordsman_gaul_b.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/units/cart/infantry_swordsman_gaul_b.xml (revision 27244)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/units/cart/infantry_swordsman_gaul_b.xml (revision 27245)
@@ -1,25 +1,27 @@
structures/{civ}/super_dock
structures/{civ}/embassy_celtic
structures/{civ}/embassy_iberian
structures/{civ}/embassy_italic
cartGallic Mercenary SwordsmanSeḫīr Ḥerevunits/cart/infantry_swordsman_gaul_bunits/cart/infantry_swordsman.png
- phase_town
+
+ phase_town
+ units/cart/infantry_swordsman_gaul_aunits/gauls/infantry_swordsman_b.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/units/gaul/cavalry_swordsman_b.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/units/gaul/cavalry_swordsman_b.xml (revision 27244)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/units/gaul/cavalry_swordsman_b.xml (revision 27245)
@@ -1,16 +1,18 @@
gaulunits/gaul/cavalry_swordsman_bEporedosunits/gaul/cavalry_swordsman.png
- phase_town
+
+ phase_town
+ units/gaul/cavalry_swordsman_aunits/gauls/cavalry_swordsman_b_m.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/units/gaul/support_female_citizen_house.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/units/gaul/support_female_citizen_house.xml (revision 27244)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/units/gaul/support_female_citizen_house.xml (revision 27245)
@@ -1,9 +1,11 @@
30
- unlock_females_house
+
+ unlock_females_house
+
Index: ps/trunk/binaries/data/mods/public/simulation/templates/units/han/champion_cavalry_spearman_academy.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/units/han/champion_cavalry_spearman_academy.xml (revision 27244)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/units/han/champion_cavalry_spearman_academy.xml (revision 27245)
@@ -1,13 +1,15 @@
hanWu Wei Yin Cao Cao Guardunits/han/champion_cavalryunits/han/champion_cavalry_spearman.png
- phase_city
+
+ phase_city
+ units/han/cavalry_spearman_c_m.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/units/han/champion_infantry_spearman_academy.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/units/han/champion_infantry_spearman_academy.xml (revision 27244)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/units/han/champion_infantry_spearman_academy.xml (revision 27245)
@@ -1,14 +1,16 @@
hanPalace Guard SpearmanHǔ Bēnunits/han/champion_infantry_spearman
- phase_cityunits/han/champion_infantry_swordsman.png
+
+ phase_city
+ units/han/infantry_spearman_c.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/units/han/support_female_citizen_house.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/units/han/support_female_citizen_house.xml (revision 27244)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/units/han/support_female_citizen_house.xml (revision 27245)
@@ -1,9 +1,11 @@
30
- unlock_females_house
+
+ unlock_females_house
+
Index: ps/trunk/binaries/data/mods/public/simulation/templates/units/brit/champion_chariot.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/units/brit/champion_chariot.xml (revision 27244)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/units/brit/champion_chariot.xml (revision 27245)
@@ -1,19 +1,21 @@
6.0britCeltic ChariotEssedonunits/brit/champion_chariotChariotunits/brit/champion_chariot.png
- unlock_champion_chariots
+
+ unlock_champion_chariots
+ units/britons/chariot_javelinist_c_m.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/units/cart/cavalry_spearman_ital_b.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/units/cart/cavalry_spearman_ital_b.xml (revision 27244)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/units/cart/cavalry_spearman_ital_b.xml (revision 27245)
@@ -1,17 +1,19 @@
cartItalic CavalryḤayyāl Romaḥ Raḫūvunits/cart/cavalry_spearman_ital_bunits/cart/cavalry_spearman.png
- phase_town
+
+ phase_town
+ units/cart/cavalry_spearman_ital_aunits/carthaginians/cavalry_spearman_b_m.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/units/cart/infantry_slinger_iber_b.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/units/cart/infantry_slinger_iber_b.xml (revision 27244)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/units/cart/infantry_slinger_iber_b.xml (revision 27245)
@@ -1,25 +1,27 @@
structures/{civ}/super_dock
structures/{civ}/embassy_celtic
structures/{civ}/embassy_iberian
structures/{civ}/embassy_italic
cartBalearic SlingerQallāʿ Ibušimiunits/cart/infantry_slinger_iber_bunits/cart/infantry_slinger.png
- phase_town
+
+ phase_town
+ units/cart/infantry_slinger_iber_aunits/iberians/infantry_slinger_b.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/units/cart/support_female_citizen_house.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/units/cart/support_female_citizen_house.xml (revision 27244)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/units/cart/support_female_citizen_house.xml (revision 27245)
@@ -1,9 +1,11 @@
30
- unlock_females_house
+
+ unlock_females_house
+
Index: ps/trunk/binaries/data/mods/public/simulation/templates/units/gaul/infantry_slinger_b.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/units/gaul/infantry_slinger_b.xml (revision 27244)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/units/gaul/infantry_slinger_b.xml (revision 27245)
@@ -1,21 +1,23 @@
structures/gaul/assembly
gaulunits/gaul/infantry_slinger_bTalmorisunits/gaul/infantry_slinger.png
- phase_town
+
+ phase_town
+ units/gaul/infantry_slinger_aunits/gauls/infantry_slinger_b.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/units/han/cavalry_spearman_b.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/units/han/cavalry_spearman_b.xml (revision 27244)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/units/han/cavalry_spearman_b.xml (revision 27245)
@@ -1,16 +1,18 @@
hanunits/han/cavalry_spearman_bMáo Qíbīngunits/han/cavalry_spearman.png
- phase_town
+
+ phase_town
+ units/han/cavalry_spearman_aunits/han/cavalry_spearman_b_m.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/units/han/champion_infantry_crossbowman_academy.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/units/han/champion_infantry_crossbowman_academy.xml (revision 27244)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/units/han/champion_infantry_crossbowman_academy.xml (revision 27245)
@@ -1,12 +1,14 @@
hanJuezhangunits/han/champion_infantry_crossbowman.png
- phase_city
+
+ phase_city
+ units/han/infantry_crossbowman_c.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/units/han/infantry_swordsman_special_b.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/units/han/infantry_swordsman_special_b.xml (revision 27244)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/units/han/infantry_swordsman_special_b.xml (revision 27245)
@@ -1,21 +1,23 @@
-
-
-
-
- -structures/wallset_palisade
-
-
-
- han
- units/han/infantry_swordsman_b
- Dāo Bīng
- units/han/infantry_swordsman.png
- phase_town
-
-
- units/han/infantry_swordsman_special_a
-
-
- units/han/infantry_swordsman_b.xml
-
-
+
+
+
+
+ -structures/wallset_palisade
+
+
+
+ han
+ units/han/infantry_swordsman_b
+ Dāo Bīng
+ units/han/infantry_swordsman.png
+
+ phase_town
+
+
+
+ units/han/infantry_swordsman_special_a
+
+
+ units/han/infantry_swordsman_b.xml
+
+
Index: ps/trunk/binaries/data/mods/public/simulation/templates/units/iber/cavalry_spearman_b.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/units/iber/cavalry_spearman_b.xml (revision 27244)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/units/iber/cavalry_spearman_b.xml (revision 27245)
@@ -1,16 +1,18 @@
iberunits/iber/cavalry_spearman_bLantzariunits/iber/cavalry_spearman.png
- phase_town
+
+ phase_town
+ units/iber/cavalry_spearman_aunits/iberians/cavalry_spearman_b_m.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/units/iber/support_female_citizen_house.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/units/iber/support_female_citizen_house.xml (revision 27244)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/units/iber/support_female_citizen_house.xml (revision 27245)
@@ -1,9 +1,11 @@
30
- unlock_females_house
+
+ unlock_females_house
+
Index: ps/trunk/binaries/data/mods/public/simulation/templates/units/kush/champion_infantry_archer.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/units/kush/champion_infantry_archer.xml (revision 27244)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/units/kush/champion_infantry_archer.xml (revision 27245)
@@ -1,14 +1,16 @@
kushnapatanNoble ArcherHry pdtyunits/kush/champion_archer.png
- unlock_champion_infantry
+
+ unlock_champion_infantry
+ units/kushites/infantry_archer_c.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/units/iber/infantry_spearman_b.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/units/iber/infantry_spearman_b.xml (revision 27244)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/units/iber/infantry_spearman_b.xml (revision 27245)
@@ -1,21 +1,23 @@
structures/iber/monument
iberunits/iber/infantry_spearman_bEzkutariunits/iber/infantry_spearman.png
- phase_town
+
+ phase_town
+ units/iber/infantry_spearman_aunits/iberians/infantry_spearman_b.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/units/kush/cavalry_spearman_b.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/units/kush/cavalry_spearman_b.xml (revision 27244)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/units/kush/cavalry_spearman_b.xml (revision 27245)
@@ -1,18 +1,20 @@
kushnapatanunits/kush/cavalry_spearman_bMeroitic Heavy CavalryHtrunits/kush/cavalry_spearman.png
- phase_town
+
+ phase_town
+ units/kush/cavalry_spearman_aunits/kushites/cavalry_spearman_b_m.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/units/iber/infantry_slinger_b.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/units/iber/infantry_slinger_b.xml (revision 27244)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/units/iber/infantry_slinger_b.xml (revision 27245)
@@ -1,21 +1,23 @@
structures/iber/monument
iberunits/iber/infantry_slinger_bHabailariunits/iber/infantry_slinger.png
- phase_town
+
+ phase_town
+ units/iber/infantry_slinger_aunits/iberians/infantry_slinger_b.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/units/kush/cavalry_javelineer_merc_b.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/units/kush/cavalry_javelineer_merc_b.xml (revision 27244)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/units/kush/cavalry_javelineer_merc_b.xml (revision 27245)
@@ -1,39 +1,41 @@
7.5kushnapatanunits/kush/cavalry_javelineer_merc_bBlemmye Desert Raidernhw Bulahau gmlCamelunits/kush/camel_javelinist.png
- phase_town
+
+ phase_town
+ units/kush/cavalry_javelineer_merc_aactor/fauna/movement/camel_order.xmlactor/fauna/death/death_camel.xml8.5units/kushites/camel_javelinist_b_m.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/units/kush/infantry_javelineer_merc_b.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/units/kush/infantry_javelineer_merc_b.xml (revision 27244)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/units/kush/infantry_javelineer_merc_b.xml (revision 27245)
@@ -1,26 +1,28 @@
structures/kush/pyramid_large
structures/kush/temple_amun
structures/kush/camp_blemmye
structures/kush/camp_noba
kushnapatanunits/kush/infantry_javelineer_merc_bNoba Skirmishernhw ʿhȝw Nobaunits/kush/infantry_javelinist.png
- phase_town
+
+ phase_town
+ units/kush/infantry_javelineer_merc_aunits/kushites/infantry_javelinist_b.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/units/iber/champion_infantry_swordsman.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/units/iber/champion_infantry_swordsman.xml (revision 27244)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/units/iber/champion_infantry_swordsman.xml (revision 27245)
@@ -1,17 +1,19 @@
SwordiberLeial Ezpatariunits/iber/champion_infantry.png
- unlock_champion_infantry
+
+ unlock_champion_infantry
+ units/iberians/infantry_swordsman_c.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/units/kush/cavalry_javelineer_b.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/units/kush/cavalry_javelineer_b.xml (revision 27244)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/units/kush/cavalry_javelineer_b.xml (revision 27245)
@@ -1,18 +1,20 @@
kushnapatanunits/kush/cavalry_javelineer_biry hr ssmwttNapatan Light Cavalry
- phase_villageunits/kush/cavalry_javelinist.png
+
+ phase_village
+ units/kush/cavalry_javelineer_aunits/kushites/cavalry_javelinist_b_m.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/units/kush/infantry_archer_b.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/units/kush/infantry_archer_b.xml (revision 27244)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/units/kush/infantry_archer_b.xml (revision 27245)
@@ -1,26 +1,28 @@
structures/kush/pyramid_large
structures/kush/temple_amun
structures/kush/camp_blemmye
structures/kush/camp_noba
kushnapatanunits/kush/infantry_archer_bNubian ArcherPdty Nhsyw
- phase_villageunits/kush/infantry_archer.png
+
+ phase_village
+ units/kush/infantry_archer_aunits/kushites/infantry_archer_b.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/units/kush/infantry_pikeman_b.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/units/kush/infantry_pikeman_b.xml (revision 27244)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/units/kush/infantry_pikeman_b.xml (revision 27245)
@@ -1,26 +1,28 @@
structures/kush/pyramid_large
structures/kush/temple_amun
structures/kush/camp_blemmye
structures/kush/camp_noba
kushnapatanunits/kush/infantry_pikeman_bMeroitic Pikemansiȝwrdunits/kush/infantry_pikeman.png
- phase_town
+
+ phase_town
+ units/kush/infantry_pikeman_aunits/kushites/infantry_pikeman_b.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/units/mace/cavalry_javelineer_b.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/units/mace/cavalry_javelineer_b.xml (revision 27244)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/units/mace/cavalry_javelineer_b.xml (revision 27245)
@@ -1,18 +1,20 @@
macegreekunits/mace/cavalry_javelineer_bOdrysian Skirmish CavalryHippakontistḕs Odrysósunits/mace/cavalry_javelinist.png
- phase_town
+
+ phase_town
+ units/mace/cavalry_javelineer_aunits/macedonians/cavalry_javelinist_b_m.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/units/mace/infantry_slinger_b.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/units/mace/infantry_slinger_b.xml (revision 27244)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/units/mace/infantry_slinger_b.xml (revision 27245)
@@ -1,18 +1,20 @@
macegreekunits/mace/infantry_slinger_bRhodian SlingerSphendonḗtēs Rhódiosunits/mace/infantry_slinger.png
- phase_town
+
+ phase_town
+ units/mace/infantry_slinger_aunits/macedonians/infantry_slinger_b.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/units/maur/champion_infantry_maceman.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/units/maur/champion_infantry_maceman.xml (revision 27244)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/units/maur/champion_infantry_maceman.xml (revision 27245)
@@ -1,14 +1,16 @@
maurWarriorYōddhaunits/maur/champion_infantryunits/maur/champion_maceman.png
- unlock_champion_infantry
+
+ unlock_champion_infantry
+ units/mauryas/infantry_maceman_c.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/units/maur/support_female_citizen_house.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/units/maur/support_female_citizen_house.xml (revision 27244)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/units/maur/support_female_citizen_house.xml (revision 27245)
@@ -1,9 +1,11 @@
30
- unlock_females_house
+
+ unlock_females_house
+
Index: ps/trunk/binaries/data/mods/public/simulation/templates/units/pers/cavalry_axeman_b_trireme.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/units/pers/cavalry_axeman_b_trireme.xml (revision 27244)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/units/pers/cavalry_axeman_b_trireme.xml (revision 27245)
@@ -1,9 +1,11 @@
- equine_transports
+
+ equine_transports
+ units/pers/cavalry_axeman_a_trireme
Index: ps/trunk/binaries/data/mods/public/simulation/templates/units/pers/cavalry_javelineer_e_trireme.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/units/pers/cavalry_javelineer_e_trireme.xml (revision 27244)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/units/pers/cavalry_javelineer_e_trireme.xml (revision 27245)
@@ -1,6 +1,8 @@
- equine_transports
+
+ equine_transports
+
Index: ps/trunk/binaries/data/mods/public/simulation/templates/units/pers/support_female_citizen_house.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/units/pers/support_female_citizen_house.xml (revision 27244)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/units/pers/support_female_citizen_house.xml (revision 27245)
@@ -1,9 +1,11 @@
30
- unlock_females_house
+
+ unlock_females_house
+
Index: ps/trunk/binaries/data/mods/public/simulation/templates/units/kush/infantry_maceman_merc_b.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/units/kush/infantry_maceman_merc_b.xml (revision 27244)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/units/kush/infantry_maceman_merc_b.xml (revision 27245)
@@ -1,26 +1,28 @@
structures/kush/pyramid_large
structures/kush/temple_amun
structures/kush/camp_blemmye
structures/kush/camp_noba
kushnapatanunits/kush/infantry_maceman_bNoba Macemannhw Nobaunits/kush/infantry_maceman.png
- phase_town
+
+ phase_town
+ units/kush/infantry_maceman_merc_aunits/kushites/infantry_maceman_b.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/units/kush/support_female_citizen_house.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/units/kush/support_female_citizen_house.xml (revision 27244)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/units/kush/support_female_citizen_house.xml (revision 27245)
@@ -1,9 +1,11 @@
30
- unlock_females_house
+
+ unlock_females_house
+
Index: ps/trunk/binaries/data/mods/public/simulation/templates/units/mace/infantry_archer_b.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/units/mace/infantry_archer_b.xml (revision 27244)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/units/mace/infantry_archer_b.xml (revision 27245)
@@ -1,18 +1,20 @@
macegreekunits/mace/infantry_archer_bCretan Mercenary ArcherToxótēs Krētikósunits/mace/infantry_archer.png
- phase_town
+
+ phase_town
+ units/mace/infantry_archer_aunits/macedonians/infantry_archer_b.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/units/maur/champion_chariot.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/units/maur/champion_chariot.xml (revision 27244)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/units/maur/champion_chariot.xml (revision 27245)
@@ -1,19 +1,21 @@
6.0maurWar ChariotRathunits/maur/champion_chariotChariotunits/maur/champion_chariot.png
- unlock_champion_chariots
+
+ unlock_champion_chariots
+ units/mauryas/chariot_archer_c_m.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/units/maur/infantry_swordsman_b.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/units/maur/infantry_swordsman_b.xml (revision 27244)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/units/maur/infantry_swordsman_b.xml (revision 27245)
@@ -1,28 +1,30 @@
Sword
structures/maur/palace
structures/maur/pillar_ashoka
maurunits/maur/infantry_swordsman_bIndian SwordsmanKhadagdhariunits/maur/infantry_swordsman.png
- phase_town
+
+ phase_town
+ units/maur/infantry_swordsman_aunits/mauryas/infantry_swordsman_b.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/units/pers/cavalry_axeman_b.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/units/pers/cavalry_axeman_b.xml (revision 27244)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/units/pers/cavalry_axeman_b.xml (revision 27245)
@@ -1,18 +1,20 @@
perspersianunits/pers/cavalry_axeman_bHyrcanian CavalryAsabāra Varkaniyaunits/pers/cavalry_axeman.png
- phase_town
+
+ phase_town
+ units/pers/cavalry_axeman_aunits/persians/cavalry_axeman_b_m.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/units/pers/cavalry_javelineer_b_trireme.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/units/pers/cavalry_javelineer_b_trireme.xml (revision 27244)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/units/pers/cavalry_javelineer_b_trireme.xml (revision 27245)
@@ -1,9 +1,11 @@
- equine_transports
+
+ equine_transports
+ units/pers/cavalry_javelineer_a_trireme
Index: ps/trunk/binaries/data/mods/public/simulation/templates/units/pers/infantry_javelineer_b.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/units/pers/infantry_javelineer_b.xml (revision 27244)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/units/pers/infantry_javelineer_b.xml (revision 27245)
@@ -1,18 +1,20 @@
perspersianunits/pers/infantry_javelineer_bLydian AuxiliaryPastiš Spardiyaunits/pers/infantry_javelinist.png
- phase_town
+
+ phase_town
+ units/pers/infantry_javelineer_aunits/persians/infantry_javelinist_b.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/units/ptol/cavalry_spearman_merc_b.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/units/ptol/cavalry_spearman_merc_b.xml (revision 27244)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/units/ptol/cavalry_spearman_merc_b.xml (revision 27245)
@@ -1,17 +1,19 @@
ptolunits/ptol/cavalry_spearman_merc_bMacedonian Settler CavalryHippeús Makedonikósunits/ptol/cavalry_spearman.png
- phase_town
+
+ phase_town
+ units/ptol/cavalry_spearman_merc_aunits/ptolemies/cavalry_spearman_b_m.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/units/kush/infantry_swordsman_b.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/units/kush/infantry_swordsman_b.xml (revision 27244)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/units/kush/infantry_swordsman_b.xml (revision 27245)
@@ -1,26 +1,28 @@
structures/kush/pyramid_large
structures/kush/temple_amun
structures/kush/camp_blemmye
structures/kush/camp_noba
kushnapatanunits/kush/infantry_swordsman_bMeroitic Swordsmanknw hpsunits/kush/infantry_swordsman.png
- phase_town
+
+ phase_town
+ units/kush/infantry_swordsman_aunits/kushites/infantry_swordsman_b.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/units/mace/champion_infantry_swordsman.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/units/mace/champion_infantry_swordsman.xml (revision 27244)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/units/mace/champion_infantry_swordsman.xml (revision 27245)
@@ -1,19 +1,21 @@
RhomphaiamacegreekThracian Black CloakRhomphaiaphorosunits/thrac/champion_infantry_swordsman.png
- unlock_champion_infantry
+
+ unlock_champion_infantry
+ units/macedonians/infantry_swordsman_c_thracian.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/units/maur/cavalry_swordsman_b.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/units/maur/cavalry_swordsman_b.xml (revision 27244)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/units/maur/cavalry_swordsman_b.xml (revision 27245)
@@ -1,17 +1,19 @@
maurunits/maur/cavalry_swordsman_bIndian Raiding CavalryAśvārohagaṇaḥunits/maur/cavalry_swordsman.png
- phase_town
+
+ phase_town
+ units/maur/cavalry_swordsman_aunits/mauryas/cavalry_swordsman_b_m.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/units/maur/hero_chandragupta_infantry.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/units/maur/hero_chandragupta_infantry.xml (revision 27244)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/units/maur/hero_chandragupta_infantry.xml (revision 27245)
@@ -1,24 +1,26 @@
maurChandragupta MauryaChandragupta Mauryaunits/maur/hero_chandragupta.pngunits/maur/hero_chandraguptaThis gives Chandragupta Maurya his War Elephant.
- phase_city200200
+
+ phase_city
+ units/mauryas/hero_infantry_swordsman_chandragupta.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/units/pers/cavalry_axeman_a_trireme.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/units/pers/cavalry_axeman_a_trireme.xml (revision 27244)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/units/pers/cavalry_axeman_a_trireme.xml (revision 27245)
@@ -1,9 +1,11 @@
- equine_transports
+
+ equine_transports
+ units/pers/cavalry_axeman_e_trireme
Index: ps/trunk/binaries/data/mods/public/simulation/templates/units/pers/cavalry_javelineer_a_trireme.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/units/pers/cavalry_javelineer_a_trireme.xml (revision 27244)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/units/pers/cavalry_javelineer_a_trireme.xml (revision 27245)
@@ -1,9 +1,11 @@
- equine_transports
+
+ equine_transports
+ units/pers/cavalry_javelineer_e_trireme
Index: ps/trunk/binaries/data/mods/public/simulation/templates/units/pers/champion_chariot.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/units/pers/champion_chariot.xml (revision 27244)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/units/pers/champion_chariot.xml (revision 27245)
@@ -1,19 +1,21 @@
6.0perspersianBabylonian Scythed ChariotRaθa BābiruviyaChariotunits/pers/chariot_archer.png
- unlock_champion_chariots
+
+ unlock_champion_chariots
+ units/persians/chariot_archer_e_m.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/units/ptol/cavalry_javelineer_merc_b.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/units/ptol/cavalry_javelineer_merc_b.xml (revision 27244)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/units/ptol/cavalry_javelineer_merc_b.xml (revision 27245)
@@ -1,17 +1,19 @@
ptolunits/ptol/cavalry_javelineer_merc_bTarantine Settler CavalryHippeús Tarantînosunits/hele/tarentine_cavalry_e.png
- phase_town
+
+ phase_town
+ units/ptol/cavalry_javelineer_merc_aunits/ptolemies/cavalry_javelinist_b_m.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/units/kush/infantry_spearman_b.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/units/kush/infantry_spearman_b.xml (revision 27244)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/units/kush/infantry_spearman_b.xml (revision 27245)
@@ -1,26 +1,28 @@
structures/kush/pyramid_large
structures/kush/temple_amun
structures/kush/camp_blemmye
structures/kush/camp_noba
kushnapatanunits/kush/infantry_spearman_bNubian Spearmaniry-rdwy Nhsyw
- phase_villageunits/kush/infantry_spearman.png
+
+ phase_village
+ units/kush/infantry_spearman_aunits/kushites/infantry_spearman_b.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/units/mace/champion_infantry_spearman.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/units/mace/champion_infantry_spearman.xml (revision 27244)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/units/mace/champion_infantry_spearman.xml (revision 27245)
@@ -1,19 +1,21 @@
macegreekMacedonian Shield BearerHypaspistḗsunits/mace/champion_infantry_spearmanunits/mace/hypaspist.png
- unlock_champion_infantry
+
+ unlock_champion_infantry
+ units/mace/champion_infantry_spearman_022000units/macedonians/infantry_spearman_c_a.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/units/mace/support_female_citizen_house.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/units/mace/support_female_citizen_house.xml (revision 27244)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/units/mace/support_female_citizen_house.xml (revision 27245)
@@ -1,9 +1,11 @@
30
- unlock_females_house
+
+ unlock_females_house
+
Index: ps/trunk/binaries/data/mods/public/simulation/templates/units/maur/elephant_archer_b.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/units/maur/elephant_archer_b.xml (revision 27244)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/units/maur/elephant_archer_b.xml (revision 27245)
@@ -1,17 +1,19 @@
maurElephant ArcherVachii Gajaunits/maur/elephant_archer_bunits/maur/elephant_archer.png
- phase_town
+
+ phase_town
+ units/maur/elephant_archer_aunits/mauryas/elephantry_archer_b_m.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/units/pers/cavalry_archer_b.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/units/pers/cavalry_archer_b.xml (revision 27244)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/units/pers/cavalry_archer_b.xml (revision 27245)
@@ -1,18 +1,20 @@
perspersianunits/pers/cavalry_archer_bParthian Horse ArcherAsabāra Parθavaunits/pers/cavalry_archer.png
- phase_town
+
+ phase_town
+ units/pers/cavalry_archer_aunits/persians/cavalry_archer_b_m.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/units/pers/cavalry_axeman_e_trireme.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/units/pers/cavalry_axeman_e_trireme.xml (revision 27244)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/units/pers/cavalry_axeman_e_trireme.xml (revision 27245)
@@ -1,6 +1,8 @@
- equine_transports
+
+ equine_transports
+
Index: ps/trunk/binaries/data/mods/public/simulation/templates/units/pers/cavalry_spearman_b.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/units/pers/cavalry_spearman_b.xml (revision 27244)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/units/pers/cavalry_spearman_b.xml (revision 27245)
@@ -1,18 +1,20 @@
perspersianunits/pers/cavalry_spearman_bCappadocian CavalryAsabāra Katpatukaunits/pers/cavalry_spearman.png
- phase_town
+
+ phase_town
+ units/pers/cavalry_spearman_aunits/persians/cavalry_spearman_b_m.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/units/ptol/cavalry_archer_b.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/units/ptol/cavalry_archer_b.xml (revision 27244)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/units/ptol/cavalry_archer_b.xml (revision 27245)
@@ -1,38 +1,40 @@
7.5ptolCamelunits/ptol/cavalry_archerNabataean Camel ArcherMutsābiq Gamal Nabatuunits/ptol/camel_archer.png
- phase_village
+
+ phase_village
+ units/ptol/cavalry_archer_aactor/fauna/movement/camel_order.xmlactor/fauna/death/death_camel.xml8.5units/ptolemies/camel_archer_b_m.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/units/ptol/champion_infantry_pikeman.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/units/ptol/champion_infantry_pikeman.xml (revision 27244)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/units/ptol/champion_infantry_pikeman.xml (revision 27245)
@@ -1,14 +1,16 @@
ptolgreekRoyal Guard InfantryPhalangitès Agemaunits/ptol/champion_infantry.png
- unlock_champion_infantry
+
+ unlock_champion_infantry
+ units/ptolemies/infantry_pikeman_c.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/units/ptol/infantry_spearman_merc_b.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/units/ptol/infantry_spearman_merc_b.xml (revision 27244)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/units/ptol/infantry_spearman_merc_b.xml (revision 27245)
@@ -1,23 +1,25 @@
structures/ptol/lighthouse
structures/ptol/library
ptolunits/ptol/infantry_spearman_merc_bMercenary Thureos SpearmanThureophóros Misthophórosunits/ptol/infantry_spearman_2.png
- phase_town
+
+ phase_town
+ units/ptol/infantry_spearman_merc_aunits/ptolemies/infantry_spearman_b.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/units/rome/champion_infantry_spear_gladiator.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/units/rome/champion_infantry_spear_gladiator.xml (revision 27244)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/units/rome/champion_infantry_spear_gladiator.xml (revision 27245)
@@ -1,47 +1,49 @@
structures/rome/army_camp
structures/rome/temple_vesta
-10205GladiatorromelatinGladiator SpearmanHoplomachusEliteunits/rome/champion_infantry_gladiator_spear.png
- phase_town
+
+ phase_town
+ -2Gladiator1.51.50.5units/romans/infantry_gladiator_spearman.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/units/ptol/infantry_slinger_b.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/units/ptol/infantry_slinger_b.xml (revision 27244)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/units/ptol/infantry_slinger_b.xml (revision 27245)
@@ -1,23 +1,25 @@
structures/ptol/lighthouse
structures/ptol/library
ptolunits/ptol/infantry_slinger_bJudean SlingerHebraikós Sphendonḗtēsunits/ptol/infantry_slinger.png
- phase_village
+
+ phase_village
+ units/ptol/infantry_slinger_aunits/ptolemies/infantry_slinger_b.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/units/rome/cavalry_javelineer_b.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/units/rome/cavalry_javelineer_b.xml (revision 27244)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/units/rome/cavalry_javelineer_b.xml (revision 27245)
@@ -1,18 +1,20 @@
romelatinAllied CavalryEques Sociusunits/rome/cavalry_javelineer_bunits/rome/cavalry_javelinist.png
- phase_town
+
+ phase_town
+ units/rome/cavalry_javelineer_aunits/romans/cavalry_javelinist_b_m.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/units/ptol/infantry_javelineer_b.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/units/ptol/infantry_javelineer_b.xml (revision 27244)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/units/ptol/infantry_javelineer_b.xml (revision 27245)
@@ -1,23 +1,25 @@
structures/ptol/lighthouse
structures/ptol/library
ptolunits/ptol/infantry_javelineer_bMercenary Thureos SkirmisherThureophóros Akrobolistḗsunits/ptol/infantry_javelinist_merc.png
- phase_town
+
+ phase_town
+ units/ptol/infantry_javelineer_aunits/ptolemies/infantry_javelinist_b.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/units/ptol/support_female_citizen_house.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/units/ptol/support_female_citizen_house.xml (revision 27244)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/units/ptol/support_female_citizen_house.xml (revision 27245)
@@ -1,9 +1,11 @@
30
- unlock_females_house
+
+ unlock_females_house
+
Index: ps/trunk/binaries/data/mods/public/simulation/templates/units/rome/champion_infantry_swordsman.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/units/rome/champion_infantry_swordsman.xml (revision 27244)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/units/rome/champion_infantry_swordsman.xml (revision 27245)
@@ -1,21 +1,23 @@
0.850.45romelatinItalic Heavy InfantryExtrāōrdināriusunits/rome/champion_infantry_swordsmanunits/rome/champion_infantry.png
- unlock_champion_infantry
+
+ unlock_champion_infantry
+ units/romans/infantry_swordsman_c.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/units/ptol/infantry_archer_b.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/units/ptol/infantry_archer_b.xml (revision 27244)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/units/ptol/infantry_archer_b.xml (revision 27245)
@@ -1,23 +1,25 @@
structures/ptol/lighthouse
structures/ptol/library
ptolunits/ptol/infantry_archer_bCretan Mercenary ArcherToxótēs Krētikósunits/mace/infantry_archer.png
- phase_town
+
+ phase_town
+ units/ptol/infantry_archer_aunits/ptolemies/infantry_archer_b.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/units/ptol/infantry_swordsman_merc_b.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/units/ptol/infantry_swordsman_merc_b.xml (revision 27244)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/units/ptol/infantry_swordsman_merc_b.xml (revision 27245)
@@ -1,23 +1,25 @@
structures/ptol/lighthouse
structures/ptol/library
ptolGallic Mercenary SwordsmanGallikós Mistophorósunits/ptol/infantry_swordsman_merc_bunits/cart/infantry_swordsman.png
- phase_town
+
+ phase_town
+ units/ptol/infantry_swordsman_merc_aunits/ptolemies/infantry_swordsman_b.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/units/rome/champion_infantry_sword_gladiator.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/units/rome/champion_infantry_sword_gladiator.xml (revision 27244)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/units/rome/champion_infantry_sword_gladiator.xml (revision 27245)
@@ -1,48 +1,50 @@
structures/rome/army_camp
structures/rome/temple_vesta
-10205GladiatorromelatinGladiator SwordsmanMurmilloEliteunits/rome/champion_infantry_gladiator_sword.png
- phase_town
+
+ phase_town
+ -1-1Gladiator1.41.40.5units/romans/infantry_gladiator_swordsman.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/units/rome/support_female_citizen_house.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/units/rome/support_female_citizen_house.xml (revision 27244)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/units/rome/support_female_citizen_house.xml (revision 27245)
@@ -1,9 +1,11 @@
30
- unlock_females_house
+
+ unlock_females_house
+
Index: ps/trunk/binaries/data/mods/public/simulation/templates/units/sele/champion_infantry_pikeman.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/units/sele/champion_infantry_pikeman.xml (revision 27244)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/units/sele/champion_infantry_pikeman.xml (revision 27245)
@@ -1,14 +1,16 @@
selegreekSilver ShieldArgyraspisunits/sele/champion_pikeman.png
- traditional_army_sele
+
+ traditional_army_sele
+ units/seleucids/infantry_pikeman_c.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/units/sele/infantry_swordsman_merc_b.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/units/sele/infantry_swordsman_merc_b.xml (revision 27244)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/units/sele/infantry_swordsman_merc_b.xml (revision 27245)
@@ -1,23 +1,25 @@
Rhomphaiaselegreekunits/sele/infantry_swordsman_merc_bThracian Mercenary SwordsmanRhomphaiaphoros Thrakikósunits/sele/infantry_swordsman.png
- phase_town
+
+ phase_town
+ units/sele/infantry_swordsman_merc_aunits/seleucids/infantry_swordsman_b.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/units/spart/support_female_citizen_house.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/units/spart/support_female_citizen_house.xml (revision 27244)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/units/spart/support_female_citizen_house.xml (revision 27245)
@@ -1,9 +1,11 @@
30
- unlock_females_house
+
+ unlock_females_house
+
Index: ps/trunk/binaries/data/mods/public/simulation/templates/units/rome/infantry_spearman_b.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/units/rome/infantry_spearman_b.xml (revision 27244)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/units/rome/infantry_spearman_b.xml (revision 27245)
@@ -1,35 +1,37 @@
structures/rome/army_camp
structures/rome/temple_vesta
0.850.45romelatinunits/rome/infantry_spearman_bVeteran SpearmanTriāriusunits/rome/infantry_spearman.png
- phase_town
+
+ phase_town
+ units/rome/infantry_spearman_a
special/formations/anti_cavalry
units/romans/infantry_spearman_b.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/units/sele/champion_chariot.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/units/sele/champion_chariot.xml (revision 27244)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/units/sele/champion_chariot.xml (revision 27245)
@@ -1,19 +1,21 @@
6.0selegreekScythed ChariotDrepanèphorosChariotunits/sele/champion_chariot.png
- unlock_champion_chariots
+
+ unlock_champion_chariots
+ units/seleucids/chariot_archer_c_m.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/units/sele/infantry_javelineer_b.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/units/sele/infantry_javelineer_b.xml (revision 27244)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/units/sele/infantry_javelineer_b.xml (revision 27245)
@@ -1,18 +1,20 @@
selegreekunits/sele/infantry_javelineer_bArab JavelineerPezakontistès Aravikósunits/sele/infantry_javelinist.png
- phase_village
+
+ phase_village
+ units/sele/infantry_javelineer_aunits/seleucids/infantry_javelinist_b.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/units/spart/champion_infantry_swordsman.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/units/spart/champion_infantry_swordsman.xml (revision 27244)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/units/spart/champion_infantry_swordsman.xml (revision 27245)
@@ -1,30 +1,32 @@
structures/spart/syssiton
-structures/{civ}/wallset_stone
25spartgreekSkiritai CommandoÉkdromos SkirítēsEliteunits/spart/champion_infantry_sword.png
- phase_town
+
+ phase_town
+ 3units/spartans/infantry_swordsman_c.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/units/sele/cavalry_spearman_merc_b.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/units/sele/cavalry_spearman_merc_b.xml (revision 27244)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/units/sele/cavalry_spearman_merc_b.xml (revision 27245)
@@ -1,18 +1,20 @@
selegreekunits/sele/cavalry_spearman_merc_bCompanion CavalryHippos Hetairikeunits/sele/cavalry_spearman_merc.png
- phase_town
+
+ phase_town
+ units/sele/cavalry_spearman_merc_aunits/seleucids/cavalry_spearman_b_m.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/units/sele/infantry_archer_merc_b.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/units/sele/infantry_archer_merc_b.xml (revision 27244)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/units/sele/infantry_archer_merc_b.xml (revision 27245)
@@ -1,18 +1,20 @@
selegreekunits/sele/infantry_archer_merc_bSyrian ArcherToxótēs Syríasunits/sele/infantry_archer.png
- phase_town
+
+ phase_town
+ units/sele/infantry_archer_merc_aunits/seleucids/infantry_archer_b.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/units/spart/cavalry_spearman_b.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/units/spart/cavalry_spearman_b.xml (revision 27244)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/units/spart/cavalry_spearman_b.xml (revision 27245)
@@ -1,18 +1,20 @@
spartgreekunits/spart/cavalry_spearman_bGreek Allied CavalryHippeús Symmakhikósunits/spart/cavalry_spearman.png
- phase_town
+
+ phase_town
+ units/spart/cavalry_spearman_aunits/spartans/cavalry_spearman_b_m.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/units/sele/cavalry_archer_b.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/units/sele/cavalry_archer_b.xml (revision 27244)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/units/sele/cavalry_archer_b.xml (revision 27245)
@@ -1,18 +1,20 @@
selegreekunits/sele/cavalry_archer_bDahae Horse ArcherHippotoxotès Dahaeunits/pers/cavalry_archer.png
- phase_town
+
+ phase_town
+ units/sele/cavalry_archer_aunits/persians/cavalry_archer_b_m.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/units/sele/champion_infantry_swordsman.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/units/sele/champion_infantry_swordsman.xml (revision 27244)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/units/sele/champion_infantry_swordsman.xml (revision 27245)
@@ -1,14 +1,16 @@
selegreekRomanized Heavy SwordsmanThorakitès Rhomaïkósunits/sele/champion_swordsman.png
- reformed_army_sele
+
+ reformed_army_sele
+ units/seleucids/infantry_swordsman_c.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/units/sele/support_female_citizen_house.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/units/sele/support_female_citizen_house.xml (revision 27244)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/units/sele/support_female_citizen_house.xml (revision 27245)
@@ -1,9 +1,11 @@
30
- unlock_females_house
+
+ unlock_females_house
+