diff --git a/Moose_DualCoalitionZoneCapture/Moose_DualCoalitionZoneCapture.lua b/Moose_DualCoalitionZoneCapture/Moose_DualCoalitionZoneCapture.lua index e70944a..f2e70cf 100644 --- a/Moose_DualCoalitionZoneCapture/Moose_DualCoalitionZoneCapture.lua +++ b/Moose_DualCoalitionZoneCapture/Moose_DualCoalitionZoneCapture.lua @@ -1,6 +1,21 @@ --- Setup Capture Missions & Zones -- Refactored version with configurable zone ownership +-- ========================================== +-- MESSAGE AND TIMING CONFIGURATION +-- ========================================== +local MESSAGE_CONFIG = { + STATUS_BROADCAST_FREQUENCY = 3602, -- Zone status broadcast cadence (seconds) + STATUS_BROADCAST_START_DELAY = 10, -- Delay before first broadcast (seconds) + COLOR_VERIFICATION_FREQUENCY = 240, -- Zone color verification cadence (seconds) + COLOR_VERIFICATION_START_DELAY = 60, -- Delay before first color check (seconds) + TACTICAL_UPDATE_FREQUENCY = 180, -- Tactical marker update cadence (seconds) + TACTICAL_UPDATE_START_DELAY = 30, -- Delay before first tactical update (seconds) + STATUS_MESSAGE_DURATION = 15, -- How long general status messages stay onscreen + VICTORY_MESSAGE_DURATION = 300, -- How long victory/defeat alerts stay onscreen + CAPTURE_MESSAGE_DURATION = 15, -- Duration for capture/guard/empty notices + ATTACK_MESSAGE_DURATION = 15 -- Duration for attack alerts +} + -- ========================================== -- ZONE COLOR CONFIGURATION (Centralized) -- ========================================== @@ -164,7 +179,7 @@ end -- Logging configuration: toggle logging behavior for this module -- Set `CAPTURE_ZONE_LOGGING.enabled = false` to silence module logs if not CAPTURE_ZONE_LOGGING then - CAPTURE_ZONE_LOGGING = { enabled = true, prefix = "[CAPTURE Module]" } + CAPTURE_ZONE_LOGGING = { enabled = false, prefix = "[CAPTURE Module]" } end local function log(message, detailed) @@ -254,43 +269,63 @@ end local totalZones = InitializeZones() --- Helper functions for tactical information - -- Global cached unit set - created once and maintained automatically by MOOSE local CachedUnitSet = nil +-- Utility guard to safely test whether a unit is inside a zone without throwing +local function IsUnitInZone(unit, zone) + if not unit or not zone then + return false + end + + local ok, point = pcall(function() + return unit:GetPointVec3() + end) + + if not ok or not point then + return false + end + + local inZone = false + pcall(function() + inZone = zone:IsPointVec3InZone(point) + end) + + return inZone +end + -- Initialize the cached unit set once local function InitializeCachedUnitSet() if not CachedUnitSet then CachedUnitSet = SET_UNIT:New() :FilterCategories({"ground", "plane", "helicopter"}) -- Only scan relevant unit types - :FilterOnce() -- Don't filter continuously, we'll use the live set + :FilterStart() -- Keep the set updated by MOOSE without recreating it log("[PERFORMANCE] Initialized cached unit set for zone scanning") end end local function GetZoneForceStrengths(ZoneCapture) - if not ZoneCapture then - return {red = 0, blue = 0, neutral = 0} + if not ZoneCapture then + return { red = 0, blue = 0, neutral = 0 } end - local success, zone = pcall(function() return ZoneCapture:GetZone() end) - if not success or not zone then - return {red = 0, blue = 0, neutral = 0} + local success, zone = pcall(function() + return ZoneCapture:GetZone() + end) + + if not success or not zone then + return { red = 0, blue = 0, neutral = 0 } end - + local redCount = 0 - local blueCount = 0 + local blueCount = 0 local neutralCount = 0 - - -- Get all units in the zone using MOOSE's zone scanning - local unitsInZone = SET_UNIT:New() - :FilterZones({zone}) - :FilterOnce() - - if unitsInZone then - unitsInZone:ForEachUnit(function(unit) - if unit and unit:IsAlive() then + + InitializeCachedUnitSet() + + if CachedUnitSet then + CachedUnitSet:ForEachUnit(function(unit) + if unit and unit:IsAlive() and IsUnitInZone(unit, zone) then local unitCoalition = unit:GetCoalition() if unitCoalition == coalition.side.RED then redCount = redCount + 1 @@ -302,10 +337,10 @@ local function GetZoneForceStrengths(ZoneCapture) end end) end - - log(string.format("[TACTICAL] Zone %s scan result: R:%d B:%d N:%d", + + log(string.format("[TACTICAL] Zone %s scan result: R:%d B:%d N:%d", ZoneCapture:GetZoneName(), redCount, blueCount, neutralCount)) - + return { red = redCount, blue = blueCount, @@ -314,63 +349,62 @@ local function GetZoneForceStrengths(ZoneCapture) end local function GetEnemyUnitMGRSCoords(ZoneCapture, enemyCoalition) - local zone = ZoneCapture:GetZone() - if not zone then return {} end - + if not ZoneCapture or not enemyCoalition then + return {} + end + + local success, zone = pcall(function() + return ZoneCapture:GetZone() + end) + + if not success or not zone then + return {} + end + + InitializeCachedUnitSet() + local coords = {} - - -- Get all units in the zone using MOOSE's zone scanning - local unitsInZone = SET_UNIT:New() - :FilterZones({zone}) - :FilterOnce() - local totalUnits = 0 local enemyUnits = 0 local unitsWithCoords = 0 - - if unitsInZone then - unitsInZone:ForEachUnit(function(unit) - totalUnits = totalUnits + 1 - if unit and unit:IsAlive() then + + if CachedUnitSet then + CachedUnitSet:ForEachUnit(function(unit) + if unit and unit:IsAlive() and IsUnitInZone(unit, zone) then + totalUnits = totalUnits + 1 local unitCoalition = unit:GetCoalition() - - -- Process units of the specified enemy coalition + if unitCoalition == enemyCoalition then enemyUnits = enemyUnits + 1 local coord = unit:GetCoordinate() - + if coord then - -- Try multiple methods to get coordinates local mgrs = nil local success_mgrs = false - - -- Method 1: Try ToStringMGRS + success_mgrs, mgrs = pcall(function() return coord:ToStringMGRS(5) end) - - -- Method 2: Try ToStringMGRS without precision parameter + if not success_mgrs or not mgrs then success_mgrs, mgrs = pcall(function() return coord:ToStringMGRS() end) end - - -- Method 3: Try ToMGRS + if not success_mgrs or not mgrs then success_mgrs, mgrs = pcall(function() return coord:ToMGRS() end) end - - -- Method 4: Fallback to Lat/Long + if not success_mgrs or not mgrs then success_mgrs, mgrs = pcall(function() local lat, lon = coord:GetLLDDM() return string.format("N%s E%s", lat, lon) end) end - + if success_mgrs and mgrs then unitsWithCoords = unitsWithCoords + 1 local unitType = unit:GetTypeName() or "Unknown" @@ -389,13 +423,13 @@ local function GetEnemyUnitMGRSCoords(ZoneCapture, enemyCoalition) end end) end - - log(string.format("[TACTICAL DEBUG] %s - Total units scanned: %d, Enemy units: %d, units with MGRS: %d", + + log(string.format("[TACTICAL DEBUG] %s - Total units scanned: %d, Enemy units: %d, units with MGRS: %d", ZoneCapture:GetZoneName(), totalUnits, enemyUnits, unitsWithCoords)) - - log(string.format("[TACTICAL] Found %d enemy units with coordinates in %s", + + log(string.format("[TACTICAL] Found %d enemy units with coordinates in %s", #coords, ZoneCapture:GetZoneName())) - + return coords end @@ -463,19 +497,39 @@ local function CreateTacticalInfoMarker(ZoneCapture) if coord then local offsetCoord = coord:Translate(200, 45) -- 200m NE + local function removeMarker(markerID) + if not markerID then + return + end + + local removed = pcall(function() + offsetCoord:RemoveMark(markerID) + end) + + if not removed then + removed = pcall(function() + trigger.action.removeMark(markerID) + end) + end + + if not removed then + pcall(function() + coord:RemoveMark(markerID) + end) + end + end + -- Remove legacy single marker if present if ZoneCapture.TacticalMarkerID then log(string.format("[TACTICAL] Removing old marker ID %d for %s", ZoneCapture.TacticalMarkerID, zoneName)) - pcall(function() offsetCoord:RemoveMark(ZoneCapture.TacticalMarkerID) end) - pcall(function() trigger.action.removeMark(ZoneCapture.TacticalMarkerID) end) - pcall(function() coord:RemoveMark(ZoneCapture.TacticalMarkerID) end) + removeMarker(ZoneCapture.TacticalMarkerID) ZoneCapture.TacticalMarkerID = nil end -- BLUE Coalition Marker if ZoneCapture.TacticalMarkerID_BLUE then log(string.format("[TACTICAL] Removing old BLUE marker ID %d for %s", ZoneCapture.TacticalMarkerID_BLUE, zoneName)) - pcall(function() offsetCoord:RemoveMark(ZoneCapture.TacticalMarkerID_BLUE) end) + removeMarker(ZoneCapture.TacticalMarkerID_BLUE) ZoneCapture.TacticalMarkerID_BLUE = nil end local successBlue, markerIDBlue = pcall(function() @@ -492,7 +546,7 @@ local function CreateTacticalInfoMarker(ZoneCapture) -- RED Coalition Marker if ZoneCapture.TacticalMarkerID_RED then log(string.format("[TACTICAL] Removing old RED marker ID %d for %s", ZoneCapture.TacticalMarkerID_RED, zoneName)) - pcall(function() offsetCoord:RemoveMark(ZoneCapture.TacticalMarkerID_RED) end) + removeMarker(ZoneCapture.TacticalMarkerID_RED) ZoneCapture.TacticalMarkerID_RED = nil end local successRed, markerIDRed = pcall(function() @@ -516,16 +570,18 @@ local function OnEnterGuarded(ZoneCapture, From, Event, To) ZoneCapture:Smoke( SMOKECOLOR.Blue ) -- Update zone visual markers to BLUE ZoneCapture:UndrawZone() - ZoneCapture:DrawZone(-1, ZONE_COLORS.BLUE_CAPTURED, 0.5, ZONE_COLORS.BLUE_CAPTURED, 0.2, 2, true) - US_CC:MessageTypeToCoalition( string.format( "%s is under protection of the USA", ZoneCapture:GetZoneName() ), MESSAGE.Type.Information ) - RU_CC:MessageTypeToCoalition( string.format( "%s is under protection of the USA", ZoneCapture:GetZoneName() ), MESSAGE.Type.Information ) + local color = ZONE_COLORS.BLUE_CAPTURED + ZoneCapture:DrawZone(-1, {0, 0, 0}, 1, color, 0.2, 2, true) + US_CC:MessageTypeToCoalition( string.format( "%s is under protection of the USA", ZoneCapture:GetZoneName() ), MESSAGE.Type.Information, MESSAGE_CONFIG.CAPTURE_MESSAGE_DURATION ) + RU_CC:MessageTypeToCoalition( string.format( "%s is under protection of the USA", ZoneCapture:GetZoneName() ), MESSAGE.Type.Information, MESSAGE_CONFIG.CAPTURE_MESSAGE_DURATION ) else ZoneCapture:Smoke( SMOKECOLOR.Red ) -- Update zone visual markers to RED ZoneCapture:UndrawZone() - ZoneCapture:DrawZone(-1, ZONE_COLORS.RED_CAPTURED, 0.5, ZONE_COLORS.RED_CAPTURED, 0.2, 2, true) - RU_CC:MessageTypeToCoalition( string.format( "%s is under protection of Russia", ZoneCapture:GetZoneName() ), MESSAGE.Type.Information ) - US_CC:MessageTypeToCoalition( string.format( "%s is under protection of Russia", ZoneCapture:GetZoneName() ), MESSAGE.Type.Information ) + local color = ZONE_COLORS.RED_CAPTURED + ZoneCapture:DrawZone(-1, {0, 0, 0}, 1, color, 0.2, 2, true) + RU_CC:MessageTypeToCoalition( string.format( "%s is under protection of Russia", ZoneCapture:GetZoneName() ), MESSAGE.Type.Information, MESSAGE_CONFIG.CAPTURE_MESSAGE_DURATION ) + US_CC:MessageTypeToCoalition( string.format( "%s is under protection of Russia", ZoneCapture:GetZoneName() ), MESSAGE.Type.Information, MESSAGE_CONFIG.CAPTURE_MESSAGE_DURATION ) end -- Create/update tactical information marker CreateTacticalInfoMarker(ZoneCapture) @@ -536,9 +592,10 @@ local function OnEnterEmpty(ZoneCapture) ZoneCapture:Smoke( SMOKECOLOR.Green ) -- Update zone visual markers to GREEN (neutral) ZoneCapture:UndrawZone() - ZoneCapture:DrawZone(-1, ZONE_COLORS.EMPTY, 0.5, ZONE_COLORS.EMPTY, 0.2, 2, true) - US_CC:MessageTypeToCoalition( string.format( "%s is unprotected, and can be captured!", ZoneCapture:GetZoneName() ), MESSAGE.Type.Information ) - RU_CC:MessageTypeToCoalition( string.format( "%s is unprotected, and can be captured!", ZoneCapture:GetZoneName() ), MESSAGE.Type.Information ) + local color = ZONE_COLORS.EMPTY + ZoneCapture:DrawZone(-1, {0, 0, 0}, 1, color, 0.2, 2, true) + US_CC:MessageTypeToCoalition( string.format( "%s is unprotected, and can be captured!", ZoneCapture:GetZoneName() ), MESSAGE.Type.Information, MESSAGE_CONFIG.CAPTURE_MESSAGE_DURATION ) + RU_CC:MessageTypeToCoalition( string.format( "%s is unprotected, and can be captured!", ZoneCapture:GetZoneName() ), MESSAGE.Type.Information, MESSAGE_CONFIG.CAPTURE_MESSAGE_DURATION ) -- Create/update tactical information marker CreateTacticalInfoMarker(ZoneCapture) end @@ -551,14 +608,14 @@ local function OnEnterAttacked(ZoneCapture) local color if Coalition == coalition.side.BLUE then color = ZONE_COLORS.BLUE_ATTACKED - US_CC:MessageTypeToCoalition( string.format( "%s is under attack by Russia", ZoneCapture:GetZoneName() ), MESSAGE.Type.Information ) - RU_CC:MessageTypeToCoalition( string.format( "We are attacking %s", ZoneCapture:GetZoneName() ), MESSAGE.Type.Information ) + US_CC:MessageTypeToCoalition( string.format( "%s is under attack by Russia", ZoneCapture:GetZoneName() ), MESSAGE.Type.Information, MESSAGE_CONFIG.ATTACK_MESSAGE_DURATION ) + RU_CC:MessageTypeToCoalition( string.format( "We are attacking %s", ZoneCapture:GetZoneName() ), MESSAGE.Type.Information, MESSAGE_CONFIG.ATTACK_MESSAGE_DURATION ) else color = ZONE_COLORS.RED_ATTACKED - RU_CC:MessageTypeToCoalition( string.format( "%s is under attack by the USA", ZoneCapture:GetZoneName() ), MESSAGE.Type.Information ) - US_CC:MessageTypeToCoalition( string.format( "We are attacking %s", ZoneCapture:GetZoneName() ), MESSAGE.Type.Information ) + RU_CC:MessageTypeToCoalition( string.format( "%s is under attack by the USA", ZoneCapture:GetZoneName() ), MESSAGE.Type.Information, MESSAGE_CONFIG.ATTACK_MESSAGE_DURATION ) + US_CC:MessageTypeToCoalition( string.format( "We are attacking %s", ZoneCapture:GetZoneName() ), MESSAGE.Type.Information, MESSAGE_CONFIG.ATTACK_MESSAGE_DURATION ) end - ZoneCapture:DrawZone(-1, color, 0.5, color, 0.2, 2, true) + ZoneCapture:DrawZone(-1, {0, 0, 0}, 1, color, 0.2, 2, true) -- Create/update tactical information marker CreateTacticalInfoMarker(ZoneCapture) end @@ -591,13 +648,13 @@ local function CheckVictoryCondition() "VICTORY! All capture zones have been secured by coalition forces!\n\n" .. "Operation Polar Shield is complete. Outstanding work!\n" .. "Mission will end in 60 seconds.", - MESSAGE.Type.Information, 30 + MESSAGE.Type.Information, MESSAGE_CONFIG.VICTORY_MESSAGE_DURATION ) RU_CC:MessageTypeToCoalition( "DEFEAT! All strategic positions have been lost to coalition forces.\n\n" .. "Operation Polar Shield has failed. Mission ending in 60 seconds.", - MESSAGE.Type.Information, 30 + MESSAGE.Type.Information, MESSAGE_CONFIG.VICTORY_MESSAGE_DURATION ) -- Add victory celebration effects @@ -632,13 +689,13 @@ local function CheckVictoryCondition() "VICTORY! All strategic positions secured for the Motherland!\n\n" .. "NATO forces have been repelled. Outstanding work!\n" .. "Mission will end in 60 seconds.", - MESSAGE.Type.Information, 30 + MESSAGE.Type.Information, MESSAGE_CONFIG.VICTORY_MESSAGE_DURATION ) US_CC:MessageTypeToCoalition( "DEFEAT! All capture zones have been lost to Russian forces.\n\n" .. "Operation Polar Shield has failed. Mission ending in 60 seconds.", - MESSAGE.Type.Information, 30 + MESSAGE.Type.Information, MESSAGE_CONFIG.VICTORY_MESSAGE_DURATION ) -- Add victory celebration effects @@ -673,15 +730,17 @@ local function OnEnterCaptured(ZoneCapture) if Coalition == coalition.side.BLUE then -- Update zone visual markers to BLUE for captured ZoneCapture:UndrawZone() - ZoneCapture:DrawZone(-1, ZONE_COLORS.BLUE_CAPTURED, 0.5, ZONE_COLORS.BLUE_CAPTURED, 0.2, 2, true) - RU_CC:MessageTypeToCoalition( string.format( "%s is captured by the USA, we lost it!", ZoneCapture:GetZoneName() ), MESSAGE.Type.Information ) - US_CC:MessageTypeToCoalition( string.format( "We captured %s, Excellent job!", ZoneCapture:GetZoneName() ), MESSAGE.Type.Information ) + local color = ZONE_COLORS.BLUE_CAPTURED + ZoneCapture:DrawZone(-1, {0, 0, 0}, 1, color, 0.2, 2, true) + RU_CC:MessageTypeToCoalition( string.format( "%s is captured by the USA, we lost it!", ZoneCapture:GetZoneName() ), MESSAGE.Type.Information, MESSAGE_CONFIG.CAPTURE_MESSAGE_DURATION ) + US_CC:MessageTypeToCoalition( string.format( "We captured %s, Excellent job!", ZoneCapture:GetZoneName() ), MESSAGE.Type.Information, MESSAGE_CONFIG.CAPTURE_MESSAGE_DURATION ) else -- Update zone visual markers to RED for captured ZoneCapture:UndrawZone() - ZoneCapture:DrawZone(-1, ZONE_COLORS.RED_CAPTURED, 0.5, ZONE_COLORS.RED_CAPTURED, 0.2, 2, true) - US_CC:MessageTypeToCoalition( string.format( "%s is captured by Russia, we lost it!", ZoneCapture:GetZoneName() ), MESSAGE.Type.Information ) - RU_CC:MessageTypeToCoalition( string.format( "We captured %s, Excellent job!", ZoneCapture:GetZoneName() ), MESSAGE.Type.Information ) + local color = ZONE_COLORS.RED_CAPTURED + ZoneCapture:DrawZone(-1, {0, 0, 0}, 1, color, 0.2, 2, true) + US_CC:MessageTypeToCoalition( string.format( "%s is captured by Russia, we lost it!", ZoneCapture:GetZoneName() ), MESSAGE.Type.Information, MESSAGE_CONFIG.CAPTURE_MESSAGE_DURATION ) + RU_CC:MessageTypeToCoalition( string.format( "We captured %s, Excellent job!", ZoneCapture:GetZoneName() ), MESSAGE.Type.Information, MESSAGE_CONFIG.CAPTURE_MESSAGE_DURATION ) end ZoneCapture:AddScore( "Captured", "Zone captured: Extra points granted.", ZONE_SETTINGS.captureScore ) @@ -712,17 +771,17 @@ for i, zoneCapture in ipairs(zoneCaptureObjects) do -- Get initial coalition color for this zone local initialCoalition = zoneCapture:GetCoalition() - local colorRGB = {0, 1, 0} -- Default green for neutral - + local colorRGB = ZONE_COLORS.EMPTY + if initialCoalition == coalition.side.RED then - colorRGB = {1, 0, 0} -- Red + colorRGB = ZONE_COLORS.RED_CAPTURED elseif initialCoalition == coalition.side.BLUE then - colorRGB = {0, 0, 1} -- Blue + colorRGB = ZONE_COLORS.BLUE_CAPTURED end - + -- Initialize zone borders with appropriate initial color local drawSuccess, drawError = pcall(function() - zone:DrawZone(-1, colorRGB, 0.5, colorRGB, 0.2, 2, true) + zone:DrawZone(-1, {0, 0, 0}, 1, colorRGB, 0.2, 2, true) end) if not drawSuccess then @@ -834,13 +893,14 @@ local function BroadcastZoneStatus() local fullMessage = reportMessage .. detailMessage -- Broadcast to BOTH coalitions with their specific victory progress - local blueProgressPercent = math.floor((status.blue / status.total) * 100) + local totalZones = math.max(status.total, 1) + local blueProgressPercent = math.floor((status.blue / totalZones) * 100) local blueFullMessage = fullMessage .. string.format("\n\nYour Progress to Victory: %d%%", blueProgressPercent) - US_CC:MessageTypeToCoalition( blueFullMessage, MESSAGE.Type.Information, 15 ) + US_CC:MessageTypeToCoalition( blueFullMessage, MESSAGE.Type.Information, MESSAGE_CONFIG.STATUS_MESSAGE_DURATION ) - local redProgressPercent = math.floor((status.red / status.total) * 100) + local redProgressPercent = math.floor((status.red / totalZones) * 100) local redFullMessage = fullMessage .. string.format("\n\nYour Progress to Victory: %d%%", redProgressPercent) - RU_CC:MessageTypeToCoalition( redFullMessage, MESSAGE.Type.Information, 15 ) + RU_CC:MessageTypeToCoalition( redFullMessage, MESSAGE.Type.Information, MESSAGE_CONFIG.STATUS_MESSAGE_DURATION ) log("[ZONE STATUS] " .. reportMessage:gsub("\n", " | ")) @@ -856,13 +916,13 @@ local ZoneMonitorScheduler = SCHEDULER:New( nil, function() US_CC:MessageTypeToCoalition( string.format("APPROACHING VICTORY! %d more zone(s) needed for complete success!", status.total - status.blue), - MESSAGE.Type.Information, 10 + MESSAGE.Type.Information, MESSAGE_CONFIG.VICTORY_MESSAGE_DURATION ) RU_CC:MessageTypeToCoalition( string.format("CRITICAL SITUATION! Coalition forces control %d/%d zones! We must recapture territory!", status.blue, status.total), - MESSAGE.Type.Information, 10 + MESSAGE.Type.Information, MESSAGE_CONFIG.VICTORY_MESSAGE_DURATION ) end @@ -871,17 +931,17 @@ local ZoneMonitorScheduler = SCHEDULER:New( nil, function() RU_CC:MessageTypeToCoalition( string.format("APPROACHING VICTORY! %d more zone(s) needed for complete success!", status.total - status.red), - MESSAGE.Type.Information, 10 + MESSAGE.Type.Information, MESSAGE_CONFIG.VICTORY_MESSAGE_DURATION ) US_CC:MessageTypeToCoalition( string.format("CRITICAL SITUATION! Russian forces control %d/%d zones! We must recapture territory!", status.red, status.total), - MESSAGE.Type.Information, 10 + MESSAGE.Type.Information, MESSAGE_CONFIG.VICTORY_MESSAGE_DURATION ) end -end, {}, 10, 300 ) -- Start after 10 seconds, repeat every 300 seconds (5 minutes) +end, {}, MESSAGE_CONFIG.STATUS_BROADCAST_START_DELAY, MESSAGE_CONFIG.STATUS_BROADCAST_FREQUENCY ) -- Periodic zone color verification system (every 2 minutes) local ZoneColorVerificationScheduler = SCHEDULER:New( nil, function() @@ -897,8 +957,8 @@ local ZoneColorVerificationScheduler = SCHEDULER:New( nil, function() local zoneColor = GetZoneColor(zoneCapture) -- Force redraw the zone with correct color based on CURRENT STATE - zoneCapture:UndrawZone() - zoneCapture:DrawZone(-1, zoneColor, 0.5, zoneColor, 0.2, 2, true) + zoneCapture:UndrawZone() + zoneCapture:DrawZone(-1, {0, 0, 0}, 1, zoneColor, 0.2, 2, true) -- Log the color assignment for debugging local colorName = "UNKNOWN" @@ -917,20 +977,35 @@ local ZoneColorVerificationScheduler = SCHEDULER:New( nil, function() end end -end, {}, 60, 120 ) -- Start after 60 seconds, repeat every 120 seconds (2 minutes) +end, {}, MESSAGE_CONFIG.COLOR_VERIFICATION_START_DELAY, MESSAGE_CONFIG.COLOR_VERIFICATION_FREQUENCY ) --- Periodic tactical marker update system (every 1 minute) +-- Periodic tactical marker update system with change detection +local __lastForceCountsByZone = {} local TacticalMarkerUpdateScheduler = SCHEDULER:New( nil, function() - log("[TACTICAL] Running periodic tactical marker update...") - - -- Update tactical markers for all zones + log("[TACTICAL] Running periodic tactical marker update (change-detected)...") + for i, zoneCapture in ipairs(zoneCaptureObjects) do if zoneCapture then - CreateTacticalInfoMarker(zoneCapture) + local zoneName = zoneCapture.GetZoneName and zoneCapture:GetZoneName() or (zoneNames[i] or ("Zone " .. i)) + local counts = GetZoneForceStrengths(zoneCapture) + local last = __lastForceCountsByZone[zoneName] + local changed = (not last) + or (last.red ~= counts.red) + or (last.blue ~= counts.blue) + or (last.neutral ~= counts.neutral) + + if changed then + __lastForceCountsByZone[zoneName] = { + red = counts.red, + blue = counts.blue, + neutral = counts.neutral + } + CreateTacticalInfoMarker(zoneCapture) + end end end - -end, {}, 30, 60 ) -- Start after 30 seconds, repeat every 60 seconds (1 minute) + +end, {}, MESSAGE_CONFIG.TACTICAL_UPDATE_START_DELAY, MESSAGE_CONFIG.TACTICAL_UPDATE_FREQUENCY ) -- Function to refresh all zone colors based on current ownership local function RefreshAllZoneColors() @@ -945,11 +1020,11 @@ local function RefreshAllZoneColors() -- Get color for current state/ownership local zoneColor = GetZoneColor(zoneCapture) - -- Clear existing drawings - zoneCapture:UndrawZone() + -- Clear existing drawings + zoneCapture:UndrawZone() - -- Redraw with correct color - zoneCapture:DrawZone(-1, zoneColor, 0.5, zoneColor, 0.2, 2, true) + -- Redraw with correct color + zoneCapture:DrawZone(-1, {0, 0, 0}, 1, zoneColor, 0.2, 2, true) -- Log the color assignment for debugging local colorName = "UNKNOWN" @@ -969,8 +1044,8 @@ local function RefreshAllZoneColors() end -- Notify BOTH coalitions - US_CC:MessageTypeToCoalition("Zone visual markers have been refreshed!", MESSAGE.Type.Information, 5) - RU_CC:MessageTypeToCoalition("Zone visual markers have been refreshed!", MESSAGE.Type.Information, 5) + US_CC:MessageTypeToCoalition("Zone visual markers have been refreshed!", MESSAGE.Type.Information, MESSAGE_CONFIG.STATUS_MESSAGE_DURATION) + RU_CC:MessageTypeToCoalition("Zone visual markers have been refreshed!", MESSAGE.Type.Information, MESSAGE_CONFIG.STATUS_MESSAGE_DURATION) end -- Manual zone status commands for players (F10 radio menu) - BOTH COALITIONS @@ -982,7 +1057,8 @@ local function SetupZoneStatusCommands() MENU_COALITION_COMMAND:New( coalition.side.BLUE, "Check Victory Progress", USMenu, function() local status = GetZoneOwnershipStatus() - local progressPercent = math.floor((status.blue / status.total) * 100) + local totalZones = math.max(status.total, 1) + local progressPercent = math.floor((status.blue / totalZones) * 100) US_CC:MessageTypeToCoalition( string.format( @@ -998,7 +1074,7 @@ local function SetupZoneStatusCommands() progressPercent >= 50 and "GOOD PROGRESS!" or "KEEP FIGHTING!" ), - MESSAGE.Type.Information, 10 + MESSAGE.Type.Information, MESSAGE_CONFIG.STATUS_MESSAGE_DURATION ) end ) @@ -1013,7 +1089,8 @@ local function SetupZoneStatusCommands() MENU_COALITION_COMMAND:New( coalition.side.RED, "Check Victory Progress", RUMenu, function() local status = GetZoneOwnershipStatus() - local progressPercent = math.floor((status.red / status.total) * 100) + local totalZones = math.max(status.total, 1) + local progressPercent = math.floor((status.red / totalZones) * 100) RU_CC:MessageTypeToCoalition( string.format( @@ -1029,7 +1106,7 @@ local function SetupZoneStatusCommands() progressPercent >= 50 and "GOOD PROGRESS!" or "KEEP FIGHTING!" ), - MESSAGE.Type.Information, 10 + MESSAGE.Type.Information, MESSAGE_CONFIG.STATUS_MESSAGE_DURATION ) end ) diff --git a/Moose_TADC/Moose_TADC_CargoDispatcher.lua b/Moose_TADC/Moose_TADC_CargoDispatcher.lua index e033542..908c018 100644 --- a/Moose_TADC/Moose_TADC_CargoDispatcher.lua +++ b/Moose_TADC/Moose_TADC_CargoDispatcher.lua @@ -5,7 +5,8 @@ ═══════════════════════════════════════════════════════════════════════════════ DESCRIPTION: - This script monitors RED and BLUE squadrons for low aircraft counts and automatically dispatches CARGO aircraft from a list of supply airfields to replenish them. It spawns cargo aircraft and routes them to destination airbases. Delivery detection and replenishment is handled by the main TADC system. + This script monitors RED and BLUE squadrons for low aircraft counts and automatically dispatches CARGO aircraft from a list of supply airfields to replenish them. + It spawns cargo aircraft and routes them to destination airbases. Delivery detection and replenishment is handled by the main TADC system. CONFIGURATION: - Update static templates and airfield lists as needed for your mission. @@ -19,6 +20,13 @@ REQUIRES: ═══════════════════════════════════════════════════════════════════════════════ ]] +-- Single-run guard to prevent duplicate dispatcher loops if script is reloaded +if _G.__TDAC_DISPATCHER_RUNNING then + env.info("[TDAC] CargoDispatcher already running; aborting duplicate load") + return +end +_G.__TDAC_DISPATCHER_RUNNING = true + --[[ GLOBAL STATE AND CONFIGURATION -------------------------------------------------------------------------- @@ -264,9 +272,9 @@ local function cleanupCargoMissions() for _, coalitionKey in ipairs({"red", "blue"}) do for i = #cargoMissions[coalitionKey], 1, -1 do local m = cargoMissions[coalitionKey][i] - if m.status == "failed" then + if m.status == "failed" or m.status == "completed" then if not (m.group and m.group:IsAlive()) then - log("Cleaning up failed cargo mission: " .. (m.group and m.group:GetName() or "nil group") .. " status: failed") + log("Cleaning up " .. m.status .. " cargo mission: " .. (m.group and m.group:GetName() or "nil group")) table.remove(cargoMissions[coalitionKey], i) end end @@ -444,12 +452,21 @@ local function dispatchCargo(squadron, coalitionKey) rat:SetDeparture(origin) rat:SetDestination(destination) rat:NoRespawn() - rat:InitUnControlled(false) -- ensure template-level 'Uncontrolled' flag does not leave transports parked + rat:InitUnControlled(false) -- force departing transports to spawn in a controllable state rat:InitLateActivated(false) rat:SetSpawnLimit(1) rat:SetSpawnDelay(1) - -- Ensure RAT takes off immediately from the runway (hot start) instead of staying parked - if rat.SetTakeoffHot then rat:SetTakeoffHot() end + + -- CRITICAL: Force takeoff from runway to prevent aircraft getting stuck at parking + -- SetTakeoffRunway() ensures aircraft spawn directly on runway and take off immediately + if rat.SetTakeoffRunway then + rat:SetTakeoffRunway() + log("DEBUG: Configured cargo to take off from runway at " .. origin, true) + else + log("WARNING: SetTakeoffRunway() not available - falling back to SetTakeoffHot()", true) + if rat.SetTakeoffHot then rat:SetTakeoffHot() end + end + -- Ensure RAT will look for parking and not despawn the group immediately on landing. -- This makes the group taxi to parking and come to a stop so other scripts (e.g. Load2nd) -- that detect parked/stopped cargo aircraft can register the delivery. diff --git a/Moose_TADC/Moose_TADC_Load2nd.lua b/Moose_TADC/Moose_TADC_Load2nd.lua index f67e6b4..749b23c 100644 --- a/Moose_TADC/Moose_TADC_Load2nd.lua +++ b/Moose_TADC/Moose_TADC_Load2nd.lua @@ -140,15 +140,15 @@ local TADC_SETTINGS = { -- 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) + statusReportInterval = 1805, -- How often to report airbase status (seconds) + squadronSummaryInterval = 1800, -- 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 = 600, -- RED cooldown after squadron launch (seconds) - interceptRatio = 0.8, -- RED interceptors per threat aircraft + interceptRatio = 1.2, -- 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) @@ -158,7 +158,7 @@ local TADC_SETTINGS = { blue = { maxActiveCAP = 24, -- Maximum BLUE fighters airborne at once squadronCooldown = 600, -- BLUE cooldown after squadron launch (seconds) - interceptRatio = 0.8, -- BLUE interceptors per threat aircraft + interceptRatio = 1.2, -- 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) @@ -276,6 +276,105 @@ local airbaseHealthStatus = { blue = {} } +local function coalitionKeyFromSide(side) + if side == coalition.side.RED then return "red" end + if side == coalition.side.BLUE then return "blue" end + return nil +end + +local function cleanupInterceptorEntry(interceptorName, coalitionKey) + if not interceptorName or not coalitionKey then return end + if activeInterceptors[coalitionKey] then + activeInterceptors[coalitionKey][interceptorName] = nil + end + if aircraftSpawnTracking[coalitionKey] then + aircraftSpawnTracking[coalitionKey][interceptorName] = nil + end +end + +local function destroyInterceptorGroup(interceptor, coalitionKey, delaySeconds) + if not interceptor then return end + + local name = nil + if interceptor.GetName then + local ok, value = pcall(function() return interceptor:GetName() end) + if ok then name = value end + end + + local resolvedKey = coalitionKey + if not resolvedKey and interceptor.GetCoalition then + local ok, side = pcall(function() return interceptor:GetCoalition() end) + if ok then + resolvedKey = coalitionKeyFromSide(side) + end + end + + local function doDestroy() + if interceptor and interceptor.IsAlive and interceptor:IsAlive() then + pcall(function() interceptor:Destroy() end) + end + if name and resolvedKey then + cleanupInterceptorEntry(name, resolvedKey) + end + end + + if delaySeconds and delaySeconds > 0 then + timer.scheduleFunction(function() + doDestroy() + return + end, {}, timer.getTime() + delaySeconds) + else + doDestroy() + end +end + +local function finalizeCargoMission(cargoGroup, squadron, coalitionKey) + if not cargoMissions or not coalitionKey or not squadron or not squadron.airbaseName then + return + end + + local coalitionBucket = cargoMissions[coalitionKey] + if type(coalitionBucket) ~= "table" then + return + end + + local groupName = nil + if cargoGroup and cargoGroup.GetName then + local ok, value = pcall(function() return cargoGroup:GetName() end) + if ok then groupName = value end + end + + for idx = #coalitionBucket, 1, -1 do + local mission = coalitionBucket[idx] + if mission and mission.destination == squadron.airbaseName then + local missionGroupName = nil + if mission.group and mission.group.GetName then + local ok, value = pcall(function() return mission.group:GetName() end) + if ok then missionGroupName = value end + end + + if not groupName or missionGroupName == groupName then + mission.status = "completed" + mission.completedAt = timer.getTime() + + if mission.group and mission.group.Destroy then + local targetGroup = mission.group + timer.scheduleFunction(function() + pcall(function() + if targetGroup and targetGroup.IsAlive and targetGroup:IsAlive() then + targetGroup:Destroy() + end + end) + return + end, {}, timer.getTime() + 90) + end + + table.remove(coalitionBucket, idx) + end + end + end +end + -- Logging function local function log(message, detailed) if not detailed or ADVANCED_SETTINGS.enableDetailedLogging then @@ -709,6 +808,8 @@ local function processCargoDelivery(cargoGroup, squadron, coalitionSide, coaliti MESSAGE:New(msg, 10):ToCoalition(coalitionSide) USERSOUND:New("Cargo_Delivered.ogg"):ToCoalition(coalitionSide) end + + finalizeCargoMission(cargoGroup, squadron, coalitionKey) end -- Event handler for cargo aircraft landing (backup for actual landings) @@ -1271,10 +1372,9 @@ local function monitorStuckAircraft() -- Mark airbase as having stuck aircraft airbaseHealthStatus[coalitionKey][trackingData.airbase] = "stuck-aircraft" - -- Remove the stuck aircraft - trackingData.group:Destroy() - activeInterceptors[coalitionKey][aircraftName] = nil - aircraftSpawnTracking[coalitionKey][aircraftName] = nil + -- Remove the stuck aircraft and clear tracking + pcall(function() trackingData.group:Destroy() end) + cleanupInterceptorEntry(aircraftName, coalitionKey) -- Reassign squadron to alternative airbase reassignSquadronToAlternativeAirbase(trackingData.squadron, coalitionKey) @@ -1342,9 +1442,14 @@ local function sendInterceptorHome(interceptor, coalitionSide) 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) + local name = nil + if interceptor and interceptor.GetName then + local ok, value = pcall(function() return interceptor:GetName() end) + if ok then name = value end + end + if name and activeInterceptors[coalitionKey][name] then + destroyInterceptorGroup(interceptor, coalitionKey, 0) + log("Cleaned up " .. coalitionName .. " " .. name .. " after RTB", true) end end, {}, flightTime) else @@ -1616,6 +1721,7 @@ local function launchInterceptor(threatGroup, coalitionSide) log("ERROR: Failed to create SPAWN object for " .. coalitionName .. " " .. squadron.templateName) return end + spawn:InitCleanUp(900) local interceptors = {} @@ -1674,11 +1780,14 @@ local function launchInterceptor(threatGroup, coalitionSide) -- 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 - -- Also clean up spawn tracking - aircraftSpawnTracking[coalitionKey][interceptor:GetName()] = nil + local name = nil + if interceptor and interceptor.GetName then + local ok, value = pcall(function() return interceptor:GetName() end) + if ok then name = value end + end + if name and activeInterceptors[coalitionKey][name] then + log("Emergency cleanup of " .. coalitionName .. " " .. name .. " (should have RTB'd)") + destroyInterceptorGroup(interceptor, coalitionKey, 0) end end, {}, coalitionSettings.emergencyCleanupTime) end @@ -2182,20 +2291,29 @@ end initializeSystem() -- Add F10 menu command for squadron summary -local menuRoot = MENU_MISSION:New("TADC Utilities") +-- Use MenuManager to create coalition-specific menus (not mission-wide) +local menuRootBlue, menuRootRed -MENU_COALITION_COMMAND:New(coalition.side.RED, "Show Squadron Resource Summary", menuRoot, function() +if MenuManager then + menuRootBlue = MenuManager.CreateCoalitionMenu(coalition.side.BLUE, "TADC Utilities") + menuRootRed = MenuManager.CreateCoalitionMenu(coalition.side.RED, "TADC Utilities") +else + menuRootBlue = MENU_COALITION:New(coalition.side.BLUE, "TADC Utilities") + menuRootRed = MENU_COALITION:New(coalition.side.RED, "TADC Utilities") +end + +MENU_COALITION_COMMAND:New(coalition.side.RED, "Show Squadron Resource Summary", menuRootRed, 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() +MENU_COALITION_COMMAND:New(coalition.side.BLUE, "Show Squadron Resource Summary", menuRootBlue, 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() +MENU_COALITION_COMMAND:New(coalition.side.RED, "Show Airbase Status Report", menuRootRed, function() local report = "=== RED Airbase Status ===\n" for _, squadron in pairs(RED_SQUADRON_CONFIG) do local usable, status = isAirbaseUsable(squadron.airbaseName, coalition.side.RED) @@ -2212,7 +2330,7 @@ MENU_COALITION_COMMAND:New(coalition.side.RED, "Show Airbase Status Report", men MESSAGE:New(report, 20):ToCoalition(coalition.side.RED) end) -MENU_COALITION_COMMAND:New(coalition.side.BLUE, "Show Airbase Status Report", menuRoot, function() +MENU_COALITION_COMMAND:New(coalition.side.BLUE, "Show Airbase Status Report", menuRootBlue, function() local report = "=== BLUE Airbase Status ===\n" for _, squadron in pairs(BLUE_SQUADRON_CONFIG) do local usable, status = isAirbaseUsable(squadron.airbaseName, coalition.side.BLUE) @@ -2230,7 +2348,7 @@ MENU_COALITION_COMMAND:New(coalition.side.BLUE, "Show Airbase Status Report", me end) -- 2. Show Active Interceptors -MENU_COALITION_COMMAND:New(coalition.side.RED, "Show Active Interceptors", menuRoot, function() +MENU_COALITION_COMMAND:New(coalition.side.RED, "Show Active Interceptors", menuRootRed, function() local lines = {"Active RED Interceptors:"} for name, data in pairs(activeInterceptors.red) do if data and data.group and data.group:IsAlive() then @@ -2240,7 +2358,7 @@ MENU_COALITION_COMMAND:New(coalition.side.RED, "Show Active Interceptors", menuR MESSAGE:New(table.concat(lines, "\n"), 20):ToCoalition(coalition.side.RED) end) -MENU_COALITION_COMMAND:New(coalition.side.BLUE, "Show Active Interceptors", menuRoot, function() +MENU_COALITION_COMMAND:New(coalition.side.BLUE, "Show Active Interceptors", menuRootBlue, function() local lines = {"Active BLUE Interceptors:"} for name, data in pairs(activeInterceptors.blue) do if data and data.group and data.group:IsAlive() then @@ -2251,7 +2369,7 @@ MENU_COALITION_COMMAND:New(coalition.side.BLUE, "Show Active Interceptors", menu end) -- 3. Show Threat Summary -MENU_COALITION_COMMAND:New(coalition.side.RED, "Show Threat Summary", menuRoot, function() +MENU_COALITION_COMMAND:New(coalition.side.RED, "Show Threat Summary", menuRootRed, function() local lines = {"Detected BLUE Threats:"} if cachedSets.blueAircraft then cachedSets.blueAircraft:ForEach(function(group) @@ -2263,7 +2381,7 @@ MENU_COALITION_COMMAND:New(coalition.side.RED, "Show Threat Summary", menuRoot, MESSAGE:New(table.concat(lines, "\n"), 20):ToCoalition(coalition.side.RED) end) -MENU_COALITION_COMMAND:New(coalition.side.BLUE, "Show Threat Summary", menuRoot, function() +MENU_COALITION_COMMAND:New(coalition.side.BLUE, "Show Threat Summary", menuRootBlue, function() local lines = {"Detected RED Threats:"} if cachedSets.redAircraft then cachedSets.redAircraft:ForEach(function(group) @@ -2276,18 +2394,18 @@ MENU_COALITION_COMMAND:New(coalition.side.BLUE, "Show Threat Summary", menuRoot, end) -- 4. Request Immediate Squadron Summary Broadcast -MENU_COALITION_COMMAND:New(coalition.side.RED, "Broadcast Squadron Summary Now", menuRoot, function() +MENU_COALITION_COMMAND:New(coalition.side.RED, "Broadcast Squadron Summary Now", menuRootRed, 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() +MENU_COALITION_COMMAND:New(coalition.side.BLUE, "Broadcast Squadron Summary Now", menuRootBlue, 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() +MENU_COALITION_COMMAND:New(coalition.side.RED, "Show Cargo Delivery Log", menuRootRed, function() local lines = {"Recent RED Cargo Deliveries:"} if _G.processedDeliveries then for key, timestamp in pairs(_G.processedDeliveries) do @@ -2299,7 +2417,7 @@ MENU_COALITION_COMMAND:New(coalition.side.RED, "Show Cargo Delivery Log", menuRo 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() +MENU_COALITION_COMMAND:New(coalition.side.BLUE, "Show Cargo Delivery Log", menuRootBlue, function() local lines = {"Recent BLUE Cargo Deliveries:"} if _G.processedDeliveries then for key, timestamp in pairs(_G.processedDeliveries) do @@ -2312,7 +2430,7 @@ MENU_COALITION_COMMAND:New(coalition.side.BLUE, "Show Cargo Delivery Log", menuR end) -- 6. Show Zone Coverage Map -MENU_COALITION_COMMAND:New(coalition.side.RED, "Show Zone Coverage Map", menuRoot, function() +MENU_COALITION_COMMAND:New(coalition.side.RED, "Show Zone Coverage Map", menuRootRed, function() local lines = {"RED Zone Coverage:"} for _, squadron in pairs(RED_SQUADRON_CONFIG) do local zones = {} @@ -2324,7 +2442,7 @@ MENU_COALITION_COMMAND:New(coalition.side.RED, "Show Zone Coverage Map", menuRoo 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() +MENU_COALITION_COMMAND:New(coalition.side.BLUE, "Show Zone Coverage Map", menuRootBlue, function() local lines = {"BLUE Zone Coverage:"} for _, squadron in pairs(BLUE_SQUADRON_CONFIG) do local zones = {} @@ -2336,40 +2454,71 @@ MENU_COALITION_COMMAND:New(coalition.side.BLUE, "Show Zone Coverage Map", menuRo 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() +-- 7. Admin/Debug Commands - Create submenus under each coalition's TADC Utilities +local menuAdminBlue = MENU_COALITION:New(coalition.side.BLUE, "Admin / Debug", menuRootBlue) +local menuAdminRed = MENU_COALITION:New(coalition.side.RED, "Admin / Debug", menuRootRed) + +MENU_COALITION_COMMAND:New(coalition.side.BLUE, "Emergency Cleanup Interceptors", menuAdminBlue, function() local cleaned = 0 - for _, interceptors in pairs(activeInterceptors.red) do + for name, interceptors in pairs(activeInterceptors.red) do if interceptors and interceptors.group and not interceptors.group:IsAlive() then - interceptors.group = nil + cleanupInterceptorEntry(name, "red") cleaned = cleaned + 1 end end - for _, interceptors in pairs(activeInterceptors.blue) do + for name, interceptors in pairs(activeInterceptors.blue) do if interceptors and interceptors.group and not interceptors.group:IsAlive() then - interceptors.group = nil + cleanupInterceptorEntry(name, "blue") cleaned = cleaned + 1 end end - MESSAGE:New("Cleaned up " .. cleaned .. " dead interceptor groups.", 20):ToAll() + MESSAGE:New("Cleaned up " .. cleaned .. " dead interceptor groups.", 20):ToBlue() +end) + +MENU_COALITION_COMMAND:New(coalition.side.RED, "Emergency Cleanup Interceptors", menuAdminRed, function() + local cleaned = 0 + for name, interceptors in pairs(activeInterceptors.red) do + if interceptors and interceptors.group and not interceptors.group:IsAlive() then + cleanupInterceptorEntry(name, "red") + cleaned = cleaned + 1 + end + end + for name, interceptors in pairs(activeInterceptors.blue) do + if interceptors and interceptors.group and not interceptors.group:IsAlive() then + cleanupInterceptorEntry(name, "blue") + cleaned = cleaned + 1 + end + end + MESSAGE:New("Cleaned up " .. cleaned .. " dead interceptor groups.", 20):ToRed() end) -- 9. Show System Uptime/Status local systemStartTime = timer.getTime() -MENU_MISSION_COMMAND:New("Show TADC System Status", menuRoot, function() +MENU_COALITION_COMMAND:New(coalition.side.BLUE, "Show TADC System Status", menuAdminBlue, 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() + MESSAGE:New(status, 20):ToBlue() +end) + +MENU_COALITION_COMMAND:New(coalition.side.RED, "Show TADC System Status", menuAdminRed, 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):ToRed() end) -- 10. Check for Stuck Aircraft (manual trigger) -MENU_MISSION_COMMAND:New("Check for Stuck Aircraft", menuRoot, function() +MENU_COALITION_COMMAND:New(coalition.side.BLUE, "Check for Stuck Aircraft", menuAdminBlue, function() monitorStuckAircraft() - MESSAGE:New("Stuck aircraft check completed", 10):ToAll() + MESSAGE:New("Stuck aircraft check completed", 10):ToBlue() +end) + +MENU_COALITION_COMMAND:New(coalition.side.RED, "Check for Stuck Aircraft", menuAdminRed, function() + monitorStuckAircraft() + MESSAGE:New("Stuck aircraft check completed", 10):ToRed() end) -- 11. Show Airbase Health Status -MENU_MISSION_COMMAND:New("Show Airbase Health Status", menuRoot, function() +MENU_COALITION_COMMAND:New(coalition.side.BLUE, "Show Airbase Health Status", menuAdminBlue, function() local lines = {"Airbase Health Status:"} for _, coalitionKey in ipairs({"red", "blue"}) do local coalitionName = (coalitionKey == "red") and "RED" or "BLUE" @@ -2378,7 +2527,19 @@ MENU_MISSION_COMMAND:New("Show Airbase Health Status", menuRoot, function() table.insert(lines, " " .. airbaseName .. ": " .. status) end end - MESSAGE:New(table.concat(lines, "\n"), 20):ToAll() + MESSAGE:New(table.concat(lines, "\n"), 20):ToBlue() +end) + +MENU_COALITION_COMMAND:New(coalition.side.RED, "Show Airbase Health Status", menuAdminRed, function() + local lines = {"Airbase Health Status:"} + for _, coalitionKey in ipairs({"red", "blue"}) do + local coalitionName = (coalitionKey == "red") and "RED" or "BLUE" + table.insert(lines, coalitionName .. " Coalition:") + for airbaseName, status in pairs(airbaseHealthStatus[coalitionKey]) do + table.insert(lines, " " .. airbaseName .. ": " .. status) + end + end + MESSAGE:New(table.concat(lines, "\n"), 20):ToRed() end) -- Initialize airbase health status for all configured airbases diff --git a/Moose_TADC/TADC_Example.miz b/Moose_TADC/TADC_Example.miz index 5790ba7..bd22a9f 100644 Binary files a/Moose_TADC/TADC_Example.miz and b/Moose_TADC/TADC_Example.miz differ