diff --git a/DCS_Afgainistan/Insurgent_Sandstorm/F99th-Insurgent_Sandstorm_2.0.0.miz b/DCS_Afgainistan/Insurgent_Sandstorm/F99th-Insurgent_Sandstorm_2.0.0.miz index c162674..91f75c5 100644 Binary files a/DCS_Afgainistan/Insurgent_Sandstorm/F99th-Insurgent_Sandstorm_2.0.0.miz and b/DCS_Afgainistan/Insurgent_Sandstorm/F99th-Insurgent_Sandstorm_2.0.0.miz differ diff --git a/DCS_Afgainistan/Insurgent_Sandstorm/Moose_CTLD_Init_DualCoalitions.lua b/DCS_Afgainistan/Insurgent_Sandstorm/Moose_CTLD_Init_DualCoalitions.lua index a5058b0..6a36481 100644 --- a/DCS_Afgainistan/Insurgent_Sandstorm/Moose_CTLD_Init_DualCoalitions.lua +++ b/DCS_Afgainistan/Insurgent_Sandstorm/Moose_CTLD_Init_DualCoalitions.lua @@ -34,7 +34,10 @@ local blueCfg = { Zones = { PickupZones = { { name = 'S1', flag = 9001, activeWhen = 0 }, { name = "S2", flag = 9004, activeWhen = 0 }, - { name = "S3", flag = 9005, activeWhen = 0 } }, + { name = "S3", flag = 9005, activeWhen = 0 }, + { name = "S4", flag = 9006, activeWhen = 0 }, + { name = "S5", flag = 9007, activeWhen = 0 }, + { name = "S6", flag = 9008, activeWhen = 0 } }, --DropZones = { { name = 'BRAVO', flag = 9002, activeWhen = 0 } }, --FOBZones = { { name = 'CHARLIE', flag = 9003, activeWhen = 0 } }, --MASHZones = { { name = 'MASH Alpha', freq = '251.0 AM', radius = 500, flag = 9010, activeWhen = 0 } }, @@ -67,7 +70,8 @@ local redCfg = { { name = "RedLoadZone2", flag = 9104, activeWhen = 0 }, { name = "RedLoadZone3", flag = 9105, activeWhen = 0 }, { name = "RedLoadZone4", flag = 9106, activeWhen = 0 }, - { name = "RedLoadZone5", flag = 9107, activeWhen = 0 } }, + { name = "RedLoadZone5", flag = 9107, activeWhen = 0 }, + { name = "RedLoadZone6", flag = 9108, activeWhen = 0 } }, --DropZones = { { name = 'ECHO', flag = 9102, activeWhen = 0 } }, --FOBZones = { { name = 'FOXTROT', flag = 9103, activeWhen = 0 } }, --MASHZones = { { name = 'MASH Bravo', freq = '252.0 AM', radius = 500, flag = 9111, activeWhen = 0 } }, diff --git a/Moose_CTLD_Pure/catalogs/Moose_CTLD_Catalog_LowCounts.lua b/Moose_CTLD_Pure/catalogs/Moose_CTLD_Catalog_LowCounts.lua new file mode 100644 index 0000000..940e181 --- /dev/null +++ b/Moose_CTLD_Pure/catalogs/Moose_CTLD_Catalog_LowCounts.lua @@ -0,0 +1,334 @@ +-- CrateCatalog_CTLD_Extract.lua +-- Auto-generated from CTLD.lua (Operation_Polar_Shield) spawnableCrates config +-- Returns a table of crate definitions suitable for CTLD:MergeCatalog() +-- Notes: +-- - Each entry has keys: description/menu, dcsCargoType, required or requires (composite), side, category, build(point, headingDeg) +-- - Single-unit entries spawn one unit by DCS type. Composite "SITE" entries spawn a multi-unit group approximating system components. + +local function singleUnit(unitType) + return function(point, headingDeg) + local name = string.format('%s-%d', unitType, math.random(100000,999999)) + local hdg = math.rad(headingDeg or 0) + return { + visible=false, lateActivation=false, tasks={}, task='Ground Nothing', route={}, + units={ { type=unitType, name=name, x=point.x, y=point.z, heading=hdg } }, + name = 'CTLD_'..name + } + end +end + +-- Build a single AIR unit that spawns in the air at a configured altitude/speed. +-- Falls back gracefully to singleUnit behavior if config is unavailable/disabled. +local function singleAirUnit(unitType) + return function(point, headingDeg) + local cfg = (rawget(_G, 'CTLD') and CTLD.Config and CTLD.Config.DroneAirSpawn) or nil + if not cfg or cfg.Enabled == false then + return singleUnit(unitType)(point, headingDeg) + end + + local name = string.format('%s-%d', unitType, math.random(100000,999999)) + local hdgDeg = headingDeg or 0 + local hdg = math.rad(hdgDeg) + local alt = tonumber(cfg.AltitudeMeters) or 1200 + local spd = tonumber(cfg.SpeedMps) or 120 + + -- Create a tiny 2-point route to ensure forward flight at the chosen altitude. + local function fwdOffset(px, pz, meters, headingRadians) + return px + math.sin(headingRadians) * meters, pz + math.cos(headingRadians) * meters + end + local p1x, p1z = point.x, point.z + local p2x, p2z = fwdOffset(point.x, point.z, 1000, hdg) -- 1 km ahead + + local group = { + visible=false, + lateActivation=false, + tasks={}, + task='CAS', + route={ + points={ + { + alt = alt, alt_type = 'BARO', + type = 'Turning Point', action = 'Turning Point', + x = p1x, y = p1z, + speed = spd, ETA = 0, ETA_locked = false, + task = {} + }, + { + alt = alt, alt_type = 'BARO', + type = 'Turning Point', action = 'Turning Point', + x = p2x, y = p2z, + speed = spd, ETA = 0, ETA_locked = false, + task = {} + } + } + }, + units={ + { + type=unitType, name=name, + x=p1x, y=p1z, + heading=hdg, + speed = spd, + alt = alt, alt_type = 'BARO' + } + }, + name = 'CTLD_'..name + } + return group + end +end + +local function multiUnits(units) + -- units: array of { type, dx, dz } + return function(point, headingDeg) + local hdg = math.rad(headingDeg or 0) + local function off(dx, dz) return { x = point.x + dx, z = point.z + dz } end + local list = {} + for i,u in ipairs(units) do + local p = off(u.dx or 0, u.dz or 3*i) + table.insert(list, { + type = u.type, name = string.format('CTLD-%s-%d', u.type, math.random(100000,999999)), + x = p.x, y = p.z, heading = hdg + }) + end + return { visible=false, lateActivation=false, tasks={}, task='Ground Nothing', route={}, units=list, name=string.format('CTLD_SITE_%d', math.random(100000,999999)) } + end +end + +local BLUE = coalition.side.BLUE +local RED = coalition.side.RED + +local cat = {} + +cat['BLUE_M1128_STRYKER_MGS_CRATE'] = { hidden=true, description='M1128 Stryker MGS crate', dcsCargoType='container_cargo', required=1, initialStock=30, side=BLUE, category=Group.Category.GROUND } +cat['BLUE_M1128_STRYKER_MGS'] = { menuCategory='Combat Vehicles', menu='M1128 Stryker MGS', description='M1128 Stryker MGS', dcsCargoType='container_cargo', requires={ BLUE_M1128_STRYKER_MGS_CRATE=3 }, initialStock=0, side=BLUE, category=Group.Category.GROUND, build=singleUnit('M1128 Stryker MGS'), unitType='M1128 Stryker MGS', MEDEVAC=true, salvageValue=3, crewSize=3 } +cat['BLUE_M60A3_PATTON_CRATE'] = { hidden=true, description='M-60A3 Patton crate', dcsCargoType='container_cargo', required=1, initialStock=30, side=BLUE, category=Group.Category.GROUND } +cat['BLUE_M60A3_PATTON'] = { menuCategory='Combat Vehicles', menu='M-60A3 Patton', description='M-60A3 Patton', dcsCargoType='container_cargo', requires={ BLUE_M60A3_PATTON_CRATE=3 }, initialStock=0, side=BLUE, category=Group.Category.GROUND, build=singleUnit('M-60'), unitType='M-60', MEDEVAC=true, salvageValue=3, crewSize=4 } +cat['BLUE_HMMWV_TOW_CRATE'] = { hidden=true, description='Humvee - TOW crate', dcsCargoType='container_cargo', required=1, initialStock=36, side=BLUE, category=Group.Category.GROUND } +cat['BLUE_HMMWV_TOW'] = { menuCategory='Combat Vehicles', menu='Humvee - TOW', description='Humvee - TOW', dcsCargoType='container_cargo', requires={ BLUE_HMMWV_TOW_CRATE=3 }, initialStock=0, side=BLUE, category=Group.Category.GROUND, build=singleUnit('M1045 HMMWV TOW'), unitType='M1045 HMMWV TOW', MEDEVAC=true, salvageValue=3, crewSize=2 } +cat['BLUE_M1134_STRYKER_ATGM_CRATE']= { hidden=true, description='M1134 Stryker ATGM crate', dcsCargoType='container_cargo', required=1, initialStock=24, side=BLUE, category=Group.Category.GROUND } +cat['BLUE_M1134_STRYKER_ATGM'] = { menuCategory='Combat Vehicles', menu='M1134 Stryker ATGM', description='M1134 Stryker ATGM', dcsCargoType='container_cargo', requires={ BLUE_M1134_STRYKER_ATGM_CRATE=3 }, initialStock=0, side=BLUE, category=Group.Category.GROUND, build=singleUnit('M1134 Stryker ATGM'), unitType='M1134 Stryker ATGM', MEDEVAC=true, salvageValue=3, crewSize=3 } +cat['BLUE_LAV25_CRATE'] = { hidden=true, description='LAV-25 crate', dcsCargoType='container_cargo', required=1, initialStock=30, side=BLUE, category=Group.Category.GROUND } +cat['BLUE_LAV25'] = { menuCategory='Combat Vehicles', menu='LAV-25', description='LAV-25', dcsCargoType='container_cargo', requires={ BLUE_LAV25_CRATE=3 }, initialStock=0, side=BLUE, category=Group.Category.GROUND, build=singleUnit('LAV-25'), unitType='LAV-25', MEDEVAC=true, salvageValue=3, crewSize=3 } +cat['BLUE_M2A2_BRADLEY_CRATE'] = { hidden=true, description='M2A2 Bradley crate', dcsCargoType='container_cargo', required=1, initialStock=30, side=BLUE, category=Group.Category.GROUND } +cat['BLUE_M2A2_BRADLEY'] = { menuCategory='Combat Vehicles', menu='M2A2 Bradley', description='M2A2 Bradley', dcsCargoType='container_cargo', requires={ BLUE_M2A2_BRADLEY_CRATE=3 }, initialStock=0, side=BLUE, category=Group.Category.GROUND, build=singleUnit('M-2 Bradley'), unitType='M-2 Bradley', MEDEVAC=true, salvageValue=3, crewSize=3 } +cat['BLUE_VAB_MEPHISTO_CRATE'] = { hidden=true, description='ATGM VAB Mephisto crate', dcsCargoType='container_cargo', required=1, initialStock=24, side=BLUE, category=Group.Category.GROUND } +cat['BLUE_VAB_MEPHISTO'] = { menuCategory='Combat Vehicles', menu='ATGM VAB Mephisto', description='ATGM VAB Mephisto', dcsCargoType='container_cargo', requires={ BLUE_VAB_MEPHISTO_CRATE=3 }, initialStock=0, side=BLUE, category=Group.Category.GROUND, build=singleUnit('VAB_Mephisto'), unitType='VAB_Mephisto', MEDEVAC=true, salvageValue=3, crewSize=3 } +cat['BLUE_M1A2C_ABRAMS_CRATE'] = { hidden=true, description='M1A2C Abrams crate', dcsCargoType='container_cargo', required=1, initialStock=24, side=BLUE, category=Group.Category.GROUND } +cat['BLUE_M1A2C_ABRAMS'] = { menuCategory='Combat Vehicles', menu='M1A2C Abrams', description='M1A2C Abrams', dcsCargoType='container_cargo', requires={ BLUE_M1A2C_ABRAMS_CRATE=3 }, initialStock=0, side=BLUE, category=Group.Category.GROUND, build=singleUnit('M1A2C_SEP_V3'), unitType='M1A2C_SEP_V3', MEDEVAC=true, salvageValue=3, crewSize=4 } + +-- Combat Vehicles (RED) +cat['RED_BTR82A_CRATE'] = { hidden=true, description='BTR-82A crate', dcsCargoType='container_cargo', required=1, initialStock=30, side=RED, category=Group.Category.GROUND } +cat['RED_BTR82A'] = { menuCategory='Combat Vehicles', menu='BTR-82A', description='BTR-82A', dcsCargoType='container_cargo', requires={ RED_BTR82A_CRATE=3 }, initialStock=0, side=RED, category=Group.Category.GROUND, build=singleUnit('BTR-82A'), unitType='BTR-82A', MEDEVAC=true, salvageValue=2, crewSize=3 } +cat['RED_BRDM2_CRATE'] = { hidden=true, description='BRDM-2 crate', dcsCargoType='container_cargo', required=1, initialStock=30, side=RED, category=Group.Category.GROUND } +cat['RED_BRDM2'] = { menuCategory='Combat Vehicles', menu='BRDM-2', description='BRDM-2', dcsCargoType='container_cargo', requires={ RED_BRDM2_CRATE=3 }, initialStock=0, side=RED, category=Group.Category.GROUND, build=singleUnit('BRDM-2'), unitType='BRDM-2', MEDEVAC=true, salvageValue=2, crewSize=2 } +cat['RED_BMP3_CRATE'] = { hidden=true, description='BMP-3 crate', dcsCargoType='container_cargo', required=1, initialStock=30, side=RED, category=Group.Category.GROUND } +cat['RED_BMP3'] = { menuCategory='Combat Vehicles', menu='BMP-3', description='BMP-3', dcsCargoType='container_cargo', requires={ RED_BMP3_CRATE=3 }, initialStock=0, side=RED, category=Group.Category.GROUND, build=singleUnit('BMP-3'), unitType='BMP-3', MEDEVAC=true, salvageValue=2, crewSize=3 } +cat['RED_BMP2_CRATE'] = { hidden=true, description='BMP-2 crate', dcsCargoType='container_cargo', required=1, initialStock=30, side=RED, category=Group.Category.GROUND } +cat['RED_BMP2'] = { menuCategory='Combat Vehicles', menu='BMP-2', description='BMP-2', dcsCargoType='container_cargo', requires={ RED_BMP2_CRATE=3 }, initialStock=0, side=RED, category=Group.Category.GROUND, build=singleUnit('BMP-2'), unitType='BMP-2', MEDEVAC=true, salvageValue=2, crewSize=3 } +cat['RED_BTR80_CRATE'] = { hidden=true, description='BTR-80 crate', dcsCargoType='container_cargo', required=1, initialStock=30, side=RED, category=Group.Category.GROUND } +cat['RED_BTR80'] = { menuCategory='Combat Vehicles', menu='BTR-80', description='BTR-80', dcsCargoType='container_cargo', requires={ RED_BTR80_CRATE=3 }, initialStock=0, side=RED, category=Group.Category.GROUND, build=singleUnit('BTR-80'), unitType='BTR-80', MEDEVAC=true, salvageValue=2, crewSize=3 } +cat['RED_T72B3_CRATE'] = { hidden=true, description='T-72B3 crate', dcsCargoType='container_cargo', required=1, initialStock=24, side=RED, category=Group.Category.GROUND } +cat['RED_T72B3'] = { menuCategory='Combat Vehicles', menu='T-72B3', description='T-72B3', dcsCargoType='container_cargo', requires={ RED_T72B3_CRATE=3 }, initialStock=0, side=RED, category=Group.Category.GROUND, build=singleUnit('T-72B3'), unitType='T-72B3', MEDEVAC=true, salvageValue=3, crewSize=3 } +cat['RED_T90M_CRATE'] = { hidden=true, description='T-90M crate', dcsCargoType='container_cargo', required=1, initialStock=24, side=RED, category=Group.Category.GROUND } +cat['RED_T90M'] = { menuCategory='Combat Vehicles', menu='T-90M', description='T-90M', dcsCargoType='container_cargo', requires={ RED_T90M_CRATE=3 }, initialStock=0, side=RED, category=Group.Category.GROUND, build=singleUnit('CHAP_T90M'), unitType='CHAP_T90M', MEDEVAC=true, salvageValue=3, crewSize=3 } + +-- Support (BLUE) +cat['BLUE_MRAP_JTAC'] = { menuCategory='Support', menu='MRAP - JTAC', description='JTAC MRAP', dcsCargoType='container_cargo', required=1, initialStock=12, side=BLUE, category=Group.Category.GROUND, build=singleUnit('MaxxPro_MRAP'), MEDEVAC=true, salvageValue=1, crewSize=4, roles={'JTAC'}, jtac={ platform='ground' } } +cat['BLUE_M818_AMMO'] = { menuCategory='Support', menu='M-818 Ammo Truck', description='M-818 Ammo Truck', dcsCargoType='container_cargo', required=1, initialStock=12, side=BLUE, category=Group.Category.GROUND, build=singleUnit('M 818'), salvageValue=1, crewSize=2 } +cat['BLUE_M978_TANKER'] = { menuCategory='Support', menu='M-978 Tanker', description='M-978 Tanker', dcsCargoType='container_cargo', required=1, initialStock=10, side=BLUE, category=Group.Category.GROUND, build=singleUnit('M978 HEMTT Tanker'), salvageValue=1, crewSize=2 } +cat['BLUE_EWR_FPS117'] = { menuCategory='Support', menu='EWR Radar FPS-117', description='EWR Radar FPS-117', dcsCargoType='container_cargo', required=1, initialStock=6, side=BLUE, category=Group.Category.GROUND, build=singleUnit('FPS-117'), salvageValue=1, crewSize=3 } + +-- Support (RED) +cat['RED_TIGR_JTAC'] = { menuCategory='Support', menu='Tigr - JTAC', description='JTAC Tigr', dcsCargoType='container_cargo', required=1, initialStock=12, side=RED, category=Group.Category.GROUND, build=singleUnit('Tigr_233036'), MEDEVAC=true, salvageValue=1, crewSize=4, roles={'JTAC'}, jtac={ platform='ground' } } +cat['RED_URAL4320_AMMO'] = { menuCategory='Support', menu='Ural-4320-31 Ammo Truck', description='Ural-4320-31 Ammo Truck', dcsCargoType='container_cargo', required=1, initialStock=12, side=RED, category=Group.Category.GROUND, build=singleUnit('Ural-4320-31'), salvageValue=1, crewSize=2 } +cat['RED_ATZ10_TANKER'] = { menuCategory='Support', menu='ATZ-10 Refueler', description='ATZ-10 Refueler', dcsCargoType='container_cargo', required=1, initialStock=10, side=RED, category=Group.Category.GROUND, build=singleUnit('ATZ-10'), salvageValue=1, crewSize=2 } +cat['RED_EWR_1L13'] = { menuCategory='Support', menu='EWR Radar 1L13', description='EWR Radar 1L13', dcsCargoType='container_cargo', required=1, initialStock=6, side=RED, category=Group.Category.GROUND, build=singleUnit('1L13 EWR'), salvageValue=1, crewSize=3 } + +-- Artillery (BLUE) +cat['BLUE_MLRS_CRATE'] = { hidden=true, description='MLRS crate', dcsCargoType='container_cargo', required=1, initialStock=16, side=BLUE, category=Group.Category.GROUND } +cat['BLUE_MLRS'] = { menuCategory='Artillery', menu='MLRS', description='MLRS', dcsCargoType='container_cargo', requires={ BLUE_MLRS_CRATE=2 }, initialStock=0, side=BLUE, category=Group.Category.GROUND, build=singleUnit('MLRS'), salvageValue=2, crewSize=3 } +cat['BLUE_SMERCH_CM_CRATE'] = { hidden=true, description='Smerch (CM) crate', dcsCargoType='container_cargo', required=1, initialStock=12, side=BLUE, category=Group.Category.GROUND } +cat['BLUE_SMERCH_CM'] = { menuCategory='Artillery', menu='Smerch_CM', description='Smerch (CM)', dcsCargoType='container_cargo', requires={ BLUE_SMERCH_CM_CRATE=2 }, initialStock=0, side=BLUE, category=Group.Category.GROUND, build=singleUnit('Smerch'), salvageValue=2, crewSize=3 } +cat['BLUE_L118_105MM'] = { menuCategory='Artillery', menu='L118 Light Artillery 105mm', description='L118 105mm', dcsCargoType='container_cargo', required=1, initialStock=10, side=BLUE, category=Group.Category.GROUND, build=singleUnit('L118_Unit'), salvageValue=1, crewSize=5 } +cat['BLUE_SMERCH_HE_CRATE'] = { hidden=true, description='Smerch (HE) crate', dcsCargoType='container_cargo', required=1, initialStock=12, side=BLUE, category=Group.Category.GROUND } +cat['BLUE_SMERCH_HE'] = { menuCategory='Artillery', menu='Smerch_HE', description='Smerch (HE)', dcsCargoType='container_cargo', requires={ BLUE_SMERCH_HE_CRATE=2 }, initialStock=0, side=BLUE, category=Group.Category.GROUND, build=singleUnit('Smerch_HE'), salvageValue=2, crewSize=3 } +cat['BLUE_M109_CRATE'] = { hidden=true, description='M-109 crate', dcsCargoType='container_cargo', required=1, initialStock=16, side=BLUE, category=Group.Category.GROUND } +cat['BLUE_M109'] = { menuCategory='Artillery', menu='M-109', description='M-109', dcsCargoType='container_cargo', requires={ BLUE_M109_CRATE=2 }, initialStock=0, side=BLUE, category=Group.Category.GROUND, build=singleUnit('M-109'), salvageValue=2, crewSize=4 } + +-- Artillery (RED) +cat['RED_GVOZDIKA_CRATE'] = { hidden=true, description='SAU Gvozdika crate', dcsCargoType='container_cargo', required=1, initialStock=16, side=RED, category=Group.Category.GROUND } +cat['RED_GVOZDika'] = { menuCategory='Artillery', menu='SAU Gvozdika', description='SAU Gvozdika', dcsCargoType='container_cargo', requires={ RED_GVOZDIKA_CRATE=2 }, initialStock=0, side=RED, category=Group.Category.GROUND, build=singleUnit('SAU Gvozdika'), salvageValue=2, crewSize=3 } +cat['RED_2S19_MSTA_CRATE'] = { hidden=true, description='SPH 2S19 Msta crate', dcsCargoType='container_cargo', required=1, initialStock=16, side=RED, category=Group.Category.GROUND } +cat['RED_2S19_MSTA'] = { menuCategory='Artillery', menu='SPH 2S19 Msta', description='SPH 2S19 Msta', dcsCargoType='container_cargo', requires={ RED_2S19_MSTA_CRATE=2 }, initialStock=0, side=RED, category=Group.Category.GROUND, build=singleUnit('SAU Msta'), salvageValue=2, crewSize=4 } +cat['RED_URAGAN_BM27_CRATE'] = { hidden=true, description='Uragan BM-27 crate', dcsCargoType='container_cargo', required=1, initialStock=12, side=RED, category=Group.Category.GROUND } +cat['RED_URAGAN_BM27'] = { menuCategory='Artillery', menu='Uragan_BM-27', description='Uragan BM-27', dcsCargoType='container_cargo', requires={ RED_URAGAN_BM27_CRATE=2 }, initialStock=0, side=RED, category=Group.Category.GROUND, build=singleUnit('Uragan_BM-27'), salvageValue=2, crewSize=3 } +cat['RED_BM21_GRAD_CRATE'] = { hidden=true, description='BM-21 Grad crate', dcsCargoType='container_cargo', required=1, initialStock=16, side=RED, category=Group.Category.GROUND } +cat['RED_BM21_GRAD'] = { menuCategory='Artillery', menu='BM-21 Grad Ural', description='BM-21 Grad Ural', dcsCargoType='container_cargo', requires={ RED_BM21_GRAD_CRATE=2 }, initialStock=0, side=RED, category=Group.Category.GROUND, build=singleUnit('Grad-URAL'), salvageValue=2, crewSize=3 } +cat['RED_PLZ05_CRATE'] = { hidden=true, description='PLZ-05 crate', dcsCargoType='container_cargo', required=1, initialStock=12, side=RED, category=Group.Category.GROUND } +cat['RED_PLZ05'] = { menuCategory='Artillery', menu='PLZ-05 Mobile Artillery', description='PLZ-05', dcsCargoType='container_cargo', requires={ RED_PLZ05_CRATE=2 }, initialStock=0, side=RED, category=Group.Category.GROUND, build=singleUnit('PLZ05'), salvageValue=2, crewSize=4 } + +-- AAA (BLUE) +cat['BLUE_GEPARD'] = { menuCategory='AAA', menu='Gepard AAA', description='Gepard AAA', dcsCargoType='container_cargo', required=1, initialStock=10, side=BLUE, category=Group.Category.GROUND, build=singleUnit('Gepard'), salvageValue=1, crewSize=3 } +cat['BLUE_CRAM'] = { menuCategory='AAA', menu='LPWS C-RAM', description='LPWS C-RAM', dcsCargoType='container_cargo', required=1, initialStock=8, side=BLUE, category=Group.Category.GROUND, build=singleUnit('HEMTT_C-RAM_Phalanx'), salvageValue=1, crewSize=2 } +cat['BLUE_VULCAN_M163'] = { menuCategory='AAA', menu='SPAAA Vulcan M163', description='Vulcan M163', dcsCargoType='container_cargo', required=1, initialStock=10, side=BLUE, category=Group.Category.GROUND, build=singleUnit('Vulcan'), salvageValue=1, crewSize=2 } +cat['BLUE_BOFORS40'] = { menuCategory='AAA', menu='Bofors 40mm', description='Bofors 40mm', dcsCargoType='container_cargo', required=1, initialStock=12, side=BLUE, category=Group.Category.GROUND, build=singleUnit('bofors40'), salvageValue=1, crewSize=4 } + +-- AAA (RED) +cat['RED_URAL_ZU23'] = { menuCategory='AAA', menu='Ural-375 ZU-23', description='Ural-375 ZU-23', dcsCargoType='container_cargo', required=1, initialStock=12, side=RED, category=Group.Category.GROUND, build=singleUnit('Ural-375 ZU-23'), salvageValue=1, crewSize=3 } +cat['RED_SHILKA'] = { menuCategory='AAA', menu='ZSU-23-4 Shilka', description='ZSU-23-4 Shilka', dcsCargoType='container_cargo', required=1, initialStock=10, side=RED, category=Group.Category.GROUND, build=singleUnit('ZSU-23-4 Shilka'), salvageValue=1, crewSize=3 } +cat['RED_ZSU57_2'] = { menuCategory='AAA', menu='ZSU_57_2', description='ZSU_57_2', dcsCargoType='container_cargo', required=1, initialStock=10, side=RED, category=Group.Category.GROUND, build=singleUnit('ZSU_57_2'), salvageValue=1, crewSize=3 } + +cat['BLUE_M1097_AVENGER_CRATE'] = { hidden=true, description='M1097 Avenger crate', dcsCargoType='container_cargo', required=1, initialStock=16, side=BLUE, category=Group.Category.GROUND } +cat['BLUE_M1097_AVENGER'] = { menuCategory='SAM short range', menu='M1097 Avenger', description='M1097 Avenger', dcsCargoType='container_cargo', requires={ BLUE_M1097_AVENGER_CRATE=2 }, initialStock=0, side=BLUE, category=Group.Category.GROUND, build=singleUnit('M1097 Avenger') } +cat['BLUE_M48_CHAPARRAL_CRATE'] = { hidden=true, description='M48 Chaparral crate', dcsCargoType='container_cargo', required=1, initialStock=16, side=BLUE, category=Group.Category.GROUND } +cat['BLUE_M48_CHAPARRAL'] = { menuCategory='SAM short range', menu='M48 Chaparral', description='M48 Chaparral', dcsCargoType='container_cargo', requires={ BLUE_M48_CHAPARRAL_CRATE=2 }, initialStock=0, side=BLUE, category=Group.Category.GROUND, build=singleUnit('M48 Chaparral') } +cat['BLUE_ROLAND_ADS_CRATE'] = { hidden=true, description='Roland ADS crate', dcsCargoType='container_cargo', required=1, initialStock=16, side=BLUE, category=Group.Category.GROUND } +cat['BLUE_ROLAND_ADS'] = { menuCategory='SAM short range', menu='Roland ADS', description='Roland ADS', dcsCargoType='container_cargo', requires={ BLUE_ROLAND_ADS_CRATE=2 }, initialStock=0, side=BLUE, category=Group.Category.GROUND, build=singleUnit('Roland ADS') } +cat['BLUE_M6_LINEBACKER'] = { menuCategory='SAM short range', menu='M6 Linebacker', description='M6 Linebacker', dcsCargoType='container_cargo', required=1, initialStock=10, side=BLUE, category=Group.Category.GROUND, build=singleUnit('M6 Linebacker') } +cat['BLUE_RAPIER_LN'] = { menuCategory='SAM short range', menu='Rapier Launcher', description='Rapier Launcher', dcsCargoType='container_cargo', required=1, initialStock=10, side=BLUE, category=Group.Category.GROUND, build=singleUnit('rapier_fsa_launcher') } +cat['BLUE_RAPIER_SR'] = { menuCategory='SAM short range', menu='Rapier SR', description='Rapier SR', dcsCargoType='container_cargo', required=1, initialStock=10, side=BLUE, category=Group.Category.GROUND, build=singleUnit('rapier_fsa_blindfire_radar') } +cat['BLUE_RAPIER_TR'] = { menuCategory='SAM short range', menu='Rapier Tracker', description='Rapier Tracker', dcsCargoType='container_cargo', required=1, initialStock=10, side=BLUE, category=Group.Category.GROUND, build=singleUnit('rapier_fsa_optical_tracker_unit') } +cat['BLUE_RAPIER_SITE'] = { menuCategory='SAM short range', menu='Rapier - All crates', description='Rapier Site', dcsCargoType='container_cargo', requires={ BLUE_RAPIER_LN=1, BLUE_RAPIER_SR=1, BLUE_RAPIER_TR=1 }, initialStock=0, side=BLUE, category=Group.Category.GROUND, + build=multiUnits({ {type='rapier_fsa_launcher'}, {type='rapier_fsa_blindfire_radar', dx=12, dz=6}, {type='rapier_fsa_optical_tracker_unit', dx=-12, dz=6} }) } + +-- SAM short range (RED) +cat['RED_OSA_9K33_CRATE'] = { hidden=true, description='9K33 Osa crate', dcsCargoType='container_cargo', required=1, initialStock=16, side=RED, category=Group.Category.GROUND } +cat['RED_OSA_9K33'] = { menuCategory='SAM short range', menu='9K33 Osa', description='9K33 Osa', dcsCargoType='container_cargo', requires={ RED_OSA_9K33_CRATE=2 }, initialStock=0, side=RED, category=Group.Category.GROUND, build=singleUnit('Osa 9A33 ln') } +cat['RED_STRELA1_9P31_CRATE'] = { hidden=true, description='9P31 Strela-1 crate', dcsCargoType='container_cargo', required=1, initialStock=16, side=RED, category=Group.Category.GROUND } +cat['RED_STRELA1_9P31'] = { menuCategory='SAM short range', menu='9P31 Strela-1', description='9P31 Strela-1', dcsCargoType='container_cargo', requires={ RED_STRELA1_9P31_CRATE=2 }, initialStock=0, side=RED, category=Group.Category.GROUND, build=singleUnit('Strela-1 9P31') } +cat['RED_TUNGUSKA_2S6_CRATE'] = { hidden=true, description='2K22 Tunguska crate', dcsCargoType='container_cargo', required=1, initialStock=16, side=RED, category=Group.Category.GROUND } +cat['RED_TUNGUSKA_2S6'] = { menuCategory='SAM short range', menu='2K22 Tunguska', description='2K22 Tunguska', dcsCargoType='container_cargo', requires={ RED_TUNGUSKA_2S6_CRATE=2 }, initialStock=0, side=RED, category=Group.Category.GROUND, build=singleUnit('2S6 Tunguska') } +cat['RED_STRELA10M3_CRATE'] = { hidden=true, description='SA-13 Strela-10M3 crate', dcsCargoType='container_cargo', required=1, initialStock=16, side=RED, category=Group.Category.GROUND } +cat['RED_STRELA10M3'] = { menuCategory='SAM short range', menu='SA-13 Strela-10M3', description='SA-13 Strela-10M3', dcsCargoType='container_cargo', requires={ RED_STRELA10M3_CRATE=2 }, initialStock=0, side=RED, category=Group.Category.GROUND, build=singleUnit('Strela-10M3') } +-- HQ-7 components and site +cat['RED_HQ7_LN_CRATE'] = { hidden=true, description='HQ-7 Launcher crate', dcsCargoType='container_cargo', required=1, initialStock=20, side=RED, category=Group.Category.GROUND } +cat['RED_HQ7_LN'] = { menuCategory='SAM short range', menu='HQ-7_Launcher', description='HQ-7 Launcher', dcsCargoType='container_cargo', requires={ RED_HQ7_LN_CRATE=2 }, initialStock=0, side=RED, category=Group.Category.GROUND, build=singleUnit('HQ-7_LN_SP') } +cat['RED_HQ7_STR'] = { menuCategory='SAM short range', menu='HQ-7_STR_SP', description='HQ-7 STR', dcsCargoType='container_cargo', required=1, initialStock=10, side=RED, category=Group.Category.GROUND, build=singleUnit('HQ-7_STR_SP') } +cat['RED_HQ7_SITE'] = { menuCategory='SAM short range', menu='HQ-7 - All crates', description='HQ-7 Site', dcsCargoType='container_cargo', requires={ RED_HQ7_LN=1, RED_HQ7_STR=1 }, initialStock=0, side=RED, category=Group.Category.GROUND, + build=multiUnits({ {type='HQ-7_LN_SP'}, {type='HQ-7_STR_SP', dx=10, dz=8} }) } + +-- SAM mid range (BLUE) HAWK + NASAMS +cat['BLUE_HAWK_LN'] = { menuCategory='SAM mid range', menu='HAWK Launcher', description='HAWK Launcher', dcsCargoType='container_cargo', required=1, initialStock=8, side=BLUE, category=Group.Category.GROUND, build=singleUnit('Hawk ln') } +cat['BLUE_HAWK_SR'] = { menuCategory='SAM mid range', menu='HAWK Search Radar', description='HAWK SR', dcsCargoType='container_cargo', required=1, initialStock=8, side=BLUE, category=Group.Category.GROUND, build=singleUnit('Hawk sr') } +cat['BLUE_HAWK_TR'] = { menuCategory='SAM mid range', menu='HAWK Track Radar', description='HAWK TR', dcsCargoType='container_cargo', required=1, initialStock=8, side=BLUE, category=Group.Category.GROUND, build=singleUnit('Hawk tr') } +cat['BLUE_HAWK_PCP'] = { menuCategory='SAM mid range', menu='HAWK PCP', description='HAWK PCP', dcsCargoType='container_cargo', required=1, initialStock=8, side=BLUE, category=Group.Category.GROUND, build=singleUnit('Hawk pcp') } +cat['BLUE_HAWK_CWAR'] = { menuCategory='SAM mid range', menu='HAWK CWAR', description='HAWK CWAR', dcsCargoType='container_cargo', required=1, initialStock=8, side=BLUE, category=Group.Category.GROUND, build=singleUnit('Hawk cwar') } +cat['BLUE_HAWK_SITE'] = { menuCategory='SAM mid range', menu='HAWK - All crates', description='HAWK Site', dcsCargoType='container_cargo', requires={ BLUE_HAWK_LN=1, BLUE_HAWK_SR=1, BLUE_HAWK_TR=1, BLUE_HAWK_PCP=1, BLUE_HAWK_CWAR=1 }, initialStock=0, side=BLUE, category=Group.Category.GROUND, + build=multiUnits({ {type='Hawk ln'}, {type='Hawk sr', dx=12, dz=8}, {type='Hawk tr', dx=-12, dz=8}, {type='Hawk pcp', dx=18, dz=12}, {type='Hawk cwar', dx=-18, dz=12} }) } + +-- HAWK site repair/augment (adds +1 launcher, repairs site by respawn) +cat['BLUE_HAWK_REPAIR'] = { menuCategory='SAM mid range', menu='HAWK Repair/Launcher +1', description='HAWK Repair (adds launcher)', dcsCargoType='container_cargo', required=1, initialStock=8, side=BLUE, category=Group.Category.GROUND, isRepair=true, build=function(point, headingDeg) + -- Build is handled specially in CTLD:BuildSpecificAtGroup for isRepair entries + return singleUnit('Ural-375')(point, headingDeg) +end } + +cat['BLUE_NASAMS_LN'] = { menuCategory='SAM mid range', menu='NASAMS Launcher 120C', description='NASAMS LN 120C', dcsCargoType='container_cargo', required=1, initialStock=8, side=BLUE, category=Group.Category.GROUND, build=singleUnit('NASAMS_LN_C') } +cat['BLUE_NASAMS_RADAR'] = { menuCategory='SAM mid range', menu='NASAMS Search/Track Radar', description='NASAMS Radar', dcsCargoType='container_cargo', required=1, initialStock=8, side=BLUE, category=Group.Category.GROUND, build=singleUnit('NASAMS_Radar_MPQ64F1') } +cat['BLUE_NASAMS_CP'] = { menuCategory='SAM mid range', menu='NASAMS Command Post', description='NASAMS CP', dcsCargoType='container_cargo', required=1, initialStock=8, side=BLUE, category=Group.Category.GROUND, build=singleUnit('NASAMS_Command_Post') } +cat['BLUE_NASAMS_SITE'] = { menuCategory='SAM mid range', menu='NASAMS - All crates', description='NASAMS Site', dcsCargoType='container_cargo', requires={ BLUE_NASAMS_LN=1, BLUE_NASAMS_RADAR=1, BLUE_NASAMS_CP=1 }, initialStock=0, side=BLUE, category=Group.Category.GROUND, + build=multiUnits({ {type='NASAMS_LN_C'}, {type='NASAMS_Radar_MPQ64F1', dx=12, dz=8}, {type='NASAMS_Command_Post', dx=-12, dz=8} }) } + +-- SAM mid range (RED) KUB +cat['RED_KUB_LN'] = { menuCategory='SAM mid range', menu='KUB Launcher', description='KUB Launcher', dcsCargoType='container_cargo', required=1, initialStock=8, side=RED, category=Group.Category.GROUND, build=singleUnit('Kub 2P25 ln') } +cat['RED_KUB_RADAR'] = { menuCategory='SAM mid range', menu='KUB Radar', description='KUB Radar', dcsCargoType='container_cargo', required=1, initialStock=8, side=RED, category=Group.Category.GROUND, build=singleUnit('Kub 1S91 str') } +cat['RED_KUB_SITE'] = { menuCategory='SAM mid range', menu='KUB - All crates', description='KUB Site', dcsCargoType='container_cargo', requires={ RED_KUB_LN=1, RED_KUB_RADAR=1 }, initialStock=0, side=RED, category=Group.Category.GROUND, + build=multiUnits({ {type='Kub 2P25 ln'}, {type='Kub 1S91 str', dx=12, dz=8} }) } + +-- KUB site repair/augment (adds +1 launcher, repairs site by respawn) +cat['RED_KUB_REPAIR'] = { menuCategory='SAM mid range', menu='KUB Repair/Launcher +1', description='KUB Repair (adds launcher)', dcsCargoType='container_cargo', required=1, initialStock=8, side=RED, category=Group.Category.GROUND, isRepair=true, build=function(point, headingDeg) + return singleUnit('Ural-375')(point, headingDeg) +end } + +-- SAM long range (BLUE) Patriot +cat['BLUE_PATRIOT_LN'] = { menuCategory='SAM long range', menu='Patriot Launcher', description='Patriot Launcher', dcsCargoType='container_cargo', required=1, initialStock=6, side=BLUE, category=Group.Category.GROUND, build=singleUnit('Patriot ln') } +cat['BLUE_PATRIOT_RADAR'] = { menuCategory='SAM long range', menu='Patriot Radar', description='Patriot Radar', dcsCargoType='container_cargo', required=1, initialStock=6, side=BLUE, category=Group.Category.GROUND, build=singleUnit('Patriot str') } +cat['BLUE_PATRIOT_ECS'] = { menuCategory='SAM long range', menu='Patriot ECS', description='Patriot ECS', dcsCargoType='container_cargo', required=1, initialStock=6, side=BLUE, category=Group.Category.GROUND, build=singleUnit('Patriot ECS') } +cat['BLUE_PATRIOT_SITE'] = { menuCategory='SAM long range', menu='Patriot - All crates', description='Patriot Site', dcsCargoType='container_cargo', requires={ BLUE_PATRIOT_LN=1, BLUE_PATRIOT_RADAR=1, BLUE_PATRIOT_ECS=1 }, initialStock=0, side=BLUE, category=Group.Category.GROUND, + build=multiUnits({ {type='Patriot ln'}, {type='Patriot str', dx=14, dz=10}, {type='Patriot ECS', dx=-14, dz=10} }) } + +-- Patriot site repair/augment (adds +1 launcher, repairs site by respawn) +cat['BLUE_PATRIOT_REPAIR'] = { menuCategory='SAM long range', menu='Patriot Repair/Launcher +1', description='Patriot Repair (adds launcher)', dcsCargoType='container_cargo', required=1, initialStock=6, side=BLUE, category=Group.Category.GROUND, isRepair=true, build=function(point, headingDeg) + return singleUnit('Ural-375')(point, headingDeg) +end } + +-- SAM long range (RED) BUK +cat['RED_BUK_LN'] = { menuCategory='SAM long range', menu='BUK Launcher', description='BUK Launcher', dcsCargoType='container_cargo', required=1, initialStock=6, side=RED, category=Group.Category.GROUND, build=singleUnit('SA-11 Buk LN 9A310M1') } +cat['RED_BUK_SR'] = { menuCategory='SAM long range', menu='BUK Search Radar', description='BUK Search Radar', dcsCargoType='container_cargo', required=1, initialStock=6, side=RED, category=Group.Category.GROUND, build=singleUnit('SA-11 Buk SR 9S18M1') } +cat['RED_BUK_CC'] = { menuCategory='SAM long range', menu='BUK CC Radar', description='BUK CC Radar', dcsCargoType='container_cargo', required=1, initialStock=6, side=RED, category=Group.Category.GROUND, build=singleUnit('SA-11 Buk CC 9S470M1') } +cat['RED_BUK_SITE'] = { menuCategory='SAM long range', menu='BUK - All crates', description='BUK Site', dcsCargoType='container_cargo', requires={ RED_BUK_LN=1, RED_BUK_SR=1, RED_BUK_CC=1 }, initialStock=0, side=RED, category=Group.Category.GROUND, + build=multiUnits({ {type='SA-11 Buk LN 9A310M1'}, {type='SA-11 Buk SR 9S18M1', dx=12, dz=8}, {type='SA-11 Buk CC 9S470M1', dx=-12, dz=8} }) } + +-- BUK site repair/augment (adds +1 launcher, repairs site by respawn) +cat['RED_BUK_REPAIR'] = { menuCategory='SAM long range', menu='BUK Repair/Launcher +1', description='BUK Repair (adds launcher)', dcsCargoType='container_cargo', required=1, initialStock=6, side=RED, category=Group.Category.GROUND, isRepair=true, build=function(point, headingDeg) + return singleUnit('Ural-375')(point, headingDeg) +end } + +-- Drones (JTAC) +cat['BLUE_MQ9'] = { menuCategory='Drones', menu='MQ-9 Reaper - JTAC', description='MQ-9 JTAC', dcsCargoType='container_cargo', required=1, initialStock=3, side=BLUE, category=Group.Category.AIRPLANE, build=singleAirUnit('MQ-9 Reaper'), roles={'JTAC'}, jtac={ platform='air' } } +cat['RED_WINGLOONG'] = { menuCategory='Drones', menu='WingLoong-I - JTAC', description='WingLoong-I JTAC', dcsCargoType='container_cargo', required=1, initialStock=3, side=RED, category=Group.Category.AIRPLANE, build=singleAirUnit('WingLoong-I'), roles={'JTAC'}, jtac={ platform='air' } } + +-- FOB crates (Support) — three small crates build a FOB site +cat['FOB_SMALL'] = { hidden=true, description='FOB small crate', dcsCargoType='container_cargo', required=1, initialStock=12, side=nil, category=Group.Category.GROUND, build=function(point, headingDeg) + -- spawns a harmless placeholder truck for visibility; consumed by FOB_SITE build + return singleUnit('Ural-375')(point, headingDeg) +end } +cat['FOB_SITE'] = { menuCategory='Support', menu='FOB Crates - All', description='FOB Site', isFOB=true, dcsCargoType='container_cargo', requires={ FOB_SMALL=3 }, initialStock=0, side=nil, category=Group.Category.GROUND, + build=multiUnits({ {type='HEMTT TFFT'}, {type='Ural-375 PBU', dx=10, dz=8}, {type='Ural-375', dx=-10, dz=8} }) } + +-- Mobile MASH (Support) — three crates build a Mobile MASH unit +cat['MOBILE_MASH_SMALL'] = { hidden=true, description='Mobile MASH crate', dcsCargoType='container_cargo', required=1, initialStock=6, side=nil, category=Group.Category.GROUND, build=function(point, headingDeg) + -- spawns placeholder truck for visibility; consumed by MOBILE_MASH build + return singleUnit('Ural-375')(point, headingDeg) +end } +cat['BLUE_MOBILE_MASH'] = { menuCategory='Support', menu='Mobile MASH - All', description='Blue Mobile MASH Unit', isMobileMASH=true, dcsCargoType='container_cargo', requires={ MOBILE_MASH_SMALL=3 }, initialStock=0, side=BLUE, category=Group.Category.GROUND, build=singleUnit('M-113') } +cat['RED_MOBILE_MASH'] = { menuCategory='Support', menu='Mobile MASH - All', description='Red Mobile MASH Unit', isMobileMASH=true, dcsCargoType='container_cargo', requires={ MOBILE_MASH_SMALL=3 }, initialStock=0, side=RED, category=Group.Category.GROUND, build=singleUnit('BTR_D') } + +-- ========================= +-- Troop Type Definitions +-- ========================= +-- These define the composition of troop squads for Load/Unload Troops (NOT crates) +-- Structure: { label, size, unitsBlue, unitsRed, units (fallback) } +local troops = {} + +-- Assault Squad: general-purpose rifles/MG +troops['AS'] = { + label = 'Assault Squad', + size = 8, + unitsBlue = { 'Soldier M4', 'Soldier M249' }, + unitsRed = { 'Infantry AK', 'Infantry AK ver3' }, + units = { 'Infantry AK' }, +} + +-- MANPADS Team: Anti-air element +troops['AA'] = { + label = 'MANPADS Team', + size = 4, + unitsBlue = { 'Soldier stinger', 'Stinger comm' }, + unitsRed = { 'SA-18 Igla-S manpad', 'SA-18 Igla comm' }, + units = { 'Infantry AK' }, +} + +-- AT Team: Anti-tank element +troops['AT'] = { + label = 'AT Team', + size = 4, + unitsBlue = { 'Soldier RPG', 'Soldier RPG' }, + unitsRed = { 'Soldier RPG', 'Soldier RPG' }, + units = { 'Infantry AK' }, +} + +-- Mortar Team: Indirect fire element +troops['AR'] = { + label = 'Mortar Team', + size = 4, + unitsBlue = { '2B11 mortar' }, + unitsRed = { '2B11 mortar' }, + units = { '2B11 mortar' }, +} + +-- Export troop types +_CTLD_TROOP_TYPES = troops + +-- Also export as a global for mission setups that load via DO SCRIPT FILE (no return capture) +_CTLD_EXTRACTED_CATALOG = cat +return cat diff --git a/Moose_DynamicGroundBattle/Moose_DynamicGroundBattle_Plugin.lua b/Moose_DynamicGroundBattle/Moose_DynamicGroundBattle_Plugin.lua index 5f55593..44ba551 100644 --- a/Moose_DynamicGroundBattle/Moose_DynamicGroundBattle_Plugin.lua +++ b/Moose_DynamicGroundBattle/Moose_DynamicGroundBattle_Plugin.lua @@ -9,6 +9,7 @@ - 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 @@ -41,8 +42,12 @@ 4. Updated every UPDATE_MARK_POINTS_SCHED seconds AI Task Assignment: - - Groups spawn in friendly zones, then patrol toward nearest enemy zone - - Reassignment occurs every ASSIGN_TASKS_SCHED seconds + - 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 @@ -72,6 +77,10 @@ -- 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) @@ -80,24 +89,28 @@ local ENABLE_WAREHOUSE_MARKERS = true -- Enable/disable warehouse map markers ( 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 = 1800 -- Base spawn frequency for Red Infantry (seconds) +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 = 300 -- Base spawn frequency for Red Armor (seconds) +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 = 1800 -- Base spawn frequency for Blue Infantry (seconds) +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 = 300 -- Base spawn frequency for Blue Armor (seconds) +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) @@ -264,6 +277,14 @@ env.info("[DGB PLUGIN] Found " .. #zoneCaptureObjects .. " zones from DualCoalit -- 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() @@ -395,11 +416,172 @@ local function IsInfantryGroup(group) 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) @@ -411,9 +593,23 @@ local function AssignTasks(group, currentZoneCapture) local groupCoordinate = group:GetCoordinate() local currentZone = currentZoneCapture and currentZoneCapture:GetZone() or nil - -- If the group is sitting inside a friendly zone that is currently under attack, - -- keep them local so they fight for the objective instead of leaving it exposed. - if currentZoneCapture and currentZone and currentZoneCapture.GetCoalition and currentZoneCapture:GetCoalition() == groupCoalition then + -- 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())) @@ -454,6 +650,7 @@ 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 @@ -474,18 +671,23 @@ local function AssignTasksToGroups() end if inFriendlyZone then - -- Skip infantry if movement is disabled - if IsInfantryGroup(group) and not MOVING_INFANTRY_PATROLS 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.", tasksAssigned)) + env.info(string.format("[DGB PLUGIN] Task assignment complete. %d groups tasked (%d defenders).", tasksAssigned, defendersActive)) end -- Function to monitor and announce warehouse status @@ -496,16 +698,184 @@ local function MonitorWarehouses() local redSpawnFrequencyPercentage = CalculateSpawnFrequencyPercentage(redWarehouses) local blueSpawnFrequencyPercentage = CalculateSpawnFrequencyPercentage(blueWarehouses) - local msg = "[Warehouse Status]\n" - msg = msg .. "Red warehouses alive: " .. redWarehousesAlive .. " Reinforcements: " .. redSpawnFrequencyPercentage .. "%\n" - msg = msg .. "Blue warehouses alive: " .. blueWarehousesAlive .. " Reinforcements: " .. blueSpawnFrequencyPercentage .. "%\n" - MESSAGE:New(msg, 30):ToAll() + 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 ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -573,38 +943,55 @@ blueArmorSpawn = SPAWN:New(BLUE_ARMOR_SPAWN_GROUP) -- Helper to schedule spawns per category so each uses its intended cadence. local function ScheduleSpawner(spawnObject, getZonesFn, warehouses, baseFrequency, label, cadenceScalar) - local scheduler + local lastSpawnTime = 0 + local checkInterval = 10 -- Check every 10 seconds if it's time to spawn - local function spawnCycle() - local nextInterval = CalculateSpawnFrequency(warehouses, baseFrequency, cadenceScalar) + local function spawnCheck() + local currentTime = timer.getTime() + local spawnInterval = CalculateSpawnFrequency(warehouses, baseFrequency, cadenceScalar) - if not nextInterval then - env.info(string.format("[DGB PLUGIN] %s spawn paused (no warehouses alive)", label)) - if scheduler then - scheduler:Stop() - scheduler:Start(NO_WAREHOUSE_RECHECK_DELAY, NO_WAREHOUSE_RECHECK_DELAY) + 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 - local friendlyZones = getZonesFn() - local zonesAvailable = #friendlyZones + -- 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)] - spawnObject:SpawnInZone(chosenZone, false) - else - env.info(string.format("[DGB PLUGIN] %s spawn skipped (no friendly zones)", label)) - end - - if scheduler then - scheduler:Stop() - scheduler:Start(nextInterval, nextInterval) + 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 - local initialFrequency = baseFrequency * (cadenceScalar or 1) - scheduler = SCHEDULER:New(nil, spawnCycle, {}, math.random(5, 15), initialFrequency) + -- Single scheduler that runs continuously at fixed check interval + local scheduler = SCHEDULER:New(nil, spawnCheck, {}, math.random(5, 15), checkInterval) return scheduler end @@ -620,7 +1007,9 @@ if ENABLE_WAREHOUSE_MARKERS then end -- Schedule warehouse monitoring -SCHEDULER:New(nil, MonitorWarehouses, {}, 30, 120) +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) @@ -630,15 +1019,29 @@ 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"))