Index: binaries/data/mods/public/gui/session/input.js
===================================================================
--- binaries/data/mods/public/gui/session/input.js	(revision 13491)
+++ binaries/data/mods/public/gui/session/input.js	(working copy)
@@ -121,7 +121,23 @@
 			// Show placement info tooltip if invalid position
 			placementSupport.tooltipError = !result.success;
 			placementSupport.tooltipMessage = result.success ? "" : result.message;
-			return result.success;
+
+			if (!result.success)
+				return false;
+
+			if (placementSupport.attack)
+			{
+				// building can be placed here, and has an attack
+				// show the range advantage in the tooltip
+				var cmd = {x: placementSupport.position.x, 
+				    z: placementSupport.position.z,
+				    range: placementSupport.attack.maxRange,
+				    elevationBonus: placementSupport.attack.elevationBonus,
+				};
+				var averageRange = Engine.GuiInterfaceCall("GetAverageRangeForBuildings",cmd);
+				placementSupport.tooltipMessage = "Basic range: "+Math.round(cmd.range/4)+"\nAverage bonus range: "+Math.round((averageRange - cmd.range)/4);
+			}
+			return true;
 		}
 	}
 	else if (placementSupport.mode === "wall")
@@ -1494,6 +1510,14 @@
 		placementSupport.template = buildTemplate;
 		inputState = INPUT_BUILDING_PLACEMENT;
 	}
+
+	if (templateData.attack && 
+	    templateData.attack.Ranged && 
+	    templateData.attack.Ranged.maxRange) 
+	{
+		// add attack information to display a good tooltip
+		placementSupport.attack = templateData.attack.Ranged;
+	}
 }
 
 // Called by GUI when user changes preferred trading goods
Index: binaries/data/mods/public/gui/session/placement.js
===================================================================
--- binaries/data/mods/public/gui/session/placement.js	(revision 13491)
+++ binaries/data/mods/public/gui/session/placement.js	(working copy)
@@ -22,6 +22,8 @@
 	
 	this.SetDefaultAngle();
 	this.RandomizeActorSeed();
+
+	this.attack = null;
 	
 	Engine.GuiInterfaceCall("SetBuildingPlacementPreview", {"template": ""});
 	Engine.GuiInterfaceCall("SetWallPlacementPreview", {"wallSet": null});
Index: binaries/data/mods/public/gui/session/selection_details.js
===================================================================
--- binaries/data/mods/public/gui/session/selection_details.js	(revision 13491)
+++ binaries/data/mods/public/gui/session/selection_details.js	(working copy)
@@ -183,18 +183,36 @@
 		getGUIObjectByName("icon").sprite = "bkFillBlack";
 	}
 
-	// Attack and Armor
-	var type = "";
-	if (entState.attack)
-		type = entState.attack.type + " ";
-
-	attack = "[font=\"serif-bold-13\"]"+type+"Attack:[/font] " + damageTypeDetails(entState.attack);
-	// Show max attack range if ranged attack, also convert to tiles (4m per tile)
-	if (entState.attack && entState.attack.type == "Ranged")
-		attack += ", [font=\"serif-bold-13\"]Range:[/font] " + Math.round(entState.attack.maxRange/4);
-	getGUIObjectByName("attackAndArmorStats").tooltip = attack + "\n[font=\"serif-bold-13\"]Armor:[/font] " + armorTypeDetails(entState.armour);
-
-	// Icon Tooltip
+	// Attack and Armor
+	var type = "";
+	if (entState.attack)
+	{
+		type = entState.attack.type + " ";
+
+		attack = "[font=\"serif-bold-13\"]"+type+"Attack:[/font] " + damageTypeDetails(entState.attack);
+		// Show max attack range if ranged attack, also convert to tiles (4m per tile)
+		if (entState.attack.type == "Ranged")
+		{
+			var realRange = entState.attack.elevationAdaptedRange;
+			var range =  entState.attack.maxRange;
+			attack += ", [font=\"serif-bold-13\"]Range:[/font] " + 
+				Math.round(range/4);
+
+			if (Math.round((realRange - range)/4) > 0)
+			{ 
+				attack += " (+" + Math.round((realRange - range)/4) + ")";
+			}
+			else if (Math.round((realRange - range)/4) < 0)
+			{
+				attack += " (" + Math.round((realRange - range)/4) + ")";
+			} // don't show when it's 0
+
+		}
+	}
+	
+	getGUIObjectByName("attackAndArmorStats").tooltip = attack + "\n[font=\"serif-bold-13\"]Armor:[/font] " + armorTypeDetails(entState.armour);
+
+	// Icon Tooltip
 	var iconTooltip = "";
 
 	if (genericName)
Index: binaries/data/mods/public/simulation/components/Attack.js
===================================================================
--- binaries/data/mods/public/simulation/components/Attack.js	(revision 13491)
+++ binaries/data/mods/public/simulation/components/Attack.js	(working copy)
@@ -67,6 +67,9 @@
 			"<Crush>0.0</Crush>" +
 			"<MaxRange>44.0</MaxRange>" +
 			"<MinRange>20.0</MinRange>" +
+			"<optional>"+
+				"<element name='ElevationBonus' a:help='give an elevation advantage (in meters)'><ref name='nonNegativeDecimal'/></element>" +
+			"</optional>" +
 			"<PrepareTime>800</PrepareTime>" +
 			"<RepeatTime>1600</RepeatTime>" +
 			"<ProjectileSpeed>50.0</ProjectileSpeed>" +
@@ -125,6 +128,9 @@
 				"<element name='Crush' a:help='Crush damage strength'><ref name='nonNegativeDecimal'/></element>" +
 				"<element name='MaxRange' a:help='Maximum attack range (in metres)'><ref name='nonNegativeDecimal'/></element>" +
 				"<element name='MinRange' a:help='Minimum attack range (in metres)'><ref name='nonNegativeDecimal'/></element>" +
+				"<optional>"+
+					"<element name='ElevationBonus' a:help='give an elevation advantage (in meters)'><ref name='nonNegativeDecimal'/></element>" +
+				"</optional>" +
 				"<element name='PrepareTime' a:help='Time from the start of the attack command until the attack actually occurs (in milliseconds). This value relative to RepeatTime should closely match the \"event\" point in the actor&apos;s attack animation'>" +
 					"<data type='nonNegativeInteger'/>" +
 				"</element>" +
@@ -360,8 +366,11 @@
 	
 	var min = +(this.template[type].MinRange || 0);
 	min = ApplyTechModificationsToEntity("Attack/" + type + "/MinRange", min, this.entity);
+
+	var elevationBonus = +(this.template[type].ElevationBonus || 0);
+	elevationBonus = ApplyTechModificationsToEntity("Attack/" + type + "/ElevationBonus", elevationBonus, this.entity);
 	
-	return { "max": max, "min": min };
+	return { "max": max, "min": min, "elevationBonus": elevationBonus};
 };
 
 // Calculate the attack damage multiplier against a target
Index: binaries/data/mods/public/simulation/components/BuildingAI.js
===================================================================
--- binaries/data/mods/public/simulation/components/BuildingAI.js	(revision 13491)
+++ binaries/data/mods/public/simulation/components/BuildingAI.js	(working copy)
@@ -12,6 +12,7 @@
 		"<ref name='nonNegativeDecimal'/>" +
 	"</element>";
 
+
 /**
  * Initialize BuildingAI Component
  */
@@ -99,7 +100,7 @@
 	if (cmpAttack)
 	{
 		var range = cmpAttack.GetRange("Ranged");
-		this.enemyUnitsQuery = cmpRangeManager.CreateActiveQuery(this.entity, range.min, range.max, players, IID_DamageReceiver, cmpRangeManager.GetEntityFlagMask("normal"));
+		this.enemyUnitsQuery = cmpRangeManager.CreateActiveParabolicQuery(this.entity, range.min, range.max, range.elevationBonus, players, IID_DamageReceiver, cmpRangeManager.GetEntityFlagMask("normal"));
 		cmpRangeManager.EnableActiveQuery(this.enemyUnitsQuery);
 	}
 };
@@ -133,7 +134,7 @@
 		var range = cmpAttack.GetRange("Ranged");
 
 		// This query is only interested in Gaia entities that can attack.
-		this.gaiaUnitsQuery = rangeMan.CreateActiveQuery(this.entity, range.min, range.max, [0], IID_Attack, rangeMan.GetEntityFlagMask("normal"));
+		this.gaiaUnitsQuery = rangeMan.CreateActiveParabolicQuery(this.entity, range.min, range.max, range.elevationBonus, [0], IID_Attack, rangeMan.GetEntityFlagMask("normal"));
 		rangeMan.EnableActiveQuery(this.gaiaUnitsQuery);
 	}
 };
@@ -214,6 +215,7 @@
 	var cmpAttack = Engine.QueryInterface(this.entity, IID_Attack);
 	if (cmpAttack)
 	{
+
 		var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
 		this.timer = cmpTimer.SetTimeout(this.entity, IID_BuildingAI, "FireArrows", timerInterval, {});
 		var arrowsToFire = 0;
@@ -239,12 +241,33 @@
 			//Fire N arrows, 0 <= N <= Number of arrows left
 			arrowsToFire = Math.floor(Math.random() * this.arrowsLeft);
 		}
+
 		if (this.targetUnits.length > 0)
 		{
+			var clonedTargets = this.targetUnits.slice();
 			for (var i = 0;i < arrowsToFire;i++)
 			{
-				cmpAttack.PerformAttack("Ranged", this.targetUnits[Math.floor(Math.random() * this.targetUnits.length)]);
-				PlaySound("arrowfly", this.entity);
+				var target = clonedTargets[Math.floor(Math.random() * this.targetUnits.length)];
+				if (
+					target && 
+					this.CheckTargetVisible(target) 
+				   ) 
+				{
+					cmpAttack.PerformAttack("Ranged", target);
+					PlaySound("arrowfly", this.entity);
+
+				}
+				else 
+				{
+					clonedTargets.splice(clonedTargets.indexOf(target),1);
+					i--; // one extra arrow left to fire
+					if(clonedTargets.length < 1) 
+					{
+						this.arrowsLeft += arrowsToFire;
+						// no targets found in this round, save arrows and go to next round
+						break;
+					}
+				}
 			}
 			this.arrowsLeft -= arrowsToFire;
 		}
@@ -252,4 +275,22 @@
 	}
 };
 
+/**
+ * Returns true if the target entity is visible through the FoW/SoD.
+ */
+BuildingAI.prototype.CheckTargetVisible = function(target)
+{
+	var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership);
+	if (!cmpOwnership)
+		return false;
+
+	var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
+
+	if (cmpRangeManager.GetLosVisibility(target, cmpOwnership.GetOwner(), false) == "hidden")
+		return false;
+
+	// Either visible directly, or visible in fog
+	return true;
+};
+
 Engine.RegisterComponentType(IID_BuildingAI, "BuildingAI", BuildingAI);
Index: binaries/data/mods/public/simulation/components/GuiInterface.js
===================================================================
--- binaries/data/mods/public/simulation/components/GuiInterface.js	(revision 13491)
+++ binaries/data/mods/public/simulation/components/GuiInterface.js	(working copy)
@@ -173,6 +173,7 @@
 	if (cmpPosition && cmpPosition.IsInWorld())
 	{
 		ret.position = cmpPosition.GetPosition();
+		ret.rotation = cmpPosition.GetRotation();
 	}
 
 	var cmpHealth = Engine.QueryInterface(ent, IID_Health);
@@ -184,7 +185,10 @@
 		ret.needsHeal = !cmpHealth.IsUnhealable();
 	}
 
+	var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
+	var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
 	var cmpAttack = Engine.QueryInterface(ent, IID_Attack);
+
 	if (cmpAttack)
 	{
 		var type = cmpAttack.GetBestAttack(); // TODO: how should we decide which attack to show? show all?
@@ -193,6 +197,32 @@
 		ret.attack.type = type;
 		ret.attack.minRange = range.min;
 		ret.attack.maxRange = range.max;
+		if (type == "Ranged")
+		{
+			ret.attack.elevationBonus = range.elevationBonus;
+			if (cmpUnitAI && cmpPosition && cmpPosition.IsInWorld())
+			{
+				// For units, take the rage in front of it, no spread. So angle = 0
+				ret.attack.elevationAdaptedRange = cmpRangeManager.GetElevationAdaptedRange(ret.position, ret.rotation, range.max, range.elevationBonus, 0);
+			}
+			else if(cmpPosition && cmpPosition.IsInWorld())
+			{
+				// For buildings, take the average elevation around it. So angle = 2*pi
+				ret.attack.elevationAdaptedRange = cmpRangeManager.GetElevationAdaptedRange(ret.position, ret.rotation, range.max, range.elevationBonus, 2*Math.PI);
+			}
+			else
+			{
+				// not in world, set a default?
+				ret.attack.elevationAdaptedRange = ret.attack.maxRange;
+			}
+			
+		}
+		else
+		{
+			// not a ranged attack, set some defaults
+			ret.attack.elevationBonus = 0;
+			ret.attack.elevationAdaptedRange = ret.attack.maxRange;
+		}
 	}
 
 	var cmpArmour = Engine.QueryInterface(ent, IID_DamageReceiver);
@@ -310,8 +340,7 @@
 			"req": cmpPromotion.GetRequiredXp()
 		};
 	}
-	
-	var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
+
 	if (cmpUnitAI)
 	{
 		ret.unitAI = {
@@ -322,7 +351,7 @@
 		if (cmpUnitAI.isGarrisoned && ret.player)
 			ret.template = "p" + ret.player + "&" + ret.template;
 	}
-	
+
 	var cmpGate = Engine.QueryInterface(ent, IID_Gate);
 	if (cmpGate)
 	{
@@ -346,12 +375,26 @@
 		};
 	}
 
-	var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
 	ret.visibility = cmpRangeManager.GetLosVisibility(ent, player, false);
 
 	return ret;
 };
 
+GuiInterface.prototype.GetAverageRangeForBuildings = function(player, cmd)
+{
+	var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
+	var cmpTerrain = Engine.QueryInterface(SYSTEM_ENTITY, IID_Terrain);
+	var rot = {x:0, y:0, z:0};
+	var pos = {x:cmd.x,z:cmd.z};
+	pos.y = cmpTerrain.GetGroundLevel(cmd.x, cmd.z);
+	var elevationBonus = cmd.elevationBonus || 0;
+	var range = cmd.range;
+
+	return cmpRangeManager.GetElevationAdaptedRange(pos, rot, range, elevationBonus, 2*Math.PI);
+};
+
+
+
 GuiInterface.prototype.GetTemplateData = function(player, extendedName)
 {
 	var name = extendedName;
@@ -390,6 +433,7 @@
 				"crush": GetTechModifiedProperty(techMods, template, "Attack/"+type+"/Crush", +(template.Attack[type].Crush || 0)),
 				"minRange": GetTechModifiedProperty(techMods, template, "Attack/"+type+"/MinRange", +(template.Attack[type].MinRange || 0)),
 				"maxRange": GetTechModifiedProperty(techMods, template, "Attack/"+type+"/MaxRange", +template.Attack[type].MaxRange),
+				"elevationBonus": GetTechModifiedProperty(techMods, template, "Attack/"+type+"/ElevationBonus", +(template.Attack[type].ElevationBonus || 0)),
 			};
 		}
 	}
@@ -1726,6 +1770,7 @@
 	"GetRenamedEntities": 1,
 	"ClearRenamedEntities": 1,
 	"GetEntityState": 1,
+	"GetAverageRangeForBuildings": 1,
 	"GetTemplateData": 1,
 	"GetTechnologyData": 1,
 	"IsTechnologyResearched": 1,
Index: binaries/data/mods/public/simulation/components/UnitAI.js
===================================================================
--- binaries/data/mods/public/simulation/components/UnitAI.js	(revision 13491)
+++ binaries/data/mods/public/simulation/components/UnitAI.js	(working copy)
@@ -367,7 +367,7 @@
 		this.attackType = type;
 
 		// If we are already at the target, try attacking it from here
-		if (this.CheckTargetRange(this.order.data.target, IID_Attack, this.attackType))
+		if (this.CheckTargetAttackRange(this.order.data.target, IID_Attack, this.attackType))
 		{
 			this.StopMoving();
 			// For packable units within attack range:
@@ -427,8 +427,8 @@
 			return;
 		}
 
-		// Try to move within attack range
-		if (this.MoveToTargetRange(this.order.data.target, IID_Attack, this.attackType))
+		// Try to move within attack range 
+		if (this.MoveToTargetAttackRange(this.order.data.target, IID_Attack, this.attackType,0.5))
 		{
 			// We've started walking to the given point
 			if (this.IsAnimal())
@@ -1314,11 +1314,27 @@
 				},
 
 				"MoveCompleted": function() {
-					// If the unit needs to unpack, do so
-					if (this.CanUnpack())
-						this.SetNextState("UNPACKING");
-					else
-						this.SetNextState("ATTACKING");
+
+					if (this.CheckTargetAttackRange(this.order.data.target, IID_Attack , this.attackType)) 
+					{
+						// If the unit needs to unpack, do so
+						if (this.CanUnpack())
+							this.SetNextState("UNPACKING");
+						else
+							this.SetNextState("ATTACKING");
+					} 
+					else 
+					{
+						if (this.MoveToTargetAttackRange(this.order.data.target, IID_Attack, this.attackType,0))
+						{
+							this.SetNextState("APPROACHING");
+						}
+						else
+						{
+							// Give up
+							this.FinishOrder();
+						}
+					}
 				},
 
 				"Attacked": function(msg) {
@@ -1334,9 +1350,9 @@
 			"UNPACKING": {
 				"enter": function() {
 					// If we're not in range yet (maybe we stopped moving), move to target again
-					if (!this.CheckTargetRange(this.order.data.target, IID_Attack, this.attackType))
+					if (!this.CheckTargetAttackRange(this.order.data.target, IID_Attack, this.attackType))
 					{
-						if (this.MoveToTargetRange(this.order.data.target, IID_Attack, this.attackType))
+						if (this.MoveToTargetAttackRange(this.order.data.target, IID_Attack, this.attackType,0))
 							this.SetNextState("APPROACHING");
 						else
 						{
@@ -1402,7 +1418,7 @@
 					if (this.TargetIsAlive(target) && this.CanAttack(target, this.order.data.forceResponse || null))
 					{
 						// Check we can still reach the target
-						if (this.CheckTargetRange(target, IID_Attack, this.attackType))
+						if (this.CheckTargetAttackRange(target, IID_Attack, this.attackType))
 						{
 							var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
 							this.lastAttacked = cmpTimer.GetTime() - msg.lateness;
@@ -3287,6 +3303,54 @@
 	return cmpUnitMotion.MoveToTargetRange(target, range.min, range.max);
 };
 
+/**
+ * Move unit so we hope the target is in the attack range
+ * for melee attacks, this goes straight to the default range checks
+ * for ranged attacks, the parabolic range is used, so we can't know exactly at what horizontal range the target can be reached
+ * That's why a guess is needed
+ * a guess of 1 will take the maximum of the possible ranges, and stay far away
+ * a guess of 0 will take the minimum of the possible ranges and, in most cases, will have the target in range.
+ * every guess inbetween is a linear interpollation
+ */
+UnitAI.prototype.MoveToTargetAttackRange = function(target, iid, type,guess)
+{
+
+	if(type!= "Ranged") {
+		return this.MoveToTargetRange(target, iid, type);
+	}
+	if (!this.CheckTargetVisible(target)) {
+		return false;
+	}
+	var cmpRanged = Engine.QueryInterface(this.entity, iid);
+	var range = cmpRanged.GetRange(type);
+
+	var thisCmpPosition = Engine.QueryInterface(this.entity, IID_Position);
+	var s = thisCmpPosition.GetPosition();
+
+	var targetCmpPosition = Engine.QueryInterface(target, IID_Position);
+	if(!targetCmpPosition.IsInWorld()) 
+		return false;   
+
+	var t = targetCmpPosition.GetPosition();
+	// h is positive when I'm higher than the target
+	var h = s.y-t.y+range.elevationBonus;
+
+	// No negative roots please
+	if(h>-range.max/2) {
+		var parabolicMaxRange = Math.sqrt(range.max*range.max+2*range.max*h);
+	} else {
+		// return false? Or hope you come close enough?
+		var parabolicMaxRange = 0;
+		//return false;
+	}
+	// the parabole changes while walking, take something in the middle
+	var guessedMaxRange = Math.max(range.max, parabolicMaxRange)*guess+Math.min(range.max, parabolicMaxRange)*(1-guess) ;
+	
+	var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion);
+	var r = cmpUnitMotion.MoveToTargetRange(target, range.min, guessedMaxRange);
+	return r;
+};
+
 UnitAI.prototype.MoveToTargetRangeExplicit = function(target, min, max)
 {
 	if (!this.CheckTargetVisible(target))
@@ -3311,6 +3375,47 @@
 	return cmpUnitMotion.IsInTargetRange(target, range.min, range.max);
 };
 
+/**
+ * Check if the target is inside the attack range 
+ * For melee attacks, this goes straigt to the regular range calculation
+ * For ranged attacks, the parabolic formula is used to accout for bigger ranges
+ * when the target is lower, and smaller ranges when the target is higher
+ */ 
+UnitAI.prototype.CheckTargetAttackRange = function(target, iid, type)
+{
+
+	if (type != "Ranged") 
+	{
+		return this.CheckTargetRange(target,iid,type);
+	}
+	
+	var targetCmpPosition = Engine.QueryInterface(target, IID_Position);
+	if (!targetCmpPosition || !targetCmpPosition.IsInWorld()) 
+	{
+		return false; 
+	}
+
+	var cmpRanged = Engine.QueryInterface(this.entity, iid);
+	var range = cmpRanged.GetRange(type);
+
+	var thisCmpPosition = Engine.QueryInterface(this.entity, IID_Position);
+	var s = thisCmpPosition.GetPosition();
+
+	var t = targetCmpPosition.GetPosition();
+
+	var h = s.y-t.y+range.elevationBonus;
+	var maxRangeSq = 2*range.max*(h + range.max/2);
+	if (maxRangeSq<0)
+	{ 
+		// certainly outside the range, the target is even too high to reach.
+		return false;
+	}
+
+	// use native range checking function to take account of target shape 
+	// in case of big targets
+	return this.CheckTargetRangeExplicit(target,range.min,Math.sqrt(maxRangeSq));
+};
+
 UnitAI.prototype.CheckTargetRangeExplicit = function(target, min, max)
 {
 	var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion);
Index: binaries/data/mods/public/simulation/templates/template_structure_defense_defense_tower.xml
===================================================================
--- binaries/data/mods/public/simulation/templates/template_structure_defense_defense_tower.xml	(revision 13491)
+++ binaries/data/mods/public/simulation/templates/template_structure_defense_defense_tower.xml	(working copy)
@@ -2,14 +2,15 @@
 <Entity parent="template_structure_defense">
   <Attack>
     <Ranged>
-      <Hack>0.0</Hack>
-      <Pierce>20.0</Pierce>
-      <Crush>0.0</Crush>
-      <MaxRange>70.0</MaxRange>
-      <MinRange>16.0</MinRange>
-      <ProjectileSpeed>75.0</ProjectileSpeed>
-      <PrepareTime>1200</PrepareTime>
-      <RepeatTime>2000</RepeatTime>
+      <Hack>0.0</Hack>
+      <Pierce>20.0</Pierce>
+      <Crush>0.0</Crush>
+      <MaxRange>57.0</MaxRange>
+      <MinRange>16.0</MinRange>
+      <ElevationBonus>15</ElevationBonus>
+      <ProjectileSpeed>75.0</ProjectileSpeed>
+      <PrepareTime>1200</PrepareTime>
+      <RepeatTime>2000</RepeatTime>
       <Spread>1.5</Spread>
     </Ranged>
   </Attack>
Index: source/scriptinterface/ScriptInterface.h
===================================================================
--- source/scriptinterface/ScriptInterface.h	(revision 13491)
+++ source/scriptinterface/ScriptInterface.h	(working copy)
@@ -38,7 +38,7 @@
 // Set the maximum number of function arguments that can be handled
 // (This should be as small as possible (for compiler efficiency),
 // but as large as necessary for all wrapped functions)
-#define SCRIPT_INTERFACE_MAX_ARGS 7
+#define SCRIPT_INTERFACE_MAX_ARGS 8
 
 // TODO: what's a good default?
 #define DEFAULT_RUNTIME_SIZE 16 * 1024 * 1024
Index: source/simulation2/components/CCmpRangeManager.cpp
===================================================================
--- source/simulation2/components/CCmpRangeManager.cpp	(revision 13491)
+++ source/simulation2/components/CCmpRangeManager.cpp	(working copy)
@@ -20,10 +20,12 @@
 #include "simulation2/system/Component.h"
 #include "ICmpRangeManager.h"
 
+#include "ICmpTerrain.h"
 #include "simulation2/MessageTypes.h"
 #include "simulation2/components/ICmpPosition.h"
 #include "simulation2/components/ICmpTerritoryManager.h"
 #include "simulation2/components/ICmpVision.h"
+#include "simulation2/components/ICmpWaterManager.h"
 #include "simulation2/helpers/Render.h"
 #include "simulation2/helpers/Spatial.h"
 
@@ -44,9 +46,11 @@
 struct Query
 {
 	bool enabled;
+	bool parabolic;
 	entity_id_t source;
 	entity_pos_t minRange;
 	entity_pos_t maxRange;
+	entity_pos_t elevationBonus;
 	u32 ownersMask;
 	i32 interface;
 	std::vector<entity_id_t> lastMatch;
@@ -88,6 +92,44 @@
 }
 
 /**
+ * Checks whether v is in a parabolic range of (0,0,0)
+ * The highest point of the paraboloid is (0,range/2,0)
+ * and the circle of distance 'range' around (0,0,0) on height y=0 is part of the paraboloid
+ * 
+ * Avoids sqrting and overflowing.
+ */
+static bool InParabolicRange(CFixedVector3D v, fixed range) 
+{
+	i64 x = (i64)v.X.GetInternalValue(); // abs(x) <= 2^31
+	i64 z = (i64)v.Z.GetInternalValue();
+	i64 xx = (x * x); // xx <= 2^62
+	i64 zz = (z * z);
+	i64 d2 = (xx + zz) >> 1; // d2 <= 2^62 (no overflow)
+	
+	i64 y = (i64)v.Y.GetInternalValue();
+
+	i64 c = (i64)range.GetInternalValue();
+	i64 c_2 = c >> 1; 
+
+	i64 c2 = (c_2-y)*c;
+
+	if (d2 <= c2)
+		return true;
+
+	return false;
+}
+
+struct EntityParabolicRangeOutline
+{
+	entity_id_t source;
+	CFixedVector3D position;
+	entity_pos_t range;
+	std::vector<entity_pos_t> outline;
+};
+
+static std::map<entity_id_t, EntityParabolicRangeOutline> ParabolicRangesOutlines;
+
+/**
  * Representation of an entity, with the data needed for queries.
  */
 struct EntityData
@@ -113,9 +155,11 @@
 	void operator()(S& serialize, const char* UNUSED(name), Query& value)
 	{
 		serialize.Bool("enabled", value.enabled);
+		serialize.Bool("parabolic",value.parabolic);
 		serialize.NumberU32_Unbounded("source", value.source);
 		serialize.NumberFixed_Unbounded("min range", value.minRange);
 		serialize.NumberFixed_Unbounded("max range", value.maxRange);
+		serialize.NumberFixed_Unbounded("elevation bonus", value.elevationBonus);
 		serialize.NumberU32_Unbounded("owners mask", value.ownersMask);
 		serialize.NumberI32_Unbounded("interface", value.interface);
 		SerializeVector<SerializeU32_Unbounded>()(serialize, "last match", value.lastMatch);
@@ -589,6 +633,16 @@
 		return id;
 	}
 
+	virtual tag_t CreateActiveParabolicQuery(entity_id_t source,
+		entity_pos_t minRange, entity_pos_t maxRange, entity_pos_t elevationBonus,
+		std::vector<int> owners, int requiredInterface, u8 flags)
+	{
+		tag_t id = m_QueryNext++;
+		m_Queries[id] = ConstructParabolicQuery(source, minRange, maxRange, elevationBonus, owners, requiredInterface, flags);
+
+		return id;
+	}
+
 	virtual void DestroyActiveQuery(tag_t tag)
 	{
 		if (m_Queries.find(tag) == m_Queries.end())
@@ -812,8 +866,49 @@
 				r.push_back(it->first);
 			}
 		}
-		else
+		// Not the entire world, so check a parabolic range, or a regular range
+		else if (q.parabolic) 
 		{
+			// elevationBonus is part of the 3D position, as the source is really that much heigher
+			CFixedVector3D pos3d = cmpSourcePosition->GetPosition()+
+			    CFixedVector3D(entity_pos_t::Zero(), q.elevationBonus, entity_pos_t::Zero()) ;
+			// Get a quick list of entities that are potentially in range, with a cutoff of 2*maxRange
+			std::vector<entity_id_t> ents = m_Subdivision.GetNear(pos, q.maxRange*2);
+
+			for (size_t i = 0; i < ents.size(); ++i)
+			{
+				std::map<entity_id_t, EntityData>::const_iterator it = m_EntityData.find(ents[i]);
+				ENSURE(it != m_EntityData.end());
+
+				if (!TestEntityQuery(q, it->first, it->second))
+					continue;
+				
+				CmpPtr<ICmpPosition> cmpSecondPosition(GetSimContext(), ents[i]);
+				if (!cmpSecondPosition || !cmpSecondPosition->IsInWorld())
+					continue;
+				CFixedVector3D secondPosition = cmpSecondPosition->GetPosition();
+
+				// Restrict based on precise distance
+				if (!InParabolicRange(
+						CFixedVector3D(it->second.x, secondPosition.Y, it->second.z) 
+							- pos3d, 
+						q.maxRange))
+					continue;
+
+				if (!q.minRange.IsZero())
+				{
+					int distVsMin = (CFixedVector2D(it->second.x, it->second.z) - pos).CompareLength(q.minRange);
+					if (distVsMin < 0)
+						continue;
+				}
+
+				r.push_back(it->first);
+
+			}
+		}
+		// check a regular range (i.e. not the entire world, and not parabolic)
+		else 
+		{
 			// Get a quick list of entities that are potentially in range
 			std::vector<entity_id_t> ents = m_Subdivision.GetNear(pos, q.maxRange);
 
@@ -838,10 +933,116 @@
 				}
 
 				r.push_back(it->first);
+
 			}
 		}
 	}
 
+
+	virtual entity_pos_t GetElevationAdaptedRange(CFixedVector3D pos, CFixedVector3D rot, entity_pos_t range, entity_pos_t elevationBonus, entity_pos_t angle)
+	{
+		entity_pos_t r = entity_pos_t::Zero() ;
+		
+		pos.Y += elevationBonus;
+		entity_pos_t orientation = rot.Y;
+
+		entity_pos_t maxAngle = orientation + angle/2;
+		entity_pos_t minAngle = orientation - angle/2;
+
+		int numberOfSteps = 16;
+
+		if (angle == entity_pos_t::Zero())
+			numberOfSteps = 1;
+
+		std::vector<entity_pos_t> coords = getParabolicRangeForm(pos, range, range*2, minAngle, maxAngle, numberOfSteps);
+
+		entity_pos_t part =  entity_pos_t::FromInt(numberOfSteps);
+
+		for (int i = 0; i < numberOfSteps; i++)
+		{
+			r = r + CFixedVector2D(coords[2*i],coords[2*i+1]).Length() / part;
+		}
+		
+		return r;
+		
+	}
+
+	virtual std::vector<entity_pos_t> getParabolicRangeForm(CFixedVector3D pos, entity_pos_t maxRange, entity_pos_t cutoff, entity_pos_t minAngle, entity_pos_t maxAngle, int numberOfSteps) 
+	{
+		
+		// angle = 0 goes in the positive Z direction
+		entity_pos_t precision = entity_pos_t::FromInt((int)TERRAIN_TILE_SIZE)/8;
+
+		std::vector<entity_pos_t> r;
+		
+
+		CmpPtr<ICmpTerrain> cmpTerrain(GetSimContext(), SYSTEM_ENTITY);
+		CmpPtr<ICmpWaterManager> cmpWaterManager(GetSimContext(), SYSTEM_ENTITY);
+		entity_pos_t waterLevel = cmpWaterManager->GetWaterLevel(pos.X,pos.Z);
+		entity_pos_t thisHeight = pos.Y > waterLevel ? pos.Y : waterLevel;
+
+		if (cmpTerrain) 
+		{
+			for (int i = 0; i < numberOfSteps; i++)
+			{
+				entity_pos_t angle = minAngle + (maxAngle - minAngle) / numberOfSteps * i;
+				entity_pos_t sin;
+				entity_pos_t cos;
+				entity_pos_t minDistance = entity_pos_t::Zero();
+				entity_pos_t maxDistance = cutoff;
+				sincos_approx(angle,sin,cos);
+
+				CFixedVector2D minVector = CFixedVector2D(entity_pos_t::Zero(),entity_pos_t::Zero());
+				CFixedVector2D maxVector = CFixedVector2D(sin,cos).Multiply(cutoff);
+				entity_pos_t targetHeight = cmpTerrain->GetGroundLevel(pos.X+maxVector.X,pos.Z+maxVector.Y);
+				// use water level to display range on water
+				targetHeight = targetHeight > waterLevel ? targetHeight : waterLevel;
+
+				if (InParabolicRange(CFixedVector3D(maxVector.X,targetHeight-thisHeight,maxVector.Y),maxRange))
+				{
+					r.push_back(maxVector.X);
+					r.push_back(maxVector.Y);
+					continue;
+				}
+				
+				// Loop until vectors come close enough
+				while ((maxVector - minVector).CompareLength(precision) > 0)
+				{
+					// difference still bigger than precision, bisect to get smaller difference
+					entity_pos_t newDistance = (minDistance+maxDistance)/entity_pos_t::FromInt(2);
+
+					CFixedVector2D newVector = CFixedVector2D(sin,cos).Multiply(newDistance);
+
+					// get the height of the ground
+					targetHeight = cmpTerrain->GetGroundLevel(pos.X+newVector.X,pos.Z+newVector.Y);
+					targetHeight = targetHeight > waterLevel ? targetHeight : waterLevel;
+
+					if (InParabolicRange(CFixedVector3D(newVector.X,targetHeight-thisHeight,newVector.Y),maxRange))
+					{
+						// new vector is in parabolic range, so this is a new minVector
+						minVector = newVector;
+						minDistance = newDistance;
+					}
+					else 
+					{
+						// new vector is out parabolic range, so this is a new maxVector
+						maxVector = newVector;
+						maxDistance = newDistance;
+					}
+					
+				}
+				r.push_back(maxVector.X);
+				r.push_back(maxVector.Y);
+				
+			}
+			r.push_back(r[0]);
+			r.push_back(r[1]); 
+
+		}
+		return r;
+
+	}
+	
 	Query ConstructQuery(entity_id_t source,
 		entity_pos_t minRange, entity_pos_t maxRange,
 		const std::vector<int>& owners, int requiredInterface, u8 flagsMask)
@@ -856,9 +1057,11 @@
 
 		Query q;
 		q.enabled = false;
+		q.parabolic = false;
 		q.source = source;
 		q.minRange = minRange;
 		q.maxRange = maxRange;
+		q.elevationBonus = entity_pos_t::Zero();
 
 		q.ownersMask = 0;
 		for (size_t i = 0; i < owners.size(); ++i)
@@ -870,11 +1073,21 @@
 		return q;
 	}
 
+	Query ConstructParabolicQuery(entity_id_t source,
+		entity_pos_t minRange, entity_pos_t maxRange, entity_pos_t elevationBonus,
+		const std::vector<int>& owners, int requiredInterface, u8 flagsMask)
+	{
+		Query q = ConstructQuery(source,minRange,maxRange,owners,requiredInterface,flagsMask);
+		q.parabolic = true;
+		q.elevationBonus = elevationBonus;
+		return q;
+	}
+
+
 	void RenderSubmit(SceneCollector& collector)
 	{
 		if (!m_DebugOverlayEnabled)
 			return;
-
 		CColor enabledRingColour(0, 1, 0, 1);
 		CColor disabledRingColour(1, 0, 0, 1);
 		CColor rayColour(1, 1, 0, 0.2f);
@@ -893,15 +1106,74 @@
 				CFixedVector2D pos = cmpSourcePosition->GetPosition2D();
 
 				// Draw the max range circle
-				m_DebugOverlayLines.push_back(SOverlayLine());
-				m_DebugOverlayLines.back().m_Color = (q.enabled ? enabledRingColour : disabledRingColour);
-				SimRender::ConstructCircleOnGround(GetSimContext(), pos.X.ToFloat(), pos.Y.ToFloat(), q.maxRange.ToFloat(), m_DebugOverlayLines.back(), true);
+				if (!q.parabolic)
+				{
+					m_DebugOverlayLines.push_back(SOverlayLine());
+					m_DebugOverlayLines.back().m_Color = (q.enabled ? enabledRingColour : disabledRingColour);
+					SimRender::ConstructCircleOnGround(GetSimContext(), pos.X.ToFloat(), pos.Y.ToFloat(), q.maxRange.ToFloat(), m_DebugOverlayLines.back(), true);
+				}
+				else 
+				{ 
+					// elevation bonus is part of the 3D position. As if the unit is really that much higher
+					CFixedVector3D pos = cmpSourcePosition->GetPosition(); 
+					pos.Y += q.elevationBonus;
 
+					std::vector<entity_pos_t> coords;
+					
+					// Get the outline from cache if possible
+					if (ParabolicRangesOutlines.find(q.source) != ParabolicRangesOutlines.end())
+					{
+						EntityParabolicRangeOutline e = ParabolicRangesOutlines[q.source];
+						if (e.position == pos && e.range == q.maxRange) 
+						{
+							// outline is cached correctly, use it
+							coords = e.outline;	
+						}
+						else
+						{
+							// outline was cached, but important parameters changed 
+							// (position, elevation, range)
+							// update it
+							coords = getParabolicRangeForm(pos,q.maxRange,q.maxRange*2, entity_pos_t::Zero(), entity_pos_t::FromFloat(2.0f*3.14f),70);
+							e.outline = coords;
+							e.range = q.maxRange;
+							e.position = pos;
+							ParabolicRangesOutlines[q.source] = e;
+						}
+					}
+					else 
+					{
+						// outline wasn't cached (first time you enable the range overlay 
+						// or you created a new entiy)
+						// cache a new outline
+						coords = getParabolicRangeForm(pos,q.maxRange,q.maxRange*2, entity_pos_t::Zero(), entity_pos_t::FromFloat(2.0f*3.14f),70);
+						EntityParabolicRangeOutline e;
+						e.source = q.source;
+						e.range = q.maxRange;
+						e.position = pos;
+						e.outline = coords;
+						ParabolicRangesOutlines[q.source] = e;
+					}
+					
+					CColor thiscolor = q.enabled ? enabledRingColour : disabledRingColour;
+					
+					// draw the outline (piece by piece)	
+					for (size_t i = 3; i < coords.size(); i += 2)
+					{
+						std::vector<float> c;
+						c.push_back((coords[i-3]+pos.X).ToFloat());
+						c.push_back((coords[i-2]+pos.Z).ToFloat());
+						c.push_back((coords[i-1]+pos.X).ToFloat());
+						c.push_back((coords[i]+pos.Z).ToFloat());
+						m_DebugOverlayLines.push_back(SOverlayLine());
+						m_DebugOverlayLines.back().m_Color = thiscolor;
+						SimRender::ConstructLineOnGround(GetSimContext(), c, m_DebugOverlayLines.back(), true);
+					}
+				}
+
 				// Draw the min range circle
 				if (!q.minRange.IsZero())
 				{
-					m_DebugOverlayLines.push_back(SOverlayLine());
-					m_DebugOverlayLines.back().m_Color = (q.enabled ? enabledRingColour : disabledRingColour);
 					SimRender::ConstructCircleOnGround(GetSimContext(), pos.X.ToFloat(), pos.Y.ToFloat(), q.minRange.ToFloat(), m_DebugOverlayLines.back(), true);
 				}
 
Index: source/simulation2/components/ICmpRangeManager.cpp
===================================================================
--- source/simulation2/components/ICmpRangeManager.cpp	(revision 13491)
+++ source/simulation2/components/ICmpRangeManager.cpp	(working copy)
@@ -36,6 +36,7 @@
 BEGIN_INTERFACE_WRAPPER(RangeManager)
 DEFINE_INTERFACE_METHOD_5("ExecuteQuery", std::vector<entity_id_t>, ICmpRangeManager, ExecuteQuery, entity_id_t, entity_pos_t, entity_pos_t, std::vector<int>, int)
 DEFINE_INTERFACE_METHOD_6("CreateActiveQuery", ICmpRangeManager::tag_t, ICmpRangeManager, CreateActiveQuery, entity_id_t, entity_pos_t, entity_pos_t, std::vector<int>, int, u8)
+DEFINE_INTERFACE_METHOD_7("CreateActiveParabolicQuery", ICmpRangeManager::tag_t, ICmpRangeManager, CreateActiveParabolicQuery, entity_id_t, entity_pos_t, entity_pos_t, entity_pos_t, std::vector<int>, int, u8)
 DEFINE_INTERFACE_METHOD_1("DestroyActiveQuery", void, ICmpRangeManager, DestroyActiveQuery, ICmpRangeManager::tag_t)
 DEFINE_INTERFACE_METHOD_1("EnableActiveQuery", void, ICmpRangeManager, EnableActiveQuery, ICmpRangeManager::tag_t)
 DEFINE_INTERFACE_METHOD_1("DisableActiveQuery", void, ICmpRangeManager, DisableActiveQuery, ICmpRangeManager::tag_t)
@@ -45,6 +46,7 @@
 DEFINE_INTERFACE_METHOD_1("GetEntitiesByPlayer", std::vector<entity_id_t>, ICmpRangeManager, GetEntitiesByPlayer, player_id_t)
 DEFINE_INTERFACE_METHOD_1("SetDebugOverlay", void, ICmpRangeManager, SetDebugOverlay, bool)
 DEFINE_INTERFACE_METHOD_2("SetLosRevealAll", void, ICmpRangeManager, SetLosRevealAll, player_id_t, bool)
+DEFINE_INTERFACE_METHOD_5("GetElevationAdaptedRange", entity_pos_t, ICmpRangeManager, GetElevationAdaptedRange, CFixedVector3D, CFixedVector3D, entity_pos_t, entity_pos_t, entity_pos_t)
 DEFINE_INTERFACE_METHOD_3("GetLosVisibility", std::string, ICmpRangeManager, GetLosVisibility_wrapper, entity_id_t, player_id_t, bool)
 DEFINE_INTERFACE_METHOD_1("SetLosCircular", void, ICmpRangeManager, SetLosCircular, bool)
 DEFINE_INTERFACE_METHOD_0("GetLosCircular", bool, ICmpRangeManager, GetLosCircular)
Index: source/simulation2/components/ICmpRangeManager.h
===================================================================
--- source/simulation2/components/ICmpRangeManager.h	(revision 13491)
+++ source/simulation2/components/ICmpRangeManager.h	(working copy)
@@ -18,6 +18,8 @@
 #ifndef INCLUDED_ICMPRANGEMANAGER
 #define INCLUDED_ICMPRANGEMANAGER
 
+#include "maths/FixedVector3D.h"
+
 #include "simulation2/system/Interface.h"
 #include "simulation2/helpers/Position.h"
 #include "simulation2/helpers/Player.h"
@@ -102,7 +104,33 @@
 	virtual tag_t CreateActiveQuery(entity_id_t source,
 		entity_pos_t minRange, entity_pos_t maxRange, std::vector<int> owners, int requiredInterface, u8 flags) = 0;
 
+    /**
+	 * Construct an active query of a paraboloic form around the unit. 
+	 * The query will be disabled by default.
+	 * @param source the entity around which the range will be computed.
+	 * @param minRange non-negative minimum horizontal distance in metres (inclusive). MinRange doesn't do parabolic checks.
+	 * @param maxRange non-negative maximum distance in metres (inclusive) for units on the same elevation; 
+	 *      or -1.0 to ignore distance. 
+	 *      For units on a different elevation, a physical correct paraboloid with height=maxRange/2 above the unit is used to query them
+	 * @param elevationBonus extra bonus so the source can be placed higher and shoot further
+	 * @param owners list of player IDs that matching entities may have; -1 matches entities with no owner.
+	 * @param requiredInterface if non-zero, an interface ID that matching entities must implement.
+	 * @param flags if a entity in range has one of the flags set it will show up.
+	 * @return unique non-zero identifier of query.
+	 */
+	virtual tag_t CreateActiveParabolicQuery(entity_id_t source,
+		entity_pos_t minRange, entity_pos_t maxRange, entity_pos_t elevationBonus, std::vector<int> owners, int requiredInterface, u8 flags) = 0;
+
+
 	/**
+	 * Get the average elevation over 8 points on distance range around the entity
+	 * @param id the entity id to look around
+	 * @param range the distance to compare terrain height with
+	 * @return a fixed number representing the average difference. It's positive when the entity is on average higher than the terrain surrounding it.
+	 */
+	virtual entity_pos_t GetElevationAdaptedRange(CFixedVector3D pos, CFixedVector3D rot, entity_pos_t range, entity_pos_t elevationBonus, entity_pos_t angle) = 0;
+
+	/**
 	 * Destroy a query and clean up resources. This must be called when an entity no longer needs its
 	 * query (e.g. when the entity is destroyed).
 	 * @param tag identifier of query.
Index: source/simulation2/system/InterfaceScripted.h
===================================================================
--- source/simulation2/system/InterfaceScripted.h	(revision 13491)
+++ source/simulation2/system/InterfaceScripted.h	(working copy)
@@ -85,4 +85,10 @@
 		6, \
 		JSPROP_ENUMERATE|JSPROP_READONLY|JSPROP_PERMANENT },
 
+#define DEFINE_INTERFACE_METHOD_7(scriptname, rettype, classname, methodname, arg1, arg2, arg3, arg4, arg5, arg6, arg7) \
+	{ scriptname, \
+		ScriptInterface::callMethod<rettype, arg1, arg2, arg3, arg4, arg5, arg6, arg7, &class_##classname, classname, &classname::methodname>, \
+		7, \
+		JSPROP_ENUMERATE|JSPROP_READONLY|JSPROP_PERMANENT },
+
 #endif // INCLUDED_INTERFACE_SCRIPTED
