Ticket #1960: elevation_advantage_with_gui.2.diff
File elevation_advantage_with_gui.2.diff, 39.4 KB (added by , 11 years ago) |
---|
-
binaries/data/mods/public/gui/session/input.js
121 121 // Show placement info tooltip if invalid position 122 122 placementSupport.tooltipError = !result.success; 123 123 placementSupport.tooltipMessage = result.success ? "" : result.message; 124 return result.success; 124 125 if (!result.success) 126 return false; 127 128 if (placementSupport.attack) 129 { 130 // building can be placed here, and has an attack 131 // show the range advantage in the tooltip 132 var cmd = {x: placementSupport.position.x, 133 z: placementSupport.position.z, 134 range: placementSupport.attack.maxRange, 135 elevationBonus: placementSupport.attack.elevationBonus, 136 }; 137 var averageRange = Engine.GuiInterfaceCall("GetAverageRangeForBuildings",cmd); 138 placementSupport.tooltipMessage = "Basic range: "+Math.round(cmd.range/4)+"\nAverage bonus range: "+Math.round((averageRange - cmd.range)/4); 139 } 140 return true; 125 141 } 126 142 } 127 143 else if (placementSupport.mode === "wall") … … 1494 1510 placementSupport.template = buildTemplate; 1495 1511 inputState = INPUT_BUILDING_PLACEMENT; 1496 1512 } 1513 1514 if (templateData.attack && 1515 templateData.attack.Ranged && 1516 templateData.attack.Ranged.maxRange) 1517 { 1518 // add attack information to display a good tooltip 1519 placementSupport.attack = templateData.attack.Ranged; 1520 } 1497 1521 } 1498 1522 1499 1523 // Called by GUI when user changes preferred trading goods -
binaries/data/mods/public/gui/session/placement.js
22 22 23 23 this.SetDefaultAngle(); 24 24 this.RandomizeActorSeed(); 25 26 this.attack = null; 25 27 26 28 Engine.GuiInterfaceCall("SetBuildingPlacementPreview", {"template": ""}); 27 29 Engine.GuiInterfaceCall("SetWallPlacementPreview", {"wallSet": null}); -
binaries/data/mods/public/gui/session/selection_details.js
183 183 getGUIObjectByName("icon").sprite = "bkFillBlack"; 184 184 } 185 185 186 // Attack and Armor 187 var type = ""; 188 if (entState.attack) 189 type = entState.attack.type + " "; 190 191 attack = "[font=\"serif-bold-13\"]"+type+"Attack:[/font] " + damageTypeDetails(entState.attack); 192 // Show max attack range if ranged attack, also convert to tiles (4m per tile) 193 if (entState.attack && entState.attack.type == "Ranged") 194 attack += ", [font=\"serif-bold-13\"]Range:[/font] " + Math.round(entState.attack.maxRange/4); 195 getGUIObjectByName("attackAndArmorStats").tooltip = attack + "\n[font=\"serif-bold-13\"]Armor:[/font] " + armorTypeDetails(entState.armour); 196 197 // Icon Tooltip 186 // Attack and Armor 187 var type = ""; 188 if (entState.attack) 189 { 190 type = entState.attack.type + " "; 191 192 attack = "[font=\"serif-bold-13\"]"+type+"Attack:[/font] " + damageTypeDetails(entState.attack); 193 // Show max attack range if ranged attack, also convert to tiles (4m per tile) 194 if (entState.attack.type == "Ranged") 195 { 196 var realRange = entState.attack.elevationAdaptedRange; 197 var range = entState.attack.maxRange; 198 attack += ", [font=\"serif-bold-13\"]Range:[/font] " + 199 Math.round(range/4); 200 201 if (Math.round((realRange - range)/4) > 0) 202 { 203 attack += " (+" + Math.round((realRange - range)/4) + ")"; 204 } 205 else if (Math.round((realRange - range)/4) < 0) 206 { 207 attack += " (" + Math.round((realRange - range)/4) + ")"; 208 } // don't show when it's 0 209 210 } 211 } 212 213 getGUIObjectByName("attackAndArmorStats").tooltip = attack + "\n[font=\"serif-bold-13\"]Armor:[/font] " + armorTypeDetails(entState.armour); 214 215 // Icon Tooltip 198 216 var iconTooltip = ""; 199 217 200 218 if (genericName) -
binaries/data/mods/public/simulation/components/Attack.js
67 67 "<Crush>0.0</Crush>" + 68 68 "<MaxRange>44.0</MaxRange>" + 69 69 "<MinRange>20.0</MinRange>" + 70 "<optional>"+ 71 "<element name='ElevationBonus' a:help='give an elevation advantage (in meters)'><ref name='nonNegativeDecimal'/></element>" + 72 "</optional>" + 70 73 "<PrepareTime>800</PrepareTime>" + 71 74 "<RepeatTime>1600</RepeatTime>" + 72 75 "<ProjectileSpeed>50.0</ProjectileSpeed>" + … … 125 128 "<element name='Crush' a:help='Crush damage strength'><ref name='nonNegativeDecimal'/></element>" + 126 129 "<element name='MaxRange' a:help='Maximum attack range (in metres)'><ref name='nonNegativeDecimal'/></element>" + 127 130 "<element name='MinRange' a:help='Minimum attack range (in metres)'><ref name='nonNegativeDecimal'/></element>" + 131 "<optional>"+ 132 "<element name='ElevationBonus' a:help='give an elevation advantage (in meters)'><ref name='nonNegativeDecimal'/></element>" + 133 "</optional>" + 128 134 "<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's attack animation'>" + 129 135 "<data type='nonNegativeInteger'/>" + 130 136 "</element>" + … … 360 366 361 367 var min = +(this.template[type].MinRange || 0); 362 368 min = ApplyTechModificationsToEntity("Attack/" + type + "/MinRange", min, this.entity); 369 370 var elevationBonus = +(this.template[type].ElevationBonus || 0); 371 elevationBonus = ApplyTechModificationsToEntity("Attack/" + type + "/ElevationBonus", elevationBonus, this.entity); 363 372 364 return { "max": max, "min": min 373 return { "max": max, "min": min, "elevationBonus": elevationBonus}; 365 374 }; 366 375 367 376 // Calculate the attack damage multiplier against a target -
binaries/data/mods/public/simulation/components/BuildingAI.js
12 12 "<ref name='nonNegativeDecimal'/>" + 13 13 "</element>"; 14 14 15 15 16 /** 16 17 * Initialize BuildingAI Component 17 18 */ … … 99 100 if (cmpAttack) 100 101 { 101 102 var range = cmpAttack.GetRange("Ranged"); 102 this.enemyUnitsQuery = cmpRangeManager.CreateActive Query(this.entity, range.min, range.max, players, IID_DamageReceiver, cmpRangeManager.GetEntityFlagMask("normal"));103 this.enemyUnitsQuery = cmpRangeManager.CreateActiveParabolicQuery(this.entity, range.min, range.max, range.elevationBonus, players, IID_DamageReceiver, cmpRangeManager.GetEntityFlagMask("normal")); 103 104 cmpRangeManager.EnableActiveQuery(this.enemyUnitsQuery); 104 105 } 105 106 }; … … 133 134 var range = cmpAttack.GetRange("Ranged"); 134 135 135 136 // This query is only interested in Gaia entities that can attack. 136 this.gaiaUnitsQuery = rangeMan.CreateActive Query(this.entity, range.min, range.max, [0], IID_Attack, rangeMan.GetEntityFlagMask("normal"));137 this.gaiaUnitsQuery = rangeMan.CreateActiveParabolicQuery(this.entity, range.min, range.max, range.elevationBonus, [0], IID_Attack, rangeMan.GetEntityFlagMask("normal")); 137 138 rangeMan.EnableActiveQuery(this.gaiaUnitsQuery); 138 139 } 139 140 }; … … 214 215 var cmpAttack = Engine.QueryInterface(this.entity, IID_Attack); 215 216 if (cmpAttack) 216 217 { 218 217 219 var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); 218 220 this.timer = cmpTimer.SetTimeout(this.entity, IID_BuildingAI, "FireArrows", timerInterval, {}); 219 221 var arrowsToFire = 0; … … 239 241 //Fire N arrows, 0 <= N <= Number of arrows left 240 242 arrowsToFire = Math.floor(Math.random() * this.arrowsLeft); 241 243 } 244 242 245 if (this.targetUnits.length > 0) 243 246 { 247 var clonedTargets = this.targetUnits.slice(); 244 248 for (var i = 0;i < arrowsToFire;i++) 245 249 { 246 cmpAttack.PerformAttack("Ranged", this.targetUnits[Math.floor(Math.random() * this.targetUnits.length)]); 247 PlaySound("arrowfly", this.entity); 250 var target = clonedTargets[Math.floor(Math.random() * this.targetUnits.length)]; 251 if ( 252 target && 253 this.CheckTargetVisible(target) 254 ) 255 { 256 cmpAttack.PerformAttack("Ranged", target); 257 PlaySound("arrowfly", this.entity); 258 259 } 260 else 261 { 262 clonedTargets.splice(clonedTargets.indexOf(target),1); 263 i--; // one extra arrow left to fire 264 if(clonedTargets.length < 1) 265 { 266 this.arrowsLeft += arrowsToFire; 267 // no targets found in this round, save arrows and go to next round 268 break; 269 } 270 } 248 271 } 249 272 this.arrowsLeft -= arrowsToFire; 250 273 } … … 252 275 } 253 276 }; 254 277 278 /** 279 * Returns true if the target entity is visible through the FoW/SoD. 280 */ 281 BuildingAI.prototype.CheckTargetVisible = function(target) 282 { 283 var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership); 284 if (!cmpOwnership) 285 return false; 286 287 var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); 288 289 if (cmpRangeManager.GetLosVisibility(target, cmpOwnership.GetOwner(), false) == "hidden") 290 return false; 291 292 // Either visible directly, or visible in fog 293 return true; 294 }; 295 255 296 Engine.RegisterComponentType(IID_BuildingAI, "BuildingAI", BuildingAI); -
binaries/data/mods/public/simulation/components/GuiInterface.js
173 173 if (cmpPosition && cmpPosition.IsInWorld()) 174 174 { 175 175 ret.position = cmpPosition.GetPosition(); 176 ret.rotation = cmpPosition.GetRotation(); 176 177 } 177 178 178 179 var cmpHealth = Engine.QueryInterface(ent, IID_Health); … … 184 185 ret.needsHeal = !cmpHealth.IsUnhealable(); 185 186 } 186 187 188 var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI); 189 var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); 187 190 var cmpAttack = Engine.QueryInterface(ent, IID_Attack); 191 188 192 if (cmpAttack) 189 193 { 190 194 var type = cmpAttack.GetBestAttack(); // TODO: how should we decide which attack to show? show all? … … 193 197 ret.attack.type = type; 194 198 ret.attack.minRange = range.min; 195 199 ret.attack.maxRange = range.max; 200 if (type == "Ranged") 201 { 202 ret.attack.elevationBonus = range.elevationBonus; 203 if (cmpUnitAI && cmpPosition && cmpPosition.IsInWorld()) 204 { 205 // For units, take the rage in front of it, no spread. So angle = 0 206 ret.attack.elevationAdaptedRange = cmpRangeManager.GetElevationAdaptedRange(ret.position, ret.rotation, range.max, range.elevationBonus, 0); 207 } 208 else if(cmpPosition && cmpPosition.IsInWorld()) 209 { 210 // For buildings, take the average elevation around it. So angle = 2*pi 211 ret.attack.elevationAdaptedRange = cmpRangeManager.GetElevationAdaptedRange(ret.position, ret.rotation, range.max, range.elevationBonus, 2*Math.PI); 212 } 213 else 214 { 215 // not in world, set a default? 216 ret.attack.elevationAdaptedRange = ret.attack.maxRange; 217 } 218 219 } 220 else 221 { 222 // not a ranged attack, set some defaults 223 ret.attack.elevationBonus = 0; 224 ret.attack.elevationAdaptedRange = ret.attack.maxRange; 225 } 196 226 } 197 227 198 228 var cmpArmour = Engine.QueryInterface(ent, IID_DamageReceiver); … … 310 340 "req": cmpPromotion.GetRequiredXp() 311 341 }; 312 342 } 313 314 var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI); 343 315 344 if (cmpUnitAI) 316 345 { 317 346 ret.unitAI = { … … 322 351 if (cmpUnitAI.isGarrisoned && ret.player) 323 352 ret.template = "p" + ret.player + "&" + ret.template; 324 353 } 325 354 326 355 var cmpGate = Engine.QueryInterface(ent, IID_Gate); 327 356 if (cmpGate) 328 357 { … … 346 375 }; 347 376 } 348 377 349 var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);350 378 ret.visibility = cmpRangeManager.GetLosVisibility(ent, player, false); 351 379 352 380 return ret; 353 381 }; 354 382 383 GuiInterface.prototype.GetAverageRangeForBuildings = function(player, cmd) 384 { 385 var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); 386 var cmpTerrain = Engine.QueryInterface(SYSTEM_ENTITY, IID_Terrain); 387 var rot = {x:0, y:0, z:0}; 388 var pos = {x:cmd.x,z:cmd.z}; 389 pos.y = cmpTerrain.GetGroundLevel(cmd.x, cmd.z); 390 var elevationBonus = cmd.elevationBonus || 0; 391 var range = cmd.range; 392 393 return cmpRangeManager.GetElevationAdaptedRange(pos, rot, range, elevationBonus, 2*Math.PI); 394 }; 395 396 397 355 398 GuiInterface.prototype.GetTemplateData = function(player, extendedName) 356 399 { 357 400 var name = extendedName; … … 390 433 "crush": GetTechModifiedProperty(techMods, template, "Attack/"+type+"/Crush", +(template.Attack[type].Crush || 0)), 391 434 "minRange": GetTechModifiedProperty(techMods, template, "Attack/"+type+"/MinRange", +(template.Attack[type].MinRange || 0)), 392 435 "maxRange": GetTechModifiedProperty(techMods, template, "Attack/"+type+"/MaxRange", +template.Attack[type].MaxRange), 436 "elevationBonus": GetTechModifiedProperty(techMods, template, "Attack/"+type+"/ElevationBonus", +(template.Attack[type].ElevationBonus || 0)), 393 437 }; 394 438 } 395 439 } … … 1726 1770 "GetRenamedEntities": 1, 1727 1771 "ClearRenamedEntities": 1, 1728 1772 "GetEntityState": 1, 1773 "GetAverageRangeForBuildings": 1, 1729 1774 "GetTemplateData": 1, 1730 1775 "GetTechnologyData": 1, 1731 1776 "IsTechnologyResearched": 1, -
binaries/data/mods/public/simulation/components/UnitAI.js
367 367 this.attackType = type; 368 368 369 369 // If we are already at the target, try attacking it from here 370 if (this.CheckTarget Range(this.order.data.target, IID_Attack, this.attackType))370 if (this.CheckTargetAttackRange(this.order.data.target, IID_Attack, this.attackType)) 371 371 { 372 372 this.StopMoving(); 373 373 // For packable units within attack range: … … 427 427 return; 428 428 } 429 429 430 // Try to move within attack range 431 if (this.MoveToTarget Range(this.order.data.target, IID_Attack, this.attackType))430 // Try to move within attack range 431 if (this.MoveToTargetAttackRange(this.order.data.target, IID_Attack, this.attackType,0.5)) 432 432 { 433 433 // We've started walking to the given point 434 434 if (this.IsAnimal()) … … 1314 1314 }, 1315 1315 1316 1316 "MoveCompleted": function() { 1317 // If the unit needs to unpack, do so 1318 if (this.CanUnpack()) 1319 this.SetNextState("UNPACKING"); 1320 else 1321 this.SetNextState("ATTACKING"); 1317 1318 if (this.CheckTargetAttackRange(this.order.data.target, IID_Attack , this.attackType)) 1319 { 1320 // If the unit needs to unpack, do so 1321 if (this.CanUnpack()) 1322 this.SetNextState("UNPACKING"); 1323 else 1324 this.SetNextState("ATTACKING"); 1325 } 1326 else 1327 { 1328 if (this.MoveToTargetAttackRange(this.order.data.target, IID_Attack, this.attackType,0)) 1329 { 1330 this.SetNextState("APPROACHING"); 1331 } 1332 else 1333 { 1334 // Give up 1335 this.FinishOrder(); 1336 } 1337 } 1322 1338 }, 1323 1339 1324 1340 "Attacked": function(msg) { … … 1334 1350 "UNPACKING": { 1335 1351 "enter": function() { 1336 1352 // If we're not in range yet (maybe we stopped moving), move to target again 1337 if (!this.CheckTarget Range(this.order.data.target, IID_Attack, this.attackType))1353 if (!this.CheckTargetAttackRange(this.order.data.target, IID_Attack, this.attackType)) 1338 1354 { 1339 if (this.MoveToTarget Range(this.order.data.target, IID_Attack, this.attackType))1355 if (this.MoveToTargetAttackRange(this.order.data.target, IID_Attack, this.attackType,0)) 1340 1356 this.SetNextState("APPROACHING"); 1341 1357 else 1342 1358 { … … 1402 1418 if (this.TargetIsAlive(target) && this.CanAttack(target, this.order.data.forceResponse || null)) 1403 1419 { 1404 1420 // Check we can still reach the target 1405 if (this.CheckTarget Range(target, IID_Attack, this.attackType))1421 if (this.CheckTargetAttackRange(target, IID_Attack, this.attackType)) 1406 1422 { 1407 1423 var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); 1408 1424 this.lastAttacked = cmpTimer.GetTime() - msg.lateness; … … 3287 3303 return cmpUnitMotion.MoveToTargetRange(target, range.min, range.max); 3288 3304 }; 3289 3305 3306 /** 3307 * Move unit so we hope the target is in the attack range 3308 * for melee attacks, this goes straight to the default range checks 3309 * for ranged attacks, the parabolic range is used, so we can't know exactly at what horizontal range the target can be reached 3310 * That's why a guess is needed 3311 * a guess of 1 will take the maximum of the possible ranges, and stay far away 3312 * a guess of 0 will take the minimum of the possible ranges and, in most cases, will have the target in range. 3313 * every guess inbetween is a linear interpollation 3314 */ 3315 UnitAI.prototype.MoveToTargetAttackRange = function(target, iid, type,guess) 3316 { 3317 3318 if(type!= "Ranged") { 3319 return this.MoveToTargetRange(target, iid, type); 3320 } 3321 if (!this.CheckTargetVisible(target)) { 3322 return false; 3323 } 3324 var cmpRanged = Engine.QueryInterface(this.entity, iid); 3325 var range = cmpRanged.GetRange(type); 3326 3327 var thisCmpPosition = Engine.QueryInterface(this.entity, IID_Position); 3328 var s = thisCmpPosition.GetPosition(); 3329 3330 var targetCmpPosition = Engine.QueryInterface(target, IID_Position); 3331 if(!targetCmpPosition.IsInWorld()) 3332 return false; 3333 3334 var t = targetCmpPosition.GetPosition(); 3335 // h is positive when I'm higher than the target 3336 var h = s.y-t.y+range.elevationBonus; 3337 3338 // No negative roots please 3339 if(h>-range.max/2) { 3340 var parabolicMaxRange = Math.sqrt(range.max*range.max+2*range.max*h); 3341 } else { 3342 // return false? Or hope you come close enough? 3343 var parabolicMaxRange = 0; 3344 //return false; 3345 } 3346 // the parabole changes while walking, take something in the middle 3347 var guessedMaxRange = Math.max(range.max, parabolicMaxRange)*guess+Math.min(range.max, parabolicMaxRange)*(1-guess) ; 3348 3349 var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion); 3350 var r = cmpUnitMotion.MoveToTargetRange(target, range.min, guessedMaxRange); 3351 return r; 3352 }; 3353 3290 3354 UnitAI.prototype.MoveToTargetRangeExplicit = function(target, min, max) 3291 3355 { 3292 3356 if (!this.CheckTargetVisible(target)) … … 3311 3375 return cmpUnitMotion.IsInTargetRange(target, range.min, range.max); 3312 3376 }; 3313 3377 3378 /** 3379 * Check if the target is inside the attack range 3380 * For melee attacks, this goes straigt to the regular range calculation 3381 * For ranged attacks, the parabolic formula is used to accout for bigger ranges 3382 * when the target is lower, and smaller ranges when the target is higher 3383 */ 3384 UnitAI.prototype.CheckTargetAttackRange = function(target, iid, type) 3385 { 3386 3387 if (type != "Ranged") 3388 { 3389 return this.CheckTargetRange(target,iid,type); 3390 } 3391 3392 var targetCmpPosition = Engine.QueryInterface(target, IID_Position); 3393 if (!targetCmpPosition || !targetCmpPosition.IsInWorld()) 3394 { 3395 return false; 3396 } 3397 3398 var cmpRanged = Engine.QueryInterface(this.entity, iid); 3399 var range = cmpRanged.GetRange(type); 3400 3401 var thisCmpPosition = Engine.QueryInterface(this.entity, IID_Position); 3402 var s = thisCmpPosition.GetPosition(); 3403 3404 var t = targetCmpPosition.GetPosition(); 3405 3406 var h = s.y-t.y+range.elevationBonus; 3407 var maxRangeSq = 2*range.max*(h + range.max/2); 3408 if (maxRangeSq<0) 3409 { 3410 // certainly outside the range, the target is even too high to reach. 3411 return false; 3412 } 3413 3414 // use native range checking function to take account of target shape 3415 // in case of big targets 3416 return this.CheckTargetRangeExplicit(target,range.min,Math.sqrt(maxRangeSq)); 3417 }; 3418 3314 3419 UnitAI.prototype.CheckTargetRangeExplicit = function(target, min, max) 3315 3420 { 3316 3421 var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion); -
binaries/data/mods/public/simulation/templates/template_structure_defense_defense_tower.xml
2 2 <Entity parent="template_structure_defense"> 3 3 <Attack> 4 4 <Ranged> 5 <Hack>0.0</Hack> 6 <Pierce>20.0</Pierce> 7 <Crush>0.0</Crush> 8 <MaxRange>70.0</MaxRange> 9 <MinRange>16.0</MinRange> 10 <ProjectileSpeed>75.0</ProjectileSpeed> 11 <PrepareTime>1200</PrepareTime> 12 <RepeatTime>2000</RepeatTime> 5 <Hack>0.0</Hack> 6 <Pierce>20.0</Pierce> 7 <Crush>0.0</Crush> 8 <MaxRange>57.0</MaxRange> 9 <MinRange>16.0</MinRange> 10 <ElevationBonus>15</ElevationBonus> 11 <ProjectileSpeed>75.0</ProjectileSpeed> 12 <PrepareTime>1200</PrepareTime> 13 <RepeatTime>2000</RepeatTime> 13 14 <Spread>1.5</Spread> 14 15 </Ranged> 15 16 </Attack> -
source/scriptinterface/ScriptInterface.h
38 38 // Set the maximum number of function arguments that can be handled 39 39 // (This should be as small as possible (for compiler efficiency), 40 40 // but as large as necessary for all wrapped functions) 41 #define SCRIPT_INTERFACE_MAX_ARGS 741 #define SCRIPT_INTERFACE_MAX_ARGS 8 42 42 43 43 // TODO: what's a good default? 44 44 #define DEFAULT_RUNTIME_SIZE 16 * 1024 * 1024 -
source/simulation2/components/CCmpRangeManager.cpp
20 20 #include "simulation2/system/Component.h" 21 21 #include "ICmpRangeManager.h" 22 22 23 #include "ICmpTerrain.h" 23 24 #include "simulation2/MessageTypes.h" 24 25 #include "simulation2/components/ICmpPosition.h" 25 26 #include "simulation2/components/ICmpTerritoryManager.h" 26 27 #include "simulation2/components/ICmpVision.h" 28 #include "simulation2/components/ICmpWaterManager.h" 27 29 #include "simulation2/helpers/Render.h" 28 30 #include "simulation2/helpers/Spatial.h" 29 31 … … 44 46 struct Query 45 47 { 46 48 bool enabled; 49 bool parabolic; 47 50 entity_id_t source; 48 51 entity_pos_t minRange; 49 52 entity_pos_t maxRange; 53 entity_pos_t elevationBonus; 50 54 u32 ownersMask; 51 55 i32 interface; 52 56 std::vector<entity_id_t> lastMatch; … … 88 92 } 89 93 90 94 /** 95 * Checks whether v is in a parabolic range of (0,0,0) 96 * The highest point of the paraboloid is (0,range/2,0) 97 * and the circle of distance 'range' around (0,0,0) on height y=0 is part of the paraboloid 98 * 99 * Avoids sqrting and overflowing. 100 */ 101 static bool InParabolicRange(CFixedVector3D v, fixed range) 102 { 103 i64 x = (i64)v.X.GetInternalValue(); // abs(x) <= 2^31 104 i64 z = (i64)v.Z.GetInternalValue(); 105 i64 xx = (x * x); // xx <= 2^62 106 i64 zz = (z * z); 107 i64 d2 = (xx + zz) >> 1; // d2 <= 2^62 (no overflow) 108 109 i64 y = (i64)v.Y.GetInternalValue(); 110 111 i64 c = (i64)range.GetInternalValue(); 112 i64 c_2 = c >> 1; 113 114 i64 c2 = (c_2-y)*c; 115 116 if (d2 <= c2) 117 return true; 118 119 return false; 120 } 121 122 struct EntityParabolicRangeOutline 123 { 124 entity_id_t source; 125 CFixedVector3D position; 126 entity_pos_t range; 127 std::vector<entity_pos_t> outline; 128 }; 129 130 static std::map<entity_id_t, EntityParabolicRangeOutline> ParabolicRangesOutlines; 131 132 /** 91 133 * Representation of an entity, with the data needed for queries. 92 134 */ 93 135 struct EntityData … … 113 155 void operator()(S& serialize, const char* UNUSED(name), Query& value) 114 156 { 115 157 serialize.Bool("enabled", value.enabled); 158 serialize.Bool("parabolic",value.parabolic); 116 159 serialize.NumberU32_Unbounded("source", value.source); 117 160 serialize.NumberFixed_Unbounded("min range", value.minRange); 118 161 serialize.NumberFixed_Unbounded("max range", value.maxRange); 162 serialize.NumberFixed_Unbounded("elevation bonus", value.elevationBonus); 119 163 serialize.NumberU32_Unbounded("owners mask", value.ownersMask); 120 164 serialize.NumberI32_Unbounded("interface", value.interface); 121 165 SerializeVector<SerializeU32_Unbounded>()(serialize, "last match", value.lastMatch); … … 589 633 return id; 590 634 } 591 635 636 virtual tag_t CreateActiveParabolicQuery(entity_id_t source, 637 entity_pos_t minRange, entity_pos_t maxRange, entity_pos_t elevationBonus, 638 std::vector<int> owners, int requiredInterface, u8 flags) 639 { 640 tag_t id = m_QueryNext++; 641 m_Queries[id] = ConstructParabolicQuery(source, minRange, maxRange, elevationBonus, owners, requiredInterface, flags); 642 643 return id; 644 } 645 592 646 virtual void DestroyActiveQuery(tag_t tag) 593 647 { 594 648 if (m_Queries.find(tag) == m_Queries.end()) … … 812 866 r.push_back(it->first); 813 867 } 814 868 } 815 else 869 // Not the entire world, so check a parabolic range, or a regular range 870 else if (q.parabolic) 816 871 { 872 // elevationBonus is part of the 3D position, as the source is really that much heigher 873 CFixedVector3D pos3d = cmpSourcePosition->GetPosition()+ 874 CFixedVector3D(entity_pos_t::Zero(), q.elevationBonus, entity_pos_t::Zero()) ; 875 // Get a quick list of entities that are potentially in range, with a cutoff of 2*maxRange 876 std::vector<entity_id_t> ents = m_Subdivision.GetNear(pos, q.maxRange*2); 877 878 for (size_t i = 0; i < ents.size(); ++i) 879 { 880 std::map<entity_id_t, EntityData>::const_iterator it = m_EntityData.find(ents[i]); 881 ENSURE(it != m_EntityData.end()); 882 883 if (!TestEntityQuery(q, it->first, it->second)) 884 continue; 885 886 CmpPtr<ICmpPosition> cmpSecondPosition(GetSimContext(), ents[i]); 887 if (!cmpSecondPosition || !cmpSecondPosition->IsInWorld()) 888 continue; 889 CFixedVector3D secondPosition = cmpSecondPosition->GetPosition(); 890 891 // Restrict based on precise distance 892 if (!InParabolicRange( 893 CFixedVector3D(it->second.x, secondPosition.Y, it->second.z) 894 - pos3d, 895 q.maxRange)) 896 continue; 897 898 if (!q.minRange.IsZero()) 899 { 900 int distVsMin = (CFixedVector2D(it->second.x, it->second.z) - pos).CompareLength(q.minRange); 901 if (distVsMin < 0) 902 continue; 903 } 904 905 r.push_back(it->first); 906 907 } 908 } 909 // check a regular range (i.e. not the entire world, and not parabolic) 910 else 911 { 817 912 // Get a quick list of entities that are potentially in range 818 913 std::vector<entity_id_t> ents = m_Subdivision.GetNear(pos, q.maxRange); 819 914 … … 838 933 } 839 934 840 935 r.push_back(it->first); 936 841 937 } 842 938 } 843 939 } 844 940 941 942 virtual entity_pos_t GetElevationAdaptedRange(CFixedVector3D pos, CFixedVector3D rot, entity_pos_t range, entity_pos_t elevationBonus, entity_pos_t angle) 943 { 944 entity_pos_t r = entity_pos_t::Zero() ; 945 946 pos.Y += elevationBonus; 947 entity_pos_t orientation = rot.Y; 948 949 entity_pos_t maxAngle = orientation + angle/2; 950 entity_pos_t minAngle = orientation - angle/2; 951 952 int numberOfSteps = 16; 953 954 if (angle == entity_pos_t::Zero()) 955 numberOfSteps = 1; 956 957 std::vector<entity_pos_t> coords = getParabolicRangeForm(pos, range, range*2, minAngle, maxAngle, numberOfSteps); 958 959 entity_pos_t part = entity_pos_t::FromInt(numberOfSteps); 960 961 for (int i = 0; i < numberOfSteps; i++) 962 { 963 r = r + CFixedVector2D(coords[2*i],coords[2*i+1]).Length() / part; 964 } 965 966 return r; 967 968 } 969 970 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) 971 { 972 973 // angle = 0 goes in the positive Z direction 974 entity_pos_t precision = entity_pos_t::FromInt((int)TERRAIN_TILE_SIZE)/8; 975 976 std::vector<entity_pos_t> r; 977 978 979 CmpPtr<ICmpTerrain> cmpTerrain(GetSimContext(), SYSTEM_ENTITY); 980 CmpPtr<ICmpWaterManager> cmpWaterManager(GetSimContext(), SYSTEM_ENTITY); 981 entity_pos_t waterLevel = cmpWaterManager->GetWaterLevel(pos.X,pos.Z); 982 entity_pos_t thisHeight = pos.Y > waterLevel ? pos.Y : waterLevel; 983 984 if (cmpTerrain) 985 { 986 for (int i = 0; i < numberOfSteps; i++) 987 { 988 entity_pos_t angle = minAngle + (maxAngle - minAngle) / numberOfSteps * i; 989 entity_pos_t sin; 990 entity_pos_t cos; 991 entity_pos_t minDistance = entity_pos_t::Zero(); 992 entity_pos_t maxDistance = cutoff; 993 sincos_approx(angle,sin,cos); 994 995 CFixedVector2D minVector = CFixedVector2D(entity_pos_t::Zero(),entity_pos_t::Zero()); 996 CFixedVector2D maxVector = CFixedVector2D(sin,cos).Multiply(cutoff); 997 entity_pos_t targetHeight = cmpTerrain->GetGroundLevel(pos.X+maxVector.X,pos.Z+maxVector.Y); 998 // use water level to display range on water 999 targetHeight = targetHeight > waterLevel ? targetHeight : waterLevel; 1000 1001 if (InParabolicRange(CFixedVector3D(maxVector.X,targetHeight-thisHeight,maxVector.Y),maxRange)) 1002 { 1003 r.push_back(maxVector.X); 1004 r.push_back(maxVector.Y); 1005 continue; 1006 } 1007 1008 // Loop until vectors come close enough 1009 while ((maxVector - minVector).CompareLength(precision) > 0) 1010 { 1011 // difference still bigger than precision, bisect to get smaller difference 1012 entity_pos_t newDistance = (minDistance+maxDistance)/entity_pos_t::FromInt(2); 1013 1014 CFixedVector2D newVector = CFixedVector2D(sin,cos).Multiply(newDistance); 1015 1016 // get the height of the ground 1017 targetHeight = cmpTerrain->GetGroundLevel(pos.X+newVector.X,pos.Z+newVector.Y); 1018 targetHeight = targetHeight > waterLevel ? targetHeight : waterLevel; 1019 1020 if (InParabolicRange(CFixedVector3D(newVector.X,targetHeight-thisHeight,newVector.Y),maxRange)) 1021 { 1022 // new vector is in parabolic range, so this is a new minVector 1023 minVector = newVector; 1024 minDistance = newDistance; 1025 } 1026 else 1027 { 1028 // new vector is out parabolic range, so this is a new maxVector 1029 maxVector = newVector; 1030 maxDistance = newDistance; 1031 } 1032 1033 } 1034 r.push_back(maxVector.X); 1035 r.push_back(maxVector.Y); 1036 1037 } 1038 r.push_back(r[0]); 1039 r.push_back(r[1]); 1040 1041 } 1042 return r; 1043 1044 } 1045 845 1046 Query ConstructQuery(entity_id_t source, 846 1047 entity_pos_t minRange, entity_pos_t maxRange, 847 1048 const std::vector<int>& owners, int requiredInterface, u8 flagsMask) … … 856 1057 857 1058 Query q; 858 1059 q.enabled = false; 1060 q.parabolic = false; 859 1061 q.source = source; 860 1062 q.minRange = minRange; 861 1063 q.maxRange = maxRange; 1064 q.elevationBonus = entity_pos_t::Zero(); 862 1065 863 1066 q.ownersMask = 0; 864 1067 for (size_t i = 0; i < owners.size(); ++i) … … 870 1073 return q; 871 1074 } 872 1075 1076 Query ConstructParabolicQuery(entity_id_t source, 1077 entity_pos_t minRange, entity_pos_t maxRange, entity_pos_t elevationBonus, 1078 const std::vector<int>& owners, int requiredInterface, u8 flagsMask) 1079 { 1080 Query q = ConstructQuery(source,minRange,maxRange,owners,requiredInterface,flagsMask); 1081 q.parabolic = true; 1082 q.elevationBonus = elevationBonus; 1083 return q; 1084 } 1085 1086 873 1087 void RenderSubmit(SceneCollector& collector) 874 1088 { 875 1089 if (!m_DebugOverlayEnabled) 876 1090 return; 877 878 1091 CColor enabledRingColour(0, 1, 0, 1); 879 1092 CColor disabledRingColour(1, 0, 0, 1); 880 1093 CColor rayColour(1, 1, 0, 0.2f); … … 893 1106 CFixedVector2D pos = cmpSourcePosition->GetPosition2D(); 894 1107 895 1108 // Draw the max range circle 896 m_DebugOverlayLines.push_back(SOverlayLine()); 897 m_DebugOverlayLines.back().m_Color = (q.enabled ? enabledRingColour : disabledRingColour); 898 SimRender::ConstructCircleOnGround(GetSimContext(), pos.X.ToFloat(), pos.Y.ToFloat(), q.maxRange.ToFloat(), m_DebugOverlayLines.back(), true); 1109 if (!q.parabolic) 1110 { 1111 m_DebugOverlayLines.push_back(SOverlayLine()); 1112 m_DebugOverlayLines.back().m_Color = (q.enabled ? enabledRingColour : disabledRingColour); 1113 SimRender::ConstructCircleOnGround(GetSimContext(), pos.X.ToFloat(), pos.Y.ToFloat(), q.maxRange.ToFloat(), m_DebugOverlayLines.back(), true); 1114 } 1115 else 1116 { 1117 // elevation bonus is part of the 3D position. As if the unit is really that much higher 1118 CFixedVector3D pos = cmpSourcePosition->GetPosition(); 1119 pos.Y += q.elevationBonus; 899 1120 1121 std::vector<entity_pos_t> coords; 1122 1123 // Get the outline from cache if possible 1124 if (ParabolicRangesOutlines.find(q.source) != ParabolicRangesOutlines.end()) 1125 { 1126 EntityParabolicRangeOutline e = ParabolicRangesOutlines[q.source]; 1127 if (e.position == pos && e.range == q.maxRange) 1128 { 1129 // outline is cached correctly, use it 1130 coords = e.outline; 1131 } 1132 else 1133 { 1134 // outline was cached, but important parameters changed 1135 // (position, elevation, range) 1136 // update it 1137 coords = getParabolicRangeForm(pos,q.maxRange,q.maxRange*2, entity_pos_t::Zero(), entity_pos_t::FromFloat(2.0f*3.14f),70); 1138 e.outline = coords; 1139 e.range = q.maxRange; 1140 e.position = pos; 1141 ParabolicRangesOutlines[q.source] = e; 1142 } 1143 } 1144 else 1145 { 1146 // outline wasn't cached (first time you enable the range overlay 1147 // or you created a new entiy) 1148 // cache a new outline 1149 coords = getParabolicRangeForm(pos,q.maxRange,q.maxRange*2, entity_pos_t::Zero(), entity_pos_t::FromFloat(2.0f*3.14f),70); 1150 EntityParabolicRangeOutline e; 1151 e.source = q.source; 1152 e.range = q.maxRange; 1153 e.position = pos; 1154 e.outline = coords; 1155 ParabolicRangesOutlines[q.source] = e; 1156 } 1157 1158 CColor thiscolor = q.enabled ? enabledRingColour : disabledRingColour; 1159 1160 // draw the outline (piece by piece) 1161 for (size_t i = 3; i < coords.size(); i += 2) 1162 { 1163 std::vector<float> c; 1164 c.push_back((coords[i-3]+pos.X).ToFloat()); 1165 c.push_back((coords[i-2]+pos.Z).ToFloat()); 1166 c.push_back((coords[i-1]+pos.X).ToFloat()); 1167 c.push_back((coords[i]+pos.Z).ToFloat()); 1168 m_DebugOverlayLines.push_back(SOverlayLine()); 1169 m_DebugOverlayLines.back().m_Color = thiscolor; 1170 SimRender::ConstructLineOnGround(GetSimContext(), c, m_DebugOverlayLines.back(), true); 1171 } 1172 } 1173 900 1174 // Draw the min range circle 901 1175 if (!q.minRange.IsZero()) 902 1176 { 903 m_DebugOverlayLines.push_back(SOverlayLine());904 m_DebugOverlayLines.back().m_Color = (q.enabled ? enabledRingColour : disabledRingColour);905 1177 SimRender::ConstructCircleOnGround(GetSimContext(), pos.X.ToFloat(), pos.Y.ToFloat(), q.minRange.ToFloat(), m_DebugOverlayLines.back(), true); 906 1178 } 907 1179 -
source/simulation2/components/ICmpRangeManager.cpp
36 36 BEGIN_INTERFACE_WRAPPER(RangeManager) 37 37 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) 38 38 DEFINE_INTERFACE_METHOD_6("CreateActiveQuery", ICmpRangeManager::tag_t, ICmpRangeManager, CreateActiveQuery, entity_id_t, entity_pos_t, entity_pos_t, std::vector<int>, int, u8) 39 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) 39 40 DEFINE_INTERFACE_METHOD_1("DestroyActiveQuery", void, ICmpRangeManager, DestroyActiveQuery, ICmpRangeManager::tag_t) 40 41 DEFINE_INTERFACE_METHOD_1("EnableActiveQuery", void, ICmpRangeManager, EnableActiveQuery, ICmpRangeManager::tag_t) 41 42 DEFINE_INTERFACE_METHOD_1("DisableActiveQuery", void, ICmpRangeManager, DisableActiveQuery, ICmpRangeManager::tag_t) … … 45 46 DEFINE_INTERFACE_METHOD_1("GetEntitiesByPlayer", std::vector<entity_id_t>, ICmpRangeManager, GetEntitiesByPlayer, player_id_t) 46 47 DEFINE_INTERFACE_METHOD_1("SetDebugOverlay", void, ICmpRangeManager, SetDebugOverlay, bool) 47 48 DEFINE_INTERFACE_METHOD_2("SetLosRevealAll", void, ICmpRangeManager, SetLosRevealAll, player_id_t, bool) 49 DEFINE_INTERFACE_METHOD_5("GetElevationAdaptedRange", entity_pos_t, ICmpRangeManager, GetElevationAdaptedRange, CFixedVector3D, CFixedVector3D, entity_pos_t, entity_pos_t, entity_pos_t) 48 50 DEFINE_INTERFACE_METHOD_3("GetLosVisibility", std::string, ICmpRangeManager, GetLosVisibility_wrapper, entity_id_t, player_id_t, bool) 49 51 DEFINE_INTERFACE_METHOD_1("SetLosCircular", void, ICmpRangeManager, SetLosCircular, bool) 50 52 DEFINE_INTERFACE_METHOD_0("GetLosCircular", bool, ICmpRangeManager, GetLosCircular) -
source/simulation2/components/ICmpRangeManager.h
18 18 #ifndef INCLUDED_ICMPRANGEMANAGER 19 19 #define INCLUDED_ICMPRANGEMANAGER 20 20 21 #include "maths/FixedVector3D.h" 22 21 23 #include "simulation2/system/Interface.h" 22 24 #include "simulation2/helpers/Position.h" 23 25 #include "simulation2/helpers/Player.h" … … 102 104 virtual tag_t CreateActiveQuery(entity_id_t source, 103 105 entity_pos_t minRange, entity_pos_t maxRange, std::vector<int> owners, int requiredInterface, u8 flags) = 0; 104 106 107 /** 108 * Construct an active query of a paraboloic form around the unit. 109 * The query will be disabled by default. 110 * @param source the entity around which the range will be computed. 111 * @param minRange non-negative minimum horizontal distance in metres (inclusive). MinRange doesn't do parabolic checks. 112 * @param maxRange non-negative maximum distance in metres (inclusive) for units on the same elevation; 113 * or -1.0 to ignore distance. 114 * For units on a different elevation, a physical correct paraboloid with height=maxRange/2 above the unit is used to query them 115 * @param elevationBonus extra bonus so the source can be placed higher and shoot further 116 * @param owners list of player IDs that matching entities may have; -1 matches entities with no owner. 117 * @param requiredInterface if non-zero, an interface ID that matching entities must implement. 118 * @param flags if a entity in range has one of the flags set it will show up. 119 * @return unique non-zero identifier of query. 120 */ 121 virtual tag_t CreateActiveParabolicQuery(entity_id_t source, 122 entity_pos_t minRange, entity_pos_t maxRange, entity_pos_t elevationBonus, std::vector<int> owners, int requiredInterface, u8 flags) = 0; 123 124 105 125 /** 126 * Get the average elevation over 8 points on distance range around the entity 127 * @param id the entity id to look around 128 * @param range the distance to compare terrain height with 129 * @return a fixed number representing the average difference. It's positive when the entity is on average higher than the terrain surrounding it. 130 */ 131 virtual entity_pos_t GetElevationAdaptedRange(CFixedVector3D pos, CFixedVector3D rot, entity_pos_t range, entity_pos_t elevationBonus, entity_pos_t angle) = 0; 132 133 /** 106 134 * Destroy a query and clean up resources. This must be called when an entity no longer needs its 107 135 * query (e.g. when the entity is destroyed). 108 136 * @param tag identifier of query. -
source/simulation2/system/InterfaceScripted.h
85 85 6, \ 86 86 JSPROP_ENUMERATE|JSPROP_READONLY|JSPROP_PERMANENT }, 87 87 88 #define DEFINE_INTERFACE_METHOD_7(scriptname, rettype, classname, methodname, arg1, arg2, arg3, arg4, arg5, arg6, arg7) \ 89 { scriptname, \ 90 ScriptInterface::callMethod<rettype, arg1, arg2, arg3, arg4, arg5, arg6, arg7, &class_##classname, classname, &classname::methodname>, \ 91 7, \ 92 JSPROP_ENUMERATE|JSPROP_READONLY|JSPROP_PERMANENT }, 93 88 94 #endif // INCLUDED_INTERFACE_SCRIPTED