function Formation() {}
Formation.prototype.Schema =
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
""+
"2"+
""+
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"";
// 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.PI/2 - Math.atan2(vector.y, vector.x)
}
/**
* 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",
"memberPositions",
"maxRowsUsed",
"maxColumnsUsed",
"waitingOnController",
"columnar",
"rearrange",
"formationMembersWithAura",
"width",
"depth",
"twinFormations",
"formationSeparation",
"offsets"
];
Formation.prototype.Init = function(deserialized = false)
{
this.sortingClasses = this.template.SortingClasses.split(/\s+/g);
this.shiftRows = this.template.ShiftRows == "true";
this.separationMultiplier = {
"width": +this.template.UnitSeparationWidthMultiplier,
"depth": +this.template.UnitSeparationDepthMultiplier
};
this.sloppiness = +this.template.Sloppiness;
this.widthDepthRatio = +this.template.WidthDepthRatio;
this.minColumns = +(this.template.MinColumns || 0);
this.maxColumns = +(this.template.MaxColumns || 0);
this.maxRows = +(this.template.MaxRows || 0);
this.centerGap = +(this.template.CenterGap || 0);
if (this.template.AnimationVariants)
{
this.animationvariants = [];
let differentAnimationVariants = this.template.AnimationVariants.split(/\s*;\s*/);
// Loop over the different rectangulars that will map to different animation variants.
for (let rectAnimationVariant of differentAnimationVariants)
{
let rect, replacementAnimationVariant;
[rect, replacementAnimationVariant] = rectAnimationVariant.split(/\s*:\s*/);
let rows, columns;
[rows, columns] = rect.split(/\s*,\s*/);
let minRow, maxRow, minColumn, maxColumn;
[minRow, maxRow] = rows.split(/\s*\.\.\s*/);
[minColumn, maxColumn] = columns.split(/\s*\.\.\s*/);
this.animationvariants.push({
"minRow": +minRow,
"maxRow": +maxRow,
"minColumn": +minColumn,
"maxColumn": +maxColumn,
"name": replacementAnimationVariant
});
}
}
this.lastOrderVariant = undefined;
// Entity IDs currently belonging to this formation.
this.members = [];
this.memberPositions = {};
this.maxRowsUsed = 0;
this.maxColumnsUsed = [];
// Entities that are waiting on the controller.
this.waitingOnController = [];
// Whether we're travelling in column (vs box) formation.
this.columnar = false;
// Whether we should rearrange all formation members.
this.rearrange = true;
// Members with a formation aura.
this.formationMembersWithAura = [];
this.width = 0;
this.depth = 0;
this.twinFormations = [];
// Distance from which two twin formations will merge into one.
this.formationSeparation = 0;
if (deserialized)
return;
Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer)
.SetInterval(this.entity, IID_Formation, "ShapeUpdate", 1000, 1000, null);
};
Formation.prototype.Serialize = function()
{
let result = {};
for (let key of this.variablesToSerialize)
result[key] = this[key];
return result;
};
Formation.prototype.Deserialize = function(data)
{
this.Init(true);
for (let key in data)
this[key] = data[key];
};
/**
* Set the value from which two twin formations will become one.
*/
Formation.prototype.SetFormationSeparation = function(value)
{
this.formationSeparation = value;
};
Formation.prototype.GetSize = function()
{
return { "width": this.width, "depth": this.depth };
};
Formation.prototype.GetSpeedMultiplier = function()
{
return +this.template.SpeedMultiplier;
};
Formation.prototype.GetMemberCount = function()
{
return this.members.length;
};
Formation.prototype.GetMembers = function()
{
return this.members;
};
Formation.prototype.GetClosestMember = function(ent, filter)
{
let cmpEntPosition = Engine.QueryInterface(ent, IID_Position);
if (!cmpEntPosition || !cmpEntPosition.IsInWorld())
return INVALID_ENTITY;
let entPosition = cmpEntPosition.GetPosition2D();
let closestMember = INVALID_ENTITY;
let closestDistance = Infinity;
for (let member of this.members)
{
if (filter && !filter(ent))
continue;
let cmpPosition = Engine.QueryInterface(member, IID_Position);
if (!cmpPosition || !cmpPosition.IsInWorld())
continue;
let pos = cmpPosition.GetPosition2D();
let dist = entPosition.distanceToSquared(pos);
if (dist < closestDistance)
{
closestMember = member;
closestDistance = dist;
}
}
return closestMember;
};
/**
* Returns the 'primary' member of this formation (typically the most
* important unit type), for e.g. playing a representative sound.
* Returns undefined if no members.
* TODO: Actually implement something like that. Currently this just returns
* the arbitrary first one.
*/
Formation.prototype.GetPrimaryMember = function()
{
return this.members[0];
};
/**
* Get the formation animation variant for a certain member of this formation.
* @param entity The entity ID to get the animation for.
* @return The name of the animation variant as defined in the template,
* e.g. "testudo_front" or undefined if does not exist.
*/
Formation.prototype.GetFormationAnimationVariant = function(entity)
{
if (!this.animationvariants || !this.animationvariants.length || this.columnar || !this.memberPositions[entity])
return undefined;
let row = this.memberPositions[entity].row;
let column = this.memberPositions[entity].column;
for (let i = 0; i < this.animationvariants.length; ++i)
{
let minRow = this.animationvariants[i].minRow;
if (minRow < 0)
minRow += this.maxRowsUsed + 1;
if (row < minRow)
continue;
let maxRow = this.animationvariants[i].maxRow;
if (maxRow < 0)
maxRow += this.maxRowsUsed + 1;
if (row > maxRow)
continue;
let minColumn = this.animationvariants[i].minColumn;
if (minColumn < 0)
minColumn += this.maxColumnsUsed[row] + 1;
if (column < minColumn)
continue;
let maxColumn = this.animationvariants[i].maxColumn;
if (maxColumn < 0)
maxColumn += this.maxColumnsUsed[row] + 1;
if (column > maxColumn)
continue;
return this.animationvariants[i].name;
}
return undefined;
};
Formation.prototype.SetWaitingOnController = function(ent)
{
// Rotate the entity to the right angle.
let cmpPosition = Engine.QueryInterface(this.entity, IID_Position);
let cmpEntPosition = Engine.QueryInterface(ent, IID_Position);
if (cmpEntPosition && cmpEntPosition.IsInWorld() && cmpPosition && cmpPosition.IsInWorld())
cmpEntPosition.TurnTo(cmpPosition.GetRotation().y);
if (this.waitingOnController.indexOf(ent) !== -1)
return;
this.waitingOnController.push(ent);
};
Formation.prototype.UnsetWaitingOnController = function(ent)
{
let ind = this.waitingOnController.indexOf(ent);
if (ind !== -1)
this.waitingOnController.splice(ind, 1);
};
Formation.prototype.ResetWaitingEntities = function()
{
this.waitingOnController = [];
};
Formation.prototype.AreAllMembersWaiting = function()
{
return this.waitingOnController.length === this.members.length;
};
/**
* Set whether we are allowed to rearrange formation members.
*/
Formation.prototype.SetRearrange = function(rearrange)
{
this.rearrange = rearrange;
};
/**
* 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
(prevents disbanding of the formation when under the member limit).
*/
Formation.prototype.RemoveMembers = function(ents, renamed = false)
{
this.offsets = undefined;
this.members = this.members.filter(ent => ents.indexOf(ent) === -1);
this.waitingOnController = this.waitingOnController.filter(ent => ents.indexOf(ent) === -1);
for (let ent of ents)
{
let cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
cmpUnitAI.UpdateWorkOrders();
cmpUnitAI.SetFormationController(INVALID_ENTITY);
}
for (let ent of this.formationMembersWithAura)
{
let cmpAuras = Engine.QueryInterface(ent, IID_Auras);
cmpAuras.RemoveFormationAura(ents);
// The unit with the aura is also removed from the formation.
if (ents.indexOf(ent) !== -1)
cmpAuras.RemoveFormationAura(this.members);
}
this.formationMembersWithAura = this.formationMembersWithAura.filter(function(e) { return ents.indexOf(e) == -1; });
// If there's nobody left, destroy the formation
// unless this is a rename where we can have 0 members temporarily.
if (this.members.length < +this.template.RequiredMemberCount && !renamed)
{
this.Disband();
return;
}
this.ComputeMotionParameters();
if (!this.rearrange)
return;
// Rearrange the remaining members.
this.MoveMembersIntoFormation(true, true, this.lastOrderVariant);
};
Formation.prototype.AddMembers = function(ents)
{
this.offsets = undefined;
for (let ent of this.formationMembersWithAura)
{
let cmpAuras = Engine.QueryInterface(ent, IID_Auras);
cmpAuras.ApplyFormationAura(ents);
}
this.members = this.members.concat(ents);
for (let ent of ents)
{
let cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
cmpUnitAI.SetFormationController(this.entity);
if (!cmpUnitAI.GetOrders().length)
cmpUnitAI.SetNextState("FORMATIONMEMBER.IDLE");
let cmpAuras = Engine.QueryInterface(ent, IID_Auras);
if (cmpAuras && cmpAuras.HasFormationAura())
{
this.formationMembersWithAura.push(ent);
cmpAuras.ApplyFormationAura(this.members);
}
}
this.ComputeMotionParameters();
if (!this.rearrange)
return;
this.MoveMembersIntoFormation(true, true, this.lastOrderVariant);
};
/**
* Remove all members and destroy the formation.
*/
Formation.prototype.Disband = function()
{
for (let ent of this.members)
{
let cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
cmpUnitAI.SetFormationController(INVALID_ENTITY);
}
for (let ent of this.formationMembersWithAura)
{
let cmpAuras = Engine.QueryInterface(ent, IID_Auras);
cmpAuras.RemoveFormationAura(this.members);
}
this.members = [];
this.waitingOnController = [];
this.formationMembersWithAura = [];
this.offsets = undefined;
let cmpUnitAI = Engine.QueryInterface(this.entity, IID_UnitAI);
// Hack: switch to a clean state to stop timers.
cmpUnitAI.UnitFsm.SwitchToNextState(cmpUnitAI, "");
Engine.DestroyEntity(this.entity);
};
/**
* Set all members to form up into the formation shape.
* @param {boolean} moveCenter - The formation center will be reinitialized
* to the center of the units.
* @param {boolean} force - All individual orders of the formation units are replaced,
* 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)
{
if (!this.members.length)
return;
let active = [];
let positions = [];
for (let ent of this.members)
{
let cmpPosition = Engine.QueryInterface(ent, IID_Position);
if (!cmpPosition || !cmpPosition.IsInWorld())
continue;
active.push(ent);
// Query the 2D position as the exact height calculation isn't needed,
// but bring the position to the correct coordinates.
positions.push(cmpPosition.GetPosition2D());
}
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 || (cmpFormationPosition && !cmpFormationPosition.IsInWorld()))
{
let old_rotation = cmpFormationPosition.GetRotation().y;
let avgpos = Vector2D.average(positions);
cmpFormationPosition.JumpTo(avgpos.x, avgpos.y);
// 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));
cmpFormationPosition.TurnTo(direction);
let projection = new Vector2D(formationProjectionDistance * Math.cos(direction),
formationProjectionDistance * Math.sin(direction));
goto_position = avgpos.add(projection);
sharp_turn_flag = !areAnglesSimilar(direction, old_rotation);
}
this.lastOrderVariant = variant;
let offsetsChanged = false;
if (!this.offsets || sharp_turn_flag)
{
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;
for (let offset of this.offsets)
{
let cmpUnitAI = Engine.QueryInterface(offset.ent, IID_UnitAI);
if (!cmpUnitAI)
{
warn("Entities without UnitAI in formation are not supported.");
continue;
}
let data =
{
"target": this.entity,
"x": offset.x,
"z": offset.y,
"offsetsChanged": offsetsChanged,
"variant": variant
};
cmpUnitAI.AddOrder("FormationWalk", data, !force);
xMax = Math.max(xMax, offset.x);
yMax = Math.max(yMax, offset.y);
xMin = Math.min(xMin, offset.x);
yMin = Math.min(yMin, offset.y);
}
this.width = xMax - xMin;
this.depth = yMax - yMin;
};
Formation.prototype.GetAvgFootprint = function(active)
{
let footprints = [];
for (let ent of active)
{
let cmpFootprint = Engine.QueryInterface(ent, IID_Footprint);
if (cmpFootprint)
footprints.push(cmpFootprint.GetShape());
}
if (!footprints.length)
return { "width": 1, "depth": 1 };
let r = { "width": 0, "depth": 0 };
for (let shape of footprints)
{
if (shape.type == "circle")
{
r.width += shape.radius * 2;
r.depth += shape.radius * 2;
}
else if (shape.type == "square")
{
r.width += shape.width;
r.depth += shape.depth;
}
}
r.width /= footprints.length;
r.depth /= footprints.length;
return r;
};
/**
* 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 = this.sortingClasses.slice();
sortingClasses.push("Unknown");
// types is an object that splits the entities to be assigned to offsets,
// based on their classes
// TODO annullare qua
let types = {};
for (let cls of sortingClasses)
types[cls] = [];
for (let i in active)
{
let cmpIdentity = Engine.QueryInterface(active[i], IID_Identity);
let classes = cmpIdentity.GetClassesList();
let done = false;
for (let cls of sortingClasses.slice(0, -1))
{
if (classes.indexOf(cls) > -1)
{
types[cls].push({ "ent": active[i], "pos": positions[i] });
done = true;
break;
}
}
if (!done)
types.Unknown.push({ "ent": active[i], "pos": positions[i] });
}
let count = active.length;
let shape = this.template.FormationShape;
let shiftRows = this.shiftRows;
let centerGap = this.centerGap;
let sortingOrder = this.template.SortingOrder;
let offsets = [];
// Choose a sensible size/shape for the various formations, depending on number of units.
let cols;
{
let depth = Math.sqrt(count / this.widthDepthRatio);
if (this.maxRows && depth > this.maxRows)
depth = this.maxRows;
cols = Math.ceil(count / Math.ceil(depth) + (this.shiftRows ? 0.5 : 0));
if (cols < this.minColumns)
cols = Math.min(count, this.minColumns);
if (this.maxColumns && cols > this.maxColumns && this.maxRows != depth)
cols = this.maxColumns;
}
// Define special formations here.
if (this.template.FormationName == "Scatter")
{
let width = Math.sqrt(count) * (separation.width + separation.depth) * 2.5;
for (let i = 0; i < count; ++i)
{
let obj = new Vector2D(randFloat(0, width), randFloat(0, width));
obj.row = 1;
obj.column = i + 1;
offsets.push(obj);
}
}
// For non-special formations, calculate the positions based on the number of entities.
this.maxColumnsUsed = [];
this.maxRowsUsed = 0;
if (shape != "special")
{
offsets = [];
let r = 0;
let left = count;
// While there are units left, start a new row in the formation.
while (left > 0)
{
// Save the position of the row.
let z = -r * separation.depth;
// Alternate between the left and right side of the center to have a symmetrical distribution.
let side = 1;
let n;
// Determine the number of entities in this row of the formation.
if (shape == "square")
{
n = cols;
if (shiftRows)
n -= r % 2;
}
else if (shape == "triangle")
{
if (shiftRows)
n = r + 1;
else
n = r * 2 + 1;
}
if (!shiftRows && n > left)
n = left;
for (let c = 0; c < n && left > 0; ++c)
{
// Switch sides for the next entity.
side *= -1;
let x;
if (n % 2 == 0)
x = side * (Math.floor(c / 2) + 0.5) * separation.width;
else
x = side * Math.ceil(c / 2) * separation.width;
if (centerGap)
{
// Don't use the center position with a center gap.
if (x == 0)
continue;
x += side * centerGap / 2;
}
let column = Math.ceil(n / 2) + Math.ceil(c / 2) * side;
let r1 = randFloat(-1, 1) * this.sloppiness;
let r2 = randFloat(-1, 1) * this.sloppiness;
offsets.push(new Vector2D(x + r1, z + r2));
offsets[offsets.length - 1].row = r + 1;
offsets[offsets.length - 1].column = column;
left--;
}
++r;
this.maxColumnsUsed[r] = n;
}
this.maxRowsUsed = r;
}
// Make sure the average offset is zero, as the formation is centered around that
// calculating offset distances without a zero average makes no sense, as the formation
// will jump to a different position any time.
let avgoffset = Vector2D.average(offsets);
offsets.forEach(function(o) {o.sub(avgoffset);});
// Sort the available places in certain ways.
// The places first in the list will contain the heaviest units as defined by the order
// of the types list.
if (sortingOrder == "fillFromTheSides")
offsets.sort(function(o1, o2) { return Math.abs(o1.x) < Math.abs(o2.x);});
else if (sortingOrder == "fillToTheCenter")
offsets.sort(function(o1, o2) {
return Math.max(Math.abs(o1.x), Math.abs(o1.y)) < Math.max(Math.abs(o2.x), Math.abs(o2.y));
});
//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 = [];
for (let cls of sortingClasses.reverse())
{
let unit_list = types[cls];
if (!unit_list.length)
continue;
// 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);
newOffsets.push(usedOffsets.splice(closestOffsetId, 1)[0]);
newOffsets[newOffsets.length - 1].ent = entPos.ent;
}
}
return newOffsets;
};
/**
* 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.
* @param offsets
* @return The index of the closest offset position.
*/
Formation.prototype.TakeClosestOffset = function(entPos, realPositions, offsets)
{
let pos = entPos.pos;
let closestOffsetId = -1;
let offsetDistanceSq = Infinity;
for (let i in realPositions)
{
let distSq = pos.distanceToSquared(realPositions[i]);
if (distSq < offsetDistanceSq)
{
offsetDistanceSq = distSq;
closestOffsetId = i;
}
}
this.memberPositions[entPos.ent] = { "row": offsets[closestOffsetId].row, "column": offsets[closestOffsetId].column };
return closestOffsetId;
};
/**
* Set formation controller's speed based on its current members.
*/
Formation.prototype.ComputeMotionParameters = function()
{
let maxRadius = 0;
let minSpeed = Infinity;
for (let ent of this.members)
{
let cmpUnitMotion = Engine.QueryInterface(ent, IID_UnitMotion);
if (cmpUnitMotion)
minSpeed = Math.min(minSpeed, cmpUnitMotion.GetWalkSpeed());
}
minSpeed *= this.GetSpeedMultiplier();
let cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion);
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;
for (let i = this.twinFormations.length - 1; i >= 0; --i)
{
// Only do the check on one side.
if (this.twinFormations[i] <= this.entity)
continue;
let cmpPosition = Engine.QueryInterface(this.entity, IID_Position);
let cmpOtherPosition = Engine.QueryInterface(this.twinFormations[i], IID_Position);
let cmpOtherFormation = Engine.QueryInterface(this.twinFormations[i], IID_Formation);
if (!cmpPosition || !cmpOtherPosition || !cmpOtherFormation ||
!cmpPosition.IsInWorld() || !cmpOtherPosition.IsInWorld())
continue;
let thisPosition = cmpPosition.GetPosition2D();
let otherPosition = cmpOtherPosition.GetPosition2D();
let dx = thisPosition.x - otherPosition.x;
let dy = thisPosition.y - otherPosition.y;
let dist = Math.sqrt(dx * dx + dy * dy);
let thisSize = this.GetSize();
let otherSize = cmpOtherFormation.GetSize();
let 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.
let otherMembers = cmpOtherFormation.members;
cmpOtherFormation.RemoveMembers(otherMembers);
this.AddMembers(otherMembers);
Engine.DestroyEntity(this.twinFormations[i]);
this.twinFormations.splice(i, 1);
}
};
Formation.prototype.ResetOrderVariant = function()
{
this.lastOrderVariant = undefined;
};
Formation.prototype.OnGlobalOwnershipChanged = function(msg)
{
// When an entity is captured or destroyed, it should no longer be
// controlled by this formation.
if (this.members.indexOf(msg.entity) != -1)
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)
return;
let waitingIndex = this.waitingOnController.indexOf(msg.entity);
if (waitingIndex !== -1)
this.waitingOnController.splice(waitingIndex, 1, msg.newentity);
// Save rearranging to temporarily set it to false.
let temp = this.rearrange;
this.rearrange = false;
// First remove the old member to be able to reuse its position.
this.RemoveMembers([msg.entity], true);
this.AddMembers([msg.newentity]);
this.memberPositions[msg.newentity] = this.memberPositions[msg.entity];
this.rearrange = temp;
};
Formation.prototype.RegisterTwinFormation = function(entity)
{
let cmpFormation = Engine.QueryInterface(entity, IID_Formation);
if (!cmpFormation)
return;
this.twinFormations.push(entity);
cmpFormation.twinFormations.push(this.entity);
};
Formation.prototype.DeleteTwinFormations = function()
{
for (let ent of this.twinFormations)
{
let cmpFormation = Engine.QueryInterface(ent, IID_Formation);
if (cmpFormation)
cmpFormation.twinFormations.splice(cmpFormation.twinFormations.indexOf(this.entity), 1);
}
this.twinFormations = [];
};
Formation.prototype.LoadFormation = function(newTemplate)
{
// Get the old formation info.
let members = this.members.slice();
let cmpThisUnitAI = Engine.QueryInterface(this.entity, IID_UnitAI);
let orders = cmpThisUnitAI.GetOrders().slice();
this.Disband();
let newFormation = Engine.AddEntity(newTemplate);
// Apply the info from the old formation to the new one.
let cmpNewOwnership = Engine.QueryInterface(newFormation, IID_Ownership);
let cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership);
if (cmpOwnership && cmpNewOwnership)
cmpNewOwnership.SetOwner(cmpOwnership.GetOwner());
let cmpNewPosition = Engine.QueryInterface(newFormation, IID_Position);
let cmpPosition = Engine.QueryInterface(this.entity, IID_Position);
if (cmpPosition && cmpPosition.IsInWorld() && cmpNewPosition)
cmpNewPosition.TurnTo(cmpPosition.GetRotation().y);
let cmpFormation = Engine.QueryInterface(newFormation, IID_Formation);
let cmpNewUnitAI = Engine.QueryInterface(newFormation, IID_UnitAI);
cmpFormation.SetMembers(members);
if (orders.length)
cmpNewUnitAI.AddOrders(orders);
else
cmpNewUnitAI.MoveIntoFormation();
Engine.PostMessage(this.entity, MT_EntityRenamed, { "entity": this.entity, "newentity": newFormation });
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);