Index: ps/trunk/binaries/data/mods/public/simulation/components/Builder.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/Builder.js +++ ps/trunk/binaries/data/mods/public/simulation/components/Builder.js @@ -18,12 +18,15 @@ "" + ""; +/* + * Build interval and repeat time, in ms. + */ +Builder.prototype.BUILD_INTERVAL = 1000; + Builder.prototype.Init = function() { }; -Builder.prototype.Serialize = null; // we have no dynamic state to save - Builder.prototype.GetEntitiesList = function() { let string = this.template.Entities._string; @@ -80,28 +83,120 @@ }; /** - * Build/repair the target entity. This should only be called after a successful range check. - * It should be called at a rate of once per second. + * @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.PerformBuilding = function(target) +Builder.prototype.StopRepairing = function(reason) { - let rate = this.GetRate(); + if (this.timer) + { + let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); + cmpTimer.CancelTimer(this.timer); + delete this.timer; + } + + if (this.target) + { + 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 repairing again, + // replacing the callerIID, hence save that. + let callerIID = this.callerIID; + delete this.callerIID; - let cmpFoundation = Engine.QueryInterface(target, IID_Foundation); + 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, rate); + cmpFoundation.Build(this.entity, this.GetRate()); return; } - let cmpRepairable = Engine.QueryInterface(target, IID_Repairable); + let cmpRepairable = Engine.QueryInterface(this.target, IID_Repairable); if (cmpRepairable) { - cmpRepairable.Repair(this.entity, rate); + 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'))) Index: ps/trunk/binaries/data/mods/public/simulation/components/UnitAI.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/UnitAI.js +++ ps/trunk/binaries/data/mods/public/simulation/components/UnitAI.js @@ -2961,6 +2961,13 @@ "REPAIRING": { "enter": function() { + let cmpBuilder = Engine.QueryInterface(this.entity, IID_Builder); + if (!cmpBuilder) + { + this.FinishOrder(); + return true; + } + // If this order was forced, the player probably gave it, but now we've reached the target // switch to an unforced order (can be interrupted by attacks) if (this.order.data.force) @@ -2968,68 +2975,43 @@ this.order.data.force = false; - // Needed to remove the entity from the builder list when leaving this state. - this.repairTarget = this.order.data.target; - - if (!this.CanRepair(this.repairTarget)) + if (!this.CheckTargetRange(this.order.data.target, IID_Builder)) { - this.FinishOrder(); - return true; - } - - if (!this.CheckTargetRange(this.repairTarget, IID_Builder)) - { - this.SetNextState("APPROACHING"); + this.ProcessMessage("OutOfRange"); return true; } - let cmpHealth = Engine.QueryInterface(this.repairTarget, IID_Health); + let cmpHealth = Engine.QueryInterface(this.order.data.target, IID_Health); if (cmpHealth && cmpHealth.GetHitpoints() >= cmpHealth.GetMaxHitpoints()) { // The building was already finished/fully repaired before we arrived; // let the ConstructionFinished handler handle this. - this.ConstructionFinished({ "entity": this.repairTarget, "newentity": this.repairTarget }); + this.ConstructionFinished({ "entity": this.order.data.target, "newentity": this.order.data.target }); return true; } - let cmpBuilderList = QueryBuilderListInterface(this.repairTarget); - if (cmpBuilderList) - cmpBuilderList.AddBuilder(this.entity); - - this.FaceTowardsTarget(this.repairTarget); + if (!cmpBuilder.StartRepairing(this.order.data.target, IID_UnitAI)) + { + this.ProcessMessage("TargetInvalidated"); + return true; + } - this.SelectAnimation("build"); - this.StartTimer(1000, 1000); + this.FaceTowardsTarget(this.order.data.target); return false; }, "leave": function() { - let cmpBuilderList = QueryBuilderListInterface(this.repairTarget); - if (cmpBuilderList) - cmpBuilderList.RemoveBuilder(this.entity); - delete this.repairTarget; - this.StopTimer(); - this.ResetAnimation(); + let cmpBuilder = Engine.QueryInterface(this.entity, IID_Builder); + if (cmpBuilder) + cmpBuilder.StopRepairing(); }, - "Timer": function(msg) { - if (!this.CanRepair(this.repairTarget)) - { - this.FinishOrder(); - return; - } - - this.FaceTowardsTarget(this.repairTarget); + "OutOfRange": function(msg) { + this.SetNextState("APPROACHING"); + }, - let cmpBuilder = Engine.QueryInterface(this.entity, IID_Builder); - cmpBuilder.PerformBuilding(this.repairTarget); - // If the building is completed, the leave() function will be called - // by the ConstructionFinished message. - // In that case, the repairTarget is deleted, and we can just return. - if (!this.repairTarget) - return; - if (!this.CheckTargetRange(this.repairTarget, IID_Builder)) - this.SetNextState("APPROACHING"); + "TargetInvalidated": function(msg) { + this.FinishOrder(); }, }, 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 +++ ps/trunk/binaries/data/mods/public/simulation/components/tests/test_Builder.js @@ -1,11 +1,21 @@ Engine.LoadHelperScript("Player.js"); Engine.LoadComponentScript("interfaces/Builder.js"); +Engine.LoadComponentScript("interfaces/Foundation.js"); +Engine.LoadComponentScript("interfaces/Repairable.js"); +Engine.LoadComponentScript("interfaces/Timer.js"); +Engine.LoadComponentScript("interfaces/UnitAI.js"); Engine.LoadComponentScript("Builder.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 }); @@ -14,7 +24,7 @@ let cmpBuilder = ConstructComponent(builderId, "Builder", { - "Rate": 1.0, + "Rate": "1.0", "Entities": { "_string": "structures/{civ}/barracks structures/{civ}/civil_centre structures/{native}/house" } }); @@ -81,3 +91,30 @@ }); 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); Index: ps/trunk/binaries/data/mods/public/simulation/components/tests/test_UnitAI.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/tests/test_UnitAI.js +++ ps/trunk/binaries/data/mods/public/simulation/components/tests/test_UnitAI.js @@ -68,8 +68,14 @@ ); TestTargetEntityRenaming( - "INDIVIDUAL.REPAIR.REPAIRING", "INDIVIDUAL.IDLE", + "INDIVIDUAL.REPAIR.REPAIRING", "INDIVIDUAL.REPAIR.REPAIRING", (unitAI, player_ent, target_ent) => { + + AddMock(player_ent, IID_Builder, { + "StartRepairing": () => true, + "StopRepairing": () => {} + }); + QueryBuilderListInterface = () => {}; unitAI.CheckTargetRange = () => true; unitAI.CanRepair = (target) => target == target_ent;