Index: binaries/data/mods/public/globalscripts/Templates.js
===================================================================
--- binaries/data/mods/public/globalscripts/Templates.js	(revision 18450)
+++ binaries/data/mods/public/globalscripts/Templates.js	(working copy)
@@ -74,24 +74,23 @@ function MatchesClassList(classes, match
  */
 function GetTemplateDataHelper(template, player, auraTemplates)
 {
 	var ret = {};
 
+	// TODO: the second argument of func can be derived from the first
 	var func;
 	if (player)
 		func = ApplyValueModificationsToTemplate;
 	else
 		func = function(a, val, c, d) { return val; }
 
 	if (template.Armour)
-	{
 		ret.armour = {
 			"hack": func("Armour/Hack", +template.Armour.Hack, player, template),
 			"pierce": func("Armour/Pierce", +template.Armour.Pierce, player, template),
 			"crush": func("Armour/Crush", +template.Armour.Crush, player, template),
 		};
-	}
 
 	if (template.Attack)
 	{
 		let getAttackStat = function(type, stat)
 		{
@@ -130,10 +129,21 @@ function GetTemplateDataHelper(template,
 					"description": aura.auraDescription || null
 				};
 		}
 	}
 
+	if (template.BuildingAI)
+	{
+		ret.buildingAI = {};
+		if (template.BuildingAI.DefaultArrowCount)
+			ret.buildingAI.defaultArrowCount = func("BuildingAI/DefaultArrowCount", +template.BuildingAI.DefaultArrowCount, player, template);
+		if (template.BuildingAI.GarrisonArrowMultiplier)
+			ret.buildingAI.garrisonArrowMultiplier = func("BuildingAI/GarrisonArrowMultiplier", +template.BuildingAI.GarrisonArrowMultiplier, player, template);
+		if (template.BuildingAI.MaxArrowCount)
+			ret.buildingAI.maxArrowCount = func("BuildingAI/MaxArrowCount", +template.BuildingAI.MaxArrowCount, player, template);
+	}
+
 	if (template.BuildRestrictions)
 	{
 		// required properties
 		ret.buildRestrictions = {
 			"placementType": template.BuildRestrictions.PlacementType,
@@ -181,10 +191,17 @@ function GetTemplateDataHelper(template,
 			ret.footprint.circle = {"radius": +template.Footprint.Circle["@radius"]};
 		else
 			warn("GetTemplateDataHelper(): Unrecognized Footprint type");
 	}
 
+	if (template.GarrisonHolder)
+	{
+		ret.garrisonHolder = {};
+		if (template.GarrisonHolder.Max)
+			ret.garrisonHolder.max = func("GarrisonHolder/Max", +template.GarrisonHolder.Max, player, template);
+	}
+
 	if (template.Obstruction)
 	{
 		ret.obstruction = {
 			"active": ("" + template.Obstruction.Active == "true"),
 			"blockMovement": ("" + template.Obstruction.BlockMovement == "true"),
Index: binaries/data/mods/public/gui/common/tooltips.js
===================================================================
--- binaries/data/mods/public/gui/common/tooltips.js	(revision 18450)
+++ binaries/data/mods/public/gui/common/tooltips.js	(working copy)
@@ -8,251 +8,291 @@ const g_CostDisplayIcons = {
 };
 
 const g_TooltipTextFormats = {
 	"unit": ['[font="sans-10"][color="orange"]', '[/color][/font]'],
 	"header": ['[font="sans-bold-13"]', '[/font]'],
-	"body": ['[font="sans-13"]', '[/font]']
+	"body": ['[font="sans-13"]', '[/font]'],
+	"comma": ['[font="sans-12"]', '[/font]']
 };
 
+const g_AttackTypes = {
+	"Charge": translate("Charge Attack:"),
+	"Melee": translate("Melee Attack:"),
+	"Ranged": translate("Ranged Attack:"),
+	"Capture": translate("Capture Attack:")
+};
+
+const g_DamageTypes = {
+	"hack": translate("Hack"),
+	"pierce": translate("Pierce"),
+	"crush": translate("Crush"),
+};
+
+function bodyFont(text)
+{
+	return g_TooltipTextFormats.body[0] + text + g_TooltipTextFormats.body[1];
+}
+
+function headerFont(text)
+{
+	return g_TooltipTextFormats.header[0] + text + g_TooltipTextFormats.header[1];
+}
+
+function unitFont(text)
+{
+	return g_TooltipTextFormats.unit[0] + text + g_TooltipTextFormats.unit[1];
+}
+
+function commaFont(text)
+{
+	return g_TooltipTextFormats.comma[0] + text + g_TooltipTextFormats.comma[1];
+}
+
 function damageValues(dmg)
 {
 	if (!dmg)
 		return [0, 0, 0];
 
 	return [dmg.hack || 0, dmg.pierce || 0, dmg.crush || 0];
 }
 
-function damageTypeDetails(dmg)
+function getEntityTooltip(template)
 {
-	if (!dmg)
-		return '[font="sans-12"]' + translate("(None)") + '[/font]';
-
-	let dmgArray = [];
-
-	if (dmg.hack)
-		dmgArray.push(sprintf(translate("%(damage)s %(damageType)s"), {
-			"damage": dmg.hack.toFixed(1),
-			"damageType": g_TooltipTextFormats.unit[0] + translate("Hack") + g_TooltipTextFormats.unit[1]
-		}));
+	if (!template.tooltip)
+		return "";
 
-	if (dmg.pierce)
-		dmgArray.push(sprintf(translate("%(damage)s %(damageType)s"), {
-			"damage": dmg.pierce.toFixed(1),
-			"damageType": g_TooltipTextFormats.unit[0] + translate("Pierce") + g_TooltipTextFormats.unit[1]
-		}));
+	return bodyFont(template.tooltip);
+}
 
-	if (dmg.crush)
-		dmgArray.push(sprintf(translate("%(damage)s %(damageType)s"), {
-			"damage": dmg.crush.toFixed(1),
-			"damageType": g_TooltipTextFormats.unit[0] + translate("Crush") + g_TooltipTextFormats.unit[1]
-		}));
+function getHealthTooltip(template)
+{
+	if (!template.health)
+		return "";
 
-	return dmgArray.join(translate(", "));
+	return sprintf(translate("%(label)s %(details)s"), {
+		"label": headerFont(translate("Health:")),
+		"details": template.health
+	})
 }
 
-function attackRateDetails(entState, type)
+function attackRateDetails(template, type)
 {
 	// Either one arrow shot by UnitAI,
-	let time = entState.attack[type].repeatTime / 1000;
+	let time = template.attack[type].repeatTime / 1000;
 	let timeString = sprintf(translatePlural("%(time)s %(second)s", "%(time)s %(second)s", time), {
 		"time": time,
-		"second": g_TooltipTextFormats.unit[0] + translatePlural("second", "seconds", time) + g_TooltipTextFormats.unit[1]
+		"second": unitFont(translatePlural("second", "seconds", time))
 	});
 
 	// or multiple arrows shot by BuildingAI
-	if (!entState.buildingAI)
+	if (!template.buildingAI || type != "Ranged")
 		return timeString;
 
-	let arrows = entState.buildingAI.arrowCount;
-	let arrowString = sprintf(translatePlural("%(arrowcount)s %(arrow)s", "%(arrowcount)s %(arrow)s", arrows), {
+	// Show either current rate from simulation or default count if the sim is not running
+	let arrows = template.buildingAI.arrowCount || template.buildingAI.defaultArrowCount;
+	let arrowString = sprintf(translatePlural("%(arrowcount)s %(arrows)s", "%(arrowcount)s %(arrows)s", arrows), {
 		"arrowcount": arrows,
-		"arrow": g_TooltipTextFormats.unit[0] + translatePlural("arrow", "arrows", arrows) + g_TooltipTextFormats.unit[1]
+		"arrows": unitFont(translatePlural("arrow", "arrows", arrows))
 	});
 
 	return sprintf(translate("%(arrowString)s / %(timeString)s"), {
 		"arrowString": arrowString,
 		"timeString": timeString
 	});
 }
 
-// Converts an armor level into the actual reduction percentage
+/**
+ * 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.
+	return sprintf(translate("%(percentage)s%%"), {
+		"percentage": (100 - Math.round(Math.pow(0.9, level) * 100))
+	});
 }
 
-function getArmorTooltip(dmg)
+function getArmorTooltip(template)
 {
-	let label = g_TooltipTextFormats.header[0] + translate("Armor:") + g_TooltipTextFormats.header[1];
-	if (!dmg)
+	// TODO showns none currently for techs
+	let label = headerFont(translate("Armor:"));
+
+	if (!template.armour)
 		return sprintf(translate("%(label)s %(details)s"), {
 			"label": label,
 			"details": '[font="sans-12"]' + translate("(None)") + '[/font]'
 		});
 
-	let dmgArray = [];
-	if (dmg.hack)
-		dmgArray.push(sprintf(translate("%(damage)s %(damageType)s %(armorPercentage)s"), {
-			"damage": dmg.hack.toFixed(1),
-			"damageType": g_TooltipTextFormats.unit[0] + translate("Hack") + g_TooltipTextFormats.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.toFixed(1),
-			"damageType": g_TooltipTextFormats.unit[0] + translate("Pierce") + g_TooltipTextFormats.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.toFixed(1),
-			"damageType": g_TooltipTextFormats.unit[0] + translate("Crush") + g_TooltipTextFormats.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]')
+		"details":
+			Object.keys(g_DamageTypes).filter(
+				dmgType => template.armour[dmgType]).map(
+				dmgType => sprintf(
+					translate("%(damage)s %(damageType)s %(armorPercentage)s"), {
+						"damage": template.armour[dmgType].toFixed(1),
+						"damageType": unitFont(g_DamageTypes[dmgType]),
+						"armorPercentage":
+							'[font="sans-10"]' +
+							sprintf(translate("(%(armorPercentage)s)"), {
+								"armorPercentage": armorLevelToPercentageString(template.armour[dmgType])
+							}) + '[/font]'
+					})
+				).join(commaFont(translate(", ")))
 	});
 }
 
 function damageTypesToText(dmg)
 {
 	if (!dmg)
 		return '[font="sans-12"]' + translate("(None)") + '[/font]';
 
-	let dmgArray = [];
-	if (dmg.hack)
-		dmgArray.push(sprintf(translate("%(damage)s %(damageType)s"), {
-			"damage": dmg.hack.toFixed(1),
-			"damageType": g_TooltipTextFormats.unit[0] + translate("Hack") + g_TooltipTextFormats.unit[1]
-		}));
-
-	if (dmg.pierce)
-		dmgArray.push(sprintf(translate("%(damage)s %(damageType)s"), {
-			"damage": dmg.pierce.toFixed(1),
-			"damageType": g_TooltipTextFormats.unit[0] + translate("Pierce") + g_TooltipTextFormats.unit[1]
-		}));
-
-	if (dmg.crush)
-		dmgArray.push(sprintf(translate("%(damage)s %(damageType)s"), {
-			"damage": dmg.crush.toFixed(1),
-			"damageType": g_TooltipTextFormats.unit[0] + translate("Crush") + g_TooltipTextFormats.unit[1]
-		}));
-
-	return dmgArray.join('[font="sans-12"]' + translate(", ") + '[/font]');
+	return Object.keys(g_DamageTypes).filter(
+		dmgType => dmg[dmgType]).map(
+		dmgType => sprintf(translate("%(damage)s %(damageType)s"), {
+			"damage": dmg[dmgType].toFixed(1),
+			"damageType": unitFont(g_DamageTypes[dmgType])
+		})).join(commaFont(translate(", ")))
 }
 
 function getAttackTypeLabel(type)
 {
-	if (type === "Charge") return translate("Charge Attack:");
-	if (type === "Melee") return translate("Melee Attack:");
-	if (type === "Ranged") return translate("Ranged Attack:");
-	if (type === "Capture") return translate("Capture Attack:");
-
-	warn(sprintf("Internationalization: Unexpected attack type found with code ‘%(attackType)s’. This attack type must be internationalized.", { "attackType": type }));
-	return translate("Attack:");
+	if (!g_AttackTypes[type])
+	{
+		warn("Unexpected attack type:" + type);
+		return translate("Attack:");
+	}
+	return g_AttackTypes[type];
 }
 
+// TODO: should also show minRange and splash damage
 function getAttackTooltip(template)
 {
-	let attacks = [];
 	if (!template.attack)
 		return "";
 
-	let rateLabel = g_TooltipTextFormats.header[0] + (template.buildingAI ? translate("Interval:") : translate("Rate:")) + g_TooltipTextFormats.header[1];
-
+	let attacks = [];
 	for (let type in template.attack)
 	{
 		if (type == "Slaughter")
-			continue; // Slaughter is not a real attack, so do not show it.
+			continue; // Slaughter is used to kill animals, so do not show it.
 		if (type == "Charge")
 			continue; // Charging isn't implemented yet and shouldn't be displayed.
 
 		let rate = sprintf(translate("%(label)s %(details)s"), {
-			"label": rateLabel,
+			"label": headerFont(template.buildingAI && type == "Ranged" ?
+				translate("Interval:") :
+				translate("Rate:")),
 			"details": attackRateDetails(template, type)
 		});
 
-		let attackLabel = g_TooltipTextFormats.header[0] + getAttackTypeLabel(type) + g_TooltipTextFormats.header[1];
-		if (type == "Capture")
+		let attackLabel = headerFont(getAttackTypeLabel(type));
+		if (type == "Capture" || type != "Ranged")
 		{
 			attacks.push(sprintf(translate("%(attackLabel)s %(details)s, %(rate)s"), {
 				"attackLabel": attackLabel,
-				"details": template.attack[type].value,
-				"rate": rate
-			}));
-			continue;
-		}
-		if (type != "Ranged")
-		{
-			attacks.push(sprintf(translate("%(attackLabel)s %(details)s, %(rate)s"), {
-				"attackLabel": attackLabel,
-				"details": damageTypesToText(template.attack[type]),
+				"details":
+					type == "Capture" ?
+						template.attack.Capture.value :
+						damageTypesToText(template.attack[type]),
 				"rate": rate
 			}));
 			continue;
 		}
 
 		let realRange = template.attack[type].elevationAdaptedRange;
 		let range = Math.round(template.attack[type].maxRange);
-		let rangeLabel = g_TooltipTextFormats.header[0] + translate("Range:") + g_TooltipTextFormats.header[1];
-		let relativeRange = Math.round((realRange - range));
-		let meters = g_TooltipTextFormats.unit[0] + translatePlural("meter", "meters", range) + g_TooltipTextFormats.unit[1];
+		let relativeRange = realRange ? Math.round(realRange - range) : 0;
 
-		if (relativeRange) // show if it is non-zero
-			attacks.push(sprintf(translate("%(attackLabel)s %(details)s, %(rangeLabel)s %(rangeString)s (%(relative)s), %(rate)s"), {
-				"attackLabel": attackLabel,
-				"details": damageTypesToText(template.attack[type]),
-				"rangeLabel": rangeLabel,
-				"rangeString": sprintf(
-					translatePlural("%(range)s %(meters)s", "%(range)s %(meters)s", range), {
-						"range": range,
-						"meters": meters
-					}),
-				"relative": relativeRange > 0 ? "+" + relativeRange : relativeRange,
-				"rate": rate
-			}));
-		else
-			attacks.push(sprintf(translate("%(attackLabel)s %(damageTypes)s, %(rangeLabel)s %(rangeString)s, %(rate)s"), {
-				"attackLabel": attackLabel,
-				"damageTypes": damageTypesToText(template.attack[type]),
-				"rangeLabel": rangeLabel,
-				"rangeString": sprintf(
-					translatePlural("%(range)s %(meters)s", "%(range)s %(meters)s", range), {
-						"range": range,
-						"meters": meters
-					}),
-				rate: rate
-			}));
+		let rangeString = relativeRange ?
+			translate("%(attackLabel)s %(damageTypes)s, %(rangeLabel)s %(rangeString)s (%(relative)s), %(rate)s") :
+			translate("%(attackLabel)s %(damageTypes)s, %(rangeLabel)s %(rangeString)s, %(rate)s");
+
+		attacks.push(sprintf(rangeString, {
+			"attackLabel": attackLabel,
+			"damageTypes": damageTypesToText(template.attack[type]),
+			"rangeLabel": translate("Range:"),
+			"rangeString": sprintf(
+				translatePlural("%(range)s %(meters)s", "%(range)s %(meters)s", range), {
+					"range": range,
+					"meters": unitFont(translatePlural("meter", "meters", range))
+				}),
+			"rate": rate,
+			"relative": relativeRange ? "+" + relativeRange : "",
+		}));
 	}
-
 	return attacks.join("\n");
 }
 
-function getRepairRateTooltip(rate)
+function getGarrisonTooltip(template)
+{
+	if (!template.garrisonHolder)
+		return "";
+
+	return sprintf(translate("%(label)s: %(garrisonLimit)s"), {
+		"label": headerFont(translate("Garrison Limit")),
+		"garrisonLimit": template.garrisonHolder.capacity || template.garrisonHolder.max
+	});
+}
+
+function getProjectilesTooltip(template)
+{
+	if (!template.garrisonHolder || !template.buildingAI)
+		return "";
+
+	let limit = Math.min(
+		template.buildingAI.maxArrowCount || Infinity,
+		template.buildingAI.defaultArrowCount +
+			template.buildingAI.garrisonArrowMultiplier *
+			(template.garrisonHolder.capacity || template.garrisonHolder.max)
+	);
+
+	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
+		})
+	].join(commaFont(translate(", ")));
+}
+
+function getRepairRateTooltip(template)
 {
-	return "\n" + sprintf(translate("%(repairRateLabel)s %(value)s %(health)s / %(second)s / %(worker)s"), {
-		"repairRateLabel": g_TooltipTextFormats.header[0] + translate("Repair Rate:") + g_TooltipTextFormats.header[1],
-		"value": Math.round(rate * 10) / 10,
-		"health": g_TooltipTextFormats.unit[0] + translate("health") + g_TooltipTextFormats.unit[1],
-		"second": g_TooltipTextFormats.unit[0] + translate("second") + g_TooltipTextFormats.unit[1],
-		"worker": g_TooltipTextFormats.unit[0] + translate("worker") + g_TooltipTextFormats.unit[1]
+	if (!template.repairRate)
+		return "";
+
+	return sprintf(translate("%(repairRateLabel)s %(value)s %(health)s / %(second)s / %(worker)s"), {
+		"repairRateLabel": headerFont(translate("Repair Rate:")),
+		"value": Math.round(template.repairRate * 10) / 10,
+		"health": unitFont(translate("health")),
+		"second": unitFont(translate("second")),
+		"worker": unitFont(translate("worker"))
 	});
 }
 
-function getBuildRateTooltip(rate)
+function getBuildRateTooltip(template)
 {
-	return "\n" + sprintf(translate("%(buildRateLabel)s %(value)s %(health)s / %(second)s / %(worker)s"), {
-		"buildRateLabel": g_TooltipTextFormats.header[0] + translate("Build Rate:") + g_TooltipTextFormats.header[1],
-		"value": Math.round(rate * 10) / 10,
-		"health": g_TooltipTextFormats.unit[0] + translate("health") + g_TooltipTextFormats.unit[1],
-		"second": g_TooltipTextFormats.unit[0] + translate("second") + g_TooltipTextFormats.unit[1],
-		"worker": g_TooltipTextFormats.unit[0] + translate("worker") + g_TooltipTextFormats.unit[1]
+	if (!template.buildRate)
+		return "";
+
+	return sprintf(translate("%(buildRateLabel)s %(value)s %(health)s / %(second)s / %(worker)s"), {
+		"buildRateLabel": headerFont(translate("Build Rate:")),
+		"value": Math.round(template.buildRate * 10) / 10,
+		"health": unitFont(translate("health")),
+		"second": unitFont(translate("second")),
+		"worker": unitFont(translate("worker"))
 	});
 }
 
 /**
  * Translates a cost component identifier as they are used internally
@@ -261,11 +301,11 @@ function getBuildRateTooltip(rate)
 function getCostComponentDisplayIcon(costComponentName)
 {
 	if (costComponentName in g_CostDisplayIcons)
 		return g_CostDisplayIcons[costComponentName];
 
-	warn(sprintf("The specified cost component, ‘%(component)s’, is not currently supported.", { "component": costComponentName }));
+	warn("The specified cost component, ‘" + costComponentName + "’, is not currently supported.");
 	return "";
 }
 
 /**
  * Multiplies the costs for a template by a given batch size.
@@ -299,10 +339,26 @@ function getEntityCostComponentsTooltipS
 				"cost": totalCosts[type]
 			}));
 
 	return costs;
 }
+function getGatherTooltip(template)
+{
+	if (!template.gather)
+		return "";
+
+	return sprintf(translate("%(label)s %(details)s"), {
+		"label": headerFont(translate("Gather Rates:")),
+		"details":
+			Object.keys(template.gather).map(
+				type => sprintf(translate("%(resourceIcon)s %(rate)s"), {
+					"resourceIcon": getCostComponentDisplayIcon(type),
+					"rate": template.gather[type]
+				})
+			).join("  ")
+	});
+}
 
 /**
  * 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'.
@@ -398,50 +454,53 @@ function getEntityCostTooltip(template,
  */
 function getPopulationBonusTooltip(template)
 {
 	let popBonus = "";
 	if (template.cost && template.cost.populationBonus)
-		popBonus = "\n" + sprintf(translate("%(label)s %(populationBonus)s"), {
-			"label": g_TooltipTextFormats.header[0] + translate("Population Bonus:") + g_TooltipTextFormats.header[1],
+		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"]' + getCostComponentDisplayIcon(resource) + '[/font]',
 			"cost": resources[resource]
 		}));
 
-	return '\n\n[font="sans-bold-13"][color="red"]' + translate("Insufficient resources:") + '[/color][/font]\n' + formatted.join("  ");
+	return '\n[font="sans-bold-13"][color="red"]' + translate("Insufficient resources:") + '[/color][/font]\n' + formatted.join("  ");
 }
 
 function getSpeedTooltip(template)
 {
 	if (!template.speed)
 		return "";
 
-	let label = g_TooltipTextFormats.header[0] + translate("Speed:") + g_TooltipTextFormats.header[1];
+	let label = headerFont(translate("Speed:"));
 	let speeds = [];
 
 	if (template.speed.walk)
 		speeds.push(sprintf(translate("%(speed)s %(movementType)s"), {
 			"speed": Math.round(template.speed.walk),
-			"movementType": g_TooltipTextFormats.unit[0] + translate("Walk") + g_TooltipTextFormats.unit[1]
+			"movementType": unitFont(translate("Walk"))
 		}));
 
 	if (template.speed.run)
 		speeds.push(sprintf(translate("%(speed)s %(movementType)s"), {
 			"speed": Math.round(template.speed.run),
-			"movementType": g_TooltipTextFormats.unit[0] + translate("Run") + g_TooltipTextFormats.unit[1]
+			"movementType": unitFont(translate("Run"))
 		}));
 
 	return sprintf(translate("%(label)s %(speeds)s"), {
 		"label": label,
 		"speeds": speeds.join(translate(", "))
@@ -451,45 +510,43 @@ function getSpeedTooltip(template)
 function getHealerTooltip(template)
 {
 	if (!template.healer)
 		return "";
 
-	let healer = [
+	return [
 		sprintf(translatePlural("%(label)s %(val)s %(unit)s", "%(label)s %(val)s %(unit)s", template.healer.HP), {
-			"label": g_TooltipTextFormats.header[0] + translate("Heal:") + g_TooltipTextFormats.header[1],
+			"label": headerFont(translate("Heal:")),
 			"val": template.healer.HP,
 			// Translation: Short for hit points (or health points) that are healed in one healing action
-			"unit": g_TooltipTextFormats.unit[0] + translatePlural("HP", "HP", template.healer.HP) + g_TooltipTextFormats.unit[1]
+			"unit": unitFont(translatePlural("HP", "HP", template.healer.HP))
 		}),
 		sprintf(translatePlural("%(label)s %(val)s %(unit)s", "%(label)s %(val)s %(unit)s", template.healer.Range), {
-			"label": g_TooltipTextFormats.header[0] + translate("Range:") + g_TooltipTextFormats.header[1],
+			"label": headerFont(translate("Range:")),
 			"val": template.healer.Range,
-			"unit": g_TooltipTextFormats.unit[0] + translatePlural("meter", "meters", template.healer.Range) + g_TooltipTextFormats.unit[1]
+			"unit": unitFont(translatePlural("meter", "meters", template.healer.Range))
 		}),
 		sprintf(translatePlural("%(label)s %(val)s %(unit)s", "%(label)s %(val)s %(unit)s", template.healer.Rate/1000), {
-			"label": g_TooltipTextFormats.header[0] + translate("Rate:") + g_TooltipTextFormats.header[1],
+			"label": headerFont(translate("Rate:")),
 			"val": template.healer.Rate/1000,
-			"unit": g_TooltipTextFormats.unit[0] + translatePlural("second", "seconds", template.healer.Rate/1000) + g_TooltipTextFormats.unit[1]
+			"unit": unitFont(translatePlural("second", "seconds", template.healer.Rate / 1000))
 		})
-	];
-	return healer.join(translate(", "));
+	].join(translate(", "));
 }
 
 function getAurasTooltip(template)
 {
 	if (!template.auras)
 		return "";
 
-	let txt = "";
-	for (let aura in template.auras)
-		txt += '\n' + sprintf(translate("%(auralabel)s %(aurainfo)s"), {
-			"auralabel": g_TooltipTextFormats.header[0] + sprintf(translate("%(auraname)s:"), {
+	let tooltips = Object.keys(template.auras).map(
+		aura => sprintf(translate("%(auralabel)s %(aurainfo)s"), {
+			"auralabel": headerFont(sprintf(translate("%(auraname)s:"), {
 				"auraname": translate(template.auras[aura].name)
-			}) + g_TooltipTextFormats.header[1],
-			"aurainfo": g_TooltipTextFormats.body[0] + translate(template.auras[aura].description) + g_TooltipTextFormats.body[1]
-		});
-	return txt;
+			})),
+			"aurainfo": bodyFont(translate(template.auras[aura].description))
+		}));
+	return tooltips.join("\n");
 }
 
 function getEntityNames(template)
 {
 	if (template.name.specific)
@@ -523,23 +580,26 @@ function getEntityNamesFormatted(templat
 			names += '[font="sans-bold-16"] (' + generic + ')[/font]';
 	}
 	else if (generic)
 		names = '[font="sans-bold-16"]' + generic + "[/font]";
 	else
+		// TODO: translate and proper string
 		names = "???";
 
 	return names;
 }
 
 function getVisibleEntityClassesFormatted(template)
 {
-	let r = "";
-	if (template.visibleIdentityClasses && template.visibleIdentityClasses.length)
-	{
-		r += '\n' + g_TooltipTextFormats.header[0] + translate("Classes:") + g_TooltipTextFormats.header[1];
-		let classes = [];
-		for (let c of template.visibleIdentityClasses)
-			classes.push(translate(c));
-		r += ' ' + g_TooltipTextFormats.body[0] + classes.join(translate(", ")) + g_TooltipTextFormats.body[1];
-	}
+	if (!template.visibleIdentityClasses || !template.visibleIdentityClasses.length)
+		return "";
+
+
+	let r = headerFont(translate("Classes:"));
+
+	let classes = [];
+	for (let c of template.visibleIdentityClasses)
+		classes.push(translate(c));
+
+	r += ' ' + bodyFont(classes.join(translate(", ")));
 	return r;
 }
Index: binaries/data/mods/public/gui/session/selection_details.js
===================================================================
--- binaries/data/mods/public/gui/session/selection_details.js	(revision 18450)
+++ binaries/data/mods/public/gui/session/selection_details.js	(working copy)
@@ -286,28 +286,22 @@ function displaySingle(entState)
 	{
 		Engine.GetGUIObjectByName("playerCivIcon").sprite = "";
 		Engine.GetGUIObjectByName("player").tooltip = "";
 	}
 
-	// Icon image
 	// TODO: we should require all entities to have icons
 	Engine.GetGUIObjectByName("icon").sprite = template.icon ? ("stretched:session/portraits/" + template.icon) : "bkFillBlack";
 
-	let armorString = getArmorTooltip(entState.armour);
+	Engine.GetGUIObjectByName("attackAndArmorStats").tooltip = [
+		getAttackTooltip,
+		getArmorTooltip,
+		getRepairRateTooltip,
+		getBuildRateTooltip,
+		getGarrisonTooltip,
+		getProjectilesTooltip
+	].map(func => func(entState)).filter(tip => tip).join("\n");
 
-	// Attack and Armor
-	Engine.GetGUIObjectByName("attackAndArmorStats").tooltip = entState.attack ? (getAttackTooltip(entState) + "\n" + armorString) : armorString;
-
-	// Repair Rate
-	if (entState.repairRate)
-		Engine.GetGUIObjectByName("attackAndArmorStats").tooltip += getRepairRateTooltip(entState.repairRate);
-
-	// Build Rate
-	if (entState.buildRate)
-		Engine.GetGUIObjectByName("attackAndArmorStats").tooltip += getBuildRateTooltip(entState.buildRate);
-
-	// Icon Tooltip
 	let iconTooltip = "";
 
 	if (genericName)
 		iconTooltip = "[font=\"sans-bold-16\"]" + genericName + "[/font]";
 
@@ -319,14 +313,14 @@ function displaySingle(entState)
 			template.visibleIdentityClasses.map(c => translate(c)).join(translate(", ")) +
 			"[/font]";
 	}
 
 	if (template.auras)
-		iconTooltip += getAurasTooltip(template);
+		iconTooltip += "\n" + getAurasTooltip(template);
 
 	if (template.tooltip)
-		iconTooltip += "\n[font=\"sans-13\"]" + template.tooltip + "[/font]";
+		iconTooltip += "\n" + getEntityTooltip(template);
 
 	Engine.GetGUIObjectByName("iconBorder").tooltip = iconTooltip;
 
 	// Unhide Details Area
 	Engine.GetGUIObjectByName("detailsAreaSingle").hidden = false;
Index: binaries/data/mods/public/gui/session/selection_panels.js
===================================================================
--- binaries/data/mods/public/gui/session/selection_panels.js	(revision 18450)
+++ binaries/data/mods/public/gui/session/selection_panels.js	(working copy)
@@ -299,38 +299,37 @@ g_SelectionPanels.Construction = {
 			neededResources = Engine.GuiInterfaceCall("GetNeededResources", {
 				"cost": multiplyEntityCosts(template, 1),
 				"player": data.unitEntState.player
 			});
 
-		let limits = getEntityLimitAndCount(data.playerState, data.item);
-
 		if (template.wallSet)
 			template.auras = GetTemplateData(template.wallSet.templates.long).auras;
 
 		data.button.onPress = function () { startBuildingPlacement(data.item, data.playerState); };
 
-		let tooltip = getEntityNamesFormatted(template);
-		tooltip += getVisibleEntityClassesFormatted(template);
-		tooltip += getAurasTooltip(template);
-
-		if (template.tooltip)
-			tooltip += "\n[font=\"sans-13\"]" + template.tooltip + "[/font]";
+		let tooltips = [
+			getEntityNamesFormatted,
+			getVisibleEntityClassesFormatted,
+			getAurasTooltip,
+			getEntityTooltip,
+			getEntityCostTooltip,
+			getGarrisonTooltip,
+			getProjectilesTooltip,
+			getPopulationBonusTooltip
+		].map(func => func(template));
 
-		tooltip += "\n" + getEntityCostTooltip(template);
-		tooltip += getPopulationBonusTooltip(template);
-
-		tooltip += formatLimitString(limits.entLimit, limits.entCount, limits.entLimitChangers);
+		let limits = getEntityLimitAndCount(data.playerState, data.item);
+		tooltips.push(formatLimitString(limits.entLimit, limits.entCount, limits.entLimitChangers));
 
 		if (!technologyEnabled)
-			tooltip += "\n" + sprintf(translate("Requires %(technology)s"), {
+			tooltips.push(sprintf(translate("Requires %(technology)s"), {
 				"technology": getEntityNames(GetTechnologyData(template.requiredTechnology))
-			});
+			}));
 
-		if (neededResources)
-			tooltip += getNeededResourcesTooltip(neededResources);
+		tooltips.push(getNeededResourcesTooltip(neededResources));
 
-		data.button.tooltip = tooltip;
+		data.button.tooltip = tooltips.filter(tip => tip).join("\n");
 
 		let modifier = "";
 		if (!technologyEnabled || limits.canBeAddedCount == 0)
 		{
 			data.button.enabled = false;
@@ -527,31 +526,30 @@ g_SelectionPanels.Gate = {
 	},
 	"setupButton": function(data)
 	{
 		data.button.onPress = function() {data.item.callback(data.item); };
 
-		let tooltip = data.item.tooltip;
+		let tooltips = [data.item.tooltip];
 		if (data.item.template)
 		{
 			data.template = GetTemplateData(data.item.template);
-			data.wallCount = data.selection.reduce(function (count, ent) {
+			data.wallCount = data.selection.reduce(count, ent => {
 					let state = GetEntityState(ent);
 					if (hasClass(state, "LongWall") && !state.gate)
 						++count;
 					return count;
 				}, 0);
 
-			tooltip += "\n" + getEntityCostTooltip(data.template, data.wallCount);
+			tooltips.push(getEntityCostTooltip(data.template, data.wallCount));
 
 			data.neededResources = Engine.GuiInterfaceCall("GetNeededResources", {
 				"cost": multiplyEntityCosts(data.template, data.wallCount)
 			});
 
-			if (data.neededResources)
-				tooltip += getNeededResourcesTooltip(data.neededResources);
+			tooltips.push(getNeededResourcesTooltip(data.neededResources));
 		}
-		data.button.tooltip = tooltip;
+		data.button.tooltip = tooltips.filter(tip => tip).join("\n");
 
 		data.button.enabled = controlsPlayer(data.unitEntState.player);
 		let gateIcon;
 		if (data.item.gate)
 		{
@@ -781,29 +779,32 @@ g_SelectionPanels.Research = {
 			});
 
 			let button = Engine.GetGUIObjectByName("unitResearchButton[" + position + "]");
 			let icon = Engine.GetGUIObjectByName("unitResearchIcon[" + position + "]");
 
-			let tooltip = getEntityNamesFormatted(template);
-			if (template.tooltip)
-				tooltip += "\n[font=\"sans-13\"]" + template.tooltip + "[/font]";
+			let tooltips = [
+				getEntityNamesFormatted(template),
+				getEntityTooltip(template),
+				getEntityCostTooltip(template)
+			];
 
-			tooltip += "\n" + getEntityCostTooltip(template);
 			if (!requirementsPassed)
 			{
-				tooltip += "\n" + template.requirementsTooltip;
+				let tip = template.requirementsTooltip;
 				if (template.classRequirements)
 				{
 					let player = data.unitEntState.player;
 					let current = GetSimState().players[player].classCounts[template.classRequirements.class] || 0;
 					let remaining = template.classRequirements.number - current;
-					tooltip += " " + sprintf(translatePlural("Remaining: %(number)s to build.", "Remaining: %(number)s to build.", remaining), { "number": remaining });
+					tip += " " + sprintf(translatePlural("Remaining: %(number)s to build.", "Remaining: %(number)s to build.", remaining), {
+						"number": remaining
+					});
 				}
+				tooltips.push(tip);
 			}
-			if (neededResources)
-				tooltip += getNeededResourcesTooltip(neededResources);
-			button.tooltip = tooltip;
+			tooltips.push(getNeededResourcesTooltip(neededResources));
+			button.tooltip = tooltips.filter(tip => tip).join("\n");
 
 			button.onPress = function () {
 				addResearchToQueue(data.unitEntState.id, tech);
 			};
 
@@ -986,51 +987,47 @@ g_SelectionPanels.Training = {
 
 		data.button.onPress = function() { addTrainingToQueue(data.selection, data.item, data.playerState); };
 
 		data.countDisplay.caption = trainNum > 1 ? trainNum : "";
 
-		let tooltip = "[font=\"sans-bold-16\"]" +
-		colorizeHotkey("%(hotkey)s", "session.queueunit." + (data.i + 1)) +
-			"[/font]"; 
-
-		tooltip += getEntityNamesFormatted(template);
-		tooltip += getVisibleEntityClassesFormatted(template);
-		tooltip += getAurasTooltip(template);
-
-		if (template.tooltip)
-			tooltip += "\n[font=\"sans-13\"]" + template.tooltip + "[/font]";
-
-		tooltip += "\n" + getEntityCostTooltip(template, trainNum, data.unitEntState.id);
+		let tooltips = [
+			"[font=\"sans-bold-16\"]" +
+				colorizeHotkey("%(hotkey)s", "session.queueunit." + (data.i + 1)) +
+				"[/font]" + getEntityNamesFormatted(template),
+			getVisibleEntityClassesFormatted(template),
+			getAurasTooltip(template),
+			getEntityTooltip(template),
+			getEntityCostTooltip(template, trainNum, data.unitEntState.id)
+		];
 
 		let limits = getEntityLimitAndCount(data.playerState, data.item);
+		tooltips.push(formatLimitString(limits.entLimit, limits.entCount, limits.entLimitChangers));
 
-		tooltip += formatLimitString(limits.entLimit, limits.entCount, limits.entLimitChangers);
 		if (Engine.ConfigDB_GetValue("user", "showdetailedtooltips") === "true")
-		{
-			if (template.health)
-				tooltip += "\n[font=\"sans-bold-13\"]" + translate("Health:") + "[/font] " + template.health;
-			if (template.attack)
-				tooltip += "\n" + getAttackTooltip(template);
-			if (template.armour)
-				tooltip += "\n" + getArmorTooltip(template.armour);
-			if (template.speed)
-				tooltip += "\n" + getSpeedTooltip(template);
-		}
+			tooltips.push(
+				getHealthTooltip(template),
+				getAttackTooltip(template),
+				getArmorTooltip(template),
+				getGarrisonTooltip(template),
+				getProjectilesTooltip(template),
+				getSpeedTooltip(template)
+			);
 
-		tooltip += "[color=\"" + g_HotkeyColor + "\"]" +
+		tooltips.push(
+			"[color=\"" + g_HotkeyColor + "\"]" +
 			formatBatchTrainingString(buildingsCountToTrainFullBatch, fullBatchSize, remainderBatch) +
-			"[/color]";
+			"[/color]");
 
 		if (!technologyEnabled)
-		{
-			let techName = getEntityNames(GetTechnologyData(template.requiredTechnology));
-			tooltip += "\n" + sprintf(translate("Requires %(technology)s"), { "technology": techName });
-		}
+			tooltips.push(sprintf(translate("Requires %(technology)s"), {
+				"technology": getEntityNames(GetTechnologyData(template.requiredTechnology))
+			}));
+
 		if (neededResources)
-			tooltip += getNeededResourcesTooltip(neededResources);
+			tooltips.push(getNeededResourcesTooltip(neededResources));
 
-		data.button.tooltip = tooltip;
+		data.button.tooltip = tooltips.filter(tip => tip).join("\n");
 
 		let modifier = "";
 		if (!technologyEnabled || limits.canBeAddedCount == 0)
 		{
 			data.button.enabled = false;
Index: binaries/data/mods/public/gui/session/selection_panels_helpers.js
===================================================================
--- binaries/data/mods/public/gui/session/selection_panels_helpers.js	(revision 18450)
+++ binaries/data/mods/public/gui/session/selection_panels_helpers.js	(working copy)
@@ -77,21 +77,31 @@ function getStanceTooltip(name)
 function formatLimitString(trainEntLimit, trainEntCount, trainEntLimitChangers)
 {
 	if (trainEntLimit == undefined)
 		return "";
 
-	var text = "\n\n" + sprintf(translate("Current Count: %(count)s, Limit: %(limit)s."), { "count": trainEntCount, "limit": trainEntLimit });
+	var text = "\n" + sprintf(translate("Current Count: %(count)s, Limit: %(limit)s."), {
+		"count": trainEntCount,
+		"limit": trainEntLimit
+	});
 
 	if (trainEntCount >= trainEntLimit)
 		text = "[color=\"red\"]" + text + "[/color]";
 
 	for (var c in trainEntLimitChangers)
 	{
-		if (trainEntLimitChangers[c] > 0)
-			text += "\n" + sprintf(translate("%(changer)s enlarges the limit with %(change)s."), { "changer": translate(c), "change": trainEntLimitChangers[c] });
-		else if (trainEntLimitChangers[c] < 0)
-			text += "\n" + sprintf(translate("%(changer)s lessens the limit with %(change)s."), { "changer": translate(c), "change": (-trainEntLimitChangers[c]) });
+		if (!trainEntLimitChangers[c])
+			continue;
+
+		let string = trainEntLimitChangers[c] > 0 ?
+			translate("%(changer)s enlarges the limit with %(change)s.") :
+			translate("%(changer)s lessens the limit with %(change)s.");
+
+		text += "\n" + sprintf(string, {
+			"changer": translate(c),
+			"change": trainEntLimitChangers[c]
+		});
 	}
 	return text;
 }
 
 /**
@@ -131,30 +141,22 @@ function formatBatchTrainingString(build
 	var action = "[font=\"sans-bold-13\"]" + translate("Shift-click") + "[/font]";
 
 	// We need to display the batch details part if there is either more than
 	// one building with full batch or one building with the full batch and
 	// another with a partial batch
-	if (buildingsCountToTrainFullBatch > 1 ||
-		(buildingsCountToTrainFullBatch == 1 && remainderBatch > 0))
-	{
-		if (remainderBatch > 0)
-			return "\n[font=\"sans-13\"]" + sprintf(translate("%(action)s to train %(number)s (%(fullBatch)s + %(remainderBatch)s)."), {
-				"action": action,
-				"number": totalBatchTrainingCount,
-				"fullBatch": fullBatchesString,
-				"remainderBatch": remainderBatch
-			}) + "[/font]";
-
-		return "\n[font=\"sans-13\"]" + sprintf(translate("%(action)s to train %(number)s (%(fullBatch)s)."), {
-			"action": action,
-			"number": totalBatchTrainingCount,
-			"fullBatch": fullBatchesString
-		}) + "[/font]";
-	}
+	var batchString =
+		buildingsCountToTrainFullBatch > 1 ||
+		buildingsCountToTrainFullBatch == 1 && remainderBatch > 0 ?
+			translate("%(action)s to train %(number)s.") :
+		remainderBatch ?
+			translate("%(action)s to train %(number)s (%(fullBatch)s + %(remainderBatch)s).") :
+			translate("%(action)s to train %(number)s (%(fullBatch)s).");
 
-	return "\n[font=\"sans-13\"]" + sprintf(translate("%(action)s to train %(number)s."), {
+	return "[font=\"sans-13\"]" + sprintf(batchString, {
 		"action": action,
-		"number": totalBatchTrainingCount
+		"number": totalBatchTrainingCount,
+		"fullBatch": fullBatchesString,
+		"remainderBatch": remainderBatch
 	}) + "[/font]";
 }
 
 
Index: binaries/data/mods/public/gui/session/session.js
===================================================================
--- binaries/data/mods/public/gui/session/session.js	(revision 18452)
+++ binaries/data/mods/public/gui/session/session.js	(working copy)
@@ -822,26 +822,21 @@ function updateHeroes()
 	}
 }
 
 function createHeroTooltip(heroState, template)
 {
-	let tooltip = "[font=\"sans-bold-16\"]" + template.name.specific + "[/font]" + "\n" +
-		sprintf(translate("%(label)s %(current)s / %(max)s"), {
-			"label": "[font=\"sans-bold-13\"]" + translate("Health:") + "[/font]",
-			"current": Math.ceil(heroState.hitpoints),
-			"max": Math.ceil(heroState.maxHitpoints)
-		});
-
-	if (heroState.attack)
-		tooltip += "\n" + getAttackTooltip(heroState);
-
-	tooltip += "\n" + getArmorTooltip(heroState.armour);
-
-	if (template.tooltip)
-		tooltip += "\n" + template.tooltip;
-
-	return tooltip;
+	return [
+		"[font=\"sans-bold-16\"]" + template.name.specific + "[/font]" + "\n" +
+			sprintf(translate("%(label)s %(current)s / %(max)s"), {
+				"label": "[font=\"sans-bold-13\"]" + translate("Health:") + "[/font]",
+				"current": Math.ceil(heroState.hitpoints),
+				"max": Math.ceil(heroState.maxHitpoints)
+			}),
+		getAttackTooltip(heroState),
+		getArmorTooltip(heroState),
+		getEntityTooltip(heroState)
+	].filter(tip => tip).join("\n");
 }
 
 function displayHeroes()
 {
 	let buttons = Engine.GetGUIObjectByName("unitHeroPanel").children;
Index: binaries/data/mods/public/gui/structree/draw.js
===================================================================
--- binaries/data/mods/public/gui/structree/draw.js	(revision 18450)
+++ binaries/data/mods/public/gui/structree/draw.js	(working copy)
@@ -1,7 +1,23 @@
 var g_DrawLimits = {}; // GUI limits. Populated by predraw()
 
+var g_TooltipFunctions = [
+	getEntityNamesFormatted,
+	getEntityCostTooltip,
+	getEntityTooltip,
+	getAurasTooltip,
+	getHealthTooltip,
+	getHealerTooltip,
+	getAttackTooltip,
+	getArmorTooltip,
+	getGarrisonTooltip,
+	getProjectilesTooltip,
+	getSpeedTooltip,
+	getGatherTooltip,
+	getPopulationBonusTooltip
+];
+
 /**
  * Draw the structree
  *
  * (Actually resizes and changes visibility of elements, and populates text)
  */
@@ -361,56 +377,11 @@ function predraw()
 
 /**
  * Assemble a tooltip text
  *
  * @param  template Information about a Unit, a Structure or a Technology
- *
  * @return  The tooltip text, formatted.
  */
 function assembleTooltip(template)
 {
-	let txt = getEntityNamesFormatted(template);
-	txt += '\n' + getEntityCostTooltip(template, 1);
-
-	if (template.tooltip)
-		txt += '\n' + g_TooltipTextFormats.body[0] +  translate(template.tooltip) + g_TooltipTextFormats.body[1];
-
-	if (template.auras)
-		txt += getAurasTooltip(template);
-
-	if (template.health)
-		txt += '\n' + sprintf(translate("%(label)s %(details)s"), {
-			"label": g_TooltipTextFormats.header[0] + translate("Health:") + g_TooltipTextFormats.header[1],
-			"details": template.health
-		});
-
-	if (template.healer)
-		txt += '\n' + getHealerTooltip(template);
-
-	if (template.attack)
-		txt += '\n' + getAttackTooltip(template);
-
-	if (template.armour)
-		txt += '\n' + getArmorTooltip(template.armour);
-
-	if (template.speed)
-		txt += '\n' + getSpeedTooltip(template);
-
-	if (template.gather)
-	{
-		let rates = [];
-		for (let type in template.gather)
-			rates.push(sprintf(translate("%(resourceIcon)s %(rate)s"), {
-				"resourceIcon": getCostComponentDisplayIcon(type),
-				"rate": template.gather[type]
-			}));
-
-		txt += '\n' + sprintf(translate("%(label)s %(details)s"), {
-			"label": g_TooltipTextFormats.header[0] + translate("Gather Rates:") + g_TooltipTextFormats.header[1],
-			"details": rates.join("  ")
-		});
-	}
-
-	txt += getPopulationBonusTooltip(template);
-
-	return txt;
+	return g_TooltipFunctions.map(func => func(template)).filter(tip => tip).join("\n");
 }
Index: binaries/data/mods/public/simulation/templates/other/hellenic_epic_temple.xml
===================================================================
--- binaries/data/mods/public/simulation/templates/other/hellenic_epic_temple.xml	(revision 18450)
+++ binaries/data/mods/public/simulation/templates/other/hellenic_epic_temple.xml	(working copy)
@@ -27,11 +27,11 @@
   <Identity>
     <Civ>gaia</Civ>
     <GenericName>Epic Temple</GenericName>
     <SpecificName>Naos Parthenos</SpecificName>
     <History>The Hellenes built marvelous temples in order to honour their polytheistic pantheon. While all gods were venerated, a specific patron deity was supposed to watch over each polis.</History>
-    <Tooltip>Garrison up to 30 units to heal them at a quick rate.</Tooltip>
+    <Tooltip>Garrison units to heal them at a quick rate.</Tooltip>
   </Identity>
   <Loot>
     <xp>50</xp>
     <food>0</food>
     <wood>0</wood>
Index: binaries/data/mods/public/simulation/templates/structures/athen_fortress.xml
===================================================================
--- binaries/data/mods/public/simulation/templates/structures/athen_fortress.xml	(revision 18450)
+++ binaries/data/mods/public/simulation/templates/structures/athen_fortress.xml	(working copy)
@@ -5,11 +5,11 @@
     <Height>8.0</Height>
   </Footprint>
   <Identity>
     <Civ>athen</Civ>
     <SpecificName>Epiteíkhisma</SpecificName>
-    <Tooltip>Build siege engines. Garrison up to 20 soldiers inside for stout defense.</Tooltip>
+    <Tooltip>Build siege engines. Garrison soldiers inside for stout defense.</Tooltip>
     <History>Fortresses (also called "Phroúria") were built to guard passes and atop hills in order to command plains and valleys below. One such Athenian fortress, Gyphtokastro, guarded the pass from Attica into Boeotia.</History>
   </Identity>
   <Obstruction>
     <Static width="24.0" depth="26.0"/>
   </Obstruction>
Index: binaries/data/mods/public/simulation/templates/structures/athen_wonder.xml
===================================================================
--- binaries/data/mods/public/simulation/templates/structures/athen_wonder.xml	(revision 18450)
+++ binaries/data/mods/public/simulation/templates/structures/athen_wonder.xml	(working copy)
@@ -12,11 +12,11 @@
   </GarrisonHolder>
   <Identity>
     <Civ>athen</Civ>
     <SpecificName>Naós Parthenṓn</SpecificName>
     <History>The Hellenes built marvelous temples in order to honour their polytheistic pantheon. While all gods were venerated, a specific patron deity was supposed to watch over each polis.</History>
-    <Tooltip>Bring glory to your civilization and add large tracts of land to your empire. Garrison up to 30 units to heal them at an extremely quick rate.</Tooltip>
+    <Tooltip>Bring glory to your civilization and add large tracts of land to your empire. Garrison units to heal them at an extremely quick rate.</Tooltip>
   </Identity>
   <Obstruction>
     <Static width="27.0" depth="57.0"/>
   </Obstruction>
   <VisualActor>
Index: binaries/data/mods/public/simulation/templates/structures/cart_temple.xml
===================================================================
--- binaries/data/mods/public/simulation/templates/structures/cart_temple.xml	(revision 18450)
+++ binaries/data/mods/public/simulation/templates/structures/cart_temple.xml	(working copy)
@@ -11,11 +11,11 @@
   </Footprint>
   <Identity>
     <Civ>cart</Civ>
     <SpecificName>Maqdaš</SpecificName>
     <History>What little we know of the Carthaginian religion has be pieced together from scattered sources. Tanit, a fertility goddess, was one of two principle gods in the Carthaginian pantheon, the other being her consort Ba'al, a deity of Phoenician origin.</History>
-    <Tooltip>Train priestesses to heal your troops. Train Sacred Band pikemen. Garrison up to 20 units to heal them at a quick rate.</Tooltip>
+    <Tooltip>Train priestesses to heal your troops. Train Sacred Band pikemen. Garrison units to heal them at a quick rate.</Tooltip>
   </Identity>
   <Obstruction>
     <Static width="17.0" depth="30.0"/>
   </Obstruction>
   <ProductionQueue>
Index: binaries/data/mods/public/simulation/templates/structures/cart_wonder.xml
===================================================================
--- binaries/data/mods/public/simulation/templates/structures/cart_wonder.xml	(revision 18450)
+++ binaries/data/mods/public/simulation/templates/structures/cart_wonder.xml	(working copy)
@@ -12,11 +12,11 @@
   </GarrisonHolder>
   <Identity>
    <Civ>cart</Civ>
     <SpecificName>Temple of Ba'al Hammon</SpecificName>
     <History>Dating from the 2nd Century BC, the Mausoleum of Atban in northern Tunisia is over twenty metres high and was built by the inhabitants of Dougga for a Numidian prince.</History>
-    <Tooltip>Bring glory to your civilization and add large tracts of land to your empire. Garrison up to 30 units to heal them at an extremely quick rate.</Tooltip>
+    <Tooltip>Bring glory to your civilization and add large tracts of land to your empire. Garrison units to heal them at an extremely quick rate.</Tooltip>
   </Identity>
   <Obstruction>
     <Static width="27.0" depth="57.0"/>
   </Obstruction>
   <VisualActor>
Index: binaries/data/mods/public/simulation/templates/structures/mace_fortress.xml
===================================================================
--- binaries/data/mods/public/simulation/templates/structures/mace_fortress.xml	(revision 18450)
+++ binaries/data/mods/public/simulation/templates/structures/mace_fortress.xml	(working copy)
@@ -11,11 +11,11 @@
     <Height>8.0</Height>
   </Footprint>
   <Identity>
     <Civ>mace</Civ>
     <SpecificName>Teíchisma</SpecificName>
-    <Tooltip>Train Champions and Heroes. Garrison up to 20 soldiers inside for stout defense.</Tooltip>
+    <Tooltip>Train Champions and Heroes. Garrison soldiers inside for stout defense.</Tooltip>
     <History>The Akropolis was usually a fortified citadel in the upper part of the city. The Athenian Akropolis was renowned for its marvelous temples, among which was the Parthenon, while the Acro-Corinthus was highly prized by the Macedonians for its strategic location and good defenses. Fortresses (also called a "phrourion") were also built to guard passes and atop hills in order to command plains and valleys below.</History>
   </Identity>
   <Obstruction>
     <Static width="24.0" depth="26.0"/>
   </Obstruction>
Index: binaries/data/mods/public/simulation/templates/structures/mace_wonder.xml
===================================================================
--- binaries/data/mods/public/simulation/templates/structures/mace_wonder.xml	(revision 18450)
+++ binaries/data/mods/public/simulation/templates/structures/mace_wonder.xml	(working copy)
@@ -12,11 +12,11 @@
   </GarrisonHolder>
   <Identity>
     <Civ>mace</Civ>
     <SpecificName>Naós Parthenṓn</SpecificName>
     <History>The Hellenes built marvelous temples in order to honour their polytheistic pantheon. While all gods were venerated, a specific patron deity was supposed to watch over each polis.</History>
-    <Tooltip>Bring glory to your civilization and add large tracts of land to your empire. Garrison up to 30 units to heal them at an extremely quick rate.</Tooltip>
+    <Tooltip>Bring glory to your civilization and add large tracts of land to your empire. Garrison units to heal them at an extremely quick rate.</Tooltip>
   </Identity>
   <Obstruction>
     <Static width="27.0" depth="57.0"/>
   </Obstruction>
   <VisualActor>
Index: binaries/data/mods/public/simulation/templates/structures/ptol_barracks.xml
===================================================================
--- binaries/data/mods/public/simulation/templates/structures/ptol_barracks.xml	(revision 18450)
+++ binaries/data/mods/public/simulation/templates/structures/ptol_barracks.xml	(working copy)
@@ -13,11 +13,11 @@
     <SpawnEntityOnDeath>rubble/rubble_stone_5x5</SpawnEntityOnDeath>
   </Health>
   <Identity>
     <Civ>ptol</Civ>
     <SpecificName>ḥwt-n-mš'</SpecificName>
-    <Tooltip>Train Egyptian and Middle-Eastern citizen-soldiers. Research training improvements. Garrison: 10.</Tooltip>
+    <Tooltip>Train Egyptian and Middle-Eastern citizen-soldiers. Research training improvements.</Tooltip>
   </Identity>
   <Obstruction>
     <Static width="20.0" depth="20.0"/>
   </Obstruction>
   <VisualActor>
Index: binaries/data/mods/public/simulation/templates/structures/ptol_wonder.xml
===================================================================
--- binaries/data/mods/public/simulation/templates/structures/ptol_wonder.xml	(revision 18450)
+++ binaries/data/mods/public/simulation/templates/structures/ptol_wonder.xml	(working copy)
@@ -12,11 +12,11 @@
   </GarrisonHolder>
   <Identity>
     <Civ>ptol</Civ>
     <SpecificName>Temple of Edfu</SpecificName>
     <History>The Temple of Edfu is an ancient Egyptian temple located on the west bank of the Nile in the city of Edfu which was known in Greco-Roman times as Apollonopolis Magna, after the chief god Horus-Apollo.The temple, dedicated to the falcon god Horus, was built in the Ptolemaic period between 237 and 57 BCE. In modern times, it is one of the best preserved temples of Egypt.</History>
-    <Tooltip>Bring glory to your civilization and add large tracts of land to your empire. Garrison up to 30 units to heal them at an extremely quick rate.</Tooltip>
+    <Tooltip>Bring glory to your civilization and add large tracts of land to your empire. Garrison units to heal them at an extremely quick rate.</Tooltip>
   </Identity>
   <Obstruction>
     <Static width="44.0" depth="60.0"/>
   </Obstruction>
   <VisualActor>
Index: binaries/data/mods/public/simulation/templates/structures/rome_temple_mars.xml
===================================================================
--- binaries/data/mods/public/simulation/templates/structures/rome_temple_mars.xml	(revision 18450)
+++ binaries/data/mods/public/simulation/templates/structures/rome_temple_mars.xml	(working copy)
@@ -10,11 +10,11 @@
   <Identity>
     <Civ>rome</Civ>
     <GenericName>Temple of Mars</GenericName>
     <SpecificName>Aedes Martis</SpecificName>
     <History>Roman temples in general were not meant for congregational worship. Instead the temple housed a statue of whatever deity the temple was dedicated to and what was needed to carry out the ceremonial and cultic practice necessary for worship. Any actual worship activity was performed outside.</History>
-    <Tooltip>Train healers. Garrison up to 30 units to heal them at a quick rate.</Tooltip>
+    <Tooltip>Train healers. Garrison units to heal them at a quick rate.</Tooltip>
   </Identity>
   <Obstruction>
     <Static width="20.0" depth="40.0"/>
   </Obstruction>
   <TerritoryInfluence>
Index: binaries/data/mods/public/simulation/templates/structures/rome_wonder.xml
===================================================================
--- binaries/data/mods/public/simulation/templates/structures/rome_wonder.xml	(revision 18450)
+++ binaries/data/mods/public/simulation/templates/structures/rome_wonder.xml	(working copy)
@@ -11,11 +11,11 @@
     <BuffHeal>8</BuffHeal>
   </GarrisonHolder>
   <Identity>
     <Civ>rome</Civ>
     <SpecificName>Aedes Iovis Optimi Maximi</SpecificName>
-    <Tooltip>Bring glory to your civilization and add large tracts of land to your empire. Garrison up to 30 units to heal them at an extremely quick rate.</Tooltip>
+    <Tooltip>Bring glory to your civilization and add large tracts of land to your empire. Garrison units to heal them at an extremely quick rate.</Tooltip>
   </Identity>
   <Obstruction>
     <Static width="20.0" depth="40.0"/>
   </Obstruction>
   <VisualActor>
Index: binaries/data/mods/public/simulation/templates/structures/sele_fortress.xml
===================================================================
--- binaries/data/mods/public/simulation/templates/structures/sele_fortress.xml	(revision 18450)
+++ binaries/data/mods/public/simulation/templates/structures/sele_fortress.xml	(working copy)
@@ -5,11 +5,11 @@
     <Height>8.0</Height>
   </Footprint>
   <Identity>
     <Civ>sele</Civ>
     <SpecificName>Phrourion</SpecificName>
-    <Tooltip>Build siege engines and train Champions. Garrison up to 20 soldiers inside for stout defense.</Tooltip>
+    <Tooltip>Build siege engines and train Champions. Garrison soldiers inside for stout defense.</Tooltip>
     <History>Fortresses were built to guard passes and atop hills in order to command plains and valleys below.</History>
   </Identity>
   <Obstruction>
     <Static width="24.0" depth="26.0"/>
   </Obstruction>
Index: binaries/data/mods/public/simulation/templates/structures/spart_wonder.xml
===================================================================
--- binaries/data/mods/public/simulation/templates/structures/spart_wonder.xml	(revision 18450)
+++ binaries/data/mods/public/simulation/templates/structures/spart_wonder.xml	(working copy)
@@ -12,11 +12,11 @@
   </GarrisonHolder>
   <Identity>
     <Civ>spart</Civ>
     <SpecificName>Naós Parthenṓn</SpecificName>
     <History>The Hellenes built marvelous temples in order to honour their polytheistic pantheon. While all gods were venerated, a specific patron deity was supposed to watch over each polis.</History>
-    <Tooltip>Bring glory to your civilization and add large tracts of land to your empire. Garrison up to 30 units to heal them at a quick rate.</Tooltip>
+    <Tooltip>Bring glory to your civilization and add large tracts of land to your empire. Garrison units to heal them at a quick rate.</Tooltip>
   </Identity>
   <Obstruction>
     <Static width="27.0" depth="57.0"/>
   </Obstruction>
   <VisualActor>
Index: binaries/data/mods/public/simulation/templates/template_structure_civic_civil_centre.xml
===================================================================
--- binaries/data/mods/public/simulation/templates/template_structure_civic_civil_centre.xml	(revision 18450)
+++ binaries/data/mods/public/simulation/templates/template_structure_civic_civil_centre.xml	(working copy)
@@ -64,11 +64,11 @@
     <Max>3000</Max>
     <SpawnEntityOnDeath>rubble/rubble_stone_6x6</SpawnEntityOnDeath>
   </Health>
   <Identity>
     <GenericName>Civic Center</GenericName>
-    <Tooltip>Build to acquire large tracts of territory. Train citizens. Garrison: 20.</Tooltip>
+    <Tooltip>Build to acquire large tracts of territory. Train citizens.</Tooltip>
     <Classes datatype="tokens">Defensive CivCentre</Classes>
     <VisibleClasses datatype="tokens">CivilCentre</VisibleClasses>
     <Icon>structures/civic_centre.png</Icon>
   </Identity>
   <Loot>
Index: binaries/data/mods/public/simulation/templates/template_structure_civic_temple.xml
===================================================================
--- binaries/data/mods/public/simulation/templates/template_structure_civic_temple.xml	(revision 18450)
+++ binaries/data/mods/public/simulation/templates/template_structure_civic_temple.xml	(working copy)
@@ -27,11 +27,11 @@
     <Max>2000</Max>
     <SpawnEntityOnDeath>rubble/rubble_stone_4x6</SpawnEntityOnDeath>
   </Health>
   <Identity>
     <GenericName>Temple</GenericName>
-    <Tooltip>Train healers. Garrison up to 20 units to heal them at a quick rate (3 HP per second). Research healing and religious improvements.</Tooltip>
+    <Tooltip>Train healers. Garrison units to heal them at a quick rate (3 HP per second). Research healing and religious improvements.</Tooltip>
     <VisibleClasses datatype="tokens">Town Temple</VisibleClasses>
     <Icon>structures/temple.png</Icon>
     <RequiredTechnology>phase_town</RequiredTechnology>
   </Identity>
   <Loot>
Index: binaries/data/mods/public/simulation/templates/template_structure_military_barracks.xml
===================================================================
--- binaries/data/mods/public/simulation/templates/template_structure_military_barracks.xml	(revision 18450)
+++ binaries/data/mods/public/simulation/templates/template_structure_military_barracks.xml	(working copy)
@@ -25,11 +25,11 @@
     <Max>2000</Max>
     <SpawnEntityOnDeath>rubble/rubble_stone_4x4</SpawnEntityOnDeath>
   </Health>
   <Identity>
     <GenericName>Barracks</GenericName>
-    <Tooltip>Train citizen-soldiers. Research training improvements. Garrison: 10.</Tooltip>
+    <Tooltip>Train citizen-soldiers. Research training improvements.</Tooltip>
     <VisibleClasses datatype="tokens">Village Barracks</VisibleClasses>
     <Icon>structures/barracks.png</Icon>
     <RequiredTechnology>phase_village</RequiredTechnology>
   </Identity>
   <Loot>
Index: binaries/data/mods/public/simulation/templates/template_structure_military_fortress.xml
===================================================================
--- binaries/data/mods/public/simulation/templates/template_structure_military_fortress.xml	(revision 18450)
+++ binaries/data/mods/public/simulation/templates/template_structure_military_fortress.xml	(working copy)
@@ -59,11 +59,11 @@
     <Max>4200</Max>
     <SpawnEntityOnDeath>rubble/rubble_stone_6x6</SpawnEntityOnDeath>
   </Health>
   <Identity>
     <GenericName>Fortress</GenericName>
-    <Tooltip>Train heroes, champions, and siege weapons. Research siege weapon improvements. Garrison: 20.</Tooltip>
+    <Tooltip>Train heroes, champions, and siege weapons. Research siege weapon improvements.</Tooltip>
     <Classes datatype="tokens">GarrisonFortress</Classes>
     <VisibleClasses datatype="tokens">Defensive City Fortress</VisibleClasses>
     <Icon>structures/fortress.png</Icon>
     <RequiredTechnology>phase_city</RequiredTechnology>
   </Identity>
Index: binaries/data/mods/public/simulation/templates/template_unit_mechanical_ship_bireme.xml
===================================================================
--- binaries/data/mods/public/simulation/templates/template_unit_mechanical_ship_bireme.xml	(revision 18450)
+++ binaries/data/mods/public/simulation/templates/template_unit_mechanical_ship_bireme.xml	(working copy)
@@ -45,11 +45,11 @@
   </Health>
   <Identity>
     <GenericName>Light Warship</GenericName>
     <Classes datatype="tokens">Warship Light Bow Ranged</Classes>
     <RequiredTechnology>phase_town</RequiredTechnology>
-    <Tooltip>Garrison up to 20 units for transport. Garrison increases the firepower up to 10 arrows.</Tooltip>
+    <Tooltip>Garrison units for transport and to increase firepower.</Tooltip>
   </Identity>
   <ResourceGatherer disable=""/>
   <Sound>
     <SoundGroups>
       <attack>attack/weapon/arrowfly.xml</attack>
Index: binaries/data/mods/public/simulation/templates/template_unit_mechanical_ship_quinquereme.xml
===================================================================
--- binaries/data/mods/public/simulation/templates/template_unit_mechanical_ship_quinquereme.xml	(revision 18450)
+++ binaries/data/mods/public/simulation/templates/template_unit_mechanical_ship_quinquereme.xml	(working copy)
@@ -21,11 +21,11 @@
       </Splash>
     </Ranged>
   </Attack>
   <BuildingAI>
     <DefaultArrowCount>1</DefaultArrowCount>
-    <MaxArrowCount>11</MaxArrowCount>
+    <MaxArrowCount>10</MaxArrowCount>
     <GarrisonArrowMultiplier>1</GarrisonArrowMultiplier>
     <GarrisonArrowClasses>Catapult</GarrisonArrowClasses>
   </BuildingAI>
   <Cost>
     <Population>3</Population>
@@ -52,11 +52,11 @@
   <Health>
     <Max>2000</Max>
   </Health>
   <Identity>
     <GenericName>Heavy Warship</GenericName>
-    <Tooltip>Garrison up to 10 catapults to increase fire power.</Tooltip>
+    <Tooltip>Garrison units for transport and to increase firepower.</Tooltip>
     <Classes datatype="tokens">Warship Heavy Ranged</Classes>
 	<RequiredTechnology>phase_city</RequiredTechnology>
   </Identity>
   <Sound>
     <SoundGroups>
Index: binaries/data/mods/public/simulation/templates/template_unit_mechanical_ship_trireme.xml
===================================================================
--- binaries/data/mods/public/simulation/templates/template_unit_mechanical_ship_trireme.xml	(revision 18450)
+++ binaries/data/mods/public/simulation/templates/template_unit_mechanical_ship_trireme.xml	(working copy)
@@ -45,11 +45,11 @@
   </Health>
   <Identity>
     <GenericName>Medium Warship</GenericName>
     <VisibleClasses datatype="tokens">Warship Medium Ranged</VisibleClasses>
     <RequiredTechnology>phase_town</RequiredTechnology>
-    <Tooltip>Garrison up to 30 units for transport. Garrison increases the firepower up to 13 arrows.</Tooltip>
+    <Tooltip>Garrison units for transport and to increase firepower.</Tooltip>
   </Identity>
   <ResourceGatherer disable=""/>
   <Sound>
     <SoundGroups>
       <attack>attack/weapon/arrowfly.xml</attack>
Index: binaries/data/mods/public/simulation/templates/template_unit_mechanical_siege_tower.xml
===================================================================
--- binaries/data/mods/public/simulation/templates/template_unit_mechanical_siege_tower.xml	(revision 18450)
+++ binaries/data/mods/public/simulation/templates/template_unit_mechanical_siege_tower.xml	(working copy)
@@ -53,11 +53,11 @@
     <Max>500</Max>
   </Health>
   <Identity>
     <GenericName>Siege Tower</GenericName>
     <Classes datatype="tokens">Ranged SiegeTower</Classes>
-    <Tooltip>Garrison up to 20 infantry inside to increase arrow count from 0 to 10.</Tooltip>
+    <Tooltip>Garrison units for transport and to increase firepower.</Tooltip>
   </Identity>
   <Selectable>
     <Overlay>
       <Texture>
         <MainTexture>circle/256x256.png</MainTexture>
