Index: binaries/data/mods/public/simulation/components/Attack.js =================================================================== --- binaries/data/mods/public/simulation/components/Attack.js +++ binaries/data/mods/public/simulation/components/Attack.js @@ -244,8 +244,6 @@ { }; -Attack.prototype.Serialize = null; // we have no dynamic state to save - Attack.prototype.GetAttackTypes = function(wantedTypes) { let types = g_AttackTypes.filter(type => !!this.template[type]); @@ -496,6 +494,124 @@ }; /** + * @param {number} target - The target to attack. + * @param {string} type - The type of attack to use. + * @param {number} callerIID - The IID to notify on specific events. + * + * @return {boolean} - Whether we started attacking. + */ +Attack.prototype.StartAttacking = function(target, type, callerIID) +{ + if (this.target) + this.StopAttacking(); + + if (!this.CanAttack(target)) + return false; + + let timings = this.GetTimers(type); + let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); + + // If the repeat time since the last attack hasn't elapsed, + // delay the action to avoid attacking too fast. + let prepare = timings.prepare; + if (this.lastAttacked) + { + let repeatLeft = this.lastAttacked + timings.repeat - cmpTimer.GetTime(); + prepare = Math.max(prepare, repeatLeft); + } + + let cmpVisual = Engine.QueryInterface(this.entity, IID_Visual); + if (cmpVisual) + { + cmpVisual.SelectAnimation("attack_" + type.toLowerCase(), 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.callerIID = callerIID; + this.timer = cmpTimer.SetInterval(this.entity, IID_Attack, "Attack", prepare, timings.repeat, type); + + return true; +}; + +/** + * @param {string} reason - The reason why we stopped attacking. + */ +Attack.prototype.StopAttacking = function(reason) +{ + if (!this.target) + return; + + let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); + cmpTimer.CancelTimer(this.timer); + delete this.timer; + + delete this.target; + + let cmpVisual = Engine.QueryInterface(this.entity, IID_Visual); + if (cmpVisual) + cmpVisual.SelectAnimation("idle", false, 1.0); + + // The callerIID component may start again, + // replacing the callerIID, hence save that. + let callerIID = this.callerIID; + delete this.callerIID; + + if (reason && callerIID) + { + let component = Engine.QueryInterface(this.entity, callerIID); + if (component) + component.ProcessMessage(reason, null); + } +}; + +/** + * Attack our target entity. + * @param {string} data - The attack type to use. + * @param {number} lateness - The offset of the actual call and when it was expected. + */ +Attack.prototype.Attack = function(type, lateness) +{ + if (!this.CanAttack(this.target)) + { + this.StopAttacking("TargetInvalidated"); + return; + } + + // ToDo: Enable entities to keep facing a target. + Engine.QueryInterface(this.entity, IID_UnitAI)?.FaceTowardsTarget(this.target); + + let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); + this.lastAttacked = cmpTimer.GetTime() - lateness; + + // BuildingAI has its own attack routine. + if (!Engine.QueryInterface(this.entity, IID_BuildingAI)) + this.PerformAttack(type, this.target); + + // We check the range after the attack to facilitate chasing. + if (!this.target || !this.IsTargetInRange(this.target, type)) + { + this.StopAttacking("OutOfRange"); + return; + } + + if (this.resyncAnimation) + { + let cmpVisual = Engine.QueryInterface(this.entity, IID_Visual); + if (cmpVisual) + { + let repeat = this.GetTimers(type).repeat; + cmpVisual.SetAnimationSyncRepeat(repeat); + cmpVisual.SetAnimationSyncOffset(repeat); + } + delete this.resyncAnimation; + } +}; + +/** * Attack 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 PerformAttack. @@ -634,6 +750,36 @@ Attacking.HandleAttackEffects(target, data); }; +/** + * @param {number} - The entity ID of the target to check. + * @return {boolean} - Whether this entity is in range of its target. + */ +Attack.prototype.IsTargetInRange = function(target, type) +{ + let range = this.GetRange(type); + if (type == "Ranged") + { + let cmpPositionTarget = Engine.QueryInterface(target, IID_Position); + if (!cmpPositionTarget || !cmpPositionTarget.IsInWorld()) + return false; + + let cmpPositionSelf = Engine.QueryInterface(this.entity, IID_Position); + if (!cmpPositionSelf || !cmpPositionSelf.IsInWorld()) + return false; + + let positionSelf = cmpPositionSelf.GetPosition(); + let positionTarget = cmpPositionTarget.GetPosition(); + + let heightDifference = positionSelf.y + range.elevationBonus - positionTarget.y; + range.max = Math.sqrt(Math.square(range.max) + 2 * range.max * heightDifference); + + if (range.max < 0) + return false; + } + let cmpObstructionManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ObstructionManager); + return cmpObstructionManager.IsInTargetRange(this.entity, target, range.min, range.max, false); +}; + Attack.prototype.OnValueModification = function(msg) { if (msg.component != "Attack") Index: binaries/data/mods/public/simulation/components/Builder.js =================================================================== --- binaries/data/mods/public/simulation/components/Builder.js +++ binaries/data/mods/public/simulation/components/Builder.js @@ -117,27 +117,24 @@ */ Builder.prototype.StopRepairing = function(reason) { - if (this.timer) - { - let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); - cmpTimer.CancelTimer(this.timer); - delete this.timer; - } + if (!this.target) + return; - if (this.target) - { - let cmpBuilderList = QueryBuilderListInterface(this.target); - if (cmpBuilderList) - cmpBuilderList.RemoveBuilder(this.entity); + let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); + cmpTimer.CancelTimer(this.timer); + delete this.timer; - delete 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, + // The callerIID component may start again, // replacing the callerIID, hence save that. let callerIID = this.callerIID; delete this.callerIID; 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 @@ -166,25 +166,24 @@ }; /** - * @param {string} reason - The reason why we stopped healing. Currently implemented are: - * "outOfRange", "targetInvalidated". + * @param {string} reason - The reason why we stopped healing. */ Heal.prototype.StopHealing = function(reason) { - if (this.timer) - { - let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); - cmpTimer.CancelTimer(this.timer); - delete this.timer; - } + if (!this.target) + return; + + let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); + cmpTimer.CancelTimer(this.timer); + delete this.timer; + + delete this.target; let cmpVisual = Engine.QueryInterface(this.entity, IID_Visual); if (cmpVisual) cmpVisual.SelectAnimation("idle", false, 1.0); - delete this.target; - - // The callerIID component may start healing again, + // The callerIID component may start again, // replacing the callerIID, hence save that. let callerIID = this.callerIID; delete this.callerIID; @@ -199,7 +198,8 @@ /** * Heal our target entity. - * @params - data and lateness are unused. + * @param data - Unused. + * @param {number} lateness - The offset of the actual call and when it was expected. */ Heal.prototype.PerformHeal = function(data, lateness) { Index: binaries/data/mods/public/simulation/components/ResourceGatherer.js =================================================================== --- binaries/data/mods/public/simulation/components/ResourceGatherer.js +++ binaries/data/mods/public/simulation/components/ResourceGatherer.js @@ -217,27 +217,25 @@ */ ResourceGatherer.prototype.StopGathering = function(reason) { - if (this.timer) - { - let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); - cmpTimer.CancelTimer(this.timer); - delete this.timer; - } + if (!this.target) + return; - if (this.target) - { - let cmpResourceSupply = Engine.QueryInterface(this.target, IID_ResourceSupply); - if (cmpResourceSupply) - cmpResourceSupply.RemoveGatherer(this.entity); - this.RemoveFromPlayerCounter(); - delete this.target; - } + let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); + cmpTimer.CancelTimer(this.timer); + delete this.timer; + + let cmpResourceSupply = Engine.QueryInterface(this.target, IID_ResourceSupply); + if (cmpResourceSupply) + cmpResourceSupply.RemoveGatherer(this.entity); + this.RemoveFromPlayerCounter(); + + delete this.target; let cmpVisual = Engine.QueryInterface(this.entity, IID_Visual); if (cmpVisual) cmpVisual.SelectAnimation("idle", false, 1.0); - // The callerIID component may start gathering again, + // The callerIID component may start again, // replacing the callerIID, hence save that. let callerIID = this.callerIID; delete this.callerIID; Index: binaries/data/mods/public/simulation/components/TreasureCollecter.js =================================================================== --- binaries/data/mods/public/simulation/components/TreasureCollecter.js +++ binaries/data/mods/public/simulation/components/TreasureCollecter.js @@ -65,19 +65,20 @@ */ TreasureCollecter.prototype.StopCollecting = function(reason) { - if (this.timer) - { - let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); - cmpTimer.CancelTimer(this.timer); - delete this.timer; - } + if (!this.target) + return; + + let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); + cmpTimer.CancelTimer(this.timer); + delete this.timer; + delete this.target; let cmpVisual = Engine.QueryInterface(this.entity, IID_Visual); if (cmpVisual) cmpVisual.SelectAnimation("idle", false, 1.0); - // The callerIID component may start collecting again, + // The callerIID component may start again, // replacing the callerIID, hence save that. let callerIID = this.callerIID; delete this.callerIID; 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 @@ -2089,9 +2089,11 @@ } this.shouldCheer = false; - if (!this.CanAttack(target)) + + let cmpAttack = Engine.QueryInterface(this.entity, IID_Attack); + if (!cmpAttack) { - this.SetNextState("COMBAT.FINDINGNEWTARGET"); + this.FinishOrder(); return true; } @@ -2103,36 +2105,24 @@ return true; } - this.SetNextState("COMBAT.APPROACHING"); + this.ProcessMessage("OutOfRange"); return true; } - let cmpAttack = Engine.QueryInterface(this.entity, IID_Attack); - this.attackTimers = cmpAttack.GetTimers(this.order.data.attackType); - - // If the repeat time since the last attack hasn't elapsed, - // delay this attack to avoid attacking too fast. - let prepare = this.attackTimers.prepare; - if (this.lastAttacked) - { - let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); - let repeatLeft = this.lastAttacked + this.attackTimers.repeat - cmpTimer.GetTime(); - prepare = Math.max(prepare, repeatLeft); - } - if (!this.formationAnimationVariant) this.SetAnimationVariant("combat"); - this.oldAttackType = this.order.data.attackType; - this.SelectAnimation("attack_" + this.order.data.attackType.toLowerCase()); - this.SetAnimationSync(prepare, this.attackTimers.repeat); - this.StartTimer(prepare, this.attackTimers.repeat); - // TODO: we should probably only bother syncing projectile attacks, not melee + this.FaceTowardsTarget(this.order.data.target); - // If using a non-default prepare time, re-sync the animation when the timer runs. - this.resyncAnimation = prepare != this.attackTimers.prepare; + this.RememberTargetPosition(); + if (this.order.data.hunting && this.orderQueue.length > 1 && this.orderQueue[1].type === "Gather") + this.RememberTargetPosition(this.orderQueue[1].data); - this.FaceTowardsTarget(this.order.data.target); + if (!cmpAttack.StartAttacking(this.order.data.target, this.order.data.attackType, IID_UnitAI)) + { + this.ProcessMessage("TargetInvalidated"); + return true; + } let cmpBuildingAI = Engine.QueryInterface(this.entity, IID_BuildingAI); if (cmpBuildingAI) @@ -2153,65 +2143,26 @@ let cmpBuildingAI = Engine.QueryInterface(this.entity, IID_BuildingAI); if (cmpBuildingAI) cmpBuildingAI.SetUnitAITarget(0); - this.StopTimer(); - this.ResetAnimation(); + let cmpAttack = Engine.QueryInterface(this.entity, IID_Attack); + if (cmpAttack) + cmpAttack.StopAttacking(); }, - "Timer": function(msg) { - let target = this.order.data.target; - let attackType = this.order.data.attackType; - - if (!this.CanAttack(target)) - { - this.SetNextState("COMBAT.FINDINGNEWTARGET"); - return; - } - - this.RememberTargetPosition(); - if (this.order.data.hunting && this.orderQueue.length > 1 && this.orderQueue[1].type === "Gather") - this.RememberTargetPosition(this.orderQueue[1].data); - - let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); - this.lastAttacked = cmpTimer.GetTime() - msg.lateness; - - this.FaceTowardsTarget(target); - - // BuildingAI has it's own attack-routine - let cmpBuildingAI = Engine.QueryInterface(this.entity, IID_BuildingAI); - if (!cmpBuildingAI) - { - let cmpAttack = Engine.QueryInterface(this.entity, IID_Attack); - cmpAttack.PerformAttack(attackType, target); - } - - // PerformAttack might have triggered messages that moved us to another state. - // (use 'ends with' to handle formation members copying our state). - if (!this.GetCurrentState().endsWith("COMBAT.ATTACKING")) - return; - - - // Check we can still reach the target for the next attack - if (this.CheckTargetAttackRange(target, attackType)) - { - if (this.resyncAnimation) - { - this.SetAnimationSync(this.attackTimers.repeat, this.attackTimers.repeat); - this.resyncAnimation = false; - } - return; - } - - if (this.ShouldChaseTargetedEntity(target, this.order.data.force)) + "OutOfRange": function() { + if (this.ShouldChaseTargetedEntity(this.order.data.target, this.order.data.force)) { if (this.CanPack()) { this.PushOrderFront("Pack", { "force": true }); return; } - this.SetNextState("COMBAT.CHASING"); + this.SetNextState("CHASING"); return; } + this.SetNextState("FINDINGNEWTARGET"); + }, + "TargetInvalidated": function() { this.SetNextState("FINDINGNEWTARGET"); }, @@ -3399,9 +3350,6 @@ this.isGuardOf = undefined; - // For preventing increased action rate due to Stop orders or target death. - this.lastAttacked = undefined; - this.formationAnimationVariant = undefined; this.cheeringTime = +(this.template.CheeringTime || 0); this.SetStance(this.template.DefaultStance); @@ -4789,33 +4737,8 @@ if (cmpFormation) target = cmpFormation.GetClosestMember(this.entity); - if (type != "Ranged") - return this.CheckTargetRange(target, IID_Attack, type); - - let targetCmpPosition = Engine.QueryInterface(target, IID_Position); - if (!targetCmpPosition || !targetCmpPosition.IsInWorld()) - return false; - - let range = this.GetRange(IID_Attack, type, target); - if (!range) - return false; - - let thisCmpPosition = Engine.QueryInterface(this.entity, IID_Position); - if (!thisCmpPosition.IsInWorld()) - return false; - - let s = thisCmpPosition.GetPosition(); - - let t = targetCmpPosition.GetPosition(); - - let h = s.y - t.y + range.elevationBonus; - let maxRange = Math.sqrt(Math.square(range.max) + 2 * range.max * h); - - if (maxRange < 0) - return false; - - let cmpObstructionManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ObstructionManager); - return cmpObstructionManager.IsInTargetRange(this.entity, target, range.min, maxRange, false); + let cmpAttack = Engine.QueryInterface(this.entity, IID_Attack); + return cmpAttack && cmpAttack.IsTargetInRange(target, type); }; UnitAI.prototype.CheckTargetRangeExplicit = function(target, min, max) 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 @@ -149,11 +149,6 @@ "GetEnemies": function() { return [2]; }, }); - AddMock(SYSTEM_ENTITY, IID_ObstructionManager, { - "IsInTargetRange": () => true, - "IsInPointRange": () => true - }); - var unitAI = ConstructComponent(unit, "UnitAI", { "FormationController": "false", "DefaultStance": "aggressive" }); AddMock(unit, IID_Identity, { @@ -194,6 +189,8 @@ "GetTimers": function() { return { "prepare": 500, "repeat": 1000 }; }, "CanAttack": function(v) { return true; }, "CompareEntitiesByPreference": function(a, b) { return 0; }, + "IsTargetInRange": () => true, + "StartAttacking": () => true }); unitAI.OnCreate(); @@ -370,6 +367,9 @@ "GetTimers": function() { return { "prepare": 500, "repeat": 1000 }; }, "CanAttack": function(v) { return true; }, "CompareEntitiesByPreference": function(a, b) { return 0; }, + "IsTargetInRange": () => true, + "StartAttacking": () => true, + "StopAttacking": () => {} }); unitAI.OnCreate();