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 @@ -402,6 +402,8 @@ } this.order.data.attackType = type; + this.RememberTargetPosition(); + // If we are already at the target, try attacking it from here if (this.CheckTargetAttackRange(this.order.data.target, this.order.data.attackType)) { @@ -526,6 +528,8 @@ return; } + this.RememberTargetPosition(); + if (this.CheckTargetRange(this.order.data.target, IID_ResourceGatherer)) this.SetNextState("INDIVIDUAL.GATHER.GATHERING"); else @@ -1766,11 +1770,33 @@ if (this.GetStance().respondHoldGround) this.WalkToHeldPosition(); } + else + this.RememberTargetPosition(); }, - "MovementUpdate": function() { + "MovementUpdate": function(msg) { + if (msg.error) + { + if (this.orderQueue.length > 1) + { + this.FinishOrder(); + return; + } + else if (!this.order.data.force) + { + this.SetNextState("COMBAT.FINDINGNEWTARGET"); + return; + } + // Go to the last known position and try to find enemies there. + let lastPos = this.order.data.lastPos; + this.PushOrder("Walk", { "x": lastPos.x, "z": lastPos.z, "force": false }); + this.PushOrder("WalkAndFight", { "x": lastPos.x, "z": lastPos.z, "force": false }); + return; + } + if (!this.CheckTargetAttackRange(this.order.data.target, this.order.data.attackType)) return; + // If the unit needs to unpack, do so if (this.CanUnpack()) { @@ -1783,8 +1809,8 @@ "ATTACKING": { "enter": function() { - var target = this.order.data.target; - var cmpFormation = Engine.QueryInterface(target, IID_Formation); + let target = this.order.data.target; + let cmpFormation = Engine.QueryInterface(target, IID_Formation); // if the target is a formation, save the attacking formation, and pick a member if (cmpFormation) { @@ -1794,7 +1820,6 @@ } // Check the target is still alive and attackable if (this.CanAttack(target) && !this.CheckTargetAttackRange(target, this.order.data.attackType)) - { // Can't reach it - try to chase after it if (this.ShouldChaseTargetedEntity(target, this.order.data.force)) { @@ -1807,50 +1832,54 @@ this.SetNextState("COMBAT.CHASING"); return true; } - } this.StopMoving(); - var cmpAttack = Engine.QueryInterface(this.entity, IID_Attack); + 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. - var prepare = this.attackTimers.prepare; + let prepare = this.attackTimers.prepare; if (this.lastAttacked) { - var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); - var repeatLeft = this.lastAttacked + this.attackTimers.repeat - cmpTimer.GetTime(); + let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); + let repeatLeft = this.lastAttacked + this.attackTimers.repeat - cmpTimer.GetTime(); prepare = Math.max(prepare, repeatLeft); } this.oldAttackType = this.order.data.attackType; // add prefix + no capital first letter for attackType - var animationName = "attack_" + this.order.data.attackType.toLowerCase(); + let animationName = "attack_" + this.order.data.attackType.toLowerCase(); if (this.IsFormationMember()) { - var cmpFormation = Engine.QueryInterface(this.formationController, IID_Formation); - if (cmpFormation) - animationName = cmpFormation.GetFormationAnimation(this.entity, animationName); + let cmpFormationController = Engine.QueryInterface(this.formationController, IID_Formation); + if (cmpFormationController) + animationName = cmpFormationController.GetFormationAnimation(this.entity, animationName); } + this.SetAnimationVariant("combat"); - this.SelectAnimation(animationName); - this.SetAnimationSync(prepare, this.attackTimers.repeat); this.StartTimer(prepare, this.attackTimers.repeat); - // TODO: we should probably only bother syncing projectile attacks, not melee - // If using a non-default prepare time, re-sync the animation when the timer runs. - this.resyncAnimation = (prepare != this.attackTimers.prepare) ? true : false; - - this.FaceTowardsTarget(this.order.data.target); - - var cmpBuildingAI = Engine.QueryInterface(this.entity, IID_BuildingAI); + let cmpBuildingAI = Engine.QueryInterface(this.entity, IID_BuildingAI); if (cmpBuildingAI) cmpBuildingAI.SetUnitAITarget(this.order.data.target); + + if (this.CheckTargetAttackRange(target, this.order.data.attackType)) + { + this.SelectAnimation(animationName); + this.SetAnimationSync(prepare, this.attackTimers.repeat); + // TODO: we should probably only bother syncing projectile attacks, not melee + + // If using a non-default prepare time, re-sync the animation when the timer runs. + this.resyncAnimation = prepare != this.attackTimers.prepare; + + this.FaceTowardsTarget(this.order.data.target); + } }, "leave": function() { - var cmpBuildingAI = Engine.QueryInterface(this.entity, IID_BuildingAI); + let cmpBuildingAI = Engine.QueryInterface(this.entity, IID_BuildingAI); if (cmpBuildingAI) cmpBuildingAI.SetUnitAITarget(0); this.StopTimer(); @@ -1858,78 +1887,84 @@ }, "Timer": function(msg) { - var target = this.order.data.target; - var cmpFormation = Engine.QueryInterface(target, IID_Formation); + let target = this.order.data.target; + let cmpFormation = Engine.QueryInterface(target, IID_Formation); + // if the target is a formation, save the attacking formation, and pick a member if (cmpFormation) { - var thisObject = this; - var filter = function(t) { + let thisObject = this; + let filter = function(t) { return thisObject.CanAttack(t); }; this.order.data.formationTarget = target; target = cmpFormation.GetClosestMember(this.entity, filter); this.order.data.target = target; } + // Check the target is still alive and attackable - if (this.CanAttack(target)) + if (!this.CanAttack(target)) { - // If we are hunting, first update the target position of the gather order so we know where will be the killed animal - if (this.order.data.hunting && this.orderQueue[1] && this.orderQueue[1].data.lastPos) - { - var cmpPosition = Engine.QueryInterface(this.order.data.target, IID_Position); - if (cmpPosition && cmpPosition.IsInWorld()) - { - // Store the initial position, so that we can find the rest of the herd later - if (!this.orderQueue[1].data.initPos) - this.orderQueue[1].data.initPos = this.orderQueue[1].data.lastPos; - this.orderQueue[1].data.lastPos = cmpPosition.GetPosition(); - // We still know where the animal is, so we shouldn't give up before going there - this.orderQueue[1].data.secondTry = undefined; - } - } + this.SetNextState("COMBAT.FINDINGNEWTARGET"); + return; + } - var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); - this.lastAttacked = cmpTimer.GetTime() - msg.lateness; + this.RememberTargetPosition(); - this.FaceTowardsTarget(target); + let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); + this.lastAttacked = cmpTimer.GetTime() - msg.lateness; - // BuildingAI has it's own attack-routine - var cmpBuildingAI = Engine.QueryInterface(this.entity, IID_BuildingAI); - if (!cmpBuildingAI) - { - let cmpAttack = Engine.QueryInterface(this.entity, IID_Attack); - cmpAttack.PerformAttack(this.order.data.attackType, target); - } + 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(this.order.data.attackType, target); + } - // Check we can still reach the target for the next attack - if (this.CheckTargetAttackRange(target, this.order.data.attackType)) + // Check we can still reach the target for the next attack + if (this.CheckTargetAttackRange(target, this.order.data.attackType)) + { + if (this.resyncAnimation) { - if (this.resyncAnimation) - { - this.SetAnimationSync(this.attackTimers.repeat, this.attackTimers.repeat); - this.resyncAnimation = false; - } - return; + this.SetAnimationSync(this.attackTimers.repeat, this.attackTimers.repeat); + this.resyncAnimation = false; } + return; + } - // Can't reach it - try to chase after it - if (this.ShouldChaseTargetedEntity(target, this.order.data.force)) + // Can't reach it - try to chase after it + if (this.ShouldChaseTargetedEntity(target, this.order.data.force)) + { + if (this.CanPack()) { - if (this.CanPack()) - { - this.PushOrderFront("Pack", { "force": true }); - return; - } - this.SetNextState("COMBAT.CHASING"); + this.PushOrderFront("Pack", { "force": true }); return; } + this.SetNextState("COMBAT.CHASING"); + return; } + }, + + // TODO: respond to target deaths immediately, rather than waiting + // until the next Timer event + + "Attacked": function(msg) { + // If we are capturing and are attacked by something that we would not capture, attack that entity instead + if (this.order.data.attackType == "Capture" && (this.GetStance().targetAttackersAlways || !this.order.data.force) + && this.order.data.target != msg.data.attacker && this.GetBestAttackAgainst(msg.data.attacker, true) != "Capture") + this.RespondToTargetedEntities([msg.data.attacker]); + }, + }, - // if we're targetting a formation, find a new member of that formation - var cmpTargetFormation = Engine.QueryInterface(this.order.data.formationTarget || INVALID_ENTITY, IID_Formation); + "FINDINGNEWTARGET": { + "enter": function() { + // If we're targetting a formation, find a new member of that formation. + let cmpTargetFormation = Engine.QueryInterface(this.order.data.formationTarget || INVALID_ENTITY, IID_Formation); // if there is no target, it means previously searching for the target inside the target formation failed, so don't repeat the search - if (target && cmpTargetFormation) + if (this.order.data.target && cmpTargetFormation) { this.order.data.target = this.order.data.formationTarget; this.TimerHandler(msg.data, msg.lateness); @@ -1937,7 +1972,7 @@ } // Can't reach it, no longer owned by enemy, or it doesn't exist any more - give up - // Except if in WalkAndFight mode where we look for more ennemies around before moving again + // except if in WalkAndFight mode where we look for more enemies around before moving again. if (this.FinishOrder()) { if (this.IsWalkingAndFighting()) @@ -1947,29 +1982,12 @@ // See if we can switch to a new nearby enemy if (this.FindNewTargets()) - { - // Attempt to immediately re-enter the timer function, to avoid wasting the attack. - // Packable units may have switched to PACKING state, thus canceling the timer and having order.data.attackType undefined. - if (this.orderQueue.length > 0 && this.orderQueue[0].data && this.orderQueue[0].data.attackType && - this.orderQueue[0].data.attackType == this.oldAttackType) - this.TimerHandler(msg.data, msg.lateness); return; - } // Return to our original position if (this.GetStance().respondHoldGround) this.WalkToHeldPosition(); }, - - // TODO: respond to target deaths immediately, rather than waiting - // until the next Timer event - - "Attacked": function(msg) { - // If we are capturing and are attacked by something that we would not capture, attack that entity instead - if (this.order.data.attackType == "Capture" && (this.GetStance().targetAttackersAlways || !this.order.data.force) - && this.order.data.target != msg.data.attacker && this.GetBestAttackAgainst(msg.data.attacker, true) != "Capture") - this.RespondToTargetedEntities([msg.data.attacker]); - }, }, "CHASING": { @@ -4918,7 +4936,16 @@ this.WalkToTarget(target, queued); return; } - this.AddOrder("Attack", { "target": target, "force": true, "allowCapture": allowCapture}, queued); + + let order = { + "target": target, + "force": true, + "allowCapture": allowCapture, + }; + + this.RememberTargetPosition(order); + + this.AddOrder("Attack", order, queued); }; /** @@ -4993,14 +5020,16 @@ if (template.indexOf("resource|") != -1) template = template.slice(9); - // Remember the position of our target, if any, in case it disappears - // later and we want to head to its last known position - var lastPos = undefined; - var cmpPosition = Engine.QueryInterface(target, IID_Position); - if (cmpPosition && cmpPosition.IsInWorld()) - lastPos = cmpPosition.GetPosition(); + let order = { + "target": target, + "type": type, + "template": template, + "force": force, + }; - this.AddOrder("Gather", { "target": target, "type": type, "template": template, "lastPos": lastPos, "force": force }, queued); + this.RememberTargetPosition(order); + + this.AddOrder("Gather", order, queued); }; /** @@ -5518,6 +5547,20 @@ cmpUnitMotion.SetSpeedMultiplier(speed); }; +/* + * Remember the position of the target (in lastPos), if any, in case it disappears later + * and we want to head to its last known position. + * @param orderData - The order data to set this on. Defaults to this.order.data + */ +UnitAI.prototype.RememberTargetPosition = function(orderData) +{ + if (!orderData) + orderData = this.order.data; + let cmpPosition = Engine.QueryInterface(orderData.target, IID_Position); + if (cmpPosition && cmpPosition.IsInWorld()) + orderData.lastPos = cmpPosition.GetPosition(); +}; + UnitAI.prototype.SetHeldPosition = function(x, z) { this.heldPosition = {"x": x, "z": z};