LCOV - code coverage report
Current view: top level - simulation/helpers - Attack.js (source / functions) Hit Total Coverage
Test: lcov.info Lines: 123 132 93.2 %
Date: 2023-04-02 12:52:40 Functions: 11 11 100.0 %

          Line data    Source code
       1             : /**
       2             :  * Provides attack and damage-related helpers.
       3             :  */
       4             : function AttackHelper() {}
       5             : 
       6             : const DirectEffectsSchema =
       7           5 :         "<element name='Damage'>" +
       8             :                 "<oneOrMore>" +
       9             :                         "<element a:help='One or more elements describing damage types'>" +
      10             :                                 "<anyName/>" +
      11             :                                 "<ref name='nonNegativeDecimal' />" +
      12             :                         "</element>" +
      13             :                 "</oneOrMore>" +
      14             :         "</element>" +
      15             :         "<element name='Capture' a:help='Capture points value'>" +
      16             :                 "<ref name='nonNegativeDecimal'/>" +
      17             :         "</element>";
      18             : 
      19             : const StatusEffectsSchema =
      20           5 :         "<element name='ApplyStatus' a:help='Effects like poisoning or burning a unit.'>" +
      21             :                 "<oneOrMore>" +
      22             :                         "<element>" +
      23             :                                 "<anyName a:help='The name must have a matching JSON file in data/status_effects.'/>" +
      24             :                                 "<interleave>" +
      25             :                                         "<optional>" +
      26             :                                                 "<element name='Duration' a:help='The duration of the status while the effect occurs.'><ref name='nonNegativeDecimal'/></element>" +
      27             :                                         "</optional>" +
      28             :                                         "<optional>" +
      29             :                                                 "<interleave>" +
      30             :                                                         "<element name='Interval' a:help='Interval between the occurances of the effect.'><ref name='nonNegativeDecimal'/></element>" +
      31             :                                                         "<oneOrMore>" +
      32             :                                                                 "<choice>" +
      33             :                                                                         DirectEffectsSchema +
      34             :                                                                 "</choice>" +
      35             :                                                         "</oneOrMore>" +
      36             :                                                 "</interleave>" +
      37             :                                         "</optional>" +
      38             :                                         "<optional>" +
      39             :                                                 ModificationsSchema +
      40             :                                         "</optional>" +
      41             :                                         "<element name='Stackability' a:help='Defines how this status effect stacks, i.e. how subsequent status effects of the same kind are handled. Choices are: “Ignore”, which means a new one is ignored, “Extend”, which means the duration of a new one is added to the already active status effect, “Replace”, which means the currently active status effect is removed and the new one is put in place and “Stack”, which means that the status effect can be added multiple times.'>" +
      42             :                                                 "<choice>" +
      43             :                                                         "<value>Ignore</value>" +
      44             :                                                         "<value>Extend</value>" +
      45             :                                                         "<value>Replace</value>" +
      46             :                                                         "<value>Stack</value>" +
      47             :                                                 "</choice>" +
      48             :                                         "</element>" +
      49             :                                 "</interleave>" +
      50             :                         "</element>" +
      51             :                 "</oneOrMore>" +
      52             :         "</element>";
      53             : 
      54             : /**
      55             :  * Builds a RelaxRNG schema of possible attack effects.
      56             :  * See globalscripts/AttackEffects.js for possible elements.
      57             :  * Attacks may also have a "Bonuses" element.
      58             :  *
      59             :  * @return {string} - RelaxNG schema string.
      60             :  */
      61           5 : AttackHelper.prototype.BuildAttackEffectsSchema = function()
      62             : {
      63           5 :         return "" +
      64             :         "<oneOrMore>" +
      65             :                 "<choice>" +
      66             :                         DirectEffectsSchema +
      67             :                         StatusEffectsSchema +
      68             :                 "</choice>" +
      69             :         "</oneOrMore>" +
      70             :         "<optional>" +
      71             :                 "<element name='Bonuses'>" +
      72             :                         "<zeroOrMore>" +
      73             :                                 "<element>" +
      74             :                                         "<anyName/>" +
      75             :                                         "<interleave>" +
      76             :                                                 "<optional>" +
      77             :                                                         "<element name='Civ' a:help='If an entity has this civ then the bonus is applied'><text/></element>" +
      78             :                                                 "</optional>" +
      79             :                                                 "<element name='Classes' a:help='If an entity has all these classes then the bonus is applied'><text/></element>" +
      80             :                                                 "<element name='Multiplier' a:help='The effect strength is multiplied by this'><ref name='nonNegativeDecimal'/></element>" +
      81             :                                         "</interleave>" +
      82             :                                 "</element>" +
      83             :                         "</zeroOrMore>" +
      84             :                 "</element>" +
      85             :         "</optional>";
      86             : };
      87             : 
      88             : /**
      89             :  * Returns a template-like object of attack effects.
      90             :  */
      91           5 : AttackHelper.prototype.GetAttackEffectsData = function(valueModifRoot, template, entity)
      92             : {
      93          25 :         let ret = {};
      94             : 
      95          25 :         if (template.Damage)
      96             :         {
      97          16 :                 ret.Damage = {};
      98          16 :                 let applyMods = damageType =>
      99          46 :                         ApplyValueModificationsToEntity(valueModifRoot + "/Damage/" + damageType, +(template.Damage[damageType] || 0), entity);
     100          16 :                 for (let damageType in template.Damage)
     101          46 :                         ret.Damage[damageType] = applyMods(damageType);
     102             :         }
     103          25 :         if (template.Capture)
     104           5 :                 ret.Capture = ApplyValueModificationsToEntity(valueModifRoot + "/Capture", +(template.Capture || 0), entity);
     105             : 
     106          25 :         if (template.ApplyStatus)
     107           1 :                 ret.ApplyStatus = this.GetStatusEffectsData(valueModifRoot, template.ApplyStatus, entity);
     108             : 
     109          25 :         if (template.Bonuses)
     110          10 :                 ret.Bonuses = template.Bonuses;
     111             : 
     112          25 :         return ret;
     113             : };
     114             : 
     115           5 : AttackHelper.prototype.GetStatusEffectsData = function(valueModifRoot, template, entity)
     116             : {
     117           1 :         let result = {};
     118           1 :         for (let effect in template)
     119             :         {
     120           1 :                 let statusTemplate = template[effect];
     121           1 :                 result[effect] = {
     122             :                         "Duration": ApplyValueModificationsToEntity(valueModifRoot + "/ApplyStatus/" + effect + "/Duration", +(statusTemplate.Duration || 0), entity),
     123             :                         "Interval": ApplyValueModificationsToEntity(valueModifRoot + "/ApplyStatus/" + effect + "/Interval", +(statusTemplate.Interval || 0), entity),
     124             :                         "Stackability": statusTemplate.Stackability
     125             :                 };
     126           1 :                 Object.assign(result[effect], this.GetAttackEffectsData(valueModifRoot + "/ApplyStatus" + effect, statusTemplate, entity));
     127           1 :                 if (statusTemplate.Modifiers)
     128           1 :                         result[effect].Modifiers = this.GetStatusEffectsModifications(valueModifRoot, statusTemplate.Modifiers, entity, effect);
     129             :         }
     130           1 :         return result;
     131             : };
     132             : 
     133           5 : AttackHelper.prototype.GetStatusEffectsModifications = function(valueModifRoot, template, entity, effect)
     134             : {
     135           1 :         let modifiers = {};
     136           1 :         for (let modifier in template)
     137             :         {
     138           1 :                 let modifierTemplate = template[modifier];
     139           1 :                 modifiers[modifier] = {
     140             :                         "Paths": modifierTemplate.Paths,
     141             :                         "Affects": modifierTemplate.Affects
     142             :                 };
     143           1 :                 if (modifierTemplate.Add !== undefined)
     144           1 :                         modifiers[modifier].Add = ApplyValueModificationsToEntity(valueModifRoot + "/ApplyStatus/" + effect + "/Modifiers/" + modifier + "/Add", +modifierTemplate.Add, entity);
     145           1 :                 if (modifierTemplate.Multiply !== undefined)
     146           0 :                         modifiers[modifier].Multiply = ApplyValueModificationsToEntity(valueModifRoot + "/ApplyStatus/" + effect + "/Modifiers/" + modifier + "/Multiply", +modifierTemplate.Multiply, entity);
     147           1 :                 if (modifierTemplate.Replace !== undefined)
     148           0 :                         modifiers[modifier].Replace = modifierTemplate.Replace;
     149             :         }
     150           1 :         return modifiers;
     151             : };
     152             : 
     153             : /**
     154             :  * Calculate the total effect taking bonus and resistance into account.
     155             :  *
     156             :  * @param {number} target - The target of the attack.
     157             :  * @param {Object} effectData - The effects calculate the effect for.
     158             :  * @param {string} effectType - The type of effect to apply (e.g. Damage, Capture or ApplyStatus).
     159             :  * @param {number} bonusMultiplier - The factor to multiply the total effect with.
     160             :  * @param {Object} cmpResistance - Optionally the resistance component of the target.
     161             :  *
     162             :  * @return {number} - The total value of the effect.
     163             :  */
     164           5 : AttackHelper.prototype.GetTotalAttackEffects = function(target, effectData, effectType, bonusMultiplier, cmpResistance)
     165             : {
     166          59 :         let total = 0;
     167          59 :         if (!cmpResistance)
     168          49 :                 cmpResistance = Engine.QueryInterface(target, IID_Resistance);
     169             : 
     170          59 :         let resistanceStrengths = cmpResistance ? cmpResistance.GetEffectiveResistanceAgainst(effectType) : {};
     171             : 
     172          59 :         if (effectType == "Damage")
     173          46 :                 for (let type in effectData.Damage)
     174         110 :                         total += effectData.Damage[type] * Math.pow(0.9, resistanceStrengths.Damage ? resistanceStrengths.Damage[type] || 0 : 0);
     175          13 :         else if (effectType == "Capture")
     176             :         {
     177           8 :                 total = effectData.Capture * Math.pow(0.9, resistanceStrengths.Capture || 0);
     178             : 
     179             :                 // If Health is lower we are more susceptible to capture attacks.
     180           8 :                 let cmpHealth = Engine.QueryInterface(target, IID_Health);
     181           8 :                 if (cmpHealth)
     182           7 :                         total /= 0.1 + 0.9 * cmpHealth.GetHitpoints() / cmpHealth.GetMaxHitpoints();
     183             :         }
     184          59 :         if (effectType != "ApplyStatus")
     185          54 :                 return total * bonusMultiplier;
     186             : 
     187           5 :         if (!resistanceStrengths.ApplyStatus)
     188           2 :                 return effectData[effectType];
     189             : 
     190           3 :         let result = {};
     191           3 :         for (let statusEffect in effectData[effectType])
     192             :         {
     193           4 :                 if (!resistanceStrengths.ApplyStatus[statusEffect])
     194             :                 {
     195           0 :                         result[statusEffect] = effectData[effectType][statusEffect];
     196           0 :                         continue;
     197             :                 }
     198             : 
     199           4 :                 if (randBool(resistanceStrengths.ApplyStatus[statusEffect].blockChance))
     200           2 :                         continue;
     201             : 
     202           2 :                 result[statusEffect] = effectData[effectType][statusEffect];
     203             : 
     204           2 :                 if (effectData[effectType][statusEffect].Duration)
     205           2 :                         result[statusEffect].Duration = effectData[effectType][statusEffect].Duration *
     206             :                                 resistanceStrengths.ApplyStatus[statusEffect].duration;
     207             :         }
     208           3 :         return result;
     209             : 
     210             : };
     211             : 
     212             : /**
     213             :  * Get the list of players affected by the damage.
     214             :  * @param {number}  attackerOwner - The player id of the attacker.
     215             :  * @param {boolean} friendlyFire - A flag indicating if allied entities are also damaged.
     216             :  * @return {number[]} The ids of players need to be damaged.
     217             :  */
     218           5 : AttackHelper.prototype.GetPlayersToDamage = function(attackerOwner, friendlyFire)
     219             : {
     220          20 :         if (!friendlyFire)
     221          10 :                 return QueryPlayerIDInterface(attackerOwner).GetEnemies();
     222             : 
     223          10 :         return Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager).GetAllPlayers();
     224             : };
     225             : 
     226             : /**
     227             :  * Damages units around a given origin.
     228             :  * @param {Object}   data - The data sent by the caller.
     229             :  * @param {string}   data.type - The type of damage.
     230             :  * @param {Object}   data.attackData - The attack data.
     231             :  * @param {number}   data.attacker - The entity id of the attacker.
     232             :  * @param {number}   data.attackerOwner - The player id of the attacker.
     233             :  * @param {Vector2D} data.origin - The origin of the projectile hit.
     234             :  * @param {number}   data.radius - The radius of the splash damage.
     235             :  * @param {string}   data.shape - The shape of the radius.
     236             :  * @param {Vector3D} [data.direction] - The unit vector defining the direction. Needed for linear splash damage.
     237             :  * @param {boolean}  data.friendlyFire - A flag indicating if allied entities also ought to be damaged.
     238             :  */
     239           5 : AttackHelper.prototype.CauseDamageOverArea = function(data)
     240             : {
     241          10 :         let nearEnts = PositionHelper.EntitiesNearPoint(data.origin, data.radius,
     242             :                 this.GetPlayersToDamage(data.attackerOwner, data.friendlyFire));
     243          10 :         let damageMultiplier = 1;
     244             : 
     245          10 :         let cmpObstructionManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ObstructionManager);
     246             : 
     247             :         // Cycle through all the nearby entities and damage it appropriately based on its distance from the origin.
     248          10 :         for (let ent of nearEnts)
     249             :         {
     250             :                 // Correct somewhat for the entity's obstruction radius.
     251             :                 // TODO: linear falloff should arguably use something cleverer.
     252          20 :                 let distance = cmpObstructionManager.DistanceToPoint(ent, data.origin.x, data.origin.y);
     253             : 
     254          20 :                 if (data.shape == 'Circular') // circular effect with quadratic falloff in every direction
     255          14 :                         damageMultiplier = 1 - distance * distance / (data.radius * data.radius);
     256           6 :                 else if (data.shape == 'Linear') // linear effect with quadratic falloff in two directions (only used for certain missiles)
     257             :                 {
     258             :                         // The entity has a position here since it was returned by the range manager.
     259           6 :                         let entityPosition = Engine.QueryInterface(ent, IID_Position).GetPosition2D();
     260           6 :                         let relativePos = entityPosition.sub(data.origin).normalize().mult(distance);
     261             : 
     262             :                         // Get the position relative to the missile direction.
     263           6 :                         let direction = Vector2D.from3D(data.direction);
     264           6 :                         let parallelPos = relativePos.dot(direction);
     265           6 :                         let perpPos = relativePos.cross(direction);
     266             : 
     267             :                         // The width of linear splash is one fifth of the normal splash radius.
     268           6 :                         let width = data.radius / 5;
     269             : 
     270             :                         // Check that the unit is within the distance splash width of the line starting at the missile's
     271             :                         // landing point which extends in the direction of the missile for length splash radius.
     272           6 :                         if (parallelPos >= 0 && Math.abs(perpPos) < width) // If in radius, quadratic falloff in both directions
     273           4 :                                 damageMultiplier = (1 - parallelPos * parallelPos / (data.radius * data.radius)) *
     274             :                                         (1 - perpPos * perpPos / (width * width));
     275             :                         else
     276           2 :                                 damageMultiplier = 0;
     277             :                 }
     278             :                 else // In case someone calls this function with an invalid shape.
     279             :                 {
     280           0 :                         warn("The " + data.shape + " splash damage shape is not implemented!");
     281             :                 }
     282             :                 // The RangeManager can return units that are too far away (due to approximations there)
     283             :                 // so the multiplier can end up below 0.
     284          20 :                 damageMultiplier = Math.max(0, damageMultiplier);
     285             : 
     286          20 :                 data.type += ".Splash";
     287          20 :                 this.HandleAttackEffects(ent, data, damageMultiplier);
     288             :         }
     289             : };
     290             : /**
     291             :  * Handle an attack peformed on an entity.
     292             :  *
     293             :  * @param {number} target - The targetted entityID.
     294             :  * @param {Object} data - The data of the attack.
     295             :  * @param {string} data.type - The type of attack that was performed (e.g. "Melee" or "Capture").
     296             :  * @param {Object} data.effectData - The effects use.
     297             :  * @param {number} data.attacker - The entityID that attacked us.
     298             :  * @param {number} data.attackerOwner - The playerID that owned the attacker when the attack was performed.
     299             :  * @param {number} bonusMultiplier - The factor to multiply the total effect with, defaults to 1.
     300             :  *
     301             :  * @return {boolean} - Whether we handled the attack.
     302             :  */
     303           5 : AttackHelper.prototype.HandleAttackEffects = function(target, data, bonusMultiplier = 1)
     304             : {
     305          51 :         let cmpResistance = Engine.QueryInterface(target, IID_Resistance);
     306          51 :         if (cmpResistance && cmpResistance.IsInvulnerable())
     307           1 :                 return false;
     308             : 
     309          50 :         bonusMultiplier *= !data.attackData.Bonuses ? 1 : this.GetAttackBonus(data.attacker, target, data.type, data.attackData.Bonuses);
     310             : 
     311          50 :         let targetState = {};
     312          50 :         for (let receiver of g_AttackEffects.Receivers())
     313             :         {
     314          84 :                 if (!data.attackData[receiver.type])
     315          17 :                         continue;
     316             : 
     317          67 :                 let cmpReceiver = Engine.QueryInterface(target, global[receiver.IID]);
     318          67 :                 if (!cmpReceiver)
     319           8 :                         continue;
     320             : 
     321          59 :                 Object.assign(targetState, cmpReceiver[receiver.method](this.GetTotalAttackEffects(target, data.attackData, receiver.type, bonusMultiplier, cmpResistance), data.attacker, data.attackerOwner));
     322             :         }
     323             : 
     324          50 :         if (!Object.keys(targetState).length)
     325           5 :                 return false;
     326             : 
     327          45 :         Engine.PostMessage(target, MT_Attacked, {
     328             :                 "type": data.type,
     329             :                 "target": target,
     330             :                 "attacker": data.attacker,
     331             :                 "attackerOwner": data.attackerOwner,
     332             :                 "damage": -(targetState.healthChange || 0),
     333             :                 "capture": targetState.captureChange || 0,
     334             :                 "statusEffects": targetState.inflictedStatuses || [],
     335             :                 "fromStatusEffect": !!data.attackData.StatusEffect,
     336             :         });
     337             : 
     338             :         // We do not want an entity to get XP from active Status Effects.
     339          45 :         if (!!data.attackData.StatusEffect)
     340           0 :                 return true;
     341             : 
     342          45 :         let cmpPromotion = Engine.QueryInterface(data.attacker, IID_Promotion);
     343          45 :         if (cmpPromotion && targetState.xp)
     344           0 :                 cmpPromotion.IncreaseXp(targetState.xp);
     345             : 
     346          45 :         return true;
     347             : };
     348             : 
     349             : /**
     350             :  * Calculates the attack damage multiplier against a target.
     351             :  * @param {number} source - The source entity's id.
     352             :  * @param {number} target - The target entity's id.
     353             :  * @param {string} type - The type of attack.
     354             :  * @param {Object} template - The bonus' template.
     355             :  * @return {number} - The source entity's attack bonus against the specified target.
     356             :  */
     357           5 : AttackHelper.prototype.GetAttackBonus = function(source, target, type, template)
     358             : {
     359          21 :         let cmpIdentity = Engine.QueryInterface(target, IID_Identity);
     360          21 :         if (!cmpIdentity)
     361           0 :                 return 1;
     362             : 
     363          21 :         let attackBonus = 1;
     364          21 :         let targetClasses = cmpIdentity.GetClassesList();
     365          21 :         let targetCiv = cmpIdentity.GetCiv();
     366             : 
     367             :         // Multiply the bonuses for all matching classes.
     368          21 :         for (let key in template)
     369             :         {
     370          13 :                 let bonus = template[key];
     371          13 :                 if (bonus.Civ && bonus.Civ !== targetCiv)
     372           0 :                         continue;
     373          13 :                 if (!bonus.Classes || MatchesClassList(targetClasses, bonus.Classes))
     374          11 :                         attackBonus *= ApplyValueModificationsToEntity("Attack/" + type + "/Bonuses/" + key + "/Multiplier", +bonus.Multiplier, source);
     375             :         }
     376             : 
     377          21 :         return attackBonus;
     378             : };
     379             : 
     380           5 : Engine.RegisterGlobal("AttackHelper", new AttackHelper());
     381           5 : Engine.RegisterGlobal("g_AttackEffects", new AttackEffects());

Generated by: LCOV version 1.14