Index: ps/trunk/binaries/data/mods/public/simulation/data/auras/structures/library.json
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/data/auras/structures/library.json (revision 25999)
+++ ps/trunk/binaries/data/mods/public/simulation/data/auras/structures/library.json (revision 26000)
@@ -1,13 +1,13 @@
{
"type": "global",
"affects": ["Structure"],
"modifications": [
- { "value": "ProductionQueue/TechCostMultiplier/food", "multiply": 0.85 },
- { "value": "ProductionQueue/TechCostMultiplier/wood", "multiply": 0.85 },
- { "value": "ProductionQueue/TechCostMultiplier/stone", "multiply": 0.85 },
- { "value": "ProductionQueue/TechCostMultiplier/metal", "multiply": 0.85 },
- { "value": "ProductionQueue/TechCostMultiplier/time", "multiply": 0.85 }
+ { "value": "Researcher/TechCostMultiplier/food", "multiply": 0.85 },
+ { "value": "Researcher/TechCostMultiplier/wood", "multiply": 0.85 },
+ { "value": "Researcher/TechCostMultiplier/stone", "multiply": 0.85 },
+ { "value": "Researcher/TechCostMultiplier/metal", "multiply": 0.85 },
+ { "value": "Researcher/TechCostMultiplier/time", "multiply": 0.85 }
],
"auraDescription": "Structures −15% technology resource costs and research time.",
"auraName": "Centre of Scholarship"
}
Index: ps/trunk/binaries/data/mods/public/simulation/data/auras/teambonuses/maur_player_teambonus.json
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/data/auras/teambonuses/maur_player_teambonus.json (revision 25999)
+++ ps/trunk/binaries/data/mods/public/simulation/data/auras/teambonuses/maur_player_teambonus.json (revision 26000)
@@ -1,17 +1,17 @@
{
"type": "global",
"affects": ["Temple"],
"affectedPlayers": ["MutualAlly"],
"modifications": [
{ "value": "Cost/BuildTime", "multiply": 0.5 },
{ "value": "Cost/Resources/wood", "multiply": 0.5 },
{ "value": "Cost/Resources/stone", "multiply": 0.5 },
- { "value": "ProductionQueue/TechCostMultiplier/food", "multiply": 0.5 },
- { "value": "ProductionQueue/TechCostMultiplier/wood", "multiply": 0.5 },
- { "value": "ProductionQueue/TechCostMultiplier/stone", "multiply": 0.5 },
- { "value": "ProductionQueue/TechCostMultiplier/metal", "multiply": 0.5 },
- { "value": "ProductionQueue/TechCostMultiplier/time", "multiply": 0.5 }
+ { "value": "Researcher/TechCostMultiplier/food", "multiply": 0.5 },
+ { "value": "Researcher/TechCostMultiplier/wood", "multiply": 0.5 },
+ { "value": "Researcher/TechCostMultiplier/stone", "multiply": 0.5 },
+ { "value": "Researcher/TechCostMultiplier/metal", "multiply": 0.5 },
+ { "value": "Researcher/TechCostMultiplier/time", "multiply": 0.5 }
],
"auraName": "Ashoka's Religious Support",
"auraDescription": "Temples −50% resource costs and building time; Temple technologies −50% resource costs and research time."
}
Index: ps/trunk/binaries/data/mods/public/globalscripts/Templates.js
===================================================================
--- ps/trunk/binaries/data/mods/public/globalscripts/Templates.js (revision 25999)
+++ ps/trunk/binaries/data/mods/public/globalscripts/Templates.js (revision 26000)
@@ -1,626 +1,626 @@
/**
* Loads history and gameplay data of all civs.
*
* @param selectableOnly {boolean} - Only load civs that can be selected
* in the gamesetup. Scenario maps might set non-selectable civs.
*/
function loadCivFiles(selectableOnly)
{
let propertyNames = [
"Code", "Culture", "Name", "Emblem", "History", "Music", "CivBonuses", "StartEntities",
"Formations", "AINames", "SkirmishReplacements", "SelectableInGameSetup"];
let civData = {};
for (let filename of Engine.ListDirectoryFiles("simulation/data/civs/", "*.json", false))
{
let data = Engine.ReadJSONFile(filename);
for (let prop of propertyNames)
if (data[prop] === undefined)
throw new Error(filename + " doesn't contain " + prop);
if (!selectableOnly || data.SelectableInGameSetup)
civData[data.Code] = data;
}
return civData;
}
/**
* @return {string[]} - All the classes for this identity template.
*/
function GetIdentityClasses(template)
{
let classString = "";
if (template.Classes && template.Classes._string)
classString += " " + template.Classes._string;
if (template.VisibleClasses && template.VisibleClasses._string)
classString += " " + template.VisibleClasses._string;
if (template.Rank)
classString += " " + template.Rank;
return classString.length > 1 ? classString.substring(1).split(" ") : [];
}
/**
* Gets an array with all classes for this identity template
* that should be shown in the GUI
*/
function GetVisibleIdentityClasses(template)
{
return template.VisibleClasses && template.VisibleClasses._string ? template.VisibleClasses._string.split(" ") : [];
}
/**
* Check if a given list of classes matches another list of classes.
* Useful f.e. for checking identity classes.
*
* @param classes - List of the classes to check against.
* @param match - Either a string in the form
* "Class1 Class2+Class3"
* where spaces are handled as OR and '+'-signs as AND,
* and ! is handled as NOT, thus Class1+!Class2 = Class1 AND NOT Class2.
* Or a list in the form
* [["Class1"], ["Class2", "Class3"]]
* where the outer list is combined as OR, and the inner lists are AND-ed.
* Or a hybrid format containing a list of strings, where the list is
* combined as OR, and the strings are split by space and '+' and AND-ed.
*
* @return undefined if there are no classes or no match object
* true if the the logical combination in the match object matches the classes
* false otherwise.
*/
function MatchesClassList(classes, match)
{
if (!match || !classes)
return undefined;
// Transform the string to an array
if (typeof match == "string")
match = match.split(/\s+/);
for (let sublist of match)
{
// If the elements are still strings, split them by space or by '+'
if (typeof sublist == "string")
sublist = sublist.split(/[+\s]+/);
if (sublist.every(c => (c[0] == "!" && classes.indexOf(c.substr(1)) == -1) ||
(c[0] != "!" && classes.indexOf(c) != -1)))
return true;
}
return false;
}
/**
* Gets the value originating at the value_path as-is, with no modifiers applied.
*
* @param {Object} template - A valid template as returned from a template loader.
* @param {string} value_path - Route to value within the xml template structure.
* @return {number}
*/
function GetBaseTemplateDataValue(template, value_path)
{
let current_value = template;
for (let property of value_path.split("/"))
current_value = current_value[property] || 0;
return +current_value;
}
/**
* Gets the value originating at the value_path with the modifiers dictated by the mod_key applied.
*
* @param {Object} template - A valid template as returned from a template loader.
* @param {string} value_path - Route to value within the xml template structure.
* @param {string} mod_key - Tech modification key, if different from value_path.
* @param {number} player - Optional player id.
* @param {Object} modifiers - Value modifiers from auto-researched techs, unit upgrades,
* etc. Optional as only used if no player id provided.
* @return {number} Modifier altered value.
*/
function GetModifiedTemplateDataValue(template, value_path, mod_key, player, modifiers={})
{
let current_value = GetBaseTemplateDataValue(template, value_path);
mod_key = mod_key || value_path;
if (player)
current_value = ApplyValueModificationsToTemplate(mod_key, current_value, player, template);
else if (modifiers && modifiers[mod_key])
current_value = GetTechModifiedProperty(modifiers[mod_key], GetIdentityClasses(template.Identity), current_value);
// Using .toFixed() to get around spidermonkey's treatment of numbers (3 * 1.1 = 3.3000000000000003 for instance).
return +current_value.toFixed(8);
}
/**
* Get information about a template with or without technology modifications.
*
* NOTICE: The data returned here should have the same structure as
* the object returned by GetEntityState and GetExtendedEntityState!
*
* @param {Object} template - A valid template as returned by the template loader.
* @param {number} player - An optional player id to get the technology modifications
* of properties.
* @param {Object} auraTemplates - In the form of { key: { "auraName": "", "auraDescription": "" } }.
* @param {Object} modifiers - Modifications from auto-researched techs, unit upgrades
* etc. Optional as only used if there's no player
* id provided.
*/
function GetTemplateDataHelper(template, player, auraTemplates, modifiers = {})
{
// Return data either from template (in tech tree) or sim state (ingame).
// @param {string} value_path - Route to the value within the template.
// @param {string} mod_key - Modification key, if not the same as the value_path.
let getEntityValue = function(value_path, mod_key) {
return GetModifiedTemplateDataValue(template, value_path, mod_key, player, modifiers);
};
let ret = {};
if (template.Resistance)
{
// Don't show Foundation resistance.
ret.resistance = {};
if (template.Resistance.Entity)
{
if (template.Resistance.Entity.Damage)
{
ret.resistance.Damage = {};
for (let damageType in template.Resistance.Entity.Damage)
ret.resistance.Damage[damageType] = getEntityValue("Resistance/Entity/Damage/" + damageType);
}
if (template.Resistance.Entity.Capture)
ret.resistance.Capture = getEntityValue("Resistance/Entity/Capture");
if (template.Resistance.Entity.ApplyStatus)
{
ret.resistance.ApplyStatus = {};
for (let statusEffect in template.Resistance.Entity.ApplyStatus)
ret.resistance.ApplyStatus[statusEffect] = {
"blockChance": getEntityValue("Resistance/Entity/ApplyStatus/" + statusEffect + "/BlockChance"),
"duration": getEntityValue("Resistance/Entity/ApplyStatus/" + statusEffect + "/Duration")
};
}
}
}
let getAttackEffects = (temp, path) => {
let effects = {};
if (temp.Capture)
effects.Capture = getEntityValue(path + "/Capture");
if (temp.Damage)
{
effects.Damage = {};
for (let damageType in temp.Damage)
effects.Damage[damageType] = getEntityValue(path + "/Damage/" + damageType);
}
if (temp.ApplyStatus)
effects.ApplyStatus = temp.ApplyStatus;
return effects;
};
if (template.Attack)
{
ret.attack = {};
for (let type in template.Attack)
{
let getAttackStat = function(stat) {
return getEntityValue("Attack/" + type + "/" + stat);
};
ret.attack[type] = {
"attackName": {
"name": template.Attack[type].AttackName._string || template.Attack[type].AttackName,
"context": template.Attack[type].AttackName["@context"]
},
"minRange": getAttackStat("MinRange"),
"maxRange": getAttackStat("MaxRange"),
"elevationBonus": getAttackStat("ElevationBonus")
};
ret.attack[type].elevationAdaptedRange = Math.sqrt(ret.attack[type].maxRange *
(2 * ret.attack[type].elevationBonus + ret.attack[type].maxRange));
ret.attack[type].repeatTime = getAttackStat("RepeatTime");
if (template.Attack[type].Projectile)
ret.attack[type].friendlyFire = template.Attack[type].Projectile.FriendlyFire == "true";
Object.assign(ret.attack[type], getAttackEffects(template.Attack[type], "Attack/" + type));
if (template.Attack[type].Splash)
{
ret.attack[type].splash = {
"friendlyFire": template.Attack[type].Splash.FriendlyFire != "false",
"shape": template.Attack[type].Splash.Shape,
};
Object.assign(ret.attack[type].splash, getAttackEffects(template.Attack[type].Splash, "Attack/" + type + "/Splash"));
}
}
}
if (template.DeathDamage)
{
ret.deathDamage = {
"friendlyFire": template.DeathDamage.FriendlyFire != "false",
};
Object.assign(ret.deathDamage, getAttackEffects(template.DeathDamage, "DeathDamage"));
}
if (template.Auras && auraTemplates)
{
ret.auras = {};
for (let auraID of template.Auras._string.split(/\s+/))
ret.auras[auraID] = GetAuraDataHelper(auraTemplates[auraID]);
}
if (template.BuildingAI)
ret.buildingAI = {
"defaultArrowCount": Math.round(getEntityValue("BuildingAI/DefaultArrowCount")),
"garrisonArrowMultiplier": getEntityValue("BuildingAI/GarrisonArrowMultiplier"),
"maxArrowCount": Math.round(getEntityValue("BuildingAI/MaxArrowCount"))
};
if (template.BuildRestrictions)
{
// required properties
ret.buildRestrictions = {
"placementType": template.BuildRestrictions.PlacementType,
"territory": template.BuildRestrictions.Territory,
"category": template.BuildRestrictions.Category,
};
// optional properties
if (template.BuildRestrictions.Distance)
{
ret.buildRestrictions.distance = {
"fromClass": template.BuildRestrictions.Distance.FromClass,
};
if (template.BuildRestrictions.Distance.MinDistance)
ret.buildRestrictions.distance.min = getEntityValue("BuildRestrictions/Distance/MinDistance");
if (template.BuildRestrictions.Distance.MaxDistance)
ret.buildRestrictions.distance.max = getEntityValue("BuildRestrictions/Distance/MaxDistance");
}
}
if (template.TrainingRestrictions)
{
ret.trainingRestrictions = {
"category": template.TrainingRestrictions.Category
};
if (template.TrainingRestrictions.MatchLimit)
ret.trainingRestrictions.matchLimit = +template.TrainingRestrictions.MatchLimit;
}
if (template.Cost)
{
ret.cost = {};
for (let resCode in template.Cost.Resources)
ret.cost[resCode] = getEntityValue("Cost/Resources/" + resCode);
if (template.Cost.Population)
ret.cost.population = getEntityValue("Cost/Population");
if (template.Cost.BuildTime)
ret.cost.time = getEntityValue("Cost/BuildTime");
}
if (template.Footprint)
{
ret.footprint = { "height": template.Footprint.Height };
if (template.Footprint.Square)
ret.footprint.square = {
"width": +template.Footprint.Square["@width"],
"depth": +template.Footprint.Square["@depth"]
};
else if (template.Footprint.Circle)
ret.footprint.circle = { "radius": +template.Footprint.Circle["@radius"] };
else
warn("GetTemplateDataHelper(): Unrecognized Footprint type");
}
if (template.Garrisonable)
ret.garrisonable = {
"size": getEntityValue("Garrisonable/Size")
};
if (template.GarrisonHolder)
{
ret.garrisonHolder = {
"buffHeal": getEntityValue("GarrisonHolder/BuffHeal")
};
if (template.GarrisonHolder.Max)
ret.garrisonHolder.capacity = getEntityValue("GarrisonHolder/Max");
}
if (template.Heal)
ret.heal = {
"health": getEntityValue("Heal/Health"),
"range": getEntityValue("Heal/Range"),
"interval": getEntityValue("Heal/Interval")
};
if (template.ResourceGatherer)
{
ret.resourceGatherRates = {};
let baseSpeed = getEntityValue("ResourceGatherer/BaseSpeed");
for (let type in template.ResourceGatherer.Rates)
ret.resourceGatherRates[type] = getEntityValue("ResourceGatherer/Rates/"+ type) * baseSpeed;
}
if (template.ResourceDropsite)
ret.resourceDropsite = {
"types": template.ResourceDropsite.Types.split(" ")
};
if (template.ResourceTrickle)
{
ret.resourceTrickle = {
"interval": +template.ResourceTrickle.Interval,
"rates": {}
};
for (let type in template.ResourceTrickle.Rates)
ret.resourceTrickle.rates[type] = getEntityValue("ResourceTrickle/Rates/" + type);
}
if (template.Loot)
{
ret.loot = {};
for (let type in template.Loot)
ret.loot[type] = getEntityValue("Loot/"+ type);
}
if (template.Obstruction)
{
ret.obstruction = {
"active": ("" + template.Obstruction.Active == "true"),
"blockMovement": ("" + template.Obstruction.BlockMovement == "true"),
"blockPathfinding": ("" + template.Obstruction.BlockPathfinding == "true"),
"blockFoundation": ("" + template.Obstruction.BlockFoundation == "true"),
"blockConstruction": ("" + template.Obstruction.BlockConstruction == "true"),
"disableBlockMovement": ("" + template.Obstruction.DisableBlockMovement == "true"),
"disableBlockPathfinding": ("" + template.Obstruction.DisableBlockPathfinding == "true"),
"shape": {}
};
if (template.Obstruction.Static)
{
ret.obstruction.shape.type = "static";
ret.obstruction.shape.width = +template.Obstruction.Static["@width"];
ret.obstruction.shape.depth = +template.Obstruction.Static["@depth"];
}
else if (template.Obstruction.Unit)
{
ret.obstruction.shape.type = "unit";
ret.obstruction.shape.radius = +template.Obstruction.Unit["@radius"];
}
else
ret.obstruction.shape.type = "cluster";
}
if (template.Pack)
ret.pack = {
"state": template.Pack.State,
"time": getEntityValue("Pack/Time"),
};
if (template.Population && template.Population.Bonus)
ret.population = {
"bonus": getEntityValue("Population/Bonus")
};
if (template.Health)
ret.health = Math.round(getEntityValue("Health/Max"));
if (template.Identity)
{
ret.selectionGroupName = template.Identity.SelectionGroupName;
ret.name = {
"specific": (template.Identity.SpecificName || template.Identity.GenericName),
"generic": template.Identity.GenericName
};
ret.icon = template.Identity.Icon;
ret.tooltip = template.Identity.Tooltip;
ret.requiredTechnology = template.Identity.RequiredTechnology;
ret.visibleIdentityClasses = GetVisibleIdentityClasses(template.Identity);
ret.nativeCiv = template.Identity.Civ;
}
if (template.UnitMotion)
{
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
});
}
}
- if (template.ProductionQueue)
+ if (template.Researcher)
{
ret.techCostMultiplier = {};
- for (let res in template.ProductionQueue.TechCostMultiplier)
- ret.techCostMultiplier[res] = getEntityValue("ProductionQueue/TechCostMultiplier/" + res);
+ for (const res in template.Researcher.TechCostMultiplier)
+ ret.techCostMultiplier[res] = getEntityValue("Researcher/TechCostMultiplier/" + res);
}
if (template.Trader)
ret.trader = {
"GainMultiplier": getEntityValue("Trader/GainMultiplier")
};
if (template.Treasure)
{
ret.treasure = {
"collectTime": getEntityValue("Treasure/CollectTime"),
"resources": {}
};
for (let resource in template.Treasure.Resources)
ret.treasure.resources[resource] = getEntityValue("Treasure/Resources/" + resource);
}
if (template.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.
*/
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/reference/common/TemplateLister.js
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/reference/common/TemplateLister.js (revision 25999)
+++ ps/trunk/binaries/data/mods/public/gui/reference/common/TemplateLister.js (revision 26000)
@@ -1,142 +1,142 @@
/**
* This class compiles and stores lists of which templates can be built/trained/researched by other templates.
*/
class TemplateLister
{
constructor(TemplateLoader)
{
this.TemplateLoader = TemplateLoader;
this.templateLists = new Map();
}
/**
* Compile lists of templates buildable/trainable/researchable of a given civ.
*
* @param {Object} civCode
* @param {Object} civData - Data defining every civ in the game.
*/
compileTemplateLists(civCode, civData)
{
if (this.hasTemplateLists(civCode))
return this.templateLists.get(civCode);
let templatesToParse = civData[civCode].StartEntities.map(entity => entity.Template);
let templateLists = {
"units": new Map(),
"structures": new Map(),
"techs": new Map(),
"wallsetPieces": new Map()
};
do
{
const templatesThisIteration = templatesToParse;
templatesToParse = [];
for (let templateBeingParsed of templatesThisIteration)
{
let baseOfTemplateBeingParsed = this.TemplateLoader.getVariantBaseAndType(templateBeingParsed, civCode)[0];
let list = this.deriveTemplateListsFromTemplate(templateBeingParsed, civCode);
for (let type in list)
for (let templateName of list[type])
{
if (type != "techs")
{
let templateVariance = this.TemplateLoader.getVariantBaseAndType(templateName, civCode);
if (templateVariance[1].passthru)
templateName = templateVariance[0];
}
if (!templateLists[type].has(templateName))
{
templateLists[type].set(templateName, [baseOfTemplateBeingParsed]);
if (type != "techs")
templatesToParse.push(templateName);
}
else if (templateLists[type].get(templateName).indexOf(baseOfTemplateBeingParsed) == -1)
templateLists[type].get(templateName).push(baseOfTemplateBeingParsed);
}
}
} while (templatesToParse.length);
// Expand/filter tech pairs
for (let [techCode, researcherList] of templateLists.techs)
{
if (!this.TemplateLoader.isPairTech(techCode))
continue;
for (let subTech of this.TemplateLoader.loadTechnologyPairTemplate(techCode, civCode).techs)
if (!templateLists.techs.has(subTech))
templateLists.techs.set(subTech, researcherList);
else
for (let researcher of researcherList)
if (templateLists.techs.get(subTech).indexOf(researcher) == -1)
templateLists.techs.get(subTech).push(researcher);
templateLists.techs.delete(techCode);
}
// Remove wallset pieces, as they've served their purpose.
delete templateLists.wallsetPieces;
this.templateLists.set(civCode, templateLists);
return this.templateLists.get(civCode);
}
/**
* Returns a civ's template list.
*
* Note: this civ must have gone through the compilation process above!
*
* @param {string} civCode
* @return {Object} containing lists of template names, grouped by type.
*/
getTemplateLists(civCode)
{
if (this.hasTemplateLists(civCode))
return this.templateLists.get(civCode);
error("Template lists of \"" + civCode + "\" requested, but this civ has not been loaded.");
return {};
}
/**
* Returns whether the civ of the given civCode has been loaded into cache.
*
* @param {string} civCode
* @return {boolean}
*/
hasTemplateLists(civCode)
{
return this.templateLists.has(civCode);
}
/**
* Compiles lists of buildable, trainable, or researchable entities from
* a named template.
*/
deriveTemplateListsFromTemplate(templateName, civCode)
{
if (!templateName || !Engine.TemplateExists(templateName))
return {};
let template = this.TemplateLoader.loadEntityTemplate(templateName, civCode);
- let templateLists = this.TemplateLoader.deriveProductionQueue(template, civCode);
+ const templateLists = this.TemplateLoader.deriveProduction(template, civCode);
templateLists.structures = this.TemplateLoader.deriveBuildQueue(template, civCode);
if (template.WallSet)
{
templateLists.wallsetPieces = [];
for (let segment in template.WallSet.Templates)
{
segment = template.WallSet.Templates[segment].replace(/\{(civ|native)\}/g, civCode);
if (Engine.TemplateExists(segment))
templateLists.wallsetPieces.push(segment);
}
}
return templateLists;
}
}
Index: ps/trunk/binaries/data/mods/public/simulation/data/technologies/barracks_batch_training.json
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/data/technologies/barracks_batch_training.json (revision 25999)
+++ ps/trunk/binaries/data/mods/public/simulation/data/technologies/barracks_batch_training.json (revision 26000)
@@ -1,17 +1,17 @@
{
"genericName": "Conscription",
"description": "Significantly increase training speed of infantry by training them in large batches or companies.",
"cost": {
"food": 500
},
"requirements": { "tech": "phase_city" },
"requirementsTooltip": "Unlocked in City Phase.",
"icon": "fist_spear_fire.png",
"researchTime": 40,
"tooltip": "Barracks −10% batch training time.",
"modifications": [
- { "value": "ProductionQueue/BatchTimeModifier", "add": -0.1 }
+ { "value": "Trainer/BatchTimeModifier", "add": -0.1 }
],
"affects": ["Barracks"],
"soundComplete": "interface/alarm/alarm_upgradearmory.xml"
}
Index: ps/trunk/binaries/data/mods/public/gui/reference/common/TemplateLoader.js
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/reference/common/TemplateLoader.js (revision 25999)
+++ ps/trunk/binaries/data/mods/public/gui/reference/common/TemplateLoader.js (revision 26000)
@@ -1,340 +1,340 @@
/**
* 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)
};
}
- deriveProductionQueue(template, civCode)
+ deriveProduction(template, civCode)
{
- let production = {
+ const production = {
"techs": [],
"units": []
};
- if (!template.ProductionQueue)
+ if (!template.Researcher && !template.Trainer)
return production;
- if (template.ProductionQueue.Entities && template.ProductionQueue.Entities._string)
- for (let templateName of template.ProductionQueue.Entities._string.split(" "))
+ 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);
}
- let appendTechnology = (technologyName) => {
- let technology = this.loadTechnologyTemplate(technologyName, civCode);
+ const appendTechnology = (technologyName) => {
+ const technology = this.loadTechnologyTemplate(technologyName, civCode);
if (DeriveTechnologyRequirements(technology, civCode))
production.techs.push(technologyName);
};
- if (template.ProductionQueue.Technologies && template.ProductionQueue.Technologies._string)
- for (let technologyName of template.ProductionQueue.Technologies._string.split(" "))
+ if (template.Researcher?.Technologies?._string)
+ for (let technologyName of template.Researcher.Technologies._string.split(" "))
{
if (technologyName.indexOf("{civ}") != -1)
{
- let civTechName = technologyName.replace("{civ}", civCode);
+ 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 + this.PlayerTemplatePrefix + civCode;
if (Engine.TemplateExists(templateName))
return templateName;
return this.PlayerPath + this.PlayerTemplateFallback;
}
/**
* 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 (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/player/";
TemplateLoader.prototype.TechnologyPath = "simulation/data/technologies/";
TemplateLoader.prototype.DefaultCiv = "gaia";
/**
* Expected prefix for player templates, and the file to use if a civ doesn't have its own.
*/
TemplateLoader.prototype.PlayerTemplatePrefix = "player_";
TemplateLoader.prototype.PlayerTemplateFallback = "player";
/**
* 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/simulation/data/auras/units/catafalques/ptol_catafalque.json
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/data/auras/units/catafalques/ptol_catafalque.json (revision 25999)
+++ ps/trunk/binaries/data/mods/public/simulation/data/auras/units/catafalques/ptol_catafalque.json (revision 26000)
@@ -1,12 +1,12 @@
{
"type": "global",
"affects": ["Structure"],
"modifications": [
- { "value": "ProductionQueue/TechCostMultiplier/food", "multiply": 0.9 },
- { "value": "ProductionQueue/TechCostMultiplier/wood", "multiply": 0.9 },
- { "value": "ProductionQueue/TechCostMultiplier/stone", "multiply": 0.9 },
- { "value": "ProductionQueue/TechCostMultiplier/metal", "multiply": 0.9 }
+ { "value": "Researcher/TechCostMultiplier/food", "multiply": 0.9 },
+ { "value": "Researcher/TechCostMultiplier/wood", "multiply": 0.9 },
+ { "value": "Researcher/TechCostMultiplier/stone", "multiply": 0.9 },
+ { "value": "Researcher/TechCostMultiplier/metal", "multiply": 0.9 }
],
"auraName": "Great Librarian",
"auraDescription": "Continuing his predecessors' work on the Great Library of Alexandria, he seized every book brought to the city, thus leaving to his people a vast amount of hoarded wisdom.\nStructure technologies −10% resource costs."
}
Index: ps/trunk/binaries/data/mods/public/simulation/data/auras/units/heroes/athen_hero_themistocles_1.json
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/data/auras/units/heroes/athen_hero_themistocles_1.json (revision 25999)
+++ ps/trunk/binaries/data/mods/public/simulation/data/auras/units/heroes/athen_hero_themistocles_1.json (revision 26000)
@@ -1,11 +1,11 @@
{
"type": "garrison",
"affects": ["Ship"],
"affectedPlayers": ["MutualAlly"],
"modifications": [
- { "value": "ProductionQueue/BatchTimeModifier", "multiply": 0.7 },
+ { "value": "Trainer/BatchTimeModifier", "multiply": 0.7 },
{ "value": "UnitMotion/WalkSpeed", "multiply": 1.5 }
],
"auraName": "Naval Commander",
"auraDescription": "When garrisoned, the Ship has −30% batch training time and +50% movement speed."
}
Index: ps/trunk/binaries/data/mods/public/simulation/data/auras/units/heroes/maur_hero_chanakya_1.json
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/data/auras/units/heroes/maur_hero_chanakya_1.json (revision 25999)
+++ ps/trunk/binaries/data/mods/public/simulation/data/auras/units/heroes/maur_hero_chanakya_1.json (revision 26000)
@@ -1,14 +1,14 @@
{
"type": "garrison",
"affects": ["Structure"],
"modifications": [
- { "value": "ProductionQueue/TechCostMultiplier/food", "multiply": 0.8 },
- { "value": "ProductionQueue/TechCostMultiplier/wood", "multiply": 0.8 },
- { "value": "ProductionQueue/TechCostMultiplier/stone", "multiply": 0.8 },
- { "value": "ProductionQueue/TechCostMultiplier/metal", "multiply": 0.8 },
- { "value": "ProductionQueue/TechCostMultiplier/time", "multiply": 0.5 }
+ { "value": "Researcher/TechCostMultiplier/food", "multiply": 0.8 },
+ { "value": "Researcher/TechCostMultiplier/wood", "multiply": 0.8 },
+ { "value": "Researcher/TechCostMultiplier/stone", "multiply": 0.8 },
+ { "value": "Researcher/TechCostMultiplier/metal", "multiply": 0.8 },
+ { "value": "Researcher/TechCostMultiplier/time", "multiply": 0.5 }
],
"auraDescription": "When garrisoned, the Structure's technologies have −20% resource cost and −50% research time.",
"auraName": "Teacher",
"overlayIcon": "art/textures/ui/session/auras/build_bonus.png"
}
Index: ps/trunk/binaries/data/mods/public/simulation/data/technologies/stable_batch_training.json
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/data/technologies/stable_batch_training.json (revision 25999)
+++ ps/trunk/binaries/data/mods/public/simulation/data/technologies/stable_batch_training.json (revision 26000)
@@ -1,17 +1,17 @@
{
"genericName": "Conscription",
"description": "Significantly increase training speed of cavalry by training them in large batches or squadrons.",
"cost": {
"food": 500
},
"requirements": { "tech": "phase_city" },
"requirementsTooltip": "Unlocked in City Phase.",
"icon": "horseshoe_gold.png",
"researchTime": 40,
"tooltip": "Stables −10% batch training time.",
"modifications": [
- { "value": "ProductionQueue/BatchTimeModifier", "add": -0.1 }
+ { "value": "Trainer/BatchTimeModifier", "add": -0.1 }
],
"affects": ["Stable"],
"soundComplete": "interface/alarm/alarm_upgradearmory.xml"
}
Index: ps/trunk/binaries/data/mods/public/simulation/helpers/Cheat.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/helpers/Cheat.js (revision 25999)
+++ ps/trunk/binaries/data/mods/public/simulation/helpers/Cheat.js (revision 26000)
@@ -1,195 +1,197 @@
function Cheat(input)
{
var cmpPlayerManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager);
if (!cmpPlayerManager || input.player < 0)
return;
var playerEnt = cmpPlayerManager.GetPlayerByID(input.player);
if (playerEnt == INVALID_ENTITY)
return;
var cmpPlayer = Engine.QueryInterface(playerEnt, IID_Player);
if (!cmpPlayer)
return;
var cmpGuiInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface);
if (!cmpPlayer.GetCheatsEnabled())
return;
switch(input.action)
{
case "addresource":
cmpPlayer.AddResource(input.text, input.parameter);
return;
case "revealmap":
var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
cmpRangeManager.SetLosRevealAll(-1, true);
return;
case "maxpopulation":
cmpPlayer.SetPopulationBonuses((cmpPlayerManager.GetMaxWorldPopulation() || cmpPlayer.GetMaxPopulation()) + 500);
return;
case "changemaxpopulation":
{
let cmpModifiersManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ModifiersManager);
cmpModifiersManager.AddModifiers("cheat/maxpopulation", {
"Player/MaxPopulation": [{ "affects": ["Player"], "add": 500 }],
}, playerEnt);
return;
}
case "convertunit":
for (let ent of input.selected)
{
let cmpOwnership = Engine.QueryInterface(ent, IID_Ownership);
if (cmpOwnership)
cmpOwnership.SetOwner(cmpPlayer.GetPlayerID());
}
return;
case "killunits":
for (let ent of input.selected)
{
let cmpHealth = Engine.QueryInterface(ent, IID_Health);
if (cmpHealth)
cmpHealth.Kill();
else
Engine.DestroyEntity(ent);
}
return;
case "defeatplayer":
cmpPlayer = QueryPlayerIDInterface(input.parameter);
if (cmpPlayer)
cmpPlayer.SetState("defeated", markForTranslation("%(player)s has been defeated (cheat)."));
return;
case "createunits":
- var cmpProductionQueue = input.selected.length && Engine.QueryInterface(input.selected[0], IID_ProductionQueue);
- if (!cmpProductionQueue)
+ {
+ const cmpTrainer = input.selected.length && Engine.QueryInterface(input.selected[0], IID_Trainer);
+ if (!cmpTrainer)
{
cmpGuiInterface.PushNotification({
"type": "text",
"players": [input.player],
"message": markForTranslation("You need to select a building that trains units."),
"translateMessage": true
});
return;
}
let owner = input.player;
- let cmpOwnership = Engine.QueryInterface(input.selected[0], IID_Ownership);
+ const cmpOwnership = Engine.QueryInterface(input.selected[0], IID_Ownership);
if (cmpOwnership)
owner = cmpOwnership.GetOwner();
for (let i = 0; i < Math.min(input.parameter, cmpPlayer.GetMaxPopulation() - cmpPlayer.GetPopulationCount()); ++i)
- cmpProductionQueue.SpawnUnits({
- "player": owner,
- "metadata": null,
- "entity": {
- "template": input.templates[i % input.templates.length],
- "count": 1
- }
- });
+ {
+ const batch = new cmpTrainer.Item(input.templates[i % input.templates.length], 1, input.selected[0], null);
+ batch.player = owner;
+ batch.Finish();
+ // ToDo: If not able to spawn, cancel the batch.
+ }
return;
+ }
case "fastactions":
{
let cmpModifiersManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ModifiersManager);
if (cmpModifiersManager.HasAnyModifier("cheat/fastactions", playerEnt))
cmpModifiersManager.RemoveAllModifiers("cheat/fastactions", playerEnt);
else
cmpModifiersManager.AddModifiers("cheat/fastactions", {
"Cost/BuildTime": [{ "affects": [["Structure"], ["Unit"]], "multiply": 0.01 }],
"ResourceGatherer/BaseSpeed": [{ "affects": [["Structure"], ["Unit"]], "multiply": 1000 }],
"Pack/Time": [{ "affects": [["Structure"], ["Unit"]], "multiply": 0.01 }],
"Upgrade/Time": [{ "affects": [["Structure"], ["Unit"]], "multiply": 0.01 }],
- "ProductionQueue/TechCostMultiplier/time": [{ "affects": [["Structure"], ["Unit"]], "multiply": 0.01 }]
+ "Researcher/TechCostMultiplier/time": [{ "affects": [["Structure"], ["Unit"]], "multiply": 0.01 }]
}, playerEnt);
return;
}
case "changephase":
var cmpTechnologyManager = Engine.QueryInterface(playerEnt, IID_TechnologyManager);
if (!cmpTechnologyManager)
return;
// store the phase we want in the next input parameter
var parameter;
if (!cmpTechnologyManager.IsTechnologyResearched("phase_town"))
parameter = "phase_town";
else if (!cmpTechnologyManager.IsTechnologyResearched("phase_city"))
parameter = "phase_city";
else
return;
if (TechnologyTemplates.Has(parameter + "_" + cmpPlayer.civ))
parameter += "_" + cmpPlayer.civ;
else
parameter += "_generic";
Cheat({ "player": input.player, "action": "researchTechnology", "parameter": parameter, "selected": input.selected });
return;
case "researchTechnology":
+ {
if (!input.parameter.length)
return;
var techname = input.parameter;
var cmpTechnologyManager = Engine.QueryInterface(playerEnt, IID_TechnologyManager);
if (!cmpTechnologyManager)
return;
// check, if building is selected
if (input.selected[0])
{
- var cmpProductionQueue = Engine.QueryInterface(input.selected[0], IID_ProductionQueue);
- if (cmpProductionQueue)
+ const cmpResearcher = Engine.QueryInterface(input.selected[0], IID_Researcher);
+ if (cmpResearcher)
{
// try to spilt the input
var tmp = input.parameter.split(/\s+/);
var number = +tmp[0];
var pair = tmp.length > 1 && (tmp[1] == "top" || tmp[1] == "bottom") ? tmp[1] : "top"; // use top as default value
// check, if valid number was parsed.
if (number || number === 0)
{
// get name of tech
- var techs = cmpProductionQueue.GetTechnologiesList();
+ const techs = cmpResearcher.GetTechnologiesList();
if (number > 0 && number <= techs.length)
{
var tech = techs[number-1];
if (!tech)
return;
// get name of tech
if (tech.pair)
techname = tech[pair];
else
techname = tech;
}
else
return;
}
}
}
if (TechnologyTemplates.Has(techname) &&
!cmpTechnologyManager.IsTechnologyResearched(techname))
cmpTechnologyManager.ResearchTechnology(techname);
return;
+ }
case "metaCheat":
for (let resource of Resources.GetCodes())
Cheat({ "player": input.player, "action": "addresource", "text": resource, "parameter": input.parameter });
Cheat({ "player": input.player, "action": "maxpopulation" });
Cheat({ "player": input.player, "action": "changemaxpopulation" });
Cheat({ "player": input.player, "action": "fastactions" });
for (let i=0; i<2; ++i)
Cheat({ "player": input.player, "action": "changephase", "selected": input.selected });
return;
case "playRetro":
let play = input.parameter.toLowerCase() != "off";
cmpGuiInterface.PushNotification({
"type": "play-tracks",
"tracks": play && input.parameter.split(" "),
"lock": play,
"players": [input.player]
});
return;
default:
warn("Cheat '" + input.action + "' is not implemented");
return;
}
}
Engine.RegisterGlobal("Cheat", Cheat);
Index: ps/trunk/binaries/data/mods/public/simulation/templates/campaigns/campaign_city_test.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/campaigns/campaign_city_test.xml (revision 25999)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/campaigns/campaign_city_test.xml (revision 26000)
@@ -1,45 +1,45 @@
25
50.0
5
0.5
8.0
athen
Market
Settlement
Greek Polis
This is a major Greek city.
-
+
+ true
+ 300
+ 35000
+
+
-units/{civ}/support_female_citizen
campaigns/army_mace_hero_alexander
campaigns/army_mace_standard
units/{civ}/support_trader
-
-
- true
- 300
- 35000
-
+
campaigns/structures/hellenes/settlement_curtainwall.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/mixins/trading_post.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/mixins/trading_post.xml (revision 25999)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/mixins/trading_post.xml (revision 26000)
@@ -1,25 +1,25 @@
-
-
-
- 500
-
-
-
- Trading Post
- true
-
-
-
- -trader_health
- -trade_gain_01
- -trade_gain_02
- -trade_commercial_treaty
-
-
-
-
-
-
- structures/fndn_5x5.xml
-
-
+
+
+
+ 500
+
+
+
+ Trading Post
+ true
+
+
+
+ -trader_health
+ -trade_gain_01
+ -trade_gain_02
+ -trade_commercial_treaty
+
+
+
+
+
+
+ structures/fndn_5x5.xml
+
+
Index: ps/trunk/binaries/data/mods/public/simulation/templates/skirmish/structures/default_civil_centre.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/skirmish/structures/default_civil_centre.xml (revision 25999)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/skirmish/structures/default_civil_centre.xml (revision 26000)
@@ -1,13 +1,15 @@
skirm
+
structures/{civ}/civil_centre
+
structures/athenians/civil_centre.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/campaigns/campaign_city_minor_test.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/campaigns/campaign_city_minor_test.xml (revision 25999)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/campaigns/campaign_city_minor_test.xml (revision 26000)
@@ -1,44 +1,44 @@
25
50.0
3
0.5
8.0
athen
Market
Settlement
Minor Greek Polis
This is a minor Greek city.
-
-
- -units/{civ}/support_female_citizen
- campaigns/army_mace_hero_alexander
- campaigns/army_mace_standard
-
-
true
150
35000
+
+
+ -units/{civ}/support_female_citizen
+ campaigns/army_mace_hero_alexander
+ campaigns/army_mace_standard
+
+
campaigns/structures/hellenes/settlement_curtainwall.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/mixins/shrine.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/mixins/shrine.xml (revision 25999)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/mixins/shrine.xml (revision 26000)
@@ -1,43 +1,45 @@
-
-
-
- own neutral ally
-
-
-
- 8.0
-
-
-
-
- Shrine
-
- true
-
-
-
-
-
-
- -units/{civ}/support_healer_b
- units/{native}/support_healer_e
-
-
- -heal_range
- -heal_range_2
- -heal_rate
- -heal_rate_2
- -garrison_heal
- -health_regen_units
-
-
-
- 15.0
-
-
-
-
-
- structures/fndn_4x4.xml
-
-
+
+
+
+ own neutral ally
+
+
+
+ 8.0
+
+
+
+
+ Shrine
+
+ true
+
+
+
+
+
+
+ -heal_range
+ -heal_range_2
+ -heal_rate
+ -heal_rate_2
+ -garrison_heal
+ -health_regen_units
+
+
+
+
+ 15.0
+
+
+
+
+
+ -units/{civ}/support_healer_b
+ units/{native}/support_healer_e
+
+
+
+ structures/fndn_4x4.xml
+
+
Index: ps/trunk/binaries/data/mods/public/simulation/templates/skirmish/structures/default_barracks.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/skirmish/structures/default_barracks.xml (revision 25999)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/skirmish/structures/default_barracks.xml (revision 26000)
@@ -1,13 +1,15 @@
skirm
+
structures/{civ}/barracks
+
structures/athenians/barracks.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/skirmish/structures/default_dock.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/skirmish/structures/default_dock.xml (revision 25999)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/skirmish/structures/default_dock.xml (revision 26000)
@@ -1,13 +1,15 @@
skirm
+
structures/{civ}/dock
+
structures/athenians/dock.xml
Index: ps/trunk/binaries/data/mods/public/gui/reference/common/TemplateParser.js
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/reference/common/TemplateParser.js (revision 25999)
+++ ps/trunk/binaries/data/mods/public/gui/reference/common/TemplateParser.js (revision 26000)
@@ -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);
let parsed = GetTemplateDataHelper(template, null, this.TemplateLoader.auraData, this.modifiers[civCode] || {});
parsed.name.internal = templateName;
parsed.history = template.Identity.History;
- parsed.production = this.TemplateLoader.deriveProductionQueue(template, civCode);
+ 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)
parsed.phase = this.phaseList[0];
else if (this.TemplateLoader.isPhaseTech(parsed.requiredTechnology))
parsed.phase = this.getActualPhase(parsed.requiredTechnology);
else
parsed.phase = this.getPhaseOfTechnology(parsed.requiredTechnology, civCode);
if (template.Identity.Rank)
parsed.promotion = {
"current_rank": template.Identity.Rank,
"entity": template.Promotion && template.Promotion.Entity
};
if (template.ResourceSupply)
parsed.supply = {
"type": template.ResourceSupply.Type.split("."),
"amount": template.ResourceSupply.Max,
};
if (parsed.upgrades)
parsed.upgrades = this.getActualUpgradeData(parsed.upgrades, civCode);
if (parsed.wallSet)
{
parsed.wallset = {};
if (!parsed.upgrades)
parsed.upgrades = [];
// Note: An assumption is made here that wall segments all have the same resistance and auras
let struct = this.getEntity(parsed.wallSet.templates.long, civCode);
parsed.resistance = struct.resistance;
parsed.auras = struct.auras;
// For technology cost multiplier, we need to use the tower
struct = this.getEntity(parsed.wallSet.templates.tower, civCode);
parsed.techCostMultiplier = struct.techCostMultiplier;
let health;
for (let wSegm in parsed.wallSet.templates)
{
if (wSegm == "fort" || wSegm == "curves")
continue;
let wPart = this.getEntity(parsed.wallSet.templates[wSegm], civCode);
parsed.wallset[wSegm] = wPart;
for (let research of wPart.production.techs)
parsed.production.techs.push(research);
if (wPart.upgrades)
Array.prototype.push.apply(parsed.upgrades, wPart.upgrades);
if (["gate", "tower"].indexOf(wSegm) != -1)
continue;
if (!health)
{
health = { "min": wPart.health, "max": wPart.health };
continue;
}
health.min = Math.min(health.min, wPart.health);
health.max = Math.max(health.max, wPart.health);
}
if (parsed.wallSet.templates.curves)
for (let curve of parsed.wallSet.templates.curves)
{
let wPart = this.getEntity(curve, civCode);
health.min = Math.min(health.min, wPart.health);
health.max = Math.max(health.max, wPart.health);
}
if (health.min == health.max)
parsed.health = health.min;
else
parsed.health = sprintf(translate("%(health_min)s to %(health_max)s"), {
"health_min": health.min,
"health_max": health.max
});
}
this.entities[civCode][templateName] = parsed;
return parsed;
}
/**
* Load and parse technology from json template.
*
* @param {string} technologyName
* @param {string} civCode
* @return {Object} Sanitized data about the requested technology.
*/
getTechnology(technologyName, civCode)
{
if (!TechnologyTemplateExists(technologyName))
return null;
if (this.TemplateLoader.isPhaseTech(technologyName) && technologyName in this.phases)
return this.phases[technologyName];
if (!(civCode in this.techs))
this.techs[civCode] = {};
else if (technologyName in this.techs[civCode])
return this.techs[civCode][technologyName];
let template = this.TemplateLoader.loadTechnologyTemplate(technologyName);
let tech = GetTechnologyDataHelper(template, civCode, g_ResourceData);
tech.name.internal = technologyName;
if (template.pair !== undefined)
{
tech.pair = template.pair;
tech.reqs = this.mergeRequirements(tech.reqs, this.TemplateLoader.loadTechnologyPairTemplate(template.pair).reqs);
}
if (this.TemplateLoader.isPhaseTech(technologyName))
{
tech.actualPhase = technologyName;
if (tech.replaces !== undefined)
tech.actualPhase = tech.replaces[0];
this.phases[technologyName] = tech;
}
else
this.techs[civCode][technologyName] = tech;
return tech;
}
/**
* @param {string} phaseCode
* @param {string} civCode
* @return {Object} Sanitized object containing phase data
*/
getPhase(phaseCode, civCode)
{
return this.getTechnology(phaseCode, civCode);
}
/**
* 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);
let data = GetTemplateDataHelper(this.TemplateLoader.loadEntityTemplate(upgrade.entity, civCode), null, this.TemplateLoader.auraData, this.modifiers[civCode] || {});
data.name.internal = upgrade.entity;
data.cost = upgrade.cost;
data.icon = upgrade.icon || data.icon;
data.tooltip = upgrade.tooltip || data.tooltip;
data.requiredTechnology = upgrade.requiredTechnology || data.requiredTechnology;
if (!data.requiredTechnology)
data.phase = this.phaseList[0];
else if (this.TemplateLoader.isPhaseTech(data.requiredTechnology))
data.phase = this.getActualPhase(data.requiredTechnology);
else
data.phase = this.getPhaseOfTechnology(data.requiredTechnology, civCode);
newUpgrades.push(data);
}
return newUpgrades;
}
/**
* Determines and returns the phase in which a given technology can be
* first researched. Works recursively through the given tech's
* pre-requisite and superseded techs if necessary.
*
* @param {string} techName - The Technology's name
* @param {string} civCode
* @return The name of the phase the technology belongs to, or false if
* the current civ can't research this tech
*/
getPhaseOfTechnology(techName, civCode)
{
let phaseIdx = -1;
if (basename(techName).startsWith("phase"))
{
if (!this.phases[techName].reqs)
return false;
phaseIdx = this.phaseList.indexOf(this.getActualPhase(techName));
if (phaseIdx > 0)
return this.phaseList[phaseIdx - 1];
}
let techReqs = this.getTechnology(techName, civCode).reqs;
if (!techReqs)
return false;
for (let option of techReqs)
if (option.techs)
for (let tech of option.techs)
{
if (basename(tech).startsWith("phase"))
return tech;
if (basename(tech).startsWith("pair"))
continue;
phaseIdx = Math.max(phaseIdx, this.phaseList.indexOf(this.getPhaseOfTechnology(tech, civCode)));
}
return this.phaseList[phaseIdx] || false;
}
/**
* Returns the actual phase a certain phase tech represents or stands in for.
*
* For example, passing `phase_city_athen` would result in `phase_city`.
*
* @param {string} phaseName
* @return {string}
*/
getActualPhase(phaseName)
{
if (this.phases[phaseName])
return this.phases[phaseName].actualPhase;
warn("Unrecognized phase (" + phaseName + ")");
return this.phaseList[0];
}
getModifiers(civCode)
{
return this.modifiers[civCode];
}
deriveModifications(civCode)
{
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/input.js
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/session/input.js (revision 25999)
+++ ps/trunk/binaries/data/mods/public/gui/session/input.js (revision 26000)
@@ -1,1716 +1,1716 @@
const SDL_BUTTON_LEFT = 1;
const SDL_BUTTON_MIDDLE = 2;
const SDL_BUTTON_RIGHT = 3;
const SDLK_LEFTBRACKET = 91;
const SDLK_RIGHTBRACKET = 93;
const SDLK_RSHIFT = 303;
const SDLK_LSHIFT = 304;
const SDLK_RCTRL = 305;
const SDLK_LCTRL = 306;
const SDLK_RALT = 307;
const SDLK_LALT = 308;
// TODO: these constants should be defined somewhere else instead, in
// case any other code wants to use them too.
const ACTION_NONE = 0;
const ACTION_GARRISON = 1;
const ACTION_REPAIR = 2;
const ACTION_GUARD = 3;
const ACTION_PATROL = 4;
const ACTION_OCCUPY_TURRET = 5;
const ACTION_CALLTOARMS = 6;
var preSelectedAction = ACTION_NONE;
const INPUT_NORMAL = 0;
const INPUT_SELECTING = 1;
const INPUT_BANDBOXING = 2;
const INPUT_BUILDING_PLACEMENT = 3;
const INPUT_BUILDING_CLICK = 4;
const INPUT_BUILDING_DRAG = 5;
const INPUT_BATCHTRAINING = 6;
const INPUT_PRESELECTEDACTION = 7;
const INPUT_BUILDING_WALL_CLICK = 8;
const INPUT_BUILDING_WALL_PATHING = 9;
const INPUT_UNIT_POSITION_START = 10;
const INPUT_UNIT_POSITION = 11;
const INPUT_FLARE = 12;
var inputState = INPUT_NORMAL;
const INVALID_ENTITY = 0;
var mouseX = 0;
var mouseY = 0;
var mouseIsOverObject = false;
/**
* Containing the ingame position which span the line.
*/
var g_FreehandSelection_InputLine = [];
/**
* Minimum squared distance when a mouse move is called a drag.
*/
const g_FreehandSelection_ResolutionInputLineSquared = 1;
/**
* Minimum length a dragged line should have to use the freehand selection.
*/
const g_FreehandSelection_MinLengthOfLine = 8;
/**
* To start the freehandSelection function you need a minimum number of units.
* Minimum must be 2, for better performance you could set it higher.
*/
const g_FreehandSelection_MinNumberOfUnits = 2;
/**
* Number of pixels the mouse can move before the action is considered a drag.
*/
const g_MaxDragDelta = 4;
/**
* Used for remembering mouse coordinates at start of drag operations.
*/
var g_DragStart;
/**
* Store the clicked entity on mousedown or mouseup for single/double/triple clicks to select entities.
* If any mousedown or mouseup of a sequence of clicks lands on a unit,
* that unit will be selected, which makes it easier to click on moving units.
*/
var clickedEntity = INVALID_ENTITY;
/**
* Store the last time the flare functionality was used to prevent overusage.
*/
var g_LastFlareTime;
/**
* The duration in ms for which we disable flaring after each flare to prevent overusage.
*/
const g_FlareCooldown = 3000;
// Same double-click behaviour for hotkey presses.
const doublePressTime = 500;
var doublePressTimer = 0;
var prevHotkey = 0;
function updateCursorAndTooltip()
{
let cursorSet = false;
let tooltipSet = false;
let informationTooltip = Engine.GetGUIObjectByName("informationTooltip");
if (inputState == INPUT_FLARE || inputState == INPUT_NORMAL && Engine.HotkeyIsPressed("session.flare") && !g_IsObserver)
{
Engine.SetCursor("action-flare");
cursorSet = true;
}
else if (!mouseIsOverObject && (inputState == INPUT_NORMAL || inputState == INPUT_PRESELECTEDACTION) || g_MiniMapPanel.isMouseOverMiniMap())
{
let action = determineAction(mouseX, mouseY, g_MiniMapPanel.isMouseOverMiniMap());
if (action)
{
if (action.cursor)
{
Engine.SetCursor(action.cursor);
cursorSet = true;
}
if (action.tooltip)
{
tooltipSet = true;
informationTooltip.caption = action.tooltip;
informationTooltip.hidden = false;
}
}
}
if (!cursorSet)
Engine.ResetCursor();
if (!tooltipSet)
informationTooltip.hidden = true;
let placementTooltip = Engine.GetGUIObjectByName("placementTooltip");
if (placementSupport.tooltipMessage)
placementTooltip.sprite = placementSupport.tooltipError ? "BackgroundErrorTooltip" : "BackgroundInformationTooltip";
placementTooltip.caption = placementSupport.tooltipMessage || "";
placementTooltip.hidden = !placementSupport.tooltipMessage;
}
function updateBuildingPlacementPreview()
{
// The preview should be recomputed every turn, so that it responds to obstructions/fog/etc moving underneath it, or
// in the case of the wall previews, in response to new tower foundations getting constructed for it to snap to.
// See onSimulationUpdate in session.js.
if (placementSupport.mode === "building")
{
if (placementSupport.template && placementSupport.position)
{
let result = Engine.GuiInterfaceCall("SetBuildingPlacementPreview", {
"template": placementSupport.template,
"x": placementSupport.position.x,
"z": placementSupport.position.z,
"angle": placementSupport.angle,
"actorSeed": placementSupport.actorSeed
});
placementSupport.tooltipError = !result.success;
placementSupport.tooltipMessage = "";
if (!result.success)
{
if (result.message && result.parameters)
{
let message = result.message;
if (result.translateMessage)
if (result.pluralMessage)
message = translatePlural(result.message, result.pluralMessage, result.pluralCount);
else
message = translate(message);
let parameters = result.parameters;
if (result.translateParameters)
translateObjectKeys(parameters, result.translateParameters);
placementSupport.tooltipMessage = sprintf(message, parameters);
}
return false;
}
if (placementSupport.attack && placementSupport.attack.Ranged)
{
let cmd = {
"x": placementSupport.position.x,
"z": placementSupport.position.z,
"range": placementSupport.attack.Ranged.maxRange,
"elevationBonus": placementSupport.attack.Ranged.elevationBonus
};
let averageRange = Math.round(Engine.GuiInterfaceCall("GetAverageRangeForBuildings", cmd) - cmd.range);
let range = Math.round(cmd.range);
placementSupport.tooltipMessage = sprintf(translatePlural("Basic range: %(range)s meter", "Basic range: %(range)s meters", range), { "range": range }) + "\n" +
sprintf(translatePlural("Average bonus range: %(range)s meter", "Average bonus range: %(range)s meters", averageRange), { "range": averageRange });
}
return true;
}
}
else if (placementSupport.mode === "wall" &&
placementSupport.wallSet && placementSupport.position)
{
placementSupport.wallSnapEntities = Engine.PickSimilarPlayerEntities(
placementSupport.wallSet.templates.tower,
placementSupport.wallSnapEntitiesIncludeOffscreen,
true, // require exact template match
true // include foundations
);
return Engine.GuiInterfaceCall("SetWallPlacementPreview", {
"wallSet": placementSupport.wallSet,
"start": placementSupport.position,
"end": placementSupport.wallEndPosition,
"snapEntities": placementSupport.wallSnapEntities // snapping entities (towers) for starting a wall segment
});
}
return false;
}
/**
* Determine the context-sensitive action that should be performed when the mouse is at (x,y)
*/
function determineAction(x, y, fromMiniMap)
{
let selection = g_Selection.toList();
if (!selection.length)
{
preSelectedAction = ACTION_NONE;
return undefined;
}
let entState = GetEntityState(selection[0]);
if (!entState)
return undefined;
if (!selection.every(ownsEntity) &&
!(g_SimState.players[g_ViewedPlayer] &&
g_SimState.players[g_ViewedPlayer].controlsAll))
return undefined;
let target;
if (!fromMiniMap)
{
let ent = Engine.PickEntityAtPoint(x, y);
if (ent != INVALID_ENTITY)
target = ent;
}
// Decide between the following ordered actions,
// if two actions are possible, the first one is taken
// thus the most specific should appear first.
if (preSelectedAction != ACTION_NONE)
{
for (let action of g_UnitActionsSortedKeys)
if (g_UnitActions[action].preSelectedActionCheck)
{
let r = g_UnitActions[action].preSelectedActionCheck(target, selection);
if (r)
return r;
}
return { "type": "none", "cursor": "", "target": target };
}
for (let action of g_UnitActionsSortedKeys)
if (g_UnitActions[action].hotkeyActionCheck)
{
let r = g_UnitActions[action].hotkeyActionCheck(target, selection);
if (r)
return r;
}
for (let action of g_UnitActionsSortedKeys)
if (g_UnitActions[action].actionCheck)
{
let r = g_UnitActions[action].actionCheck(target, selection);
if (r)
return r;
}
return { "type": "none", "cursor": "", "target": target };
}
function ownsEntity(ent)
{
let entState = GetEntityState(ent);
return entState && entState.player == g_ViewedPlayer;
}
function isAttackMovePressed()
{
return Engine.HotkeyIsPressed("session.attackmove") ||
Engine.HotkeyIsPressed("session.attackmoveUnit");
}
function isSnapToEdgesEnabled()
{
let config = Engine.ConfigDB_GetValue("user", "gui.session.snaptoedges");
let hotkeyPressed = Engine.HotkeyIsPressed("session.snaptoedges");
return hotkeyPressed == (config == "disabled");
}
function tryPlaceBuilding(queued, pushFront)
{
if (placementSupport.mode !== "building")
{
error("tryPlaceBuilding expected 'building', got '" + placementSupport.mode + "'");
return false;
}
if (!updateBuildingPlacementPreview())
{
Engine.GuiInterfaceCall("PlaySound", {
"name": "invalid_building_placement",
"entity": g_Selection.getFirstSelected()
});
return false;
}
let selection = Engine.HotkeyIsPressed("session.orderone") &&
popOneFromSelection({ "type": "construct", "target": placementSupport }) || g_Selection.toList();
Engine.PostNetworkCommand({
"type": "construct",
"template": placementSupport.template,
"x": placementSupport.position.x,
"z": placementSupport.position.z,
"angle": placementSupport.angle,
"actorSeed": placementSupport.actorSeed,
"entities": selection,
"autorepair": true,
"autocontinue": true,
"queued": queued,
"pushFront": pushFront,
"formation": g_AutoFormation.getNull()
});
Engine.GuiInterfaceCall("PlaySound", { "name": "order_build", "entity": selection[0] });
if (!queued || !g_Selection.size())
placementSupport.Reset();
else
placementSupport.RandomizeActorSeed();
return true;
}
function tryPlaceWall(queued)
{
if (placementSupport.mode !== "wall")
{
error("tryPlaceWall expected 'wall', got '" + placementSupport.mode + "'");
return false;
}
let wallPlacementInfo = updateBuildingPlacementPreview(); // entities making up the wall (wall segments, towers, ...)
if (!(wallPlacementInfo === false || typeof wallPlacementInfo === "object"))
{
error("Invalid updateBuildingPlacementPreview return value: " + uneval(wallPlacementInfo));
return false;
}
if (!wallPlacementInfo)
return false;
let selection = Engine.HotkeyIsPressed("session.orderone") &&
popOneFromSelection({ "type": "construct", "target": placementSupport }) || g_Selection.toList();
let cmd = {
"type": "construct-wall",
"autorepair": true,
"autocontinue": true,
"queued": queued,
"entities": selection,
"wallSet": placementSupport.wallSet,
"pieces": wallPlacementInfo.pieces,
"startSnappedEntity": wallPlacementInfo.startSnappedEnt,
"endSnappedEntity": wallPlacementInfo.endSnappedEnt,
"formation": g_AutoFormation.getNull()
};
// Make sure that there's at least one non-tower entity getting built, to prevent silly edge cases where the start and end
// point are too close together for the algorithm to place a wall segment inbetween, and only the towers are being previewed
// (this is somewhat non-ideal and hardcode-ish).
let hasWallSegment = false;
for (let piece of cmd.pieces)
{
if (piece.template != cmd.wallSet.templates.tower) // TODO: hardcode-ish :(
{
hasWallSegment = true;
break;
}
}
if (hasWallSegment)
{
Engine.PostNetworkCommand(cmd);
Engine.GuiInterfaceCall("PlaySound", { "name": "order_build", "entity": selection[0] });
}
return true;
}
/**
* Updates the bandbox object with new positions and visibility.
* @returns {array} The coordinates of the vertices of the bandbox.
*/
function updateBandbox(bandbox, ev, hidden)
{
let scale = +Engine.ConfigDB_GetValue("user", "gui.scale");
let vMin = Vector2D.min(g_DragStart, ev);
let vMax = Vector2D.max(g_DragStart, ev);
bandbox.size = new GUISize(vMin.x / scale, vMin.y / scale, vMax.x / scale, vMax.y / scale);
bandbox.hidden = hidden;
return [vMin.x, vMin.y, vMax.x, vMax.y];
}
// Define some useful unit filters for getPreferredEntities.
var unitFilters = {
"isUnit": entity => {
let entState = GetEntityState(entity);
return entState && hasClass(entState, "Unit");
},
"isDefensive": entity => {
let entState = GetEntityState(entity);
return entState && hasClass(entState, "Defensive");
},
"isMilitary": entity => {
let entState = GetEntityState(entity);
return entState &&
g_MilitaryTypes.some(c => hasClass(entState, c));
},
"isNonMilitary": entity => {
let entState = GetEntityState(entity);
return entState &&
hasClass(entState, "Unit") &&
!g_MilitaryTypes.some(c => hasClass(entState, c));
},
"isIdle": entity => {
let entState = GetEntityState(entity);
return entState &&
hasClass(entState, "Unit") &&
entState.unitAI &&
entState.unitAI.isIdle &&
!hasClass(entState, "Domestic");
},
"isWounded": entity => {
let entState = GetEntityState(entity);
return entState &&
hasClass(entState, "Unit") &&
entState.maxHitpoints &&
100 * entState.hitpoints <= entState.maxHitpoints * Engine.ConfigDB_GetValue("user", "gui.session.woundedunithotkeythreshold");
},
"isAnything": entity => {
return true;
}
};
// Choose, inside a list of entities, which ones will be selected.
// We may use several entity filters, until one returns at least one element.
function getPreferredEntities(ents)
{
let filters = [unitFilters.isUnit, unitFilters.isDefensive, unitFilters.isAnything];
if (Engine.HotkeyIsPressed("selection.militaryonly"))
filters = [unitFilters.isMilitary];
if (Engine.HotkeyIsPressed("selection.nonmilitaryonly"))
filters = [unitFilters.isNonMilitary];
if (Engine.HotkeyIsPressed("selection.idleonly"))
filters = [unitFilters.isIdle];
if (Engine.HotkeyIsPressed("selection.woundedonly"))
filters = [unitFilters.isWounded];
let preferredEnts = [];
for (let i = 0; i < filters.length; ++i)
{
preferredEnts = ents.filter(filters[i]);
if (preferredEnts.length)
break;
}
return preferredEnts;
}
function handleInputBeforeGui(ev, hoveredObject)
{
if (GetSimState().cinemaPlaying)
return false;
// Capture cursor position so we can use it for displaying cursors,
// and key states.
switch (ev.type)
{
case "mousebuttonup":
case "mousebuttondown":
case "mousemotion":
mouseX = ev.x;
mouseY = ev.y;
break;
}
mouseIsOverObject = (hoveredObject != null);
// Close the menu when interacting with the game world.
if (!mouseIsOverObject && (ev.type =="mousebuttonup" || ev.type == "mousebuttondown") &&
(ev.button == SDL_BUTTON_LEFT || ev.button == SDL_BUTTON_RIGHT))
g_Menu.close();
// State-machine processing:
//
// (This is for states which should override the normal GUI processing - events will
// be processed here before being passed on, and propagation will stop if this function
// returns true)
//
// TODO: it'd probably be nice to have a better state-machine system, with guaranteed
// entry/exit functions, since this is a bit broken now
switch (inputState)
{
case INPUT_BANDBOXING:
let bandbox = Engine.GetGUIObjectByName("bandbox");
switch (ev.type)
{
case "mousemotion":
{
let rect = updateBandbox(bandbox, ev, false);
let ents = Engine.PickPlayerEntitiesInRect(rect[0], rect[1], rect[2], rect[3], g_ViewedPlayer);
let preferredEntities = getPreferredEntities(ents);
g_Selection.setHighlightList(preferredEntities);
return false;
}
case "mousebuttonup":
if (ev.button == SDL_BUTTON_LEFT)
{
let rect = updateBandbox(bandbox, ev, true);
let ents = getPreferredEntities(Engine.PickPlayerEntitiesInRect(rect[0], rect[1], rect[2], rect[3], g_ViewedPlayer));
g_Selection.setHighlightList([]);
if (Engine.HotkeyIsPressed("selection.add"))
g_Selection.addList(ents);
else if (Engine.HotkeyIsPressed("selection.remove"))
g_Selection.removeList(ents);
else
{
g_Selection.reset();
g_Selection.addList(ents);
}
inputState = INPUT_NORMAL;
return true;
}
if (ev.button == SDL_BUTTON_RIGHT)
{
// Cancel selection.
bandbox.hidden = true;
g_Selection.setHighlightList([]);
inputState = INPUT_NORMAL;
return true;
}
break;
}
break;
case INPUT_UNIT_POSITION:
switch (ev.type)
{
case "mousemotion":
return positionUnitsFreehandSelectionMouseMove(ev);
case "mousebuttonup":
return positionUnitsFreehandSelectionMouseUp(ev);
}
break;
case INPUT_BUILDING_CLICK:
switch (ev.type)
{
case "mousemotion":
// If the mouse moved far enough from the original click location,
// then switch to drag-orientation mode.
let maxDragDelta = 16;
if (g_DragStart.distanceTo(ev) >= maxDragDelta)
{
inputState = INPUT_BUILDING_DRAG;
return false;
}
break;
case "mousebuttonup":
if (ev.button == SDL_BUTTON_LEFT)
{
// If queued, let the player continue placing another of the same building.
let queued = Engine.HotkeyIsPressed("session.queue");
if (tryPlaceBuilding(queued, Engine.HotkeyIsPressed("session.pushorderfront")))
{
if (queued && g_Selection.size())
inputState = INPUT_BUILDING_PLACEMENT;
else
inputState = INPUT_NORMAL;
}
else
inputState = INPUT_BUILDING_PLACEMENT;
return true;
}
break;
case "mousebuttondown":
if (ev.button == SDL_BUTTON_RIGHT)
{
// Cancel building.
placementSupport.Reset();
inputState = INPUT_NORMAL;
return true;
}
break;
}
break;
case INPUT_BUILDING_WALL_CLICK:
// User is mid-click in choosing a starting point for building a wall. The build process can still be cancelled at this point
// by right-clicking; releasing the left mouse button will 'register' the starting point and commence endpoint choosing mode.
switch (ev.type)
{
case "mousebuttonup":
if (ev.button === SDL_BUTTON_LEFT)
{
inputState = INPUT_BUILDING_WALL_PATHING;
return true;
}
break;
case "mousebuttondown":
if (ev.button == SDL_BUTTON_RIGHT)
{
// Cancel building.
placementSupport.Reset();
updateBuildingPlacementPreview();
inputState = INPUT_NORMAL;
return true;
}
break;
}
break;
case INPUT_BUILDING_WALL_PATHING:
// User has chosen a starting point for constructing the wall, and is now looking to set the endpoint.
// Right-clicking cancels wall building mode, left-clicking sets the endpoint and builds the wall and returns to
// normal input mode. Optionally, shift + left-clicking does not return to normal input, and instead allows the
// user to continue building walls.
switch (ev.type)
{
case "mousemotion":
placementSupport.wallEndPosition = Engine.GetTerrainAtScreenPoint(ev.x, ev.y);
// Update the structure placement preview, and by extension, the list of snapping candidate entities for both (!)
// the ending point and the starting point to snap to.
//
// TODO: Note that here, we need to fetch all similar entities, including any offscreen ones, to support the case
// where the snap entity for the starting point has moved offscreen, or has been deleted/destroyed, or was a
// foundation and has been replaced with a completed entity since the user first chose it. Fetching all towers on
// the entire map instead of only the current screen might get expensive fast since walls all have a ton of towers
// in them. Might be useful to query only for entities within a certain range around the starting point and ending
// points.
placementSupport.wallSnapEntitiesIncludeOffscreen = true;
let result = updateBuildingPlacementPreview(); // includes an update of the snap entity candidates
if (result && result.cost)
{
let neededResources = Engine.GuiInterfaceCall("GetNeededResources", { "cost": result.cost });
placementSupport.tooltipMessage = [
getEntityCostTooltip(result),
getNeededResourcesTooltip(neededResources)
].filter(tip => tip).join("\n");
}
break;
case "mousebuttondown":
if (ev.button == SDL_BUTTON_LEFT)
{
let queued = Engine.HotkeyIsPressed("session.queue");
if (tryPlaceWall(queued))
{
if (queued)
{
// Continue building, just set a new starting position where we left off.
placementSupport.position = placementSupport.wallEndPosition;
placementSupport.wallEndPosition = undefined;
inputState = INPUT_BUILDING_WALL_CLICK;
}
else
{
placementSupport.Reset();
inputState = INPUT_NORMAL;
}
}
else
placementSupport.tooltipMessage = translate("Cannot build wall here!");
updateBuildingPlacementPreview();
return true;
}
if (ev.button == SDL_BUTTON_RIGHT)
{
placementSupport.Reset();
updateBuildingPlacementPreview();
inputState = INPUT_NORMAL;
return true;
}
break;
}
break;
case INPUT_BUILDING_DRAG:
switch (ev.type)
{
case "mousemotion":
let maxDragDelta = 16;
if (g_DragStart.distanceTo(ev) >= maxDragDelta)
// Rotate in the direction of the cursor.
placementSupport.angle = placementSupport.position.horizAngleTo(Engine.GetTerrainAtScreenPoint(ev.x, ev.y));
else
// If the cursor is near the center, snap back to the default orientation.
placementSupport.SetDefaultAngle();
let snapData = Engine.GuiInterfaceCall("GetFoundationSnapData", {
"template": placementSupport.template,
"x": placementSupport.position.x,
"z": placementSupport.position.z,
"angle": placementSupport.angle,
"snapToEdges": isSnapToEdgesEnabled() && Engine.GetEdgesOfStaticObstructionsOnScreenNearTo(
placementSupport.position.x, placementSupport.position.z)
});
if (snapData)
{
placementSupport.angle = snapData.angle;
placementSupport.position.x = snapData.x;
placementSupport.position.z = snapData.z;
}
updateBuildingPlacementPreview();
break;
case "mousebuttonup":
if (ev.button == SDL_BUTTON_LEFT)
{
// If queued, let the player continue placing another of the same structure.
let queued = Engine.HotkeyIsPressed("session.queue");
if (tryPlaceBuilding(queued, Engine.HotkeyIsPressed("session.pushorderfront")))
{
if (queued && g_Selection.size())
inputState = INPUT_BUILDING_PLACEMENT;
else
inputState = INPUT_NORMAL;
}
else
inputState = INPUT_BUILDING_PLACEMENT;
return true;
}
break;
case "mousebuttondown":
if (ev.button == SDL_BUTTON_RIGHT)
{
// Cancel building.
placementSupport.Reset();
inputState = INPUT_NORMAL;
return true;
}
break;
}
break;
case INPUT_BATCHTRAINING:
if (ev.type == "hotkeyup" && ev.hotkey == "session.batchtrain")
{
flushTrainingBatch();
inputState = INPUT_NORMAL;
}
break;
}
return false;
}
function handleInputAfterGui(ev)
{
if (GetSimState().cinemaPlaying)
return false;
if (ev.hotkey === undefined)
ev.hotkey = null;
if (ev.hotkey == "session.highlightguarding")
{
g_ShowGuarding = (ev.type == "hotkeypress");
updateAdditionalHighlight();
}
else if (ev.hotkey == "session.highlightguarded")
{
g_ShowGuarded = (ev.type == "hotkeypress");
updateAdditionalHighlight();
}
if (inputState != INPUT_NORMAL && inputState != INPUT_SELECTING)
clickedEntity = INVALID_ENTITY;
// State-machine processing:
switch (inputState)
{
case INPUT_NORMAL:
switch (ev.type)
{
case "mousemotion":
let ent = Engine.PickEntityAtPoint(ev.x, ev.y);
if (ent != INVALID_ENTITY)
g_Selection.setHighlightList([ent]);
else
g_Selection.setHighlightList([]);
return false;
case "mousebuttondown":
if (Engine.HotkeyIsPressed("session.flare") && controlsPlayer(g_ViewedPlayer))
{
triggerFlareAction(Engine.GetTerrainAtScreenPoint(ev.x, ev.y));
return true;
}
if (ev.button == SDL_BUTTON_LEFT)
{
g_DragStart = new Vector2D(ev.x, ev.y);
inputState = INPUT_SELECTING;
// If a single click occured, reset the clickedEntity.
// Also set it if we're double/triple clicking and missed the unit earlier.
if (ev.clicks == 1 || clickedEntity == INVALID_ENTITY)
clickedEntity = Engine.PickEntityAtPoint(ev.x, ev.y);
return true;
}
else if (ev.button == SDL_BUTTON_RIGHT)
{
if (!controlsPlayer(g_ViewedPlayer))
break;
g_DragStart = new Vector2D(ev.x, ev.y);
inputState = INPUT_UNIT_POSITION_START;
}
break;
case "hotkeypress":
if (ev.hotkey.indexOf("selection.group.") == 0)
{
let now = Date.now();
if (now - doublePressTimer < doublePressTime && ev.hotkey == prevHotkey)
{
if (ev.hotkey.indexOf("selection.group.select.") == 0)
{
let sptr = ev.hotkey.split(".");
performGroup("snap", sptr[3]);
}
}
else
{
let sptr = ev.hotkey.split(".");
performGroup(sptr[2], sptr[3]);
doublePressTimer = now;
prevHotkey = ev.hotkey;
}
}
break;
}
break;
case INPUT_PRESELECTEDACTION:
switch (ev.type)
{
case "mousemotion":
let ent = Engine.PickEntityAtPoint(ev.x, ev.y);
if (ent != INVALID_ENTITY)
g_Selection.setHighlightList([ent]);
else
g_Selection.setHighlightList([]);
return false;
case "mousebuttondown":
if (ev.button == SDL_BUTTON_LEFT && preSelectedAction != ACTION_NONE)
{
let action = determineAction(ev.x, ev.y);
if (!action)
break;
if (!Engine.HotkeyIsPressed("session.queue") && !Engine.HotkeyIsPressed("session.orderone"))
{
preSelectedAction = ACTION_NONE;
inputState = INPUT_NORMAL;
}
return doAction(action, ev);
}
if (ev.button == SDL_BUTTON_RIGHT && preSelectedAction != ACTION_NONE)
{
preSelectedAction = ACTION_NONE;
inputState = INPUT_NORMAL;
break;
}
default:
// Slight hack: If selection is empty, reset the input state.
if (!g_Selection.size())
{
preSelectedAction = ACTION_NONE;
inputState = INPUT_NORMAL;
break;
}
}
break;
case INPUT_SELECTING:
switch (ev.type)
{
case "mousemotion":
if (g_DragStart.distanceTo(ev) >= g_MaxDragDelta)
{
inputState = INPUT_BANDBOXING;
return false;
}
let ent = Engine.PickEntityAtPoint(ev.x, ev.y);
if (ent != INVALID_ENTITY)
g_Selection.setHighlightList([ent]);
else
g_Selection.setHighlightList([]);
return false;
case "mousebuttonup":
if (ev.button == SDL_BUTTON_LEFT)
{
if (clickedEntity == INVALID_ENTITY)
clickedEntity = Engine.PickEntityAtPoint(ev.x, ev.y);
// Abort if we didn't click on an entity or if the entity was removed before the mousebuttonup event.
if (clickedEntity == INVALID_ENTITY || !GetEntityState(clickedEntity))
{
clickedEntity = INVALID_ENTITY;
if (!Engine.HotkeyIsPressed("selection.add") && !Engine.HotkeyIsPressed("selection.remove"))
{
g_Selection.reset();
resetIdleUnit();
}
inputState = INPUT_NORMAL;
return true;
}
if (Engine.GetFollowedEntity() != clickedEntity)
Engine.CameraFollow(0);
let ents = [];
if (ev.clicks == 1)
ents = [clickedEntity];
else
{
let showOffscreen = Engine.HotkeyIsPressed("selection.offscreen");
let matchRank = true;
let templateToMatch;
if (ev.clicks == 2)
{
templateToMatch = GetEntityState(clickedEntity).identity.selectionGroupName;
if (templateToMatch)
matchRank = false;
else
// No selection group name defined, so fall back to exact match.
templateToMatch = GetEntityState(clickedEntity).template;
}
else
// Triple click
// Select units matching exact template name (same rank).
templateToMatch = GetEntityState(clickedEntity).template;
// TODO: Should we handle "control all units" here as well?
ents = Engine.PickSimilarPlayerEntities(templateToMatch, showOffscreen, matchRank, false);
}
if (Engine.HotkeyIsPressed("selection.add"))
g_Selection.addList(ents);
else if (Engine.HotkeyIsPressed("selection.remove"))
g_Selection.removeList(ents);
else
{
g_Selection.reset();
g_Selection.addList(ents);
}
inputState = INPUT_NORMAL;
return true;
}
break;
}
break;
case INPUT_UNIT_POSITION_START:
switch (ev.type)
{
case "mousemotion":
if (g_DragStart.distanceToSquared(ev) >= Math.square(g_MaxDragDelta))
{
inputState = INPUT_UNIT_POSITION;
return false;
}
break;
case "mousebuttonup":
inputState = INPUT_NORMAL;
if (ev.button == SDL_BUTTON_RIGHT)
{
let action = determineAction(ev.x, ev.y);
if (action)
return doAction(action, ev);
}
break;
}
break;
case INPUT_BUILDING_PLACEMENT:
switch (ev.type)
{
case "mousemotion":
placementSupport.position = Engine.GetTerrainAtScreenPoint(ev.x, ev.y);
if (placementSupport.mode === "wall")
{
// Including only the on-screen towers in the next snap candidate list is sufficient here, since the user is
// still selecting a starting point (which must necessarily be on-screen). (The update of the snap entities
// itself happens in the call to updateBuildingPlacementPreview below.)
placementSupport.wallSnapEntitiesIncludeOffscreen = false;
}
else
{
if (placementSupport.template && Engine.GuiInterfaceCall("GetNeededResources", { "cost": GetTemplateData(placementSupport.template).cost }))
{
placementSupport.Reset();
inputState = INPUT_NORMAL;
return true;
}
if (isSnapToEdgesEnabled())
{
// We need to reset the angle before the snapping to edges,
// because we want to get the angle near to the default one.
placementSupport.SetDefaultAngle();
}
let snapData = Engine.GuiInterfaceCall("GetFoundationSnapData", {
"template": placementSupport.template,
"x": placementSupport.position.x,
"z": placementSupport.position.z,
"angle": placementSupport.angle,
"snapToEdges": isSnapToEdgesEnabled() && Engine.GetEdgesOfStaticObstructionsOnScreenNearTo(
placementSupport.position.x, placementSupport.position.z)
});
if (snapData)
{
placementSupport.angle = snapData.angle;
placementSupport.position.x = snapData.x;
placementSupport.position.z = snapData.z;
}
}
updateBuildingPlacementPreview(); // includes an update of the snap entity candidates
return false; // continue processing mouse motion
case "mousebuttondown":
if (ev.button == SDL_BUTTON_LEFT)
{
if (placementSupport.mode === "wall")
{
let validPlacement = updateBuildingPlacementPreview();
if (validPlacement !== false)
inputState = INPUT_BUILDING_WALL_CLICK;
}
else
{
placementSupport.position = Engine.GetTerrainAtScreenPoint(ev.x, ev.y);
if (isSnapToEdgesEnabled())
{
let snapData = Engine.GuiInterfaceCall("GetFoundationSnapData", {
"template": placementSupport.template,
"x": placementSupport.position.x,
"z": placementSupport.position.z,
"angle": placementSupport.angle,
"snapToEdges": Engine.GetEdgesOfStaticObstructionsOnScreenNearTo(
placementSupport.position.x, placementSupport.position.z)
});
if (snapData)
{
placementSupport.angle = snapData.angle;
placementSupport.position.x = snapData.x;
placementSupport.position.z = snapData.z;
}
}
g_DragStart = new Vector2D(ev.x, ev.y);
inputState = INPUT_BUILDING_CLICK;
}
return true;
}
else if (ev.button == SDL_BUTTON_RIGHT)
{
// Cancel building.
placementSupport.Reset();
inputState = INPUT_NORMAL;
return true;
}
break;
case "hotkeydown":
let rotation_step = Math.PI / 12; // 24 clicks make a full rotation
switch (ev.hotkey)
{
case "session.rotate.cw":
placementSupport.angle += rotation_step;
updateBuildingPlacementPreview();
break;
case "session.rotate.ccw":
placementSupport.angle -= rotation_step;
updateBuildingPlacementPreview();
break;
}
break;
}
break;
case INPUT_FLARE:
if (ev.type == "mousebuttondown")
{
if (ev.button == SDL_BUTTON_LEFT && controlsPlayer(g_ViewedPlayer))
{
triggerFlareAction(Engine.GetTerrainAtScreenPoint(ev.x, ev.y));
inputState = INPUT_NORMAL;
return true;
}
else if (ev.button == SDL_BUTTON_RIGHT)
{
inputState = INPUT_NORMAL;
return true;
}
}
}
return false;
}
function doAction(action, ev)
{
if (!controlsPlayer(g_ViewedPlayer))
return false;
return handleUnitAction(Engine.GetTerrainAtScreenPoint(ev.x, ev.y), action);
}
function popOneFromSelection(action)
{
// Pick the first unit that can do this order.
let unit = action.firstAbleEntity || g_Selection.find(entity =>
["preSelectedActionCheck", "hotkeyActionCheck", "actionCheck"].some(method =>
g_UnitActions[action.type][method] &&
g_UnitActions[action.type][method](action.target || undefined, [entity])
));
if (unit)
{
g_Selection.removeList([unit], true);
return [unit];
}
return null;
}
function positionUnitsFreehandSelectionMouseMove(ev)
{
// Converting the input line into a List of points.
// For better performance the points must have a minimum distance to each other.
let target = Vector2D.from3D(Engine.GetTerrainAtScreenPoint(ev.x, ev.y));
if (!g_FreehandSelection_InputLine.length ||
target.distanceToSquared(g_FreehandSelection_InputLine[g_FreehandSelection_InputLine.length - 1]) >=
g_FreehandSelection_ResolutionInputLineSquared)
g_FreehandSelection_InputLine.push(target);
return false;
}
function positionUnitsFreehandSelectionMouseUp(ev)
{
inputState = INPUT_NORMAL;
let inputLine = g_FreehandSelection_InputLine;
g_FreehandSelection_InputLine = [];
if (ev.button != SDL_BUTTON_RIGHT)
return true;
let lengthOfLine = 0;
for (let i = 1; i < inputLine.length; ++i)
lengthOfLine += inputLine[i].distanceTo(inputLine[i - 1]);
const selection = g_Selection.filter(ent => !!GetEntityState(ent).unitAI).sort((a, b) => a - b);
// Checking the line for a minimum length to save performance.
if (lengthOfLine < g_FreehandSelection_MinLengthOfLine || selection.length < g_FreehandSelection_MinNumberOfUnits)
{
let action = determineAction(ev.x, ev.y);
return !!action && doAction(action, ev);
}
// Even distribution of the units on the line.
let p0 = inputLine[0];
let entityDistribution = [p0];
let distanceBetweenEnts = lengthOfLine / (selection.length - 1);
let freeDist = -distanceBetweenEnts;
for (let i = 1; i < inputLine.length; ++i)
{
let p1 = inputLine[i];
freeDist += inputLine[i - 1].distanceTo(p1);
while (freeDist >= 0)
{
p0 = Vector2D.sub(p0, p1).normalize().mult(freeDist).add(p1);
entityDistribution.push(p0);
freeDist -= distanceBetweenEnts;
}
}
// Rounding errors can lead to missing or too many points.
entityDistribution = entityDistribution.slice(0, selection.length);
entityDistribution = entityDistribution.concat(new Array(selection.length - entityDistribution.length).fill(inputLine[inputLine.length - 1]));
if (Vector2D.from3D(GetEntityState(selection[0]).position).distanceTo(entityDistribution[0]) +
Vector2D.from3D(GetEntityState(selection[selection.length - 1]).position).distanceTo(entityDistribution[selection.length - 1]) >
Vector2D.from3D(GetEntityState(selection[0]).position).distanceTo(entityDistribution[selection.length - 1]) +
Vector2D.from3D(GetEntityState(selection[selection.length - 1]).position).distanceTo(entityDistribution[0]))
entityDistribution.reverse();
Engine.PostNetworkCommand({
"type": isAttackMovePressed() ? "attack-walk-custom" : "walk-custom",
"entities": selection,
"targetPositions": entityDistribution.map(pos => pos.toFixed(2)),
"targetClasses": Engine.HotkeyIsPressed("session.attackmoveUnit") ? { "attack": ["Unit"] } : { "attack": ["Unit", "Structure"] },
"queued": Engine.HotkeyIsPressed("session.queue"),
"pushFront": Engine.HotkeyIsPressed("session.pushorderfront"),
"formation": NULL_FORMATION,
});
// Add target markers with a minimum distance of 5 to each other.
let entitiesBetweenMarker = Math.ceil(5 / distanceBetweenEnts);
for (let i = 0; i < entityDistribution.length; i += entitiesBetweenMarker)
DrawTargetMarker({ "x": entityDistribution[i].x, "z": entityDistribution[i].y });
Engine.GuiInterfaceCall("PlaySound", {
"name": "order_walk",
"entity": selection[0]
});
return true;
}
function triggerFlareAction(target)
{
let now = Date.now();
if (g_LastFlareTime && now < g_LastFlareTime + g_FlareCooldown)
return;
g_LastFlareTime = now;
displayFlare(target, Engine.GetPlayerID());
Engine.PlayUISound(g_FlareSound, false);
Engine.PostNetworkCommand({
"type": "map-flare",
"target": target
});
}
function handleUnitAction(target, action)
{
if (!g_UnitActions[action.type] || !g_UnitActions[action.type].execute)
{
error("Invalid action.type " + action.type);
return false;
}
let selection = Engine.HotkeyIsPressed("session.orderone") &&
popOneFromSelection(action) || g_Selection.toList();
// If the session.queue hotkey is down, add the order to the unit's order queue instead
// of running it immediately. If the pushorderfront hotkey is down, execute the order
// immidiately and continue the rest of the queue afterwards.
return g_UnitActions[action.type].execute(
target,
action,
selection,
Engine.HotkeyIsPressed("session.queue"),
Engine.HotkeyIsPressed("session.pushorderfront"));
}
function getEntityLimitAndCount(playerState, entType)
{
let ret = {
"entLimit": undefined,
"entCount": undefined,
"entLimitChangers": undefined,
"canBeAddedCount": undefined,
"matchLimit": undefined,
"matchCount": undefined,
"type": undefined
};
if (!playerState.entityLimits)
return ret;
let template = GetTemplateData(entType);
let entCategory;
let matchLimit;
if (template.trainingRestrictions)
{
entCategory = template.trainingRestrictions.category;
matchLimit = template.trainingRestrictions.matchLimit;
ret.type = "training";
}
else if (template.buildRestrictions)
{
entCategory = template.buildRestrictions.category;
matchLimit = template.buildRestrictions.matchLimit;
ret.type = "build";
}
if (entCategory && playerState.entityLimits[entCategory] !== undefined)
{
ret.entLimit = playerState.entityLimits[entCategory] || 0;
ret.entCount = playerState.entityCounts[entCategory] || 0;
ret.entLimitChangers = playerState.entityLimitChangers[entCategory];
ret.canBeAddedCount = Math.max(ret.entLimit - ret.entCount, 0);
}
if (matchLimit)
{
ret.matchLimit = matchLimit;
ret.matchCount = playerState.matchEntityCounts[entType] || 0;
ret.canBeAddedCount = Math.min(Math.max(ret.entLimit - ret.entCount, 0), Math.max(ret.matchLimit - ret.matchCount, 0));
}
return ret;
}
/**
* Called by GUI when user clicks construction button.
* @param {string} buildTemplate - Template name of the entity the user wants to build.
*/
function startBuildingPlacement(buildTemplate, playerState)
{
if (getEntityLimitAndCount(playerState, buildTemplate).canBeAddedCount == 0)
return;
// TODO: we should clear any highlight selection rings here. If the cursor was over an entity before going onto the GUI
// to start building a structure, then the highlight selection rings are kept during the construction of the structure.
// Gives the impression that somehow the hovered-over entity has something to do with the structure you're building.
placementSupport.Reset();
let templateData = GetTemplateData(buildTemplate);
if (templateData.wallSet)
{
placementSupport.mode = "wall";
placementSupport.wallSet = templateData.wallSet;
inputState = INPUT_BUILDING_PLACEMENT;
}
else
{
placementSupport.mode = "building";
placementSupport.template = buildTemplate;
inputState = INPUT_BUILDING_PLACEMENT;
}
if (templateData.attack &&
templateData.attack.Ranged &&
templateData.attack.Ranged.maxRange)
placementSupport.attack = templateData.attack;
}
// Batch training:
// When the user shift-clicks, we set these variables and switch to INPUT_BATCHTRAINING
// When the user releases shift, or clicks on a different training button, we create the batched units
var g_BatchTrainingEntities;
var g_BatchTrainingType;
var g_NumberOfBatches;
var g_BatchTrainingEntityAllowedCount;
var g_BatchSize = getDefaultBatchTrainingSize();
function OnTrainMouseWheel(dir)
{
if (!Engine.HotkeyIsPressed("session.batchtrain"))
return;
g_BatchSize += dir / Engine.ConfigDB_GetValue("user", "gui.session.scrollbatchratio");
if (g_BatchSize < 1 || !Number.isFinite(g_BatchSize))
g_BatchSize = 1;
updateSelectionDetails();
}
function getBuildingsWhichCanTrainEntity(entitiesToCheck, trainEntType)
{
return entitiesToCheck.filter(entity => {
- let state = GetEntityState(entity);
- return state && state.production && state.production.entities.length &&
- state.production.entities.indexOf(trainEntType) != -1 && (!state.upgrade || !state.upgrade.isUpgrading);
+ const state = GetEntityState(entity);
+ return state?.trainer?.entities?.indexOf(trainEntType) != -1 &&
+ (!state.upgrade || !state.upgrade.isUpgrading);
});
}
function initBatchTrain()
{
registerConfigChangeHandler(changes => {
if (changes.has("gui.session.batchtrainingsize"))
updateDefaultBatchSize();
});
}
function getDefaultBatchTrainingSize()
{
let num = +Engine.ConfigDB_GetValue("user", "gui.session.batchtrainingsize");
return Number.isInteger(num) && num > 0 ? num : 5;
}
function getBatchTrainingSize()
{
return Math.max(Math.round(g_BatchSize), 1);
}
function updateDefaultBatchSize()
{
g_BatchSize = getDefaultBatchTrainingSize();
}
/**
* Add the unit shown at position to the training queue for all entities in the selection.
* @param {number} position - The position of the template to train.
*/
function addTrainingByPosition(position)
{
let playerState = GetSimState().players[Engine.GetPlayerID()];
let selection = g_Selection.toList();
if (!playerState || !selection.length)
return;
let trainableEnts = getAllTrainableEntitiesFromSelection();
let entToTrain = trainableEnts[position];
if (!entToTrain)
return;
addTrainingToQueue(selection, entToTrain, playerState);
}
// Called by GUI when user clicks training button
function addTrainingToQueue(selection, trainEntType, playerState)
{
let appropriateBuildings = getBuildingsWhichCanTrainEntity(selection, trainEntType);
let canBeAddedCount = getEntityLimitAndCount(playerState, trainEntType).canBeAddedCount;
let decrement = Engine.HotkeyIsPressed("selection.remove");
let template;
if (!decrement)
template = GetTemplateData(trainEntType);
// Batch training only possible if we can train at least 2 units.
if (Engine.HotkeyIsPressed("session.batchtrain") && (canBeAddedCount == undefined || canBeAddedCount > 1))
{
if (inputState == INPUT_BATCHTRAINING)
{
// Check if we are training in the same structure(s) as the last batch.
// NOTE: We just check if the arrays are the same and if the order is the same.
// If the order changed, we have a new selection and we should create a new batch.
// If we're already creating a batch of this unit (in the same structure(s)), then just extend it
// (if training limits allow).
if (g_BatchTrainingEntities.length == selection.length &&
g_BatchTrainingEntities.every((ent, i) => ent == selection[i]) &&
g_BatchTrainingType == trainEntType)
{
if (decrement)
{
--g_NumberOfBatches;
if (g_NumberOfBatches <= 0)
inputState = INPUT_NORMAL;
}
else if (canBeAddedCount == undefined ||
canBeAddedCount > g_NumberOfBatches * getBatchTrainingSize() * appropriateBuildings.length)
{
if (Engine.GuiInterfaceCall("GetNeededResources", {
"cost": multiplyEntityCosts(template, (g_NumberOfBatches + 1) * getBatchTrainingSize())
}))
return;
++g_NumberOfBatches;
}
g_BatchTrainingEntityAllowedCount = canBeAddedCount;
return;
}
else if (!decrement)
flushTrainingBatch();
}
if (decrement || Engine.GuiInterfaceCall("GetNeededResources", {
"cost": multiplyEntityCosts(template, getBatchTrainingSize())
}))
return;
inputState = INPUT_BATCHTRAINING;
g_BatchTrainingEntities = selection;
g_BatchTrainingType = trainEntType;
g_BatchTrainingEntityAllowedCount = canBeAddedCount;
g_NumberOfBatches = 1;
}
else
{
let buildingsForTraining = appropriateBuildings;
if (canBeAddedCount !== undefined)
buildingsForTraining = buildingsForTraining.slice(0, canBeAddedCount);
Engine.PostNetworkCommand({
"type": "train",
"template": trainEntType,
"count": 1,
"entities": buildingsForTraining,
"pushFront": Engine.HotkeyIsPressed("session.pushorderfront")
});
}
}
/**
* Returns the number of units that will be present in a batch if the user clicks
* the training button depending on the batch training modifier hotkey.
*/
function getTrainingStatus(selection, trainEntType, playerState)
{
let appropriateBuildings = getBuildingsWhichCanTrainEntity(selection, trainEntType);
let nextBatchTrainingCount = 0;
let canBeAddedCount;
if (inputState == INPUT_BATCHTRAINING && g_BatchTrainingType == trainEntType)
{
nextBatchTrainingCount = g_NumberOfBatches * getBatchTrainingSize();
canBeAddedCount = g_BatchTrainingEntityAllowedCount;
}
else
canBeAddedCount = getEntityLimitAndCount(playerState, trainEntType).canBeAddedCount;
// We need to calculate count after the next increment if possible.
if ((canBeAddedCount == undefined || canBeAddedCount > nextBatchTrainingCount * appropriateBuildings.length) &&
Engine.HotkeyIsPressed("session.batchtrain"))
nextBatchTrainingCount += getBatchTrainingSize();
nextBatchTrainingCount = Math.max(nextBatchTrainingCount, 1);
// If training limits don't allow us to train batchedSize in each appropriate structure,
// train as many full batches as we can and the remainder in one more structure.
let buildingsCountToTrainFullBatch = appropriateBuildings.length;
let remainderToTrain = 0;
if (canBeAddedCount !== undefined &&
canBeAddedCount < nextBatchTrainingCount * appropriateBuildings.length)
{
buildingsCountToTrainFullBatch = Math.floor(canBeAddedCount / nextBatchTrainingCount);
remainderToTrain = canBeAddedCount % nextBatchTrainingCount;
}
return [buildingsCountToTrainFullBatch, nextBatchTrainingCount, remainderToTrain];
}
function flushTrainingBatch()
{
let batchedSize = g_NumberOfBatches * getBatchTrainingSize();
let appropriateBuildings = getBuildingsWhichCanTrainEntity(g_BatchTrainingEntities, g_BatchTrainingType);
// If training limits don't allow us to train batchedSize in each appropriate structure.
if (g_BatchTrainingEntityAllowedCount !== undefined &&
g_BatchTrainingEntityAllowedCount < batchedSize * appropriateBuildings.length)
{
// Train as many full batches as we can.
let buildingsCountToTrainFullBatch = Math.floor(g_BatchTrainingEntityAllowedCount / batchedSize);
Engine.PostNetworkCommand({
"type": "train",
"entities": appropriateBuildings.slice(0, buildingsCountToTrainFullBatch),
"template": g_BatchTrainingType,
"count": batchedSize,
"pushFront": Engine.HotkeyIsPressed("session.pushorderfront")
});
// Train remainer in one more structure.
let remainer = g_BatchTrainingEntityAllowedCount % batchedSize;
if (remainer)
Engine.PostNetworkCommand({
"type": "train",
"entities": [appropriateBuildings[buildingsCountToTrainFullBatch]],
"template": g_BatchTrainingType,
"count": remainer,
"pushFront": Engine.HotkeyIsPressed("session.pushorderfront")
});
}
else
Engine.PostNetworkCommand({
"type": "train",
"entities": appropriateBuildings,
"template": g_BatchTrainingType,
"count": batchedSize,
"pushFront": Engine.HotkeyIsPressed("session.pushorderfront")
});
}
function performGroup(action, groupId)
{
switch (action)
{
case "snap":
case "select":
case "add":
let toSelect = [];
g_Groups.update();
for (let ent in g_Groups.groups[groupId].ents)
toSelect.push(+ent);
if (action != "add")
g_Selection.reset();
g_Selection.addList(toSelect);
if (action == "snap" && toSelect.length)
{
let entState = GetEntityState(getEntityOrHolder(toSelect[0]));
let position = entState.position;
if (position && entState.visibility != "hidden")
Engine.CameraMoveTo(position.x, position.z);
}
break;
case "save":
case "breakUp":
g_Groups.groups[groupId].reset();
if (action == "save")
g_Groups.addEntities(groupId, g_Selection.toList());
updateGroups();
break;
}
}
var lastIdleUnit = 0;
var currIdleClassIndex = 0;
var lastIdleClasses = [];
function resetIdleUnit()
{
lastIdleUnit = 0;
currIdleClassIndex = 0;
lastIdleClasses = [];
}
function findIdleUnit(classes)
{
let append = Engine.HotkeyIsPressed("selection.add");
let selectall = Engine.HotkeyIsPressed("selection.offscreen");
// Reset the last idle unit, etc., if the selection type has changed.
if (selectall || classes.length != lastIdleClasses.length || !classes.every((v, i) => v === lastIdleClasses[i]))
resetIdleUnit();
lastIdleClasses = classes;
let data = {
"viewedPlayer": g_ViewedPlayer,
"excludeUnits": append ? g_Selection.toList() : [],
// If the current idle class index is not 0, put the class at that index first.
"idleClasses": classes.slice(currIdleClassIndex, classes.length).concat(classes.slice(0, currIdleClassIndex))
};
if (!selectall)
{
data.limit = 1;
data.prevUnit = lastIdleUnit;
}
let idleUnits = Engine.GuiInterfaceCall("FindIdleUnits", data);
if (!idleUnits.length)
{
// TODO: display a message to indicate no more idle units, or something
Engine.GuiInterfaceCall("PlaySoundForPlayer", {
"name": "no_idle_unit"
});
resetIdleUnit();
return;
}
if (!append)
g_Selection.reset();
g_Selection.addList(idleUnits);
if (selectall)
return;
lastIdleUnit = idleUnits[0];
let entityState = GetEntityState(lastIdleUnit);
if (entityState.position)
Engine.CameraMoveTo(entityState.position.x, entityState.position.z);
// Move the idle class index to the first class an idle unit was found for.
let indexChange = data.idleClasses.findIndex(elem => MatchesClassList(entityState.identity.classes, elem));
currIdleClassIndex = (currIdleClassIndex + indexChange) % classes.length;
}
function clearSelection()
{
if (inputState==INPUT_BUILDING_PLACEMENT || inputState==INPUT_BUILDING_WALL_PATHING)
{
inputState = INPUT_NORMAL;
placementSupport.Reset();
}
else
g_Selection.reset();
preSelectedAction = ACTION_NONE;
}
Index: ps/trunk/binaries/data/mods/public/gui/session/selection_panels.js
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/session/selection_panels.js (revision 25999)
+++ ps/trunk/binaries/data/mods/public/gui/session/selection_panels.js (revision 26000)
@@ -1,1282 +1,1282 @@
/**
* 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,
"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),
getNeededResourcesTooltip(neededResources));
data.button.tooltip = tooltips.filter(tip => tip).join("\n");
let modifier = "";
if (!technologyEnabled || 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.identity || !state.identity.hasSomeFormation))
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.identity && state.identity.formations.indexOf(formation) != -1));
},
"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 : "";
if (data.item.ghost)
{
data.button.enabled = false;
Engine.GetGUIObjectByName("unitQueueProgressSlider[" + data.i + "]").sprite="color:0 150 250 50";
}
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"));
const guiObject = Engine.GetGUIObjectByName("unitQueueProgressSlider[" + data.i + "]");
guiObject.sprite = "queueProgressSlider";
const size = guiObject.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));
guiObject.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)
- return !unitEntStates[0].production || !unitEntStates[0].production.technologies ? ret :
- unitEntStates[0].production.technologies.map(tech => ({
+ return !unitEntStates[0].researcher || !unitEntStates[0].researcher.technologies ? ret :
+ unitEntStates[0].researcher.technologies.map(tech => ({
"tech": tech,
- "techCostMultiplier": unitEntStates[0].production.techCostMultiplier,
+ "techCostMultiplier": unitEntStates[0].researcher.techCostMultiplier,
"researchFacilityId": unitEntStates[0].id,
"isUpgrading": !!unitEntStates[0].upgrade && unitEntStates[0].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.production || !state.production.technologies)
+ if (!state.researcher || !state.researcher.technologies)
continue;
// Remove the techs we already have in ret (with the same name and techCostMultiplier)
- let filteredTechs = state.production.technologies.filter(
+ 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.production.techCostMultiplier[k])
+ 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.production.techCostMultiplier,
+ "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];
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,
"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),
getNeededResourcesTooltip(neededResources));
data.button.tooltip = tooltips.filter(tip => tip).join("\n");
let modifier = "";
if (!technologyEnabled || 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,
"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),
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 &&
!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/gui/session/selection_panels_helpers.js
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/session/selection_panels_helpers.js (revision 25999)
+++ ps/trunk/binaries/data/mods/public/gui/session/selection_panels_helpers.js (revision 26000)
@@ -1,577 +1,577 @@
/**
* @file Contains all helper functions that are needed only for selection_panels.js
* and some that are needed for hotkeys, but not for anything inside input.js.
*/
const UPGRADING_NOT_STARTED = -2;
const UPGRADING_CHOSEN_OTHER = -1;
function canMoveSelectionIntoFormation(formationTemplate)
{
if (formationTemplate == NULL_FORMATION)
return true;
if (!(formationTemplate in g_canMoveIntoFormation))
g_canMoveIntoFormation[formationTemplate] = Engine.GuiInterfaceCall("CanMoveEntsIntoFormation", {
"ents": g_Selection.toList(),
"formationTemplate": formationTemplate
});
return g_canMoveIntoFormation[formationTemplate];
}
function hasSameRestrictionCategory(templateName1, templateName2)
{
let template1 = GetTemplateData(templateName1);
let template2 = GetTemplateData(templateName2);
if (template1.trainingRestrictions && template2.trainingRestrictions)
return template1.trainingRestrictions.category == template2.trainingRestrictions.category;
if (template1.buildRestrictions && template2.buildRestrictions)
return template1.buildRestrictions.category == template2.buildRestrictions.category;
return false;
}
/**
* Returns a "color:255 0 0 Alpha" string based on how many resources are needed.
*/
function resourcesToAlphaMask(neededResources)
{
let totalCost = 0;
for (let resource in neededResources)
totalCost += +neededResources[resource];
return "color:255 0 0 " + Math.min(125, Math.round(+totalCost / 10) + 50);
}
function getStanceDisplayName(name)
{
switch (name)
{
case "violent":
return translateWithContext("stance", "Violent");
case "aggressive":
return translateWithContext("stance", "Aggressive");
case "defensive":
return translateWithContext("stance", "Defensive");
case "passive":
return translateWithContext("stance", "Passive");
case "standground":
return translateWithContext("stance", "Standground");
default:
warn("Internationalization: Unexpected stance found: " + name);
return name;
}
}
function getStanceTooltip(name)
{
switch (name)
{
case "violent":
return translateWithContext("stance", "Attack nearby opponents, focus on attackers and chase while visible");
case "aggressive":
return translateWithContext("stance", "Attack nearby opponents");
case "defensive":
return translateWithContext("stance", "Attack nearby opponents, chase a short distance and return to the original location");
case "passive":
return translateWithContext("stance", "Flee if attacked");
case "standground":
return translateWithContext("stance", "Attack opponents in range, but don't move");
default:
return "";
}
}
/**
* Format entity count/limit message for the tooltip
*/
function formatLimitString(trainEntLimit, trainEntCount, trainEntLimitChangers)
{
if (trainEntLimit == undefined)
return "";
var text = sprintf(translate("Current Count: %(count)s, Limit: %(limit)s."), {
"count": trainEntCount,
"limit": trainEntLimit
});
if (trainEntCount >= trainEntLimit)
text = coloredText(text, "red");
for (var c in trainEntLimitChangers)
{
if (!trainEntLimitChangers[c])
continue;
let string = trainEntLimitChangers[c] > 0 ?
translate("%(changer)s enlarges the limit with %(change)s.") :
translate("%(changer)s lessens the limit with %(change)s.");
text += "\n" + sprintf(string, {
"changer": translate(c),
"change": trainEntLimitChangers[c]
});
}
return text;
}
/**
* Format template match count/limit message for the tooltip.
*
* @param {number} matchEntLimit - The limit of the entity.
* @param {number} matchEntCount - The count of the entity.
* @param {string} type - The type of the action (i.e. "build" or "training").
*
* @return {string} - The string to show the user with information regarding the limit of this template.
*/
function formatMatchLimitString(matchEntLimit, matchEntCount, type)
{
if (matchEntLimit == undefined)
return "";
let passedLimit = matchEntCount >= matchEntLimit;
let count = matchEntLimit - matchEntCount;
let text;
if (type == "build")
{
if (passedLimit)
text = sprintf(translatePlural("Could only be constructed once.", "Could only be constructed %(limit)s times.", matchEntLimit), {
"limit": matchEntLimit
});
else if (matchEntLimit == 1)
text = translate("Can be constructed only once.");
else
text = sprintf(translatePlural("Can be constructed %(count)s more time.", "Can be constructed %(count)s more times.", count), {
"count": count
});
}
else if (type == "training")
{
if (passedLimit)
text = sprintf(translatePlural("Could only be trained once.", "Could only be trained %(limit)s times.", matchEntLimit), {
"limit": matchEntLimit
});
else if (matchEntLimit == 1)
text = translate("Can be trained only once.");
else
text = sprintf(translatePlural("Can be trained %(count)s more time.", "Can be trained %(count)s more times.", count), {
"count": count
});
}
else
{
if (passedLimit)
text = sprintf(translatePlural("Could only be created once.", "Could only be created %(limit)s times.", matchEntLimit), {
"limit": matchEntLimit
});
else if (matchEntLimit == 1)
text = translate("Can be created only once.");
else
text = sprintf(translatePlural("Can be created %(count)s more time.", "Can be created %(count)s more times.", count), {
"count": count
});
}
return passedLimit ? coloredText(text, "red") : text;
}
/**
* Format batch training string for the tooltip
* Examples:
* buildingsCountToTrainFullBatch = 1, fullBatchSize = 5, remainderBatch = 0:
* "Shift-click to train 5"
* buildingsCountToTrainFullBatch = 2, fullBatchSize = 5, remainderBatch = 0:
* "Shift-click to train 10 (2*5)"
* buildingsCountToTrainFullBatch = 1, fullBatchSize = 15, remainderBatch = 12:
* "Shift-click to train 27 (15 + 12)"
*/
function formatBatchTrainingString(buildingsCountToTrainFullBatch, fullBatchSize, remainderBatch)
{
var totalBatchTrainingCount = buildingsCountToTrainFullBatch * fullBatchSize + remainderBatch;
// Don't show the batch training tooltip if either units of this type can't be trained at all
// or only one unit can be trained
if (totalBatchTrainingCount < 2)
return "";
let fullBatchesString = "";
if (buildingsCountToTrainFullBatch > 1)
fullBatchesString = sprintf(translate("%(buildings)s*%(batchSize)s"), {
"buildings": buildingsCountToTrainFullBatch,
"batchSize": fullBatchSize
});
else if (buildingsCountToTrainFullBatch == 1)
fullBatchesString = fullBatchSize;
// We need to display the batch details part if there is either more than
// one structure with full batch or one structure with the full batch and
// another with a partial batch
let batchString;
if (buildingsCountToTrainFullBatch > 1 ||
buildingsCountToTrainFullBatch == 1 && remainderBatch > 0)
if (remainderBatch > 0)
batchString = translate("%(action)s to train %(number)s (%(fullBatch)s + %(remainderBatch)s).");
else
batchString = translate("%(action)s to train %(number)s (%(fullBatch)s).");
else
batchString = translate("%(action)s to train %(number)s.");
return "[font=\"sans-13\"]" +
setStringTags(
sprintf(batchString, {
"action": "[font=\"sans-bold-13\"]" + translate("Shift-click") + "[/font]",
"number": totalBatchTrainingCount,
"fullBatch": fullBatchesString,
"remainderBatch": remainderBatch
}),
g_HotkeyTags) +
"[/font]";
}
/**
* Camera jumping: when the user presses a hotkey the current camera location is marked.
* When pressing another camera jump hotkey the camera jumps back to that position.
* When the camera is already roughly at that location, jump back to where it was previously.
*/
var g_JumpCameraPositions = [];
var g_JumpCameraLast;
function jumpCamera(index)
{
let position = g_JumpCameraPositions[index];
if (!position)
return;
let threshold = Engine.ConfigDB_GetValue("user", "gui.session.camerajump.threshold");
let cameraPivot = Engine.GetCameraPivot();
if (g_JumpCameraLast &&
Math.abs(cameraPivot.x - position.x) < threshold &&
Math.abs(cameraPivot.z - position.z) < threshold)
{
Engine.CameraMoveTo(g_JumpCameraLast.x, g_JumpCameraLast.z);
}
else
{
g_JumpCameraLast = cameraPivot;
Engine.CameraMoveTo(position.x, position.z);
}
}
function setJumpCamera(index)
{
g_JumpCameraPositions[index] = Engine.GetCameraPivot();
}
/**
* Called by GUI when user clicks a research button.
*/
function addResearchToQueue(entity, researchType)
{
Engine.PostNetworkCommand({
"type": "research",
"entity": entity,
"template": researchType,
"pushFront": Engine.HotkeyIsPressed("session.pushorderfront")
});
}
/**
* Called by GUI when user clicks a production queue item.
*/
function removeFromProductionQueue(entity, id)
{
Engine.PostNetworkCommand({
"type": "stop-production",
"entity": entity,
"id": id
});
}
/**
* Called by unit selection buttons.
*/
function makePrimarySelectionGroup(templateName)
{
g_Selection.makePrimarySelection(templateName);
}
function removeFromSelectionGroup(templateName)
{
g_Selection.removeGroupFromSelection(templateName);
}
function performCommand(entStates, commandName)
{
if (!entStates.length)
return;
if (getCommandInfo(commandName, entStates))
g_EntityCommands[commandName].execute(entStates);
}
function performFormation(entities, formationTemplate)
{
if (!entities)
return;
Engine.PostNetworkCommand({
"type": "formation",
"entities": entities,
"formation": formationTemplate
});
}
function performStance(entities, stanceName)
{
if (!entities)
return;
Engine.PostNetworkCommand({
"type": "stance",
"entities": entities,
"name": stanceName
});
}
function lockGate(lock)
{
Engine.PostNetworkCommand({
"type": "lock-gate",
"entities": g_Selection.toList(),
"lock": lock
});
}
function packUnit(pack)
{
Engine.PostNetworkCommand({
"type": "pack",
"entities": g_Selection.toList(),
"pack": pack,
"queued": false
});
}
function cancelPackUnit(pack)
{
Engine.PostNetworkCommand({
"type": "cancel-pack",
"entities": g_Selection.toList(),
"pack": pack,
"queued": false
});
}
function upgradeEntity(Template, selection)
{
Engine.PostNetworkCommand({
"type": "upgrade",
"entities": selection,
"template": Template,
"queued": false
});
}
function cancelUpgradeEntity()
{
Engine.PostNetworkCommand({
"type": "cancel-upgrade",
"entities": g_Selection.toList(),
"queued": false
});
}
/**
* Set the camera to follow the given entity if it's a unit.
* Otherwise stop following.
*/
function setCameraFollow(entity)
{
let entState = entity && GetEntityState(entity);
if (entState && hasClass(entState, "Unit"))
Engine.CameraFollow(entity);
else
Engine.CameraFollow(0);
}
function stopUnits(entities)
{
Engine.PostNetworkCommand({
"type": "stop",
"entities": entities,
"queued": false
});
}
function unloadTemplate(template, owner)
{
Engine.PostNetworkCommand({
"type": "unload-template",
"all": Engine.HotkeyIsPressed("session.unloadtype"),
"template": template,
"owner": owner,
// Filter out all entities that aren't garrisonable.
"garrisonHolders": g_Selection.filter(ent => {
let state = GetEntityState(ent);
return state && !!state.garrisonHolder;
})
});
}
function unloadAll()
{
const garrisonHolders = g_Selection.filter(e => {
let state = GetEntityState(e);
return state && !!state.garrisonHolder;
});
if (!garrisonHolders.length)
return;
let ownEnts = [];
let otherEnts = [];
for (let ent of garrisonHolders)
{
if (controlsPlayer(GetEntityState(ent).player))
ownEnts.push(ent);
else
otherEnts.push(ent);
}
if (ownEnts.length)
Engine.PostNetworkCommand({
"type": "unload-all",
"garrisonHolders": ownEnts
});
if (otherEnts.length)
Engine.PostNetworkCommand({
"type": "unload-all-by-owner",
"garrisonHolders": otherEnts
});
}
function unloadAllTurrets()
{
const turretHolders = g_Selection.filter(e => {
let state = GetEntityState(e);
return state && !!state.turretHolder;
});
if (!turretHolders.length)
return;
let ownedHolders = [];
let ejectables = [];
for (let ent of turretHolders)
{
let turretHolderState = GetEntityState(ent);
if (controlsPlayer(turretHolderState.player))
ownedHolders.push(ent);
else
{
for (let turret of turretHolderState.turretHolder.turretPoints.map(tp => tp.entity))
if (turret && controlsPlayer(GetEntityState(turret).player))
ejectables.push(turret);
}
}
if (ejectables.length)
Engine.PostNetworkCommand({
"type": "leave-turret",
"entities": ejectables
});
if (ownedHolders.length)
Engine.PostNetworkCommand({
"type": "unload-turrets",
"entities": ownedHolders
});
}
function leaveTurretPoints()
{
const entities = g_Selection.filter(entity => {
let entState = GetEntityState(entity);
return entState && entState.turretable &&
entState.turretable.holder != INVALID_ENTITY;
});
Engine.PostNetworkCommand({
"type": "leave-turret",
"entities": entities
});
}
function backToWork()
{
Engine.PostNetworkCommand({
"type": "back-to-work",
// Filter out all entities that can't go back to work.
"entities": g_Selection.filter(ent => {
let state = GetEntityState(ent);
return state && state.unitAI && state.unitAI.hasWorkOrders;
})
});
}
function removeGuard()
{
Engine.PostNetworkCommand({
"type": "remove-guard",
// Filter out all entities that are currently guarding/escorting.
"entities": g_Selection.filter(ent => {
let state = GetEntityState(ent);
return state && state.unitAI && state.unitAI.isGuarding;
})
});
}
function raiseAlert()
{
Engine.PostNetworkCommand({
"type": "alert-raise",
"entities": g_Selection.filter(ent => {
let state = GetEntityState(ent);
return state && !!state.alertRaiser;
})
});
}
function endOfAlert()
{
Engine.PostNetworkCommand({
"type": "alert-end",
"entities": g_Selection.filter(ent => {
let state = GetEntityState(ent);
return state && !!state.alertRaiser;
})
});
}
function turnAutoQueueOn()
{
Engine.PostNetworkCommand({
"type": "autoqueue-on",
"entities": g_Selection.filter(ent => {
let state = GetEntityState(ent);
- return !!state?.production?.entities.length &&
+ return !!state?.trainer?.entities?.length &&
!state.production.autoqueue;
})
});
}
function turnAutoQueueOff()
{
Engine.PostNetworkCommand({
"type": "autoqueue-off",
"entities": g_Selection.filter(ent => {
let state = GetEntityState(ent);
- return !!state?.production?.entities.length &&
+ return !!state?.trainer?.entities?.length &&
state.production.autoqueue;
})
});
}
Index: ps/trunk/binaries/data/mods/public/gui/session/unit_actions.js
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/session/unit_actions.js (revision 25999)
+++ ps/trunk/binaries/data/mods/public/gui/session/unit_actions.js (revision 26000)
@@ -1,1957 +1,1957 @@
/**
* Specifies which template should indicate the target location of a player command,
* given a command type.
*/
var g_TargetMarker = {
"move": "special/target_marker",
"map_flare": "special/flare_target_marker"
};
/**
* Sound we play when displaying a flare.
*/
var g_FlareSound = "audio/interface/alarm/alarmally_1.ogg";
/**
* Which enemy entity types will be attacked on sight when patroling.
*/
var g_PatrolTargets = ["Unit"];
const g_DisabledTags = { "color": "255 140 0" };
/**
* List of different actions units can execute,
* this is mostly used to determine which actions can be executed
*
* "execute" is meant to send the command to the engine
*
* The next functions will always return false
* in case you have to continue to seek
* (i.e. look at the next entity for getActionInfo, the next
* possible action for the actionCheck ...)
* They will return an object when the searching is finished
*
* "getActionInfo" is used to determine if the action is possible,
* and also give visual feedback to the user (tooltips, cursors, ...)
*
* "preSelectedActionCheck" is used to select actions when the gui buttons
* were used to set them, but still require a target (like the guard button)
*
* "hotkeyActionCheck" is used to check the possibility of actions when
* a hotkey is pressed
*
* "actionCheck" is used to check the possibilty of actions without specific
* command. For that, the specificness variable is used
*
* "specificness" is used to determine how specific an action is,
* The lower the number, the more specific an action is, and the bigger
* the chance of selecting that action when multiple actions are possible
*/
var g_UnitActions =
{
"move":
{
"execute": function(target, action, selection, queued, pushFront)
{
Engine.PostNetworkCommand({
"type": "walk",
"entities": selection,
"x": target.x,
"z": target.z,
"queued": queued,
"pushFront": pushFront,
"formation": g_AutoFormation.getDefault()
});
DrawTargetMarker(target);
Engine.GuiInterfaceCall("PlaySound", {
"name": "order_walk",
"entity": action.firstAbleEntity
});
return true;
},
"getActionInfo": function(entState, targetState)
{
if (!entState.unitAI)
return false;
return { "possible": true };
},
"hotkeyActionCheck": function(target, selection)
{
return Engine.HotkeyIsPressed("session.move") &&
this.actionCheck(target, selection);
},
"actionCheck": function(target, selection)
{
let actionInfo = getActionInfo("move", target, selection);
return actionInfo.possible && {
"type": "move",
"firstAbleEntity": actionInfo.entity
};
},
"specificness": 12,
},
"attack-move":
{
"execute": function(target, action, selection, queued, pushFront)
{
let targetClasses;
if (Engine.HotkeyIsPressed("session.attackmoveUnit"))
targetClasses = { "attack": ["Unit"] };
else
targetClasses = { "attack": ["Unit", "Structure"] };
Engine.PostNetworkCommand({
"type": "attack-walk",
"entities": selection,
"x": target.x,
"z": target.z,
"targetClasses": targetClasses,
"queued": queued,
"pushFront": pushFront,
"formation": g_AutoFormation.getNull()
});
DrawTargetMarker(target);
Engine.GuiInterfaceCall("PlaySound", {
"name": "order_walk",
"entity": action.firstAbleEntity
});
return true;
},
"getActionInfo": function(entState, targetState)
{
if (!entState.unitAI)
return false;
return { "possible": true };
},
"hotkeyActionCheck": function(target, selection)
{
return isAttackMovePressed() &&
this.actionCheck(target, selection);
},
"actionCheck": function(target, selection)
{
let actionInfo = getActionInfo("attack-move", target, selection);
return actionInfo.possible && {
"type": "attack-move",
"cursor": "action-attack-move",
"firstAbleEntity": actionInfo.entity
};
},
"specificness": 30,
},
"capture":
{
"execute": function(target, action, selection, queued, pushFront)
{
Engine.PostNetworkCommand({
"type": "attack",
"entities": selection,
"target": action.target,
"allowCapture": true,
"queued": queued,
"pushFront": pushFront,
"formation": g_AutoFormation.getNull()
});
Engine.GuiInterfaceCall("PlaySound", {
"name": "order_attack",
"entity": action.firstAbleEntity
});
return true;
},
"getActionInfo": function(entState, targetState)
{
if (!entState.attack || !targetState || !targetState.capturePoints)
return false;
return {
"possible": Engine.GuiInterfaceCall("CanAttack", {
"entity": entState.id,
"target": targetState.id,
"types": ["Capture"]
})
};
},
"actionCheck": function(target, selection)
{
let actionInfo = getActionInfo("capture", target, selection);
return actionInfo.possible && {
"type": "capture",
"cursor": "action-capture",
"target": target,
"firstAbleEntity": actionInfo.entity
};
},
"specificness": 9,
},
"attack":
{
"execute": function(target, action, selection, queued, pushFront)
{
Engine.PostNetworkCommand({
"type": "attack",
"entities": selection,
"target": action.target,
"queued": queued,
"pushFront": pushFront,
"allowCapture": false,
"formation": g_AutoFormation.getNull()
});
Engine.GuiInterfaceCall("PlaySound", {
"name": "order_attack",
"entity": action.firstAbleEntity
});
return true;
},
"getActionInfo": function(entState, targetState)
{
if (!entState.attack || !targetState || !targetState.hitpoints)
return false;
return {
"possible": Engine.GuiInterfaceCall("CanAttack", {
"entity": entState.id,
"target": targetState.id,
"types": ["!Capture"]
})
};
},
"hotkeyActionCheck": function(target, selection)
{
return Engine.HotkeyIsPressed("session.attack") &&
this.actionCheck(target, selection);
},
"actionCheck": function(target, selection)
{
let actionInfo = getActionInfo("attack", target, selection);
return actionInfo.possible && {
"type": "attack",
"cursor": "action-attack",
"target": target,
"firstAbleEntity": actionInfo.entity
};
},
"specificness": 10,
},
"call-to-arms": {
"execute": function(target, action, selection, queued, pushFront)
{
let targetClasses;
if (Engine.HotkeyIsPressed("session.attackmoveUnit"))
targetClasses = { "attack": ["Unit"] };
else
targetClasses = { "attack": ["Unit", "Structure"] };
Engine.PostNetworkCommand({
"type": "call-to-arms",
"entities": selection,
"target": target,
"targetClasses": targetClasses,
"queued": queued,
"pushFront": pushFront,
"allowCapture": true,
"formation": g_AutoFormation.getNull()
});
return true;
},
"getActionInfo": function(entState, targetState)
{
return { "possible": !!entState.unitAI };
},
"actionCheck": function(target, selection)
{
const actionInfo = getActionInfo("call-to-arms", target, selection);
return actionInfo.possible && {
"type": "call-to-arms",
"cursor": "action-attack",
"target": target,
"firstAbleEntity": actionInfo.entity
};
},
"hotkeyActionCheck": function(target, selection)
{
return Engine.HotkeyIsPressed("session.calltoarms") &&
this.actionCheck(target, selection);
},
"preSelectedActionCheck": function(target, selection)
{
return preSelectedAction == ACTION_CALLTOARMS &&
this.actionCheck(target, selection);
},
"specificness": 50,
},
"patrol":
{
"execute": function(target, action, selection, queued, pushFront)
{
Engine.PostNetworkCommand({
"type": "patrol",
"entities": selection,
"x": target.x,
"z": target.z,
"target": action.target,
"targetClasses": { "attack": g_PatrolTargets },
"queued": queued,
"allowCapture": false,
"formation": g_AutoFormation.getDefault()
});
DrawTargetMarker(target);
Engine.GuiInterfaceCall("PlaySound", {
"name": "order_patrol",
"entity": action.firstAbleEntity
});
return true;
},
"getActionInfo": function(entState, targetState)
{
if (!entState.unitAI || !entState.unitAI.canPatrol)
return false;
return { "possible": true };
},
"hotkeyActionCheck": function(target, selection)
{
return Engine.HotkeyIsPressed("session.patrol") &&
this.actionCheck(target, selection);
},
"preSelectedActionCheck": function(target, selection)
{
return preSelectedAction == ACTION_PATROL &&
this.actionCheck(target, selection);
},
"actionCheck": function(target, selection)
{
let actionInfo = getActionInfo("patrol", target, selection);
return actionInfo.possible && {
"type": "patrol",
"cursor": "action-patrol",
"target": target,
"firstAbleEntity": actionInfo.entity
};
},
"specificness": 37,
},
"heal":
{
"execute": function(target, action, selection, queued, pushFront)
{
Engine.PostNetworkCommand({
"type": "heal",
"entities": selection,
"target": action.target,
"queued": queued,
"pushFront": pushFront,
"formation": g_AutoFormation.getNull()
});
Engine.GuiInterfaceCall("PlaySound", {
"name": "order_heal",
"entity": action.firstAbleEntity
});
return true;
},
"getActionInfo": function(entState, targetState)
{
if (!entState.heal || !targetState ||
!hasClass(targetState, "Unit") || !targetState.needsHeal ||
!playerCheck(entState, targetState, ["Player", "Ally"]) ||
entState.id == targetState.id) // Healers can't heal themselves.
return false;
let unhealableClasses = entState.heal.unhealableClasses;
if (MatchesClassList(targetState.identity.classes, unhealableClasses))
return false;
let healableClasses = entState.heal.healableClasses;
if (!MatchesClassList(targetState.identity.classes, healableClasses))
return false;
return { "possible": true };
},
"actionCheck": function(target, selection)
{
let actionInfo = getActionInfo("heal", target, selection);
return actionInfo.possible && {
"type": "heal",
"cursor": "action-heal",
"target": target,
"firstAbleEntity": actionInfo.entity
};
},
"specificness": 7,
},
// "Fake" action to check if an entity can be ordered to "construct"
// which is handled differently from repair as the target does not exist.
"construct":
{
"preSelectedActionCheck": function(target, selection)
{
let state = GetEntityState(selection[0]);
if (state && state.builder &&
target && target.constructor && target.constructor.name == "PlacementSupport")
return { "type": "construct" };
return false;
},
"specificness": 0,
},
"repair":
{
"execute": function(target, action, selection, queued, pushFront)
{
Engine.PostNetworkCommand({
"type": "repair",
"entities": selection,
"target": action.target,
"autocontinue": true,
"queued": queued,
"pushFront": pushFront,
"formation": g_AutoFormation.getNull()
});
Engine.GuiInterfaceCall("PlaySound", {
"name": action.foundation ? "order_build" : "order_repair",
"entity": action.firstAbleEntity
});
return true;
},
"getActionInfo": function(entState, targetState)
{
if (!entState.builder || !targetState ||
!targetState.needsRepair && !targetState.foundation ||
!playerCheck(entState, targetState, ["Player", "Ally"]))
return false;
return {
"possible": true,
"foundation": targetState.foundation
};
},
"preSelectedActionCheck": function(target, selection)
{
return preSelectedAction == ACTION_REPAIR && (this.actionCheck(target, selection) || {
"type": "none",
"cursor": "action-repair-disabled",
"target": null
});
},
"hotkeyActionCheck": function(target, selection)
{
return Engine.HotkeyIsPressed("session.repair") &&
this.actionCheck(target, selection);
},
"actionCheck": function(target, selection)
{
let actionInfo = getActionInfo("repair", target, selection);
return actionInfo.possible && {
"type": "repair",
"cursor": "action-repair",
"target": target,
"foundation": actionInfo.foundation,
"firstAbleEntity": actionInfo.entity
};
},
"specificness": 11,
},
"gather":
{
"execute": function(target, action, selection, queued, pushFront)
{
Engine.PostNetworkCommand({
"type": "gather",
"entities": selection,
"target": action.target,
"queued": queued,
"pushFront": pushFront,
"formation": g_AutoFormation.getNull()
});
Engine.GuiInterfaceCall("PlaySound", {
"name": "order_gather",
"entity": action.firstAbleEntity
});
return true;
},
"getActionInfo": function(entState, targetState)
{
if (!entState.resourceGatherRates ||
!targetState || !targetState.resourceSupply)
return false;
let resource;
if (entState.resourceGatherRates[targetState.resourceSupply.type.generic + "." + targetState.resourceSupply.type.specific])
resource = targetState.resourceSupply.type.specific;
else if (entState.resourceGatherRates[targetState.resourceSupply.type.generic])
resource = targetState.resourceSupply.type.generic;
if (!resource)
return false;
return {
"possible": true,
"cursor": "action-gather-" + resource
};
},
"actionCheck": function(target, selection)
{
let actionInfo = getActionInfo("gather", target, selection);
return actionInfo.possible && {
"type": "gather",
"cursor": actionInfo.cursor,
"target": target,
"firstAbleEntity": actionInfo.entity
};
},
"specificness": 1,
},
"returnresource":
{
"execute": function(target, action, selection, queued, pushFront)
{
Engine.PostNetworkCommand({
"type": "returnresource",
"entities": selection,
"target": action.target,
"queued": queued,
"pushFront": pushFront,
"formation": g_AutoFormation.getNull()
});
Engine.GuiInterfaceCall("PlaySound", {
"name": "order_gather",
"entity": action.firstAbleEntity
});
return true;
},
"getActionInfo": function(entState, targetState)
{
if (!targetState || !targetState.resourceDropsite)
return false;
let playerState = GetSimState().players[entState.player];
if (playerState.hasSharedDropsites && targetState.resourceDropsite.shared)
{
if (!playerCheck(entState, targetState, ["Player", "MutualAlly"]))
return false;
}
else if (!playerCheck(entState, targetState, ["Player"]))
return false;
if (!entState.resourceCarrying || !entState.resourceCarrying.length)
return false;
let carriedType = entState.resourceCarrying[0].type;
if (targetState.resourceDropsite.types.indexOf(carriedType) == -1)
return false;
return {
"possible": true,
"cursor": "action-return-" + carriedType
};
},
"actionCheck": function(target, selection)
{
let actionInfo = getActionInfo("returnresource", target, selection);
return actionInfo.possible && {
"type": "returnresource",
"cursor": actionInfo.cursor,
"target": target,
"firstAbleEntity": actionInfo.entity
};
},
"specificness": 2,
},
"cancel-setup-trade-route":
{
"execute": function(target, action, selection, queued, pushFront)
{
Engine.PostNetworkCommand({
"type": "cancel-setup-trade-route",
"entities": selection,
"target": action.target,
"queued": queued
});
return true;
},
"getActionInfo": function(entState, targetState)
{
if (!targetState || targetState.foundation || !entState.trader || !targetState.market ||
playerCheck(entState, targetState, ["Enemy"]) ||
!(targetState.market.land && hasClass(entState, "Organic") ||
targetState.market.naval && hasClass(entState, "Ship")))
return false;
let tradingDetails = Engine.GuiInterfaceCall("GetTradingDetails", {
"trader": entState.id,
"target": targetState.id
});
if (!tradingDetails || !tradingDetails.type)
return false;
if (tradingDetails.type == "is first" && !tradingDetails.hasBothMarkets)
return {
"possible": true,
"tooltip": translate("This is the origin trade market.\nRight-click to cancel trade route.")
};
return false;
},
"actionCheck": function(target, selection)
{
let actionInfo = getActionInfo("cancel-setup-trade-route", target, selection);
return actionInfo.possible && {
"type": "cancel-setup-trade-route",
"cursor": "action-cancel-setup-trade-route",
"tooltip": actionInfo.tooltip,
"target": target,
"firstAbleEntity": actionInfo.entity
};
},
"specificness": 2,
},
"setup-trade-route":
{
"execute": function(target, action, selection, queued)
{
Engine.PostNetworkCommand({
"type": "setup-trade-route",
"entities": selection,
"target": action.target,
"source": null,
"route": null,
"queued": queued,
"formation": g_AutoFormation.getNull()
});
Engine.GuiInterfaceCall("PlaySound", {
"name": "order_trade",
"entity": action.firstAbleEntity
});
return true;
},
"getActionInfo": function(entState, targetState)
{
if (!targetState || targetState.foundation || !entState.trader || !targetState.market ||
playerCheck(entState, targetState, ["Enemy"]) ||
!(targetState.market.land && hasClass(entState, "Organic") ||
targetState.market.naval && hasClass(entState, "Ship")))
return false;
let tradingDetails = Engine.GuiInterfaceCall("GetTradingDetails", {
"trader": entState.id,
"target": targetState.id
});
if (!tradingDetails)
return false;
let tooltip;
switch (tradingDetails.type)
{
case "is first":
tooltip = translate("Origin trade market.") + "\n";
if (tradingDetails.hasBothMarkets)
tooltip += sprintf(translate("Gain: %(gain)s"), {
"gain": getTradingTooltip(tradingDetails.gain)
});
else
return false;
break;
case "is second":
tooltip = translate("Destination trade market.") + "\n" +
sprintf(translate("Gain: %(gain)s"), {
"gain": getTradingTooltip(tradingDetails.gain)
});
break;
case "set first":
tooltip = translate("Right-click to set as origin trade market");
break;
case "set second":
if (tradingDetails.gain.traderGain == 0)
return {
"possible": true,
"tooltip": setStringTags(translate("This market is too close to the origin market."), g_DisabledTags),
"disabled": true
};
tooltip = translate("Right-click to set as destination trade market.") + "\n" +
sprintf(translate("Gain: %(gain)s"), {
"gain": getTradingTooltip(tradingDetails.gain)
});
break;
}
return {
"possible": true,
"tooltip": tooltip
};
},
"actionCheck": function(target, selection)
{
let actionInfo = getActionInfo("setup-trade-route", target, selection);
if (actionInfo.disabled)
return {
"type": "none",
"cursor": "action-setup-trade-route-disabled",
"target": null,
"tooltip": actionInfo.tooltip
};
return actionInfo.possible && {
"type": "setup-trade-route",
"cursor": "action-setup-trade-route",
"tooltip": actionInfo.tooltip,
"target": target,
"firstAbleEntity": actionInfo.entity
};
},
"specificness": 0,
},
"occupy-turret":
{
"execute": function(target, action, selection, queued, pushFront)
{
Engine.PostNetworkCommand({
"type": "occupy-turret",
"entities": selection,
"target": action.target,
"queued": queued,
"pushFront": pushFront,
"formation": g_AutoFormation.getNull()
});
Engine.GuiInterfaceCall("PlaySound", {
"name": "order_garrison",
"entity": action.firstAbleEntity
});
return true;
},
"getActionInfo": function(entState, targetState)
{
if (!entState.turretable || !targetState || !targetState.turretHolder ||
!playerCheck(entState, targetState, ["Player", "MutualAlly"]))
return false;
if (!targetState.turretHolder.turretPoints.find(point =>
!point.allowedClasses || MatchesClassList(entState.identity.classes, point.allowedClasses)))
return false;
let occupiedTurrets = targetState.turretHolder.turretPoints.filter(point => point.entity != null);
let tooltip = sprintf(translate("Current turrets: %(occupied)s/%(capacity)s"), {
"occupied": occupiedTurrets.length,
"capacity": targetState.turretHolder.turretPoints.length
});
if (occupiedTurrets.length == targetState.turretHolder.turretPoints.length)
tooltip = coloredText(tooltip, "orange");
return {
"possible": true,
"tooltip": tooltip
};
},
"preSelectedActionCheck": function(target, selection)
{
return preSelectedAction == ACTION_OCCUPY_TURRET && (this.actionCheck(target, selection) || {
"type": "none",
"cursor": "action-occupy-turret-disabled",
"target": null
});
},
"hotkeyActionCheck": function(target, selection)
{
return Engine.HotkeyIsPressed("session.occupyturret") &&
this.actionCheck(target, selection);
},
"actionCheck": function(target, selection)
{
let actionInfo = getActionInfo("occupy-turret", target, selection);
return actionInfo.possible && {
"type": "occupy-turret",
"cursor": "action-occupy-turret",
"tooltip": actionInfo.tooltip,
"target": target,
"firstAbleEntity": actionInfo.entity
};
},
"specificness": 21,
},
"garrison":
{
"execute": function(target, action, selection, queued, pushFront)
{
Engine.PostNetworkCommand({
"type": "garrison",
"entities": selection,
"target": action.target,
"queued": queued,
"pushFront": pushFront,
"formation": g_AutoFormation.getNull()
});
Engine.GuiInterfaceCall("PlaySound", {
"name": "order_garrison",
"entity": action.firstAbleEntity
});
return true;
},
"getActionInfo": function(entState, targetState)
{
if (!entState.garrisonable || !targetState || !targetState.garrisonHolder ||
!playerCheck(entState, targetState, ["Player", "MutualAlly"]))
return false;
let tooltip = sprintf(translate("Current garrison: %(garrisoned)s/%(capacity)s"), {
"garrisoned": targetState.garrisonHolder.occupiedSlots,
"capacity": targetState.garrisonHolder.capacity
});
let extraCount = entState.garrisonable.size;
if (entState.garrisonHolder)
extraCount += entState.garrisonHolder.occupiedSlots;
if (targetState.garrisonHolder.occupiedSlots + extraCount > targetState.garrisonHolder.capacity)
tooltip = coloredText(tooltip, "orange");
if (!MatchesClassList(entState.identity.classes, targetState.garrisonHolder.allowedClasses))
return false;
return {
"possible": true,
"tooltip": tooltip
};
},
"preSelectedActionCheck": function(target, selection)
{
return preSelectedAction == ACTION_GARRISON && (this.actionCheck(target, selection) || {
"type": "none",
"cursor": "action-garrison-disabled",
"target": null
});
},
"hotkeyActionCheck": function(target, selection)
{
return Engine.HotkeyIsPressed("session.garrison") &&
this.actionCheck(target, selection);
},
"actionCheck": function(target, selection)
{
let actionInfo = getActionInfo("garrison", target, selection);
return actionInfo.possible && {
"type": "garrison",
"cursor": "action-garrison",
"tooltip": actionInfo.tooltip,
"target": target,
"firstAbleEntity": actionInfo.entity
};
},
"specificness": 20,
},
"guard":
{
"execute": function(target, action, selection, queued, pushFront)
{
Engine.PostNetworkCommand({
"type": "guard",
"entities": selection,
"target": action.target,
"queued": queued,
"pushFront": pushFront,
"formation": g_AutoFormation.getNull()
});
Engine.GuiInterfaceCall("PlaySound", {
"name": "order_guard",
"entity": action.firstAbleEntity
});
return true;
},
"getActionInfo": function(entState, targetState)
{
if (!targetState || !targetState.guard || entState.id == targetState.id ||
!playerCheck(entState, targetState, ["Player", "Ally"]) ||
!entState.unitAI || !entState.unitAI.canGuard)
return false;
return { "possible": true };
},
"preSelectedActionCheck": function(target, selection)
{
return preSelectedAction == ACTION_GUARD && (this.actionCheck(target, selection) || {
"type": "none",
"cursor": "action-guard-disabled",
"target": null
});
},
"hotkeyActionCheck": function(target, selection)
{
return Engine.HotkeyIsPressed("session.guard") &&
this.actionCheck(target, selection);
},
"actionCheck": function(target, selection)
{
let actionInfo = getActionInfo("guard", target, selection);
return actionInfo.possible && {
"type": "guard",
"cursor": "action-guard",
"target": target,
"firstAbleEntity": actionInfo.entity
};
},
"specificness": 40,
},
"collect-treasure":
{
"execute": function(target, action, selection, queued)
{
Engine.PostNetworkCommand({
"type": "collect-treasure",
"entities": selection,
"target": action.target,
"queued": queued,
"formation": g_AutoFormation.getNull()
});
Engine.GuiInterfaceCall("PlaySound", {
"name": "order_collect_treasure",
"entity": action.firstAbleEntity
});
return true;
},
"getActionInfo": function(entState, targetState)
{
if (!entState.treasureCollector ||
!targetState || !targetState.treasure)
return false;
return {
"possible": true,
"cursor": "action-collect-treasure"
};
},
"actionCheck": function(target, selection)
{
let actionInfo = getActionInfo("collect-treasure", target, selection);
return actionInfo.possible && {
"type": "collect-treasure",
"cursor": actionInfo.cursor,
"target": target,
"firstAbleEntity": actionInfo.entity
};
},
"specificness": 1,
},
"remove-guard":
{
"execute": function(target, action, selection, queued, pushFront)
{
Engine.PostNetworkCommand({
"type": "remove-guard",
"entities": selection,
"target": action.target,
"queued": queued,
"pushFront": pushFront
});
Engine.GuiInterfaceCall("PlaySound", {
"name": "order_guard",
"entity": action.firstAbleEntity
});
return true;
},
"getActionInfo": function(entState, targetState)
{
if (!entState.unitAI || !entState.unitAI.isGuarding)
return false;
return { "possible": true };
},
"hotkeyActionCheck": function(target, selection)
{
return Engine.HotkeyIsPressed("session.guard") &&
this.actionCheck(target, selection);
},
"actionCheck": function(target, selection)
{
let actionInfo = getActionInfo("remove-guard", target, selection);
return actionInfo.possible && {
"type": "remove-guard",
"cursor": "action-remove-guard",
"firstAbleEntity": actionInfo.entity
};
},
"specificness": 41,
},
"set-rallypoint":
{
"execute": function(target, action, selection, queued, pushFront)
{
// if there is a position set in the action then use this so that when setting a
// rally point on an entity it is centered on that entity
if (action.position)
target = action.position;
Engine.PostNetworkCommand({
"type": "set-rallypoint",
"entities": selection,
"x": target.x,
"z": target.z,
"data": action.data,
"queued": queued
});
// Display rally point at the new coordinates, to avoid display lag
Engine.GuiInterfaceCall("DisplayRallyPoint", {
"entities": selection,
"x": target.x,
"z": target.z,
"queued": queued
});
return true;
},
"getActionInfo": function(entState, targetState)
{
if (!entState.rallyPoint)
return false;
// Don't allow the rally point to be set on any of the currently selected entities (used for unset)
// except if the autorallypoint hotkey is pressed and the target can produce entities.
if (targetState && (!Engine.HotkeyIsPressed("session.autorallypoint") ||
- !targetState.production ||
- !targetState.production.entities.length))
+ !targetState.trainer ||
+ !targetState.trainer.entities.length))
for (const ent of g_Selection.toList())
if (targetState.id == ent)
return false;
let tooltip;
let disabled = false;
// default to walking there (or attack-walking if hotkey pressed)
let data = { "command": "walk" };
let cursor = "";
if (isAttackMovePressed())
{
let targetClasses;
if (Engine.HotkeyIsPressed("session.attackmoveUnit"))
targetClasses = { "attack": ["Unit"] };
else
targetClasses = { "attack": ["Unit", "Structure"] };
data.command = "attack-walk";
data.targetClasses = targetClasses;
cursor = "action-attack-move";
}
if (Engine.HotkeyIsPressed("session.repair") && targetState &&
(targetState.needsRepair || targetState.foundation) &&
playerCheck(entState, targetState, ["Player", "Ally"]))
{
data.command = "repair";
data.target = targetState.id;
cursor = "action-repair";
}
else if (targetState && targetState.garrisonHolder &&
playerCheck(entState, targetState, ["Player", "MutualAlly"]))
{
data.command = "garrison";
data.target = targetState.id;
cursor = "action-garrison";
tooltip = sprintf(translate("Current garrison: %(garrisoned)s/%(capacity)s"), {
"garrisoned": targetState.garrisonHolder.occupiedSlots,
"capacity": targetState.garrisonHolder.capacity
});
if (targetState.garrisonHolder.occupiedSlots >=
targetState.garrisonHolder.capacity)
tooltip = coloredText(tooltip, "orange");
}
else if (targetState && targetState.turretHolder &&
playerCheck(entState, targetState, ["Player", "MutualAlly"]))
{
data.command = "occupy-turret";
data.target = targetState.id;
cursor = "action-garrison";
let occupiedTurrets = targetState.turretHolder.turretPoints.filter(point => point.entity != null);
tooltip = sprintf(translate("Current turrets: %(occupied)s/%(capacity)s"), {
"occupied": occupiedTurrets.length,
"capacity": targetState.turretHolder.turretPoints.length
});
if (occupiedTurrets.length >= targetState.turretHolder.turretPoints.length)
tooltip = coloredText(tooltip, "orange");
}
else if (targetState && targetState.resourceSupply)
{
let resourceType = targetState.resourceSupply.type;
cursor = "action-gather-" + resourceType.specific;
data.command = "gather-near-position";
data.resourceType = resourceType;
data.resourceTemplate = targetState.template;
if (!targetState.speed)
{
data.command = "gather";
data.target = targetState.id;
}
}
else if (targetState && targetState.treasure)
{
cursor = "action-collect-treasure";
data.command = "collect-treasure-near-position";
if (!targetState.speed)
{
data.command = "collect-treasure";
data.target = targetState.id;
}
}
else if (entState.market && targetState && targetState.market &&
entState.id != targetState.id &&
(!entState.market.naval || targetState.market.naval) &&
!playerCheck(entState, targetState, ["Enemy"]))
{
// Find a trader (if any) that this structure can train.
let trader;
- if (entState.production && entState.production.entities.length)
- for (let i = 0; i < entState.production.entities.length; ++i)
- if ((trader = GetTemplateData(entState.production.entities[i]).trader))
+ if (entState.trainer?.entities?.length)
+ for (let i = 0; i < entState.trainer.entities.length; ++i)
+ if ((trader = GetTemplateData(entState.trainer.entities[i]).trader))
break;
let traderData = {
"firstMarket": entState.id,
"secondMarket": targetState.id,
"template": trader
};
let gain = Engine.GuiInterfaceCall("GetTradingRouteGain", traderData);
if (gain)
{
data.command = "trade";
data.target = traderData.secondMarket;
data.source = traderData.firstMarket;
cursor = "action-setup-trade-route";
if (gain.traderGain)
tooltip = translate("Right-click to establish a default route for new traders.") + "\n" +
sprintf(
trader ?
translate("Gain: %(gain)s") :
translate("Expected gain: %(gain)s"),
{ "gain": getTradingTooltip(gain) });
else
{
disabled = true;
tooltip = setStringTags(translate("This market is too close to the origin market."), g_DisabledTags);
cursor = "action-setup-trade-route-disabled";
}
}
}
else if (targetState && (targetState.needsRepair || targetState.foundation) && playerCheck(entState, targetState, ["Ally"]))
{
data.command = "repair";
data.target = targetState.id;
cursor = "action-repair";
}
else if (targetState && playerCheck(entState, targetState, ["Enemy"]))
{
data.target = targetState.id;
data.command = "attack";
cursor = "action-attack";
}
return {
"possible": true,
"data": data,
"position": targetState && targetState.position,
"cursor": cursor,
"disabled": disabled,
"tooltip": tooltip
};
},
"hotkeyActionCheck": function(target, selection)
{
// Hotkeys are checked in the actionInfo.
return this.actionCheck(target, selection);
},
"actionCheck": function(target, selection)
{
// We want commands to units take precedence.
if (selection.some(ent => {
let entState = GetEntityState(ent);
return entState && !!entState.unitAI;
}))
return false;
let actionInfo = getActionInfo("set-rallypoint", target, selection);
if (actionInfo.disabled)
return {
"type": "none",
"cursor": actionInfo.cursor,
"target": null,
"tooltip": actionInfo.tooltip
};
return actionInfo.possible && {
"type": "set-rallypoint",
"cursor": actionInfo.cursor,
"data": actionInfo.data,
"tooltip": actionInfo.tooltip,
"position": actionInfo.position,
"firstAbleEntity": actionInfo.entity
};
},
"specificness": 6,
},
"unset-rallypoint":
{
"execute": function(target, action, selection, queued, pushFront)
{
Engine.PostNetworkCommand({
"type": "unset-rallypoint",
"entities": selection
});
// Remove displayed rally point
Engine.GuiInterfaceCall("DisplayRallyPoint", {
"entities": []
});
return true;
},
"getActionInfo": function(entState, targetState)
{
if (!targetState ||
entState.id != targetState.id || entState.unitAI ||
!entState.rallyPoint || !entState.rallyPoint.position)
return false;
return { "possible": true };
},
"actionCheck": function(target, selection)
{
let actionInfo = getActionInfo("unset-rallypoint", target, selection);
return actionInfo.possible && {
"type": "unset-rallypoint",
"cursor": "action-unset-rally",
"firstAbleEntity": actionInfo.entity
};
},
"specificness": 11,
},
// This is a "fake" action to show a failure cursor
// when only uncontrollable entities are selected.
"uncontrollable":
{
"execute": function(target, action, selection, queued)
{
return true;
},
"actionCheck": function(target, selection)
{
// Only show this action if all entities are marked uncontrollable.
let playerState = g_SimState.players[g_ViewedPlayer];
if (playerState && playerState.controlsAll || selection.some(ent => {
let entState = GetEntityState(ent);
return entState && entState.identity && entState.identity.controllable;
}))
return false;
return {
"type": "none",
"cursor": "cursor-no",
"tooltip": translatePlural("This entity cannot be controlled.", "These entities cannot be controlled.", selection.length)
};
},
"specificness": 100,
},
"none":
{
"execute": function(target, action, selection, queued)
{
return true;
},
"specificness": 100,
},
};
var g_UnitActionsSortedKeys = Object.keys(g_UnitActions).sort((a, b) => g_UnitActions[a].specificness - g_UnitActions[b].specificness);
/**
* Info and actions for the entity commands
* Currently displayed in the bottom of the central panel
*/
var g_EntityCommands =
{
"unload-all": {
"getInfo": function(entStates)
{
let count = 0;
for (let entState of entStates)
{
if (!entState.garrisonHolder)
continue;
if (allowedPlayersCheck([entState], ["Player"]))
count += entState.garrisonHolder.entities.length;
else
for (let entity of entState.garrisonHolder.entities)
if (allowedPlayersCheck([GetEntityState(entity)], ["Player"]))
++count;
}
if (!count)
return false;
return {
"tooltip": colorizeHotkey("%(hotkey)s" + " ", "session.unload") +
translate("Unload All."),
"icon": "garrison-out.png",
"count": count,
"enabled": true
};
},
"execute": function()
{
unloadAll();
},
"allowedPlayers": ["Player", "Ally"]
},
"unload-all-turrets": {
"getInfo": function(entStates)
{
let count = 0;
for (let entState of entStates)
{
if (!entState.turretHolder)
continue;
if (allowedPlayersCheck([entState], ["Player"]))
count += entState.turretHolder.turretPoints.filter(turretPoint => turretPoint.entity && turretPoint.ejectable).length;
else
for (let turretPoint of entState.turretHolder.turretPoints)
if (turretPoint.entity && allowedPlayersCheck([GetEntityState(turretPoint.entity)], ["Player"]))
++count;
}
if (!count)
return false;
return {
"tooltip": colorizeHotkey("%(hotkey)s" + " ", "session.unloadturrets") +
translate("Unload Turrets."),
"icon": "garrison-out.png",
"count": count,
"enabled": true
};
},
"execute": function()
{
unloadAllTurrets();
},
"allowedPlayers": ["Player", "Ally"]
},
"delete": {
"getInfo": function(entStates)
{
return entStates.some(entState => !isUndeletable(entState)) ?
{
"tooltip":
colorizeHotkey("%(hotkey)s" + " ", "session.kill") +
translate("Destroy the selected units or structures.") + "\n" +
colorizeHotkey(
translate("Use %(hotkey)s to avoid the confirmation dialog."),
"session.noconfirmation"
),
"icon": "kill_small.png",
"enabled": true
} :
{
// Get all delete reasons and remove duplications
"tooltip": entStates.map(entState => isUndeletable(entState))
.filter((reason, pos, self) =>
self.indexOf(reason) == pos && reason
).join("\n"),
"icon": "kill_small_disabled.png",
"enabled": false
};
},
"execute": function(entStates)
{
let entityIDs = entStates.reduce(
(ids, entState) => {
if (!isUndeletable(entState))
ids.push(entState.id);
return ids;
},
[]);
if (!entityIDs.length)
return;
let deleteSelection = () => Engine.PostNetworkCommand({
"type": "delete-entities",
"entities": entityIDs
});
if (Engine.HotkeyIsPressed("session.noconfirmation"))
deleteSelection();
else
(new DeleteSelectionConfirmation(deleteSelection)).display();
},
"allowedPlayers": ["Player"]
},
"stop": {
"getInfo": function(entStates)
{
if (entStates.every(entState => !entState.unitAI))
return false;
return {
"tooltip": colorizeHotkey("%(hotkey)s" + " ", "session.stop") +
translate("Abort the current order."),
"icon": "stop.png",
"enabled": true
};
},
"execute": function(entStates)
{
if (entStates.length)
stopUnits(entStates.map(entState => entState.id));
},
"allowedPlayers": ["Player"]
},
"call-to-arms": {
"getInfo": function(entStates)
{
const classes = ["Soldier", "Warship", "Siege", "Healer"];
if (entStates.every(entState => !MatchesClassList(entState.identity.classes, classes)))
return false;
return {
"tooltip": colorizeHotkey("%(hotkey)s" + " ", "session.calltoarms") +
translate("Send the selected units on attack move to the specified location after dropping resources."),
"icon": "call-to-arms.png",
"enabled": true
};
},
"execute": function(entStates)
{
inputState = INPUT_PRESELECTEDACTION;
preSelectedAction = ACTION_CALLTOARMS;
},
"allowedPlayers": ["Player"]
},
"garrison": {
"getInfo": function(entStates)
{
if (entStates.every(entState => !entState.garrisonable ||
entState.garrisonable.holder != INVALID_ENTITY))
return false;
return {
"tooltip": colorizeHotkey("%(hotkey)s" + " ", "session.garrison") +
translate("Order the selected units to garrison in a structure or unit."),
"icon": "garrison.png",
"enabled": true
};
},
"execute": function()
{
inputState = INPUT_PRESELECTEDACTION;
preSelectedAction = ACTION_GARRISON;
},
"allowedPlayers": ["Player"]
},
"occupy-turret": {
"getInfo": function(entStates)
{
if (entStates.every(entState => !entState.turretable ||
entState.turretable.holder != INVALID_ENTITY))
return false;
return {
"tooltip": colorizeHotkey("%(hotkey)s" + " ", "session.occupyturret") +
translate("Order the selected units to occupy a turret point."),
"icon": "occupy-turret.png",
"enabled": true
};
},
"execute": function()
{
inputState = INPUT_PRESELECTEDACTION;
preSelectedAction = ACTION_OCCUPY_TURRET;
},
"allowedPlayers": ["Player"]
},
"leave-turret": {
"getInfo": function(entStates)
{
if (entStates.every(entState => !entState.turretable ||
entState.turretable.holder == INVALID_ENTITY ||
!entState.turretable.ejectable))
return false;
return {
"tooltip": translate("Unload"),
"icon": "leave-turret.png",
"enabled": true
};
},
"execute": function(entStates)
{
leaveTurretPoints();
},
"allowedPlayers": ["Player"]
},
"repair": {
"getInfo": function(entStates)
{
if (entStates.every(entState => !entState.builder))
return false;
return {
"tooltip": colorizeHotkey("%(hotkey)s" + " ", "session.repair") +
translate("Order the selected units to repair a structure, ship, or siege engine."),
"icon": "repair.png",
"enabled": true
};
},
"execute": function()
{
inputState = INPUT_PRESELECTEDACTION;
preSelectedAction = ACTION_REPAIR;
},
"allowedPlayers": ["Player"]
},
"focus-rally": {
"getInfo": function(entStates)
{
if (entStates.every(entState => !entState.rallyPoint))
return false;
return {
"tooltip": colorizeHotkey("%(hotkey)s" + " ", "camera.rallypointfocus") +
translate("Focus on Rally Point."),
"icon": "focus-rally.png",
"enabled": true
};
},
"execute": function(entStates)
{
// TODO: Would be nicer to cycle between the rallypoints of multiple entities instead of just using the first
let focusTarget;
for (let entState of entStates)
if (entState.rallyPoint && entState.rallyPoint.position)
{
focusTarget = entState.rallyPoint.position;
break;
}
if (!focusTarget)
for (let entState of entStates)
if (entState.position)
{
focusTarget = entState.position;
break;
}
if (focusTarget)
Engine.CameraMoveTo(focusTarget.x, focusTarget.z);
},
"allowedPlayers": ["Player", "Observer"]
},
"back-to-work": {
"getInfo": function(entStates)
{
if (entStates.every(entState => !entState.unitAI || !entState.unitAI.hasWorkOrders))
return false;
return {
"tooltip": colorizeHotkey("%(hotkey)s" + " ", "session.backtowork") +
translate("Back to Work"),
"icon": "back-to-work.png",
"enabled": true
};
},
"execute": function()
{
backToWork();
},
"allowedPlayers": ["Player"]
},
"add-guard": {
"getInfo": function(entStates)
{
if (entStates.every(entState =>
!entState.unitAI || !entState.unitAI.canGuard || entState.unitAI.isGuarding))
return false;
return {
"tooltip": colorizeHotkey("%(hotkey)s" + " ", "session.guard") +
translate("Order the selected units to guard a structure or unit."),
"icon": "add-guard.png",
"enabled": true
};
},
"execute": function()
{
inputState = INPUT_PRESELECTEDACTION;
preSelectedAction = ACTION_GUARD;
},
"allowedPlayers": ["Player"]
},
"remove-guard": {
"getInfo": function(entStates)
{
if (entStates.every(entState => !entState.unitAI || !entState.unitAI.isGuarding))
return false;
return {
"tooltip": translate("Remove guard"),
"icon": "remove-guard.png",
"enabled": true
};
},
"execute": function()
{
removeGuard();
},
"allowedPlayers": ["Player"]
},
"select-trading-goods": {
"getInfo": function(entStates)
{
if (entStates.every(entState => !entState.market))
return false;
return {
"tooltip": translate("Barter & Trade"),
"icon": "economics.png",
"enabled": true
};
},
"execute": function()
{
g_TradeDialog.toggle();
},
"allowedPlayers": ["Player"]
},
"patrol": {
"getInfo": function(entStates)
{
if (!entStates.some(entState => entState.unitAI && entState.unitAI.canPatrol))
return false;
return {
"tooltip": colorizeHotkey("%(hotkey)s" + " ", "session.patrol") +
translate("Patrol") + "\n" +
translate("Attack all encountered enemy units while avoiding structures."),
"icon": "patrol.png",
"enabled": true
};
},
"execute": function()
{
inputState = INPUT_PRESELECTEDACTION;
preSelectedAction = ACTION_PATROL;
},
"allowedPlayers": ["Player"]
},
"share-dropsite": {
"getInfo": function(entStates)
{
let sharableEntities = entStates.filter(
entState => entState.resourceDropsite && entState.resourceDropsite.sharable);
if (!sharableEntities.length)
return false;
// Returns if none of the entities belong to a player with a mutual ally.
if (entStates.every(entState => !GetSimState().players[entState.player].isMutualAlly.some(
(isAlly, playerId) => isAlly && playerId != entState.player)))
return false;
return sharableEntities.some(entState => !entState.resourceDropsite.shared) ?
{
"tooltip": translate("Press to allow allies to use this dropsite"),
"icon": "locked_small.png",
"enabled": true
} :
{
"tooltip": translate("Press to prevent allies from using this dropsite"),
"icon": "unlocked_small.png",
"enabled": true
};
},
"execute": function(entStates)
{
let sharableEntities = entStates.filter(
entState => entState.resourceDropsite && entState.resourceDropsite.sharable);
if (sharableEntities)
Engine.PostNetworkCommand({
"type": "set-dropsite-sharing",
"entities": sharableEntities.map(entState => entState.id),
"shared": sharableEntities.some(entState => !entState.resourceDropsite.shared)
});
},
"allowedPlayers": ["Player"]
},
"is-dropsite-shared": {
"getInfo": function(entStates)
{
let shareableEntities = entStates.filter(
entState => entState.resourceDropsite && entState.resourceDropsite.sharable);
if (!shareableEntities.length)
return false;
let player = Engine.GetPlayerID();
let simState = GetSimState();
if (!g_IsObserver && !simState.players[player].hasSharedDropsites ||
shareableEntities.every(entState => controlsPlayer(entState.player)))
return false;
if (!shareableEntities.every(entState => entState.resourceDropsite.shared))
return {
"tooltip": translate("The use of this dropsite is prohibited"),
"icon": "locked_small.png",
"enabled": false
};
return {
"tooltip": g_IsObserver ? translate("Allies are allowed to use this dropsite.") :
translate("You are allowed to use this dropsite"),
"icon": "unlocked_small.png",
"enabled": false
};
},
"execute": function(entState)
{
// This command button is always disabled.
},
"allowedPlayers": ["Ally", "Observer"]
},
"autoqueue-on": {
"getInfo": function(entStates)
{
- if (entStates.every(entState => !entState.production || !entState.production.entities.length || entState.production.autoqueue))
+ if (entStates.every(entState => !entState.trainer || !entState.trainer.entities.length || entState.production.autoqueue))
return false;
return {
"tooltip": colorizeHotkey("%(hotkey)s" + " ", "session.queueunit.autoqueueon") +
translate("Activate auto-queue for selected structures."),
"icon": "autoqueue-on.png",
"enabled": true
};
},
"execute": function(entStates)
{
if (entStates.length)
turnAutoQueueOn();
},
"allowedPlayers": ["Player"]
},
"autoqueue-off": {
"getInfo": function(entStates)
{
- if (entStates.every(entState => !entState.production || !entState.production.entities.length || !entState.production.autoqueue))
+ if (entStates.every(entState => !entState.trainer || !entState.trainer.entities.length || !entState.production.autoqueue))
return false;
return {
"tooltip": colorizeHotkey("%(hotkey)s" + " ", "session.queueunit.autoqueueoff") +
translate("Deactivate auto-queue for selected structures."),
"icon": "autoqueue-off.png",
"enabled": true
};
},
"execute": function(entStates)
{
if (entStates.length)
turnAutoQueueOff();
},
"allowedPlayers": ["Player"]
},
};
function playerCheck(entState, targetState, validPlayers)
{
let playerState = GetSimState().players[entState.player];
for (let player of validPlayers)
if (player == "Gaia" && targetState.player == 0 ||
player == "Player" && targetState.player == entState.player ||
playerState["is" + player] && playerState["is" + player][targetState.player])
return true;
return false;
}
/**
* Checks whether the entities have the right diplomatic status
* with respect to the currently active player.
* Also "Observer" can be used.
*
* @param {Object[]} entStates - An array containing the entity states to check.
* @param {string[]} validPlayers - An array containing the diplomatic statuses.
*
* @return {boolean} - Whether the currently active player is allowed.
*/
function allowedPlayersCheck(entStates, validPlayers)
{
// Assume we can only select entities from one player,
// or it does not matter (e.g. observer).
let targetState = entStates[0];
let playerState = GetSimState().players[Engine.GetPlayerID()];
return validPlayers.some(player =>
player == "Observer" && g_IsObserver ||
player == "Player" && controlsPlayer(targetState.player) ||
playerState && playerState["is" + player] && playerState["is" + player][targetState.player]);
}
function hasClass(entState, className)
{
// note: use the functions in globalscripts/Templates.js for more versatile matching
return entState.identity && entState.identity.classes.indexOf(className) != -1;
}
/**
* Keep in sync with Commands.js.
*/
function isUndeletable(entState)
{
let playerState = g_SimState.players[entState.player];
if (playerState && playerState.controlsAll)
return false;
if (entState.resourceSupply && entState.resourceSupply.killBeforeGather)
return translate("The entity has to be killed before it can be gathered from");
if (entState.capturePoints && entState.capturePoints[entState.player] < entState.maxCapturePoints / 2)
return translate("You cannot destroy this entity as you own less than half the capture points");
if (!entState.identity.canDelete)
return translate("This entity is undeletable");
return false;
}
function DrawTargetMarker(target)
{
Engine.GuiInterfaceCall("AddTargetMarker", {
"template": g_TargetMarker.move,
"x": target.x,
"z": target.z
});
}
function displayFlare(target, playerID)
{
Engine.GuiInterfaceCall("AddTargetMarker", {
"template": g_TargetMarker.map_flare,
"x": target.x,
"z": target.z,
"owner": playerID
});
g_MiniMapPanel.flare(target, playerID);
}
function getCommandInfo(command, entStates)
{
return entStates && g_EntityCommands[command] &&
allowedPlayersCheck(entStates, g_EntityCommands[command].allowedPlayers) &&
g_EntityCommands[command].getInfo(entStates);
}
function getActionInfo(action, target, selection)
{
if (!selection || !selection.length || !GetEntityState(selection[0]))
return { "possible": false };
// Look at the first targeted entity
// (TODO: maybe we eventually want to look at more, and be more context-sensitive?
// e.g. prefer to attack an enemy unit, even if some friendly units are closer to the mouse)
let targetState = GetEntityState(target);
let simState = GetSimState();
let playerState = g_SimState.players[g_ViewedPlayer];
// Check if any entities in the selection can do some of the available actions.
for (let entityID of selection)
{
let entState = GetEntityState(entityID);
if (!entState)
continue;
if (playerState && !playerState.controlsAll && !entState.identity.controllable)
continue;
if (g_UnitActions[action] && g_UnitActions[action].getActionInfo)
{
let r = g_UnitActions[action].getActionInfo(entState, targetState, simState);
if (r && r.possible)
{
r.entity = entityID;
return r;
}
}
}
return { "possible": false };
}
Index: ps/trunk/binaries/data/mods/public/gui/session/unit_commands.js
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/session/unit_commands.js (revision 25999)
+++ ps/trunk/binaries/data/mods/public/gui/session/unit_commands.js (revision 26000)
@@ -1,234 +1,234 @@
// The number of currently visible buttons (used to optimise showing/hiding)
var g_unitPanelButtons = {
"Selection": 0,
"Queue": 0,
"Formation": 0,
"Garrison": 0,
"Training": 0,
"Research": 0,
"Alert": 0,
"Barter": 0,
"Construction": 0,
"Command": 0,
"Stance": 0,
"Gate": 0,
"Pack": 0,
"Upgrade": 0
};
/**
* Set the position of a panel object according to the index,
* from left to right, from top to bottom.
* Will wrap around to subsequent rows if the index
* is larger than rowLength.
*/
function setPanelObjectPosition(object, index, rowLength, vMargin = 1, hMargin = 1)
{
var size = object.size;
// horizontal position
var oWidth = size.right - size.left;
var hIndex = index % rowLength;
size.left = hIndex * (oWidth + vMargin);
size.right = size.left + oWidth;
// vertical position
var oHeight = size.bottom - size.top;
var vIndex = Math.floor(index / rowLength);
size.top = vIndex * (oHeight + hMargin);
size.bottom = size.top + oHeight;
object.size = size;
}
/**
* Helper function for updateUnitCommands; sets up "unit panels"
* (i.e. panels with rows of icons) for the currently selected unit.
*
* @param guiName Short identifier string of this panel. See g_SelectionPanels.
* @param unitEntStates Entity states of the selected units
* @param playerState Player state
*/
function setupUnitPanel(guiName, unitEntStates, playerState)
{
if (!g_SelectionPanels[guiName])
{
error("unknown guiName used '" + guiName + "'");
return;
}
let items = g_SelectionPanels[guiName].getItems(unitEntStates);
if (!items || !items.length)
return;
let numberOfItems = Math.min(items.length, g_SelectionPanels[guiName].getMaxNumberOfItems());
let rowLength = g_SelectionPanels[guiName].rowLength || 8;
if (g_SelectionPanels[guiName].resizePanel)
g_SelectionPanels[guiName].resizePanel(numberOfItems, rowLength);
for (let i = 0; i < numberOfItems; ++i)
{
let data = {
"i": i,
"item": items[i],
"playerState": playerState,
"player": unitEntStates[0].player,
"unitEntStates": unitEntStates,
"rowLength": rowLength,
"numberOfItems": numberOfItems,
// depending on the XML, some of the GUI objects may be undefined
"button": Engine.GetGUIObjectByName("unit" + guiName + "Button[" + i + "]"),
"icon": Engine.GetGUIObjectByName("unit" + guiName + "Icon[" + i + "]"),
"guiSelection": Engine.GetGUIObjectByName("unit" + guiName + "Selection[" + i + "]"),
"countDisplay": Engine.GetGUIObjectByName("unit" + guiName + "Count[" + i + "]")
};
if (data.button)
{
data.button.hidden = false;
data.button.enabled = true;
data.button.tooltip = "";
data.button.caption = "";
}
if (g_SelectionPanels[guiName].setupButton &&
!g_SelectionPanels[guiName].setupButton(data))
continue;
// TODO: we should require all entities to have icons, so this case never occurs
if (data.icon && !data.icon.sprite)
data.icon.sprite = "BackgroundBlack";
}
// Hide any buttons we're no longer using
for (let i = numberOfItems; i < g_unitPanelButtons[guiName]; ++i)
if (g_SelectionPanels[guiName].hideItem)
g_SelectionPanels[guiName].hideItem(i, rowLength);
else
Engine.GetGUIObjectByName("unit" + guiName + "Button[" + i + "]").hidden = true;
g_unitPanelButtons[guiName] = numberOfItems;
g_SelectionPanels[guiName].used = true;
}
/**
* Updates the selection panels where buttons are supposed to
* depend on the context.
* Runs in the main session loop via updateSelectionDetails().
* Delegates to setupUnitPanel to set up individual subpanels,
* appropriately activated depending on the selected unit's state.
*
* @param entStates Entity states of the selected units
* @param supplementalDetailsPanel Reference to the
* "supplementalSelectionDetails" GUI Object
* @param commandsPanel Reference to the "commandsPanel" GUI Object
*/
function updateUnitCommands(entStates, supplementalDetailsPanel, commandsPanel)
{
for (let panel in g_SelectionPanels)
g_SelectionPanels[panel].used = false;
// Get player state to check some constraints
// e.g. presence of a hero or build limits.
let playerStates = GetSimState().players;
let playerState = playerStates[Engine.GetPlayerID()];
setupUnitPanel("Selection", entStates, playerStates[entStates[0].player]);
// Command panel always shown for it can contain commands
// for which the entity does not need to be owned.
setupUnitPanel("Command", entStates, playerState);
if (g_IsObserver || entStates.every(entState =>
controlsPlayer(entState.player) &&
(!entState.identity || entState.identity.controllable)) ||
playerState.controlsAll)
{
for (let guiName of g_PanelsOrder)
{
if (g_SelectionPanels[guiName].conflictsWith &&
g_SelectionPanels[guiName].conflictsWith.some(p => g_SelectionPanels[p].used))
continue;
setupUnitPanel(guiName, entStates, playerStates[entStates[0].player]);
}
supplementalDetailsPanel.hidden = false;
commandsPanel.hidden = false;
}
else if (playerState.isMutualAlly[entStates[0].player])
{
// TODO if there's a second panel needed for a different player
// we should consider adding the players list to g_SelectionPanels
setupUnitPanel("Garrison", entStates, playerState);
supplementalDetailsPanel.hidden = !g_SelectionPanels.Garrison.used;
commandsPanel.hidden = true;
}
else
{
supplementalDetailsPanel.hidden = true;
commandsPanel.hidden = true;
}
// Hides / unhides Unit Panels (panels should be grouped by type, not by order, but we will leave that for another time)
for (let panelName in g_SelectionPanels)
Engine.GetGUIObjectByName("unit" + panelName + "Panel").hidden = !g_SelectionPanels[panelName].used;
}
// Force hide commands panels
function hideUnitCommands()
{
for (var panelName in g_SelectionPanels)
Engine.GetGUIObjectByName("unit" + panelName + "Panel").hidden = true;
}
// Get all of the available entities which can be trained by the selected entities
function getAllTrainableEntities(selection)
{
let trainableEnts = [];
// Get all buildable and trainable entities
for (let ent of selection)
{
let state = GetEntityState(ent);
- if (state && state.production && state.production.entities.length)
- trainableEnts = trainableEnts.concat(state.production.entities);
+ if (state?.trainer?.entities?.length)
+ trainableEnts = trainableEnts.concat(state.trainer.entities);
}
// Remove duplicates
removeDupes(trainableEnts);
return trainableEnts;
}
function getAllTrainableEntitiesFromSelection()
{
if (!g_allTrainableEntities)
g_allTrainableEntities = getAllTrainableEntities(g_Selection.toList());
return g_allTrainableEntities;
}
// Get all of the available entities which can be built by the selected entities
function getAllBuildableEntities(selection)
{
return Engine.GuiInterfaceCall("GetAllBuildableEntities", { "entities": selection });
}
function getAllBuildableEntitiesFromSelection()
{
if (!g_allBuildableEntities)
g_allBuildableEntities = getAllBuildableEntities(g_Selection.toList());
return g_allBuildableEntities;
}
function getNumberOfRightPanelButtons()
{
var sum = 0;
for (let prop of ["Construction", "Training", "Pack", "Gate", "Upgrade"])
if (g_SelectionPanels[prop].used)
sum += g_unitPanelButtons[prop];
return sum;
}
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 25999)
+++ ps/trunk/binaries/data/mods/public/simulation/ai/common-api/entity.js (revision 26000)
@@ -1,1027 +1,1026 @@
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);
},
// specifically
"phase": function() {
let techRequired = this.requiredTech();
if (!techRequired)
return 0;
if (techRequired == "phase_village")
return 1;
if (techRequired == "phase_town")
return 2;
if (techRequired == "phase_city")
return 3;
if (techRequired.startsWith("phase_"))
return 4;
return 0;
},
"cost": function(productionQueue) {
if (!this.get("Cost"))
return {};
let ret = {};
for (let type in this.get("Cost/Resources"))
ret[type] = +this.get("Cost/Resources/" + type);
return ret;
},
"costSum": function(productionQueue) {
let cost = this.cost(productionQueue);
if (!cost)
return 0;
let ret = 0;
for (let type in cost)
ret += cost[type];
return ret;
},
"techCostMultiplier": function(type) {
- return +(this.get("ProductionQueue/TechCostMultiplier/"+type) || 1);
+ 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) {
- let templates = this.get("ProductionQueue/Entities/_string");
+ 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) {
- let templates = this.get("ProductionQueue/Technologies/_string");
+ 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(productionQueue) {
+ "buildTime": function(researcher) {
let time = +this.get("Cost/BuildTime");
- if (productionQueue)
- time *= productionQueue.techCostMultiplier("time");
+ 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);
},
"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._entity.resourceSupplyAmount;
},
"resourceSupplyNumGatherers": function()
{
return this._entity.resourceSupplyNumGatherers;
},
"isFull": function()
{
if (this._entity.resourceSupplyNumGatherers !== undefined)
return this.maxGatherers() === this._entity.resourceSupplyNumGatherers;
return undefined;
},
"resourceCarrying": function() {
return this._entity.resourceCarrying;
},
"currentGatherRate": function() {
// returns the gather rate for the current target if applicable.
if (!this.get("ResourceGatherer"))
return undefined;
if (this.unitAIOrderData().length &&
this.unitAIState().split(".")[1] == "GATHER")
{
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, promotedTypes, pushFront = false)
+ "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,
- "promoted": promotedTypes,
"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/common-api/technology.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/ai/common-api/technology.js (revision 25999)
+++ ps/trunk/binaries/data/mods/public/simulation/ai/common-api/technology.js (revision 26000)
@@ -1,138 +1,138 @@
LoadModificationTemplates();
var API3 = function(m)
{
/** Wrapper around a technology template */
m.Technology = function(templateName)
{
this._templateName = templateName;
let template = TechnologyTemplates.Get(templateName);
// check if this is one of two paired technologies.
this._isPair = template.pair !== undefined;
if (this._isPair)
{
let pairTech = TechnologyTemplates.Get(template.pair);
this._pairedWith = pairTech.top == templateName ? pairTech.bottom : pairTech.top;
}
// check if it only defines a pair:
this._definesPair = template.top !== undefined;
this._template = template;
};
/** returns generic, or specific if civ provided. */
m.Technology.prototype.name = function(civ)
{
if (civ === undefined)
return this._template.genericName;
if (this._template.specificName === undefined || this._template.specificName[civ] === undefined)
return undefined;
return this._template.specificName[civ];
};
m.Technology.prototype.pairDef = function()
{
return this._definesPair;
};
/** in case this defines a pair only, returns the two paired technologies. */
m.Technology.prototype.getPairedTechs = function()
{
if (!this._definesPair)
return undefined;
return [
new m.Technology(this._template.top),
new m.Technology(this._template.bottom)
];
};
m.Technology.prototype.pair = function()
{
if (!this._isPair)
return undefined;
return this._template.pair;
};
m.Technology.prototype.pairedWith = function()
{
if (!this._isPair)
return undefined;
return this._pairedWith;
};
-m.Technology.prototype.cost = function(productionQueue)
+m.Technology.prototype.cost = function(researcher)
{
if (!this._template.cost)
return undefined;
let cost = {};
for (let type in this._template.cost)
{
cost[type] = +this._template.cost[type];
- if (productionQueue)
- cost[type] *= productionQueue.techCostMultiplier(type);
+ if (researcher)
+ cost[type] *= researcher.techCostMultiplier(type);
}
return cost;
};
-m.Technology.prototype.costSum = function(productionQueue)
+m.Technology.prototype.costSum = function(researcher)
{
- let cost = this.cost(productionQueue);
+ const cost = this.cost(researcher);
if (!cost)
return 0;
let ret = 0;
for (let type in cost)
ret += cost[type];
return ret;
};
m.Technology.prototype.researchTime = function()
{
return this._template.researchTime || 0;
};
m.Technology.prototype.requirements = function(civ)
{
return DeriveTechnologyRequirements(this._template, civ);
};
m.Technology.prototype.autoResearch = function()
{
if (!this._template.autoResearch)
return undefined;
return this._template.autoResearch;
};
m.Technology.prototype.supersedes = function()
{
if (!this._template.supersedes)
return undefined;
return this._template.supersedes;
};
m.Technology.prototype.modifications = function()
{
if (!this._template.modifications)
return undefined;
return this._template.modifications;
};
m.Technology.prototype.affects = function()
{
if (!this._template.affects)
return undefined;
return this._template.affects;
};
m.Technology.prototype.isAffected = function(classes)
{
return this._template.affects && this._template.affects.some(affect => MatchesClassList(classes, affect));
};
return m;
}(API3);
Index: ps/trunk/binaries/data/mods/public/simulation/ai/petra/queueplanTraining.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/ai/petra/queueplanTraining.js (revision 25999)
+++ ps/trunk/binaries/data/mods/public/simulation/ai/petra/queueplanTraining.js (revision 26000)
@@ -1,187 +1,157 @@
PETRA.TrainingPlan = function(gameState, type, metadata, number = 1, maxMerge = 5)
{
if (!PETRA.QueuePlan.call(this, gameState, type, metadata))
{
API3.warn(" Plan training " + type + " canceled");
return false;
}
// Refine the estimated cost and add pop cost
let trainers = this.getBestTrainers(gameState);
let trainer = trainers ? trainers[0] : undefined;
this.cost = new API3.Resources(this.template.cost(trainer), +this.template._template.Cost.Population);
this.category = "unit";
this.number = number;
this.maxMerge = maxMerge;
return true;
};
PETRA.TrainingPlan.prototype = Object.create(PETRA.QueuePlan.prototype);
PETRA.TrainingPlan.prototype.canStart = function(gameState)
{
this.trainers = this.getBestTrainers(gameState);
if (!this.trainers)
return false;
this.cost = new API3.Resources(this.template.cost(this.trainers[0]), +this.template._template.Cost.Population);
return true;
};
PETRA.TrainingPlan.prototype.getBestTrainers = function(gameState)
{
if (this.metadata && this.metadata.trainer)
{
let trainer = gameState.getEntityById(this.metadata.trainer);
if (trainer)
return [trainer];
}
let allTrainers = gameState.findTrainers(this.type);
if (this.metadata && this.metadata.sea)
allTrainers = allTrainers.filter(API3.Filters.byMetadata(PlayerID, "sea", this.metadata.sea));
if (this.metadata && this.metadata.base)
allTrainers = allTrainers.filter(API3.Filters.byMetadata(PlayerID, "base", this.metadata.base));
if (!allTrainers || !allTrainers.hasEntities())
return undefined;
// Keep only trainers with smallest cost
let costMin = Math.min();
let trainers;
for (let ent of allTrainers.values())
{
let cost = this.template.costSum(ent);
if (cost === costMin)
trainers.push(ent);
else if (cost < costMin)
{
costMin = cost;
trainers = [ent];
}
}
return trainers;
};
PETRA.TrainingPlan.prototype.start = function(gameState)
{
if (this.metadata && this.metadata.trainer)
{
let metadata = {};
for (let key in this.metadata)
if (key !== "trainer")
metadata[key] = this.metadata[key];
this.metadata = metadata;
}
if (this.trainers.length > 1)
{
let wantedIndex;
if (this.metadata && this.metadata.index)
wantedIndex = this.metadata.index;
let workerUnit = this.metadata && this.metadata.role && this.metadata.role == "worker";
let supportUnit = this.template.hasClass("Support");
this.trainers.sort(function(a, b) {
// Prefer training buildings with short queues
let aa = a.trainingQueueTime();
let bb = b.trainingQueueTime();
// Give priority to support units in the cc
if (a.hasClass("Civic") && !supportUnit)
aa += 10;
if (b.hasClass("Civic") && !supportUnit)
bb += 10;
// And support units should not be too near to dangerous place
if (supportUnit)
{
if (gameState.ai.HQ.isNearInvadingArmy(a.position()))
aa += 50;
if (gameState.ai.HQ.isNearInvadingArmy(b.position()))
bb += 50;
}
// Give also priority to buildings with the right accessibility
let aBase = a.getMetadata(PlayerID, "base");
let bBase = b.getMetadata(PlayerID, "base");
if (wantedIndex)
{
if (!aBase || gameState.ai.HQ.getBaseByID(aBase).accessIndex != wantedIndex)
aa += 30;
if (!bBase || gameState.ai.HQ.getBaseByID(bBase).accessIndex != wantedIndex)
bb += 30;
}
// Then, if workers, small preference for bases with less workers
if (workerUnit && aBase && bBase && aBase != bBase)
{
let apop = gameState.ai.HQ.getBaseByID(aBase).workers.length;
let bpop = gameState.ai.HQ.getBaseByID(bBase).workers.length;
if (apop > bpop)
aa++;
else if (bpop > apop)
bb++;
}
return aa - bb;
});
}
if (this.metadata && this.metadata.base !== undefined && this.metadata.base === 0)
this.metadata.base = this.trainers[0].getMetadata(PlayerID, "base");
- this.trainers[0].train(gameState.getPlayerCiv(), this.type, this.number, this.metadata, this.promotedTypes(gameState));
+ this.trainers[0].train(gameState.getPlayerCiv(), this.type, this.number, this.metadata);
this.onStart(gameState);
};
PETRA.TrainingPlan.prototype.addItem = function(amount = 1)
{
this.number += amount;
};
-/** Find the promoted types corresponding to this.type */
-PETRA.TrainingPlan.prototype.promotedTypes = function(gameState)
-{
- let types = [];
- let promotion = this.template.promotion();
- let previous;
- let template;
- while (promotion)
- {
- types.push(promotion);
- previous = promotion;
- template = gameState.getTemplate(promotion);
- if (!template)
- {
- if (gameState.ai.Config.debug > 0)
- API3.warn(" promotion template " + promotion + " is not found");
- promotion = undefined;
- break;
- }
- promotion = template.promotion();
- if (previous === promotion)
- {
- if (gameState.ai.Config.debug > 0)
- API3.warn(" unit " + promotion + " is its own promoted unit");
- promotion = undefined;
- }
- }
- return types;
-};
-
PETRA.TrainingPlan.prototype.Serialize = function()
{
return {
"category": this.category,
"type": this.type,
"ID": this.ID,
"metadata": this.metadata,
"cost": this.cost.Serialize(),
"number": this.number,
"maxMerge": this.maxMerge
};
};
PETRA.TrainingPlan.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 25999)
+++ ps/trunk/binaries/data/mods/public/simulation/components/GuiInterface.js (revision 26000)
@@ -1,2157 +1,2163 @@
function GuiInterface() {}
GuiInterface.prototype.Schema =
"";
GuiInterface.prototype.Serialize = function()
{
// This component isn't network-synchronized for the biggest part,
// so most of the attributes shouldn't be serialized.
// Return an object with a small selection of deterministic data.
return {
"timeNotifications": this.timeNotifications,
"timeNotificationID": this.timeNotificationID
};
};
GuiInterface.prototype.Deserialize = function(data)
{
this.Init();
this.timeNotifications = data.timeNotifications;
this.timeNotificationID = data.timeNotificationID;
};
GuiInterface.prototype.Init = function()
{
this.placementEntity = undefined; // = undefined or [templateName, entityID]
this.placementWallEntities = undefined;
this.placementWallLastAngle = 0;
this.notifications = [];
this.renamedEntities = [];
this.miragedEntities = [];
this.timeNotificationID = 1;
this.timeNotifications = [];
this.entsRallyPointsDisplayed = [];
this.entsWithAuraAndStatusBars = new Set();
this.enabledVisualRangeOverlayTypes = {};
this.templateModified = {};
this.selectionDirty = {};
this.obstructionSnap = new ObstructionSnap();
};
/*
* All of the functions defined below are called via Engine.GuiInterfaceCall(name, arg)
* from GUI scripts, and executed here with arguments (player, arg).
*
* CAUTION: The input to the functions in this module is not network-synchronised, so it
* mustn't affect the simulation state (i.e. the data that is serialised and can affect
* the behaviour of the rest of the simulation) else it'll cause out-of-sync errors.
*/
/**
* Returns global information about the current game state.
* This is used by the GUI and also by AI scripts.
*/
GuiInterface.prototype.GetSimulationState = function()
{
let ret = {
"players": []
};
let cmpPlayerManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager);
let numPlayers = cmpPlayerManager.GetNumPlayers();
for (let i = 0; i < numPlayers; ++i)
{
let cmpPlayer = QueryPlayerIDInterface(i);
let cmpPlayerEntityLimits = QueryPlayerIDInterface(i, IID_EntityLimits);
// Work out which phase we are in.
let phase = "";
let cmpTechnologyManager = QueryPlayerIDInterface(i, IID_TechnologyManager);
if (cmpTechnologyManager)
{
if (cmpTechnologyManager.IsTechnologyResearched("phase_city"))
phase = "city";
else if (cmpTechnologyManager.IsTechnologyResearched("phase_town"))
phase = "town";
else if (cmpTechnologyManager.IsTechnologyResearched("phase_village"))
phase = "village";
}
let allies = [];
let mutualAllies = [];
let neutrals = [];
let enemies = [];
for (let j = 0; j < numPlayers; ++j)
{
allies[j] = cmpPlayer.IsAlly(j);
mutualAllies[j] = cmpPlayer.IsMutualAlly(j);
neutrals[j] = cmpPlayer.IsNeutral(j);
enemies[j] = cmpPlayer.IsEnemy(j);
}
ret.players.push({
"name": cmpPlayer.GetName(),
"civ": cmpPlayer.GetCiv(),
"color": cmpPlayer.GetColor(),
"controlsAll": cmpPlayer.CanControlAllUnits(),
"popCount": cmpPlayer.GetPopulationCount(),
"popLimit": cmpPlayer.GetPopulationLimit(),
"popMax": cmpPlayer.GetMaxPopulation(),
"panelEntities": cmpPlayer.GetPanelEntities(),
"resourceCounts": cmpPlayer.GetResourceCounts(),
"resourceGatherers": cmpPlayer.GetResourceGatherers(),
"trainingBlocked": cmpPlayer.IsTrainingBlocked(),
"state": cmpPlayer.GetState(),
"team": cmpPlayer.GetTeam(),
"teamsLocked": cmpPlayer.GetLockTeams(),
"cheatsEnabled": cmpPlayer.GetCheatsEnabled(),
"disabledTemplates": cmpPlayer.GetDisabledTemplates(),
"disabledTechnologies": cmpPlayer.GetDisabledTechnologies(),
"hasSharedDropsites": cmpPlayer.HasSharedDropsites(),
"hasSharedLos": cmpPlayer.HasSharedLos(),
"spyCostMultiplier": cmpPlayer.GetSpyCostMultiplier(),
"phase": phase,
"isAlly": allies,
"isMutualAlly": mutualAllies,
"isNeutral": neutrals,
"isEnemy": enemies,
"entityLimits": cmpPlayerEntityLimits ? cmpPlayerEntityLimits.GetLimits() : null,
"entityCounts": cmpPlayerEntityLimits ? cmpPlayerEntityLimits.GetCounts() : null,
"matchEntityCounts": cmpPlayerEntityLimits ? cmpPlayerEntityLimits.GetMatchCounts() : null,
"entityLimitChangers": cmpPlayerEntityLimits ? cmpPlayerEntityLimits.GetLimitChangers() : null,
"researchQueued": cmpTechnologyManager ? cmpTechnologyManager.GetQueuedResearch() : null,
"researchStarted": cmpTechnologyManager ? cmpTechnologyManager.GetStartedTechs() : null,
"researchedTechs": cmpTechnologyManager ? cmpTechnologyManager.GetResearchedTechs() : null,
"classCounts": cmpTechnologyManager ? cmpTechnologyManager.GetClassCounts() : null,
"typeCountsByClass": cmpTechnologyManager ? cmpTechnologyManager.GetTypeCountsByClass() : null,
"canBarter": cmpPlayer.CanBarter(),
"barterPrices": Engine.QueryInterface(SYSTEM_ENTITY, IID_Barter).GetPrices(cmpPlayer)
});
}
let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
if (cmpRangeManager)
ret.circularMap = cmpRangeManager.GetLosCircular();
let cmpTerrain = Engine.QueryInterface(SYSTEM_ENTITY, IID_Terrain);
if (cmpTerrain)
ret.mapSize = cmpTerrain.GetMapSize();
let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
ret.timeElapsed = cmpTimer.GetTime();
let cmpCeasefireManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_CeasefireManager);
if (cmpCeasefireManager)
{
ret.ceasefireActive = cmpCeasefireManager.IsCeasefireActive();
ret.ceasefireTimeRemaining = ret.ceasefireActive ? cmpCeasefireManager.GetCeasefireStartedTime() + cmpCeasefireManager.GetCeasefireTime() - ret.timeElapsed : 0;
}
let cmpCinemaManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_CinemaManager);
if (cmpCinemaManager)
ret.cinemaPlaying = cmpCinemaManager.IsPlaying();
let cmpEndGameManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_EndGameManager);
ret.victoryConditions = cmpEndGameManager.GetVictoryConditions();
ret.alliedVictory = cmpEndGameManager.GetAlliedVictory();
ret.maxWorldPopulation = cmpPlayerManager.GetMaxWorldPopulation();
for (let i = 0; i < numPlayers; ++i)
{
let cmpPlayerStatisticsTracker = QueryPlayerIDInterface(i, IID_StatisticsTracker);
if (cmpPlayerStatisticsTracker)
ret.players[i].statistics = cmpPlayerStatisticsTracker.GetBasicStatistics();
}
return ret;
};
/**
* Returns global information about the current game state, plus statistics.
* This is used by the GUI at the end of a game, in the summary screen.
* Note: Amongst statistics, the team exploration map percentage is computed from
* scratch, so the extended simulation state should not be requested too often.
*/
GuiInterface.prototype.GetExtendedSimulationState = function()
{
let ret = this.GetSimulationState();
let numPlayers = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager).GetNumPlayers();
for (let i = 0; i < numPlayers; ++i)
{
let cmpPlayerStatisticsTracker = QueryPlayerIDInterface(i, IID_StatisticsTracker);
if (cmpPlayerStatisticsTracker)
ret.players[i].sequences = cmpPlayerStatisticsTracker.GetSequences();
}
return ret;
};
/**
* Returns the gamesettings that were chosen at the time the match started.
*/
GuiInterface.prototype.GetInitAttributes = function()
{
return InitAttributes;
};
/**
* This data will be stored in the replay metadata file after a match has been finished recording.
*/
GuiInterface.prototype.GetReplayMetadata = function()
{
let extendedSimState = this.GetExtendedSimulationState();
return {
"timeElapsed": extendedSimState.timeElapsed,
"playerStates": extendedSimState.players,
"mapSettings": InitAttributes.settings
};
};
/**
* Called when the game ends if the current game is part of a campaign run.
*/
GuiInterface.prototype.GetCampaignGameEndData = function(player)
{
let cmpTrigger = Engine.QueryInterface(SYSTEM_ENTITY, IID_Trigger);
if (Trigger.prototype.OnCampaignGameEnd)
return Trigger.prototype.OnCampaignGameEnd();
return {};
};
GuiInterface.prototype.GetRenamedEntities = function(player)
{
if (this.miragedEntities[player])
return this.renamedEntities.concat(this.miragedEntities[player]);
return this.renamedEntities;
};
GuiInterface.prototype.ClearRenamedEntities = function()
{
this.renamedEntities = [];
this.miragedEntities = [];
};
GuiInterface.prototype.AddMiragedEntity = function(player, entity, mirage)
{
if (!this.miragedEntities[player])
this.miragedEntities[player] = [];
this.miragedEntities[player].push({ "entity": entity, "newentity": mirage });
};
/**
* Get common entity info, often used in the gui.
*/
GuiInterface.prototype.GetEntityState = function(player, ent)
{
let cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager);
if (!ent)
return null;
// All units must have a template; if not then it's a nonexistent entity id.
let template = cmpTemplateManager.GetCurrentTemplateName(ent);
if (!template)
return null;
let ret = {
"id": ent,
"player": INVALID_PLAYER,
"template": template
};
let cmpMirage = Engine.QueryInterface(ent, IID_Mirage);
if (cmpMirage)
ret.mirage = true;
let cmpIdentity = Engine.QueryInterface(ent, IID_Identity);
if (cmpIdentity)
ret.identity = {
"rank": cmpIdentity.GetRank(),
"classes": cmpIdentity.GetClassesList(),
"selectionGroupName": cmpIdentity.GetSelectionGroupName(),
"canDelete": !cmpIdentity.IsUndeletable(),
"hasSomeFormation": cmpIdentity.HasSomeFormation(),
"formations": cmpIdentity.GetFormationsList(),
"controllable": cmpIdentity.IsControllable()
};
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 = {
- "entities": cmpProductionQueue.GetEntitiesList(),
- "technologies": cmpProductionQueue.GetTechnologiesList(),
- "techCostMultiplier": cmpProductionQueue.GetTechCostMultiplier(),
"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(),
"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;
let timers = cmpAttack.GetTimers(type);
ret.attack[type].prepareTime = timers.prepare;
ret.attack[type].repeatTime = timers.repeat;
if (type != "Ranged")
{
// Not a ranged attack, set some defaults.
ret.attack[type].elevationBonus = 0;
ret.attack[type].elevationAdaptedRange = ret.attack.maxRange;
continue;
}
ret.attack[type].elevationBonus = range.elevationBonus;
if (cmpPosition && cmpPosition.IsInWorld())
// For units, take the range in front of it, no spread, so angle = 0,
// else, take the average elevation around it: angle = 2 * pi.
ret.attack[type].elevationAdaptedRange = cmpRangeManager.GetElevationAdaptedRange(cmpPosition.GetPosition(), cmpPosition.GetRotation(), range.max, range.elevationBonus, cmpUnitAI ? 0 : 2 * Math.PI);
else
// Not in world, set a default?
ret.attack[type].elevationAdaptedRange = ret.attack.maxRange;
}
}
let cmpResistance = 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
};
let elevationBonus = cmd.elevationBonus || 0;
let range = cmd.range;
return cmpRangeManager.GetElevationAdaptedRange(pos, rot, range, elevationBonus, 2 * Math.PI);
};
GuiInterface.prototype.GetTemplateData = function(player, data)
{
let templateName = data.templateName;
let owner = data.player !== undefined ? data.player : player;
let cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager);
let template = cmpTemplateManager.GetTemplate(templateName);
if (!template)
return null;
let aurasTemplate = {};
if (!template.Auras)
return GetTemplateDataHelper(template, owner, aurasTemplate);
let auraNames = template.Auras._string.split(/\s+/);
for (let name of auraNames)
{
let auraTemplate = AuraTemplates.Get(name);
if (!auraTemplate)
error("Template " + templateName + " has undefined aura " + name);
else
aurasTemplate[name] = auraTemplate;
}
return GetTemplateDataHelper(template, owner, aurasTemplate);
};
GuiInterface.prototype.IsTechnologyResearched = function(player, data)
{
if (!data.tech)
return true;
let cmpTechnologyManager = QueryPlayerIDInterface(data.player !== undefined ? data.player : player, IID_TechnologyManager);
if (!cmpTechnologyManager)
return false;
return cmpTechnologyManager.IsTechnologyResearched(data.tech);
};
/**
* Checks whether the requirements for this technology have been met.
*/
GuiInterface.prototype.CheckTechnologyRequirements = function(player, data)
{
let cmpTechnologyManager = QueryPlayerIDInterface(data.player !== undefined ? data.player : player, IID_TechnologyManager);
if (!cmpTechnologyManager)
return false;
return cmpTechnologyManager.CanResearch(data.tech);
};
/**
* Returns technologies that are being actively researched, along with
* which entity is researching them and how far along the research is.
*/
GuiInterface.prototype.GetStartedResearch = function(player)
{
let cmpTechnologyManager = QueryPlayerIDInterface(player, IID_TechnologyManager);
if (!cmpTechnologyManager)
return {};
let ret = {};
for (let tech of cmpTechnologyManager.GetStartedTechs())
{
ret[tech] = { "researcher": cmpTechnologyManager.GetResearcher(tech) };
- let cmpProductionQueue = Engine.QueryInterface(ret[tech].researcher, IID_ProductionQueue);
- if (cmpProductionQueue)
+ const cmpResearcher = Engine.QueryInterface(ret[tech].researcher, IID_Researcher);
+ if (cmpResearcher)
{
- const research = cmpProductionQueue.GetQueue().find(item => item.technologyTemplate === tech);
+ const research = cmpResearcher.GetResearchingTechnologyByName(tech);
ret[tech].progress = research.progress;
ret[tech].timeRemaining = research.timeRemaining;
ret[tech].paused = research.paused;
}
else
{
ret[tech].progress = 0;
ret[tech].timeRemaining = 0;
ret[tech].paused = true;
}
}
return ret;
};
/**
* Returns the battle state of the player.
*/
GuiInterface.prototype.GetBattleState = function(player)
{
let cmpBattleDetection = QueryPlayerIDInterface(player, IID_BattleDetection);
if (!cmpBattleDetection)
return false;
return cmpBattleDetection.GetState();
};
/**
* Returns a list of ongoing attacks against the player.
*/
GuiInterface.prototype.GetIncomingAttacks = function(player)
{
let cmpAttackDetection = QueryPlayerIDInterface(player, IID_AttackDetection);
if (!cmpAttackDetection)
return [];
return cmpAttackDetection.GetIncomingAttacks();
};
/**
* Used to show a red square over GUI elements you can't yet afford.
*/
GuiInterface.prototype.GetNeededResources = function(player, data)
{
let cmpPlayer = QueryPlayerIDInterface(data.player !== undefined ? data.player : player);
return cmpPlayer ? cmpPlayer.GetNeededResources(data.cost) : {};
};
/**
* State of the templateData (player dependent): true when some template values have been modified
* and need to be reloaded by the gui.
*/
GuiInterface.prototype.OnTemplateModification = function(msg)
{
this.templateModified[msg.player] = true;
this.selectionDirty[msg.player] = true;
};
GuiInterface.prototype.IsTemplateModified = function(player)
{
return this.templateModified[player] || false;
};
GuiInterface.prototype.ResetTemplateModified = function()
{
this.templateModified = {};
};
/**
* Some changes may require an update to the selection panel,
* which is cached for efficiency. Inform the GUI it needs reloading.
*/
GuiInterface.prototype.OnDisabledTemplatesChanged = function(msg)
{
this.selectionDirty[msg.player] = true;
};
GuiInterface.prototype.OnDisabledTechnologiesChanged = function(msg)
{
this.selectionDirty[msg.player] = true;
};
GuiInterface.prototype.SetSelectionDirty = function(player)
{
this.selectionDirty[player] = true;
};
GuiInterface.prototype.IsSelectionDirty = function(player)
{
return this.selectionDirty[player] || false;
};
GuiInterface.prototype.ResetSelectionDirty = function()
{
this.selectionDirty = {};
};
/**
* Add a timed notification.
* Warning: timed notifacations are serialised
* (to also display them on saved games or after a rejoin)
* so they should allways be added and deleted in a deterministic way.
*/
GuiInterface.prototype.AddTimeNotification = function(notification, duration = 10000)
{
let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
notification.endTime = duration + cmpTimer.GetTime();
notification.id = ++this.timeNotificationID;
// Let all players and observers receive the notification by default.
if (!notification.players)
{
notification.players = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager).GetAllPlayers();
notification.players[0] = -1;
}
this.timeNotifications.push(notification);
this.timeNotifications.sort((n1, n2) => n2.endTime - n1.endTime);
cmpTimer.SetTimeout(this.entity, IID_GuiInterface, "DeleteTimeNotification", duration, this.timeNotificationID);
return this.timeNotificationID;
};
GuiInterface.prototype.DeleteTimeNotification = function(notificationID)
{
this.timeNotifications = this.timeNotifications.filter(n => n.id != notificationID);
};
GuiInterface.prototype.GetTimeNotifications = function(player)
{
let time = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer).GetTime();
// Filter on players and time, since the delete timer might be executed with a delay.
return this.timeNotifications.filter(n => n.players.indexOf(player) != -1 && n.endTime > time);
};
GuiInterface.prototype.PushNotification = function(notification)
{
if (!notification.type || notification.type == "text")
this.AddTimeNotification(notification);
else
this.notifications.push(notification);
};
GuiInterface.prototype.GetNotifications = function()
{
let n = this.notifications;
this.notifications = [];
return n;
};
GuiInterface.prototype.GetAvailableFormations = function(player, wantedPlayer)
{
let cmpPlayer = QueryPlayerIDInterface(wantedPlayer);
if (!cmpPlayer)
return [];
return cmpPlayer.GetFormations();
};
GuiInterface.prototype.GetFormationRequirements = function(player, data)
{
return GetFormationRequirements(data.formationTemplate);
};
GuiInterface.prototype.CanMoveEntsIntoFormation = function(player, data)
{
return CanMoveEntsIntoFormation(data.ents, data.formationTemplate);
};
GuiInterface.prototype.GetFormationInfoFromTemplate = function(player, data)
{
let cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager);
let template = cmpTemplateManager.GetTemplate(data.templateName);
if (!template || !template.Formation)
return {};
return {
"name": template.Formation.FormationName,
"tooltip": template.Formation.DisabledTooltip || "",
"icon": template.Formation.Icon
};
};
GuiInterface.prototype.IsFormationSelected = function(player, data)
{
return data.ents.some(ent => {
let cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
return cmpUnitAI && cmpUnitAI.GetFormationTemplate() == data.formationTemplate;
});
};
GuiInterface.prototype.IsStanceSelected = function(player, data)
{
for (let ent of data.ents)
{
let cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
if (cmpUnitAI && cmpUnitAI.GetStanceName() == data.stance)
return true;
}
return false;
};
GuiInterface.prototype.GetAllBuildableEntities = function(player, cmd)
{
let buildableEnts = [];
for (let ent of cmd.entities)
{
let cmpBuilder = Engine.QueryInterface(ent, IID_Builder);
if (!cmpBuilder)
continue;
for (let building of cmpBuilder.GetEntitiesList())
if (buildableEnts.indexOf(building) == -1)
buildableEnts.push(building);
}
return buildableEnts;
};
GuiInterface.prototype.UpdateDisplayedPlayerColors = function(player, data)
{
let updateEntityColor = (iids, entities) => {
for (let ent of entities)
for (let iid of iids)
{
let cmp = Engine.QueryInterface(ent, iid);
if (cmp)
cmp.UpdateColor();
}
};
let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
let numPlayers = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager).GetNumPlayers();
for (let i = 1; i < numPlayers; ++i)
{
let cmpPlayer = QueryPlayerIDInterface(i, IID_Player);
if (!cmpPlayer)
continue;
cmpPlayer.SetDisplayDiplomacyColor(data.displayDiplomacyColors);
if (data.displayDiplomacyColors)
cmpPlayer.SetDiplomacyColor(data.displayedPlayerColors[i]);
updateEntityColor(data.showAllStatusBars && (i == player || player == -1) ?
[IID_Minimap, IID_RangeOverlayRenderer, IID_RallyPointRenderer, IID_StatusBars] :
[IID_Minimap, IID_RangeOverlayRenderer, IID_RallyPointRenderer],
cmpRangeManager.GetEntitiesByPlayer(i));
}
updateEntityColor([IID_Selectable, IID_StatusBars], data.selected);
Engine.QueryInterface(SYSTEM_ENTITY, IID_TerritoryManager).UpdateColors();
};
GuiInterface.prototype.SetSelectionHighlight = function(player, cmd)
{
// Cache of owner -> color map
let playerColors = {};
for (let ent of cmd.entities)
{
let cmpSelectable = Engine.QueryInterface(ent, IID_Selectable);
if (!cmpSelectable)
continue;
// Find the entity's owner's color.
let owner = INVALID_PLAYER;
let cmpOwnership = Engine.QueryInterface(ent, IID_Ownership);
if (cmpOwnership)
owner = cmpOwnership.GetOwner();
let color = playerColors[owner];
if (!color)
{
color = { "r": 1, "g": 1, "b": 1 };
let cmpPlayer = QueryPlayerIDInterface(owner);
if (cmpPlayer)
color = cmpPlayer.GetDisplayedColor();
playerColors[owner] = color;
}
cmpSelectable.SetSelectionHighlight({ "r": color.r, "g": color.g, "b": color.b, "a": cmd.alpha }, cmd.selected);
let cmpRangeOverlayManager = Engine.QueryInterface(ent, IID_RangeOverlayManager);
if (!cmpRangeOverlayManager || player != owner && player != INVALID_PLAYER)
continue;
cmpRangeOverlayManager.SetEnabled(cmd.selected, this.enabledVisualRangeOverlayTypes, false);
}
};
GuiInterface.prototype.EnableVisualRangeOverlayType = function(player, data)
{
this.enabledVisualRangeOverlayTypes[data.type] = data.enabled;
};
GuiInterface.prototype.GetEntitiesWithStatusBars = function()
{
return Array.from(this.entsWithAuraAndStatusBars);
};
GuiInterface.prototype.SetStatusBars = function(player, cmd)
{
let affectedEnts = new Set();
for (let ent of cmd.entities)
{
let cmpStatusBars = Engine.QueryInterface(ent, IID_StatusBars);
if (!cmpStatusBars)
continue;
cmpStatusBars.SetEnabled(cmd.enabled, cmd.showRank, cmd.showExperience);
let cmpAuras = Engine.QueryInterface(ent, IID_Auras);
if (!cmpAuras)
continue;
for (let name of cmpAuras.GetAuraNames())
{
if (!cmpAuras.GetOverlayIcon(name))
continue;
for (let e of cmpAuras.GetAffectedEntities(name))
affectedEnts.add(e);
if (cmd.enabled)
this.entsWithAuraAndStatusBars.add(ent);
else
this.entsWithAuraAndStatusBars.delete(ent);
}
}
for (let ent of affectedEnts)
{
let cmpStatusBars = Engine.QueryInterface(ent, IID_StatusBars);
if (cmpStatusBars)
cmpStatusBars.RegenerateSprites();
}
};
GuiInterface.prototype.SetRangeOverlays = function(player, cmd)
{
for (let ent of cmd.entities)
{
let cmpRangeOverlayManager = Engine.QueryInterface(ent, IID_RangeOverlayManager);
if (cmpRangeOverlayManager)
cmpRangeOverlayManager.SetEnabled(cmd.enabled, this.enabledVisualRangeOverlayTypes, true);
}
};
GuiInterface.prototype.GetPlayerEntities = function(player)
{
return Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager).GetEntitiesByPlayer(player);
};
GuiInterface.prototype.GetNonGaiaEntities = function()
{
return Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager).GetNonGaiaEntities();
};
/**
* Displays the rally points of a given list of entities (carried in cmd.entities).
*
* The 'cmd' object may carry its own x/z coordinate pair indicating the location where the rally point should
* be rendered, in order to support instantaneously rendering a rally point marker at a specified location
* instead of incurring a delay while PostNetworkCommand processes the set-rallypoint command (see input.js).
* If cmd doesn't carry a custom location, then the position to render the marker at will be read from the
* RallyPoint component.
*/
GuiInterface.prototype.DisplayRallyPoint = function(player, cmd)
{
let cmpPlayer = QueryPlayerIDInterface(player);
// If there are some rally points already displayed, first hide them.
for (let ent of this.entsRallyPointsDisplayed)
{
let cmpRallyPointRenderer = Engine.QueryInterface(ent, IID_RallyPointRenderer);
if (cmpRallyPointRenderer)
cmpRallyPointRenderer.SetDisplayed(false);
}
this.entsRallyPointsDisplayed = [];
// Show the rally points for the passed entities.
for (let ent of cmd.entities)
{
let cmpRallyPointRenderer = Engine.QueryInterface(ent, IID_RallyPointRenderer);
if (!cmpRallyPointRenderer)
continue;
// Entity must have a rally point component to display a rally point marker
// (regardless of whether cmd specifies a custom location).
let cmpRallyPoint = Engine.QueryInterface(ent, IID_RallyPoint);
if (!cmpRallyPoint)
continue;
// Verify the owner.
let cmpOwnership = Engine.QueryInterface(ent, IID_Ownership);
if (!(cmpPlayer && cmpPlayer.CanControlAllUnits()))
if (!cmpOwnership || cmpOwnership.GetOwner() != player)
continue;
// If the command was passed an explicit position, use that and
// override the real rally point position; otherwise use the real position.
let pos;
if (cmd.x && cmd.z)
pos = cmd;
else
// May return undefined if no rally point is set.
pos = cmpRallyPoint.GetPositions()[0];
if (pos)
{
// Only update the position if we changed it (cmd.queued is set).
// Note that Add-/SetPosition take a CFixedVector2D which has X/Y components, not X/Z.
if ("queued" in cmd)
{
if (cmd.queued == true)
cmpRallyPointRenderer.AddPosition(new Vector2D(pos.x, pos.z));
else
cmpRallyPointRenderer.SetPosition(new Vector2D(pos.x, pos.z));
}
else if (!cmpRallyPointRenderer.IsSet())
// Rebuild the renderer when not set (when reading saved game or in case of building update).
for (let posi of cmpRallyPoint.GetPositions())
cmpRallyPointRenderer.AddPosition(new Vector2D(posi.x, posi.z));
cmpRallyPointRenderer.SetDisplayed(true);
// Remember which entities have their rally points displayed so we can hide them again.
this.entsRallyPointsDisplayed.push(ent);
}
}
};
GuiInterface.prototype.AddTargetMarker = function(player, cmd)
{
let ent = Engine.AddLocalEntity(cmd.template);
if (!ent)
return;
let 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)
{
- let cmpProductionQueue = Engine.QueryInterface(data.entity, IID_ProductionQueue);
- if (!cmpProductionQueue)
- return 0;
-
- return cmpProductionQueue.GetBatchTime(data.batchSize);
+ 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,
"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/Player.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/components/Player.js (revision 25999)
+++ ps/trunk/binaries/data/mods/public/simulation/components/Player.js (revision 26000)
@@ -1,959 +1,969 @@
function Player() {}
Player.prototype.Schema =
"" +
"" +
"" +
Resources.BuildSchema("positiveDecimal") +
"" +
"" +
Resources.BuildSchema("positiveDecimal") +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"";
/**
* Don't serialize diplomacyColor or displayDiplomacyColor since they're modified by the GUI.
*/
Player.prototype.Serialize = function()
{
let state = {};
for (let key in this)
if (this.hasOwnProperty(key))
state[key] = this[key];
state.diplomacyColor = undefined;
state.displayDiplomacyColor = false;
return state;
};
Player.prototype.Deserialize = function(state)
{
for (let prop in state)
this[prop] = state[prop];
};
/**
* Which units will be shown with special icons at the top.
*/
var panelEntityClasses = "Hero Relic";
Player.prototype.Init = function()
{
this.playerID = undefined;
this.name = undefined; // Define defaults elsewhere (supporting other languages).
this.civ = undefined;
this.color = undefined;
this.diplomacyColor = undefined;
this.displayDiplomacyColor = false;
this.popUsed = 0; // Population of units owned or trained by this player.
this.popBonuses = 0; // Sum of population bonuses of player's entities.
this.maxPop = 300; // Maximum population.
this.trainingBlocked = false; // Indicates whether any training queue is currently blocked.
this.resourceCount = {};
this.resourceGatherers = {};
this.tradingGoods = []; // Goods for next trade-route and its probabilities * 100.
this.team = -1; // Team number of the player, players on the same team will always have ally diplomatic status. Also this is useful for team emblems, scoring, etc.
this.teamsLocked = false;
this.state = "active"; // Game state. One of "active", "defeated", "won".
this.diplomacy = []; // Array of diplomatic stances for this player with respect to other players (including gaia and self).
this.sharedDropsites = false;
this.formations = [];
this.startCam = undefined;
this.controlAllUnits = false;
this.isAI = false;
this.cheatsEnabled = false;
this.panelEntities = [];
this.resourceNames = {};
this.disabledTemplates = {};
this.disabledTechnologies = {};
this.spyCostMultiplier = +this.template.SpyCostMultiplier;
this.barterEntities = [];
this.barterMultiplier = {
"buy": clone(this.template.BarterMultiplier.Buy),
"sell": clone(this.template.BarterMultiplier.Sell)
};
// Initial resources.
let resCodes = Resources.GetCodes();
for (let res of resCodes)
{
this.resourceCount[res] = 300;
this.resourceNames[res] = Resources.GetResource(res).name;
this.resourceGatherers[res] = 0;
}
// Trading goods probability in steps of 5.
let resTradeCodes = Resources.GetTradableCodes();
let quotient = Math.floor(20 / resTradeCodes.length);
let remainder = 20 % resTradeCodes.length;
for (let i in resTradeCodes)
this.tradingGoods.push({
"goods": resTradeCodes[i],
"proba": 5 * (quotient + (+i < remainder ? 1 : 0))
});
};
Player.prototype.SetPlayerID = function(id)
{
this.playerID = id;
};
Player.prototype.GetPlayerID = function()
{
return this.playerID;
};
Player.prototype.SetName = function(name)
{
this.name = name;
};
Player.prototype.GetName = function()
{
return this.name;
};
Player.prototype.SetCiv = function(civcode)
{
let oldCiv = this.civ;
this.civ = civcode;
// Normally, the civ is only set once. But in Atlas, map designers can change civs at any time.
if (oldCiv && this.playerID && oldCiv != civcode)
Engine.BroadcastMessage(MT_CivChanged, {
"player": this.playerID,
"from": oldCiv,
"to": civcode
});
};
Player.prototype.GetCiv = function()
{
return this.civ;
};
Player.prototype.SetColor = function(r, g, b)
{
let colorInitialized = !!this.color;
this.color = { "r": r / 255, "g": g / 255, "b": b / 255, "a": 1 };
// Used in Atlas.
if (colorInitialized)
Engine.BroadcastMessage(MT_PlayerColorChanged, {
"player": this.playerID
});
};
Player.prototype.SetDiplomacyColor = function(color)
{
this.diplomacyColor = { "r": color.r / 255, "g": color.g / 255, "b": color.b / 255, "a": 1 };
};
Player.prototype.SetDisplayDiplomacyColor = function(displayDiplomacyColor)
{
this.displayDiplomacyColor = displayDiplomacyColor;
};
Player.prototype.GetColor = function()
{
return this.color;
};
Player.prototype.GetDisplayedColor = function()
{
return this.displayDiplomacyColor ? this.diplomacyColor : this.color;
};
// Try reserving num population slots. Returns 0 on success or number of missing slots otherwise.
Player.prototype.TryReservePopulationSlots = function(num)
{
if (num != 0 && num > (this.GetPopulationLimit() - this.popUsed))
return num - (this.GetPopulationLimit() - this.popUsed);
this.popUsed += num;
return 0;
};
Player.prototype.UnReservePopulationSlots = function(num)
{
this.popUsed -= num;
};
Player.prototype.GetPopulationCount = function()
{
return this.popUsed;
};
Player.prototype.AddPopulation = function(num)
{
this.popUsed += num;
};
Player.prototype.SetPopulationBonuses = function(num)
{
this.popBonuses = num;
};
Player.prototype.AddPopulationBonuses = function(num)
{
this.popBonuses += num;
};
Player.prototype.GetPopulationLimit = function()
{
return Math.min(this.GetMaxPopulation(), this.popBonuses);
};
Player.prototype.SetMaxPopulation = function(max)
{
this.maxPop = max;
};
Player.prototype.GetMaxPopulation = function()
{
return Math.round(ApplyValueModificationsToEntity("Player/MaxPopulation", this.maxPop, this.entity));
};
Player.prototype.CanBarter = function()
{
return this.barterEntities.length > 0;
};
Player.prototype.GetBarterMultiplier = function()
{
return this.barterMultiplier;
};
Player.prototype.GetSpyCostMultiplier = function()
{
return this.spyCostMultiplier;
};
Player.prototype.GetPanelEntities = function()
{
return this.panelEntities;
};
Player.prototype.IsTrainingBlocked = function()
{
return this.trainingBlocked;
};
Player.prototype.BlockTraining = function()
{
this.trainingBlocked = true;
};
Player.prototype.UnBlockTraining = function()
{
this.trainingBlocked = false;
};
Player.prototype.SetResourceCounts = function(resources)
{
for (let res in resources)
this.resourceCount[res] = resources[res];
};
Player.prototype.GetResourceCounts = function()
{
return this.resourceCount;
};
Player.prototype.GetResourceGatherers = function()
{
return this.resourceGatherers;
};
/**
* @param {string} type - The generic type of resource to add the gatherer for.
*/
Player.prototype.AddResourceGatherer = function(type)
{
++this.resourceGatherers[type];
};
/**
* @param {string} type - The generic type of resource to remove the gatherer from.
*/
Player.prototype.RemoveResourceGatherer = function(type)
{
--this.resourceGatherers[type];
};
/**
* Add resource of specified type to player.
* @param {string} type - Generic type of resource.
* @param {number} amount - Amount of resource, which should be added.
*/
Player.prototype.AddResource = function(type, amount)
{
this.resourceCount[type] += +amount;
};
/**
* Add resources to player.
*/
Player.prototype.AddResources = function(amounts)
{
for (let type in amounts)
this.resourceCount[type] += +amounts[type];
};
Player.prototype.GetNeededResources = function(amounts)
{
// Check if we can afford it all.
let amountsNeeded = {};
for (let type in amounts)
if (this.resourceCount[type] != undefined && amounts[type] > this.resourceCount[type])
amountsNeeded[type] = amounts[type] - Math.floor(this.resourceCount[type]);
if (Object.keys(amountsNeeded).length == 0)
return undefined;
return amountsNeeded;
};
Player.prototype.SubtractResourcesOrNotify = function(amounts)
{
let amountsNeeded = this.GetNeededResources(amounts);
// If we don't have enough resources, send a notification to the player.
if (amountsNeeded)
{
let parameters = {};
let i = 0;
for (let type in amountsNeeded)
{
++i;
parameters["resourceType" + i] = this.resourceNames[type];
parameters["resourceAmount" + i] = amountsNeeded[type];
}
let msg = "";
// When marking strings for translations, you need to include the actual string,
// not some way to derive the string.
if (i < 1)
warn("Amounts needed but no amounts given?");
else if (i == 1)
msg = markForTranslation("Insufficient resources - %(resourceAmount1)s %(resourceType1)s");
else if (i == 2)
msg = markForTranslation("Insufficient resources - %(resourceAmount1)s %(resourceType1)s, %(resourceAmount2)s %(resourceType2)s");
else if (i == 3)
msg = markForTranslation("Insufficient resources - %(resourceAmount1)s %(resourceType1)s, %(resourceAmount2)s %(resourceType2)s, %(resourceAmount3)s %(resourceType3)s");
else if (i == 4)
msg = markForTranslation("Insufficient resources - %(resourceAmount1)s %(resourceType1)s, %(resourceAmount2)s %(resourceType2)s, %(resourceAmount3)s %(resourceType3)s, %(resourceAmount4)s %(resourceType4)s");
else
warn("Localisation: Strings are not localised for more than 4 resources");
// Send as time-notification.
let cmpGUIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface);
cmpGUIInterface.PushNotification({
"players": [this.playerID],
"message": msg,
"parameters": parameters,
"translateMessage": true,
"translateParameters": {
"resourceType1": "withinSentence",
"resourceType2": "withinSentence",
"resourceType3": "withinSentence",
"resourceType4": "withinSentence"
}
});
return false;
}
for (let type in amounts)
this.resourceCount[type] -= amounts[type];
return true;
};
Player.prototype.TrySubtractResources = function(amounts)
{
if (!this.SubtractResourcesOrNotify(amounts))
return false;
let cmpStatisticsTracker = QueryPlayerIDInterface(this.playerID, IID_StatisticsTracker);
if (cmpStatisticsTracker)
for (let type in amounts)
cmpStatisticsTracker.IncreaseResourceUsedCounter(type, amounts[type]);
return true;
};
+Player.prototype.RefundResources = function(amounts)
+{
+ const cmpStatisticsTracker = QueryPlayerIDInterface(this.playerID, IID_StatisticsTracker);
+ if (cmpStatisticsTracker)
+ for (const type in amounts)
+ cmpStatisticsTracker.IncreaseResourceUsedCounter(type, -amounts[type]);
+
+ this.AddResources(amounts);
+};
+
Player.prototype.GetNextTradingGoods = function()
{
let value = randFloat(0, 100);
let last = this.tradingGoods.length - 1;
let sumProba = 0;
for (let i = 0; i < last; ++i)
{
sumProba += this.tradingGoods[i].proba;
if (value < sumProba)
return this.tradingGoods[i].goods;
}
return this.tradingGoods[last].goods;
};
Player.prototype.GetTradingGoods = function()
{
let tradingGoods = {};
for (let resource of this.tradingGoods)
tradingGoods[resource.goods] = resource.proba;
return tradingGoods;
};
Player.prototype.SetTradingGoods = function(tradingGoods)
{
let resTradeCodes = Resources.GetTradableCodes();
let sumProba = 0;
for (let resource in tradingGoods)
{
if (resTradeCodes.indexOf(resource) == -1 || tradingGoods[resource] < 0)
{
error("Invalid trading goods: " + uneval(tradingGoods));
return;
}
sumProba += tradingGoods[resource];
}
if (sumProba != 100)
{
error("Invalid trading goods probability: " + uneval(sumProba));
return;
}
this.tradingGoods = [];
for (let resource in tradingGoods)
this.tradingGoods.push({
"goods": resource,
"proba": tradingGoods[resource]
});
};
Player.prototype.GetState = function()
{
return this.state;
};
/**
* @param {string} newState - Either "defeated" or "won".
* @param {string|undefined} message - A string to be shown in chat, for example
* markForTranslation("%(player)s has been defeated (failed objective).").
* If it is undefined, the caller MUST send that GUI notification manually.
*/
Player.prototype.SetState = function(newState, message)
{
if (this.state != "active")
return;
if (newState != "won" && newState != "defeated")
{
warn("Can't change playerstate to " + this.state);
return;
}
if (!this.playerID)
{
warn("Gaia can't change state.");
return;
}
this.state = newState;
let won = newState == "won";
let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
if (won)
cmpRangeManager.SetLosRevealAll(this.playerID, true);
else
{
// Reassign all player's entities to Gaia.
let entities = cmpRangeManager.GetEntitiesByPlayer(this.playerID);
// The ownership change is done in two steps so that entities don't hit idle
// (and thus possibly look for "enemies" to attack) before nearby allies get
// converted to Gaia as well.
for (let entity of entities)
{
let cmpOwnership = Engine.QueryInterface(entity, IID_Ownership);
cmpOwnership.SetOwnerQuiet(0);
}
// With the real ownership change complete, send OwnershipChanged messages.
for (let entity of entities)
Engine.PostMessage(entity, MT_OwnershipChanged, {
"entity": entity,
"from": this.playerID,
"to": 0
});
}
Engine.PostMessage(this.entity, won ? MT_PlayerWon : MT_PlayerDefeated, { "playerId": this.playerID });
if (message)
{
let cmpGUIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface);
cmpGUIInterface.PushNotification({
"type": won ? "won" : "defeat",
"players": [this.playerID],
"allies": [this.playerID],
"message": message
});
}
};
Player.prototype.GetTeam = function()
{
return this.team;
};
Player.prototype.SetTeam = function(team)
{
if (this.teamsLocked)
return;
this.team = team;
// Set all team members as allies.
if (this.team != -1)
{
let numPlayers = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager).GetNumPlayers();
for (let i = 0; i < numPlayers; ++i)
{
let cmpPlayer = QueryPlayerIDInterface(i);
if (this.team != cmpPlayer.GetTeam())
continue;
this.SetAlly(i);
cmpPlayer.SetAlly(this.playerID);
}
}
Engine.BroadcastMessage(MT_DiplomacyChanged, {
"player": this.playerID,
"otherPlayer": null
});
};
Player.prototype.SetLockTeams = function(value)
{
this.teamsLocked = value;
};
Player.prototype.GetLockTeams = function()
{
return this.teamsLocked;
};
Player.prototype.GetDiplomacy = function()
{
return this.diplomacy.slice();
};
Player.prototype.SetDiplomacy = function(dipl)
{
this.diplomacy = dipl.slice();
Engine.BroadcastMessage(MT_DiplomacyChanged, {
"player": this.playerID,
"otherPlayer": null
});
};
Player.prototype.SetDiplomacyIndex = function(idx, value)
{
let cmpPlayer = QueryPlayerIDInterface(idx);
if (!cmpPlayer)
return;
if (this.state != "active" || cmpPlayer.state != "active")
return;
this.diplomacy[idx] = value;
Engine.BroadcastMessage(MT_DiplomacyChanged, {
"player": this.playerID,
"otherPlayer": cmpPlayer.GetPlayerID()
});
// Mutual worsening of relations.
if (cmpPlayer.diplomacy[this.playerID] > value)
cmpPlayer.SetDiplomacyIndex(this.playerID, value);
};
Player.prototype.UpdateSharedLos = function()
{
let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
let cmpTechnologyManager = Engine.QueryInterface(this.entity, IID_TechnologyManager);
if (!cmpRangeManager || !cmpTechnologyManager)
return;
if (!cmpTechnologyManager.IsTechnologyResearched(this.template.SharedLosTech))
{
cmpRangeManager.SetSharedLos(this.playerID, [this.playerID]);
return;
}
cmpRangeManager.SetSharedLos(this.playerID, this.GetMutualAllies());
};
Player.prototype.GetFormations = function()
{
return this.formations;
};
Player.prototype.SetFormations = function(formations)
{
this.formations = formations;
};
Player.prototype.GetStartingCameraPos = function()
{
return this.startCam.position;
};
Player.prototype.GetStartingCameraRot = function()
{
return this.startCam.rotation;
};
Player.prototype.SetStartingCamera = function(pos, rot)
{
this.startCam = { "position": pos, "rotation": rot };
};
Player.prototype.HasStartingCamera = function()
{
return this.startCam !== undefined;
};
Player.prototype.HasSharedLos = function()
{
let cmpTechnologyManager = Engine.QueryInterface(this.entity, IID_TechnologyManager);
return cmpTechnologyManager && cmpTechnologyManager.IsTechnologyResearched(this.template.SharedLosTech);
};
Player.prototype.HasSharedDropsites = function()
{
return this.sharedDropsites;
};
Player.prototype.SetControlAllUnits = function(c)
{
this.controlAllUnits = c;
};
Player.prototype.CanControlAllUnits = function()
{
return this.controlAllUnits;
};
Player.prototype.SetAI = function(flag)
{
this.isAI = flag;
};
Player.prototype.IsAI = function()
{
return this.isAI;
};
Player.prototype.GetPlayersByDiplomacy = function(func)
{
let players = [];
for (let i = 0; i < this.diplomacy.length; ++i)
if (this[func](i))
players.push(i);
return players;
};
Player.prototype.SetAlly = function(id)
{
this.SetDiplomacyIndex(id, 1);
};
/**
* Check if given player is our ally.
*/
Player.prototype.IsAlly = function(id)
{
return this.diplomacy[id] > 0;
};
Player.prototype.GetAllies = function()
{
return this.GetPlayersByDiplomacy("IsAlly");
};
/**
* Check if given player is our ally excluding ourself
*/
Player.prototype.IsExclusiveAlly = function(id)
{
return this.playerID != id && this.IsAlly(id);
};
/**
* Check if given player is our ally, and we are its ally
*/
Player.prototype.IsMutualAlly = function(id)
{
let cmpPlayer = QueryPlayerIDInterface(id);
return this.IsAlly(id) && cmpPlayer && cmpPlayer.IsAlly(this.playerID);
};
Player.prototype.GetMutualAllies = function()
{
return this.GetPlayersByDiplomacy("IsMutualAlly");
};
/**
* Check if given player is our ally, and we are its ally, excluding ourself
*/
Player.prototype.IsExclusiveMutualAlly = function(id)
{
return this.playerID != id && this.IsMutualAlly(id);
};
Player.prototype.SetEnemy = function(id)
{
this.SetDiplomacyIndex(id, -1);
};
/**
* Check if given player is our enemy
*/
Player.prototype.IsEnemy = function(id)
{
return this.diplomacy[id] < 0;
};
Player.prototype.GetEnemies = function()
{
return this.GetPlayersByDiplomacy("IsEnemy");
};
Player.prototype.SetNeutral = function(id)
{
this.SetDiplomacyIndex(id, 0);
};
/**
* Check if given player is neutral
*/
Player.prototype.IsNeutral = function(id)
{
return this.diplomacy[id] == 0;
};
/**
* Do some map dependant initializations
*/
Player.prototype.OnGlobalInitGame = function(msg)
{
// Replace the "{civ}" code with this civ ID.
let disabledTemplates = this.disabledTemplates;
this.disabledTemplates = {};
for (let template in disabledTemplates)
if (disabledTemplates[template])
this.disabledTemplates[template.replace(/\{civ\}/g, this.civ)] = true;
};
/**
* Keep track of population effects of all entities that
* become owned or unowned by this player.
*/
Player.prototype.OnGlobalOwnershipChanged = function(msg)
{
if (msg.from != this.playerID && msg.to != this.playerID)
return;
let cmpCost = Engine.QueryInterface(msg.entity, IID_Cost);
if (msg.from == this.playerID)
{
if (cmpCost)
this.popUsed -= cmpCost.GetPopCost();
let panelIndex = this.panelEntities.indexOf(msg.entity);
if (panelIndex >= 0)
this.panelEntities.splice(panelIndex, 1);
let barterIndex = this.barterEntities.indexOf(msg.entity);
if (barterIndex >= 0)
this.barterEntities.splice(barterIndex, 1);
}
if (msg.to == this.playerID)
{
if (cmpCost)
this.popUsed += cmpCost.GetPopCost();
let cmpIdentity = Engine.QueryInterface(msg.entity, IID_Identity);
if (!cmpIdentity)
return;
if (MatchesClassList(cmpIdentity.GetClassesList(), panelEntityClasses))
this.panelEntities.push(msg.entity);
if (cmpIdentity.HasClass("Barter") && !Engine.QueryInterface(msg.entity, IID_Foundation))
this.barterEntities.push(msg.entity);
}
};
Player.prototype.OnResearchFinished = function(msg)
{
if (msg.tech == this.template.SharedLosTech)
this.UpdateSharedLos();
else if (msg.tech == this.template.SharedDropsitesTech)
this.sharedDropsites = true;
};
Player.prototype.OnDiplomacyChanged = function()
{
this.UpdateSharedLos();
};
Player.prototype.OnValueModification = function(msg)
{
if (msg.component != "Player")
return;
if (msg.valueNames.indexOf("Player/SpyCostMultiplier") != -1)
this.spyCostMultiplier = ApplyValueModificationsToEntity("Player/SpyCostMultiplier", +this.template.SpyCostMultiplier, this.entity);
if (msg.valueNames.some(mod => mod.startsWith("Player/BarterMultiplier/")))
for (let res in this.template.BarterMultiplier.Buy)
{
this.barterMultiplier.buy[res] = ApplyValueModificationsToEntity("Player/BarterMultiplier/Buy/"+res, +this.template.BarterMultiplier.Buy[res], this.entity);
this.barterMultiplier.sell[res] = ApplyValueModificationsToEntity("Player/BarterMultiplier/Sell/"+res, +this.template.BarterMultiplier.Sell[res], this.entity);
}
};
Player.prototype.SetCheatsEnabled = function(flag)
{
this.cheatsEnabled = flag;
};
Player.prototype.GetCheatsEnabled = function()
{
return this.cheatsEnabled;
};
Player.prototype.TributeResource = function(player, amounts)
{
let cmpPlayer = QueryPlayerIDInterface(player);
if (!cmpPlayer)
return;
if (this.state != "active" || cmpPlayer.state != "active")
return;
let resTribCodes = Resources.GetTributableCodes();
for (let resCode in amounts)
if (resTribCodes.indexOf(resCode) == -1 ||
!Number.isInteger(amounts[resCode]) ||
amounts[resCode] < 0)
{
warn("Invalid tribute amounts: " + uneval(resCode) + ": " + uneval(amounts));
return;
}
if (!this.SubtractResourcesOrNotify(amounts))
return;
cmpPlayer.AddResources(amounts);
let total = Object.keys(amounts).reduce((sum, type) => sum + amounts[type], 0);
let cmpOurStatisticsTracker = QueryPlayerIDInterface(this.playerID, IID_StatisticsTracker);
if (cmpOurStatisticsTracker)
cmpOurStatisticsTracker.IncreaseTributesSentCounter(total);
let cmpTheirStatisticsTracker = QueryPlayerIDInterface(player, IID_StatisticsTracker);
if (cmpTheirStatisticsTracker)
cmpTheirStatisticsTracker.IncreaseTributesReceivedCounter(total);
let cmpGUIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface);
if (cmpGUIInterface)
cmpGUIInterface.PushNotification({
"type": "tribute",
"players": [player],
"donator": this.playerID,
"amounts": amounts
});
Engine.BroadcastMessage(MT_TributeExchanged, {
"to": player,
"from": this.playerID,
"amounts": amounts
});
};
Player.prototype.AddDisabledTemplate = function(template)
{
this.disabledTemplates[template] = true;
Engine.BroadcastMessage(MT_DisabledTemplatesChanged, { "player": this.playerID });
};
Player.prototype.RemoveDisabledTemplate = function(template)
{
this.disabledTemplates[template] = false;
Engine.BroadcastMessage(MT_DisabledTemplatesChanged, { "player": this.playerID });
};
Player.prototype.SetDisabledTemplates = function(templates)
{
this.disabledTemplates = {};
for (let template of templates)
this.disabledTemplates[template] = true;
Engine.BroadcastMessage(MT_DisabledTemplatesChanged, { "player": this.playerID });
};
Player.prototype.GetDisabledTemplates = function()
{
return this.disabledTemplates;
};
Player.prototype.AddDisabledTechnology = function(tech)
{
this.disabledTechnologies[tech] = true;
Engine.BroadcastMessage(MT_DisabledTechnologiesChanged, { "player": this.playerID });
};
Player.prototype.RemoveDisabledTechnology = function(tech)
{
this.disabledTechnologies[tech] = false;
Engine.BroadcastMessage(MT_DisabledTechnologiesChanged, { "player": this.playerID });
};
Player.prototype.SetDisabledTechnologies = function(techs)
{
this.disabledTechnologies = {};
for (let tech of techs)
this.disabledTechnologies[tech] = true;
Engine.BroadcastMessage(MT_DisabledTechnologiesChanged, { "player": this.playerID });
};
Player.prototype.GetDisabledTechnologies = function()
{
return this.disabledTechnologies;
};
Player.prototype.OnGlobalPlayerDefeated = function(msg)
{
let cmpSound = Engine.QueryInterface(this.entity, IID_Sound);
if (!cmpSound)
return;
let soundGroup = cmpSound.GetSoundGroup(this.playerID === msg.playerId ? "defeated" : this.IsAlly(msg.playerId) ? "defeated_ally" : this.state === "won" ? "won" : "defeated_enemy");
if (soundGroup)
Engine.QueryInterface(SYSTEM_ENTITY, IID_SoundManager).PlaySoundGroupForPlayer(soundGroup, this.playerID);
};
Engine.RegisterComponentType(IID_Player, "Player", Player);
Index: ps/trunk/binaries/data/mods/public/simulation/components/ProductionQueue.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/components/ProductionQueue.js (revision 25999)
+++ ps/trunk/binaries/data/mods/public/simulation/components/ProductionQueue.js (revision 26000)
@@ -1,1018 +1,518 @@
function ProductionQueue() {}
ProductionQueue.prototype.Schema =
- "Allows the building to train new units and research technologies" +
- "" +
- "0.7" +
- "" +
- "\n units/{civ}/support_female_citizen\n units/{native}/support_trader\n units/athen/infantry_spearman_b\n " +
- "" +
- "" +
- "" +
- "" +
- "" +
- "" +
- "" +
- "" +
- "tokens" +
- "" +
- "" +
- "" +
- "" +
- "" +
- "" +
- "" +
- "tokens" +
- "" +
- "" +
- "" +
- "" +
- "" +
- Resources.BuildSchema("nonNegativeDecimal", ["time"]) +
- "";
+ "Helps the building to train new units and research technologies." +
+ "";
ProductionQueue.prototype.ProgressInterval = 1000;
ProductionQueue.prototype.MaxQueueSize = 16;
-ProductionQueue.prototype.Init = function()
+/**
+ * This object represents an item in the queue.
+ */
+ProductionQueue.prototype.Item = function() {};
+
+/**
+ * @param {number} producer - The entity ID of our producer.
+ * @param {string} metadata - Optionally any metadata attached to us.
+ */
+ProductionQueue.prototype.Item.prototype.Init = function(producer, metadata)
{
- this.nextID = 1;
+ this.producer = producer;
+ this.metadata = metadata;
- this.queue = [];
- /**
- Queue items are:
- {
- "id": 1,
- "player": 1, // Who paid for this batch; we need this to cope with refunds cleanly.
- "productionStarted": false, // true iff production has started (we have reserved population).
- "timeTotal": 15000, // msecs
- "timeRemaining": 10000, // msecs
- "paused": false, // Whether the item is currently paused (e.g. not the first item or parent is garrisoned).
- "resources": { "wood": 100, ... }, // Total resources of the item when queued.
- "entity": {
- "template": "units/example",
- "count": 10,
- "neededSlots": 3, // Number of population slots missing for production to begin.
- "population": 1, // Population per unit, multiply by count to get total.
- "resources": { "wood": 100, ... }, // Resources per entity, multiply by count to get total.
- "entityCache": [189, ...], // The entities created but not spawned yet.
- },
- "technology": {
- "template": "example_tech",
- "resources": { "wood": 100, ... },
- }
- }
- */
};
-/*
- * Returns list of entities that can be trained by this building.
- */
-ProductionQueue.prototype.GetEntitiesList = function()
+ProductionQueue.prototype.Item.prototype.QueueEntity = function(templateName, count)
+{
+ const cmpTrainer = Engine.QueryInterface(this.producer, IID_Trainer);
+ if (!cmpTrainer)
+ return false;
+ this.entity = cmpTrainer.QueueBatch(templateName, count, this.metadata);
+ if (this.entity == -1)
+ return false;
+ this.originalItem = {
+ "templateName": templateName,
+ "count": count,
+ "metadata": this.metadata
+ };
+
+ return true;
+};
+
+ProductionQueue.prototype.Item.prototype.QueueTechnology = function(templateName)
{
- return Array.from(this.entitiesMap.values());
+ const cmpResearcher = Engine.QueryInterface(this.producer, IID_Researcher);
+ if (!cmpResearcher)
+ return false;
+ this.technology = cmpResearcher.QueueTechnology(templateName, this.metadata);
+ if (this.technology == -1)
+ return false;
+
+ return true;
};
/**
- * @return {boolean} - Whether we are automatically queuing items.
+ * @param {number} id - The id of this item.
*/
-ProductionQueue.prototype.IsAutoQueueing = function()
+ProductionQueue.prototype.Item.prototype.SetID = function(id)
{
- return !!this.autoqueuing;
+ this.id = id;
+};
+
+ProductionQueue.prototype.Item.prototype.Stop = function()
+{
+ if (this.entity)
+ {
+ const cmpTrainer = Engine.QueryInterface(this.producer, IID_Trainer);
+ if (cmpTrainer)
+ cmpTrainer.StopBatch(this.entity);
+ }
+
+ if (this.technology)
+ {
+ const cmpResearcher = Engine.QueryInterface(this.producer, IID_Researcher);
+ if (cmpResearcher)
+ cmpResearcher.StopResearching(this.technology);
+ }
};
/**
- * Turn on Auto-Queue.
+ * Called when the first work is performed.
*/
-ProductionQueue.prototype.EnableAutoQueue = function()
+ProductionQueue.prototype.Item.prototype.Start = function()
{
- this.autoqueuing = true;
+ this.started = true;
+};
+
+ProductionQueue.prototype.Item.prototype.IsStarted = function()
+{
+ return !!this.started;
};
/**
- * Turn off Auto-Queue.
+ * @return {boolean} - Whether this item is finished.
*/
-ProductionQueue.prototype.DisableAutoQueue = function()
+ProductionQueue.prototype.Item.prototype.IsFinished = function()
{
- delete this.autoqueuing;
+ return !!this.finished;
};
/**
- * Calculate the new list of producible entities
- * and update any entities currently being produced.
+ * @param {number} allocatedTime - The time allocated to this item.
+ * @return {number} - The time used for this item.
*/
-ProductionQueue.prototype.CalculateEntitiesMap = function()
+ProductionQueue.prototype.Item.prototype.Progress = function(allocatedTime)
{
- // Don't reset the map, it's used below to update entities.
- if (!this.entitiesMap)
- this.entitiesMap = new Map();
- if (!this.template.Entities)
- return;
-
- let string = this.template.Entities._string;
- // Tokens can be added -> process an empty list to get them.
- let addedTokens = ApplyValueModificationsToEntity("ProductionQueue/Entities/_string", "", this.entity);
- if (!addedTokens && !string)
- return;
-
- addedTokens = addedTokens == "" ? [] : addedTokens.split(/\s+/);
+ if (this.entity)
+ {
+ const cmpTrainer = Engine.QueryInterface(this.producer, IID_Trainer);
+ allocatedTime -= cmpTrainer.Progress(this.entity, allocatedTime);
+ if (!cmpTrainer.HasBatch(this.entity))
+ delete this.entity;
+ }
+ if (this.technology)
+ {
+ const cmpResearcher = Engine.QueryInterface(this.producer, IID_Researcher);
+ allocatedTime -= cmpResearcher.Progress(this.technology, allocatedTime);
+ if (!cmpResearcher.HasItem(this.technology))
+ delete this.technology;
+ }
+ if (!this.entity && !this.technology)
+ this.finished = true;
- let cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager);
- let cmpPlayer = QueryOwnerInterface(this.entity);
- let cmpIdentity = Engine.QueryInterface(this.entity, IID_Identity);
-
- let disabledEntities = cmpPlayer ? cmpPlayer.GetDisabledTemplates() : {};
-
- /**
- * Process tokens:
- * - process token modifiers (this is a bit tricky).
- * - replace the "{civ}" and "{native}" codes with the owner's civ ID and entity's civ ID
- * - remove disabled entities
- * - upgrade templates where necessary
- * This also updates currently queued production (it's more convenient to do it here).
- */
-
- let removeAllQueuedTemplate = (token) => {
- let queue = clone(this.queue);
- let template = this.entitiesMap.get(token);
- for (let item of queue)
- if (item.entity?.template && item.entity.template === template)
- this.RemoveItem(item.id);
- };
- let updateAllQueuedTemplate = (token, updateTo) => {
- let template = this.entitiesMap.get(token);
- for (let item of this.queue)
- if (item.entity?.template && item.entity.template === template)
- item.entity.template = updateTo;
- };
+ return allocatedTime;
+};
- let toks = string.split(/\s+/);
- for (let tok of addedTokens)
- toks.push(tok);
-
- let addedDict = addedTokens.reduce((out, token) => { out[token] = true; return out; }, {});
- this.entitiesMap = toks.reduce((entMap, token) => {
- let rawToken = token;
- if (!(token in addedDict))
- {
- // This is a bit wasteful but I can't think of a simpler/better way.
- // The list of token is unlikely to be a performance bottleneck anyways.
- token = ApplyValueModificationsToEntity("ProductionQueue/Entities/_string", token, this.entity);
- token = token.split(/\s+/);
- if (token.every(tok => addedTokens.indexOf(tok) !== -1))
- {
- removeAllQueuedTemplate(rawToken);
- return entMap;
- }
- token = token[0];
- }
- // Replace the "{civ}" and "{native}" codes with the owner's civ ID and entity's civ ID.
- if (cmpIdentity)
- token = token.replace(/\{native\}/g, cmpIdentity.GetCiv());
- if (cmpPlayer)
- token = token.replace(/\{civ\}/g, cmpPlayer.GetCiv());
+ProductionQueue.prototype.Item.prototype.Pause = function()
+{
+ this.paused = true;
+ if (this.entity)
+ Engine.QueryInterface(this.producer, IID_Trainer).PauseBatch(this.entity);
+ if (this.technology)
+ Engine.QueryInterface(this.producer, IID_Researcher).PauseTechnology(this.technology);
+};
- // Filter out disabled and invalid entities.
- if (disabledEntities[token] || !cmpTemplateManager.TemplateExists(token))
- {
- removeAllQueuedTemplate(rawToken);
- return entMap;
- }
+ProductionQueue.prototype.Item.prototype.Unpause = function()
+{
+ delete this.paused;
+ if (this.entity)
+ Engine.QueryInterface(this.producer, IID_Trainer).UnpauseBatch(this.entity);
+ if (this.technology)
+ Engine.QueryInterface(this.producer, IID_Researcher).UnpauseTechnology(this.technology);
+};
- token = this.GetUpgradedTemplate(token);
- entMap.set(rawToken, token);
- updateAllQueuedTemplate(rawToken, token);
- return entMap;
- }, new Map());
+ProductionQueue.prototype.Item.prototype.IsPaused = function()
+{
+ return !!this.paused;
};
-/*
- * Returns the upgraded template name if necessary.
+/**
+ * @return {Object} - Some basic information of this item.
*/
-ProductionQueue.prototype.GetUpgradedTemplate = function(templateName)
+ProductionQueue.prototype.Item.prototype.GetBasicInfo = function()
{
- let cmpPlayer = QueryOwnerInterface(this.entity);
- if (!cmpPlayer)
- return templateName;
-
- let cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager);
- let template = cmpTemplateManager.GetTemplate(templateName);
- while (template && template.Promotion !== undefined)
- {
- let requiredXp = ApplyValueModificationsToTemplate(
- "Promotion/RequiredXp",
- +template.Promotion.RequiredXp,
- cmpPlayer.GetPlayerID(),
- template);
- if (requiredXp > 0)
- break;
- templateName = template.Promotion.Entity;
- template = cmpTemplateManager.GetTemplate(templateName);
- }
- return templateName;
+ let result;
+ if (this.technology)
+ result = Engine.QueryInterface(this.producer, IID_Researcher).GetResearchingTechnology(this.technology);
+ else if (this.entity)
+ result = Engine.QueryInterface(this.producer, IID_Trainer).GetBatch(this.entity);
+ result.id = this.id;
+ result.paused = this.paused;
+ return result;
};
-/*
- * Returns list of technologies that can be researched by this building.
+/**
+ * @return {Object} - The originally queued item.
*/
-ProductionQueue.prototype.GetTechnologiesList = function()
+ProductionQueue.prototype.Item.prototype.OriginalItem = function()
{
- if (!this.template.Technologies)
- return [];
-
- let string = this.template.Technologies._string;
- string = ApplyValueModificationsToEntity("ProductionQueue/Technologies/_string", string, this.entity);
-
- if (!string)
- return [];
-
- let cmpTechnologyManager = QueryOwnerInterface(this.entity, IID_TechnologyManager);
- if (!cmpTechnologyManager)
- return [];
-
- let cmpPlayer = QueryOwnerInterface(this.entity);
- if (!cmpPlayer)
- return [];
-
- let techs = string.split(/\s+/);
-
- // Replace the civ specific technologies.
- for (let i = 0; i < techs.length; ++i)
- {
- let tech = techs[i];
- if (tech.indexOf("{civ}") == -1)
- continue;
- let civTech = tech.replace("{civ}", cmpPlayer.GetCiv());
- techs[i] = TechnologyTemplates.Has(civTech) ? civTech : tech.replace("{civ}", "generic");
- }
+ return this.originalItem;
+};
- // Remove any technologies that can't be researched by this civ.
- techs = techs.filter(tech =>
- cmpTechnologyManager.CheckTechnologyRequirements(
- DeriveTechnologyRequirements(TechnologyTemplates.Get(tech), cmpPlayer.GetCiv()),
- true));
+ProductionQueue.prototype.Item.prototype.Serialize = function()
+{
+ return {
+ "id": this.id,
+ "metadata": this.metadata,
+ "paused": this.paused,
+ "producer": this.producer,
+ "entity": this.entity,
+ "technology": this.technology,
+ "started": this.started,
+ "originalItem": this.originalItem
+ };
+};
- let techList = [];
- // Stores the tech which supersedes the key.
- let superseded = {};
+ProductionQueue.prototype.Item.prototype.Deserialize = function(data)
+{
+ this.Init(data.producer, data.metadata);
- let disabledTechnologies = cmpPlayer.GetDisabledTechnologies();
+ this.id = data.id;
+ this.paused = data.paused;
+ this.entity = data.entity;
+ this.technology = data.technology;
+ this.started = data.started;
+ this.originalItem = data.originalItem;
+};
- // Add any top level technologies to an array which corresponds to the displayed icons.
- // Also store what technology is superseded in the superseded object { "tech1":"techWhichSupercedesTech1", ... }.
- for (let tech of techs)
- {
- if (disabledTechnologies && disabledTechnologies[tech])
- continue;
+ProductionQueue.prototype.Init = function()
+{
+ this.nextID = 1;
- let template = TechnologyTemplates.Get(tech);
- if (!template.supersedes || techs.indexOf(template.supersedes) === -1)
- techList.push(tech);
- else
- superseded[template.supersedes] = tech;
- }
+ this.queue = [];
+};
- // Now make researched/in progress techs invisible.
- for (let i in techList)
- {
- let tech = techList[i];
- while (this.IsTechnologyResearchedOrInProgress(tech))
- tech = superseded[tech];
+ProductionQueue.prototype.Serialize = function()
+{
+ const queue = [];
+ for (const item of this.queue)
+ queue.push(item.Serialize());
+
+ return {
+ "autoqueuing": this.autoqueuing,
+ "nextID": this.nextID,
+ "paused": this.paused,
+ "timer": this.timer,
+ "queue": queue
+ };
+};
- techList[i] = tech;
- }
+ProductionQueue.prototype.Deserialize = function(data)
+{
+ this.Init();
- let ret = [];
+ this.autoqueuing = data.autoqueuing;
+ this.nextID = data.nextID;
+ this.paused = data.paused;
+ this.timer = data.timer;
- // This inserts the techs into the correct positions to line up the technology pairs.
- for (let i = 0; i < techList.length; ++i)
+ for (const item of data.queue)
{
- let tech = techList[i];
- if (!tech)
- {
- ret[i] = undefined;
- continue;
- }
-
- let template = TechnologyTemplates.Get(tech);
- if (template.top)
- ret[i] = { "pair": true, "top": template.top, "bottom": template.bottom };
- else
- ret[i] = tech;
+ const newItem = new this.Item();
+ newItem.Deserialize(item);
+ this.queue.push(newItem);
}
-
- return ret;
};
-ProductionQueue.prototype.GetTechCostMultiplier = function()
+/**
+ * @return {boolean} - Whether we are automatically queuing items.
+ */
+ProductionQueue.prototype.IsAutoQueueing = function()
{
- let techCostMultiplier = {};
- for (let res in this.template.TechCostMultiplier)
- techCostMultiplier[res] = ApplyValueModificationsToEntity(
- "ProductionQueue/TechCostMultiplier/" + res,
- +this.template.TechCostMultiplier[res],
- this.entity);
-
- return techCostMultiplier;
+ return !!this.autoqueuing;
};
-ProductionQueue.prototype.IsTechnologyResearchedOrInProgress = function(tech)
+/**
+ * Turn on Auto-Queue.
+ */
+ProductionQueue.prototype.EnableAutoQueue = function()
{
- if (!tech)
- return false;
-
- let cmpTechnologyManager = QueryOwnerInterface(this.entity, IID_TechnologyManager);
- if (!cmpTechnologyManager)
- return false;
-
- let template = TechnologyTemplates.Get(tech);
- if (template.top)
- return cmpTechnologyManager.IsTechnologyResearched(template.top) ||
- cmpTechnologyManager.IsInProgress(template.top) ||
- cmpTechnologyManager.IsTechnologyResearched(template.bottom) ||
- cmpTechnologyManager.IsInProgress(template.bottom);
+ this.autoqueuing = true;
+};
- return cmpTechnologyManager.IsTechnologyResearched(tech) || cmpTechnologyManager.IsInProgress(tech);
+/**
+ * Turn off Auto-Queue.
+ */
+ProductionQueue.prototype.DisableAutoQueue = function()
+{
+ delete this.autoqueuing;
};
/*
* Adds a new batch of identical units to train or a technology to research to the production queue.
* @param {string} templateName - The template to start production on.
* @param {string} type - The type of production (i.e. "unit" or "technology").
* @param {number} count - The amount of units to be produced. Ignored for a tech.
* @param {any} metadata - Optionally any metadata to be attached to the item.
* @param {boolean} pushFront - Whether to push the item to the front of the queue and pause any item(s) currently in progress.
*
* @return {boolean} - Whether the addition of the item has succeeded.
*/
ProductionQueue.prototype.AddItem = function(templateName, type, count, metadata, pushFront = false)
{
// TODO: there should be a way for the GUI to determine whether it's going
// to be possible to add a batch (based on resource costs and length limits).
- let cmpPlayer = QueryOwnerInterface(this.entity);
- if (!cmpPlayer)
- return false;
- let player = cmpPlayer.GetPlayerID();
if (!this.queue.length)
{
- let cmpUpgrade = Engine.QueryInterface(this.entity, IID_Upgrade);
+ const cmpPlayer = QueryOwnerInterface(this.entity);
+ if (!cmpPlayer)
+ return false;
+ const player = cmpPlayer.GetPlayerID();
+ const cmpUpgrade = Engine.QueryInterface(this.entity, IID_Upgrade);
if (cmpUpgrade && cmpUpgrade.IsUpgrading())
{
let cmpGUIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface);
cmpGUIInterface.PushNotification({
"players": [player],
"message": markForTranslation("Entity is being upgraded. Cannot start production."),
"translateMessage": true
});
return false;
}
}
else if (this.queue.length >= this.MaxQueueSize)
{
- let cmpGUIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface);
+ const cmpPlayer = QueryOwnerInterface(this.entity);
+ if (!cmpPlayer)
+ return false;
+ const player = cmpPlayer.GetPlayerID();
+ const cmpGUIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface);
cmpGUIInterface.PushNotification({
"players": [player],
"message": markForTranslation("The production queue is full."),
"translateMessage": true,
});
return false;
}
- const item = {
- "player": player,
- "metadata": metadata,
- "productionStarted": false,
- "resources": {}, // The total resource costs.
- "paused": false
- };
-
- // ToDo: Still some duplication here, some can might be combined,
- // but requires some more refactoring.
+ const item = new this.Item();
+ item.Init(this.entity, metadata);
if (type == "unit")
{
- if (!Number.isInteger(count) || count <= 0)
- {
- error("Invalid batch count " + count);
- return false;
- }
-
- let cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager);
- let template = cmpTemplateManager.GetTemplate(this.GetUpgradedTemplate(templateName));
- if (!template)
+ if (!item.QueueEntity(templateName, count))
return false;
-
- item.entity = {
- "template": templateName,
- "count": count,
- "population": ApplyValueModificationsToTemplate(
- "Cost/Population",
- +template.Cost.Population,
- player,
- template),
- "resources": {}, // The resource costs per entity.
- };
-
- for (let res in template.Cost.Resources)
- {
- item.entity.resources[res] = ApplyValueModificationsToTemplate(
- "Cost/Resources/" + res,
- +template.Cost.Resources[res],
- player,
- template);
-
- item.resources[res] = Math.floor(count * item.entity.resources[res]);
- }
-
- if (template.TrainingRestrictions)
- {
- let unitCategory = template.TrainingRestrictions.Category;
- let cmpPlayerEntityLimits = QueryPlayerIDInterface(player, IID_EntityLimits);
- if (cmpPlayerEntityLimits)
- {
- if (!cmpPlayerEntityLimits.AllowedToTrain(unitCategory, count, templateName, template.TrainingRestrictions.MatchLimit))
- // Already warned, return.
- return false;
- cmpPlayerEntityLimits.ChangeCount(unitCategory, count);
- if (template.TrainingRestrictions.MatchLimit)
- cmpPlayerEntityLimits.ChangeMatchCount(templateName, count);
- }
- }
-
- const buildTime = ApplyValueModificationsToTemplate(
- "Cost/BuildTime",
- +template.Cost.BuildTime,
- player,
- template);
- const time = this.GetBatchTime(count) * buildTime * 1000;
- item.timeTotal = time;
- item.timeRemaining = time;
}
else if (type == "technology")
{
- if (!TechnologyTemplates.Has(templateName))
- return false;
-
- if (!this.GetTechnologiesList().some(tech =>
- tech &&
- (tech == templateName ||
- tech.pair &&
- (tech.top == templateName || tech.bottom == templateName))))
- {
- error("This entity cannot research " + templateName);
+ if (!item.QueueTechnology(templateName))
return false;
- }
-
- item.technology = {
- "template": templateName,
- "resources": {}
- };
-
- let template = TechnologyTemplates.Get(templateName);
- let techCostMultiplier = this.GetTechCostMultiplier();
-
- if (template.cost)
- for (const res in template.cost)
- {
- item.technology.resources[res] = Math.floor((techCostMultiplier[res] || 1) * template.cost[res]);
- item.resources[res] = item.technology.resources[res];
- }
-
- const time = techCostMultiplier.time * (template.researchTime || 0) * 1000;
- item.timeTotal = time;
- item.timeRemaining = time;
}
else
{
warn("Tried to add invalid item of type \"" + type + "\" and template \"" + templateName + "\" to a production queue");
return false;
}
- // TrySubtractResources should report error to player (they ran out of resources).
- if (!cmpPlayer.TrySubtractResources(item.resources))
- return false;
-
- item.id = this.nextID++;
+ item.SetID(this.nextID++);
if (pushFront)
{
- if (this.queue[0])
- this.queue[0].paused = true;
+ this.queue[0]?.Pause();
this.queue.unshift(item);
}
else
this.queue.push(item);
- const cmpTrigger = Engine.QueryInterface(SYSTEM_ENTITY, IID_Trigger);
- if (item.entity)
- cmpTrigger.CallEvent("OnTrainingQueued", {
- "playerid": player,
- "unitTemplate": item.entity.template,
- "count": count,
- "metadata": metadata,
- "trainerEntity": this.entity
- });
- if (item.technology)
- {
- // Tell the technology manager that we have started researching this
- // such that players can't research the same thing twice.
- const cmpTechnologyManager = QueryOwnerInterface(this.entity, IID_TechnologyManager);
- cmpTechnologyManager.QueuedResearch(templateName, this.entity);
-
- cmpTrigger.CallEvent("OnResearchQueued", {
- "playerid": player,
- "technologyTemplate": item.technology.template,
- "researcherEntity": this.entity
- });
- }
-
Engine.PostMessage(this.entity, MT_ProductionQueueChanged, null);
if (!this.timer)
this.StartTimer();
return true;
};
/*
* Removes an item from the queue.
- * Refunds resource costs and population reservations.
- * item.player is used as this.entity's owner may have changed.
*/
ProductionQueue.prototype.RemoveItem = function(id)
{
let itemIndex = this.queue.findIndex(item => item.id == id);
if (itemIndex == -1)
return;
- let item = this.queue[itemIndex];
-
- // Destroy any cached entities (those which didn't spawn for some reason).
- if (item.entity?.cache?.length)
- {
- for (const ent of item.entity.cache)
- Engine.DestroyEntity(ent);
-
- delete item.entity.cache;
- }
-
- const cmpPlayer = QueryPlayerIDInterface(item.player);
-
- if (item.entity)
- {
- let cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager);
- const template = cmpTemplateManager.GetTemplate(item.entity.template);
- if (template.TrainingRestrictions)
- {
- let cmpPlayerEntityLimits = QueryPlayerIDInterface(item.player, IID_EntityLimits);
- if (cmpPlayerEntityLimits)
- cmpPlayerEntityLimits.ChangeCount(template.TrainingRestrictions.Category, -item.entity.count);
- if (template.TrainingRestrictions.MatchLimit)
- cmpPlayerEntityLimits.ChangeMatchCount(item.entity.template, -item.entity.count);
- }
- if (cmpPlayer)
- {
- if (item.productionStarted)
- cmpPlayer.UnReservePopulationSlots(item.entity.population * item.entity.count);
- if (itemIndex == 0)
- cmpPlayer.UnBlockTraining();
- }
- }
-
- let cmpStatisticsTracker = QueryPlayerIDInterface(item.player, IID_StatisticsTracker);
-
- const totalCosts = {};
- for (let resource in item.resources)
- {
- totalCosts[resource] = 0;
- if (item.entity)
- totalCosts[resource] += Math.floor(item.entity.count * item.entity.resources[resource]);
- if (item.technology)
- totalCosts[resource] += item.technology.resources[resource];
- if (cmpStatisticsTracker)
- cmpStatisticsTracker.IncreaseResourceUsedCounter(resource, -totalCosts[resource]);
- }
-
- if (cmpPlayer)
- cmpPlayer.AddResources(totalCosts);
-
- if (item.technology)
- {
- let cmpTechnologyManager = QueryPlayerIDInterface(item.player, IID_TechnologyManager);
- if (cmpTechnologyManager)
- cmpTechnologyManager.StoppedResearch(item.technology.template, true);
- }
+ this.queue.splice(itemIndex, 1)[0].Stop();
- this.queue.splice(itemIndex, 1);
Engine.PostMessage(this.entity, MT_ProductionQueueChanged, null);
if (!this.queue.length)
this.StopTimer();
};
ProductionQueue.prototype.SetAnimation = function(name)
{
let cmpVisual = Engine.QueryInterface(this.entity, IID_Visual);
if (cmpVisual)
cmpVisual.SelectAnimation(name, false, 1);
};
/*
* Returns basic data from all batches in the production queue.
*/
ProductionQueue.prototype.GetQueue = function()
{
- return this.queue.map(item => ({
- "id": item.id,
- "unitTemplate": item.entity?.template,
- "technologyTemplate": item.technology?.template,
- "count": item.entity?.count,
- "neededSlots": item.entity?.neededSlots,
- "progress": 1 - (item.timeRemaining / (item.timeTotal || 1)),
- "timeRemaining": item.timeRemaining,
- "paused": item.paused,
- "metadata": item.metadata
- }));
+ return this.queue.map(item => item.GetBasicInfo());
};
/*
* Removes all existing batches from the queue.
*/
ProductionQueue.prototype.ResetQueue = function()
{
while (this.queue.length)
this.RemoveItem(this.queue[0].id);
this.DisableAutoQueue();
};
/*
- * Returns batch build time.
- */
-ProductionQueue.prototype.GetBatchTime = function(batchSize)
-{
- // TODO: work out what equation we should use here.
- return Math.pow(batchSize, ApplyValueModificationsToEntity(
- "ProductionQueue/BatchTimeModifier",
- +this.template.BatchTimeModifier,
- this.entity));
-};
-
-ProductionQueue.prototype.OnOwnershipChanged = function(msg)
-{
- // Reset the production queue whenever the owner changes.
- // (This should prevent players getting surprised when they capture
- // an enemy building, and then loads of the enemy's civ's soldiers get
- // created from it. Also it means we don't have to worry about
- // updating the reserved pop slots.)
- this.ResetQueue();
-
- if (msg.to != INVALID_PLAYER)
- this.CalculateEntitiesMap();
-};
-
-ProductionQueue.prototype.OnCivChanged = function()
-{
- this.CalculateEntitiesMap();
-};
-
-/*
- * This function creates the entities and places them in world if possible
- * (some of these entities may be garrisoned directly if autogarrison, the others are spawned).
- * @param {Object} item - The item to spawn units for.
- * @param {number} item.entity.count - The number of entities to spawn.
- * @param {string} item.player - The owner of the item.
- * @param {string} item.entity.template - The template to spawn.
- * @param {any} - item.metadata - Optionally any metadata to add to the TrainingFinished message.
- *
- * @return {number} - The number of successfully created entities
- */
-ProductionQueue.prototype.SpawnUnits = function(item)
-{
- let createdEnts = [];
- let spawnedEnts = [];
-
- // We need entities to test spawning, but we don't want to waste resources,
- // so only create them once and use as needed.
- if (!item.entity.cache)
- {
- item.entity.cache = [];
- for (let i = 0; i < item.entity.count; ++i)
- item.entity.cache.push(Engine.AddEntity(item.entity.template));
- }
-
- let autoGarrison;
- let cmpRallyPoint = Engine.QueryInterface(this.entity, IID_RallyPoint);
- if (cmpRallyPoint)
- {
- let data = cmpRallyPoint.GetData()[0];
- if (data && data.target && data.target == this.entity && data.command == "garrison")
- autoGarrison = true;
- }
-
- let cmpFootprint = Engine.QueryInterface(this.entity, IID_Footprint);
- let cmpPosition = Engine.QueryInterface(this.entity, IID_Position);
- let positionSelf = cmpPosition && cmpPosition.GetPosition();
-
- let cmpPlayerEntityLimits = QueryPlayerIDInterface(item.player, IID_EntityLimits);
- let cmpPlayerStatisticsTracker = QueryPlayerIDInterface(item.player, IID_StatisticsTracker);
- while (item.entity.cache.length)
- {
- const ent = item.entity.cache[0];
- let cmpNewOwnership = Engine.QueryInterface(ent, IID_Ownership);
- let garrisoned = false;
-
- if (autoGarrison)
- {
- let cmpGarrisonable = Engine.QueryInterface(ent, IID_Garrisonable);
- if (cmpGarrisonable)
- {
- // Temporary owner affectation needed for GarrisonHolder checks.
- cmpNewOwnership.SetOwnerQuiet(item.player);
- garrisoned = cmpGarrisonable.Garrison(this.entity);
- cmpNewOwnership.SetOwnerQuiet(INVALID_PLAYER);
- }
- }
-
- if (!garrisoned)
- {
- let pos = cmpFootprint.PickSpawnPoint(ent);
- if (pos.y < 0)
- break;
-
- let cmpNewPosition = Engine.QueryInterface(ent, IID_Position);
- cmpNewPosition.JumpTo(pos.x, pos.z);
-
- if (positionSelf)
- cmpNewPosition.SetYRotation(positionSelf.horizAngleTo(pos));
-
- spawnedEnts.push(ent);
- }
-
- // Decrement entity count in the EntityLimits component
- // since it will be increased by EntityLimits.OnGlobalOwnershipChanged,
- // i.e. we replace a 'trained' entity by 'alive' one.
- // Must be done after spawn check so EntityLimits decrements only if unit spawns.
- if (cmpPlayerEntityLimits)
- {
- let cmpTrainingRestrictions = Engine.QueryInterface(ent, IID_TrainingRestrictions);
- if (cmpTrainingRestrictions)
- cmpPlayerEntityLimits.ChangeCount(cmpTrainingRestrictions.GetCategory(), -1);
- }
- cmpNewOwnership.SetOwner(item.player);
-
- if (cmpPlayerStatisticsTracker)
- cmpPlayerStatisticsTracker.IncreaseTrainedUnitsCounter(ent);
-
- item.entity.cache.shift();
- createdEnts.push(ent);
- }
-
- if (spawnedEnts.length && !autoGarrison && cmpRallyPoint)
- for (let com of GetRallyPointCommands(cmpRallyPoint, spawnedEnts))
- ProcessCommand(item.player, com);
-
- if (createdEnts.length)
- {
- // Play a sound, but only for the first in the batch (to avoid nasty phasing effects).
- PlaySound("trained", createdEnts[0]);
- Engine.PostMessage(this.entity, MT_TrainingFinished, {
- "entities": createdEnts,
- "owner": item.player,
- "metadata": item.metadata
- });
- }
-
- return createdEnts.length;
-};
-
-/*
- * Increments progress on the first item in the production queue and blocks the
- * queue if population limit is reached or some units failed to spawn.
+ * Increments progress on the first item in the production queue.
* @param {Object} data - Unused in this case.
* @param {number} lateness - The time passed since the expected time to fire the function.
*/
ProductionQueue.prototype.ProgressTimeout = function(data, lateness)
{
if (this.paused)
return;
- let cmpPlayer = QueryOwnerInterface(this.entity);
- if (!cmpPlayer)
- return;
-
// Allocate available time to as many queue items as it takes
// until we've used up all the time (so that we work accurately
// with items that take fractions of a second).
let time = this.ProgressInterval + lateness;
- let cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager);
while (this.queue.length)
{
let item = this.queue[0];
- if (item.paused)
- item.paused = false;
- if (!item.productionStarted)
+ if (item.IsPaused())
+ item.Unpause();
+ if (!item.IsStarted())
{
if (item.entity)
- {
- const template = cmpTemplateManager.GetTemplate(item.entity.template);
- item.entity.population = ApplyValueModificationsToTemplate(
- "Cost/Population",
- +template.Cost.Population,
- item.player,
- template);
-
- item.entity.neededSlots = cmpPlayer.TryReservePopulationSlots(item.entity.population * item.entity.count);
- if (item.entity.neededSlots)
- {
- cmpPlayer.BlockTraining();
- return;
- }
this.SetAnimation("training");
-
- cmpPlayer.UnBlockTraining();
-
- Engine.PostMessage(this.entity, MT_TrainingStarted, { "entity": this.entity });
- }
if (item.technology)
- {
- let cmpTechnologyManager = QueryOwnerInterface(this.entity, IID_TechnologyManager);
- if (cmpTechnologyManager)
- cmpTechnologyManager.StartedResearch(item.technology.template, true);
- else
- warn("Failed to start researching " + item.technology.template + ": No TechnologyManager available.");
-
this.SetAnimation("researching");
- }
- item.productionStarted = true;
+ item.Start();
}
-
- if (item.timeRemaining > time)
+ time -= item.Progress(time);
+ if (!item.IsFinished())
{
- item.timeRemaining -= time;
Engine.PostMessage(this.entity, MT_ProductionQueueChanged, null);
return;
}
- if (item.entity)
- {
- let numSpawned = this.SpawnUnits(item);
- if (numSpawned)
- cmpPlayer.UnReservePopulationSlots(item.entity.population * numSpawned);
- if (numSpawned == item.entity.count)
- {
- cmpPlayer.UnBlockTraining();
- delete this.spawnNotified;
- }
- else
- {
- if (numSpawned)
- {
- item.entity.count -= numSpawned;
- Engine.PostMessage(this.entity, MT_ProductionQueueChanged, null);
- }
-
- cmpPlayer.BlockTraining();
-
- if (!this.spawnNotified)
- {
- let cmpGUIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface);
- cmpGUIInterface.PushNotification({
- "players": [cmpPlayer.GetPlayerID()],
- "message": markForTranslation("Can't find free space to spawn trained units"),
- "translateMessage": true
- });
- this.spawnNotified = true;
- }
- return;
- }
- }
- if (item.technology)
- {
- let cmpTechnologyManager = QueryOwnerInterface(this.entity, IID_TechnologyManager);
- if (cmpTechnologyManager)
- cmpTechnologyManager.ResearchTechnology(item.technology.template);
- else
- warn("Failed to finish researching " + item.technology.template + ": No TechnologyManager available.");
-
- const template = TechnologyTemplates.Get(item.technology.template);
- if (template && template.soundComplete)
- {
- let cmpSoundManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_SoundManager);
- if (cmpSoundManager)
- cmpSoundManager.PlaySoundGroup(template.soundComplete, this.entity);
- }
- }
-
- time -= item.timeRemaining;
this.queue.shift();
Engine.PostMessage(this.entity, MT_ProductionQueueChanged, null);
// If autoqueuing, push a new unit on the queue immediately,
// but don't start right away. This 'wastes' some time, making
// autoqueue slightly worse than regular queuing, and also ensures
// that autoqueue doesn't train more than one item per turn,
// if the units would take fewer than ProgressInterval ms to train.
- if (this.autoqueuing && item.entity)
+ if (this.autoqueuing)
{
- if (!this.AddItem(item.entity.template, "unit", item.entity.count, item.metadata))
+ const autoqueueData = item.OriginalItem();
+ if (!autoqueueData)
+ continue;
+
+ if (!this.AddItem(autoqueueData.templateName, "unit", autoqueueData.count, autoqueueData.metadata))
{
this.DisableAutoQueue();
const cmpGUIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface);
cmpGUIInterface.PushNotification({
- "players": [cmpPlayer.GetPlayerID()],
+ "players": [QueryOwnerInterface(this.entity).GetPlayerID()],
"message": markForTranslation("Could not auto-queue unit, de-activating."),
"translateMessage": true
});
}
break;
}
}
if (!this.queue.length)
this.StopTimer();
};
ProductionQueue.prototype.PauseProduction = function()
{
this.StopTimer();
this.paused = true;
- if (this.queue[0])
- this.queue[0].paused = true;
+ this.queue[0]?.Pause();
+ this.StopTimer();
};
ProductionQueue.prototype.UnpauseProduction = function()
{
- if (this.queue[0])
- this.queue[0].paused = false;
+ this.queue[0]?.Unpause();
delete this.paused;
this.StartTimer();
};
ProductionQueue.prototype.StartTimer = function()
{
if (this.timer)
return;
this.timer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer).SetInterval(
this.entity,
IID_ProductionQueue,
"ProgressTimeout",
this.ProgressInterval,
this.ProgressInterval,
null
);
};
ProductionQueue.prototype.StopTimer = function()
{
if (!this.timer)
return;
this.SetAnimation("idle");
Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer).CancelTimer(this.timer);
delete this.timer;
};
-ProductionQueue.prototype.OnValueModification = function(msg)
-{
- // If the promotion requirements of units is changed,
- // update the entities list so that automatically promoted units are shown
- // appropriately in the list.
- if (msg.component != "Promotion" && (msg.component != "ProductionQueue" ||
- !msg.valueNames.some(val => val.startsWith("ProductionQueue/Entities/"))))
- return;
-
- if (msg.entities.indexOf(this.entity) === -1)
- return;
-
- // This also updates the queued production if necessary.
- this.CalculateEntitiesMap();
-
- // Inform the GUI that it'll need to recompute the selection panel.
- // TODO: it would be better to only send the message if something actually changing
- // for the current production queue.
- let cmpPlayer = QueryOwnerInterface(this.entity);
- if (cmpPlayer)
- Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface).SetSelectionDirty(cmpPlayer.GetPlayerID());
-};
-
ProductionQueue.prototype.HasQueuedProduction = function()
{
return this.queue.length > 0;
};
-ProductionQueue.prototype.OnDisabledTemplatesChanged = function(msg)
+ProductionQueue.prototype.OnOwnershipChanged = function(msg)
{
- this.CalculateEntitiesMap();
+ // Reset the production queue whenever the owner changes.
+ // (This should prevent players getting surprised when they capture
+ // an enemy building, and then loads of the enemy's civ's soldiers get
+ // created from it. Also it means we don't have to worry about
+ // updating the reserved pop slots.)
+ this.ResetQueue();
};
ProductionQueue.prototype.OnGarrisonedStateChanged = function(msg)
{
if (msg.holderID != INVALID_ENTITY)
this.PauseProduction();
else
this.UnpauseProduction();
};
Engine.RegisterComponentType(IID_ProductionQueue, "ProductionQueue", ProductionQueue);
Index: ps/trunk/binaries/data/mods/public/simulation/components/Researcher.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/components/Researcher.js (nonexistent)
+++ ps/trunk/binaries/data/mods/public/simulation/components/Researcher.js (revision 26000)
@@ -0,0 +1,447 @@
+function Researcher() {}
+
+Researcher.prototype.Schema =
+ "Allows the entity to research technologies." +
+ "" +
+ "" +
+ "0.5" +
+ "0.1" +
+ "0" +
+ "2" +
+ "" +
+ "" +
+ "" +
+ "\n phase_town_{civ}\n phase_metropolis_ptol\n unlock_shared_los\n wonder_population_cap\n " +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "tokens" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ Resources.BuildSchema("nonNegativeDecimal", ["time"]) +
+ "" +
+ "";
+
+/**
+ * This object represents a technology being researched.
+ */
+Researcher.prototype.Item = function() {};
+
+/**
+ * @param {string} templateName - The name of the template we ought to research.
+ * @param {number} researcher - The entity ID of our researcher.
+ * @param {string} metadata - Optionally any metadata to attach to us.
+ */
+Researcher.prototype.Item.prototype.Init = function(templateName, researcher, metadata)
+{
+ this.templateName = templateName;
+ this.researcher = researcher;
+ this.metadata = metadata;
+};
+
+/**
+ * Prepare for the queue.
+ * @param {Object} techCostMultiplier - The multipliers to use when calculating costs.
+ * @return {boolean} - Whether the item was successfully initiated.
+ */
+Researcher.prototype.Item.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] === undefined ? 1 : techCostMultiplier[res]) * template.cost[res]);
+
+ const cmpPlayer = QueryOwnerInterface(this.researcher);
+
+ // TrySubtractResources should report error to player (they ran out of resources).
+ if (!cmpPlayer?.TrySubtractResources(this.resources))
+ return false;
+ this.player = cmpPlayer.GetPlayerID();
+
+ const time = (techCostMultiplier.time || 1) * (template.researchTime || 0) * 1000;
+ this.timeRemaining = time;
+ this.timeTotal = time;
+
+ // Tell the technology manager that we have queued researching this
+ // such that players can't research the same thing twice.
+ const cmpTechnologyManager = QueryPlayerIDInterface(this.player, IID_TechnologyManager);
+ cmpTechnologyManager.QueuedResearch(this.templateName, this.researcher);
+
+ return true;
+};
+
+Researcher.prototype.Item.prototype.Stop = function()
+{
+ const cmpTechnologyManager = QueryPlayerIDInterface(this.player, IID_TechnologyManager);
+ if (cmpTechnologyManager)
+ cmpTechnologyManager.StoppedResearch(this.templateName, true);
+
+ QueryPlayerIDInterface(this.player)?.RefundResources(this.resources);
+ delete this.resources;
+};
+
+/**
+ * Called when the first work is performed.
+ */
+Researcher.prototype.Item.prototype.Start = function()
+{
+ const cmpTechnologyManager = QueryPlayerIDInterface(this.player, IID_TechnologyManager);
+ cmpTechnologyManager.StartedResearch(this.templateName, true);
+ this.started = true;
+};
+
+Researcher.prototype.Item.prototype.Finish = function()
+{
+ const cmpTechnologyManager = QueryPlayerIDInterface(this.player, IID_TechnologyManager);
+ cmpTechnologyManager.ResearchTechnology(this.templateName);
+
+ const template = TechnologyTemplates.Get(this.templateName);
+ if (template?.soundComplete)
+ Engine.QueryInterface(SYSTEM_ENTITY, IID_SoundManager)?.PlaySoundGroup(template.soundComplete, this.researcher);
+ this.finished = true;
+};
+
+/**
+ * @param {number} allocatedTime - The time allocated to this item.
+ * @return {number} - The time used for this item.
+ */
+Researcher.prototype.Item.prototype.Progress = function(allocatedTime)
+{
+ if (!this.started)
+ this.Start();
+
+ if (this.timeRemaining > allocatedTime)
+ {
+ this.timeRemaining -= allocatedTime;
+ return allocatedTime;
+ }
+ this.Finish();
+ return this.timeRemaining;
+};
+
+Researcher.prototype.Item.prototype.Pause = function()
+{
+ this.paused = true;
+};
+
+Researcher.prototype.Item.prototype.Unpause = function()
+{
+ delete this.paused;
+};
+
+/**
+ * @return {Object} - Some basic information of this item.
+ */
+Researcher.prototype.Item.prototype.GetBasicInfo = function()
+{
+ return {
+ "technologyTemplate": this.templateName,
+ "progress": 1 - (this.timeRemaining / this.timeTotal),
+ "timeRemaining": this.timeRemaining,
+ "paused": this.paused,
+ "metadata": this.metadata
+ };
+};
+
+Researcher.prototype.Item.prototype.Serialize = function(id)
+{
+ return {
+ "id": id,
+ "metadata": this.metadata,
+ "paused": this.paused,
+ "player": this.player,
+ "researcher": this.researcher,
+ "resource": this.resources,
+ "started": this.started,
+ "templateName": this.templateName,
+ "timeRemaining": this.timeRemaining,
+ "timeTotal": this.timeTotal,
+ };
+};
+
+Researcher.prototype.Item.prototype.Deserialize = function(data)
+{
+ this.Init(data.templateName, data.researcher, data.metadata);
+
+ this.paused = data.paused;
+ this.player = data.player;
+ this.researcher = data.researcher;
+ this.resources = data.resources;
+ this.started = data.started;
+ this.timeRemaining = data.timeRemaining;
+ this.timeTotal = data.timeTotal;
+};
+
+Researcher.prototype.Init = function()
+{
+ this.nextID = 1;
+ this.queue = new Map();
+};
+
+Researcher.prototype.Serialize = function()
+{
+ const queue = [];
+ for (const [id, item] of this.queue)
+ queue.push(item.Serialize(id));
+
+ return {
+ "nextID": this.nextID,
+ "queue": queue
+ };
+};
+
+Researcher.prototype.Deserialize = function(data)
+{
+ this.Init();
+ this.nextID = data.nextID;
+ for (const item of data.queue)
+ {
+ const newItem = new this.Item();
+ newItem.Deserialize(item);
+ this.queue.set(item.id, newItem);
+ }
+};
+
+/*
+ * Returns list of technologies that can be researched by this entity.
+ */
+Researcher.prototype.GetTechnologiesList = function()
+{
+ if (!this.template.Technologies)
+ return [];
+
+ let string = this.template.Technologies._string;
+ string = ApplyValueModificationsToEntity("Researcher/Technologies/_string", string, this.entity);
+
+ if (!string)
+ return [];
+
+ const cmpTechnologyManager = QueryOwnerInterface(this.entity, IID_TechnologyManager);
+ if (!cmpTechnologyManager)
+ return [];
+
+ const cmpPlayer = QueryOwnerInterface(this.entity);
+ if (!cmpPlayer)
+ return [];
+
+ let techs = string.split(/\s+/);
+
+ // Replace the civ specific technologies.
+ for (let i = 0; i < techs.length; ++i)
+ {
+ const tech = techs[i];
+ if (tech.indexOf("{civ}") == -1)
+ continue;
+ const civTech = tech.replace("{civ}", cmpPlayer.GetCiv());
+ techs[i] = TechnologyTemplates.Has(civTech) ? civTech : tech.replace("{civ}", "generic");
+ }
+
+ // Remove any technologies that can't be researched by this civ.
+ techs = techs.filter(tech =>
+ cmpTechnologyManager.CheckTechnologyRequirements(
+ DeriveTechnologyRequirements(TechnologyTemplates.Get(tech), cmpPlayer.GetCiv()),
+ true));
+
+ const techList = [];
+ const superseded = {};
+
+ const disabledTechnologies = cmpPlayer.GetDisabledTechnologies();
+
+ // Add any top level technologies to an array which corresponds to the displayed icons.
+ // Also store what technology is superseded in the superseded object { "tech1":"techWhichSupercedesTech1", ... }.
+ for (const tech of techs)
+ {
+ if (disabledTechnologies && disabledTechnologies[tech])
+ continue;
+
+ const template = TechnologyTemplates.Get(tech);
+ if (!template.supersedes || techs.indexOf(template.supersedes) === -1)
+ techList.push(tech);
+ else
+ superseded[template.supersedes] = tech;
+ }
+
+ // Now make researched/in progress techs invisible.
+ for (const i in techList)
+ {
+ let tech = techList[i];
+ while (this.IsTechnologyResearchedOrInProgress(tech))
+ tech = superseded[tech];
+
+ techList[i] = tech;
+ }
+
+ const ret = [];
+
+ // This inserts the techs into the correct positions to line up the technology pairs.
+ for (let i = 0; i < techList.length; ++i)
+ {
+ const tech = techList[i];
+ if (!tech)
+ {
+ ret[i] = undefined;
+ continue;
+ }
+
+ const template = TechnologyTemplates.Get(tech);
+ if (template.top)
+ ret[i] = { "pair": true, "top": template.top, "bottom": template.bottom };
+ else
+ ret[i] = tech;
+ }
+
+ return ret;
+};
+
+/**
+ * @return {Object} - The multipliers to change the costs of any research with.
+ */
+Researcher.prototype.GetTechCostMultiplier = function()
+{
+ const techCostMultiplier = {};
+ for (const res in this.template.TechCostMultiplier)
+ techCostMultiplier[res] = ApplyValueModificationsToEntity(
+ "Researcher/TechCostMultiplier/" + res,
+ +this.template.TechCostMultiplier[res],
+ this.entity);
+
+ return techCostMultiplier;
+};
+
+/**
+ * Checks whether we can research the given technology, minding paired techs.
+ */
+Researcher.prototype.IsTechnologyResearchedOrInProgress = function(tech)
+{
+ if (!tech)
+ return false;
+
+ const cmpTechnologyManager = QueryOwnerInterface(this.entity, IID_TechnologyManager);
+ if (!cmpTechnologyManager)
+ return false;
+
+ const template = TechnologyTemplates.Get(tech);
+ if (template.top)
+ return cmpTechnologyManager.IsTechnologyResearched(template.top) ||
+ cmpTechnologyManager.IsInProgress(template.top) ||
+ cmpTechnologyManager.IsTechnologyResearched(template.bottom) ||
+ cmpTechnologyManager.IsInProgress(template.bottom);
+
+ return cmpTechnologyManager.IsTechnologyResearched(tech) || cmpTechnologyManager.IsInProgress(tech);
+};
+
+/**
+ * @param {string} templateName - The technology to queue.
+ * @param {string} metadata - Any metadata attached to the item.
+ * @return {number} - The ID of the item. -1 if the item could not be researched.
+ */
+Researcher.prototype.QueueTechnology = function(templateName, metadata)
+{
+ if (!this.GetTechnologiesList().some(tech =>
+ tech && (tech == templateName ||
+ tech.pair && (tech.top == templateName || tech.bottom == templateName))))
+ {
+ error("This entity cannot research " + templateName + ".");
+ return -1;
+ }
+
+ const item = new this.Item();
+ item.Init(templateName, this.entity, metadata);
+
+ const techCostMultiplier = this.GetTechCostMultiplier();
+ if (!item.Queue(techCostMultiplier))
+ return -1;
+
+ const id = this.nextID++;
+ this.queue.set(id, item);
+ return id;
+};
+
+/**
+ * @param {number} id - The id of the technology researched here we need to stop.
+ */
+Researcher.prototype.StopResearching = function(id)
+{
+ this.queue.get(id).Stop();
+ this.queue.delete(id);
+};
+
+/**
+ * @param {number} id - The id of the technology.
+ */
+Researcher.prototype.PauseTechnology = function(id)
+{
+ this.queue.get(id).Pause();
+};
+
+/**
+ * @param {number} id - The id of the technology.
+ */
+Researcher.prototype.UnpauseTechnology = function(id)
+{
+ this.queue.get(id).Unpause();
+};
+
+/**
+ * @param {number} id - The ID of the item to check.
+ * @return {boolean} - Whether we are currently training the item.
+ */
+Researcher.prototype.HasItem = function(id)
+{
+ return this.queue.has(id);
+};
+
+/**
+ * @parameter {number} id - The id of the research.
+ * @return {Object} - Some basic information about the research.
+ */
+Researcher.prototype.GetResearchingTechnology = function(id)
+{
+ return this.queue.get(id).GetBasicInfo();
+};
+
+/**
+ * @parameter {string} technologyName - The name of the research.
+ * @return {Object} - Some basic information about the research.
+ */
+Researcher.prototype.GetResearchingTechnologyByName = function(technologyName)
+{
+ let techID;
+ for (const [id, value] of this.queue)
+ if (value.templateName === technologyName)
+ {
+ techID = id;
+ break;
+ }
+ if (!techID)
+ return undefined;
+
+ return this.GetResearchingTechnology(techID);
+};
+
+/**
+ * @param {number} id - The ID of the item we spent time on.
+ * @param {number} allocatedTime - The time we spent on the given item.
+ * @return {number} - The time we've actually used.
+ */
+Researcher.prototype.Progress = function(id, allocatedTime)
+{
+ const item = this.queue.get(id);
+ const usedTime = item.Progress(allocatedTime);
+ if (item.finished)
+ this.queue.delete(id);
+ return usedTime;
+};
+
+Engine.RegisterComponentType(IID_Researcher, "Researcher", Researcher);
Property changes on: ps/trunk/binaries/data/mods/public/simulation/components/Researcher.js
___________________________________________________________________
Added: svn:eol-style
## -0,0 +1 ##
+native
\ No newline at end of property
Added: svn:mime-type
## -0,0 +1 ##
+text/plain
\ No newline at end of property
Index: ps/trunk/binaries/data/mods/public/simulation/components/TechnologyManager.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/components/TechnologyManager.js (revision 25999)
+++ ps/trunk/binaries/data/mods/public/simulation/components/TechnologyManager.js (revision 26000)
@@ -1,376 +1,387 @@
function TechnologyManager() {}
TechnologyManager.prototype.Schema =
"";
TechnologyManager.prototype.Init = function()
{
// Holds names of technologies that have been researched.
this.researchedTechs = new Set();
// Maps from technolgy name to the entityID of the researcher.
this.researchQueued = new Map();
// Holds technologies which are being researched currently (non-queued).
this.researchStarted = new Set();
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.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 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);
};
TechnologyManager.prototype.IsTechnologyStarted = function(tech)
{
return this.researchStarted.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_Player).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];
}
}
}
}
};
/**
* Marks a technology as researched.
* Note that this does not verify that the requirements are met.
*
* @param {string} tech - The technology to mark as researched.
*/
TechnologyManager.prototype.ResearchTechnology = function(tech)
{
this.StoppedResearch(tech, false);
let modifiedComponents = {};
this.researchedTechs.add(tech);
// Store the modifications in an easy to access structure.
let template = TechnologyTemplates.Get(tech);
if (template.modifications)
{
let cmpModifiersManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ModifiersManager);
cmpModifiersManager.AddModifiers("tech/" + tech, DeriveModificationsFromTech(template), this.entity);
}
if (template.replaces && template.replaces.length > 0)
{
for (let i of template.replaces)
{
if (!i || this.IsTechnologyResearched(i))
continue;
this.researchedTechs.add(i);
// Change the EntityLimit if any.
let cmpPlayer = Engine.QueryInterface(this.entity, IID_Player);
if (cmpPlayer && cmpPlayer.GetPlayerID() !== undefined)
{
let playerID = cmpPlayer.GetPlayerID();
let cmpPlayerEntityLimits = QueryPlayerIDInterface(playerID, IID_EntityLimits);
if (cmpPlayerEntityLimits)
cmpPlayerEntityLimits.UpdateLimitsFromTech(i);
}
}
}
this.UpdateAutoResearch();
let cmpPlayer = Engine.QueryInterface(this.entity, IID_Player);
if (!cmpPlayer || cmpPlayer.GetPlayerID() === undefined)
return;
let playerID = cmpPlayer.GetPlayerID();
// Change the EntityLimit if any.
let cmpPlayerEntityLimits = QueryPlayerIDInterface(playerID, IID_EntityLimits);
if (cmpPlayerEntityLimits)
cmpPlayerEntityLimits.UpdateLimitsFromTech(tech);
// Always send research finished message.
Engine.PostMessage(this.entity, MT_ResearchFinished, { "player": playerID, "tech": tech });
if (tech.startsWith("phase") && !template.autoResearch)
{
let cmpGUIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface);
cmpGUIInterface.PushNotification({
"type": "phase",
"players": [playerID],
"phaseName": tech,
"phaseState": "completed"
});
}
};
/**
* Marks a technology as being queued for research at the given entityID.
*/
TechnologyManager.prototype.QueuedResearch = function(tech, researcher)
{
this.researchQueued.set(tech, researcher);
+
+ const cmpPlayer = Engine.QueryInterface(this.entity, IID_Player);
+ if (!cmpPlayer)
+ return;
+ const playerID = cmpPlayer.GetPlayerID();
+
+ Engine.QueryInterface(SYSTEM_ENTITY, IID_Trigger).CallEvent("OnResearchQueued", {
+ "playerid": playerID,
+ "technologyTemplate": tech,
+ "researcherEntity": researcher
+ });
};
// Marks a technology as actively being researched
TechnologyManager.prototype.StartedResearch = function(tech, notification)
{
this.researchStarted.add(tech);
if (notification && tech.startsWith("phase"))
{
let cmpPlayer = Engine.QueryInterface(this.entity, IID_Player);
let cmpGuiInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface);
cmpGuiInterface.PushNotification({
"type": "phase",
"players": [cmpPlayer.GetPlayerID()],
"phaseName": tech,
"phaseState": "started"
});
}
};
/**
* Marks a technology as not being currently researched and optionally sends a GUI notification.
*/
TechnologyManager.prototype.StoppedResearch = function(tech, notification)
{
if (notification && tech.startsWith("phase") && this.researchStarted.has(tech))
{
let cmpPlayer = Engine.QueryInterface(this.entity, IID_Player);
let cmpGUIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface);
cmpGUIInterface.PushNotification({
"type": "phase",
"players": [cmpPlayer.GetPlayerID()],
"phaseName": tech,
"phaseState": "aborted"
});
}
this.researchQueued.delete(tech);
this.researchStarted.delete(tech);
};
/**
* Checks whether a technology is set to be researched.
*/
TechnologyManager.prototype.IsInProgress = function(tech)
{
return this.researchQueued.has(tech);
};
/**
* Returns the names of technologies that are currently being researched (non-queued).
*/
TechnologyManager.prototype.GetStartedTechs = function()
{
return this.researchStarted;
};
/**
* Gets the entity currently researching the technology.
*/
TechnologyManager.prototype.GetResearcher = function(tech)
{
return this.researchQueued.get(tech);
};
/**
* 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/Trainer.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/components/Trainer.js (nonexistent)
+++ ps/trunk/binaries/data/mods/public/simulation/components/Trainer.js (revision 26000)
@@ -0,0 +1,727 @@
+function Trainer() {}
+
+Trainer.prototype.Schema =
+ "Allows the entity to train new units." +
+ "" +
+ "0.7" +
+ "" +
+ "\n units/{civ}/support_female_citizen\n units/{native}/support_trader\n units/athen/infantry_spearman_b\n " +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "tokens" +
+ "" +
+ "" +
+ "" +
+ "";
+
+/**
+ * This object represents a batch of entities being trained.
+ */
+Trainer.prototype.Item = function() {};
+
+/**
+ * @param {string} templateName - The name of the template we ought to train.
+ * @param {number} count - The size of the batch to train.
+ * @param {number} trainer - The entity ID of our trainer.
+ * @param {string} metadata - Optionally any metadata to attach to us.
+ */
+Trainer.prototype.Item.prototype.Init = function(templateName, count, trainer, metadata)
+{
+ this.count = count;
+ this.templateName = templateName;
+ this.trainer = trainer;
+ this.metadata = metadata;
+};
+
+/**
+ * Prepare for the queue.
+ * @param {Object} trainCostMultiplier - The multipliers to use when calculating costs.
+ * @param {number} batchTimeMultiplier - The factor to use when training this batches.
+ *
+ * @return {boolean} - Whether the item was successfully initiated.
+ */
+Trainer.prototype.Item.prototype.Queue = function(trainCostMultiplier, batchTimeMultiplier)
+{
+ if (!Number.isInteger(this.count) || this.count <= 0)
+ {
+ error("Invalid batch count " + this.count + ".");
+ return false;
+ }
+ const cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager);
+ const template = cmpTemplateManager.GetTemplate(this.templateName);
+ if (!template)
+ return false;
+
+ const cmpPlayer = QueryOwnerInterface(this.trainer);
+ if (!cmpPlayer)
+ return false;
+ this.player = cmpPlayer.GetPlayerID();
+
+ this.resources = {};
+ const totalResources = {};
+
+ for (const res in template.Cost.Resources)
+ {
+ this.resources[res] = (trainCostMultiplier[res] === undefined ? 1 : trainCostMultiplier[res]) *
+ ApplyValueModificationsToTemplate(
+ "Cost/Resources/" + res,
+ +template.Cost.Resources[res],
+ this.player,
+ template);
+
+ totalResources[res] = Math.floor(this.count * this.resources[res]);
+ }
+ // TrySubtractResources should report error to player (they ran out of resources).
+ if (!cmpPlayer.TrySubtractResources(totalResources))
+ return false;
+
+ this.population = ApplyValueModificationsToTemplate("Cost/Population", +template.Cost.Population, this.player, template);
+
+ if (template.TrainingRestrictions)
+ {
+ const unitCategory = template.TrainingRestrictions.Category;
+ const cmpPlayerEntityLimits = QueryPlayerIDInterface(this.player, IID_EntityLimits);
+ if (cmpPlayerEntityLimits)
+ {
+ if (!cmpPlayerEntityLimits.AllowedToTrain(unitCategory, this.count, this.templateName, template.TrainingRestrictions.MatchLimit))
+ // Already warned, return.
+ {
+ cmpPlayer.RefundResources(totalResources);
+ return false;
+ }
+ // ToDo: Should warn here v and return?
+ cmpPlayerEntityLimits.ChangeCount(unitCategory, this.count);
+ if (template.TrainingRestrictions.MatchLimit)
+ cmpPlayerEntityLimits.ChangeMatchCount(this.templateName, this.count);
+ }
+ }
+
+ const buildTime = ApplyValueModificationsToTemplate("Cost/BuildTime", +template.Cost.BuildTime, this.player, template);
+
+ const time = batchTimeMultiplier * (trainCostMultiplier.time || 1) * buildTime * 1000;
+ this.timeRemaining = time;
+ this.timeTotal = time;
+
+ const cmpTrigger = Engine.QueryInterface(SYSTEM_ENTITY, IID_Trigger);
+ cmpTrigger.CallEvent("OnTrainingQueued", {
+ "playerid": this.player,
+ "unitTemplate": this.templateName,
+ "count": this.count,
+ "metadata": this.metadata,
+ "trainerEntity": this.trainer
+ });
+
+ return true;
+};
+
+/**
+ * Destroy cached entities, refund resources and free (population) limits.
+ */
+Trainer.prototype.Item.prototype.Stop = function()
+{
+ // Destroy any cached entities (those which didn't spawn for some reason).
+ if (this.entities?.length)
+ {
+ for (const ent of this.entities)
+ Engine.DestroyEntity(ent);
+
+ delete this.entities;
+ }
+
+ const cmpPlayer = QueryPlayerIDInterface(this.player);
+
+ const cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager);
+ const template = cmpTemplateManager.GetTemplate(this.templateName);
+ if (template.TrainingRestrictions)
+ {
+ const cmpPlayerEntityLimits = QueryPlayerIDInterface(this.player, IID_EntityLimits);
+ if (cmpPlayerEntityLimits)
+ cmpPlayerEntityLimits.ChangeCount(template.TrainingRestrictions.Category, -this.count);
+ if (template.TrainingRestrictions.MatchLimit)
+ cmpPlayerEntityLimits.ChangeMatchCount(this.templateName, -this.count);
+ }
+
+ const cmpStatisticsTracker = QueryPlayerIDInterface(this.player, IID_StatisticsTracker);
+ const totalCosts = {};
+ for (const resource in this.resources)
+ {
+ totalCosts[resource] = Math.floor(this.count * this.resources[resource]);
+ if (cmpStatisticsTracker)
+ cmpStatisticsTracker.IncreaseResourceUsedCounter(resource, -totalCosts[resource]);
+ }
+
+ if (cmpPlayer)
+ {
+ if (this.started)
+ cmpPlayer.UnReservePopulationSlots(this.population * this.count);
+ cmpPlayer.RefundResources(totalCosts);
+ cmpPlayer.UnBlockTraining();
+ }
+
+ delete this.resources;
+};
+
+/**
+ * This starts the item, reserving population.
+ * @return {boolean} - Whether the item was started successfully.
+ */
+Trainer.prototype.Item.prototype.Start = function()
+{
+ const cmpPlayer = QueryPlayerIDInterface(this.player);
+ if (!cmpPlayer)
+ return false;
+
+ const template = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager).GetTemplate(this.templateName);
+ this.population = ApplyValueModificationsToTemplate(
+ "Cost/Population",
+ +template.Cost.Population,
+ this.player,
+ template);
+
+ this.missingPopSpace = cmpPlayer.TryReservePopulationSlots(this.population * this.count);
+ if (this.missingPopSpace)
+ {
+ cmpPlayer.BlockTraining();
+ return false;
+ }
+ cmpPlayer.UnBlockTraining();
+
+ Engine.PostMessage(this.trainer, MT_TrainingStarted, { "entity": this.trainer });
+
+ this.started = true;
+ return true;
+};
+
+Trainer.prototype.Item.prototype.Finish = function()
+{
+ this.Spawn();
+ if (!this.count)
+ this.finished = true;
+};
+
+/*
+ * This function creates the entities and places them in world if possible
+ * (some of these entities may be garrisoned directly if autogarrison, the others are spawned).
+ */
+Trainer.prototype.Item.prototype.Spawn = function()
+{
+ const createdEnts = [];
+ const spawnedEnts = [];
+
+ // We need entities to test spawning, but we don't want to waste resources,
+ // so only create them once and use as needed.
+ if (!this.entities)
+ {
+ this.entities = [];
+ for (let i = 0; i < this.count; ++i)
+ this.entities.push(Engine.AddEntity(this.templateName));
+ }
+
+ let autoGarrison;
+ const cmpRallyPoint = Engine.QueryInterface(this.trainer, IID_RallyPoint);
+ if (cmpRallyPoint)
+ {
+ const data = cmpRallyPoint.GetData()[0];
+ if (data?.target && data.target == this.trainer && data.command == "garrison")
+ autoGarrison = true;
+ }
+
+ const cmpFootprint = Engine.QueryInterface(this.trainer, IID_Footprint);
+ const cmpPosition = Engine.QueryInterface(this.trainer, IID_Position);
+ const positionTrainer = cmpPosition && cmpPosition.GetPosition();
+
+ const cmpPlayerEntityLimits = QueryPlayerIDInterface(this.player, IID_EntityLimits);
+ const cmpPlayerStatisticsTracker = QueryPlayerIDInterface(this.player, IID_StatisticsTracker);
+ while (this.entities.length)
+ {
+ const ent = this.entities[0];
+ const cmpNewOwnership = Engine.QueryInterface(ent, IID_Ownership);
+ let garrisoned = false;
+
+ if (autoGarrison)
+ {
+ const cmpGarrisonable = Engine.QueryInterface(ent, IID_Garrisonable);
+ if (cmpGarrisonable)
+ {
+ // Temporary owner affectation needed for GarrisonHolder checks.
+ cmpNewOwnership.SetOwnerQuiet(this.player);
+ garrisoned = cmpGarrisonable.Garrison(this.trainer);
+ cmpNewOwnership.SetOwnerQuiet(INVALID_PLAYER);
+ }
+ }
+
+ if (!garrisoned)
+ {
+ const pos = cmpFootprint.PickSpawnPoint(ent);
+ if (pos.y < 0)
+ break;
+
+ const cmpNewPosition = Engine.QueryInterface(ent, IID_Position);
+ cmpNewPosition.JumpTo(pos.x, pos.z);
+
+ if (positionTrainer)
+ cmpNewPosition.SetYRotation(positionTrainer.horizAngleTo(pos));
+
+ spawnedEnts.push(ent);
+ }
+
+ // Decrement entity count in the EntityLimits component
+ // since it will be increased by EntityLimits.OnGlobalOwnershipChanged,
+ // i.e. we replace a 'trained' entity by 'alive' one.
+ // Must be done after spawn check so EntityLimits decrements only if unit spawns.
+ if (cmpPlayerEntityLimits)
+ {
+ const cmpTrainingRestrictions = Engine.QueryInterface(ent, IID_TrainingRestrictions);
+ if (cmpTrainingRestrictions)
+ cmpPlayerEntityLimits.ChangeCount(cmpTrainingRestrictions.GetCategory(), -1);
+ }
+ cmpNewOwnership.SetOwner(this.player);
+
+ if (cmpPlayerStatisticsTracker)
+ cmpPlayerStatisticsTracker.IncreaseTrainedUnitsCounter(ent);
+
+ this.count--;
+ this.entities.shift();
+ createdEnts.push(ent);
+ }
+
+ if (spawnedEnts.length && !autoGarrison && cmpRallyPoint)
+ for (const com of GetRallyPointCommands(cmpRallyPoint, spawnedEnts))
+ ProcessCommand(this.player, com);
+
+ const cmpPlayer = QueryOwnerInterface(this.trainer);
+ if (createdEnts.length)
+ {
+ if (this.population)
+ cmpPlayer.UnReservePopulationSlots(this.population * createdEnts.length);
+ // Play a sound, but only for the first in the batch (to avoid nasty phasing effects).
+ PlaySound("trained", createdEnts[0]);
+ Engine.PostMessage(this.trainer, MT_TrainingFinished, {
+ "entities": createdEnts,
+ "owner": this.player,
+ "metadata": this.metadata
+ });
+ }
+ if (this.count)
+ {
+ cmpPlayer.BlockTraining();
+
+ if (!this.spawnNotified)
+ {
+ Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface).PushNotification({
+ "players": [cmpPlayer.GetPlayerID()],
+ "message": markForTranslation("Can't find free space to spawn trained units."),
+ "translateMessage": true
+ });
+ this.spawnNotified = true;
+ }
+ }
+ else
+ {
+ cmpPlayer.UnBlockTraining();
+ delete this.spawnNotified;
+ }
+};
+
+/**
+ * @param {number} allocatedTime - The time allocated to this item.
+ * @return {number} - The time used for this item.
+ */
+Trainer.prototype.Item.prototype.Progress = function(allocatedTime)
+{
+ // We couldn't start this timeout, try again later.
+ if (!this.started && !this.Start())
+ return allocatedTime;
+
+ if (this.timeRemaining > allocatedTime)
+ {
+ this.timeRemaining -= allocatedTime;
+ return allocatedTime;
+ }
+ this.Finish();
+ return this.timeRemaining;
+};
+
+Trainer.prototype.Item.prototype.Pause = function()
+{
+ this.paused = true;
+};
+
+Trainer.prototype.Item.prototype.Unpause = function()
+{
+ delete this.paused;
+};
+
+/**
+ * @return {Object} - Some basic information of this batch.
+ */
+Trainer.prototype.Item.prototype.GetBasicInfo = function()
+{
+ return {
+ "unitTemplate": this.templateName,
+ "count": this.count,
+ "neededSlots": this.missingPopSpace,
+ "progress": 1 - (this.timeRemaining / this.timeTotal),
+ "timeRemaining": this.timeRemaining,
+ "paused": this.paused,
+ "metadata": this.metadata
+ };
+};
+
+Trainer.prototype.Item.prototype.Serialize = function(id)
+{
+ return {
+ "id": id,
+ "count": this.count,
+ "entities": this.entities,
+ "metadata": this.metadata,
+ "missingPopSpace": this.missingPopSpace,
+ "paused": this.paused,
+ "player": this.player,
+ "trainer": this.trainer,
+ "resource": this.resources,
+ "started": this.started,
+ "templateName": this.templateName,
+ "timeRemaining": this.timeRemaining,
+ "timeTotal": this.timeTotal,
+ };
+};
+
+Trainer.prototype.Item.prototype.Deserialize = function(data)
+{
+ this.Init(data.templateName, data.count, data.trainer, data.metadata);
+
+ this.entities = data.entities;
+ this.missingPopSpace = data.missingPopSpace;
+ this.paused = data.paused;
+ this.player = data.player;
+ this.trainer = data.trainer;
+ this.resources = data.resources;
+ this.started = data.started;
+ this.timeRemaining = data.timeRemaining;
+ this.timeTotal = data.timeTotal;
+};
+
+Trainer.prototype.Init = function()
+{
+ this.nextID = 1;
+ this.queue = new Map();
+};
+
+Trainer.prototype.Serialize = function()
+{
+ const queue = [];
+ for (const [id, item] of this.queue)
+ queue.push(item.Serialize(id));
+
+ return {
+ "entitiesMap": this.entitiesMap,
+ "nextID": this.nextID,
+ "queue": queue
+ };
+};
+
+Trainer.prototype.Deserialize = function(data)
+{
+ this.Init();
+ this.entitiesMap = data.entitiesMap;
+ this.nextID = data.nextID;
+ for (const item of data.queue)
+ {
+ const newItem = new this.Item();
+ newItem.Deserialize(item);
+ this.queue.set(item.id, newItem);
+ }
+};
+
+/*
+ * Returns list of entities that can be trained by this entity.
+ */
+Trainer.prototype.GetEntitiesList = function()
+{
+ return Array.from(this.entitiesMap.values());
+};
+
+/**
+ * Calculate the new list of producible entities
+ * and update any entities currently being produced.
+ */
+Trainer.prototype.CalculateEntitiesMap = function()
+{
+ // Don't reset the map, it's used below to update entities.
+ if (!this.entitiesMap)
+ this.entitiesMap = new Map();
+ if (!this.template.Entities)
+ return;
+
+ const string = this.template.Entities._string;
+ // Tokens can be added -> process an empty list to get them.
+ let addedTokens = ApplyValueModificationsToEntity("Trainer/Entities/_string", "", this.entity);
+ if (!addedTokens && !string)
+ return;
+
+ addedTokens = addedTokens == "" ? [] : addedTokens.split(/\s+/);
+
+ const cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager);
+ const cmpPlayer = QueryOwnerInterface(this.entity);
+
+ const disabledEntities = cmpPlayer ? cmpPlayer.GetDisabledTemplates() : {};
+
+ /**
+ * Process tokens:
+ * - process token modifiers (this is a bit tricky).
+ * - replace the "{civ}" and "{native}" codes with the owner's civ ID and entity's civ ID
+ * - remove disabled entities
+ * - upgrade templates where necessary
+ * This also updates currently queued production (it's more convenient to do it here).
+ */
+
+ const removeAllQueuedTemplate = (token) => {
+ const queue = clone(this.queue);
+ const template = this.entitiesMap.get(token);
+ for (const [id, item] of queue)
+ if (item.templateName == template)
+ this.StopBatch(id);
+ };
+
+ // ToDo: Notice this doesn't account for entity limits changing due to the template change.
+ const updateAllQueuedTemplate = (token, updateTo) => {
+ const template = this.entitiesMap.get(token);
+ for (const [id, item] of this.queue)
+ if (item.templateName === template)
+ item.templateName = updateTo;
+ };
+
+ const toks = string.split(/\s+/);
+ for (const tok of addedTokens)
+ toks.push(tok);
+
+ const nativeCiv = Engine.QueryInterface(this.entity, IID_Identity)?.GetCiv();
+ const playerCiv = cmpPlayer?.GetCiv();
+
+ const addedDict = addedTokens.reduce((out, token) => { out[token] = true; return out; }, {});
+ this.entitiesMap = toks.reduce((entMap, token) => {
+ const rawToken = token;
+ if (!(token in addedDict))
+ {
+ // This is a bit wasteful but I can't think of a simpler/better way.
+ // The list of token is unlikely to be a performance bottleneck anyways.
+ token = ApplyValueModificationsToEntity("Trainer/Entities/_string", token, this.entity);
+ token = token.split(/\s+/);
+ if (token.every(tok => addedTokens.indexOf(tok) !== -1))
+ {
+ removeAllQueuedTemplate(rawToken);
+ return entMap;
+ }
+ token = token[0];
+ }
+ // Replace the "{civ}" and "{native}" codes with the owner's civ ID and entity's civ ID.
+ if (nativeCiv)
+ token = token.replace(/\{native\}/g, nativeCiv);
+ if (playerCiv)
+ token = token.replace(/\{civ\}/g, playerCiv);
+
+ // Filter out disabled and invalid entities.
+ if (disabledEntities[token] || !cmpTemplateManager.TemplateExists(token))
+ {
+ removeAllQueuedTemplate(rawToken);
+ return entMap;
+ }
+
+ token = this.GetUpgradedTemplate(token);
+ entMap.set(rawToken, token);
+ updateAllQueuedTemplate(rawToken, token);
+ return entMap;
+ }, new Map());
+};
+
+/*
+ * Returns the upgraded template name if necessary.
+ */
+Trainer.prototype.GetUpgradedTemplate = function(templateName)
+{
+ const cmpPlayer = QueryOwnerInterface(this.entity);
+ if (!cmpPlayer)
+ return templateName;
+
+ const cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager);
+ let template = cmpTemplateManager.GetTemplate(templateName);
+ while (template && template.Promotion !== undefined)
+ {
+ const requiredXp = ApplyValueModificationsToTemplate(
+ "Promotion/RequiredXp",
+ +template.Promotion.RequiredXp,
+ cmpPlayer.GetPlayerID(),
+ template);
+ if (requiredXp > 0)
+ break;
+ templateName = template.Promotion.Entity;
+ template = cmpTemplateManager.GetTemplate(templateName);
+ }
+ return templateName;
+};
+
+/**
+ * @return {Object} - The multipliers to change the costs of any training activity with.
+ */
+Trainer.prototype.GetTrainCostMultiplier = function()
+{
+ const trainCostMultiplier = {};
+ for (const res in this.template.TrainCostMultiplier)
+ trainCostMultiplier[res] = ApplyValueModificationsToEntity(
+ "Trainer/TrainCostMultiplier/" + res,
+ +this.template.TrainCostMultiplier[res],
+ this.entity);
+
+ return trainCostMultiplier;
+};
+
+/*
+ * Returns batch build time.
+ */
+Trainer.prototype.GetBatchTime = function(batchSize)
+{
+ // TODO: work out what equation we should use here.
+ return Math.pow(batchSize, ApplyValueModificationsToEntity(
+ "Trainer/BatchTimeModifier",
+ +(this.template.BatchTimeModifier || 1),
+ this.entity));
+};
+
+/**
+ * @param {string} templateName - The template name to check.
+ * @return {boolean} - Whether we can train this template.
+ */
+Trainer.prototype.CanTrain = function(templateName)
+{
+ return this.GetEntitiesList().includes(templateName);
+};
+
+/**
+ * @param {string} templateName - The entity to queue.
+ * @param {number} count - The batch size.
+ * @param {string} metadata - Any metadata attached to the item.
+ *
+ * @return {number} - The ID of the item. -1 if the item could not be queued.
+ */
+Trainer.prototype.QueueBatch = function(templateName, count, metadata)
+{
+ const item = new this.Item();
+ item.Init(templateName, count, this.entity, metadata);
+
+ const trainCostMultiplier = this.GetTrainCostMultiplier();
+ const batchTimeMultiplier = this.GetBatchTime(count);
+ if (!item.Queue(trainCostMultiplier, batchTimeMultiplier))
+ return -1;
+
+ const id = this.nextID++;
+ this.queue.set(id, item);
+ return id;
+};
+
+/**
+ * @param {number} id - The ID of the batch being trained here we need to stop.
+ */
+Trainer.prototype.StopBatch = function(id)
+{
+ this.queue.get(id).Stop();
+ this.queue.delete(id);
+};
+
+/**
+ * @param {number} id - The ID of the training.
+ */
+Trainer.prototype.PauseBatch = function(id)
+{
+ this.queue.get(id).Pause();
+};
+
+/**
+ * @param {number} id - The ID of the training.
+ */
+Trainer.prototype.UnpauseBatch = function(id)
+{
+ this.queue.get(id).Unpause();
+};
+
+/**
+ * @param {number} id - The ID of the batch to check.
+ * @return {boolean} - Whether we are currently training the batch.
+ */
+Trainer.prototype.HasBatch = function(id)
+{
+ return this.queue.has(id);
+};
+
+/**
+ * @parameter {number} id - The id of the training.
+ * @return {Object} - Some basic information about the training.
+ */
+Trainer.prototype.GetBatch = function(id)
+{
+ const item = this.queue.get(id);
+ return item?.GetBasicInfo();
+};
+
+/**
+ * @param {number} id - The ID of the item we spent time on.
+ * @param {number} allocatedTime - The time we spent on the given item.
+ * @return {number} - The time we've actually used.
+ */
+Trainer.prototype.Progress = function(id, allocatedTime)
+{
+ const item = this.queue.get(id);
+ const usedTime = item.Progress(allocatedTime);
+ if (item.finished)
+ this.queue.delete(id);
+ return usedTime;
+};
+
+Trainer.prototype.OnCivChanged = function()
+{
+ this.CalculateEntitiesMap();
+};
+
+Trainer.prototype.OnOwnershipChanged = function(msg)
+{
+ if (msg.to != INVALID_PLAYER)
+ this.CalculateEntitiesMap();
+};
+
+Trainer.prototype.OnValueModification = function(msg)
+{
+ // If the promotion requirements of units is changed,
+ // update the entities list so that automatically promoted units are shown
+ // appropriately in the list.
+ if (msg.component != "Promotion" && (msg.component != "Trainer" ||
+ !msg.valueNames.some(val => val.startsWith("Trainer/Entities/"))))
+ return;
+
+ if (msg.entities.indexOf(this.entity) === -1)
+ return;
+
+ // This also updates the queued production if necessary.
+ this.CalculateEntitiesMap();
+
+ // Inform the GUI that it'll need to recompute the selection panel.
+ // TODO: it would be better to only send the message if something actually changing
+ // for the current training queue.
+ const cmpPlayer = QueryOwnerInterface(this.entity);
+ if (cmpPlayer)
+ Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface).SetSelectionDirty(cmpPlayer.GetPlayerID());
+};
+
+Trainer.prototype.OnDisabledTemplatesChanged = function(msg)
+{
+ this.CalculateEntitiesMap();
+};
+
+Engine.RegisterComponentType(IID_Trainer, "Trainer", Trainer);
Property changes on: ps/trunk/binaries/data/mods/public/simulation/components/Trainer.js
___________________________________________________________________
Added: svn:eol-style
## -0,0 +1 ##
+native
\ No newline at end of property
Added: svn:mime-type
## -0,0 +1 ##
+text/plain
\ No newline at end of property
Index: ps/trunk/binaries/data/mods/public/simulation/components/interfaces/ProductionQueue.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/components/interfaces/ProductionQueue.js (revision 25999)
+++ ps/trunk/binaries/data/mods/public/simulation/components/interfaces/ProductionQueue.js (revision 26000)
@@ -1,19 +1,7 @@
Engine.RegisterInterface("ProductionQueue");
/**
* Message of the form {}
* sent from ProductionQueue component to the current entity whenever the training queue changes.
*/
Engine.RegisterMessageType("ProductionQueueChanged");
-
-/**
- * Message of the form { "entity": number }
- * sent from ProductionQueue component to the current entity whenever a unit is about to be trained.
- */
-Engine.RegisterMessageType("TrainingStarted");
-
-/**
- * Message of the form { "entities": number[], "owner": number, "metadata": object }
- * sent from ProductionQueue component to the current entity whenever a unit has been trained.
- */
-Engine.RegisterMessageType("TrainingFinished");
Index: ps/trunk/binaries/data/mods/public/simulation/components/interfaces/Researcher.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/components/interfaces/Researcher.js (nonexistent)
+++ ps/trunk/binaries/data/mods/public/simulation/components/interfaces/Researcher.js (revision 26000)
@@ -0,0 +1 @@
+Engine.RegisterInterface("Researcher");
Property changes on: ps/trunk/binaries/data/mods/public/simulation/components/interfaces/Researcher.js
___________________________________________________________________
Added: svn:eol-style
## -0,0 +1 ##
+native
\ No newline at end of property
Added: svn:mime-type
## -0,0 +1 ##
+text/plain
\ No newline at end of property
Index: ps/trunk/binaries/data/mods/public/simulation/components/interfaces/Trainer.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/components/interfaces/Trainer.js (nonexistent)
+++ ps/trunk/binaries/data/mods/public/simulation/components/interfaces/Trainer.js (revision 26000)
@@ -0,0 +1,13 @@
+Engine.RegisterInterface("Trainer");
+
+/**
+ * Message of the form { "entity": number }
+ * sent from Trainer component to the current entity whenever a unit is about to be trained.
+ */
+Engine.RegisterMessageType("TrainingStarted");
+
+/**
+ * Message of the form { "entities": number[], "owner": number, "metadata": object }
+ * sent from Trainer component to the current entity whenever a unit has been trained.
+ */
+Engine.RegisterMessageType("TrainingFinished");
Property changes on: ps/trunk/binaries/data/mods/public/simulation/components/interfaces/Trainer.js
___________________________________________________________________
Added: svn:eol-style
## -0,0 +1 ##
+native
\ No newline at end of property
Added: svn:mime-type
## -0,0 +1 ##
+text/plain
\ No newline at end of property
Index: ps/trunk/binaries/data/mods/public/simulation/components/tests/test_GuiInterface.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/components/tests/test_GuiInterface.js (revision 25999)
+++ ps/trunk/binaries/data/mods/public/simulation/components/tests/test_GuiInterface.js (revision 26000)
@@ -1,617 +1,619 @@
Engine.LoadHelperScript("ObstructionSnap.js");
Engine.LoadHelperScript("Player.js");
Engine.LoadComponentScript("interfaces/AlertRaiser.js");
Engine.LoadComponentScript("interfaces/Auras.js");
Engine.LoadComponentScript("interfaces/Barter.js");
Engine.LoadComponentScript("interfaces/Builder.js");
Engine.LoadComponentScript("interfaces/Capturable.js");
Engine.LoadComponentScript("interfaces/CeasefireManager.js");
-Engine.LoadComponentScript("interfaces/Resistance.js");
Engine.LoadComponentScript("interfaces/DeathDamage.js");
Engine.LoadComponentScript("interfaces/EndGameManager.js");
Engine.LoadComponentScript("interfaces/EntityLimits.js");
Engine.LoadComponentScript("interfaces/Formation.js");
Engine.LoadComponentScript("interfaces/Foundation.js");
Engine.LoadComponentScript("interfaces/Garrisonable.js");
Engine.LoadComponentScript("interfaces/GarrisonHolder.js");
Engine.LoadComponentScript("interfaces/Gate.js");
Engine.LoadComponentScript("interfaces/Guard.js");
Engine.LoadComponentScript("interfaces/Heal.js");
Engine.LoadComponentScript("interfaces/Health.js");
Engine.LoadComponentScript("interfaces/Loot.js");
Engine.LoadComponentScript("interfaces/Market.js");
Engine.LoadComponentScript("interfaces/Pack.js");
Engine.LoadComponentScript("interfaces/Population.js");
Engine.LoadComponentScript("interfaces/ProductionQueue.js");
Engine.LoadComponentScript("interfaces/Promotion.js");
Engine.LoadComponentScript("interfaces/Repairable.js");
+Engine.LoadComponentScript("interfaces/Researcher.js");
+Engine.LoadComponentScript("interfaces/Resistance.js");
Engine.LoadComponentScript("interfaces/ResourceDropsite.js");
Engine.LoadComponentScript("interfaces/ResourceGatherer.js");
Engine.LoadComponentScript("interfaces/ResourceTrickle.js");
Engine.LoadComponentScript("interfaces/ResourceSupply.js");
Engine.LoadComponentScript("interfaces/TechnologyManager.js");
Engine.LoadComponentScript("interfaces/Trader.js");
+Engine.LoadComponentScript("interfaces/Trainer.js");
Engine.LoadComponentScript("interfaces/TurretHolder.js");
Engine.LoadComponentScript("interfaces/Timer.js");
Engine.LoadComponentScript("interfaces/Treasure.js");
Engine.LoadComponentScript("interfaces/TreasureCollector.js");
Engine.LoadComponentScript("interfaces/Turretable.js");
Engine.LoadComponentScript("interfaces/StatisticsTracker.js");
Engine.LoadComponentScript("interfaces/StatusEffectsReceiver.js");
Engine.LoadComponentScript("interfaces/UnitAI.js");
Engine.LoadComponentScript("interfaces/Upgrade.js");
Engine.LoadComponentScript("interfaces/Upkeep.js");
Engine.LoadComponentScript("interfaces/BuildingAI.js");
Engine.LoadComponentScript("GuiInterface.js");
Resources = {
"GetCodes": () => ["food", "metal", "stone", "wood"],
"GetNames": () => ({
"food": "Food",
"metal": "Metal",
"stone": "Stone",
"wood": "Wood"
}),
"GetResource": resource => ({
"aiAnalysisInfluenceGroup":
resource == "food" ? "ignore" :
resource == "wood" ? "abundant" : "sparse"
})
};
var cmp = ConstructComponent(SYSTEM_ENTITY, "GuiInterface");
AddMock(SYSTEM_ENTITY, IID_Barter, {
"GetPrices": function() {
return {
"buy": { "food": 150 },
"sell": { "food": 25 }
};
}
});
AddMock(SYSTEM_ENTITY, IID_EndGameManager, {
"GetVictoryConditions": () => ["conquest", "wonder"],
"GetAlliedVictory": function() { return false; }
});
AddMock(SYSTEM_ENTITY, IID_PlayerManager, {
"GetNumPlayers": function() { return 2; },
"GetPlayerByID": function(id) { TS_ASSERT(id === 0 || id === 1); return 100 + id; },
"GetMaxWorldPopulation": function() {}
});
AddMock(SYSTEM_ENTITY, IID_RangeManager, {
"GetLosVisibility": function(ent, player) { return "visible"; },
"GetLosCircular": function() { return false; }
});
AddMock(SYSTEM_ENTITY, IID_TemplateManager, {
"GetCurrentTemplateName": function(ent) { return "example"; },
"GetTemplate": function(name) { return ""; }
});
AddMock(SYSTEM_ENTITY, IID_Timer, {
"GetTime": function() { return 0; },
"SetTimeout": function(ent, iid, funcname, time, data) { return 0; }
});
AddMock(100, IID_Player, {
"GetName": function() { return "Player 1"; },
"GetCiv": function() { return "gaia"; },
"GetColor": function() { return { "r": 1, "g": 1, "b": 1, "a": 1 }; },
"CanControlAllUnits": function() { return false; },
"GetPopulationCount": function() { return 10; },
"GetPopulationLimit": function() { return 20; },
"GetMaxPopulation": function() { return 200; },
"GetResourceCounts": function() { return { "food": 100 }; },
"GetResourceGatherers": function() { return { "food": 1 }; },
"GetPanelEntities": function() { return []; },
"IsTrainingBlocked": function() { return false; },
"GetState": function() { return "active"; },
"GetTeam": function() { return -1; },
"GetLockTeams": function() { return false; },
"GetCheatsEnabled": function() { return false; },
"GetDiplomacy": function() { return [-1, 1]; },
"IsAlly": function() { return false; },
"IsMutualAlly": function() { return false; },
"IsNeutral": function() { return false; },
"IsEnemy": function() { return true; },
"GetDisabledTemplates": function() { return {}; },
"GetDisabledTechnologies": function() { return {}; },
"CanBarter": function() { return false; },
"GetSpyCostMultiplier": function() { return 1; },
"HasSharedDropsites": function() { return false; },
"HasSharedLos": function() { return false; }
});
AddMock(100, IID_EntityLimits, {
"GetLimits": function() { return { "Foo": 10 }; },
"GetCounts": function() { return { "Foo": 5 }; },
"GetLimitChangers": function() { return { "Foo": {} }; },
"GetMatchCounts": function() { return { "Bar": 0 }; }
});
AddMock(100, IID_TechnologyManager, {
"IsTechnologyResearched": tech => tech == "phase_village",
"GetQueuedResearch": () => new Map(),
"GetStartedTechs": () => new Set(),
"GetResearchedTechs": () => new Set(),
"GetClassCounts": () => ({}),
"GetTypeCountsByClass": () => ({})
});
AddMock(100, IID_StatisticsTracker, {
"GetBasicStatistics": function() {
return {
"resourcesGathered": {
"food": 100,
"wood": 0,
"metal": 0,
"stone": 0,
"vegetarianFood": 0
},
"percentMapExplored": 10
};
},
"GetSequences": function() {
return {
"unitsTrained": [0, 10],
"unitsLost": [0, 42],
"buildingsConstructed": [1, 3],
"buildingsCaptured": [3, 7],
"buildingsLost": [3, 10],
"civCentresBuilt": [4, 10],
"resourcesGathered": {
"food": [5, 100],
"wood": [0, 0],
"metal": [0, 0],
"stone": [0, 0],
"vegetarianFood": [0, 0]
},
"treasuresCollected": [1, 20],
"lootCollected": [0, 2],
"percentMapExplored": [0, 10],
"teamPercentMapExplored": [0, 10],
"percentMapControlled": [0, 10],
"teamPercentMapControlled": [0, 10],
"peakPercentOfMapControlled": [0, 10],
"teamPeakPercentOfMapControlled": [0, 10]
};
},
"IncreaseTrainedUnitsCounter": function() { return 1; },
"IncreaseConstructedBuildingsCounter": function() { return 1; },
"IncreaseBuiltCivCentresCounter": function() { return 1; }
});
AddMock(101, IID_Player, {
"GetName": function() { return "Player 2"; },
"GetCiv": function() { return "mace"; },
"GetColor": function() { return { "r": 1, "g": 0, "b": 0, "a": 1 }; },
"CanControlAllUnits": function() { return true; },
"GetPopulationCount": function() { return 40; },
"GetPopulationLimit": function() { return 30; },
"GetMaxPopulation": function() { return 300; },
"GetResourceCounts": function() { return { "food": 200 }; },
"GetResourceGatherers": function() { return { "food": 3 }; },
"GetPanelEntities": function() { return []; },
"IsTrainingBlocked": function() { return false; },
"GetState": function() { return "active"; },
"GetTeam": function() { return -1; },
"GetLockTeams": function() {return false; },
"GetCheatsEnabled": function() { return false; },
"GetDiplomacy": function() { return [-1, 1]; },
"IsAlly": function() { return true; },
"IsMutualAlly": function() {return false; },
"IsNeutral": function() { return false; },
"IsEnemy": function() { return false; },
"GetDisabledTemplates": function() { return {}; },
"GetDisabledTechnologies": function() { return {}; },
"CanBarter": function() { return false; },
"GetSpyCostMultiplier": function() { return 1; },
"HasSharedDropsites": function() { return false; },
"HasSharedLos": function() { return false; }
});
AddMock(101, IID_EntityLimits, {
"GetLimits": function() { return { "Bar": 20 }; },
"GetCounts": function() { return { "Bar": 0 }; },
"GetLimitChangers": function() { return { "Bar": {} }; },
"GetMatchCounts": function() { return { "Foo": 0 }; }
});
AddMock(101, IID_TechnologyManager, {
"IsTechnologyResearched": tech => tech == "phase_village",
"GetQueuedResearch": () => new Map(),
"GetStartedTechs": () => new Set(),
"GetResearchedTechs": () => new Set(),
"GetClassCounts": () => ({}),
"GetTypeCountsByClass": () => ({})
});
AddMock(101, IID_StatisticsTracker, {
"GetBasicStatistics": function() {
return {
"resourcesGathered": {
"food": 100,
"wood": 0,
"metal": 0,
"stone": 0,
"vegetarianFood": 0
},
"percentMapExplored": 10
};
},
"GetSequences": function() {
return {
"unitsTrained": [0, 10],
"unitsLost": [0, 9],
"buildingsConstructed": [0, 5],
"buildingsCaptured": [0, 7],
"buildingsLost": [0, 4],
"civCentresBuilt": [0, 1],
"resourcesGathered": {
"food": [0, 100],
"wood": [0, 0],
"metal": [0, 0],
"stone": [0, 0],
"vegetarianFood": [0, 0]
},
"treasuresCollected": [0, 0],
"lootCollected": [0, 0],
"percentMapExplored": [0, 10],
"teamPercentMapExplored": [0, 10],
"percentMapControlled": [0, 10],
"teamPercentMapControlled": [0, 10],
"peakPercentOfMapControlled": [0, 10],
"teamPeakPercentOfMapControlled": [0, 10]
};
},
"IncreaseTrainedUnitsCounter": function() { return 1; },
"IncreaseConstructedBuildingsCounter": function() { return 1; },
"IncreaseBuiltCivCentresCounter": function() { return 1; }
});
// Note: property order matters when using TS_ASSERT_UNEVAL_EQUALS,
// because uneval preserves property order. So make sure this object
// matches the ordering in GuiInterface.
TS_ASSERT_UNEVAL_EQUALS(cmp.GetSimulationState(), {
"players": [
{
"name": "Player 1",
"civ": "gaia",
"color": { "r": 1, "g": 1, "b": 1, "a": 1 },
"controlsAll": false,
"popCount": 10,
"popLimit": 20,
"popMax": 200,
"panelEntities": [],
"resourceCounts": { "food": 100 },
"resourceGatherers": { "food": 1 },
"trainingBlocked": false,
"state": "active",
"team": -1,
"teamsLocked": false,
"cheatsEnabled": false,
"disabledTemplates": {},
"disabledTechnologies": {},
"hasSharedDropsites": false,
"hasSharedLos": false,
"spyCostMultiplier": 1,
"phase": "village",
"isAlly": [false, false],
"isMutualAlly": [false, false],
"isNeutral": [false, false],
"isEnemy": [true, true],
"entityLimits": { "Foo": 10 },
"entityCounts": { "Foo": 5 },
"matchEntityCounts": { "Bar": 0 },
"entityLimitChangers": { "Foo": {} },
"researchQueued": new Map(),
"researchStarted": new Set(),
"researchedTechs": new Set(),
"classCounts": {},
"typeCountsByClass": {},
"canBarter": false,
"barterPrices": {
"buy": { "food": 150 },
"sell": { "food": 25 }
},
"statistics": {
"resourcesGathered": {
"food": 100,
"wood": 0,
"metal": 0,
"stone": 0,
"vegetarianFood": 0
},
"percentMapExplored": 10
}
},
{
"name": "Player 2",
"civ": "mace",
"color": { "r": 1, "g": 0, "b": 0, "a": 1 },
"controlsAll": true,
"popCount": 40,
"popLimit": 30,
"popMax": 300,
"panelEntities": [],
"resourceCounts": { "food": 200 },
"resourceGatherers": { "food": 3 },
"trainingBlocked": false,
"state": "active",
"team": -1,
"teamsLocked": false,
"cheatsEnabled": false,
"disabledTemplates": {},
"disabledTechnologies": {},
"hasSharedDropsites": false,
"hasSharedLos": false,
"spyCostMultiplier": 1,
"phase": "village",
"isAlly": [true, true],
"isMutualAlly": [false, false],
"isNeutral": [false, false],
"isEnemy": [false, false],
"entityLimits": { "Bar": 20 },
"entityCounts": { "Bar": 0 },
"matchEntityCounts": { "Foo": 0 },
"entityLimitChangers": { "Bar": {} },
"researchQueued": new Map(),
"researchStarted": new Set(),
"researchedTechs": new Set(),
"classCounts": {},
"typeCountsByClass": {},
"canBarter": false,
"barterPrices": {
"buy": { "food": 150 },
"sell": { "food": 25 }
},
"statistics": {
"resourcesGathered": {
"food": 100,
"wood": 0,
"metal": 0,
"stone": 0,
"vegetarianFood": 0
},
"percentMapExplored": 10
}
}
],
"circularMap": false,
"timeElapsed": 0,
"victoryConditions": ["conquest", "wonder"],
"alliedVictory": false,
"maxWorldPopulation": undefined
});
TS_ASSERT_UNEVAL_EQUALS(cmp.GetExtendedSimulationState(), {
"players": [
{
"name": "Player 1",
"civ": "gaia",
"color": { "r": 1, "g": 1, "b": 1, "a": 1 },
"controlsAll": false,
"popCount": 10,
"popLimit": 20,
"popMax": 200,
"panelEntities": [],
"resourceCounts": { "food": 100 },
"resourceGatherers": { "food": 1 },
"trainingBlocked": false,
"state": "active",
"team": -1,
"teamsLocked": false,
"cheatsEnabled": false,
"disabledTemplates": {},
"disabledTechnologies": {},
"hasSharedDropsites": false,
"hasSharedLos": false,
"spyCostMultiplier": 1,
"phase": "village",
"isAlly": [false, false],
"isMutualAlly": [false, false],
"isNeutral": [false, false],
"isEnemy": [true, true],
"entityLimits": { "Foo": 10 },
"entityCounts": { "Foo": 5 },
"matchEntityCounts": { "Bar": 0 },
"entityLimitChangers": { "Foo": {} },
"researchQueued": new Map(),
"researchStarted": new Set(),
"researchedTechs": new Set(),
"classCounts": {},
"typeCountsByClass": {},
"canBarter": false,
"barterPrices": {
"buy": { "food": 150 },
"sell": { "food": 25 }
},
"statistics": {
"resourcesGathered": {
"food": 100,
"wood": 0,
"metal": 0,
"stone": 0,
"vegetarianFood": 0
},
"percentMapExplored": 10
},
"sequences": {
"unitsTrained": [0, 10],
"unitsLost": [0, 42],
"buildingsConstructed": [1, 3],
"buildingsCaptured": [3, 7],
"buildingsLost": [3, 10],
"civCentresBuilt": [4, 10],
"resourcesGathered": {
"food": [5, 100],
"wood": [0, 0],
"metal": [0, 0],
"stone": [0, 0],
"vegetarianFood": [0, 0]
},
"treasuresCollected": [1, 20],
"lootCollected": [0, 2],
"percentMapExplored": [0, 10],
"teamPercentMapExplored": [0, 10],
"percentMapControlled": [0, 10],
"teamPercentMapControlled": [0, 10],
"peakPercentOfMapControlled": [0, 10],
"teamPeakPercentOfMapControlled": [0, 10]
}
},
{
"name": "Player 2",
"civ": "mace",
"color": { "r": 1, "g": 0, "b": 0, "a": 1 },
"controlsAll": true,
"popCount": 40,
"popLimit": 30,
"popMax": 300,
"panelEntities": [],
"resourceCounts": { "food": 200 },
"resourceGatherers": { "food": 3 },
"trainingBlocked": false,
"state": "active",
"team": -1,
"teamsLocked": false,
"cheatsEnabled": false,
"disabledTemplates": {},
"disabledTechnologies": {},
"hasSharedDropsites": false,
"hasSharedLos": false,
"spyCostMultiplier": 1,
"phase": "village",
"isAlly": [true, true],
"isMutualAlly": [false, false],
"isNeutral": [false, false],
"isEnemy": [false, false],
"entityLimits": { "Bar": 20 },
"entityCounts": { "Bar": 0 },
"matchEntityCounts": { "Foo": 0 },
"entityLimitChangers": { "Bar": {} },
"researchQueued": new Map(),
"researchStarted": new Set(),
"researchedTechs": new Set(),
"classCounts": {},
"typeCountsByClass": {},
"canBarter": false,
"barterPrices": {
"buy": { "food": 150 },
"sell": { "food": 25 }
},
"statistics": {
"resourcesGathered": {
"food": 100,
"wood": 0,
"metal": 0,
"stone": 0,
"vegetarianFood": 0
},
"percentMapExplored": 10
},
"sequences": {
"unitsTrained": [0, 10],
"unitsLost": [0, 9],
"buildingsConstructed": [0, 5],
"buildingsCaptured": [0, 7],
"buildingsLost": [0, 4],
"civCentresBuilt": [0, 1],
"resourcesGathered": {
"food": [0, 100],
"wood": [0, 0],
"metal": [0, 0],
"stone": [0, 0],
"vegetarianFood": [0, 0]
},
"treasuresCollected": [0, 0],
"lootCollected": [0, 0],
"percentMapExplored": [0, 10],
"teamPercentMapExplored": [0, 10],
"percentMapControlled": [0, 10],
"teamPercentMapControlled": [0, 10],
"peakPercentOfMapControlled": [0, 10],
"teamPeakPercentOfMapControlled": [0, 10]
}
}
],
"circularMap": false,
"timeElapsed": 0,
"victoryConditions": ["conquest", "wonder"],
"alliedVictory": false,
"maxWorldPopulation": undefined
});
AddMock(10, IID_Builder, {
"GetEntitiesList": function() {
return ["test1", "test2"];
},
});
AddMock(10, IID_Health, {
"GetHitpoints": function() { return 50; },
"GetMaxHitpoints": function() { return 60; },
"IsRepairable": function() { return false; },
"IsUnhealable": function() { return false; }
});
AddMock(10, IID_Identity, {
"GetClassesList": function() { return ["class1", "class2"]; },
"GetRank": function() { return "foo"; },
"GetSelectionGroupName": function() { return "Selection Group Name"; },
"HasClass": function() { return true; },
"IsUndeletable": function() { return false; },
"IsControllable": function() { return true; },
"HasSomeFormation": function() { return false; },
"GetFormationsList": function() { return []; },
});
AddMock(10, IID_Position, {
"GetTurretParent": function() { return INVALID_ENTITY; },
"GetPosition": function() {
return { "x": 1, "y": 2, "z": 3 };
},
"IsInWorld": function() {
return true;
}
});
AddMock(10, IID_ResourceTrickle, {
"GetInterval": () => 1250,
"GetRates": () => ({ "food": 2, "wood": 3, "stone": 5, "metal": 9 })
});
// Note: property order matters when using TS_ASSERT_UNEVAL_EQUALS,
// because uneval preserves property order. So make sure this object
// matches the ordering in GuiInterface.
TS_ASSERT_UNEVAL_EQUALS(cmp.GetEntityState(-1, 10), {
"id": 10,
"player": INVALID_PLAYER,
"template": "example",
"identity": {
"rank": "foo",
"classes": ["class1", "class2"],
"selectionGroupName": "Selection Group Name",
"canDelete": true,
"hasSomeFormation": false,
"formations": [],
"controllable": true,
},
"position": { "x": 1, "y": 2, "z": 3 },
"hitpoints": 50,
"maxHitpoints": 60,
"needsRepair": false,
"needsHeal": true,
"builder": true,
"visibility": "visible",
"isBarterMarket": true,
"resourceTrickle": {
"interval": 1250,
"rates": { "food": 2, "wood": 3, "stone": 5, "metal": 9 }
}
});
Index: ps/trunk/binaries/data/mods/public/simulation/components/tests/test_ProductionQueue.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/components/tests/test_ProductionQueue.js (revision 25999)
+++ ps/trunk/binaries/data/mods/public/simulation/components/tests/test_ProductionQueue.js (revision 26000)
@@ -1,626 +1,83 @@
Engine.LoadHelperScript("Player.js");
-Engine.LoadHelperScript("Sound.js");
-Engine.LoadComponentScript("interfaces/TechnologyManager.js");
Engine.LoadComponentScript("interfaces/ProductionQueue.js");
-Engine.LoadComponentScript("interfaces/BuildRestrictions.js");
-Engine.LoadComponentScript("interfaces/EntityLimits.js");
-Engine.LoadComponentScript("interfaces/Foundation.js");
-Engine.LoadComponentScript("interfaces/StatisticsTracker.js");
+Engine.LoadComponentScript("interfaces/Researcher.js");
Engine.LoadComponentScript("interfaces/Timer.js");
-Engine.LoadComponentScript("interfaces/TrainingRestrictions.js");
-Engine.LoadComponentScript("interfaces/Trigger.js");
+Engine.LoadComponentScript("interfaces/Trainer.js");
Engine.LoadComponentScript("interfaces/Upgrade.js");
-Engine.LoadComponentScript("EntityLimits.js");
Engine.LoadComponentScript("Timer.js");
-Engine.RegisterGlobal("Resources", {
- "BuildSchema": (a, b) => {}
-});
Engine.LoadComponentScript("ProductionQueue.js");
-Engine.LoadComponentScript("TrainingRestrictions.js");
-Engine.RegisterGlobal("ApplyValueModificationsToEntity", (_, value) => value);
-Engine.RegisterGlobal("ApplyValueModificationsToTemplate", (_, value) => value);
+const playerEnt = 2;
+const playerID = 1;
+const testEntity = 3;
+
+AddMock(SYSTEM_ENTITY, IID_Timer, {
+ "CancelTimer": (id) => {},
+ "SetInterval": (ent, iid, func) => 1
+});
+
+AddMock(SYSTEM_ENTITY, IID_PlayerManager, {
+ "GetPlayerByID": id => playerEnt
+});
+
+AddMock(playerEnt, IID_Player, {
+ "GetPlayerID": () => playerID
+});
+
+AddMock(testEntity, IID_Ownership, {
+ "GetOwner": () => playerID
+});
+
+AddMock(testEntity, IID_Trainer, {
+ "GetBatch": (id) => ({}),
+ "HasBatch": (id) => false, // Assume we've finished.
+ "Progress": (time) => time,
+ "QueueBatch": () => 1,
+ "StopBatch": (id) => {}
+});
+
+const cmpProdQueue = ConstructComponent(testEntity, "ProductionQueue", null);
+
+
+// Test autoqueue.
+cmpProdQueue.EnableAutoQueue();
+
+cmpProdQueue.AddItem("some_template", "unit", 3);
+TS_ASSERT_EQUALS(cmpProdQueue.GetQueue().length, 1);
+cmpProdQueue.ProgressTimeout(null, 0);
+TS_ASSERT_EQUALS(cmpProdQueue.GetQueue().length, 1);
+
+cmpProdQueue.RemoveItem(cmpProdQueue.nextID -1);
+TS_ASSERT_EQUALS(cmpProdQueue.GetQueue().length, 0);
+
+cmpProdQueue.DisableAutoQueue();
+
+
+// Test items which don't use all the time.
+AddMock(testEntity, IID_Trainer, {
+ "GetBatch": (id) => ({}),
+ "HasBatch": (id) => false, // Assume we've finished.
+ "PauseBatch": (id) => {},
+ "Progress": (time) => time - 250,
+ "QueueBatch": () => 1,
+ "StopBatch": (id) => {},
+ "UnpauseBatch": (id) => {}
+});
+
+cmpProdQueue.AddItem("some_template", "unit", 2);
+cmpProdQueue.AddItem("some_template", "unit", 3);
+TS_ASSERT_EQUALS(cmpProdQueue.GetQueue().length, 2);
+cmpProdQueue.ProgressTimeout(null, 0);
+TS_ASSERT_EQUALS(cmpProdQueue.GetQueue().length, 0);
+
+
+// Test pushing an item to the front.
+cmpProdQueue.AddItem("some_template", "unit", 2);
+cmpProdQueue.AddItem("some_template", "unit", 3, null, true);
+TS_ASSERT_EQUALS(cmpProdQueue.GetQueue().length, 2);
+TS_ASSERT_EQUALS(cmpProdQueue.GetQueue()[0].id, cmpProdQueue.nextID - 1);
+TS_ASSERT(cmpProdQueue.GetQueue()[1].paused);
-function testEntitiesList()
-{
- Engine.RegisterGlobal("TechnologyTemplates", {
- "Has": name => name == "phase_town_athen" || name == "phase_city_athen",
- "Get": () => ({})
- });
-
- const productionQueueId = 6;
- const playerId = 1;
- const playerEntityID = 2;
-
- AddMock(SYSTEM_ENTITY, IID_TemplateManager, {
- "TemplateExists": () => true,
- "GetTemplate": name => ({})
- });
-
- let cmpProductionQueue = ConstructComponent(productionQueueId, "ProductionQueue", {
- "Entities": { "_string": "units/{civ}/cavalry_javelineer_b " +
- "units/{civ}/infantry_swordsman_b " +
- "units/{native}/support_female_citizen" },
- "Technologies": { "_string": "gather_fishing_net " +
- "phase_town_{civ} " +
- "phase_city_{civ}" }
- });
- cmpProductionQueue.GetUpgradedTemplate = (template) => template;
-
- AddMock(SYSTEM_ENTITY, IID_PlayerManager, {
- "GetPlayerByID": id => playerEntityID
- });
-
- AddMock(playerEntityID, IID_Player, {
- "GetCiv": () => "iber",
- "GetDisabledTechnologies": () => ({}),
- "GetDisabledTemplates": () => ({}),
- "GetPlayerID": () => playerId
- });
-
- AddMock(playerEntityID, IID_TechnologyManager, {
- "CheckTechnologyRequirements": () => true,
- "IsInProgress": () => false,
- "IsTechnologyResearched": () => false
- });
-
- AddMock(productionQueueId, IID_Ownership, {
- "GetOwner": () => playerId
- });
-
- AddMock(productionQueueId, IID_Identity, {
- "GetCiv": () => "iber"
- });
-
- AddMock(productionQueueId, IID_Upgrade, {
- "IsUpgrading": () => false
- });
-
- cmpProductionQueue.CalculateEntitiesMap();
- TS_ASSERT_UNEVAL_EQUALS(
- cmpProductionQueue.GetEntitiesList(),
- ["units/iber/cavalry_javelineer_b", "units/iber/infantry_swordsman_b", "units/iber/support_female_citizen"]
- );
- TS_ASSERT_UNEVAL_EQUALS(
- cmpProductionQueue.GetTechnologiesList(),
- ["gather_fishing_net", "phase_town_generic", "phase_city_generic"]
- );
-
- AddMock(SYSTEM_ENTITY, IID_TemplateManager, {
- "TemplateExists": name => name == "units/iber/support_female_citizen",
- "GetTemplate": name => ({})
- });
-
- cmpProductionQueue.CalculateEntitiesMap();
- TS_ASSERT_UNEVAL_EQUALS(cmpProductionQueue.GetEntitiesList(), ["units/iber/support_female_citizen"]);
-
- AddMock(SYSTEM_ENTITY, IID_TemplateManager, {
- "TemplateExists": () => true,
- "GetTemplate": name => ({})
- });
-
- AddMock(playerEntityID, IID_Player, {
- "GetCiv": () => "iber",
- "GetDisabledTechnologies": () => ({}),
- "GetDisabledTemplates": () => ({ "units/athen/infantry_swordsman_b": true }),
- "GetPlayerID": () => playerId
- });
-
- cmpProductionQueue.CalculateEntitiesMap();
- TS_ASSERT_UNEVAL_EQUALS(
- cmpProductionQueue.GetEntitiesList(),
- ["units/iber/cavalry_javelineer_b", "units/iber/infantry_swordsman_b", "units/iber/support_female_citizen"]
- );
-
- AddMock(playerEntityID, IID_Player, {
- "GetCiv": () => "iber",
- "GetDisabledTechnologies": () => ({}),
- "GetDisabledTemplates": () => ({ "units/iber/infantry_swordsman_b": true }),
- "GetPlayerID": () => playerId
- });
-
- cmpProductionQueue.CalculateEntitiesMap();
- TS_ASSERT_UNEVAL_EQUALS(
- cmpProductionQueue.GetEntitiesList(),
- ["units/iber/cavalry_javelineer_b", "units/iber/support_female_citizen"]
- );
-
- AddMock(playerEntityID, IID_Player, {
- "GetCiv": () => "athen",
- "GetDisabledTechnologies": () => ({ "gather_fishing_net": true }),
- "GetDisabledTemplates": () => ({ "units/athen/infantry_swordsman_b": true }),
- "GetPlayerID": () => playerId
- });
-
- cmpProductionQueue.CalculateEntitiesMap();
- TS_ASSERT_UNEVAL_EQUALS(
- cmpProductionQueue.GetEntitiesList(),
- ["units/athen/cavalry_javelineer_b", "units/iber/support_female_citizen"]
- );
- TS_ASSERT_UNEVAL_EQUALS(cmpProductionQueue.GetTechnologiesList(), ["phase_town_athen",
- "phase_city_athen"]
- );
-
- AddMock(playerEntityID, IID_TechnologyManager, {
- "CheckTechnologyRequirements": () => true,
- "IsInProgress": () => false,
- "IsTechnologyResearched": tech => tech == "phase_town_athen"
- });
- TS_ASSERT_UNEVAL_EQUALS(cmpProductionQueue.GetTechnologiesList(), [undefined, "phase_city_athen"]);
-
- AddMock(playerEntityID, IID_Player, {
- "GetCiv": () => "iber",
- "GetDisabledTechnologies": () => ({}),
- "GetPlayerID": () => playerId
- });
- TS_ASSERT_UNEVAL_EQUALS(
- cmpProductionQueue.GetTechnologiesList(),
- ["gather_fishing_net", "phase_town_generic", "phase_city_generic"]
- );
-}
-
-function regression_test_d1879()
-{
- // Setup
- let playerEnt = 2;
- let playerID = 1;
- let testEntity = 3;
- let spawedEntityIDs = [4, 5, 6, 7, 8];
- let spawned = 0;
-
- Engine.AddEntity = () => {
- let id = spawedEntityIDs[spawned++];
-
- ConstructComponent(id, "TrainingRestrictions", {
- "Category": "some_limit"
- });
-
- AddMock(id, IID_Identity, {
- "GetClassesList": () => []
- });
-
- AddMock(id, IID_Position, {
- "JumpTo": () => {}
- });
-
- AddMock(id, IID_Ownership, {
- "SetOwner": (pid) => {
- let cmpEntLimits = QueryOwnerInterface(id, IID_EntityLimits);
- cmpEntLimits.OnGlobalOwnershipChanged({
- "entity": id,
- "from": -1,
- "to": pid
- });
- },
- "GetOwner": () => playerID
- });
-
- return id;
- };
-
- ConstructComponent(playerEnt, "EntityLimits", {
- "Limits": {
- "some_limit": 8
- },
- "LimitChangers": {},
- "LimitRemovers": {}
- });
-
- AddMock(SYSTEM_ENTITY, IID_GuiInterface, {
- "PushNotification": () => {}
- });
-
- AddMock(SYSTEM_ENTITY, IID_Trigger, {
- "CallEvent": () => {}
- });
-
- AddMock(SYSTEM_ENTITY, IID_Timer, {
- "SetInterval": (ent, iid, func) => 1,
- "CancelTimer": (id) => {}
- });
-
- AddMock(SYSTEM_ENTITY, IID_TemplateManager, {
- "TemplateExists": () => true,
- "GetTemplate": name => ({
- "Cost": {
- "BuildTime": 0,
- "Population": 1,
- "Resources": {}
- },
- "TrainingRestrictions": {
- "Category": "some_limit",
- "MatchLimit": "7"
- }
- })
- });
-
- AddMock(SYSTEM_ENTITY, IID_PlayerManager, {
- "GetPlayerByID": id => playerEnt
- });
-
- AddMock(playerEnt, IID_Player, {
- "GetCiv": () => "iber",
- "GetPlayerID": () => playerID,
- "GetTimeMultiplier": () => 0,
- "BlockTraining": () => {},
- "UnBlockTraining": () => {},
- "UnReservePopulationSlots": () => {},
- "TrySubtractResources": () => true,
- "AddResources": () => true,
- "TryReservePopulationSlots": () => false // Always have pop space.
- });
-
- AddMock(testEntity, IID_Ownership, {
- "GetOwner": () => playerID
- });
-
- let cmpProdQueue = ConstructComponent(testEntity, "ProductionQueue", {
- "Entities": { "_string": "some_template" },
- "BatchTimeModifier": 1
- });
-
- let cmpEntLimits = QueryOwnerInterface(testEntity, IID_EntityLimits);
- TS_ASSERT(cmpEntLimits.AllowedToTrain("some_limit", 8));
- TS_ASSERT(!cmpEntLimits.AllowedToTrain("some_limit", 9));
- TS_ASSERT(cmpEntLimits.AllowedToTrain("some_limit", 5, "some_template", 8));
- TS_ASSERT(!cmpEntLimits.AllowedToTrain("some_limit", 10, "some_template", 8));
-
- // Check that the entity limits do get updated if the spawn succeeds.
- AddMock(testEntity, IID_Footprint, {
- "PickSpawnPoint": () => ({ "x": 0, "y": 1, "z": 0 })
- });
-
- cmpProdQueue.AddItem("some_template", "unit", 3);
-
- TS_ASSERT_EQUALS(cmpEntLimits.GetCounts().some_limit, 3);
- TS_ASSERT_EQUALS(cmpEntLimits.GetMatchCounts().some_template, 3);
-
- cmpProdQueue.ProgressTimeout(null, 0);
-
- TS_ASSERT_EQUALS(cmpEntLimits.GetCounts().some_limit, 3);
- TS_ASSERT_EQUALS(cmpEntLimits.GetMatchCounts().some_template, 3);
-
- // Now check that it doesn't get updated when the spawn doesn't succeed.
- AddMock(testEntity, IID_Footprint, {
- "PickSpawnPoint": () => ({ "x": -1, "y": -1, "z": -1 })
- });
-
- AddMock(testEntity, IID_Upgrade, {
- "IsUpgrading": () => false
- });
-
- cmpProdQueue.AddItem("some_template", "unit", 3);
- cmpProdQueue.ProgressTimeout(null, 0);
-
- TS_ASSERT_EQUALS(cmpProdQueue.GetQueue().length, 1);
- TS_ASSERT_EQUALS(cmpEntLimits.GetCounts().some_limit, 6);
- TS_ASSERT_EQUALS(cmpEntLimits.GetMatchCounts().some_template, 6);
-
- // Check that when the batch is removed the counts are subtracted again.
- cmpProdQueue.RemoveItem(cmpProdQueue.GetQueue()[0].id);
- TS_ASSERT_EQUALS(cmpEntLimits.GetCounts().some_limit, 3);
- TS_ASSERT_EQUALS(cmpEntLimits.GetMatchCounts().some_template, 3);
-}
-
-function test_batch_adding()
-{
- let playerEnt = 2;
- let playerID = 1;
- let testEntity = 3;
-
- ConstructComponent(playerEnt, "EntityLimits", {
- "Limits": {
- "some_limit": 8
- },
- "LimitChangers": {},
- "LimitRemovers": {}
- });
-
- AddMock(SYSTEM_ENTITY, IID_GuiInterface, {
- "PushNotification": () => {}
- });
-
- AddMock(SYSTEM_ENTITY, IID_Trigger, {
- "CallEvent": () => {}
- });
-
- AddMock(SYSTEM_ENTITY, IID_Timer, {
- "SetInterval": (ent, iid, func) => 1
- });
-
- AddMock(SYSTEM_ENTITY, IID_TemplateManager, {
- "TemplateExists": () => true,
- "GetTemplate": name => ({
- "Cost": {
- "BuildTime": 0,
- "Population": 1,
- "Resources": {}
- },
- "TrainingRestrictions": {
- "Category": "some_limit"
- }
- })
- });
-
- AddMock(SYSTEM_ENTITY, IID_PlayerManager, {
- "GetPlayerByID": id => playerEnt
- });
-
- AddMock(playerEnt, IID_Player, {
- "GetCiv": () => "iber",
- "GetPlayerID": () => playerID,
- "GetTimeMultiplier": () => 0,
- "BlockTraining": () => {},
- "UnBlockTraining": () => {},
- "UnReservePopulationSlots": () => {},
- "TrySubtractResources": () => true,
- "TryReservePopulationSlots": () => false // Always have pop space.
- });
-
- AddMock(testEntity, IID_Ownership, {
- "GetOwner": () => playerID
- });
-
- let cmpProdQueue = ConstructComponent(testEntity, "ProductionQueue", {
- "Entities": { "_string": "some_template" },
- "BatchTimeModifier": 1
- });
-
-
- TS_ASSERT_EQUALS(cmpProdQueue.GetQueue().length, 0);
- AddMock(testEntity, IID_Upgrade, {
- "IsUpgrading": () => true
- });
-
- cmpProdQueue.AddItem("some_template", "unit", 3);
- TS_ASSERT_EQUALS(cmpProdQueue.GetQueue().length, 0);
-
- AddMock(testEntity, IID_Upgrade, {
- "IsUpgrading": () => false
- });
-
- cmpProdQueue.AddItem("some_template", "unit", 3);
- TS_ASSERT_EQUALS(cmpProdQueue.GetQueue().length, 1);
-}
-
-function test_batch_removal()
-{
- let playerEnt = 2;
- let playerID = 1;
- let testEntity = 3;
-
- ConstructComponent(playerEnt, "EntityLimits", {
- "Limits": {
- "some_limit": 8
- },
- "LimitChangers": {},
- "LimitRemovers": {}
- });
-
- AddMock(SYSTEM_ENTITY, IID_GuiInterface, {
- "PushNotification": () => {}
- });
-
- AddMock(SYSTEM_ENTITY, IID_Trigger, {
- "CallEvent": () => {}
- });
-
- let cmpTimer = ConstructComponent(SYSTEM_ENTITY, "Timer", null);
-
- AddMock(SYSTEM_ENTITY, IID_TemplateManager, {
- "TemplateExists": () => true,
- "GetTemplate": name => ({
- "Cost": {
- "BuildTime": 0,
- "Population": 1,
- "Resources": {}
- },
- "TrainingRestrictions": {
- "Category": "some_limit"
- }
- })
- });
-
- AddMock(SYSTEM_ENTITY, IID_PlayerManager, {
- "GetPlayerByID": id => playerEnt
- });
-
- let cmpPlayer = AddMock(playerEnt, IID_Player, {
- "GetCiv": () => "iber",
- "GetPlayerID": () => playerID,
- "GetTimeMultiplier": () => 0,
- "BlockTraining": () => {},
- "UnBlockTraining": () => {},
- "UnReservePopulationSlots": () => {},
- "TrySubtractResources": () => true,
- "AddResources": () => {},
- "TryReservePopulationSlots": () => 1
- });
- let cmpPlayerBlockSpy = new Spy(cmpPlayer, "BlockTraining");
- let cmpPlayerUnblockSpy = new Spy(cmpPlayer, "UnBlockTraining");
-
- AddMock(testEntity, IID_Ownership, {
- "GetOwner": () => playerID
- });
-
- let cmpProdQueue = ConstructComponent(testEntity, "ProductionQueue", {
- "Entities": { "_string": "some_template" },
- "BatchTimeModifier": 1
- });
-
- cmpProdQueue.AddItem("some_template", "unit", 3);
- TS_ASSERT_EQUALS(cmpProdQueue.GetQueue().length, 1);
- cmpTimer.OnUpdate({ "turnLength": 1 });
- TS_ASSERT_EQUALS(cmpPlayerBlockSpy._called, 1);
-
- cmpProdQueue.AddItem("some_template", "unit", 2);
- TS_ASSERT_EQUALS(cmpProdQueue.GetQueue().length, 2);
-
- cmpProdQueue.RemoveItem(1);
- TS_ASSERT_EQUALS(cmpProdQueue.GetQueue().length, 1);
- TS_ASSERT_EQUALS(cmpPlayerUnblockSpy._called, 1);
-
- cmpProdQueue.RemoveItem(2);
- TS_ASSERT_EQUALS(cmpProdQueue.GetQueue().length, 0);
- cmpTimer.OnUpdate({ "turnLength": 1 });
- TS_ASSERT_EQUALS(cmpPlayerUnblockSpy._called, 2);
-
- cmpProdQueue.AddItem("some_template", "unit", 3);
- cmpProdQueue.AddItem("some_template", "unit", 3);
- cmpPlayer.TryReservePopulationSlots = () => false;
- cmpProdQueue.RemoveItem(3);
- TS_ASSERT_EQUALS(cmpPlayerUnblockSpy._called, 3);
- cmpTimer.OnUpdate({ "turnLength": 1 });
- TS_ASSERT_EQUALS(cmpPlayerUnblockSpy._called, 4);
-}
-
-function test_token_changes()
-{
- const ent = 10;
- let cmpProductionQueue = ConstructComponent(10, "ProductionQueue", {
- "Entities": { "_string": "units/{civ}/a " +
- "units/{civ}/b" },
- "Technologies": { "_string": "a " +
- "b_{civ} " +
- "c_{civ}" },
- "BatchTimeModifier": 1
- });
- cmpProductionQueue.GetUpgradedTemplate = (template) => template;
-
- // Merges interface of multiple components because it's enough here.
- Engine.RegisterGlobal("QueryOwnerInterface", () => ({
- // player
- "GetCiv": () => "test",
- "GetDisabledTemplates": () => [],
- "GetDisabledTechnologies": () => [],
- "TryReservePopulationSlots": () => false, // Always have pop space.
- "TrySubtractResources": () => true,
- "UnBlockTraining": () => {},
- "AddResources": () => {},
- "GetPlayerID": () => 1,
- // entitylimits
- "ChangeCount": () => {},
- "AllowedToTrain": () => true,
- // techmanager
- "CheckTechnologyRequirements": () => true,
- "IsTechnologyResearched": () => false,
- "IsInProgress": () => false
- }));
- Engine.RegisterGlobal("QueryPlayerIDInterface", QueryOwnerInterface);
-
- AddMock(SYSTEM_ENTITY, IID_GuiInterface, {
- "SetSelectionDirty": () => {}
- });
-
- // Test Setup
- cmpProductionQueue.CalculateEntitiesMap();
- TS_ASSERT_UNEVAL_EQUALS(
- cmpProductionQueue.GetEntitiesList(), ["units/test/a", "units/test/b"]
- );
- TS_ASSERT_UNEVAL_EQUALS(
- cmpProductionQueue.GetTechnologiesList(), ["a", "b_generic", "c_generic"]
- );
- // Add a unit of each type to our queue, validate.
- cmpProductionQueue.AddItem("units/test/a", "unit", 1, {});
- cmpProductionQueue.AddItem("units/test/b", "unit", 1, {});
- TS_ASSERT_EQUALS(cmpProductionQueue.GetQueue()[0].unitTemplate, "units/test/a");
- TS_ASSERT_EQUALS(cmpProductionQueue.GetQueue()[1].unitTemplate, "units/test/b");
-
- // Add a modifier that replaces unit A with unit C,
- // adds a unit D and removes unit B from the roster.
- Engine.RegisterGlobal("ApplyValueModificationsToEntity", (_, val) => {
- return HandleTokens(val, "units/{civ}/a>units/{civ}/c units/{civ}/d -units/{civ}/b");
- });
-
- cmpProductionQueue.OnValueModification({
- "component": "ProductionQueue",
- "valueNames": ["ProductionQueue/Entities/_string"],
- "entities": [ent]
- });
-
- TS_ASSERT_UNEVAL_EQUALS(
- cmpProductionQueue.GetEntitiesList(), ["units/test/c", "units/test/d"]
- );
- TS_ASSERT_EQUALS(cmpProductionQueue.GetQueue()[0].unitTemplate, "units/test/c");
- TS_ASSERT_EQUALS(cmpProductionQueue.GetQueue().length, 1);
-}
-
-function test_auto_queue()
-{
- let playerEnt = 2;
- let playerID = 1;
- let testEntity = 3;
-
- ConstructComponent(playerEnt, "EntityLimits", {
- "Limits": {
- "some_limit": 8
- },
- "LimitChangers": {},
- "LimitRemovers": {}
- });
-
- AddMock(SYSTEM_ENTITY, IID_GuiInterface, {
- "PushNotification": () => {}
- });
-
- AddMock(SYSTEM_ENTITY, IID_Trigger, {
- "CallEvent": () => {}
- });
-
- AddMock(SYSTEM_ENTITY, IID_Timer, {
- "SetInterval": (ent, iid, func) => 1
- });
-
- AddMock(SYSTEM_ENTITY, IID_TemplateManager, {
- "TemplateExists": () => true,
- "GetTemplate": name => ({
- "Cost": {
- "BuildTime": 0,
- "Population": 1,
- "Resources": {}
- },
- "TrainingRestrictions": {
- "Category": "some_limit"
- }
- })
- });
-
- AddMock(SYSTEM_ENTITY, IID_PlayerManager, {
- "GetPlayerByID": id => playerEnt
- });
-
- AddMock(playerEnt, IID_Player, {
- "GetCiv": () => "iber",
- "GetPlayerID": () => playerID,
- "GetTimeMultiplier": () => 0,
- "BlockTraining": () => {},
- "UnBlockTraining": () => {},
- "UnReservePopulationSlots": () => {},
- "TrySubtractResources": () => true,
- "TryReservePopulationSlots": () => false // Always have pop space.
- });
-
- AddMock(testEntity, IID_Ownership, {
- "GetOwner": () => playerID
- });
-
- let cmpProdQueue = ConstructComponent(testEntity, "ProductionQueue", {
- "Entities": { "_string": "some_template" },
- "BatchTimeModifier": 1
- });
-
- cmpProdQueue.EnableAutoQueue();
-
- cmpProdQueue.AddItem("some_template", "unit", 3);
- TS_ASSERT_EQUALS(cmpProdQueue.GetQueue().length, 1);
- cmpProdQueue.ProgressTimeout(null, 0);
- TS_ASSERT_EQUALS(cmpProdQueue.GetQueue().length, 1);
-}
-
-testEntitiesList();
-regression_test_d1879();
-test_batch_adding();
-test_batch_removal();
-test_auto_queue();
-test_token_changes();
+cmpProdQueue.ProgressTimeout(null, 0);
+TS_ASSERT_EQUALS(cmpProdQueue.GetQueue().length, 0);
Index: ps/trunk/binaries/data/mods/public/simulation/components/tests/test_Researcher.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/components/tests/test_Researcher.js (nonexistent)
+++ ps/trunk/binaries/data/mods/public/simulation/components/tests/test_Researcher.js (revision 26000)
@@ -0,0 +1,153 @@
+Engine.RegisterGlobal("Resources", {
+ "BuildSchema": (a, b) => {}
+});
+Engine.LoadHelperScript("Player.js");
+Engine.LoadComponentScript("interfaces/TechnologyManager.js");
+Engine.LoadComponentScript("interfaces/Researcher.js");
+Engine.LoadComponentScript("Researcher.js");
+
+Engine.RegisterGlobal("ApplyValueModificationsToEntity", (_, value) => value);
+
+const playerID = 1;
+const playerEntityID = 11;
+const entityID = 21;
+
+Engine.RegisterGlobal("TechnologyTemplates", {
+ "Has": name => name == "phase_town_athen" || name == "phase_city_athen",
+ "Get": () => ({})
+});
+
+AddMock(SYSTEM_ENTITY, IID_PlayerManager, {
+ "GetPlayerByID": id => playerEntityID
+});
+
+AddMock(playerEntityID, IID_Player, {
+ "GetCiv": () => "iber",
+ "GetDisabledTechnologies": () => ({})
+});
+
+AddMock(playerEntityID, IID_TechnologyManager, {
+ "CheckTechnologyRequirements": () => true,
+ "IsInProgress": () => false,
+ "IsTechnologyResearched": () => false
+});
+
+AddMock(entityID, IID_Ownership, {
+ "GetOwner": () => playerID
+});
+
+AddMock(entityID, IID_Identity, {
+ "GetCiv": () => "iber"
+});
+
+const cmpResearcher = ConstructComponent(entityID, "Researcher", {
+ "Technologies": { "_string": "gather_fishing_net " +
+ "phase_town_{civ} " +
+ "phase_city_{civ}" }
+});
+
+TS_ASSERT_UNEVAL_EQUALS(
+ cmpResearcher.GetTechnologiesList(),
+ ["gather_fishing_net", "phase_town_generic", "phase_city_generic"]
+);
+
+AddMock(playerEntityID, IID_Player, {
+ "GetCiv": () => "athen",
+ "GetDisabledTechnologies": () => ({ "gather_fishing_net": true })
+});
+TS_ASSERT_UNEVAL_EQUALS(cmpResearcher.GetTechnologiesList(), ["phase_town_athen", "phase_city_athen"]);
+
+AddMock(playerEntityID, IID_TechnologyManager, {
+ "CheckTechnologyRequirements": () => true,
+ "IsInProgress": () => false,
+ "IsTechnologyResearched": tech => tech == "phase_town_athen"
+});
+TS_ASSERT_UNEVAL_EQUALS(cmpResearcher.GetTechnologiesList(), [undefined, "phase_city_athen"]);
+
+AddMock(playerEntityID, IID_Player, {
+ "GetCiv": () => "iber",
+ "GetDisabledTechnologies": () => ({})
+});
+TS_ASSERT_UNEVAL_EQUALS(
+ cmpResearcher.GetTechnologiesList(),
+ ["gather_fishing_net", "phase_town_generic", "phase_city_generic"]
+);
+
+Engine.RegisterGlobal("ApplyValueModificationsToEntity", (_, value) => value + " some_test");
+TS_ASSERT_UNEVAL_EQUALS(
+ cmpResearcher.GetTechnologiesList(),
+ ["gather_fishing_net", "phase_town_generic", "phase_city_generic", "some_test"]
+);
+
+
+// Test Queuing a tech.
+const queuedTech = "gather_fishing_net";
+const cost = {
+ "food": 10
+};
+Engine.RegisterGlobal("TechnologyTemplates", {
+ "Has": () => true,
+ "Get": () => ({
+ "cost": cost,
+ "researchTime": 1
+ })
+});
+
+const cmpPlayer = AddMock(playerEntityID, IID_Player, {
+ "GetCiv": () => "iber",
+ "GetDisabledTechnologies": () => ({}),
+ "GetPlayerID": () => playerID,
+ "TrySubtractResources": (resources) => {
+ TS_ASSERT_UNEVAL_EQUALS(resources, cost);
+ // Just have enough resources.
+ return true;
+ },
+ "RefundResources": (resources) => {
+ TS_ASSERT_UNEVAL_EQUALS(resources, cost);
+ },
+});
+let spyCmpPlayer = new Spy(cmpPlayer, "TrySubtractResources");
+const techManager = AddMock(playerEntityID, IID_TechnologyManager, {
+ "CheckTechnologyRequirements": () => true,
+ "IsInProgress": () => false,
+ "IsTechnologyResearched": () => false,
+ "QueuedResearch": (templateName, researcher) => {
+ TS_ASSERT_UNEVAL_EQUALS(templateName, queuedTech);
+ TS_ASSERT_UNEVAL_EQUALS(researcher, entityID);
+ },
+ "StoppedResearch": (templateName, _) => {
+ TS_ASSERT_UNEVAL_EQUALS(templateName, queuedTech);
+ },
+ "StartedResearch": (templateName, _) => {
+ TS_ASSERT_UNEVAL_EQUALS(templateName, queuedTech);
+ },
+ "ResearchTechnology": (templateName, _) => {
+ TS_ASSERT_UNEVAL_EQUALS(templateName, queuedTech);
+ }
+});
+let spyTechManager = new Spy(techManager, "QueuedResearch");
+let id = cmpResearcher.QueueTechnology(queuedTech);
+TS_ASSERT_EQUALS(spyTechManager._called, 1);
+TS_ASSERT_EQUALS(spyCmpPlayer._called, 1);
+TS_ASSERT_EQUALS(cmpResearcher.queue.size, 1);
+
+
+// Test removing a queued tech.
+spyCmpPlayer = new Spy(cmpPlayer, "RefundResources");
+spyTechManager = new Spy(techManager, "StoppedResearch");
+cmpResearcher.StopResearching(id);
+TS_ASSERT_EQUALS(spyTechManager._called, 1);
+TS_ASSERT_EQUALS(spyCmpPlayer._called, 1);
+TS_ASSERT_EQUALS(cmpResearcher.queue.size, 0);
+
+
+// Test finishing a queued tech.
+id = cmpResearcher.QueueTechnology(queuedTech);
+TS_ASSERT_EQUALS(cmpResearcher.GetResearchingTechnology(id).progress, 0);
+TS_ASSERT_EQUALS(cmpResearcher.Progress(id, 500), 500);
+TS_ASSERT_EQUALS(cmpResearcher.GetResearchingTechnology(id).progress, 0.5);
+
+spyTechManager = new Spy(techManager, "ResearchTechnology");
+TS_ASSERT_EQUALS(cmpResearcher.Progress(id, 1000), 500);
+TS_ASSERT_EQUALS(spyTechManager._called, 1);
+TS_ASSERT_EQUALS(cmpResearcher.queue.size, 0);
Property changes on: ps/trunk/binaries/data/mods/public/simulation/components/tests/test_Researcher.js
___________________________________________________________________
Added: svn:eol-style
## -0,0 +1 ##
+native
\ No newline at end of property
Added: svn:mime-type
## -0,0 +1 ##
+text/plain
\ No newline at end of property
Index: ps/trunk/binaries/data/mods/public/simulation/components/tests/test_Trainer.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/components/tests/test_Trainer.js (nonexistent)
+++ ps/trunk/binaries/data/mods/public/simulation/components/tests/test_Trainer.js (revision 26000)
@@ -0,0 +1,301 @@
+Engine.RegisterGlobal("Resources", {
+ "BuildSchema": (a, b) => {}
+});
+Engine.LoadHelperScript("Player.js");
+Engine.LoadHelperScript("Sound.js");
+Engine.LoadComponentScript("interfaces/BuildRestrictions.js");
+Engine.LoadComponentScript("interfaces/EntityLimits.js");
+Engine.LoadComponentScript("interfaces/Foundation.js");
+Engine.LoadComponentScript("interfaces/StatisticsTracker.js");
+Engine.LoadComponentScript("interfaces/Trainer.js");
+Engine.LoadComponentScript("interfaces/TrainingRestrictions.js");
+Engine.LoadComponentScript("interfaces/Trigger.js");
+Engine.LoadComponentScript("EntityLimits.js");
+Engine.LoadComponentScript("Trainer.js");
+Engine.LoadComponentScript("TrainingRestrictions.js");
+
+Engine.RegisterGlobal("ApplyValueModificationsToEntity", (_, value) => value);
+Engine.RegisterGlobal("ApplyValueModificationsToTemplate", (_, value) => value);
+
+const playerID = 1;
+const playerEntityID = 11;
+const entityID = 21;
+
+AddMock(SYSTEM_ENTITY, IID_TemplateManager, {
+ "TemplateExists": () => true,
+ "GetTemplate": name => ({})
+});
+
+const cmpTrainer = ConstructComponent(entityID, "Trainer", {
+ "Entities": { "_string": "units/{civ}/cavalry_javelineer_b " +
+ "units/{civ}/infantry_swordsman_b " +
+ "units/{native}/support_female_citizen" }
+});
+cmpTrainer.GetUpgradedTemplate = (template) => template;
+
+AddMock(SYSTEM_ENTITY, IID_PlayerManager, {
+ "GetPlayerByID": id => playerEntityID
+});
+
+AddMock(playerEntityID, IID_Player, {
+ "GetCiv": () => "iber",
+ "GetDisabledTemplates": () => ({}),
+ "GetPlayerID": () => playerID
+});
+
+AddMock(entityID, IID_Ownership, {
+ "GetOwner": () => playerID
+});
+
+AddMock(entityID, IID_Identity, {
+ "GetCiv": () => "iber"
+});
+
+cmpTrainer.CalculateEntitiesMap();
+TS_ASSERT_UNEVAL_EQUALS(
+ cmpTrainer.GetEntitiesList(),
+ ["units/iber/cavalry_javelineer_b", "units/iber/infantry_swordsman_b", "units/iber/support_female_citizen"]
+);
+
+AddMock(SYSTEM_ENTITY, IID_TemplateManager, {
+ "TemplateExists": name => name == "units/iber/support_female_citizen",
+ "GetTemplate": name => ({})
+});
+
+cmpTrainer.CalculateEntitiesMap();
+TS_ASSERT_UNEVAL_EQUALS(cmpTrainer.GetEntitiesList(), ["units/iber/support_female_citizen"]);
+
+AddMock(SYSTEM_ENTITY, IID_TemplateManager, {
+ "TemplateExists": () => true,
+ "GetTemplate": name => ({})
+});
+
+AddMock(playerEntityID, IID_Player, {
+ "GetCiv": () => "iber",
+ "GetDisabledTemplates": () => ({ "units/athen/infantry_swordsman_b": true }),
+ "GetPlayerID": () => playerID
+});
+
+cmpTrainer.CalculateEntitiesMap();
+TS_ASSERT_UNEVAL_EQUALS(
+ cmpTrainer.GetEntitiesList(),
+ ["units/iber/cavalry_javelineer_b", "units/iber/infantry_swordsman_b", "units/iber/support_female_citizen"]
+);
+
+AddMock(playerEntityID, IID_Player, {
+ "GetCiv": () => "iber",
+ "GetDisabledTemplates": () => ({ "units/iber/infantry_swordsman_b": true }),
+ "GetPlayerID": () => playerID
+});
+
+cmpTrainer.CalculateEntitiesMap();
+TS_ASSERT_UNEVAL_EQUALS(
+ cmpTrainer.GetEntitiesList(),
+ ["units/iber/cavalry_javelineer_b", "units/iber/support_female_citizen"]
+);
+
+AddMock(playerEntityID, IID_Player, {
+ "GetCiv": () => "athen",
+ "GetDisabledTemplates": () => ({ "units/athen/infantry_swordsman_b": true }),
+ "GetPlayerID": () => playerID
+});
+
+cmpTrainer.CalculateEntitiesMap();
+TS_ASSERT_UNEVAL_EQUALS(
+ cmpTrainer.GetEntitiesList(),
+ ["units/athen/cavalry_javelineer_b", "units/iber/support_female_citizen"]
+);
+
+AddMock(playerEntityID, IID_Player, {
+ "GetCiv": () => "iber",
+ "GetDisabledTemplates": () => ({ "units/iber/infantry_swordsman_b": false }),
+ "GetPlayerID": () => playerID
+});
+
+cmpTrainer.CalculateEntitiesMap();
+TS_ASSERT_UNEVAL_EQUALS(
+ cmpTrainer.GetEntitiesList(),
+ ["units/iber/cavalry_javelineer_b", "units/iber/infantry_swordsman_b", "units/iber/support_female_citizen"]
+);
+
+
+// Test Queuing a unit.
+const queuedUnit = "units/iber/infantry_swordsman_b";
+const cost = {
+ "food": 10
+};
+
+AddMock(SYSTEM_ENTITY, IID_TemplateManager, {
+ "TemplateExists": () => true,
+ "GetTemplate": name => ({
+ "Cost": {
+ "BuildTime": 1,
+ "Population": 1,
+ "Resources": cost
+ },
+ "TrainingRestrictions": {
+ "Category": "some_limit",
+ "MatchLimit": "7"
+ }
+ })
+});
+AddMock(SYSTEM_ENTITY, IID_Trigger, {
+ "CallEvent": () => {}
+});
+AddMock(SYSTEM_ENTITY, IID_GuiInterface, {
+ "PushNotification": () => {},
+ "SetSelectionDirty": () => {}
+});
+
+const cmpPlayer = AddMock(playerEntityID, IID_Player, {
+ "BlockTraining": () => {},
+ "GetCiv": () => "iber",
+ "GetPlayerID": () => playerID,
+ "RefundResources": (resources) => {
+ TS_ASSERT_UNEVAL_EQUALS(resources, cost);
+ },
+ "TrySubtractResources": (resources) => {
+ TS_ASSERT_UNEVAL_EQUALS(resources, cost);
+ // Just have enough resources.
+ return true;
+ },
+ "TryReservePopulationSlots": () => false, // Always have pop space.
+ "UnReservePopulationSlots": () => {}, // Always have pop space.
+ "UnBlockTraining": () => {},
+ "GetDisabledTemplates": () => ({})
+});
+const spyCmpPlayerSubtract = new Spy(cmpPlayer, "TrySubtractResources");
+const spyCmpPlayerRefund = new Spy(cmpPlayer, "RefundResources");
+const spyCmpPlayerPop = new Spy(cmpPlayer, "TryReservePopulationSlots");
+
+ConstructComponent(playerEntityID, "EntityLimits", {
+ "Limits": {
+ "some_limit": 0
+ },
+ "LimitChangers": {},
+ "LimitRemovers": {}
+});
+// Test that we can't exceed the entity limit.
+TS_ASSERT_EQUALS(cmpTrainer.QueueBatch(queuedUnit, 1), -1);
+// And that in that case, the resources are not lost.
+// ToDo: This is a bad test, it relies on the order of subtraction in the cmp.
+// Better would it be to check the states before and after the queue.
+TS_ASSERT_EQUALS(spyCmpPlayerSubtract._called, spyCmpPlayerRefund._called);
+
+ConstructComponent(playerEntityID, "EntityLimits", {
+ "Limits": {
+ "some_limit": 5
+ },
+ "LimitChangers": {},
+ "LimitRemovers": {}
+});
+let id = cmpTrainer.QueueBatch(queuedUnit, 1);
+TS_ASSERT_EQUALS(spyCmpPlayerSubtract._called, 2);
+TS_ASSERT_EQUALS(cmpTrainer.queue.size, 1);
+
+
+// Test removing a queued batch.
+cmpTrainer.StopBatch(id);
+TS_ASSERT_EQUALS(spyCmpPlayerRefund._called, 2);
+TS_ASSERT_EQUALS(cmpTrainer.queue.size, 0);
+
+const cmpEntLimits = QueryOwnerInterface(entityID, IID_EntityLimits);
+TS_ASSERT(cmpEntLimits.AllowedToTrain("some_limit", 5));
+
+
+// Test finishing a queued batch.
+id = cmpTrainer.QueueBatch(queuedUnit, 1);
+TS_ASSERT(cmpEntLimits.AllowedToTrain("some_limit", 4));
+TS_ASSERT_EQUALS(cmpTrainer.GetBatch(id).progress, 0);
+TS_ASSERT_EQUALS(cmpTrainer.Progress(id, 500), 500);
+TS_ASSERT_EQUALS(spyCmpPlayerPop._called, 1);
+TS_ASSERT_EQUALS(cmpTrainer.GetBatch(id).progress, 0.5);
+
+const spawedEntityIDs = [4, 5, 6, 7, 8];
+let spawned = 0;
+
+Engine.AddEntity = () => {
+ const ent = spawedEntityIDs[spawned++];
+
+ ConstructComponent(ent, "TrainingRestrictions", {
+ "Category": "some_limit"
+ });
+
+ AddMock(ent, IID_Identity, {
+ "GetClassesList": () => []
+ });
+
+ AddMock(ent, IID_Position, {
+ "JumpTo": () => {}
+ });
+
+ AddMock(ent, IID_Ownership, {
+ "SetOwner": (pid) => {
+ QueryOwnerInterface(ent, IID_EntityLimits).OnGlobalOwnershipChanged({
+ "entity": ent,
+ "from": -1,
+ "to": pid
+ });
+ },
+ "GetOwner": () => playerID
+ });
+
+ return ent;
+};
+AddMock(entityID, IID_Footprint, {
+ "PickSpawnPoint": () => ({ "x": 0, "y": 1, "z": 0 })
+});
+
+TS_ASSERT_EQUALS(cmpTrainer.Progress(id, 1000), 500);
+TS_ASSERT(!cmpTrainer.HasBatch(id));
+TS_ASSERT(!cmpEntLimits.AllowedToTrain("some_limit", 5));
+TS_ASSERT(cmpEntLimits.AllowedToTrain("some_limit", 4));
+
+TS_ASSERT_EQUALS(cmpEntLimits.GetCounts().some_limit, 1);
+TS_ASSERT_EQUALS(cmpEntLimits.GetMatchCounts()["units/iber/infantry_swordsman_b"], 1);
+
+
+// Now check that it doesn't get updated when the spawn doesn't succeed. (regression_test_d1879)
+cmpPlayer.TrySubtractResources = () => true;
+cmpPlayer.RefundResources = () => {};
+AddMock(entityID, IID_Footprint, {
+ "PickSpawnPoint": () => ({ "x": -1, "y": -1, "z": -1 })
+});
+id = cmpTrainer.QueueBatch(queuedUnit, 2);
+TS_ASSERT_EQUALS(cmpTrainer.Progress(id, 2000), 2000);
+TS_ASSERT(cmpTrainer.HasBatch(id));
+
+TS_ASSERT_EQUALS(cmpEntLimits.GetCounts().some_limit, 3);
+TS_ASSERT_EQUALS(cmpEntLimits.GetMatchCounts()["units/iber/infantry_swordsman_b"], 3);
+
+// Check that when the batch is removed the counts are subtracted again.
+cmpTrainer.StopBatch(id);
+TS_ASSERT_EQUALS(cmpEntLimits.GetCounts().some_limit, 1);
+TS_ASSERT_EQUALS(cmpEntLimits.GetMatchCounts()["units/iber/infantry_swordsman_b"], 1);
+
+const queuedSecondUnit = "units/iber/cavalry_javelineer_b";
+// Check changing the allowed entities has effect.
+const id1 = cmpTrainer.QueueBatch(queuedUnit, 1);
+const id2 = cmpTrainer.QueueBatch(queuedSecondUnit, 1);
+TS_ASSERT_EQUALS(cmpTrainer.queue.size, 2);
+TS_ASSERT_EQUALS(cmpTrainer.GetBatch(id1).unitTemplate, queuedUnit);
+TS_ASSERT_EQUALS(cmpTrainer.GetBatch(id2).unitTemplate, queuedSecondUnit);
+
+// Add a modifier that replaces unit A with unit C,
+// adds a unit D and removes unit B from the roster.
+Engine.RegisterGlobal("ApplyValueModificationsToEntity", (_, val) => {
+ return HandleTokens(val, "units/{civ}/cavalry_javelineer_b>units/{civ}/c units/{civ}/d -units/{civ}/infantry_swordsman_b");
+});
+
+cmpTrainer.OnValueModification({
+ "component": "Trainer",
+ "valueNames": ["Trainer/Entities/_string"],
+ "entities": [entityID]
+});
+
+TS_ASSERT_UNEVAL_EQUALS(
+ cmpTrainer.GetEntitiesList(), ["units/iber/c", "units/iber/support_female_citizen", "units/iber/d"]
+);
+TS_ASSERT_EQUALS(cmpTrainer.queue.size, 1);
+TS_ASSERT_EQUALS(cmpTrainer.GetBatch(id1), undefined);
+TS_ASSERT_EQUALS(cmpTrainer.GetBatch(id2).unitTemplate, "units/iber/c");
Property changes on: ps/trunk/binaries/data/mods/public/simulation/components/tests/test_Trainer.js
___________________________________________________________________
Added: svn:eol-style
## -0,0 +1 ##
+native
\ No newline at end of property
Added: svn:mime-type
## -0,0 +1 ##
+text/plain
\ No newline at end of property
Index: ps/trunk/binaries/data/mods/public/simulation/data/auras/teambonuses/gaul_player_teambonus.json
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/data/auras/teambonuses/gaul_player_teambonus.json (revision 25999)
+++ ps/trunk/binaries/data/mods/public/simulation/data/auras/teambonuses/gaul_player_teambonus.json (revision 26000)
@@ -1,14 +1,14 @@
{
"type": "global",
"affects": ["Forge"],
"affectedPlayers": ["MutualAlly"],
"modifications": [
- { "value": "ProductionQueue/TechCostMultiplier/food", "multiply": 0.85 },
- { "value": "ProductionQueue/TechCostMultiplier/wood", "multiply": 0.85 },
- { "value": "ProductionQueue/TechCostMultiplier/stone", "multiply": 0.85 },
- { "value": "ProductionQueue/TechCostMultiplier/metal", "multiply": 0.85 },
- { "value": "ProductionQueue/TechCostMultiplier/time", "multiply": 0.85 }
+ { "value": "Researcher/TechCostMultiplier/food", "multiply": 0.85 },
+ { "value": "Researcher/TechCostMultiplier/wood", "multiply": 0.85 },
+ { "value": "Researcher/TechCostMultiplier/stone", "multiply": 0.85 },
+ { "value": "Researcher/TechCostMultiplier/metal", "multiply": 0.85 },
+ { "value": "Researcher/TechCostMultiplier/time", "multiply": 0.85 }
],
"auraName": "Products from Gaul",
"auraDescription": "Forges −15% technology resource costs and research time."
}
Index: ps/trunk/binaries/data/mods/public/simulation/data/auras/units/catafalques/athen_catafalque_2.json
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/data/auras/units/catafalques/athen_catafalque_2.json (revision 25999)
+++ ps/trunk/binaries/data/mods/public/simulation/data/auras/units/catafalques/athen_catafalque_2.json (revision 26000)
@@ -1,12 +1,12 @@
{
"type": "global",
"affects": ["Economic"],
"modifications": [
- { "value": "ProductionQueue/TechCostMultiplier/food", "multiply": 0.9 },
- { "value": "ProductionQueue/TechCostMultiplier/wood", "multiply": 0.9 },
- { "value": "ProductionQueue/TechCostMultiplier/stone", "multiply": 0.9 },
- { "value": "ProductionQueue/TechCostMultiplier/metal", "multiply": 0.9 }
+ { "value": "Researcher/TechCostMultiplier/food", "multiply": 0.9 },
+ { "value": "Researcher/TechCostMultiplier/wood", "multiply": 0.9 },
+ { "value": "Researcher/TechCostMultiplier/stone", "multiply": 0.9 },
+ { "value": "Researcher/TechCostMultiplier/metal", "multiply": 0.9 }
],
"auraName": "Economic Fortune",
"auraDescription": "Solon brought in a new system of weights and measures, fathers were encouraged to find trades for their sons.\nEconomic technologies −10% resource costs."
}
Index: ps/trunk/binaries/data/mods/public/simulation/data/auras/units/catafalques/sele_catafalque_1.json
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/data/auras/units/catafalques/sele_catafalque_1.json (revision 25999)
+++ ps/trunk/binaries/data/mods/public/simulation/data/auras/units/catafalques/sele_catafalque_1.json (revision 26000)
@@ -1,16 +1,16 @@
{
"type": "global",
"affects": ["Temple"],
"modifications": [
{ "value": "Cost/Resources/food", "multiply": 0.9 },
{ "value": "Cost/Resources/wood", "multiply": 0.9 },
{ "value": "Cost/Resources/stone", "multiply": 0.9 },
{ "value": "Cost/Resources/metal", "multiply": 0.9 },
- { "value": "ProductionQueue/TechCostMultiplier/food", "multiply": 0.9 },
- { "value": "ProductionQueue/TechCostMultiplier/wood", "multiply": 0.9 },
- { "value": "ProductionQueue/TechCostMultiplier/stone", "multiply": 0.9 },
- { "value": "ProductionQueue/TechCostMultiplier/metal", "multiply": 0.9 }
+ { "value": "Researcher/TechCostMultiplier/food", "multiply": 0.9 },
+ { "value": "Researcher/TechCostMultiplier/wood", "multiply": 0.9 },
+ { "value": "Researcher/TechCostMultiplier/stone", "multiply": 0.9 },
+ { "value": "Researcher/TechCostMultiplier/metal", "multiply": 0.9 }
],
"auraName": "Founder of the Ezida Temple",
"auraDescription": "Antiochus I laid the foundation for the Ezida Temple in Borsippa.\nTemples −10% resource costs; Temple technologies −10% resource costs."
}
Index: ps/trunk/binaries/data/mods/public/simulation/data/auras/units/heroes/maur_hero_ashoka.json
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/data/auras/units/heroes/maur_hero_ashoka.json (revision 25999)
+++ ps/trunk/binaries/data/mods/public/simulation/data/auras/units/heroes/maur_hero_ashoka.json (revision 26000)
@@ -1,17 +1,17 @@
{
"type": "global",
"affects": ["Temple"],
"modifications": [
{ "value": "Cost/BuildTime", "multiply": 0.5 },
{ "value": "Cost/Resources/wood", "multiply": 0.5 },
{ "value": "Cost/Resources/stone", "multiply": 0.5 },
- { "value": "ProductionQueue/TechCostMultiplier/food", "multiply": 0.5 },
- { "value": "ProductionQueue/TechCostMultiplier/wood", "multiply": 0.5 },
- { "value": "ProductionQueue/TechCostMultiplier/stone", "multiply": 0.5 },
- { "value": "ProductionQueue/TechCostMultiplier/metal", "multiply": 0.5 },
- { "value": "ProductionQueue/TechCostMultiplier/time", "multiply": 0.5 }
+ { "value": "Researcher/TechCostMultiplier/food", "multiply": 0.5 },
+ { "value": "Researcher/TechCostMultiplier/wood", "multiply": 0.5 },
+ { "value": "Researcher/TechCostMultiplier/stone", "multiply": 0.5 },
+ { "value": "Researcher/TechCostMultiplier/metal", "multiply": 0.5 },
+ { "value": "Researcher/TechCostMultiplier/time", "multiply": 0.5 }
],
"auraDescription": "Temples −50% resource costs and build time. Temple technologies −50% resource costs and research time.",
"auraName": "Buddhism",
"overlayIcon": "art/textures/ui/session/auras/build_bonus.png"
}
Index: ps/trunk/binaries/data/mods/public/simulation/data/technologies/hoplite_tradition.json
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/data/technologies/hoplite_tradition.json (revision 25999)
+++ ps/trunk/binaries/data/mods/public/simulation/data/technologies/hoplite_tradition.json (revision 26000)
@@ -1,29 +1,29 @@
-{
- "genericName": "Hoplite Tradition",
- "description": "Hoplite soldiers constituted most of the armies of Greece.",
- "cost": {
- "food": 400,
- "metal": 300
- },
- "requirements": {
- "all": [
- { "tech": "phase_town" },
- {
- "any": [
- { "civ": "athen" },
- { "civ": "spart" }
- ]
- }
- ]
- },
- "requirementsTooltip": "Unlocked in Town Phase.",
- "icon": "armor_corinthian.png",
- "researchTime": 60,
- "tooltip": "Hoplites −25% training time and −50% promotion experience.",
- "modifications": [
- { "value": "Cost/BuildTime", "multiply": 0.75 },
- { "value": "Promotion/RequiredXp", "multiply": 0.5 }
- ],
- "affects": ["Infantry Spearman !Hero"],
- "soundComplete": "interface/alarm/alarm_upgradearmory.xml"
-}
+{
+ "genericName": "Hoplite Tradition",
+ "description": "Hoplite soldiers constituted most of the armies of Greece.",
+ "cost": {
+ "food": 400,
+ "metal": 300
+ },
+ "requirements": {
+ "all": [
+ { "tech": "phase_town" },
+ {
+ "any": [
+ { "civ": "athen" },
+ { "civ": "spart" }
+ ]
+ }
+ ]
+ },
+ "requirementsTooltip": "Unlocked in Town Phase.",
+ "icon": "armor_corinthian.png",
+ "researchTime": 60,
+ "tooltip": "Hoplites −25% training time and −50% promotion experience.",
+ "modifications": [
+ { "value": "Cost/BuildTime", "multiply": 0.75 },
+ { "value": "Promotion/RequiredXp", "multiply": 0.5 }
+ ],
+ "affects": ["Infantry Spearman !Hero"],
+ "soundComplete": "interface/alarm/alarm_upgradearmory.xml"
+}
Index: ps/trunk/binaries/data/mods/public/simulation/helpers/Commands.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/helpers/Commands.js (revision 25999)
+++ ps/trunk/binaries/data/mods/public/simulation/helpers/Commands.js (revision 26000)
@@ -1,1864 +1,1857 @@
// 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;
}
- var queue = Engine.QueryInterface(ent, IID_ProductionQueue);
+ 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 (queue && data.cmpPlayer.IsAI())
- {
- var list = queue.GetEntitiesList();
- if (list.indexOf(cmd.template) === -1 && cmd.promoted)
- {
- for (var promoted of cmd.promoted)
- {
- if (list.indexOf(promoted) === -1)
- continue;
- cmd.template = promoted;
- break;
- }
- }
- }
- if (queue && queue.GetEntitiesList().indexOf(cmd.template) != -1)
- queue.AddItem(cmd.template, "unit", +cmd.count, cmd.metadata, cmd.pushFront);
+ 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.SetState("defeated", 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 (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;
let cmpIdentity = Engine.QueryInterface(ent, IID_Identity);
// TODO: We only check if the formation is usable by some units
// if we move them to it. We should check if we can use formations
// for the other cases.
let nullFormation = (formationTemplate || cmpUnitAI.GetFormationTemplate()) == NULL_FORMATION;
if (nullFormation || !cmpIdentity || !cmpIdentity.CanUseFormation(formationTemplate || NULL_FORMATION))
{
if (nullFormation && cmpUnitAI.GetFormationController())
cmpUnitAI.LeaveFormation(cmd.queued || false);
nonformedUnitAIs.push(cmpUnitAI);
}
else
formedUnits.push(ent);
}
if (nonformedUnitAIs.length == ents.length)
{
// No units support the formation.
return nonformedUnitAIs;
}
if (!formationUnitAIs.length)
{
// We need to give the selected units a new formation controller.
// TODO replace the fixed 60 with something sensible, based on vision range f.e.
let formationSeparation = 60;
let clusters = ClusterEntities(formedUnits, formationSeparation);
let formationEnts = [];
for (let cluster of clusters)
{
RemoveFromFormation(cluster);
if (!formationTemplate || !CanMoveEntsIntoFormation(cluster, formationTemplate))
{
for (let ent of cluster)
nonformedUnitAIs.push(Engine.QueryInterface(ent, IID_UnitAI));
continue;
}
// Create the new controller.
let formationEnt = Engine.AddEntity(formationTemplate);
let cmpFormation = Engine.QueryInterface(formationEnt, IID_Formation);
formationUnitAIs.push(Engine.QueryInterface(formationEnt, IID_UnitAI));
cmpFormation.SetFormationSeparation(formationSeparation);
cmpFormation.SetMembers(cluster);
for (let ent of formationEnts)
cmpFormation.RegisterTwinFormation(ent);
formationEnts.push(formationEnt);
let cmpOwnership = Engine.QueryInterface(formationEnt, IID_Ownership);
cmpOwnership.SetOwner(player);
}
}
return nonformedUnitAIs.concat(formationUnitAIs);
}
/**
* Group a list of entities in clusters via single-links
*/
function ClusterEntities(ents, separationDistance)
{
let clusters = [];
if (!ents.length)
return clusters;
let distSq = separationDistance * separationDistance;
let positions = [];
// triangular matrix with the (squared) distances between the different clusters
// the other half is not initialised
let matrix = [];
for (let i = 0; i < ents.length; ++i)
{
matrix[i] = [];
clusters.push([ents[i]]);
let cmpPosition = Engine.QueryInterface(ents[i], IID_Position);
positions.push(cmpPosition.GetPosition2D());
for (let j = 0; j < i; ++j)
matrix[i][j] = positions[i].distanceToSquared(positions[j]);
}
while (clusters.length > 1)
{
// search two clusters that are closer than the required distance
let closeClusters = undefined;
for (let i = matrix.length - 1; i >= 0 && !closeClusters; --i)
for (let j = i - 1; j >= 0 && !closeClusters; --j)
if (matrix[i][j] < distSq)
closeClusters = [i,j];
// if no more close clusters found, just return all found clusters so far
if (!closeClusters)
return clusters;
// make a new cluster with the entities from the two found clusters
let newCluster = clusters[closeClusters[0]].concat(clusters[closeClusters[1]]);
// calculate the minimum distance between the new cluster and all other remaining
// clusters by taking the minimum of the two distances.
let distances = [];
for (let i = 0; i < clusters.length; ++i)
{
let a = closeClusters[1];
let b = closeClusters[0];
if (i == a || i == b)
continue;
let dist1 = matrix[a][i] !== undefined ? matrix[a][i] : matrix[i][a];
let dist2 = matrix[b][i] !== undefined ? matrix[b][i] : matrix[i][b];
distances.push(Math.min(dist1, dist2));
}
// remove the rows and columns in the matrix for the merged clusters,
// and the clusters themselves from the cluster list
clusters.splice(closeClusters[0],1);
clusters.splice(closeClusters[1],1);
matrix.splice(closeClusters[0],1);
matrix.splice(closeClusters[1],1);
for (let i = 0; i < matrix.length; ++i)
{
if (matrix[i].length > closeClusters[0])
matrix[i].splice(closeClusters[0],1);
if (matrix[i].length > closeClusters[1])
matrix[i].splice(closeClusters[1],1);
}
// add a new row of distances to the matrix and the new cluster
clusters.push(newCluster);
matrix.push(distances);
}
return clusters;
}
function GetFormationRequirements(formationTemplate)
{
var template = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager).GetTemplate(formationTemplate);
if (!template.Formation)
return false;
return { "minCount": +template.Formation.RequiredMemberCount };
}
function CanMoveEntsIntoFormation(ents, formationTemplate)
{
// TODO: should check the player's civ is allowed to use this formation
// See simulation/components/Player.js GetFormations() for a list of all allowed formations
var requirements = GetFormationRequirements(formationTemplate);
if (!requirements)
return false;
var count = 0;
for (let ent of ents)
{
var cmpIdentity = Engine.QueryInterface(ent, IID_Identity);
if (!cmpIdentity || !cmpIdentity.CanUseFormation(formationTemplate))
continue;
++count;
}
return count >= requirements.minCount;
}
/**
* Check if player can control this entity
* returns: true if the entity is owned by the player and controllable
* or control all units is activated, else false
*/
function CanControlUnit(entity, player, controlAll)
{
let cmpIdentity = Engine.QueryInterface(entity, IID_Identity);
let canBeControlled = IsOwnedByPlayer(player, entity) &&
(!cmpIdentity || cmpIdentity.IsControllable()) ||
controlAll;
if (!canBeControlled)
notifyOrderFailure(entity, player);
return canBeControlled;
}
/**
* @param {number} entity - The entityID to verify.
* @param {number} player - The playerID to check against.
* @return {boolean}.
*/
function IsOwnedByPlayerOrMutualAlly(entity, player)
{
return IsOwnedByPlayer(player, entity) || IsOwnedByMutualAllyOfPlayer(player, entity);
}
/**
* Check if player can control this entity
* @return {boolean} - True if the entity is valid and controlled by the player
* or the entity is owned by an mutualAlly and can be controlled
* or control all units is activated, else false.
*/
function CanPlayerOrAllyControlUnit(entity, player, controlAll)
{
return CanControlUnit(player, entity, controlAll) ||
IsOwnedByMutualAllyOfPlayer(player, entity) && CanOwnerControlEntity(entity);
}
/**
* @return {boolean} - Whether the owner of this entity can control the entity.
*/
function CanOwnerControlEntity(entity)
{
let cmpOwner = QueryOwnerInterface(entity);
return cmpOwner && CanControlUnit(entity, cmpOwner.GetPlayerID());
}
/**
* Filter entities which the player can control.
*/
function FilterEntityList(entities, player, controlAll)
{
return entities.filter(ent => CanControlUnit(ent, player, controlAll));
}
/**
* Filter entities which the player can control or are mutualAlly
*/
function FilterEntityListWithAllies(entities, player, controlAll)
{
return entities.filter(ent => CanPlayerOrAllyControlUnit(ent, player, controlAll));
}
/**
* Incur the player with the cost of a bribe, optionally multiply the cost with
* the additionalMultiplier
*/
function IncurBribeCost(template, player, playerBribed, failedBribe)
{
let cmpPlayerBribed = QueryPlayerIDInterface(playerBribed);
if (!cmpPlayerBribed)
return false;
let costs = {};
// Additional cost for this owner
let multiplier = cmpPlayerBribed.GetSpyCostMultiplier();
if (failedBribe)
multiplier *= template.VisionSharing.FailureCostRatio;
for (let res in template.Cost.Resources)
costs[res] = Math.floor(multiplier * ApplyValueModificationsToTemplate("Cost/Resources/" + res, +template.Cost.Resources[res], player, template));
let cmpPlayer = QueryPlayerIDInterface(player);
return cmpPlayer && cmpPlayer.TrySubtractResources(costs);
}
Engine.RegisterGlobal("GetFormationRequirements", GetFormationRequirements);
Engine.RegisterGlobal("CanMoveEntsIntoFormation", CanMoveEntsIntoFormation);
Engine.RegisterGlobal("GetDockAngle", GetDockAngle);
Engine.RegisterGlobal("ProcessCommand", ProcessCommand);
Engine.RegisterGlobal("g_Commands", g_Commands);
Engine.RegisterGlobal("IncurBribeCost", IncurBribeCost);
Index: ps/trunk/binaries/data/mods/public/simulation/templates/campaigns/campaign_religious_test.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/campaigns/campaign_religious_test.xml (revision 25999)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/campaigns/campaign_religious_test.xml (revision 26000)
@@ -1,27 +1,29 @@
12.0
2200
athen
Religious Sanctuary
Greek Religious Sanctuary
+
true
100
65535
+
structures/hellenes/temple.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/skirmish/structures/default_arsenal.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/skirmish/structures/default_arsenal.xml (revision 25999)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/skirmish/structures/default_arsenal.xml (revision 26000)
@@ -1,13 +1,15 @@
skirm
+
structures/{civ}/arsenal
+
structures/hellenes/workshop.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/skirmish/structures/default_corral.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/skirmish/structures/default_corral.xml (revision 25999)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/skirmish/structures/default_corral.xml (revision 26000)
@@ -1,19 +1,21 @@
skirm
+
structures/{civ}/corral
+
structures/hellenes/corral.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/skirmish/structures/default_fortress.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/skirmish/structures/default_fortress.xml (revision 25999)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/skirmish/structures/default_fortress.xml (revision 26000)
@@ -1,13 +1,15 @@
skirm
+
structures/{civ}/fortress
+
structures/athenians/fortress.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/skirmish/structures/default_range.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/skirmish/structures/default_range.xml (revision 25999)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/skirmish/structures/default_range.xml (revision 26000)
@@ -1,19 +1,21 @@
skirm
+
structures/{civ}/range
+
structures/hellenes/range.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/skirmish/structures/default_market.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/skirmish/structures/default_market.xml (revision 25999)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/skirmish/structures/default_market.xml (revision 26000)
@@ -1,13 +1,15 @@
skirm
+
structures/{civ}/market
+
structures/hellenes/market.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/skirmish/structures/default_house_5.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/skirmish/structures/default_house_5.xml (revision 25999)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/skirmish/structures/default_house_5.xml (revision 26000)
@@ -1,18 +1,20 @@
skirm
Changes in a 5-pop house for civilisations with those houses, is deleted for other civs
+
+
structures/ptolemies/house.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/skirmish/structures/default_house_10.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/skirmish/structures/default_house_10.xml (revision 25999)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/skirmish/structures/default_house_10.xml (revision 26000)
@@ -1,12 +1,14 @@
skirm
Changes in a 10-pop house for civilisations with those houses, is deleted for other civs
+
+
structures/hellenes/house.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/skirmish/structures/default_stable.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/skirmish/structures/default_stable.xml (revision 25999)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/skirmish/structures/default_stable.xml (revision 26000)
@@ -1,19 +1,21 @@
skirm
+
structures/{civ}/stable
+
structures/hellenes/stable.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/athen/civil_centre.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/structures/athen/civil_centre.xml (revision 25999)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/athen/civil_centre.xml (revision 26000)
@@ -1,20 +1,20 @@
decay|rubble/rubble_hele_cc
athen
Agora
-
+
units/{civ}/infantry_spearman_b
units/{civ}/infantry_slinger_b
units/{civ}/cavalry_javelineer_b
-
+
structures/athenians/civil_centre.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/bench.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/structures/bench.xml (revision 25999)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/bench.xml (revision 26000)
@@ -1,28 +1,30 @@
4.0
75
Bench
Wooden Bench
gaia/special_fence.png
+
6.0
+
props/special/eyecandy/bench_1.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/cart/civil_centre.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/structures/cart/civil_centre.xml (revision 25999)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/cart/civil_centre.xml (revision 26000)
@@ -1,23 +1,25 @@
decay|rubble/rubble_kart_cc
cart
Merkāz
-
+
+
+ colonization
+
+
+
units/{civ}/infantry_spearman_b
units/{civ}/infantry_archer_b
units/{civ}/cavalry_javelineer_b
-
- colonization
-
-
+
structures/carthaginians/civil_centre.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/cart/embassy_iberian.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/structures/cart/embassy_iberian.xml (revision 25999)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/cart/embassy_iberian.xml (revision 26000)
@@ -1,31 +1,31 @@
200
cart
Iberian Embassy
Train Iberian Mercenaries and research Mercenary technologies.
CivSpecific
structures/embassy_iberian.png
40
-
+
units/{native}/infantry_javelineer_iber_b
units/{native}/infantry_slinger_iber_b
units/{native}/cavalry_swordsman_iber_b
-
+
structures/carthaginians/embassy_iberian.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/cart/temple.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/structures/cart/temple.xml (revision 25999)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/cart/temple.xml (revision 26000)
@@ -1,23 +1,23 @@
12.0
cart
Maqdaš
Train Healers and Champion Infantry and research healing technologies.
-
+
units/{civ}/champion_infantry
-
+
structures/carthaginians/temple_big.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/column_doric_fallen.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/structures/column_doric_fallen.xml (revision 25999)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/column_doric_fallen.xml (revision 26000)
@@ -1,28 +1,30 @@
4.0
220
Column
Fallen Doric Column
gaia/special_fence.png
20
+
6.0
+
props/special/eyecandy/column_doric_fallen.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/fence_stone.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/structures/fence_stone.xml (revision 25999)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/fence_stone.xml (revision 26000)
@@ -1,30 +1,32 @@
4.0
220
Stone Fence
Stone Fence
gaia/special_fence.png
20
+
6.0
+
props/special/eyecandy/fence_stone_medit.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/hellenic_epic_temple.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/structures/hellenic_epic_temple.xml (revision 25999)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/hellenic_epic_temple.xml (revision 26000)
@@ -1,46 +1,48 @@
500
0
0
1000
500
12.0
30
5000
Epic Temple
Naos Parthenos
Garrison units to heal them at a quick rate.
200
500
+
60
65535
+
80
structures/fndn_6x6.xml
structures/hellenes/temple_epic.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/iber/fortress.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/structures/iber/fortress.xml (revision 25999)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/iber/fortress.xml (revision 26000)
@@ -1,24 +1,24 @@
iber
Castro
Train Heroes. Garrison Soldiers for additional arrows.
-
+
units/{civ}/hero_caros
units/{civ}/hero_indibil
units/{civ}/hero_viriato
-
+
structures/iberians/fortress.xml
24.5
Index: ps/trunk/binaries/data/mods/public/simulation/templates/skirmish/structures/default_wonder.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/skirmish/structures/default_wonder.xml (revision 25999)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/skirmish/structures/default_wonder.xml (revision 26000)
@@ -1,19 +1,21 @@
skirm
+
structures/{civ}/wonder
+
structures/hellenes/temple_epic.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/athen/prytaneion.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/structures/athen/prytaneion.xml (revision 25999)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/athen/prytaneion.xml (revision 26000)
@@ -1,65 +1,67 @@
Council
200
100
200
8.0
2000
athen
Council Chamber
Prytaneion
Train Heroes and research technologies.
ConquestCritical CivSpecific
Council
structures/tholos.png
20
40
-
- 0.7
-
- units/{civ}/hero_themistocles
- units/{civ}/hero_pericles
- units/{civ}/hero_iphicrates
-
+
long_walls
iphicratean_reforms
-
+
interface/complete/building/complete_tholos.xml
false
38
40000
+
+ 0.7
+
+ units/{civ}/hero_themistocles
+ units/{civ}/hero_pericles
+ units/{civ}/hero_iphicrates
+
+
40
structures/athenians/prytaneion.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/brit/fortress.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/structures/brit/fortress.xml (revision 25999)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/brit/fortress.xml (revision 26000)
@@ -1,38 +1,38 @@
18.0
decay|rubble/rubble_stone_5x5
brit
Dunon
Train Heroes. Garrison Soldiers for additional arrows.
-
-
- units/{civ}/hero_boudicca
- units/{civ}/hero_caratacos
- units/{civ}/hero_cunobelin
-
-
interface/complete/building/complete_broch.xml
+
+
+ units/{civ}/hero_boudicca
+ units/{civ}/hero_caratacos
+ units/{civ}/hero_cunobelin
+
+
structures/britons/fortress.xml
structures/fndn_9x9.xml
28
Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/cart/embassy_celtic.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/structures/cart/embassy_celtic.xml (revision 25999)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/cart/embassy_celtic.xml (revision 26000)
@@ -1,37 +1,37 @@
200
11.0
1200
cart
Celtic Embassy
Train Celtic Mercenaries and research Mercenary technologies.
CivSpecific
structures/embassy_celtic.png
40
-
+
units/{native}/infantry_swordsman_gaul_b
units/{native}/cavalry_swordsman_gaul_b
-
+
structures/carthaginians/embassy_celtic.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/cart/super_dock.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/structures/cart/super_dock.xml (revision 25999)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/cart/super_dock.xml (revision 26000)
@@ -1,94 +1,94 @@
structures/cart_super_dock_repair
own ally neutral
shore
Dock
4
10
500
300
200
8.0
5
0.1
Unit
Support Infantry Cavalry Ship
0
2
5000
cart
Naval Shipyard
Cothon
Build upon a shoreline in own, neutral, or allied territory. Acquire large tracts of territory. Territory root. Construct Warships and research technologies.
ConquestCritical CivSpecific
Naval Shipyard
structures/uber_dock.png
60
40
true
0.0
-
- 0.7
-
- units/{civ}/ship_bireme
- units/{civ}/ship_trireme
- units/{civ}/ship_quinquereme
-
-
ship
35
interface/complete/building/complete_dock.xml
true
200
25000
+
+ 0.7
+
+ units/{civ}/ship_bireme
+ units/{civ}/ship_trireme
+ units/{civ}/ship_quinquereme
+
+
100
structures/carthaginians/super_dock.xml
structures/fndn_dock_super.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/column_doric.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/structures/column_doric.xml (revision 25999)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/column_doric.xml (revision 26000)
@@ -1,28 +1,30 @@
4.0
220
Column
Doric Column
gaia/special_fence.png
20
+
8.0
+
props/special/eyecandy/column_doric.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/fence_short.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/structures/fence_short.xml (revision 25999)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/fence_short.xml (revision 26000)
@@ -1,37 +1,39 @@
4.0
50
Fence
Short Wooden Fence
gaia/special_fence.png
+
6.0
+
true
temp/fence_wood_short_a.xml
6
0.5
Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/gaul/temple.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/structures/gaul/temple.xml (revision 25999)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/gaul/temple.xml (revision 26000)
@@ -1,36 +1,36 @@
300
0
8.0
decay|rubble/rubble_stone_5x5
gaul
Nemeton
60
0
-
+
units/{civ}/champion_fanatic
-
+
structures/celts/temple.xml
structures/fndn_7x7.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/iber/civil_centre.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/structures/iber/civil_centre.xml (revision 25999)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/iber/civil_centre.xml (revision 26000)
@@ -1,20 +1,20 @@
decay|rubble/rubble_iber_cc
iber
Oppidum
-
+
units/{civ}/infantry_swordsman_b
units/{civ}/infantry_javelineer_b
units/{civ}/cavalry_javelineer_b
-
+
structures/iberians/civic_center.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/kush/camp_blemmye.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/structures/kush/camp_blemmye.xml (revision 25999)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/kush/camp_blemmye.xml (revision 26000)
@@ -1,47 +1,47 @@
own neutral
100
100
12.0
1000
decay|rubble/rubble_stone_5x5
kush
Blemmye Camp
Train Blemmye Mercenaries.
CivSpecific
structures/mercenary_camp.png
20
20
-
-
- units/{native}/cavalry_javelineer_merc_b
-
-
1
+
+
+ units/{native}/cavalry_javelineer_merc_b
+
+
structures/mercenaries/camp_blemmye.xml
structures/fndn_8x7.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/skirmish/structures/default_temple.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/skirmish/structures/default_temple.xml (revision 25999)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/skirmish/structures/default_temple.xml (revision 26000)
@@ -1,13 +1,15 @@
skirm
+
structures/{civ}/temple
+
structures/athenians/temple.xml
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 25999)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/athen/gymnasium.xml (revision 26000)
@@ -1,56 +1,56 @@
200
200
200
8.0
10
2000
athen
Gymnasium
Gymnasion
Train Champions.
ConquestCritical CivSpecific
Gymnasium
structures/gymnasium.png
40
40
-
- 0.7
-
- units/{civ}/champion_infantry
- units/{civ}/champion_ranged
-
-
interface/complete/building/complete_gymnasium.xml
+
+ 0.7
+
+ units/{civ}/champion_infantry
+ units/{civ}/champion_ranged
+
+
40
structures/athenians/gymnasium.xml
structures/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 25999)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/brit/crannog.xml (revision 26000)
@@ -1,50 +1,52 @@
own ally neutral
shore
8.0
brit
Island Settlement
Cranogion
Build 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.
CivSpecific
Naval
structures/crannog.png
phase_town
true
0.0
-
+
+ ship
+
+
+
+ -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
-
- -phase_town_{civ}
- -hellenistic_metropolis
-
-
-
- ship
-
+
structures/britons/crannog.xml
structures/fndn_8x8.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/cart/embassy.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/structures/cart/embassy.xml (revision 25999)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/cart/embassy.xml (revision 26000)
@@ -1,44 +1,44 @@
400
200
15.0
decay|rubble/rubble_stone_5x5
cart
Embassy
Train Mercenaries and research Mercenary technologies.
structures/embassy_italic.png
80
40
-
+
units/{native}/infantry_swordsman_gaul_b
units/{native}/cavalry_swordsman_gaul_b
units/{native}/infantry_javelineer_iber_b
units/{native}/infantry_slinger_iber_b
units/{native}/cavalry_swordsman_iber_b
units/{native}/infantry_swordsman_ital_b
units/{native}/cavalry_spearman_ital_b
-
+
structures/carthaginians/embassy.xml
structures/fndn_8x8.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/cart/fortress.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/structures/cart/fortress.xml (revision 25999)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/cart/fortress.xml (revision 26000)
@@ -1,30 +1,30 @@
8.0
cart
Ḥamet
Train Heroes. Garrison Soldiers for additional arrows.
-
+
units/{civ}/hero_hamilcar
units/{civ}/hero_hannibal
units/{civ}/hero_maharbal
-
+
structures/carthaginians/fndn_fortress.xml
structures/carthaginians/fortress.xml
25.0
6.4
Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/celt_longhouse.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/structures/celt_longhouse.xml (revision 25999)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/celt_longhouse.xml (revision 26000)
@@ -1,32 +1,34 @@
300
0
100
100
0
10.0
1000
Longhouse
10
+
+
structures/celts/longhouse.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/fence_long.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/structures/fence_long.xml (revision 25999)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/fence_long.xml (revision 26000)
@@ -1,37 +1,39 @@
4.0
100
Fence
Long Wooden Fence
gaia/special_fence.png
20
+
6.0
+
temp/fence_wood_long_a.xml
12
0.5
Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/gaul/civil_centre.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/structures/gaul/civil_centre.xml (revision 25999)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/gaul/civil_centre.xml (revision 26000)
@@ -1,27 +1,27 @@
8.0
decay|rubble/rubble_gaul_cc
gaul
Lissos
-
+
units/{civ}/infantry_spearman_b
units/{civ}/infantry_javelineer_b
units/{civ}/cavalry_javelineer_b
-
+
structures/gauls/civic_centre.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/hellenic_stoa.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/structures/hellenic_stoa.xml (revision 25999)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/hellenic_stoa.xml (revision 26000)
@@ -1,48 +1,50 @@
Stoa
110
0
0
100
100
10.0
1100
Stoa
Hellenic Stoa
gaia/special_stoa.png
50
50
+
false
36
65535
+
40
special/greek_stoa.xml
structures/fndn_6x4.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/ishtar_gate.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/structures/ishtar_gate.xml (revision 25999)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/ishtar_gate.xml (revision 26000)
@@ -1,60 +1,62 @@
structures/loyalty_regen
200
250
500
8.0
10
2000
Ishtar Gate of Babylon
Territory root.
structures/pers_gate.png
50
100
+
interface/complete/building/complete_broch.xml
true
38
40000
+
40
special/pers_ishtar_gate.xml
structures/fndn_9x4.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/athen/dock.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/structures/athen/dock.xml (revision 25999)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/athen/dock.xml (revision 26000)
@@ -1,28 +1,28 @@
8.0
decay|rubble/rubble_hele_dock
athen
Limēn
-
+
0.7
units/{civ}/infantry_marine_archer_b
units/{civ}/champion_marine
-
+
structures/athenians/dock.xml
structures/fndn_6x4_dock.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/brit/civil_centre.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/structures/brit/civil_centre.xml (revision 25999)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/brit/civil_centre.xml (revision 26000)
@@ -1,28 +1,28 @@
8.0
decay|rubble/rubble_brit_cc
brit
Tigernotreba
-
+
units/{civ}/infantry_spearman_b
units/{civ}/infantry_slinger_b
units/{civ}/cavalry_javelineer_b
-
+
structures/britons/civic_centre.xml
structures/fndn_7x8.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/cart/corral.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/structures/cart/corral.xml (revision 25999)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/cart/corral.xml (revision 26000)
@@ -1,22 +1,22 @@
cart
Rēfet
-
+
-gaia/fauna_cattle_cow_trainable
gaia/fauna_cattle_sanga_trainable
-
+
structures/carthaginians/corral.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/cart/embassy_italic.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/structures/cart/embassy_italic.xml (revision 25999)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/cart/embassy_italic.xml (revision 26000)
@@ -1,37 +1,37 @@
200
12.0
1500
cart
Italic Embassy
Train Italic Mercenaries and research Mercenary technologies.
CivSpecific
structures/embassy_italic.png
40
-
+
units/{native}/infantry_swordsman_ital_b
units/{native}/cavalry_spearman_ital_b
-
+
structures/carthaginians/embassy_italic.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/celt_hut.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/structures/celt_hut.xml (revision 25999)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/celt_hut.xml (revision 26000)
@@ -1,35 +1,37 @@
0
50
0
0
5.0
500
decay|rubble/rubble_stone_2x2
Hut
2
+
7.0
+
structures/celts/hut.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/column_doric_fallen_b.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/structures/column_doric_fallen_b.xml (revision 25999)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/column_doric_fallen_b.xml (revision 26000)
@@ -1,28 +1,30 @@
4.0
110
Column
Fallen Doric Column
gaia/special_fence.png
10
+
6.0
+
props/special/eyecandy/column_doric_fallen_b.xml
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 25999)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/gaul/assembly.xml (revision 26000)
@@ -1,77 +1,77 @@
200
400
10.0
20
0.1
Unit
Support Infantry Cavalry
0
2
2000
decay|rubble/rubble_stone_6x6
gaul
Assembly of Princes
Remogantion
Train Champion Trumpeters and Heroes.
ConquestCritical CivSpecific
City Council
structures/tholos.png
phase_city
80
-
- 0.7
-
- units/{civ}/champion_infantry_trumpeter
- units/{civ}/hero_brennus
- units/{civ}/hero_viridomarus
- units/{civ}/hero_vercingetorix
-
-
20
30
3
interface/complete/building/complete_iber_monument.xml
false
40
40000
+
+ 0.7
+
+ units/{civ}/champion_infantry_trumpeter
+ units/{civ}/hero_brennus
+ units/{civ}/hero_viridomarus
+ units/{civ}/hero_vercingetorix
+
+
40
structures/gauls/theater.xml
structures/fndn_6x6.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/hellenic_propylaea.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/structures/hellenic_propylaea.xml (revision 25999)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/hellenic_propylaea.xml (revision 26000)
@@ -1,44 +1,46 @@
200
0
0
200
200
8.0
2000
Portico
Propylaea
structures/tholos.png
75
75
+
false
40
65535
+
20
structures/hellenes/propylaea.xml
structures/fndn_6x6.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 25999)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/iber/monument.xml (revision 26000)
@@ -1,67 +1,69 @@
structures/iber_monument
Monument
Monument
150
120
100
100
8.0
1200
decay|rubble/rubble_stone_2x2
iber
Revered Monument
Gur Oroigarri
CivSpecific
Monument Town
structures/iberian_bull.png
phase_town
20
20
+
20
30
3
interface/complete/building/complete_iber_monument.xml
+
structures/iberians/sb_1.xml
structures/fndn_2x2.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/kush/camp_noba.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/structures/kush/camp_noba.xml (revision 25999)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/kush/camp_noba.xml (revision 26000)
@@ -1,48 +1,48 @@
own neutral
100
100
12.0
1000
decay|rubble/rubble_stone_5x5
kush
Noba Village
Train Noba Mercenaries.
CivSpecific
structures/mercenary_camp.png
20
20
-
-
- units/{native}/infantry_maceman_merc_b
- units/{native}/infantry_javelineer_merc_b
-
-
1
+
+
+ units/{native}/infantry_maceman_merc_b
+ units/{native}/infantry_javelineer_merc_b
+
+
structures/mercenaries/camp_nuba.xml
structures/fndn_8x8.xml
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 25999)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/kush/pyramid_large.xml (revision 26000)
@@ -1,62 +1,64 @@
structures/kush_pyramids_military
PyramidLarge
300
450
150
20.0
3000
decay|rubble/rubble_stone_6x6
kush
Large Pyramid
mr ʿȝ
-ConquestCritical CivSpecific
City Pyramid
phase_city
structures/kush_pyramid_big.png
90
30
+
interface/complete/building/complete_iber_monument.xml
15.0
false
40
40000
+
40
structures/kushites/pyramid_large.xml
structures/fndn_5x7.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/kush/fortress.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/structures/kush/fortress.xml (revision 25999)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/kush/fortress.xml (revision 26000)
@@ -1,29 +1,29 @@
8.0
kush
Htm
Train Heroes. Garrison Soldiers for additional arrows.
-
+
units/{civ}/hero_nastasen
units/{civ}/hero_amanirenas
units/{civ}/hero_arakamani
-
+
structures/kushites/fortress.xml
28.5
6.7
Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/kush/corral.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/structures/kush/corral.xml (revision 25999)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/kush/corral.xml (revision 26000)
@@ -1,24 +1,24 @@
8.0
kush
ihy
-
+
-gaia/fauna_cattle_cow_trainable
gaia/fauna_cattle_sanga_trainable
-
+
structures/kushites/corral.xml
structures/fndn_4x4.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/kush/civil_centre.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/structures/kush/civil_centre.xml (revision 25999)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/kush/civil_centre.xml (revision 26000)
@@ -1,27 +1,29 @@
10.0
kush
Pr-nsw
-
+
+
+ architecture_kush
+
+
+
units/{civ}/infantry_spearman_b
units/{civ}/infantry_archer_b
units/{civ}/cavalry_javelineer_b
-
- architecture_kush
-
-
+
structures/kushites/civic_centre_kush.xml
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 25999)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/kush/pyramid_small.xml (revision 26000)
@@ -1,59 +1,61 @@
structures/kush_pyramids_economic
PyramidSmall
200
300
100
15.0
2000
decay|rubble/rubble_stone_4x4
kush
Small Pyramid
mr
-ConquestCritical CivSpecific
Town Pyramid
phase_town
structures/kush_pyramid_small.png
60
20
+
interface/complete/building/complete_iber_monument.xml
false
30
30000
+
30
structures/kushites/pyramid_small.xml
structures/fndn_4x5.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 25999)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/kush/temple_amun.xml (revision 26000)
@@ -1,55 +1,55 @@
structures/kush_temple_amun
TempleOfAmun
2
2
2
2
decay|rubble/rubble_stone_6x6
kush
Grand Temple of Amun
Pr-ʿImn
Train Amun Champions and Elite Healers. Research healing technologies.
CivSpecific
-Town City TempleOfAmun
structures/temple_epic.png
phase_city
2
-
+
+ 2
+
+
-units/{civ}/support_healer_b
units/{civ}/support_healer_e
units/{civ}/champion_infantry_amun
-
-
- 2
-
+
structures/kushites/temple_amun.xml
structures/fndn_9x9.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/maur/corral.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/structures/maur/corral.xml (revision 25999)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/maur/corral.xml (revision 26000)
@@ -1,22 +1,22 @@
maur
Gotra
-
+
-gaia/fauna_cattle_cow_trainable
gaia/fauna_cattle_zebu_trainable
-
+
structures/mauryas/corral.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/obelisk.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/structures/obelisk.xml (revision 25999)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/obelisk.xml (revision 26000)
@@ -1,27 +1,29 @@
10.0
1200
Obelisk
Egyptian Obelisk
gaia/special_obelisk.png
200
200
+
+
props/special/eyecandy/obelisk.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/pers/tacara.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/structures/pers/tacara.xml (revision 25999)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/pers/tacara.xml (revision 26000)
@@ -1,55 +1,57 @@
300
200
400
8.0
10
3000
decay|rubble/rubble_stone_6x6
pers
Palace
Taçara
ConquestCritical
Palace
structures/palace.png
40
80
+
interface/complete/building/complete_broch.xml
true
48
40000
+
40
structures/persians/palace.xml
structures/fndn_6x6.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/ptol/lighthouse.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/structures/ptol/lighthouse.xml (revision 25999)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/ptol/lighthouse.xml (revision 26000)
@@ -1,58 +1,60 @@
own ally neutral
shore
Lighthouse
200
200
200
8.0
2000
decay|rubble/rubble_stone_4x6
ptol
Lighthouse
Pharos
Build upon a shoreline in own, neutral, or allied territory. Very large vision range.
CivSpecific
Lighthouse
structures/lighthouse.png
40
40
true
0.0
+
interface/complete/building/complete_temple.xml
+
200
structures/ptolemies/lighthouse.xml
structures/fndn_4x4_dock.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 25999)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/rome/army_camp.xml (revision 26000)
@@ -1,127 +1,127 @@
Bow
10
60
1200
2000
100
1.5
50
false
Human
outline_border.png
outline_border_mask.png
0.175
-
- 3
- 15
- 1
- Soldier
-
neutral enemy
ArmyCamp
ArmyCamp
80
+
+ 3
+ 15
+ 1
+ Soldier
+
3
10.0
3.0
250
500
100
100
12.0
20
0.1
Unit
Support Infantry Cavalry Siege
0
6
2250
decay|rubble/rubble_rome_sb
rome
Army Camp
Castra
Build in neutral or enemy territory. Train Advanced Melee Infantry. Construct Rams. Garrison Soldiers for additional arrows.
ConquestCritical CivSpecific
City ArmyCamp
structures/roman_camp.png
phase_city
100
-
- 0.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
-
-
15
35
3
interface/complete/building/complete_broch.xml
attack/weapon/bow_attack.xml
attack/impact/arrow_impact.xml
2
+
+ 0.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
+
+
90
structures/romans/camp.xml
structures/fndn_8x8.xml
29.5
8
Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/sele/civil_centre.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/structures/sele/civil_centre.xml (revision 25999)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/sele/civil_centre.xml (revision 26000)
@@ -1,20 +1,20 @@
sele
Agora
-
+
units/{civ}/infantry_spearman_b
units/{civ}/infantry_javelineer_b
units/{civ}/cavalry_javelineer_b
units/{civ}/hero_seleucus_i
units/{civ}/hero_antiochus_iii
units/{civ}/hero_antiochus_iv
-
+
structures/seleucids/civic_center.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/spart/civil_centre.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/structures/spart/civil_centre.xml (revision 25999)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/spart/civil_centre.xml (revision 26000)
@@ -1,20 +1,20 @@
decay|rubble/rubble_hele_cc
spart
Agora
-
+
units/{civ}/infantry_spearman_b
units/{civ}/infantry_javelineer_b
units/{civ}/cavalry_javelineer_b
-
+
structures/spartans/civic_center.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/table_square.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/structures/table_square.xml (revision 25999)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/table_square.xml (revision 26000)
@@ -1,32 +1,34 @@
4.0
100
Table
Square Table
gaia/special_fence.png
20
+
6.0
+
props/special/eyecandy/table_square.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/kush/temple.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/structures/kush/temple.xml (revision 25999)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/kush/temple.xml (revision 26000)
@@ -1,25 +1,25 @@
kush
Temple of Apedemak
Pr-ʿIprmk
Train Healers and Apedemak Champions and research healing technologies.
TempleOfApedemak
-
+
units/{civ}/champion_infantry_apedemak
-
+
structures/kushites/temple.xml
structures/fndn_6x9.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/maur/civil_centre.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/structures/maur/civil_centre.xml (revision 25999)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/maur/civil_centre.xml (revision 26000)
@@ -1,28 +1,28 @@
8.0
decay|rubble/rubble_maur_cc
maur
Rajadhanika
-
+
units/{civ}/infantry_spearman_b
units/{civ}/infantry_archer_b
units/{civ}/cavalry_javelineer_b
units/{civ}/support_elephant
-
+
structures/mauryas/civil_centre.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 25999)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/merc_camp_egyptian.xml (revision 26000)
@@ -1,65 +1,65 @@
own neutral
MercenaryCamp
100
300
100
0
100
12.0
1200
decay|rubble/rubble_stone_5x5
ptol
Egyptian Mercenary Camp
Stratopedeia Misthophorōn
MercenaryCamp
Capture this structure to train mercenaries from Hellenistic Egypt.
structures/military_settlement.png
phase_town
20
0
20
-
-
- units/{civ}/infantry_spearman_merc_b
- units/{civ}/infantry_swordsman_merc_b
- units/{civ}/cavalry_spearman_merc_b
- units/{civ}/cavalry_javelineer_merc_b
-
-
interface/complete/building/complete_gymnasium.xml
1
+
+
+ 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.xml
structures/fndn_7x7.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/pers/civil_centre.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/structures/pers/civil_centre.xml (revision 25999)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/pers/civil_centre.xml (revision 26000)
@@ -1,24 +1,26 @@
decay|rubble/rubble_pers_cc
pers
Provincial Governor
Xšaçapāvan
-
+
+
+ architecture_pers
+
+
+
units/{civ}/infantry_spearman_b
units/{civ}/infantry_archer_b
units/{civ}/cavalry_javelineer_b
-
- architecture_pers
-
-
+
structures/persians/civil_centre.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/ptol/dock.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/structures/ptol/dock.xml (revision 25999)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/ptol/dock.xml (revision 26000)
@@ -1,23 +1,23 @@
8.0
ptol
Limēn
-
+
units/{civ}/champion_juggernaut
-
+
structures/ptolemies/dock.xml
structures/fndn_6x4_dock.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/rome/arch.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/structures/rome/arch.xml (revision 25999)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/rome/arch.xml (revision 26000)
@@ -1,47 +1,49 @@
200
200
400
8.0
2000
decay|rubble/rubble_stone_4x4
rome
Triumphal Arch
Arcus Triumphālis
TriumphalArch
structures/arch.png
40
80
+
interface/complete/building/complete_theater.xml
+
structures/romans/triumphal_arch.xml
structures/fndn_5x3.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/rome/tent.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/structures/rome/tent.xml (revision 25999)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/rome/tent.xml (revision 26000)
@@ -1,49 +1,51 @@
own neutral enemy
30
50
5.0
5
200
rome
Tent
Tabernāculum
-Village
A temporary shelter for soldiers.
10
+
interface/complete/building/complete_universal.xml
1
+
props/structures/romans/rome_tent.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/sele_colonnade.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/structures/sele_colonnade.xml (revision 25999)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/sele_colonnade.xml (revision 26000)
@@ -1,28 +1,30 @@
4.0
220
Colonnade
Corinthian Colonnade
gaia/special_fence.png
20
+
8.0
+
props/special/eyecandy/sele_colonnade.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/table_rectangle.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/structures/table_rectangle.xml (revision 25999)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/table_rectangle.xml (revision 26000)
@@ -1,32 +1,34 @@
4.0
100
Table
Rectangle Table
gaia/special_fence.png
20
+
6.0
+
props/special/eyecandy/table_rectangle.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_civic_civil_centre.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_civic_civil_centre.xml (revision 25999)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_civic_civil_centre.xml (revision 26000)
@@ -1,143 +1,145 @@
FemaleCitizen
120
180
100
Bow
10
60
1200
2000
100
1.5
50
false
Human
outline_border.png
outline_border_mask.png
0.175
-
- 3
- 1
- Soldier
-
own neutral
CivilCentre
CivilCentre
200
+
+ 3
+ 1
+ Soldier
+
2500
5.0
500
500
500
500
8.0
20
0.1
Unit
Support Infantry Cavalry
1
1
3000
decay|rubble/rubble_stone_6x6
Civic Center
template_structure_civic_civil_centre
Build in own or neutral territory. Acquire large tracts of territory. Territory root. Train Citizens and research technologies. Garrison Soldiers for additional arrows.
CivCentre
Defensive CivilCentre
structures/civic_centre.png
100
100
100
20
-
- 0.8
-
- units/{civ}/support_female_citizen
-
+
phase_town_{civ}
phase_city_{civ}
unlock_shared_los
unlock_shared_dropsites
unlock_spies
spy_counter
archery_tradition
hoplite_tradition
hellenistic_metropolis
-
+
5
5
food wood stone metal
true
interface/complete/building/complete_civ_center.xml
interface/alarm/alarm_alert_0.xml
interface/alarm/alarm_alert_1.xml
attack/weapon/bow_attack.xml
attack/impact/arrow_impact.xml
true
140
10000
+
+ 0.8
+
+ units/{civ}/support_female_citizen
+
+
90
structures/fndn_8x8.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/kush/shrine.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/structures/kush/shrine.xml (revision 25999)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/kush/shrine.xml (revision 26000)
@@ -1,23 +1,25 @@
12.0
decay|rubble/rubble_stone_4x4
kush
Shrine
ḥwt-nṯr
+
+
structures/kushites/shrine.xml
structures/fndn_6x6.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/mace/fortress.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/structures/mace/fortress.xml (revision 25999)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/mace/fortress.xml (revision 26000)
@@ -1,32 +1,34 @@
8.0
mace
Phrourion
Train Heroes. Garrison Soldiers for additional arrows.
-
+
+
+ silvershields
+
+
+
units/{civ}/hero_philip_ii
units/{civ}/hero_alexander_iii
units/{civ}/hero_demetrius_i
-
- silvershields
-
-
+
structures/macedonians/fortress.xml
24.0
7.6
Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/maur/pillar_ashoka.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/structures/maur/pillar_ashoka.xml (revision 25999)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/maur/pillar_ashoka.xml (revision 26000)
@@ -1,58 +1,60 @@
structures/maur_pillar
Pillar
Pillar
75
80
100
100
5.0
1000
decay|rubble/rubble_stone_2x2
maur
Edict Pillar of Ashoka
Śāsana Stambha Aśokā
CivSpecific
Pillar
structures/ashoka_pillar.png
20
20
+
interface/complete/building/complete_iber_monument.xml
+
props/structures/mauryas/ashoka_pillar.xml
structures/fndn_2x2.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/pers/apadana.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/structures/pers/apadana.xml (revision 25999)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/pers/apadana.xml (revision 26000)
@@ -1,79 +1,81 @@
Palace
300
300
200
8.0
10
3000
decay|rubble/rubble_stone_6x6
pers
Throne Hall
Apadāna
Train Champions and Heroes.
ConquestCritical CivSpecific
Palace
structures/palace.png
60
40
-
- 0.8
-
- units/{civ}/champion_infantry
- units/{civ}/hero_cyrus_ii
- units/{civ}/hero_darius_i
- units/{civ}/hero_xerxes_i
-
+
immortals
-
+
1.0
1.0
0.75
0.75
2000
interface/complete/building/complete_broch.xml
true
48
40000
+
+ 0.8
+
+ units/{civ}/champion_infantry
+ units/{civ}/hero_cyrus_ii
+ units/{civ}/hero_darius_i
+ units/{civ}/hero_xerxes_i
+
+
40
structures/persians/apadana.xml
structures/fndn_8x9.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/ptol/corral.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/structures/ptol/corral.xml (revision 25999)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/ptol/corral.xml (revision 26000)
@@ -1,23 +1,23 @@
ptol
Epaulos
-
+
-gaia/fauna_cattle_cow_trainable
gaia/fauna_cattle_sanga_trainable
-
+
structures/ptolemies/corral.xml
structures/fndn_4x6.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/ptol/military_colony.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/structures/ptol/military_colony.xml (revision 25999)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/ptol/military_colony.xml (revision 26000)
@@ -1,28 +1,28 @@
12
ptol
Klērouchia
CivSpecific
-
+
units/{civ}/support_female_citizen
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/military_colony.xml
structures/fndn_9x9.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/rome/fortress.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/structures/rome/fortress.xml (revision 25999)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/rome/fortress.xml (revision 26000)
@@ -1,22 +1,22 @@
rome
Castellum
Train Heroes. Garrison Soldiers for additional arrows.
-
+
units/{civ}/hero_marcellus
units/{civ}/hero_maximus
units/{civ}/hero_scipio
-
+
structures/romans/fortress.xml
25.0
8.4
Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/sele/military_colony.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/structures/sele/military_colony.xml (revision 25999)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/sele/military_colony.xml (revision 26000)
@@ -1,26 +1,26 @@
8
sele
Klērouchia
CivSpecific
-
+
units/{civ}/infantry_swordsman_merc_b
units/{civ}/infantry_archer_merc_b
units/{civ}/cavalry_spearman_merc_b
-
+
structures/seleucids/military_colony.xml
structures/fndn_9x9.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/spart/syssiton.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/structures/spart/syssiton.xml (revision 25999)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/spart/syssiton.xml (revision 26000)
@@ -1,67 +1,69 @@
200
200
200
8.0
10
2000
decay|rubble/rubble_stone_4x6
spart
Military Mess Hall
Syssition
Train Champions and Heroes.
ConquestCritical CivSpecific
Syssiton
structures/syssition.png
40
40
-
- 0.7
-
- units/{civ}/champion_infantry_spear
- units/{civ}/hero_leonidas
- units/{civ}/hero_brasidas
- units/{civ}/hero_agis
-
+
agoge
-
+
interface/complete/building/complete_gymnasium.xml
false
38
40000
+
+ 0.7
+
+ units/{civ}/champion_infantry_spear
+ units/{civ}/hero_leonidas
+ units/{civ}/hero_brasidas
+ units/{civ}/hero_agis
+
+
40
structures/spartans/syssiton.xml
structures/fndn_5x8.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_structure.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/template_structure.xml (revision 25999)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/template_structure.xml (revision 26000)
@@ -1,182 +1,185 @@
land
own
Structure
500
0.5
5.0
0
10
0
0
0
0
false
false
0.0
3.0
9.8
0.85
0.65
0.35
corpse
0
0
true
gaia
Structure
Structure
false
0
0
0
0
0
structure
true
true
true
true
false
false
false
false
0
upright
false
0
6
-
- 1.0
-
- 1.0
- 1.0
- 1.0
- 1.0
-
-
-
+
special/rallypoint
art/textures/misc/rallypoint_line.png
art/textures/misc/rallypoint_line_mask.png
0.25
square
round
default
2.0
+
+
+ 1.0
+ 1.0
+ 1.0
+ 1.0
+
+
+
0.0
1
1
10
1
0.0
1
1
10
1
outline_border.png
outline_border_mask.png
0.4
interface/complete/building/complete_universal.xml
attack/destruction/building_collapse_large.xml
interface/alarm/alarm_attackplayer.xml
interface/alarm/alarm_attacked_gaia.xml
interface/alarm/alarm_attackplayer.xml
interface/alarm/alarm_attacked_gaia.xml
6.0
0.6
12.0
20
neutral enemy
+
+ 1.0
+
true
false
false
false
4
false
false
true
false
Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/mace/civil_centre.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/structures/mace/civil_centre.xml (revision 25999)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/mace/civil_centre.xml (revision 26000)
@@ -1,20 +1,20 @@
decay|rubble/rubble_hele_cc
mace
Agora
-
+
units/{civ}/infantry_pikeman_b
units/{civ}/infantry_javelineer_b
units/{civ}/cavalry_spearman_b
-
+
structures/macedonians/civic_centre.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/maur/palace.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/structures/maur/palace.xml (revision 25999)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/maur/palace.xml (revision 26000)
@@ -1,61 +1,61 @@
200
200
200
5.0
2000
maur
Palace
Harmya
Territory root. Train Maiden Guards and Heroes.
ConquestCritical CivSpecific
Palace
gaia/special_stoa.png
40
40
-
- 0.7
-
- units/{civ}/champion_maiden
- units/{civ}/champion_maiden_archer
- units/{civ}/hero_chanakya
- units/{civ}/hero_chandragupta
- units/{civ}/hero_ashoka
-
-
interface/complete/building/complete_broch.xml
true
48
40000
+
+ 0.7
+
+ units/{civ}/champion_maiden
+ units/{civ}/champion_maiden_archer
+ units/{civ}/hero_chanakya
+ units/{civ}/hero_chandragupta
+ units/{civ}/hero_ashoka
+
+
40
structures/mauryas/misc_structure_01.xml
structures/fndn_9x4.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/palisades_fort.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/structures/palisades_fort.xml (revision 25999)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/palisades_fort.xml (revision 26000)
@@ -1,23 +1,25 @@
9.0
Wooden Tower
structures/palisade_fort.png
+
+
props/special/palisade_rocks_fort.xml
8.0
Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/ptol/civil_centre.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/structures/ptol/civil_centre.xml (revision 25999)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/ptol/civil_centre.xml (revision 26000)
@@ -1,27 +1,27 @@
8.0
ptol
Agora
-
+
units/{civ}/infantry_pikeman_b
units/{civ}/infantry_slinger_b
units/{civ}/cavalry_archer_b
units/{civ}/hero_ptolemy_i
units/{civ}/hero_ptolemy_iv
units/{civ}/hero_cleopatra_vii
-
+
structures/ptolemies/civic_centre.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 25999)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/ptol/mercenary_camp.xml (revision 26000)
@@ -1,64 +1,64 @@
own neutral
MercenaryCamp
100
300
100
100
12.0
1200
decay|rubble/rubble_stone_5x5
ptol
Mercenary Camp
Stratopedeia Misthophorōn
MercenaryCamp
Cheap Barracks-like structure that is buildable in neutral territory, but casts no territory influence. Train Mercenaries.
structures/mercenary_camp.png
phase_town
20
0
20
-
-
- units/{civ}/infantry_spearman_merc_b
- units/{civ}/infantry_swordsman_merc_b
- units/{civ}/cavalry_spearman_merc_b
- units/{civ}/cavalry_javelineer_merc_b
-
-
interface/complete/building/complete_gymnasium.xml
1
+
+
+ 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.xml
structures/fndn_7x7.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/rome/civil_centre.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/structures/rome/civil_centre.xml (revision 25999)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/rome/civil_centre.xml (revision 26000)
@@ -1,28 +1,28 @@
8.0
decay|rubble/rubble_rome_cc
rome
Forum
-
+
units/{civ}/infantry_swordsman_b
units/{civ}/infantry_javelineer_b
units/{civ}/cavalry_spearman_b
-
+
structures/romans/civic_centre.xml
structures/fndn_9x9.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/sele/fortress.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/structures/sele/fortress.xml (revision 25999)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/sele/fortress.xml (revision 26000)
@@ -1,26 +1,26 @@
8.0
sele
Phrourion
-
+
parade_of_daphne
-
+
structures/seleucids/fortress.xml
23
7.8
Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/spart/gerousia.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/structures/spart/gerousia.xml (revision 25999)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/spart/gerousia.xml (revision 26000)
@@ -1,59 +1,59 @@
Council
200
100
200
8.0
2000
spart
Spartan Senate
Gerousia
Train Heroes.
ConquestCritical
Council
structures/tholos.png
20
40
-
- 0.7
-
- units/{civ}/hero_leonidas
-
-
interface/complete/building/complete_tholos.xml
false
38
40000
+
+ 0.7
+
+ units/{civ}/hero_leonidas
+
+
40
structures/spartans/gerousia.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/uffington_horse.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/structures/uffington_horse.xml (revision 25999)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/uffington_horse.xml (revision 26000)
@@ -1,29 +1,31 @@
10.0
brit
Uffington White Horse
false
false
+
8.0
+
structures/fndn_stonehenge.xml
structures/britons/uffington_horse.xml
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 25999)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_civic_civil_centre_military_colony.xml (revision 26000)
@@ -1,54 +1,54 @@
-
- 1
-
own neutral
Colony
CivilCentre
120
+
+ 1
+
300
200
200
200
2000
decay|rubble/rubble_stone_5x5
Military Colony
template_structure_civic_civil_centre_military_colony
Colony
structures/military_settlement.png
phase_town
40
40
40
-
+
-phase_town_{civ}
-phase_city_{civ}
-hellenistic_metropolis
-
+
interface/complete/building/complete_gymnasium.xml
80
Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_defensive_palisade.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_defensive_palisade.xml (revision 25999)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_defensive_palisade.xml (revision 26000)
@@ -1,36 +1,38 @@
land-shore
own neutral
Wall
1000
decay|rubble/rubble_stone_2x2
Palisade
template_structure_defensive_palisade
Wall off an area. Build in own or neutral territory.
Palisade
gaia/special_palisade.png
+
4
25
2
interface/complete/building/complete_wall.xml
+
Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_defensive_outpost.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_defensive_outpost.xml (revision 25999)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_defensive_outpost.xml (revision 26000)
@@ -1,81 +1,81 @@
structures/wall_garrisoned
own neutral
Outpost
50
30
60
13.0
400
decay|rubble/rubble_stone_2x2
Outpost
template_structure_defensive_outpost
Build in own or neutral territory.
Outpost
structures/outpost.png
12
-
+
outpost_vision
-
+
10
20
1
interface/complete/building/complete_tower.xml
14.0
enemy
0
8
0
90
structures/fndn_2x2.xml
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 25999)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_civic_temple.xml (revision 26000)
@@ -1,73 +1,75 @@
structures/temple_heal
200
300
12.0
20
0.1
Unit
Support Infantry Cavalry
3
2
2000
decay|rubble/rubble_stone_4x6
Temple
template_structure_civic_temple
Train Healers and research healing technologies.
Town Temple
structures/temple.png
phase_town
60
-
- 0.8
-
- units/{civ}/support_healer_b
-
+
heal_range
heal_range_2
heal_rate
heal_rate_2
garrison_heal
health_regen_units
-
+
interface/complete/building/complete_temple.xml
false
40
30000
+
+ 0.8
+
+ units/{civ}/support_healer_b
+
+
40
structures/fndn_4x6.xml
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 25999)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_civic_house.xml (revision 26000)
@@ -1,72 +1,74 @@
30
75
5.0
3
0
0.1
Unit
Support+!Elephant
1
800
decay|rubble/rubble_stone_2x2
House
template_structure_civic_house
Village House
structures/house.png
phase_village
15
5
-
-
- units/{civ}/support_female_citizen_house
-
+
health_females_01
pop_house_01
pop_house_02
unlock_females_house
-
+
interface/complete/building/complete_house.xml
8.0
false
16
65535
+
+
+ units/{civ}/support_female_citizen_house
+
+
20
structures/fndn_3x3.xml
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 25999)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_defensive_tower_artillery.xml (revision 26000)
@@ -1,79 +1,79 @@
Stone
90
0
80
40
15
4500
5000
40
6
9.81
false
props/units/weapons/tower_artillery_projectile.xml
props/units/weapons/tower_artillery_projectile_impact.xml
0.3
-Human !Organic
1
0
200
200
200
15.0
5
1400
Artillery Tower
template_structure_defensive_tower_artillery
ArtilleryTower
structures/tower_artillery.png
phase_city
40
40
-
+
tower_health
-
+
attack/impact/siegeprojectilehit.xml
attack/siege/ballist_attack.xml
false
32
30000
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 25999)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_defensive_tower_stone.xml (revision 26000)
@@ -1,54 +1,54 @@
15
150
100
100
15.0
5
1000
Stone Tower
template_structure_defensive_tower_stone
Garrison Infantry for additional arrows. Needs the “Murder Holes” technology to protect its foot.
StoneTower
structures/defense_tower.png
phase_town
20
20
-
+
tower_watch
tower_crenellations
tower_range
tower_murderholes
tower_health
-
+
false
32
30000
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 25999)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_economic_storehouse.xml (revision 26000)
@@ -1,78 +1,78 @@
FemaleCitizen
50
100
100
40
100
8.0
800
decay|rubble/rubble_stone_3x3
Storehouse
template_structure_economic_storehouse
Research gathering technologies.
DropsiteWood DropsiteMetal DropsiteStone
Village Storehouse
structures/storehouse.png
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 metal
true
interface/complete/building/complete_storehouse.xml
interface/alarm/alarm_alert_0.xml
interface/alarm/alarm_alert_1.xml
false
20
30000
20
structures/fndn_3x3.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 25999)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_military_elephant_stable.xml (revision 26000)
@@ -1,63 +1,63 @@
structures/xp_trickle
180
200
200
8.0
5
Elephant
3000
decay|rubble/rubble_stone_6x6
Elephant Stable
template_structure_military_elephant_stable
Train Elephants and research Elephant technologies.
City ElephantStable
phase_city
structures/stable_elephant.png
40
40
-
- 0.7
-
- units/{civ}/support_elephant
- units/{civ}/elephant_archer_b
- units/{civ}/champion_elephant
-
-
interface/complete/building/complete_elephant_stable.xml
38
+
+ 0.7
+
+ units/{civ}/support_elephant
+ units/{civ}/elephant_archer_b
+ units/{civ}/champion_elephant
+
+
40
structures/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 25999)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_military_range.xml (revision 26000)
@@ -1,55 +1,55 @@
120
200
12.0
10
Infantry
2000
decay|rubble/rubble_stone_5x5
Practice Range
template_structure_military_range
Train Ranged Infantry and research technologies.
Village Range
structures/range.png
phase_village
40
-
- 0.8
-
- units/{civ}/infantry_javelineer_b
- units/{civ}/infantry_slinger_b
- units/{civ}/infantry_archer_b
-
-
interface/complete/building/complete_range.xml
+
+ 0.8
+
+ units/{civ}/infantry_javelineer_b
+ units/{civ}/infantry_slinger_b
+ units/{civ}/infantry_archer_b
+
+
32
structures/fndn_7x7.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_special_amphitheater.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_special_amphitheater.xml (revision 25999)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_special_amphitheater.xml (revision 26000)
@@ -1,54 +1,54 @@
500
500
500
20.0
2000
Amphitheater
template_structure_special_amphitheater
Amphitheater
structures/theater.png
100
100
-
- 0.8
-
- units/{civ}/champion_infantry_spear_gladiator
- units/{civ}/champion_infantry_sword_gladiator
-
-
interface/complete/building/complete_tholos.xml
false
100
40000
+
+ 0.8
+
+ units/{civ}/champion_infantry_spear_gladiator
+ units/{civ}/champion_infantry_sword_gladiator
+
+
40
structures/fndn_6x6.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 25999)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_wonder.xml (revision 26000)
@@ -1,94 +1,94 @@
structures/wonder_population_cap
Wonder
4
1000
1000
1500
1000
10.0
50
0.1
Unit
Support Soldier
5
2
5000
decay|rubble/rubble_stone_6x6
Wonder
template_structure_wonder
Bring glory to your civilization and add large tracts of land to your empire.
ConquestCritical
City Wonder
structures/wonder.png
phase_city
200
300
200
-
+
wonder_population_cap
-
+
15
25
3
1.0
1.0
1.0
1.0
2000
interface/complete/building/complete_wonder.xml
true
100
65535
72
structures/fndn_9x9.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 25999)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_defensive_tower_sentry.xml (revision 26000)
@@ -1,64 +1,64 @@
9
3
40
100
9.0
3
400
Sentry Tower
template_structure_defensive_tower_sentry
Garrison Infantry for additional arrows. Needs the “Murder Holes” technology to protect its foot.
SentryTower
structures/sentry_tower.png
phase_village
20
-
+
tower_watch
-
+
false
16
30000
structures/{civ}/defense_tower
Reinforce with stone and upgrade to a defense tower.
phase_town
50
100
upgrading
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 25999)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_economic_market.xml (revision 26000)
@@ -1,73 +1,75 @@
Trader+!Ship
-1
-1
100
150
300
8.0
1500
decay|rubble/rubble_stone_5x5
Market
template_structure_economic_market
Barter resources. Establish trade routes. Train Traders and research trade and barter technologies.
Barter
Trade Town Market
structures/market.png
phase_town
60
land
0.2
-
- 0.7
+
trader_health
trade_gain_01
trade_gain_02
trade_commercial_treaty
-
- units/{civ}/support_trader
-
-
+
interface/complete/building/complete_market.xml
interface/alarm/alarm_alert_0.xml
interface/alarm/alarm_alert_1.xml
false
40
30000
+
+ 0.7
+
+ units/{civ}/support_trader
+
+
32
structures/fndn_8x8.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_military_dock.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_military_dock.xml (revision 25999)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_military_dock.xml (revision 26000)
@@ -1,84 +1,86 @@
own ally neutral
shore
Dock
150
200
8.0
2500
decay|rubble/rubble_stone_4x4
Dock
template_structure_military_dock
Build upon a shoreline in own, neutral, or allied territory. Establish trade routes. Construct Ships and research Ship technologies.
Economic Naval Trade Village Dock
structures/dock.png
40
land naval
0.2
true
0.0
-
- 0.8
-
- units/{civ}/ship_fishing
- units/{civ}/ship_merchant
- units/{civ}/ship_bireme
- units/{civ}/ship_trireme
- units/{civ}/ship_quinquereme
- units/{civ}/ship_fire
-
+
+ ship
+
+
fishing_boat_gather_rate
fishing_boat_gather_capacity
ship_cost_time
ship_health
ship_movement_speed
equine_transports
-
-
- ship
-
+
food wood stone metal
true
interface/complete/building/complete_dock.xml
+
+ 0.8
+
+ units/{civ}/ship_fishing
+ units/{civ}/ship_merchant
+ units/{civ}/ship_bireme
+ units/{civ}/ship_trireme
+ units/{civ}/ship_quinquereme
+ units/{civ}/ship_fire
+
+
40
structures/fndn_4x4_dock.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 25999)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_military_fortress.xml (revision 26000)
@@ -1,106 +1,108 @@
Bow
10
60
1200
2000
100
1.5
50
false
Human
outline_border.png
outline_border_mask.png
0.175
-
- 4
- 1
- Soldier
-
Fortress
Fortress
80
+
+ 4
+ 1
+ Soldier
+
8
10.0
450
300
600
8.0
20
0.075
Support Infantry Cavalry Siege
6
5200
decay|rubble/rubble_stone_6x6
Fortress
template_structure_military_fortress
Garrison Soldiers for additional arrows.
GarrisonFortress
Defensive Fortress
structures/fortress.png
phase_city
60
120
-
- 0.8
+
attack_soldiers_will
-
+
interface/complete/building/complete_fortress.xml
attack/weapon/bow_attack.xml
attack/impact/arrow_impact.xml
2
80
+
+ 0.8
+
90
structures/fndn_8x8.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_resource_field.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_resource_field.xml (revision 25999)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_resource_field.xml (revision 26000)
@@ -1,71 +1,73 @@
50
100
2.0
250
decay|rubble/rubble_field
Field
template_structure_resource_field
Field
Harvest grain for food. Each subsequent gatherer works less efficiently.
structures/field.png
50
false
false
+
15
40
5
false
Infinity
food.grain
5
0.90
interface/complete/building/complete_field.xml
8.0
+
0
structures/plot_field_found.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_special_theater.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_special_theater.xml (revision 25999)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_special_theater.xml (revision 26000)
@@ -1,53 +1,55 @@
structures/theater
Theater
500
200
600
200
10.0
3000
Theater
template_structure_special_theater
Theater
structures/theater.png
40
120
40
+
interface/complete/building/complete_greek_theater.xml
false
100
40000
+
40
Index: ps/trunk/binaries/data/mods/public/simulation/templates/units/pers/ship_trireme.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/units/pers/ship_trireme.xml (revision 25999)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/units/pers/ship_trireme.xml (revision 26000)
@@ -1,31 +1,25 @@
8.0
pers
persian
Phoenician Trireme
Hamaraniyanava Vazarka
units/pers_ship_trireme.png
-
+
+
0.8
units/pers/cavalry_axeman_b_trireme
units/pers/cavalry_javelineer_b_trireme
-
- 1.0
- 1.0
- 1.0
- 1.0
-
-
-
+
structures/persians/trireme.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 25999)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_defensive_tower_bolt.xml (revision 26000)
@@ -1,76 +1,76 @@
Bolt
100
90
30
15
500
4000
150
1
9.81
false
props/units/weapons/tower_artillery_projectile_impact.xml
0.1
1
0
200
200
100
15.0
5
1400
Bolt Tower
template_structure_defensive_tower_bolt
BoltTower
structures/tower_bolt.png
phase_city
40
20
-
+
tower_health
-
+
attack/weapon/arrowfly.xml
attack/impact/arrow_metal.xml
false
32
30000
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 25999)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_economic_farmstead.xml (revision 26000)
@@ -1,70 +1,70 @@
FemaleCitizen
50
100
100
300
45
100
8.0
900
decay|rubble/rubble_stone_4x4
Farmstead
template_structure_economic_farmstead
Research food gathering technologies.
DropsiteFood
Village Farmstead
structures/farmstead.png
phase_village
20
-
+
+
gather_wicker_baskets
gather_farming_plows
gather_farming_training
gather_farming_fertilizer
gather_farming_harvester
-
-
+
food
true
interface/complete/building/complete_farmstead.xml
interface/alarm/alarm_alert_0.xml
interface/alarm/alarm_alert_1.xml
false
20
30000
20
structures/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 25999)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_military_barracks.xml (revision 26000)
@@ -1,79 +1,81 @@
structures/xp_trickle
150
200
100
12.0
10
Infantry Cavalry
2000
decay|rubble/rubble_stone_4x4
Barracks
template_structure_military_barracks
Train Infantry and research Infantry technologies.
Village Barracks
structures/barracks.png
phase_village
40
20
-
+
+
+ barracks_batch_training
+ infantry_cost_time
+ unlock_champion_infantry
+ pair_unlock_champions_sele
+
+
+
+
+
+ interface/complete/building/complete_barracks.xml
+
+
+
0.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
-
- barracks_batch_training
- infantry_cost_time
- unlock_champion_infantry
- pair_unlock_champions_sele
-
-
-
-
-
- interface/complete/building/complete_barracks.xml
-
-
+
32
structures/fndn_6x6.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 25999)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_military_forge.xml (revision 26000)
@@ -1,70 +1,70 @@
120
200
12.0
1
Infantry Healer
2000
decay|rubble/rubble_stone_4x4
Forge
template_structure_military_forge
Research attack damage and damage resistance technologies.
-ConquestCritical
Town Forge
structures/blacksmith.png
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.xml
38
30000
32
structures/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 25999)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_resource_corral.xml (revision 26000)
@@ -1,67 +1,69 @@
50
100
5.0
500
decay|rubble/rubble_stone_3x3
Corral
template_structure_resource_corral
Raise Domestic Animals for food.
Economic Village Corral
structures/corral.png
phase_village
20
-
- 0.7
-
- gaia/fauna_goat_trainable
- gaia/fauna_sheep_trainable
- gaia/fauna_pig_trainable
- gaia/fauna_cattle_cow_trainable
-
+
gather_animals_stockbreeding
-
+
20
interface/complete/building/complete_corral.xml
false
20
30000
+
+ 0.7
+
+ gaia/fauna_goat_trainable
+ gaia/fauna_sheep_trainable
+ gaia/fauna_pig_trainable
+ gaia/fauna_cattle_cow_trainable
+
+
20
structures/fndn_3x3.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_special_rotarymill.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_special_rotarymill.xml (revision 25999)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_special_rotarymill.xml (revision 26000)
@@ -1,57 +1,59 @@
100
200
100
6.0
2000
Rotary Mill
template_structure_special_rotarymill
RotaryMill
structures/rotarymill.png
40
20
+
food
true
interface/complete/building/complete_ffactri.xml
8.0
false
32
40000
+
40
structures/fndn_6x6.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/units/pers/hero_cyrus_ii.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/units/pers/hero_cyrus_ii.xml (revision 25999)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/units/pers/hero_cyrus_ii.xml (revision 26000)
@@ -1,29 +1,23 @@
units/heroes/pers_hero_cyrus_ii
pers
persian
Cyrus II The Great
Kuruš
units/pers_hero_cyrus.png
-
+
+
0.7
units/pers/champion_infantry
-
- 1.0
- 1.0
- 1.0
- 1.0
-
-
-
+
units/persians/hero_cavalry_spearman_cyrus_m.xml
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 25999)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_defensive_wall.xml (revision 26000)
@@ -1,37 +1,39 @@
land-shore
Wall
8.0
Wall
template_structure_defensive_wall
Wall off your town for a stout defense.
Wall
structures/wall.png
phase_town
4.5
+
interface/complete/building/complete_wall.xml
false
20
65535
+
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 25999)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_military_arsenal.xml (revision 26000)
@@ -1,73 +1,75 @@
structures/arsenal_repair
180
300
12.0
2
Siege
2000
decay|rubble/rubble_stone_5x5
Arsenal
template_structure_military_arsenal
Train Champion Infantry Crossbowmen, construct Siege Engines, and research Siege Engine technologies.
City Arsenal
structures/siege_workshop.png
phase_city
60
-
- 0.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
-
+
siege_attack
siege_cost_time
siege_health
siege_pack_unpack
siege_bolt_accuracy
-
+
interface/complete/building/complete_barracks.xml
38
+
+ 0.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
+
+
40
structures/fndn_8x8.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 25999)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_military_embassy.xml (revision 26000)
@@ -1,55 +1,55 @@
Embassy
150
12.0
6
Support Infantry Cavalry
2000
decay|rubble/rubble_stone_3x3
Embassy
template_structure_military_embassy
Town Embassy
phase_town
-
- 0.8
-
30
interface/complete/building/complete_gymnasium.xml
25
+
+ 0.8
+
24
structures/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 25999)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_military_stable.xml (revision 26000)
@@ -1,71 +1,73 @@
structures/xp_trickle
120
200
50
12.0
10
Cavalry
2000
decay|rubble/rubble_stone_5x5
Stable
template_structure_military_stable
Train Cavalry and research Cavalry technologies.
Village Stable
structures/stable.png
phase_village
40
10
-
- 0.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_chariot
- units/{civ}/war_dog
-
+
stable_batch_training
cavalry_cost_time
cavalry_movement_speed
cavalry_health
nisean_horses
unlock_champion_cavalry
unlock_champion_chariots
-
+
interface/complete/building/complete_stable.xml
+
+ 0.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_chariot
+ units/{civ}/war_dog
+
+
32
Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_special_library.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_special_library.xml (revision 25999)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_special_library.xml (revision 26000)
@@ -1,55 +1,57 @@
structures/library
Library
200
200
200
9.0
2000
decay|rubble/rubble_stone_4x6
Library
template_structure_special_library
Library
structures/library_scroll.png
40
40
+
interface/complete/building/complete_library.xml
false
50
40000
+
40
structures/fndn_7x9.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/units/athen/ship_trireme.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/units/athen/ship_trireme.xml (revision 25999)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/units/athen/ship_trireme.xml (revision 26000)
@@ -1,29 +1,23 @@
8.0
Triḗrēs Athēnaía
Athenian Trireme
units/hele_ship_trireme.png
-
+
+
0.7
units/athen/infantry_marine_archer_b
units/athen/champion_marine
-
- 1.0
- 1.0
- 1.0
- 1.0
-
-
-
+
structures/athenians/trireme.xml