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>
<p>
<strong>Format:</strong> <span class="inline-lua"><span class="lua-variable">CAPROUTE_[Name]</span></span> <br />
<strong>Example:</strong> <span class="inline-lua"><span class="lua-variable">CAPROUTE_ALPHA</span></span>
<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_103_ALPHA</span></span>
</p>
<p>
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>
<pre>
<span class="lua-comment">1 at x:</span> <span class="lua-variable">[&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;capStage&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 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 at Active:</span> <span class="lua-variable">[&lt;activeStage&gt;-&lt;activeStage&gt;]A</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;CapRouteID&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;CapRouteID&gt;</span>
<span class="lua-comment">Divider: |</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[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[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>
<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>
</pre>
<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
---@field groupNames Array<string>
---@field database Database
---@field airbaseName string
---@field logger table
---@field activeStage number
---@field capConfig table
---@field activeCapStages number
---@field lastStatesByName table<string, string>
---@field groupsByName table<string, CapGroup>
---@field PrimaryGroups Array<CapGroup>
---@field BackupGroups Array<CapGroup>
---@field private airbaseName string
---@field private logger table
---@field private database Database
---@field private activeStage number
---@field private capConfig table
---@field private capGroupsByName table<string, CapGroup>
---@field private runwayBombingTracker RunwayBombingTracker
---@field private runwayStrikeMissions table<string, RunwayStrikeMission>
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
-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
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
+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 = {};
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
+104 -10
View File
@@ -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
+38 -110
View File
@@ -8,7 +8,7 @@
---@field StageZonesByNumber table<string, Array<string>> Stage zones grouped by index number
---@field AllFarpZones Array<string>
---@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 BlueSams Array<string> All blue sam zones
---@field SupplyHubZones Array<string> All supply hub zones
@@ -19,13 +19,10 @@
---@field FarpZoneData table<string,FarpZoneData>
---@field missionCodes table<string, boolean>
---@class CapData
---@field routes Array<CapRoute>
---@field current integer
---@class CapRoute
---@field point1 Vec3
---@field point2 Vec3?
---@field zones Array<SpearheadTriggerZone>
---@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<string> result a list of stage zone names
@@ -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()
@@ -9,6 +9,8 @@
local BattleManager = {}
BattleManager.__index = BattleManager
local debugDrawing = false
---@param redGroups Array<SpearheadGroup>
---@param blueGroups Array<SpearheadGroup>
---@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
@@ -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 ..."
@@ -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)
+16 -3
View File
@@ -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
},
+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\\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"))()
+2 -2
View File
@@ -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