-- Setup Capture Missions & Zones -- ================================================================================ -- MESSAGE AND TIMING CONFIGURATION -- Control how often messages are sent and how long they are displayed -- ================================================================================ local MESSAGE_CONFIG = { -- Zone status broadcast frequency (in seconds) STATUS_BROADCAST_FREQUENCY = 3602, -- Default: 3600 seconds (1 hour) STATUS_BROADCAST_START_DELAY = 10, -- Default: 10 seconds initial delay -- Zone color verification frequency (in seconds) COLOR_VERIFICATION_FREQUENCY = 240, -- Default: 240 seconds (4 minutes) COLOR_VERIFICATION_START_DELAY = 60, -- Default: 60 seconds initial delay -- Tactical marker update frequency (in seconds) TACTICAL_UPDATE_FREQUENCY = 180, -- Default: 180 seconds (3 minutes) TACTICAL_UPDATE_START_DELAY = 30, -- Default: 30 seconds initial delay -- Message display durations (in seconds) STATUS_MESSAGE_DURATION = 15, -- Default: 15 seconds VICTORY_MESSAGE_DURATION = 300, -- Default: 300 seconds CAPTURE_MESSAGE_DURATION = 15, -- Default: 15 seconds ATTACK_MESSAGE_DURATION = 15, -- Default: 15 seconds } -- ================================================================================ -- ZONE COLOR CONFIGURATION -- Mission makers can easily customize zone colors here -- Colors are in RGB format: {Red, Green, Blue} where each value is 0.0 to 1.0 -- ================================================================================ local ZONE_COLORS = { -- Blue coalition zones BLUE_CAPTURED = {0, 0, 1}, -- Blue (owned by Blue) BLUE_ATTACKED = {0, 1, 1}, -- Cyan (owned by Blue, under attack) -- Red coalition zones RED_CAPTURED = {1, 0, 0}, -- Red (owned by Red) RED_ATTACKED = {1, 0.5, 0}, -- Orange (owned by Red, under attack) -- Neutral/Empty zones EMPTY = {0, 1, 0} -- Green (no owner) } -- Helper function to get the appropriate color for a zone local function GetZoneColor(zoneCapture) local zoneCoalition = zoneCapture:GetCoalition() local state = zoneCapture:GetCurrentState() -- Priority 1: Check if zone is under attack if state == "Attacked" then if zoneCoalition == coalition.side.BLUE then return ZONE_COLORS.BLUE_ATTACKED elseif zoneCoalition == coalition.side.RED then return ZONE_COLORS.RED_ATTACKED end end -- Priority 2: Check if zone is empty/neutral if state == "Empty" then return ZONE_COLORS.EMPTY end -- Priority 3: Show ownership color (Guarded or Captured states) if zoneCoalition == coalition.side.BLUE then return ZONE_COLORS.BLUE_CAPTURED elseif zoneCoalition == coalition.side.RED then return ZONE_COLORS.RED_CAPTURED end -- Fallback to green return ZONE_COLORS.EMPTY end -- Setup BLUE Missions do -- Missions US_Mission_Capture_Airfields = MISSION:New( US_CC, "Capture the Airfields", "Primary", "Capture the Air Bases marked on your F10 map.\n" .. "Destroy enemy ground forces in the surrounding area, " .. "then occupy each capture zone with a platoon.\n " .. "Your orders are to hold position until all capture zones are taken.\n" .. "Use the map (F10) for a clear indication of the location of each capture zone.\n" .. "Note that heavy resistance can be expected at the airbases!\n\n" .. "VICTORY CONDITION: Capture ALL 14 strategic zones to achieve total victory.\n" .. "CRITICAL: Do NOT lose all three of your starting bases (Luostari Pechenga, Ivalo, Alakurtti) or the mission will be lost!\n" , coalition.side.BLUE) --US_Score = SCORING:New( "Capture Airfields" ) --US_Mission_Capture_Airfields:AddScoring( US_Score ) US_Mission_Capture_Airfields:Start() 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 = false, prefix = "[CAPTURE Module]" } end local function log(message, detailed) if CAPTURE_ZONE_LOGGING.enabled then -- Preserve the previous prefixing used across the module if CAPTURE_ZONE_LOGGING.prefix then env.info(tostring(CAPTURE_ZONE_LOGGING.prefix) .. " " .. tostring(message)) else env.info(tostring(message)) end end end -- Red Airbases (from TADC configuration) log("[DEBUG] Initializing Capture Zone: Kilpyavr") CaptureZone_Kilpyavr = ZONE:New( "Capture Kilpyavr" ) ZoneCapture_Kilpyavr = ZONE_CAPTURE_COALITION:New( CaptureZone_Kilpyavr, coalition.side.RED ) -- SetMarkReadOnly method not available in this MOOSE version - feature disabled ZoneCapture_Kilpyavr:__Guard( 1 ) ZoneCapture_Kilpyavr:Start( 30, 30 ) log("[DEBUG] Kilpyavr zone initialization complete") log("[DEBUG] Initializing Capture Zone: Severomorsk-1") CaptureZone_Severomorsk_1 = ZONE:New( "Capture Severomorsk-1" ) ZoneCapture_Severomorsk_1 = ZONE_CAPTURE_COALITION:New( CaptureZone_Severomorsk_1, coalition.side.RED ) -- SetMarkReadOnly method not available in this MOOSE version - feature disabled ZoneCapture_Severomorsk_1:__Guard( 1 ) ZoneCapture_Severomorsk_1:Start( 30, 30 ) log("[DEBUG] Severomorsk-1 zone initialization complete") log("[DEBUG] Initializing Capture Zone: Severomorsk-3") CaptureZone_Severomorsk_3 = ZONE:New( "Capture Severomorsk-3" ) ZoneCapture_Severomorsk_3 = ZONE_CAPTURE_COALITION:New( CaptureZone_Severomorsk_3, coalition.side.RED ) -- SetMarkReadOnly method not available in this MOOSE version - feature disabled ZoneCapture_Severomorsk_3:__Guard( 1 ) ZoneCapture_Severomorsk_3:Start( 30, 30 ) log("[DEBUG] Severomorsk-3 zone initialization complete") log("[DEBUG] Initializing Capture Zone: Murmansk International") CaptureZone_Murmansk_International = ZONE:New( "Capture Murmansk International" ) ZoneCapture_Murmansk_International = ZONE_CAPTURE_COALITION:New( CaptureZone_Murmansk_International, coalition.side.RED ) -- SetMarkReadOnly method not available in this MOOSE version - feature disabled ZoneCapture_Murmansk_International:__Guard( 1 ) ZoneCapture_Murmansk_International:Start( 30, 30 ) log("[DEBUG] Murmansk International zone initialization complete") log("[DEBUG] Initializing Capture Zone: Monchegorsk") CaptureZone_Monchegorsk = ZONE:New( "Capture Monchegorsk" ) ZoneCapture_Monchegorsk = ZONE_CAPTURE_COALITION:New( CaptureZone_Monchegorsk, coalition.side.RED ) -- SetMarkReadOnly method not available in this MOOSE version - feature disabled ZoneCapture_Monchegorsk:__Guard( 1 ) ZoneCapture_Monchegorsk:Start( 30, 30 ) log("[DEBUG] Monchegorsk zone initialization complete") log("[DEBUG] Initializing Capture Zone: Olenya") CaptureZone_Olenya = ZONE:New( "Capture Olenya" ) ZoneCapture_Olenya = ZONE_CAPTURE_COALITION:New( CaptureZone_Olenya, coalition.side.RED ) -- SetMarkReadOnly method not available in this MOOSE version - feature disabled ZoneCapture_Olenya:__Guard( 1 ) ZoneCapture_Olenya:Start( 30, 30 ) log("[DEBUG] Olenya zone initialization complete") log("[DEBUG] Initializing Capture Zone: Afrikanda") CaptureZone_Afrikanda = ZONE:New( "Capture Afrikanda" ) ZoneCapture_Afrikanda = ZONE_CAPTURE_COALITION:New( CaptureZone_Afrikanda, coalition.side.RED ) -- SetMarkReadOnly method not available in this MOOSE version - feature disabled ZoneCapture_Afrikanda:__Guard( 1 ) ZoneCapture_Afrikanda:Start( 30, 30 ) log("[DEBUG] Afrikanda zone initialization complete") log("[DEBUG] Initializing Capture Zone: The Mountain") CaptureZone_The_Mountain = ZONE:New( "Capture The Mountain" ) ZoneCapture_The_Mountain = ZONE_CAPTURE_COALITION:New( CaptureZone_The_Mountain, coalition.side.RED ) -- SetMarkReadOnly method not available in this MOOSE version - feature disabled ZoneCapture_The_Mountain:__Guard( 1 ) ZoneCapture_The_Mountain:Start( 30, 30 ) log("[DEBUG] The Mountain zone initialization complete") log("[DEBUG] Initializing Capture Zone: The River") CaptureZone_The_River = ZONE:New( "Capture The River" ) ZoneCapture_The_River = ZONE_CAPTURE_COALITION:New( CaptureZone_The_River, coalition.side.RED ) -- SetMarkReadOnly method not available in this MOOSE version - feature disabled ZoneCapture_The_River:__Guard( 1 ) ZoneCapture_The_River:Start( 30, 30 ) log("[DEBUG] The River zone initialization complete") log("[DEBUG] Initializing Capture Zone: The Gulf") CaptureZone_The_Gulf = ZONE:New( "Capture The Gulf" ) ZoneCapture_The_Gulf = ZONE_CAPTURE_COALITION:New( CaptureZone_The_Gulf, coalition.side.RED ) -- SetMarkReadOnly method not available in this MOOSE version - feature disabled ZoneCapture_The_Gulf:__Guard( 1 ) ZoneCapture_The_Gulf:Start( 30, 30 ) log("[DEBUG] The Gulf zone initialization complete") log("[DEBUG] Initializing Capture Zone: The Lakes") CaptureZone_The_Lakes = ZONE:New( "Capture The Lakes" ) ZoneCapture_The_Lakes = ZONE_CAPTURE_COALITION:New( CaptureZone_The_Lakes, coalition.side.RED ) -- SetMarkReadOnly method not available in this MOOSE version - feature disabled ZoneCapture_The_Lakes:__Guard( 1 ) ZoneCapture_The_Lakes:Start( 30, 30 ) log("[DEBUG] The Lakes zone initialization complete") log("[DEBUG] Initializing Capture of Zone: Capture Luostari Pechenga") CaptureZone_Luostari_Pechenga = ZONE:New( "Capture Luostari Pechenga" ) ZoneCapture_Luostari_Pechenga = ZONE_CAPTURE_COALITION:New( CaptureZone_Luostari_Pechenga, coalition.side.BLUE ) -- SetMarkReadOnly method not available in this MOOSE version - feature disabled ZoneCapture_Luostari_Pechenga:__Guard( 1 ) ZoneCapture_Luostari_Pechenga:Start( 30, 30 ) log("[DEBUG] Luostari Pechenga zone initialization complete") log("[DEBUG] Initializing Capture of Zone: Capture Ivalo") CaptureZone_Ivalo = ZONE:New( "Capture Ivalo" ) ZoneCapture_Ivalo = ZONE_CAPTURE_COALITION:New( CaptureZone_Ivalo, coalition.side.BLUE ) -- SetMarkReadOnly method not available in this MOOSE version - feature disabled ZoneCapture_Ivalo:__Guard( 1 ) ZoneCapture_Ivalo:Start( 30, 30 ) log("[DEBUG] Ivalo zone initialization complete") log("[DEBUG] Initializing Capture of Zone: Capture Alakurtti") CaptureZone_Alakurtti = ZONE:New( "Capture Alakurtti" ) ZoneCapture_Alakurtti = ZONE_CAPTURE_COALITION:New( CaptureZone_Alakurtti, coalition.side.BLUE ) -- SetMarkReadOnly method not available in this MOOSE version - feature disabled ZoneCapture_Alakurtti:__Guard( 1 ) ZoneCapture_Alakurtti:Start( 30, 30 ) log("[DEBUG] Alakurtti zone initialization complete") -- Helper functions for tactical information -- Global cached unit set - created once and maintained automatically by MOOSE local CachedUnitSet = nil -- Utility to guard point-in-zone checks that may throw when objects despawn mid-loop 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 :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} end 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 neutralCount = 0 -- Ensure the cached set exists before scanning 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 elseif unitCoalition == coalition.side.BLUE then blueCount = blueCount + 1 elseif unitCoalition == coalition.side.NEUTRAL then neutralCount = neutralCount + 1 end end end) end log(string.format("[TACTICAL] Zone %s scan result: R:%d B:%d N:%d", ZoneCapture:GetZoneName(), redCount, blueCount, neutralCount)) return { red = redCount, blue = blueCount, neutral = neutralCount } end local function GetRedUnitMGRSCoords(ZoneCapture) if not ZoneCapture then return {} end local success, zone = pcall(function() return ZoneCapture:GetZone() end) if not success or not zone then return {} end local coords = {} -- Ensure the cached set exists before scanning InitializeCachedUnitSet() local totalUnits = 0 local redUnits = 0 local unitsWithCoords = 0 if CachedUnitSet then CachedUnitSet:ForEachUnit(function(unit) if unit and unit:IsAlive() and IsUnitInZone(unit, zone) then totalUnits = totalUnits + 1 local unitCoalition = unit:GetCoalition() -- Only process RED units if unitCoalition == coalition.side.RED then redUnits = redUnits + 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" table.insert(coords, { name = unit:GetName(), type = unitType, mgrs = mgrs }) else log(string.format("[TACTICAL DEBUG] All coordinate methods failed for unit %s", unit:GetName() or "unknown")) end else log(string.format("[TACTICAL DEBUG] No coordinate for unit %s", unit:GetName() or "unknown")) end end end end) end log(string.format("[TACTICAL DEBUG] %s - Total units scanned: %d, RED units: %d, units with MGRS: %d", ZoneCapture:GetZoneName(), totalUnits, redUnits, unitsWithCoords)) log(string.format("[TACTICAL] Found %d RED units with coordinates in %s", #coords, ZoneCapture:GetZoneName())) return coords end local function CreateTacticalInfoMarker(ZoneCapture) -- Validate ZoneCapture object if not ZoneCapture then log("[TACTICAL ERROR] ZoneCapture object is nil") return end -- Safely get the zone with error handling local success, zone = pcall(function() return ZoneCapture:GetZone() end) if not success or not zone then log("[TACTICAL ERROR] Failed to get zone from ZoneCapture object") return end local forces = GetZoneForceStrengths(ZoneCapture) local zoneName = ZoneCapture:GetZoneName() -- Build tactical info text local tacticalText = string.format("TACTICAL: %s\nForces: R:%d B:%d", zoneName, forces.red, forces.blue) if forces.neutral > 0 then tacticalText = tacticalText .. string.format(" C:%d", forces.neutral) end -- Add MGRS coordinates if RED forces <= 10 if forces.red > 0 and forces.red <= 10 then local redCoords = GetRedUnitMGRSCoords(ZoneCapture) log(string.format("[TACTICAL DEBUG] Building marker text for %d RED units", #redCoords)) if #redCoords > 0 then tacticalText = tacticalText .. "\nTGTS:" for i, unit in ipairs(redCoords) do if i <= 10 then -- Show up to 10 units (the threshold) -- Shorten unit type names to fit better local shortType = unit.type:gsub("^%w+%-", ""):gsub("%s.*", "") -- Clean up MGRS string - remove "MGRS " prefix and compress spacing local cleanMgrs = unit.mgrs:gsub("^MGRS%s+", ""):gsub("%s+", " ") -- Ultra-compact: comma-separated on same line if i == 1 then tacticalText = tacticalText .. string.format(" %s@%s", shortType, cleanMgrs) else tacticalText = tacticalText .. string.format(", %s@%s", shortType, cleanMgrs) end log(string.format("[TACTICAL DEBUG] Added unit %d: %s at %s", i, shortType, cleanMgrs)) end end if #redCoords > 10 then tacticalText = tacticalText .. string.format(" (+%d)", #redCoords - 10) end end end -- Debug: Log the complete marker text that will be displayed log(string.format("[TACTICAL DEBUG] Complete marker text for %s:\n%s", zoneName, tacticalText)) log(string.format("[TACTICAL DEBUG] Marker text length: %d characters", string.len(tacticalText))) -- Create tactical marker offset from zone center local coord = zone:GetCoordinate() if coord then -- Offset the tactical marker slightly northeast of the main zone marker local offsetCoord = coord:Translate(200, 45) -- 200m northeast -- Remove any existing tactical marker first if ZoneCapture.TacticalMarkerID then log(string.format("[TACTICAL] Removing old marker ID %d for %s", ZoneCapture.TacticalMarkerID, zoneName)) -- Try multiple removal methods local success1 = pcall(function() offsetCoord:RemoveMark(ZoneCapture.TacticalMarkerID) end) if not success1 then local success2 = pcall(function() trigger.action.removeMark(ZoneCapture.TacticalMarkerID) end) if not success2 then -- Try using coordinate removal pcall(function() coord:RemoveMark(ZoneCapture.TacticalMarkerID) end) end end ZoneCapture.TacticalMarkerID = nil end -- Create new tactical marker for BLUE coalition only local success, markerID = pcall(function() return offsetCoord:MarkToCoalition(tacticalText, coalition.side.BLUE) end) if success and markerID then ZoneCapture.TacticalMarkerID = markerID -- Try to make the marker read-only (if available in this MOOSE version) pcall(function() offsetCoord:SetMarkReadOnly(markerID, true) end) log(string.format("[TACTICAL] Created read-only marker for %s with %d RED, %d BLUE units", zoneName, forces.red, forces.blue)) else log(string.format("[TACTICAL] Failed to create marker for %s", zoneName)) end end end -- Event handler functions - define them separately for each zone local function OnEnterGuarded(ZoneCapture, From, Event, To) if From ~= To then local Coalition = ZoneCapture:GetCoalition() if Coalition == coalition.side.BLUE then ZoneCapture:Smoke( SMOKECOLOR.Blue ) -- Update zone visual markers to BLUE (captured) ZoneCapture:UndrawZone() 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 (captured) ZoneCapture:UndrawZone() 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) end end local function OnEnterEmpty(ZoneCapture) ZoneCapture:Smoke( SMOKECOLOR.Green ) -- Update zone visual markers to GREEN (neutral/empty) ZoneCapture:UndrawZone() 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 local function OnEnterAttacked(ZoneCapture) ZoneCapture:Smoke( SMOKECOLOR.White ) -- Update zone visual markers based on who owns it (attacked state) ZoneCapture:UndrawZone() local Coalition = ZoneCapture:GetCoalition() local color if Coalition == coalition.side.BLUE then color = ZONE_COLORS.BLUE_ATTACKED -- Light blue for Blue zone under attack 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 -- Orange for Red zone under attack 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, {0, 0, 0}, 1, color, 0.2, 2, true) -- Create/update tactical information marker CreateTacticalInfoMarker(ZoneCapture) end -- Apply event handlers to all zone capture objects local zoneCaptureObjects = { ZoneCapture_Kilpyavr, ZoneCapture_Severomorsk_1, ZoneCapture_Severomorsk_3, ZoneCapture_Murmansk_International, ZoneCapture_Monchegorsk, ZoneCapture_Olenya, ZoneCapture_Afrikanda, ZoneCapture_The_Mountain, ZoneCapture_The_River, ZoneCapture_The_Gulf, ZoneCapture_The_Lakes, ZoneCapture_Luostari_Pechenga, ZoneCapture_Ivalo, ZoneCapture_Alakurtti } -- Victory condition monitoring local function CheckVictoryCondition() local blueZonesCount = 0 local totalZones = #zoneCaptureObjects -- Count BLUE zone ownership for i, zoneCapture in ipairs(zoneCaptureObjects) do if zoneCapture and zoneCapture:GetCoalition() == coalition.side.BLUE then blueZonesCount = blueZonesCount + 1 end end -- Check if RED owns all 3 critical BLUE starting bases local redOwnsLuostari = ZoneCapture_Luostari_Pechenga:GetCoalition() == coalition.side.RED local redOwnsIvalo = ZoneCapture_Ivalo:GetCoalition() == coalition.side.RED local redOwnsAlakurtti = ZoneCapture_Alakurtti:GetCoalition() == coalition.side.RED log(string.format("[VICTORY CHECK] Blue owns %d/%d zones | RED critical bases: Luostari=%s, Ivalo=%s, Alakurtti=%s", blueZonesCount, totalZones, tostring(redOwnsLuostari), tostring(redOwnsIvalo), tostring(redOwnsAlakurtti))) -- RED VICTORY CONDITION: Owns all 3 BLUE starting bases if redOwnsLuostari and redOwnsIvalo and redOwnsAlakurtti then log("[VICTORY] RED has eliminated BLUE's foothold! Triggering RED victory sequence...") -- RED Victory messages RU_CC:MessageTypeToCoalition( "VICTORY! All coalition forward operating bases have been eliminated!\n\n" .. "BLUE forces have lost their strategic foothold in the region.\n" .. "Operation Polar Shield is complete. Excellent work!\n" .. "Mission will end in 60 seconds.", MESSAGE.Type.Information, 30 ) US_CC:MessageTypeToCoalition( "DEFEAT! All forward operating bases have been lost!\n\n" .. "We have been driven from the region. Mission failed.\n" .. "Mission ending in 60 seconds.", MESSAGE.Type.Information, 30 ) -- Victory celebration effects for the 3 critical zones ZoneCapture_Luostari_Pechenga:Smoke( SMOKECOLOR.Red ) ZoneCapture_Ivalo:Smoke( SMOKECOLOR.Red ) ZoneCapture_Alakurtti:Smoke( SMOKECOLOR.Red ) -- Schedule mission end after 60 seconds SCHEDULER:New( nil, function() log("[VICTORY] Ending mission due to RED eliminating BLUE's foothold") trigger.action.setUserFlag("RED_VICTORY", 1) RU_CC:MessageTypeToCoalition( "Mission Complete! Congratulations on your victory!\n" .. "Final Status: Coalition forces eliminated from the region.", MESSAGE.Type.Information, 10 ) end, {}, 60 ) return true -- RED Victory achieved end -- BLUE VICTORY CONDITION: Owns all zones if blueZonesCount >= totalZones then log("[VICTORY] All zones captured by BLUE! Triggering BLUE victory sequence...") -- BLUE Victory messages US_CC:MessageTypeToCoalition( "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 ) 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 ) -- Victory celebration effects for _, zoneCapture in ipairs(zoneCaptureObjects) do if zoneCapture then zoneCapture:Smoke( SMOKECOLOR.Blue ) local zone = zoneCapture:GetZone() if zone then zone:FlareZone( FLARECOLOR.Blue, 90, 60 ) end end end -- Schedule mission end after 60 seconds SCHEDULER:New( nil, function() log("[VICTORY] Ending mission due to complete zone capture by BLUE") trigger.action.setUserFlag("BLUE_VICTORY", 1) US_CC:MessageTypeToCoalition( "Mission Complete! Congratulations on your victory!\n" .. "Final Status: All 14 strategic zones secured.", MESSAGE.Type.Information, 10 ) end, {}, 60 ) return true -- BLUE Victory achieved end return false -- Victory not yet achieved end local function OnEnterCaptured(ZoneCapture) local Coalition = ZoneCapture:GetCoalition() local zoneName = ZoneCapture:GetZoneName() -- Check if this is one of the critical BLUE starting bases local isCriticalBase = (zoneName == "Capture Luostari Pechenga" or zoneName == "Capture Ivalo" or zoneName == "Capture Alakurtti") if Coalition == coalition.side.BLUE then -- Update zone visual markers to BLUE (captured) ZoneCapture:UndrawZone() local color = ZONE_COLORS.BLUE_CAPTURED ZoneCapture:DrawZone(-1, {0, 0, 0}, 1, color, 0.2, 2, true) -- Enhanced messaging for critical bases if isCriticalBase then RU_CC:MessageTypeToCoalition( string.format( "%s is captured by the USA, we lost it!", zoneName ), MESSAGE.Type.Information ) US_CC:MessageTypeToCoalition( string.format( "We recaptured %s! Critical base secured.", zoneName ), MESSAGE.Type.Information ) else RU_CC:MessageTypeToCoalition( string.format( "%s is captured by the USA, we lost it!", zoneName ), MESSAGE.Type.Information ) US_CC:MessageTypeToCoalition( string.format( "We captured %s, Excellent job!", zoneName ), MESSAGE.Type.Information ) end else -- Update zone visual markers to RED (captured) ZoneCapture:UndrawZone() local color = ZONE_COLORS.RED_CAPTURED ZoneCapture:DrawZone(-1, {0, 0, 0}, 1, color, 0.2, 2, true) -- Enhanced messaging for critical bases if isCriticalBase then -- Count how many critical bases RED now owns local criticalBasesOwned = 0 if ZoneCapture_Luostari_Pechenga:GetCoalition() == coalition.side.RED then criticalBasesOwned = criticalBasesOwned + 1 end if ZoneCapture_Ivalo:GetCoalition() == coalition.side.RED then criticalBasesOwned = criticalBasesOwned + 1 end if ZoneCapture_Alakurtti:GetCoalition() == coalition.side.RED then criticalBasesOwned = criticalBasesOwned + 1 end US_CC:MessageTypeToCoalition( string.format( "⚠️ CRITICAL: %s captured by Russia! %d/3 starting bases lost!", zoneName, criticalBasesOwned ), MESSAGE.Type.Information ) RU_CC:MessageTypeToCoalition( string.format( "Excellent! %s captured. %d/3 critical bases secured!", zoneName, criticalBasesOwned ), MESSAGE.Type.Information ) else US_CC:MessageTypeToCoalition( string.format( "%s is captured by Russia, we lost it!", zoneName ), MESSAGE.Type.Information ) RU_CC:MessageTypeToCoalition( string.format( "We captured %s, Excellent job!", zoneName ), MESSAGE.Type.Information ) end end ZoneCapture:AddScore( "Captured", "Zone captured: Extra points granted.", 200 ) ZoneCapture:__Guard( 30 ) -- Create/update tactical information marker CreateTacticalInfoMarker(ZoneCapture) -- Check victory condition after any zone capture CheckVictoryCondition() end -- Set up event handlers for each zone with proper MOOSE methods and debugging local zoneNames = { "Kilpyavr", "Severomorsk-1", "Severomorsk-3", "Murmansk International", "Monchegorsk", "Olenya", "Afrikanda", "The Mountain", "The River", "The Gulf", "The Lakes", "Luostari Pechenga", "Ivalo", "Alakurtti" } for i, zoneCapture in ipairs(zoneCaptureObjects) do if zoneCapture then local zoneName = zoneNames[i] or ("Zone " .. i) -- Proper MOOSE event handlers for ZONE_CAPTURE_COALITION zoneCapture.OnEnterGuarded = OnEnterGuarded zoneCapture.OnEnterEmpty = OnEnterEmpty zoneCapture.OnEnterAttacked = OnEnterAttacked zoneCapture.OnEnterCaptured = OnEnterCaptured -- Debug: Check if the underlying zone exists local success, zone = pcall(function() return zoneCapture:GetZone() end) if success and zone then log("✓ Zone 'Capture " .. zoneName .. "' successfully created and linked") -- Initialize zone borders with initial RED color (all zones start as RED coalition) local drawSuccess, drawError = pcall(function() local color = ZONE_COLORS.RED_CAPTURED zone:DrawZone(-1, {0, 0, 0}, 1, color, 0.2, 2, true) end) if not drawSuccess then log("⚠ Zone 'Capture " .. zoneName .. "' border drawing failed: " .. tostring(drawError)) -- Alternative: Try simpler zone marking pcall(function() zone:SmokeZone(SMOKECOLOR.Red, 30) end) else log("✓ Zone 'Capture " .. zoneName .. "' border drawn successfully with RED initial color") end else log("✗ ERROR: Zone 'Capture " .. zoneName .. "' not found in mission editor!") log(" Make sure you have a trigger zone named exactly: 'Capture " .. zoneName .. "'") end else log("✗ ERROR: Zone capture object " .. i .. " (" .. (zoneNames[i] or "Unknown") .. ") is nil!") end end -- ========================================== -- VICTORY MONITORING SYSTEM -- ========================================== -- Function to get current zone ownership status local function GetZoneOwnershipStatus() local status = { blue = 0, red = 0, neutral = 0, total = #zoneCaptureObjects, zones = {} } -- Explicitly reference the global coalition table to avoid parameter shadowing local coalitionTable = _G.coalition or coalition for i, zoneCapture in ipairs(zoneCaptureObjects) do if zoneCapture then local zoneCoalition = zoneCapture:GetCoalition() local zoneName = zoneNames[i] or ("Zone " .. i) -- Get the current state of the zone local currentState = zoneCapture:GetCurrentState() local stateString = "" -- Determine status based on coalition and state if zoneCoalition == coalitionTable.side.BLUE then status.blue = status.blue + 1 if currentState == "Attacked" then status.zones[zoneName] = "BLUE (Under Attack)" else status.zones[zoneName] = "BLUE" end elseif zoneCoalition == coalitionTable.side.RED then status.red = status.red + 1 if currentState == "Attacked" then status.zones[zoneName] = "RED (Under Attack)" else status.zones[zoneName] = "RED" end else status.neutral = status.neutral + 1 if currentState == "Attacked" then status.zones[zoneName] = "NEUTRAL (Under Attack)" else status.zones[zoneName] = "NEUTRAL" end end end end return status end -- Function to broadcast zone status report local function BroadcastZoneStatus() local status = GetZoneOwnershipStatus() local reportMessage = string.format( "ZONE CONTROL REPORT:\n" .. "Blue Coalition: %d/%d zones\n" .. "Red Coalition: %d/%d zones\n" .. "Neutral: %d/%d zones\n\n" .. "Progress to Victory: %d%%", status.blue, status.total, status.red, status.total, status.neutral, status.total, math.floor((status.blue / status.total) * 100) ) -- Add detailed zone status local detailMessage = "\nZONE DETAILS:\n" for zoneName, owner in pairs(status.zones) do detailMessage = detailMessage .. string.format("• %s: %s\n", zoneName, owner) end local fullMessage = reportMessage .. detailMessage US_CC:MessageTypeToCoalition( fullMessage, MESSAGE.Type.Information, MESSAGE_CONFIG.STATUS_MESSAGE_DURATION ) log("[ZONE STATUS] " .. reportMessage:gsub("\n", " | ")) return status end -- Periodic zone monitoring (every 5 minutes) local ZoneMonitorScheduler = SCHEDULER:New( nil, function() local status = BroadcastZoneStatus() -- Check if we're close to victory (80% or more zones captured) if status.blue >= math.floor(status.total * 0.8) and status.blue < status.total then US_CC:MessageTypeToCoalition( string.format("APPROACHING VICTORY! %d more zone(s) needed for complete success!", status.total - status.blue), MESSAGE.Type.Information, MESSAGE_CONFIG.VICTORY_MESSAGE_DURATION ) RU_CC:MessageTypeToCoalition( string.format("CRITICAL SITUATION! Only %d zone(s) remain under our control!", status.red), MESSAGE.Type.Information, MESSAGE_CONFIG.VICTORY_MESSAGE_DURATION ) end end, {}, MESSAGE_CONFIG.STATUS_BROADCAST_START_DELAY, MESSAGE_CONFIG.STATUS_BROADCAST_FREQUENCY ) -- Configurable timing -- Periodic zone color verification system (every 2 minutes) local ZoneColorVerificationScheduler = SCHEDULER:New( nil, function() log("[ZONE COLORS] Running periodic zone color verification...") -- Verify each zone's visual marker matches its CURRENT STATE (not just coalition) for i, zoneCapture in ipairs(zoneCaptureObjects) do if zoneCapture then local zoneCoalition = zoneCapture:GetCoalition() local zoneName = zoneNames[i] or ("Zone " .. i) local currentState = zoneCapture:GetCurrentState() -- Get the appropriate color for this zone's current state local zoneColor = GetZoneColor(zoneCapture) -- Force redraw the zone with correct color based on CURRENT STATE zoneCapture:UndrawZone() zoneCapture:DrawZone(-1, {0, 0, 0}, 1, zoneColor, 0.2, 2, true) -- Log the color assignment for debugging local colorName = "UNKNOWN" if currentState == "Attacked" then colorName = (zoneCoalition == coalition.side.BLUE) and "LIGHT BLUE (Blue Attacked)" or "ORANGE (Red Attacked)" elseif currentState == "Empty" then colorName = "GREEN (Empty)" elseif zoneCoalition == coalition.side.BLUE then colorName = "BLUE (Blue Captured)" elseif zoneCoalition == coalition.side.RED then colorName = "RED (Red Captured)" else colorName = "GREEN (Fallback)" end log(string.format("[ZONE COLORS] %s: Set to %s", zoneName, colorName)) end end end, {}, MESSAGE_CONFIG.COLOR_VERIFICATION_START_DELAY, MESSAGE_CONFIG.COLOR_VERIFICATION_FREQUENCY ) -- Configurable timing -- Periodic tactical marker update system with change detection (every 3 minutes) local __lastForceCountsByZone = {} local TacticalMarkerUpdateScheduler = SCHEDULER:New( nil, function() log("[TACTICAL] Running periodic tactical marker update (change-detected)...") for i, zoneCapture in ipairs(zoneCaptureObjects) do if zoneCapture then local zoneName = zoneNames and zoneNames[i] or (zoneCapture.GetZoneName and zoneCapture:GetZoneName()) 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) else -- unchanged: skip marker churn end end end end, {}, MESSAGE_CONFIG.TACTICAL_UPDATE_START_DELAY, MESSAGE_CONFIG.TACTICAL_UPDATE_FREQUENCY ) -- Configurable timing -- Function to refresh all zone colors based on current ownership local function RefreshAllZoneColors() log("[ZONE COLORS] Refreshing all zone visual markers...") for i, zoneCapture in ipairs(zoneCaptureObjects) do if zoneCapture then local zoneCoalition = zoneCapture:GetCoalition() local zoneName = zoneNames[i] or ("Zone " .. i) local currentState = zoneCapture:GetCurrentState() -- Get the appropriate color for this zone's current state local zoneColor = GetZoneColor(zoneCapture) -- Clear existing drawings zoneCapture:UndrawZone() -- 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" if currentState == "Attacked" then colorName = (zoneCoalition == coalition.side.BLUE) and "LIGHT BLUE (Blue Attacked)" or "ORANGE (Red Attacked)" elseif currentState == "Empty" then colorName = "GREEN (Empty)" elseif zoneCoalition == coalition.side.BLUE then colorName = "BLUE (Blue Captured)" elseif zoneCoalition == coalition.side.RED then colorName = "RED (Red Captured)" else colorName = "GREEN (Fallback)" end log(string.format("[ZONE COLORS] %s: Set to %s", zoneName, colorName)) end end US_CC:MessageTypeToCoalition("Zone visual markers have been refreshed!", MESSAGE.Type.Information, 5) end -- Manual zone status command for players (F10 radio menu) local function SetupZoneStatusCommands() -- Add F10 radio menu commands for zone status if US_CC then -- Use MenuManager if available, otherwise create root menu local USMenu if MenuManager then USMenu = MenuManager.CreateCoalitionMenu(coalition.side.BLUE, "Zone Control") else USMenu = MENU_COALITION:New(coalition.side.BLUE, "Zone Control") end MENU_COALITION_COMMAND:New( coalition.side.BLUE, "Get Zone Status Report", USMenu, BroadcastZoneStatus ) MENU_COALITION_COMMAND:New( coalition.side.BLUE, "Check Victory Progress", USMenu, function() local status = GetZoneOwnershipStatus() local progressPercent = math.floor((status.blue / status.total) * 100) US_CC:MessageTypeToCoalition( string.format( "VICTORY PROGRESS: %d%%\n" .. "Zones Captured: %d/%d\n" .. "Remaining: %d zones\n\n" .. "%s", progressPercent, status.blue, status.total, status.total - status.blue, progressPercent >= 100 and "MISSION COMPLETE!" or progressPercent >= 80 and "ALMOST THERE!" or progressPercent >= 50 and "GOOD PROGRESS!" or "KEEP FIGHTING!" ), MESSAGE.Type.Information, 10 ) end ) -- Add command to refresh zone colors (troubleshooting tool) MENU_COALITION_COMMAND:New( coalition.side.BLUE, "Refresh Zone Colors", USMenu, RefreshAllZoneColors ) end end -- Initialize zone status monitoring SCHEDULER:New( nil, function() log("[VICTORY SYSTEM] Initializing zone monitoring system...") -- Initialize performance optimization caches InitializeCachedUnitSet() SetupZoneStatusCommands() -- Initial status report SCHEDULER:New( nil, function() log("[VICTORY SYSTEM] Broadcasting initial zone status...") BroadcastZoneStatus() end, {}, 30 ) -- Initial report after 30 seconds end, {}, 5 ) -- Initialize after 5 seconds log("[VICTORY SYSTEM] Zone capture victory monitoring system loaded successfully!")