Ticket #252: t252_secondattack_3.diff
File t252_secondattack_3.diff, 35.2 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-attack-move"; //TODO another cursor 580 } 509 581 510 582 if (targetState.garrisonHolder && playerCheck(entState, targetState, ["Player", "MutualAlly"])) 511 583 { -
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 cmpPlayer = QueryOwnerInterface(this.entity); 282 let cmpTargetPlayer =QueryOwnerInterface(target); 283 261 284 for (let type of this.GetAttackTypes()) 262 285 { 263 286 if (type == "Capture" && !QueryMiragedInterface(target, IID_Capturable)) 264 287 continue; 265 288 266 if (type == "Slaughter" && targetClasses.indexOf("Domestic") == -1) 289 if (type != "Slaughter" && cmpPlayer == cmpTargetPlayer) 290 291 if (type == "Slaughter", targetClasses.indexOf("Domestic") == -1) 267 292 continue; 268 293 269 294 if (heightDiff > this.GetRange(type).max) … … 288 313 return false; 289 314 }; 290 315 316 Attack.prototype.CanSecondAttack = function(target) 317 { 318 for (let type of this.GetAttackTypes()) 319 if (this.template[type].AttackOrder && this.template[type].AttackOrder == "secondary") 320 return this.CanAttack(target); 321 return false; 322 }; 291 323 /** 292 324 * Returns null if we have no preference or the lowest index of a preferred class. 293 325 */ … … 294 326 Attack.prototype.GetPreference = function(target) 295 327 { 296 328 const cmpIdentity = Engine.QueryInterface(target, IID_Identity); 297 if (!cmpIdentity) 329 if (!cmpIdentity) 298 330 return undefined; 299 331 300 332 const targetClasses = cmpIdentity.GetClassesList(); … … 327 359 if (type == "Slaughter") 328 360 continue; 329 361 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; 362 ret.min = Math.min(ret.min, range.min) 363 ret.max = Math.max(ret.max, range.max) 334 364 } 335 365 return ret; 336 366 }; 337 367 338 Attack.prototype.GetBestAttackAgainst = function(target, allowCapture)368 Attack.prototype.GetBestAttackAgainst = function(target, prefType) 339 369 { 340 370 let cmpFormation = Engine.QueryInterface(target, IID_Formation); 341 371 if (cmpFormation) … … 350 380 } 351 381 352 382 let cmpIdentity = Engine.QueryInterface(target, IID_Identity); 353 if (!cmpIdentity) 383 if (!cmpIdentity) 354 384 return undefined; 355 385 356 386 let targetClasses = cmpIdentity.GetClassesList(); … … 357 387 let isTargetClass = function (className) { return targetClasses.indexOf(className) != -1; }; 358 388 359 389 // Always slaughter domestic animals instead of using a normal attack 360 if (isTargetClass("Domestic") && this.template.Slaughter) 390 if (isTargetClass("Domestic") && this.template.Slaughter) 361 391 return "Slaughter"; 362 392 363 393 let attack = this; … … 365 395 366 396 let types = this.GetAttackTypes().filter(isAllowed); 367 397 368 // check if the target is capturable 369 let captureIndex = types.indexOf("Capture"); 370 if (captureIndex != -1) 398 if (prefType) 371 399 { 372 let cmpCapturable = QueryMiragedInterface(target, IID_Capturable); 400 if (types.indexOf(prefType) != -1) 401 return prefType 402 prefType = this.GetPrefAttack(types, prefType); 403 if (prefType && types.indexOf(prefType) != -1) 404 return prefType; 405 } 406 else 407 { 408 // check if the target is capturable 409 let captureIndex = types.indexOf("Capture"); 410 if (captureIndex != -1) 411 { 412 let cmpCapturable = QueryMiragedInterface(target, IID_Capturable); 373 413 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); 414 let cmpPlayer = QueryOwnerInterface(this.entity); 415 if (cmpPlayer && cmpCapturable && cmpCapturable.CanCapture(cmpPlayer.GetPlayerID())) 416 return "Capture"; 417 // not captureable, so remove this attack 418 types.splice(captureIndex, 1); 419 } 379 420 } 380 421 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) ); }; 422 // ignore charges for now: TODO implement these 423 let chargeIndex = types.indexOf("Charge"); 424 if (chargeIndex != -1) 425 types.splice(chargeIndex, 1); 383 426 384 return types.sort(byPreference).pop(); 427 // only ranged and/or melee attack left 428 // if one attacktype left choose this one 429 if (types.indexOf("Melee") == -1 || types.indexOf("Ranged") == -1) 430 return types[0]; 431 432 if (this.HasPreferredClasses(types)) 433 { 434 let isPreferred = function (className) { return attack.GetPreferredClasses(className).some(isTargetClass); }; 435 let byPreference = function (a, b) { return (types.indexOf(a) + (isPreferred(a) ? types.length : 0) ) - (types.indexOf(b) + (isPreferred(b) ? types.length : 0) ); }; 436 437 return types.sort(byPreference).pop(); 438 } 439 // assume ranged and melee attack 440 // TODO stop assuming that? 441 let cmpPosition = Engine.QueryInterface(this.entity, IID_Position); 442 if (!cmpPosition || !cmpPosition.IsInWorld()) 443 return undefined; 444 let selfPosition = cmpPosition.GetPosition(); 445 let cmpTargetPosition = Engine.QueryInterface(target, IID_Position); 446 if (!cmpTargetPosition || !cmpTargetPosition.IsInWorld()) 447 return undefined; 448 let targetPosition = cmpTargetPosition.GetPosition(); 449 let horizDistance = targetPosition.horizDistanceTo(selfPosition); 450 if (horizDistance <= this.template.ChangeDistance) 451 return "Melee"; 452 return "Ranged" 453 385 454 }; 386 455 387 456 Attack.prototype.CompareEntitiesByPreference = function(a, b) … … 478 547 return attackBonus; 479 548 }; 480 549 550 // Returns preferred attack type if exists 551 Attack.prototype.GetPrefAttack = function(types, pref) 552 { 553 for (let type of types) 554 if (this.template[type].AttackOrder && pref == this.template[type].AttackOrder) 555 return type; 556 return undefined; 557 }; 558 559 Attack.prototype.HasPreferredClasses = function(types) 560 { 561 for (let type of types) 562 if (this.template[type].PreferredClasses) 563 return true; 564 return false 565 }; 481 566 // Returns a 2d random distribution scaled for a spread of scale 1. 482 567 // The current implementation is a 2d gaussian with sigma = 1 483 568 Attack.prototype.GetNormalDistribution = function(){ … … 486 571 let a = Math.random(); 487 572 let b = Math.random(); 488 573 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);574 let c = Math.sqrt(-2 * Math.log(a)) * Math.cos(2 * Math.PI * b); 575 let d = Math.sqrt(-2 * Math.log(a)) * Math.sin(2 * Math.PI * b); 491 576 492 577 return [c, d]; 493 578 }; … … 503 588 if (type == "Ranged") 504 589 { 505 590 let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); 506 let turnLength = cmpTimer.GetLatestTurnLength() /1000;591 let turnLength = cmpTimer.GetLatestTurnLength() / 1000; 507 592 // In the future this could be extended: 508 593 // * Obstacles like trees could reduce the probability of the target being hit 509 594 // * Obstacles like walls should block projectiles entirely … … 535 620 536 621 let horizDistance = targetPosition.horizDistanceTo(selfPosition); 537 622 538 // This is an approximation of the time ot the target, it assumes that the target has a constant radial 539 // 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 623 // This is an approximation of the time ot the target, it assumes that the target has a constant radial 624 // velocity, but since units move in straight lines this is not true. The exact value would be more 625 // difficult to calculate and I think this is sufficiently accurate. (I tested and for cavalry it was 541 626 // about 5% of the units radius out in the worst case) 542 627 let timeToTarget = horizDistance / (horizSpeed - radialSpeed); 543 628 … … 620 705 Attack.prototype.InterpolatedLocation = function(ent, lateness) 621 706 { 622 707 let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); 623 let turnLength = cmpTimer.GetLatestTurnLength() /1000;708 let turnLength = cmpTimer.GetLatestTurnLength() / 1000; 624 709 let cmpTargetPosition = Engine.QueryInterface(ent, IID_Position); 625 710 if (!cmpTargetPosition || !cmpTargetPosition.IsInWorld()) // TODO: handle dead target properly 626 711 return undefined; … … 658 743 let d = Vector3D.sub(point, targetPosition); 659 744 d = Vector2D.from3D(d).rotate(-angle); 660 745 661 return d.x < Math.abs(targetShape.width /2) && d.y < Math.abs(targetShape.depth/2);746 return d.x < Math.abs(targetShape.width / 2) && d.y < Math.abs(targetShape.depth / 2); 662 747 } 663 748 }; 664 749 … … 740 825 return; 741 826 742 827 for (let type of this.GetAttackTypes()) 743 if (msg.valueNames.indexOf("Attack/" +type+"/MaxRange") !== -1)828 if (msg.valueNames.indexOf("Attack/" + type + "/MaxRange") !== -1) 744 829 { 745 830 cmpUnitAI.UpdateRangeQueries(); 746 831 return; -
binaries/data/mods/public/simulation/components/GuiInterface.js
1760 1760 return false; 1761 1761 }; 1762 1762 1763 GuiInterface.prototype.CanSecondAttack = function(player, data) 1764 { 1765 let cmpAttack = Engine.QueryInterface(data.entity, IID_Attack); 1766 if (!cmpAttack) 1767 return false; 1768 1769 return cmpAttack.CanSecondAttack(data.target) 1770 }; 1771 1763 1772 /* 1764 1773 * Returns batch build time. 1765 1774 */ … … 1903 1912 "GetTradingDetails": 1, 1904 1913 "CanCapture": 1, 1905 1914 "CanAttack": 1, 1915 "CanSecondAttack": 1, 1906 1916 "GetBatchTime": 1, 1907 1917 1908 1918 "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>