diff --git a/DCS_Kola/Operation_Polar_Shield/F99th-Operation_Polar_Shield_1.0.5.miz b/DCS_Kola/Operation_Polar_Shield/F99th-Operation_Polar_Shield_1.0.5.miz index 3190f50..8933efc 100644 Binary files a/DCS_Kola/Operation_Polar_Shield/F99th-Operation_Polar_Shield_1.0.5.miz and b/DCS_Kola/Operation_Polar_Shield/F99th-Operation_Polar_Shield_1.0.5.miz differ diff --git a/DCS_Kola/Operation_Polar_Shield/F99th-Operation_Polar_Shield_1.0.6.miz b/DCS_Kola/Operation_Polar_Shield/F99th-Operation_Polar_Shield_1.0.6.miz new file mode 100644 index 0000000..a52d663 Binary files /dev/null and b/DCS_Kola/Operation_Polar_Shield/F99th-Operation_Polar_Shield_1.0.6.miz differ diff --git a/DCS_Kola/Operation_Polar_Shield/Simple_TADC.lua b/DCS_Kola/Operation_Polar_Shield/Simple_TADC.lua index 856a6dd..473f982 100644 --- a/DCS_Kola/Operation_Polar_Shield/Simple_TADC.lua +++ b/DCS_Kola/Operation_Polar_Shield/Simple_TADC.lua @@ -5,8 +5,8 @@ local TADC_CONFIG = { checkInterval = 30, -- Check for threats every 30 seconds maxActiveCAP = 24, -- Max fighters airborne at once - squadronCooldown = 1800, -- Squadron cooldown after launch (30 minutes) - interceptRatio = 0.5, -- Launch interceptors per threat (see chart below) + 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: @@ -43,7 +43,7 @@ local squadronConfigs = { templateName = "FIGHTER_SWEEP_RED_Kilpyavr", displayName = "Kilpyavr CAP", airbaseName = "Kilpyavr", - aircraft = 1, + aircraft = 12, -- Maximum aircraft in squadron skill = AI.Skill.GOOD, altitude = 15000, speed = 300, @@ -54,7 +54,7 @@ local squadronConfigs = { templateName = "FIGHTER_SWEEP_RED_Severomorsk-1", displayName = "Severomorsk-1 CAP", airbaseName = "Severomorsk-1", - aircraft = 1, + aircraft = 16, -- Maximum aircraft in squadron skill = AI.Skill.GOOD, altitude = 20000, speed = 350, @@ -65,7 +65,7 @@ local squadronConfigs = { templateName = "FIGHTER_SWEEP_RED_Severomorsk-3", displayName = "Severomorsk-3 CAP", airbaseName = "Severomorsk-3", - aircraft = 1, + aircraft = 14, -- Maximum aircraft in squadron skill = AI.Skill.GOOD, altitude = 25000, speed = 400, @@ -76,7 +76,7 @@ local squadronConfigs = { templateName = "FIGHTER_SWEEP_RED_Murmansk", displayName = "Murmansk CAP", airbaseName = "Murmansk International", - aircraft = 1, + aircraft = 18, -- Maximum aircraft in squadron skill = AI.Skill.GOOD, altitude = 18000, speed = 320, @@ -87,7 +87,7 @@ local squadronConfigs = { templateName = "FIGHTER_SWEEP_RED_Monchegorsk", displayName = "Monchegorsk CAP", airbaseName = "Monchegorsk", - aircraft = 1, + aircraft = 10, -- Maximum aircraft in squadron skill = AI.Skill.GOOD, altitude = 22000, speed = 380, @@ -98,7 +98,7 @@ local squadronConfigs = { templateName = "FIGHTER_SWEEP_RED_Olenya", displayName = "Olenya CAP", airbaseName = "Olenya", - aircraft = 1, + aircraft = 20, -- Maximum aircraft in squadron skill = AI.Skill.GOOD, altitude = 30000, speed = 450, @@ -125,13 +125,84 @@ local squadronConfigs = { local activeInterceptors = {} local lastLaunchTime = {} local assignedThreats = {} -- Track which threats already have interceptors assigned -local squadronCooldowns = {} -- Track squadron cooldowns after destruction +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 @@ -227,6 +298,15 @@ local function findBestSquadron(threatCoord) 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) @@ -316,6 +396,15 @@ local function launchInterceptor(threatGroup) 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 = {} @@ -373,8 +462,13 @@ local function launchInterceptor(threatGroup) -- 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) + threatSize .. " x " .. threatName .. " (Remaining: " .. remainingCount .. "/" .. squadron.aircraft .. ")") assignedThreats[threatName] = interceptors -- Track which interceptors are assigned to this threat lastLaunchTime[threatName] = timer.getTime() @@ -465,6 +559,11 @@ local function checkAirbaseStatus() 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 @@ -472,15 +571,16 @@ local function checkAirbaseStatus() if currentTime < cooldownEnd then local timeLeft = math.ceil((cooldownEnd - currentTime) / 60) cooldownStatus = " (COOLDOWN: " .. timeLeft .. "m)" - status = status .. cooldownStatus end end - if usable and cooldownStatus == "" then + local fullStatus = status .. aircraftStatus .. cooldownStatus + + if usable and cooldownStatus == "" and aircraftCount > 0 then usableCount = usableCount + 1 - log("✓ " .. squadron.airbaseName .. " - " .. status) + log("✓ " .. squadron.airbaseName .. " - " .. fullStatus) else - log("✗ " .. squadron.airbaseName .. " - " .. status) + log("✗ " .. squadron.airbaseName .. " - " .. fullStatus) end end @@ -500,4 +600,14 @@ SCHEDULER:New(nil, monitorInterceptors, {}, 10, 30) -- Run airbase status check every 2 minutes SCHEDULER:New(nil, checkAirbaseStatus, {}, 30, 120) -log("Simple TADC operational!") \ No newline at end of file +-- 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 \ No newline at end of file diff --git a/Universal_TADC.lua b/Universal_TADC.lua new file mode 100644 index 0000000..f8d6a63 --- /dev/null +++ b/Universal_TADC.lua @@ -0,0 +1,953 @@ +--[[ +═══════════════════════════════════════════════════════════════════════════════ + UNIVERSAL TADC + Dual-Coalition Tactical Air Defense Controller +═══════════════════════════════════════════════════════════════════════════════ + +DESCRIPTION: +This script provides an automated air defense system for BOTH RED and BLUE coalitions. +Each side detects enemy aircraft and launches interceptors independently. Perfect for +creating dynamic air-to-air combat scenarios or testing AI vs AI engagements. + +FEATURES: +• Dual-coalition support (RED and BLUE operate independently) +• Automatic threat detection and response for both sides +• Multiple squadron management with individual cooldowns per side +• Aircraft inventory tracking and cargo replenishment +• Configurable intercept ratios and response patterns per coalition +• Smart interceptor routing and RTB behavior +• Airbase status monitoring (operational/captured/destroyed) +• Comprehensive logging and status reports +• Asymmetric warfare support (different capabilities per side) + +SETUP INSTRUCTIONS: +1. Create fighter aircraft templates for BOTH coalitions in the mission editor +2. Configure RED squadrons in RED_SQUADRON_CONFIG section below +3. Configure BLUE squadrons in BLUE_SQUADRON_CONFIG section below +4. Set desired behavior for each coalition in TADC_SETTINGS +5. Add this script as a "DO SCRIPT" trigger at mission start +6. Optionally create cargo aircraft with standard naming for replenishment + +REQUIREMENTS: +• MOOSE framework (https://github.com/FlightControl-Master/MOOSE) +• Fighter aircraft templates must exist for each coalition +• Airbases must be under correct coalition control to launch + +TACTICAL SCENARIOS: +• Balanced air warfare (equal capabilities) +• Asymmetric scenarios (one side stronger/weaker) +• Training missions (AI vs AI for observation) +• Dynamic frontline battles with air support + +═══════════════════════════════════════════════════════════════════════════════ +]] + +--[[ +═══════════════════════════════════════════════════════════════════════════════ + MAIN SETTINGS +═══════════════════════════════════════════════════════════════════════════════ +]] + +-- Core TADC behavior settings - applies to BOTH coalitions unless overridden +local TADC_SETTINGS = { + -- Enable/Disable coalitions + enableRed = true, -- Set to false to disable RED TADC + enableBlue = true, -- Set to false to disable BLUE TADC + + -- Timing settings (applies to both coalitions) + checkInterval = 30, -- How often to scan for threats (seconds) + monitorInterval = 30, -- How often to check interceptor status (seconds) + statusReportInterval = 120, -- How often to report airbase status (seconds) + cargoCheckInterval = 15, -- How often to check for cargo deliveries (seconds) + + -- RED Coalition Settings + red = { + maxActiveCAP = 24, -- Maximum RED fighters airborne at once + squadronCooldown = 900, -- RED cooldown after squadron launch (seconds) + interceptRatio = 0.8, -- RED interceptors per threat aircraft + cargoReplenishmentAmount = 4, -- RED aircraft added per cargo delivery + emergencyCleanupTime = 7200, -- RED force cleanup time (seconds) + rtbFlightBuffer = 300, -- RED extra landing time before cleanup (seconds) + }, + + -- BLUE Coalition Settings + blue = { + maxActiveCAP = 24, -- Maximum BLUE fighters airborne at once + squadronCooldown = 900, -- BLUE cooldown after squadron launch (seconds) + interceptRatio = 0.8, -- BLUE interceptors per threat aircraft + cargoReplenishmentAmount = 4, -- BLUE aircraft added per cargo delivery + emergencyCleanupTime = 7200, -- BLUE force cleanup time (seconds) + rtbFlightBuffer = 300, -- BLUE extra landing time before cleanup (seconds) + }, +} + +--[[ +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) <- DEFAULT +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) +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, creates balanced dogfights +• 1.2-1.4: Coalition advantage, challenging for enemy +• 1.6-1.8: Strong defense, difficult penetration missions +• 1.9-2.0: Nearly impenetrable, may exhaust squadrons quickly + +SQUADRON IMPACT: +• Low ratios (0.2-0.8): Squadrons last longer, sustained defense +• High ratios (1.6-2.0): Rapid squadron depletion, coverage gaps +• Sweet spot (1.0-1.4): Balanced response with good coverage duration + +ASYMMETRIC SCENARIOS: +• Set RED ratio 1.2, BLUE ratio 0.8 = RED advantage +• Set RED ratio 0.6, BLUE ratio 1.4 = BLUE advantage +• Different maxActiveCAP values create capacity imbalances +]] + +--[[ +═══════════════════════════════════════════════════════════════════════════════ + SQUADRON CONFIGURATION +═══════════════════════════════════════════════════════════════════════════════ + +INSTRUCTIONS: +1. Create fighter aircraft templates for BOTH coalitions in the mission editor +2. Place them at or near the airbases you want them to operate from +3. Configure RED squadrons in RED_SQUADRON_CONFIG +4. Configure BLUE squadrons in BLUE_SQUADRON_CONFIG + +TEMPLATE NAMING SUGGESTIONS: +• RED: "RED_CAP_Batumi_F15", "RED_INTERCEPT_Senaki_MiG29" +• BLUE: "BLUE_CAP_Nellis_F16", "BLUE_INTERCEPT_Creech_F22" +• Include coalition and airbase name for easy identification + +AIRBASE NAMES: +• Use exact names as they appear in DCS (case sensitive) +• RED examples: "Batumi", "Senaki", "Gudauta" +• BLUE examples: "Nellis AFB", "McCarran International", "Tonopah Test Range" +• Find airbase names in the mission editor + +AIRCRAFT NUMBERS: +• Set realistic numbers based on mission requirements +• Consider aircraft consumption and cargo replenishment +• Balance between realism and gameplay performance +]] + +-- ═══════════════════════════════════════════════════════════════════════════ +-- RED COALITION SQUADRONS +-- ═══════════════════════════════════════════════════════════════════════════ + +local RED_SQUADRON_CONFIG = { + --[[ EXAMPLE RED SQUADRON - CUSTOMIZE FOR YOUR MISSION + { + templateName = "RED_CAP_Batumi_F15", -- Template name from mission editor + displayName = "Batumi F-15C CAP", -- Human-readable name for logs + airbaseName = "Batumi", -- Exact airbase name from DCS + aircraft = 12, -- Maximum aircraft in squadron + skill = AI.Skill.GOOD, -- AI skill level + altitude = 20000, -- Patrol altitude (feet) + speed = 350, -- Patrol speed (knots) + patrolTime = 25, -- Time on station (minutes) + type = "FIGHTER" -- Aircraft type + }, + ]] + + -- ADD YOUR RED SQUADRONS HERE + { + templateName = "RED_CAP_SQUADRON_1", -- Change to your RED template name + displayName = "RED Squadron 1", -- Change to your preferred name + airbaseName = "YOUR_RED_AIRBASE_1", -- Change to your RED airbase + aircraft = 12, -- Adjust aircraft count + skill = AI.Skill.GOOD, -- AVERAGE, GOOD, HIGH, EXCELLENT + altitude = 20000, -- Patrol altitude (feet) + speed = 350, -- Patrol speed (knots) + patrolTime = 25, -- Time on station (minutes) + type = "FIGHTER" + }, + + { + templateName = "RED_CAP_SQUADRON_2", -- Change to your RED template name + displayName = "RED Squadron 2", -- Change to your preferred name + airbaseName = "YOUR_RED_AIRBASE_2", -- Change to your RED airbase + aircraft = 16, -- Adjust aircraft count + skill = AI.Skill.GOOD, -- AVERAGE, GOOD, HIGH, EXCELLENT + altitude = 25000, -- Patrol altitude (feet) + speed = 400, -- Patrol speed (knots) + patrolTime = 30, -- Time on station (minutes) + type = "FIGHTER" + }, +} + +-- ═══════════════════════════════════════════════════════════════════════════ +-- BLUE COALITION SQUADRONS +-- ═══════════════════════════════════════════════════════════════════════════ + +local BLUE_SQUADRON_CONFIG = { + --[[ EXAMPLE BLUE SQUADRON - CUSTOMIZE FOR YOUR MISSION + { + templateName = "BLUE_CAP_Nellis_F16", -- Template name from mission editor + displayName = "Nellis F-16C CAP", -- Human-readable name for logs + airbaseName = "Nellis AFB", -- Exact airbase name from DCS + aircraft = 14, -- Maximum aircraft in squadron + skill = AI.Skill.EXCELLENT, -- AI skill level + altitude = 22000, -- Patrol altitude (feet) + speed = 380, -- Patrol speed (knots) + patrolTime = 28, -- Time on station (minutes) + type = "FIGHTER" -- Aircraft type + }, + ]] + + -- ADD YOUR BLUE SQUADRONS HERE + { + templateName = "BLUE_CAP_SQUADRON_1", -- Change to your BLUE template name + displayName = "BLUE Squadron 1", -- Change to your preferred name + airbaseName = "YOUR_BLUE_AIRBASE_1", -- Change to your BLUE airbase + aircraft = 14, -- Adjust aircraft count + skill = AI.Skill.GOOD, -- AVERAGE, GOOD, HIGH, EXCELLENT + altitude = 22000, -- Patrol altitude (feet) + speed = 380, -- Patrol speed (knots) + patrolTime = 28, -- Time on station (minutes) + type = "FIGHTER" + }, + + { + templateName = "BLUE_CAP_SQUADRON_2", -- Change to your BLUE template name + displayName = "BLUE Squadron 2", -- Change to your preferred name + airbaseName = "YOUR_BLUE_AIRBASE_2", -- Change to your BLUE airbase + aircraft = 18, -- 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" + }, +} + +--[[ +═══════════════════════════════════════════════════════════════════════════════ + ADVANCED SETTINGS +═══════════════════════════════════════════════════════════════════════════════ + +These settings control more detailed behavior. Most users won't need to change these. +]] + +local ADVANCED_SETTINGS = { + -- Cargo aircraft detection patterns (aircraft with these names will replenish squadrons) + cargoPatterns = {"CARGO", "TRANSPORT", "C130", "C-130", "AN26", "AN-26"}, + + -- Distance from airbase to consider cargo "landed" (meters) + cargoLandingDistance = 3000, + + -- Velocity below which aircraft is considered "landed" (km/h) + cargoLandedVelocity = 5, + + -- RTB settings + rtbAltitude = 3000, -- Return to base altitude (feet) + rtbSpeed = 250, -- Return to base speed (knots) + + -- Logging settings + enableDetailedLogging = true, -- Set to false to reduce log spam + logPrefix = "[Universal TADC]", -- Prefix for all log messages +} + +--[[ +═══════════════════════════════════════════════════════════════════════════════ + SYSTEM CODE + (DO NOT MODIFY BELOW THIS LINE) +═══════════════════════════════════════════════════════════════════════════════ +]] + +-- Internal tracking variables - separate for each coalition +local activeInterceptors = { + red = {}, + blue = {} +} +local lastLaunchTime = { + red = {}, + blue = {} +} +local assignedThreats = { + red = {}, + blue = {} +} +local squadronCooldowns = { + red = {}, + blue = {} +} +local squadronAircraftCounts = { + red = {}, + blue = {} +} + +-- Initialize squadron aircraft counts for both coalitions +for _, squadron in pairs(RED_SQUADRON_CONFIG) do + if squadron.aircraft and squadron.templateName then + squadronAircraftCounts.red[squadron.templateName] = squadron.aircraft + end +end + +for _, squadron in pairs(BLUE_SQUADRON_CONFIG) do + if squadron.aircraft and squadron.templateName then + squadronAircraftCounts.blue[squadron.templateName] = squadron.aircraft + 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 + +-- Coalition-specific settings helper +local function getCoalitionSettings(coalitionSide) + if coalitionSide == coalition.side.RED then + return TADC_SETTINGS.red, "RED" + elseif coalitionSide == coalition.side.BLUE then + return TADC_SETTINGS.blue, "BLUE" + else + return nil, "UNKNOWN" + end +end + +-- Startup validation +local function validateConfiguration() + local errors = {} + + -- Check coalition enablement + if not TADC_SETTINGS.enableRed and not TADC_SETTINGS.enableBlue then + table.insert(errors, "Both coalitions disabled - enable at least one in TADC_SETTINGS") + end + + -- Validate RED squadrons if enabled + if TADC_SETTINGS.enableRed then + if #RED_SQUADRON_CONFIG == 0 then + table.insert(errors, "No RED squadrons configured but RED TADC is enabled") + else + for i, squadron in pairs(RED_SQUADRON_CONFIG) do + local prefix = "RED Squadron " .. i .. ": " + + if not squadron.templateName or squadron.templateName == "" or + squadron.templateName == "RED_CAP_SQUADRON_1" or + squadron.templateName == "RED_CAP_SQUADRON_2" then + table.insert(errors, prefix .. "templateName not configured or using default example") + end + + if not squadron.displayName or squadron.displayName == "" then + table.insert(errors, prefix .. "displayName not configured") + end + + if not squadron.airbaseName or squadron.airbaseName == "" or + squadron.airbaseName:find("YOUR_RED_AIRBASE") then + table.insert(errors, prefix .. "airbaseName not configured or using default example") + end + + if not squadron.aircraft or squadron.aircraft <= 0 then + table.insert(errors, prefix .. "aircraft count not configured or invalid") + end + end + end + end + + -- Validate BLUE squadrons if enabled + if TADC_SETTINGS.enableBlue then + if #BLUE_SQUADRON_CONFIG == 0 then + table.insert(errors, "No BLUE squadrons configured but BLUE TADC is enabled") + else + for i, squadron in pairs(BLUE_SQUADRON_CONFIG) do + local prefix = "BLUE Squadron " .. i .. ": " + + if not squadron.templateName or squadron.templateName == "" or + squadron.templateName == "BLUE_CAP_SQUADRON_1" or + squadron.templateName == "BLUE_CAP_SQUADRON_2" then + table.insert(errors, prefix .. "templateName not configured or using default example") + end + + if not squadron.displayName or squadron.displayName == "" then + table.insert(errors, prefix .. "displayName not configured") + end + + if not squadron.airbaseName or squadron.airbaseName == "" or + squadron.airbaseName:find("YOUR_BLUE_AIRBASE") then + table.insert(errors, prefix .. "airbaseName not configured or using default example") + end + + if not squadron.aircraft or squadron.aircraft <= 0 then + table.insert(errors, prefix .. "aircraft count not configured or invalid") + end + end + end + end + + -- Report errors + if #errors > 0 then + log("CONFIGURATION ERRORS DETECTED:") + for _, error in pairs(errors) do + log(" ✗ " .. error) + end + log("Please fix configuration before using Universal TADC!") + return false + else + log("Configuration validation passed ✓") + return true + end +end + +-- Monitor cargo aircraft landings for squadron replenishment +local function monitorCargoReplenishment() + -- Process RED cargo aircraft + if TADC_SETTINGS.enableRed then + local redCargo = SET_GROUP:New():FilterCoalitions("red"):FilterCategoryAirplane():FilterStart() + + redCargo:ForEach(function(cargoGroup) + if cargoGroup and cargoGroup:IsAlive() then + local cargoName = cargoGroup: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 = cargoGroup:GetCoordinate() + local cargoVelocity = cargoGroup:GetVelocityKMH() + + -- 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 + -- Check if we haven't already processed this delivery + local deliveryKey = cargoName .. "_RED_" .. squadron.airbaseName + if not _G.processedDeliveries then + _G.processedDeliveries = {} + end + + if not _G.processedDeliveries[deliveryKey] then + -- 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 + log("RED CARGO DELIVERY: " .. cargoName .. " delivered " .. actualAdded .. + " aircraft to " .. squadron.displayName .. + " (" .. newCount .. "/" .. maxCount .. ")") + + -- Mark delivery as processed + _G.processedDeliveries[deliveryKey] = timer.getTime() + else + log("RED CARGO DELIVERY: " .. squadron.displayName .. " already at max capacity", true) + end + end + end + end + end + end + end + end + end) + end + + -- Process BLUE cargo aircraft + if TADC_SETTINGS.enableBlue then + local blueCargo = SET_GROUP:New():FilterCoalitions("blue"):FilterCategoryAirplane():FilterStart() + + blueCargo:ForEach(function(cargoGroup) + if cargoGroup and cargoGroup:IsAlive() then + local cargoName = cargoGroup: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 = 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 + -- Check if we haven't already processed this delivery + local deliveryKey = cargoName .. "_BLUE_" .. squadron.airbaseName + if not _G.processedDeliveries then + _G.processedDeliveries = {} + end + + if not _G.processedDeliveries[deliveryKey] then + -- 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 + log("BLUE CARGO DELIVERY: " .. cargoName .. " delivered " .. actualAdded .. + " aircraft to " .. squadron.displayName .. + " (" .. newCount .. "/" .. maxCount .. ")") + + -- Mark delivery as processed + _G.processedDeliveries[deliveryKey] = timer.getTime() + else + log("BLUE CARGO DELIVERY: " .. squadron.displayName .. " already at max capacity", true) + end + end + end + end + end + end + end + end + end) + end +end + +-- Send interceptor back to base (dual coalition) +local function sendInterceptorHome(interceptor, coalitionSide) + 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(SQUADRON_CONFIG) 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 = ADVANCED_SETTINGS.rtbAltitude * 0.3048 -- Convert feet to meters + local rtbCoord = airbaseCoord:SetAltitude(rtbAltitude) + + -- Clear current tasks and route home + interceptor:ClearTasks() + interceptor:RouteAirTo(rtbCoord, ADVANCED_SETTINGS.rtbSpeed * 0.5144, "BARO") -- Convert knots to m/s + + log("Sending " .. interceptor:GetName() .. " back to " .. nearestAirbase:GetName(), true) + + -- Schedule cleanup after they should have landed + local flightTime = math.ceil(shortestDistance / (ADVANCED_SETTINGS.rtbSpeed * 0.5144)) + TADC_SETTINGS.rtbFlightBuffer + SCHEDULER:New(nil, function() + if activeInterceptors[interceptor:GetName()] then + activeInterceptors[interceptor:GetName()] = nil + log("Cleaned up " .. interceptor:GetName() .. " after RTB", true) + 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(SQUADRON_CONFIG) 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", true) + squadronAvailable = false + else + -- Cooldown expired, remove it + squadronCooldowns[squadron.templateName] = nil + log("Squadron " .. squadron.displayName .. " cooldown expired, available for launch", true) + 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 .. ")", true) + 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 template exists and can spawn + 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 + else + log("Warning: Template " .. squadron.templateName .. " not found in mission", true) + 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() + + -- 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 + local interceptorsNeeded = math.max(threatSize, math.ceil(threatSize * TADC_SETTINGS.interceptRatio)) + + -- Check if we have capacity + if countActiveFighters() + interceptorsNeeded > TADC_SETTINGS.maxActiveCAP then + interceptorsNeeded = TADC_SETTINGS.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 knots 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) + 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, {}, TADC_SETTINGS.emergencyCleanupTime) + 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 + lastLaunchTime[threatName] = timer.getTime() + + -- Apply cooldown immediately when squadron launches + local currentTime = timer.getTime() + squadronCooldowns[squadron.templateName] = currentTime + TADC_SETTINGS.squadronCooldown + local cooldownMinutes = TADC_SETTINGS.squadronCooldown / 60 + log("Squadron " .. squadron.displayName .. " LAUNCHED! Applying " .. cooldownMinutes .. " minute cooldown") + end +end + +-- Main threat detection loop +local function detectThreats() + log("Scanning for threats...", true) + + -- 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() .. ")", true) + + -- 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, true) + + -- 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(SQUADRON_CONFIG) 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 .. "/" .. #SQUADRON_CONFIG .. " airbases operational") +end + +-- System initialization +local function initializeSystem() + log("Universal TADC starting...") + + -- Validate configuration + if not validateConfiguration() then + log("System startup aborted due to configuration errors!") + return false + end + + log("Squadrons configured: " .. #SQUADRON_CONFIG) + + -- Log initial squadron aircraft counts + for _, squadron in pairs(SQUADRON_CONFIG) do + local count = squadronAircraftCounts[squadron.templateName] + log("Initial: " .. squadron.displayName .. " has " .. count .. "/" .. squadron.aircraft .. " aircraft") + end + + -- Start schedulers + SCHEDULER:New(nil, detectThreats, {}, 5, TADC_SETTINGS.checkInterval) + SCHEDULER:New(nil, monitorInterceptors, {}, 10, TADC_SETTINGS.monitorInterval) + SCHEDULER:New(nil, checkAirbaseStatus, {}, 30, TADC_SETTINGS.statusReportInterval) + SCHEDULER:New(nil, monitorCargoReplenishment, {}, 15, TADC_SETTINGS.cargoCheckInterval) + + log("Universal TADC operational!") + log("Aircraft replenishment: " .. TADC_SETTINGS.cargoReplenishmentAmount .. " aircraft per cargo delivery") + + return true +end + +-- Start the system +initializeSystem() \ No newline at end of file diff --git a/Universal_TADC_Dual.lua b/Universal_TADC_Dual.lua new file mode 100644 index 0000000..f2c1669 --- /dev/null +++ b/Universal_TADC_Dual.lua @@ -0,0 +1,1525 @@ +--[[ +═══════════════════════════════════════════════════════════════════════════════ + UNIVERSAL TADC + Dual-Coalition Tactical Air Defense Controller + Advanced Zone-Based System +═══════════════════════════════════════════════════════════════════════════════ + +DESCRIPTION: +This script provides a sophisticated automated air defense system for BOTH RED and +BLUE coalitions operating independently. Features advanced zone-based area of +responsibility (AOR) management, allowing squadrons to respond differently based +on threat location and priority levels. Perfect for complex scenarios requiring +realistic air defense behavior and tactical depth. + +CORE FEATURES: +• Dual-coalition support with completely independent operation +• Advanced zone-based area of responsibility system (Primary/Secondary/Tertiary) +• Automatic threat detection with intelligent interceptor allocation +• Multi-squadron management with individual cooldowns and aircraft tracking +• Dynamic cargo aircraft replenishment system +• Configurable intercept ratios with zone-specific response modifiers +• Smart interceptor routing, engagement, and RTB (Return to Base) behavior +• Real-time airbase status monitoring (operational/captured/destroyed) +• Comprehensive configuration validation and error reporting +• Asymmetric warfare support with coalition-specific capabilities +• Emergency cleanup systems and safety nets for mission stability + +ADVANCED ZONE SYSTEM: +Each squadron can be configured with up to three zone types: +• PRIMARY ZONE: Main area of responsibility (full response ratio) +• SECONDARY ZONE: Support area (reduced response, optional low-priority filtering) +• TERTIARY ZONE: Emergency/fallback area (enhanced response when base threatened) + +Zone response behaviors include: +• Distance-based engagement limits (max range from airbase) +• Priority thresholds for threat classification (major vs minor threats) +• Fallback conditions (auto-switch to tertiary when squadron weakened) +• Response ratio multipliers per zone type +• Low-priority threat filtering in secondary zones + +REPLENISHMENT SYSTEM: +Automated cargo aircraft detection system that monitors for transport aircraft +landings to replenish squadron aircraft counts: + +• 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 +• 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. + +INTERCEPT RATIO SYSTEM: +Sophisticated threat response calculation with zone-based modifiers: +• Base intercept ratio (e.g., 0.8 = 8 interceptors per 10 threats) +• Zone-specific multipliers (primary: 1.0, secondary: 0.6, tertiary: 1.4) +• Threat size considerations (larger formations get proportional response) +• Squadron selection based on zone priority and proximity +• Aircraft availability and cooldown status factored into decisions + +SETUP INSTRUCTIONS: +1. Load MOOSE framework in mission before this script +2. Create fighter aircraft GROUP templates for both coalitions in mission editor +3. Configure RED squadrons in RED_SQUADRON_CONFIG section +4. Configure BLUE squadrons in BLUE_SQUADRON_CONFIG section +5. Optionally create zones in mission editor for area-of-responsibility +6. Set coalition behavior parameters in TADC_SETTINGS +7. Configure cargo patterns in ADVANCED_SETTINGS if using replenishment +8. Add this script as "DO SCRIPT" trigger at mission start (after MOOSE loaded) +9. Create and manage cargo aircraft missions for replenishment (optional) + +CONFIGURATION VALIDATION: +Built-in validation system checks for: +• Template existence and proper naming +• Airbase name accuracy and coalition control +• Zone existence in mission editor +• Parameter ranges and logical consistency +• Coalition enablement and squadron availability +• Prevents common configuration errors before mission starts + +TACTICAL SCENARIOS SUPPORTED: +• Balanced air warfare with equal capabilities and symmetric response +• Asymmetric scenarios with different coalition strengths and capabilities +• Layered air defense with overlapping squadron zones +• Border/perimeter defense with primary and fallback positions +• Training missions for AI vs AI air combat observation +• Dynamic frontline battles with shifting territorial control +• Long-duration missions with cargo resupply operations +• Emergency response scenarios with threat priority management + +LOGGING AND MONITORING: +• Real-time threat detection and interceptor launch notifications +• Squadron status reports including aircraft counts and cooldown timers +• Airbase operational status with capture/destruction detection +• Cargo delivery tracking and replenishment confirmations +• Zone-based engagement decisions with detailed reasoning +• Configuration validation results and error reporting +• Performance monitoring with emergency cleanup notifications + +REQUIREMENTS: +• MOOSE framework (https://github.com/FlightControl-Master/MOOSE) +• Fighter aircraft GROUP templates (not UNIT templates) for each coalition +• Airbases must exist in mission and be under correct coalition control +• Zone objects in mission editor (if using zone-based features) +• Proper template naming matching squadron configuration + +═══════════════════════════════════════════════════════════════════════════════ +]] + +--[[ +═══════════════════════════════════════════════════════════════════════════════ + MAIN SETTINGS +═══════════════════════════════════════════════════════════════════════════════ +]] + +-- Core TADC behavior settings - applies to BOTH coalitions unless overridden +local TADC_SETTINGS = { + -- Enable/Disable coalitions + enableRed = true, -- Set to false to disable RED TADC + enableBlue = true, -- Set to false to disable BLUE TADC + + -- Timing settings (applies to both coalitions) + checkInterval = 30, -- How often to scan for threats (seconds) + monitorInterval = 30, -- How often to check interceptor status (seconds) + statusReportInterval = 120, -- How often to report airbase status (seconds) + cargoCheckInterval = 15, -- How often to check for cargo deliveries (seconds) + + -- RED Coalition Settings + red = { + maxActiveCAP = 24, -- Maximum RED fighters airborne at once + squadronCooldown = 900, -- RED cooldown after squadron launch (seconds) + interceptRatio = 0.8, -- RED interceptors per threat aircraft + cargoReplenishmentAmount = 4, -- RED aircraft added per cargo delivery + emergencyCleanupTime = 7200, -- RED force cleanup time (seconds) + rtbFlightBuffer = 300, -- RED extra landing time before cleanup (seconds) + }, + + -- BLUE Coalition Settings + blue = { + maxActiveCAP = 24, -- Maximum BLUE fighters airborne at once + squadronCooldown = 900, -- BLUE cooldown after squadron launch (seconds) + interceptRatio = 0.8, -- BLUE interceptors per threat aircraft + cargoReplenishmentAmount = 4, -- BLUE aircraft added per cargo delivery + emergencyCleanupTime = 7200, -- BLUE force cleanup time (seconds) + rtbFlightBuffer = 300, -- BLUE extra landing time before cleanup (seconds) + }, +} + +--[[ +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) <- DEFAULT +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) +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, creates balanced dogfights +• 1.2-1.4: Coalition advantage, challenging for enemy +• 1.6-1.8: Strong defense, difficult penetration missions +• 1.9-2.0: Nearly impenetrable, may exhaust squadrons quickly + +SQUADRON IMPACT: +• Low ratios (0.2-0.8): Squadrons last longer, sustained defense +• High ratios (1.6-2.0): Rapid squadron depletion, coverage gaps +• Sweet spot (1.0-1.4): Balanced response with good coverage duration + +ASYMMETRIC SCENARIOS: +• Set RED ratio 1.2, BLUE ratio 0.8 = RED advantage +• Set RED ratio 0.6, BLUE ratio 1.4 = BLUE advantage +• Different maxActiveCAP values create capacity imbalances +]] + +--[[ +═══════════════════════════════════════════════════════════════════════════════ + SQUADRON CONFIGURATION +═══════════════════════════════════════════════════════════════════════════════ + +INSTRUCTIONS: +1. Create fighter aircraft templates for BOTH coalitions in the mission editor +2. Place them at or near the airbases you want them to operate from +3. Configure RED squadrons in RED_SQUADRON_CONFIG +4. Configure BLUE squadrons in BLUE_SQUADRON_CONFIG + +TEMPLATE NAMING SUGGESTIONS: +• RED: "RED_CAP_Batumi_F15", "RED_INTERCEPT_Senaki_MiG29" +• BLUE: "BLUE_CAP_Nellis_F16", "BLUE_INTERCEPT_Creech_F22" +• Include coalition and airbase name for easy identification + +AIRBASE NAMES: +• Use exact names as they appear in DCS (case sensitive) +• RED examples: "Batumi", "Senaki", "Gudauta" +• BLUE examples: "Nellis AFB", "McCarran International", "Tonopah Test Range" +• Find airbase names in the mission editor + +AIRCRAFT NUMBERS: +• Set realistic numbers based on mission requirements +• Consider aircraft consumption and cargo replenishment +• Balance between realism and gameplay performance + +ZONE-BASED AREAS OF RESPONSIBILITY: +• Create zones in mission editor (polygons, circles, etc.) +• primaryZone: Squadron's main area (full response) +• secondaryZone: Backup/support area (reduced response) +• tertiaryZone: Emergency fallback area (enhanced response) +• Leave zones as nil for global threat response +• Multiple squadrons can share overlapping zones +• Use zone names exactly as they appear in mission editor + +ZONE BEHAVIOR EXAMPLES: +• Border Defense: primaryZone = "SECTOR_ALPHA", secondaryZone = "BUFFER_ZONE" +• Base Defense: tertiaryZone = "BASE_PERIMETER", enableFallback = true +• Layered Defense: Different zones per squadron with overlap +• Emergency Response: High tertiaryResponse ratio for critical areas +]] + +-- ═══════════════════════════════════════════════════════════════════════════ +-- RED COALITION SQUADRONS +-- ═══════════════════════════════════════════════════════════════════════════ + +local RED_SQUADRON_CONFIG = { + --[[ EXAMPLE RED SQUADRON - CUSTOMIZE FOR YOUR MISSION + { + templateName = "RED_CAP_Batumi_F15", -- Template name from mission editor + displayName = "Batumi F-15C CAP", -- Human-readable name for logs + airbaseName = "Batumi", -- Exact airbase name from DCS + aircraft = 12, -- Maximum aircraft in squadron + skill = AI.Skill.GOOD, -- AI skill level + altitude = 20000, -- Patrol altitude (feet) + speed = 350, -- Patrol speed (knots) + patrolTime = 25, -- Time on station (minutes) + type = "FIGHTER" -- Aircraft type + }, + ]] + + -- ADD YOUR RED SQUADRONS HERE + { + templateName = "RED_CAP_SQUADRON_1", -- Change to your RED template name + displayName = "RED Squadron 1", -- Change to your preferred name + airbaseName = "YOUR_RED_AIRBASE_1", -- Change to your RED airbase + aircraft = 12, -- Adjust aircraft count + skill = AI.Skill.GOOD, -- 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 = nil, -- 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 = "RED_CAP_SQUADRON_2", -- Change to your RED template name + displayName = "RED Squadron 2", -- Change to your preferred name + airbaseName = "YOUR_RED_AIRBASE_2", -- Change to your RED airbase + aircraft = 16, -- Adjust aircraft count + skill = AI.Skill.GOOD, -- AVERAGE, GOOD, HIGH, EXCELLENT + altitude = 25000, -- Patrol altitude (feet) + speed = 400, -- Patrol speed (knots) + patrolTime = 30, -- Time on station (minutes) + type = "FIGHTER", + + -- Zone-based Areas of Responsibility (optional - leave nil for global response) + primaryZone = nil, -- 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 + } + }, +} + +-- ═══════════════════════════════════════════════════════════════════════════ +-- BLUE COALITION SQUADRONS +-- ═══════════════════════════════════════════════════════════════════════════ + +local BLUE_SQUADRON_CONFIG = { + --[[ EXAMPLE BLUE SQUADRON - CUSTOMIZE FOR YOUR MISSION + { + templateName = "BLUE_CAP_Nellis_F16", -- Template name from mission editor + displayName = "Nellis F-16C CAP", -- Human-readable name for logs + airbaseName = "Nellis AFB", -- Exact airbase name from DCS + aircraft = 14, -- Maximum aircraft in squadron + skill = AI.Skill.EXCELLENT, -- AI skill level + altitude = 22000, -- Patrol altitude (feet) + speed = 380, -- Patrol speed (knots) + patrolTime = 28, -- Time on station (minutes) + type = "FIGHTER" -- Aircraft type + }, + ]] + + -- ADD YOUR BLUE SQUADRONS HERE + { + templateName = "BLUE_CAP_SQUADRON_1", -- Change to your BLUE template name + displayName = "BLUE Squadron 1", -- Change to your preferred name + airbaseName = "YOUR_BLUE_AIRBASE_1", -- Change to your BLUE airbase + aircraft = 14, -- Adjust aircraft count + skill = AI.Skill.GOOD, -- AVERAGE, GOOD, HIGH, EXCELLENT + altitude = 22000, -- Patrol altitude (feet) + speed = 380, -- Patrol speed (knots) + patrolTime = 28, -- Time on station (minutes) + type = "FIGHTER", + + -- Zone-based Areas of Responsibility (optional - leave nil for global response) + primaryZone = nil, -- 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 = "BLUE_CAP_SQUADRON_2", -- Change to your BLUE template name + displayName = "BLUE Squadron 2", -- Change to your preferred name + airbaseName = "YOUR_BLUE_AIRBASE_2", -- Change to your BLUE airbase + aircraft = 18, -- 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 = nil, -- 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 + } + }, +} + +--[[ +═══════════════════════════════════════════════════════════════════════════════ + ADVANCED SETTINGS +═══════════════════════════════════════════════════════════════════════════════ + +These settings control more detailed behavior. Most users won't need to change these. +]] + +local ADVANCED_SETTINGS = { + -- Cargo aircraft detection patterns (aircraft with these names will replenish squadrons) + cargoPatterns = {"CARGO", "TRANSPORT", "C130", "C-130", "AN26", "AN-26"}, + + -- Distance from airbase to consider cargo "landed" (meters) + cargoLandingDistance = 3000, + + -- Velocity below which aircraft is considered "landed" (km/h) + cargoLandedVelocity = 5, + + -- RTB settings + rtbAltitude = 6000, -- Return to base altitude (feet) + rtbSpeed = 430, -- Return to base speed (knots) + + -- Logging settings + enableDetailedLogging = true, -- Set to false to reduce log spam + logPrefix = "[Universal TADC]", -- Prefix for all log messages +} + +--[[ +═══════════════════════════════════════════════════════════════════════════════ + SYSTEM CODE + (DO NOT MODIFY BELOW THIS LINE) +═══════════════════════════════════════════════════════════════════════════════ +]] + +-- Internal tracking variables - separate for each coalition +local activeInterceptors = { + red = {}, + blue = {} +} +local lastLaunchTime = { + red = {}, + blue = {} +} +local assignedThreats = { + red = {}, + blue = {} +} +local squadronCooldowns = { + red = {}, + blue = {} +} +local squadronAircraftCounts = { + red = {}, + blue = {} +} + +-- Performance optimization: Cache SET_GROUP objects to avoid repeated creation +local cachedSets = { + redCargo = nil, + blueCargo = nil, + redAircraft = nil, + blueAircraft = nil +} + +-- Initialize squadron aircraft counts for both coalitions +for _, squadron in pairs(RED_SQUADRON_CONFIG) do + if squadron.aircraft and squadron.templateName then + squadronAircraftCounts.red[squadron.templateName] = squadron.aircraft + end +end + +for _, squadron in pairs(BLUE_SQUADRON_CONFIG) do + if squadron.aircraft and squadron.templateName then + squadronAircraftCounts.blue[squadron.templateName] = squadron.aircraft + 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 + +-- Coalition-specific settings helper +local function getCoalitionSettings(coalitionSide) + if coalitionSide == coalition.side.RED then + return TADC_SETTINGS.red, "RED" + elseif coalitionSide == coalition.side.BLUE then + return TADC_SETTINGS.blue, "BLUE" + else + return nil, "UNKNOWN" + end +end + +-- Get squadron config for coalition +local function getSquadronConfig(coalitionSide) + if coalitionSide == coalition.side.RED then + return RED_SQUADRON_CONFIG + elseif coalitionSide == coalition.side.BLUE then + return BLUE_SQUADRON_CONFIG + else + return {} + end +end + +-- Check if coordinate is within a zone +local function isInZone(coordinate, zoneName) + if not zoneName or zoneName == "" then + return false + end + + -- Try to find the zone + local zone = ZONE:FindByName(zoneName) + if zone then + return zone:IsCoordinateInZone(coordinate) + else + log("Warning: Zone '" .. zoneName .. "' not found in mission", true) + return false + end +end + +-- Get default zone configuration +local function getDefaultZoneConfig() + return { + primaryResponse = 1.0, + secondaryResponse = 0.6, + tertiaryResponse = 1.4, + maxRange = 200, + enableFallback = false, + priorityThreshold = 4, + ignoreLowPriority = false, + } +end + +-- Check if squadron should respond to fallback conditions +local function checkFallbackConditions(squadron, coalitionSide) + local coalitionKey = (coalitionSide == coalition.side.RED) and "red" or "blue" + + -- Check if airbase is under attack (simplified - check if base has low aircraft) + local currentAircraft = squadronAircraftCounts[coalitionKey][squadron.templateName] or 0 + local maxAircraft = squadron.aircraft + local aircraftRatio = currentAircraft / maxAircraft + + -- Trigger fallback if squadron is below 50% strength or base is threatened + if aircraftRatio < 0.5 then + return true + end + + -- Could add more complex conditions here (base under attack, etc.) + return false +end + +-- Get threat zone priority and response ratio for squadron +local function getThreatZonePriority(threatCoord, squadron, coalitionSide) + local zoneConfig = squadron.zoneConfig or getDefaultZoneConfig() + + -- Check distance from airbase first + local airbase = AIRBASE:FindByName(squadron.airbaseName) + if airbase then + local airbaseCoord = airbase:GetCoordinate() + local distance = airbaseCoord:Get2DDistance(threatCoord) / 1852 -- Convert meters to nautical miles + + if distance > zoneConfig.maxRange then + return "none", 0, "out of range (" .. math.floor(distance) .. "nm > " .. zoneConfig.maxRange .. "nm)" + end + end + + -- Check tertiary zone first (highest priority if fallback enabled) + if squadron.tertiaryZone and zoneConfig.enableFallback then + if checkFallbackConditions(squadron, coalitionSide) then + if isInZone(threatCoord, squadron.tertiaryZone) then + return "tertiary", zoneConfig.tertiaryResponse, "fallback zone (enhanced response)" + end + end + end + + -- Check primary zone + if squadron.primaryZone and isInZone(threatCoord, squadron.primaryZone) then + return "primary", zoneConfig.primaryResponse, "primary AOR" + end + + -- Check secondary zone + if squadron.secondaryZone and isInZone(threatCoord, squadron.secondaryZone) then + return "secondary", zoneConfig.secondaryResponse, "secondary AOR" + end + + -- Check tertiary zone (normal priority) + if squadron.tertiaryZone and isInZone(threatCoord, squadron.tertiaryZone) then + return "tertiary", zoneConfig.tertiaryResponse, "tertiary zone" + end + + -- If no zones are defined, use global response + if not squadron.primaryZone and not squadron.secondaryZone and not squadron.tertiaryZone then + return "global", 1.0, "global response (no zones defined)" + end + + -- Outside all defined zones + return "none", 0, "outside defined zones" +end + +-- Startup validation +local function validateConfiguration() + local errors = {} + + -- Check coalition enablement + if not TADC_SETTINGS.enableRed and not TADC_SETTINGS.enableBlue then + table.insert(errors, "Both coalitions disabled - enable at least one in TADC_SETTINGS") + end + + -- Validate RED squadrons if enabled + if TADC_SETTINGS.enableRed then + if #RED_SQUADRON_CONFIG == 0 then + table.insert(errors, "No RED squadrons configured but RED TADC is enabled") + else + for i, squadron in pairs(RED_SQUADRON_CONFIG) do + local prefix = "RED Squadron " .. i .. ": " + + if not squadron.templateName or squadron.templateName == "" or + squadron.templateName == "RED_CAP_SQUADRON_1" or + squadron.templateName == "RED_CAP_SQUADRON_2" then + table.insert(errors, prefix .. "templateName not configured or using default example") + end + + if not squadron.displayName or squadron.displayName == "" then + table.insert(errors, prefix .. "displayName not configured") + end + + if not squadron.airbaseName or squadron.airbaseName == "" or + squadron.airbaseName:find("YOUR_RED_AIRBASE") then + table.insert(errors, prefix .. "airbaseName not configured or using default example") + end + + if not squadron.aircraft or squadron.aircraft <= 0 then + table.insert(errors, prefix .. "aircraft count not configured or invalid") + end + + -- Validate zone configuration if zones are specified + if squadron.primaryZone or squadron.secondaryZone or squadron.tertiaryZone then + if squadron.zoneConfig then + local zc = squadron.zoneConfig + if zc.primaryResponse and (zc.primaryResponse < 0 or zc.primaryResponse > 5) then + table.insert(errors, prefix .. "primaryResponse ratio out of range (0-5)") + end + if zc.secondaryResponse and (zc.secondaryResponse < 0 or zc.secondaryResponse > 5) then + table.insert(errors, prefix .. "secondaryResponse ratio out of range (0-5)") + end + if zc.tertiaryResponse and (zc.tertiaryResponse < 0 or zc.tertiaryResponse > 5) then + table.insert(errors, prefix .. "tertiaryResponse ratio out of range (0-5)") + end + if zc.maxRange and (zc.maxRange < 10 or zc.maxRange > 1000) then + table.insert(errors, prefix .. "maxRange out of range (10-1000 nm)") + end + end + + -- Check if specified zones exist in mission + local zones = {} + if squadron.primaryZone then table.insert(zones, squadron.primaryZone) end + if squadron.secondaryZone then table.insert(zones, squadron.secondaryZone) end + if squadron.tertiaryZone then table.insert(zones, squadron.tertiaryZone) end + + for _, zoneName in ipairs(zones) do + local zoneObj = ZONE:FindByName(zoneName) + if not zoneObj then + table.insert(errors, prefix .. "zone '" .. zoneName .. "' not found in mission") + end + end + end + end + end + end + + -- Validate BLUE squadrons if enabled + if TADC_SETTINGS.enableBlue then + if #BLUE_SQUADRON_CONFIG == 0 then + table.insert(errors, "No BLUE squadrons configured but BLUE TADC is enabled") + else + for i, squadron in pairs(BLUE_SQUADRON_CONFIG) do + local prefix = "BLUE Squadron " .. i .. ": " + + if not squadron.templateName or squadron.templateName == "" or + squadron.templateName == "BLUE_CAP_SQUADRON_1" or + squadron.templateName == "BLUE_CAP_SQUADRON_2" then + table.insert(errors, prefix .. "templateName not configured or using default example") + end + + if not squadron.displayName or squadron.displayName == "" then + table.insert(errors, prefix .. "displayName not configured") + end + + if not squadron.airbaseName or squadron.airbaseName == "" or + squadron.airbaseName:find("YOUR_BLUE_AIRBASE") then + table.insert(errors, prefix .. "airbaseName not configured or using default example") + end + + if not squadron.aircraft or squadron.aircraft <= 0 then + table.insert(errors, prefix .. "aircraft count not configured or invalid") + end + + -- Validate zone configuration if zones are specified + if squadron.primaryZone or squadron.secondaryZone or squadron.tertiaryZone then + if squadron.zoneConfig then + local zc = squadron.zoneConfig + if zc.primaryResponse and (zc.primaryResponse < 0 or zc.primaryResponse > 5) then + table.insert(errors, prefix .. "primaryResponse ratio out of range (0-5)") + end + if zc.secondaryResponse and (zc.secondaryResponse < 0 or zc.secondaryResponse > 5) then + table.insert(errors, prefix .. "secondaryResponse ratio out of range (0-5)") + end + if zc.tertiaryResponse and (zc.tertiaryResponse < 0 or zc.tertiaryResponse > 5) then + table.insert(errors, prefix .. "tertiaryResponse ratio out of range (0-5)") + end + if zc.maxRange and (zc.maxRange < 10 or zc.maxRange > 1000) then + table.insert(errors, prefix .. "maxRange out of range (10-1000 nm)") + end + end + + -- Check if specified zones exist in mission + local zones = {} + if squadron.primaryZone then table.insert(zones, squadron.primaryZone) end + if squadron.secondaryZone then table.insert(zones, squadron.secondaryZone) end + if squadron.tertiaryZone then table.insert(zones, squadron.tertiaryZone) end + + for _, zoneName in ipairs(zones) do + local zoneObj = ZONE:FindByName(zoneName) + if not zoneObj then + table.insert(errors, prefix .. "zone '" .. zoneName .. "' not found in mission") + end + end + end + end + end + end + + -- Report errors + if #errors > 0 then + log("CONFIGURATION ERRORS DETECTED:") + for _, error in pairs(errors) do + log(" ✗ " .. error) + end + log("Please fix configuration before using Universal TADC!") + return false + else + log("Configuration validation passed ✓") + return true + end +end + +-- Monitor cargo aircraft landings for squadron replenishment +local function monitorCargoReplenishment() + -- Process RED cargo aircraft + if TADC_SETTINGS.enableRed then + -- Use cached set for performance, create if needed + if not cachedSets.redCargo then + cachedSets.redCargo = SET_GROUP:New():FilterCoalitions("red"):FilterCategoryAirplane():FilterStart() + end + local redCargo = cachedSets.redCargo + + redCargo:ForEach(function(cargoGroup) + if cargoGroup and cargoGroup:IsAlive() then + local cargoName = cargoGroup: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 = cargoGroup:GetCoordinate() + local cargoVelocity = cargoGroup:GetVelocityKMH() + + -- 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 + log("RED CARGO DELIVERY: " .. cargoName .. " delivered " .. actualAdded .. + " aircraft to " .. squadron.displayName .. + " (" .. newCount .. "/" .. maxCount .. ")") + else + log("RED CARGO DELIVERY: " .. squadron.displayName .. " already at max capacity", true) + end + end + end + end + end + end + end + end + end) + end + + -- Process BLUE cargo aircraft + if TADC_SETTINGS.enableBlue then + -- Use cached set for performance, create if needed + if not cachedSets.blueCargo then + cachedSets.blueCargo = SET_GROUP:New():FilterCoalitions("blue"):FilterCategoryAirplane():FilterStart() + end + local blueCargo = cachedSets.blueCargo + + blueCargo:ForEach(function(cargoGroup) + if cargoGroup and cargoGroup:IsAlive() then + local cargoName = cargoGroup: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 = 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 + log("BLUE CARGO DELIVERY: " .. cargoName .. " delivered " .. actualAdded .. + " aircraft to " .. squadron.displayName .. + " (" .. newCount .. "/" .. maxCount .. ")") + else + log("BLUE CARGO DELIVERY: " .. squadron.displayName .. " already at max capacity", true) + end + end + end + end + end + end + end + end + end) + end +end + +-- Send interceptor back to base +local function sendInterceptorHome(interceptor, coalitionSide) + if not interceptor or not interceptor:IsAlive() then + return + end + + -- Find nearest friendly airbase + local interceptorCoord = interceptor:GetCoordinate() + if not interceptorCoord then + log("ERROR: Could not get interceptor coordinates for RTB", true) + return + end + local nearestAirbase = nil + local shortestDistance = math.huge + local squadronConfig = getSquadronConfig(coalitionSide) + + -- Check all squadron airbases to find the nearest one that's still friendly + for _, squadron in pairs(squadronConfig) do + local airbase = AIRBASE:FindByName(squadron.airbaseName) + if airbase and airbase:GetCoalition() == coalitionSide 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 = ADVANCED_SETTINGS.rtbAltitude * 0.3048 -- Convert feet to meters + local rtbCoord = airbaseCoord:SetAltitude(rtbAltitude) + + -- Clear current tasks and route home + interceptor:ClearTasks() + interceptor:RouteAirTo(rtbCoord, ADVANCED_SETTINGS.rtbSpeed * 0.5144, "BARO") -- Convert knots to m/s + + local _, coalitionName = getCoalitionSettings(coalitionSide) + log("Sending " .. coalitionName .. " " .. interceptor:GetName() .. " back to " .. nearestAirbase:GetName(), true) + + -- Schedule cleanup after they should have landed + local coalitionSettings = getCoalitionSettings(coalitionSide) + local flightTime = math.ceil(shortestDistance / (ADVANCED_SETTINGS.rtbSpeed * 0.5144)) + coalitionSettings.rtbFlightBuffer + + SCHEDULER:New(nil, function() + local coalitionKey = (coalitionSide == coalition.side.RED) and "red" or "blue" + if activeInterceptors[coalitionKey][interceptor:GetName()] then + activeInterceptors[coalitionKey][interceptor:GetName()] = nil + log("Cleaned up " .. coalitionName .. " " .. interceptor:GetName() .. " after RTB", true) + end + end, {}, flightTime) + else + local _, coalitionName = getCoalitionSettings(coalitionSide) + log("No friendly airbase found for " .. coalitionName .. " " .. interceptor:GetName() .. ", will clean up normally") + end +end + +-- Check if airbase is still usable +local function isAirbaseUsable(airbaseName, expectedCoalition) + local airbase = AIRBASE:FindByName(airbaseName) + if not airbase then + return false, "not found" + elseif airbase:GetCoalition() ~= expectedCoalition then + local capturedBy = "Unknown" + if airbase:GetCoalition() == coalition.side.RED then + capturedBy = "Red" + elseif airbase:GetCoalition() == coalition.side.BLUE then + capturedBy = "Blue" + else + capturedBy = "Neutral" + end + return false, "captured by " .. capturedBy + elseif not airbase:IsAlive() then + return false, "destroyed" + else + return true, "operational" + end +end + +-- Count active fighters for coalition +local function countActiveFighters(coalitionSide) + local count = 0 + local coalitionKey = (coalitionSide == coalition.side.RED) and "red" or "blue" + + for _, interceptorData in pairs(activeInterceptors[coalitionKey]) 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 for coalition using zone-based priorities +local function findBestSquadron(threatCoord, threatSize, coalitionSide) + local bestSquadron = nil + local bestPriority = "none" + local bestResponseRatio = 0 + local shortestDistance = math.huge + local currentTime = timer.getTime() + local squadronConfig = getSquadronConfig(coalitionSide) + local coalitionSettings, coalitionName = getCoalitionSettings(coalitionSide) + local coalitionKey = (coalitionSide == coalition.side.RED) and "red" or "blue" + local zonePriorityOrder = {"tertiary", "primary", "secondary", "global"} + + -- First pass: find squadrons that can respond to this threat + local availableSquadrons = {} + + for _, squadron in pairs(squadronConfig) do + -- Check basic availability + local squadronAvailable = true + local unavailableReason = "" + + -- Check cooldown + if squadronCooldowns[coalitionKey][squadron.templateName] then + local cooldownEnd = squadronCooldowns[coalitionKey][squadron.templateName] + if currentTime < cooldownEnd then + local timeLeft = math.ceil((cooldownEnd - currentTime) / 60) + squadronAvailable = false + unavailableReason = "on cooldown for " .. timeLeft .. "m" + else + -- Cooldown expired, remove it + squadronCooldowns[coalitionKey][squadron.templateName] = nil + log(coalitionName .. " Squadron " .. squadron.displayName .. " cooldown expired, available for launch", true) + end + end + + -- Check aircraft availability + if squadronAvailable then + local availableAircraft = squadronAircraftCounts[coalitionKey][squadron.templateName] or 0 + if availableAircraft <= 0 then + squadronAvailable = false + unavailableReason = "no aircraft available (" .. availableAircraft .. "/" .. squadron.aircraft .. ")" + end + end + + -- Check airbase status + if squadronAvailable then + local airbase = AIRBASE:FindByName(squadron.airbaseName) + if not airbase then + squadronAvailable = false + unavailableReason = "airbase not found" + elseif airbase:GetCoalition() ~= coalitionSide then + squadronAvailable = false + unavailableReason = "airbase no longer under " .. coalitionName .. " control" + elseif not airbase:IsAlive() then + squadronAvailable = false + unavailableReason = "airbase destroyed" + end + end + + -- Check template exists (Note: Templates are validated during SPAWN:New() call) + -- Template validation is handled by MOOSE SPAWN class during actual spawning + + if squadronAvailable then + -- Get zone priority and response ratio + local zonePriority, responseRatio, zoneDescription = getThreatZonePriority(threatCoord, squadron, coalitionSide) + + -- Check if threat meets priority threshold for secondary zones + local zoneConfig = squadron.zoneConfig or getDefaultZoneConfig() + if zonePriority == "secondary" and zoneConfig.ignoreLowPriority then + if threatSize < zoneConfig.priorityThreshold then + log(coalitionName .. " " .. squadron.displayName .. " ignoring low-priority threat in secondary zone (" .. + threatSize .. " < " .. zoneConfig.priorityThreshold .. ")", true) + responseRatio = 0 + zonePriority = "none" + end + end + + if responseRatio > 0 then + local airbase = AIRBASE:FindByName(squadron.airbaseName) + local airbaseCoord = airbase:GetCoordinate() + local distance = airbaseCoord:Get2DDistance(threatCoord) + + table.insert(availableSquadrons, { + squadron = squadron, + zonePriority = zonePriority, + responseRatio = responseRatio, + distance = distance, + zoneDescription = zoneDescription + }) + + log(coalitionName .. " " .. squadron.displayName .. " can respond: " .. zoneDescription .. + " (ratio: " .. responseRatio .. ", distance: " .. math.floor(distance/1852) .. "nm)", true) + else + log(coalitionName .. " " .. squadron.displayName .. " will not respond: " .. zoneDescription, true) + end + else + log(coalitionName .. " " .. squadron.displayName .. " unavailable: " .. unavailableReason, true) + end + end + + -- Second pass: select best squadron by priority and distance + if #availableSquadrons > 0 then + -- Sort by zone priority (higher priority first), then by distance (closer first) + table.sort(availableSquadrons, function(a, b) + -- Get priority indices + local aPriorityIndex = 5 + local bPriorityIndex = 5 + for i, priority in ipairs(zonePriorityOrder) do + if a.zonePriority == priority then aPriorityIndex = i end + if b.zonePriority == priority then bPriorityIndex = i end + end + + -- First sort by priority (lower index = higher priority) + if aPriorityIndex ~= bPriorityIndex then + return aPriorityIndex < bPriorityIndex + end + + -- Then sort by distance (closer is better) + return a.distance < b.distance + end) + + local selected = availableSquadrons[1] + log("Selected " .. coalitionName .. " " .. selected.squadron.displayName .. " for response: " .. + selected.zoneDescription .. " (distance: " .. math.floor(selected.distance/1852) .. "nm)") + + return selected.squadron, selected.responseRatio, selected.zoneDescription + end + + log("No " .. coalitionName .. " squadron available for threat at coordinates") + return nil, 0, "no available squadrons" +end + +-- Launch interceptor for coalition +local function launchInterceptor(threatGroup, coalitionSide) + if not threatGroup or not threatGroup:IsAlive() then + return + end + + local threatCoord = threatGroup:GetCoordinate() + local threatName = threatGroup:GetName() + local threatSize = threatGroup:GetSize() + local coalitionSettings, coalitionName = getCoalitionSettings(coalitionSide) + local coalitionKey = (coalitionSide == coalition.side.RED) and "red" or "blue" + + -- Check if threat already has interceptors assigned + if assignedThreats[coalitionKey][threatName] then + local assignedInterceptors = assignedThreats[coalitionKey][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[coalitionKey][threatName] = nil + end + end + + -- Find best squadron using zone-based priority system first + local squadron, zoneResponseRatio, zoneDescription = findBestSquadron(threatCoord, threatSize, coalitionSide) + + if not squadron then + log("No " .. coalitionName .. " squadron available") + return + end + + -- Calculate how many interceptors to launch using zone-modified ratio + local finalInterceptRatio = coalitionSettings.interceptRatio * zoneResponseRatio + local interceptorsNeeded = math.max(1, math.ceil(threatSize * finalInterceptRatio)) + + -- Check if we have capacity + if countActiveFighters(coalitionSide) + interceptorsNeeded > coalitionSettings.maxActiveCAP then + interceptorsNeeded = coalitionSettings.maxActiveCAP - countActiveFighters(coalitionSide) + if interceptorsNeeded <= 0 then + log(coalitionName .. " max fighters airborne, skipping launch") + return + end + end + if not squadron then + log("No " .. coalitionName .. " squadron available") + return + end + + -- Limit interceptors to available aircraft + local availableAircraft = squadronAircraftCounts[coalitionKey][squadron.templateName] or 0 + interceptorsNeeded = math.min(interceptorsNeeded, availableAircraft) + + if interceptorsNeeded <= 0 then + log(coalitionName .. " Squadron " .. squadron.displayName .. " has no aircraft to launch") + return + end + + -- Launch multiple interceptors to match threat + local spawn = SPAWN:New(squadron.templateName) + if not spawn then + log("ERROR: Failed to create SPAWN object for " .. coalitionName .. " " .. squadron.templateName) + return + end + + 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 knots 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[coalitionKey][interceptor:GetName()] = { + group = interceptor, + squadron = squadron.templateName, + displayName = squadron.displayName + } + + -- Emergency cleanup (safety net) + SCHEDULER:New(nil, function() + if activeInterceptors[coalitionKey][interceptor:GetName()] then + log("Emergency cleanup of " .. coalitionName .. " " .. interceptor:GetName() .. " (should have RTB'd)") + activeInterceptors[coalitionKey][interceptor:GetName()] = nil + end + end, {}, coalitionSettings.emergencyCleanupTime) + end + end + + -- Log the launch and track assignment + if #interceptors > 0 then + -- Decrement squadron aircraft count + local currentCount = squadronAircraftCounts[coalitionKey][squadron.templateName] or 0 + squadronAircraftCounts[coalitionKey][squadron.templateName] = math.max(0, currentCount - #interceptors) + local remainingCount = squadronAircraftCounts[coalitionKey][squadron.templateName] + + log("Launched " .. #interceptors .. " x " .. coalitionName .. " " .. squadron.displayName .. " to intercept " .. + threatSize .. " x " .. threatName .. " (" .. zoneDescription .. ", ratio: " .. string.format("%.1f", finalInterceptRatio) .. + ", remaining: " .. remainingCount .. "/" .. squadron.aircraft .. ")") + assignedThreats[coalitionKey][threatName] = interceptors + lastLaunchTime[coalitionKey][threatName] = timer.getTime() + + -- Apply cooldown immediately when squadron launches + local currentTime = timer.getTime() + squadronCooldowns[coalitionKey][squadron.templateName] = currentTime + coalitionSettings.squadronCooldown + local cooldownMinutes = coalitionSettings.squadronCooldown / 60 + log(coalitionName .. " Squadron " .. squadron.displayName .. " LAUNCHED! Applying " .. cooldownMinutes .. " minute cooldown") + end +end + +-- Main threat detection loop for coalition +local function detectThreatsForCoalition(coalitionSide) + local coalitionSettings, coalitionName = getCoalitionSettings(coalitionSide) + local enemyCoalition = (coalitionSide == coalition.side.RED) and "blue" or "red" + local coalitionKey = (coalitionSide == coalition.side.RED) and "red" or "blue" + + log("Scanning for " .. coalitionName .. " threats...", true) + + -- Clean up dead threats from tracking + local currentThreats = {} + + -- Find all enemy aircraft using cached set for performance + local cacheKey = enemyCoalition .. "Aircraft" + if not cachedSets[cacheKey] then + cachedSets[cacheKey] = SET_GROUP:New():FilterCoalitions(enemyCoalition):FilterCategoryAirplane():FilterStart() + end + local enemyAircraft = cachedSets[cacheKey] + local threatCount = 0 + + enemyAircraft:ForEach(function(enemyGroup) + if enemyGroup and enemyGroup:IsAlive() then + threatCount = threatCount + 1 + currentThreats[enemyGroup:GetName()] = true + log("Found " .. coalitionName .. " threat: " .. enemyGroup:GetName() .. " (" .. enemyGroup:GetTypeName() .. ")", true) + + -- Launch interceptor for this threat + launchInterceptor(enemyGroup, coalitionSide) + end + end) + + -- Clean up assignments for threats that no longer exist and send interceptors home + for threatName, assignedInterceptors in pairs(assignedThreats[coalitionKey]) do + if not currentThreats[threatName] then + log("Threat " .. threatName .. " eliminated, sending " .. coalitionName .. " 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, coalitionSide) + end + end + else + -- Handle legacy single interceptor assignment + if assignedInterceptors and assignedInterceptors:IsAlive() then + sendInterceptorHome(assignedInterceptors, coalitionSide) + end + end + + assignedThreats[coalitionKey][threatName] = nil + end + end + + -- Count assigned threats + local assignedCount = 0 + for _ in pairs(assignedThreats[coalitionKey]) do assignedCount = assignedCount + 1 end + + log(coalitionName .. " scan complete: " .. threatCount .. " threats, " .. countActiveFighters(coalitionSide) .. " active fighters, " .. + assignedCount .. " assigned") +end + +-- Main threat detection loop - calls both coalitions +local function detectThreats() + if TADC_SETTINGS.enableRed then + detectThreatsForCoalition(coalition.side.RED) + end + + if TADC_SETTINGS.enableBlue then + detectThreatsForCoalition(coalition.side.BLUE) + end +end + +-- Monitor interceptor groups for cleanup when destroyed +local function monitorInterceptors() + -- Check RED interceptors + if TADC_SETTINGS.enableRed then + for interceptorName, interceptorData in pairs(activeInterceptors.red) 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("RED Interceptor from " .. displayName .. " destroyed: " .. interceptorName, true) + + -- Remove from active tracking + activeInterceptors.red[interceptorName] = nil + end + end + end + end + + -- Check BLUE interceptors + if TADC_SETTINGS.enableBlue then + for interceptorName, interceptorData in pairs(activeInterceptors.blue) 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("BLUE Interceptor from " .. displayName .. " destroyed: " .. interceptorName, true) + + -- Remove from active tracking + activeInterceptors.blue[interceptorName] = nil + end + end + end + end +end + +-- Periodic airbase status check +local function checkAirbaseStatus() + log("=== AIRBASE STATUS REPORT ===") + + local redUsableCount = 0 + local blueUsableCount = 0 + local currentTime = timer.getTime() + + -- Check RED airbases + if TADC_SETTINGS.enableRed then + log("=== RED COALITION STATUS ===") + for _, squadron in pairs(RED_SQUADRON_CONFIG) do + local usable, status = isAirbaseUsable(squadron.airbaseName, coalition.side.RED) + + -- Add aircraft count to status + local aircraftCount = squadronAircraftCounts.red[squadron.templateName] or 0 + local maxAircraft = squadron.aircraft + local aircraftStatus = " Aircraft: " .. aircraftCount .. "/" .. maxAircraft + + -- Add zone information if configured + local zoneStatus = "" + if squadron.primaryZone or squadron.secondaryZone or squadron.tertiaryZone then + local zones = {} + if squadron.primaryZone then table.insert(zones, "P:" .. squadron.primaryZone) end + if squadron.secondaryZone then table.insert(zones, "S:" .. squadron.secondaryZone) end + if squadron.tertiaryZone then table.insert(zones, "T:" .. squadron.tertiaryZone) end + zoneStatus = " Zones: " .. table.concat(zones, " ") + end + + -- Check if squadron is on cooldown + local cooldownStatus = "" + if squadronCooldowns.red[squadron.templateName] then + local cooldownEnd = squadronCooldowns.red[squadron.templateName] + if currentTime < cooldownEnd then + local timeLeft = math.ceil((cooldownEnd - currentTime) / 60) + cooldownStatus = " (COOLDOWN: " .. timeLeft .. "m)" + end + end + + local fullStatus = status .. aircraftStatus .. zoneStatus .. cooldownStatus + + if usable and cooldownStatus == "" and aircraftCount > 0 then + redUsableCount = redUsableCount + 1 + log("✓ " .. squadron.airbaseName .. " - " .. fullStatus) + else + log("✗ " .. squadron.airbaseName .. " - " .. fullStatus) + end + end + log("RED Status: " .. redUsableCount .. "/" .. #RED_SQUADRON_CONFIG .. " airbases operational") + end + + -- Check BLUE airbases + if TADC_SETTINGS.enableBlue then + log("=== BLUE COALITION STATUS ===") + for _, squadron in pairs(BLUE_SQUADRON_CONFIG) do + local usable, status = isAirbaseUsable(squadron.airbaseName, coalition.side.BLUE) + + -- Add aircraft count to status + local aircraftCount = squadronAircraftCounts.blue[squadron.templateName] or 0 + local maxAircraft = squadron.aircraft + local aircraftStatus = " Aircraft: " .. aircraftCount .. "/" .. maxAircraft + + -- Add zone information if configured + local zoneStatus = "" + if squadron.primaryZone or squadron.secondaryZone or squadron.tertiaryZone then + local zones = {} + if squadron.primaryZone then table.insert(zones, "P:" .. squadron.primaryZone) end + if squadron.secondaryZone then table.insert(zones, "S:" .. squadron.secondaryZone) end + if squadron.tertiaryZone then table.insert(zones, "T:" .. squadron.tertiaryZone) end + zoneStatus = " Zones: " .. table.concat(zones, " ") + end + + -- Check if squadron is on cooldown + local cooldownStatus = "" + if squadronCooldowns.blue[squadron.templateName] then + local cooldownEnd = squadronCooldowns.blue[squadron.templateName] + if currentTime < cooldownEnd then + local timeLeft = math.ceil((cooldownEnd - currentTime) / 60) + cooldownStatus = " (COOLDOWN: " .. timeLeft .. "m)" + end + end + + local fullStatus = status .. aircraftStatus .. zoneStatus .. cooldownStatus + + if usable and cooldownStatus == "" and aircraftCount > 0 then + blueUsableCount = blueUsableCount + 1 + log("✓ " .. squadron.airbaseName .. " - " .. fullStatus) + else + log("✗ " .. squadron.airbaseName .. " - " .. fullStatus) + end + end + log("BLUE Status: " .. blueUsableCount .. "/" .. #BLUE_SQUADRON_CONFIG .. " airbases operational") + end +end + +-- Cleanup old delivery records to prevent memory buildup +local function cleanupOldDeliveries() + if _G.processedDeliveries then + local currentTime = timer.getTime() + local cleanupAge = 3600 -- Remove delivery records older than 1 hour + local removedCount = 0 + + for deliveryKey, timestamp in pairs(_G.processedDeliveries) do + if currentTime - timestamp > cleanupAge then + _G.processedDeliveries[deliveryKey] = nil + removedCount = removedCount + 1 + end + end + + if removedCount > 0 then + log("Cleaned up " .. removedCount .. " old cargo delivery records", true) + end + end +end + +-- System initialization +local function initializeSystem() + log("Universal Dual-Coalition TADC starting...") + + -- Validate configuration + if not validateConfiguration() then + log("System startup aborted due to configuration errors!") + return false + end + + -- Log enabled coalitions + local enabledCoalitions = {} + if TADC_SETTINGS.enableRed then + table.insert(enabledCoalitions, "RED (" .. #RED_SQUADRON_CONFIG .. " squadrons)") + end + if TADC_SETTINGS.enableBlue then + table.insert(enabledCoalitions, "BLUE (" .. #BLUE_SQUADRON_CONFIG .. " squadrons)") + end + log("Enabled coalitions: " .. table.concat(enabledCoalitions, ", ")) + + -- Log initial squadron aircraft counts + if TADC_SETTINGS.enableRed then + for _, squadron in pairs(RED_SQUADRON_CONFIG) do + local count = squadronAircraftCounts.red[squadron.templateName] + log("Initial RED: " .. squadron.displayName .. " has " .. count .. "/" .. squadron.aircraft .. " aircraft") + end + end + + if TADC_SETTINGS.enableBlue then + for _, squadron in pairs(BLUE_SQUADRON_CONFIG) do + local count = squadronAircraftCounts.blue[squadron.templateName] + log("Initial BLUE: " .. squadron.displayName .. " has " .. count .. "/" .. squadron.aircraft .. " aircraft") + end + end + + -- Start schedulers + SCHEDULER:New(nil, detectThreats, {}, 5, TADC_SETTINGS.checkInterval) + SCHEDULER:New(nil, monitorInterceptors, {}, 10, TADC_SETTINGS.monitorInterval) + SCHEDULER:New(nil, checkAirbaseStatus, {}, 30, TADC_SETTINGS.statusReportInterval) + SCHEDULER:New(nil, monitorCargoReplenishment, {}, 15, TADC_SETTINGS.cargoCheckInterval) + SCHEDULER:New(nil, cleanupOldDeliveries, {}, 60, 3600) -- Cleanup old delivery records every hour + + log("Universal Dual-Coalition TADC operational!") + log("RED Replenishment: " .. TADC_SETTINGS.red.cargoReplenishmentAmount .. " aircraft per cargo delivery") + log("BLUE Replenishment: " .. TADC_SETTINGS.blue.cargoReplenishmentAmount .. " aircraft per cargo delivery") + + return true +end + +-- Start the system +initializeSystem() \ No newline at end of file