--[[ 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 - Zone garrison system (defenders stay in captured 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 - Each zone maintains a minimum garrison (defenders) that patrol only their zone - Non-defender groups patrol toward nearest enemy zone - Election system assigns defenders automatically based on zone needs - Defenders are never reassigned and stay permanently in their zone - Reassignment occurs every ASSIGN_TASKS_SCHED seconds for non-defenders only - 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 ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Zone Garrison (Defender) Settings local DEFENDERS_PER_ZONE = 2 -- Minimum number of groups that will garrison each friendly zone (recommended: 2) local ALLOW_DEFENDER_ROTATION = true -- If true, fresh units can replace existing defenders when zone is over-garrisoned -- 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 -- Warehouse Status Message Settings local ENABLE_WAREHOUSE_STATUS_MESSAGES = true -- Enable/disable periodic warehouse status announcements local WAREHOUSE_STATUS_MESSAGE_FREQUENCY = 1800 -- How often to announce warehouse status (seconds, default: 1800 = 30 minutes) -- 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 = 1200 -- 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 = 200 -- 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 = 1200 -- 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 = 200 -- 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 = {} -- Zone Garrison Tracking System -- Structure: zoneGarrisons[zoneName] = { defenders = {groupName1, groupName2, ...}, lastUpdate = timestamp } local zoneGarrisons = {} -- Group garrison assignments -- Structure: groupGarrisonAssignments[groupName] = zoneName (or nil if not a defender) local groupGarrisonAssignments = {} -- 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 -- Function to check if a group is assigned as a zone defender local function IsDefender(group) if not group then return false end local groupName = group:GetName() return groupGarrisonAssignments[groupName] ~= nil end -- Function to get garrison info for a zone local function GetZoneGarrison(zoneName) if not zoneGarrisons[zoneName] then zoneGarrisons[zoneName] = { defenders = {}, lastUpdate = timer.getTime() } end return zoneGarrisons[zoneName] end -- Function to count alive defenders in a zone local function CountAliveDefenders(zoneName) local garrison = GetZoneGarrison(zoneName) local aliveCount = 0 local deadDefenders = {} for _, groupName in ipairs(garrison.defenders) do local group = GROUP:FindByName(groupName) if group and group:IsAlive() then aliveCount = aliveCount + 1 else -- Mark for cleanup table.insert(deadDefenders, groupName) end end -- Clean up dead defenders for _, deadGroupName in ipairs(deadDefenders) do for i, groupName in ipairs(garrison.defenders) do if groupName == deadGroupName then table.remove(garrison.defenders, i) groupGarrisonAssignments[deadGroupName] = nil env.info(string.format("[DGB PLUGIN] Removed destroyed defender %s from zone %s", deadGroupName, zoneName)) break end end end return aliveCount end -- Function to elect a group as a zone defender local function ElectDefender(group, zone, reason) if not group or not zone then return false end local groupName = group:GetName() local zoneName = zone:GetName() -- Check if already a defender if IsDefender(group) then return false end local garrison = GetZoneGarrison(zoneName) -- Add to garrison table.insert(garrison.defenders, groupName) groupGarrisonAssignments[groupName] = zoneName garrison.lastUpdate = timer.getTime() -- Assign patrol task for the zone group:PatrolZones({zone}, 20, "Cone", 30, 60) env.info(string.format("[DGB PLUGIN] Elected %s as defender of zone %s (%s)", groupName, zoneName, reason)) return true end -- Function to check if a zone needs more defenders local function ZoneNeedsDefenders(zoneName) local aliveDefenders = CountAliveDefenders(zoneName) return aliveDefenders < DEFENDERS_PER_ZONE end -- Function to handle defender rotation (replace old defender with fresh unit) local function TryDefenderRotation(group, zone) if not ALLOW_DEFENDER_ROTATION then return false end local zoneName = zone:GetName() local garrison = GetZoneGarrison(zoneName) -- Count idle groups in zone (including current group) local idleGroups = {} local allGroups = getAllGroups() allGroups:ForEachGroup(function(g) if g and g:IsAlive() and g:GetCoalition() == group:GetCoalition() then if g:IsCompletelyInZone(zone) then local velocity = g:GetVelocityVec3() local speed = math.sqrt(velocity.x^2 + velocity.y^2 + velocity.z^2) if speed <= 0.5 then table.insert(idleGroups, g) end end end end) -- Only rotate if we have more than DEFENDERS_PER_ZONE idle units if #idleGroups > DEFENDERS_PER_ZONE then -- Find oldest defender to replace local oldestDefender = nil local oldestDefenderGroup = nil for _, defenderName in ipairs(garrison.defenders) do local defenderGroup = GROUP:FindByName(defenderName) if defenderGroup and defenderGroup:IsAlive() then if not oldestDefender then oldestDefender = defenderName oldestDefenderGroup = defenderGroup end break -- Just take the first one for rotation end end if oldestDefender and oldestDefenderGroup:GetName() ~= group:GetName() then -- Remove old defender for i, defenderName in ipairs(garrison.defenders) do if defenderName == oldestDefender then table.remove(garrison.defenders, i) groupGarrisonAssignments[oldestDefender] = nil env.info(string.format("[DGB PLUGIN] Rotated out defender %s from zone %s", oldestDefender, zoneName)) break end end -- Elect new defender ElectDefender(group, zone, "rotation") -- Old defender becomes mobile force 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 -- GARRISON SYSTEM: Defenders never leave their zone if IsDefender(group) then local assignedZoneName = groupGarrisonAssignments[group:GetName()] if assignedZoneName then -- Find the zone object for idx, zoneCapture in ipairs(zoneCaptureObjects) do local zone = zoneCapture:GetZone() if zone and zone:GetName() == assignedZoneName then -- Keep patrolling home zone group:PatrolZones({zone}, 20, "Cone", 30, 60) return end end end -- If we get here, the defender's zone was lost or not found, but they still stay put 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 -- GARRISON SYSTEM: Check if current zone needs defenders if currentZoneCapture and currentZone and currentZoneCapture:GetCoalition() == groupCoalition then local zoneName = currentZone:GetName() -- Try to elect as defender if zone needs one if ZoneNeedsDefenders(zoneName) then if ElectDefender(group, currentZone, "zone under-garrisoned") then return end else -- Try rotation if enabled if TryDefenderRotation(group, currentZone) then return end end -- If the zone is under attack, all units help defend (even non-defenders) 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 local defendersActive = 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 (unless they're defenders) if IsInfantryGroup(group) and not MOVING_INFANTRY_PATROLS and not IsDefender(group) then return end -- Count defenders if IsDefender(group) then defendersActive = defendersActive + 1 end AssignTasks(group, currentZoneCapture) tasksAssigned = tasksAssigned + 1 end end end) env.info(string.format("[DGB PLUGIN] Task assignment complete. %d groups tasked (%d defenders).", tasksAssigned, defendersActive)) 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) if ENABLE_WAREHOUSE_STATUS_MESSAGES then 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() end env.info(string.format("[DGB PLUGIN] Warehouse status - Red: %d/%d (%d%%), Blue: %d/%d (%d%%)", redWarehousesAlive, redWarehouseTotal, redSpawnFrequencyPercentage, blueWarehousesAlive, blueWarehouseTotal, blueSpawnFrequencyPercentage)) end -- Function to count active units by coalition and type local function CountActiveUnits(targetCoalition) local infantry = 0 local armor = 0 local total = 0 local defenders = 0 local mobile = 0 local allGroups = getAllGroups() allGroups:ForEachGroup(function(group) if group and group:IsAlive() and group:GetCoalition() == targetCoalition then total = total + 1 if IsDefender(group) then defenders = defenders + 1 else mobile = mobile + 1 end if IsInfantryGroup(group) then infantry = infantry + 1 else armor = armor + 1 end end end) return { total = total, infantry = infantry, armor = armor, defenders = defenders, mobile = mobile } end -- Function to get garrison status across all zones local function GetGarrisonStatus(targetCoalition) local garrisonedZones = 0 local underGarrisonedZones = 0 local totalFriendlyZones = 0 for idx, zoneCapture in ipairs(zoneCaptureObjects) do if zoneCapture:GetCoalition() == targetCoalition then totalFriendlyZones = totalFriendlyZones + 1 local zone = zoneCapture:GetZone() if zone then local zoneName = zone:GetName() local defenderCount = CountAliveDefenders(zoneName) if defenderCount >= DEFENDERS_PER_ZONE then garrisonedZones = garrisonedZones + 1 else underGarrisonedZones = underGarrisonedZones + 1 end end end end return { totalZones = totalFriendlyZones, garrisoned = garrisonedZones, underGarrisoned = underGarrisonedZones } end -- Function to display comprehensive system statistics local function ShowSystemStatistics(playerCoalition) -- Get warehouse stats local redWarehousesAlive, redWarehouseTotal = GetWarehouseStats(redWarehouses) local blueWarehousesAlive, blueWarehouseTotal = GetWarehouseStats(blueWarehouses) -- Get unit counts local redUnits = CountActiveUnits(coalition.side.RED) local blueUnits = CountActiveUnits(coalition.side.BLUE) -- Get garrison info local redGarrison = GetGarrisonStatus(coalition.side.RED) local blueGarrison = GetGarrisonStatus(coalition.side.BLUE) -- Get spawn frequencies local redSpawnFreqPct = CalculateSpawnFrequencyPercentage(redWarehouses) local blueSpawnFreqPct = CalculateSpawnFrequencyPercentage(blueWarehouses) -- Calculate actual spawn intervals local redInfantryInterval = CalculateSpawnFrequency(redWarehouses, SPAWN_SCHED_RED_INFANTRY, RED_INFANTRY_CADENCE_SCALAR) local redArmorInterval = CalculateSpawnFrequency(redWarehouses, SPAWN_SCHED_RED_ARMOR, RED_ARMOR_CADENCE_SCALAR) local blueInfantryInterval = CalculateSpawnFrequency(blueWarehouses, SPAWN_SCHED_BLUE_INFANTRY, BLUE_INFANTRY_CADENCE_SCALAR) local blueArmorInterval = CalculateSpawnFrequency(blueWarehouses, SPAWN_SCHED_BLUE_ARMOR, BLUE_ARMOR_CADENCE_SCALAR) -- Build comprehensive report local msg = "═══════════════════════════════════════\n" msg = msg .. "DYNAMIC GROUND BATTLE - SYSTEM STATUS\n" msg = msg .. "═══════════════════════════════════════\n\n" -- Configuration Section msg = msg .. "【CONFIGURATION】\n" msg = msg .. " Defenders per Zone: " .. DEFENDERS_PER_ZONE .. "\n" msg = msg .. " Defender Rotation: " .. (ALLOW_DEFENDER_ROTATION and "ENABLED" or "DISABLED") .. "\n" msg = msg .. " Infantry Movement: " .. (MOVING_INFANTRY_PATROLS and "ENABLED" or "DISABLED") .. "\n" msg = msg .. " Task Reassignment: Every " .. ASSIGN_TASKS_SCHED .. "s\n" msg = msg .. " Warehouse Markers: " .. (ENABLE_WAREHOUSE_MARKERS and "ENABLED" or "DISABLED") .. "\n\n" -- Spawn Limits Section msg = msg .. "【SPAWN LIMITS】\n" msg = msg .. " Red Infantry: " .. INIT_RED_INFANTRY .. "/" .. MAX_RED_INFANTRY .. "\n" msg = msg .. " Red Armor: " .. INIT_RED_ARMOR .. "/" .. MAX_RED_ARMOR .. "\n" msg = msg .. " Blue Infantry: " .. INIT_BLUE_INFANTRY .. "/" .. MAX_BLUE_INFANTRY .. "\n" msg = msg .. " Blue Armor: " .. INIT_BLUE_ARMOR .. "/" .. MAX_BLUE_ARMOR .. "\n\n" -- Red Coalition Section msg = msg .. "【RED COALITION】\n" msg = msg .. " Warehouses: " .. redWarehousesAlive .. "/" .. redWarehouseTotal .. " (" .. redSpawnFreqPct .. "%)\n" msg = msg .. " Active Units: " .. redUnits.total .. " (" .. redUnits.infantry .. " inf, " .. redUnits.armor .. " armor)\n" msg = msg .. " Defenders: " .. redUnits.defenders .. " | Mobile: " .. redUnits.mobile .. "\n" msg = msg .. " Controlled Zones: " .. redGarrison.totalZones .. "\n" msg = msg .. " - Garrisoned: " .. redGarrison.garrisoned .. "\n" msg = msg .. " - Under-Garrisoned: " .. redGarrison.underGarrisoned .. "\n" if redInfantryInterval then msg = msg .. " Infantry Spawn: " .. math.floor(redInfantryInterval) .. "s\n" else msg = msg .. " Infantry Spawn: PAUSED (no warehouses)\n" end if redArmorInterval then msg = msg .. " Armor Spawn: " .. math.floor(redArmorInterval) .. "s\n\n" else msg = msg .. " Armor Spawn: PAUSED (no warehouses)\n\n" end -- Blue Coalition Section msg = msg .. "【BLUE COALITION】\n" msg = msg .. " Warehouses: " .. blueWarehousesAlive .. "/" .. blueWarehouseTotal .. " (" .. blueSpawnFreqPct .. "%)\n" msg = msg .. " Active Units: " .. blueUnits.total .. " (" .. blueUnits.infantry .. " inf, " .. blueUnits.armor .. " armor)\n" msg = msg .. " Defenders: " .. blueUnits.defenders .. " | Mobile: " .. blueUnits.mobile .. "\n" msg = msg .. " Controlled Zones: " .. blueGarrison.totalZones .. "\n" msg = msg .. " - Garrisoned: " .. blueGarrison.garrisoned .. "\n" msg = msg .. " - Under-Garrisoned: " .. blueGarrison.underGarrisoned .. "\n" if blueInfantryInterval then msg = msg .. " Infantry Spawn: " .. math.floor(blueInfantryInterval) .. "s\n" else msg = msg .. " Infantry Spawn: PAUSED (no warehouses)\n" end if blueArmorInterval then msg = msg .. " Armor Spawn: " .. math.floor(blueArmorInterval) .. "s\n\n" else msg = msg .. " Armor Spawn: PAUSED (no warehouses)\n\n" end -- System Info msg = msg .. "【SYSTEM INFO】\n" msg = msg .. " Total Zones: " .. #zoneCaptureObjects .. "\n" msg = msg .. " Active Garrisons: " .. (redGarrison.garrisoned + blueGarrison.garrisoned) .. "\n" msg = msg .. " Total Active Units: " .. (redUnits.total + blueUnits.total) .. "\n\n" msg = msg .. "═══════════════════════════════════════" MESSAGE:New(msg, 45):ToCoalition(playerCoalition) env.info("[DGB PLUGIN] System statistics displayed to coalition " .. playerCoalition) 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 lastSpawnTime = 0 local checkInterval = 10 -- Check every 10 seconds if it's time to spawn local function spawnCheck() local currentTime = timer.getTime() local spawnInterval = CalculateSpawnFrequency(warehouses, baseFrequency, cadenceScalar) if not spawnInterval then -- No warehouses alive - use recheck delay spawnInterval = NO_WAREHOUSE_RECHECK_DELAY if currentTime - lastSpawnTime >= spawnInterval then env.info(string.format("[DGB PLUGIN] %s spawn paused (no warehouses alive, will recheck)", label)) lastSpawnTime = currentTime end return end -- Check if enough time has passed to spawn if currentTime - lastSpawnTime >= spawnInterval then local friendlyZones = getZonesFn() local zonesAvailable = #friendlyZones if zonesAvailable > 0 then local chosenZone = friendlyZones[math.random(zonesAvailable)] local spawnedGroup = spawnObject:SpawnInZone(chosenZone, false) -- Check if the spawn zone needs defenders and auto-elect if so if spawnedGroup then local zoneName = chosenZone:GetName() if ZoneNeedsDefenders(zoneName) then SCHEDULER:New(nil, function() local grp = GROUP:FindByName(spawnedGroup:GetName()) if grp and grp:IsAlive() then ElectDefender(grp, chosenZone, "spawn in under-garrisoned zone") end end, {}, 2) -- Delay 2 seconds to ensure group is fully initialized end end lastSpawnTime = currentTime else env.info(string.format("[DGB PLUGIN] %s spawn skipped (no friendly zones)", label)) lastSpawnTime = currentTime -- Reset timer even if no zones available end end end -- Single scheduler that runs continuously at fixed check interval local scheduler = SCHEDULER:New(nil, spawnCheck, {}, math.random(5, 15), checkInterval) 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 if ENABLE_WAREHOUSE_STATUS_MESSAGES then SCHEDULER:New(nil, MonitorWarehouses, {}, 30, WAREHOUSE_STATUS_MESSAGE_FREQUENCY) end -- 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) MENU_COALITION_COMMAND:New(coalition.side.BLUE, "Show System Statistics", blueMenu, function() ShowSystemStatistics(coalition.side.BLUE) end) local redMenu = MenuManager.CreateCoalitionMenu(coalition.side.RED, "Ground Battle") MENU_COALITION_COMMAND:New(coalition.side.RED, "Check Warehouse Status", redMenu, MonitorWarehouses) MENU_COALITION_COMMAND:New(coalition.side.RED, "Show System Statistics", redMenu, function() ShowSystemStatistics(coalition.side.RED) end) else -- Fallback to root-level mission menu local missionMenu = MENU_MISSION:New("Ground Battle") MENU_MISSION_COMMAND:New("Check Warehouse Status", missionMenu, MonitorWarehouses) MENU_MISSION_COMMAND:New("Show Blue Statistics", missionMenu, function() ShowSystemStatistics(coalition.side.BLUE) end) MENU_MISSION_COMMAND:New("Show Red Statistics", missionMenu, function() ShowSystemStatistics(coalition.side.RED) end) end env.info("[DGB PLUGIN] Dynamic Ground Battle Plugin initialized successfully!") env.info(string.format("[DGB PLUGIN] Zone garrison system: %d defenders per zone", DEFENDERS_PER_ZONE)) env.info(string.format("[DGB PLUGIN] Defender rotation: %s", ALLOW_DEFENDER_ROTATION and "ENABLED" or "DISABLED")) 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"))