Index: ps/trunk/binaries/data/mods/public/simulation/components/Health.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/Health.js (revision 23600) +++ ps/trunk/binaries/data/mods/public/simulation/components/Health.js (revision 23601) @@ -1,452 +1,456 @@ 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; }; /** * @return {boolean} Whether the units are injured. Dead units are not considered injured. */ Health.prototype.IsInjured = function() { return this.hitpoints > 0 && this.hitpoints < this.GetMaxHitpoints(); }; 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.IsInjured()); 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.hitpoints <= 0 || !this.IsInjured(); }; 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.IsInjured() && this.GetRegenRate() >= 0 && this.GetIdleRegenRate() >= 0 || this.hitpoints == 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); }; /** * Take damage according to the entity's resistance. * @param {Object} strengths - { "hack": number, "pierce": number, "crush": number } or something like that. * @param {number} bonusMultiplier - the damage multiplier. * Returns object of the form { "killed": false, "change": -12 }. */ Health.prototype.TakeDamage = function(effectData, attacker, attackerOwner, bonusMultiplier) { let cmpResistance = Engine.QueryInterface(this.entity, IID_Resistance); if (cmpResistance && cmpResistance.IsInvulnerable()) return { "killed": false }; let total = Attacking.GetTotalAttackEffects(effectData, "Damage", cmpResistance) * bonusMultiplier; // Reduce health let change = this.Reduce(total); let cmpLoot = Engine.QueryInterface(this.entity, IID_Loot); if (cmpLoot && cmpLoot.GetXp() > 0 && change.HPchange < 0) change.xp = cmpLoot.GetXp() * -change.HPchange / this.GetMaxHitpoints(); return change; }; /** * @param {number} amount - The amount of hitpoints to substract. Kills the entity if required. * @return {{killed:boolean, HPchange:number}} - Number of health points lost and whether the entity was killed. */ 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, "HPchange": 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 oldHitpoints = this.hitpoints; // If we reached 0, then die. if (amount >= this.hitpoints) { this.hitpoints = 0; this.RegisterHealthChanged(oldHitpoints); this.HandleDeath(); return { "killed": true, "HPchange": -oldHitpoints }; } // If we are not marked as injured, do it now if (!this.IsInjured()) { let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); if (cmpRangeManager) cmpRangeManager.SetEntityFlag(this.entity, "injured", true); } this.hitpoints -= amount; this.RegisterHealthChanged(oldHitpoints); return { "killed": false, "HPchange": this.hitpoints - oldHitpoints }; }; /** * 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) { 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; - } + return; 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.IsInjured()) 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.IsInjured()) { 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) +Health.prototype.CreateCorpse = function() { // 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 + // since there's nowhere for the corpse to be placed. let cmpPosition = Engine.QueryInterface(this.entity, IID_Position); - if (!cmpPosition.IsInWorld()) - return INVALID_ENTITY; + if (!cmpPosition || !cmpPosition.IsInWorld()) + return; // 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); + let templateName = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager).GetCurrentTemplateName(this.entity); - // Copy various parameters so it looks just like us + let entCorpse; + let cmpResourceSupply = Engine.QueryInterface(this.entity, IID_ResourceSupply); + let resource = cmpResourceSupply && cmpResourceSupply.GetKillBeforeGather(); + if (resource) + entCorpse = Engine.AddEntity("resource|" + templateName); + else + entCorpse = Engine.AddLocalEntity("corpse|" + templateName); - let cmpCorpsePosition = Engine.QueryInterface(corpse, IID_Position); + // Copy various parameters so it looks just like us. + let cmpPositionCorpse = Engine.QueryInterface(entCorpse, IID_Position); let pos = cmpPosition.GetPosition(); - cmpCorpsePosition.JumpTo(pos.x, pos.z); + cmpPositionCorpse.JumpTo(pos.x, pos.z); let rot = cmpPosition.GetRotation(); - cmpCorpsePosition.SetYRotation(rot.y); - cmpCorpsePosition.SetXZRotation(rot.x, rot.z); + cmpPositionCorpse.SetYRotation(rot.y); + cmpPositionCorpse.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 cmpOwnershipCorpse = Engine.QueryInterface(entCorpse, IID_Ownership); + if (cmpOwnership && cmpOwnershipCorpse) + cmpOwnershipCorpse.SetOwner(cmpOwnership.GetOwner()); - let cmpVisual = Engine.QueryInterface(this.entity, IID_Visual); - let cmpCorpseVisual = Engine.QueryInterface(corpse, IID_Visual); - cmpCorpseVisual.SetActorSeed(cmpVisual.GetActorSeed()); + let cmpVisualCorpse = Engine.QueryInterface(entCorpse, IID_Visual); + if (cmpVisualCorpse) + { + let cmpVisual = Engine.QueryInterface(this.entity, IID_Visual); + if (cmpVisual) + cmpVisualCorpse.SetActorSeed(cmpVisual.GetActorSeed()); - // Make it fall over - cmpCorpseVisual.SelectAnimation("death", true, 1.0); + cmpVisualCorpse.SelectAnimation("death", true, 1); + } - return corpse; + if (resource) + Engine.PostMessage(this.entity, MT_EntityRenamed, { + "entity": this.entity, + "newentity": entCorpse + }); }; 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.hitpoints / 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.RecalculateValues = function() { let oldMaxHitpoints = this.GetMaxHitpoints(); let newMaxHitpoints = ApplyValueModificationsToEntity("Health/Max", +this.template.Max, this.entity); if (oldMaxHitpoints != newMaxHitpoints) { let newHitpoints = this.hitpoints * 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.OnValueModification = function(msg) { if (msg.component == "Health") this.RecalculateValues(); }; Health.prototype.OnOwnershipChanged = function(msg) { if (msg.to != INVALID_PLAYER) this.RecalculateValues(); -} +}; 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); Index: ps/trunk/binaries/data/mods/public/simulation/components/tests/test_Health.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/tests/test_Health.js (revision 23600) +++ ps/trunk/binaries/data/mods/public/simulation/components/tests/test_Health.js (revision 23601) @@ -1,149 +1,150 @@ Engine.LoadComponentScript("interfaces/ModifiersManager.js"); +Engine.LoadComponentScript("interfaces/ResourceSupply.js"); Engine.LoadHelperScript("Player.js"); Engine.LoadHelperScript("ValueModification.js"); Engine.LoadHelperScript("Sound.js"); Engine.LoadComponentScript("interfaces/DeathDamage.js"); Engine.LoadComponentScript("interfaces/Health.js"); Engine.LoadComponentScript("Health.js"); const entity_id = 5; const corpse_id = entity_id + 1; const health_template = { "Max": 50, "RegenRate": 0, "IdleRegenRate": 0, "DeathType": "corpse", "Unhealable": false }; var injured_flag = false; var corpse_entity; function setEntityUp() { let cmpHealth = ConstructComponent(entity_id, "Health", health_template); AddMock(entity_id, IID_DeathDamage, { "CauseDeathDamage": () => {} }); AddMock(entity_id, IID_Position, { "IsInWorld": () => true, "GetPosition": () => ({ "x": 0, "z": 0 }), "GetRotation": () => ({ "x": 0, "y": 0, "z": 0 }) }); AddMock(entity_id, IID_Ownership, { "GetOwner": () => 1 }); AddMock(entity_id, IID_Visual, { "GetActorSeed": () => 1 }); AddMock(SYSTEM_ENTITY, IID_TemplateManager, { "GetCurrentTemplateName": () => "test" }); AddMock(SYSTEM_ENTITY, IID_RangeManager, { "SetEntityFlag": (ent, flag, value) => (injured_flag = value) }); return cmpHealth; } var cmpHealth = setEntityUp(); TS_ASSERT_EQUALS(cmpHealth.GetHitpoints(), 50); TS_ASSERT_EQUALS(cmpHealth.GetMaxHitpoints(), 50); TS_ASSERT_EQUALS(cmpHealth.IsInjured(), false); TS_ASSERT_EQUALS(cmpHealth.IsUnhealable(), true); var change = cmpHealth.Reduce(25); TS_ASSERT_EQUALS(injured_flag, true); TS_ASSERT_EQUALS(change.killed, false); TS_ASSERT_EQUALS(change.HPchange, -25); TS_ASSERT_EQUALS(cmpHealth.GetHitpoints(), 25); TS_ASSERT_EQUALS(cmpHealth.GetMaxHitpoints(), 50); TS_ASSERT_EQUALS(cmpHealth.IsInjured(), true); TS_ASSERT_EQUALS(cmpHealth.IsUnhealable(), false); change = cmpHealth.Increase(25); TS_ASSERT_EQUALS(injured_flag, false); TS_ASSERT_EQUALS(change.new, 50); TS_ASSERT_EQUALS(cmpHealth.GetHitpoints(), 50); TS_ASSERT_EQUALS(cmpHealth.GetMaxHitpoints(), 50); TS_ASSERT_EQUALS(cmpHealth.IsInjured(), false); TS_ASSERT_EQUALS(cmpHealth.IsUnhealable(), true); // Check death. Engine.AddLocalEntity = function(template) { corpse_entity = template; AddMock(corpse_id, IID_Position, { "JumpTo": () => {}, "SetYRotation": () => {}, "SetXZRotation": () => {}, }); AddMock(corpse_id, IID_Ownership, { "SetOwner": () => {}, }); AddMock(corpse_id, IID_Visual, { "SetActorSeed": () => {}, "SelectAnimation": () => {}, }); return corpse_id; }; change = cmpHealth.Reduce(50); // Assert we create a corpse with the proper template. TS_ASSERT_EQUALS(corpse_entity, "corpse|test"); // Check that we are not marked as injured. TS_ASSERT_EQUALS(injured_flag, false); TS_ASSERT_EQUALS(change.killed, true); TS_ASSERT_EQUALS(change.HPchange, -50); TS_ASSERT_EQUALS(cmpHealth.GetHitpoints(), 0); TS_ASSERT_EQUALS(cmpHealth.GetMaxHitpoints(), 50); TS_ASSERT_EQUALS(cmpHealth.IsInjured(), false); // Check that we can't be revived once dead. change = cmpHealth.Increase(25); TS_ASSERT_EQUALS(change.new, 0); TS_ASSERT_EQUALS(cmpHealth.GetHitpoints(), 0); TS_ASSERT_EQUALS(cmpHealth.GetMaxHitpoints(), 50); TS_ASSERT_EQUALS(cmpHealth.IsInjured(), false); // Check that we can't die twice. change = cmpHealth.Reduce(50); TS_ASSERT_EQUALS(change.killed, false); TS_ASSERT_EQUALS(change.HPchange, 0); TS_ASSERT_EQUALS(cmpHealth.GetHitpoints(), 0); TS_ASSERT_EQUALS(cmpHealth.GetMaxHitpoints(), 50); TS_ASSERT_EQUALS(cmpHealth.IsInjured(), false); cmpHealth = setEntityUp(); // Check that we still die with > Max HP of damage. change = cmpHealth.Reduce(60); TS_ASSERT_EQUALS(change.killed, true); TS_ASSERT_EQUALS(change.HPchange, -50); TS_ASSERT_EQUALS(cmpHealth.GetHitpoints(), 0); TS_ASSERT_EQUALS(cmpHealth.GetMaxHitpoints(), 50); TS_ASSERT_EQUALS(cmpHealth.IsInjured(), false); cmpHealth = setEntityUp(); // Check that increasing by more than required puts us at the max HP change = cmpHealth.Reduce(30); change = cmpHealth.Increase(30); TS_ASSERT_EQUALS(injured_flag, false); TS_ASSERT_EQUALS(change.new, 50); TS_ASSERT_EQUALS(cmpHealth.GetHitpoints(), 50); TS_ASSERT_EQUALS(cmpHealth.GetMaxHitpoints(), 50); TS_ASSERT_EQUALS(cmpHealth.IsInjured(), false); TS_ASSERT_EQUALS(cmpHealth.IsUnhealable(), true); Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_fauna_herd.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_fauna_herd.xml (revision 23600) +++ ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_fauna_herd.xml (revision 23601) @@ -1,18 +1,15 @@ - - remain - Kill to gather meat for food. true 100 food.meat 8 20 Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_fauna_hunt.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_fauna_hunt.xml (revision 23600) +++ ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_fauna_hunt.xml (revision 23601) @@ -1,19 +1,18 @@ 50 - remain Kill to gather meat for food. 10 true 100 food.meat 8 Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_fauna_hunt_whale.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_fauna_hunt_whale.xml (revision 23600) +++ ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_fauna_hunt_whale.xml (revision 23601) @@ -1,55 +1,54 @@ 200 - remain true Kill to butcher for food. 10 false false true 0.0 true 2000 food.fish 5 circle/512x512.png circle/512x512_mask.png 4.0 0.666 5.0 skittish 60.0 60.0 100000 300000 1 2 ship-small 1.8 1