Index: ps/trunk/binaries/data/mods/public/simulation/components/Health.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/Health.js (revision 22195) +++ ps/trunk/binaries/data/mods/public/simulation/components/Health.js (revision 22196) @@ -1,406 +1,411 @@ function Health() {} Health.prototype.Schema = "Deals with hitpoints and death." + "" + "100" + "1.0" + "0" + "corpse" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "0" + "1" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "vanish" + "corpse" + "remain" + "" + "" + "" + "" + "" + "" + "" + "" + "" + ""; Health.prototype.Init = function() { // Cache this value so it allows techs to maintain previous health level this.maxHitpoints = +this.template.Max; // Default to , but use if it's undefined or zero // (Allowing 0 initial HP would break our death detection code) this.hitpoints = +(this.template.Initial || this.GetMaxHitpoints()); this.regenRate = ApplyValueModificationsToEntity("Health/RegenRate", +this.template.RegenRate, this.entity); this.idleRegenRate = ApplyValueModificationsToEntity("Health/IdleRegenRate", +this.template.IdleRegenRate, this.entity); this.CheckRegenTimer(); this.UpdateActor(); }; /** * Returns the current hitpoint value. * This is 0 if (and only if) the unit is dead. */ Health.prototype.GetHitpoints = function() { return this.hitpoints; }; Health.prototype.GetMaxHitpoints = function() { return this.maxHitpoints; }; Health.prototype.SetHitpoints = function(value) { // If we're already dead, don't allow resurrection if (this.hitpoints == 0) return; // Before changing the value, activate Fogging if necessary to hide changes let cmpFogging = Engine.QueryInterface(this.entity, IID_Fogging); if (cmpFogging) cmpFogging.Activate(); let old = this.hitpoints; this.hitpoints = Math.max(1, Math.min(this.GetMaxHitpoints(), value)); let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); if (cmpRangeManager) cmpRangeManager.SetEntityFlag(this.entity, "injured", this.hitpoints < this.GetMaxHitpoints()); this.RegisterHealthChanged(old); }; Health.prototype.IsRepairable = function() { return Engine.QueryInterface(this.entity, IID_Repairable) != null; }; Health.prototype.IsUnhealable = function() { return this.template.Unhealable == "true" || this.GetHitpoints() <= 0 || this.GetHitpoints() >= this.GetMaxHitpoints(); }; Health.prototype.GetIdleRegenRate = function() { return this.idleRegenRate; }; Health.prototype.GetRegenRate = function() { return this.regenRate; }; Health.prototype.ExecuteRegeneration = function() { let regen = this.GetRegenRate(); if (this.GetIdleRegenRate() != 0) { let cmpUnitAI = Engine.QueryInterface(this.entity, IID_UnitAI); if (cmpUnitAI && (cmpUnitAI.IsIdle() || cmpUnitAI.IsGarrisoned() && !cmpUnitAI.IsTurret())) regen += this.GetIdleRegenRate(); } if (regen > 0) this.Increase(regen); else this.Reduce(-regen); }; /* * Check if the regeneration timer needs to be started or stopped */ Health.prototype.CheckRegenTimer = function() { // check if we need a timer if (this.GetRegenRate() == 0 && this.GetIdleRegenRate() == 0 || this.GetHitpoints() == this.GetMaxHitpoints() && this.GetRegenRate() >= 0 && this.GetIdleRegenRate() >= 0 || this.GetHitpoints() == 0) { // we don't need a timer, disable if one exists if (this.regenTimer) { let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); cmpTimer.CancelTimer(this.regenTimer); this.regenTimer = undefined; } return; } // we need a timer, enable if one doesn't exist if (this.regenTimer) return; let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); this.regenTimer = cmpTimer.SetInterval(this.entity, IID_Health, "ExecuteRegeneration", 1000, 1000, null); }; Health.prototype.Kill = function() { this.Reduce(this.hitpoints); }; /** - * Reduces entity's health by amount HP. - * Returns object of the form { "killed": false, "change": -12 } + * Reduces entity's health by amount HP. Kill if required. + * @return {{ "killed": boolean, "change": Number }} the status change of the entity. */ Health.prototype.Reduce = function(amount) { + // If we are dead, do not do anything + // (The entity will exist a little while after calling DestroyEntity so this + // might get called multiple times) + // Likewise if the amount is 0. + if (!amount || !this.hitpoints) + return {"killed": false, "change": 0}; + // Before changing the value, activate Fogging if necessary to hide changes let cmpFogging = Engine.QueryInterface(this.entity, IID_Fogging); if (cmpFogging) cmpFogging.Activate(); - let state = { "killed": false }; - if (amount >= 0 && this.hitpoints == this.GetMaxHitpoints()) + let oldHitpoints = this.hitpoints; + // If we reached 0, then die. + if (amount >= this.hitpoints) + { + this.hitpoints = 0; + this.RegisterHealthChanged(oldHitpoints); + this.HandleDeath(); + return {"killed": true, "change": -oldHitpoints}; + } + + // If we are not marked as injured, do it now + if (this.hitpoints == this.GetMaxHitpoints()) { let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); if (cmpRangeManager) cmpRangeManager.SetEntityFlag(this.entity, "injured", true); } - let oldHitpoints = this.hitpoints; - if (amount >= this.hitpoints) - { - // If this is the first time we reached 0, then die. - // (The entity will exist a little while after calling DestroyEntity so this - // might get called multiple times) - if (this.hitpoints) - { - this.hitpoints = 0; - this.RegisterHealthChanged(oldHitpoints); - state.killed = true; - - let cmpDeathDamage = Engine.QueryInterface(this.entity, IID_DeathDamage); - if (cmpDeathDamage) - cmpDeathDamage.CauseDeathDamage(); - - PlaySound("death", this.entity); - - if (this.template.SpawnEntityOnDeath) - this.CreateDeathSpawnedEntity(); - - switch (this.template.DeathType) - { - case "corpse": - this.CreateCorpse(); - break; - - case "remain": - { - let resource = this.CreateCorpse(true); - if (resource != INVALID_ENTITY) - Engine.PostMessage(this.entity, MT_EntityRenamed, { "entity": this.entity, "newentity": resource }); - break; - } - - case "vanish": - break; - - default: - error("Invalid template.DeathType: " + this.template.DeathType); - break; - } - Engine.DestroyEntity(this.entity); - } + this.hitpoints -= amount; + this.RegisterHealthChanged(oldHitpoints); + return {"killed": false, "change": (this.hitpoints - oldHitpoints)}; +}; - } - else +/** + * Handle what happens when the entity dies. + */ +Health.prototype.HandleDeath = function() +{ + let cmpDeathDamage = Engine.QueryInterface(this.entity, IID_DeathDamage); + if (cmpDeathDamage) + cmpDeathDamage.CauseDeathDamage(); + PlaySound("death", this.entity); + + if (this.template.SpawnEntityOnDeath) + this.CreateDeathSpawnedEntity(); + + switch (this.template.DeathType) { - this.hitpoints -= amount; - this.RegisterHealthChanged(oldHitpoints); + case "corpse": + this.CreateCorpse(); + break; + + case "remain": + { + let resource = this.CreateCorpse(true); + if (resource != INVALID_ENTITY) + Engine.PostMessage(this.entity, MT_EntityRenamed, { "entity": this.entity, "newentity": resource }); + break; } - state.change = this.hitpoints - oldHitpoints; - return state; -}; + + case "vanish": + break; + + default: + error("Invalid template.DeathType: " + this.template.DeathType); + break; + } + + Engine.DestroyEntity(this.entity); +} Health.prototype.Increase = function(amount) { // Before changing the value, activate Fogging if necessary to hide changes let cmpFogging = Engine.QueryInterface(this.entity, IID_Fogging); if (cmpFogging) cmpFogging.Activate(); if (this.hitpoints == this.GetMaxHitpoints()) return { "old": this.hitpoints, "new": this.hitpoints }; // If we're already dead, don't allow resurrection if (this.hitpoints == 0) return undefined; let old = this.hitpoints; this.hitpoints = Math.min(this.hitpoints + amount, this.GetMaxHitpoints()); if (this.hitpoints == this.GetMaxHitpoints()) { let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); if (cmpRangeManager) cmpRangeManager.SetEntityFlag(this.entity, "injured", false); } this.RegisterHealthChanged(old); return { "old": old, "new": this.hitpoints }; }; Health.prototype.CreateCorpse = function(leaveResources) { // If the unit died while not in the world, don't create any corpse for it // since there's nowhere for the corpse to be placed let cmpPosition = Engine.QueryInterface(this.entity, IID_Position); if (!cmpPosition.IsInWorld()) return INVALID_ENTITY; // Either creates a static local version of the current entity, or a // persistent corpse retaining the ResourceSupply element of the parent. let cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager); let templateName = cmpTemplateManager.GetCurrentTemplateName(this.entity); let corpse; if (leaveResources) corpse = Engine.AddEntity("resource|" + templateName); else corpse = Engine.AddLocalEntity("corpse|" + templateName); // Copy various parameters so it looks just like us let cmpCorpsePosition = Engine.QueryInterface(corpse, IID_Position); let pos = cmpPosition.GetPosition(); cmpCorpsePosition.JumpTo(pos.x, pos.z); let rot = cmpPosition.GetRotation(); cmpCorpsePosition.SetYRotation(rot.y); cmpCorpsePosition.SetXZRotation(rot.x, rot.z); let cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership); let cmpCorpseOwnership = Engine.QueryInterface(corpse, IID_Ownership); cmpCorpseOwnership.SetOwner(cmpOwnership.GetOwner()); let cmpVisual = Engine.QueryInterface(this.entity, IID_Visual); let cmpCorpseVisual = Engine.QueryInterface(corpse, IID_Visual); cmpCorpseVisual.SetActorSeed(cmpVisual.GetActorSeed()); // Make it fall over cmpCorpseVisual.SelectAnimation("death", true, 1.0); return corpse; }; Health.prototype.CreateDeathSpawnedEntity = function() { // If the unit died while not in the world, don't spawn a death entity for it // since there's nowhere for it to be placed let cmpPosition = Engine.QueryInterface(this.entity, IID_Position); if (!cmpPosition.IsInWorld()) return INVALID_ENTITY; // Create SpawnEntityOnDeath entity let spawnedEntity = Engine.AddLocalEntity(this.template.SpawnEntityOnDeath); // Move to same position let cmpSpawnedPosition = Engine.QueryInterface(spawnedEntity, IID_Position); let pos = cmpPosition.GetPosition(); cmpSpawnedPosition.JumpTo(pos.x, pos.z); let rot = cmpPosition.GetRotation(); cmpSpawnedPosition.SetYRotation(rot.y); cmpSpawnedPosition.SetXZRotation(rot.x, rot.z); let cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership); let cmpSpawnedOwnership = Engine.QueryInterface(spawnedEntity, IID_Ownership); if (cmpOwnership && cmpSpawnedOwnership) cmpSpawnedOwnership.SetOwner(cmpOwnership.GetOwner()); return spawnedEntity; }; Health.prototype.UpdateActor = function() { if (!this.template.DamageVariants) return; let ratio = this.GetHitpoints() / this.GetMaxHitpoints(); let newDamageVariant = "alive"; if (ratio > 0) { let minTreshold = 1; for (let key in this.template.DamageVariants) { let treshold = +this.template.DamageVariants[key]; if (treshold < ratio || treshold > minTreshold) continue; newDamageVariant = key; minTreshold = treshold; } } else newDamageVariant = "death"; if (this.damageVariant && this.damageVariant == newDamageVariant) return; this.damageVariant = newDamageVariant; let cmpVisual = Engine.QueryInterface(this.entity, IID_Visual); if (cmpVisual) cmpVisual.SetVariant("health", newDamageVariant); }; Health.prototype.OnValueModification = function(msg) { if (msg.component != "Health") return; let oldMaxHitpoints = this.GetMaxHitpoints(); let newMaxHitpoints = ApplyValueModificationsToEntity("Health/Max", +this.template.Max, this.entity); if (oldMaxHitpoints != newMaxHitpoints) { let newHitpoints = this.GetHitpoints() * newMaxHitpoints/oldMaxHitpoints; this.maxHitpoints = newMaxHitpoints; this.SetHitpoints(newHitpoints); } let oldRegenRate = this.regenRate; this.regenRate = ApplyValueModificationsToEntity("Health/RegenRate", +this.template.RegenRate, this.entity); let oldIdleRegenRate = this.idleRegenRate; this.idleRegenRate = ApplyValueModificationsToEntity("Health/IdleRegenRate", +this.template.IdleRegenRate, this.entity); if (this.regenRate != oldRegenRate || this.idleRegenRate != oldIdleRegenRate) this.CheckRegenTimer(); }; Health.prototype.RegisterHealthChanged = function(from) { this.CheckRegenTimer(); this.UpdateActor(); Engine.PostMessage(this.entity, MT_HealthChanged, { "from": from, "to": this.hitpoints }); }; Engine.RegisterComponentType(IID_Health, "Health", Health);