Ticket #252: t252_secondattack_19.diff
File t252_secondattack_19.diff, 66.8 KB (added by , 8 years ago) |
---|
-
binaries/data/config/default.cfg
offscreen = Alt ; Include o 276 276 9 = 9 277 277 278 278 [hotkey.session] 279 279 kill = Delete ; Destroy selected units 280 280 stop = "H" ; Stop the current action 281 attack = Ctrl ; Modifier to attack instead of another action (eg capture)282 attackmove = Ctrl ; Modifier to attackmove when clicking on a point283 attackmoveUnit = "Ctrl+Q" ; Modifier to attackmove targeting only units when clicking on a point (should contain the attackmove keys)281 attack = Ctrl ; Modifier to primary attack instead of another action (eg capture) 282 attackmove = Ctrl ; Modifier to primary attackmove when clicking on a point 283 attackmoveUnit = "Ctrl+Q" ; Modifier to primary attackmove targeting only units when clicking on a point (should contain the attackmove keys) 284 284 garrison = Ctrl ; Modifier to garrison when clicking on building 285 285 autorallypoint = Ctrl ; Modifier to set the rally point on the building itself 286 meleeattack = Alt ; Modifier to melee attack instead of another action 287 meleeattackmove = Alt ; Modifier to melee attackmove when clicking on a point 288 meleeattackmoveUnit = "Alt+Q"; Modifier to melee attackmove targeting only units when clicking on a point 289 rangedattack = space ; Modifier to ranged attack instead of another action 290 rangedattackmove = space ; Modifier to ranged attackmove when clicking on a point 291 rangedattackmoveUnit = "space+Q"; Modifier to ranged attackmove targeting only units when clicking on a point 286 292 guard = "G" ; Modifier to escort/guard when clicking on unit/building 287 293 queue = Shift ; Modifier to queue unit orders instead of replacing 288 294 batchtrain = Shift ; Modifier to train units in batches 289 295 massbarter = Shift ; Modifier to barter bunch of resources 290 296 masstribute = Shift ; Modifier to tribute bunch of resources -
binaries/data/mods/public/art/actors/units/persians/champion_unit_1.xml
40 40 </variant> 41 41 </group> 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/globalscripts/Templates.js
function MatchesClassList(classes, match 64 64 65 65 return false; 66 66 } 67 67 68 68 /** 69 * Check if entity has all needed attack types. 70 */ 71 function HasNeededAttackTypes(attack, neededAttackTypes) 72 { 73 if (!neededAttackTypes) 74 return true; 75 // transform the string to an array 76 if (typeof neededAttackTypes == "string") 77 neededAttackTypes = neededAttackTypes.split(/\s+/); 78 79 for (let type of neededAttackTypes) 80 if (!attack || attack.constructor === Object && !attack[type] || 81 attack.constructor === Array && attack.indexOf(type) == -1) 82 return false; 83 return true; 84 } 85 86 /** 69 87 * Get information about a template with or without technology modifications. 70 88 * @param template A valid template as returned by the template loader. 71 89 * @param player An optional player id to get the technology modifications 72 90 * of properties. 73 91 * @param auraTemplates An object in the form of {key: {auraName: "", auraDescription: ""}} -
binaries/data/mods/public/gui/common/tooltips.js
function getAttackTooltip(template) 144 144 return ""; 145 145 146 146 let attacks = []; 147 147 for (let type in template.attack) 148 148 { 149 if (type == "ChangeDistance") 150 continue; // not an attack type 149 151 if (type == "Slaughter") 150 152 continue; // Slaughter is used to kill animals, so do not show it. 151 153 if (type == "Charge") 152 154 continue; // Charging isn't implemented yet and shouldn't be displayed. 153 155 -
binaries/data/mods/public/gui/session/input.js
function getActionInfo(action, target) 203 203 { 204 204 data.command = "attack-walk"; 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 211 226 return { "possible": (action == "move" || action == "attack-move" || action == "remove-guard") }; 212 227 } … … function getActionInfo(action, target) 217 232 var targetState = GetExtendedEntityState(target); 218 233 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 } 235 250 return { "possible": false }; 236 251 } -
binaries/data/mods/public/gui/session/unit_actions.js
var unitActions = 56 56 if (!someUnitAI(selection) || !getActionInfo("move", target).possible) 57 57 return false; 58 58 59 59 return { "type": "move" }; 60 60 }, 61 "specificness": 1 2,61 "specificness": 13, 62 62 }, 63 63 64 64 "attack-move": 65 65 { 66 66 "execute": function(target, action, selection, queued) … … var unitActions = 75 75 "type": "attack-walk", 76 76 "entities": selection, 77 77 "x": target.x, 78 78 "z": target.z, 79 79 "targetClasses": targetClasses, 80 "queued": queued 80 "queued": queued, 81 "prefAttackType": "primary" 81 82 }); 82 83 83 84 Engine.GuiInterfaceCall("PlaySound", { 84 85 "name": "order_walk", 85 86 "entity": selection[0] … … var unitActions = 104 105 }; 105 106 }, 106 107 "specificness": 30, 107 108 }, 108 109 110 "melee-attack-move": 111 { 112 "execute": function(target, action, selection, queued) 113 { 114 let targetClasses; 115 if (Engine.HotkeyIsPressed("session.meleeattackmoveUnit")) 116 targetClasses = { "meleeattack": ["Unit"] }; 117 else 118 targetClasses = { "meleeattack": ["Unit", "Structure"] }; 119 120 Engine.PostNetworkCommand({ 121 "type": "attack-walk", 122 "entities": selection, 123 "x": target.x, 124 "z": target.z, 125 "targetClasses": targetClasses, 126 "queued": queued, 127 "prefAttackType": "Melee" 128 }); 129 130 Engine.GuiInterfaceCall("PlaySound", { 131 "name": "order_walk", 132 "entity": selection[0] 133 }); 134 135 return true; 136 }, 137 "getActionInfo": function(entState, targetState) 138 { 139 return { "possible": true }; 140 }, 141 "hotkeyActionCheck": function(target, selection) 142 { 143 if (someUnitAI(selection) && Engine.HotkeyIsPressed("session.meleeattackmove") && 144 getActionInfo("melee-attack-move", target).possible) 145 return { 146 "type": "melee-attack-move", 147 "cursor": "action-melee-attack" 148 }; 149 return false; 150 }, 151 "specificness": 31, 152 }, 153 154 "ranged-attack-move": 155 { 156 "execute": function(target, action, selection, queued) 157 { 158 let targetClasses; 159 if (Engine.HotkeyIsPressed("session.rangedattackmoveUnit")) 160 targetClasses = { "rangedattack": ["Unit"] }; 161 else 162 targetClasses = { "rangedattack": ["Unit", "Structure"] }; 163 164 Engine.PostNetworkCommand({ 165 "type": "attack-walk", 166 "entities": selection, 167 "x": target.x, 168 "z": target.z, 169 "targetClasses": targetClasses, 170 "queued": queued, 171 "prefAttackType": "Ranged" 172 }); 173 174 Engine.GuiInterfaceCall("PlaySound", { 175 "name": "order_walk", 176 "entity": selection[0] 177 }); 178 179 return true; 180 }, 181 "getActionInfo": function(entState, targetState) 182 { 183 return { "possible": true }; 184 }, 185 "hotkeyActionCheck": function(target, selection) 186 { 187 if (someUnitAI(selection) && Engine.HotkeyIsPressed("session.rangedattackmove") && 188 getActionInfo("ranged-attack-move", target).possible) 189 return { 190 "type": "ranged-attack-move", 191 "cursor": "action-ranged-attack" 192 }; 193 return false; 194 }, 195 "specificness": 32, 196 }, 197 109 198 "capture": 110 199 { 111 200 "execute": function(target, action, selection, queued) 112 201 { 113 202 Engine.PostNetworkCommand({ 114 203 "type": "attack", 115 204 "entities": selection, 116 205 "target": action.target, 117 206 "allowCapture": true, 118 "queued": queued 207 "queued": queued, 208 "prefAttackType": "Capture" 119 209 }); 120 210 121 211 Engine.GuiInterfaceCall("PlaySound", { 122 212 "name": "order_attack", 123 213 "entity": selection[0] … … var unitActions = 146 236 "type": "capture", 147 237 "cursor": "action-capture", 148 238 "target": target 149 239 }; 150 240 }, 151 "specificness": 9,241 "specificness": 7, 152 242 }, 153 243 154 244 "attack": 155 245 { 156 246 "execute": function(target, action, selection, queued) … … var unitActions = 158 248 Engine.PostNetworkCommand({ 159 249 "type": "attack", 160 250 "entities": selection, 161 251 "target": action.target, 162 252 "queued": queued, 163 "allowCapture": false 253 "allowCapture": false, 254 "prefAttackType": "primary" 164 255 }); 165 256 166 257 Engine.GuiInterfaceCall("PlaySound", { 167 258 "name": "order_attack", 168 259 "entity": selection[0] … … var unitActions = 203 294 "type": "attack", 204 295 "cursor": "action-attack", 205 296 "target": target 206 297 }; 207 298 }, 299 "specificness": 8, 300 }, 301 302 "melee-attack": 303 { 304 "execute": function(target, action, selection, queued) 305 { 306 Engine.PostNetworkCommand({ 307 "type": "attack", 308 "entities": selection, 309 "target": action.target, 310 "queued": queued, 311 "prefAttackType": "Melee" 312 }); 313 314 Engine.GuiInterfaceCall("PlaySound", { 315 "name": "order_attack", 316 "entity": selection[0] 317 }); 318 return true; 319 }, 320 "getActionInfo": function(entState, targetState) 321 { 322 if (!entState.attack || !targetState.hitpoints) 323 return false; 324 return { "possible": Engine.GuiInterfaceCall("CanAttackWithType", { 325 "entity": entState.id, 326 "target": targetState.id, 327 "type": "Melee" 328 }) }; 329 }, 330 "hotkeyActionCheck": function(target) 331 { 332 if (Engine.HotkeyIsPressed("session.meleeattack") && getActionInfo("melee-attack", target).possible) 333 return { 334 "type": "melee-attack", 335 "cursor": "action-melee-attack", 336 "target": target 337 }; 338 return false; 339 }, 340 "actionCheck": function(target) 341 { 342 if (getActionInfo("melee-attack", target).possible) 343 return { 344 "type": "melee-attack", 345 "cursor": "action-melee-attack", 346 "target": target 347 }; 348 return false; 349 }, 350 "specificness": 9, 351 }, 352 353 "ranged-attack": 354 { 355 "execute": function(target, action, selection, queued) 356 { 357 Engine.PostNetworkCommand({ 358 "type": "attack", 359 "entities": selection, 360 "target": action.target, 361 "queued": queued, 362 "prefAttackType": "Ranged" 363 }); 364 365 Engine.GuiInterfaceCall("PlaySound", { 366 "name": "order_attack", 367 "entity": selection[0] 368 }); 369 370 return true; 371 }, 372 "getActionInfo": function(entState, targetState) 373 { 374 if (!entState.attack || !targetState.hitpoints) // hack 375 return false; 376 return { "possible": Engine.GuiInterfaceCall("CanAttackWithType", { 377 "entity": entState.id, 378 "target": targetState.id, 379 "type": "Ranged" 380 }) }; 381 }, 382 "hotkeyActionCheck": function(target, selection) 383 { 384 if (Engine.HotkeyIsPressed("session.rangedattack") && getActionInfo("ranged-attack", target).possible) 385 return { 386 "type": "ranged-attack", 387 "cursor": "action-ranged-attack", 388 "target": target 389 }; 390 return false; 391 }, 392 "actionCheck": function(target, selection) 393 { 394 if (getActionInfo("ranged-attack", target).possible) 395 return { 396 "type": "ranged-attack", 397 "cursor": "action-ranged-attack", 398 "target": target 399 }; 400 return false; 401 }, 208 402 "specificness": 10, 209 403 }, 210 404 211 405 "heal": 212 406 { … … var unitActions = 253 447 "type": "heal", 254 448 "cursor": "action-heal", 255 449 "target": target 256 450 }; 257 451 }, 258 "specificness": 7,452 "specificness": 6, 259 453 }, 260 454 261 455 "build": 262 456 { 263 457 "execute": function(target, action, selection, queued) … … var unitActions = 353 547 "type": "build", 354 548 "cursor": "action-repair", 355 549 "target": target 356 550 }; 357 551 }, 358 "specificness": 1 1,552 "specificness": 12, 359 553 }, 360 554 361 555 "gather": 362 556 { 363 557 "execute": function(target, action, selection, queued) … … var unitActions = 593 787 extraCount += entState.garrisonHolder.garrisonedEntitiesCount; 594 788 595 789 if (targetState.garrisonHolder.garrisonedEntitiesCount + extraCount >= targetState.garrisonHolder.capacity) 596 790 tooltip = "[color=\"orange\"]" + tooltip + "[/color]"; 597 791 598 if (!MatchesClassList(entState.identity.classes, targetState.garrisonHolder.allowedClasses)) 599 return false; 792 if (MatchesClassList(entState.identity.classes, targetState.garrisonHolder.allowedClasses) && 793 HasNeededAttackTypes(entState.attack, targetState.garrisonHolder.neededAttackTypes)) 794 return { 795 "possible": true, 796 "tooltip": tooltip 797 }; 600 798 601 return { 602 "possible": true, 603 "tooltip": tooltip 604 }; 799 return { "possible": false }; 605 800 }, 606 801 "preSelectedActionCheck": function(target) 607 802 { 608 803 if (preSelectedAction != ACTION_GARRISON) 609 804 return false; … … var unitActions = 781 976 data.command = "attack-walk"; 782 977 data.targetClasses = targetClasses; 783 978 cursor = "action-attack-move"; 784 979 } 785 980 981 if (Engine.HotkeyIsPressed("session.meleeattackmove")) 982 { 983 let targetClasses; 984 if (Engine.HotkeyIsPressed("session.meleeattackmoveUnit")) 985 targetClasses = { "melee-attack": ["Unit"] }; 986 else 987 targetClasses = { "melee-attack": ["Unit", "Structure"] }; 988 data.command = "melee-attack-walk"; 989 data.targetClasses = targetClasses; 990 cursor = "action-melee-attack"; 991 } 992 993 if (Engine.HotkeyIsPressed("session.rangedattackmove")) 994 { 995 let targetClasses; 996 if (Engine.HotkeyIsPressed("session.rangedattackmoveUnit")) 997 targetClasses = { "ranged-attack": ["Unit"] }; 998 else 999 targetClasses = { "ranged-attack": ["Unit", "Structure"] }; 1000 data.command = "ranged-attack-walk"; 1001 data.targetClasses = targetClasses; 1002 cursor = "action-ranged-attack"; 1003 } 1004 786 1005 if (targetState.garrisonHolder && 787 1006 playerCheck(entState, targetState, ["Player", "MutualAlly"])) 788 1007 { 789 1008 data.command = "garrison"; 790 1009 data.target = targetState.id; … … var unitActions = 899 1118 "data": actionInfo.data, 900 1119 "tooltip": actionInfo.tooltip, 901 1120 "position": actionInfo.position 902 1121 }; 903 1122 }, 904 "specificness": 6,1123 "specificness": 5, 905 1124 }, 906 1125 907 1126 "unset-rallypoint": 908 1127 { 909 1128 "execute": function(target, action, selection, queued) -
binaries/data/mods/public/simulation/ai/common-api/entity.js
m.Entity = m.Class({ 744 744 if (r.split('.')[0] === type) 745 745 return true; 746 746 return false; 747 747 }, 748 748 749 isCapturable: function() { return this.get("Capturable") !== undefined; }, 749 750 isGarrisonHolder: function() { return this.get("GarrisonHolder") !== undefined; }, 750 751 garrisoned: function() { return this._entity.garrisoned; }, 751 752 canGarrisonInside: function() { return this._entity.garrisoned.length < this.garrisonMax(); }, 752 753 753 754 move: function(x, z, queued = false) { 754 755 Engine.PostCommand(PlayerID,{"type": "walk", "entities": [this.id()], "x": x, "z": z, "queued": queued }); 755 756 return this; 756 757 }, 757 758 758 759 moveToRange: function(x, z, min, max, queued = false) { 759 Engine.PostCommand(PlayerID,{ "type": "walk-to-range", "entities": [this.id()], "x": x, "z": z, "min": min, "max": max, "queued": queued });760 Engine.PostCommand(PlayerID,{ "type": "walk-to-range", "entities": [this.id()], "x": x, "z": z, "min": min, "max": max, "queued": queued }); 760 761 return this; 761 762 }, 762 763 763 attackMove: function(x, z, targetClasses, queued = false) {764 Engine.PostCommand(PlayerID,{"type": "attack-walk", "entities": [this.id()], "x": x, "z": z, "targetClasses": targetClasses, " queued": queued});764 attackMove: function(x, z, targetClasses, prefAttackType = "undefined", queued = false) { // hack 765 Engine.PostCommand(PlayerID,{"type": "attack-walk", "entities": [this.id()], "x": x, "z": z, "targetClasses": targetClasses, "prefAttackType": prefAttackType, "queued": queued}); 765 766 return this; 766 767 }, 767 768 768 769 // violent, aggressive, defensive, passive, standground 769 770 setStance: function(stance, queued = false) { … … m.Entity = m.Class({ 793 794 garrison: function(target, queued = false) { 794 795 Engine.PostCommand(PlayerID,{"type": "garrison", "entities": [this.id()], "target": target.id(),"queued": queued}); 795 796 return this; 796 797 }, 797 798 798 attack: function(unitId, allowCapture = true, queued = false) {799 Engine.PostCommand(PlayerID,{"type": "attack", "entities": [this.id()], "target": unitId, " allowCapture": allowCapture, "queued": queued});799 attack: function(unitId, prefAttackType = undefined, queued = false) { 800 Engine.PostCommand(PlayerID,{"type": "attack", "entities": [this.id()], "target": unitId, "prefAttackType": prefAttackType, "queued": queued}); 800 801 return this; 801 802 }, 802 803 803 804 // moveApart from a point in the opposite direction with a distance dist 804 805 moveApart: function(point, dist) { -
binaries/data/mods/public/simulation/ai/common-api/entitycollection.js
m.EntityCollection.prototype.move = func 154 154 }; 155 155 156 156 m.EntityCollection.prototype.attackMove = function(x, z, targetClasses, queued) 157 157 { 158 158 queued = queued || false; 159 Engine.PostCommand(PlayerID, {"type": "attack-walk", "entities": this.toIdArray(), "x": x, "z": z, "targetClasses": targetClasses, "queued": queued});159 Engine.PostCommand(PlayerID, { "type": "attack-walk", "entities": this.toIdArray(), "x": x, "z": z, "targetClasses": targetClasses, "queued": queued, "prefAttackType": undefined }); // hack 160 160 return this; 161 161 }; 162 162 163 163 m.EntityCollection.prototype.moveIndiv = function(x, z, queued) 164 164 { … … m.EntityCollection.prototype.destroy = f 180 180 return this; 181 181 }; 182 182 183 183 m.EntityCollection.prototype.attack = function(unitId) 184 184 { 185 Engine.PostCommand(PlayerID,{"type": "attack", "entities": this.toIdArray(), "target": unitId, "queued": false });185 Engine.PostCommand(PlayerID,{"type": "attack", "entities": this.toIdArray(), "target": unitId, "queued": false, "prefAttackType": undefined}); 186 186 return this; 187 187 }; 188 188 189 189 /** violent, aggressive, defensive, passive, standground */ 190 190 m.EntityCollection.prototype.setStance = function(stance) -
binaries/data/mods/public/simulation/ai/petra/attackPlan.js
m.AttackPlan.prototype.update = function 1235 1235 let collec = this.unitCollection.filter(API3.Filters.not(API3.Filters.byClass("Siege"))).filterNearest(ourUnit.position(), 5); 1236 1236 for (let ent of collec.values()) 1237 1237 { 1238 1238 if (this.isSiegeUnit(gameState, ent)) // needed as mauryan elephants are not filtered out 1239 1239 continue; 1240 ent.attack(attacker.id(), !this.noCapture.has(attacker.id()));1240 ent.attack(attacker.id(), m.getPrefAttackType(ent, attacker, this.noCapture)); 1241 1241 ent.setMetadata(PlayerID, "lastAttackPlanUpdateTime", time); 1242 1242 } 1243 1243 // And if this attacker is a non-ranged siege unit and our unit also, attack it 1244 1244 if (this.isSiegeUnit(gameState, attacker) && attacker.hasClass("Melee") && ourUnit.hasClass("Melee")) 1245 1245 { 1246 ourUnit.attack(attacker.id(), false);1246 ourUnit.attack(attacker.id(), "Melee"); 1247 1247 ourUnit.setMetadata(PlayerID, "lastAttackPlanUpdateTime", time); 1248 1248 } 1249 1249 } 1250 1250 else 1251 1251 { … … m.AttackPlan.prototype.update = function 1258 1258 else if (this.isSiegeUnit(gameState, attacker)) 1259 1259 { // if our unit is attacked by a siege unit, we'll send some melee units to help it. 1260 1260 let collec = this.unitCollection.filter(API3.Filters.byClass("Melee")).filterNearest(ourUnit.position(), 5); 1261 1261 for (let ent of collec.values()) 1262 1262 { 1263 ent.attack(attacker.id(), false);1263 ent.attack(attacker.id(), m.getPrefAttackType(ent, attacker, this.noCapture)); 1264 1264 ent.setMetadata(PlayerID, "lastAttackPlanUpdateTime", time); 1265 1265 } 1266 1266 } 1267 1267 else 1268 1268 { // if units are attacked, abandon their target (if it was a structure or a support) and retaliate … … m.AttackPlan.prototype.update = function 1277 1277 { 1278 1278 if (!target.hasClass("Ranged") || !attacker.hasClass("Melee")) 1279 1279 continue; 1280 1280 } 1281 1281 } 1282 ourUnit.attack(attacker.id(), !this.noCapture.has(attacker.id()));1282 ourUnit.attack(attacker.id(), m.getPrefAttackType(ourUnit, attacker, this.noCapture)); 1283 1283 ourUnit.setMetadata(PlayerID, "lastAttackPlanUpdateTime", time); 1284 1284 } 1285 1285 } 1286 1286 } 1287 1287 … … m.AttackPlan.prototype.update = function 1457 1457 else if (structb.hasClass("ConquestCritical")) 1458 1458 valb += 200; 1459 1459 return valb - vala; 1460 1460 }); 1461 1461 if (mStruct[0].hasClass("Gates")) 1462 ent.attack(mStruct[0].id(), false);1462 ent.attack(mStruct[0].id(), m.getPrefAttackType(ent, mStruct[0], this.noCapture)); 1463 1463 else 1464 1464 { 1465 1465 let rand = Math.floor(Math.random() * mStruct.length * 0.2); 1466 let newTarget Id = mStruct[rand].id();1467 ent.attack(newTarget Id, !this.noCapture.has(newTargetId));1466 let newTarget = mStruct[rand]; 1467 ent.attack(newTarget.id(), m.getPrefAttackType(ent, newTarget, this.noCapture)); 1468 1468 } 1469 1469 } 1470 1470 else 1471 1471 { 1472 1472 if (!ent.hasClass("Ranged")) … … m.AttackPlan.prototype.update = function 1520 1520 if (veto[unitB.id()]) 1521 1521 valb -= 20000; 1522 1522 return valb - vala; 1523 1523 }); 1524 1524 let rand = Math.floor(Math.random() * mUnit.length * 0.1); 1525 let newTarget Id = mUnit[rand].id();1526 ent.attack(newTarget Id, !this.noCapture.has(newTargetId));1525 let newTarget = mUnit[rand]; 1526 ent.attack(newTarget.id(), m.getPrefAttackType(ent, newTarget, this.noCapture)); 1527 1527 } 1528 1528 else if (this.isBlocked) 1529 1529 ent.attack(this.target.id(), false); 1530 1530 else if (API3.SquareVectorDistance(this.targetPos, ent.position()) > 2500 ) 1531 1531 { … … m.AttackPlan.prototype.update = function 1568 1568 else if (structb.hasClass("ConquestCritical")) 1569 1569 valb += 100; 1570 1570 return valb - vala; 1571 1571 }); 1572 1572 if (mStruct[0].hasClass("Gates")) 1573 ent.attack(mStruct[0].id(), false);1573 ent.attack(mStruct[0].id(), m.getPrefAttackType(ent, mStruct[0], this.noCapture)); 1574 1574 else 1575 1575 { 1576 1576 let rand = Math.floor(Math.random() * mStruct.length * 0.2); 1577 let newTarget Id = mStruct[rand].id();1578 ent.attack(newTarget Id, !this.noCapture.has(newTargetId));1577 let newTarget = mStruct[rand]; 1578 ent.attack(newTarget.id(), m.getPrefAttackType(ent, newTarget, this.noCapture)); 1579 1579 } 1580 1580 } 1581 1581 else if (needsUpdate) // really nothing let's try to help our nearest unit 1582 1582 { 1583 1583 let distmin = Math.min(); 1584 1584 let attackerId; 1585 let attacker; 1585 1586 this.unitCollection.forEach( function (unit) { 1586 1587 if (!unit.position()) 1587 1588 return; 1588 1589 if (unit.unitAIState().split(".")[1] !== "COMBAT" || !unit.unitAIOrderData().length || 1589 1590 !unit.unitAIOrderData()[0].target) 1590 1591 return; 1591 1592 let dist = API3.SquareVectorDistance(unit.position(), ent.position()); 1592 1593 if (dist > distmin) 1593 1594 return; 1595 if (!gameState.getEntityById(unit.unitAIOrderData()[0].target)) 1596 return; 1594 1597 distmin = dist; 1595 1598 attackerId = unit.unitAIOrderData()[0].target; 1599 attacker = gameState.getEntityById(attackerId); 1596 1600 }); 1597 1601 if (attackerId) 1598 ent.attack(attackerId, !this.noCapture.has(attackerId));1602 ent.attack(attackerId, m.getPrefAttackType(ent, attacker, this.noCapture)); 1599 1603 } 1600 1604 } 1601 1605 } 1602 1606 } 1603 1607 this.unitCollUpdateArray.splice(0, lgth); … … m.AttackPlan.prototype.UpdateTransportin 1646 1650 { 1647 1651 if (ent.getMetadata(PlayerID, "transport") !== undefined) 1648 1652 continue; 1649 1653 if (!ent.isIdle()) 1650 1654 continue; 1651 ent.attack(attacker.id(), !this.noCapture.has(attacker.id() ));1655 ent.attack(attacker.id(), !this.noCapture.has(attacker.id(), this.noCapture)); 1652 1656 } 1653 1657 break; 1654 1658 } 1655 1659 }; 1656 1660 … … m.AttackPlan.prototype.CheckCapture = fu 2004 2008 if (!target) 2005 2009 return false; 2006 2010 2007 2011 if (this.noCapture.has(targetId)) 2008 2012 { 2009 ent.attack(targetId, false);2013 ent.attack(targetId, m.getPrefAttackType(ent, target, this.noCapture)); 2010 2014 return true; 2011 2015 } 2012 2016 2013 2017 // Do not try to (re)capture an allied decaying structuring 2014 2018 if (gameState.isPlayerAlly(target.owner())) … … m.AttackPlan.prototype.CheckCapture = fu 2016 2020 2017 2021 // For the time being, do not try to capture rams 2018 2022 if (target.hasClass("Siege") && target.hasClass("Melee")) 2019 2023 { 2020 2024 this.noCapture.add(targetId); 2021 ent.attack(targetId, false);2025 ent.attack(targetId, m.getPrefAttackType(ent, target, this.noCapture)); 2022 2026 return true; 2023 2027 } 2024 2028 2025 2029 // TODO need to know how many units are currently capturing this target 2026 2030 // For the time being, we check on our full army … … m.AttackPlan.prototype.CheckCapture = fu 2033 2037 if (target.decaying()) 2034 2038 antiCapture -= target.territoryDecayRate(); 2035 2039 if (antiCapture >= this.captureStrength) 2036 2040 { 2037 2041 this.noCapture.add(targetId); 2038 ent.attack(targetId, false);2042 ent.attack(targetId, "primary"); // hack 2039 2043 return true; 2040 2044 } 2041 2045 2042 2046 // If the structure has defensive fire, require a minimal army size 2043 2047 if (target.hasDefensiveFire() && target.isGarrisonHolder() && target.garrisoned() && 2044 2048 this.unitCollection.length < 2*target.garrisoned().length) 2045 2049 { 2046 2050 this.noCapture.add(targetId); 2047 ent.attack(targetId, false);2051 ent.attack(targetId, "primary"); // hack 2048 2052 return true; 2049 2053 } 2050 2054 2051 2055 return true; 2052 2056 }; -
binaries/data/mods/public/simulation/ai/petra/defenseArmy.js
m.DefenseArmy.prototype.assignUnit = fun 75 75 let foeIndex = gameState.ai.accessibility.getAccessValue(foePosition); 76 76 if (ownIndex == foeIndex || ent.hasClass("Ship")) 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); 84 84 return true; 85 85 }; … … m.DefenseArmy.prototype.update = functio 113 113 if (!orderData.length && !ent.getMetadata(PlayerID, "transport")) 114 114 this.assignUnit(gameState, entId); 115 115 else if (orderData.length && orderData[0].target && orderData[0].attackType && orderData[0].attackType === "Capture") 116 116 { 117 117 let target = gameState.getEntityById(orderData[0].target); 118 if (target && !m.allowCapture(ent, target)) 119 ent.attack(orderData[0].target, false); 118 if (target) 119 { 120 let prefAttackType = m.getPrefAttackType(ent, target); 121 if (prefAttackType !== "Capture") 122 ent.attack(orderData[0].target, prefAttackType); 123 } 120 124 } 121 125 } 122 126 123 127 return this.onUpdate(gameState); 124 128 }; -
binaries/data/mods/public/simulation/ai/petra/entityExtend.js
m.getMaxStrength = function(ent, against 79 79 } 80 80 81 81 return strength * ent.maxHitpoints() / 100.0; 82 82 }; 83 83 84 /** Decide if we should try to capture or destroy */ 85 m.allowCapture = function(ent, target) 84 // Decide if we should try to capture, melee or range attack 85 // TODO make this function less hacky 86 m.getPrefAttackType = function(ent, target, noCapture) 86 87 { 87 return !target.hasClass("Siege") || !ent.hasClass("Melee") || 88 !target.isGarrisonHolder() || !target.garrisoned().length; 88 if (target.hasClass("Siege") || (noCapture && noCapture.has(target.id()))) 89 return ent.hasClass("Melee") ? "Melee" : "Range"; // Don't capture for now 90 91 if (target.isCapturable() && (!target.isGarrisonHolder() || !target.garrisoned().length)) 92 return "Capture"; 93 return "primary"; 89 94 }; 90 95 91 96 /** Makes the worker deposit the currently carried resources at the closest accessible dropsite */ 92 97 m.returnResources = function(gameState, ent) 93 98 { -
binaries/data/mods/public/simulation/components/Attack.js
Attack.prototype.restrictedClassesSchema 39 39 "</optional>"; 40 40 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>" + 48 50 "<MaxRange>4.0</MaxRange>" + 49 51 "<RepeatTime>1000</RepeatTime>" + … … Attack.prototype.Schema = 60 62 "</Bonuses>" + 61 63 "<RestrictedClasses datatype=\"tokens\">Champion</RestrictedClasses>" + 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>" + 68 71 "<MaxRange>44.0</MaxRange>" + 69 72 "<MinRange>20.0</MinRange>" + … … Attack.prototype.Schema = 101 104 "<Crush>0.0</Crush>" + 102 105 "<MaxRange>4.0</MaxRange>" + 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>" + 110 126 "<element name='Crush' a:help='Crush damage strength'><ref name='nonNegativeDecimal'/></element>" + 111 127 "<element name='MaxRange' a:help='Maximum attack range (in metres)'><ref name='nonNegativeDecimal'/></element>" + … … Attack.prototype.Schema = 118 134 "</interleave>" + 119 135 "</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>" + 126 150 "<element name='Crush' a:help='Crush damage strength'><ref name='nonNegativeDecimal'/></element>" + 127 151 "<element name='MaxRange' a:help='Maximum attack range (in metres)'><ref name='nonNegativeDecimal'/></element>" + … … Attack.prototype.GetRestrictedClasses = 227 251 return this.template[type].RestrictedClasses._string.split(/\s+/); 228 252 229 253 return []; 230 254 }; 231 255 232 Attack.prototype.CanAttack = function(target) 256 Attack.prototype.GetChangeDistance = function() 257 { 258 return +(this.template.ChangeDistance || 0); 259 }; 260 261 Attack.prototype.CanAttack = function(target, wantedType) 233 262 { 263 if (wantedType && !this.template[wantedType]) 264 return false; 265 234 266 let cmpFormation = Engine.QueryInterface(target, IID_Formation); 235 267 if (cmpFormation) 236 268 return true; 237 269 238 270 let cmpThisPosition = Engine.QueryInterface(this.entity, IID_Position); 239 271 let cmpTargetPosition = Engine.QueryInterface(target, IID_Position); 240 272 if (!cmpThisPosition || !cmpTargetPosition || !cmpThisPosition.IsInWorld() || !cmpTargetPosition.IsInWorld()) 241 273 return false; 242 274 243 // Check if the relative height difference is larger than the attack range244 // If the relative height is bigger, it means they will never be able to245 // reach each other, no matter how close they come.246 let heightDiff = Math.abs(cmpThisPosition.GetHeightOffset() - cmpTargetPosition.GetHeightOffset());247 248 275 const cmpIdentity = Engine.QueryInterface(target, IID_Identity); 249 276 if (!cmpIdentity) 250 277 return undefined; 251 278 279 let cmpEntityPlayer = QueryOwnerInterface(this.entity); 280 let cmpTargetPlayer = QueryOwnerInterface(target); 281 if (!cmpTargetPlayer || !cmpEntityPlayer) 282 return false; 283 252 284 const targetClasses = cmpIdentity.GetClassesList(); 253 285 286 let cmpCapturable = QueryMiragedInterface(target, IID_Capturable); 287 if (targetClasses.indexOf("Domestic") == -1 && !cmpEntityPlayer.IsEnemy(cmpTargetPlayer.GetPlayerID()) && 288 (!cmpCapturable || !cmpCapturable.CanCapture(cmpEntityPlayer.GetPlayerID()))) 289 return false; 290 291 // Check if the relative height difference is larger than the attack range 292 // If the relative height is bigger, it means they will never be able to 293 // reach each other, no matter how close they come. 294 let heightDiff = Math.abs(cmpThisPosition.GetHeightOffset() - cmpTargetPosition.GetHeightOffset()); 295 254 296 for (let type of this.GetAttackTypes()) 255 297 { 256 if (type == "Capture" && !QueryMiragedInterface(target, IID_Capturable)) 298 if (wantedType && type != wantedType) 299 continue; 300 301 if (type == "Capture" && !cmpCapturable) 257 302 continue; 258 303 259 304 if (type == "Slaughter" && targetClasses.indexOf("Domestic") == -1) 260 305 continue; 261 306 … … Attack.prototype.GetFullAttackRange = fu 315 360 ret.max = Math.max(ret.max, range.max); 316 361 } 317 362 return ret; 318 363 }; 319 364 320 Attack.prototype.GetBestAttackAgainst = function(target, allowCapture)365 Attack.prototype.GetBestAttackAgainst = function(target, prefAttackType) 321 366 { 322 367 let cmpFormation = Engine.QueryInterface(target, IID_Formation); 323 368 if (cmpFormation) 324 369 { 325 370 // TODO: Formation against formation needs review 326 371 let types = this.GetAttackTypes(); 327 return ["Ranged", "Melee", "Capture"].find(attack => types.indexOf(attack) != -1); 372 return ["Ranged", "Melee", "Capture"].find(attack => 373 types.indexOf(attack) != -1 && (!prefAttackType || "no" + attack != prefAttackType)); 328 374 } 329 375 330 376 let cmpIdentity = Engine.QueryInterface(target, IID_Identity); 331 377 if (!cmpIdentity) 332 378 return undefined; 333 379 380 // If we are visisble garrisoned always do ranged attack if we can. 381 let cmpPosition = Engine.QueryInterface(this.entity, IID_Position); 382 if (cmpPosition && cmpPosition.GetTurretParent() != INVALID_ENTITY) 383 return this.template.Ranged && "Ranged"; 384 334 385 let targetClasses = cmpIdentity.GetClassesList(); 335 386 let isTargetClass = className => targetClasses.indexOf(className) != -1; 336 387 337 388 // Always slaughter domestic animals instead of using a normal attack 338 389 if (isTargetClass("Domestic") && this.template.Slaughter) 339 390 return "Slaughter"; 340 391 341 392 let attack = this; 342 let types = this.GetAttackTypes().filter(type => attack.GetRestrictedClasses(type).every(isTargetClass)); 393 let types = this.GetAttackTypes().filter(type => 394 attack.GetRestrictedClasses(type).every(isTargetClass) && 395 (!prefAttackType || "no" + type != prefAttackType)); 343 396 344 // check if the target is capturable 345 let captureIndex = types.indexOf("Capture"); 346 if (captureIndex != -1) 397 if (!types.length) 398 return undefined; 399 400 prefAttackType = this.GetAttackTypeFromOrder(prefAttackType); 401 402 if (prefAttackType && prefAttackType != "Capture" && types.indexOf(prefAttackType) != -1) 403 return prefAttackType; 404 405 if (!prefAttackType || prefAttackType == "Capture") 406 { 407 // check if the target is capturable 408 let captureIndex = types.indexOf("Capture"); 409 if (captureIndex != -1) 410 { 411 let cmpCapturable = QueryMiragedInterface(target, IID_Capturable); 412 let cmpPlayer = QueryOwnerInterface(this.entity); 413 if (cmpPlayer && cmpCapturable && cmpCapturable.CanCapture(cmpPlayer.GetPlayerID())) 414 return "Capture"; 415 // not captureable, so remove this attack 416 types.splice(captureIndex, 1); 417 } 418 } 419 420 // ignore charges for now: TODO implement these 421 let chargeIndex = types.indexOf("Charge"); 422 if (chargeIndex != -1) 423 types.splice(chargeIndex, 1); 424 425 if (!types.length) 426 return undefined; 427 // assume ranged and/or melee attack left 428 // TODO stop assuming that? 429 let meleeIndex = types.indexOf("Melee"); 430 let rangedIndex = types.indexOf("Ranged"); 431 if (meleeIndex != -1 && rangedIndex != -1) 347 432 { 348 let cmpCapturable = QueryMiragedInterface(target, IID_Capturable); 433 if (this.HasTargetPreferredClass(types, targetClasses)) 434 { 435 let isPreferred = className => attack.GetPreferredClasses(className).some(isTargetClass); 436 437 return types.sort((a, b) => 438 (types.indexOf(a) + (isPreferred(a) ? types.length : 0) ) - 439 (types.indexOf(b) + (isPreferred(b) ? types.length : 0))).pop(); 440 } 349 441 350 let cmpPlayer = QueryOwnerInterface(this.entity); 351 if (allowCapture && cmpPlayer && cmpCapturable && cmpCapturable.CanCapture(cmpPlayer.GetPlayerID())) 352 return "Capture"; 353 // not captureable, so remove this attack 354 types.splice(captureIndex, 1); 442 if (!cmpPosition.IsInWorld()) 443 return undefined; 444 445 let cmpTargetPosition = Engine.QueryInterface(target, IID_Position); 446 if (!cmpTargetPosition || !cmpTargetPosition.IsInWorld()) 447 return undefined; 448 449 let selfPosition = cmpPosition.GetPosition2D(); 450 let targetPosition = cmpTargetPosition.GetPosition2D(); 451 if (targetPosition.distanceToSquared(selfPosition) <= Math.pow(this.GetChangeDistance(), 2)) 452 return "Melee"; 453 454 return "Ranged"; 355 455 } 356 456 357 let isPreferred = className => attack.GetPreferredClasses(className).some(isTargetClass); 457 if (meleeIndex != -1) 458 return "Melee"; 459 460 if (rangedIndex != -1) 461 return "Ranged"; 358 462 359 return types.sort((a, b) => 360 (types.indexOf(a) + (isPreferred(a) ? types.length : 0) ) - 361 (types.indexOf(b) + (isPreferred(b) ? types.length : 0))).pop(); 463 return types[0]; 362 464 }; 363 465 364 466 Attack.prototype.CompareEntitiesByPreference = function(a, b) 365 467 { 366 468 let aPreference = this.GetPreference(a); … … Attack.prototype.GetAttackBonus = functi 464 566 } 465 567 466 568 return attackBonus; 467 569 }; 468 570 571 Attack.prototype.GetAttackTypeFromOrder = function(prefAttackType) 572 { 573 let types = this.GetAttackTypes(); 574 for (let type of types) 575 if (this.template[type].AttackOrder && this.template[type].AttackOrder == prefAttackType) 576 return type; 577 return prefAttackType; 578 }; 579 580 Attack.prototype.HasTargetPreferredClass = function(types, targetClasses) 581 { 582 let isTargetClass = function (className) { return targetClasses.indexOf(className) != -1; }; 583 for (let type of types) 584 if (this.template[type].PreferredClasses && this.GetPreferredClasses(type).some(isTargetClass)) 585 return true; 586 return false 587 }; 588 469 589 // Returns a 2d random distribution scaled for a spread of scale 1. 470 590 // The current implementation is a 2d gaussian with sigma = 1 471 591 Attack.prototype.GetNormalDistribution = function(){ 472 592 473 593 // Use the Box-Muller transform to get a gaussian distribution 474 594 let a = Math.random(); 475 595 let b = Math.random(); 476 596 477 let c = Math.sqrt(-2 *Math.log(a)) * Math.cos(2*Math.PI*b);478 let d = Math.sqrt(-2 *Math.log(a)) * Math.sin(2*Math.PI*b);597 let c = Math.sqrt(-2 * Math.log(a)) * Math.cos(2 * Math.PI * b); 598 let d = Math.sqrt(-2 * Math.log(a)) * Math.sin(2 * Math.PI * b); 479 599 480 600 return [c, d]; 481 601 }; 482 602 483 603 /** … … Attack.prototype.PerformAttack = functio 489 609 { 490 610 // If this is a ranged attack, then launch a projectile 491 611 if (type == "Ranged") 492 612 { 493 613 let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); 494 let turnLength = cmpTimer.GetLatestTurnLength() /1000;614 let turnLength = cmpTimer.GetLatestTurnLength() / 1000; 495 615 // In the future this could be extended: 496 616 // * Obstacles like trees could reduce the probability of the target being hit 497 617 // * Obstacles like walls should block projectiles entirely 498 618 499 619 // Get some data about the entity … … Attack.prototype.PerformAttack = functio 521 641 // The component of the targets velocity radially away from the archer 522 642 let radialSpeed = relativePosition.dot(targetVelocity) / relativePosition.length(); 523 643 524 644 let horizDistance = targetPosition.horizDistanceTo(selfPosition); 525 645 526 // This is an approximation of the time otthe target, it assumes that the target has a constant radial646 // This is an approximation of the time to the target, it assumes that the target has a constant radial 527 647 // velocity, but since units move in straight lines this is not true. The exact value would be more 528 648 // difficult to calculate and I think this is sufficiently accurate. (I tested and for cavalry it was 529 649 // about 5% of the units radius out in the worst case) 530 650 let timeToTarget = horizDistance / (horizSpeed - radialSpeed); 531 651 … … Attack.prototype.PerformAttack = functio 606 726 }; 607 727 608 728 Attack.prototype.InterpolatedLocation = function(ent, lateness) 609 729 { 610 730 let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); 611 let turnLength = cmpTimer.GetLatestTurnLength() /1000;731 let turnLength = cmpTimer.GetLatestTurnLength() / 1000; 612 732 let cmpTargetPosition = Engine.QueryInterface(ent, IID_Position); 613 733 if (!cmpTargetPosition || !cmpTargetPosition.IsInWorld()) // TODO: handle dead target properly 614 734 return undefined; 615 735 let curPos = cmpTargetPosition.GetPosition(); 616 736 let prevPos = cmpTargetPosition.GetPreviousPosition(); … … Attack.prototype.testCollision = functio 644 764 let angle = Engine.QueryInterface(ent, IID_Position).GetRotation().y; 645 765 646 766 let d = Vector3D.sub(point, targetPosition); 647 767 d = Vector2D.from3D(d).rotate(-angle); 648 768 649 return d.x < Math.abs(targetShape.width /2) && d.y < Math.abs(targetShape.depth/2);769 return d.x < Math.abs(targetShape.width / 2) && d.y < Math.abs(targetShape.depth / 2); 650 770 } 651 771 }; 652 772 653 773 Attack.prototype.MissileHit = function(data, lateness) 654 774 { -
binaries/data/mods/public/simulation/components/GarrisonHolder.js
GarrisonHolder.prototype.Schema = 8 8 "<attribute name='datatype'>" + 9 9 "<value>tokens</value>" + 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>" + 16 24 "<element name='EjectClassesOnDestroy' a:help='Classes of entities to be ejected on destroy. Others are killed'>" + 17 25 "<attribute name='datatype'>" + … … GarrisonHolder.prototype.GetAllowedClass 117 125 { 118 126 return this.template.List._string; 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() 125 142 { 126 143 return ApplyValueModificationsToEntity("GarrisonHolder/Max", +this.template.Max, this.entity); -
binaries/data/mods/public/simulation/components/GuiInterface.js
GuiInterface.prototype.GetEntityState = 355 355 let cmpGarrisonHolder = Engine.QueryInterface(ent, IID_GarrisonHolder); 356 356 if (cmpGarrisonHolder) 357 357 ret.garrisonHolder = { 358 358 "entities": cmpGarrisonHolder.GetEntities(), 359 359 "allowedClasses": cmpGarrisonHolder.GetAllowedClasses(), 360 "neededAttackTypes": cmpGarrisonHolder.GetNeededAttackTypes(), 360 361 "capacity": cmpGarrisonHolder.GetCapacity(), 361 362 "garrisonedEntitiesCount": cmpGarrisonHolder.GetGarrisonedEntitiesCount() 362 363 }; 363 364 364 365 let cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI); … … GuiInterface.prototype.GetExtendedEntity 436 437 if (cmpAttack) 437 438 { 438 439 let types = cmpAttack.GetAttackTypes(); 439 440 if (types.length) 440 441 ret.attack = {}; 442 441 443 for (let type of types) 442 444 { 443 445 ret.attack[type] = cmpAttack.GetAttackStrengths(type); 444 446 ret.attack[type].splash = cmpAttack.GetSplashDamage(type); 445 447 … … GuiInterface.prototype.CanAttack = funct 1825 1827 { 1826 1828 let cmpAttack = Engine.QueryInterface(data.entity, IID_Attack); 1827 1829 if (!cmpAttack) 1828 1830 return false; 1829 1831 1830 let cmpEntityPlayer = QueryOwnerInterface(data.entity, IID_Player); 1831 let cmpTargetPlayer = QueryOwnerInterface(data.target, IID_Player); 1832 if (!cmpEntityPlayer || !cmpTargetPlayer) 1833 return false; 1832 return cmpAttack.CanAttack(data.target); 1833 }; 1834 1834 1835 // if the owner is an enemy, it's up to the attack component to decide 1836 if (cmpEntityPlayer.IsEnemy(cmpTargetPlayer.GetPlayerID())) 1837 return cmpAttack.CanAttack(data.target); 1835 GuiInterface.prototype.CanAttackWithType = function(player, data) 1836 { 1837 let cmpAttack = Engine.QueryInterface(data.entity, IID_Attack); 1838 if (!cmpAttack) 1839 return false; 1838 1840 1839 return false;1841 return cmpAttack.CanAttack(data.target, data.type); 1840 1842 }; 1841 1843 1842 1844 /* 1843 1845 * Returns batch build time. 1844 1846 */ … … let exposedFunctions = { 1981 1983 "HasIdleUnits": 1, 1982 1984 "GetTradingRouteGain": 1, 1983 1985 "GetTradingDetails": 1, 1984 1986 "CanCapture": 1, 1985 1987 "CanAttack": 1, 1988 "CanAttackWithType": 1, 1986 1989 "GetBatchTime": 1, 1987 1990 1988 1991 "IsMapRevealed": 1, 1989 1992 "SetPathfinderDebugOverlay": 1, 1990 1993 "SetPathfinderHierDebugOverlay": 1, -
binaries/data/mods/public/simulation/components/UnitAI.js
UnitAI.prototype.UnitFsmSpec = { 418 418 this.FinishOrder(); 419 419 return; 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 427 427 this.FinishOrder(); 428 428 return; … … UnitAI.prototype.UnitFsmSpec = { 561 561 "Order.Gather": function(msg) { 562 562 // If the target is still alive, we need to kill it first 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 570 570 this.FinishOrder(); 571 571 return; … … UnitAI.prototype.UnitFsmSpec = { 585 585 this.FinishOrder(); 586 586 } 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 594 594 // Try to move within range 595 595 if (this.MoveToTargetRange(this.order.data.target, IID_ResourceGatherer)) … … UnitAI.prototype.UnitFsmSpec = { 840 840 this.CallMemberFunction("Stop", [false]); 841 841 this.FinishOrder(); 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 851 851 var cmpAttack = Engine.QueryInterface(this.entity, IID_Attack); 852 852 // Check if we are already in range, otherwise walk there … … UnitAI.prototype.UnitFsmSpec = { 861 861 } 862 862 } 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 870 870 this.SetNextState("MEMBER"); 871 871 }, … … UnitAI.prototype.UnitFsmSpec = { 916 916 this.FinishOrder(); 917 917 } 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 925 925 // TODO: on what should we base this range? 926 926 // Check if we are already in range, otherwise walk there … … UnitAI.prototype.UnitFsmSpec = { 1153 1153 cmpFormation.MoveMembersIntoFormation(true, true); 1154 1154 }, 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 1162 1162 this.SetNextState("MEMBER"); 1163 1163 }, 1164 1164 }, 1165 1165 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(); 1181 1181 return true; 1182 1182 } … … UnitAI.prototype.UnitFsmSpec = { 1188 1188 this.StartTimer(200, 200); 1189 1189 return false; 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(); 1205 1205 return; 1206 1206 } … … UnitAI.prototype.UnitFsmSpec = { 1408 1408 return; 1409 1409 } 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); 1417 1417 if (!cmpPosition || !cmpPosition.IsInWorld()) 1418 1418 return; … … UnitAI.prototype.UnitFsmSpec = { 1803 1803 !this.CheckTargetAttackRange(target, this.order.data.attackType)) 1804 1804 { 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"); 1811 1813 return; 1812 1814 } … … UnitAI.prototype.UnitFsmSpec = { 1914 1916 } 1915 1917 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"); 1922 1930 return; 1923 1931 } … … UnitAI.prototype.CheckTargetIsInVisionRa 4547 4555 var distance = DistanceBetweenEntities(this.entity, target); 4548 4556 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) 4561 4569 { 4562 4570 var cmpAttack = Engine.QueryInterface(this.entity, IID_Attack); … … UnitAI.prototype.AttackVisibleEntity = f 4574 4582 { 4575 4583 var target = ents.find(target => this.CanAttack(target, forceResponse)); 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 4583 4591 /** 4584 4592 * Try to find one of the given entities which can be attacked … … UnitAI.prototype.AttackVisibleEntity = f 4587 4595 */ 4588 4596 UnitAI.prototype.AttackEntityInZone = function(ents, forceResponse) 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 4602 4610 /** 4603 4611 * Try to respond appropriately given our current stance, … … UnitAI.prototype.WalkToTarget = function 4984 4992 /** 4985 4993 * Adds walk-and-fight order to queue, this only occurs in response 4986 4994 * to a player order, and so is forced. 4987 4995 * If targetClasses is given, only entities matching the targetClasses can be attacked. 4988 4996 */ 4989 UnitAI.prototype.WalkAndFight = function(x, z, targetClasses, queued )4997 UnitAI.prototype.WalkAndFight = function(x, z, targetClasses, queued, prefAttackType) 4990 4998 { 4991 this.AddOrder("WalkAndFight", { "x": x, "z": z, "targetClasses": targetClasses, "force": true }, queued);4999 this.AddOrder("WalkAndFight", { "x": x, "z": z, "targetClasses": targetClasses, "force": true, "prefAttackType": prefAttackType }, queued); 4992 5000 }; 4993 5001 4994 5002 /** 4995 5003 * Adds leave foundation order to queue, treated as forced. 4996 5004 */ … … UnitAI.prototype.LeaveFoundation = funct 5007 5015 }; 5008 5016 5009 5017 /** 5010 5018 * Adds attack order to the queue, forced by the player. 5011 5019 */ 5012 UnitAI.prototype.Attack = function(target, queued, allowCapture)5020 UnitAI.prototype.Attack = function(target, queued, prefAttackType) 5013 5021 { 5014 5022 if (!this.CanAttack(target)) 5015 5023 { 5016 5024 // We don't want to let healers walk to the target unit so they can be easily killed. 5017 5025 // Instead we just let them get into healing range. … … UnitAI.prototype.Attack = function(targe 5019 5027 this.MoveToTargetRange(target, IID_Heal); 5020 5028 else 5021 5029 this.WalkToTarget(target, queued); 5022 5030 return; 5023 5031 } 5024 this.AddOrder("Attack", { "target": target, "force": true, " allowCapture": allowCapture}, queued);5032 this.AddOrder("Attack", { "target": target, "force": true, "prefAttackType": prefAttackType }, queued); 5025 5033 }; 5026 5034 5027 5035 /** 5028 5036 * Adds garrison order to the queue, forced by the player. 5029 5037 */ … … UnitAI.prototype.FindWalkAndFightTargets 5448 5456 continue; 5449 5457 // Only used by the AIs to prevent some choices of targets 5450 5458 if (targetClasses.vetoEntities && targetClasses.vetoEntities[targ]) 5451 5459 continue; 5452 5460 } 5453 this.PushOrderFront("Attack", { "target": targ, "force": true, " allowCapture": true});5461 this.PushOrderFront("Attack", { "target": targ, "force": true, "prefAttackType": undefined }); 5454 5462 return true; 5455 5463 } 5456 5464 } 5457 5465 return false; 5458 5466 } … … UnitAI.prototype.FindWalkAndFightTargets 5474 5482 continue; 5475 5483 // Only used by the AIs to prevent some choices of targets 5476 5484 if (targetClasses.vetoEntities && targetClasses.vetoEntities[targ]) 5477 5485 continue; 5478 5486 } 5479 this.PushOrderFront("Attack", { "target": targ, "force": true, " allowCapture": true});5487 this.PushOrderFront("Attack", { "target": targ, "force": true, "prefAttackType": undefined }); 5480 5488 return true; 5481 5489 } 5482 5490 5483 5491 // healers on a walk-and-fight order should heal injured units 5484 5492 if (this.IsHealer()) … … UnitAI.prototype.CanGarrison = function( 5674 5682 // Formation controllers should always respond to commands 5675 5683 // (then the individual units can make up their own minds) 5676 5684 if (this.IsFormationController()) 5677 5685 return true; 5678 5686 5679 varcmpGarrisonHolder = Engine.QueryInterface(target, IID_GarrisonHolder);5687 let cmpGarrisonHolder = Engine.QueryInterface(target, IID_GarrisonHolder); 5680 5688 if (!cmpGarrisonHolder) 5681 5689 return false; 5682 5690 5683 5691 // Verify that the target is owned by this entity's player or a mutual ally of this player 5684 varcmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership);5692 let cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership); 5685 5693 if (!cmpOwnership || !(IsOwnedByPlayer(cmpOwnership.GetOwner(), target) || IsOwnedByMutualAllyOfPlayer(cmpOwnership.GetOwner(), target))) 5686 5694 return false; 5687 5695 5688 5696 // Don't let animals garrison for now 5689 5697 // (If we want to support that, we'll need to change Order.Garrison so it 5690 5698 // doesn't move the animal into an INVIDIDUAL.* state) 5691 5699 if (this.IsAnimal()) 5692 5700 return false; 5693 5701 5702 let cmpIdentity = Engine.QueryInterface(this.entity, IID_Identity) 5703 if (!cmpIdentity) 5704 return false; 5705 5706 let cmpAttack = Engine.QueryInterface(this.entity, IID_Attack) 5707 let attackTypes = cmpAttack ? cmpAttack.GetAttackTypes() : []; 5708 if (!MatchesClassList(cmpIdentity.GetClassesList(), cmpGarrisonHolder.GetAllowedClasses()) || 5709 !HasNeededAttackTypes(attackTypes, cmpGarrisonHolder.GetNeededAttackTypes())) 5710 return false; 5694 5711 return true; 5695 5712 }; 5696 5713 5697 5714 UnitAI.prototype.CanGather = function(target) 5698 5715 { -
binaries/data/mods/public/simulation/components/tests/test_UnitAI.js
function TestFormationExiting(mode) 95 95 }); 96 96 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; }, 104 104 CompareEntitiesByPreference: function(a, b) { return 0; }, 105 105 }); … … function TestMoveIntoFormationWhileAttac 245 245 }); 246 246 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; }, 254 254 }); 255 255 -
binaries/data/mods/public/simulation/helpers/Commands.js
var g_Commands = { 158 158 }, 159 159 160 160 "attack-walk": function(player, cmd, data) 161 161 { 162 162 GetFormationUnitAIs(data.entities, player).forEach(cmpUnitAI => { 163 cmpUnitAI.WalkAndFight(cmd.x, cmd.z, cmd.targetClasses, cmd.queued );163 cmpUnitAI.WalkAndFight(cmd.x, cmd.z, cmd.targetClasses, cmd.queued, cmd.prefAttackType); 164 164 }); 165 165 }, 166 166 167 167 "attack": function(player, cmd, data) 168 168 { 169 169 if (g_DebugCommands && !(IsOwnedByEnemyOfPlayer(player, cmd.target) || IsOwnedByNeutralOfPlayer(player, cmd.target))) 170 170 warn("Invalid command: attack target is not owned by enemy of player "+player+": "+uneval(cmd)); 171 171 172 let allowCapture = cmd.allowCapture || cmd.allowCapture == null;173 172 // See UnitAI.CanAttack for target checks 174 173 GetFormationUnitAIs(data.entities, player).forEach(cmpUnitAI => { 175 cmpUnitAI.Attack(cmd.target, cmd.queued, allowCapture);174 cmpUnitAI.Attack(cmd.target, cmd.queued, cmd.prefAttackType); 176 175 }); 177 176 }, 178 177 179 178 "heal": function(player, cmd, data) 180 179 { -
binaries/data/mods/public/simulation/templates/template_structure_defense_wall_long.xml
10 10 <Health> 11 11 <SpawnEntityOnDeath>rubble/rubble_stone_wall_long</SpawnEntityOnDeath> 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> 19 20 <LoadingRange>2</LoadingRange> 20 21 <VisibleGarrisonPoints> -
binaries/data/mods/public/simulation/templates/template_structure_defense_wall_medium.xml
7 7 <stone>22</stone> 8 8 </Resources> 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> 16 17 <LoadingRange>2</LoadingRange> 17 18 <VisibleGarrisonPoints> -
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> 6 25 <SpecificName>Anusiya</SpecificName> 7 26 <Classes datatype="tokens">Immortal</Classes>