Index: binaries/data/mods/public/simulation/components/Heal.js =================================================================== --- binaries/data/mods/public/simulation/components/Heal.js +++ binaries/data/mods/public/simulation/components/Heal.js @@ -47,11 +47,9 @@ Heal.prototype.Init = function() { + this.target = INVALID_ENTITY; }; -// We have no dynamic state to save. -Heal.prototype.Serialize = null; - Heal.prototype.GetTimers = function() { return { @@ -97,7 +95,7 @@ Heal.prototype.CanHeal = function(target) { let cmpHealth = Engine.QueryInterface(target, IID_Health); - if (!cmpHealth || cmpHealth.IsUnhealable()) + if (!cmpHealth || cmpHealth.IsUnhealable() || !cmpHealth.IsInjured()) return false; // Verify that the target is owned by an ally or the player self. @@ -129,29 +127,135 @@ }; /** - * Heal the target entity. This should only be called after a successful range - * check, and should only be called after GetTimers().repeat msec has passed - * since the last call to PerformHeal. + * @param {number} target - The target to heal. + * @param {Object} callback - An object with the functions to call on specific events. + * @return {boolean} - Whether we started healing. */ -Heal.prototype.PerformHeal = function(target) +Heal.prototype.StartHealing = function(target, callback) { - let cmpHealth = Engine.QueryInterface(target, IID_Health); - if (!cmpHealth) + if (this.target != INVALID_ENTITY) + this.StopHealing(); + + let timings = this.GetTimers(); + let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); + + // If the repeat time since the last heal hasn't elapsed, + // delay the action to avoid healing too fast. + let prepare = timings.prepare; + if (this.lastHealed) + { + let repeatLeft = this.lastHealed + timings.repeat - cmpTimer.GetTime(); + prepare = Math.max(prepare, repeatLeft); + } + + let cmpVisual = Engine.QueryInterface(this.entity, IID_Visual); + if (cmpVisual) + { + cmpVisual.SelectAnimation("heal", false, 1.0); + cmpVisual.SetAnimationSyncRepeat(timings.repeat); + cmpVisual.SetAnimationSyncOffset(prepare); + } + + // If using a non-default prepare time, re-sync the animation when the timer runs. + this.resyncAnimation = prepare != timings.prepare; + this.target = target; + this.callback = callback; + this.healTimer = cmpTimer.SetInterval(this.entity, IID_Heal, "PerformHeal", prepare, timings.repeat, null); + + return true; +} + +/** + * @param {string} reason - The reason why we stopped healing. Currently implemented are: + * "outOfRange", "targetInvalidated". + */ +Heal.prototype.StopHealing = function(reason) +{ + if (this.healTimer) + { + let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); + cmpTimer.CancelTimer(this.healTimer); + delete this.healTimer; + } + + let cmpVisual = Engine.QueryInterface(this.entity, IID_Visual); + if (cmpVisual) + cmpVisual.SelectAnimation("idle", false, 1.0); + + if (this.target != INVALID_ENTITY) + this.target = INVALID_ENTITY; + + if (reason && this.callback && this.callback[reason]) + { + let component = Engine.QueryInterface(this.entity, this.callback.iid); + if (component) + component[this.callback[reason]].apply(component); + } + delete this.callback; +} + +/** + * Heal the target entity. This should only be called after GetTimers().repeat + * msec has passed since the last call to PerformHeal. + */ +Heal.prototype.PerformHeal = function(data, lateness) +{ + if (!this.CanHeal(this.target)) + { + this.StopHealing("targetInvalidated"); + return; + } + if (!this.IsTargetInRange(this.target)) + { + this.StopHealing("outOfRange"); return; + } + + let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); + this.lastHealed = cmpTimer.GetTime() - lateness; + let cmpHealth = Engine.QueryInterface(this.target, IID_Health); let targetState = cmpHealth.Increase(this.GetHealth()); // Add experience. - let cmpLoot = Engine.QueryInterface(target, IID_Loot); + let cmpLoot = Engine.QueryInterface(this.target, IID_Loot); let cmpPromotion = Engine.QueryInterface(this.entity, IID_Promotion); if (targetState !== undefined && cmpLoot && cmpPromotion) - { // Health healed times experience per health. cmpPromotion.IncreaseXp((targetState.new - targetState.old) / cmpHealth.GetMaxHitpoints() * cmpLoot.GetXp()); + + // TODO we need a sound file. + // PlaySound("heal_impact", this.entity); + + if (!cmpHealth.IsInjured()) + { + this.StopHealing("targetInvalidated"); + return; } - // TODO we need a sound file -// PlaySound("heal_impact", this.entity); -}; + + if (this.resyncAnimation) + { + let cmpVisual = Engine.QueryInterface(this.entity, IID_Visual); + if (cmpVisual) + { + let repeat = this.GetTimers().repeat; + cmpVisual.SetAnimationSyncRepeat(repeat); + cmpVisual.SetAnimationSyncOffset(repeat); + } + this.resyncAnimation = false; + } +} + +/** + * @param {number} - The entity ID of the target to check. + * @return {boolean} - Whether this entity is in range of its target. + */ +Heal.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); +} Heal.prototype.OnValueModification = function(msg) { @@ -165,4 +269,10 @@ cmpUnitAI.UpdateRangeQueries(); }; +Heal.prototype.OnGlobalEntityRenamed = function(msg) +{ + if (msg.entity == this.target) + this.target = msg.newentity; +}; + Engine.RegisterComponentType(IID_Heal, "Heal", Heal); Index: binaries/data/mods/public/simulation/components/UnitAI.js =================================================================== --- binaries/data/mods/public/simulation/components/UnitAI.js +++ binaries/data/mods/public/simulation/components/UnitAI.js @@ -2522,39 +2522,32 @@ "HEALING": { "enter": function() { - if (!this.CheckRange(this.order.data, IID_Heal)) + let cmpHeal = Engine.QueryInterface(this.entity, IID_Heal); + if (!cmpHeal) { - this.SetNextState("APPROACHING"); + this.FinishOrder(); return true; } - if (!this.TargetIsAlive(this.order.data.target) || - !this.CanHeal(this.order.data.target)) + if (!this.CheckRange(this.order.data, IID_Heal)) { - this.SetNextState("FINDINGNEWTARGET"); + this.OnOutOfRange(); return true; } - let cmpHeal = Engine.QueryInterface(this.entity, IID_Heal); - this.healTimers = cmpHeal.GetTimers(); - - // If the repeat time since the last heal hasn't elapsed, - // delay the action to avoid healing too fast. - var prepare = this.healTimers.prepare; - if (this.lastHealed) + let callback = { + "iid": IID_UnitAI, + "outOfRange": "OnOutOfRange", + "targetInvalidated": "OnTargetInvalidated", + "inventoryFilled": "OnInventoryFilled" + }; + if (!cmpHeal.StartHealing(this.order.data.target, callback)) { - var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); - var repeatLeft = this.lastHealed + this.healTimers.repeat - cmpTimer.GetTime(); - prepare = Math.max(prepare, repeatLeft); + this.OnTargetInvalidated(); + return true; } - this.SelectAnimation("heal"); - this.SetAnimationSync(prepare, this.healTimers.repeat); - this.StartTimer(prepare, this.healTimers.repeat); - - // If using a non-default prepare time, re-sync the animation when the timer runs. - this.resyncAnimation = prepare != this.healTimers.prepare; - + this.StartTimer(1000, 1000); this.FaceTowardsTarget(this.order.data.target); return false; }, @@ -2564,41 +2557,24 @@ this.StopTimer(); }, - "Timer": function(msg) { - let target = this.order.data.target; - if (!this.TargetIsAlive(target) || !this.CanHeal(target)) - { - this.SetNextState("FINDINGNEWTARGET"); - return; - } - if (!this.CheckRange(this.order.data, IID_Heal)) + "OutOfRange": function(msg) { + if (this.ShouldChaseTargetedEntity(this.order.data.target, this.order.data.force)) { - if (this.ShouldChaseTargetedEntity(target, this.order.data.force)) - { - if (this.CanPack()) - { - this.PushOrderFront("Pack", { "force": true }); - return; - } - this.SetNextState("HEAL.APPROACHING"); - } + if (this.CanPack()) + this.PushOrderFront("Pack", { "force": true }); else - this.SetNextState("FINDINGNEWTARGET"); - return; + this.SetNextState("HEAL.APPROACHING"); } + else + this.SetNextState("FINDINGNEWTARGET"); + }, - let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); - this.lastHealed = cmpTimer.GetTime() - msg.lateness; - - this.FaceTowardsTarget(target); - let cmpHeal = Engine.QueryInterface(this.entity, IID_Heal); - cmpHeal.PerformHeal(target); + "TargetInvalidated": function(msg) { + this.SetNextState("FINDINGNEWTARGET"); + }, - if (this.resyncAnimation) - { - this.SetAnimationSync(this.healTimers.repeat, this.healTimers.repeat); - this.resyncAnimation = false; - } + "Timer": function(msg) { + this.FaceTowardsTarget(this.order.data.target); }, }, @@ -4153,6 +4129,16 @@ this.UnitFsm.ProcessMessage(this, {"type": "PackFinished", "packed": msg.packed}); }; +UnitAI.prototype.OnOutOfRange = function(msg) +{ + this.UnitFsm.ProcessMessage(this, { "type": "OutOfRange", "data": msg }); +}; + +UnitAI.prototype.OnTargetInvalidated = function(msg) +{ + this.UnitFsm.ProcessMessage(this, { "type": "TargetInvalidated", "data": msg }); +}; + //// Helper functions to be called by the FSM //// UnitAI.prototype.GetWalkSpeed = function() Index: binaries/data/mods/public/simulation/components/tests/test_Heal.js =================================================================== --- binaries/data/mods/public/simulation/components/tests/test_Heal.js +++ binaries/data/mods/public/simulation/components/tests/test_Heal.js @@ -5,22 +5,28 @@ Engine.LoadComponentScript("interfaces/Health.js"); Engine.LoadComponentScript("interfaces/Loot.js"); Engine.LoadComponentScript("interfaces/Promotion.js"); +Engine.LoadComponentScript("interfaces/Timer.js"); Engine.LoadComponentScript("interfaces/UnitAI.js"); Engine.LoadComponentScript("Heal.js"); +Engine.LoadComponentScript("Timer.js"); const entity = 60; const player = 1; const otherPlayer = 2 +AddMock(SYSTEM_ENTITY, IID_ObstructionManager, { + "IsInTargetRange": () => true +}); + let template = { - "Range": 20, + "Range": "20", "RangeOverlay": { "LineTexture": "heal_overlay_range.png", "LineTextureMask": "heal_overlay_range_mask.png", - "LineThickness": 0.35 + "LineThickness": "0.35" }, - "Health": 5, - "Interval": 2000, + "Health": "5", + "Interval": "2000", "UnhealableClasses": { "_string": "Cavalry" }, "HealableClasses": { "_string": "Support Infantry" }, }; @@ -59,6 +65,7 @@ }; let cmpHeal = ConstructComponent(60, "Heal", template); +let cmpTimer; // Test Getters TS_ASSERT_EQUALS(cmpHeal.GetInterval(), 2000 + 200); @@ -80,7 +87,6 @@ "thickness": 0.35 }]); -// Test PerformHeal let target = 70; AddMock(target, IID_Ownership, { @@ -92,6 +98,7 @@ "GetClassesList": () => targetClasses }); +cmpTimer = ConstructComponent(SYSTEM_ENTITY, "Timer"); let increased; let unhealable = false; AddMock(target, IID_Health, { @@ -101,10 +108,15 @@ TS_ASSERT_EQUALS(amount, 5 + 100); return { "old": 600, "new": 600 + 5 + 100 }; }, - "IsUnhealable": () => unhealable + "IsUnhealable": () => unhealable, + "IsInjured": () => true }); -cmpHeal.PerformHeal(target); +cmpHeal.StartHealing(target); +cmpTimer.OnUpdate({ "turnLength": 1 }); +TS_ASSERT(increased); +increased = false; +cmpTimer.OnUpdate({ "turnLength": 2.2 }); TS_ASSERT(increased); let looted; @@ -123,7 +135,8 @@ }); increased = false; -cmpHeal.PerformHeal(target); +cmpHeal.StartHealing(target); +cmpTimer.OnUpdate({ "turnLength": 1 }); TS_ASSERT(increased && looted && promoted); // Test OnValueModification @@ -165,6 +178,38 @@ }); AddMock(otherTarget, IID_Health, { - "IsUnhealable": () => false + "IsUnhealable": () => false, + "IsInjured": () => true }); TS_ASSERT_UNEVAL_EQUALS(cmpHeal.CanHeal(otherTarget), false); + +cmpTimer = ConstructComponent(SYSTEM_ENTITY, "Timer"); + +increased = false; +AddMock(target, IID_Health, { + "GetMaxHitpoints": () => 700, + "Increase": amount => { + increased = true; + TS_ASSERT_EQUALS(amount, 5 + 100); + return { "old": 600, "new": 600 + 5 + 100 }; + }, + "IsUnhealable": () => false, + "IsInjured": () => true +}); +cmpHeal.StartHealing(target); +cmpTimer.OnUpdate({ "turnLength": 1 }); +TS_ASSERT(increased); + +increased = false; +AddMock(target, IID_Health, { + "GetMaxHitpoints": () => 700, + "Increase": amount => { + increased = true; + TS_ASSERT_EQUALS(amount, 5 + 100); + return { "old": 600, "new": 600 + 5 + 100 }; + }, + "IsUnhealable": () => false, + "IsInjured": () => false +}); +cmpTimer.OnUpdate({ "turnLength": 2.2 }); +TS_ASSERT(!increased);