Index: ps/trunk/binaries/data/mods/public/simulation/components/Foundation.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/Foundation.js (revision 25298) +++ ps/trunk/binaries/data/mods/public/simulation/components/Foundation.js (revision 25299) @@ -1,438 +1,441 @@ 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.buildTimePenalty = 0.7; // Penalty for having multiple builders + + 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.buildTimePenalty) / num; + 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("ConstructionStarted", { "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); 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/tests/test_Foundation.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/tests/test_Foundation.js (revision 25298) +++ ps/trunk/binaries/data/mods/public/simulation/components/tests/test_Foundation.js (revision 25299) @@ -1,279 +1,281 @@ Engine.LoadHelperScript("Player.js"); Engine.LoadHelperScript("Transform.js"); Engine.LoadHelperScript("ValueModification.js"); Engine.LoadComponentScript("interfaces/AutoBuildable.js"); Engine.LoadComponentScript("interfaces/Builder.js"); Engine.LoadComponentScript("interfaces/Capturable.js"); Engine.LoadComponentScript("interfaces/Cost.js"); Engine.LoadComponentScript("interfaces/Foundation.js"); Engine.LoadComponentScript("interfaces/Guard.js"); Engine.LoadComponentScript("interfaces/Health.js"); Engine.LoadComponentScript("interfaces/ModifiersManager.js"); Engine.LoadComponentScript("interfaces/Population.js"); Engine.LoadComponentScript("interfaces/Promotion.js"); Engine.LoadComponentScript("interfaces/Repairable.js"); Engine.LoadComponentScript("interfaces/ResourceGatherer.js"); Engine.LoadComponentScript("interfaces/StatisticsTracker.js"); Engine.LoadComponentScript("interfaces/StatusEffectsReceiver.js"); Engine.LoadComponentScript("interfaces/TerritoryDecay.js"); Engine.LoadComponentScript("interfaces/Trigger.js"); Engine.LoadComponentScript("interfaces/Timer.js"); Engine.LoadComponentScript("interfaces/UnitAI.js"); Engine.LoadComponentScript("AutoBuildable.js"); Engine.LoadComponentScript("Foundation.js"); Engine.LoadComponentScript("Timer.js"); let player = 1; let playerEnt = 3; let foundationEnt = 20; let previewEnt = 21; let newEnt = 22; let finalTemplate = "structures/athen/civil_centre.xml"; function testFoundation(...mocks) { ResetState(); let foundationHP = 1; let maxHP = 100; let rot = new Vector3D(1, 2, 3); let pos = new Vector2D(4, 5); let cmpFoundation; AddMock(SYSTEM_ENTITY, IID_Trigger, { "CallEvent": () => {}, }); AddMock(SYSTEM_ENTITY, IID_PlayerManager, { "GetPlayerByID": () => playerEnt, }); AddMock(SYSTEM_ENTITY, IID_TerritoryManager, { "GetOwner": (x, y) => { TS_ASSERT_EQUALS(x, pos.x); TS_ASSERT_EQUALS(y, pos.y); return player; }, }); Engine.RegisterGlobal("PlaySound", (name, source) => { TS_ASSERT_EQUALS(name, "constructed"); TS_ASSERT_EQUALS(source, newEnt); }); Engine.RegisterGlobal("MT_EntityRenamed", "entityRenamed"); AddMock(foundationEnt, IID_Cost, { "GetBuildTime": () => 50, "GetResourceCosts": () => ({ "wood": 100 }), }); AddMock(foundationEnt, IID_Health, { "GetHitpoints": () => foundationHP, "GetMaxHitpoints": () => maxHP, "Increase": hp => { foundationHP = Math.min(foundationHP + hp, maxHP); cmpFoundation.OnHealthChanged(); }, }); AddMock(foundationEnt, IID_Obstruction, { "GetBlockMovementFlag": () => true, "GetEntitiesBlockingConstruction": () => [], "GetEntitiesDeletedUponConstruction": () => [], "SetDisableBlockMovementPathfinding": () => {}, }); AddMock(foundationEnt, IID_Ownership, { "GetOwner": () => player, }); AddMock(foundationEnt, IID_Position, { "GetPosition2D": () => pos, "GetRotation": () => rot, "SetConstructionProgress": () => {}, "IsInWorld": () => true, "GetHeightOffset": () => {}, "MoveOutOfWorld": () => {} }); AddMock(previewEnt, IID_Ownership, { "SetOwner": owner => { TS_ASSERT_EQUALS(owner, player); }, }); AddMock(previewEnt, IID_Position, { "JumpTo": (x, y) => { TS_ASSERT_EQUALS(x, pos.x); TS_ASSERT_EQUALS(y, pos.y); }, "SetConstructionProgress": p => {}, "SetYRotation": r => { TS_ASSERT_EQUALS(r, rot.y); }, "SetXZRotation": (rx, rz) => { TS_ASSERT_EQUALS(rx, rot.x); TS_ASSERT_EQUALS(rz, rot.z); }, }); AddMock(newEnt, IID_Ownership, { "SetOwner": owner => { TS_ASSERT_EQUALS(owner, player); }, }); AddMock(newEnt, IID_Position, { "GetPosition2D": () => pos, "JumpTo": (x, y) => { TS_ASSERT_EQUALS(x, pos.x); TS_ASSERT_EQUALS(y, pos.y); }, "SetYRotation": r => { TS_ASSERT_EQUALS(r, rot.y); }, "SetXZRotation": (rx, rz) => { TS_ASSERT_EQUALS(rx, rot.x); TS_ASSERT_EQUALS(rz, rot.z); }, "SetHeightOffset": () => {} }); for (let mock of mocks) AddMock(...mock); // INITIALISE Engine.AddLocalEntity = function(template) { TS_ASSERT_EQUALS(template, "construction|" + finalTemplate); return previewEnt; }; - cmpFoundation = ConstructComponent(foundationEnt, "Foundation", {}); + cmpFoundation = ConstructComponent(foundationEnt, "Foundation", { + "BuildTimeModifier": "0.7" + }); cmpFoundation.InitialiseConstruction(finalTemplate); TS_ASSERT_EQUALS(cmpFoundation.finalTemplateName, finalTemplate); TS_ASSERT_EQUALS(cmpFoundation.maxProgress, 0); TS_ASSERT_EQUALS(cmpFoundation.initialised, true); // BUILDER COUNT, BUILD RATE, TIME REMAINING AddMock(10, IID_Builder, { "GetRate": () => 1.0 }); AddMock(11, IID_Builder, { "GetRate": () => 1.0 }); - let twoBuilderMultiplier = Math.pow(2, cmpFoundation.buildTimePenalty) / 2; - let threeBuilderMultiplier = Math.pow(3, cmpFoundation.buildTimePenalty) / 3; + let twoBuilderMultiplier = Math.pow(2, cmpFoundation.buildTimeModifier) / 2; + let threeBuilderMultiplier = Math.pow(3, cmpFoundation.buildTimeModifier) / 3; TS_ASSERT_EQUALS(cmpFoundation.CalculateBuildMultiplier(1), 1); TS_ASSERT_EQUALS(cmpFoundation.CalculateBuildMultiplier(2), twoBuilderMultiplier); TS_ASSERT_EQUALS(cmpFoundation.CalculateBuildMultiplier(3), threeBuilderMultiplier); TS_ASSERT_EQUALS(cmpFoundation.GetBuildRate(), 2); TS_ASSERT_EQUALS(cmpFoundation.GetNumBuilders(), 0); TS_ASSERT_EQUALS(cmpFoundation.totalBuilderRate, 0); cmpFoundation.AddBuilder(10); TS_ASSERT_EQUALS(cmpFoundation.GetNumBuilders(), 1); TS_ASSERT_EQUALS(cmpFoundation.buildMultiplier, 1); TS_ASSERT_EQUALS(cmpFoundation.totalBuilderRate, 1); // Foundation starts with 1 hp, so there's 50 * 99/100 = 49.5 seconds left. TS_ASSERT_UNEVAL_EQUALS(cmpFoundation.GetBuildTime(), { 'timeRemaining': 49.5, 'timeRemainingNew': 49.5 / (2 * twoBuilderMultiplier) }); cmpFoundation.AddBuilder(11); TS_ASSERT_EQUALS(cmpFoundation.GetNumBuilders(), 2); TS_ASSERT_EQUALS(cmpFoundation.buildMultiplier, twoBuilderMultiplier); TS_ASSERT_EQUALS(cmpFoundation.totalBuilderRate, 2); TS_ASSERT_UNEVAL_EQUALS(cmpFoundation.GetBuildTime(), { 'timeRemaining': 49.5 / (2 * twoBuilderMultiplier), 'timeRemainingNew': 49.5 / (3 * threeBuilderMultiplier) }); cmpFoundation.AddBuilder(11); TS_ASSERT_EQUALS(cmpFoundation.GetNumBuilders(), 2); TS_ASSERT_EQUALS(cmpFoundation.buildMultiplier, twoBuilderMultiplier); cmpFoundation.RemoveBuilder(11); TS_ASSERT_EQUALS(cmpFoundation.GetNumBuilders(), 1); TS_ASSERT_EQUALS(cmpFoundation.buildMultiplier, 1); cmpFoundation.RemoveBuilder(11); TS_ASSERT_EQUALS(cmpFoundation.GetNumBuilders(), 1); TS_ASSERT_EQUALS(cmpFoundation.buildMultiplier, 1); TS_ASSERT_EQUALS(cmpFoundation.totalBuilderRate, 1); // COMMIT FOUNDATION TS_ASSERT_EQUALS(cmpFoundation.committed, false); let work = 5; cmpFoundation.Build(10, work); TS_ASSERT_EQUALS(cmpFoundation.committed, true); TS_ASSERT_EQUALS(foundationHP, 1 + work * cmpFoundation.GetBuildRate() * cmpFoundation.buildMultiplier); TS_ASSERT_EQUALS(cmpFoundation.maxProgress, foundationHP / maxHP); TS_ASSERT_EQUALS(cmpFoundation.totalBuilderRate, 5); // FINISH CONSTRUCTION Engine.AddEntity = function(template) { TS_ASSERT_EQUALS(template, finalTemplate); return newEnt; }; cmpFoundation.Build(10, 1000); TS_ASSERT_EQUALS(cmpFoundation.maxProgress, 1); TS_ASSERT_EQUALS(foundationHP, maxHP); } testFoundation(); testFoundation([foundationEnt, IID_Visual, { "SetVariable": (key, num) => { TS_ASSERT_EQUALS(key, "numbuilders"); TS_ASSERT(num == 1 || num == 2); }, "SelectAnimation": (name, once, speed) => name, "HasConstructionPreview": () => true, }]); testFoundation([newEnt, IID_TerritoryDecay, { "HasTerritoryOwnership": () => true, }]); testFoundation([playerEnt, IID_StatisticsTracker, { "IncreaseConstructedBuildingsCounter": ent => { TS_ASSERT_EQUALS(ent, newEnt); }, }]); // Test autobuild feature. const foundationEnt2 = 42; let turnLength = 0.2; let currentFoundationHP = 1; let cmpTimer = ConstructComponent(SYSTEM_ENTITY, "Timer"); AddMock(foundationEnt2, IID_Cost, { "GetBuildTime": () => 50, "GetResourceCosts": () => ({ "wood": 100 }), }); const cmpAutoBuildingFoundation = ConstructComponent(foundationEnt2, "Foundation", {}); AddMock(foundationEnt2, IID_Health, { "GetHitpoints": () => currentFoundationHP, "GetMaxHitpoints": () => 100, "Increase": hp => { currentFoundationHP = Math.min(currentFoundationHP + hp, 100); cmpAutoBuildingFoundation.OnHealthChanged(); }, }); const cmpBuildableAuto = ConstructComponent(foundationEnt2, "AutoBuildable", { "Rate": "1.0" }); cmpAutoBuildingFoundation.InitialiseConstruction(finalTemplate); // We start at 3 cause there is no delay on the first run. cmpTimer.OnUpdate({ "turnLength": turnLength }); for (let i = 0; i < 10; ++i) { if (i == 8) { cmpBuildableAuto.CancelTimer(); TS_ASSERT_EQUALS(cmpAutoBuildingFoundation.GetNumBuilders(), 0); } let currentPercentage = cmpAutoBuildingFoundation.GetBuildPercentage(); cmpTimer.OnUpdate({ "turnLength": turnLength * 5 }); let newPercentage = cmpAutoBuildingFoundation.GetBuildPercentage(); if (i >= 8) TS_ASSERT_EQUALS(currentPercentage, newPercentage); else // Rate * Max Health / Cost. TS_ASSERT_EQUALS(currentPercentage + 2, newPercentage); } Index: ps/trunk/binaries/data/mods/public/simulation/templates/special/filter/foundation.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/special/filter/foundation.xml (revision 25298) +++ ps/trunk/binaries/data/mods/public/simulation/templates/special/filter/foundation.xml (revision 25299) @@ -1,50 +1,52 @@ - + + 0.7 + 1 Foundation true true 0 0 false