Index: ps/trunk/binaries/data/mods/public/globalscripts/StatusEffects.js
===================================================================
--- ps/trunk/binaries/data/mods/public/globalscripts/StatusEffects.js (revision 24161)
+++ ps/trunk/binaries/data/mods/public/globalscripts/StatusEffects.js (revision 24162)
@@ -1,44 +1,68 @@
/**
* This class provides a cache for accessing status effects metadata stored in JSON files.
* Note that status effects need not be defined in JSON files to be handled in-game.
* This class must be initialised before using, as initialising it directly in globalscripts would
* introduce disk I/O every time e.g. a GUI page is loaded.
*/
class StatusEffectsMetadata
{
constructor()
{
this.statusEffectData = {};
- let files = Engine.ListDirectoryFiles("simulation/data/template_helpers/status_effects", "*.json", false);
+ let files = Engine.ListDirectoryFiles("simulation/data/status_effects", "*.json", false);
for (let filename of files)
{
let data = Engine.ReadJSONFile(filename);
if (!data)
continue;
if (data.code in this.statusEffectData)
{
error("Encountered two status effects with the code " + data.code);
continue;
}
- this.statusEffectData[data.code] = data;
+ this.statusEffectData[data.code] = {
+ "applierTooltip": data.applierTooltip || "",
+ "code": data.code,
+ "icon": data.icon || "default",
+ "statusName": data.statusName || "data.code",
+ "receiverTooltip": data.receiverTooltip || ""
+ };
}
}
/**
- * @returns the default data for @param code status effects, augmented with the given template data,
- * or simply @param templateData if the code is not found in JSON files.
+ * @param {string} code - The code of the Status Effect.
+ * @return {Object} - The JSON data corresponding to the code.
*/
- augment(code, templateData)
+ getData(code)
{
- if (!templateData && this.statusEffectData[code])
+ if (this.statusEffectData[code])
return this.statusEffectData[code];
- if (this.statusEffectData[code])
- return Object.assign({}, this.statusEffectData[code], templateData);
+ warn("No status effects data found for: " + code + ".");
+ return {};
+ }
- return templateData;
+ getApplierTooltip(code)
+ {
+ return this.getData(code).applierTooltip;
+ }
+
+ getIcon(code)
+ {
+ return this.getData(code).icon;
+ }
+
+ getName(code)
+ {
+ return this.getData(code).statusName;
+ }
+
+ getReceiverTooltip(code)
+ {
+ return this.getData(code).receiverTooltip;
}
}
Index: ps/trunk/binaries/data/mods/public/globalscripts/Templates.js
===================================================================
--- ps/trunk/binaries/data/mods/public/globalscripts/Templates.js (revision 24161)
+++ ps/trunk/binaries/data/mods/public/globalscripts/Templates.js (revision 24162)
@@ -1,568 +1,575 @@
/**
* 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", "Factions", "CivBonuses", "TeamBonuses",
"Structures", "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");
-
- // ToDo: Resistance against StatusEffects.
+ 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] = {
"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+/))
{
let aura = auraTemplates[auraID];
ret.auras[auraID] = {
"name": aura.auraName,
"description": aura.auraDescription || null,
"radius": aura.radius || null
};
}
}
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.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.PopulationBonus)
ret.cost.populationBonus = getEntityValue("Cost/PopulationBonus");
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.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.Health)
ret.health = Math.round(getEntityValue("Health/Max"));
if (template.Identity)
{
ret.selectionGroupName = template.Identity.SelectionGroupName;
ret.name = {
"specific": (template.Identity.SpecificName || template.Identity.GenericName),
"generic": template.Identity.GenericName
};
ret.icon = template.Identity.Icon;
ret.tooltip = template.Identity.Tooltip;
ret.requiredTechnology = template.Identity.RequiredTechnology;
ret.visibleIdentityClasses = GetVisibleIdentityClasses(template.Identity);
ret.nativeCiv = template.Identity.Civ;
}
if (template.UnitMotion)
{
ret.speed = {
"walk": getEntityValue("UnitMotion/WalkSpeed"),
};
ret.speed.run = getEntityValue("UnitMotion/WalkSpeed");
if (template.UnitMotion.RunMultiplier)
ret.speed.run *= getEntityValue("UnitMotion/RunMultiplier");
}
if (template.Upgrade)
{
ret.upgrades = [];
for (let upgradeName in template.Upgrade)
{
let upgrade = template.Upgrade[upgradeName];
let cost = {};
if (upgrade.Cost)
for (let res in upgrade.Cost)
cost[res] = getEntityValue("Upgrade/" + upgradeName + "/Cost/" + res, "Upgrade/Cost/" + res);
if (upgrade.Time)
cost.time = getEntityValue("Upgrade/" + upgradeName + "/Time", "Upgrade/Time");
ret.upgrades.push({
"entity": upgrade.Entity,
"tooltip": upgrade.Tooltip,
"cost": cost,
"icon": upgrade.Icon || undefined,
"requiredTechnology": upgrade.RequiredTechnology || undefined
});
}
}
if (template.ProductionQueue)
{
ret.techCostMultiplier = {};
for (let res in template.ProductionQueue.TechCostMultiplier)
ret.techCostMultiplier[res] = getEntityValue("ProductionQueue/TechCostMultiplier/" + res);
}
if (template.Trader)
ret.trader = {
"GainMultiplier": getEntityValue("Trader/GainMultiplier")
};
if (template.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;
}
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/globalscripts/tests/test_statusEffects.js
===================================================================
--- ps/trunk/binaries/data/mods/public/globalscripts/tests/test_statusEffects.js (revision 24161)
+++ ps/trunk/binaries/data/mods/public/globalscripts/tests/test_statusEffects.js (revision 24162)
@@ -1,31 +1,36 @@
let statusEffects = {
"test_A": {
"code": "test_a",
- "StatusName": "A",
- "StatusTooltip": "TTA"
+ "statusName": "A",
+ "applierTooltip": "TTA"
},
"test_B": {
"code": "test_b",
- "StatusName": "B",
- "StatusTooltip": "TTB"
+ "statusName": "B",
+ "applierTooltip": "TTB"
}
};
Engine.ListDirectoryFiles = () => Object.keys(statusEffects);
Engine.ReadJSONFile = (file) => statusEffects[file];
let sem = new StatusEffectsMetadata();
-// Template data takes precedence over generic data.
-TS_ASSERT_UNEVAL_EQUALS(sem.augment("test_a"), {
- "code": "test_a", "StatusName": "A", "StatusTooltip": "TTA"
+TS_ASSERT_UNEVAL_EQUALS(sem.getData("test_a"), {
+ "applierTooltip": "TTA",
+ "code": "test_a",
+ "icon": "default",
+ "statusName": "A",
+ "receiverTooltip": ""
});
-TS_ASSERT_UNEVAL_EQUALS(sem.augment("test_b"), {
- "code": "test_b", "StatusName": "B", "StatusTooltip": "TTB"
-});
-TS_ASSERT_UNEVAL_EQUALS(sem.augment("test_a", { "StatusName": "test" }), {
- "code": "test_a", "StatusName": "test", "StatusTooltip": "TTA"
-});
-TS_ASSERT_UNEVAL_EQUALS(sem.augment("test_c", { "StatusName": "test" }), {
- "StatusName": "test"
+TS_ASSERT_UNEVAL_EQUALS(sem.getData("test_b"), {
+ "applierTooltip": "TTB",
+ "code": "test_b",
+ "icon": "default",
+ "statusName": "B",
+ "receiverTooltip": ""
});
+TS_ASSERT_UNEVAL_EQUALS(sem.getApplierTooltip("test_a"), "TTA");
+TS_ASSERT_UNEVAL_EQUALS(sem.getIcon("test_b"), "default");
+TS_ASSERT_UNEVAL_EQUALS(sem.getName("test_a"), "A");
+TS_ASSERT_UNEVAL_EQUALS(sem.getReceiverTooltip("test_b"), "");
Index: ps/trunk/binaries/data/mods/public/gui/common/tooltips.js
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/common/tooltips.js (revision 24161)
+++ ps/trunk/binaries/data/mods/public/gui/common/tooltips.js (revision 24162)
@@ -1,1015 +1,1055 @@
var g_TooltipTextFormats = {
"unit": { "font": "sans-10", "color": "orange" },
"header": { "font": "sans-bold-13" },
"body": { "font": "sans-13" },
"comma": { "font": "sans-12" },
"nameSpecificBig": { "font": "sans-bold-16" },
"nameSpecificSmall": { "font": "sans-bold-12" },
"nameGeneric": { "font": "sans-bold-16" }
};
/**
* String of four spaces to be used as indentation in gui strings.
*/
var g_Indent = " ";
var g_DamageTypesMetadata = new DamageTypesMetadata();
var g_StatusEffectsMetadata = new StatusEffectsMetadata();
/**
* If true, always shows whether the splash damage deals friendly fire.
* Otherwise display the friendly fire tooltip only if it does.
*/
var g_AlwaysDisplayFriendlyFire = false;
function getCostTypes()
{
return g_ResourceData.GetCodes().concat(["population", "populationBonus", "time"]);
}
function resourceIcon(resource)
{
return '[icon="icon_' + resource + '"]';
}
function resourceNameFirstWord(type)
{
return translateWithContext("firstWord", g_ResourceData.GetNames()[type]);
}
function resourceNameWithinSentence(type)
{
return translateWithContext("withinSentence", g_ResourceData.GetNames()[type]);
}
/**
* Format resource amounts to proper english and translate (for example: "200 food, 100 wood and 300 metal").
*/
function getLocalizedResourceAmounts(resources)
{
let amounts = g_ResourceData.GetCodes()
.filter(type => !!resources[type])
.map(type => sprintf(translate("%(amount)s %(resourceType)s"), {
"amount": resources[type],
"resourceType": resourceNameWithinSentence(type)
}));
if (amounts.length < 2)
return amounts.join();
let lastAmount = amounts.pop();
return sprintf(translate("%(previousAmounts)s and %(lastAmount)s"), {
// Translation: This comma is used for separating first to penultimate elements in an enumeration.
"previousAmounts": amounts.join(translate(", ")),
"lastAmount": lastAmount
});
}
function bodyFont(text)
{
return setStringTags(text, g_TooltipTextFormats.body);
}
function headerFont(text)
{
return setStringTags(text, g_TooltipTextFormats.header);
}
function unitFont(text)
{
return setStringTags(text, g_TooltipTextFormats.unit);
}
function commaFont(text)
{
return setStringTags(text, g_TooltipTextFormats.comma);
}
function getSecondsString(seconds)
{
return sprintf(translatePlural("%(time)s %(second)s", "%(time)s %(second)s", seconds), {
"time": seconds,
"second": unitFont(translatePlural("second", "seconds", seconds))
});
}
/**
* Entity templates have a `Tooltip` tag in the Identity component.
* (The contents of which are copied to a `tooltip` attribute in globalscripts.)
*
* Technologies have a `tooltip` attribute.
*/
function getEntityTooltip(template)
{
if (!template.tooltip)
return "";
return bodyFont(template.tooltip);
}
/**
* Technologies have a `description` attribute, and Auras have an `auraDescription`
* attribute, which becomes `description`.
*
* (For technologies, this happens in globalscripts.)
*
* (For auras, this happens either in the Auras component (for session gui) or
* reference/common/load.js (for Reference Suite gui))
*/
function getDescriptionTooltip(template)
{
if (!template.description)
return "";
return bodyFont(template.description);
}
/**
* Entity templates have a `History` tag in the Identity component.
* (The contents of which are copied to a `history` attribute in globalscripts.)
*/
function getHistoryTooltip(template)
{
if (!template.history)
return "";
return bodyFont(template.history);
}
function getHealthTooltip(template)
{
if (!template.health)
return "";
return sprintf(translate("%(label)s %(details)s"), {
"label": headerFont(translate("Health:")),
"details": template.health
});
}
function getCurrentHealthTooltip(entState, label)
{
if (!entState.maxHitpoints)
return "";
return sprintf(translate("%(healthLabel)s %(current)s / %(max)s"), {
"healthLabel": headerFont(label || translate("Health:")),
"current": Math.round(entState.hitpoints),
"max": Math.round(entState.maxHitpoints)
});
}
/**
* Converts an resistance level into the actual reduction percentage.
*/
function resistanceLevelToPercentageString(level)
{
return sprintf(translate("%(percentage)s%%"), {
"percentage": (100 - Math.round(Math.pow(0.9, level) * 100))
});
}
function getResistanceTooltip(template)
{
if (!template.resistance)
return "";
let details = [];
if (template.resistance.Damage)
details.push(getDamageResistanceTooltip(template.resistance.Damage));
if (template.resistance.Capture)
details.push(getCaptureResistanceTooltip(template.resistance.Capture));
- // TODO: Status effects resistance.
+ if (template.resistance.ApplyStatus)
+ details.push(getStatusEffectsResistanceTooltip(template.resistance.ApplyStatus));
return sprintf(translate("%(label)s\n%(details)s"), {
"label": headerFont(translate("Resistance:")),
"details": g_Indent + details.join("\n" + g_Indent)
});
}
function getDamageResistanceTooltip(resistanceTypeTemplate)
{
if (!resistanceTypeTemplate)
return "";
return sprintf(translate("%(label)s %(details)s"), {
"label": headerFont(translate("Damage:")),
"details":
g_DamageTypesMetadata.sort(Object.keys(resistanceTypeTemplate)).map(
dmgType => sprintf(translate("%(damage)s %(damageType)s %(resistancePercentage)s"), {
"damage": resistanceTypeTemplate[dmgType].toFixed(1),
"damageType": unitFont(translateWithContext("damage type", g_DamageTypesMetadata.getName(dmgType))),
"resistancePercentage":
'[font="sans-10"]' +
sprintf(translate("(%(resistancePercentage)s)"), {
"resistancePercentage": resistanceLevelToPercentageString(resistanceTypeTemplate[dmgType])
}) + '[/font]'
})
).join(commaFont(translate(", ")))
});
}
function getCaptureResistanceTooltip(resistanceTypeTemplate)
{
if (!resistanceTypeTemplate)
return "";
return sprintf(translate("%(label)s %(details)s"), {
"label": headerFont(translate("Capture:")),
"details":
sprintf(translate("%(damage)s %(damageType)s %(resistancePercentage)s"), {
"damage": resistanceTypeTemplate.toFixed(1),
"damageType": unitFont(translateWithContext("damage type", "Capture")),
"resistancePercentage":
'[font="sans-10"]' +
sprintf(translate("(%(resistancePercentage)s)"), {
"resistancePercentage": resistanceLevelToPercentageString(resistanceTypeTemplate)
}) + '[/font]'
})
});
}
+function getStatusEffectsResistanceTooltip(resistanceTypeTemplate)
+{
+ if (!resistanceTypeTemplate)
+ return "";
+ return sprintf(translate("%(label)s %(details)s"), {
+ "label": headerFont(translate("Status Effects:")),
+ "details":
+ Object.keys(resistanceTypeTemplate).map(
+ statusEffect => {
+ if (resistanceTypeTemplate[statusEffect].blockChance == 1)
+ return sprintf(translate("Blocks %(name)s"), {
+ "name": unitFont(translateWithContext("status effect", g_StatusEffectsMetadata.getName(statusEffect)))
+ });
+
+ if (resistanceTypeTemplate[statusEffect].blockChance == 0)
+ return sprintf(translate("%(name)s %(details)s"), {
+ "name": unitFont(translateWithContext("status effect", g_StatusEffectsMetadata.getName(statusEffect))),
+ "details": sprintf(translate("Duration reduction: %(durationReduction)s%%"), {
+ "durationReduction": (100 - resistanceTypeTemplate[statusEffect].duration * 100)
+ })
+ });
+
+ if (resistanceTypeTemplate[statusEffect].duration == 1)
+ return sprintf(translate("%(name)s %(details)s"), {
+ "name": unitFont(translateWithContext("status effect", g_StatusEffectsMetadata.getName(statusEffect))),
+ "details": sprintf(translate("Blocks: %(blockPercentage)s%%"), {
+ "blockPercentage": resistanceTypeTemplate[statusEffect].blockChance * 100
+ })
+ });
+
+ return sprintf(translate("%(name)s %(details)s"), {
+ "name": unitFont(translateWithContext("status effect", g_StatusEffectsMetadata.getName(statusEffect))),
+ "details": sprintf(translate("Blocks: %(blockPercentage)s%%, Duration reduction: %(durationReduction)s%%"), {
+ "blockPercentage": resistanceTypeTemplate[statusEffect].blockChance * 100,
+ "durationReduction": (100 - resistanceTypeTemplate[statusEffect].duration * 100)
+ })
+ });
+ }
+ ).join(commaFont(translate(", ")))
+ });
+}
+
function attackRateDetails(interval, projectiles)
{
if (!interval)
return "";
if (projectiles === 0)
return translate("Garrison to fire arrows");
let attackRateString = getSecondsString(interval / 1000);
let header = headerFont(translate("Interval:"));
if (projectiles && +projectiles > 1)
{
header = headerFont(translate("Rate:"));
let projectileString = sprintf(translatePlural("%(projectileCount)s %(projectileName)s", "%(projectileCount)s %(projectileName)s", projectiles), {
"projectileCount": projectiles,
"projectileName": unitFont(translatePlural("arrow", "arrows", projectiles))
});
attackRateString = sprintf(translate("%(projectileString)s / %(attackRateString)s"), {
"projectileString": projectileString,
"attackRateString": attackRateString
});
}
return sprintf(translate("%(label)s %(details)s"), {
"label": header,
"details": attackRateString
});
}
function rangeDetails(attackTypeTemplate)
{
if (!attackTypeTemplate.maxRange)
return "";
let rangeTooltipString = {
"relative": {
// Translation: For example: Range: 2 to 10 (+2) meters
"minRange": translate("%(rangeLabel)s %(minRange)s to %(maxRange)s (%(relativeRange)s) %(rangeUnit)s"),
// Translation: For example: Range: 10 (+2) meters
"no-minRange": translate("%(rangeLabel)s %(maxRange)s (%(relativeRange)s) %(rangeUnit)s"),
},
"non-relative": {
// Translation: For example: Range: 2 to 10 meters
"minRange": translate("%(rangeLabel)s %(minRange)s to %(maxRange)s %(rangeUnit)s"),
// Translation: For example: Range: 10 meters
"no-minRange": translate("%(rangeLabel)s %(maxRange)s %(rangeUnit)s"),
}
};
let minRange = Math.round(attackTypeTemplate.minRange);
let maxRange = Math.round(attackTypeTemplate.maxRange);
let realRange = attackTypeTemplate.elevationAdaptedRange;
let relativeRange = realRange ? Math.round(realRange - maxRange) : 0;
return sprintf(rangeTooltipString[relativeRange ? "relative" : "non-relative"][minRange ? "minRange" : "no-minRange"], {
"rangeLabel": headerFont(translate("Range:")),
"minRange": minRange,
"maxRange": maxRange,
"relativeRange": relativeRange > 0 ? sprintf(translate("+%(number)s"), { "number": relativeRange }) : relativeRange,
"rangeUnit":
unitFont(minRange || relativeRange ?
// Translation: For example "0.5 to 1 meters", "1 (+1) meters" or "1 to 2 (+3) meters"
translate("meters") :
translatePlural("meter", "meters", maxRange))
});
}
function damageDetails(damageTemplate)
{
if (!damageTemplate)
return "";
return g_DamageTypesMetadata.sort(Object.keys(damageTemplate).filter(dmgType => damageTemplate[dmgType])).map(
dmgType => sprintf(translate("%(damage)s %(damageType)s"), {
"damage": (+damageTemplate[dmgType]).toFixed(1),
"damageType": unitFont(translateWithContext("damage type", g_DamageTypesMetadata.getName(dmgType)))
})).join(commaFont(translate(", ")));
}
function captureDetails(captureTemplate)
{
if (!captureTemplate)
return "";
return sprintf(translate("%(amount)s %(name)s"), {
"amount": (+captureTemplate).toFixed(1),
"name": unitFont(translateWithContext("damage type", "Capture"))
});
}
function splashDetails(splashTemplate)
{
let splashLabel = sprintf(headerFont(translate("%(splashShape)s Splash")), {
"splashShape": splashTemplate.shape
});
let splashDamageTooltip = sprintf(translate("%(label)s: %(effects)s"), {
"label": splashLabel,
"effects": attackEffectsDetails(splashTemplate)
});
if (g_AlwaysDisplayFriendlyFire || splashTemplate.friendlyFire)
splashDamageTooltip += commaFont(translate(", ")) + sprintf(translate("Friendly Fire: %(enabled)s"), {
"enabled": splashTemplate.friendlyFire ? translate("Yes") : translate("No")
});
return splashDamageTooltip;
}
function applyStatusDetails(applyStatusTemplate)
{
if (!applyStatusTemplate)
return "";
return sprintf(translate("gives %(name)s"), {
- "name": Object.keys(applyStatusTemplate).map(x => {
- let template = g_StatusEffectsMetadata.augment(x, applyStatusTemplate[x]);
- return unitFont(translateWithContext("status effect", template.StatusName));
- }).join(commaFont(translate(", "))),
+ "name": Object.keys(applyStatusTemplate).map(x =>
+ unitFont(translateWithContext("status effect", g_StatusEffectsMetadata.getName(x)))
+ ).join(commaFont(translate(", "))),
});
}
function attackEffectsDetails(attackTypeTemplate)
{
if (!attackTypeTemplate)
return "";
let effects = [
captureDetails(attackTypeTemplate.Capture || undefined),
damageDetails(attackTypeTemplate.Damage || undefined),
applyStatusDetails(attackTypeTemplate.ApplyStatus || undefined)
];
return effects.filter(effect => effect).join(commaFont(translate(", ")));
}
function getAttackTooltip(template)
{
if (!template.attack)
return "";
let tooltips = [];
for (let attackType in template.attack)
{
// Slaughter is used to kill animals, so do not show it.
if (attackType == "Slaughter")
continue;
let attackLabel = sprintf(headerFont(translate("%(attackType)s")), {
"attackType": attackType
});
let attackTypeTemplate = template.attack[attackType];
let projectiles;
// Use either current rate from simulation or default count if the sim is not running.
// TODO: This ought to be extended to include units which fire multiple projectiles.
if (template.buildingAI)
projectiles = template.buildingAI.arrowCount || template.buildingAI.defaultArrowCount;
let splashTemplate = attackTypeTemplate.splash;
// Show the effects of status effects below.
let statusEffectsDetails = [];
if (attackTypeTemplate.ApplyStatus)
for (let status in attackTypeTemplate.ApplyStatus)
- {
- let status_template = g_StatusEffectsMetadata.augment(status, attackTypeTemplate.ApplyStatus[status]);
- statusEffectsDetails.push("\n" + g_Indent + g_Indent + getStatusEffectsTooltip(status_template, true));
- }
+ statusEffectsDetails.push("\n" + g_Indent + g_Indent + getStatusEffectsTooltip(status, attackTypeTemplate.ApplyStatus[status], true));
statusEffectsDetails = statusEffectsDetails.join("");
tooltips.push(sprintf(translate("%(attackLabel)s: %(effects)s, %(range)s, %(rate)s%(statusEffects)s%(splash)s"), {
"attackLabel": attackLabel,
"effects": attackEffectsDetails(attackTypeTemplate),
"range": rangeDetails(attackTypeTemplate),
"rate": attackRateDetails(attackTypeTemplate.repeatTime, projectiles),
"splash": splashTemplate ? "\n" + g_Indent + g_Indent + splashDetails(splashTemplate) : "",
"statusEffects": statusEffectsDetails
}));
}
return sprintf(translate("%(label)s\n%(details)s"), {
"label": headerFont(translate("Attack:")),
"details": g_Indent + tooltips.join("\n" + g_Indent)
});
}
/**
* @param applier - if true, return the tooltip for the Applier. If false, Receiver is returned.
*/
-function getStatusEffectsTooltip(template, applier)
+function getStatusEffectsTooltip(statusCode, template, applier)
{
let tooltipAttributes = [];
- if (applier && template.ApplierTooltip)
- tooltipAttributes.push(translate(template.ApplierTooltip));
- else if (!applier && template.ReceiverTooltip)
- tooltipAttributes.push(translate(template.ReceiverTooltip));
-
+ let statusData = g_StatusEffectsMetadata.getData(statusCode);
if (template.Damage || template.Capture)
tooltipAttributes.push(attackEffectsDetails(template));
if (template.Interval)
tooltipAttributes.push(attackRateDetails(+template.Interval));
if (template.Duration)
tooltipAttributes.push(getStatusEffectDurationTooltip(template));
+ if (applier && statusData.applierTooltip)
+ tooltipAttributes.push(translate(statusData.applierTooltip));
+ else if (!applier && statusData.receiverTooltip)
+ tooltipAttributes.push(translate(statusData.receiverTooltip));
+
if (applier)
return sprintf(translate("%(statusName)s: %(statusInfo)s %(stackability)s"), {
- "statusName": headerFont(translateWithContext("status effect", template.StatusName)),
+ "statusName": headerFont(translateWithContext("status effect", statusData.statusName)),
"statusInfo": tooltipAttributes.join(commaFont(translate(", "))),
"stackability": getStatusEffectStackabilityTooltip(template)
});
return sprintf(translate("%(statusName)s: %(statusInfo)s"), {
- "statusName": headerFont(translateWithContext("status effect", template.StatusName)),
+ "statusName": headerFont(translateWithContext("status effect", statusData.statusName)),
"statusInfo": tooltipAttributes.join(commaFont(translate(", ")))
});
}
function getStatusEffectDurationTooltip(template)
{
if (!template.Duration)
return "";
return sprintf(translate("%(durName)s: %(duration)s"), {
"durName": headerFont(translate("Duration")),
"duration": getSecondsString((template._timeElapsed ?
+template.Duration - template._timeElapsed :
+template.Duration) / 1000)
});
}
function getStatusEffectStackabilityTooltip(template)
{
if (!template.Stackability || template.Stackability == "Ignore")
return "";
let stackabilityString = "";
if (template.Stackability === "Extend")
stackabilityString = translateWithContext("status effect stackability", "(extends)");
else if (template.Stackability === "Replace")
stackabilityString = translateWithContext("status effect stackability", "(replaces)");
else if (template.Stackability === "Stack")
stackabilityString = translateWithContext("status effect stackability", "(stacks)");
return sprintf(translate("%(stackability)s"), {
"stackability": stackabilityString
});
}
function getGarrisonTooltip(template)
{
if (!template.garrisonHolder)
return "";
let tooltips = [
sprintf(translate("%(label)s: %(garrisonLimit)s"), {
"label": headerFont(translate("Garrison Limit")),
"garrisonLimit": template.garrisonHolder.capacity
})
];
if (template.garrisonHolder.buffHeal)
tooltips.push(
sprintf(translate("%(healRateLabel)s %(value)s %(health)s / %(second)s"), {
"healRateLabel": headerFont(translate("Heal:")),
"value": Math.round(template.garrisonHolder.buffHeal),
"health": unitFont(translate("Health")),
"second": unitFont(translate("second")),
})
);
return tooltips.join(commaFont(translate(", ")));
}
function getProjectilesTooltip(template)
{
if (!template.garrisonHolder || !template.buildingAI)
return "";
let limit = Math.min(
template.buildingAI.maxArrowCount || Infinity,
template.buildingAI.defaultArrowCount +
Math.round(template.buildingAI.garrisonArrowMultiplier *
template.garrisonHolder.capacity)
);
if (!limit)
return "";
return [
sprintf(translate("%(label)s: %(value)s"), {
"label": headerFont(translate("Projectile Limit")),
"value": limit
}),
sprintf(translate("%(label)s: %(value)s"), {
"label": headerFont(translateWithContext("projectiles", "Default")),
"value": template.buildingAI.defaultArrowCount
}),
sprintf(translate("%(label)s: %(value)s"), {
"label": headerFont(translateWithContext("projectiles", "Per Unit")),
"value": +template.buildingAI.garrisonArrowMultiplier.toFixed(2)
})
].join(commaFont(translate(", ")));
}
function getRepairTimeTooltip(entState)
{
return sprintf(translate("%(label)s %(details)s"), {
"label": headerFont(translate("Number of repairers:")),
"details": entState.repairable.numBuilders
}) + "\n" + (entState.repairable.numBuilders ?
sprintf(translatePlural(
"Add another worker to speed up the repairs by %(second)s second.",
"Add another worker to speed up the repairs by %(second)s seconds.",
Math.round(entState.repairable.buildTime.timeRemaining - entState.repairable.buildTime.timeRemainingNew)),
{
"second": Math.round(entState.repairable.buildTime.timeRemaining - entState.repairable.buildTime.timeRemainingNew)
}) :
sprintf(translatePlural(
"Add a worker to finish the repairs in %(second)s second.",
"Add a worker to finish the repairs in %(second)s seconds.",
Math.round(entState.repairable.buildTime.timeRemainingNew)),
{
"second": Math.round(entState.repairable.buildTime.timeRemainingNew)
}));
}
function getBuildTimeTooltip(entState)
{
return sprintf(translate("%(label)s %(details)s"), {
"label": headerFont(translate("Number of builders:")),
"details": entState.foundation.numBuilders
}) + "\n" + (entState.foundation.numBuilders ?
sprintf(translatePlural(
"Add another worker to speed up the construction by %(second)s second.",
"Add another worker to speed up the construction by %(second)s seconds.",
Math.round(entState.foundation.buildTime.timeRemaining - entState.foundation.buildTime.timeRemainingNew)),
{
"second": Math.round(entState.foundation.buildTime.timeRemaining - entState.foundation.buildTime.timeRemainingNew)
}) :
sprintf(translatePlural(
"Add a worker to finish the construction in %(second)s second.",
"Add a worker to finish the construction in %(second)s seconds.",
Math.round(entState.foundation.buildTime.timeRemainingNew)),
{
"second": Math.round(entState.foundation.buildTime.timeRemainingNew)
}));
}
/**
* Multiplies the costs for a template by a given batch size.
*/
function multiplyEntityCosts(template, trainNum)
{
let totalCosts = {};
for (let r of getCostTypes())
if (template.cost[r])
totalCosts[r] = Math.floor(template.cost[r] * trainNum);
return totalCosts;
}
/**
* Helper function for getEntityCostTooltip.
*/
function getEntityCostComponentsTooltipString(template, entity, buildingsCountToTrainFullBatch = 1, fullBatchSize = 1, remainderBatch = 0)
{
let totalCosts = multiplyEntityCosts(template, buildingsCountToTrainFullBatch * fullBatchSize + remainderBatch);
if (template.cost.time)
totalCosts.time = Math.ceil(template.cost.time * (entity ? Engine.GuiInterfaceCall("GetBatchTime", {
"entity": entity,
"batchSize": buildingsCountToTrainFullBatch > 0 ? fullBatchSize : remainderBatch
}) : 1));
let costs = [];
for (let type of getCostTypes())
// Population bonus is shown in the tooltip
if (type != "populationBonus" && totalCosts[type])
costs.push(sprintf(translate("%(component)s %(cost)s"), {
"component": resourceIcon(type),
"cost": totalCosts[type]
}));
return costs;
}
function getGatherTooltip(template)
{
if (!template.resourceGatherRates)
return "";
// Average the resource rates (TODO: distinguish between subtypes)
let rates = {};
for (let resource of g_ResourceData.GetResources())
{
let types = [resource.code];
for (let subtype in resource.subtypes)
// We ignore ruins as those are not that common and skew the results
if (subtype !== "ruins")
types.push(resource.code + "." + subtype);
let [rate, count] = types.reduce((sum, t) => {
let r = template.resourceGatherRates[t];
return [sum[0] + (r > 0 ? r : 0), sum[1] + (r > 0 ? 1 : 0)];
}, [0, 0]);
if (rate > 0)
rates[resource.code] = +(rate / count).toFixed(2);
}
if (!Object.keys(rates).length)
return "";
return sprintf(translate("%(label)s %(details)s"), {
"label": headerFont(translate("Gather Rates:")),
"details":
Object.keys(rates).map(
type => sprintf(translate("%(resourceIcon)s %(rate)s"), {
"resourceIcon": resourceIcon(type),
"rate": rates[type]
})
).join(" ")
});
}
/**
* Returns the resources this entity supplies in the specified entity's tooltip
*/
function getResourceSupplyTooltip(template)
{
if (!template.supply)
return "";
let supply = template.supply;
let type = supply.type[0] == "treasure" ? supply.type[1] : supply.type[0];
// Translation: Label in tooltip showing the resource type and quantity of a given resource supply.
return sprintf(translate("%(label)s %(component)s %(amount)s"), {
"label": headerFont(translate("Resource Supply:")),
"component": resourceIcon(type),
// Translation: Marks that a resource supply entity has an unending, infinite, supply of its resource.
"amount": Number.isFinite(+supply.amount) ? supply.amount : translate("∞")
});
}
function getResourceTrickleTooltip(template)
{
if (!template.resourceTrickle)
return "";
let resCodes = g_ResourceData.GetCodes().filter(res => !!template.resourceTrickle.rates[res]);
if (!resCodes.length)
return "";
return sprintf(translate("%(label)s %(details)s"), {
"label": headerFont(translate("Resource Trickle:")),
"details": sprintf(translate("%(resources)s / %(time)s"), {
"resources":
resCodes.map(
res => sprintf(translate("%(resourceIcon)s %(rate)s"), {
"resourceIcon": resourceIcon(res),
"rate": template.resourceTrickle.rates[res]
})
).join(" "),
"time": getSecondsString(template.resourceTrickle.interval / 1000)
})
});
}
/**
* Returns an array of strings for a set of wall pieces. If the pieces share
* resource type requirements, output will be of the form '10 to 30 Stone',
* otherwise output will be, e.g. '10 Stone, 20 Stone, 30 Stone'.
*/
function getWallPieceTooltip(wallTypes)
{
let out = [];
let resourceCount = {};
for (let resource of getCostTypes())
if (wallTypes[0].cost[resource])
resourceCount[resource] = [wallTypes[0].cost[resource]];
let sameTypes = true;
for (let i = 1; i < wallTypes.length; ++i)
{
for (let resource in wallTypes[i].cost)
// Break out of the same-type mode if this wall requires
// resource types that the first didn't.
if (wallTypes[i].cost[resource] && !resourceCount[resource])
{
sameTypes = false;
break;
}
for (let resource in resourceCount)
if (wallTypes[i].cost[resource])
resourceCount[resource].push(wallTypes[i].cost[resource]);
else
{
sameTypes = false;
break;
}
}
if (sameTypes)
for (let resource in resourceCount)
// Translation: This string is part of the resources cost string on
// the tooltip for wall structures.
out.push(sprintf(translate("%(resourceIcon)s %(minimum)s to %(resourceIcon)s %(maximum)s"), {
"resourceIcon": resourceIcon(resource),
"minimum": Math.min.apply(Math, resourceCount[resource]),
"maximum": Math.max.apply(Math, resourceCount[resource])
}));
else
for (let i = 0; i < wallTypes.length; ++i)
out.push(getEntityCostComponentsTooltipString(wallTypes[i]).join(", "));
return out;
}
/**
* Returns the cost information to display in the specified entity's construction button tooltip.
*/
function getEntityCostTooltip(template, player, entity, buildingsCountToTrainFullBatch, fullBatchSize, remainderBatch)
{
// Entities with a wallset component are proxies for initiating wall placement and as such do not have a cost of
// their own; the individual wall pieces within it do.
if (template.wallSet)
{
let templateLong = GetTemplateData(template.wallSet.templates.long, player);
let templateMedium = GetTemplateData(template.wallSet.templates.medium, player);
let templateShort = GetTemplateData(template.wallSet.templates.short, player);
let templateTower = GetTemplateData(template.wallSet.templates.tower, player);
let wallCosts = getWallPieceTooltip([templateShort, templateMedium, templateLong]);
let towerCosts = getEntityCostComponentsTooltipString(templateTower);
return sprintf(translate("Walls: %(costs)s"), { "costs": wallCosts.join(" ") }) + "\n" +
sprintf(translate("Towers: %(costs)s"), { "costs": towerCosts.join(" ") });
}
if (template.cost)
{
let costs = getEntityCostComponentsTooltipString(template, entity, buildingsCountToTrainFullBatch, fullBatchSize, remainderBatch).join(" ");
if (costs)
// Translation: Label in tooltip showing cost of a unit, structure or technology.
return sprintf(translate("%(label)s %(costs)s"), {
"label": headerFont(translate("Cost:")),
"costs": costs
});
}
return "";
}
function getRequiredTechnologyTooltip(technologyEnabled, requiredTechnology, civ)
{
if (technologyEnabled)
return "";
return sprintf(translate("Requires %(technology)s"), {
"technology": getEntityNames(GetTechnologyData(requiredTechnology, civ))
});
}
/**
* Returns the population bonus information to display in the specified entity's construction button tooltip.
*/
function getPopulationBonusTooltip(template)
{
let popBonus = "";
if (template.cost && template.cost.populationBonus)
popBonus = sprintf(translate("%(label)s %(populationBonus)s"), {
"label": headerFont(translate("Population Bonus:")),
"populationBonus": template.cost.populationBonus
});
return popBonus;
}
/**
* Returns a message with the amount of each resource needed to create an entity.
*/
function getNeededResourcesTooltip(resources)
{
if (!resources)
return "";
let formatted = [];
for (let resource in resources)
formatted.push(sprintf(translate("%(component)s %(cost)s"), {
"component": '[font="sans-12"]' + resourceIcon(resource) + '[/font]',
"cost": resources[resource]
}));
return coloredText(
'[font="sans-bold-13"]' + translate("Insufficient resources:") + '[/font]',
"red") + " " +
formatted.join(" ");
}
function getSpeedTooltip(template)
{
if (!template.speed)
return "";
let walk = template.speed.walk.toFixed(1);
let run = template.speed.run.toFixed(1);
if (walk == 0 && run == 0)
return "";
return sprintf(translate("%(label)s %(speeds)s"), {
"label": headerFont(translate("Speed:")),
"speeds":
sprintf(translate("%(speed)s %(movementType)s"), {
"speed": walk,
"movementType": unitFont(translate("Walk"))
}) +
commaFont(translate(", ")) +
sprintf(translate("%(speed)s %(movementType)s"), {
"speed": run,
"movementType": unitFont(translate("Run"))
})
});
}
function getHealerTooltip(template)
{
if (!template.heal)
return "";
let health = +(template.heal.health.toFixed(1));
let range = +(template.heal.range.toFixed(0));
let interval = +((template.heal.interval / 1000).toFixed(1));
return [
sprintf(translatePlural("%(label)s %(val)s %(unit)s", "%(label)s %(val)s %(unit)s", health), {
"label": headerFont(translate("Heal:")),
"val": health,
"unit": unitFont(translatePlural("Health", "Health", health))
}),
sprintf(translatePlural("%(label)s %(val)s %(unit)s", "%(label)s %(val)s %(unit)s", range), {
"label": headerFont(translate("Range:")),
"val": range,
"unit": unitFont(translatePlural("meter", "meters", range))
}),
sprintf(translatePlural("%(label)s %(val)s %(unit)s", "%(label)s %(val)s %(unit)s", interval), {
"label": headerFont(translate("Interval:")),
"val": interval,
"unit": unitFont(translatePlural("second", "seconds", interval))
})
].join(translate(", "));
}
function getAurasTooltip(template)
{
let auras = template.auras || template.wallSet && GetTemplateData(template.wallSet.templates.long).auras;
if (!auras)
return "";
let tooltips = [];
for (let auraID in auras)
{
let tooltip = sprintf(translate("%(auralabel)s %(aurainfo)s"), {
"auralabel": headerFont(sprintf(translate("%(auraname)s:"), {
"auraname": translate(auras[auraID].name)
})),
"aurainfo": bodyFont(translate(auras[auraID].description))
});
let radius = +auras[auraID].radius;
if (radius)
tooltip += " " + sprintf(translatePlural("%(label)s %(val)s %(unit)s", "%(label)s %(val)s %(unit)s", radius), {
"label": translateWithContext("aura", "Range:"),
"val": radius,
"unit": unitFont(translatePlural("meter", "meters", radius))
});
tooltips.push(tooltip);
}
return tooltips.join("\n");
}
function getEntityNames(template)
{
if (!template.name.specific)
return template.name.generic;
if (template.name.specific == template.name.generic)
return template.name.specific;
return sprintf(translate("%(specificName)s (%(genericName)s)"), {
"specificName": template.name.specific,
"genericName": template.name.generic
});
}
function getEntityNamesFormatted(template)
{
if (!template.name.specific)
return setStringTags(template.name.generic, g_TooltipTextFormats.nameSpecificBig);
// Translation: Example: "Epibátēs Athēnaîos [font="sans-bold-16"](Athenian Marine)[/font]"
return sprintf(translate("%(specificName)s %(fontStart)s(%(genericName)s)%(fontEnd)s"), {
"specificName":
setStringTags(template.name.specific[0], g_TooltipTextFormats.nameSpecificBig) +
setStringTags(template.name.specific.slice(1).toUpperCase(), g_TooltipTextFormats.nameSpecificSmall),
"genericName": template.name.generic,
"fontStart": '[font="' + g_TooltipTextFormats.nameGeneric.font + '"]',
"fontEnd": '[/font]'
});
}
function getVisibleEntityClassesFormatted(template)
{
if (!template.visibleIdentityClasses || !template.visibleIdentityClasses.length)
return "";
return headerFont(translate("Classes:")) + ' ' +
bodyFont(template.visibleIdentityClasses.map(c => translate(c)).join(translate(", ")));
}
function getLootTooltip(template)
{
if (!template.loot && !template.resourceCarrying)
return "";
let resourcesCarried = [];
if (template.resourceCarrying)
resourcesCarried = calculateCarriedResources(
template.resourceCarrying,
template.trader && template.trader.goods
);
let lootLabels = [];
for (let type of g_ResourceData.GetCodes().concat(["xp"]))
{
let loot =
(template.loot && template.loot[type] || 0) +
(resourcesCarried[type] || 0);
if (!loot)
continue;
// Translation: %(component) will be the icon for the loot type and %(loot) will be the value.
lootLabels.push(sprintf(translate("%(component)s %(loot)s"), {
"component": resourceIcon(type),
"loot": loot
}));
}
if (!lootLabels.length)
return "";
return sprintf(translate("%(label)s %(details)s"), {
"label": headerFont(translate("Loot:")),
"details": lootLabels.join(" ")
});
}
function getResourceDropsiteTooltip(template)
{
if (!template || !template.resourceDropsite || !template.resourceDropsite.types)
return "";
return sprintf(translate("%(label)s %(icons)s"), {
"label": headerFont(translate("Dropsite for:")),
"icons": template.resourceDropsite.types.map(type => resourceIcon(type)).join(" ")
});
}
function showTemplateViewerOnRightClickTooltip()
{
// Translation: Appears in a tooltip to indicate that right-clicking the corresponding GUI element will open the Template Details GUI page.
return translate("Right-click to view more information.");
}
function showTemplateViewerOnClickTooltip()
{
// Translation: Appears in a tooltip to indicate that clicking the corresponding GUI element will open the Template Details GUI page.
return translate("Click to view more information.");
}
Index: ps/trunk/binaries/data/mods/public/gui/session/selection_details.js
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/session/selection_details.js (revision 24161)
+++ ps/trunk/binaries/data/mods/public/gui/session/selection_details.js (revision 24162)
@@ -1,543 +1,543 @@
function layoutSelectionSingle()
{
Engine.GetGUIObjectByName("detailsAreaSingle").hidden = false;
Engine.GetGUIObjectByName("detailsAreaMultiple").hidden = true;
}
function layoutSelectionMultiple()
{
Engine.GetGUIObjectByName("detailsAreaMultiple").hidden = false;
Engine.GetGUIObjectByName("detailsAreaSingle").hidden = true;
}
function getResourceTypeDisplayName(resourceType)
{
return resourceNameFirstWord(
resourceType.generic == "treasure" ?
resourceType.specific :
resourceType.generic);
}
// Updates the health bar of garrisoned units
function updateGarrisonHealthBar(entState, selection)
{
if (!entState.garrisonHolder)
return;
// Summing up the Health of every single unit
let totalGarrisonHealth = 0;
let maxGarrisonHealth = 0;
for (let selEnt of selection)
{
let selEntState = GetEntityState(selEnt);
if (selEntState.garrisonHolder)
for (let ent of selEntState.garrisonHolder.entities)
{
let state = GetEntityState(ent);
totalGarrisonHealth += state.hitpoints || 0;
maxGarrisonHealth += state.maxHitpoints || 0;
}
}
// Configuring the health bar
let healthGarrison = Engine.GetGUIObjectByName("healthGarrison");
healthGarrison.hidden = totalGarrisonHealth <= 0;
if (totalGarrisonHealth > 0)
{
let healthBarGarrison = Engine.GetGUIObjectByName("healthBarGarrison");
let healthSize = healthBarGarrison.size;
healthSize.rtop = 100 - 100 * Math.max(0, Math.min(1, totalGarrisonHealth / maxGarrisonHealth));
healthBarGarrison.size = healthSize;
healthGarrison.tooltip = getCurrentHealthTooltip({
"hitpoints": totalGarrisonHealth,
"maxHitpoints": maxGarrisonHealth
});
}
}
// Fills out information that most entities have
function displaySingle(entState)
{
// Get general unit and player data
let template = GetTemplateData(entState.template);
let specificName = template.name.specific;
let genericName = template.name.generic;
// If packed, add that to the generic name (reduces template clutter)
if (genericName && template.pack && template.pack.state == "packed")
genericName = sprintf(translate("%(genericName)s — Packed"), { "genericName": genericName });
let playerState = g_Players[entState.player];
let civName = g_CivData[playerState.civ].Name;
let civEmblem = g_CivData[playerState.civ].Emblem;
let playerName = playerState.name;
// Indicate disconnected players by prefixing their name
if (g_Players[entState.player].offline)
playerName = sprintf(translate("\\[OFFLINE] %(player)s"), { "player": playerName });
// Rank
if (entState.identity && entState.identity.rank && entState.identity.classes)
{
Engine.GetGUIObjectByName("rankIcon").tooltip = sprintf(translate("%(rank)s Rank"), {
"rank": translateWithContext("Rank", entState.identity.rank)
});
Engine.GetGUIObjectByName("rankIcon").sprite = "stretched:session/icons/ranks/" + entState.identity.rank + ".png";
Engine.GetGUIObjectByName("rankIcon").hidden = false;
}
else
{
Engine.GetGUIObjectByName("rankIcon").hidden = true;
Engine.GetGUIObjectByName("rankIcon").tooltip = "";
}
if (entState.statusEffects)
{
let statusEffectsSection = Engine.GetGUIObjectByName("statusEffectsIcons");
statusEffectsSection.hidden = false;
let statusIcons = statusEffectsSection.children;
let i = 0;
- for (let effectName in entState.statusEffects)
+ for (let effectCode in entState.statusEffects)
{
- let effect = entState.statusEffects[effectName];
+ let effect = entState.statusEffects[effectCode];
statusIcons[i].hidden = false;
- statusIcons[i].sprite = "stretched:session/icons/status_effects/" + (effect.Icon || "default") + ".png";
- statusIcons[i].tooltip = getStatusEffectsTooltip(effect, false);
+ statusIcons[i].sprite = "stretched:session/icons/status_effects/" + g_StatusEffectsMetadata.getIcon(effect.baseCode) + ".png";
+ statusIcons[i].tooltip = getStatusEffectsTooltip(effect.baseCode, effect, false);
let size = statusIcons[i].size;
size.top = i * 18;
size.bottom = i * 18 + 16;
statusIcons[i].size = size;
if (++i >= statusIcons.length)
break;
}
for (; i < statusIcons.length; ++i)
statusIcons[i].hidden = true;
}
else
Engine.GetGUIObjectByName("statusEffectsIcons").hidden = true;
let showHealth = entState.hitpoints;
let showResource = entState.resourceSupply;
let healthSection = Engine.GetGUIObjectByName("healthSection");
let captureSection = Engine.GetGUIObjectByName("captureSection");
let resourceSection = Engine.GetGUIObjectByName("resourceSection");
let sectionPosTop = Engine.GetGUIObjectByName("sectionPosTop");
let sectionPosMiddle = Engine.GetGUIObjectByName("sectionPosMiddle");
let sectionPosBottom = Engine.GetGUIObjectByName("sectionPosBottom");
// Hitpoints
healthSection.hidden = !showHealth;
if (showHealth)
{
let unitHealthBar = Engine.GetGUIObjectByName("healthBar");
let healthSize = unitHealthBar.size;
healthSize.rright = 100 * Math.max(0, Math.min(1, entState.hitpoints / entState.maxHitpoints));
unitHealthBar.size = healthSize;
Engine.GetGUIObjectByName("healthStats").caption = sprintf(translate("%(hitpoints)s / %(maxHitpoints)s"), {
"hitpoints": Math.ceil(entState.hitpoints),
"maxHitpoints": Math.ceil(entState.maxHitpoints)
});
healthSection.size = sectionPosTop.size;
captureSection.size = showResource ? sectionPosMiddle.size : sectionPosBottom.size;
resourceSection.size = showResource ? sectionPosBottom.size : sectionPosMiddle.size;
}
else
{
captureSection.size = sectionPosBottom.size;
resourceSection.size = sectionPosTop.size;
}
// CapturePoints
captureSection.hidden = !entState.capturePoints;
if (entState.capturePoints)
{
let setCaptureBarPart = function(playerID, startSize) {
let unitCaptureBar = Engine.GetGUIObjectByName("captureBar[" + playerID + "]");
let sizeObj = unitCaptureBar.size;
sizeObj.rleft = startSize;
let size = 100 * Math.max(0, Math.min(1, entState.capturePoints[playerID] / entState.maxCapturePoints));
sizeObj.rright = startSize + size;
unitCaptureBar.size = sizeObj;
unitCaptureBar.sprite = "color:" + g_DiplomacyColors.getPlayerColor(playerID, 128);
unitCaptureBar.hidden = false;
return startSize + size;
};
// first handle the owner's points, to keep those points on the left for clarity
let size = setCaptureBarPart(entState.player, 0);
for (let i in entState.capturePoints)
if (i != entState.player)
size = setCaptureBarPart(i, size);
let captureText = sprintf(translate("%(capturePoints)s / %(maxCapturePoints)s"), {
"capturePoints": Math.ceil(entState.capturePoints[entState.player]),
"maxCapturePoints": Math.ceil(entState.maxCapturePoints)
});
let showSmallCapture = showResource && showHealth;
Engine.GetGUIObjectByName("captureStats").caption = showSmallCapture ? "" : captureText;
Engine.GetGUIObjectByName("capture").tooltip = showSmallCapture ? captureText : "";
}
// Experience
Engine.GetGUIObjectByName("experience").hidden = !entState.promotion;
if (entState.promotion)
{
let experienceBar = Engine.GetGUIObjectByName("experienceBar");
let experienceSize = experienceBar.size;
experienceSize.rtop = 100 - (100 * Math.max(0, Math.min(1, 1.0 * +entState.promotion.curr / +entState.promotion.req)));
experienceBar.size = experienceSize;
if (entState.promotion.curr < entState.promotion.req)
Engine.GetGUIObjectByName("experience").tooltip = sprintf(translate("%(experience)s %(current)s / %(required)s"), {
"experience": "[font=\"sans-bold-13\"]" + translate("Experience:") + "[/font]",
"current": Math.floor(entState.promotion.curr),
"required": entState.promotion.req
});
else
Engine.GetGUIObjectByName("experience").tooltip = sprintf(translate("%(experience)s %(current)s"), {
"experience": "[font=\"sans-bold-13\"]" + translate("Experience:") + "[/font]",
"current": Math.floor(entState.promotion.curr)
});
}
// Resource stats
resourceSection.hidden = !showResource;
if (entState.resourceSupply)
{
let resources = entState.resourceSupply.isInfinite ? translate("∞") : // Infinity symbol
sprintf(translate("%(amount)s / %(max)s"), {
"amount": Math.ceil(+entState.resourceSupply.amount),
"max": entState.resourceSupply.max
});
let unitResourceBar = Engine.GetGUIObjectByName("resourceBar");
let resourceSize = unitResourceBar.size;
resourceSize.rright = entState.resourceSupply.isInfinite ? 100 :
100 * Math.max(0, Math.min(1, +entState.resourceSupply.amount / +entState.resourceSupply.max));
unitResourceBar.size = resourceSize;
Engine.GetGUIObjectByName("resourceLabel").caption = sprintf(translate("%(resource)s:"), {
"resource": getResourceTypeDisplayName(entState.resourceSupply.type)
});
Engine.GetGUIObjectByName("resourceStats").caption = resources;
}
let resourceCarryingIcon = Engine.GetGUIObjectByName("resourceCarryingIcon");
let resourceCarryingText = Engine.GetGUIObjectByName("resourceCarryingText");
resourceCarryingIcon.hidden = false;
resourceCarryingText.hidden = false;
// Resource carrying
if (entState.resourceCarrying && entState.resourceCarrying.length)
{
// We should only be carrying one resource type at once, so just display the first
let carried = entState.resourceCarrying[0];
resourceCarryingIcon.sprite = "stretched:session/icons/resources/" + carried.type + ".png";
resourceCarryingText.caption = sprintf(translate("%(amount)s / %(max)s"), { "amount": carried.amount, "max": carried.max });
resourceCarryingIcon.tooltip = "";
}
// Use the same indicators for traders
else if (entState.trader && entState.trader.goods.amount)
{
resourceCarryingIcon.sprite = "stretched:session/icons/resources/" + entState.trader.goods.type + ".png";
let totalGain = entState.trader.goods.amount.traderGain;
if (entState.trader.goods.amount.market1Gain)
totalGain += entState.trader.goods.amount.market1Gain;
if (entState.trader.goods.amount.market2Gain)
totalGain += entState.trader.goods.amount.market2Gain;
resourceCarryingText.caption = totalGain;
resourceCarryingIcon.tooltip = sprintf(translate("Gain: %(gain)s"), {
"gain": getTradingTooltip(entState.trader.goods.amount)
});
}
// And for number of workers
else if (entState.foundation)
{
resourceCarryingIcon.sprite = "stretched:session/icons/repair.png";
resourceCarryingIcon.tooltip = getBuildTimeTooltip(entState);
resourceCarryingText.caption = entState.foundation.numBuilders ?
Engine.FormatMillisecondsIntoDateStringGMT(entState.foundation.buildTime.timeRemaining * 1000, translateWithContext("countdown format", "m:ss")) : "";
}
else if (entState.resourceSupply && (!entState.resourceSupply.killBeforeGather || !entState.hitpoints))
{
resourceCarryingIcon.sprite = "stretched:session/icons/repair.png";
resourceCarryingText.caption = sprintf(translate("%(amount)s / %(max)s"), {
"amount": entState.resourceSupply.numGatherers,
"max": entState.resourceSupply.maxGatherers
});
Engine.GetGUIObjectByName("resourceCarryingIcon").tooltip = translate("Current/max gatherers");
}
else if (entState.repairable && entState.needsRepair)
{
resourceCarryingIcon.sprite = "stretched:session/icons/repair.png";
resourceCarryingIcon.tooltip = getRepairTimeTooltip(entState);
resourceCarryingText.caption = entState.repairable.numBuilders ?
Engine.FormatMillisecondsIntoDateStringGMT(entState.repairable.buildTime.timeRemaining * 1000, translateWithContext("countdown format", "m:ss")) : "";
}
else
{
resourceCarryingIcon.hidden = true;
resourceCarryingText.hidden = true;
}
Engine.GetGUIObjectByName("specific").caption = specificName;
Engine.GetGUIObjectByName("player").caption = playerName;
Engine.GetGUIObjectByName("playerColorBackground").sprite =
"color:" + g_DiplomacyColors.getPlayerColor(entState.player, 128);
Engine.GetGUIObjectByName("generic").caption = genericName == specificName ? "" :
sprintf(translate("(%(genericName)s)"), {
"genericName": genericName
});
let isGaia = playerState.civ == "gaia";
Engine.GetGUIObjectByName("playerCivIcon").sprite = isGaia ? "" : "stretched:grayscale:" + civEmblem;
Engine.GetGUIObjectByName("player").tooltip = isGaia ? "" : civName;
// TODO: we should require all entities to have icons
Engine.GetGUIObjectByName("icon").sprite = template.icon ? ("stretched:session/portraits/" + template.icon) : "BackgroundBlack";
if (template.icon)
Engine.GetGUIObjectByName("iconBorder").onPressRight = () => {
showTemplateDetails(entState.template);
};
Engine.GetGUIObjectByName("attackAndResistanceStats").tooltip = [
getAttackTooltip,
getHealerTooltip,
getResistanceTooltip,
getGatherTooltip,
getSpeedTooltip,
getGarrisonTooltip,
getProjectilesTooltip,
getResourceTrickleTooltip,
getLootTooltip
].map(func => func(entState)).filter(tip => tip).join("\n");
let iconTooltips = [];
if (genericName)
iconTooltips.push("[font=\"sans-bold-16\"]" + genericName + "[/font]");
iconTooltips = iconTooltips.concat([
getVisibleEntityClassesFormatted,
getAurasTooltip,
getEntityTooltip,
showTemplateViewerOnRightClickTooltip
].map(func => func(template)));
Engine.GetGUIObjectByName("iconBorder").tooltip = iconTooltips.filter(tip => tip).join("\n");
Engine.GetGUIObjectByName("detailsAreaSingle").hidden = false;
Engine.GetGUIObjectByName("detailsAreaMultiple").hidden = true;
}
// Fills out information for multiple entities
function displayMultiple(entStates)
{
let averageHealth = 0;
let maxHealth = 0;
let maxCapturePoints = 0;
let capturePoints = (new Array(g_MaxPlayers + 1)).fill(0);
let playerID = 0;
let totalCarrying = {};
let totalLoot = {};
for (let entState of entStates)
{
playerID = entState.player; // trust that all selected entities have the same owner
if (entState.hitpoints)
{
averageHealth += entState.hitpoints;
maxHealth += entState.maxHitpoints;
}
if (entState.capturePoints)
{
maxCapturePoints += entState.maxCapturePoints;
capturePoints = entState.capturePoints.map((v, i) => v + capturePoints[i]);
}
let carrying = calculateCarriedResources(
entState.resourceCarrying || null,
entState.trader && entState.trader.goods
);
if (entState.loot)
for (let type in entState.loot)
totalLoot[type] = (totalLoot[type] || 0) + entState.loot[type];
for (let type in carrying)
{
totalCarrying[type] = (totalCarrying[type] || 0) + carrying[type];
totalLoot[type] = (totalLoot[type] || 0) + carrying[type];
}
}
Engine.GetGUIObjectByName("healthMultiple").hidden = averageHealth <= 0;
if (averageHealth > 0)
{
let unitHealthBar = Engine.GetGUIObjectByName("healthBarMultiple");
let healthSize = unitHealthBar.size;
healthSize.rtop = 100 - 100 * Math.max(0, Math.min(1, averageHealth / maxHealth));
unitHealthBar.size = healthSize;
Engine.GetGUIObjectByName("healthMultiple").tooltip = getCurrentHealthTooltip({
"hitpoints": averageHealth,
"maxHitpoints": maxHealth
});
}
Engine.GetGUIObjectByName("captureMultiple").hidden = maxCapturePoints <= 0;
if (maxCapturePoints > 0)
{
let setCaptureBarPart = function(pID, startSize)
{
let unitCaptureBar = Engine.GetGUIObjectByName("captureBarMultiple[" + pID + "]");
let sizeObj = unitCaptureBar.size;
sizeObj.rtop = startSize;
let size = 100 * Math.max(0, Math.min(1, capturePoints[pID] / maxCapturePoints));
sizeObj.rbottom = startSize + size;
unitCaptureBar.size = sizeObj;
unitCaptureBar.sprite = "color:" + g_DiplomacyColors.getPlayerColor(pID, 128);
unitCaptureBar.hidden = false;
return startSize + size;
};
let size = 0;
for (let i in capturePoints)
if (i != playerID)
size = setCaptureBarPart(i, size);
// last handle the owner's points, to keep those points on the bottom for clarity
setCaptureBarPart(playerID, size);
Engine.GetGUIObjectByName("captureMultiple").tooltip = getCurrentHealthTooltip(
{
"hitpoints": capturePoints[playerID],
"maxHitpoints": maxCapturePoints
},
translate("Capture Points:"));
}
let numberOfUnits = Engine.GetGUIObjectByName("numberOfUnits");
numberOfUnits.caption = entStates.length;
numberOfUnits.tooltip = "";
if (Object.keys(totalCarrying).length)
numberOfUnits.tooltip = sprintf(translate("%(label)s %(details)s\n"), {
"label": headerFont(translate("Carrying:")),
"details": bodyFont(Object.keys(totalCarrying).filter(
res => totalCarrying[res] != 0).map(
res => sprintf(translate("%(type)s %(amount)s"),
{ "type": resourceIcon(res), "amount": totalCarrying[res] })).join(" "))
});
if (Object.keys(totalLoot).length)
numberOfUnits.tooltip += sprintf(translate("%(label)s %(details)s"), {
"label": headerFont(translate("Loot:")),
"details": bodyFont(Object.keys(totalLoot).filter(
res => totalLoot[res] != 0).map(
res => sprintf(translate("%(type)s %(amount)s"),
{ "type": resourceIcon(res), "amount": totalLoot[res] })).join(" "))
});
// Unhide Details Area
Engine.GetGUIObjectByName("detailsAreaMultiple").hidden = false;
Engine.GetGUIObjectByName("detailsAreaSingle").hidden = true;
}
// Updates middle entity Selection Details Panel and left Unit Commands Panel
function updateSelectionDetails()
{
let supplementalDetailsPanel = Engine.GetGUIObjectByName("supplementalSelectionDetails");
let detailsPanel = Engine.GetGUIObjectByName("selectionDetails");
let commandsPanel = Engine.GetGUIObjectByName("unitCommands");
let entStates = [];
for (let sel of g_Selection.toList())
{
let entState = GetEntityState(sel);
if (!entState)
continue;
entStates.push(entState);
}
if (entStates.length == 0)
{
Engine.GetGUIObjectByName("detailsAreaMultiple").hidden = true;
Engine.GetGUIObjectByName("detailsAreaSingle").hidden = true;
hideUnitCommands();
supplementalDetailsPanel.hidden = true;
detailsPanel.hidden = true;
commandsPanel.hidden = true;
return;
}
// Fill out general info and display it
if (entStates.length == 1)
displaySingle(entStates[0]);
else
displayMultiple(entStates);
// Show basic details.
detailsPanel.hidden = false;
// Fill out commands panel for specific unit selected (or first unit of primary group)
updateUnitCommands(entStates, supplementalDetailsPanel, commandsPanel);
// Show health bar for garrisoned units if the garrison panel is visible
if (Engine.GetGUIObjectByName("unitGarrisonPanel") && !Engine.GetGUIObjectByName("unitGarrisonPanel").hidden)
updateGarrisonHealthBar(entStates[0], g_Selection.toList());
}
function tradingGainString(gain, owner)
{
// Translation: Used in the trading gain tooltip
return sprintf(translate("%(gain)s (%(player)s)"), {
"gain": gain,
"player": GetSimState().players[owner].name
});
}
/**
* Returns a message with the details of the trade gain.
*/
function getTradingTooltip(gain)
{
if (!gain)
return "";
let markets = [
{ "gain": gain.market1Gain, "owner": gain.market1Owner },
{ "gain": gain.market2Gain, "owner": gain.market2Owner }
];
let primaryGain = gain.traderGain;
for (let market of markets)
if (market.gain && market.owner == gain.traderOwner)
// Translation: Used in the trading gain tooltip to concatenate profits of different players
primaryGain += translate("+") + market.gain;
let tooltip = tradingGainString(primaryGain, gain.traderOwner);
for (let market of markets)
if (market.gain && market.owner != gain.traderOwner)
tooltip +=
translateWithContext("Separation mark in an enumeration", ", ") +
tradingGainString(market.gain, market.owner);
return tooltip;
}
Index: ps/trunk/binaries/data/mods/public/l10n/messages.json
===================================================================
--- ps/trunk/binaries/data/mods/public/l10n/messages.json (revision 24161)
+++ ps/trunk/binaries/data/mods/public/l10n/messages.json (revision 24162)
@@ -1,808 +1,799 @@
[
{
"output": "public-civilizations.pot",
"inputRoot": "..",
"project": "0 A.D. — Empires Ascendant",
"copyrightHolder": "Wildfire Games",
"rules": [
{
"extractor": "json",
"filemasks": [
"simulation/data/civs/**.json"
],
"options": {
"keywords": [
"Name",
"Description",
"History",
"Special",
"AINames"
]
}
}
]
},
{
"output": "public-gui-ingame.pot",
"inputRoot": "..",
"project": "0 A.D. — Empires Ascendant",
"copyrightHolder": "Wildfire Games",
"rules": [
{
"extractor": "javascript",
"filemasks": [
"gui/session/**.js"
],
"options": {
"format": "javascript-format",
"keywords": {
"translate": [1],
"translatePlural": [1, 2],
"translateWithContext": [[1], 2],
"translatePluralWithContext": [[1], 2, 3],
"markForTranslation": [1],
"markForTranslationWithContext": [[1], 2],
"markForPluralTranslation": [1, 2]
},
"commentTags": [
"Translation:"
]
}
},
{
"extractor": "xml",
"filemasks": [
"gui/session/**.xml"
],
"options": {
"keywords": {
"translatableAttribute": {
"locationAttributes": ["id"]
},
"translate": {}
}
}
}
]
},
{
"output": "public-gui-gamesetup.pot",
"inputRoot": "..",
"project": "0 A.D. — Empires Ascendant",
"copyrightHolder": "Wildfire Games",
"rules": [
{
"extractor": "javascript",
"filemasks": [
"gui/gamesetup/**.js",
"gui/gamesetup_mp/**.js",
"gui/loading/**.js"
],
"options": {
"format": "javascript-format",
"keywords": {
"translate": [1],
"translatePlural": [1, 2],
"translateWithContext": [[1], 2],
"translatePluralWithContext": [[1], 2, 3],
"markForTranslation": [1],
"markForTranslationWithContext": [[1], 2],
"markForPluralTranslation": [1, 2]
},
"commentTags": [
"Translation:"
]
}
},
{
"extractor": "xml",
"filemasks": [
"gui/gamesetup/**.xml",
"gui/gamesetup_mp/**.xml",
"gui/loading/**.xml"
],
"options": {
"keywords": {
"translatableAttribute": {
"locationAttributes": ["id"]
},
"translate": {}
}
}
},
{
"extractor": "txt",
"filemasks": [
"gui/text/quotes.txt"
],
"options": {
}
}
]
},
{
"output": "public-gui-lobby.pot",
"inputRoot": "..",
"project": "0 A.D. — Empires Ascendant",
"copyrightHolder": "Wildfire Games",
"rules": [
{
"extractor": "javascript",
"filemasks": [
"gui/lobby/**.js",
"gui/prelobby/**.js"
],
"options": {
"format": "javascript-format",
"keywords": {
"translate": [1],
"translatePlural": [1, 2],
"translateWithContext": [[1], 2],
"translatePluralWithContext": [[1], 2, 3],
"markForTranslation": [1],
"markForTranslationWithContext": [[1], 2],
"markForPluralTranslation": [1, 2]
},
"commentTags": [
"Translation:"
]
}
},
{
"extractor": "xml",
"filemasks": [
"gui/lobby/**.xml",
"gui/prelobby/**.xml"
],
"options": {
"keywords": {
"translatableAttribute": {
"locationAttributes": ["id"]
},
"translate": {}
}
}
},
{
"extractor": "txt",
"filemasks": [
"gui/prelobby/common/terms/*.txt"
],
"options": {
}
}
]
},
{
"output": "public-gui-manual.pot",
"inputRoot": "..",
"project": "0 A.D. — Empires Ascendant",
"copyrightHolder": "Wildfire Games",
"rules": [
{
"extractor": "javascript",
"filemasks": [
"gui/manual/**.js"
],
"options": {
"format": "javascript-format",
"keywords": {
"translate": [1],
"translatePlural": [1, 2],
"translateWithContext": [[1], 2],
"translatePluralWithContext": [[1], 2, 3],
"markForTranslation": [1],
"markForTranslationWithContext": [[1], 2],
"markForPluralTranslation": [1, 2]
},
"commentTags": [
"Translation:"
]
}
},
{
"extractor": "xml",
"filemasks": [
"gui/manual/**.xml"
],
"options": {
"keywords": {
"translatableAttribute": {
"locationAttributes": ["id"]
},
"translate": {}
}
}
},
{
"extractor": "txt",
"filemasks": [
"gui/manual/intro.txt"
],
"options": {
}
}
]
},
{
"output": "public-gui-userreport.pot",
"inputRoot": "..",
"project": "0 A.D. — Empires Ascendant",
"copyrightHolder": "Wildfire Games",
"rules": [
{
"extractor": "txt",
"filemasks": [
"gui/userreport/**.txt"
],
"options": {
}
}
]
},
{
"output": "public-gui-other.pot",
"inputRoot": "..",
"project": "0 A.D. — Empires Ascendant",
"copyrightHolder": "Wildfire Games",
"rules": [
{
"extractor": "javascript",
"filemasks": [
"globalscripts/**.js",
"gui/civinfo/**.js",
"gui/common/**.js",
"gui/credits/**.js",
"gui/loadgame/**.js",
"gui/locale/**.js",
"gui/options/**.js",
"gui/pregame/**.js",
"gui/reference/common/**.js",
"gui/reference/structree/**.js",
"gui/reference/viewer/**.js",
"gui/replaymenu/**.js",
"gui/splashscreen/**.js",
"gui/summary/**.js"
],
"options": {
"format": "javascript-format",
"keywords": {
"translate": [1],
"translatePlural": [1, 2],
"translateWithContext": [[1], 2],
"translatePluralWithContext": [[1], 2, 3],
"markForTranslation": [1],
"markForTranslationWithContext": [[1], 2],
"markForPluralTranslation": [1, 2]
},
"commentTags": [
"dennis-ignore:",
"Translation:"
]
}
},
{
"extractor": "xml",
"filemasks": [
"globalscripts/**.xml",
"gui/civinfo/**.xml",
"gui/common/**.xml",
"gui/credits/**.xml",
"gui/loadgame/**.xml",
"gui/locale/**.xml",
"gui/options/**.xml",
"gui/pregame/**.xml",
"gui/reference/structree/**.xml",
"gui/reference/viewer/**.xml",
"gui/replaymenu/**.xml",
"gui/splashscreen/**.xml",
"gui/summary/**.xml"
],
"options": {
"keywords": {
"translatableAttribute": {
"locationAttributes": ["id"]
},
"translate": {}
}
}
},
{
"extractor": "json",
"filemasks": [
"gui/credits/texts/**.json"
],
"options": {
"keywords": [
"Title",
"Subtitle"
]
}
},
{
"extractor": "json",
"filemasks": [
"gui/options/**.json"
],
"options": {
"keywords": [
"label",
"tooltip"
]
}
},
{
"extractor": "json",
"filemasks": [
"simulation/data/resources/**.json"
],
"options": {
"keywords": [
"description"
]
}
},
{
"extractor": "json",
"filemasks": [
"simulation/data/resources/**.json"
],
"options": {
"keywords": [
"name",
"subtypes"
],
"comments": [
"Translation: Word as used at the beginning of a sentence or as a single-word sentence."
],
"context": "firstWord"
}
},
{
"extractor": "json",
"filemasks": [
"simulation/data/resources/**.json"
],
"options": {
"keywords": [
"name",
"subtypes"
],
"comments": [
"Translation: Word as used in the middle of a sentence (which may require using lowercase for your language)."
],
"context": "withinSentence"
}
},
{
"extractor": "txt",
"filemasks": [
"gui/gamesetup/**.txt",
"gui/splashscreen/splashscreen.txt",
"gui/text/tips/**.txt"
],
"options": {
}
}
]
},
{
"output": "public-templates-units.pot",
"inputRoot": "..",
"project": "0 A.D. — Empires Ascendant",
"copyrightHolder": "Wildfire Games",
"rules": [
{
"extractor": "xml",
"filemasks": [
"simulation/templates/template_unit_*.xml",
"simulation/templates/units/**.xml"
],
"options": {
"keywords": {
"StatusName": {
"customContext": "status effect"
},
"ApplierTooltip": {
"customContext": "status effect"
},
"ReceiverTooltip": {
"customContext": "status effect"
},
"GenericName": {},
"SpecificName": {},
"History": {},
"VisibleClasses": {
"splitOnWhitespace": true
},
"Tooltip": {},
"DisabledTooltip": {},
"FormationName": {},
"FromClass": {},
"Rank": {
"tagAsContext": true
}
}
}
}
]
},
{
"output": "public-templates-buildings.pot",
"inputRoot": "..",
"project": "0 A.D. — Empires Ascendant",
"copyrightHolder": "Wildfire Games",
"rules": [
{
"extractor": "xml",
"filemasks": [
"simulation/templates/template_structure_*.xml",
"simulation/templates/structures/**.xml"
],
"options": {
"keywords": {
"StatusName": {
"customContext": "status effect"
},
"ApplierTooltip": {
"customContext": "status effect"
},
"ReceiverTooltip": {
"customContext": "status effect"
},
"GenericName": {},
"SpecificName": {},
"History": {},
"VisibleClasses": {
"splitOnWhitespace": true
},
"Tooltip": {},
"DisabledTooltip": {},
"FormationName": {},
"FromClass": {},
"Rank": {
"tagAsContext": true
}
}
}
}
]
},
{
"output": "public-templates-other.pot",
"inputRoot": "..",
"project": "0 A.D. — Empires Ascendant",
"copyrightHolder": "Wildfire Games",
"rules": [
{
"extractor": "xml",
"filemasks": {
"includeMasks": [
"simulation/templates/**.xml"
],
"excludeMasks": [
"simulation/templates/structures/**.xml",
"simulation/templates/template_structure_*.xml",
"simulation/templates/template_unit_*.xml",
"simulation/templates/units/**.xml"
]
},
"options": {
"keywords": {
- "StatusName": {
- "customContext": "status effect"
- },
- "ApplierTooltip": {
- "customContext": "status effect"
- },
- "ReceiverTooltip": {
- "customContext": "status effect"
- },
"GenericName": {},
"SpecificName": {},
"History": {},
"VisibleClasses": {
"splitOnWhitespace": true
},
"Tooltip": {},
"DisabledTooltip": {},
"FormationName": {},
"FromClass": {},
"Rank": {
"tagAsContext": true
}
}
}
},
{
"extractor": "json",
"filemasks": [
"simulation/data/template_helpers/damage_types/*.json"
],
"options": {
"keywords": [
"name",
"description"
],
"context": "damage type"
}
},
{
"extractor": "json",
"filemasks": [
- "simulation/data/template_helpers/status_effects/*.json"
+ "simulation/data/status_effects/*.json"
],
"options": {
"keywords": [
- "StatusName",
- "ApplierTooltip",
- "ReceiverTooltip"
+ "statusName",
+ "applierTooltip",
+ "receiverTooltip"
],
"context": "status effect"
}
}
]
},
{
"output": "public-simulation-auras.pot",
"inputRoot": "..",
"project": "0 A.D. — Empires Ascendant",
"copyrightHolder": "Wildfire Games",
"rules": [
{
"extractor": "json",
"filemasks": [
"simulation/data/auras/**.json"
],
"options": {
"keywords": [
"auraName",
"auraDescription"
]
}
}
]
},
{
"output": "public-simulation-technologies.pot",
"inputRoot": "..",
"project": "0 A.D. — Empires Ascendant",
"copyrightHolder": "Wildfire Games",
"rules": [
{
"extractor": "json",
"filemasks": [
"simulation/data/technologies/**.json"
],
"options": {
"keywords": [
"specificName",
"genericName",
"description",
"tooltip",
"requirementsTooltip"
]
}
}
]
},
{
"output": "public-simulation-other.pot",
"inputRoot": "..",
"project": "0 A.D. — Empires Ascendant",
"copyrightHolder": "Wildfire Games",
"rules": [
{
"extractor": "javascript",
"filemasks": [
"simulation/ai/**.js",
"simulation/components/**.js",
"simulation/helpers/**.js"
],
"options": {
"format": "javascript-format",
"keywords": {
"translate": [1],
"translatePlural": [1, 2],
"translateWithContext": [[1], 2],
"translatePluralWithContext": [[1], 2, 3],
"markForTranslation": [1],
"markForTranslationWithContext": [[1], 2],
"markForPluralTranslation": [1, 2]
},
"commentTags": [
"Translation:"
]
}
},
{
"extractor": "json",
"filemasks": [
"simulation/data/settings/player_defaults.json"
],
"options": {
"keywords": [
"Name"
]
}
},
{
"extractor": "json",
"filemasks": [
"simulation/data/settings/game_speeds.json"
],
"options": {
"keywords": ["Title"]
}
},
{
"extractor": "json",
"filemasks": [
"simulation/data/settings/victory_conditions/*.json"
],
"options": {
"keywords": ["Title", "Description"]
}
},
{
"extractor": "json",
"filemasks": [
"simulation/data/settings/starting_resources.json"
],
"options": {
"keywords": ["Title"],
"context": "startingResources"
}
},
{
"extractor": "json",
"filemasks": [
"simulation/data/settings/trigger_difficulties.json"
],
"options": {
"keywords": ["Title", "Tooltip"]
}
},
{
"extractor": "json",
"filemasks": [
"simulation/data/settings/map_sizes.json"
],
"options": {
"keywords": [
"Name",
"Tooltip"
]
}
},
{
"extractor": "json",
"filemasks": [
"simulation/ai/**.json"
],
"options": {
"keywords": [
"name",
"description"
]
}
}
]
},
{
"output": "public-maps.pot",
"inputRoot": "..",
"project": "0 A.D. — Empires Ascendant",
"copyrightHolder": "Wildfire Games",
"rules": [
{
"extractor": "json",
"filemasks": {
"includeMasks": [
"maps/random/**.json"
],
"excludeMasks": [
"maps/random/rmbiome/**.json"
]
},
"options": {
"keywords": [
"Name",
"Description"
]
}
},
{
"extractor": "javascript",
"filemasks": [
"maps/scenarios/**.js",
"maps/skirmishes/**.js",
"maps/random/**.js"
],
"options": {
"format": "javascript-format",
"keywords": {
"markForTranslation": [1],
"markForTranslationWithContext": [[1], 2],
"markForPluralTranslation": [1, 2]
},
"commentTags": [
"Translation:"
]
}
},
{
"extractor": "xml",
"filemasks": [
"maps/scenarios/**.xml",
"maps/skirmishes/**.xml"
],
"options": {
"keywords": {
"ScriptSettings": {
"extractJson": {
"keywords": [
"Name",
"Description"
]
}
}
}
}
},
{
"extractor": "json",
"filemasks": [
"maps/random/rmbiome/**.json"
],
"options": {
"keywords": ["Description"],
"context": "biome definition"
}
}
]
},
{
"output": "public-tutorials.pot",
"inputRoot": "..",
"project": "0 A.D. — Empires Ascendant",
"copyrightHolder": "Wildfire Games",
"rules": [
{
"extractor": "javascript",
"filemasks": [
"maps/tutorials/**.js"
],
"options": {
"format": "javascript-format",
"keywords": {
"markForTranslation": [1],
"markForTranslationWithContext": [[1], 2],
"markForPluralTranslation": [1, 2]
},
"commentTags": [
"Translation:"
]
}
},
{
"extractor": "xml",
"filemasks": [
"maps/tutorials/**.xml"
],
"options": {
"keywords": {
"ScriptSettings": {
"extractJson": {
"keywords": [
"Name",
"Description"
]
}
}
}
}
}
]
}
]
Index: ps/trunk/binaries/data/mods/public/simulation/components/Resistance.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/components/Resistance.js (revision 24161)
+++ ps/trunk/binaries/data/mods/public/simulation/components/Resistance.js (revision 24162)
@@ -1,128 +1,154 @@
function Resistance() {}
/**
* Builds a RelaxRNG schema of possible attack effects.
- * ToDo: Resistance to StatusEffects.
*
* @return {string} - RelaxNG schema string.
*/
Resistance.prototype.BuildResistanceSchema = function()
{
return "" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
"" +
"";
};
Resistance.prototype.Schema =
"Controls the damage resistance of the unit." +
"" +
"" +
"" +
"10.0" +
"0.0" +
"5.0" +
"" +
"10" +
"" +
"" +
"" +
"5" +
"" +
"" +
"" +
"" +
"" +
"" +
Resistance.prototype.BuildResistanceSchema() +
"" +
"" +
Resistance.prototype.BuildResistanceSchema() +
"" +
"" +
"";
Resistance.prototype.Init = function()
{
this.invulnerable = false;
};
Resistance.prototype.IsInvulnerable = function()
{
return this.invulnerable;
};
Resistance.prototype.SetInvulnerability = function(invulnerability)
{
this.invulnerable = invulnerability;
Engine.PostMessage(this.entity, MT_InvulnerabilityChanged, { "entity": this.entity, "invulnerability": invulnerability });
};
/**
* Calculate the effective resistance of an entity to a particular effect.
* ToDo: Support resistance against status effects.
* @param {string} effectType - The type of attack effect the resistance has to be calculated for (e.g. "Damage", "Capture").
* @return {Object} - An object of the type { "Damage": { "Crush": number, "Hack": number }, "Capture": number }.
*/
Resistance.prototype.GetEffectiveResistanceAgainst = function(effectType)
{
let ret = {};
let template = this.GetResistanceOfForm(Engine.QueryInterface(this.entity, IID_Foundation) ? "Foundation" : "Entity");
if (template[effectType])
ret[effectType] = template[effectType];
return ret;
};
/**
* Get all separate resistances for showing in the GUI.
* @return {Object} - All resistances ordered by type.
*/
Resistance.prototype.GetFullResistance = function()
{
let ret = {};
for (let entityForm in this.template)
ret[entityForm] = this.GetResistanceOfForm(entityForm);
return ret;
};
/**
* Get the resistance of a particular type, i.e. Foundation or Entity.
* @param {string} entityForm - The form of the entity to query.
* @return {Object} - An object containing the resistances.
*/
Resistance.prototype.GetResistanceOfForm = function(entityForm)
{
let ret = {};
let template = this.template[entityForm];
if (!template)
return ret;
if (template.Damage)
{
ret.Damage = {};
for (let damageType in template.Damage)
ret.Damage[damageType] = ApplyValueModificationsToEntity("Resistance/" + entityForm + "/Damage/" + damageType, +this.template[entityForm].Damage[damageType], this.entity);
}
if (template.Capture)
ret.Capture = ApplyValueModificationsToEntity("Resistance/" + entityForm + "/Capture", +this.template[entityForm].Capture, this.entity);
+ if (template.ApplyStatus)
+ {
+ ret.ApplyStatus = {};
+ for (let effect in template.ApplyStatus)
+ ret.ApplyStatus[effect] = {
+ "duration": ApplyValueModificationsToEntity("Resistance/" + entityForm + "/ApplyStatus/" + effect + "/Duration", +(template.ApplyStatus[effect].Duration || 1), this.entity),
+ "blockChance": ApplyValueModificationsToEntity("Resistance/" + entityForm + "/ApplyStatus/" + effect + "/BlockChance", +(template.ApplyStatus[effect].BlockChance || 0), this.entity)
+ };
+ }
+
return ret;
};
Engine.RegisterComponentType(IID_Resistance, "Resistance", Resistance);
Index: ps/trunk/binaries/data/mods/public/simulation/components/StatusEffectsReceiver.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/components/StatusEffectsReceiver.js (revision 24161)
+++ ps/trunk/binaries/data/mods/public/simulation/components/StatusEffectsReceiver.js (revision 24162)
@@ -1,162 +1,165 @@
function StatusEffectsReceiver() {}
StatusEffectsReceiver.prototype.DefaultInterval = 1000;
/**
* Initialises the status effects.
*/
StatusEffectsReceiver.prototype.Init = function()
{
this.activeStatusEffects = {};
};
/**
* Which status effects are active on this entity.
*
* @return {Object} - An object containing the status effects which currently affect the entity.
*/
StatusEffectsReceiver.prototype.GetActiveStatuses = function()
{
return this.activeStatusEffects;
};
/**
* Called by Attacking effects. Adds status effects for each entry in the effectData.
*
* @param {Object} effectData - An object containing the status effects to give to the entity.
* @param {number} attacker - The entity ID of the attacker.
* @param {number} attackerOwner - The player ID of the attacker.
* @param {number} bonusMultiplier - A value to multiply the damage with (not implemented yet for SE).
*
- * @return {Object} - The names of the status effects which were processed.
+ * @return {Object} - The codes of the status effects which were processed.
*/
StatusEffectsReceiver.prototype.ApplyStatus = function(effectData, attacker, attackerOwner)
{
let attackerData = { "entity": attacker, "owner": attackerOwner };
for (let effect in effectData)
this.AddStatus(effect, effectData[effect], attackerData);
- // TODO: implement loot / resistance.
+ // TODO: implement loot?
return { "inflictedStatuses": Object.keys(effectData) };
};
/**
* Adds a status effect to the entity.
*
- * @param {string} statusName - The name of the status effect.
+ * @param {string} statusCode - The code of the status effect.
* @param {Object} data - The various effects and timings.
* @param {Object} attackerData - The attacker and attackerOwner.
*/
-StatusEffectsReceiver.prototype.AddStatus = function(statusName, data, attackerData)
+StatusEffectsReceiver.prototype.AddStatus = function(baseCode, data, attackerData)
{
- if (this.activeStatusEffects[statusName])
+ let statusCode = baseCode;
+ if (this.activeStatusEffects[statusCode])
{
if (data.Stackability == "Ignore")
return;
if (data.Stackability == "Extend")
{
- this.activeStatusEffects[statusName].Duration += data.Duration;
+ this.activeStatusEffects[statusCode].Duration += data.Duration;
return;
}
if (data.Stackability == "Replace")
- this.RemoveStatus(statusName);
+ this.RemoveStatus(statusCode);
else if (data.Stackability == "Stack")
{
let i = 0;
let temp;
do
- temp = statusName + "_" + i++;
+ temp = statusCode + "_" + i++;
while (!!this.activeStatusEffects[temp]);
- statusName = temp;
+ statusCode = temp;
}
}
- this.activeStatusEffects[statusName] = {};
- let status = this.activeStatusEffects[statusName];
+ this.activeStatusEffects[statusCode] = {
+ "baseCode": baseCode
+ };
+ let status = this.activeStatusEffects[statusCode];
Object.assign(status, data);
if (status.Modifiers)
{
let modifications = DeriveModificationsFromXMLTemplate(status.Modifiers);
let cmpModifiersManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ModifiersManager);
- cmpModifiersManager.AddModifiers(statusName, modifications, this.entity);
+ cmpModifiersManager.AddModifiers(statusCode, modifications, this.entity);
}
// With neither an interval nor a duration, there is no point in starting a timer.
if (!status.Duration && !status.Interval)
return;
// We need this to prevent Status Effects from giving XP
// to the entity that applied them.
status.StatusEffect = true;
// We want an interval to update the GUI to show how much time of the status effect
// is left even if the status effect itself has no interval.
if (!status.Interval)
status._interval = this.DefaultInterval;
status._timeElapsed = 0;
status._firstTime = true;
status.source = attackerData;
let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
- status._timer = cmpTimer.SetInterval(this.entity, IID_StatusEffectsReceiver, "ExecuteEffect", 0, +(status.Interval || status._interval), statusName);
+ status._timer = cmpTimer.SetInterval(this.entity, IID_StatusEffectsReceiver, "ExecuteEffect", 0, +(status.Interval || status._interval), statusCode);
};
/**
* Removes a status effect from the entity.
*
- * @param {string} statusName - The status effect to be removed.
+ * @param {string} statusCode - The status effect to be removed.
*/
-StatusEffectsReceiver.prototype.RemoveStatus = function(statusName)
+StatusEffectsReceiver.prototype.RemoveStatus = function(statusCode)
{
- let statusEffect = this.activeStatusEffects[statusName];
+ let statusEffect = this.activeStatusEffects[statusCode];
if (!statusEffect)
return;
if (statusEffect.Modifiers)
{
let cmpModifiersManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ModifiersManager);
- cmpModifiersManager.RemoveAllModifiers(statusName, this.entity);
+ cmpModifiersManager.RemoveAllModifiers(statusCode, this.entity);
}
if (statusEffect._timer)
{
let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
cmpTimer.CancelTimer(statusEffect._timer);
}
- delete this.activeStatusEffects[statusName];
+ delete this.activeStatusEffects[statusCode];
};
/**
* Called by the timers. Executes a status effect.
*
- * @param {string} statusName - The name of the status effect to be executed.
+ * @param {string} statusCode - The status effect to be executed.
* @param {number} lateness - The delay between the calling of the function and the actual execution (turn time?).
*/
-StatusEffectsReceiver.prototype.ExecuteEffect = function(statusName, lateness)
+StatusEffectsReceiver.prototype.ExecuteEffect = function(statusCode, lateness)
{
- let status = this.activeStatusEffects[statusName];
+ let status = this.activeStatusEffects[statusCode];
if (!status)
return;
if (status.Damage || status.Capture)
- Attacking.HandleAttackEffects(this.entity, statusName, status, status.source.entity, status.source.owner);
+ Attacking.HandleAttackEffects(this.entity, statusCode, status, status.source.entity, status.source.owner);
if (!status.Duration)
return;
if (status._firstTime)
{
status._firstTime = false;
status._timeElapsed += lateness;
}
else
status._timeElapsed += +(status.Interval || status._interval) + lateness;
if (status._timeElapsed >= +status.Duration)
- this.RemoveStatus(statusName);
+ this.RemoveStatus(statusCode);
};
Engine.RegisterComponentType(IID_StatusEffectsReceiver, "StatusEffectsReceiver", StatusEffectsReceiver);
Index: ps/trunk/binaries/data/mods/public/simulation/components/tests/test_Resistance.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/components/tests/test_Resistance.js (revision 24161)
+++ ps/trunk/binaries/data/mods/public/simulation/components/tests/test_Resistance.js (revision 24162)
@@ -1,250 +1,350 @@
Engine.LoadHelperScript("Attacking.js");
Engine.LoadHelperScript("Player.js");
Engine.LoadHelperScript("ValueModification.js");
Engine.LoadComponentScript("interfaces/Capturable.js");
Engine.LoadComponentScript("interfaces/Foundation.js");
Engine.LoadComponentScript("interfaces/Health.js");
Engine.LoadComponentScript("interfaces/Looter.js");
Engine.LoadComponentScript("interfaces/ModifiersManager.js");
Engine.LoadComponentScript("interfaces/PlayerManager.js");
Engine.LoadComponentScript("interfaces/Promotion.js");
Engine.LoadComponentScript("interfaces/Resistance.js");
Engine.LoadComponentScript("interfaces/StatisticsTracker.js");
+Engine.LoadComponentScript("interfaces/StatusEffectsReceiver.js");
Engine.LoadComponentScript("Resistance.js");
class testResistance
{
constructor()
{
this.cmpResistance = null;
this.PlayerID = 1;
this.EnemyID = 2;
this.EntityID = 3;
this.AttackerID = 4;
}
Reset(schema = {})
{
this.cmpResistance = ConstructComponent(this.EntityID, "Resistance", schema);
- DeleteMock(this.EntityID, IID_Health);
DeleteMock(this.EntityID, IID_Capturable);
+ DeleteMock(this.EntityID, IID_Health);
DeleteMock(this.EntityID, IID_Identity);
+ DeleteMock(this.EntityID, IID_StatusEffectsReceiver);
}
TestInvulnerability()
{
this.Reset();
let damage = 5;
let attackData = { "Damage": { "Name": damage } };
let attackType = "Test";
TS_ASSERT(!this.cmpResistance.IsInvulnerable());
let cmpHealth = AddMock(this.EntityID, IID_Health, {
"TakeDamage": (amount, __, ___) => {
TS_ASSERT_EQUALS(amount, damage);
return { "healthChange": -amount };
}
});
let spy = new Spy(cmpHealth, "TakeDamage");
Attacking.HandleAttackEffects(this.EntityID, attackType, attackData, this.AttackerID, this.EnemyID);
TS_ASSERT_EQUALS(spy._called, 1);
this.cmpResistance.SetInvulnerability(true);
TS_ASSERT(this.cmpResistance.IsInvulnerable());
Attacking.HandleAttackEffects(this.EntityID, attackType, attackData, this.AttackerID, this.EnemyID);
TS_ASSERT_EQUALS(spy._called, 1);
}
TestBonus()
{
this.Reset();
let damage = 5;
let bonus = 2;
let classes = "Entity";
let attackData = {
"Damage": { "Name": damage },
"Bonuses": {
"bonus": {
"Classes": classes,
"Multiplier": bonus
}
}
};
AddMock(this.EntityID, IID_Identity, {
"GetClassesList": () => [classes],
"GetCiv": () => "civ"
});
let cmpHealth = AddMock(this.EntityID, IID_Health, {
"TakeDamage": (amount, __, ___) => {
TS_ASSERT_EQUALS(amount, damage * bonus);
return { "healthChange": -amount };
}
});
let spy = new Spy(cmpHealth, "TakeDamage");
Attacking.HandleAttackEffects(this.EntityID, "Test", attackData, this.AttackerID, this.EnemyID);
TS_ASSERT_EQUALS(spy._called, 1);
}
TestDamageResistanceApplies()
{
let resistanceValue = 2;
let damageType = "Name";
this.Reset({
"Entity": {
"Damage": {
[damageType]: resistanceValue
}
}
});
let damage = 5;
let attackData = {
"Damage": { "Name": damage }
};
let cmpHealth = AddMock(this.EntityID, IID_Health, {
"TakeDamage": (amount, __, ___) => {
TS_ASSERT_EQUALS(amount, damage * Math.pow(0.9, resistanceValue));
return { "healthChange": -amount };
}
});
let spy = new Spy(cmpHealth, "TakeDamage");
Attacking.HandleAttackEffects(this.EntityID, "Test", attackData, this.AttackerID, this.EnemyID);
TS_ASSERT_EQUALS(spy._called, 1);
}
TestCaptureResistanceApplies()
{
let resistanceValue = 2;
this.Reset({
"Entity": {
"Capture": resistanceValue
}
});
let damage = 5;
let attackData = {
"Capture": damage
};
let cmpCapturable = AddMock(this.EntityID, IID_Capturable, {
"Capture": (amount, __, ___) => {
TS_ASSERT_EQUALS(amount, damage * Math.pow(0.9, resistanceValue));
return { "captureChange": amount };
}
});
let spy = new Spy(cmpCapturable, "Capture");
Attacking.HandleAttackEffects(this.EntityID, "Test", attackData, this.AttackerID, this.EnemyID);
TS_ASSERT_EQUALS(spy._called, 1);
}
+ TestStatusEffectsResistancesApplies()
+ {
+ // Test duration reduction.
+ let durationFactor = 0.5;
+ let statusName = "statusName";
+ this.Reset({
+ "Entity": {
+ "ApplyStatus": {
+ [statusName]: {
+ "Duration": durationFactor
+ }
+ }
+ }
+ });
+
+ let duration = 10;
+ let attackData = {
+ "ApplyStatus": {
+ [statusName]: {
+ "Duration": duration
+ }
+ }
+ };
+
+ let cmpStatusEffectsReceiver = AddMock(this.EntityID, IID_StatusEffectsReceiver, {
+ "ApplyStatus": (effectData, __, ___) => {
+ TS_ASSERT_EQUALS(effectData[statusName].Duration, duration * durationFactor);
+ return { "inflictedStatuses": Object.keys(effectData) };
+ }
+ });
+ let spy = new Spy(cmpStatusEffectsReceiver, "ApplyStatus");
+
+ Attacking.HandleAttackEffects(this.EntityID, "Test", attackData, this.AttackerID, this.EnemyID);
+ TS_ASSERT_EQUALS(spy._called, 1);
+
+ // Test blocking.
+ this.Reset({
+ "Entity": {
+ "ApplyStatus": {
+ [statusName]: {
+ "BlockChance": "1"
+ }
+ }
+ }
+ });
+
+ cmpStatusEffectsReceiver = AddMock(this.EntityID, IID_StatusEffectsReceiver, {
+ "ApplyStatus": (effectData, __, ___) => {
+ TS_ASSERT_UNEVAL_EQUALS(effectData, {});
+ return { "inflictedStatuses": Object.keys(effectData) };
+ }
+ });
+ spy = new Spy(cmpStatusEffectsReceiver, "ApplyStatus");
+
+ Attacking.HandleAttackEffects(this.EntityID, "Test", attackData, this.AttackerID, this.EnemyID);
+ TS_ASSERT_EQUALS(spy._called, 1);
+
+ // Test multiple resistances.
+ let reducedStatusName = "reducedStatus";
+ let blockedStatusName = "blockedStatus";
+ this.Reset({
+ "Entity": {
+ "ApplyStatus": {
+ [reducedStatusName]: {
+ "Duration": durationFactor
+ },
+ [blockedStatusName]: {
+ "BlockChance": "1"
+ }
+ }
+ }
+ });
+
+ attackData = {
+ "ApplyStatus": {
+ [reducedStatusName]: {
+ "Duration": duration
+ },
+ [blockedStatusName]: {
+ "Duration": duration
+ }
+ }
+ };
+
+ cmpStatusEffectsReceiver = AddMock(this.EntityID, IID_StatusEffectsReceiver, {
+ "ApplyStatus": (effectData, __, ___) => {
+ TS_ASSERT_EQUALS(effectData[reducedStatusName].Duration, duration * durationFactor);
+ TS_ASSERT_UNEVAL_EQUALS(Object.keys(effectData), [reducedStatusName]);
+ return { "inflictedStatuses": Object.keys(effectData) };
+ }
+ });
+ spy = new Spy(cmpStatusEffectsReceiver, "ApplyStatus");
+
+ Attacking.HandleAttackEffects(this.EntityID, "Test", attackData, this.AttackerID, this.EnemyID);
+ TS_ASSERT_EQUALS(spy._called, 1);
+ }
+
TestResistanceAndBonus()
{
let resistanceValue = 2;
let damageType = "Name";
this.Reset({
"Entity": {
"Damage": {
[damageType]: resistanceValue
}
}
});
let damage = 5;
let bonus = 2;
let classes = "Entity";
let attackData = {
"Damage": { "Name": damage },
"Bonuses": {
"bonus": {
"Classes": classes,
"Multiplier": bonus
}
}
};
AddMock(this.EntityID, IID_Identity, {
"GetClassesList": () => [classes],
"GetCiv": () => "civ"
});
let cmpHealth = AddMock(this.EntityID, IID_Health, {
"TakeDamage": (amount, __, ___) => {
TS_ASSERT_EQUALS(amount, damage * bonus * Math.pow(0.9, resistanceValue));
return { "healthChange": -amount };
}
});
let spy = new Spy(cmpHealth, "TakeDamage");
Attacking.HandleAttackEffects(this.EntityID, "Test", attackData, this.AttackerID, this.EnemyID);
TS_ASSERT_EQUALS(spy._called, 1);
}
TestMultipleEffects()
{
let captureResistanceValue = 2;
this.Reset({
"Entity": {
"Capture": captureResistanceValue
}
});
let damage = 5;
let bonus = 2;
let classes = "Entity";
let attackData = {
"Damage": { "Name": damage },
"Capture": damage,
"Bonuses": {
"bonus": {
"Classes": classes,
"Multiplier": bonus
}
}
};
AddMock(this.EntityID, IID_Identity, {
"GetClassesList": () => [classes],
"GetCiv": () => "civ"
});
let cmpCapturable = AddMock(this.EntityID, IID_Capturable, {
"Capture": (amount, __, ___) => {
TS_ASSERT_EQUALS(amount, damage * bonus * Math.pow(0.9, captureResistanceValue));
return { "captureChange": amount };
}
});
let cmpHealth = AddMock(this.EntityID, IID_Health, {
"TakeDamage": (amount, __, ___) => {
TS_ASSERT_EQUALS(amount, damage * bonus);
return { "healthChange": -amount };
},
"GetHitpoints": () => 1,
"GetMaxHitpoints": () => 1
});
let healthSpy = new Spy(cmpHealth, "TakeDamage");
let captureSpy = new Spy(cmpCapturable, "Capture");
Attacking.HandleAttackEffects(this.EntityID, "Test", attackData, this.AttackerID, this.EnemyID);
TS_ASSERT_EQUALS(healthSpy._called, 1);
TS_ASSERT_EQUALS(captureSpy._called, 1);
}
}
let cmp = new testResistance();
cmp.TestInvulnerability();
cmp.TestBonus();
cmp.TestDamageResistanceApplies();
cmp.TestCaptureResistanceApplies();
+cmp.TestStatusEffectsResistancesApplies();
cmp.TestResistanceAndBonus();
cmp.TestMultipleEffects();
Index: ps/trunk/binaries/data/mods/public/simulation/helpers/Attacking.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/helpers/Attacking.js (revision 24161)
+++ ps/trunk/binaries/data/mods/public/simulation/helpers/Attacking.js (revision 24162)
@@ -1,373 +1,383 @@
/**
* Provides attack and damage-related helpers under the Attacking umbrella (to avoid name ambiguity with the component).
*/
function Attacking() {}
const DirectEffectsSchema =
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"";
const StatusEffectsSchema =
"" +
"" +
"" +
- "" +
+ "" +
"" +
"" +
- "" +
- "" +
- "" +
- "" +
- "" +
- "" +
- "" +
- "" +
- "" +
- "" +
- "" +
- "" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
DirectEffectsSchema +
"" +
"" +
"" +
"" +
"" +
ModificationsSchema +
"" +
"" +
"" +
"Ignore" +
"Extend" +
"Replace" +
"Stack" +
"" +
"" +
"" +
"" +
"" +
"";
/**
* Builds a RelaxRNG schema of possible attack effects.
* See globalscripts/AttackEffects.js for possible elements.
* Attacks may also have a "Bonuses" element.
*
* @return {string} - RelaxNG schema string.
*/
Attacking.prototype.BuildAttackEffectsSchema = function()
{
return "" +
"" +
"" +
DirectEffectsSchema +
StatusEffectsSchema +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"";
};
/**
* Returns a template-like object of attack effects.
*/
Attacking.prototype.GetAttackEffectsData = function(valueModifRoot, template, entity)
{
let ret = {};
if (template.Damage)
{
ret.Damage = {};
let applyMods = damageType =>
ApplyValueModificationsToEntity(valueModifRoot + "/Damage/" + damageType, +(template.Damage[damageType] || 0), entity);
for (let damageType in template.Damage)
ret.Damage[damageType] = applyMods(damageType);
}
if (template.Capture)
ret.Capture = ApplyValueModificationsToEntity(valueModifRoot + "/Capture", +(template.Capture || 0), entity);
if (template.ApplyStatus)
ret.ApplyStatus = this.GetStatusEffectsData(valueModifRoot, template.ApplyStatus, entity);
if (template.Bonuses)
ret.Bonuses = template.Bonuses;
return ret;
};
Attacking.prototype.GetStatusEffectsData = function(valueModifRoot, template, entity)
{
let result = {};
for (let effect in template)
{
let statusTemplate = template[effect];
result[effect] = {
"StatusName": statusTemplate.StatusName,
"ApplierTooltip": statusTemplate.ApplierTooltip,
"ReceiverTooltip": statusTemplate.ReceiverTooltip,
"Duration": ApplyValueModificationsToEntity(valueModifRoot + "/ApplyStatus/" + effect + "/Duration", +(statusTemplate.Duration || 0), entity),
"Interval": ApplyValueModificationsToEntity(valueModifRoot + "/ApplyStatus/" + effect + "/Interval", +(statusTemplate.Interval || 0), entity),
"Stackability": statusTemplate.Stackability
};
Object.assign(result[effect], this.GetAttackEffectsData(valueModifRoot + "/ApplyStatus" + effect, statusTemplate, entity));
if (statusTemplate.Modifiers)
result[effect].Modifiers = this.GetStatusEffectsModifications(valueModifRoot, statusTemplate.Modifiers, entity, effect);
}
return result;
};
Attacking.prototype.GetStatusEffectsModifications = function(valueModifRoot, template, entity, effect)
{
let modifiers = {};
for (let modifier in template)
{
let modifierTemplate = template[modifier];
modifiers[modifier] = {
"Paths": modifierTemplate.Paths,
"Affects": modifierTemplate.Affects
};
if (modifierTemplate.Add !== undefined)
modifiers[modifier].Add = ApplyValueModificationsToEntity(valueModifRoot + "/ApplyStatus/" + effect + "/Modifiers/" + modifier + "/Add", +modifierTemplate.Add, entity);
if (modifierTemplate.Multiply !== undefined)
modifiers[modifier].Multiply = ApplyValueModificationsToEntity(valueModifRoot + "/ApplyStatus/" + effect + "/Modifiers/" + modifier + "/Multiply", +modifierTemplate.Multiply, entity);
if (modifierTemplate.Replace !== undefined)
modifiers[modifier].Replace = modifierTemplate.Replace;
}
return modifiers;
};
/**
* Calculate the total effect taking bonus and resistance into account.
*
* @param {number} target - The target of the attack.
* @param {Object} effectData - The effects calculate the effect for.
* @param {string} effectType - The type of effect to apply (e.g. Damage, Capture or ApplyStatus).
* @param {number} bonusMultiplier - The factor to multiply the total effect with.
* @param {Object} cmpResistance - Optionally the resistance component of the target.
*
* @return {number} - The total value of the effect.
*/
Attacking.prototype.GetTotalAttackEffects = function(target, effectData, effectType, bonusMultiplier, cmpResistance)
{
let total = 0;
if (!cmpResistance)
cmpResistance = Engine.QueryInterface(target, IID_Resistance);
let resistanceStrengths = cmpResistance ? cmpResistance.GetEffectiveResistanceAgainst(effectType) : {};
if (effectType == "Damage")
for (let type in effectData.Damage)
total += effectData.Damage[type] * Math.pow(0.9, resistanceStrengths.Damage ? resistanceStrengths.Damage[type] || 0 : 0);
else if (effectType == "Capture")
{
total = effectData.Capture * Math.pow(0.9, resistanceStrengths.Capture || 0);
// If Health is lower we are more susceptible to capture attacks.
let cmpHealth = Engine.QueryInterface(target, IID_Health);
if (cmpHealth)
total /= 0.1 + 0.9 * cmpHealth.GetHitpoints() / cmpHealth.GetMaxHitpoints();
}
- else if (effectType == "ApplyStatus")
+ if (effectType != "ApplyStatus")
+ return total * bonusMultiplier;
+
+ if (!resistanceStrengths.ApplyStatus)
return effectData[effectType];
- return total * bonusMultiplier;
+ let result = {};
+ for (let statusEffect in effectData[effectType])
+ {
+ if (!resistanceStrengths.ApplyStatus[statusEffect])
+ {
+ result[statusEffect] = effectData[effectType][statusEffect];
+ continue;
+ }
+
+ if (randBool(resistanceStrengths.ApplyStatus[statusEffect].blockChance))
+ continue;
+
+ result[statusEffect] = effectData[effectType][statusEffect];
+
+ if (effectData[effectType][statusEffect].Duration)
+ result[statusEffect].Duration = effectData[effectType][statusEffect].Duration *
+ resistanceStrengths.ApplyStatus[statusEffect].duration;
+ }
+ return result;
+
};
/**
* Get the list of players affected by the damage.
* @param {number} attackerOwner - The player id of the attacker.
* @param {boolean} friendlyFire - A flag indicating if allied entities are also damaged.
* @return {number[]} The ids of players need to be damaged.
*/
Attacking.prototype.GetPlayersToDamage = function(attackerOwner, friendlyFire)
{
if (!friendlyFire)
return QueryPlayerIDInterface(attackerOwner).GetEnemies();
return Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager).GetAllPlayers();
};
/**
* Damages units around a given origin.
* @param {Object} data - The data sent by the caller.
* @param {string} data.type - The type of damage.
* @param {Object} data.attackData - The attack data.
* @param {number} data.attacker - The entity id of the attacker.
* @param {number} data.attackerOwner - The player id of the attacker.
* @param {Vector2D} data.origin - The origin of the projectile hit.
* @param {number} data.radius - The radius of the splash damage.
* @param {string} data.shape - The shape of the radius.
* @param {Vector3D} [data.direction] - The unit vector defining the direction. Needed for linear splash damage.
* @param {boolean} data.friendlyFire - A flag indicating if allied entities also ought to be damaged.
*/
Attacking.prototype.CauseDamageOverArea = function(data)
{
let nearEnts = PositionHelper.EntitiesNearPoint(data.origin, data.radius,
this.GetPlayersToDamage(data.attackerOwner, data.friendlyFire));
let damageMultiplier = 1;
let cmpObstructionManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ObstructionManager);
// Cycle through all the nearby entities and damage it appropriately based on its distance from the origin.
for (let ent of nearEnts)
{
// Correct somewhat for the entity's obstruction radius.
// TODO: linear falloff should arguably use something cleverer.
let distance = cmpObstructionManager.DistanceToPoint(ent, data.origin.x, data.origin.y);
if (data.shape == 'Circular') // circular effect with quadratic falloff in every direction
damageMultiplier = 1 - distance * distance / (data.radius * data.radius);
else if (data.shape == 'Linear') // linear effect with quadratic falloff in two directions (only used for certain missiles)
{
// The entity has a position here since it was returned by the range manager.
let entityPosition = Engine.QueryInterface(ent, IID_Position).GetPosition2D();
let relativePos = entityPosition.sub(data.origin).normalize().mult(distance);
// Get the position relative to the missile direction.
let direction = Vector2D.from3D(data.direction);
let parallelPos = relativePos.dot(direction);
let perpPos = relativePos.cross(direction);
// The width of linear splash is one fifth of the normal splash radius.
let width = data.radius / 5;
// Check that the unit is within the distance splash width of the line starting at the missile's
// landing point which extends in the direction of the missile for length splash radius.
if (parallelPos >= 0 && Math.abs(perpPos) < width) // If in radius, quadratic falloff in both directions
damageMultiplier = (1 - parallelPos * parallelPos / (data.radius * data.radius)) *
(1 - perpPos * perpPos / (width * width));
else
damageMultiplier = 0;
}
else // In case someone calls this function with an invalid shape.
{
warn("The " + data.shape + " splash damage shape is not implemented!");
}
// The RangeManager can return units that are too far away (due to approximations there)
// so the multiplier can end up below 0.
damageMultiplier = Math.max(0, damageMultiplier);
this.HandleAttackEffects(ent, data.type + ".Splash", data.attackData, data.attacker, data.attackerOwner, damageMultiplier);
}
};
/**
* Handle an attack peformed on an entity.
*
* @param {number} target - The targetted entityID.
* @param {string} attackType - The type of attack that was performed (e.g. "Melee" or "Capture").
* @param {Object} effectData - The effects use.
* @param {number} attacker - The entityID that attacked us.
* @param {number} attackerOwner - The playerID that owned the attacker when the attack was performed.
* @param {number} bonusMultiplier - The factor to multiply the total effect with, defaults to 1.
*
* @return {boolean} - Whether we handled the attack.
*/
Attacking.prototype.HandleAttackEffects = function(target, attackType, attackData, attacker, attackerOwner, bonusMultiplier = 1)
{
let cmpResistance = Engine.QueryInterface(target, IID_Resistance);
if (cmpResistance && cmpResistance.IsInvulnerable())
return false;
bonusMultiplier *= !attackData.Bonuses ? 1 : this.GetAttackBonus(attacker, target, attackType, attackData.Bonuses);
let targetState = {};
for (let effectType of g_EffectTypes)
{
if (!attackData[effectType])
continue;
let receiver = g_EffectReceiver[effectType];
let cmpReceiver = Engine.QueryInterface(target, global[receiver.IID]);
if (!cmpReceiver)
continue;
Object.assign(targetState, cmpReceiver[receiver.method](this.GetTotalAttackEffects(target, attackData, effectType, bonusMultiplier, cmpResistance), attacker, attackerOwner));
}
if (!Object.keys(targetState).length)
return false;
Engine.PostMessage(target, MT_Attacked, {
"type": attackType,
"target": target,
"attacker": attacker,
"attackerOwner": attackerOwner,
"damage": -(targetState.healthChange || 0),
"capture": targetState.captureChange || 0,
"statusEffects": targetState.inflictedStatuses || [],
"fromStatusEffect": !!attackData.StatusEffect,
});
// We do not want an entity to get XP from active Status Effects.
if (!!attackData.StatusEffect)
return true;
let cmpPromotion = Engine.QueryInterface(attacker, IID_Promotion);
if (cmpPromotion && targetState.xp)
cmpPromotion.IncreaseXp(targetState.xp);
return true;
};
/**
* Calculates the attack damage multiplier against a target.
* @param {number} source - The source entity's id.
* @param {number} target - The target entity's id.
* @param {string} type - The type of attack.
* @param {Object} template - The bonus' template.
* @return {number} - The source entity's attack bonus against the specified target.
*/
Attacking.prototype.GetAttackBonus = function(source, target, type, template)
{
let cmpIdentity = Engine.QueryInterface(target, IID_Identity);
if (!cmpIdentity)
return 1;
let attackBonus = 1;
let targetClasses = cmpIdentity.GetClassesList();
let targetCiv = cmpIdentity.GetCiv();
// Multiply the bonuses for all matching classes.
for (let key in template)
{
let bonus = template[key];
if (bonus.Civ && bonus.Civ !== targetCiv)
continue;
if (!bonus.Classes || MatchesClassList(targetClasses, bonus.Classes))
attackBonus *= ApplyValueModificationsToEntity("Attack/" + type + "/Bonuses/" + key + "/Multiplier", +bonus.Multiplier, source);
}
return attackBonus;
};
var AttackingInstance = new Attacking();
Engine.RegisterGlobal("Attacking", AttackingInstance);