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

Last change on this file since 25102 was 25102, checked in by wraitii, 3 years ago

Optimise FindWalkAndFightTargets.

FindWalkAndFightTargets is used during attack-walk (and a few other situations) to find new entities to attack. This function can be a bit slow, taking large chunks of time during battles.

This optimises it by assuming that one of the surrounding unit will match preferred criteria (which, for most soldiers, are 'Human'), thus returning the first attackable entity. In the worst case, it should still be slightly faster than the current code.

Differential Revision: https://code.wildfiregames.com/D3446

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