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
- [<activeStage>]<capStage>
- [<activeStage>,<activeStage>]<capStage>
- [<activeStage>-<activeStage>]<capStage>
- [<activeStage>-<activeStage>,<activeStage>]<capStage>
- [<activeStage>-<activeStage>]A
+ [<activeStage>]<CapRouteID>
+ [<activeStage>,<activeStage>]<CapRouteID>
+ [<activeStage>-<activeStage>]<CapRouteID>
+ [<activeStage>-<activeStage>,<activeStage>]<CapRouteID>
CAP_A[1-4,6]7|[5,7]8_SomeName
- CAP_A[2-5]5|[6]6_SomeName
- CAP_A[1-5]A|[6]7_SomeName
+ CAP_A[2-5]5|[6]6_SomeName
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