From 4b18037c32e58005ad6578707927ee41123efc67 Mon Sep 17 00:00:00 2001 From: iTracerFacer <134304944+iTracerFacer@users.noreply.github.com> Date: Mon, 20 Oct 2025 08:21:48 -0500 Subject: [PATCH] Automatically detect when bases are captured Stop dispatching cargo to captured bases Stop using captured squadrons for intercepts Display clear status information to players showing which bases are operational vs captured Treat base capture as normal mission state, not an error condition --- .../Moose_OperationPolarShield_TADC.lua | 2682 ----------------- .../Moose_TADC_CargoDispatcher.lua | 28 +- .../Moose_TADC_Load2nd.lua | 188 +- Moose_TADC/Moose_TADC_CargoDispatcher.lua | 28 +- Moose_TADC/Moose_TADC_Load2nd.lua | 188 +- Moose_TADC/TADC_Example.miz | Bin 970918 -> 971641 bytes 6 files changed, 352 insertions(+), 2762 deletions(-) delete mode 100644 DCS_Kola/Operation_Polar_Shield/Moose_OperationPolarShield_TADC.lua diff --git a/DCS_Kola/Operation_Polar_Shield/Moose_OperationPolarShield_TADC.lua b/DCS_Kola/Operation_Polar_Shield/Moose_OperationPolarShield_TADC.lua deleted file mode 100644 index c735d0b..0000000 --- a/DCS_Kola/Operation_Polar_Shield/Moose_OperationPolarShield_TADC.lua +++ /dev/null @@ -1,2682 +0,0 @@ --- Operation Polar Shield - MOOSE Mission Script --- --- KNOWN ISSUES WITH EASYGCICAP v0.1.30: --- 1. Internal MOOSE errors during intercept assignments (_AssignIntercept) --- 2. Inconsistent wing size requests (alternates between 1 and 2 aircraft) --- 3. These errors do NOT prevent CAP operations - system works despite them --- --- VERIFICATION: Check F10 map for active CAP flights patrolling border zones --- The script successfully initializes all squadrons and patrol points. - --- STATUS: OPERATIONAL ✅ --- All squadrons added, all patrol points configured, RedCAP started successfully --- --- NEW FEATURES: --- ✅ Randomized AI Patrol System - Aircraft pick random patrol areas within their zones --- ✅ Anti-Clustering Behavior - Each aircraft patrols different areas to prevent clustering --- ✅ Dynamic Patrol Rotation - Aircraft change patrol areas every AI_PATROL_TIME seconds --- ✅ Configurable Patrol Parameters - Adjust patrol timing and area sizes via config - --- Optional: Reduce MOOSE error message boxes in game --- env.setErrorMessageBoxEnabled(false) - ---================================================================================================ --- TACTICAL AIR DEFENSE CONTROLLER (TADC) CONFIGURATION --- ================================================================================================ - --- ================================================================================================ --- TADC LOGGING HELPER FUNCTION --- ================================================================================================ - --- Helper function to prefix all TADC logging with module identifier -local function TADC_Log(level, message) - if level == "error" then - env.error("[TADC Module] " .. message) - elseif level == "warning" then - env.warning("[TADC Module] " .. message) - else - env.info("[TADC Module] " .. message) - end -end - --- SIMPLE GCI Configuration -local GCI_Config = { - - -- Basic Response Parameters - threatRatio = 0.5, -- Send 0.5x defenders per attacker - -- THREAT RATIO REFERENCE TABLE: - -- Formula: defendersNeeded = math.ceil(threat.size * threatRatio) - -- - -- Threat Size → 1 2 3 4 5 6 7 8 10 - -- Ratio 0.5 1 1 2 2 3 3 4 4 5 - -- Ratio 0.75 1 2 3 3 4 5 6 6 8 - -- Ratio 1.0 1 2 3 4 5 6 7 8 10 - -- Ratio 1.25 2 3 4 5 7 8 9 10 13 - -- Ratio 1.5 2 3 5 6 8 9 11 12 15 - -- Ratio 2.0 2 4 6 8 10 12 14 16 20 - -- - -- Current (0.5) = Conservative Response (good for limited resources) - -- Recommended 1.0 = Proportional Response (balanced) - -- Aggressive 1.5+ = Overwhelming Response (resource intensive) - - maxSimultaneousCAP = 12, -- Maximum total airborne aircraft at once - - -- EWR Detection - useEWRDetection = true, -- Use EWR radar for detection - ewrDetectionRadius = 50000, -- EWR detection radius (50km) - - -- Simple Timing - mainLoopInterval = 15, -- Check for threats every 15 seconds - mainLoopDelay = 10, -- Initial delay before starting main loop - squadronCooldown = 120, -- 2 minutes between squadron launches - - -- Supply and Squadron Management - supplyMode = "INFINITE", -- INFINITE or FINITE - defaultSquadronSize = 4, -- Default aircraft per squadron - reservePercent = 0.3, -- Reserve 30% of aircraft - responseDelay = 5, -- Delay before responding to threats - - -- CAP Management - capSetupDelay = 30, -- Delay before setting up CAP - capOrbitRadius = 10000, -- CAP orbit radius in meters - capEngagementRange = 25000, -- CAP engagement range in meters - capZoneConstraint = true, -- Keep CAP within assigned zones - - -- Spawn Parameters - spawnDistanceMin = 5000, -- Minimum spawn distance from airbase (5km) - spawnDistanceMax = 15000, -- Maximum spawn distance from airbase (15km) - - -- Mission Parameters - minPatrolDuration = 900, -- Minimum patrol duration (15 minutes) - rtbDuration = 300, -- RTB and cleanup duration (5 minutes) - AI_PATROL_TIME = 1800, -- AI patrol rotation time (30 minutes) - - -- Status and Monitoring - statusReportInterval = 300, -- Status report every 5 minutes - engagementUpdateInterval = 30, -- Update engagements every 30 seconds - - -- Combat Effectiveness Ratios - fighterVsFighter = 1.0, -- Fighter vs Fighter effectiveness - fighterVsBomber = 1.5, -- Fighter vs Bomber effectiveness - fighterVsHelicopter = 2.0, -- Fighter vs Helicopter effectiveness - - -- Threat Management - threatTimeout = 300, -- Remove threats after 5 minutes of no contact - - -- Testing and Debug - forceOmniscientDetection = false, -- Force omniscient detection for testing - debugLevel = 2, -- Verbose logging - - -- Persistent CAP Configuration - enablePersistentCAP = true, -- Enable continuous standing patrols - persistentCAPCount = 2, -- Number of persistent CAP flights to maintain - persistentCAPInterval = 600, -- Check/maintain persistent CAP every 2 minutes - persistentCAPReserve = 0.3, -- Reserve 30% of maxSimultaneousCAP slots for threat response - - -- Dynamic Resource Management - enableDynamicReserves = true, -- Adjust reserves based on threat level - highThreatReserve = 0.4, -- 40% reserve during high threat - lowThreatReserve = 0.2, -- 20% reserve during low threat - threatThreshold = 3, -- Number of threats to trigger high alert - persistentCAPPriority = { -- Priority order for persistent CAP squadrons - "FIGHTER_SWEEP_RED_Severomorsk-1", -- Primary intercept base - "FIGHTER_SWEEP_RED_Olenya", -- Northern coverage - "FIGHTER_SWEEP_RED_Murmansk", -- Western coverage - "HELO_SWEEP_RED_Afrikanda" -- Helicopter patrol - }, - - -- Optional Features - initialStandingPatrols = true -- Launch standing patrols on startup (ENABLED FOR TESTING) -} - - --- Initialize TADC Data Structures - Must be defined before any usage -local TADC = { - -- Squadron Management - squadrons = {}, -- Squadron data and status - activeCAPs = {}, -- Currently airborne CAP flights - launchQueue = {}, -- Pending launch orders - - -- Threat Tracking - threats = {}, -- Detected threat contacts - threatHistory = {}, -- Historical threat data - - -- Mission Control - missions = {}, -- Active intercept missions - reserves = {}, -- Aircraft held in reserve - threatAssignments = {}, -- Maps threat IDs to assigned squadrons - squadronMissions = {}, -- Maps squadrons to their current threats - - -- Persistent CAP Management - persistentCAPs = {}, -- Currently active persistent CAP flights - lastPersistentCheck = 0, -- Last time persistent CAP was checked/maintained - - -- Enhanced Statistics & Performance Monitoring - stats = { - threatsDetected = 0, - interceptsLaunched = 0, - successfulEngagements = 0, - aircraftLost = 0, - avgResponseTime = 0, - maxResponseTime = 0, - systemLoadTime = 0, - memoryUsage = 0 - }, - - -- Performance tracking - performance = { - lastLoopTime = 0, - avgLoopTime = 0, - maxLoopTime = 0, - loopCount = 0 - } -} - --- Configuration Validation -local function validateConfiguration() - local errors = {} - - if GCI_Config.maxSimultaneousCAP < 1 then - table.insert(errors, "maxSimultaneousCAP must be at least 1") - end - - if GCI_Config.threatRatio <= 0 then - table.insert(errors, "threatRatio must be positive") - end - - if GCI_Config.reservePercent < 0 or GCI_Config.reservePercent > 1 then - table.insert(errors, "reservePercent must be between 0 and 1") - end - - if #errors > 0 then - TADC_Log("error", "TADC Configuration Errors:") - for _, error in pairs(errors) do - TADC_Log("error", " - " .. error) - end - return false - end - - return true -end - --- Setup Distributed Multi-Base CAP System --- This creates a dynamic response system where each squadron launches from its designated airbase --- and responds to threats based on proximity and zone coverage. - --- Create border zones with error checking -local redBorderGroup = GROUP:FindByName("RED BORDER") -local heloBorderGroup = GROUP:FindByName("HELO BORDER") - -if redBorderGroup then - CCCPBorderZone = ZONE_POLYGON:New("RED BORDER", redBorderGroup) - TADC_Log("info", "RED BORDER zone created successfully") -else - TADC_Log("error", "RED BORDER group not found!") - return -end - -if heloBorderGroup then - HeloBorderZone = ZONE_POLYGON:New("HELO BORDER", heloBorderGroup) - TADC_Log("info", "HELO BORDER zone created successfully") -else - TADC_Log("error", "HELO BORDER group not found!") - return -end - --- Define squadron configurations with their designated airbases and patrol zones -local squadronConfigs = { - -- Fixed-wing fighters patrol RED BORDER zone - { - templateName = "FIGHTER_SWEEP_RED_Kilpyavr", - displayName = "Kilpyavr CAP", - airbaseName = "Kilpyavr", - patrolZone = CCCPBorderZone, - aircraft = 1, - skill = AI.Skill.GOOD, - altitude = 15000, - speed = 300, - patrolTime = 20, - type = "FIGHTER" - }, - { - templateName = "FIGHTER_SWEEP_RED_Severomorsk-1", - displayName = "Severomorsk-1 CAP", - airbaseName = "Severomorsk-1", - patrolZone = CCCPBorderZone, - aircraft = 1, - skill = AI.Skill.GOOD, - altitude = 20000, - speed = 350, - patrolTime = 25, - type = "FIGHTER" - }, - { - templateName = "FIGHTER_SWEEP_RED_Severomorsk-3", - displayName = "Severomorsk-3 CAP", - airbaseName = "Severomorsk-3", - patrolZone = CCCPBorderZone, - aircraft = 1, - skill = AI.Skill.GOOD, - altitude = 25000, - speed = 400, - patrolTime = 30, - type = "FIGHTER" - }, - { - templateName = "FIGHTER_SWEEP_RED_Murmansk", - displayName = "Murmansk CAP", - airbaseName = "Murmansk International", - patrolZone = CCCPBorderZone, - aircraft = 1, - skill = AI.Skill.GOOD, - altitude = 18000, - speed = 320, - patrolTime = 22, - type = "FIGHTER" - }, - { - templateName = "FIGHTER_SWEEP_RED_Monchegorsk", - displayName = "Monchegorsk CAP", - airbaseName = "Monchegorsk", - patrolZone = CCCPBorderZone, - aircraft = 1, - skill = AI.Skill.GOOD, - altitude = 22000, - speed = 380, - patrolTime = 25, - type = "FIGHTER" - }, - { - templateName = "FIGHTER_SWEEP_RED_Olenya", - displayName = "Olenya CAP", - airbaseName = "Olenya", - patrolZone = CCCPBorderZone, - aircraft = 1, - skill = AI.Skill.GOOD, - altitude = 30000, - speed = 450, - patrolTime = 35, - type = "FIGHTER" - }, - -- Helicopter squadron patrols HELO BORDER zone - { - templateName = "HELO_SWEEP_RED_Afrikanda", - displayName = "Afrikanda Helo CAP", - airbaseName = "Afrikanda", - patrolZone = HeloBorderZone, - aircraft = 4, - skill = AI.Skill.GOOD, - altitude = 1000, - speed = 150, - patrolTime = 30, - type = "HELICOPTER" - } -} - --- Check which squadron templates exist in the mission -TADC_Log("info", "=== CHECKING SQUADRON TEMPLATES ===") -local availableSquadrons = {} - --- First, let's verify what airbases are actually available -TADC_Log("info", "=== VERIFYING AIRBASE NAMES ===") -local testAirbaseNames = { - "Kilpyavr", "Severomorsk-1", "Severomorsk-3", - "Murmansk International", "Monchegorsk", "Olenya", "Afrikanda" -} -for _, airbaseName in pairs(testAirbaseNames) do - local airbaseObj = AIRBASE:FindByName(airbaseName) - if airbaseObj then - TADC_Log("info", "✓ Airbase found: " .. airbaseName) - else - TADC_Log("info", "✗ Airbase NOT found: " .. airbaseName) - end -end - --- ================================================================================================ --- SQUADRON MANAGEMENT SYSTEM --- ================================================================================================ - -local function initializeSquadron(config) - return { - -- Basic Info - templateName = config.templateName, - displayName = config.displayName, - airbaseName = config.airbaseName, - type = config.type, - - -- Aircraft Management - totalAircraft = GCI_Config.supplyMode == "INFINITE" and 999 or (config.totalAircraft or GCI_Config.defaultSquadronSize), - availableAircraft = GCI_Config.supplyMode == "INFINITE" and 999 or (config.totalAircraft or GCI_Config.defaultSquadronSize), - airborneAircraft = 0, - reserveAircraft = 0, - - -- Mission Parameters - patrolZone = config.patrolZone, - altitude = config.altitude, - speed = config.speed, - patrolTime = config.patrolTime, - skill = config.skill, - homebase = AIRBASE:FindByName(config.airbaseName), -- Add homebase reference - - -- Enhanced Status Management - readinessLevel = "READY", -- READY, BUSY, MAINTENANCE, UNAVAILABLE, ALERT - lastLaunch = -GCI_Config.squadronCooldown, -- Allow immediate launch (set to -cooldown) - launchCooldown = GCI_Config.squadronCooldown, -- Cooldown from config - alertLevel = "GREEN", -- GREEN, YELLOW, RED - maintenanceUntil = 0, -- Time when maintenance completes - fatigue = 0, -- Pilot fatigue factor (0-100) - - -- Statistics - sorties = 0, - kills = 0, - losses = 0 - } -end - -TADC_Log("info", "=== INITIALIZING SQUADRON DATABASE ===") -for _, config in pairs(squadronConfigs) do - local template = GROUP:FindByName(config.templateName) - if template then - TADC_Log("info", "✓ Found squadron template: " .. config.templateName) - - -- Verify airbase exists and is Red coalition - local airbaseObj = AIRBASE:FindByName(config.airbaseName) - if airbaseObj then - local airbaseCoalition = airbaseObj:GetCoalition() - if airbaseCoalition == 1 then -- Red coalition - TADC_Log("info", " ✓ Airbase verified: " .. config.airbaseName .. " (Red Coalition)") - - -- Initialize squadron in TADC database - local squadron = initializeSquadron(config) - TADC.squadrons[config.templateName] = squadron - availableSquadrons[config.templateName] = config -- Keep for compatibility - - TADC_Log("info", " ✓ Squadron initialized: " .. squadron.availableAircraft .. " aircraft available") - else - TADC_Log("info", " ✗ Airbase " .. config.airbaseName .. " not Red coalition - squadron disabled") - end - else - TADC_Log("info", " ✗ Airbase NOT found: " .. config.airbaseName .. " - squadron disabled") - end - else - TADC_Log("info", "✗ Missing squadron template: " .. config.templateName) - end -end - -local squadronCount = 0 -local totalAircraft = 0 -for _, squadron in pairs(TADC.squadrons) do - squadronCount = squadronCount + 1 - totalAircraft = totalAircraft + squadron.availableAircraft -end - -TADC_Log("info", "✓ TADC Squadron Database: " .. squadronCount .. " squadrons, " .. totalAircraft .. " total aircraft") -if GCI_Config.supplyMode == "INFINITE" then - TADC_Log("info", "✓ Supply Mode: INFINITE - unlimited aircraft spawning") -else - TADC_Log("info", "✓ Supply Mode: FINITE - " .. totalAircraft .. " aircraft available") -end - --- ================================================================================================ --- TACTICAL AIR DEFENSE CONTROLLER (TADC) SYSTEM --- A comprehensive GCI system for intelligent air defense coordination --- ================================================================================================ - -TADC_Log("info", "=== INITIALIZING TACTICAL AIR DEFENSE CONTROLLER ===") - --- Create EWR Detection Network with Detection System -local RedEWR = SET_GROUP:New():FilterPrefixes("RED-EWR"):FilterStart() -local RedDetection = nil - --- Check EWR network availability -TADC_Log("info", "Searching for EWR groups with prefix 'RED-EWR'...") -TADC_Log("info", "Found " .. RedEWR:Count() .. " EWR groups") - --- TESTING: Force disable EWR detection if configured -if GCI_Config.forceOmniscientDetection then - TADC_Log("info", "✓ TESTING MODE: Forcing omniscient detection (EWR disabled)") - GCI_Config.useEWRDetection = false -end - -if GCI_Config.useEWRDetection and RedEWR:Count() > 0 then - TADC_Log("info", "✓ Red EWR Network: " .. RedEWR:Count() .. " detection groups") - - -- Create MOOSE Detection system using EWR network (basic version for compatibility) - local success, errorMsg = pcall(function() - RedDetection = DETECTION_AREAS:New(RedEWR, GCI_Config.ewrDetectionRadius) - RedDetection:Start() - end) - - if success then - TADC_Log("info", "✓ EWR-based threat detection system initialized (" .. (GCI_Config.ewrDetectionRadius/1000) .. "km range)") - else - TADC_Log("info", "⚠ EWR detection failed: " .. tostring(errorMsg) .. " - falling back to omniscient detection") - RedDetection = nil - end -else - if GCI_Config.useEWRDetection then - TADC_Log("info", "⚠ Warning: No RED-EWR groups found - falling back to omniscient detection") - else - TADC_Log("info", "✓ Using omniscient detection (EWR detection disabled in config)") - end -end - --- ================================================================================================ --- BACKUP IMMEDIATE RESPONSE SYSTEM (Like RU_INTERCEPT) --- ================================================================================================ - --- Forward declarations -local launchInterceptMission - --- Immediate response timer for aggressive intercepts -local lastImmediateCheck = 0 -local immediateResponseInterval = 5 -- Check every 5 seconds - --- Simple immediate intercept function (backup to complex TADC system) -local function immediateInterceptCheck() - local currentTime = timer.getTime() - if currentTime - lastImmediateCheck < immediateResponseInterval then - return - end - lastImmediateCheck = currentTime - - -- Quick scan for blue aircraft in border zones - local BlueAircraft = SET_GROUP:New():FilterCoalitions("blue"):FilterCategoryAirplane():FilterStart() - local threatsFound = 0 - - BlueAircraft:ForEach(function(blueGroup) - if blueGroup and blueGroup:IsAlive() then - local blueCoord = blueGroup:GetCoordinate() - local inRedZone = CCCPBorderZone and CCCPBorderZone:IsCoordinateInZone(blueCoord) - local inHeloZone = HeloBorderZone and HeloBorderZone:IsCoordinateInZone(blueCoord) - - if inRedZone or inHeloZone then - threatsFound = threatsFound + 1 - TADC_Log("info", "🚨 IMMEDIATE THREAT: " .. blueGroup:GetName() .. " in " .. (inRedZone and "RED_BORDER" or "HELO_BORDER")) - - -- Find nearest ready squadron and launch immediately - local bestSquadron = nil - local bestDistance = 999999 - - for templateName, squadron in pairs(TADC.squadrons) do - if squadron.readinessLevel == "READY" and squadron.availableAircraft > 0 then - local squadronCoord = squadron.homebase and squadron.homebase:GetCoordinate() - if squadronCoord then - local distance = squadronCoord:Get2DDistance(blueCoord) - if distance < bestDistance then - bestDistance = distance - bestSquadron = squadron - end - end - end - end - - if bestSquadron then - TADC_Log("info", "🚀 IMMEDIATE LAUNCH: " .. bestSquadron.displayName .. " responding to immediate threat") - -- Launch using simplified parameters - local simpleThreat = { - id = blueGroup:GetName(), - group = blueGroup, - coordinate = blueCoord, - classification = "FIGHTER", - zone = inRedZone and "RED_BORDER" or "HELO_BORDER" - } - - -- Find the original squadron config - local squadronConfig = nil - for _, config in pairs(squadronConfigs) do - if config.templateName == bestSquadron.templateName then - squadronConfig = config - break - end - end - - if squadronConfig then - launchInterceptMission(squadronConfig, simpleThreat, "IMMEDIATE_RESPONSE") - else - TADC_Log("error", "Could not find config for immediate response squadron: " .. bestSquadron.templateName) - end - end - end - end - end) - - if threatsFound > 0 then - TADC_Log("info", "🚨 IMMEDIATE SCAN: " .. threatsFound .. " threats found in border zones") - end -end - --- ================================================================================================ --- THREAT DETECTION AND ASSESSMENT SYSTEM --- ================================================================================================ - -local function classifyThreat(group) - local category = group:GetCategory() - local typeName = group:GetTypeName() or "Unknown" - - -- Classify by DCS category and type name - if category == Group.Category.AIRPLANE then - if string.find(typeName:upper(), "B-") or string.find(typeName:upper(), "BOMBER") then - return "BOMBER" - elseif string.find(typeName:upper(), "A-") or string.find(typeName:upper(), "ATTACK") then - return "ATTACK" - else - return "FIGHTER" - end - elseif category == Group.Category.HELICOPTER then - return "HELICOPTER" - else - return "UNKNOWN" - end -end - --- ================================================================================================ --- SMART THREAT PRIORITIZATION SYSTEM --- ================================================================================================ - --- Strategic target definitions with importance weights -local STRATEGIC_TARGETS = { - -- Airbases (highest priority) - {name = "Severomorsk-1", coord = nil, importance = 100, type = "AIRBASE"}, - {name = "Olenya", coord = nil, importance = 95, type = "AIRBASE"}, - {name = "Murmansk", coord = nil, importance = 90, type = "AIRBASE"}, - {name = "Afrikanda", coord = nil, importance = 85, type = "AIRBASE"}, - - -- SAM sites (medium-high priority) - {name = "SA-10 Sites", coord = nil, importance = 70, type = "SAM"}, - {name = "SA-11 Sites", coord = nil, importance = 60, type = "SAM"}, - - -- Command centers (high priority) - {name = "Command Centers", coord = nil, importance = 80, type = "COMMAND"} -} - --- Initialize strategic target coordinates -local function initializeStrategicTargets() - for _, target in pairs(STRATEGIC_TARGETS) do - if target.type == "AIRBASE" then - local airbase = AIRBASE:FindByName(target.name) - if airbase then - target.coord = airbase:GetCoordinate() - if GCI_Config.debugLevel >= 1 then - TADC_Log("info", "✓ Strategic target: " .. target.name .. " (Importance: " .. target.importance .. ")") - end - end - end - end -end - --- Enhanced multi-factor threat priority calculation -local function calculateThreatPriority(classification, coordinate, size, velocity, heading, group) - local priority = 0.0 - - -- 1. BASE THREAT TYPE PRIORITY (40% weight) - local basePriority = 0 - if classification == "BOMBER" then - basePriority = 100 -- Highest threat - can destroy strategic targets - elseif classification == "ATTACK" then - basePriority = 85 -- High threat - ground attack capability - elseif classification == "FIGHTER" then - basePriority = 70 -- Medium threat - air superiority - elseif classification == "HELICOPTER" then - basePriority = 50 -- Lower threat but still dangerous - else - basePriority = 30 -- Unknown/other aircraft - end - priority = priority + (basePriority * 0.4) - - -- 2. FORMATION SIZE FACTOR (15% weight) - local sizeMultiplier = 1.0 - if size and size > 1 then - sizeMultiplier = 1.0 + ((size - 1) * 0.3) -- Each additional aircraft adds 30% - sizeMultiplier = math.min(sizeMultiplier, 2.5) -- Cap at 250% - end - priority = priority * sizeMultiplier - - -- 3. STRATEGIC PROXIMITY ANALYSIS (25% weight) - local proximityScore = 0 - if coordinate then - local maxProximityScore = 0 - - for _, target in pairs(STRATEGIC_TARGETS) do - if target.coord then - local distance = coordinate:Get2DDistance(target.coord) - local threatRadius = 75000 -- 75km threat radius - - if distance <= threatRadius then - -- Exponential decay - closer threats much more dangerous - local proximityFactor = math.exp(-distance / (threatRadius * 0.3)) - local targetScore = (target.importance / 100) * proximityFactor * 100 - maxProximityScore = math.max(maxProximityScore, targetScore) - - if GCI_Config.debugLevel >= 2 then - TADC_Log("info", "Threat proximity: " .. target.name .. " (" .. math.floor(distance/1000) .. "km) Score: " .. math.floor(targetScore)) - end - end - end - end - - proximityScore = maxProximityScore - end - priority = priority + (proximityScore * 0.25) - - -- 4. SPEED AND HEADING ANALYSIS (10% weight) - local speedThreatFactor = 0 - if velocity and coordinate then - -- Calculate speed magnitude from velocity vector - local speed = 0 - if type(velocity) == "table" then - -- DCS velocity is typically a 3D vector {x, y, z} - local x = velocity.x or velocity[1] or 0 - local y = velocity.y or velocity[2] or 0 - local z = velocity.z or velocity[3] or 0 - speed = math.sqrt(x*x + y*y + z*z) - elseif type(velocity) == "number" then - speed = velocity - end - - -- Fast-moving aircraft are more threatening (less intercept time) - if speed > 250 then -- ~500 knots - speedThreatFactor = 25 -- Very fast (supersonic) - elseif speed > 150 then -- ~300 knots - speedThreatFactor = 15 -- Fast - elseif speed > 75 then -- ~150 knots - speedThreatFactor = 10 -- Medium speed - else - speedThreatFactor = 5 -- Slow/hovering - end - - -- Analyze heading threat (if heading toward strategic targets) - if heading and type(heading) == "number" then - local headingThreatBonus = 0 - for _, target in pairs(STRATEGIC_TARGETS) do - if target.coord then - local bearingToTarget = coordinate:GetAngleDegrees(coordinate:GetDirectionVec3(target.coord)) - local headingDiff = math.abs(heading - bearingToTarget) - if headingDiff > 180 then headingDiff = 360 - headingDiff end - - -- If heading within 30 degrees of target, significant bonus - if headingDiff <= 30 then - headingThreatBonus = math.max(headingThreatBonus, 20 * (target.importance / 100)) - elseif headingDiff <= 60 then - headingThreatBonus = math.max(headingThreatBonus, 10 * (target.importance / 100)) - end - end - end - speedThreatFactor = speedThreatFactor + headingThreatBonus - end - end - priority = priority + (speedThreatFactor * 0.1) - - -- 5. TEMPORAL FACTORS (10% weight) - local temporalFactor = 0 - local currentTime = timer.getTime() - - -- Night operations (reduced visibility for defenders) - local timeOfDay = (currentTime % 86400) / 3600 -- Hours since midnight - if timeOfDay >= 20 or timeOfDay <= 6 then -- Night time - temporalFactor = temporalFactor + 15 - end - - -- Weather considerations (if available) - -- Note: DCS weather API would be needed for full implementation - - priority = priority + (temporalFactor * 0.1) - - -- 6. ELECTRONIC WARFARE CONSIDERATIONS - local ewFactor = 0 - if group then - -- Check for jamming or stealth characteristics - local typeName = group:GetTypeName() or "" - if string.find(typeName:upper(), "EA-") or string.find(typeName:upper(), "EF-") then - ewFactor = 20 -- Electronic warfare aircraft are high priority - elseif string.find(typeName:upper(), "F-22") or string.find(typeName:upper(), "F-35") then - ewFactor = 15 -- Stealth aircraft harder to track - end - end - priority = priority + ewFactor - - -- Final priority clamping and rounding - priority = math.max(1, math.min(priority, 200)) -- Clamp between 1-200 - - if GCI_Config.debugLevel >= 2 then - TADC_Log("info", string.format("Smart Priority: %s (x%d) = %.1f [Base:%.1f, Size:%.1f, Prox:%.1f, Speed:%.1f, Time:%.1f, EW:%.1f]", - classification, size or 1, priority, basePriority, sizeMultiplier, proximityScore, speedThreatFactor, temporalFactor, ewFactor)) - end - - return math.ceil(priority) -end - --- Enhanced threat assessment with predictive analysis -local function assessThreatWithPrediction(threats) - local assessedThreats = {} - local currentTime = timer.getTime() - - for threatId, threat in pairs(threats) do - local assessment = { - threat = threat, - priority = threat.priority or 1, - timeToTarget = nil, - predictedPosition = nil, - interceptWindow = nil, - recommendedResponse = "STANDARD" - } - - -- Predictive position analysis - if threat.coordinate and threat.velocity and threat.heading then - -- Calculate speed from velocity vector - local speed = 0 - if type(threat.velocity) == "table" then - local x = threat.velocity.x or threat.velocity[1] or 0 - local y = threat.velocity.y or threat.velocity[2] or 0 - local z = threat.velocity.z or threat.velocity[3] or 0 - speed = math.sqrt(x*x + y*y + z*z) - elseif type(threat.velocity) == "number" then - speed = threat.velocity - end - - -- Predict position in 5 minutes - local futureTime = 300 -- 5 minutes - local futureDistance = speed * futureTime - assessment.predictedPosition = threat.coordinate:Translate(futureDistance, threat.heading) - - -- Calculate time to closest strategic target - local minTimeToTarget = math.huge - for _, target in pairs(STRATEGIC_TARGETS) do - if target.coord then - local distance = threat.coordinate:Get2DDistance(target.coord) - local timeToTarget = distance / math.max(speed, 50) -- Minimum 50 m/s - minTimeToTarget = math.min(minTimeToTarget, timeToTarget) - end - end - assessment.timeToTarget = minTimeToTarget - - -- Determine recommended response urgency - if minTimeToTarget < 300 then -- Less than 5 minutes - assessment.recommendedResponse = "EMERGENCY" - assessment.priority = assessment.priority * 1.5 - elseif minTimeToTarget < 600 then -- Less than 10 minutes - assessment.recommendedResponse = "URGENT" - assessment.priority = assessment.priority * 1.3 - elseif minTimeToTarget < 1200 then -- Less than 20 minutes - assessment.recommendedResponse = "HIGH" - assessment.priority = assessment.priority * 1.1 - end - end - - assessedThreats[threatId] = assessment - end - - -- Sort by priority (highest first) - local sortedThreats = {} - for _, assessment in pairs(assessedThreats) do - table.insert(sortedThreats, assessment) - end - - table.sort(sortedThreats, function(a, b) - return a.priority > b.priority - end) - - return sortedThreats, assessedThreats -end - -local function assessThreatStrength(threats) - local fighters = 0 - local bombers = 0 - local helicopters = 0 - local totalThreat = 0 - - for _, threat in pairs(threats) do - local aircraft = threat.size or 1 - if threat.classification == "FIGHTER" then - fighters = fighters + aircraft - totalThreat = totalThreat + (aircraft * GCI_Config.fighterVsFighter) - elseif threat.classification == "BOMBER" then - bombers = bombers + aircraft - totalThreat = totalThreat + (aircraft * GCI_Config.fighterVsBomber) - elseif threat.classification == "HELICOPTER" then - helicopters = helicopters + aircraft - totalThreat = totalThreat + (aircraft * GCI_Config.fighterVsHelicopter) - end - end - - return { - fighters = fighters, - bombers = bombers, - helicopters = helicopters, - totalAircraft = fighters + bombers + helicopters, - requiredDefenders = math.ceil(totalThreat) - } -end - --- SIMPLE THREAT DETECTION - Just find blue aircraft detected by EWR in border zones -local function simpleDetectThreats() - local newThreats = {} -- This will be our return value - local currentTime = timer.getTime() - - TADC_Log("info", "Checking for threats...") - - -- Use EWR detection if available and enabled - if GCI_Config.useEWRDetection and RedDetection then - local detectedItems = RedDetection:GetDetectedItems() - TADC_Log("info", "EWR detected " .. #detectedItems .. " items") - - for _, detectedItem in pairs(detectedItems) do - local detectedSet = detectedItem.Set - if detectedSet then - detectedSet:ForEach(function(blueGroup) - if blueGroup and blueGroup:IsAlive() and blueGroup:GetCoalition() == coalition.side.BLUE then - local coord = blueGroup:GetCoordinate() - - -- Check if in border zones - local inRedZone = CCCPBorderZone and CCCPBorderZone:IsCoordinateInZone(coord) - local inHeloZone = HeloBorderZone and HeloBorderZone:IsCoordinateInZone(coord) - - if inRedZone or inHeloZone then - local threatId = blueGroup:GetName() - local classification = classifyThreat(blueGroup) - local size = blueGroup:GetSize() - local heading = blueGroup:GetHeading() - local velocity = blueGroup:GetVelocity() - - -- Enhanced threat data with smart prioritization - newThreats[threatId] = { - id = threatId, - group = blueGroup, - coordinate = coord, - classification = classification, - size = size, - zone = inRedZone and "RED_BORDER" or "HELO_BORDER", - firstDetected = TADC.threats[threatId] and TADC.threats[threatId].firstDetected or currentTime, - lastSeen = currentTime, - heading = heading, - velocity = velocity, - priority = calculateThreatPriority(classification, coord, size, velocity, heading, blueGroup), - detectionMethod = "EWR", - -- Additional smart assessment data - typeName = blueGroup:GetTypeName(), - altitude = coord:GetLandHeight() + (blueGroup:GetCoordinate():GetY() or 0) - } - - -- Update statistics - if not TADC.threats[threatId] then - TADC.stats.threatsDetected = TADC.stats.threatsDetected + 1 - if GCI_Config.debugLevel >= 1 then - TADC_Log("info", "NEW EWR THREAT: " .. threatId .. " (" .. classification .. ", " .. size .. " aircraft) in " .. newThreats[threatId].zone) - end - end - end - end - end) - end - end - else - -- Fallback: Omniscient detection (original method) - TADC_Log("info", "🔍 Using OMNISCIENT detection mode") - TADC_Log("info", "🔍 Zone check - CCCPBorderZone: " .. tostring(CCCPBorderZone ~= nil) .. ", HeloBorderZone: " .. tostring(HeloBorderZone ~= nil)) - - local BlueAircraft = SET_GROUP:New():FilterCoalitions("blue"):FilterCategoryAirplane():FilterStart() - - -- DEBUG: Log how many blue aircraft we found - local blueCount = BlueAircraft:Count() - TADC_Log("info", "🔍 OMNISCIENT SCAN: Found " .. blueCount .. " blue aircraft on map") - if blueCount == 0 then - TADC_Log("warning", "⚠ No blue aircraft found on map - check coalition filtering") - end - - BlueAircraft:ForEach(function(blueGroup) - if blueGroup and blueGroup:IsAlive() then - local blueCoord = blueGroup:GetCoordinate() - local threatId = blueGroup:GetName() - - -- DEBUG: Log each blue aircraft found - TADC_Log("info", "🔍 CHECKING BLUE AIRCRAFT: " .. threatId .. " (" .. blueGroup:GetTypeName() .. ") at " .. blueCoord:ToStringLLDMS()) - - -- Check if threat is in any patrol zone - local inRedZone = CCCPBorderZone and CCCPBorderZone:IsCoordinateInZone(blueCoord) - local inHeloZone = HeloBorderZone and HeloBorderZone:IsCoordinateInZone(blueCoord) - - -- DEBUG: Log zone check results - TADC_Log("info", "🔍 Zone Check - RED: " .. tostring(inRedZone) .. ", HELO: " .. tostring(inHeloZone)) - - if inRedZone or inHeloZone then - TADC_Log("info", "🎯 THREAT IN ZONE: " .. threatId .. " detected in " .. (inRedZone and "RED_BORDER" or "HELO_BORDER")) - - local classification = classifyThreat(blueGroup) - local size = blueGroup:GetSize() - local heading = blueGroup:GetHeading() - local velocity = blueGroup:GetVelocity() - - TADC_Log("info", "🎯 Details: " .. classification .. ", " .. size .. " aircraft, heading " .. heading .. "°, " .. velocity .. " kts") - - newThreats[threatId] = { - id = threatId, - group = blueGroup, - coordinate = blueCoord, - classification = classification, - size = size, - zone = inRedZone and "RED_BORDER" or "HELO_BORDER", - firstDetected = TADC.threats[threatId] and TADC.threats[threatId].firstDetected or currentTime, - lastSeen = currentTime, - heading = heading, - velocity = velocity, - priority = calculateThreatPriority(classification, blueCoord, size, velocity, heading, blueGroup), - detectionMethod = "OMNISCIENT", - -- Additional smart assessment data - typeName = blueGroup:GetTypeName(), - altitude = blueCoord:GetLandHeight() + (blueGroup:GetCoordinate():GetY() or 0) - } - - -- Update statistics - if not TADC.threats[threatId] then - TADC.stats.threatsDetected = TADC.stats.threatsDetected + 1 - TADC_Log("info", "✅ NEW THREAT REGISTERED: " .. threatId .. " (" .. classification .. ", " .. size .. " aircraft) in " .. newThreats[threatId].zone) - else - TADC_Log("info", "🔄 EXISTING THREAT UPDATED: " .. threatId) - end - else - TADC_Log("debug", "❌ Aircraft " .. threatId .. " not in patrol zones - ignored") - end - end - end) - end - - -- Remove old threats (not seen for threatTimeout seconds) - for threatId, threat in pairs(TADC.threats) do - if not newThreats[threatId] and (currentTime - threat.lastSeen) > GCI_Config.threatTimeout then - if GCI_Config.debugLevel >= 1 then - TADC_Log("info", "THREAT TIMEOUT: " .. threatId .. " - removing from threat picture") - end - end - end - - TADC.threats = newThreats - - -- Summary logging - local threatCount = 0 - for _ in pairs(newThreats) do threatCount = threatCount + 1 end - TADC_Log("info", "📊 Threat picture update complete: " .. threatCount .. " active threats") - - return newThreats -end - -local function findOptimalDefenders(threats, zone) - local assessment = assessThreatStrength(threats) - local availableSquadrons = {} - local totalAvailable = 0 - - -- Find available squadrons for this zone - local currentTime = timer.getTime() - for templateName, squadron in pairs(TADC.squadrons) do - -- Check availability, aircraft count, AND cooldown status - local cooldownRemaining = squadron.launchCooldown - (currentTime - squadron.lastLaunch) - if squadron.readinessLevel == "READY" and - squadron.availableAircraft > 0 and - cooldownRemaining <= 0 then - -- Check if squadron type matches zone - local canRespond = false - if zone == "RED_BORDER" and squadron.type == "FIGHTER" then - canRespond = true - elseif zone == "HELO_BORDER" and squadron.type == "HELICOPTER" then - canRespond = true - end - - if canRespond then - local airbaseObj = AIRBASE:FindByName(squadron.airbaseName) - if airbaseObj then - -- Calculate average distance to threats - local totalDistance = 0 - local threatCount = 0 - for _, threat in pairs(threats) do - if threat.zone == zone then - local distance = threat.coordinate:Get2DDistance(airbaseObj:GetCoordinate()) - totalDistance = totalDistance + distance - threatCount = threatCount + 1 - end - end - - if threatCount > 0 then - availableSquadrons[templateName] = { - squadron = squadron, - averageDistance = totalDistance / threatCount, - priority = squadron.type == "FIGHTER" and 2 or 1 - } - totalAvailable = totalAvailable + squadron.availableAircraft - end - end - end - end - end - - -- Sort by distance and priority - local sortedSquadrons = {} - for templateName, data in pairs(availableSquadrons) do - table.insert(sortedSquadrons, {templateName = templateName, data = data}) - end - - table.sort(sortedSquadrons, function(a, b) - if a.data.priority ~= b.data.priority then - return a.data.priority > b.data.priority - else - return a.data.averageDistance < b.data.averageDistance - end - end) - - return sortedSquadrons, assessment, totalAvailable -end - --- ================================================================================================ --- MISSION PLANNING AND LAUNCH COORDINATION --- ================================================================================================ - -local function calculateRequiredForce(threats, zone) - local zoneThreats = {} - for _, threat in pairs(threats) do - if threat.zone == zone then - table.insert(zoneThreats, threat) - end - end - - if #zoneThreats == 0 then - return 0, {} - end - - local assessment = assessThreatStrength(zoneThreats) - return assessment.requiredDefenders, zoneThreats -end - -local function assignThreatsToSquadrons(threats, zone) - TADC_Log("info", "🎯 ASSIGNING THREATS TO SQUADRONS for " .. zone) - - local zoneThreats = {} - for _, threat in pairs(threats) do - if threat.zone == zone then - table.insert(zoneThreats, threat) - end - end - - TADC_Log("info", "Found " .. #zoneThreats .. " threats in " .. zone) - - if #zoneThreats == 0 then - TADC_Log("info", "No threats in zone, returning empty assignments") - return {} - end - - -- Get enhanced threat assessment with smart prioritization - local sortedThreats, threatAssessments = assessThreatWithPrediction(zoneThreats) - - -- Smart threat assessment logging - if GCI_Config.debugLevel >= 1 and #sortedThreats > 0 then - TADC_Log("info", "=== SMART THREAT ASSESSMENT: " .. zone .. " ===") - for i, assessment in ipairs(sortedThreats) do - local threat = assessment.threat - local timeStr = assessment.timeToTarget and string.format("%.1fm", assessment.timeToTarget/60) or "N/A" - TADC_Log("info", string.format("%d. %s (%s x%d) Priority:%d TTT:%s Response:%s", - i, threat.id, threat.classification, threat.size or 1, - math.floor(assessment.priority), timeStr, assessment.recommendedResponse)) - end - end - - local availableSquadrons, assessment, totalAvailable = findOptimalDefenders(threats, zone) - - -- Debug: Show squadron selection results - if GCI_Config.debugLevel >= 1 then - TADC_Log("info", "Squadron Selection for " .. zone .. ":") - for i, squadronData in pairs(availableSquadrons) do - local squadron = squadronData.data.squadron - local currentTime = timer.getTime() - local cooldownRemaining = math.max(0, squadron.launchCooldown - (currentTime - squadron.lastLaunch)) - TADC_Log("info", " " .. i .. ". " .. squadron.displayName .. " - Available: " .. squadron.availableAircraft .. ", Cooldown: " .. math.ceil(cooldownRemaining) .. "s") - end - end - - -- Assign each unassigned threat to the best available squadron (using smart-prioritized order) - local newAssignments = {} - - -- Process threats in smart priority order (highest priority first) - for _, assessment in ipairs(sortedThreats) do - local threat = assessment.threat - local threatId = threat.id .. "_" .. threat.firstDetected - - -- Skip if threat is already assigned to an active squadron - local skipThreat = false - if TADC.threatAssignments[threatId] then - local assignedSquadron = TADC.threatAssignments[threatId] - local squadron = TADC.squadrons[assignedSquadron] - if squadron and squadron.readinessLevel == "BUSY" then - if GCI_Config.debugLevel >= 2 then - TADC_Log("info", "Threat " .. threat.id .. " already assigned to " .. assignedSquadron) - end - skipThreat = true -- Skip this threat, it's being handled - else - -- Squadron is no longer busy, clear the assignment - TADC.threatAssignments[threatId] = nil - if TADC.squadronMissions[assignedSquadron] then - TADC.squadronMissions[assignedSquadron] = nil - end - end - end - - -- Only process threat if not skipped - if not skipThreat then - -- Enhanced squadron selection with smart threat matching - local bestSquadron = nil - local bestScore = -1 - - for _, squadronData in pairs(availableSquadrons) do - local squadron = squadronData.data.squadron - local templateName = squadronData.templateName - - -- Check if squadron is available (not already assigned to another threat) - if not TADC.squadronMissions[templateName] and squadron.availableAircraft > 0 then - - -- Find the original squadron config - local originalConfig = nil - for _, config in pairs(squadronConfigs) do - if config.templateName == templateName then - originalConfig = config - break - end - end - - if originalConfig then - -- Calculate enhanced squadron suitability score - local score = 0 - local distanceToThreat = squadronData.data.averageDistance or math.huge - - -- 1. DISTANCE FACTOR (40% of score) - Closer squadrons respond faster - local distanceScore = 40 * math.exp(-distanceToThreat / 75000) -- 75km ideal range - score = score + distanceScore - - -- 2. THREAT TYPE SPECIALIZATION (30% of score) - local typeBonus = 0 - if threat.classification == "HELICOPTER" and originalConfig.type == "HELICOPTER" then - typeBonus = 30 -- Perfect match for helo vs helo - elseif threat.classification == "BOMBER" and originalConfig.type == "FIGHTER" then - typeBonus = 25 -- Fighters excellent vs bombers - elseif threat.classification == "ATTACK" and originalConfig.type == "FIGHTER" then - typeBonus = 20 -- Fighters good vs attack aircraft - elseif threat.classification == "FIGHTER" and originalConfig.type == "FIGHTER" then - typeBonus = 15 -- Fighter vs fighter - elseif originalConfig.type == "FIGHTER" then - typeBonus = 10 -- Fighters can handle most threats - end - score = score + typeBonus - - -- 3. SQUADRON READINESS (20% of score) - local readinessBonus = squadron.availableAircraft * 3 -- More aircraft = better - if squadron.alertLevel == "GREEN" then - readinessBonus = readinessBonus + 10 - elseif squadron.alertLevel == "YELLOW" then - readinessBonus = readinessBonus + 5 - end - score = score + readinessBonus - - -- 4. RESPONSE URGENCY MATCHING (10% of score) - local urgencyBonus = 0 - if assessment.recommendedResponse == "EMERGENCY" then - urgencyBonus = 10 -- All squadrons get urgency bonus - score = score * 1.2 -- Emergency multiplier - elseif assessment.recommendedResponse == "URGENT" then - urgencyBonus = 7 - score = score * 1.1 -- Urgent multiplier - elseif assessment.recommendedResponse == "HIGH" then - urgencyBonus = 4 - end - score = score + urgencyBonus - - -- 5. PENALTY FACTORS - local penalties = 0 - local currentTime = timer.getTime() - local timeSinceLaunch = currentTime - squadron.lastLaunch - if timeSinceLaunch < squadron.launchCooldown * 1.5 then - penalties = penalties + 5 - end - if squadron.fatigue and squadron.fatigue > 50 then - penalties = penalties + (squadron.fatigue - 50) * 0.2 - end - score = score - penalties - - if GCI_Config.debugLevel >= 2 then - TADC_Log("info", string.format(" %s vs %s: Score=%.1f [Dist:%.1f, Type:%d, Ready:%.1f, Urg:%d, Pen:%.1f]", - squadron.displayName, threat.id, score, distanceScore, typeBonus, readinessBonus, urgencyBonus, penalties)) - end - - if score > bestScore then - bestScore = score - bestSquadron = { - templateName = templateName, - squadron = squadron, - distance = distanceToThreat, - threat = threat, - threatId = threatId, - config = originalConfig, - matchScore = score, - assessment = assessment -- Include threat assessment - } - end - end - end - end - - if bestSquadron then - table.insert(newAssignments, bestSquadron) - -- Mark squadron as assigned to prevent double-booking - TADC.squadronMissions[bestSquadron.templateName] = bestSquadron.threatId - TADC.threatAssignments[bestSquadron.threatId] = bestSquadron.templateName - - local timeStr = bestSquadron.assessment and bestSquadron.assessment.timeToTarget and - string.format(" TTT:%.1fm", bestSquadron.assessment.timeToTarget/60) or "" - local responseStr = bestSquadron.assessment and bestSquadron.assessment.recommendedResponse or "STANDARD" - TADC_Log("info", string.format("✅ ASSIGNMENT CREATED: %s → %s (%s x%d) Score:%.1f Priority:%d%s %s", - bestSquadron.squadron.displayName, threat.id, threat.classification, - threat.size or 1, bestSquadron.matchScore or 0, - bestSquadron.assessment and math.floor(bestSquadron.assessment.priority) or threat.priority, - timeStr, responseStr)) - else - TADC_Log("warning", "❌ NO SQUADRON AVAILABLE for threat " .. threat.id .. " - checking why...") - -- Debug why no squadron was available - local availableCount = 0 - for templateName, squadron in pairs(TADC.squadrons) do - if squadron.readinessLevel == "READY" and squadron.availableAircraft >= 1 then - availableCount = availableCount + 1 - TADC_Log("info", " Available: " .. squadron.displayName) - else - TADC_Log("info", " Unavailable: " .. squadron.displayName .. " (" .. squadron.readinessLevel .. ", " .. squadron.availableAircraft .. " aircraft)") - end - end - TADC_Log("info", "Total available squadrons: " .. availableCount) - end - end - end - - TADC_Log("info", "📋 ASSIGNMENT SUMMARY: Created " .. #newAssignments .. " assignments for " .. #zoneThreats .. " threats in " .. zone) - return newAssignments -end - --- ================================================================================================ --- CAP LAUNCH FUNCTION (Must be defined before executeInterceptMission) --- ================================================================================================ - -local function launchCAP(config, aircraftCount, reason) - aircraftCount = aircraftCount or 1 - - TADC_Log("info", "=== LAUNCHING CAP ===") - TADC_Log("info", "Squadron: " .. config.displayName) - TADC_Log("info", "Airbase: " .. config.airbaseName) - TADC_Log("info", "Aircraft: " .. aircraftCount) - TADC_Log("info", "Reason: " .. reason) - - local success, errorMsg = pcall(function() - -- Find the airbase object - local airbaseObj = AIRBASE:FindByName(config.airbaseName) - if not airbaseObj then - TADC_Log("info", "✗ Could not find airbase: " .. config.airbaseName) - return - end - - TADC_Log("info", "✓ Airbase object found, attempting spawn...") - TADC_Log("info", "Template: " .. config.templateName) - TADC_Log("info", "Aircraft count: " .. config.aircraft) - TADC_Log("info", "Skill: " .. tostring(config.skill)) - - -- Check if template exists - local templateGroup = GROUP:FindByName(config.templateName) - if not templateGroup then - TADC_Log("info", "✗ CRITICAL: Template group not found: " .. config.templateName) - TADC_Log("info", "SOLUTION: In Mission Editor, ensure group '" .. config.templateName .. "' exists and is set to 'Late Activation'") - return - end - - -- Check template group properties - local coalition = templateGroup:GetCoalition() - local coalitionName = coalition == 1 and "Red" or (coalition == 2 and "Blue" or "Neutral") - TADC_Log("info", "✓ Template group found - Coalition: " .. coalitionName) - - if coalition ~= 1 then - TADC_Log("info", "✗ CRITICAL: Template group is not Red coalition (coalition=" .. coalition .. ")") - TADC_Log("info", "SOLUTION: In Mission Editor, set group '" .. config.templateName .. "' to Red coalition") - return - end - - -- Template groups should NOT be alive if Late Activation is working correctly - local isAlive = templateGroup:IsAlive() - TADC_Log("info", "Template Status - Alive: " .. tostring(isAlive) .. " (should be false for Late Activation)") - - if isAlive then - TADC_Log("info", "⚠ Warning: Template group is alive - Late Activation may not be set correctly") - TADC_Log("info", "This means the group has already spawned in the mission") - else - TADC_Log("info", "✓ Template group correctly set to Late Activation") - end - - -- Create SPAWN object with proper initialization - TADC_Log("info", "Creating SPAWN object...") - local spawner = SPAWN:New(config.templateName) - - - -- Try different spawn methods that actually work - local spawnedGroup = nil - - -- Enhanced spawn system with better error handling and retry logic - local airbaseCoord = airbaseObj:GetCoordinate() - local spawnAttempts = 0 - local maxAttempts = 3 - - local function attemptSpawn() - spawnAttempts = spawnAttempts + 1 - - -- Method 1: Air spawn with validation - if spawnAttempts == 1 then - local spawnCoord = airbaseCoord:Translate( - math.random(GCI_Config.spawnDistanceMin, GCI_Config.spawnDistanceMax), - math.random(0, 360) - ):SetAltitude(config.altitude * 0.3048) -- Convert feet to meters - - TADC_Log("info", "Attempt " .. spawnAttempts .. ": Air spawn at " .. config.altitude .. "ft near " .. config.airbaseName) - return spawner:SpawnFromCoordinate(spawnCoord, nil, SPAWN.Takeoff.Air) - - -- Method 2: Hot start from airbase - elseif spawnAttempts == 2 then - TADC_Log("info", "Attempt " .. spawnAttempts .. ": Hot start from airbase") - return spawner:SpawnAtAirbase(airbaseObj, SPAWN.Takeoff.Hot) - - -- Method 3: Cold start from airbase - elseif spawnAttempts == 3 then - TADC_Log("info", "Attempt " .. spawnAttempts .. ": Cold start from airbase") - return spawner:SpawnAtAirbase(airbaseObj, SPAWN.Takeoff.Cold) - end - - return nil - end - - -- Retry spawn with delays - while spawnAttempts < maxAttempts and not spawnedGroup do - spawnedGroup = attemptSpawn() - if not spawnedGroup and spawnAttempts < maxAttempts then - TADC_Log("info", "Spawn attempt " .. spawnAttempts .. " failed, retrying in 2 seconds...") - -- Note: In actual implementation, you'd want to use SCHEDULER for the delay - end - end - - if spawnedGroup then - TADC_Log("info", "✓ Aircraft spawned successfully: " .. config.displayName) - -- Note: Skip immediate altitude check as coordinates may not be ready yet - -- Altitude will be set properly in the scheduled CAP setup task - - -- Wait a moment then set up proper CAP mission - SCHEDULER:New(nil, function() - if spawnedGroup and spawnedGroup:IsAlive() then - TADC_Log("info", "Setting up CAP mission for " .. config.displayName) - - -- Set proper altitude and speed (with enhanced safety checks) - local currentCoord = nil - local coordSuccess = pcall(function() - currentCoord = spawnedGroup:GetCoordinate() - end) - - if coordSuccess and currentCoord then - local properAltCoord = currentCoord:SetAltitude(config.altitude) - spawnedGroup:RouteAirTo(properAltCoord, config.speed, "BARO") - TADC_Log("info", "✓ Set altitude to " .. config.altitude .. "ft") - else - TADC_Log("info", "⚠ Coordinate not ready yet, CAP task will handle altitude") - end - - -- Set up AGGRESSIVE AI options with error handling - local success, errorMsg = pcall(function() - -- ENGAGEMENT RULES - AGGRESSIVE - spawnedGroup:OptionROEOpenFire() -- Engage enemies immediately - spawnedGroup:OptionROTVertical() -- No altitude restrictions (for all aircraft) - - -- DETECTION AND TARGETING - AGGRESSIVE - spawnedGroup:OptionECM_Never() -- Never use ECM to stay hidden - -- spawnedGroup:OptionRadarUsing(AI.Option.Ground.val.RADAR_USING.FOR_SEARCH_IF_REQUIRED) -- Skip this - not for air units - - -- RTB CONDITIONS - AGGRESSIVE (stay longer) - spawnedGroup:OptionRTBBingoFuel() -- RTB when low fuel - spawnedGroup:OptionRTBAmmo(0.05) -- RTB when 5% ammo left (was 10%) - - -- COMBAT BEHAVIOR - AGGRESSIVE - spawnedGroup:OptionAAAttackRange(AI.Option.Air.val.AA_ATTACK_RANGE.MAX_RANGE) -- Use maximum weapon range - spawnedGroup:OptionMissileAttack(AI.Option.Air.val.MISSILE_ATTACK.MAX_RANGE) -- Fire missiles at max range - - -- FORMATION AND MANEUVERING - AGGRESSIVE - if config.type == "HELICOPTER" then - spawnedGroup:OptionFormation(AI.Option.Air.val.FORMATION.LINE_ABREAST) -- Spread out for helicopters - else - spawnedGroup:OptionFormation(AI.Option.Air.val.FORMATION.FINGER_FOUR) -- Combat formation for fighters - end - - TADC_Log("info", "✓ Aggressive AI options set for " .. config.displayName) - end) - - if not success then - TADC_Log("info", "⚠ Warning: Could not set all AI options: " .. tostring(errorMsg)) - end - - -- Create randomized patrol system to prevent clustering - local function setupRandomPatrol() - if spawnedGroup and spawnedGroup:IsAlive() and config.patrolZone then - -- Get a random point within the patrol zone - local patrolZoneCoord = config.patrolZone:GetCoordinate() - if patrolZoneCoord then - -- Generate random patrol point within zone boundaries - local maxRadius = math.min(GCI_Config.capOrbitRadius, 25000) -- Max 25km from zone center - local randomRadius = math.random(GCI_Config.minPatrolSeparation, maxRadius) - local randomBearing = math.random(0, 360) - - local patrolPoint = patrolZoneCoord:Translate(randomRadius, randomBearing) - patrolPoint = patrolPoint:SetAltitude(config.altitude * 0.3048) -- Convert to meters - - TADC_Log("info", "Setting new patrol area for " .. config.displayName .. " at " .. randomRadius .. "m/" .. randomBearing .. "°") - - -- Clear old tasks and set up AGGRESSIVE HUNTER-KILLER tasks - spawnedGroup:ClearTasks() - - -- PRIMARY TASK: AGGRESSIVE AREA SWEEP (Priority 1 - Most Important) - local sweepTask = { - id = 'EngageTargetsInZone', - params = { - targetTypes = {'Air'}, - priority = 1, -- HIGHEST PRIORITY - zone = { - point = {x = patrolPoint.x, y = patrolPoint.z}, - radius = GCI_Config.capEngagementRange, -- Large search area - }, - noTargetTypes = {}, -- Engage ALL air targets - value = 'All', -- Engage all found targets - } - } - spawnedGroup:PushTask(sweepTask, 1) - - -- SECONDARY TASK: COMBAT AIR PATROL with AGGRESSIVE SEARCH (Priority 2) - local aggressiveCAP = { - id = 'ComboTask', - params = { - tasks = { - -- Search Pattern - { - id = 'EngageTargets', - params = { - targetTypes = {'Air'}, - priority = 1, - maxDistEnabled = true, - maxDist = GCI_Config.capEngagementRange * 1.2, -- 20% larger search range - direction = 0, -- Search all directions - attackQtyLimit = 0, -- No limit on attacks - directionEnabled = false, -- Search all directions - altitudeEnabled = false, -- Search all altitudes - } - }, - -- Fallback Patrol (only if no targets) - { - id = 'Orbit', - params = { - pattern = config.type == "HELICOPTER" and 'Race-Track' or 'Circle', - point = {x = patrolPoint.x, y = patrolPoint.z}, - radius = config.type == "HELICOPTER" and 5000 or GCI_Config.patrolAreaRadius, - altitude = config.altitude * 0.3048, - speed = config.speed * 0.514444 * 1.1, -- 10% faster for aggressive patrol - } - } - } - } - } - spawnedGroup:PushTask(aggressiveCAP, 2) - - TADC_Log("info", "✓ " .. config.displayName .. " assigned to patrol area " .. randomRadius .. "m from zone center") - end - end - end - - -- Set up initial patrol area - local capSuccess, capError = pcall(function() - setupRandomPatrol() - - -- Schedule patrol area changes every AI_PATROL_TIME seconds - if TADC.activeCAPs[config.templateName] then - TADC.activeCAPs[config.templateName].patrolScheduler = SCHEDULER:New(nil, setupRandomPatrol, {}, GCI_Config.AI_PATROL_TIME, GCI_Config.AI_PATROL_TIME) - end - end) - - if not capSuccess then - TADC_Log("info", "⚠ Warning: Could not set CAP tasks: " .. tostring(capError)) - -- Fallback: just set basic engage task - spawnedGroup:OptionROEOpenFire() - end - - TADC_Log("info", "✓ CAP mission established at " .. config.altitude .. "ft altitude") - - end - end, {}, 5) -- 5 second delay to let aircraft stabilize - - -- Mark as active - TADC.activeCAPs[config.templateName] = { - group = spawnedGroup, - launchTime = timer.getTime(), - config = config - } - - -- Set up extended patrol timer (much longer than before) - local patrolDuration = math.max(config.patrolTime * 60, GCI_Config.minPatrolDuration) -- Minimum from config - SCHEDULER:New(nil, function() - if TADC.activeCAPs[config.templateName] then - TADC_Log("info", config.displayName .. " completing patrol mission - RTB") - local group = TADC.activeCAPs[config.templateName].group - if group and group:IsAlive() then - -- Clear current tasks - group:ClearTasks() - - -- Send back to base - local airbaseObj = AIRBASE:FindByName(config.airbaseName) - if airbaseObj then - group:RouteRTB(airbaseObj) - TADC_Log("info", "✓ " .. config.displayName .. " returning to " .. config.airbaseName) - end - - -- Clean up after RTB delay - SCHEDULER:New(nil, function() - if TADC.activeCAPs[config.templateName] then - local capData = TADC.activeCAPs[config.templateName] - local rtbGroup = capData.group - - -- Stop patrol scheduler - if capData.patrolScheduler then - capData.patrolScheduler:Stop() - end - - if rtbGroup and rtbGroup:IsAlive() then - rtbGroup:Destroy() - TADC_Log("info", "✓ " .. config.displayName .. " landed and available for next sortie") - end - TADC.activeCAPs[config.templateName] = nil - end - end, {}, GCI_Config.rtbDuration) -- RTB time from config - else - -- Stop patrol scheduler if CAP is being cleaned up early - if TADC.activeCAPs[config.templateName] and TADC.activeCAPs[config.templateName].patrolScheduler then - TADC.activeCAPs[config.templateName].patrolScheduler:Stop() - end - TADC.activeCAPs[config.templateName] = nil - end - end - end, {}, patrolDuration) - - else - TADC_Log("info", "✗ Failed to spawn " .. config.displayName) - end - end) - - if not success then - TADC_Log("info", "✗ Error launching CAP: " .. tostring(errorMsg)) - return false - else - TADC_Log("info", "✓ CAP launch completed successfully") - return true - end -end - --- ================================================================================================ --- AGGRESSIVE INTERCEPT FUNCTION - Direct threat vectoring --- ================================================================================================ - -launchInterceptMission = function(config, threat, reason) - TADC_Log("info", "=== LAUNCHING INTERCEPT MISSION ===") - TADC_Log("info", "Squadron: " .. config.displayName) - TADC_Log("info", "Target: " .. (threat and threat.id or "Unknown")) - TADC_Log("info", "Target Type: " .. (threat and threat.classification or "Unknown")) - TADC_Log("info", "Reason: " .. reason) - - local success, errorMsg = pcall(function() - -- Find the airbase object - local airbaseObj = AIRBASE:FindByName(config.airbaseName) - if not airbaseObj then - TADC_Log("error", "✗ Could not find airbase: " .. config.airbaseName) - error("Airbase not found: " .. config.airbaseName) - end - - -- Check if template exists - local templateGroup = GROUP:FindByName(config.templateName) - if not templateGroup then - TADC_Log("error", "✗ CRITICAL: Template group not found: " .. config.templateName) - error("Template group not found: " .. config.templateName) - end - - -- Create SPAWN object - local spawner = SPAWN:New(config.templateName) - - -- Spawn aircraft in air at proper altitude - local airbaseCoord = airbaseObj:GetCoordinate() - local spawnCoord = airbaseCoord:Translate(math.random(GCI_Config.spawnDistanceMin, GCI_Config.spawnDistanceMax), math.random(0, 360)) - spawnCoord = spawnCoord:SetAltitude(config.altitude) - - TADC_Log("info", "Spawning interceptor at " .. config.altitude .. "ft near " .. config.airbaseName) - local spawnedGroup = spawner:SpawnFromCoordinate(spawnCoord, nil, SPAWN.Takeoff.Air) - - if not spawnedGroup then - -- Fallback spawn methods - spawnedGroup = spawner:SpawnAtAirbase(airbaseObj, SPAWN.Takeoff.Hot) - if not spawnedGroup then - spawnedGroup = spawner:SpawnFromCoordinate(airbaseCoord) - end - end - - if spawnedGroup then - TADC_Log("info", "✓ Interceptor spawned successfully: " .. config.displayName) - - -- Wait a moment then set up AGGRESSIVE INTERCEPT mission - SCHEDULER:New(nil, function() - -- Enhanced safety checks - if not (spawnedGroup and spawnedGroup:IsAlive()) then - TADC_Log("warning", "⚠ Spawned group " .. config.displayName .. " is not alive, aborting intercept setup") - return - end - - if not (threat and threat.group and threat.group:IsAlive()) then - TADC_Log("warning", "⚠ Threat is no longer valid, aborting intercept setup for " .. config.displayName) - return - end - - -- Test if we can get coordinates before proceeding - local testCoord = nil - local coordSuccess = pcall(function() - testCoord = spawnedGroup:GetCoordinate() - end) - - if not coordSuccess or not testCoord then - TADC_Log("warning", "⚠ Cannot get coordinates for " .. config.displayName .. ", delaying intercept setup") - -- Try again in 3 more seconds - SCHEDULER:New(nil, function() - if spawnedGroup and spawnedGroup:IsAlive() then - TADC_Log("info", "Retrying intercept setup for " .. config.displayName) - -- TODO: Repeat the setup logic here if needed - end - end, {}, 3) - return - end - - TADC_Log("info", "Setting up AGGRESSIVE INTERCEPT mission for " .. config.displayName .. " vs " .. threat.id) - - -- Set MAXIMUM AGGRESSION AI options - local success, errorMsg = pcall(function() - spawnedGroup:OptionROEOpenFire() - spawnedGroup:OptionROTVertical() - spawnedGroup:OptionECM_Never() - spawnedGroup:OptionAAAttackRange(AI.Option.Air.val.AA_ATTACK_RANGE.MAX_RANGE) - spawnedGroup:OptionMissileAttack(AI.Option.Air.val.MISSILE_ATTACK.MAX_RANGE) - spawnedGroup:OptionFormation(AI.Option.Air.val.FORMATION.FINGER_FOUR) - spawnedGroup:OptionRTBBingoFuel() - spawnedGroup:OptionRTBAmmo(0.03) -- Stay until almost no ammo (3%) - - TADC_Log("info", "✓ Maximum aggression AI options set") - end) - - -- DIRECT THREAT VECTORING - This is the key difference! - local threatCoord = nil - if threat.coordinate then - threatCoord = threat.coordinate - elseif threat.group and threat.group:IsAlive() then - threatCoord = threat.group:GetCoordinate() - end - - if threatCoord then - TADC_Log("info", "VECTORING " .. config.displayName .. " directly to threat at " .. threatCoord:ToStringLLDMS()) - - -- Clear any existing tasks - spawnedGroup:ClearTasks() - - -- TASK 1: Direct intercept to threat location (HIGHEST PRIORITY) - local interceptCoord = threatCoord:SetAltitude(config.altitude * 0.3048) - - -- Additional safety check before routing - local interceptorCoord = spawnedGroup:GetCoordinate() - if interceptorCoord and interceptCoord then - spawnedGroup:RouteAirTo(interceptCoord, config.speed * 1.2, "BARO") -- 20% faster to intercept - else - TADC_Log("warning", "Cannot route " .. config.displayName .. " - interceptor coordinate invalid") - end - - -- TASK 2: Attack the specific threat group (Priority 1) - local attackTask = { - id = 'AttackGroup', - params = { - groupId = threat.group:GetID(), - weaponType = 'Auto', -- Use best available weapon - attackQtyLimit = 0, -- No attack limit - priority = 1 - } - } - spawnedGroup:PushTask(attackTask, 1) - - -- TASK 3: Engage all targets in area around threat (Priority 2) - local engageTask = { - id = 'EngageTargetsInZone', - params = { - targetTypes = {'Air'}, - priority = 2, - zone = { - point = {x = threatCoord.x, y = threatCoord.z}, - radius = 20000, -- 20km around threat - }, - noTargetTypes = {}, - value = 'All', - } - } - spawnedGroup:PushTask(engageTask, 2) - - -- TASK 4: Continuous threat hunting if initial target is destroyed (Priority 3) - local huntTask = { - id = 'EngageTargets', - params = { - targetTypes = {'Air'}, - priority = 3, - maxDistEnabled = true, - maxDist = GCI_Config.capEngagementRange, - attackQtyLimit = 0, - } - } - spawnedGroup:PushTask(huntTask, 3) - - TADC_Log("info", "✓ " .. config.displayName .. " vectored to intercept " .. threat.id .. " with aggressive hunter-killer tasks") - - -- Set up threat tracking updates every 30 seconds - local trackingScheduler = SCHEDULER:New(nil, function() - if spawnedGroup and spawnedGroup:IsAlive() and threat and threat.group and threat.group:IsAlive() then - local currentThreatCoord = threat.group:GetCoordinate() - if currentThreatCoord then - -- Update intercept vector to current threat position - local newInterceptCoord = currentThreatCoord:SetAltitude(config.altitude * 0.3048) - - -- Enhanced safety check before routing - local interceptorCoord = nil - local coordSuccess = pcall(function() - interceptorCoord = spawnedGroup:GetCoordinate() - end) - - if coordSuccess and interceptorCoord and newInterceptCoord then - spawnedGroup:RouteAirTo(newInterceptCoord, config.speed * 1.1, "BARO") - - if GCI_Config.debugLevel >= 2 then - TADC_Log("info", "Updated vector: " .. config.displayName .. " → " .. threat.id) - end - else - TADC_Log("warning", "Cannot route " .. config.displayName .. " - invalid coordinates") - end - else - TADC_Log("warning", "Cannot get threat coordinate for " .. threat.id) - end - else - -- Stop tracking if threat or interceptor is dead - if GCI_Config.debugLevel >= 1 then - TADC_Log("info", "Stopping threat tracking for " .. config.displayName) - end - return false -- Stop scheduler - end - end, {}, 30, 30) -- Update every 30 seconds - - else - TADC_Log("info", "⚠ Could not get threat coordinate for vectoring") - -- Fallback to aggressive patrol - local patrolZoneCoord = config.patrolZone:GetCoordinate() - if patrolZoneCoord then - local patrolCoord = patrolZoneCoord:SetAltitude(config.altitude * 0.3048) - -- Enhanced safety check before routing - local interceptorCoord = nil - local coordSuccess = pcall(function() - interceptorCoord = spawnedGroup:GetCoordinate() - end) - - if coordSuccess and interceptorCoord then - spawnedGroup:RouteAirTo(patrolCoord, config.speed, "BARO") - TADC_Log("info", "✓ Fallback patrol routing successful for " .. config.displayName) - else - TADC_Log("warning", "Cannot route " .. config.displayName .. " to patrol - GetCoordinate failed") - end - else - TADC_Log("warning", "Cannot get patrol zone coordinate for " .. config.displayName) - end - end - end, {}, 5) -- Increased to 5 second delay to allow full group initialization - - -- Mark as active - TADC.activeCAPs[config.templateName] = { - group = spawnedGroup, - launchTime = timer.getTime(), - config = config, - isIntercept = true, -- Mark as intercept mission - targetThreat = threat - } - - -- Set up mission duration - local patrolDuration = math.max(config.patrolTime * 60, GCI_Config.minPatrolDuration) - SCHEDULER:New(nil, function() - if TADC.activeCAPs[config.templateName] then - TADC_Log("info", config.displayName .. " completing intercept mission - RTB") - local group = TADC.activeCAPs[config.templateName].group - if group and group:IsAlive() then - group:ClearTasks() - local airbaseObj = AIRBASE:FindByName(config.airbaseName) - if airbaseObj then - group:RouteRTB(airbaseObj) - end - - SCHEDULER:New(nil, function() - if TADC.activeCAPs[config.templateName] then - local rtbGroup = TADC.activeCAPs[config.templateName].group - if rtbGroup and rtbGroup:IsAlive() then - rtbGroup:Destroy() - end - TADC.activeCAPs[config.templateName] = nil - end - end, {}, GCI_Config.rtbDuration) - else - TADC.activeCAPs[config.templateName] = nil - end - end - end, {}, patrolDuration) - else - TADC_Log("info", "✗ Failed to spawn interceptor: " .. config.displayName) - end - end) - - if not success then - TADC_Log("info", "✗ Error launching intercept: " .. tostring(errorMsg)) - return false - else - TADC_Log("info", "✓ Intercept mission launched successfully") - return true - end -end - --- ================================================================================================ --- MISSION EXECUTION FUNCTION (Now can call launchCAP) --- ================================================================================================ - -local function executeThreatsAssignments(assignments) - if not assignments or #assignments == 0 then - return false - end - - local currentTime = timer.getTime() - - TADC_Log("info", "=== EXECUTING THREAT ASSIGNMENTS ===") - TADC_Log("info", "Processing " .. #assignments .. " threat assignments") - - local launchedFlights = {} - - for _, assignment in pairs(assignments) do - local squadron = assignment.squadron - local templateName = assignment.templateName - - -- Check squadron availability and cooldown - if squadron.readinessLevel == "READY" and - squadron.availableAircraft >= 1 and - (currentTime - squadron.lastLaunch) >= squadron.launchCooldown then - - local reason = "Intercept: " .. (assignment.threat and assignment.threat.id or "Unknown") .. " (" .. (assignment.threat and assignment.threat.classification or "Unknown") .. ")" - - -- Use AGGRESSIVE INTERCEPT MISSION instead of generic CAP - local success = false - if assignment.threat then - success = launchInterceptMission(assignment.config, assignment.threat, reason) - else - success = launchCAP(assignment.config, 1, reason) -- Fallback to CAP if no specific threat - end - - if success then - -- Update squadron status - squadron is now BUSY with this threat - squadron.availableAircraft = squadron.availableAircraft - 1 - squadron.airborneAircraft = squadron.airborneAircraft + 1 - squadron.lastLaunch = currentTime - squadron.sorties = squadron.sorties + 1 - -- TESTING: Don't mark squadron as BUSY - allow multiple launches - -- squadron.readinessLevel = "BUSY" -- Squadron is now handling this threat - - TADC.stats.interceptsLaunched = TADC.stats.interceptsLaunched + 1 - - -- Store mission details - TADC.missions[assignment.threatId] = { - threatId = assignment.threatId, - squadron = templateName, - threat = assignment.threat, - startTime = currentTime, - status = "ACTIVE" - } - - table.insert(launchedFlights, { - squadron = templateName, - threat = assignment.threat and assignment.threat.id or "Unknown", - launchTime = currentTime - }) - - TADC_Log("info", "✓ Launched: " .. squadron.displayName .. " → " .. (assignment.threat and assignment.threat.id or "Unknown")) - else - TADC_Log("info", "✗ Launch failed: " .. squadron.displayName) - end - else - local reason = "Unknown" - if squadron.readinessLevel ~= "READY" then - reason = "Not ready (" .. squadron.readinessLevel .. ")" - elseif squadron.availableAircraft < 1 then - reason = "Insufficient aircraft (" .. squadron.availableAircraft .. " available)" - elseif (currentTime - squadron.lastLaunch) < squadron.launchCooldown then - reason = "On cooldown (" .. math.ceil(squadron.launchCooldown - (currentTime - squadron.lastLaunch)) .. "s remaining)" - end - TADC_Log("info", "✗ Cannot launch " .. squadron.displayName .. ": " .. reason) - end - end - - -- Store mission for tracking - -- (Removed assignment to TADC.missions[missionId] due to undefined variables) - - return #launchedFlights > 0 -end - --- ================================================================================================ --- PERSISTENT CAP HELPER FUNCTIONS --- ================================================================================================ - -local function getPersistentCAPCount() - local count = 0 - for _, capData in pairs(TADC.persistentCAPs) do - if capData.group and capData.group:IsAlive() then - count = count + 1 - else - -- Clean up dead persistent CAPs - TADC.persistentCAPs[capData.templateName] = nil - end - end - return count -end - -local function launchPersistentCAP(templateName, reason) - -- Find the original squadron config - local config = nil - for _, squadronConfig in pairs(squadronConfigs) do - if squadronConfig.templateName == templateName then - config = squadronConfig - break - end - end - - if not config or not TADC.squadrons[templateName] then - return false - end - - local squadron = TADC.squadrons[templateName] - local currentTime = timer.getTime() - - -- Check if squadron is available (not on cooldown, has aircraft) - if squadron.readinessLevel ~= "READY" or - squadron.availableAircraft < 1 or - (currentTime - squadron.lastLaunch) < squadron.launchCooldown then - return false - end - - -- Launch the CAP - local success = launchCAP(config, 1, reason) - if success then - -- Track as persistent CAP (separate from regular intercept CAPs) - TADC.persistentCAPs[templateName] = { - templateName = templateName, - group = TADC.activeCAPs[templateName] and TADC.activeCAPs[templateName].group, - launchTime = currentTime, - isPersistent = true - } - - if GCI_Config.debugLevel >= 1 then - TADC_Log("info", "✓ Persistent CAP launched: " .. squadron.displayName) - end - - return true - end - - return false -end - -local function maintainPersistentCAP() - if not GCI_Config.enablePersistentCAP then - return - end - - local currentTime = timer.getTime() - - -- Only check every persistentCAPInterval seconds - if (currentTime - TADC.lastPersistentCheck) < GCI_Config.persistentCAPInterval then - return - end - - TADC.lastPersistentCheck = currentTime - - -- Calculate total airborne aircraft to respect maxSimultaneousCAP limit - local totalAirborne = 0 - for _, squadron in pairs(TADC.squadrons) do - totalAirborne = totalAirborne + squadron.airborneAircraft - end - - local currentPersistentCount = getPersistentCAPCount() - local needed = GCI_Config.persistentCAPCount - currentPersistentCount - - -- Respect maxSimultaneousCAP limit with reserve for threat response - local maxPersistentAllowed = math.floor(GCI_Config.maxSimultaneousCAP * (1 - GCI_Config.persistentCAPReserve)) - local effectiveTarget = math.min(GCI_Config.persistentCAPCount, maxPersistentAllowed) - local availableSlots = maxPersistentAllowed - currentPersistentCount - - -- Recalculate needed based on effective limits - needed = math.min(needed, availableSlots) - needed = math.max(0, needed) - - if needed < (GCI_Config.persistentCAPCount - currentPersistentCount) and GCI_Config.debugLevel >= 1 then - TADC_Log("info", "⚠ Persistent CAP limited: Target=" .. GCI_Config.persistentCAPCount .. ", Effective=" .. effectiveTarget .. " (reserving " .. math.ceil(GCI_Config.maxSimultaneousCAP * GCI_Config.persistentCAPReserve) .. " slots for threats)") - end - - if needed > 0 then - if GCI_Config.debugLevel >= 1 then - TADC_Log("info", "=== PERSISTENT CAP MAINTENANCE ===") - TADC_Log("info", "Current: " .. currentPersistentCount .. ", Target: " .. GCI_Config.persistentCAPCount .. ", Need: " .. needed) - TADC_Log("info", "Total airborne: " .. totalAirborne .. "/" .. GCI_Config.maxSimultaneousCAP .. " (available slots: " .. availableSlots .. ")") - end - - -- Launch needed persistent CAPs from priority list - local launched = 0 - for _, templateName in pairs(GCI_Config.persistentCAPPriority) do - if launched >= needed then - break - end - - -- Skip if this squadron already has a persistent CAP - if not TADC.persistentCAPs[templateName] or - not TADC.persistentCAPs[templateName].group or - not TADC.persistentCAPs[templateName].group:IsAlive() then - - if launchPersistentCAP(templateName, "Persistent CAP maintenance") then - launched = launched + 1 - end - end - end - - if GCI_Config.debugLevel >= 1 then - TADC_Log("info", "✓ Persistent CAP maintenance complete: " .. launched .. " new patrols launched") - end - end -end - - - --- ================================================================================================ --- ENHANCED AI AWARENESS AND TARGET SHARING SYSTEM --- ================================================================================================ - -local function enhanceAIAwareness() - -- Update all active CAP flights with current threat information - for templateName, capData in pairs(TADC.activeCAPs) do - if capData.group and capData.group:IsAlive() then - local group = capData.group - local config = capData.config - - -- Find nearby threats to make AI more aware - local groupCoord = group:GetCoordinate() - if groupCoord then - local nearbyThreats = {} - - -- Collect threats within engagement range - for _, threat in pairs(TADC.threats) do - if threat.group and threat.group:IsAlive() then - local threatCoord = threat.coordinate or threat.group:GetCoordinate() - if threatCoord then - local distance = groupCoord:Get2DDistance(threatCoord) - local maxPursuitRange = GCI_Config.hyperAggressiveMode and GCI_Config.pursuitRange or (GCI_Config.capEngagementRange * 1.5) - if distance <= maxPursuitRange then - table.insert(nearbyThreats, { - threat = threat, - distance = distance, - coordinate = threatCoord - }) - end - end - end - end - - -- If threats are nearby, vector the aircraft towards the closest one - if #nearbyThreats > 0 then - -- Sort by distance (closest first) - table.sort(nearbyThreats, function(a, b) return a.distance < b.distance end) - - local closestThreat = nearbyThreats[1] - - -- Only update if this is a significant threat change - if not capData.lastTargetedThreat or - capData.lastTargetedThreat ~= closestThreat.threat.id or - (timer.getTime() - (capData.lastVectorUpdate or 0)) > GCI_Config.engagementUpdateInterval then - - if GCI_Config.debugLevel >= 2 then - TADC_Log("info", "Vectoring " .. config.displayName .. " to nearby threat: " .. closestThreat.threat.id .. " (" .. math.floor(closestThreat.distance/1000) .. "km)") - end - - -- Clear old tasks and add new aggressive intercept - group:ClearTasks() - - -- Route towards threat at higher speed - if closestThreat.coordinate then - local interceptCoord = closestThreat.coordinate:SetAltitude(config.altitude * 0.3048) - -- Safety check before routing - local groupCoord = group:GetCoordinate() - if groupCoord and interceptCoord then - group:RouteAirTo(interceptCoord, config.speed * 1.2, "BARO") - else - TADC_Log("warning", "Cannot route " .. config.displayName .. " - invalid coordinates for intercept") - end - else - TADC_Log("warning", "No coordinate available for threat " .. closestThreat.threat.id) - end - - -- Add aggressive attack task - local aggressiveAttack = { - id = 'AttackGroup', - params = { - groupId = closestThreat.threat.group:GetID(), - weaponType = 'Auto', - attackQtyLimit = 0, - priority = 1 - } - } - group:PushTask(aggressiveAttack, 1) - - -- Add area sweep task - local areaSweep = { - id = 'EngageTargetsInZone', - params = { - targetTypes = {'Air'}, - priority = 2, - zone = { - point = {x = closestThreat.coordinate.x, y = closestThreat.coordinate.z}, - radius = 15000, -- 15km area sweep - }, - noTargetTypes = {}, - value = 'All', - } - } - group:PushTask(areaSweep, 2) - - -- Update tracking info - capData.lastTargetedThreat = closestThreat.threat.id - capData.lastVectorUpdate = timer.getTime() - end - end - end - end - end -end - --- ================================================================================================ --- MAIN TADC CONTROL LOOP --- ================================================================================================ - --- SIMPLE GCI MAIN LOOP - Just detect threats and launch intercepts -local function simpleGCILoop() - local currentTime = timer.getTime() - - -- Count current airborne aircraft - local airborneCount = 0 - for _, squadron in pairs(TADC.squadrons) do - airborneCount = airborneCount + squadron.airborneAircraft - end - - -- Only proceed if we're under the airborne limit - if airborneCount >= GCI_Config.maxSimultaneousCAP then - TADC_Log("info", "Max aircraft limit reached (" .. airborneCount .. "/" .. GCI_Config.maxSimultaneousCAP .. ")") - return - end - - -- Detect threats using simple detection - local threats = simpleDetectThreats() - - -- For each threat, find closest airfield and launch intercept - for threatId, threat in pairs(threats) do - TADC_Log("info", "Processing threat: " .. threatId) - - -- Calculate how many defenders needed (1 to 1.5 ratio) - local defendersNeeded = math.ceil(threat.size * GCI_Config.threatRatio) - - -- Find closest available squadron - local bestSquadron = nil - local bestDistance = 999999 - - for templateName, squadron in pairs(TADC.squadrons) do - if squadron.readinessLevel == "READY" and squadron.availableAircraft >= defendersNeeded then - -- Check cooldown - local cooldown = currentTime - squadron.lastLaunch - if cooldown >= GCI_Config.squadronCooldown then - local squadronCoord = squadron.homebase and squadron.homebase:GetCoordinate() - if squadronCoord then - local distance = squadronCoord:Get2DDistance(threat.coordinate) - if distance < bestDistance then - bestDistance = distance - bestSquadron = squadron - bestSquadron.templateName = templateName - end - end - end - end - end - - -- Launch intercept if squadron found - if bestSquadron then - TADC_Log("info", "🚀 LAUNCHING: " .. bestSquadron.displayName .. " to intercept " .. threatId) - - -- Find the original squadron config for this squadron - local squadronConfig = nil - for _, config in pairs(squadronConfigs) do - if config.templateName == bestSquadron.templateName then - squadronConfig = config - break - end - end - - if squadronConfig then - launchInterceptMission(squadronConfig, threat, "GCI_INTERCEPT") - else - TADC_Log("error", "Could not find config for squadron: " .. bestSquadron.templateName) - end - else - TADC_Log("info", "⚠ No available squadrons for " .. threatId) - end - end - - -- Clean up completed missions and free squadrons - for missionId, mission in pairs(TADC.missions) do - local squadron = TADC.squadrons[mission.squadron] - if squadron then - -- Check if mission should be completed (threat destroyed, timed out, or squadron has no airborne aircraft) - local missionAge = currentTime - mission.startTime - local shouldComplete = false - - -- Mission timeout (30 minutes) - if missionAge > 1800 then - shouldComplete = true - end - - -- Squadron has returned (no airborne aircraft) - if squadron.airborneAircraft == 0 and missionAge > 300 then -- Give 5 min minimum mission time - shouldComplete = true - end - - -- Threat no longer exists - local threatExists = false - for _, threat in pairs(threats) do - if (threat.id .. "_" .. threat.firstDetected) == missionId then - threatExists = true - break - end - end - if not threatExists and missionAge > 60 then -- 1 minute grace period - shouldComplete = true - end - - if shouldComplete then - -- Free the squadron for new missions - squadron.readinessLevel = "READY" - TADC.squadronMissions[mission.squadron] = nil - TADC.threatAssignments[missionId] = nil - TADC.missions[missionId] = nil - - if GCI_Config.debugLevel >= 1 then - TADC_Log("info", "Mission completed: " .. mission.squadron .. " freed from " .. (mission.threat and mission.threat.id or "unknown threat")) - end - end - else - -- Squadron doesn't exist anymore, clean up - TADC.missions[missionId] = nil - TADC.threatAssignments[missionId] = nil - end - end - - -- Periodic status report - if GCI_Config.debugLevel >= 1 and (currentTime % GCI_Config.statusReportInterval) < 30 then -- Status reports from config - local totalAirborne = 0 - local totalAvailable = 0 - for _, squadron in pairs(TADC.squadrons) do - totalAirborne = totalAirborne + squadron.airborneAircraft - totalAvailable = totalAvailable + squadron.availableAircraft - end - - TADC_Log("info", "=== TADC STATUS REPORT ===") - TADC_Log("info", "Threats: " .. threatCount .. " active") - TADC_Log("info", "Aircraft: " .. totalAirborne .. " airborne, " .. totalAvailable .. " available") - TADC_Log("info", "Statistics: " .. TADC.stats.threatsDetected .. " threats detected, " .. TADC.stats.interceptsLaunched .. " intercepts launched") - - -- Persistent CAP Status - if GCI_Config.enablePersistentCAP then - local persistentCount = getPersistentCAPCount() - local maxPersistentAllowed = math.floor(GCI_Config.maxSimultaneousCAP * (1 - GCI_Config.persistentCAPReserve)) - local threatReserve = GCI_Config.maxSimultaneousCAP - maxPersistentAllowed - TADC_Log("info", "Persistent CAP: " .. persistentCount .. "/" .. GCI_Config.persistentCAPCount .. " target (" .. maxPersistentAllowed .. " max, " .. threatReserve .. " reserved for threats)") - end - end - - -- Enhanced AI Awareness and Target Sharing - enhanceAIAwareness() - - -- Persistent CAP Management - if GCI_Config.enablePersistentCAP then - maintainPersistentCAP() - end -end - --- ================================================================================================ --- MAIN TADC LOOP FUNCTION (COMPREHENSIVE VERSION) --- ================================================================================================ - -local function mainTADCLoop() - local currentTime = timer.getTime() - local startTime = currentTime - - -- Performance tracking - TADC.performance.loopCount = TADC.performance.loopCount + 1 - - -- Update system statistics - TADC.stats.systemLoadTime = currentTime - - -- 1. IMMEDIATE RESPONSE CHECK (like RU_INTERCEPT backup) - immediateInterceptCheck() - - -- 2. COMPREHENSIVE THREAT DETECTION - local threats = simpleDetectThreats() - local threatCount = 0 - for _ in pairs(threats) do threatCount = threatCount + 1 end - - if threatCount > 0 then - TADC_Log("info", "🎯 MAIN LOOP: Detected " .. threatCount .. " active threats") - TADC.stats.threatsDetected = TADC.stats.threatsDetected + threatCount - - -- Debug: Show which threats were detected - for threatId, threat in pairs(threats) do - TADC_Log("info", " Threat: " .. threatId .. " (" .. threat.classification .. ") in " .. threat.zone) - end - else - TADC_Log("info", "🎯 MAIN LOOP: No threats detected") - end - - -- 3. PROCESS THREATS BY ZONE - local zones = {"RED_BORDER", "HELO_BORDER"} - - for _, zone in pairs(zones) do - -- Get threats in this zone - local zoneThreats = {} - for threatId, threat in pairs(threats) do - if threat.zone == zone then - table.insert(zoneThreats, threat) - end - end - - if #zoneThreats > 0 then - if GCI_Config.debugLevel >= 1 then - TADC_Log("info", "Processing " .. #zoneThreats .. " threats in " .. zone) - end - - -- Assign threats to squadrons using smart algorithm - local assignments = assignThreatsToSquadrons(zoneThreats, zone) - - -- Execute the assignments - if #assignments > 0 then - TADC_Log("info", "📋 EXECUTING " .. #assignments .. " ASSIGNMENTS FOR " .. zone) - TADC.stats.interceptsLaunched = TADC.stats.interceptsLaunched + #assignments - executeThreatsAssignments(assignments) - else - TADC_Log("warning", "⚠ NO ASSIGNMENTS GENERATED for " .. #zoneThreats .. " threats in " .. zone) - -- Debug squadron availability - local readySquadrons = 0 - local totalSquadrons = 0 - for templateName, squadron in pairs(TADC.squadrons) do - totalSquadrons = totalSquadrons + 1 - if squadron.readinessLevel == "READY" and squadron.availableAircraft >= 1 then - readySquadrons = readySquadrons + 1 - TADC_Log("info", "✓ READY: " .. squadron.displayName .. " (" .. squadron.availableAircraft .. " aircraft)") - else - TADC_Log("info", "✗ NOT READY: " .. squadron.displayName .. " - " .. squadron.readinessLevel .. " (" .. squadron.availableAircraft .. " aircraft)") - end - end - TADC_Log("info", "Squadron Status: " .. readySquadrons .. "/" .. totalSquadrons .. " ready") - end - end - end - - -- 4. SQUADRON STATUS MANAGEMENT - -- Clean up completed missions and free squadrons - for missionId, mission in pairs(TADC.missions) do - local squadron = TADC.squadrons[mission.squadron] - if squadron then - local missionAge = currentTime - mission.startTime - local shouldComplete = false - - -- Mission completion conditions - if missionAge > 1800 then -- 30 minutes timeout - shouldComplete = true - elseif squadron.airborneAircraft == 0 and missionAge > 300 then -- 5 min minimum - shouldComplete = true - end - - -- Check if threat still exists - local threatExists = false - for _, threat in pairs(threats) do - if (threat.id .. "_" .. threat.firstDetected) == missionId then - threatExists = true - break - end - end - if not threatExists and missionAge > 60 then - shouldComplete = true - end - - if shouldComplete then - squadron.readinessLevel = "READY" - TADC.squadronMissions[mission.squadron] = nil - TADC.threatAssignments[missionId] = nil - TADC.missions[missionId] = nil - - if GCI_Config.debugLevel >= 1 then - TADC_Log("info", "Mission completed: " .. mission.squadron .. " freed") - end - end - else - TADC.missions[missionId] = nil - TADC.threatAssignments[missionId] = nil - end - end - - -- 5. ENHANCED AI AWARENESS AND TARGET SHARING - enhanceAIAwareness() - - -- 6. PERSISTENT CAP MANAGEMENT - if GCI_Config.enablePersistentCAP then - maintainPersistentCAP() - end - - -- 7. PERIODIC STATUS REPORTING - if GCI_Config.debugLevel >= 1 and (currentTime % GCI_Config.statusReportInterval) < GCI_Config.mainLoopInterval then - local totalAirborne = 0 - local totalAvailable = 0 - for _, squadron in pairs(TADC.squadrons) do - totalAirborne = totalAirborne + squadron.airborneAircraft - totalAvailable = totalAvailable + squadron.availableAircraft - end - - TADC_Log("info", "=== TADC STATUS REPORT ===") - TADC_Log("info", "Threats: " .. threatCount .. " active") - TADC_Log("info", "Aircraft: " .. totalAirborne .. " airborne, " .. totalAvailable .. " available") - TADC_Log("info", "Statistics: " .. TADC.stats.threatsDetected .. " threats detected, " .. TADC.stats.interceptsLaunched .. " intercepts launched") - - if GCI_Config.enablePersistentCAP then - local persistentCount = getPersistentCAPCount() - TADC_Log("info", "Persistent CAP: " .. persistentCount .. "/" .. GCI_Config.persistentCAPCount .. " target") - end - end - - -- 8. PERFORMANCE TRACKING - local loopTime = timer.getTime() - startTime - TADC.performance.lastLoopTime = loopTime - TADC.performance.avgLoopTime = (TADC.performance.avgLoopTime * (TADC.performance.loopCount - 1) + loopTime) / TADC.performance.loopCount - TADC.performance.maxLoopTime = math.max(TADC.performance.maxLoopTime, loopTime) - - if loopTime > 1.0 and GCI_Config.debugLevel >= 1 then - TADC_Log("warning", "TADC loop took " .. string.format("%.2f", loopTime) .. "s (performance warning)") - end -end - --- ================================================================================================ --- PERSISTENT CAP MANAGEMENT SYSTEM --- ================================================================================================ - -local function setupTADC() - TADC_Log("info", "=== INITIALIZING TACTICAL AIR DEFENSE CONTROLLER ===") - - -- Validate configuration before starting - if not validateConfiguration() then - TADC_Log("info", "✗ TADC configuration validation failed") - return - end - - TADC_Log("info", "✓ Configuration loaded and validated:") - TADC_Log("info", " - Threat Ratio: " .. GCI_Config.threatRatio .. ":1") - TADC_Log("info", " - Max Simultaneous CAP: " .. GCI_Config.maxSimultaneousCAP) - TADC_Log("info", " - Reserve Percentage: " .. (GCI_Config.reservePercent * 100) .. "%") - TADC_Log("info", " - Supply Mode: " .. GCI_Config.supplyMode) - TADC_Log("info", " - Response Delay: " .. GCI_Config.responseDelay .. " seconds") - - -- Persistent CAP Configuration - if GCI_Config.enablePersistentCAP then - local maxPersistentAllowed = math.floor(GCI_Config.maxSimultaneousCAP * (1 - GCI_Config.persistentCAPReserve)) - local threatReserve = math.ceil(GCI_Config.maxSimultaneousCAP * GCI_Config.persistentCAPReserve) - TADC_Log("info", " - Persistent CAP: ENABLED (" .. GCI_Config.persistentCAPCount .. " target, " .. maxPersistentAllowed .. " max allowed)") - TADC_Log("info", " - Threat Response Reserve: " .. threatReserve .. " aircraft slots") - TADC_Log("info", " - Persistent CAP Check Interval: " .. GCI_Config.persistentCAPInterval .. " seconds") - else - TADC_Log("info", " - Persistent CAP: DISABLED") - end - - -- CAP Behavior Configuration - TADC_Log("info", " - CAP Orbit Radius: " .. (GCI_Config.capOrbitRadius / 1000) .. "km") - TADC_Log("info", " - CAP Engagement Range: " .. (GCI_Config.capEngagementRange / 1000) .. "km") - TADC_Log("info", " - Zone Constraint: " .. (GCI_Config.capZoneConstraint and "ENABLED" or "DISABLED")) - - -- Start main control loop - SCHEDULER:New(nil, mainTADCLoop, {}, GCI_Config.mainLoopDelay, GCI_Config.mainLoopInterval) -- Main loop timing from config - - TADC_Log("info", "✓ TADC main control loop started") - TADC_Log("info", "✓ Tactical Air Defense Controller operational!") -end - --- Initialize the TADC system -SCHEDULER:New(nil, function() - setupTADC() - - -- Launch initial persistent CAP flights if enabled - if GCI_Config.enablePersistentCAP then - TADC_Log("info", "=== LAUNCHING INITIAL PERSISTENT CAP ===") - TADC.lastPersistentCheck = 0 -- Force immediate check - maintainPersistentCAP() - - -- Schedule another check in 30 seconds to ensure CAP gets airborne - SCHEDULER:New(nil, function() - TADC_Log("info", "=== PERSISTENT CAP FOLLOW-UP CHECK ===") - TADC.lastPersistentCheck = 0 -- Force another immediate check - maintainPersistentCAP() - end, {}, 30) - end - - -- Legacy: Optional initial standing patrols (if configured) - if GCI_Config.initialStandingPatrols then - local initialPatrols = { - "FIGHTER_SWEEP_RED_Severomorsk-1", -- Main base always has standing patrol - "HELO_SWEEP_RED_Afrikanda" -- Helo patrol coverage - } - - for _, templateName in pairs(initialPatrols) do - if TADC.squadrons[templateName] then - -- Find the original squadron config - local config = nil - for _, squadronConfig in pairs(squadronConfigs) do - if squadronConfig.templateName == templateName then - config = squadronConfig - break - end - end - - if config then - launchCAP(config, 1, "Initial standing patrol") - end - end - end - end - - -- Initialize strategic targets for smart prioritization - initializeStrategicTargets() - - TADC_Log("info", "=== TADC INITIALIZATION COMPLETE ===") - TADC_Log("info", "✓ Smart threat prioritization system with multi-factor analysis") - TADC_Log("info", "✓ Predictive threat assessment and response") - TADC_Log("info", "✓ Intelligent threat assessment and response") - TADC_Log("info", "✓ Multi-squadron coordinated intercepts") - TADC_Log("info", "✓ Dynamic force sizing based on threat strength") - TADC_Log("info", "✓ Resource management with reserve forces") - TADC_Log("info", "✓ EWR network integration with " .. (RedEWR:Count()) .. " detection groups") - TADC_Log("info", "✓ Strategic target protection with distance-based prioritization") - TADC_Log("info", "✓ Enhanced squadron-threat matching algorithm") - TADC_Log("info", "✓ Tactical Air Defense Controller operational!") - -end, {}, 5) - --- ================================================================================================ --- SYSTEM STARTUP AND INITIALIZATION --- ================================================================================================ - --- Initialize strategic target coordinates -initializeStrategicTargets() - --- Start the TADC system with proper delays -SCHEDULER:New(nil, function() - TADC_Log("info", "=== STARTING TADC SYSTEM ===") - setupTADC() - - -- Launch initial standing patrols if configured - if GCI_Config.initialStandingPatrols then - TADC_Log("info", "Launching initial standing patrols...") - - -- Wait a bit more then launch initial CAPs - SCHEDULER:New(nil, function() - local launched = 0 - local maxInitialCAP = math.min(2, GCI_Config.persistentCAPCount) -- Start with 2 max - - for _, templateName in pairs(GCI_Config.persistentCAPPriority) do - if launched >= maxInitialCAP then break end - - if launchPersistentCAP(templateName, "Initial standing patrol") then - launched = launched + 1 - -- Stagger launches by 30 seconds - if launched < maxInitialCAP then - SCHEDULER:New(nil, function() end, {}, 30) - end - end - end - - if launched > 0 then - TADC_Log("info", "✓ Initial standing patrols launched: " .. launched .. " flights") - else - TADC_Log("info", "⚠ Could not launch initial standing patrols - will retry during maintenance cycle") - end - end, {}, GCI_Config.capSetupDelay + 15) -- Extra delay for initial patrols - end - - -- Start the main TADC control loop - SCHEDULER:New(nil, function() - TADC_Log("info", "✓ TADC Main Control Loop starting...") - - -- Schedule the main loop to run every mainLoopInterval seconds - SCHEDULER:New(nil, mainTADCLoop, {}, 0, GCI_Config.mainLoopInterval) - - TADC_Log("info", "✓ TADC System fully operational!") - TADC_Log("info", "✓ Monitoring for threats every " .. GCI_Config.mainLoopInterval .. " seconds") - - end, {}, GCI_Config.mainLoopDelay + GCI_Config.capSetupDelay) - -end, {}, 3) -- Small initial delay to let MOOSE fully initialize diff --git a/DCS_Kola/Operation_Polar_Shield/Moose_TADC_CargoDispatcher.lua b/DCS_Kola/Operation_Polar_Shield/Moose_TADC_CargoDispatcher.lua index 4e8e1ee..aea608f 100644 --- a/DCS_Kola/Operation_Polar_Shield/Moose_TADC_CargoDispatcher.lua +++ b/DCS_Kola/Operation_Polar_Shield/Moose_TADC_CargoDispatcher.lua @@ -404,8 +404,8 @@ local function dispatchCargo(squadron, coalitionKey) local destCoalition = destAirbase:GetCoalition() if destCoalition ~= coalitionSide then - log("WARNING: Destination airbase '" .. destination .. "' is not controlled by " .. coalitionKey .. " coalition (currently coalition " .. tostring(destCoalition) .. "). This will cause RAT to fail with 'Airbase doesn't exist' error. Skipping dispatch.") - announceToCoalition(coalitionKey, "Resupply mission to " .. destination .. " aborted (airbase captured by enemy)!") + log("INFO: Destination airbase '" .. destination .. "' captured by enemy - cargo dispatch skipped (normal mission state).", true) + -- No announcement to coalition - this is expected behavior when base is captured -- Mark mission as failed and cleanup immediately mission.status = "failed" return @@ -423,10 +423,8 @@ local function dispatchCargo(squadron, coalitionKey) local originCoalition = originAirbase:GetCoalition() if originCoalition ~= coalitionSide then - log("WARNING: Origin airbase '" .. origin .. "' is not controlled by " .. coalitionKey .. " coalition (currently coalition " .. tostring(originCoalition) .. "). This will cause RAT to fail. Skipping dispatch.") - announceToCoalition(coalitionKey, "Resupply mission from " .. origin .. " failed (origin airbase captured by enemy)!") - -- Mark mission as failed and cleanup immediately - mission.status = "failed" + log("INFO: Origin airbase '" .. origin .. "' captured by enemy - trying another supply source.", true) + -- Don't announce or mark as failed - the dispatcher will try another origin return end @@ -581,18 +579,24 @@ end -------------------------------------------------------------------------- Checks all squadrons for each coalition. If a squadron is below the resupply threshold and has no active cargo mission, triggers a supply request and dispatches a cargo aircraft. + Skips squadrons that are captured or not operational. ]] local function monitorSquadrons() for _, coalitionKey in ipairs({"red", "blue"}) do local config = CARGO_SUPPLY_CONFIG[coalitionKey] local squadrons = (coalitionKey == "red") and RED_SQUADRON_CONFIG or BLUE_SQUADRON_CONFIG for _, squadron in ipairs(squadrons) do - local current, max, ratio = getSquadronStatus(squadron, coalitionKey) - log("Squadron status: " .. squadron.displayName .. " (" .. coalitionKey .. ") " .. current .. "/" .. max .. " ratio: " .. string.format("%.2f", ratio)) - if ratio <= config.threshold and not hasActiveCargoMission(coalitionKey, squadron.airbaseName) then - log("Supply request triggered for " .. squadron.displayName .. " at " .. squadron.airbaseName) - announceToCoalition(coalitionKey, "Supply requested for " .. squadron.airbaseName .. "! Squadron: " .. squadron.displayName) - dispatchCargo(squadron, coalitionKey) + -- Skip non-operational squadrons (captured, destroyed, etc.) + if squadron.state and squadron.state ~= "operational" then + log("Squadron " .. squadron.displayName .. " (" .. coalitionKey .. ") is " .. squadron.state .. " - skipping cargo dispatch", true) + else + local current, max, ratio = getSquadronStatus(squadron, coalitionKey) + log("Squadron status: " .. squadron.displayName .. " (" .. coalitionKey .. ") " .. current .. "/" .. max .. " ratio: " .. string.format("%.2f", ratio)) + if ratio <= config.threshold and not hasActiveCargoMission(coalitionKey, squadron.airbaseName) then + log("Supply request triggered for " .. squadron.displayName .. " at " .. squadron.airbaseName) + announceToCoalition(coalitionKey, "Supply requested for " .. squadron.airbaseName .. "! Squadron: " .. squadron.displayName) + dispatchCargo(squadron, coalitionKey) + end end end end diff --git a/DCS_Kola/Operation_Polar_Shield/Moose_TADC_Load2nd.lua b/DCS_Kola/Operation_Polar_Shield/Moose_TADC_Load2nd.lua index 6a29065..3f903b6 100644 --- a/DCS_Kola/Operation_Polar_Shield/Moose_TADC_Load2nd.lua +++ b/DCS_Kola/Operation_Polar_Shield/Moose_TADC_Load2nd.lua @@ -319,11 +319,15 @@ end -- Squadron resource summary generator local function getSquadronResourceSummary(coalitionSide) - local function getStatus(remaining, max) + local function getStatus(remaining, max, state) + if state == "captured" then return "[CAPTURED]" end + if state == "destroyed" then return "[DESTROYED]" end + if state ~= "operational" then return "[OFFLINE]" end + local percent = (remaining / max) * 100 - if percent <= 10 then return "[CRITICAL]" end - if percent <= 25 then return "[LOW]" end - return "OK" + if percent <= 10 then return "[CRITICAL]" end + if percent <= 25 then return "[LOW]" end + return "OK" end local lines = {} @@ -336,19 +340,21 @@ local function getSquadronResourceSummary(coalitionSide) for _, squadron in pairs(RED_SQUADRON_CONFIG) do local remaining = squadronAircraftCounts.red[squadron.templateName] or 0 local max = squadron.aircraft or 0 - local status = getStatus(remaining, max) + local state = squadron.state or "operational" + local status = getStatus(remaining, max, state) table.insert(lines, string.format("| %-13s | %2d / %-15d | %-11s |", squadron.displayName or squadron.templateName, remaining, max, status)) end elseif coalitionSide == coalition.side.BLUE then for _, squadron in pairs(BLUE_SQUADRON_CONFIG) do local remaining = squadronAircraftCounts.blue[squadron.templateName] or 0 local max = squadron.aircraft or 0 - local status = getStatus(remaining, max) + local state = squadron.state or "operational" + local status = getStatus(remaining, max, state) table.insert(lines, string.format("| %-13s | %2d / %-15d | %-11s |", squadron.displayName or squadron.templateName, remaining, max, status)) end end - table.insert(lines, "\n- [LOW]: Below 25%\n- [CRITICAL]: Below 10%\n- OK: Above 25%") + table.insert(lines, "\n- [CAPTURED]: Airbase captured by enemy\n- [LOW]: Below 25%\n- [CRITICAL]: Below 10%\n- OK: Above 25%") return table.concat(lines, "\n") end @@ -1404,8 +1410,20 @@ local function findBestSquadron(threatCoord, threatSize, coalitionSide) local squadronAvailable = true local unavailableReason = "" + -- Check squadron state first + if squadron.state and squadron.state ~= "operational" then + squadronAvailable = false + if squadron.state == "captured" then + unavailableReason = "airbase captured by enemy" + elseif squadron.state == "destroyed" then + unavailableReason = "airbase destroyed" + else + unavailableReason = "squadron not operational (state: " .. tostring(squadron.state) .. ")" + end + end + -- Check cooldown - if squadronCooldowns[coalitionKey][squadron.templateName] then + if squadronAvailable and squadronCooldowns[coalitionKey][squadron.templateName] then local cooldownEnd = squadronCooldowns[coalitionKey][squadron.templateName] if currentTime < cooldownEnd then local timeLeft = math.ceil((cooldownEnd - currentTime) / 60) @@ -1808,12 +1826,36 @@ local function checkAirbaseStatus() 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 airbase = AIRBASE:FindByName(squadron.airbaseName) local aircraftCount = squadronAircraftCounts.red[squadron.templateName] or 0 local maxAircraft = squadron.aircraft - local aircraftStatus = " Aircraft: " .. aircraftCount .. "/" .. maxAircraft + + -- Determine status based on squadron state + local statusPrefix = "✗" + local statusText = "" + local usable = false + + if squadron.state == "operational" then + statusPrefix = "✓" + statusText = "Operational: " .. aircraftCount .. "/" .. maxAircraft .. " aircraft" + usable = true + elseif squadron.state == "captured" then + -- Determine who captured it + local capturedBy = "enemy" + if airbase and airbase:IsAlive() then + local airbaseCoalition = airbase:GetCoalition() + if airbaseCoalition == coalition.side.BLUE then + capturedBy = "Blue" + elseif airbaseCoalition == coalition.side.NEUTRAL then + capturedBy = "neutral forces" + end + end + statusText = "Captured by " .. capturedBy .. ": " .. aircraftCount .. "/" .. maxAircraft .. " aircraft" + elseif squadron.state == "destroyed" then + statusText = "Destroyed: " .. aircraftCount .. "/" .. maxAircraft .. " aircraft" + else + statusText = "Unknown state: " .. aircraftCount .. "/" .. maxAircraft .. " aircraft" + end -- Add zone information if configured local zoneStatus = "" @@ -1825,9 +1867,9 @@ local function checkAirbaseStatus() zoneStatus = " Zones: " .. table.concat(zones, " ") end - -- Check if squadron is on cooldown + -- Check if squadron is on cooldown (only show for operational squadrons) local cooldownStatus = "" - if squadronCooldowns.red[squadron.templateName] then + if squadron.state == "operational" and squadronCooldowns.red[squadron.templateName] then local cooldownEnd = squadronCooldowns.red[squadron.templateName] if currentTime < cooldownEnd then local timeLeft = math.ceil((cooldownEnd - currentTime) / 60) @@ -1835,14 +1877,13 @@ local function checkAirbaseStatus() end end - local fullStatus = status .. aircraftStatus .. zoneStatus .. cooldownStatus + local fullStatus = statusText .. zoneStatus .. cooldownStatus if usable and cooldownStatus == "" and aircraftCount > 0 then redUsableCount = redUsableCount + 1 - log("✓ " .. squadron.airbaseName .. " - " .. fullStatus) - else - log("✗ " .. squadron.airbaseName .. " - " .. fullStatus) end + + log(statusPrefix .. " " .. squadron.airbaseName .. " - " .. fullStatus) end log("RED Status: " .. redUsableCount .. "/" .. #RED_SQUADRON_CONFIG .. " airbases operational") end @@ -1851,12 +1892,36 @@ local function checkAirbaseStatus() 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 airbase = AIRBASE:FindByName(squadron.airbaseName) local aircraftCount = squadronAircraftCounts.blue[squadron.templateName] or 0 local maxAircraft = squadron.aircraft - local aircraftStatus = " Aircraft: " .. aircraftCount .. "/" .. maxAircraft + + -- Determine status based on squadron state + local statusPrefix = "✗" + local statusText = "" + local usable = false + + if squadron.state == "operational" then + statusPrefix = "✓" + statusText = "Operational: " .. aircraftCount .. "/" .. maxAircraft .. " aircraft" + usable = true + elseif squadron.state == "captured" then + -- Determine who captured it + local capturedBy = "enemy" + if airbase and airbase:IsAlive() then + local airbaseCoalition = airbase:GetCoalition() + if airbaseCoalition == coalition.side.RED then + capturedBy = "Red" + elseif airbaseCoalition == coalition.side.NEUTRAL then + capturedBy = "neutral forces" + end + end + statusText = "Captured by " .. capturedBy .. ": " .. aircraftCount .. "/" .. maxAircraft .. " aircraft" + elseif squadron.state == "destroyed" then + statusText = "Destroyed: " .. aircraftCount .. "/" .. maxAircraft .. " aircraft" + else + statusText = "Unknown state: " .. aircraftCount .. "/" .. maxAircraft .. " aircraft" + end -- Add zone information if configured local zoneStatus = "" @@ -1868,9 +1933,9 @@ local function checkAirbaseStatus() zoneStatus = " Zones: " .. table.concat(zones, " ") end - -- Check if squadron is on cooldown + -- Check if squadron is on cooldown (only show for operational squadrons) local cooldownStatus = "" - if squadronCooldowns.blue[squadron.templateName] then + if squadron.state == "operational" and squadronCooldowns.blue[squadron.templateName] then local cooldownEnd = squadronCooldowns.blue[squadron.templateName] if currentTime < cooldownEnd then local timeLeft = math.ceil((cooldownEnd - currentTime) / 60) @@ -1878,14 +1943,14 @@ local function checkAirbaseStatus() end end - local fullStatus = status .. aircraftStatus .. zoneStatus .. cooldownStatus + local fullStatus = statusText .. zoneStatus .. cooldownStatus if usable and cooldownStatus == "" and aircraftCount > 0 then blueUsableCount = blueUsableCount + 1 - log("✓ " .. squadron.airbaseName .. " - " .. fullStatus) - else - log("✗ " .. squadron.airbaseName .. " - " .. fullStatus) end + + log(statusPrefix .. " " .. squadron.airbaseName .. " - " .. fullStatus) + end end log("BLUE Status: " .. blueUsableCount .. "/" .. #BLUE_SQUADRON_CONFIG .. " airbases operational") end @@ -1911,6 +1976,63 @@ local function cleanupOldDeliveries() end end +-- Update squadron states based on airbase coalition control +local function updateSquadronStates() + -- Update RED squadrons + for _, squadron in pairs(RED_SQUADRON_CONFIG) do + local airbase = AIRBASE:FindByName(squadron.airbaseName) + if airbase and airbase:IsAlive() then + local airbaseCoalition = airbase:GetCoalition() + if airbaseCoalition == coalition.side.RED then + -- Only update to operational if not already operational (avoid spam) + if squadron.state ~= "operational" then + squadron.state = "operational" + log("RED Squadron " .. squadron.displayName .. " at " .. squadron.airbaseName .. " is now operational") + end + else + -- Airbase captured + if squadron.state ~= "captured" then + squadron.state = "captured" + log("RED Squadron " .. squadron.displayName .. " at " .. squadron.airbaseName .. " has been captured by enemy") + end + end + else + -- Airbase destroyed or not found + if squadron.state ~= "destroyed" then + squadron.state = "destroyed" + log("RED Squadron " .. squadron.displayName .. " at " .. squadron.airbaseName .. " airbase destroyed or not found") + end + end + end + + -- Update BLUE squadrons + for _, squadron in pairs(BLUE_SQUADRON_CONFIG) do + local airbase = AIRBASE:FindByName(squadron.airbaseName) + if airbase and airbase:IsAlive() then + local airbaseCoalition = airbase:GetCoalition() + if airbaseCoalition == coalition.side.BLUE then + -- Only update to operational if not already operational (avoid spam) + if squadron.state ~= "operational" then + squadron.state = "operational" + log("BLUE Squadron " .. squadron.displayName .. " at " .. squadron.airbaseName .. " is now operational") + end + else + -- Airbase captured + if squadron.state ~= "captured" then + squadron.state = "captured" + log("BLUE Squadron " .. squadron.displayName .. " at " .. squadron.airbaseName .. " has been captured by enemy") + end + end + else + -- Airbase destroyed or not found + if squadron.state ~= "destroyed" then + squadron.state = "destroyed" + log("BLUE Squadron " .. squadron.displayName .. " at " .. squadron.airbaseName .. " airbase destroyed or not found") + end + end + end +end + -- System initialization local function initializeSystem() log("Universal Dual-Coalition TADC starting...") @@ -1962,6 +2084,15 @@ local function initializeSystem() return false end + -- Initialize squadron states + for _, squadron in pairs(RED_SQUADRON_CONFIG) do + squadron.state = "operational" + end + for _, squadron in pairs(BLUE_SQUADRON_CONFIG) do + squadron.state = "operational" + end + log("Squadron states initialized") + -- Log enabled coalitions local enabledCoalitions = {} if TADC_SETTINGS.enableRed then @@ -2037,6 +2168,7 @@ local function initializeSystem() 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, updateSquadronStates, {}, 15, 30) -- Update squadron states every 30 seconds SCHEDULER:New(nil, cleanupOldDeliveries, {}, 60, 3600) -- Cleanup old delivery records every hour -- Start periodic squadron summary broadcast diff --git a/Moose_TADC/Moose_TADC_CargoDispatcher.lua b/Moose_TADC/Moose_TADC_CargoDispatcher.lua index d6b374a..e2577a8 100644 --- a/Moose_TADC/Moose_TADC_CargoDispatcher.lua +++ b/Moose_TADC/Moose_TADC_CargoDispatcher.lua @@ -404,8 +404,8 @@ local function dispatchCargo(squadron, coalitionKey) local destCoalition = destAirbase:GetCoalition() if destCoalition ~= coalitionSide then - log("WARNING: Destination airbase '" .. destination .. "' is not controlled by " .. coalitionKey .. " coalition (currently coalition " .. tostring(destCoalition) .. "). This will cause RAT to fail with 'Airbase doesn't exist' error. Skipping dispatch.") - announceToCoalition(coalitionKey, "Resupply mission to " .. destination .. " aborted (airbase captured by enemy)!") + log("INFO: Destination airbase '" .. destination .. "' captured by enemy - cargo dispatch skipped (normal mission state).", true) + -- No announcement to coalition - this is expected behavior when base is captured -- Mark mission as failed and cleanup immediately mission.status = "failed" return @@ -423,10 +423,8 @@ local function dispatchCargo(squadron, coalitionKey) local originCoalition = originAirbase:GetCoalition() if originCoalition ~= coalitionSide then - log("WARNING: Origin airbase '" .. origin .. "' is not controlled by " .. coalitionKey .. " coalition (currently coalition " .. tostring(originCoalition) .. "). This will cause RAT to fail. Skipping dispatch.") - announceToCoalition(coalitionKey, "Resupply mission from " .. origin .. " failed (origin airbase captured by enemy)!") - -- Mark mission as failed and cleanup immediately - mission.status = "failed" + log("INFO: Origin airbase '" .. origin .. "' captured by enemy - trying another supply source.", true) + -- Don't announce or mark as failed - the dispatcher will try another origin return end @@ -581,18 +579,24 @@ end -------------------------------------------------------------------------- Checks all squadrons for each coalition. If a squadron is below the resupply threshold and has no active cargo mission, triggers a supply request and dispatches a cargo aircraft. + Skips squadrons that are captured or not operational. ]] local function monitorSquadrons() for _, coalitionKey in ipairs({"red", "blue"}) do local config = CARGO_SUPPLY_CONFIG[coalitionKey] local squadrons = (coalitionKey == "red") and RED_SQUADRON_CONFIG or BLUE_SQUADRON_CONFIG for _, squadron in ipairs(squadrons) do - local current, max, ratio = getSquadronStatus(squadron, coalitionKey) - log("Squadron status: " .. squadron.displayName .. " (" .. coalitionKey .. ") " .. current .. "/" .. max .. " ratio: " .. string.format("%.2f", ratio)) - if ratio <= config.threshold and not hasActiveCargoMission(coalitionKey, squadron.airbaseName) then - log("Supply request triggered for " .. squadron.displayName .. " at " .. squadron.airbaseName) - announceToCoalition(coalitionKey, "Supply requested for " .. squadron.airbaseName .. "! Squadron: " .. squadron.displayName) - dispatchCargo(squadron, coalitionKey) + -- Skip non-operational squadrons (captured, destroyed, etc.) + if squadron.state and squadron.state ~= "operational" then + log("Squadron " .. squadron.displayName .. " (" .. coalitionKey .. ") is " .. squadron.state .. " - skipping cargo dispatch", true) + else + local current, max, ratio = getSquadronStatus(squadron, coalitionKey) + log("Squadron status: " .. squadron.displayName .. " (" .. coalitionKey .. ") " .. current .. "/" .. max .. " ratio: " .. string.format("%.2f", ratio)) + if ratio <= config.threshold and not hasActiveCargoMission(coalitionKey, squadron.airbaseName) then + log("Supply request triggered for " .. squadron.displayName .. " at " .. squadron.airbaseName) + announceToCoalition(coalitionKey, "Supply requested for " .. squadron.airbaseName .. "! Squadron: " .. squadron.displayName) + dispatchCargo(squadron, coalitionKey) + end end end end diff --git a/Moose_TADC/Moose_TADC_Load2nd.lua b/Moose_TADC/Moose_TADC_Load2nd.lua index 6a29065..3f903b6 100644 --- a/Moose_TADC/Moose_TADC_Load2nd.lua +++ b/Moose_TADC/Moose_TADC_Load2nd.lua @@ -319,11 +319,15 @@ end -- Squadron resource summary generator local function getSquadronResourceSummary(coalitionSide) - local function getStatus(remaining, max) + local function getStatus(remaining, max, state) + if state == "captured" then return "[CAPTURED]" end + if state == "destroyed" then return "[DESTROYED]" end + if state ~= "operational" then return "[OFFLINE]" end + local percent = (remaining / max) * 100 - if percent <= 10 then return "[CRITICAL]" end - if percent <= 25 then return "[LOW]" end - return "OK" + if percent <= 10 then return "[CRITICAL]" end + if percent <= 25 then return "[LOW]" end + return "OK" end local lines = {} @@ -336,19 +340,21 @@ local function getSquadronResourceSummary(coalitionSide) for _, squadron in pairs(RED_SQUADRON_CONFIG) do local remaining = squadronAircraftCounts.red[squadron.templateName] or 0 local max = squadron.aircraft or 0 - local status = getStatus(remaining, max) + local state = squadron.state or "operational" + local status = getStatus(remaining, max, state) table.insert(lines, string.format("| %-13s | %2d / %-15d | %-11s |", squadron.displayName or squadron.templateName, remaining, max, status)) end elseif coalitionSide == coalition.side.BLUE then for _, squadron in pairs(BLUE_SQUADRON_CONFIG) do local remaining = squadronAircraftCounts.blue[squadron.templateName] or 0 local max = squadron.aircraft or 0 - local status = getStatus(remaining, max) + local state = squadron.state or "operational" + local status = getStatus(remaining, max, state) table.insert(lines, string.format("| %-13s | %2d / %-15d | %-11s |", squadron.displayName or squadron.templateName, remaining, max, status)) end end - table.insert(lines, "\n- [LOW]: Below 25%\n- [CRITICAL]: Below 10%\n- OK: Above 25%") + table.insert(lines, "\n- [CAPTURED]: Airbase captured by enemy\n- [LOW]: Below 25%\n- [CRITICAL]: Below 10%\n- OK: Above 25%") return table.concat(lines, "\n") end @@ -1404,8 +1410,20 @@ local function findBestSquadron(threatCoord, threatSize, coalitionSide) local squadronAvailable = true local unavailableReason = "" + -- Check squadron state first + if squadron.state and squadron.state ~= "operational" then + squadronAvailable = false + if squadron.state == "captured" then + unavailableReason = "airbase captured by enemy" + elseif squadron.state == "destroyed" then + unavailableReason = "airbase destroyed" + else + unavailableReason = "squadron not operational (state: " .. tostring(squadron.state) .. ")" + end + end + -- Check cooldown - if squadronCooldowns[coalitionKey][squadron.templateName] then + if squadronAvailable and squadronCooldowns[coalitionKey][squadron.templateName] then local cooldownEnd = squadronCooldowns[coalitionKey][squadron.templateName] if currentTime < cooldownEnd then local timeLeft = math.ceil((cooldownEnd - currentTime) / 60) @@ -1808,12 +1826,36 @@ local function checkAirbaseStatus() 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 airbase = AIRBASE:FindByName(squadron.airbaseName) local aircraftCount = squadronAircraftCounts.red[squadron.templateName] or 0 local maxAircraft = squadron.aircraft - local aircraftStatus = " Aircraft: " .. aircraftCount .. "/" .. maxAircraft + + -- Determine status based on squadron state + local statusPrefix = "✗" + local statusText = "" + local usable = false + + if squadron.state == "operational" then + statusPrefix = "✓" + statusText = "Operational: " .. aircraftCount .. "/" .. maxAircraft .. " aircraft" + usable = true + elseif squadron.state == "captured" then + -- Determine who captured it + local capturedBy = "enemy" + if airbase and airbase:IsAlive() then + local airbaseCoalition = airbase:GetCoalition() + if airbaseCoalition == coalition.side.BLUE then + capturedBy = "Blue" + elseif airbaseCoalition == coalition.side.NEUTRAL then + capturedBy = "neutral forces" + end + end + statusText = "Captured by " .. capturedBy .. ": " .. aircraftCount .. "/" .. maxAircraft .. " aircraft" + elseif squadron.state == "destroyed" then + statusText = "Destroyed: " .. aircraftCount .. "/" .. maxAircraft .. " aircraft" + else + statusText = "Unknown state: " .. aircraftCount .. "/" .. maxAircraft .. " aircraft" + end -- Add zone information if configured local zoneStatus = "" @@ -1825,9 +1867,9 @@ local function checkAirbaseStatus() zoneStatus = " Zones: " .. table.concat(zones, " ") end - -- Check if squadron is on cooldown + -- Check if squadron is on cooldown (only show for operational squadrons) local cooldownStatus = "" - if squadronCooldowns.red[squadron.templateName] then + if squadron.state == "operational" and squadronCooldowns.red[squadron.templateName] then local cooldownEnd = squadronCooldowns.red[squadron.templateName] if currentTime < cooldownEnd then local timeLeft = math.ceil((cooldownEnd - currentTime) / 60) @@ -1835,14 +1877,13 @@ local function checkAirbaseStatus() end end - local fullStatus = status .. aircraftStatus .. zoneStatus .. cooldownStatus + local fullStatus = statusText .. zoneStatus .. cooldownStatus if usable and cooldownStatus == "" and aircraftCount > 0 then redUsableCount = redUsableCount + 1 - log("✓ " .. squadron.airbaseName .. " - " .. fullStatus) - else - log("✗ " .. squadron.airbaseName .. " - " .. fullStatus) end + + log(statusPrefix .. " " .. squadron.airbaseName .. " - " .. fullStatus) end log("RED Status: " .. redUsableCount .. "/" .. #RED_SQUADRON_CONFIG .. " airbases operational") end @@ -1851,12 +1892,36 @@ local function checkAirbaseStatus() 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 airbase = AIRBASE:FindByName(squadron.airbaseName) local aircraftCount = squadronAircraftCounts.blue[squadron.templateName] or 0 local maxAircraft = squadron.aircraft - local aircraftStatus = " Aircraft: " .. aircraftCount .. "/" .. maxAircraft + + -- Determine status based on squadron state + local statusPrefix = "✗" + local statusText = "" + local usable = false + + if squadron.state == "operational" then + statusPrefix = "✓" + statusText = "Operational: " .. aircraftCount .. "/" .. maxAircraft .. " aircraft" + usable = true + elseif squadron.state == "captured" then + -- Determine who captured it + local capturedBy = "enemy" + if airbase and airbase:IsAlive() then + local airbaseCoalition = airbase:GetCoalition() + if airbaseCoalition == coalition.side.RED then + capturedBy = "Red" + elseif airbaseCoalition == coalition.side.NEUTRAL then + capturedBy = "neutral forces" + end + end + statusText = "Captured by " .. capturedBy .. ": " .. aircraftCount .. "/" .. maxAircraft .. " aircraft" + elseif squadron.state == "destroyed" then + statusText = "Destroyed: " .. aircraftCount .. "/" .. maxAircraft .. " aircraft" + else + statusText = "Unknown state: " .. aircraftCount .. "/" .. maxAircraft .. " aircraft" + end -- Add zone information if configured local zoneStatus = "" @@ -1868,9 +1933,9 @@ local function checkAirbaseStatus() zoneStatus = " Zones: " .. table.concat(zones, " ") end - -- Check if squadron is on cooldown + -- Check if squadron is on cooldown (only show for operational squadrons) local cooldownStatus = "" - if squadronCooldowns.blue[squadron.templateName] then + if squadron.state == "operational" and squadronCooldowns.blue[squadron.templateName] then local cooldownEnd = squadronCooldowns.blue[squadron.templateName] if currentTime < cooldownEnd then local timeLeft = math.ceil((cooldownEnd - currentTime) / 60) @@ -1878,14 +1943,14 @@ local function checkAirbaseStatus() end end - local fullStatus = status .. aircraftStatus .. zoneStatus .. cooldownStatus + local fullStatus = statusText .. zoneStatus .. cooldownStatus if usable and cooldownStatus == "" and aircraftCount > 0 then blueUsableCount = blueUsableCount + 1 - log("✓ " .. squadron.airbaseName .. " - " .. fullStatus) - else - log("✗ " .. squadron.airbaseName .. " - " .. fullStatus) end + + log(statusPrefix .. " " .. squadron.airbaseName .. " - " .. fullStatus) + end end log("BLUE Status: " .. blueUsableCount .. "/" .. #BLUE_SQUADRON_CONFIG .. " airbases operational") end @@ -1911,6 +1976,63 @@ local function cleanupOldDeliveries() end end +-- Update squadron states based on airbase coalition control +local function updateSquadronStates() + -- Update RED squadrons + for _, squadron in pairs(RED_SQUADRON_CONFIG) do + local airbase = AIRBASE:FindByName(squadron.airbaseName) + if airbase and airbase:IsAlive() then + local airbaseCoalition = airbase:GetCoalition() + if airbaseCoalition == coalition.side.RED then + -- Only update to operational if not already operational (avoid spam) + if squadron.state ~= "operational" then + squadron.state = "operational" + log("RED Squadron " .. squadron.displayName .. " at " .. squadron.airbaseName .. " is now operational") + end + else + -- Airbase captured + if squadron.state ~= "captured" then + squadron.state = "captured" + log("RED Squadron " .. squadron.displayName .. " at " .. squadron.airbaseName .. " has been captured by enemy") + end + end + else + -- Airbase destroyed or not found + if squadron.state ~= "destroyed" then + squadron.state = "destroyed" + log("RED Squadron " .. squadron.displayName .. " at " .. squadron.airbaseName .. " airbase destroyed or not found") + end + end + end + + -- Update BLUE squadrons + for _, squadron in pairs(BLUE_SQUADRON_CONFIG) do + local airbase = AIRBASE:FindByName(squadron.airbaseName) + if airbase and airbase:IsAlive() then + local airbaseCoalition = airbase:GetCoalition() + if airbaseCoalition == coalition.side.BLUE then + -- Only update to operational if not already operational (avoid spam) + if squadron.state ~= "operational" then + squadron.state = "operational" + log("BLUE Squadron " .. squadron.displayName .. " at " .. squadron.airbaseName .. " is now operational") + end + else + -- Airbase captured + if squadron.state ~= "captured" then + squadron.state = "captured" + log("BLUE Squadron " .. squadron.displayName .. " at " .. squadron.airbaseName .. " has been captured by enemy") + end + end + else + -- Airbase destroyed or not found + if squadron.state ~= "destroyed" then + squadron.state = "destroyed" + log("BLUE Squadron " .. squadron.displayName .. " at " .. squadron.airbaseName .. " airbase destroyed or not found") + end + end + end +end + -- System initialization local function initializeSystem() log("Universal Dual-Coalition TADC starting...") @@ -1962,6 +2084,15 @@ local function initializeSystem() return false end + -- Initialize squadron states + for _, squadron in pairs(RED_SQUADRON_CONFIG) do + squadron.state = "operational" + end + for _, squadron in pairs(BLUE_SQUADRON_CONFIG) do + squadron.state = "operational" + end + log("Squadron states initialized") + -- Log enabled coalitions local enabledCoalitions = {} if TADC_SETTINGS.enableRed then @@ -2037,6 +2168,7 @@ local function initializeSystem() 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, updateSquadronStates, {}, 15, 30) -- Update squadron states every 30 seconds SCHEDULER:New(nil, cleanupOldDeliveries, {}, 60, 3600) -- Cleanup old delivery records every hour -- Start periodic squadron summary broadcast diff --git a/Moose_TADC/TADC_Example.miz b/Moose_TADC/TADC_Example.miz index 73dbd3a317d8d5a7713a6e6c8a7241280f4af564..5790ba718a1d19faeda57dff3a7bab2c87a2d086 100644 GIT binary patch delta 30622 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 delta 29899 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 S=Q`rQWeW)gMm~-FFZ&;b#Ns~y