forked from 0ad/0ad
Allow changing the formation, while under attack. Fixes #1624. Patch by Simikolon. This was SVN commit r12911.
This commit is contained in:
parent
ddd8c36d6f
commit
5800be1334
@ -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)
|
||||
};
|
||||
|
@ -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;
|
||||
|
@ -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();
|
||||
|
@ -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;
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user