Ticket #1960: elevation_advantage_with_gui.diff
File elevation_advantage_with_gui.diff, 40.0 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
199 199 else 200 200 { 201 201 // TODO: we should require all entities to have icons, so this case never occurs 202 getGUIObjectByName("icon").sprite = "bkFillBlack"; 203 } 204 205 // Attack and Armor 206 var type = ""; 207 if (entState.attack) 208 type = entState.attack.type + " "; 209 210 attack = "[font=\"serif-bold-13\"]"+type+"Attack:[/font] " + damageTypeDetails(entState.attack); 211 // Show max attack range if ranged attack, also convert to tiles (4m per tile) 212 if (entState.attack && entState.attack.type == "Ranged") 213 attack += ", [font=\"serif-bold-13\"]Range:[/font] " + Math.round(entState.attack.maxRange/4); 214 getGUIObjectByName("attackAndArmorStats").tooltip = attack + "\n[font=\"serif-bold-13\"]Armor:[/font] " + armorTypeDetails(entState.armour); 215 216 // Icon Tooltip 217 var iconTooltip = ""; 218 219 if (genericName) 202 getGUIObjectByName("icon").sprite = "bkFillBlack"; 203 } 204 205 // Attack and Armor 206 var type = ""; 207 var attack = "[font=\"serif-bold-13\"]"+type+"Attack:[/font] " + damageTypeDetails(entState.attack); 208 if (entState.attack) 209 { 210 type = entState.attack.type + " "; 211 212 // Show max attack range if ranged attack, also convert to tiles (4m per tile) 213 if (entState.attack.type == "Ranged") 214 { 215 var realRange = entState.attack.elevationAdaptedRange; 216 var range = entState.attack.maxRange; 217 attack += ", [font=\"serif-bold-13\"]Range:[/font] " + 218 Math.round(range/4); 219 220 if (Math.round((realRange - range)/4) > 0) 221 { 222 attack += " (+" + Math.round((realRange - range)/4) + ")"; 223 } 224 else if (Math.round((realRange - range)/4) < 0) 225 { 226 attack += " (" + Math.round((realRange - range)/4) + ")"; 227 } // don't show when it's 0 228 229 } 230 } 231 232 getGUIObjectByName("attackAndArmorStats").tooltip = attack + "\n[font=\"serif-bold-13\"]Armor:[/font] " + armorTypeDetails(entState.armour); 233 234 // Icon Tooltip 235 var iconTooltip = ""; 236 237 if (genericName) 220 238 iconTooltip = "[font=\"serif-bold-16\"]" + genericName + "[/font]"; 221 239 222 240 if (template.tooltip) -
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); … … 311 341 "req": cmpPromotion.GetRequiredXp() 312 342 }; 313 343 } 314 315 var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI); 344 316 345 if (cmpUnitAI) 317 346 { 318 347 ret.unitAI = { … … 323 352 if (cmpUnitAI.isGarrisoned && ret.player) 324 353 ret.template = "p" + ret.player + "&" + ret.template; 325 354 } 326 355 327 356 var cmpGate = Engine.QueryInterface(ent, IID_Gate); 328 357 if (cmpGate) 329 358 { … … 347 376 }; 348 377 } 349 378 350 var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);351 379 ret.visibility = cmpRangeManager.GetLosVisibility(ent, player, false); 352 380 353 381 return ret; 354 382 }; 355 383 384 GuiInterface.prototype.GetAverageRangeForBuildings = function(player, cmd) 385 { 386 var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); 387 var cmpTerrain = Engine.QueryInterface(SYSTEM_ENTITY, IID_Terrain); 388 var rot = {x:0, y:0, z:0}; 389 var pos = {x:cmd.x,z:cmd.z}; 390 pos.y = cmpTerrain.GetGroundLevel(cmd.x, cmd.z); 391 var elevationBonus = cmd.elevationBonus || 0; 392 var range = cmd.range; 393 394 return cmpRangeManager.GetElevationAdaptedRange(pos, rot, range, elevationBonus, 2*Math.PI); 395 }; 396 397 398 356 399 GuiInterface.prototype.GetTemplateData = function(player, extendedName) 357 400 { 358 401 var name = extendedName; … … 391 434 "crush": GetTechModifiedProperty(techMods, template, "Attack/"+type+"/Crush", +(template.Attack[type].Crush || 0)), 392 435 "minRange": GetTechModifiedProperty(techMods, template, "Attack/"+type+"/MinRange", +(template.Attack[type].MinRange || 0)), 393 436 "maxRange": GetTechModifiedProperty(techMods, template, "Attack/"+type+"/MaxRange", +template.Attack[type].MaxRange), 437 "elevationBonus": GetTechModifiedProperty(techMods, template, "Attack/"+type+"/ElevationBonus", +(template.Attack[type].ElevationBonus || 0)), 394 438 }; 395 439 } 396 440 } … … 1727 1771 "GetRenamedEntities": 1, 1728 1772 "ClearRenamedEntities": 1, 1729 1773 "GetEntityState": 1, 1774 "GetAverageRangeForBuildings": 1, 1730 1775 "GetTemplateData": 1, 1731 1776 "GetTechnologyData": 1, 1732 1777 "IsTechnologyResearched": 1, -
binaries/data/mods/public/simulation/components/UnitAI.js
367 367 this.order.data.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.order.data.attackType))370 if (this.CheckTargetAttackRange(this.order.data.target, IID_Attack, this.order.data.attackType)) 371 371 { 372 372 this.StopMoving(); 373 373 // For packable units within attack range: … … 428 428 } 429 429 430 430 // Try to move within attack range 431 if (this.MoveToTarget Range(this.order.data.target, IID_Attack, this.order.data.attackType))431 if (this.MoveToTargetAttackRange(this.order.data.target, IID_Attack, this.order.data.attackType,0.5)) 432 432 { 433 433 // We've started walking to the given point 434 434 if (this.IsAnimal()) … … 1315 1315 }, 1316 1316 1317 1317 "MoveCompleted": function() { 1318 // If the unit needs to unpack, do so 1319 if (this.CanUnpack()) 1320 this.SetNextState("UNPACKING"); 1321 else 1322 this.SetNextState("ATTACKING"); 1318 1319 if (this.CheckTargetAttackRange(this.order.data.target, IID_Attack , this.order.data.attackType)) 1320 { 1321 // If the unit needs to unpack, do so 1322 if (this.CanUnpack()) 1323 this.SetNextState("UNPACKING"); 1324 else 1325 this.SetNextState("ATTACKING"); 1326 } 1327 else 1328 { 1329 if (this.MoveToTargetAttackRange(this.order.data.target, IID_Attack, this.order.data.attackType,0)) 1330 { 1331 this.SetNextState("APPROACHING"); 1332 } 1333 else 1334 { 1335 // Give up 1336 this.FinishOrder(); 1337 } 1338 } 1323 1339 }, 1324 1340 1325 1341 "Attacked": function(msg) { … … 1335 1351 "UNPACKING": { 1336 1352 "enter": function() { 1337 1353 // If we're not in range yet (maybe we stopped moving), move to target again 1338 if (!this.CheckTarget Range(this.order.data.target, IID_Attack, this.order.data.attackType))1354 if (!this.CheckTargetAttackRange(this.order.data.target, IID_Attack, this.order.data.attackType)) 1339 1355 { 1340 if (this.MoveToTarget Range(this.order.data.target, IID_Attack, this.order.data.attackType))1356 if (this.MoveToTargetAttackRange(this.order.data.target, IID_Attack, this.order.data.attackType,0.5)) 1341 1357 this.SetNextState("APPROACHING"); 1342 1358 else 1343 1359 { … … 1403 1419 if (this.TargetIsAlive(target) && this.CanAttack(target, this.order.data.forceResponse || null)) 1404 1420 { 1405 1421 // Check we can still reach the target 1406 if (this.CheckTarget Range(target, IID_Attack, this.order.data.attackType))1422 if (this.CheckTargetAttackRange(target, IID_Attack, this.order.data.attackType)) 1407 1423 { 1408 1424 var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); 1409 1425 this.lastAttacked = cmpTimer.GetTime() - msg.lateness; … … 3282 3298 return cmpUnitMotion.MoveToTargetRange(target, range.min, range.max); 3283 3299 }; 3284 3300 3301 /** 3302 * Move unit so we hope the target is in the attack range 3303 * for melee attacks, this goes straight to the default range checks 3304 * for ranged attacks, the parabolic range is used, so we can't know exactly at what horizontal range the target can be reached 3305 * That's why a guess is needed 3306 * a guess of 1 will take the maximum of the possible ranges, and stay far away 3307 * a guess of 0 will take the minimum of the possible ranges and, in most cases, will have the target in range. 3308 * every guess inbetween is a linear interpollation 3309 */ 3310 UnitAI.prototype.MoveToTargetAttackRange = function(target, iid, type,guess) 3311 { 3312 3313 if(type!= "Ranged") 3314 return this.MoveToTargetRange(target, iid, type); 3315 3316 if (!this.CheckTargetVisible(target)) 3317 return false; 3318 3319 var cmpRanged = Engine.QueryInterface(this.entity, iid); 3320 var range = cmpRanged.GetRange(type); 3321 3322 var thisCmpPosition = Engine.QueryInterface(this.entity, IID_Position); 3323 var s = thisCmpPosition.GetPosition(); 3324 3325 var targetCmpPosition = Engine.QueryInterface(target, IID_Position); 3326 if(!targetCmpPosition.IsInWorld()) 3327 return false; 3328 3329 var t = targetCmpPosition.GetPosition(); 3330 // h is positive when I'm higher than the target 3331 var h = s.y-t.y+range.elevationBonus; 3332 3333 // No negative roots please 3334 if(h>-range.max/2) 3335 var parabolicMaxRange = Math.sqrt(range.max*range.max+2*range.max*h); 3336 else 3337 // return false? Or hope you come close enough? 3338 var parabolicMaxRange = 0; 3339 //return false; 3340 3341 // the parabole changes while walking, take something in the middle 3342 var guessedMaxRange = Math.max(range.max, parabolicMaxRange)*guess+Math.min(range.max, parabolicMaxRange)*(1-guess) ; 3343 3344 var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion); 3345 return cmpUnitMotion.MoveToTargetRange(target, range.min, guessedMaxRange); 3346 }; 3347 3285 3348 UnitAI.prototype.MoveToTargetRangeExplicit = function(target, min, max) 3286 3349 { 3287 3350 if (!this.CheckTargetVisible(target)) … … 3306 3369 return cmpUnitMotion.IsInTargetRange(target, range.min, range.max); 3307 3370 }; 3308 3371 3372 /** 3373 * Check if the target is inside the attack range 3374 * For melee attacks, this goes straigt to the regular range calculation 3375 * For ranged attacks, the parabolic formula is used to accout for bigger ranges 3376 * when the target is lower, and smaller ranges when the target is higher 3377 */ 3378 UnitAI.prototype.CheckTargetAttackRange = function(target, iid, type) 3379 { 3380 3381 if (type != "Ranged") 3382 return this.CheckTargetRange(target,iid,type); 3383 3384 var targetCmpPosition = Engine.QueryInterface(target, IID_Position); 3385 if (!targetCmpPosition || !targetCmpPosition.IsInWorld()) 3386 return false; 3387 3388 var cmpRanged = Engine.QueryInterface(this.entity, iid); 3389 var range = cmpRanged.GetRange(type); 3390 3391 var thisCmpPosition = Engine.QueryInterface(this.entity, IID_Position); 3392 var s = thisCmpPosition.GetPosition(); 3393 3394 var t = targetCmpPosition.GetPosition(); 3395 3396 var h = s.y-t.y+range.elevationBonus; 3397 var maxRangeSq = 2*range.max*(h + range.max/2); 3398 3399 if (maxRangeSq < 0) 3400 return false; 3401 3402 var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion); 3403 return cmpUnitMotion.IsInTargetRange(target, range.min, Math.sqrt(maxRangeSq)); 3404 3405 return maxRangeSq >= distanceSq && range.min*range.min <= distanceSq; 3406 3407 }; 3408 3309 3409 UnitAI.prototype.CheckTargetRangeExplicit = function(target, min, max) 3310 3410 { 3311 3411 var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion); -
binaries/data/mods/public/simulation/templates/template_structure_defense_defense_tower.xml
1 1 <?xml version="1.0" encoding="utf-8"?> 2 <Entity parent="template_structure_defense"> 3 <Attack> 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> 13 <Spread>1.5</Spread> 14 </Ranged> 15 </Attack> 2 <Entity parent="template_structure_defense"> 3 <Attack> 4 <Ranged> 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> 14 <Spread>1.5</Spread> 15 </Ranged> 16 </Attack> 16 17 <BuildingAI> 17 18 <DefaultArrowCount>1</DefaultArrowCount> 18 19 <GarrisonArrowMultiplier>1</GarrisonArrowMultiplier> -
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