Index: binaries/data/mods/public/globalscripts/Templates.js
===================================================================
--- binaries/data/mods/public/globalscripts/Templates.js	(revision 18915)
+++ binaries/data/mods/public/globalscripts/Templates.js	(working copy)
@@ -120,11 +120,12 @@
 					"hack": getAttackStat("Hack"),
 					"pierce": getAttackStat("Pierce"),
 					"crush": getAttackStat("Crush"),
 					"minRange": getAttackStat("MinRange"),
 					"maxRange": getAttackStat("MaxRange"),
-					"elevationBonus": getAttackStat("ElevationBonus")
+					"elevationBonus": getAttackStat("ElevationBonus"),
+					"spread": getAttackStat("Spread")
 				};
 				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");
Index: binaries/data/mods/public/gui/common/tooltips.js
===================================================================
--- binaries/data/mods/public/gui/common/tooltips.js	(revision 18915)
+++ binaries/data/mods/public/gui/common/tooltips.js	(working copy)
@@ -15,10 +15,21 @@
 	"hack": translate("Hack"),
 	"pierce": translate("Pierce"),
 	"crush": translate("Crush"),
 };
 
+const g_RangeTooltipString = {
+	"relative": {
+		"minRange": translate("%(attackLabel)s %(damageTypes)s, %(rangeLabel)s %(minRange)s to %(maxRange)s (%(relativeRange)s) %(rangeUnit)s, %(spread)s, %(rate)s"),
+		"no-minRange": translate("%(attackLabel)s %(damageTypes)s, %(rangeLabel)s %(maxRange)s (%(relativeRange)s) %(rangeUnit)s, %(spread)s, %(rate)s"),
+	},
+	"non-relative": {
+		"minRange": translate("%(attackLabel)s %(damageTypes)s, %(rangeLabel)s %(minRange)s to %(maxRange)s %(rangeUnit)s, %(spread)s, %(rate)s"),
+		"no-minRange": translate("%(attackLabel)s %(damageTypes)s, %(rangeLabel)s %(maxRange)s %(rangeUnit)s, %(spread)s, %(rate)s"),
+	}
+};
+
 function costIcon(resource)
 {
 	return '[icon="icon_' + resource + '"]';
 }
 
@@ -130,17 +141,16 @@
 			"damage": dmg[dmgType].toFixed(1),
 			"damageType": unitFont(g_DamageTypes[dmgType])
 		})).join(commaFont(translate(", ")));
 }
 
-// TODO: should also show minRange
 function getAttackTooltip(template)
 {
 	if (!template.attack)
 		return "";
 
-	let attacks = [];
+	let tooltips = [];
 	for (let type in template.attack)
 	{
 		if (type == "Slaughter")
 			continue; // Slaughter is used to kill animals, so do not show it.
 
@@ -154,43 +164,50 @@
 		});
 
 		let attackLabel = headerFont(g_AttackTypes[type]);
 		if (type == "Capture" || type != "Ranged")
 		{
-			attacks.push(sprintf(translate("%(attackLabel)s %(details)s, %(rate)s"), {
+			tooltips.push(sprintf(translate("%(attackLabel)s %(details)s, %(rate)s"), {
 				"attackLabel": attackLabel,
 				"details":
 					type == "Capture" ?
 						template.attack.Capture.value :
 						damageTypesToText(template.attack[type]),
 				"rate": rate
 			}));
 			continue;
 		}
 
+		let minRange = Math.round(template.attack[type].minRange);
+		let maxRange = Math.round(template.attack[type].maxRange);
 		let realRange = template.attack[type].elevationAdaptedRange;
-		let range = Math.round(template.attack[type].maxRange);
-		let relativeRange = realRange ? Math.round(realRange - range) : 0;
+		let relativeRange = realRange ? Math.round(realRange - maxRange) : 0;
 
-		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");
+		// Compare spread at a defined distance
+		let spread = +(+template.attack[type].spread / maxRange * 100).toFixed(1);
 
-		attacks.push(sprintf(rangeString, {
+		tooltips.push(sprintf(g_RangeTooltipString[relativeRange ? "relative" : "non-relative"][minRange ? "minRange" : "no-minRange"], {
 			"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))
-				}),
+			"rangeLabel": headerFont(translate("Range:")),
+			"minRange": minRange,
+			"maxRange": maxRange,
+			"relativeRange": relativeRange > 0 ? "+" + 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)),
 			"rate": rate,
-			"relative": relativeRange > 0 ? "+" + relativeRange : relativeRange,
+			"spread": sprintf(translatePlural("%(label)s %(val)s %(unit)s", "%(label)s %(val)s %(unit)s", spread), {
+				"label": headerFont(translate("Spread:")),
+				"val": spread,
+				"unit": unitFont(translatePlural("meter", "meters", spread))
+			})
 		}));
 	}
-	return attacks.join("\n");
+	return tooltips.join("\n");
 }
 
 function getSplashDamageTooltip(template)
 {
 	if (!template.attack)
Index: binaries/data/mods/public/simulation/components/Attack.js
===================================================================
--- binaries/data/mods/public/simulation/components/Attack.js	(revision 18915)
+++ binaries/data/mods/public/simulation/components/Attack.js	(working copy)
@@ -379,10 +379,19 @@
 		"pierce": applyMods("Pierce"),
 		"crush": applyMods("Crush")
 	};
 };
 
+Attack.prototype.GetSpread = function(type)
+{
+	if (type != "Ranged" || !this.template[type])
+		return 0;
+
+	let spread = +this.template[type].Spread;
+	return ApplyValueModificationsToEntity("Attack/" + type + "/Spread", spread, this.entity);
+}
+
 Attack.prototype.GetSplashDamage = function(type)
 {
 	if (!this.template[type].Splash)
 		return false;
 
@@ -439,11 +448,12 @@
 	return attackBonus;
 };
 
 // Returns a 2d random distribution scaled for a spread of scale 1.
 // The current implementation is a 2d gaussian with sigma = 1
-Attack.prototype.GetNormalDistribution = function(){
+Attack.prototype.GetNormalDistribution = function()
+{
 
 	// Use the Box-Muller transform to get a gaussian distribution
 	let a = Math.random();
 	let b = Math.random();
 
@@ -474,13 +484,10 @@
 
 		// Get some data about the entity
 		let horizSpeed = +this.template[type].ProjectileSpeed;
 		let gravity = 9.81; // this affects the shape of the curve; assume it's constant for now
 
-		let spread = +this.template.Ranged.Spread;
-		spread = ApplyValueModificationsToEntity("Attack/Ranged/Spread", spread, this.entity);
-
 		//horizSpeed /= 2; gravity /= 2; // slow it down for testing
 
 		let cmpPosition = Engine.QueryInterface(this.entity, IID_Position);
 		if (!cmpPosition || !cmpPosition.IsInWorld())
 			return;
@@ -510,11 +517,11 @@
 
 		// Compute the real target point (based on spread and target speed)
 		let range = this.GetRange(type);
 		let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
 		let elevationAdaptedMaxRange = cmpRangeManager.GetElevationAdaptedRange(selfPosition, cmpPosition.GetRotation(), range.max, range.elevationBonus, 0);
-		let distanceModifiedSpread = spread * horizDistance/elevationAdaptedMaxRange;
+		let distanceModifiedSpread = this.GetSpread(type) * horizDistance/elevationAdaptedMaxRange;
 
 		let randNorm = this.GetNormalDistribution();
 		let offsetX = randNorm[0] * distanceModifiedSpread * (1 + targetVelocity.length() / 20);
 		let offsetZ = randNorm[1] * distanceModifiedSpread * (1 + targetVelocity.length() / 20);
 
Index: binaries/data/mods/public/simulation/components/GuiInterface.js
===================================================================
--- binaries/data/mods/public/simulation/components/GuiInterface.js	(revision 18915)
+++ binaries/data/mods/public/simulation/components/GuiInterface.js	(working copy)
@@ -442,10 +442,11 @@
 		if (types.length)
 			ret.attack = {};
 		for (let type of types)
 		{
 			ret.attack[type] = cmpAttack.GetAttackStrengths(type);
+			ret.attack[type].spread = cmpAttack.GetSpread(type);
 			ret.attack[type].splash = cmpAttack.GetSplashDamage(type);
 
 			let range = cmpAttack.GetRange(type);
 			ret.attack[type].minRange = range.min;
 			ret.attack[type].maxRange = range.max;

