Ticket #252: t252_secondattack_15.diff
File t252_secondattack_15.diff, 62.1 KB (added by , 8 years ago) |
---|
-
binaries/data/config/default.cfg
274 274 [hotkey.session] 275 275 kill = Delete ; Destroy selected units 276 276 stop = "H" ; Stop the current action 277 attack = Ctrl ; Modifier to attack instead of another action (eg capture)278 attackmove = Ctrl ; Modifier to attackmove when clicking on a point279 attackmoveUnit = "Ctrl+Q" ; Modifier to attackmove targeting only units when clicking on a point (should contain the attackmove keys)277 attack = Ctrl ; Modifier to primary attack instead of another action (eg capture) 278 attackmove = Ctrl ; Modifier to primary attackmove when clicking on a point 279 attackmoveUnit = "Ctrl+Q" ; Modifier to primary attackmove targeting only units when clicking on a point (should contain the attackmove keys) 280 280 garrison = Ctrl ; Modifier to garrison when clicking on building 281 281 autorallypoint = Ctrl ; Modifier to set the rally point on the building itself 282 meleeattack = Alt ; Modifier to melee attack instead of another action 283 meleeattackmove = Alt ; Modifier to melee attackmove when clicking on a point 284 meleeattackmoveUnit = "Alt+Q"; Modifier to melee attackmove targeting only units when clicking on a point 285 rangedattack = space ; Modifier to ranged attack instead of another action 286 rangedattackmove = space ; Modifier to ranged attackmove when clicking on a point 287 rangedattackmoveUnit = "space+Q"; Modifier to ranged attackmove targeting only units when clicking on a point 282 288 guard = "G" ; Modifier to escort/guard when clicking on unit/building 283 289 queue = Shift ; Modifier to queue unit orders instead of replacing 284 290 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[Type]) 81 return false; 82 return true; 83 } 84 85 /** 69 86 * Get information about a template with or without technology modifications. 70 87 * @param template A valid template as returned by the template loader. 71 88 * @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
1 1 /** 2 * List of different actions units can execute, 2 * List of different actions units can execute, 3 3 * this is mostly used to determine which actions can be executed 4 4 * 5 5 * "execute" is meant to send the command to the engine 6 6 * 7 * The next functions will always return false 7 * The next functions will always return false 8 8 * in case you have to continue to seek 9 * (i.e. look at the next entity for getActionInfo, the next 9 * (i.e. look at the next entity for getActionInfo, the next 10 10 * possible action for the actionCheck ...) 11 11 * They will return an object when the searching is finished 12 12 * … … 24 24 * 25 25 * "specificness" is used to determine how specific an action is, 26 26 * The lower the number, the more specific an action is, and the bigger 27 * the chance of selecting that action when multiple actions are possible 27 * the chance of selecting that action when multiple actions are possible 28 28 */ 29 29 30 var unitActions = 30 var unitActions = 31 31 { 32 32 "move": 33 33 { … … 53 53 return {"type": "move"}; 54 54 return false; 55 55 }, 56 "specificness": 1 2,56 "specificness": 13, 57 57 }, 58 58 59 "attack-move": 59 "attack-move": 60 60 { 61 61 "execute": function(target, action, selection, queued) 62 62 { … … 65 65 else 66 66 var targetClasses = { "attack": ["Unit", "Structure"] }; 67 67 68 Engine.PostNetworkCommand({ "type": "attack-walk", "entities": selection, "x": target.x, "z": target.z, "targetClasses": targetClasses, "queued": queued});68 Engine.PostNetworkCommand({ "type": "attack-walk", "entities": selection, "x": target.x, "z": target.z, "targetClasses": targetClasses, "queued": queued, "prefAttackType": "primary" }); 69 69 Engine.GuiInterfaceCall("PlaySound", { "name": "order_walk", "entity": selection[0] }); 70 70 return true; 71 71 }, 72 72 "getActionInfo": function(entState, targetState) 73 73 { 74 return { "possible": true};74 return { "possible": true }; 75 75 }, 76 76 "hotkeyActionCheck": function(target, selection) 77 77 { … … 81 81 return entState && entState.unitAI; 82 82 }); 83 83 if (haveUnitAI && Engine.HotkeyIsPressed("session.attackmove") && getActionInfo("attack-move", target).possible) 84 return { "type": "attack-move", "cursor": "action-attack-move"};84 return { "type": "attack-move", "cursor": "action-attack-move" }; 85 85 return false; 86 86 }, 87 87 "specificness": 30, 88 88 }, 89 89 90 " capture":90 "melee-attack-move": 91 91 { 92 92 "execute": function(target, action, selection, queued) 93 93 { 94 Engine.PostNetworkCommand({"type": "attack", "entities": selection, "target": action.target, "allowCapture": true, "queued": queued}); 94 if (Engine.HotkeyIsPressed("session.meleeattackmoveUnit")) 95 var targetClasses = { "meleeattack": ["Unit"] }; 96 else 97 var targetClasses = { "meleeattack": ["Unit", "Structure"] }; 98 99 Engine.PostNetworkCommand({ "type": "attack-walk", "entities": selection, "x": target.x, "z": target.z, "targetClasses": targetClasses, "queued": queued, "prefAttackType": "Melee" }); 100 Engine.GuiInterfaceCall("PlaySound", { "name": "order_walk", "entity": selection[0] }); 101 return true; 102 }, 103 "getActionInfo": function(entState, targetState) 104 { 105 return { "possible": true }; 106 }, 107 "hotkeyActionCheck": function(target, selection) 108 { 109 // Work out whether at least part of the selection have UnitAI 110 let haveUnitAI = selection.some(function(ent) { 111 let entState = GetEntityState(ent); 112 return entState && entState.unitAI && getActionInfo("melee-attack-move", target).possible; 113 }); 114 115 if (haveUnitAI && Engine.HotkeyIsPressed("session.meleeattackmove") && getActionInfo("melee-attack-move", target).possible) 116 return { "type": "melee-attack-move", "cursor": "action-melee-attack" }; 117 return false; 118 }, 119 "specificness": 31, 120 }, 121 122 "ranged-attack-move": 123 { 124 "execute": function(target, action, selection, queued) 125 { 126 if (Engine.HotkeyIsPressed("session.rangedattackmoveUnit")) 127 var targetClasses = { "rangedattack": ["Unit"] }; 128 else 129 var targetClasses = { "rangedattack": ["Unit", "Structure"] }; 130 131 Engine.PostNetworkCommand({ "type": "attack-walk", "entities": selection, "x": target.x, "z": target.z, "targetClasses": targetClasses, "queued": queued, "prefAttackType": "Ranged" }); 132 Engine.GuiInterfaceCall("PlaySound", { "name": "order_walk", "entity": selection[0] }); 133 return true; 134 }, 135 "getActionInfo": function(entState, targetState) 136 { 137 return { "possible": true }; 138 }, 139 "hotkeyActionCheck": function(target, selection) 140 { 141 // Work out whether at least part of the selection have UnitAI 142 let haveUnitAI = selection.some(function(ent) { 143 let entState = GetEntityState(ent); 144 return entState && entState.unitAI && getActionInfo("ranged-attack-move", target).possible; 145 }); 146 147 if (haveUnitAI && Engine.HotkeyIsPressed("session.rangedattackmove") && getActionInfo("ranged-attack-move", target).possible) 148 return { "type": "ranged-attack-move", "cursor": "action-ranged-attack" }; 149 return false; 150 }, 151 "specificness": 32, 152 }, 153 154 "capture": 155 { 156 "execute": function(target, action, selection, queued) 157 { 158 Engine.PostNetworkCommand({ "type": "attack", "entities": selection, "target": action.target, "queued": queued, "prefAttackType": "Capture" }); 95 159 Engine.GuiInterfaceCall("PlaySound", { "name": "order_attack", "entity": selection[0] }); 96 160 return true; 97 161 }, … … 99 163 { 100 164 if (!entState.attack || !targetState.hitpoints) 101 165 return false; 102 return { "possible": Engine.GuiInterfaceCall("CanCapture", {"entity": entState.id, "target": targetState.id})};166 return { "possible": Engine.GuiInterfaceCall("CanCapture", { "entity": entState.id, "target": targetState.id }) }; 103 167 }, 104 168 "actionCheck": function(target) 105 169 { 106 170 if (getActionInfo("capture", target).possible) 107 return { "type": "capture", "cursor": "action-capture", "target": target};171 return { "type": "capture", "cursor": "action-capture", "target": target }; 108 172 return false; 109 173 }, 110 "specificness": 9,174 "specificness": 7, 111 175 }, 112 176 113 177 "attack": … … 114 178 { 115 179 "execute": function(target, action, selection, queued) 116 180 { 117 Engine.PostNetworkCommand({ "type": "attack", "entities": selection, "target": action.target, "queued": queued, "allowCapture": false});181 Engine.PostNetworkCommand({ "type": "attack", "entities": selection, "target": action.target, "queued": queued, "prefAttackType": "primary" }); 118 182 Engine.GuiInterfaceCall("PlaySound", { "name": "order_attack", "entity": selection[0] }); 119 183 return true; 120 184 }, … … 122 186 { 123 187 if (!entState.attack || !targetState.hitpoints) 124 188 return false; 125 return { "possible": Engine.GuiInterfaceCall("CanAttack", {"entity": entState.id, "target": targetState.id})};189 return { "possible": Engine.GuiInterfaceCall("CanAttack", { "entity": entState.id, "target": targetState.id }) }; 126 190 }, 127 191 "hotkeyActionCheck": function(target) 128 192 { 129 193 if (Engine.HotkeyIsPressed("session.attack") && getActionInfo("attack", target).possible) 130 return { "type": "attack", "cursor": "action-attack", "target": target};194 return { "type": "attack", "cursor": "action-attack", "target": target }; 131 195 return false; 132 196 }, 133 197 "actionCheck": function(target) 134 198 { 135 199 if (getActionInfo("attack", target).possible) 136 return { "type": "attack", "cursor": "action-attack", "target": target};200 return { "type": "attack", "cursor": "action-attack", "target": target }; 137 201 return false; 138 202 }, 203 "specificness": 8, 204 }, 205 206 "melee-attack": 207 { 208 "execute": function(target, action, selection, queued) 209 { 210 Engine.PostNetworkCommand({ "type": "attack", "entities": selection, "target": action.target, "queued": queued, "prefAttackType": "Melee" }); 211 Engine.GuiInterfaceCall("PlaySound", { "name": "order_attack", "entity": selection[0] }); 212 return true; 213 }, 214 "getActionInfo": function(entState, targetState) 215 { 216 if (!entState.attack || !targetState.hitpoints) 217 return false; 218 return { "possible": Engine.GuiInterfaceCall("CanAttackWithType", { "entity": entState.id, "target": targetState.id, "type": "Melee" }) }; 219 }, 220 "hotkeyActionCheck": function(target) 221 { 222 if (Engine.HotkeyIsPressed("session.meleeattack") && getActionInfo("melee-attack", target).possible) 223 return { "type": "melee-attack", "cursor": "action-melee-attack", "target": target }; 224 return false; 225 }, 226 "actionCheck": function(target) 227 { 228 if (getActionInfo("melee-attack", target).possible) 229 return { "type": "melee-attack", "cursor": "action-melee-attack", "target": target }; 230 return false; 231 }, 232 "specificness": 9, 233 }, 234 235 "ranged-attack": 236 { 237 "execute": function(target, action, selection, queued) 238 { 239 Engine.PostNetworkCommand({ "type": "attack", "entities": selection, "target": action.target, "queued": queued, "prefAttackType": "Ranged" }); 240 Engine.GuiInterfaceCall("PlaySound", { "name": "order_attack", "entity": selection[0] }); 241 return true; 242 }, 243 "getActionInfo": function(entState, targetState) 244 { 245 if (!entState.attack || !targetState.hitpoints) // hack 246 return false; 247 return { "possible": Engine.GuiInterfaceCall("CanAttackWithType", { "entity": entState.id, "target": targetState.id, "type": "Ranged" }) }; 248 }, 249 "hotkeyActionCheck": function(target, selection) 250 { 251 if (Engine.HotkeyIsPressed("session.rangedattack") && getActionInfo("ranged-attack", target).possible) 252 return { "type": "ranged-attack", "cursor": "action-ranged-attack", "target": target }; 253 return false; 254 }, 255 "actionCheck": function(target, selection) 256 { 257 if (getActionInfo("ranged-attack", target).possible) 258 return { "type": "ranged-attack", "cursor": "action-ranged-attack", "target": target }; 259 return false; 260 }, 139 261 "specificness": 10, 140 262 }, 141 263 142 "heal": 264 "heal": 143 265 { 144 266 "execute": function(target, action, selection, queued) 145 267 { 146 Engine.PostNetworkCommand({ "type": "heal", "entities": selection, "target": action.target, "queued": queued});268 Engine.PostNetworkCommand({ "type": "heal", "entities": selection, "target": action.target, "queued": queued }); 147 269 Engine.GuiInterfaceCall("PlaySound", { "name": "order_heal", "entity": selection[0] }); 148 270 return true; 149 271 }, … … 176 298 return {"type": "heal", "cursor": "action-heal", "target": target}; 177 299 return false; 178 300 }, 179 "specificness": 7,301 "specificness": 6, 180 302 }, 181 303 182 "build": 304 "build": 183 305 { 184 306 "execute": function(target, action, selection, queued) 185 307 { … … 202 324 "specificness": 3, 203 325 }, 204 326 205 "repair": 327 "repair": 206 328 { 207 329 "execute": function(target, action, selection, queued) 208 330 { … … 230 352 return {"type": "build", "cursor": "action-repair", "target": target}; 231 353 return false; 232 354 }, 233 "specificness": 1 1,355 "specificness": 12, 234 356 }, 235 357 236 "gather": 358 "gather": 237 359 { 238 360 "execute": function(target, action, selection, queued) 239 361 { … … 260 382 "specificness": 1, 261 383 }, 262 384 263 "returnresource": 385 "returnresource": 264 386 { 265 387 "execute": function(target, action, selection, queued) 266 388 { … … 297 419 "specificness": 2, 298 420 }, 299 421 300 "setup-trade-route": 422 "setup-trade-route": 301 423 { 302 424 "execute": function(target, action, selection, queued) 303 425 { … … 362 484 "specificness": 0, 363 485 }, 364 486 365 "garrison": 487 "garrison": 366 488 { 367 489 "execute": function(target, action, selection, queued) 368 490 { … … 376 498 return false; 377 499 if (!playerCheck(entState, targetState, ["Player", "MutualAlly"])) 378 500 return false; 379 vartooltip = sprintf(translate("Current garrison: %(garrisoned)s/%(capacity)s"), {501 let tooltip = sprintf(translate("Current garrison: %(garrisoned)s/%(capacity)s"), { 380 502 garrisoned: targetState.garrisonHolder.garrisonedEntitiesCount, 381 503 capacity: targetState.garrisonHolder.capacity 382 504 }); 383 varextraCount = 0;505 let extraCount = 0; 384 506 if (entState.garrisonHolder) 385 507 extraCount += entState.garrisonHolder.garrisonedEntitiesCount; 386 508 if (targetState.garrisonHolder.garrisonedEntitiesCount + extraCount >= targetState.garrisonHolder.capacity) 387 509 tooltip = "[color=\"orange\"]" + tooltip + "[/color]"; 388 if (MatchesClassList(entState.identity.classes, targetState.garrisonHolder.allowedClasses)) 389 return {"possible": true, "tooltip": tooltip}; 510 if (MatchesClassList(entState.identity.classes, targetState.garrisonHolder.allowedClasses) && 511 HasNeededAttackTypes(entState.attack, targetState.garrisonHolder.neededAttackTypes)) 512 return { "possible": true, "tooltip": tooltip }; 390 513 return false; 391 514 392 515 }, … … 409 532 "specificness": 20, 410 533 }, 411 534 412 "guard": 535 "guard": 413 536 { 414 537 "execute": function(target, action, selection, queued) 415 538 { … … 446 569 "specificness": 40, 447 570 }, 448 571 449 "remove-guard": 572 "remove-guard": 450 573 { 451 574 "execute": function(target, action, selection, queued) 452 575 { … … 471 594 "specificness": 41, 472 595 }, 473 596 474 "set-rallypoint": 597 "set-rallypoint": 475 598 { 476 599 "execute": function(target, action, selection, queued) 477 600 { 478 // if there is a position set in the action then use this so that when setting a 601 // if there is a position set in the action then use this so that when setting a 479 602 // rally point on an entity it is centered on that entity 480 603 if (action.position) 481 604 target = action.position; … … 507 630 cursor = "action-attack-move"; 508 631 } 509 632 633 if (Engine.HotkeyIsPressed("session.meleeattackmove")) 634 { 635 if (Engine.HotkeyIsPressed("session.meleeattackmoveUnit")) 636 var targetClasses = { "melee-attack": ["Unit"] }; 637 else 638 var targetClasses = { "melee-attack": ["Unit", "Structure"] }; 639 data.command = "melee-attack-walk"; 640 data.targetClasses = targetClasses; 641 cursor = "action-melee-attack"; 642 } 643 644 if (Engine.HotkeyIsPressed("session.rangedattackmove")) 645 { 646 if (Engine.HotkeyIsPressed("session.rangedattackmoveUnit")) 647 var targetClasses = { "ranged-attack": ["Unit"] }; 648 else 649 var targetClasses = { "ranged-attack": ["Unit", "Structure"] }; 650 data.command = "ranged-attack-walk"; 651 data.targetClasses = targetClasses; 652 cursor = "action-ranged-attack"; 653 } 654 510 655 if (targetState.garrisonHolder && playerCheck(entState, targetState, ["Player", "MutualAlly"])) 511 656 { 512 657 data.command = "garrison"; … … 610 755 return false; 611 756 return {"type": "set-rallypoint", "cursor": actionInfo.cursor, "data": actionInfo.data, "tooltip": actionInfo.tooltip, "position": actionInfo.position}; 612 757 }, 613 "specificness": 6,758 "specificness": 5, 614 759 }, 615 760 616 "unset-rallypoint": 761 "unset-rallypoint": 617 762 { 618 763 "execute": function(target, action, selection, queued) 619 764 { … … 654 799 "specificness": 11, 655 800 }, 656 801 657 "none": 802 "none": 658 803 { 659 804 "execute": function(target, action, selection, queued) 660 805 { … … 668 813 * Info and actions for the entity commands 669 814 * Currently displayed in the bottom of the central panel 670 815 */ 671 var g_EntityCommands = 816 var g_EntityCommands = 672 817 { 673 818 // Unload 674 819 "unload-all": { -
binaries/data/mods/public/simulation/ai/common-api/entity.js
749 749 return false; 750 750 }, 751 751 752 isCapturable: function() { return this.get("Capturable") !== undefined; }, 752 753 isGarrisonHolder: function() { return this.get("GarrisonHolder") !== undefined; }, 753 754 garrisoned: function() { return this._entity.garrisoned; }, 754 755 canGarrisonInside: function() { return this._entity.garrisoned.length < this.garrisonMax(); }, … … 759 760 }, 760 761 761 762 moveToRange: function(x, z, min, max, queued = false) { 762 Engine.PostCommand(PlayerID,{ "type": "walk-to-range", "entities": [this.id()], "x": x, "z": z, "min": min, "max": max, "queued": queued });763 Engine.PostCommand(PlayerID,{ "type": "walk-to-range", "entities": [this.id()], "x": x, "z": z, "min": min, "max": max, "queued": queued }); 763 764 return this; 764 765 }, 765 766 766 attackMove: function(x, z, targetClasses, queued = false) {767 Engine.PostCommand(PlayerID,{"type": "attack-walk", "entities": [this.id()], "x": x, "z": z, "targetClasses": targetClasses, " queued": queued});767 attackMove: function(x, z, targetClasses, prefAttackType = "undefined", queued = false) { // hack 768 Engine.PostCommand(PlayerID,{"type": "attack-walk", "entities": [this.id()], "x": x, "z": z, "targetClasses": targetClasses, "prefAttackType": prefAttackType, "queued": queued}); 768 769 return this; 769 770 }, 770 771 … … 798 799 return this; 799 800 }, 800 801 801 attack: function(unitId, allowCapture = true, queued = false) {802 Engine.PostCommand(PlayerID,{"type": "attack", "entities": [this.id()], "target": unitId, " allowCapture": allowCapture, "queued": queued});802 attack: function(unitId, prefAttackType = undefined, queued = false) { 803 Engine.PostCommand(PlayerID,{"type": "attack", "entities": [this.id()], "target": unitId, "prefAttackType": prefAttackType, "queued": queued}); 803 804 return this; 804 805 }, 805 806 -
binaries/data/mods/public/simulation/ai/common-api/entitycollection.js
153 153 m.EntityCollection.prototype.attackMove = function(x, z, targetClasses, queued) 154 154 { 155 155 queued = queued || false; 156 Engine.PostCommand(PlayerID, {"type": "attack-walk", "entities": this.toIdArray(), "x": x, "z": z, "targetClasses": targetClasses, "queued": queued});156 Engine.PostCommand(PlayerID, { "type": "attack-walk", "entities": this.toIdArray(), "x": x, "z": z, "targetClasses": targetClasses, "queued": queued, "prefAttackType": undefined }); // hack 157 157 return this; 158 158 }; 159 159 … … 180 180 m.EntityCollection.prototype.attack = function(unit) 181 181 { 182 182 var unitId = unit; 183 Engine.PostCommand(PlayerID, {"type": "attack", "entities": this.toIdArray(), "target": unitId, "queued": false});183 Engine.PostCommand(PlayerID, { "type": "attack", "entities": this.toIdArray(), "target": unitId, "queued": false, "prefAttackType": undefined }); 184 184 return this; 185 185 }; 186 186 -
binaries/data/mods/public/simulation/ai/petra/attackPlan.js
1078 1078 continue; 1079 1079 if (!ent.isIdle()) 1080 1080 continue; 1081 ent.attack(attacker.id(), !this.noCapture.has(attacker.id()));1081 ent.attack(attacker.id(), m.getPrefAttackType(ent, attacker, this.noCapture)); 1082 1082 } 1083 1083 break; 1084 1084 } … … 1314 1314 { 1315 1315 if (this.isSiegeUnit(gameState, ent)) // needed as mauryan elephants are not filtered out 1316 1316 continue; 1317 ent.attack(attacker.id(), !this.noCapture.has(attacker.id()));1317 ent.attack(attacker.id(), m.getPrefAttackType(ent, attacker, this.noCapture)); 1318 1318 ent.setMetadata(PlayerID, "lastAttackPlanUpdateTime", time); 1319 1319 } 1320 1320 // And if this attacker is a non-ranged siege unit and our unit also, attack it 1321 1321 if (this.isSiegeUnit(gameState, attacker) && attacker.hasClass("Melee") && ourUnit.hasClass("Melee")) 1322 1322 { 1323 ourUnit.attack(attacker.id(), false);1323 ourUnit.attack(attacker.id(), "Melee"); 1324 1324 ourUnit.setMetadata(PlayerID, "lastAttackPlanUpdateTime", time); 1325 1325 } 1326 1326 } … … 1331 1331 let collec = this.unitCollection.filter(API3.Filters.byClass("Melee")).filterNearest(ourUnit.position(), 5); 1332 1332 for (let ent of collec.values()) 1333 1333 { 1334 ent.attack(attacker.id(), false);1334 ent.attack(attacker.id(), m.getPrefAttackType(ent, attacker, this.noCapture)); 1335 1335 ent.setMetadata(PlayerID, "lastAttackPlanUpdateTime", time); 1336 1336 } 1337 1337 } … … 1350 1350 continue; 1351 1351 } 1352 1352 } 1353 ourUnit.attack(attacker.id(), !this.noCapture.has(attacker.id()));1353 ourUnit.attack(attacker.id(), m.getPrefAttackType(ourUnit, attacker, this.noCapture)); 1354 1354 ourUnit.setMetadata(PlayerID, "lastAttackPlanUpdateTime", time); 1355 1355 } 1356 1356 } … … 1521 1521 return valb - vala; 1522 1522 }); 1523 1523 if (mStruct[0].hasClass("Gates")) 1524 ent.attack(mStruct[0].id(), false);1524 ent.attack(mStruct[0].id(), m.getPrefAttackType(ent, mStruct[0], this.noCapture)); 1525 1525 else 1526 1526 { 1527 1527 let rand = Math.floor(Math.random() * mStruct.length * 0.2); 1528 let newTarget Id = mStruct[rand].id();1529 ent.attack(newTarget Id, !this.noCapture.has(newTargetId));1528 let newTarget = mStruct[rand]; 1529 ent.attack(newTarget.id(), m.getPrefAttackType(ent, newTarget, this.noCapture)); 1530 1530 } 1531 1531 } 1532 1532 else … … 1584 1584 return valb - vala; 1585 1585 }); 1586 1586 let rand = Math.floor(Math.random() * mUnit.length * 0.1); 1587 let newTarget Id = mUnit[rand].id();1588 ent.attack(newTarget Id, !this.noCapture.has(newTargetId));1587 let newTarget = mUnit[rand]; 1588 ent.attack(newTarget.id(), m.getPrefAttackType(ent, newTarget, this.noCapture)); 1589 1589 } 1590 1590 else if (API3.SquareVectorDistance(this.targetPos, ent.position()) > 2500 ) 1591 1591 { … … 1628 1628 return valb - vala; 1629 1629 }); 1630 1630 if (mStruct[0].hasClass("Gates")) 1631 ent.attack(mStruct[0].id(), false);1631 ent.attack(mStruct[0].id(), m.getPrefAttackType(ent, mStruct[0], this.noCapture)); 1632 1632 else 1633 1633 { 1634 1634 let rand = Math.floor(Math.random() * mStruct.length * 0.2); 1635 let newTarget Id = mStruct[rand].id();1636 ent.attack(newTarget Id, !this.noCapture.has(newTargetId));1635 let newTarget = mStruct[rand]; 1636 ent.attack(newTarget.id(), m.getPrefAttackType(ent, newTarget, this.noCapture)); 1637 1637 } 1638 1638 } 1639 1639 else if (needsUpdate) // really nothing let's try to help our nearest unit … … 1640 1640 { 1641 1641 let distmin = Math.min(); 1642 1642 let attackerId; 1643 let attacker; 1643 1644 this.unitCollection.forEach( function (unit) { 1644 1645 if (!unit.position()) 1645 1646 return; … … 1649 1650 let dist = API3.SquareVectorDistance(unit.position(), ent.position()); 1650 1651 if (dist > distmin) 1651 1652 return; 1653 if (!gameState.getEntityById(unit.unitAIOrderData()[0].target)) 1654 return; 1652 1655 distmin = dist; 1653 1656 attackerId = unit.unitAIOrderData()[0].target; 1654 1657 attacker = gameState.getEntityById(attackerId); 1655 1658 }); 1656 1659 if (attackerId) 1657 ent.attack(attackerId, !this.noCapture.has(attackerId));1660 ent.attack(attackerId, m.getPrefAttackType(ent, attacker, this.noCapture)); 1658 1661 } 1659 1662 } 1660 1663 } … … 1828 1831 1829 1832 if (this.noCapture.has(targetId)) 1830 1833 { 1831 ent.attack(targetId, false);1834 ent.attack(targetId, m.getPrefAttackType(ent, target, this.noCapture)); 1832 1835 return true; 1833 1836 } 1834 1837 … … 1840 1843 if (target.hasClass("Siege") && target.hasClass("Melee")) 1841 1844 { 1842 1845 this.noCapture.add(targetId); 1843 ent.attack(targetId, false);1846 ent.attack(targetId, m.getPrefAttackType(ent, target, this.noCapture)); 1844 1847 return true; 1845 1848 } 1846 1849 … … 1857 1860 if (antiCapture >= this.captureStrength) 1858 1861 { 1859 1862 this.noCapture.add(targetId); 1860 ent.attack(targetId, false);1863 ent.attack(targetId, "primary"); // hack 1861 1864 return true; 1862 1865 } 1863 1866 … … 1866 1869 this.unitCollection.length < 2*target.garrisoned().length) 1867 1870 { 1868 1871 this.noCapture.add(targetId); 1869 ent.attack(targetId, false);1872 ent.attack(targetId, "primary"); // hack 1870 1873 return true; 1871 1874 } 1872 1875 -
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); … … 116 116 else if (orderData.length && orderData[0].target && orderData[0].attackType && orderData[0].attackType === "Capture") 117 117 { 118 118 let target = gameState.getEntityById(orderData[0].target); 119 if (target && !m.allowCapture(ent, target)) 120 ent.attack(orderData[0].target, false); 119 if (target) 120 { 121 let prefAttackType = m.getPrefAttackType(ent, target); 122 if (prefAttackType !== "Capture") 123 ent.attack(orderData[0].target, prefAttackType); 124 } 121 125 } 122 126 } 123 127 -
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 if (target.isCapturable() && (!target.isGarrisonHolder() || !target.garrisoned().length)) 91 return "Capture"; 92 return "primary"; 89 93 }; 90 94 91 95 // Makes the worker deposit the currently carried resources at the closest accessible dropsite -
binaries/data/mods/public/simulation/components/Attack.js
1 1 function Attack() {} 2 2 3 Attack.prototype.bonusesSchema = 3 Attack.prototype.bonusesSchema = 4 4 "<optional>" + 5 5 "<element name='Bonuses'>" + 6 6 "<zeroOrMore>" + … … 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 278 const cmpIdentity = Engine.QueryInterface(target, IID_Identity); 279 if (!cmpIdentity) 280 return undefined; 281 282 let cmpEntityPlayer = QueryOwnerInterface(this.entity); 283 let cmpTargetPlayer = QueryOwnerInterface(target); 284 if (!cmpTargetPlayer || !cmpEntityPlayer) 285 return false; 286 287 const targetClasses = cmpIdentity.GetClassesList(); 288 let cmpCapturable = QueryMiragedInterface(target, IID_Capturable); 289 if (targetClasses.indexOf("Domestic") == -1 && !cmpEntityPlayer.IsEnemy(cmpTargetPlayer.GetPlayerID()) && 290 (!cmpCapturable || !cmpCapturable.CanCapture(cmpEntityPlayer.GetPlayerID()))) 291 return false; 292 250 293 // Check if the relative height difference is larger than the attack range 251 294 // If the relative height is bigger, it means they will never be able to 252 295 // reach each other, no matter how close they come. 253 296 let heightDiff = Math.abs(cmpThisPosition.GetHeightOffset() - cmpTargetPosition.GetHeightOffset()); 254 297 255 const cmpIdentity = Engine.QueryInterface(target, IID_Identity);256 if (!cmpIdentity)257 return undefined;258 259 const targetClasses = cmpIdentity.GetClassesList();260 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 … … 294 334 Attack.prototype.GetPreference = function(target) 295 335 { 296 336 const cmpIdentity = Engine.QueryInterface(target, IID_Identity); 297 if (!cmpIdentity) 337 if (!cmpIdentity) 298 338 return undefined; 299 339 300 340 const targetClasses = cmpIdentity.GetClassesList(); … … 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) … … 344 382 let best = ["Ranged", "Melee", "Capture"]; 345 383 let types = this.GetAttackTypes(); 346 384 for (let attack of best) 347 if (types.indexOf(attack) != -1 )385 if (types.indexOf(attack) != -1 && (!prefAttackType || "no" + attack != prefAttackType)) 348 386 return attack; 349 387 return undefined; 350 388 } 351 389 352 390 let cmpIdentity = Engine.QueryInterface(target, IID_Identity); 353 if (!cmpIdentity) 391 if (!cmpIdentity) 354 392 return undefined; 355 393 394 // If we are visisble garrisoned always do ranged attack if we can. 395 let cmpPosition = Engine.QueryInterface(this.entity, IID_Position); 396 if (cmpPosition && cmpPosition.GetTurretParent() != INVALID_ENTITY) 397 return this.template.Ranged ? "Ranged" : undefined; 398 356 399 let targetClasses = cmpIdentity.GetClassesList(); 357 400 let isTargetClass = function (className) { return targetClasses.indexOf(className) != -1; }; 358 401 359 402 // Always slaughter domestic animals instead of using a normal attack 360 if (isTargetClass("Domestic") && this.template.Slaughter) 403 if (isTargetClass("Domestic") && this.template.Slaughter) 361 404 return "Slaughter"; 362 405 363 406 let attack = this; 364 let isAllowed = function (type) { return !attack.GetRestrictedClasses(type).some(isTargetClass) ; };407 let isAllowed = function (type) { return !attack.GetRestrictedClasses(type).some(isTargetClass) && (!prefAttackType || "no" + type != prefAttackType); }; 365 408 366 409 let types = this.GetAttackTypes().filter(isAllowed); 410 if (types.length === 0) 411 return undefined; 367 412 368 // check if the target is capturable 369 let captureIndex = types.indexOf("Capture"); 370 if (captureIndex != -1) 413 prefAttackType = this.GetAttackTypeFromOrder(prefAttackType); 414 if (prefAttackType && prefAttackType != "Capture" && types.indexOf(prefAttackType) != -1) 415 return prefAttackType; 416 else if (!prefAttackType || prefAttackType == "Capture") 371 417 { 372 let cmpCapturable = QueryMiragedInterface(target, IID_Capturable); 373 374 let cmpPlayer = QueryOwnerInterface(this.entity); 375 if (allowCapture && cmpPlayer && cmpCapturable && cmpCapturable.CanCapture(cmpPlayer.GetPlayerID())) 376 return "Capture"; 377 // not captureable, so remove this attack 378 types.splice(captureIndex, 1); 418 // check if the target is capturable 419 let captureIndex = types.indexOf("Capture"); 420 if (captureIndex != -1) 421 { 422 let cmpCapturable = QueryMiragedInterface(target, IID_Capturable); 423 let cmpPlayer = QueryOwnerInterface(this.entity); 424 if (cmpPlayer && cmpCapturable && cmpCapturable.CanCapture(cmpPlayer.GetPlayerID())) 425 return "Capture"; 426 // not captureable, so remove this attack 427 types.splice(captureIndex, 1); 428 } 379 429 } 380 430 381 let isPreferred = function (className) { return attack.GetPreferredClasses(className).some(isTargetClass); }; 382 let byPreference = function (a, b) { return (types.indexOf(a) + (isPreferred(a) ? types.length : 0) ) - (types.indexOf(b) + (isPreferred(b) ? types.length : 0) ); }; 431 // ignore charges for now: TODO implement these 432 let chargeIndex = types.indexOf("Charge"); 433 if (chargeIndex != -1) 434 types.splice(chargeIndex, 1); 383 435 384 return types.sort(byPreference).pop(); 436 if (types.length === 0) 437 return undefined; 438 439 // assume ranged and/or melee attack left 440 // TODO stop assuming that? 441 let meleeIndex = types.indexOf("Melee"); 442 let rangedIndex = types.indexOf("Ranged"); 443 if (meleeIndex != -1 && rangedIndex != -1) 444 { 445 if (this.HasTargetPreferredClass(types, targetClasses)) 446 { 447 let isPreferred = function (className) { return attack.GetPreferredClasses(className).some(isTargetClass); }; 448 let byPreference = function (a, b) { return (types.indexOf(a) + (isPreferred(a) ? types.length : 0) ) - (types.indexOf(b) + (isPreferred(b) ? types.length : 0) ); }; 449 450 return types.sort(byPreference).pop(); 451 } 452 453 if (!cmpPosition.IsInWorld()) 454 return undefined; 455 let selfPosition = cmpPosition.GetPosition2D(); 456 let cmpTargetPosition = Engine.QueryInterface(target, IID_Position); 457 if (!cmpTargetPosition || !cmpTargetPosition.IsInWorld()) 458 return undefined; 459 let targetPosition = cmpTargetPosition.GetPosition2D(); 460 if (targetPosition.distanceToSquared(selfPosition) <= Math.pow(this.GetChangeDistance(), 2)) 461 return "Melee"; 462 return "Ranged"; 463 } 464 else if (meleeIndex != -1) 465 return "Melee"; 466 else if (rangedIndex != -1) 467 return "Ranged"; 468 return types[0]; 385 469 }; 386 470 387 471 Attack.prototype.CompareEntitiesByPreference = function(a, b) … … 478 562 return attackBonus; 479 563 }; 480 564 565 Attack.prototype.GetAttackTypeFromOrder = function(prefAttackType) 566 { 567 let types = this.GetAttackTypes(); 568 for (let type of types) 569 if (this.template[type].AttackOrder && this.template[type].AttackOrder == prefAttackType) 570 return type; 571 return prefAttackType; 572 }; 573 574 Attack.prototype.HasTargetPreferredClass = function(types, targetClasses) 575 { 576 let isTargetClass = function (className) { return targetClasses.indexOf(className) != -1; }; 577 for (let type of types) 578 if (this.template[type].PreferredClasses && this.GetPreferredClasses(type).some(isTargetClass)) 579 return true; 580 return false 581 }; 582 481 583 // Returns a 2d random distribution scaled for a spread of scale 1. 482 584 // The current implementation is a 2d gaussian with sigma = 1 483 585 Attack.prototype.GetNormalDistribution = function(){ … … 486 588 let a = Math.random(); 487 589 let b = Math.random(); 488 590 489 let c = Math.sqrt(-2 *Math.log(a)) * Math.cos(2*Math.PI*b);490 let d = Math.sqrt(-2 *Math.log(a)) * Math.sin(2*Math.PI*b);591 let c = Math.sqrt(-2 * Math.log(a)) * Math.cos(2 * Math.PI * b); 592 let d = Math.sqrt(-2 * Math.log(a)) * Math.sin(2 * Math.PI * b); 491 593 492 594 return [c, d]; 493 595 }; … … 503 605 if (type == "Ranged") 504 606 { 505 607 let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); 506 let turnLength = cmpTimer.GetLatestTurnLength() /1000;608 let turnLength = cmpTimer.GetLatestTurnLength() / 1000; 507 609 // In the future this could be extended: 508 610 // * Obstacles like trees could reduce the probability of the target being hit 509 611 // * Obstacles like walls should block projectiles entirely … … 535 637 536 638 let horizDistance = targetPosition.horizDistanceTo(selfPosition); 537 639 538 // This is an approximation of the time ot the target, it assumes that the target has a constant radial539 // velocity, but since units move in straight lines this is not true. The exact value would be more540 // difficult to calculate and I think this is sufficiently accurate. (I tested and for cavalry it was640 // This is an approximation of the time to the target, it assumes that the target has a constant radial 641 // velocity, but since units move in straight lines this is not true. The exact value would be more 642 // difficult to calculate and I think this is sufficiently accurate. (I tested and for cavalry it was 541 643 // about 5% of the units radius out in the worst case) 542 644 let timeToTarget = horizDistance / (horizSpeed - radialSpeed); 543 645 … … 620 722 Attack.prototype.InterpolatedLocation = function(ent, lateness) 621 723 { 622 724 let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); 623 let turnLength = cmpTimer.GetLatestTurnLength() /1000;725 let turnLength = cmpTimer.GetLatestTurnLength() / 1000; 624 726 let cmpTargetPosition = Engine.QueryInterface(ent, IID_Position); 625 727 if (!cmpTargetPosition || !cmpTargetPosition.IsInWorld()) // TODO: handle dead target properly 626 728 return undefined; … … 658 760 let d = Vector3D.sub(point, targetPosition); 659 761 d = Vector2D.from3D(d).rotate(-angle); 660 762 661 return d.x < Math.abs(targetShape.width /2) && d.y < Math.abs(targetShape.depth/2);763 return d.x < Math.abs(targetShape.width / 2) && d.y < Math.abs(targetShape.depth / 2); 662 764 } 663 765 }; 664 766 … … 740 842 return; 741 843 742 844 for (let type of this.GetAttackTypes()) 743 if (msg.valueNames.indexOf("Attack/" +type+"/MaxRange") !== -1)845 if (msg.valueNames.indexOf("Attack/" + type + "/MaxRange") !== -1) 744 846 { 745 847 cmpUnitAI.UpdateRangeQueries(); 746 848 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 }; … … 1807 1808 if (!cmpAttack) 1808 1809 return false; 1809 1810 1810 let cmpEntityPlayer = QueryOwnerInterface(data.entity, IID_Player); 1811 let cmpTargetPlayer = QueryOwnerInterface(data.target, IID_Player); 1812 if (!cmpEntityPlayer || !cmpTargetPlayer) 1811 return cmpAttack.CanAttack(data.target); 1812 }; 1813 1814 GuiInterface.prototype.CanAttackWithType = function(player, data) 1815 { 1816 let cmpAttack = Engine.QueryInterface(data.entity, IID_Attack); 1817 if (!cmpAttack) 1813 1818 return false; 1814 1819 1815 // if the owner is an enemy, it's up to the attack component to decide 1816 if (cmpEntityPlayer.IsEnemy(cmpTargetPlayer.GetPlayerID())) 1817 return cmpAttack.CanAttack(data.target); 1818 1819 return false; 1820 return cmpAttack.CanAttack(data.target, data.type); 1820 1821 }; 1821 1822 1822 1823 /* … … 1963 1964 "GetTradingDetails": 1, 1964 1965 "CanCapture": 1, 1965 1966 "CanAttack": 1, 1967 "CanAttackWithType": 1, 1966 1968 "GetBatchTime": 1, 1967 1969 1968 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 return false; -
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>