forked from 0ad/0ad
leper
e1b13dead9
Remove useless tooltip from spart citizen, as the classes should be done using visible classes. This was SVN commit r16262.
473 lines
16 KiB
JavaScript
473 lines
16 KiB
JavaScript
const COST_DISPLAY_NAMES = {
|
|
"food": '[icon="iconFood"]',
|
|
"wood": '[icon="iconWood"]',
|
|
"stone": '[icon="iconStone"]',
|
|
"metal": '[icon="iconMetal"]',
|
|
"population": '[icon="iconPopulation"]',
|
|
"time": '[icon="iconTime"]'
|
|
};
|
|
|
|
const txtFormats = {
|
|
"unit": ['[font="sans-10"][color="orange"]', '[/color][/font]'],
|
|
"header": ['[font="sans-bold-13"]', '[/font]'],
|
|
"body": ['[font="sans-13"]', '[/font]']
|
|
};
|
|
|
|
function damageValues(dmg)
|
|
{
|
|
if (!dmg)
|
|
return [0, 0, 0];
|
|
|
|
return [dmg.hack || 0, dmg.pierce || 0, dmg.crush || 0];
|
|
}
|
|
|
|
function damageTypeDetails(dmg)
|
|
{
|
|
if (!dmg)
|
|
return '[font="sans-12"]' + translate("(None)") + '[/font]';
|
|
|
|
var dmgArray = [];
|
|
if (dmg.hack)
|
|
dmgArray.push(sprintf(translate("%(damage)s %(damageType)s"), {
|
|
damage: dmg.hack.toFixed(1),
|
|
damageType: txtFormats.unit[0] + translate("Hack") + txtFormats.unit[1]
|
|
}));
|
|
if (dmg.pierce)
|
|
dmgArray.push(sprintf(translate("%(damage)s %(damageType)s"), {
|
|
damage: dmg.pierce.toFixed(1),
|
|
damageType: txtFormats.unit[0] + translate("Pierce") + txtFormats.unit[1]
|
|
}));
|
|
if (dmg.crush)
|
|
dmgArray.push(sprintf(translate("%(damage)s %(damageType)s"), {
|
|
damage: dmg.crush.toFixed(1),
|
|
damageType: txtFormats.unit[0] + translate("Crush") + txtFormats.unit[1]
|
|
}));
|
|
|
|
return dmgArray.join(translate(", "));
|
|
}
|
|
|
|
function attackRateDetails(entState, type)
|
|
{
|
|
var time = entState.attack[type].repeatTime / 1000;
|
|
var timeString = sprintf(translate("%(time)s %(second)s"), {
|
|
time: time,
|
|
second: txtFormats.unit[0] + translatePlural("second", "seconds", time) + txtFormats.unit[1]
|
|
});
|
|
|
|
if (!entState.buildingAI)
|
|
return timeString;
|
|
|
|
var arrows = Math.max(entState.buildingAI.arrowCount, entState.buildingAI.defaultArrowCount);
|
|
var arrowString = sprintf(translate("%(arrowcount)s %(arrow)s"), {
|
|
arrowcount: arrows,
|
|
arrow: txtFormats.unit[0] + translatePlural("arrow", "arrows", arrows) + txtFormats.unit[1]
|
|
});
|
|
return sprintf(translate("%(arrowString)s / %(timeString)s"), {
|
|
arrowString: arrowString,
|
|
timeString: timeString
|
|
});
|
|
}
|
|
|
|
// Converts an armor level into the actual reduction percentage
|
|
function armorLevelToPercentageString(level)
|
|
{
|
|
return (100 - Math.round(Math.pow(0.9, level) * 100)) + "%";
|
|
// return sprintf(translate("%(armorPercentage)s%"), { armorPercentage: (100 - Math.round(Math.pow(0.9, level) * 100)) }); // Not supported by our sprintf implementation.
|
|
}
|
|
|
|
function getArmorTooltip(dmg)
|
|
{
|
|
var label = txtFormats.header[0] + translate("Armor:") + txtFormats.header[1];
|
|
if (!dmg)
|
|
return sprintf(translate("%(label)s %(details)s"), {
|
|
"label": label,
|
|
"details": '[font="sans-12"]' + translate("(None)") + '[/font]'
|
|
});
|
|
|
|
var dmgArray = [];
|
|
if (dmg.hack)
|
|
dmgArray.push(sprintf(translate("%(damage)s %(damageType)s %(armorPercentage)s"), {
|
|
damage: dmg.hack,
|
|
damageType: txtFormats.unit[0] + translate("Hack") + txtFormats.unit[1],
|
|
armorPercentage: '[font="sans-10"]' + sprintf(translate("(%(armorPercentage)s)"), { armorPercentage: armorLevelToPercentageString(dmg.hack) }) + '[/font]'
|
|
}));
|
|
if (dmg.pierce)
|
|
dmgArray.push(sprintf(translate("%(damage)s %(damageType)s %(armorPercentage)s"), {
|
|
damage: dmg.pierce,
|
|
damageType: txtFormats.unit[0] + translate("Pierce") + txtFormats.unit[1],
|
|
armorPercentage: '[font="sans-10"]' + sprintf(translate("(%(armorPercentage)s)"), { armorPercentage: armorLevelToPercentageString(dmg.pierce) }) + '[/font]'
|
|
}));
|
|
if (dmg.crush)
|
|
dmgArray.push(sprintf(translate("%(damage)s %(damageType)s %(armorPercentage)s"), {
|
|
damage: dmg.crush,
|
|
damageType: txtFormats.unit[0] + translate("Crush") + txtFormats.unit[1],
|
|
armorPercentage: '[font="sans-10"]' + sprintf(translate("(%(armorPercentage)s)"), { armorPercentage: armorLevelToPercentageString(dmg.crush) }) + '[/font]'
|
|
}));
|
|
|
|
return sprintf(translate("%(label)s %(details)s"), {
|
|
"label": label,
|
|
"details": dmgArray.join('[font="sans-12"]' + translate(", ") + '[/font]')
|
|
});
|
|
}
|
|
|
|
function damageTypesToText(dmg)
|
|
{
|
|
if (!dmg)
|
|
return '[font="sans-12"]' + translate("(None)") + '[/font]';
|
|
|
|
var dmgArray = [];
|
|
if (dmg.hack)
|
|
dmgArray.push(sprintf(translate("%(damage)s %(damageType)s"), {
|
|
damage: dmg.hack.toFixed(1),
|
|
damageType: txtFormats.unit[0] + translate("Hack") + txtFormats.unit[1]
|
|
}));
|
|
if (dmg.pierce)
|
|
dmgArray.push(sprintf(translate("%(damage)s %(damageType)s"), {
|
|
damage: dmg.pierce.toFixed(1),
|
|
damageType: txtFormats.unit[0] + translate("Pierce") + txtFormats.unit[1]
|
|
}));
|
|
if (dmg.crush)
|
|
dmgArray.push(sprintf(translate("%(damage)s %(damageType)s"), {
|
|
damage: dmg.crush.toFixed(1),
|
|
damageType: txtFormats.unit[0] + translate("Crush") + txtFormats.unit[1]
|
|
}));
|
|
|
|
return dmgArray.join('[font="sans-12"]' + translate(", ") + '[/font]');
|
|
}
|
|
|
|
function getAttackTypeLabel(type)
|
|
{
|
|
if (type === "Charge") return translate("Charge Attack:");
|
|
if (type === "Melee") return translate("Melee Attack:");
|
|
if (type === "Ranged") return translate("Ranged Attack:");
|
|
|
|
warn(sprintf("Internationalization: Unexpected attack type found with code ‘%(attackType)s’. This attack type must be internationalized.", { attackType: type }));
|
|
return translate("Attack:");
|
|
}
|
|
|
|
function getAttackTooltip(template)
|
|
{
|
|
var attacks = [];
|
|
if (!template.attack)
|
|
return "";
|
|
|
|
if (template.buildingAI)
|
|
var rateLabel = txtFormats.header[0] + translate("Interval:") + txtFormats.header[1];
|
|
else
|
|
var rateLabel = txtFormats.header[0] + translate("Rate:") + txtFormats.header[1];
|
|
|
|
for (var type in template.attack)
|
|
{
|
|
if (type == "Slaughter")
|
|
continue; // Slaughter is not a real attack, so do not show it.
|
|
if (type == "Charge")
|
|
continue; // Charging isn't implemented yet and shouldn't be displayed.
|
|
|
|
var rate = sprintf(translate("%(label)s %(details)s"), {
|
|
label: rateLabel,
|
|
details: attackRateDetails(template, type)
|
|
});
|
|
|
|
var attackLabel = txtFormats.header[0] + getAttackTypeLabel(type) + txtFormats.header[1];
|
|
if (type != "Ranged")
|
|
{
|
|
attacks.push(sprintf(translate("%(attackLabel)s %(details)s, %(rate)s"), {
|
|
attackLabel: attackLabel,
|
|
details: damageTypesToText(template.attack[type]),
|
|
rate: rate
|
|
}));
|
|
continue;
|
|
}
|
|
|
|
var realRange = template.attack[type].elevationAdaptedRange;
|
|
var range = Math.round(template.attack[type].maxRange);
|
|
var rangeLabel = txtFormats.header[0] + translate("Range:") + txtFormats.header[1];
|
|
var relativeRange = Math.round((realRange - range));
|
|
var meters = txtFormats.unit[0] + translate("meters") + txtFormats.unit[1];
|
|
|
|
if (relativeRange) // show if it is non-zero
|
|
attacks.push(sprintf(translate("%(attackLabel)s %(details)s, %(rangeLabel)s %(range)s %(meters)s (%(relative)s), %(rate)s"), {
|
|
attackLabel: attackLabel,
|
|
details: damageTypesToText(template.attack[type]),
|
|
rangeLabel: rangeLabel,
|
|
range: range,
|
|
meters: meters,
|
|
relative: relativeRange > 0 ? "+" + relativeRange : relativeRange,
|
|
rate: rate
|
|
}));
|
|
else
|
|
attacks.push(sprintf(translate("%(attackLabel)s %(damageTypes)s, %(rangeLabel)s %(range)s %(meters)s, %(rate)s"), {
|
|
attackLabel: attackLabel,
|
|
damageTypes: damageTypesToText(template.attack[type]),
|
|
rangeLabel: rangeLabel,
|
|
range: range,
|
|
meters: meters,
|
|
rate: rate
|
|
}));
|
|
}
|
|
|
|
return attacks.join(translate(", "));
|
|
}
|
|
|
|
/**
|
|
* Translates a cost component identifier as they are used internally
|
|
* (e.g. "population", "food", etc.) to proper display names.
|
|
*/
|
|
function getCostComponentDisplayName(costComponentName)
|
|
{
|
|
if (costComponentName in COST_DISPLAY_NAMES)
|
|
return COST_DISPLAY_NAMES[costComponentName];
|
|
|
|
warn(sprintf("The specified cost component, ‘%(component)s’, is not currently supported.", { component: costComponentName }));
|
|
return "";
|
|
}
|
|
|
|
/**
|
|
* Multiplies the costs for a template by a given batch size.
|
|
*/
|
|
function multiplyEntityCosts(template, trainNum)
|
|
{
|
|
var totalCosts = {};
|
|
for (var r in template.cost)
|
|
totalCosts[r] = Math.floor(template.cost[r] * trainNum);
|
|
|
|
return totalCosts;
|
|
}
|
|
|
|
/**
|
|
* Helper function for getEntityCostTooltip.
|
|
*/
|
|
function getEntityCostComponentsTooltipString(template, trainNum, entity)
|
|
{
|
|
if (!trainNum)
|
|
trainNum = 1;
|
|
|
|
var totalCosts = multiplyEntityCosts(template, trainNum);
|
|
totalCosts.time = Math.ceil(template.cost.time * (entity ? Engine.GuiInterfaceCall("GetBatchTime", {"entity": entity, "batchSize": trainNum}) : 1));
|
|
|
|
var costs = [];
|
|
if (totalCosts.food) costs.push(sprintf(translate("%(component)s %(cost)s"), { component: getCostComponentDisplayName("food"), cost: totalCosts.food }));
|
|
if (totalCosts.wood) costs.push(sprintf(translate("%(component)s %(cost)s"), { component: getCostComponentDisplayName("wood"), cost: totalCosts.wood }));
|
|
if (totalCosts.metal) costs.push(sprintf(translate("%(component)s %(cost)s"), { component: getCostComponentDisplayName("metal"), cost: totalCosts.metal }));
|
|
if (totalCosts.stone) costs.push(sprintf(translate("%(component)s %(cost)s"), { component: getCostComponentDisplayName("stone"), cost: totalCosts.stone }));
|
|
if (totalCosts.population) costs.push(sprintf(translate("%(component)s %(cost)s"), { component: getCostComponentDisplayName("population"), cost: totalCosts.population }));
|
|
if (totalCosts.time) costs.push(sprintf(translate("%(component)s %(cost)s"), { component: getCostComponentDisplayName("time"), cost: totalCosts.time }));
|
|
return costs;
|
|
}
|
|
|
|
/**
|
|
* 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)
|
|
{
|
|
var out = [];
|
|
var resourceCount = {};
|
|
|
|
// Initialize the acceptable types for '$x to $y $resource' mode.
|
|
for (var resource in wallTypes[0].cost)
|
|
if (wallTypes[0].cost[resource])
|
|
resourceCount[resource] = [wallTypes[0].cost[resource]];
|
|
|
|
var sameTypes = true;
|
|
for (var i = 1; i < wallTypes.length; ++i)
|
|
{
|
|
for (var 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 (var resource in resourceCount)
|
|
{
|
|
if (wallTypes[i].cost[resource])
|
|
resourceCount[resource].push(wallTypes[i].cost[resource]);
|
|
else
|
|
{
|
|
sameTypes = false;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (sameTypes)
|
|
{
|
|
for (var resource in resourceCount)
|
|
{
|
|
var resourceMin = Math.min.apply(Math, resourceCount[resource]);
|
|
var resourceMax = Math.max.apply(Math, resourceCount[resource]);
|
|
|
|
// 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: getCostComponentDisplayName(resource),
|
|
minimum: resourceMin,
|
|
maximum: resourceMax
|
|
}));
|
|
}
|
|
}
|
|
else
|
|
for (var 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, trainNum, entity)
|
|
{
|
|
// 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)
|
|
{
|
|
var templateLong = GetTemplateData(template.wallSet.templates.long);
|
|
var templateMedium = GetTemplateData(template.wallSet.templates.medium);
|
|
var templateShort = GetTemplateData(template.wallSet.templates.short);
|
|
var templateTower = GetTemplateData(template.wallSet.templates.tower);
|
|
|
|
var wallCosts = getWallPieceTooltip([templateShort, templateMedium, templateLong]);
|
|
var towerCosts = getEntityCostComponentsTooltipString(templateTower);
|
|
|
|
return sprintf(translate("Walls: %(costs)s"), { costs: wallCosts.join(" ") }) + "\n"
|
|
+ sprintf(translate("Towers: %(costs)s"), { costs: towerCosts.join(" ") });
|
|
}
|
|
|
|
if (template.cost)
|
|
return getEntityCostComponentsTooltipString(template, trainNum, entity).join(" ");
|
|
|
|
return "";
|
|
}
|
|
|
|
/**
|
|
* Returns the population bonus information to display in the specified entity's construction button tooltip.
|
|
*/
|
|
function getPopulationBonusTooltip(template)
|
|
{
|
|
var popBonus = "";
|
|
if (template.cost && template.cost.populationBonus)
|
|
popBonus = "\n" + sprintf(translate("%(label)s %(populationBonus)s"), {
|
|
label: txtFormats.header[0] + translate("Population Bonus:") + txtFormats.header[1],
|
|
populationBonus: template.cost.populationBonus
|
|
});
|
|
return popBonus;
|
|
}
|
|
|
|
/**
|
|
* Returns a message with the amount of each resource needed to create an entity.
|
|
*/
|
|
function getNeededResourcesTooltip(resources)
|
|
{
|
|
var formatted = [];
|
|
for (var resource in resources)
|
|
formatted.push(sprintf(translate("%(component)s %(cost)s"), {
|
|
component: '[font="sans-12"]' + getCostComponentDisplayName(resource) + '[/font]',
|
|
cost: resources[resource]
|
|
}));
|
|
|
|
return '\n\n[font="sans-bold-13"][color="red"]' + translate("Insufficient resources:") + '[/color][/font]\n' + formatted.join(" ");
|
|
}
|
|
|
|
function getSpeedTooltip(template)
|
|
{
|
|
if (!template.speed)
|
|
return "";
|
|
|
|
var label = txtFormats.header[0] + translate("Speed:") + txtFormats.header[1];
|
|
var speeds = [];
|
|
if (template.speed.walk)
|
|
speeds.push(sprintf(translate("%(speed)s %(movementType)s"), { speed: template.speed.walk, movementType: txtFormats.unit[0] + translate("Walk") + txtFormats.unit[1]}));
|
|
if (template.speed.run)
|
|
speeds.push(sprintf(translate("%(speed)s %(movementType)s"), { speed: template.speed.run, movementType: txtFormats.unit[0] + translate("Run") + txtFormats.unit[1]}));
|
|
|
|
return sprintf(translate("%(label)s %(speeds)s"), { label: label, speeds: speeds.join(translate(", ")) });
|
|
}
|
|
|
|
function getHealerTooltip(template)
|
|
{
|
|
if (!template.healer)
|
|
return "";
|
|
|
|
var healer = [
|
|
sprintf(translate("%(label)s %(val)s %(unit)s"), {
|
|
label: txtFormats.header[0] + translate("Heal:") + txtFormats.header[1],
|
|
val: template.healer.HP,
|
|
// Translation: Short for Health Points (that are healed in one healing action)
|
|
unit: txtFormats.unit[0] + translate("HP") + txtFormats.unit[1]
|
|
}),
|
|
sprintf(translate("%(label)s %(val)s %(unit)s"), {
|
|
label: txtFormats.header[0] + translate("Range:") + txtFormats.header[1],
|
|
val: template.healer.Range,
|
|
unit: txtFormats.unit[0] + translate("meters") + txtFormats.unit[1]
|
|
}),
|
|
sprintf(translate("%(label)s %(val)s %(unit)s"), {
|
|
label: txtFormats.header[0] + translate("Rate:") + txtFormats.header[1],
|
|
val: template.healer.Rate/1000,
|
|
unit: txtFormats.unit[0] + translatePlural("second", "seconds", template.healer.Rate/1000) + txtFormats.unit[1]
|
|
})
|
|
];
|
|
return healer.join(translate(", "));
|
|
}
|
|
|
|
|
|
function getEntityNames(template)
|
|
{
|
|
if (template.name.specific)
|
|
{
|
|
if (template.name.generic && template.name.specific != template.name.generic)
|
|
return sprintf(translate("%(specificName)s (%(genericName)s)"), {
|
|
specificName: template.name.specific,
|
|
genericName: template.name.generic
|
|
});
|
|
return template.name.specific;
|
|
}
|
|
if (template.name.generic)
|
|
return template.name.generic;
|
|
|
|
warn("Entity name requested on an entity without a name, specific or generic.");
|
|
return translate("???");
|
|
}
|
|
|
|
function getEntityNamesFormatted(template)
|
|
{
|
|
var names = "";
|
|
var generic = template.name.generic;
|
|
var specific = template.name.specific;
|
|
if (specific)
|
|
{
|
|
// drop caps for specific name
|
|
names += '[font="sans-bold-16"]' + specific[0] + '[/font]' +
|
|
'[font="sans-bold-12"]' + specific.slice(1).toUpperCase() + '[/font]';
|
|
|
|
if (generic)
|
|
names += '[font="sans-bold-16"] (' + generic + ')[/font]';
|
|
}
|
|
else if (generic)
|
|
names = '[font="sans-bold-16"]' + generic + "[/font]";
|
|
else
|
|
names = "???";
|
|
|
|
return names;
|
|
}
|
|
|
|
function getVisibleEntityClassesFormatted(template)
|
|
{
|
|
var r = ""
|
|
if (template.visibleIdentityClasses && template.visibleIdentityClasses.length)
|
|
{
|
|
r += '\n' + txtFormats.header[0] + translate("Classes:") + txtFormats.header[1];
|
|
var classes = [];
|
|
for (var c of template.visibleIdentityClasses)
|
|
classes.push(translate(c));
|
|
r += ' ' + txtFormats.body[0] + classes.join(translate(", ")) + txtFormats.body[1];
|
|
}
|
|
return r;
|
|
}
|