From 1a1fff976047c166fd0491c94beee8fb4656c37f Mon Sep 17 00:00:00 2001 From: speed2CZ Date: Wed, 26 Aug 2026 18:56:34 +0200 Subject: [PATCH 1/2] Refactor formation commands - Created a new file with helper functions for formation commands that orient the formations correctly by angle on each node. - Refactored platoon and ScenarioFramework to use the new formation commads. - Attack platoons in campaign now orient formations correctly, so units move through path nodes faster. --- lua/ScenarioFramework.lua | 224 +++++++++++++++++++++----------- lua/platoon.lua | 238 +++++++--------------------------- lua/sim/FormationCommands.lua | 98 ++++++++++++++ 3 files changed, 293 insertions(+), 267 deletions(-) create mode 100644 lua/sim/FormationCommands.lua diff --git a/lua/ScenarioFramework.lua b/lua/ScenarioFramework.lua index 30887ff004..f21e07c1c4 100644 --- a/lua/ScenarioFramework.lua +++ b/lua/ScenarioFramework.lua @@ -27,6 +27,7 @@ local SyncVoice = import("/lua/simsyncutils.lua").SyncVoice local CategoryToString = import("/lua/sim/categoryutils.lua").ToString local Cinematics = import("/lua/cinematics.lua") local Game = import("/lua/game.lua") +local FormationCommands = import("/lua/sim/formationcommands.lua") local ScenarioUtils = import("/lua/sim/scenarioutilities.lua") local SimCamera = import("/lua/simcamera.lua").SimCamera local SimUIVars = import("/lua/sim/simuistate.lua") @@ -430,59 +431,175 @@ CreateUnitToPositionDistanceTrigger = TriggerFile.CreateUnitToPositionDistanceTr CreateUnitToMarkerDistanceTrigger = CreateUnitToPositionDistanceTrigger -- got renamed for some reason CreateUnitNearTypeTrigger = TriggerFile.CreateUnitNearTypeTrigger --- platoon functions REQUIRE `squad` to be non-nil when present +--- Orders a group to patrol along a chain +---@param units Unit[] +---@param chain ChainName +function GroupPatrolChain(units, chain) + for _, pos in ScenarioUtils.ChainToPositions(chain) do + IssuePatrol(units, pos) + end +end ---- Orders a platoon to move along a route ----@param platoon Platoon +--- Orders a group to patrol a route +---@param units Unit[] ---@param route (MarkerName | Vector)[] ----@param squad? PlatoonSquads -function PlatoonMoveRoute(platoon, route, squad) +function GroupPatrolRoute(units, route) for _, node in route do if type(node) == 'string' then node = ScenarioUtils.MarkerToPosition(node) end - if squad then - platoon:MoveToLocation(node, false, squad) - else - platoon:MoveToLocation(node, false) - end + IssuePatrol(units, node) end end ---- Orders platoon to patrol a route ----@param platoon Platoon +--- Orders a group to patrol a route in formation +--- +---`IssueFormPatrol` Does NOT return `SimCommand` +---@param units Unit[] +---@param chain ChainName +---@param formation string +function GroupFormPatrolChain(units, chain, formation) + local path = ScenarioUtils.ChainToPositions(chain) + local angles = FormationCommands.GetAnglesForRoute(path) + + for i, pos in ipairs(path) do + IssueFormPatrol(units, pos, formation, angles[i]) + end +end + +--- Orders a group to attack-move a along a chain +---@param units Unit[] +---@param chain ChainName +function GroupAttackChain(units, chain) + for _, pos in ScenarioUtils.ChainToPositions(chain) do + IssueAggressiveMove(units, pos) + end +end + +--- Orders a group to attack-move a along a route +---@param units Unit[] ---@param route (MarkerName | Vector)[] ----@param squad? PlatoonSquads -function PlatoonPatrolRoute(platoon, route, squad) +function GroupAttackRoute(units, route) for _, node in route do if type(node) == 'string' then node = ScenarioUtils.MarkerToPosition(node) end - if squad then - platoon:Patrol(node, squad) - else - platoon:Patrol(node) - end + IssueAggressiveMove(units, node) end end ---- Orders a platoon to attack-move along a route ----@param platoon Platoon +--- Orders a group to move along a chain +---@param units Unit[] +---@param chain ChainName +function GroupMoveChain(units, chain) + for _, pos in ScenarioUtils.ChainToPositions(chain) do + IssueMove(units, pos) + end +end + +--- Orders a group to move a along a route +---@param units Unit[] ---@param route (MarkerName | Vector)[] ----@param squad? PlatoonSquads -function PlatoonAttackRoute(platoon, route, squad) +function GroupMoveRoute(units, route) for _, node in route do if type(node) == 'string' then node = ScenarioUtils.MarkerToPosition(node) end - if squad then - platoon:AggressiveMoveToLocation(node, squad) - else - platoon:AggressiveMoveToLocation(node) + IssueMove(units, node) + end +end + +---Converts route from marker names to marker positions. +---@param route any +local function routeToPositions(route) + for k, v in pairs(route) do + if type(v) == "string" then + route[k] = ScenarioUtils.MarkerToPosition(v) + end + end +end + +--- Orders a platoon to move along a route +---@param platoon Platoon +---@param route (MarkerName | Vector)[] +---@param squad? PlatoonSquads Issues the commands to specific squad or to all squads of the platoon +---@param formation? UnitFormations +function PlatoonMoveRoute(platoon, route, squad, formation) + formation = formation or platoon:GetFormationFromPlatoonData() + local squads = FormationCommands.GetSquadsForFormationOrder(squad) + routeToPositions(route) + + for _, squadName in pairs(squads) do + local units = platoon:GetSquadUnits(squadName) + + if not table.empty(units) then + if formation == 'NoFormation' then + GroupMoveRoute(units, route) + return + end + local angles = FormationCommands.GetAnglesForRoute(route, platoon:GetSquadPosition(squadName)) + FormationCommands.UnitsFormationOrder(units, IssueFormMove, route, angles, formation) end end end +--- Orders platoon to patrol a route +--- +--- `IssueFormPatrol` Does NOT return `SimCommand` +---@param platoon Platoon +---@param route (MarkerName | Vector)[] +---@param squad? PlatoonSquads Issues the commands to specific squad or to all squads of the platoon +---@param formation? UnitFormations +function PlatoonPatrolRoute(platoon, route, squad, formation) + formation = formation or platoon:GetFormationFromPlatoonData() + local squads = FormationCommands.GetSquadsForFormationOrder(squad) + routeToPositions(route) + + -- Since the patrol has no end, the angles are gonna be the same for all squads + local angles = FormationCommands.GetAnglesForRoute(route) + + for _, squadName in pairs(squads) do + local units = platoon:GetSquadUnits(squadName) + + if not table.empty(units) then + if formation == 'NoFormation' then + GroupPatrolRoute(units, route) + return + end + FormationCommands.UnitsFormationOrder(units, IssueFormPatrol, route, angles, formation) + end + end +end + +--- Orders a platoon to attack-move along a route +---@param platoon Platoon +---@param route (MarkerName | Vector)[] +---@param squad? PlatoonSquads Issues the commands to specific squad or to all squads of the platoon +---@param formation? UnitFormations +---@return SimCommand? command Last command issued to the platoon, if any +function PlatoonAttackRoute(platoon, route, squad, formation) + formation = formation or platoon:GetFormationFromPlatoonData() + local squads = FormationCommands.GetSquadsForFormationOrder(squad) + routeToPositions(route) + + local cmd + for _, squadName in pairs(squads) do + local units = platoon:GetSquadUnits(squadName) + + if not table.empty(units) then + if formation == 'NoFormation' then + GroupAttackRoute(units, route) + return {} + end + local angles = FormationCommands.GetAnglesForRoute(route, platoon:GetSquadPosition(squadName)) + local commands = FormationCommands.UnitsFormationOrder(units, IssueFormAggressiveMove, route, angles, formation) + cmd = commands[table.getn(commands)] + end + end + + return cmd +end + --- Orders a platoon to move along a chain ---@param platoon Platoon ---@param chain ChainName @@ -503,57 +620,9 @@ end ---@param platoon Platoon ---@param chain ChainName ---@param squad? PlatoonSquads +---@return SimCommand? command Last command issued to the platoon, if any function PlatoonAttackChain(platoon, chain, squad) - PlatoonAttackRoute(platoon, ScenarioUtils.ChainToPositions(chain), squad) -end - ---- Orders a group to patrol along a chain ----@param units Unit[] ----@param chain ChainName -function GroupPatrolChain(units, chain) - for _, pos in ScenarioUtils.ChainToPositions(chain) do - IssuePatrol(units, pos) - end -end - ---- Orders a group to patrol a route ----@param units Unit[] ----@param route (MarkerName | Vector)[] -function GroupPatrolRoute(units, route) - for _, node in route do - if type(node) == 'string' then - node = ScenarioUtils.MarkerToPosition(node) - end - IssuePatrol(units, node) - end -end - ---- Orders a group to patrol a route in formation ----@param units Unit[] ----@param chain ChainName ----@param formation string -function GroupFormPatrolChain(units, chain, formation) - for _, pos in ScenarioUtils.ChainToPositions(chain) do - IssueFormPatrol(units, pos, formation, 0) - end -end - ---- Orders a group to attack-move a along a chain ----@param units Unit[] ----@param chain ChainName -function GroupAttackChain(units, chain) - for _, pos in ScenarioUtils.ChainToPositions(chain) do - IssueAggressiveMove(units, pos) - end -end - ---- Orders a group to move along a chain ----@param units Unit[] ----@param chain ChainName -function GroupMoveChain(units, chain) - for _, pos in ScenarioUtils.ChainToPositions(chain) do - IssueMove(units, pos) - end + return PlatoonAttackRoute(platoon, ScenarioUtils.ChainToPositions(chain), squad) end ---@param units Unit[] @@ -2231,7 +2300,6 @@ function AntiOffMapMainThread() local WaitTicks = WaitTicks local GetUnitsInRect = GetUnitsInRect local MoveOnMapThread = MoveOnMapThread - local IsHumanUnit = IsHumanUnit GenerateOffMapAreas() while ScenarioInfo.OffMapPreventionThreadAllowed do diff --git a/lua/platoon.lua b/lua/platoon.lua index ba325b2984..9810de1eb5 100644 --- a/lua/platoon.lua +++ b/lua/platoon.lua @@ -14,6 +14,7 @@ local AIUtils = import("/lua/ai/aiutilities.lua") local TransportUtils = import("/lua/ai/transportutilities.lua") local Utilities = import("/lua/utilities.lua") local AIBuildStructures = import("/lua/ai/aibuildstructures.lua") +local FormationCommands = import("/lua/sim/formationcommands.lua") local UpgradeTemplates = import("/lua/upgradetemplates.lua") local Behaviors = import("/lua/ai/aibehaviors.lua") local AIAttackUtils = import("/lua/ai/aiattackutilities.lua") @@ -28,6 +29,7 @@ local tableEmpty = table.empty local tableSort = table.sort local tableCopy = table.copy +local pairs, ipairs = pairs, ipairs local ForkThread, unpack = ForkThread, unpack local GetGameTimeSeconds = GetGameTimeSeconds @@ -77,6 +79,7 @@ local SUtils = import("/lua/ai/sorianutilities.lua") Platoon = Class(moho.platoon_methods) { NeedCoolDown = false, LastAttackDestination = {}, + SquadNames = {'Attack', 'Artillery', 'Guard', 'Scout', 'Support','Unassigned'}, ---@param self Platoon ---@param plan table @@ -99,6 +102,14 @@ Platoon = Class(moho.platoon_methods) { self.PlatoonData = tableDeepcopy(dataTable) end, + --- Tries to get formation saved in `PlatoonData` + ---@param self Platoon + ---@return UnitFormations #Defaults to `AttackFormation` + GetFormationFromPlatoonData = function(self) + local data = self.PlatoonData + return data and (data.OverrideFormation or data.UseFormation) or 'AttackFormation' + end, + ---@param self Platoon SetPartOfAttackForce = function(self) local data = self.PlatoonData @@ -4138,176 +4149,70 @@ Platoon = Class(moho.platoon_methods) { ---@param self Platoon ---@param path Vector[] path A table of positions, preferably of type Vector. Converted otherwise. ---@param formation? UnitFormations self.PlatoonData.UseFormation The formation to apply, such as GrowthFormation, AttackFormation or NoFormation. - ---@return PlatoonCommand[] + ---@return SimCommand[] IssuePatrolAlongRoute = function(self, path, formation) - - -- check for optional / default values - local formation = formation or self.PlatoonData.UseFormation or 'NoFormation' - - -- check if the parameters are correct - if not path or not (type(path) == 'table') then - error("IssuePatrolAlongRoute: The path is not a table. For paths with only one node, use { node } as the path.") - return { } - end + formation = formation or self.PlatoonData.UseFormation or 'NoFormation' if tableEmpty(path) then - error("IssuePatrolAlongRoute: The path is empty."); - return { } + WARN("Platoon:IssuePatrolAlongRoute: The path is empty.") + return {} end -- keep track of all the commands we issued - local commands = { } + local commands = {} -- we have no formation, further computations are not required. We use this -- shortcut because calling IssueFormPatrol() with no formation causes them -- to not move at all. if formation == 'NoFormation' then local units = self:GetPlatoonUnits() - for k, node in path do - local command = IssuePatrol(units, node) + for _, pos in ipairs(path) do + local command = IssuePatrol(units, pos) tableInsert(commands, command) end return commands end - -- check if we have a path of tables, instead to a path of vectors. A lot of the functionality provided by - -- this library generates lists of tables instead of lists of vectors. Functionality in this file requires - -- a list of vectors. Convert it if neccesary. - if not path[1].x then - local oldPath = path - path = {} - for k, node in oldPath do - tableInsert(path, Vector(node[1], node[2], node[3])) - end - end - - -- store locally for better performance - local count = tableGetn(path) - local GetAngleCCW = Utilities.GetAngleCCW - local GetDirectionVector = Utilities.GetDirectionVector - - -- pre-compute the angles - local angles = { } - for k = 1, count do - - local curr = path[k - 1] - local next = path[k] - - -- if we're trying to look before the first node of the path, use the last node instead - if k - 1 < 1 then - curr = path[count] - end - - -- base orientation when the angle is 0 for the function IssueFormMove - local base = Vector( 0, 0, 1 ) - local direction = GetDirectionVector(next, curr) - local angle = GetAngleCCW(base, direction) - angles[k] = angle - end - - -- move over the path in formation + local angles = FormationCommands.GetAnglesForRoute(path) local units = self:GetPlatoonUnits() - for k = 1, count do - local point = path[k] - local angle = angles[k] - local command = IssueFormPatrol(units, point, formation, angle) - tableInsert(commands, command) - end - - return commands + return FormationCommands.UnitsFormationOrder(units, IssueFormPatrol, path, angles, formation) end, --- Aggressive-moves the platoon along the path, orientating at each node to match the line from the previous node to the current node. ---@param self Platoon ---@param path Vector[] ---@param formation? UnitFormations - ---@return PlatoonCommand[] + ---@return SimCommand[] IssueAggressiveMoveAlongRoute = function(self, path, formation) - -- check for optional / default values - local formation = formation or self.PlatoonData.UseFormation or 'NoFormation' - - -- check if the parameters are correct - if not path or not (type(path) == 'table') then - error("IssueAggressiveMoveAlongRoute: The path is not a table. For paths with only one node, use { node } as the path.") - return { } - end + formation = formation or self.PlatoonData.UseFormation or 'NoFormation' if tableEmpty(path) then - error("IssueAggressiveMoveAlongRoute: The path is empty."); - return { } + WARN("Platoon:IssueAggressiveMoveAlongRoute: The path is empty.") + return {} end -- keep track of all the commands we issued - local commands = { } + local commands = {} -- we have no formation, further computations are not required. We use this - -- shortcut because calling IssueFormAggressiveMove() with no formation causes - -- them to not move at all. + -- shortcut because calling IssueFormPatrol() with no formation causes them + -- to not move at all. if formation == 'NoFormation' then - -- store the commands / orders local units = self:GetPlatoonUnits() - - for k, node in path do - local command = IssueAggressiveMove(units, node) + for _, pos in ipairs(path) do + local command = IssueAggressiveMove(units, pos) tableInsert(commands, command) end return commands end - -- check if we have a path of tables, instead of a path of vectors. A lot of the functionality provided by - -- this library generates lists of tables instead of lists of vectors. Functionality in this file requires - -- a list of vectors. Convert it if neccesary. - if not path[1].x then - local oldPath = path - path = {} - for k, node in oldPath do - tableInsert(path, Vector(node[1], node[2], node[3])) - end - end - - -- store locally for better performance - local count = tableGetn(path) - local GetAngleCCW = Utilities.GetAngleCCW - local GetDirectionVector = Utilities.GetDirectionVector - - -- pre-compute the angles - local angles = { } - for k = 1, count do - - local curr = path[k - 1] - local next = path[k] - - -- if we're trying to look before the first node of the path, use the platoons current position instead - if k - 1 < 1 then - local pos = self:GetPlatoonPosition() - if not pos then - error("IssueAggressiveMoveAlongRoute: The platoon has no position.") - return { } - end - curr = Vector(pos[1], pos[2], pos[3]) - end - - -- base orientation when the angle is 0 - local base = Vector( 0, 0, 1 ) - local direction = GetDirectionVector(next, curr) - local angle = GetAngleCCW(base, direction) - angles[k] = angle - end - - -- move over the path, store the commands + local angles = FormationCommands.GetAnglesForRoute(path, self:GetPlatoonPosition()) local units = self:GetPlatoonUnits() - for k = 1, count do - local point = path[k] - local angle = angles[k] - local command = IssueFormAggressiveMove(units, point, formation, angle) - tableInsert(commands, command) - end - - return commands + FormationCommands.UnitsFormationOrder(units, IssueFormAggressiveMove, path, angles, formation) end, --- Moves the platoon along the path, orientating at each node to match the line from the previous node to the current node. @@ -4316,89 +4221,44 @@ Platoon = Class(moho.platoon_methods) { ---@param formation? UnitFormations self.PlatoonData.UseFormation The formation to apply, such as GrowthFormation, AttackFormation or NoFormation. ---@return PlatoonCommand[] IssueMoveAlongRoute = function(self, path, formation) - -- check for optional / default values - local formation = formation or self.PlatoonData.UseFormation or 'NoFormation' - - -- check if the parameters are correct - if not path or not (type(path) == 'table') then - error("IssueMoveAlongRoute: The path is not a table. For paths with only one node, use { node } as the path.") - return { } - end + formation = formation or self.PlatoonData.UseFormation or 'NoFormation' if tableEmpty(path) then - error("IssueMoveAlongRoute: The path is empty."); - return { } + WARN("Platoon:IssueMoveAlongRoute: The path is empty.") + return {} end -- keep track of all the commands we issued - local commands = { } + local commands = {} -- we have no formation, further computations are not required. We use this - -- shortcut because calling IssueFormMove() with no formation causes them + -- shortcut because calling IssueFormPatrol() with no formation causes them -- to not move at all. if formation == 'NoFormation' then - -- store the commands / orders local units = self:GetPlatoonUnits() - for k, node in path do - local command = IssueMove(units, node) + for _, pos in ipairs(path) do + local command = IssueMove(units, pos) tableInsert(commands, command) end return commands end - -- check if we have a path of tables, instead of a path of vectors. A lot of the functionality provided by - -- this library generates lists of tables instead of lists of vectors. Functionality in this file requires - -- a list of vectors. Convert it if neccesary. - if not path[1].x then - local oldPath = path - path = {} - for k, node in oldPath do - tableInsert(path, Vector(node[1], node[2], node[3])) - end - end - - -- store locally for better performance - local count = tableGetn(path) - local GetAngleCCW = Utilities.GetAngleCCW - local GetDirectionVector = Utilities.GetDirectionVector - - -- pre-compute the angles - local angles = { } - for k = 1, count do + local angles = FormationCommands.GetAnglesForRoute(path, self:GetPlatoonPosition()) + local units = self:GetPlatoonUnits() - local curr = path[k - 1] - local next = path[k] + -- Last command should be aggressive move + local numPositions = tableGetn(path) + ---@type Vector + local lastPos = tableRemove(path, numPositions) - -- if we're trying to look before the first node of the path, use the platoons current position instead - if k - 1 < 1 then - local pos = self:GetPlatoonPosition() - if not pos then - error("IssueMoveAlongRoute: The platoon has no position.") - return { } - end - curr = Vector(pos[1], pos[2], pos[3]) + if numPositions > 1 then + local moveCommands = FormationCommands.UnitsFormationOrder(units, IssueFormMove, path, angles, formation) + for _, c in moveCommands do + tableInsert(commands, c) end - - -- base orientation when the angle is 0 - local base = Vector( 0, 0, 1 ) - local direction = GetDirectionVector(next, curr) - local angle = GetAngleCCW(base, direction) - angles[k] = angle end - - -- move over the path, store the commands - local units = self:GetPlatoonUnits() - - for k = 1, count -1 do - local point = path[k] - local angle = angles[k] - local command = IssueFormMove(units, point, formation, angle) - tableInsert(commands, command) - end - - -- aggressive move for the final path node - tableInsert(commands, IssueFormAggressiveMove(units, path[count], formation, angles[count])) + tableInsert(commands, IssueFormAggressiveMove(units, lastPos, formation, angles[numPositions])) return commands end, diff --git a/lua/sim/FormationCommands.lua b/lua/sim/FormationCommands.lua new file mode 100644 index 0000000000..33b74bea0d --- /dev/null +++ b/lua/sim/FormationCommands.lua @@ -0,0 +1,98 @@ +--- +--- Helper functions to calculate the angles for the formations commands and issue those commands. +--- +--- All the IssueForm commands take `angle` argument that defines which way the platoon should be facing. +--- Same as when the player holds right click and by rotating the mouse chooses the orientation of the formation. +--- +--- Commands on the `Platoon` class don't support setting the angle and the formation is always facing south. +--- + +local Platoon = import("/lua/platoon.lua").Platoon +local Utilities = import("/lua/utilities.lua") + +local ipairs = ipairs +local tableGetn = table.getn +local tableInsert = table.insert +local tableMap = table.map +local Vector = Vector + +-- Base orientation vector when the formation angle is 0 +local baseZeroAngle = Vector(0, 0, 1) + +---Converts a table with position to vector if its not a vector already +---@param pos table<{[1]: number, [2]: number, [3]: number}>|Vector +local function positionToVector(pos) + if pos.x then + return pos + end + return Vector(pos[1], pos[2], pos[3]) +end + +--- Returns angles the unit formation should face in `IssueFormation` commands for given `path`. +---@param path Vector[] +---@param initialPosition? Vector Position that is used to calculate the first angle of the path. Defaults to last position of the path +---@return number[] angles List of angles for every position +function GetAnglesForRoute(path, initialPosition) + local count = tableGetn(path) + if count == 0 then return {} end + + -- Convert positions from table to `Vector` when needed + ---@type Vector[] + local positions = tableMap(path, positionToVector) + ---@type number[] + local angles = {} + + for k = 1, count do + local currPos = positions[k - 1] or initialPosition or positions[count] + local nextPos = positions[k] + + local direction = Utilities.GetDirectionVector(nextPos, currPos) + angles[k] = Utilities.GetAngleCCW(baseZeroAngle, direction) + end + + return angles +end + +--- Cache for `GetSquadsForFormationOrder` to avoid creating a new table every time an order is issued. +local cachedSquad = {} +--- Returns a list of squads to issue formation order to. +--- +---If `squad` is provided, returns a cached list with only that squad, otherwise returns all squad names. +---@param squad? PlatoonSquads +---@return PlatoonSquads[] +function GetSquadsForFormationOrder(squad) + local squads + if squad then + cachedSquad[1] = squad + squads = cachedSquad + else + squads = Platoon.SquadNames + end + + return squads +end + +---@alias UnitsFormationCommand +---| `IssueFormMove` #Formation move +---| `IssueFormAggressiveMove` # Formation attack move +---| `IssueFormPatrol` # Formation patrol. Does NOT return `SimCommand` +---| `IssueFormAttack` # Formation attack + +--- Issues formation order to the units, using `angles` to oreient units at each node. +---@param units Unit[] +---@param fn UnitsFormationCommand One of the `IssueFormationCommand` functions +---@param positions Vector[] +---@param angles number[] +---@param formation UnitFormations +---@return SimCommand[] #If `fn` is `IssueFormPatrol` the returned table will be empty +function UnitsFormationOrder(units, fn, positions, angles, formation) + local commands = {} + for i, position in ipairs(positions) do + local cmd = fn(units, position, formation, angles[i]) + if cmd then + tableInsert(commands, cmd) + end + end + + return commands +end From b3cd3956c8fbb61dc81a8765fc2fe5ebb8bb72cf Mon Sep 17 00:00:00 2001 From: speed2CZ Date: Wed, 2 Sep 2026 02:19:19 +0200 Subject: [PATCH 2/2] Create ai.7267.md --- changelog/snippets/ai.7267.md | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 changelog/snippets/ai.7267.md diff --git a/changelog/snippets/ai.7267.md b/changelog/snippets/ai.7267.md new file mode 100644 index 0000000000..e9d36b40b5 --- /dev/null +++ b/changelog/snippets/ai.7267.md @@ -0,0 +1,3 @@ +- Created a new file with helper functions for formation commands that orient the formations correctly by angle on each node. (#7267) +- Refactored platoon and ScenarioFramework to use the new formation commads. (#7267) +- Attack platoons in campaign now orient formations correctly, so units move through path nodes faster. (#7267) \ No newline at end of file