Index: binaries/data/mods/public/gui/credits/texts/programming.json
===================================================================
--- binaries/data/mods/public/gui/credits/texts/programming.json
+++ binaries/data/mods/public/gui/credits/texts/programming.json
@@ -29,6 +29,7 @@
{ "nick": "Alan", "name": "Alan Kemp" },
{ "nick": "Alex", "name": "Alexander Yakobovich" },
{ "nick": "alpha123", "name": "Peter P. Cannici" },
+ { "nick": "alre" },
{ "nick": "Ampaex", "name": "Antonio Vazquez" },
{ "name": "André Puel" },
{ "nick": "andy5995", "name": "Andy Alt" },
Index: binaries/data/mods/public/simulation/components/Formation.js
===================================================================
--- binaries/data/mods/public/simulation/components/Formation.js
+++ binaries/data/mods/public/simulation/components/Formation.js
@@ -23,6 +23,9 @@
"" +
"" +
"" +
+ "" +
+ "" +
+ "" +
"" +
"" +
"" +
@@ -73,6 +76,9 @@
// Distance at which we'll switch between column/box formations.
var g_ColumnDistanceThreshold = 128;
+// Distance under which the formation will not try to turn towards the target position.
+var g_RotateDistanceThreshold = 1;
+
Formation.prototype.variablesToSerialize = [
"lastOrderVariant",
"members",
@@ -93,6 +99,7 @@
Formation.prototype.Init = function(deserialized = false)
{
+ this.maxTurningAngle = +this.template.MaxTurningAngle;
this.sortingClasses = this.template.SortingClasses.split(/\s+/g);
this.shiftRows = this.template.ShiftRows == "true";
this.separationMultiplier = {
@@ -496,7 +503,6 @@
let active = [];
let positions = [];
- let rotations = 0;
for (let ent of this.members)
{
@@ -508,29 +514,37 @@
// 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);
// 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);
+ {
+ const oldRotation = cmpPosition.GetRotation().y;
+ const avgpos = Vector2D.average(positions);
- 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;
+ // Switch between column and box if necessary.
+ const cmpFormationUnitAI = Engine.QueryInterface(this.entity, IID_UnitAI);
+ const columnar = cmpFormationUnitAI.ComputeWalkingDistance() > g_ColumnDistanceThreshold;
+ if (columnar != this.columnar)
+ {
+ this.columnar = columnar;
+ this.offsets = undefined;
+ }
+
+ let newRotation = oldRotation;
+ const targetPosition = cmpFormationUnitAI.GetTargetPositions()[0];
+ if (targetPosition !== undefined && avgpos.distanceToSquared(targetPosition) > g_RotateDistanceThreshold)
+ newRotation = avgpos.angleTo(targetPosition);
+
+ cmpPosition.TurnTo(newRotation);
+ if (!this.areAnglesSimilar(newRotation, oldRotation))
+ this.offsets = undefined;
}
+ this.lastOrderVariant = variant;
+
let offsetsChanged = false;
- let newOrientation = this.GetEstimatedOrientation(avgpos);
if (!this.offsets)
{
this.offsets = this.ComputeFormationOffsets(active, positions);
@@ -807,16 +821,14 @@
});
// Query the 2D position of the formation.
- let cmpPosition = Engine.QueryInterface(this.entity, IID_Position);
- let formationPos = cmpPosition.GetPosition2D();
+ const realPositions = this.GetRealOffsetPositions(offsets);
// Use realistic 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 (const i of sortingClasses.reverse())
{
- let t = types[sortingClasses[i - 1]];
+ const t = types[i];
if (!t.length)
continue;
let usedOffsets = offsets.splice(-t.length);
@@ -861,10 +873,14 @@
/**
* Get the world positions for a list of offsets in this formation.
*/
-Formation.prototype.GetRealOffsetPositions = function(offsets, pos)
+Formation.prototype.GetRealOffsetPositions = function(offsets)
{
+ const cmpPosition = Engine.QueryInterface(this.entity, IID_Position);
+ const pos = cmpPosition.GetPosition2D();
+ const rot = cmpPosition.GetRotation().y;
+ const sin = Math.sin(rot);
+ const cos = Math.cos(rot);
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));
@@ -873,19 +889,15 @@
};
/**
- * Calculate the estimated rotation of the formation based on the current rotation.
- * Return the sine and cosine of the angle.
+ * Returns true if the two given angles (in radians)
+ * are smaller than the maximum turning angle of the formation and therfore allow
+ * the formation turn without reassigning positions.
*/
-Formation.prototype.GetEstimatedOrientation = function(pos)
+
+Formation.prototype.areAnglesSimilar = function(a1, a2)
{
- 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;
+ const d = Math.abs(a1 - a2) % 2 * Math.PI;
+ return d < this.maxTurningAngle || d > 2 * Math.PI - this.maxTurningAngle;
};
/**
@@ -893,7 +905,6 @@
*/
Formation.prototype.ComputeMotionParameters = function()
{
- let maxRadius = 0;
let minSpeed = Infinity;
let minAcceleration = Infinity;
Index: binaries/data/mods/public/simulation/components/tests/test_UnitAI.js
===================================================================
--- binaries/data/mods/public/simulation/components/tests/test_UnitAI.js
+++ binaries/data/mods/public/simulation/components/tests/test_UnitAI.js
@@ -234,6 +234,7 @@
AddMock(controller, IID_Position, {
"JumpTo": function(x, z) { this.x = x; this.z = z; },
+ "TurnTo": function() {},
"GetTurretParent": function() { return INVALID_ENTITY; },
"GetPosition": function() { return new Vector3D(this.x, 0, this.z); },
"GetPosition2D": function() { return new Vector2D(this.x, this.z); },
@@ -409,6 +410,7 @@
AddMock(controller, IID_Position, {
"GetTurretParent": () => INVALID_ENTITY,
"JumpTo": function(x, z) { this.x = x; this.z = z; },
+ "TurnTo": function() {},
"GetPosition": function(){ return new Vector3D(this.x, 0, this.z); },
"GetPosition2D": function(){ return new Vector2D(this.x, this.z); },
"GetRotation": () => ({ "y": 0 }),
Index: binaries/data/mods/public/simulation/templates/template_formation.xml
===================================================================
--- binaries/data/mods/public/simulation/templates/template_formation.xml
+++ binaries/data/mods/public/simulation/templates/template_formation.xml
@@ -20,6 +20,7 @@
Requires at least 2 Soldiers or Siege Engines.
1
square
+ 0.785
Hero Champion Cavalry Melee Ranged
false
1