Move FormationName and Icon from cmpFormation to cmpIdentity.
[0ad.git] / binaries / data / mods / public / simulation / components / Formation.js
blob50efbb152593c936884ce204ef0d75f970c21288
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'>"+
7                     "2"+
8                   "</param>"+
9                 "</data>" +
10         "</element>" +
11         "<element name='DisabledTooltip' a:help='Tooltip shown when the formation is disabled.'>" +
12                 "<text/>" +
13         "</element>" +
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'/>" +
16         "</element>" +
17         "<element name='FormationShape' a:help='Formation shape, currently supported are square, triangle and special, where special will be defined in the source code.'>" +
18                 "<text/>" +
19         "</element>" +
20         "<element name='ShiftRows' a:help='Set the value to true to shift subsequent rows.'>" +
21                 "<text/>" +
22         "</element>" +
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.'>" +
24                 "<text/>" +
25         "</element>" +
26         "<optional>" +
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.'>" +
28                         "<text/>" +
29                 "</element>" +
30         "</optional>" +
31         "<element name='WidthDepthRatio' a:help='Average width-to-depth ratio, counted in number of units.'>" +
32                 "<ref name='nonNegativeDecimal'/>" +
33         "</element>" +
34         "<element name='Sloppiness' a:help='The maximum difference between the actual and the perfectly aligned formation position, in meters.'>" +
35                 "<ref name='nonNegativeDecimal'/>" +
36         "</element>" +
37         "<optional>" +
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'/>" +
40                 "</element>" +
41         "</optional>" +
42         "<optional>" +
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'/>" +
45                 "</element>" +
46         "</optional>" +
47         "<optional>" +
48                 "<element name='MaxRows' a:help='The maximum number of rows in the formation.'>" +
49                         "<data type='nonNegativeInteger'/>" +
50                 "</element>" +
51         "</optional>" +
52         "<optional>" +
53                 "<element name='CenterGap' a:help='The size of the central gap, expressed in number of units wide.'>" +
54                         "<ref name='nonNegativeDecimal'/>" +
55                 "</element>" +
56         "</optional>" +
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'/>" +
59         "</element>" +
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'/>" +
62         "</element>" +
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.'/>" +
65         "</element>";
67 // Distance at which we'll switch between column/box formations.
68 var g_ColumnDistanceThreshold = 128;
70 Formation.prototype.variablesToSerialize = [
71         "lastOrderVariant",
72         "members",
73         "memberPositions",
74         "maxRowsUsed",
75         "maxColumnsUsed",
76         "finishedEntities",
77         "idleEntities",
78         "columnar",
79         "rearrange",
80         "formationMembersWithAura",
81         "width",
82         "depth",
83         "twinFormations",
84         "formationSeparation",
85         "offsets"
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
95         };
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)
104         {
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)
109                 {
110                         let rect, replacementAnimationVariant;
111                         [rect, replacementAnimationVariant] = rectAnimationVariant.split(/\s*:\s*/);
112                         let rows, columns;
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({
118                                 "minRow": +minRow,
119                                 "maxRow": +maxRow,
120                                 "minColumn": +minColumn,
121                                 "maxColumn": +maxColumn,
122                                 "name": replacementAnimationVariant
123                         });
124                 }
125         }
127         this.lastOrderVariant = undefined;
128         // Entity IDs currently belonging to this formation.
129         this.members = [];
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 = [];
142         this.width = 0;
143         this.depth = 0;
144         this.twinFormations = [];
145         // Distance from which two twin formations will merge into one.
146         this.formationSeparation = 0;
148         if (deserialized)
149                 return;
151         Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer)
152                 .SetInterval(this.entity, IID_Formation, "ShapeUpdate", 1000, 1000, null);
155 Formation.prototype.Serialize = function()
157         let result = {};
158         for (let key of this.variablesToSerialize)
159                 result[key] = this[key];
161         return result;
164 Formation.prototype.Deserialize = function(data)
166         this.Init(true);
167         for (let key in data)
168                 this[key] = data[key];
172  * Set the value from which two twin formations will become one.
173  */
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()
196         return this.members;
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)
209         {
210                 if (filter && !filter(ent))
211                         continue;
213                 let cmpPosition = Engine.QueryInterface(member, IID_Position);
214                 if (!cmpPosition || !cmpPosition.IsInWorld())
215                         continue;
217                 let pos = cmpPosition.GetPosition2D();
218                 let dist = entPosition.distanceToSquared(pos);
219                 if (dist < closestDistance)
220                 {
221                         closestMember = member;
222                         closestDistance = dist;
223                 }
224         }
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.
234  */
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.
245  */
246 Formation.prototype.GetFormationAnimationVariant = function(entity)
248         if (!this.animationvariants || !this.animationvariants.length || this.columnar || !this.memberPositions[entity])
249                 return undefined;
250         let row = this.memberPositions[entity].row;
251         let column = this.memberPositions[entity].column;
252         for (let i = 0; i < this.animationvariants.length; ++i)
253         {
254                 let minRow = this.animationvariants[i].minRow;
255                 if (minRow < 0)
256                         minRow += this.maxRowsUsed + 1;
257                 if (row < minRow)
258                         continue;
260                 let maxRow = this.animationvariants[i].maxRow;
261                 if (maxRow < 0)
262                         maxRow += this.maxRowsUsed + 1;
263                 if (row > maxRow)
264                         continue;
266                 let minColumn = this.animationvariants[i].minColumn;
267                 if (minColumn < 0)
268                         minColumn += this.maxColumnsUsed[row] + 1;
269                 if (column < minColumn)
270                         continue;
272                 let maxColumn = this.animationvariants[i].maxColumn;
273                 if (maxColumn < 0)
274                         maxColumn += this.maxColumnsUsed[row] + 1;
275                 if (column > maxColumn)
276                         continue;
278                 return this.animationvariants[i].name;
279         }
280         return undefined;
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.
331  */
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.
341  */
342 Formation.prototype.SetMembers = function(ents)
344         this.members = ents;
346         for (let ent of this.members)
347         {
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())
353                 {
354                         this.formationMembersWithAura.push(ent);
355                         cmpAuras.ApplyFormationAura(ents);
356                 }
357         }
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).
372  */
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)
379         {
380                 this.finishedEntities.delete(ent);
381                 let cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
382                 cmpUnitAI.UpdateWorkOrders();
383                 cmpUnitAI.SetFormationController(INVALID_ENTITY);
384         }
386         for (let ent of this.formationMembersWithAura)
387         {
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);
394         }
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)
401         {
402                 this.Disband();
403                 return;
404         }
406         this.ComputeMotionParameters();
408         if (!this.rearrange)
409                 return;
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)
420         {
421                 let cmpAuras = Engine.QueryInterface(ent, IID_Auras);
422                 cmpAuras.ApplyFormationAura(ents);
423         }
425         this.members = this.members.concat(ents);
427         for (let ent of ents)
428         {
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())
436                 {
437                         this.formationMembersWithAura.push(ent);
438                         cmpAuras.ApplyFormationAura(this.members);
439                 }
440         }
442         this.ComputeMotionParameters();
444         if (!this.rearrange)
445                 return;
447         this.MoveMembersIntoFormation(true, true, this.lastOrderVariant);
451  * Remove all members and destroy the formation.
452  */
453 Formation.prototype.Disband = function()
455         for (let ent of this.members)
456         {
457                 let cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
458                 cmpUnitAI.SetFormationController(INVALID_ENTITY);
459         }
461         for (let ent of this.formationMembersWithAura)
462         {
463                 let cmpAuras = Engine.QueryInterface(ent, IID_Auras);
464                 cmpAuras.RemoveFormationAura(this.members);
465         }
467         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.
485  */
486 Formation.prototype.MoveMembersIntoFormation = function(moveCenter, force, variant)
488         if (!this.members.length)
489                 return;
491         let active = [];
492         let positions = [];
493         let rotations = 0;
495         for (let ent of this.members)
496         {
497                 let cmpPosition = Engine.QueryInterface(ent, IID_Position);
498                 if (!cmpPosition || !cmpPosition.IsInWorld())
499                         continue;
501                 active.push(ent);
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;
506         }
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)
521         {
522                 this.columnar = columnar;
523                 this.offsets = undefined;
524         }
526         let offsetsChanged = false;
527         let newOrientation = this.GetEstimatedOrientation(avgpos);
528         if (!this.offsets)
529         {
530                 this.offsets = this.ComputeFormationOffsets(active, positions);
531                 offsetsChanged = true;
532         }
534         let xMax = 0;
535         let yMax = 0;
536         let xMin = 0;
537         let yMin = 0;
539         if (force)
540                 // Reset finishedEntities as FormationWalk is called.
541                 this.ResetFinishedEntities();
543         for (let i = 0; i < this.offsets.length; ++i)
544         {
545                 let offset = this.offsets[i];
547                 let cmpUnitAI = Engine.QueryInterface(offset.ent, IID_UnitAI);
548                 if (!cmpUnitAI)
549                 {
550                         warn("Entities without UnitAI in formation are not supported.");
551                         continue;
552                 }
554                 let data =
555                 {
556                         "target": this.entity,
557                         "x": offset.x,
558                         "z": offset.y,
559                         "offsetsChanged": offsetsChanged,
560                         "variant": variant
561                 };
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);
567         }
568         this.width = xMax - xMin;
569         this.depth = yMax - yMin;
572 Formation.prototype.MoveToMembersCenter = function()
574         let positions = [];
575         let rotations = 0;
577         for (let ent of this.members)
578         {
579                 let cmpPosition = Engine.QueryInterface(ent, IID_Position);
580                 if (!cmpPosition || !cmpPosition.IsInWorld())
581                         continue;
583                 positions.push(cmpPosition.GetPosition2D());
584                 rotations += cmpPosition.GetRotation().y;
585         }
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);
598         if (!cmpPosition)
599                 return;
600         let wasInWorld = cmpPosition.IsInWorld();
601         cmpPosition.JumpTo(x, y);
603         if (wasInWorld)
604                 return;
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)
613         let footprints = [];
614         for (let ent of active)
615         {
616                 let cmpFootprint = Engine.QueryInterface(ent, IID_Footprint);
617                 if (cmpFootprint)
618                         footprints.push(cmpFootprint.GetShape());
619         }
620         if (!footprints.length)
621                 return { "width": 1, "depth": 1 };
623         let r = { "width": 0, "depth": 0 };
624         for (let shape of footprints)
625         {
626                 if (shape.type == "circle")
627                 {
628                         r.width += shape.radius * 2;
629                         r.depth += shape.radius * 2;
630                 }
631                 else if (shape.type == "square")
632                 {
633                         r.width += shape.width;
634                         r.depth += shape.depth;
635                 }
636         }
637         r.width /= footprints.length;
638         r.depth /= footprints.length;
639         return r;
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;
648         let sortingClasses;
649         if (this.columnar)
650                 sortingClasses = ["Cavalry", "Infantry"];
651         else
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.
657         let types = {};
658         for (let i = 0; i < sortingClasses.length; ++i)
659                 types[sortingClasses[i]] = [];
661         for (let i in active)
662         {
663                 let cmpIdentity = Engine.QueryInterface(active[i], IID_Identity);
664                 let classes = cmpIdentity.GetClassesList();
665                 let done = false;
666                 for (let c = 0; c < sortingClasses.length; ++c)
667                 {
668                         if (classes.indexOf(sortingClasses[c]) > -1)
669                         {
670                                 types[sortingClasses[c]].push({ "ent": active[i], "pos": positions[i] });
671                                 done = true;
672                                 break;
673                         }
674                 }
675                 if (!done)
676                         types.Unknown.push({ "ent": active[i], "pos": positions[i] });
677         }
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;
685         let offsets = [];
687         // Choose a sensible size/shape for the various formations, depending on number of units.
688         let cols;
690         if (this.columnar)
691         {
692                 shape = "square";
693                 cols = Math.min(count, 3);
694                 shiftRows = false;
695                 centerGap = 0;
696                 sortingOrder = null;
697         }
698         else
699         {
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;
708         }
710         // Define special formations here.
711         if (this.template.FormationShape == "special" && Engine.QueryInterface(this.entity, IID_Identity).GetGenericName() == "Scatter")
712         {
713                 let width = Math.sqrt(count) * (separation.width + separation.depth) * 2.5;
715                 for (let i = 0; i < count; ++i)
716                 {
717                         let obj = new Vector2D(randFloat(0, width), randFloat(0, width));
718                         obj.row = 1;
719                         obj.column = i + 1;
720                         offsets.push(obj);
721                 }
722         }
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")
728         {
729                 offsets = [];
730                 let r = 0;
731                 let left = count;
732                 // While there are units left, start a new row in the formation.
733                 while (left > 0)
734                 {
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.
738                         let side = 1;
739                         let n;
740                         // Determine the number of entities in this row of the formation.
741                         if (shape == "square")
742                         {
743                                 n = cols;
744                                 if (shiftRows)
745                                         n -= r % 2;
746                         }
747                         else if (shape == "triangle")
748                         {
749                                 if (shiftRows)
750                                         n = r + 1;
751                                 else
752                                         n = r * 2 + 1;
753                         }
754                         if (!shiftRows && n > left)
755                                 n = left;
756                         for (let c = 0; c < n && left > 0; ++c)
757                         {
758                                 // Switch sides for the next entity.
759                                 side *= -1;
760                                 let x;
761                                 if (n % 2 == 0)
762                                         x = side * (Math.floor(c / 2) + 0.5) * separation.width;
763                                 else
764                                         x = side * Math.ceil(c / 2) * separation.width;
765                                 if (centerGap)
766                                 {
767                                         // Don't use the center position with a center gap.
768                                         if (x == 0)
769                                                 continue;
770                                         x += side * centerGap / 2;
771                                 }
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;
779                                 left--;
780                         }
781                         ++r;
782                         this.maxColumnsUsed[r] = n;
783                 }
784                 this.maxRowsUsed = r;
785         }
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));
801                 });
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.
809         let newOffsets = [];
810         let realPositions = this.GetRealOffsetPositions(offsets, formationPos);
811         for (let i = sortingClasses.length; i; --i)
812         {
813                 let t = types[sortingClasses[i - 1]];
814                 if (!t.length)
815                         continue;
816                 let usedOffsets = offsets.splice(-t.length);
817                 let usedRealPositions = realPositions.splice(-t.length);
818                 for (let entPos of t)
819                 {
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;
824                 }
825         }
827         return newOffsets;
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.
834  * @param offsets
835  * @return The index of the closest offset position.
836  */
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)
843         {
844                 let distSq = pos.distanceToSquared(realPositions[i]);
845                 if (distSq < offsetDistanceSq)
846                 {
847                         offsetDistanceSq = distSq;
848                         closestOffsetId = i;
849                 }
850         }
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.
857  */
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.
872  */
873 Formation.prototype.GetEstimatedOrientation = function(pos)
875         let r = {};
876         let cmpPosition = Engine.QueryInterface(this.entity, IID_Position);
877         if (!cmpPosition)
878                 return r;
879         let rot = cmpPosition.GetRotation().y;
880         r.sin = Math.sin(rot);
881         r.cos = Math.cos(rot);
882         return r;
886  * Set formation controller's speed based on its current members.
887  */
888 Formation.prototype.ComputeMotionParameters = function()
890         let maxRadius = 0;
891         let minSpeed = Infinity;
892         let minAcceleration = Infinity;
894         for (let ent of this.members)
895         {
896                 let cmpUnitMotion = Engine.QueryInterface(ent, IID_UnitMotion);
897                 if (cmpUnitMotion)
898                 {
899                         minSpeed = Math.min(minSpeed, cmpUnitMotion.GetWalkSpeed());
900                         minAcceleration = Math.min(minAcceleration, cmpUnitMotion.GetAcceleration());
901                 }
902         }
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()
912         if (!this.rearrange)
913                 return;
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)
918         {
919                 // Only do the check on one side.
920                 if (this.twinFormations[i] <= this.entity)
921                         continue;
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())
927                         continue;
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;
942                 if (minDist < dist)
943                         continue;
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);
952         }
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)
958         {
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);
964         }
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)
985                 return;
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);
1005         if (!cmpFormation)
1006                 return;
1007         this.twinFormations.push(entity);
1008         cmpFormation.twinFormations.push(this.entity);
1011 Formation.prototype.DeleteTwinFormations = function()
1013         for (let ent of this.twinFormations)
1014         {
1015                 let cmpFormation = Engine.QueryInterface(ent, IID_Formation);
1016                 if (cmpFormation)
1017                         cmpFormation.twinFormations.splice(cmpFormation.twinFormations.indexOf(this.entity), 1);
1018         }
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);
1032         this.Disband();
1033         Engine.QueryInterface(msg.newentity, IID_Formation).SetMembers(members);
1036 Engine.RegisterComponentType(IID_Formation, "Formation", Formation);