Dcs lua tool (#66)

* custom drawings and animations for video

* wip

* Work in progress refactor for toolkit

* wip

* Refactored everything for Lua Compile Tool

* Trying out the github action

* updated the github action ref

* initial working version

---------

Co-authored-by: ex61wi <tim.rorije@ing.com>
Co-authored-by: dutchie031 <dutchie031>
This commit is contained in:
2026-06-06 19:26:32 +02:00
committed by GitHub
co-authored by ex61wi dutchie031 <dutchie031>
parent c027921527
commit 128aed1d2b
82 changed files with 8963 additions and 4390 deletions
+434
View File
@@ -0,0 +1,434 @@
local Util = require("classes.util.Util")
local CapGroup = require("classes.capClasses.airGroups.CapGroup")
local SweepGroup = require("classes.capClasses.airGroups.SweepGroup")
local InterceptGroup = require("classes.capClasses.airGroups.InterceptGroup")
local RunwayBombingTracker = require("classes.capClasses.runwayBombing.RunwayBombingTracker")
local SpearheadEvents = require("classes.spearhead_events")
local RunwayStrikeMission = require("classes.stageClasses.missions.RunwayStrikeMission")
---@class CapBase : OnStageChangedListener
---@field private airbaseName string
---@field private logger table
---@field private database Database
---@field private detectionManager DetectionManager
---@field private activeStage number
---@field private capConfig table
---@field private capGroupsByName table<string, CapGroup>
---@field private sweepGroupsByName table<string, SweepGroup>
---@field private interceptGroupsByName table<string, InterceptGroup>
---@field private runwayBombingTracker RunwayBombingTracker
---@field private runwayStrikeMissions table<string, RunwayStrikeMission>
local CapBase = {}
---comment
---@param self CapBase
---@param time number
---@return number?
local CheckStateContinuous = function(self, time)
self:CheckAndScheduleCAP()
self:CheckAndScheduleSweep()
self:CheckAndScheduleIntercept()
return time + 15
end
---comment
---@param airbaseName string
---@param database Database
---@param logger table
---@param capConfig table
---@param stageConfig table
---@param runwayBombingTracker RunwayBombingTracker
---@param detectionManager DetectionManager
---@param spawnManager SpawnManager
---@return CapBase
function CapBase.new(airbaseName, database, logger, capConfig, stageConfig, runwayBombingTracker, detectionManager, spawnManager)
CapBase.__index = CapBase
local self = setmetatable({}, { __index = CapBase }) --[[@as CapBase]]
self.runwayBombingTracker = runwayBombingTracker
self.runwayStrikeMissions = {}
self.airbaseName = airbaseName
self.logger = logger
self.activeStage = 0
self.capConfig = capConfig
self.database = database
self.capGroupsByName = {}
self.sweepGroupsByName = {}
self.interceptGroupsByName = {}
self.detectionManager = detectionManager
local baseData = database:getAirbaseDataForZone(airbaseName)
if baseData and baseData.CapGroups then
for key, name in pairs(baseData.CapGroups) do
local capGroup = CapGroup.New(name, capConfig, logger, spawnManager)
if capGroup then
self.capGroupsByName[name] = capGroup
end
end
end
if baseData and baseData.SweepGroups then
for key, name in pairs(baseData.SweepGroups) do
local sweepGroup = SweepGroup.New(name, capConfig, logger, spawnManager)
if sweepGroup then
self.sweepGroupsByName[name] = sweepGroup
end
end
end
if baseData and baseData.InterceptGroups then
for key, name in pairs(baseData.InterceptGroups) do
local interceptGroup = InterceptGroup.New(name, capConfig, logger, detectionManager, spawnManager)
if interceptGroup then
self.interceptGroupsByName[name] = interceptGroup
end
end
end
local capFlights = Util.tableLength(self.capGroupsByName)
local sweepFlights = Util.tableLength(self.sweepGroupsByName)
local interceptFlights = Util.tableLength(self.interceptGroupsByName)
logger:info(airbaseName .. " : " .. capFlights .." CAP | " .. sweepFlights .. " SWEEP | " .. interceptFlights .. " INTERCEPT")
self:CreateRunwayStrikeMission(database)
SpearheadEvents.AddStageNumberChangedListener(self)
timer.scheduleFunction(CheckStateContinuous, self, timer.getTime() + 15)
return self
end
---@private
---@param database Database
function CapBase:CreateRunwayStrikeMission(database)
local airbase = Airbase.getByName(self.airbaseName)
if not airbase then
self.logger:debug("Could not find a airbase with name to create runway mission" .. self.airbaseName)
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 = RunwayStrikeMission.new(runway, self.airbaseName,
database, self.logger, self.runwayBombingTracker)
self.runwayStrikeMissions[runway.Name] = mission
end
end
end
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
for groupName, sweepGroup in pairs(self.sweepGroupsByName) do
local targetStage = sweepGroup:GetZoneIDWhenStageID(tostring(self.activeStage))
if targetStage ~= nil and sweepGroup:GetState() == "UnSpawned" then
sweepGroup:Spawn()
end
end
for groupName, interceptGroup in pairs(self.interceptGroupsByName) do
local targetStage = interceptGroup:GetZoneIDWhenStageID(tostring(self.activeStage))
if targetStage ~= nil and interceptGroup:GetState() == "UnSpawned" then
interceptGroup:Spawn()
end
end
end
function CapBase:CheckAndScheduleCAP()
self.logger:debug("Check taskings for airbase " .. self.airbaseName)
local countPerStage = {}
local requiredPerStage = {}
local airbase = Airbase.getByName(self.airbaseName)
if not airbase then
return nil
end
local activeStageID = tostring(self.activeStage)
--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()
if supposedTargetZoneID == nil then
self.logger:debug("CapGroup " .. group:GetName() .. " has no target zone for stage " .. activeStageID)
group:SendRTB(airbase)
else
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
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
if countPerStage[supposedTargetZoneID] == nil then
countPerStage[supposedTargetZoneID] = 0
end
if supposedTargetZoneID == group:GetCurrentTargetZoneID() and (state == "OnStation" or state =="InTransit") then
countPerStage[supposedTargetZoneID] = countPerStage[supposedTargetZoneID] + 1
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 requiredPerStage[supposedZone] == nil then
requiredPerStage[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:CheckAndScheduleSweep()
self.logger:debug("Check sweep taskings for airbase " .. self.airbaseName)
local airbase = Airbase.getByName(self.airbaseName)
if not airbase then
return nil
end
local activeStageID = tostring(self.activeStage)
for _, group in pairs(self.sweepGroupsByName) do
local targetZoneID = group:GetZoneIDWhenStageID(activeStageID)
if targetZoneID then
local triggerZone = self.database:GetCapZoneForZoneID(targetZoneID)
if triggerZone then
if group:GetState() == "ReadyOnTheRamp" then
group:SendToZone(triggerZone, targetZoneID, airbase)
end
else
self.logger:debug("SweepGroup " .. group:GetName() .. " has no trigger zone for stage " .. activeStageID)
group:SendRTB(airbase)
end
end
end
end
function CapBase:CheckAndScheduleIntercept()
self.logger:debug("Check intercept taskings for airbase " .. self.airbaseName)
local interceptZoneIDs = {}
local interceptZoneIDs = {}
local airbase = Airbase.getByName(self.airbaseName)
if not airbase then
return nil
end
for name, group in pairs(self.interceptGroupsByName) do
local targetZoneID = group:GetZoneIDWhenStageID(tostring(self.activeStage))
if targetZoneID then
interceptZoneIDs[targetZoneID] = true
end
end
---@type table<string, Array<string>>
local unitsToInterceptPerZone = {}
local detectedUnits = self.detectionManager:GetDetectedUnitsBy(coalition.side.RED)
for targetZoneID, _ in pairs(interceptZoneIDs) do
local zones = self.database:GetInterceptZonesForZoneID(targetZoneID)
if zones then
for _, zone in pairs(zones) do
self.logger:debug("Check intercept zone " .. zone.name .. " for airbase " .. self.airbaseName)
for _, unitName in pairs(detectedUnits) do
self.logger:debug("Check unit " .. unitName .. " for intercept in zone " .. zone.name)
local unit = Unit.getByName(unitName)
if unit and unit:isExist() then
local unitPos = unit:getPoint()
if Util.is3dPointInZone(unitPos, zone) == true then
if not unitsToInterceptPerZone[zone.name] then
unitsToInterceptPerZone[zone.name] = {}
end
table.insert(unitsToInterceptPerZone[zone.name], unitName)
end
end
end
end
end
end
---RATIO. Amount of enemy fighters required per zone before another group gets added.
local ratio = 4 -- Ratio of units to intercept per zone, can be adjusted
for name, targets in pairs(unitsToInterceptPerZone) do
local required = 0
local nrTargets = Util.tableLength(targets)
if targets and nrTargets > 0 then
required = math.ceil(nrTargets / ratio)
end
local total = 0
for _, group in pairs(self.interceptGroupsByName) do
if group:GetState() == "OnStation" and group:GetCurrentTargetZone() == name then
group:SetTargetUnits(targets)
total = total + 1
end
end
if total < required then
for _, group in pairs(self.interceptGroupsByName) do
if total < required then
local zoneID = group:GetZoneIDWhenStageID(tostring(self.activeStage))
if group:GetState() == "ReadyOnTheRamp" then
group:SendToInterceptUnits(targets, name, airbase)
total = total + 1
self.logger:debug("Intercept group " .. group:GetName() .. " sent to intercept zone " .. name )
elseif group:GetState() == "InTransit" or group:GetState() == "OnStation" then
group:SetTargetUnits(targets)
end
end
end
end
end
end
function CapBase:OnStageNumberChanged(number)
self.activeStage = number
if self:IsBaseActiveWhenStageIsActive(number) == true then
for _, mission in pairs(self.runwayStrikeMissions) do
mission:SpawnActive()
end
end
self:SpawnIfApplicable()
end
---@param stageNumber number
---@return boolean
function CapBase:IsBaseActiveWhenStageIsActive(stageNumber)
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
for _, group in pairs(self.sweepGroupsByName) do
local target = group:GetZoneIDWhenStageID(tostring(stageNumber))
if target ~= nil then
return true
end
end
for _, group in pairs(self.interceptGroupsByName) do
local target = group:GetZoneIDWhenStageID(tostring(stageNumber))
if target ~= nil then
return true
end
end
return false
end
return CapBase
@@ -0,0 +1,73 @@
local Logger = require("classes.util.Logger")
local Util = require("classes.util.Util")
local RunwayBombingTracker = require("classes.capClasses.runwayBombing.RunwayBombingTracker")
local CapAirbase = require("classes.capClasses.CapAirbase")
---@class GlobalCapManager
local GlobalCapManager = {}
do
local airbasesPerStage = {}
local allAirbasesByName = {}
local activeAirbasesPerActiveStage = {}
local unitsPerzonePerStage = {}
local initiated = false
---comment
---@param database Database
---@param capConfig table
---@param stageConfig StageConfig
---@param detectionManager DetectionManager
---@param logLevel LogLevel
---@param spawnManager SpawnManager
function GlobalCapManager.start(database, capConfig, detectionManager, stageConfig, logLevel, spawnManager)
if initiated == true then return end
local logger = Logger.new("AirbaseManager", logLevel)
local bombTrackLogger = Logger.new("RunwayBombingTracker", logLevel)
local runwayBombingTracker = RunwayBombingTracker.new(bombTrackLogger)
local zones = database:getStagezoneNames()
if zones then
for key, stageName in pairs(zones) do
if airbasesPerStage[stageName] == nil then
airbasesPerStage[stageName] = {}
end
local airbaseNames = database:getAirbaseNamesInStage(stageName)
if airbaseNames then
for _, airbaseName in pairs(airbaseNames) do
if airbaseName then
local airbaseSpecificLogger = Logger.new("CAP_" .. airbaseName, logLevel)
local airbase = CapAirbase.new(airbaseName, database, airbaseSpecificLogger, capConfig, stageConfig, runwayBombingTracker, detectionManager, spawnManager)
if airbase then
table.insert(airbasesPerStage[stageName], airbase)
allAirbasesByName[airbaseName] = airbase
end
end
end
end
end
end
logger:info("Initiated " .. Util.tableLength(allAirbasesByName) .. " airbases for cap")
initiated = true
---returns if there is CAP active
---@param zoneName any
---@param activeZoneNumber number
---@return boolean
GlobalCapManager.IsCapActiveWhenZoneIsActive = function(zoneName, activeZoneNumber)
for _, airbase in pairs(airbasesPerStage[zoneName]) do
if airbase:IsBaseActiveWhenStageIsActive(activeZoneNumber) == true then
return true
end
end
return false
end
end
end
return GlobalCapManager
@@ -0,0 +1,459 @@
local SpearheadEvents = require("classes.spearhead_events")
local RTBMission = require("classes.capClasses.taskings.RTB")
local Util = require("classes.util.Util")
local DcsUtil = require("classes.util.DcsUtil")
---@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
---@field protected _spawnManager SpawnManager
local AirGroup = {}
AirGroup.__index = AirGroup
---@param logger Logger
---@param groupName string
---@param groupType AirGroupType
---@param config CapConfig
---@param spawnManager SpawnManager
function AirGroup:New(groupName, groupType, config, logger, spawnManager)
self._groupName = groupName
self._groupType = groupType
self._isSpawned = false
self._state = "UnSpawned" -- Default state
self._config = config
self._logger = logger
self._spawnManager = spawnManager
local group = Group.getByName(self._groupName)
if group then
SpearheadEvents.addOnGroupRTBListener(self._groupName, self)
SpearheadEvents.addOnGroupRTBInTenListener(self._groupName, self)
SpearheadEvents.addOnGroupOnStationListener(self._groupName, self)
for _, unit in pairs(group:getUnits()) do
SpearheadEvents.addOnUnitLostEventListener(unit:getName(), self)
SpearheadEvents.addOnUnitLandEventListener(unit:getName(), self)
end
spawnManager:DestroyGroup(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 = RTBMission.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
function AirGroup:IsInAir()
local group = Group.getByName(self._groupName)
if group then
local units = group:getUnits()
for _, unit in pairs(units) do
if unit:inAir() == true then
return true
end
end
end
return 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
---@type SpawnOverrides
local overrides = {
emptyLoadouts = withoutLoadout,
uncontrolled = true
}
local group, isStatic = self._spawnManager:SpawnGroup(self._groupName, overrides, false)
if isStatic == true then
--- If For Some reaons someone tries to schedule static units as CAP classes
self._state = "UnSpawned"
return
end
group = group --[[@as Group]]
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
--try remove. Throws Error when number does not exist anymore, hence the pcall
pcall(function()
timer.removeFunction(self._checkLivenessNumber)
end)
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 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")
---@param selfA AirGroup
local respawnTask = function(selfA, time)
selfA:StartRepair()
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
---comment
---@param selfA AirGroup
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()
self._spawnManager:DestroyGroup(self._groupName)
end
return AirGroup
---@alias AirGroupState
---| "UnSpawned"
---| "ReadyOnTheRamp
---| "InTransit"
---| "OnStation"
---| "RtbInTen"
---| "Rtb"
---| "Dead"
---| "Repairing"
---| "Rearming"
---@alias AirGroupType
---| "CAP"
---| "SWEEP"
---| "INTERCEPT"
---| "CAS"
---| "SEAD"
---| "INTERCEPT"
---| ""
@@ -0,0 +1,146 @@
local AirGroup = require("classes.capClasses.airGroups.AirGroup")
local CAP = require("classes.capClasses.taskings.CAP")
local Util = require("classes.util.Util")
local MissionEditorWarner = require("classes.util.MissionEditorWarnings")
---@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
---@param spawnManager SpawnManager
---@return CapGroup
function CapGroup.New(groupName, config, logger, spawnManager)
setmetatable(CapGroup, AirGroup)
local self = setmetatable({}, CapGroup) --[[@as CapGroup]]
AirGroup.New(self, groupName, "CAP", config, logger, spawnManager)
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 = CAP.getAsMission(self._groupName, airbase, zone, self._config)
self:SetMission(mission)
else
local mission = 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 = Util.split_string(groupName, "_")
local partCount = 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
MissionEditorWarner.Add("Could not parse the CAP config for group: " .. groupName)
return
end
local subsplit = Util.split_string(configPart, "|")
if subsplit then
for key, value in pairs(subsplit) do
local keySplit = Util.split_string(value, "]")
local targetZone = keySplit[2]
local allActives = string.sub(keySplit[1], 2, #keySplit[1])
local commaSeperated = Util.split_string(allActives, ",")
for _, value in pairs(commaSeperated) do
local dashSeperated = Util.split_string(value, "-")
if Util.tableLength(dashSeperated) > 1 then
local from = tonumber(dashSeperated[1])
local till = tonumber(dashSeperated[2])
for i = from, till do
if Util.strContains(targetZone, "A") == true then
self._targetZoneIdPerStage[tostring(i)] = string.gsub(targetZone, "A", tostring(i))
else
self._targetZoneIdPerStage[tostring(i)] = targetZone
end
end
else
if Util.strContains(targetZone, "A") == true then
self._targetZoneIdPerStage[tostring(dashSeperated[1])] = string.gsub(targetZone, "A", tostring(dashSeperated[1]))
else
self._targetZoneIdPerStage[tostring(dashSeperated[1])] = targetZone
end
end
end
end
end
env.info("Capgroup parsed with table: " .. Util.toString(self._targetZoneIdPerStage))
else
MissionEditorWarner.Add("CAP Group with name: " .. groupName .. "should have at least 3 parts, but has " .. partCount)
end
end
return CapGroup
@@ -0,0 +1,363 @@
local AirGroup = require("classes.capClasses.airGroups.AirGroup")
local INTERCEPT = require("classes.capClasses.taskings.INTERCEPT")
local Util = require("classes.util.Util")
local DcsUtil = require("classes.util.DcsUtil")
local MissionEditorWarner = require("classes.util.MissionEditorWarnings")
---@class InterceptGroup : AirGroup
---@field private _targetNames Array<string>
---@field private _targetZoneName string
---@field private _detectionManager DetectionManager
---@field private _coalitionSide CoalitionSide
---@field private _lastKnownTargetAt number
---@field private _currentTargetName string?
---@field private _targetZoneIdPerStage table<string, string>
---@field private _config CapConfig
---@field private _airbase Airbase
---@field private _maxSpeed number?
---@field private _updateTaskID number|nil
local InterceptGroup = {}
InterceptGroup.__index = InterceptGroup
---@param groupName string
---@param config CapConfig
---@param logger Logger
---@param detectionManager DetectionManager
---@param spawnManager SpawnManager
---@return InterceptGroup?
function InterceptGroup.New(groupName, config, logger, detectionManager, spawnManager)
setmetatable(InterceptGroup, AirGroup)
local self = setmetatable({}, InterceptGroup)
AirGroup.New(self, groupName, "INTERCEPT", config, logger, spawnManager)
local group = Group.getByName(groupName)
if not group then
logger:error("InterceptGroup: Group " .. groupName .. " does not exist")
return nil
end
local unit = group:getUnit(1)
if unit then
local desc = unit:getDesc()
if desc["speedMax10K"] then
self._maxSpeed = desc["speedMax10K"] * 0.75
self._logger:debug("InterceptGroup: Max speed for group " .. groupName .. " is set to " .. self._maxSpeed .. " m/s")
end
end
self._coalitionSide = group:getCoalition()
self._detectionManager = detectionManager
self._config = config
self._targetNames = {}
self._targetZoneIdPerStage = {}
self:InitWithName(groupName)
return self
end
---comment
---@param units Array<string>
---@param homeAirbase Airbase
function InterceptGroup:SendToInterceptUnits(units, zoneName, homeAirbase)
self._airbase = homeAirbase
self._targetZoneName = zoneName
self:SetTargetUnits(units)
end
---@return string?
function InterceptGroup:GetZoneIDWhenStageID(stageID)
return self._targetZoneIdPerStage[stageID]
end
---@return string?
function InterceptGroup:GetCurrentTargetZone()
return self._targetZoneName
end
---comment
---@param unitNames Array<string>
function InterceptGroup:SetTargetUnits(unitNames)
self._targetNames = unitNames
self:UpdateTask()
if self._updateTaskID then
pcall(function()
timer.removeFunction(self._updateTaskID)
end)
end
local updateContinous = function(selfA, time)
local next = selfA:UpdateTask()
if next then
return time + next
else
return nil
end
end
self._updateTaskID = timer.scheduleFunction(updateContinous, self, timer.getTime() + 30)
end
function InterceptGroup:RemoveTargetUnit(unit)
if not unit then return end
local name = unit:getName()
for key, value in pairs(self._targetNames) do
if value == "name" then
table.remove(self._targetNames, key)
self._logger:debug("InterceptGroup: Removed target unit " .. name .. " from group " .. self._groupName)
break
end
end
end
---@private
---@return Unit?
---@return Vec3? groupPoint
function InterceptGroup:GetClosestTarget()
local group = Group.getByName(self._groupName)
if not group then return nil end
local groupPoint = nil
for _, unit in pairs(group:getUnits()) do
if unit and unit:isExist() then
groupPoint = unit:getPoint()
break
end
end
if not groupPoint then return nil end
local closestUnit = nil
local closestDistance = math.huge
for _, targetName in pairs(self._targetNames) do
local targetUnit = Unit.getByName(targetName)
if targetUnit and targetUnit:isExist() then
local pos = targetUnit:getPoint()
local distance = Util.VectorDistance3d(groupPoint, pos)
if distance < closestDistance then
closestDistance = distance
closestUnit = targetUnit
end
end
end
return closestUnit, groupPoint
end
---@return number? @interval or null if no retry required
function InterceptGroup:UpdateTask()
local group = Group.getByName(self._groupName)
if not group then return end
local closestUnit, groupPoint = self:GetClosestTarget()
if not closestUnit then return 15 end
if not groupPoint then return 15 end
local closesUnitVec = closestUnit:getPoint()
local alt = closesUnitVec.y
if alt < 1000 then
alt = 1000 -- Ensure minimum altitude for intercept
end
local selfDetected = false
for _, unit in pairs(group:getUnits()) do
if unit and unit:isExist() then
local controller = unit:getController()
if controller then
for _, detected in pairs(controller:getDetectedTargets(Controller.Detection.VISUAL, Controller.Detection.OPTIC, Controller.Detection.RADAR)) do
if detected and detected.object and detected.distance == true then
if detected.object:getName() == closestUnit:getName() then
selfDetected = true
break
end
end
end
end
end
end
if DcsUtil.IsBingoFuel(self._groupName) then
self._logger:debug("InterceptGroup: " .. self._groupName .. " is at bingo fuel, returning to base")
self:SendRTB(self._airbase)
return nil -- Return to base if bingo fuel
end
if selfDetected and self:IsInAir() == true then
if self._currentTargetName and self._currentTargetName == closestUnit:getName() then
local distance = Util.VectorDistance3d(closestUnit:getPoint(), groupPoint)
if distance < 30 * 1852 then
-- If the target is within 10 nautical miles, continue attacking
self._logger:debug("InterceptGroup: " .. self._groupName .. " continues attacking target " .. closestUnit:getName())
return 15 -- Continue attacking the detected target
end
end
self._logger:debug("InterceptGroup: " .. self._groupName .. " has detected target " .. closestUnit:getName() .. ", creating intercept mission")
local vec3 = closestUnit:getPoint()
local vec2 = { x = vec3.x, y = vec3.z }
local groupPointVec2 = { x = groupPoint.x, y = groupPoint.z }
local mission = INTERCEPT.getUnitInterceptMissionFromAir(self._groupName, groupPointVec2, vec2, closestUnit, self._airbase, self._config, self._maxSpeed, alt)
self:SetMission(mission)
self._currentTargetName = closestUnit:getName()
return 15 -- Just continue attacking the detected target
else
self._currentTargetName = nil
end
local speed = self._config:getMaxSpeed()
local interceptPoint = self:GetInterceptPoint(groupPoint, speed, closestUnit)
if interceptPoint == nil then
return 30 -- Return to base if no intercept point could be calculated
end
local mission = nil
if self:IsInAir() == true then
-- If the group is in the air, create an intercept mission
mission = INTERCEPT.getMissionFromInAir(
self._groupName,
{ x = groupPoint.x, y = groupPoint.z },
interceptPoint,
self._airbase,
self._config,
self._maxSpeed,
alt
)
else
mission = INTERCEPT.getMissionFromAirbase(
self._groupName,
interceptPoint,
self._airbase,
self._config,
self._maxSpeed,
alt
)
end
if mission then
self:SetMission(mission)
else
self._logger:warn("InterceptGroup: Could not create mission for group " .. self._groupName)
return 30 -- Return to base if mission could not be created
end
return 15
end
---@param originatingUnit Vec3
---@param speed number
---@param targetUnit Unit
---@return Vec2? @Returns the intercept point as a Vec2 or nil if no intercept point could be calculated
function InterceptGroup:GetInterceptPoint(originatingUnit, speed, targetUnit)
-- Calculate intercept point for a moving target
-- originatingUnit: Vec3 (our position)
-- speed: our speed (scalar, m/s)
-- targetUnit: Unit (target)
if not targetUnit or not targetUnit:isExist() then return nil end
local targetPos = targetUnit:getPoint()
local targetVel = targetUnit:getVelocity() -- Vec3
local relPos = {
x = targetPos.x - originatingUnit.x,
y = targetPos.y - originatingUnit.y,
z = targetPos.z - originatingUnit.z
}
local relVel = {
x = targetVel.x,
y = targetVel.y,
z = targetVel.z
}
local relPos2 = relPos.x^2 + relPos.y^2 + relPos.z^2
local relVel2 = relVel.x^2 + relVel.y^2 + relVel.z^2
local speed2 = speed^2
local dot = relPos.x * relVel.x + relPos.y * relVel.y + relPos.z * relVel.z
-- Quadratic formula: a*t^2 + b*t + c = 0
local a = relVel2 - speed2
local b = 2 * dot
local c = relPos2
local discriminant = b^2 - 4*a*c
if discriminant < 0 or a == 0 then
-- No solution, just head to current position
return { x = targetPos.x, y = targetPos.z }
end
local sqrtDisc = math.sqrt(discriminant)
local t1 = (-b + sqrtDisc) / (2*a)
local t2 = (-b - sqrtDisc) / (2*a)
local t = math.min(t1, t2)
if t < 0 then t = math.max(t1, t2) end
if t < 0 then
-- No valid intercept time, just head to current position
return { x = targetPos.x, y = targetPos.z }
end
-- Intercept point
local intercept = {
x = targetPos.x + targetVel.x * t,
y = targetPos.z + targetVel.z * t
}
return intercept
end
---@private
function InterceptGroup:InitWithName(groupName)
local split_string = Util.split_string(groupName, "_")
local partCount = Util.tableLength(split_string)
if partCount >= 3 then
local configPart = split_string[2]
configPart = string.sub(configPart, 2, #configPart)
local subsplit = Util.split_string(configPart, "|")
if subsplit then
for key, value in pairs(subsplit) do
local keySplit = Util.split_string(value, "]")
local targetZone = keySplit[2]
local allActives = string.sub(keySplit[1], 2, #keySplit[1])
local commaSeperated = Util.split_string(allActives, ",")
for _, value in pairs(commaSeperated) do
local dashSeperated = Util.split_string(value, "-")
if Util.tableLength(dashSeperated) > 1 then
local from = tonumber(dashSeperated[1])
local till = tonumber(dashSeperated[2])
for i = from, till do
if Util.strContains(targetZone, "A") == true then
self._targetZoneIdPerStage[tostring(i)] = string.gsub(targetZone, "A", tostring(i))
else
self._targetZoneIdPerStage[tostring(i)] = targetZone
end
end
else
if Util.strContains(targetZone, "A") == true then
self._targetZoneIdPerStage[tostring(dashSeperated[1])] = string.gsub(targetZone, "A", tostring(dashSeperated[1]))
else
self._targetZoneIdPerStage[tostring(dashSeperated[1])] = targetZone
end
end
end
end
end
env.info("interceptGroup parsed with table: " .. Util.toString(self._targetZoneIdPerStage))
else
MissionEditorWarner.Add("CAP Group with name: " .. groupName .. "should have at least 3 parts, but has " .. partCount)
end
end
return InterceptGroup
@@ -0,0 +1,122 @@
local AirGroup = require("classes.capClasses.airGroups.AirGroup")
local SWEEP = require("classes.capClasses.taskings.SWEEP")
local Util = require("classes.util.Util")
local MissionEditorWarner = require("classes.util.MissionEditorWarnings")
---@class SweepGroup : AirGroup
---@field _targetZoneIdPerStage table<number, string>
---@field _currentTargetZoneID string?
local SweepGroup = {}
SweepGroup.__index = SweepGroup
---@param groupName string
---@param config CapConfig
---@param logger Logger
---@param spawnManager SpawnManager
---@return SweepGroup
function SweepGroup.New(groupName, config, logger, spawnManager)
setmetatable(SweepGroup, AirGroup)
local self = setmetatable({}, SweepGroup) --[[@as SweepGroup]]
AirGroup.New(self, groupName, "SWEEP", config, logger, spawnManager)
self._targetZoneIdPerStage = {}
self:InitWithName(groupName)
return self
end
---@return string?
function SweepGroup:GetZoneIDWhenStageID(stageID)
return self._targetZoneIdPerStage[stageID]
end
---@return string?
function SweepGroup:GetCurrentTargetZoneID()
return self._currentTargetZoneID
end
---@class SetTaskParams
---@field task table
---@field self SweepGroup
---@param params SetTaskParams
local setMissionDelayedTask = function(params, time)
params.self:SetMissionPrivate(params.task)
end
function SweepGroup: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 mission = SWEEP.getAsMissionFromAirbase(self._groupName, airbase, zone, self._config)
if mission then
---@type SetTaskParams
local params = {
task = mission,
self = self
}
local delay = math.random(120, 600)
timer.scheduleFunction(setMissionDelayedTask, params, timer.getTime() + delay)
else
self._logger:error("SweepGroup:SendToZone - Mission could not be created for group: " .. self._groupName)
end
end
function SweepGroup:SendToZoneInternal()
end
---@private
function SweepGroup:InitWithName(groupName)
local split_string = Util.split_string(groupName, "_")
local partCount = Util.tableLength(split_string)
if partCount >= 3 then
local configPart = split_string[2]
configPart = string.sub(configPart, 2, #configPart)
local subsplit = Util.split_string(configPart, "|")
if subsplit then
for key, value in pairs(subsplit) do
local keySplit = Util.split_string(value, "]")
local targetZone = keySplit[2]
local allActives = string.sub(keySplit[1], 2, #keySplit[1])
local commaSeperated = Util.split_string(allActives, ",")
for _, value in pairs(commaSeperated) do
local dashSeperated = Util.split_string(value, "-")
if Util.tableLength(dashSeperated) > 1 then
local from = tonumber(dashSeperated[1])
local till = tonumber(dashSeperated[2])
for i = from, till do
if Util.strContains(targetZone, "A") == true then
self._targetZoneIdPerStage[tostring(i)] = string.gsub(targetZone, "A", tostring(i))
else
self._targetZoneIdPerStage[tostring(i)] = targetZone
end
end
else
if Util.strContains(targetZone, "A") == true then
self._targetZoneIdPerStage[tostring(dashSeperated[1])] = string.gsub(targetZone, "A",
tostring(dashSeperated[1]))
else
self._targetZoneIdPerStage[tostring(dashSeperated[1])] = targetZone
end
end
end
end
end
env.info("SweepGroup parsed with table: " .. Util.toString(self._targetZoneIdPerStage))
else
MissionEditorWarner.Add("SWEEP Group with name: " ..
groupName .. "should have at least 3 parts, but has " .. partCount)
end
end
return SweepGroup
@@ -0,0 +1,164 @@
---@class DetectionManager
---@field private _logger Logger
---@field private _detectedUnits table<string, table<string, number>>
---@field private _detectingUnits table<string, table<string, Unit>>
local DetectionManager = {}
DetectionManager.__index = DetectionManager
---@param logger Logger
function DetectionManager.New(logger)
local self = setmetatable({}, DetectionManager)
self._logger = logger
self._detectedUnits = {
[tostring(coalition.side.RED)] = {},
[tostring(coalition.side.BLUE)] = {}
}
self._detectingUnits = {
[tostring(coalition.side.RED)] = {},
[tostring(coalition.side.BLUE)] = {}
}
---@param selfA DetectionManager
---@param time number
local updateDetectingUnitsTask = function(selfA, time)
selfA:UpdateDetectingUnits()
return time + 120
end
timer.scheduleFunction(updateDetectingUnitsTask, self, timer.getTime() + 120)
---@param selfA DetectionManager
---@param time number
local updateDetected = function(selfA, time)
selfA:UpdateDetectedUnits()
return time + 10
end
timer.scheduleFunction(updateDetected, self, timer.getTime() + 130)
return self
end
---@param unitName string
---@param coalitionSide CoalitionSide
function DetectionManager:IsUnitDetectedBy(unitName, coalitionSide)
local coalitionString = tostring(coalitionSide)
if not self._detectedUnits[coalitionString] then
return false
end
if not self._detectedUnits[coalitionString][unitName] then
return false
end
return timer.getTime() - self._detectedUnits[coalitionString][unitName] < 20
end
---@return Array<string>
function DetectionManager:GetDetectedUnitsBy(coalitionSide)
local coalitionString = tostring(coalitionSide)
if not self._detectedUnits[coalitionString] then
return {}
end
local detectedUnits = {}
for unitName, _ in pairs(self._detectedUnits[coalitionString]) do
if self:IsUnitDetectedBy(unitName, coalitionSide) then
table.insert(detectedUnits, unitName)
end
end
return detectedUnits
end
function DetectionManager:UpdateDetectingUnits()
self:UpdateDetectingInCoalition(coalition.side.RED)
self:UpdateDetectingInCoalition(coalition.side.BLUE)
self._logger:debug("Updated detecting units")
end
function DetectionManager:UpdateDetectedUnits()
self:UpdateDetectedUnitsByCoalition(coalition.side.RED)
self:UpdateDetectedUnitsByCoalition(coalition.side.BLUE)
self._logger:debug("Updated detected units")
end
---@private
---@param unit Unit
---@return boolean
function DetectionManager:IsDetectingType(unit)
return unit:hasAttribute("EWR") or unit:hasAttribute("AWACS") or unit:hasAttribute("SAM SR")
end
---@private
---@param coalitionSide CoalitionSide
function DetectionManager:UpdateDetectingInCoalition(coalitionSide)
local coalitionString = tostring(coalitionSide)
local airGroups = coalition.getGroups(coalitionSide, Group.Category.AIRPLANE)
for _, group in ipairs(airGroups) do
if group and group:isExist() then
for _, unit in ipairs(group:getUnits()) do
if unit and self:IsDetectingType(unit) == true then
self._detectingUnits[coalitionString][unit:getName()] = unit
end
end
end
end
local groundGroups = coalition.getGroups(coalitionSide, Group.Category.GROUND)
for _, group in ipairs(groundGroups) do
if group and group:isExist() then
for _, unit in ipairs(group:getUnits()) do
if unit and self:IsDetectingType(unit) == true then
self._detectingUnits[coalitionString][unit:getName()] = unit
end
end
end
end
local ships = coalition.getGroups(coalitionSide, Group.Category.SHIP)
for _, group in ipairs(ships) do
if group and group:isExist() then
for _, unit in ipairs(group:getUnits()) do
if unit and self:IsDetectingType(unit) == true then
self._detectingUnits[coalitionString][unit:getName()] = unit
end
end
end
end
end
---@private
---@param coalitionSide CoalitionSide
function DetectionManager:UpdateDetectedUnitsByCoalition(coalitionSide)
local coalitionString = tostring(coalitionSide)
if not self._detectingUnits[coalitionString] then return end
if not self._detectedUnits[coalitionString] then
self._detectedUnits[coalitionString] = {}
end
for _, detectingUnit in pairs(self._detectingUnits[coalitionString]) do
if detectingUnit and detectingUnit:isExist() then
local controller = detectingUnit:getController()
if controller then
local targets = controller:getDetectedTargets(Controller.Detection.RADAR)
for _, target in pairs(targets) do
if target and target.object ~= nil and target.distance == true then
local targetUnit = target.object
local name = targetUnit:getName()
self._detectedUnits[coalitionString][name] = timer.getTime()
end
end
end
end
end
end
return DetectionManager
@@ -0,0 +1,122 @@
local SpearheadEvents = require("classes.spearhead_events")
local Util = require("classes.util.Util")
---@class RunwayBombingTracker : OnWeaponFiredListener
---@field trackedRunways table<Runway, RunwayStrikeMission>
---@field private _logger Logger
local RunwayBombingTracker = {}
RunwayBombingTracker.__index = RunwayBombingTracker
--- Constructor
--- @param logger Logger
--- @return RunwayBombingTracker
function RunwayBombingTracker.new(logger)
local self = setmetatable({}, RunwayBombingTracker)
self._logger = logger
SpearheadEvents.AddWeaponFiredListener(self)
return self
end
---comment
---@param weapon Weapon
function RunwayBombingTracker:OnWeaponFired(unit, weapon, target)
if weapon == nil then
return
end
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
local weaponTrackingArgs = {
weapon = weapon,
self = self
}
timer.scheduleFunction(RunwayBombingTracker.trackWeaponTask, weaponTrackingArgs, timer.getTime() + 1)
end
end
function RunwayBombingTracker:RegisterRunway(runway, strikeMission)
if not self.trackedRunways then
self.trackedRunways = {}
end
if not self.trackedRunways[runway] then
self.trackedRunways[runway] = strikeMission
end
end
---@class WeaponTrackingArgs
---@field weapon Weapon
---@field self RunwayBombingTracker
---@private
---@param weaponTrackingArgs WeaponTrackingArgs
function RunwayBombingTracker.trackWeaponTask(weaponTrackingArgs, time)
local weapon = weaponTrackingArgs.weapon
local self = weaponTrackingArgs.self
if not weapon or weapon:isExist() == false then return nil end
local pos = weapon:getPoint()
local velocity = weapon:getVelocity()
local ground = land.getHeight({ x = pos.x, y = pos.z })
local MpS = velocity.y -- increase the speed to make sure you don't miss it.
if MpS > 0 then
return time + 3
end
local nextInterval = (pos.y - ground) / math.abs(MpS)
if nextInterval < 1 then
-- Calculate the impact point of the weapon
---@type Vec2
local impactPoint = {
x = pos.x + velocity.x * nextInterval,
y = pos.z + velocity.z * nextInterval
}
self:OnWeaponImpact(weapon:getDesc(), impactPoint)
return nil
end
if nextInterval > 5 then
nextInterval = 5
end
return time + (nextInterval /2)
end
---comment
---@param weaponDesc table
---@param impactPoint Vec2
function RunwayBombingTracker:OnWeaponImpact(weaponDesc, impactPoint)
self._logger:debug("RunwayBombingTracker:OnWeaponImpact")
local warhead = weaponDesc.warhead
local explosiveMass = warhead.explosiveMass or warhead.shapedExplosiveMass
for runway, strikeMission in pairs(self.trackedRunways) do
local zone= strikeMission:GetRunwayZone()
if Util.is3dPointInZone({ x = impactPoint.x, z = impactPoint.y, y = 0 }, zone) then
strikeMission:RunwayHit(impactPoint, explosiveMass)
end
end
end
return RunwayBombingTracker
+326
View File
@@ -0,0 +1,326 @@
local Util = require("classes.util.Util")
local RTB = require("classes.capClasses.taskings.RTB")
---@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 = 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 = Util.vectorHeadingFromTo(pointA, pointB)
if Util.VectorDistance2d(baseVec2, pointB) > Util.VectorDistance2d(baseVec2, pointA) then
furthest = pointB
closest = pointA
heading = 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 = Util.vectorHeadingFromTo(airbaseVec2, capZone.location)
local point = 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] = RTB.getApproachPoint(airbase, capZone.location, capConfig),
[5] = RTB.getInitialPoint(airbase),
[6] = 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] = RTB.getApproachPoint(airbase, capZone.location, capConfig),
[3] = RTB.getInitialPoint(airbase),
[4] = 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
return CAP
@@ -0,0 +1,396 @@
local Util = require("classes.util.Util")
local RTB = require("classes.capClasses.taskings.RTB")
---@class InterceptTasking
local INTERCEPT = {}
---@param groupName string
---@param interceptPoint Vec2
---@param airbase Airbase
---@param speed number?
---@param alt number?
---@param config CapConfig
function INTERCEPT.getMissionFromAirbase(groupName, interceptPoint, airbase, config, speed, alt)
local airbaseVec3 = airbase:getPoint()
local airbaseVec2 = { x = airbaseVec3.x, y = airbaseVec3.z }
local heading = Util.vectorHeadingFromTo(airbaseVec2, interceptPoint)
env.info("BLAAT: heading from runway to first point: " .. heading)
local point = Util.vectorMove(airbaseVec2, heading, 3*1852)
local pointA, pointB, pointC, pointD = INTERCEPT.getInterceptTaskPoint(groupName, point, interceptPoint, airbaseVec2, config, speed, alt)
local points = {
[1] = pointA,
[2] = pointA,
[3] = pointB,
[4] = pointC,
[5] = pointD,
[6] = RTB.getApproachPoint(airbase, interceptPoint, config),
[7] = RTB.getInitialPoint(airbase),
[8] = RTB.getLandingPoint(airbase)
}
local mission = {
id = 'Mission',
params = {
airborne = true,
route = {
points = points
}
}
}
return mission
end
---comment
---@param groupName string
---@param currentPosition Vec2
---@param interceptPoint Vec2
---@param airbase Airbase
---@param speed number?
---@param alt number?
---@param config CapConfig
---@return table
function INTERCEPT.getMissionFromInAir(groupName, currentPosition, interceptPoint, airbase, config, speed, alt)
local pointAirbasev3 = airbase:getPoint()
local airbase2 = { x = pointAirbasev3.x, y = pointAirbasev3.z }
local pointA, pointB, pointC, pointD = INTERCEPT.getInterceptTaskPoint(groupName, currentPosition, interceptPoint, airbase2, config, speed, alt)
local points = {
[1] = pointA,
[2] = pointA,
[3] = pointB,
[4] = pointC,
[5] = pointD,
[6] = RTB.getApproachPoint(airbase, interceptPoint, config),
[7] = RTB.getInitialPoint(airbase),
[8] = RTB.getLandingPoint(airbase)
}
local mission = {
id = 'Mission',
params = {
airborne = true,
route = {
points = points
}
}
}
return mission
end
---comment
---@param groupName string
---@param currentPoint Vec2
---@param targetPosition Vec2
---@param targetUnit Unit
---@param airbase any
---@param config any
---@param speed number?
---@param alt number?
---@return table
function INTERCEPT.getUnitInterceptMissionFromAir(groupName, currentPoint, targetPosition, targetUnit, airbase, config, speed, alt)
local pointAirbasev3 = airbase:getPoint()
local airbase2 = { x = pointAirbasev3.x, y = pointAirbasev3.z }
local pointA, pointB, pointC, pointD = INTERCEPT.getUnitInterceptTaskPoint(groupName, currentPoint, targetPosition, targetUnit, airbase2, config, speed, alt)
local points = {
[1] = pointA,
[2] = pointA,
[3] = pointB,
[4] = pointC,
[5] = pointD,
[6] = RTB.getApproachPoint(airbase, currentPoint, config),
[7] = RTB.getInitialPoint(airbase),
[8] = RTB.getLandingPoint(airbase)
}
local mission = {
id = 'Mission',
params = {
airborne = true,
route = {
points = points
}
}
}
return mission
end
---@private
---@param groupName string
---@param currentPoint Vec2
---@param targetPoint Vec2
---@param airbasePoint Vec2
---@param config CapConfig
---@param speed number?
---@param alt number?
---@return table pointA @Starts the task right away
---@return table pointB @The actual target point to fly to and search, but searching started at pointA
---@return table pointC A fly over point halfway between the target and the airbase, if nothing it found then the unit will fly it's route till here
---@return table pointD THe point after Point C where the unit will execute the RTB command
function INTERCEPT.getInterceptTaskPoint(groupName, currentPoint, targetPoint, airbasePoint, config, speed, alt)
local pointA = {
alt = alt or config:getMinAlt(),
action = "Turning Point",
alt_type = "BARO",
speed = speed or config:getMaxSpeed(),
ETA = 0,
ETA_locked = false,
x = currentPoint.x,
y = currentPoint.y,
speed_locked = true,
formation_template = "",
task = {
id = "ComboTask",
params = {
tasks = {
id = 'EngageTargetsInZone',
params = {
point = targetPoint,
zoneRadius = 10 * 1852, -- 10 NM, point will be updated, so target should be in this zone.
targetTypes = {
[1] = "Planes",
},
priority = 0
}
}
}
}
}
local pointB = {
alt = alt or config:getMinAlt(),
action = "Turning Point",
alt_type = "BARO",
speed = speed or config:getMaxSpeed(),
ETA = 0,
ETA_locked = false,
x = targetPoint.x,
y = targetPoint.y,
speed_locked = true,
formation_template = "",
task = {
id = "ComboTask",
params = {
tasks = {
id = 'EngageTargetsInZone',
params = {
point = targetPoint,
zoneRadius = 10 * 1852, -- 10 NM, point will be updated, so target should be in this zone.
targetTypes = {
[1] = "Planes",
},
priority = 0
}
}
}
}
}
local midPoint = {
x = (targetPoint.x + airbasePoint.x) / 2,
y = (targetPoint.y + airbasePoint.y) / 2
}
local pointC = {
alt = config:getMinAlt(),
action = "Fly Over Point",
alt_type = "BARO",
speed = config:getMaxSpeed(),
ETA = 0,
ETA_locked = false,
x = midPoint.x,
y = midPoint.y,
speed_locked = true,
formation_template = "",
task = {
id = "ComboTask",
params = {}
}
}
local pointD = {
alt = config:getMinAlt(),
action = "Turning Point",
alt_type = "BARO",
speed = config:getMaxSpeed(),
ETA = 0,
ETA_locked = false,
x = midPoint.x,
y = midPoint.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.PublishRTB, \"" .. groupName .. "\")"
}
}
}
}
}
}
}
}
return pointA, pointB, pointC, pointD
end
---@private
---@param groupName string
---@param currentPoint Vec2
---@param targetPosition Vec2
---@param targetUnit Unit
---@param airbasePoint Vec2
---@param config CapConfig
---@param speed number?
---@param alt number?
---@return table pointA @Starts the task right away
---@return table pointB @The actual target point to fly to and search, but searching started at pointA
---@return table pointC A fly over point halfway between the target and the airbase, if nothing it found then the unit will fly it's route till here
---@return table pointD THe point after Point C where the unit will execute the RTB command
function INTERCEPT.getUnitInterceptTaskPoint(groupName, currentPoint, targetPosition, targetUnit, airbasePoint, config, speed, alt)
local pointA = {
alt = alt or config:getMinAlt(),
action = "Turning Point",
alt_type = "BARO",
speed = speed or config:getMaxSpeed(),
ETA = 0,
ETA_locked = false,
x = currentPoint.x,
y = currentPoint.y,
speed_locked = true,
formation_template = "",
task = {
id = "ComboTask",
params = {
tasks = {
[1] = {
id = 'EngageUnit',
params = {
unitId = targetUnit:getID(),
groupAttack = true,
priority = 0
}
}
}
}
}
}
local pointB = {
alt = alt or config:getMinAlt(),
action = "Turning Point",
alt_type = "BARO",
speed = speed or config:getMaxSpeed(),
ETA = 0,
ETA_locked = false,
x = targetPosition.x,
y = targetPosition.y,
speed_locked = true,
formation_template = "",
task = {
id = "ComboTask",
params = {
tasks = {
[1] = {
id = 'EngageUnit',
params = {
unitId = targetUnit:getID(),
groupAttack = true,
priority = 0
}
}
}
}
}
}
local midPoint = {
x = (targetPosition.x + airbasePoint.x) / 2,
y = (targetPosition.y + airbasePoint.y) / 2
}
local pointC = {
alt = config:getMinAlt(),
action = "Fly Over Point",
alt_type = "BARO",
speed = config:getMaxSpeed(),
ETA = 0,
ETA_locked = false,
x = midPoint.x,
y = midPoint.y,
speed_locked = true,
formation_template = "",
task = {
id = "ComboTask",
params = {}
}
}
local pointD = {
alt = config:getMinAlt(),
action = "Turning Point",
alt_type = "BARO",
speed = config:getMaxSpeed(),
ETA = 0,
ETA_locked = false,
x = midPoint.x,
y = midPoint.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.PublishRTB, \"" .. groupName .. "\")"
}
}
}
}
}
}
}
}
return pointA, pointB, pointC, pointD
end
return INTERCEPT
+198
View File
@@ -0,0 +1,198 @@
local Util = require("classes.util.Util")
---@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 = 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 = 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 = 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 = 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 = Util.vectorMove(initialPoint, headingFromRunway - 45, 9000)
local pointB = Util.vectorMove(initialPoint, headingFromRunway + 45, 9000)
if Util.VectorDistance2d(missionPoint, pointA) > 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
return RTB
+286
View File
@@ -0,0 +1,286 @@
local Util = require("classes.util.Util")
local RTB = require("classes.capClasses.taskings.RTB")
---@class SWEEPTasking
local SWEEP = {}
---@param attackHelos boolean
---@return table
local function GetCAPTargetTypes(attackHelos)
local targetTypes = {
[1] = "Planes",
}
if attackHelos then
targetTypes[2] = "Helicopters"
end
return targetTypes
end
---@class SWEEPTaskingOptions
---@field furthest Vec2
---@field closest Vec2
---@param capZone SpearheadTriggerZone
---@param airBase Airbase
---@return SWEEPTaskingOptions
local function GetCAPPointFromTriggerZone(airBase, capZone)
local furthestA = nil
local furthestB = nil
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 = 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 = Util.vectorHeadingFromTo(pointA, pointB)
if Util.VectorDistance2d(baseVec2, pointB) > Util.VectorDistance2d(baseVec2, pointA) then
furthest = pointB
closest = pointA
heading = 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 = Util.vectorHeadingFromTo(airbaseVec2, capZone.location)
local point = 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(),
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 SWEEP.getAsMissionFromAirbase(groupName, airbase, capZone, capConfig)
local pointA, pointB, pointC = SWEEP.getAsTasking(groupName, airbase, capZone, capConfig)
local points = {
[1] = GetOutboundTask(airbase, capZone, capConfig),
[2] = GetOutboundTask(airbase, capZone, capConfig),
[3] = pointA,
[4] = pointB,
[5] = pointC,
[6] = RTB.getApproachPoint(airbase, capZone.location, capConfig),
[7] = RTB.getInitialPoint(airbase),
[8] = 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
---@return table TurningPointA
---@return table TurningPointB
---@return table TurningPointC
function SWEEP.getAsTasking(groupName, airbase, capZone, capConfig)
local capTaskingOptions = GetCAPPointFromTriggerZone(airbase, capZone)
local alt = math.random(capConfig:getMinAlt(), capConfig:getMaxAlt())
local speed = capConfig:getMaxSpeed()
local pointA = {
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
}
}
}
}
}
}
local pointB = {
alt = alt,
action = "Turning Point",
alt_type = "BARO",
speed = speed,
ETA = 0,
ETA_locked = false,
x = capTaskingOptions.furthest.x,
y = capTaskingOptions.furthest.y,
speed_locked = true,
formation_template = "",
task = {
id = "ComboTask",
params = {
tasks = {
[1] = {
id = 'EngageTargets',
params = {
maxDist = capConfig:getMaxDeviationRange(),
maxDistEnabled = capConfig:getMaxDeviationRange() >= 0, -- required to check maxDist
targetTypes = GetCAPTargetTypes(false),
priority = 0
}
}
}
}
}
}
local pointC = {
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] = {
id = 'EngageTargets',
params = {
maxDist = capConfig:getMaxDeviationRange(),
maxDistEnabled = capConfig:getMaxDeviationRange() >= 0, -- required to check maxDist
targetTypes = GetCAPTargetTypes(false),
priority = 0
}
},
[2] = {
number = 2,
auto = false,
id = "WrappedAction",
enabled = "true",
params = {
action = {
id = "Script",
params = {
command = "pcall(Spearhead.Events.PublishRTB, \"" .. groupName .. "\")"
}
}
}
}
}
}
}
}
return pointA, pointB, pointC
end
return SWEEP