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