diff --git a/binaries/data/mods/public/simulation/components/Formation.js b/binaries/data/mods/public/simulation/components/Formation.js index 9272049fc3..a2b6b917cc 100644 --- a/binaries/data/mods/public/simulation/components/Formation.js +++ b/binaries/data/mods/public/simulation/components/Formation.js @@ -16,6 +16,19 @@ Formation.prototype.Init = function() this.width = 0; this.depth = 0; this.oldOrientation = {"sin": 0, "cos": 0}; + this.twinFormations = []; + // distance from which two twin formations will merge into one. + this.formationSeparation = 0; + Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer) + .SetInterval(this.entity, IID_Formation, "ShapeUpdate", 1000, 1000, null); +}; + +/** + * Set the value from which two twin formations will become one. + */ +Formation.prototype.SetFormationSeparation = function(value) +{ + this.formationSeparation = value; }; Formation.prototype.GetSize = function() @@ -157,6 +170,39 @@ Formation.prototype.RemoveMembers = function(ents) this.MoveMembersIntoFormation(true, true); }; +Formation.prototype.AddMembers = function(ents) +{ + this.offsets = undefined; + this.inPosition = []; + + for each (var ent in this.formationMembersWithAura) + { + var cmpAuras = Engine.QueryInterface(ent, IID_Auras); + cmpAuras.RemoveFormationBonus(ents); + + // the unit with the aura is also removed from the formation + if (ents.indexOf(ent) !== -1) + cmpAuras.RemoveFormationBonus(this.members); + } + + this.members = this.members.concat(ents); + + for each (var ent in this.members) + { + var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI); + cmpUnitAI.SetFormationController(this.entity); + + var cmpAuras = Engine.QueryInterface(ent, IID_Auras); + if (cmpAuras && cmpAuras.HasFormationAura()) + { + this.formationMembersWithAura.push(ent); + cmpAuras.ApplyFormationBonus(ents); + } + } + + this.MoveMembersIntoFormation(true, true); +}; + /** * Called when the formation stops moving in order to detect * units that have already reached their final positions. @@ -293,6 +339,16 @@ Formation.prototype.MoveMembersIntoFormation = function(moveCenter, force) } } + // Switch between column and box if necessary + var cmpUnitAI = Engine.QueryInterface(this.entity, IID_UnitAI); + var walkingDistance = cmpUnitAI.ComputeWalkingDistance(); + var columnar = walkingDistance > g_ColumnDistanceThreshold; + if (columnar != this.columnar) + { + this.columnar = columnar; + this.offsets = undefined; + } + var newOrientation = this.GetTargetOrientation(avgpos); var dSin = Math.abs(newOrientation.sin - this.oldOrientation.sin); var dCos = Math.abs(newOrientation.cos - this.oldOrientation.cos); @@ -850,11 +906,45 @@ Formation.prototype.ComputeMotionParameters = function() // TODO: we also need to do something about PassabilityClass, CostClass }; -Formation.prototype.OnUpdate_Final = function(msg) +Formation.prototype.ShapeUpdate = function() { + // Check the distance to twin formations, and merge if when + // the formations could collide + for (var i = this.twinFormations.length - 1; i >= 0; --i) + { + // only do the check on one side + if (this.twinFormations[i] <= this.entity) + continue; + + var cmpPosition = Engine.QueryInterface(this.entity, IID_Position); + var cmpOtherPosition = Engine.QueryInterface(this.twinFormations[i], IID_Position); + var cmpOtherFormation = Engine.QueryInterface(this.twinFormations[i], IID_Formation); + if (!cmpPosition || !cmpOtherPosition || !cmpOtherFormation) + continue; + + var thisPosition = cmpPosition.GetPosition2D(); + var otherPosition = cmpOtherPosition.GetPosition2D(); + var dx = thisPosition.x - otherPosition.x; + var dy = thisPosition.y - otherPosition.y; + var dist = Math.sqrt(dx * dx + dy * dy); + + var thisSize = this.GetSize(); + var otherSize = cmpOtherFormation.GetSize(); + var minDist = Math.max(thisSize.width / 2, thisSize.depth / 2) + + Math.max(otherSize.width / 2, otherSize.depth / 2) + + this.formationSeparation; + + if (minDist < dist) + continue; + + // merge the members from the twin formation into this one + // twin formations should always have exactly the same orders + this.AddMembers(cmpOtherFormation.members); + Engine.DestroyEntity(this.twinFormations[i]); + this.twinFormations.splice(i,1); + } // Switch between column and box if necessary var cmpUnitAI = Engine.QueryInterface(this.entity, IID_UnitAI); - // TODO do we really need to calculate the distance every turn? var walkingDistance = cmpUnitAI.ComputeWalkingDistance(); var columnar = walkingDistance > g_ColumnDistanceThreshold; if (columnar != this.columnar) @@ -897,6 +987,26 @@ Formation.prototype.OnGlobalEntityRenamed = function(msg) } } +Formation.prototype.RegisterTwinFormation = function(entity) +{ + var cmpFormation = Engine.QueryInterface(entity, IID_Formation); + if (!cmpFormation) + return; + this.twinFormations.push(entity); + cmpFormation.twinFormations.push(this.entity); +}; + +Formation.prototype.DeleteTwinFormations = function() +{ + for each (var ent in this.twinFormations) + { + var cmpFormation = Engine.QueryInterface(ent, IID_Formation); + if (cmpFormation) + cmpFormation.twinFormations.splice(cmpFormation.twinFormations.indexOf(this.entity), 1); + } + this.twinFormations = []; +}; + Formation.prototype.LoadFormation = function(formationName) { this.formationName = formationName; diff --git a/binaries/data/mods/public/simulation/helpers/Commands.js b/binaries/data/mods/public/simulation/helpers/Commands.js index 48d3872998..fa3ef0d921 100644 --- a/binaries/data/mods/public/simulation/helpers/Commands.js +++ b/binaries/data/mods/public/simulation/helpers/Commands.js @@ -1202,7 +1202,7 @@ function GetFormationUnitAIs(ents, player, formName) // Find what formations the formationable selected entities are currently in var formation = ExtractFormations(formedEnts); - var formationEnt = undefined; + var formationUnitAIs = []; if (formation.ids.length == 1) { // Selected units either belong to this formation or have no formation @@ -1212,12 +1212,12 @@ function GetFormationUnitAIs(ents, player, formName) if (cmpFormation && cmpFormation.GetMemberCount() == formation.members[fid].length && cmpFormation.GetMemberCount() == formation.entities.length) { + cmpFormation.DeleteTwinFormations(); // The whole formation was selected, so reuse its controller for this command - formationEnt = +fid; + formationUnitAIs = [Engine.QueryInterface(+fid, IID_UnitAI)]; } } - - if (!formationEnt) + if (!formationUnitAIs.length) { // We need to give the selected units a new formation controller @@ -1228,52 +1228,141 @@ function GetFormationUnitAIs(ents, player, formName) if (cmpFormation) cmpFormation.RemoveMembers(formation.members[fid]); } - - // Create the new controller - formationEnt = Engine.AddEntity("special/formation"); - var cmpFormation = Engine.QueryInterface(formationEnt, IID_Formation); - cmpFormation.SetMembers(formation.entities); - - var cmpOwnership = Engine.QueryInterface(formationEnt, IID_Ownership); - cmpOwnership.SetOwner(player); - - // If all the selected units were previously in formations of the same shape, - // then set this new formation to that shape too; otherwise use the default shape - var lastFormationName = undefined; - for each (var ent in formation.entities) + // TODO replace the fixed 60 with something sensible, based on vision range f.e. + var formationSeparation = 60; + var clusters = ClusterEntities(formation.entities, formationSeparation); + var formationEnts = []; + for each (var cluster in clusters) { - var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI); - if (cmpUnitAI) + // Create the new controller + var formationEnt = Engine.AddEntity("special/formation"); + var cmpFormation = Engine.QueryInterface(formationEnt, IID_Formation); + formationUnitAIs.push(Engine.QueryInterface(formationEnt, IID_UnitAI)); + cmpFormation.SetFormationSeparation(formationSeparation); + cmpFormation.SetMembers(cluster); + + for each (var ent in formationEnts) + cmpFormation.RegisterTwinFormation(ent); + + formationEnts.push(formationEnt); + var cmpOwnership = Engine.QueryInterface(formationEnt, IID_Ownership); + cmpOwnership.SetOwner(player); + + // If all the selected units were previously in formations of the same shape, + // then set this new formation to that shape too; otherwise use the default shape + var lastFormationName = undefined; + for each (var ent in cluster) { - var name = cmpUnitAI.GetLastFormationName(); - if (lastFormationName === undefined) + var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI); + if (cmpUnitAI) { - lastFormationName = name; - } - else if (lastFormationName != name) - { - lastFormationName = undefined; - break; + var name = cmpUnitAI.GetLastFormationName(); + if (lastFormationName === undefined) + { + lastFormationName = name; + } + else if (lastFormationName != name) + { + lastFormationName = undefined; + break; + } } } - } - var formationName; - if (lastFormationName) - formationName = lastFormationName; - else - formationName = "Line Closed"; + var formationName; + if (lastFormationName) + formationName = lastFormationName; + else + formationName = "Line Closed"; - if (CanMoveEntsIntoFormation(formation.entities, formationName)) - { - cmpFormation.LoadFormation(formationName); - } - else - { - cmpFormation.LoadFormation("Scatter"); + if (CanMoveEntsIntoFormation(cluster, formationName)) + cmpFormation.LoadFormation(formationName); + else + cmpFormation.LoadFormation("Scatter"); } } - return nonformedUnitAIs.concat(Engine.QueryInterface(formationEnt, IID_UnitAI)); + return nonformedUnitAIs.concat(formationUnitAIs); +} + +/** + * Group a list of entities in clusters via single-links + */ +function ClusterEntities(ents, separationDistance) +{ + var clusters = []; + if (!ents.length) + return clusters; + + var distSq = separationDistance * separationDistance; + var positions = []; + // triangular matrix with the (squared) distances between the different clusters + // the other half is not initialised + var matrix = []; + for (var i = 0; i < ents.length; i++) + { + matrix[i] = []; + clusters.push([ents[i]]); + var cmpPosition = Engine.QueryInterface(ents[i], IID_Position); + if (!cmpPosition) + { + error("Asked to cluster entities without position: "+ents[i]); + return clusters; + } + positions.push(cmpPosition.GetPosition2D()); + for (var j = 0; j < i; j++) + { + var dx = positions[i].x - positions[j].x; + var dy = positions[i].y - positions[j].y; + matrix[i][j] = dx * dx + dy * dy; + } + } + while (clusters.length > 1) + { + // search two clusters that are closer than the required distance + var smallDist = Infinity; + var closeClusters = undefined; + + for (var i = matrix.length - 1; i >= 0 && !closeClusters; --i) + for (var j = i - 1; j >= 0 && !closeClusters; --j) + if (matrix[i][j] < distSq) + closeClusters = [i,j]; + + // if no more close clusters found, just return all found clusters so far + if (!closeClusters) + return clusters; + + // make a new cluster with the entities from the two found clusters + var newCluster = clusters[closeClusters[0]].concat(clusters[closeClusters[1]]); + + // calculate the minimum distance between the new cluster and all other remaining + // clusters by taking the minimum of the two distances. + var distances = []; + for (var i = 0; i < clusters.length; i++) + { + if (i == closeClusters[1] || i == closeClusters[0]) + continue; + var dist1 = matrix[closeClusters[1]][i] || matrix[i][closeClusters[1]]; + var dist2 = matrix[closeClusters[0]][i] || matrix[i][closeClusters[0]]; + distances.push(Math.min(dist1, dist2)); + } + // remove the rows and columns in the matrix for the merged clusters, + // and the clusters themselves from the cluster list + clusters.splice(closeClusters[0],1); + clusters.splice(closeClusters[1],1); + matrix.splice(closeClusters[0],1); + matrix.splice(closeClusters[1],1); + for (var i = 0; i < matrix.length; i++) + { + if (matrix[i].length > closeClusters[0]) + matrix[i].splice(closeClusters[0],1); + if (matrix[i].length > closeClusters[1]) + matrix[i].splice(closeClusters[1],1); + } + // add a new row of distances to the matrix and the new cluster + clusters.push(newCluster); + matrix.push(distances); + } + return clusters; } function GetFormationRequirements(formationName)