1 | function Attack() {}
|
---|
2 |
|
---|
3 | var g_AttackTypes = ["Melee", "Ranged", "Capture"];
|
---|
4 |
|
---|
5 | Attack.prototype.preferredClassesSchema =
|
---|
6 | "<optional>" +
|
---|
7 | "<element name='PreferredClasses' a:help='Space delimited list of classes preferred for attacking. If an entity has any of theses classes, it is preferred. The classes are in decending order of preference'>" +
|
---|
8 | "<attribute name='datatype'>" +
|
---|
9 | "<value>tokens</value>" +
|
---|
10 | "</attribute>" +
|
---|
11 | "<text/>" +
|
---|
12 | "</element>" +
|
---|
13 | "</optional>";
|
---|
14 |
|
---|
15 | Attack.prototype.restrictedClassesSchema =
|
---|
16 | "<optional>" +
|
---|
17 | "<element name='RestrictedClasses' a:help='Space delimited list of classes that cannot be attacked by this entity. If target entity has any of these classes, it cannot be attacked'>" +
|
---|
18 | "<attribute name='datatype'>" +
|
---|
19 | "<value>tokens</value>" +
|
---|
20 | "</attribute>" +
|
---|
21 | "<text/>" +
|
---|
22 | "</element>" +
|
---|
23 | "</optional>";
|
---|
24 |
|
---|
25 | Attack.prototype.Schema =
|
---|
26 | "<a:help>Controls the attack abilities and strengths of the unit.</a:help>" +
|
---|
27 | "<a:example>" +
|
---|
28 | "<Melee>" +
|
---|
29 | "<AttackName>Spear</AttackName>" +
|
---|
30 | "<Damage>" +
|
---|
31 | "<Hack>10.0</Hack>" +
|
---|
32 | "<Pierce>0.0</Pierce>" +
|
---|
33 | "<Crush>5.0</Crush>" +
|
---|
34 | "</Damage>" +
|
---|
35 | "<MaxRange>4.0</MaxRange>" +
|
---|
36 | "<RepeatTime>1000</RepeatTime>" +
|
---|
37 | "<Bonuses>" +
|
---|
38 | "<Bonus1>" +
|
---|
39 | "<Civ>pers</Civ>" +
|
---|
40 | "<Classes>Infantry</Classes>" +
|
---|
41 | "<Multiplier>1.5</Multiplier>" +
|
---|
42 | "</Bonus1>" +
|
---|
43 | "<BonusCavMelee>" +
|
---|
44 | "<Classes>Cavalry Melee</Classes>" +
|
---|
45 | "<Multiplier>1.5</Multiplier>" +
|
---|
46 | "</BonusCavMelee>" +
|
---|
47 | "</Bonuses>" +
|
---|
48 | "<RestrictedClasses datatype=\"tokens\">Champion</RestrictedClasses>" +
|
---|
49 | "<PreferredClasses datatype=\"tokens\">Cavalry Infantry</PreferredClasses>" +
|
---|
50 | "</Melee>" +
|
---|
51 | "<Ranged>" +
|
---|
52 | "<AttackName>Bow</AttackName>" +
|
---|
53 | "<Damage>" +
|
---|
54 | "<Hack>0.0</Hack>" +
|
---|
55 | "<Pierce>10.0</Pierce>" +
|
---|
56 | "<Crush>0.0</Crush>" +
|
---|
57 | "</Damage>" +
|
---|
58 | "<MaxRange>44.0</MaxRange>" +
|
---|
59 | "<MinRange>20.0</MinRange>" +
|
---|
60 | "<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'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 |
|
---|
190 | Attack.prototype.Init = function()
|
---|
191 | {
|
---|
192 | };
|
---|
193 |
|
---|
194 | Attack.prototype.Serialize = null; // we have no dynamic state to save
|
---|
195 |
|
---|
196 | Attack.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 |
|
---|
207 | Attack.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 |
|
---|
216 | Attack.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 |
|
---|
225 | Attack.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 | */
|
---|
286 | Attack.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 | */
|
---|
315 | Attack.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 |
|
---|
327 | Attack.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 | */
|
---|
341 | Attack.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 |
|
---|
378 | Attack.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 |
|
---|
389 | Attack.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 |
|
---|
397 | Attack.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 |
|
---|
407 | Attack.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 |
|
---|
415 | Attack.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 |
|
---|
428 | Attack.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 | */
|
---|
450 | Attack.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 |
|
---|
585 | Attack.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 |
|
---|
599 | Attack.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 |
|
---|
617 | Engine.RegisterComponentType(IID_Attack, "Attack", Attack);
|
---|