Ticket #252: t252_secondattack_13.diff

File t252_secondattack_13.diff, 56.8 KB (added by bb, 8 years ago)

cleanup as above

  • binaries/data/config/default.cfg

     
    274274[hotkey.session]
    275275kill = Delete                ; Destroy selected units
    276276stop = "H"                   ; Stop the current action
    277 attack = Ctrl                ; Modifier to attack instead of another action (eg capture)
    278 attackmove = Ctrl            ; Modifier to attackmove when clicking on a point
    279 attackmoveUnit = "Ctrl+Q"    ; Modifier to attackmove targeting only units when clicking on a point (should contain the attackmove keys)
     277attack = Ctrl                ; Modifier to primary attack instead of another action (eg capture)
     278attackmove = Ctrl            ; Modifier to primary attackmove when clicking on a point
     279attackmoveUnit = "Ctrl+Q"    ; Modifier to primary attackmove targeting only units when clicking on a point (should contain the attackmove keys)
    280280garrison = Ctrl              ; Modifier to garrison when clicking on building
    281281autorallypoint = Ctrl        ; Modifier to set the rally point on the building itself
     282meleeattack = Alt            ; Modifier to melee attack instead of another action
     283meleeattackmove = Alt        ; Modifier to melee attackmove when clicking on a point
     284meleeattackmoveUnit = "Alt+Q"; Modifier to melee attackmove targeting only units when clicking on a point
     285rangedattack = space         ; Modifier to ranged attack instead of another action
     286rangedattackmove = space     ; Modifier to ranged attackmove when clicking on a point
     287rangedattackmoveUnit = "space+Q"; Modifier to ranged attackmove targeting only units when clicking on a point
    282288guard = "G"                  ; Modifier to escort/guard when clicking on unit/building
    283289queue = Shift                ; Modifier to queue unit orders instead of replacing
    284290batchtrain = 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/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

     
    11/**
    2  * List of different actions units can execute, 
     2 * List of different actions units can execute,
    33 * this is mostly used to determine which actions can be executed
    44 *
    55 * "execute" is meant to send the command to the engine
    66 *
    7  * The next functions will always return false 
     7 * The next functions will always return false
    88 * in case you have to continue to seek
    9  * (i.e. look at the next entity for getActionInfo, the next 
     9 * (i.e. look at the next entity for getActionInfo, the next
    1010 * possible action for the actionCheck ...)
    1111 * They will return an object when the searching is finished
    1212 *
     
    2424 *
    2525 * "specificness" is used to determine how specific an action is,
    2626 * The lower the number, the more specific an action is, and the bigger
    27  * the chance of selecting that action when multiple actions are possible 
     27 * the chance of selecting that action when multiple actions are possible
    2828 */
    2929
    30 var unitActions = 
     30var unitActions =
    3131{
    3232    "move":
    3333    {
     
    5353                return {"type": "move"};
    5454            return false;
    5555        },
    56         "specificness": 12,
     56        "specificness": 13,
    5757    },
    5858
    59     "attack-move": 
     59    "attack-move":
    6060    {
    6161        "execute": function(target, action, selection, queued)
    6262        {
     
    6565            else
    6666                var targetClasses = { "attack": ["Unit", "Structure"] };
    6767
    68             Engine.PostNetworkCommand({"type": "attack-walk", "entities": selection, "x": target.x, "z": target.z, "targetClasses": targetClasses, "queued": queued});
     68            Engine.PostNetworkCommand({ "type": "attack-walk", "entities": selection, "x": target.x, "z": target.z, "targetClasses": targetClasses, "queued": queued, "prefAttackType": "primary" });
    6969            Engine.GuiInterfaceCall("PlaySound", { "name": "order_walk", "entity": selection[0] });
    7070            return true;
    7171        },
    7272        "getActionInfo": function(entState, targetState)
    7373        {
    74             return {"possible": true};
     74            return { "possible": true };
    7575        },
    7676        "hotkeyActionCheck": function(target, selection)
    7777        {
     
    8181                return entState && entState.unitAI;
    8282            });
    8383            if (haveUnitAI && Engine.HotkeyIsPressed("session.attackmove") && getActionInfo("attack-move", target).possible)
    84                 return {"type": "attack-move", "cursor": "action-attack-move"};
     84                return { "type": "attack-move", "cursor": "action-attack-move" };
    8585            return false;
    8686        },
    8787        "specificness": 30,
    8888    },
    8989
    90     "capture":
     90    "melee-attack-move":
    9191    {
    9292        "execute": function(target, action, selection, queued)
    9393        {
    94             Engine.PostNetworkCommand({"type": "attack", "entities": selection, "target": action.target, "allowCapture": true, "queued": queued});
     94            if (Engine.HotkeyIsPressed("session.meleeattackmoveUnit"))
     95                var targetClasses = { "meleeattack": ["Unit"] };
     96            else
     97                var targetClasses = { "meleeattack": ["Unit", "Structure"] };
     98
     99            Engine.PostNetworkCommand({ "type": "attack-walk", "entities": selection, "x": target.x, "z": target.z, "targetClasses": targetClasses, "queued": queued, "prefAttackType": "Melee" });
     100            Engine.GuiInterfaceCall("PlaySound", { "name": "order_walk", "entity": selection[0] });
     101            return true;
     102        },
     103        "getActionInfo": function(entState, targetState)
     104        {
     105            return { "possible": true };
     106        },
     107        "hotkeyActionCheck": function(target, selection)
     108        {
     109            // Work out whether at least part of the selection have UnitAI
     110            let haveUnitAI = selection.some(function(ent) {
     111                let entState = GetEntityState(ent);
     112                return entState && entState.unitAI && getActionInfo("melee-attack-move", target).possible;
     113            });
     114
     115            if (haveUnitAI && Engine.HotkeyIsPressed("session.meleeattackmove") && getActionInfo("melee-attack-move", target).possible)
     116                return { "type": "melee-attack-move", "cursor": "action-melee-attack" };
     117            return false;
     118        },
     119        "specificness": 31,
     120    },
     121
     122    "ranged-attack-move":
     123    {
     124        "execute": function(target, action, selection, queued)
     125        {
     126            if (Engine.HotkeyIsPressed("session.rangedattackmoveUnit"))
     127                var targetClasses = { "rangedattack": ["Unit"] };
     128            else
     129                var targetClasses = { "rangedattack": ["Unit", "Structure"] };
     130
     131            Engine.PostNetworkCommand({ "type": "attack-walk", "entities": selection, "x": target.x, "z": target.z, "targetClasses": targetClasses, "queued": queued, "prefAttackType": "Ranged" });
     132            Engine.GuiInterfaceCall("PlaySound", { "name": "order_walk", "entity": selection[0] });
     133            return true;
     134        },
     135        "getActionInfo": function(entState, targetState)
     136        {
     137            return { "possible": true };
     138        },
     139        "hotkeyActionCheck": function(target, selection)
     140        {
     141            // Work out whether at least part of the selection have UnitAI
     142            let haveUnitAI = selection.some(function(ent) {
     143                let entState = GetEntityState(ent);
     144                return entState && entState.unitAI && getActionInfo("ranged-attack-move", target).possible;
     145            });
     146
     147            if (haveUnitAI && Engine.HotkeyIsPressed("session.rangedattackmove") && getActionInfo("ranged-attack-move", target).possible)
     148                return { "type": "ranged-attack-move", "cursor": "action-ranged-attack" };
     149            return false;
     150        },
     151        "specificness": 32,
     152    },
     153
     154    "capture":
     155    {
     156        "execute": function(target, action, selection, queued)
     157        {
     158            Engine.PostNetworkCommand({ "type": "attack", "entities": selection, "target": action.target, "queued": queued, "prefAttackType": "Capture" });
    95159            Engine.GuiInterfaceCall("PlaySound", { "name": "order_attack", "entity": selection[0] });
    96160            return true;
    97161        },
     
    99163        {
    100164            if (!entState.attack || !targetState.hitpoints)
    101165                return false;
    102             return {"possible": Engine.GuiInterfaceCall("CanCapture", {"entity": entState.id, "target": targetState.id})};
     166            return { "possible": Engine.GuiInterfaceCall("CanCapture", { "entity": entState.id, "target": targetState.id }) };
    103167        },
    104168        "actionCheck": function(target)
    105169        {
    106170            if (getActionInfo("capture", target).possible)
    107                 return {"type": "capture", "cursor": "action-capture", "target": target};
     171                return { "type": "capture", "cursor": "action-capture", "target": target };
    108172            return false;
    109173        },
    110         "specificness": 9,
     174        "specificness": 7,
    111175    },
    112176
    113177    "attack":
     
    114178    {
    115179        "execute": function(target, action, selection, queued)
    116180        {
    117             Engine.PostNetworkCommand({"type": "attack", "entities": selection, "target": action.target, "queued": queued, "allowCapture": false});
     181            Engine.PostNetworkCommand({ "type": "attack", "entities": selection, "target": action.target, "queued": queued, "prefAttackType": "primary" });
    118182            Engine.GuiInterfaceCall("PlaySound", { "name": "order_attack", "entity": selection[0] });
    119183            return true;
    120184        },
     
    122186        {
    123187            if (!entState.attack || !targetState.hitpoints)
    124188                return false;
    125             return {"possible": Engine.GuiInterfaceCall("CanAttack", {"entity": entState.id, "target": targetState.id})};
     189            return { "possible": Engine.GuiInterfaceCall("CanAttack", { "entity": entState.id, "target": targetState.id }) };
    126190        },
    127191        "hotkeyActionCheck": function(target)
    128192        {
    129193            if (Engine.HotkeyIsPressed("session.attack") && getActionInfo("attack", target).possible)
    130                 return {"type": "attack", "cursor": "action-attack", "target": target};
     194                return { "type": "attack", "cursor": "action-attack", "target": target };
    131195            return false;
    132196        },
    133197        "actionCheck": function(target)
    134198        {
    135199            if (getActionInfo("attack", target).possible)
    136                 return {"type": "attack", "cursor": "action-attack", "target": target};
     200                return { "type": "attack", "cursor": "action-attack", "target": target };
    137201            return false;
    138202        },
     203        "specificness": 8,
     204    },
     205
     206    "melee-attack":
     207    {
     208        "execute": function(target, action, selection, queued)
     209        {
     210            Engine.PostNetworkCommand({ "type": "attack", "entities": selection, "target": action.target, "queued": queued, "prefAttackType": "Melee" });
     211            Engine.GuiInterfaceCall("PlaySound", { "name": "order_attack", "entity": selection[0] });
     212            return true;
     213        },
     214        "getActionInfo": function(entState, targetState)
     215        {
     216            if (!entState.attack || !targetState.hitpoints) // hack
     217                return false;
     218            return { "possible": Engine.GuiInterfaceCall("CanAttackWithType", { "entity": entState.id, "target": targetState.id, "type": "Melee" }) };
     219        },
     220        "hotkeyActionCheck": function(target)
     221        {
     222            if (Engine.HotkeyIsPressed("session.meleeattack") && getActionInfo("melee-attack", target).possible)
     223                return { "type": "melee-attack", "cursor": "action-melee-attack", "target": target };
     224            return false;
     225        },
     226        "actionCheck": function(target)
     227        {
     228            if (getActionInfo("melee-attack", target).possible)
     229                    return { "type": "melee-attack", "cursor": "action-melee-attack", "target": target };
     230            return false;
     231        },
     232        "specificness": 9,
     233    },
     234
     235    "ranged-attack":
     236    {
     237        "execute": function(target, action, selection, queued)
     238        {
     239            Engine.PostNetworkCommand({ "type": "attack", "entities": selection, "target": action.target, "queued": queued, "prefAttackType": "Ranged" });
     240            Engine.GuiInterfaceCall("PlaySound", { "name": "order_attack", "entity": selection[0] });
     241            return true;
     242        },
     243        "getActionInfo": function(entState, targetState)
     244        {
     245            if (!entState.attack || !targetState.hitpoints) // hack
     246                return false;
     247            return { "possible": Engine.GuiInterfaceCall("CanAttackWithType", { "entity": entState.id, "target": targetState.id, "type": "Ranged" }) };
     248        },
     249        "hotkeyActionCheck": function(target, selection)
     250        {
     251            if (Engine.HotkeyIsPressed("session.rangedattack") && getActionInfo("ranged-attack", target).possible)
     252                return { "type": "ranged-attack", "cursor": "action-ranged-attack", "target": target };
     253            return false;
     254        },
     255        "actionCheck": function(target, selection)
     256        {
     257            if (getActionInfo("ranged-attack", target).possible)
     258                    return { "type": "ranged-attack", "cursor": "action-ranged-attack", "target": target };
     259            return false;
     260        },
    139261        "specificness": 10,
    140262    },
    141263
    142     "heal": 
     264    "heal":
    143265    {
    144266        "execute": function(target, action, selection, queued)
    145267        {
    146             Engine.PostNetworkCommand({"type": "heal", "entities": selection, "target": action.target, "queued": queued});
     268            Engine.PostNetworkCommand({ "type": "heal", "entities": selection, "target": action.target, "queued": queued });
    147269            Engine.GuiInterfaceCall("PlaySound", { "name": "order_heal", "entity": selection[0] });
    148270            return true;
    149271        },
     
    176298                return {"type": "heal", "cursor": "action-heal", "target": target};
    177299            return false;
    178300        },
    179         "specificness": 7,
     301        "specificness": 6,
    180302    },
    181303
    182     "build": 
     304    "build":
    183305    {
    184306        "execute": function(target, action, selection, queued)
    185307        {
     
    202324        "specificness": 3,
    203325    },
    204326
    205     "repair": 
     327    "repair":
    206328    {
    207329        "execute": function(target, action, selection, queued)
    208330        {
     
    230352                return {"type": "build", "cursor": "action-repair", "target": target};
    231353            return false;
    232354        },
    233         "specificness": 11,
     355        "specificness": 12,
    234356    },
    235357
    236     "gather": 
     358    "gather":
    237359    {
    238360        "execute": function(target, action, selection, queued)
    239361        {
     
    260382        "specificness": 1,
    261383    },
    262384
    263     "returnresource": 
     385    "returnresource":
    264386    {
    265387        "execute": function(target, action, selection, queued)
    266388        {
     
    297419        "specificness": 2,
    298420    },
    299421
    300     "setup-trade-route": 
     422    "setup-trade-route":
    301423    {
    302424        "execute": function(target, action, selection, queued)
    303425        {
     
    362484        "specificness": 0,
    363485    },
    364486
    365     "garrison": 
     487    "garrison":
    366488    {
    367489        "execute": function(target, action, selection, queued)
    368490        {
     
    409531        "specificness": 20,
    410532    },
    411533
    412     "guard": 
     534    "guard":
    413535    {
    414536        "execute": function(target, action, selection, queued)
    415537        {
     
    446568        "specificness": 40,
    447569    },
    448570
    449     "remove-guard": 
     571    "remove-guard":
    450572    {
    451573        "execute": function(target, action, selection, queued)
    452574        {
     
    471593        "specificness": 41,
    472594    },
    473595
    474     "set-rallypoint": 
     596    "set-rallypoint":
    475597    {
    476598        "execute": function(target, action, selection, queued)
    477599        {
    478             // if there is a position set in the action then use this so that when setting a 
     600            // if there is a position set in the action then use this so that when setting a
    479601            // rally point on an entity it is centered on that entity
    480602            if (action.position)
    481603                target = action.position;
     
    507629                cursor = "action-attack-move";
    508630            }
    509631
     632            if (Engine.HotkeyIsPressed("session.meleeattackmove"))
     633            {
     634                if (Engine.HotkeyIsPressed("session.meleeattackmoveUnit"))
     635                    var targetClasses = { "melee-attack": ["Unit"] };
     636                else
     637                    var targetClasses = { "melee-attack": ["Unit", "Structure"] };
     638                data.command = "melee-attack-walk";
     639                data.targetClasses = targetClasses;
     640                cursor = "action-melee-attack";
     641            }
     642
     643            if (Engine.HotkeyIsPressed("session.rangedattackmove"))
     644            {
     645                if (Engine.HotkeyIsPressed("session.rangedattackmoveUnit"))
     646                    var targetClasses = { "ranged-attack": ["Unit"] };
     647                else
     648                    var targetClasses = { "ranged-attack": ["Unit", "Structure"] };
     649                data.command = "ranged-attack-walk";
     650                data.targetClasses = targetClasses;
     651                cursor = "action-ranged-attack";
     652            }
     653
    510654            if (targetState.garrisonHolder && playerCheck(entState, targetState, ["Player", "MutualAlly"]))
    511655            {
    512656                data.command = "garrison";
     
    610754                return false;
    611755            return {"type": "set-rallypoint", "cursor": actionInfo.cursor, "data": actionInfo.data, "tooltip": actionInfo.tooltip, "position": actionInfo.position};
    612756        },
    613         "specificness": 6,
     757        "specificness": 5,
    614758    },
    615759
    616     "unset-rallypoint": 
     760    "unset-rallypoint":
    617761    {
    618762        "execute": function(target, action, selection, queued)
    619763        {
     
    654798        "specificness": 11,
    655799    },
    656800
    657     "none": 
     801    "none":
    658802    {
    659803        "execute": function(target, action, selection, queued)
    660804        {
     
    668812 * Info and actions for the entity commands
    669813 * Currently displayed in the bottom of the central panel
    670814 */
    671 var g_EntityCommands = 
     815var g_EntityCommands =
    672816{
    673817    // Unload
    674818    "unload-all": {
  • binaries/data/mods/public/simulation/ai/common-api/entity.js

     
    743743        return false;
    744744    },
    745745
     746    isCapturable: function() { return this.get("Capturable") !== undefined; },
    746747    isGarrisonHolder: function() { return this.get("GarrisonHolder") !== undefined; },
    747748    garrisoned: function() { return this._entity.garrisoned; },
    748749    canGarrisonInside: function() { return this._entity.garrisoned.length < this.garrisonMax(); },
     
    753754    },
    754755
    755756    moveToRange: function(x, z, min, max, queued = false) {
    756         Engine.PostCommand(PlayerID,{"type": "walk-to-range", "entities": [this.id()], "x": x, "z": z, "min": min, "max": max, "queued": queued });
     757        Engine.PostCommand(PlayerID,{ "type": "walk-to-range", "entities": [this.id()], "x": x, "z": z, "min": min, "max": max, "queued": queued });
    757758        return this;
    758759    },
    759760
    760     attackMove: function(x, z, targetClasses, queued = false) {
    761         Engine.PostCommand(PlayerID,{"type": "attack-walk", "entities": [this.id()], "x": x, "z": z, "targetClasses": targetClasses, "queued": queued });
     761    attackMove: function(x, z, targetClasses, queued = false, prefAttackType = "primary") { // hack
     762        Engine.PostCommand(PlayerID,{"type": "attack-walk", "entities": [this.id()], "x": x, "z": z, "targetClasses": targetClasses,  "queued": queued, "prefAttackType": prefAttackType,});
    762763        return this;
    763764    },
    764765
     
    792793        return this;
    793794    },
    794795
    795     attack: function(unitId, allowCapture = true, queued = false) {
    796         Engine.PostCommand(PlayerID,{"type": "attack", "entities": [this.id()], "target": unitId, "allowCapture": allowCapture, "queued": queued});
     796    attack: function(unitId, queued = false, prefAttackType) {
     797        Engine.PostCommand(PlayerID,{"type": "attack", "entities": [this.id()], "target": unitId, "queued": queued, "prefAttackType": prefAttackType});
    797798        return this;
    798799    },
    799800
  • binaries/data/mods/public/simulation/ai/common-api/entitycollection.js

     
    153153m.EntityCollection.prototype.attackMove = function(x, z, targetClasses, queued)
    154154{
    155155    queued = queued || false;
    156     Engine.PostCommand(PlayerID,{"type": "attack-walk", "entities": this.toIdArray(), "x": x, "z": z, "targetClasses": targetClasses, "queued": queued});
     156    Engine.PostCommand(PlayerID, { "type": "attack-walk", "entities": this.toIdArray(), "x": x, "z": z, "targetClasses": targetClasses, "queued": queued, "prefAttackType": "primary" }); // hack
    157157    return this;
    158158};
    159159
     
    180180m.EntityCollection.prototype.attack = function(unit)
    181181{
    182182    var unitId = unit;
    183     Engine.PostCommand(PlayerID,{"type": "attack", "entities": this.toIdArray(), "target": unitId, "queued": false});
     183    Engine.PostCommand(PlayerID, { "type": "attack", "entities": this.toIdArray(), "target": unitId, "queued": false, "prefAttackType": "Capture" });
    184184    return this;
    185185};
    186186
  • binaries/data/mods/public/simulation/ai/petra/attackPlan.js

     
    10781078                        continue;
    10791079                    if (!ent.isIdle())
    10801080                        continue;
    1081                     ent.attack(attacker.id(), !this.noCapture.has(attacker.id()));
     1081                    ent.attack(attacker.id(), m.getPrefAttackType(ent, attacker, this.noCapture));
    10821082                }
    10831083                break;
    10841084            }
     
    13141314                {
    13151315                    if (this.isSiegeUnit(gameState, ent))   // needed as mauryan elephants are not filtered out
    13161316                        continue;
    1317                     ent.attack(attacker.id(), !this.noCapture.has(attacker.id()));
     1317                    ent.attack(attacker.id(), m.getPrefAttackType(ent, attacker, this.noCapture));
    13181318                    ent.setMetadata(PlayerID, "lastAttackPlanUpdateTime", time);
    13191319                }
    13201320                // And if this attacker is a non-ranged siege unit and our unit also, attack it
    13211321                if (this.isSiegeUnit(gameState, attacker) && attacker.hasClass("Melee") && ourUnit.hasClass("Melee"))
    13221322                {
    1323                     ourUnit.attack(attacker.id(), false);
     1323                    ourUnit.attack(attacker.id(), "Melee");
    13241324                    ourUnit.setMetadata(PlayerID, "lastAttackPlanUpdateTime", time);
    13251325                }
    13261326            }
     
    13311331                    let collec = this.unitCollection.filter(API3.Filters.byClass("Melee")).filterNearest(ourUnit.position(), 5);
    13321332                    for (let ent of collec.values())
    13331333                    {
    1334                         ent.attack(attacker.id(), false);
     1334                        ent.attack(attacker.id(), m.getPrefAttackType(ent, attacker, this.noCapture));
    13351335                        ent.setMetadata(PlayerID, "lastAttackPlanUpdateTime", time);
    13361336                    }
    13371337                }
     
    13501350                                continue;
    13511351                        }
    13521352                    }
    1353                     ourUnit.attack(attacker.id(), !this.noCapture.has(attacker.id()));
     1353                    ourUnit.attack(attacker.id(), m.getPrefAttackType(ourUnit, attacker, this.noCapture));
    13541354                    ourUnit.setMetadata(PlayerID, "lastAttackPlanUpdateTime", time);
    13551355                }
    13561356            }
     
    15211521                        return (valb - vala);
    15221522                    });
    15231523                    if (mStruct[0].hasClass("Gates"))
    1524                         ent.attack(mStruct[0].id(), false);
     1524                        ent.attack(mStruct[0].id(), m.getPrefAttackType(ent, mStruct[0], this.noCapture));
    15251525                    else
    15261526                    {
    15271527                        let rand = Math.floor(Math.random() * mStruct.length * 0.2);
    1528                         let newTargetId = mStruct[rand].id();
    1529                         ent.attack(newTargetId, !this.noCapture.has(newTargetId));
     1528                        let newTarget = mStruct[rand];
     1529                        ent.attack(newTarget.id(), m.getPrefAttackType(ent, newTarget, this.noCapture));
    15301530                    }
    15311531                }
    15321532                else
     
    15841584                        return valb - vala;
    15851585                    });
    15861586                    let rand = Math.floor(Math.random() * mUnit.length * 0.1);
    1587                     let newTargetId = mUnit[rand].id();
    1588                     ent.attack(newTargetId, !this.noCapture.has(newTargetId));
     1587                    let newTarget = mUnit[rand];
     1588                    ent.attack(newTarget.id(), m.getPrefAttackType(ent, newTarget, this.noCapture));
    15891589                }
    15901590                else if (API3.SquareVectorDistance(this.targetPos, ent.position()) > 2500 )
    15911591                {
     
    16281628                            return (valb - vala);
    16291629                        });
    16301630                        if (mStruct[0].hasClass("Gates"))
    1631                             ent.attack(mStruct[0].id(), false);
     1631                            ent.attack(mStruct[0].id(), m.getPrefAttackType(ent, mStruct[0], this.noCapture));
    16321632                        else
    16331633                        {
    16341634                            let rand = Math.floor(Math.random() * mStruct.length * 0.2);
    1635                             let newTargetId = mStruct[rand].id();
    1636                             ent.attack(newTargetId, !this.noCapture.has(newTargetId));
     1635                            let newTarget = mStruct[rand];
     1636                            ent.attack(newTarget.id(), m.getPrefAttackType(ent, newTarget, this.noCapture));
    16371637                        }
    16381638                    }
    16391639                    else if (needsUpdate)  // really nothing   let's try to help our nearest unit
     
    16541654
    16551655                        });
    16561656                        if (attackerId)
    1657                             ent.attack(attackerId, !this.noCapture.has(attackerId));
     1657                            ent.attack(attackerId, m.getPrefAttackType(ent, attacker, this.noCapture));
    16581658                    }
    16591659                }
    16601660            }
     
    18281828
    18291829    if (this.noCapture.has(targetId))
    18301830    {
    1831         ent.attack(targetId, false);
     1831        ent.attack(targetId, m.getPrefAttackType(ent, target, this.noCapture));
    18321832        return true;
    18331833    }
    18341834
     
    18401840    if (target.hasClass("Siege") && target.hasClass("Melee"))
    18411841    {
    18421842        this.noCapture.add(targetId);
    1843         ent.attack(targetId, false);
     1843        ent.attack(targetId, m.getPrefAttackType(ent, target, this.noCapture));
    18441844        return true;
    18451845    }
    18461846
     
    18571857    if (antiCapture >= this.captureStrength)
    18581858    {
    18591859        this.noCapture.add(targetId);
    1860         ent.attack(targetId, false);
     1860        ent.attack(targetId, "primary"); // hack
    18611861        return true;
    18621862    }
    18631863
     
    18661866        this.unitCollection.length < 2*target.garrisoned().length)
    18671867    {
    18681868        this.noCapture.add(targetId);
    1869         ent.attack(targetId, false);
     1869        ent.attack(targetId, "primary"); // hack
    18701870        return true;
    18711871    }
    18721872
  • 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);
     
    116116        else if (orderData.length && orderData[0].target && orderData[0].attackType && orderData[0].attackType === "Capture")
    117117        {
    118118            let target = gameState.getEntityById(orderData[0].target);
    119             if (target && !m.allowCapture(ent, target))
    120                 ent.attack(orderData[0].target, false);
     119            if (target)
     120            {
     121                let prefAttackType = m.getPrefAttackType(ent, target);
     122                if (prefAttackType !== "Capture")
     123                    ent.attack(orderData[0].target, prefAttackType);
     124            }
    121125        }
    122126    }
    123127
  • 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    if (target.isCapturable() && (!target.isGarrisonHolder() || !target.garrisoned().length))
     91        return "Capture";
     92    return "primary";
    8993};
    9094
    9195// Makes the worker deposit the currently carried resources at the closest accessible dropsite
  • binaries/data/mods/public/simulation/components/Attack.js

     
    11function Attack() {}
    22
    3 Attack.prototype.bonusesSchema = 
     3Attack.prototype.bonusesSchema =
    44    "<optional>" +
    55        "<element name='Bonuses'>" +
    66            "<zeroOrMore>" +
     
    4141Attack.prototype.Schema =
    4242    "<a:help>Controls the attack abilities and strengths of the unit.</a:help>" +
    4343    "<a:example>" +
     44        "<ChangeDistance>20</ChangeDistance>" +
    4445        "<Melee>" +
     46            "<AttackOrder>primary</AttackOrder>" +
    4547            "<Hack>10.0</Hack>" +
    4648            "<Pierce>0.0</Pierce>" +
    4749            "<Crush>5.0</Crush>" +
     
    6264            "<PreferredClasses datatype=\"tokens\">Cavalry Infantry</PreferredClasses>" +
    6365        "</Melee>" +
    6466        "<Ranged>" +
     67            "<AttackOrder>secondary</AttackOrder>" +
    6568            "<Hack>0.0</Hack>" +
    6669            "<Pierce>10.0</Pierce>" +
    6770            "<Crush>0.0</Crush>" +
     
    103106        "</Slaughter>" +
    104107    "</a:example>" +
    105108    "<optional>" +
     109        "<element name='ChangeDistance' a:help='Distance to change between Melee and Ranged attack'>" +
     110            "<ref name='nonNegativeDecimal'/>" +
     111        "</element>" +
     112    "</optional>" +
     113    "<optional>" +
    106114        "<element name='Melee'>" +
     115            "<optional>" +
     116                "<element name='AttackOrder'>" +
     117                    "<choice>" +
     118                        "<value>primary</value>" +
     119                        "<value>secondary</value>" +
     120                    "</choice>" +
     121                "</element>" +
     122            "</optional>" +
    107123            "<interleave>" +
    108124                "<element name='Hack' a:help='Hack damage strength'><ref name='nonNegativeDecimal'/></element>" +
    109125                "<element name='Pierce' a:help='Pierce damage strength'><ref name='nonNegativeDecimal'/></element>" +
     
    120136    "</optional>" +
    121137    "<optional>" +
    122138        "<element name='Ranged'>" +
     139            "<optional>" +
     140                "<element name='AttackOrder'>" +
     141                    "<choice>" +
     142                        "<value>primary</value>" +
     143                        "<value>secondary</value>" +
     144                    "</choice>" +
     145                "</element>" +
     146            "</optional>" +
    123147            "<interleave>" +
    124148                "<element name='Hack' a:help='Hack damage strength'><ref name='nonNegativeDecimal'/></element>" +
    125149                "<element name='Pierce' a:help='Pierce damage strength'><ref name='nonNegativeDecimal'/></element>" +
     
    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;
     262};
     263
     264Attack.prototype.CanAttack = function(target, wantedType)
     265{
    241266    let cmpFormation = Engine.QueryInterface(target, IID_Formation);
    242267    if (cmpFormation)
    243268        return true;
     
    247272    if (!cmpThisPosition || !cmpTargetPosition || !cmpThisPosition.IsInWorld() || !cmpTargetPosition.IsInWorld())
    248273        return false;
    249274
     275    const cmpIdentity = Engine.QueryInterface(target, IID_Identity);
     276    if (!cmpIdentity)
     277        return undefined;
     278
     279    let cmpEntityPlayer = QueryOwnerInterface(this.entity);
     280    let cmpTargetPlayer = QueryOwnerInterface(target);
     281    if (!cmpTargetPlayer || !cmpEntityPlayer)
     282        return false;
     283
     284    const targetClasses = cmpIdentity.GetClassesList();
     285    let cmpCapturable = QueryMiragedInterface(target, IID_Capturable);
     286    if (targetClasses.indexOf("Domestic") == -1 && !cmpEntityPlayer.IsEnemy(cmpTargetPlayer.GetPlayerID()) &&
     287       (!cmpCapturable || !cmpCapturable.CanCapture(cmpEntityPlayer.GetPlayerID())))
     288        return false;
     289
    250290    // Check if the relative height difference is larger than the attack range
    251291    // If the relative height is bigger, it means they will never be able to
    252292    // reach each other, no matter how close they come.
    253293    let heightDiff = Math.abs(cmpThisPosition.GetHeightOffset() - cmpTargetPosition.GetHeightOffset());
    254294
    255     const cmpIdentity = Engine.QueryInterface(target, IID_Identity);
    256     if (!cmpIdentity)
    257         return undefined;
    258 
    259     const targetClasses = cmpIdentity.GetClassesList();
    260 
    261295    for (let type of this.GetAttackTypes())
    262296    {
    263         if (type == "Capture" && !QueryMiragedInterface(target, IID_Capturable))
     297        if (wantedType && type != wantedType)
    264298            continue;
    265299
     300        if (type == "Capture" && !cmpCapturable)
     301            continue;
     302
    266303        if (type == "Slaughter" && targetClasses.indexOf("Domestic") == -1)
    267304            continue;
    268305
     
    288325    return false;
    289326};
    290327
     328Attack.prototype.CanAttackWithType = function(target, type)
     329{
     330    if (this.template[type])
     331        return this.CanAttack(target, type);
     332    return false;
     333};
     334
    291335/**
    292336 * Returns null if we have no preference or the lowest index of a preferred class.
    293337 */
     
    294338Attack.prototype.GetPreference = function(target)
    295339{
    296340    const cmpIdentity = Engine.QueryInterface(target, IID_Identity);
    297     if (!cmpIdentity) 
     341    if (!cmpIdentity)
    298342        return undefined;
    299343
    300344    const targetClasses = cmpIdentity.GetClassesList();
     
    327371        if (type == "Slaughter")
    328372            continue;
    329373        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;
     374        ret.min = Math.min(ret.min, range.min);
     375        ret.max = Math.max(ret.max, range.max);
    334376    }
    335377    return ret;
    336378};
    337379
    338 Attack.prototype.GetBestAttackAgainst = function(target, allowCapture)
     380Attack.prototype.GetBestAttackAgainst = function(target, prefAttackType)
    339381{
    340382    let cmpFormation = Engine.QueryInterface(target, IID_Formation);
    341383    if (cmpFormation)
     
    350392    }
    351393
    352394    let cmpIdentity = Engine.QueryInterface(target, IID_Identity);
    353     if (!cmpIdentity) 
     395    if (!cmpIdentity)
    354396        return undefined;
    355397
    356398    let targetClasses = cmpIdentity.GetClassesList();
     
    357399    let isTargetClass = function (className) { return targetClasses.indexOf(className) != -1; };
    358400
    359401    // Always slaughter domestic animals instead of using a normal attack
    360     if (isTargetClass("Domestic") && this.template.Slaughter) 
     402    if (isTargetClass("Domestic") && this.template.Slaughter)
    361403        return "Slaughter";
    362404
    363405    let attack = this;
     
    365407
    366408    let types = this.GetAttackTypes().filter(isAllowed);
    367409
    368     // check if the target is capturable
    369     let captureIndex = types.indexOf("Capture");
    370     if (captureIndex != -1)
     410    if (prefAttackType && prefAttackType != "Capture" && types.indexOf(prefAttackType) != -1)
     411        return prefAttackType;
     412    else if (!prefAttackType || prefAttackType == "Capture")
    371413    {
    372         let cmpCapturable = QueryMiragedInterface(target, IID_Capturable);
     414        // check if the target is capturable
     415        let captureIndex = types.indexOf("Capture");
     416        if (captureIndex != -1)
     417        {
     418            let cmpCapturable = QueryMiragedInterface(target, IID_Capturable);
     419            let cmpPlayer = QueryOwnerInterface(this.entity);
     420            if (cmpPlayer && cmpCapturable && cmpCapturable.CanCapture(cmpPlayer.GetPlayerID()))
     421                return "Capture";
     422            // not captureable, so remove this attack
     423            types.splice(captureIndex, 1);
     424        }
     425    }
    373426
    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);
     427    // ignore charges for now: TODO implement these
     428    let chargeIndex = types.indexOf("Charge");
     429    if (chargeIndex != -1)
     430        types.splice(chargeIndex, 1);
     431
     432    // only ranged and/or melee attack left
     433    // if one attacktype left choose this one
     434    if (types.indexOf("Melee") == -1 || types.indexOf("Ranged") == -1)
     435        return types[0];
     436
     437    if (this.HasTargetPreferredClass(types, targetClasses))
     438    {
     439        let isPreferred = function (className) { return attack.GetPreferredClasses(className).some(isTargetClass); };
     440        let byPreference = function (a, b) { return (types.indexOf(a) + (isPreferred(a) ? types.length : 0) ) - (types.indexOf(b) + (isPreferred(b) ? types.length : 0) ); };
     441
     442        return types.sort(byPreference).pop();
    379443    }
    380444
    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) ); };
     445    // assume ranged and melee attack
     446    // TODO stop assuming that?
     447    let cmpPosition = Engine.QueryInterface(this.entity, IID_Position);
     448    if (!cmpPosition || !cmpPosition.IsInWorld())
     449        return undefined;
     450    let selfPosition = cmpPosition.GetPosition2D();
     451    let cmpTargetPosition = Engine.QueryInterface(target, IID_Position);
     452    if (!cmpTargetPosition || !cmpTargetPosition.IsInWorld())
     453        return undefined;
     454    let targetPosition = cmpTargetPosition.GetPosition2D();
     455    if (targetPosition.distanceToSquared(selfPosition) <= Math.pow(this.GetChangeDistance(), 2))
     456        return "Melee";
     457    return "Ranged";
    383458
    384     return types.sort(byPreference).pop();
    385459};
    386460
    387461Attack.prototype.CompareEntitiesByPreference = function(a, b)
     
    478552    return attackBonus;
    479553};
    480554
     555Attack.prototype.GetAttackTypeFromOrder = function(prefAttackType)
     556{
     557    let types = this.GetAttackTypes();
     558    for (let type of types)
     559        if ((this.template[type].AttackOrder && this.template[type].AttackOrder == prefAttackType) || prefAttackType == type)
     560            return type;
     561
     562    if (types.indexOf("Melee") != -1 && types.indexOf("Ranged") != -1)
     563        return undefined; // we have a problem, should never happen
     564    return types.indexOf("Melee") != -1 ? "Melee": "Ranged";
     565};
     566
     567Attack.prototype.HasTargetPreferredClass = function(types, targetClasses)
     568{
     569    let isTargetClass = function (className) { return targetClasses.indexOf(className) != -1; };
     570    for (let type of types)
     571        if (this.template[type].PreferredClasses && this.GetPreferredClasses(type).some(isTargetClass))
     572            return true;
     573    return false
     574};
     575
    481576// Returns a 2d random distribution scaled for a spread of scale 1.
    482577// The current implementation is a 2d gaussian with sigma = 1
    483578Attack.prototype.GetNormalDistribution = function(){
     
    486581    let a = Math.random();
    487582    let b = Math.random();
    488583
    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);
     584    let c = Math.sqrt(-2 * Math.log(a)) * Math.cos(2 * Math.PI * b);
     585    let d = Math.sqrt(-2 * Math.log(a)) * Math.sin(2 * Math.PI * b);
    491586
    492587    return [c, d];
    493588};
     
    503598    if (type == "Ranged")
    504599    {
    505600        let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
    506         let turnLength = cmpTimer.GetLatestTurnLength()/1000;
     601        let turnLength = cmpTimer.GetLatestTurnLength() / 1000;
    507602        // In the future this could be extended:
    508603        //  * Obstacles like trees could reduce the probability of the target being hit
    509604        //  * Obstacles like walls should block projectiles entirely
     
    535630
    536631        let horizDistance = targetPosition.horizDistanceTo(selfPosition);
    537632
    538         // This is an approximation of the time ot the target, it assumes that the target has a constant radial
    539         // velocity, but since units move in straight lines this is not true.  The exact value would be more
    540         // difficult to calculate and I think this is sufficiently accurate.  (I tested and for cavalry it was
     633        // This is an approximation of the time to the target, it assumes that the target has a constant radial
     634        // velocity, but since units move in straight lines this is not true. The exact value would be more
     635        // difficult to calculate and I think this is sufficiently accurate. (I tested and for cavalry it was
    541636        // about 5% of the units radius out in the worst case)
    542637        let timeToTarget = horizDistance / (horizSpeed - radialSpeed);
    543638
     
    620715Attack.prototype.InterpolatedLocation = function(ent, lateness)
    621716{
    622717    let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
    623     let turnLength = cmpTimer.GetLatestTurnLength()/1000;
     718    let turnLength = cmpTimer.GetLatestTurnLength() / 1000;
    624719    let cmpTargetPosition = Engine.QueryInterface(ent, IID_Position);
    625720    if (!cmpTargetPosition || !cmpTargetPosition.IsInWorld()) // TODO: handle dead target properly
    626721        return undefined;
     
    658753        let d = Vector3D.sub(point, targetPosition);
    659754        d = Vector2D.from3D(d).rotate(-angle);
    660755
    661         return d.x < Math.abs(targetShape.width/2) && d.y < Math.abs(targetShape.depth/2);
     756        return d.x < Math.abs(targetShape.width / 2) && d.y < Math.abs(targetShape.depth / 2);
    662757    }
    663758};
    664759
     
    740835        return;
    741836
    742837    for (let type of this.GetAttackTypes())
    743         if (msg.valueNames.indexOf("Attack/"+type+"/MaxRange") !== -1)
     838        if (msg.valueNames.indexOf("Attack/" + type + "/MaxRange") !== -1)
    744839        {
    745840            cmpUnitAI.UpdateRangeQueries();
    746841            return;
  • binaries/data/mods/public/simulation/components/GuiInterface.js

     
    17481748    if (!cmpAttack)
    17491749        return false;
    17501750
    1751     let cmpEntityPlayer = QueryOwnerInterface(data.entity, IID_Player);
    1752     let cmpTargetPlayer = QueryOwnerInterface(data.target, IID_Player);
    1753     if (!cmpEntityPlayer || !cmpTargetPlayer)
     1751    return cmpAttack.CanAttack(data.target);
     1752};
     1753
     1754GuiInterface.prototype.CanAttackWithType = function(player, data)
     1755{
     1756    let cmpAttack = Engine.QueryInterface(data.entity, IID_Attack);
     1757    if (!cmpAttack)
    17541758        return false;
    17551759
    1756     // if the owner is an enemy, it's up to the attack component to decide
    1757     if (cmpEntityPlayer.IsEnemy(cmpTargetPlayer.GetPlayerID()))
    1758         return cmpAttack.CanAttack(data.target);
     1760    return cmpAttack.CanAttackWithType(data.target, data.type);
     1761};
    17591762
    1760     return false;
     1763GuiInterface.prototype.GetAttackTypeFromOrder = function(player, data)
     1764{
     1765    let cmpAttack = Engine.QueryInterface(data.entity, IID_Attack);
     1766    if (!cmpAttack)
     1767        return undefined;
     1768
     1769    return cmpAttack.GetAttackTypeFromOrder(data.attackOrder);
    17611770};
    17621771
    17631772/*
     
    19031912    "GetTradingDetails": 1,
    19041913    "CanCapture": 1,
    19051914    "CanAttack": 1,
     1915    "CanAttackWithType": 1,
     1916    "GetAttackTypeFromOrder": 1,
    19061917    "GetBatchTime": 1,
    19071918
    19081919    "IsMapRevealed": 1,
  • binaries/data/mods/public/simulation/components/UnitAI.js

     
    416416        }
    417417
    418418        // Work out how to attack the given target
    419         var type = this.GetBestAttackAgainst(this.order.data.target, this.order.data.allowCapture);
     419        let type = this.GetBestAttackAgainst(this.order.data.target, this.order.data.prefAttackType);
    420420        if (!type)
    421421        {
    422422            // Oops, we can't attack at all
     
    559559        if (this.MustKillGatherTarget(this.order.data.target))
    560560        {
    561561            // Make sure we can attack the target, else we'll get very stuck
    562             if (!this.GetBestAttackAgainst(this.order.data.target, false))
     562            if (!this.GetBestAttackAgainst(this.order.data.target))
    563563            {
    564564                // Oops, we can't attack at all - give up
    565565                // TODO: should do something so the player knows why this failed
     
    583583                return;
    584584            }
    585585
    586             this.PushOrderFront("Attack", { "target": this.order.data.target, "force": false, "hunting": true, "allowCapture": false });
     586            this.PushOrderFront("Attack", { "target": this.order.data.target, "force": false, "hunting": true, "prefAttackType": undefined });
    587587            return;
    588588        }
    589589
     
    838838        },
    839839
    840840        "Order.Attack": function(msg) {
    841             var target = msg.data.target;
    842             var allowCapture = msg.data.allowCapture;
    843             var cmpTargetUnitAI = Engine.QueryInterface(target, IID_UnitAI);
     841            let target = msg.data.target;
     842            let prefAttackType = msg.data.prefAttackType;
     843            let cmpTargetUnitAI = Engine.QueryInterface(target, IID_UnitAI);
    844844            if (cmpTargetUnitAI && cmpTargetUnitAI.IsFormationMember())
    845845                target = cmpTargetUnitAI.GetFormationController();
    846846
     
    859859                this.FinishOrder();
    860860                return;
    861861            }
    862             this.CallMemberFunction("Attack", [target, false, allowCapture]);
     862            this.CallMemberFunction("Attack", [target, false, prefAttackType]);
    863863            if (cmpAttack.CanAttackAsFormation())
    864864                this.SetNextState("COMBAT.ATTACKING");
    865865            else
     
    914914                    return;
    915915                }
    916916
    917                 this.PushOrderFront("Attack", { "target": msg.data.target, "hunting": true, "allowCapture": false });
     917                this.PushOrderFront("Attack", { "target": msg.data.target, "hunting": true, "prefAttackType": undefined });
    918918                return;
    919919            }
    920920
     
    11511151
    11521152                "MoveCompleted": function(msg) {
    11531153                    var cmpAttack = Engine.QueryInterface(this.entity, IID_Attack);
    1154                     this.CallMemberFunction("Attack", [this.order.data.target, false, this.order.data.allowCapture]);
     1154                    this.CallMemberFunction("Attack", [this.order.data.target, false, this.order.data.prefAttackType]);
    11551155                    if (cmpAttack.CanAttackAsFormation())
    11561156                        this.SetNextState("COMBAT.ATTACKING");
    11571157                    else
     
    11621162            "ATTACKING": {
    11631163                // Wait for individual members to finish
    11641164                "enter": function(msg) {
    1165                     var target = this.order.data.target;
    1166                     var allowCapture = this.order.data.allowCapture;
     1165                    let target = this.order.data.target;
     1166                    let prefAttackType = this.order.data.prefAttackType;
    11671167                    // Check if we are already in range, otherwise walk there
    11681168                    if (!this.CheckTargetAttackRange(target, target))
    11691169                    {
     
    11701170                        if (this.TargetIsAlive(target) && this.CheckTargetVisible(target))
    11711171                        {
    11721172                            this.FinishOrder();
    1173                             this.PushOrderFront("Attack", { "target": target, "force": false, "allowCapture": allowCapture });
     1173                            this.PushOrderFront("Attack", { "target": target, "force": false, "prefAttackType": prefAttackType });
    11741174                            return true;
    11751175                        }
    11761176                        this.FinishOrder();
     
    11861186                },
    11871187
    11881188                "Timer": function(msg) {
    1189                     var target = this.order.data.target;
    1190                     var allowCapture = this.order.data.allowCapture;
     1189                    let target = this.order.data.target;
     1190                    let prefAttackType = this.order.data.prefAttackType;
    11911191                    // Check if we are already in range, otherwise walk there
    11921192                    if (!this.CheckTargetAttackRange(target, target))
    11931193                    {
     
    11941194                        if (this.TargetIsAlive(target) && this.CheckTargetVisible(target))
    11951195                        {
    11961196                            this.FinishOrder();
    1197                             this.PushOrderFront("Attack", { "target": target, "force": false, "allowCapture": allowCapture });
     1197                            this.PushOrderFront("Attack", { "target": target, "force": false, "prefAttackType": prefAttackType });
    11981198                            return;
    11991199                        }
    12001200                        this.FinishOrder();
     
    14061406
    14071407            // target the unit
    14081408            if (this.CheckTargetVisible(msg.data.attacker))
    1409                 this.PushOrderFront("Attack", { "target": msg.data.attacker, "force": false, "allowCapture": true });
     1409                this.PushOrderFront("Attack", { "target": msg.data.attacker, "force": false, "prefAttackType": undefined });
    14101410            else
    14111411            {
    14121412                var cmpPosition = Engine.QueryInterface(msg.data.attacker, IID_Position);
     
    18011801                        // Can't reach it - try to chase after it
    18021802                        if (this.ShouldChaseTargetedEntity(target, this.order.data.force))
    18031803                        {
     1804                            if (!this.order.data.force)
     1805                                this.order.data.attackType = this.GetBestAttackAgainst(target);
    18041806                            if (this.MoveToTargetAttackRange(target, this.order.data.attackType))
    18051807                            {
    18061808                                this.SetNextState("COMBAT.CHASING");
     
    19121914                        // Can't reach it - try to chase after it
    19131915                        if (this.ShouldChaseTargetedEntity(target, this.order.data.force))
    19141916                        {
     1917                            if (!this.order.data.force)
     1918                                this.order.data.attackType = this.GetBestAttackAgainst(target);
    19151919                            if (this.MoveToTargetRange(target, IID_Attack, this.order.data.attackType))
    19161920                            {
    19171921                                this.SetNextState("COMBAT.CHASING");
     
    45354539    return distance < range;
    45364540};
    45374541
    4538 UnitAI.prototype.GetBestAttackAgainst = function(target, allowCapture)
     4542UnitAI.prototype.GetBestAttackAgainst = function(target, prefAttackType)
    45394543{
    45404544    var cmpAttack = Engine.QueryInterface(this.entity, IID_Attack);
    45414545    if (!cmpAttack)
    45424546        return undefined;
    4543     return cmpAttack.GetBestAttackAgainst(target, allowCapture);
     4547    return cmpAttack.GetBestAttackAgainst(target, prefAttackType);
    45444548};
    45454549
    45464550UnitAI.prototype.GetAttackBonus = function(type, target)
     
    45624566    if (!target)
    45634567        return false;
    45644568
    4565     this.PushOrderFront("Attack", { "target": target, "force": false, "forceResponse": forceResponse, "allowCapture": true });
     4569    this.PushOrderFront("Attack", { "target": target, "force": false, "forceResponse": forceResponse, "prefAttackType": undefined });
    45664570    return true;
    45674571};
    45684572
     
    45754579{
    45764580    var target = ents.find(target =>
    45774581        this.CanAttack(target, forceResponse)
    4578         && this.CheckTargetDistanceFromHeldPosition(target, IID_Attack, this.GetBestAttackAgainst(target, true))
     4582        && this.CheckTargetDistanceFromHeldPosition(target, IID_Attack, this.GetBestAttackAgainst(target))
    45794583        && (this.GetStance().respondChaseBeyondVision || this.CheckTargetIsInVisionRange(target))
    45804584    );
    45814585    if (!target)
    45824586        return false;
    45834587
    4584     this.PushOrderFront("Attack", { "target": target, "force": false, "forceResponse": forceResponse, "allowCapture": true });
     4588    this.PushOrderFront("Attack", { "target": target, "force": false, "forceResponse": forceResponse, "prefAttackType": undefined });
    45854589    return true;
    45864590};
    45874591
     
    49784982 * to a player order, and so is forced.
    49794983 * If targetClasses is given, only entities matching the targetClasses can be attacked.
    49804984 */
    4981 UnitAI.prototype.WalkAndFight = function(x, z, targetClasses, queued)
     4985UnitAI.prototype.WalkAndFight = function(x, z, targetClasses, queued, prefAttackType)
    49824986{
    4983     this.AddOrder("WalkAndFight", { "x": x, "z": z, "targetClasses": targetClasses, "force": true }, queued);
     4987    let cmpAttack = Engine.QueryInterface(this.entity, IID_Attack);
     4988    if (cmpAttack)
     4989        this.AddOrder("WalkAndFight", { "x": x, "z": z, "targetClasses": targetClasses, "force": true, "prefAttackType": cmpAttack.GetAttackTypeFromOrder(prefAttackType) }, queued);
    49844990};
    49854991
    49864992/**
     
    50015007/**
    50025008 * Adds attack order to the queue, forced by the player.
    50035009 */
    5004 UnitAI.prototype.Attack = function(target, queued, allowCapture)
     5010UnitAI.prototype.Attack = function(target, queued, prefAttackType)
    50055011{
    50065012    if (!this.CanAttack(target))
    50075013    {
     
    50135019            this.WalkToTarget(target, queued);
    50145020        return;
    50155021    }
    5016     this.AddOrder("Attack", { "target": target, "force": true, "allowCapture": allowCapture}, queued);
     5022
     5023    let cmpAttack = Engine.QueryInterface(this.entity, IID_Attack);
     5024    if (cmpAttack) // not needed as canAttack is true
     5025        this.AddOrder("Attack", { "target": target, "force": true, "prefAttackType": cmpAttack.GetAttackTypeFromOrder(prefAttackType) }, queued);
    50175026};
    50185027
    50195028/**
     
    54305439                    if (targetClasses.vetoEntities && targetClasses.vetoEntities[targ])
    54315440                        continue;
    54325441                }
    5433                 this.PushOrderFront("Attack", { "target": targ, "force": true, "allowCapture": true });
     5442                this.PushOrderFront("Attack", { "target": targ, "force": true, "prefAttackType": undefined });
    54345443                return true;
    54355444            }
    54365445        }
     
    54565465            if (targetClasses.vetoEntities && targetClasses.vetoEntities[targ])
    54575466                continue;
    54585467        }
    5459         this.PushOrderFront("Attack", { "target": targ, "force": true, "allowCapture": true });
     5468        this.PushOrderFront("Attack", { "target": targ, "force": true, "prefAttackType": undefined });
    54605469        return true;
    54615470    }
    54625471    return false;
  • 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; },
    104104        CompareEntitiesByPreference: function(a, b) { return 0; },
     105        GetAttackTypeFromOrder: function(t) { return "Melee"; },
    105106    });
    106107
    107108    unitAI.OnCreate();
     
    247248        AddMock(unit + i, IID_Attack, {
    248249            GetRange: function() { return {"max":10, "min": 0}; },
    249250            GetFullAttackRange: function() { return { "max": 40, "min": 0}; },
    250             GetBestAttackAgainst: function(t) { return "melee"; },
     251            GetBestAttackAgainst: function(t) { return "Melee"; },
    251252            GetTimers: function() { return { "prepare": 500, "repeat": 1000 }; },
    252253            CanAttack: function(v) { return true; },
    253254            CompareEntitiesByPreference: function(a, b) { return 0; },
     255            GetAttackTypeFromOrder: function(t) { return "Melee"; },
    254256        });
    255257       
    256258        unitAI.OnCreate();
     
    287289    AddMock(controller, IID_Attack, {
    288290        GetRange: function() { return {"max":10, "min": 0}; },
    289291        CanAttackAsFormation: function() { return false },
     292        GetAttackTypeFromOrder: function(t) { return "Melee"; },
    290293    });
    291294
    292295    controllerAI.OnCreate();
  • 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/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>