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:
committed by
GitHub
co-authored by
ex61wi
dutchie031 <dutchie031>
parent
c027921527
commit
128aed1d2b
@@ -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
|
||||
Reference in New Issue
Block a user