Index: ps/trunk/binaries/data/mods/public/simulation/components/Foundation.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/Foundation.js (revision 25831) +++ ps/trunk/binaries/data/mods/public/simulation/components/Foundation.js (revision 25832) @@ -1,447 +1,460 @@ function Foundation() {} Foundation.prototype.Schema = "" + "" + ""; Foundation.prototype.Init = function() { // Foundations are initially 'uncommitted' and do not block unit movement at all // (to prevent players exploiting free foundations to confuse enemy units). // The first builder to reach the uncommitted foundation will tell friendly units // and animals to move out of the way, then will commit the foundation and enable // its obstruction once there's nothing in the way. this.committed = false; this.builders = new Map(); // Map of builder entities to their work per second this.totalBuilderRate = 0; // Total amount of work the builders do each second this.buildMultiplier = 1; // Multiplier for the amount of work builders do this.buildTimeModifier = +this.template.BuildTimeModifier; this.previewEntity = INVALID_ENTITY; }; Foundation.prototype.Serialize = function() { let ret = Object.assign({}, this); ret.previewEntity = INVALID_ENTITY; return ret; }; Foundation.prototype.Deserialize = function(data) { this.Init(); Object.assign(this, data); }; Foundation.prototype.OnDeserialized = function() { this.CreateConstructionPreview(); }; Foundation.prototype.InitialiseConstruction = function(template) { this.finalTemplateName = template; // Remember the cost here, so if it changes after construction begins (from auras or technologies) // we will use the correct values to refund partial construction costs. let cmpCost = Engine.QueryInterface(this.entity, IID_Cost); if (!cmpCost) error("A foundation, from " + template + ", must have a cost component to know the build time"); this.costs = cmpCost.GetResourceCosts(); this.maxProgress = 0; this.initialised = true; }; /** * Moving the revelation logic from Build to here makes the building sink if * it is attacked. */ Foundation.prototype.OnHealthChanged = function(msg) { let cmpPosition = Engine.QueryInterface(this.previewEntity, IID_Position); if (cmpPosition) cmpPosition.SetConstructionProgress(this.GetBuildProgress()); Engine.PostMessage(this.entity, MT_FoundationProgressChanged, { "to": this.GetBuildPercentage() }); }; /** * Returns the current build progress in a [0,1] range. */ Foundation.prototype.GetBuildProgress = function() { let cmpHealth = Engine.QueryInterface(this.entity, IID_Health); if (!cmpHealth) return 0; return cmpHealth.GetHitpoints() / cmpHealth.GetMaxHitpoints(); }; Foundation.prototype.GetBuildPercentage = function() { return Math.floor(this.GetBuildProgress() * 100); }; /** * @return {number[]} - An array containing the entity IDs of assigned builders. */ Foundation.prototype.GetBuilders = function() { return Array.from(this.builders.keys()); }; Foundation.prototype.GetNumBuilders = function() { return this.builders.size; }; Foundation.prototype.IsFinished = function() { return (this.GetBuildProgress() == 1.0); }; Foundation.prototype.OnOwnershipChanged = function(msg) { if (msg.to != INVALID_PLAYER && this.previewEntity != INVALID_ENTITY) { let cmpPreviewOwnership = Engine.QueryInterface(this.previewEntity, IID_Ownership); if (cmpPreviewOwnership) cmpPreviewOwnership.SetOwner(msg.to); return; } if (msg.to != INVALID_PLAYER || !this.initialised) return; if (this.previewEntity != INVALID_ENTITY) { Engine.DestroyEntity(this.previewEntity); this.previewEntity = INVALID_ENTITY; } if (this.IsFinished()) return; let cmpPlayer = QueryPlayerIDInterface(msg.from); let cmpStatisticsTracker = QueryPlayerIDInterface(msg.from, IID_StatisticsTracker); // Refund a portion of the construction cost, proportional // to the amount of build progress remaining. for (let r in this.costs) { let scaled = Math.ceil(this.costs[r] * (1.0 - this.maxProgress)); if (scaled) { if (cmpPlayer) cmpPlayer.AddResource(r, scaled); if (cmpStatisticsTracker) cmpStatisticsTracker.IncreaseResourceUsedCounter(r, -scaled); } } }; /** * @param {number[]} builders - An array containing the entity IDs of builders to assign. */ Foundation.prototype.AddBuilders = function(builders) { let changed = false; for (let builder of builders) changed = this.AddBuilderHelper(builder) || changed; if (changed) this.HandleBuildersChanged(); }; /** * @param {number} builderEnt - The entity to add. * @return {boolean} - Whether the addition was successful. */ Foundation.prototype.AddBuilderHelper = function(builderEnt) { if (this.builders.has(builderEnt)) return false; let cmpBuilder = Engine.QueryInterface(builderEnt, IID_Builder) || Engine.QueryInterface(this.entity, IID_AutoBuildable); if (!cmpBuilder) return false; let buildRate = cmpBuilder.GetRate(); this.builders.set(builderEnt, buildRate); this.totalBuilderRate += buildRate; return true; }; /** * @param {number} builderEnt - The entity to add. */ Foundation.prototype.AddBuilder = function(builderEnt) { if (this.AddBuilderHelper(builderEnt)) this.HandleBuildersChanged(); }; /** * @param {number} builderEnt - The entity to remove. */ Foundation.prototype.RemoveBuilder = function(builderEnt) { if (!this.builders.has(builderEnt)) return; this.totalBuilderRate -= this.builders.get(builderEnt); this.builders.delete(builderEnt); this.HandleBuildersChanged(); }; /** * This has to be called whenever the number of builders change. */ Foundation.prototype.HandleBuildersChanged = function() { this.SetBuildMultiplier(); let cmpVisual = Engine.QueryInterface(this.entity, IID_Visual); if (cmpVisual) cmpVisual.SetVariable("numbuilders", this.GetNumBuilders()); Engine.PostMessage(this.entity, MT_FoundationBuildersChanged, { "to": this.GetBuilders() }); }; /** * The build multiplier is a penalty that is applied to each builder. * For example, ten women build at a combined rate of 10^0.7 = 5.01 instead of 10. */ Foundation.prototype.CalculateBuildMultiplier = function(num) { // Avoid division by zero, in particular 0/0 = NaN which isn't reliably serialized return num < 2 ? 1 : Math.pow(num, this.buildTimeModifier) / num; }; Foundation.prototype.SetBuildMultiplier = function() { this.buildMultiplier = this.CalculateBuildMultiplier(this.GetNumBuilders()); }; Foundation.prototype.GetBuildTime = function() { let timeLeft = (1 - this.GetBuildProgress()) * Engine.QueryInterface(this.entity, IID_Cost).GetBuildTime(); let rate = this.totalBuilderRate * this.buildMultiplier; let rateNew = (this.totalBuilderRate + 1) * this.CalculateBuildMultiplier(this.GetNumBuilders() + 1); return { // Avoid division by zero, in particular 0/0 = NaN which isn't reliably serialized "timeRemaining": rate ? timeLeft / rate : 0, "timeRemainingNew": timeLeft / rateNew }; }; /** * @return {boolean} - Whether the foundation has been committed sucessfully. */ Foundation.prototype.Commit = function() { if (this.committed) return false; let cmpObstruction = Engine.QueryInterface(this.entity, IID_Obstruction); if (cmpObstruction && cmpObstruction.GetBlockMovementFlag(true)) { for (let ent of cmpObstruction.GetEntitiesDeletedUponConstruction()) Engine.DestroyEntity(ent); let collisions = cmpObstruction.GetEntitiesBlockingConstruction(); if (collisions.length) { for (let ent of collisions) { let cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI); if (cmpUnitAI) cmpUnitAI.LeaveFoundation(this.entity); // TODO: What if an obstruction has no UnitAI? } // TODO: maybe we should tell the builder to use a special // animation to indicate they're waiting for people to get // out the way return false; } } // The obstruction always blocks new foundations/construction, // but we've temporarily allowed units to walk all over it // (via CCmpTemplateManager). Now we need to remove that temporary // blocker-disabling, so that we'll perform standard unit blocking instead. if (cmpObstruction) cmpObstruction.SetDisableBlockMovementPathfinding(false, false, -1); let cmpTrigger = Engine.QueryInterface(SYSTEM_ENTITY, IID_Trigger); cmpTrigger.CallEvent("OnConstructionStarted", { "foundation": this.entity, "template": this.finalTemplateName }); let cmpFoundationVisual = Engine.QueryInterface(this.entity, IID_Visual); if (cmpFoundationVisual) cmpFoundationVisual.SelectAnimation("scaffold", false, 1.0); this.committed = true; this.CreateConstructionPreview(); return true; }; /** * Perform some number of seconds of construction work. * Returns true if the construction is completed. */ Foundation.prototype.Build = function(builderEnt, work) { // Do nothing if we've already finished building // (The entity will be destroyed soon after completion so // this won't happen much.) if (this.IsFinished()) return; if (!this.committed && !this.Commit()) return; let cmpHealth = Engine.QueryInterface(this.entity, IID_Health); if (!cmpHealth) { error("Foundation " + this.entity + " does not have a health component."); return; } let deltaHP = work * this.GetBuildRate() * this.buildMultiplier; if (deltaHP > 0) cmpHealth.Increase(deltaHP); // Update the total builder rate. this.totalBuilderRate += work - this.builders.get(builderEnt); this.builders.set(builderEnt, work); // Remember our max progress for partial refund in case of destruction. this.maxProgress = Math.max(this.maxProgress, this.GetBuildProgress()); if (this.maxProgress >= 1.0) { let cmpPlayerStatisticsTracker = QueryOwnerInterface(this.entity, IID_StatisticsTracker); let building = ChangeEntityTemplate(this.entity, this.finalTemplateName); // Make sure the foundation object is the same as the final object. const cmpVisual = Engine.QueryInterface(this.entity, IID_Visual); const cmpBuildingVisual = Engine.QueryInterface(building, IID_Visual); if (cmpVisual && cmpBuildingVisual) cmpBuildingVisual.SetActorSeed(cmpVisual.GetActorSeed()); + const cmpIdentity = Engine.QueryInterface(this.entity, IID_Identity); + const cmpBuildingIdentity = Engine.QueryInterface(building, IID_Identity); + if (cmpIdentity && cmpBuildingIdentity) + { + const oldPhenotype = cmpIdentity.GetPhenotype(); + if (cmpBuildingIdentity.GetPhenotype() !== oldPhenotype) + { + cmpBuildingIdentity.SetPhenotype(oldPhenotype); + if (cmpVisualCorpse) + cmpVisualCorpse.RecomputeActorName(); + } + } + if (cmpPlayerStatisticsTracker) cmpPlayerStatisticsTracker.IncreaseConstructedBuildingsCounter(building); PlaySound("constructed", building); Engine.PostMessage(this.entity, MT_ConstructionFinished, { "entity": this.entity, "newentity": building }); for (let builder of this.GetBuilders()) { let cmpUnitAIBuilder = Engine.QueryInterface(builder, IID_UnitAI); if (cmpUnitAIBuilder) cmpUnitAIBuilder.ConstructionFinished({ "entity": this.entity, "newentity": building }); } } }; Foundation.prototype.GetBuildRate = function() { let cmpHealth = Engine.QueryInterface(this.entity, IID_Health); let cmpCost = Engine.QueryInterface(this.entity, IID_Cost); // Return infinity for instant structure conversion return cmpHealth.GetMaxHitpoints() / cmpCost.GetBuildTime(); }; /** * Create preview entity and copy various parameters from the foundation. */ Foundation.prototype.CreateConstructionPreview = function() { if (this.previewEntity) { Engine.DestroyEntity(this.previewEntity); this.previewEntity = INVALID_ENTITY; } if (!this.committed) return; let cmpFoundationVisual = Engine.QueryInterface(this.entity, IID_Visual); if (!cmpFoundationVisual || !cmpFoundationVisual.HasConstructionPreview()) return; this.previewEntity = Engine.AddLocalEntity("construction|"+this.finalTemplateName); let cmpFoundationOwnership = Engine.QueryInterface(this.entity, IID_Ownership); let cmpPreviewOwnership = Engine.QueryInterface(this.previewEntity, IID_Ownership); if (cmpFoundationOwnership && cmpPreviewOwnership) cmpPreviewOwnership.SetOwner(cmpFoundationOwnership.GetOwner()); // TODO: the 'preview' would be invisible if it doesn't have the below component, // Maybe it makes more sense to simply delete it then? // Initially hide the preview underground let cmpPreviewPosition = Engine.QueryInterface(this.previewEntity, IID_Position); let cmpFoundationPosition = Engine.QueryInterface(this.entity, IID_Position); if (cmpPreviewPosition && cmpFoundationPosition) { let rot = cmpFoundationPosition.GetRotation(); cmpPreviewPosition.SetYRotation(rot.y); cmpPreviewPosition.SetXZRotation(rot.x, rot.z); let pos = cmpFoundationPosition.GetPosition2D(); cmpPreviewPosition.JumpTo(pos.x, pos.y); cmpPreviewPosition.SetConstructionProgress(this.GetBuildProgress()); } let cmpPreviewVisual = Engine.QueryInterface(this.previewEntity, IID_Visual); if (cmpPreviewVisual && cmpFoundationVisual) { cmpPreviewVisual.SetActorSeed(cmpFoundationVisual.GetActorSeed()); cmpPreviewVisual.SelectAnimation("scaffold", false, 1.0); } }; Foundation.prototype.OnEntityRenamed = function(msg) { let cmpFoundationNew = Engine.QueryInterface(msg.newentity, IID_Foundation); if (cmpFoundationNew) cmpFoundationNew.AddBuilders(this.GetBuilders()); }; function FoundationMirage() {} FoundationMirage.prototype.Init = function(cmpFoundation) { this.numBuilders = cmpFoundation.GetNumBuilders(); this.buildTime = cmpFoundation.GetBuildTime(); }; FoundationMirage.prototype.GetNumBuilders = function() { return this.numBuilders; }; FoundationMirage.prototype.GetBuildTime = function() { return this.buildTime; }; Engine.RegisterGlobal("FoundationMirage", FoundationMirage); Foundation.prototype.Mirage = function() { let mirage = new FoundationMirage(); mirage.Init(this); return mirage; }; Engine.RegisterComponentType(IID_Foundation, "Foundation", Foundation); Index: ps/trunk/binaries/data/mods/public/simulation/components/Health.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/Health.js (revision 25831) +++ ps/trunk/binaries/data/mods/public/simulation/components/Health.js (revision 25832) @@ -1,510 +1,526 @@ 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() { let cmpRepairable = Engine.QueryInterface(this.entity, IID_Repairable); return cmpRepairable && cmpRepairable.IsRepairable(); }; 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()) 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); }; /** * @param {number} amount - The amount of damage to be taken. * @param {number} attacker - The entityID of the attacker. * @param {number} attackerOwner - The playerID of the owner of the attacker. * * @eturn {Object} - Object of the form { "healthChange": number }. */ Health.prototype.TakeDamage = function(amount, attacker, attackerOwner) { if (!amount || !this.hitpoints) return { "healthChange": 0 }; let change = this.Reduce(amount); let cmpLoot = Engine.QueryInterface(this.entity, IID_Loot); if (cmpLoot && cmpLoot.GetXp() > 0 && change.healthChange < 0) change.xp = cmpLoot.GetXp() * -change.healthChange / this.GetMaxHitpoints(); if (!this.hitpoints) this.KilledBy(attacker, attackerOwner); return change; }; /** * Called when an entity kills us. * @param {number} attacker - The entityID of the killer. * @param {number} attackerOwner - The playerID of the attacker. */ Health.prototype.KilledBy = function(attacker, attackerOwner) { let cmpAttackerOwnership = Engine.QueryInterface(attacker, IID_Ownership); if (cmpAttackerOwnership) { let currentAttackerOwner = cmpAttackerOwnership.GetOwner(); if (currentAttackerOwner != INVALID_PLAYER) attackerOwner = currentAttackerOwner; } // Add to killer statistics. let cmpKillerPlayerStatisticsTracker = QueryPlayerIDInterface(attackerOwner, IID_StatisticsTracker); if (cmpKillerPlayerStatisticsTracker) cmpKillerPlayerStatisticsTracker.KilledEntity(this.entity); // Add to loser statistics. let cmpTargetPlayerStatisticsTracker = QueryOwnerInterface(this.entity, IID_StatisticsTracker); if (cmpTargetPlayerStatisticsTracker) cmpTargetPlayerStatisticsTracker.LostEntity(this.entity); let cmpLooter = Engine.QueryInterface(attacker, IID_Looter); if (cmpLooter) cmpLooter.Collect(this.entity); }; /** * @param {number} amount - The amount of hitpoints to substract. Kills the entity if required. * @return {{ healthChange:number }} - Number of health points lost. */ 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 { "healthChange": 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 { "healthChange": -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 { "healthChange": 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": 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() { // 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 || !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 templateName = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager).GetCurrentTemplateName(this.entity); 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); // Copy various parameters so it looks just like us. let cmpPositionCorpse = Engine.QueryInterface(entCorpse, IID_Position); let pos = cmpPosition.GetPosition(); cmpPositionCorpse.JumpTo(pos.x, pos.z); let rot = cmpPosition.GetRotation(); cmpPositionCorpse.SetYRotation(rot.y); cmpPositionCorpse.SetXZRotation(rot.x, rot.z); let cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership); let cmpOwnershipCorpse = Engine.QueryInterface(entCorpse, IID_Ownership); if (cmpOwnership && cmpOwnershipCorpse) cmpOwnershipCorpse.SetOwner(cmpOwnership.GetOwner()); let cmpVisualCorpse = Engine.QueryInterface(entCorpse, IID_Visual); if (cmpVisualCorpse) { let cmpVisual = Engine.QueryInterface(this.entity, IID_Visual); if (cmpVisual) cmpVisualCorpse.SetActorSeed(cmpVisual.GetActorSeed()); cmpVisualCorpse.SelectAnimation("death", true, 1); } + const cmpIdentityCorpse = Engine.QueryInterface(entCorpse, IID_Identity); + if (cmpIdentityCorpse) + { + const cmpIdentity = Engine.QueryInterface(this.entity, IID_Identity); + if (cmpIdentity) + { + const oldPhenotype = cmpIdentity.GetPhenotype(); + if (cmpIdentityCorpse.GetPhenotype() !== oldPhenotype) + { + cmpIdentityCorpse.SetPhenotype(oldPhenotype); + if (cmpVisualCorpse) + cmpVisualCorpse.RecomputeActorName(); + } + } + } + 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 }); }; function HealthMirage() {} HealthMirage.prototype.Init = function(cmpHealth) { this.maxHitpoints = cmpHealth.GetMaxHitpoints(); this.hitpoints = cmpHealth.GetHitpoints(); this.repairable = cmpHealth.IsRepairable(); this.injured = cmpHealth.IsInjured(); this.unhealable = cmpHealth.IsUnhealable(); }; HealthMirage.prototype.GetMaxHitpoints = function() { return this.maxHitpoints; }; HealthMirage.prototype.GetHitpoints = function() { return this.hitpoints; }; HealthMirage.prototype.IsRepairable = function() { return this.repairable; }; HealthMirage.prototype.IsInjured = function() { return this.injured; }; HealthMirage.prototype.IsUnhealable = function() { return this.unhealable; }; Engine.RegisterGlobal("HealthMirage", HealthMirage); Health.prototype.Mirage = function() { let mirage = new HealthMirage(); mirage.Init(this); return mirage; }; Engine.RegisterComponentType(IID_Health, "Health", Health); Index: ps/trunk/binaries/data/mods/public/simulation/components/Identity.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/Identity.js (revision 25831) +++ ps/trunk/binaries/data/mods/public/simulation/components/Identity.js (revision 25832) @@ -1,224 +1,229 @@ function Identity() {} Identity.prototype.Schema = "Specifies various names and values associated with the entity, typically for GUI display to users." + "" + "athen" + "Athenian Hoplite" + "Hoplī́tēs Athēnaïkós" + "units/athen_infantry_spearman.png" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "tokens" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "Basic" + "Advanced" + "Elite" + "" + "" + "" + "" + "" + "" + "tokens" + "" + "" + "" + "" + "" + "" + "" + "tokens" + "" + "" + "" + "" + "" + "" + "" + "tokens" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + ""; Identity.prototype.Init = function() { this.classesList = GetIdentityClasses(this.template); this.visibleClassesList = GetVisibleIdentityClasses(this.template); if (this.template.Phenotype) this.phenotype = pickRandom(this.GetPossiblePhenotypes()); else this.phenotype = "default"; this.controllable = this.template.Controllable ? this.template.Controllable == "true" : true; }; Identity.prototype.HasSomeFormation = function() { return this.GetFormationsList().length > 0; }; Identity.prototype.GetCiv = function() { return this.template.Civ; }; Identity.prototype.GetLang = function() { return this.template.Lang || "greek"; // ugly default }; /** * Get a list of possible Phenotypes. * @return {string[]} A list of possible phenotypes. */ Identity.prototype.GetPossiblePhenotypes = function() { return this.template.Phenotype._string.split(/\s+/); }; /** * Get the current Phenotype. * @return {string} The current phenotype. */ Identity.prototype.GetPhenotype = function() { return this.phenotype; }; Identity.prototype.GetRank = function() { return this.template.Rank || ""; }; Identity.prototype.GetClassesList = function() { return this.classesList; }; Identity.prototype.GetVisibleClassesList = function() { return this.visibleClassesList; }; Identity.prototype.HasClass = function(name) { return this.GetClassesList().indexOf(name) != -1; }; Identity.prototype.GetFormationsList = function() { if (this.template.Formations && this.template.Formations._string) return this.template.Formations._string.split(/\s+/); return []; }; Identity.prototype.CanUseFormation = function(template) { return this.GetFormationsList().indexOf(template) != -1; }; Identity.prototype.GetSelectionGroupName = function() { return this.template.SelectionGroupName || ""; }; Identity.prototype.GetGenericName = function() { return this.template.GenericName; }; Identity.prototype.IsUndeletable = function() { return this.template.Undeletable == "true"; }; Identity.prototype.IsControllable = function() { return this.controllable; }; Identity.prototype.SetControllable = function(controllability) { this.controllable = controllability; }; +Identity.prototype.SetPhenotype = function(phenotype) +{ + this.phenotype = phenotype; +}; + function IdentityMirage() {} IdentityMirage.prototype.Init = function(cmpIdentity) { // Mirages don't get identity classes via the template-filter, so that code can query // identity components via Engine.QueryInterface without having to explicitly check for mirages. // This is cloned as otherwise we get a reference to Identity's property, // and that array is deleted when serializing (as it's not seralized), which ends in OOS. this.classes = clone(cmpIdentity.GetClassesList()); }; IdentityMirage.prototype.GetClassesList = function() { return this.classes; }; Engine.RegisterGlobal("IdentityMirage", IdentityMirage); Identity.prototype.Mirage = function() { let mirage = new IdentityMirage(); mirage.Init(this); return mirage; }; Engine.RegisterComponentType(IID_Identity, "Identity", Identity);