1 function Formation() {}
3 Formation.prototype.Schema =
4 "<element name='RequiredMemberCount' a:help='Minimum number of entities the formation should contain (at least 2).'>" +
5 "<data type='integer'>" +
6 "<param name='minInclusive'>"+
11 "<element name='DisabledTooltip' a:help='Tooltip shown when the formation is disabled.'>" +
14 "<element name='SpeedMultiplier' a:help='The speed of the formation is determined by the minimum speed of all members, multiplied with this number.'>" +
15 "<ref name='nonNegativeDecimal'/>" +
17 "<element name='FormationShape' a:help='Formation shape, currently supported are square, triangle and special, where special will be defined in the source code.'>" +
20 "<element name='ShiftRows' a:help='Set the value to true to shift subsequent rows.'>" +
23 "<element name='SortingClasses' a:help='Classes will be added to the formation in this order. Where the classes will be added first depends on the formation.'>" +
27 "<element name='SortingOrder' a:help='The order of sorting. This defaults to an order where the formation is filled from the first row to the last, and each row from the center to the sides. Other possible sort orders are “fillFromTheSides”, where the most important units are on the sides of each row, and “fillToTheCenter”, where the most vulnerable units are in the center of the formation.'>" +
31 "<element name='WidthDepthRatio' a:help='Average width-to-depth ratio, counted in number of units.'>" +
32 "<ref name='nonNegativeDecimal'/>" +
34 "<element name='Sloppiness' a:help='The maximum difference between the actual and the perfectly aligned formation position, in meters.'>" +
35 "<ref name='nonNegativeDecimal'/>" +
38 "<element name='MinColumns' a:help='When possible, this number of colums will be created. Overriding the wanted width-to-depth ratio.'>" +
39 "<data type='nonNegativeInteger'/>" +
43 "<element name='MaxColumns' a:help='When possible within the number of units, and the maximum number of rows, this will be the maximum number of columns.'>" +
44 "<data type='nonNegativeInteger'/>" +
48 "<element name='MaxRows' a:help='The maximum number of rows in the formation.'>" +
49 "<data type='nonNegativeInteger'/>" +
53 "<element name='CenterGap' a:help='The size of the central gap, expressed in number of units wide.'>" +
54 "<ref name='nonNegativeDecimal'/>" +
57 "<element name='UnitSeparationWidthMultiplier' a:help='Place the units in the formation closer or further to each other. The standard separation is the footprint size.'>" +
58 "<ref name='nonNegativeDecimal'/>" +
60 "<element name='UnitSeparationDepthMultiplier' a:help='Place the units in the formation closer or further to each other. The standard separation is the footprint size.'>" +
61 "<ref name='nonNegativeDecimal'/>" +
63 "<element name='AnimationVariants' a:help='Give a list of animation variants to use for the particular formation members, based on their positions.'>" +
64 "<text a:help='example text: “1..1,1..-1:animationVariant1;2..2,1..-1;animationVariant2”, this will set animationVariant1 for the first row, and animation2 for the second row. The first part of the numbers (1..1 and 2..2) means the row range. Every row between (and including) those values will switch animationvariants. The second part of the numbers (1..-1) denote the columns inside those rows that will be affected. Note that in both cases, you can use -1 for the last row/column, -2 for the second to last, etc.'/>" +
67 // Distance at which we'll switch between column/box formations.
68 var g_ColumnDistanceThreshold = 128;
70 Formation.prototype.variablesToSerialize = [
80 "formationMembersWithAura",
84 "formationSeparation",
88 Formation.prototype.Init = function(deserialized = false)
90 this.sortingClasses = this.template.SortingClasses.split(/\s+/g);
91 this.shiftRows = this.template.ShiftRows == "true";
92 this.separationMultiplier = {
93 "width": +this.template.UnitSeparationWidthMultiplier,
94 "depth": +this.template.UnitSeparationDepthMultiplier
96 this.sloppiness = +this.template.Sloppiness;
97 this.widthDepthRatio = +this.template.WidthDepthRatio;
98 this.minColumns = +(this.template.MinColumns || 0);
99 this.maxColumns = +(this.template.MaxColumns || 0);
100 this.maxRows = +(this.template.MaxRows || 0);
101 this.centerGap = +(this.template.CenterGap || 0);
103 if (this.template.AnimationVariants)
105 this.animationvariants = [];
106 let differentAnimationVariants = this.template.AnimationVariants.split(/\s*;\s*/);
107 // Loop over the different rectangulars that will map to different animation variants.
108 for (let rectAnimationVariant of differentAnimationVariants)
110 let rect, replacementAnimationVariant;
111 [rect, replacementAnimationVariant] = rectAnimationVariant.split(/\s*:\s*/);
113 [rows, columns] = rect.split(/\s*,\s*/);
114 let minRow, maxRow, minColumn, maxColumn;
115 [minRow, maxRow] = rows.split(/\s*\.\.\s*/);
116 [minColumn, maxColumn] = columns.split(/\s*\.\.\s*/);
117 this.animationvariants.push({
120 "minColumn": +minColumn,
121 "maxColumn": +maxColumn,
122 "name": replacementAnimationVariant
127 this.lastOrderVariant = undefined;
128 // Entity IDs currently belonging to this formation.
130 this.memberPositions = {};
131 this.maxRowsUsed = 0;
132 this.maxColumnsUsed = [];
133 // Entities that have finished the original task.
134 this.finishedEntities = new Set();
135 this.idleEntities = new Set();
136 // Whether we're travelling in column (vs box) formation.
137 this.columnar = false;
138 // Whether we should rearrange all formation members.
139 this.rearrange = true;
140 // Members with a formation aura.
141 this.formationMembersWithAura = [];
144 this.twinFormations = [];
145 // Distance from which two twin formations will merge into one.
146 this.formationSeparation = 0;
151 Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer)
152 .SetInterval(this.entity, IID_Formation, "ShapeUpdate", 1000, 1000, null);
155 Formation.prototype.Serialize = function()
158 for (let key of this.variablesToSerialize)
159 result[key] = this[key];
164 Formation.prototype.Deserialize = function(data)
167 for (let key in data)
168 this[key] = data[key];
172 * Set the value from which two twin formations will become one.
174 Formation.prototype.SetFormationSeparation = function(value)
176 this.formationSeparation = value;
179 Formation.prototype.GetSize = function()
181 return { "width": this.width, "depth": this.depth };
184 Formation.prototype.GetSpeedMultiplier = function()
186 return +this.template.SpeedMultiplier;
189 Formation.prototype.GetMemberCount = function()
191 return this.members.length;
194 Formation.prototype.GetMembers = function()
199 Formation.prototype.GetClosestMember = function(ent, filter)
201 let cmpEntPosition = Engine.QueryInterface(ent, IID_Position);
202 if (!cmpEntPosition || !cmpEntPosition.IsInWorld())
203 return INVALID_ENTITY;
205 let entPosition = cmpEntPosition.GetPosition2D();
206 let closestMember = INVALID_ENTITY;
207 let closestDistance = Infinity;
208 for (let member of this.members)
210 if (filter && !filter(ent))
213 let cmpPosition = Engine.QueryInterface(member, IID_Position);
214 if (!cmpPosition || !cmpPosition.IsInWorld())
217 let pos = cmpPosition.GetPosition2D();
218 let dist = entPosition.distanceToSquared(pos);
219 if (dist < closestDistance)
221 closestMember = member;
222 closestDistance = dist;
225 return closestMember;
229 * Returns the 'primary' member of this formation (typically the most
230 * important unit type), for e.g. playing a representative sound.
231 * Returns undefined if no members.
232 * TODO: Actually implement something like that. Currently this just returns
233 * the arbitrary first one.
235 Formation.prototype.GetPrimaryMember = function()
237 return this.members[0];
241 * Get the formation animation variant for a certain member of this formation.
242 * @param entity The entity ID to get the animation for.
243 * @return The name of the animation variant as defined in the template,
244 * e.g. "testudo_front" or undefined if does not exist.
246 Formation.prototype.GetFormationAnimationVariant = function(entity)
248 if (!this.animationvariants || !this.animationvariants.length || this.columnar || !this.memberPositions[entity])
250 let row = this.memberPositions[entity].row;
251 let column = this.memberPositions[entity].column;
252 for (let i = 0; i < this.animationvariants.length; ++i)
254 let minRow = this.animationvariants[i].minRow;
256 minRow += this.maxRowsUsed + 1;
260 let maxRow = this.animationvariants[i].maxRow;
262 maxRow += this.maxRowsUsed + 1;
266 let minColumn = this.animationvariants[i].minColumn;
268 minColumn += this.maxColumnsUsed[row] + 1;
269 if (column < minColumn)
272 let maxColumn = this.animationvariants[i].maxColumn;
274 maxColumn += this.maxColumnsUsed[row] + 1;
275 if (column > maxColumn)
278 return this.animationvariants[i].name;
283 Formation.prototype.SetFinishedEntity = function(ent)
285 // Rotate the entity to the correct angle.
286 const cmpPosition = Engine.QueryInterface(this.entity, IID_Position);
287 const cmpEntPosition = Engine.QueryInterface(ent, IID_Position);
288 if (cmpEntPosition && cmpEntPosition.IsInWorld() && cmpPosition && cmpPosition.IsInWorld())
289 cmpEntPosition.TurnTo(cmpPosition.GetRotation().y);
291 this.finishedEntities.add(ent);
294 Formation.prototype.UnsetFinishedEntity = function(ent)
296 this.finishedEntities.delete(ent);
299 Formation.prototype.ResetFinishedEntities = function()
301 this.finishedEntities.clear();
304 Formation.prototype.AreAllMembersFinished = function()
306 return this.finishedEntities.size === this.members.length;
309 Formation.prototype.SetIdleEntity = function(ent)
311 this.idleEntities.add(ent);
314 Formation.prototype.UnsetIdleEntity = function(ent)
316 this.idleEntities.delete(ent);
319 Formation.prototype.ResetIdleEntities = function()
321 this.idleEntities.clear();
324 Formation.prototype.AreAllMembersIdle = function()
326 return this.idleEntities.size === this.members.length;
330 * Set whether we are allowed to rearrange formation members.
332 Formation.prototype.SetRearrange = function(rearrange)
334 this.rearrange = rearrange;
338 * Initialize the members of this formation.
339 * Must only be called once.
340 * All members must implement UnitAI.
342 Formation.prototype.SetMembers = function(ents)
346 for (let ent of this.members)
348 let cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
349 cmpUnitAI.SetFormationController(this.entity);
351 let cmpAuras = Engine.QueryInterface(ent, IID_Auras);
352 if (cmpAuras && cmpAuras.HasFormationAura())
354 this.formationMembersWithAura.push(ent);
355 cmpAuras.ApplyFormationAura(ents);
359 this.offsets = undefined;
360 // Locate this formation controller in the middle of its members.
361 this.MoveToMembersCenter();
363 // Compute the speed etc. of the formation.
364 this.ComputeMotionParameters();
368 * Remove the given list of entities.
369 * The entities must already be members of this formation.
370 * @param {boolean} rename - Whether the removal was part of an entity rename
371 (prevents disbanding of the formation when under the member limit).
373 Formation.prototype.RemoveMembers = function(ents, renamed = false)
375 this.offsets = undefined;
376 this.members = this.members.filter(ent => ents.indexOf(ent) === -1);
378 for (let ent of ents)
380 this.finishedEntities.delete(ent);
381 let cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
382 cmpUnitAI.UpdateWorkOrders();
383 cmpUnitAI.SetFormationController(INVALID_ENTITY);
386 for (let ent of this.formationMembersWithAura)
388 let cmpAuras = Engine.QueryInterface(ent, IID_Auras);
389 cmpAuras.RemoveFormationAura(ents);
391 // The unit with the aura is also removed from the formation.
392 if (ents.indexOf(ent) !== -1)
393 cmpAuras.RemoveFormationAura(this.members);
396 this.formationMembersWithAura = this.formationMembersWithAura.filter(function(e) { return ents.indexOf(e) == -1; });
398 // If there's nobody left, destroy the formation
399 // unless this is a rename where we can have 0 members temporarily.
400 if (this.members.length < +this.template.RequiredMemberCount && !renamed)
406 this.ComputeMotionParameters();
411 // Rearrange the remaining members.
412 this.MoveMembersIntoFormation(true, true, this.lastOrderVariant);
415 Formation.prototype.AddMembers = function(ents)
417 this.offsets = undefined;
419 for (let ent of this.formationMembersWithAura)
421 let cmpAuras = Engine.QueryInterface(ent, IID_Auras);
422 cmpAuras.ApplyFormationAura(ents);
425 this.members = this.members.concat(ents);
427 for (let ent of ents)
429 let cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
430 cmpUnitAI.SetFormationController(this.entity);
431 if (!cmpUnitAI.GetOrders().length)
432 cmpUnitAI.SetNextState("FORMATIONMEMBER.IDLE");
434 let cmpAuras = Engine.QueryInterface(ent, IID_Auras);
435 if (cmpAuras && cmpAuras.HasFormationAura())
437 this.formationMembersWithAura.push(ent);
438 cmpAuras.ApplyFormationAura(this.members);
442 this.ComputeMotionParameters();
447 this.MoveMembersIntoFormation(true, true, this.lastOrderVariant);
451 * Remove all members and destroy the formation.
453 Formation.prototype.Disband = function()
455 for (let ent of this.members)
457 let cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
458 cmpUnitAI.SetFormationController(INVALID_ENTITY);
461 for (let ent of this.formationMembersWithAura)
463 let cmpAuras = Engine.QueryInterface(ent, IID_Auras);
464 cmpAuras.RemoveFormationAura(this.members);
468 this.finishedEntities.clear();
469 this.formationMembersWithAura = [];
470 this.offsets = undefined;
472 let cmpUnitAI = Engine.QueryInterface(this.entity, IID_UnitAI);
473 // Hack: switch to a clean state to stop timers.
474 cmpUnitAI.UnitFsm.SwitchToNextState(cmpUnitAI, "");
475 Engine.DestroyEntity(this.entity);
479 * Set all members to form up into the formation shape.
480 * @param {boolean} moveCenter - The formation center will be reinitialized
481 * to the center of the units.
482 * @param {boolean} force - All individual orders of the formation units are replaced,
483 * otherwise the order to walk into formation is just pushed to the front.
484 * @param {string | undefined} variant - Variant to be passed as order parameter.
486 Formation.prototype.MoveMembersIntoFormation = function(moveCenter, force, variant)
488 if (!this.members.length)
495 for (let ent of this.members)
497 let cmpPosition = Engine.QueryInterface(ent, IID_Position);
498 if (!cmpPosition || !cmpPosition.IsInWorld())
502 // Query the 2D position as the exact height calculation isn't needed,
503 // but bring the position to the correct coordinates.
504 positions.push(cmpPosition.GetPosition2D());
505 rotations += cmpPosition.GetRotation().y;
508 let avgpos = Vector2D.average(positions);
510 let cmpPosition = Engine.QueryInterface(this.entity, IID_Position);
511 // Reposition the formation if we're told to or if we don't already have a position.
512 if (moveCenter || (cmpPosition && !cmpPosition.IsInWorld()))
513 this.SetupPositionAndHandleRotation(avgpos.x, avgpos.y, rotations / active.length);
515 this.lastOrderVariant = variant;
516 // Switch between column and box if necessary.
517 let cmpFormationUnitAI = Engine.QueryInterface(this.entity, IID_UnitAI);
518 let walkingDistance = cmpFormationUnitAI.ComputeWalkingDistance();
519 let columnar = walkingDistance > g_ColumnDistanceThreshold;
520 if (columnar != this.columnar)
522 this.columnar = columnar;
523 this.offsets = undefined;
526 let offsetsChanged = false;
527 let newOrientation = this.GetEstimatedOrientation(avgpos);
530 this.offsets = this.ComputeFormationOffsets(active, positions);
531 offsetsChanged = true;
540 // Reset finishedEntities as FormationWalk is called.
541 this.ResetFinishedEntities();
543 for (let i = 0; i < this.offsets.length; ++i)
545 let offset = this.offsets[i];
547 let cmpUnitAI = Engine.QueryInterface(offset.ent, IID_UnitAI);
550 warn("Entities without UnitAI in formation are not supported.");
556 "target": this.entity,
559 "offsetsChanged": offsetsChanged,
562 cmpUnitAI.AddOrder("FormationWalk", data, !force);
563 xMax = Math.max(xMax, offset.x);
564 yMax = Math.max(yMax, offset.y);
565 xMin = Math.min(xMin, offset.x);
566 yMin = Math.min(yMin, offset.y);
568 this.width = xMax - xMin;
569 this.depth = yMax - yMin;
572 Formation.prototype.MoveToMembersCenter = function()
577 for (let ent of this.members)
579 let cmpPosition = Engine.QueryInterface(ent, IID_Position);
580 if (!cmpPosition || !cmpPosition.IsInWorld())
583 positions.push(cmpPosition.GetPosition2D());
584 rotations += cmpPosition.GetRotation().y;
587 let avgpos = Vector2D.average(positions);
588 this.SetupPositionAndHandleRotation(avgpos.x, avgpos.y, rotations / positions.length);
592 * Set formation position.
593 * If formation is not in world at time this is called, set new rotation and flag for range manager.
595 Formation.prototype.SetupPositionAndHandleRotation = function(x, y, rot)
597 let cmpPosition = Engine.QueryInterface(this.entity, IID_Position);
600 let wasInWorld = cmpPosition.IsInWorld();
601 cmpPosition.JumpTo(x, y);
606 let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
607 cmpRangeManager.SetEntityFlag(this.entity, "normal", false);
608 cmpPosition.TurnTo(rot);
611 Formation.prototype.GetAvgFootprint = function(active)
614 for (let ent of active)
616 let cmpFootprint = Engine.QueryInterface(ent, IID_Footprint);
618 footprints.push(cmpFootprint.GetShape());
620 if (!footprints.length)
621 return { "width": 1, "depth": 1 };
623 let r = { "width": 0, "depth": 0 };
624 for (let shape of footprints)
626 if (shape.type == "circle")
628 r.width += shape.radius * 2;
629 r.depth += shape.radius * 2;
631 else if (shape.type == "square")
633 r.width += shape.width;
634 r.depth += shape.depth;
637 r.width /= footprints.length;
638 r.depth /= footprints.length;
642 Formation.prototype.ComputeFormationOffsets = function(active, positions)
644 let separation = this.GetAvgFootprint(active);
645 separation.width *= this.separationMultiplier.width;
646 separation.depth *= this.separationMultiplier.depth;
650 sortingClasses = ["Cavalry", "Infantry"];
652 sortingClasses = this.sortingClasses.slice();
653 sortingClasses.push("Unknown");
655 // The entities will be assigned to positions in the formation in
656 // the same order as the types list is ordered.
658 for (let i = 0; i < sortingClasses.length; ++i)
659 types[sortingClasses[i]] = [];
661 for (let i in active)
663 let cmpIdentity = Engine.QueryInterface(active[i], IID_Identity);
664 let classes = cmpIdentity.GetClassesList();
666 for (let c = 0; c < sortingClasses.length; ++c)
668 if (classes.indexOf(sortingClasses[c]) > -1)
670 types[sortingClasses[c]].push({ "ent": active[i], "pos": positions[i] });
676 types.Unknown.push({ "ent": active[i], "pos": positions[i] });
679 let count = active.length;
681 let shape = this.template.FormationShape;
682 let shiftRows = this.shiftRows;
683 let centerGap = this.centerGap;
684 let sortingOrder = this.template.SortingOrder;
687 // Choose a sensible size/shape for the various formations, depending on number of units.
693 cols = Math.min(count, 3);
700 let depth = Math.sqrt(count / this.widthDepthRatio);
701 if (this.maxRows && depth > this.maxRows)
702 depth = this.maxRows;
703 cols = Math.ceil(count / Math.ceil(depth) + (this.shiftRows ? 0.5 : 0));
704 if (cols < this.minColumns)
705 cols = Math.min(count, this.minColumns);
706 if (this.maxColumns && cols > this.maxColumns && this.maxRows != depth)
707 cols = this.maxColumns;
710 // Define special formations here.
711 if (this.template.FormationShape == "special" && Engine.QueryInterface(this.entity, IID_Identity).GetGenericName() == "Scatter")
713 let width = Math.sqrt(count) * (separation.width + separation.depth) * 2.5;
715 for (let i = 0; i < count; ++i)
717 let obj = new Vector2D(randFloat(0, width), randFloat(0, width));
724 // For non-special formations, calculate the positions based on the number of entities.
725 this.maxColumnsUsed = [];
726 this.maxRowsUsed = 0;
727 if (shape != "special")
732 // While there are units left, start a new row in the formation.
735 // Save the position of the row.
736 let z = -r * separation.depth;
737 // Alternate between the left and right side of the center to have a symmetrical distribution.
740 // Determine the number of entities in this row of the formation.
741 if (shape == "square")
747 else if (shape == "triangle")
754 if (!shiftRows && n > left)
756 for (let c = 0; c < n && left > 0; ++c)
758 // Switch sides for the next entity.
762 x = side * (Math.floor(c / 2) + 0.5) * separation.width;
764 x = side * Math.ceil(c / 2) * separation.width;
767 // Don't use the center position with a center gap.
770 x += side * centerGap / 2;
772 let column = Math.ceil(n / 2) + Math.ceil(c / 2) * side;
773 let r1 = randFloat(-1, 1) * this.sloppiness;
774 let r2 = randFloat(-1, 1) * this.sloppiness;
776 offsets.push(new Vector2D(x + r1, z + r2));
777 offsets[offsets.length - 1].row = r + 1;
778 offsets[offsets.length - 1].column = column;
782 this.maxColumnsUsed[r] = n;
784 this.maxRowsUsed = r;
787 // Make sure the average offset is zero, as the formation is centered around that
788 // calculating offset distances without a zero average makes no sense, as the formation
789 // will jump to a different position any time.
790 let avgoffset = Vector2D.average(offsets);
791 offsets.forEach(function(o) {o.sub(avgoffset);});
793 // Sort the available places in certain ways.
794 // The places first in the list will contain the heaviest units as defined by the order
795 // of the types list.
796 if (sortingOrder == "fillFromTheSides")
797 offsets.sort(function(o1, o2) { return Math.abs(o1.x) < Math.abs(o2.x);});
798 else if (sortingOrder == "fillToTheCenter")
799 offsets.sort(function(o1, o2) {
800 return Math.max(Math.abs(o1.x), Math.abs(o1.y)) < Math.max(Math.abs(o2.x), Math.abs(o2.y));
803 // Query the 2D position of the formation.
804 let cmpPosition = Engine.QueryInterface(this.entity, IID_Position);
805 let formationPos = cmpPosition.GetPosition2D();
807 // Use realistic place assignment,
808 // every soldier searches the closest available place in the formation.
810 let realPositions = this.GetRealOffsetPositions(offsets, formationPos);
811 for (let i = sortingClasses.length; i; --i)
813 let t = types[sortingClasses[i - 1]];
816 let usedOffsets = offsets.splice(-t.length);
817 let usedRealPositions = realPositions.splice(-t.length);
818 for (let entPos of t)
820 let closestOffsetId = this.TakeClosestOffset(entPos, usedRealPositions, usedOffsets);
821 usedRealPositions.splice(closestOffsetId, 1);
822 newOffsets.push(usedOffsets.splice(closestOffsetId, 1)[0]);
823 newOffsets[newOffsets.length - 1].ent = entPos.ent;
831 * Search the closest position in the realPositions list to the given entity.
832 * @param entPos - Object with entity position and entity ID.
833 * @param realPositions - The world coordinates of the available offsets.
835 * @return The index of the closest offset position.
837 Formation.prototype.TakeClosestOffset = function(entPos, realPositions, offsets)
839 let pos = entPos.pos;
840 let closestOffsetId = -1;
841 let offsetDistanceSq = Infinity;
842 for (let i = 0; i < realPositions.length; ++i)
844 let distSq = pos.distanceToSquared(realPositions[i]);
845 if (distSq < offsetDistanceSq)
847 offsetDistanceSq = distSq;
851 this.memberPositions[entPos.ent] = { "row": offsets[closestOffsetId].row, "column": offsets[closestOffsetId].column };
852 return closestOffsetId;
856 * Get the world positions for a list of offsets in this formation.
858 Formation.prototype.GetRealOffsetPositions = function(offsets, pos)
860 let offsetPositions = [];
861 let { sin, cos } = this.GetEstimatedOrientation(pos);
862 // Calculate the world positions.
863 for (let o of offsets)
864 offsetPositions.push(new Vector2D(pos.x + o.y * sin + o.x * cos, pos.y + o.y * cos - o.x * sin));
866 return offsetPositions;
870 * Calculate the estimated rotation of the formation based on the current rotation.
871 * Return the sine and cosine of the angle.
873 Formation.prototype.GetEstimatedOrientation = function(pos)
876 let cmpPosition = Engine.QueryInterface(this.entity, IID_Position);
879 let rot = cmpPosition.GetRotation().y;
880 r.sin = Math.sin(rot);
881 r.cos = Math.cos(rot);
886 * Set formation controller's speed based on its current members.
888 Formation.prototype.ComputeMotionParameters = function()
891 let minSpeed = Infinity;
892 let minAcceleration = Infinity;
894 for (let ent of this.members)
896 let cmpUnitMotion = Engine.QueryInterface(ent, IID_UnitMotion);
899 minSpeed = Math.min(minSpeed, cmpUnitMotion.GetWalkSpeed());
900 minAcceleration = Math.min(minAcceleration, cmpUnitMotion.GetAcceleration());
903 minSpeed *= this.GetSpeedMultiplier();
905 let cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion);
906 cmpUnitMotion.SetSpeedMultiplier(minSpeed / cmpUnitMotion.GetWalkSpeed());
907 cmpUnitMotion.SetAcceleration(minAcceleration);
910 Formation.prototype.ShapeUpdate = function()
915 // Check the distance to twin formations, and merge if
916 // the formations could collide.
917 for (let i = this.twinFormations.length - 1; i >= 0; --i)
919 // Only do the check on one side.
920 if (this.twinFormations[i] <= this.entity)
922 let cmpPosition = Engine.QueryInterface(this.entity, IID_Position);
923 let cmpOtherPosition = Engine.QueryInterface(this.twinFormations[i], IID_Position);
924 let cmpOtherFormation = Engine.QueryInterface(this.twinFormations[i], IID_Formation);
925 if (!cmpPosition || !cmpOtherPosition || !cmpOtherFormation ||
926 !cmpPosition.IsInWorld() || !cmpOtherPosition.IsInWorld())
929 let thisPosition = cmpPosition.GetPosition2D();
930 let otherPosition = cmpOtherPosition.GetPosition2D();
932 let dx = thisPosition.x - otherPosition.x;
933 let dy = thisPosition.y - otherPosition.y;
934 let dist = Math.sqrt(dx * dx + dy * dy);
936 let thisSize = this.GetSize();
937 let otherSize = cmpOtherFormation.GetSize();
938 let minDist = Math.max(thisSize.width / 2, thisSize.depth / 2) +
939 Math.max(otherSize.width / 2, otherSize.depth / 2) +
940 this.formationSeparation;
945 // Merge the members from the twin formation into this one
946 // twin formations should always have exactly the same orders.
947 let otherMembers = cmpOtherFormation.members;
948 cmpOtherFormation.RemoveMembers(otherMembers);
949 this.AddMembers(otherMembers);
950 Engine.DestroyEntity(this.twinFormations[i]);
951 this.twinFormations.splice(i, 1);
953 // Switch between column and box if necessary.
954 let cmpUnitAI = Engine.QueryInterface(this.entity, IID_UnitAI);
955 let walkingDistance = cmpUnitAI.ComputeWalkingDistance();
956 let columnar = walkingDistance > g_ColumnDistanceThreshold;
957 if (columnar != this.columnar)
959 this.offsets = undefined;
960 this.columnar = columnar;
961 // Disable moveCenter so we can't get stuck in a loop of switching
962 // shape causing center to change causing shape to switch back.
963 this.MoveMembersIntoFormation(false, true, this.lastOrderVariant);
967 Formation.prototype.ResetOrderVariant = function()
969 this.lastOrderVariant = undefined;
972 Formation.prototype.OnGlobalOwnershipChanged = function(msg)
974 // When an entity is captured or destroyed, it should no longer be
975 // controlled by this formation.
976 if (this.members.indexOf(msg.entity) != -1)
977 this.RemoveMembers([msg.entity]);
978 if (msg.entity === this.entity && msg.to !== INVALID_PLAYER)
979 Engine.QueryInterface(this.entity, IID_Visual)?.SetVariant("animationVariant", QueryPlayerIDInterface(msg.to, IID_Identity).GetCiv());
982 Formation.prototype.OnGlobalEntityRenamed = function(msg)
984 if (this.members.indexOf(msg.entity) === -1)
987 if (this.finishedEntities.delete(msg.entity))
988 this.finishedEntities.add(msg.newentity);
990 // Save rearranging to temporarily set it to false.
991 let temp = this.rearrange;
992 this.rearrange = false;
994 // First remove the old member to be able to reuse its position.
995 this.RemoveMembers([msg.entity], true);
996 this.AddMembers([msg.newentity]);
997 this.memberPositions[msg.newentity] = this.memberPositions[msg.entity];
999 this.rearrange = temp;
1002 Formation.prototype.RegisterTwinFormation = function(entity)
1004 let cmpFormation = Engine.QueryInterface(entity, IID_Formation);
1007 this.twinFormations.push(entity);
1008 cmpFormation.twinFormations.push(this.entity);
1011 Formation.prototype.DeleteTwinFormations = function()
1013 for (let ent of this.twinFormations)
1015 let cmpFormation = Engine.QueryInterface(ent, IID_Formation);
1017 cmpFormation.twinFormations.splice(cmpFormation.twinFormations.indexOf(this.entity), 1);
1019 this.twinFormations = [];
1022 Formation.prototype.LoadFormation = function(newTemplate)
1024 const newFormation = ChangeEntityTemplate(this.entity, newTemplate);
1025 return Engine.QueryInterface(newFormation, IID_UnitAI);
1029 Formation.prototype.OnEntityRenamed = function(msg)
1031 const members = clone(this.members);
1033 Engine.QueryInterface(msg.newentity, IID_Formation).SetMembers(members);
1036 Engine.RegisterComponentType(IID_Formation, "Formation", Formation);