Ticket #252: t252_secondattack_17.diff

File t252_secondattack_17.diff, 56.5 KB (added by bb, 8 years ago)

rebased

  • binaries/data/config/default.cfg

     
    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
  • binaries/data/mods/public/art/actors/units/persians/champion_unit_1.xml

     
    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/art/textures/cursors/action-melee-attack.txt

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

     
     11 1
  • binaries/data/mods/public/globalscripts/Templates.js

     
    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
  • 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.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
     
    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    }
  • binaries/data/mods/public/gui/session/unit_actions.js

     
    5858
    5959            return { "type": "move" };
    6060        },
    61         "specificness": 12,
     61        "specificness": 13,
    6262    },
    6363
    6464    "attack-move":
     
    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", {
     
    106107        "specificness": 30,
    107108    },
    108109
     110
     111    "melee-attack-move":
     112    {
     113        "execute": function(target, action, selection, queued)
     114        {
     115            if (Engine.HotkeyIsPressed("session.meleeattackmoveUnit"))
     116                var targetClasses = { "meleeattack": ["Unit"] };
     117            else
     118                var 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            // Work out whether at least part of the selection have UnitAI
     144            let haveUnitAI = selection.some(function(ent) {
     145                let entState = GetEntityState(ent);
     146                return entState && entState.unitAI && getActionInfo("melee-attack-move", target).possible;
     147            });
     148
     149            if (haveUnitAI && Engine.HotkeyIsPressed("session.meleeattackmove") &&
     150                getActionInfo("melee-attack-move", target).possible)
     151                return {
     152                    "type": "melee-attack-move",
     153                    "cursor": "action-melee-attack"
     154                };
     155            return false;
     156        },
     157        "specificness": 31,
     158    },
     159
     160    "ranged-attack-move":
     161    {
     162        "execute": function(target, action, selection, queued)
     163        {
     164            if (Engine.HotkeyIsPressed("session.rangedattackmoveUnit"))
     165                var targetClasses = { "rangedattack": ["Unit"] };
     166            else
     167                var targetClasses = { "rangedattack": ["Unit", "Structure"] };
     168
     169            Engine.PostNetworkCommand({
     170                "type": "attack-walk",
     171                "entities": selection,
     172                "x": target.x,
     173                "z": target.z,
     174                "targetClasses": targetClasses,
     175                "queued": queued,
     176                "prefAttackType": "Ranged"
     177            });
     178
     179            Engine.GuiInterfaceCall("PlaySound", {
     180                "name": "order_walk",
     181                "entity": selection[0]
     182            });
     183
     184            return true;
     185        },
     186        "getActionInfo": function(entState, targetState)
     187        {
     188            return { "possible": true };
     189        },
     190        "hotkeyActionCheck": function(target, selection)
     191        {
     192            // Work out whether at least part of the selection have UnitAI
     193            let haveUnitAI = selection.some(function(ent) {
     194                let entState = GetEntityState(ent);
     195                return entState && entState.unitAI && getActionInfo("ranged-attack-move", target).possible;
     196            });
     197
     198            if (haveUnitAI && Engine.HotkeyIsPressed("session.rangedattackmove") &&
     199                getActionInfo("ranged-attack-move", target).possible)
     200                return {
     201                    "type": "ranged-attack-move",
     202                    "cursor": "action-ranged-attack"
     203                };
     204            return false;
     205        },
     206        "specificness": 32,
     207    },
     208
    109209    "capture":
    110210    {
    111211        "execute": function(target, action, selection, queued)
     
    115215                "entities": selection,
    116216                "target": action.target,
    117217                "allowCapture": true,
    118                 "queued": queued
     218                "queued": queued,
     219                "prefAttackType": "Capture"
    119220            });
    120221
    121222            Engine.GuiInterfaceCall("PlaySound", {
     
    148249                "target": target
    149250            };
    150251        },
    151         "specificness": 9,
     252        "specificness": 7,
    152253    },
    153254
    154255    "attack":
     
    160261                "entities": selection,
    161262                "target": action.target,
    162263                "queued": queued,
    163                 "allowCapture": false
     264                "allowCapture": false,
     265                "prefAttackType": "primary"
    164266            });
    165267
    166268            Engine.GuiInterfaceCall("PlaySound", {
     
    205307                "target": target
    206308            };
    207309        },
     310        "specificness": 8,
     311    },
     312
     313    "melee-attack":
     314    {
     315        "execute": function(target, action, selection, queued)
     316        {
     317            Engine.PostNetworkCommand({
     318                "type": "attack",
     319                "entities": selection,
     320                "target": action.target,
     321                "queued": queued,
     322                "prefAttackType": "Melee"
     323            });
     324
     325            Engine.GuiInterfaceCall("PlaySound", {
     326                "name": "order_attack",
     327                "entity": selection[0]
     328            });
     329            return true;
     330        },
     331        "getActionInfo": function(entState, targetState)
     332        {
     333            if (!entState.attack || !targetState.hitpoints)
     334                return false;
     335            return { "possible": Engine.GuiInterfaceCall("CanAttackWithType", {
     336                "entity": entState.id,
     337                "target": targetState.id,
     338                "type": "Melee"
     339            }) };
     340        },
     341        "hotkeyActionCheck": function(target)
     342        {
     343            if (Engine.HotkeyIsPressed("session.meleeattack") && getActionInfo("melee-attack", target).possible)
     344                return {
     345                    "type": "melee-attack",
     346                    "cursor": "action-melee-attack",
     347                    "target": target
     348                };
     349            return false;
     350        },
     351        "actionCheck": function(target)
     352        {
     353            if (getActionInfo("melee-attack", target).possible)
     354                return {
     355                    "type": "melee-attack",
     356                    "cursor": "action-melee-attack",
     357                    "target": target
     358                };
     359            return false;
     360        },
     361        "specificness": 9,
     362    },
     363
     364    "ranged-attack":
     365    {
     366        "execute": function(target, action, selection, queued)
     367        {
     368            Engine.PostNetworkCommand({
     369                "type": "attack",
     370                "entities": selection,
     371                "target": action.target,
     372                "queued": queued,
     373                "prefAttackType": "Ranged"
     374            });
     375
     376            Engine.GuiInterfaceCall("PlaySound", {
     377                "name": "order_attack",
     378                "entity": selection[0]
     379            });
     380
     381            return true;
     382        },
     383        "getActionInfo": function(entState, targetState)
     384        {
     385            if (!entState.attack || !targetState.hitpoints) // hack
     386                return false;
     387            return { "possible": Engine.GuiInterfaceCall("CanAttackWithType", {
     388                "entity": entState.id,
     389                "target": targetState.id,
     390                "type": "Ranged"
     391            }) };
     392        },
     393        "hotkeyActionCheck": function(target, selection)
     394        {
     395            if (Engine.HotkeyIsPressed("session.rangedattack") && getActionInfo("ranged-attack", target).possible)
     396                return {
     397                    "type": "ranged-attack",
     398                    "cursor": "action-ranged-attack",
     399                    "target": target
     400                };
     401            return false;
     402        },
     403        "actionCheck": function(target, selection)
     404        {
     405            if (getActionInfo("ranged-attack", target).possible)
     406                return {
     407                    "type": "ranged-attack",
     408                    "cursor": "action-ranged-attack",
     409                    "target": target
     410                };
     411            return false;
     412        },
    208413        "specificness": 10,
    209414    },
    210415
     
    255460                "target": target
    256461            };
    257462        },
    258         "specificness": 7,
     463        "specificness": 6,
    259464    },
    260465
    261466    "build":
     
    355560                "target": target
    356561            };
    357562        },
    358         "specificness": 11,
     563        "specificness": 12,
    359564    },
    360565
    361566    "gather":
     
    595800            if (targetState.garrisonHolder.garrisonedEntitiesCount + extraCount >= targetState.garrisonHolder.capacity)
    596801                tooltip = "[color=\"orange\"]" + tooltip + "[/color]";
    597802
    598             if (!MatchesClassList(entState.identity.classes, targetState.garrisonHolder.allowedClasses))
    599                 return false;
    600803
    601             return {
    602                 "possible": true,
    603                 "tooltip": tooltip
    604             };
     804            if (MatchesClassList(entState.identity.classes, targetState.garrisonHolder.allowedClasses) &&
     805                HasNeededAttackTypes(entState.attack, targetState.garrisonHolder.neededAttackTypes))
     806                return {
     807                    "possible": true,
     808                    "tooltip": tooltip
     809                };
     810
     811            return { "possible": false };
    605812        },
    606813        "preSelectedActionCheck": function(target)
    607814        {
     
    783990                cursor = "action-attack-move";
    784991            }
    785992
     993            if (Engine.HotkeyIsPressed("session.meleeattackmove"))
     994            {
     995                if (Engine.HotkeyIsPressed("session.meleeattackmoveUnit"))
     996                    var targetClasses = { "melee-attack": ["Unit"] };
     997                else
     998                    var targetClasses = { "melee-attack": ["Unit", "Structure"] };
     999                data.command = "melee-attack-walk";
     1000                data.targetClasses = targetClasses;
     1001                cursor = "action-melee-attack";
     1002            }
     1003
     1004            if (Engine.HotkeyIsPressed("session.rangedattackmove"))
     1005            {
     1006                if (Engine.HotkeyIsPressed("session.rangedattackmoveUnit"))
     1007                    var targetClasses = { "ranged-attack": ["Unit"] };
     1008                else
     1009                    var targetClasses = { "ranged-attack": ["Unit", "Structure"] };
     1010                data.command = "ranged-attack-walk";
     1011                data.targetClasses = targetClasses;
     1012                cursor = "action-ranged-attack";
     1013            }
     1014
    7861015            if (targetState.garrisonHolder &&
    7871016                playerCheck(entState, targetState, ["Player", "MutualAlly"]))
    7881017            {
     
    9011130                "position": actionInfo.position
    9021131            };
    9031132        },
    904         "specificness": 6,
     1133        "specificness": 5,
    9051134    },
    9061135
    9071136    "unset-rallypoint":
  • binaries/data/mods/public/simulation/ai/common-api/entity.js

     
    744744        return false;
    745745    },
    746746
     747    isCapturable: function() { return this.get("Capturable") !== undefined; },
    747748    isGarrisonHolder: function() { return this.get("GarrisonHolder") !== undefined; },
    748749    garrisoned: function() { return this._entity.garrisoned; },
    749750    canGarrisonInside: function() { return this._entity.garrisoned.length < this.garrisonMax(); },
     
    754755    },
    755756
    756757    moveToRange: function(x, z, min, max, queued = false) {
    757         Engine.PostCommand(PlayerID,{"type": "walk-to-range", "entities": [this.id()], "x": x, "z": z, "min": min, "max": max, "queued": queued });
     758        Engine.PostCommand(PlayerID,{ "type": "walk-to-range", "entities": [this.id()], "x": x, "z": z, "min": min, "max": max, "queued": queued });
    758759        return this;
    759760    },
    760761
    761     attackMove: function(x, z, targetClasses, queued = false) {
    762         Engine.PostCommand(PlayerID,{"type": "attack-walk", "entities": [this.id()], "x": x, "z": z, "targetClasses": targetClasses, "queued": queued });
     762    attackMove: function(x, z, targetClasses, prefAttackType = "undefined", queued = false) { // hack
     763        Engine.PostCommand(PlayerID,{"type": "attack-walk", "entities": [this.id()], "x": x, "z": z, "targetClasses": targetClasses, "prefAttackType": prefAttackType, "queued": queued});
    763764        return this;
    764765    },
    765766
     
    793794        return this;
    794795    },
    795796
    796     attack: function(unitId, allowCapture = true, queued = false) {
    797         Engine.PostCommand(PlayerID,{"type": "attack", "entities": [this.id()], "target": unitId, "allowCapture": allowCapture, "queued": queued});
     797    attack: function(unitId, prefAttackType = undefined, queued = false) {
     798        Engine.PostCommand(PlayerID,{"type": "attack", "entities": [this.id()], "target": unitId, "prefAttackType": prefAttackType, "queued": queued});
    798799        return this;
    799800    },
    800801
  • binaries/data/mods/public/simulation/ai/common-api/entitycollection.js

     
    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
     
    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
  • binaries/data/mods/public/simulation/ai/petra/attackPlan.js

     
    12361236                {
    12371237                    if (this.isSiegeUnit(gameState, ent))   // needed as mauryan elephants are not filtered out
    12381238                        continue;
    1239                     ent.attack(attacker.id(), !this.noCapture.has(attacker.id()));
     1239                    ent.attack(attacker.id(), m.getPrefAttackType(ent, attacker, this.noCapture));
    12401240                    ent.setMetadata(PlayerID, "lastAttackPlanUpdateTime", time);
    12411241                }
    12421242                // And if this attacker is a non-ranged siege unit and our unit also, attack it
    12431243                if (this.isSiegeUnit(gameState, attacker) && attacker.hasClass("Melee") && ourUnit.hasClass("Melee"))
    12441244                {
    1245                     ourUnit.attack(attacker.id(), false);
     1245                    ourUnit.attack(attacker.id(), "Melee");
    12461246                    ourUnit.setMetadata(PlayerID, "lastAttackPlanUpdateTime", time);
    12471247                }
    12481248            }
     
    12591259                    let collec = this.unitCollection.filter(API3.Filters.byClass("Melee")).filterNearest(ourUnit.position(), 5);
    12601260                    for (let ent of collec.values())
    12611261                    {
    1262                         ent.attack(attacker.id(), false);
     1262                        ent.attack(attacker.id(), m.getPrefAttackType(ent, attacker, this.noCapture));
    12631263                        ent.setMetadata(PlayerID, "lastAttackPlanUpdateTime", time);
    12641264                    }
    12651265                }
     
    12781278                                continue;
    12791279                        }
    12801280                    }
    1281                     ourUnit.attack(attacker.id(), !this.noCapture.has(attacker.id()));
     1281                    ourUnit.attack(attacker.id(), m.getPrefAttackType(ourUnit, attacker, this.noCapture));
    12821282                    ourUnit.setMetadata(PlayerID, "lastAttackPlanUpdateTime", time);
    12831283                }
    12841284            }
     
    14581458                        return valb - vala;
    14591459                    });
    14601460                    if (mStruct[0].hasClass("Gates"))
    1461                         ent.attack(mStruct[0].id(), false);
     1461                        ent.attack(mStruct[0].id(), m.getPrefAttackType(ent, mStruct[0], this.noCapture));
    14621462                    else
    14631463                    {
    14641464                        let rand = Math.floor(Math.random() * mStruct.length * 0.2);
    1465                         let newTargetId = mStruct[rand].id();
    1466                         ent.attack(newTargetId, !this.noCapture.has(newTargetId));
     1465                        let newTarget = mStruct[rand];
     1466                        ent.attack(newTarget.id(), m.getPrefAttackType(ent, newTarget, this.noCapture));
    14671467                    }
    14681468                }
    14691469                else
     
    15211521                        return valb - vala;
    15221522                    });
    15231523                    let rand = Math.floor(Math.random() * mUnit.length * 0.1);
    1524                     let newTargetId = mUnit[rand].id();
    1525                     ent.attack(newTargetId, !this.noCapture.has(newTargetId));
     1524                    let newTarget = mUnit[rand];
     1525                    ent.attack(newTarget.id(), m.getPrefAttackType(ent, newTarget, this.noCapture));
    15261526                }
    15271527                else if (this.isBlocked)
    15281528                    ent.attack(this.target.id(), false);
     
    15691569                            return valb - vala;
    15701570                        });
    15711571                        if (mStruct[0].hasClass("Gates"))
    1572                             ent.attack(mStruct[0].id(), false);
     1572                            ent.attack(mStruct[0].id(), m.getPrefAttackType(ent, mStruct[0], this.noCapture));
    15731573                        else
    15741574                        {
    15751575                            let rand = Math.floor(Math.random() * mStruct.length * 0.2);
    1576                             let newTargetId = mStruct[rand].id();
    1577                             ent.attack(newTargetId, !this.noCapture.has(newTargetId));
     1576                            let newTarget = mStruct[rand];
     1577                            ent.attack(newTarget.id(), m.getPrefAttackType(ent, newTarget, this.noCapture));
    15781578                        }
    15791579                    }
    15801580                    else if (needsUpdate)  // really nothing   let's try to help our nearest unit
     
    15811581                    {
    15821582                        let distmin = Math.min();
    15831583                        let attackerId;
     1584                        let attacker;
    15841585                        this.unitCollection.forEach( function (unit) {
    15851586                            if (!unit.position())
    15861587                                return;
     
    15901591                            let dist = API3.SquareVectorDistance(unit.position(), ent.position());
    15911592                            if (dist > distmin)
    15921593                                return;
     1594                            if (!gameState.getEntityById(unit.unitAIOrderData()[0].target))
     1595                                return;
    15931596                            distmin = dist;
    15941597                            attackerId = unit.unitAIOrderData()[0].target;
    1595 
     1598                            attacker = gameState.getEntityById(attackerId);
    15961599                        });
    15971600                        if (attackerId)
    1598                             ent.attack(attackerId, !this.noCapture.has(attackerId));
     1601                            ent.attack(attackerId, m.getPrefAttackType(ent, attacker, this.noCapture));
    15991602                    }
    16001603                }
    16011604            }
     
    16481651                continue;
    16491652            if (!ent.isIdle())
    16501653                continue;
    1651             ent.attack(attacker.id(), !this.noCapture.has(attacker.id()));
     1654            ent.attack(attacker.id(), !this.noCapture.has(attacker.id(), this.noCapture));
    16521655        }
    16531656        break;
    16541657    }
     
    20042007
    20052008    if (this.noCapture.has(targetId))
    20062009    {
    2007         ent.attack(targetId, false);
     2010        ent.attack(targetId, m.getPrefAttackType(ent, target, this.noCapture));
    20082011        return true;
    20092012    }
    20102013
     
    20162019    if (target.hasClass("Siege") && target.hasClass("Melee"))
    20172020    {
    20182021        this.noCapture.add(targetId);
    2019         ent.attack(targetId, false);
     2022        ent.attack(targetId, m.getPrefAttackType(ent, target, this.noCapture));
    20202023        return true;
    20212024    }
    20222025
     
    20332036    if (antiCapture >= this.captureStrength)
    20342037    {
    20352038        this.noCapture.add(targetId);
    2036         ent.attack(targetId, false);
     2039        ent.attack(targetId, "primary"); // hack
    20372040        return true;
    20382041    }
    20392042
     
    20422045        this.unitCollection.length < 2*target.garrisoned().length)
    20432046    {
    20442047        this.noCapture.add(targetId);
    2045         ent.attack(targetId, false);
     2048        ent.attack(targetId, "primary"); // hack
    20462049        return true;
    20472050    }
    20482051
  • 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.getPrefAttackType(ent, foeEnt), queued);
    8181    }
    8282    else
    8383        gameState.ai.HQ.navalManager.requireTransport(gameState, ent, ownIndex, foeIndex, foePosition);
     
    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
  • binaries/data/mods/public/simulation/ai/petra/entityExtend.js

     
    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 */
  • binaries/data/mods/public/simulation/components/Attack.js

     
    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>" +
     
    220244{
    221245    if (this.template[type] && this.template[type].PreferredClasses &&
    222246        this.template[type].PreferredClasses._string)
    223     {
    224247        return this.template[type].PreferredClasses._string.split(/\s+/);
    225     }
    226248    return [];
    227249};
    228250
     
    230252{
    231253    if (this.template[type] && this.template[type].RestrictedClasses &&
    232254        this.template[type].RestrictedClasses._string)
    233     {
    234255        return this.template[type].RestrictedClasses._string.split(/\s+/);
    235     }
    236256    return [];
    237257};
    238258
    239 Attack.prototype.CanAttack = function(target)
     259Attack.prototype.GetChangeDistance = function()
    240260{
     261    return +(this.template.ChangeDistance || 0);
     262};
     263
     264Attack.prototype.CanAttack = function(target, wantedType)
     265{
    241266    let cmpFormation = Engine.QueryInterface(target, IID_Formation);
    242267    if (cmpFormation)
    243268        return true;
    244269
     270    if (wantedType && !this.template[wantedType])
     271        return false;
     272
    245273    let cmpThisPosition = Engine.QueryInterface(this.entity, IID_Position);
    246274    let cmpTargetPosition = Engine.QueryInterface(target, IID_Position);
    247275    if (!cmpThisPosition || !cmpTargetPosition || !cmpThisPosition.IsInWorld() || !cmpTargetPosition.IsInWorld())
    248276        return false;
    249277
    250     // Check if the relative height difference is larger than the attack range
    251     // If the relative height is bigger, it means they will never be able to
    252     // reach each other, no matter how close they come.
    253     let heightDiff = Math.abs(cmpThisPosition.GetHeightOffset() - cmpTargetPosition.GetHeightOffset());
    254 
    255278    const cmpIdentity = Engine.QueryInterface(target, IID_Identity);
    256279    if (!cmpIdentity)
    257280        return undefined;
    258281
     282    let cmpEntityPlayer = QueryOwnerInterface(this.entity);
     283    let cmpTargetPlayer = QueryOwnerInterface(target);
     284    if (!cmpTargetPlayer || !cmpEntityPlayer)
     285        return false;
     286
    259287    const targetClasses = cmpIdentity.GetClassesList();
     288    let cmpCapturable = QueryMiragedInterface(target, IID_Capturable);
     289    if (targetClasses.indexOf("Domestic") == -1 && !cmpEntityPlayer.IsEnemy(cmpTargetPlayer.GetPlayerID()) &&
     290       (!cmpCapturable || !cmpCapturable.CanCapture(cmpEntityPlayer.GetPlayerID())))
     291        return false;
    260292
     293    // Check if the relative height difference is larger than the attack range
     294    // If the relative height is bigger, it means they will never be able to
     295    // reach each other, no matter how close they come.
     296    let heightDiff = Math.abs(cmpThisPosition.GetHeightOffset() - cmpTargetPosition.GetHeightOffset());
     297
    261298    for (let type of this.GetAttackTypes())
    262299    {
    263         if (type == "Capture" && !QueryMiragedInterface(target, IID_Capturable))
     300        if (wantedType && type != wantedType)
    264301            continue;
    265302
     303        if (type == "Capture" && !cmpCapturable)
     304            continue;
     305
    266306        if (type == "Slaughter" && targetClasses.indexOf("Domestic") == -1)
    267307            continue;
    268308
     
    327367        if (type == "Slaughter")
    328368            continue;
    329369        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;
     370        ret.min = Math.min(ret.min, range.min);
     371        ret.max = Math.max(ret.max, range.max);
    334372    }
    335373    return ret;
    336374};
    337375
    338 Attack.prototype.GetBestAttackAgainst = function(target, allowCapture)
     376Attack.prototype.GetBestAttackAgainst = function(target, prefAttackType)
    339377{
    340378    let cmpFormation = Engine.QueryInterface(target, IID_Formation);
    341379    if (cmpFormation)
    342380    {
    343381        // TODO: Formation against formation needs review
     382
     383
    344384        let best = ["Ranged", "Melee", "Capture"];
    345385        let types = this.GetAttackTypes();
    346386        for (let attack of best)
    347             if (types.indexOf(attack) != -1)
     387            if (types.indexOf(attack) != -1 && (!prefAttackType || "no" + attack != prefAttackType))
    348388                return attack;
    349389        return undefined;
    350390    }
     
    353393    if (!cmpIdentity)
    354394        return undefined;
    355395
     396    // If we are visisble garrisoned always do ranged attack if we can.
     397    let cmpPosition = Engine.QueryInterface(this.entity, IID_Position);
     398    if (cmpPosition && cmpPosition.GetTurretParent() != INVALID_ENTITY)
     399        return this.template.Ranged && "Ranged";
     400
    356401    let targetClasses = cmpIdentity.GetClassesList();
    357402    let isTargetClass = function (className) { return targetClasses.indexOf(className) != -1; };
    358403
    359404    // Always slaughter domestic animals instead of using a normal attack
     405
    360406    if (isTargetClass("Domestic") && this.template.Slaughter)
    361407        return "Slaughter";
    362408
    363409    let attack = this;
    364     let isAllowed = function (type) { return !attack.GetRestrictedClasses(type).some(isTargetClass); };
     410    let isAllowed = function (type) { return !attack.GetRestrictedClasses(type).some(isTargetClass) && (!prefAttackType || "no" + type != prefAttackType); };
    365411
    366412    let types = this.GetAttackTypes().filter(isAllowed);
     413    if (!types.length)
     414        return undefined;
    367415
    368     // check if the target is capturable
    369     let captureIndex = types.indexOf("Capture");
    370     if (captureIndex != -1)
     416    prefAttackType = this.GetAttackTypeFromOrder(prefAttackType);
     417    if (prefAttackType && prefAttackType != "Capture" && types.indexOf(prefAttackType) != -1)
     418        return prefAttackType;
     419    else if (!prefAttackType || prefAttackType == "Capture")
    371420    {
    372         let cmpCapturable = QueryMiragedInterface(target, IID_Capturable);
    373 
    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);
     421        // check if the target is capturable
     422        let captureIndex = types.indexOf("Capture");
     423        if (captureIndex != -1)
     424        {
     425            let cmpCapturable = QueryMiragedInterface(target, IID_Capturable);
     426            let cmpPlayer = QueryOwnerInterface(this.entity);
     427            if (cmpPlayer && cmpCapturable && cmpCapturable.CanCapture(cmpPlayer.GetPlayerID()))
     428                return "Capture";
     429            // not captureable, so remove this attack
     430            types.splice(captureIndex, 1);
     431        }
    379432    }
    380433
    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) ); };
     434    // ignore charges for now: TODO implement these
     435    let chargeIndex = types.indexOf("Charge");
     436    if (chargeIndex != -1)
     437        types.splice(chargeIndex, 1);
    383438
    384     return types.sort(byPreference).pop();
     439    if (!types.length)
     440        return undefined;
     441
     442    // assume ranged and/or melee attack left
     443    // TODO stop assuming that?
     444    let meleeIndex = types.indexOf("Melee");
     445    let rangedIndex = types.indexOf("Ranged");
     446    if (meleeIndex != -1 && rangedIndex != -1)
     447    {
     448        if (this.HasTargetPreferredClass(types, targetClasses))
     449        {
     450            let isPreferred = function (className) { return attack.GetPreferredClasses(className).some(isTargetClass); };
     451            let byPreference = function (a, b) { return (types.indexOf(a) + (isPreferred(a) ? types.length : 0) ) - (types.indexOf(b) + (isPreferred(b) ? types.length : 0) ); };
     452
     453            return types.sort(byPreference).pop();
     454        }
     455
     456        if (!cmpPosition.IsInWorld())
     457            return undefined;
     458        let selfPosition = cmpPosition.GetPosition2D();
     459        let cmpTargetPosition = Engine.QueryInterface(target, IID_Position);
     460        if (!cmpTargetPosition || !cmpTargetPosition.IsInWorld())
     461            return undefined;
     462        let targetPosition = cmpTargetPosition.GetPosition2D();
     463        if (targetPosition.distanceToSquared(selfPosition) <= Math.pow(this.GetChangeDistance(), 2))
     464            return "Melee";
     465        return "Ranged";
     466    }
     467    else if (meleeIndex != -1)
     468        return "Melee";
     469    else if (rangedIndex != -1)
     470        return "Ranged";
     471    return types[0];
    385472};
    386473
    387474Attack.prototype.CompareEntitiesByPreference = function(a, b)
     
    478565    return attackBonus;
    479566};
    480567
     568Attack.prototype.GetAttackTypeFromOrder = function(prefAttackType)
     569{
     570    let types = this.GetAttackTypes();
     571    for (let type of types)
     572        if (this.template[type].AttackOrder && this.template[type].AttackOrder == prefAttackType)
     573            return type;
     574    return prefAttackType;
     575};
     576
     577Attack.prototype.HasTargetPreferredClass = function(types, targetClasses)
     578{
     579    let isTargetClass = function (className) { return targetClasses.indexOf(className) != -1; };
     580    for (let type of types)
     581        if (this.template[type].PreferredClasses && this.GetPreferredClasses(type).some(isTargetClass))
     582            return true;
     583    return false
     584};
     585
    481586// Returns a 2d random distribution scaled for a spread of scale 1.
    482587// The current implementation is a 2d gaussian with sigma = 1
    483588Attack.prototype.GetNormalDistribution = function(){
     
    486591    let a = Math.random();
    487592    let b = Math.random();
    488593
    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);
     594    let c = Math.sqrt(-2 * Math.log(a)) * Math.cos(2 * Math.PI * b);
     595    let d = Math.sqrt(-2 * Math.log(a)) * Math.sin(2 * Math.PI * b);
    491596
    492597    return [c, d];
    493598};
     
    503608    if (type == "Ranged")
    504609    {
    505610        let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
    506         let turnLength = cmpTimer.GetLatestTurnLength()/1000;
     611        let turnLength = cmpTimer.GetLatestTurnLength() / 1000;
    507612        // In the future this could be extended:
    508613        //  * Obstacles like trees could reduce the probability of the target being hit
    509614        //  * Obstacles like walls should block projectiles entirely
     
    535640
    536641        let horizDistance = targetPosition.horizDistanceTo(selfPosition);
    537642
    538         // This is an approximation of the time ot the target, it assumes that the target has a constant radial
     643        // This is an approximation of the time to the target, it assumes that the target has a constant radial
    539644        // velocity, but since units move in straight lines this is not true.  The exact value would be more
    540645        // difficult to calculate and I think this is sufficiently accurate.  (I tested and for cavalry it was
    541646        // about 5% of the units radius out in the worst case)
     
    620725Attack.prototype.InterpolatedLocation = function(ent, lateness)
    621726{
    622727    let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
    623     let turnLength = cmpTimer.GetLatestTurnLength()/1000;
     728    let turnLength = cmpTimer.GetLatestTurnLength() / 1000;
    624729    let cmpTargetPosition = Engine.QueryInterface(ent, IID_Position);
    625730    if (!cmpTargetPosition || !cmpTargetPosition.IsInWorld()) // TODO: handle dead target properly
    626731        return undefined;
     
    658763        let d = Vector3D.sub(point, targetPosition);
    659764        d = Vector2D.from3D(d).rotate(-angle);
    660765
    661         return d.x < Math.abs(targetShape.width/2) && d.y < Math.abs(targetShape.depth/2);
     766        return d.x < Math.abs(targetShape.width / 2) && d.y < Math.abs(targetShape.depth / 2);
    662767    }
    663768};
    664769
     
    740845        return;
    741846
    742847    for (let type of this.GetAttackTypes())
    743         if (msg.valueNames.indexOf("Attack/"+type+"/MaxRange") !== -1)
     848        if (msg.valueNames.indexOf("Attack/" + type + "/MaxRange") !== -1)
    744849        {
    745850            cmpUnitAI.UpdateRangeQueries();
    746851            return;
  • binaries/data/mods/public/simulation/components/GarrisonHolder.js

     
    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>" +
     
    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()
  • binaries/data/mods/public/simulation/components/GuiInterface.js

     
    352352        ret.garrisonHolder = {
    353353            "entities": cmpGarrisonHolder.GetEntities(),
    354354            "allowedClasses": cmpGarrisonHolder.GetAllowedClasses(),
     355            "neededAttackTypes": cmpGarrisonHolder.GetNeededAttackTypes(),
    355356            "capacity": cmpGarrisonHolder.GetCapacity(),
    356357            "garrisonedEntitiesCount": cmpGarrisonHolder.GetGarrisonedEntitiesCount()
    357358        };
     
    432433        let types = cmpAttack.GetAttackTypes();
    433434        if (types.length)
    434435            ret.attack = {};
     436
    435437        for (let type of types)
    436438        {
    437439            ret.attack[type] = cmpAttack.GetAttackStrengths(type);
     
    18061808    if (!cmpAttack)
    18071809        return false;
    18081810
    1809     let cmpEntityPlayer = QueryOwnerInterface(data.entity, IID_Player);
    1810     let cmpTargetPlayer = QueryOwnerInterface(data.target, IID_Player);
    1811     if (!cmpEntityPlayer || !cmpTargetPlayer)
     1811    return cmpAttack.CanAttack(data.target);
     1812};
     1813
     1814GuiInterface.prototype.CanAttackWithType = function(player, data)
     1815{
     1816    let cmpAttack = Engine.QueryInterface(data.entity, IID_Attack);
     1817    if (!cmpAttack)
    18121818        return false;
    18131819
    1814     // if the owner is an enemy, it's up to the attack component to decide
    1815     if (cmpEntityPlayer.IsEnemy(cmpTargetPlayer.GetPlayerID()))
    1816         return cmpAttack.CanAttack(data.target);
    1817 
    1818     return false;
     1820    return cmpAttack.CanAttack(data.target, data.type);
    18191821};
    18201822
    18211823/*
     
    19621964    "GetTradingDetails": 1,
    19631965    "CanCapture": 1,
    19641966    "CanAttack": 1,
     1967    "CanAttackWithType": 1,
    19651968    "GetBatchTime": 1,
    19661969
    19671970    "IsMapRevealed": 1,
  • binaries/data/mods/public/simulation/components/UnitAI.js

     
    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
     
    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
     
    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
     
    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
     
    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
     
    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
     
    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
     
    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();
     
    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();
     
    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);
     
    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");
     
    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");
     
    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)
     
    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
     
    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
     
    49925000 * to a player order, and so is forced.
    49935001 * If targetClasses is given, only entities matching the targetClasses can be attacked.
    49945002 */
    4995 UnitAI.prototype.WalkAndFight = function(x, z, targetClasses, queued)
     5003UnitAI.prototype.WalkAndFight = function(x, z, targetClasses, queued, prefAttackType)
    49965004{
    4997     this.AddOrder("WalkAndFight", { "x": x, "z": z, "targetClasses": targetClasses, "force": true }, queued);
     5005    this.AddOrder("WalkAndFight", { "x": x, "z": z, "targetClasses": targetClasses, "force": true, "prefAttackType": prefAttackType }, queued);
    49985006};
    49995007
    50005008/**
     
    50155023/**
    50165024 * Adds attack order to the queue, forced by the player.
    50175025 */
    5018 UnitAI.prototype.Attack = function(target, queued, allowCapture)
     5026UnitAI.prototype.Attack = function(target, queued, prefAttackType)
    50195027{
    50205028    if (!this.CanAttack(target))
    50215029    {
     
    50275035            this.WalkToTarget(target, queued);
    50285036        return;
    50295037    }
    5030     this.AddOrder("Attack", { "target": target, "force": true, "allowCapture": allowCapture}, queued);
     5038    this.AddOrder("Attack", { "target": target, "force": true, "prefAttackType": prefAttackType }, queued);
    50315039};
    50325040
    50335041/**
     
    54565464                    if (targetClasses.vetoEntities && targetClasses.vetoEntities[targ])
    54575465                        continue;
    54585466                }
    5459                 this.PushOrderFront("Attack", { "target": targ, "force": true, "allowCapture": true });
     5467                this.PushOrderFront("Attack", { "target": targ, "force": true, "prefAttackType": undefined });
    54605468                return true;
    54615469            }
    54625470        }
     
    54825490            if (targetClasses.vetoEntities && targetClasses.vetoEntities[targ])
    54835491                continue;
    54845492        }
    5485         this.PushOrderFront("Attack", { "target": targ, "force": true, "allowCapture": true });
     5493        this.PushOrderFront("Attack", { "target": targ, "force": true, "prefAttackType": undefined });
    54865494        return true;
    54875495    }
    54885496
     
    56825690    if (this.IsFormationController())
    56835691        return true;
    56845692
    5685     var cmpGarrisonHolder = Engine.QueryInterface(target, IID_GarrisonHolder);
     5693    let cmpGarrisonHolder = Engine.QueryInterface(target, IID_GarrisonHolder);
    56865694    if (!cmpGarrisonHolder)
    56875695        return false;
    56885696
    56895697    // Verify that the target is owned by this entity's player or a mutual ally of this player
    5690     var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership);
     5698    let cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership);
    56915699    if (!cmpOwnership || !(IsOwnedByPlayer(cmpOwnership.GetOwner(), target) || IsOwnedByMutualAllyOfPlayer(cmpOwnership.GetOwner(), target)))
    56925700        return false;
    56935701
     
    56975705    if (this.IsAnimal())
    56985706        return false;
    56995707
     5708    let cmpIdentity = Engine.QueryInterface(this.entity, IID_Identity)
     5709    if (!cmpIdentity)
     5710        return false;
     5711
     5712    let cmpAttack = Engine.QueryInterface(this.entity, IID_Attack)
     5713    let attackTypes = cmpAttack ? cmpAttack.GetAttackTypes() : [];
     5714    if (!MatchesClassList(cmpIdentity.GetClassesList(), cmpGarrisonHolder.GetAllowedClasses()) ||
     5715        !HasNeededAttackTypes(attackTypes, cmpGarrisonHolder.GetNeededAttackTypes()))
     5716        return false;
    57005717    return true;
    57015718};
    57025719
  • binaries/data/mods/public/simulation/components/tests/test_UnitAI.js

     
    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; },
     
    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; },
  • 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.prefAttackType);
    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.prefAttackType);
    174173        });
    175174    },
    176175
  • binaries/data/mods/public/simulation/templates/template_structure_defense_wall_long.xml

     
    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>
  • binaries/data/mods/public/simulation/templates/template_structure_defense_wall_medium.xml

     
    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>
  • 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>