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 @@ -73,6 +73,9 @@ // Distance at which we'll switch between column/box formations. var g_ColumnDistanceThreshold = 128; +// Turning angle over which formation positions are recomputed. +Formation.prototype.turningAngleThreshold = Math.PI / 4; + Formation.prototype.variablesToSerialize = [ "lastOrderVariant", "members", @@ -496,7 +499,6 @@ let active = []; let positions = []; - let rotations = 0; for (let ent of this.members) { @@ -508,30 +510,41 @@ // 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 sharpTurnFlag = false; 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 walkingDistance = cmpFormationUnitAI.ComputeWalkingDistance(); + const columnar = walkingDistance > g_ColumnDistanceThreshold; + if (columnar != this.columnar) + { + this.columnar = columnar; + this.offsets = undefined; + } + let newRotation = oldRotation; + const targetPosition = cmpFormationUnitAI.GetTargetPositions()[0]; + if (targetPosition === undefined) + newRotation = oldRotation; + else + newRotation = avgpos.angleTo(targetPosition); + + cmpPosition.TurnTo(newRotation); + sharpTurnFlag = !this.areAnglesSimilar(newRotation, oldRotation); } + this.lastOrderVariant = variant; + let offsetsChanged = false; - let newOrientation = this.GetEstimatedOrientation(avgpos); - if (!this.offsets) + if (!this.offsets || sharpTurnFlag) { this.offsets = this.ComputeFormationOffsets(active, positions); offsetsChanged = true; @@ -807,16 +820,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 +872,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 +888,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 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. */ -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); + return d < this.turningAngleThreshold || d > 2 * Math.PI - this.turningAngleThreshold; }; /** @@ -893,7 +904,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 }),