source: ps/trunk/binaries/data/mods/public/simulation/components/Attack.js@ 25233

Last change on this file since 25233 was 25233, checked in by Freagarach, 3 years ago

Combine attack times in a single node.

Reduces duplication, allows all attack types to cause splash damage.
While at it, makes delay and minimal range optional (for obvious defaults are available).

Split from D368, so basically a patch by @bb.

Note that it may seem like one can arbitrarily name an attack now, but that is not true.

Differential revision: D2002
Comments by: @bb, @Nescio, @Stan, @wraitii

  • Property svn:eol-style set to native
File size: 22.0 KB
Line 
1function Attack() {}
2
3var g_AttackTypes = ["Melee", "Ranged", "Capture"];
4
5Attack.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
15Attack.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
25Attack.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 "<ElevationBonus>15.0</ElevationBonus>" +
61 "<PrepareTime>800</PrepareTime>" +
62 "<RepeatTime>1600</RepeatTime>" +
63 "<Delay>1000</Delay>" +
64 "<Bonuses>" +
65 "<Bonus1>" +
66 "<Classes>Cavalry</Classes>" +
67 "<Multiplier>2</Multiplier>" +
68 "</Bonus1>" +
69 "</Bonuses>" +
70 "<Projectile>" +
71 "<Speed>50.0</Speed>" +
72 "<Spread>2.5</Spread>" +
73 "<ActorName>props/units/weapons/rock_flaming.xml</ActorName>" +
74 "<ImpactActorName>props/units/weapons/rock_explosion.xml</ImpactActorName>" +
75 "<ImpactAnimationLifetime>0.1</ImpactAnimationLifetime>" +
76 "<FriendlyFire>false</FriendlyFire>" +
77 "</Projectile>" +
78 "<RestrictedClasses datatype=\"tokens\">Champion</RestrictedClasses>" +
79 "<Splash>" +
80 "<Shape>Circular</Shape>" +
81 "<Range>20</Range>" +
82 "<FriendlyFire>false</FriendlyFire>" +
83 "<Damage>" +
84 "<Hack>0.0</Hack>" +
85 "<Pierce>10.0</Pierce>" +
86 "<Crush>0.0</Crush>" +
87 "</Damage>" +
88 "</Splash>" +
89 "</Ranged>" +
90 "<Slaughter>" +
91 "<Damage>" +
92 "<Hack>1000.0</Hack>" +
93 "<Pierce>0.0</Pierce>" +
94 "<Crush>0.0</Crush>" +
95 "</Damage>" +
96 "<RepeatTime>1000</RepeatTime>" +
97 "<MaxRange>4.0</MaxRange>" +
98 "</Slaughter>" +
99 "</a:example>" +
100 "<oneOrMore>" +
101 "<element>" +
102 "<anyName a:help='Currently one of Melee, Ranged, Capture or Slaughter.'/>" +
103 "<interleave>" +
104 "<element name='AttackName' a:help='Name of the attack, to be displayed in the GUI. Optionally includes a translate context attribute.'>" +
105 "<optional>" +
106 "<attribute name='context'>" +
107 "<text/>" +
108 "</attribute>" +
109 "</optional>" +
110 "<text/>" +
111 "</element>" +
112 Attacking.BuildAttackEffectsSchema() +
113 "<element name='MaxRange' a:help='Maximum attack range (in metres)'><ref name='nonNegativeDecimal'/></element>" +
114 "<optional>" +
115 "<element name='MinRange' a:help='Minimum attack range (in metres). Defaults to 0.'><ref name='nonNegativeDecimal'/></element>" +
116 "</optional>" +
117 "<optional>"+
118 "<element name='ElevationBonus' a:help='The offset height from which the attack occurs, relative to the entity position. Defaults to 0.'><ref name='nonNegativeDecimal'/></element>" +
119 "</optional>" +
120 "<optional>" +
121 "<element name='RangeOverlay'>" +
122 "<interleave>" +
123 "<element name='LineTexture'><text/></element>" +
124 "<element name='LineTextureMask'><text/></element>" +
125 "<element name='LineThickness'><ref name='nonNegativeDecimal'/></element>" +
126 "</interleave>" +
127 "</element>" +
128 "</optional>" +
129 "<optional>" +
130 "<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.'>" +
131 "<data type='nonNegativeInteger'/>" +
132 "</element>" +
133 "</optional>" +
134 "<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
135 "<data type='positiveInteger'/>" +
136 "</element>" +
137 "<optional>" +
138 "<element name='Delay' a:help='Delay of applying the effects in milliseconds after the attack has landed. Defaults to 0.'><ref name='nonNegativeDecimal'/></element>" +
139 "</optional>" +
140 "<optional>" +
141 "<element name='Splash'>" +
142 "<interleave>" +
143 "<element name='Shape' a:help='Shape of the splash damage, can be circular or linear'><text/></element>" +
144 "<element name='Range' a:help='Size of the area affected by the splash'><ref name='nonNegativeDecimal'/></element>" +
145 "<element name='FriendlyFire' a:help='Whether the splash damage can hurt non enemy units'><data type='boolean'/></element>" +
146 Attacking.BuildAttackEffectsSchema() +
147 "</interleave>" +
148 "</element>" +
149 "</optional>" +
150 "<optional>" +
151 "<element name='Projectile'>" +
152 "<interleave>" +
153 "<element name='Speed' a:help='Speed of projectiles (in meters per second).'>" +
154 "<ref name='positiveDecimal'/>" +
155 "</element>" +
156 "<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>" +
157 "<element name='Gravity' a:help='The gravity affecting the projectile. This affects the shape of the flight curve.'>" +
158 "<ref name='nonNegativeDecimal'/>" +
159 "</element>" +
160 "<element name='FriendlyFire' a:help='Whether stray missiles can hurt non enemy units.'><data type='boolean'/></element>" +
161 "<optional>" +
162 "<element name='LaunchPoint' a:help='Delta from the unit position where to launch the projectile.'>" +
163 "<attribute name='y'>" +
164 "<data type='decimal'/>" +
165 "</attribute>" +
166 "</element>" +
167 "</optional>" +
168 "<optional>" +
169 "<element name='ActorName' a:help='actor of the projectile animation.'>" +
170 "<text/>" +
171 "</element>" +
172 "</optional>" +
173 "<optional>" +
174 "<element name='ImpactActorName' a:help='actor of the projectile impact animation'>" +
175 "<text/>" +
176 "</element>" +
177 "<element name='ImpactAnimationLifetime' a:help='length of the projectile impact animation.'>" +
178 "<ref name='positiveDecimal'/>" +
179 "</element>" +
180 "</optional>" +
181 "</interleave>" +
182 "</element>" +
183 "</optional>" +
184 Attack.prototype.preferredClassesSchema +
185 Attack.prototype.restrictedClassesSchema +
186 "</interleave>" +
187 "</element>" +
188 "</oneOrMore>";
189
190Attack.prototype.Init = function()
191{
192};
193
194Attack.prototype.Serialize = null; // we have no dynamic state to save
195
196Attack.prototype.GetAttackTypes = function(wantedTypes)
197{
198 let types = g_AttackTypes.filter(type => !!this.template[type]);
199 if (!wantedTypes)
200 return types;
201
202 let wantedTypesReal = wantedTypes.filter(wtype => wtype.indexOf("!") != 0);
203 return types.filter(type => wantedTypes.indexOf("!" + type) == -1 &&
204 (!wantedTypesReal || !wantedTypesReal.length || wantedTypesReal.indexOf(type) != -1));
205};
206
207Attack.prototype.GetPreferredClasses = function(type)
208{
209 if (this.template[type] && this.template[type].PreferredClasses &&
210 this.template[type].PreferredClasses._string)
211 return this.template[type].PreferredClasses._string.split(/\s+/);
212
213 return [];
214};
215
216Attack.prototype.GetRestrictedClasses = function(type)
217{
218 if (this.template[type] && this.template[type].RestrictedClasses &&
219 this.template[type].RestrictedClasses._string)
220 return this.template[type].RestrictedClasses._string.split(/\s+/);
221
222 return [];
223};
224
225Attack.prototype.CanAttack = function(target, wantedTypes)
226{
227 let cmpFormation = Engine.QueryInterface(target, IID_Formation);
228 if (cmpFormation)
229 return true;
230
231 let cmpThisPosition = Engine.QueryInterface(this.entity, IID_Position);
232 let cmpTargetPosition = Engine.QueryInterface(target, IID_Position);
233 if (!cmpThisPosition || !cmpTargetPosition || !cmpThisPosition.IsInWorld() || !cmpTargetPosition.IsInWorld())
234 return false;
235
236 let cmpIdentity = QueryMiragedInterface(target, IID_Identity);
237 if (!cmpIdentity)
238 return false;
239
240 let cmpHealth = QueryMiragedInterface(target, IID_Health);
241 let targetClasses = cmpIdentity.GetClassesList();
242 if (targetClasses.indexOf("Domestic") != -1 && this.template.Slaughter && cmpHealth && cmpHealth.GetHitpoints() &&
243 (!wantedTypes || !wantedTypes.filter(wType => wType.indexOf("!") != 0).length))
244 return true;
245
246 let cmpEntityPlayer = QueryOwnerInterface(this.entity);
247 let cmpTargetPlayer = QueryOwnerInterface(target);
248 if (!cmpTargetPlayer || !cmpEntityPlayer)
249 return false;
250
251 let types = this.GetAttackTypes(wantedTypes);
252 let entityOwner = cmpEntityPlayer.GetPlayerID();
253 let targetOwner = cmpTargetPlayer.GetPlayerID();
254 let cmpCapturable = QueryMiragedInterface(target, IID_Capturable);
255
256 // Check if the relative height difference is larger than the attack range
257 // If the relative height is bigger, it means they will never be able to
258 // reach each other, no matter how close they come.
259 let heightDiff = Math.abs(cmpThisPosition.GetHeightOffset() - cmpTargetPosition.GetHeightOffset());
260
261 for (let type of types)
262 {
263 if (type != "Capture" && (!cmpEntityPlayer.IsEnemy(targetOwner) || !cmpHealth || !cmpHealth.GetHitpoints()))
264 continue;
265
266 if (type == "Capture" && (!cmpCapturable || !cmpCapturable.CanCapture(entityOwner)))
267 continue;
268
269 if (heightDiff > this.GetRange(type).max)
270 continue;
271
272 let restrictedClasses = this.GetRestrictedClasses(type);
273 if (!restrictedClasses.length)
274 return true;
275
276 if (!MatchesClassList(targetClasses, restrictedClasses))
277 return true;
278 }
279
280 return false;
281};
282
283/**
284 * Returns undefined if we have no preference or the lowest index of a preferred class.
285 */
286Attack.prototype.GetPreference = function(target)
287{
288 let cmpIdentity = Engine.QueryInterface(target, IID_Identity);
289 if (!cmpIdentity)
290 return undefined;
291
292 let targetClasses = cmpIdentity.GetClassesList();
293
294 let minPref;
295 for (let type of this.GetAttackTypes())
296 {
297 let preferredClasses = this.GetPreferredClasses(type);
298 for (let pref = 0; pref < preferredClasses.length; ++pref)
299 {
300 if (MatchesClassList(targetClasses, preferredClasses[pref]))
301 {
302 if (pref === 0)
303 return pref;
304 if ((minPref === undefined || minPref > pref))
305 minPref = pref;
306 }
307 }
308 }
309 return minPref;
310};
311
312/**
313 * Get the full range of attack using all available attack types.
314 */
315Attack.prototype.GetFullAttackRange = function()
316{
317 let ret = { "min": Infinity, "max": 0 };
318 for (let type of this.GetAttackTypes())
319 {
320 let range = this.GetRange(type);
321 ret.min = Math.min(ret.min, range.min);
322 ret.max = Math.max(ret.max, range.max);
323 }
324 return ret;
325};
326
327Attack.prototype.GetAttackEffectsData = function(type, splash)
328{
329 let template = this.template[type];
330 if (splash)
331 template = template.Splash;
332 return Attacking.GetAttackEffectsData("Attack/" + type + (splash ? "/Splash" : ""), template, this.entity);
333};
334
335/**
336 * Find the best attack against a target.
337 * @param {number} target - The entity-ID of the target.
338 * @param {boolean} allowCapture - Whether capturing is allowed.
339 * @return {string} - The preferred attack type.
340 */
341Attack.prototype.GetBestAttackAgainst = function(target, allowCapture)
342{
343 let cmpFormation = Engine.QueryInterface(target, IID_Formation);
344 if (cmpFormation)
345 {
346 // TODO: Formation against formation needs review
347 let types = this.GetAttackTypes();
348 return g_AttackTypes.find(attack => types.indexOf(attack) != -1);
349 }
350
351 let cmpIdentity = Engine.QueryInterface(target, IID_Identity);
352 if (!cmpIdentity)
353 return undefined;
354
355 // Always slaughter domestic animals instead of using a normal attack
356 if (this.template.Slaughter && cmpIdentity.HasClass("Domestic"))
357 return "Slaughter";
358
359 let types = this.GetAttackTypes().filter(type => this.CanAttack(target, [type]));
360
361 // Check whether the target is capturable and prefer that when it is allowed.
362 let captureIndex = types.indexOf("Capture");
363 if (captureIndex != -1)
364 {
365 if (allowCapture)
366 return "Capture";
367 types.splice(captureIndex, 1);
368 }
369
370 let targetClasses = cmpIdentity.GetClassesList();
371 let isPreferred = attackType => MatchesClassList(targetClasses, this.GetPreferredClasses(attackType));
372
373 return types.sort((a, b) =>
374 (types.indexOf(a) + (isPreferred(a) ? types.length : 0)) -
375 (types.indexOf(b) + (isPreferred(b) ? types.length : 0))).pop();
376};
377
378Attack.prototype.CompareEntitiesByPreference = function(a, b)
379{
380 let aPreference = this.GetPreference(a);
381 let bPreference = this.GetPreference(b);
382
383 if (aPreference === null && bPreference === null) return 0;
384 if (aPreference === null) return 1;
385 if (bPreference === null) return -1;
386 return aPreference - bPreference;
387};
388
389Attack.prototype.GetAttackName = function(type)
390{
391 return {
392 "name": this.template[type].AttackName._string || this.template[type].AttackName,
393 "context": this.template[type].AttackName["@context"]
394 };
395};
396
397Attack.prototype.GetRepeatTime = function(type)
398{
399 let repeatTime = 1000;
400
401 if (this.template[type] && this.template[type].RepeatTime)
402 repeatTime = +this.template[type].RepeatTime;
403
404 return ApplyValueModificationsToEntity("Attack/" + type + "/RepeatTime", repeatTime, this.entity);
405};
406
407Attack.prototype.GetTimers = function(type)
408{
409 return {
410 "prepare": ApplyValueModificationsToEntity("Attack/" + type + "/PrepareTime", +(this.template[type].PrepareTime || 0), this.entity),
411 "repeat": this.GetRepeatTime(type)
412 };
413};
414
415Attack.prototype.GetSplashData = function(type)
416{
417 if (!this.template[type].Splash)
418 return undefined;
419
420 return {
421 "attackData": this.GetAttackEffectsData(type, true),
422 "friendlyFire": this.template[type].Splash.FriendlyFire == "true",
423 "radius": ApplyValueModificationsToEntity("Attack/" + type + "/Splash/Range", +this.template[type].Splash.Range, this.entity),
424 "shape": this.template[type].Splash.Shape,
425 };
426};
427
428Attack.prototype.GetRange = function(type)
429{
430 if (!type)
431 return this.GetFullAttackRange();
432
433 let max = +this.template[type].MaxRange;
434 max = ApplyValueModificationsToEntity("Attack/" + type + "/MaxRange", max, this.entity);
435
436 let min = +(this.template[type].MinRange || 0);
437 min = ApplyValueModificationsToEntity("Attack/" + type + "/MinRange", min, this.entity);
438
439 let elevationBonus = +(this.template[type].ElevationBonus || 0);
440 elevationBonus = ApplyValueModificationsToEntity("Attack/" + type + "/ElevationBonus", elevationBonus, this.entity);
441
442 return { "max": max, "min": min, "elevationBonus": elevationBonus };
443};
444
445/**
446 * Attack the target entity. This should only be called after a successful range check,
447 * and should only be called after GetTimers().repeat msec has passed since the last
448 * call to PerformAttack.
449 */
450Attack.prototype.PerformAttack = function(type, target)
451{
452 let cmpPosition = Engine.QueryInterface(this.entity, IID_Position);
453 if (!cmpPosition || !cmpPosition.IsInWorld())
454 return;
455 let selfPosition = cmpPosition.GetPosition();
456
457 let cmpTargetPosition = Engine.QueryInterface(target, IID_Position);
458 if (!cmpTargetPosition || !cmpTargetPosition.IsInWorld())
459 return;
460 let targetPosition = cmpTargetPosition.GetPosition();
461
462 let cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership);
463 if (!cmpOwnership)
464 return;
465 let attackerOwner = cmpOwnership.GetOwner();
466
467 let data = {
468 "type": type,
469 "attackData": this.GetAttackEffectsData(type),
470 "splash": this.GetSplashData(type),
471 "attacker": this.entity,
472 "attackerOwner": attackerOwner,
473 "target": target,
474 };
475
476 let delay = +(this.template[type].Delay || 0);
477
478 if (this.template[type].Projectile)
479 {
480 let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
481 let turnLength = cmpTimer.GetLatestTurnLength()/1000;
482 // In the future this could be extended:
483 // * Obstacles like trees could reduce the probability of the target being hit
484 // * Obstacles like walls should block projectiles entirely
485
486 let horizSpeed = +this.template[type].Projectile.Speed;
487 let gravity = +this.template[type].Projectile.Gravity;
488 // horizSpeed /= 2; gravity /= 2; // slow it down for testing
489
490 // We will try to estimate the position of the target, where we can hit it.
491 // We first estimate the time-till-hit by extrapolating linearly the movement
492 // of the last turn. We compute the time till an arrow will intersect the target.
493 let targetVelocity = Vector3D.sub(targetPosition, cmpTargetPosition.GetPreviousPosition()).div(turnLength);
494
495 let timeToTarget = PositionHelper.PredictTimeToTarget(selfPosition, horizSpeed, targetPosition, targetVelocity);
496
497 // 'Cheat' and use UnitMotion to predict the position in the near-future.
498 // This avoids 'dancing' issues with units zigzagging over very short distances.
499 // However, this could fail if the player gives several short move orders, so
500 // occasionally fall back to basic interpolation.
501 let predictedPosition = targetPosition;
502 if (timeToTarget !== false)
503 {
504 // Don't predict too far in the future, but avoid threshold effects.
505 // After 1 second, always use the 'dumb' interpolated past-motion prediction.
506 let useUnitMotion = randBool(Math.max(0, 0.75 - timeToTarget / 1.333));
507 if (useUnitMotion)
508 {
509 let cmpTargetUnitMotion = Engine.QueryInterface(target, IID_UnitMotion);
510 let cmpTargetUnitAI = Engine.QueryInterface(target, IID_UnitAI);
511 if (cmpTargetUnitMotion && (!cmpTargetUnitAI || !cmpTargetUnitAI.IsFormationMember()))
512 {
513 let pos2D = cmpTargetUnitMotion.EstimateFuturePosition(timeToTarget);
514 predictedPosition.x = pos2D.x;
515 predictedPosition.z = pos2D.y;
516 }
517 else
518 predictedPosition = Vector3D.mult(targetVelocity, timeToTarget).add(targetPosition);
519 }
520 else
521 predictedPosition = Vector3D.mult(targetVelocity, timeToTarget).add(targetPosition);
522 }
523
524 let predictedHeight = cmpTargetPosition.GetHeightAt(predictedPosition.x, predictedPosition.z);
525
526 // Add inaccuracy based on spread.
527 let distanceModifiedSpread = ApplyValueModificationsToEntity("Attack/" + type + "/Spread", +this.template[type].Projectile.Spread, this.entity) *
528 predictedPosition.horizDistanceTo(selfPosition) / 100;
529
530 let randNorm = randomNormal2D();
531 let offsetX = randNorm[0] * distanceModifiedSpread;
532 let offsetZ = randNorm[1] * distanceModifiedSpread;
533
534 data.position = new Vector3D(predictedPosition.x + offsetX, predictedHeight, predictedPosition.z + offsetZ);
535
536 let realHorizDistance = data.position.horizDistanceTo(selfPosition);
537 timeToTarget = realHorizDistance / horizSpeed;
538 delay += timeToTarget * 1000;
539
540 data.direction = Vector3D.sub(data.position, selfPosition).div(realHorizDistance);
541
542 let actorName = this.template[type].Projectile.ActorName || "";
543 let impactActorName = this.template[type].Projectile.ImpactActorName || "";
544 let impactAnimationLifetime = this.template[type].Projectile.ImpactAnimationLifetime || 0;
545
546 // TODO: Use unit rotation to implement x/z offsets.
547 let deltaLaunchPoint = new Vector3D(0, +this.template[type].Projectile.LaunchPoint["@y"], 0);
548 let launchPoint = Vector3D.add(selfPosition, deltaLaunchPoint);
549
550 let cmpVisual = Engine.QueryInterface(this.entity, IID_Visual);
551 if (cmpVisual)
552 {
553 // if the projectile definition is missing from the template
554 // then fallback to the projectile name and launchpoint in the visual actor
555 if (!actorName)
556 actorName = cmpVisual.GetProjectileActor();
557
558 let visualActorLaunchPoint = cmpVisual.GetProjectileLaunchPoint();
559 if (visualActorLaunchPoint.length() > 0)
560 launchPoint = visualActorLaunchPoint;
561 }
562
563 let cmpProjectileManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ProjectileManager);
564 data.projectileId = cmpProjectileManager.LaunchProjectileAtPoint(launchPoint, data.position, horizSpeed, gravity, actorName, impactActorName, impactAnimationLifetime);
565
566 let cmpSound = Engine.QueryInterface(this.entity, IID_Sound);
567 data.attackImpactSound = cmpSound ? cmpSound.GetSoundGroup("attack_impact_" + type.toLowerCase()) : "";
568
569 data.friendlyFire = this.template[type].Projectile.FriendlyFire == "true";
570 }
571 else
572 {
573 data.position = targetPosition;
574 data.direction = Vector3D.sub(targetPosition, selfPosition);
575 }
576 if (delay)
577 {
578 let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
579 cmpTimer.SetTimeout(SYSTEM_ENTITY, IID_DelayedDamage, "Hit", delay, data);
580 }
581 else
582 Engine.QueryInterface(SYSTEM_ENTITY, IID_DelayedDamage).Hit(data, 0);
583};
584
585Attack.prototype.OnValueModification = function(msg)
586{
587 if (msg.component != "Attack")
588 return;
589
590 let cmpUnitAI = Engine.QueryInterface(this.entity, IID_UnitAI);
591 if (!cmpUnitAI)
592 return;
593
594 if (this.GetAttackTypes().some(type =>
595 msg.valueNames.indexOf("Attack/" + type + "/MaxRange") != -1))
596 cmpUnitAI.UpdateRangeQueries();
597};
598
599Attack.prototype.GetRangeOverlays = function(type = "Ranged")
600{
601 if (!this.template[type] || !this.template[type].RangeOverlay)
602 return [];
603
604 let range = this.GetRange(type);
605 let rangeOverlays = [];
606 for (let i in range)
607 if ((i == "min" || i == "max") && range[i])
608 rangeOverlays.push({
609 "radius": range[i],
610 "texture": this.template[type].RangeOverlay.LineTexture,
611 "textureMask": this.template[type].RangeOverlay.LineTextureMask,
612 "thickness": +this.template[type].RangeOverlay.LineThickness,
613 });
614 return rangeOverlays;
615};
616
617Engine.RegisterComponentType(IID_Attack, "Attack", Attack);
Note: See TracBrowser for help on using the repository browser.