From 1481f566549b23069328bb222c2919b4e491cd59 Mon Sep 17 00:00:00 2001 From: iTracerFacer <134304944+iTracerFacer@users.noreply.github.com> Date: Wed, 15 Oct 2025 16:51:38 -0500 Subject: [PATCH] Split the squadron setup file out of the TADC for smoother upgrades. --- .../Moose_TADC_Load2nd.lua | 244 +-- .../Moose_TADC_SquadronConfigs_Load1st.lua | 244 +++ Moose_TADC/Moose_TADC_Load2nd.lua | 1660 +++++++++++++++++ .../Moose_TADC_SquadronConfigs_Load1st.lua | 244 +++ Moose_TADC/TADC_Example.miz | Bin 822600 -> 823566 bytes 5 files changed, 2151 insertions(+), 241 deletions(-) rename Moose_TADC/Moose_TADC.lua => DCS_Kola/Operation_Polar_Shield/Moose_TADC_Load2nd.lua (83%) create mode 100644 DCS_Kola/Operation_Polar_Shield/Moose_TADC_SquadronConfigs_Load1st.lua create mode 100644 Moose_TADC/Moose_TADC_Load2nd.lua create mode 100644 Moose_TADC/Moose_TADC_SquadronConfigs_Load1st.lua diff --git a/Moose_TADC/Moose_TADC.lua b/DCS_Kola/Operation_Polar_Shield/Moose_TADC_Load2nd.lua similarity index 83% rename from Moose_TADC/Moose_TADC.lua rename to DCS_Kola/Operation_Polar_Shield/Moose_TADC_Load2nd.lua index 99df3e1..3507b2c 100644 --- a/Moose_TADC/Moose_TADC.lua +++ b/DCS_Kola/Operation_Polar_Shield/Moose_TADC_Load2nd.lua @@ -164,6 +164,9 @@ local TADC_SETTINGS = { }, } +-- Load squadron configs from external file +-- RED_SQUADRON_CONFIG and BLUE_SQUADRON_CONFIG are now global, loaded by the trigger + --[[ INTERCEPT RATIO CHART - How many interceptors launch per threat aircraft: @@ -197,247 +200,6 @@ ASYMMETRIC SCENARIOS: • Different maxActiveCAP values create capacity imbalances ]] ---[[ -═══════════════════════════════════════════════════════════════════════════════ - SQUADRON CONFIGURATION -═══════════════════════════════════════════════════════════════════════════════ - -INSTRUCTIONS: -1. Create fighter aircraft templates for BOTH coalitions in the mission editor -2. Place them at or near the airbases you want them to operate from -3. Configure RED squadrons in RED_SQUADRON_CONFIG -4. Configure BLUE squadrons in BLUE_SQUADRON_CONFIG - -TEMPLATE NAMING SUGGESTIONS: -• RED: "RED_CAP_Batumi_F15", "RED_INTERCEPT_Senaki_MiG29" -• BLUE: "BLUE_CAP_Nellis_F16", "BLUE_INTERCEPT_Creech_F22" -• Include coalition and airbase name for easy identification - -AIRBASE NAMES: -• Use exact names as they appear in DCS (case sensitive) -• RED examples: "Batumi", "Senaki", "Gudauta" -• BLUE examples: "Nellis AFB", "McCarran International", "Tonopah Test Range" -• Find airbase names in the mission editor - -AIRCRAFT NUMBERS: -• Set realistic numbers based on mission requirements -• Consider aircraft consumption and cargo replenishment -• Balance between realism and gameplay performance - -ZONE-BASED AREAS OF RESPONSIBILITY: -• Create zones in mission editor (MOOSE polygons, circles, etc.) -• primaryZone: Squadron's main area (full response) -• secondaryZone: Backup/support area (reduced response) -• tertiaryZone: Emergency fallback area (enhanced response) -• Leave zones as nil for global threat response -• Multiple squadrons can share overlapping zones -• Use zone names exactly as they appear in mission editor - -ZONE BEHAVIOR EXAMPLES: -• Border Defense: primaryZone = "SECTOR_ALPHA", secondaryZone = "BUFFER_ZONE" -• Base Defense: tertiaryZone = "BASE_PERIMETER", enableFallback = true -• Layered Defense: Different zones per squadron with overlap -• Emergency Response: High tertiaryResponse ratio for critical areas -]] - --- ═══════════════════════════════════════════════════════════════════════════ --- RED COALITION SQUADRONS --- ═══════════════════════════════════════════════════════════════════════════ - -local RED_SQUADRON_CONFIG = { - --[[ EXAMPLE RED SQUADRON - CUSTOMIZE FOR YOUR MISSION - { - templateName = "RED_CAP_Batumi_F15", -- Template name from mission editor - displayName = "Batumi F-15C CAP", -- Human-readable name for logs - airbaseName = "Batumi", -- Exact airbase name from DCS - aircraft = 12, -- Maximum aircraft in squadron - skill = AI.Skill.GOOD, -- AI skill level - altitude = 20000, -- Patrol altitude (feet) - speed = 350, -- Patrol speed (knots) - patrolTime = 25, -- Time on station (minutes) - type = "FIGHTER" -- Aircraft type - - -- Zone-based Areas of Responsibility (optional - leave nil for global response) - primaryZone = "RED_BORDER", -- Main responsibility area (zone name from mission editor) - secondaryZone = nil, -- Secondary coverage area (zone name) - tertiaryZone = nil, -- Emergency/fallback zone (zone name) - - -- Zone behavior settings (optional - uses defaults if not specified) - zoneConfig = { - primaryResponse = 1.0, -- Intercept ratio multiplier in primary zone - secondaryResponse = 0.6, -- Intercept ratio multiplier in secondary zone - tertiaryResponse = 1.4, -- Intercept ratio multiplier in tertiary zone - maxRange = 200, -- Maximum engagement range from airbase (nm) - enableFallback = false, -- Auto-switch to tertiary when base threatened - priorityThreshold = 4, -- Min aircraft count for "major threat" - ignoreLowPriority = false, -- Ignore threats below threshold in secondary zones - } - }, - ]] - - -- ADD YOUR RED SQUADRONS HERE - { - templateName = "Sukhumi CAP", -- Change to your RED template name - displayName = "Sukhumi CAP", -- Change to your preferred name - airbaseName = "Sukhumi-Babushara", -- Change to your RED airbase - aircraft = 12, -- Adjust aircraft count - skill = AI.Skill.ACE, -- AVERAGE, GOOD, HIGH, EXCELLENT, ACE - altitude = 20000, -- Patrol altitude (feet) - speed = 350, -- Patrol speed (knots) - patrolTime = 25, -- Time on station (minutes) - type = "FIGHTER", - - -- Zone-based Areas of Responsibility (optional - leave nil for global response) - primaryZone = "RED_BORDER", -- Main responsibility area (zone name from mission editor) - secondaryZone = nil, -- Secondary coverage area (zone name) - tertiaryZone = nil, -- Emergency/fallback zone (zone name) - - -- Zone behavior settings (optional - uses defaults if not specified) - zoneConfig = { - primaryResponse = 1.0, -- Intercept ratio multiplier in primary zone - secondaryResponse = 0.6, -- Intercept ratio multiplier in secondary zone - tertiaryResponse = 1.4, -- Intercept ratio multiplier in tertiary zone - maxRange = 200, -- Maximum engagement range from airbase (nm) - enableFallback = false, -- Auto-switch to tertiary when base threatened - priorityThreshold = 4, -- Min aircraft count for "major threat" - ignoreLowPriority = false, -- Ignore threats below threshold in secondary zones - } - }, - - { - templateName = "Gudauta CAP-MiG-21", -- Change to your RED template name - displayName = "Gudauta CAP-MiG-21", -- Change to your preferred name - airbaseName = "Gudauta", -- Change to your RED airbase - aircraft = 12, -- Adjust aircraft count - skill = AI.Skill.ACE, -- AVERAGE, GOOD, HIGH, EXCELLENT - altitude = 20000, -- Patrol altitude (feet) - speed = 350, -- Patrol speed (knots) - patrolTime = 25, -- Time on station (minutes) - type = "FIGHTER", - - -- Zone-based Areas of Responsibility (optional - leave nil for global response) - primaryZone = "GUDAUTA_BORDER", -- Main responsibility area (zone name from mission editor) - secondaryZone = nil, -- Secondary coverage area (zone name) - tertiaryZone = nil, -- Emergency/fallback zone (zone name) - - -- Zone behavior settings (optional - uses defaults if not specified) - zoneConfig = { - primaryResponse = 1.0, -- Intercept ratio multiplier in primary zone - secondaryResponse = 0.6, -- Intercept ratio multiplier in secondary zone - tertiaryResponse = 1.4, -- Intercept ratio multiplier in tertiary zone - maxRange = 200, -- Maximum engagement range from airbase (nm) - enableFallback = false, -- Auto-switch to tertiary when base threatened - priorityThreshold = 4, -- Min aircraft count for "major threat" - ignoreLowPriority = false, -- Ignore threats below threshold in secondary zones - } - }, - - { - templateName = "Gudauta CAP-MiG-23", -- Change to your RED template name - displayName = "Gudauta CAP-MiG-23", -- Change to your preferred name - airbaseName = "Gudauta", -- Change to your RED airbase - aircraft = 14, -- Adjust aircraft count - skill = AI.Skill.ACE, -- AVERAGE, GOOD, HIGH, EXCELLENT - altitude = 20000, -- Patrol altitude (feet) - speed = 350, -- Patrol speed (knots) - patrolTime = 25, -- Time on station (minutes) - type = "FIGHTER", - - -- Zone-based Areas of Responsibility (optional - leave nil for global response) - primaryZone = "GUDAUTA_BORDER", -- Main responsibility area (zone name from mission editor) - secondaryZone = "RED_BORDER", -- Secondary coverage area (zone name) - tertiaryZone = nil, -- Emergency/fallback zone (zone name) - - -- Zone behavior settings (optional - uses defaults if not specified) - zoneConfig = { - primaryResponse = 1.0, -- Intercept ratio multiplier in primary zone - secondaryResponse = 0.6, -- Intercept ratio multiplier in secondary zone - tertiaryResponse = 1.4, -- Intercept ratio multiplier in tertiary zone - maxRange = 200, -- Maximum engagement range from airbase (nm) - enableFallback = false, -- Auto-switch to tertiary when base threatened - priorityThreshold = 4, -- Min aircraft count for "major threat" - ignoreLowPriority = false, -- Ignore threats below threshold in secondary zones - } - }, -} - --- ═══════════════════════════════════════════════════════════════════════════ --- BLUE COALITION SQUADRONS --- ═══════════════════════════════════════════════════════════════════════════ - -local BLUE_SQUADRON_CONFIG = { - --[[ EXAMPLE BLUE SQUADRON - CUSTOMIZE FOR YOUR MISSION - { - templateName = "BLUE_CAP_Nellis_F16", -- Template name from mission editor - displayName = "Nellis F-16C CAP", -- Human-readable name for logs - airbaseName = "Nellis AFB", -- Exact airbase name from DCS - aircraft = 14, -- Maximum aircraft in squadron - skill = AI.Skill.EXCELLENT, -- AI skill level - altitude = 22000, -- Patrol altitude (feet) - speed = 380, -- Patrol speed (knots) - patrolTime = 28, -- Time on station (minutes) - type = "FIGHTER" -- Aircraft type - }, - ]] - - -- ADD YOUR BLUE SQUADRONS HERE - - { - templateName = "Kutaisi CAP", -- Change to your BLUE template name - displayName = "Kutaisi CAP", -- Change to your preferred name - airbaseName = "Kutaisi", -- Change to your BLUE airbase - aircraft = 18, -- Adjust aircraft count - skill = AI.Skill.EXCELLENT, -- AVERAGE, GOOD, HIGH, EXCELLENT - altitude = 18000, -- Patrol altitude (feet) - speed = 320, -- Patrol speed (knots) - patrolTime = 22, -- Time on station (minutes) - type = "FIGHTER", - - -- Zone-based Areas of Responsibility (optional - leave nil for global response) - primaryZone = "BLUE_BORDER", -- Main responsibility area (zone name from mission editor) - secondaryZone = nil, -- Secondary coverage area (zone name) - tertiaryZone = nil, -- Emergency/fallback zone (zone name) - - -- Zone behavior settings (optional - uses defaults if not specified) - zoneConfig = { - primaryResponse = 1.0, -- Intercept ratio multiplier in primary zone - secondaryResponse = 0.6, -- Intercept ratio multiplier in secondary zone - tertiaryResponse = 1.4, -- Intercept ratio multiplier in tertiary zone - maxRange = 200, -- Maximum engagement range from airbase (nm) - enableFallback = true, -- Auto-switch to tertiary when base threatened - priorityThreshold = 4, -- Min aircraft count for "major threat" - ignoreLowPriority = false, -- Ignore threats below threshold in secondary zones - } - }, - - { - templateName = "Batumi CAP", -- Change to your BLUE template name - displayName = "Batumi CAP", -- Change to your preferred name - airbaseName = "Batumi", -- Change to your BLUE airbase - aircraft = 18, -- Adjust aircraft count - skill = AI.Skill.EXCELLENT, -- AVERAGE, GOOD, HIGH, EXCELLENT - altitude = 18000, -- Patrol altitude (feet) - speed = 320, -- Patrol speed (knots) - patrolTime = 22, -- Time on station (minutes) - type = "FIGHTER", - - -- Zone-based Areas of Responsibility (optional - leave nil for global response) - primaryZone = "BATUMI_BORDER", -- Main responsibility area (zone name from mission editor) - secondaryZone = "BLUE_BORDER", -- Secondary coverage area (zone name) - tertiaryZone = "BATUMI_BORDER", -- Emergency/fallback zone (zone name) - - -- Zone behavior settings (optional - uses defaults if not specified) - zoneConfig = { - primaryResponse = 1.0, -- Intercept ratio multiplier in primary zone - secondaryResponse = 0.6, -- Intercept ratio multiplier in secondary zone - tertiaryResponse = 1.4, -- Intercept ratio multiplier in tertiary zone - maxRange = 200, -- Maximum engagement range from airbase (nm) - enableFallback = true, -- Auto-switch to tertiary when base threatened - priorityThreshold = 4, -- Min aircraft count for "major threat" - ignoreLowPriority = false, -- Ignore threats below threshold in secondary zones - } - }, -} --[[ ═══════════════════════════════════════════════════════════════════════════════ diff --git a/DCS_Kola/Operation_Polar_Shield/Moose_TADC_SquadronConfigs_Load1st.lua b/DCS_Kola/Operation_Polar_Shield/Moose_TADC_SquadronConfigs_Load1st.lua new file mode 100644 index 0000000..01f1335 --- /dev/null +++ b/DCS_Kola/Operation_Polar_Shield/Moose_TADC_SquadronConfigs_Load1st.lua @@ -0,0 +1,244 @@ + +--[[ THIS FILE MUST BE LOADED BEFORE THE MAIN Moose_TADC.lua SCRIPT +═══════════════════════════════════════════════════════════════════════════════ + SQUADRON CONFIGURATION +═══════════════════════════════════════════════════════════════════════════════ + +INSTRUCTIONS: +1. Create fighter aircraft templates for BOTH coalitions in the mission editor +2. Place them at or near the airbases you want them to operate from +3. Configure RED squadrons in RED_SQUADRON_CONFIG +4. Configure BLUE squadrons in BLUE_SQUADRON_CONFIG + +TEMPLATE NAMING SUGGESTIONS: +• RED: "RED_CAP_Batumi_F15", "RED_INTERCEPT_Senaki_MiG29" +• BLUE: "BLUE_CAP_Nellis_F16", "BLUE_INTERCEPT_Creech_F22" +• Include coalition and airbase name for easy identification + +AIRBASE NAMES: +• Use exact names as they appear in DCS (case sensitive) +• RED examples: "Batumi", "Senaki", "Gudauta" +• BLUE examples: "Nellis AFB", "McCarran International", "Tonopah Test Range" +• Find airbase names in the mission editor + +AIRCRAFT NUMBERS: +• Set realistic numbers based on mission requirements +• Consider aircraft consumption and cargo replenishment +• Balance between realism and gameplay performance + +ZONE-BASED AREAS OF RESPONSIBILITY: +• Create zones in mission editor (MOOSE polygons, circles, etc.) +• primaryZone: Squadron's main area (full response) +• secondaryZone: Backup/support area (reduced response) +• tertiaryZone: Emergency fallback area (enhanced response) +• Leave zones as nil for global threat response +• Multiple squadrons can share overlapping zones +• Use zone names exactly as they appear in mission editor + +ZONE BEHAVIOR EXAMPLES: +• Border Defense: primaryZone = "SECTOR_ALPHA", secondaryZone = "BUFFER_ZONE" +• Base Defense: tertiaryZone = "BASE_PERIMETER", enableFallback = true +• Layered Defense: Different zones per squadron with overlap +• Emergency Response: High tertiaryResponse ratio for critical areas +]] + +-- ═══════════════════════════════════════════════════════════════════════════ +-- RED COALITION SQUADRONS +-- ═══════════════════════════════════════════════════════════════════════════ + +RED_SQUADRON_CONFIG = { + --[[ EXAMPLE RED SQUADRON - CUSTOMIZE FOR YOUR MISSION + { + templateName = "RED_CAP_Batumi_F15", -- Template name from mission editor + displayName = "Batumi F-15C CAP", -- Human-readable name for logs + airbaseName = "Batumi", -- Exact airbase name from DCS + aircraft = 12, -- Maximum aircraft in squadron + skill = AI.Skill.GOOD, -- AI skill level + altitude = 20000, -- Patrol altitude (feet) + speed = 350, -- Patrol speed (knots) + patrolTime = 25, -- Time on station (minutes) + type = "FIGHTER" + + -- Zone-based Areas of Responsibility (optional - leave nil for global response) + primaryZone = "RED_BORDER", -- Main responsibility area (zone name from mission editor) + secondaryZone = nil, -- Secondary coverage area (zone name) + tertiaryZone = nil, -- Emergency/fallback zone (zone name) + + -- Zone behavior settings (optional - uses defaults if not specified) + zoneConfig = { + primaryResponse = 1.0, -- Intercept ratio multiplier in primary zone + secondaryResponse = 0.6, -- Intercept ratio multiplier in secondary zone + tertiaryResponse = 1.4, -- Intercept ratio multiplier in tertiary zone + maxRange = 200, -- Maximum engagement range from airbase (nm) + enableFallback = false, -- Auto-switch to tertiary when base threatened + priorityThreshold = 4, -- Min aircraft count for "major threat" + ignoreLowPriority = false, -- Ignore threats below threshold in secondary zones + } + }, + ]] + + -- ADD YOUR RED SQUADRONS HERE + { + templateName = "Sukhumi CAP", -- Change to your RED template name + displayName = "Sukhumi CAP", -- Change to your preferred name + airbaseName = "Sukhumi-Babushara", -- Change to your RED airbase + aircraft = 12, -- Adjust aircraft count + skill = AI.Skill.ACE, -- AVERAGE, GOOD, HIGH, EXCELLENT, ACE + altitude = 20000, -- Patrol altitude (feet) + speed = 350, -- Patrol speed (knots) + patrolTime = 25, -- Time on station (minutes) + type = "FIGHTER", + + -- Zone-based Areas of Responsibility (optional - leave nil for global response) + primaryZone = "RED_BORDER", -- Main responsibility area (zone name from mission editor) + secondaryZone = nil, -- Secondary coverage area (zone name) + tertiaryZone = nil, -- Emergency/fallback zone (zone name) + + -- Zone behavior settings (optional - uses defaults if not specified) + zoneConfig = { + primaryResponse = 1.0, -- Intercept ratio multiplier in primary zone + secondaryResponse = 0.6, -- Intercept ratio multiplier in secondary zone + tertiaryResponse = 1.4, -- Intercept ratio multiplier in tertiary zone + maxRange = 200, -- Maximum engagement range from airbase (nm) + enableFallback = false, -- Auto-switch to tertiary when base threatened + priorityThreshold = 4, -- Min aircraft count for "major threat" + ignoreLowPriority = false, -- Ignore threats below threshold in secondary zones + } + }, + + { + templateName = "Gudauta CAP-MiG-21", -- Change to your RED template name + displayName = "Gudauta CAP-MiG-21", -- Change to your preferred name + airbaseName = "Gudauta", -- Change to your RED airbase + aircraft = 12, -- Adjust aircraft count + skill = AI.Skill.ACE, -- AVERAGE, GOOD, HIGH, EXCELLENT + altitude = 20000, -- Patrol altitude (feet) + speed = 350, -- Patrol speed (knots) + patrolTime = 25, -- Time on station (minutes) + type = "FIGHTER", + + -- Zone-based Areas of Responsibility (optional - leave nil for global response) + primaryZone = "GUDAUTA_BORDER", -- Main responsibility area (zone name from mission editor) + secondaryZone = nil, -- Secondary coverage area (zone name) + tertiaryZone = nil, -- Emergency/fallback zone (zone name) + + -- Zone behavior settings (optional - uses defaults if not specified) + zoneConfig = { + primaryResponse = 1.0, -- Intercept ratio multiplier in primary zone + secondaryResponse = 0.6, -- Intercept ratio multiplier in secondary zone + tertiaryResponse = 1.4, -- Intercept ratio multiplier in tertiary zone + maxRange = 200, -- Maximum engagement range from airbase (nm) + enableFallback = false, -- Auto-switch to tertiary when base threatened + priorityThreshold = 4, -- Min aircraft count for "major threat" + ignoreLowPriority = false, -- Ignore threats below threshold in secondary zones + } + }, + + { + templateName = "Gudauta CAP-MiG-23", -- Change to your RED template name + displayName = "Gudauta CAP-MiG-23", -- Change to your preferred name + airbaseName = "Gudauta", -- Change to your RED airbase + aircraft = 14, -- Adjust aircraft count + skill = AI.Skill.ACE, -- AVERAGE, GOOD, HIGH, EXCELLENT + altitude = 20000, -- Patrol altitude (feet) + speed = 350, -- Patrol speed (knots) + patrolTime = 25, -- Time on station (minutes) + type = "FIGHTER", + + -- Zone-based Areas of Responsibility (optional - leave nil for global response) + primaryZone = "GUDAUTA_BORDER", -- Main responsibility area (zone name from mission editor) + secondaryZone = "RED_BORDER", -- Secondary coverage area (zone name) + tertiaryZone = nil, -- Emergency/fallback zone (zone name) + + -- Zone behavior settings (optional - uses defaults if not specified) + zoneConfig = { + primaryResponse = 1.0, -- Intercept ratio multiplier in primary zone + secondaryResponse = 0.6, -- Intercept ratio multiplier in secondary zone + tertiaryResponse = 1.4, -- Intercept ratio multiplier in tertiary zone + maxRange = 200, -- Maximum engagement range from airbase (nm) + enableFallback = false, -- Auto-switch to tertiary when base threatened + priorityThreshold = 4, -- Min aircraft count for "major threat" + ignoreLowPriority = false, -- Ignore threats below threshold in secondary zones + } + }, +} + +-- ═══════════════════════════════════════════════════════════════════════════ +-- BLUE COALITION SQUADRONS +-- ═══════════════════════════════════════════════════════════════════════════ + +BLUE_SQUADRON_CONFIG = { + --[[ EXAMPLE BLUE SQUADRON - CUSTOMIZE FOR YOUR MISSION + { + templateName = "BLUE_CAP_Nellis_F16", -- Template name from mission editor + displayName = "Nellis F-16C CAP", -- Human-readable name for logs + airbaseName = "Nellis AFB", -- Exact airbase name from DCS + aircraft = 14, -- Maximum aircraft in squadron + skill = AI.Skill.EXCELLENT, -- AI skill level + altitude = 22000, -- Patrol altitude (feet) + speed = 380, -- Patrol speed (knots) + patrolTime = 28, -- Time on station (minutes) + type = "FIGHTER" -- Aircraft type + }, + ]] + + -- ADD YOUR BLUE SQUADRONS HERE + + { + templateName = "Kutaisi CAP", -- Change to your BLUE template name + displayName = "Kutaisi CAP", -- Change to your preferred name + airbaseName = "Kutaisi", -- Change to your BLUE airbase + aircraft = 18, -- Adjust aircraft count + skill = AI.Skill.EXCELLENT, -- AVERAGE, GOOD, HIGH, EXCELLENT + altitude = 18000, -- Patrol altitude (feet) + speed = 320, -- Patrol speed (knots) + patrolTime = 22, -- Time on station (minutes) + type = "FIGHTER", + + -- Zone-based Areas of Responsibility (optional - leave nil for global response) + primaryZone = "BLUE_BORDER", -- Main responsibility area (zone name from mission editor) + secondaryZone = nil, -- Secondary coverage area (zone name) + tertiaryZone = nil, -- Emergency/fallback zone (zone name) + + -- Zone behavior settings (optional - uses defaults if not specified) + zoneConfig = { + primaryResponse = 1.0, -- Intercept ratio multiplier in primary zone + secondaryResponse = 0.6, -- Intercept ratio multiplier in secondary zone + tertiaryResponse = 1.4, -- Intercept ratio multiplier in tertiary zone + maxRange = 200, -- Maximum engagement range from airbase (nm) + enableFallback = true, -- Auto-switch to tertiary when base threatened + priorityThreshold = 4, -- Min aircraft count for "major threat" + ignoreLowPriority = false, -- Ignore threats below threshold in secondary zones + } + }, + + { + templateName = "Batumi CAP", -- Change to your BLUE template name + displayName = "Batumi CAP", -- Change to your preferred name + airbaseName = "Batumi", -- Change to your BLUE airbase + aircraft = 18, -- Adjust aircraft count + skill = AI.Skill.EXCELLENT, -- AVERAGE, GOOD, HIGH, EXCELLENT + altitude = 18000, -- Patrol altitude (feet) + speed = 320, -- Patrol speed (knots) + patrolTime = 22, -- Time on station (minutes) + type = "FIGHTER", + + -- Zone-based Areas of Responsibility (optional - leave nil for global response) + primaryZone = "BATUMI_BORDER", -- Main responsibility area (zone name from mission editor) + secondaryZone = "BLUE_BORDER", -- Secondary coverage area (zone name) + tertiaryZone = "BATUMI_BORDER", -- Emergency/fallback zone (zone name) + + -- Zone behavior settings (optional - uses defaults if not specified) + zoneConfig = { + primaryResponse = 1.0, -- Intercept ratio multiplier in primary zone + secondaryResponse = 0.6, -- Intercept ratio multiplier in secondary zone + tertiaryResponse = 1.4, -- Intercept ratio multiplier in tertiary zone + maxRange = 200, -- Maximum engagement range from airbase (nm) + enableFallback = true, -- Auto-switch to tertiary when base threatened + priorityThreshold = 4, -- Min aircraft count for "major threat" + ignoreLowPriority = false, -- Ignore threats below threshold in secondary zones + } + }, +} + + diff --git a/Moose_TADC/Moose_TADC_Load2nd.lua b/Moose_TADC/Moose_TADC_Load2nd.lua new file mode 100644 index 0000000..3507b2c --- /dev/null +++ b/Moose_TADC/Moose_TADC_Load2nd.lua @@ -0,0 +1,1660 @@ +--[[ +═══════════════════════════════════════════════════════════════════════════════ + UNIVERSAL TADC + Dual-Coalition Tactical Air Defense Controller + Advanced Zone-Based System +═══════════════════════════════════════════════════════════════════════════════ + +DESCRIPTION: +This script provides a sophisticated automated air defense system for BOTH RED and +BLUE coalitions operating independently. Features advanced zone-based area of +responsibility (AOR) management, allowing squadrons to respond differently based +on threat location and priority levels. Perfect for complex scenarios requiring +realistic air defense behavior and tactical depth. + +CORE FEATURES: +• Dual-coalition support with completely independent operation +• Advanced zone-based area of responsibility system (Primary/Secondary/Tertiary) +• Automatic threat detection with intelligent interceptor allocation +• Multi-squadron management with individual cooldowns and aircraft tracking +• Dynamic cargo aircraft replenishment system +• Configurable intercept ratios with zone-specific response modifiers +• Smart interceptor routing, engagement, and RTB (Return to Base) behavior +• Real-time airbase status monitoring (operational/captured/destroyed) +• Comprehensive configuration validation and error reporting +• Asymmetric warfare support with coalition-specific capabilities +• Emergency cleanup systems and safety nets for mission stability + +ADVANCED ZONE SYSTEM: +Each squadron can be configured with up to three zone types: +• PRIMARY ZONE: Main area of responsibility (full response ratio) +• SECONDARY ZONE: Support area (reduced response, optional low-priority filtering) +• TERTIARY ZONE: Emergency/fallback area (enhanced response when base threatened) +• Squadrons will respond based on threat location relative to their zones +• Zone-specific response modifiers can be configured for each squadron +• Zones may overlap between squadrons for layered defense. + +ADVANCED ZONE SETUP: +• Create zones in the mission editor (MOOSE polygons, circles, etc.) +• Assign zone names to squadrons in the configuration (exact match required) +• Leave zones as nil for global threat response (no zone restrictions) +• Each zone is defined by placing a helicopter group with waypoints outlining the area +• The script will create polygon zones from the helicopter waypoints automatically + +Zone response behaviors include: +• Distance-based engagement limits (max range from airbase) +• Priority thresholds for threat classification (major vs minor threats) +• Fallback conditions (auto-switch to tertiary when squadron weakened) +• Response ratio multipliers per zone type +• Low-priority threat filtering in secondary zones + +REPLENISHMENT SYSTEM: +• Automated cargo aircraft detection system that monitors for transport aircraft + landings to replenish squadron aircraft counts (fixed wing only): +• Detects cargo aircraft by name patterns (CARGO, TRANSPORT, C130, C-130, AN26, AN-26) +• Monitors landing status based on velocity and proximity to friendly airbases +• Replenishes squadron aircraft up to maximum capacity per airbase +• Prevents duplicate processing of the same cargo delivery +• Coalition-specific replenishment amounts configurable independently +• Supports sustained operations over extended mission duration + +*** This system does not spawn or manage cargo aircraft - it only detects when +your existing cargo aircraft complete deliveries. Create and route your own +transport missions to maintain squadron strength. *** + +INTERCEPT RATIO SYSTEM: +Sophisticated threat response calculation with zone-based modifiers: +• Base intercept ratio (e.g., 0.8 = 8 interceptors per 10 threats) +• Zone-specific multipliers (primary: 1.0, secondary: 0.6, tertiary: 1.4) +• Threat size considerations (larger formations get proportional response) +• Squadron selection based on zone priority and proximity +• Aircraft availability and cooldown status factored into decisions + +SETUP INSTRUCTIONS: +1. Load MOOSE framework in mission before this script +2. Configure Squadrons: Create fighter aircraft GROUP templates for both coalitions in mission editor +3. Configure RED squadrons in RED_SQUADRON_CONFIG section +4. Configure BLUE squadrons in BLUE_SQUADRON_CONFIG section +5. Optionally create zones in mission editor for area-of-responsibility using helicopter groups with waypoints. +6. Set coalition behavior parameters in TADC_SETTINGS +7. Configure cargo patterns in ADVANCED_SETTINGS if using replenishment +8. Add this script as "DO SCRIPT" trigger at mission start (after MOOSE loaded) +9. Create and manage cargo aircraft missions for replenishment (optional) + +CONFIGURATION VALIDATION: +Built-in validation system checks for: +• Template existence and proper naming +• Airbase name accuracy and coalition control +• Zone existence in mission editor +• Parameter ranges and logical consistency +• Coalition enablement and squadron availability +• Prevents common configuration errors before mission starts + +TACTICAL SCENARIOS SUPPORTED: +• Balanced air warfare with equal capabilities and symmetric response +• Asymmetric scenarios with different coalition strengths and capabilities +• Layered air defense with overlapping squadron zones +• Border/perimeter defense with primary and fallback positions +• Training missions for AI vs AI air combat observation +• Dynamic frontline battles with shifting territorial control +• Long-duration missions with cargo resupply operations +• Emergency response scenarios with threat priority management + +LOGGING AND MONITORING: +• Real-time threat detection and interceptor launch notifications +• Squadron status reports including aircraft counts and cooldown timers +• Airbase operational status with capture/destruction detection +• Cargo delivery tracking and replenishment confirmations +• Zone-based engagement decisions with detailed reasoning +• Configuration validation results and error reporting +• Performance monitoring with emergency cleanup notifications + +REQUIREMENTS: +• MOOSE framework (https://github.com/FlightControl-Master/MOOSE) +• Fighter aircraft GROUP templates (not UNIT templates) for each coalition +• Airbases must exist in mission and be under correct coalition control +• Zone objects in mission editor (if using zone-based features) +• Proper template naming matching squadron configuration + +AUTHOR: +• Based off MOOSE framework by FlightControl-Master +• Developed and customized by Mission Designer "F99th-TracerFacer" + +VERSION: 1.0 +═══════════════════════════════════════════════════════════════════════════════ +]] + +--[[ +═══════════════════════════════════════════════════════════════════════════════ + MAIN SETTINGS +═══════════════════════════════════════════════════════════════════════════════ +]] + +-- Core TADC behavior settings - applies to BOTH coalitions unless overridden +local TADC_SETTINGS = { + -- Enable/Disable coalitions + enableRed = true, -- Set to false to disable RED TADC + enableBlue = true, -- Set to false to disable BLUE TADC + + -- Timing settings (applies to both coalitions) + checkInterval = 30, -- How often to scan for threats (seconds) + monitorInterval = 30, -- How often to check interceptor status (seconds) + statusReportInterval = 120, -- How often to report airbase status (seconds) + squadronSummaryInterval = 600, -- How often to broadcast squadron summary (seconds) + cargoCheckInterval = 15, -- How often to check for cargo deliveries (seconds) + + -- RED Coalition Settings + red = { + maxActiveCAP = 24, -- Maximum RED fighters airborne at once + squadronCooldown = 300, -- RED cooldown after squadron launch (seconds) + interceptRatio = 0.8, -- RED interceptors per threat aircraft + cargoReplenishmentAmount = 4, -- RED aircraft added per cargo delivery + emergencyCleanupTime = 7200, -- RED force cleanup time (seconds) + rtbFlightBuffer = 300, -- RED extra landing time before cleanup (seconds) + }, + + -- BLUE Coalition Settings + blue = { + maxActiveCAP = 24, -- Maximum BLUE fighters airborne at once + squadronCooldown = 300, -- BLUE cooldown after squadron launch (seconds) + interceptRatio = 0.8, -- BLUE interceptors per threat aircraft + cargoReplenishmentAmount = 4, -- BLUE aircraft added per cargo delivery + emergencyCleanupTime = 7200, -- BLUE force cleanup time (seconds) + rtbFlightBuffer = 300, -- BLUE extra landing time before cleanup (seconds) + }, +} + +-- Load squadron configs from external file +-- RED_SQUADRON_CONFIG and BLUE_SQUADRON_CONFIG are now global, loaded by the trigger + +--[[ +INTERCEPT RATIO CHART - How many interceptors launch per threat aircraft: + +Threat Size: 1 2 4 8 12 16 (aircraft) +==================================================================== +interceptRatio 0.2: 1 1 1 2 3 4 (conservative) +interceptRatio 0.5: 1 1 2 4 6 8 (light response) +interceptRatio 0.8: 1 2 4 7 10 13 (balanced) <- DEFAULT +interceptRatio 1.0: 1 2 4 8 12 16 (1:1 parity) +interceptRatio 1.2: 2 3 5 10 15 20 (slight advantage) +interceptRatio 1.4: 2 3 6 12 17 23 (good advantage) +interceptRatio 1.6: 2 4 7 13 20 26 (strong response) +interceptRatio 1.8: 2 4 8 15 22 29 (overwhelming) +interceptRatio 2.0: 2 4 8 16 24 32 (overkill) + +TACTICAL EFFECTS: +• 0.2-0.5: Minimal response, may be overwhelmed by large formations +• 0.8-1.0: Realistic parity, creates balanced dogfights +• 1.2-1.4: Coalition advantage, challenging for enemy +• 1.6-1.8: Strong defense, difficult penetration missions +• 1.9-2.0: Nearly impenetrable, may exhaust squadrons quickly + +SQUADRON IMPACT: +• Low ratios (0.2-0.8): Squadrons last longer, sustained defense +• High ratios (1.6-2.0): Rapid squadron depletion, coverage gaps +• Sweet spot (1.0-1.4): Balanced response with good coverage duration + +ASYMMETRIC SCENARIOS: +• Set RED ratio 1.2, BLUE ratio 0.8 = RED advantage +• Set RED ratio 0.6, BLUE ratio 1.4 = BLUE advantage +• Different maxActiveCAP values create capacity imbalances +]] + + +--[[ +═══════════════════════════════════════════════════════════════════════════════ + ADVANCED SETTINGS +═══════════════════════════════════════════════════════════════════════════════ + +These settings control more detailed behavior. Most users won't need to change these. +]] + +local ADVANCED_SETTINGS = { + -- Cargo aircraft detection patterns (aircraft with these names will replenish squadrons (Currently only fixed wing aircraft supported)) + cargoPatterns = {"CARGO", "TRANSPORT", "C130", "C-130", "AN26", "AN-26"}, + + -- Distance from airbase to consider cargo "landed" (meters) + cargoLandingDistance = 3000, + + -- Velocity below which aircraft is considered "landed" (km/h) + cargoLandedVelocity = 5, + + -- RTB settings + rtbAltitude = 6000, -- Return to base altitude (feet) + rtbSpeed = 430, -- Return to base speed (knots) + + -- Logging settings + enableDetailedLogging = true, -- Set to false to reduce log spam + logPrefix = "[Universal TADC]", -- Prefix for all log messages +} + +--[[ +═══════════════════════════════════════════════════════════════════════════════ + SYSTEM CODE + (DO NOT MODIFY BELOW THIS LINE) +═══════════════════════════════════════════════════════════════════════════════ +]] + +-- Internal tracking variables - separate for each coalition +local activeInterceptors = { + red = {}, + blue = {} +} +local lastLaunchTime = { + red = {}, + blue = {} +} +local assignedThreats = { + red = {}, + blue = {} +} +local squadronCooldowns = { + red = {}, + blue = {} +} +local squadronAircraftCounts = { + red = {}, + blue = {} +} + +-- Performance optimization: Cache SET_GROUP objects to avoid repeated creation +local cachedSets = { + redCargo = nil, + blueCargo = nil, + redAircraft = nil, + blueAircraft = nil +} + +-- Initialize squadron aircraft counts for both coalitions +for _, squadron in pairs(RED_SQUADRON_CONFIG) do + if squadron.aircraft and squadron.templateName then + squadronAircraftCounts.red[squadron.templateName] = squadron.aircraft + end +end + +for _, squadron in pairs(BLUE_SQUADRON_CONFIG) do + if squadron.aircraft and squadron.templateName then + squadronAircraftCounts.blue[squadron.templateName] = squadron.aircraft + end +end + +-- Logging function +local function log(message, detailed) + if not detailed or ADVANCED_SETTINGS.enableDetailedLogging then + env.info(ADVANCED_SETTINGS.logPrefix .. " " .. message) + end +end + +-- Squadron resource summary generator + +local function getSquadronResourceSummary(coalitionSide) + local function getStatus(remaining, max) + local percent = (remaining / max) * 100 + if percent <= 10 then return "[CRITICAL]" end + if percent <= 25 then return "[LOW]" end + return "OK" + end + + local lines = {} + table.insert(lines, "-=[ Tactical Air Defense Controller ]=-\n") + table.insert(lines, "Squadron Resource Summary:\n") + table.insert(lines, "| Squadron | Aircraft Remaining | Status |") + table.insert(lines, "|--------------|--------------------|-------------|") + + if coalitionSide == coalition.side.RED then + 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) + 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) + 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%") + return table.concat(lines, "\n") +end + +-- Broadcast squadron summary to all players +local function broadcastSquadronSummary() + if TADC_SETTINGS.enableRed then + local summaryRed = getSquadronResourceSummary(coalition.side.RED) + MESSAGE:New(summaryRed, 20):ToCoalition(coalition.side.RED) + end + if TADC_SETTINGS.enableBlue then + local summaryBlue = getSquadronResourceSummary(coalition.side.BLUE) + MESSAGE:New(summaryBlue, 20):ToCoalition(coalition.side.BLUE) + end +end + +-- Coalition-specific settings helper +local function getCoalitionSettings(coalitionSide) + if coalitionSide == coalition.side.RED then + return TADC_SETTINGS.red, "RED" + elseif coalitionSide == coalition.side.BLUE then + return TADC_SETTINGS.blue, "BLUE" + else + return nil, "UNKNOWN" + end +end + +-- Get squadron config for coalition +local function getSquadronConfig(coalitionSide) + if coalitionSide == coalition.side.RED then + return RED_SQUADRON_CONFIG + elseif coalitionSide == coalition.side.BLUE then + return BLUE_SQUADRON_CONFIG + else + return {} + end +end + +-- Check if coordinate is within a zone +local function isInZone(coordinate, zoneName) + if not zoneName or zoneName == "" then + return false + end + + -- Try to find the zone + local zone = ZONE:FindByName(zoneName) + if zone then + return zone:IsCoordinateInZone(coordinate) + else + -- Try to create polygon zone from helicopter group waypoints if not found + local group = GROUP:FindByName(zoneName) + if group then + -- Create polygon zone using the group's waypoints as vertices + zone = ZONE_POLYGON:NewFromGroupName(zoneName, zoneName) + if zone then + log("Created polygon zone '" .. zoneName .. "' from helicopter waypoints") + return zone:IsCoordinateInZone(coordinate) + else + log("Warning: Could not create polygon zone from group '" .. zoneName .. "' - check waypoints") + end + else + log("Warning: No group named '" .. zoneName .. "' found for zone creation") + end + + log("Warning: Zone '" .. zoneName .. "' not found in mission and could not create from helicopter group", true) + return false + end +end + +-- Get default zone configuration +local function getDefaultZoneConfig() + return { + primaryResponse = 1.0, + secondaryResponse = 0.6, + tertiaryResponse = 1.4, + maxRange = 200, + enableFallback = false, + priorityThreshold = 4, + ignoreLowPriority = false, + } +end + +-- Check if squadron should respond to fallback conditions +local function checkFallbackConditions(squadron, coalitionSide) + local coalitionKey = (coalitionSide == coalition.side.RED) and "red" or "blue" + + -- Check if airbase is under attack (simplified - check if base has low aircraft) + local currentAircraft = squadronAircraftCounts[coalitionKey][squadron.templateName] or 0 + local maxAircraft = squadron.aircraft + local aircraftRatio = currentAircraft / maxAircraft + + -- Trigger fallback if squadron is below 50% strength or base is threatened + if aircraftRatio < 0.5 then + return true + end + + -- Could add more complex conditions here (base under attack, etc.) + return false +end + +-- Get threat zone priority and response ratio for squadron +local function getThreatZonePriority(threatCoord, squadron, coalitionSide) + local zoneConfig = squadron.zoneConfig or getDefaultZoneConfig() + + -- Check distance from airbase first + local airbase = AIRBASE:FindByName(squadron.airbaseName) + if airbase then + local airbaseCoord = airbase:GetCoordinate() + local distance = airbaseCoord:Get2DDistance(threatCoord) / 1852 -- Convert meters to nautical miles + + if distance > zoneConfig.maxRange then + return "none", 0, "out of range (" .. math.floor(distance) .. "nm > " .. zoneConfig.maxRange .. "nm)" + end + end + + -- Check tertiary zone first (highest priority if fallback enabled) + if squadron.tertiaryZone and zoneConfig.enableFallback then + if checkFallbackConditions(squadron, coalitionSide) then + if isInZone(threatCoord, squadron.tertiaryZone) then + return "tertiary", zoneConfig.tertiaryResponse, "fallback zone (enhanced response)" + end + end + end + + -- Check primary zone + if squadron.primaryZone and isInZone(threatCoord, squadron.primaryZone) then + return "primary", zoneConfig.primaryResponse, "primary AOR" + end + + -- Check secondary zone + if squadron.secondaryZone and isInZone(threatCoord, squadron.secondaryZone) then + return "secondary", zoneConfig.secondaryResponse, "secondary AOR" + end + + -- Check tertiary zone (normal priority) + if squadron.tertiaryZone and isInZone(threatCoord, squadron.tertiaryZone) then + return "tertiary", zoneConfig.tertiaryResponse, "tertiary zone" + end + + -- If no zones are defined, use global response + if not squadron.primaryZone and not squadron.secondaryZone and not squadron.tertiaryZone then + return "global", 1.0, "global response (no zones defined)" + end + + -- Outside all defined zones + return "none", 0, "outside defined zones" +end + +-- Startup validation +local function validateConfiguration() + local errors = {} + + -- Check coalition enablement + if not TADC_SETTINGS.enableRed and not TADC_SETTINGS.enableBlue then + table.insert(errors, "Both coalitions disabled - enable at least one in TADC_SETTINGS") + end + + -- Validate RED squadrons if enabled + if TADC_SETTINGS.enableRed then + if #RED_SQUADRON_CONFIG == 0 then + table.insert(errors, "No RED squadrons configured but RED TADC is enabled") + else + for i, squadron in pairs(RED_SQUADRON_CONFIG) do + local prefix = "RED Squadron " .. i .. ": " + + if not squadron.templateName or squadron.templateName == "" or + squadron.templateName == "RED_CAP_SQUADRON_1" or + squadron.templateName == "RED_CAP_SQUADRON_2" then + table.insert(errors, prefix .. "templateName not configured or using default example") + end + + if not squadron.displayName or squadron.displayName == "" then + table.insert(errors, prefix .. "displayName not configured") + end + + if not squadron.airbaseName or squadron.airbaseName == "" or + squadron.airbaseName:find("YOUR_RED_AIRBASE") then + table.insert(errors, prefix .. "airbaseName not configured or using default example") + end + + if not squadron.aircraft or squadron.aircraft <= 0 then + table.insert(errors, prefix .. "aircraft count not configured or invalid") + end + + -- Validate zone configuration if zones are specified + if squadron.primaryZone or squadron.secondaryZone or squadron.tertiaryZone then + if squadron.zoneConfig then + local zc = squadron.zoneConfig + if zc.primaryResponse and (zc.primaryResponse < 0 or zc.primaryResponse > 5) then + table.insert(errors, prefix .. "primaryResponse ratio out of range (0-5)") + end + if zc.secondaryResponse and (zc.secondaryResponse < 0 or zc.secondaryResponse > 5) then + table.insert(errors, prefix .. "secondaryResponse ratio out of range (0-5)") + end + if zc.tertiaryResponse and (zc.tertiaryResponse < 0 or zc.tertiaryResponse > 5) then + table.insert(errors, prefix .. "tertiaryResponse ratio out of range (0-5)") + end + if zc.maxRange and (zc.maxRange < 10 or zc.maxRange > 1000) then + table.insert(errors, prefix .. "maxRange out of range (10-1000 nm)") + end + end + + -- Check if specified zones exist in mission + local zones = {} + if squadron.primaryZone then table.insert(zones, squadron.primaryZone) end + if squadron.secondaryZone then table.insert(zones, squadron.secondaryZone) end + if squadron.tertiaryZone then table.insert(zones, squadron.tertiaryZone) end + + for _, zoneName in ipairs(zones) do + local zoneObj = ZONE:FindByName(zoneName) + if not zoneObj then + -- Check if there's a helicopter unit/group with this name for zone creation + local unit = UNIT:FindByName(zoneName) + local group = GROUP:FindByName(zoneName) + if not unit and not group then + table.insert(errors, prefix .. "zone '" .. zoneName .. "' not found in mission (no zone or helicopter unit named '" .. zoneName .. "')") + end + end + end + end + end + end + end + + -- Validate BLUE squadrons if enabled + if TADC_SETTINGS.enableBlue then + if #BLUE_SQUADRON_CONFIG == 0 then + table.insert(errors, "No BLUE squadrons configured but BLUE TADC is enabled") + else + for i, squadron in pairs(BLUE_SQUADRON_CONFIG) do + local prefix = "BLUE Squadron " .. i .. ": " + + if not squadron.templateName or squadron.templateName == "" or + squadron.templateName == "BLUE_CAP_SQUADRON_1" or + squadron.templateName == "BLUE_CAP_SQUADRON_2" then + table.insert(errors, prefix .. "templateName not configured or using default example") + end + + if not squadron.displayName or squadron.displayName == "" then + table.insert(errors, prefix .. "displayName not configured") + end + + if not squadron.airbaseName or squadron.airbaseName == "" or + squadron.airbaseName:find("YOUR_BLUE_AIRBASE") then + table.insert(errors, prefix .. "airbaseName not configured or using default example") + end + + if not squadron.aircraft or squadron.aircraft <= 0 then + table.insert(errors, prefix .. "aircraft count not configured or invalid") + end + + -- Validate zone configuration if zones are specified + if squadron.primaryZone or squadron.secondaryZone or squadron.tertiaryZone then + if squadron.zoneConfig then + local zc = squadron.zoneConfig + if zc.primaryResponse and (zc.primaryResponse < 0 or zc.primaryResponse > 5) then + table.insert(errors, prefix .. "primaryResponse ratio out of range (0-5)") + end + if zc.secondaryResponse and (zc.secondaryResponse < 0 or zc.secondaryResponse > 5) then + table.insert(errors, prefix .. "secondaryResponse ratio out of range (0-5)") + end + if zc.tertiaryResponse and (zc.tertiaryResponse < 0 or zc.tertiaryResponse > 5) then + table.insert(errors, prefix .. "tertiaryResponse ratio out of range (0-5)") + end + if zc.maxRange and (zc.maxRange < 10 or zc.maxRange > 1000) then + table.insert(errors, prefix .. "maxRange out of range (10-1000 nm)") + end + end + + -- Check if specified zones exist in mission + local zones = {} + if squadron.primaryZone then table.insert(zones, squadron.primaryZone) end + if squadron.secondaryZone then table.insert(zones, squadron.secondaryZone) end + if squadron.tertiaryZone then table.insert(zones, squadron.tertiaryZone) end + + for _, zoneName in ipairs(zones) do + local zoneObj = ZONE:FindByName(zoneName) + if not zoneObj then + -- Check if there's a helicopter unit/group with this name for zone creation + local unit = UNIT:FindByName(zoneName) + local group = GROUP:FindByName(zoneName) + if not unit and not group then + table.insert(errors, prefix .. "zone '" .. zoneName .. "' not found in mission (no zone or helicopter unit named '" .. zoneName .. "')") + end + end + end + end + end + end + end + + -- Report errors + if #errors > 0 then + log("CONFIGURATION ERRORS DETECTED:") + MESSAGE:New("CONFIGURATION ERRORS DETECTED:", 30):ToAll() + for _, error in pairs(errors) do + log(" ✗ " .. error) + MESSAGE:New("CONFIG ERROR: " .. error, 30):ToAll() + end + log("Please fix configuration before using Universal TADC!") + MESSAGE:New("Please fix configuration before using Universal TADC!", 30):ToAll() + return false + else + log("Configuration validation passed ✓") + MESSAGE:New("Universal TADC configuration passed ✓", 10):ToAll() + return true + end +end + +-- Monitor cargo aircraft landings for squadron replenishment +local function monitorCargoReplenishment() + -- Process RED cargo aircraft + if TADC_SETTINGS.enableRed then + -- Use cached set for performance, create if needed + if not cachedSets.redCargo then + cachedSets.redCargo = SET_GROUP:New():FilterCoalitions("red"):FilterCategoryAirplane():FilterStart() + end + local redCargo = cachedSets.redCargo + + redCargo:ForEach(function(cargoGroup) + if cargoGroup and cargoGroup:IsAlive() then + local cargoName = cargoGroup:GetName():upper() + local isCargoAircraft = false + + -- Check if aircraft name matches cargo patterns + for _, pattern in pairs(ADVANCED_SETTINGS.cargoPatterns) do + if string.find(cargoName, pattern) then + isCargoAircraft = true + break + end + end + + if isCargoAircraft then + local cargoCoord = cargoGroup:GetCoordinate() + local cargoVelocity = cargoGroup:GetVelocityKMH() + + -- Consider aircraft "landed" if velocity is very low + if cargoVelocity < ADVANCED_SETTINGS.cargoLandedVelocity then + -- Check which RED airbase it's near + for _, squadron in pairs(RED_SQUADRON_CONFIG) do + local airbase = AIRBASE:FindByName(squadron.airbaseName) + if airbase and airbase:GetCoalition() == coalition.side.RED then + local airbaseCoord = airbase:GetCoordinate() + local distance = cargoCoord:Get2DDistance(airbaseCoord) + + -- If within configured distance of airbase, consider it a delivery + if distance < ADVANCED_SETTINGS.cargoLandingDistance then + -- Initialize processed deliveries table + if not _G.processedDeliveries then + _G.processedDeliveries = {} + end + + -- Create unique delivery key including timestamp to prevent race conditions + local deliveryKey = cargoName .. "_RED_" .. squadron.airbaseName .. "_" .. cargoGroup:GetID() + + if not _G.processedDeliveries[deliveryKey] then + -- Mark delivery as processed immediately to prevent race conditions + _G.processedDeliveries[deliveryKey] = timer.getTime() + -- Process replenishment + local currentCount = squadronAircraftCounts.red[squadron.templateName] or 0 + local maxCount = squadron.aircraft + local newCount = math.min(currentCount + TADC_SETTINGS.red.cargoReplenishmentAmount, maxCount) + local actualAdded = newCount - currentCount + + if actualAdded > 0 then + squadronAircraftCounts.red[squadron.templateName] = newCount + local msg = "RED CARGO DELIVERY: " .. cargoName .. " delivered " .. actualAdded .. + " aircraft to " .. squadron.displayName .. + " (" .. newCount .. "/" .. maxCount .. ")" + log(msg) + MESSAGE:New(msg, 20):ToAll() + else + local msg = "RED CARGO DELIVERY: " .. squadron.displayName .. " already at max capacity" + log(msg, true) + MESSAGE:New(msg, 15):ToAll() + end + end + end + end + end + end + end + end + end) + end + + -- Process BLUE cargo aircraft + if TADC_SETTINGS.enableBlue then + -- Use cached set for performance, create if needed + if not cachedSets.blueCargo then + cachedSets.blueCargo = SET_GROUP:New():FilterCoalitions("blue"):FilterCategoryAirplane():FilterStart() + end + local blueCargo = cachedSets.blueCargo + + blueCargo:ForEach(function(cargoGroup) + if cargoGroup and cargoGroup:IsAlive() then + local cargoName = cargoGroup:GetName():upper() + local isCargoAircraft = false + + -- Check if aircraft name matches cargo patterns + for _, pattern in pairs(ADVANCED_SETTINGS.cargoPatterns) do + if string.find(cargoName, pattern) then + isCargoAircraft = true + break + end + end + + if isCargoAircraft then + local cargoCoord = cargoGroup:GetCoordinate() + local cargoVelocity = cargoGroup:GetVelocityKMH() + + -- Consider aircraft "landed" if velocity is very low + if cargoVelocity < ADVANCED_SETTINGS.cargoLandedVelocity then + -- Check which BLUE airbase it's near + for _, squadron in pairs(BLUE_SQUADRON_CONFIG) do + local airbase = AIRBASE:FindByName(squadron.airbaseName) + if airbase and airbase:GetCoalition() == coalition.side.BLUE then + local airbaseCoord = airbase:GetCoordinate() + local distance = cargoCoord:Get2DDistance(airbaseCoord) + + -- If within configured distance of airbase, consider it a delivery + if distance < ADVANCED_SETTINGS.cargoLandingDistance then + -- Initialize processed deliveries table + if not _G.processedDeliveries then + _G.processedDeliveries = {} + end + + -- Create unique delivery key including timestamp to prevent race conditions + local deliveryKey = cargoName .. "_BLUE_" .. squadron.airbaseName .. "_" .. cargoGroup:GetID() + + if not _G.processedDeliveries[deliveryKey] then + -- Mark delivery as processed immediately to prevent race conditions + _G.processedDeliveries[deliveryKey] = timer.getTime() + -- Process replenishment + local currentCount = squadronAircraftCounts.blue[squadron.templateName] or 0 + local maxCount = squadron.aircraft + local newCount = math.min(currentCount + TADC_SETTINGS.blue.cargoReplenishmentAmount, maxCount) + local actualAdded = newCount - currentCount + + if actualAdded > 0 then + squadronAircraftCounts.blue[squadron.templateName] = newCount + local msg = "BLUE CARGO DELIVERY: " .. cargoName .. " delivered " .. actualAdded .. + " aircraft to " .. squadron.displayName .. + " (" .. newCount .. "/" .. maxCount .. ")" + log(msg) + MESSAGE:New(msg, 20):ToAll() + else + local msg = "BLUE CARGO DELIVERY: " .. squadron.displayName .. " already at max capacity" + log(msg, true) + MESSAGE:New(msg, 15):ToAll() + end + end + end + end + end + end + end + end + end) + end +end + +-- Send interceptor back to base +local function sendInterceptorHome(interceptor, coalitionSide) + if not interceptor or not interceptor:IsAlive() then + return + end + + -- Find nearest friendly airbase + local interceptorCoord = interceptor:GetCoordinate() + if not interceptorCoord then + log("ERROR: Could not get interceptor coordinates for RTB", true) + return + end + local nearestAirbase = nil + local shortestDistance = math.huge + local squadronConfig = getSquadronConfig(coalitionSide) + + -- Check all squadron airbases to find the nearest one that's still friendly + for _, squadron in pairs(squadronConfig) do + local airbase = AIRBASE:FindByName(squadron.airbaseName) + if airbase and airbase:GetCoalition() == coalitionSide and airbase:IsAlive() then + local airbaseCoord = airbase:GetCoordinate() + local distance = interceptorCoord:Get2DDistance(airbaseCoord) + if distance < shortestDistance then + shortestDistance = distance + nearestAirbase = airbase + end + end + end + + if nearestAirbase then + local airbaseCoord = nearestAirbase:GetCoordinate() + local rtbAltitude = ADVANCED_SETTINGS.rtbAltitude * 0.3048 -- Convert feet to meters + local rtbCoord = airbaseCoord:SetAltitude(rtbAltitude) + + -- Clear current tasks and route home + interceptor:ClearTasks() + interceptor:RouteAirTo(rtbCoord, ADVANCED_SETTINGS.rtbSpeed * 0.5144, "BARO") -- Convert knots to m/s + + local _, coalitionName = getCoalitionSettings(coalitionSide) + log("Sending " .. coalitionName .. " " .. interceptor:GetName() .. " back to " .. nearestAirbase:GetName(), true) + + -- Schedule cleanup after they should have landed + local coalitionSettings = getCoalitionSettings(coalitionSide) + local flightTime = math.ceil(shortestDistance / (ADVANCED_SETTINGS.rtbSpeed * 0.5144)) + coalitionSettings.rtbFlightBuffer + + SCHEDULER:New(nil, function() + local coalitionKey = (coalitionSide == coalition.side.RED) and "red" or "blue" + if activeInterceptors[coalitionKey][interceptor:GetName()] then + activeInterceptors[coalitionKey][interceptor:GetName()] = nil + log("Cleaned up " .. coalitionName .. " " .. interceptor:GetName() .. " after RTB", true) + end + end, {}, flightTime) + else + local _, coalitionName = getCoalitionSettings(coalitionSide) + log("No friendly airbase found for " .. coalitionName .. " " .. interceptor:GetName() .. ", will clean up normally") + end +end + +-- Check if airbase is still usable +local function isAirbaseUsable(airbaseName, expectedCoalition) + local airbase = AIRBASE:FindByName(airbaseName) + if not airbase then + return false, "not found" + elseif airbase:GetCoalition() ~= expectedCoalition then + local capturedBy = "Unknown" + if airbase:GetCoalition() == coalition.side.RED then + capturedBy = "Red" + elseif airbase:GetCoalition() == coalition.side.BLUE then + capturedBy = "Blue" + else + capturedBy = "Neutral" + end + return false, "captured by " .. capturedBy + elseif not airbase:IsAlive() then + return false, "destroyed" + else + return true, "operational" + end +end + +-- Count active fighters for coalition +local function countActiveFighters(coalitionSide) + local count = 0 + local coalitionKey = (coalitionSide == coalition.side.RED) and "red" or "blue" + + for _, interceptorData in pairs(activeInterceptors[coalitionKey]) do + if interceptorData and interceptorData.group and interceptorData.group:IsAlive() then + count = count + interceptorData.group:GetSize() + end + end + return count +end + +-- Find best squadron to launch for coalition using zone-based priorities +local function findBestSquadron(threatCoord, threatSize, coalitionSide) + local bestSquadron = nil + local bestPriority = "none" + local bestResponseRatio = 0 + local shortestDistance = math.huge + local currentTime = timer.getTime() + local squadronConfig = getSquadronConfig(coalitionSide) + local coalitionSettings, coalitionName = getCoalitionSettings(coalitionSide) + local coalitionKey = (coalitionSide == coalition.side.RED) and "red" or "blue" + local zonePriorityOrder = {"tertiary", "primary", "secondary", "global"} + + -- First pass: find squadrons that can respond to this threat + local availableSquadrons = {} + + for _, squadron in pairs(squadronConfig) do + -- Check basic availability + local squadronAvailable = true + local unavailableReason = "" + + -- Check cooldown + if squadronCooldowns[coalitionKey][squadron.templateName] then + local cooldownEnd = squadronCooldowns[coalitionKey][squadron.templateName] + if currentTime < cooldownEnd then + local timeLeft = math.ceil((cooldownEnd - currentTime) / 60) + squadronAvailable = false + unavailableReason = "on cooldown for " .. timeLeft .. "m" + else + -- Cooldown expired, remove it + squadronCooldowns[coalitionKey][squadron.templateName] = nil + log(coalitionName .. " Squadron " .. squadron.displayName .. " cooldown expired, available for launch", true) + end + end + + -- Check aircraft availability + if squadronAvailable then + local availableAircraft = squadronAircraftCounts[coalitionKey][squadron.templateName] or 0 + if availableAircraft <= 0 then + squadronAvailable = false + unavailableReason = "no aircraft available (" .. availableAircraft .. "/" .. squadron.aircraft .. ")" + end + end + + -- Check airbase status + if squadronAvailable then + local airbase = AIRBASE:FindByName(squadron.airbaseName) + if not airbase then + squadronAvailable = false + unavailableReason = "airbase not found" + elseif airbase:GetCoalition() ~= coalitionSide then + squadronAvailable = false + unavailableReason = "airbase no longer under " .. coalitionName .. " control" + elseif not airbase:IsAlive() then + squadronAvailable = false + unavailableReason = "airbase destroyed" + end + end + + -- Check template exists (Note: Templates are validated during SPAWN:New() call) + -- Template validation is handled by MOOSE SPAWN class during actual spawning + + if squadronAvailable then + -- Get zone priority and response ratio + local zonePriority, responseRatio, zoneDescription = getThreatZonePriority(threatCoord, squadron, coalitionSide) + + -- Check if threat meets priority threshold for secondary zones + local zoneConfig = squadron.zoneConfig or getDefaultZoneConfig() + if zonePriority == "secondary" and zoneConfig.ignoreLowPriority then + if threatSize < zoneConfig.priorityThreshold then + log(coalitionName .. " " .. squadron.displayName .. " ignoring low-priority threat in secondary zone (" .. + threatSize .. " < " .. zoneConfig.priorityThreshold .. ")", true) + responseRatio = 0 + zonePriority = "none" + end + end + + if responseRatio > 0 then + local airbase = AIRBASE:FindByName(squadron.airbaseName) + local airbaseCoord = airbase:GetCoordinate() + local distance = airbaseCoord:Get2DDistance(threatCoord) + + table.insert(availableSquadrons, { + squadron = squadron, + zonePriority = zonePriority, + responseRatio = responseRatio, + distance = distance, + zoneDescription = zoneDescription + }) + + log(coalitionName .. " " .. squadron.displayName .. " can respond: " .. zoneDescription .. + " (ratio: " .. responseRatio .. ", distance: " .. math.floor(distance/1852) .. "nm)", true) + else + log(coalitionName .. " " .. squadron.displayName .. " will not respond: " .. zoneDescription, true) + end + else + log(coalitionName .. " " .. squadron.displayName .. " unavailable: " .. unavailableReason, true) + end + end + + -- Second pass: select best squadron by priority and distance + if #availableSquadrons > 0 then + -- Sort by zone priority (higher priority first), then by distance (closer first) + table.sort(availableSquadrons, function(a, b) + -- Get priority indices + local aPriorityIndex = 5 + local bPriorityIndex = 5 + for i, priority in ipairs(zonePriorityOrder) do + if a.zonePriority == priority then aPriorityIndex = i end + if b.zonePriority == priority then bPriorityIndex = i end + end + + -- First sort by priority (lower index = higher priority) + if aPriorityIndex ~= bPriorityIndex then + return aPriorityIndex < bPriorityIndex + end + + -- Then sort by distance (closer is better) + return a.distance < b.distance + end) + + local selected = availableSquadrons[1] + log("Selected " .. coalitionName .. " " .. selected.squadron.displayName .. " for response: " .. + selected.zoneDescription .. " (distance: " .. math.floor(selected.distance/1852) .. "nm)") + + return selected.squadron, selected.responseRatio, selected.zoneDescription + end + + log("No " .. coalitionName .. " squadron available for threat at coordinates") + return nil, 0, "no available squadrons" +end + +-- Launch interceptor for coalition +local function launchInterceptor(threatGroup, coalitionSide) + if not threatGroup or not threatGroup:IsAlive() then + return + end + + local threatCoord = threatGroup:GetCoordinate() + local threatName = threatGroup:GetName() + local threatSize = threatGroup:GetSize() + local coalitionSettings, coalitionName = getCoalitionSettings(coalitionSide) + local coalitionKey = (coalitionSide == coalition.side.RED) and "red" or "blue" + + -- Check if threat already has interceptors assigned + if assignedThreats[coalitionKey][threatName] then + local assignedInterceptors = assignedThreats[coalitionKey][threatName] + local aliveCount = 0 + + -- Check if assigned interceptors are still alive + if type(assignedInterceptors) == "table" then + for _, interceptor in pairs(assignedInterceptors) do + if interceptor and interceptor:IsAlive() then + aliveCount = aliveCount + 1 + end + end + else + -- Handle legacy single interceptor assignment + if assignedInterceptors and assignedInterceptors:IsAlive() then + aliveCount = 1 + end + end + + if aliveCount > 0 then + return -- Still being intercepted + else + -- All interceptors are dead, clear the assignment + assignedThreats[coalitionKey][threatName] = nil + end + end + + -- Find best squadron using zone-based priority system first + local squadron, zoneResponseRatio, zoneDescription = findBestSquadron(threatCoord, threatSize, coalitionSide) + + if not squadron then + log("No " .. coalitionName .. " squadron available") + return + end + + -- Calculate how many interceptors to launch using zone-modified ratio + local finalInterceptRatio = coalitionSettings.interceptRatio * zoneResponseRatio + local interceptorsNeeded = math.max(1, math.ceil(threatSize * finalInterceptRatio)) + + -- Check if we have capacity + if countActiveFighters(coalitionSide) + interceptorsNeeded > coalitionSettings.maxActiveCAP then + interceptorsNeeded = coalitionSettings.maxActiveCAP - countActiveFighters(coalitionSide) + if interceptorsNeeded <= 0 then + log(coalitionName .. " max fighters airborne, skipping launch") + return + end + end + if not squadron then + log("No " .. coalitionName .. " squadron available") + return + end + + -- Limit interceptors to available aircraft + local availableAircraft = squadronAircraftCounts[coalitionKey][squadron.templateName] or 0 + interceptorsNeeded = math.min(interceptorsNeeded, availableAircraft) + + if interceptorsNeeded <= 0 then + log(coalitionName .. " Squadron " .. squadron.displayName .. " has no aircraft to launch") + return + end + + -- Launch multiple interceptors to match threat + local spawn = SPAWN:New(squadron.templateName) + if not spawn then + log("ERROR: Failed to create SPAWN object for " .. coalitionName .. " " .. squadron.templateName) + return + end + + local interceptors = {} + + for i = 1, interceptorsNeeded do + local interceptor = spawn:Spawn() + + if interceptor then + table.insert(interceptors, interceptor) + + -- Wait a moment for initialization + SCHEDULER:New(nil, function() + if interceptor and interceptor:IsAlive() then + -- Set aggressive AI + interceptor:OptionROEOpenFire() + interceptor:OptionROTVertical() + + -- Route to threat + local currentThreatCoord = threatGroup:GetCoordinate() + if currentThreatCoord then + local interceptCoord = currentThreatCoord:SetAltitude(squadron.altitude * 0.3048) -- Convert feet to meters + interceptor:RouteAirTo(interceptCoord, squadron.speed * 0.5144, "BARO") -- Convert knots to m/s + + -- Attack the threat + local attackTask = { + id = 'AttackGroup', + params = { + groupId = threatGroup:GetID(), + weaponType = 'Auto', + attackQtyLimit = 0, + priority = 1 + } + } + interceptor:PushTask(attackTask, 1) + end + end + end, {}, 3) + + -- Track the interceptor with squadron info + activeInterceptors[coalitionKey][interceptor:GetName()] = { + group = interceptor, + squadron = squadron.templateName, + displayName = squadron.displayName + } + + -- Emergency cleanup (safety net) + SCHEDULER:New(nil, function() + if activeInterceptors[coalitionKey][interceptor:GetName()] then + log("Emergency cleanup of " .. coalitionName .. " " .. interceptor:GetName() .. " (should have RTB'd)") + activeInterceptors[coalitionKey][interceptor:GetName()] = nil + end + end, {}, coalitionSettings.emergencyCleanupTime) + end + end + + -- Log the launch and track assignment + if #interceptors > 0 then + -- Decrement squadron aircraft count + local currentCount = squadronAircraftCounts[coalitionKey][squadron.templateName] or 0 + squadronAircraftCounts[coalitionKey][squadron.templateName] = math.max(0, currentCount - #interceptors) + local remainingCount = squadronAircraftCounts[coalitionKey][squadron.templateName] + + log("Launched " .. #interceptors .. " x " .. coalitionName .. " " .. squadron.displayName .. " to intercept " .. + threatSize .. " x " .. threatName .. " (" .. zoneDescription .. ", ratio: " .. string.format("%.1f", finalInterceptRatio) .. + ", remaining: " .. remainingCount .. "/" .. squadron.aircraft .. ")") + assignedThreats[coalitionKey][threatName] = interceptors + lastLaunchTime[coalitionKey][threatName] = timer.getTime() + + -- Apply cooldown immediately when squadron launches + local currentTime = timer.getTime() + squadronCooldowns[coalitionKey][squadron.templateName] = currentTime + coalitionSettings.squadronCooldown + local cooldownMinutes = coalitionSettings.squadronCooldown / 60 + log(coalitionName .. " Squadron " .. squadron.displayName .. " LAUNCHED! Applying " .. cooldownMinutes .. " minute cooldown") + end +end + +-- Main threat detection loop for coalition +local function detectThreatsForCoalition(coalitionSide) + local coalitionSettings, coalitionName = getCoalitionSettings(coalitionSide) + local enemyCoalition = (coalitionSide == coalition.side.RED) and "blue" or "red" + local coalitionKey = (coalitionSide == coalition.side.RED) and "red" or "blue" + + log("Scanning for " .. coalitionName .. " threats...", true) + + -- Clean up dead threats from tracking + local currentThreats = {} + + -- Find all enemy aircraft using cached set for performance + local cacheKey = enemyCoalition .. "Aircraft" + if not cachedSets[cacheKey] then + cachedSets[cacheKey] = SET_GROUP:New():FilterCoalitions(enemyCoalition):FilterCategoryAirplane():FilterStart() + end + local enemyAircraft = cachedSets[cacheKey] + local threatCount = 0 + + enemyAircraft:ForEach(function(enemyGroup) + if enemyGroup and enemyGroup:IsAlive() then + threatCount = threatCount + 1 + currentThreats[enemyGroup:GetName()] = true + log("Found " .. coalitionName .. " threat: " .. enemyGroup:GetName() .. " (" .. enemyGroup:GetTypeName() .. ")", true) + + -- Launch interceptor for this threat + launchInterceptor(enemyGroup, coalitionSide) + end + end) + + -- Clean up assignments for threats that no longer exist and send interceptors home + for threatName, assignedInterceptors in pairs(assignedThreats[coalitionKey]) do + if not currentThreats[threatName] then + log("Threat " .. threatName .. " eliminated, sending " .. coalitionName .. " interceptors home...") + + -- Send assigned interceptors back to base + if type(assignedInterceptors) == "table" then + for _, interceptor in pairs(assignedInterceptors) do + if interceptor and interceptor:IsAlive() then + sendInterceptorHome(interceptor, coalitionSide) + end + end + else + -- Handle legacy single interceptor assignment + if assignedInterceptors and assignedInterceptors:IsAlive() then + sendInterceptorHome(assignedInterceptors, coalitionSide) + end + end + + assignedThreats[coalitionKey][threatName] = nil + end + end + + -- Count assigned threats + local assignedCount = 0 + for _ in pairs(assignedThreats[coalitionKey]) do assignedCount = assignedCount + 1 end + + log(coalitionName .. " scan complete: " .. threatCount .. " threats, " .. countActiveFighters(coalitionSide) .. " active fighters, " .. + assignedCount .. " assigned") +end + +-- Main threat detection loop - calls both coalitions +local function detectThreats() + if TADC_SETTINGS.enableRed then + detectThreatsForCoalition(coalition.side.RED) + end + + if TADC_SETTINGS.enableBlue then + detectThreatsForCoalition(coalition.side.BLUE) + end +end + +-- Monitor interceptor groups for cleanup when destroyed +local function monitorInterceptors() + -- Check RED interceptors + if TADC_SETTINGS.enableRed then + for interceptorName, interceptorData in pairs(activeInterceptors.red) do + if interceptorData and interceptorData.group then + if not interceptorData.group:IsAlive() then + -- Interceptor group is destroyed - just clean up tracking + local displayName = interceptorData.displayName + log("RED Interceptor from " .. displayName .. " destroyed: " .. interceptorName, true) + + -- Remove from active tracking + activeInterceptors.red[interceptorName] = nil + end + end + end + end + + -- Check BLUE interceptors + if TADC_SETTINGS.enableBlue then + for interceptorName, interceptorData in pairs(activeInterceptors.blue) do + if interceptorData and interceptorData.group then + if not interceptorData.group:IsAlive() then + -- Interceptor group is destroyed - just clean up tracking + local displayName = interceptorData.displayName + log("BLUE Interceptor from " .. displayName .. " destroyed: " .. interceptorName, true) + + -- Remove from active tracking + activeInterceptors.blue[interceptorName] = nil + end + end + end + end +end + +-- Periodic airbase status check +local function checkAirbaseStatus() + log("=== AIRBASE STATUS REPORT ===") + + local redUsableCount = 0 + local blueUsableCount = 0 + local currentTime = timer.getTime() + + -- Check RED airbases + if TADC_SETTINGS.enableRed then + log("=== RED COALITION STATUS ===") + for _, squadron in pairs(RED_SQUADRON_CONFIG) do + local usable, status = isAirbaseUsable(squadron.airbaseName, coalition.side.RED) + + -- Add aircraft count to status + local aircraftCount = squadronAircraftCounts.red[squadron.templateName] or 0 + local maxAircraft = squadron.aircraft + local aircraftStatus = " Aircraft: " .. aircraftCount .. "/" .. maxAircraft + + -- Add zone information if configured + local zoneStatus = "" + if squadron.primaryZone or squadron.secondaryZone or squadron.tertiaryZone then + local zones = {} + if squadron.primaryZone then table.insert(zones, "P:" .. squadron.primaryZone) end + if squadron.secondaryZone then table.insert(zones, "S:" .. squadron.secondaryZone) end + if squadron.tertiaryZone then table.insert(zones, "T:" .. squadron.tertiaryZone) end + zoneStatus = " Zones: " .. table.concat(zones, " ") + end + + -- Check if squadron is on cooldown + local cooldownStatus = "" + if squadronCooldowns.red[squadron.templateName] then + local cooldownEnd = squadronCooldowns.red[squadron.templateName] + if currentTime < cooldownEnd then + local timeLeft = math.ceil((cooldownEnd - currentTime) / 60) + cooldownStatus = " (COOLDOWN: " .. timeLeft .. "m)" + end + end + + local fullStatus = status .. aircraftStatus .. zoneStatus .. cooldownStatus + + if usable and cooldownStatus == "" and aircraftCount > 0 then + redUsableCount = redUsableCount + 1 + log("✓ " .. squadron.airbaseName .. " - " .. fullStatus) + else + log("✗ " .. squadron.airbaseName .. " - " .. fullStatus) + end + end + log("RED Status: " .. redUsableCount .. "/" .. #RED_SQUADRON_CONFIG .. " airbases operational") + end + + -- Check BLUE airbases + if TADC_SETTINGS.enableBlue then + log("=== BLUE COALITION STATUS ===") + for _, squadron in pairs(BLUE_SQUADRON_CONFIG) do + local usable, status = isAirbaseUsable(squadron.airbaseName, coalition.side.BLUE) + + -- Add aircraft count to status + local aircraftCount = squadronAircraftCounts.blue[squadron.templateName] or 0 + local maxAircraft = squadron.aircraft + local aircraftStatus = " Aircraft: " .. aircraftCount .. "/" .. maxAircraft + + -- Add zone information if configured + local zoneStatus = "" + if squadron.primaryZone or squadron.secondaryZone or squadron.tertiaryZone then + local zones = {} + if squadron.primaryZone then table.insert(zones, "P:" .. squadron.primaryZone) end + if squadron.secondaryZone then table.insert(zones, "S:" .. squadron.secondaryZone) end + if squadron.tertiaryZone then table.insert(zones, "T:" .. squadron.tertiaryZone) end + zoneStatus = " Zones: " .. table.concat(zones, " ") + end + + -- Check if squadron is on cooldown + local cooldownStatus = "" + if squadronCooldowns.blue[squadron.templateName] then + local cooldownEnd = squadronCooldowns.blue[squadron.templateName] + if currentTime < cooldownEnd then + local timeLeft = math.ceil((cooldownEnd - currentTime) / 60) + cooldownStatus = " (COOLDOWN: " .. timeLeft .. "m)" + end + end + + local fullStatus = status .. aircraftStatus .. zoneStatus .. cooldownStatus + + if usable and cooldownStatus == "" and aircraftCount > 0 then + blueUsableCount = blueUsableCount + 1 + log("✓ " .. squadron.airbaseName .. " - " .. fullStatus) + else + log("✗ " .. squadron.airbaseName .. " - " .. fullStatus) + end + end + log("BLUE Status: " .. blueUsableCount .. "/" .. #BLUE_SQUADRON_CONFIG .. " airbases operational") + end +end + +-- Cleanup old delivery records to prevent memory buildup +local function cleanupOldDeliveries() + if _G.processedDeliveries then + local currentTime = timer.getTime() + local cleanupAge = 3600 -- Remove delivery records older than 1 hour + local removedCount = 0 + + for deliveryKey, timestamp in pairs(_G.processedDeliveries) do + if currentTime - timestamp > cleanupAge then + _G.processedDeliveries[deliveryKey] = nil + removedCount = removedCount + 1 + end + end + + if removedCount > 0 then + log("Cleaned up " .. removedCount .. " old cargo delivery records", true) + end + end +end + +-- System initialization +local function initializeSystem() + log("Universal Dual-Coalition TADC starting...") + + -- Create zones from late-activated helicopter units (MOOSE method) + -- This allows using helicopters named "RED_BORDER", "BLUE_BORDER" etc. as zone markers + -- Uses the helicopter's waypoints as polygon vertices (standard MOOSE method) + local function createZoneFromUnit(unitName) + -- Try to find as a group first (this is the standard MOOSE way) + local group = GROUP:FindByName(unitName) + if group then + -- Create polygon zone using the group's waypoints as vertices + local zone = ZONE_POLYGON:NewFromGroupName(unitName, unitName) + if zone then + log("Created polygon zone '" .. unitName .. "' from helicopter waypoints") + return zone + else + log("Warning: Could not create polygon zone from group '" .. unitName .. "' - check waypoints") + end + else + log("Warning: No group named '" .. unitName .. "' found for zone creation") + end + return nil + end + + -- Try to create zones for all configured zone names + local zoneNames = {} + for _, squadron in pairs(RED_SQUADRON_CONFIG) do + if squadron.primaryZone then table.insert(zoneNames, squadron.primaryZone) end + if squadron.secondaryZone then table.insert(zoneNames, squadron.secondaryZone) end + if squadron.tertiaryZone then table.insert(zoneNames, squadron.tertiaryZone) end + end + for _, squadron in pairs(BLUE_SQUADRON_CONFIG) do + if squadron.primaryZone then table.insert(zoneNames, squadron.primaryZone) end + if squadron.secondaryZone then table.insert(zoneNames, squadron.secondaryZone) end + if squadron.tertiaryZone then table.insert(zoneNames, squadron.tertiaryZone) end + end + + -- Create zones from helicopters + for _, zoneName in ipairs(zoneNames) do + if not ZONE:FindByName(zoneName) then + createZoneFromUnit(zoneName) + end + end + + -- Validate configuration + if not validateConfiguration() then + log("System startup aborted due to configuration errors!") + return false + end + + -- Log enabled coalitions + local enabledCoalitions = {} + if TADC_SETTINGS.enableRed then + table.insert(enabledCoalitions, "RED (" .. #RED_SQUADRON_CONFIG .. " squadrons)") + end + if TADC_SETTINGS.enableBlue then + table.insert(enabledCoalitions, "BLUE (" .. #BLUE_SQUADRON_CONFIG .. " squadrons)") + end + log("Enabled coalitions: " .. table.concat(enabledCoalitions, ", ")) + + -- Log initial squadron aircraft counts + if TADC_SETTINGS.enableRed then + for _, squadron in pairs(RED_SQUADRON_CONFIG) do + local count = squadronAircraftCounts.red[squadron.templateName] + log("Initial RED: " .. squadron.displayName .. " has " .. count .. "/" .. squadron.aircraft .. " aircraft") + end + end + + if TADC_SETTINGS.enableBlue then + for _, squadron in pairs(BLUE_SQUADRON_CONFIG) do + local count = squadronAircraftCounts.blue[squadron.templateName] + log("Initial BLUE: " .. squadron.displayName .. " has " .. count .. "/" .. squadron.aircraft .. " aircraft") + end + end + + -- Start schedulers + SCHEDULER:New(nil, detectThreats, {}, 5, TADC_SETTINGS.checkInterval) + SCHEDULER:New(nil, monitorInterceptors, {}, 10, TADC_SETTINGS.monitorInterval) + SCHEDULER:New(nil, checkAirbaseStatus, {}, 30, TADC_SETTINGS.statusReportInterval) + SCHEDULER:New(nil, monitorCargoReplenishment, {}, 15, TADC_SETTINGS.cargoCheckInterval) + SCHEDULER:New(nil, cleanupOldDeliveries, {}, 60, 3600) -- Cleanup old delivery records every hour + + -- Start periodic squadron summary broadcast + SCHEDULER:New(nil, broadcastSquadronSummary, {}, 10, TADC_SETTINGS.squadronSummaryInterval) + + log("Universal Dual-Coalition TADC operational!") + log("RED Replenishment: " .. TADC_SETTINGS.red.cargoReplenishmentAmount .. " aircraft per cargo delivery") + log("BLUE Replenishment: " .. TADC_SETTINGS.blue.cargoReplenishmentAmount .. " aircraft per cargo delivery") + + return true +end + + +initializeSystem() + +-- Add F10 menu command for squadron summary +local menuRoot = MENU_MISSION:New("TADC Utilities") + +MENU_COALITION_COMMAND:New(coalition.side.RED, "Show Squadron Resource Summary", menuRoot, function() + local summary = getSquadronResourceSummary(coalition.side.RED) + MESSAGE:New(summary, 20):ToCoalition(coalition.side.RED) +end) + +MENU_COALITION_COMMAND:New(coalition.side.BLUE, "Show Squadron Resource Summary", menuRoot, function() + local summary = getSquadronResourceSummary(coalition.side.BLUE) + MESSAGE:New(summary, 20):ToCoalition(coalition.side.BLUE) +end) + +-- 1. Show Airbase Status Report +MENU_COALITION_COMMAND:New(coalition.side.RED, "Show Airbase Status Report", menuRoot, function() + local report = "=== RED Airbase Status ===\n" + for _, squadron in pairs(RED_SQUADRON_CONFIG) do + local usable, status = isAirbaseUsable(squadron.airbaseName, coalition.side.RED) + local aircraftCount = squadronAircraftCounts.red[squadron.templateName] or 0 + local maxAircraft = squadron.aircraft + local cooldown = squadronCooldowns.red[squadron.templateName] + local cooldownStatus = "" + if cooldown then + local timeLeft = math.ceil((cooldown - timer.getTime()) / 60) + if timeLeft > 0 then cooldownStatus = " (COOLDOWN: " .. timeLeft .. "m)" end + end + report = report .. string.format("%s: %s | Aircraft: %d/%d%s\n", squadron.displayName, status, aircraftCount, maxAircraft, cooldownStatus) + end + MESSAGE:New(report, 20):ToCoalition(coalition.side.RED) +end) + +MENU_COALITION_COMMAND:New(coalition.side.BLUE, "Show Airbase Status Report", menuRoot, function() + local report = "=== BLUE Airbase Status ===\n" + for _, squadron in pairs(BLUE_SQUADRON_CONFIG) do + local usable, status = isAirbaseUsable(squadron.airbaseName, coalition.side.BLUE) + local aircraftCount = squadronAircraftCounts.blue[squadron.templateName] or 0 + local maxAircraft = squadron.aircraft + local cooldown = squadronCooldowns.blue[squadron.templateName] + local cooldownStatus = "" + if cooldown then + local timeLeft = math.ceil((cooldown - timer.getTime()) / 60) + if timeLeft > 0 then cooldownStatus = " (COOLDOWN: " .. timeLeft .. "m)" end + end + report = report .. string.format("%s: %s | Aircraft: %d/%d%s\n", squadron.displayName, status, aircraftCount, maxAircraft, cooldownStatus) + end + MESSAGE:New(report, 20):ToCoalition(coalition.side.BLUE) +end) + +-- 2. Show Active Interceptors +MENU_COALITION_COMMAND:New(coalition.side.RED, "Show Active Interceptors", menuRoot, function() + local lines = {"Active RED Interceptors:"} + for name, data in pairs(activeInterceptors.red) do + if data and data.group and data.group:IsAlive() then + table.insert(lines, string.format("%s (Squadron: %s, Threat: %s)", name, data.displayName or data.squadron, assignedThreats.red[name] or "N/A")) + end + end + MESSAGE:New(table.concat(lines, "\n"), 20):ToCoalition(coalition.side.RED) +end) + +MENU_COALITION_COMMAND:New(coalition.side.BLUE, "Show Active Interceptors", menuRoot, function() + local lines = {"Active BLUE Interceptors:"} + for name, data in pairs(activeInterceptors.blue) do + if data and data.group and data.group:IsAlive() then + table.insert(lines, string.format("%s (Squadron: %s, Threat: %s)", name, data.displayName or data.squadron, assignedThreats.blue[name] or "N/A")) + end + end + MESSAGE:New(table.concat(lines, "\n"), 20):ToCoalition(coalition.side.BLUE) +end) + +-- 3. Show Threat Summary +MENU_COALITION_COMMAND:New(coalition.side.RED, "Show Threat Summary", menuRoot, function() + local lines = {"Detected BLUE Threats:"} + if cachedSets.blueAircraft then + cachedSets.blueAircraft:ForEach(function(group) + if group and group:IsAlive() then + table.insert(lines, string.format("%s (Size: %d)", group:GetName(), group:GetSize())) + end + end) + end + MESSAGE:New(table.concat(lines, "\n"), 20):ToCoalition(coalition.side.RED) +end) + +MENU_COALITION_COMMAND:New(coalition.side.BLUE, "Show Threat Summary", menuRoot, function() + local lines = {"Detected RED Threats:"} + if cachedSets.redAircraft then + cachedSets.redAircraft:ForEach(function(group) + if group and group:IsAlive() then + table.insert(lines, string.format("%s (Size: %d)", group:GetName(), group:GetSize())) + end + end) + end + MESSAGE:New(table.concat(lines, "\n"), 20):ToCoalition(coalition.side.BLUE) +end) + +-- 4. Request Immediate Squadron Summary Broadcast +MENU_COALITION_COMMAND:New(coalition.side.RED, "Broadcast Squadron Summary Now", menuRoot, function() + local summary = getSquadronResourceSummary(coalition.side.RED) + MESSAGE:New(summary, 20):ToCoalition(coalition.side.RED) +end) + +MENU_COALITION_COMMAND:New(coalition.side.BLUE, "Broadcast Squadron Summary Now", menuRoot, function() + local summary = getSquadronResourceSummary(coalition.side.BLUE) + MESSAGE:New(summary, 20):ToCoalition(coalition.side.BLUE) +end) + +-- 5. Show Cargo Delivery Log +MENU_COALITION_COMMAND:New(coalition.side.RED, "Show Cargo Delivery Log", menuRoot, function() + local lines = {"Recent RED Cargo Deliveries:"} + if _G.processedDeliveries then + for key, timestamp in pairs(_G.processedDeliveries) do + if string.find(key, "RED") then + table.insert(lines, string.format("%s at %d", key, timestamp)) + end + end + end + MESSAGE:New(table.concat(lines, "\n"), 20):ToCoalition(coalition.side.RED) +end) + +MENU_COALITION_COMMAND:New(coalition.side.BLUE, "Show Cargo Delivery Log", menuRoot, function() + local lines = {"Recent BLUE Cargo Deliveries:"} + if _G.processedDeliveries then + for key, timestamp in pairs(_G.processedDeliveries) do + if string.find(key, "BLUE") then + table.insert(lines, string.format("%s at %d", key, timestamp)) + end + end + end + MESSAGE:New(table.concat(lines, "\n"), 20):ToCoalition(coalition.side.BLUE) +end) + +-- 6. Show Zone Coverage Map +MENU_COALITION_COMMAND:New(coalition.side.RED, "Show Zone Coverage Map", menuRoot, function() + local lines = {"RED Zone Coverage:"} + for _, squadron in pairs(RED_SQUADRON_CONFIG) do + local zones = {} + if squadron.primaryZone then table.insert(zones, "Primary: " .. squadron.primaryZone) end + if squadron.secondaryZone then table.insert(zones, "Secondary: " .. squadron.secondaryZone) end + if squadron.tertiaryZone then table.insert(zones, "Tertiary: " .. squadron.tertiaryZone) end + table.insert(lines, string.format("%s: %s", squadron.displayName, table.concat(zones, ", "))) + end + MESSAGE:New(table.concat(lines, "\n"), 20):ToCoalition(coalition.side.RED) +end) + +MENU_COALITION_COMMAND:New(coalition.side.BLUE, "Show Zone Coverage Map", menuRoot, function() + local lines = {"BLUE Zone Coverage:"} + for _, squadron in pairs(BLUE_SQUADRON_CONFIG) do + local zones = {} + if squadron.primaryZone then table.insert(zones, "Primary: " .. squadron.primaryZone) end + if squadron.secondaryZone then table.insert(zones, "Secondary: " .. squadron.secondaryZone) end + if squadron.tertiaryZone then table.insert(zones, "Tertiary: " .. squadron.tertiaryZone) end + table.insert(lines, string.format("%s: %s", squadron.displayName, table.concat(zones, ", "))) + end + MESSAGE:New(table.concat(lines, "\n"), 20):ToCoalition(coalition.side.BLUE) +end) + +-- 7. Request Emergency Cleanup (admin/global) +MENU_MISSION_COMMAND:New("Emergency Cleanup Interceptors", menuRoot, function() + local cleaned = 0 + for _, interceptors in pairs(activeInterceptors.red) do + if interceptors and interceptors.group and not interceptors.group:IsAlive() then + interceptors.group = nil + cleaned = cleaned + 1 + end + end + for _, interceptors in pairs(activeInterceptors.blue) do + if interceptors and interceptors.group and not interceptors.group:IsAlive() then + interceptors.group = nil + cleaned = cleaned + 1 + end + end + MESSAGE:New("Cleaned up " .. cleaned .. " dead interceptor groups.", 20):ToAll() +end) + +-- 9. Show System Uptime/Status +local systemStartTime = timer.getTime() +MENU_MISSION_COMMAND:New("Show TADC System Status", menuRoot, function() + local uptime = math.floor((timer.getTime() - systemStartTime) / 60) + local status = string.format("TADC System Uptime: %d minutes\nCheck Interval: %ds\nMonitor Interval: %ds\nStatus Report Interval: %ds\nSquadron Summary Interval: %ds\nCargo Check Interval: %ds", uptime, TADC_SETTINGS.checkInterval, TADC_SETTINGS.monitorInterval, TADC_SETTINGS.statusReportInterval, TADC_SETTINGS.squadronSummaryInterval, TADC_SETTINGS.cargoCheckInterval) + MESSAGE:New(status, 20):ToAll() +end) + + + diff --git a/Moose_TADC/Moose_TADC_SquadronConfigs_Load1st.lua b/Moose_TADC/Moose_TADC_SquadronConfigs_Load1st.lua new file mode 100644 index 0000000..01f1335 --- /dev/null +++ b/Moose_TADC/Moose_TADC_SquadronConfigs_Load1st.lua @@ -0,0 +1,244 @@ + +--[[ THIS FILE MUST BE LOADED BEFORE THE MAIN Moose_TADC.lua SCRIPT +═══════════════════════════════════════════════════════════════════════════════ + SQUADRON CONFIGURATION +═══════════════════════════════════════════════════════════════════════════════ + +INSTRUCTIONS: +1. Create fighter aircraft templates for BOTH coalitions in the mission editor +2. Place them at or near the airbases you want them to operate from +3. Configure RED squadrons in RED_SQUADRON_CONFIG +4. Configure BLUE squadrons in BLUE_SQUADRON_CONFIG + +TEMPLATE NAMING SUGGESTIONS: +• RED: "RED_CAP_Batumi_F15", "RED_INTERCEPT_Senaki_MiG29" +• BLUE: "BLUE_CAP_Nellis_F16", "BLUE_INTERCEPT_Creech_F22" +• Include coalition and airbase name for easy identification + +AIRBASE NAMES: +• Use exact names as they appear in DCS (case sensitive) +• RED examples: "Batumi", "Senaki", "Gudauta" +• BLUE examples: "Nellis AFB", "McCarran International", "Tonopah Test Range" +• Find airbase names in the mission editor + +AIRCRAFT NUMBERS: +• Set realistic numbers based on mission requirements +• Consider aircraft consumption and cargo replenishment +• Balance between realism and gameplay performance + +ZONE-BASED AREAS OF RESPONSIBILITY: +• Create zones in mission editor (MOOSE polygons, circles, etc.) +• primaryZone: Squadron's main area (full response) +• secondaryZone: Backup/support area (reduced response) +• tertiaryZone: Emergency fallback area (enhanced response) +• Leave zones as nil for global threat response +• Multiple squadrons can share overlapping zones +• Use zone names exactly as they appear in mission editor + +ZONE BEHAVIOR EXAMPLES: +• Border Defense: primaryZone = "SECTOR_ALPHA", secondaryZone = "BUFFER_ZONE" +• Base Defense: tertiaryZone = "BASE_PERIMETER", enableFallback = true +• Layered Defense: Different zones per squadron with overlap +• Emergency Response: High tertiaryResponse ratio for critical areas +]] + +-- ═══════════════════════════════════════════════════════════════════════════ +-- RED COALITION SQUADRONS +-- ═══════════════════════════════════════════════════════════════════════════ + +RED_SQUADRON_CONFIG = { + --[[ EXAMPLE RED SQUADRON - CUSTOMIZE FOR YOUR MISSION + { + templateName = "RED_CAP_Batumi_F15", -- Template name from mission editor + displayName = "Batumi F-15C CAP", -- Human-readable name for logs + airbaseName = "Batumi", -- Exact airbase name from DCS + aircraft = 12, -- Maximum aircraft in squadron + skill = AI.Skill.GOOD, -- AI skill level + altitude = 20000, -- Patrol altitude (feet) + speed = 350, -- Patrol speed (knots) + patrolTime = 25, -- Time on station (minutes) + type = "FIGHTER" + + -- Zone-based Areas of Responsibility (optional - leave nil for global response) + primaryZone = "RED_BORDER", -- Main responsibility area (zone name from mission editor) + secondaryZone = nil, -- Secondary coverage area (zone name) + tertiaryZone = nil, -- Emergency/fallback zone (zone name) + + -- Zone behavior settings (optional - uses defaults if not specified) + zoneConfig = { + primaryResponse = 1.0, -- Intercept ratio multiplier in primary zone + secondaryResponse = 0.6, -- Intercept ratio multiplier in secondary zone + tertiaryResponse = 1.4, -- Intercept ratio multiplier in tertiary zone + maxRange = 200, -- Maximum engagement range from airbase (nm) + enableFallback = false, -- Auto-switch to tertiary when base threatened + priorityThreshold = 4, -- Min aircraft count for "major threat" + ignoreLowPriority = false, -- Ignore threats below threshold in secondary zones + } + }, + ]] + + -- ADD YOUR RED SQUADRONS HERE + { + templateName = "Sukhumi CAP", -- Change to your RED template name + displayName = "Sukhumi CAP", -- Change to your preferred name + airbaseName = "Sukhumi-Babushara", -- Change to your RED airbase + aircraft = 12, -- Adjust aircraft count + skill = AI.Skill.ACE, -- AVERAGE, GOOD, HIGH, EXCELLENT, ACE + altitude = 20000, -- Patrol altitude (feet) + speed = 350, -- Patrol speed (knots) + patrolTime = 25, -- Time on station (minutes) + type = "FIGHTER", + + -- Zone-based Areas of Responsibility (optional - leave nil for global response) + primaryZone = "RED_BORDER", -- Main responsibility area (zone name from mission editor) + secondaryZone = nil, -- Secondary coverage area (zone name) + tertiaryZone = nil, -- Emergency/fallback zone (zone name) + + -- Zone behavior settings (optional - uses defaults if not specified) + zoneConfig = { + primaryResponse = 1.0, -- Intercept ratio multiplier in primary zone + secondaryResponse = 0.6, -- Intercept ratio multiplier in secondary zone + tertiaryResponse = 1.4, -- Intercept ratio multiplier in tertiary zone + maxRange = 200, -- Maximum engagement range from airbase (nm) + enableFallback = false, -- Auto-switch to tertiary when base threatened + priorityThreshold = 4, -- Min aircraft count for "major threat" + ignoreLowPriority = false, -- Ignore threats below threshold in secondary zones + } + }, + + { + templateName = "Gudauta CAP-MiG-21", -- Change to your RED template name + displayName = "Gudauta CAP-MiG-21", -- Change to your preferred name + airbaseName = "Gudauta", -- Change to your RED airbase + aircraft = 12, -- Adjust aircraft count + skill = AI.Skill.ACE, -- AVERAGE, GOOD, HIGH, EXCELLENT + altitude = 20000, -- Patrol altitude (feet) + speed = 350, -- Patrol speed (knots) + patrolTime = 25, -- Time on station (minutes) + type = "FIGHTER", + + -- Zone-based Areas of Responsibility (optional - leave nil for global response) + primaryZone = "GUDAUTA_BORDER", -- Main responsibility area (zone name from mission editor) + secondaryZone = nil, -- Secondary coverage area (zone name) + tertiaryZone = nil, -- Emergency/fallback zone (zone name) + + -- Zone behavior settings (optional - uses defaults if not specified) + zoneConfig = { + primaryResponse = 1.0, -- Intercept ratio multiplier in primary zone + secondaryResponse = 0.6, -- Intercept ratio multiplier in secondary zone + tertiaryResponse = 1.4, -- Intercept ratio multiplier in tertiary zone + maxRange = 200, -- Maximum engagement range from airbase (nm) + enableFallback = false, -- Auto-switch to tertiary when base threatened + priorityThreshold = 4, -- Min aircraft count for "major threat" + ignoreLowPriority = false, -- Ignore threats below threshold in secondary zones + } + }, + + { + templateName = "Gudauta CAP-MiG-23", -- Change to your RED template name + displayName = "Gudauta CAP-MiG-23", -- Change to your preferred name + airbaseName = "Gudauta", -- Change to your RED airbase + aircraft = 14, -- Adjust aircraft count + skill = AI.Skill.ACE, -- AVERAGE, GOOD, HIGH, EXCELLENT + altitude = 20000, -- Patrol altitude (feet) + speed = 350, -- Patrol speed (knots) + patrolTime = 25, -- Time on station (minutes) + type = "FIGHTER", + + -- Zone-based Areas of Responsibility (optional - leave nil for global response) + primaryZone = "GUDAUTA_BORDER", -- Main responsibility area (zone name from mission editor) + secondaryZone = "RED_BORDER", -- Secondary coverage area (zone name) + tertiaryZone = nil, -- Emergency/fallback zone (zone name) + + -- Zone behavior settings (optional - uses defaults if not specified) + zoneConfig = { + primaryResponse = 1.0, -- Intercept ratio multiplier in primary zone + secondaryResponse = 0.6, -- Intercept ratio multiplier in secondary zone + tertiaryResponse = 1.4, -- Intercept ratio multiplier in tertiary zone + maxRange = 200, -- Maximum engagement range from airbase (nm) + enableFallback = false, -- Auto-switch to tertiary when base threatened + priorityThreshold = 4, -- Min aircraft count for "major threat" + ignoreLowPriority = false, -- Ignore threats below threshold in secondary zones + } + }, +} + +-- ═══════════════════════════════════════════════════════════════════════════ +-- BLUE COALITION SQUADRONS +-- ═══════════════════════════════════════════════════════════════════════════ + +BLUE_SQUADRON_CONFIG = { + --[[ EXAMPLE BLUE SQUADRON - CUSTOMIZE FOR YOUR MISSION + { + templateName = "BLUE_CAP_Nellis_F16", -- Template name from mission editor + displayName = "Nellis F-16C CAP", -- Human-readable name for logs + airbaseName = "Nellis AFB", -- Exact airbase name from DCS + aircraft = 14, -- Maximum aircraft in squadron + skill = AI.Skill.EXCELLENT, -- AI skill level + altitude = 22000, -- Patrol altitude (feet) + speed = 380, -- Patrol speed (knots) + patrolTime = 28, -- Time on station (minutes) + type = "FIGHTER" -- Aircraft type + }, + ]] + + -- ADD YOUR BLUE SQUADRONS HERE + + { + templateName = "Kutaisi CAP", -- Change to your BLUE template name + displayName = "Kutaisi CAP", -- Change to your preferred name + airbaseName = "Kutaisi", -- Change to your BLUE airbase + aircraft = 18, -- Adjust aircraft count + skill = AI.Skill.EXCELLENT, -- AVERAGE, GOOD, HIGH, EXCELLENT + altitude = 18000, -- Patrol altitude (feet) + speed = 320, -- Patrol speed (knots) + patrolTime = 22, -- Time on station (minutes) + type = "FIGHTER", + + -- Zone-based Areas of Responsibility (optional - leave nil for global response) + primaryZone = "BLUE_BORDER", -- Main responsibility area (zone name from mission editor) + secondaryZone = nil, -- Secondary coverage area (zone name) + tertiaryZone = nil, -- Emergency/fallback zone (zone name) + + -- Zone behavior settings (optional - uses defaults if not specified) + zoneConfig = { + primaryResponse = 1.0, -- Intercept ratio multiplier in primary zone + secondaryResponse = 0.6, -- Intercept ratio multiplier in secondary zone + tertiaryResponse = 1.4, -- Intercept ratio multiplier in tertiary zone + maxRange = 200, -- Maximum engagement range from airbase (nm) + enableFallback = true, -- Auto-switch to tertiary when base threatened + priorityThreshold = 4, -- Min aircraft count for "major threat" + ignoreLowPriority = false, -- Ignore threats below threshold in secondary zones + } + }, + + { + templateName = "Batumi CAP", -- Change to your BLUE template name + displayName = "Batumi CAP", -- Change to your preferred name + airbaseName = "Batumi", -- Change to your BLUE airbase + aircraft = 18, -- Adjust aircraft count + skill = AI.Skill.EXCELLENT, -- AVERAGE, GOOD, HIGH, EXCELLENT + altitude = 18000, -- Patrol altitude (feet) + speed = 320, -- Patrol speed (knots) + patrolTime = 22, -- Time on station (minutes) + type = "FIGHTER", + + -- Zone-based Areas of Responsibility (optional - leave nil for global response) + primaryZone = "BATUMI_BORDER", -- Main responsibility area (zone name from mission editor) + secondaryZone = "BLUE_BORDER", -- Secondary coverage area (zone name) + tertiaryZone = "BATUMI_BORDER", -- Emergency/fallback zone (zone name) + + -- Zone behavior settings (optional - uses defaults if not specified) + zoneConfig = { + primaryResponse = 1.0, -- Intercept ratio multiplier in primary zone + secondaryResponse = 0.6, -- Intercept ratio multiplier in secondary zone + tertiaryResponse = 1.4, -- Intercept ratio multiplier in tertiary zone + maxRange = 200, -- Maximum engagement range from airbase (nm) + enableFallback = true, -- Auto-switch to tertiary when base threatened + priorityThreshold = 4, -- Min aircraft count for "major threat" + ignoreLowPriority = false, -- Ignore threats below threshold in secondary zones + } + }, +} + + diff --git a/Moose_TADC/TADC_Example.miz b/Moose_TADC/TADC_Example.miz index ff6968e77583dfd4c84f828424caf879523cd470..27061aee2af93cce9258c405e3b031502fc8af80 100644 GIT binary patch delta 32004 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%0DI^HkqaA-b>hh7?(T_$l}r1O_gVfG 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 U{AVece@{FJ5Rlow6cEt=0(f4>D*ylh delta 31121 zcmZs?b95lzwk;gnw(X8>+v(Uw$4)9XI_el5r(@e@$F^-JoqqY9d+z=2ci#JB&QWXb zwb$OGMvYxnvuelXvyNo2ey0WS6zVSSp0#N1+0l)8PyHhak3d82V@(;>NLO+?`+Y>A z3w}&|0kao=v{IS?gvyzPJ?131A~Fka`zNSr+J4p7{i97j~$*5BSf zwarT~$oTPa)hrXdX~d5!;7Z-!chrBi=XhVf{A$7dx`C^2nBx0()hrb|q{7`Ih_^{_ z0EK~!0*-Ws#PrJP5X2dLZOnRlpOl-}d{VOYhHU(iq~e+VlE;!Z8z;L3gjQMC+UhL2 zJ-$#TZakMy<#w43ac%-r}r^q)7`L$o*!v7}qp00B) z=w)<&JX-3J>El|$*New^%o+Q^d#R_7Ts^d*a^>&rHg^^dbRh!NhI}X&65K?BF>S_Y zH{W*la{s>J0DD4J88-!0^8oSR1LnWAhw%?;{&u^>#HkfWE4o8h1RrqFNihRADo@7z zxSI!pZC*I8cf|L6&6Li4V6bv+@;cPW#7I>>ieKXUM*FR-cOX;-+iZ#sq z>&yr|Z(cu3HYb4@{W@ys1R{t#>Vn?fzs~G0s7SYrs;!iUrsC#%<8Ezv-jRJ0zdV*+ zEZdi|JLItPy)qesZp45@wUM1kg>Ehw_)L3|ka|Xe@~%5SK_Qnx0gF|U^$j!bllvoG zF^j8Z*`v|9Ewkpr%6oL7_sQLo$QJUMEmI~U$sODuOX0wj{xfMaXe@8HPw<+PzsaSX z`t!5Y)hjQ7FF}@y^Jc5za;^{-*X2FF?*r*do6DNTt%5Bta;{6iE9$+R9JSh&5KLkqtz+?iZ|CVR zbnO9W#yieelhm7qw{*BBcdd8gyCmDjs(jkA4Mc@D)N&7Wg(FAhRW-#Di*GD%BQMWF zgTwoO07~~P7S(!T8PRkV*Uer3`S$)VH}voCp1DBWfuC#Mq^|2*|4qKZ&@iFhLT<2Q zR*agNKkGTNR4R8RbY!fH{D;i-71Uo=WFjS^IT7Oc@IRYwhvi?1g_7l{nOm%C5Hns* z{g?60D7mmP3aQzMe+vy)qa~%WDroeuQ;UD8+_b=ZXLFcV4jbwDN>y1iFtPu29{eTV zE4Ks4SCR>V!9X2zOz_(6P-05o0p$pBq^od$7(MbkJ<#V&9-xQvgzl)GsVh_(i^K|? zUbQM`aJesItET<&G#b%BHyy?z328~G&k9uwm8@zSyF2udVX{fByGf~i9>Se7c8FZ* z86UPi^gTeB4(m=MpZu$lCX}8WX&Oi0jOii3A%9@!#GvoMFuYgDh@Kr|5e+ZfTBOI@ z#-6^Sis|QU4JH{f<5_BAJ}1$LNMjsC9~-)@2WzugR(Xv6TKZKO=Fkvbcagd|s7BaX zqG4WfX4NaT?w1@Mqo7TwGW#1E?u@eq*i_jp9^LsVYx|~5qZKY}aX#aj+E*(r5{_aZ zQ}#f|_t#ucGGBLI%4ZNlzLvZkwcXSGb&A4-yGZQ4+G zYsR8GMU4YlQjEDsjUY@?Pnq21ChY(>nb72k)>Mn+A9cT;=6^s@o{{in&zw@o(nntW zZdGOVLD^Uy;zT!?p-#_9)Rx;3Qq%tljF}kF?BTJQvWBz9>2RkNOSyacVEYKpLLL0E zcsfo&z=~DFDVUD=B39~U3r5d)2f%aDv(2Rj#NBA6Ty zD_-=ohzgBUZVEh_d~_Diz5h_dPbo~K;VDfRcc{U*?fyQ>T|6QjRX)?d%~-e$Ab~n! zFJ5+3<;3RBi-bjx$BJgsM(oeq9jIu6Cxh2PDKGHLh1KpCt*V1ZaCgDlJ{GMmocKui zftICDQ3oX7k!bfaL`)!~Zc8`J+|sSh5=w5rY6dPQB!pv!+B?WZ8kH6QFs0+Mn6;?~ zEeLSF$G|DTOBr;`Gv?KinHR{c_<$_$ z#SQisW-}&CH9*ISM^GB9v96)?i&{{CDz1Grn$rP`O(zyBPe+*Zm~?0W=+hBYo#PO( zJmi=C2QD|wT3Tgd!Yb1c@~cLbe^S?+U@-VaXUom8;^V?6_0v(KX(5xJuJ@N$U=-DO z^n!$`LB?U~x8?4piLxnHei?J&+Ef71J}E9TP%r%&6%f)}~ok{k*T zt#$0apDacOc;SAvLy&hamZK;m3q~%F-M%(%h;vdkr`>l#J`zb|{Wg^$hYPoGTowBudvjMvMLqtHYXl42>-r zEeBb}Dyp%Gd#XzXI9{k!Kn_80^41o!nYK-ub67PV%k4Lp1TOx-l{ta0mwz70!u>jN z?Q=T_hg}ZM7l?YVCxTt z?`5$(D-6eWTmh^Y2?BO@NhVARRb~@GAWQTa0g_t@*Z8~;(6KXsR^!YN$lA-q#Md#- zHSUJ|4ao(4itVSdBi`tefV7q~Lk}volSS_vL_zgFL8v(xxMa;puIcmKA2@lOd{ka` z7Gd#GTG=9V?iO0H-;1p))%)tx3FrUMH{PO(Y zdY2{j;Z%Dk%x7jpLYp^xv*~8Qn7AReI-Jfm?=HYnex$qBEu`I6Z<&)rIWQ$B(#>}{;YX!2O5ZL_3 z!lqRYVa;B9h#R5VU(U-qNT_!<{9s~msVE2Kl0m+dbjJ|i2?aysS8Bjbq^ul zKgK)Rb$jRh@?quy`}>x#ZP=aq=1UT<`|Ue!z*RA}0dTnyDxp2a713>44lngBKH%~S zA#O;BI9@I7p(X%ai8Irnn`Yr1Avu!Pe?Bu&Y9iNf17C?(ZB*R0>YQiAkojDK4Rin( zMCf|S%3JA}-<5AK+^Mc#{OiRUPbtfa$Ol(-F6H&*nk#h7@1XT>?Zy;6^9#19lo9JP zK*aB~ffPake#nh}!ycms^k89FrKz}+-$y0z>j29ctKR>ahxV(K?_%kGfZRkJdrtDYzPia2#gL0P5|z8X_dp4j zkT0Wrs!X^2U$tuWE`ft>5qBnncq$h*Pt3E&0 z7e86nAFf$sxa0TL;!J9Ya`Wb|$z}{EhKUq{FaJo^SF9?)YJ*U^)y2KX&DUA2@1qz+ zCJA&m+r_}3n2ewMR-p`k#ED^K+i{}4Wmh3V8MG< z9!s!L_}?t<$*tpYdL5fbgZ*3eL!n5=b2)`m>tOmE{wH^C0eCE8D2SI?%c3pA+F8=3 zI;HH$ydiB@1bi5a5z`i)a48!w)WSvHV(_zaIG7l^bjO_L3S%osf9nPu)HHq^&CU;l?WVGrE;nsL#JTnj+fHM0&`YK>ODd z5?9%os{bGEmX53UFww1zrxNXKo)X91j_eQ>xV`Erd6hbYljX#awgUsMKfd#=mx#>$ zGB3dZH5;X%wE>Z3x5(|+#F$m)J9TQ>-^fWUZ*41Fnf?H($$0|v``%HaaVl@>-EZ2F zs%vvsH|aU>5|8)^u8s81n+|}p-Q;&qdwThN(MNu`6t|~&w`*EE_|6&GLYx%(%~Ey3 zKMUW&mAoy-YqJ}9Z*tH6=b)eEd@DXPf9W+)xUP59P0Ox_p|4%A(s|Qn7gVg^9~N)! zU3cwX61+CW*ffKR<~(eU2#xHqHTNJU*A-W7CpWZ{sIn40m>30x25B+|sNEJ=Go2e+ zMtAND6hogo?M8FIvK}jpW!|tCJHMGa&*N@{E=lbXcfQp%4MwZEA5&I`vCKIpcPjx* zUaRU3-+naahgM@tVw7)WVz$$I$##i2!&lcTDhYAPZE}9R{}MX?a4oxj|0*(s0VX6| zMRFj8xhfxziykT&P>zi^C9B(ns_qKa6wvNj(3N54@vygk#*pAE$iDBve{|Bs1t~rP zy1cL~E0P$XT@@D^%vK%`LW?GR4pIiBjPcH^kk*KU@7IASNgO!LCmhTt__*=gxfO29?_9t!k}-tMNN#%75YGt&&hAHW%=Nkj(T6U5^Ju$u3BO zq`;Xh6sSOr%|jvN(Q$OeX{3?5)6}FvL}ijAVrx2Q`BbIT)uc7lNL^}_s=xE8OK%Ih z8Z{W9y6aSRih<$u)x3Z%MkdP^6T*$Eht!~h*y6Vrfa@OItN8fwJNiM?!D^dAKtOLt ze7CdrGYiNfWyB+K&w8&y$Ik$80yil0Mq;j{N!=7lW3@)WwWRkoj%l{I+bsBZ!eV1w z%qmPuQhKaym!QfL4t{7KhO_PzC2A8LMTMAjvJf5Z%H=qfH8ZB!HO*Mi02i6N1Kp9EH1Ao&8p>m!50e^3VR8#OW%ZIp0v!7 z++oE{!Sux;TM;5xSa;)81WQUWUu4Eu)mV6)necqV+jLbDg@M(o5*7)plwU9 z@w|@lXvu!|;6}!();3miUbFVSp__XI7WOzVErgMqpzDvcZtI*&Q`X;P9GuutniCRH z86Tcu9Hu7DAKjv%2OT=stcxL_I0qdZqrGR+_ZXBtsTgcjm(l?v>Tcb7obexbr3+WC z02W>x5ERI3eXk{)61CN)rKGu%0oYG>WBcQEKIK>JF0ft}y@atm2$M>Hd;>B5G1`oW z)P-Cyh93}H1~#Po9R4M(=x&YGrw0v5aq@mD{xf(x?Xb?=_;#~TF_Ci z6hmEpzC_aZ*bv@yQm5UsVV&d~zX4g7C^9+}n)LHPUjU*-g4Samc)u%=kY2Z##mjB- zF|~rxcYKj|MtrXyrX)sT@M7}E)b0Vvuq!M~k5RiUxol9pI7A6rMGHb>j2d|tv}JwBG?;1x873vX zqJN<6Y4mp2XD`VQki3CwN;|f7z2yYaqU6x(9iImcySbNxr?6Upc*7&nJN2}g zsFKQohdgp8HZE3q@}cp96oJ!Y$U-Rl_4K$`e}7XONjpiEDB=kyRR1jOuD#z7NJo%> z3Sd*gh8k2TMpf_2J`oVqY|!r$u>p>=WXnj`)f`GA1o2V6K^0gOm8&g^cH44R>Y`}A zUaDuG^Y!st&mD%wWDzlm=T)a!E`3HvaSj-PR3W9`QJ30}zB^t8w_O+!ll)hrAlVuI zL5}h+q>0CP`;q{KL4|cpwE}w$SQWv%|kfogU zE&u1&a9a~0pv8Mhu-iMA<(#NKzln3J(PxiGvqT$RKex}Q&^Y7Y1yN5%Io8Vg!>KKMLMK9vMfaotEF6Hk|HF(;_#8%jJw0H}yCwpbQJ(mR-N9&N zK2cJkp5no1S~5r(r1-*vV~C1O_tfzX0*Wnk#m(AHxG{PWe^_^I^T0tglW206^iH<< zcK=glIWq103M6jKmZ?Gg)!g98!O0Q1ewgt2p*vm!!#6BaqPu=qB%{boFHD4QDz1QQ zZhpaoxD*#%TjNZ*#7wPURNMkO;achOa?tUBcRu`EqB;-uGWy?~!cRm|F zX0|p;wgAiwqfWV;x#ijDl!LmvyNLTO?yu|$YTj0rnbAS=AN*pS^!t`=AzGVuf5d(; zNXJ)G^LHr??mb|{e^Yn&6>&Yqtpa6^(l-SFR? z>zwx(i7qTOv@ z{0$n)E&3L^RJ>+1Bi%3XQ?cz+td_bG{wsvf{8!jL?X{wr0b{!bJayQWo3%gsO-DD$ zHe|LiPgRA=fe<>0PZCy-LmYArXYI$pXblISi3fWOXAh)B^H^I5V2dYe`unLp>Ni=P zXNERIQm)+-S{{>dzHMuL8srz@_~o2$|1wI$9j1y{i;H>oR4q@w#n%t85r-uat5CEr zAG4l^Z^+QHmH-*8*1lV`(YN2eS6BziJV!zhE#%edC>QcntdOj5{^|-7#nElb3QK1>(Z$Ti-j zphdhB{;LTT*D}Ne3co!h$XAc$ZU;rCg5XYmoRp1%-}u0JDWpZOVA@Vr54XN7@#*H* zv-9#5aseJt{00$t(~N zP@hx8tS%7PpW; zXlCyc!0rp^eeEmAJeW+P!>lw$(`EifewsX(c6xr3dZ?upjC0`l?%+7rYkxX3BTfB{ zhd{xb%bI5Ofj?>8f>$N0vm;c&oB!?fS+EzLkV`1L1+P$(Su%Jn*JT>EX#6HsMD+w9 z@1$v$OsTQ&;h1&hkQVp%a&lea_h^(E@at+&AiH^zTRSe(C$=0U&UTd?A$C+`RS5BC z^*)YGZIFf7LK<%26k?iL6#uX+*kr7Fn9^kLB#ZM09lLTEvPsp8%)DFAVzkeg8t(Tf zG`)wqL>-B7j5EQB(S`rY$}`zpi2VTgxf;yyXO~!*_nU=f0P39bR3~-2krJ-*{@{Eg zcj{H}HLG)8zYOf{GKh*ria%;CbbjYxNO!9S=ZyEy6aVQuBmKGkW#ko%Fy*gB720it z2YyZNPo2u?u&XYDSxM7xdf;>ge`eQXIOkMt6}_eir4tm(k@Ih@?cMzt9fOV~TDn z`C%pV*RO-Y$k6dLwdKB&0<&5xx*EkgFLIn&hlNS)U zCf`WPZgW@+yIwRo$8la|7An~~qR-Jp-Hl`%wtqkyrKXF_b&mJSYN^TrC7krxu646z z_O-JKjb6vgviz>85xqzbTwk$$A$i-LtdcSy@J$NaPoxxcpNxmS?)pT8z+A7ZxL(-b zFCu;pQYkkto#ehBBS5KX_J>NA$+>wIutVF(6in8u%lA!fw6aOq>EjF>Jq{n%y`7pg zVf(yMYCEBQGv+_d0A z6dpH>GQQTaaon0;HD4Ys$PN#JasMh}!1|fJLMJJ$4qroHKFuxwY}Y-RXya}?W%s%X z>Yof|#jI5sqDD>?d1F1~DyJb}jw*lH{hVf zA0OtibDvMXfj8EK8Cmn29?i-74dyMMjXUV?ADEgPhZYNElhM;Lx;>fTTS_Kh-OIj` zK(jC@2{#oStyKh~|01p_?{~@~pQWDHs z`MlHWfP_Pa=(xTfO(;oO+X_|abCf)@aU{^VHlV2;Ll@JLPcDAS4HAh;2kb^W&utA#y zUgEClb^DTGjkLD%u!g;@JsmE+eAxes20C1JQh2bxrn>|Rm>!AZ`h`6)d0quQF?A?jiMl67 zi~9L(LTYcd_aINT3v=!3e^iUf9Xw?MBv(9)Yu%9q@fV*c~3d{~yUs_+T5P zdx#`^VTb(BAbs%HOc5!tyV);K3oB3NS`*;q)IzCJo6e62H#wx6Yy&=Fp1)D9aCZg7 zIxHie{!0Y@OZ{6SMv^H5!4YhG~-$n(wDm4MYgbn#a~G{!{eX zysfk~tV+7L)a>&O9e0<}co8B@SAoI}gV{(A-5C5A4Z5VDN08?U>@HVe@L`BUAjwBX zCpkfb>vofPs#0J1H+%`~n>wOrA$UpXB|lwWGJKn-{*7?jrJSq}51& z3bNmZ8+#sZ3Q`IADjR;Yb^G6BV6xeIJ(dNZL%8|xi-w=?z1)#0g0ZImYNFMMb5GA` z;6zV(VG2@G(~-&3-SbNCdJ6Kt3GHqQ2-P~&yWYS4@0z1K!ZamnVh5`GbK$-dW2c(}gzU|ihL z6QS($%-@Sghw$d$>tj5vpq;$8yT4VDdHTDzziqa>zCnb0yyGFB3}pkVs&RajDcThs z7*=iK%x9(MoQn&m@ExS;LG#ujH`=%r;D0_ z#X*w#PP10Dt9B7=KC4#Nt2S9RZ06j2gE$%E(AVng7Wf2Kk_llBjUdN1ISqIql%ta6 zWgL7em5Z?DJ=+4PP%^-(LSN$!qJ&bQCoN2dyBr5hsF!Lb%OAFiymgyjaDgfwaUsE#XWvRgr8UyvP-dQG^fmW3R z`x#1W-9YI#(aL~L@B_)OaDt}#yN`-A9SvedUW~PUt9G|I^IR6xh_%TW%!R6SZwxGs zN)${NzTbpb4fgv|RTf6$HJkylhpUm=IM`&eDuP7rl7`e^LyC0Hp0bUs9_Ht_Q#cP2 zT%rNVxN*0jtr8*H zQ=vvBS|h<~(k%0kt2RH263zY~Ch9(WfyKFc>MiI4aI~hC3#w}CIqqh{hkaY{z2B<8 zhZlb*X`KlS+)}Qg$?s?qRs9)*kgO>RyL2)C?(f(SSLdSf#iJe3W{Ne@@6Ip4G}*6IZ4ssyEH|HtAg)$Du{SP(;18 zkgbSXp*aSSBj$7P%}RGwzN5E}-=nu0W&n?&q4jh;Z=-)M!mELnL(GYbtJNc|D_vSb z9~JG)tJTgMf^xOuu-w-qbHW(jja-~l4246h$D1dTAT^1Ot5mg`+fPTQ4R-43&=6m? zp<3taulvJgHXo=IujiI=0FK>$Pc#i~D>>br6T!fh!-x#xfDG2Hw^LN=i|34BC*Wzr zt7uF-0kY=QO?g)LTb24a`&+c1`wPJV$#pVDSEbxrlv|z5)Y6*@HxhN@bS?+F11N>K z;>R8?$pI1rDR*l4CKRLdDZJH){BdmT0RKIDc*vmxOM#&=8m@i;Yrb{C$|VA>O37X?(|( z!)I9=F-t+JBgA(+H`rc2rbpz-HFegY!(c>GUN>Z=f%jYA>raq^enf{^bBf6kWjpJi zhU>(Q2(zUwd#_PVY4?lqv{bv$&+wPmXw?LQ&J)#mv zu0josF|I(MjA-@rUU_QU*8I4U^}0>WorbD>`6pa6ijwj^Y!tsp0yWJoghWoBl|m5N z4s6t;s4@nqm=_6|EcmGYvAGWsXXC^eRX;O%MasyP12-a8w=SUg}-$u}g zn64u(H!N}kByl2ZvaJbn8jw3uzPDLNjo3tA05Y?0mH_Gn>1H;hV|FTe(vjGDY{alY zMIsvHpgAXYNR2i@zn}9QnHLG_O!ghaY~oc0NBXY!5L>!XnQ3-R{y_!C>=&f^7dSK| z_mOw;ZL*u!mfSlXIP{SS&4XxjS_6JaonP9zv*09jYDL>5 z^?dE57kp{tR|U?}I|8B(t!kSp2p2TY6@e-3x@Qb6uM}ctcKP5W^n@F0uRwOkNk;m0 zJ4-JR5o*x+dv2WRSD z4-`g$YvUYU=ty?3wLHQ!(Yxs-WsaQfeOPh9Km+hN6$rX!H9DamOX7oA1^Cg$ih?#{ zFe71=kY$TimrJ(J=*INT29!@?35rGwy`ZphuFrwFJqiQ8z-#;;C7az}uW+I^$&~OA zHgPK(*8AS;;s)eTN5KtHYgnQTEf(k>5U~&s;uQKcEsmQu!Xr_#=)z()ft@gRgnPFpZokJ-Y zTO()!CAMNp1B50$O)xBGyXfn_DTlgO3jebWnxt@2zAkh=jgqoVK2IV1{CDx%FD0^g z=c5F=6bsH;9{7!nvLcfFCEGu^AzuQW;BE<>;hX_YMY@RT4&~$?hA?yRBY?7mg#HM_ zwLsi9K)k+;5%9I<>NX0#?g-*B=pDL^q(gysR$@4ggcPXFqsF=)2+H|7jSlZ^BJv-i zBbOOzmX}5E5!33D6k`k9OqZZTZ+7J4LjwGzd_#4-zN2NnpWl5DAAo*AzmL719eMo< z1LZ-rwlvK0PqtN*h!#T=E1E7nk`D}fFxy!xNg|H)7fhL*c49W?dh1|XjiTcJQLtSmG0mDwGD<2_O7mOcY@TYL$_~ZbeEJ~X< zmaO-PzL7!6RAV9FTv68e%YuCP-kYL!gkSJ}SVP2<7uk*mM`1Y@l2z9PMo;<-*=`UT zqyB>;EHtkYO@YXSvDV>shM4(}n>yc)R-U$qYj9?QP)*oF=UhzuL929q;RaTmUsNPo zdFc!R#%=u{ZZ6Ka_KgQSf#-?VeqB~LyF<|Fs_OB&gJ)KN=OuUKQ$7?uhAJb*Z-zX- z*X&_O*IC>KDUj?Gh3BMr*(G?*ZiAI^!X;jCEz5}qO9{+)JjCzLA?dSt+$vt%(9jY$ z9u#{j7y(?Xqqt$`G!!fJh2>+v_JsjQ{-ogG(^L;<7F{G5H##puLgp3y!9YU7D(njX z&bn0HqvNl@5f*#`NcF?fYBNFYx~^rJOSsN3GrlwE$xln~ocL-SxEjjpsi(_flN|!5`WTQ3>L{Afzv^O`@pr(ETFM z#KUn%2tr!8h6iHVct(~g)&x=s+1lyZE`fR@4nMe*#x5GdWY+6DVoTMwEPvGx z8)g{lBksc7-?aBCKsT33Ns(uF95}L{Y!n)cF5SsHXP$_EnZCv1Jh?e9-+O}?cUpgU z-L?4+=yc*BIsj85)bHxUhEOu2`SzebG!tdpCa) z__gMB6%SQENl8G01oz#IM`#`Is}yLx%J=c`T%mux24SD$#SqI8EHteC^}YSL)#t;V z>qcp|&L{e<9h}u*CYDF?tpK*^Lo)NbyDoc~y8WN4J@cKhAhG&^?ACvrRTIjOnj-KT zMyfRud~1|m39+xVc#`S$lDf0*(Ix=H1zVjj2xPTvq>}jw$k(%bahnC3tcKqtfj4H;fz_ax*_j=Q%`nl^(R*?=O#7 zZs+?a+Ied$py*r;~0TECyo(A2+w9{I>e+FyzMr_9jiETZeOw|- zHsgU+`KM5ad6rY}MUt)`9t=b2QcAY#L?nONV=^OKWHb|*!mQu-e!$=8A9f&#+B9?eiQU4@t%TEGc_X71h^NrW;8=U%-)(C*nbXkrQP4GO%S%++CIsAS3 zx-jm!3(5LQT|^lrR9TbT3iBp>Z2K|IyM~onM7KqLTM`vzjtt8TV}5(Nk@TAP2?q{` zu?D-KWh|bb3ib(^whlX>K6mv@nxO;=QUF?g>loTzOjd?N2fm*%nQ71!_6R zihMqoW4LoIMpt1D*#p`>4*=OvFVn63r}^*Urxsx1u>7$jEy;=R!RSymQv66&0Pzyj zJQBkHyA63N{qAm98Y8Q@wULVGm_DVV4wj<@P12fbpERj=!P(3;Q}fWw8{(p=?U8sCEXnyj2=!nX!t+(I9fozfiz^KE zsck^g;UF<_HB%S=g0}kC7wv)i9}WX-Tz!+Ujn^APRhx1pKONDv@H-;;sH?nJ>8LKs zp03(F&Rg;6t^#j8?116N|3&Bh9j0>Qp$H5|2o@1%&g7I_RMWD$`z$lyU8Zbp2Yj4d zf$4whKjH{S*8{qeVIafPW5O=MDe=?N*m+ zKrR8&f=ad^o)x)r@;A~k_6PK}5f?%hlS;JHy#Aln>l>{XHCBxJ^D16aOo%({w~eGD zW6D#mdAQUuI$C)KY8bBwd97HHm3*?YTLvieTG3T1jIXnV$Qp6ZGwgXG@F0<)I}vVx zhNPIAHM{GmfwpD&1k?hG;HBFu;$mKxdqXwhg{{=v>dYL1-**xM&5Prq*~$6+*hm2HvFP7*a*P$nWDAaz~u`ng)ZOtc#(FZU#OdqqD}Wx}sj= zN637O6tm}j&_C1{Sb)8r30Q)58gWJ#I65>hWZJK2wnN|d$)m%4BglQ3<6=^j?q*BV zfiXv8V822lHbK_8*PmI0*|XMv7Ae`v+>F1@Nbq1xQUBaW_0H(qAuHIPm@wcN8~odX zj$&vpp2~4HhUk^8+PoppqC}yiZTju}ZJugS<4KDajVowYtUA`B zvseAqnT~GnB5*oOG*`gj5@KH^rM61WJXf8*zcsOlNaZvZhx;1?tThTIAVtPxf}f4`N%FT${1s>is8NMKjvOr4!$PIwvaYb@FLCy_P^bt$!2( zU)zT}z1qYm&^vh3u)86;l5fKS8SANVu6xWa+nQCL_hS_vpl0oX?!F#gaGVAPG?(QB zEYk`dU6(SmVtDt6TmL{}cenE~)J*jm^-n$Dm>C<6u|IFT0JytgkA(U7uKj&#TgcRt zGEdyyFz83BRp!P_#X^~}+ZDt-3eoyoAB0si%uj>yrUCP!YuATR6D5(plE&0JH=Pc2 zKbV+IpE_YRpZ1N{Ph&Fvd|Z3%xMO9@F3nU{KJQ=Ut>9Tw-x{+-y^nQ9SU%LfyolY| z?uZ~?%YSZI1WqnIKVBrtH+;388Q!VM@PZYEGS*GrT$c@f_QDna?sh!eUty=2d_yUe zHo&V+?z+6HaS_s(pNQ5I6c%WU^yy?VUow6=((p(=O*LJ-?T#m2x$;GRbJ@Z0SN_WR z4GWpuLf(w#o$s!G*;as{MB0`jl^!4Z7n z5qzMZ0Q&^^Cm=on`3a~`Kz{<}6R@9v`viP*1RnxFDnRjB!R`x-1GBhQVZ=h&4QDMt`@eNg*5;+!WZvW4|6AV?&kjRK|38qAEcz*ey!IpJ-C-vXBp62xXlkWSm; zaN>$b=8=T_iNN<{05QRDJcJh&Q?sFCp&mjfhcIo~zr$Q<`lvh?x=sq3J=&&0^;X|0{ zAQ`Y^3@Pg&kOxr1L)AI6Ky?G;{FBOjg*2aUju`4bsMD_)u;@I>3W-p`i{Q7`6MHft zwmpRKbvy)d2%EgIpxkKM8V)W1_a<%_8$VwGA$M&L1aZEK0)r!~Tmc_R_p={_^!|8J zwl}gkhw4625v?Mh=xZX5y*YA{47-x?$&;}+xT|RrNr`+YS(=>mJ%$tk&xK4f7%c*E z&<;0TNi^;jcvGsjCpzebLV=egwJn+@Zo+F;pjS`94DU;^n-hZk!P_Mu!FLQS5jXta z32Erx$>u-0KBOg4twKA_I)u3b=O_Ou*^t@$Zj5FA?yLXEdsMmU0r5n_jl5^Qxl_8y zFk~tL5+WXV;Q!X*+qo%o=Hw`*wC0XC`{1OH68+0)g zg{NK_f*@v324PJ-JA@bnbZ(8NMJ6dSPgU?ru9n2t%N_dYhdqNOX1@~*zAw6qB7BX1 z2=f-W)3Dl$`jByUR1q2ZE?Qv}S!qJ=X5Zo7l=J4m55}?`LAWj8CbXWd=$jsk$P^p? z9xT!}32mQ|Qtk}*!h-dVKeovIynkzLV@Hb-6GmHB#FXF2EI z*nD)B9I?cV1nM0Dq;Uyf;%4vawg;nWTm5_H_D&H2?#(w7><{~CKnMGc`Fd;U13vD| zr4~w1tG5eBfb=X<7>UFtJxf85L|}0tpVS<4c6#gwuqybN`I!i0jXiFB!O$0SVS%CiYmH zO5pps6yg{9_7FE&kGVkwo zrq|%jg)gh~~}Vzgr2lU*4!IRAZb0B|E{*f4cP=6}>m2 zn@L8M_^KU|SDbu`ktt1waH4bfk^MIj+VmFtNAc?<5_%jhWo$(vH>Dl$iZ5^ON24vK z;}308#(t!QMl5=Xt7ah_H`ibXPYsrT%uxftEcI`sX0x;FhUs7X5hPu(SOfp3kZ+99 z1ZcJ%&yH=|=8kRKw!PzLY}>YN+t{&f+cS6HH@Ugrzppx}q^r|izdET@opWH}W}Y9% zC&^clNPIE_7-6U>9CXJ0s{G0#?BlHl+iUuPY03tA`cI-DMg-tV6Mk$Mk@;s6WDY~9 zO0X-dq0uTvU@1gC(C%U*cQcnYPl|?Z*9*ZVuEcCCISeLQRpgzyA!WUuigni7K8MC}HYaOpXB zZG8Wbkb;nL!o{*;zeV^aGGPB^`BhT3NIw}rnJh_ToU=FtEr+Jqe)$D{JN+ilY&-r_ zecb}7IllNRx2YsWR`1|(flsU{W1V1t8D8DE6sYhm>*&1#J3DX!c~^)Hks_wd3fONL zf`Y|+O+?0|mIDX&1w06$;=(6VDu-h{Lcg@DD5i`DAJ6i|33JI2PNdQbR1<;#jl7XBQXniz0l~c9PF0y6>x@k8_b(X7sH%JPI=N?qnSL96I zMj$_e4d}`p0U@<3w?fz!k%D>inaiwq-xcYIgZMb&YLSXy3wLJZ#FS9#f^>2xtcPXvp(M?CgqPU%&}8m|ie|XEKY8D9DRX zjCfGFKyZrCM~+wG(0msci}=%)S?aW`SUJszFWG+(i2#BtIE~!We&gwI~S- zhyf#L;v}qfOCIcetSCFcS$#Qgpz13^3&w2?yQK#AE*43l9}0oQGKLy-G#Heu1*Mc$a8{7A|c;uN2{Q(Q@3b+Z6bGpjsEk1Ot}8pqgwDJpLLSo zC4$=<;&bc@_PeLiX!yG1cbF>_lXK(;%%eiUp4#wCbw3IJZ%@21)DAcfSv z&7u(z>>Bgzv(F4FL2(#XQ`kc&iCthB;WGg;iA{QxIl4Xo{}SChVaqOZnEQUlT{@dZ z5Rko}gu8|Nf{Ks;eV4|0Vki!Yc0b)Ebz6 z*l*SO_5mBf&C98>DN@!r!$qIFhXKM5ivQ*?z=3+n`kJR4T%SE4 z@!9g?+-4UD1mmHfIq4ejGT#`n>ORAqC(l@){--?+B#b;LyjP8-1$$yBE%5hw;Jr-f zy-*kb>Y(V=f|FgMi3;q&uzxVz0BGhAUp3g^+F=Y}YVZPoe`yGej&yYt;pHbvKw%-! zVbpyfpb|aEN2Cng|Jjw`=i5kMz#Stty;HHHf4#Ky%GmM)B%3WvX{LXNWj^cCl~WAC z!eqh&2-2Ss=k1M+qZjC*LC|Y6QIgb#=3>)7%8~Wj1c$73DD+KXbsfSsYX2Sii}4+v zM`ahFCqOSaa?hH<6Xwr=9W=%#%<;!*X?z7;I17A}=k|5ei8Q<)V223LP{25>lLQ4o zcTsM6JRQ_+n`14Z^;1dT>tqUk4u0S{&C&-kLNEbRD>78UGSXj$FPem0QwTZZ+9i{{ z(87(D&+2!FD~lI!2xpWA`U_)Te@u!;bTJAr1rBitoI~=#nF&ff@%#8V#55n=rdgfcRe(&QIgj)T@@3%bX3Y>@#8{2f#YO2=Mr7iSIt ztG^NM(OoLuom&LNv47H$Q>2tB$X;P9s0{WN*HhyV#()zW1VLcu5j9P1s(eyeHGnO^ z-x?fPQlV*LW&#vz0Gmmfc&k1y_Y@f9uYd0PsRNr7m7h+I<*Wq0wIy5^h)9mB+GJc`7!M50aUM(4crSs>vxch)IFt49&r;F_nb%>ZyLzX|6c~WU5@t1|U6};`+n0^C8jz?GT5E3O z68NGA-wbE?2bJEX!4F(<0&7~F>vu+xH!{Z z4so$lKPjQ^%f33EWJ@<-)_*F&ZNzef(Cbkx?;yFp1P>`FFy#eC6;Z)2cat}(ZwPbFgBuq3+`w`v{I^$>sHBp4TpZL)Zf0gz! ze26=L^?H!NV-m@ndm;kb&Jj9JvTumY3FP+vY*p$xY@fYIJ`L7qn9G)NV-U3me1hn^ zUp3DWs%zQ66ZQ!K0j+iVP2_=>Mt7f#0{hisZa;(90mf*c#Q~@+arbzCrJ2FwC;3r~ zim#CoMnohQGoe9e(1W^wINrun|ty_c?X7QYaQ zdINan)H$0pfje^w??Lruw*HxCe-*tjEKnuU5>DMOHUc~ou!P31$dk?|Ar`@YBjJi0 z!l-Jm53@%)z#oij{#p#df>QTuQ0c;$(nyb@z(E_cO%cM=m9dP-zHwZE6p$+|p4+ID zqB$YG2&hJ08gV_d77m~zN-?^#@^GzoD`mdDdCv1^If)e1I4PcHl*e?HRN|#SE|0d2 zU<5KvIWO!U2x3?-%ablmnIOy<^ms%ZrMUv0z?D0%05kXfoOcM?Ic$(n2?22^!(GCZ12wc^ozDs&H}S8|k|O^7SF!zm4@p7iM2 zLa7}jW2D)0ni@ep76%OA9MlFSg%Ns25oXSl%7H*#b2ws0+pvmL9PTt zYi3gW0PHXt9NPE28p0NP%(=6gI%!;3&tWjUoq^_W`YjvEL9L+1lHdWCU2eZAcAFK5Fu!BFE(0R4wnta;=zm?WhDVo5k5esu z!~g_mE6#`41gf5P&oPLueh>7G^Om?ouhv@!TJ?|M{QLQNkt>^p$IyjZfgRbo8sML| z>JiaZt?<2LDmusLr7eHAp;2vBdOAs|Xot90tFS9RSF8BeDms_nr~-O7OciU<1m5nz z|84>jS~I%``7HO2orc*hcZ|chH4^p!7Hilld;rPu$?5{43#*gU>-#QQY);KoUD5l7 zd;pmcU5f%G8nlrP|FLES%i9(Nq)Xs*=V#3)z_|Jp1_#obHibhIx^{xNIuA(R=+z`^iC&Oi9SrzxmxOe zH{&F9a(F3COMwEv{3kfz4z1KG-h$7-iFGKWjbyTBK&@_9mbJnU zTWBfvsX*Jj3Oxo))YRGSio=H_Vva@35r6@@pv*TS(}Pr(zx{02;B4MYgDt5M+VA|q zyLwB)Qt3Qht1X4+P1Gdlt@aF_Z|cP+fVp)7z01;AkioWI_y8D}%qDAX^>pOy2HAot zHNA^ULeIf=xrCNR^V{-+pjt@qI`SH5qAtfpt~ZcozV4ipYuXT!DC6jNsW{N zlu^yqV@FsPj}+img_bU=A?kzgVLiq?f~!=V6r%?v?PR8BBVWg?sBuL>Y{HH^xs;`N zL~CcAOc1f9WGA?V?a*wNT<-?n@+1~X*vC{^paTze*)z!Z^=q54Eid?+P8k$~vBb+D(_ld-sT!x()p$L!>`$5ICu9O4vVEF*k90iZs z5VXF&T1nJ88&QhVzd-T2x6y$e6aou7Rgoh-%2swfR00x`f;q3^deSs-Eju{PQYwb; zHqYz4kWqKZvAxINXO!IvWeLQ^_oZBite^LIKo9=sT+w=dXa^2gGP`YPhcd20hj6Lb zNj))ANEo0|;jJ;VwY-txt7t{fn9*ZDvXEJH4zEt!v<6Fk%m0dyD?i;ytO>ML9Dyd^%znCJIF7s-H-m#AY_#Bb5IQ~5`I3t z762rbBjVs!3_KbAbqQ21ZH^6z+h-p#20~3|gtj3BqJ`Z%#-V}L488b4wW$K>K5YPC zrC{Hle9)&0ysu3Kx66xP_mb!46;FLx3fZ&mY71CM{DgBS%+&RTv}y`=(vjdp6JAxzVFF|?Nw>S?>bG&`2;8LD;+ zB(P-S3y5%NbMJ^CTy0_3mCy!wJnhYag2dL2$Ds4Y>Gsbm$UO${T_)adZwKkLdH|pR zoemJVsN!gM&m+lREL4GP!$dRn^teKiYO7l0hLoYbMN1=&Vch|+>j~rbT#Qe)y|@9P zb(k!{Bo@W?j@S2sk1NsP$Qkwd&1=|!PDE|93i*0t|Mcj>zuJ_JBpQ#Y%2UI+3QIP~ zCV1cu#0p4sIR!ohjcQz&0lR{jFaTS}<#Z#1stSmuvvC+S$%FIWS5%LtPrMY(@>g|H zdX|f>N;SBshb;p*A==+%#3D8U0P}o-bFj!@=vWCJm96x8zLK7o0}jaSOAY!dP0Q&} z6Sm)aI4SBM#`eS%bhxMPzm9n=x?AUvogj`l2+SmTRG9{G;qn=Hp?cfQ&8m zKol9=nj!;h5#2Urbq|f)JOhYx`L{hVojAqc{xP&LP735K`n{jPD!36~P94Zo zjyELD+%-A9hM`m8U=aTC@}B`O<>9m}imocAuuV*+*h^2*z`uMq7Lx!CzOgWI&dZqv zc;E1%;iA|hXI0=9{6lWVLk@j$&J`=D-#R4w6-Ah#19894G0>#r#)`woG_&ru%@8XF zk5pL7X8B6M;5{h!96g`mF-a=kw8SB72WX|r-Xy-z*KvGI`Drk~;yFD1x+35IHx#V28@t6Vud}+%U=e zIMOkGV zRBrC@O~17o#zbJ(QD~fe5UhYBnpnF|M$-)F#1m0*NOP+HRUfMZm@TtXNG>o2>Jcst zrXL3-r)QWe|0Ekmt?%BON3DHBwDw}tDqE7$I=pEuT)hCv``~G7hS$hbL9@t_j1Y%( z!h9z5L-Q@?>xE?K#yRMxX&FY+*p4z2xZ&aorxQt*Q|y0C@tVj*P2HqAI{IrET&Efu z76Rb3c~haI8MeOIde{*U^%*ijt5TG7XqJ;4FkUa;rmWkgujr4O{g<`Q zTpLq}U8nKk9rS@gKEi~#JcDMgi(y;ty2UB2L^}>st9Zh8K}nkPKd=bj+uEMEQPc>% zFSY2G2F_dq_(LK?!#d9xqDRT{3v~cI7M!PE_bWhO7W!@SnZAmXsvn6fA^9Ex9rtGc zHN`V9`(N<;0+LNQNPVw`qWjUXj!UOvFC6~{{qgwSc5ZIvdH^WT8|otd;AUE`G$**b zFlw&-7~=5}QVvEma}AhBS576ZVyJ12Lml4;j*9*wbm(`cpjA39T~Wp)NXuR+m;_ z44SebW4mHaP23CeG|&t|p60x8?^6~(ygFYKnn^;3>g>txMBZ4)$FzF?!+HW$3K2LC zt+PNH`mx=C$Fw+fFsWh9%9BG&KXxx3&`{vS7=H}#%f#zoM_TKIJzIvM5O@!A_V-diF z5Xz0z44S=rq*keOW1;)o(Sw!Ke2dsEnlUA@M^450QAn7OZBbYgW-NMp&rsj(=$8$g zrs&wOZnWO;YWV*Czcut+foQ2MdIs%4lB{TkNyq=lMur#weYyh5E{L&S+98Y~ei+Re zCEpHgc_%*F3(lm7j4kEHEY+(!=sbYtRqa-Dfv)ZJ7<1Lx#`y{Kr)OU24^iR^Kd2R z%rN~`R`=WtcFTcDERyn0ZJ*J$Id{;>hrnagb$_>ef1Y->H8XBn%_^dUt5O0yG~cSJ zFAjOqjrMr-TEw*|0mf>D6f8<|sy@UiY(}P~GMJRq=W`3Ws1NxA%uqTPW0X(*-bWEo z*gzN1?6Jw2eH+%S3+WXWiWqBOV{>_bW9*I{@Jp>!Sxv*CQ^((OxKx^%EK;rdEJpiM z57FAIq{4Uaozuo_6ZizSSuj81n#g+cigK;Ciy$;Q)11>+>A)Ej&nC;0IMD^wg{Pxj^A~VgBP~S}#xQJS7^e7Rp`xg%@9tQW- zV2Wijf5WTrA*6j6^spAlnjpIz;yZhfNqb?PEQTut)w(Vc5;Xf+sw>qHX9d5q#0)d| z-yM^2KDF!ZtbF|gs04^JnVjy2WskMWD{G36b-7-80hk$X6aw+Bt|S5gW2nB#Tm#AG zPr0gN>ZE_oTl$L%5CZD!U1HIgY(82*xbItDT*y;~&k?)OvTibXhMCOw0ro1G@$R$< zb5h_4*~^ByEYz6@sEViBYa%G=&a?h_n1*fXKmAvOt$*N*abDQ%ezGQaE?fpN7i)B@ zq<(gpfBlbkFxtwOs+mhhX8OTo4-yL7Z)~?ESxLvHB1-ERRu}szKm7b!S%|IQRL%_( zg(VIfI~FjnhN9Sc1>I-d!B%a?r53C?e*Rd4F;N8g(HGtkEGGbok4*MEE_I&piT(M% zwRE!Gv|tDHM7o%p3U4__4B#9EUqTnYh8`LVfK$D`0vH7a=x!_8^H{rbsF4%c&SJLy z&cNJUUAeASOW|xa&*XBzYt<5x zrAE!WrJNT6U*9)vv3-X!`(s9L>YoDo44>`mV7H|OtU{a@@ z0JLs|^Fl3C)hR)uuFIB|b;_-Nph8hHtV!28l~0f8#?c#R{AI(ykyWTGV1{sm`7%RJ z7{y{e1oN4%?`Fo<7jL|&WK~=#-nPg5+=aDtFJW;Ba;Zqo{-=X+;`O>4aaCeEmw#wL z=~OSk>GO}+CjYya&%)hrzr$2*gt=ufyFt*Me&j8~dr}p)vLM`%g=(k%f_`X~gT(k6sAQzT=j?io|(>S!FOwa(#c|In#2XWL~#hB6=9WV!(% zX-{W@EM>Dr6LhbE`!r$Jq{iRufF+d7A;RN~(PJLj!Ph&n+j9s8H0-5ZI;Z|>`%A8n zYFY22Y3+p&m(#&oclR*~AV&f>i@%mEx?DdgJcAHT$WJ8GNRty189rdTnOZ;J2nrdG zkz3{rPtXagDu|LD+rLlX?!uZ83f*;F;Vn?Q(Upo4txG6}cg>rf!)Vr?AQLoGy|KzYn!yR2tBpeiqt&`}q1oNJ0DITH)vlJ!hbuZR z+08q}3n6XBN1USw$y$y#(A5zf3vB3E4@O)*G@PJq1vix$F+i=fe0{ELr|g7CiZr^I zYuX4{$$|h`ldl*dyh`Q!4|X#^GVa!4FZ@=zrzv@wk+X{e!QNir1x#PZO(|L~b(7sp z=^I2FjslZ3ch!9eAS*23ErKKwukEC9fFCy%@=VS4tQ!6!&X0a zl^*%=*Q-kgk4Rww1V@#Z16jK0rBZlS+XCw^U6d$m|8=+%ybEoNm-hU~YdUPF32~Lk zh-((}t?SX6q1kAlXrip32Zc3|NsUHJJOl5=0Mrb2df(+PK(eFj4M*^Mci&W8C*2pK z-|7dVe))35qJGbX@Nt&%hIGi4YZ1p62$`9TuH&@sF-GC-h%RWkBk4K%X#7SH@Ks}> z1REI3Bw8cC!(UOdv)bmn8ls3DD_b&+dEF0S$2Tn(vQ^#Dq$4e6_3$qEck#8Z-{%`7 zL&u|>Lg&OafId%-;V=^R^c89;Yw*5k;`IXP2l*IdaGH^W;1*D`#maL&iy2V$taCVl z(-Jxo0%4t=(2yER;}54MU1$$F1iz$rf!jH{=w5P&Dqc?AouEz~nc?vb#8jUn6E6p( zc!^G>ah3)5JF{iV^<7>$j58-XzWc0S`*>Gp5|RB?0EkUl+P`u$sUtKIf5UHsvPC(Z z2fxeC3vswiE(dOnRt~(Lt|a7);{Pa1#MP%#(<#fvSP`mBxY!UAP3-y6qbUb+pQ46W zV;^{Ycd2i#fq?cGVrQaN+~Zvfn;#=92ceQvO<=(Vi_~B%dLg8X-C6NF^BV1kN52fu zH*21d0l-}Y@TDUch6Lv{SU(&n3XG)fm`L>FESI8*NdlkPb^nCor)#QM+}c2&fppk1 zC9`mSPd=afUP^*kmA0yo0v|c$$nuzM)qP1*8Qa)+?zc2Ipesf^OhojU2tE;~i$nJN z0j8++!}*}&{F?f}rt<)6@6Q28_W=b=_$yQJfJl%MH*10>1dCZ_Y?jWe|DblTP;ZI- z;7NcQHCR=Opl<5*b>$v%nMZ?LBea%DCjN}L=vLF`QmCe>8vhP&FvOm!6RY1MA(5!YBINchL~tTQs=Dp8T5bn5sWhK4i;Bfc_ZZ!2CWr1??(8+iIQ zPJJ_Rj56L$h!^Qb1i>*=#N`q+wzIB|9u?PY0S5fMcBcjYJG5=6>XK=-s2%?p1Xw zFff>NR(`(VD+JrGNA$^H++WKW1DsHwZFE7-JHX1wKx8_mnR~vhP6RIx=z!qY3qaMs z>X*UEBiXtK@I#kR8LIm$Yb^Z$r%2LH5=rlU`(Nc5twd(4iwO!fnRd+K6EY9Qh;`0V z9v$@ky=x!+lIXj!7-IO~b7p)ug8OJXnJfg(9>^i$)Ru21kdM)E^-#FIytk3{sK7+%Be+iN&SO8|Iw=H&LP-k4^3S z?&!`=mi|OFCiBUm112I@zvH9Ie46B#iC^NW$q>G#uqA5g%Mteb!K`6nB0J?1uWxv& zM>Bo#_F!5uTJxxL6r*fg*dl;pfmI5+*gB*h5Ym!uI^ofr0JkSeck`a^NbRzkdGI`> zS;j0b83(xY$98l`lm5y82v9+mER=^(EE8;Za@oB{Xv<$cAg^|DsxWn){YPUacmZg9 zy62e%A~<-0>jVJUXSEqCH&)rcE9-ku!HtOxhhJ}+0@DX$S*+E|-GPO+p@OhUPiJpN zXI9b4*VWgegHTk6zSe1tnpPSSV9t2?se<$zS|M~}v41S>!6<40a&H_-M=6o@_Y||x z0jOnXIemZh{BYb}h(HkvH+pr(#c`LkkBz)^R5mwwR#HiO5`wB1Cyx6FLJ#%=a0~v7 z^Oj*RKst-7Zu^CdNirL;wX<=MrPdJUk0i_l7vCI1j~tT#92Bz?0lB&gaKAj@Xg?LU zDy@k`W&6JRIWn~Xbn-5n;M?847%nCQh+I_(F3?5U-I3-jPKph4`Rch0TmwJtC=TfI`|ZMyQd_L{ zw7!=bCcx$YW;ZVz9o!h1jY%61;vq?j3x5yN6YQ(%kfLS*GJV|yvoiIQe~0T|Ln-8g zK$tbgL?L{;G}v9;*(raRXQ&9-3NBIgd_%y45-telpBh>Dn=K%UZTwdkDYu$ZF_!{5 z?47qZ=sQ51$5MRfMgf)0xJ!Zr^+n04e4xgBlf)7`Au6i|33+yX%cSSf?iH7{aa=t_ zx`~^a zUJ)H&J=`p=H({#I+M_X|1zAo=o(-j>jY6k>KhiM2(mT9QgfibL(z}=^B5A zWyA`&e6S)tPRmW%t$-`sSe@ zc;{tpqkDX#Y(1F>VcXv*J%$?88$AKWqv*j+|M2WBL@$D9p~z=qlp_+jx5IF2f0XnLA#ZFrV(m zaUVFza41zOr!kpy0RnxSY!qs~(jGo4A<@~$V9adH?mtlfMc8^8@0h05h_(5h2sIAy z^eORz#)}{sfwxaO!mTNY#EqTkUEce!>t6TU!s)tvcyRAXQ)X*8Vx)ZYF)n%2yf_Cr zTE4+-X#SWxo=O9Re#h+`ZD#O5BBR!)Af0aT6r_+gk_mD=GnV!)h$L%%X)?@_hGi+Z8#dt8ZF5`D8)Ke(WXWZ) zCz8}+lTSMcgBtN7JrKE)VNGK8KO=fZv~Q_G7_5~Y6oqnBU{|84vp&RNTc5~oNXVqV zGMqK44{KGOg9R+F1$UCX`}IklDG06eHmUm_n{~m{-a8^^!zUJVgSQc!@!tXZa#7Qy z@f_4+6`x%_=4J>K7RF|a zmzNedQ7Rt)%1kEjrk*Tj11-a#y|6bzGnr~NBY^vDDZYeIWn0v!i#l9S{B>k4C*(Jw z|C7UezEY9*xe@d!cN(Cn)IA8$WMu9&-H>6c{ET;Q^i0nF?lwF1cHVn{YiH90`HNlL zw(O#HsGFM;}M25yuYI<6k6sA9<$rW^aD}9 zCyn(WddFz#ndbM6H!XUfN$=;vTT0(H@OPEAGRP=}t8Bw9_qQo~5FiMETby*fk#`~1 zCx-0MI4)^2%vGmGU!&7Fg^b;##PBd-nrt(p@UE-jQK+_>sKUQ`h<$iO-u4noj`>R>3h&CwpNmQq>nN`nzN|IT#T}Tz`viRWsD*0UUP>;6(8Y+dB)e!Zvr7kwT z6?sdbou6JHF^W(dku6nd%UG$*G$QG zE^2F~WW@%4x?h#xaL}}s1IYBN zPRaR!ZWpsLKyS2XK(y^MpjP^okas{TiBDHiVf;yPq1x`EO89H{jk@`Wwa-2Xtp~P;X|GXXI`WaxT~H3OhB>#a6RwCAMu(15zcNxiMoI{+~{B3f(cA$0h0t+uif7xT=GCZ#wfQ z?1l45dDi`f00|$2R5$iqN#y0&87(U0NXqzX$MH|?&zQazzT53zL~C#Vx|)9!Hi01B zqVN?sseGIP*m*y*h509XH*snxI26JIBNNFcZ{6Y(&L<(1oi(WuRmqI}QdD&K3&K`( zRf0nuR?_Vw=9!DytSck4)ooSFvF4TSHf*WY$M?=w^r9yZOcm0=o7LC%+H<-(e*(V7 z?bW@A-t5a5p4ZvFhAFP4sTsBEt>cNf@yl!csOp9QE=gyyKoq+af%Y{RckWGT?f4WB zCEUx{UzYQkN_e#DQkn+{jVmp7cWVrKm)BPGyyk3mBIk6#_%pm=VGK(0=j5=sXnIWI zN7B{?8Y}P0QoK{WTD8_1`nJM>Psu&3KfkF~bmuPuo6xO3-$vPPlyL-lCc4?qT%=kegbR{Q z{D`86DUZiu`wGD&Vl|HG=uUJ)h?GN7-UUDa=Dk;k&~uA)0;r$lw!aLskIulX81s`C zeO?)*ji##mm-EJ7_t4cobLjQ@4^(NZKsx?X-pZ_+K?Q-U7|8*H`%QbGVOVcH6@FkM zH1wl&u^!$*b$H^dPz_U>u{3YPJG*iQv}yJ;uQgS;=KKL&Iu6!dq=&{ugkK}yRk6qe zL^Jr8^0?w1Kl!{Ad_6aKJ#SUg>yC>2`!h*QVX5GVh{M~C_h>1ZxO2O z^CwZPY+6ayce)O{M~H}8mi}+HX$^-qOWn4o{nXQeS3!Sr;LzvBa`nTBEv@Jslry>` zvkCakObXF52xYM?I+SRxU5L`FFUWQQB7CeGpvQLx+~`Xe8G4~8lb6_!oZMTBTKw+y z8HQpv)u~JqYHTAmW1=Ce5fEu`T%)fEy*vZO4Z~)FCfi6s8K2CI40^^(4TT(DC^Yf0 z=a;%XSGLsqQp9iW<*t1E1Wja(;U5>wxk|4;*o^hU6T7*`10-q=QC$Mmv+0jdTgy!cP$;MW4<&LrDmo+Gn5d~O z4miro<|kVN$y)W&iK{h&L}j52_HItIjzZ3-7=(lUN=O;M)~|)>xV_X-ytpaj zV(Hl@%xQH@mY5Ppw@}S>?-6?V+wlXZu-o67n41`kwl09h08{$xux}&4HiEUfIq|5( zU*DFrJU3*WQb|tikOJg1bE|tO$D5kjkYigFGd{EG?k2RFx7MPlBtrqvpU>U2Mj*8j z_CEI}w)y+4VMBimO*yCoA5y)PUPOfNgB^<`U{j2+wv&T1lw9dR9%4)ruB{F5M4yg6 zo_onLI$W>T;8ZYzOEM@xL;b_^@Y2SETA*{7TD zb_Zwv$(9QR?2v&Xn}?RjTR|^t_eBo7^-(deCh3|#!6^mf? zN9XFgujD#sXLi#>Ks?X4GR?A4(*wn z`=GQXNX)HRElMzr13-HHpx1o@%g^Ih6a70#}(5!RyNdTAfbbxAg(>>^XM z72PP4`kl$#`9l+cxnVZLDEZeF{?6&^lfV+cp0uY{zo%}<9z854iry#^fkRA-$s{!jEWn8=AGwMzmOnD(yLp861Eqx z|4;+uvFwX~+>4b_cLQ22ih3atnn9oUg zms>gwh+Bb3vum|$2FHlHL3^=yV8skWNyP4~l}zN#h$6y%Zv*b&qpyU}sB;QOb#ddFYX*Q;oF;=;a>eN~fYZr++} zj+h(6_m2pysqv~&6f}E$_X78u(+ZWyy;!YGP7i9*wd)sHA5V1JrFJJm;eP$r5Wyt! z9&EIrQx*grkE*iBKP>91?it5Ru&(8Jj&L!6O!Y^+;^{+Ol?e*p-R7ht&W!r2Hq+-#(-YqlY-)hEUH>cE};(1M8Ur+Op^ zm!76+tqZS`axXnd4I>Ro8FK9e3^nr6N|O<#GjICAqT_fX2B`g?Z~p)A*U$7W3;u%h z0s-Z?rp5)s;-(74z_LOAA1yxBKL%D5s1Z|DVOk!2W+J@u|IWu=GR?5wJim7N&+SPNoXd;1K_L0_xuq1OWoN`*#Ec^gk~H B8F&By