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
@@ -1,4 +1,205 @@
-function Heal() {}
+class Heal
+{
+ Init()
+ {
+ this.target = INVALID_ENTITY;
+ }
+
+ GetTimers()
+ {
+ return {
+ "prepare": 1000,
+ "repeat": this.GetRate()
+ };
+ }
+
+ /**
+ * @return {number} - The amount of HitPoints regenerated per interval.
+ */
+ GetHP()
+ {
+ return ApplyValueModificationsToEntity("Heal/HP", +this.template.HP, this.entity);
+ }
+
+ GetRate()
+ {
+ return ApplyValueModificationsToEntity("Heal/Rate", +this.template.Rate, this.entity);
+ }
+
+ GetRange()
+ {
+ return {
+ "min": 0,
+ "max": ApplyValueModificationsToEntity("Heal/Range", +this.template.Range, this.entity)
+ };
+ }
+
+ GetUnhealableClasses()
+ {
+ return this.template.UnhealableClasses._string || "";
+ }
+
+ GetHealableClasses()
+ {
+ return this.template.HealableClasses._string || "";
+ }
+
+ GetRangeOverlays()
+ {
+ if (!this.template.RangeOverlay)
+ return [];
+
+ return [{
+ "radius": this.GetRange().max,
+ "texture": this.template.RangeOverlay.LineTexture,
+ "textureMask": this.template.RangeOverlay.LineTextureMask,
+ "thickness": +this.template.RangeOverlay.LineThickness
+ }];
+ }
+
+ /**
+ * Starts healing the specified target.
+ *
+ * @param {number} target - The target to heal.
+ * @return {Object} - The timers in the form { "prepare": {number}, "repeat": {number} }.
+ */
+ StartHealing(target)
+ {
+ if (!this.IsInRange(target))
+ {
+ this.StopHealing(false, true);
+ return;
+ }
+
+ // We were already healing!
+ 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.healTimer = cmpTimer.SetInterval(this.entity, IID_Heal, "PerformHeal", prepare, timings.repeat, null);
+
+ return timings;
+ }
+
+ /**
+ * Stops healing.
+ * @param {boolean} healed - Whether we stopped because the target was healed.
+ * @param {boolean} outOfRange - Whether we stopped because the target got out of our range.
+ */
+ StopHealing(healed = false, outOfRange = false)
+ {
+ 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;
+
+ Engine.PostMessage(this.entity, MT_HealingStopped, {
+ "healed": healed,
+ "outOfRange": outOfRange
+ });
+ }
+
+ /**
+ * 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.
+ */
+ PerformHeal(data, lateness)
+ {
+ if (this.target == INVALID_ENTITY)
+ {
+ this.StopHealing(true);
+ return;
+ }
+ if (!this.IsInRange(this.target))
+ {
+ this.StopHealing(false, true);
+ return;
+ }
+
+ let cmpHealth = Engine.QueryInterface(this.target, IID_Health);
+ if (!cmpHealth || !cmpHealth.IsInjured())
+ {
+ this.StopHealing(true);
+ return;
+ }
+
+ let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
+ this.lastHealed = cmpTimer.GetTime() - lateness;
+
+ let targetState = cmpHealth.Increase(this.GetHP());
+
+ // Add experience points.
+ let cmpLoot = Engine.QueryInterface(this.target, IID_Loot);
+ let cmpPromotion = Engine.QueryInterface(this.entity, IID_Promotion);
+ if (targetState !== undefined && cmpLoot && cmpPromotion)
+ // HP healed * XP per HP
+ 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(true);
+ return;
+ }
+
+ 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.
+ */
+ IsInRange(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.Schema =
"Controls the healing abilities of the unit." +
@@ -14,7 +215,7 @@
"Cavalry" +
"Support Infantry" +
"" +
- "" +
+ "" +
"" +
"" +
"" +
@@ -26,105 +227,25 @@
"" +
"" +
"" +
- "" +
+ "" +
"" +
"" +
- "" +
+ "" +
"" +
"" +
- "" +
+ "" +
"" +
"tokens" +
"" +
"" +
"" +
- "" +
+ "" +
"" +
"tokens" +
"" +
"" +
"";
-Heal.prototype.Init = function()
-{
-};
-
-Heal.prototype.Serialize = null; // we have no dynamic state to save
-
-Heal.prototype.GetTimers = function()
-{
- return {
- "prepare": 1000,
- "repeat": this.GetRate()
- };
-};
-
-Heal.prototype.GetHP = function()
-{
- return ApplyValueModificationsToEntity("Heal/HP", +this.template.HP, this.entity);
-};
-
-Heal.prototype.GetRate = function()
-{
- return ApplyValueModificationsToEntity("Heal/Rate", +this.template.Rate, this.entity);
-};
-
-Heal.prototype.GetRange = function()
-{
- return {
- "min": 0,
- "max": ApplyValueModificationsToEntity("Heal/Range", +this.template.Range, this.entity)
- };
-};
-
-Heal.prototype.GetUnhealableClasses = function()
-{
- return this.template.UnhealableClasses._string || "";
-};
-
-Heal.prototype.GetHealableClasses = function()
-{
- return this.template.HealableClasses._string || "";
-};
-
-Heal.prototype.GetRangeOverlays = function()
-{
- if (!this.template.RangeOverlay)
- return [];
-
- return [{
- "radius": this.GetRange().max,
- "texture": this.template.RangeOverlay.LineTexture,
- "textureMask": this.template.RangeOverlay.LineTextureMask,
- "thickness": +this.template.RangeOverlay.LineThickness
- }];
-};
-
-/**
- * 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.
- */
-Heal.prototype.PerformHeal = function(target)
-{
- let cmpHealth = Engine.QueryInterface(target, IID_Health);
- if (!cmpHealth)
- return;
-
- let targetState = cmpHealth.Increase(this.GetHP());
-
- // Add XP
- let cmpLoot = Engine.QueryInterface(target, IID_Loot);
- let cmpPromotion = Engine.QueryInterface(this.entity, IID_Promotion);
- if (targetState !== undefined && cmpLoot && cmpPromotion)
- {
- // HP healed * XP per HP
- cmpPromotion.IncreaseXp((targetState.new - targetState.old) / cmpHealth.GetMaxHitpoints() * cmpLoot.GetXp());
- }
- //TODO we need a sound file
-// PlaySound("heal_impact", this.entity);
-};
-
Heal.prototype.OnValueModification = function(msg)
{
if (msg.component != "Heal" || msg.valueNames.indexOf("Heal/Range") === -1)
@@ -137,4 +258,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
@@ -193,6 +193,10 @@
// ignore
},
+ "HealingStopped": function(msg) {
+ // ignore
+ },
+
// Formation handlers:
"FormationLeave": function(msg) {
@@ -2527,48 +2531,30 @@
}
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)
- {
- var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
- var repeatLeft = this.lastHealed + this.healTimers.repeat - cmpTimer.GetTime();
- prepare = Math.max(prepare, repeatLeft);
- }
-
- 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;
+ let healTimers = cmpHeal.StartHealing(this.order.data.target);
+ this.StartTimer(healTimers.prepare, healTimers.repeat);
this.FaceTowardsTarget(this.order.data.target);
return false;
},
"leave": function() {
- this.ResetAnimation();
+ let cmpHeal = Engine.QueryInterface(this.entity, IID_Heal);
+ if (cmpHeal)
+ cmpHeal.StopHealing();
this.StopTimer();
},
- "Timer": function(msg) {
- let target = this.order.data.target;
- // Check the target is still alive and healable
- if (!this.TargetIsAlive(target) || !this.CanHeal(target))
+ "HealingStopped": function(msg) {
+ if (msg.data.healed)
{
this.SetNextState("FINDINGNEWTARGET");
return;
}
- // Check if we can still reach the target
- if (!this.CheckRange(this.order.data, IID_Heal))
+ if (msg.data.outOfRange)
{
- if (this.ShouldChaseTargetedEntity(target, this.order.data.force))
+ if (this.ShouldChaseTargetedEntity(this.order.data.target, this.order.data.force))
{
- // Can't reach it - try to chase after it
if (this.CanPack())
{
this.PushOrderFront("Pack", { "force": true });
@@ -2578,21 +2564,19 @@
}
else
this.SetNextState("FINDINGNEWTARGET");
- return;
}
+ },
- 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);
-
- if (this.resyncAnimation)
+ "Timer": function(msg) {
+ let target = this.order.data.target;
+ // Check the target is still alive and healable
+ if (!this.TargetIsAlive(target) || !this.CanHeal(target))
{
- this.SetAnimationSync(this.healTimers.repeat, this.healTimers.repeat);
- this.resyncAnimation = false;
+ this.SetNextState("FINDINGNEWTARGET");
+ return;
}
+
+ this.FaceTowardsTarget(target);
},
},
@@ -4136,6 +4120,11 @@
this.UnitFsm.ProcessMessage(this, {"type": "PackFinished", "packed": msg.packed});
};
+UnitAI.prototype.OnHealingStopped = function(msg)
+{
+ this.UnitFsm.ProcessMessage(this, { "type": "HealingStopped", "data": msg });
+};
+
//// Helper functions to be called by the FSM ////
UnitAI.prototype.GetWalkSpeed = function()
Index: binaries/data/mods/public/simulation/components/interfaces/Heal.js
===================================================================
--- binaries/data/mods/public/simulation/components/interfaces/Heal.js
+++ binaries/data/mods/public/simulation/components/interfaces/Heal.js
@@ -1 +1,7 @@
Engine.RegisterInterface("Heal");
+
+/**
+ * Message of the form { "healed": boolean, "outOfRange": boolean }
+ * sent from Healing component whenever the entity stops healing.
+ */
+Engine.RegisterMessageType("HealingStopped");
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
@@ -3,20 +3,26 @@
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 entity = 60;
+let target = 70;
+AddMock(SYSTEM_ENTITY, IID_ObstructionManager, {
+ "IsInTargetRange": () => true
+});
let template = {
- "Range": 20,
- "RangeOverlay" : {
+ "Range": "20",
+ "RangeOverlay": {
"LineTexture": "heal_overlay_range.png",
"LineTextureMask": "heal_overlay_range_mask.png",
- "LineThickness": 0.35
+ "LineThickness": "0.35"
},
- "HP": 5,
- "Rate": 2000,
+ "HP": "5",
+ "Rate": "2000",
"UnhealableClasses": { "_string": "Cavalry" },
"HealableClasses": { "_string": "Support Infantry" },
};
@@ -39,72 +45,117 @@
};
let cmpHeal = ConstructComponent(60, "Heal", template);
+let cmpTimer;
-// Test Getters
-TS_ASSERT_EQUALS(cmpHeal.GetRate(), 2000 + 200);
-
-TS_ASSERT_UNEVAL_EQUALS(cmpHeal.GetTimers(), { "prepare": 1000, "repeat": 2000 + 200 });
-
-TS_ASSERT_EQUALS(cmpHeal.GetHP(), 5 + 100);
-
-TS_ASSERT_UNEVAL_EQUALS(cmpHeal.GetRange(), {"min":0, "max": 20 + 300 });
-
-TS_ASSERT_EQUALS(cmpHeal.GetHealableClasses(), "Support Infantry");
+function test_Getters()
+{
+ TS_ASSERT_EQUALS(cmpHeal.GetRate(), 2000 + 200);
-TS_ASSERT_EQUALS(cmpHeal.GetUnhealableClasses(), "Cavalry");
+ TS_ASSERT_UNEVAL_EQUALS(cmpHeal.GetTimers(), { "prepare": 1000, "repeat": 2000 + 200 });
-TS_ASSERT_UNEVAL_EQUALS(cmpHeal.GetRangeOverlays(), [{
- "radius": 20 + 300,
- "texture": "heal_overlay_range.png",
- "textureMask": "heal_overlay_range_mask.png",
- "thickness": 0.35
-}]);
+ TS_ASSERT_EQUALS(cmpHeal.GetHP(), 5 + 100);
-// Test PerformHeal
-let target = 70;
+ TS_ASSERT_UNEVAL_EQUALS(cmpHeal.GetRange(), { "min": 0, "max": 20 + 300 });
-let increased;
-AddMock(target, IID_Health, {
- "GetMaxHitpoints": () => 700,
- "Increase": amount => {
- increased = true;
- TS_ASSERT_EQUALS(amount, 5 + 100);
- return { "old": 600, "new": 600 + 5 + 100 };
- }
-});
+ TS_ASSERT_EQUALS(cmpHeal.GetHealableClasses(), "Support Infantry");
-cmpHeal.PerformHeal(target);
-TS_ASSERT(increased);
+ TS_ASSERT_EQUALS(cmpHeal.GetUnhealableClasses(), "Cavalry");
-let looted;
-AddMock(target, IID_Loot, {
- "GetXp": () => {
- looted = true; return 80;
- }
-});
+ TS_ASSERT_UNEVAL_EQUALS(cmpHeal.GetRangeOverlays(), [{
+ "radius": 20 + 300,
+ "texture": "heal_overlay_range.png",
+ "textureMask": "heal_overlay_range_mask.png",
+ "thickness": 0.35
+ }]);
+}
+test_Getters();
-let promoted;
-AddMock(entity, IID_Promotion, {
- "IncreaseXp": amount => {
- promoted = true;
- TS_ASSERT_EQUALS(amount, (5 + 100) * 80 / 700);
- }
-});
+function test_Healing()
+{
+ cmpTimer = ConstructComponent(SYSTEM_ENTITY, "Timer");
-increased = false;
-cmpHeal.PerformHeal(target);
-TS_ASSERT(increased && looted && promoted);
-
-// Test OnValueModification
-let updated;
-AddMock(entity, IID_UnitAI, {
- "UpdateRangeQueries": () => {
- updated = true;
- }
-});
+ let increased;
+ AddMock(target, IID_Health, {
+ "GetMaxHitpoints": () => 700,
+ "Increase": amount => {
+ increased = true;
+ TS_ASSERT_EQUALS(amount, 5 + 100);
+ return { "old": 600, "new": 600 + 5 + 100 };
+ },
+ "IsInjured": () => true,
+ });
+
+ cmpHeal.StartHealing(target);
+ cmpTimer.OnUpdate({ "turnLength": 1 });
+ TS_ASSERT(increased);
+ increased = false;
+ cmpTimer.OnUpdate({ "turnLength": 2.2 });
+ TS_ASSERT(increased);
+
+ let looted;
+ AddMock(target, IID_Loot, {
+ "GetXp": () => {
+ looted = true; return 80;
+ }
+ });
+
+ let promoted;
+ AddMock(entity, IID_Promotion, {
+ "IncreaseXp": amount => {
+ promoted = true;
+ TS_ASSERT_EQUALS(amount, (5 + 100) * 80 / 700);
+ }
+ });
+
+ increased = false;
+ cmpHeal.PerformHeal(target);
+ TS_ASSERT(increased && looted && promoted);
+
+ // Test OnValueModification
+ let updated;
+ AddMock(entity, IID_UnitAI, {
+ "UpdateRangeQueries": () => {
+ updated = true;
+ }
+ });
+
+ cmpHeal.OnValueModification({ "component": "Heal", "valueNames": ["Heal/HP"] });
+ TS_ASSERT(!updated);
+
+ cmpHeal.OnValueModification({ "component": "Heal", "valueNames": ["Heal/Range"] });
+ TS_ASSERT(updated);
+}
+test_Healing();
-cmpHeal.OnValueModification({ "component": "Heal", "valueNames": ["Heal/HP"] });
-TS_ASSERT(!updated);
+function testStopHealing()
+{
+ cmpTimer = ConstructComponent(SYSTEM_ENTITY, "Timer");
-cmpHeal.OnValueModification({ "component": "Heal", "valueNames": ["Heal/Range"] });
-TS_ASSERT(updated);
+ let 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 };
+ },
+ "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 };
+ },
+ "IsInjured": () => false,
+ });
+ cmpTimer.OnUpdate({ "turnLength": 2.2 });
+ TS_ASSERT(!increased);
+}
+testStopHealing();