Index: Formation.js =================================================================== --- Formation.js +++ Formation.js @@ -70,9 +70,37 @@ "" + ""; -// 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; + +// distance at which the position of the formation is projected for ComputeFormationOffsets +// to decide the order at which units are assigned to formation positions +const formationProjectionDistance = 0; + +/** + * 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.atan2(vector.x, vector.y) +} + +/** + * 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 +351,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 @@ -471,15 +469,13 @@ * otherwise the order to walk into formation is just pushed to the front. * @param {string | undefined} variant - Variant to be passed as order parameter. */ -Formation.prototype.MoveMembersIntoFormation = function(moveCenter, force, variant) +Formation.prototype.MoveMembersIntoFormation = function(moveCenter, force, variant, reorient = true) { 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 +486,63 @@ // 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); + let goto_position = cmpFormationPosition.GetPosition2D(); // to be passed to ComputeFormationOffsets + // 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)); + + + let projection = new Vector2D(formationProjectionDistance * Math.cos(direction), + formationProjectionDistance * Math.sin(direction)); + goto_position = avgpos.add(projection); + + if (reorient) + { + cmpFormationPosition.TurnTo(direction); + warn('angles difference: ' + (direction - old_rotation)); + sharp_turn_flag = !areAnglesSimilar(direction, old_rotation); + } + else + cmpFormationPosition.TurnTo(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); + this.offsets = this.ComputeFormationOffsets(active, positions, goto_position); 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 +564,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,24 +600,30 @@ return r; }; -Formation.prototype.ComputeFormationOffsets = function(active, positions) +/** + * 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. + * @param goto_position - position that the formation is expected to occupy in a brief time + * span (the time at which all units will have reached the formation). Requested by the + * algorithm that assignes the units to the offsets, for better efficiency. + */ +Formation.prototype.ComputeFormationOffsets = function(active, positions, goto_position) { let separation = this.GetAvgFootprint(active); 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 +630,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 +654,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 +758,27 @@ 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 targetPosition = Engine.QueryInterface(this.entity, IID_UnitAI).GetTargetPositions()[0] + let realPositions = this.GetRealOffsetPositions(offsets, goto_position); + + // 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) + + // giving precedence to units further from the target position increases the + // efficiency of the movement into formation in a good range of scenarios + unit_list.sort(function(o1, o2) { + o2.pos.distanceToSquared(goto_position) - o1.pos.distanceToSquared(goto_position); + }); + + 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 +791,22 @@ }; /** + * Get the world positions for a list of offsets in this formation. + */ +Formation.prototype.GetRealOffsetPositions = function(offsets, formation_center) +{ + let rot = Engine.QueryInterface(this.entity, IID_Position).GetRotation().y; + let [ sin, cos ] = [Math.sin(rot), Math.cos(rot)]; + + let offsetPositions = []; + for (let o of offsets) + offsetPositions.push(new Vector2D(o.y * sin + o.x * cos, + o.y * cos - o.x * sin).add(formation_center)); + + 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 +818,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 +832,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 +851,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 +895,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 +911,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 +992,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);