Ticket #252: t252_secondattack_18.diff
File t252_secondattack_18.diff, 56.0 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 "melee-attack-move": 111 { 112 "execute": function(target, action, selection, queued) 113 { 114 let targetClasses; 115 if (Engine.HotkeyIsPressed("session.meleeattackmoveUnit")) 116 targetClasses = { "meleeattack": ["Unit"] }; 117 else 118 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 if (someUnitAI(selection) && Engine.HotkeyIsPressed("session.meleeattackmove") && 144 getActionInfo("melee-attack-move", target).possible) 145 return { 146 "type": "melee-attack-move", 147 "cursor": "action-melee-attack" 148 }; 149 return false; 150 }, 151 "specificness": 31, 152 }, 153 154 "ranged-attack-move": 155 { 156 "execute": function(target, action, selection, queued) 157 { 158 let targetClasses; 159 if (Engine.HotkeyIsPressed("session.rangedattackmoveUnit")) 160 targetClasses = { "rangedattack": ["Unit"] }; 161 else 162 targetClasses = { "rangedattack": ["Unit", "Structure"] }; 163 164 Engine.PostNetworkCommand({ 165 "type": "attack-walk", 166 "entities": selection, 167 "x": target.x, 168 "z": target.z, 169 "targetClasses": targetClasses, 170 "queued": queued, 171 "prefAttackType": "Ranged" 172 }); 173 174 Engine.GuiInterfaceCall("PlaySound", { 175 "name": "order_walk", 176 "entity": selection[0] 177 }); 178 179 return true; 180 }, 181 "getActionInfo": function(entState, targetState) 182 { 183 return { "possible": true }; 184 }, 185 "hotkeyActionCheck": function(target, selection) 186 { 187 if (someUnitAI(selection) && Engine.HotkeyIsPressed("session.rangedattackmove") && 188 getActionInfo("ranged-attack-move", target).possible) 189 return { 190 "type": "ranged-attack-move", 191 "cursor": "action-ranged-attack" 192 }; 193 return false; 194 }, 195 "specificness": 32, 196 }, 197 109 198 "capture": 110 199 { 111 200 "execute": function(target, action, selection, queued) … … 115 204 "entities": selection, 116 205 "target": action.target, 117 206 "allowCapture": true, 118 "queued": queued 207 "queued": queued, 208 "prefAttackType": "Capture" 119 209 }); 120 210 121 211 Engine.GuiInterfaceCall("PlaySound", { … … 148 238 "target": target 149 239 }; 150 240 }, 151 "specificness": 9,241 "specificness": 7, 152 242 }, 153 243 154 244 "attack": … … 160 250 "entities": selection, 161 251 "target": action.target, 162 252 "queued": queued, 163 "allowCapture": false 253 "allowCapture": false, 254 "prefAttackType": "primary" 164 255 }); 165 256 166 257 Engine.GuiInterfaceCall("PlaySound", { … … 205 296 "target": target 206 297 }; 207 298 }, 299 "specificness": 8, 300 }, 301 302 "melee-attack": 303 { 304 "execute": function(target, action, selection, queued) 305 { 306 Engine.PostNetworkCommand({ 307 "type": "attack", 308 "entities": selection, 309 "target": action.target, 310 "queued": queued, 311 "prefAttackType": "Melee" 312 }); 313 314 Engine.GuiInterfaceCall("PlaySound", { 315 "name": "order_attack", 316 "entity": selection[0] 317 }); 318 return true; 319 }, 320 "getActionInfo": function(entState, targetState) 321 { 322 if (!entState.attack || !targetState.hitpoints) 323 return false; 324 return { "possible": Engine.GuiInterfaceCall("CanAttackWithType", { 325 "entity": entState.id, 326 "target": targetState.id, 327 "type": "Melee" 328 }) }; 329 }, 330 "hotkeyActionCheck": function(target) 331 { 332 if (Engine.HotkeyIsPressed("session.meleeattack") && getActionInfo("melee-attack", target).possible) 333 return { 334 "type": "melee-attack", 335 "cursor": "action-melee-attack", 336 "target": target 337 }; 338 return false; 339 }, 340 "actionCheck": function(target) 341 { 342 if (getActionInfo("melee-attack", target).possible) 343 return { 344 "type": "melee-attack", 345 "cursor": "action-melee-attack", 346 "target": target 347 }; 348 return false; 349 }, 350 "specificness": 9, 351 }, 352 353 "ranged-attack": 354 { 355 "execute": function(target, action, selection, queued) 356 { 357 Engine.PostNetworkCommand({ 358 "type": "attack", 359 "entities": selection, 360 "target": action.target, 361 "queued": queued, 362 "prefAttackType": "Ranged" 363 }); 364 365 Engine.GuiInterfaceCall("PlaySound", { 366 "name": "order_attack", 367 "entity": selection[0] 368 }); 369 370 return true; 371 }, 372 "getActionInfo": function(entState, targetState) 373 { 374 if (!entState.attack || !targetState.hitpoints) // hack 375 return false; 376 return { "possible": Engine.GuiInterfaceCall("CanAttackWithType", { 377 "entity": entState.id, 378 "target": targetState.id, 379 "type": "Ranged" 380 }) }; 381 }, 382 "hotkeyActionCheck": function(target, selection) 383 { 384 if (Engine.HotkeyIsPressed("session.rangedattack") && getActionInfo("ranged-attack", target).possible) 385 return { 386 "type": "ranged-attack", 387 "cursor": "action-ranged-attack", 388 "target": target 389 }; 390 return false; 391 }, 392 "actionCheck": function(target, selection) 393 { 394 if (getActionInfo("ranged-attack", target).possible) 395 return { 396 "type": "ranged-attack", 397 "cursor": "action-ranged-attack", 398 "target": target 399 }; 400 return false; 401 }, 208 402 "specificness": 10, 209 403 }, 210 404 … … 255 449 "target": target 256 450 }; 257 451 }, 258 "specificness": 7,452 "specificness": 6, 259 453 }, 260 454 261 455 "build": … … 355 549 "target": target 356 550 }; 357 551 }, 358 "specificness": 1 1,552 "specificness": 12, 359 553 }, 360 554 361 555 "gather": … … 595 789 if (targetState.garrisonHolder.garrisonedEntitiesCount + extraCount >= targetState.garrisonHolder.capacity) 596 790 tooltip = "[color=\"orange\"]" + tooltip + "[/color]"; 597 791 598 if (!MatchesClassList(entState.identity.classes, targetState.garrisonHolder.allowedClasses)) 599 return false; 792 if (MatchesClassList(entState.identity.classes, targetState.garrisonHolder.allowedClasses) && 793 HasNeededAttackTypes(entState.attack, targetState.garrisonHolder.neededAttackTypes)) 794 return { 795 "possible": true, 796 "tooltip": tooltip 797 }; 600 798 601 return { 602 "possible": true, 603 "tooltip": tooltip 604 }; 799 return { "possible": false }; 605 800 }, 606 801 "preSelectedActionCheck": function(target) 607 802 { … … 783 978 cursor = "action-attack-move"; 784 979 } 785 980 981 if (Engine.HotkeyIsPressed("session.meleeattackmove")) 982 { 983 let targetClasses; 984 if (Engine.HotkeyIsPressed("session.meleeattackmoveUnit")) 985 targetClasses = { "melee-attack": ["Unit"] }; 986 else 987 targetClasses = { "melee-attack": ["Unit", "Structure"] }; 988 data.command = "melee-attack-walk"; 989 data.targetClasses = targetClasses; 990 cursor = "action-melee-attack"; 991 } 992 993 if (Engine.HotkeyIsPressed("session.rangedattackmove")) 994 { 995 let targetClasses; 996 if (Engine.HotkeyIsPressed("session.rangedattackmoveUnit")) 997 targetClasses = { "ranged-attack": ["Unit"] }; 998 else 999 targetClasses = { "ranged-attack": ["Unit", "Structure"] }; 1000 data.command = "ranged-attack-walk"; 1001 data.targetClasses = targetClasses; 1002 cursor = "action-ranged-attack"; 1003 } 1004 786 1005 if (targetState.garrisonHolder && 787 1006 playerCheck(entState, targetState, ["Player", "MutualAlly"])) 788 1007 { … … 901 1120 "position": actionInfo.position 902 1121 }; 903 1122 }, 904 "specificness": 6,1123 "specificness": 5, 905 1124 }, 906 1125 907 1126 "unset-rallypoint": -
binaries/data/mods/public/simulation/ai/common-api/entity.js
746 746 return false; 747 747 }, 748 748 749 isCapturable: function() { return this.get("Capturable") !== undefined; }, 749 750 isGarrisonHolder: function() { return this.get("GarrisonHolder") !== undefined; }, 750 751 garrisoned: function() { return this._entity.garrisoned; }, 751 752 canGarrisonInside: function() { return this._entity.garrisoned.length < this.garrisonMax(); }, … … 756 757 }, 757 758 758 759 moveToRange: function(x, z, min, max, queued = false) { 759 Engine.PostCommand(PlayerID,{ "type": "walk-to-range", "entities": [this.id()], "x": x, "z": z, "min": min, "max": max, "queued": queued });760 Engine.PostCommand(PlayerID,{ "type": "walk-to-range", "entities": [this.id()], "x": x, "z": z, "min": min, "max": max, "queued": queued }); 760 761 return this; 761 762 }, 762 763 763 attackMove: function(x, z, targetClasses, queued = false) {764 Engine.PostCommand(PlayerID,{"type": "attack-walk", "entities": [this.id()], "x": x, "z": z, "targetClasses": targetClasses, " queued": queued});764 attackMove: function(x, z, targetClasses, prefAttackType = "undefined", queued = false) { // hack 765 Engine.PostCommand(PlayerID,{"type": "attack-walk", "entities": [this.id()], "x": x, "z": z, "targetClasses": targetClasses, "prefAttackType": prefAttackType, "queued": queued}); 765 766 return this; 766 767 }, 767 768 … … 795 796 return this; 796 797 }, 797 798 798 attack: function(unitId, allowCapture = true, queued = false) {799 Engine.PostCommand(PlayerID,{"type": "attack", "entities": [this.id()], "target": unitId, " allowCapture": allowCapture, "queued": queued});799 attack: function(unitId, prefAttackType = undefined, queued = false) { 800 Engine.PostCommand(PlayerID,{"type": "attack", "entities": [this.id()], "target": unitId, "prefAttackType": prefAttackType, "queued": queued}); 800 801 return this; 801 802 }, 802 803 -
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
351 351 ret.garrisonHolder = { 352 352 "entities": cmpGarrisonHolder.GetEntities(), 353 353 "allowedClasses": cmpGarrisonHolder.GetAllowedClasses(), 354 "neededAttackTypes": cmpGarrisonHolder.GetNeededAttackTypes(), 354 355 "capacity": cmpGarrisonHolder.GetCapacity(), 355 356 "garrisonedEntitiesCount": cmpGarrisonHolder.GetGarrisonedEntitiesCount() 356 357 }; … … 431 432 let types = cmpAttack.GetAttackTypes(); 432 433 if (types.length) 433 434 ret.attack = {}; 435 434 436 for (let type of types) 435 437 { 436 438 ret.attack[type] = cmpAttack.GetAttackStrengths(type); … … 1804 1806 if (!cmpAttack) 1805 1807 return false; 1806 1808 1807 let cmpEntityPlayer = QueryOwnerInterface(data.entity, IID_Player); 1808 let cmpTargetPlayer = QueryOwnerInterface(data.target, IID_Player); 1809 if (!cmpEntityPlayer || !cmpTargetPlayer) 1809 return cmpAttack.CanAttack(data.target); 1810 }; 1811 1812 GuiInterface.prototype.CanAttackWithType = function(player, data) 1813 { 1814 let cmpAttack = Engine.QueryInterface(data.entity, IID_Attack); 1815 if (!cmpAttack) 1810 1816 return false; 1811 1817 1812 // if the owner is an enemy, it's up to the attack component to decide 1813 if (cmpEntityPlayer.IsEnemy(cmpTargetPlayer.GetPlayerID())) 1814 return cmpAttack.CanAttack(data.target); 1815 1816 return false; 1818 return cmpAttack.CanAttack(data.target, data.type); 1817 1819 }; 1818 1820 1819 1821 /* … … 1960 1962 "GetTradingDetails": 1, 1961 1963 "CanCapture": 1, 1962 1964 "CanAttack": 1, 1965 "CanAttackWithType": 1, 1963 1966 "GetBatchTime": 1, 1964 1967 1965 1968 "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
160 160 "attack-walk": function(player, cmd, data) 161 161 { 162 162 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); 164 164 }); 165 165 }, 166 166 … … 169 169 if (g_DebugCommands && !(IsOwnedByEnemyOfPlayer(player, cmd.target) || IsOwnedByNeutralOfPlayer(player, cmd.target))) 170 170 warn("Invalid command: attack target is not owned by enemy of player "+player+": "+uneval(cmd)); 171 171 172 let allowCapture = cmd.allowCapture || cmd.allowCapture == null;173 172 // See UnitAI.CanAttack for target checks 174 173 GetFormationUnitAIs(data.entities, player).forEach(cmpUnitAI => { 175 cmpUnitAI.Attack(cmd.target, cmd.queued, allowCapture);174 cmpUnitAI.Attack(cmd.target, cmd.queued, cmd.prefAttackType); 176 175 }); 177 176 }, 178 177 -
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>