Line data Source code
1 : function Health() {}
2 :
3 3 : Health.prototype.Schema =
4 : "<a:help>Deals with hitpoints and death.</a:help>" +
5 : "<a:example>" +
6 : "<Max>100</Max>" +
7 : "<RegenRate>1.0</RegenRate>" +
8 : "<IdleRegenRate>0</IdleRegenRate>" +
9 : "<DeathType>corpse</DeathType>" +
10 : "</a:example>" +
11 : "<element name='Max' a:help='Maximum hitpoints'>" +
12 : "<ref name='nonNegativeDecimal'/>" +
13 : "</element>" +
14 : "<optional>" +
15 : "<element name='Initial' a:help='Initial hitpoints. Default if unspecified is equal to Max'>" +
16 : "<ref name='nonNegativeDecimal'/>" +
17 : "</element>" +
18 : "</optional>" +
19 : "<optional>" +
20 : "<element name='DamageVariants'>" +
21 : "<oneOrMore>" +
22 : "<element a:help='Name of the variant to select when health drops under the defined ratio'>" +
23 : "<anyName/>" +
24 : "<data type='decimal'>" +
25 : "<param name='minInclusive'>0</param>" +
26 : "<param name='maxInclusive'>1</param>" +
27 : "</data>" +
28 : "</element>" +
29 : "</oneOrMore>" +
30 : "</element>" +
31 : "</optional>" +
32 : "<element name='RegenRate' a:help='Hitpoint regeneration rate per second.'>" +
33 : "<data type='decimal'/>" +
34 : "</element>" +
35 : "<element name='IdleRegenRate' a:help='Hitpoint regeneration rate per second when idle or garrisoned.'>" +
36 : "<data type='decimal'/>" +
37 : "</element>" +
38 : "<element name='DeathType' a:help='Behaviour when the unit dies'>" +
39 : "<choice>" +
40 : "<value a:help='Disappear instantly'>vanish</value>" +
41 : "<value a:help='Turn into a corpse'>corpse</value>" +
42 : "<value a:help='Remain in the world with 0 health'>remain</value>" +
43 : "</choice>" +
44 : "</element>" +
45 : "<optional>" +
46 : "<element name='SpawnEntityOnDeath' a:help='Entity template to spawn when this entity dies. Note: this is different than the corpse, which retains the original entity's appearance'>" +
47 : "<text/>" +
48 : "</element>" +
49 : "</optional>" +
50 : "<element name='Unhealable' a:help='Indicates that the entity can not be healed by healer units'>" +
51 : "<data type='boolean'/>" +
52 : "</element>";
53 :
54 3 : Health.prototype.Init = function()
55 : {
56 : // Cache this value so it allows techs to maintain previous health level
57 4 : this.maxHitpoints = +this.template.Max;
58 : // Default to <Initial>, but use <Max> if it's undefined or zero
59 : // (Allowing 0 initial HP would break our death detection code)
60 4 : this.hitpoints = +(this.template.Initial || this.GetMaxHitpoints());
61 4 : this.regenRate = ApplyValueModificationsToEntity("Health/RegenRate", +this.template.RegenRate, this.entity);
62 4 : this.idleRegenRate = ApplyValueModificationsToEntity("Health/IdleRegenRate", +this.template.IdleRegenRate, this.entity);
63 4 : this.CheckRegenTimer();
64 4 : this.UpdateActor();
65 : };
66 :
67 : /**
68 : * Returns the current hitpoint value.
69 : * This is 0 if (and only if) the unit is dead.
70 : */
71 3 : Health.prototype.GetHitpoints = function()
72 : {
73 16 : return this.hitpoints;
74 : };
75 :
76 3 : Health.prototype.GetMaxHitpoints = function()
77 : {
78 44 : return this.maxHitpoints;
79 : };
80 :
81 : /**
82 : * @return {boolean} Whether the units are injured. Dead units are not considered injured.
83 : */
84 3 : Health.prototype.IsInjured = function()
85 : {
86 25 : return this.hitpoints > 0 && this.hitpoints < this.GetMaxHitpoints();
87 : };
88 :
89 3 : Health.prototype.SetHitpoints = function(value)
90 : {
91 : // If we're already dead, don't allow resurrection
92 1 : if (this.hitpoints == 0)
93 0 : return;
94 :
95 : // Before changing the value, activate Fogging if necessary to hide changes
96 1 : let cmpFogging = Engine.QueryInterface(this.entity, IID_Fogging);
97 1 : if (cmpFogging)
98 0 : cmpFogging.Activate();
99 :
100 1 : let old = this.hitpoints;
101 1 : this.hitpoints = Math.max(1, Math.min(this.GetMaxHitpoints(), value));
102 :
103 1 : let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
104 1 : if (cmpRangeManager)
105 0 : cmpRangeManager.SetEntityFlag(this.entity, "injured", this.IsInjured());
106 :
107 1 : this.RegisterHealthChanged(old);
108 : };
109 :
110 3 : Health.prototype.IsRepairable = function()
111 : {
112 0 : let cmpRepairable = Engine.QueryInterface(this.entity, IID_Repairable);
113 0 : return cmpRepairable && cmpRepairable.IsRepairable();
114 : };
115 :
116 3 : Health.prototype.IsUnhealable = function()
117 : {
118 4 : return this.template.Unhealable == "true" ||
119 : this.hitpoints <= 0 || !this.IsInjured();
120 : };
121 :
122 3 : Health.prototype.GetIdleRegenRate = function()
123 : {
124 14 : return this.idleRegenRate;
125 : };
126 :
127 3 : Health.prototype.GetRegenRate = function()
128 : {
129 14 : return this.regenRate;
130 : };
131 :
132 3 : Health.prototype.ExecuteRegeneration = function()
133 : {
134 0 : let regen = this.GetRegenRate();
135 0 : if (this.GetIdleRegenRate() != 0)
136 : {
137 0 : let cmpUnitAI = Engine.QueryInterface(this.entity, IID_UnitAI);
138 0 : if (cmpUnitAI && cmpUnitAI.IsIdle())
139 0 : regen += this.GetIdleRegenRate();
140 : }
141 :
142 0 : if (regen > 0)
143 0 : this.Increase(regen);
144 : else
145 0 : this.Reduce(-regen);
146 : };
147 :
148 : /*
149 : * Check if the regeneration timer needs to be started or stopped
150 : */
151 3 : Health.prototype.CheckRegenTimer = function()
152 : {
153 : // check if we need a timer
154 14 : if (this.GetRegenRate() == 0 && this.GetIdleRegenRate() == 0 ||
155 : !this.IsInjured() && this.GetRegenRate() >= 0 && this.GetIdleRegenRate() >= 0 ||
156 : this.hitpoints == 0)
157 : {
158 : // we don't need a timer, disable if one exists
159 14 : if (this.regenTimer)
160 : {
161 0 : let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
162 0 : cmpTimer.CancelTimer(this.regenTimer);
163 0 : this.regenTimer = undefined;
164 : }
165 14 : return;
166 : }
167 :
168 : // we need a timer, enable if one doesn't exist
169 0 : if (this.regenTimer)
170 0 : return;
171 :
172 0 : let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
173 0 : this.regenTimer = cmpTimer.SetInterval(this.entity, IID_Health, "ExecuteRegeneration", 1000, 1000, null);
174 : };
175 :
176 3 : Health.prototype.Kill = function()
177 : {
178 0 : this.Reduce(this.hitpoints);
179 : };
180 :
181 : /**
182 : * @param {number} amount - The amount of damage to be taken.
183 : * @param {number} attacker - The entityID of the attacker.
184 : * @param {number} attackerOwner - The playerID of the owner of the attacker.
185 : *
186 : * @eturn {Object} - Object of the form { "healthChange": number }.
187 : */
188 3 : Health.prototype.TakeDamage = function(amount, attacker, attackerOwner)
189 : {
190 0 : if (!amount || !this.hitpoints)
191 0 : return { "healthChange": 0 };
192 :
193 0 : let change = this.Reduce(amount);
194 :
195 0 : let cmpLoot = Engine.QueryInterface(this.entity, IID_Loot);
196 0 : if (cmpLoot && cmpLoot.GetXp() > 0 && change.healthChange < 0)
197 0 : change.xp = cmpLoot.GetXp() * -change.healthChange / this.GetMaxHitpoints();
198 :
199 0 : if (!this.hitpoints)
200 0 : this.KilledBy(attacker, attackerOwner);
201 :
202 0 : return change;
203 : };
204 :
205 : /**
206 : * Called when an entity kills us.
207 : * @param {number} attacker - The entityID of the killer.
208 : * @param {number} attackerOwner - The playerID of the attacker.
209 : */
210 3 : Health.prototype.KilledBy = function(attacker, attackerOwner)
211 : {
212 0 : let cmpAttackerOwnership = Engine.QueryInterface(attacker, IID_Ownership);
213 0 : if (cmpAttackerOwnership)
214 : {
215 0 : let currentAttackerOwner = cmpAttackerOwnership.GetOwner();
216 0 : if (currentAttackerOwner != INVALID_PLAYER)
217 0 : attackerOwner = currentAttackerOwner;
218 : }
219 :
220 : // Add to killer statistics.
221 0 : let cmpKillerPlayerStatisticsTracker = QueryPlayerIDInterface(attackerOwner, IID_StatisticsTracker);
222 0 : if (cmpKillerPlayerStatisticsTracker)
223 0 : cmpKillerPlayerStatisticsTracker.KilledEntity(this.entity);
224 :
225 : // Add to loser statistics.
226 0 : let cmpTargetPlayerStatisticsTracker = QueryOwnerInterface(this.entity, IID_StatisticsTracker);
227 0 : if (cmpTargetPlayerStatisticsTracker)
228 0 : cmpTargetPlayerStatisticsTracker.LostEntity(this.entity);
229 :
230 0 : let cmpLooter = Engine.QueryInterface(attacker, IID_Looter);
231 0 : if (cmpLooter)
232 0 : cmpLooter.Collect(this.entity);
233 : };
234 :
235 : /**
236 : * @param {number} amount - The amount of hitpoints to substract. Kills the entity if required.
237 : * @return {{ healthChange:number }} - Number of health points lost.
238 : */
239 3 : Health.prototype.Reduce = function(amount)
240 : {
241 : // If we are dead, do not do anything
242 : // (The entity will exist a little while after calling DestroyEntity so this
243 : // might get called multiple times)
244 : // Likewise if the amount is 0.
245 5 : if (!amount || !this.hitpoints)
246 1 : return { "healthChange": 0 };
247 :
248 : // Before changing the value, activate Fogging if necessary to hide changes
249 4 : let cmpFogging = Engine.QueryInterface(this.entity, IID_Fogging);
250 4 : if (cmpFogging)
251 0 : cmpFogging.Activate();
252 :
253 4 : let oldHitpoints = this.hitpoints;
254 : // If we reached 0, then die.
255 4 : if (amount >= this.hitpoints)
256 : {
257 2 : this.hitpoints = 0;
258 2 : this.RegisterHealthChanged(oldHitpoints);
259 2 : this.HandleDeath();
260 2 : return { "healthChange": -oldHitpoints };
261 : }
262 :
263 : // If we are not marked as injured, do it now
264 2 : if (!this.IsInjured())
265 : {
266 2 : let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
267 2 : if (cmpRangeManager)
268 2 : cmpRangeManager.SetEntityFlag(this.entity, "injured", true);
269 : }
270 :
271 2 : this.hitpoints -= amount;
272 2 : this.RegisterHealthChanged(oldHitpoints);
273 2 : return { "healthChange": this.hitpoints - oldHitpoints };
274 : };
275 :
276 : /**
277 : * Handle what happens when the entity dies.
278 : */
279 3 : Health.prototype.HandleDeath = function()
280 : {
281 2 : let cmpDeathDamage = Engine.QueryInterface(this.entity, IID_DeathDamage);
282 2 : if (cmpDeathDamage)
283 2 : cmpDeathDamage.CauseDeathDamage();
284 2 : PlaySound("death", this.entity);
285 :
286 2 : if (this.template.SpawnEntityOnDeath)
287 0 : this.CreateDeathSpawnedEntity();
288 :
289 2 : switch (this.template.DeathType)
290 : {
291 : case "corpse":
292 2 : this.CreateCorpse();
293 2 : break;
294 :
295 : case "remain":
296 0 : return;
297 :
298 : case "vanish":
299 0 : break;
300 :
301 : default:
302 0 : error("Invalid template.DeathType: " + this.template.DeathType);
303 0 : break;
304 : }
305 :
306 2 : Engine.DestroyEntity(this.entity);
307 : };
308 :
309 3 : Health.prototype.Increase = function(amount)
310 : {
311 : // Before changing the value, activate Fogging if necessary to hide changes
312 6 : let cmpFogging = Engine.QueryInterface(this.entity, IID_Fogging);
313 6 : if (cmpFogging)
314 0 : cmpFogging.Activate();
315 :
316 6 : if (!this.IsInjured())
317 1 : return { "old": this.hitpoints, "new": this.hitpoints };
318 :
319 : // If we're already dead, don't allow resurrection
320 5 : if (this.hitpoints == 0)
321 0 : return undefined;
322 :
323 5 : let old = this.hitpoints;
324 5 : this.hitpoints = Math.min(this.hitpoints + amount, this.GetMaxHitpoints());
325 :
326 5 : if (!this.IsInjured())
327 : {
328 2 : let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
329 2 : if (cmpRangeManager)
330 2 : cmpRangeManager.SetEntityFlag(this.entity, "injured", false);
331 : }
332 :
333 5 : this.RegisterHealthChanged(old);
334 :
335 5 : return { "old": old, "new": this.hitpoints };
336 : };
337 :
338 3 : Health.prototype.CreateCorpse = function()
339 : {
340 : // If the unit died while not in the world, don't create any corpse for it
341 : // since there's nowhere for the corpse to be placed.
342 2 : let cmpPosition = Engine.QueryInterface(this.entity, IID_Position);
343 2 : if (!cmpPosition || !cmpPosition.IsInWorld())
344 0 : return;
345 :
346 : // Either creates a static local version of the current entity, or a
347 : // persistent corpse retaining the ResourceSupply element of the parent.
348 2 : let templateName = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager).GetCurrentTemplateName(this.entity);
349 :
350 : let entCorpse;
351 2 : let cmpResourceSupply = Engine.QueryInterface(this.entity, IID_ResourceSupply);
352 2 : let resource = cmpResourceSupply && cmpResourceSupply.GetKillBeforeGather();
353 2 : if (resource)
354 0 : entCorpse = Engine.AddEntity("resource|" + templateName);
355 : else
356 2 : entCorpse = Engine.AddLocalEntity("corpse|" + templateName);
357 :
358 : // Copy various parameters so it looks just like us.
359 2 : let cmpPositionCorpse = Engine.QueryInterface(entCorpse, IID_Position);
360 2 : let pos = cmpPosition.GetPosition();
361 2 : cmpPositionCorpse.JumpTo(pos.x, pos.z);
362 2 : let rot = cmpPosition.GetRotation();
363 2 : cmpPositionCorpse.SetYRotation(rot.y);
364 2 : cmpPositionCorpse.SetXZRotation(rot.x, rot.z);
365 :
366 2 : let cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership);
367 2 : let cmpOwnershipCorpse = Engine.QueryInterface(entCorpse, IID_Ownership);
368 2 : if (cmpOwnership && cmpOwnershipCorpse)
369 2 : cmpOwnershipCorpse.SetOwner(cmpOwnership.GetOwner());
370 :
371 2 : let cmpVisualCorpse = Engine.QueryInterface(entCorpse, IID_Visual);
372 2 : if (cmpVisualCorpse)
373 : {
374 2 : let cmpVisual = Engine.QueryInterface(this.entity, IID_Visual);
375 2 : if (cmpVisual)
376 2 : cmpVisualCorpse.SetActorSeed(cmpVisual.GetActorSeed());
377 :
378 2 : cmpVisualCorpse.SelectAnimation("death", true, 1);
379 : }
380 :
381 2 : const cmpIdentityCorpse = Engine.QueryInterface(entCorpse, IID_Identity);
382 2 : if (cmpIdentityCorpse)
383 : {
384 0 : const cmpIdentity = Engine.QueryInterface(this.entity, IID_Identity);
385 0 : if (cmpIdentity)
386 : {
387 0 : const oldPhenotype = cmpIdentity.GetPhenotype();
388 0 : if (cmpIdentityCorpse.GetPhenotype() !== oldPhenotype)
389 : {
390 0 : cmpIdentityCorpse.SetPhenotype(oldPhenotype);
391 0 : if (cmpVisualCorpse)
392 0 : cmpVisualCorpse.RecomputeActorName();
393 : }
394 : }
395 : }
396 :
397 2 : if (resource)
398 0 : Engine.PostMessage(this.entity, MT_EntityRenamed, {
399 : "entity": this.entity,
400 : "newentity": entCorpse
401 : });
402 : };
403 :
404 3 : Health.prototype.CreateDeathSpawnedEntity = function()
405 : {
406 : // If the unit died while not in the world, don't spawn a death entity for it
407 : // since there's nowhere for it to be placed
408 0 : let cmpPosition = Engine.QueryInterface(this.entity, IID_Position);
409 0 : if (!cmpPosition.IsInWorld())
410 0 : return INVALID_ENTITY;
411 :
412 : // Create SpawnEntityOnDeath entity
413 0 : let spawnedEntity = Engine.AddLocalEntity(this.template.SpawnEntityOnDeath);
414 :
415 : // Move to same position
416 0 : let cmpSpawnedPosition = Engine.QueryInterface(spawnedEntity, IID_Position);
417 0 : let pos = cmpPosition.GetPosition();
418 0 : cmpSpawnedPosition.JumpTo(pos.x, pos.z);
419 0 : let rot = cmpPosition.GetRotation();
420 0 : cmpSpawnedPosition.SetYRotation(rot.y);
421 0 : cmpSpawnedPosition.SetXZRotation(rot.x, rot.z);
422 :
423 0 : let cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership);
424 0 : let cmpSpawnedOwnership = Engine.QueryInterface(spawnedEntity, IID_Ownership);
425 0 : if (cmpOwnership && cmpSpawnedOwnership)
426 0 : cmpSpawnedOwnership.SetOwner(cmpOwnership.GetOwner());
427 :
428 0 : return spawnedEntity;
429 : };
430 :
431 3 : Health.prototype.UpdateActor = function()
432 : {
433 14 : if (!this.template.DamageVariants)
434 14 : return;
435 0 : let ratio = this.hitpoints / this.GetMaxHitpoints();
436 0 : let newDamageVariant = "alive";
437 0 : if (ratio > 0)
438 : {
439 0 : let minTreshold = 1;
440 0 : for (let key in this.template.DamageVariants)
441 : {
442 0 : let treshold = +this.template.DamageVariants[key];
443 0 : if (treshold < ratio || treshold > minTreshold)
444 0 : continue;
445 0 : newDamageVariant = key;
446 0 : minTreshold = treshold;
447 : }
448 : }
449 : else
450 0 : newDamageVariant = "death";
451 :
452 0 : if (this.damageVariant && this.damageVariant == newDamageVariant)
453 0 : return;
454 :
455 0 : this.damageVariant = newDamageVariant;
456 :
457 0 : let cmpVisual = Engine.QueryInterface(this.entity, IID_Visual);
458 0 : if (cmpVisual)
459 0 : cmpVisual.SetVariant("health", newDamageVariant);
460 : };
461 :
462 3 : Health.prototype.RecalculateValues = function()
463 : {
464 0 : let oldMaxHitpoints = this.GetMaxHitpoints();
465 0 : let newMaxHitpoints = ApplyValueModificationsToEntity("Health/Max", +this.template.Max, this.entity);
466 0 : if (oldMaxHitpoints != newMaxHitpoints)
467 : {
468 : // Don't recalculate hitpoints when full health due to float imprecision: #6657.
469 0 : const newHitpoints = (this.hitpoints === oldMaxHitpoints) ? newMaxHitpoints :
470 : this.hitpoints * newMaxHitpoints/oldMaxHitpoints;
471 0 : this.maxHitpoints = newMaxHitpoints;
472 0 : this.SetHitpoints(newHitpoints);
473 : }
474 :
475 0 : let oldRegenRate = this.regenRate;
476 0 : this.regenRate = ApplyValueModificationsToEntity("Health/RegenRate", +this.template.RegenRate, this.entity);
477 :
478 0 : let oldIdleRegenRate = this.idleRegenRate;
479 0 : this.idleRegenRate = ApplyValueModificationsToEntity("Health/IdleRegenRate", +this.template.IdleRegenRate, this.entity);
480 :
481 0 : if (this.regenRate != oldRegenRate || this.idleRegenRate != oldIdleRegenRate)
482 0 : this.CheckRegenTimer();
483 : };
484 :
485 3 : Health.prototype.OnValueModification = function(msg)
486 : {
487 0 : if (msg.component == "Health")
488 0 : this.RecalculateValues();
489 : };
490 :
491 3 : Health.prototype.OnOwnershipChanged = function(msg)
492 : {
493 0 : if (msg.to != INVALID_PLAYER)
494 0 : this.RecalculateValues();
495 : };
496 :
497 3 : Health.prototype.RegisterHealthChanged = function(from)
498 : {
499 10 : this.CheckRegenTimer();
500 10 : this.UpdateActor();
501 10 : Engine.PostMessage(this.entity, MT_HealthChanged, { "from": from, "to": this.hitpoints });
502 : };
503 :
504 : function HealthMirage() {}
505 3 : HealthMirage.prototype.Init = function(cmpHealth)
506 : {
507 0 : this.maxHitpoints = cmpHealth.GetMaxHitpoints();
508 0 : this.hitpoints = cmpHealth.GetHitpoints();
509 0 : this.repairable = cmpHealth.IsRepairable();
510 0 : this.injured = cmpHealth.IsInjured();
511 0 : this.unhealable = cmpHealth.IsUnhealable();
512 : };
513 3 : HealthMirage.prototype.GetMaxHitpoints = function() { return this.maxHitpoints; };
514 3 : HealthMirage.prototype.GetHitpoints = function() { return this.hitpoints; };
515 3 : HealthMirage.prototype.IsRepairable = function() { return this.repairable; };
516 3 : HealthMirage.prototype.IsInjured = function() { return this.injured; };
517 3 : HealthMirage.prototype.IsUnhealable = function() { return this.unhealable; };
518 :
519 3 : Engine.RegisterGlobal("HealthMirage", HealthMirage);
520 :
521 3 : Health.prototype.Mirage = function()
522 : {
523 0 : let mirage = new HealthMirage();
524 0 : mirage.Init(this);
525 0 : return mirage;
526 : };
527 :
528 3 : Engine.RegisterComponentType(IID_Health, "Health", Health);
|