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);