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());
|