diff --git a/_docs/pages/reference.html b/_docs/pages/reference.html index 6352d42..15b9d1c 100644 --- a/_docs/pages/reference.html +++ b/_docs/pages/reference.html @@ -145,8 +145,8 @@

CAP Routes

- Format: CAPROUTE_[Name]
- Example: CAPROUTE_ALPHA + Format: CAPROUTE_[routeID]_[Name]
+ Example: CAPROUTE_103_ALPHA

CAP routes define the patrol paths for CAP units. They are tied to specific stages and zones. @@ -249,18 +249,16 @@

CAP Group Config

-                1 at x:               [<activeStage>]<capStage>
-                n and n  at x:        [<activeStage>,<activeStage>]<capStage>
-                n till n at x:        [<activeStage>-<activeStage>]<capStage>
-                n till n and n at x:  [<activeStage>-<activeStage>,<activeStage>]<capStage>
-                n till n at Active:   [<activeStage>-<activeStage>]A
+                1 at x:               [<activeStage>]<CapRouteID>
+                n and n  at x:        [<activeStage>,<activeStage>]<CapRouteID>
+                n till n at x:        [<activeStage>-<activeStage>]<CapRouteID>
+                n till n and n at x:  [<activeStage>-<activeStage>,<activeStage>]<CapRouteID>
 
                 Divider: |
 
                 Examples:
                 CAP_A[1-4,6]7|[5,7]8_SomeName => Will fly CAP at stage 7 when stages 1 through 4 and 6 are active and will fly CAP at 8 when 5 and 7 are active.
-                CAP_A[2-5]5|[6]6_SomeName => Will fly CAP at stage 5 when stages 2 through 5 are active and will fly CAP at 6 when 6 is active.
-                CAP_A[1-5]A|[6]7_SomeName => Will fly CAP at the ACTIVE stage if stages 1-5 are active. Then when 6 is active, it will fly in 7.
+                CAP_A[2-5]5|[6]6_SomeName => Will fly CAP at stage 5 when stages 2 through 5 are active and will fly CAP at CapRoute 6 when 6 is active.
                 

Active and Backup CAP

diff --git a/_miz/SPEARHEADDEV.miz b/_miz/SPEARHEADDEV.miz index 1b1cbfe..8f67eb8 100644 Binary files a/_miz/SPEARHEADDEV.miz and b/_miz/SPEARHEADDEV.miz differ diff --git a/_miz/SPEARHEAD_CAP_DEV.miz b/_miz/SPEARHEAD_CAP_DEV.miz new file mode 100644 index 0000000..697738b Binary files /dev/null and b/_miz/SPEARHEAD_CAP_DEV.miz differ diff --git a/classes/capClasses/AirGroup.lua b/classes/capClasses/AirGroup.lua deleted file mode 100644 index 9ebfc66..0000000 --- a/classes/capClasses/AirGroup.lua +++ /dev/null @@ -1,74 +0,0 @@ - - ----@class AirGroup ----@field groupName string ----@field groupState GroupState -local AirGroup = {} - ----@alias GroupState ----| "UnSpawned" ----| "ReadOnTheRamp ----| "InTransit" ----| "OnStation" ----| "RtbInTen" ----| "Rtb" ----| "Dead" ----| "Rearming" - - ----@alias AirGroupType ----| "CAP" - ----| "CAS" ----| "SEAD" ----| "INTERCEPT" ----| "" - - ----@class GroupNameData ----@field type AirGroupType ----@field isBackup boolean ----@field zonesConfig table - -local function parseGroupName(groupName) - - local split_string = Spearhead.Util.split_string(groupName, "_") - local partCount = Spearhead.Util.tableLength(split_string) - - if partCount >= 3 then - - ---@type boolean - local isBackup = false - - do -- config - - - end - - else - Spearhead.AddMissionEditorWarning("CAP Group with name: " .. groupName .. "should have at least 3 parts, but has " .. partCount) - return nil - end - - -end - - ----comment ----@generic T: AirGroup ----@param o T ----@param groupName string ----@return T -function AirGroup.New(o, groupName) - AirGroup.__index = AirGroup - local o = o or {} - local self = setmetatable(o, AirGroup) - - self.groupName = groupName - self.groupState = "UnSpawned" - - - - return self -end - diff --git a/classes/capClasses/CapAirbase.lua b/classes/capClasses/CapAirbase.lua index 29741fb..f601487 100644 --- a/classes/capClasses/CapAirbase.lua +++ b/classes/capClasses/CapAirbase.lua @@ -1,22 +1,18 @@ ---@class CapBase : OnStageChangedListener ----@field groupNames Array ----@field database Database ----@field airbaseName string ----@field logger table ----@field activeStage number ----@field capConfig table ----@field activeCapStages number ----@field lastStatesByName table ----@field groupsByName table ----@field PrimaryGroups Array ----@field BackupGroups Array +---@field private airbaseName string +---@field private logger table +---@field private database Database +---@field private activeStage number +---@field private capConfig table +---@field private capGroupsByName table ---@field private runwayBombingTracker RunwayBombingTracker ---@field private runwayStrikeMissions table local CapBase = {} -local CheckReschedulingAsync = function(self, time) +local CheckStateContinuous = function(self, time) self:CheckAndScheduleCAP() + return time + 15 end ---comment @@ -28,181 +24,201 @@ end ---@param runwayBombingTracker RunwayBombingTracker ---@return CapBase function CapBase.new(airbaseName, database, logger, capConfig, stageConfig, runwayBombingTracker) - CapBase.__index = CapBase local self = setmetatable({}, { __index = CapBase }) --[[@as CapBase]] - local baseData = database:getAirbaseDataForZone(airbaseName) - local groupNames = {} - if baseData then groupNames = baseData.CapGroups or {} end - - self.groupNames = groupNames - self.database = database self.runwayBombingTracker = runwayBombingTracker self.runwayStrikeMissions = {} - + self.airbaseName = airbaseName self.logger = logger self.activeStage = 0 self.capConfig = capConfig - self.activeCapStages = (stageConfig or {}).capActiveStages or 10 + self.database = database + self.capGroupsByName = {} - self.lastStatesByName = {} - self.groupsByName = {} - self.PrimaryGroups = {} - self.BackupGroups = {} - - for key, name in pairs(self.groupNames) do - local capGroup = Spearhead.classes.capClasses.CapGroup.new(name, airbaseName, logger, database, capConfig) - if capGroup then - self.groupsByName[name] = capGroup - - if capGroup.isBackup ==true then - table.insert(self.BackupGroups, capGroup) - else - table.insert(self.PrimaryGroups, capGroup) + local baseData = database:getAirbaseDataForZone(airbaseName) + if baseData and baseData.CapGroups then + for key, name in pairs(baseData.CapGroups) do + local capGroup = Spearhead.classes.capClasses.airGroups.CapGroup.New(name, capConfig, logger) + if capGroup then + self.capGroupsByName[name] = capGroup end - - capGroup:AddOnStateUpdatedListener(self) end end - logger:info("Airbase with name '" .. airbaseName .. "' has a total of " .. Spearhead.Util.tableLength(self.groupsByName) .. " cap flights registered") - self:CreateRunwayStrikeMission() + logger:info("Airbase with name '" .. + airbaseName .. "' has a total of " .. Spearhead.Util.tableLength(self.capGroupsByName) .. " cap flights registered") + + self:CreateRunwayStrikeMission(database) Spearhead.Events.AddStageNumberChangedListener(self) - return self + timer.scheduleFunction(CheckStateContinuous, self, timer.getTime() + 15) + + return self end ---@private -function CapBase:CreateRunwayStrikeMission() - +---@param database Database +function CapBase:CreateRunwayStrikeMission(database) local airbase = Airbase.getByName(self.airbaseName) - if not airbase then + if not airbase then self.logger:debug("Could not find a airbase with name to create runway mission" .. self.airbaseName) - return + return end for _, runway in pairs(airbase:getRunways()) do if runway then - self.logger:debug("Runway " .. runway.Name .. " at airbase " .. self.airbaseName .. " with heading " .. runway.course .. " and length " .. runway.length .. " and width " .. runway.width) - local mission = Spearhead.classes.stageClasses.missions.RunwayStrikeMission.new(runway, self.airbaseName, self.database, self.logger, self.runwayBombingTracker) + self.logger:debug("Runway " .. + runway.Name .. + " at airbase " .. + self.airbaseName .. + " with heading " .. runway.course .. " and length " .. runway.length .. " and width " .. runway.width) + local mission = Spearhead.classes.stageClasses.missions.RunwayStrikeMission.new(runway, self.airbaseName, + database, self.logger, self.runwayBombingTracker) self.runwayStrikeMissions[runway.Name] = mission end end end -function CapBase:onGroupStateUpdated(capGroup) - --[[ - There is no update needed for INTRANSIT, ONSTATION or REARMING as the PREVIOUS state already was checked and nothing changes in the actual overal state. - ]]-- - if capGroup.state == Spearhead.classes.capClasses.CapGroup.GroupState.INTRANSIT - or capGroup.state == Spearhead.classes.capClasses.CapGroup.GroupState.ONSTATION - or capGroup.state == Spearhead.classes.capClasses.CapGroup.GroupState.REARMING - then - return +function CapBase:SpawnIfApplicable() + self.logger:debug("Check spawns for airbase " .. self.airbaseName) + for groupName, capGroup in pairs(self.capGroupsByName) do + local targetStage = capGroup:GetZoneIDWhenStageID(tostring(self.activeStage)) + + if targetStage ~= nil and capGroup:GetState() == "UnSpawned" then + capGroup:Spawn() + end end - timer.scheduleFunction(CheckReschedulingAsync, self, timer.getTime() + 1) end -function CapBase:SpawnIfApplicable() - self.logger:debug("Check spawns for airbase " .. self.airbaseName ) - for groupName, capGroup in pairs(self.groupsByName) do - - local targetStage = capGroup:GetTargetZone(self.activeStage) +function CapBase:CheckAndScheduleCAP() + self.logger:debug("Check taskings for airbase " .. self.airbaseName) - if targetStage ~= nil and capGroup.state == Spearhead.classes.capClasses.CapGroup.GroupState.UNSPAWNED then - capGroup:SpawnOnTheRamp() - end - end + local countPerStage = {} + local requiredPerStage = {} + + local airbase = Airbase.getByName(self.airbaseName) + if not airbase then + return nil end -function CapBase:CheckAndScheduleCAP() + local activeStageID = tostring(self.activeStage) - self.logger:debug("Check taskings for airbase " .. self.airbaseName ) - - local countPerStage = {} - local requiredPerStage = {} + --Count back up groups that are active or reassign to the new zone if that's needed + for _, group in pairs(self.capGroupsByName) do + if group:IsBackup() == true then + local state = group:GetState() + if state == "InTransit" or state == "OnStation" or state == "RtbInTen" then + + local supposedTargetZoneID = group:GetZoneIDWhenStageID(activeStageID) + local currentTargetZone = group:GetCurrentTargetZoneID() - --Count back up groups that are active or reassign to the new zone if that's needed - for _, backupGroup in pairs(self.BackupGroups) do - if backupGroup.state == Spearhead.classes.capClasses.CapGroup.GroupState.INTRANSIT or backupGroup.state == Spearhead.classes.capClasses.CapGroup.GroupState.ONSTATION then - local supposedTargetStage = backupGroup:GetTargetZone(self.activeStage) - if supposedTargetStage then - if supposedTargetStage ~= backupGroup.assignedStageNumber then - backupGroup:SendToStage(supposedTargetStage) - end - - if countPerStage[supposedTargetStage] == nil then - countPerStage[supposedTargetStage] = 0 - end - countPerStage[supposedTargetStage] = countPerStage[supposedTargetStage] + 1 + if supposedTargetZoneID == nil then + self.logger:debug("CapGroup " .. group:GetName() .. " has no target zone for stage " .. activeStageID) + group:SendRTB(airbase) else - backupGroup:SendRTBAndDespawn() - end - elseif backupGroup.state == Spearhead.classes.capClasses.CapGroup.GroupState.RTBINTEN and backupGroup:GetTargetZone(self.activeStage) ~= backupGroup.assignedStageNumber then - backupGroup:SendRTB() - end - end - - --Schedule or reassign primary units if applicable - for _, primaryGroup in pairs(self.PrimaryGroups) do - local supposedTargetStage = primaryGroup:GetTargetZone(self.activeStage) - if supposedTargetStage then - if requiredPerStage[supposedTargetStage] == nil then - requiredPerStage[supposedTargetStage] = 0 - end - - if countPerStage[supposedTargetStage] == nil - then - countPerStage[supposedTargetStage] = 0 - end - - requiredPerStage[supposedTargetStage] = requiredPerStage[supposedTargetStage] + 1 - - if primaryGroup.state == Spearhead.classes.capClasses.CapGroup.GroupState.READYONRAMP then - if countPerStage[supposedTargetStage] < requiredPerStage[supposedTargetStage] then - primaryGroup:SendToStage(supposedTargetStage) - countPerStage[supposedTargetStage] = countPerStage[supposedTargetStage] + 1 - end - elseif primaryGroup.state == Spearhead.classes.capClasses.CapGroup.GroupState.INTRANSIT or primaryGroup.state == Spearhead.classes.capClasses.CapGroup.GroupState.ONSTATION then - if supposedTargetStage ~= primaryGroup.assignedStageNumber then - if countPerStage[supposedTargetStage] < requiredPerStage[supposedTargetStage] then - primaryGroup:SendToStage(supposedTargetStage) + if supposedTargetZoneID and supposedTargetZoneID ~= currentTargetZone then + if state == "RtbInTen" then + self.logger:debug("CapGroup " .. group:GetName() .. " is RTB in 10 minutes, sending to RTB already") + group:SendRTB(airbase) else - countPerStage[supposedTargetStage] = countPerStage[supposedTargetStage] + 1 - primaryGroup:SendRTB() + local triggerZone = self.database:GetCapZoneForZoneID(supposedTargetZoneID) + if triggerZone then + group:SendToZone(triggerZone, supposedTargetZoneID, airbase) + else + self.logger:debug("CapGroup " .. group:GetName() .. " has no trigger zone for stage " .. activeStageID) + group:SendRTB(airbase) + end end end - countPerStage[supposedTargetStage] = countPerStage[supposedTargetStage] + 1 - elseif primaryGroup.state == Spearhead.classes.capClasses.CapGroup.GroupState.RTBINTEN and primaryGroup:GetTargetZone(self.activeStage) ~= primaryGroup.assignedStageNumber then - primaryGroup:SendRTB() - end - else - primaryGroup:SendRTBAndDespawn() - end - end + + if countPerStage[supposedTargetZoneID] == nil then + countPerStage[supposedTargetZoneID] = 0 + end - for _, backupGroup in pairs(self.BackupGroups) do - if backupGroup.state == Spearhead.classes.capClasses.CapGroup.GroupState.READYONRAMP then - local supposedTargetStage = backupGroup:GetTargetZone(self.activeStage) - if supposedTargetStage then - if countPerStage[supposedTargetStage] == nil then - countPerStage[supposedTargetStage] = 0 + if supposedTargetZoneID == group:GetCurrentTargetZoneID() and (state == "OnStation" or state =="InTransit") then + countPerStage[supposedTargetZoneID] = countPerStage[supposedTargetZoneID] + 1 end - - if countPerStage[supposedTargetStage] < requiredPerStage[supposedTargetStage] then - backupGroup:SendToStage(supposedTargetStage) - countPerStage[supposedTargetStage] = countPerStage[supposedTargetStage] + 1 - end - else - backupGroup:SendRTBAndDespawn() end end end end + --Schedule or reassign primary units if applicable + for _, group in pairs(self.capGroupsByName) do + if group:IsBackup() == false then + local state = group:GetState() + local supposedZone = group:GetZoneIDWhenStageID(activeStageID) + if supposedZone then + if requiredPerStage[supposedZone] == nil then + requiredPerStage[supposedZone] = 0 + end + + if countPerStage[supposedZone] == nil then + countPerStage[supposedZone] = 0 + end + + requiredPerStage[supposedZone] = requiredPerStage[supposedZone] + 1 + + if state == "ReadyOnTheRamp" then + if countPerStage[supposedZone] < requiredPerStage[supposedZone] then + local triggerZone = self.database:GetCapZoneForZoneID(supposedZone) + if triggerZone then + group:SendToZone(triggerZone, supposedZone, airbase) + end + + countPerStage[supposedZone] = countPerStage[supposedZone] + 1 + end + elseif state == "InTransit" or state == "OnStation" then + + if supposedZone ~= group:GetCurrentTargetZoneID() then + if countPerStage[supposedZone] < requiredPerStage[supposedZone] then + local triggerZone = self.database:GetCapZoneForZoneID(supposedZone) + if triggerZone then + group:SendToZone(triggerZone, supposedZone, airbase) + else + group:SendRTB(airbase) + end + end + end + countPerStage[supposedZone] = countPerStage[supposedZone] + 1 + elseif state == "RtbInTen" and supposedZone ~= group:GetCurrentTargetZoneID() then + group:SendRTB(airbase) + end + else + if state == "InTransit" or state == "OnStation" or state == "RtbInTen" then + -- If the group is in transit or on station but has no target zone, send it back to base + group:SendRTB(airbase) + end + end + end + end + + for _, group in pairs(self.capGroupsByName) do + if group:IsBackup() == true then + if group:GetState() == "ReadyOnTheRamp" then + local supposedZone = group:GetZoneIDWhenStageID(activeStageID) + if supposedZone then + if countPerStage[supposedZone] == nil then + countPerStage[supposedZone] = 0 + end + + if countPerStage[supposedZone] < requiredPerStage[supposedZone] then + local triggerZone = self.database:GetCapZoneForZoneID(supposedZone) + if triggerZone then + group:SendToZone(triggerZone, supposedZone, airbase) + end + + countPerStage[supposedZone] = countPerStage[supposedZone] + 1 + end + end + end + end + end +end + function CapBase:OnStageNumberChanged(number) self.activeStage = number @@ -211,25 +227,21 @@ function CapBase:OnStageNumberChanged(number) mission:SpawnActive() end end - self:SpawnIfApplicable() - timer.scheduleFunction(CheckReschedulingAsync, self, timer.getTime() + 5) end - ---@param stageNumber number ---@return boolean function CapBase:IsBaseActiveWhenStageIsActive(stageNumber) - for _, group in pairs(self.PrimaryGroups) do - local target = group:GetTargetZone(stageNumber) - if target ~= nil then + for _, group in pairs(self.capGroupsByName) do + local target = group:GetZoneIDWhenStageID(tostring(stageNumber)) + if group:IsBackup() == false and target ~= nil then return true end end return false end - if not Spearhead then Spearhead = {} end if not Spearhead.classes then Spearhead.classes = {} end if not Spearhead.classes.capClasses then Spearhead.classes.capClasses = {} end diff --git a/classes/capClasses/CapGroup.lua b/classes/capClasses/CapGroup.lua deleted file mode 100644 index 0039699..0000000 --- a/classes/capClasses/CapGroup.lua +++ /dev/null @@ -1,429 +0,0 @@ - - -local CapHelper = {} -do - ---comment - ---@param groupName string - ---@return table? - CapHelper.ParseGroupName = function(groupName) - local split_string = Spearhead.Util.split_string(groupName, "_") - local partCount = Spearhead.Util.tableLength(split_string) - if partCount >= 3 then - local result = {} - result.zonesConfig = {} - - -- CAP_[1-5]5|[6]6|[7]7_Sukhoi - -- CAP_[1-5,7]A|[6]7_Sukhoi - - local configPart = split_string[2] - local first = configPart:sub(1, 1) - if first == "A" then - result.isBackup = false - configPart = string.sub(configPart, 2, #configPart) - elseif first == "B" then - configPart = string.sub(configPart, 2, #configPart) - result.isBackup = true - elseif first == "[" then - result.isBackup = false - else - Spearhead.AddMissionEditorWarning("Could not parse the CAP config for group: " .. groupName) - return nil - end - - local subsplit = Spearhead.Util.split_string(configPart, "|") - if subsplit then - for key, value in pairs(subsplit) do - local keySplit = Spearhead.Util.split_string(value, "]") - local targetZone = keySplit[2] - local allActives = string.sub(keySplit[1], 2, #keySplit[1]) - local commaSeperated = Spearhead.Util.split_string(allActives, ",") - for _, value in pairs(commaSeperated) do - local dashSeperated = Spearhead.Util.split_string(value, "-") - if Spearhead.Util.tableLength(dashSeperated) > 1 then - local from = tonumber(dashSeperated[1]) - local till = tonumber(dashSeperated[2]) - - for i = from, till do - if targetZone == "A" then - result.zonesConfig[tostring(i)] = tostring(i) - else - result.zonesConfig[tostring(i)] = tostring(targetZone) - end - end - else - if targetZone == "A" then - result.zonesConfig[tostring(dashSeperated[1])] = tostring(dashSeperated[1]) - else - result.zonesConfig[tostring(dashSeperated[1])] = tostring(targetZone) - end - end - end - end - end - return result - else - Spearhead.AddMissionEditorWarning("CAP Group with name: " .. groupName .. "should have at least 3 parts, but has " .. partCount) - return nil - end - end -end - ----comment ----@param input table { groupName, task, logger } ----@param time number ----@return nil -local function setTaskAsync(input, time) - local task = input.task - local groupName = input.groupName - local group = Group.getByName(groupName) - - if task and group then - group:getController():setTask(task) - if input.logger ~= nil then - input.logger:debug("task set succesfully to group " .. groupName) - end - end - return nil -end - - ----@class OnUpdateListener ----@field onGroupStateUpdated fun(self: OnUpdateListener, capGroup: CapGroup) - ----@class CapGroup : OnUnitLostListener ----@field isBackup boolean ----@field groupName string ----@field assignedStageNumber string ----@field private airbaseName string ----@field private logger Logger ----@field private database Database ----@field private capConfig table ----@field private capZonesConfig table ----@field private aliveUnits table ----@field private onStatusUpdatedListener Array -local CapGroup = {} - -CapGroup.GroupState = { - UNSPAWNED = 0, - READYONRAMP = 1, - INTRANSIT = 2, - ONSTATION = 3, - RTBINTEN = 4, - RTB = 5, - DEAD = 6, - REARMING = 7 -} - -local function SetReadyOnRampAsync(self, time) - self:SetState(CapGroup.GroupState.READYONRAMP) -end - -local RESPAWN_AFTER_TOUCHDOWN_SECONDS = 180 - ----comment ----@param groupName string ----@param airbaseName string ----@param logger Logger logger dependency injection ----@param database Database database dependency injection ----@param capConfig table config dependency injection ----@return CapGroup? self -function CapGroup.new(groupName, airbaseName, logger, database, capConfig) - - CapGroup.__index = CapGroup - local self = setmetatable({}, CapGroup) - - Spearhead.DcsUtil.DestroyGroup(groupName) - - -- initials - self.groupName = groupName - self.airbaseName = airbaseName - - local airbase = Airbase.getByName(airbaseName) - if airbase == nil then - logger:error("Airbase with name " .. airbaseName .. " does not exist") - return nil - end - - self.airbaseId = airbase:getID() - self.logger = logger - self.database = database - - local parsed = CapHelper.ParseGroupName(groupName) - if parsed == nil then return nil end - self.capZonesConfig = parsed.zonesConfig - self.isBackup = parsed.isBackup - - --vars - self.assignedStageNumber = nil - - self.state = CapGroup.GroupState.UNSPAWNED - self.aliveUnits = {} - self.landedUnits = {} - self.unitCount = 0 - self.onStationSince = 0 - self.currentCapTaskingDuration = 0 - self.markedForDespawn = false - - self.onStatusUpdatedListener = {} - - --config - self.capConfig = capConfig - - Spearhead.Events.addOnGroupRTBListener(self.groupName, self) - Spearhead.Events.addOnGroupRTBInTenListener(self.groupName, self) - Spearhead.Events.addOnGroupOnStationListener(self.groupName, self) - local units = Group.getByName(groupName):getUnits() - for key, unit in pairs(units) do - Spearhead.Events.addOnUnitLandEventListener(unit:getName(), self) - Spearhead.Events.addOnUnitLostEventListener(unit:getName(), self) - end - - return self -end - ----@param currentActive number ----@return string -function CapGroup:GetTargetZone(currentActive) - return self.capZonesConfig[tostring(currentActive)] -end - -function CapGroup:SetState(state) - self.state = state - self:PublishUnitUpdatedEvent() -end - -function CapGroup:StartRearm() - self:SpawnOnTheRamp() - self:SetState(CapGroup.GroupState.REARMING) - timer.scheduleFunction(SetReadyOnRampAsync, self, timer.getTime() + self.capConfig:getRearmDelay() - RESPAWN_AFTER_TOUCHDOWN_SECONDS) -end - -function CapGroup:SpawnOnTheRamp() - self.markedForDespawn = false - self.logger:debug("Spawning group " .. self.groupName) - self.aliveUnits = {} - self.landedUnits = {} - self.onStationSince = 0 - - local group = Spearhead.DcsUtil.SpawnGroupTemplate(self.groupName, nil, nil, true) - if group then - self.unitCount = group:getInitialSize() - - if self.state == CapGroup.GroupState.UNSPAWNED then - self:SetState(CapGroup.GroupState.READYONRAMP) - end - - for _, unit in pairs(group:getUnits()) do - local name = unit:getName() - self.aliveUnits[name] = true - self.landedUnits[name] = false - end - end -end - -function CapGroup:Despawn() - self.logger:debug("Despawning group " .. self.groupName) - Spearhead.DcsUtil.DestroyGroup(self.groupName) - self:SetState(CapGroup.GroupState.UNSPAWNED) -end - -function CapGroup:SendRTB() - local group = Group.getByName(self.groupName) - if group and group:isExist() then - local speed = math.random(self.capConfig:getMinSpeed(), self.capConfig:getMaxSpeed()) - local rtbTask, errormessage = Spearhead.RouteUtil.CreateRTBMission(self.groupName, self.airbaseId, speed) - if rtbTask then - timer.scheduleFunction(setTaskAsync, { task = rtbTask, groupName = self.groupName, logger = self.logger }, timer.getTime() + 3) - else - self.logger:error("No RTB task could be created for group: " .. self.groupName .. " due to " .. errormessage) - if self.markedForDespawn == true then - self:Despawn() - end - end - end -end - -function CapGroup:SendRTBAndDespawn() - self.markedForDespawn = true - self:SendRTB() -end - ----@param stageZoneNumber string -function CapGroup:SendToStage(stageZoneNumber) - if self.state == CapGroup.GroupState.DEAD or self.state == CapGroup.GroupState.RTB then - return --Can't task a unit that's dead or RTB - end - - self.assignedStageNumber = stageZoneNumber - local group = Group.getByName(self.groupName) - if group and group:isExist() then - self.logger:debug("Sending group out " .. self.groupName) - local controller = group:getController() - local capPoints = self.database:getCapRouteInZone(stageZoneNumber, self.airbaseName) - - if not capPoints then - self.logger:error("No CAP route found for group " .. self.groupName .. " in zone " .. stageZoneNumber) - return - end - - local altitude = math.random(self.capConfig:getMinAlt(), self.capConfig:getMaxAlt()) - local speed = math.random(self.capConfig:getMinSpeed(), self.capConfig:getMaxSpeed()) - local attackHelos = false - local deviationDistance = self.capConfig:getMaxDeviationRange() - local capTask - if self.state == CapGroup.GroupState.ONRAMP or self.onStationSince == 0 then - controller:setCommand({ - id = 'Start', - params = {} - }) - local duration = math.random(self.capConfig:getMinDurationOnStation(), self.capConfig:getmaxDurationOnStation()) - self.currentCapTaskingDuration = duration - - - capTask = Spearhead.RouteUtil.createCapMission(self.groupName, self.airbaseId, capPoints.point1, capPoints.point2, altitude, speed, duration, attackHelos, deviationDistance) - else - local duration = self.currentCapTaskingDuration - (timer.getTime() - self.onStationSince) - capTask = Spearhead.RouteUtil.createCapMission(self.groupName, self.airbaseId, capPoints.point1, capPoints.point2, altitude, speed, duration, attackHelos, deviationDistance) - end - - if capTask then - timer.scheduleFunction(setTaskAsync, - { task = capTask, groupName = self.groupName, logger = self.logger }, timer.getTime() + 3) - end - self:SetState(CapGroup.GroupState.INTRANSIT) - end -end - ----@param airdomeId any -function CapGroup:SendToAirbase(airdomeId) - self.airbaseId = airdomeId - local speed = math.random(self.capConfig:getMinSpeed(), self.capConfig:getMaxSpeed()) - local rtbTask = Spearhead.RouteUtil.CreateRTBMission(self.groupName, airdomeId, speed) - local group = Group.getByName(self.groupName) - - if not group then return end - local controller = group:getController() - controller:setCommand({ - id = 'Start', - params = {} - }) - timer.scheduleFunction(setTaskAsync, { task = rtbTask, groupName = self.groupName, logger = self.logger }, - timer.getTime() + 5) -end - -function CapGroup:OnGroupRTB(groupName) - if groupName == self.groupName then - self.logger:debug("Setting group " .. - groupName .. - " to state RTB after a total of " .. - timer.getTime() - self.onStationSince .. "s of the " .. self.currentCapTaskingDuration .. "s") - self:SetState(CapGroup.GroupState.RTB) - end -end - -function CapGroup:OnGroupRTBInTen(groupName) - if groupName == self.groupName then - self:SetState(CapGroup.GroupState.RTBINTEN) - end -end - -function CapGroup:OnGroupOnStation(groupName) - if groupName == self.groupName then - self.onStationSince = timer.getTime() - self.logger:debug("Setting group " .. groupName .. " to state Onstation") - self:SetState(CapGroup.GroupState.ONSTATION) - end -end - - ----@param proActive boolean Will check all units in group for aliveness -function CapGroup:UpdateState(proActive) - local landed = false - local landedCount = 0 - for name, landedBool in pairs(self.landedUnits) do - if landedBool == true then - landedCount = landedCount + 1 - landed = true - end - end - - local deadCount = 0 - for name, isAlive in pairs(self.aliveUnits) do - if isAlive == false then - deadCount = deadCount + 1 - end - end - - local function DelayedStartRearm(input, time) - local capGroup = input.self - capGroup:StartRearm() - end - - if landedCount + deadCount == self.unitCount then - if landed then - if self.markedForDespawn == true then - self:Despawn() - else - timer.scheduleFunction(DelayedStartRearm, { self = self }, timer.getTime() + RESPAWN_AFTER_TOUCHDOWN_SECONDS) - end - else - if self.markedForDespawn == true then - self:Despawn() - else - local delay = self.capConfig:getDeathDelay() - self.capConfig:getRearmDelay() + RESPAWN_AFTER_TOUCHDOWN_SECONDS - timer.scheduleFunction(DelayedStartRearm, { self = self }, timer.getTime() + delay) - end - end - end -end - - ----@param listener table object with function OnGroupStateUpdated(capGroupTable) -function CapGroup:AddOnStateUpdatedListener(listener) - if type(listener) ~= "table" then - self.logger:error("Listener not of type table for AddOnStateUpdatedListener") - return - end - - if listener.onGroupStateUpdated == nil then - self.logger:error("Listener does not implement onGroupStateUpdated") - return - end - table.insert(self.onStatusUpdatedListener, listener) -end - -function CapGroup:PublishUnitUpdatedEvent() - for _, callable in pairs(self.onStatusUpdatedListener) do - local _, error = pcall(function() - callable:onGroupStateUpdated(self) - end) - if error then - self.logger:error(error) - end - end -end - -function CapGroup:OnUnitLanded(initiatorUnit, airbase) - if airbase then - self.airbaseName = airbase:getName() - end - local name = initiatorUnit:getName() - self.logger:debug("Received unit land event for unit " .. name .. " of group " .. self.groupName) - - self.landedUnits[name] = true - self:UpdateState(false) -end - -function CapGroup:OnUnitLost(initiatorUnit) - self.logger:debug("Received unit lost event for group " .. self.groupName) - if initiatorUnit then - self.aliveUnits[initiatorUnit:getName()] = false - end - self:UpdateState(false) -end - - -if not Spearhead then Spearhead = {} end -if not Spearhead.classes then Spearhead.classes = {} end -if not Spearhead.classes.capClasses then Spearhead.classes.capClasses = {} end -Spearhead.classes.capClasses.CapGroup = CapGroup diff --git a/classes/capClasses/airGroups/AirGroup.lua b/classes/capClasses/airGroups/AirGroup.lua new file mode 100644 index 0000000..39ac951 --- /dev/null +++ b/classes/capClasses/airGroups/AirGroup.lua @@ -0,0 +1,427 @@ +---@class AirGroup : OnUnitLostListener +---@field protected _logger Logger +---@field protected _groupName string +---@field protected _groupType AirGroupType +---@field protected _state AirGroupState +---@field protected _isSpawned boolean +---@field protected _config CapConfig +---@field protected _checkLivenessNumber number +local AirGroup = {} +AirGroup.__index = AirGroup + +---@param logger Logger +---@param groupName string +---@param groupType AirGroupType +---@param config CapConfig +function AirGroup:New(groupName, groupType, config, logger) + self._groupName = groupName + self._groupType = groupType + self._isSpawned = false + self._state = "UnSpawned" -- Default state + self._config = config + self._logger = logger + + local group = Group.getByName(self._groupName) + if group then + Spearhead.Events.addOnGroupRTBListener(self._groupName, self) + Spearhead.Events.addOnGroupRTBInTenListener(self._groupName, self) + Spearhead.Events.addOnGroupOnStationListener(self._groupName, self) + + for _, unit in pairs(group:getUnits()) do + Spearhead.Events.addOnUnitLostEventListener(unit:getName(), self) + Spearhead.Events.addOnUnitLandEventListener(unit:getName(), self) + end + Spearhead.DcsUtil.DestroyGroup(self._groupName) + end +end + +function AirGroup:GetState() + return self._state +end + +function AirGroup:GetName() + return self._groupName +end + +function AirGroup:MarkRearmComplete() + self:Respawn(false) + if self._state == "Rearming" then + self:SetState("ReadyOnTheRamp") + end +end + +---@protected +function AirGroup:SetMission(mission) + self:SetState("InTransit") + + local group = Group.getByName(self._groupName) + if group then + local controller = group:getController() + if controller then + controller:setCommand({ + id = 'Start', + params = {} + }) + end + end + + local setMissionDelayed = function(data, time) + data.self:SetMissionPrivate(data.mission) + end + + local data = { + self = self, + mission = mission + } + + timer.scheduleFunction(setMissionDelayed, data, timer.getTime() + 5) +end + +function AirGroup:SetMissionPrivate(mission) + self:SetState("InTransit") + local group = Group.getByName(self._groupName) + if group and mission then + group:getController():setTask(mission) + self._logger:debug("mission - Task set for group: " .. self._groupName) + end +end + +---@param airbase Airbase +function AirGroup:SendRTB(airbase) + self._logger:debug("AirGroup:SendRTB called for group: " .. self._groupName) + self:SetState("Rtb") + local group = Group.getByName(self._groupName) + if group then + ---@type Vec3 + local location = nil + for _, unit in pairs(group:getUnits()) do + if unit and unit:isExist() == true and unit:inAir() == true then + location = unit:getPoint() + break + end + end + if location then + local mission = Spearhead.classes.capClasses.taskings.RTB.getAsMission(airbase, { x= location.x, y= location.z }, self._config) + group:getController():setTask(mission) + self._logger:debug("AirGroup:SendRTB - Task set for group: " .. self._groupName) + end + end +end + +function AirGroup:Spawn() + if self._isSpawned then return end + self:SpawnInternal(false) +end + +---@param force boolean +---@param withoutLoadout boolean? +---@protected +function AirGroup:SpawnInternal(force, withoutLoadout) + + if withoutLoadout == nil then withoutLoadout = false end + + if self._isSpawned and force ~= true then return end + + local group, isStatic = Spearhead.DcsUtil.SpawnGroupTemplate(self._groupName, nil, nil, true, nil, withoutLoadout) + if isStatic == true then + --- If For Some reaons someone tries to schedule static units as CAP classes + self._state = "UnSpawned" + return + end + + if group then + self._group = group + self._isSpawned = true + self._initialSize = #group:getUnits() + self._liveState = {} + else + self._logger:error("Failed to spawn group: " .. self._groupName) + end + + if self._state == "UnSpawned" then + self:SetState("ReadyOnTheRamp") + end + + ---@param selfA AirGroup + local function CheckLivenessTask(selfA, time) + local interval = selfA:CheckLiveness() + if not interval then return end + return time + interval + end + + if self._checkLivenessNumber then + timer.removeFunction(self._checkLivenessNumber) + end + + self._checkLivenessNumber = timer.scheduleFunction(CheckLivenessTask, self, timer.getTime() + 5) +end + + +---@param withoutLoadout boolean? +function AirGroup:Respawn(withoutLoadout) + self:SpawnInternal(true, withoutLoadout) +end + +---@protected +---@param state AirGroupState +function AirGroup:SetState(state) + if self._state == state then return end + self._state = state + self._logger:debug("AirGroup:State changed for group: " .. self._groupName .. " to state: " .. state) +end + +---@return number? timeInterval +function AirGroup:CheckLiveness() + local isAlive = false + local group = Group.getByName(self._groupName) + + if group and group:isExist() == true then + for _, unit in pairs(group:getUnits()) do + if unit and unit:isExist() == true and unit:getLife() > (unit:getLife0() * 0.3) then + isAlive = true + end + end + end + + if isAlive == false then + self:SetState("Dead") + self:CheckStateAndStartRepairRearm() + return nil + end + + return 10 +end + +---@protected +function AirGroup:OnLastUnitLanded() + --- Once landed monitor the units until it's at it's designated location (or died) + + -- Units + + ---@class CheckGroupForRestartData + ---@field self AirGroup + ---@field lastLocations table + ---@field lastChangeTime number + + ---@param data CheckGroupForRestartData + ---@param time number + local checkGroupForRestart = function(data, time) + local group = Group.getByName(data.self:GetName()) + if not group then + self:CheckStateAndStartRepairRearm() + return + end + + for _, unit in pairs(group:getUnits()) do + if unit and unit:isExist() then + local pos = unit:getPoint() + + + if data.lastLocations[unit:getName()] == nil then + data.lastLocations[unit:getName()] = pos + return time + 5 + end + + if pos and Spearhead.Util.VectorDistance3d(pos, data.lastLocations[unit:getName()]) > 10 then + data.lastChangeTime = time + end + data.lastLocations[unit:getName()] = pos + end + end + + if data.lastChangeTime + 30 < time then + -- If no change in 30 seconds, assume all units are parked + local withoutLoadout = true + data.self:Respawn(withoutLoadout) + data.self:CheckStateAndStartRepairRearm() + return + end + + return time + 5 + end + + ---@type CheckGroupForRestartData + local data = { + self = self, + lastLocations = {}, + lastChangeTime = timer.getTime() + } + + local group = Group.getByName(self._groupName) + if group then + for _, unit in pairs(group:getUnits()) do + data.lastLocations[unit:getName()] = unit:getPoint() + end + end + timer.scheduleFunction(checkGroupForRestart, data, timer.getTime() + 5) + self._logger:debug("AirGroup:OnLastUnitLanded - Monitoring group: " .. self._groupName) +end + +function AirGroup:CheckStateAndStartRepairRearm() + self._logger:debug("AirGroup:CheckStateAndStartRepairRearm called for group: " .. self._groupName) + local group = Group.getByName(self._groupName) + local anyAlive = false + local allAlive = true + + if group then + for _, unit in pairs(group:getUnits()) do + if unit and unit:isExist() == true and unit:getLife() > (unit:getLife0() * 0.3) then + anyAlive = true + else + allAlive = false + end + end + end + + if anyAlive == false then + -- Schedule Spawn + Repair + Rearm + self:StartRespawn() + return + end + + if allAlive == false then + --- Schedule Spawn + Repair + Rearm + self:StartRepair() + return + end + + --- Reschedule Spawn + Rearm + self:StartRearm() +end + +do --- RESPAWN FUNCTIONS + --[[ + TODO: Checks to be added to the functions in case a group is destroyed while waiting for repair/rearm. + ]] + + function AirGroup:StartRespawn() + self:SetState("Dead") + + local respawnTask = function(selfA, time) + selfA:RepairDelayed() + end + + local delay = self._config:getDeathDelay() + if delay < 2 then + delay = 2 + end + return timer.scheduleFunction(respawnTask, self, timer.getTime() + delay) + end + + function AirGroup:StartRepair() + self:SetState("Repairing") + + if self._isSpawned == false then + self:Spawn() + end + + local rearmTask = function(selfA, time) + self:StartRearm() + end + + local delay = self._config:getRepairDelay() + if delay < 2 then + delay = 2 + end + + return timer.scheduleFunction(rearmTask, self, timer.getTime() + delay) + end + + function AirGroup:StartRearm() + self:SetState("Rearming") + + if self._isSpawned == false then + self:Spawn() + end + + local rearmDelay = self._config:getRearmDelay() + if rearmDelay < 2 then + rearmDelay = 2 + end + + ---@param selfA AirGroup + local rearmTask = function(selfA, time) + selfA:MarkRearmComplete() + end + + -- Schedule Rearm Complete + return timer.scheduleFunction(rearmTask, self, timer.getTime() + rearmDelay) + end +end + + +do --EVENT LISTENERS + ---@param unit Unit + function AirGroup:OnUnitLost(unit) + self:CheckLiveness() + end + + ---@param groupName string + function AirGroup:OnGroupRTBInTen(groupName) + if self._groupName == groupName then + self._logger:debug("AirGroup:OnGroupRTBInTen called for group: " .. self._groupName) + self:SetState("RtbInTen") + end + end + + ---@param groupName string + function AirGroup:OnGroupRTB(groupName) + if self._groupName == groupName then + self._logger:debug("AirGroup:OnGroupRTB called for group: " .. self._groupName) + self:SetState("Rtb") + end + end + + function AirGroup:OnUnitLanded(unit, airbase) + local anyInAir = false + local group = Group.getByName(self._groupName) + if group then + for _, u in pairs(group:getUnits()) do + if u and u:isExist() == true and u:inAir() == true then + anyInAir = true + break + end + end + end + + if not anyInAir then + self:OnLastUnitLanded() + end + end + + function AirGroup:OnGroupOnStation(groupName) + if self._groupName == groupName then + self:SetState("OnStation") + end + end +end + +function AirGroup:Destroy() + Spearhead.DcsUtil.DestroyGroup(self._groupName) +end + +if not Spearhead then Spearhead = {} end +if not Spearhead.classes then Spearhead.classes = {} end +if not Spearhead.classes.capClasses then Spearhead.classes.capClasses = {} end +if not Spearhead.classes.capClasses.airGroups then Spearhead.classes.capClasses.airGroups = {} end +Spearhead.classes.capClasses.airGroups.AirGroup = AirGroup + +---@alias AirGroupState +---| "UnSpawned" +---| "ReadyOnTheRamp +---| "InTransit" +---| "OnStation" +---| "RtbInTen" +---| "Rtb" +---| "Dead" +---| "Repairing" +---| "Rearming" + + +---@alias AirGroupType +---| "CAP" + +---| "CAS" +---| "SEAD" +---| "INTERCEPT" +---| "" diff --git a/classes/capClasses/airGroups/CapGroup.lua b/classes/capClasses/airGroups/CapGroup.lua new file mode 100644 index 0000000..f489302 --- /dev/null +++ b/classes/capClasses/airGroups/CapGroup.lua @@ -0,0 +1,144 @@ + +---@class CapGroup : AirGroup +---@field private _targetZoneIdPerStage table +---@field private _isBackup boolean +---@field private _currentTargetZoneID string? +local CapGroup = {} +CapGroup.__index = CapGroup + + +---@param groupName string +---@param config CapConfig +---@param logger Logger +---@return CapGroup +function CapGroup.New(groupName, config, logger) + + setmetatable(CapGroup, Spearhead.classes.capClasses.airGroups.AirGroup) + local self = setmetatable({}, CapGroup) --[[@as CapGroup]] + Spearhead.classes.capClasses.airGroups.AirGroup.New(self, groupName, "CAP", config, logger) + + self._targetZoneIdPerStage = {} + + self:InitWithName(groupName) + + return self +end + +---@return boolean +function CapGroup:IsBackup() + return self._isBackup +end + +---@return string? +function CapGroup:GetZoneIDWhenStageID(stageID) + return self._targetZoneIdPerStage[stageID] +end + +---@return string? +function CapGroup:GetCurrentTargetZoneID() + return self._currentTargetZoneID +end + +---@param zone SpearheadTriggerZone +---@param targetZoneID string +---@param airbase Airbase +function CapGroup:SendToZone(zone, targetZoneID, airbase) + + self._logger:debug("Airgroup " .. self._groupName .. " called to zone: " .. zone.name) + + self._currentTargetZoneID = targetZoneID + local group = Group.getByName(self._groupName) + + local isInAir = false + if group then + local units = group:getUnits() + for _, unit in pairs(units) do + if unit:inAir() == true then + isInAir = true + break + end + end + else + self._logger:debug("CapGroup:SendToZone - Group not found: " .. self._groupName) + return + end + + if isInAir == true then + local mission = Spearhead.classes.capClasses.taskings.CAP.getAsMission(self._groupName, airbase, zone, self._config) + self:SetMission(mission) + else + local mission = Spearhead.classes.capClasses.taskings.CAP.getAsMissionFromAirbase(self._groupName, airbase, zone, self._config) + if mission then + self:SetMission(mission) + else + self._logger:warn("CapGroup:SendToZone - Mission could not be created for group: " .. self._groupName) + return + end + end +end + +---@private +function CapGroup:InitWithName(groupName) + local split_string = Spearhead.Util.split_string(groupName, "_") + local partCount = Spearhead.Util.tableLength(split_string) + if partCount >= 3 then + + local configPart = split_string[2] + local first = configPart:sub(1, 1) + if first == "A" then + self._isBackup = false + configPart = string.sub(configPart, 2, #configPart) + elseif first == "B" then + configPart = string.sub(configPart, 2, #configPart) + self._isBackup = true + elseif first == "[" then + self._isBackup = false + else + Spearhead.AddMissionEditorWarning("Could not parse the CAP config for group: " .. groupName) + return + end + + local subsplit = Spearhead.Util.split_string(configPart, "|") + if subsplit then + for key, value in pairs(subsplit) do + local keySplit = Spearhead.Util.split_string(value, "]") + local targetZone = keySplit[2] + local allActives = string.sub(keySplit[1], 2, #keySplit[1]) + local commaSeperated = Spearhead.Util.split_string(allActives, ",") + for _, value in pairs(commaSeperated) do + local dashSeperated = Spearhead.Util.split_string(value, "-") + if Spearhead.Util.tableLength(dashSeperated) > 1 then + local from = tonumber(dashSeperated[1]) + local till = tonumber(dashSeperated[2]) + + for i = from, till do + if targetZone == "A" then + self._targetZoneIdPerStage[tostring(i)] = tostring(i) + else + self._targetZoneIdPerStage[tostring(i)] = targetZone + end + end + else + if targetZone == "A" then + self._targetZoneIdPerStage[tostring(dashSeperated[1])] = tostring(dashSeperated[1]) + else + self._targetZoneIdPerStage[tostring(dashSeperated[1])] = targetZone + end + end + end + end + end + + env.info("Capgroup parsed with table: " .. Spearhead.Util.toString(self._targetZoneIdPerStage)) + + else + Spearhead.AddMissionEditorWarning("CAP Group with name: " .. groupName .. "should have at least 3 parts, but has " .. partCount) + return nil + end +end + +if not Spearhead then Spearhead = {} end +if not Spearhead.classes then Spearhead.classes = {} end +if not Spearhead.classes.capClasses then Spearhead.classes.capClasses = {} end +if not Spearhead.classes.capClasses.airGroups then Spearhead.classes.capClasses.airGroups = {} end +Spearhead.classes.capClasses.airGroups.CapGroup = CapGroup \ No newline at end of file diff --git a/classes/capClasses/runwayBombing/RunwayBombingTracker.lua b/classes/capClasses/runwayBombing/RunwayBombingTracker.lua index ab64ceb..b6a945c 100644 --- a/classes/capClasses/runwayBombing/RunwayBombingTracker.lua +++ b/classes/capClasses/runwayBombing/RunwayBombingTracker.lua @@ -21,16 +21,12 @@ end ---@param weapon Weapon function RunwayBombingTracker:OnWeaponFired(unit, weapon, target) - if weapon == nil then + if weapon == nil then return end - self._logger:debug("Tracking weapon for runway impact onWeaponFired") - local desc = weapon:getDesc() - local isTrackable = desc.category == Weapon.Category.BOMB or (desc.category == Weapon.Category.MISSILE and desc.missileCategory == Weapon.MissileCategory.CRUISE) - if isTrackable == true then ---@type WeaponTrackingArgs diff --git a/classes/capClasses/taskings/CAP.lua b/classes/capClasses/taskings/CAP.lua new file mode 100644 index 0000000..d198e39 --- /dev/null +++ b/classes/capClasses/taskings/CAP.lua @@ -0,0 +1,327 @@ +---@class CAPTasking +local CAP = {} + +---@param attackHelos boolean +---@return table +local function GetCAPTargetTypes(attackHelos) + local targetTypes = { + [1] = "Planes", + } + + if attackHelos then + targetTypes[2] = "Helicopters" + end + + return targetTypes +end + +---@class CapTaskingOptions +---@field furthest Vec2 +---@field closest Vec2 +---@field legLength number +---@field width number +---@field hotLegDir number + + +---@param capZone SpearheadTriggerZone +---@param airBase Airbase +---@return CapTaskingOptions +local function GetCAPPointFromTriggerZone(airBase, capZone) + local furthestA = nil + local furthestB = nil + + local furthestFromBase = nil + local furthestFromBaseDistance = 0 + + local furthestDistance = 0 + + for indexA, pointA in ipairs(capZone.verts) do + for indexB, pointB in ipairs(capZone.verts) do + if pointA ~= pointB then + local distance = Spearhead.Util.VectorDistance2d(pointA, pointB) + if distance > furthestDistance then + furthestDistance = distance + furthestA = indexA + furthestB = indexB + end + end + end + end + + local baseVec3 = airBase:getPoint() + + ---@type Vec2 + local baseVec2 = { x = baseVec3.x, y = baseVec3.z } + + local pointA = capZone.verts[furthestA] + local pointB = capZone.verts[furthestB] + local furthest = pointA + local closest = pointB + + local heading = Spearhead.Util.vectorHeadingFromTo(pointA, pointB) + + if Spearhead.Util.VectorDistance2d(baseVec2, pointB) > Spearhead.Util.VectorDistance2d(baseVec2, pointA) then + furthest = pointB + closest = pointA + heading = Spearhead.Util.vectorHeadingFromTo(pointB, pointA) + end + + local distance = furthestDistance + if distance > 15000 then + distance = distance - 10000 + end + + return { + width = 10000, + furthest = furthest, + closest = closest, + legLength = distance, + hotLegDir = math.rad(heading), + orbitOriginPoint = closest + } +end + +---@param airbase Airbase +---@param capZone SpearheadTriggerZone +---@param capConfig CapConfig +local GetOutboundTask = function(airbase, capZone, capConfig) + local airbaseVec3 = airbase:getPoint() + local airbaseVec2 = { x = airbaseVec3.x, y = airbaseVec3.z } + local heading = Spearhead.Util.vectorHeadingFromTo(airbaseVec2, capZone.location) + local point = Spearhead.Util.vectorMove(airbaseVec2, heading, 18520) + + return { + alt = 2000, + action = "Fly Over Point", + type = "Turning Point", + alt_type = "BARO", + speed = capConfig:getMinSpeed(), + ETA = 0, + ETA_locked = false, + x = point.x, + y = point.y, + speed_locked = false, + formation_template = "", + task = { + id = "ComboTask", + params = { + tasks = { + [1] = { + id = 'EngageTargets', + params = { + maxDist = capConfig:getMaxDeviationRange() + 10 * 1852, + maxDistEnabled = capConfig:getMaxDeviationRange() > 0, -- required to check maxDist + targetTypes = GetCAPTargetTypes(false), + priority = 0 + } + }, + } + } + } + } +end + +---@param groupName string +---@param airbase Airbase +---@param capZone SpearheadTriggerZone +---@param capConfig CapConfig +function CAP.getAsMissionFromAirbase(groupName, airbase, capZone, capConfig) + local points = { + [1] = GetOutboundTask(airbase, capZone, capConfig), + [2] = GetOutboundTask(airbase, capZone, capConfig), + [3] = CAP.getAsTasking(groupName, airbase, capZone, capConfig), + [4] = Spearhead.classes.capClasses.taskings.RTB.getApproachPoint(airbase, capZone.location, capConfig), + [5] = Spearhead.classes.capClasses.taskings.RTB.getInitialPoint(airbase), + [6] = Spearhead.classes.capClasses.taskings.RTB.getLandingPoint(airbase) + } + + local mission = { + id = 'Mission', + params = { + airborne = true, + route = { + points = points + } + } + } + + return mission +end + +---@param groupName string +---@param airbase Airbase +---@param capZone SpearheadTriggerZone +---@param capConfig CapConfig +function CAP.getAsMission(groupName, airbase, capZone, capConfig) + local points = { + [1] = CAP.getAsTasking(groupName, airbase, capZone, capConfig), + [2] = Spearhead.classes.capClasses.taskings.RTB.getApproachPoint(airbase, capZone.location, capConfig), + [3] = Spearhead.classes.capClasses.taskings.RTB.getInitialPoint(airbase), + [4] = Spearhead.classes.capClasses.taskings.RTB.getLandingPoint(airbase) + } + + local mission = { + id = 'Mission', + params = { + airborne = true, + route = { + points = points + } + } + } + + return mission +end + +---@private +---@param groupName string +---@param airbase Airbase +---@param capZone SpearheadTriggerZone +---@param capConfig CapConfig +function CAP.getAsTasking(groupName, airbase, capZone, capConfig) + local duration = math.random(capConfig:getMinDurationOnStation(), capConfig:getMaxDurationOnStation()) or 1500 + + local capTaskingOptions = GetCAPPointFromTriggerZone(airbase, capZone) + + local durationBefore10 = duration - 600 + if durationBefore10 < 0 then durationBefore10 = 0 end + local durationAfter10 = 600 + if duration < 600 then + durationAfter10 = duration + end + + local alt = math.random(capConfig:getMinAlt(), capConfig:getMaxAlt()) + local speed = math.random(capConfig:getMinSpeed(), capConfig:getMaxSpeed()) + + return { + alt = alt, + action = "Turning Point", + alt_type = "BARO", + speed = speed, + ETA = 0, + ETA_locked = false, + x = capTaskingOptions.closest.x, + y = capTaskingOptions.closest.y, + speed_locked = true, + formation_template = "", + task = { + id = "ComboTask", + params = { + tasks = { + [1] = { + number = 1, + auto = false, + id = "WrappedAction", + enabled = "true", + params = { + action = { + id = "Script", + params = { + command = "pcall(Spearhead.Events.PublishOnStation, \"" .. groupName .. "\")" + } + } + } + }, + + [2] = { + id = 'EngageTargets', + params = { + maxDist = capConfig:getMaxDeviationRange(), + maxDistEnabled = capConfig:getMaxDeviationRange() >= 0, -- required to check maxDist + targetTypes = GetCAPTargetTypes(false), + priority = 0 + } + }, + [3] = { + number = 3, + auto = false, + id = "ControlledTask", + enabled = true, + params = { + task = { + id = "Orbit", + params = { + altitude = alt, + pattern = "Anchored", + speed = speed, + point = { + x = capTaskingOptions.closest.x, + y = capTaskingOptions.closest.y + }, + speedEdited = true, + clockWise = false, + hotLegDir = capTaskingOptions.hotLegDir, + legLength = capTaskingOptions.legLength, + width = capTaskingOptions.width, + } + }, + stopCondition = { + duration = durationBefore10, + condition = "return Spearhead.DcsUtil.NeedsRTBInTen(\"" .. groupName .. "\", 0.10)", + } + } + }, + [4] = { + number = 4, + auto = false, + id = "WrappedAction", + enabled = "true", + params = { + action = { + id = "Script", + params = { + command = "pcall(Spearhead.Events.PublishRTBInTen, \"" .. groupName .. "\")" + } + } + } + }, + [5] = { + number = 5, + auto = false, + id = "ControlledTask", + enabled = true, + params = { + task = { + id = "Orbit", + params = { + altitude = alt, + pattern = "Circle", + speed = speed, + -- speedEdited = true, + -- clockWise = false, + -- hotLegDir = capTaskingOptions.hotLegDir, + -- legLength = capTaskingOptions.legLength, + -- width = capTaskingOptions.width, + } + }, + stopCondition = { + duration = durationAfter10, + condition = "return Spearhead.DcsUtil.IsBingoFuel(\"" .. groupName .. "\")", + } + } + }, + [6] = { + number = 6, + auto = false, + id = "WrappedAction", + enabled = "true", + params = { + action = { + id = "Script", + params = { + command = "pcall(Spearhead.Events.PublishRTB, \"" .. groupName .. "\")" + } + } + } + } + } + } + } + } +end + +if not Spearhead then Spearhead = {} end +if not Spearhead.classes then Spearhead.classes = {} end +if not Spearhead.classes.capClasses then Spearhead.classes.capClasses = {} end +if not Spearhead.classes.capClasses.taskings then Spearhead.classes.capClasses.taskings = {} end +Spearhead.classes.capClasses.taskings.CAP = CAP diff --git a/classes/capClasses/taskings/RTB.lua b/classes/capClasses/taskings/RTB.lua new file mode 100644 index 0000000..c00108c --- /dev/null +++ b/classes/capClasses/taskings/RTB.lua @@ -0,0 +1,201 @@ + +---@class RTBTasking +local RTB = {} + + +---@param airbase Airbase +---@param missionPoint Vec2 +---@param capConfig CapConfig +function RTB.getAsMission(airbase, missionPoint, capConfig) + return { + id = "Mission", + params = { + airborne = true, + route = { + points = { + [1] = RTB.getApproachPoint(airbase, missionPoint, capConfig), + [2] = RTB.getInitialPoint(airbase), + [3] = RTB.getLandingPoint(airbase) + } + } + } + } + +end + +---@param airbase Airbase +---@return number +local getRunwayIntoWindCourseRad = function(airbase) + + local activeCourse = nil + local minAlignment = nil + + local runways = airbase:getRunways() + + local windVec = atmosphere.getWind(airbase:getPoint()) + local mPerS = Spearhead.Util.vectorMagnitude(windVec) + + if mPerS > 0 then + for i = 1, #runways do + local runway = runways[i] + + do --normal + local rad = runway.course + if rad < 0 then + rad = math.abs(rad) + else + rad = 0 - rad + end + + local runwayVec = {x = math.cos(rad), z = math.sin(rad), y = 0} + local alignment = Spearhead.Util.vectorAlignment(windVec, runwayVec) + + if minAlignment == nil or alignment < minAlignment then + activeCourse = i + minAlignment = alignment + end + end + + do --inverse + local degree = math.deg(runway.course) + degree = (degree + 180) % 360 + + local rad = math.rad(degree) + if rad < 0 then + rad = math.abs(rad) + else + rad = 0 - rad + end + + local runwayVec = {x = math.cos(rad), z = math.sin(rad), y = 0} + local alignment = Spearhead.Util.vectorAlignment(windVec, runwayVec) + + if minAlignment == nil or alignment < minAlignment then + activeCourse = i + minAlignment = alignment + end + end + end + end + + local rad = runways[1].course + + if activeCourse ~= nil then + rad = runways[activeCourse].course + end + + if rad < 0 then + return math.abs(rad) + else + return 0 - rad + end + +end + +---comment +---@param airbase Airbase +---@return Vec2 +---@return number headingFromRunwayDegrees +local function calcInitialPoint(airbase) + local runwayCourseRad = getRunwayIntoWindCourseRad(airbase) + local heading = math.deg(runwayCourseRad) + local flipped = (heading + 180) % 360 + local basePoint = airbase:getPoint() + local basePointVec2 = {x = basePoint.x, y = basePoint.z} + local point = Spearhead.Util.vectorMove(basePointVec2, flipped, 22000) + + return point, flipped + +end + + +---comment +---@param airbase Airbase +---@param missionPoint Vec2 +---@param capConfig CapConfig +function RTB.getApproachPoint(airbase, missionPoint, capConfig) + + local initialPoint, headingFromRunway = calcInitialPoint(airbase) + local pointA = Spearhead.Util.vectorMove(initialPoint, headingFromRunway - 45, 9000) + local pointB = Spearhead.Util.vectorMove(initialPoint, headingFromRunway + 45, 9000) + + if Spearhead.Util.VectorDistance2d(missionPoint, pointA) > Spearhead.Util.VectorDistance2d(missionPoint, pointB) then + pointA = pointB + end + + return { + alt = 3000, + action = "Turning Point", + alt_type = "BARO", + speed = capConfig:getMinSpeed(), + ETA = 0, + ETA_locked = false, + x = pointA.x, + y = pointA.y, + speed_locked = true, + formation_template = "", + type = "Turning Point", + task = { + id = "ComboTask", + params = { + tasks = {} + } + } + } + +end + +---@param airbase Airbase +function RTB.getInitialPoint(airbase) + local point = calcInitialPoint(airbase) + return { + alt = 600, + action = "Turning Point", + alt_type = "BARO", + speed = 180, + ETA = 0, + ETA_locked = false, + x = point.x, + y = point.y, + speed_locked = true, + formation_template = "", + type = "Turning Point", + task = { + id = "ComboTask", + params = { + tasks = {} + } + } + } +end + +function RTB.getLandingPoint(airbase) + local basePoint = airbase:getPoint() + return { + alt = basePoint.y, + action = "Landing", + alt_type = "BARO", + speed = 70, + ETA = 0, + ETA_locked = false, + x = basePoint.x, + y = basePoint.z, + speed_locked = true, + formation_template = "", + airdromeId = airbase:getID(), + type = "Land", + task = { + id = "ComboTask", + params = { + tasks = {} + } + } + } +end + + +if not Spearhead then Spearhead = {} end +if not Spearhead.classes then Spearhead.classes = {} end +if not Spearhead.classes.capClasses then Spearhead.classes.capClasses = {} end +if not Spearhead.classes.capClasses.taskings then Spearhead.classes.capClasses.taskings = {} end +Spearhead.classes.capClasses.taskings.RTB = RTB \ No newline at end of file diff --git a/classes/configuration/CapConfig.lua b/classes/configuration/CapConfig.lua index a88c40e..bac55f3 100644 --- a/classes/configuration/CapConfig.lua +++ b/classes/configuration/CapConfig.lua @@ -1,56 +1,98 @@ - +---@class CapConfig +---@field private _isEnabled boolean +---@field private _minSpeed number +---@field private _maxSpeed number +---@field private _minAlt number +---@field private _maxAlt number +---@field private _minDurationOnStation number +---@field private _maxDurationOnStation number +---@field private _maxDeviationRange number +---@field private _rearmDelay number +---@field private _repairDelay number +---@field private _deathDelay number local CapConfig = {}; -function CapConfig:new() - local o = {} - setmetatable(o, { __index = self }) +CapConfig.__index = CapConfig + +---@return CapConfig +function CapConfig.new() + local self = setmetatable({}, CapConfig) if SpearheadConfig == nil then SpearheadConfig = {} end if SpearheadConfig.CapConfig == nil then SpearheadConfig.CapConfig = {} end local enabled = SpearheadConfig.CapConfig.enabled if enabled == nil then enabled = true end - ---@return boolean - o.isEnabled = function(self) return enabled == true end + self._isEnabled = enabled - local minSpeed = (tonumber(SpearheadConfig.CapConfig.minSpeed) or 400) * 0.514444 - ---@return number - o.getMinSpeed = function(self) return minSpeed end + self._minSpeed = (tonumber(SpearheadConfig.CapConfig.minSpeed) or 400) * 0.514444 + self._maxSpeed = (tonumber(SpearheadConfig.CapConfig.maxSpeed) or 400) * 0.514444 - local maxSpeed = (tonumber(SpearheadConfig.CapConfig.maxSpeed) or 400) * 0.514444 - ---@return number - o.getMaxSpeed = function(self) return maxSpeed end + self._minAlt = (tonumber(SpearheadConfig.CapConfig.minAlt) or 18000) * 0.3048 + self._maxAlt = (tonumber(SpearheadConfig.CapConfig.maxAlt) or 28000) * 0.3048 - local minAlt = (tonumber(SpearheadConfig.CapConfig.minAlt) or 18000) * 0.3048 - ---@return number - o.getMinAlt = function(self) return minAlt end + self._minDurationOnStation = 1200 + self._maxDurationOnStation = 2700 - local maxAlt = (tonumber(SpearheadConfig.CapConfig.maxAlt) or 28000) * 0.3048 - ---@return number - o.getMaxAlt = function(self) return maxAlt end + self._maxDeviationRange = 35 * 1852 -- in meters + self._rearmDelay = tonumber(SpearheadConfig.CapConfig.rearmDelay) or 600 + self._repairDelay = tonumber(SpearheadConfig.CapConfig.repairDelay) or 600 + self._deathDelay = tonumber(SpearheadConfig.CapConfig.deathDelay) or 1800 - local minDurationOnStation = 1200 - ---@return number - o.getMinDurationOnStation = function(self) return minDurationOnStation end + return self; +end - local maxDurationOnStation = 2700 - ---@return number - o.getmaxDurationOnStation = function(self) return maxDurationOnStation end +function CapConfig:isEnabled() + return self._isEnabled +end - local maxDeviationRange = 20 * 1852; - ---@return number - o.getMaxDeviationRange = function(self) return maxDeviationRange end +---@return number +function CapConfig:getMinSpeed() + return self._minSpeed +end - local rearmDelay = tonumber(SpearheadConfig.CapConfig.rearmDelay) or 600 - ---@return number - o.getRearmDelay = function(self) return rearmDelay end +---@return number +function CapConfig:getMaxSpeed() + return self._maxSpeed +end - local deathDelay = tonumber(SpearheadConfig.CapConfig.deathDelay) or 1800 - ---@return number - o.getDeathDelay = function(self) return deathDelay end - o.logLevel = "INFO" +---@return number +function CapConfig:getMinAlt() + return self._minAlt +end - return o; +---@return number +function CapConfig:getMaxAlt() + return self._maxAlt +end + +---@return number +function CapConfig:getMinDurationOnStation() + return self._minDurationOnStation +end + +---@return number +function CapConfig:getMaxDurationOnStation() + return self._maxDurationOnStation +end + +---@return number +function CapConfig:getMaxDeviationRange() + return self._maxDeviationRange +end + +---@return number +function CapConfig:getRearmDelay() + return self._rearmDelay +end + +function CapConfig:getRepairDelay() + return self._repairDelay +end + +---@return number +function CapConfig:getDeathDelay() + return self._deathDelay end if not Spearhead.internal then Spearhead.internal = {} end diff --git a/classes/spearhead_base.lua b/classes/spearhead_base.lua index e0826d4..d6b78f5 100644 --- a/classes/spearhead_base.lua +++ b/classes/spearhead_base.lua @@ -36,15 +36,15 @@ do -- INIT UTIL ---@param orig table ---@return table copy - function UTIL.copyTable(orig) + function UTIL.deepCopyTable(orig) local orig_type = type(orig) local copy if orig_type == 'table' then copy = {} for orig_key, orig_value in next, orig, nil do - copy[UTIL.copyTable(orig_key)] = UTIL.copyTable(orig_value) + copy[UTIL.deepCopyTable(orig_key)] = UTIL.deepCopyTable(orig_value) end - setmetatable(copy, UTIL.copyTable(getmetatable(orig))) + setmetatable(copy, UTIL.deepCopyTable(getmetatable(orig))) else -- number, string, boolean, etc copy = orig end @@ -88,15 +88,15 @@ do -- INIT UTIL table.insert(sb, string.rep(" ", indent)) -- indent it if type(value) == "table" and not done[value] then done[value] = true - table.insert(sb, key .. " = {\n"); + table.insert(sb, "[\"" ..key .. "\"]" .. " = {\n"); table.insert(sb, table_print(value, indent + 2, done)) table.insert(sb, string.rep(" ", indent)) -- indent it - table.insert(sb, "}\n"); + table.insert(sb, "},\n"); elseif "number" == type(key) then - table.insert(sb, string.format("\"%s\"\n", tostring(value))) + table.insert(sb, string.format("\"%s\",\n", tostring(value))) else table.insert(sb, string.format( - "%s = \"%s\"\n", tostring(key), tostring(value))) + "[\"%s\"] = \"%s\",\n", tostring(key), tostring(value))) end end return table.concat(sb) @@ -172,6 +172,54 @@ do -- INIT UTIL return (vec.x ^ 2 + vec.y ^ 2 + vec.z ^ 2) ^ 0.5 end + ---@param vec Vec3 + ---@return Vec3 + function UTIL.vectorNormalize(vec) + local magnitude = UTIL.vectorMagnitude(vec) + if magnitude == 0 then + return { x = 0, y = 0, z = 0 } + end + return { x = vec.x / magnitude, y = vec.y / magnitude, z = vec.z / magnitude } + end + + ---@param vec Vec2 + ---@param direction number @in degrees + ---@param distance number + ---@return Vec2 + function UTIL.vectorMove(vec, direction, distance) + local rad = math.rad(direction) + local x = vec.x + (math.cos(rad) * distance) + local y = vec.y + (math.sin(rad) * distance) + + return { x = x, y = y} + end + + ---comment + ---@param vec1 Vec2 + ---@param vec2 Vec2 + ---@return number in degrees + function UTIL.vectorHeadingFromTo(vec1, vec2) + local dx = vec2.x - vec1.x + local dy = vec2.y - vec1.y + local heading = math.deg(math.atan2(dy, dx)) + if heading < 0 then + heading = heading + 360 + end + return heading + end + + + + ---@param vec1 Vec3 + ---@param vec2 Vec3 + ---@return number alignment a number in range [-1,1]. > 0 same direction. < 0 opposite direction + function UTIL.vectorAlignment(vec1, vec2) + local vec1Norm = UTIL.vectorNormalize(vec1) + local vec2Norm = UTIL.vectorNormalize(vec2) + + return ((vec1Norm.x * vec2Norm.x) + (vec1Norm.y * vec2Norm.y) + (vec1Norm.z * vec2Norm.z)) + end + local function isInComplexPolygon(polygon, x, y) local function getEdges(poly) local result = {} @@ -706,7 +754,7 @@ do -- INIT DCS_UTIL ---@param groupName string function DCS_UTIL.IsGroupStatic(groupName) if DCS_UTIL.__miz_groups[groupName] then - return DCS_UTIL.__miz_groups[groupName].category == DCS_UTIL.GroupCategory.STATIC; + return DCS_UTIL.__miz_groups[groupName].category == DCS_UTIL.GroupCategory.STATIC end return StaticObject.getByName(groupName) ~= nil @@ -1255,9 +1303,10 @@ do -- INIT DCS_UTIL ---@param route table? route of the group. If nil wil be the default route. ---@param uncontrolled boolean? Sets the group to be uncontrolled on spawn ---@param countryId CountryID? Overwrites the country + ---@param emptyLoadouts boolean? If true, the group will spawn with empty loadouts ---@return table? new_group the Group class that was spawned ---@return boolean? isStatic whether the group is a static or not - function DCS_UTIL.SpawnGroupTemplate(groupName, location, route, uncontrolled, countryId) + function DCS_UTIL.SpawnGroupTemplate(groupName, location, route, uncontrolled, countryId, emptyLoadouts) if groupName == nil then return nil, nil end @@ -1280,7 +1329,7 @@ do -- INIT DCS_UTIL return object, true end else - local spawn_template = template.group_template + local spawn_template = Spearhead.Util.deepCopyTable(template.group_template) if location ~= nil then local x_offset if location.x ~= nil then x_offset = spawn_template.x - location.x end @@ -1298,6 +1347,26 @@ do -- INIT DCS_UTIL end end + if spawn_template.units then + for _, unit in pairs(spawn_template.units) do + if unit["parking"] then + unit["parking_landing"] = unit["parking"] + end + + if unit["parking_id"] then + unit["parking_landing_id"] = unit["parking_id"] + end + end + + if emptyLoadouts == true then + for _, unit in pairs(spawn_template.units) do + if unit["payload"] and unit["payload"]["pylons"] then + unit["payload"]["pylons"] = {} + end + end + end + end + if route ~= nil then spawn_template.route = route end @@ -1305,6 +1374,7 @@ do -- INIT DCS_UTIL if uncontrolled ~= nil then spawn_template.uncontrolled = uncontrolled end + local country = countryId or template.country_id local new_group = coalition.addGroup(country, template.category, spawn_template) return new_group, false @@ -1320,6 +1390,30 @@ do -- INIT DCS_UTIL end end + + function DCS_UTIL.NeedsRTBInTen(groupName, fuelOffset) + + local isBingo = Spearhead.DcsUtil.IsBingoFuel(groupName, fuelOffset) + if isBingo then return true end + + local aliveUnits = 0 + local group = Group.getByName(groupName) + if group then + for _ , unit in pairs(group:getUnits()) do + if unit and unit:isExist() == true and unit:inAir() == true then + aliveUnits = aliveUnits + 1 + end + end + + if aliveUnits / group:getInitialSize() <= 0.5 then + return true + end + end + + return false + end + + ---@return boolean function DCS_UTIL.IsBingoFuel(groupName, offset) if offset == nil then offset = 0 end local bingoSetting = 0.20 diff --git a/classes/spearhead_db.lua b/classes/spearhead_db.lua index 52236a7..dc71dcf 100644 --- a/classes/spearhead_db.lua +++ b/classes/spearhead_db.lua @@ -8,7 +8,7 @@ ---@field StageZonesByNumber table> Stage zones grouped by index number ---@field AllFarpZones Array ---@field CapRoutes Array All Cap route zone names ----@field CapDataPerStageNumber table table +---@field capZonesByCapZoneID table table ---@field CarrierRouteZones Array All Carrier routes zones ---@field BlueSams Array All blue sam zones ---@field SupplyHubZones Array All supply hub zones @@ -19,13 +19,10 @@ ---@field FarpZoneData table ---@field missionCodes table ----@class CapData ----@field routes Array ----@field current integer ---@class CapRoute ----@field point1 Vec3 ----@field point2 Vec3? +---@field zones Array +---@field current number ---@class MissionAnnotations ---@field description string? @@ -82,7 +79,7 @@ function Database.New(Logger) AllZoneNames = {}, BlueSams = {}, CapRoutes = {}, - CapDataPerStageNumber = {}, + capZonesByCapZoneID = {}, CarrierRouteZones = {}, MissionZones = {}, MissionZonesLocations = {}, @@ -367,55 +364,21 @@ function Database.New(Logger) self:loadMiscGroupsInStages() - for _, zoneData in pairs(self._tables.StageZones) do - local number = zoneData.StageIndex + for _, cap_route_zone in pairs(self._tables.CapRoutes) do + local split = Spearhead.Util.split_string(cap_route_zone, "_") + local zoneID = split[2] - for _, cap_route_zone in pairs(self._tables.CapRoutes) do - if Spearhead.DcsUtil.isZoneInZone(cap_route_zone, zoneData.StageZoneName) == true then - local zone = Spearhead.DcsUtil.getZoneByName(cap_route_zone) - if zone then - ---@type CapData - local capData = self._tables.CapDataPerStageNumber[number] - if capData == nil then - capData = { - routes = {}, - current = 0 - } - self._tables.CapDataPerStageNumber[number] = capData - end + if zoneID then + if tables.capZonesByCapZoneID[zoneID] == nil then + tables.capZonesByCapZoneID[zoneID] = { + zones = {}, + current = 1 + } + end - - if zone.zone_type == Spearhead.DcsUtil.ZoneType.Cilinder then - table.insert(capData.routes, - { point1 = { x = zone.location.x, z = zone.location.y, y = 0 }, point2 = nil }) - else - local biggest = nil - local biggestA = nil - local biggestB = nil - - for i = 1, 3 do - for ii = i + 1, 4 do - local a = zone.verts[i] - local b = zone.verts[ii] - local dist = Spearhead.Util.VectorDistance2d(a, b) - - if biggest == nil or dist > biggest then - biggestA = a - biggestB = b - biggest = dist - end - end - end - - if biggestA and biggestB then - table.insert(capData.routes, - { - point1 = { x = biggestA.x, z = biggestA.y }, - point2 = { x = biggestB.x, z = biggestB.y } - }) - end - end - end + local zone = Spearhead.DcsUtil.getZoneByName(cap_route_zone) + if zone then + table.insert(tables.capZonesByCapZoneID[zoneID].zones, zone) end end end @@ -842,66 +805,31 @@ function Database:GetLocationForMissionZone(missionZoneName) return self._tables.MissionZonesLocations[missionZoneName] end ----comment ----@param stageNumber string ----@param baseName string ----@return table? -function Database:getCapRouteInZone(stageNumber, baseName) - local stageNumber = tostring(stageNumber) or "nothing" - local routeData = self._tables.CapDataPerStageNumber[stageNumber] - if routeData then - local count = Spearhead.Util.tableLength(routeData.routes) - if count > 0 then - routeData.current = routeData.current + 1 - if count < routeData.current then - routeData.current = 1 - end - return routeData.routes[routeData.current] - end - end - do - local function GetClosestPointOnCircle(pC, radius, p) - local vX = p.x - pC.x; - local vY = p.z - pC.z; - local magV = math.sqrt(vX * vX + vY * vY); - local aX = pC.x + vX / magV * radius; - local aY = pC.z + vY / magV * radius; - return { x = aX, z = aY } - end - local stageZoneName = Spearhead.Util.randomFromList(self._tables.StageZonesByNumber[stageNumber]) or "none" - local stagezone = Spearhead.DcsUtil.getZoneByName(stageZoneName) - if stagezone then - ---@type Airbase? - local base = Airbase.getByName(baseName) - if base then - local closest = nil - if stagezone.zone_type == Spearhead.DcsUtil.ZoneType.Cilinder then - closest = GetClosestPointOnCircle({ x = stagezone.location.x, z = stagezone.location.y }, - stagezone.radius, - base:getPoint()) - else - local closestDistance = -1 - for _, vert in pairs(stagezone.verts) do - local pos = base:getPoint() - local vec2 = { x = pos.x, y = pos.z } - local distance = Spearhead.Util.VectorDistance2d(vert, vec2) - if closestDistance == -1 or distance < closestDistance then - closestDistance = distance - closest = vert - end - end - end +---@param zoneID string +---@return SpearheadTriggerZone? +function Database:GetCapZoneForZoneID(zoneID) + zoneID = tostring(zoneID) or "nothing" - if math.random(1, 2) % 2 == 0 then - return { point1 = closest, point2 = { x = stagezone.location.x, z = stagezone.location.y } } - else - return { point1 = { x = stagezone.location.x, z = stagezone.location.y }, point2 = closest } - end - end + local capZonesForID = self._tables.capZonesByCapZoneID[zoneID] + + if capZonesForID and capZonesForID.zones then + + local count = Spearhead.Util.tableLength(capZonesForID.zones) + if count == 0 then + self._logger:warn("Tried to get cap zone for zoneID: " .. zoneID .. " but cap zones were empty for this ID") + return nil end - return nil + capZonesForID.current = capZonesForID.current + 1 + if Spearhead.Util.tableLength(capZonesForID.zones) < capZonesForID.current then + capZonesForID.current = 1 + end + return capZonesForID.zones[capZonesForID.current] + else + self._logger:warn("Tried to get cap zone for zoneID: " .. zoneID .. " but no cap zones were found for this ID") end + + return nil end ---@return Array result a list of stage zone names diff --git a/classes/stageClasses/SpecialZones/StageBase.lua b/classes/stageClasses/SpecialZones/StageBase.lua index dc20472..18be081 100644 --- a/classes/stageClasses/SpecialZones/StageBase.lua +++ b/classes/stageClasses/SpecialZones/StageBase.lua @@ -159,14 +159,17 @@ function StageBase:SpawnBlueUnits() end function StageBase:ActivateRedStage() - if self._initialSide == 2 and self._airbase then - self._airbase:setCoalition(1) + self._logger:debug("Activate red stage for airbase: " .. self._airbase:getName()) + if self._airbase and (self._initialSide == 2 or self._initialSide == 1) then + self._airbase:setCoalition(coalition.side.RED) self._airbase:autoCapture(false) end self:SpawnRedUnits() end function StageBase:ActivateBlueStage() + self._logger:debug("Activate blue stage for airbase: " .. self._airbase:getName()) + self:CleanRedUnits() if self._buildableMission and self._buildableMission:getState() ~= "COMPLETED" then @@ -178,7 +181,8 @@ end function StageBase:FinaliseBlueStage() if self._initialSide == 2 and self._airbase then - self._airbase:setCoalition(2) + self._airbase:setCoalition(coalition.side.BLUE) + self._airbase:autoCapture(false) end self:SpawnBlueUnits() diff --git a/classes/stageClasses/helpers/BattleManager.lua b/classes/stageClasses/helpers/BattleManager.lua index 3215526..c589653 100644 --- a/classes/stageClasses/helpers/BattleManager.lua +++ b/classes/stageClasses/helpers/BattleManager.lua @@ -9,6 +9,8 @@ local BattleManager = {} BattleManager.__index = BattleManager +local debugDrawing = false + ---@param redGroups Array ---@param blueGroups Array ---@param name string @@ -131,12 +133,10 @@ function BattleManager:LetUnitsShoot(groups, targetGroups) } } - if SpearheadConfig and SpearheadConfig.debugEnabled == true then + if debugDrawing == true then self:DrawDebugLine(point, unit) end - self._logger:debug("Red unit " .. unit:getName() .. " will shoot " .. qty .. " rounds at point: " .. tostring(point)) - local controller = unit:getController() if controller then controller:setTask(shootTask) @@ -241,7 +241,7 @@ function BattleManager:GetRandomPoint(origin, groupHulls) if not hull then return nil end local shootPoints = Spearhead.Util.GetTangentHullPointsFromOrigin(hull, origin) - if SpearheadConfig and SpearheadConfig.debugEnabled == true then + if debugDrawing == true then self:DrawDebugZone({ hull }) end diff --git a/classes/stageClasses/helpers/MissionCommandsHelper.lua b/classes/stageClasses/helpers/MissionCommandsHelper.lua index 086ae6d..ed0cbf3 100644 --- a/classes/stageClasses/helpers/MissionCommandsHelper.lua +++ b/classes/stageClasses/helpers/MissionCommandsHelper.lua @@ -271,8 +271,8 @@ function MissionCommandsHelper:AddAllMissionCommandsToGroup(groupID) local mission = self.missionsByCode[code] if mission and mission.priority == "primary" then count = count + 1 - if count <= perFolder then - local copied = Spearhead.Util.copyTable(path) + if count <= perFolder then + local copied = Spearhead.Util.deepCopyTable(path) self:addMissionCommands(groupID, copied, mission) else local name = "Next Menu ..." @@ -294,7 +294,7 @@ function MissionCommandsHelper:AddAllMissionCommandsToGroup(groupID) if mission and mission.priority == "secondary" then count = count + 1 if count <= perFolder then - local copied = Spearhead.Util.copyTable(path) + local copied = Spearhead.Util.deepCopyTable(path) self:addMissionCommands(groupID, copied, mission) else local name = "Next Menu ..." diff --git a/classes/stageClasses/helpers/SupplyUnitsTracker.lua b/classes/stageClasses/helpers/SupplyUnitsTracker.lua index 0baa4af..72c7797 100644 --- a/classes/stageClasses/helpers/SupplyUnitsTracker.lua +++ b/classes/stageClasses/helpers/SupplyUnitsTracker.lua @@ -29,6 +29,7 @@ function SupplyUnitsTracker.getOrCreate(logLevel) singleton._supplyUnitsByName = {} singleton._droppedCrates = {} singleton._registeredHubs = {} + singleton._supplyUnitSpawnedListener = {} singleton._commandsHelper = Spearhead.classes.stageClasses.helpers.MissionCommandsHelper.getOrCreate(singleton._logger.LogLevel) diff --git a/config.lua b/config.lua index b075651..3024d25 100644 --- a/config.lua +++ b/config.lua @@ -25,13 +25,26 @@ SpearheadConfig = { -- unit: feet maxAlt = 28000, -- default 28000 + -- DELAYS. + -- Delays work as follow. + -- When an aircraft lands alive and well it will be rearmed and ready to go. + -- When part of the flight died it will be repaired and then rearmed. (both delays) + -- When the entire group dies it will first wait for the deathDelay, then it will be repaired and rearmed. + -- while rearming: Spawned on the ramp. + -- while repairing: Spawned on the ramp. + -- while deathDelay no unit is spawned. + --Delay for aircraft from touchdown to off the chocks. -- unit: seconds rearmDelay = 600, -- default 600 - --Delay for aircraft from death to takeoff. - --When the seconds remaining is the same at the rearmDelay it will be spawned on the ramp and follow the rearm logic. - -- !! Can not be lower than rearmDelay + -- Delay for aircraft that has died to be repaired before rearming starts. + -- unit: seconds + repairDelay = 600, -- default 600 + + --Delay for aircraft before the repairing and rearming cycles begin. + -- applied when the whole group dies. + -- (see this as an additional bonus delay for destroying an entire group) -- unit: seconds deathDelay = 1800, -- default 1800 }, diff --git a/dev/dev_classes.lua b/dev/dev_classes.lua index c1efe59..fc7d9c5 100644 --- a/dev/dev_classes.lua +++ b/dev/dev_classes.lua @@ -44,7 +44,10 @@ assert(loadfile(classPath .. "stageClasses\\SpecialZones\\BlueSam.lua"))() assert(loadfile(classPath .. "stageClasses\\SpecialZones\\FarpZone.lua"))() assert(loadfile(classPath .. "stageClasses\\SpecialZones\\SupplyHub.lua"))() -assert(loadfile(classPath .. "capClasses\\CapGroup.lua"))() +assert(loadfile(classPath .. "capClasses\\taskings\\RTB.lua"))() +assert(loadfile(classPath .. "capClasses\\taskings\\CAP.lua"))() +assert(loadfile(classPath .. "capClasses\\airGroups\\AirGroup.lua"))() +assert(loadfile(classPath .. "capClasses\\airGroups\\CapGroup.lua"))() assert(loadfile(classPath .. "capClasses\\GlobalCapManager.lua"))() assert(loadfile(classPath .. "capClasses\\CapAirbase.lua"))() assert(loadfile(classPath .. "capClasses\\runwayBombing\\RunwayBombingTracker.lua"))() diff --git a/dev/dev_config.lua b/dev/dev_config.lua index e5ceaae..2cec94a 100644 --- a/dev/dev_config.lua +++ b/dev/dev_config.lua @@ -25,8 +25,8 @@ SpearheadConfig = { --Delay for aircraft from touchdown to off the chocks. -- unit: seconds - rearmDelay = 600, -- default 600 - + rearmDelay = 180, -- default 600 + repairDelay = 600, -- default 600 --Delay for aircraft from death to takeoff. --When the seconds remaining is the same at the rearmDelay it will be spawned on the ramp and follow the rearm logic. -- !! Can not be lower than rearmDelay