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();