Ticket #252: t252_secondattack_20.diff

File t252_secondattack_20.diff, 53.5 KB (added by bb, 8 years ago)
  • binaries/data/config/default.cfg

     
    278278[hotkey.session]
    279279kill = Delete                ; Destroy selected units
    280280stop = "H"                   ; Stop the current action
    281 attack = Ctrl                ; Modifier to attack instead of another action (eg capture)
    282 attackmove = Ctrl            ; Modifier to attackmove when clicking on a point
    283 attackmoveUnit = "Ctrl+Q"    ; Modifier to attackmove targeting only units when clicking on a point (should contain the attackmove keys)
     281attack = Ctrl                ; Modifier to primary attack instead of another action (eg capture)
     282attackmove = Ctrl            ; Modifier to primary attackmove when clicking on a point
     283attackmoveUnit = "Ctrl+Q"    ; Modifier to primary attackmove targeting only units when clicking on a point (should contain the attackmove keys)
    284284garrison = Ctrl              ; Modifier to garrison when clicking on building
    285285autorallypoint = Ctrl        ; Modifier to set the rally point on the building itself
     286meleeattack = Alt            ; Modifier to melee attack instead of another action
     287rangedattack = space         ; Modifier to ranged attack instead of another action
    286288guard = "G"                  ; Modifier to escort/guard when clicking on unit/building
    287289queue = Shift                ; Modifier to queue unit orders instead of replacing
    288290batchtrain = 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/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/session/input.js

     
    205205                data.targetClasses = Engine.HotkeyIsPressed("session.attackmoveUnit") ? { "attack": ["Unit"] } : { "attack": ["Unit", "Structure"] };
    206206                cursor = "action-attack-move";
    207207            }
     208
    208209            return { "possible": true, "data": data, "cursor": cursor };
    209210        }
    210211
     
    219220    // Check if the target entity is a resource, dropsite, foundation, or enemy unit.
    220221    // Check if any entities in the selection can gather the requested resource,
    221222    // can return to the dropsite, can build the foundation, or can attack the enemy
    222     for each (var entityID in selection)
     223    for (let entityID of selection)
    223224    {
    224         var entState = GetExtendedEntityState(entityID);
     225        let entState = GetExtendedEntityState(entityID);
    225226        if (!entState)
    226227            continue;
    227228
     
    228229        if (unitActions[action] && unitActions[action].getActionInfo)
    229230        {
    230231            var r = unitActions[action].getActionInfo(entState, targetState, simState);
    231             if (r) // return true if it's possible for one of the entities
     232            if (r && r.possible) // return true if it's possible for one of the entities
    232233                return r;
    233234        }
    234235    }
  • 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":
     
    355465                "target": target
    356466            };
    357467        },
    358         "specificness": 11,
     468        "specificness": 12,
    359469    },
    360470
    361471    "gather":
     
    376486
    377487            return true;
    378488        },
    379         "getActionInfo":  function(entState, targetState)
     489        "getActionInfo": function(entState, targetState)
    380490        {
    381491            if (!targetState.resourceSupply)
    382492                return false;
     
    595705            if (targetState.garrisonHolder.garrisonedEntitiesCount + extraCount >= targetState.garrisonHolder.capacity)
    596706                tooltip = "[color=\"orange\"]" + tooltip + "[/color]";
    597707
    598             if (!MatchesClassList(entState.identity.classes, targetState.garrisonHolder.allowedClasses))
    599                 return false;
     708            if (!MatchesClassList(entState.identity.classes, targetState.garrisonHolder.allowedClasses) ||
     709                !HasNeededAttackTypes(entState.attack, targetState.garrisonHolder.neededAttackTypes))
     710                return { "possible": false };
    600711
    601712            return {
    602713                "possible": true,
     
    9011012                "position": actionInfo.position
    9021013            };
    9031014        },
    904         "specificness": 6,
     1015        "specificness": 5,
    9051016    },
    9061017
    9071018    "unset-rallypoint":
     
    9831094            unloadAll();
    9841095        },
    9851096    },
     1097
    9861098    "delete": {
    9871099        "getInfo": function(entState)
    9881100        {
     
    10211133                openDeleteDialog(selection);
    10221134        },
    10231135    },
     1136
    10241137    "stop": {
    10251138        "getInfo": function(entState)
    10261139        {
  • 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(); },
     
    752753    },
    753754
    754755    moveToRange: function(x, z, min, max, queued = false) {
    755         Engine.PostCommand(PlayerID,{"type": "walk-to-range", "entities": [this.id()], "x": x, "z": z, "min": min, "max": max, "queued": queued });
     756        Engine.PostCommand(PlayerID,{"type": "walk-to-range", "entities": [this.id()], "x": x, "z": z, "min": min, "max": max, "queued": queued});
    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

     
    229229    return [];
    230230};
    231231
    232 Attack.prototype.CanAttack = function(target)
     232Attack.prototype.CanAttack = function(target, wantedType)
    233233{
    234234    let cmpFormation = Engine.QueryInterface(target, IID_Formation);
    235235    if (cmpFormation)
    236236        return true;
    237237
     238    if (wantedType && !this.template[wantedType])
     239        return false;
     240
    238241    let cmpThisPosition = Engine.QueryInterface(this.entity, IID_Position);
    239242    let cmpTargetPosition = Engine.QueryInterface(target, IID_Position);
    240243    if (!cmpThisPosition || !cmpTargetPosition || !cmpThisPosition.IsInWorld() || !cmpTargetPosition.IsInWorld())
    241244        return false;
    242245
    243     // Check if the relative height difference is larger than the attack range
    244     // If the relative height is bigger, it means they will never be able to
    245     // reach each other, no matter how close they come.
    246     let heightDiff = Math.abs(cmpThisPosition.GetHeightOffset() - cmpTargetPosition.GetHeightOffset());
    247 
    248246    const cmpIdentity = Engine.QueryInterface(target, IID_Identity);
    249247    if (!cmpIdentity)
    250248        return undefined;
    251249
     250    let cmpEntityPlayer = QueryOwnerInterface(this.entity);
     251    let cmpTargetPlayer = QueryOwnerInterface(target);
     252    if (!cmpTargetPlayer || !cmpEntityPlayer)
     253        return false;
     254
    252255    const targetClasses = cmpIdentity.GetClassesList();
     256    let cmpCapturable = QueryMiragedInterface(target, IID_Capturable);
     257    if (targetClasses.indexOf("Domestic") == -1 && !cmpEntityPlayer.IsEnemy(cmpTargetPlayer.GetPlayerID()) &&
     258       (!cmpCapturable || !cmpCapturable.CanCapture(cmpEntityPlayer.GetPlayerID())))
     259        return false;
    253260
     261    // Check if the relative height difference is larger than the attack range
     262    // If the relative height is bigger, it means they will never be able to
     263    // reach each other, no matter how close they come.
     264    let heightDiff = Math.abs(cmpThisPosition.GetHeightOffset() - cmpTargetPosition.GetHeightOffset());
     265
    254266    for (let type of this.GetAttackTypes())
    255267    {
    256         if (type == "Capture" && !QueryMiragedInterface(target, IID_Capturable))
     268        if (wantedType && type != wantedType)
    257269            continue;
    258270
     271        if (type == "Capture" && !cmpCapturable)
     272            continue;
     273
    259274        if (heightDiff > this.GetRange(type).max)
    260275            continue;
    261276
     
    312327    return ret;
    313328};
    314329
    315 Attack.prototype.GetBestAttackAgainst = function(target, allowCapture)
     330Attack.prototype.GetBestAttackAgainst = function(target, prefAttackType)
    316331{
    317332    let cmpFormation = Engine.QueryInterface(target, IID_Formation);
    318333    if (cmpFormation)
     
    319334    {
    320335        // TODO: Formation against formation needs review
    321336        let types = this.GetAttackTypes();
    322         return ["Ranged", "Melee", "Capture"].find(attack => types.indexOf(attack) != -1);
     337        return ["Ranged", "Melee", "Capture"].find(attack => types.indexOf(attack) != -1
     338            && (!prefAttackType || "no" + attack != prefAttackType));
    323339    }
    324340
    325341    let cmpIdentity = Engine.QueryInterface(target, IID_Identity);
     
    326342    if (!cmpIdentity)
    327343        return undefined;
    328344
     345    // If we are visisble garrisoned always do ranged attack if we can.
     346    let cmpPosition = Engine.QueryInterface(this.entity, IID_Position);
     347    if (cmpPosition && cmpPosition.GetTurretParent() != INVALID_ENTITY)
     348        return this.template.Ranged && "Ranged";
     349
    329350    let targetClasses = cmpIdentity.GetClassesList();
    330351    let isTargetClass = className => targetClasses.indexOf(className) != -1;
    331352
     
    334355        return "Slaughter";
    335356
    336357    let attack = this;
     358    let isAllowed = function (type) { return !attack.GetRestrictedClasses(type).some(isTargetClass)
     359        && (!prefAttackType || "no" + type != prefAttackType); };
     360
    337361    let types = this.GetAttackTypes().filter(type => !attack.GetRestrictedClasses(type).some(isTargetClass));
     362    if (!types.length)
     363        return undefined;
    338364
    339     // check if the target is capturable
    340     let captureIndex = types.indexOf("Capture");
    341     if (captureIndex != -1)
     365    if (prefAttackType && prefAttackType != "Capture" && types.indexOf(prefAttackType) != -1)
     366        return prefAttackType;
     367    else if (!prefAttackType || prefAttackType == "Capture")
    342368    {
    343         let cmpCapturable = QueryMiragedInterface(target, IID_Capturable);
    344 
    345         let cmpPlayer = QueryOwnerInterface(this.entity);
    346         if (allowCapture && cmpPlayer && cmpCapturable && cmpCapturable.CanCapture(cmpPlayer.GetPlayerID()))
    347             return "Capture";
    348         // not captureable, so remove this attack
    349         types.splice(captureIndex, 1);
     369        // check if the target is capturable
     370        let captureIndex = types.indexOf("Capture");
     371        if (captureIndex != -1)
     372        {
     373            let cmpCapturable = QueryMiragedInterface(target, IID_Capturable);
     374            let cmpPlayer = QueryOwnerInterface(this.entity);
     375            if (cmpPlayer && cmpCapturable && cmpCapturable.CanCapture(cmpPlayer.GetPlayerID()))
     376                return "Capture";
     377            // not captureable, so remove this attack
     378            types.splice(captureIndex, 1);
     379        }
    350380    }
    351381
    352     let isPreferred = className => attack.GetPreferredClasses(className).some(isTargetClass);
     382    // ignore charges for now: TODO implement these
     383    let chargeIndex = types.indexOf("Charge");
     384    if (chargeIndex != -1)
     385        types.splice(chargeIndex, 1);
    353386
    354     return types.sort((a, b) =>
    355         (types.indexOf(a) + (isPreferred(a) ? types.length : 0)) -
    356         (types.indexOf(b) + (isPreferred(b) ? types.length : 0))).pop();
     387    if (!types.length)
     388        return undefined;
     389
     390    // assume ranged and/or melee attack left
     391    // TODO stop assuming that?
     392    let meleeIndex = types.indexOf("Melee");
     393    let rangedIndex = types.indexOf("Ranged");
     394    if (meleeIndex != -1 && rangedIndex != -1)
     395    {
     396        if (!cmpPosition.IsInWorld())
     397            return undefined;
     398        let selfPosition = cmpPosition.GetPosition2D();
     399        let cmpTargetPosition = Engine.QueryInterface(target, IID_Position);
     400        if (!cmpTargetPosition || !cmpTargetPosition.IsInWorld())
     401            return undefined;
     402        let targetPosition = cmpTargetPosition.GetPosition2D();
     403        let distanceToSquared = targetPosition.distanceToSquared(selfPosition);
     404        if (distanceToSquared <= Math.pow(this.GetRange("Ranged").min, 2))
     405            return "Melee";
     406
     407        let cmpDamageReceiver = Engine.QueryInterface(target, IID_DamageReceiver);
     408        if (!cmpDamageReceiver)
     409            return undefined;
     410
     411        let attackStrengthsMelee = this.GetAttackStrengths("Melee");
     412        let attackBonusMelee = this.GetAttackBonus("Melee", target);
     413        let DPSMelee = cmpDamageReceiver.GetDamage(
     414            attackStrengthsMelee.hack * attackBonusMelee,
     415            attackStrengthsMelee.pierce * attackBonusMelee,
     416            attackStrengthsMelee.crush * attackBonusMelee
     417            ) / this.GetTimers("Melee").repeat;
     418
     419        let attackStrengthsRanged = this.GetAttackStrengths("Ranged");
     420        let attackBonusRanged = this.GetAttackBonus("Ranged", target);
     421        let DPSRanged = cmpDamageReceiver.GetDamage(
     422            attackStrengthsRanged.hack * attackBonusRanged,
     423            attackStrengthsRanged.pierce * attackBonusRanged,
     424            attackStrengthsRanged.crush * attackBonusRanged
     425            ) / this.GetTimers("Ranged").repeat;
     426
     427        // When we are out of range do DPS only else take distance into account
     428        let maxRange = this.GetFullAttackRange().max;
     429        if (distanceToSquared > 1.2 * Math.pow(maxRange, 2))
     430        {
     431            if (DPSRanged > DPSMelee)
     432                return "Ranged";
     433            return "Melee";
     434        }
     435
     436        let strenghtRanged = DPSRanged * distanceToSquared / Math.pow(maxRange, 2);
     437        let strenghtMelee = DPSMelee * (1 - distanceToSquared / Math.pow(maxRange, 2));
     438
     439        if (strenghtRanged > strenghtMelee)
     440            return "Ranged";
     441        return "Melee";
     442    }
     443    else if (meleeIndex != -1)
     444        return "Melee";
     445    else if (rangedIndex != -1)
     446        return "Ranged";
     447    return types[0];
    357448};
    358449
    359450Attack.prototype.CompareEntitiesByPreference = function(a, b)
     
    517608
    518609        let horizDistance = targetPosition.horizDistanceTo(selfPosition);
    519610
    520         // This is an approximation of the time ot the target, it assumes that the target has a constant radial
     611        // This is an approximation of the time to the target, it assumes that the target has a constant radial
    521612        // velocity, but since units move in straight lines this is not true.  The exact value would be more
    522613        // difficult to calculate and I think this is sufficiently accurate.  (I tested and for cavalry it was
    523614        // about 5% of the units radius out in the worst case)
     
    593684            "target": target,
    594685            "attacker": this.entity,
    595686            "multiplier": this.GetAttackBonus(type, target),
    596             "type":type
     687            "type": type
    597688        });
    598689    }
    599690    // TODO: charge attacks (need to design how they work)
  • 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

     
    357357        ret.garrisonHolder = {
    358358            "entities": cmpGarrisonHolder.GetEntities(),
    359359            "allowedClasses": cmpGarrisonHolder.GetAllowedClasses(),
     360            "neededAttackTypes": cmpGarrisonHolder.GetNeededAttackTypes(),
    360361            "capacity": cmpGarrisonHolder.GetCapacity(),
    361362            "garrisonedEntitiesCount": cmpGarrisonHolder.GetGarrisonedEntitiesCount()
    362363        };
     
    438439        let types = cmpAttack.GetAttackTypes();
    439440        if (types.length)
    440441            ret.attack = {};
     442
    441443        for (let type of types)
    442444        {
    443445            ret.attack[type] = cmpAttack.GetAttackStrengths(type);
     
    18271829    if (!cmpAttack)
    18281830        return false;
    18291831
    1830     let cmpEntityPlayer = QueryOwnerInterface(data.entity, IID_Player);
    1831     let cmpTargetPlayer = QueryOwnerInterface(data.target, IID_Player);
    1832     if (!cmpEntityPlayer || !cmpTargetPlayer)
    1833         return false;
    1834 
    1835     // if the owner is an enemy, it's up to the attack component to decide
    1836     if (cmpEntityPlayer.IsEnemy(cmpTargetPlayer.GetPlayerID()))
    1837         return cmpAttack.CanAttack(data.target);
    1838 
    1839     return false;
     1832    let attackType = data.type || undefined;
     1833    return cmpAttack.CanAttack(data.target, attackType);
    18401834};
    18411835
    18421836/*
  • 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_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>