mirror of
https://github.com/iTracerFacer/Moose_DualCoalitionZoneCapture.git
synced 2026-07-16 14:32:31 +00:00
1645 lines
72 KiB
Lua
1645 lines
72 KiB
Lua
--[[
|
|
- **Author**: F99th-TracerFacer
|
|
- **Discord:** https://discord.gg/NdZ2JuSU (The Fighting 99th Discord Server where I spend most of my time.)
|
|
|
|
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
|
|
--]]
|
|
---@diagnostic disable: undefined-global, lowercase-global
|
|
-- MOOSE framework globals are defined at runtime by DCS World
|
|
|
|
----------------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
|
-- 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 = false -- If true, fresh units can replace existing defenders when zone is over-garrisoned
|
|
local DEFENDER_PATROL_INTERVAL = 3600 -- How often defenders may get a patrol task (seconds, e.g. 3600 = 1 hour)
|
|
|
|
-- Infantry Patrol Settings
|
|
local MOVING_INFANTRY_PATROLS = false -- 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 = 15 -- 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 = 30 -- 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 = 15 -- 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 = 30 -- 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 = 900 -- 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
|
|
|
|
-- 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"
|
|
|
|
-- AI Tasking Behavior
|
|
-- Note: DCS engine can crash with "CREATING PATH MAKES TOO LONG" if units try to path too far
|
|
-- Keep these values conservative to reduce pathfinding load and avoid server crashes
|
|
-- OPTIMIZATION: Reduced MAX_ATTACK_DISTANCE from 25km to 20km to reduce pathfinding complexity
|
|
local MAX_ATTACK_DISTANCE = 20000 -- Maximum distance in meters for attacking enemy zones. Units won't attack zones farther than this. (20km ≈ 10.8nm)
|
|
local ATTACK_RETRY_COOLDOWN = 1800 -- Seconds a group will wait before re-attempting an attack if no valid enemy zone was found (30 minutes)
|
|
|
|
-- Define warehouses for each side
|
|
local redWarehouses = {
|
|
STATIC:FindByName("RedWarehouse-1-1"),
|
|
STATIC:FindByName("RedWarehouse-2-1"),
|
|
STATIC:FindByName("RedWarehouse-3-1"),
|
|
STATIC:FindByName("RedWarehouse-4-1"),
|
|
STATIC:FindByName("RedWarehouse-5-1"),
|
|
STATIC:FindByName("RedWarehouse-6-1"),
|
|
}
|
|
|
|
local blueWarehouses = {
|
|
STATIC:FindByName("BlueWarehouse-1-1"),
|
|
STATIC:FindByName("BlueWarehouse-2-1"),
|
|
STATIC:FindByName("BlueWarehouse-3-1"),
|
|
STATIC:FindByName("BlueWarehouse-4-1"),
|
|
STATIC:FindByName("BlueWarehouse-5-1"),
|
|
STATIC:FindByName("BlueWarehouse-6-1"),
|
|
}
|
|
|
|
-- Define unit templates (these groups must exist in mission editor as LATE ACTIVATE)
|
|
local redInfantryTemplates = {
|
|
"RedInfantry1",
|
|
"RedInfantry2",
|
|
|
|
}
|
|
|
|
local redArmorTemplates = {
|
|
"RedArmor1",
|
|
"RedArmor2",
|
|
|
|
}
|
|
|
|
local blueInfantryTemplates = {
|
|
"BlueInfantry1",
|
|
"BlueInfantry2",
|
|
|
|
}
|
|
|
|
local blueArmorTemplates = {
|
|
"BlueArmor1",
|
|
"BlueArmor2",
|
|
|
|
}
|
|
|
|
|
|
|
|
----------------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
|
-- DO NOT EDIT BELOW THIS LINE
|
|
----------------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
|
|
|
-- ==========================================
|
|
-- MULTILINGUAL SUPPORT (uses main script's language settings)
|
|
-- ==========================================
|
|
-- This plugin uses the playerLanguages table from Moose_DualCoalitionZoneCapture.lua
|
|
-- Players set their language preference in the main script's F10 menu
|
|
|
|
local DGB_LANGUAGES = {
|
|
EN = {
|
|
-- Warehouse messages
|
|
warehouseStatus = "[Warehouse Status]\nRed warehouses alive: %d Reinforcements: %d%%\nBlue warehouses alive: %d Reinforcements: %d%%\n",
|
|
reinforcementCapacity = "%s reinforcement capacity: %d%%",
|
|
warehouseFriendly = "Warehouse: %s\nThis warehouse needs to be protected.\n",
|
|
warehouseEnemy = "Warehouse: %s\nThis is a primary target as it is directly supplying enemy units.\n",
|
|
|
|
-- Menu items
|
|
menuGroundBattle = "Ground Battle",
|
|
menuWarehouseStatus = "Check Warehouse Status",
|
|
menuSystemStats = "Show System Statistics",
|
|
|
|
-- System statistics
|
|
statsTitle = "DYNAMIC GROUND BATTLE - SYSTEM STATUS",
|
|
statsConfiguration = "【CONFIGURATION】",
|
|
statsDefendersPerZone = " Defenders per Zone: %d",
|
|
statsDefenderRotation = " Defender Rotation: %s",
|
|
statsInfantryMovement = " Infantry Movement: %s",
|
|
statsTaskReassignment = " Task Reassignment: Every %ds",
|
|
statsWarehouseMarkers = " Warehouse Markers: %s",
|
|
statsSpawnLimits = "【SPAWN LIMITS】",
|
|
statsRedInfantry = " Red Infantry: %d/%d",
|
|
statsRedArmor = " Red Armor: %d/%d",
|
|
statsBlueInfantry = " Blue Infantry: %d/%d",
|
|
statsBlueArmor = " Blue Armor: %d/%d",
|
|
statsRedCoalition = "【RED COALITION】",
|
|
statsBlueCoalition = "【BLUE COALITION】",
|
|
statsWarehouses = " Warehouses: %d/%d (%d%%)",
|
|
statsActiveUnits = " Active Units: %d (%d inf, %d armor)",
|
|
statsDefenders = " Defenders: %d | Mobile: %d",
|
|
statsControlledZones = " Controlled Zones: %d",
|
|
statsGarrisoned = " - Garrisoned: %d",
|
|
statsUnderGarrisoned = " - Under-Garrisoned: %d",
|
|
statsInfantrySpawn = " Infantry Spawn: %ds",
|
|
statsInfantrySpawnPaused = " Infantry Spawn: PAUSED (no warehouses)",
|
|
statsArmorSpawn = " Armor Spawn: %ds",
|
|
statsArmorSpawnPaused = " Armor Spawn: PAUSED (no warehouses)",
|
|
statsSystemInfo = "【SYSTEM INFO】",
|
|
statsTotalZones = " Total Zones: %d",
|
|
statsActiveGarrisons = " Active Garrisons: %d",
|
|
statsTotalActiveUnits = " Total Active Units: %d",
|
|
statsTrackedGroups = " Tracked Groups: %d",
|
|
statsLuaMemory = " Lua Memory: %.1f MB",
|
|
statsWarningMemory = " ⚠️ WARNING: High memory usage!",
|
|
statsWarningGroups = " ⚠️ WARNING: High group count!",
|
|
enabled = "ENABLED",
|
|
disabled = "DISABLED",
|
|
},
|
|
|
|
DE = {
|
|
-- Warehouse messages
|
|
warehouseStatus = "[Lagerstatus]\nRote Lager aktiv: %d Verstärkungen: %d%%\nBlaue Lager aktiv: %d Verstärkungen: %d%%\n",
|
|
reinforcementCapacity = "%s Verstärkungskapazität: %d%%",
|
|
warehouseFriendly = "Lager: %s\nDieses Lager muss geschützt werden.\n",
|
|
warehouseEnemy = "Lager: %s\nDies ist ein Hauptziel, da es feindliche Einheiten direkt versorgt.\n",
|
|
|
|
-- Menu items
|
|
menuGroundBattle = "Bodenkampf",
|
|
menuWarehouseStatus = "Lagerstatus prüfen",
|
|
menuSystemStats = "Systemstatistiken anzeigen",
|
|
|
|
-- System statistics
|
|
statsTitle = "DYNAMISCHER BODENKAMPF - SYSTEMSTATUS",
|
|
statsConfiguration = "【KONFIGURATION】",
|
|
statsDefendersPerZone = " Verteidiger pro Zone: %d",
|
|
statsDefenderRotation = " Verteidiger-Rotation: %s",
|
|
statsInfantryMovement = " Infanterie-Bewegung: %s",
|
|
statsTaskReassignment = " Aufgabenzuweisung: Alle %ds",
|
|
statsWarehouseMarkers = " Lagermarkierungen: %s",
|
|
statsSpawnLimits = "【SPAWN-LIMITS】",
|
|
statsRedInfantry = " Rote Infanterie: %d/%d",
|
|
statsRedArmor = " Rote Panzer: %d/%d",
|
|
statsBlueInfantry = " Blaue Infanterie: %d/%d",
|
|
statsBlueArmor = " Blaue Panzer: %d/%d",
|
|
statsRedCoalition = "【ROTE KOALITION】",
|
|
statsBlueCoalition = "【BLAUE KOALITION】",
|
|
statsWarehouses = " Lager: %d/%d (%d%%)",
|
|
statsActiveUnits = " Aktive Einheiten: %d (%d Inf, %d Panzer)",
|
|
statsDefenders = " Verteidiger: %d | Mobil: %d",
|
|
statsControlledZones = " Kontrollierte Zonen: %d",
|
|
statsGarrisoned = " - Besetzt: %d",
|
|
statsUnderGarrisoned = " - Unterbesetzt: %d",
|
|
statsInfantrySpawn = " Infanterie-Spawn: %ds",
|
|
statsInfantrySpawnPaused = " Infanterie-Spawn: PAUSIERT (keine Lager)",
|
|
statsArmorSpawn = " Panzer-Spawn: %ds",
|
|
statsArmorSpawnPaused = " Panzer-Spawn: PAUSIERT (keine Lager)",
|
|
statsSystemInfo = "【SYSTEMINFO】",
|
|
statsTotalZones = " Zonen gesamt: %d",
|
|
statsActiveGarrisons = " Aktive Garnisonen: %d",
|
|
statsTotalActiveUnits = " Aktive Einheiten gesamt: %d",
|
|
statsTrackedGroups = " Verfolgte Gruppen: %d",
|
|
statsLuaMemory = " Lua-Speicher: %.1f MB",
|
|
statsWarningMemory = " ⚠️ WARNUNG: Hoher Speicherverbrauch!",
|
|
statsWarningGroups = " ⚠️ WARNUNG: Hohe Gruppenanzahl!",
|
|
enabled = "AKTIVIERT",
|
|
disabled = "DEAKTIVIERT",
|
|
},
|
|
|
|
FR = {
|
|
-- Warehouse messages
|
|
warehouseStatus = "[Statut des entrepôts]\nEntrepôts rouges actifs : %d Renforts : %d%%\nEntrepôts bleus actifs : %d Renforts : %d%%\n",
|
|
reinforcementCapacity = "Capacité de renfort %s : %d%%",
|
|
warehouseFriendly = "Entrepôt : %s\nCet entrepôt doit être protégé.\n",
|
|
warehouseEnemy = "Entrepôt : %s\nC'est une cible prioritaire car il approvisionne directement les unités ennemies.\n",
|
|
|
|
-- Menu items
|
|
menuGroundBattle = "Combat terrestre",
|
|
menuWarehouseStatus = "Vérifier le statut des entrepôts",
|
|
menuSystemStats = "Afficher les statistiques système",
|
|
|
|
-- System statistics
|
|
statsTitle = "COMBAT TERRESTRE DYNAMIQUE - ÉTAT DU SYSTÈME",
|
|
statsConfiguration = "【CONFIGURATION】",
|
|
statsDefendersPerZone = " Défenseurs par zone : %d",
|
|
statsDefenderRotation = " Rotation des défenseurs : %s",
|
|
statsInfantryMovement = " Mouvement infanterie : %s",
|
|
statsTaskReassignment = " Réaffectation des tâches : Tous les %ds",
|
|
statsWarehouseMarkers = " Marqueurs d'entrepôt : %s",
|
|
statsSpawnLimits = "【LIMITES DE SPAWN】",
|
|
statsRedInfantry = " Infanterie rouge : %d/%d",
|
|
statsRedArmor = " Blindés rouges : %d/%d",
|
|
statsBlueInfantry = " Infanterie bleue : %d/%d",
|
|
statsBlueArmor = " Blindés bleus : %d/%d",
|
|
statsRedCoalition = "【COALITION ROUGE】",
|
|
statsBlueCoalition = "【COALITION BLEUE】",
|
|
statsWarehouses = " Entrepôts : %d/%d (%d%%)",
|
|
statsActiveUnits = " Unités actives : %d (%d inf, %d blindés)",
|
|
statsDefenders = " Défenseurs : %d | Mobiles : %d",
|
|
statsControlledZones = " Zones contrôlées : %d",
|
|
statsGarrisoned = " - En garnison : %d",
|
|
statsUnderGarrisoned = " - Sous-garnison : %d",
|
|
statsInfantrySpawn = " Spawn infanterie : %ds",
|
|
statsInfantrySpawnPaused = " Spawn infanterie : PAUSE (pas d'entrepôts)",
|
|
statsArmorSpawn = " Spawn blindés : %ds",
|
|
statsArmorSpawnPaused = " Spawn blindés : PAUSE (pas d'entrepôts)",
|
|
statsSystemInfo = "【INFO SYSTÈME】",
|
|
statsTotalZones = " Zones totales : %d",
|
|
statsActiveGarrisons = " Garnisons actives : %d",
|
|
statsTotalActiveUnits = " Unités actives totales : %d",
|
|
statsTrackedGroups = " Groupes suivis : %d",
|
|
statsLuaMemory = " Mémoire Lua : %.1f Mo",
|
|
statsWarningMemory = " ⚠️ ATTENTION : Utilisation mémoire élevée !",
|
|
statsWarningGroups = " ⚠️ ATTENTION : Nombre de groupes élevé !",
|
|
enabled = "ACTIVÉ",
|
|
disabled = "DÉSACTIVÉ",
|
|
},
|
|
|
|
ES = {
|
|
-- Warehouse messages
|
|
warehouseStatus = "[Estado de almacenes]\nAlmacenes rojos activos: %d Refuerzos: %d%%\nAlmacenes azules activos: %d Refuerzos: %d%%\n",
|
|
reinforcementCapacity = "Capacidad de refuerzo %s: %d%%",
|
|
warehouseFriendly = "Almacén: %s\nEste almacén necesita ser protegido.\n",
|
|
warehouseEnemy = "Almacén: %s\nEste es un objetivo prioritario ya que está suministrando directamente unidades enemigas.\n",
|
|
|
|
-- Menu items
|
|
menuGroundBattle = "Batalla terrestre",
|
|
menuWarehouseStatus = "Verificar estado de almacenes",
|
|
menuSystemStats = "Mostrar estadísticas del sistema",
|
|
|
|
-- System statistics
|
|
statsTitle = "BATALLA TERRESTRE DINÁMICA - ESTADO DEL SISTEMA",
|
|
statsConfiguration = "【CONFIGURACIÓN】",
|
|
statsDefendersPerZone = " Defensores por zona: %d",
|
|
statsDefenderRotation = " Rotación de defensores: %s",
|
|
statsInfantryMovement = " Movimiento de infantería: %s",
|
|
statsTaskReassignment = " Reasignación de tareas: Cada %ds",
|
|
statsWarehouseMarkers = " Marcadores de almacén: %s",
|
|
statsSpawnLimits = "【LÍMITES DE APARICIÓN】",
|
|
statsRedInfantry = " Infantería roja: %d/%d",
|
|
statsRedArmor = " Blindados rojos: %d/%d",
|
|
statsBlueInfantry = " Infantería azul: %d/%d",
|
|
statsBlueArmor = " Blindados azules: %d/%d",
|
|
statsRedCoalition = "【COALICIÓN ROJA】",
|
|
statsBlueCoalition = "【COALICIÓN AZUL】",
|
|
statsWarehouses = " Almacenes: %d/%d (%d%%)",
|
|
statsActiveUnits = " Unidades activas: %d (%d inf, %d blindados)",
|
|
statsDefenders = " Defensores: %d | Móviles: %d",
|
|
statsControlledZones = " Zonas controladas: %d",
|
|
statsGarrisoned = " - Guarnecidas: %d",
|
|
statsUnderGarrisoned = " - Subguarnecidas: %d",
|
|
statsInfantrySpawn = " Aparición infantería: %ds",
|
|
statsInfantrySpawnPaused = " Aparición infantería: PAUSADA (sin almacenes)",
|
|
statsArmorSpawn = " Aparición blindados: %ds",
|
|
statsArmorSpawnPaused = " Aparición blindados: PAUSADA (sin almacenes)",
|
|
statsSystemInfo = "【INFO DEL SISTEMA】",
|
|
statsTotalZones = " Zonas totales: %d",
|
|
statsActiveGarrisons = " Guarniciones activas: %d",
|
|
statsTotalActiveUnits = " Unidades activas totales: %d",
|
|
statsTrackedGroups = " Grupos rastreados: %d",
|
|
statsLuaMemory = " Memoria Lua: %.1f MB",
|
|
statsWarningMemory = " ⚠️ ADVERTENCIA: ¡Uso de memoria elevado!",
|
|
statsWarningGroups = " ⚠️ ADVERTENCIA: ¡Cantidad de grupos elevada!",
|
|
enabled = "HABILITADO",
|
|
disabled = "DESHABILITADO",
|
|
},
|
|
|
|
RU = {
|
|
-- Warehouse messages
|
|
warehouseStatus = "[Статус складов]\nКрасные склады активны: %d Подкрепления: %d%%\nСиние склады активны: %d Подкрепления: %d%%\n",
|
|
reinforcementCapacity = "Мощность подкреплений %s: %d%%",
|
|
warehouseFriendly = "Склад: %s\nЭтот склад нужно защищать.\n",
|
|
warehouseEnemy = "Склад: %s\nЭто приоритетная цель, так как она напрямую снабжает вражеские подразделения.\n",
|
|
|
|
-- Menu items
|
|
menuGroundBattle = "Наземный бой",
|
|
menuWarehouseStatus = "Проверить статус складов",
|
|
menuSystemStats = "Показать статистику системы",
|
|
|
|
-- System statistics
|
|
statsTitle = "ДИНАМИЧЕСКИЙ НАЗЕМНЫЙ БОЙ - СТАТУС СИСТЕМЫ",
|
|
statsConfiguration = "【КОНФИГУРАЦИЯ】",
|
|
statsDefendersPerZone = " Защитников на зону: %d",
|
|
statsDefenderRotation = " Ротация защитников: %s",
|
|
statsInfantryMovement = " Движение пехоты: %s",
|
|
statsTaskReassignment = " Переназначение задач: Каждые %ds",
|
|
statsWarehouseMarkers = " Маркеры складов: %s",
|
|
statsSpawnLimits = "【ЛИМИТЫ ПОЯВЛЕНИЯ】",
|
|
statsRedInfantry = " Красная пехота: %d/%d",
|
|
statsRedArmor = " Красная бронетехника: %d/%d",
|
|
statsBlueInfantry = " Синяя пехота: %d/%d",
|
|
statsBlueArmor = " Синяя бронетехника: %d/%d",
|
|
statsRedCoalition = "【КРАСНАЯ КОАЛИЦИЯ】",
|
|
statsBlueCoalition = "【СИНЯЯ КОАЛИЦИЯ】",
|
|
statsWarehouses = " Склады: %d/%d (%d%%)",
|
|
statsActiveUnits = " Активные подразделения: %d (%d пех, %d брон)",
|
|
statsDefenders = " Защитники: %d | Мобильные: %d",
|
|
statsControlledZones = " Контролируемые зоны: %d",
|
|
statsGarrisoned = " - С гарнизоном: %d",
|
|
statsUnderGarrisoned = " - Недоукомплектованные: %d",
|
|
statsInfantrySpawn = " Появление пехоты: %ds",
|
|
statsInfantrySpawnPaused = " Появление пехоты: ПАУЗА (нет складов)",
|
|
statsArmorSpawn = " Появление техники: %ds",
|
|
statsArmorSpawnPaused = " Появление техники: ПАУЗА (нет складов)",
|
|
statsSystemInfo = "【ИНФОРМАЦИЯ О СИСТЕМЕ】",
|
|
statsTotalZones = " Всего зон: %d",
|
|
statsActiveGarrisons = " Активные гарнизоны: %d",
|
|
statsTotalActiveUnits = " Всего активных единиц: %d",
|
|
statsTrackedGroups = " Отслеживаемые группы: %d",
|
|
statsLuaMemory = " Память Lua: %.1f МБ",
|
|
statsWarningMemory = " ⚠️ ВНИМАНИЕ: Высокое использование памяти!",
|
|
statsWarningGroups = " ⚠️ ВНИМАНИЕ: Высокое количество групп!",
|
|
enabled = "ВКЛЮЧЕНО",
|
|
disabled = "ВЫКЛЮЧЕНО",
|
|
}
|
|
}
|
|
|
|
-- Helper function to get player's language from main script
|
|
-- Falls back to English if playerLanguages is not available or player not found
|
|
local function DGB_GetPlayerLanguage(playerName)
|
|
-- Try to use the main script's playerLanguages table
|
|
if playerLanguages and playerName and playerLanguages[playerName] then
|
|
return playerLanguages[playerName]
|
|
end
|
|
|
|
-- Try to use the main script's GetPlayerLanguage function if available
|
|
if GetPlayerLanguage and type(GetPlayerLanguage) == "function" and playerName then
|
|
local lang = GetPlayerLanguage(playerName)
|
|
if lang then return lang end
|
|
end
|
|
|
|
-- Fall back to main script's default language
|
|
if LANGUAGE_CONFIG and LANGUAGE_CONFIG.defaultLanguage then
|
|
return LANGUAGE_CONFIG.defaultLanguage
|
|
end
|
|
|
|
-- Ultimate fallback to English
|
|
return "EN"
|
|
end
|
|
|
|
-- Helper function to get coalition's language (uses first player's language)
|
|
local function DGB_GetCoalitionLanguage(coalitionSide)
|
|
local playerList = coalition.getPlayers(coalitionSide)
|
|
|
|
if playerList and #playerList > 0 then
|
|
local unit = playerList[1]
|
|
if unit then
|
|
local playerName = unit:getPlayerName()
|
|
if playerName then
|
|
return DGB_GetPlayerLanguage(playerName)
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Fall back to default language
|
|
if LANGUAGE_CONFIG and LANGUAGE_CONFIG.defaultLanguage then
|
|
return LANGUAGE_CONFIG.defaultLanguage
|
|
end
|
|
|
|
return "EN"
|
|
end
|
|
|
|
-- Helper function to get translated text
|
|
local function DGB_GetText(textKey, playerName)
|
|
local lang = DGB_GetPlayerLanguage(playerName)
|
|
local langTable = DGB_LANGUAGES[lang] or DGB_LANGUAGES.EN
|
|
return langTable[textKey] or DGB_LANGUAGES.EN[textKey] or textKey
|
|
end
|
|
|
|
-- Helper function to get translated text for a coalition
|
|
local function DGB_GetTextForCoalition(textKey, coalitionSide)
|
|
local lang = DGB_GetCoalitionLanguage(coalitionSide)
|
|
local langTable = DGB_LANGUAGES[lang] or DGB_LANGUAGES.EN
|
|
return langTable[textKey] or DGB_LANGUAGES.EN[textKey] or textKey
|
|
end
|
|
|
|
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 warehouse markers per warehouse and coalition
|
|
local warehouseMarkers = {}
|
|
|
|
-- Add event handlers for warehouse destruction
|
|
local function SetupWarehouseEventHandlers()
|
|
local allWarehouses = {}
|
|
for _, wh in ipairs(redWarehouses) do table.insert(allWarehouses, wh) end
|
|
for _, wh in ipairs(blueWarehouses) do table.insert(allWarehouses, wh) end
|
|
|
|
for _, warehouse in ipairs(allWarehouses) do
|
|
if warehouse then
|
|
warehouse:HandleEvent(EVENTS.Dead, function(event)
|
|
-- Remove markers for this warehouse from both coalitions
|
|
local name = warehouse:GetName()
|
|
for _, coalition in ipairs({1, 2}) do
|
|
local key = name .. "_coalition_" .. coalition
|
|
if warehouseMarkers[key] then
|
|
warehouseMarkers[key]:Remove()
|
|
warehouseMarkers[key] = nil
|
|
env.info(string.format("[DGB PLUGIN] Removed marker for destroyed warehouse %s (coalition %d)", name, coalition))
|
|
end
|
|
end
|
|
|
|
env.info(string.format("[DGB PLUGIN] Warehouse %s destroyed - markers removed", name))
|
|
end)
|
|
end
|
|
end
|
|
end
|
|
|
|
SetupWarehouseEventHandlers()
|
|
|
|
-- 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 = {}
|
|
|
|
-- Track all groups spawned by this plugin
|
|
-- Structure: spawnedGroups[groupName] = true
|
|
local spawnedGroups = {}
|
|
|
|
-- Track per-group attack cooldowns to avoid hammering the pathfinder for problematic routes
|
|
-- Structure: groupAttackCooldown[groupName] = nextAllowedTime (DCS timer.getTime())
|
|
local groupAttackCooldown = {}
|
|
|
|
-- Reusable SET_GROUP to prevent repeated creation within a single function call
|
|
local function getAllGroups()
|
|
-- Only return groups that were spawned by this plugin
|
|
local groupSet = SET_GROUP:New()
|
|
for groupName, _ in pairs(spawnedGroups) do
|
|
local group = GROUP:FindByName(groupName)
|
|
if group and group:IsAlive() then
|
|
groupSet:AddGroup(group)
|
|
else
|
|
-- Clean up dead groups from tracking table to prevent memory bloat
|
|
if group == nil or not group:IsAlive() then
|
|
spawnedGroups[groupName] = nil
|
|
groupGarrisonAssignments[groupName] = nil
|
|
groupAttackCooldown[groupName] = nil
|
|
end
|
|
end
|
|
end
|
|
return groupSet
|
|
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 and warehouse:GetLife() > 0 then
|
|
local warehousePos = warehouse:GetVec3()
|
|
local details
|
|
|
|
if coalition == 2 then -- Blue viewing
|
|
if warehouse:GetCoalition() == 2 then
|
|
details = string.format(DGB_GetTextForCoalition("warehouseFriendly", coalition), warehouse:GetName())
|
|
else
|
|
details = string.format(DGB_GetTextForCoalition("warehouseEnemy", coalition), warehouse:GetName())
|
|
end
|
|
elseif coalition == 1 then -- Red viewing
|
|
if warehouse:GetCoalition() == 1 then
|
|
details = string.format(DGB_GetTextForCoalition("warehouseFriendly", coalition), warehouse:GetName())
|
|
else
|
|
details = string.format(DGB_GetTextForCoalition("warehouseEnemy", coalition), warehouse:GetName())
|
|
end
|
|
end
|
|
|
|
local coordinate = COORDINATE:NewFromVec3(warehousePos)
|
|
local marker = MARKER:New(coordinate, details):ToCoalition(coalition):ReadOnly()
|
|
local key = warehouse:GetName() .. "_coalition_" .. coalition
|
|
warehouseMarkers[key] = marker
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Function to update warehouse markers
|
|
local function updateMarkPoints()
|
|
-- Clean up old markers first
|
|
for key, marker in pairs(warehouseMarkers) do
|
|
marker:Remove()
|
|
warehouseMarkers[key] = nil
|
|
end
|
|
|
|
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
|
|
|
|
local markerCount = 0
|
|
for _ in pairs(warehouseMarkers) do markerCount = markerCount + 1 end
|
|
env.info(string.format("[DGB PLUGIN] Updated warehouse markers (%d total)", markerCount))
|
|
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()
|
|
|
|
-- Record last patrol time for this defender so we can give them
|
|
-- an occasional "stretch their legs" patrol without hammering pathfinding.
|
|
garrison.lastPatrolTime = garrison.lastPatrolTime or {}
|
|
garrison.lastPatrolTime[groupName] = timer.getTime()
|
|
|
|
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 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)
|
|
-- This function is no longer needed as its logic has been integrated into AssignTasksToGroups
|
|
end
|
|
|
|
-- Function to assign tasks to all groups
|
|
local function AssignTasksToGroups()
|
|
env.info("[DGB PLUGIN] ============================================")
|
|
env.info("[DGB PLUGIN] Starting task assignment cycle...")
|
|
local allGroups = getAllGroups()
|
|
local tasksAssigned = 0
|
|
local defendersActive = 0
|
|
local mobileAssigned = 0
|
|
local groupsProcessed = 0
|
|
local groupsSkipped = 0
|
|
|
|
-- Create a quick lookup table for zone objects by name
|
|
local zoneLookup = {}
|
|
for _, zc in ipairs(zoneCaptureObjects) do
|
|
local zone = zc:GetZone()
|
|
if zone then
|
|
zoneLookup[zone:GetName()] = { zone = zone, capture = zc }
|
|
end
|
|
end
|
|
|
|
allGroups:ForEachGroup(function(group)
|
|
if not group or not group:IsAlive() then return end
|
|
|
|
groupsProcessed = groupsProcessed + 1
|
|
local groupName = group:GetName()
|
|
local groupCoalition = group:GetCoalition()
|
|
env.info(string.format("[DGB PLUGIN] Processing group %s (coalition %d)", groupName, groupCoalition))
|
|
|
|
-- 1. HANDLE DEFENDERS
|
|
if IsDefender(group) then
|
|
defendersActive = defendersActive + 1
|
|
|
|
-- Very slow, in-zone patrol for defenders, at most once per DEFENDER_PATROL_INTERVAL.
|
|
-- This keeps them mostly static while adding some life, without constantly re-pathing.
|
|
local zoneName = groupGarrisonAssignments[groupName]
|
|
local garrison = zoneName and GetZoneGarrison(zoneName) or nil
|
|
local lastPatrolTime = garrison and garrison.lastPatrolTime and garrison.lastPatrolTime[groupName] or 0
|
|
local now = timer.getTime()
|
|
|
|
if garrison and zoneName and now - lastPatrolTime >= DEFENDER_PATROL_INTERVAL then
|
|
local zoneInfo = zoneLookup[zoneName]
|
|
if zoneInfo and zoneInfo.zone then
|
|
env.info(string.format("[DGB PLUGIN] %s: Defender patrol in zone %s", groupName, zoneName))
|
|
-- Use simpler patrol method to reduce pathfinding memory
|
|
-- Reduced patrol radius from 0.5 to 0.3 to create simpler paths
|
|
local zoneCoord = zoneInfo.zone:GetCoordinate()
|
|
if zoneCoord then
|
|
local patrolPoint = zoneCoord:GetRandomCoordinateInRadius(zoneInfo.zone:GetRadius() * 0.3) -- Reduced from 0.5
|
|
local speed = IsInfantryGroup(group) and 15 or 25 -- km/h - slow patrol
|
|
group:RouteGroundTo(patrolPoint, speed, "Vee", 1)
|
|
end
|
|
garrison.lastPatrolTime[groupName] = now
|
|
tasksAssigned = tasksAssigned + 1
|
|
else
|
|
env.info(string.format("[DGB PLUGIN] %s: Defender holding (zone not found)", groupName))
|
|
end
|
|
else
|
|
env.info(string.format("[DGB PLUGIN] %s: Defender holding (patrol not due)", groupName))
|
|
end
|
|
|
|
return -- Defenders do not get any other tasks
|
|
end
|
|
|
|
-- 2. HANDLE MOBILE FORCES (NON-DEFENDERS)
|
|
|
|
-- Skip infantry if movement is disabled
|
|
if IsInfantryGroup(group) and not MOVING_INFANTRY_PATROLS then
|
|
env.info(string.format("[DGB PLUGIN] %s: Skipped (infantry movement disabled)", groupName))
|
|
groupsSkipped = groupsSkipped + 1
|
|
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)
|
|
env.info(string.format("[DGB PLUGIN] %s: Current speed %.2f m/s", groupName, speed))
|
|
if speed > 0.5 then
|
|
env.info(string.format("[DGB PLUGIN] %s: Skipped (already moving)", groupName))
|
|
groupsSkipped = groupsSkipped + 1
|
|
return
|
|
end
|
|
|
|
-- Find which zone the group is in
|
|
local currentZone = nil
|
|
local currentZoneCapture = nil
|
|
for _, zc in ipairs(zoneCaptureObjects) do
|
|
local zone = zc:GetZone()
|
|
if zone and group:IsCompletelyInZone(zone) then
|
|
currentZone = zone
|
|
currentZoneCapture = zc
|
|
break
|
|
end
|
|
end
|
|
|
|
-- 3. HANDLE GROUPS IN FRIENDLY ZONES
|
|
if currentZone and currentZoneCapture and currentZoneCapture:GetCoalition() == groupCoalition then
|
|
local zoneName = currentZone:GetName()
|
|
|
|
-- PRIORITY 1: If the zone is under attack, all non-defenders should help defend it
|
|
local zoneState = currentZoneCapture.GetCurrentState and currentZoneCapture:GetCurrentState() or nil
|
|
if zoneState == "Attacked" then
|
|
env.info(string.format("[DGB PLUGIN] %s defending contested zone %s", groupName, zoneName))
|
|
-- Use simpler routing to reduce pathfinding memory
|
|
local zoneCoord = currentZone:GetCoordinate()
|
|
if zoneCoord then
|
|
local defendPoint = zoneCoord:GetRandomCoordinateInRadius(currentZone:GetRadius() * 0.6)
|
|
local speed = IsInfantryGroup(group) and 30 or 50 -- km/h - faster response
|
|
group:RouteGroundTo(defendPoint, speed, "Vee", 2)
|
|
end
|
|
tasksAssigned = tasksAssigned + 1
|
|
mobileAssigned = mobileAssigned + 1
|
|
return
|
|
end
|
|
|
|
-- PRIORITY 2: Elect as defender if zone needs one (before attacking)
|
|
if ZoneNeedsDefenders(zoneName) then
|
|
if ElectDefender(group, currentZone, "zone under-garrisoned") then
|
|
tasksAssigned = tasksAssigned + 1
|
|
defendersActive = defendersActive + 1
|
|
return
|
|
end
|
|
end
|
|
|
|
-- PRIORITY 3: Defender rotation (if enabled and zone is over-garrisoned)
|
|
if TryDefenderRotation(group, currentZone) then
|
|
tasksAssigned = tasksAssigned + 1
|
|
defendersActive = defendersActive + 1
|
|
return -- Rotated in as a defender, task is set
|
|
end
|
|
end
|
|
|
|
-- 4. PATROL TO NEAREST ENEMY ZONE (for all mobile forces, regardless of current location)
|
|
-- Respect per-group attack cooldown to avoid hammering the pathfinder for problematic routes
|
|
local now = timer.getTime()
|
|
local nextAllowed = groupAttackCooldown[groupName]
|
|
if nextAllowed and now < nextAllowed then
|
|
env.info(string.format("[DGB PLUGIN] %s: Attack on cooldown for another %.0fs", groupName, nextAllowed - now))
|
|
groupsSkipped = groupsSkipped + 1
|
|
return
|
|
end
|
|
|
|
local closestEnemyZone = nil
|
|
local closestDistance = math.huge
|
|
local groupCoordinate = group:GetCoordinate()
|
|
|
|
for _, zc in ipairs(zoneCaptureObjects) do
|
|
local zoneCoalition = zc:GetCoalition()
|
|
if zoneCoalition ~= groupCoalition and zoneCoalition ~= coalition.side.NEUTRAL then
|
|
local zone = zc:GetZone()
|
|
if zone then
|
|
local distance = groupCoordinate:Get2DDistance(zone:GetCoordinate())
|
|
if distance < closestDistance and distance <= MAX_ATTACK_DISTANCE then
|
|
closestDistance = distance
|
|
closestEnemyZone = zone
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
if closestEnemyZone then
|
|
env.info(string.format("[DGB PLUGIN] %s: Attacking enemy zone %s (%.1fkm away)",
|
|
groupName, closestEnemyZone:GetName(), closestDistance / 1000))
|
|
|
|
-- Use simpler waypoint-based routing instead of TaskRouteToZone to reduce pathfinding memory load
|
|
-- This prevents the "CREATING PATH MAKES TOO LONG" memory buildup
|
|
-- Reduced radius from 0.7 to 0.5 to create simpler, shorter paths
|
|
local zoneCoord = closestEnemyZone:GetCoordinate()
|
|
if zoneCoord then
|
|
local randomPoint = zoneCoord:GetRandomCoordinateInRadius(closestEnemyZone:GetRadius() * 0.5) -- Reduced from 0.7
|
|
local speed = IsInfantryGroup(group) and 20 or 40 -- km/h
|
|
group:RouteGroundTo(randomPoint, speed, "Vee", 1)
|
|
end
|
|
|
|
tasksAssigned = tasksAssigned + 1
|
|
mobileAssigned = mobileAssigned + 1
|
|
return -- Task assigned, done with this group
|
|
end
|
|
|
|
-- 5. FALLBACK: No valid enemy zone within range - set cooldown to avoid repeated failed attempts
|
|
groupAttackCooldown[groupName] = now + ATTACK_RETRY_COOLDOWN
|
|
if closestDistance > MAX_ATTACK_DISTANCE and closestDistance < math.huge then
|
|
env.info(string.format("[DGB PLUGIN] %s: No enemy zones within range (closest is %.1fkm away, max is %.1fkm). Putting attacks on cooldown for %ds",
|
|
groupName, closestDistance / 1000, MAX_ATTACK_DISTANCE / 1000, ATTACK_RETRY_COOLDOWN))
|
|
else
|
|
env.info(string.format("[DGB PLUGIN] %s: No tasks available (no enemy zones found). Putting attacks on cooldown for %ds", groupName, ATTACK_RETRY_COOLDOWN))
|
|
end
|
|
end)
|
|
|
|
env.info(string.format("[DGB PLUGIN] Task assignment complete. Processed: %d, Skipped: %d, Tasked: %d (%d defenders, %d mobile)",
|
|
groupsProcessed, groupsSkipped, tasksAssigned, defendersActive, mobileAssigned))
|
|
env.info("[DGB PLUGIN] ============================================")
|
|
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
|
|
-- Send to Blue coalition in their language
|
|
local blueLang = DGB_GetCoalitionLanguage(coalition.side.BLUE)
|
|
local blueMsg = string.format(
|
|
DGB_LANGUAGES[blueLang].warehouseStatus,
|
|
redWarehousesAlive, redSpawnFrequencyPercentage,
|
|
blueWarehousesAlive, blueSpawnFrequencyPercentage
|
|
)
|
|
MESSAGE:New(blueMsg, 30):ToBlue()
|
|
|
|
-- Send to Red coalition in their language
|
|
local redLang = DGB_GetCoalitionLanguage(coalition.side.RED)
|
|
local redMsg = string.format(
|
|
DGB_LANGUAGES[redLang].warehouseStatus,
|
|
redWarehousesAlive, redSpawnFrequencyPercentage,
|
|
blueWarehousesAlive, blueSpawnFrequencyPercentage
|
|
)
|
|
MESSAGE:New(redMsg, 30):ToRed()
|
|
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)
|
|
|
|
-- Get language for this coalition
|
|
local lang = DGB_GetCoalitionLanguage(playerCoalition)
|
|
local T = DGB_LANGUAGES[lang]
|
|
|
|
-- Build comprehensive report
|
|
local msg = "═══════════════════════════════════════\n"
|
|
msg = msg .. T.statsTitle .. "\n"
|
|
msg = msg .. "═══════════════════════════════════════\n\n"
|
|
|
|
-- Configuration Section
|
|
msg = msg .. T.statsConfiguration .. "\n"
|
|
msg = msg .. string.format(T.statsDefendersPerZone, DEFENDERS_PER_ZONE) .. "\n"
|
|
msg = msg .. string.format(T.statsDefenderRotation, ALLOW_DEFENDER_ROTATION and T.enabled or T.disabled) .. "\n"
|
|
msg = msg .. string.format(T.statsInfantryMovement, MOVING_INFANTRY_PATROLS and T.enabled or T.disabled) .. "\n"
|
|
msg = msg .. string.format(T.statsTaskReassignment, ASSIGN_TASKS_SCHED) .. "\n"
|
|
msg = msg .. string.format(T.statsWarehouseMarkers, ENABLE_WAREHOUSE_MARKERS and T.enabled or T.disabled) .. "\n\n"
|
|
|
|
-- Spawn Limits Section
|
|
msg = msg .. T.statsSpawnLimits .. "\n"
|
|
msg = msg .. string.format(T.statsRedInfantry, INIT_RED_INFANTRY, MAX_RED_INFANTRY) .. "\n"
|
|
msg = msg .. string.format(T.statsRedArmor, INIT_RED_ARMOR, MAX_RED_ARMOR) .. "\n"
|
|
msg = msg .. string.format(T.statsBlueInfantry, INIT_BLUE_INFANTRY, MAX_BLUE_INFANTRY) .. "\n"
|
|
msg = msg .. string.format(T.statsBlueArmor, INIT_BLUE_ARMOR, MAX_BLUE_ARMOR) .. "\n\n"
|
|
|
|
-- Red Coalition Section
|
|
msg = msg .. T.statsRedCoalition .. "\n"
|
|
msg = msg .. string.format(T.statsWarehouses, redWarehousesAlive, redWarehouseTotal, redSpawnFreqPct) .. "\n"
|
|
msg = msg .. string.format(T.statsActiveUnits, redUnits.total, redUnits.infantry, redUnits.armor) .. "\n"
|
|
msg = msg .. string.format(T.statsDefenders, redUnits.defenders, redUnits.mobile) .. "\n"
|
|
msg = msg .. string.format(T.statsControlledZones, redGarrison.totalZones) .. "\n"
|
|
msg = msg .. string.format(T.statsGarrisoned, redGarrison.garrisoned) .. "\n"
|
|
msg = msg .. string.format(T.statsUnderGarrisoned, redGarrison.underGarrisoned) .. "\n"
|
|
|
|
if redInfantryInterval then
|
|
msg = msg .. string.format(T.statsInfantrySpawn, math.floor(redInfantryInterval)) .. "\n"
|
|
else
|
|
msg = msg .. T.statsInfantrySpawnPaused .. "\n"
|
|
end
|
|
|
|
if redArmorInterval then
|
|
msg = msg .. string.format(T.statsArmorSpawn, math.floor(redArmorInterval)) .. "\n\n"
|
|
else
|
|
msg = msg .. T.statsArmorSpawnPaused .. "\n\n"
|
|
end
|
|
|
|
-- Blue Coalition Section
|
|
msg = msg .. T.statsBlueCoalition .. "\n"
|
|
msg = msg .. string.format(T.statsWarehouses, blueWarehousesAlive, blueWarehouseTotal, blueSpawnFreqPct) .. "\n"
|
|
msg = msg .. string.format(T.statsActiveUnits, blueUnits.total, blueUnits.infantry, blueUnits.armor) .. "\n"
|
|
msg = msg .. string.format(T.statsDefenders, blueUnits.defenders, blueUnits.mobile) .. "\n"
|
|
msg = msg .. string.format(T.statsControlledZones, blueGarrison.totalZones) .. "\n"
|
|
msg = msg .. string.format(T.statsGarrisoned, blueGarrison.garrisoned) .. "\n"
|
|
msg = msg .. string.format(T.statsUnderGarrisoned, blueGarrison.underGarrisoned) .. "\n"
|
|
|
|
if blueInfantryInterval then
|
|
msg = msg .. string.format(T.statsInfantrySpawn, math.floor(blueInfantryInterval)) .. "\n"
|
|
else
|
|
msg = msg .. T.statsInfantrySpawnPaused .. "\n"
|
|
end
|
|
|
|
if blueArmorInterval then
|
|
msg = msg .. string.format(T.statsArmorSpawn, math.floor(blueArmorInterval)) .. "\n\n"
|
|
else
|
|
msg = msg .. T.statsArmorSpawnPaused .. "\n\n"
|
|
end
|
|
|
|
-- System Info
|
|
msg = msg .. T.statsSystemInfo .. "\n"
|
|
msg = msg .. string.format(T.statsTotalZones, #zoneCaptureObjects) .. "\n"
|
|
msg = msg .. string.format(T.statsActiveGarrisons, redGarrison.garrisoned + blueGarrison.garrisoned) .. "\n"
|
|
msg = msg .. string.format(T.statsTotalActiveUnits, redUnits.total + blueUnits.total) .. "\n"
|
|
|
|
-- Memory and Performance Tracking
|
|
local totalSpawnedGroups = 0
|
|
for _ in pairs(spawnedGroups) do
|
|
totalSpawnedGroups = totalSpawnedGroups + 1
|
|
end
|
|
|
|
local luaMemoryKB = collectgarbage("count")
|
|
msg = msg .. string.format(T.statsTrackedGroups, totalSpawnedGroups) .. "\n"
|
|
msg = msg .. string.format(T.statsLuaMemory, luaMemoryKB / 1024) .. "\n"
|
|
|
|
-- Warning if memory is high
|
|
if luaMemoryKB > 512000 then -- More than 500MB
|
|
msg = msg .. T.statsWarningMemory .. "\n"
|
|
end
|
|
|
|
-- Warning if too many groups
|
|
if totalSpawnedGroups > 200 then
|
|
msg = msg .. T.statsWarningGroups .. "\n"
|
|
end
|
|
|
|
msg = msg .. "\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)
|
|
|
|
local redLang = DGB_GetCoalitionLanguage(coalition.side.RED)
|
|
local redMsg = string.format(DGB_LANGUAGES[redLang].reinforcementCapacity, "Red", redSpawnFrequencyPercentage)
|
|
MESSAGE:New(redMsg, 30):ToRed()
|
|
|
|
local blueLang = DGB_GetCoalitionLanguage(coalition.side.BLUE)
|
|
local blueMsg = string.format(DGB_LANGUAGES[blueLang].reinforcementCapacity, "Blue", blueSpawnFrequencyPercentage)
|
|
MESSAGE:New(blueMsg, 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. This is a self-rescheduling function.
|
|
local function ScheduleSpawner(spawnObject, getZonesFn, warehouses, baseFrequency, label, cadenceScalar)
|
|
local function spawnAndReschedule()
|
|
-- Calculate the next spawn interval first
|
|
local spawnInterval = CalculateSpawnFrequency(warehouses, baseFrequency, cadenceScalar)
|
|
|
|
if not spawnInterval then
|
|
-- No warehouses. Pause spawning and check again after the delay.
|
|
env.info(string.format("[DGB PLUGIN] %s spawn paused (no warehouses). Rechecking in %ds.", label, NO_WAREHOUSE_RECHECK_DELAY))
|
|
SCHEDULER:New(nil, spawnAndReschedule, {}, NO_WAREHOUSE_RECHECK_DELAY)
|
|
return
|
|
end
|
|
|
|
-- Get friendly zones
|
|
local friendlyZones = getZonesFn()
|
|
if #friendlyZones > 0 then
|
|
local chosenZone = friendlyZones[math.random(#friendlyZones)]
|
|
local spawnedGroup = spawnObject:SpawnInZone(chosenZone, true)
|
|
|
|
if spawnedGroup then
|
|
local groupName = spawnedGroup:GetName()
|
|
spawnedGroups[groupName] = true
|
|
env.info(string.format("[DGB PLUGIN] Spawned %s in zone %s. Task assignment will occur on next cycle.",
|
|
groupName, chosenZone:GetName()))
|
|
end
|
|
else
|
|
env.info(string.format("[DGB PLUGIN] %s spawn skipped (no friendly zones).", label))
|
|
end
|
|
|
|
-- Schedule the next run
|
|
SCHEDULER:New(nil, spawnAndReschedule, {}, spawnInterval)
|
|
env.info(string.format("[DGB PLUGIN] Next %s spawn scheduled in %d seconds.", label, math.floor(spawnInterval)))
|
|
end
|
|
|
|
-- Kick off the first spawn with a random delay to stagger the different spawners
|
|
local initialDelay = math.random(5, 15)
|
|
SCHEDULER:New(nil, spawnAndReschedule, {}, initialDelay)
|
|
env.info(string.format("[DGB PLUGIN] %s spawner initialized. First check in %d seconds.", label, initialDelay))
|
|
end
|
|
|
|
-- Schedule spawns (each spawner now runs at its own configured cadence)
|
|
if redInfantryValid and redWarehousesValid then
|
|
ScheduleSpawner(redInfantrySpawn, GetRedZones, redWarehouses, SPAWN_SCHED_RED_INFANTRY, "Red Infantry", RED_INFANTRY_CADENCE_SCALAR)
|
|
end
|
|
if redArmorValid and redWarehousesValid then
|
|
ScheduleSpawner(redArmorSpawn, GetRedZones, redWarehouses, SPAWN_SCHED_RED_ARMOR, "Red Armor", RED_ARMOR_CADENCE_SCALAR)
|
|
end
|
|
if blueInfantryValid and blueWarehousesValid then
|
|
ScheduleSpawner(blueInfantrySpawn, GetBlueZones, blueWarehouses, SPAWN_SCHED_BLUE_INFANTRY, "Blue Infantry", BLUE_INFANTRY_CADENCE_SCALAR)
|
|
end
|
|
if blueArmorValid and blueWarehousesValid then
|
|
ScheduleSpawner(blueArmorSpawn, GetBlueZones, blueWarehouses, SPAWN_SCHED_BLUE_ARMOR, "Blue Armor", BLUE_ARMOR_CADENCE_SCALAR)
|
|
end
|
|
|
|
-- 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
|
|
|
|
-- Comprehensive cleanup function to prevent memory accumulation
|
|
local function CleanupStaleData()
|
|
local cleanedGroups = 0
|
|
local cleanedCooldowns = 0
|
|
local cleanedGarrisons = 0
|
|
|
|
-- Clean up spawnedGroups, groupGarrisonAssignments, and groupAttackCooldown
|
|
for groupName, _ in pairs(spawnedGroups) do
|
|
local group = GROUP:FindByName(groupName)
|
|
if not group or not group:IsAlive() then
|
|
spawnedGroups[groupName] = nil
|
|
cleanedGroups = cleanedGroups + 1
|
|
|
|
if groupGarrisonAssignments[groupName] then
|
|
groupGarrisonAssignments[groupName] = nil
|
|
end
|
|
|
|
if groupAttackCooldown[groupName] then
|
|
groupAttackCooldown[groupName] = nil
|
|
cleanedCooldowns = cleanedCooldowns + 1
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Clean up garrison data for zones that changed ownership or have stale defenders
|
|
for zoneName, garrison in pairs(zoneGarrisons) do
|
|
local zoneStillExists = false
|
|
local currentZoneCoalition = nil
|
|
|
|
-- Check if zone still exists and get its current owner
|
|
for _, zc in ipairs(zoneCaptureObjects) do
|
|
local zone = zc:GetZone()
|
|
if zone and zone:GetName() == zoneName then
|
|
zoneStillExists = true
|
|
currentZoneCoalition = zc:GetCoalition()
|
|
break
|
|
end
|
|
end
|
|
|
|
if not zoneStillExists then
|
|
-- Zone doesn't exist anymore, clean up all garrison data
|
|
for _, defenderName in ipairs(garrison.defenders) do
|
|
groupGarrisonAssignments[defenderName] = nil
|
|
end
|
|
zoneGarrisons[zoneName] = nil
|
|
cleanedGarrisons = cleanedGarrisons + 1
|
|
else
|
|
-- Zone exists, clean up dead defenders from the garrison list
|
|
local deadDefenders = {}
|
|
for i, defenderName in ipairs(garrison.defenders) do
|
|
local group = GROUP:FindByName(defenderName)
|
|
if not group or not group:IsAlive() then
|
|
table.insert(deadDefenders, i)
|
|
groupGarrisonAssignments[defenderName] = nil
|
|
end
|
|
end
|
|
|
|
-- Remove dead defenders in reverse order to maintain indices
|
|
for i = #deadDefenders, 1, -1 do
|
|
table.remove(garrison.defenders, deadDefenders[i])
|
|
end
|
|
|
|
-- Clean up lastPatrolTime for dead defenders
|
|
if garrison.lastPatrolTime then
|
|
for defenderName, _ in pairs(garrison.lastPatrolTime) do
|
|
local group = GROUP:FindByName(defenderName)
|
|
if not group or not group:IsAlive() then
|
|
garrison.lastPatrolTime[defenderName] = nil
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Force aggressive Lua garbage collection to reclaim memory
|
|
-- Step-based collection helps ensure thorough cleanup
|
|
collectgarbage("collect") -- Full collection
|
|
collectgarbage("collect") -- Second pass to catch finalized objects
|
|
|
|
if cleanedGroups > 0 or cleanedCooldowns > 0 or cleanedGarrisons > 0 then
|
|
env.info(string.format("[DGB PLUGIN] Cleanup: Removed %d groups, %d cooldowns, %d garrisons",
|
|
cleanedGroups, cleanedCooldowns, cleanedGarrisons))
|
|
end
|
|
end
|
|
|
|
-- Optional periodic memory usage logging (Lua-only; shows in dcs.log)
|
|
local ENABLE_MEMORY_LOGGING = true
|
|
local MEMORY_LOG_INTERVAL = 600 -- seconds (10 minutes) - reduced from 15 minutes
|
|
local CLEANUP_INTERVAL = 300 -- seconds (5 minutes) - reduced from 10 minutes for more aggressive cleanup
|
|
|
|
local function LogMemoryUsage()
|
|
-- Force garbage collection before measuring to get accurate readings
|
|
collectgarbage("collect")
|
|
|
|
local luaMemoryKB = collectgarbage("count")
|
|
local luaMemoryMB = luaMemoryKB / 1024
|
|
|
|
local totalSpawnedGroups = 0
|
|
for _ in pairs(spawnedGroups) do
|
|
totalSpawnedGroups = totalSpawnedGroups + 1
|
|
end
|
|
|
|
local totalCooldowns = 0
|
|
for _ in pairs(groupAttackCooldown) do
|
|
totalCooldowns = totalCooldowns + 1
|
|
end
|
|
|
|
local totalGarrisons = 0
|
|
local totalDefenders = 0
|
|
for _, garrison in pairs(zoneGarrisons) do
|
|
totalGarrisons = totalGarrisons + 1
|
|
totalDefenders = totalDefenders + #garrison.defenders
|
|
end
|
|
|
|
local msg = string.format("[DGB PLUGIN] Memory: Lua=%.1f MB, Groups=%d, Cooldowns=%d, Garrisons=%d, Defenders=%d",
|
|
luaMemoryMB, totalSpawnedGroups, totalCooldowns, totalGarrisons, totalDefenders)
|
|
env.info(msg)
|
|
end
|
|
|
|
if ENABLE_MEMORY_LOGGING then
|
|
SCHEDULER:New(nil, LogMemoryUsage, {}, 60, MEMORY_LOG_INTERVAL)
|
|
end
|
|
|
|
-- Schedule periodic cleanup
|
|
SCHEDULER:New(nil, CleanupStaleData, {}, 120, CLEANUP_INTERVAL)
|
|
|
|
-- Schedule task assignments (runs quickly at start, then every ASSIGN_TASKS_SCHED seconds)
|
|
SCHEDULER:New(nil, AssignTasksToGroups, {}, 15, ASSIGN_TASKS_SCHED)
|
|
|
|
-- Add F10 menu for manual checks (using MenuManager if available)
|
|
if MenuManager then
|
|
-- Create coalition-specific menus under Mission Options
|
|
local blueMenuText = DGB_GetTextForCoalition("menuGroundBattle", coalition.side.BLUE)
|
|
local blueMenu = MenuManager.CreateCoalitionMenu(coalition.side.BLUE, blueMenuText)
|
|
MENU_COALITION_COMMAND:New(coalition.side.BLUE, DGB_GetTextForCoalition("menuWarehouseStatus", coalition.side.BLUE), blueMenu, MonitorWarehouses)
|
|
MENU_COALITION_COMMAND:New(coalition.side.BLUE, DGB_GetTextForCoalition("menuSystemStats", coalition.side.BLUE), blueMenu, function()
|
|
ShowSystemStatistics(coalition.side.BLUE)
|
|
end)
|
|
|
|
local redMenuText = DGB_GetTextForCoalition("menuGroundBattle", coalition.side.RED)
|
|
local redMenu = MenuManager.CreateCoalitionMenu(coalition.side.RED, redMenuText)
|
|
MENU_COALITION_COMMAND:New(coalition.side.RED, DGB_GetTextForCoalition("menuWarehouseStatus", coalition.side.RED), redMenu, MonitorWarehouses)
|
|
MENU_COALITION_COMMAND:New(coalition.side.RED, DGB_GetTextForCoalition("menuSystemStats", coalition.side.RED), redMenu, function()
|
|
ShowSystemStatistics(coalition.side.RED)
|
|
end)
|
|
else
|
|
-- Fallback to root-level mission menu
|
|
local missionMenuText = DGB_GetTextForCoalition("menuGroundBattle", coalition.side.BLUE)
|
|
local missionMenu = MENU_MISSION:New(missionMenuText)
|
|
MENU_MISSION_COMMAND:New(DGB_GetTextForCoalition("menuWarehouseStatus", coalition.side.BLUE), missionMenu, MonitorWarehouses)
|
|
MENU_MISSION_COMMAND:New(DGB_GetTextForCoalition("menuSystemStats", coalition.side.BLUE) .. " (Blue)", missionMenu, function()
|
|
ShowSystemStatistics(coalition.side.BLUE)
|
|
end)
|
|
MENU_MISSION_COMMAND:New(DGB_GetTextForCoalition("menuSystemStats", coalition.side.RED) .. " (Red)", 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"))
|