Ticket #252: t252_secondattack_4.diff
File t252_secondattack_4.diff, 38.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 secondattack = Alt ; Modifier to secondary attack instead of another action 283 secondattackmove = Alt ; Modifier to secondary attackmove when clicking on a point 284 secondattackmoveUnit = "Alt+Q"; Modifier to secondary attackmove targeting only units when clicking on a point 282 285 guard = "G" ; Modifier to escort/guard when clicking on unit/building 283 286 queue = Shift ; Modifier to queue unit orders instead of replacing 284 287 batchtrain = Shift ; Modifier to train units in batches -
binaries/data/mods/public/art/actors/units/persians/champion_unit_1.xml
14 14 <animation event="0.5" file="infantry/sword/attack/isw_s_def_06.psa" name="attack_melee" speed="80"/> 15 15 <animation event="0.5" file="infantry/sword/attack/isw_s_def_06.psa" name="attack_melee" speed="80"/> 16 16 <animation event="0.5" file="infantry/sword/attack/isw_s_off_05.psa" name="attack_melee" speed="80"/> 17 <animation event="0.84" file="biped/inf_arch_atk_a.psa" load="0.16" name="attack_ranged" speed="90"/> 17 18 <animation file="infantry/sword/move/run/isw_s_off_01.psa" name="Run" speed="25"/> 18 19 <animation file="infantry/sword/move/run/isw_s_def_02.psa" name="Run" speed="30"/> 19 20 <animation file="infantry/sword/move/run/isw_s_em_03.psa" name="Run" speed="30"/> … … 28 29 <props> 29 30 <prop actor="props/units/heads/head_pers_tiara.xml" attachpoint="head"/> 30 31 <prop actor="props/units/heads/pers_kidaris_tied.xml" attachpoint="helmet"/> 31 <prop actor="props/units/shields/gerron_b.xml" attachpoint="shield"/>32 <prop actor="props/units/weapons/spear_ball.xml" attachpoint="r_hand"/>33 32 <prop actor="props/units/pers_quiver_back.xml" attachpoint="back"/> 34 33 </props> 35 34 </variant> … … 39 38 <textures><texture file="skeletal/pers_su1_anusiya_iron.dds" name="baseTex"/></textures> 40 39 </variant> 41 40 </group> 41 <group> 42 <variant name="attack_melee"> 43 <props> 44 <prop actor="props/units/shields/gerron_b.xml" attachpoint="shield"/> 45 <prop actor="props/units/weapons/spear_ball.xml" attachpoint="r_hand"/> 46 </props> 47 </variant> 48 <variant name="attack_ranged"> 49 <props> 50 <prop actor="props/units/weapons/bow_recurve.xml" attachpoint="l_hand"/> 51 <prop actor="props/units/weapons/arrow_back.xml" attachpoint="loaded-r_hand"/> 52 <prop actor="props/units/weapons/arrow_front.xml" attachpoint="projectile"/> 53 </props> 54 </variant> 55 </group> 42 56 <material>player_trans.xml</material> 43 57 </actor> -
binaries/data/mods/public/art/textures/cursors/action-second-attack-move.txt
1 1 1 -
binaries/data/mods/public/art/textures/cursors/action-second-attack.txt
1 1 1 -
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.secondattackmove")) 210 { 211 data.command = "attack-walk"; 212 data.targetClasses = Engine.HotkeyIsPressed("session.secondattackmoveUnit") ? { "attack": ["Unit"] } : { "attack": ["Unit", "Structure"] }; 213 cursor = "action-second-attack-move"; 214 } 208 215 return { "possible": true, "data": data, "cursor": cursor }; 209 216 } 210 217 -
binaries/data/mods/public/gui/session/unit_actions.js
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, "prefType": "primary" }); 69 69 Engine.GuiInterfaceCall("PlaySound", { "name": "order_walk", "entity": selection[0] }); 70 70 return true; 71 71 }, … … 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 "second-attack-move": // TODO is this one needed? 91 { 92 "execute": function(target, action, selection, queued) 93 { 94 if (Engine.HotkeyIsPressed("session.secondattackmoveUnit")) 95 var targetClasses = { "secondattack": ["Unit"] }; 96 else 97 var targetClasses = { "secondattack": ["Unit", "Structure"] }; 98 99 Engine.PostNetworkCommand({ "type": "attack-walk", "entities": selection, "x": target.x, "z": target.z, "targetClasses": targetClasses, "queued": queued, "prefType": "secondary" }); 100 Engine.GuiInterfaceCall("PlaySound", { "name": "order_walk", "entity": selection[0] }); 101 return true; 102 }, 103 "getActionInfo": function(entState, targetState) 104 { 105 if (!entState.attack || !targetState.hitpoints) 106 return false; 107 return { "possible": Engine.GuiInterfaceCall("CanSecondAttack", { "entity": entState.id, "target": targetState.id }) }; 108 }, 109 "hotkeyActionCheck": function(target, selection) 110 { 111 // Work out whether at least part of the selection have UnitAI 112 var haveUnitAI = selection.some(function(ent) { 113 var entState = GetEntityState(ent); 114 return entState && entState.unitAI; 115 }); 116 if (haveUnitAI && Engine.HotkeyIsPressed("session.secondattackmove") && getActionInfo("second-attack-move", target).possible) 117 return { "type": "second-attack-move", "cursor": "action-second-attack-move" }; // TODO another cursor 118 return false; 119 }, 120 "specificness": 32, 121 }, 122 90 123 "capture": 91 124 { 92 125 "execute": function(target, action, selection, queued) 93 126 { 94 Engine.PostNetworkCommand({ "type": "attack", "entities": selection, "target": action.target, "allowCapture": true, "queued": queued});127 Engine.PostNetworkCommand({ "type": "attack", "entities": selection, "target": action.target, "prefType": "Capture", "queued": queued }); 95 128 Engine.GuiInterfaceCall("PlaySound", { "name": "order_attack", "entity": selection[0] }); 96 129 return true; 97 130 }, … … 99 132 { 100 133 if (!entState.attack || !targetState.hitpoints) 101 134 return false; 102 return { "possible": Engine.GuiInterfaceCall("CanCapture", {"entity": entState.id, "target": targetState.id})};135 return { "possible": Engine.GuiInterfaceCall("CanCapture", { "entity": entState.id, "target": targetState.id }) }; 103 136 }, 104 137 "actionCheck": function(target) 105 138 { 106 139 if (getActionInfo("capture", target).possible) 107 return { "type": "capture", "cursor": "action-capture", "target": target};140 return { "type": "capture", "cursor": "action-capture", "target": target }; 108 141 return false; 109 142 }, 110 143 "specificness": 9, … … 114 147 { 115 148 "execute": function(target, action, selection, queued) 116 149 { 117 Engine.PostNetworkCommand({ "type": "attack", "entities": selection, "target": action.target, "queued": queued, "allowCapture": false});150 Engine.PostNetworkCommand({ "type": "attack", "entities": selection, "target": action.target, "queued": queued, "prefType": "primary" }); 118 151 Engine.GuiInterfaceCall("PlaySound", { "name": "order_attack", "entity": selection[0] }); 119 152 return true; 120 153 }, … … 122 155 { 123 156 if (!entState.attack || !targetState.hitpoints) 124 157 return false; 125 return { "possible": Engine.GuiInterfaceCall("CanAttack", {"entity": entState.id, "target": targetState.id})};158 return { "possible": Engine.GuiInterfaceCall("CanAttack", {"entity": entState.id, "target": targetState.id }) }; 126 159 }, 127 160 "hotkeyActionCheck": function(target) 128 161 { 129 162 if (Engine.HotkeyIsPressed("session.attack") && getActionInfo("attack", target).possible) 130 return { "type": "attack", "cursor": "action-attack", "target": target};163 return { "type": "attack", "cursor": "action-attack", "target": target }; 131 164 return false; 132 165 }, 133 166 "actionCheck": function(target) 134 167 { 135 168 if (getActionInfo("attack", target).possible) 136 return { "type": "attack", "cursor": "action-attack", "target": target};169 return { "type": "attack", "cursor": "action-attack", "target": target }; 137 170 return false; 138 171 }, 139 172 "specificness": 10, 140 173 }, 141 174 175 "second-attack": 176 { 177 "execute": function(target, action, selection, queued) 178 { 179 Engine.PostNetworkCommand({ "type": "attack", "entities": selection, "target": action.target, "queued": queued, "prefType": "secondary" }); 180 Engine.GuiInterfaceCall("PlaySound", { "name": "order_attack", "entity": selection[0] }); 181 return true; 182 }, 183 "getActionInfo": function(entState, targetState) 184 { 185 if (!entState.attack || !targetState.hitpoints) 186 return false; 187 return { "possible": Engine.GuiInterfaceCall("CanSecondAttack", {"entity": entState.id, "target": targetState.id }) }; 188 }, 189 "hotkeyActionCheck": function(target) 190 { 191 if (Engine.HotkeyIsPressed("session.secondattack") && getActionInfo("second-attack", target).possible) 192 return { "type": "second-attack", "cursor": "action-second-attack", "target": target }; 193 return false; 194 }, 195 "actionCheck": function(target) 196 { 197 if (getActionInfo("second-attack", target).possible) 198 return { "type": "second-attack", "cursor": "action-second-attack", "target": target }; 199 return false; 200 }, 201 "specificness": 15, 202 }, 203 142 204 "heal": 143 205 { 144 206 "execute": function(target, action, selection, queued) 145 207 { 146 Engine.PostNetworkCommand({ "type": "heal", "entities": selection, "target": action.target, "queued": queued});208 Engine.PostNetworkCommand({ "type": "heal", "entities": selection, "target": action.target, "queued": queued }); 147 209 Engine.GuiInterfaceCall("PlaySound", { "name": "order_heal", "entity": selection[0] }); 148 210 return true; 149 211 }, … … 506 568 data.targetClasses = targetClasses; 507 569 cursor = "action-attack-move"; 508 570 } 571 if (Engine.HotkeyIsPressed("session.secondattackmove")) 572 { 573 if (Engine.HotkeyIsPressed("session.secondattackmoveUnit")) 574 var targetClasses = { "second-attack": ["Unit"] }; 575 else 576 var targetClasses = { "second-attack": ["Unit", "Structure"] }; 577 data.command = "second-attack-walk"; 578 data.targetClasses = targetClasses; 579 cursor = "action-second-attack-move"; 580 } 509 581 510 582 if (targetState.garrisonHolder && playerCheck(entState, targetState, ["Player", "MutualAlly"])) 511 583 { -
binaries/data/mods/public/simulation/ai/common-api/entity.js
792 792 return this; 793 793 }, 794 794 795 attack: function(unitId, allowCapture = true, queued = false) {796 Engine.PostCommand(PlayerID,{"type": "attack", "entities": [this.id()], "target": unitId, " allowCapture": allowCapture, "queued": queued});795 attack: function(unitId, prefType = "Capture", queued = false) { 796 Engine.PostCommand(PlayerID,{"type": "attack", "entities": [this.id()], "target": unitId, "prefType": prefType, "queued": queued}); 797 797 return this; 798 798 }, 799 799 -
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.prefType(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))119 if (target && !m.prefType(ent, target)) 120 120 ent.attack(orderData[0].target, false); 121 121 } 122 122 } -
binaries/data/mods/public/simulation/ai/petra/entityExtend.js
82 82 }; 83 83 84 84 // Decide if we should try to capture or destroy 85 m.allowCapture = function(ent, target) 85 // TODO make this function less hacky 86 m.prefType = function(ent, target) 86 87 { 87 88 return !target.hasClass("Siege") || !ent.hasClass("Melee") || 88 !target.isGarrisonHolder() || !target.garrisoned().length ;89 !target.isGarrisonHolder() || !target.garrisoned().length ? "Capture": "primary"; 89 90 }; 90 91 91 92 // 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>" + … … 219 243 Attack.prototype.GetPreferredClasses = function(type) 220 244 { 221 245 if (this.template[type] && this.template[type].PreferredClasses && 222 this.template[type].PreferredClasses._string) 223 { 246 this.template[type].PreferredClasses._string) 224 247 return this.template[type].PreferredClasses._string.split(/\s+/); 225 }226 248 return []; 227 249 }; 228 250 … … 229 251 Attack.prototype.GetRestrictedClasses = function(type) 230 252 { 231 253 if (this.template[type] && this.template[type].RestrictedClasses && 232 this.template[type].RestrictedClasses._string) 233 { 254 this.template[type].RestrictedClasses._string) 234 255 return this.template[type].RestrictedClasses._string.split(/\s+/); 235 }236 256 return []; 237 257 }; 238 258 … … 253 273 let heightDiff = Math.abs(cmpThisPosition.GetHeightOffset() - cmpTargetPosition.GetHeightOffset()); 254 274 255 275 const cmpIdentity = Engine.QueryInterface(target, IID_Identity); 256 if (!cmpIdentity) 276 if (!cmpIdentity) 257 277 return undefined; 258 278 259 279 const targetClasses = cmpIdentity.GetClassesList(); 260 280 281 let cmpEntityPlayer = QueryOwnerInterface(this.entity); 282 let cmpTargetPlayer = QueryOwnerInterface(target); 283 if (!cmpTargetPlayer || !cmpEntityPlayer) 284 return false; 285 261 286 for (let type of this.GetAttackTypes()) 262 287 { 263 288 if (type == "Capture" && !QueryMiragedInterface(target, IID_Capturable)) 264 289 continue; 265 290 291 if (targetClasses.indexOf("Domestic") == -1 && !cmpEntityPlayer.IsEnemy(cmpTargetPlayer.GetPlayerID())) 292 continue; 293 266 294 if (type == "Slaughter" && targetClasses.indexOf("Domestic") == -1) 267 295 continue; 268 296 … … 288 316 return false; 289 317 }; 290 318 319 Attack.prototype.CanSecondAttack = function(target) 320 { 321 for (let type of this.GetAttackTypes()) 322 if (this.template[type].AttackOrder && this.template[type].AttackOrder == "secondary") 323 return this.CanAttack(target); 324 return false; 325 }; 291 326 /** 292 327 * Returns null if we have no preference or the lowest index of a preferred class. 293 328 */ … … 294 329 Attack.prototype.GetPreference = function(target) 295 330 { 296 331 const cmpIdentity = Engine.QueryInterface(target, IID_Identity); 297 if (!cmpIdentity) 332 if (!cmpIdentity) 298 333 return undefined; 299 334 300 335 const targetClasses = cmpIdentity.GetClassesList(); … … 327 362 if (type == "Slaughter") 328 363 continue; 329 364 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; 365 ret.min = Math.min(ret.min, range.min) 366 ret.max = Math.max(ret.max, range.max) 334 367 } 335 368 return ret; 336 369 }; 337 370 338 Attack.prototype.GetBestAttackAgainst = function(target, allowCapture)371 Attack.prototype.GetBestAttackAgainst = function(target, prefType) 339 372 { 340 373 let cmpFormation = Engine.QueryInterface(target, IID_Formation); 341 374 if (cmpFormation) … … 350 383 } 351 384 352 385 let cmpIdentity = Engine.QueryInterface(target, IID_Identity); 353 if (!cmpIdentity) 386 if (!cmpIdentity) 354 387 return undefined; 355 388 356 389 let targetClasses = cmpIdentity.GetClassesList(); … … 357 390 let isTargetClass = function (className) { return targetClasses.indexOf(className) != -1; }; 358 391 359 392 // Always slaughter domestic animals instead of using a normal attack 360 if (isTargetClass("Domestic") && this.template.Slaughter) 393 if (isTargetClass("Domestic") && this.template.Slaughter) 361 394 return "Slaughter"; 362 395 363 396 let attack = this; … … 365 398 366 399 let types = this.GetAttackTypes().filter(isAllowed); 367 400 368 // check if the target is capturable 369 let captureIndex = types.indexOf("Capture"); 370 if (captureIndex != -1) 401 if (prefType) 371 402 { 372 let cmpCapturable = QueryMiragedInterface(target, IID_Capturable); 403 if (types.indexOf(prefType) != -1) 404 return prefType 405 prefType = this.GetPrefAttack(types, prefType); 406 if (prefType && types.indexOf(prefType) != -1) 407 return prefType; 408 } 409 else 410 { 411 // check if the target is capturable 412 let captureIndex = types.indexOf("Capture"); 413 if (captureIndex != -1) 414 { 415 let cmpCapturable = QueryMiragedInterface(target, IID_Capturable); 373 416 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); 417 let cmpPlayer = QueryOwnerInterface(this.entity); 418 if (cmpPlayer && cmpCapturable && cmpCapturable.CanCapture(cmpPlayer.GetPlayerID())) 419 return "Capture"; 420 // not captureable, so remove this attack 421 types.splice(captureIndex, 1); 422 } 379 423 } 380 424 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) ); }; 425 // ignore charges for now: TODO implement these 426 let chargeIndex = types.indexOf("Charge"); 427 if (chargeIndex != -1) 428 types.splice(chargeIndex, 1); 383 429 384 return types.sort(byPreference).pop(); 430 // only ranged and/or melee attack left 431 // if one attacktype left choose this one 432 if (types.indexOf("Melee") == -1 || types.indexOf("Ranged") == -1) 433 return types[0]; 434 435 if (this.HasPreferredClasses(types)) 436 { 437 let isPreferred = function (className) { return attack.GetPreferredClasses(className).some(isTargetClass); }; 438 let byPreference = function (a, b) { return (types.indexOf(a) + (isPreferred(a) ? types.length : 0) ) - (types.indexOf(b) + (isPreferred(b) ? types.length : 0) ); }; 439 440 return types.sort(byPreference).pop(); 441 } 442 // assume ranged and melee attack 443 // TODO stop assuming that? 444 let cmpPosition = Engine.QueryInterface(this.entity, IID_Position); 445 if (!cmpPosition || !cmpPosition.IsInWorld()) 446 return undefined; 447 let selfPosition = cmpPosition.GetPosition(); 448 let cmpTargetPosition = Engine.QueryInterface(target, IID_Position); 449 if (!cmpTargetPosition || !cmpTargetPosition.IsInWorld()) 450 return undefined; 451 let targetPosition = cmpTargetPosition.GetPosition(); 452 let horizDistance = targetPosition.horizDistanceTo(selfPosition); 453 if (horizDistance <= this.template.ChangeDistance) 454 return "Melee"; 455 return "Ranged" 456 385 457 }; 386 458 387 459 Attack.prototype.CompareEntitiesByPreference = function(a, b) … … 478 550 return attackBonus; 479 551 }; 480 552 553 // Returns preferred attack type if exists 554 Attack.prototype.GetPrefAttack = function(types, pref) 555 { 556 for (let type of types) 557 if (this.template[type].AttackOrder && pref == this.template[type].AttackOrder) 558 return type; 559 return undefined; 560 }; 561 562 Attack.prototype.HasPreferredClasses = function(types) 563 { 564 for (let type of types) 565 if (this.template[type].PreferredClasses) 566 return true; 567 return false 568 }; 481 569 // Returns a 2d random distribution scaled for a spread of scale 1. 482 570 // The current implementation is a 2d gaussian with sigma = 1 483 571 Attack.prototype.GetNormalDistribution = function(){ … … 486 574 let a = Math.random(); 487 575 let b = Math.random(); 488 576 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);577 let c = Math.sqrt(-2 * Math.log(a)) * Math.cos(2 * Math.PI * b); 578 let d = Math.sqrt(-2 * Math.log(a)) * Math.sin(2 * Math.PI * b); 491 579 492 580 return [c, d]; 493 581 }; … … 503 591 if (type == "Ranged") 504 592 { 505 593 let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); 506 let turnLength = cmpTimer.GetLatestTurnLength() /1000;594 let turnLength = cmpTimer.GetLatestTurnLength() / 1000; 507 595 // In the future this could be extended: 508 596 // * Obstacles like trees could reduce the probability of the target being hit 509 597 // * Obstacles like walls should block projectiles entirely … … 535 623 536 624 let horizDistance = targetPosition.horizDistanceTo(selfPosition); 537 625 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 more 540 // difficult to calculate and I think this is sufficiently accurate. (I tested and for cavalry it was 626 // This is an approximation of the time to the target, it assumes that the target has a constant radial 627 // velocity, but since units move in straight lines this is not true. The exact value would be more 628 // difficult to calculate and I think this is sufficiently accurate. (I tested and for cavalry it was 541 629 // about 5% of the units radius out in the worst case) 542 630 let timeToTarget = horizDistance / (horizSpeed - radialSpeed); 543 631 … … 620 708 Attack.prototype.InterpolatedLocation = function(ent, lateness) 621 709 { 622 710 let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); 623 let turnLength = cmpTimer.GetLatestTurnLength() /1000;711 let turnLength = cmpTimer.GetLatestTurnLength() / 1000; 624 712 let cmpTargetPosition = Engine.QueryInterface(ent, IID_Position); 625 713 if (!cmpTargetPosition || !cmpTargetPosition.IsInWorld()) // TODO: handle dead target properly 626 714 return undefined; … … 658 746 let d = Vector3D.sub(point, targetPosition); 659 747 d = Vector2D.from3D(d).rotate(-angle); 660 748 661 return d.x < Math.abs(targetShape.width /2) && d.y < Math.abs(targetShape.depth/2);749 return d.x < Math.abs(targetShape.width / 2) && d.y < Math.abs(targetShape.depth / 2); 662 750 } 663 751 }; 664 752 … … 740 828 return; 741 829 742 830 for (let type of this.GetAttackTypes()) 743 if (msg.valueNames.indexOf("Attack/" +type+"/MaxRange") !== -1)831 if (msg.valueNames.indexOf("Attack/" + type + "/MaxRange") !== -1) 744 832 { 745 833 cmpUnitAI.UpdateRangeQueries(); 746 834 return; -
binaries/data/mods/public/simulation/components/GuiInterface.js
1748 1748 if (!cmpAttack) 1749 1749 return false; 1750 1750 1751 let cmpEntityPlayer = QueryOwnerInterface(data.entity, IID_Player); 1752 let cmpTargetPlayer = QueryOwnerInterface(data.target, IID_Player); 1753 if (!cmpEntityPlayer || !cmpTargetPlayer) 1751 return cmpAttack.CanAttack(data.target); 1752 }; 1753 1754 GuiInterface.prototype.CanSecondAttack = function(player, data) 1755 { 1756 let cmpAttack = Engine.QueryInterface(data.entity, IID_Attack); 1757 if (!cmpAttack) 1754 1758 return false; 1755 1759 1756 // if the owner is an enemy, it's up to the attack component to decide 1757 if (cmpEntityPlayer.IsEnemy(cmpTargetPlayer.GetPlayerID())) 1758 return cmpAttack.CanAttack(data.target); 1759 1760 return false; 1760 return cmpAttack.CanSecondAttack(data.target) 1761 1761 }; 1762 1762 1763 1763 /* … … 1903 1903 "GetTradingDetails": 1, 1904 1904 "CanCapture": 1, 1905 1905 "CanAttack": 1, 1906 "CanSecondAttack": 1, 1906 1907 "GetBatchTime": 1, 1907 1908 1908 1909 "IsMapRevealed": 1, -
binaries/data/mods/public/simulation/components/UnitAI.js
416 416 } 417 417 418 418 // Work out how to attack the given target 419 var type = this.GetBestAttackAgainst(this.order.data.target, this.order.data.allowCapture);419 let type = this.GetBestAttackAgainst(this.order.data.target, this.order.data.prefType); 420 420 if (!type) 421 421 { 422 422 // Oops, we can't attack at all … … 583 583 return; 584 584 } 585 585 586 this.PushOrderFront("Attack", { "target": this.order.data.target, "force": false, "hunting": true, " allowCapture": false});586 this.PushOrderFront("Attack", { "target": this.order.data.target, "force": false, "hunting": true, "prefType": undefined }); 587 587 return; 588 588 } 589 589 … … 838 838 }, 839 839 840 840 "Order.Attack": function(msg) { 841 vartarget = msg.data.target;842 var allowCapture = msg.data.allowCapture;843 varcmpTargetUnitAI = Engine.QueryInterface(target, IID_UnitAI);841 let target = msg.data.target; 842 let prefType = msg.data.prefType; 843 let cmpTargetUnitAI = Engine.QueryInterface(target, IID_UnitAI); 844 844 if (cmpTargetUnitAI && cmpTargetUnitAI.IsFormationMember()) 845 845 target = cmpTargetUnitAI.GetFormationController(); 846 846 … … 859 859 this.FinishOrder(); 860 860 return; 861 861 } 862 this.CallMemberFunction("Attack", [target, false, allowCapture]);862 this.CallMemberFunction("Attack", [target, false, prefType]); 863 863 if (cmpAttack.CanAttackAsFormation()) 864 864 this.SetNextState("COMBAT.ATTACKING"); 865 865 else … … 914 914 return; 915 915 } 916 916 917 this.PushOrderFront("Attack", { "target": msg.data.target, "hunting": true, " allowCapture": false});917 this.PushOrderFront("Attack", { "target": msg.data.target, "hunting": true, "prefType": undefined }); 918 918 return; 919 919 } 920 920 … … 1151 1151 1152 1152 "MoveCompleted": function(msg) { 1153 1153 var cmpAttack = Engine.QueryInterface(this.entity, IID_Attack); 1154 this.CallMemberFunction("Attack", [this.order.data.target, false, this.order.data. allowCapture]);1154 this.CallMemberFunction("Attack", [this.order.data.target, false, this.order.data.prefType]); 1155 1155 if (cmpAttack.CanAttackAsFormation()) 1156 1156 this.SetNextState("COMBAT.ATTACKING"); 1157 1157 else … … 1162 1162 "ATTACKING": { 1163 1163 // Wait for individual members to finish 1164 1164 "enter": function(msg) { 1165 vartarget = this.order.data.target;1166 var allowCapture = this.order.data.allowCapture;1165 let target = this.order.data.target; 1166 let prefType = this.order.data.prefType; 1167 1167 // Check if we are already in range, otherwise walk there 1168 1168 if (!this.CheckTargetAttackRange(target, target)) 1169 1169 { … … 1170 1170 if (this.TargetIsAlive(target) && this.CheckTargetVisible(target)) 1171 1171 { 1172 1172 this.FinishOrder(); 1173 this.PushOrderFront("Attack", { "target": target, "force": false, " allowCapture": allowCapture });1173 this.PushOrderFront("Attack", { "target": target, "force": false, "prefType": prefType }); 1174 1174 return true; 1175 1175 } 1176 1176 this.FinishOrder(); … … 1186 1186 }, 1187 1187 1188 1188 "Timer": function(msg) { 1189 vartarget = this.order.data.target;1190 var allowCapture = this.order.data.allowCapture;1189 let target = this.order.data.target; 1190 let prefType = this.order.data.prefType; 1191 1191 // Check if we are already in range, otherwise walk there 1192 1192 if (!this.CheckTargetAttackRange(target, target)) 1193 1193 { … … 1194 1194 if (this.TargetIsAlive(target) && this.CheckTargetVisible(target)) 1195 1195 { 1196 1196 this.FinishOrder(); 1197 this.PushOrderFront("Attack", { "target": target, "force": false, " allowCapture": allowCapture });1197 this.PushOrderFront("Attack", { "target": target, "force": false, "prefType": prefType }); 1198 1198 return; 1199 1199 } 1200 1200 this.FinishOrder(); … … 1406 1406 1407 1407 // target the unit 1408 1408 if (this.CheckTargetVisible(msg.data.attacker)) 1409 this.PushOrderFront("Attack", { "target": msg.data.attacker, "force": false, " allowCapture": true});1409 this.PushOrderFront("Attack", { "target": msg.data.attacker, "force": false, "prefType": undefined }); 1410 1410 else 1411 1411 { 1412 1412 var cmpPosition = Engine.QueryInterface(msg.data.attacker, IID_Position); … … 4535 4535 return distance < range; 4536 4536 }; 4537 4537 4538 UnitAI.prototype.GetBestAttackAgainst = function(target, allowCapture)4538 UnitAI.prototype.GetBestAttackAgainst = function(target, prefType) 4539 4539 { 4540 4540 var cmpAttack = Engine.QueryInterface(this.entity, IID_Attack); 4541 4541 if (!cmpAttack) 4542 4542 return undefined; 4543 return cmpAttack.GetBestAttackAgainst(target, allowCapture);4543 return cmpAttack.GetBestAttackAgainst(target, prefType); 4544 4544 }; 4545 4545 4546 4546 UnitAI.prototype.GetAttackBonus = function(type, target) … … 4562 4562 if (!target) 4563 4563 return false; 4564 4564 4565 this.PushOrderFront("Attack", { "target": target, "force": false, "forceResponse": forceResponse, " allowCapture": true});4565 this.PushOrderFront("Attack", { "target": target, "force": false, "forceResponse": forceResponse, "prefType": undefined }); 4566 4566 return true; 4567 4567 }; 4568 4568 … … 4575 4575 { 4576 4576 var target = ents.find(target => 4577 4577 this.CanAttack(target, forceResponse) 4578 && this.CheckTargetDistanceFromHeldPosition(target, IID_Attack, this.GetBestAttackAgainst(target, true ))4578 && this.CheckTargetDistanceFromHeldPosition(target, IID_Attack, this.GetBestAttackAgainst(target, true, undefined)) 4579 4579 && (this.GetStance().respondChaseBeyondVision || this.CheckTargetIsInVisionRange(target)) 4580 4580 ); 4581 4581 if (!target) 4582 4582 return false; 4583 4583 4584 this.PushOrderFront("Attack", { "target": target, "force": false, "forceResponse": forceResponse, " allowCapture": true});4584 this.PushOrderFront("Attack", { "target": target, "force": false, "forceResponse": forceResponse, "prefType": undefined }); 4585 4585 return true; 4586 4586 }; 4587 4587 … … 4978 4978 * to a player order, and so is forced. 4979 4979 * If targetClasses is given, only entities matching the targetClasses can be attacked. 4980 4980 */ 4981 UnitAI.prototype.WalkAndFight = function(x, z, targetClasses, queued )4981 UnitAI.prototype.WalkAndFight = function(x, z, targetClasses, queued, prefType) 4982 4982 { 4983 this.AddOrder("WalkAndFight", { "x": x, "z": z, "targetClasses": targetClasses, "force": true }, queued);4983 this.AddOrder("WalkAndFight", { "x": x, "z": z, "targetClasses": targetClasses, "force": true, "prefType": prefType }, queued); 4984 4984 }; 4985 4985 4986 4986 /** … … 5001 5001 /** 5002 5002 * Adds attack order to the queue, forced by the player. 5003 5003 */ 5004 UnitAI.prototype.Attack = function(target, queued, allowCapture)5004 UnitAI.prototype.Attack = function(target, queued, prefType) 5005 5005 { 5006 5006 if (!this.CanAttack(target)) 5007 5007 { … … 5013 5013 this.WalkToTarget(target, queued); 5014 5014 return; 5015 5015 } 5016 this.AddOrder("Attack", { "target": target, "force": true, " allowCapture": allowCapture}, queued);5016 this.AddOrder("Attack", { "target": target, "force": true, "prefType": prefType }, queued); 5017 5017 }; 5018 5018 5019 5019 /** … … 5430 5430 if (targetClasses.vetoEntities && targetClasses.vetoEntities[targ]) 5431 5431 continue; 5432 5432 } 5433 this.PushOrderFront("Attack", { "target": targ, "force": true, " allowCapture": true});5433 this.PushOrderFront("Attack", { "target": targ, "force": true, "prefType": undefined }); 5434 5434 return true; 5435 5435 } 5436 5436 } … … 5456 5456 if (targetClasses.vetoEntities && targetClasses.vetoEntities[targ]) 5457 5457 continue; 5458 5458 } 5459 this.PushOrderFront("Attack", { "target": targ, "force": true, " allowCapture": true});5459 this.PushOrderFront("Attack", { "target": targ, "force": true, "prefType": undefined }); 5460 5460 return true; 5461 5461 } 5462 5462 return false; -
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.prefType); 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.prefType); 174 173 }); 175 174 }, 176 175 -
binaries/data/mods/public/simulation/templates/template_unit_champion_infantry_spearman.xml
5 5 <Pierce op="add">3</Pierce> 6 6 </Armour> 7 7 <Attack> 8 <ChangeDistance>20</ChangeDistance> 8 9 <Melee> 10 <AttackOrder>primary</AttackOrder> 9 11 <Hack>6.0</Hack> 10 12 <Pierce>5.0</Pierce> 11 13 <Crush>0.0</Crush> … … 18 20 </BonusCavMelee> 19 21 </Bonuses> 20 22 </Melee> 23 <Ranged> 24 <AttackOrder>secondary</AttackOrder> 25 <Hack>0</Hack> 26 <Pierce>6.0</Pierce> 27 <Crush>0</Crush> 28 <MaxRange>72.0</MaxRange> 29 <MinRange>0.0</MinRange> 30 <ProjectileSpeed>120.0</ProjectileSpeed> 31 <PrepareTime>1000</PrepareTime> 32 <RepeatTime>1000</RepeatTime> 33 <Spread>2.0</Spread> 34 </Ranged> 21 35 <Charge> 22 36 <Hack>15.0</Hack> 23 37 <Pierce>40.0</Pierce>