Ticket #252: t252_secondattack_21.diff

File t252_secondattack_21.diff, 55.1 KB (added by bb, 8 years ago)

rebased, fixed broken test and updated intro.txt

  • binaries/data/config/default.cfg

     
    280280kill = Delete                ; Destroy selected units
    281281stop = "H"                   ; Stop the current action
    282282unload = "U"                 ; Unload garrisoned units when a building/mechanical unit is selected
    283 attack = Ctrl                ; Modifier to attack instead of another action (eg capture)
    284 attackmove = Ctrl            ; Modifier to attackmove when clicking on a point
    285 attackmoveUnit = "Ctrl+Q"    ; Modifier to attackmove targeting only units when clicking on a point (should contain the attackmove keys)
     283attack = Ctrl                ; Modifier to primary attack instead of another action (eg capture)
     284attackmove = Ctrl            ; Modifier to primary attackmove when clicking on a point
     285attackmoveUnit = "Ctrl+Q"    ; Modifier to primary attackmove targeting only units when clicking on a point (should contain the attackmove keys)
    286286garrison = Ctrl              ; Modifier to garrison when clicking on building
    287287autorallypoint = Ctrl        ; Modifier to set the rally point on the building itself
     288meleeattack = Alt            ; Modifier to melee attack instead of another action
     289rangedattack = Space         ; Modifier to ranged attack instead of another action
    288290guard = "G"                  ; Modifier to escort/guard when clicking on unit/building
    289291repair = "J"                 ; Modifier to repair when clicking on building/mechanical unit
    290292queue = Shift                ; Modifier to queue unit orders instead of replacing
  • 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/actors/units/persians/infantry_archer_a.xml

     
    3636  <group>
    3737    <variant frequency="100" name="Idle"/>
    3838    <variant file="biped/attack_ranged_archer.xml"/>
     39    <variant file="biped/attack_melee_ranged.xml"/>
    3940    <variant file="biped/attack_capture.xml"/>
    4041    <variant file="biped/attack_slaughter.xml"/>
    4142    <variant file="biped/gather_tree.xml"/>
  • binaries/data/mods/public/art/actors/units/persians/infantry_archer_b.xml

     
    2424  <group>
    2525    <variant frequency="100" name="Idle"/>
    2626    <variant file="biped/attack_ranged_archer.xml"/>
     27    <variant file="biped/attack_melee_ranged.xml"/>
    2728    <variant file="biped/attack_capture.xml"/>
    2829    <variant file="biped/attack_slaughter.xml"/>
    2930    <variant file="biped/gather_tree.xml"/>
  • binaries/data/mods/public/art/actors/units/persians/infantry_archer_e.xml

     
    3737  <group>
    3838    <variant frequency="100" name="Idle"/>
    3939    <variant file="biped/attack_ranged_archer.xml"/>
     40    <variant file="biped/attack_melee_ranged.xml"/>
    4041    <variant file="biped/attack_capture.xml"/>
    4142    <variant file="biped/attack_slaughter.xml"/>
    4243    <variant file="biped/gather_tree.xml"/>
  • 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/art/variants/biped/attack_melee_ranged.xml

     
     1<variant name="attack_melee">
     2  <animations>
     3    <animation event="0.5" file="infantry/sword/attack/isw_s_off_05.psa" name="attack_melee" speed="100"/>
     4  </animations>
     5  <props>
     6    <prop actor="props/units/weapons/knife.xml" attachpoint="r_hand"/>
     7  </props>
     8</variant>
  • binaries/data/mods/public/globalscripts/Templates.js

     
    6666}
    6767
    6868/**
     69 * Check if entity has all needed attack types.
     70 */
     71function HasNeededAttackTypes(attack, neededAttackTypes)
     72{
     73    if (!neededAttackTypes)
     74        return true;
     75    // transform the string to an array
     76    if (typeof neededAttackTypes == "string")
     77        neededAttackTypes = neededAttackTypes.split(/\s+/);
     78
     79    for (let type of neededAttackTypes)
     80        if (!attack || attack.constructor === Object && !attack[type] ||
     81            attack.constructor === Array && attack.indexOf(type) == -1)
     82            return false;
     83    return true;
     84}
     85
     86/**
    6987 * Get information about a template with or without technology modifications.
    7088 *
    7189 * NOTICE: The data returned here should have the same structure as
  • binaries/data/mods/public/gui/manual/intro.txt

     
    9494Right Click with a building/buildings selected: sets a rally point for units created/ungarrisoned from that building.
    9595Ctrl + Right Click with units selected:
    9696    - If the cursor is over an allied structure: Garrison
    97     - If the cursor is over a non-allied unit or building: Attack (instead of capture or gather)
    98     - Otherwise: Attack move (by default all enemy units and structures along the way are targeted, use Ctrl + Q + Right Click to target only units).
     97    - If the cursor is over a non-allied unit or building: Primary Attack (instead of capture, gather)
     98    - Otherwise: Attack move (by default all enemy units and structures along the way are targeted, use Ctrl + Q + Right Click to target only units).
     99Alt + Right Click on non-allied unit or building: Melee attack
     100Space + Right Click on non-allied unit or building: Ranged attack
    99101
    100102[font="sans-bold-14"]Overlays
    101103[font="sans-14"]Alt + G: Hide/show the GUI
  • binaries/data/mods/public/gui/session/input.js

     
    228228        if (unitActions[action] && unitActions[action].getActionInfo)
    229229        {
    230230            var r = unitActions[action].getActionInfo(entState, targetState, simState);
    231             if (r) // return true if it's possible for one of the entities
     231            if (r && r.possible) // return true if it's possible for one of the entities
    232232                return r;
    233233        }
    234234    }
  • binaries/data/mods/public/gui/session/unit_actions.js

     
    5858
    5959            return { "type": "move" };
    6060        },
    61         "specificness": 12,
     61        "specificness": 13,
    6262    },
    6363
    6464    "attack-move":
     
    7777                "x": target.x,
    7878                "z": target.z,
    7979                "targetClasses": targetClasses,
    80                 "queued": queued
     80                "queued": queued,
     81                "prefAttackType": "noCapture"
    8182            });
    8283
    8384            Engine.GuiInterfaceCall("PlaySound", {
     
    114115                "type": "attack",
    115116                "entities": selection,
    116117                "target": action.target,
    117                 "allowCapture": true,
    118                 "queued": queued
     118                "queued": queued,
     119                "prefAttackType": "Capture"
    119120            });
    120121
    121122            Engine.GuiInterfaceCall("PlaySound", {
     
    130131            if (!entState.attack || !targetState.hitpoints)
    131132                return false;
    132133
    133             return {
    134                 "possible": Engine.GuiInterfaceCall("CanCapture", {
     134            return { "possible": Engine.GuiInterfaceCall("CanCapture", {
    135135                    "entity": entState.id,
    136136                    "target": targetState.id
    137137                })
     
    148148                "target": target
    149149            };
    150150        },
    151         "specificness": 9,
     151        "specificness": 7,
    152152    },
    153153
    154154    "attack":
     
    160160                "entities": selection,
    161161                "target": action.target,
    162162                "queued": queued,
    163                 "allowCapture": false
     163                "prefAttackType": "noCapture"
    164164            });
    165165
    166166            Engine.GuiInterfaceCall("PlaySound", {
     
    205205                "target": target
    206206            };
    207207        },
     208        "specificness": 8,
     209    },
     210
     211    "melee-attack":
     212    {
     213        "execute": function(target, action, selection, queued)
     214        {
     215            Engine.PostNetworkCommand({
     216                "type": "attack",
     217                "entities": selection,
     218                "target": action.target,
     219                "queued": queued,
     220                "prefAttackType": "Melee"
     221            });
     222
     223            Engine.GuiInterfaceCall("PlaySound", {
     224                "name": "order_attack",
     225                "entity": selection[0]
     226            });
     227
     228            return true;
     229        },
     230        "getActionInfo": function(entState, targetState)
     231        {
     232            if (!entState.attack || !targetState.hitpoints)
     233                return false;
     234
     235            return { "possible": Engine.GuiInterfaceCall("CanAttack", {
     236                "entity": entState.id,
     237                "target": targetState.id,
     238                "type": "Melee"
     239            }) };
     240        },
     241        "hotkeyActionCheck": function(target)
     242        {
     243            if (!Engine.HotkeyIsPressed("session.meleeattack") || !getActionInfo("melee-attack", target).possible)
     244                return false;
     245
     246            return {
     247                "type": "melee-attack",
     248                "cursor": "action-melee-attack",
     249                "target": target
     250            };
     251        },
     252        "actionCheck": function(target)
     253        {
     254            if (!getActionInfo("melee-attack", target).possible)
     255                return false;
     256
     257            return {
     258                "type": "melee-attack",
     259                "cursor": "action-melee-attack",
     260                "target": target
     261            };
     262        },
     263        "specificness": 9,
     264    },
     265
     266    "ranged-attack":
     267    {
     268        "execute": function(target, action, selection, queued)
     269        {
     270            Engine.PostNetworkCommand({
     271                "type": "attack",
     272                "entities": selection,
     273                "target": action.target,
     274                "queued": queued,
     275                "prefAttackType": "Ranged"
     276            });
     277
     278            Engine.GuiInterfaceCall("PlaySound", {
     279                "name": "order_attack",
     280                "entity": selection[0]
     281            });
     282
     283            return true;
     284        },
     285        "getActionInfo": function(entState, targetState)
     286        {
     287            if (!entState.attack || !targetState.hitpoints)
     288                return false;
     289
     290            return { "possible": Engine.GuiInterfaceCall("CanAttack", {
     291                "entity": entState.id,
     292                "target": targetState.id,
     293                "type": "Ranged"
     294            }) };
     295        },
     296        "hotkeyActionCheck": function(target, selection)
     297        {
     298            if (!Engine.HotkeyIsPressed("session.rangedattack") || !getActionInfo("ranged-attack", target).possible)
     299                return false;
     300
     301            return {
     302                "type": "ranged-attack",
     303                "cursor": "action-ranged-attack",
     304                "target": target
     305            };
     306        },
     307        "actionCheck": function(target, selection)
     308        {
     309            if (!getActionInfo("ranged-attack", target).possible)
     310                return false;
     311
     312            return {
     313                "type": "ranged-attack",
     314                "cursor": "action-ranged-attack",
     315                "target": target
     316            };
     317        },
    208318        "specificness": 10,
    209319    },
    210320
     
    255365                "target": target
    256366            };
    257367        },
    258         "specificness": 7,
     368        "specificness": 6,
    259369    },
    260370
    261371    "build":
     
    368478                "target": target
    369479            };
    370480        },
    371         "specificness": 11,
     481        "specificness": 12,
    372482    },
    373483
    374484    "gather":
     
    389499
    390500            return true;
    391501        },
    392         "getActionInfo":  function(entState, targetState)
     502        "getActionInfo": function(entState, targetState)
    393503        {
    394504            if (!targetState.resourceSupply)
    395505                return false;
     
    608718            if (targetState.garrisonHolder.garrisonedEntitiesCount + extraCount >= targetState.garrisonHolder.capacity)
    609719                tooltip = "[color=\"orange\"]" + tooltip + "[/color]";
    610720
    611             if (!MatchesClassList(entState.identity.classes, targetState.garrisonHolder.allowedClasses))
    612                 return false;
     721            if (!MatchesClassList(entState.identity.classes, targetState.garrisonHolder.allowedClasses) ||
     722                !HasNeededAttackTypes(entState.attack, targetState.garrisonHolder.neededAttackTypes))
     723                return { "possible": false };
    613724
    614725            return {
    615726                "possible": true,
     
    9221033                "position": actionInfo.position
    9231034            };
    9241035        },
    925         "specificness": 6,
     1036        "specificness": 5,
    9261037    },
    9271038
    9281039    "unset-rallypoint":
     
    10051116            unloadAll();
    10061117        },
    10071118    },
     1119
    10081120    "delete": {
    10091121        "getInfo": function(entState)
    10101122        {
     
    10431155                openDeleteDialog(selection);
    10441156        },
    10451157    },
     1158
    10461159    "stop": {
    10471160        "getInfo": function(entState)
    10481161        {
  • binaries/data/mods/public/simulation/ai/common-api/entity.js

     
    742742        return false;
    743743    },
    744744
     745    isCapturable: function() { return this.get("Capturable") !== undefined; },
    745746    isGarrisonHolder: function() { return this.get("GarrisonHolder") !== undefined; },
    746747    garrisoned: function() { return this._entity.garrisoned; },
    747748    canGarrisonInside: function() { return this._entity.garrisoned.length < this.garrisonMax(); },
     
    756757        return this;
    757758    },
    758759
    759     attackMove: function(x, z, targetClasses, queued = false) {
    760         Engine.PostCommand(PlayerID,{"type": "attack-walk", "entities": [this.id()], "x": x, "z": z, "targetClasses": targetClasses, "queued": queued });
     760    attackMove: function(x, z, targetClasses, prefAttackType = "undefined", queued = false) {
     761        Engine.PostCommand(PlayerID,{ "type": "attack-walk", "entities": [this.id()], "x": x, "z": z, "targetClasses": targetClasses, "prefAttackType": prefAttackType, "queued": queued });
    761762        return this;
    762763    },
    763764
     
    791792        return this;
    792793    },
    793794
    794     attack: function(unitId, allowCapture = true, queued = false) {
    795         Engine.PostCommand(PlayerID,{"type": "attack", "entities": [this.id()], "target": unitId, "allowCapture": allowCapture, "queued": queued});
     795    attack: function(unitId, prefAttackType = undefined, queued = false) {
     796        Engine.PostCommand(PlayerID,{ "type": "attack", "entities": [this.id()], "target": unitId, "prefAttackType": prefAttackType, "queued": queued });
    796797        return this;
    797798    },
    798799
  • binaries/data/mods/public/simulation/ai/common-api/entitycollection.js

     
    156156m.EntityCollection.prototype.attackMove = function(x, z, targetClasses, queued)
    157157{
    158158    queued = queued || false;
    159     Engine.PostCommand(PlayerID,{"type": "attack-walk", "entities": this.toIdArray(), "x": x, "z": z, "targetClasses": targetClasses, "queued": queued});
     159    Engine.PostCommand(PlayerID, { "type": "attack-walk", "entities": this.toIdArray(), "x": x, "z": z, "targetClasses": targetClasses, "queued": queued, "prefAttackType": undefined });
    160160    return this;
    161161};
    162162
     
    182182
    183183m.EntityCollection.prototype.attack = function(unitId)
    184184{
    185     Engine.PostCommand(PlayerID,{"type": "attack", "entities": this.toIdArray(), "target": unitId, "queued": false});
     185    Engine.PostCommand(PlayerID,{ "type": "attack", "entities": this.toIdArray(), "target": unitId, "queued": false, "prefAttackType": undefined });
    186186    return this;
    187187};
    188188
  • binaries/data/mods/public/simulation/ai/petra/attackPlan.js

     
    12371237                {
    12381238                    if (this.isSiegeUnit(gameState, ent))   // needed as mauryan elephants are not filtered out
    12391239                        continue;
    1240                     ent.attack(attacker.id(), !this.noCapture.has(attacker.id()));
     1240                    ent.attack(attacker.id(), m.getPrefAttackType(ent, attacker, this.noCapture));
    12411241                    ent.setMetadata(PlayerID, "lastAttackPlanUpdateTime", time);
    12421242                }
    12431243                // And if this attacker is a non-ranged siege unit and our unit also, attack it
    12441244                if (this.isSiegeUnit(gameState, attacker) && attacker.hasClass("Melee") && ourUnit.hasClass("Melee"))
    12451245                {
    1246                     ourUnit.attack(attacker.id(), false);
     1246                    ourUnit.attack(attacker.id(), "Melee");
    12471247                    ourUnit.setMetadata(PlayerID, "lastAttackPlanUpdateTime", time);
    12481248                }
    12491249            }
     
    12601260                    let collec = this.unitCollection.filter(API3.Filters.byClass("Melee")).filterNearest(ourUnit.position(), 5);
    12611261                    for (let ent of collec.values())
    12621262                    {
    1263                         ent.attack(attacker.id(), false);
     1263                        ent.attack(attacker.id(), m.getPrefAttackType(ent, attacker, this.noCapture));
    12641264                        ent.setMetadata(PlayerID, "lastAttackPlanUpdateTime", time);
    12651265                    }
    12661266                }
     
    12791279                                continue;
    12801280                        }
    12811281                    }
    1282                     ourUnit.attack(attacker.id(), !this.noCapture.has(attacker.id()));
     1282                    ourUnit.attack(attacker.id(), m.getPrefAttackType(ourUnit, attacker, this.noCapture));
    12831283                    ourUnit.setMetadata(PlayerID, "lastAttackPlanUpdateTime", time);
    12841284                }
    12851285            }
     
    14591459                        return valb - vala;
    14601460                    });
    14611461                    if (mStruct[0].hasClass("Gates"))
    1462                         ent.attack(mStruct[0].id(), false);
     1462                        ent.attack(mStruct[0].id(), m.getPrefAttackType(ent, mStruct[0], this.noCapture));
    14631463                    else
    14641464                    {
    14651465                        let rand = Math.floor(Math.random() * mStruct.length * 0.2);
    1466                         let newTargetId = mStruct[rand].id();
    1467                         ent.attack(newTargetId, !this.noCapture.has(newTargetId));
     1466                        let newTarget = mStruct[rand];
     1467                        ent.attack(newTarget.id(), m.getPrefAttackType(ent, newTarget, this.noCapture));
    14681468                    }
    14691469                }
    14701470                else
     
    15221522                        return valb - vala;
    15231523                    });
    15241524                    let rand = Math.floor(Math.random() * mUnit.length * 0.1);
    1525                     let newTargetId = mUnit[rand].id();
    1526                     ent.attack(newTargetId, !this.noCapture.has(newTargetId));
     1525                    let newTarget = mUnit[rand];
     1526                    ent.attack(newTarget.id(), m.getPrefAttackType(ent, newTarget, this.noCapture));
    15271527                }
    15281528                else if (this.isBlocked)
    15291529                    ent.attack(this.target.id(), false);
     
    15701570                            return valb - vala;
    15711571                        });
    15721572                        if (mStruct[0].hasClass("Gates"))
    1573                             ent.attack(mStruct[0].id(), false);
     1573                            ent.attack(mStruct[0].id(), m.getPrefAttackType(ent, mStruct[0], this.noCapture));
    15741574                        else
    15751575                        {
    15761576                            let rand = Math.floor(Math.random() * mStruct.length * 0.2);
    1577                             let newTargetId = mStruct[rand].id();
    1578                             ent.attack(newTargetId, !this.noCapture.has(newTargetId));
     1577                            let newTarget = mStruct[rand];
     1578                            ent.attack(newTarget.id(), m.getPrefAttackType(ent, newTarget, this.noCapture));
    15791579                        }
    15801580                    }
    15811581                    else if (needsUpdate)  // really nothing   let's try to help our nearest unit
     
    15821582                    {
    15831583                        let distmin = Math.min();
    15841584                        let attackerId;
     1585                        let attacker;
    15851586                        this.unitCollection.forEach( function (unit) {
    15861587                            if (!unit.position())
    15871588                                return;
     
    15911592                            let dist = API3.SquareVectorDistance(unit.position(), ent.position());
    15921593                            if (dist > distmin)
    15931594                                return;
     1595                            if (!gameState.getEntityById(unit.unitAIOrderData()[0].target))
     1596                                return;
    15941597                            distmin = dist;
    15951598                            attackerId = unit.unitAIOrderData()[0].target;
     1599                            attacker = gameState.getEntityById(attackerId);
    15961600                        });
    15971601                        if (attackerId)
    1598                             ent.attack(attackerId, !this.noCapture.has(attackerId));
     1602                            ent.attack(attackerId, m.getPrefAttackType(ent, attacker, this.noCapture));
    15991603                    }
    16001604                }
    16011605            }
     
    16481652                continue;
    16491653            if (!ent.isIdle())
    16501654                continue;
    1651             ent.attack(attacker.id(), !this.noCapture.has(attacker.id()));
     1655            ent.attack(attacker.id(), !this.noCapture.has(attacker.id(), this.noCapture));
    16521656        }
    16531657        break;
    16541658    }
     
    20062010
    20072011    if (this.noCapture.has(targetId))
    20082012    {
    2009         ent.attack(targetId, false);
     2013        ent.attack(targetId, m.getPrefAttackType(ent, target, this.noCapture));
    20102014        return true;
    20112015    }
    20122016
     
    20182022    if (target.hasClass("Siege") && target.hasClass("Melee"))
    20192023    {
    20202024        this.noCapture.add(targetId);
    2021         ent.attack(targetId, false);
     2025        ent.attack(targetId, m.getPrefAttackType(ent, target, this.noCapture));
    20222026        return true;
    20232027    }
    20242028
     
    20352039    if (antiCapture >= this.captureStrength)
    20362040    {
    20372041        this.noCapture.add(targetId);
    2038         ent.attack(targetId, false);
     2042        ent.attack(targetId, "noCapture");
    20392043        return true;
    20402044    }
    20412045
     
    20442048        this.unitCollection.length < 2*target.garrisoned().length)
    20452049    {
    20462050        this.noCapture.add(targetId);
    2047         ent.attack(targetId, false);
     2051        ent.attack(targetId, "noCapture");
    20482052        return true;
    20492053    }
    20502054
  • binaries/data/mods/public/simulation/ai/petra/defenseArmy.js

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

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

     
    5151    if (this.invulnerable)
    5252        return { "killed": false, "change": 0 };
    5353
    54     // Adjust damage values based on armour; exponential armour: damage = attack * 0.9^armour
    55     var armourStrengths = this.GetArmourStrengths();
    56     var adjHack = hack * Math.pow(0.9, armourStrengths.hack);
    57     var adjPierce = pierce * Math.pow(0.9, armourStrengths.pierce);
    58     var adjCrush = crush * Math.pow(0.9, armourStrengths.crush);
     54    let damage = this.GetDamage(hack, pierce, crush);
    5955
    60     // Total is sum of individual damages
    61     // Don't bother rounding, since HP is no longer integral.
    62     var total = adjHack + adjPierce + adjCrush;
    63 
    6456    // Reduce health
    65     var cmpHealth = Engine.QueryInterface(this.entity, IID_Health);
    66     return cmpHealth.Reduce(total);
     57    let cmpHealth = Engine.QueryInterface(this.entity, IID_Health);
     58    return cmpHealth.Reduce(damage);
    6759};
    6860
    6961Armour.prototype.GetArmourStrengths = function()
    7062{
    7163    // Work out the armour values with technology effects
    72     var applyMods = (type, foundation) => {
    73         var strength;
     64    let applyMods = (type, foundation) => {
     65        let strength;
    7466        if (foundation)
    7567        {
    7668            strength = +this.template.Foundation[type];
     
    8274        return ApplyValueModificationsToEntity("Armour/" + type, strength, this.entity);
    8375    };
    8476   
    85     var foundation = Engine.QueryInterface(this.entity, IID_Foundation) && this.template.Foundation;
     77    let foundation = Engine.QueryInterface(this.entity, IID_Foundation) && this.template.Foundation;
    8678
    8779    return {
    8880        "hack": applyMods("Hack", foundation),
     
    9183    };
    9284};
    9385
     86Armour.prototype.GetDamage = function(hack, pierce, crush)
     87{
     88    // Adjust damage values based on armour; exponential armour: damage = attack * 0.9^armour
     89    let armourStrengths = this.GetArmourStrengths();
     90    let adjHack = hack * Math.pow(0.9, armourStrengths.hack);
     91    let adjPierce = pierce * Math.pow(0.9, armourStrengths.pierce);
     92    let adjCrush = crush * Math.pow(0.9, armourStrengths.crush);
     93
     94    // Total is sum of individual damages
     95    // Don't bother rounding, since HP is no longer integral.
     96    return adjHack + adjPierce + adjCrush;
     97};
     98
    9499Engine.RegisterComponentType(IID_DamageReceiver, "Armour", Armour);
  • binaries/data/mods/public/simulation/components/Attack.js

     
    208208    return [];
    209209};
    210210
    211 Attack.prototype.CanAttack = function(target)
     211Attack.prototype.CanAttack = function(target, wantedType)
    212212{
    213213    let cmpFormation = Engine.QueryInterface(target, IID_Formation);
    214214    if (cmpFormation)
    215215        return true;
    216216
     217    if (wantedType && !this.template[wantedType])
     218        return false;
     219
    217220    let cmpThisPosition = Engine.QueryInterface(this.entity, IID_Position);
    218221    let cmpTargetPosition = Engine.QueryInterface(target, IID_Position);
    219222    if (!cmpThisPosition || !cmpTargetPosition || !cmpThisPosition.IsInWorld() || !cmpTargetPosition.IsInWorld())
    220223        return false;
    221224
    222     // Check if the relative height difference is larger than the attack range
    223     // If the relative height is bigger, it means they will never be able to
    224     // reach each other, no matter how close they come.
    225     let heightDiff = Math.abs(cmpThisPosition.GetHeightOffset() - cmpTargetPosition.GetHeightOffset());
    226 
    227225    const cmpIdentity = Engine.QueryInterface(target, IID_Identity);
    228226    if (!cmpIdentity)
    229227        return undefined;
    230228
     229    let cmpEntityPlayer = QueryOwnerInterface(this.entity);
     230    let cmpTargetPlayer = QueryOwnerInterface(target);
     231    if (!cmpTargetPlayer || !cmpEntityPlayer)
     232        return false;
     233
    231234    const targetClasses = cmpIdentity.GetClassesList();
     235    let cmpCapturable = QueryMiragedInterface(target, IID_Capturable);
     236    if (targetClasses.indexOf("Domestic") == -1 && !cmpEntityPlayer.IsEnemy(cmpTargetPlayer.GetPlayerID()) &&
     237       (!cmpCapturable || !cmpCapturable.CanCapture(cmpEntityPlayer.GetPlayerID())))
     238        return false;
    232239
     240    // Check if the relative height difference is larger than the attack range
     241    // If the relative height is bigger, it means they will never be able to
     242    // reach each other, no matter how close they come.
     243    let heightDiff = Math.abs(cmpThisPosition.GetHeightOffset() - cmpTargetPosition.GetHeightOffset());
     244
    233245    for (let type of this.GetAttackTypes())
    234246    {
    235         if (type == "Capture" && !QueryMiragedInterface(target, IID_Capturable))
     247        if (wantedType && type != wantedType)
    236248            continue;
    237249
     250        if (type == "Capture" && !cmpCapturable)
     251            continue;
     252
    238253        if (heightDiff > this.GetRange(type).max)
    239254            continue;
    240255
     
    291306    return ret;
    292307};
    293308
    294 Attack.prototype.GetBestAttackAgainst = function(target, allowCapture)
     309Attack.prototype.GetBestAttackAgainst = function(target, prefAttackType)
    295310{
    296311    let cmpFormation = Engine.QueryInterface(target, IID_Formation);
    297312    if (cmpFormation)
     
    298313    {
    299314        // TODO: Formation against formation needs review
    300315        let types = this.GetAttackTypes();
    301         return ["Ranged", "Melee", "Capture"].find(attack => types.indexOf(attack) != -1);
     316        return ["Ranged", "Melee", "Capture"].find(attack => types.indexOf(attack) != -1
     317            && (!prefAttackType || "no" + attack != prefAttackType));
    302318    }
    303319
    304320    let cmpIdentity = Engine.QueryInterface(target, IID_Identity);
     
    305321    if (!cmpIdentity)
    306322        return undefined;
    307323
     324    let cmpPosition = Engine.QueryInterface(this.entity, IID_Position);
     325    if (!cmpPosition || !cmpPosition.IsInWorld())
     326        return undefined;
     327
     328    // If we are visisble garrisoned always do ranged attack if we can.
     329    if (cmpPosition.GetTurretParent() != INVALID_ENTITY)
     330        return this.template.Ranged && "Ranged";
     331
    308332    let targetClasses = cmpIdentity.GetClassesList();
    309333    let isTargetClass = className => targetClasses.indexOf(className) != -1;
    310334
     
    313337        return "Slaughter";
    314338
    315339    let attack = this;
     340    let isAllowed = function (type) { return !attack.GetRestrictedClasses(type).some(isTargetClass)
     341        && (!prefAttackType || "no" + type != prefAttackType); };
     342
    316343    let types = this.GetAttackTypes().filter(type => !attack.GetRestrictedClasses(type).some(isTargetClass));
     344    if (!types.length)
     345        return undefined;
    317346
    318     // check if the target is capturable
    319     let captureIndex = types.indexOf("Capture");
    320     if (captureIndex != -1)
     347    if (prefAttackType && prefAttackType != "Capture" && types.indexOf(prefAttackType) != -1)
     348        return prefAttackType;
     349    else if (!prefAttackType || prefAttackType == "Capture")
    321350    {
    322         let cmpCapturable = QueryMiragedInterface(target, IID_Capturable);
    323 
    324         let cmpPlayer = QueryOwnerInterface(this.entity);
    325         if (allowCapture && cmpPlayer && cmpCapturable && cmpCapturable.CanCapture(cmpPlayer.GetPlayerID()))
    326             return "Capture";
    327         // not captureable, so remove this attack
    328         types.splice(captureIndex, 1);
     351        // check if the target is capturable
     352        let captureIndex = types.indexOf("Capture");
     353        if (captureIndex != -1)
     354        {
     355            let cmpCapturable = QueryMiragedInterface(target, IID_Capturable);
     356            let cmpPlayer = QueryOwnerInterface(this.entity);
     357            if (cmpPlayer && cmpCapturable && cmpCapturable.CanCapture(cmpPlayer.GetPlayerID()))
     358                return "Capture";
     359            // not captureable, so remove this attack
     360            types.splice(captureIndex, 1);
     361        }
    329362    }
    330363
    331     let isPreferred = className => attack.GetPreferredClasses(className).some(isTargetClass);
     364    // ignore charges for now: TODO implement these
     365    let chargeIndex = types.indexOf("Charge");
     366    if (chargeIndex != -1)
     367        types.splice(chargeIndex, 1);
    332368
    333     return types.sort((a, b) =>
    334         (types.indexOf(a) + (isPreferred(a) ? types.length : 0)) -
    335         (types.indexOf(b) + (isPreferred(b) ? types.length : 0))).pop();
     369    if (!types.length)
     370        return undefined;
     371
     372    // assume ranged and/or melee attack left
     373    // TODO stop assuming that?
     374    let meleeIndex = types.indexOf("Melee");
     375    let rangedIndex = types.indexOf("Ranged");
     376    if (meleeIndex != -1 && rangedIndex != -1)
     377    {
     378        let selfPosition = cmpPosition.GetPosition2D();
     379        let cmpTargetPosition = Engine.QueryInterface(target, IID_Position);
     380        if (!cmpTargetPosition || !cmpTargetPosition.IsInWorld())
     381            return undefined;
     382        let targetPosition = cmpTargetPosition.GetPosition2D();
     383        let distanceToSquared = targetPosition.distanceToSquared(selfPosition);
     384        if (distanceToSquared <= Math.pow(this.GetRange("Ranged").min, 2))
     385            return "Melee";
     386
     387        let cmpDamageReceiver = Engine.QueryInterface(target, IID_DamageReceiver);
     388        if (!cmpDamageReceiver)
     389            return undefined;
     390
     391        let attackStrengthsMelee = this.GetAttackStrengths("Melee");
     392        let attackBonusMelee = this.GetAttackBonus("Melee", target);
     393        let DPSMelee = cmpDamageReceiver.GetDamage(
     394            attackStrengthsMelee.hack * attackBonusMelee,
     395            attackStrengthsMelee.pierce * attackBonusMelee,
     396            attackStrengthsMelee.crush * attackBonusMelee
     397            ) / this.GetTimers("Melee").repeat;
     398
     399        let attackStrengthsRanged = this.GetAttackStrengths("Ranged");
     400        let attackBonusRanged = this.GetAttackBonus("Ranged", target);
     401        let DPSRanged = cmpDamageReceiver.GetDamage(
     402            attackStrengthsRanged.hack * attackBonusRanged,
     403            attackStrengthsRanged.pierce * attackBonusRanged,
     404            attackStrengthsRanged.crush * attackBonusRanged
     405            ) / this.GetTimers("Ranged").repeat;
     406
     407        // When we are out of range do DPS only else take distance into account
     408        let maxRange = this.GetFullAttackRange().max;
     409        if (distanceToSquared > 1.2 * Math.pow(maxRange, 2))
     410        {
     411            if (DPSRanged > DPSMelee)
     412                return "Ranged";
     413            return "Melee";
     414        }
     415
     416        let strenghtRanged = DPSRanged * distanceToSquared / Math.pow(maxRange, 2);
     417        let strenghtMelee = DPSMelee * (1 - distanceToSquared / Math.pow(maxRange, 2));
     418
     419        if (strenghtRanged > strenghtMelee)
     420            return "Ranged";
     421        return "Melee";
     422    }
     423    else if (meleeIndex != -1)
     424        return "Melee";
     425    else if (rangedIndex != -1)
     426        return "Ranged";
     427    return types[0];
    336428};
    337429
    338430Attack.prototype.CompareEntitiesByPreference = function(a, b)
     
    496588
    497589        let horizDistance = targetPosition.horizDistanceTo(selfPosition);
    498590
    499         // This is an approximation of the time ot the target, it assumes that the target has a constant radial
     591        // This is an approximation of the time to the target, it assumes that the target has a constant radial
    500592        // velocity, but since units move in straight lines this is not true.  The exact value would be more
    501593        // difficult to calculate and I think this is sufficiently accurate.  (I tested and for cavalry it was
    502594        // about 5% of the units radius out in the worst case)
     
    572664            "target": target,
    573665            "attacker": this.entity,
    574666            "multiplier": this.GetAttackBonus(type, target),
    575             "type":type
     667            "type": type
    576668        });
    577669    }
    578670};
  • 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

     
    359359            "entities": cmpGarrisonHolder.GetEntities(),
    360360            "buffHeal": cmpGarrisonHolder.GetHealRate(),
    361361            "allowedClasses": cmpGarrisonHolder.GetAllowedClasses(),
     362            "neededAttackTypes": cmpGarrisonHolder.GetNeededAttackTypes(),
    362363            "capacity": cmpGarrisonHolder.GetCapacity(),
    363364            "garrisonedEntitiesCount": cmpGarrisonHolder.GetGarrisonedEntitiesCount()
    364365        };
     
    440441        let types = cmpAttack.GetAttackTypes();
    441442        if (types.length)
    442443            ret.attack = {};
     444
    443445        for (let type of types)
    444446        {
    445447            ret.attack[type] = cmpAttack.GetAttackStrengths(type);
     
    18291831    if (!cmpAttack)
    18301832        return false;
    18311833
    1832     let cmpEntityPlayer = QueryOwnerInterface(data.entity, IID_Player);
    1833     let cmpTargetPlayer = QueryOwnerInterface(data.target, IID_Player);
    1834     if (!cmpEntityPlayer || !cmpTargetPlayer)
    1835         return false;
    1836 
    1837     // if the owner is an enemy, it's up to the attack component to decide
    1838     if (cmpEntityPlayer.IsEnemy(cmpTargetPlayer.GetPlayerID()))
    1839         return cmpAttack.CanAttack(data.target);
    1840 
    1841     return false;
     1834    let attackType = data.type || undefined;
     1835    return cmpAttack.CanAttack(data.target, attackType);
    18421836};
    18431837
    18441838/*
  • 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)
     
    45724580 */
    45734581UnitAI.prototype.AttackVisibleEntity = function(ents, forceResponse)
    45744582{
    4575     var target = ents.find(target => this.CanAttack(target, forceResponse));
     4583    let target = ents.find(target => this.CanAttack(target, forceResponse));
    45764584    if (!target)
    45774585        return false;
    45784586
    4579     this.PushOrderFront("Attack", { "target": target, "force": false, "forceResponse": forceResponse, "allowCapture": true });
     4587    let prefAttackType = this.GetStance().respondStandGround && "Ranged";
     4588    this.PushOrderFront("Attack", { "target": target, "force": false, "forceResponse": forceResponse, "prefAttackType": prefAttackType });
    45804589    return true;
    45814590};
    45824591
     
    45894598{
    45904599    var target = ents.find(target =>
    45914600        this.CanAttack(target, forceResponse)
    4592         && this.CheckTargetDistanceFromHeldPosition(target, IID_Attack, this.GetBestAttackAgainst(target, true))
     4601        && this.CheckTargetDistanceFromHeldPosition(target, IID_Attack, this.GetBestAttackAgainst(target))
    45934602        && (this.GetStance().respondChaseBeyondVision || this.CheckTargetIsInVisionRange(target))
    45944603    );
    45954604    if (!target)
    45964605        return false;
    45974606
    4598     this.PushOrderFront("Attack", { "target": target, "force": false, "forceResponse": forceResponse, "allowCapture": true });
     4607    this.PushOrderFront("Attack", { "target": target, "force": false, "forceResponse": forceResponse, "prefAttackType": undefined });
    45994608    return true;
    46004609};
    46014610
     
    49864995 * to a player order, and so is forced.
    49874996 * If targetClasses is given, only entities matching the targetClasses can be attacked.
    49884997 */
    4989 UnitAI.prototype.WalkAndFight = function(x, z, targetClasses, queued)
     4998UnitAI.prototype.WalkAndFight = function(x, z, targetClasses, queued, prefAttackType)
    49904999{
    4991     this.AddOrder("WalkAndFight", { "x": x, "z": z, "targetClasses": targetClasses, "force": true }, queued);
     5000    this.AddOrder("WalkAndFight", { "x": x, "z": z, "targetClasses": targetClasses, "force": true, "prefAttackType": prefAttackType }, queued);
    49925001};
    49935002
    49945003/**
     
    50095018/**
    50105019 * Adds attack order to the queue, forced by the player.
    50115020 */
    5012 UnitAI.prototype.Attack = function(target, queued, allowCapture)
     5021UnitAI.prototype.Attack = function(target, queued, prefAttackType)
    50135022{
    50145023    if (!this.CanAttack(target))
    50155024    {
     
    50215030            this.WalkToTarget(target, queued);
    50225031        return;
    50235032    }
    5024     this.AddOrder("Attack", { "target": target, "force": true, "allowCapture": allowCapture}, queued);
     5033    this.AddOrder("Attack", { "target": target, "force": true, "prefAttackType": prefAttackType }, queued);
    50255034};
    50265035
    50275036/**
     
    54505459                    if (targetClasses.vetoEntities && targetClasses.vetoEntities[targ])
    54515460                        continue;
    54525461                }
    5453                 this.PushOrderFront("Attack", { "target": targ, "force": true, "allowCapture": true });
     5462                this.PushOrderFront("Attack", { "target": targ, "force": true, "prefAttackType": undefined });
    54545463                return true;
    54555464            }
    54565465        }
     
    54765485            if (targetClasses.vetoEntities && targetClasses.vetoEntities[targ])
    54775486                continue;
    54785487        }
    5479         this.PushOrderFront("Attack", { "target": targ, "force": true, "allowCapture": true });
     5488        this.PushOrderFront("Attack", { "target": targ, "force": true, "prefAttackType": undefined });
    54805489        return true;
    54815490    }
    54825491
     
    56765685    if (this.IsFormationController())
    56775686        return true;
    56785687
    5679     var cmpGarrisonHolder = Engine.QueryInterface(target, IID_GarrisonHolder);
     5688    let cmpGarrisonHolder = Engine.QueryInterface(target, IID_GarrisonHolder);
    56805689    if (!cmpGarrisonHolder)
    56815690        return false;
    56825691
    56835692    // Verify that the target is owned by this entity's player or a mutual ally of this player
    5684     var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership);
     5693    let cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership);
    56855694    if (!cmpOwnership || !(IsOwnedByPlayer(cmpOwnership.GetOwner(), target) || IsOwnedByMutualAllyOfPlayer(cmpOwnership.GetOwner(), target)))
    56865695        return false;
    56875696
     
    56915700    if (this.IsAnimal())
    56925701        return false;
    56935702
     5703    let cmpIdentity = Engine.QueryInterface(this.entity, IID_Identity)
     5704    if (!cmpIdentity)
     5705        return false;
     5706
     5707    let cmpAttack = Engine.QueryInterface(this.entity, IID_Attack)
     5708    let attackTypes = cmpAttack ? cmpAttack.GetAttackTypes() : [];
     5709    if (!MatchesClassList(cmpIdentity.GetClassesList(), cmpGarrisonHolder.GetAllowedClasses()) ||
     5710        !HasNeededAttackTypes(attackTypes, cmpGarrisonHolder.GetNeededAttackTypes()))
     5711        return false;
    56945712    return true;
    56955713};
    56965714
  • binaries/data/mods/public/simulation/components/tests/test_Attack.js

     
    2222        });
    2323
    2424        AddMock(playerEnt1, IID_Player, {
    25             "GetPlayerID": () => 1
     25            "GetPlayerID": () => 1,
     26            "IsEnemy": () => true
    2627        });
    2728    }
    2829
     
    3031
    3132    AddMock(attacker, IID_Position, {
    3233        "IsInWorld": () => true,
     34        "GetTurretParent": () => INVALID_ENTITY,
    3335        "GetHeightOffset": () => 5
    3436    });
    3537
     
    8789        "HasClass": className => className == defenderClass
    8890    });
    8991
     92    AddMock(defender, IID_Ownership, {
     93        "GetOwner": () => 1
     94    });
     95
    9096    AddMock(defender, IID_Position, {
    9197        "IsInWorld": () => true,
     98        "GetTurretParent": () => INVALID_ENTITY,
    9299        "GetHeightOffset": () => 0
    93100    });
    94101
     
    148155
    149156        TS_ASSERT_EQUALS(cmpAttack.CanAttack(defender), true);
    150157
    151         let allowCapturing = [true];
     158        let prefAttackType = "Capture";
    152159        if (!isBuilding)
    153             allowCapturing.push(false);
     160            prefAttackType = bestAttack;
    154161
    155         for (let allowCapturing of allowCapturing)
    156             TS_ASSERT_EQUALS(cmpAttack.GetBestAttackAgainst(defender, allowCapturing), bestAttack);
     162        TS_ASSERT_EQUALS(cmpAttack.GetBestAttackAgainst(defender, prefAttackType), bestAttack);
    157163    });
    158164}
    159165
  • 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

     
    160160    "attack-walk": function(player, cmd, data)
    161161    {
    162162        GetFormationUnitAIs(data.entities, player).forEach(cmpUnitAI => {
    163             cmpUnitAI.WalkAndFight(cmd.x, cmd.z, cmd.targetClasses, cmd.queued);
     163            cmpUnitAI.WalkAndFight(cmd.x, cmd.z, cmd.targetClasses, cmd.queued, cmd.prefAttackType);
    164164        });
    165165    },
    166166
     
    169169        if (g_DebugCommands && !(IsOwnedByEnemyOfPlayer(player, cmd.target) || IsOwnedByNeutralOfPlayer(player, cmd.target)))
    170170            warn("Invalid command: attack target is not owned by enemy of player "+player+": "+uneval(cmd));
    171171
    172         let allowCapture = cmd.allowCapture || cmd.allowCapture == null;
    173 
    174172        GetFormationUnitAIs(data.entities, player).forEach(cmpUnitAI => {
    175             cmpUnitAI.Attack(cmd.target, cmd.queued, allowCapture);
     173            cmpUnitAI.Attack(cmd.target, cmd.queued, cmd.prefAttackType);
    176174        });
    177175    },
    178176
  • 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/template_unit_infantry_ranged.xml

     
    66    <Crush>10</Crush>
    77  </Armour>
    88  <Attack>
     9    <Melee>
     10      <Hack>3</Hack>
     11      <Pierce>0</Pierce>
     12      <Crush>0</Crush>
     13      <MaxRange>4.0</MaxRange>
     14      <RepeatTime>1000</RepeatTime>
     15      <PreferredClasses datatype="tokens">Human</PreferredClasses>
     16    </Melee>
    917    <Ranged>
    1018      <Hack>0</Hack>
    1119      <Pierce>1.5</Pierce>
  • binaries/data/mods/public/simulation/templates/template_unit_infantry_ranged_archer.xml

     
    1010      <Pierce>6.0</Pierce>
    1111      <Crush>0</Crush>
    1212      <MaxRange>72.0</MaxRange>
    13       <MinRange>0.0</MinRange>
     13      <MinRange>10.0</MinRange>
    1414      <ProjectileSpeed>120.0</ProjectileSpeed>
    1515      <PrepareTime>1000</PrepareTime>
    1616      <RepeatTime>1000</RepeatTime>
  • 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    <Ranged>
     5      <Hack>0</Hack>
     6      <Pierce>6.0</Pierce>
     7      <Crush>0</Crush>
     8      <MaxRange>65.0</MaxRange>
     9      <MinRange>10.0</MinRange>
     10      <ProjectileSpeed>120.0</ProjectileSpeed>
     11      <PrepareTime>1000</PrepareTime>
     12      <RepeatTime>1000</RepeatTime>
     13      <Spread>3.0</Spread>
     14    </Ranged>
     15  </Attack>
    316  <Identity>
    417    <Civ>pers</Civ>
    518    <GenericName>Persian Immortal</GenericName>