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