diff --git a/DCS_Caucasus/TADC_Example.miz b/DCS_Caucasus/TADC_Example.miz deleted file mode 100644 index b689ef7..0000000 Binary files a/DCS_Caucasus/TADC_Example.miz and /dev/null differ diff --git a/DCS_Caucasus/Test.lua b/DCS_Caucasus/Test.lua deleted file mode 100644 index e69de29..0000000 diff --git a/Universal_TADC.lua b/Universal_TADC.lua deleted file mode 100644 index f8d6a63..0000000 --- a/Universal_TADC.lua +++ /dev/null @@ -1,953 +0,0 @@ ---[[ -═══════════════════════════════════════════════════════════════════════════════ - 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 deleted file mode 100644 index 91ba99a..0000000 --- a/Universal_TADC_Dual.lua +++ /dev/null @@ -1,1538 +0,0 @@ ---[[ -═══════════════════════════════════════════════════════════════════════════════ - 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 = "Sukhumi CAP", -- Change to your RED template name - displayName = "Sukhumi CAP", -- Change to your preferred name - airbaseName = "Sukhumi-Babushara", -- 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 = "RED_BORDER", -- Main responsibility area (zone name from mission editor) - secondaryZone = nil, -- Secondary coverage area (zone name) - tertiaryZone = nil, -- Emergency/fallback zone (zone name) - - -- Zone behavior settings (optional - uses defaults if not specified) - zoneConfig = { - primaryResponse = 1.0, -- Intercept ratio multiplier in primary zone - secondaryResponse = 0.6, -- Intercept ratio multiplier in secondary zone - tertiaryResponse = 1.4, -- Intercept ratio multiplier in tertiary zone - maxRange = 200, -- Maximum engagement range from airbase (nm) - enableFallback = false, -- Auto-switch to tertiary when base threatened - priorityThreshold = 4, -- Min aircraft count for "major threat" - ignoreLowPriority = false, -- Ignore threats below threshold in secondary zones - } - }, - - -} - --- ═══════════════════════════════════════════════════════════════════════════ --- 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 = "Kutaisi CAP", -- Change to your BLUE template name - displayName = "Kutaisi CAP", -- Change to your preferred name - airbaseName = "Kutaisi", -- Change to your BLUE airbase - aircraft = 18, -- Adjust aircraft count - skill = AI.Skill.EXCELLENT, -- AVERAGE, GOOD, HIGH, EXCELLENT - altitude = 18000, -- Patrol altitude (feet) - speed = 320, -- Patrol speed (knots) - patrolTime = 22, -- Time on station (minutes) - type = "FIGHTER", - - -- Zone-based Areas of Responsibility (optional - leave nil for global response) - primaryZone = "BLUE_BORDER", -- Main responsibility area (zone name from mission editor) - secondaryZone = nil, -- Secondary coverage area (zone name) - tertiaryZone = nil, -- Emergency/fallback zone (zone name) - - -- Zone behavior settings (optional - uses defaults if not specified) - zoneConfig = { - 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 - -- Try to create polygon zone from helicopter group waypoints if not found - local group = GROUP:FindByName(zoneName) - if group then - -- Create polygon zone using the group's waypoints as vertices - zone = ZONE_POLYGON:NewFromGroupName(zoneName, zoneName) - if zone then - log("Created polygon zone '" .. zoneName .. "' from helicopter waypoints") - return zone:IsCoordinateInZone(coordinate) - else - log("Warning: Could not create polygon zone from group '" .. zoneName .. "' - check waypoints") - end - else - log("Warning: No group named '" .. zoneName .. "' found for zone creation") - end - - log("Warning: Zone '" .. zoneName .. "' not found in mission and could not create from helicopter group", 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 - -- Check if there's a helicopter unit/group with this name for zone creation - local unit = UNIT:FindByName(zoneName) - local group = GROUP:FindByName(zoneName) - if not unit and not group then - table.insert(errors, prefix .. "zone '" .. zoneName .. "' not found in mission (no zone or helicopter unit named '" .. zoneName .. "')") - end - 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 - -- Check if there's a helicopter unit/group with this name for zone creation - local unit = UNIT:FindByName(zoneName) - local group = GROUP:FindByName(zoneName) - if not unit and not group then - table.insert(errors, prefix .. "zone '" .. zoneName .. "' not found in mission (no zone or helicopter unit named '" .. zoneName .. "')") - end - 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...") - - -- Create zones from late-activated helicopter units (MOOSE method) - -- This allows using helicopters named "RED_BORDER", "BLUE_BORDER" etc. as zone markers - -- Uses the helicopter's waypoints as polygon vertices (standard MOOSE method) - local function createZoneFromUnit(unitName) - -- Try to find as a group first (this is the standard MOOSE way) - local group = GROUP:FindByName(unitName) - if group then - -- Create polygon zone using the group's waypoints as vertices - local zone = ZONE_POLYGON:NewFromGroupName(unitName, unitName) - if zone then - log("Created polygon zone '" .. unitName .. "' from helicopter waypoints") - return zone - else - log("Warning: Could not create polygon zone from group '" .. unitName .. "' - check waypoints") - end - else - log("Warning: No group named '" .. unitName .. "' found for zone creation") - end - return nil - end - - -- Try to create zones for all configured zone names - local zoneNames = {} - for _, squadron in pairs(RED_SQUADRON_CONFIG) do - if squadron.primaryZone then table.insert(zoneNames, squadron.primaryZone) end - if squadron.secondaryZone then table.insert(zoneNames, squadron.secondaryZone) end - if squadron.tertiaryZone then table.insert(zoneNames, squadron.tertiaryZone) end - end - for _, squadron in pairs(BLUE_SQUADRON_CONFIG) do - if squadron.primaryZone then table.insert(zoneNames, squadron.primaryZone) end - if squadron.secondaryZone then table.insert(zoneNames, squadron.secondaryZone) end - if squadron.tertiaryZone then table.insert(zoneNames, squadron.tertiaryZone) end - end - - -- Create zones from helicopters - for _, zoneName in ipairs(zoneNames) do - if not ZONE:FindByName(zoneName) then - createZoneFromUnit(zoneName) - end - end - - -- 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