Index: components/Formation.js
===================================================================
--- components/Formation.js
+++ components/Formation.js
@@ -70,9 +70,33 @@
"" +
"";
-// Distance at which we'll switch between column/box formations.
-var g_ColumnDistanceThreshold = 128;
+// Distance at which we'll switch between column/box formations (not implemented anymore).
+//var g_ColumnDistanceThreshold = 128;
+// Turning angle over which formation positions are recomputed.
+const turningAngleThreshold = Math.PI/4;
+
+/**
+ * Returns orientation in radiants of a vector as how the method TurnTo accepts it
+ * (it's rotated respect to the actual mathematical value)
+ */
+let fromVectorToAngle = function(vector)
+{
+ return Math.PI/2 - Math.atan2(vector.y, vector.x)
+}
+
+/**
+ * Returns true if the two given angles (in radiants) make a little enough difference.
+ * A difference is little enough if it's ok for a formation to make such a turn without
+ * reassigning positions.
+ */
+let areAnglesSimilar = function(a1, a2)
+{
+ let d = Math.abs(a1 - a2);
+ return d < turningAngleThreshold || d > 2*Math.PI - turningAngleThreshold;
+}
+
+
Formation.prototype.variablesToSerialize = [
"lastOrderVariant",
"members",
@@ -323,36 +347,6 @@
};
/**
- * Initialize the members of this formation.
- * Must only be called once.
- * All members must implement UnitAI.
- */
-Formation.prototype.SetMembers = function(ents)
-{
- this.members = ents;
-
- for (let ent of this.members)
- {
- let cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
- cmpUnitAI.SetFormationController(this.entity);
-
- let cmpAuras = Engine.QueryInterface(ent, IID_Auras);
- if (cmpAuras && cmpAuras.HasFormationAura())
- {
- this.formationMembersWithAura.push(ent);
- cmpAuras.ApplyFormationAura(ents);
- }
- }
-
- this.offsets = undefined;
- // Locate this formation controller in the middle of its members.
- this.MoveToMembersCenter();
-
- // Compute the speed etc. of the formation.
- this.ComputeMotionParameters();
-};
-
-/**
* Remove the given list of entities.
* The entities must already be members of this formation.
* @param {boolean} rename - Whether the removal was part of an entity rename
@@ -475,11 +469,9 @@
{
if (!this.members.length)
return;
-
+
let active = [];
let positions = [];
- let rotations = 0;
-
for (let ent of this.members)
{
let cmpPosition = Engine.QueryInterface(ent, IID_Position);
@@ -490,48 +482,51 @@
// Query the 2D position as the exact height calculation isn't needed,
// but bring the position to the correct coordinates.
positions.push(cmpPosition.GetPosition2D());
- rotations += cmpPosition.GetRotation().y;
}
-
- let avgpos = Vector2D.average(positions);
-
- let cmpPosition = Engine.QueryInterface(this.entity, IID_Position);
+
+ let sharp_turn_flag = false;
+
+ let cmpFormationPosition = Engine.QueryInterface(this.entity, IID_Position);
+
// Reposition the formation if we're told to or if we don't already have a position.
- if (moveCenter || (cmpPosition && !cmpPosition.IsInWorld()))
- this.SetupPositionAndHandleRotation(avgpos.x, avgpos.y, rotations / active.length);
+ if (moveCenter || (cmpFormationPosition && !cmpFormationPosition.IsInWorld()))
+ {
+ let old_rotation = cmpFormationPosition.GetRotation().y;
+
+ let avgpos = Vector2D.average(positions);
+ cmpFormationPosition.JumpTo(avgpos.x, avgpos.y);
- this.lastOrderVariant = variant;
- // Switch between column and box if necessary.
- let cmpFormationUnitAI = Engine.QueryInterface(this.entity, IID_UnitAI);
- let walkingDistance = cmpFormationUnitAI.ComputeWalkingDistance();
- let columnar = walkingDistance > g_ColumnDistanceThreshold;
- if (columnar != this.columnar)
- {
- this.columnar = columnar;
- this.offsets = undefined;
+ // Don't make the formation controller entity show up in range queries (rP13484)
+ let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
+ cmpRangeManager.SetEntityFlag(this.entity, "normal", false);
+
+ let cmpFormationUnitAI = Engine.QueryInterface(this.entity, IID_UnitAI);
+ let direction = fromVectorToAngle(cmpFormationUnitAI.GetTargetPositions()[0].sub(avgpos));
+ cmpFormationPosition.TurnTo(direction);
+
+ sharp_turn_flag = !areAnglesSimilar(direction, old_rotation);
}
+ this.lastOrderVariant = variant;
+
let offsetsChanged = false;
- let newOrientation = this.GetEstimatedOrientation(avgpos);
- if (!this.offsets)
+ if (!this.offsets || sharp_turn_flag)
{
this.offsets = this.ComputeFormationOffsets(active, positions);
offsetsChanged = true;
}
+ if (force)
+ // Reset waitingOnController as FormationWalk is called.
+ this.ResetWaitingEntities();
+
let xMax = 0;
let yMax = 0;
let xMin = 0;
let yMin = 0;
-
- if (force)
- // Reset waitingOnController as FormationWalk is called.
- this.ResetWaitingEntities();
-
- for (let i = 0; i < this.offsets.length; ++i)
+
+ for (let offset of this.offsets)
{
- let offset = this.offsets[i];
-
let cmpUnitAI = Engine.QueryInterface(offset.ent, IID_UnitAI);
if (!cmpUnitAI)
{
@@ -553,49 +548,11 @@
xMin = Math.min(xMin, offset.x);
yMin = Math.min(yMin, offset.y);
}
+
this.width = xMax - xMin;
this.depth = yMax - yMin;
};
-Formation.prototype.MoveToMembersCenter = function()
-{
- let positions = [];
- let rotations = 0;
-
- for (let ent of this.members)
- {
- let cmpPosition = Engine.QueryInterface(ent, IID_Position);
- if (!cmpPosition || !cmpPosition.IsInWorld())
- continue;
-
- positions.push(cmpPosition.GetPosition2D());
- rotations += cmpPosition.GetRotation().y;
- }
-
- let avgpos = Vector2D.average(positions);
- this.SetupPositionAndHandleRotation(avgpos.x, avgpos.y, rotations / positions.length);
-};
-
-/**
-* Set formation position.
-* If formation is not in world at time this is called, set new rotation and flag for range manager.
-*/
-Formation.prototype.SetupPositionAndHandleRotation = function(x, y, rot)
-{
- let cmpPosition = Engine.QueryInterface(this.entity, IID_Position);
- if (!cmpPosition)
- return;
- let wasInWorld = cmpPosition.IsInWorld();
- cmpPosition.JumpTo(x, y);
-
- if (wasInWorld)
- return;
-
- let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
- cmpRangeManager.SetEntityFlag(this.entity, "normal", false);
- cmpPosition.TurnTo(rot);
-};
-
Formation.prototype.GetAvgFootprint = function(active)
{
let footprints = [];
@@ -627,6 +584,12 @@
return r;
};
+/**
+ * Computes the offsets (slots for units to take into the formation), their arrangement, and
+ * which unit is assigned to which offset.
+ * @param active - Array of entities members of the formation.
+ * @param positions - Array of same length, enumerating the positions of such entities.
+ */
Formation.prototype.ComputeFormationOffsets = function(active, positions)
{
let separation = this.GetAvgFootprint(active);
@@ -633,18 +596,15 @@
separation.width *= this.separationMultiplier.width;
separation.depth *= this.separationMultiplier.depth;
- let sortingClasses;
- if (this.columnar)
- sortingClasses = ["Cavalry", "Infantry"];
- else
- sortingClasses = this.sortingClasses.slice();
+ let sortingClasses = this.sortingClasses.slice();
sortingClasses.push("Unknown");
- // The entities will be assigned to positions in the formation in
- // the same order as the types list is ordered.
+ // types is an object that splits the entities to be assigned to offsets,
+ // based on their classes
+ // TODO annullare qua
let types = {};
- for (let i = 0; i < sortingClasses.length; ++i)
- types[sortingClasses[i]] = [];
+ for (let cls of sortingClasses)
+ types[cls] = [];
for (let i in active)
{
@@ -651,11 +611,11 @@
let cmpIdentity = Engine.QueryInterface(active[i], IID_Identity);
let classes = cmpIdentity.GetClassesList();
let done = false;
- for (let c = 0; c < sortingClasses.length; ++c)
+ for (let cls of sortingClasses.slice(0, -1))
{
- if (classes.indexOf(sortingClasses[c]) > -1)
+ if (classes.indexOf(cls) > -1)
{
- types[sortingClasses[c]].push({ "ent": active[i], "pos": positions[i] });
+ types[cls].push({ "ent": active[i], "pos": positions[i] });
done = true;
break;
}
@@ -675,16 +635,7 @@
// Choose a sensible size/shape for the various formations, depending on number of units.
let cols;
- if (this.columnar)
{
- shape = "square";
- cols = Math.min(count, 3);
- shiftRows = false;
- centerGap = 0;
- sortingOrder = null;
- }
- else
- {
let depth = Math.sqrt(count / this.widthDepthRatio);
if (this.maxRows && depth > this.maxRows)
depth = this.maxRows;
@@ -788,22 +739,20 @@
return Math.max(Math.abs(o1.x), Math.abs(o1.y)) < Math.max(Math.abs(o2.x), Math.abs(o2.y));
});
- // Query the 2D position of the formation.
- let cmpPosition = Engine.QueryInterface(this.entity, IID_Position);
- let formationPos = cmpPosition.GetPosition2D();
-
- // Use realistic place assignment,
+ let realPositions = this.GetRealOffsetPositions(offsets);
+
+ // Use efficient place assignment,
// every soldier searches the closest available place in the formation.
let newOffsets = [];
- let realPositions = this.GetRealOffsetPositions(offsets, formationPos);
- for (let i = sortingClasses.length; i; --i)
+ for (let cls of sortingClasses.reverse())
{
- let t = types[sortingClasses[i - 1]];
- if (!t.length)
+ let unit_list = types[cls];
+ if (!unit_list.length)
continue;
- let usedOffsets = offsets.splice(-t.length);
- let usedRealPositions = realPositions.splice(-t.length);
- for (let entPos of t)
+
+ let usedOffsets = offsets.splice(-unit_list.length);
+ let usedRealPositions = realPositions.splice(-unit_list.length);
+ for (let entPos of unit_list)
{
let closestOffsetId = this.TakeClosestOffset(entPos, usedRealPositions, usedOffsets);
usedRealPositions.splice(closestOffsetId, 1);
@@ -816,6 +765,23 @@
};
/**
+ * Get the world positions for a list of offsets in this formation.
+ */
+Formation.prototype.GetRealOffsetPositions = function(offsets)
+{
+ let cmpPosition = Engine.QueryInterface(this.entity, IID_Position);
+ let [ pos, rot ] = [cmpPosition.GetPosition2D(), cmpPosition.GetRotation().y];
+ let [ sin, cos ] = [Math.sin(rot), Math.cos(rot)];
+
+ let offsetPositions = [];
+ for (let o of offsets)
+ offsetPositions.push(new Vector2D(pos.x + o.y * sin + o.x * cos,
+ pos.y + o.y * cos - o.x * sin));
+
+ return offsetPositions;
+};
+
+/**
* Search the closest position in the realPositions list to the given entity.
* @param entPos - Object with entity position and entity ID.
* @param realPositions - The world coordinates of the available offsets.
@@ -827,7 +793,7 @@
let pos = entPos.pos;
let closestOffsetId = -1;
let offsetDistanceSq = Infinity;
- for (let i = 0; i < realPositions.length; ++i)
+ for (let i in realPositions)
{
let distSq = pos.distanceToSquared(realPositions[i]);
if (distSq < offsetDistanceSq)
@@ -841,36 +807,6 @@
};
/**
- * Get the world positions for a list of offsets in this formation.
- */
-Formation.prototype.GetRealOffsetPositions = function(offsets, pos)
-{
- let offsetPositions = [];
- let { sin, cos } = this.GetEstimatedOrientation(pos);
- // Calculate the world positions.
- for (let o of offsets)
- offsetPositions.push(new Vector2D(pos.x + o.y * sin + o.x * cos, pos.y + o.y * cos - o.x * sin));
-
- return offsetPositions;
-};
-
-/**
- * Calculate the estimated rotation of the formation based on the current rotation.
- * Return the sine and cosine of the angle.
- */
-Formation.prototype.GetEstimatedOrientation = function(pos)
-{
- let r = {};
- let cmpPosition = Engine.QueryInterface(this.entity, IID_Position);
- if (!cmpPosition)
- return r;
- let rot = cmpPosition.GetRotation().y;
- r.sin = Math.sin(rot);
- r.cos = Math.cos(rot);
- return r;
-};
-
-/**
* Set formation controller's speed based on its current members.
*/
Formation.prototype.ComputeMotionParameters = function()
@@ -890,13 +826,14 @@
cmpUnitMotion.SetSpeedMultiplier(minSpeed / cmpUnitMotion.GetWalkSpeed());
};
+/**
+ * Check the distance to twin formations, and merge if the formations could collide.
+ */
Formation.prototype.ShapeUpdate = function()
{
if (!this.rearrange)
return;
- // Check the distance to twin formations, and merge if
- // the formations could collide.
for (let i = this.twinFormations.length - 1; i >= 0; --i)
{
// Only do the check on one side.
@@ -933,18 +870,7 @@
Engine.DestroyEntity(this.twinFormations[i]);
this.twinFormations.splice(i, 1);
}
- // Switch between column and box if necessary.
- let cmpUnitAI = Engine.QueryInterface(this.entity, IID_UnitAI);
- let walkingDistance = cmpUnitAI.ComputeWalkingDistance();
- let columnar = walkingDistance > g_ColumnDistanceThreshold;
- if (columnar != this.columnar)
- {
- this.offsets = undefined;
- this.columnar = columnar;
- // Disable moveCenter so we can't get stuck in a loop of switching
- // shape causing center to change causing shape to switch back.
- this.MoveMembersIntoFormation(false, true, this.lastOrderVariant);
- }
+
};
Formation.prototype.ResetOrderVariant = function()
@@ -960,6 +886,10 @@
this.RemoveMembers([msg.entity]);
};
+/**
+ * Simply swaps the old entity with the new one, when this gets renamed
+ (e.g. promoted).
+ */
Formation.prototype.OnGlobalEntityRenamed = function(msg)
{
if (this.members.indexOf(msg.entity) === -1)
@@ -1037,4 +967,76 @@
return cmpNewUnitAI;
};
+///// the following functions seem to be used only by LoadFormation, they apparently
+///// have a similar behaviour of MoveMembersIntoFormation
+
+/**
+ * Initialize the members of this formation.
+ * Must only be called once.
+ * All members must implement UnitAI.
+ */
+Formation.prototype.SetMembers = function(ents)
+{
+ this.members = ents;
+
+ for (let ent of this.members)
+ {
+ let cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
+ cmpUnitAI.SetFormationController(this.entity);
+
+ let cmpAuras = Engine.QueryInterface(ent, IID_Auras);
+ if (cmpAuras && cmpAuras.HasFormationAura())
+ {
+ this.formationMembersWithAura.push(ent);
+ cmpAuras.ApplyFormationAura(ents);
+ }
+ }
+
+ this.offsets = undefined;
+ // Locate this formation controller in the middle of its members.
+ this.MoveToMembersCenter();
+
+ // Compute the speed etc. of the formation.
+ this.ComputeMotionParameters();
+};
+
+Formation.prototype.MoveToMembersCenter = function()
+{
+ let positions = [];
+ let rotations = 0;
+
+ for (let ent of this.members)
+ {
+ let cmpPosition = Engine.QueryInterface(ent, IID_Position);
+ if (!cmpPosition || !cmpPosition.IsInWorld())
+ continue;
+
+ positions.push(cmpPosition.GetPosition2D());
+ rotations += cmpPosition.GetRotation().y;
+ }
+
+ let avgpos = Vector2D.average(positions);
+ this.SetupPositionAndHandleRotation(avgpos.x, avgpos.y, rotations / positions.length);
+};
+
+/**
+* Set formation position.
+* If formation is not in world at time this is called, set new rotation and flag for range manager.
+*/
+Formation.prototype.SetupPositionAndHandleRotation = function(x, y, rot)
+{
+ let cmpPosition = Engine.QueryInterface(this.entity, IID_Position);
+ if (!cmpPosition)
+ return;
+ let wasInWorld = cmpPosition.IsInWorld();
+ cmpPosition.JumpTo(x, y);
+
+ if (wasInWorld)
+ return;
+
+ let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
+ cmpRangeManager.SetEntityFlag(this.entity, "normal", false);
+ cmpPosition.TurnTo(rot);
+};
+
Engine.RegisterComponentType(IID_Formation, "Formation", Formation);
Index: templates/template_formation.xml
===================================================================
--- templates/template_formation.xml
+++ templates/template_formation.xml
@@ -54,7 +54,7 @@
upright
false
0
- 10
+ 9999
0.75