Index: ps/trunk/binaries/data/mods/public/simulation/components/AutoBuildable.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/AutoBuildable.js (nonexistent) +++ ps/trunk/binaries/data/mods/public/simulation/components/AutoBuildable.js (revision 23514) @@ -0,0 +1,99 @@ +class AutoBuildable +{ + Init() + { + this.rate = ApplyValueModificationsToEntity("AutoBuildable/Rate", +this.template.Rate , this.entity); + if (this.rate) + this.StartTimer(); + } + + get Schema() + { + return "Defines whether the entity can be built by itself." + + "" + + "1.0" + + "" + + "" + + "" + + ""; + } + + /** + * @return {number} - The rate with technologies and aura modification applied. + */ + GetRate() + { + return this.rate; + } + + UpdateRate() + { + this.rate = ApplyValueModificationsToEntity("AutoBuildable/Rate", +this.template.Rate , this.entity); + + if (this.rate) + this.StartTimer(); + } + + StartTimer() + { + if (this.timer || !this.rate) + return; + + let cmpFoundation = Engine.QueryInterface(this.entity, IID_Foundation); + if (!cmpFoundation) + return; + + cmpFoundation.AddBuilder(this.entity); + let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); + this.timer = cmpTimer.SetInterval(this.entity, IID_AutoBuildable, "AutoBuild", 0, 1000, undefined); + } + + CancelTimer() + { + if (!this.timer) + return; + + let cmpFoundation = Engine.QueryInterface(this.entity, IID_Foundation); + if (cmpFoundation) + cmpFoundation.RemoveBuilder(this.entity); + + let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); + cmpTimer.CancelTimer(this.timer); + delete this.timer; + } + + AutoBuild() + { + if (!this.rate) + { + this.CancelTimer(); + return; + } + let cmpFoundation = Engine.QueryInterface(this.entity, IID_Foundation); + if (!cmpFoundation) + { + this.CancelTimer(); + return; + } + + cmpFoundation.Build(this.entity, this.rate); + } +} + +AutoBuildable.prototype.OnValueModification = function(msg) +{ + if (msg.component != "AutoBuildable") + return; + + this.UpdateRate(); +}; + +AutoBuildable.prototype.OnOwnershipChanged = function(msg) +{ + if (msg.to == INVALID_PLAYER) + return; + + this.UpdateRate(); +} + +Engine.RegisterComponentType(IID_AutoBuildable, "AutoBuildable", AutoBuildable); Property changes on: ps/trunk/binaries/data/mods/public/simulation/components/AutoBuildable.js ___________________________________________________________________ Added: svn:eol-style ## -0,0 +1 ## +native \ No newline at end of property Index: ps/trunk/binaries/data/mods/public/simulation/components/Foundation.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/Foundation.js (revision 23513) +++ ps/trunk/binaries/data/mods/public/simulation/components/Foundation.js (revision 23514) @@ -1,468 +1,473 @@ 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.previewEntity = INVALID_ENTITY; }; Foundation.prototype.InitialiseConstruction = function(owner, template) { this.finalTemplateName = template; // We need to know the owner in OnDestroy, but at that point the entity has already been // decoupled from its owner, so we need to remember it in here (and assume it won't change) this.owner = owner; // 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 must have a cost component to know the build time"); this.costs = cmpCost.GetResourceCosts(owner); 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) { // Gradually reveal the final building preview var 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() { var cmpHealth = Engine.QueryInterface(this.entity, IID_Health); if (!cmpHealth) return 0; var hitpoints = cmpHealth.GetHitpoints(); var maxHitpoints = cmpHealth.GetMaxHitpoints(); return hitpoints / maxHitpoints; }; Foundation.prototype.GetBuildPercentage = function() { return Math.floor(this.GetBuildProgress() * 100); }; /** * Returns the current builders. * * @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.OnDestroy = function() { // Refund a portion of the construction cost, proportional to the amount of build progress remaining if (!this.initialised) // this happens if the foundation was destroyed because the player had insufficient resources return; if (this.previewEntity != INVALID_ENTITY) { Engine.DestroyEntity(this.previewEntity); this.previewEntity = INVALID_ENTITY; } if (this.IsFinished()) return; let cmpPlayer = QueryPlayerIDInterface(this.owner); for (var r in this.costs) { var scaled = Math.ceil(this.costs[r] * (1.0 - this.maxProgress)); if (scaled) { cmpPlayer.AddResource(r, scaled); var cmpStatisticsTracker = QueryPlayerIDInterface(this.owner, IID_StatisticsTracker); if (cmpStatisticsTracker) cmpStatisticsTracker.IncreaseResourceUsedCounter(r, -scaled); } } }; /** * Adds an array of builders. * * @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(); }; /** * Adds a single builder to this entity. * * @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 buildRate = Engine.QueryInterface(builderEnt, IID_Builder).GetRate(); + 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; } /** * Adds a builder to the counter. * * @param {number} builderEnt - The entity to add. */ Foundation.prototype.AddBuilder = function(builderEnt) { if (this.AddBuilderHelper(builderEnt)) this.HandleBuildersChanged(); }; 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.builders.size); 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; }; 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; // The rate if we add another woman to the foundation. 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 }; }; /** * 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.GetBuildProgress() == 1.0) return; var cmpObstruction = Engine.QueryInterface(this.entity, IID_Obstruction); // If there are any units in the way, ask them to move away and return early from this method. if (cmpObstruction && cmpObstruction.GetBlockMovementFlag()) { // Remove animal corpses for (let ent of cmpObstruction.GetEntitiesDeletedUponConstruction()) Engine.DestroyEntity(ent); let collisions = cmpObstruction.GetEntitiesBlockingConstruction(); if (collisions.length) { for (var ent of collisions) { var 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; } } // Handle the initial 'committing' of the foundation if (!this.committed) { // 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.GetBlockMovementFlag()) cmpObstruction.SetDisableBlockMovementPathfinding(false, false, -1); // Call the related trigger event var cmpTrigger = Engine.QueryInterface(SYSTEM_ENTITY, IID_Trigger); cmpTrigger.CallEvent("ConstructionStarted", { "foundation": this.entity, "template": this.finalTemplateName }); // Switch foundation to scaffold variant var cmpFoundationVisual = Engine.QueryInterface(this.entity, IID_Visual); if (cmpFoundationVisual) cmpFoundationVisual.SelectAnimation("scaffold", false, 1.0); // Create preview entity and copy various parameters from the foundation if (cmpFoundationVisual && cmpFoundationVisual.HasConstructionPreview()) { this.previewEntity = Engine.AddEntity("construction|"+this.finalTemplateName); var cmpFoundationOwnership = Engine.QueryInterface(this.entity, IID_Ownership); var cmpPreviewOwnership = Engine.QueryInterface(this.previewEntity, IID_Ownership); cmpPreviewOwnership.SetOwner(cmpFoundationOwnership.GetOwner()); // Initially hide the preview underground var cmpPreviewPosition = Engine.QueryInterface(this.previewEntity, IID_Position); cmpPreviewPosition.SetConstructionProgress(0.0); var cmpPreviewVisual = Engine.QueryInterface(this.previewEntity, IID_Visual); if (cmpPreviewVisual) { cmpPreviewVisual.SetActorSeed(cmpFoundationVisual.GetActorSeed()); cmpPreviewVisual.SelectAnimation("scaffold", false, 1.0, ""); } var cmpFoundationPosition = Engine.QueryInterface(this.entity, IID_Position); var pos = cmpFoundationPosition.GetPosition2D(); var rot = cmpFoundationPosition.GetRotation(); cmpPreviewPosition.SetYRotation(rot.y); cmpPreviewPosition.SetXZRotation(rot.x, rot.z); cmpPreviewPosition.JumpTo(pos.x, pos.y); } this.committed = true; } // Add an appropriate proportion of hitpoints var cmpHealth = Engine.QueryInterface(this.entity, IID_Health); if (!cmpHealth) { error("Foundation " + this.entity + " does not have a health component."); return; } var 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); var progress = this.GetBuildProgress(); // Remember our max progress for partial refund in case of destruction this.maxProgress = Math.max(this.maxProgress, progress); if (progress >= 1.0) { // Finished construction // Create the real entity var building = Engine.AddEntity(this.finalTemplateName); // Copy various parameters from the foundation var cmpVisual = Engine.QueryInterface(this.entity, IID_Visual); var cmpBuildingVisual = Engine.QueryInterface(building, IID_Visual); if (cmpVisual && cmpBuildingVisual) cmpBuildingVisual.SetActorSeed(cmpVisual.GetActorSeed()); var cmpPosition = Engine.QueryInterface(this.entity, IID_Position); if (!cmpPosition || !cmpPosition.IsInWorld()) { error("Foundation " + this.entity + " does not have a position in-world."); Engine.DestroyEntity(building); return; } var cmpBuildingPosition = Engine.QueryInterface(building, IID_Position); if (!cmpBuildingPosition) { error("New building " + building + " has no position component."); Engine.DestroyEntity(building); return; } var pos = cmpPosition.GetPosition2D(); cmpBuildingPosition.JumpTo(pos.x, pos.y); var rot = cmpPosition.GetRotation(); cmpBuildingPosition.SetYRotation(rot.y); cmpBuildingPosition.SetXZRotation(rot.x, rot.z); // TODO: should add a ICmpPosition::CopyFrom() instead of all this var cmpRallyPoint = Engine.QueryInterface(this.entity, IID_RallyPoint); var cmpBuildingRallyPoint = Engine.QueryInterface(building, IID_RallyPoint); if(cmpRallyPoint && cmpBuildingRallyPoint) { var rallyCoords = cmpRallyPoint.GetPositions(); var rallyData = cmpRallyPoint.GetData(); for (var i = 0; i < rallyCoords.length; ++i) { cmpBuildingRallyPoint.AddPosition(rallyCoords[i].x, rallyCoords[i].z); cmpBuildingRallyPoint.AddData(rallyData[i]); } } // ---------------------------------------------------------------------- var owner; var cmpTerritoryDecay = Engine.QueryInterface(building, IID_TerritoryDecay); if (cmpTerritoryDecay && cmpTerritoryDecay.HasTerritoryOwnership()) { let cmpTerritoryManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TerritoryManager); owner = cmpTerritoryManager.GetOwner(pos.x, pos.y); } else { let cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership); if (!cmpOwnership) { error("Foundation " + this.entity + " has no ownership."); Engine.DestroyEntity(building); return; } owner = cmpOwnership.GetOwner(); } var cmpBuildingOwnership = Engine.QueryInterface(building, IID_Ownership); if (!cmpBuildingOwnership) { error("New Building " + building + " has no ownership."); Engine.DestroyEntity(building); return; } cmpBuildingOwnership.SetOwner(owner); /* Copy over the obstruction control group IDs from the foundation entities. This is needed to ensure that when a foundation is completed and replaced by a new entity, it remains in the same control group(s) as any other foundation entities that may surround it. This is the mechanism that is used to e.g. enable wall pieces to be built closely together, ignoring their mutual obstruction shapes (since they would otherwise be prevented from being built so closely together). If the control groups are not copied over, the new entity will default to a new control group containing only itself, and will hence block construction of any surrounding foundations that it was previously in the same control group with. Note that this will result in the completed building entities having control group IDs that equal entity IDs of old (and soon to be deleted) foundation entities. This should not have any consequences, however, since the control group IDs are only meant to be unique identifiers, which is still true when reusing the old ones. */ var cmpBuildingObstruction = Engine.QueryInterface(building, IID_Obstruction); if (cmpObstruction && cmpBuildingObstruction) { cmpBuildingObstruction.SetControlGroup(cmpObstruction.GetControlGroup()); cmpBuildingObstruction.SetControlGroup2(cmpObstruction.GetControlGroup2()); } var cmpPlayerStatisticsTracker = QueryOwnerInterface(this.entity, IID_StatisticsTracker); if (cmpPlayerStatisticsTracker) cmpPlayerStatisticsTracker.IncreaseConstructedBuildingsCounter(building); var cmpBuildingHealth = Engine.QueryInterface(building, IID_Health); if (cmpBuildingHealth) cmpBuildingHealth.SetHitpoints(progress * cmpBuildingHealth.GetMaxHitpoints()); PlaySound("constructed", building); Engine.PostMessage(this.entity, MT_ConstructionFinished, { "entity": this.entity, "newentity": building }); Engine.PostMessage(this.entity, MT_EntityRenamed, { "entity": this.entity, "newentity": building }); Engine.DestroyEntity(this.entity); } }; 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(); }; Engine.RegisterComponentType(IID_Foundation, "Foundation", Foundation); Index: ps/trunk/binaries/data/mods/public/simulation/components/interfaces/AutoBuildable.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/interfaces/AutoBuildable.js (nonexistent) +++ ps/trunk/binaries/data/mods/public/simulation/components/interfaces/AutoBuildable.js (revision 23514) @@ -0,0 +1 @@ +Engine.RegisterInterface("AutoBuildable"); Property changes on: ps/trunk/binaries/data/mods/public/simulation/components/interfaces/AutoBuildable.js ___________________________________________________________________ Added: svn:eol-style ## -0,0 +1 ## +native \ No newline at end of property Index: ps/trunk/binaries/data/mods/public/simulation/components/tests/test_AutoBuildable.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/tests/test_AutoBuildable.js (nonexistent) +++ ps/trunk/binaries/data/mods/public/simulation/components/tests/test_AutoBuildable.js (revision 23514) @@ -0,0 +1,16 @@ +Engine.LoadHelperScript("ValueModification.js"); +Engine.LoadComponentScript("interfaces/AutoBuildable.js"); +Engine.LoadComponentScript("interfaces/Foundation.js"); +Engine.LoadComponentScript("interfaces/ModifiersManager.js"); +Engine.LoadComponentScript("AutoBuildable.js"); + +const cmpBuildableAuto = ConstructComponent(10, "AutoBuildable", { + "Rate": "1.0" +}); + +TS_ASSERT_EQUALS(cmpBuildableAuto.GetRate(), 1); + +const cmpBuildableNoRate = ConstructComponent(12, "AutoBuildable", { + "Rate": "0" +}); +TS_ASSERT_EQUALS(cmpBuildableNoRate.GetRate(), 0); Property changes on: ps/trunk/binaries/data/mods/public/simulation/components/tests/test_AutoBuildable.js ___________________________________________________________________ Added: svn:eol-style ## -0,0 +1 ## +native \ No newline at end of property 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 23513) +++ ps/trunk/binaries/data/mods/public/simulation/components/tests/test_Foundation.js (revision 23514) @@ -1,211 +1,266 @@ Engine.LoadHelperScript("Player.js"); +Engine.LoadHelperScript("ValueModification.js"); +Engine.LoadComponentScript("interfaces/AutoBuildable.js"); Engine.LoadComponentScript("interfaces/Builder.js"); Engine.LoadComponentScript("interfaces/Cost.js"); Engine.LoadComponentScript("interfaces/Foundation.js"); Engine.LoadComponentScript("interfaces/Health.js"); +Engine.LoadComponentScript("interfaces/ModifiersManager.js"); Engine.LoadComponentScript("interfaces/StatisticsTracker.js"); Engine.LoadComponentScript("interfaces/TerritoryDecay.js"); Engine.LoadComponentScript("interfaces/Trigger.js"); +Engine.LoadComponentScript("interfaces/Timer.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 finalTemplate = "structures/athen_civil_centre.xml"; 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, }); 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, { "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); }, }); for (let mock of mocks) AddMock(...mock); // INITIALISE Engine.AddEntity = function(template) { TS_ASSERT_EQUALS(template, "construction|" + finalTemplate); return previewEnt; }; cmpFoundation = ConstructComponent(foundationEnt, "Foundation", {}); cmpFoundation.InitialiseConstruction(player, finalTemplate); TS_ASSERT_EQUALS(cmpFoundation.owner, player); 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; 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(player, 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 23513) +++ ps/trunk/binaries/data/mods/public/simulation/templates/special/filter/foundation.xml (revision 23514) @@ -1,45 +1,46 @@ + 0 1 true true 0 false