diff --git a/binaries/data/mods/public/gui/session/input.js b/binaries/data/mods/public/gui/session/input.js
index 8f05f67057..fec649fa5a 100644
--- a/binaries/data/mods/public/gui/session/input.js
+++ b/binaries/data/mods/public/gui/session/input.js
@@ -121,7 +121,23 @@ function updateBuildingPlacementPreview()
// 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 @@ function startBuildingPlacement(buildTemplate, playerState)
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
diff --git a/binaries/data/mods/public/gui/session/placement.js b/binaries/data/mods/public/gui/session/placement.js
index 189d765d7e..cfb1775e06 100644
--- a/binaries/data/mods/public/gui/session/placement.js
+++ b/binaries/data/mods/public/gui/session/placement.js
@@ -22,6 +22,8 @@ PlacementSupport.prototype.Reset = function()
this.SetDefaultAngle();
this.RandomizeActorSeed();
+
+ this.attack = null;
Engine.GuiInterfaceCall("SetBuildingPlacementPreview", {"template": ""});
Engine.GuiInterfaceCall("SetWallPlacementPreview", {"wallSet": null});
diff --git a/binaries/data/mods/public/gui/session/selection_details.js b/binaries/data/mods/public/gui/session/selection_details.js
index 9d1f502a12..2589bc39ab 100644
--- a/binaries/data/mods/public/gui/session/selection_details.js
+++ b/binaries/data/mods/public/gui/session/selection_details.js
@@ -206,24 +206,42 @@ function displaySingle(entState, template)
else
{
// TODO: we should require all entities to have icons, so this case never occurs
- 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
- var iconTooltip = "";
-
- if (genericName)
+ getGUIObjectByName("icon").sprite = "bkFillBlack";
+ }
+
+ // Attack and Armor
+ var type = "";
+ var attack = "[font=\"serif-bold-13\"]"+type+"Attack:[/font] " + damageTypeDetails(entState.attack);
+ if (entState.attack)
+ {
+ type = entState.attack.type + " ";
+
+ // 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)
iconTooltip = "[font=\"serif-bold-16\"]" + genericName + "[/font]";
if (template.tooltip)
diff --git a/binaries/data/mods/public/simulation/components/Attack.js b/binaries/data/mods/public/simulation/components/Attack.js
index 2ba373d51c..0272b38961 100644
--- a/binaries/data/mods/public/simulation/components/Attack.js
+++ b/binaries/data/mods/public/simulation/components/Attack.js
@@ -67,6 +67,9 @@ Attack.prototype.Schema =
"0.0" +
"44.0" +
"20.0" +
+ ""+
+ "" +
+ "" +
"800" +
"1600" +
"50.0" +
@@ -125,6 +128,9 @@ Attack.prototype.Schema =
"" +
"" +
"" +
+ ""+
+ "" +
+ "" +
"" +
"" +
"" +
@@ -360,8 +366,11 @@ Attack.prototype.GetRange = function(type)
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
diff --git a/binaries/data/mods/public/simulation/components/BuildingAI.js b/binaries/data/mods/public/simulation/components/BuildingAI.js
index 250b0f4395..c551fc0447 100644
--- a/binaries/data/mods/public/simulation/components/BuildingAI.js
+++ b/binaries/data/mods/public/simulation/components/BuildingAI.js
@@ -12,6 +12,7 @@ BuildingAI.prototype.Schema =
"" +
"";
+
/**
* Initialize BuildingAI Component
*/
@@ -99,7 +100,7 @@ BuildingAI.prototype.SetupRangeQuery = function(owner)
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 @@ BuildingAI.prototype.SetupGaiaRangeQuery = function()
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 @@ BuildingAI.prototype.FireArrows = function()
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 @@ BuildingAI.prototype.FireArrows = function()
//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 @@ BuildingAI.prototype.FireArrows = function()
}
};
+/**
+ * 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);
diff --git a/binaries/data/mods/public/simulation/components/GuiInterface.js b/binaries/data/mods/public/simulation/components/GuiInterface.js
index 907a52159f..0b93128870 100644
--- a/binaries/data/mods/public/simulation/components/GuiInterface.js
+++ b/binaries/data/mods/public/simulation/components/GuiInterface.js
@@ -174,6 +174,7 @@ GuiInterface.prototype.GetEntityState = function(player, ent)
if (cmpPosition && cmpPosition.IsInWorld())
{
ret.position = cmpPosition.GetPosition();
+ ret.rotation = cmpPosition.GetRotation();
}
var cmpHealth = Engine.QueryInterface(ent, IID_Health);
@@ -185,7 +186,10 @@ GuiInterface.prototype.GetEntityState = function(player, ent)
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?
@@ -194,6 +198,32 @@ GuiInterface.prototype.GetEntityState = function(player, ent)
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);
@@ -313,8 +343,7 @@ GuiInterface.prototype.GetEntityState = function(player, ent)
"req": cmpPromotion.GetRequiredXp()
};
}
-
- var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
+
if (cmpUnitAI)
{
ret.unitAI = {
@@ -325,7 +354,7 @@ GuiInterface.prototype.GetEntityState = function(player, ent)
if (cmpUnitAI.isGarrisoned && ret.player)
ret.template = "p" + ret.player + "&" + ret.template;
}
-
+
var cmpGate = Engine.QueryInterface(ent, IID_Gate);
if (cmpGate)
{
@@ -349,12 +378,26 @@ GuiInterface.prototype.GetEntityState = function(player, ent)
};
}
- 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;
@@ -393,6 +436,7 @@ GuiInterface.prototype.GetTemplateData = function(player, extendedName)
"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)),
};
}
}
@@ -1729,6 +1773,7 @@ var exposedFunctions = {
"GetRenamedEntities": 1,
"ClearRenamedEntities": 1,
"GetEntityState": 1,
+ "GetAverageRangeForBuildings": 1,
"GetTemplateData": 1,
"GetTechnologyData": 1,
"IsTechnologyResearched": 1,
diff --git a/binaries/data/mods/public/simulation/components/UnitAI.js b/binaries/data/mods/public/simulation/components/UnitAI.js
index e68a754028..35e5bcf79e 100644
--- a/binaries/data/mods/public/simulation/components/UnitAI.js
+++ b/binaries/data/mods/public/simulation/components/UnitAI.js
@@ -367,7 +367,7 @@ var UnitFsmSpec = {
this.order.data.attackType = type;
// If we are already at the target, try attacking it from here
- if (this.CheckTargetRange(this.order.data.target, IID_Attack, this.order.data.attackType))
+ if (this.CheckTargetAttackRange(this.order.data.target, IID_Attack, this.order.data.attackType))
{
this.StopMoving();
// For packable units within attack range:
@@ -428,7 +428,7 @@ var UnitFsmSpec = {
}
// Try to move within attack range
- if (this.MoveToTargetRange(this.order.data.target, IID_Attack, this.order.data.attackType))
+ if (this.MoveToTargetAttackRange(this.order.data.target, IID_Attack, this.order.data.attackType,0.5))
{
// We've started walking to the given point
if (this.IsAnimal())
@@ -1315,11 +1315,27 @@ var UnitFsmSpec = {
},
"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.order.data.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.order.data.attackType,0))
+ {
+ this.SetNextState("APPROACHING");
+ }
+ else
+ {
+ // Give up
+ this.FinishOrder();
+ }
+ }
},
"Attacked": function(msg) {
@@ -1335,9 +1351,9 @@ var UnitFsmSpec = {
"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.order.data.attackType))
+ if (!this.CheckTargetAttackRange(this.order.data.target, IID_Attack, this.order.data.attackType))
{
- if (this.MoveToTargetRange(this.order.data.target, IID_Attack, this.order.data.attackType))
+ if (this.MoveToTargetAttackRange(this.order.data.target, IID_Attack, this.order.data.attackType,0.5))
this.SetNextState("APPROACHING");
else
{
@@ -1403,7 +1419,7 @@ var UnitFsmSpec = {
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.order.data.attackType))
+ if (this.CheckTargetAttackRange(target, IID_Attack, this.order.data.attackType))
{
var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
this.lastAttacked = cmpTimer.GetTime() - msg.lateness;
@@ -3289,6 +3305,53 @@ UnitAI.prototype.MoveToTargetRange = function(target, iid, type)
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);
+ return cmpUnitMotion.MoveToTargetRange(target, range.min, guessedMaxRange);
+};
+
UnitAI.prototype.MoveToTargetRangeExplicit = function(target, min, max)
{
if (!this.CheckTargetVisible(target))
@@ -3313,6 +3376,43 @@ UnitAI.prototype.CheckTargetRange = function(target, iid, type)
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)
+ return false;
+
+ var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion);
+ return cmpUnitMotion.IsInTargetRange(target, range.min, Math.sqrt(maxRangeSq));
+
+ return maxRangeSq >= distanceSq && range.min*range.min <= distanceSq;
+
+};
+
UnitAI.prototype.CheckTargetRangeExplicit = function(target, min, max)
{
var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion);
diff --git a/binaries/data/mods/public/simulation/templates/template_structure_defense_defense_tower.xml b/binaries/data/mods/public/simulation/templates/template_structure_defense_defense_tower.xml
index 1fa314c5bf..fc74e7ced6 100644
--- a/binaries/data/mods/public/simulation/templates/template_structure_defense_defense_tower.xml
+++ b/binaries/data/mods/public/simulation/templates/template_structure_defense_defense_tower.xml
@@ -1,18 +1,19 @@
-
-
-
- 0.0
- 20.0
- 0.0
- 70.0
- 16.0
- 75.0
- 1200
- 2000
- 1.5
-
-
+
+
+
+ 0.0
+ 20.0
+ 0.0
+ 57.0
+ 16.0
+ 15
+ 75.0
+ 1200
+ 2000
+ 1.5
+
+
1
1
diff --git a/source/scriptinterface/ScriptInterface.h b/source/scriptinterface/ScriptInterface.h
index f97981e7a6..b6c6848948 100644
--- a/source/scriptinterface/ScriptInterface.h
+++ b/source/scriptinterface/ScriptInterface.h
@@ -38,7 +38,7 @@ class AutoGCRooter;
// 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
diff --git a/source/simulation2/components/CCmpRangeManager.cpp b/source/simulation2/components/CCmpRangeManager.cpp
index 733684583e..d5d0df7c30 100644
--- a/source/simulation2/components/CCmpRangeManager.cpp
+++ b/source/simulation2/components/CCmpRangeManager.cpp
@@ -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 lastMatch;
@@ -87,6 +91,44 @@ static u32 CalcSharedLosMask(std::vector players)
return playerMask;
}
+/**
+ * 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 outline;
+};
+
+static std::map ParabolicRangesOutlines;
+
/**
* Representation of an entity, with the data needed for queries.
*/
@@ -113,9 +155,11 @@ struct SerializeQuery
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()(serialize, "last match", value.lastMatch);
@@ -619,6 +663,16 @@ public:
return id;
}
+ virtual tag_t CreateActiveParabolicQuery(entity_id_t source,
+ entity_pos_t minRange, entity_pos_t maxRange, entity_pos_t elevationBonus,
+ std::vector 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())
@@ -842,7 +896,48 @@ public:
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 ents = m_Subdivision.GetNear(pos, q.maxRange*2);
+
+ for (size_t i = 0; i < ents.size(); ++i)
+ {
+ std::map::const_iterator it = m_EntityData.find(ents[i]);
+ ENSURE(it != m_EntityData.end());
+
+ if (!TestEntityQuery(q, it->first, it->second))
+ continue;
+
+ CmpPtr 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 ents = m_Subdivision.GetNear(pos, q.maxRange);
@@ -868,10 +963,116 @@ public:
}
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 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 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 r;
+
+
+ CmpPtr cmpTerrain(GetSimContext(), SYSTEM_ENTITY);
+ CmpPtr 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& owners, int requiredInterface, u8 flagsMask)
@@ -886,9 +1087,11 @@ public:
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)
@@ -900,11 +1103,21 @@ public:
return q;
}
+ Query ConstructParabolicQuery(entity_id_t source,
+ entity_pos_t minRange, entity_pos_t maxRange, entity_pos_t elevationBonus,
+ const std::vector& 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);
@@ -923,15 +1136,74 @@ public:
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 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 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);
}
diff --git a/source/simulation2/components/ICmpRangeManager.cpp b/source/simulation2/components/ICmpRangeManager.cpp
index b8cb7c9d59..fa1cc447fc 100644
--- a/source/simulation2/components/ICmpRangeManager.cpp
+++ b/source/simulation2/components/ICmpRangeManager.cpp
@@ -36,6 +36,7 @@ std::string ICmpRangeManager::GetLosVisibility_wrapper(entity_id_t ent, int play
BEGIN_INTERFACE_WRAPPER(RangeManager)
DEFINE_INTERFACE_METHOD_5("ExecuteQuery", std::vector, ICmpRangeManager, ExecuteQuery, entity_id_t, entity_pos_t, entity_pos_t, std::vector, int)
DEFINE_INTERFACE_METHOD_6("CreateActiveQuery", ICmpRangeManager::tag_t, ICmpRangeManager, CreateActiveQuery, entity_id_t, entity_pos_t, entity_pos_t, std::vector, 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, 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("GetEntityFlagMask", u8, ICmpRangeManager, GetEntityFl
DEFINE_INTERFACE_METHOD_1("GetEntitiesByPlayer", std::vector, 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)
diff --git a/source/simulation2/components/ICmpRangeManager.h b/source/simulation2/components/ICmpRangeManager.h
index 671c2c15e3..6592794c66 100644
--- a/source/simulation2/components/ICmpRangeManager.h
+++ b/source/simulation2/components/ICmpRangeManager.h
@@ -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,6 +104,32 @@ public:
virtual tag_t CreateActiveQuery(entity_id_t source,
entity_pos_t minRange, entity_pos_t maxRange, std::vector 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 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).
diff --git a/source/simulation2/system/InterfaceScripted.h b/source/simulation2/system/InterfaceScripted.h
index f111586542..0b3d9e847b 100644
--- a/source/simulation2/system/InterfaceScripted.h
+++ b/source/simulation2/system/InterfaceScripted.h
@@ -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, \
+ 7, \
+ JSPROP_ENUMERATE|JSPROP_READONLY|JSPROP_PERMANENT },
+
#endif // INCLUDED_INTERFACE_SCRIPTED