Ticket #1960: elevation_advantage_with_gui.diff

File elevation_advantage_with_gui.diff, 40.0 KB (added by sanderd17, 11 years ago)

update with svn

  • binaries/data/mods/public/gui/session/input.js

     
    121121            // Show placement info tooltip if invalid position
    122122            placementSupport.tooltipError = !result.success;
    123123            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;
    125141        }
    126142    }
    127143    else if (placementSupport.mode === "wall")
     
    14941510        placementSupport.template = buildTemplate;
    14951511        inputState = INPUT_BUILDING_PLACEMENT;
    14961512    }
     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    }
    14971521}
    14981522
    14991523// Called by GUI when user changes preferred trading goods
  • binaries/data/mods/public/gui/session/placement.js

     
    2222   
    2323    this.SetDefaultAngle();
    2424    this.RandomizeActorSeed();
     25
     26    this.attack = null;
    2527   
    2628    Engine.GuiInterfaceCall("SetBuildingPlacementPreview", {"template": ""});
    2729    Engine.GuiInterfaceCall("SetWallPlacementPreview", {"wallSet": null});
  • binaries/data/mods/public/gui/session/selection_details.js

     
    199199    else
    200200    {
    201201        // 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)
    220238        iconTooltip = "[font=\"serif-bold-16\"]" + genericName + "[/font]";
    221239
    222240    if (template.tooltip)
  • binaries/data/mods/public/simulation/components/Attack.js

     
    6767            "<Crush>0.0</Crush>" +
    6868            "<MaxRange>44.0</MaxRange>" +
    6969            "<MinRange>20.0</MinRange>" +
     70            "<optional>"+
     71                "<element name='ElevationBonus' a:help='give an elevation advantage (in meters)'><ref name='nonNegativeDecimal'/></element>" +
     72            "</optional>" +
    7073            "<PrepareTime>800</PrepareTime>" +
    7174            "<RepeatTime>1600</RepeatTime>" +
    7275            "<ProjectileSpeed>50.0</ProjectileSpeed>" +
     
    125128                "<element name='Crush' a:help='Crush damage strength'><ref name='nonNegativeDecimal'/></element>" +
    126129                "<element name='MaxRange' a:help='Maximum attack range (in metres)'><ref name='nonNegativeDecimal'/></element>" +
    127130                "<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>" +
    128134                "<element name='PrepareTime' a:help='Time from the start of the attack command until the attack actually occurs (in milliseconds). This value relative to RepeatTime should closely match the \"event\" point in the actor&apos;s attack animation'>" +
    129135                    "<data type='nonNegativeInteger'/>" +
    130136                "</element>" +
     
    360366   
    361367    var min = +(this.template[type].MinRange || 0);
    362368    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);
    363372   
    364     return { "max": max, "min": min };
     373    return { "max": max, "min": min, "elevationBonus": elevationBonus};
    365374};
    366375
    367376// Calculate the attack damage multiplier against a target
  • binaries/data/mods/public/simulation/components/BuildingAI.js

     
    1212        "<ref name='nonNegativeDecimal'/>" +
    1313    "</element>";
    1414
     15
    1516/**
    1617 * Initialize BuildingAI Component
    1718 */
     
    99100    if (cmpAttack)
    100101    {
    101102        var range = cmpAttack.GetRange("Ranged");
    102         this.enemyUnitsQuery = cmpRangeManager.CreateActiveQuery(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"));
    103104        cmpRangeManager.EnableActiveQuery(this.enemyUnitsQuery);
    104105    }
    105106};
     
    133134        var range = cmpAttack.GetRange("Ranged");
    134135
    135136        // This query is only interested in Gaia entities that can attack.
    136         this.gaiaUnitsQuery = rangeMan.CreateActiveQuery(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"));
    137138        rangeMan.EnableActiveQuery(this.gaiaUnitsQuery);
    138139    }
    139140};
     
    214215    var cmpAttack = Engine.QueryInterface(this.entity, IID_Attack);
    215216    if (cmpAttack)
    216217    {
     218
    217219        var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
    218220        this.timer = cmpTimer.SetTimeout(this.entity, IID_BuildingAI, "FireArrows", timerInterval, {});
    219221        var arrowsToFire = 0;
     
    239241            //Fire N arrows, 0 <= N <= Number of arrows left
    240242            arrowsToFire = Math.floor(Math.random() * this.arrowsLeft);
    241243        }
     244
    242245        if (this.targetUnits.length > 0)
    243246        {
     247            var clonedTargets = this.targetUnits.slice();
    244248            for (var i = 0;i < arrowsToFire;i++)
    245249            {
    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                }
    248271            }
    249272            this.arrowsLeft -= arrowsToFire;
    250273        }
     
    252275    }
    253276};
    254277
     278/**
     279 * Returns true if the target entity is visible through the FoW/SoD.
     280 */
     281BuildingAI.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
    255296Engine.RegisterComponentType(IID_BuildingAI, "BuildingAI", BuildingAI);
  • binaries/data/mods/public/simulation/components/GuiInterface.js

     
    173173    if (cmpPosition && cmpPosition.IsInWorld())
    174174    {
    175175        ret.position = cmpPosition.GetPosition();
     176        ret.rotation = cmpPosition.GetRotation();
    176177    }
    177178
    178179    var cmpHealth = Engine.QueryInterface(ent, IID_Health);
     
    184185        ret.needsHeal = !cmpHealth.IsUnhealable();
    185186    }
    186187
     188    var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
     189    var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
    187190    var cmpAttack = Engine.QueryInterface(ent, IID_Attack);
     191
    188192    if (cmpAttack)
    189193    {
    190194        var type = cmpAttack.GetBestAttack(); // TODO: how should we decide which attack to show? show all?
     
    193197        ret.attack.type = type;
    194198        ret.attack.minRange = range.min;
    195199        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        }
    196226    }
    197227
    198228    var cmpArmour = Engine.QueryInterface(ent, IID_DamageReceiver);
     
    311341            "req": cmpPromotion.GetRequiredXp()
    312342        };
    313343    }
    314    
    315     var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
     344
    316345    if (cmpUnitAI)
    317346    {
    318347        ret.unitAI = {
     
    323352        if (cmpUnitAI.isGarrisoned && ret.player)
    324353            ret.template = "p" + ret.player + "&" + ret.template;
    325354    }
    326    
     355
    327356    var cmpGate = Engine.QueryInterface(ent, IID_Gate);
    328357    if (cmpGate)
    329358    {
     
    347376        };
    348377    }
    349378
    350     var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
    351379    ret.visibility = cmpRangeManager.GetLosVisibility(ent, player, false);
    352380
    353381    return ret;
    354382};
    355383
     384GuiInterface.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
    356399GuiInterface.prototype.GetTemplateData = function(player, extendedName)
    357400{
    358401    var name = extendedName;
     
    391434                "crush": GetTechModifiedProperty(techMods, template, "Attack/"+type+"/Crush", +(template.Attack[type].Crush || 0)),
    392435                "minRange": GetTechModifiedProperty(techMods, template, "Attack/"+type+"/MinRange", +(template.Attack[type].MinRange || 0)),
    393436                "maxRange": GetTechModifiedProperty(techMods, template, "Attack/"+type+"/MaxRange", +template.Attack[type].MaxRange),
     437                "elevationBonus": GetTechModifiedProperty(techMods, template, "Attack/"+type+"/ElevationBonus", +(template.Attack[type].ElevationBonus || 0)),
    394438            };
    395439        }
    396440    }
     
    17271771    "GetRenamedEntities": 1,
    17281772    "ClearRenamedEntities": 1,
    17291773    "GetEntityState": 1,
     1774    "GetAverageRangeForBuildings": 1,
    17301775    "GetTemplateData": 1,
    17311776    "GetTechnologyData": 1,
    17321777    "IsTechnologyResearched": 1,
  • binaries/data/mods/public/simulation/components/UnitAI.js

     
    367367        this.order.data.attackType = type;
    368368
    369369        // If we are already at the target, try attacking it from here
    370         if (this.CheckTargetRange(this.order.data.target, IID_Attack, this.order.data.attackType))
     370        if (this.CheckTargetAttackRange(this.order.data.target, IID_Attack, this.order.data.attackType))
    371371        {
    372372            this.StopMoving();
    373373            // For packable units within attack range:
     
    428428        }
    429429
    430430        // Try to move within attack range
    431         if (this.MoveToTargetRange(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))
    432432        {
    433433            // We've started walking to the given point
    434434            if (this.IsAnimal())
     
    13151315                },
    13161316
    13171317                "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                    }
    13231339                },
    13241340
    13251341                "Attacked": function(msg) {
     
    13351351            "UNPACKING": {
    13361352                "enter": function() {
    13371353                    // If we're not in range yet (maybe we stopped moving), move to target again
    1338                     if (!this.CheckTargetRange(this.order.data.target, IID_Attack, this.order.data.attackType))
     1354                    if (!this.CheckTargetAttackRange(this.order.data.target, IID_Attack, this.order.data.attackType))
    13391355                    {
    1340                         if (this.MoveToTargetRange(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))
    13411357                            this.SetNextState("APPROACHING");
    13421358                        else
    13431359                        {
     
    14031419                    if (this.TargetIsAlive(target) && this.CanAttack(target, this.order.data.forceResponse || null))
    14041420                    {
    14051421                        // Check we can still reach the target
    1406                         if (this.CheckTargetRange(target, IID_Attack, this.order.data.attackType))
     1422                        if (this.CheckTargetAttackRange(target, IID_Attack, this.order.data.attackType))
    14071423                        {
    14081424                            var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
    14091425                            this.lastAttacked = cmpTimer.GetTime() - msg.lateness;
     
    32823298    return cmpUnitMotion.MoveToTargetRange(target, range.min, range.max);
    32833299};
    32843300
     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 */
     3310UnitAI.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
    32853348UnitAI.prototype.MoveToTargetRangeExplicit = function(target, min, max)
    32863349{
    32873350    if (!this.CheckTargetVisible(target))
     
    33063369    return cmpUnitMotion.IsInTargetRange(target, range.min, range.max);
    33073370};
    33083371
     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 */
     3378UnitAI.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
    33093409UnitAI.prototype.CheckTargetRangeExplicit = function(target, min, max)
    33103410{
    33113411    var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion);
  • binaries/data/mods/public/simulation/templates/template_structure_defense_defense_tower.xml

     
    11<?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>
    1617  <BuildingAI>
    1718    <DefaultArrowCount>1</DefaultArrowCount>
    1819    <GarrisonArrowMultiplier>1</GarrisonArrowMultiplier>
  • source/scriptinterface/ScriptInterface.h

     
    3838// Set the maximum number of function arguments that can be handled
    3939// (This should be as small as possible (for compiler efficiency),
    4040// but as large as necessary for all wrapped functions)
    41 #define SCRIPT_INTERFACE_MAX_ARGS 7
     41#define SCRIPT_INTERFACE_MAX_ARGS 8
    4242
    4343// TODO: what's a good default?
    4444#define DEFAULT_RUNTIME_SIZE 16 * 1024 * 1024
  • source/simulation2/components/CCmpRangeManager.cpp

     
    2020#include "simulation2/system/Component.h"
    2121#include "ICmpRangeManager.h"
    2222
     23#include "ICmpTerrain.h"
    2324#include "simulation2/MessageTypes.h"
    2425#include "simulation2/components/ICmpPosition.h"
    2526#include "simulation2/components/ICmpTerritoryManager.h"
    2627#include "simulation2/components/ICmpVision.h"
     28#include "simulation2/components/ICmpWaterManager.h"
    2729#include "simulation2/helpers/Render.h"
    2830#include "simulation2/helpers/Spatial.h"
    2931
     
    4446struct Query
    4547{
    4648    bool enabled;
     49    bool parabolic;
    4750    entity_id_t source;
    4851    entity_pos_t minRange;
    4952    entity_pos_t maxRange;
     53    entity_pos_t elevationBonus;
    5054    u32 ownersMask;
    5155    i32 interface;
    5256    std::vector<entity_id_t> lastMatch;
     
    8892}
    8993
    9094/**
     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 */
     101static 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
     122struct EntityParabolicRangeOutline
     123{
     124    entity_id_t source;
     125    CFixedVector3D position;
     126    entity_pos_t range;
     127    std::vector<entity_pos_t> outline;
     128};
     129
     130static std::map<entity_id_t, EntityParabolicRangeOutline> ParabolicRangesOutlines;
     131
     132/**
    91133 * Representation of an entity, with the data needed for queries.
    92134 */
    93135struct EntityData
     
    113155    void operator()(S& serialize, const char* UNUSED(name), Query& value)
    114156    {
    115157        serialize.Bool("enabled", value.enabled);
     158        serialize.Bool("parabolic",value.parabolic);
    116159        serialize.NumberU32_Unbounded("source", value.source);
    117160        serialize.NumberFixed_Unbounded("min range", value.minRange);
    118161        serialize.NumberFixed_Unbounded("max range", value.maxRange);
     162        serialize.NumberFixed_Unbounded("elevation bonus", value.elevationBonus);
    119163        serialize.NumberU32_Unbounded("owners mask", value.ownersMask);
    120164        serialize.NumberI32_Unbounded("interface", value.interface);
    121165        SerializeVector<SerializeU32_Unbounded>()(serialize, "last match", value.lastMatch);
     
    589633        return id;
    590634    }
    591635
     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
    592646    virtual void DestroyActiveQuery(tag_t tag)
    593647    {
    594648        if (m_Queries.find(tag) == m_Queries.end())
     
    812866                r.push_back(it->first);
    813867            }
    814868        }
    815         else
     869        // Not the entire world, so check a parabolic range, or a regular range
     870        else if (q.parabolic)
    816871        {
     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        {
    817912            // Get a quick list of entities that are potentially in range
    818913            std::vector<entity_id_t> ents = m_Subdivision.GetNear(pos, q.maxRange);
    819914
     
    838933                }
    839934
    840935                r.push_back(it->first);
     936
    841937            }
    842938        }
    843939    }
    844940
     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   
    8451046    Query ConstructQuery(entity_id_t source,
    8461047        entity_pos_t minRange, entity_pos_t maxRange,
    8471048        const std::vector<int>& owners, int requiredInterface, u8 flagsMask)
     
    8561057
    8571058        Query q;
    8581059        q.enabled = false;
     1060        q.parabolic = false;
    8591061        q.source = source;
    8601062        q.minRange = minRange;
    8611063        q.maxRange = maxRange;
     1064        q.elevationBonus = entity_pos_t::Zero();
    8621065
    8631066        q.ownersMask = 0;
    8641067        for (size_t i = 0; i < owners.size(); ++i)
     
    8701073        return q;
    8711074    }
    8721075
     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
    8731087    void RenderSubmit(SceneCollector& collector)
    8741088    {
    8751089        if (!m_DebugOverlayEnabled)
    8761090            return;
    877 
    8781091        CColor enabledRingColour(0, 1, 0, 1);
    8791092        CColor disabledRingColour(1, 0, 0, 1);
    8801093        CColor rayColour(1, 1, 0, 0.2f);
     
    8931106                CFixedVector2D pos = cmpSourcePosition->GetPosition2D();
    8941107
    8951108                // 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;
    8991120
     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
    9001174                // Draw the min range circle
    9011175                if (!q.minRange.IsZero())
    9021176                {
    903                     m_DebugOverlayLines.push_back(SOverlayLine());
    904                     m_DebugOverlayLines.back().m_Color = (q.enabled ? enabledRingColour : disabledRingColour);
    9051177                    SimRender::ConstructCircleOnGround(GetSimContext(), pos.X.ToFloat(), pos.Y.ToFloat(), q.minRange.ToFloat(), m_DebugOverlayLines.back(), true);
    9061178                }
    9071179
  • source/simulation2/components/ICmpRangeManager.cpp

     
    3636BEGIN_INTERFACE_WRAPPER(RangeManager)
    3737DEFINE_INTERFACE_METHOD_5("ExecuteQuery", std::vector<entity_id_t>, ICmpRangeManager, ExecuteQuery, entity_id_t, entity_pos_t, entity_pos_t, std::vector<int>, int)
    3838DEFINE_INTERFACE_METHOD_6("CreateActiveQuery", ICmpRangeManager::tag_t, ICmpRangeManager, CreateActiveQuery, entity_id_t, entity_pos_t, entity_pos_t, std::vector<int>, int, u8)
     39DEFINE_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)
    3940DEFINE_INTERFACE_METHOD_1("DestroyActiveQuery", void, ICmpRangeManager, DestroyActiveQuery, ICmpRangeManager::tag_t)
    4041DEFINE_INTERFACE_METHOD_1("EnableActiveQuery", void, ICmpRangeManager, EnableActiveQuery, ICmpRangeManager::tag_t)
    4142DEFINE_INTERFACE_METHOD_1("DisableActiveQuery", void, ICmpRangeManager, DisableActiveQuery, ICmpRangeManager::tag_t)
     
    4546DEFINE_INTERFACE_METHOD_1("GetEntitiesByPlayer", std::vector<entity_id_t>, ICmpRangeManager, GetEntitiesByPlayer, player_id_t)
    4647DEFINE_INTERFACE_METHOD_1("SetDebugOverlay", void, ICmpRangeManager, SetDebugOverlay, bool)
    4748DEFINE_INTERFACE_METHOD_2("SetLosRevealAll", void, ICmpRangeManager, SetLosRevealAll, player_id_t, bool)
     49DEFINE_INTERFACE_METHOD_5("GetElevationAdaptedRange", entity_pos_t, ICmpRangeManager, GetElevationAdaptedRange, CFixedVector3D, CFixedVector3D, entity_pos_t, entity_pos_t, entity_pos_t)
    4850DEFINE_INTERFACE_METHOD_3("GetLosVisibility", std::string, ICmpRangeManager, GetLosVisibility_wrapper, entity_id_t, player_id_t, bool)
    4951DEFINE_INTERFACE_METHOD_1("SetLosCircular", void, ICmpRangeManager, SetLosCircular, bool)
    5052DEFINE_INTERFACE_METHOD_0("GetLosCircular", bool, ICmpRangeManager, GetLosCircular)
  • source/simulation2/components/ICmpRangeManager.h

     
    1818#ifndef INCLUDED_ICMPRANGEMANAGER
    1919#define INCLUDED_ICMPRANGEMANAGER
    2020
     21#include "maths/FixedVector3D.h"
     22
    2123#include "simulation2/system/Interface.h"
    2224#include "simulation2/helpers/Position.h"
    2325#include "simulation2/helpers/Player.h"
     
    102104    virtual tag_t CreateActiveQuery(entity_id_t source,
    103105        entity_pos_t minRange, entity_pos_t maxRange, std::vector<int> owners, int requiredInterface, u8 flags) = 0;
    104106
     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
    105125    /**
     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    /**
    106134     * Destroy a query and clean up resources. This must be called when an entity no longer needs its
    107135     * query (e.g. when the entity is destroyed).
    108136     * @param tag identifier of query.
  • source/simulation2/system/InterfaceScripted.h

     
    8585        6, \
    8686        JSPROP_ENUMERATE|JSPROP_READONLY|JSPROP_PERMANENT },
    8787
     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
    8894#endif // INCLUDED_INTERFACE_SCRIPTED