1
0
forked from 0ad/0ad

UnitMotion - Explicit handling of movement errors (such as target death) in Combat.Approaching

In A23, units that are approaching an enemy (Combat.Approaching) will
carry on moving to their last waypoint if the target dies, and then go
idle.
As of 69d3e76fd2, this no longer happens and units stop in their tracks,
staying in attacking forever.

This lets Combat.Approaching explicitly handle the error, deciding what
is best to do:
- go to the next order (for hunting and queued orders)
- find new targets nearby (in case the attack wasn't a forced order)
- walk to the target's last known position and find units to attack
there (in case it was). This is A23 behaviour.

Differential Revision: https://code.wildfiregames.com/D1992
This was SVN commit r22425.
This commit is contained in:
wraitii 2019-07-02 06:49:27 +00:00
parent ff9d162c8c
commit 5568bd4c16

View File

@ -402,6 +402,8 @@ UnitAI.prototype.UnitFsmSpec = {
}
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 @@ UnitAI.prototype.UnitFsmSpec = {
return;
}
this.RememberTargetPosition();
if (this.CheckTargetRange(this.order.data.target, IID_ResourceGatherer))
this.SetNextState("INDIVIDUAL.GATHER.GATHERING");
else
@ -1766,11 +1770,34 @@ UnitAI.prototype.UnitFsmSpec = {
if (this.GetStance().respondHoldGround)
this.WalkToHeldPosition();
}
else
this.RememberTargetPosition();
},
"MovementUpdate": function() {
"MovementUpdate": function(msg) {
if (msg.error)
{
// This also handles hunting.
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 +1810,8 @@ UnitAI.prototype.UnitFsmSpec = {
"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)
{
@ -1792,65 +1819,69 @@ UnitAI.prototype.UnitFsmSpec = {
target = cmpFormation.GetClosestMember(this.entity);
this.order.data.target = target;
}
// 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))
{
if (this.CanPack())
{
this.PushOrderFront("Pack", { "force": true });
return;
}
this.SetNextState("COMBAT.CHASING");
if (!this.CanAttack(target))
{
this.SetNextState("COMBAT.FINDINGNEWTARGET");
return true;
}
if (!this.CheckTargetAttackRange(target, this.order.data.attackType))
{
if (this.CanPack())
{
this.PushOrderFront("Pack", { "force": true });
return true;
}
this.SetNextState("COMBAT.APPROACHING");
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.resyncAnimation = prepare != this.attackTimers.prepare;
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);
},
"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,107 +1889,65 @@ UnitAI.prototype.UnitFsmSpec = {
},
"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)
this.SetNextState("COMBAT.FINDINGNEWTARGET");
return;
}
this.RememberTargetPosition();
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(this.order.data.attackType, target);
}
// Check we can still reach the target for the next attack
if (this.CheckTargetAttackRange(target, this.order.data.attackType))
{
if (this.resyncAnimation)
{
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.SetAnimationSync(this.attackTimers.repeat, this.attackTimers.repeat);
this.resyncAnimation = false;
}
return;
}
var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
this.lastAttacked = cmpTimer.GetTime() - msg.lateness;
this.FaceTowardsTarget(target);
// BuildingAI has it's own attack-routine
var cmpBuildingAI = Engine.QueryInterface(this.entity, IID_BuildingAI);
if (!cmpBuildingAI)
// Can't reach it - try to chase after it
if (this.ShouldChaseTargetedEntity(target, this.order.data.force))
{
if (this.CanPack())
{
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))
{
if (this.resyncAnimation)
{
this.SetAnimationSync(this.attackTimers.repeat, this.attackTimers.repeat);
this.resyncAnimation = false;
}
this.PushOrderFront("Pack", { "force": true });
return;
}
// Can't reach it - try to chase after it
if (this.ShouldChaseTargetedEntity(target, this.order.data.force))
{
if (this.CanPack())
{
this.PushOrderFront("Pack", { "force": true });
return;
}
this.SetNextState("COMBAT.CHASING");
return;
}
}
// 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);
// 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)
{
this.order.data.target = this.order.data.formationTarget;
this.TimerHandler(msg.data, msg.lateness);
this.SetNextState("COMBAT.CHASING");
return;
}
// 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
if (this.FinishOrder())
{
if (this.IsWalkingAndFighting())
this.FindWalkAndFightTargets();
return;
}
// 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
@ -1972,6 +1961,39 @@ UnitAI.prototype.UnitFsmSpec = {
},
},
"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 (this.order.data.target && cmpTargetFormation)
{
this.order.data.target = this.order.data.formationTarget;
this.TimerHandler(msg.data, msg.lateness);
return;
}
// 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 enemies around before moving again.
if (this.FinishOrder())
{
if (this.IsWalkingAndFighting())
this.FindWalkAndFightTargets();
return true;
}
// See if we can switch to a new nearby enemy
if (this.FindNewTargets())
return true;
// Return to our original position
if (this.GetStance().respondHoldGround)
this.WalkToHeldPosition();
return true;
},
},
"CHASING": {
"enter": function() {
if (!this.MoveToTargetAttackRange(this.order.data.target, this.order.data.attackType))
@ -4917,7 +4939,16 @@ UnitAI.prototype.Attack = function(target, allowCapture = true, queued = false)
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);
};
/**
@ -4992,14 +5023,16 @@ UnitAI.prototype.PerformGather = function(target, queued, force)
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);
};
/**
@ -5517,6 +5550,20 @@ UnitAI.prototype.SetSpeedMultiplier = function(speed)
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};