mirror of
https://gitea.wildfiregames.com/0ad/0ad.git
synced 2026-06-21 03:06:29 +00:00
Group units in clusters of separate formations when they're too far apart.
This was SVN commit r14483.
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user