LCOV - code coverage report
Current view: top level - simulation/components - Attack.js (source / functions) Hit Total Coverage
Test: lcov.info Lines: 176 294 59.9 %
Date: 2023-04-02 12:52:40 Functions: 22 33 66.7 %

          Line data    Source code
       1             : function Attack() {}
       2             : 
       3           2 : var g_AttackTypes = ["Melee", "Ranged", "Capture"];
       4             : 
       5           2 : Attack.prototype.preferredClassesSchema =
       6             :         "<optional>" +
       7             :                 "<element name='PreferredClasses' a:help='Space delimited list of classes preferred for attacking. If an entity has any of theses classes, it is preferred. The classes are in decending order of preference'>" +
       8             :                         "<attribute name='datatype'>" +
       9             :                                 "<value>tokens</value>" +
      10             :                         "</attribute>" +
      11             :                         "<text/>" +
      12             :                 "</element>" +
      13             :         "</optional>";
      14             : 
      15           2 : Attack.prototype.restrictedClassesSchema =
      16             :         "<optional>" +
      17             :                 "<element name='RestrictedClasses' a:help='Space delimited list of classes that cannot be attacked by this entity. If target entity has any of these classes, it cannot be attacked'>" +
      18             :                         "<attribute name='datatype'>" +
      19             :                                 "<value>tokens</value>" +
      20             :                         "</attribute>" +
      21             :                         "<text/>" +
      22             :                 "</element>" +
      23             :         "</optional>";
      24             : 
      25           2 : Attack.prototype.Schema =
      26             :         "<a:help>Controls the attack abilities and strengths of the unit.</a:help>" +
      27             :         "<a:example>" +
      28             :                 "<Melee>" +
      29             :                         "<AttackName>Spear</AttackName>" +
      30             :                         "<Damage>" +
      31             :                                 "<Hack>10.0</Hack>" +
      32             :                                 "<Pierce>0.0</Pierce>" +
      33             :                                 "<Crush>5.0</Crush>" +
      34             :                         "</Damage>" +
      35             :                         "<MaxRange>4.0</MaxRange>" +
      36             :                         "<RepeatTime>1000</RepeatTime>" +
      37             :                         "<Bonuses>" +
      38             :                                 "<Bonus1>" +
      39             :                                         "<Civ>pers</Civ>" +
      40             :                                         "<Classes>Infantry</Classes>" +
      41             :                                         "<Multiplier>1.5</Multiplier>" +
      42             :                                 "</Bonus1>" +
      43             :                                 "<BonusCavMelee>" +
      44             :                                         "<Classes>Cavalry Melee</Classes>" +
      45             :                                         "<Multiplier>1.5</Multiplier>" +
      46             :                                 "</BonusCavMelee>" +
      47             :                         "</Bonuses>" +
      48             :                         "<RestrictedClasses datatype=\"tokens\">Champion</RestrictedClasses>" +
      49             :                         "<PreferredClasses datatype=\"tokens\">Cavalry Infantry</PreferredClasses>" +
      50             :                 "</Melee>" +
      51             :                 "<Ranged>" +
      52             :                         "<AttackName>Bow</AttackName>" +
      53             :                         "<Damage>" +
      54             :                                 "<Hack>0.0</Hack>" +
      55             :                                 "<Pierce>10.0</Pierce>" +
      56             :                                 "<Crush>0.0</Crush>" +
      57             :                         "</Damage>" +
      58             :                         "<MaxRange>44.0</MaxRange>" +
      59             :                         "<MinRange>20.0</MinRange>" +
      60             :                         "<Origin>" +
      61             :                                 "<X>0</X>" +
      62             :                                 "<Y>10.0</Y>" +
      63             :                                 "<Z>0</Z>" +
      64             :                         "</Origin>" +
      65             :                         "<PrepareTime>800</PrepareTime>" +
      66             :                         "<RepeatTime>1600</RepeatTime>" +
      67             :                         "<EffectDelay>1000</EffectDelay>" +
      68             :                         "<Bonuses>" +
      69             :                                 "<Bonus1>" +
      70             :                                         "<Classes>Cavalry</Classes>" +
      71             :                                         "<Multiplier>2</Multiplier>" +
      72             :                                 "</Bonus1>" +
      73             :                         "</Bonuses>" +
      74             :                         "<Projectile>" +
      75             :                                 "<Speed>50.0</Speed>" +
      76             :                                 "<Spread>2.5</Spread>" +
      77             :                                 "<ActorName>props/units/weapons/rock_flaming.xml</ActorName>" +
      78             :                                 "<ImpactActorName>props/units/weapons/rock_explosion.xml</ImpactActorName>" +
      79             :                                 "<ImpactAnimationLifetime>0.1</ImpactAnimationLifetime>" +
      80             :                                 "<FriendlyFire>false</FriendlyFire>" +
      81             :                         "</Projectile>" +
      82             :                         "<RestrictedClasses datatype=\"tokens\">Champion</RestrictedClasses>" +
      83             :                         "<Splash>" +
      84             :                                 "<Shape>Circular</Shape>" +
      85             :                                 "<Range>20</Range>" +
      86             :                                 "<FriendlyFire>false</FriendlyFire>" +
      87             :                                 "<Damage>" +
      88             :                                         "<Hack>0.0</Hack>" +
      89             :                                         "<Pierce>10.0</Pierce>" +
      90             :                                         "<Crush>0.0</Crush>" +
      91             :                                 "</Damage>" +
      92             :                         "</Splash>" +
      93             :                 "</Ranged>" +
      94             :                 "<Slaughter>" +
      95             :                         "<Damage>" +
      96             :                                 "<Hack>1000.0</Hack>" +
      97             :                                 "<Pierce>0.0</Pierce>" +
      98             :                                 "<Crush>0.0</Crush>" +
      99             :                         "</Damage>" +
     100             :                         "<RepeatTime>1000</RepeatTime>" +
     101             :                         "<MaxRange>4.0</MaxRange>" +
     102             :                 "</Slaughter>" +
     103             :         "</a:example>" +
     104             :         "<oneOrMore>" +
     105             :                 "<element>" +
     106             :                         "<anyName a:help='Currently one of Melee, Ranged, Capture or Slaughter.'/>" +
     107             :                         "<interleave>" +
     108             :                                 "<element name='AttackName' a:help='Name of the attack, to be displayed in the GUI. Optionally includes a translate context attribute.'>" +
     109             :                                         "<optional>" +
     110             :                                                 "<attribute name='context'>" +
     111             :                                                         "<text/>" +
     112             :                                                 "</attribute>" +
     113             :                                         "</optional>" +
     114             :                                         "<text/>" +
     115             :                                 "</element>" +
     116             :                                 AttackHelper.BuildAttackEffectsSchema() +
     117             :                                 "<element name='MaxRange' a:help='Maximum attack range (in metres)'><ref name='nonNegativeDecimal'/></element>" +
     118             :                                 "<optional>" +
     119             :                                         "<element name='MinRange' a:help='Minimum attack range (in metres). Defaults to 0.'><ref name='nonNegativeDecimal'/></element>" +
     120             :                                 "</optional>" +
     121             :                                 "<optional>"+
     122             :                                         "<element name='Origin' a:help='The offset from which the attack occurs, relative to the entity position. Defaults to {0,0,0}.'>" +
     123             :                                                 "<interleave>" +
     124             :                                                         "<element name='X'>" +
     125             :                                                                 "<ref name='nonNegativeDecimal'/>" +
     126             :                                                         "</element>" +
     127             :                                                         "<element name='Y'>" +
     128             :                                                                 "<ref name='nonNegativeDecimal'/>" +
     129             :                                                         "</element>" +
     130             :                                                         "<element name='Z'>" +
     131             :                                                                 "<ref name='nonNegativeDecimal'/>" +
     132             :                                                         "</element>" +
     133             :                                                 "</interleave>" +
     134             :                                         "</element>" +
     135             :                                 "</optional>" +
     136             :                                 "<optional>" +
     137             :                                         "<element name='RangeOverlay'>" +
     138             :                                                 "<interleave>" +
     139             :                                                         "<element name='LineTexture'><text/></element>" +
     140             :                                                         "<element name='LineTextureMask'><text/></element>" +
     141             :                                                         "<element name='LineThickness'><ref name='nonNegativeDecimal'/></element>" +
     142             :                                                 "</interleave>" +
     143             :                                         "</element>" +
     144             :                                 "</optional>" +
     145             :                                 "<optional>" +
     146             :                                         "<element name='PrepareTime' a:help='Time from the start of the attack command until the attack actually occurs (in milliseconds). This value relative to RepeatTime should closely match the \"event\" point in the actor&apos;s attack animation. Defaults to 0.'>" +
     147             :                                                 "<data type='nonNegativeInteger'/>" +
     148             :                                         "</element>" +
     149             :                                 "</optional>" +
     150             :                                 "<element name='RepeatTime' a:help='Time between attacks (in milliseconds). The attack animation will be stretched to match this time'>" + // TODO: it shouldn't be stretched
     151             :                                         "<data type='positiveInteger'/>" +
     152             :                                 "</element>" +
     153             :                                 "<optional>" +
     154             :                                         "<element name='EffectDelay' a:help='Delay of applying the effects, in milliseconds after the attack has landed. Defaults to 0.'><ref name='nonNegativeDecimal'/></element>" +
     155             :                                 "</optional>" +
     156             :                                 "<optional>" +
     157             :                                         "<element name='Splash'>" +
     158             :                                                 "<interleave>" +
     159             :                                                         "<element name='Shape' a:help='Shape of the splash damage, can be circular or linear'><text/></element>" +
     160             :                                                         "<element name='Range' a:help='Size of the area affected by the splash'><ref name='nonNegativeDecimal'/></element>" +
     161             :                                                         "<element name='FriendlyFire' a:help='Whether the splash damage can hurt non enemy units'><data type='boolean'/></element>" +
     162             :                                                         AttackHelper.BuildAttackEffectsSchema() +
     163             :                                                 "</interleave>" +
     164             :                                         "</element>" +
     165             :                                 "</optional>" +
     166             :                                 "<optional>" +
     167             :                                         "<element name='Projectile'>" +
     168             :                                                 "<interleave>" +
     169             :                                                         "<element name='Speed' a:help='Speed of projectiles (in meters per second).'>" +
     170             :                                                                 "<ref name='positiveDecimal'/>" +
     171             :                                                         "</element>" +
     172             :                                                         "<element name='Spread' a:help='Standard deviation of the bivariate normal distribution of hits at 100 meters. A disk at 100 meters from the attacker with this radius (2x this radius, 3x this radius) is expected to include the landing points of 39.3% (86.5%, 98.9%) of the rounds.'><ref name='nonNegativeDecimal'/></element>" +
     173             :                                                         "<element name='Gravity' a:help='The gravity affecting the projectile. This affects the shape of the flight curve.'>" +
     174             :                                                                 "<ref name='nonNegativeDecimal'/>" +
     175             :                                                         "</element>" +
     176             :                                                         "<element name='FriendlyFire' a:help='Whether stray missiles can hurt non enemy units.'><data type='boolean'/></element>" +
     177             :                                                         "<optional>" +
     178             :                                                                 "<element name='LaunchPoint' a:help='Delta from the unit position where to launch the projectile.'>" +
     179             :                                                                         "<attribute name='y'>" +
     180             :                                                                                 "<data type='decimal'/>" +
     181             :                                                                         "</attribute>" +
     182             :                                                                 "</element>" +
     183             :                                                         "</optional>" +
     184             :                                                         "<optional>" +
     185             :                                                                 "<element name='ActorName' a:help='actor of the projectile animation.'>" +
     186             :                                                                         "<text/>" +
     187             :                                                                 "</element>" +
     188             :                                                         "</optional>" +
     189             :                                                         "<optional>" +
     190             :                                                                 "<element name='ImpactActorName' a:help='actor of the projectile impact animation'>" +
     191             :                                                                         "<text/>" +
     192             :                                                                 "</element>" +
     193             :                                                                 "<element name='ImpactAnimationLifetime' a:help='length of the projectile impact animation.'>" +
     194             :                                                                         "<ref name='positiveDecimal'/>" +
     195             :                                                                 "</element>" +
     196             :                                                         "</optional>" +
     197             :                                                 "</interleave>" +
     198             :                                         "</element>" +
     199             :                                 "</optional>" +
     200             :                                 Attack.prototype.preferredClassesSchema +
     201             :                                 Attack.prototype.restrictedClassesSchema +
     202             :                         "</interleave>" +
     203             :                 "</element>" +
     204             :         "</oneOrMore>";
     205             : 
     206           2 : Attack.prototype.Init = function()
     207             : {
     208             : };
     209             : 
     210           2 : Attack.prototype.GetAttackTypes = function(wantedTypes)
     211             : {
     212         474 :         let types = g_AttackTypes.filter(type => !!this.template[type]);
     213         158 :         if (!wantedTypes)
     214          33 :                 return types;
     215             : 
     216         162 :         let wantedTypesReal = wantedTypes.filter(wtype => wtype.indexOf("!") != 0);
     217         375 :         return types.filter(type => wantedTypes.indexOf("!" + type) == -1 &&
     218             :               (!wantedTypesReal || !wantedTypesReal.length || wantedTypesReal.indexOf(type) != -1));
     219             : };
     220             : 
     221           2 : Attack.prototype.GetPreferredClasses = function(type)
     222             : {
     223          17 :         if (this.template[type] && this.template[type].PreferredClasses &&
     224             :             this.template[type].PreferredClasses._string)
     225          16 :                 return this.template[type].PreferredClasses._string.split(/\s+/);
     226             : 
     227           1 :         return [];
     228             : };
     229             : 
     230           2 : Attack.prototype.GetRestrictedClasses = function(type)
     231             : {
     232          57 :         if (this.template[type] && this.template[type].RestrictedClasses &&
     233             :             this.template[type].RestrictedClasses._string)
     234          46 :                 return this.template[type].RestrictedClasses._string.split(/\s+/);
     235             : 
     236          11 :         return [];
     237             : };
     238             : 
     239           2 : Attack.prototype.CanAttack = function(target, wantedTypes)
     240             : {
     241         133 :         const cmpFormation = Engine.QueryInterface(target, IID_Formation);
     242         133 :         if (cmpFormation)
     243           0 :                 return true;
     244             : 
     245         133 :         const cmpThisPosition = Engine.QueryInterface(this.entity, IID_Position);
     246         133 :         const cmpTargetPosition = Engine.QueryInterface(target, IID_Position);
     247         133 :         if (!cmpThisPosition || !cmpTargetPosition || !cmpThisPosition.IsInWorld() || !cmpTargetPosition.IsInWorld())
     248           0 :                 return false;
     249             : 
     250         133 :         const cmpResistance = QueryMiragedInterface(target, IID_Resistance);
     251         133 :         if (!cmpResistance)
     252           0 :                 return false;
     253             : 
     254         133 :         const cmpIdentity = QueryMiragedInterface(target, IID_Identity);
     255         133 :         if (!cmpIdentity)
     256           0 :                 return false;
     257             : 
     258         133 :         const cmpHealth = QueryMiragedInterface(target, IID_Health);
     259         133 :         const targetClasses = cmpIdentity.GetClassesList();
     260         133 :         if (targetClasses.indexOf("Domestic") != -1 && this.template.Slaughter && cmpHealth && cmpHealth.GetHitpoints() &&
     261          22 :            (!wantedTypes || !wantedTypes.filter(wType => wType.indexOf("!") != 0).length || wantedTypes.indexOf("Slaughter") != -1))
     262           8 :                 return true;
     263             : 
     264         125 :         const cmpEntityPlayer = QueryOwnerInterface(this.entity);
     265         125 :         const cmpTargetPlayer = QueryOwnerInterface(target);
     266         125 :         if (!cmpTargetPlayer || !cmpEntityPlayer)
     267           0 :                 return false;
     268             : 
     269         125 :         const types = this.GetAttackTypes(wantedTypes);
     270         125 :         const entityOwner = cmpEntityPlayer.GetPlayerID();
     271         125 :         const targetOwner = cmpTargetPlayer.GetPlayerID();
     272         125 :         const cmpCapturable = QueryMiragedInterface(target, IID_Capturable);
     273             : 
     274             :         // Check if the relative height difference is larger than the attack range
     275             :         // If the relative height is bigger, it means they will never be able to
     276             :         // reach each other, no matter how close they come.
     277         125 :         const heightDiff = Math.abs(cmpThisPosition.GetHeightOffset() - cmpTargetPosition.GetHeightOffset());
     278             : 
     279         125 :         for (const type of types)
     280             :         {
     281         150 :                 if (type != "Capture" && (!cmpEntityPlayer.IsEnemy(targetOwner) || !cmpHealth || !cmpHealth.GetHitpoints()))
     282          49 :                         continue;
     283             : 
     284         101 :                 if (type == "Capture" && (!cmpCapturable || !cmpCapturable.CanCapture(entityOwner)))
     285          45 :                         continue;
     286             : 
     287          56 :                 if (heightDiff > this.GetRange(type).max)
     288           0 :                         continue;
     289             : 
     290          56 :                 const restrictedClasses = this.GetRestrictedClasses(type);
     291          56 :                 if (!restrictedClasses.length)
     292          11 :                         return true;
     293             : 
     294          45 :                 if (!MatchesClassList(targetClasses, restrictedClasses))
     295          38 :                         return true;
     296             :         }
     297             : 
     298          76 :         return false;
     299             : };
     300             : 
     301             : /**
     302             :  * Returns undefined if we have no preference or the lowest index of a preferred class.
     303             :  */
     304           2 : Attack.prototype.GetPreference = function(target)
     305             : {
     306           4 :         let cmpIdentity = Engine.QueryInterface(target, IID_Identity);
     307           4 :         if (!cmpIdentity)
     308           0 :                 return undefined;
     309             : 
     310           4 :         let targetClasses = cmpIdentity.GetClassesList();
     311             : 
     312             :         let minPref;
     313           4 :         for (let type of this.GetAttackTypes())
     314             :         {
     315           4 :                 let preferredClasses = this.GetPreferredClasses(type);
     316           4 :                 for (let pref = 0; pref < preferredClasses.length; ++pref)
     317             :                 {
     318           7 :                         if (MatchesClassList(targetClasses, preferredClasses[pref]))
     319             :                         {
     320           2 :                                 if (pref === 0)
     321           1 :                                         return pref;
     322           1 :                                 if ((minPref === undefined || minPref > pref))
     323           1 :                                         minPref = pref;
     324             :                         }
     325             :                 }
     326             :         }
     327           3 :         return minPref;
     328             : };
     329             : 
     330             : /**
     331             :  * Get the full range of attack using all available attack types.
     332             :  */
     333           2 : Attack.prototype.GetFullAttackRange = function()
     334             : {
     335           1 :         let ret = { "min": Infinity, "max": 0 };
     336           1 :         for (let type of this.GetAttackTypes())
     337             :         {
     338           3 :                 let range = this.GetRange(type);
     339           3 :                 ret.min = Math.min(ret.min, range.min);
     340           3 :                 ret.max = Math.max(ret.max, range.max);
     341             :         }
     342           1 :         return ret;
     343             : };
     344             : 
     345           2 : Attack.prototype.GetAttackEffectsData = function(type, splash)
     346             : {
     347          20 :         let template = this.template[type];
     348          20 :         if (!template)
     349           0 :                 return undefined;
     350          20 :         if (splash)
     351           4 :                 template = template.Splash;
     352          20 :         return AttackHelper.GetAttackEffectsData("Attack/" + type + (splash ? "/Splash" : ""), template, this.entity);
     353             : };
     354             : 
     355             : /**
     356             :  * Find the best attack against a target.
     357             :  * @param {number} target - The entity-ID of the target.
     358             :  * @param {boolean} allowCapture - Whether capturing is allowed.
     359             :  * @return {string} - The preferred attack type.
     360             :  */
     361           2 : Attack.prototype.GetBestAttackAgainst = function(target, allowCapture)
     362             : {
     363          18 :         let types = this.GetAttackTypes();
     364          18 :         if (Engine.QueryInterface(target, IID_Formation))
     365             :                 // TODO: Formation against formation needs review
     366           0 :                 return g_AttackTypes.find(attack => types.indexOf(attack) != -1);
     367             : 
     368          18 :         const cmpIdentity = Engine.QueryInterface(target, IID_Identity);
     369          18 :         if (!cmpIdentity)
     370           0 :                 return undefined;
     371             : 
     372             :         // Always slaughter domestic animals instead of using a normal attack
     373          18 :         if (this.template.Slaughter && cmpIdentity.HasClass("Domestic"))
     374           4 :                 return "Slaughter";
     375             : 
     376          14 :         const targetClasses = cmpIdentity.GetClassesList();
     377          14 :         const getPreferrence = attackType => {
     378          12 :                 let pref = 0;
     379          12 :                 if (MatchesClassList(targetClasses, this.GetPreferredClasses(attackType)))
     380           2 :                         pref += 2;
     381          12 :                 if (allowCapture ? attackType === "Capture" : attackType !== "Capture")
     382           5 :                         pref++;
     383          12 :                 return pref;
     384             :         };
     385             : 
     386          42 :         return types.filter(type => this.CanAttack(target, [type])).sort((a, b) => {
     387           6 :                 const prefA = getPreferrence(a);
     388           6 :                 const prefB = getPreferrence(b);
     389           6 :                 return (types.indexOf(a) + (prefA > 0 ? prefA + types.length : 0)) -
     390             :                         (types.indexOf(b) + (prefB > 0 ? prefB + types.length : 0))
     391             :         }).pop();
     392             : };
     393             : 
     394           2 : Attack.prototype.CompareEntitiesByPreference = function(a, b)
     395             : {
     396           0 :         let aPreference = this.GetPreference(a);
     397           0 :         let bPreference = this.GetPreference(b);
     398             : 
     399           0 :         if (aPreference === null && bPreference === null) return 0;
     400           0 :         if (aPreference === null) return 1;
     401           0 :         if (bPreference === null) return -1;
     402           0 :         return aPreference - bPreference;
     403             : };
     404             : 
     405           2 : Attack.prototype.GetAttackName = function(type)
     406             : {
     407           0 :         return {
     408             :                 "name": this.template[type].AttackName._string || this.template[type].AttackName,
     409             :                 "context": this.template[type].AttackName["@context"]
     410             :         };
     411             : };
     412             : 
     413           2 : Attack.prototype.GetRepeatTime = function(type)
     414             : {
     415           4 :         let repeatTime = 1000;
     416             : 
     417           4 :         if (this.template[type] && this.template[type].RepeatTime)
     418           2 :                 repeatTime = +this.template[type].RepeatTime;
     419             : 
     420           4 :         return ApplyValueModificationsToEntity("Attack/" + type + "/RepeatTime", repeatTime, this.entity);
     421             : };
     422             : 
     423           2 : Attack.prototype.GetTimers = function(type)
     424             : {
     425           2 :         return {
     426             :                 "prepare": ApplyValueModificationsToEntity("Attack/" + type + "/PrepareTime", +(this.template[type].PrepareTime || 0), this.entity),
     427             :                 "repeat": this.GetRepeatTime(type)
     428             :         };
     429             : };
     430             : 
     431           2 : Attack.prototype.GetSplashData = function(type)
     432             : {
     433           2 :         if (!this.template[type].Splash)
     434           1 :                 return undefined;
     435             : 
     436           1 :         return {
     437             :                 "attackData": this.GetAttackEffectsData(type, true),
     438             :                 "friendlyFire": this.template[type].Splash.FriendlyFire == "true",
     439             :                 "radius": ApplyValueModificationsToEntity("Attack/" + type + "/Splash/Range", +this.template[type].Splash.Range, this.entity),
     440             :                 "shape": this.template[type].Splash.Shape,
     441             :         };
     442             : };
     443             : 
     444           2 : Attack.prototype.GetRange = function(type)
     445             : {
     446          59 :         if (!type)
     447           0 :                 return this.GetFullAttackRange();
     448             : 
     449          59 :         let max = +this.template[type].MaxRange;
     450          59 :         max = ApplyValueModificationsToEntity("Attack/" + type + "/MaxRange", max, this.entity);
     451             : 
     452          59 :         let min = +(this.template[type].MinRange || 0);
     453          59 :         min = ApplyValueModificationsToEntity("Attack/" + type + "/MinRange", min, this.entity);
     454             : 
     455          59 :         return { "max": max, "min": min };
     456             : };
     457             : 
     458           2 : Attack.prototype.GetAttackYOrigin = function(type)
     459             : {
     460           0 :         if (!this.template[type].Origin)
     461           0 :                 return 0;
     462           0 :         return ApplyValueModificationsToEntity("Attack/" + type + "/Origin/Y", +this.template[type].Origin.Y, this.entity);
     463             : };
     464             : 
     465             : /**
     466             :  * @param {number} target - The target to attack.
     467             :  * @param {string} type - The type of attack to use.
     468             :  * @param {number} callerIID - The IID to notify on specific events.
     469             :  *
     470             :  * @return {boolean} - Whether we started attacking.
     471             :  */
     472           2 : Attack.prototype.StartAttacking = function(target, type, callerIID)
     473             : {
     474           0 :         if (this.target)
     475           0 :                 this.StopAttacking();
     476             : 
     477           0 :         if (!this.CanAttack(target, [type]))
     478           0 :                 return false;
     479             : 
     480           0 :         const cmpResistance = QueryMiragedInterface(target, IID_Resistance);
     481           0 :         if (!cmpResistance || !cmpResistance.AddAttacker(this.entity))
     482           0 :                 return false;
     483             : 
     484           0 :         let timings = this.GetTimers(type);
     485           0 :         let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
     486             : 
     487             :         // If the repeat time since the last attack hasn't elapsed,
     488             :         // delay the action to avoid attacking too fast.
     489           0 :         let prepare = timings.prepare;
     490           0 :         if (this.lastAttacked)
     491             :         {
     492           0 :                 let repeatLeft = this.lastAttacked + timings.repeat - cmpTimer.GetTime();
     493           0 :                 prepare = Math.max(prepare, repeatLeft);
     494             :         }
     495             : 
     496           0 :         let cmpVisual = Engine.QueryInterface(this.entity, IID_Visual);
     497           0 :         if (cmpVisual)
     498             :         {
     499           0 :                 cmpVisual.SelectAnimation("attack_" + type.toLowerCase(), false, 1.0);
     500           0 :                 cmpVisual.SetAnimationSyncRepeat(timings.repeat);
     501           0 :                 cmpVisual.SetAnimationSyncOffset(prepare);
     502             :         }
     503             : 
     504             :         // If using a non-default prepare time, re-sync the animation when the timer runs.
     505           0 :         this.resyncAnimation = prepare != timings.prepare;
     506           0 :         this.target = target;
     507           0 :         this.callerIID = callerIID;
     508           0 :         this.timer = cmpTimer.SetInterval(this.entity, IID_Attack, "Attack", prepare, timings.repeat, type);
     509             : 
     510           0 :         return true;
     511             : };
     512             : 
     513             : /**
     514             :  * @param {string} reason - The reason why we stopped attacking.
     515             :  */
     516           2 : Attack.prototype.StopAttacking = function(reason)
     517             : {
     518           0 :         if (!this.target)
     519           0 :                 return;
     520             : 
     521           0 :         let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
     522           0 :         cmpTimer.CancelTimer(this.timer);
     523           0 :         delete this.timer;
     524             : 
     525           0 :         const cmpResistance = QueryMiragedInterface(this.target, IID_Resistance);
     526           0 :         if (cmpResistance)
     527           0 :                 cmpResistance.RemoveAttacker(this.entity);
     528             : 
     529           0 :         delete this.target;
     530             : 
     531           0 :         let cmpVisual = Engine.QueryInterface(this.entity, IID_Visual);
     532           0 :         if (cmpVisual)
     533           0 :                 cmpVisual.SelectAnimation("idle", false, 1.0);
     534             : 
     535             :         // The callerIID component may start again,
     536             :         // replacing the callerIID, hence save that.
     537           0 :         let callerIID = this.callerIID;
     538           0 :         delete this.callerIID;
     539             : 
     540           0 :         if (reason && callerIID)
     541             :         {
     542           0 :                 let component = Engine.QueryInterface(this.entity, callerIID);
     543           0 :                 if (component)
     544           0 :                         component.ProcessMessage(reason, null);
     545             :         }
     546             : };
     547             : 
     548             : /**
     549             :  * Attack our target entity.
     550             :  * @param {string} data - The attack type to use.
     551             :  * @param {number} lateness - The offset of the actual call and when it was expected.
     552             :  */
     553           2 : Attack.prototype.Attack = function(type, lateness)
     554             : {
     555           0 :         if (!this.CanAttack(this.target, [type]))
     556             :         {
     557           0 :                 this.StopAttacking("TargetInvalidated");
     558           0 :                 return;
     559             :         }
     560             : 
     561             :         // ToDo: Enable entities to keep facing a target.
     562           0 :         Engine.QueryInterface(this.entity, IID_UnitAI)?.FaceTowardsTarget(this.target);
     563             : 
     564           0 :         let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
     565           0 :         this.lastAttacked = cmpTimer.GetTime() - lateness;
     566             : 
     567             :         // BuildingAI has its own attack routine.
     568           0 :         if (!Engine.QueryInterface(this.entity, IID_BuildingAI))
     569           0 :                 this.PerformAttack(type, this.target);
     570             : 
     571           0 :         if (!this.target)
     572           0 :                 return;
     573             : 
     574             :         // We check the range after the attack to facilitate chasing.
     575           0 :         if (!this.IsTargetInRange(this.target, type))
     576             :         {
     577           0 :                 this.StopAttacking("OutOfRange");
     578           0 :                 return;
     579             :         }
     580             : 
     581           0 :         if (this.resyncAnimation)
     582             :         {
     583           0 :                 let cmpVisual = Engine.QueryInterface(this.entity, IID_Visual);
     584           0 :                 if (cmpVisual)
     585             :                 {
     586           0 :                         let repeat = this.GetTimers(type).repeat;
     587           0 :                         cmpVisual.SetAnimationSyncRepeat(repeat);
     588           0 :                         cmpVisual.SetAnimationSyncOffset(repeat);
     589             :                 }
     590           0 :                 delete this.resyncAnimation;
     591             :         }
     592             : };
     593             : 
     594             : /**
     595             :  * Attack the target entity. This should only be called after a successful range check,
     596             :  * and should only be called after GetTimers().repeat msec has passed since the last
     597             :  * call to PerformAttack.
     598             :  */
     599           2 : Attack.prototype.PerformAttack = function(type, target)
     600             : {
     601           1 :         let cmpPosition = Engine.QueryInterface(this.entity, IID_Position);
     602           1 :         if (!cmpPosition || !cmpPosition.IsInWorld())
     603           0 :                 return;
     604           1 :         let selfPosition = cmpPosition.GetPosition();
     605             : 
     606           1 :         let cmpTargetPosition = Engine.QueryInterface(target, IID_Position);
     607           1 :         if (!cmpTargetPosition || !cmpTargetPosition.IsInWorld())
     608           0 :                 return;
     609           1 :         let targetPosition = cmpTargetPosition.GetPosition();
     610             : 
     611           1 :         let cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership);
     612           1 :         if (!cmpOwnership)
     613           0 :                 return;
     614           1 :         let attackerOwner = cmpOwnership.GetOwner();
     615             : 
     616           1 :         let data = {
     617             :                 "type": type,
     618             :                 "attackData": this.GetAttackEffectsData(type),
     619             :                 "splash": this.GetSplashData(type),
     620             :                 "attacker": this.entity,
     621             :                 "attackerOwner": attackerOwner,
     622             :                 "target": target,
     623             :         };
     624             : 
     625           1 :         let delay = +(this.template[type].EffectDelay || 0);
     626             : 
     627           1 :         if (this.template[type].Projectile)
     628             :         {
     629           1 :                 let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
     630           1 :                 let turnLength = cmpTimer.GetLatestTurnLength()/1000;
     631             :                 // In the future this could be extended:
     632             :                 //  * Obstacles like trees could reduce the probability of the target being hit
     633             :                 //  * Obstacles like walls should block projectiles entirely
     634             : 
     635           1 :                 let horizSpeed = +this.template[type].Projectile.Speed;
     636           1 :                 let gravity = +this.template[type].Projectile.Gravity;
     637             :                 // horizSpeed /= 2; gravity /= 2; // slow it down for testing
     638             : 
     639             :                 // We will try to estimate the position of the target, where we can hit it.
     640             :                 // We first estimate the time-till-hit by extrapolating linearly the movement
     641             :                 // of the last turn. We compute the time till an arrow will intersect the target.
     642           1 :                 let targetVelocity = Vector3D.sub(targetPosition, cmpTargetPosition.GetPreviousPosition()).div(turnLength);
     643             : 
     644           1 :                 let timeToTarget = PositionHelper.PredictTimeToTarget(selfPosition, horizSpeed, targetPosition, targetVelocity);
     645             : 
     646             :                 // 'Cheat' and use UnitMotion to predict the position in the near-future.
     647             :                 // This avoids 'dancing' issues with units zigzagging over very short distances.
     648             :                 // However, this could fail if the player gives several short move orders, so
     649             :                 // occasionally fall back to basic interpolation.
     650           1 :                 let predictedPosition = targetPosition;
     651           1 :                 if (timeToTarget !== false)
     652             :                 {
     653             :                         // Don't predict too far in the future, but avoid threshold effects.
     654             :                         // After 1 second, always use the 'dumb' interpolated past-motion prediction.
     655           1 :                         let useUnitMotion = randBool(Math.max(0, 0.75 - timeToTarget / 1.333));
     656           1 :                         if (useUnitMotion)
     657             :                         {
     658           0 :                                 let cmpTargetUnitMotion = Engine.QueryInterface(target, IID_UnitMotion);
     659           0 :                                 let cmpTargetUnitAI = Engine.QueryInterface(target, IID_UnitAI);
     660           0 :                                 if (cmpTargetUnitMotion && (!cmpTargetUnitAI || !cmpTargetUnitAI.IsFormationMember()))
     661             :                                 {
     662           0 :                                         let pos2D = cmpTargetUnitMotion.EstimateFuturePosition(timeToTarget);
     663           0 :                                         predictedPosition.x = pos2D.x;
     664           0 :                                         predictedPosition.z = pos2D.y;
     665             :                                 }
     666             :                                 else
     667           0 :                                         predictedPosition = Vector3D.mult(targetVelocity, timeToTarget).add(targetPosition);
     668             :                         }
     669             :                         else
     670           1 :                                 predictedPosition = Vector3D.mult(targetVelocity, timeToTarget).add(targetPosition);
     671             :                 }
     672             : 
     673           1 :                 let predictedHeight = cmpTargetPosition.GetHeightAt(predictedPosition.x, predictedPosition.z);
     674             : 
     675             :                 // Add inaccuracy based on spread.
     676           1 :                 let distanceModifiedSpread = ApplyValueModificationsToEntity("Attack/" + type + "/Spread", +this.template[type].Projectile.Spread, this.entity) *
     677             :                         predictedPosition.horizDistanceTo(selfPosition) / 100;
     678             : 
     679           1 :                 let randNorm = randomNormal2D();
     680           1 :                 let offsetX = randNorm[0] * distanceModifiedSpread;
     681           1 :                 let offsetZ = randNorm[1] * distanceModifiedSpread;
     682             : 
     683           1 :                 data.position = new Vector3D(predictedPosition.x + offsetX, predictedHeight, predictedPosition.z + offsetZ);
     684             : 
     685           1 :                 let realHorizDistance = data.position.horizDistanceTo(selfPosition);
     686           1 :                 timeToTarget = realHorizDistance / horizSpeed;
     687           1 :                 delay += timeToTarget * 1000;
     688             : 
     689           1 :                 data.direction = Vector3D.sub(data.position, selfPosition).div(realHorizDistance);
     690             : 
     691           1 :                 let actorName = this.template[type].Projectile.ActorName || "";
     692           1 :                 let impactActorName = this.template[type].Projectile.ImpactActorName || "";
     693           1 :                 let impactAnimationLifetime = this.template[type].Projectile.ImpactAnimationLifetime || 0;
     694             : 
     695             :                 // TODO: Use unit rotation to implement x/z offsets.
     696           1 :                 let deltaLaunchPoint = new Vector3D(0, +this.template[type].Projectile.LaunchPoint["@y"], 0);
     697           1 :                 let launchPoint = Vector3D.add(selfPosition, deltaLaunchPoint);
     698             : 
     699           1 :                 let cmpVisual = Engine.QueryInterface(this.entity, IID_Visual);
     700           1 :                 if (cmpVisual)
     701             :                 {
     702             :                         // if the projectile definition is missing from the template
     703             :                         // then fallback to the projectile name and launchpoint in the visual actor
     704           0 :                         if (!actorName)
     705           0 :                                 actorName = cmpVisual.GetProjectileActor();
     706             : 
     707           0 :                         let visualActorLaunchPoint = cmpVisual.GetProjectileLaunchPoint();
     708           0 :                         if (visualActorLaunchPoint.length() > 0)
     709           0 :                                 launchPoint = visualActorLaunchPoint;
     710             :                 }
     711             : 
     712           1 :                 let cmpProjectileManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ProjectileManager);
     713           1 :                 data.projectileId = cmpProjectileManager.LaunchProjectileAtPoint(launchPoint, data.position, horizSpeed, gravity, actorName, impactActorName, impactAnimationLifetime);
     714             : 
     715           1 :                 let cmpSound = Engine.QueryInterface(this.entity, IID_Sound);
     716           1 :                 data.attackImpactSound = cmpSound ? cmpSound.GetSoundGroup("attack_impact_" + type.toLowerCase()) : "";
     717             : 
     718           1 :                 data.friendlyFire = this.template[type].Projectile.FriendlyFire == "true";
     719             :         }
     720             :         else
     721             :         {
     722           0 :                 data.position = targetPosition;
     723           0 :                 data.direction = Vector3D.sub(targetPosition, selfPosition);
     724             :         }
     725           1 :         if (delay)
     726             :         {
     727           1 :                 let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
     728           1 :                 cmpTimer.SetTimeout(SYSTEM_ENTITY, IID_DelayedDamage, "Hit", delay, data);
     729             :         }
     730             :         else
     731           0 :                 Engine.QueryInterface(SYSTEM_ENTITY, IID_DelayedDamage).Hit(data, 0);
     732             : };
     733             : 
     734             : /**
     735             :  * @param {number} - The entity ID of the target to check.
     736             :  * @return {boolean} - Whether this entity is in range of its target.
     737             :  */
     738           2 : Attack.prototype.IsTargetInRange = function(target, type)
     739             : {
     740           0 :         const range = this.GetRange(type);
     741           0 :         return Engine.QueryInterface(SYSTEM_ENTITY, IID_ObstructionManager).IsInTargetParabolicRange(
     742             :                 this.entity,
     743             :                 target,
     744             :                 range.min,
     745             :                 range.max,
     746             :                 this.GetAttackYOrigin(type),
     747             :                 false);
     748             : };
     749             : 
     750           2 : Attack.prototype.OnValueModification = function(msg)
     751             : {
     752           0 :         if (msg.component != "Attack")
     753           0 :                 return;
     754             : 
     755           0 :         let cmpUnitAI = Engine.QueryInterface(this.entity, IID_UnitAI);
     756           0 :         if (!cmpUnitAI)
     757           0 :                 return;
     758             : 
     759           0 :         if (this.GetAttackTypes().some(type =>
     760           0 :               msg.valueNames.indexOf("Attack/" + type + "/MaxRange") != -1))
     761           0 :                 cmpUnitAI.UpdateRangeQueries();
     762             : };
     763             : 
     764           2 : Attack.prototype.GetRangeOverlays = function(type = "Ranged")
     765             : {
     766           0 :         if (!this.template[type] || !this.template[type].RangeOverlay)
     767           0 :                 return [];
     768             : 
     769           0 :         let range = this.GetRange(type);
     770           0 :         let rangeOverlays = [];
     771           0 :         for (let i in range)
     772           0 :                 if ((i == "min" || i == "max") && range[i])
     773           0 :                         rangeOverlays.push({
     774             :                                 "radius": range[i],
     775             :                                 "texture": this.template[type].RangeOverlay.LineTexture,
     776             :                                 "textureMask": this.template[type].RangeOverlay.LineTextureMask,
     777             :                                 "thickness": +this.template[type].RangeOverlay.LineThickness,
     778             :                         });
     779           0 :         return rangeOverlays;
     780             : };
     781             : 
     782           2 : Engine.RegisterComponentType(IID_Attack, "Attack", Attack);

Generated by: LCOV version 1.14