diff --git a/binaries/data/mods/public/simulation/components/Formation.js b/binaries/data/mods/public/simulation/components/Formation.js
index 16967b9a5f..d1fecf43ff 100644
--- a/binaries/data/mods/public/simulation/components/Formation.js
+++ b/binaries/data/mods/public/simulation/components/Formation.js
@@ -3,9 +3,12 @@ function Formation() {}
Formation.prototype.Schema =
"";
+var g_ColumnDistanceThreshold = 32; // distance at which we'll switch between column/box formations
+
Formation.prototype.Init = function()
{
- this.members = [];
+ this.members = []; // entity IDs currently belonging to this formation
+ this.columnar = false; // whether we're travelling in column (vs box) formation
};
Formation.prototype.GetMemberCount = function()
@@ -111,7 +114,12 @@ Formation.prototype.MoveMembersIntoFormation = function()
types["Unknown"] += 1; // TODO
}
- var offsets = this.ComputeFormationOffsets(types);
+ // Work out whether this should be a column or box formation
+ var cmpUnitAI = Engine.QueryInterface(this.entity, IID_UnitAI);
+ var walkingDistance = cmpUnitAI.ComputeWalkingDistance();
+ this.columnar = (walkingDistance > g_ColumnDistanceThreshold);
+
+ var offsets = this.ComputeFormationOffsets(types, this.columnar);
var avgpos = this.ComputeAveragePosition(positions);
var avgoffset = this.ComputeAveragePosition(offsets);
@@ -153,22 +161,35 @@ Formation.prototype.MoveToMembersCenter = function()
cmpPosition.JumpTo(avgpos.x, avgpos.z);
};
-Formation.prototype.ComputeFormationOffsets = function(types)
+Formation.prototype.ComputeFormationOffsets = function(types, columnar)
{
var separation = 4; // TODO: don't hardcode this
var count = types["Unknown"];
- // Choose a sensible width for the basic box formation
+ // Choose a sensible width for the basic default formation
var cols;
- if (count <= 4)
- cols = count;
- if (count <= 8)
- cols = 4;
- else if (count <= 16)
- cols = Math.ceil(count / 2);
+ if (columnar)
+ {
+ // Have at most 3 files
+ if (count <= 3)
+ cols = count;
+ else
+ cols = 3;
+ }
else
- cols = 8;
+ {
+ // Try to have at least 5 files (so batch training gives a single line),
+ // and at most 8
+ if (count <= 5)
+ cols = count;
+ if (count <= 10)
+ cols = 5;
+ else if (count <= 16)
+ cols = Math.ceil(count / 2);
+ else
+ cols = 8;
+ }
var ranks = Math.ceil(count / cols);
@@ -228,6 +249,16 @@ Formation.prototype.ComputeMotionParameters = function()
// TODO: we also need to do something about PassabilityClass, CostClass
};
+Formation.prototype.OnUpdate_Final = function(msg)
+{
+ // Switch between column and box if necessary
+ var cmpUnitAI = Engine.QueryInterface(this.entity, IID_UnitAI);
+ var walkingDistance = cmpUnitAI.ComputeWalkingDistance();
+ var columnar = (walkingDistance > g_ColumnDistanceThreshold);
+ if (columnar != this.columnar)
+ this.MoveMembersIntoFormation();
+};
+
Formation.prototype.OnGlobalOwnershipChanged = function(msg)
{
// When an entity is captured or destroyed, it should no longer be
diff --git a/binaries/data/mods/public/simulation/components/UnitAI.js b/binaries/data/mods/public/simulation/components/UnitAI.js
index b154894fa1..a5b32c33ae 100644
--- a/binaries/data/mods/public/simulation/components/UnitAI.js
+++ b/binaries/data/mods/public/simulation/components/UnitAI.js
@@ -848,6 +848,68 @@ UnitAI.prototype.GetFormationController = function()
return this.formationController;
};
+/**
+ * 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).
+ * Intended for Formation to switch to column layout on long walks.
+ */
+UnitAI.prototype.ComputeWalkingDistance = function()
+{
+ var distance = 0;
+
+ var cmpPosition = Engine.QueryInterface(this.entity, IID_Position);
+ if (!cmpPosition || !cmpPosition.IsInWorld())
+ return 0;
+
+ // Keep track of the position at the start of each order
+ var pos = cmpPosition.GetPosition();
+
+ for (var i = 0; i < this.orderQueue.length; ++i)
+ {
+ var order = this.orderQueue[i];
+ switch (order.type)
+ {
+ case "Walk":
+ // Add the distance to the target point
+ var dx = order.data.x - pos.x;
+ var dz = order.data.z - pos.z;
+ var d = Math.sqrt(dx*dx + dz*dz);
+ distance += d;
+
+ // Remember this as the start position for the next order
+ pos = order.data;
+
+ break; // and continue the loop
+
+ case "WalkToTarget":
+ case "Attack":
+ case "Gather":
+ case "Repair":
+ // Find the target unit's position
+ var cmpTargetPosition = Engine.QueryInterface(order.data.target, IID_Position);
+ if (!cmpTargetPosition || !cmpTargetPosition.IsInWorld())
+ return distance;
+ var targetPos = cmpTargetPosition.GetPosition();
+
+ // Add the distance to the target unit
+ var dx = targetPos.x - pos.x;
+ var dz = targetPos.z - pos.z;
+ var d = Math.sqrt(dx*dx + dz*dz);
+ distance += d;
+
+ // Return the total distance to the target
+ return distance;
+
+ default:
+ error("Unrecognised order type '"+order.type+"'");
+ return distance;
+ }
+ }
+
+ // Return the total distance to the end of the order queue
+ return distance;
+};
+
UnitAI.prototype.AddOrder = function(type, data, queued)
{
if (queued)