--[[ Script: Moose_DynamicGroundBattle_Plugin.lua Written by: [F99th-TracerFacer] Version: 1.0.0 Date: 15 November 2024 Description: Warehouse-driven ground unit spawning system that works as a plugin with Moose_DualCoalitionZoneCapture.lua This script handles: - Warehouse-based reinforcement system - Dynamic spawn frequency based on warehouse survival - Automated AI tasking to patrol nearest enemy zones - Optional infantry patrol control - Warehouse status intel markers - CTLD troop integration What this script DOES NOT do: - Zone capture logic (handled by Moose_DualCoalitionZoneCapture.lua) - Win conditions (handled by Moose_DualCoalitionZoneCapture.lua) - Zone coloring/messaging (handled by Moose_DualCoalitionZoneCapture.lua) Load Order (in Mission Editor Triggers): 1. DO SCRIPT FILE Moose_.lua 2. DO SCRIPT FILE Moose_DualCoalitionZoneCapture.lua 3. DO SCRIPT FILE Moose_DynamicGroundBattle_Plugin.lua <-- This file 4. DO SCRIPT FILE CTLD.lua (optional) 5. DO SCRIPT FILE CSAR.lua (optional) Requirements: - MOOSE framework must be loaded first - Moose_DualCoalitionZoneCapture.lua must be loaded BEFORE this script - Zone configuration comes from DualCoalitionZoneCapture's ZONE_CONFIG - Groups and warehouses must exist in mission editor (see below) Warehouse System & Spawn Frequency Behavior: 1. Each side has warehouses defined in `redWarehouses` and `blueWarehouses` tables 2. Spawn frequency dynamically adjusts based on alive warehouses: - 100% alive = 100% spawn rate (base frequency) - 50% alive = 50% spawn rate (2x delay) - 0% alive = no spawns (critical attrition) 3. Map markers show warehouse locations and nearby units 4. Updated every UPDATE_MARK_POINTS_SCHED seconds AI Task Assignment: - Groups spawn in friendly zones, then patrol toward nearest enemy zone - Reassignment occurs every ASSIGN_TASKS_SCHED seconds - Only stationary units get new orders (moving units are left alone) - CTLD-dropped troops automatically integrate Groups to Create in Mission Editor (all LATE ACTIVATE): RED SIDE: - Infantry Templates: RedInfantry1, RedInfantry2, RedInfantry3, RedInfantry4, RedInfantry5, RedInfantry6 - Armor Templates: RedArmor1, RedArmor2, RedArmor3, RedArmor4, RedArmor5, RedArmor6 - Spawn Groups: Names defined by RED_INFANTRY_SPAWN_GROUP and RED_ARMOR_SPAWN_GROUP variables (default: RedInfantryGroup, RedArmorGroup) - Warehouses (Static Objects): RedWarehouse1-1, RedWarehouse2-1, RedWarehouse3-1, etc. BLUE SIDE: - Infantry Templates: BlueInfantry1, BlueInfantry2, BlueInfantry3, BlueInfantry4, BlueInfantry5, BlueInfantry6 - Armor Templates: BlueArmor1, BlueArmor2, BlueArmor3, BlueArmor4, BlueArmor5 - Spawn Groups: Names defined by BLUE_INFANTRY_SPAWN_GROUP and BLUE_ARMOR_SPAWN_GROUP variables (default: BlueInfantryGroup, BlueArmorGroup) - Warehouses (Static Objects): BlueWarehouse1-1, BlueWarehouse2-1, BlueWarehouse3-1, etc. NOTE: Warehouse names use the static "Unit Name" in mission editor, not the "Name" field! NOTE: Spawn groups should be simple groups set to LATE ACTIVATE. You can customize their names in the USER CONFIGURATION section. Integration with DualCoalitionZoneCapture: - This script reads zoneCaptureObjects and zoneNames from DualCoalitionZoneCapture - Spawns occur in zones controlled by the appropriate coalition - AI tasks units to patrol zones from DualCoalitionZoneCapture's ZONE_CONFIG --]] ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- USER CONFIGURATION SECTION ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Infantry Patrol Settings local MOVING_INFANTRY_PATROLS = true -- Set to false to disable infantry movement (they spawn and hold position) -- Warehouse Marker Settings local ENABLE_WAREHOUSE_MARKERS = true -- Enable/disable warehouse map markers (disabled by default if you have other marker systems) local UPDATE_MARK_POINTS_SCHED = 300 -- Update warehouse markers every 300 seconds (5 minutes) local MAX_WAREHOUSE_UNIT_LIST_DISTANCE = 5000 -- Max distance to search for units near warehouses for markers -- Spawn Frequency and Limits -- Red Side Settings local INIT_RED_INFANTRY = 25 -- Initial number of Red Infantry groups local MAX_RED_INFANTRY = 100 -- Maximum number of Red Infantry groups local SPAWN_SCHED_RED_INFANTRY = 1800 -- Base spawn frequency for Red Infantry (seconds) local INIT_RED_ARMOR = 25 -- Initial number of Red Armor groups local MAX_RED_ARMOR = 500 -- Maximum number of Red Armor groups local SPAWN_SCHED_RED_ARMOR = 300 -- Base spawn frequency for Red Armor (seconds) -- Blue Side Settings local INIT_BLUE_INFANTRY = 25 -- Initial number of Blue Infantry groups local MAX_BLUE_INFANTRY = 100 -- Maximum number of Blue Infantry groups local SPAWN_SCHED_BLUE_INFANTRY = 1800 -- Base spawn frequency for Blue Infantry (seconds) local INIT_BLUE_ARMOR = 25 -- Initial number of Blue Armor groups local MAX_BLUE_ARMOR = 500 -- Maximum number of Blue Armor groups local SPAWN_SCHED_BLUE_ARMOR = 300 -- Base spawn frequency for Blue Armor (seconds) local ASSIGN_TASKS_SCHED = 600 -- How often to reassign tasks to idle groups (seconds) -- Per-side cadence scalars (tune to make one side faster/slower without touching base frequencies) local RED_INFANTRY_CADENCE_SCALAR = 1.0 local RED_ARMOR_CADENCE_SCALAR = 1.0 local BLUE_INFANTRY_CADENCE_SCALAR = 1.0 local BLUE_ARMOR_CADENCE_SCALAR = 1.0 -- When a side loses every warehouse we pause spawning and re-check after this delay local NO_WAREHOUSE_RECHECK_DELAY = 180 -- Define warehouses for each side local redWarehouses = { STATIC:FindByName("RedWarehouse1-1"), STATIC:FindByName("RedWarehouse2-1"), STATIC:FindByName("RedWarehouse3-1"), STATIC:FindByName("RedWarehouse4-1"), STATIC:FindByName("RedWarehouse5-1"), STATIC:FindByName("RedWarehouse6-1") } local blueWarehouses = { STATIC:FindByName("BlueWarehouse1-1"), STATIC:FindByName("BlueWarehouse2-1"), STATIC:FindByName("BlueWarehouse3-1"), STATIC:FindByName("BlueWarehouse4-1"), STATIC:FindByName("BlueWarehouse5-1"), STATIC:FindByName("BlueWarehouse6-1") } -- Define unit templates (these groups must exist in mission editor as LATE ACTIVATE) local redInfantryTemplates = { "RedInfantry1", "RedInfantry2", "RedInfantry3", "RedInfantry4", "RedInfantry5", "RedInfantry6" } local redArmorTemplates = { "RedArmor1", "RedArmor2", "RedArmor3", "RedArmor4", "RedArmor5", "RedArmor6" } local blueInfantryTemplates = { "BlueInfantry1", "BlueInfantry2", "BlueInfantry3", "BlueInfantry4", "BlueInfantry5", "BlueInfantry6" } local blueArmorTemplates = { "BlueArmor1", "BlueArmor2", "BlueArmor3", "BlueArmor4", "BlueArmor5" } -- Spawn Group Names (these are the base groups SPAWN:New() uses for spawning) local RED_INFANTRY_SPAWN_GROUP = "RedInfantryGroup" local RED_ARMOR_SPAWN_GROUP = "RedArmorGroup" local BLUE_INFANTRY_SPAWN_GROUP = "BlueInfantryGroup" local BLUE_ARMOR_SPAWN_GROUP = "BlueArmorGroup" ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- DO NOT EDIT BELOW THIS LINE ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- env.info("[DGB PLUGIN] Dynamic Ground Battle Plugin initializing...") -- Validate that DualCoalitionZoneCapture is loaded if not zoneCaptureObjects or not zoneNames then env.error("[DGB PLUGIN] ERROR: Moose_DualCoalitionZoneCapture.lua must be loaded BEFORE this plugin!") env.error("[DGB PLUGIN] Make sure zoneCaptureObjects and zoneNames are available.") return end -- Validate warehouses exist local function ValidateWarehouses(warehouses, label) local foundCount = 0 local missingCount = 0 for i, wh in ipairs(warehouses) do if wh then foundCount = foundCount + 1 env.info(string.format("[DGB PLUGIN] %s warehouse %d: %s (OK)", label, i, wh:GetName())) else missingCount = missingCount + 1 env.warning(string.format("[DGB PLUGIN] %s warehouse at index %d NOT FOUND in mission editor!", label, i)) end end env.info(string.format("[DGB PLUGIN] %s warehouses: %d found, %d missing", label, foundCount, missingCount)) return foundCount > 0 end -- Validate unit templates exist local function ValidateTemplates(templates, label) local foundCount = 0 local missingCount = 0 for i, templateName in ipairs(templates) do local group = GROUP:FindByName(templateName) if group then foundCount = foundCount + 1 env.info(string.format("[DGB PLUGIN] %s template %d: %s (OK)", label, i, templateName)) else missingCount = missingCount + 1 env.warning(string.format("[DGB PLUGIN] %s template '%s' NOT FOUND in mission editor!", label, templateName)) end end env.info(string.format("[DGB PLUGIN] %s templates: %d found, %d missing", label, foundCount, missingCount)) return foundCount > 0 end env.info("[DGB PLUGIN] Validating configuration...") -- Validate all warehouses local redWarehousesValid = ValidateWarehouses(redWarehouses, "Red") local blueWarehousesValid = ValidateWarehouses(blueWarehouses, "Blue") if not redWarehousesValid then env.warning("[DGB PLUGIN] WARNING: No valid Red warehouses found! Red spawning will be disabled.") end if not blueWarehousesValid then env.warning("[DGB PLUGIN] WARNING: No valid Blue warehouses found! Blue spawning will be disabled.") end -- Validate all templates local redInfantryValid = ValidateTemplates(redInfantryTemplates, "Red Infantry") local redArmorValid = ValidateTemplates(redArmorTemplates, "Red Armor") local blueInfantryValid = ValidateTemplates(blueInfantryTemplates, "Blue Infantry") local blueArmorValid = ValidateTemplates(blueArmorTemplates, "Blue Armor") if not redInfantryValid then env.warning("[DGB PLUGIN] WARNING: No valid Red Infantry templates found! Red Infantry spawning will fail.") end if not redArmorValid then env.warning("[DGB PLUGIN] WARNING: No valid Red Armor templates found! Red Armor spawning will fail.") end if not blueInfantryValid then env.warning("[DGB PLUGIN] WARNING: No valid Blue Infantry templates found! Blue Infantry spawning will fail.") end if not blueArmorValid then env.warning("[DGB PLUGIN] WARNING: No valid Blue Armor templates found! Blue Armor spawning will fail.") end env.info("[DGB PLUGIN] Found " .. #zoneCaptureObjects .. " zones from DualCoalitionZoneCapture") -- Track active markers to prevent memory leaks local activeMarkers = {} -- Reusable SET_GROUP to prevent memory leaks from repeated creation local cachedAllGroups = nil local function getAllGroups() if not cachedAllGroups then cachedAllGroups = SET_GROUP:New():FilterActive():FilterStart() env.info("[DGB PLUGIN] Created cached SET_GROUP for performance") end return cachedAllGroups end -- Function to get zones controlled by a specific coalition local function GetZonesByCoalition(targetCoalition) local zones = {} for idx, zoneCapture in ipairs(zoneCaptureObjects) do if zoneCapture and zoneCapture:GetCoalition() == targetCoalition then local zone = zoneCapture:GetZone() if zone then table.insert(zones, zone) end end end env.info(string.format("[DGB PLUGIN] Found %d zones for coalition %d", #zones, targetCoalition)) return zones end -- Helper to count warehouse availability local function GetWarehouseStats(warehouses) local alive = 0 local total = 0 for _, warehouse in ipairs(warehouses) do if warehouse then total = total + 1 local life = warehouse:GetLife() if life and life > 0 then alive = alive + 1 end end end return alive, total end -- Function to calculate spawn frequency based on warehouse survival local function CalculateSpawnFrequency(warehouses, baseFrequency, cadenceScalar) local aliveWarehouses, totalWarehouses = GetWarehouseStats(warehouses) cadenceScalar = cadenceScalar or 1 if totalWarehouses == 0 then return baseFrequency * cadenceScalar end if aliveWarehouses == 0 then return nil -- Pause spawning until logistics return end local frequency = baseFrequency * cadenceScalar * (totalWarehouses / aliveWarehouses) return frequency end -- Function to calculate spawn frequency as a percentage local function CalculateSpawnFrequencyPercentage(warehouses) local aliveWarehouses, totalWarehouses = GetWarehouseStats(warehouses) if totalWarehouses == 0 then return 0 end local percentage = (aliveWarehouses / totalWarehouses) * 100 return math.floor(percentage) end -- Function to add warehouse markers on the map local function addMarkPoints(warehouses, coalition) for _, warehouse in ipairs(warehouses) do if warehouse then local warehousePos = warehouse:GetVec3() local details if coalition == 2 then -- Blue viewing if warehouse:GetCoalition() == 2 then details = "Warehouse: " .. warehouse:GetName() .. "\nThis warehouse needs to be protected.\n" else details = "Warehouse: " .. warehouse:GetName() .. "\nThis is a primary target as it is directly supplying enemy units.\n" end elseif coalition == 1 then -- Red viewing if warehouse:GetCoalition() == 1 then details = "Warehouse: " .. warehouse:GetName() .. "\nThis warehouse needs to be protected.\n" else details = "Warehouse: " .. warehouse:GetName() .. "\nThis is a primary target as it is directly supplying enemy units.\n" end end local coordinate = COORDINATE:NewFromVec3(warehousePos) local marker = MARKER:New(coordinate, details):ToCoalition(coalition):ReadOnly() table.insert(activeMarkers, marker) end end end -- Function to update warehouse markers local function updateMarkPoints() -- Clean up old markers first for _, marker in ipairs(activeMarkers) do if marker then marker:Remove() end end activeMarkers = {} addMarkPoints(redWarehouses, 2) -- Blue coalition sees red warehouses addMarkPoints(blueWarehouses, 2) -- Blue coalition sees blue warehouses addMarkPoints(redWarehouses, 1) -- Red coalition sees red warehouses addMarkPoints(blueWarehouses, 1) -- Red coalition sees blue warehouses env.info("[DGB PLUGIN] Updated warehouse markers") end -- Function to check if a group contains infantry units local function IsInfantryGroup(group) for _, unit in ipairs(group:GetUnits()) do local unitTypeName = unit:GetTypeName() if unitTypeName:find("Infantry") or unitTypeName:find("Soldier") or unitTypeName:find("Paratrooper") then return true end end return false end local function AssignTasks(group, currentZoneCapture) if not group or not group.GetCoalition or not group.GetCoordinate or not group.GetVelocityVec3 then return end -- Don't reassign if already moving local velocity = group:GetVelocityVec3() local speed = math.sqrt(velocity.x^2 + velocity.y^2 + velocity.z^2) if speed > 0.5 then return end local groupCoalition = group:GetCoalition() local groupCoordinate = group:GetCoordinate() local currentZone = currentZoneCapture and currentZoneCapture:GetZone() or nil -- If the group is sitting inside a friendly zone that is currently under attack, -- keep them local so they fight for the objective instead of leaving it exposed. if currentZoneCapture and currentZone and currentZoneCapture.GetCoalition and currentZoneCapture:GetCoalition() == groupCoalition then local zoneState = currentZoneCapture.GetCurrentState and currentZoneCapture:GetCurrentState() or nil if zoneState == "Attacked" then env.info(string.format("[DGB PLUGIN] %s defending contested zone %s", group:GetName(), currentZone:GetName())) group:PatrolZones({ currentZone }, 20, "Cone", 30, 60) return end end local closestZone = nil local closestDistance = math.huge -- Find nearest enemy zone for idx, zoneCapture in ipairs(zoneCaptureObjects) do local zoneCoalition = zoneCapture:GetCoalition() if zoneCoalition ~= groupCoalition and zoneCoalition ~= coalition.side.NEUTRAL then local zone = zoneCapture:GetZone() if zone then local zoneCoordinate = zone:GetCoordinate() local distance = groupCoordinate:Get2DDistance(zoneCoordinate) if distance < closestDistance then closestDistance = distance closestZone = zone end end end end if closestZone then env.info(string.format("[DGB PLUGIN] %s patrolling %s", group:GetName(), closestZone:GetName())) group:PatrolZones({closestZone}, 20, "Cone", 30, 60) end end -- Function to assign tasks to all groups local function AssignTasksToGroups() env.info("[DGB PLUGIN] Starting task assignment cycle...") local allGroups = getAllGroups() local tasksAssigned = 0 allGroups:ForEachGroup(function(group) if group and group:IsAlive() then -- Check if group is in a friendly zone local groupCoalition = group:GetCoalition() local inFriendlyZone = false local currentZoneCapture = nil for idx, zoneCapture in ipairs(zoneCaptureObjects) do if zoneCapture:GetCoalition() == groupCoalition then local zone = zoneCapture:GetZone() if zone and group:IsCompletelyInZone(zone) then inFriendlyZone = true currentZoneCapture = zoneCapture break end end end if inFriendlyZone then -- Skip infantry if movement is disabled if IsInfantryGroup(group) and not MOVING_INFANTRY_PATROLS then return end AssignTasks(group, currentZoneCapture) tasksAssigned = tasksAssigned + 1 end end end) env.info(string.format("[DGB PLUGIN] Task assignment complete. %d groups tasked.", tasksAssigned)) end -- Function to monitor and announce warehouse status local function MonitorWarehouses() local blueWarehousesAlive, blueWarehouseTotal = GetWarehouseStats(blueWarehouses) local redWarehousesAlive, redWarehouseTotal = GetWarehouseStats(redWarehouses) local redSpawnFrequencyPercentage = CalculateSpawnFrequencyPercentage(redWarehouses) local blueSpawnFrequencyPercentage = CalculateSpawnFrequencyPercentage(blueWarehouses) local msg = "[Warehouse Status]\n" msg = msg .. "Red warehouses alive: " .. redWarehousesAlive .. " Reinforcements: " .. redSpawnFrequencyPercentage .. "%\n" msg = msg .. "Blue warehouses alive: " .. blueWarehousesAlive .. " Reinforcements: " .. blueSpawnFrequencyPercentage .. "%\n" MESSAGE:New(msg, 30):ToAll() env.info(string.format("[DGB PLUGIN] Warehouse status - Red: %d/%d (%d%%), Blue: %d/%d (%d%%)", redWarehousesAlive, redWarehouseTotal, redSpawnFrequencyPercentage, blueWarehousesAlive, blueWarehouseTotal, blueSpawnFrequencyPercentage)) end ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- INITIALIZATION ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Get initial zone lists for each coalition local redZones = GetZonesByCoalition(coalition.side.RED) local blueZones = GetZonesByCoalition(coalition.side.BLUE) -- Calculate and display initial spawn frequency percentages local redSpawnFrequencyPercentage = CalculateSpawnFrequencyPercentage(redWarehouses) local blueSpawnFrequencyPercentage = CalculateSpawnFrequencyPercentage(blueWarehouses) MESSAGE:New("Red reinforcement capacity: " .. redSpawnFrequencyPercentage .. "%", 30):ToRed() MESSAGE:New("Blue reinforcement capacity: " .. blueSpawnFrequencyPercentage .. "%", 30):ToBlue() -- Initialize spawners env.info("[DGB PLUGIN] Initializing spawn systems...") -- Note: Spawn zones will be dynamically updated based on zone capture states -- We'll use a function to get current friendly zones on each spawn local function GetRedZones() return GetZonesByCoalition(coalition.side.RED) end local function GetBlueZones() return GetZonesByCoalition(coalition.side.BLUE) end -- Validate spawn groups exist before creating spawners local spawnGroups = { {name = RED_INFANTRY_SPAWN_GROUP, label = "Red Infantry Spawn Group"}, {name = RED_ARMOR_SPAWN_GROUP, label = "Red Armor Spawn Group"}, {name = BLUE_INFANTRY_SPAWN_GROUP, label = "Blue Infantry Spawn Group"}, {name = BLUE_ARMOR_SPAWN_GROUP, label = "Blue Armor Spawn Group"} } for _, spawnGroup in ipairs(spawnGroups) do local group = GROUP:FindByName(spawnGroup.name) if group then env.info(string.format("[DGB PLUGIN] %s '%s' found (OK)", spawnGroup.label, spawnGroup.name)) else env.error(string.format("[DGB PLUGIN] ERROR: %s '%s' NOT FOUND! Create this group in mission editor as LATE ACTIVATE.", spawnGroup.label, spawnGroup.name)) end end -- Red Infantry Spawner redInfantrySpawn = SPAWN:New(RED_INFANTRY_SPAWN_GROUP) :InitRandomizeTemplate(redInfantryTemplates) :InitLimit(INIT_RED_INFANTRY, MAX_RED_INFANTRY) -- Red Armor Spawner redArmorSpawn = SPAWN:New(RED_ARMOR_SPAWN_GROUP) :InitRandomizeTemplate(redArmorTemplates) :InitLimit(INIT_RED_ARMOR, MAX_RED_ARMOR) -- Blue Infantry Spawner blueInfantrySpawn = SPAWN:New(BLUE_INFANTRY_SPAWN_GROUP) :InitRandomizeTemplate(blueInfantryTemplates) :InitLimit(INIT_BLUE_INFANTRY, MAX_BLUE_INFANTRY) -- Blue Armor Spawner blueArmorSpawn = SPAWN:New(BLUE_ARMOR_SPAWN_GROUP) :InitRandomizeTemplate(blueArmorTemplates) :InitLimit(INIT_BLUE_ARMOR, MAX_BLUE_ARMOR) -- Helper to schedule spawns per category so each uses its intended cadence. local function ScheduleSpawner(spawnObject, getZonesFn, warehouses, baseFrequency, label, cadenceScalar) local scheduler local function spawnCycle() local nextInterval = CalculateSpawnFrequency(warehouses, baseFrequency, cadenceScalar) if not nextInterval then env.info(string.format("[DGB PLUGIN] %s spawn paused (no warehouses alive)", label)) if scheduler then scheduler:Stop() scheduler:Start(NO_WAREHOUSE_RECHECK_DELAY, NO_WAREHOUSE_RECHECK_DELAY) end return end local friendlyZones = getZonesFn() local zonesAvailable = #friendlyZones if zonesAvailable > 0 then local chosenZone = friendlyZones[math.random(zonesAvailable)] spawnObject:SpawnInZone(chosenZone, false) else env.info(string.format("[DGB PLUGIN] %s spawn skipped (no friendly zones)", label)) end if scheduler then scheduler:Stop() scheduler:Start(nextInterval, nextInterval) end end local initialFrequency = baseFrequency * (cadenceScalar or 1) scheduler = SCHEDULER:New(nil, spawnCycle, {}, math.random(5, 15), initialFrequency) return scheduler end -- Schedule spawns (each spawner now runs at its own configured cadence) ScheduleSpawner(redInfantrySpawn, GetRedZones, redWarehouses, SPAWN_SCHED_RED_INFANTRY, "Red Infantry", RED_INFANTRY_CADENCE_SCALAR) ScheduleSpawner(redArmorSpawn, GetRedZones, redWarehouses, SPAWN_SCHED_RED_ARMOR, "Red Armor", RED_ARMOR_CADENCE_SCALAR) ScheduleSpawner(blueInfantrySpawn, GetBlueZones, blueWarehouses, SPAWN_SCHED_BLUE_INFANTRY, "Blue Infantry", BLUE_INFANTRY_CADENCE_SCALAR) ScheduleSpawner(blueArmorSpawn, GetBlueZones, blueWarehouses, SPAWN_SCHED_BLUE_ARMOR, "Blue Armor", BLUE_ARMOR_CADENCE_SCALAR) -- Schedule warehouse marker updates if ENABLE_WAREHOUSE_MARKERS then SCHEDULER:New(nil, updateMarkPoints, {}, 10, UPDATE_MARK_POINTS_SCHED) end -- Schedule warehouse monitoring SCHEDULER:New(nil, MonitorWarehouses, {}, 30, 120) -- Schedule task assignments SCHEDULER:New(nil, AssignTasksToGroups, {}, 120, ASSIGN_TASKS_SCHED) -- Add F10 menu for manual checks (using MenuManager if available) if MenuManager then -- Create coalition-specific menus under Mission Options local blueMenu = MenuManager.CreateCoalitionMenu(coalition.side.BLUE, "Ground Battle") MENU_COALITION_COMMAND:New(coalition.side.BLUE, "Check Warehouse Status", blueMenu, MonitorWarehouses) local redMenu = MenuManager.CreateCoalitionMenu(coalition.side.RED, "Ground Battle") MENU_COALITION_COMMAND:New(coalition.side.RED, "Check Warehouse Status", redMenu, MonitorWarehouses) else -- Fallback to root-level mission menu local missionMenu = MENU_MISSION:New("Ground Battle") MENU_MISSION_COMMAND:New("Check Warehouse Status", missionMenu, MonitorWarehouses) end env.info("[DGB PLUGIN] Dynamic Ground Battle Plugin initialized successfully!") env.info(string.format("[DGB PLUGIN] Infantry movement: %s", MOVING_INFANTRY_PATROLS and "ENABLED" or "DISABLED")) env.info(string.format("[DGB PLUGIN] Warehouse markers: %s", ENABLE_WAREHOUSE_MARKERS and "ENABLED" or "DISABLED"))