Ticket #252: t252_secondattack_15.diff

File t252_secondattack_15.diff, 62.1 KB (added by bb, 8 years ago)

dunno if that new function in template.js is in the right place

  • 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/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[Type])
     81            return false;
     82    return true;
     83}
     84
     85/**
    6986 * Get information about a template with or without technology modifications.
    7087 * @param template A valid template as returned by the template loader.
    7188 * @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

     
    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)
     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        {
     
    376498                return false;
    377499            if (!playerCheck(entState, targetState, ["Player", "MutualAlly"]))
    378500                return false;
    379             var tooltip = sprintf(translate("Current garrison: %(garrisoned)s/%(capacity)s"), {
     501            let tooltip = sprintf(translate("Current garrison: %(garrisoned)s/%(capacity)s"), {
    380502                garrisoned: targetState.garrisonHolder.garrisonedEntitiesCount,
    381503                capacity: targetState.garrisonHolder.capacity
    382504            });
    383             var extraCount = 0;
     505            let extraCount = 0;
    384506            if (entState.garrisonHolder)
    385507                extraCount += entState.garrisonHolder.garrisonedEntitiesCount;
    386508            if (targetState.garrisonHolder.garrisonedEntitiesCount + extraCount >= targetState.garrisonHolder.capacity)
    387509                tooltip = "[color=\"orange\"]" + tooltip + "[/color]";
    388             if (MatchesClassList(entState.identity.classes, targetState.garrisonHolder.allowedClasses))
    389                 return {"possible": true, "tooltip": tooltip};
     510            if (MatchesClassList(entState.identity.classes, targetState.garrisonHolder.allowedClasses) &&
     511                HasNeededAttackTypes(entState.attack, targetState.garrisonHolder.neededAttackTypes))
     512                return { "possible": true, "tooltip": tooltip };
    390513            return false;
    391514
    392515        },
     
    409532        "specificness": 20,
    410533    },
    411534
    412     "guard": 
     535    "guard":
    413536    {
    414537        "execute": function(target, action, selection, queued)
    415538        {
     
    446569        "specificness": 40,
    447570    },
    448571
    449     "remove-guard": 
     572    "remove-guard":
    450573    {
    451574        "execute": function(target, action, selection, queued)
    452575        {
     
    471594        "specificness": 41,
    472595    },
    473596
    474     "set-rallypoint": 
     597    "set-rallypoint":
    475598    {
    476599        "execute": function(target, action, selection, queued)
    477600        {
    478             // if there is a position set in the action then use this so that when setting a 
     601            // if there is a position set in the action then use this so that when setting a
    479602            // rally point on an entity it is centered on that entity
    480603            if (action.position)
    481604                target = action.position;
     
    507630                cursor = "action-attack-move";
    508631            }
    509632
     633            if (Engine.HotkeyIsPressed("session.meleeattackmove"))
     634            {
     635                if (Engine.HotkeyIsPressed("session.meleeattackmoveUnit"))
     636                    var targetClasses = { "melee-attack": ["Unit"] };
     637                else
     638                    var targetClasses = { "melee-attack": ["Unit", "Structure"] };
     639                data.command = "melee-attack-walk";
     640                data.targetClasses = targetClasses;
     641                cursor = "action-melee-attack";
     642            }
     643
     644            if (Engine.HotkeyIsPressed("session.rangedattackmove"))
     645            {
     646                if (Engine.HotkeyIsPressed("session.rangedattackmoveUnit"))
     647                    var targetClasses = { "ranged-attack": ["Unit"] };
     648                else
     649                    var targetClasses = { "ranged-attack": ["Unit", "Structure"] };
     650                data.command = "ranged-attack-walk";
     651                data.targetClasses = targetClasses;
     652                cursor = "action-ranged-attack";
     653            }
     654
    510655            if (targetState.garrisonHolder && playerCheck(entState, targetState, ["Player", "MutualAlly"]))
    511656            {
    512657                data.command = "garrison";
     
    610755                return false;
    611756            return {"type": "set-rallypoint", "cursor": actionInfo.cursor, "data": actionInfo.data, "tooltip": actionInfo.tooltip, "position": actionInfo.position};
    612757        },
    613         "specificness": 6,
     758        "specificness": 5,
    614759    },
    615760
    616     "unset-rallypoint": 
     761    "unset-rallypoint":
    617762    {
    618763        "execute": function(target, action, selection, queued)
    619764        {
     
    654799        "specificness": 11,
    655800    },
    656801
    657     "none": 
     802    "none":
    658803    {
    659804        "execute": function(target, action, selection, queued)
    660805        {
     
    668813 * Info and actions for the entity commands
    669814 * Currently displayed in the bottom of the central panel
    670815 */
    671 var g_EntityCommands = 
     816var g_EntityCommands =
    672817{
    673818    // Unload
    674819    "unload-all": {
  • binaries/data/mods/public/simulation/ai/common-api/entity.js

     
    749749        return false;
    750750    },
    751751
     752    isCapturable: function() { return this.get("Capturable") !== undefined; },
    752753    isGarrisonHolder: function() { return this.get("GarrisonHolder") !== undefined; },
    753754    garrisoned: function() { return this._entity.garrisoned; },
    754755    canGarrisonInside: function() { return this._entity.garrisoned.length < this.garrisonMax(); },
     
    759760    },
    760761
    761762    moveToRange: function(x, z, min, max, queued = false) {
    762         Engine.PostCommand(PlayerID,{"type": "walk-to-range", "entities": [this.id()], "x": x, "z": z, "min": min, "max": max, "queued": queued });
     763        Engine.PostCommand(PlayerID,{ "type": "walk-to-range", "entities": [this.id()], "x": x, "z": z, "min": min, "max": max, "queued": queued });
    763764        return this;
    764765    },
    765766
    766     attackMove: function(x, z, targetClasses, queued = false) {
    767         Engine.PostCommand(PlayerID,{"type": "attack-walk", "entities": [this.id()], "x": x, "z": z, "targetClasses": targetClasses, "queued": queued });
     767    attackMove: function(x, z, targetClasses, prefAttackType = "undefined", queued = false) { // hack
     768        Engine.PostCommand(PlayerID,{"type": "attack-walk", "entities": [this.id()], "x": x, "z": z, "targetClasses": targetClasses, "prefAttackType": prefAttackType, "queued": queued});
    768769        return this;
    769770    },
    770771
     
    798799        return this;
    799800    },
    800801
    801     attack: function(unitId, allowCapture = true, queued = false) {
    802         Engine.PostCommand(PlayerID,{"type": "attack", "entities": [this.id()], "target": unitId, "allowCapture": allowCapture, "queued": queued});
     802    attack: function(unitId, prefAttackType = undefined, queued = false) {
     803        Engine.PostCommand(PlayerID,{"type": "attack", "entities": [this.id()], "target": unitId, "prefAttackType": prefAttackType, "queued": queued});
    803804        return this;
    804805    },
    805806
  • 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": undefined }); // 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": undefined });
    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
     
    16401640                    {
    16411641                        let distmin = Math.min();
    16421642                        let attackerId;
     1643                        let attacker;
    16431644                        this.unitCollection.forEach( function (unit) {
    16441645                            if (!unit.position())
    16451646                                return;
     
    16491650                            let dist = API3.SquareVectorDistance(unit.position(), ent.position());
    16501651                            if (dist > distmin)
    16511652                                return;
     1653                            if (!gameState.getEntityById(unit.unitAIOrderData()[0].target))
     1654                                return;
    16521655                            distmin = dist;
    16531656                            attackerId = unit.unitAIOrderData()[0].target;
    1654 
     1657                            attacker = gameState.getEntityById(attackerId);
    16551658                        });
    16561659                        if (attackerId)
    1657                             ent.attack(attackerId, !this.noCapture.has(attackerId));
     1660                            ent.attack(attackerId, m.getPrefAttackType(ent, attacker, this.noCapture));
    16581661                    }
    16591662                }
    16601663            }
     
    18281831
    18291832    if (this.noCapture.has(targetId))
    18301833    {
    1831         ent.attack(targetId, false);
     1834        ent.attack(targetId, m.getPrefAttackType(ent, target, this.noCapture));
    18321835        return true;
    18331836    }
    18341837
     
    18401843    if (target.hasClass("Siege") && target.hasClass("Melee"))
    18411844    {
    18421845        this.noCapture.add(targetId);
    1843         ent.attack(targetId, false);
     1846        ent.attack(targetId, m.getPrefAttackType(ent, target, this.noCapture));
    18441847        return true;
    18451848    }
    18461849
     
    18571860    if (antiCapture >= this.captureStrength)
    18581861    {
    18591862        this.noCapture.add(targetId);
    1860         ent.attack(targetId, false);
     1863        ent.attack(targetId, "primary"); // hack
    18611864        return true;
    18621865    }
    18631866
     
    18661869        this.unitCollection.length < 2*target.garrisoned().length)
    18671870    {
    18681871        this.noCapture.add(targetId);
    1869         ent.attack(targetId, false);
     1872        ent.attack(targetId, "primary"); // hack
    18701873        return true;
    18711874    }
    18721875
  • 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 || 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
     278    const cmpIdentity = Engine.QueryInterface(target, IID_Identity);
     279    if (!cmpIdentity)
     280        return undefined;
     281
     282    let cmpEntityPlayer = QueryOwnerInterface(this.entity);
     283    let cmpTargetPlayer = QueryOwnerInterface(target);
     284    if (!cmpTargetPlayer || !cmpEntityPlayer)
     285        return false;
     286
     287    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;
     292
    250293    // Check if the relative height difference is larger than the attack range
    251294    // If the relative height is bigger, it means they will never be able to
    252295    // reach each other, no matter how close they come.
    253296    let heightDiff = Math.abs(cmpThisPosition.GetHeightOffset() - cmpTargetPosition.GetHeightOffset());
    254297
    255     const cmpIdentity = Engine.QueryInterface(target, IID_Identity);
    256     if (!cmpIdentity)
    257         return undefined;
    258 
    259     const targetClasses = cmpIdentity.GetClassesList();
    260 
    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
     
    294334Attack.prototype.GetPreference = function(target)
    295335{
    296336    const cmpIdentity = Engine.QueryInterface(target, IID_Identity);
    297     if (!cmpIdentity) 
     337    if (!cmpIdentity)
    298338        return undefined;
    299339
    300340    const targetClasses = cmpIdentity.GetClassesList();
     
    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)
     
    344382        let best = ["Ranged", "Melee", "Capture"];
    345383        let types = this.GetAttackTypes();
    346384        for (let attack of best)
    347             if (types.indexOf(attack) != -1)
     385            if (types.indexOf(attack) != -1 && (!prefAttackType || "no" + attack != prefAttackType))
    348386                return attack;
    349387        return undefined;
    350388    }
    351389
    352390    let cmpIdentity = Engine.QueryInterface(target, IID_Identity);
    353     if (!cmpIdentity) 
     391    if (!cmpIdentity)
    354392        return undefined;
    355393
     394    // If we are visisble garrisoned always do ranged attack if we can.
     395    let cmpPosition = Engine.QueryInterface(this.entity, IID_Position);
     396    if (cmpPosition && cmpPosition.GetTurretParent() != INVALID_ENTITY)
     397        return this.template.Ranged ? "Ranged" : undefined;
     398
    356399    let targetClasses = cmpIdentity.GetClassesList();
    357400    let isTargetClass = function (className) { return targetClasses.indexOf(className) != -1; };
    358401
    359402    // Always slaughter domestic animals instead of using a normal attack
    360     if (isTargetClass("Domestic") && this.template.Slaughter) 
     403    if (isTargetClass("Domestic") && this.template.Slaughter)
    361404        return "Slaughter";
    362405
    363406    let attack = this;
    364     let isAllowed = function (type) { return !attack.GetRestrictedClasses(type).some(isTargetClass); };
     407    let isAllowed = function (type) { return !attack.GetRestrictedClasses(type).some(isTargetClass) && (!prefAttackType || "no" + type != prefAttackType); };
    365408
    366409    let types = this.GetAttackTypes().filter(isAllowed);
     410    if (types.length === 0)
     411        return undefined;
    367412
    368     // check if the target is capturable
    369     let captureIndex = types.indexOf("Capture");
    370     if (captureIndex != -1)
     413    prefAttackType = this.GetAttackTypeFromOrder(prefAttackType);
     414    if (prefAttackType && prefAttackType != "Capture" && types.indexOf(prefAttackType) != -1)
     415        return prefAttackType;
     416    else if (!prefAttackType || prefAttackType == "Capture")
    371417    {
    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);
     418        // check if the target is capturable
     419        let captureIndex = types.indexOf("Capture");
     420        if (captureIndex != -1)
     421        {
     422            let cmpCapturable = QueryMiragedInterface(target, IID_Capturable);
     423            let cmpPlayer = QueryOwnerInterface(this.entity);
     424            if (cmpPlayer && cmpCapturable && cmpCapturable.CanCapture(cmpPlayer.GetPlayerID()))
     425                return "Capture";
     426            // not captureable, so remove this attack
     427            types.splice(captureIndex, 1);
     428        }
    379429    }
    380430
    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) ); };
     431    // ignore charges for now: TODO implement these
     432    let chargeIndex = types.indexOf("Charge");
     433    if (chargeIndex != -1)
     434        types.splice(chargeIndex, 1);
    383435
    384     return types.sort(byPreference).pop();
     436    if (types.length === 0)
     437        return undefined;
     438
     439    // assume ranged and/or melee attack left
     440    // TODO stop assuming that?
     441    let meleeIndex = types.indexOf("Melee");
     442    let rangedIndex = types.indexOf("Ranged");
     443    if (meleeIndex != -1 && rangedIndex != -1)
     444    {
     445        if (this.HasTargetPreferredClass(types, targetClasses))
     446        {
     447            let isPreferred = function (className) { return attack.GetPreferredClasses(className).some(isTargetClass); };
     448            let byPreference = function (a, b) { return (types.indexOf(a) + (isPreferred(a) ? types.length : 0) ) - (types.indexOf(b) + (isPreferred(b) ? types.length : 0) ); };
     449
     450            return types.sort(byPreference).pop();
     451        }
     452
     453        if (!cmpPosition.IsInWorld())
     454            return undefined;
     455        let selfPosition = cmpPosition.GetPosition2D();
     456        let cmpTargetPosition = Engine.QueryInterface(target, IID_Position);
     457        if (!cmpTargetPosition || !cmpTargetPosition.IsInWorld())
     458            return undefined;
     459        let targetPosition = cmpTargetPosition.GetPosition2D();
     460        if (targetPosition.distanceToSquared(selfPosition) <= Math.pow(this.GetChangeDistance(), 2))
     461            return "Melee";
     462        return "Ranged";
     463    }
     464    else if (meleeIndex != -1)
     465        return "Melee";
     466    else if (rangedIndex != -1)
     467        return "Ranged";
     468    return types[0];
    385469};
    386470
    387471Attack.prototype.CompareEntitiesByPreference = function(a, b)
     
    478562    return attackBonus;
    479563};
    480564
     565Attack.prototype.GetAttackTypeFromOrder = function(prefAttackType)
     566{
     567    let types = this.GetAttackTypes();
     568    for (let type of types)
     569        if (this.template[type].AttackOrder && this.template[type].AttackOrder == prefAttackType)
     570            return type;
     571    return prefAttackType;
     572};
     573
     574Attack.prototype.HasTargetPreferredClass = function(types, targetClasses)
     575{
     576    let isTargetClass = function (className) { return targetClasses.indexOf(className) != -1; };
     577    for (let type of types)
     578        if (this.template[type].PreferredClasses && this.GetPreferredClasses(type).some(isTargetClass))
     579            return true;
     580    return false
     581};
     582
    481583// Returns a 2d random distribution scaled for a spread of scale 1.
    482584// The current implementation is a 2d gaussian with sigma = 1
    483585Attack.prototype.GetNormalDistribution = function(){
     
    486588    let a = Math.random();
    487589    let b = Math.random();
    488590
    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);
     591    let c = Math.sqrt(-2 * Math.log(a)) * Math.cos(2 * Math.PI * b);
     592    let d = Math.sqrt(-2 * Math.log(a)) * Math.sin(2 * Math.PI * b);
    491593
    492594    return [c, d];
    493595};
     
    503605    if (type == "Ranged")
    504606    {
    505607        let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
    506         let turnLength = cmpTimer.GetLatestTurnLength()/1000;
     608        let turnLength = cmpTimer.GetLatestTurnLength() / 1000;
    507609        // In the future this could be extended:
    508610        //  * Obstacles like trees could reduce the probability of the target being hit
    509611        //  * Obstacles like walls should block projectiles entirely
     
    535637
    536638        let horizDistance = targetPosition.horizDistanceTo(selfPosition);
    537639
    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
     640        // This is an approximation of the time to the target, it assumes that the target has a constant radial
     641        // velocity, but since units move in straight lines this is not true. The exact value would be more
     642        // difficult to calculate and I think this is sufficiently accurate. (I tested and for cavalry it was
    541643        // about 5% of the units radius out in the worst case)
    542644        let timeToTarget = horizDistance / (horizSpeed - radialSpeed);
    543645
     
    620722Attack.prototype.InterpolatedLocation = function(ent, lateness)
    621723{
    622724    let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
    623     let turnLength = cmpTimer.GetLatestTurnLength()/1000;
     725    let turnLength = cmpTimer.GetLatestTurnLength() / 1000;
    624726    let cmpTargetPosition = Engine.QueryInterface(ent, IID_Position);
    625727    if (!cmpTargetPosition || !cmpTargetPosition.IsInWorld()) // TODO: handle dead target properly
    626728        return undefined;
     
    658760        let d = Vector3D.sub(point, targetPosition);
    659761        d = Vector2D.from3D(d).rotate(-angle);
    660762
    661         return d.x < Math.abs(targetShape.width/2) && d.y < Math.abs(targetShape.depth/2);
     763        return d.x < Math.abs(targetShape.width / 2) && d.y < Math.abs(targetShape.depth / 2);
    662764    }
    663765};
    664766
     
    740842        return;
    741843
    742844    for (let type of this.GetAttackTypes())
    743         if (msg.valueNames.indexOf("Attack/"+type+"/MaxRange") !== -1)
     845        if (msg.valueNames.indexOf("Attack/" + type + "/MaxRange") !== -1)
    744846        {
    745847            cmpUnitAI.UpdateRangeQueries();
    746848            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

     
    351351        ret.garrisonHolder = {
    352352            "entities": cmpGarrisonHolder.GetEntities(),
    353353            "allowedClasses": cmpGarrisonHolder.GetAllowedClasses(),
     354            "neededAttackTypes": cmpGarrisonHolder.GetNeededAttackTypes(),
    354355            "capacity": cmpGarrisonHolder.GetCapacity(),
    355356            "garrisonedEntitiesCount": cmpGarrisonHolder.GetGarrisonedEntitiesCount()
    356357        };
     
    18071808    if (!cmpAttack)
    18081809        return false;
    18091810
    1810     let cmpEntityPlayer = QueryOwnerInterface(data.entity, IID_Player);
    1811     let cmpTargetPlayer = QueryOwnerInterface(data.target, IID_Player);
    1812     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)
    18131818        return false;
    18141819
    1815     // if the owner is an enemy, it's up to the attack component to decide
    1816     if (cmpEntityPlayer.IsEnemy(cmpTargetPlayer.GetPlayerID()))
    1817         return cmpAttack.CanAttack(data.target);
    1818 
    1819     return false;
     1820    return cmpAttack.CanAttack(data.target, data.type);
    18201821};
    18211822
    18221823/*
     
    19631964    "GetTradingDetails": 1,
    19641965    "CanCapture": 1,
    19651966    "CanAttack": 1,
     1967    "CanAttackWithType": 1,
    19661968    "GetBatchTime": 1,
    19671969
    19681970    "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    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; },
     
    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>