Cap rework (#48)

- Refactored all CAP classes.
- Improved timing
- Zones are now not tied to MissionStages
  - CAPROUTE_<routeID>_<free>
  - routeID is now what is used in the naming convention.
- Units taxi back to their spawn parking space and rearm there.
  (Units destroyed while in the Rearming or Repairing state are not taken into account)
This commit is contained in:
2025-06-01 18:58:35 +02:00
committed by GitHub
parent 059081890d
commit 0233b9e2d7
21 changed files with 1519 additions and 832 deletions
+7 -9
View File
@@ -145,8 +145,8 @@
<h3 id="cap-routes">CAP Routes</h3> <h3 id="cap-routes">CAP Routes</h3>
<p> <p>
<strong>Format:</strong> <span class="inline-lua"><span class="lua-variable">CAPROUTE_[Name]</span></span> <br /> <strong>Format:</strong> <span class="inline-lua"><span class="lua-variable">CAPROUTE_[routeID]_[Name]</span></span> <br />
<strong>Example:</strong> <span class="inline-lua"><span class="lua-variable">CAPROUTE_ALPHA</span></span> <strong>Example:</strong> <span class="inline-lua"><span class="lua-variable">CAPROUTE_103_ALPHA</span></span>
</p> </p>
<p> <p>
CAP routes define the patrol paths for CAP units. They are tied to specific stages and zones. CAP routes define the patrol paths for CAP units. They are tied to specific stages and zones.
@@ -249,18 +249,16 @@
<h3 id="cap-group-config">CAP Group Config</h3> <h3 id="cap-group-config">CAP Group Config</h3>
<pre> <pre>
<span class="lua-comment">1 at x:</span> <span class="lua-variable">[&lt;activeStage&gt;]&lt;capStage&gt;</span> <span class="lua-comment">1 at x:</span> <span class="lua-variable">[&lt;activeStage&gt;]&lt;CapRouteID&gt;</span>
<span class="lua-comment">n and n at x:</span> <span class="lua-variable">[&lt;activeStage&gt;,&lt;activeStage&gt;]&lt;capStage&gt;</span> <span class="lua-comment">n and n at x:</span> <span class="lua-variable">[&lt;activeStage&gt;,&lt;activeStage&gt;]&lt;CapRouteID&gt;</span>
<span class="lua-comment">n till n at x:</span> <span class="lua-variable">[&lt;activeStage&gt;-&lt;activeStage&gt;]&lt;capStage&gt;</span> <span class="lua-comment">n till n at x:</span> <span class="lua-variable">[&lt;activeStage&gt;-&lt;activeStage&gt;]&lt;CapRouteID&gt;</span>
<span class="lua-comment">n till n and n at x:</span> <span class="lua-variable">[&lt;activeStage&gt;-&lt;activeStage&gt;,&lt;activeStage&gt;]&lt;capStage&gt;</span> <span class="lua-comment">n till n and n at x:</span> <span class="lua-variable">[&lt;activeStage&gt;-&lt;activeStage&gt;,&lt;activeStage&gt;]&lt;CapRouteID&gt;</span>
<span class="lua-comment">n till n at Active:</span> <span class="lua-variable">[&lt;activeStage&gt;-&lt;activeStage&gt;]A</span>
<span class="lua-comment">Divider: |</span> <span class="lua-comment">Divider: |</span>
<span class="lua-comment">Examples:</span> <span class="lua-comment">Examples:</span>
<span class="lua-variable">CAP_A[1-4,6]7|[5,7]8_SomeName</span> <span class="lua-comment">=&gt; 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.</span> <span class="lua-variable">CAP_A[1-4,6]7|[5,7]8_SomeName</span> <span class="lua-comment">=&gt; 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.</span>
<span class="lua-variable">CAP_A[2-5]5|[6]6_SomeName</span> <span class="lua-comment">=&gt; Will fly CAP at stage 5 when stages 2 through 5 are active and will fly CAP at 6 when 6 is active.</span> <span class="lua-variable">CAP_A[2-5]5|[6]6_SomeName</span> <span class="lua-comment">=&gt; Will fly CAP at stage 5 when stages 2 through 5 are active and will fly CAP at CapRoute 6 when 6 is active.</span>
<span class="lua-variable">CAP_A[1-5]A|[6]7_SomeName</span> <span class="lua-comment">=&gt; Will fly CAP at the ACTIVE stage if stages 1-5 are active. Then when 6 is active, it will fly in 7.</span>
</pre> </pre>
<h3 id="active-and-backup-cap">Active and Backup CAP</h3> <h3 id="active-and-backup-cap">Active and Backup CAP</h3>
Binary file not shown.
Binary file not shown.
-74
View File
@@ -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<string, string>
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
+156 -144
View File
@@ -1,22 +1,18 @@
---@class CapBase : OnStageChangedListener ---@class CapBase : OnStageChangedListener
---@field groupNames Array<string> ---@field private airbaseName string
---@field database Database ---@field private logger table
---@field airbaseName string ---@field private database Database
---@field logger table ---@field private activeStage number
---@field activeStage number ---@field private capConfig table
---@field capConfig table ---@field private capGroupsByName table<string, CapGroup>
---@field activeCapStages number
---@field lastStatesByName table<string, string>
---@field groupsByName table<string, CapGroup>
---@field PrimaryGroups Array<CapGroup>
---@field BackupGroups Array<CapGroup>
---@field private runwayBombingTracker RunwayBombingTracker ---@field private runwayBombingTracker RunwayBombingTracker
---@field private runwayStrikeMissions table<string, RunwayStrikeMission> ---@field private runwayStrikeMissions table<string, RunwayStrikeMission>
local CapBase = {} local CapBase = {}
local CheckReschedulingAsync = function(self, time) local CheckStateContinuous = function(self, time)
self:CheckAndScheduleCAP() self:CheckAndScheduleCAP()
return time + 15
end end
---comment ---comment
@@ -28,181 +24,201 @@ end
---@param runwayBombingTracker RunwayBombingTracker ---@param runwayBombingTracker RunwayBombingTracker
---@return CapBase ---@return CapBase
function CapBase.new(airbaseName, database, logger, capConfig, stageConfig, runwayBombingTracker) function CapBase.new(airbaseName, database, logger, capConfig, stageConfig, runwayBombingTracker)
CapBase.__index = CapBase CapBase.__index = CapBase
local self = setmetatable({}, { __index = CapBase }) --[[@as 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.runwayBombingTracker = runwayBombingTracker
self.runwayStrikeMissions = {} self.runwayStrikeMissions = {}
self.airbaseName = airbaseName self.airbaseName = airbaseName
self.logger = logger self.logger = logger
self.activeStage = 0 self.activeStage = 0
self.capConfig = capConfig self.capConfig = capConfig
self.activeCapStages = (stageConfig or {}).capActiveStages or 10 self.database = database
self.capGroupsByName = {}
self.lastStatesByName = {} local baseData = database:getAirbaseDataForZone(airbaseName)
self.groupsByName = {} if baseData and baseData.CapGroups then
self.PrimaryGroups = {} for key, name in pairs(baseData.CapGroups) do
self.BackupGroups = {} local capGroup = Spearhead.classes.capClasses.airGroups.CapGroup.New(name, capConfig, logger)
if capGroup then
for key, name in pairs(self.groupNames) do self.capGroupsByName[name] = capGroup
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)
end end
capGroup:AddOnStateUpdatedListener(self)
end end
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) Spearhead.Events.AddStageNumberChangedListener(self)
return self timer.scheduleFunction(CheckStateContinuous, self, timer.getTime() + 15)
return self
end end
---@private ---@private
function CapBase:CreateRunwayStrikeMission() ---@param database Database
function CapBase:CreateRunwayStrikeMission(database)
local airbase = Airbase.getByName(self.airbaseName) 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) self.logger:debug("Could not find a airbase with name to create runway mission" .. self.airbaseName)
return return
end end
for _, runway in pairs(airbase:getRunways()) do for _, runway in pairs(airbase:getRunways()) do
if runway then 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) self.logger:debug("Runway " ..
local mission = Spearhead.classes.stageClasses.missions.RunwayStrikeMission.new(runway, self.airbaseName, self.database, self.logger, self.runwayBombingTracker) 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 self.runwayStrikeMissions[runway.Name] = mission
end end
end end
end end
function CapBase:onGroupStateUpdated(capGroup) function CapBase:SpawnIfApplicable()
--[[ self.logger:debug("Check spawns for airbase " .. self.airbaseName)
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. for groupName, capGroup in pairs(self.capGroupsByName) do
]]-- local targetStage = capGroup:GetZoneIDWhenStageID(tostring(self.activeStage))
if capGroup.state == Spearhead.classes.capClasses.CapGroup.GroupState.INTRANSIT
or capGroup.state == Spearhead.classes.capClasses.CapGroup.GroupState.ONSTATION if targetStage ~= nil and capGroup:GetState() == "UnSpawned" then
or capGroup.state == Spearhead.classes.capClasses.CapGroup.GroupState.REARMING capGroup:Spawn()
then end
return
end end
timer.scheduleFunction(CheckReschedulingAsync, self, timer.getTime() + 1)
end end
function CapBase:SpawnIfApplicable() function CapBase:CheckAndScheduleCAP()
self.logger:debug("Check spawns for airbase " .. self.airbaseName ) self.logger:debug("Check taskings for airbase " .. self.airbaseName)
for groupName, capGroup in pairs(self.groupsByName) do
local targetStage = capGroup:GetTargetZone(self.activeStage)
if targetStage ~= nil and capGroup.state == Spearhead.classes.capClasses.CapGroup.GroupState.UNSPAWNED then local countPerStage = {}
capGroup:SpawnOnTheRamp() local requiredPerStage = {}
end
end local airbase = Airbase.getByName(self.airbaseName)
if not airbase then
return nil
end end
function CapBase:CheckAndScheduleCAP() local activeStageID = tostring(self.activeStage)
self.logger:debug("Check taskings for airbase " .. self.airbaseName ) --Count back up groups that are active or reassign to the new zone if that's needed
for _, group in pairs(self.capGroupsByName) do
local countPerStage = {} if group:IsBackup() == true then
local requiredPerStage = {} 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 if supposedTargetZoneID == nil then
for _, backupGroup in pairs(self.BackupGroups) do self.logger:debug("CapGroup " .. group:GetName() .. " has no target zone for stage " .. activeStageID)
if backupGroup.state == Spearhead.classes.capClasses.CapGroup.GroupState.INTRANSIT or backupGroup.state == Spearhead.classes.capClasses.CapGroup.GroupState.ONSTATION then group:SendRTB(airbase)
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
else else
backupGroup:SendRTBAndDespawn() if supposedTargetZoneID and supposedTargetZoneID ~= currentTargetZone then
end if state == "RtbInTen" then
elseif backupGroup.state == Spearhead.classes.capClasses.CapGroup.GroupState.RTBINTEN and backupGroup:GetTargetZone(self.activeStage) ~= backupGroup.assignedStageNumber then self.logger:debug("CapGroup " .. group:GetName() .. " is RTB in 10 minutes, sending to RTB already")
backupGroup:SendRTB() group:SendRTB(airbase)
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)
else else
countPerStage[supposedTargetStage] = countPerStage[supposedTargetStage] + 1 local triggerZone = self.database:GetCapZoneForZoneID(supposedTargetZoneID)
primaryGroup:SendRTB() 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
end end
countPerStage[supposedTargetStage] = countPerStage[supposedTargetStage] + 1
elseif primaryGroup.state == Spearhead.classes.capClasses.CapGroup.GroupState.RTBINTEN and primaryGroup:GetTargetZone(self.activeStage) ~= primaryGroup.assignedStageNumber then if countPerStage[supposedTargetZoneID] == nil then
primaryGroup:SendRTB() countPerStage[supposedTargetZoneID] = 0
end end
else
primaryGroup:SendRTBAndDespawn()
end
end
for _, backupGroup in pairs(self.BackupGroups) do if supposedTargetZoneID == group:GetCurrentTargetZoneID() and (state == "OnStation" or state =="InTransit") then
if backupGroup.state == Spearhead.classes.capClasses.CapGroup.GroupState.READYONRAMP then countPerStage[supposedTargetZoneID] = countPerStage[supposedTargetZoneID] + 1
local supposedTargetStage = backupGroup:GetTargetZone(self.activeStage)
if supposedTargetStage then
if countPerStage[supposedTargetStage] == nil then
countPerStage[supposedTargetStage] = 0
end end
if countPerStage[supposedTargetStage] < requiredPerStage[supposedTargetStage] then
backupGroup:SendToStage(supposedTargetStage)
countPerStage[supposedTargetStage] = countPerStage[supposedTargetStage] + 1
end
else
backupGroup:SendRTBAndDespawn()
end end
end end
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) function CapBase:OnStageNumberChanged(number)
self.activeStage = number self.activeStage = number
@@ -211,25 +227,21 @@ function CapBase:OnStageNumberChanged(number)
mission:SpawnActive() mission:SpawnActive()
end end
end end
self:SpawnIfApplicable() self:SpawnIfApplicable()
timer.scheduleFunction(CheckReschedulingAsync, self, timer.getTime() + 5)
end end
---@param stageNumber number ---@param stageNumber number
---@return boolean ---@return boolean
function CapBase:IsBaseActiveWhenStageIsActive(stageNumber) function CapBase:IsBaseActiveWhenStageIsActive(stageNumber)
for _, group in pairs(self.PrimaryGroups) do for _, group in pairs(self.capGroupsByName) do
local target = group:GetTargetZone(stageNumber) local target = group:GetZoneIDWhenStageID(tostring(stageNumber))
if target ~= nil then if group:IsBackup() == false and target ~= nil then
return true return true
end end
end end
return false return false
end end
if not Spearhead then Spearhead = {} end if not Spearhead then Spearhead = {} end
if not Spearhead.classes then Spearhead.classes = {} end if not Spearhead.classes then Spearhead.classes = {} end
if not Spearhead.classes.capClasses then Spearhead.classes.capClasses = {} end if not Spearhead.classes.capClasses then Spearhead.classes.capClasses = {} end
-429
View File
@@ -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<string, string>
---@field private aliveUnits table<string, boolean>
---@field private onStatusUpdatedListener Array<OnUpdateListener>
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
+427
View File
@@ -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<string,Vec3>
---@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"
---| ""
+144
View File
@@ -0,0 +1,144 @@
---@class CapGroup : AirGroup
---@field private _targetZoneIdPerStage table<string, string>
---@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
@@ -21,16 +21,12 @@ end
---@param weapon Weapon ---@param weapon Weapon
function RunwayBombingTracker:OnWeaponFired(unit, weapon, target) function RunwayBombingTracker:OnWeaponFired(unit, weapon, target)
if weapon == nil then if weapon == nil then
return return
end end
self._logger:debug("Tracking weapon for runway impact onWeaponFired")
local desc = weapon:getDesc() local desc = weapon:getDesc()
local isTrackable = desc.category == Weapon.Category.BOMB or (desc.category == Weapon.Category.MISSILE and desc.missileCategory == Weapon.MissileCategory.CRUISE) local isTrackable = desc.category == Weapon.Category.BOMB or (desc.category == Weapon.Category.MISSILE and desc.missileCategory == Weapon.MissileCategory.CRUISE)
if isTrackable == true then if isTrackable == true then
---@type WeaponTrackingArgs ---@type WeaponTrackingArgs
+327
View File
@@ -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
+201
View File
@@ -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
+77 -35
View File
@@ -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 = {}; local CapConfig = {};
function CapConfig:new() CapConfig.__index = CapConfig
local o = {}
setmetatable(o, { __index = self }) ---@return CapConfig
function CapConfig.new()
local self = setmetatable({}, CapConfig)
if SpearheadConfig == nil then SpearheadConfig = {} end if SpearheadConfig == nil then SpearheadConfig = {} end
if SpearheadConfig.CapConfig == nil then SpearheadConfig.CapConfig = {} end if SpearheadConfig.CapConfig == nil then SpearheadConfig.CapConfig = {} end
local enabled = SpearheadConfig.CapConfig.enabled local enabled = SpearheadConfig.CapConfig.enabled
if enabled == nil then enabled = true end if enabled == nil then enabled = true end
---@return boolean self._isEnabled = enabled
o.isEnabled = function(self) return enabled == true end
local minSpeed = (tonumber(SpearheadConfig.CapConfig.minSpeed) or 400) * 0.514444 self._minSpeed = (tonumber(SpearheadConfig.CapConfig.minSpeed) or 400) * 0.514444
---@return number self._maxSpeed = (tonumber(SpearheadConfig.CapConfig.maxSpeed) or 400) * 0.514444
o.getMinSpeed = function(self) return minSpeed end
local maxSpeed = (tonumber(SpearheadConfig.CapConfig.maxSpeed) or 400) * 0.514444 self._minAlt = (tonumber(SpearheadConfig.CapConfig.minAlt) or 18000) * 0.3048
---@return number self._maxAlt = (tonumber(SpearheadConfig.CapConfig.maxAlt) or 28000) * 0.3048
o.getMaxSpeed = function(self) return maxSpeed end
local minAlt = (tonumber(SpearheadConfig.CapConfig.minAlt) or 18000) * 0.3048 self._minDurationOnStation = 1200
---@return number self._maxDurationOnStation = 2700
o.getMinAlt = function(self) return minAlt end
local maxAlt = (tonumber(SpearheadConfig.CapConfig.maxAlt) or 28000) * 0.3048 self._maxDeviationRange = 35 * 1852 -- in meters
---@return number self._rearmDelay = tonumber(SpearheadConfig.CapConfig.rearmDelay) or 600
o.getMaxAlt = function(self) return maxAlt end self._repairDelay = tonumber(SpearheadConfig.CapConfig.repairDelay) or 600
self._deathDelay = tonumber(SpearheadConfig.CapConfig.deathDelay) or 1800
local minDurationOnStation = 1200 return self;
---@return number end
o.getMinDurationOnStation = function(self) return minDurationOnStation end
local maxDurationOnStation = 2700 function CapConfig:isEnabled()
---@return number return self._isEnabled
o.getmaxDurationOnStation = function(self) return maxDurationOnStation end end
local maxDeviationRange = 20 * 1852; ---@return number
---@return number function CapConfig:getMinSpeed()
o.getMaxDeviationRange = function(self) return maxDeviationRange end return self._minSpeed
end
local rearmDelay = tonumber(SpearheadConfig.CapConfig.rearmDelay) or 600 ---@return number
---@return number function CapConfig:getMaxSpeed()
o.getRearmDelay = function(self) return rearmDelay end return self._maxSpeed
end
local deathDelay = tonumber(SpearheadConfig.CapConfig.deathDelay) or 1800 ---@return number
---@return number function CapConfig:getMinAlt()
o.getDeathDelay = function(self) return deathDelay end return self._minAlt
o.logLevel = "INFO" 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 end
if not Spearhead.internal then Spearhead.internal = {} end if not Spearhead.internal then Spearhead.internal = {} end
+104 -10
View File
@@ -36,15 +36,15 @@ do -- INIT UTIL
---@param orig table ---@param orig table
---@return table copy ---@return table copy
function UTIL.copyTable(orig) function UTIL.deepCopyTable(orig)
local orig_type = type(orig) local orig_type = type(orig)
local copy local copy
if orig_type == 'table' then if orig_type == 'table' then
copy = {} copy = {}
for orig_key, orig_value in next, orig, nil do 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 end
setmetatable(copy, UTIL.copyTable(getmetatable(orig))) setmetatable(copy, UTIL.deepCopyTable(getmetatable(orig)))
else -- number, string, boolean, etc else -- number, string, boolean, etc
copy = orig copy = orig
end end
@@ -88,15 +88,15 @@ do -- INIT UTIL
table.insert(sb, string.rep(" ", indent)) -- indent it table.insert(sb, string.rep(" ", indent)) -- indent it
if type(value) == "table" and not done[value] then if type(value) == "table" and not done[value] then
done[value] = true 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, table_print(value, indent + 2, done))
table.insert(sb, string.rep(" ", indent)) -- indent it table.insert(sb, string.rep(" ", indent)) -- indent it
table.insert(sb, "}\n"); table.insert(sb, "},\n");
elseif "number" == type(key) then elseif "number" == type(key) then
table.insert(sb, string.format("\"%s\"\n", tostring(value))) table.insert(sb, string.format("\"%s\",\n", tostring(value)))
else else
table.insert(sb, string.format( table.insert(sb, string.format(
"%s = \"%s\"\n", tostring(key), tostring(value))) "[\"%s\"] = \"%s\",\n", tostring(key), tostring(value)))
end end
end end
return table.concat(sb) return table.concat(sb)
@@ -172,6 +172,54 @@ do -- INIT UTIL
return (vec.x ^ 2 + vec.y ^ 2 + vec.z ^ 2) ^ 0.5 return (vec.x ^ 2 + vec.y ^ 2 + vec.z ^ 2) ^ 0.5
end 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 isInComplexPolygon(polygon, x, y)
local function getEdges(poly) local function getEdges(poly)
local result = {} local result = {}
@@ -706,7 +754,7 @@ do -- INIT DCS_UTIL
---@param groupName string ---@param groupName string
function DCS_UTIL.IsGroupStatic(groupName) function DCS_UTIL.IsGroupStatic(groupName)
if DCS_UTIL.__miz_groups[groupName] then 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 end
return StaticObject.getByName(groupName) ~= nil 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 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 uncontrolled boolean? Sets the group to be uncontrolled on spawn
---@param countryId CountryID? Overwrites the country ---@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 table? new_group the Group class that was spawned
---@return boolean? isStatic whether the group is a static or not ---@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 if groupName == nil then
return nil, nil return nil, nil
end end
@@ -1280,7 +1329,7 @@ do -- INIT DCS_UTIL
return object, true return object, true
end end
else else
local spawn_template = template.group_template local spawn_template = Spearhead.Util.deepCopyTable(template.group_template)
if location ~= nil then if location ~= nil then
local x_offset local x_offset
if location.x ~= nil then x_offset = spawn_template.x - location.x end if location.x ~= nil then x_offset = spawn_template.x - location.x end
@@ -1298,6 +1347,26 @@ do -- INIT DCS_UTIL
end end
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 if route ~= nil then
spawn_template.route = route spawn_template.route = route
end end
@@ -1305,6 +1374,7 @@ do -- INIT DCS_UTIL
if uncontrolled ~= nil then if uncontrolled ~= nil then
spawn_template.uncontrolled = uncontrolled spawn_template.uncontrolled = uncontrolled
end end
local country = countryId or template.country_id local country = countryId or template.country_id
local new_group = coalition.addGroup(country, template.category, spawn_template) local new_group = coalition.addGroup(country, template.category, spawn_template)
return new_group, false return new_group, false
@@ -1320,6 +1390,30 @@ do -- INIT DCS_UTIL
end end
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) function DCS_UTIL.IsBingoFuel(groupName, offset)
if offset == nil then offset = 0 end if offset == nil then offset = 0 end
local bingoSetting = 0.20 local bingoSetting = 0.20
+38 -110
View File
@@ -8,7 +8,7 @@
---@field StageZonesByNumber table<string, Array<string>> Stage zones grouped by index number ---@field StageZonesByNumber table<string, Array<string>> Stage zones grouped by index number
---@field AllFarpZones Array<string> ---@field AllFarpZones Array<string>
---@field CapRoutes Array<string> All Cap route zone names ---@field CapRoutes Array<string> All Cap route zone names
---@field CapDataPerStageNumber table<integer, CapData> table<StageNumber, table> ---@field capZonesByCapZoneID table<string, CapRoute> table<StageNumber, table>
---@field CarrierRouteZones Array<string> All Carrier routes zones ---@field CarrierRouteZones Array<string> All Carrier routes zones
---@field BlueSams Array<string> All blue sam zones ---@field BlueSams Array<string> All blue sam zones
---@field SupplyHubZones Array<string> All supply hub zones ---@field SupplyHubZones Array<string> All supply hub zones
@@ -19,13 +19,10 @@
---@field FarpZoneData table<string,FarpZoneData> ---@field FarpZoneData table<string,FarpZoneData>
---@field missionCodes table<string, boolean> ---@field missionCodes table<string, boolean>
---@class CapData
---@field routes Array<CapRoute>
---@field current integer
---@class CapRoute ---@class CapRoute
---@field point1 Vec3 ---@field zones Array<SpearheadTriggerZone>
---@field point2 Vec3? ---@field current number
---@class MissionAnnotations ---@class MissionAnnotations
---@field description string? ---@field description string?
@@ -82,7 +79,7 @@ function Database.New(Logger)
AllZoneNames = {}, AllZoneNames = {},
BlueSams = {}, BlueSams = {},
CapRoutes = {}, CapRoutes = {},
CapDataPerStageNumber = {}, capZonesByCapZoneID = {},
CarrierRouteZones = {}, CarrierRouteZones = {},
MissionZones = {}, MissionZones = {},
MissionZonesLocations = {}, MissionZonesLocations = {},
@@ -367,55 +364,21 @@ function Database.New(Logger)
self:loadMiscGroupsInStages() self:loadMiscGroupsInStages()
for _, zoneData in pairs(self._tables.StageZones) do for _, cap_route_zone in pairs(self._tables.CapRoutes) do
local number = zoneData.StageIndex local split = Spearhead.Util.split_string(cap_route_zone, "_")
local zoneID = split[2]
for _, cap_route_zone in pairs(self._tables.CapRoutes) do if zoneID then
if Spearhead.DcsUtil.isZoneInZone(cap_route_zone, zoneData.StageZoneName) == true then if tables.capZonesByCapZoneID[zoneID] == nil then
local zone = Spearhead.DcsUtil.getZoneByName(cap_route_zone) tables.capZonesByCapZoneID[zoneID] = {
if zone then zones = {},
---@type CapData current = 1
local capData = self._tables.CapDataPerStageNumber[number] }
if capData == nil then end
capData = {
routes = {},
current = 0
}
self._tables.CapDataPerStageNumber[number] = capData
end
local zone = Spearhead.DcsUtil.getZoneByName(cap_route_zone)
if zone.zone_type == Spearhead.DcsUtil.ZoneType.Cilinder then if zone then
table.insert(capData.routes, table.insert(tables.capZonesByCapZoneID[zoneID].zones, zone)
{ 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
end end
end end
end end
@@ -842,66 +805,31 @@ function Database:GetLocationForMissionZone(missionZoneName)
return self._tables.MissionZonesLocations[missionZoneName] return self._tables.MissionZonesLocations[missionZoneName]
end end
---comment ---@param zoneID string
---@param stageNumber string ---@return SpearheadTriggerZone?
---@param baseName string function Database:GetCapZoneForZoneID(zoneID)
---@return table? zoneID = tostring(zoneID) or "nothing"
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
if math.random(1, 2) % 2 == 0 then local capZonesForID = self._tables.capZonesByCapZoneID[zoneID]
return { point1 = closest, point2 = { x = stagezone.location.x, z = stagezone.location.y } }
else if capZonesForID and capZonesForID.zones then
return { point1 = { x = stagezone.location.x, z = stagezone.location.y }, point2 = closest }
end local count = Spearhead.Util.tableLength(capZonesForID.zones)
end 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 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 end
return nil
end end
---@return Array<string> result a list of stage zone names ---@return Array<string> result a list of stage zone names
@@ -159,14 +159,17 @@ function StageBase:SpawnBlueUnits()
end end
function StageBase:ActivateRedStage() function StageBase:ActivateRedStage()
if self._initialSide == 2 and self._airbase then self._logger:debug("Activate red stage for airbase: " .. self._airbase:getName())
self._airbase:setCoalition(1) if self._airbase and (self._initialSide == 2 or self._initialSide == 1) then
self._airbase:setCoalition(coalition.side.RED)
self._airbase:autoCapture(false) self._airbase:autoCapture(false)
end end
self:SpawnRedUnits() self:SpawnRedUnits()
end end
function StageBase:ActivateBlueStage() function StageBase:ActivateBlueStage()
self._logger:debug("Activate blue stage for airbase: " .. self._airbase:getName())
self:CleanRedUnits() self:CleanRedUnits()
if self._buildableMission and self._buildableMission:getState() ~= "COMPLETED" then if self._buildableMission and self._buildableMission:getState() ~= "COMPLETED" then
@@ -178,7 +181,8 @@ end
function StageBase:FinaliseBlueStage() function StageBase:FinaliseBlueStage()
if self._initialSide == 2 and self._airbase then if self._initialSide == 2 and self._airbase then
self._airbase:setCoalition(2) self._airbase:setCoalition(coalition.side.BLUE)
self._airbase:autoCapture(false)
end end
self:SpawnBlueUnits() self:SpawnBlueUnits()
@@ -9,6 +9,8 @@
local BattleManager = {} local BattleManager = {}
BattleManager.__index = BattleManager BattleManager.__index = BattleManager
local debugDrawing = false
---@param redGroups Array<SpearheadGroup> ---@param redGroups Array<SpearheadGroup>
---@param blueGroups Array<SpearheadGroup> ---@param blueGroups Array<SpearheadGroup>
---@param name string ---@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) self:DrawDebugLine(point, unit)
end end
self._logger:debug("Red unit " .. unit:getName() .. " will shoot " .. qty .. " rounds at point: " .. tostring(point))
local controller = unit:getController() local controller = unit:getController()
if controller then if controller then
controller:setTask(shootTask) controller:setTask(shootTask)
@@ -241,7 +241,7 @@ function BattleManager:GetRandomPoint(origin, groupHulls)
if not hull then return nil end if not hull then return nil end
local shootPoints = Spearhead.Util.GetTangentHullPointsFromOrigin(hull, origin) local shootPoints = Spearhead.Util.GetTangentHullPointsFromOrigin(hull, origin)
if SpearheadConfig and SpearheadConfig.debugEnabled == true then if debugDrawing == true then
self:DrawDebugZone({ hull }) self:DrawDebugZone({ hull })
end end
@@ -271,8 +271,8 @@ function MissionCommandsHelper:AddAllMissionCommandsToGroup(groupID)
local mission = self.missionsByCode[code] local mission = self.missionsByCode[code]
if mission and mission.priority == "primary" then if mission and mission.priority == "primary" then
count = count + 1 count = count + 1
if count <= perFolder then if count <= perFolder then
local copied = Spearhead.Util.copyTable(path) local copied = Spearhead.Util.deepCopyTable(path)
self:addMissionCommands(groupID, copied, mission) self:addMissionCommands(groupID, copied, mission)
else else
local name = "Next Menu ..." local name = "Next Menu ..."
@@ -294,7 +294,7 @@ function MissionCommandsHelper:AddAllMissionCommandsToGroup(groupID)
if mission and mission.priority == "secondary" then if mission and mission.priority == "secondary" then
count = count + 1 count = count + 1
if count <= perFolder then if count <= perFolder then
local copied = Spearhead.Util.copyTable(path) local copied = Spearhead.Util.deepCopyTable(path)
self:addMissionCommands(groupID, copied, mission) self:addMissionCommands(groupID, copied, mission)
else else
local name = "Next Menu ..." local name = "Next Menu ..."
@@ -29,6 +29,7 @@ function SupplyUnitsTracker.getOrCreate(logLevel)
singleton._supplyUnitsByName = {} singleton._supplyUnitsByName = {}
singleton._droppedCrates = {} singleton._droppedCrates = {}
singleton._registeredHubs = {} singleton._registeredHubs = {}
singleton._supplyUnitSpawnedListener = {}
singleton._commandsHelper = Spearhead.classes.stageClasses.helpers.MissionCommandsHelper.getOrCreate(singleton._logger.LogLevel) singleton._commandsHelper = Spearhead.classes.stageClasses.helpers.MissionCommandsHelper.getOrCreate(singleton._logger.LogLevel)
+16 -3
View File
@@ -25,13 +25,26 @@ SpearheadConfig = {
-- unit: feet -- unit: feet
maxAlt = 28000, -- default 28000 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. --Delay for aircraft from touchdown to off the chocks.
-- unit: seconds -- unit: seconds
rearmDelay = 600, -- default 600 rearmDelay = 600, -- default 600
--Delay for aircraft from death to takeoff. -- Delay for aircraft that has died to be repaired before rearming starts.
--When the seconds remaining is the same at the rearmDelay it will be spawned on the ramp and follow the rearm logic. -- unit: seconds
-- !! Can not be lower than rearmDelay 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 -- unit: seconds
deathDelay = 1800, -- default 1800 deathDelay = 1800, -- default 1800
}, },
+4 -1
View File
@@ -44,7 +44,10 @@ assert(loadfile(classPath .. "stageClasses\\SpecialZones\\BlueSam.lua"))()
assert(loadfile(classPath .. "stageClasses\\SpecialZones\\FarpZone.lua"))() assert(loadfile(classPath .. "stageClasses\\SpecialZones\\FarpZone.lua"))()
assert(loadfile(classPath .. "stageClasses\\SpecialZones\\SupplyHub.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\\GlobalCapManager.lua"))()
assert(loadfile(classPath .. "capClasses\\CapAirbase.lua"))() assert(loadfile(classPath .. "capClasses\\CapAirbase.lua"))()
assert(loadfile(classPath .. "capClasses\\runwayBombing\\RunwayBombingTracker.lua"))() assert(loadfile(classPath .. "capClasses\\runwayBombing\\RunwayBombingTracker.lua"))()
+2 -2
View File
@@ -25,8 +25,8 @@ SpearheadConfig = {
--Delay for aircraft from touchdown to off the chocks. --Delay for aircraft from touchdown to off the chocks.
-- unit: seconds -- unit: seconds
rearmDelay = 600, -- default 600 rearmDelay = 180, -- default 600
repairDelay = 600, -- default 600
--Delay for aircraft from death to takeoff. --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. --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 -- !! Can not be lower than rearmDelay