Index: ps/trunk/binaries/data/mods/public/simulation/components/Builder.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/Builder.js (revision 25392) +++ ps/trunk/binaries/data/mods/public/simulation/components/Builder.js (revision 25393) @@ -1,208 +1,208 @@ function Builder() {} Builder.prototype.Schema = "Allows the unit to construct and repair buildings." + "" + "1.0" + "" + "\n structures/{civ}/barracks\n structures/{native}/civil_centre\n structures/pers/apadana\n " + "" + "" + "" + "" + "" + "" + "" + "tokens" + "" + "" + ""; /* * Build interval and repeat time, in ms. */ Builder.prototype.BUILD_INTERVAL = 1000; Builder.prototype.Init = function() { }; Builder.prototype.GetEntitiesList = function() { let string = this.template.Entities._string; if (!string) return []; let cmpPlayer = QueryOwnerInterface(this.entity); if (!cmpPlayer) return []; string = ApplyValueModificationsToEntity("Builder/Entities/_string", string, this.entity); let cmpIdentity = Engine.QueryInterface(this.entity, IID_Identity); if (cmpIdentity) string = string.replace(/\{native\}/g, cmpIdentity.GetCiv()); let entities = string.replace(/\{civ\}/g, cmpPlayer.GetCiv()).split(/\s+/); let disabledTemplates = cmpPlayer.GetDisabledTemplates(); let cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager); return entities.filter(ent => !disabledTemplates[ent] && cmpTemplateManager.TemplateExists(ent)); }; Builder.prototype.GetRange = function() { let max = 2; let cmpObstruction = Engine.QueryInterface(this.entity, IID_Obstruction); if (cmpObstruction) max += cmpObstruction.GetSize(); return { "max": max, "min": 0 }; }; Builder.prototype.GetRate = function() { return ApplyValueModificationsToEntity("Builder/Rate", +this.template.Rate, this.entity); }; /** * @param {number} target - The target to check. * @return {boolean} - Whether we can build/repair the given target. */ Builder.prototype.CanRepair = function(target) { let cmpFoundation = QueryMiragedInterface(target, IID_Foundation); let cmpRepairable = QueryMiragedInterface(target, IID_Repairable); - if (!cmpFoundation && !cmpRepairable) + if (!cmpFoundation && (!cmpRepairable || !cmpRepairable.IsRepairable())) return false; let cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership); return cmpOwnership && IsOwnedByAllyOfPlayer(cmpOwnership.GetOwner(), target); }; /** * @param {number} target - The target to repair. * @param {number} callerIID - The IID to notify on specific events. * @return {boolean} - Whether we started repairing. */ Builder.prototype.StartRepairing = function(target, callerIID) { if (this.target) this.StopRepairing(); if (!this.CanRepair(target)) return false; let cmpBuilderList = QueryBuilderListInterface(target); if (cmpBuilderList) cmpBuilderList.AddBuilder(this.entity); let cmpVisual = Engine.QueryInterface(this.entity, IID_Visual); if (cmpVisual) cmpVisual.SelectAnimation("build", false, 1.0); this.target = target; this.callerIID = callerIID; let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); this.timer = cmpTimer.SetInterval(this.entity, IID_Builder, "PerformBuilding", this.BUILD_INTERVAL, this.BUILD_INTERVAL, null); return true; }; /** * @param {string} reason - The reason why we stopped repairing. */ Builder.prototype.StopRepairing = function(reason) { if (!this.target) return; let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); cmpTimer.CancelTimer(this.timer); delete this.timer; let cmpBuilderList = QueryBuilderListInterface(this.target); if (cmpBuilderList) cmpBuilderList.RemoveBuilder(this.entity); delete this.target; let cmpVisual = Engine.QueryInterface(this.entity, IID_Visual); if (cmpVisual) cmpVisual.SelectAnimation("idle", false, 1.0); // The callerIID component may start again, // replacing the callerIID, hence save that. let callerIID = this.callerIID; delete this.callerIID; if (reason && callerIID) { let component = Engine.QueryInterface(this.entity, callerIID); if (component) component.ProcessMessage(reason, null); } }; /** * Repair our target entity. * @params - data and lateness are unused. */ Builder.prototype.PerformBuilding = function(data, lateness) { if (!this.CanRepair(this.target)) { this.StopRepairing("TargetInvalidated"); return; } if (!this.IsTargetInRange(this.target)) { this.StopRepairing("OutOfRange"); return; } // ToDo: Enable entities to keep facing a target. Engine.QueryInterface(this.entity, IID_UnitAI)?.FaceTowardsTarget(this.target); let cmpFoundation = Engine.QueryInterface(this.target, IID_Foundation); if (cmpFoundation) { cmpFoundation.Build(this.entity, this.GetRate()); return; } let cmpRepairable = Engine.QueryInterface(this.target, IID_Repairable); if (cmpRepairable) { cmpRepairable.Repair(this.entity, this.GetRate()); return; } }; /** * @param {number} - The entity ID of the target to check. * @return {boolean} - Whether this entity is in range of its target. */ Builder.prototype.IsTargetInRange = function(target) { let range = this.GetRange(); let cmpObstructionManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ObstructionManager); return cmpObstructionManager.IsInTargetRange(this.entity, target, range.min, range.max, false); }; Builder.prototype.OnValueModification = function(msg) { if (msg.component != "Builder" || !msg.valueNames.some(name => name.endsWith('_string'))) return; // Token changes may require selection updates. let cmpPlayer = QueryOwnerInterface(this.entity, IID_Player); if (cmpPlayer) Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface).SetSelectionDirty(cmpPlayer.GetPlayerID()); }; Engine.RegisterComponentType(IID_Builder, "Builder", Builder); Index: ps/trunk/binaries/data/mods/public/simulation/components/Health.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/Health.js (revision 25392) +++ ps/trunk/binaries/data/mods/public/simulation/components/Health.js (revision 25393) @@ -1,509 +1,510 @@ 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; + 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); } 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/Repairable.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/Repairable.js (revision 25392) +++ ps/trunk/binaries/data/mods/public/simulation/components/Repairable.js (revision 25393) @@ -1,180 +1,193 @@ function Repairable() {} Repairable.prototype.Schema = "Deals with repairable structures and units." + "" + "2.0" + "" + "" + "" + ""; Repairable.prototype.Init = function() { 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.buildTimePenalty = 0.7; // Penalty for having multiple builders this.repairTimeRatio = +this.template.RepairTimeRatio; }; /** * Returns the current build progress in a [0,1] range. */ Repairable.prototype.GetBuildProgress = function() { var cmpHealth = Engine.QueryInterface(this.entity, IID_Health); if (!cmpHealth) return 0; var hitpoints = cmpHealth.GetHitpoints(); var maxHitpoints = cmpHealth.GetMaxHitpoints(); return hitpoints / maxHitpoints; }; /** + * @return whether this entity can be repaired (this does not account for health). + */ +Repairable.prototype.IsRepairable = function() +{ + return !this.unrepairable; +}; + +Repairable.prototype.SetRepairability = function(repairable) +{ + this.unrepairable = !repairable; +}; + +/** * Returns the current builders. * * @return {number[]} - An array containing the entity IDs of assigned builders. */ Repairable.prototype.GetBuilders = function() { return Array.from(this.builders.keys()); }; Repairable.prototype.GetNumBuilders = function() { return this.builders.size; }; /** * Adds an array of builders. * * @param {number[]} - An array containing the entity IDs of builders to assign. */ Repairable.prototype.AddBuilders = function(builders) { for (let builder of builders) this.AddBuilder(builder); }; Repairable.prototype.AddBuilder = function(builderEnt) { if (this.builders.has(builderEnt)) return; this.builders.set(builderEnt, Engine.QueryInterface(builderEnt, IID_Builder).GetRate()); this.totalBuilderRate += this.builders.get(builderEnt); this.SetBuildMultiplier(); }; Repairable.prototype.RemoveBuilder = function(builderEnt) { if (!this.builders.has(builderEnt)) return; this.totalBuilderRate -= this.builders.get(builderEnt); this.builders.delete(builderEnt); this.SetBuildMultiplier(); }; /** * 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. */ Repairable.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.buildTimePenalty) / num; }; Repairable.prototype.SetBuildMultiplier = function() { this.buildMultiplier = this.CalculateBuildMultiplier(this.GetNumBuilders()); }; Repairable.prototype.GetBuildTime = function() { let timeLeft = (1 - this.GetBuildProgress()) * Engine.QueryInterface(this.entity, IID_Cost).GetBuildTime() * this.repairTimeRatio; let rate = this.totalBuilderRate * this.buildMultiplier; // The rate if we add another woman to the repairs 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 }; }; // TODO: should we have resource costs? Repairable.prototype.Repair = function(builderEnt, rate) { let cmpHealth = Engine.QueryInterface(this.entity, IID_Health); let cmpCost = Engine.QueryInterface(this.entity, IID_Cost); if (!cmpHealth || !cmpCost) return; let damage = cmpHealth.GetMaxHitpoints() - cmpHealth.GetHitpoints(); if (damage <= 0) return; // Calculate the amount of hitpoints that will be added (using diminishing rate when several builders) let work = rate * this.buildMultiplier * this.GetRepairRate(); let amount = Math.min(damage, work); cmpHealth.Increase(amount); // Update the total builder rate this.totalBuilderRate += rate - this.builders.get(builderEnt); this.builders.set(builderEnt, rate); // If we repaired all the damage, send a message to entities to stop repairing this building if (amount >= damage) { Engine.PostMessage(this.entity, MT_ConstructionFinished, { "entity": this.entity, "newentity": this.entity }); // Inform the builders that repairing has finished. // This not done by listening to a global message due to performance. for (let builder of this.GetBuilders()) { let cmpUnitAIBuilder = Engine.QueryInterface(builder, IID_UnitAI); if (cmpUnitAIBuilder) cmpUnitAIBuilder.ConstructionFinished({ "entity": this.entity, "newentity": this.entity }); } } }; Repairable.prototype.GetRepairRate = function() { let cmpHealth = Engine.QueryInterface(this.entity, IID_Health); let cmpCost = Engine.QueryInterface(this.entity, IID_Cost); let repairTime = this.repairTimeRatio * cmpCost.GetBuildTime(); return repairTime ? cmpHealth.GetMaxHitpoints() / repairTime : 1; }; Repairable.prototype.OnEntityRenamed = function(msg) { let cmpRepairableNew = Engine.QueryInterface(msg.newentity, IID_Repairable); if (cmpRepairableNew) cmpRepairableNew.AddBuilders(this.GetBuilders()); }; function RepairableMirage() {} RepairableMirage.prototype.Init = function(cmpRepairable) { this.numBuilders = cmpRepairable.GetNumBuilders(); this.buildTime = cmpRepairable.GetBuildTime(); }; RepairableMirage.prototype.GetNumBuilders = function() { return this.numBuilders; }; RepairableMirage.prototype.GetBuildTime = function() { return this.buildTime; }; Engine.RegisterGlobal("RepairableMirage", RepairableMirage); Repairable.prototype.Mirage = function() { let mirage = new RepairableMirage(); mirage.Init(this); return mirage; }; Engine.RegisterComponentType(IID_Repairable, "Repairable", Repairable); Index: ps/trunk/binaries/data/mods/public/simulation/components/tests/test_Builder.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/tests/test_Builder.js (revision 25392) +++ ps/trunk/binaries/data/mods/public/simulation/components/tests/test_Builder.js (revision 25393) @@ -1,120 +1,190 @@ Engine.LoadHelperScript("Player.js"); Engine.LoadComponentScript("interfaces/Builder.js"); +Engine.LoadComponentScript("interfaces/Cost.js"); Engine.LoadComponentScript("interfaces/Foundation.js"); +Engine.LoadComponentScript("interfaces/Health.js"); Engine.LoadComponentScript("interfaces/Repairable.js"); Engine.LoadComponentScript("interfaces/Timer.js"); Engine.LoadComponentScript("interfaces/UnitAI.js"); Engine.LoadComponentScript("Builder.js"); +Engine.LoadComponentScript("Health.js"); +Engine.LoadComponentScript("Repairable.js"); Engine.LoadComponentScript("Timer.js"); const builderId = 6; const target = 7; const playerId = 1; const playerEntityID = 2; AddMock(SYSTEM_ENTITY, IID_ObstructionManager, { "IsInTargetRange": () => true }); AddMock(SYSTEM_ENTITY, IID_TemplateManager, { "TemplateExists": () => true }); Engine.RegisterGlobal("ApplyValueModificationsToEntity", (prop, oVal, ent) => oVal); - -let cmpBuilder = ConstructComponent(builderId, "Builder", { - "Rate": "1.0", - "Entities": { "_string": "structures/{civ}/barracks structures/{civ}/civil_centre structures/{native}/house" } -}); - -TS_ASSERT_UNEVAL_EQUALS(cmpBuilder.GetEntitiesList(), []); - -AddMock(SYSTEM_ENTITY, IID_PlayerManager, { - "GetPlayerByID": id => playerEntityID -}); - -AddMock(playerEntityID, IID_Player, { - "GetCiv": () => "iber", - "GetDisabledTemplates": () => ({}), - "GetPlayerID": () => playerId -}); - -AddMock(builderId, IID_Ownership, { - "GetOwner": () => playerId -}); - -AddMock(builderId, IID_Identity, { - "GetCiv": () => "iber" -}); - -TS_ASSERT_UNEVAL_EQUALS(cmpBuilder.GetEntitiesList(), ["structures/iber/barracks", "structures/iber/civil_centre", "structures/iber/house"]); - -AddMock(SYSTEM_ENTITY, IID_TemplateManager, { - "TemplateExists": name => name == "structures/iber/civil_centre" -}); - -TS_ASSERT_UNEVAL_EQUALS(cmpBuilder.GetEntitiesList(), ["structures/iber/civil_centre"]); - -AddMock(SYSTEM_ENTITY, IID_TemplateManager, { - "TemplateExists": () => true -}); - -AddMock(playerEntityID, IID_Player, { - "GetCiv": () => "iber", - "GetDisabledTemplates": () => ({ "structures/athen/barracks": true }), - "GetPlayerID": () => playerId -}); - -TS_ASSERT_UNEVAL_EQUALS(cmpBuilder.GetEntitiesList(), ["structures/iber/barracks", "structures/iber/civil_centre", "structures/iber/house"]); - -AddMock(playerEntityID, IID_Player, { - "GetCiv": () => "iber", - "GetDisabledTemplates": () => ({ "structures/iber/barracks": true }), - "GetPlayerID": () => playerId -}); - -TS_ASSERT_UNEVAL_EQUALS(cmpBuilder.GetEntitiesList(), ["structures/iber/civil_centre", "structures/iber/house"]); - -AddMock(playerEntityID, IID_Player, { - "GetCiv": () => "athen", - "GetDisabledTemplates": () => ({ "structures/athen/barracks": true }), - "GetPlayerID": () => playerId -}); - -TS_ASSERT_UNEVAL_EQUALS(cmpBuilder.GetEntitiesList(), ["structures/athen/civil_centre", "structures/iber/house"]); - -TS_ASSERT_UNEVAL_EQUALS(cmpBuilder.GetRange(), { "max": 2, "min": 0 }); - -AddMock(builderId, IID_Obstruction, { - "GetSize": () => 1 -}); - -TS_ASSERT_UNEVAL_EQUALS(cmpBuilder.GetRange(), { "max": 3, "min": 0 }); - -// Test repairing. -AddMock(playerEntityID, IID_Player, { - "IsAlly": (p) => p == playerId -}); - -AddMock(target, IID_Ownership, { - "GetOwner": () => playerId -}); - -let increased = false; -AddMock(target, IID_Foundation, { - "Build": (entity, amount) => { - increased = true; - TS_ASSERT_EQUALS(amount, 1); - }, - "AddBuilder": () => {} -}); - -let cmpTimer = ConstructComponent(SYSTEM_ENTITY, "Timer"); - -TS_ASSERT(cmpBuilder.StartRepairing(target)); -cmpTimer.OnUpdate({ "turnLength": 1 }); -TS_ASSERT(increased); -increased = false; -cmpTimer.OnUpdate({ "turnLength": 2 }); -TS_ASSERT(increased); +function testEntitiesList() +{ + let cmpBuilder = ConstructComponent(builderId, "Builder", { + "Rate": "1.0", + "Entities": { "_string": "structures/{civ}/barracks structures/{civ}/civil_centre structures/{native}/house" } + }); + + TS_ASSERT_UNEVAL_EQUALS(cmpBuilder.GetEntitiesList(), []); + + AddMock(SYSTEM_ENTITY, IID_PlayerManager, { + "GetPlayerByID": id => playerEntityID + }); + + AddMock(playerEntityID, IID_Player, { + "GetCiv": () => "iber", + "GetDisabledTemplates": () => ({}), + "GetPlayerID": () => playerId + }); + + AddMock(builderId, IID_Ownership, { + "GetOwner": () => playerId + }); + + AddMock(builderId, IID_Identity, { + "GetCiv": () => "iber" + }); + + TS_ASSERT_UNEVAL_EQUALS(cmpBuilder.GetEntitiesList(), ["structures/iber/barracks", "structures/iber/civil_centre", "structures/iber/house"]); + + AddMock(SYSTEM_ENTITY, IID_TemplateManager, { + "TemplateExists": name => name == "structures/iber/civil_centre" + }); + + TS_ASSERT_UNEVAL_EQUALS(cmpBuilder.GetEntitiesList(), ["structures/iber/civil_centre"]); + + AddMock(SYSTEM_ENTITY, IID_TemplateManager, { + "TemplateExists": () => true + }); + + AddMock(playerEntityID, IID_Player, { + "GetCiv": () => "iber", + "GetDisabledTemplates": () => ({ "structures/athen/barracks": true }), + "GetPlayerID": () => playerId + }); + + TS_ASSERT_UNEVAL_EQUALS(cmpBuilder.GetEntitiesList(), ["structures/iber/barracks", "structures/iber/civil_centre", "structures/iber/house"]); + + AddMock(playerEntityID, IID_Player, { + "GetCiv": () => "iber", + "GetDisabledTemplates": () => ({ "structures/iber/barracks": true }), + "GetPlayerID": () => playerId + }); + + TS_ASSERT_UNEVAL_EQUALS(cmpBuilder.GetEntitiesList(), ["structures/iber/civil_centre", "structures/iber/house"]); + + AddMock(playerEntityID, IID_Player, { + "GetCiv": () => "athen", + "GetDisabledTemplates": () => ({ "structures/athen/barracks": true }), + "GetPlayerID": () => playerId + }); + + TS_ASSERT_UNEVAL_EQUALS(cmpBuilder.GetEntitiesList(), ["structures/athen/civil_centre", "structures/iber/house"]); + + TS_ASSERT_UNEVAL_EQUALS(cmpBuilder.GetRange(), { "max": 2, "min": 0 }); + + AddMock(builderId, IID_Obstruction, { + "GetSize": () => 1 + }); + + TS_ASSERT_UNEVAL_EQUALS(cmpBuilder.GetRange(), { "max": 3, "min": 0 }); +} +testEntitiesList(); + +function testBuildingFoundation() +{ + let cmpBuilder = ConstructComponent(builderId, "Builder", { + "Rate": "1.0", + "Entities": { "_string": "" } + }); + + AddMock(playerEntityID, IID_Player, { + "IsAlly": (p) => p == playerId + }); + + AddMock(target, IID_Ownership, { + "GetOwner": () => playerId + }); + + let increased = false; + AddMock(target, IID_Foundation, { + "Build": (entity, amount) => { + increased = true; + TS_ASSERT_EQUALS(amount, 1); + }, + "AddBuilder": () => {} + }); + + let cmpTimer = ConstructComponent(SYSTEM_ENTITY, "Timer"); + + TS_ASSERT(cmpBuilder.StartRepairing(target)); + cmpTimer.OnUpdate({ "turnLength": 1 }); + TS_ASSERT(increased); + increased = false; + cmpTimer.OnUpdate({ "turnLength": 2 }); + TS_ASSERT(increased); +} +testBuildingFoundation(); + +function testRepairing() +{ + AddMock(playerEntityID, IID_Player, { + "IsAlly": (p) => p == playerId + }); + + let cmpBuilder = ConstructComponent(builderId, "Builder", { + "Rate": "1.0", + "Entities": { "_string": "" } + }); + + AddMock(target, IID_Ownership, { + "GetOwner": () => playerId + }); + + AddMock(target, IID_Cost, { + "GetBuildTime": () => 100 + }); + + let cmpTargetHealth = ConstructComponent(target, "Health", { + "Max": 100, + "RegenRate": 0, + "IdleRegenRate": 0, + "DeathType": "vanish", + "Unhealable": false + }); + + cmpTargetHealth.SetHitpoints(50); + + DeleteMock(target, IID_Foundation); + let cmpTargetRepairable = ConstructComponent(target, "Repairable", { + "RepairTimeRatio": 1, + }); + + let cmpTimer = ConstructComponent(SYSTEM_ENTITY, "Timer"); + + TS_ASSERT(cmpTargetRepairable.IsRepairable()); + TS_ASSERT(cmpBuilder.StartRepairing(target)); + cmpTimer.OnUpdate({ "turnLength": 1 }); + TS_ASSERT_EQUALS(cmpTargetHealth.GetHitpoints(), 51); + cmpTimer.OnUpdate({ "turnLength": 1 }); + TS_ASSERT_EQUALS(cmpTargetHealth.GetHitpoints(), 52); + cmpTargetRepairable.SetRepairability(false); + cmpTimer.OnUpdate({ "turnLength": 1 }); + TS_ASSERT_EQUALS(cmpTargetHealth.GetHitpoints(), 52); + cmpTargetRepairable.SetRepairability(true); + // Check that we indeed stopped - shouldn't restart on its own. + cmpTimer.OnUpdate({ "turnLength": 1 }); + TS_ASSERT_EQUALS(cmpTargetHealth.GetHitpoints(), 52); + TS_ASSERT(cmpBuilder.StartRepairing(target)); + cmpTimer.OnUpdate({ "turnLength": 1 }); + TS_ASSERT_EQUALS(cmpTargetHealth.GetHitpoints(), 53); +} +testRepairing();