Ticket #252: t252_secondattack_17.diff
File t252_secondattack_17.diff, 56.5 KB (added by , 8 years ago) |
---|
-
binaries/data/config/default.cfg
278 278 [hotkey.session] 279 279 kill = Delete ; Destroy selected units 280 280 stop = "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 point283 attackmoveUnit = "Ctrl+Q" ; Modifier to attackmove targeting only units when clicking on a point (should contain the attackmove keys)281 attack = Ctrl ; Modifier to primary attack instead of another action (eg capture) 282 attackmove = Ctrl ; Modifier to primary attackmove when clicking on a point 283 attackmoveUnit = "Ctrl+Q" ; Modifier to primary attackmove targeting only units when clicking on a point (should contain the attackmove keys) 284 284 garrison = Ctrl ; Modifier to garrison when clicking on building 285 285 autorallypoint = Ctrl ; Modifier to set the rally point on the building itself 286 meleeattack = Alt ; Modifier to melee attack instead of another action 287 meleeattackmove = Alt ; Modifier to melee attackmove when clicking on a point 288 meleeattackmoveUnit = "Alt+Q"; Modifier to melee attackmove targeting only units when clicking on a point 289 rangedattack = space ; Modifier to ranged attack instead of another action 290 rangedattackmove = space ; Modifier to ranged attackmove when clicking on a point 291 rangedattackmoveUnit = "space+Q"; Modifier to ranged attackmove targeting only units when clicking on a point 286 292 guard = "G" ; Modifier to escort/guard when clicking on unit/building 287 293 queue = Shift ; Modifier to queue unit orders instead of replacing 288 294 batchtrain = Shift ; Modifier to train units in batches -
binaries/data/mods/public/art/actors/units/persians/champion_unit_1.xml
42 42 <group> 43 43 <variant frequency="1" name="Idle"/> 44 44 <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> 45 51 </group> 46 52 <material>player_trans.xml</material> 47 53 </actor> -
binaries/data/mods/public/art/textures/cursors/action-melee-attack.txt
1 1 1 -
binaries/data/mods/public/art/textures/cursors/action-ranged-attack.txt
1 1 1 -
binaries/data/mods/public/globalscripts/Templates.js
66 66 } 67 67 68 68 /** 69 * Check if entity has all needed attack types. 70 */ 71 function 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 /** 69 87 * Get information about a template with or without technology modifications. 70 88 * @param template A valid template as returned by the template loader. 71 89 * @param player An optional player id to get the technology modifications -
binaries/data/mods/public/gui/common/tooltips.js
166 166 167 167 for (let type in template.attack) 168 168 { 169 if (type == "ChangeDistance") 170 continue; // not an attack type 169 171 if (type == "Slaughter") 170 172 continue; // Slaughter is not a real attack, so do not show it. 171 173 if (type == "Charge") -
binaries/data/mods/public/gui/session/input.js
205 205 data.targetClasses = Engine.HotkeyIsPressed("session.attackmoveUnit") ? { "attack": ["Unit"] } : { "attack": ["Unit", "Structure"] }; 206 206 cursor = "action-attack-move"; 207 207 } 208 209 if (Engine.HotkeyIsPressed("session.meleeattackmove")) 210 { 211 data.command = "attack-walk"; 212 data.targetClasses = Engine.HotkeyIsPressed("session.meleeattackmoveUnit") ? { "attack": ["Unit"] } : { "attack": ["Unit", "Structure"] }; 213 cursor = "action-melee-attack-move"; 214 } 215 216 if (Engine.HotkeyIsPressed("session.rangedattackmove")) 217 { 218 data.command = "attack-walk"; 219 data.targetClasses = Engine.HotkeyIsPressed("session.rangedattackmoveUnit") ? { "attack": ["Unit"] } : { "attack": ["Unit", "Structure"] }; 220 cursor = "action-ranged-attack-move"; 221 } 222 208 223 return { "possible": true, "data": data, "cursor": cursor }; 209 224 } 210 225 … … 219 234 // Check if the target entity is a resource, dropsite, foundation, or enemy unit. 220 235 // Check if any entities in the selection can gather the requested resource, 221 236 // can return to the dropsite, can build the foundation, or can attack the enemy 222 for each (var entityID inselection)237 for (let entityID of selection) 223 238 { 224 varentState = GetExtendedEntityState(entityID);239 let entState = GetExtendedEntityState(entityID); 225 240 if (!entState) 226 241 continue; 227 242 … … 228 243 if (unitActions[action] && unitActions[action].getActionInfo) 229 244 { 230 245 var r = unitActions[action].getActionInfo(entState, targetState, simState); 231 if (r ) // return true if it's possible for one of the entities246 if (r && r.possible) // return true if it's possible for one of the entities 232 247 return r; 233 248 } 234 249 } -
binaries/data/mods/public/gui/session/unit_actions.js
58 58 59 59 return { "type": "move" }; 60 60 }, 61 "specificness": 1 2,61 "specificness": 13, 62 62 }, 63 63 64 64 "attack-move": … … 77 77 "x": target.x, 78 78 "z": target.z, 79 79 "targetClasses": targetClasses, 80 "queued": queued 80 "queued": queued, 81 "prefAttackType": "primary" 81 82 }); 82 83 83 84 Engine.GuiInterfaceCall("PlaySound", { … … 106 107 "specificness": 30, 107 108 }, 108 109 110 111 "melee-attack-move": 112 { 113 "execute": function(target, action, selection, queued) 114 { 115 if (Engine.HotkeyIsPressed("session.meleeattackmoveUnit")) 116 var targetClasses = { "meleeattack": ["Unit"] }; 117 else 118 var targetClasses = { "meleeattack": ["Unit", "Structure"] }; 119 120 Engine.PostNetworkCommand({ 121 "type": "attack-walk", 122 "entities": selection, 123 "x": target.x, 124 "z": target.z, 125 "targetClasses": targetClasses, 126 "queued": queued, 127 "prefAttackType": "Melee" 128 }); 129 130 Engine.GuiInterfaceCall("PlaySound", { 131 "name": "order_walk", 132 "entity": selection[0] 133 }); 134 135 return true; 136 }, 137 "getActionInfo": function(entState, targetState) 138 { 139 return { "possible": true }; 140 }, 141 "hotkeyActionCheck": function(target, selection) 142 { 143 // Work out whether at least part of the selection have UnitAI 144 let haveUnitAI = selection.some(function(ent) { 145 let entState = GetEntityState(ent); 146 return entState && entState.unitAI && getActionInfo("melee-attack-move", target).possible; 147 }); 148 149 if (haveUnitAI && Engine.HotkeyIsPressed("session.meleeattackmove") && 150 getActionInfo("melee-attack-move", target).possible) 151 return { 152 "type": "melee-attack-move", 153 "cursor": "action-melee-attack" 154 }; 155 return false; 156 }, 157 "specificness": 31, 158 }, 159 160 "ranged-attack-move": 161 { 162 "execute": function(target, action, selection, queued) 163 { 164 if (Engine.HotkeyIsPressed("session.rangedattackmoveUnit")) 165 var targetClasses = { "rangedattack": ["Unit"] }; 166 else 167 var targetClasses = { "rangedattack": ["Unit", "Structure"] }; 168 169 Engine.PostNetworkCommand({ 170 "type": "attack-walk", 171 "entities": selection, 172 "x": target.x, 173 "z": target.z, 174 "targetClasses": targetClasses, 175 "queued": queued, 176 "prefAttackType": "Ranged" 177 }); 178 179 Engine.GuiInterfaceCall("PlaySound", { 180 "name": "order_walk", 181 "entity": selection[0] 182 }); 183 184 return true; 185 }, 186 "getActionInfo": function(entState, targetState) 187 { 188 return { "possible": true }; 189 }, 190 "hotkeyActionCheck": function(target, selection) 191 { 192 // Work out whether at least part of the selection have UnitAI 193 let haveUnitAI = selection.some(function(ent) { 194 let entState = GetEntityState(ent); 195 return entState && entState.unitAI && getActionInfo("ranged-attack-move", target).possible; 196 }); 197 198 if (haveUnitAI && Engine.HotkeyIsPressed("session.rangedattackmove") && 199 getActionInfo("ranged-attack-move", target).possible) 200 return { 201 "type": "ranged-attack-move", 202 "cursor": "action-ranged-attack" 203 }; 204 return false; 205 }, 206 "specificness": 32, 207 }, 208 109 209 "capture": 110 210 { 111 211 "execute": function(target, action, selection, queued) … … 115 215 "entities": selection, 116 216 "target": action.target, 117 217 "allowCapture": true, 118 "queued": queued 218 "queued": queued, 219 "prefAttackType": "Capture" 119 220 }); 120 221 121 222 Engine.GuiInterfaceCall("PlaySound", { … … 148 249 "target": target 149 250 }; 150 251 }, 151 "specificness": 9,252 "specificness": 7, 152 253 }, 153 254 154 255 "attack": … … 160 261 "entities": selection, 161 262 "target": action.target, 162 263 "queued": queued, 163 "allowCapture": false 264 "allowCapture": false, 265 "prefAttackType": "primary" 164 266 }); 165 267 166 268 Engine.GuiInterfaceCall("PlaySound", { … … 205 307 "target": target 206 308 }; 207 309 }, 310 "specificness": 8, 311 }, 312 313 "melee-attack": 314 { 315 "execute": function(target, action, selection, queued) 316 { 317 Engine.PostNetworkCommand({ 318 "type": "attack", 319 "entities": selection, 320 "target": action.target, 321 "queued": queued, 322 "prefAttackType": "Melee" 323 }); 324 325 Engine.GuiInterfaceCall("PlaySound", { 326 "name": "order_attack", 327 "entity": selection[0] 328 }); 329 return true; 330 }, 331 "getActionInfo": function(entState, targetState) 332 { 333 if (!entState.attack || !targetState.hitpoints) 334 return false; 335 return { "possible": Engine.GuiInterfaceCall("CanAttackWithType", { 336 "entity": entState.id, 337 "target": targetState.id, 338 "type": "Melee" 339 }) }; 340 }, 341 "hotkeyActionCheck": function(target) 342 { 343 if (Engine.HotkeyIsPressed("session.meleeattack") && getActionInfo("melee-attack", target).possible) 344 return { 345 "type": "melee-attack", 346 "cursor": "action-melee-attack", 347 "target": target 348 }; 349 return false; 350 }, 351 "actionCheck": function(target) 352 { 353 if (getActionInfo("melee-attack", target).possible) 354 return { 355 "type": "melee-attack", 356 "cursor": "action-melee-attack", 357 "target": target 358 }; 359 return false; 360 }, 361 "specificness": 9, 362 }, 363 364 "ranged-attack": 365 { 366 "execute": function(target, action, selection, queued) 367 { 368 Engine.PostNetworkCommand({ 369 "type": "attack", 370 "entities": selection, 371 "target": action.target, 372 "queued": queued, 373 "prefAttackType": "Ranged" 374 }); 375 376 Engine.GuiInterfaceCall("PlaySound", { 377 "name": "order_attack", 378 "entity": selection[0] 379 }); 380 381 return true; 382 }, 383 "getActionInfo": function(entState, targetState) 384 { 385 if (!entState.attack || !targetState.hitpoints) // hack 386 return false; 387 return { "possible": Engine.GuiInterfaceCall("CanAttackWithType", { 388 "entity": entState.id, 389 "target": targetState.id, 390 "type": "Ranged" 391 }) }; 392 }, 393 "hotkeyActionCheck": function(target, selection) 394 { 395 if (Engine.HotkeyIsPressed("session.rangedattack") && getActionInfo("ranged-attack", target).possible) 396 return { 397 "type": "ranged-attack", 398 "cursor": "action-ranged-attack", 399 "target": target 400 }; 401 return false; 402 }, 403 "actionCheck": function(target, selection) 404 { 405 if (getActionInfo("ranged-attack", target).possible) 406 return { 407 "type": "ranged-attack", 408 "cursor": "action-ranged-attack", 409 "target": target 410 }; 411 return false; 412 }, 208 413 "specificness": 10, 209 414 }, 210 415 … … 255 460 "target": target 256 461 }; 257 462 }, 258 "specificness": 7,463 "specificness": 6, 259 464 }, 260 465 261 466 "build": … … 355 560 "target": target 356 561 }; 357 562 }, 358 "specificness": 1 1,563 "specificness": 12, 359 564 }, 360 565 361 566 "gather": … … 595 800 if (targetState.garrisonHolder.garrisonedEntitiesCount + extraCount >= targetState.garrisonHolder.capacity) 596 801 tooltip = "[color=\"orange\"]" + tooltip + "[/color]"; 597 802 598 if (!MatchesClassList(entState.identity.classes, targetState.garrisonHolder.allowedClasses))599 return false;600 803 601 return { 602 "possible": true, 603 "tooltip": tooltip 604 }; 804 if (MatchesClassList(entState.identity.classes, targetState.garrisonHolder.allowedClasses) && 805 HasNeededAttackTypes(entState.attack, targetState.garrisonHolder.neededAttackTypes)) 806 return { 807 "possible": true, 808 "tooltip": tooltip 809 }; 810 811 return { "possible": false }; 605 812 }, 606 813 "preSelectedActionCheck": function(target) 607 814 { … … 783 990 cursor = "action-attack-move"; 784 991 } 785 992 993 if (Engine.HotkeyIsPressed("session.meleeattackmove")) 994 { 995 if (Engine.HotkeyIsPressed("session.meleeattackmoveUnit")) 996 var targetClasses = { "melee-attack": ["Unit"] }; 997 else 998 var targetClasses = { "melee-attack": ["Unit", "Structure"] }; 999 data.command = "melee-attack-walk"; 1000 data.targetClasses = targetClasses; 1001 cursor = "action-melee-attack"; 1002 } 1003 1004 if (Engine.HotkeyIsPressed("session.rangedattackmove")) 1005 { 1006 if (Engine.HotkeyIsPressed("session.rangedattackmoveUnit")) 1007 var targetClasses = { "ranged-attack": ["Unit"] }; 1008 else 1009 var targetClasses = { "ranged-attack": ["Unit", "Structure"] }; 1010 data.command = "ranged-attack-walk"; 1011 data.targetClasses = targetClasses; 1012 cursor = "action-ranged-attack"; 1013 } 1014 786 1015 if (targetState.garrisonHolder && 787 1016 playerCheck(entState, targetState, ["Player", "MutualAlly"])) 788 1017 { … … 901 1130 "position": actionInfo.position 902 1131 }; 903 1132 }, 904 "specificness": 6,1133 "specificness": 5, 905 1134 }, 906 1135 907 1136 "unset-rallypoint": -
binaries/data/mods/public/simulation/ai/common-api/entity.js
744 744 return false; 745 745 }, 746 746 747 isCapturable: function() { return this.get("Capturable") !== undefined; }, 747 748 isGarrisonHolder: function() { return this.get("GarrisonHolder") !== undefined; }, 748 749 garrisoned: function() { return this._entity.garrisoned; }, 749 750 canGarrisonInside: function() { return this._entity.garrisoned.length < this.garrisonMax(); }, … … 754 755 }, 755 756 756 757 moveToRange: function(x, z, min, max, queued = false) { 757 Engine.PostCommand(PlayerID,{ "type": "walk-to-range", "entities": [this.id()], "x": x, "z": z, "min": min, "max": max, "queued": queued });758 Engine.PostCommand(PlayerID,{ "type": "walk-to-range", "entities": [this.id()], "x": x, "z": z, "min": min, "max": max, "queued": queued }); 758 759 return this; 759 760 }, 760 761 761 attackMove: function(x, z, targetClasses, queued = false) {762 Engine.PostCommand(PlayerID,{"type": "attack-walk", "entities": [this.id()], "x": x, "z": z, "targetClasses": targetClasses, " queued": queued});762 attackMove: function(x, z, targetClasses, prefAttackType = "undefined", queued = false) { // hack 763 Engine.PostCommand(PlayerID,{"type": "attack-walk", "entities": [this.id()], "x": x, "z": z, "targetClasses": targetClasses, "prefAttackType": prefAttackType, "queued": queued}); 763 764 return this; 764 765 }, 765 766 … … 793 794 return this; 794 795 }, 795 796 796 attack: function(unitId, allowCapture = true, queued = false) {797 Engine.PostCommand(PlayerID,{"type": "attack", "entities": [this.id()], "target": unitId, " allowCapture": allowCapture, "queued": queued});797 attack: function(unitId, prefAttackType = undefined, queued = false) { 798 Engine.PostCommand(PlayerID,{"type": "attack", "entities": [this.id()], "target": unitId, "prefAttackType": prefAttackType, "queued": queued}); 798 799 return this; 799 800 }, 800 801 -
binaries/data/mods/public/simulation/ai/common-api/entitycollection.js
156 156 m.EntityCollection.prototype.attackMove = function(x, z, targetClasses, queued) 157 157 { 158 158 queued = queued || false; 159 Engine.PostCommand(PlayerID, {"type": "attack-walk", "entities": this.toIdArray(), "x": x, "z": z, "targetClasses": targetClasses, "queued": queued});159 Engine.PostCommand(PlayerID, { "type": "attack-walk", "entities": this.toIdArray(), "x": x, "z": z, "targetClasses": targetClasses, "queued": queued, "prefAttackType": undefined }); // hack 160 160 return this; 161 161 }; 162 162 … … 182 182 183 183 m.EntityCollection.prototype.attack = function(unitId) 184 184 { 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}); 186 186 return this; 187 187 }; 188 188 -
binaries/data/mods/public/simulation/ai/petra/attackPlan.js
1236 1236 { 1237 1237 if (this.isSiegeUnit(gameState, ent)) // needed as mauryan elephants are not filtered out 1238 1238 continue; 1239 ent.attack(attacker.id(), !this.noCapture.has(attacker.id()));1239 ent.attack(attacker.id(), m.getPrefAttackType(ent, attacker, this.noCapture)); 1240 1240 ent.setMetadata(PlayerID, "lastAttackPlanUpdateTime", time); 1241 1241 } 1242 1242 // And if this attacker is a non-ranged siege unit and our unit also, attack it 1243 1243 if (this.isSiegeUnit(gameState, attacker) && attacker.hasClass("Melee") && ourUnit.hasClass("Melee")) 1244 1244 { 1245 ourUnit.attack(attacker.id(), false);1245 ourUnit.attack(attacker.id(), "Melee"); 1246 1246 ourUnit.setMetadata(PlayerID, "lastAttackPlanUpdateTime", time); 1247 1247 } 1248 1248 } … … 1259 1259 let collec = this.unitCollection.filter(API3.Filters.byClass("Melee")).filterNearest(ourUnit.position(), 5); 1260 1260 for (let ent of collec.values()) 1261 1261 { 1262 ent.attack(attacker.id(), false);1262 ent.attack(attacker.id(), m.getPrefAttackType(ent, attacker, this.noCapture)); 1263 1263 ent.setMetadata(PlayerID, "lastAttackPlanUpdateTime", time); 1264 1264 } 1265 1265 } … … 1278 1278 continue; 1279 1279 } 1280 1280 } 1281 ourUnit.attack(attacker.id(), !this.noCapture.has(attacker.id()));1281 ourUnit.attack(attacker.id(), m.getPrefAttackType(ourUnit, attacker, this.noCapture)); 1282 1282 ourUnit.setMetadata(PlayerID, "lastAttackPlanUpdateTime", time); 1283 1283 } 1284 1284 } … … 1458 1458 return valb - vala; 1459 1459 }); 1460 1460 if (mStruct[0].hasClass("Gates")) 1461 ent.attack(mStruct[0].id(), false);1461 ent.attack(mStruct[0].id(), m.getPrefAttackType(ent, mStruct[0], this.noCapture)); 1462 1462 else 1463 1463 { 1464 1464 let rand = Math.floor(Math.random() * mStruct.length * 0.2); 1465 let newTarget Id = mStruct[rand].id();1466 ent.attack(newTarget Id, !this.noCapture.has(newTargetId));1465 let newTarget = mStruct[rand]; 1466 ent.attack(newTarget.id(), m.getPrefAttackType(ent, newTarget, this.noCapture)); 1467 1467 } 1468 1468 } 1469 1469 else … … 1521 1521 return valb - vala; 1522 1522 }); 1523 1523 let rand = Math.floor(Math.random() * mUnit.length * 0.1); 1524 let newTarget Id = mUnit[rand].id();1525 ent.attack(newTarget Id, !this.noCapture.has(newTargetId));1524 let newTarget = mUnit[rand]; 1525 ent.attack(newTarget.id(), m.getPrefAttackType(ent, newTarget, this.noCapture)); 1526 1526 } 1527 1527 else if (this.isBlocked) 1528 1528 ent.attack(this.target.id(), false); … … 1569 1569 return valb - vala; 1570 1570 }); 1571 1571 if (mStruct[0].hasClass("Gates")) 1572 ent.attack(mStruct[0].id(), false);1572 ent.attack(mStruct[0].id(), m.getPrefAttackType(ent, mStruct[0], this.noCapture)); 1573 1573 else 1574 1574 { 1575 1575 let rand = Math.floor(Math.random() * mStruct.length * 0.2); 1576 let newTarget Id = mStruct[rand].id();1577 ent.attack(newTarget Id, !this.noCapture.has(newTargetId));1576 let newTarget = mStruct[rand]; 1577 ent.attack(newTarget.id(), m.getPrefAttackType(ent, newTarget, this.noCapture)); 1578 1578 } 1579 1579 } 1580 1580 else if (needsUpdate) // really nothing let's try to help our nearest unit … … 1581 1581 { 1582 1582 let distmin = Math.min(); 1583 1583 let attackerId; 1584 let attacker; 1584 1585 this.unitCollection.forEach( function (unit) { 1585 1586 if (!unit.position()) 1586 1587 return; … … 1590 1591 let dist = API3.SquareVectorDistance(unit.position(), ent.position()); 1591 1592 if (dist > distmin) 1592 1593 return; 1594 if (!gameState.getEntityById(unit.unitAIOrderData()[0].target)) 1595 return; 1593 1596 distmin = dist; 1594 1597 attackerId = unit.unitAIOrderData()[0].target; 1595 1598 attacker = gameState.getEntityById(attackerId); 1596 1599 }); 1597 1600 if (attackerId) 1598 ent.attack(attackerId, !this.noCapture.has(attackerId));1601 ent.attack(attackerId, m.getPrefAttackType(ent, attacker, this.noCapture)); 1599 1602 } 1600 1603 } 1601 1604 } … … 1648 1651 continue; 1649 1652 if (!ent.isIdle()) 1650 1653 continue; 1651 ent.attack(attacker.id(), !this.noCapture.has(attacker.id() ));1654 ent.attack(attacker.id(), !this.noCapture.has(attacker.id(), this.noCapture)); 1652 1655 } 1653 1656 break; 1654 1657 } … … 2004 2007 2005 2008 if (this.noCapture.has(targetId)) 2006 2009 { 2007 ent.attack(targetId, false);2010 ent.attack(targetId, m.getPrefAttackType(ent, target, this.noCapture)); 2008 2011 return true; 2009 2012 } 2010 2013 … … 2016 2019 if (target.hasClass("Siege") && target.hasClass("Melee")) 2017 2020 { 2018 2021 this.noCapture.add(targetId); 2019 ent.attack(targetId, false);2022 ent.attack(targetId, m.getPrefAttackType(ent, target, this.noCapture)); 2020 2023 return true; 2021 2024 } 2022 2025 … … 2033 2036 if (antiCapture >= this.captureStrength) 2034 2037 { 2035 2038 this.noCapture.add(targetId); 2036 ent.attack(targetId, false);2039 ent.attack(targetId, "primary"); // hack 2037 2040 return true; 2038 2041 } 2039 2042 … … 2042 2045 this.unitCollection.length < 2*target.garrisoned().length) 2043 2046 { 2044 2047 this.noCapture.add(targetId); 2045 ent.attack(targetId, false);2048 ent.attack(targetId, "primary"); // hack 2046 2049 return true; 2047 2050 } 2048 2051 -
binaries/data/mods/public/simulation/ai/petra/defenseArmy.js
77 77 { 78 78 this.assignedTo[entID] = idFoe; 79 79 this.assignedAgainst[idFoe].push(entID); 80 ent.attack(idFoe, m. allowCapture(ent, foeEnt), queued);80 ent.attack(idFoe, m.getPrefAttackType(ent, foeEnt), queued); 81 81 } 82 82 else 83 83 gameState.ai.HQ.navalManager.requireTransport(gameState, ent, ownIndex, foeIndex, foePosition); … … 115 115 else if (orderData.length && orderData[0].target && orderData[0].attackType && orderData[0].attackType === "Capture") 116 116 { 117 117 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 } 120 124 } 121 125 } 122 126 -
binaries/data/mods/public/simulation/ai/petra/entityExtend.js
81 81 return strength * ent.maxHitpoints() / 100.0; 82 82 }; 83 83 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 86 m.getPrefAttackType = function(ent, target, noCapture) 86 87 { 87 return !target.hasClass("Siege") || !ent.hasClass("Melee") || 88 !target.isGarrisonHolder() || !target.garrisoned().length; 88 if (target.hasClass("Siege") || (noCapture && noCapture.has(target.id()))) 89 return ent.hasClass("Melee") ? "Melee" : "Range"; // Don't capture for now 90 91 if (target.isCapturable() && (!target.isGarrisonHolder() || !target.garrisoned().length)) 92 return "Capture"; 93 return "primary"; 89 94 }; 90 95 91 96 /** Makes the worker deposit the currently carried resources at the closest accessible dropsite */ -
binaries/data/mods/public/simulation/components/Attack.js
41 41 Attack.prototype.Schema = 42 42 "<a:help>Controls the attack abilities and strengths of the unit.</a:help>" + 43 43 "<a:example>" + 44 "<ChangeDistance>20</ChangeDistance>" + 44 45 "<Melee>" + 46 "<AttackOrder>primary</AttackOrder>" + 45 47 "<Hack>10.0</Hack>" + 46 48 "<Pierce>0.0</Pierce>" + 47 49 "<Crush>5.0</Crush>" + … … 62 64 "<PreferredClasses datatype=\"tokens\">Cavalry Infantry</PreferredClasses>" + 63 65 "</Melee>" + 64 66 "<Ranged>" + 67 "<AttackOrder>secondary</AttackOrder>" + 65 68 "<Hack>0.0</Hack>" + 66 69 "<Pierce>10.0</Pierce>" + 67 70 "<Crush>0.0</Crush>" + … … 103 106 "</Slaughter>" + 104 107 "</a:example>" + 105 108 "<optional>" + 109 "<element name='ChangeDistance' a:help='Distance to change between Melee and Ranged attack'>" + 110 "<ref name='nonNegativeDecimal'/>" + 111 "</element>" + 112 "</optional>" + 113 "<optional>" + 106 114 "<element name='Melee'>" + 115 "<optional>" + 116 "<element name='AttackOrder'>" + 117 "<choice>" + 118 "<value>primary</value>" + 119 "<value>secondary</value>" + 120 "</choice>" + 121 "</element>" + 122 "</optional>" + 107 123 "<interleave>" + 108 124 "<element name='Hack' a:help='Hack damage strength'><ref name='nonNegativeDecimal'/></element>" + 109 125 "<element name='Pierce' a:help='Pierce damage strength'><ref name='nonNegativeDecimal'/></element>" + … … 120 136 "</optional>" + 121 137 "<optional>" + 122 138 "<element name='Ranged'>" + 139 "<optional>" + 140 "<element name='AttackOrder'>" + 141 "<choice>" + 142 "<value>primary</value>" + 143 "<value>secondary</value>" + 144 "</choice>" + 145 "</element>" + 146 "</optional>" + 123 147 "<interleave>" + 124 148 "<element name='Hack' a:help='Hack damage strength'><ref name='nonNegativeDecimal'/></element>" + 125 149 "<element name='Pierce' a:help='Pierce damage strength'><ref name='nonNegativeDecimal'/></element>" + … … 220 244 { 221 245 if (this.template[type] && this.template[type].PreferredClasses && 222 246 this.template[type].PreferredClasses._string) 223 {224 247 return this.template[type].PreferredClasses._string.split(/\s+/); 225 }226 248 return []; 227 249 }; 228 250 … … 230 252 { 231 253 if (this.template[type] && this.template[type].RestrictedClasses && 232 254 this.template[type].RestrictedClasses._string) 233 {234 255 return this.template[type].RestrictedClasses._string.split(/\s+/); 235 }236 256 return []; 237 257 }; 238 258 239 Attack.prototype. CanAttack = function(target)259 Attack.prototype.GetChangeDistance = function() 240 260 { 261 return +(this.template.ChangeDistance || 0); 262 }; 263 264 Attack.prototype.CanAttack = function(target, wantedType) 265 { 241 266 let cmpFormation = Engine.QueryInterface(target, IID_Formation); 242 267 if (cmpFormation) 243 268 return true; 244 269 270 if (wantedType && !this.template[wantedType]) 271 return false; 272 245 273 let cmpThisPosition = Engine.QueryInterface(this.entity, IID_Position); 246 274 let cmpTargetPosition = Engine.QueryInterface(target, IID_Position); 247 275 if (!cmpThisPosition || !cmpTargetPosition || !cmpThisPosition.IsInWorld() || !cmpTargetPosition.IsInWorld()) 248 276 return false; 249 277 250 // Check if the relative height difference is larger than the attack range251 // If the relative height is bigger, it means they will never be able to252 // reach each other, no matter how close they come.253 let heightDiff = Math.abs(cmpThisPosition.GetHeightOffset() - cmpTargetPosition.GetHeightOffset());254 255 278 const cmpIdentity = Engine.QueryInterface(target, IID_Identity); 256 279 if (!cmpIdentity) 257 280 return undefined; 258 281 282 let cmpEntityPlayer = QueryOwnerInterface(this.entity); 283 let cmpTargetPlayer = QueryOwnerInterface(target); 284 if (!cmpTargetPlayer || !cmpEntityPlayer) 285 return false; 286 259 287 const targetClasses = cmpIdentity.GetClassesList(); 288 let cmpCapturable = QueryMiragedInterface(target, IID_Capturable); 289 if (targetClasses.indexOf("Domestic") == -1 && !cmpEntityPlayer.IsEnemy(cmpTargetPlayer.GetPlayerID()) && 290 (!cmpCapturable || !cmpCapturable.CanCapture(cmpEntityPlayer.GetPlayerID()))) 291 return false; 260 292 293 // Check if the relative height difference is larger than the attack range 294 // If the relative height is bigger, it means they will never be able to 295 // reach each other, no matter how close they come. 296 let heightDiff = Math.abs(cmpThisPosition.GetHeightOffset() - cmpTargetPosition.GetHeightOffset()); 297 261 298 for (let type of this.GetAttackTypes()) 262 299 { 263 if ( type == "Capture" && !QueryMiragedInterface(target, IID_Capturable))300 if (wantedType && type != wantedType) 264 301 continue; 265 302 303 if (type == "Capture" && !cmpCapturable) 304 continue; 305 266 306 if (type == "Slaughter" && targetClasses.indexOf("Domestic") == -1) 267 307 continue; 268 308 … … 327 367 if (type == "Slaughter") 328 368 continue; 329 369 let range = this.GetRange(type); 330 if (range.min < ret.min) 331 ret.min = range.min; 332 if (range.max > ret.max) 333 ret.max = range.max; 370 ret.min = Math.min(ret.min, range.min); 371 ret.max = Math.max(ret.max, range.max); 334 372 } 335 373 return ret; 336 374 }; 337 375 338 Attack.prototype.GetBestAttackAgainst = function(target, allowCapture)376 Attack.prototype.GetBestAttackAgainst = function(target, prefAttackType) 339 377 { 340 378 let cmpFormation = Engine.QueryInterface(target, IID_Formation); 341 379 if (cmpFormation) 342 380 { 343 381 // TODO: Formation against formation needs review 382 383 344 384 let best = ["Ranged", "Melee", "Capture"]; 345 385 let types = this.GetAttackTypes(); 346 386 for (let attack of best) 347 if (types.indexOf(attack) != -1 )387 if (types.indexOf(attack) != -1 && (!prefAttackType || "no" + attack != prefAttackType)) 348 388 return attack; 349 389 return undefined; 350 390 } … … 353 393 if (!cmpIdentity) 354 394 return undefined; 355 395 396 // If we are visisble garrisoned always do ranged attack if we can. 397 let cmpPosition = Engine.QueryInterface(this.entity, IID_Position); 398 if (cmpPosition && cmpPosition.GetTurretParent() != INVALID_ENTITY) 399 return this.template.Ranged && "Ranged"; 400 356 401 let targetClasses = cmpIdentity.GetClassesList(); 357 402 let isTargetClass = function (className) { return targetClasses.indexOf(className) != -1; }; 358 403 359 404 // Always slaughter domestic animals instead of using a normal attack 405 360 406 if (isTargetClass("Domestic") && this.template.Slaughter) 361 407 return "Slaughter"; 362 408 363 409 let attack = this; 364 let isAllowed = function (type) { return !attack.GetRestrictedClasses(type).some(isTargetClass) ; };410 let isAllowed = function (type) { return !attack.GetRestrictedClasses(type).some(isTargetClass) && (!prefAttackType || "no" + type != prefAttackType); }; 365 411 366 412 let types = this.GetAttackTypes().filter(isAllowed); 413 if (!types.length) 414 return undefined; 367 415 368 // check if the target is capturable 369 let captureIndex = types.indexOf("Capture"); 370 if (captureIndex != -1) 416 prefAttackType = this.GetAttackTypeFromOrder(prefAttackType); 417 if (prefAttackType && prefAttackType != "Capture" && types.indexOf(prefAttackType) != -1) 418 return prefAttackType; 419 else if (!prefAttackType || prefAttackType == "Capture") 371 420 { 372 let cmpCapturable = QueryMiragedInterface(target, IID_Capturable); 373 374 let cmpPlayer = QueryOwnerInterface(this.entity); 375 if (allowCapture && cmpPlayer && cmpCapturable && cmpCapturable.CanCapture(cmpPlayer.GetPlayerID())) 376 return "Capture"; 377 // not captureable, so remove this attack 378 types.splice(captureIndex, 1); 421 // check if the target is capturable 422 let captureIndex = types.indexOf("Capture"); 423 if (captureIndex != -1) 424 { 425 let cmpCapturable = QueryMiragedInterface(target, IID_Capturable); 426 let cmpPlayer = QueryOwnerInterface(this.entity); 427 if (cmpPlayer && cmpCapturable && cmpCapturable.CanCapture(cmpPlayer.GetPlayerID())) 428 return "Capture"; 429 // not captureable, so remove this attack 430 types.splice(captureIndex, 1); 431 } 379 432 } 380 433 381 let isPreferred = function (className) { return attack.GetPreferredClasses(className).some(isTargetClass); }; 382 let byPreference = function (a, b) { return (types.indexOf(a) + (isPreferred(a) ? types.length : 0) ) - (types.indexOf(b) + (isPreferred(b) ? types.length : 0) ); }; 434 // ignore charges for now: TODO implement these 435 let chargeIndex = types.indexOf("Charge"); 436 if (chargeIndex != -1) 437 types.splice(chargeIndex, 1); 383 438 384 return types.sort(byPreference).pop(); 439 if (!types.length) 440 return undefined; 441 442 // assume ranged and/or melee attack left 443 // TODO stop assuming that? 444 let meleeIndex = types.indexOf("Melee"); 445 let rangedIndex = types.indexOf("Ranged"); 446 if (meleeIndex != -1 && rangedIndex != -1) 447 { 448 if (this.HasTargetPreferredClass(types, targetClasses)) 449 { 450 let isPreferred = function (className) { return attack.GetPreferredClasses(className).some(isTargetClass); }; 451 let byPreference = function (a, b) { return (types.indexOf(a) + (isPreferred(a) ? types.length : 0) ) - (types.indexOf(b) + (isPreferred(b) ? types.length : 0) ); }; 452 453 return types.sort(byPreference).pop(); 454 } 455 456 if (!cmpPosition.IsInWorld()) 457 return undefined; 458 let selfPosition = cmpPosition.GetPosition2D(); 459 let cmpTargetPosition = Engine.QueryInterface(target, IID_Position); 460 if (!cmpTargetPosition || !cmpTargetPosition.IsInWorld()) 461 return undefined; 462 let targetPosition = cmpTargetPosition.GetPosition2D(); 463 if (targetPosition.distanceToSquared(selfPosition) <= Math.pow(this.GetChangeDistance(), 2)) 464 return "Melee"; 465 return "Ranged"; 466 } 467 else if (meleeIndex != -1) 468 return "Melee"; 469 else if (rangedIndex != -1) 470 return "Ranged"; 471 return types[0]; 385 472 }; 386 473 387 474 Attack.prototype.CompareEntitiesByPreference = function(a, b) … … 478 565 return attackBonus; 479 566 }; 480 567 568 Attack.prototype.GetAttackTypeFromOrder = function(prefAttackType) 569 { 570 let types = this.GetAttackTypes(); 571 for (let type of types) 572 if (this.template[type].AttackOrder && this.template[type].AttackOrder == prefAttackType) 573 return type; 574 return prefAttackType; 575 }; 576 577 Attack.prototype.HasTargetPreferredClass = function(types, targetClasses) 578 { 579 let isTargetClass = function (className) { return targetClasses.indexOf(className) != -1; }; 580 for (let type of types) 581 if (this.template[type].PreferredClasses && this.GetPreferredClasses(type).some(isTargetClass)) 582 return true; 583 return false 584 }; 585 481 586 // Returns a 2d random distribution scaled for a spread of scale 1. 482 587 // The current implementation is a 2d gaussian with sigma = 1 483 588 Attack.prototype.GetNormalDistribution = function(){ … … 486 591 let a = Math.random(); 487 592 let b = Math.random(); 488 593 489 let c = Math.sqrt(-2 *Math.log(a)) * Math.cos(2*Math.PI*b);490 let d = Math.sqrt(-2 *Math.log(a)) * Math.sin(2*Math.PI*b);594 let c = Math.sqrt(-2 * Math.log(a)) * Math.cos(2 * Math.PI * b); 595 let d = Math.sqrt(-2 * Math.log(a)) * Math.sin(2 * Math.PI * b); 491 596 492 597 return [c, d]; 493 598 }; … … 503 608 if (type == "Ranged") 504 609 { 505 610 let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); 506 let turnLength = cmpTimer.GetLatestTurnLength() /1000;611 let turnLength = cmpTimer.GetLatestTurnLength() / 1000; 507 612 // In the future this could be extended: 508 613 // * Obstacles like trees could reduce the probability of the target being hit 509 614 // * Obstacles like walls should block projectiles entirely … … 535 640 536 641 let horizDistance = targetPosition.horizDistanceTo(selfPosition); 537 642 538 // This is an approximation of the time otthe target, it assumes that the target has a constant radial643 // This is an approximation of the time to the target, it assumes that the target has a constant radial 539 644 // velocity, but since units move in straight lines this is not true. The exact value would be more 540 645 // difficult to calculate and I think this is sufficiently accurate. (I tested and for cavalry it was 541 646 // about 5% of the units radius out in the worst case) … … 620 725 Attack.prototype.InterpolatedLocation = function(ent, lateness) 621 726 { 622 727 let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); 623 let turnLength = cmpTimer.GetLatestTurnLength() /1000;728 let turnLength = cmpTimer.GetLatestTurnLength() / 1000; 624 729 let cmpTargetPosition = Engine.QueryInterface(ent, IID_Position); 625 730 if (!cmpTargetPosition || !cmpTargetPosition.IsInWorld()) // TODO: handle dead target properly 626 731 return undefined; … … 658 763 let d = Vector3D.sub(point, targetPosition); 659 764 d = Vector2D.from3D(d).rotate(-angle); 660 765 661 return d.x < Math.abs(targetShape.width /2) && d.y < Math.abs(targetShape.depth/2);766 return d.x < Math.abs(targetShape.width / 2) && d.y < Math.abs(targetShape.depth / 2); 662 767 } 663 768 }; 664 769 … … 740 845 return; 741 846 742 847 for (let type of this.GetAttackTypes()) 743 if (msg.valueNames.indexOf("Attack/" +type+"/MaxRange") !== -1)848 if (msg.valueNames.indexOf("Attack/" + type + "/MaxRange") !== -1) 744 849 { 745 850 cmpUnitAI.UpdateRangeQueries(); 746 851 return; -
binaries/data/mods/public/simulation/components/GarrisonHolder.js
10 10 "</attribute>" + 11 11 "<text/>" + 12 12 "</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>" + 13 21 "<element name='EjectHealth' a:help='Percentage of maximum health below which this holder no longer allows garrisoning'>" + 14 22 "<ref name='nonNegativeDecimal'/>" + 15 23 "</element>" + … … 119 127 }; 120 128 121 129 /** 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 */ 133 GarrisonHolder.prototype.GetNeededAttackTypes = function() 134 { 135 return this.template.NeededAttackTypes ? this.template.NeededAttackTypes._string : undefined; 136 }; 137 138 /** 122 139 * Get Maximum pop which can be garrisoned 123 140 */ 124 141 GarrisonHolder.prototype.GetCapacity = function() -
binaries/data/mods/public/simulation/components/GuiInterface.js
352 352 ret.garrisonHolder = { 353 353 "entities": cmpGarrisonHolder.GetEntities(), 354 354 "allowedClasses": cmpGarrisonHolder.GetAllowedClasses(), 355 "neededAttackTypes": cmpGarrisonHolder.GetNeededAttackTypes(), 355 356 "capacity": cmpGarrisonHolder.GetCapacity(), 356 357 "garrisonedEntitiesCount": cmpGarrisonHolder.GetGarrisonedEntitiesCount() 357 358 }; … … 432 433 let types = cmpAttack.GetAttackTypes(); 433 434 if (types.length) 434 435 ret.attack = {}; 436 435 437 for (let type of types) 436 438 { 437 439 ret.attack[type] = cmpAttack.GetAttackStrengths(type); … … 1806 1808 if (!cmpAttack) 1807 1809 return false; 1808 1810 1809 let cmpEntityPlayer = QueryOwnerInterface(data.entity, IID_Player); 1810 let cmpTargetPlayer = QueryOwnerInterface(data.target, IID_Player); 1811 if (!cmpEntityPlayer || !cmpTargetPlayer) 1811 return cmpAttack.CanAttack(data.target); 1812 }; 1813 1814 GuiInterface.prototype.CanAttackWithType = function(player, data) 1815 { 1816 let cmpAttack = Engine.QueryInterface(data.entity, IID_Attack); 1817 if (!cmpAttack) 1812 1818 return false; 1813 1819 1814 // if the owner is an enemy, it's up to the attack component to decide 1815 if (cmpEntityPlayer.IsEnemy(cmpTargetPlayer.GetPlayerID())) 1816 return cmpAttack.CanAttack(data.target); 1817 1818 return false; 1820 return cmpAttack.CanAttack(data.target, data.type); 1819 1821 }; 1820 1822 1821 1823 /* … … 1962 1964 "GetTradingDetails": 1, 1963 1965 "CanCapture": 1, 1964 1966 "CanAttack": 1, 1967 "CanAttackWithType": 1, 1965 1968 "GetBatchTime": 1, 1966 1969 1967 1970 "IsMapRevealed": 1, -
binaries/data/mods/public/simulation/components/UnitAI.js
420 420 } 421 421 422 422 // 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); 424 424 if (!type) 425 425 { 426 426 // Oops, we can't attack at all … … 563 563 if (this.MustKillGatherTarget(this.order.data.target)) 564 564 { 565 565 // 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)) 567 567 { 568 568 // Oops, we can't attack at all - give up 569 569 // TODO: should do something so the player knows why this failed … … 587 587 return; 588 588 } 589 589 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 }); 591 591 return; 592 592 } 593 593 … … 842 842 }, 843 843 844 844 "Order.Attack": function(msg) { 845 vartarget = msg.data.target;846 var allowCapture = msg.data.allowCapture;847 varcmpTargetUnitAI = 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); 848 848 if (cmpTargetUnitAI && cmpTargetUnitAI.IsFormationMember()) 849 849 target = cmpTargetUnitAI.GetFormationController(); 850 850 … … 863 863 this.FinishOrder(); 864 864 return; 865 865 } 866 this.CallMemberFunction("Attack", [target, false, allowCapture]);866 this.CallMemberFunction("Attack", [target, false, prefAttackType]); 867 867 if (cmpAttack.CanAttackAsFormation()) 868 868 this.SetNextState("COMBAT.ATTACKING"); 869 869 else … … 918 918 return; 919 919 } 920 920 921 this.PushOrderFront("Attack", { "target": msg.data.target, "hunting": true, " allowCapture": false});921 this.PushOrderFront("Attack", { "target": msg.data.target, "hunting": true, "prefAttackType": undefined }); 922 922 return; 923 923 } 924 924 … … 1155 1155 1156 1156 "MoveCompleted": function(msg) { 1157 1157 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]); 1159 1159 if (cmpAttack.CanAttackAsFormation()) 1160 1160 this.SetNextState("COMBAT.ATTACKING"); 1161 1161 else … … 1166 1166 "ATTACKING": { 1167 1167 // Wait for individual members to finish 1168 1168 "enter": function(msg) { 1169 vartarget = 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; 1171 1171 // Check if we are already in range, otherwise walk there 1172 1172 if (!this.CheckTargetAttackRange(target, target)) 1173 1173 { … … 1174 1174 if (this.TargetIsAlive(target) && this.CheckTargetVisible(target)) 1175 1175 { 1176 1176 this.FinishOrder(); 1177 this.PushOrderFront("Attack", { "target": target, "force": false, " allowCapture": allowCapture });1177 this.PushOrderFront("Attack", { "target": target, "force": false, "prefAttackType": prefAttackType }); 1178 1178 return true; 1179 1179 } 1180 1180 this.FinishOrder(); … … 1190 1190 }, 1191 1191 1192 1192 "Timer": function(msg) { 1193 vartarget = 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; 1195 1195 // Check if we are already in range, otherwise walk there 1196 1196 if (!this.CheckTargetAttackRange(target, target)) 1197 1197 { … … 1198 1198 if (this.TargetIsAlive(target) && this.CheckTargetVisible(target)) 1199 1199 { 1200 1200 this.FinishOrder(); 1201 this.PushOrderFront("Attack", { "target": target, "force": false, " allowCapture": allowCapture });1201 this.PushOrderFront("Attack", { "target": target, "force": false, "prefAttackType": prefAttackType }); 1202 1202 return; 1203 1203 } 1204 1204 this.FinishOrder(); … … 1410 1410 1411 1411 // target the unit 1412 1412 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 }); 1414 1414 else 1415 1415 { 1416 1416 var cmpPosition = Engine.QueryInterface(msg.data.attacker, IID_Position); … … 1805 1805 // Can't reach it - try to chase after it 1806 1806 if (this.ShouldChaseTargetedEntity(target, this.order.data.force)) 1807 1807 { 1808 if (!this.order.data.force) 1809 this.order.data.attackType = this.GetBestAttackAgainst(target); 1808 1810 if (this.MoveToTargetAttackRange(target, this.order.data.attackType)) 1809 1811 { 1810 1812 this.SetNextState("COMBAT.CHASING"); … … 1916 1918 // Can't reach it - try to chase after it 1917 1919 if (this.ShouldChaseTargetedEntity(target, this.order.data.force)) 1918 1920 { 1921 if (!this.order.data.force) 1922 { 1923 let type = this.GetBestAttackAgainst(target); 1924 if (type) 1925 this.order.data.attackType = type; 1926 } 1919 1927 if (this.MoveToTargetRange(target, IID_Attack, this.order.data.attackType)) 1920 1928 { 1921 1929 this.SetNextState("COMBAT.CHASING"); … … 4549 4557 return distance < range; 4550 4558 }; 4551 4559 4552 UnitAI.prototype.GetBestAttackAgainst = function(target, allowCapture)4560 UnitAI.prototype.GetBestAttackAgainst = function(target, prefAttackType) 4553 4561 { 4554 4562 var cmpAttack = Engine.QueryInterface(this.entity, IID_Attack); 4555 4563 if (!cmpAttack) 4556 4564 return undefined; 4557 return cmpAttack.GetBestAttackAgainst(target, allowCapture);4565 return cmpAttack.GetBestAttackAgainst(target, prefAttackType); 4558 4566 }; 4559 4567 4560 4568 UnitAI.prototype.GetAttackBonus = function(type, target) … … 4576 4584 if (!target) 4577 4585 return false; 4578 4586 4579 this.PushOrderFront("Attack", { "target": target, "force": false, "forceResponse": forceResponse, " allowCapture": true});4587 this.PushOrderFront("Attack", { "target": target, "force": false, "forceResponse": forceResponse, "prefAttackType": undefined }); 4580 4588 return true; 4581 4589 }; 4582 4590 … … 4589 4597 { 4590 4598 var target = ents.find(target => 4591 4599 this.CanAttack(target, forceResponse) 4592 && this.CheckTargetDistanceFromHeldPosition(target, IID_Attack, this.GetBestAttackAgainst(target , true))4600 && this.CheckTargetDistanceFromHeldPosition(target, IID_Attack, this.GetBestAttackAgainst(target)) 4593 4601 && (this.GetStance().respondChaseBeyondVision || this.CheckTargetIsInVisionRange(target)) 4594 4602 ); 4595 4603 if (!target) 4596 4604 return false; 4597 4605 4598 this.PushOrderFront("Attack", { "target": target, "force": false, "forceResponse": forceResponse, " allowCapture": true});4606 this.PushOrderFront("Attack", { "target": target, "force": false, "forceResponse": forceResponse, "prefAttackType": undefined }); 4599 4607 return true; 4600 4608 }; 4601 4609 … … 4992 5000 * to a player order, and so is forced. 4993 5001 * If targetClasses is given, only entities matching the targetClasses can be attacked. 4994 5002 */ 4995 UnitAI.prototype.WalkAndFight = function(x, z, targetClasses, queued )5003 UnitAI.prototype.WalkAndFight = function(x, z, targetClasses, queued, prefAttackType) 4996 5004 { 4997 this.AddOrder("WalkAndFight", { "x": x, "z": z, "targetClasses": targetClasses, "force": true }, queued);5005 this.AddOrder("WalkAndFight", { "x": x, "z": z, "targetClasses": targetClasses, "force": true, "prefAttackType": prefAttackType }, queued); 4998 5006 }; 4999 5007 5000 5008 /** … … 5015 5023 /** 5016 5024 * Adds attack order to the queue, forced by the player. 5017 5025 */ 5018 UnitAI.prototype.Attack = function(target, queued, allowCapture)5026 UnitAI.prototype.Attack = function(target, queued, prefAttackType) 5019 5027 { 5020 5028 if (!this.CanAttack(target)) 5021 5029 { … … 5027 5035 this.WalkToTarget(target, queued); 5028 5036 return; 5029 5037 } 5030 this.AddOrder("Attack", { "target": target, "force": true, " allowCapture": allowCapture}, queued);5038 this.AddOrder("Attack", { "target": target, "force": true, "prefAttackType": prefAttackType }, queued); 5031 5039 }; 5032 5040 5033 5041 /** … … 5456 5464 if (targetClasses.vetoEntities && targetClasses.vetoEntities[targ]) 5457 5465 continue; 5458 5466 } 5459 this.PushOrderFront("Attack", { "target": targ, "force": true, " allowCapture": true});5467 this.PushOrderFront("Attack", { "target": targ, "force": true, "prefAttackType": undefined }); 5460 5468 return true; 5461 5469 } 5462 5470 } … … 5482 5490 if (targetClasses.vetoEntities && targetClasses.vetoEntities[targ]) 5483 5491 continue; 5484 5492 } 5485 this.PushOrderFront("Attack", { "target": targ, "force": true, " allowCapture": true});5493 this.PushOrderFront("Attack", { "target": targ, "force": true, "prefAttackType": undefined }); 5486 5494 return true; 5487 5495 } 5488 5496 … … 5682 5690 if (this.IsFormationController()) 5683 5691 return true; 5684 5692 5685 varcmpGarrisonHolder = Engine.QueryInterface(target, IID_GarrisonHolder);5693 let cmpGarrisonHolder = Engine.QueryInterface(target, IID_GarrisonHolder); 5686 5694 if (!cmpGarrisonHolder) 5687 5695 return false; 5688 5696 5689 5697 // Verify that the target is owned by this entity's player or a mutual ally of this player 5690 varcmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership);5698 let cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership); 5691 5699 if (!cmpOwnership || !(IsOwnedByPlayer(cmpOwnership.GetOwner(), target) || IsOwnedByMutualAllyOfPlayer(cmpOwnership.GetOwner(), target))) 5692 5700 return false; 5693 5701 … … 5697 5705 if (this.IsAnimal()) 5698 5706 return false; 5699 5707 5708 let cmpIdentity = Engine.QueryInterface(this.entity, IID_Identity) 5709 if (!cmpIdentity) 5710 return false; 5711 5712 let cmpAttack = Engine.QueryInterface(this.entity, IID_Attack) 5713 let attackTypes = cmpAttack ? cmpAttack.GetAttackTypes() : []; 5714 if (!MatchesClassList(cmpIdentity.GetClassesList(), cmpGarrisonHolder.GetAllowedClasses()) || 5715 !HasNeededAttackTypes(attackTypes, cmpGarrisonHolder.GetNeededAttackTypes())) 5716 return false; 5700 5717 return true; 5701 5718 }; 5702 5719 -
binaries/data/mods/public/simulation/components/tests/test_UnitAI.js
97 97 AddMock(unit, IID_Attack, { 98 98 GetRange: function() { return { "max": 10, "min": 0}; }, 99 99 GetFullAttackRange: function() { return { "max": 40, "min": 0}; }, 100 GetBestAttackAgainst: function(t) { return " melee"; },100 GetBestAttackAgainst: function(t) { return "Melee"; }, 101 101 GetPreference: function(t) { return 0; }, 102 102 GetTimers: function() { return { "prepare": 500, "repeat": 1000 }; }, 103 103 CanAttack: function(v) { return true; }, … … 247 247 AddMock(unit + i, IID_Attack, { 248 248 GetRange: function() { return {"max":10, "min": 0}; }, 249 249 GetFullAttackRange: function() { return { "max": 40, "min": 0}; }, 250 GetBestAttackAgainst: function(t) { return " melee"; },250 GetBestAttackAgainst: function(t) { return "Melee"; }, 251 251 GetTimers: function() { return { "prepare": 500, "repeat": 1000 }; }, 252 252 CanAttack: function(v) { return true; }, 253 253 CompareEntitiesByPreference: function(a, b) { return 0; }, -
binaries/data/mods/public/simulation/helpers/Commands.js
158 158 "attack-walk": function(player, cmd, data) 159 159 { 160 160 GetFormationUnitAIs(data.entities, player).forEach(cmpUnitAI => { 161 cmpUnitAI.WalkAndFight(cmd.x, cmd.z, cmd.targetClasses, cmd.queued );161 cmpUnitAI.WalkAndFight(cmd.x, cmd.z, cmd.targetClasses, cmd.queued, cmd.prefAttackType); 162 162 }); 163 163 }, 164 164 … … 167 167 if (g_DebugCommands && !(IsOwnedByEnemyOfPlayer(player, cmd.target) || IsOwnedByNeutralOfPlayer(player, cmd.target))) 168 168 warn("Invalid command: attack target is not owned by enemy of player "+player+": "+uneval(cmd)); 169 169 170 let allowCapture = cmd.allowCapture || cmd.allowCapture == null;171 170 // See UnitAI.CanAttack for target checks 172 171 GetFormationUnitAIs(data.entities, player).forEach(cmpUnitAI => { 173 cmpUnitAI.Attack(cmd.target, cmd.queued, allowCapture);172 cmpUnitAI.Attack(cmd.target, cmd.queued, cmd.prefAttackType); 174 173 }); 175 174 }, 176 175 -
binaries/data/mods/public/simulation/templates/template_structure_defense_wall_long.xml
12 12 </Health> 13 13 <GarrisonHolder> 14 14 <Max>5</Max> 15 <List datatype="tokens">Ranged+Infantry</List> 15 <List datatype="tokens">Infantry</List> 16 <NeededAttackTypes datatype="tokens">Ranged</NeededAttackTypes> 16 17 <EjectHealth>0.1</EjectHealth> 17 18 <EjectClassesOnDestroy datatype="tokens">Unit</EjectClassesOnDestroy> 18 19 <BuffHeal>0</BuffHeal> -
binaries/data/mods/public/simulation/templates/template_structure_defense_wall_medium.xml
9 9 </Cost> 10 10 <GarrisonHolder> 11 11 <Max>3</Max> 12 <List datatype="tokens">Ranged+Infantry</List> 12 <List datatype="tokens">Infantry</List> 13 <NeededAttackTypes datatype="tokens">Ranged</NeededAttackTypes> 13 14 <EjectHealth>0.1</EjectHealth> 14 15 <EjectClassesOnDestroy datatype="tokens">Unit</EjectClassesOnDestroy> 15 16 <BuffHeal>0</BuffHeal> -
binaries/data/mods/public/simulation/templates/units/pers_champion_infantry.xml
1 1 <?xml version="1.0" encoding="utf-8"?> 2 2 <Entity parent="template_unit_champion_infantry_spearman"> 3 <Attack> 4 <ChangeDistance>25</ChangeDistance> 5 <Melee> 6 <AttackOrder>primary</AttackOrder> 7 <PreferredClasses datatype="tokens">Siege</PreferredClasses> 8 </Melee> 9 <Ranged> 10 <AttackOrder>secondary</AttackOrder> 11 <Hack>0</Hack> 12 <Pierce>6.0</Pierce> 13 <Crush>0</Crush> 14 <MaxRange>65.0</MaxRange> 15 <MinRange>10.0</MinRange> 16 <ProjectileSpeed>120.0</ProjectileSpeed> 17 <PrepareTime>1000</PrepareTime> 18 <RepeatTime>1000</RepeatTime> 19 <Spread>3.0</Spread> 20 </Ranged> 21 </Attack> 3 22 <Identity> 4 23 <Civ>pers</Civ> 5 24 <GenericName>Persian Immortal</GenericName>