Give an elevation advantage to ranged units. Patch by sanderd17. Fix #1960.

This was SVN commit r13626.
This commit is contained in:
alpha123 2013-08-03 19:20:20 +00:00
parent 3b294d3ad4
commit 8c74df2acd
13 changed files with 608 additions and 60 deletions

View file

@ -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

View file

@ -22,6 +22,8 @@ PlacementSupport.prototype.Reset = function()
this.SetDefaultAngle();
this.RandomizeActorSeed();
this.attack = null;
Engine.GuiInterfaceCall("SetBuildingPlacementPreview", {"template": ""});
Engine.GuiInterfaceCall("SetWallPlacementPreview", {"wallSet": null});

View file

@ -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)

View file

@ -67,6 +67,9 @@ Attack.prototype.Schema =
"<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 @@ Attack.prototype.Schema =
"<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 @@ 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

View file

@ -12,6 +12,7 @@ BuildingAI.prototype.Schema =
"<ref name='nonNegativeDecimal'/>" +
"</element>";
/**
* 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);

View file

@ -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,

View file

@ -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);

View file

@ -1,18 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<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>
<Spread>1.5</Spread>
</Ranged>
</Attack>
<Entity parent="template_structure_defense">
<Attack>
<Ranged>
<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>
<BuildingAI>
<DefaultArrowCount>1</DefaultArrowCount>
<GarrisonArrowMultiplier>1</GarrisonArrowMultiplier>

View file

@ -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

View file

@ -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;
@ -87,6 +91,44 @@ static u32 CalcSharedLosMask(std::vector<player_id_t> 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<entity_pos_t> outline;
};
static std::map<entity_id_t, EntityParabolicRangeOutline> 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<SerializeU32_Unbounded>()(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<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())
@ -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<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);
@ -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<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)
@ -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<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);
@ -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<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);
}

View file

@ -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<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("GetEntityFlagMask", u8, ICmpRangeManager, GetEntityFl
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)

View file

@ -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<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).

View file

@ -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