---------------------------------------------------------------------
-- File: Arty_Command.lua
-- Version: v3.0 (Operational Update)
-- Date: 2025-11-01 15:07 GMT
-- Author: Aidi + ChatGPT (collab)
--
-- Description:
--   Static Artillery Command & Fire-Control System
--
--   • Supports 6 independent batteries (SPG / MLRS / FDGN)
--   • Deploys and removes HQs with persistent map markers
--   • Realistic per-gun firing cadence and time-of-flight impacts
--   • Dynamic red target detection (MOOSE integrated)
--   • Fire mission presets and custom setups (HE / Smoke / Illum / Clu)
--   • Fully simulated ammo and resupply by CH-47 or C-130
--   • Battery illumination toggle (night ops support)
--   • Persistent battery state saving / loading
--   • Automated map marker system with live ammo & status updates
--   • Simplified, stable logic — optimized for mission performance
---------------------------------------------------------------------

---------------------------------------------------------------------
--  TABLE OF CONTENTS
---------------------------------------------------------------------
-- 1. CONFIGURATION  		General artillery, illumination, and resupply parameters.
-- 2. STATE  				Initializes battery data and base definitions.
-- 3. UTILITIES  			Core helper functions such as notifications and heading conversions.
-- 4. HQ CONTROL SYSTEM 	Handles HQ deployment, static object management, and readiness checks.
-- 5. FIRE MISSION SYSTEM   Executes fire missions with per-gun time-of-flight impacts and red-target scanning.
-- 6. FIRE MISSION SETUP  	Preset and custom loadouts, F10 menu generation, and gun-simulation logic.
-- 7. AMMO RESUPPLY SYSTEM  CH-47 and C-130 resupply logic, including illumination effects and flare signalling.
-- 8. BTY MAP MKR SYSTEM  	Self-refreshing HQ-style map markers for deployed batteries.
-- 9. PERSISTENCE SYSTEM  	Save and load artillery states to disk; includes auto-status monitoring.
-- 10. INITIALIZATION  		Final startup logic, MOOSE readiness check, and system load confirmation.
---------------------------------------------------------------------




---------------------------------------------------------------------
-- 1. CONFIGURATION
---------------------------------------------------------------------
-- General artillery, illumination, and resupply parameters.
-- Adjust these values to change engagement range, timing, and effects.
-- Distances are in meters unless otherwise stated; times are in seconds.
---------------------------------------------------------------------

-- ▪️ RANGE & BALLISTICS
local FIRE_RANGE_M                   = 30000    -- Maximum engagement range (m)
local FIRE_BASE_RANGE_M              = 11000    -- Base calibration range (m)
local FIRE_BASE_TOF_S                = 27       -- Time-of-flight at base range (s)

-- ▪️ IMPACT & EFFECTS
local FIRE_WARNING_BEFORE_IMPACT_S   = 10       -- Warning time before shells land (s)
local FIRE_SHELL_DELAY_S             = 2        -- Delay between each shell impact (s)

-- ▪️ EXPLOSION POWER LEVELS
local FIRE_EXPLOSION_LOW_PWR         = 100      -- Light explosion (small caliber / near miss)
local FIRE_EXPLOSION_MED_PWR         = 160      -- Standard HE explosion (default)
local FIRE_EXPLOSION_HIGH_PWR        = 220      -- Heavy explosion (MLRS / cluster / saturation)

-- ▪️ FIRE-MISSION TIMING
local FIRE_MSG_DURATION              = 4        -- Duration of initial mission info message
local FIRE_PLOTTING_DURATION         = 3        -- Duration of “Plotting firing solution...” message
local FIRE_ADJUST_DURATION           = 10       -- Duration of “Adjusting elevation and charge...” message
local FIRE_PAUSE_BEFORE_FIRE         = 7        -- Pause before guns begin firing (realistic delay)


---------------------------------------------------------------------
--  FIRE MISSION ILLUMINATION PARAMETERS
---------------------------------------------------------------------
local FIRE_ILLUM_HEIGHT_FT           = 500      -- Illumination burst altitude (feet AGL)
local FIRE_ILLUM_POWER_RADIUS        = 20000    -- Illumination light power / radius


---------------------------------------------------------------------
--  BATTERY ILLUMINATION (F10 TOGGLE)
---------------------------------------------------------------------
local BtyIllumEnabled                = true     -- true = ON by default, false = OFF


---------------------------------------------------------------------
--  RESUPPLY AIRCRAFT PARAMETERS
---------------------------------------------------------------------
local HELO_TPL_PREFIX                = "ReSupplyHelo_B"     -- CH-47 template prefix (e.g., "ReSupplyHelo_B1")
local C130_TPL_PREFIX                = "ResupplyC130_B"     -- C-130 template prefix (e.g., "ResupplyC130_B1")

local HELO_RTB_AIRBASE               = AIRBASE.Syria.Paphos -- RTB airbase for CH-47
local C130_RTB_AIRBASE               = AIRBASE.Syria.Paphos -- RTB airbase for C-130

local HELO_RESUPPLY_AMOUNT           = 20                   -- Ammo added after CH-47 resupply
local C130_RESUPPLY_AMOUNT           = 40                   -- Ammo added after C-130 resupply

local HELO_LZ_OFFSET_M               = 200                  -- CH-47 landing offset distance from HQ (m)
local HELO_APPROACH_SLOW_M           = 1200                 -- CH-47 slows for landing within this distance (m)

local C130_PROX_TRIGGER_M            = 200                  -- Distance (m) from HQ for C-130 ammo drop
local C130_SMOKE_LEAD_M              = 3.5 * 1852           -- Distance (m) ahead of HQ for green smoke (≈3.5 NM)

local FLARE_COLOR                    = trigger.flareColor.White -- Flare color for approach / illum
local SMOKE_COLOR                    = trigger.smokeColor.Green -- Smoke color for DZ or approach marking


---------------------------------------------------------------------
--  C-130 FLARE DISTANCE TRIGGERS (CONFIGURABLE)
---------------------------------------------------------------------
C130_FLARE_5KM_M                     = 5000     -- First flare trigger (~5 km from HQ)
C130_FLARE_2_5KM_M                   = 2500     -- Second flare trigger (~2.5 km from HQ)
C130_FLARE_1KM_M                     = 1000     -- Final flare trigger (~1 km from HQ)


---------------------------------------------------------------------
--  RESUPPLY ILLUMINATION TRIGGER PARAMETERS
---------------------------------------------------------------------
local RESUPPLY_C130_ILLUM_TRIGGER_M  = 8000     -- Distance (m) for C-130 illum trigger
local RESUPPLY_HELO_ILLUM_TRIGGER_M  = 2000     -- Distance (m) for CH-47 illum trigger
local RESUPPLY_ILLUM_HEIGHT_FT       = 500      -- Illumination burst altitude (feet AGL)
local RESUPPLY_ILLUM_POWER_RADIUS    = 20000    -- Illumination light power / radius


---------------------------------------------------------------------
-- ▪️ BATTERY SALVO FIRING BEHAVIOUR (PER-GUN)
---------------------------------------------------------------------
local GUN_FIRE_INTERVAL_S     = 10   -- Time between shots from the same gun (seconds)
local GUN_STAGGER_DELAY_S     = 5   -- Delay between Gun 1 and Gun 2 firing (seconds)

---------------------------------------------------------------------
--  BATTERY MAP MARKER SYSTEM  
---------------------------------------------------------------------

local BATTERY_MARKERS_ENABLED = true        -- toggle all markers on/off
local BATTERY_MARKER_OFFSET_M = 500         -- distance from HQ (m)
local BATTERY_MARKER_OFFSET_BRG = 0         -- direction offset (deg)
local BATTERY_MARKER_RANDOM_BEARING = false -- randomize direction per Bty
local BATTERY_MARKER_UPDATE_S = 10          -- refresh interval (s)
local _markerNextId = 5000                  -- ID counter

---------------------------------------------------------------------
-- SRS CONFIGURATION
---------------------------------------------------------------------
ARTY_SRS_FREQ_MHZ  = 243.0          -- Frequency (MHz)
ARTY_SRS_MOD       = "AM"           -- "AM" or "FM"
ARTY_SRS_LABEL     = "Arty"         -- Label for display & popup prefix
ARTY_SRS_POPUP     = true           -- true = show on-screen text with each call



local HQ_STATIC_TEMPLATE = {
  { category='Fortifications', type='Sandbag_15', dx=-18, dz=-10 },
  { category='Cargos', type='ammo_cargo', dx=25, dz=10 },
  { category='Cargos', type='ammo_cargo', dx=28, dz=10 },
  { category='Armor', type='M1045 HMMWV TOW', dx=-50, dz=-30 },
}

local PERSIST_FILE = "C:/Users/Aidi/Saved Games/DCS/Missions/Saves/Arty_v3.0.lua" -- change this to your save directory

-- #### For Persistene to work the Mission sanitization must be turned off ##### --

---------------------------------------------------------------------
-- TIMER fallback (safety if MOOSE not yet ready)
---------------------------------------------------------------------
TIMER = TIMER or {}
if not TIMER.New then
  function TIMER:New(func)
    return {
      Start = function(_, delay)
        timer.scheduleFunction(func, {}, timer.getTime() + delay)
      end
    }
  end
end


---------------------------------------------------------------------
-- 2. STATE
---------------------------------------------------------------------
local Batteries = {}
local state = {}

-- ▪ Battery type assignments (6 total slots)
local BATTERY_TYPES = { "SPG", "SPG", "MLRS", "MLRS", "FDGN", "FDGN" }

-- ▪ Initialize each battery slot with base fields
for i = 1, #BATTERY_TYPES do
  Batteries[i] = {
    id          = i,
    type        = BATTERY_TYPES[i],
    ammo        = 40,                 -- Starting ammo
    salvo       = 10,                 -- Default salvo size
    spread      = 100,                -- Default impact spread (m)
    effectPower = 160,                -- Explosion strength
    ammoType    = "H E",               -- Ammunition type
    hqCoord     = nil,                -- HQ / rally coordinate
    guns        = {},                 -- Nearby gun list
    menuParent  = nil,                -- Parent F10 menu
  }
end

---------------------------------------------------------------------
-- 3. UTILITIES
---------------------------------------------------------------------
-- ============================================================
-- SRS (MOOSE Sound.SRS over gRPC ONLY) — NO CSAR — ARTY
-- ============================================================

local _MSRS_ARTY = rawget(_G, "MSRS_ARTY")
local _msrsWarned = false

local function _initMSRS_ARTY()
  if _MSRS_ARTY then return _MSRS_ARTY end

  -- Enable gRPC backend
  if _G.GRPC and GRPC.load then pcall(GRPC.load) end
  if _G.MSRS and MSRS.SetDefaultBackendGRPC then pcall(MSRS.SetDefaultBackendGRPC) end

  -- Resolve modulation
  local mod
  if ARTY_SRS_MOD == "FM" then
    mod = (radio and radio.modulation and radio.modulation.FM) or 1
  else
    mod = (radio and radio.modulation and radio.modulation.AM) or 0
  end

  -- Create MSRS on configured frequency/modulation
  if _G.MSRS and MSRS.New then
    local ok, inst = pcall(function() return MSRS:New('', ARTY_SRS_FREQ_MHZ, mod) end)
    if ok and inst then
      if inst.SetLabel    then pcall(function() inst:SetLabel(ARTY_SRS_LABEL) end) end
      if inst.SetProvider then pcall(function() inst:SetProvider("gcloud") end) end
      if inst.SetVolume   then pcall(function() inst:SetVolume(1.0) end) end
      _MSRS_ARTY = inst
      _G.MSRS_ARTY = inst
      return inst
    end
  end
  return nil
end

-- Speak over SRS using MSRS_ARTY. Optional popup.
local function _srsSayARTY(text, showPopup)
  if not text or text == "" then return end
  local msrs = _initMSRS_ARTY()
  if not msrs then
    if not _msrsWarned then
      pcall(trigger.action.outText, "[ARTY] MSRS (gRPC) not available yet — audio muted.", 6)
      _msrsWarned = true
    end
    return
  end

  if msrs.PlayText then
    pcall(function() msrs:PlayText(text) end)
  elseif msrs.PlayTTS then
    pcall(function() msrs:PlayTTS(text) end)
  elseif msrs.TransmitTTS then
    pcall(function() msrs:TransmitTTS(text) end)
  elseif msrs.TransmitText then
    pcall(function() msrs:TransmitText(text) end)
  end

  local doMirror = (showPopup == true) or (showPopup == nil and ARTY_SRS_POPUP)
  if doMirror then
    if _G.MESSAGE and MESSAGE.New then
      MESSAGE:New((ARTY_SRS_LABEL .. ": " .. text), 8):ToAll()
    else
      trigger.action.outText((ARTY_SRS_LABEL .. ": " .. text), 8)
    end
  end
end




-------------------- End of SRS Global Function  -------------------------------

local function notify(msg, dur)
  dur = dur or 8
  trigger.action.outText(msg, dur)
end

local function headingDeg(headingRad)
  if not headingRad then return 0 end
  return (math.deg(headingRad)) % 360
end

---------------------------------------------------------------------
-- 4. HQ CONTROL SYSTEM
--    Handles HQ deployment, static objects, and basic readiness checks.
---------------------------------------------------------------------

-- ▪️ HQ STATIC TEMPLATE DEFINITIONS
local HQ_OFFSET_M = 50
local HQ_STATIC_DEF = {
  { category = "Fortifications", type = "Sandbag_15", dx = -10, dz = -10 },
  { category = "Fortifications", type = "Sandbag_15", dx =  10, dz = -10 },
  { category = "Cargos",         type = "ammo_cargo",  dx =   2, dz =   2 },
  { category = "Fortifications", type = "Tent04",      dx =   0, dz =   0 },
}


---------------------------------------------------------------------
-- STATIC HQ MANAGEMENT
---------------------------------------------------------------------

-- ▪ Spawn static HQ defenses and ammo objects
local function spawnHQStatics(coord, batteryId)
  if not coord then return {} end
  local names = {}

  for i, def in ipairs(HQ_STATIC_DEF) do
    local pos = coord:GetVec2()
    local x, z = pos.x + def.dx, pos.y + def.dz
    local name = string.format("HQ_Bty%d_%d", batteryId, i)

    coalition.addStaticObject(country.id.USA, {
      category = def.category,
      type     = def.type,
      name     = name,
      x        = x,
      y        = z,
      heading  = 0,
    })

    names[#names + 1] = name
  end

  return names
end


-- ▪ Destroy HQ statics by name
local function destroyStaticsByName(list)
  if not list then return end
  for _, n in ipairs(list) do
    local s = StaticObject.getByName(n)
    if s then pcall(function() s:destroy() end) end
  end
end


---------------------------------------------------------------------
-- HQ DEPLOYMENT & REMOVAL
---------------------------------------------------------------------

-- ▪ Deploy HQ near player helicopter
local function deployHQ(bty)
  if not bty then return end

  -- Find player helicopter
  local helo = nil
  local set = SET_GROUP:New()
    :FilterCoalitions("blue")
    :FilterCategoryHelicopter()
    :FilterActive(true)
    :FilterStart()

  set:ForEachGroup(function(g)
    if not helo then
      local u = g:GetUnit(1)
      if u and u:IsAlive() and u:IsPlayer() then helo = u end
    end
  end)

  if not helo then
    trigger.action.outText("Arty: No player helicopter found for HQ deploy.", 6)
    return
  end

  local hc   = helo:GetCoordinate()
  local hdg  = math.deg(helo:GetHeading()) % 360
  local rally = hc:Translate(HQ_OFFSET_M, (hdg + 180) % 360)

  if bty.hqNames then destroyStaticsByName(bty.hqNames) end
  bty.hqNames = spawnHQStatics(rally, bty.id)
  bty.hqCoord = rally
  bty.hdg = hdg

  trigger.action.outText(
    string.format("Arty: HQ deployed for Battery %d (%s).", bty.id, bty.type),
    8
  )

  saveState()
end


-- ▪ Remove HQ and cleanup statics
local function removeHQ(bty)
  if not bty then return end
  if bty.hqNames then destroyStaticsByName(bty.hqNames) end
  bty.hqNames, bty.hqCoord = {}, nil

  trigger.action.outText(
    string.format("Arty: HQ removed for Battery %d (%s).", bty.id, bty.type),
    8
  )

  saveState()
end


---------------------------------------------------------------------
-- UNIT DETECTION
---------------------------------------------------------------------

-- ▪ Determine if a unit matches a specific battery type
local function unitMatchesBatteryType(unit, typeKey)
  if not (unit and unit.IsAlive and unit:IsAlive()) then return false end

  local tn = (unit:GetTypeName() or ""):lower()
  local d  = unit:GetDCSObject()
  local attrs = (d and d:getDesc() or {}).attributes or {}

  local has      = function(k) return attrs[k] == true or attrs[k] == 1 end
  local contains = function(s) return tn:find(s, 1, true) ~= nil end

  if typeKey == "FDGN" or typeKey == "FDG" then typeKey = "FIELDGUN" end

  if typeKey == "MLRS" then
    return has("MLRS") or contains("mlrs") or contains("m270")
           or contains("bm-21") or contains("grad")

  elseif typeKey == "SPG" then
    if has("MLRS") then return false end
    return has("Artillery") or contains("m109") or contains("paladin")
           or contains("firtina") or contains("as90")

  elseif typeKey == "FIELDGUN" then
    if has("MLRS") then return false end
    return has("Artillery") or contains("m777") or contains("howitzer")
           or contains("d-30") or contains("field gun")
  end

  return false
end


-- ▪ Count valid guns near a coordinate
local function countMatchingGunsNearPoint(typeKey, coord, radius)
  if not coord then return 0 end
  local cnt = 0

  local set = SET_GROUP:New():FilterCoalitions("blue"):FilterActive(true):FilterStart()
  set:ForEachGroup(function(g)
    for _, u in pairs(g:GetUnits() or {}) do
      if unitMatchesBatteryType(u, typeKey) then
        if coord:Get2DDistance(u:GetCoordinate()) <= (radius or 300) then
          cnt = cnt + 1
        end
      end
    end
  end)

  return cnt
end


---------------------------------------------------------------------
-- BATTERY READINESS CHECKS
---------------------------------------------------------------------

-- ▪ Simple readiness check
local function batteryReady(bty)
  if not bty or not bty.hqCoord then return false end
  local guns = countMatchingGunsNearPoint(bty.type, bty.hqCoord, 300)
  return guns > 0
end


-- ▪ Check if battery can fire and notify if not
local function canBatteryFireOrNotify(bty)
  if not bty then return false end

  if not bty.hqCoord then
    notify(string.format("Bty%d HQ not deployed.", bty.id))
    return false
  end

  if not batteryReady(bty) then
    notify(string.format(
      "Bty%d (%s) has no valid guns nearby — unable to fire.",
      bty.id, bty.type
    ))
    return false
  end

  return true
end


---------------------------------------------------------------------
--  TACTICAL REPORTER
---------------------------------------------------------------------

local function reportAllBatteryStatus()
  if not Batteries then return end

  local lines = {}
  local deployedCount = 0
  local req = HQ_REQ_GUNS or 2

  for _, b in ipairs(Batteries) do
    if b.hqCoord then
      deployedCount = deployedCount + 1
      local guns = countMatchingGunsNearPoint(b.type, b.hqCoord, 300)
      local state = (guns >= req) and "ACTIVE" or "INACTIVE"

      local typeLabel = b.type
      if typeLabel == "SPG" then typeLabel = "SPGs" end

      lines[#lines + 1] = string.format(
        "Bty%-2d %-5s %-9s Guns %d/%-2d  Ammo %-3d",
        b.id, typeLabel, state, guns, req, b.ammo or 0
      )
    end
  end

  local msg = (deployedCount == 0)
    and "No HQs deployed."
    or ("--- TACTICAL ARTY STATUS ---\n\n" .. table.concat(lines, "\n"))

  env.info(msg)
  trigger.action.outTextForCoalition(coalition.side.BLUE, msg, 8)
end


---------------------------------------------------------------------
-- 5. FIRE MISSION SYSTEM
--    Executes battery fire missions with realistic per-gun TOF impacts.
---------------------------------------------------------------------

-- Detect all active red targets
local function findRedTargets()
  local set = SET_GROUP:New()
    :FilterCoalitions("red")
    :FilterActive(true)
    :FilterOnce()

  local reds = {}
  set:ForEachGroupAlive(function(g)
    table.insert(reds, g)
  end)

  return reds
end


---------------------------------------------------------------------
-- Fire mission execution
---------------------------------------------------------------------

local function fireMissionAtTarget(bty, target)
  if not bty or not bty.hqCoord then
    notify("Battery HQ not deployed.")
    return
  end

  if not canBatteryFireOrNotify(bty) then return end

  if (bty.ammo or 0) < bty.salvo then
    notify(string.format("Bty%d (%s) OUT OF AMMO!", bty.id, bty.type))
    return
  end

  bty.ammo = bty.ammo - bty.salvo

  if bty.ammo <= 5 then
    notify(string.format("⚠️ Bty%d low on ammo (%d rds left).", bty.id, bty.ammo))
  end

  if not target or not target:IsAlive() then
    notify("Target invalid or destroyed.")
    return
  end

  -- Calculate range and TOF
  local dist = bty.hqCoord:Get2DDistance(target:GetCoordinate())
  if dist > FIRE_RANGE_M then
    notify(string.format("Target %s out of range (%.1f km).", target:GetName(), dist / 1000))
    return
  end

  local tof = math.max(10, math.min(60, (dist / FIRE_BASE_RANGE_M) * FIRE_BASE_TOF_S))
  local rounds     = bty.salvo
  local ammoType   = bty.ammoType
  local spread     = bty.spread
  local effect     = bty.effectPower


  ---------------------------------------------------------------------
  -- Fire sequence (dialogue / messages)
  ---------------------------------------------------------------------

  timer.scheduleFunction(function()
    local msg = string.format(
      "Fire Mission: %s. Range %.1f kilometers. Time of flight %.0f seconds. %d rounds. %s. Spread %d meters.",
      target:GetName(), dist / 1000, tof, rounds, ammoType, spread
    )

    -- Fix for TTS: pronounce HE as 'H E'
    msg = msg:gsub("%f[%a]HE%f[%A]", "H E")

    notify(string.format(
      "Fire Mission: %s\nRange: %.1f km | TOF %.0fs | %drds | %s | %dm",
      target:GetName(), dist / 1000, tof, rounds, ammoType, spread
    ))

    _srsSayARTY(msg, false) -- True = Onscreen and SRS / False = SRS no onscreen message 

    return nil
  end, {}, timer.getTime() + 2)

  timer.scheduleFunction(function()
    notify("Plotting firing solution...", FIRE_PLOTTING_DURATION)
    return nil
  end, {}, timer.getTime() + 6)

  timer.scheduleFunction(function()
    notify("Adjusting elevation and charge...", FIRE_ADJUST_DURATION)
    return nil
  end, {}, timer.getTime() + 10)

  local firingStart = 2 + FIRE_MSG_DURATION + FIRE_PLOTTING_DURATION +
                      FIRE_ADJUST_DURATION + FIRE_PAUSE_BEFORE_FIRE


  ---------------------------------------------------------------------
  -- Actual firing sequence
  ---------------------------------------------------------------------

  -- ▪ Impact warning (10 seconds before impact)
  timer.scheduleFunction(function()
    local msg = "Impact in ten seconds."
    msg = msg:gsub("%f[%a]HE%f[%A]", "H E")  -- fix TTS pronunciation
    _srsSayARTY(msg, true)                   -- SRS + onscreen popup
    return nil
  end, {}, timer.getTime() + firingStart + (tof - FIRE_WARNING_BEFORE_IMPACT_S))

  -- ▪ Fire now
  timer.scheduleFunction(function()
    notify(string.format("Bty%d (%s) – Firing now!", bty.id, bty.type), 4)

    -- Each gun fires with its cadence and own TOF-based impacts
    simulateGunsFiring(
      bty,
      bty.salvo or 10,
      GUN_FIRE_INTERVAL_S or 15,
      target
    )

    -- ▪ Fire Mission Complete – 5 seconds after last shell impacts
    local totalTime = tof + ((bty.salvo or 10) * (GUN_FIRE_INTERVAL_S or 15)) + 5
    timer.scheduleFunction(function()
      local msg = string.format("Fire Mission – %s Complete – Check Fire.", target:GetName())
      msg = msg:gsub("%f[%a]HE%f[%A]", "H E")
      _srsSayARTY(msg, true) -- SRS + onscreen popup
    end, {}, timer.getTime() + totalTime)

    return nil
  end, {}, timer.getTime() + firingStart - FIRE_PAUSE_BEFORE_FIRE)
end


---------------------------------------------------------------------
-- Fire mission menu system
---------------------------------------------------------------------

local function rebuildFireMissionMenu(bty)
  if not bty or not bty.menuParent then return end

  if bty.fireMenu then bty.fireMenu:Remove() end
  bty.fireMenu = MENU_COALITION:New(coalition.side.BLUE, "Fire Mission", bty.menuParent)

  local reds = findRedTargets()

  if #reds == 0 then
    MENU_COALITION_COMMAND:New(
      coalition.side.BLUE,
      "(No Red Targets)",
      bty.fireMenu,
      function() notify("No red targets found within 30 km.") end
    )
  else
    for _, g in ipairs(reds) do
      MENU_COALITION_COMMAND:New(
        coalition.side.BLUE,
        g:GetName(),
        bty.fireMenu,
        function() fireMissionAtTarget(bty, g) end
      )
    end
  end

  return nil
end


-- Refresh all fire mission menus for every battery
local function refreshFireMissionMenus()
  if not Batteries then return end
  env.info("Arty: refreshing Fire Mission menus for all batteries.")
  for _, bty in ipairs(Batteries) do
    rebuildFireMissionMenu(bty)
  end
  return nil
end


---------------------------------------------------------------------
-- Red target event monitoring
---------------------------------------------------------------------

local RedTargetEventHandler = {}

function RedTargetEventHandler:onEvent(event)
  if not event or not event.id then return end

  if event.id == world.event.S_EVENT_GROUP_CREATED
  or event.id == world.event.S_EVENT_GROUP_REMOVED
  or event.id == world.event.S_EVENT_UNIT_LOST
  or event.id == world.event.S_EVENT_BIRTH then

    timer.scheduleFunction(function()
      refreshFireMissionMenus()
      return nil
    end, {}, timer.getTime() + 3)
  end
end

world.addEventHandler(RedTargetEventHandler)




---------------------------------------------------------------------
-- 6. FIRE MISSION SETUP
--    Preset configuration, F10 menu construction, and basic status utilities.
---------------------------------------------------------------------

---------------------------------------------------------------------
-- Preset configuration and info functions
---------------------------------------------------------------------

-- Apply a preset loadout to the selected battery
local function applyPreset(bty, name, salvo, ammo, spread, effect)
  bty.salvo    = salvo
  bty.ammoType = ammo
  bty.spread   = spread
  bty.effect   = effect

  if effect == "LOW" then
    bty.effectPower = FIRE_EXPLOSION_LOW_PWR
  elseif effect == "MED" then
    bty.effectPower = FIRE_EXPLOSION_MED_PWR
  else
    bty.effectPower = FIRE_EXPLOSION_HIGH_PWR
  end

  notify(string.format(
    "Bty%d Preset '%s' applied: %d %s %dm %s",
    bty.id, name, salvo, ammo, spread, effect
  ))

  saveState()
end


-- Display the current setup of a selected battery
local function showCurrentSetup(bty)
  notify(string.format(
    "Bty%d (%s): %drds | %s | %dm | %s",
    bty.id, bty.type, bty.salvo, bty.ammoType, bty.spread, bty.effect
  ))
end


---------------------------------------------------------------------
-- Tactical and ammo status displays
---------------------------------------------------------------------

-- Display overall artillery status table for all batteries
local function showAllArtyStatus()
  if not Batteries then
    notify("No artillery data available.")
    return
  end

  local lines, deployedCount = {}, 0
  local req = HQ_REQ_GUNS or 2

  for _, b in ipairs(Batteries) do
    if b.hqCoord then
      deployedCount = deployedCount + 1
      local guns  = countMatchingGunsNearPoint(b.type, b.hqCoord, 300)
      local state = (guns >= req) and "ACTIVE" or "INACTIVE"
      local typeLabel = (b.type == "SPG") and "SPGs" or b.type

      lines[#lines + 1] = string.format(
        "Bty%-2d %-5s %-9s Guns %d/%-2d  Ammo %-3d",
        b.id, typeLabel, state, guns, req, b.ammo or 0
      )
    end
  end

  local msg = (deployedCount == 0)
    and "No HQs deployed."
    or ("--- TACTICAL ARTY STATUS ---\n\n" .. table.concat(lines, "\n"))

  notify(msg, 12)
end


-- Display ammo count for a specific battery
local function showAmmoStatus(bty)
  if not bty then
    notify("No battery data available.")
    return
  end

  if not bty.hqCoord then
    notify(string.format("Bty%d HQ not deployed.", bty.id))
    return
  end

  local msg = string.format(
    "Bty%d (%s): Ammo Remaining – %d rounds",
    bty.id, (bty.type == "SPG" and "SPGs" or bty.type),
    bty.ammo or 0
  )

  notify(msg, 8)
end


---------------------------------------------------------------------
-- 🔫 SIMULATE GUNS FIRING + IMPACT SCHEDULER (per-gun TOF)
---------------------------------------------------------------------

local function _getGunUnitsNear(bty, radius)
  local units = {}
  if not bty or not bty.hqCoord then return units end
  local set = SET_GROUP:New():FilterCoalitions("blue"):FilterActive(true):FilterStart()
  set:ForEachGroup(function(g)
    for _, u in pairs(g:GetUnits() or {}) do
      if u and u:IsAlive() and unitMatchesBatteryType(u, bty.type) then
        if bty.hqCoord:Get2DDistance(u:GetCoordinate()) <= (radius or 300) then
          table.insert(units, u)
        end
      end
    end
  end)
  return units
end

-- ▪ Each gun fires visually, then schedules its own impact TOF seconds later.
function simulateGunsFiring(bty, rounds, rate_s, target)
  if not bty or not bty.hqCoord then return end
  if not target or not target:IsAlive() then return end
  rounds = rounds or (bty.salvo or 10)
  rate_s = bty.gunFireInterval or (GUN_FIRE_INTERVAL_S or 8)
  local staggerDelay = bty.gunStaggerDelay or (GUN_STAGGER_DELAY_S or 10)

  local muzzleOffset, muzzleHeight, flashPower = 20, 3, 1
  local guns = _getGunUnitsNear(bty, 500)
  if #guns == 0 then
    trigger.action.outText("❌ No guns found near HQ for simulateGunsFiring", 5)
    return
  end

  for gi, gun in ipairs(guns) do
    for r = 1, rounds do
      -- cadence between rounds + inter-gun stagger delay
      local delay = (r - 1) * rate_s + (gi - 1) * staggerDelay

      TIMER:New(function()
        if not gun or not gun:IsAlive() then return end
        local dcsUnit = gun:GetDCSObject()
        if not dcsUnit or not dcsUnit:isExist() then return end

        -- muzzle-flash position (20 m forward of barrel)
        local pos = dcsUnit:getPosition()
        if not pos or not pos.p or not pos.x then return end
        local p, fwd = pos.p, pos.x
        local muzzlePoint = {
          x = p.x + fwd.x * muzzleOffset,
          y = p.y + fwd.y * muzzleOffset + muzzleHeight,
          z = p.z + fwd.z * muzzleOffset,
        }

        -- 🔥 visual flash at gun
        pcall(function() trigger.action.explosion(muzzlePoint, flashPower) end)

        -----------------------------------------------------------------
        -- 🎯 Schedule impact relative to this gun’s firing moment
        -----------------------------------------------------------------
        if target and target:IsAlive() then
          local gunCoord = gun:GetCoordinate()
          local dist = gunCoord:Get2DDistance(target:GetCoordinate())
          local tof = math.max(10, math.min(60,
                      (dist / FIRE_BASE_RANGE_M) * FIRE_BASE_TOF_S))

          local impactCoord = target:GetCoordinate():Translate(
                                math.random(-bty.spread, bty.spread),
                                math.random(-bty.spread, bty.spread))

          TIMER:New(function()
            if not target:IsAlive() then return end
            local vec3 = impactCoord:GetVec3()
            local atype = (bty.ammoType or ""):lower()

            if atype == "illum" then
              local text = string.format("-illum-%d-%d",
                                         FIRE_ILLUM_HEIGHT_FT,
                                         FIRE_ILLUM_POWER_RADIUS)
              local id = math.random(100000, 999999)
              pcall(function()
                trigger.action.markToAll(id, text, vec3,
                                         coalition.side.BLUE, false, "Illum")
              end)
              TIMER:New(function()
                pcall(function() trigger.action.removeMark(id) end)
              end):Start(10)

            elseif atype == "smoke" then
              local text = "-smoke-r-5"
              local id = math.random(100000, 999999)
              pcall(function()
                trigger.action.markToAll(id, text, vec3,
                                         coalition.side.BLUE, false, "Smoke")
              end)
              TIMER:New(function()
                pcall(function() trigger.action.removeMark(id) end)
              end):Start(10)

            else
              pcall(function()
                trigger.action.explosion(vec3, bty.effectPower)
              end)
            end
          end):Start(tof)
        end
      end):Start(delay)
    end
  end
end


---------------------------------------------------------------------
-- F10 menu builder
---------------------------------------------------------------------

local function buildMenus()
  RootMenu = MENU_COALITION:New(coalition.side.BLUE, "Arty")

  MENU_COALITION_COMMAND:New(
    coalition.side.BLUE,
    "Arty Status",
    RootMenu,
    showAllArtyStatus
  )

  for i = 1, 6 do
    local bty   = Batteries[i]
    local label = string.format("Arty Bty %d (%s)", i, bty.type)
    local m     = MENU_COALITION:New(coalition.side.BLUE, label, RootMenu)
    bty.menuParent = m

    -----------------------------------------------------------------
    -- HQ control menu
    -----------------------------------------------------------------
    local deployMenu = MENU_COALITION:New(coalition.side.BLUE, "Deploy HQ", m)

    MENU_COALITION_COMMAND:New(coalition.side.BLUE, "Deploy", deployMenu, function() deployHQ(bty) end)
    MENU_COALITION_COMMAND:New(coalition.side.BLUE, "Remove", deployMenu, function() removeHQ(bty) end)

    -----------------------------------------------------------------
    -- Ammo status
    -----------------------------------------------------------------
    MENU_COALITION_COMMAND:New(coalition.side.BLUE, "Ammo Status", m, function() showAmmoStatus(bty) end)

    -----------------------------------------------------------------
    -- Ammo resupply menu (includes Battery Illum toggle)
    -----------------------------------------------------------------
    local resupplyMenu = MENU_COALITION:New(coalition.side.BLUE, "Ammo Resupply", m)

    MENU_COALITION_COMMAND:New(coalition.side.BLUE, "Request CH-47 Resupply", resupplyMenu, function() _resupplyByHelo(i) end)
    MENU_COALITION_COMMAND:New(coalition.side.BLUE, "Request C-130 Resupply", resupplyMenu, function() _resupplyByC130(i) end)
    MENU_COALITION_COMMAND:New(coalition.side.BLUE, "Deploy DZ Marker (Green Smoke)", resupplyMenu, function()
      if Batteries[i] and Batteries[i].hqCoord then
        _dropGreenSmoke(Batteries[i].hqCoord)
      else
        trigger.action.outText("Battery " .. i .. " HQ not deployed.", 6)
      end
    end)

    -----------------------------------------------------------------
    -- Battery illumination toggle
    -----------------------------------------------------------------
    BtyIllumEnabled = BtyIllumEnabled or false
    local BtyIllumMenuItem = nil
    local function updateBtyIllumLabel()
      local label = string.format("Bty Illum: %s", BtyIllumEnabled and "ON ✅" or "OFF ❌")
      if BtyIllumMenuItem then BtyIllumMenuItem:Remove() end
      BtyIllumMenuItem = MENU_COALITION_COMMAND:New(coalition.side.BLUE, label, resupplyMenu, function()
        BtyIllumEnabled = not BtyIllumEnabled
        trigger.action.outText("Battery Illumination " .. (BtyIllumEnabled and "ENABLED ✅" or "DISABLED ❌"), 6)
        updateBtyIllumLabel()
      end)
    end
    updateBtyIllumLabel()

    -----------------------------------------------------------------
    -- Fire mission and setup menus
    -----------------------------------------------------------------
    bty.fireMenu = MENU_COALITION:New(coalition.side.BLUE, "Fire Mission", m)

    MENU_COALITION_COMMAND:New(coalition.side.BLUE, "Scan for Red Targets", bty.fireMenu, function()
      trigger.action.outText(string.format("Arty: Scanning for red targets for Bty%d ...", bty.id), 4)
      local reds = findRedTargets()
      if #reds == 0 then notify("No red targets found within 30 km.") return end
      if bty.fireMenu then bty.fireMenu:Remove() end
      bty.fireMenu = MENU_COALITION:New(coalition.side.BLUE, "Fire Mission", m)
      for _, g in ipairs(reds) do
        MENU_COALITION_COMMAND:New(coalition.side.BLUE, g:GetName(), bty.fireMenu, function() fireMissionAtTarget(bty, g) end)
      end
      notify(string.format("Arty: %d red targets detected for Bty%d.", #reds, bty.id))
    end)

    -----------------------------------------------------------------
    -- Setup menus
    -----------------------------------------------------------------
    local setupMenu  = MENU_COALITION:New(coalition.side.BLUE, "Fire Mission Setup", m)
    local presetMenu = MENU_COALITION:New(coalition.side.BLUE, "Preset Setups", setupMenu)
    MENU_COALITION_COMMAND:New(coalition.side.BLUE, "Lgt Impact (5|HE|50|LOW)", presetMenu, function() applyPreset(bty, "Light", 5, "HE", 50, "LOW") end)
    MENU_COALITION_COMMAND:New(coalition.side.BLUE, "Std Impact (10|HE|100|MED)", presetMenu, function() applyPreset(bty, "Standard", 10, "HE", 100, "MED") end)
    MENU_COALITION_COMMAND:New(coalition.side.BLUE, "Hvy Impact (20|HE|200|HIGH)", presetMenu, function() applyPreset(bty, "Heavy", 20, "HE", 200, "HIGH") end)
    MENU_COALITION_COMMAND:New(coalition.side.BLUE, "MLRS Satur (20|Clu|200|HIGH)", presetMenu, function() applyPreset(bty, "MLRS Satur", 20, "Clu", 200, "HIGH") end)
    MENU_COALITION_COMMAND:New(coalition.side.BLUE, "Illumination (3|Illum|50|LOW)", presetMenu, function() applyPreset(bty, "Illumination", 3, "Illum", 50, "LOW") end)
    MENU_COALITION_COMMAND:New(coalition.side.BLUE, "Smoke Marker (1|Smoke|50|LOW)", presetMenu, function() applyPreset(bty, "Smoke", 1, "Smoke", 50, "LOW") end)

    local customMenu = MENU_COALITION:New(coalition.side.BLUE, "Custom Setup", setupMenu)
    local salvoMenu = MENU_COALITION:New(coalition.side.BLUE, "Salvo Size", customMenu)
    for _, n in ipairs({ 5, 10, 15, 20 }) do
      MENU_COALITION_COMMAND:New(coalition.side.BLUE, string.format("%d rds", n), salvoMenu, function() bty.salvo = n notify("Salvo set to " .. n) end)
    end

    local ammoMenu = MENU_COALITION:New(coalition.side.BLUE, "Ammo Type", customMenu)
    for _, t in ipairs({ "HE", "Smoke", "Illum", "Clu" }) do
      MENU_COALITION_COMMAND:New(coalition.side.BLUE, t, ammoMenu, function() bty.ammoType = t notify("Ammo set to " .. t) end)
    end

    local spreadMenu = MENU_COALITION:New(coalition.side.BLUE, "Spread", customMenu)
    for _, s in ipairs({ 50, 100, 150, 200 }) do
      MENU_COALITION_COMMAND:New(coalition.side.BLUE, s .. " m", spreadMenu, function() bty.spread = s notify("Spread set to " .. s .. " m") end)
    end

    local effectMenu = MENU_COALITION:New(coalition.side.BLUE, "Effect", customMenu)
    for _, e in ipairs({ "LOW", "MED", "HIGH" }) do
      MENU_COALITION_COMMAND:New(coalition.side.BLUE, e, effectMenu, function()
        bty.effect = e
        if e == "LOW" then bty.effectPower = FIRE_EXPLOSION_LOW_PWR
        elseif e == "MED" then bty.effectPower = FIRE_EXPLOSION_MED_PWR
        else bty.effectPower = FIRE_EXPLOSION_HIGH_PWR end
        notify("Effect set to " .. e)
      end)
    end

    MENU_COALITION_COMMAND:New(coalition.side.BLUE, "Show Current Setup", customMenu, function() showCurrentSetup(bty) end)

    -----------------------------------------------------------------
    -- ▪ Gun Timing Control Menu (per-battery)
    -----------------------------------------------------------------
    local timingMenu = MENU_COALITION:New(coalition.side.BLUE, "Gun Timing", m)

    -- ▪ Gun Firing Interval (delay between shots from the same gun)
    local fireIntervalMenu = MENU_COALITION:New(coalition.side.BLUE, "Gun Firing Interval", timingMenu)
    for _, t in ipairs({ 5, 10, 15, 20, 30 }) do
      MENU_COALITION_COMMAND:New(coalition.side.BLUE, string.format("%d sec", t), fireIntervalMenu, function()
        bty.gunFireInterval = t
        notify(string.format("Bty%d Gun Firing Interval set to %ds", bty.id, t))
      end)
    end

    -- ▪ Gun Stagger Delay (delay between each gun’s firing)
    local staggerDelayMenu = MENU_COALITION:New(coalition.side.BLUE, "Gun Stagger Delay", timingMenu)
    for _, s in ipairs({ 5, 10, 15, 20, 30 }) do
      MENU_COALITION_COMMAND:New(coalition.side.BLUE, string.format("%d sec", s), staggerDelayMenu, function()
        bty.gunStaggerDelay = s
        notify(string.format("Bty%d Gun Stagger Delay set to %ds", bty.id, s))
      end)
    end
  end
end



---------------------------------------------------------------------
-- 7. AMMO RESUPPLY SYSTEM
--     Handles CH-47 and C-130 ammunition resupply operations,
--     including optional illumination and visual effects.
---------------------------------------------------------------------

---------------------------------------------------------------------
-- HELPER FUNCTIONS
---------------------------------------------------------------------

-- Utility: adjust text for clearer TTS pronunciation
local function _fixTTS(text)
  if not text then return "" end
  -- Expand abbreviations for voice clarity
  text = text:gsub("%f[%a]HE%f[%A]", "H E")
  text = text:gsub("C%-130", "C one thirty")
  text = text:gsub("CH%-47", "C H forty seven")
  return text
end

-- Add ammunition to a battery
local function _addAmmoToBattery(i, amount)
  local b = Batteries[i]
  if not b then return end
  b.ammo = (b.ammo or 0) + amount
  local msg = string.format("Arty Bty %d resupplied +%d rounds (now %d)", i, amount, b.ammo)
  trigger.action.outText(msg, 8)
  _srsSayARTY(_fixTTS(msg), true)
  if saveState then pcall(saveState) end
end

-- Drop green smoke marker at a coordinate
local function _dropGreenSmoke(coord)
  if not coord then return end
  local v = coord:GetVec3()
  v.y = (coord:GetLandHeight() or v.y) + 1
  trigger.action.smoke(v, SMOKE_COLOR)
end

-- Drop white signal flares above a coordinate
local function _dropWhiteFlares(coord, count)
  if not coord then return end
  count = count or 2
  for j = 1, count do
    local p = coord:GetVec3()
    p.y = (coord:GetLandHeight() or p.y) + 300
    TIMER:New(function()
      trigger.action.signalFlare(p, FLARE_COLOR, 1)
    end):Start((j - 1) * 0.4)
  end
end


---------------------------------------------------------------------
-- CH-47 LANDING RESUPPLY
---------------------------------------------------------------------
function _resupplyByHelo(i)
  local b = Batteries[i]
  if not b or not b.hqCoord then
    local msg = "Arty Bty " .. i .. " has no HQ deployed."
    trigger.action.outText(msg, 8)
    _srsSayARTY(_fixTTS(msg), true)
    return
  end

  local tpl = HELO_TPL_PREFIX .. tostring(i)
  local spn = SPAWN:New(tpl):InitUnControlled(true)
  local g = spn:Spawn()
  if not g then
    local msg = "CH-47 spawn failed for Battery " .. i
    trigger.action.outText(msg, 6)
    _srsSayARTY(_fixTTS(msg), true)
    return
  end

  local msg = "CH-47 resupply en route to Battery " .. i
  trigger.action.outText(msg, 6)
  _srsSayARTY(_fixTTS(msg), true)

  -- Optional illumination trigger
  if BtyIllumEnabled then
    local watch = SCHEDULER:New(nil, function()
      if not g:IsAlive() then watch:Stop() return end
      local u = g:GetUnit(1)
      if not u or not u:IsAlive() then return end
      if u:GetCoordinate():Get2DDistance(b.hqCoord) <= RESUPPLY_HELO_ILLUM_TRIGGER_M then
        _createBatteryIllumMarker(b)
        watch:Stop()
      end
    end, {}, 3, 3)
  end

  TIMER:New(function()
    g:StartUncontrolled()
    local lz = b.hqCoord:Translate(HELO_LZ_OFFSET_M, 90)
    local v2 = lz:GetVec2()
    g:RouteToVec2(v2, 100, "Vee")

    local approach
    approach = SCHEDULER:New(nil, function()
      if not g:IsAlive() then approach:Stop(); return end
      if g:GetCoordinate():Get2DDistance(lz) < HELO_APPROACH_SLOW_M then
        approach:Stop()
        _dropGreenSmoke(lz)
        _dropWhiteFlares(lz, 2)

        local DG = g:GetDCSObject()
        if DG and DG.getController then
          local ctrl = DG:getController()
          if ctrl then
            pcall(function()
              ctrl:pushTask({
                id = 'Land',
                params = { point = { x = v2.x, y = v2.y }, duration = 90 }
              })
            end)
          end
        end

        TIMER:New(function()
          _addAmmoToBattery(i, HELO_RESUPPLY_AMOUNT)
          local msg = "CH-47 resupply complete (RTB)"
          trigger.action.outText(msg, 6)
          _srsSayARTY(_fixTTS(msg), true)
          local base = AIRBASE:FindByName(HELO_RTB_AIRBASE)
          if base and g:IsAlive() then
            g:RouteToVec2(base:GetCoordinate():GetVec2(), 100, "Vee")
            TIMER:New(function() if g:IsAlive() then g:Destroy() end end):Start(90)
          end
        end):Start(90)
      end
    end, {}, 3, 3)
  end):Start(10)
end


---------------------------------------------------------------------
-- C-130 AIRDROP RESUPPLY
---------------------------------------------------------------------
function _resupplyByC130(i)
  local b = Batteries[i]
  if not b or not b.hqCoord then
    local msg = "Arty Bty " .. i .. " has no HQ deployed."
    trigger.action.outText(msg, 8)
    _srsSayARTY(_fixTTS(msg), true)
    return
  end

  local tpl = C130_TPL_PREFIX .. tostring(i)
  local spn = SPAWN:New(tpl)
  local g = spn:Spawn()
  if not g then
    local msg = "C-130 spawn failed for Battery " .. i
    trigger.action.outText(msg, 6)
    _srsSayARTY(_fixTTS(msg), true)
    return
  end

  local msg = "C-130 resupply en route to Battery " .. i
  trigger.action.outText(msg, 6)
  _srsSayARTY(_fixTTS(msg), true)
  g:RouteToVec2(b.hqCoord:GetVec2(), 270, "OnRoad")

  -- Optional illumination trigger
  if BtyIllumEnabled then
    local watch = SCHEDULER:New(nil, function()
      if not g:IsAlive() then watch:Stop() return end
      local u = g:GetUnit(1)
      if not u or not u:IsAlive() then return end
      if u:GetCoordinate():Get2DDistance(b.hqCoord) <= RESUPPLY_C130_ILLUM_TRIGGER_M then
        _createBatteryIllumMarker(b)
        watch:Stop()
      end
    end, {}, 3, 3)
  end

  -- Flare and drop sequence
  local dropped5, dropped2, dropped1, droppedFinal = false, false, false, false

  local watch
  watch = SCHEDULER:New(nil, function()
    if not g or not g:IsAlive() then if watch then watch:Stop() end return end
    local u = g:GetUnit(1)
    if not u or not u:IsAlive() then return end
    local here = u:GetCoordinate()
    local d = here:Get2DDistance(b.hqCoord)

    if d <= C130_SMOKE_LEAD_M then _dropGreenSmoke(b.hqCoord) end
    if d <= C130_FLARE_5KM_M   and not dropped5 then dropped5 = true; _dropWhiteFlares(b.hqCoord, 1) end
    if d <= C130_FLARE_2_5KM_M and not dropped2 then dropped2 = true; _dropWhiteFlares(b.hqCoord, 1) end
    if d <= C130_FLARE_1KM_M   and not dropped1 then dropped1 = true; _dropWhiteFlares(b.hqCoord, 1) end

    if d <= C130_PROX_TRIGGER_M and not droppedFinal then
      droppedFinal = true
      _dropWhiteFlares(b.hqCoord, 2)
      _addAmmoToBattery(i, C130_RESUPPLY_AMOUNT)
      local msg = "C-130 drop complete (RTB)"
      trigger.action.outText(msg, 6)
      _srsSayARTY(_fixTTS(msg), true)
      local base = AIRBASE:FindByName(C130_RTB_AIRBASE)
      if base then g:RouteToVec2(base:GetCoordinate():GetVec2(), 270, "OnRoad") end
      TIMER:New(function() if g:IsAlive() then g:Destroy() end end):Start(90)
      if watch then watch:Stop() end
    end
  end, {}, 3, 3)
end





---------------------------------------------------------------------
-- 8. BATTERY MAP MARKER SYSTEM  
---------------------------------------------------------------------

---------------------------------------------------------------------
-- Helpers
---------------------------------------------------------------------
local function _batteryMarkerText(bty)
  if not bty then return "Unknown" end
  local guns  = countMatchingGunsNearPoint(bty.type, bty.hqCoord, 300)
  local req   = HQ_REQ_GUNS or 2
  local active = (guns >= req)
  local state = active and "ACTIVE" or "INACTIVE"
  local ammo  = bty.ammo or 0
  return string.format("Bty%-2d     %s     Ammo: %-3d", bty.id, state, ammo)
end

local function _ensureBatteryMarker(bty)
  if not (BATTERY_MARKERS_ENABLED and bty and bty.hqCoord) then return end

  -- decide stable bearing per battery if random enabled
  if BATTERY_MARKER_RANDOM_BEARING and not bty.markerBrg then
    bty.markerBrg = math.random(0, 359)
  end
  local brg  = (BATTERY_MARKER_RANDOM_BEARING and bty.markerBrg)
               or BATTERY_MARKER_OFFSET_BRG or 90
  local offM = BATTERY_MARKER_OFFSET_M or 500

  local mcoord = bty.hqCoord:Translate(offM, brg)
  local v3     = mcoord:GetVec3()

  -- remove any existing mark first
  if bty.markerId then pcall(trigger.action.removeMark, bty.markerId) end
  _markerNextId = _markerNextId + 1
  bty.markerId = _markerNextId

  pcall(function()
    trigger.action.markToCoalition(
      bty.markerId,
      _batteryMarkerText(bty),
      v3,
      coalition.side.BLUE,
      true -- visible to blue, recreated each update
    )
  end)
end

local function _updateBatteryMarker(bty) _ensureBatteryMarker(bty) end

local function _removeBatteryMarker(bty)
  if bty and bty.markerId then
    pcall(trigger.action.removeMark, bty.markerId)
    bty.markerId = nil
  end
end

---------------------------------------------------------------------
-- Periodic Update Loop
---------------------------------------------------------------------
local function _refreshBatteryMarkers()
  for _, bty in ipairs(Batteries or {}) do
    if bty.hqCoord then
      _updateBatteryMarker(bty)
    elseif bty.markerId then
      _removeBatteryMarker(bty)
    end
  end
  return timer.getTime() + BATTERY_MARKER_UPDATE_S
end

timer.scheduleFunction(_refreshBatteryMarkers, {}, timer.getTime() + BATTERY_MARKER_UPDATE_S)

---------------------------------------------------------------------
--  Hooks into HQ Deploy / Remove
---------------------------------------------------------------------
local _oldDeployHQ = deployHQ
function deployHQ(bty)
  _oldDeployHQ(bty)
  if bty and bty.hqCoord then _ensureBatteryMarker(bty) end
end

local _oldRemoveHQ = removeHQ
function removeHQ(bty)
  _oldRemoveHQ(bty)
  _removeBatteryMarker(bty)
end

---------------------------------------------------------------------
-- 🪄 Initial pass for already-deployed HQs (mission load)
---------------------------------------------------------------------
timer.scheduleFunction(function()
  for _, bty in ipairs(Batteries or {}) do
    if bty.hqCoord then _ensureBatteryMarker(bty) end
  end
end, {}, timer.getTime() + 5)


---------------------------------------------------------------------
-- 8. PERSISTENCE SYSTEM
---------------------------------------------------------------------
local function _ensureDir(path)
  local dir = path:match("^(.*)[/\\][^/\\]+$")
  if not dir then return end
  local seg = ""
  for part in dir:gmatch("[^/\\]+") do
    seg = (seg == "" and part) or (seg .. "/" .. part)
    if lfs and lfs.mkdir then pcall(lfs.mkdir, seg) end
  end
end

local function _serialize(v, indent)
  indent = indent or ""
  local t = type(v)
  if t == "number" or t == "boolean" then return tostring(v)
  elseif t == "string" then return string.format("%q", v)
  elseif t == "table" then
    local ni = indent .. "  "
    local out = { "{\n" }
    for k, val in pairs(v) do
      local key = (type(k) == "string" and k:match("^%a[%w_]*$"))
        and k or "[" .. _serialize(k, ni) .. "]"
      out[#out + 1] = ni .. key .. " = " .. _serialize(val, ni) .. ",\n"
    end
    out[#out + 1] = indent .. "}"
    return table.concat(out)
  end
  return "nil"
end

-- ▪ Save current artillery state
function saveState()
  if not io or not io.open then
    notify("Persistence disabled (I/O sandboxed).")
    return
  end
  _ensureDir(PERSIST_FILE)
  local data = { batteries = {} }
  for i = 1, 6 do
    local b = Batteries[i]
    local rec = {
      id = i,
      type = b.type,
      salvo = b.salvo,
      ammo = b.ammoType,
      spread = b.spread,
      effect = b.effect,
      ammoCount = b.ammo
    }
    if b.hqCoord then
      local v = b.hqCoord:GetVec3()
      rec.hq = { x = v.x, y = v.y, z = v.z }
    end
    table.insert(data.batteries, rec)
  end
  local f = io.open(PERSIST_FILE, "w")
  if f then
    f:write("return ", _serialize(data), "\n")
    f:close()
  end
end

-- ▪ Load artillery state
function loadState()
  if not io or not io.open then return end
  local f = io.open(PERSIST_FILE, "r")
  if not f then return end
  local chunk = f:read("*a")
  f:close()
  local func, err = loadstring(chunk)
  if not func then env.info("Arty load error: " .. tostring(err)) return end
  local ok, res = pcall(func)
  if not ok or type(res) ~= "table" then return end

  for _, rec in ipairs(res.batteries or {}) do
    local b = Batteries[rec.id]
    if b then
      b.salvo = rec.salvo or 10
      b.ammoType = rec.ammo or "HE"
      b.spread = rec.spread or 100
      b.effect = rec.effect or "MED"
      b.ammo = rec.ammoCount or 40
      if b.effect == "LOW" then
        b.effectPower = FIRE_EXPLOSION_LOW_PWR
      elseif b.effect == "MED" then
        b.effectPower = FIRE_EXPLOSION_MED_PWR
      else
        b.effectPower = FIRE_EXPLOSION_HIGH_PWR
      end
      if rec.hq and rec.hq.x then
        local c = COORDINATE:New(rec.hq.x, rec.hq.y, rec.hq.z)
        b.hqCoord = c
        b.hqNames = spawnHQStatics(c, b.id)
      end
    end
  end
end

---------------------------------------------------------------------
--  AUTO STATUS MONITOR 
---------------------------------------------------------------------
local STATUS_CHECK_INTERVAL = 10
local LAST_STATUS = {}

local function monitorBatteryStatus()
  for _, bty in ipairs(Batteries) do
    if bty.hqCoord then
      local coord = bty.hqCoord
      local cnt = 0

      local setG = SET_GROUP:New():FilterCoalitions("blue")
          :FilterActive(true):FilterStart()

      setG:ForEachGroup(function(g)
        for _, u in pairs(g:GetUnits() or {}) do
          if u and u:IsAlive() then
            local tn = (u:GetTypeName() or ""):lower()
            local dcsObj = u:GetDCSObject()
            local attrs = (dcsObj and dcsObj:getDesc() or {}).attributes or {}
            local has = function(k) return attrs[k] == true or attrs[k] == 1 end
            local contains = function(s) return tn:find(s, 1, true) ~= nil end
            local match = false

            if bty.type == "SPG" then
              if not has("MLRS") then
                match = has("Artillery") or contains("m109") or contains("paladin")
                  or contains("as90") or contains("firtina")
              end
            elseif bty.type == "MLRS" then
              match = has("MLRS") or contains("mlrs") or contains("m270")
                or contains("bm-21") or contains("grad")
            elseif bty.type == "FDGN" or bty.type == "FIELDGUN" then
              if not has("MLRS") then
                match = has("Artillery") or contains("m777") or contains("howitzer")
                  or contains("d-30") or contains("field gun")
              end
            end

            if match and coord:Get2DDistance(u:GetCoordinate()) <= 300 then
              cnt = cnt + 1
            end
          end
        end
      end)

      local req = HQ_REQ_GUNS or 2
      local active = (cnt >= req)
      local prev = LAST_STATUS[bty.id]

      -- Only announce if something actually changed
      if not prev or prev.active ~= active or prev.count ~= cnt then
        local msg
        if active then
          msg = string.format(
            "Battery %d (%s): ACTIVE – %d/%d guns detected",
            bty.id, bty.type, cnt, req
          )
        else
          msg = string.format(
            "Battery %d (%s): INACTIVE – %d/%d guns detected",
            bty.id, bty.type, cnt, req
          )
        end

        trigger.action.outText(msg, 8)
        LAST_STATUS[bty.id] = { active = active, count = cnt }
      end
    end
  end
  return timer.getTime() + STATUS_CHECK_INTERVAL
end

timer.scheduleFunction(monitorBatteryStatus, {}, timer.getTime() + STATUS_CHECK_INTERVAL)



---------------------------------------------------------------------
-- 9. INITIALIZATION
---------------------------------------------------------------------
timer.scheduleFunction(function()
  if not SET_GROUP or not MENU_COALITION then
    env.info("MOOSE not ready yet – retrying Arty init.")
    return timer.getTime() + 1
  end

  for i = 1, 6 do
    Batteries[i] = Batteries[i] or {}
    Batteries[i].id = i
    Batteries[i].type = BATTERY_TYPES[i]
    Batteries[i].ammo = Batteries[i].ammo or 40
    Batteries[i].salvo = Batteries[i].salvo or 10
    Batteries[i].ammoType = Batteries[i].ammoType or "H E"
    Batteries[i].spread = Batteries[i].spread or 100
    Batteries[i].effect = Batteries[i].effect or "MED"
    Batteries[i].effectPower = FIRE_EXPLOSION_MED_PWR
  end

  buildMenus()

  timer.scheduleFunction(function()
    loadState()
  end, {}, timer.getTime() + 5)

  notify("Arty_v3.0.")
  return nil
end, {}, timer.getTime() + 1)


---------------------------------------------------------------------
-- END OF SCRIPT
-- Artillery Command System v3.0
-- Last Updated: 01 November 2025 – 1507 hrs GMT
---------------------------------------------------------------------


