mirror of
https://github.com/iTracerFacer/DCS_MissionDev.git
synced 2025-12-03 04:14:46 +00:00
Working automated TADC Cargo Dispatch System. When bases become low, they will request resources and cargo planes will be dispatched randoml from a list of bases.
This commit is contained in:
parent
9bc2fc0662
commit
380527cf8a
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -176,7 +176,7 @@ end
|
||||
-- Create a NAVYGROUP object and activate the late activated group.
|
||||
BlueCVNGroup=NAVYGROUP:New("CVN-72 Abraham Lincoln")
|
||||
BlueCVNGroup:SetVerbosity(1)
|
||||
BlueCVNGroup:MarkWaypoints()
|
||||
-- BlueCVNGroup:MarkWaypoints() -- Commented out due to MOOSE bug causing event handler errors
|
||||
|
||||
-- Initialize patrol zones based on ship's actual waypoints
|
||||
SCHEDULER:New(nil, function()
|
||||
|
||||
806
DCS_Kola/Operation_Polar_Shield/Moose_TADC_CargoDispatcher.lua
Normal file
806
DCS_Kola/Operation_Polar_Shield/Moose_TADC_CargoDispatcher.lua
Normal file
@ -0,0 +1,806 @@
|
||||
--[[
|
||||
═══════════════════════════════════════════════════════════════════════════════
|
||||
Moose_TDAC_CargoDispatcher.lua
|
||||
Automated Logistics System for TADC Squadron Replenishment
|
||||
═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
DESCRIPTION:
|
||||
This script monitors RED and BLUE squadrons for low aircraft counts and automatically dispatches CARGO aircraft from a list of supply airfields to replenish them. It tracks each supply mission, announces key stages to players, and prevents duplicate or spam missions. The system integrates with TADC's existing cargo landing logic for replenishment.
|
||||
|
||||
CONFIGURATION:
|
||||
- Update static templates and airfield lists as needed for your mission.
|
||||
- Set thresholds and supply airfields in CARGO_SUPPLY_CONFIG.
|
||||
- Replace static templates with actual group templates from the mission editor for realism.
|
||||
|
||||
REQUIRES:
|
||||
- MOOSE framework (for SPAWN, AIRBASE, etc.)
|
||||
- Optional: MIST for deep copy of templates
|
||||
|
||||
═══════════════════════════════════════════════════════════════════════════════
|
||||
]]
|
||||
|
||||
--[[
|
||||
GLOBAL STATE AND CONFIGURATION
|
||||
--------------------------------------------------------------------------
|
||||
Tracks all active cargo missions and dispatcher configuration.
|
||||
]]
|
||||
if not cargoMissions then
|
||||
cargoMissions = { red = {}, blue = {} }
|
||||
end
|
||||
|
||||
-- Dispatcher config (interval in seconds)
|
||||
if not DISPATCHER_CONFIG then
|
||||
-- default interval (seconds) and a slightly larger grace period to account for slow servers/networks
|
||||
DISPATCHER_CONFIG = { interval = 60, gracePeriod = 25 }
|
||||
end
|
||||
|
||||
-- Safety flag: when false, do NOT fall back to spawning from in-memory template tables.
|
||||
-- Set to true if you understand the tweaked-template warning and accept the risk.
|
||||
if DISPATCHER_CONFIG.ALLOW_FALLBACK_TO_INMEM_TEMPLATE == nil then
|
||||
DISPATCHER_CONFIG.ALLOW_FALLBACK_TO_INMEM_TEMPLATE = false
|
||||
end
|
||||
|
||||
--[[
|
||||
UTILITY STUBS
|
||||
--------------------------------------------------------------------------
|
||||
selectRandomAirfield: Picks a random airfield from a list.
|
||||
announceToCoalition: Stub for in-game coalition messaging.
|
||||
Replace with your own logic as needed.
|
||||
]]
|
||||
if not selectRandomAirfield then
|
||||
function selectRandomAirfield(airfieldList)
|
||||
if type(airfieldList) == "table" and #airfieldList > 0 then
|
||||
return airfieldList[math.random(1, #airfieldList)]
|
||||
end
|
||||
return nil
|
||||
end
|
||||
end
|
||||
|
||||
-- Stub for announceToCoalition (replace with your own logic if needed)
|
||||
if not announceToCoalition then
|
||||
function announceToCoalition(coalitionKey, message)
|
||||
-- Replace with actual in-game message logic
|
||||
env.info("[ANNOUNCE] [" .. tostring(coalitionKey) .. "]: " .. tostring(message))
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
--[[
|
||||
LOGGING
|
||||
--------------------------------------------------------------------------
|
||||
Advanced logging configuration and helper function for debug output.
|
||||
]]
|
||||
local ADVANCED_LOGGING = {
|
||||
enableDetailedLogging = true,
|
||||
logPrefix = "[TDAC Cargo]"
|
||||
}
|
||||
|
||||
-- Provide a safe deepCopy if MIST is not available
|
||||
local function deepCopy(obj)
|
||||
if type(obj) ~= 'table' then return obj end
|
||||
local res = {}
|
||||
for k, v in pairs(obj) do
|
||||
if type(v) == 'table' then
|
||||
res[k] = deepCopy(v)
|
||||
else
|
||||
res[k] = v
|
||||
end
|
||||
end
|
||||
return res
|
||||
end
|
||||
|
||||
-- Dispatch cooldown per airbase (seconds) to avoid repeated immediate retries
|
||||
local CARGO_DISPATCH_COOLDOWN = DISPATCHER_CONFIG and DISPATCHER_CONFIG.cooldown or 300 -- default 5 minutes
|
||||
local lastDispatchAttempt = { red = {}, blue = {} }
|
||||
|
||||
local function getCoalitionSide(coalitionKey)
|
||||
if coalitionKey == 'blue' then return coalition.side.BLUE end
|
||||
if coalitionKey == 'red' then return coalition.side.RED end
|
||||
return nil
|
||||
end
|
||||
|
||||
-- Logging function (mimics Moose_TADC_Load2nd.lua)
|
||||
local function log(message, detailed)
|
||||
if not detailed or ADVANCED_LOGGING.enableDetailedLogging then
|
||||
env.info(ADVANCED_LOGGING.logPrefix .. " " .. message)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
log("═══════════════════════════════════════════════════════════════════════════════", true)
|
||||
log("Moose_TDAC_CargoDispatcher.lua loaded.", true)
|
||||
log("═══════════════════════════════════════════════════════════════════════════════", true)
|
||||
|
||||
--[[
|
||||
CARGO SUPPLY CONFIGURATION
|
||||
--------------------------------------------------------------------------
|
||||
Set supply airfields, cargo template names, and resupply thresholds for each coalition.
|
||||
]]
|
||||
local CARGO_SUPPLY_CONFIG = {
|
||||
red = {
|
||||
supplyAirfields = { "Kuusamo", "Kalevala", "Vuojarvi", "Kalevala", "Poduzhemye", "Kirkenes" }, -- replace with your RED supply airbase names
|
||||
cargoTemplate = "CARGO_RED_AN26", -- replace with your RED cargo aircraft template name
|
||||
threshold = 0.90 -- ratio below which to trigger resupply (testing)
|
||||
},
|
||||
blue = {
|
||||
supplyAirfields = { "Banak", "Kittila", "Alta", "Sodankyla", "Vuojarvi", "Enontekio" }, -- replace with your BLUE supply airbase names
|
||||
cargoTemplate = "CARGO_BLUE_C130", -- replace with your BLUE cargo aircraft template name
|
||||
threshold = 0.90 -- ratio below which to trigger resupply (testing)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
--[[
|
||||
getSquadronStatus(squadron, coalitionKey)
|
||||
--------------------------------------------------------------------------
|
||||
Returns the current, max, and ratio of aircraft for a squadron.
|
||||
If you track current aircraft in a table, update this logic accordingly.
|
||||
Returns: currentCount, maxCount, ratio
|
||||
]]
|
||||
local function getSquadronStatus(squadron, coalitionKey)
|
||||
local current = squadron.current or squadron.count or squadron.aircraft or 0
|
||||
local max = squadron.max or squadron.aircraft or 1
|
||||
if squadron.templateName and _G.squadronAircraftCounts and _G.squadronAircraftCounts[coalitionKey] then
|
||||
current = _G.squadronAircraftCounts[coalitionKey][squadron.templateName] or current
|
||||
end
|
||||
local ratio = (max > 0) and (current / max) or 0
|
||||
return current, max, ratio
|
||||
end
|
||||
|
||||
|
||||
|
||||
--[[
|
||||
hasActiveCargoMission(coalitionKey, airbaseName)
|
||||
--------------------------------------------------------------------------
|
||||
Returns true if there is an active (not completed/failed) cargo mission for the given airbase.
|
||||
]]
|
||||
local function hasActiveCargoMission(coalitionKey, airbaseName)
|
||||
for _, mission in pairs(cargoMissions[coalitionKey]) do
|
||||
if mission.destination == airbaseName then
|
||||
-- Ignore completed or failed missions
|
||||
if mission.status == "completed" or mission.status == "failed" then
|
||||
-- not active
|
||||
else
|
||||
-- Consider mission active only if the group is alive OR we're still within the grace window
|
||||
local stillActive = false
|
||||
if mission.group and mission.group.IsAlive and mission.group:IsAlive() then
|
||||
stillActive = true
|
||||
else
|
||||
local pending = mission._pendingStartTime
|
||||
local grace = mission._gracePeriod or DISPATCHER_CONFIG.gracePeriod or 8
|
||||
if pending and (timer.getTime() - pending) <= grace then
|
||||
stillActive = true
|
||||
end
|
||||
end
|
||||
if stillActive then
|
||||
log("Active cargo mission found for " .. airbaseName .. " (" .. coalitionKey .. ")")
|
||||
return true
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
log("No active cargo mission for " .. airbaseName .. " (" .. coalitionKey .. ")")
|
||||
return false
|
||||
end
|
||||
|
||||
--[[
|
||||
trackCargoMission(coalitionKey, mission)
|
||||
--------------------------------------------------------------------------
|
||||
Adds a new cargo mission to the tracking table and logs it.
|
||||
]]
|
||||
local function trackCargoMission(coalitionKey, mission)
|
||||
table.insert(cargoMissions[coalitionKey], mission)
|
||||
log("Tracking new cargo mission: " .. (mission.group and mission.group:GetName() or "nil group") .. " from " .. mission.origin .. " to " .. mission.destination)
|
||||
end
|
||||
|
||||
--[[
|
||||
cleanupCargoMissions()
|
||||
--------------------------------------------------------------------------
|
||||
Removes completed or failed cargo missions from the tracking table if their group is no longer alive.
|
||||
]]
|
||||
local function cleanupCargoMissions()
|
||||
for _, coalitionKey in ipairs({"red", "blue"}) do
|
||||
for i = #cargoMissions[coalitionKey], 1, -1 do
|
||||
local m = cargoMissions[coalitionKey][i]
|
||||
if m.status == "failed" then
|
||||
if not (m.group and m.group:IsAlive()) then
|
||||
log("Cleaning up failed cargo mission: " .. (m.group and m.group:GetName() or "nil group") .. " status: failed")
|
||||
table.remove(cargoMissions[coalitionKey], i)
|
||||
end
|
||||
elseif m.status == "completed" then
|
||||
if not (m.group and m.group:IsAlive()) then
|
||||
log("Cleaning up completed cargo mission: " .. (m.group and m.group:GetName() or "nil group") .. " status: completed")
|
||||
table.remove(cargoMissions[coalitionKey], i)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--[[
|
||||
dispatchCargo(squadron, coalitionKey)
|
||||
--------------------------------------------------------------------------
|
||||
Spawns a cargo aircraft from a supply airfield to the destination squadron airbase.
|
||||
Uses static templates for each coalition, assigns a unique group name, and sets a custom route.
|
||||
Tracks the mission and schedules route assignment with a delay to ensure group is alive.
|
||||
]]
|
||||
local function dispatchCargo(squadron, coalitionKey)
|
||||
local config = CARGO_SUPPLY_CONFIG[coalitionKey]
|
||||
local origin = selectRandomAirfield(config.supplyAirfields)
|
||||
-- enforce cooldown per destination to avoid immediate retries
|
||||
lastDispatchAttempt[coalitionKey] = lastDispatchAttempt[coalitionKey] or {}
|
||||
local last = lastDispatchAttempt[coalitionKey][squadron.airbaseName]
|
||||
if last and (timer.getTime() - last) < CARGO_DISPATCH_COOLDOWN then
|
||||
log("Skipping dispatch to " .. squadron.airbaseName .. " (cooldown active)")
|
||||
return
|
||||
end
|
||||
if not origin then
|
||||
log("No origin airfield found for cargo dispatch.")
|
||||
return
|
||||
end
|
||||
local destination = squadron.airbaseName
|
||||
-- Safety: check if destination has suitable parking for larger transports. If not, warn in log.
|
||||
local okParking = true
|
||||
-- Only check for likely large transports (C-130 / An-26 are large-ish) — keep conservative
|
||||
if cargoTemplate and (string.find(cargoTemplate:upper(), "C130") or string.find(cargoTemplate:upper(), "C-17") or string.find(cargoTemplate:upper(), "C17") or string.find(cargoTemplate:upper(), "AN26") ) then
|
||||
okParking = destinationHasSuitableParking(destination)
|
||||
if not okParking then
|
||||
log("WARNING: Destination '" .. tostring(destination) .. "' may not have suitable parking for " .. tostring(cargoTemplate) .. ". This can cause immediate despawn on landing.")
|
||||
end
|
||||
end
|
||||
local cargoTemplate = config.cargoTemplate
|
||||
local groupName = cargoTemplate .. "_to_" .. destination .. "_" .. math.random(1000,9999)
|
||||
|
||||
log("Dispatching cargo: " .. groupName .. " from " .. origin .. " to " .. destination)
|
||||
|
||||
-- Spawn cargo aircraft at origin using the template name ONLY for SPAWN
|
||||
-- Note: cargoTemplate is a config string; script uses in-file Lua template tables (CARGO_AIRCRAFT_TEMPLATE_*)
|
||||
log("DEBUG: Attempting spawn for group: '" .. groupName .. "' at airbase: '" .. origin .. "' (using in-file Lua template)")
|
||||
local airbaseObj = AIRBASE:FindByName(origin)
|
||||
if not airbaseObj then
|
||||
log("ERROR: AIRBASE:FindByName failed for '" .. tostring(origin) .. "'. Airbase object is nil!")
|
||||
else
|
||||
log("DEBUG: AIRBASE object found for '" .. origin .. "'. Proceeding with spawn.")
|
||||
end
|
||||
-- Select the correct template based on coalition
|
||||
local templateBase, uniqueGroupName
|
||||
if coalitionKey == "blue" then
|
||||
templateBase = CARGO_AIRCRAFT_TEMPLATE_BLUE
|
||||
uniqueGroupName = "CARGO_C130_DYNAMIC_" .. math.random(1000,9999)
|
||||
else
|
||||
templateBase = CARGO_AIRCRAFT_TEMPLATE_RED
|
||||
uniqueGroupName = "CARGO_AN26_DYNAMIC_" .. math.random(1000,9999)
|
||||
end
|
||||
-- Clone the template and set the group/unit name
|
||||
-- Prepare a mission placeholder. We'll set the group and spawnPos after successful spawn.
|
||||
local mission = {
|
||||
group = nil,
|
||||
origin = origin,
|
||||
destination = destination,
|
||||
squadron = squadron,
|
||||
status = "pending",
|
||||
-- Anchor a pending start time now to avoid the monitor loop expiring a mission
|
||||
-- before MOOSE has a chance to finalize the OnSpawnGroup callback.
|
||||
_pendingStartTime = timer.getTime(),
|
||||
_spawnPos = nil,
|
||||
_gracePeriod = DISPATCHER_CONFIG.gracePeriod or 8
|
||||
}
|
||||
|
||||
-- Helper to finalize mission after successful spawn
|
||||
local function finalizeMissionAfterSpawn(spawnedGroup, spawnPos)
|
||||
mission.group = spawnedGroup
|
||||
mission._spawnPos = spawnPos
|
||||
trackCargoMission(coalitionKey, mission)
|
||||
lastDispatchAttempt[coalitionKey][squadron.airbaseName] = timer.getTime()
|
||||
end
|
||||
|
||||
-- MOOSE-only spawn-by-name flow
|
||||
if type(cargoTemplate) ~= 'string' or cargoTemplate == '' then
|
||||
log("ERROR: cargoTemplate for coalition '" .. tostring(coalitionKey) .. "' must be a valid mission template name string. Aborting dispatch.")
|
||||
announceToCoalition(coalitionKey, "Resupply mission to " .. destination .. " aborted (invalid cargo template)!")
|
||||
return
|
||||
end
|
||||
|
||||
-- Use a per-dispatch RAT object to spawn and route cargo aircraft.
|
||||
-- Create a unique alias to avoid naming collisions and let RAT handle routing/landing.
|
||||
local alias = cargoTemplate .. "_TO_" .. destination .. "_" .. tostring(math.random(1000,9999))
|
||||
log("DEBUG: Attempting RAT spawn for template: '" .. cargoTemplate .. "' alias: '" .. alias .. "'")
|
||||
|
||||
local okNew, rat = pcall(function() return RAT:New(cargoTemplate, alias) end)
|
||||
if not okNew or not rat then
|
||||
log("ERROR: RAT:New failed for template '" .. tostring(cargoTemplate) .. "'. Error: " .. tostring(rat))
|
||||
if debug and debug.traceback then
|
||||
log("TRACEBACK: " .. tostring(debug.traceback(rat)))
|
||||
end
|
||||
announceToCoalition(coalitionKey, "Resupply mission to " .. destination .. " failed (spawn init error)!")
|
||||
return
|
||||
end
|
||||
|
||||
-- Configure RAT for a single, non-respawning dispatch
|
||||
rat:SetDeparture(origin)
|
||||
rat:SetDestination(destination)
|
||||
rat:NoRespawn()
|
||||
rat:SetSpawnLimit(1)
|
||||
rat:SetSpawnDelay(1)
|
||||
-- Ensure RAT takes off immediately from the runway (hot start) instead of staying parked
|
||||
if rat.SetTakeoffHot then rat:SetTakeoffHot() end
|
||||
-- Ensure RAT will look for parking and not despawn the group immediately on landing.
|
||||
-- This makes the group taxi to parking and come to a stop so other scripts (e.g. Load2nd)
|
||||
-- that detect parked/stopped cargo aircraft can register the delivery.
|
||||
if rat.SetParkingScanRadius then rat:SetParkingScanRadius(80) end
|
||||
if rat.SetParkingSpotSafeON then rat:SetParkingSpotSafeON() end
|
||||
if rat.SetDespawnAirOFF then rat:SetDespawnAirOFF() end
|
||||
-- Check on runway to ensure proper landing behavior (distance in meters)
|
||||
if rat.CheckOnRunway then rat:CheckOnRunway(true, 75) end
|
||||
|
||||
rat:OnSpawnGroup(function(spawnedGroup)
|
||||
-- Mark the canonical start time when MOOSE reports the group exists
|
||||
mission._pendingStartTime = timer.getTime()
|
||||
|
||||
local spawnPos = nil
|
||||
local dcsGroup = spawnedGroup:GetDCSObject()
|
||||
if dcsGroup then
|
||||
local units = dcsGroup:getUnits()
|
||||
if units and #units > 0 then
|
||||
spawnPos = units[1]:getPoint()
|
||||
end
|
||||
end
|
||||
|
||||
log("RAT spawned cargo aircraft group: " .. tostring(spawnedGroup:GetName()))
|
||||
|
||||
-- Temporary debug: log group state every 5s for 60s to trace landing/parking behavior
|
||||
local debugChecks = 12
|
||||
local checkInterval = 5
|
||||
local function debugLogState(iter)
|
||||
if iter > debugChecks then return end
|
||||
local ok, err = pcall(function()
|
||||
local name = spawnedGroup:GetName()
|
||||
local dcs = spawnedGroup:GetDCSObject()
|
||||
if dcs then
|
||||
local units = dcs:getUnits()
|
||||
if units and #units > 0 then
|
||||
local u = units[1]
|
||||
local pos = u:getPoint()
|
||||
-- Use dot accessor to test for function existence; colon-call to invoke
|
||||
local vel = (u.getVelocity and u:getVelocity()) or {x=0,y=0,z=0}
|
||||
local speed = math.sqrt((vel.x or 0)^2 + (vel.y or 0)^2 + (vel.z or 0)^2)
|
||||
local airbaseObj = AIRBASE:FindByName(destination)
|
||||
local dist = nil
|
||||
if airbaseObj then
|
||||
local dest = airbaseObj:GetCoordinate():GetVec2()
|
||||
local dx = pos.x - dest.x
|
||||
local dz = pos.z - dest.y
|
||||
dist = math.sqrt(dx*dx + dz*dz)
|
||||
end
|
||||
log(string.format("[TDAC DEBUG] %s state check %d: alive=%s pos=(%.1f,%.1f) speed=%.2f m/s distToDest=%s", name, iter, tostring(spawnedGroup:IsAlive()), pos.x or 0, pos.z or 0, speed, tostring(dist)))
|
||||
else
|
||||
log(string.format("[TDAC DEBUG] %s state check %d: DCS group has no units", tostring(spawnedGroup:GetName()), iter))
|
||||
end
|
||||
else
|
||||
log(string.format("[TDAC DEBUG] %s state check %d: no DCS group object", tostring(spawnedGroup:GetName()), iter))
|
||||
end
|
||||
end)
|
||||
if not ok then
|
||||
log("[TDAC DEBUG] Error during debugLogState: " .. tostring(err))
|
||||
end
|
||||
timer.scheduleFunction(function() debugLogState(iter + 1) end, {}, timer.getTime() + checkInterval)
|
||||
end
|
||||
timer.scheduleFunction(function() debugLogState(1) end, {}, timer.getTime() + checkInterval)
|
||||
|
||||
-- RAT should handle routing/taxi/parking. Finalize mission tracking now.
|
||||
finalizeMissionAfterSpawn(spawnedGroup, spawnPos)
|
||||
end)
|
||||
|
||||
local okSpawn, errSpawn = pcall(function() rat:Spawn(1) end)
|
||||
if not okSpawn then
|
||||
log("ERROR: rat:Spawn() failed for template '" .. tostring(cargoTemplate) .. "'. Error: " .. tostring(errSpawn))
|
||||
if debug and debug.traceback then
|
||||
log("TRACEBACK: " .. tostring(debug.traceback(errSpawn)))
|
||||
end
|
||||
announceToCoalition(coalitionKey, "Resupply mission to " .. destination .. " failed (spawn error)!")
|
||||
return
|
||||
end
|
||||
-- Assign route to destination using DCS-native AI tasking, with retries to handle slow registration
|
||||
local function assignRouteWithRetries(attempt, maxAttempts)
|
||||
attempt = attempt or 1
|
||||
maxAttempts = maxAttempts or 6
|
||||
if not (mission.group and mission.group:IsAlive()) then
|
||||
log(string.format("assignRouteWithRetries: mission.group invalid or dead (attempt %d/%d)", attempt, maxAttempts))
|
||||
if attempt >= maxAttempts then
|
||||
mission.status = "failed"
|
||||
log("Cargo mission failed: spawned group never registered/alive for mission to " .. destination)
|
||||
announceToCoalition(coalitionKey, "Resupply mission to " .. destination .. " failed (spawn issue)!")
|
||||
return
|
||||
end
|
||||
-- retry after backoff
|
||||
timer.scheduleFunction(function() assignRouteWithRetries(attempt + 1, maxAttempts) end, {}, timer.getTime() + (attempt * 2))
|
||||
return
|
||||
end
|
||||
|
||||
local destAirbase = AIRBASE:FindByName(destination)
|
||||
if not destAirbase then
|
||||
log("assignRouteWithRetries: Destination airbase not found: " .. tostring(destination))
|
||||
mission.status = "failed"
|
||||
announceToCoalition(coalitionKey, "Resupply mission to " .. destination .. " failed (no airbase)!")
|
||||
return
|
||||
end
|
||||
local dcsGroup = mission.group:GetDCSObject()
|
||||
if not dcsGroup then
|
||||
log("assignRouteWithRetries: DCS group object not available yet (attempt " .. attempt .. ")")
|
||||
if attempt >= maxAttempts then
|
||||
mission.status = "failed"
|
||||
announceToCoalition(coalitionKey, "Resupply mission to " .. destination .. " failed (no DCS group)!")
|
||||
return
|
||||
end
|
||||
timer.scheduleFunction(function() assignRouteWithRetries(attempt + 1, maxAttempts) end, {}, timer.getTime() + (attempt * 2))
|
||||
return
|
||||
end
|
||||
|
||||
local controller = dcsGroup:getController()
|
||||
if not controller then
|
||||
log("assignRouteWithRetries: Controller not available yet (attempt " .. attempt .. ")")
|
||||
if attempt >= maxAttempts then
|
||||
mission.status = "failed"
|
||||
announceToCoalition(coalitionKey, "Resupply mission to " .. destination .. " failed (no controller)!")
|
||||
return
|
||||
end
|
||||
timer.scheduleFunction(function() assignRouteWithRetries(attempt + 1, maxAttempts) end, {}, timer.getTime() + (attempt * 2))
|
||||
return
|
||||
end
|
||||
|
||||
-- Build route now that we have positions. Use the spawn position captured earlier if available,
|
||||
-- otherwise read the current unit position from the DCS group.
|
||||
local cruiseAlt = 6096 -- 20,000 feet in meters
|
||||
local destCoord = destAirbase:GetCoordinate():GetVec2()
|
||||
local destElevation = destAirbase:GetCoordinate():GetLandHeight() or 0
|
||||
local landingAlt = destElevation + 10 -- 10m above ground
|
||||
local airdromeId = destAirbase:GetID() or 0
|
||||
local destX = destCoord.x
|
||||
local destZ = destCoord.y
|
||||
|
||||
local pos = mission._spawnPos
|
||||
if not pos then
|
||||
local units = dcsGroup:getUnits()
|
||||
if units and #units > 0 then
|
||||
pos = units[1]:getPoint()
|
||||
end
|
||||
end
|
||||
if not pos or not pos.x or not pos.z then
|
||||
log("assignRouteWithRetries: Could not determine spawn position for route assignment (attempt " .. attempt .. ")")
|
||||
if attempt >= maxAttempts then
|
||||
mission.status = "failed"
|
||||
announceToCoalition(coalitionKey, "Resupply mission to " .. destination .. " failed (no position)!")
|
||||
return
|
||||
end
|
||||
timer.scheduleFunction(function() assignRouteWithRetries(attempt + 1, maxAttempts) end, {}, timer.getTime() + (attempt * 2))
|
||||
return
|
||||
end
|
||||
|
||||
local route = {
|
||||
{
|
||||
x = pos.x,
|
||||
z = pos.z,
|
||||
alt = cruiseAlt,
|
||||
type = "Turning Point",
|
||||
action = "Turning Point",
|
||||
speed = 330
|
||||
},
|
||||
{
|
||||
x = destX,
|
||||
z = destZ,
|
||||
alt = cruiseAlt,
|
||||
type = "Turning Point",
|
||||
action = "Turning Point",
|
||||
speed = 330
|
||||
},
|
||||
{
|
||||
x = destX,
|
||||
z = destZ,
|
||||
alt = landingAlt,
|
||||
type = "Land",
|
||||
action = "Landing",
|
||||
speed = 70,
|
||||
airdromeId = airdromeId
|
||||
},
|
||||
}
|
||||
|
||||
log("DEBUG: Route table assigned:")
|
||||
for i, wp in ipairs(route) do
|
||||
log(string.format(" WP%d: x=%.1f z=%.1f alt=%.1f type=%s action=%s speed=%.1f", i, wp.x, wp.z, wp.alt, wp.type, wp.action or "", wp.speed or 0))
|
||||
end
|
||||
|
||||
local okSet, errSet = pcall(function()
|
||||
controller:setTask({ id = 'Mission', params = { route = { points = route } } })
|
||||
end)
|
||||
if not okSet then
|
||||
log("ERROR: controller:setTask failed: " .. tostring(errSet))
|
||||
if debug and debug.traceback then
|
||||
log("TRACEBACK: " .. tostring(debug.traceback(errSet)))
|
||||
end
|
||||
end
|
||||
log("Assigned custom route to airbase: " .. destination)
|
||||
if mission.group and mission.group:IsAlive() then
|
||||
mission.status = "enroute"
|
||||
mission._pendingStartTime = timer.getTime()
|
||||
announceToCoalition(coalitionKey, "CARGO aircraft departing (airborne) for " .. destination .. ". Defend it!")
|
||||
else
|
||||
mission.status = "failed"
|
||||
log("Cargo mission failed after route assignment: group not alive: " .. tostring(destination))
|
||||
announceToCoalition(coalitionKey, "Resupply mission to " .. destination .. " failed after assignment!")
|
||||
end
|
||||
end
|
||||
|
||||
-- Start first attempt after short delay
|
||||
timer.scheduleFunction(function() assignRouteWithRetries(1, 5) end, {}, timer.getTime() + 2)
|
||||
end
|
||||
|
||||
|
||||
-- Parking diagnostics helper
|
||||
-- Call from DCS console: _G.TDAC_LogAirbaseParking("Luostari Pechenga")
|
||||
function _G.TDAC_LogAirbaseParking(airbaseName)
|
||||
if type(airbaseName) ~= 'string' then
|
||||
log("TDAC Parking helper: airbaseName must be a string", true)
|
||||
return false
|
||||
end
|
||||
local base = AIRBASE:FindByName(airbaseName)
|
||||
if not base then
|
||||
log("TDAC Parking helper: AIRBASE:FindByName returned nil for '" .. tostring(airbaseName) .. "'", true)
|
||||
return false
|
||||
end
|
||||
local function spotsFor(term)
|
||||
local ok, n = pcall(function() return base:GetParkingSpotsNumber(term) end)
|
||||
if not ok then return nil end
|
||||
return n
|
||||
end
|
||||
local openBig = spotsFor(AIRBASE.TerminalType.OpenBig)
|
||||
local openMed = spotsFor(AIRBASE.TerminalType.OpenMed)
|
||||
local openMedOrBig = spotsFor(AIRBASE.TerminalType.OpenMedOrBig)
|
||||
local runway = spotsFor(AIRBASE.TerminalType.Runway)
|
||||
log(string.format("TDAC Parking: %s -> OpenBig=%s OpenMed=%s OpenMedOrBig=%s Runway=%s", airbaseName, tostring(openBig), tostring(openMed), tostring(openMedOrBig), tostring(runway)), true)
|
||||
return true
|
||||
end
|
||||
|
||||
|
||||
-- Pre-dispatch safety check: ensure destination can accommodate larger transport types
|
||||
local function destinationHasSuitableParking(destination, preferredTermTypes)
|
||||
local base = AIRBASE:FindByName(destination)
|
||||
if not base then return false end
|
||||
preferredTermTypes = preferredTermTypes or { AIRBASE.TerminalType.OpenBig, AIRBASE.TerminalType.OpenMedOrBig, AIRBASE.TerminalType.OpenMed }
|
||||
for _, term in ipairs(preferredTermTypes) do
|
||||
local ok, n = pcall(function() return base:GetParkingSpotsNumber(term) end)
|
||||
if ok and n and n > 0 then
|
||||
return true
|
||||
end
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
|
||||
--[[
|
||||
monitorSquadrons()
|
||||
--------------------------------------------------------------------------
|
||||
Checks all squadrons for each coalition. If a squadron is below the resupply threshold and has no active cargo mission,
|
||||
triggers a supply request and dispatches a cargo aircraft.
|
||||
]]
|
||||
local function monitorSquadrons()
|
||||
for _, coalitionKey in ipairs({"red", "blue"}) do
|
||||
local config = CARGO_SUPPLY_CONFIG[coalitionKey]
|
||||
local squadrons = (coalitionKey == "red") and RED_SQUADRON_CONFIG or BLUE_SQUADRON_CONFIG
|
||||
for _, squadron in ipairs(squadrons) do
|
||||
local current, max, ratio = getSquadronStatus(squadron, coalitionKey)
|
||||
log("Squadron status: " .. squadron.displayName .. " (" .. coalitionKey .. ") " .. current .. "/" .. max .. " ratio: " .. string.format("%.2f", ratio))
|
||||
if ratio <= config.threshold and not hasActiveCargoMission(coalitionKey, squadron.airbaseName) then
|
||||
log("Supply request triggered for " .. squadron.displayName .. " at " .. squadron.airbaseName)
|
||||
announceToCoalition(coalitionKey, "Supply requested for " .. squadron.airbaseName .. "! Squadron: " .. squadron.displayName)
|
||||
dispatchCargo(squadron, coalitionKey)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--[[
|
||||
monitorCargoMissions()
|
||||
--------------------------------------------------------------------------
|
||||
Monitors all cargo missions, updates their status, and cleans up completed/failed ones.
|
||||
Handles mission failure after a grace period and mission completion when the group is near the destination airbase.
|
||||
]]
|
||||
local function monitorCargoMissions()
|
||||
for _, coalitionKey in ipairs({"red", "blue"}) do
|
||||
for _, mission in ipairs(cargoMissions[coalitionKey]) do
|
||||
if mission.group == nil then
|
||||
log("[DEBUG] Mission group object is nil for mission to " .. tostring(mission.destination))
|
||||
else
|
||||
log("[DEBUG] Mission group: " .. tostring(mission.group:GetName()) .. ", IsAlive(): " .. tostring(mission.group:IsAlive()))
|
||||
local dcsGroup = mission.group:GetDCSObject()
|
||||
if dcsGroup then
|
||||
local units = dcsGroup:getUnits()
|
||||
if units and #units > 0 then
|
||||
local pos = units[1]:getPoint()
|
||||
log(string.format("[DEBUG] Group position: x=%.1f y=%.1f z=%.1f", pos.x, pos.y, pos.z))
|
||||
else
|
||||
log("[DEBUG] No units found in DCS group for mission to " .. tostring(mission.destination))
|
||||
end
|
||||
else
|
||||
log("[DEBUG] DCS group object is nil for mission to " .. tostring(mission.destination))
|
||||
end
|
||||
end
|
||||
|
||||
local graceElapsed = mission._pendingStartTime and (timer.getTime() - mission._pendingStartTime > (mission._gracePeriod or 8))
|
||||
|
||||
-- Only allow mission to be failed after grace period, and only if group is truly dead.
|
||||
-- Some DCS/MOOSE group objects may momentarily report IsAlive() == false while units still exist, so
|
||||
-- also check DCS object/unit presence before declaring failure.
|
||||
if (mission.status == "pending" or mission.status == "enroute") and graceElapsed then
|
||||
local isAlive = mission.group and mission.group:IsAlive()
|
||||
local dcsGroup = mission.group and mission.group:GetDCSObject()
|
||||
local unitsPresent = false
|
||||
if dcsGroup then
|
||||
local units = dcsGroup:getUnits()
|
||||
unitsPresent = units and (#units > 0)
|
||||
end
|
||||
if not isAlive and not unitsPresent then
|
||||
mission.status = "failed"
|
||||
log("Cargo mission failed (after grace period): " .. (mission.group and mission.group:GetName() or "nil group") .. " to " .. mission.destination)
|
||||
announceToCoalition(coalitionKey, "Resupply mission to " .. mission.destination .. " failed!")
|
||||
else
|
||||
log("DEBUG: Mission appears to still have DCS units despite IsAlive=false; skipping failure for " .. tostring(mission.destination))
|
||||
end
|
||||
end
|
||||
|
||||
-- Mission completion logic (unchanged)
|
||||
if mission.status == "enroute" and mission.group and mission.group:IsAlive() then
|
||||
local destAirbase = AIRBASE:FindByName(mission.destination)
|
||||
local reached = false
|
||||
if destAirbase then
|
||||
-- Prefer native MOOSE helper if available
|
||||
if mission.group.IsNearAirbase and type(mission.group.IsNearAirbase) == "function" then
|
||||
reached = mission.group:IsNearAirbase(destAirbase, 3000)
|
||||
else
|
||||
-- Fallback: compute distance between group's first unit and airbase coordinate
|
||||
local dcsGroup = mission.group and mission.group.GetDCSObject and mission.group:GetDCSObject()
|
||||
if dcsGroup then
|
||||
local units = dcsGroup:getUnits()
|
||||
if units and #units > 0 then
|
||||
local pos = units[1]:getPoint()
|
||||
local destCoord = destAirbase:GetCoordinate():GetVec2()
|
||||
local dx = pos.x - destCoord.x
|
||||
local dz = pos.z - destCoord.y
|
||||
local dist = math.sqrt(dx * dx + dz * dz)
|
||||
if dist <= 3000 then
|
||||
reached = true
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
if reached then
|
||||
mission.status = "completed"
|
||||
local name = (mission.group and (mission.group.GetName and mission.group:GetName() or tostring(mission.group)) ) or "unknown"
|
||||
log("Cargo mission completed: " .. name .. " delivered to " .. mission.destination)
|
||||
announceToCoalition(coalitionKey, "Resupply delivered to " .. mission.destination .. "!")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
cleanupCargoMissions()
|
||||
end
|
||||
|
||||
--[[
|
||||
MAIN DISPATCHER LOOP
|
||||
--------------------------------------------------------------------------
|
||||
Runs the main dispatcher logic on a timer interval.
|
||||
]]
|
||||
local function cargoDispatcherMain()
|
||||
log("═══════════════════════════════════════════════════════════════════════════════", true)
|
||||
log("Cargo Dispatcher main loop running.", true)
|
||||
monitorSquadrons()
|
||||
monitorCargoMissions()
|
||||
-- Schedule the next run inside a protected call to avoid unhandled errors
|
||||
timer.scheduleFunction(function()
|
||||
local ok, err = pcall(cargoDispatcherMain)
|
||||
if not ok then
|
||||
log("FATAL: cargoDispatcherMain crashed on scheduled run: " .. tostring(err))
|
||||
-- do not reschedule to avoid crash loops
|
||||
end
|
||||
end, {}, timer.getTime() + DISPATCHER_CONFIG.interval)
|
||||
end
|
||||
|
||||
-- Start the dispatcher
|
||||
local ok, err = pcall(cargoDispatcherMain)
|
||||
if not ok then
|
||||
log("FATAL: cargoDispatcherMain crashed on startup: " .. tostring(err))
|
||||
end
|
||||
|
||||
log("═══════════════════════════════════════════════════════════════════════════════", true)
|
||||
-- End Moose_TDAC_CargoDispatcher.lua
|
||||
|
||||
|
||||
-- Diagnostic helper: call from DCS console to test spawn-by-name and routing.
|
||||
-- Example (paste into DCS Lua console):
|
||||
-- _G.TDAC_CargoDispatcher_TestSpawn("CARGO_BLUE_C130_TEMPLATE", "Kittila", "Luostari Pechenga")
|
||||
function _G.TDAC_CargoDispatcher_TestSpawn(templateName, originAirbase, destinationAirbase)
|
||||
env.info("[TDAC TEST] Starting test spawn for template: " .. tostring(templateName))
|
||||
local ok, err
|
||||
if type(templateName) ~= 'string' then
|
||||
env.info("[TDAC TEST] templateName must be a string")
|
||||
return false, "invalid templateName"
|
||||
end
|
||||
local spawnByName = nil
|
||||
ok, spawnByName = pcall(function() return SPAWN:New(templateName) end)
|
||||
if not ok or not spawnByName then
|
||||
env.info("[TDAC TEST] SPAWN:New failed for template " .. tostring(templateName) .. ". Error: " .. tostring(spawnByName))
|
||||
if debug and debug.traceback then env.info(debug.traceback(tostring(spawnByName))) end
|
||||
return false, "spawn_new_failed"
|
||||
end
|
||||
|
||||
spawnByName:OnSpawnGroup(function(spawnedGroup)
|
||||
env.info("[TDAC TEST] OnSpawnGroup called for: " .. tostring(spawnedGroup:GetName()))
|
||||
local dcsGroup = spawnedGroup:GetDCSObject()
|
||||
if dcsGroup then
|
||||
local units = dcsGroup:getUnits()
|
||||
if units and #units > 0 then
|
||||
local pos = units[1]:getPoint()
|
||||
env.info(string.format("[TDAC TEST] Spawned pos x=%.1f y=%.1f z=%.1f", pos.x, pos.y, pos.z))
|
||||
end
|
||||
end
|
||||
if destinationAirbase then
|
||||
local okAssign, errAssign = pcall(function()
|
||||
local base = AIRBASE:FindByName(destinationAirbase)
|
||||
if base and spawnedGroup and spawnedGroup.RouteToAirbase then
|
||||
spawnedGroup:RouteToAirbase(base, AI_Task_Land.Runway)
|
||||
env.info("[TDAC TEST] RouteToAirbase assigned to " .. tostring(destinationAirbase))
|
||||
else
|
||||
env.info("[TDAC TEST] RouteToAirbase not available or base not found")
|
||||
end
|
||||
end)
|
||||
if not okAssign then
|
||||
env.info("[TDAC TEST] RouteToAirbase pcall failed: " .. tostring(errAssign))
|
||||
if debug and debug.traceback then env.info(debug.traceback(tostring(errAssign))) end
|
||||
end
|
||||
end
|
||||
end)
|
||||
|
||||
ok, err = pcall(function() spawnByName:Spawn() end)
|
||||
if not ok then
|
||||
env.info("[TDAC TEST] spawnByName:Spawn() failed: " .. tostring(err))
|
||||
if debug and debug.traceback then env.info(debug.traceback(tostring(err))) end
|
||||
return false, "spawn_failed"
|
||||
end
|
||||
env.info("[TDAC TEST] spawnByName:Spawn() returned successfully")
|
||||
return true
|
||||
end
|
||||
|
||||
|
||||
-- Global notify API: allow external scripts (e.g. Load2nd) to mark a cargo mission as delivered.
|
||||
-- Usage: _G.TDAC_CargoDelivered(groupName, destination, coalitionKey)
|
||||
function _G.TDAC_CargoDelivered(groupName, destination, coalitionKey)
|
||||
local ok, err = pcall(function()
|
||||
if type(groupName) ~= 'string' or type(destination) ~= 'string' or type(coalitionKey) ~= 'string' then
|
||||
log("TDAC notify: invalid parameters to _G.TDAC_CargoDelivered", true)
|
||||
return false
|
||||
end
|
||||
coalitionKey = coalitionKey:lower()
|
||||
if not cargoMissions or not cargoMissions[coalitionKey] then
|
||||
log("TDAC notify: no cargoMissions table for coalition '" .. tostring(coalitionKey) .. "'", true)
|
||||
return false
|
||||
end
|
||||
-- Find any mission matching destination and group name (or group name substring) and mark completed.
|
||||
for _, mission in ipairs(cargoMissions[coalitionKey]) do
|
||||
local mname = mission.group and (mission.group.GetName and mission.group:GetName() or tostring(mission.group)) or nil
|
||||
if mission.destination == destination then
|
||||
if mname and string.find(mname:upper(), groupName:upper(), 1, true) then
|
||||
mission.status = 'completed'
|
||||
log("TDAC notify: marked mission " .. tostring(mname) .. " as completed for " .. destination .. " (" .. coalitionKey .. ")")
|
||||
return true
|
||||
end
|
||||
end
|
||||
end
|
||||
log("TDAC notify: no matching mission found for group='" .. tostring(groupName) .. "' dest='" .. tostring(destination) .. "' coal='" .. tostring(coalitionKey) .. "'")
|
||||
return false
|
||||
end)
|
||||
if not ok then
|
||||
log("ERROR: _G.TDAC_CargoDelivered failed: " .. tostring(err), true)
|
||||
return false
|
||||
end
|
||||
return true
|
||||
end
|
||||
@ -50,17 +50,18 @@ Zone response behaviors include:
|
||||
|
||||
REPLENISHMENT SYSTEM:
|
||||
• Automated cargo aircraft detection system that monitors for transport aircraft
|
||||
landings to replenish squadron aircraft counts (fixed wing only):
|
||||
flyovers to replenish squadron aircraft counts (fixed wing only):
|
||||
• Detects cargo aircraft by name patterns (CARGO, TRANSPORT, C130, C-130, AN26, AN-26)
|
||||
• Monitors landing status based on velocity and proximity to friendly airbases
|
||||
• Monitors flyover proximity to friendly airbases (no landing required)
|
||||
• Replenishes squadron aircraft up to maximum capacity per airbase
|
||||
• Prevents duplicate processing of the same cargo delivery
|
||||
• Coalition-specific replenishment amounts configurable independently
|
||||
• Supports sustained operations over extended mission duration
|
||||
|
||||
*** This system does not spawn or manage cargo aircraft - it only detects when
|
||||
your existing cargo aircraft complete deliveries. Create and route your own
|
||||
transport missions to maintain squadron strength. ***
|
||||
your existing cargo aircraft complete deliveries via flyover. Create and route your own
|
||||
transport missions to maintain squadron strength. Aircraft can deliver supplies by
|
||||
flying within 3000m of any configured airbase without needing to land. ***
|
||||
|
||||
INTERCEPT RATIO SYSTEM:
|
||||
Sophisticated threat response calculation with zone-based modifiers:
|
||||
@ -164,8 +165,6 @@ local TADC_SETTINGS = {
|
||||
},
|
||||
}
|
||||
|
||||
-- Load squadron configs from external file
|
||||
-- RED_SQUADRON_CONFIG and BLUE_SQUADRON_CONFIG are now global, loaded by the trigger
|
||||
|
||||
--[[
|
||||
INTERCEPT RATIO CHART - How many interceptors launch per threat aircraft:
|
||||
@ -213,7 +212,8 @@ local ADVANCED_SETTINGS = {
|
||||
-- Cargo aircraft detection patterns (aircraft with these names will replenish squadrons (Currently only fixed wing aircraft supported))
|
||||
cargoPatterns = {"CARGO", "TRANSPORT", "C130", "C-130", "AN26", "AN-26"},
|
||||
|
||||
-- Distance from airbase to consider cargo "landed" (meters)
|
||||
-- Distance from airbase to consider cargo "delivered" via flyover (meters)
|
||||
-- Aircraft flying within this range will count as supply delivery (no landing required)
|
||||
cargoLandingDistance = 3000,
|
||||
|
||||
-- Velocity below which aircraft is considered "landed" (km/h)
|
||||
@ -235,6 +235,8 @@ local ADVANCED_SETTINGS = {
|
||||
═══════════════════════════════════════════════════════════════════════════════
|
||||
]]
|
||||
|
||||
|
||||
|
||||
-- Internal tracking variables - separate for each coalition
|
||||
local activeInterceptors = {
|
||||
red = {},
|
||||
@ -252,11 +254,18 @@ local squadronCooldowns = {
|
||||
red = {},
|
||||
blue = {}
|
||||
}
|
||||
local squadronAircraftCounts = {
|
||||
squadronAircraftCounts = {
|
||||
red = {},
|
||||
blue = {}
|
||||
}
|
||||
|
||||
-- Logging function
|
||||
local function log(message, detailed)
|
||||
if not detailed or ADVANCED_SETTINGS.enableDetailedLogging then
|
||||
env.info(ADVANCED_SETTINGS.logPrefix .. " " .. message)
|
||||
end
|
||||
end
|
||||
|
||||
-- Performance optimization: Cache SET_GROUP objects to avoid repeated creation
|
||||
local cachedSets = {
|
||||
redCargo = nil,
|
||||
@ -265,7 +274,17 @@ local cachedSets = {
|
||||
blueAircraft = nil
|
||||
}
|
||||
|
||||
-- Initialize squadron aircraft counts for both coalitions
|
||||
if type(RED_SQUADRON_CONFIG) ~= "table" then
|
||||
local msg = "CONFIG ERROR: RED_SQUADRON_CONFIG is missing or not loaded. Make sure Moose_TADC_SquadronConfigs_Load1st.lua is loaded before this script."
|
||||
log(msg, true)
|
||||
MESSAGE:New(msg, 30):ToAll()
|
||||
end
|
||||
if type(BLUE_SQUADRON_CONFIG) ~= "table" then
|
||||
local msg = "CONFIG ERROR: BLUE_SQUADRON_CONFIG is missing or not loaded. Make sure Moose_TADC_SquadronConfigs_Load1st.lua is loaded before this script."
|
||||
log(msg, true)
|
||||
MESSAGE:New(msg, 30):ToAll()
|
||||
end
|
||||
|
||||
for _, squadron in pairs(RED_SQUADRON_CONFIG) do
|
||||
if squadron.aircraft and squadron.templateName then
|
||||
squadronAircraftCounts.red[squadron.templateName] = squadron.aircraft
|
||||
@ -278,12 +297,7 @@ for _, squadron in pairs(BLUE_SQUADRON_CONFIG) do
|
||||
end
|
||||
end
|
||||
|
||||
-- Logging function
|
||||
local function log(message, detailed)
|
||||
if not detailed or ADVANCED_SETTINGS.enableDetailedLogging then
|
||||
env.info(ADVANCED_SETTINGS.logPrefix .. " " .. message)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
-- Squadron resource summary generator
|
||||
|
||||
@ -626,7 +640,95 @@ local function validateConfiguration()
|
||||
end
|
||||
end
|
||||
|
||||
-- Monitor cargo aircraft landings for squadron replenishment
|
||||
-- Process cargo delivery for a squadron
|
||||
local function processCargoDelivery(cargoGroup, squadron, coalitionSide, coalitionKey)
|
||||
-- Initialize processed deliveries table
|
||||
if not _G.processedDeliveries then
|
||||
_G.processedDeliveries = {}
|
||||
end
|
||||
|
||||
-- Create unique delivery key including timestamp to prevent race conditions
|
||||
-- Note: Key doesn't include airbase to prevent double-counting if aircraft moves between airbases
|
||||
local deliveryKey = cargoGroup:GetName() .. "_" .. coalitionKey:upper() .. "_" .. cargoGroup:GetID()
|
||||
|
||||
if not _G.processedDeliveries[deliveryKey] then
|
||||
-- Mark delivery as processed immediately to prevent race conditions
|
||||
_G.processedDeliveries[deliveryKey] = timer.getTime()
|
||||
-- Process replenishment
|
||||
local currentCount = squadronAircraftCounts[coalitionKey][squadron.templateName] or 0
|
||||
local maxCount = squadron.aircraft
|
||||
local newCount = math.min(currentCount + TADC_SETTINGS[coalitionKey].cargoReplenishmentAmount, maxCount)
|
||||
local actualAdded = newCount - currentCount
|
||||
|
||||
if actualAdded > 0 then
|
||||
squadronAircraftCounts[coalitionKey][squadron.templateName] = newCount
|
||||
local msg = coalitionKey:upper() .. " CARGO DELIVERY: " .. cargoGroup:GetName() .. " delivered " .. actualAdded ..
|
||||
" aircraft to " .. squadron.displayName ..
|
||||
" (" .. newCount .. "/" .. maxCount .. ")"
|
||||
log(msg)
|
||||
MESSAGE:New(msg, 20):ToCoalition(coalitionSide)
|
||||
USERSOUND:New("Cargo_Delivered.ogg"):ToCoalition(coalitionSide)
|
||||
-- Notify dispatcher (if available) so it can mark the matching mission completed immediately
|
||||
if type(_G.TDAC_CargoDelivered) == 'function' then
|
||||
pcall(function()
|
||||
_G.TDAC_CargoDelivered(cargoGroup:GetName(), squadron.airbaseName, coalitionKey)
|
||||
end)
|
||||
end
|
||||
else
|
||||
local msg = coalitionKey:upper() .. " CARGO DELIVERY: " .. squadron.displayName .. " already at max capacity"
|
||||
log(msg, true)
|
||||
MESSAGE:New(msg, 15):ToCoalition(coalitionSide)
|
||||
USERSOUND:New("Cargo_Delivered.ogg"):ToCoalition(coalitionSide)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Event handler for cargo aircraft landing (backup for actual landings)
|
||||
local cargoEventHandler = {}
|
||||
function cargoEventHandler:onEvent(event)
|
||||
if event.id == world.event.S_EVENT_LAND then
|
||||
local unit = event.initiator
|
||||
if unit and type(unit) == "table" and unit.IsAlive and unit:IsAlive() then
|
||||
local group = unit:GetGroup()
|
||||
if group and type(group) == "table" and group.IsAlive and group:IsAlive() then
|
||||
local cargoName = group:GetName():upper()
|
||||
local isCargoAircraft = false
|
||||
|
||||
-- Check if aircraft name matches cargo patterns
|
||||
for _, pattern in pairs(ADVANCED_SETTINGS.cargoPatterns) do
|
||||
if string.find(cargoName, pattern) then
|
||||
isCargoAircraft = true
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
if isCargoAircraft then
|
||||
local cargoCoord = unit:GetCoordinate()
|
||||
local coalitionSide = unit:GetCoalition()
|
||||
local coalitionKey = (coalitionSide == coalition.side.RED) and "red" or "blue"
|
||||
|
||||
-- Check which airbase it's near
|
||||
local squadronConfig = getSquadronConfig(coalitionSide)
|
||||
for _, squadron in pairs(squadronConfig) do
|
||||
local airbase = AIRBASE:FindByName(squadron.airbaseName)
|
||||
if airbase and airbase:GetCoalition() == coalitionSide then
|
||||
local airbaseCoord = airbase:GetCoordinate()
|
||||
local distance = cargoCoord:Get2DDistance(airbaseCoord)
|
||||
|
||||
-- If within configured distance of airbase, consider it a landing delivery
|
||||
if distance < ADVANCED_SETTINGS.cargoLandingDistance then
|
||||
log("LANDING DELIVERY: " .. cargoName .. " landed and delivered at " .. squadron.airbaseName .. " (distance: " .. math.floor(distance) .. "m)")
|
||||
processCargoDelivery(group, squadron, coalitionSide, coalitionKey)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Monitor cargo aircraft flyovers for squadron replenishment
|
||||
local function monitorCargoReplenishment()
|
||||
-- Process RED cargo aircraft
|
||||
if TADC_SETTINGS.enableRed then
|
||||
@ -652,49 +754,24 @@ local function monitorCargoReplenishment()
|
||||
if isCargoAircraft then
|
||||
local cargoCoord = cargoGroup:GetCoordinate()
|
||||
local cargoVelocity = cargoGroup:GetVelocityKMH()
|
||||
-- DEBUG: log candidate details with timestamp for diagnosis
|
||||
if ADVANCED_SETTINGS.enableDetailedLogging then
|
||||
log(string.format("[LOAD2ND DEBUG] Evaluating cargo %s at time=%d vel=%.2f km/h coord=(%.1f,%.1f)",
|
||||
cargoGroup:GetName(), timer.getTime(), cargoVelocity, cargoCoord:GetVec2().x, cargoCoord:GetVec2().y))
|
||||
end
|
||||
|
||||
-- Consider aircraft "landed" if velocity is very low
|
||||
if cargoVelocity < ADVANCED_SETTINGS.cargoLandedVelocity then
|
||||
-- Check which RED airbase it's near
|
||||
for _, squadron in pairs(RED_SQUADRON_CONFIG) do
|
||||
local airbase = AIRBASE:FindByName(squadron.airbaseName)
|
||||
if airbase and airbase:GetCoalition() == coalition.side.RED then
|
||||
local airbaseCoord = airbase:GetCoordinate()
|
||||
local distance = cargoCoord:Get2DDistance(airbaseCoord)
|
||||
|
||||
-- If within configured distance of airbase, consider it a delivery
|
||||
if distance < ADVANCED_SETTINGS.cargoLandingDistance then
|
||||
-- Initialize processed deliveries table
|
||||
if not _G.processedDeliveries then
|
||||
_G.processedDeliveries = {}
|
||||
end
|
||||
|
||||
-- Create unique delivery key including timestamp to prevent race conditions
|
||||
local deliveryKey = cargoName .. "_RED_" .. squadron.airbaseName .. "_" .. cargoGroup:GetID()
|
||||
|
||||
if not _G.processedDeliveries[deliveryKey] then
|
||||
-- Mark delivery as processed immediately to prevent race conditions
|
||||
_G.processedDeliveries[deliveryKey] = timer.getTime()
|
||||
-- Process replenishment
|
||||
local currentCount = squadronAircraftCounts.red[squadron.templateName] or 0
|
||||
local maxCount = squadron.aircraft
|
||||
local newCount = math.min(currentCount + TADC_SETTINGS.red.cargoReplenishmentAmount, maxCount)
|
||||
local actualAdded = newCount - currentCount
|
||||
|
||||
if actualAdded > 0 then
|
||||
squadronAircraftCounts.red[squadron.templateName] = newCount
|
||||
local msg = "RED CARGO DELIVERY: " .. cargoName .. " delivered " .. actualAdded ..
|
||||
" aircraft to " .. squadron.displayName ..
|
||||
" (" .. newCount .. "/" .. maxCount .. ")"
|
||||
log(msg)
|
||||
MESSAGE:New(msg, 20):ToAll()
|
||||
else
|
||||
local msg = "RED CARGO DELIVERY: " .. squadron.displayName .. " already at max capacity"
|
||||
log(msg, true)
|
||||
MESSAGE:New(msg, 15):ToAll()
|
||||
end
|
||||
end
|
||||
end
|
||||
-- Check for flyover delivery - aircraft within range of airbase (no landing required)
|
||||
-- Check which RED airbase it's near
|
||||
for _, squadron in pairs(RED_SQUADRON_CONFIG) do
|
||||
local airbase = AIRBASE:FindByName(squadron.airbaseName)
|
||||
if airbase and airbase:GetCoalition() == coalition.side.RED then
|
||||
local airbaseCoord = airbase:GetCoordinate()
|
||||
local distance = cargoCoord:Get2DDistance(airbaseCoord)
|
||||
|
||||
-- If within configured distance of airbase, consider it a flyover delivery
|
||||
if distance < ADVANCED_SETTINGS.cargoLandingDistance then
|
||||
log("FLYOVER DELIVERY: " .. cargoName .. " delivered supplies to " .. squadron.airbaseName .. " (distance: " .. math.floor(distance) .. "m, altitude: " .. math.floor(cargoCoord.y/0.3048) .. " ft)")
|
||||
processCargoDelivery(cargoGroup, squadron, coalition.side.RED, "red")
|
||||
end
|
||||
end
|
||||
end
|
||||
@ -728,48 +805,18 @@ local function monitorCargoReplenishment()
|
||||
local cargoCoord = cargoGroup:GetCoordinate()
|
||||
local cargoVelocity = cargoGroup:GetVelocityKMH()
|
||||
|
||||
-- Consider aircraft "landed" if velocity is very low
|
||||
if cargoVelocity < ADVANCED_SETTINGS.cargoLandedVelocity then
|
||||
-- Check which BLUE airbase it's near
|
||||
for _, squadron in pairs(BLUE_SQUADRON_CONFIG) do
|
||||
local airbase = AIRBASE:FindByName(squadron.airbaseName)
|
||||
if airbase and airbase:GetCoalition() == coalition.side.BLUE then
|
||||
local airbaseCoord = airbase:GetCoordinate()
|
||||
local distance = cargoCoord:Get2DDistance(airbaseCoord)
|
||||
|
||||
-- If within configured distance of airbase, consider it a delivery
|
||||
if distance < ADVANCED_SETTINGS.cargoLandingDistance then
|
||||
-- Initialize processed deliveries table
|
||||
if not _G.processedDeliveries then
|
||||
_G.processedDeliveries = {}
|
||||
end
|
||||
|
||||
-- Create unique delivery key including timestamp to prevent race conditions
|
||||
local deliveryKey = cargoName .. "_BLUE_" .. squadron.airbaseName .. "_" .. cargoGroup:GetID()
|
||||
|
||||
if not _G.processedDeliveries[deliveryKey] then
|
||||
-- Mark delivery as processed immediately to prevent race conditions
|
||||
_G.processedDeliveries[deliveryKey] = timer.getTime()
|
||||
-- Process replenishment
|
||||
local currentCount = squadronAircraftCounts.blue[squadron.templateName] or 0
|
||||
local maxCount = squadron.aircraft
|
||||
local newCount = math.min(currentCount + TADC_SETTINGS.blue.cargoReplenishmentAmount, maxCount)
|
||||
local actualAdded = newCount - currentCount
|
||||
|
||||
if actualAdded > 0 then
|
||||
squadronAircraftCounts.blue[squadron.templateName] = newCount
|
||||
local msg = "BLUE CARGO DELIVERY: " .. cargoName .. " delivered " .. actualAdded ..
|
||||
" aircraft to " .. squadron.displayName ..
|
||||
" (" .. newCount .. "/" .. maxCount .. ")"
|
||||
log(msg)
|
||||
MESSAGE:New(msg, 20):ToAll()
|
||||
else
|
||||
local msg = "BLUE CARGO DELIVERY: " .. squadron.displayName .. " already at max capacity"
|
||||
log(msg, true)
|
||||
MESSAGE:New(msg, 15):ToAll()
|
||||
end
|
||||
end
|
||||
end
|
||||
-- Check for flyover delivery - aircraft within range of airbase (no landing required)
|
||||
-- Check which BLUE airbase it's near
|
||||
for _, squadron in pairs(BLUE_SQUADRON_CONFIG) do
|
||||
local airbase = AIRBASE:FindByName(squadron.airbaseName)
|
||||
if airbase and airbase:GetCoalition() == coalition.side.BLUE then
|
||||
local airbaseCoord = airbase:GetCoordinate()
|
||||
local distance = cargoCoord:Get2DDistance(airbaseCoord)
|
||||
|
||||
-- If within configured distance of airbase, consider it a flyover delivery
|
||||
if distance < ADVANCED_SETTINGS.cargoLandingDistance then
|
||||
log("FLYOVER DELIVERY: " .. cargoName .. " delivered supplies to " .. squadron.airbaseName .. " (distance: " .. math.floor(distance) .. "m, altitude: " .. math.floor(cargoCoord.y/0.3048) .. " ft)")
|
||||
processCargoDelivery(cargoGroup, squadron, coalition.side.BLUE, "blue")
|
||||
end
|
||||
end
|
||||
end
|
||||
@ -1456,6 +1503,9 @@ local function initializeSystem()
|
||||
end
|
||||
|
||||
-- Start schedulers
|
||||
-- Set up event handler for cargo landing detection
|
||||
world.addEventHandler(cargoEventHandler)
|
||||
|
||||
SCHEDULER:New(nil, detectThreats, {}, 5, TADC_SETTINGS.checkInterval)
|
||||
SCHEDULER:New(nil, monitorInterceptors, {}, 10, TADC_SETTINGS.monitorInterval)
|
||||
SCHEDULER:New(nil, checkAirbaseStatus, {}, 30, TADC_SETTINGS.statusReportInterval)
|
||||
|
||||
@ -79,18 +79,18 @@ RED_SQUADRON_CONFIG = {
|
||||
|
||||
-- ADD YOUR RED SQUADRONS HERE
|
||||
{
|
||||
templateName = "Sukhumi CAP", -- Change to your RED template name
|
||||
displayName = "Sukhumi CAP", -- Change to your preferred name
|
||||
airbaseName = "Sukhumi-Babushara", -- Change to your RED airbase
|
||||
templateName = "FIGHTER_SWEEP_RED_Kilpyavr", -- Change to your RED template name
|
||||
displayName = "Kilpyavr CAP", -- Change to your preferred name
|
||||
airbaseName = "Kilpyavr", -- Change to your RED airbase
|
||||
aircraft = 12, -- Adjust aircraft count
|
||||
skill = AI.Skill.ACE, -- AVERAGE, GOOD, HIGH, EXCELLENT, ACE
|
||||
altitude = 20000, -- Patrol altitude (feet)
|
||||
speed = 350, -- Patrol speed (knots)
|
||||
patrolTime = 25, -- Time on station (minutes)
|
||||
altitude = 15400, -- Patrol altitude (feet)
|
||||
speed = 312, -- Patrol speed (knots)
|
||||
patrolTime = 32, -- Time on station (minutes)
|
||||
type = "FIGHTER",
|
||||
|
||||
-- Zone-based Areas of Responsibility (optional - leave nil for global response)
|
||||
primaryZone = "RED_BORDER", -- Main responsibility area (zone name from mission editor)
|
||||
primaryZone = "RED BORDER", -- Main responsibility area (zone name from mission editor)
|
||||
secondaryZone = nil, -- Secondary coverage area (zone name)
|
||||
tertiaryZone = nil, -- Emergency/fallback zone (zone name)
|
||||
|
||||
@ -107,18 +107,130 @@ RED_SQUADRON_CONFIG = {
|
||||
},
|
||||
|
||||
{
|
||||
templateName = "Gudauta CAP-MiG-21", -- Change to your RED template name
|
||||
displayName = "Gudauta CAP-MiG-21", -- Change to your preferred name
|
||||
airbaseName = "Gudauta", -- Change to your RED airbase
|
||||
templateName = "FIGHTER_SWEEP_RED_Severomorsk-1", -- Change to your RED template name
|
||||
displayName = "Severomorsk-1 CAP", -- Change to your preferred name
|
||||
airbaseName = "Severomorsk-1", -- Change to your RED airbase
|
||||
aircraft = 10, -- Adjust aircraft count
|
||||
skill = AI.Skill.ACE, -- AVERAGE, GOOD, HIGH, EXCELLENT, ACE
|
||||
altitude = 18800, -- Patrol altitude (feet)
|
||||
speed = 420, -- Patrol speed (knots)
|
||||
patrolTime = 23, -- Time on station (minutes)
|
||||
type = "FIGHTER",
|
||||
|
||||
-- Zone-based Areas of Responsibility (optional - leave nil for global response)
|
||||
primaryZone = "RED BORDER", -- Main responsibility area (zone name from mission editor)
|
||||
secondaryZone = nil, -- Secondary coverage area (zone name)
|
||||
tertiaryZone = nil, -- Emergency/fallback zone (zone name)
|
||||
|
||||
-- Zone behavior settings (optional - uses defaults if not specified)
|
||||
zoneConfig = {
|
||||
primaryResponse = 1.0, -- Intercept ratio multiplier in primary zone
|
||||
secondaryResponse = 0.6, -- Intercept ratio multiplier in secondary zone
|
||||
tertiaryResponse = 1.4, -- Intercept ratio multiplier in tertiary zone
|
||||
maxRange = 200, -- Maximum engagement range from airbase (nm)
|
||||
enableFallback = false, -- Auto-switch to tertiary when base threatened
|
||||
priorityThreshold = 4, -- Min aircraft count for "major threat"
|
||||
ignoreLowPriority = false, -- Ignore threats below threshold in secondary zones
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
templateName = "FIGHTER_SWEEP_RED_Severomorsk-3", -- Change to your RED template name
|
||||
displayName = "Severomorsk-3 CAP", -- Change to your preferred name
|
||||
airbaseName = "Severomorsk-3", -- Change to your RED airbase
|
||||
aircraft = 15, -- Adjust aircraft count
|
||||
skill = AI.Skill.ACE, -- AVERAGE, GOOD, HIGH, EXCELLENT, ACE
|
||||
altitude = 26700, -- Patrol altitude (feet)
|
||||
speed = 335, -- Patrol speed (knots)
|
||||
patrolTime = 28, -- Time on station (minutes)
|
||||
type = "FIGHTER",
|
||||
|
||||
-- Zone-based Areas of Responsibility (optional - leave nil for global response)
|
||||
primaryZone = "RED BORDER", -- Main responsibility area (zone name from mission editor)
|
||||
secondaryZone = nil, -- Secondary coverage area (zone name)
|
||||
tertiaryZone = nil, -- Emergency/fallback zone (zone name)
|
||||
|
||||
-- Zone behavior settings (optional - uses defaults if not specified)
|
||||
zoneConfig = {
|
||||
primaryResponse = 1.0, -- Intercept ratio multiplier in primary zone
|
||||
secondaryResponse = 0.6, -- Intercept ratio multiplier in secondary zone
|
||||
tertiaryResponse = 1.4, -- Intercept ratio multiplier in tertiary zone
|
||||
maxRange = 200, -- Maximum engagement range from airbase (nm)
|
||||
enableFallback = false, -- Auto-switch to tertiary when base threatened
|
||||
priorityThreshold = 4, -- Min aircraft count for "major threat"
|
||||
ignoreLowPriority = false, -- Ignore threats below threshold in secondary zones
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
templateName = "FIGHTER_SWEEP_RED_Murmansk", -- Change to your RED template name
|
||||
displayName = "Murmansk CAP", -- Change to your preferred name
|
||||
airbaseName = "Murmansk International", -- Change to your RED airbase
|
||||
aircraft = 8, -- Adjust aircraft count
|
||||
skill = AI.Skill.ACE, -- AVERAGE, GOOD, HIGH, EXCELLENT, ACE
|
||||
altitude = 22100, -- Patrol altitude (feet)
|
||||
speed = 390, -- Patrol speed (knots)
|
||||
patrolTime = 20, -- Time on station (minutes)
|
||||
type = "FIGHTER",
|
||||
|
||||
-- Zone-based Areas of Responsibility (optional - leave nil for global response)
|
||||
primaryZone = "RED BORDER", -- Main responsibility area (zone name from mission editor)
|
||||
secondaryZone = nil, -- Secondary coverage area (zone name)
|
||||
tertiaryZone = nil, -- Emergency/fallback zone (zone name)
|
||||
|
||||
-- Zone behavior settings (optional - uses defaults if not specified)
|
||||
zoneConfig = {
|
||||
primaryResponse = 1.0, -- Intercept ratio multiplier in primary zone
|
||||
secondaryResponse = 0.6, -- Intercept ratio multiplier in secondary zone
|
||||
tertiaryResponse = 1.4, -- Intercept ratio multiplier in tertiary zone
|
||||
maxRange = 200, -- Maximum engagement range from airbase (nm)
|
||||
enableFallback = false, -- Auto-switch to tertiary when base threatened
|
||||
priorityThreshold = 4, -- Min aircraft count for "major threat"
|
||||
ignoreLowPriority = false, -- Ignore threats below threshold in secondary zones
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
templateName = "FIGHTER_SWEEP_RED_Monchegorsk", -- Change to your RED template name
|
||||
displayName = "Monchegorsk CAP", -- Change to your preferred name
|
||||
airbaseName = "Monchegorsk", -- Change to your RED airbase
|
||||
aircraft = 16, -- Adjust aircraft count
|
||||
skill = AI.Skill.ACE, -- AVERAGE, GOOD, HIGH, EXCELLENT, ACE
|
||||
altitude = 30000, -- Patrol altitude (feet)
|
||||
speed = 305, -- Patrol speed (knots)
|
||||
patrolTime = 35, -- Time on station (minutes)
|
||||
type = "FIGHTER",
|
||||
|
||||
-- Zone-based Areas of Responsibility (optional - leave nil for global response)
|
||||
primaryZone = "RED BORDER", -- Main responsibility area (zone name from mission editor)
|
||||
secondaryZone = nil, -- Secondary coverage area (zone name)
|
||||
tertiaryZone = nil, -- Emergency/fallback zone (zone name)
|
||||
|
||||
-- Zone behavior settings (optional - uses defaults if not specified)
|
||||
zoneConfig = {
|
||||
primaryResponse = 1.0, -- Intercept ratio multiplier in primary zone
|
||||
secondaryResponse = 0.6, -- Intercept ratio multiplier in secondary zone
|
||||
tertiaryResponse = 1.4, -- Intercept ratio multiplier in tertiary zone
|
||||
maxRange = 200, -- Maximum engagement range from airbase (nm)
|
||||
enableFallback = false, -- Auto-switch to tertiary when base threatened
|
||||
priorityThreshold = 4, -- Min aircraft count for "major threat"
|
||||
ignoreLowPriority = false, -- Ignore threats below threshold in secondary zones
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
templateName = "FIGHTER_SWEEP_RED_Olenya", -- Change to your RED template name
|
||||
displayName = "Olenya CAP", -- Change to your preferred name
|
||||
airbaseName = "Olenya", -- Change to your RED airbase
|
||||
aircraft = 12, -- Adjust aircraft count
|
||||
skill = AI.Skill.ACE, -- AVERAGE, GOOD, HIGH, EXCELLENT
|
||||
altitude = 20000, -- Patrol altitude (feet)
|
||||
speed = 350, -- Patrol speed (knots)
|
||||
patrolTime = 25, -- Time on station (minutes)
|
||||
skill = AI.Skill.ACE, -- AVERAGE, GOOD, HIGH, EXCELLENT, ACE
|
||||
altitude = 17800, -- Patrol altitude (feet)
|
||||
speed = 445, -- Patrol speed (knots)
|
||||
patrolTime = 27, -- Time on station (minutes)
|
||||
type = "FIGHTER",
|
||||
|
||||
-- Zone-based Areas of Responsibility (optional - leave nil for global response)
|
||||
primaryZone = "GUDAUTA_BORDER", -- Main responsibility area (zone name from mission editor)
|
||||
primaryZone = "RED BORDER", -- Main responsibility area (zone name from mission editor)
|
||||
secondaryZone = nil, -- Secondary coverage area (zone name)
|
||||
tertiaryZone = nil, -- Emergency/fallback zone (zone name)
|
||||
|
||||
@ -132,35 +244,8 @@ RED_SQUADRON_CONFIG = {
|
||||
priorityThreshold = 4, -- Min aircraft count for "major threat"
|
||||
ignoreLowPriority = false, -- Ignore threats below threshold in secondary zones
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
templateName = "Gudauta CAP-MiG-23", -- Change to your RED template name
|
||||
displayName = "Gudauta CAP-MiG-23", -- Change to your preferred name
|
||||
airbaseName = "Gudauta", -- Change to your RED airbase
|
||||
aircraft = 14, -- Adjust aircraft count
|
||||
skill = AI.Skill.ACE, -- AVERAGE, GOOD, HIGH, EXCELLENT
|
||||
altitude = 20000, -- Patrol altitude (feet)
|
||||
speed = 350, -- Patrol speed (knots)
|
||||
patrolTime = 25, -- Time on station (minutes)
|
||||
type = "FIGHTER",
|
||||
|
||||
-- Zone-based Areas of Responsibility (optional - leave nil for global response)
|
||||
primaryZone = "GUDAUTA_BORDER", -- Main responsibility area (zone name from mission editor)
|
||||
secondaryZone = "RED_BORDER", -- Secondary coverage area (zone name)
|
||||
tertiaryZone = nil, -- Emergency/fallback zone (zone name)
|
||||
|
||||
-- Zone behavior settings (optional - uses defaults if not specified)
|
||||
zoneConfig = {
|
||||
primaryResponse = 1.0, -- Intercept ratio multiplier in primary zone
|
||||
secondaryResponse = 0.6, -- Intercept ratio multiplier in secondary zone
|
||||
tertiaryResponse = 1.4, -- Intercept ratio multiplier in tertiary zone
|
||||
maxRange = 200, -- Maximum engagement range from airbase (nm)
|
||||
enableFallback = false, -- Auto-switch to tertiary when base threatened
|
||||
priorityThreshold = 4, -- Min aircraft count for "major threat"
|
||||
ignoreLowPriority = false, -- Ignore threats below threshold in secondary zones
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
}
|
||||
|
||||
-- ═══════════════════════════════════════════════════════════════════════════
|
||||
@ -185,10 +270,10 @@ BLUE_SQUADRON_CONFIG = {
|
||||
-- ADD YOUR BLUE SQUADRONS HERE
|
||||
|
||||
{
|
||||
templateName = "Kutaisi CAP", -- Change to your BLUE template name
|
||||
displayName = "Kutaisi CAP", -- Change to your preferred name
|
||||
airbaseName = "Kutaisi", -- Change to your BLUE airbase
|
||||
aircraft = 18, -- Adjust aircraft count
|
||||
templateName = "FIGHTER_SWEEP_BLUE_Luostari", -- Change to your BLUE template name
|
||||
displayName = "Luostari CAP", -- Change to your preferred name
|
||||
airbaseName = "Luostari Pechenga", -- Change to your BLUE airbase
|
||||
aircraft = 10, -- Adjust aircraft count
|
||||
skill = AI.Skill.EXCELLENT, -- AVERAGE, GOOD, HIGH, EXCELLENT
|
||||
altitude = 18000, -- Patrol altitude (feet)
|
||||
speed = 320, -- Patrol speed (knots)
|
||||
@ -196,7 +281,7 @@ BLUE_SQUADRON_CONFIG = {
|
||||
type = "FIGHTER",
|
||||
|
||||
-- Zone-based Areas of Responsibility (optional - leave nil for global response)
|
||||
primaryZone = "BLUE_BORDER", -- Main responsibility area (zone name from mission editor)
|
||||
primaryZone = "BLUE BORDER", -- Main responsibility area (zone name from mission editor)
|
||||
secondaryZone = nil, -- Secondary coverage area (zone name)
|
||||
tertiaryZone = nil, -- Emergency/fallback zone (zone name)
|
||||
|
||||
@ -213,10 +298,10 @@ BLUE_SQUADRON_CONFIG = {
|
||||
},
|
||||
|
||||
{
|
||||
templateName = "Batumi CAP", -- Change to your BLUE template name
|
||||
displayName = "Batumi CAP", -- Change to your preferred name
|
||||
airbaseName = "Batumi", -- Change to your BLUE airbase
|
||||
aircraft = 18, -- Adjust aircraft count
|
||||
templateName = "FIGHTER_SWEEP_BLUE_Ivalo", -- Change to your BLUE template name
|
||||
displayName = "Ivalo CAP", -- Change to your preferred name
|
||||
airbaseName = "Ivalo", -- Change to your BLUE airbase
|
||||
aircraft = 10, -- Adjust aircraft count
|
||||
skill = AI.Skill.EXCELLENT, -- AVERAGE, GOOD, HIGH, EXCELLENT
|
||||
altitude = 18000, -- Patrol altitude (feet)
|
||||
speed = 320, -- Patrol speed (knots)
|
||||
@ -224,9 +309,37 @@ BLUE_SQUADRON_CONFIG = {
|
||||
type = "FIGHTER",
|
||||
|
||||
-- Zone-based Areas of Responsibility (optional - leave nil for global response)
|
||||
primaryZone = "BATUMI_BORDER", -- Main responsibility area (zone name from mission editor)
|
||||
secondaryZone = "BLUE_BORDER", -- Secondary coverage area (zone name)
|
||||
tertiaryZone = "BATUMI_BORDER", -- Emergency/fallback zone (zone name)
|
||||
primaryZone = "BLUE BORDER", -- Main responsibility area (zone name from mission editor)
|
||||
secondaryZone = nil, -- Secondary coverage area (zone name)
|
||||
tertiaryZone = nil, -- Emergency/fallback zone (zone name)
|
||||
|
||||
-- Zone behavior settings (optional - uses defaults if not specified)
|
||||
zoneConfig = {
|
||||
primaryResponse = 1.0, -- Intercept ratio multiplier in primary zone
|
||||
secondaryResponse = 0.6, -- Intercept ratio multiplier in secondary zone
|
||||
tertiaryResponse = 1.4, -- Intercept ratio multiplier in tertiary zone
|
||||
maxRange = 200, -- Maximum engagement range from airbase (nm)
|
||||
enableFallback = true, -- Auto-switch to tertiary when base threatened
|
||||
priorityThreshold = 4, -- Min aircraft count for "major threat"
|
||||
ignoreLowPriority = false, -- Ignore threats below threshold in secondary zones
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
templateName = "FIGHTER_SWEEP_BLUE_Alakurtti", -- Change to your BLUE template name
|
||||
displayName = "Alakurtti CAP", -- Change to your preferred name
|
||||
airbaseName = "Alakurtti", -- Change to your BLUE airbase
|
||||
aircraft = 10, -- Adjust aircraft count
|
||||
skill = AI.Skill.EXCELLENT, -- AVERAGE, GOOD, HIGH, EXCELLENT
|
||||
altitude = 18000, -- Patrol altitude (feet)
|
||||
speed = 320, -- Patrol speed (knots)
|
||||
patrolTime = 22, -- Time on station (minutes)
|
||||
type = "FIGHTER",
|
||||
|
||||
-- Zone-based Areas of Responsibility (optional - leave nil for global response)
|
||||
primaryZone = "BLUE BORDER", -- Main responsibility area (zone name from mission editor)
|
||||
secondaryZone = nil, -- Secondary coverage area (zone name)
|
||||
tertiaryZone = nil, -- Emergency/fallback zone (zone name)
|
||||
|
||||
-- Zone behavior settings (optional - uses defaults if not specified)
|
||||
zoneConfig = {
|
||||
|
||||
613
DCS_Kola/Operation_Polar_Shield/Simple_TADC.lua
Normal file
613
DCS_Kola/Operation_Polar_Shield/Simple_TADC.lua
Normal file
@ -0,0 +1,613 @@
|
||||
-- Simple TADC - Just Works
|
||||
-- Detect blue aircraft, launch red fighters, make them intercept
|
||||
|
||||
-- Configuration
|
||||
local TADC_CONFIG = {
|
||||
checkInterval = 30, -- Check for threats every 30 seconds
|
||||
maxActiveCAP = 24, -- Max fighters airborne at once
|
||||
squadronCooldown = 900, -- Squadron cooldown after launch (15 minutes)
|
||||
interceptRatio = 0.8, -- Launch interceptors per threat (see chart below)
|
||||
}
|
||||
--[[
|
||||
INTERCEPT RATIO CHART - How many interceptors launch per threat aircraft:
|
||||
|
||||
Threat Size: 1 2 4 8 12 16 (aircraft)
|
||||
====================================================================
|
||||
interceptRatio 0.2: 1 1 1 2 3 4 (conservative)
|
||||
interceptRatio 0.5: 1 1 2 4 6 8 (light response)
|
||||
interceptRatio 0.8: 1 2 4 7 10 13 (balanced)
|
||||
interceptRatio 1.0: 1 2 4 8 12 16 (1:1 parity)
|
||||
interceptRatio 1.2: 2 3 5 10 15 20 (slight advantage)
|
||||
interceptRatio 1.4: 2 3 6 12 17 23 (good advantage) <- DEFAULT
|
||||
interceptRatio 1.6: 2 4 7 13 20 26 (strong response)
|
||||
interceptRatio 1.8: 2 4 8 15 22 29 (overwhelming)
|
||||
interceptRatio 2.0: 2 4 8 16 24 32 (overkill)
|
||||
|
||||
TACTICAL EFFECTS:
|
||||
• 0.2-0.5: Minimal response, may be overwhelmed by large formations
|
||||
• 0.8-1.0: Realistic parity, balanced dogfights
|
||||
• 1.2-1.4: Red advantage, good for challenging blue players
|
||||
• 1.6-1.8: Strong defense, difficult penetration
|
||||
• 1.9-2.0: Nearly impenetrable, may exhaust squadron pool quickly
|
||||
|
||||
SQUADRON IMPACT:
|
||||
• Low ratios (0.2-0.8): Squadrons available longer, sustained defense
|
||||
• High ratios (1.6-2.0): Rapid squadron depletion, gaps in coverage
|
||||
• Sweet spot (1.0-1.4): Balanced response with good coverage duration
|
||||
--]]
|
||||
|
||||
-- Define squadron configurations with their designated airbases and patrol zones
|
||||
local squadronConfigs = {
|
||||
-- Fixed-wing fighters patrol RED BORDER zone
|
||||
{
|
||||
templateName = "FIGHTER_SWEEP_RED_Kilpyavr",
|
||||
displayName = "Kilpyavr CAP",
|
||||
airbaseName = "Kilpyavr",
|
||||
aircraft = 12, -- Maximum aircraft in squadron
|
||||
skill = AI.Skill.GOOD,
|
||||
altitude = 15000,
|
||||
speed = 300,
|
||||
patrolTime = 20,
|
||||
type = "FIGHTER"
|
||||
},
|
||||
{
|
||||
templateName = "FIGHTER_SWEEP_RED_Severomorsk-1",
|
||||
displayName = "Severomorsk-1 CAP",
|
||||
airbaseName = "Severomorsk-1",
|
||||
aircraft = 16, -- Maximum aircraft in squadron
|
||||
skill = AI.Skill.GOOD,
|
||||
altitude = 20000,
|
||||
speed = 350,
|
||||
patrolTime = 25,
|
||||
type = "FIGHTER"
|
||||
},
|
||||
{
|
||||
templateName = "FIGHTER_SWEEP_RED_Severomorsk-3",
|
||||
displayName = "Severomorsk-3 CAP",
|
||||
airbaseName = "Severomorsk-3",
|
||||
aircraft = 14, -- Maximum aircraft in squadron
|
||||
skill = AI.Skill.GOOD,
|
||||
altitude = 25000,
|
||||
speed = 400,
|
||||
patrolTime = 30,
|
||||
type = "FIGHTER"
|
||||
},
|
||||
{
|
||||
templateName = "FIGHTER_SWEEP_RED_Murmansk",
|
||||
displayName = "Murmansk CAP",
|
||||
airbaseName = "Murmansk International",
|
||||
aircraft = 18, -- Maximum aircraft in squadron
|
||||
skill = AI.Skill.GOOD,
|
||||
altitude = 18000,
|
||||
speed = 320,
|
||||
patrolTime = 22,
|
||||
type = "FIGHTER"
|
||||
},
|
||||
{
|
||||
templateName = "FIGHTER_SWEEP_RED_Monchegorsk",
|
||||
displayName = "Monchegorsk CAP",
|
||||
airbaseName = "Monchegorsk",
|
||||
aircraft = 10, -- Maximum aircraft in squadron
|
||||
skill = AI.Skill.GOOD,
|
||||
altitude = 22000,
|
||||
speed = 380,
|
||||
patrolTime = 25,
|
||||
type = "FIGHTER"
|
||||
},
|
||||
{
|
||||
templateName = "FIGHTER_SWEEP_RED_Olenya",
|
||||
displayName = "Olenya CAP",
|
||||
airbaseName = "Olenya",
|
||||
aircraft = 20, -- Maximum aircraft in squadron
|
||||
skill = AI.Skill.GOOD,
|
||||
altitude = 30000,
|
||||
speed = 450,
|
||||
patrolTime = 35,
|
||||
type = "FIGHTER"
|
||||
},
|
||||
--[[]
|
||||
-- Helicopter squadron patrols HELO BORDER zone
|
||||
{
|
||||
templateName = "HELO_SWEEP_RED_Afrikanda",
|
||||
displayName = "Afrikanda Helo CAP",
|
||||
airbaseName = "Afrikanda",
|
||||
aircraft = 4,
|
||||
skill = AI.Skill.GOOD,
|
||||
altitude = 1000,
|
||||
speed = 150,
|
||||
patrolTime = 30,
|
||||
type = "HELICOPTER"
|
||||
}
|
||||
--]]
|
||||
}
|
||||
|
||||
-- Track active missions
|
||||
local activeInterceptors = {}
|
||||
local lastLaunchTime = {}
|
||||
local assignedThreats = {} -- Track which threats already have interceptors assigned
|
||||
local squadronCooldowns = {} -- Track squadron cooldowns after launch
|
||||
|
||||
-- Squadron aircraft tracking
|
||||
local squadronAircraftCounts = {} -- Current available aircraft per squadron
|
||||
local cargoReplenishmentAmount = 4 -- Aircraft added per cargo delivery
|
||||
|
||||
-- Initialize squadron aircraft counts
|
||||
for _, squadron in pairs(squadronConfigs) do
|
||||
squadronAircraftCounts[squadron.templateName] = squadron.aircraft
|
||||
end
|
||||
|
||||
-- Simple logging
|
||||
local function log(message)
|
||||
env.info("[Simple TADC] " .. message)
|
||||
end
|
||||
|
||||
-- Monitor cargo aircraft landings for squadron replenishment
|
||||
local function monitorCargoReplenishment()
|
||||
-- Find all red cargo aircraft
|
||||
local redCargo = SET_GROUP:New():FilterCoalitions("red"):FilterCategoryAirplane():FilterStart()
|
||||
|
||||
redCargo:ForEach(function(cargoGroup)
|
||||
if cargoGroup and cargoGroup:IsAlive() then
|
||||
-- Check if cargo aircraft contains "CARGO" or "TRANSPORT" in name
|
||||
local cargoName = cargoGroup:GetName():upper()
|
||||
if string.find(cargoName, "CARGO") or string.find(cargoName, "TRANSPORT") or
|
||||
string.find(cargoName, "C130") or string.find(cargoName, "C-130") or
|
||||
string.find(cargoName, "AN26") or string.find(cargoName, "AN-26") then
|
||||
|
||||
-- Check if landed at any squadron airbase
|
||||
local cargoCoord = cargoGroup:GetCoordinate()
|
||||
local cargoVelocity = cargoGroup:GetVelocityKMH()
|
||||
|
||||
-- Consider aircraft "landed" if velocity is very low
|
||||
if cargoVelocity < 5 then
|
||||
-- Check which airbase it's near
|
||||
for _, squadron in pairs(squadronConfigs) do
|
||||
local airbase = AIRBASE:FindByName(squadron.airbaseName)
|
||||
if airbase and airbase:GetCoalition() == coalition.side.RED then
|
||||
local airbaseCoord = airbase:GetCoordinate()
|
||||
local distance = cargoCoord:Get2DDistance(airbaseCoord)
|
||||
|
||||
-- If within 3km of airbase, consider it a delivery
|
||||
if distance < 3000 then
|
||||
-- Check if we haven't already processed this delivery
|
||||
local deliveryKey = cargoName .. "_" .. squadron.airbaseName
|
||||
if not _G.processedDeliveries then
|
||||
_G.processedDeliveries = {}
|
||||
end
|
||||
|
||||
if not _G.processedDeliveries[deliveryKey] then
|
||||
-- Process replenishment
|
||||
local currentCount = squadronAircraftCounts[squadron.templateName] or 0
|
||||
local maxCount = squadron.aircraft
|
||||
local newCount = math.min(currentCount + cargoReplenishmentAmount, maxCount)
|
||||
local actualAdded = newCount - currentCount
|
||||
|
||||
if actualAdded > 0 then
|
||||
squadronAircraftCounts[squadron.templateName] = newCount
|
||||
log("CARGO DELIVERY: " .. cargoName .. " delivered " .. actualAdded ..
|
||||
" aircraft to " .. squadron.displayName ..
|
||||
" (" .. newCount .. "/" .. maxCount .. ")")
|
||||
|
||||
-- Mark delivery as processed
|
||||
_G.processedDeliveries[deliveryKey] = timer.getTime()
|
||||
else
|
||||
log("CARGO DELIVERY: " .. squadron.displayName .. " already at max capacity")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
-- Send interceptor back to base
|
||||
local function sendInterceptorHome(interceptor)
|
||||
if not interceptor or not interceptor:IsAlive() then
|
||||
return
|
||||
end
|
||||
|
||||
-- Find nearest friendly airbase
|
||||
local interceptorCoord = interceptor:GetCoordinate()
|
||||
local nearestAirbase = nil
|
||||
local shortestDistance = math.huge
|
||||
|
||||
-- Check all squadron airbases to find the nearest one that's still friendly
|
||||
for _, squadron in pairs(squadronConfigs) do
|
||||
local airbase = AIRBASE:FindByName(squadron.airbaseName)
|
||||
if airbase and airbase:GetCoalition() == coalition.side.RED and airbase:IsAlive() then
|
||||
local airbaseCoord = airbase:GetCoordinate()
|
||||
local distance = interceptorCoord:Get2DDistance(airbaseCoord)
|
||||
if distance < shortestDistance then
|
||||
shortestDistance = distance
|
||||
nearestAirbase = airbase
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if nearestAirbase then
|
||||
local airbaseCoord = nearestAirbase:GetCoordinate()
|
||||
local rtbAltitude = 3000 -- RTB at 3000 feet
|
||||
local rtbCoord = airbaseCoord:SetAltitude(rtbAltitude * 0.3048) -- Convert feet to meters
|
||||
|
||||
-- Clear current tasks and route home
|
||||
interceptor:ClearTasks()
|
||||
interceptor:RouteAirTo(rtbCoord, 250 * 0.5144, "BARO") -- RTB at 250 knots
|
||||
|
||||
log("Sending " .. interceptor:GetName() .. " back to " .. nearestAirbase:GetName())
|
||||
|
||||
-- Schedule cleanup after they should have landed (give them time to get home)
|
||||
local flightTime = math.ceil(shortestDistance / (250 * 0.5144)) + 300 -- Flight time + 5 min buffer
|
||||
SCHEDULER:New(nil, function()
|
||||
if activeInterceptors[interceptor:GetName()] then
|
||||
activeInterceptors[interceptor:GetName()] = nil
|
||||
log("Cleaned up " .. interceptor:GetName() .. " after RTB")
|
||||
end
|
||||
end, {}, flightTime)
|
||||
else
|
||||
log("No friendly airbase found for " .. interceptor:GetName() .. ", will clean up normally")
|
||||
end
|
||||
end
|
||||
|
||||
-- Check if airbase is still usable
|
||||
local function isAirbaseUsable(airbaseName)
|
||||
local airbase = AIRBASE:FindByName(airbaseName)
|
||||
if not airbase then
|
||||
return false, "not found"
|
||||
elseif airbase:GetCoalition() ~= coalition.side.RED then
|
||||
return false, "captured by " .. (airbase:GetCoalition() == coalition.side.BLUE and "Blue" or "Neutral")
|
||||
elseif not airbase:IsAlive() then
|
||||
return false, "destroyed"
|
||||
else
|
||||
return true, "operational"
|
||||
end
|
||||
end
|
||||
|
||||
-- Count active red fighters
|
||||
local function countActiveFighters()
|
||||
local count = 0
|
||||
for _, interceptorData in pairs(activeInterceptors) do
|
||||
if interceptorData and interceptorData.group and interceptorData.group:IsAlive() then
|
||||
count = count + interceptorData.group:GetSize()
|
||||
end
|
||||
end
|
||||
return count
|
||||
end
|
||||
|
||||
-- Find best squadron to launch
|
||||
local function findBestSquadron(threatCoord)
|
||||
local bestSquadron = nil
|
||||
local shortestDistance = math.huge
|
||||
local currentTime = timer.getTime()
|
||||
|
||||
for _, squadron in pairs(squadronConfigs) do
|
||||
-- Check if squadron is on cooldown
|
||||
local squadronAvailable = true
|
||||
if squadronCooldowns[squadron.templateName] then
|
||||
local cooldownEnd = squadronCooldowns[squadron.templateName]
|
||||
if currentTime < cooldownEnd then
|
||||
local timeLeft = math.ceil((cooldownEnd - currentTime) / 60)
|
||||
log("Squadron " .. squadron.displayName .. " on cooldown for " .. timeLeft .. " more minutes")
|
||||
squadronAvailable = false
|
||||
else
|
||||
-- Cooldown expired, remove it
|
||||
squadronCooldowns[squadron.templateName] = nil
|
||||
log("Squadron " .. squadron.displayName .. " cooldown expired, available for launch")
|
||||
end
|
||||
end
|
||||
|
||||
if squadronAvailable then
|
||||
-- Check if squadron has available aircraft
|
||||
local availableAircraft = squadronAircraftCounts[squadron.templateName] or 0
|
||||
if availableAircraft <= 0 then
|
||||
log("Squadron " .. squadron.displayName .. " has no aircraft available (" .. availableAircraft .. "/" .. squadron.aircraft .. ")")
|
||||
squadronAvailable = false
|
||||
end
|
||||
end
|
||||
|
||||
if squadronAvailable then
|
||||
-- Check if airbase is still under Red control
|
||||
local airbase = AIRBASE:FindByName(squadron.airbaseName)
|
||||
if not airbase then
|
||||
log("Warning: Airbase " .. squadron.airbaseName .. " not found")
|
||||
elseif airbase:GetCoalition() ~= coalition.side.RED then
|
||||
log("Warning: Airbase " .. squadron.airbaseName .. " no longer under Red control")
|
||||
elseif not airbase:IsAlive() then
|
||||
log("Warning: Airbase " .. squadron.airbaseName .. " is destroyed")
|
||||
else
|
||||
-- Airbase is valid, check if squadron can spawn
|
||||
local spawn = SPAWN:New(squadron.templateName)
|
||||
if spawn then
|
||||
-- Get squadron's airbase
|
||||
local template = GROUP:FindByName(squadron.templateName)
|
||||
if template then
|
||||
local airbaseCoord = template:GetCoordinate()
|
||||
if airbaseCoord then
|
||||
local distance = airbaseCoord:Get2DDistance(threatCoord)
|
||||
if distance < shortestDistance then
|
||||
shortestDistance = distance
|
||||
bestSquadron = squadron
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return bestSquadron
|
||||
end
|
||||
|
||||
-- Launch interceptor
|
||||
local function launchInterceptor(threatGroup)
|
||||
if not threatGroup or not threatGroup:IsAlive() then
|
||||
return
|
||||
end
|
||||
|
||||
local threatCoord = threatGroup:GetCoordinate()
|
||||
local threatName = threatGroup:GetName()
|
||||
local threatSize = threatGroup:GetSize() -- Get the number of aircraft in the threat group
|
||||
|
||||
-- Check if threat already has interceptors assigned
|
||||
if assignedThreats[threatName] then
|
||||
local assignedInterceptors = assignedThreats[threatName]
|
||||
local aliveCount = 0
|
||||
|
||||
-- Check if assigned interceptors are still alive
|
||||
if type(assignedInterceptors) == "table" then
|
||||
for _, interceptor in pairs(assignedInterceptors) do
|
||||
if interceptor and interceptor:IsAlive() then
|
||||
aliveCount = aliveCount + 1
|
||||
end
|
||||
end
|
||||
else
|
||||
-- Handle legacy single interceptor assignment
|
||||
if assignedInterceptors and assignedInterceptors:IsAlive() then
|
||||
aliveCount = 1
|
||||
end
|
||||
end
|
||||
|
||||
if aliveCount > 0 then
|
||||
return -- Still being intercepted
|
||||
else
|
||||
-- All interceptors are dead, clear the assignment
|
||||
assignedThreats[threatName] = nil
|
||||
end
|
||||
end
|
||||
|
||||
-- Calculate how many interceptors to launch (at least match threat size, up to ratio)
|
||||
local interceptorsNeeded = math.max(threatSize, math.ceil(threatSize * TADC_CONFIG.interceptRatio))
|
||||
|
||||
-- Check if we have capacity
|
||||
if countActiveFighters() + interceptorsNeeded > TADC_CONFIG.maxActiveCAP then
|
||||
interceptorsNeeded = TADC_CONFIG.maxActiveCAP - countActiveFighters()
|
||||
if interceptorsNeeded <= 0 then
|
||||
log("Max fighters airborne, skipping launch")
|
||||
return
|
||||
end
|
||||
end
|
||||
|
||||
-- Find best squadron
|
||||
local squadron = findBestSquadron(threatCoord)
|
||||
if not squadron then
|
||||
log("No squadron available")
|
||||
return
|
||||
end
|
||||
|
||||
-- Limit interceptors to available aircraft
|
||||
local availableAircraft = squadronAircraftCounts[squadron.templateName] or 0
|
||||
interceptorsNeeded = math.min(interceptorsNeeded, availableAircraft)
|
||||
|
||||
if interceptorsNeeded <= 0 then
|
||||
log("Squadron " .. squadron.displayName .. " has no aircraft to launch")
|
||||
return
|
||||
end
|
||||
|
||||
-- Launch multiple interceptors to match threat
|
||||
local spawn = SPAWN:New(squadron.templateName)
|
||||
local interceptors = {}
|
||||
|
||||
for i = 1, interceptorsNeeded do
|
||||
local interceptor = spawn:Spawn()
|
||||
|
||||
if interceptor then
|
||||
table.insert(interceptors, interceptor)
|
||||
|
||||
-- Wait a moment for initialization
|
||||
SCHEDULER:New(nil, function()
|
||||
if interceptor and interceptor:IsAlive() then
|
||||
-- Set aggressive AI
|
||||
interceptor:OptionROEOpenFire()
|
||||
interceptor:OptionROTVertical()
|
||||
|
||||
-- Route to threat
|
||||
local currentThreatCoord = threatGroup:GetCoordinate()
|
||||
if currentThreatCoord then
|
||||
local interceptCoord = currentThreatCoord:SetAltitude(squadron.altitude * 0.3048) -- Convert feet to meters
|
||||
interceptor:RouteAirTo(interceptCoord, squadron.speed * 0.5144, "BARO") -- Convert kts to m/s
|
||||
|
||||
-- Attack the threat
|
||||
local attackTask = {
|
||||
id = 'AttackGroup',
|
||||
params = {
|
||||
groupId = threatGroup:GetID(),
|
||||
weaponType = 'Auto',
|
||||
attackQtyLimit = 0,
|
||||
priority = 1
|
||||
}
|
||||
}
|
||||
interceptor:PushTask(attackTask, 1)
|
||||
end
|
||||
end
|
||||
end, {}, 3)
|
||||
|
||||
-- Track the interceptor with squadron info
|
||||
activeInterceptors[interceptor:GetName()] = {
|
||||
group = interceptor,
|
||||
squadron = squadron.templateName,
|
||||
displayName = squadron.displayName
|
||||
}
|
||||
|
||||
-- Emergency cleanup (safety net - should normally RTB before this)
|
||||
SCHEDULER:New(nil, function()
|
||||
if activeInterceptors[interceptor:GetName()] then
|
||||
log("Emergency cleanup of " .. interceptor:GetName() .. " (should have RTB'd)")
|
||||
activeInterceptors[interceptor:GetName()] = nil
|
||||
end
|
||||
end, {}, 7200) -- Emergency cleanup after 2 hours
|
||||
end
|
||||
end
|
||||
|
||||
-- Log the launch and track assignment
|
||||
if #interceptors > 0 then
|
||||
-- Decrement squadron aircraft count
|
||||
local currentCount = squadronAircraftCounts[squadron.templateName] or 0
|
||||
squadronAircraftCounts[squadron.templateName] = math.max(0, currentCount - #interceptors)
|
||||
local remainingCount = squadronAircraftCounts[squadron.templateName]
|
||||
|
||||
log("Launched " .. #interceptors .. " x " .. squadron.displayName .. " to intercept " ..
|
||||
threatSize .. " x " .. threatName .. " (Remaining: " .. remainingCount .. "/" .. squadron.aircraft .. ")")
|
||||
assignedThreats[threatName] = interceptors -- Track which interceptors are assigned to this threat
|
||||
lastLaunchTime[threatName] = timer.getTime()
|
||||
|
||||
-- Apply cooldown immediately when squadron launches
|
||||
local currentTime = timer.getTime()
|
||||
squadronCooldowns[squadron.templateName] = currentTime + TADC_CONFIG.squadronCooldown
|
||||
local cooldownMinutes = TADC_CONFIG.squadronCooldown / 60
|
||||
log("Squadron " .. squadron.displayName .. " LAUNCHED! Applying " .. cooldownMinutes .. " minute cooldown")
|
||||
end
|
||||
end
|
||||
|
||||
-- Main threat detection loop
|
||||
local function detectThreats()
|
||||
log("Scanning for threats...")
|
||||
|
||||
-- Clean up dead threats from tracking
|
||||
local currentThreats = {}
|
||||
|
||||
-- Find all blue aircraft
|
||||
local blueAircraft = SET_GROUP:New():FilterCoalitions("blue"):FilterCategoryAirplane():FilterStart()
|
||||
local threatCount = 0
|
||||
|
||||
blueAircraft:ForEach(function(blueGroup)
|
||||
if blueGroup and blueGroup:IsAlive() then
|
||||
threatCount = threatCount + 1
|
||||
currentThreats[blueGroup:GetName()] = true
|
||||
log("Found threat: " .. blueGroup:GetName() .. " (" .. blueGroup:GetTypeName() .. ")")
|
||||
|
||||
-- Launch interceptor for this threat
|
||||
launchInterceptor(blueGroup)
|
||||
end
|
||||
end)
|
||||
|
||||
-- Clean up assignments for threats that no longer exist and send interceptors home
|
||||
for threatName, assignedInterceptors in pairs(assignedThreats) do
|
||||
if not currentThreats[threatName] then
|
||||
log("Threat " .. threatName .. " eliminated, sending interceptors home...")
|
||||
|
||||
-- Send assigned interceptors back to base
|
||||
if type(assignedInterceptors) == "table" then
|
||||
for _, interceptor in pairs(assignedInterceptors) do
|
||||
if interceptor and interceptor:IsAlive() then
|
||||
sendInterceptorHome(interceptor)
|
||||
end
|
||||
end
|
||||
else
|
||||
-- Handle legacy single interceptor assignment
|
||||
if assignedInterceptors and assignedInterceptors:IsAlive() then
|
||||
sendInterceptorHome(assignedInterceptors)
|
||||
end
|
||||
end
|
||||
|
||||
assignedThreats[threatName] = nil
|
||||
end
|
||||
end
|
||||
|
||||
-- Count assigned threats
|
||||
local assignedCount = 0
|
||||
for _ in pairs(assignedThreats) do assignedCount = assignedCount + 1 end
|
||||
|
||||
log("Scan complete: " .. threatCount .. " threats, " .. countActiveFighters() .. " active fighters, " ..
|
||||
assignedCount .. " assigned")
|
||||
end
|
||||
|
||||
-- Monitor interceptor groups for cleanup when destroyed
|
||||
local function monitorInterceptors()
|
||||
-- Check all active interceptors for cleanup
|
||||
for interceptorName, interceptorData in pairs(activeInterceptors) do
|
||||
if interceptorData and interceptorData.group then
|
||||
if not interceptorData.group:IsAlive() then
|
||||
-- Interceptor group is destroyed - just clean up tracking
|
||||
local displayName = interceptorData.displayName
|
||||
log("Interceptor from " .. displayName .. " destroyed: " .. interceptorName)
|
||||
|
||||
-- Remove from active tracking
|
||||
activeInterceptors[interceptorName] = nil
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Periodic airbase status check
|
||||
local function checkAirbaseStatus()
|
||||
log("=== AIRBASE STATUS REPORT ===")
|
||||
local usableCount = 0
|
||||
local currentTime = timer.getTime()
|
||||
|
||||
for _, squadron in pairs(squadronConfigs) do
|
||||
local usable, status = isAirbaseUsable(squadron.airbaseName)
|
||||
|
||||
-- Add aircraft count to status
|
||||
local aircraftCount = squadronAircraftCounts[squadron.templateName] or 0
|
||||
local maxAircraft = squadron.aircraft
|
||||
local aircraftStatus = " Aircraft: " .. aircraftCount .. "/" .. maxAircraft
|
||||
|
||||
-- Check if squadron is on cooldown
|
||||
local cooldownStatus = ""
|
||||
if squadronCooldowns[squadron.templateName] then
|
||||
local cooldownEnd = squadronCooldowns[squadron.templateName]
|
||||
if currentTime < cooldownEnd then
|
||||
local timeLeft = math.ceil((cooldownEnd - currentTime) / 60)
|
||||
cooldownStatus = " (COOLDOWN: " .. timeLeft .. "m)"
|
||||
end
|
||||
end
|
||||
|
||||
local fullStatus = status .. aircraftStatus .. cooldownStatus
|
||||
|
||||
if usable and cooldownStatus == "" and aircraftCount > 0 then
|
||||
usableCount = usableCount + 1
|
||||
log("✓ " .. squadron.airbaseName .. " - " .. fullStatus)
|
||||
else
|
||||
log("✗ " .. squadron.airbaseName .. " - " .. fullStatus)
|
||||
end
|
||||
end
|
||||
|
||||
log("Status: " .. usableCount .. "/" .. #squadronConfigs .. " airbases operational")
|
||||
end
|
||||
|
||||
-- Start the system
|
||||
log("Simple TADC starting...")
|
||||
log("Squadrons configured: " .. #squadronConfigs)
|
||||
|
||||
-- Run detection every interval
|
||||
SCHEDULER:New(nil, detectThreats, {}, 5, TADC_CONFIG.checkInterval)
|
||||
|
||||
-- Run interceptor monitoring every 30 seconds
|
||||
SCHEDULER:New(nil, monitorInterceptors, {}, 10, 30)
|
||||
|
||||
-- Run airbase status check every 2 minutes
|
||||
SCHEDULER:New(nil, checkAirbaseStatus, {}, 30, 120)
|
||||
|
||||
-- Monitor cargo aircraft for squadron replenishment every 15 seconds
|
||||
SCHEDULER:New(nil, monitorCargoReplenishment, {}, 15, 15)
|
||||
|
||||
log("Simple TADC operational!")
|
||||
log("Aircraft replenishment: " .. cargoReplenishmentAmount .. " aircraft per cargo delivery")
|
||||
|
||||
-- Log initial squadron aircraft counts
|
||||
for _, squadron in pairs(squadronConfigs) do
|
||||
local count = squadronAircraftCounts[squadron.templateName]
|
||||
log("Initial: " .. squadron.displayName .. " has " .. count .. "/" .. squadron.aircraft .. " aircraft")
|
||||
end
|
||||
Loading…
x
Reference in New Issue
Block a user