Formation order queueing. Fixes #592, #593 and #1716.

Allow changing the formation, while under attack. Fixes #1624. Patch by
Simikolon.

This was SVN commit r12911.
This commit is contained in:
leper 2012-12-02 01:52:27 +00:00
parent ddd8c36d6f
commit 5800be1334
4 changed files with 421 additions and 67 deletions

View File

@ -122,7 +122,7 @@ Formation.prototype.RemoveMembers = function(ents)
this.ComputeMotionParameters();
// Rearrange the remaining members
this.MoveMembersIntoFormation(true);
this.MoveMembersIntoFormation(true, true);
};
/**
@ -173,12 +173,29 @@ Formation.prototype.CallMemberFunction = function(funcname, args)
}
};
/**
* Call obj.functname(args) on UnitAI components of all members,
* and return true if all calls return true.
*/
Formation.prototype.TestAllMemberFunction = function(funcname, args)
{
for each (var ent in this.members)
{
var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
if (!cmpUnitAI[funcname].apply(cmpUnitAI, args))
return false;
}
return true;
};
/**
* Set all members to form up into the formation shape.
* If moveCenter is true, the formation center will be reinitialised
* to the center of the units.
* If force is true, all individual orders of the formation units are replaced,
* otherwise the order to walk into formation is just pushed to the front.
*/
Formation.prototype.MoveMembersIntoFormation = function(moveCenter)
Formation.prototype.MoveMembersIntoFormation = function(moveCenter, force)
{
var active = [];
var positions = [];
@ -213,11 +230,23 @@ Formation.prototype.MoveMembersIntoFormation = function(moveCenter)
var offset = offsets[i];
var cmpUnitAI = Engine.QueryInterface(offset.ent, IID_UnitAI);
cmpUnitAI.ReplaceOrder("FormationWalk", {
"target": this.entity,
"x": offset.x - avgoffset.x,
"z": offset.z - avgoffset.z
});
if (force)
{
cmpUnitAI.ReplaceOrder("FormationWalk", {
"target": this.entity,
"x": offset.x - avgoffset.x,
"z": offset.z - avgoffset.z
});
}
else
{
cmpUnitAI.PushOrderFront("FormationWalk", {
"target": this.entity,
"x": offset.x - avgoffset.x,
"z": offset.z - avgoffset.z
});
}
}
};
@ -651,7 +680,7 @@ Formation.prototype.OnUpdate_Final = function(msg)
var walkingDistance = cmpUnitAI.ComputeWalkingDistance();
var columnar = (walkingDistance > g_ColumnDistanceThreshold);
if (columnar != this.columnar)
this.MoveMembersIntoFormation(false);
this.MoveMembersIntoFormation(false, true);
// (disable moveCenter so we can't get stuck in a loop of switching
// shape causing center to change causing shape to switch back)
};

View File

@ -602,6 +602,14 @@ var UnitFsmSpec = {
this.MoveToPoint(this.order.data.x, this.order.data.z);
this.SetNextState("WALKING");
},
"Order.MoveIntoFormation": function(msg) {
var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation);
cmpFormation.CallMemberFunction("SetHeldPosition", [msg.data.x, msg.data.z]);
this.MoveToPoint(this.order.data.x, this.order.data.z);
this.SetNextState("FORMING");
},
// Only used by other orders to walk there in formation
"Order.WalkToTargetRange": function(msg) {
@ -618,27 +626,124 @@ var UnitFsmSpec = {
},
"Order.Attack": function(msg) {
// TODO: we should move in formation towards the target,
// then break up into individuals when close enough to it
// TODO: on what should we base this range?
// Check if we are already in range, otherwise walk there
if (!this.CheckTargetRangeExplicit(msg.data.target, 0, 20))
{
if (!this.TargetIsAlive(msg.data.target))
// The target was destroyed
this.FinishOrder();
else
// Out of range; move there in formation
this.PushOrderFront("WalkToTargetRange", { "target": msg.data.target, "min": 0, "max": 20 });
return;
}
var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation);
// We don't want to rearrange the formation if the individual units are carrying
// out a task and one of the members dies/leaves the formation.
cmpFormation.SetRearrange(false);
cmpFormation.CallMemberFunction("Attack", [msg.data.target, false]);
// TODO: we should wait until the target is killed, then
// move on to the next queued order.
// Don't bother now, just disband the formation immediately.
cmpFormation.Disband();
this.SetNextStateAlwaysEntering("MEMBER");
},
"Order.Garrison": function(msg) {
// TODO: on what should we base this range?
// Check if we are already in range, otherwise walk there
if (!this.CheckTargetRangeExplicit(msg.data.target, 0, 10))
{
if (!this.TargetIsAlive(msg.data.target))
// The target was destroyed
this.FinishOrder();
else
// Out of range; move there in formation
this.PushOrderFront("WalkToTargetRange", { "target": msg.data.target, "min": 0, "max": 10 });
return;
}
var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation);
// We don't want to rearrange the formation if the individual units are carrying
// out a task and one of the members dies/leaves the formation.
cmpFormation.SetRearrange(false);
cmpFormation.CallMemberFunction("Garrison", [msg.data.target, false]);
this.SetNextStateAlwaysEntering("MEMBER");
},
"Order.Gather": function(msg) {
// TODO: on what should we base this range?
// Check if we are already in range, otherwise walk there
if (!this.CheckTargetRangeExplicit(msg.data.target, 0, 10))
{
if (!this.CanGather(msg.data.target))
// The target isn't gatherable
this.FinishOrder();
// TODO: Should we issue a gather-near-position order
// if the target isn't gatherable/doesn't exist anymore?
else
// Out of range; move there in formation
this.PushOrderFront("WalkToTargetRange", { "target": msg.data.target, "min": 0, "max": 10 });
return;
}
var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation);
// We don't want to rearrange the formation if the individual units are carrying
// out a task and one of the members dies/leaves the formation.
cmpFormation.SetRearrange(false);
cmpFormation.CallMemberFunction("Gather", [msg.data.target, false]);
this.SetNextStateAlwaysEntering("MEMBER");
},
"Order.GatherNearPosition": function(msg) {
// TODO: on what should we base this range?
// Check if we are already in range, otherwise walk there
if (!this.CheckTargetRangeExplicit(msg.data.target, 0, 20))
{
if (!this.TargetIsAlive(msg.data.target))
// The target was destroyed
this.FinishOrder();
else
// Out of range; move there in formation
this.PushOrderFront("WalkToTargetRange", { "target": msg.data.target, "min": 0, "max": 20 });
return;
}
var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation);
// We don't want to rearrange the formation if the individual units are carrying
// out a task and one of the members dies/leaves the formation.
cmpFormation.SetRearrange(false);
cmpFormation.CallMemberFunction("GatherNearPosition", [msg.data.x, msg.data.z, msg.data.type, msg.data.template, false]);
this.SetNextStateAlwaysEntering("MEMBER");
},
"Order.Heal": function(msg) {
// TODO: see notes in Order.Attack
// TODO: on what should we base this range?
// Check if we are already in range, otherwise walk there
if (!this.CheckTargetRangeExplicit(msg.data.target, 0, 10))
{
if (!this.TargetIsAlive(msg.data.target))
// The target was destroyed
this.FinishOrder();
else
// Out of range; move there in formation
this.PushOrderFront("WalkToTargetRange", { "target": msg.data.target, "min": 0, "max": 10 });
return;
}
var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation);
// We don't want to rearrange the formation if the individual units are carrying
// out a task and one of the members dies/leaves the formation.
cmpFormation.SetRearrange(false);
cmpFormation.CallMemberFunction("Heal", [msg.data.target, false]);
cmpFormation.Disband();
this.SetNextStateAlwaysEntering("MEMBER");
},
"Order.Repair": function(msg) {
// TODO on what should we base this range?
// TODO: on what should we base this range?
// Check if we are already in range, otherwise walk there
if (!this.CheckTargetRangeExplicit(msg.data.target, 0, 10))
{
@ -657,55 +762,50 @@ var UnitFsmSpec = {
cmpFormation.SetRearrange(false);
cmpFormation.CallMemberFunction("Repair", [msg.data.target, msg.data.autocontinue, false]);
this.SetNextState("REPAIR");
},
"Order.Gather": function(msg) {
// TODO: see notes in Order.Attack
// If the resource no longer exists, send a GatherNearPosition order
var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation);
if (this.CanGather(msg.data.target))
cmpFormation.CallMemberFunction("Gather", [msg.data.target, false]);
else
cmpFormation.CallMemberFunction("GatherNearPosition", [msg.data.lastPos.x, msg.data.lastPos.z, msg.data.type, msg.data.template, false]);
cmpFormation.Disband();
},
"Order.GatherNearPosition": function(msg) {
// TODO: see notes in Order.Attack
var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation);
cmpFormation.CallMemberFunction("GatherNearPosition", [msg.data.x, msg.data.z, msg.data.type, msg.data.template, false]);
cmpFormation.Disband();
this.SetNextStateAlwaysEntering("MEMBER");
},
"Order.ReturnResource": function(msg) {
// TODO: see notes in Order.Attack
// TODO: on what should we base this range?
// Check if we are already in range, otherwise walk there
if (!this.CheckTargetRangeExplicit(msg.data.target, 0, 10))
{
if (!this.TargetIsAlive(msg.data.target))
// The target was destroyed
this.FinishOrder();
else
// Out of range; move there in formation
this.PushOrderFront("WalkToTargetRange", { "target": msg.data.target, "min": 0, "max": 10 });
return;
}
var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation);
// We don't want to rearrange the formation if the individual units are carrying
// out a task and one of the members dies/leaves the formation.
cmpFormation.SetRearrange(false);
cmpFormation.CallMemberFunction("ReturnResource", [msg.data.target, false]);
cmpFormation.Disband();
this.SetNextStateAlwaysEntering("MEMBER");
},
"Order.Garrison": function(msg) {
// TODO: see notes in Order.Attack
var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation);
cmpFormation.CallMemberFunction("Garrison", [msg.data.target, false]);
cmpFormation.Disband();
},
"Order.Pack": function(msg) {
// TODO: see notes in Order.Attack
var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation);
// We don't want to rearrange the formation if the individual units are carrying
// out a task and one of the members dies/leaves the formation.
cmpFormation.SetRearrange(false);
cmpFormation.CallMemberFunction("Pack", [false]);
cmpFormation.Disband();
this.SetNextStateAlwaysEntering("MEMBER");
},
"Order.Unpack": function(msg) {
// TODO: see notes in Order.Attack
var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation);
// We don't want to rearrange the formation if the individual units are carrying
// out a task and one of the members dies/leaves the formation.
cmpFormation.SetRearrange(false);
cmpFormation.CallMemberFunction("Unpack", [false]);
cmpFormation.Disband();
this.SetNextStateAlwaysEntering("MEMBER");
},
"IDLE": {
@ -715,30 +815,70 @@ var UnitFsmSpec = {
"MoveStarted": function(msg) {
var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation);
cmpFormation.SetRearrange(true);
cmpFormation.MoveMembersIntoFormation(true);
cmpFormation.MoveMembersIntoFormation(true, true);
},
"MoveCompleted": function(msg) {
if (this.FinishOrder())
return;
// If this was the last order, attempt to disband the formation.
var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation);
if (this.FinishOrder())
{
cmpFormation.CallMemberFunction("ResetFinishOrder", []);
return;
}
// If this was the last order, attempt to disband the formation.
cmpFormation.FindInPosition();
},
},
"FORMING": {
"MoveStarted": function(msg) {
var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation);
cmpFormation.SetRearrange(true);
cmpFormation.MoveMembersIntoFormation(true, false);
},
"REPAIR": {
"ConstructionFinished": function(msg) {
if (msg.data.entity != this.order.data.target)
"MoveCompleted": function(msg) {
var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation);
if (this.FinishOrder())
{
cmpFormation.CallMemberFunction("ResetFinishOrder", []);
return;
}
// If this was the last order, attempt to disband the formation.
cmpFormation.FindInPosition();
}
},
"MEMBER": {
// Wait for individual members to finish
"enter": function(msg) {
this.StartTimer(1000, 1000);
},
"Timer": function(msg) {
var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation);
// Have all members finished the task?
if (!cmpFormation.TestAllMemberFunction("HasFinishedOrder", []))
return;
cmpFormation.CallMemberFunction("ResetFinishOrder", []);
// Execute the next order
if (this.FinishOrder())
return;
var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation);
// No more order left.
cmpFormation.Disband();
},
"leave": function(msg) {
this.StopTimer();
},
},
},
@ -746,6 +886,9 @@ var UnitFsmSpec = {
// States for entities moving as part of a formation:
"FORMATIONMEMBER": {
"FormationLeave": function(msg) {
// We're not in a formation anymore, so no need to track this.
this.finishedOrder = false;
// Stop moving as soon as the formation disbands
this.StopMoving();
@ -807,6 +950,9 @@ var UnitFsmSpec = {
// is done moving. The controller is notified, and will disband the
// formation if all units are in formation and no orders remain.
"MoveCompleted": function(msg) {
if(this.FinishOrder())
return;
var cmpFormation = Engine.QueryInterface(this.formationController, IID_Formation);
cmpFormation.SetInPosition(this.entity);
},
@ -1663,7 +1809,6 @@ var UnitFsmSpec = {
"REPAIRING": {
"enter": function() {
// If this order was forced, the player probably gave it, but now we've reached the target
// switch to an unforced order (can be interrupted by attacks)
if (this.order.data.force)
@ -2032,6 +2177,7 @@ UnitAI.prototype.Init = function()
this.isGarrisoned = false;
this.isIdle = false;
this.lastFormationName = "";
this.finishedOrder = false; // used to find if all formation members finished the order
// For preventing increased action rate due to Stop orders or target death.
this.lastAttacked = undefined;
@ -2045,6 +2191,21 @@ UnitAI.prototype.IsFormationController = function()
return (this.template.FormationController == "true");
};
UnitAI.prototype.IsFormationMember = function()
{
return (this.formationController != INVALID_ENTITY);
};
UnitAI.prototype.HasFinishedOrder = function()
{
return this.finishedOrder;
};
UnitAI.prototype.ResetFinishOrder = function()
{
this.finishedOrder = false;
};
UnitAI.prototype.IsAnimal = function()
{
return (this.template.NaturalBehaviour ? true : false);
@ -2332,6 +2493,22 @@ UnitAI.prototype.FinishOrder = function()
else
{
this.SetNextState("IDLE");
// Check if there are queued formation orders
if (this.IsFormationMember())
{
var cmpUnitAI = Engine.QueryInterface(this.formationController, IID_UnitAI);
if (cmpUnitAI)
{
// Inform the formation controller that we finished this task
this.finishedOrder = true;
// We don't want to carry out the default order
// if there are still queued formation orders left
if (cmpUnitAI.GetOrders().length > 1)
return true;
}
}
return false;
}
};
@ -3089,6 +3266,24 @@ UnitAI.prototype.GetLastFormationName = function()
return this.lastFormationName;
};
UnitAI.prototype.MoveIntoFormation = function(cmd)
{
var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation);
if (!cmpFormation)
return;
cmpFormation.LoadFormation(cmd.name);
var cmpPosition = Engine.QueryInterface(this.entity, IID_Position);
if (!cmpPosition || !cmpPosition.IsInWorld())
return;
var pos = cmpPosition.GetPosition();
// Add new order to move into formation at the current position
this.PushOrderFront("MoveIntoFormation", { "x": pos.x, "z": pos.z, "force": true });
};
/**
* Returns the estimated distance that this unit will travel before either
* finishing all of its orders, or reaching a non-walk target (attack, gather, etc).
@ -3111,6 +3306,7 @@ UnitAI.prototype.ComputeWalkingDistance = function()
switch (order.type)
{
case "Walk":
case "MoveIntoFormation":
case "GatherNearPosition":
// Add the distance to the target point
var dx = order.data.x - pos.x;

View File

@ -146,6 +146,139 @@ function TestFormationExiting(mode)
TS_FAIL("invalid mode");
}
function TestMoveIntoFormationWhileAttacking()
{
ResetState();
var playerEntity = 5;
var controller = 10;
var enemy = 20;
var unit = 30;
var units = [];
var unitCount = 8;
var unitAIs = [];
AddMock(SYSTEM_ENTITY, IID_Timer, {
SetInterval: function() { },
SetTimeout: function() { },
});
AddMock(SYSTEM_ENTITY, IID_RangeManager, {
CreateActiveQuery: function(ent, minRange, maxRange, players, iid, flags) {
return 1;
},
EnableActiveQuery: function(id) { },
ResetActiveQuery: function(id) { return [enemy]; },
DisableActiveQuery: function(id) { },
GetEntityFlagMask: function(identifier) { },
});;
AddMock(SYSTEM_ENTITY, IID_PlayerManager, {
GetPlayerByID: function(id) { return playerEntity; },
GetNumPlayers: function() { return 2; },
});
AddMock(playerEntity, IID_Player, {
IsAlly: function() { return []; },
IsEnemy: function() { return []; },
});
// create units
for (var i = 0; i < unitCount; i++) {
units.push(unit + i);
var unitAI = ConstructComponent(unit + i, "UnitAI", { "FormationController": "false", "DefaultStance": "aggressive" });
AddMock(unit + i, IID_Identity, {
GetClassesList: function() { return []; },
});
AddMock(unit + i, IID_Ownership, {
GetOwner: function() { return 1; },
});
AddMock(unit + i, IID_Position, {
GetPosition: function() { return { "x": 0, "z": 0 }; },
IsInWorld: function() { return true; },
});
AddMock(unit + i, IID_UnitMotion, {
GetWalkSpeed: function() { return 1; },
MoveToFormationOffset: function(target, x, z) { },
IsInTargetRange: function(target, min, max) { return true; },
MoveToTargetRange: function(target, min, max) { },
StopMoving: function() { },
});
AddMock(unit + i, IID_Vision, {
GetRange: function() { return 10; },
});
AddMock(unit + i, IID_Attack, {
GetRange: function() { return 10; },
GetBestAttack: function() { return "melee"; },
GetBestAttackAgainst: function(t) { return "melee"; },
GetTimers: function() { return { "prepare": 500, "repeat": 1000 }; },
CanAttack: function(v) { return true; },
CompareEntitiesByPreference: function(a, b) { return 0; },
});
unitAI.OnCreate();
unitAI.SetupRangeQuery(1);
unitAIs.push(unitAI);
}
// create enemy
AddMock(enemy, IID_Health, {
GetHitpoints: function() { return 40; },
});
var controllerFormation = ConstructComponent(controller, "Formation");
var controllerAI = ConstructComponent(controller, "UnitAI", { "FormationController": "true", "DefaultStance": "aggressive" });
AddMock(controller, IID_Position, {
JumpTo: function(x, z) { this.x = x; this.z = z; },
GetPosition: function() { return { "x": this.x, "z": this.z }; },
IsInWorld: function() { return true; },
});
AddMock(controller, IID_UnitMotion, {
SetUnitRadius: function(r) { },
SetSpeed: function(speed) { },
MoveToPointRange: function(x, z, minRange, maxRange) { },
IsInTargetRange: function(target, min, max) { return true; },
});
controllerAI.OnCreate();
controllerFormation.SetMembers(units);
controllerAI.Attack(enemy, []);
for each (var ent in unitAIs) {
TS_ASSERT_EQUALS(unitAI.fsmStateName, "INDIVIDUAL.COMBAT.ATTACKING");
}
controllerAI.MoveIntoFormation({"name": "Circle"});
// let all units be in position
for each (var ent in unitAIs) {
controllerFormation.SetInPosition(ent);
}
for each (var ent in unitAIs) {
TS_ASSERT_EQUALS(unitAI.fsmStateName, "INDIVIDUAL.COMBAT.ATTACKING");
}
controllerFormation.Disband();
}
TestFormationExiting(0);
TestFormationExiting(1);
TestFormationExiting(2);
TestMoveIntoFormationWhileAttacking();

View File

@ -370,11 +370,7 @@ function ProcessCommand(player, cmd)
case "formation":
var entities = FilterEntityList(cmd.entities, player, controlAllUnits);
GetFormationUnitAIs(entities, player, cmd.name).forEach(function(cmpUnitAI) {
var cmpFormation = Engine.QueryInterface(cmpUnitAI.entity, IID_Formation);
if (!cmpFormation)
return;
cmpFormation.LoadFormation(cmd.name);
cmpFormation.MoveMembersIntoFormation(true);
cmpUnitAI.MoveIntoFormation(cmd);
});
break;