Ticket #252: t252_secondattack_4.diff

File t252_secondattack_4.diff, 38.1 KB (added by bb, 8 years ago)

Fixing some bugs found by fatherbushido, (it still crashes with any other spear champ than the persian immortal)

  • binaries/data/config/default.cfg

     
    274274[hotkey.session]
    275275kill = Delete                ; Destroy selected units
    276276stop = "H"                   ; Stop the current action
    277 attack = Ctrl                ; Modifier to attack instead of another action (eg capture)
    278 attackmove = Ctrl            ; Modifier to attackmove when clicking on a point
    279 attackmoveUnit = "Ctrl+Q"    ; Modifier to attackmove targeting only units when clicking on a point (should contain the attackmove keys)
     277attack = Ctrl                ; Modifier to primary attack instead of another action (eg capture)
     278attackmove = Ctrl            ; Modifier to primary attackmove when clicking on a point
     279attackmoveUnit = "Ctrl+Q"    ; Modifier to primary attackmove targeting only units when clicking on a point (should contain the attackmove keys)
    280280garrison = Ctrl              ; Modifier to garrison when clicking on building
    281281autorallypoint = Ctrl        ; Modifier to set the rally point on the building itself
     282secondattack = Alt           ; Modifier to secondary attack instead of another action
     283secondattackmove = Alt       ; Modifier to secondary attackmove when clicking on a point
     284secondattackmoveUnit = "Alt+Q"; Modifier to  secondary attackmove targeting only units when clicking on a point
    282285guard = "G"                  ; Modifier to escort/guard when clicking on unit/building
    283286queue = Shift                ; Modifier to queue unit orders instead of replacing
    284287batchtrain = Shift           ; Modifier to train units in batches
  • binaries/data/mods/public/art/actors/units/persians/champion_unit_1.xml

     
    1414        <animation event="0.5" file="infantry/sword/attack/isw_s_def_06.psa" name="attack_melee" speed="80"/>
    1515        <animation event="0.5" file="infantry/sword/attack/isw_s_def_06.psa" name="attack_melee" speed="80"/>
    1616        <animation event="0.5" file="infantry/sword/attack/isw_s_off_05.psa" name="attack_melee" speed="80"/>
     17        <animation event="0.84" file="biped/inf_arch_atk_a.psa" load="0.16" name="attack_ranged" speed="90"/>
    1718        <animation file="infantry/sword/move/run/isw_s_off_01.psa" name="Run" speed="25"/>
    1819        <animation file="infantry/sword/move/run/isw_s_def_02.psa" name="Run" speed="30"/>
    1920        <animation file="infantry/sword/move/run/isw_s_em_03.psa" name="Run" speed="30"/>
     
    2829      <props>
    2930        <prop actor="props/units/heads/head_pers_tiara.xml" attachpoint="head"/>
    3031        <prop actor="props/units/heads/pers_kidaris_tied.xml" attachpoint="helmet"/>
    31         <prop actor="props/units/shields/gerron_b.xml" attachpoint="shield"/>
    32         <prop actor="props/units/weapons/spear_ball.xml" attachpoint="r_hand"/>
    3332        <prop actor="props/units/pers_quiver_back.xml" attachpoint="back"/>
    3433      </props>
    3534    </variant>
     
    3938      <textures><texture file="skeletal/pers_su1_anusiya_iron.dds" name="baseTex"/></textures>
    4039    </variant>
    4140  </group>
     41  <group>
     42    <variant name="attack_melee">
     43      <props>
     44        <prop actor="props/units/shields/gerron_b.xml" attachpoint="shield"/>
     45        <prop actor="props/units/weapons/spear_ball.xml" attachpoint="r_hand"/>
     46      </props>
     47    </variant>
     48    <variant name="attack_ranged">
     49      <props>
     50        <prop actor="props/units/weapons/bow_recurve.xml" attachpoint="l_hand"/>
     51        <prop actor="props/units/weapons/arrow_back.xml" attachpoint="loaded-r_hand"/>
     52        <prop actor="props/units/weapons/arrow_front.xml" attachpoint="projectile"/>
     53      </props>
     54    </variant>
     55  </group>
    4256  <material>player_trans.xml</material>
    4357</actor>
  • binaries/data/mods/public/art/textures/cursors/action-second-attack-move.txt

     
     11 1
  • binaries/data/mods/public/art/textures/cursors/action-second-attack.txt

     
     11 1
  • binaries/data/mods/public/gui/common/tooltips.js

     
    166166
    167167    for (let type in template.attack)
    168168    {
     169        if (type == "ChangeDistance")
     170            continue; // not an attack type
    169171        if (type == "Slaughter")
    170172            continue; // Slaughter is not a real attack, so do not show it.
    171173        if (type == "Charge")
  • binaries/data/mods/public/gui/session/input.js

     
    205205                data.targetClasses = Engine.HotkeyIsPressed("session.attackmoveUnit") ? { "attack": ["Unit"] } : { "attack": ["Unit", "Structure"] };
    206206                cursor = "action-attack-move";
    207207            }
     208
     209            if (Engine.HotkeyIsPressed("session.secondattackmove"))
     210            {
     211                data.command = "attack-walk";
     212                data.targetClasses = Engine.HotkeyIsPressed("session.secondattackmoveUnit") ? { "attack": ["Unit"] } : { "attack": ["Unit", "Structure"] };
     213                cursor = "action-second-attack-move";
     214            }
    208215            return { "possible": true, "data": data, "cursor": cursor };
    209216        }
    210217
  • binaries/data/mods/public/gui/session/unit_actions.js

     
    6565            else
    6666                var targetClasses = { "attack": ["Unit", "Structure"] };
    6767
    68             Engine.PostNetworkCommand({"type": "attack-walk", "entities": selection, "x": target.x, "z": target.z, "targetClasses": targetClasses, "queued": queued});
     68            Engine.PostNetworkCommand({ "type": "attack-walk", "entities": selection, "x": target.x, "z": target.z, "targetClasses": targetClasses, "queued": queued, "prefType": "primary" });
    6969            Engine.GuiInterfaceCall("PlaySound", { "name": "order_walk", "entity": selection[0] });
    7070            return true;
    7171        },
     
    8181                return entState && entState.unitAI;
    8282            });
    8383            if (haveUnitAI && Engine.HotkeyIsPressed("session.attackmove") && getActionInfo("attack-move", target).possible)
    84                 return {"type": "attack-move", "cursor": "action-attack-move"};
     84                return { "type": "attack-move", "cursor": "action-attack-move" };
    8585            return false;
    8686        },
    8787        "specificness": 30,
    8888    },
    8989
     90    "second-attack-move": // TODO is this one needed?
     91    {
     92        "execute": function(target, action, selection, queued)
     93        {
     94            if (Engine.HotkeyIsPressed("session.secondattackmoveUnit"))
     95                var targetClasses = { "secondattack": ["Unit"] };
     96            else
     97                var targetClasses = { "secondattack": ["Unit", "Structure"] };
     98
     99            Engine.PostNetworkCommand({ "type": "attack-walk", "entities": selection, "x": target.x, "z": target.z, "targetClasses": targetClasses, "queued": queued, "prefType": "secondary" });
     100            Engine.GuiInterfaceCall("PlaySound", { "name": "order_walk", "entity": selection[0] });
     101            return true;
     102        },
     103        "getActionInfo": function(entState, targetState)
     104        {
     105            if (!entState.attack || !targetState.hitpoints)
     106                return false;
     107            return { "possible": Engine.GuiInterfaceCall("CanSecondAttack", { "entity": entState.id, "target": targetState.id }) };
     108        },
     109        "hotkeyActionCheck": function(target, selection)
     110        {
     111            // Work out whether at least part of the selection have UnitAI
     112            var haveUnitAI = selection.some(function(ent) {
     113                var entState = GetEntityState(ent);
     114                return entState && entState.unitAI;
     115            });
     116            if (haveUnitAI && Engine.HotkeyIsPressed("session.secondattackmove") && getActionInfo("second-attack-move", target).possible)
     117                return { "type": "second-attack-move", "cursor": "action-second-attack-move" }; // TODO another cursor
     118            return false;
     119        },
     120        "specificness": 32,
     121    },
     122
    90123    "capture":
    91124    {
    92125        "execute": function(target, action, selection, queued)
    93126        {
    94             Engine.PostNetworkCommand({"type": "attack", "entities": selection, "target": action.target, "allowCapture": true, "queued": queued});
     127            Engine.PostNetworkCommand({ "type": "attack", "entities": selection, "target": action.target, "prefType": "Capture", "queued": queued });
    95128            Engine.GuiInterfaceCall("PlaySound", { "name": "order_attack", "entity": selection[0] });
    96129            return true;
    97130        },
     
    99132        {
    100133            if (!entState.attack || !targetState.hitpoints)
    101134                return false;
    102             return {"possible": Engine.GuiInterfaceCall("CanCapture", {"entity": entState.id, "target": targetState.id})};
     135            return { "possible": Engine.GuiInterfaceCall("CanCapture", { "entity": entState.id, "target": targetState.id }) };
    103136        },
    104137        "actionCheck": function(target)
    105138        {
    106139            if (getActionInfo("capture", target).possible)
    107                 return {"type": "capture", "cursor": "action-capture", "target": target};
     140                return { "type": "capture", "cursor": "action-capture", "target": target };
    108141            return false;
    109142        },
    110143        "specificness": 9,
     
    114147    {
    115148        "execute": function(target, action, selection, queued)
    116149        {
    117             Engine.PostNetworkCommand({"type": "attack", "entities": selection, "target": action.target, "queued": queued, "allowCapture": false});
     150            Engine.PostNetworkCommand({ "type": "attack", "entities": selection, "target": action.target, "queued": queued, "prefType": "primary" });
    118151            Engine.GuiInterfaceCall("PlaySound", { "name": "order_attack", "entity": selection[0] });
    119152            return true;
    120153        },
     
    122155        {
    123156            if (!entState.attack || !targetState.hitpoints)
    124157                return false;
    125             return {"possible": Engine.GuiInterfaceCall("CanAttack", {"entity": entState.id, "target": targetState.id})};
     158            return { "possible": Engine.GuiInterfaceCall("CanAttack", {"entity": entState.id, "target": targetState.id }) };
    126159        },
    127160        "hotkeyActionCheck": function(target)
    128161        {
    129162            if (Engine.HotkeyIsPressed("session.attack") && getActionInfo("attack", target).possible)
    130                 return {"type": "attack", "cursor": "action-attack", "target": target};
     163                return { "type": "attack", "cursor": "action-attack", "target": target };
    131164            return false;
    132165        },
    133166        "actionCheck": function(target)
    134167        {
    135168            if (getActionInfo("attack", target).possible)
    136                 return {"type": "attack", "cursor": "action-attack", "target": target};
     169                return { "type": "attack", "cursor": "action-attack", "target": target };
    137170            return false;
    138171        },
    139172        "specificness": 10,
    140173    },
    141174
     175    "second-attack":
     176    {
     177        "execute": function(target, action, selection, queued)
     178        {
     179            Engine.PostNetworkCommand({ "type": "attack", "entities": selection, "target": action.target, "queued": queued, "prefType": "secondary" });
     180            Engine.GuiInterfaceCall("PlaySound", { "name": "order_attack", "entity": selection[0] });
     181            return true;
     182        },
     183        "getActionInfo": function(entState, targetState)
     184        {
     185            if (!entState.attack || !targetState.hitpoints)
     186                return false;
     187            return { "possible": Engine.GuiInterfaceCall("CanSecondAttack", {"entity": entState.id, "target": targetState.id }) };
     188        },
     189        "hotkeyActionCheck": function(target)
     190        {
     191            if (Engine.HotkeyIsPressed("session.secondattack") && getActionInfo("second-attack", target).possible)
     192                return { "type": "second-attack", "cursor": "action-second-attack", "target": target };
     193            return false;
     194        },
     195        "actionCheck": function(target)
     196        {
     197            if (getActionInfo("second-attack", target).possible)
     198                return { "type": "second-attack", "cursor": "action-second-attack", "target": target };
     199            return false;
     200        },
     201        "specificness": 15,
     202    },
     203
    142204    "heal":
    143205    {
    144206        "execute": function(target, action, selection, queued)
    145207        {
    146             Engine.PostNetworkCommand({"type": "heal", "entities": selection, "target": action.target, "queued": queued});
     208            Engine.PostNetworkCommand({ "type": "heal", "entities": selection, "target": action.target, "queued": queued });
    147209            Engine.GuiInterfaceCall("PlaySound", { "name": "order_heal", "entity": selection[0] });
    148210            return true;
    149211        },
     
    506568                data.targetClasses = targetClasses;
    507569                cursor = "action-attack-move";
    508570            }
     571            if (Engine.HotkeyIsPressed("session.secondattackmove"))
     572            {
     573                if (Engine.HotkeyIsPressed("session.secondattackmoveUnit"))
     574                    var targetClasses = { "second-attack": ["Unit"] };
     575                else
     576                    var targetClasses = { "second-attack": ["Unit", "Structure"] };
     577                data.command = "second-attack-walk";
     578                data.targetClasses = targetClasses;
     579                cursor = "action-second-attack-move";
     580            }
    509581
    510582            if (targetState.garrisonHolder && playerCheck(entState, targetState, ["Player", "MutualAlly"]))
    511583            {
  • binaries/data/mods/public/simulation/ai/common-api/entity.js

     
    792792        return this;
    793793    },
    794794
    795     attack: function(unitId, allowCapture = true, queued = false) {
    796         Engine.PostCommand(PlayerID,{"type": "attack", "entities": [this.id()], "target": unitId, "allowCapture": allowCapture, "queued": queued});
     795    attack: function(unitId, prefType = "Capture", queued = false) {
     796        Engine.PostCommand(PlayerID,{"type": "attack", "entities": [this.id()], "target": unitId, "prefType": prefType, "queued": queued});
    797797        return this;
    798798    },
    799799
  • binaries/data/mods/public/simulation/ai/petra/defenseArmy.js

     
    7777    {
    7878        this.assignedTo[entID] = idFoe;
    7979        this.assignedAgainst[idFoe].push(entID);
    80         ent.attack(idFoe, m.allowCapture(ent, foeEnt), queued);
     80        ent.attack(idFoe, m.prefType(ent, foeEnt), queued);
    8181    }
    8282    else
    8383        gameState.ai.HQ.navalManager.requireTransport(gameState, ent, ownIndex, foeIndex, foePosition);
     
    116116        else if (orderData.length && orderData[0].target && orderData[0].attackType && orderData[0].attackType === "Capture")
    117117        {
    118118            let target = gameState.getEntityById(orderData[0].target);
    119             if (target && !m.allowCapture(ent, target))
     119            if (target && !m.prefType(ent, target))
    120120                ent.attack(orderData[0].target, false);
    121121        }
    122122    }
  • binaries/data/mods/public/simulation/ai/petra/entityExtend.js

     
    8282};
    8383
    8484// Decide if we should try to capture or destroy
    85 m.allowCapture = function(ent, target)
     85// TODO make this function less hacky
     86m.prefType = function(ent, target)
    8687{
    8788    return !target.hasClass("Siege") || !ent.hasClass("Melee") ||
    88         !target.isGarrisonHolder() || !target.garrisoned().length;
     89        !target.isGarrisonHolder() || !target.garrisoned().length ? "Capture": "primary";
    8990};
    9091
    9192// Makes the worker deposit the currently carried resources at the closest accessible dropsite
  • binaries/data/mods/public/simulation/components/Attack.js

     
    11function Attack() {}
    22
    3 Attack.prototype.bonusesSchema = 
     3Attack.prototype.bonusesSchema =
    44    "<optional>" +
    55        "<element name='Bonuses'>" +
    66            "<zeroOrMore>" +
     
    4141Attack.prototype.Schema =
    4242    "<a:help>Controls the attack abilities and strengths of the unit.</a:help>" +
    4343    "<a:example>" +
     44        "<ChangeDistance>20</ChangeDistance>" +
    4445        "<Melee>" +
     46            "<AttackOrder>primary</AttackOrder>" +
    4547            "<Hack>10.0</Hack>" +
    4648            "<Pierce>0.0</Pierce>" +
    4749            "<Crush>5.0</Crush>" +
     
    6264            "<PreferredClasses datatype=\"tokens\">Cavalry Infantry</PreferredClasses>" +
    6365        "</Melee>" +
    6466        "<Ranged>" +
     67            "<AttackOrder>secondary</AttackOrder>" +
    6568            "<Hack>0.0</Hack>" +
    6669            "<Pierce>10.0</Pierce>" +
    6770            "<Crush>0.0</Crush>" +
     
    103106        "</Slaughter>" +
    104107    "</a:example>" +
    105108    "<optional>" +
     109        "<element name='ChangeDistance' a:help='Distance to change between Melee and Ranged attack'>" +
     110            "<ref name='nonNegativeDecimal'/>" +
     111        "</element>" +
     112    "</optional>" +
     113    "<optional>" +
    106114        "<element name='Melee'>" +
     115            "<optional>" +
     116                "<element name='AttackOrder'>" +
     117                    "<choice>" +
     118                        "<value>primary</value>" +
     119                        "<value>secondary</value>" +
     120                    "</choice>" +
     121                "</element>" +
     122            "</optional>" +
    107123            "<interleave>" +
    108124                "<element name='Hack' a:help='Hack damage strength'><ref name='nonNegativeDecimal'/></element>" +
    109125                "<element name='Pierce' a:help='Pierce damage strength'><ref name='nonNegativeDecimal'/></element>" +
     
    120136    "</optional>" +
    121137    "<optional>" +
    122138        "<element name='Ranged'>" +
     139            "<optional>" +
     140                "<element name='AttackOrder'>" +
     141                    "<choice>" +
     142                        "<value>primary</value>" +
     143                        "<value>secondary</value>" +
     144                    "</choice>" +
     145                "</element>" +
     146            "</optional>" +
    123147            "<interleave>" +
    124148                "<element name='Hack' a:help='Hack damage strength'><ref name='nonNegativeDecimal'/></element>" +
    125149                "<element name='Pierce' a:help='Pierce damage strength'><ref name='nonNegativeDecimal'/></element>" +
     
    219243Attack.prototype.GetPreferredClasses = function(type)
    220244{
    221245    if (this.template[type] && this.template[type].PreferredClasses &&
    222         this.template[type].PreferredClasses._string)
    223     {
     246        this.template[type].PreferredClasses._string)
    224247        return this.template[type].PreferredClasses._string.split(/\s+/);
    225     }
    226248    return [];
    227249};
    228250
     
    229251Attack.prototype.GetRestrictedClasses = function(type)
    230252{
    231253    if (this.template[type] && this.template[type].RestrictedClasses &&
    232         this.template[type].RestrictedClasses._string)
    233     {
     254        this.template[type].RestrictedClasses._string)
    234255        return this.template[type].RestrictedClasses._string.split(/\s+/);
    235     }
    236256    return [];
    237257};
    238258
     
    253273    let heightDiff = Math.abs(cmpThisPosition.GetHeightOffset() - cmpTargetPosition.GetHeightOffset());
    254274
    255275    const cmpIdentity = Engine.QueryInterface(target, IID_Identity);
    256     if (!cmpIdentity) 
     276    if (!cmpIdentity)
    257277        return undefined;
    258278
    259279    const targetClasses = cmpIdentity.GetClassesList();
    260280
     281    let cmpEntityPlayer = QueryOwnerInterface(this.entity);
     282    let cmpTargetPlayer = QueryOwnerInterface(target);
     283    if (!cmpTargetPlayer || !cmpEntityPlayer)
     284        return false;
     285
    261286    for (let type of this.GetAttackTypes())
    262287    {
    263288        if (type == "Capture" && !QueryMiragedInterface(target, IID_Capturable))
    264289            continue;
    265290
     291        if (targetClasses.indexOf("Domestic") == -1 && !cmpEntityPlayer.IsEnemy(cmpTargetPlayer.GetPlayerID()))
     292            continue;
     293
    266294        if (type == "Slaughter" && targetClasses.indexOf("Domestic") == -1)
    267295            continue;
    268296
     
    288316    return false;
    289317};
    290318
     319Attack.prototype.CanSecondAttack = function(target)
     320{
     321    for (let type of this.GetAttackTypes())
     322        if (this.template[type].AttackOrder && this.template[type].AttackOrder == "secondary")
     323            return this.CanAttack(target);
     324    return false;
     325};
    291326/**
    292327 * Returns null if we have no preference or the lowest index of a preferred class.
    293328 */
     
    294329Attack.prototype.GetPreference = function(target)
    295330{
    296331    const cmpIdentity = Engine.QueryInterface(target, IID_Identity);
    297     if (!cmpIdentity) 
     332    if (!cmpIdentity)
    298333        return undefined;
    299334
    300335    const targetClasses = cmpIdentity.GetClassesList();
     
    327362        if (type == "Slaughter")
    328363            continue;
    329364        let range = this.GetRange(type);
    330         if (range.min < ret.min)
    331             ret.min = range.min;
    332         if (range.max > ret.max)
    333             ret.max = range.max;
     365        ret.min = Math.min(ret.min, range.min)
     366        ret.max = Math.max(ret.max, range.max)
    334367    }
    335368    return ret;
    336369};
    337370
    338 Attack.prototype.GetBestAttackAgainst = function(target, allowCapture)
     371Attack.prototype.GetBestAttackAgainst = function(target, prefType)
    339372{
    340373    let cmpFormation = Engine.QueryInterface(target, IID_Formation);
    341374    if (cmpFormation)
     
    350383    }
    351384
    352385    let cmpIdentity = Engine.QueryInterface(target, IID_Identity);
    353     if (!cmpIdentity) 
     386    if (!cmpIdentity)
    354387        return undefined;
    355388
    356389    let targetClasses = cmpIdentity.GetClassesList();
     
    357390    let isTargetClass = function (className) { return targetClasses.indexOf(className) != -1; };
    358391
    359392    // Always slaughter domestic animals instead of using a normal attack
    360     if (isTargetClass("Domestic") && this.template.Slaughter) 
     393    if (isTargetClass("Domestic") && this.template.Slaughter)
    361394        return "Slaughter";
    362395
    363396    let attack = this;
     
    365398
    366399    let types = this.GetAttackTypes().filter(isAllowed);
    367400
    368     // check if the target is capturable
    369     let captureIndex = types.indexOf("Capture");
    370     if (captureIndex != -1)
     401    if (prefType)
    371402    {
    372         let cmpCapturable = QueryMiragedInterface(target, IID_Capturable);
     403        if (types.indexOf(prefType) != -1)
     404            return prefType
     405        prefType = this.GetPrefAttack(types, prefType);
     406        if (prefType && types.indexOf(prefType) != -1)
     407            return prefType;
     408    }
     409    else
     410    {
     411        // check if the target is capturable
     412        let captureIndex = types.indexOf("Capture");
     413        if (captureIndex != -1)
     414        {
     415            let cmpCapturable = QueryMiragedInterface(target, IID_Capturable);
    373416
    374         let cmpPlayer = QueryOwnerInterface(this.entity);
    375         if (allowCapture && cmpPlayer && cmpCapturable && cmpCapturable.CanCapture(cmpPlayer.GetPlayerID()))
    376             return "Capture";
    377         // not captureable, so remove this attack
    378         types.splice(captureIndex, 1);
     417            let cmpPlayer = QueryOwnerInterface(this.entity);
     418            if (cmpPlayer && cmpCapturable && cmpCapturable.CanCapture(cmpPlayer.GetPlayerID()))
     419                return "Capture";
     420            // not captureable, so remove this attack
     421            types.splice(captureIndex, 1);
     422        }
    379423    }
    380424
    381     let isPreferred = function (className) { return attack.GetPreferredClasses(className).some(isTargetClass); };
    382     let byPreference = function (a, b) { return (types.indexOf(a) + (isPreferred(a) ? types.length : 0) ) - (types.indexOf(b) + (isPreferred(b) ? types.length : 0) ); };
     425    // ignore charges for now: TODO implement these
     426    let chargeIndex = types.indexOf("Charge");
     427    if (chargeIndex != -1)
     428        types.splice(chargeIndex, 1);
    383429
    384     return types.sort(byPreference).pop();
     430    // only ranged and/or melee attack left
     431    // if one attacktype left choose this one
     432    if (types.indexOf("Melee") == -1 || types.indexOf("Ranged") == -1)
     433        return types[0];
     434
     435    if (this.HasPreferredClasses(types))
     436    {
     437        let isPreferred = function (className) { return attack.GetPreferredClasses(className).some(isTargetClass); };
     438        let byPreference = function (a, b) { return (types.indexOf(a) + (isPreferred(a) ? types.length : 0) ) - (types.indexOf(b) + (isPreferred(b) ? types.length : 0) ); };
     439
     440        return types.sort(byPreference).pop();
     441    }
     442        // assume ranged and melee attack
     443        // TODO stop assuming that?
     444    let cmpPosition = Engine.QueryInterface(this.entity, IID_Position);
     445    if (!cmpPosition || !cmpPosition.IsInWorld())
     446        return undefined;
     447    let selfPosition = cmpPosition.GetPosition();
     448    let cmpTargetPosition = Engine.QueryInterface(target, IID_Position);
     449    if (!cmpTargetPosition || !cmpTargetPosition.IsInWorld())
     450        return undefined;
     451    let targetPosition = cmpTargetPosition.GetPosition();
     452    let horizDistance = targetPosition.horizDistanceTo(selfPosition);
     453    if (horizDistance <= this.template.ChangeDistance)
     454        return "Melee";
     455    return "Ranged"
     456
    385457};
    386458
    387459Attack.prototype.CompareEntitiesByPreference = function(a, b)
     
    478550    return attackBonus;
    479551};
    480552
     553// Returns preferred attack type if exists
     554Attack.prototype.GetPrefAttack = function(types, pref)
     555{
     556    for (let type of types)
     557        if (this.template[type].AttackOrder && pref == this.template[type].AttackOrder)
     558            return type;
     559    return undefined;
     560};
     561
     562Attack.prototype.HasPreferredClasses = function(types)
     563{
     564    for (let type of types)
     565        if (this.template[type].PreferredClasses)
     566            return true;
     567    return false
     568};
    481569// Returns a 2d random distribution scaled for a spread of scale 1.
    482570// The current implementation is a 2d gaussian with sigma = 1
    483571Attack.prototype.GetNormalDistribution = function(){
     
    486574    let a = Math.random();
    487575    let b = Math.random();
    488576
    489     let c = Math.sqrt(-2*Math.log(a)) * Math.cos(2*Math.PI*b);
    490     let d = Math.sqrt(-2*Math.log(a)) * Math.sin(2*Math.PI*b);
     577    let c = Math.sqrt(-2 * Math.log(a)) * Math.cos(2 * Math.PI * b);
     578    let d = Math.sqrt(-2 * Math.log(a)) * Math.sin(2 * Math.PI * b);
    491579
    492580    return [c, d];
    493581};
     
    503591    if (type == "Ranged")
    504592    {
    505593        let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
    506         let turnLength = cmpTimer.GetLatestTurnLength()/1000;
     594        let turnLength = cmpTimer.GetLatestTurnLength() / 1000;
    507595        // In the future this could be extended:
    508596        //  * Obstacles like trees could reduce the probability of the target being hit
    509597        //  * Obstacles like walls should block projectiles entirely
     
    535623
    536624        let horizDistance = targetPosition.horizDistanceTo(selfPosition);
    537625
    538         // This is an approximation of the time ot the target, it assumes that the target has a constant radial
    539         // velocity, but since units move in straight lines this is not true.  The exact value would be more 
    540         // difficult to calculate and I think this is sufficiently accurate.  (I tested and for cavalry it was 
     626        // This is an approximation of the time to the target, it assumes that the target has a constant radial
     627        // velocity, but since units move in straight lines this is not true.  The exact value would be more
     628        // difficult to calculate and I think this is sufficiently accurate.  (I tested and for cavalry it was
    541629        // about 5% of the units radius out in the worst case)
    542630        let timeToTarget = horizDistance / (horizSpeed - radialSpeed);
    543631
     
    620708Attack.prototype.InterpolatedLocation = function(ent, lateness)
    621709{
    622710    let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
    623     let turnLength = cmpTimer.GetLatestTurnLength()/1000;
     711    let turnLength = cmpTimer.GetLatestTurnLength() / 1000;
    624712    let cmpTargetPosition = Engine.QueryInterface(ent, IID_Position);
    625713    if (!cmpTargetPosition || !cmpTargetPosition.IsInWorld()) // TODO: handle dead target properly
    626714        return undefined;
     
    658746        let d = Vector3D.sub(point, targetPosition);
    659747        d = Vector2D.from3D(d).rotate(-angle);
    660748
    661         return d.x < Math.abs(targetShape.width/2) && d.y < Math.abs(targetShape.depth/2);
     749        return d.x < Math.abs(targetShape.width / 2) && d.y < Math.abs(targetShape.depth / 2);
    662750    }
    663751};
    664752
     
    740828        return;
    741829
    742830    for (let type of this.GetAttackTypes())
    743         if (msg.valueNames.indexOf("Attack/"+type+"/MaxRange") !== -1)
     831        if (msg.valueNames.indexOf("Attack/" + type + "/MaxRange") !== -1)
    744832        {
    745833            cmpUnitAI.UpdateRangeQueries();
    746834            return;
  • binaries/data/mods/public/simulation/components/GuiInterface.js

     
    17481748    if (!cmpAttack)
    17491749        return false;
    17501750
    1751     let cmpEntityPlayer = QueryOwnerInterface(data.entity, IID_Player);
    1752     let cmpTargetPlayer = QueryOwnerInterface(data.target, IID_Player);
    1753     if (!cmpEntityPlayer || !cmpTargetPlayer)
     1751    return cmpAttack.CanAttack(data.target);
     1752};
     1753
     1754GuiInterface.prototype.CanSecondAttack = function(player, data)
     1755{
     1756    let cmpAttack = Engine.QueryInterface(data.entity, IID_Attack);
     1757    if (!cmpAttack)
    17541758        return false;
    17551759
    1756     // if the owner is an enemy, it's up to the attack component to decide
    1757     if (cmpEntityPlayer.IsEnemy(cmpTargetPlayer.GetPlayerID()))
    1758         return cmpAttack.CanAttack(data.target);
    1759 
    1760     return false;
     1760    return cmpAttack.CanSecondAttack(data.target)
    17611761};
    17621762
    17631763/*
     
    19031903    "GetTradingDetails": 1,
    19041904    "CanCapture": 1,
    19051905    "CanAttack": 1,
     1906    "CanSecondAttack": 1,
    19061907    "GetBatchTime": 1,
    19071908
    19081909    "IsMapRevealed": 1,
  • binaries/data/mods/public/simulation/components/UnitAI.js

     
    416416        }
    417417
    418418        // Work out how to attack the given target
    419         var type = this.GetBestAttackAgainst(this.order.data.target, this.order.data.allowCapture);
     419        let type = this.GetBestAttackAgainst(this.order.data.target, this.order.data.prefType);
    420420        if (!type)
    421421        {
    422422            // Oops, we can't attack at all
     
    583583                return;
    584584            }
    585585
    586             this.PushOrderFront("Attack", { "target": this.order.data.target, "force": false, "hunting": true, "allowCapture": false });
     586            this.PushOrderFront("Attack", { "target": this.order.data.target, "force": false, "hunting": true, "prefType": undefined });
    587587            return;
    588588        }
    589589
     
    838838        },
    839839
    840840        "Order.Attack": function(msg) {
    841             var target = msg.data.target;
    842             var allowCapture = msg.data.allowCapture;
    843             var cmpTargetUnitAI = Engine.QueryInterface(target, IID_UnitAI);
     841            let target = msg.data.target;
     842            let prefType = msg.data.prefType;
     843            let cmpTargetUnitAI = Engine.QueryInterface(target, IID_UnitAI);
    844844            if (cmpTargetUnitAI && cmpTargetUnitAI.IsFormationMember())
    845845                target = cmpTargetUnitAI.GetFormationController();
    846846
     
    859859                this.FinishOrder();
    860860                return;
    861861            }
    862             this.CallMemberFunction("Attack", [target, false, allowCapture]);
     862            this.CallMemberFunction("Attack", [target, false, prefType]);
    863863            if (cmpAttack.CanAttackAsFormation())
    864864                this.SetNextState("COMBAT.ATTACKING");
    865865            else
     
    914914                    return;
    915915                }
    916916
    917                 this.PushOrderFront("Attack", { "target": msg.data.target, "hunting": true, "allowCapture": false });
     917                this.PushOrderFront("Attack", { "target": msg.data.target, "hunting": true, "prefType": undefined });
    918918                return;
    919919            }
    920920
     
    11511151
    11521152                "MoveCompleted": function(msg) {
    11531153                    var cmpAttack = Engine.QueryInterface(this.entity, IID_Attack);
    1154                     this.CallMemberFunction("Attack", [this.order.data.target, false, this.order.data.allowCapture]);
     1154                    this.CallMemberFunction("Attack", [this.order.data.target, false, this.order.data.prefType]);
    11551155                    if (cmpAttack.CanAttackAsFormation())
    11561156                        this.SetNextState("COMBAT.ATTACKING");
    11571157                    else
     
    11621162            "ATTACKING": {
    11631163                // Wait for individual members to finish
    11641164                "enter": function(msg) {
    1165                     var target = this.order.data.target;
    1166                     var allowCapture = this.order.data.allowCapture;
     1165                    let target = this.order.data.target;
     1166                    let prefType = this.order.data.prefType;
    11671167                    // Check if we are already in range, otherwise walk there
    11681168                    if (!this.CheckTargetAttackRange(target, target))
    11691169                    {
     
    11701170                        if (this.TargetIsAlive(target) && this.CheckTargetVisible(target))
    11711171                        {
    11721172                            this.FinishOrder();
    1173                             this.PushOrderFront("Attack", { "target": target, "force": false, "allowCapture": allowCapture });
     1173                            this.PushOrderFront("Attack", { "target": target, "force": false, "prefType": prefType });
    11741174                            return true;
    11751175                        }
    11761176                        this.FinishOrder();
     
    11861186                },
    11871187
    11881188                "Timer": function(msg) {
    1189                     var target = this.order.data.target;
    1190                     var allowCapture = this.order.data.allowCapture;
     1189                    let target = this.order.data.target;
     1190                    let prefType = this.order.data.prefType;
    11911191                    // Check if we are already in range, otherwise walk there
    11921192                    if (!this.CheckTargetAttackRange(target, target))
    11931193                    {
     
    11941194                        if (this.TargetIsAlive(target) && this.CheckTargetVisible(target))
    11951195                        {
    11961196                            this.FinishOrder();
    1197                             this.PushOrderFront("Attack", { "target": target, "force": false, "allowCapture": allowCapture });
     1197                            this.PushOrderFront("Attack", { "target": target, "force": false, "prefType": prefType });
    11981198                            return;
    11991199                        }
    12001200                        this.FinishOrder();
     
    14061406
    14071407            // target the unit
    14081408            if (this.CheckTargetVisible(msg.data.attacker))
    1409                 this.PushOrderFront("Attack", { "target": msg.data.attacker, "force": false, "allowCapture": true });
     1409                this.PushOrderFront("Attack", { "target": msg.data.attacker, "force": false, "prefType": undefined });
    14101410            else
    14111411            {
    14121412                var cmpPosition = Engine.QueryInterface(msg.data.attacker, IID_Position);
     
    45354535    return distance < range;
    45364536};
    45374537
    4538 UnitAI.prototype.GetBestAttackAgainst = function(target, allowCapture)
     4538UnitAI.prototype.GetBestAttackAgainst = function(target, prefType)
    45394539{
    45404540    var cmpAttack = Engine.QueryInterface(this.entity, IID_Attack);
    45414541    if (!cmpAttack)
    45424542        return undefined;
    4543     return cmpAttack.GetBestAttackAgainst(target, allowCapture);
     4543    return cmpAttack.GetBestAttackAgainst(target, prefType);
    45444544};
    45454545
    45464546UnitAI.prototype.GetAttackBonus = function(type, target)
     
    45624562    if (!target)
    45634563        return false;
    45644564
    4565     this.PushOrderFront("Attack", { "target": target, "force": false, "forceResponse": forceResponse, "allowCapture": true });
     4565    this.PushOrderFront("Attack", { "target": target, "force": false, "forceResponse": forceResponse, "prefType": undefined });
    45664566    return true;
    45674567};
    45684568
     
    45754575{
    45764576    var target = ents.find(target =>
    45774577        this.CanAttack(target, forceResponse)
    4578         && this.CheckTargetDistanceFromHeldPosition(target, IID_Attack, this.GetBestAttackAgainst(target, true))
     4578        && this.CheckTargetDistanceFromHeldPosition(target, IID_Attack, this.GetBestAttackAgainst(target, true, undefined))
    45794579        && (this.GetStance().respondChaseBeyondVision || this.CheckTargetIsInVisionRange(target))
    45804580    );
    45814581    if (!target)
    45824582        return false;
    45834583
    4584     this.PushOrderFront("Attack", { "target": target, "force": false, "forceResponse": forceResponse, "allowCapture": true });
     4584    this.PushOrderFront("Attack", { "target": target, "force": false, "forceResponse": forceResponse, "prefType": undefined });
    45854585    return true;
    45864586};
    45874587
     
    49784978 * to a player order, and so is forced.
    49794979 * If targetClasses is given, only entities matching the targetClasses can be attacked.
    49804980 */
    4981 UnitAI.prototype.WalkAndFight = function(x, z, targetClasses, queued)
     4981UnitAI.prototype.WalkAndFight = function(x, z, targetClasses, queued, prefType)
    49824982{
    4983     this.AddOrder("WalkAndFight", { "x": x, "z": z, "targetClasses": targetClasses, "force": true }, queued);
     4983    this.AddOrder("WalkAndFight", { "x": x, "z": z, "targetClasses": targetClasses, "force": true, "prefType": prefType }, queued);
    49844984};
    49854985
    49864986/**
     
    50015001/**
    50025002 * Adds attack order to the queue, forced by the player.
    50035003 */
    5004 UnitAI.prototype.Attack = function(target, queued, allowCapture)
     5004UnitAI.prototype.Attack = function(target, queued, prefType)
    50055005{
    50065006    if (!this.CanAttack(target))
    50075007    {
     
    50135013            this.WalkToTarget(target, queued);
    50145014        return;
    50155015    }
    5016     this.AddOrder("Attack", { "target": target, "force": true, "allowCapture": allowCapture}, queued);
     5016    this.AddOrder("Attack", { "target": target, "force": true, "prefType": prefType }, queued);
    50175017};
    50185018
    50195019/**
     
    54305430                    if (targetClasses.vetoEntities && targetClasses.vetoEntities[targ])
    54315431                        continue;
    54325432                }
    5433                 this.PushOrderFront("Attack", { "target": targ, "force": true, "allowCapture": true });
     5433                this.PushOrderFront("Attack", { "target": targ, "force": true, "prefType": undefined });
    54345434                return true;
    54355435            }
    54365436        }
     
    54565456            if (targetClasses.vetoEntities && targetClasses.vetoEntities[targ])
    54575457                continue;
    54585458        }
    5459         this.PushOrderFront("Attack", { "target": targ, "force": true, "allowCapture": true });
     5459        this.PushOrderFront("Attack", { "target": targ, "force": true, "prefType": undefined });
    54605460        return true;
    54615461    }
    54625462    return false;
  • binaries/data/mods/public/simulation/helpers/Commands.js

     
    158158    "attack-walk": function(player, cmd, data)
    159159    {
    160160        GetFormationUnitAIs(data.entities, player).forEach(cmpUnitAI => {
    161             cmpUnitAI.WalkAndFight(cmd.x, cmd.z, cmd.targetClasses, cmd.queued);
     161            cmpUnitAI.WalkAndFight(cmd.x, cmd.z, cmd.targetClasses, cmd.queued, cmd.prefType);
    162162        });
    163163    },
    164164
     
    167167        if (g_DebugCommands && !(IsOwnedByEnemyOfPlayer(player, cmd.target) || IsOwnedByNeutralOfPlayer(player, cmd.target)))
    168168            warn("Invalid command: attack target is not owned by enemy of player "+player+": "+uneval(cmd));
    169169
    170         let allowCapture = cmd.allowCapture || cmd.allowCapture == null;
    171170        // See UnitAI.CanAttack for target checks
    172171        GetFormationUnitAIs(data.entities, player).forEach(cmpUnitAI => {
    173             cmpUnitAI.Attack(cmd.target, cmd.queued, allowCapture);
     172            cmpUnitAI.Attack(cmd.target, cmd.queued, cmd.prefType);
    174173        });
    175174    },
    176175
  • binaries/data/mods/public/simulation/templates/template_unit_champion_infantry_spearman.xml

     
    55    <Pierce op="add">3</Pierce>
    66  </Armour>
    77  <Attack>
     8    <ChangeDistance>20</ChangeDistance>
    89    <Melee>
     10      <AttackOrder>primary</AttackOrder>
    911      <Hack>6.0</Hack>
    1012      <Pierce>5.0</Pierce>
    1113      <Crush>0.0</Crush>
     
    1820          </BonusCavMelee>
    1921      </Bonuses>
    2022    </Melee>
     23    <Ranged>
     24      <AttackOrder>secondary</AttackOrder>
     25      <Hack>0</Hack>
     26      <Pierce>6.0</Pierce>
     27      <Crush>0</Crush>
     28      <MaxRange>72.0</MaxRange>
     29      <MinRange>0.0</MinRange>
     30      <ProjectileSpeed>120.0</ProjectileSpeed>
     31      <PrepareTime>1000</PrepareTime>
     32      <RepeatTime>1000</RepeatTime>
     33      <Spread>2.0</Spread>
     34    </Ranged>
    2135    <Charge>
    2236      <Hack>15.0</Hack>
    2337      <Pierce>40.0</Pierce>