Index: binaries/data/mods/public/simulation/components/BuildingAI.js =================================================================== --- binaries/data/mods/public/simulation/components/BuildingAI.js +++ binaries/data/mods/public/simulation/components/BuildingAI.js @@ -73,12 +73,7 @@ BuildingAI.prototype.OnDestroy = function() { - if (this.timer) - { - let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); - cmpTimer.CancelTimer(this.timer); - this.timer = undefined; - } + this.StopTimer(); // Clean up range queries let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); @@ -96,6 +91,13 @@ if (msg.component != "Attack") return; + // Restart the timer if the attack repeat time has changed + if (this.timer && msg.valueNames.indexOf("Attack/" + attackType + "/RepeatTime") != -1) + { + this.StopTimer(); + this.StartTimer(true); + } + this.targetUnits = []; this.SetupRangeQuery(); this.SetupGaiaRangeQuery(); @@ -203,7 +205,7 @@ this.StartTimer(); }; -BuildingAI.prototype.StartTimer = function() +BuildingAI.prototype.StartTimer = function(wasAttacking = false) { if (this.timer) return; @@ -215,8 +217,20 @@ var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); var attackTimers = cmpAttack.GetTimers(attackType); + // Rather than figure out the correct offset when resetting the timer, use an estimate of half of the repeat time this.timer = cmpTimer.SetInterval(this.entity, IID_BuildingAI, "FireArrows", - attackTimers.prepare, attackTimers.repeat / roundCount, null); + wasAttacking ? attackTimers.repeat / roundCount / 2 : attackTimers.prepare, + attackTimers.repeat / roundCount, null); +}; + +BuildingAI.prototype.StopTimer = function() +{ + if (!this.timer) + return; + + let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); + cmpTimer.CancelTimer(this.timer); + this.timer = undefined; }; BuildingAI.prototype.GetDefaultArrowCount = function() @@ -276,12 +290,7 @@ { if (!this.targetUnits.length && !this.unitAITarget) { - if (!this.timer) - return; - - let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); - cmpTimer.CancelTimer(this.timer); - this.timer = undefined; + this.StopTimer(); return; } 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 @@ -177,6 +177,10 @@ // ignore }, + "ValueModification": function(msg) { + // ignore + }, + // Formation handlers: "FormationLeave": function(msg) { @@ -1935,13 +1939,14 @@ // If the repeat time since the last attack hasn't elapsed, // delay this attack to avoid attacking too fast. - var prepare = this.attackTimers.prepare; + let prepare = this.attackTimers.prepare; + let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); if (this.lastAttacked) { - var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); - var repeatLeft = this.lastAttacked + this.attackTimers.repeat - cmpTimer.GetTime(); + let repeatLeft = this.lastAttacked + this.attackTimers.repeat - cmpTimer.GetTime(); prepare = Math.max(prepare, repeatLeft); } + this.lastAttacked = cmpTimer.GetTime() + prepare - this.attackTimers.repeat; this.oldAttackType = this.order.data.attackType; // add prefix + no capital first letter for attackType @@ -2074,6 +2079,27 @@ this.WalkToHeldPosition(); }, + "ValueModification": function(msg) { + // Restart the timer if the attack repeat time has changed + if (msg.data.valueNames.indexOf("Attack/" + this.oldAttackType + "/RepeatTime") != -1) + { + let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); + let cmpAttack = Engine.QueryInterface(this.entity, IID_Attack); + let attackTimers = cmpAttack.GetTimers(this.order.data.attackType); + + let oldRepeatLeft = this.lastAttacked + this.attackTimers.repeat - cmpTimer.GetTime(); + let scale = attackTimers.repeat / this.attackTimers.repeat; + let repeatLeft = scale * oldRepeatLeft; + + this.attackTimers = attackTimers; + this.lastAttacked = cmpTimer.GetTime() + repeatLeft - this.attackTimers.repeat; + + this.StopTimer(); + this.StartTimer(repeatLeft, this.attackTimers.repeat); + this.SetAnimationSync(repeatLeft, this.attackTimers.repeat); + } + }, + // TODO: respond to target deaths immediately, rather than waiting // until the next Timer event @@ -2567,13 +2593,14 @@ // If the repeat time since the last heal hasn't elapsed, // delay the action to avoid healing too fast. - var prepare = this.healTimers.prepare; + let prepare = this.healTimers.prepare; + let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); if (this.lastHealed) { - var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); - var repeatLeft = this.lastHealed + this.healTimers.repeat - cmpTimer.GetTime(); + let repeatLeft = this.lastHealed + this.healTimers.repeat - cmpTimer.GetTime(); prepare = Math.max(prepare, repeatLeft); } + this.lastHealed = cmpTimer.GetTime() + prepare - this.healTimers.repeat; this.SelectAnimation("heal", false, 1.0, "heal"); this.SetAnimationSync(prepare, this.healTimers.repeat); @@ -2633,7 +2660,29 @@ if (this.GetStance().respondHoldGround) this.WalkToHeldPosition(); }, + + "ValueModification": function(msg) { + // Restart the timer if the attack repeat time has changed + if (msg.data.valueNames.indexOf("Heal/Rate") != -1) + { + let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); + let cmpHeal = Engine.QueryInterface(this.entity, IID_Heal); + let healTimers = cmpHeal.GetTimers(); + + let oldRepeatLeft = this.lastHealed + this.healTimers.repeat - cmpTimer.GetTime(); + let scale = healTimers.repeat / this.healTimers.repeat; + let repeatLeft = scale * oldRepeatLeft; + + this.healTimers = healTimers; + this.lastHealed = cmpTimer.GetTime() + repeatLeft - this.healTimers.repeat; + + this.StopTimer(); + this.StartTimer(repeatLeft, this.healTimers.repeat); + this.SetAnimationSync(repeatLeft, this.healTimers.repeat); + } + }, }, + "CHASING": { "enter": function () { this.SelectAnimation("move"); @@ -4100,6 +4149,11 @@ this.UnitFsm.ProcessMessage(this, {"type": "PackFinished", "packed": msg.packed}); }; +UnitAI.prototype.OnValueModification = function(msg) +{ + this.UnitFsm.ProcessMessage(this, {"type": "ValueModification", "data": msg}); +}; + //// Helper functions to be called by the FSM //// UnitAI.prototype.GetWalkSpeed = function() Index: binaries/data/mods/public/simulation/components/tests/test_UnitAI.js =================================================================== --- binaries/data/mods/public/simulation/components/tests/test_UnitAI.js +++ binaries/data/mods/public/simulation/components/tests/test_UnitAI.js @@ -36,6 +36,7 @@ AddMock(SYSTEM_ENTITY, IID_Timer, { SetInterval: function() { }, SetTimeout: function() { }, + GetTime: function() { return 0; }, }); AddMock(SYSTEM_ENTITY, IID_RangeManager, { @@ -182,6 +183,7 @@ AddMock(SYSTEM_ENTITY, IID_Timer, { SetInterval: function() { }, SetTimeout: function() { }, + GetTime: function() { return 0; }, }); @@ -298,7 +300,7 @@ controllerAI.Attack(enemy, []); for (var ent of unitAIs) - TS_ASSERT_EQUALS(unitAI.fsmStateName, "INDIVIDUAL.COMBAT.ATTACKING"); + TS_ASSERT_EQUALS(ent.fsmStateName, "INDIVIDUAL.COMBAT.ATTACKING"); controllerAI.MoveIntoFormation({"name": "Circle"}); @@ -307,13 +309,78 @@ controllerFormation.SetInPosition(ent); for (var ent of unitAIs) - TS_ASSERT_EQUALS(unitAI.fsmStateName, "INDIVIDUAL.COMBAT.ATTACKING"); + TS_ASSERT_EQUALS(ent.fsmStateName, "INDIVIDUAL.COMBAT.ATTACKING"); controllerFormation.Disband(); } +function TestRepeatTime() +{ + ResetState(); + + let playerEnt = 10; + let unitEnt = 20; + let enemyEnt = 30; + + AddMock(SYSTEM_ENTITY, IID_Timer, { + SetInterval: function() { }, + SetTimeout: function() { }, + GetTime: function() { return 2000; }, + }); + + AddMock(SYSTEM_ENTITY, IID_PlayerManager, { + GetPlayerByID: function(id) { return playerEnt; }, + }); + + AddMock(playerEnt, IID_Player, { + IsEnemy: function() { return true; }, + GetEnemies: function() { return []; }, + }); + + AddMock(unitEnt, IID_Ownership, { + GetOwner: function() { return 1; }, + }); + + AddMock(unitEnt, IID_UnitMotion, { + IsInTargetRange: function(target, min, max) { return true; }, + StopMoving: function() { }, + }); + + AddMock(unitEnt, IID_Attack, { + CanAttack: function(v) { return true; }, + GetRange: function() { return { "max": 10, "min": 0}; }, + GetBestAttackAgainst: function(t) { return "Melee"; }, + GetTimers: function() { return { "prepare": 300, "repeat": 1000 }; }, + }); + + AddMock(enemyEnt, IID_Health, { + GetHitpoints: function() { return 40; }, + }); + + let cmpUnitAI = ConstructComponent(unitEnt, "UnitAI", { "DefaultStance": "aggressive", "FormationController": false }); + + cmpUnitAI.OnCreate(); + cmpUnitAI.Attack(enemyEnt, []); + + TS_ASSERT_EQUALS(cmpUnitAI.fsmStateName, "INDIVIDUAL.COMBAT.ATTACKING"); + TS_ASSERT_UNEVAL_EQUALS(cmpUnitAI.attackTimers, { "prepare": 300, "repeat": 1000 }); + TS_ASSERT_EQUALS(cmpUnitAI.lastAttacked, 2000 - 700); + + AddMock(unitEnt, IID_Attack, { + GetTimers: function() { return { "prepare": 300, "repeat": 800 }; }, + }); + + cmpUnitAI.OnValueModification({ "entities": [20], "component": "Attack", "valueNames": ["Attack/Melee/RepeatTime"] }); + + TS_ASSERT_EQUALS(cmpUnitAI.fsmStateName, "INDIVIDUAL.COMBAT.ATTACKING"); + TS_ASSERT_UNEVAL_EQUALS(cmpUnitAI.attackTimers, { "prepare": 300, "repeat": 800 }); + TS_ASSERT_EQUALS(cmpUnitAI.lastAttacked, 2000 - 700 * 800 / 1000); +} + TestFormationExiting(0); TestFormationExiting(1); TestFormationExiting(2); TestMoveIntoFormationWhileAttacking(); + +TestRepeatTime();