Ticket #252: t252_secondattack_19.diff

File t252_secondattack_19.diff, 66.8 KB (added by elexis, 8 years ago)

all your rebase are belong to us. also removed unneeded else

  • binaries/data/config/default.cfg

    offscreen = Alt ; Include o  
    2762769 = 9
    277277
    278278[hotkey.session]
    279279kill = Delete                ; Destroy selected units
    280280stop = "H"                   ; Stop the current action
    281 attack = Ctrl                ; Modifier to attack instead of another action (eg capture)
    282 attackmove = Ctrl            ; Modifier to attackmove when clicking on a point
    283 attackmoveUnit = "Ctrl+Q"    ; Modifier to attackmove targeting only units when clicking on a point (should contain the attackmove keys)
     281attack = Ctrl                ; Modifier to primary attack instead of another action (eg capture)
     282attackmove = Ctrl            ; Modifier to primary attackmove when clicking on a point
     283attackmoveUnit = "Ctrl+Q"    ; Modifier to primary attackmove targeting only units when clicking on a point (should contain the attackmove keys)
    284284garrison = Ctrl              ; Modifier to garrison when clicking on building
    285285autorallypoint = Ctrl        ; Modifier to set the rally point on the building itself
     286meleeattack = Alt            ; Modifier to melee attack instead of another action
     287meleeattackmove = Alt        ; Modifier to melee attackmove when clicking on a point
     288meleeattackmoveUnit = "Alt+Q"; Modifier to melee attackmove targeting only units when clicking on a point
     289rangedattack = space         ; Modifier to ranged attack instead of another action
     290rangedattackmove = space     ; Modifier to ranged attackmove when clicking on a point
     291rangedattackmoveUnit = "space+Q"; Modifier to ranged attackmove targeting only units when clicking on a point
    286292guard = "G"                  ; Modifier to escort/guard when clicking on unit/building
    287293queue = Shift                ; Modifier to queue unit orders instead of replacing
    288294batchtrain = Shift           ; Modifier to train units in batches
    289295massbarter = Shift           ; Modifier to barter bunch of resources
    290296masstribute = Shift          ; Modifier to tribute bunch of resources
  • binaries/data/mods/public/art/actors/units/persians/champion_unit_1.xml

     
    4040    </variant>
    4141  </group>
    4242  <group>
    4343    <variant frequency="1" name="Idle"/>
    4444    <variant file="biped/attack_capture.xml"/>
     45    <variant file="biped/attack_ranged_archer.xml">
     46      <props>
     47        <prop attachpoint="r_hand"/>
     48        <prop actor="props/units/weapons/bow_recurve.xml" attachpoint="l_hand"/>
     49      </props>
     50    </variant>
    4551  </group>
    4652  <material>player_trans.xml</material>
    4753</actor>
  • binaries/data/mods/public/globalscripts/Templates.js

    function MatchesClassList(classes, match  
    6464
    6565    return false;
    6666}
    6767
    6868/**
     69 * Check if entity has all needed attack types.
     70 */
     71function HasNeededAttackTypes(attack, neededAttackTypes)
     72{
     73    if (!neededAttackTypes)
     74        return true;
     75    // transform the string to an array
     76    if (typeof neededAttackTypes == "string")
     77        neededAttackTypes = neededAttackTypes.split(/\s+/);
     78
     79    for (let type of neededAttackTypes)
     80        if (!attack || attack.constructor === Object && !attack[type] ||
     81            attack.constructor === Array && attack.indexOf(type) == -1)
     82            return false;
     83    return true;
     84}
     85
     86/**
    6987 * Get information about a template with or without technology modifications.
    7088 * @param template A valid template as returned by the template loader.
    7189 * @param player An optional player id to get the technology modifications
    7290 *               of properties.
    7391 * @param auraTemplates An object in the form of {key: {auraName: "", auraDescription: ""}}
  • binaries/data/mods/public/gui/common/tooltips.js

    function getAttackTooltip(template)  
    144144        return "";
    145145
    146146    let attacks = [];
    147147    for (let type in template.attack)
    148148    {
     149        if (type == "ChangeDistance")
     150            continue; // not an attack type
    149151        if (type == "Slaughter")
    150152            continue; // Slaughter is used to kill animals, so do not show it.
    151153        if (type == "Charge")
    152154            continue; // Charging isn't implemented yet and shouldn't be displayed.
    153155
  • binaries/data/mods/public/gui/session/input.js

    function getActionInfo(action, target)  
    203203            {
    204204                data.command = "attack-walk";
    205205                data.targetClasses = Engine.HotkeyIsPressed("session.attackmoveUnit") ? { "attack": ["Unit"] } : { "attack": ["Unit", "Structure"] };
    206206                cursor = "action-attack-move";
    207207            }
     208
     209            if (Engine.HotkeyIsPressed("session.meleeattackmove"))
     210            {
     211                data.command = "attack-walk";
     212                data.targetClasses = Engine.HotkeyIsPressed("session.meleeattackmoveUnit") ? { "attack": ["Unit"] } : { "attack": ["Unit", "Structure"] };
     213                cursor = "action-melee-attack-move";
     214            }
     215
     216            if (Engine.HotkeyIsPressed("session.rangedattackmove"))
     217            {
     218                data.command = "attack-walk";
     219                data.targetClasses = Engine.HotkeyIsPressed("session.rangedattackmoveUnit") ? { "attack": ["Unit"] } : { "attack": ["Unit", "Structure"] };
     220                cursor = "action-ranged-attack-move";
     221            }
     222
    208223            return { "possible": true, "data": data, "cursor": cursor };
    209224        }
    210225
    211226        return { "possible": (action == "move" || action == "attack-move" || action == "remove-guard") };
    212227    }
    function getActionInfo(action, target)  
    217232    var targetState = GetExtendedEntityState(target);
    218233
    219234    // Check if the target entity is a resource, dropsite, foundation, or enemy unit.
    220235    // Check if any entities in the selection can gather the requested resource,
    221236    // can return to the dropsite, can build the foundation, or can attack the enemy
    222     for each (var entityID in selection)
     237    for (let entityID of selection)
    223238    {
    224         var entState = GetExtendedEntityState(entityID);
     239        let entState = GetExtendedEntityState(entityID);
    225240        if (!entState)
    226241            continue;
    227242
    228243        if (unitActions[action] && unitActions[action].getActionInfo)
    229244        {
    230245            var r = unitActions[action].getActionInfo(entState, targetState, simState);
    231             if (r) // return true if it's possible for one of the entities
     246            if (r && r.possible) // return true if it's possible for one of the entities
    232247                return r;
    233248        }
    234249    }
    235250    return { "possible": false };
    236251}
  • binaries/data/mods/public/gui/session/unit_actions.js

    var unitActions =  
    5656            if (!someUnitAI(selection) || !getActionInfo("move", target).possible)
    5757                return false;
    5858
    5959            return { "type": "move" };
    6060        },
    61         "specificness": 12,
     61        "specificness": 13,
    6262    },
    6363
    6464    "attack-move":
    6565    {
    6666        "execute": function(target, action, selection, queued)
    var unitActions =  
    7575                "type": "attack-walk",
    7676                "entities": selection,
    7777                "x": target.x,
    7878                "z": target.z,
    7979                "targetClasses": targetClasses,
    80                 "queued": queued
     80                "queued": queued,
     81                "prefAttackType": "primary"
    8182            });
    8283
    8384            Engine.GuiInterfaceCall("PlaySound", {
    8485                "name": "order_walk",
    8586                "entity": selection[0]
    var unitActions =  
    104105            };
    105106        },
    106107        "specificness": 30,
    107108    },
    108109
     110    "melee-attack-move":
     111    {
     112        "execute": function(target, action, selection, queued)
     113        {
     114            let targetClasses;
     115            if (Engine.HotkeyIsPressed("session.meleeattackmoveUnit"))
     116                targetClasses = { "meleeattack": ["Unit"] };
     117            else
     118                targetClasses = { "meleeattack": ["Unit", "Structure"] };
     119
     120            Engine.PostNetworkCommand({
     121                "type": "attack-walk",
     122                "entities": selection,
     123                "x": target.x,
     124                "z": target.z,
     125                "targetClasses": targetClasses,
     126                "queued": queued,
     127                "prefAttackType": "Melee"
     128            });
     129
     130            Engine.GuiInterfaceCall("PlaySound", {
     131                "name": "order_walk",
     132                "entity": selection[0]
     133            });
     134
     135            return true;
     136        },
     137        "getActionInfo": function(entState, targetState)
     138        {
     139            return { "possible": true };
     140        },
     141        "hotkeyActionCheck": function(target, selection)
     142        {
     143            if (someUnitAI(selection) && Engine.HotkeyIsPressed("session.meleeattackmove") &&
     144                getActionInfo("melee-attack-move", target).possible)
     145                return {
     146                    "type": "melee-attack-move",
     147                    "cursor": "action-melee-attack"
     148                };
     149            return false;
     150        },
     151        "specificness": 31,
     152    },
     153
     154    "ranged-attack-move":
     155    {
     156        "execute": function(target, action, selection, queued)
     157        {
     158            let targetClasses;
     159            if (Engine.HotkeyIsPressed("session.rangedattackmoveUnit"))
     160                targetClasses = { "rangedattack": ["Unit"] };
     161            else
     162                targetClasses = { "rangedattack": ["Unit", "Structure"] };
     163
     164            Engine.PostNetworkCommand({
     165                "type": "attack-walk",
     166                "entities": selection,
     167                "x": target.x,
     168                "z": target.z,
     169                "targetClasses": targetClasses,
     170                "queued": queued,
     171                "prefAttackType": "Ranged"
     172            });
     173
     174            Engine.GuiInterfaceCall("PlaySound", {
     175                "name": "order_walk",
     176                "entity": selection[0]
     177            });
     178
     179            return true;
     180        },
     181        "getActionInfo": function(entState, targetState)
     182        {
     183            return { "possible": true };
     184        },
     185        "hotkeyActionCheck": function(target, selection)
     186        {
     187            if (someUnitAI(selection) && Engine.HotkeyIsPressed("session.rangedattackmove") &&
     188                getActionInfo("ranged-attack-move", target).possible)
     189                return {
     190                    "type": "ranged-attack-move",
     191                    "cursor": "action-ranged-attack"
     192                };
     193            return false;
     194        },
     195        "specificness": 32,
     196    },
     197
    109198    "capture":
    110199    {
    111200        "execute": function(target, action, selection, queued)
    112201        {
    113202            Engine.PostNetworkCommand({
    114203                "type": "attack",
    115204                "entities": selection,
    116205                "target": action.target,
    117206                "allowCapture": true,
    118                 "queued": queued
     207                "queued": queued,
     208                "prefAttackType": "Capture"
    119209            });
    120210
    121211            Engine.GuiInterfaceCall("PlaySound", {
    122212                "name": "order_attack",
    123213                "entity": selection[0]
    var unitActions =  
    146236                "type": "capture",
    147237                "cursor": "action-capture",
    148238                "target": target
    149239            };
    150240        },
    151         "specificness": 9,
     241        "specificness": 7,
    152242    },
    153243
    154244    "attack":
    155245    {
    156246        "execute": function(target, action, selection, queued)
    var unitActions =  
    158248            Engine.PostNetworkCommand({
    159249                "type": "attack",
    160250                "entities": selection,
    161251                "target": action.target,
    162252                "queued": queued,
    163                 "allowCapture": false
     253                "allowCapture": false,
     254                "prefAttackType": "primary"
    164255            });
    165256
    166257            Engine.GuiInterfaceCall("PlaySound", {
    167258                "name": "order_attack",
    168259                "entity": selection[0]
    var unitActions =  
    203294                "type": "attack",
    204295                "cursor": "action-attack",
    205296                "target": target
    206297            };
    207298        },
     299        "specificness": 8,
     300    },
     301
     302    "melee-attack":
     303    {
     304        "execute": function(target, action, selection, queued)
     305        {
     306            Engine.PostNetworkCommand({
     307                "type": "attack",
     308                "entities": selection,
     309                "target": action.target,
     310                "queued": queued,
     311                "prefAttackType": "Melee"
     312            });
     313
     314            Engine.GuiInterfaceCall("PlaySound", {
     315                "name": "order_attack",
     316                "entity": selection[0]
     317            });
     318            return true;
     319        },
     320        "getActionInfo": function(entState, targetState)
     321        {
     322            if (!entState.attack || !targetState.hitpoints)
     323                return false;
     324            return { "possible": Engine.GuiInterfaceCall("CanAttackWithType", {
     325                "entity": entState.id,
     326                "target": targetState.id,
     327                "type": "Melee"
     328            }) };
     329        },
     330        "hotkeyActionCheck": function(target)
     331        {
     332            if (Engine.HotkeyIsPressed("session.meleeattack") && getActionInfo("melee-attack", target).possible)
     333                return {
     334                    "type": "melee-attack",
     335                    "cursor": "action-melee-attack",
     336                    "target": target
     337                };
     338            return false;
     339        },
     340        "actionCheck": function(target)
     341        {
     342            if (getActionInfo("melee-attack", target).possible)
     343                return {
     344                    "type": "melee-attack",
     345                    "cursor": "action-melee-attack",
     346                    "target": target
     347                };
     348            return false;
     349        },
     350        "specificness": 9,
     351    },
     352
     353    "ranged-attack":
     354    {
     355        "execute": function(target, action, selection, queued)
     356        {
     357            Engine.PostNetworkCommand({
     358                "type": "attack",
     359                "entities": selection,
     360                "target": action.target,
     361                "queued": queued,
     362                "prefAttackType": "Ranged"
     363            });
     364
     365            Engine.GuiInterfaceCall("PlaySound", {
     366                "name": "order_attack",
     367                "entity": selection[0]
     368            });
     369
     370            return true;
     371        },
     372        "getActionInfo": function(entState, targetState)
     373        {
     374            if (!entState.attack || !targetState.hitpoints) // hack
     375                return false;
     376            return { "possible": Engine.GuiInterfaceCall("CanAttackWithType", {
     377                "entity": entState.id,
     378                "target": targetState.id,
     379                "type": "Ranged"
     380            }) };
     381        },
     382        "hotkeyActionCheck": function(target, selection)
     383        {
     384            if (Engine.HotkeyIsPressed("session.rangedattack") && getActionInfo("ranged-attack", target).possible)
     385                return {
     386                    "type": "ranged-attack",
     387                    "cursor": "action-ranged-attack",
     388                    "target": target
     389                };
     390            return false;
     391        },
     392        "actionCheck": function(target, selection)
     393        {
     394            if (getActionInfo("ranged-attack", target).possible)
     395                return {
     396                    "type": "ranged-attack",
     397                    "cursor": "action-ranged-attack",
     398                    "target": target
     399                };
     400            return false;
     401        },
    208402        "specificness": 10,
    209403    },
    210404
    211405    "heal":
    212406    {
    var unitActions =  
    253447                "type": "heal",
    254448                "cursor": "action-heal",
    255449                "target": target
    256450            };
    257451        },
    258         "specificness": 7,
     452        "specificness": 6,
    259453    },
    260454
    261455    "build":
    262456    {
    263457        "execute": function(target, action, selection, queued)
    var unitActions =  
    353547                "type": "build",
    354548                "cursor": "action-repair",
    355549                "target": target
    356550            };
    357551        },
    358         "specificness": 11,
     552        "specificness": 12,
    359553    },
    360554
    361555    "gather":
    362556    {
    363557        "execute": function(target, action, selection, queued)
    var unitActions =  
    593787                extraCount += entState.garrisonHolder.garrisonedEntitiesCount;
    594788
    595789            if (targetState.garrisonHolder.garrisonedEntitiesCount + extraCount >= targetState.garrisonHolder.capacity)
    596790                tooltip = "[color=\"orange\"]" + tooltip + "[/color]";
    597791
    598             if (!MatchesClassList(entState.identity.classes, targetState.garrisonHolder.allowedClasses))
    599                 return false;
     792            if (MatchesClassList(entState.identity.classes, targetState.garrisonHolder.allowedClasses) &&
     793                HasNeededAttackTypes(entState.attack, targetState.garrisonHolder.neededAttackTypes))
     794                return {
     795                    "possible": true,
     796                    "tooltip": tooltip
     797                };
    600798
    601             return {
    602                 "possible": true,
    603                 "tooltip": tooltip
    604             };
     799            return { "possible": false };
    605800        },
    606801        "preSelectedActionCheck": function(target)
    607802        {
    608803            if (preSelectedAction != ACTION_GARRISON)
    609804                return false;
    var unitActions =  
    781976                data.command = "attack-walk";
    782977                data.targetClasses = targetClasses;
    783978                cursor = "action-attack-move";
    784979            }
    785980
     981            if (Engine.HotkeyIsPressed("session.meleeattackmove"))
     982            {
     983                let targetClasses;
     984                if (Engine.HotkeyIsPressed("session.meleeattackmoveUnit"))
     985                    targetClasses = { "melee-attack": ["Unit"] };
     986                else
     987                    targetClasses = { "melee-attack": ["Unit", "Structure"] };
     988                data.command = "melee-attack-walk";
     989                data.targetClasses = targetClasses;
     990                cursor = "action-melee-attack";
     991            }
     992
     993            if (Engine.HotkeyIsPressed("session.rangedattackmove"))
     994            {
     995                let targetClasses;
     996                if (Engine.HotkeyIsPressed("session.rangedattackmoveUnit"))
     997                    targetClasses = { "ranged-attack": ["Unit"] };
     998                else
     999                    targetClasses = { "ranged-attack": ["Unit", "Structure"] };
     1000                data.command = "ranged-attack-walk";
     1001                data.targetClasses = targetClasses;
     1002                cursor = "action-ranged-attack";
     1003            }
     1004
    7861005            if (targetState.garrisonHolder &&
    7871006                playerCheck(entState, targetState, ["Player", "MutualAlly"]))
    7881007            {
    7891008                data.command = "garrison";
    7901009                data.target = targetState.id;
    var unitActions =  
    8991118                "data": actionInfo.data,
    9001119                "tooltip": actionInfo.tooltip,
    9011120                "position": actionInfo.position
    9021121            };
    9031122        },
    904         "specificness": 6,
     1123        "specificness": 5,
    9051124    },
    9061125
    9071126    "unset-rallypoint":
    9081127    {
    9091128        "execute": function(target, action, selection, queued)
  • binaries/data/mods/public/simulation/ai/common-api/entity.js

    m.Entity = m.Class({  
    744744            if (r.split('.')[0] === type)
    745745                return true;
    746746        return false;
    747747    },
    748748
     749    isCapturable: function() { return this.get("Capturable") !== undefined; },
    749750    isGarrisonHolder: function() { return this.get("GarrisonHolder") !== undefined; },
    750751    garrisoned: function() { return this._entity.garrisoned; },
    751752    canGarrisonInside: function() { return this._entity.garrisoned.length < this.garrisonMax(); },
    752753
    753754    move: function(x, z, queued = false) {
    754755        Engine.PostCommand(PlayerID,{"type": "walk", "entities": [this.id()], "x": x, "z": z, "queued": queued });
    755756        return this;
    756757    },
    757758
    758759    moveToRange: function(x, z, min, max, queued = false) {
    759         Engine.PostCommand(PlayerID,{"type": "walk-to-range", "entities": [this.id()], "x": x, "z": z, "min": min, "max": max, "queued": queued });
     760        Engine.PostCommand(PlayerID,{ "type": "walk-to-range", "entities": [this.id()], "x": x, "z": z, "min": min, "max": max, "queued": queued });
    760761        return this;
    761762    },
    762763
    763     attackMove: function(x, z, targetClasses, queued = false) {
    764         Engine.PostCommand(PlayerID,{"type": "attack-walk", "entities": [this.id()], "x": x, "z": z, "targetClasses": targetClasses, "queued": queued });
     764    attackMove: function(x, z, targetClasses, prefAttackType = "undefined", queued = false) { // hack
     765        Engine.PostCommand(PlayerID,{"type": "attack-walk", "entities": [this.id()], "x": x, "z": z, "targetClasses": targetClasses, "prefAttackType": prefAttackType, "queued": queued});
    765766        return this;
    766767    },
    767768
    768769    // violent, aggressive, defensive, passive, standground
    769770    setStance: function(stance, queued = false) {
    m.Entity = m.Class({  
    793794    garrison: function(target, queued = false) {
    794795        Engine.PostCommand(PlayerID,{"type": "garrison", "entities": [this.id()], "target": target.id(),"queued": queued});
    795796        return this;
    796797    },
    797798
    798     attack: function(unitId, allowCapture = true, queued = false) {
    799         Engine.PostCommand(PlayerID,{"type": "attack", "entities": [this.id()], "target": unitId, "allowCapture": allowCapture, "queued": queued});
     799    attack: function(unitId, prefAttackType = undefined, queued = false) {
     800        Engine.PostCommand(PlayerID,{"type": "attack", "entities": [this.id()], "target": unitId, "prefAttackType": prefAttackType, "queued": queued});
    800801        return this;
    801802    },
    802803
    803804    // moveApart from a point in the opposite direction with a distance dist
    804805    moveApart: function(point, dist) {
  • binaries/data/mods/public/simulation/ai/common-api/entitycollection.js

    m.EntityCollection.prototype.move = func  
    154154};
    155155
    156156m.EntityCollection.prototype.attackMove = function(x, z, targetClasses, queued)
    157157{
    158158    queued = queued || false;
    159     Engine.PostCommand(PlayerID,{"type": "attack-walk", "entities": this.toIdArray(), "x": x, "z": z, "targetClasses": targetClasses, "queued": queued});
     159    Engine.PostCommand(PlayerID, { "type": "attack-walk", "entities": this.toIdArray(), "x": x, "z": z, "targetClasses": targetClasses, "queued": queued, "prefAttackType": undefined }); // hack
    160160    return this;
    161161};
    162162
    163163m.EntityCollection.prototype.moveIndiv = function(x, z, queued)
    164164{
    m.EntityCollection.prototype.destroy = f  
    180180    return this;
    181181};
    182182
    183183m.EntityCollection.prototype.attack = function(unitId)
    184184{
    185     Engine.PostCommand(PlayerID,{"type": "attack", "entities": this.toIdArray(), "target": unitId, "queued": false});
     185    Engine.PostCommand(PlayerID,{"type": "attack", "entities": this.toIdArray(), "target": unitId, "queued": false, "prefAttackType": undefined});
    186186    return this;
    187187};
    188188
    189189/** violent, aggressive, defensive, passive, standground */
    190190m.EntityCollection.prototype.setStance = function(stance)
  • binaries/data/mods/public/simulation/ai/petra/attackPlan.js

    m.AttackPlan.prototype.update = function  
    12351235                let collec = this.unitCollection.filter(API3.Filters.not(API3.Filters.byClass("Siege"))).filterNearest(ourUnit.position(), 5);
    12361236                for (let ent of collec.values())
    12371237                {
    12381238                    if (this.isSiegeUnit(gameState, ent))   // needed as mauryan elephants are not filtered out
    12391239                        continue;
    1240                     ent.attack(attacker.id(), !this.noCapture.has(attacker.id()));
     1240                    ent.attack(attacker.id(), m.getPrefAttackType(ent, attacker, this.noCapture));
    12411241                    ent.setMetadata(PlayerID, "lastAttackPlanUpdateTime", time);
    12421242                }
    12431243                // And if this attacker is a non-ranged siege unit and our unit also, attack it
    12441244                if (this.isSiegeUnit(gameState, attacker) && attacker.hasClass("Melee") && ourUnit.hasClass("Melee"))
    12451245                {
    1246                     ourUnit.attack(attacker.id(), false);
     1246                    ourUnit.attack(attacker.id(), "Melee");
    12471247                    ourUnit.setMetadata(PlayerID, "lastAttackPlanUpdateTime", time);
    12481248                }
    12491249            }
    12501250            else
    12511251            {
    m.AttackPlan.prototype.update = function  
    12581258                else if (this.isSiegeUnit(gameState, attacker))
    12591259                {   // if our unit is attacked by a siege unit, we'll send some melee units to help it.
    12601260                    let collec = this.unitCollection.filter(API3.Filters.byClass("Melee")).filterNearest(ourUnit.position(), 5);
    12611261                    for (let ent of collec.values())
    12621262                    {
    1263                         ent.attack(attacker.id(), false);
     1263                        ent.attack(attacker.id(), m.getPrefAttackType(ent, attacker, this.noCapture));
    12641264                        ent.setMetadata(PlayerID, "lastAttackPlanUpdateTime", time);
    12651265                    }
    12661266                }
    12671267                else
    12681268                {   // if units are attacked, abandon their target (if it was a structure or a support) and retaliate
    m.AttackPlan.prototype.update = function  
    12771277                        {
    12781278                            if (!target.hasClass("Ranged") || !attacker.hasClass("Melee"))
    12791279                                continue;
    12801280                        }
    12811281                    }
    1282                     ourUnit.attack(attacker.id(), !this.noCapture.has(attacker.id()));
     1282                    ourUnit.attack(attacker.id(), m.getPrefAttackType(ourUnit, attacker, this.noCapture));
    12831283                    ourUnit.setMetadata(PlayerID, "lastAttackPlanUpdateTime", time);
    12841284                }
    12851285            }
    12861286        }
    12871287
    m.AttackPlan.prototype.update = function  
    14571457                        else if (structb.hasClass("ConquestCritical"))
    14581458                            valb += 200;
    14591459                        return valb - vala;
    14601460                    });
    14611461                    if (mStruct[0].hasClass("Gates"))
    1462                         ent.attack(mStruct[0].id(), false);
     1462                        ent.attack(mStruct[0].id(), m.getPrefAttackType(ent, mStruct[0], this.noCapture));
    14631463                    else
    14641464                    {
    14651465                        let rand = Math.floor(Math.random() * mStruct.length * 0.2);
    1466                         let newTargetId = mStruct[rand].id();
    1467                         ent.attack(newTargetId, !this.noCapture.has(newTargetId));
     1466                        let newTarget = mStruct[rand];
     1467                        ent.attack(newTarget.id(), m.getPrefAttackType(ent, newTarget, this.noCapture));
    14681468                    }
    14691469                }
    14701470                else
    14711471                {
    14721472                    if (!ent.hasClass("Ranged"))
    m.AttackPlan.prototype.update = function  
    15201520                        if (veto[unitB.id()])
    15211521                            valb -= 20000;
    15221522                        return valb - vala;
    15231523                    });
    15241524                    let rand = Math.floor(Math.random() * mUnit.length * 0.1);
    1525                     let newTargetId = mUnit[rand].id();
    1526                     ent.attack(newTargetId, !this.noCapture.has(newTargetId));
     1525                    let newTarget = mUnit[rand];
     1526                    ent.attack(newTarget.id(), m.getPrefAttackType(ent, newTarget, this.noCapture));
    15271527                }
    15281528                else if (this.isBlocked)
    15291529                    ent.attack(this.target.id(), false);
    15301530                else if (API3.SquareVectorDistance(this.targetPos, ent.position()) > 2500 )
    15311531                {
    m.AttackPlan.prototype.update = function  
    15681568                            else if (structb.hasClass("ConquestCritical"))
    15691569                                valb += 100;
    15701570                            return valb - vala;
    15711571                        });
    15721572                        if (mStruct[0].hasClass("Gates"))
    1573                             ent.attack(mStruct[0].id(), false);
     1573                            ent.attack(mStruct[0].id(), m.getPrefAttackType(ent, mStruct[0], this.noCapture));
    15741574                        else
    15751575                        {
    15761576                            let rand = Math.floor(Math.random() * mStruct.length * 0.2);
    1577                             let newTargetId = mStruct[rand].id();
    1578                             ent.attack(newTargetId, !this.noCapture.has(newTargetId));
     1577                            let newTarget = mStruct[rand];
     1578                            ent.attack(newTarget.id(), m.getPrefAttackType(ent, newTarget, this.noCapture));
    15791579                        }
    15801580                    }
    15811581                    else if (needsUpdate)  // really nothing   let's try to help our nearest unit
    15821582                    {
    15831583                        let distmin = Math.min();
    15841584                        let attackerId;
     1585                        let attacker;
    15851586                        this.unitCollection.forEach( function (unit) {
    15861587                            if (!unit.position())
    15871588                                return;
    15881589                            if (unit.unitAIState().split(".")[1] !== "COMBAT" || !unit.unitAIOrderData().length ||
    15891590                                !unit.unitAIOrderData()[0].target)
    15901591                                return;
    15911592                            let dist = API3.SquareVectorDistance(unit.position(), ent.position());
    15921593                            if (dist > distmin)
    15931594                                return;
     1595                            if (!gameState.getEntityById(unit.unitAIOrderData()[0].target))
     1596                                return;
    15941597                            distmin = dist;
    15951598                            attackerId = unit.unitAIOrderData()[0].target;
     1599                            attacker = gameState.getEntityById(attackerId);
    15961600                        });
    15971601                        if (attackerId)
    1598                             ent.attack(attackerId, !this.noCapture.has(attackerId));
     1602                            ent.attack(attackerId, m.getPrefAttackType(ent, attacker, this.noCapture));
    15991603                    }
    16001604                }
    16011605            }
    16021606        }
    16031607        this.unitCollUpdateArray.splice(0, lgth);
    m.AttackPlan.prototype.UpdateTransportin  
    16461650        {
    16471651            if (ent.getMetadata(PlayerID, "transport") !== undefined)
    16481652                continue;
    16491653            if (!ent.isIdle())
    16501654                continue;
    1651             ent.attack(attacker.id(), !this.noCapture.has(attacker.id()));
     1655            ent.attack(attacker.id(), !this.noCapture.has(attacker.id(), this.noCapture));
    16521656        }
    16531657        break;
    16541658    }
    16551659};
    16561660
    m.AttackPlan.prototype.CheckCapture = fu  
    20042008    if (!target)
    20052009        return false;
    20062010
    20072011    if (this.noCapture.has(targetId))
    20082012    {
    2009         ent.attack(targetId, false);
     2013        ent.attack(targetId, m.getPrefAttackType(ent, target, this.noCapture));
    20102014        return true;
    20112015    }
    20122016
    20132017    // Do not try to (re)capture an allied decaying structuring
    20142018    if (gameState.isPlayerAlly(target.owner()))
    m.AttackPlan.prototype.CheckCapture = fu  
    20162020
    20172021    // For the time being, do not try to capture rams
    20182022    if (target.hasClass("Siege") && target.hasClass("Melee"))
    20192023    {
    20202024        this.noCapture.add(targetId);
    2021         ent.attack(targetId, false);
     2025        ent.attack(targetId, m.getPrefAttackType(ent, target, this.noCapture));
    20222026        return true;
    20232027    }
    20242028
    20252029    // TODO need to know how many units are currently capturing this target
    20262030    // For the time being, we check on our full army
    m.AttackPlan.prototype.CheckCapture = fu  
    20332037    if (target.decaying())
    20342038        antiCapture -= target.territoryDecayRate();
    20352039    if (antiCapture >= this.captureStrength)
    20362040    {
    20372041        this.noCapture.add(targetId);
    2038         ent.attack(targetId, false);
     2042        ent.attack(targetId, "primary"); // hack
    20392043        return true;
    20402044    }
    20412045
    20422046    // If the structure has defensive fire, require a minimal army size
    20432047    if (target.hasDefensiveFire() && target.isGarrisonHolder() && target.garrisoned() &&
    20442048        this.unitCollection.length < 2*target.garrisoned().length)
    20452049    {
    20462050        this.noCapture.add(targetId);
    2047         ent.attack(targetId, false);
     2051        ent.attack(targetId, "primary"); // hack
    20482052        return true;
    20492053    }
    20502054
    20512055    return true;
    20522056};
  • binaries/data/mods/public/simulation/ai/petra/defenseArmy.js

    m.DefenseArmy.prototype.assignUnit = fun  
    7575    let foeIndex = gameState.ai.accessibility.getAccessValue(foePosition);
    7676    if (ownIndex == foeIndex || ent.hasClass("Ship"))
    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.getPrefAttackType(ent, foeEnt), queued);
    8181    }
    8282    else
    8383        gameState.ai.HQ.navalManager.requireTransport(gameState, ent, ownIndex, foeIndex, foePosition);
    8484    return true;
    8585};
    m.DefenseArmy.prototype.update = functio  
    113113        if (!orderData.length && !ent.getMetadata(PlayerID, "transport"))
    114114            this.assignUnit(gameState, entId);
    115115        else if (orderData.length && orderData[0].target && orderData[0].attackType && orderData[0].attackType === "Capture")
    116116        {
    117117            let target = gameState.getEntityById(orderData[0].target);
    118             if (target && !m.allowCapture(ent, target))
    119                 ent.attack(orderData[0].target, false);
     118            if (target)
     119            {
     120                let prefAttackType = m.getPrefAttackType(ent, target);
     121                if (prefAttackType !== "Capture")
     122                    ent.attack(orderData[0].target, prefAttackType);
     123            }
    120124        }
    121125    }
    122126
    123127    return this.onUpdate(gameState);
    124128};
  • binaries/data/mods/public/simulation/ai/petra/entityExtend.js

    m.getMaxStrength = function(ent, against  
    7979    }
    8080
    8181    return strength * ent.maxHitpoints() / 100.0;
    8282};
    8383
    84 /** Decide if we should try to capture or destroy */
    85 m.allowCapture = function(ent, target)
     84// Decide if we should try to capture, melee or range attack
     85// TODO make this function less hacky
     86m.getPrefAttackType = function(ent, target, noCapture)
    8687{
    87     return !target.hasClass("Siege") || !ent.hasClass("Melee") ||
    88         !target.isGarrisonHolder() || !target.garrisoned().length;
     88    if (target.hasClass("Siege") || (noCapture && noCapture.has(target.id())))
     89        return ent.hasClass("Melee") ? "Melee" : "Range"; // Don't capture for now
     90
     91    if (target.isCapturable() && (!target.isGarrisonHolder() || !target.garrisoned().length))
     92        return "Capture";
     93    return "primary";
    8994};
    9095
    9196/** Makes the worker deposit the currently carried resources at the closest accessible dropsite */
    9297m.returnResources = function(gameState, ent)
    9398{
  • binaries/data/mods/public/simulation/components/Attack.js

    Attack.prototype.restrictedClassesSchema  
    3939    "</optional>";
    4040
    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>" +
    4850            "<MaxRange>4.0</MaxRange>" +
    4951            "<RepeatTime>1000</RepeatTime>" +
    Attack.prototype.Schema =  
    6062            "</Bonuses>" +
    6163            "<RestrictedClasses datatype=\"tokens\">Champion</RestrictedClasses>" +
    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>" +
    6871            "<MaxRange>44.0</MaxRange>" +
    6972            "<MinRange>20.0</MinRange>" +
    Attack.prototype.Schema =  
    101104            "<Crush>0.0</Crush>" +
    102105            "<MaxRange>4.0</MaxRange>" +
    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>" +
    110126                "<element name='Crush' a:help='Crush damage strength'><ref name='nonNegativeDecimal'/></element>" +
    111127                "<element name='MaxRange' a:help='Maximum attack range (in metres)'><ref name='nonNegativeDecimal'/></element>" +
    Attack.prototype.Schema =  
    118134            "</interleave>" +
    119135        "</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>" +
    126150                "<element name='Crush' a:help='Crush damage strength'><ref name='nonNegativeDecimal'/></element>" +
    127151                "<element name='MaxRange' a:help='Maximum attack range (in metres)'><ref name='nonNegativeDecimal'/></element>" +
    Attack.prototype.GetRestrictedClasses =  
    227251        return this.template[type].RestrictedClasses._string.split(/\s+/);
    228252
    229253    return [];
    230254};
    231255
    232 Attack.prototype.CanAttack = function(target)
     256Attack.prototype.GetChangeDistance = function()
     257{
     258    return +(this.template.ChangeDistance || 0);
     259};
     260
     261Attack.prototype.CanAttack = function(target, wantedType)
    233262{
     263    if (wantedType && !this.template[wantedType])
     264        return false;
     265
    234266    let cmpFormation = Engine.QueryInterface(target, IID_Formation);
    235267    if (cmpFormation)
    236268        return true;
    237269
    238270    let cmpThisPosition = Engine.QueryInterface(this.entity, IID_Position);
    239271    let cmpTargetPosition = Engine.QueryInterface(target, IID_Position);
    240272    if (!cmpThisPosition || !cmpTargetPosition || !cmpThisPosition.IsInWorld() || !cmpTargetPosition.IsInWorld())
    241273        return false;
    242274
    243     // Check if the relative height difference is larger than the attack range
    244     // If the relative height is bigger, it means they will never be able to
    245     // reach each other, no matter how close they come.
    246     let heightDiff = Math.abs(cmpThisPosition.GetHeightOffset() - cmpTargetPosition.GetHeightOffset());
    247 
    248275    const cmpIdentity = Engine.QueryInterface(target, IID_Identity);
    249276    if (!cmpIdentity)
    250277        return undefined;
    251278
     279    let cmpEntityPlayer = QueryOwnerInterface(this.entity);
     280    let cmpTargetPlayer = QueryOwnerInterface(target);
     281    if (!cmpTargetPlayer || !cmpEntityPlayer)
     282        return false;
     283
    252284    const targetClasses = cmpIdentity.GetClassesList();
    253285
     286    let cmpCapturable = QueryMiragedInterface(target, IID_Capturable);
     287    if (targetClasses.indexOf("Domestic") == -1 && !cmpEntityPlayer.IsEnemy(cmpTargetPlayer.GetPlayerID()) &&
     288       (!cmpCapturable || !cmpCapturable.CanCapture(cmpEntityPlayer.GetPlayerID())))
     289        return false;
     290
     291    // Check if the relative height difference is larger than the attack range
     292    // If the relative height is bigger, it means they will never be able to
     293    // reach each other, no matter how close they come.
     294    let heightDiff = Math.abs(cmpThisPosition.GetHeightOffset() - cmpTargetPosition.GetHeightOffset());
     295
    254296    for (let type of this.GetAttackTypes())
    255297    {
    256         if (type == "Capture" && !QueryMiragedInterface(target, IID_Capturable))
     298        if (wantedType && type != wantedType)
     299            continue;
     300
     301        if (type == "Capture" && !cmpCapturable)
    257302            continue;
    258303
    259304        if (type == "Slaughter" && targetClasses.indexOf("Domestic") == -1)
    260305            continue;
    261306
    Attack.prototype.GetFullAttackRange = fu  
    315360        ret.max = Math.max(ret.max, range.max);
    316361    }
    317362    return ret;
    318363};
    319364
    320 Attack.prototype.GetBestAttackAgainst = function(target, allowCapture)
     365Attack.prototype.GetBestAttackAgainst = function(target, prefAttackType)
    321366{
    322367    let cmpFormation = Engine.QueryInterface(target, IID_Formation);
    323368    if (cmpFormation)
    324369    {
    325370        // TODO: Formation against formation needs review
    326371        let types = this.GetAttackTypes();
    327         return ["Ranged", "Melee", "Capture"].find(attack => types.indexOf(attack) != -1);
     372        return ["Ranged", "Melee", "Capture"].find(attack =>
     373            types.indexOf(attack) != -1 && (!prefAttackType || "no" + attack != prefAttackType));
    328374    }
    329375
    330376    let cmpIdentity = Engine.QueryInterface(target, IID_Identity);
    331377    if (!cmpIdentity)
    332378        return undefined;
    333379
     380    // If we are visisble garrisoned always do ranged attack if we can.
     381    let cmpPosition = Engine.QueryInterface(this.entity, IID_Position);
     382    if (cmpPosition && cmpPosition.GetTurretParent() != INVALID_ENTITY)
     383        return this.template.Ranged && "Ranged";
     384
    334385    let targetClasses = cmpIdentity.GetClassesList();
    335386    let isTargetClass = className => targetClasses.indexOf(className) != -1;
    336387
    337388    // Always slaughter domestic animals instead of using a normal attack
    338389    if (isTargetClass("Domestic") && this.template.Slaughter)
    339390        return "Slaughter";
    340391
    341392    let attack = this;
    342     let types = this.GetAttackTypes().filter(type => attack.GetRestrictedClasses(type).every(isTargetClass));
     393    let types = this.GetAttackTypes().filter(type =>
     394        attack.GetRestrictedClasses(type).every(isTargetClass) &&
     395        (!prefAttackType || "no" + type != prefAttackType));
    343396
    344     // check if the target is capturable
    345     let captureIndex = types.indexOf("Capture");
    346     if (captureIndex != -1)
     397    if (!types.length)
     398        return undefined;
     399
     400    prefAttackType = this.GetAttackTypeFromOrder(prefAttackType);
     401
     402    if (prefAttackType && prefAttackType != "Capture" && types.indexOf(prefAttackType) != -1)
     403        return prefAttackType;
     404
     405    if (!prefAttackType || prefAttackType == "Capture")
     406    {
     407        // check if the target is capturable
     408        let captureIndex = types.indexOf("Capture");
     409        if (captureIndex != -1)
     410        {
     411            let cmpCapturable = QueryMiragedInterface(target, IID_Capturable);
     412            let cmpPlayer = QueryOwnerInterface(this.entity);
     413            if (cmpPlayer && cmpCapturable && cmpCapturable.CanCapture(cmpPlayer.GetPlayerID()))
     414                return "Capture";
     415            // not captureable, so remove this attack
     416            types.splice(captureIndex, 1);
     417        }
     418    }
     419
     420    // ignore charges for now: TODO implement these
     421    let chargeIndex = types.indexOf("Charge");
     422    if (chargeIndex != -1)
     423        types.splice(chargeIndex, 1);
     424
     425    if (!types.length)
     426        return undefined;
     427    // assume ranged and/or melee attack left
     428    // TODO stop assuming that?
     429    let meleeIndex = types.indexOf("Melee");
     430    let rangedIndex = types.indexOf("Ranged");
     431    if (meleeIndex != -1 && rangedIndex != -1)
    347432    {
    348         let cmpCapturable = QueryMiragedInterface(target, IID_Capturable);
     433        if (this.HasTargetPreferredClass(types, targetClasses))
     434        {
     435            let isPreferred = className => attack.GetPreferredClasses(className).some(isTargetClass);
     436
     437            return types.sort((a, b) =>
     438                (types.indexOf(a) + (isPreferred(a) ? types.length : 0) ) -
     439                (types.indexOf(b) + (isPreferred(b) ? types.length : 0))).pop();
     440        }
    349441
    350         let cmpPlayer = QueryOwnerInterface(this.entity);
    351         if (allowCapture && cmpPlayer && cmpCapturable && cmpCapturable.CanCapture(cmpPlayer.GetPlayerID()))
    352             return "Capture";
    353         // not captureable, so remove this attack
    354         types.splice(captureIndex, 1);
     442        if (!cmpPosition.IsInWorld())
     443            return undefined;
     444
     445        let cmpTargetPosition = Engine.QueryInterface(target, IID_Position);
     446        if (!cmpTargetPosition || !cmpTargetPosition.IsInWorld())
     447            return undefined;
     448
     449        let selfPosition = cmpPosition.GetPosition2D();
     450        let targetPosition = cmpTargetPosition.GetPosition2D();
     451        if (targetPosition.distanceToSquared(selfPosition) <= Math.pow(this.GetChangeDistance(), 2))
     452            return "Melee";
     453
     454        return "Ranged";
    355455    }
    356456
    357     let isPreferred = className => attack.GetPreferredClasses(className).some(isTargetClass);
     457    if (meleeIndex != -1)
     458        return "Melee";
     459
     460    if (rangedIndex != -1)
     461        return "Ranged";
    358462
    359     return types.sort((a, b) =>
    360         (types.indexOf(a) + (isPreferred(a) ? types.length : 0) ) -
    361         (types.indexOf(b) + (isPreferred(b) ? types.length : 0))).pop();
     463    return types[0];
    362464};
    363465
    364466Attack.prototype.CompareEntitiesByPreference = function(a, b)
    365467{
    366468    let aPreference = this.GetPreference(a);
    Attack.prototype.GetAttackBonus = functi  
    464566    }
    465567
    466568    return attackBonus;
    467569};
    468570
     571Attack.prototype.GetAttackTypeFromOrder = function(prefAttackType)
     572{
     573    let types = this.GetAttackTypes();
     574    for (let type of types)
     575        if (this.template[type].AttackOrder && this.template[type].AttackOrder == prefAttackType)
     576            return type;
     577    return prefAttackType;
     578};
     579
     580Attack.prototype.HasTargetPreferredClass = function(types, targetClasses)
     581{
     582    let isTargetClass = function (className) { return targetClasses.indexOf(className) != -1; };
     583    for (let type of types)
     584        if (this.template[type].PreferredClasses && this.GetPreferredClasses(type).some(isTargetClass))
     585            return true;
     586    return false
     587};
     588
    469589// Returns a 2d random distribution scaled for a spread of scale 1.
    470590// The current implementation is a 2d gaussian with sigma = 1
    471591Attack.prototype.GetNormalDistribution = function(){
    472592
    473593    // Use the Box-Muller transform to get a gaussian distribution
    474594    let a = Math.random();
    475595    let b = Math.random();
    476596
    477     let c = Math.sqrt(-2*Math.log(a)) * Math.cos(2*Math.PI*b);
    478     let d = Math.sqrt(-2*Math.log(a)) * Math.sin(2*Math.PI*b);
     597    let c = Math.sqrt(-2 * Math.log(a)) * Math.cos(2 * Math.PI * b);
     598    let d = Math.sqrt(-2 * Math.log(a)) * Math.sin(2 * Math.PI * b);
    479599
    480600    return [c, d];
    481601};
    482602
    483603/**
    Attack.prototype.PerformAttack = functio  
    489609{
    490610    // If this is a ranged attack, then launch a projectile
    491611    if (type == "Ranged")
    492612    {
    493613        let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
    494         let turnLength = cmpTimer.GetLatestTurnLength()/1000;
     614        let turnLength = cmpTimer.GetLatestTurnLength() / 1000;
    495615        // In the future this could be extended:
    496616        //  * Obstacles like trees could reduce the probability of the target being hit
    497617        //  * Obstacles like walls should block projectiles entirely
    498618
    499619        // Get some data about the entity
    Attack.prototype.PerformAttack = functio  
    521641        // The component of the targets velocity radially away from the archer
    522642        let radialSpeed = relativePosition.dot(targetVelocity) / relativePosition.length();
    523643
    524644        let horizDistance = targetPosition.horizDistanceTo(selfPosition);
    525645
    526         // This is an approximation of the time ot the target, it assumes that the target has a constant radial
     646        // This is an approximation of the time to the target, it assumes that the target has a constant radial
    527647        // velocity, but since units move in straight lines this is not true.  The exact value would be more
    528648        // difficult to calculate and I think this is sufficiently accurate.  (I tested and for cavalry it was
    529649        // about 5% of the units radius out in the worst case)
    530650        let timeToTarget = horizDistance / (horizSpeed - radialSpeed);
    531651
    Attack.prototype.PerformAttack = functio  
    606726};
    607727
    608728Attack.prototype.InterpolatedLocation = function(ent, lateness)
    609729{
    610730    let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
    611     let turnLength = cmpTimer.GetLatestTurnLength()/1000;
     731    let turnLength = cmpTimer.GetLatestTurnLength() / 1000;
    612732    let cmpTargetPosition = Engine.QueryInterface(ent, IID_Position);
    613733    if (!cmpTargetPosition || !cmpTargetPosition.IsInWorld()) // TODO: handle dead target properly
    614734        return undefined;
    615735    let curPos = cmpTargetPosition.GetPosition();
    616736    let prevPos = cmpTargetPosition.GetPreviousPosition();
    Attack.prototype.testCollision = functio  
    644764        let angle = Engine.QueryInterface(ent, IID_Position).GetRotation().y;
    645765
    646766        let d = Vector3D.sub(point, targetPosition);
    647767        d = Vector2D.from3D(d).rotate(-angle);
    648768
    649         return d.x < Math.abs(targetShape.width/2) && d.y < Math.abs(targetShape.depth/2);
     769        return d.x < Math.abs(targetShape.width / 2) && d.y < Math.abs(targetShape.depth / 2);
    650770    }
    651771};
    652772
    653773Attack.prototype.MissileHit = function(data, lateness)
    654774{
  • binaries/data/mods/public/simulation/components/GarrisonHolder.js

    GarrisonHolder.prototype.Schema =  
    88        "<attribute name='datatype'>" +
    99            "<value>tokens</value>" +
    1010        "</attribute>" +
    1111        "<text/>" +
    1212    "</element>" +
     13    "<optional>" +
     14        "<element name='NeededAttackTypes' a:help='AttackType which are needed to garrison inside this holder (from Identity)'>" +
     15            "<attribute name='datatype'>" +
     16                "<value>tokens</value>" +
     17            "</attribute>" +
     18            "<text/>" +
     19        "</element>" +
     20    "</optional>" +
    1321    "<element name='EjectHealth' a:help='Percentage of maximum health below which this holder no longer allows garrisoning'>" +
    1422        "<ref name='nonNegativeDecimal'/>" +
    1523    "</element>" +
    1624    "<element name='EjectClassesOnDestroy' a:help='Classes of entities to be ejected on destroy. Others are killed'>" +
    1725        "<attribute name='datatype'>" +
    GarrisonHolder.prototype.GetAllowedClass  
    117125{
    118126    return this.template.List._string;
    119127};
    120128
    121129/**
     130 * Returns an array of attack types which the unit garrisoned inside this
     131 * particualar entity needs to have. Obtained from the entity's template
     132 */
     133GarrisonHolder.prototype.GetNeededAttackTypes = function()
     134{
     135    return this.template.NeededAttackTypes ? this.template.NeededAttackTypes._string : undefined;
     136};
     137
     138/**
    122139 * Get Maximum pop which can be garrisoned
    123140 */
    124141GarrisonHolder.prototype.GetCapacity = function()
    125142{
    126143    return ApplyValueModificationsToEntity("GarrisonHolder/Max", +this.template.Max, this.entity);
  • binaries/data/mods/public/simulation/components/GuiInterface.js

    GuiInterface.prototype.GetEntityState =  
    355355    let cmpGarrisonHolder = Engine.QueryInterface(ent, IID_GarrisonHolder);
    356356    if (cmpGarrisonHolder)
    357357        ret.garrisonHolder = {
    358358            "entities": cmpGarrisonHolder.GetEntities(),
    359359            "allowedClasses": cmpGarrisonHolder.GetAllowedClasses(),
     360            "neededAttackTypes": cmpGarrisonHolder.GetNeededAttackTypes(),
    360361            "capacity": cmpGarrisonHolder.GetCapacity(),
    361362            "garrisonedEntitiesCount": cmpGarrisonHolder.GetGarrisonedEntitiesCount()
    362363        };
    363364
    364365    let cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
    GuiInterface.prototype.GetExtendedEntity  
    436437    if (cmpAttack)
    437438    {
    438439        let types = cmpAttack.GetAttackTypes();
    439440        if (types.length)
    440441            ret.attack = {};
     442
    441443        for (let type of types)
    442444        {
    443445            ret.attack[type] = cmpAttack.GetAttackStrengths(type);
    444446            ret.attack[type].splash = cmpAttack.GetSplashDamage(type);
    445447
    GuiInterface.prototype.CanAttack = funct  
    18251827{
    18261828    let cmpAttack = Engine.QueryInterface(data.entity, IID_Attack);
    18271829    if (!cmpAttack)
    18281830        return false;
    18291831
    1830     let cmpEntityPlayer = QueryOwnerInterface(data.entity, IID_Player);
    1831     let cmpTargetPlayer = QueryOwnerInterface(data.target, IID_Player);
    1832     if (!cmpEntityPlayer || !cmpTargetPlayer)
    1833         return false;
     1832    return cmpAttack.CanAttack(data.target);
     1833};
    18341834
    1835     // if the owner is an enemy, it's up to the attack component to decide
    1836     if (cmpEntityPlayer.IsEnemy(cmpTargetPlayer.GetPlayerID()))
    1837         return cmpAttack.CanAttack(data.target);
     1835GuiInterface.prototype.CanAttackWithType = function(player, data)
     1836{
     1837    let cmpAttack = Engine.QueryInterface(data.entity, IID_Attack);
     1838    if (!cmpAttack)
     1839        return false;
    18381840
    1839     return false;
     1841    return cmpAttack.CanAttack(data.target, data.type);
    18401842};
    18411843
    18421844/*
    18431845 * Returns batch build time.
    18441846 */
    let exposedFunctions = {  
    19811983    "HasIdleUnits": 1,
    19821984    "GetTradingRouteGain": 1,
    19831985    "GetTradingDetails": 1,
    19841986    "CanCapture": 1,
    19851987    "CanAttack": 1,
     1988    "CanAttackWithType": 1,
    19861989    "GetBatchTime": 1,
    19871990
    19881991    "IsMapRevealed": 1,
    19891992    "SetPathfinderDebugOverlay": 1,
    19901993    "SetPathfinderHierDebugOverlay": 1,
  • binaries/data/mods/public/simulation/components/UnitAI.js

    UnitAI.prototype.UnitFsmSpec = {  
    418418            this.FinishOrder();
    419419            return;
    420420        }
    421421
    422422        // Work out how to attack the given target
    423         var type = this.GetBestAttackAgainst(this.order.data.target, this.order.data.allowCapture);
     423        let type = this.GetBestAttackAgainst(this.order.data.target, this.order.data.prefAttackType);
    424424        if (!type)
    425425        {
    426426            // Oops, we can't attack at all
    427427            this.FinishOrder();
    428428            return;
    UnitAI.prototype.UnitFsmSpec = {  
    561561    "Order.Gather": function(msg) {
    562562        // If the target is still alive, we need to kill it first
    563563        if (this.MustKillGatherTarget(this.order.data.target))
    564564        {
    565565            // Make sure we can attack the target, else we'll get very stuck
    566             if (!this.GetBestAttackAgainst(this.order.data.target, false))
     566            if (!this.GetBestAttackAgainst(this.order.data.target))
    567567            {
    568568                // Oops, we can't attack at all - give up
    569569                // TODO: should do something so the player knows why this failed
    570570                this.FinishOrder();
    571571                return;
    UnitAI.prototype.UnitFsmSpec = {  
    585585                    this.FinishOrder();
    586586                }
    587587                return;
    588588            }
    589589
    590             this.PushOrderFront("Attack", { "target": this.order.data.target, "force": false, "hunting": true, "allowCapture": false });
     590            this.PushOrderFront("Attack", { "target": this.order.data.target, "force": false, "hunting": true, "prefAttackType": undefined });
    591591            return;
    592592        }
    593593
    594594        // Try to move within range
    595595        if (this.MoveToTargetRange(this.order.data.target, IID_ResourceGatherer))
    UnitAI.prototype.UnitFsmSpec = {  
    840840                this.CallMemberFunction("Stop", [false]);
    841841            this.FinishOrder();
    842842        },
    843843
    844844        "Order.Attack": function(msg) {
    845             var target = msg.data.target;
    846             var allowCapture = msg.data.allowCapture;
    847             var cmpTargetUnitAI = Engine.QueryInterface(target, IID_UnitAI);
     845            let target = msg.data.target;
     846            let prefAttackType = msg.data.prefAttackType;
     847            let cmpTargetUnitAI = Engine.QueryInterface(target, IID_UnitAI);
    848848            if (cmpTargetUnitAI && cmpTargetUnitAI.IsFormationMember())
    849849                target = cmpTargetUnitAI.GetFormationController();
    850850
    851851            var cmpAttack = Engine.QueryInterface(this.entity, IID_Attack);
    852852            // Check if we are already in range, otherwise walk there
    UnitAI.prototype.UnitFsmSpec = {  
    861861                    }
    862862                }
    863863                this.FinishOrder();
    864864                return;
    865865            }
    866             this.CallMemberFunction("Attack", [target, false, allowCapture]);
     866            this.CallMemberFunction("Attack", [target, false, prefAttackType]);
    867867            if (cmpAttack.CanAttackAsFormation())
    868868                this.SetNextState("COMBAT.ATTACKING");
    869869            else
    870870                this.SetNextState("MEMBER");
    871871        },
    UnitAI.prototype.UnitFsmSpec = {  
    916916                        this.FinishOrder();
    917917                    }
    918918                    return;
    919919                }
    920920
    921                 this.PushOrderFront("Attack", { "target": msg.data.target, "hunting": true, "allowCapture": false });
     921                this.PushOrderFront("Attack", { "target": msg.data.target, "hunting": true, "prefAttackType": undefined });
    922922                return;
    923923            }
    924924
    925925            // TODO: on what should we base this range?
    926926            // Check if we are already in range, otherwise walk there
    UnitAI.prototype.UnitFsmSpec = {  
    11531153                    cmpFormation.MoveMembersIntoFormation(true, true);
    11541154                },
    11551155
    11561156                "MoveCompleted": function(msg) {
    11571157                    var cmpAttack = Engine.QueryInterface(this.entity, IID_Attack);
    1158                     this.CallMemberFunction("Attack", [this.order.data.target, false, this.order.data.allowCapture]);
     1158                    this.CallMemberFunction("Attack", [this.order.data.target, false, this.order.data.prefAttackType]);
    11591159                    if (cmpAttack.CanAttackAsFormation())
    11601160                        this.SetNextState("COMBAT.ATTACKING");
    11611161                    else
    11621162                        this.SetNextState("MEMBER");
    11631163                },
    11641164            },
    11651165
    11661166            "ATTACKING": {
    11671167                // Wait for individual members to finish
    11681168                "enter": function(msg) {
    1169                     var target = this.order.data.target;
    1170                     var allowCapture = this.order.data.allowCapture;
     1169                    let target = this.order.data.target;
     1170                    let prefAttackType = this.order.data.prefAttackType;
    11711171                    // Check if we are already in range, otherwise walk there
    11721172                    if (!this.CheckTargetAttackRange(target, target))
    11731173                    {
    11741174                        if (this.TargetIsAlive(target) && this.CheckTargetVisible(target))
    11751175                        {
    11761176                            this.FinishOrder();
    1177                             this.PushOrderFront("Attack", { "target": target, "force": false, "allowCapture": allowCapture });
     1177                            this.PushOrderFront("Attack", { "target": target, "force": false, "prefAttackType": prefAttackType });
    11781178                            return true;
    11791179                        }
    11801180                        this.FinishOrder();
    11811181                        return true;
    11821182                    }
    UnitAI.prototype.UnitFsmSpec = {  
    11881188                    this.StartTimer(200, 200);
    11891189                    return false;
    11901190                },
    11911191
    11921192                "Timer": function(msg) {
    1193                     var target = this.order.data.target;
    1194                     var allowCapture = this.order.data.allowCapture;
     1193                    let target = this.order.data.target;
     1194                    let prefAttackType = this.order.data.prefAttackType;
    11951195                    // Check if we are already in range, otherwise walk there
    11961196                    if (!this.CheckTargetAttackRange(target, target))
    11971197                    {
    11981198                        if (this.TargetIsAlive(target) && this.CheckTargetVisible(target))
    11991199                        {
    12001200                            this.FinishOrder();
    1201                             this.PushOrderFront("Attack", { "target": target, "force": false, "allowCapture": allowCapture });
     1201                            this.PushOrderFront("Attack", { "target": target, "force": false, "prefAttackType": prefAttackType });
    12021202                            return;
    12031203                        }
    12041204                        this.FinishOrder();
    12051205                        return;
    12061206                    }
    UnitAI.prototype.UnitFsmSpec = {  
    14081408                return;
    14091409            }
    14101410
    14111411            // target the unit
    14121412            if (this.CheckTargetVisible(msg.data.attacker))
    1413                 this.PushOrderFront("Attack", { "target": msg.data.attacker, "force": false, "allowCapture": true });
     1413                this.PushOrderFront("Attack", { "target": msg.data.attacker, "force": false, "prefAttackType": undefined });
    14141414            else
    14151415            {
    14161416                var cmpPosition = Engine.QueryInterface(msg.data.attacker, IID_Position);
    14171417                if (!cmpPosition || !cmpPosition.IsInWorld())
    14181418                    return;
    UnitAI.prototype.UnitFsmSpec = {  
    18031803                        !this.CheckTargetAttackRange(target, this.order.data.attackType))
    18041804                    {
    18051805                        // Can't reach it - try to chase after it
    18061806                        if (this.ShouldChaseTargetedEntity(target, this.order.data.force))
    18071807                        {
     1808                            if (!this.order.data.force)
     1809                                this.order.data.attackType = this.GetBestAttackAgainst(target);
    18081810                            if (this.MoveToTargetAttackRange(target, this.order.data.attackType))
    18091811                            {
    18101812                                this.SetNextState("COMBAT.CHASING");
    18111813                                return;
    18121814                            }
    UnitAI.prototype.UnitFsmSpec = {  
    19141916                        }
    19151917
    19161918                        // Can't reach it - try to chase after it
    19171919                        if (this.ShouldChaseTargetedEntity(target, this.order.data.force))
    19181920                        {
     1921                            if (!this.order.data.force)
     1922                            {
     1923                                let type = this.GetBestAttackAgainst(target);
     1924                                if (type)
     1925                                    this.order.data.attackType = type;
     1926                            }
    19191927                            if (this.MoveToTargetRange(target, IID_Attack, this.order.data.attackType))
    19201928                            {
    19211929                                this.SetNextState("COMBAT.CHASING");
    19221930                                return;
    19231931                            }
    UnitAI.prototype.CheckTargetIsInVisionRa  
    45474555    var distance = DistanceBetweenEntities(this.entity, target);
    45484556
    45494557    return distance < range;
    45504558};
    45514559
    4552 UnitAI.prototype.GetBestAttackAgainst = function(target, allowCapture)
     4560UnitAI.prototype.GetBestAttackAgainst = function(target, prefAttackType)
    45534561{
    45544562    var cmpAttack = Engine.QueryInterface(this.entity, IID_Attack);
    45554563    if (!cmpAttack)
    45564564        return undefined;
    4557     return cmpAttack.GetBestAttackAgainst(target, allowCapture);
     4565    return cmpAttack.GetBestAttackAgainst(target, prefAttackType);
    45584566};
    45594567
    45604568UnitAI.prototype.GetAttackBonus = function(type, target)
    45614569{
    45624570    var cmpAttack = Engine.QueryInterface(this.entity, IID_Attack);
    UnitAI.prototype.AttackVisibleEntity = f  
    45744582{
    45754583    var target = ents.find(target => this.CanAttack(target, forceResponse));
    45764584    if (!target)
    45774585        return false;
    45784586
    4579     this.PushOrderFront("Attack", { "target": target, "force": false, "forceResponse": forceResponse, "allowCapture": true });
     4587    this.PushOrderFront("Attack", { "target": target, "force": false, "forceResponse": forceResponse, "prefAttackType": undefined });
    45804588    return true;
    45814589};
    45824590
    45834591/**
    45844592 * Try to find one of the given entities which can be attacked
    UnitAI.prototype.AttackVisibleEntity = f  
    45874595 */
    45884596UnitAI.prototype.AttackEntityInZone = function(ents, forceResponse)
    45894597{
    45904598    var target = ents.find(target =>
    45914599        this.CanAttack(target, forceResponse)
    4592         && this.CheckTargetDistanceFromHeldPosition(target, IID_Attack, this.GetBestAttackAgainst(target, true))
     4600        && this.CheckTargetDistanceFromHeldPosition(target, IID_Attack, this.GetBestAttackAgainst(target))
    45934601        && (this.GetStance().respondChaseBeyondVision || this.CheckTargetIsInVisionRange(target))
    45944602    );
    45954603    if (!target)
    45964604        return false;
    45974605
    4598     this.PushOrderFront("Attack", { "target": target, "force": false, "forceResponse": forceResponse, "allowCapture": true });
     4606    this.PushOrderFront("Attack", { "target": target, "force": false, "forceResponse": forceResponse, "prefAttackType": undefined });
    45994607    return true;
    46004608};
    46014609
    46024610/**
    46034611 * Try to respond appropriately given our current stance,
    UnitAI.prototype.WalkToTarget = function  
    49844992/**
    49854993 * Adds walk-and-fight order to queue, this only occurs in response
    49864994 * to a player order, and so is forced.
    49874995 * If targetClasses is given, only entities matching the targetClasses can be attacked.
    49884996 */
    4989 UnitAI.prototype.WalkAndFight = function(x, z, targetClasses, queued)
     4997UnitAI.prototype.WalkAndFight = function(x, z, targetClasses, queued, prefAttackType)
    49904998{
    4991     this.AddOrder("WalkAndFight", { "x": x, "z": z, "targetClasses": targetClasses, "force": true }, queued);
     4999    this.AddOrder("WalkAndFight", { "x": x, "z": z, "targetClasses": targetClasses, "force": true, "prefAttackType": prefAttackType }, queued);
    49925000};
    49935001
    49945002/**
    49955003 * Adds leave foundation order to queue, treated as forced.
    49965004 */
    UnitAI.prototype.LeaveFoundation = funct  
    50075015};
    50085016
    50095017/**
    50105018 * Adds attack order to the queue, forced by the player.
    50115019 */
    5012 UnitAI.prototype.Attack = function(target, queued, allowCapture)
     5020UnitAI.prototype.Attack = function(target, queued, prefAttackType)
    50135021{
    50145022    if (!this.CanAttack(target))
    50155023    {
    50165024        // We don't want to let healers walk to the target unit so they can be easily killed.
    50175025        // Instead we just let them get into healing range.
    UnitAI.prototype.Attack = function(targe  
    50195027            this.MoveToTargetRange(target, IID_Heal);
    50205028        else
    50215029            this.WalkToTarget(target, queued);
    50225030        return;
    50235031    }
    5024     this.AddOrder("Attack", { "target": target, "force": true, "allowCapture": allowCapture}, queued);
     5032    this.AddOrder("Attack", { "target": target, "force": true, "prefAttackType": prefAttackType }, queued);
    50255033};
    50265034
    50275035/**
    50285036 * Adds garrison order to the queue, forced by the player.
    50295037 */
    UnitAI.prototype.FindWalkAndFightTargets  
    54485456                        continue;
    54495457                    // Only used by the AIs to prevent some choices of targets
    54505458                    if (targetClasses.vetoEntities && targetClasses.vetoEntities[targ])
    54515459                        continue;
    54525460                }
    5453                 this.PushOrderFront("Attack", { "target": targ, "force": true, "allowCapture": true });
     5461                this.PushOrderFront("Attack", { "target": targ, "force": true, "prefAttackType": undefined });
    54545462                return true;
    54555463            }
    54565464        }
    54575465        return false;
    54585466    }
    UnitAI.prototype.FindWalkAndFightTargets  
    54745482                continue;
    54755483            // Only used by the AIs to prevent some choices of targets
    54765484            if (targetClasses.vetoEntities && targetClasses.vetoEntities[targ])
    54775485                continue;
    54785486        }
    5479         this.PushOrderFront("Attack", { "target": targ, "force": true, "allowCapture": true });
     5487        this.PushOrderFront("Attack", { "target": targ, "force": true, "prefAttackType": undefined });
    54805488        return true;
    54815489    }
    54825490
    54835491    // healers on a walk-and-fight order should heal injured units
    54845492    if (this.IsHealer())
    UnitAI.prototype.CanGarrison = function(  
    56745682    // Formation controllers should always respond to commands
    56755683    // (then the individual units can make up their own minds)
    56765684    if (this.IsFormationController())
    56775685        return true;
    56785686
    5679     var cmpGarrisonHolder = Engine.QueryInterface(target, IID_GarrisonHolder);
     5687    let cmpGarrisonHolder = Engine.QueryInterface(target, IID_GarrisonHolder);
    56805688    if (!cmpGarrisonHolder)
    56815689        return false;
    56825690
    56835691    // Verify that the target is owned by this entity's player or a mutual ally of this player
    5684     var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership);
     5692    let cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership);
    56855693    if (!cmpOwnership || !(IsOwnedByPlayer(cmpOwnership.GetOwner(), target) || IsOwnedByMutualAllyOfPlayer(cmpOwnership.GetOwner(), target)))
    56865694        return false;
    56875695
    56885696    // Don't let animals garrison for now
    56895697    // (If we want to support that, we'll need to change Order.Garrison so it
    56905698    // doesn't move the animal into an INVIDIDUAL.* state)
    56915699    if (this.IsAnimal())
    56925700        return false;
    56935701
     5702    let cmpIdentity = Engine.QueryInterface(this.entity, IID_Identity)
     5703    if (!cmpIdentity)
     5704        return false;
     5705
     5706    let cmpAttack = Engine.QueryInterface(this.entity, IID_Attack)
     5707    let attackTypes = cmpAttack ? cmpAttack.GetAttackTypes() : [];
     5708    if (!MatchesClassList(cmpIdentity.GetClassesList(), cmpGarrisonHolder.GetAllowedClasses()) ||
     5709        !HasNeededAttackTypes(attackTypes, cmpGarrisonHolder.GetNeededAttackTypes()))
     5710        return false;
    56945711    return true;
    56955712};
    56965713
    56975714UnitAI.prototype.CanGather = function(target)
    56985715{
  • binaries/data/mods/public/simulation/components/tests/test_UnitAI.js

    function TestFormationExiting(mode)  
    9595    });
    9696
    9797    AddMock(unit, IID_Attack, {
    9898        GetRange: function() { return { "max": 10, "min": 0}; },
    9999        GetFullAttackRange: function() { return { "max": 40, "min": 0}; },
    100         GetBestAttackAgainst: function(t) { return "melee"; },
     100        GetBestAttackAgainst: function(t) { return "Melee"; },
    101101        GetPreference: function(t) { return 0; },
    102102        GetTimers: function() { return { "prepare": 500, "repeat": 1000 }; },
    103103        CanAttack: function(v) { return true; },
    104104        CompareEntitiesByPreference: function(a, b) { return 0; },
    105105    });
    function TestMoveIntoFormationWhileAttac  
    245245        });
    246246
    247247        AddMock(unit + i, IID_Attack, {
    248248            GetRange: function() { return {"max":10, "min": 0}; },
    249249            GetFullAttackRange: function() { return { "max": 40, "min": 0}; },
    250             GetBestAttackAgainst: function(t) { return "melee"; },
     250            GetBestAttackAgainst: function(t) { return "Melee"; },
    251251            GetTimers: function() { return { "prepare": 500, "repeat": 1000 }; },
    252252            CanAttack: function(v) { return true; },
    253253            CompareEntitiesByPreference: function(a, b) { return 0; },
    254254        });
    255255
  • binaries/data/mods/public/simulation/helpers/Commands.js

    var g_Commands = {  
    158158    },
    159159
    160160    "attack-walk": function(player, cmd, data)
    161161    {
    162162        GetFormationUnitAIs(data.entities, player).forEach(cmpUnitAI => {
    163             cmpUnitAI.WalkAndFight(cmd.x, cmd.z, cmd.targetClasses, cmd.queued);
     163            cmpUnitAI.WalkAndFight(cmd.x, cmd.z, cmd.targetClasses, cmd.queued, cmd.prefAttackType);
    164164        });
    165165    },
    166166
    167167    "attack": function(player, cmd, data)
    168168    {
    169169        if (g_DebugCommands && !(IsOwnedByEnemyOfPlayer(player, cmd.target) || IsOwnedByNeutralOfPlayer(player, cmd.target)))
    170170            warn("Invalid command: attack target is not owned by enemy of player "+player+": "+uneval(cmd));
    171171
    172         let allowCapture = cmd.allowCapture || cmd.allowCapture == null;
    173172        // See UnitAI.CanAttack for target checks
    174173        GetFormationUnitAIs(data.entities, player).forEach(cmpUnitAI => {
    175             cmpUnitAI.Attack(cmd.target, cmd.queued, allowCapture);
     174            cmpUnitAI.Attack(cmd.target, cmd.queued, cmd.prefAttackType);
    176175        });
    177176    },
    178177
    179178    "heal": function(player, cmd, data)
    180179    {
  • binaries/data/mods/public/simulation/templates/template_structure_defense_wall_long.xml

     
    1010  <Health>
    1111    <SpawnEntityOnDeath>rubble/rubble_stone_wall_long</SpawnEntityOnDeath>
    1212  </Health>
    1313  <GarrisonHolder>
    1414    <Max>5</Max>
    15     <List datatype="tokens">Ranged+Infantry</List>
     15    <List datatype="tokens">Infantry</List>
     16    <NeededAttackTypes datatype="tokens">Ranged</NeededAttackTypes>
    1617    <EjectHealth>0.1</EjectHealth>
    1718    <EjectClassesOnDestroy datatype="tokens">Unit</EjectClassesOnDestroy>
    1819    <BuffHeal>0</BuffHeal>
    1920    <LoadingRange>2</LoadingRange>
    2021    <VisibleGarrisonPoints>
  • binaries/data/mods/public/simulation/templates/template_structure_defense_wall_medium.xml

     
    77      <stone>22</stone>
    88    </Resources>
    99  </Cost>
    1010  <GarrisonHolder>
    1111    <Max>3</Max>
    12     <List datatype="tokens">Ranged+Infantry</List>
     12    <List datatype="tokens">Infantry</List>
     13    <NeededAttackTypes datatype="tokens">Ranged</NeededAttackTypes>
    1314    <EjectHealth>0.1</EjectHealth>
    1415    <EjectClassesOnDestroy datatype="tokens">Unit</EjectClassesOnDestroy>
    1516    <BuffHeal>0</BuffHeal>
    1617    <LoadingRange>2</LoadingRange>
    1718    <VisibleGarrisonPoints>
  • binaries/data/mods/public/simulation/templates/units/pers_champion_infantry.xml

     
    11<?xml version="1.0" encoding="utf-8"?>
    22<Entity parent="template_unit_champion_infantry_spearman">
     3  <Attack>
     4    <ChangeDistance>25</ChangeDistance>
     5    <Melee>
     6      <AttackOrder>primary</AttackOrder>
     7      <PreferredClasses datatype="tokens">Siege</PreferredClasses>
     8    </Melee>
     9    <Ranged>
     10      <AttackOrder>secondary</AttackOrder>
     11      <Hack>0</Hack>
     12      <Pierce>6.0</Pierce>
     13      <Crush>0</Crush>
     14      <MaxRange>65.0</MaxRange>
     15      <MinRange>10.0</MinRange>
     16      <ProjectileSpeed>120.0</ProjectileSpeed>
     17      <PrepareTime>1000</PrepareTime>
     18      <RepeatTime>1000</RepeatTime>
     19      <Spread>3.0</Spread>
     20    </Ranged>
     21  </Attack>
    322  <Identity>
    423    <Civ>pers</Civ>
    524    <GenericName>Persian Immortal</GenericName>
    625    <SpecificName>Anusiya</SpecificName>
    726    <Classes datatype="tokens">Immortal</Classes>