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 541616f..c162674 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/Moose_DynamicGroundBattle/Moose_DynamicGroundBattle_Plugin.lua b/Moose_DynamicGroundBattle/Moose_DynamicGroundBattle_Plugin.lua index 0948182..5f55593 100644 --- a/Moose_DynamicGroundBattle/Moose_DynamicGroundBattle_Plugin.lua +++ b/Moose_DynamicGroundBattle/Moose_DynamicGroundBattle_Plugin.lua @@ -50,14 +50,17 @@ RED SIDE: - Infantry Templates: RedInfantry1, RedInfantry2, RedInfantry3, RedInfantry4, RedInfantry5, RedInfantry6 - Armor Templates: RedArmor1, RedArmor2, RedArmor3, RedArmor4, RedArmor5, RedArmor6 + - Spawn Groups: Names defined by RED_INFANTRY_SPAWN_GROUP and RED_ARMOR_SPAWN_GROUP variables (default: RedInfantryGroup, RedArmorGroup) - Warehouses (Static Objects): RedWarehouse1-1, RedWarehouse2-1, RedWarehouse3-1, etc. BLUE SIDE: - Infantry Templates: BlueInfantry1, BlueInfantry2, BlueInfantry3, BlueInfantry4, BlueInfantry5, BlueInfantry6 - Armor Templates: BlueArmor1, BlueArmor2, BlueArmor3, BlueArmor4, BlueArmor5 + - Spawn Groups: Names defined by BLUE_INFANTRY_SPAWN_GROUP and BLUE_ARMOR_SPAWN_GROUP variables (default: BlueInfantryGroup, BlueArmorGroup) - Warehouses (Static Objects): BlueWarehouse1-1, BlueWarehouse2-1, BlueWarehouse3-1, etc. NOTE: Warehouse names use the static "Unit Name" in mission editor, not the "Name" field! + NOTE: Spawn groups should be simple groups set to LATE ACTIVATE. You can customize their names in the USER CONFIGURATION section. Integration with DualCoalitionZoneCapture: - This script reads zoneCaptureObjects and zoneNames from DualCoalitionZoneCapture @@ -78,24 +81,35 @@ local UPDATE_MARK_POINTS_SCHED = 300 -- Update warehouse markers every 300 se local MAX_WAREHOUSE_UNIT_LIST_DISTANCE = 5000 -- Max distance to search for units near warehouses for markers -- Spawn Frequency and Limits -local INIT_RED_INFANTRY = 5 -- Initial number of Red Infantry groups +-- Red Side Settings +local INIT_RED_INFANTRY = 25 -- Initial number of Red Infantry groups local MAX_RED_INFANTRY = 100 -- Maximum number of Red Infantry groups local SPAWN_SCHED_RED_INFANTRY = 1800 -- Base spawn frequency for Red Infantry (seconds) local INIT_RED_ARMOR = 25 -- Initial number of Red Armor groups -local MAX_RED_ARMOR = 200 -- Maximum 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 INIT_BLUE_INFANTRY = 5 -- Initial number of Blue Infantry groups +-- Blue Side Settings +local INIT_BLUE_INFANTRY = 25 -- Initial number of Blue Infantry groups local MAX_BLUE_INFANTRY = 100 -- Maximum number of Blue Infantry groups local SPAWN_SCHED_BLUE_INFANTRY = 1800 -- Base spawn frequency for Blue Infantry (seconds) local INIT_BLUE_ARMOR = 25 -- Initial number of Blue Armor groups -local MAX_BLUE_ARMOR = 200 -- Maximum number of Blue Armor groups +local MAX_BLUE_ARMOR = 500 -- Maximum number of Blue Armor groups local SPAWN_SCHED_BLUE_ARMOR = 300 -- Base spawn frequency for Blue Armor (seconds) local ASSIGN_TASKS_SCHED = 600 -- How often to reassign tasks to idle groups (seconds) +-- Per-side cadence scalars (tune to make one side faster/slower without touching base frequencies) +local RED_INFANTRY_CADENCE_SCALAR = 1.0 +local RED_ARMOR_CADENCE_SCALAR = 1.0 +local BLUE_INFANTRY_CADENCE_SCALAR = 1.0 +local BLUE_ARMOR_CADENCE_SCALAR = 1.0 + +-- When a side loses every warehouse we pause spawning and re-check after this delay +local NO_WAREHOUSE_RECHECK_DELAY = 180 + -- Define warehouses for each side local redWarehouses = { STATIC:FindByName("RedWarehouse1-1"), @@ -151,6 +165,12 @@ local blueArmorTemplates = { "BlueArmor5" } +-- Spawn Group Names (these are the base groups SPAWN:New() uses for spawning) +local RED_INFANTRY_SPAWN_GROUP = "RedInfantryGroup" +local RED_ARMOR_SPAWN_GROUP = "RedArmorGroup" +local BLUE_INFANTRY_SPAWN_GROUP = "BlueInfantryGroup" +local BLUE_ARMOR_SPAWN_GROUP = "BlueArmorGroup" + ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- DO NOT EDIT BELOW THIS LINE ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -164,6 +184,81 @@ if not zoneCaptureObjects or not zoneNames then return end +-- Validate warehouses exist +local function ValidateWarehouses(warehouses, label) + local foundCount = 0 + local missingCount = 0 + + for i, wh in ipairs(warehouses) do + if wh then + foundCount = foundCount + 1 + env.info(string.format("[DGB PLUGIN] %s warehouse %d: %s (OK)", label, i, wh:GetName())) + else + missingCount = missingCount + 1 + env.warning(string.format("[DGB PLUGIN] %s warehouse at index %d NOT FOUND in mission editor!", label, i)) + end + end + + env.info(string.format("[DGB PLUGIN] %s warehouses: %d found, %d missing", label, foundCount, missingCount)) + return foundCount > 0 +end + +-- Validate unit templates exist +local function ValidateTemplates(templates, label) + local foundCount = 0 + local missingCount = 0 + + for i, templateName in ipairs(templates) do + local group = GROUP:FindByName(templateName) + if group then + foundCount = foundCount + 1 + env.info(string.format("[DGB PLUGIN] %s template %d: %s (OK)", label, i, templateName)) + else + missingCount = missingCount + 1 + env.warning(string.format("[DGB PLUGIN] %s template '%s' NOT FOUND in mission editor!", label, templateName)) + end + end + + env.info(string.format("[DGB PLUGIN] %s templates: %d found, %d missing", label, foundCount, missingCount)) + return foundCount > 0 +end + +env.info("[DGB PLUGIN] Validating configuration...") + +-- Validate all warehouses +local redWarehousesValid = ValidateWarehouses(redWarehouses, "Red") +local blueWarehousesValid = ValidateWarehouses(blueWarehouses, "Blue") + +if not redWarehousesValid then + env.warning("[DGB PLUGIN] WARNING: No valid Red warehouses found! Red spawning will be disabled.") +end + +if not blueWarehousesValid then + env.warning("[DGB PLUGIN] WARNING: No valid Blue warehouses found! Blue spawning will be disabled.") +end + +-- Validate all templates +local redInfantryValid = ValidateTemplates(redInfantryTemplates, "Red Infantry") +local redArmorValid = ValidateTemplates(redArmorTemplates, "Red Armor") +local blueInfantryValid = ValidateTemplates(blueInfantryTemplates, "Blue Infantry") +local blueArmorValid = ValidateTemplates(blueArmorTemplates, "Blue Armor") + +if not redInfantryValid then + env.warning("[DGB PLUGIN] WARNING: No valid Red Infantry templates found! Red Infantry spawning will fail.") +end + +if not redArmorValid then + env.warning("[DGB PLUGIN] WARNING: No valid Red Armor templates found! Red Armor spawning will fail.") +end + +if not blueInfantryValid then + env.warning("[DGB PLUGIN] WARNING: No valid Blue Infantry templates found! Blue Infantry spawning will fail.") +end + +if not blueArmorValid then + env.warning("[DGB PLUGIN] WARNING: No valid Blue Armor templates found! Blue Armor spawning will fail.") +end + env.info("[DGB PLUGIN] Found " .. #zoneCaptureObjects .. " zones from DualCoalitionZoneCapture") -- Track active markers to prevent memory leaks @@ -196,41 +291,44 @@ local function GetZonesByCoalition(targetCoalition) return zones end --- Function to calculate spawn frequency based on warehouse survival -local function CalculateSpawnFrequency(warehouses, baseFrequency) - local totalWarehouses = #warehouses - local aliveWarehouses = 0 +-- Helper to count warehouse availability +local function GetWarehouseStats(warehouses) + local alive = 0 + local total = 0 for _, warehouse in ipairs(warehouses) do if warehouse then + total = total + 1 local life = warehouse:GetLife() if life and life > 0 then - aliveWarehouses = aliveWarehouses + 1 + alive = alive + 1 end end end - if totalWarehouses == 0 or aliveWarehouses == 0 then - return math.huge -- Stop spawning if no warehouses remain + return alive, total +end + +-- Function to calculate spawn frequency based on warehouse survival +local function CalculateSpawnFrequency(warehouses, baseFrequency, cadenceScalar) + local aliveWarehouses, totalWarehouses = GetWarehouseStats(warehouses) + cadenceScalar = cadenceScalar or 1 + + if totalWarehouses == 0 then + return baseFrequency * cadenceScalar end - local frequency = baseFrequency * (totalWarehouses / aliveWarehouses) + if aliveWarehouses == 0 then + return nil -- Pause spawning until logistics return + end + + local frequency = baseFrequency * cadenceScalar * (totalWarehouses / aliveWarehouses) return frequency end -- Function to calculate spawn frequency as a percentage local function CalculateSpawnFrequencyPercentage(warehouses) - local totalWarehouses = #warehouses - local aliveWarehouses = 0 - - for _, warehouse in ipairs(warehouses) do - if warehouse then - local life = warehouse:GetLife() - if life and life > 0 then - aliveWarehouses = aliveWarehouses + 1 - end - end - end + local aliveWarehouses, totalWarehouses = GetWarehouseStats(warehouses) if totalWarehouses == 0 then return 0 @@ -297,8 +395,7 @@ local function IsInfantryGroup(group) return false end --- Function to assign tasks to a group -local function AssignTasks(group) +local function AssignTasks(group, currentZoneCapture) if not group or not group.GetCoalition or not group.GetCoordinate or not group.GetVelocityVec3 then return end @@ -312,6 +409,19 @@ local function AssignTasks(group) local groupCoalition = group:GetCoalition() local groupCoordinate = group:GetCoordinate() + local currentZone = currentZoneCapture and currentZoneCapture:GetZone() or nil + + -- If the group is sitting inside a friendly zone that is currently under attack, + -- keep them local so they fight for the objective instead of leaving it exposed. + if currentZoneCapture and currentZone and currentZoneCapture.GetCoalition and currentZoneCapture:GetCoalition() == groupCoalition then + local zoneState = currentZoneCapture.GetCurrentState and currentZoneCapture:GetCurrentState() or nil + if zoneState == "Attacked" then + env.info(string.format("[DGB PLUGIN] %s defending contested zone %s", group:GetName(), currentZone:GetName())) + group:PatrolZones({ currentZone }, 20, "Cone", 30, 60) + return + end + end + local closestZone = nil local closestDistance = math.huge @@ -350,12 +460,14 @@ local function AssignTasksToGroups() -- Check if group is in a friendly zone local groupCoalition = group:GetCoalition() local inFriendlyZone = false + local currentZoneCapture = nil for idx, zoneCapture in ipairs(zoneCaptureObjects) do if zoneCapture:GetCoalition() == groupCoalition then local zone = zoneCapture:GetZone() if zone and group:IsCompletelyInZone(zone) then inFriendlyZone = true + currentZoneCapture = zoneCapture break end end @@ -367,7 +479,7 @@ local function AssignTasksToGroups() return end - AssignTasks(group) + AssignTasks(group, currentZoneCapture) tasksAssigned = tasksAssigned + 1 end end @@ -378,20 +490,8 @@ end -- Function to monitor and announce warehouse status local function MonitorWarehouses() - local blueWarehousesAlive = 0 - local redWarehousesAlive = 0 - - for _, warehouse in ipairs(blueWarehouses) do - if warehouse and warehouse:IsAlive() then - blueWarehousesAlive = blueWarehousesAlive + 1 - end - end - - for _, warehouse in ipairs(redWarehouses) do - if warehouse and warehouse:IsAlive() then - redWarehousesAlive = redWarehousesAlive + 1 - end - end + local blueWarehousesAlive, blueWarehouseTotal = GetWarehouseStats(blueWarehouses) + local redWarehousesAlive, redWarehouseTotal = GetWarehouseStats(redWarehouses) local redSpawnFrequencyPercentage = CalculateSpawnFrequencyPercentage(redWarehouses) local blueSpawnFrequencyPercentage = CalculateSpawnFrequencyPercentage(blueWarehouses) @@ -402,8 +502,8 @@ local function MonitorWarehouses() MESSAGE:New(msg, 30):ToAll() env.info(string.format("[DGB PLUGIN] Warehouse status - Red: %d/%d (%d%%), Blue: %d/%d (%d%%)", - redWarehousesAlive, #redWarehouses, redSpawnFrequencyPercentage, - blueWarehousesAlive, #blueWarehouses, blueSpawnFrequencyPercentage)) + redWarehousesAlive, redWarehouseTotal, redSpawnFrequencyPercentage, + blueWarehousesAlive, blueWarehouseTotal, blueSpawnFrequencyPercentage)) end ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -414,12 +514,6 @@ end local redZones = GetZonesByCoalition(coalition.side.RED) local blueZones = GetZonesByCoalition(coalition.side.BLUE) --- Calculate initial spawn frequencies -local redInfantrySpawnFrequency = CalculateSpawnFrequency(redWarehouses, SPAWN_SCHED_RED_INFANTRY) -local redArmorSpawnFrequency = CalculateSpawnFrequency(redWarehouses, SPAWN_SCHED_RED_ARMOR) -local blueInfantrySpawnFrequency = CalculateSpawnFrequency(blueWarehouses, SPAWN_SCHED_BLUE_INFANTRY) -local blueArmorSpawnFrequency = CalculateSpawnFrequency(blueWarehouses, SPAWN_SCHED_BLUE_ARMOR) - -- Calculate and display initial spawn frequency percentages local redSpawnFrequencyPercentage = CalculateSpawnFrequencyPercentage(redWarehouses) local blueSpawnFrequencyPercentage = CalculateSpawnFrequencyPercentage(blueWarehouses) @@ -440,46 +534,85 @@ local function GetBlueZones() return GetZonesByCoalition(coalition.side.BLUE) end +-- Validate spawn groups exist before creating spawners +local spawnGroups = { + {name = RED_INFANTRY_SPAWN_GROUP, label = "Red Infantry Spawn Group"}, + {name = RED_ARMOR_SPAWN_GROUP, label = "Red Armor Spawn Group"}, + {name = BLUE_INFANTRY_SPAWN_GROUP, label = "Blue Infantry Spawn Group"}, + {name = BLUE_ARMOR_SPAWN_GROUP, label = "Blue Armor Spawn Group"} +} + +for _, spawnGroup in ipairs(spawnGroups) do + local group = GROUP:FindByName(spawnGroup.name) + if group then + env.info(string.format("[DGB PLUGIN] %s '%s' found (OK)", spawnGroup.label, spawnGroup.name)) + else + env.error(string.format("[DGB PLUGIN] ERROR: %s '%s' NOT FOUND! Create this group in mission editor as LATE ACTIVATE.", spawnGroup.label, spawnGroup.name)) + end +end + -- Red Infantry Spawner -redInfantrySpawn = SPAWN:New("RedInfantryGroup") +redInfantrySpawn = SPAWN:New(RED_INFANTRY_SPAWN_GROUP) :InitRandomizeTemplate(redInfantryTemplates) :InitLimit(INIT_RED_INFANTRY, MAX_RED_INFANTRY) -- Red Armor Spawner -redArmorSpawn = SPAWN:New("RedArmorGroup") +redArmorSpawn = SPAWN:New(RED_ARMOR_SPAWN_GROUP) :InitRandomizeTemplate(redArmorTemplates) :InitLimit(INIT_RED_ARMOR, MAX_RED_ARMOR) -- Blue Infantry Spawner -blueInfantrySpawn = SPAWN:New("BlueInfantryGroup") +blueInfantrySpawn = SPAWN:New(BLUE_INFANTRY_SPAWN_GROUP) :InitRandomizeTemplate(blueInfantryTemplates) :InitLimit(INIT_BLUE_INFANTRY, MAX_BLUE_INFANTRY) -- Blue Armor Spawner -blueArmorSpawn = SPAWN:New("BlueArmorGroup") +blueArmorSpawn = SPAWN:New(BLUE_ARMOR_SPAWN_GROUP) :InitRandomizeTemplate(blueArmorTemplates) :InitLimit(INIT_BLUE_ARMOR, MAX_BLUE_ARMOR) --- Custom spawn function that updates zones dynamically -local function SpawnWithDynamicZones() - local currentRedZones = GetRedZones() - local currentBlueZones = GetBlueZones() - - if #currentRedZones > 0 then - local randomRedZone = currentRedZones[math.random(#currentRedZones)] - redInfantrySpawn:SpawnInZone(randomRedZone, false) - redArmorSpawn:SpawnInZone(randomRedZone, false) - end - - if #currentBlueZones > 0 then - local randomBlueZone = currentBlueZones[math.random(#currentBlueZones)] - blueInfantrySpawn:SpawnInZone(randomBlueZone, false) - blueArmorSpawn:SpawnInZone(randomBlueZone, false) +-- Helper to schedule spawns per category so each uses its intended cadence. +local function ScheduleSpawner(spawnObject, getZonesFn, warehouses, baseFrequency, label, cadenceScalar) + local scheduler + + local function spawnCycle() + local nextInterval = CalculateSpawnFrequency(warehouses, baseFrequency, cadenceScalar) + + if not nextInterval then + env.info(string.format("[DGB PLUGIN] %s spawn paused (no warehouses alive)", label)) + if scheduler then + scheduler:Stop() + scheduler:Start(NO_WAREHOUSE_RECHECK_DELAY, NO_WAREHOUSE_RECHECK_DELAY) + end + return + end + + local friendlyZones = getZonesFn() + local zonesAvailable = #friendlyZones + + if zonesAvailable > 0 then + local chosenZone = friendlyZones[math.random(zonesAvailable)] + spawnObject:SpawnInZone(chosenZone, false) + else + env.info(string.format("[DGB PLUGIN] %s spawn skipped (no friendly zones)", label)) + end + + if scheduler then + scheduler:Stop() + scheduler:Start(nextInterval, nextInterval) + end end + + local initialFrequency = baseFrequency * (cadenceScalar or 1) + scheduler = SCHEDULER:New(nil, spawnCycle, {}, math.random(5, 15), initialFrequency) + return scheduler end --- Schedule spawns -SCHEDULER:New(nil, SpawnWithDynamicZones, {}, 10, math.max(SPAWN_SCHED_RED_INFANTRY, SPAWN_SCHED_BLUE_INFANTRY)) +-- Schedule spawns (each spawner now runs at its own configured cadence) +ScheduleSpawner(redInfantrySpawn, GetRedZones, redWarehouses, SPAWN_SCHED_RED_INFANTRY, "Red Infantry", RED_INFANTRY_CADENCE_SCALAR) +ScheduleSpawner(redArmorSpawn, GetRedZones, redWarehouses, SPAWN_SCHED_RED_ARMOR, "Red Armor", RED_ARMOR_CADENCE_SCALAR) +ScheduleSpawner(blueInfantrySpawn, GetBlueZones, blueWarehouses, SPAWN_SCHED_BLUE_INFANTRY, "Blue Infantry", BLUE_INFANTRY_CADENCE_SCALAR) +ScheduleSpawner(blueArmorSpawn, GetBlueZones, blueWarehouses, SPAWN_SCHED_BLUE_ARMOR, "Blue Armor", BLUE_ARMOR_CADENCE_SCALAR) -- Schedule warehouse marker updates if ENABLE_WAREHOUSE_MARKERS then diff --git a/Moose_Tanker/Moose_Tanker.lua b/Moose_Tanker/Moose_Tanker.lua new file mode 100644 index 0000000..85baf7f --- /dev/null +++ b/Moose_Tanker/Moose_Tanker.lua @@ -0,0 +1,2493 @@ +-- ============================================================================ +-- MOOSE TANKER MANAGEMENT SYSTEM +-- Comprehensive tanker lifecycle management with auto-respawn, fuel monitoring, +-- TACAN/frequency announcements, and menu controls +-- ============================================================================ + +-- ============================================================================ +-- USER CONFIGURATION +-- ============================================================================ + +-- Tanker Configuration +local TANKER_CONFIG = { + KC135 = { + groupName = "TANKER 135", + unitName = "TANKER 135-1", + displayName = "TANKER KC-135", + aircraftType = "KC-135", -- DCS aircraft type name + livery = nil, -- nil for default, or livery_id string + callsign = "SHELL", -- Map marker prefix for custom routes + tacan = "50X", -- Set to match ME or nil if none + frequency = "251.000", -- Set to match ME or nil if none + respawnDelay = 180, -- seconds before auto-respawn after destruction + emergencyRespawnDelay = 60, -- Emergency spawn delay + fuelWarningPercent = 25, -- Warn when fuel drops below this % + fuelBingoPercent = 15, -- RTB fuel level + defaultAltitude = 22000, -- Default altitude in feet (FL220) + defaultSpeed = 330, -- Default speed in knots + }, + KC135_MPRS = { + groupName = "TANKER 135 MPRS", + unitName = "TANKER 135 MPRS-1", + displayName = "TANKER KC-135 MPRS", + aircraftType = "KC135MPRS", -- DCS aircraft type name + livery = nil, + callsign = "ARCO", -- Map marker prefix for custom routes + tacan = "51X", + frequency = "252.000", + respawnDelay = 180, + emergencyRespawnDelay = 60, + fuelWarningPercent = 25, + fuelBingoPercent = 15, + defaultAltitude = 22000, + defaultSpeed = 330, + } +} + +-- Custom Route Configuration +local ROUTE_CONFIG = { + minWaypoints = 2, -- Minimum waypoints required + maxWaypoints = 10, -- Maximum waypoints allowed + deleteMarkersAfterUse = true, -- Delete markers after route creation + waypointPrefix = { -- Recognized marker prefixes + SHELL = "KC135", -- SHELL1, SHELL2, etc. → KC-135 + ARCO = "KC135_MPRS", -- ARCO1, ARCO2, etc. → KC-135 MPRS + } +} + +-- Monitoring Configuration +local FUEL_CHECK_INTERVAL = 60 -- Check fuel every 60 seconds +local DAMAGE_RTB_THRESHOLD = 50 -- RTB if hull damage exceeds this % + +-- Default Spawn Location (for non-custom route spawns) +-- Note: Using lat/lon with SetAltitude to ensure proper altitude MSL +local DEFAULT_SPAWN_COORD = COORDINATE:NewFromLLDD(34.564, 69.212):SetAltitude(22000 * 0.3048, true) -- Kabul area, FL220 + +-- ============================================================================ +-- GLOBAL STATE TRACKING +-- ============================================================================ + +TANKER_STATE = { + KC135 = { + active = false, + group = nil, + fuelWarned = false, + bingoWarned = false, + respawnScheduler = nil, + fuelMonitor = nil, + }, + KC135_MPRS = { + active = false, + group = nil, + fuelWarned = false, + bingoWarned = false, + respawnScheduler = nil, + fuelMonitor = nil, + } +} + +-- ============================================================================ +-- MENU REFERENCES (for enable/disable) +-- ============================================================================ + +local MENU_TANKER_ROOT = nil +local MENU_KC135_LAUNCH = nil +local MENU_KC135_MPRS_LAUNCH = nil + +-- ============================================================================ +-- MESSAGE POOLS FOR VARIETY +-- Randomized messages provide immersive variety across tanker operations. +-- Each category contains 100 variations selected randomly via GetRandomMessage() +-- ============================================================================ + +local TANKER_MESSAGES = { + -- Spawn Confirmation (success) + SPAWN_SUCCESS = { + "%s is airborne and ready for refueling operations.", + "%s has launched and is standing by for fuel.", + "%s is now on station and ready to pump gas.", + "%s has departed and is available for refueling.", + "%s is up and ready to service aircraft.", + "%s is airborne. Refueling services now available.", + "%s has checked in on station.", + "%s is overhead and ready for business.", + "%s is now available for aerial refueling.", + "%s has arrived on station. Ready to refuel.", + "%s is up! Time to get your drink on.", + "%s has joined the party. Bring your cups!", + "%s reporting. The bar is now open.", + "%s is flying. Get in line for your juice.", + "%s on station. Don't be shy, we got plenty.", + "%s airborne. Unlike Mo's last attempt at flying.", + "%s has successfully launched. No thanks to Mo.", + "%s is ready. Mo said he could do this but we know better.", + "%s in position. Fuel truck of the sky is open for business!", + "%s has arrived fashionably late but ready to pump.", + "%s checking in. Your gas station with wings is here.", + "%s is up there doing tanker things.", + "%s launched without hitting anything. Good start!", + "%s airborne and hasn't broken anything yet.", + "%s is ready to make it rain... JP-8.", + "%s in the pattern. Come get some dinosaur juice!", + "%s reporting for duty. Time to feed some thirsty birds.", + "%s has spawned successfully. Mo's jealous.", + "%s is flying high and ready to share the wealth.", + "%s on station. Dispensary is OPEN.", + "%s has graced you with its presence. You're welcome.", + "%s is here to save your ass from flameout.", + "%s launched. The sky gas station is open 24/7.", + "%s airborne. Better than Mo's last tanker spawn attempt.", + "%s ready to refuel. Unlike your love life, this actually works.", + "%s has arrived to keep you from embarrassing yourself.", + "%s on station and totally not judging your fuel planning.", + "%s is up. Try not to break the boom this time.", + "%s launched successfully. Mo couldn't get his off the ground.", + "%s airborne. Your aerial bartender has arrived!", + "%s ready for action. The juice is loose!", + "%s has spawned. Time to get wet... with fuel.", + "%s on station. We promise not to tell anyone you needed us.", + "%s reporting. Because someone forgot to fuel before takeoff.", + "%s is here! The flying fuel truck has arrived!", + "%s airborne and ready to fill your tanks. That's what she said.", + "%s launched. Even Mo could refuel from this... maybe.", + "%s on station. Your poor planning is our opportunity!", + "%s has arrived. The aerial milk truck is ready.", + "%s ready to pump. Get your minds out of the gutter.", + "%s airborne because you can't manage fuel apparently.", + "%s is up there waiting. Don't keep us hovering forever.", + "%s has joined the fight. By 'fight' we mean 'hovering lazily.'", + "%s on station. Premium unleaded is on tap!", + "%s launched and looking sexy up here.", + "%s ready to refuel. Try not to scratch the paint this time.", + "%s has arrived. Mo said this was impossible but here we are.", + "%s airborne. The sky's full service station is open!", + "%s on station ready to save your bacon.", + "%s has launched into the wild blue yonder!", + "%s reporting for gas pumping duty.", + "%s is up and Mo isn't. Winner: us.", + "%s airborne. Fuel flows like wine at a wedding!", + "%s on station. Unlike Mo, we actually showed up.", + "%s ready to top you off. No phrasing.", + "%s has successfully taken off. Mo's still taxiing.", + "%s airborne and operational. The real MVP.", + "%s on station doing the lord's work.", + "%s has arrived to prevent your walk of shame.", + "%s ready for refueling ops. Try to connect this time.", + "%s launched because someone has to be the adult here.", + "%s airborne. Probably more reliable than your ex.", + "%s on station ready to give you the good stuff.", + "%s has spawned. Mo's tanker is still in the hangar.", + "%s reporting. Your airborne gas station awaits!", + "%s is up! Time for some hot refueling action.", + "%s airborne and Mo's not invited to this party.", + "%s on station. We have fuel, you have need. Let's dance.", + "%s ready to dispense freedom molecules!", + "%s has arrived to fix your fuel management issues.", + "%s launched. The flying gas can is ready for customers.", + "%s airborne because apparently nobody can calculate bingo.", + "%s on station. Come get your fix!", + "%s ready to pump premium into your thirsty bird.", + "%s has spawned successfully. Suck it, Mo.", + "%s reporting. Your aerial enabler is on station.", + "%s is up there waiting like a patient parent.", + "%s airborne and ready to make your fuel gauge happy.", + "%s on station. Mo said we couldn't do it. We did it.", + "%s launched with more grace than Mo's last landing.", + "%s ready for business. The boom is ready to boom.", + "%s has arrived fashionably and ready to serve.", + "%s airborne. Your fuel problems are about to be solved!", + "%s on station doing God's work up here.", + "%s ready to refuel. We got the good stuff.", + "%s has spawned. Time to feed the hungry jets!", + "%s reporting for duty with full tanks!", + "%s launched successfully without Mo's help, thank God.", + "%s airborne. The flying filling station is OPEN!", + }, + + -- Already Active Warning + ALREADY_ACTIVE = { + "%s is already airborne!", + "%s is currently active.", + "%s is already on station.", + "%s is already flying. Check status for details.", + "%s is already up - can't spawn another.", + "%s is currently operating.", + "Cannot spawn - %s already active.", + "%s is already out there!", + "%s already flying. One at a time, please.", + "%s is already working the pattern.", + "%s is already up there, genius.", + "Dude, %s is ALREADY flying. Pay attention.", + "%s is currently active. Are you even looking?", + "Hey Einstein, %s is already airborne!", + "%s is up there right now. Use your eyes.", + "What part of '%s is active' don't you understand?", + "%s is already flying. Did Mo program this button?", + "Seriously? %s is already up. Check your radar.", + "%s is currently operational. Nice try though.", + "Negative. %s is already in the air.", + "%s is already active. Mo would have known that.", + "Can't spawn two, buttercup. %s is already flying.", + "%s is already out there doing tanker things.", + "Nice try. %s is already airborne, hotshot.", + "%s is currently flying. One's enough.", + "Hold your horses! %s is already active.", + "%s is already up. We're not running a bus service here.", + "Bruh. %s is already flying.", + "%s is currently active. Maybe learn to read?", + "Negative ghostrider. %s is already up.", + "%s is already airborne. Unlike your awareness.", + "Um, %s is ALREADY flying. Hello?", + "%s is currently on station. Wake up.", + "You can't spawn %s twice. Physics doesn't work that way.", + "%s is already active. Not sure what you expected.", + "Denied! %s is already in the pattern.", + "%s is currently flying around. Look outside.", + "News flash: %s is already airborne!", + "%s is already up there. Mo makes better decisions than this.", + "Request denied. %s is already active, chief.", + "%s is currently operational. Check your instruments.", + "Already got one! %s is flying right now.", + "%s is already airborne. Reading is fundamental.", + "Uh, no. %s is currently active.", + "%s is already out there. Situational awareness: zero.", + "Can't spawn %s again. Not a video game, buddy.", + "%s is currently flying. We only get one.", + "That's a negative. %s is already up.", + "%s is already on station. Did you even check?", + "Seriously? %s has been flying for 20 minutes.", + "%s is already active. This isn't rocket science.", + "Request rejected. %s is currently airborne.", + "%s is already up there pumping gas. Pay attention!", + "Nope. %s is already flying. Check the status board.", + "%s is currently active. Even Mo knew this.", + "Cannot comply. %s is already operational.", + "%s is already airborne. Try the status menu next time.", + "That's a no-go. %s is currently flying.", + "%s is already up. Did you think we had two?", + "Denied. %s is already on station doing its thing.", + "%s is currently active. Surprised you didn't notice.", + "Can't do it. %s is already flying around up there.", + "%s is already operational. One tanker at a time, pal.", + "Negative. %s has been active for a while now.", + "%s is already up there. Spawn button isn't a toy.", + "Request denied. %s is currently on station.", + "%s is already flying. Not cloning aircraft today.", + "Can't spawn another. %s is already airborne.", + "%s is currently active. Stop button mashing.", + "That's not happening. %s is already up.", + "%s is already operational. One's all you get.", + "No can do. %s is already in the pattern.", + "%s is currently flying. Check before clicking, maybe?", + "Request rejected. %s is already on duty.", + "%s is already airborne. Unlike your attention span.", + "Nope! %s is currently active and doing fine.", + "%s is already up there. Stop spamming the spawn button.", + "Cannot spawn duplicate. %s is already flying.", + "%s is currently operational. Mo's spawn would work better.", + "Denied! %s is already on station, genius.", + "%s is already flying. Check your tanker status!", + "That's a negative. %s is currently active.", + "%s is already airborne. One tanker per customer.", + "Can't do that. %s is already up and working.", + "%s is currently on station. Read the room.", + "Request denied. %s is already operational, chief.", + "%s is already active. Try paying attention.", + "No dice. %s is already flying the pattern.", + "%s is currently airborne. Spawn limit: 1.", + "Negative. %s is already up there doing tanker stuff.", + "%s is already active. Maybe check the status screen?", + "Can't spawn %s again. We're not made of tankers here.", + "%s is currently flying. One at a time, hotshot.", + "Request rejected. %s is already on station.", + "%s is already operational. Even Mo knows you only get one.", + "That's not possible. %s is currently airborne.", + "%s is already up there. Better situational awareness needed.", + "Denied. %s is currently active and wondering why you asked.", + "%s is already flying. The spawn button isn't for spam.", + "Cannot comply. %s is already operational, Einstein.", + }, + + -- Spawn Failure + SPAWN_FAILURE = { + "Failed to spawn %s!", + "Unable to launch %s. Try again.", + "%s spawn aborted!", + "Cannot spawn %s at this time.", + "%s failed to launch!", + "Error spawning %s. Contact support.", + "%s launch unsuccessful.", + "Unable to activate %s. Retry required.", + "%s spawn failed. Check logs.", + "Launch failure for %s!", + "%s spawn went sideways. Oops.", + "Well that didn't work. %s failed to spawn.", + "%s couldn't get off the ground. Awkward.", + "Houston, we have a problem. %s didn't spawn.", + "%s spawn failed harder than Mo's last landing.", + "Oof. %s spawn went to hell.", + "%s launch aborted. This is embarrassing.", + "Yeah, %s didn't spawn. Our bad.", + "Spawn failed for %s. Not our finest moment.", + "%s couldn't launch. Try again, genius.", + "That's a big negative on %s spawn.", + "%s failed to spawn. Did Mo write this code?", + "Error: %s spawn went boom. The bad kind.", + "%s launch unsuccessful. Better luck next time.", + "Spawn failed. %s is still in the hangar.", + "%s didn't want to fly today apparently.", + "Well crap. %s spawn totally failed.", + "%s launch aborted. Something broke.", + "That didn't work. %s spawn failed miserably.", + "%s couldn't spawn. Technical difficulties.", + "Negative spawn for %s. Try again maybe?", + "%s spawn went tits up. Sorry.", + "Launch failure! %s is grounded.", + "%s spawn crashed and burned. Not literally.", + "Unable to spawn %s. Computer says no.", + "%s launch failed. Mo could have done better.", + "Spawn error for %s. This is awkward.", + "%s didn't spawn. The universe said no.", + "Failed to launch %s. Not our day.", + "%s spawn aborted. Probably for the best.", + "Yeah... %s spawn didn't happen.", + "%s failed to spawn. Check your setup.", + "Spawn unsuccessful for %s. Womp womp.", + "%s launch went south. Way south.", + "That's a no-go. %s failed to spawn.", + "%s spawn error. Better call tech support.", + "Launch failure! %s stayed on the ground.", + "%s couldn't spawn. Even we're confused.", + "Spawn failed for %s. Mo's laughing right now.", + "%s launch aborted. Something went wrong.", + "Unable to activate %s. Try turning it off and on again.", + "%s spawn went nowhere fast.", + "Failed spawn alert: %s is still parked.", + "%s launch unsuccessful. This is fine. Everything's fine.", + "Spawn error for %s. Not ideal.", + "%s couldn't get airborne. Rough.", + "Launch aborted. %s is taking a day off.", + "%s spawn failed spectacularly.", + "Cannot spawn %s. System said 'nah.'", + "%s launch went sideways. Try again.", + "Spawn failure! %s is grounded indefinitely.", + "%s didn't spawn. Murphy's Law in effect.", + "Failed to launch %s. Mo's spawn worked better.", + "%s spawn unsuccessful. Check the logs.", + "Error spawning %s. This shouldn't happen.", + "%s launch aborted. Technical difficulties ahead.", + "Spawn failed. %s is staying home today.", + "%s couldn't spawn. Better luck next time, champ.", + "Launch failure for %s. Not sure why.", + "%s spawn went wrong. Very wrong.", + "Unable to spawn %s. Try again later.", + "%s launch failed. Mo would be disappointed.", + "Spawn error: %s didn't make it.", + "%s failed to spawn. Computer threw a tantrum.", + "Launch aborted for %s. Sorry about that.", + "%s spawn unsuccessful. Try again maybe?", + "Cannot activate %s. Spawn failed.", + "%s launch went nowhere. Like Mo's career.", + "Spawn failure! %s is MIA.", + "%s couldn't spawn. System error.", + "Failed to launch %s. This is awkward.", + "%s spawn aborted. Not today, apparently.", + "Launch error for %s. Check your setup.", + "%s didn't spawn. Computer says no way.", + "Spawn unsuccessful. %s is grounded.", + "%s launch failed. Mo's code was better.", + "Cannot spawn %s. Technical issues.", + "%s failed to activate. Try again.", + "Launch aborted. %s spawn went south.", + "%s spawn error. This isn't good.", + "Failed to spawn %s. Maybe next time.", + "%s launch unsuccessful. Something broke.", + "Spawn failure for %s. Not our best work.", + "%s couldn't get off the ground. Awkward moment.", + "Launch error! %s is still parked.", + "%s spawn went wrong. Very, very wrong.", + "Unable to spawn %s. System malfunction.", + "%s launch failed harder than expected.", + "Spawn aborted. %s is taking a sick day.", + }, + + -- Custom Route Accepted + ROUTE_ACCEPTED = { + "%s accepting custom route with %d waypoints.%s", + "%s has your route. %d waypoints loaded.%s", + "%s acknowledges custom flight plan. %d waypoints.%s", + "%s route confirmed. %d waypoints programmed.%s", + "%s copy your route. %d waypoints accepted.%s", + "%s roger. %d waypoint route loaded.%s", + "%s has the route. %d points confirmed.%s", + "%s flight plan accepted. %d waypoints.%s", + "%s confirms route. %d waypoints in the box.%s", + "%s routing confirmed with %d waypoints.%s", + "%s has your custom route. %d waypoints loaded.%s", + "%s accepts your flight plan. %d points confirmed.%s", + "%s copies custom route with %d waypoints.%s", + "%s acknowledges %d waypoint route.%s", + "%s route programmed. %d waypoints locked in.%s", + "%s flight plan confirmed with %d points.%s", + "%s roger your route. %d waypoints loaded.%s", + "%s accepts %d waypoint custom plan.%s", + "%s has your %d waypoint route locked in.%s", + "%s confirms %d waypoint flight plan.%s", + "%s copy that. %d waypoint route programmed.%s", + "%s routing accepted. %d points confirmed.%s", + "%s has the route. %d waypoints ready.%s", + "%s acknowledges %d point route.%s", + "%s flight plan loaded with %d waypoints.%s", + "%s custom route confirmed. %d points.%s", + "%s accepts your %d waypoint plan.%s", + "%s roger. %d waypoints programmed.%s", + "%s has your %d waypoint custom route.%s", + "%s routing confirmed with %d points.%s", + "%s copies %d waypoint route. Unlike Mo's attempt.%s", + "%s accepts your custom %d waypoint plan.%s", + "%s has loaded %d waypoint route.%s", + "%s confirms %d waypoint routing.%s", + "%s flight plan locked in. %d waypoints.%s", + "%s roger your %d waypoint route.%s", + "%s accepts custom route with %d points.%s", + "%s has programmed %d waypoint plan.%s", + "%s acknowledges %d waypoint custom route.%s", + "%s routing confirmed. %d waypoints ready.%s", + "%s copies %d waypoint flight plan.%s", + "%s accepts your %d point custom route.%s", + "%s has %d waypoint route confirmed.%s", + "%s roger custom plan with %d waypoints.%s", + "%s routing accepted. %d points programmed.%s", + "%s flight plan confirmed. %d waypoints loaded.%s", + "%s has your custom %d waypoint routing.%s", + "%s accepts %d waypoint plan.%s", + "%s confirms custom route. %d waypoints.%s", + "%s roger that. %d waypoint route accepted.%s", + "%s has %d waypoint custom plan loaded.%s", + "%s acknowledges %d point custom route.%s", + "%s routing programmed. %d waypoints confirmed.%s", + "%s flight plan accepted with %d points.%s", + "%s copies your %d waypoint custom route.%s", + "%s has %d waypoint route ready.%s", + "%s accepts custom plan. %d waypoints.%s", + "%s confirms %d point flight plan.%s", + "%s roger custom %d waypoint route.%s", + "%s routing locked in. %d waypoints.%s", + "%s has %d waypoint plan confirmed.%s", + "%s flight plan loaded. %d waypoints accepted.%s", + "%s acknowledges custom %d waypoint route.%s", + "%s accepts %d waypoint routing.%s", + "%s copies custom %d waypoint plan.%s", + "%s has %d waypoint route programmed.%s", + "%s confirms your %d waypoint custom route.%s", + "%s roger. %d waypoint custom plan loaded.%s", + "%s routing accepted with %d points.%s", + "%s flight plan programmed. %d waypoints.%s", + "%s has custom route with %d waypoints.%s", + "%s accepts %d waypoint custom plan.%s", + "%s acknowledges %d waypoint routing.%s", + "%s copies %d waypoint custom route.%s", + "%s has %d waypoint flight plan confirmed.%s", + "%s roger custom route. %d waypoints.%s", + "%s routing confirmed with %d waypoints.%s", + "%s flight plan accepted. %d points loaded.%s", + "%s has your %d waypoint custom plan.%s", + "%s accepts custom %d waypoint route.%s", + "%s confirms %d waypoint custom plan.%s", + "%s acknowledges %d waypoint flight plan.%s", + "%s copies %d waypoint route confirmed.%s", + "%s has custom %d waypoint routing ready.%s", + "%s roger. %d waypoints accepted and locked.%s", + "%s routing programmed with %d waypoints.%s", + "%s flight plan confirmed with %d points.%s", + "%s has %d waypoint custom route loaded.%s", + "%s accepts your custom %d waypoint routing.%s", + "%s confirms %d waypoint plan confirmed.%s", + "%s acknowledges custom route with %d points.%s", + "%s copies %d waypoint custom flight plan.%s", + "%s has %d waypoint route locked and loaded.%s", + "%s roger that. %d waypoint custom route ready.%s", + "%s routing accepted. %d waypoints programmed.%s", + "%s flight plan loaded with %d waypoints.%s", + }, + + -- Emergency Spawn + EMERGENCY_SPAWN = { + "EMERGENCY: %s launching immediately!", + "PRIORITY LAUNCH: %s is scrambling now!", + "EMERGENCY TANKER: %s departing expedited!", + "URGENT: %s is launching on priority status!", + "EMERGENCY RESPONSE: %s airborne ASAP!", + "PRIORITY: %s scrambling for emergency fuel!", + "EMERGENCY: %s launching hot!", + "URGENT LAUNCH: %s is wheels up now!", + "EMERGENCY TANKER: %s responding immediately!", + "PRIORITY STATUS: %s emergency launch in progress!", + "EMERGENCY! %s wheels up NOW!", + "SCRAMBLE SCRAMBLE: %s launching immediately!", + "PRIORITY LAUNCH: %s getting airborne right now!", + "EMERGENCY TANKER: %s departing hot and fast!", + "URGENT: %s scrambling for emergency refuel!", + "PRIORITY: %s launching on expedited status!", + "EMERGENCY RESPONSE: %s airborne immediately!", + "URGENT LAUNCH: %s departing NOW!", + "EMERGENCY: %s getting up there ASAP!", + "PRIORITY STATUS: %s scrambling right now!", + "EMERGENCY TANKER: %s wheels up immediately!", + "URGENT: %s launching on priority!", + "SCRAMBLE: %s departing expedited!", + "EMERGENCY: %s getting airborne fast!", + "PRIORITY LAUNCH: %s launching NOW!", + "URGENT TANKER: %s scrambling immediately!", + "EMERGENCY: %s departing hot!", + "PRIORITY: %s wheels up ASAP!", + "URGENT LAUNCH: %s airborne right now!", + "EMERGENCY TANKER: %s launching immediately!", + "SCRAMBLE SCRAMBLE: %s getting up there now!", + "PRIORITY: %s launching on emergency status!", + "URGENT: %s departing immediately!", + "EMERGENCY: %s scrambling for urgent refuel!", + "PRIORITY LAUNCH: %s wheels up hot!", + "URGENT TANKER: %s airborne ASAP!", + "EMERGENCY: %s launching right now!", + "PRIORITY: %s scrambling expedited!", + "URGENT LAUNCH: %s departing NOW NOW NOW!", + "EMERGENCY TANKER: %s getting airborne fast!", + "PRIORITY STATUS: %s launching immediately!", + "URGENT: %s wheels up on priority!", + "EMERGENCY: %s scrambling now!", + "PRIORITY LAUNCH: %s departing fast!", + "URGENT TANKER: %s launching ASAP!", + "EMERGENCY: %s airborne immediately!", + "PRIORITY: %s scrambling hot!", + "URGENT LAUNCH: %s wheels up right now!", + "EMERGENCY TANKER: %s departing expedited!", + "PRIORITY: %s launching on urgent status!", + "URGENT: %s getting airborne now!", + "EMERGENCY SCRAMBLE: %s departing immediately!", + "PRIORITY TANKER: %s wheels up fast!", + "URGENT: %s launching right now!", + "EMERGENCY: %s airborne ASAP!", + "PRIORITY LAUNCH: %s scrambling now!", + "URGENT TANKER: %s departing hot!", + "EMERGENCY: %s wheels up immediately!", + "PRIORITY: %s getting airborne fast!", + "URGENT LAUNCH: %s scrambling ASAP!", + "EMERGENCY TANKER: %s launching on priority!", + "PRIORITY: %s departing right now!", + "URGENT: %s airborne expedited!", + "EMERGENCY: %s scrambling immediately!", + "PRIORITY LAUNCH: %s wheels up NOW!", + "URGENT TANKER: %s launching fast!", + "EMERGENCY: %s departing ASAP!", + "PRIORITY: %s airborne right now!", + "URGENT LAUNCH: %s scrambling hot!", + "EMERGENCY TANKER: %s wheels up expedited!", + "PRIORITY: %s launching immediately!", + "URGENT: %s getting airborne ASAP!", + "EMERGENCY: %s scrambling fast!", + "PRIORITY LAUNCH: %s departing NOW!", + "URGENT TANKER: %s wheels up right now!", + "EMERGENCY: %s airborne hot!", + "PRIORITY: %s scrambling ASAP!", + "URGENT LAUNCH: %s launching immediately!", + "EMERGENCY TANKER: %s departing fast!", + "PRIORITY: %s wheels up expedited!", + "URGENT: %s airborne NOW!", + "EMERGENCY: %s launching hot and fast!", + "PRIORITY LAUNCH: %s scrambling expedited!", + "URGENT TANKER: %s departing immediately!", + "EMERGENCY: %s wheels up ASAP!", + "PRIORITY: %s getting airborne now!", + "URGENT LAUNCH: %s airborne fast!", + "EMERGENCY TANKER: %s scrambling NOW!", + "PRIORITY: %s launching expedited!", + "URGENT: %s departing hot!", + "EMERGENCY: %s airborne immediately unlike Mo!", + "PRIORITY LAUNCH: %s wheels up faster than Mo!", + "URGENT TANKER: %s scrambling (Mo couldn't do this)!", + "EMERGENCY: %s launching while Mo watches!", + "PRIORITY: %s departing - Mo take notes!", + "URGENT LAUNCH: %s airborne (unlike Mo's attempts)!", + "EMERGENCY TANKER: %s scrambling successfully!", + "PRIORITY: %s wheels up for real!", + "URGENT: %s launching like professionals do!", + }, + + -- Low Fuel Warning + LOW_FUEL = { + "%s reports fuel at %d%%. Recommend expedite refueling.", + "%s low on fuel - %d%% remaining. RTB soon.", + "%s fuel state: %d%%. Time is limited.", + "%s down to %d%% fuel. Get your gas quick.", + "%s running low - %d%% remaining.", + "%s fuel advisory: %d%% left. Don't delay.", + "%s reports %d%% fuel state. Limited time remaining.", + "%s low fuel warning at %d%%. RTB imminent.", + "%s fuel: %d%%. Better hurry up.", + "%s getting thirsty at %d%% fuel remaining.", + "%s fuel down to %d%%. Time's ticking.", + "%s running on fumes at %d%%. Get moving.", + "%s reports %d%% fuel. Clock is running.", + "%s fuel state critical at %d%%.", + "%s getting low at %d%%. Don't dawdle.", + "%s fuel: %d%%. Window is closing.", + "%s reports %d%% remaining. Hurry it up.", + "%s fuel advisory: %d%%. Time's short.", + "%s down to %d%%. Better move fast.", + "%s fuel at %d%%. RTB soon or refuel now.", + "%s running thin at %d%%. Expedite.", + "%s reports %d%% fuel. Not much time left.", + "%s fuel state %d%%. Don't mess around.", + "%s getting low - %d%% and dropping.", + "%s fuel: %d%%. Better get some quick.", + "%s reports %d%%. Running out of time.", + "%s fuel down to %d%%. Tick tock.", + "%s low on gas at %d%%. Move it.", + "%s reports %d%% fuel state. Limited window.", + "%s fuel: %d%%. Don't be slow about it.", + "%s getting thirsty - %d%% remaining.", + "%s reports %d%%. Better hurry your ass up.", + "%s fuel at %d%%. Time ain't on your side.", + "%s running low - %d%%. Get in here.", + "%s fuel state: %d%%. Mo could refuel faster.", + "%s reports %d%%. Don't be a hero, get fuel.", + "%s fuel down to %d%%. Unlike Mo we're warning you.", + "%s getting low at %d%%. Stop screwing around.", + "%s reports %d%% fuel. This isn't a drill.", + "%s fuel: %d%%. Better not screw this up.", + "%s running thin - %d%% remaining.", + "%s reports %d%%. Time to get your ass over here.", + "%s fuel state %d%%. Seriously, hurry up.", + "%s getting thirsty at %d%%. Don't be stupid.", + "%s fuel: %d%%. We're leaving soon.", + "%s reports %d%%. Better expedite refueling.", + "%s fuel down to %d%%. Window closing fast.", + "%s low on juice - %d%% remaining.", + "%s reports %d%% fuel. Get moving or RTB.", + "%s fuel state: %d%%. Don't drag ass.", + "%s getting low at %d%%. Time's running out.", + "%s reports %d%%. Stop dicking around.", + "%s fuel: %d%%. Get in the basket.", + "%s running thin at %d%%. Move faster.", + "%s reports %d%% remaining. Chop chop.", + "%s fuel down to %d%%. Unlike Mo's planning.", + "%s getting thirsty - %d%%. Don't be slow.", + "%s reports %d%% fuel state. Hurry.", + "%s fuel: %d%%. Better not flame out.", + "%s low on gas at %d%%. Get over here.", + "%s reports %d%%. Time to move it.", + "%s fuel state %d%%. We don't have all day.", + "%s getting low - %d%% and dropping fast.", + "%s reports %d%%. Stop being a pussy.", + "%s fuel: %d%%. Refuel or die trying.", + "%s running thin - %d%%. Better hurry.", + "%s reports %d%% fuel. Move your ass.", + "%s fuel down to %d%%. Not kidding here.", + "%s getting thirsty at %d%%. Expedite.", + "%s reports %d%%. Don't be like Mo.", + "%s fuel state: %d%%. Get fuel or get bent.", + "%s low at %d%%. Time's wasting.", + "%s reports %d%% remaining. Hurry up.", + "%s fuel: %d%%. Stop fucking around.", + "%s running low - %d%%. Get here now.", + "%s reports %d%%. We're not waiting forever.", + "%s fuel down to %d%%. Better get moving.", + "%s getting low at %d%%. Tick tock motherfucker.", + "%s reports %d%% fuel state. Move it.", + "%s fuel: %d%%. Don't be a jackass.", + "%s running thin at %d%%. Expedite refuel.", + "%s reports %d%%. Time's running short.", + "%s fuel state %d%%. Get in the pattern.", + "%s getting thirsty - %d%%. Don't delay.", + "%s reports %d%%. Unlike Mo we're still here.", + "%s fuel: %d%%. Better not screw this up.", + "%s low on gas - %d%% remaining.", + "%s reports %d%% fuel. Window closing.", + "%s fuel down to %d%%. Get your shit together.", + "%s getting low at %d%%. Seriously move.", + "%s reports %d%%. Don't make us leave.", + "%s fuel state: %d%%. Better expedite.", + "%s running thin - %d%%. Time's up soon.", + "%s reports %d%% remaining. Get here.", + "%s fuel: %d%%. Stop dragging ass.", + "%s getting thirsty at %d%%. Hurry.", + "%s reports %d%%. Mo would have flamed out by now.", + }, + + -- Bingo Fuel (RTB) + BINGO_FUEL = { + "%s is BINGO fuel. Returning to base immediately!", + "%s has reached BINGO. RTB in progress!", + "%s calling BINGO fuel. Departing the pattern now!", + "%s is at BINGO state. Returning to base!", + "%s BINGO fuel - heading home now!", + "%s has hit BINGO. No more refueling available!", + "%s fuel critical - RTB initiated!", + "%s at BINGO state. Breaking off now!", + "%s calling BINGO. Pattern is clear!", + "%s BINGO fuel declared. Returning to base!", + "%s is BINGO. Getting the hell out!", + "%s calling BINGO fuel. We're done here!", + "%s has reached BINGO state. Leaving NOW!", + "%s BINGO declared. RTB in progress!", + "%s at BINGO fuel. Heading home!", + "%s calling BINGO. Pattern clear!", + "%s has hit BINGO. See ya!", + "%s BINGO fuel state. Departing!", + "%s is at BINGO. RTB immediately!", + "%s calling BINGO. We're out!", + "%s has reached BINGO. Breaking off!", + "%s BINGO fuel declared. Leaving!", + "%s at BINGO state. Going home!", + "%s calling BINGO. Adios!", + "%s has hit BINGO fuel. Departing now!", + "%s BINGO declared. RTB active!", + "%s is at BINGO. Bye bye!", + "%s calling BINGO fuel. Out of here!", + "%s has reached BINGO state. Later!", + "%s BINGO fuel. Heading back!", + "%s at BINGO. Returning immediately!", + "%s calling BINGO. Pattern's yours!", + "%s has hit BINGO. Going home!", + "%s BINGO declared. Leaving the AO!", + "%s is at BINGO fuel. RTB now!", + "%s calling BINGO. Peace out!", + "%s has reached BINGO. Departing!", + "%s BINGO fuel state. We're done!", + "%s at BINGO. Heading to base!", + "%s calling BINGO. Catch you later!", + "%s has hit BINGO fuel. RTB!", + "%s BINGO declared. Getting out!", + "%s is at BINGO state. Later gator!", + "%s calling BINGO. We out!", + "%s has reached BINGO. Returning!", + "%s BINGO fuel. Leaving now!", + "%s at BINGO. Going home finally!", + "%s calling BINGO. Done pumping gas!", + "%s has hit BINGO. RTB initiated!", + "%s BINGO declared. Out of here!", + "%s is at BINGO fuel. Bye!", + "%s calling BINGO. Pattern clear!", + "%s has reached BINGO state. Departing!", + "%s BINGO fuel. Heading back!", + "%s at BINGO. RTB in progress!", + "%s calling BINGO. See ya later!", + "%s has hit BINGO fuel. Leaving!", + "%s BINGO declared. Going home!", + "%s is at BINGO. Out!", + "%s calling BINGO fuel. We're outta here!", + "%s has reached BINGO. RTB now!", + "%s BINGO fuel state. Later!", + "%s at BINGO. Returning to base!", + "%s calling BINGO. Adios amigos!", + "%s has hit BINGO. Departing!", + "%s BINGO declared. Heading home!", + "%s is at BINGO fuel. Peace!", + "%s calling BINGO. We done!", + "%s has reached BINGO state. Leaving!", + "%s BINGO fuel. RTB active!", + "%s at BINGO. Going back!", + "%s calling BINGO. That's it folks!", + "%s has hit BINGO fuel. Out of here!", + "%s BINGO declared. Returning!", + "%s is at BINGO. Later suckers!", + "%s calling BINGO fuel. Bye!", + "%s has reached BINGO. Heading home!", + "%s BINGO fuel state. Departing!", + "%s at BINGO. RTB initiated!", + "%s calling BINGO. We're gone!", + "%s has hit BINGO. Leaving now!", + "%s BINGO declared. Getting out of dodge!", + "%s is at BINGO fuel. Later!", + "%s calling BINGO. Don't wait up!", + "%s has reached BINGO state. Out!", + "%s BINGO fuel. Going home!", + "%s at BINGO. Returning immediately!", + "%s calling BINGO. Unlike Mo we planned this!", + "%s has hit BINGO fuel. Peace out!", + "%s BINGO declared. Heading back!", + "%s is at BINGO. Bye felicia!", + "%s calling BINGO fuel. That's a wrap!", + "%s has reached BINGO. We're out!", + "%s BINGO fuel state. Later gator!", + "%s at BINGO. RTB right now!", + "%s calling BINGO. Smell ya later!", + "%s has hit BINGO. Departing!", + "%s BINGO declared. Mo would've flamed out!", + "%s is at BINGO fuel. Catch you on the flip side!", + }, + + -- Tanker Destroyed + DESTROYED = { + "%s has been destroyed!", + "%s is down! Aircraft lost!", + "%s has been shot down!", + "%s destroyed in combat!", + "%s is gone - aircraft destroyed!", + "%s has been lost!", + "We've lost %s!", + "%s destroyed! No survivors!", + "%s is down and out!", + "%s has been eliminated!", + "%s has been blown to hell!", + "%s is toast! Aircraft destroyed!", + "%s went down in flames!", + "%s has been obliterated!", + "%s is scrap metal now!", + "%s got smoked!", + "RIP %s. Aircraft destroyed!", + "%s has been vaporized!", + "%s is no more!", + "%s went down hard!", + "%s has been wasted!", + "%s is KIA! Aircraft lost!", + "%s got shot the fuck down!", + "%s has been annihilated!", + "%s is sleeping with the fishes!", + "%s went boom!", + "%s has been terminated!", + "%s is dead! No survivors!", + "%s got fucked up!", + "%s has ceased to exist!", + "%s went down like Mo's career!", + "%s is destroyed! Total loss!", + "%s got hammered!", + "%s has been neutralized!", + "%s is history!", + "%s went down in a ball of fire!", + "%s has been taken out!", + "%s is gone forever!", + "%s got massacred!", + "%s has been deleted!", + "%s is pushing up daisies!", + "%s went down screaming!", + "%s has been liquidated!", + "%s is scattered across the landscape!", + "%s got wrecked!", + "%s has been erased!", + "%s is no longer operational!", + "%s went down like a brick!", + "%s has been dispatched!", + "%s is gone to the great hangar in the sky!", + "%s got absolutely demolished!", + "%s has been removed from existence!", + "%s is dead as fuck!", + "%s went down faster than Mo!", + "%s has been exterminated!", + "%s is now a smoking crater!", + "%s got absolutely destroyed!", + "%s has been converted to debris!", + "%s is no longer with us!", + "%s went down in a spectacular fashion!", + "%s has been sent to hell!", + "%s is totally fucked!", + "%s got blown out of the sky!", + "%s has been utterly destroyed!", + "%s is burning on the ground!", + "%s went down like a sack of shit!", + "%s has been wiped out!", + "%s is permanently grounded!", + "%s got turned into confetti!", + "%s has been removed from service!", + "%s is now spare parts!", + "%s went down hard and fast!", + "%s has been completely destroyed!", + "%s is toast and then some!", + "%s got absolutely annihilated!", + "%s has been blown to smithereens!", + "%s is no longer flying!", + "%s went down like the Hindenburg!", + "%s has been totally wrecked!", + "%s is deader than dead!", + "%s got straight up murdered!", + "%s has been completely obliterated!", + "%s is scattered across three counties!", + "%s went down in flames like Mo's reputation!", + "%s has been catastrophically destroyed!", + "%s is now a fireball!", + "%s got absolutely smoked!", + "%s has been reduced to atoms!", + "%s is gone gone gone!", + "%s went down and ain't coming back!", + "%s has been utterly annihilated!", + "%s is now a lawn dart!", + "%s got completely fucked!", + "%s has been sent to the shadow realm!", + "%s is now in aircraft heaven!", + "%s went down faster than your hopes and dreams!", + "%s has been totally destroyed!", + "%s is no longer a thing!", + }, + + -- Hostile Fire + TAKING_FIRE = { + "%s is taking fire!", + "%s under attack!", + "%s receiving hostile fire!", + "%s taking hits!", + "%s is being engaged!", + "%s under hostile fire!", + "Hostile fire on %s!", + "%s taking enemy fire!", + "%s is under attack!", + "%s being fired upon!", + "%s is getting shot at!", + "%s under hostile fire!", + "%s taking incoming!", + "%s is being lit up!", + "%s receiving enemy fire!", + "%s getting hammered!", + "%s under attack right now!", + "%s taking fire from hostiles!", + "%s is being engaged by enemy!", + "%s getting shot to shit!", + "%s under hostile attack!", + "%s taking heavy fire!", + "%s is being targeted!", + "%s receiving hostile rounds!", + "%s getting fucked up!", + "%s under enemy fire!", + "%s taking hits from hostiles!", + "%s is being shot at!", + "%s receiving incoming fire!", + "%s getting attacked!", + "%s under hostile engagement!", + "%s taking enemy rounds!", + "%s is being hit!", + "%s receiving fire!", + "%s getting lit up!", + "%s under attack from enemy!", + "%s taking hostile fire!", + "%s is being engaged!", + "%s receiving enemy rounds!", + "%s getting shot!", + "%s under fire right now!", + "%s taking incoming rounds!", + "%s is being attacked!", + "%s receiving hostile fire!", + "%s getting hammered by hostiles!", + "%s under enemy attack!", + "%s taking fire from below!", + "%s is being targeted by enemy!", + "%s receiving heavy fire!", + "%s getting shot at hard!", + "%s under hostile fire!", + "%s taking enemy fire now!", + "%s is being engaged by hostiles!", + "%s receiving incoming!", + "%s getting attacked by enemy!", + "%s under fire from hostiles!", + "%s taking hits!", + "%s is being shot up!", + "%s receiving hostile rounds!", + "%s getting fucked up by enemy!", + "%s under hostile engagement!", + "%s taking fire!", + "%s is being hammered!", + "%s receiving enemy fire!", + "%s getting shot at!", + "%s under enemy fire!", + "%s taking hostile rounds!", + "%s is being lit up!", + "%s receiving fire from hostiles!", + "%s getting attacked hard!", + "%s under hostile attack!", + "%s taking incoming fire!", + "%s is being engaged!", + "%s receiving hostile fire!", + "%s getting shot to hell!", + "%s under fire!", + "%s taking enemy rounds!", + "%s is being targeted!", + "%s receiving fire!", + "%s getting hammered!", + "%s under attack by hostiles!", + "%s taking fire from enemy!", + "%s is being shot at!", + "%s receiving incoming rounds!", + "%s getting attacked!", + "%s under hostile fire right now!", + "%s taking hits from enemy!", + "%s is being engaged by hostiles!", + "%s receiving hostile fire!", + "%s getting lit up by enemy!", + "%s under fire from below!", + "%s taking enemy fire!", + "%s is being attacked by hostiles!", + "%s receiving fire from enemy!", + "%s getting shot at hard!", + "%s under enemy attack!", + "%s taking hostile fire unlike Mo who'd be dead!", + "%s is being hammered by hostiles!", + "%s receiving enemy rounds!", + }, + + -- Invalid Waypoint Count (too few) + TOO_FEW_WAYPOINTS = { + "Custom route requires at least %d waypoints!\nPlace markers: %s1, %s2, etc.", + "Not enough waypoints! Need at least %d.\nUse markers: %s1, %s2, etc.", + "Insufficient waypoints - need %d minimum.\nCreate markers: %s1, %s2, etc.", + "Route rejected: need %d waypoints minimum.\nPlace %s1, %s2, etc.", + "At least %d waypoints required!\nDrop markers: %s1, %s2, etc.", + "Need more waypoints - minimum is %d.\nUse: %s1, %s2, etc.", + "Route incomplete. Need %d waypoints.\nCreate: %s1, %s2, etc.", + "Waypoint count too low - need %d.\nPlace: %s1, %s2, etc.", + "Minimum %d waypoints required!\nMark: %s1, %s2, etc.", + "Can't route with less than %d points.\nAdd markers: %s1, %s2, etc.", + }, + + -- Too Many Waypoints + TOO_MANY_WAYPOINTS = { + "Too many waypoints! Maximum is %d", + "Waypoint limit exceeded. Max: %d", + "Can't route with more than %d waypoints!", + "Route rejected - too many points. Max: %d", + "Waypoint overflow! Maximum is %d", + "Too complex - max %d waypoints allowed!", + "Exceeded waypoint limit of %d!", + "Route too long! Maximum: %d waypoints", + "Cannot accept more than %d waypoints!", + "Waypoint maximum is %d. Route rejected.", + }, + + -- No RTB Airbase Found + NO_RTB_AIRBASE = { + "No friendly airbase found for RTB!", + "Cannot locate RTB destination!", + "No suitable airbase available for recovery!", + "Unable to find friendly base for RTB!", + "No recovery airfield located!", + "RTB destination unavailable!", + "Cannot identify friendly airbase for return!", + "No airbase in range for RTB!", + "Recovery base not found!", + "Unable to locate RTB airfield!", + }, + -- EMERGENCY SPAWN MESSAGES (100 total) + EMERGENCY_SPAWN = { + "EMERGENCY: %s launching immediately!", + "PRIORITY LAUNCH: %s is scrambling now!", + "EMERGENCY TANKER: %s departing expedited!", + "URGENT: %s is launching on priority status!", + "EMERGENCY RESPONSE: %s airborne ASAP!", + "PRIORITY: %s scrambling for emergency fuel!", + "EMERGENCY: %s launching hot!", + "URGENT LAUNCH: %s is wheels up now!", + "EMERGENCY TANKER: %s responding immediately!", + "PRIORITY STATUS: %s emergency launch in progress!", + "EMERGENCY! %s wheels up NOW!", + "SCRAMBLE SCRAMBLE: %s launching immediately!", + "PRIORITY LAUNCH: %s getting airborne right now!", + "EMERGENCY TANKER: %s departing hot and fast!", + "URGENT: %s scrambling for emergency refuel!", + "PRIORITY: %s launching on expedited status!", + "EMERGENCY RESPONSE: %s airborne immediately!", + "URGENT LAUNCH: %s departing NOW!", + "EMERGENCY: %s getting up there ASAP!", + "PRIORITY STATUS: %s scrambling right now!", + "EMERGENCY TANKER: %s wheels up immediately!", + "URGENT: %s launching on priority!", + "SCRAMBLE: %s departing expedited!", + "EMERGENCY: %s getting airborne fast!", + "PRIORITY LAUNCH: %s launching NOW!", + "URGENT TANKER: %s scrambling immediately!", + "EMERGENCY: %s departing hot!", + "PRIORITY: %s wheels up ASAP!", + "URGENT LAUNCH: %s airborne right now!", + "EMERGENCY TANKER: %s launching immediately!", + "SCRAMBLE SCRAMBLE: %s getting up there now!", + "PRIORITY: %s launching on emergency status!", + "URGENT: %s departing immediately!", + "EMERGENCY: %s scrambling for urgent refuel!", + "PRIORITY LAUNCH: %s wheels up hot!", + "URGENT TANKER: %s airborne ASAP!", + "EMERGENCY: %s launching right now!", + "PRIORITY: %s scrambling expedited!", + "URGENT LAUNCH: %s departing NOW NOW NOW!", + "EMERGENCY TANKER: %s getting airborne fast!", + "PRIORITY STATUS: %s launching immediately!", + "URGENT: %s wheels up on priority!", + "EMERGENCY: %s scrambling now!", + "PRIORITY LAUNCH: %s departing fast!", + "URGENT TANKER: %s launching ASAP!", + "EMERGENCY: %s airborne immediately!", + "PRIORITY: %s scrambling hot!", + "URGENT LAUNCH: %s wheels up right now!", + "EMERGENCY TANKER: %s departing expedited!", + "PRIORITY: %s launching on urgent status!", + "URGENT: %s getting airborne now!", + "EMERGENCY SCRAMBLE: %s departing immediately!", + "PRIORITY TANKER: %s wheels up fast!", + "URGENT: %s launching right now!", + "EMERGENCY: %s airborne ASAP!", + "PRIORITY LAUNCH: %s scrambling now!", + "URGENT TANKER: %s departing hot!", + "EMERGENCY: %s wheels up immediately!", + "PRIORITY: %s getting airborne fast!", + "URGENT LAUNCH: %s scrambling ASAP!", + "EMERGENCY TANKER: %s launching on priority!", + "PRIORITY: %s departing right now!", + "URGENT: %s airborne expedited!", + "EMERGENCY: %s scrambling immediately!", + "PRIORITY LAUNCH: %s wheels up NOW!", + "URGENT TANKER: %s launching fast!", + "EMERGENCY: %s departing ASAP!", + "PRIORITY: %s airborne right now!", + "URGENT LAUNCH: %s scrambling hot!", + "EMERGENCY TANKER: %s wheels up expedited!", + "PRIORITY: %s launching immediately!", + "URGENT: %s getting airborne ASAP!", + "EMERGENCY: %s scrambling fast!", + "PRIORITY LAUNCH: %s departing NOW!", + "URGENT TANKER: %s wheels up right now!", + "EMERGENCY: %s airborne hot!", + "PRIORITY: %s scrambling ASAP!", + "URGENT LAUNCH: %s launching immediately!", + "EMERGENCY TANKER: %s departing fast!", + "PRIORITY: %s wheels up expedited!", + "URGENT: %s airborne NOW!", + "EMERGENCY: %s launching hot and fast!", + "PRIORITY LAUNCH: %s scrambling expedited!", + "URGENT TANKER: %s departing immediately!", + "EMERGENCY: %s wheels up ASAP!", + "PRIORITY: %s getting airborne now!", + "URGENT LAUNCH: %s airborne fast!", + "EMERGENCY TANKER: %s scrambling NOW!", + "PRIORITY: %s launching expedited!", + "URGENT: %s departing hot!", + "EMERGENCY: %s airborne immediately unlike Mo!", + "PRIORITY LAUNCH: %s wheels up faster than Mo!", + "URGENT TANKER: %s scrambling (Mo couldn't do this)!", + "EMERGENCY: %s launching while Mo watches!", + "PRIORITY: %s departing - Mo take notes!", + "URGENT LAUNCH: %s airborne (unlike Mo's attempts)!", + "EMERGENCY TANKER: %s scrambling successfully!", + "PRIORITY: %s wheels up for real!", + "URGENT: %s launching like professionals do!", + }, +} + +--- Get a random message from a category +--- @param category string Message category key +--- @param ... any Format arguments for string.format +--- @return string Formatted message +local function GetRandomMessage(category, ...) + local pool = TANKER_MESSAGES[category] + if not pool or #pool == 0 then + return "Message unavailable" + end + + local template = pool[math.random(1, #pool)] + + if select("#", ...) > 0 then + return string.format(template, ...) + else + return template + end +end + +-- ============================================================================ +-- UTILITY FUNCTIONS +-- ============================================================================ + +--- Update menu state based on tanker availability +local function UpdateTankerMenus() + if MENU_KC135_LAUNCH then + if TANKER_STATE.KC135.active then + MENU_KC135_LAUNCH:Remove() + MENU_KC135_LAUNCH = nil + elseif not MENU_KC135_LAUNCH then + MENU_KC135_LAUNCH = MENU_COALITION_COMMAND:New( + coalition.side.BLUE, + "Launch " .. TANKER_CONFIG.KC135.displayName, + MENU_TANKER_ROOT, + SpawnTanker + ) + end + end + + if MENU_KC135_MPRS_LAUNCH then + if TANKER_STATE.KC135_MPRS.active then + MENU_KC135_MPRS_LAUNCH:Remove() + MENU_KC135_MPRS_LAUNCH = nil + elseif not MENU_KC135_MPRS_LAUNCH then + MENU_KC135_MPRS_LAUNCH = MENU_COALITION_COMMAND:New( + coalition.side.BLUE, + "Launch " .. TANKER_CONFIG.KC135_MPRS.displayName, + MENU_TANKER_ROOT, + SpawnTankerMPRS + ) + end + end +end + +--- Announce tanker information to coalition +local function AnnounceTankerInfo(config, spawned) + local msg = GetRandomMessage("SPAWN_SUCCESS", config.displayName) .. "\n" + + if config.tacan then + msg = msg .. string.format("TACAN: %s\n", config.tacan) + end + + if config.frequency then + msg = msg .. string.format("Radio: %s MHz", config.frequency) + end + + MESSAGE:New(msg, 20):ToBlue() + env.info(string.format("[TANKER] %s spawned successfully", config.displayName)) +end + +--- Monitor tanker fuel levels +local function MonitorTankerFuel(stateKey, config) + return function() + local state = TANKER_STATE[stateKey] + + if not state.active or not state.group then + return + end + + -- Check if group still exists + if not state.group:IsAlive() then + return + end + + local fuelPercent = state.group:GetFuel() * 100 + + -- Bingo fuel check + if fuelPercent <= config.fuelBingoPercent and not state.bingoWarned then + MESSAGE:New(GetRandomMessage("BINGO_FUEL", config.displayName), 15, "WARNING"):ToBlue() + state.bingoWarned = true + env.info(string.format("[TANKER] %s bingo fuel: %.1f%%", config.displayName, fuelPercent)) + + -- Low fuel warning + elseif fuelPercent <= config.fuelWarningPercent and not state.fuelWarned then + MESSAGE:New(GetRandomMessage("LOW_FUEL", config.displayName, math.floor(fuelPercent)), 15):ToBlue() + state.fuelWarned = true + env.info(string.format("[TANKER] %s low fuel warning: %.1f%%", config.displayName, fuelPercent)) + end + end +end + +--- Start (or restart) the fuel monitor scheduler for a tanker +local function StartFuelMonitor(stateKey, config) + local state = TANKER_STATE[stateKey] + if not state then + return + end + + if state.fuelMonitor then + state.fuelMonitor:Stop() + state.fuelMonitor = nil + end + + state.fuelMonitor = SCHEDULER:New( + nil, + MonitorTankerFuel(stateKey, config), + {}, + FUEL_CHECK_INTERVAL, + FUEL_CHECK_INTERVAL + ) +end + +--- Schedule auto-respawn after tanker loss +local function ScheduleRespawn(stateKey, config, spawnFunc) + local state = TANKER_STATE[stateKey] + + -- Cancel existing respawn if any + if state.respawnScheduler then + state.respawnScheduler:Stop() + end + + local countdown = config.respawnDelay + + MESSAGE:New(string.format("%s will respawn in %d seconds", + config.displayName, countdown), 10):ToBlue() + + -- Respawn scheduler + state.respawnScheduler = SCHEDULER:New(nil, function() + env.info(string.format("[TANKER] Auto-respawning %s", config.displayName)) + spawnFunc() + end, {}, config.respawnDelay) +end + +--- Clean up tanker state +local function CleanupTankerState(stateKey) + local state = TANKER_STATE[stateKey] + + state.active = false + state.group = nil + state.fuelWarned = false + state.bingoWarned = false + + if state.fuelMonitor then + state.fuelMonitor:Stop() + state.fuelMonitor = nil + end + + if state.respawnScheduler then + state.respawnScheduler:Stop() + state.respawnScheduler = nil + end +end + +-- ============================================================================ +-- CUSTOM ROUTE FUNCTIONS +-- ============================================================================ + +--- Parse waypoint marker text for altitude and speed overrides +--- Supports formats: SHELL1, SHELL1:FL220, SHELL1:FL220:SP330, SHELL1::SP300, SHELL1:RTB +--- @param markerText string The text from the map marker +--- @param defaultAlt number Default altitude in feet +--- @param defaultSpeed number Default speed in knots +--- @return table Parsed waypoint data {altitude, speed, rtb, isValid} +local function ParseWaypointMarker(markerText, defaultAlt, defaultSpeed) + local result = { + altitude = defaultAlt, + speed = defaultSpeed, + rtb = false, + isValid = true, + originalText = markerText + } + + -- Split by colon + local parts = {} + for part in string.gmatch(markerText, "[^:]+") do + table.insert(parts, part) + end + + -- Check for RTB command + for _, part in ipairs(parts) do + if string.upper(part) == "RTB" then + result.rtb = true + return result + end + end + + -- Parse FL (Flight Level) + for _, part in ipairs(parts) do + local fl = string.match(part, "FL(%d+)") + if fl then + result.altitude = tonumber(fl) * 100 -- Convert FL to feet + end + end + + -- Parse SP (Speed) + for _, part in ipairs(parts) do + local sp = string.match(part, "SP(%d+)") + if sp then + result.speed = tonumber(sp) + end + end + + return result +end + +--- Scan map for waypoint markers matching callsign pattern +--- @param callsign string The callsign prefix to search for (e.g., "SHELL", "ARCO") +--- @return table Array of waypoint data sorted by sequence number +local function ScanForWaypointMarkers(callsign) + local waypoints = {} + local markerIds = {} + + -- Iterate through all possible marker IDs (DCS markers are numbered) + -- We'll scan up to 1000 markers (should be more than enough) + for i = 1, 1000 do + local markerData = world.getMarkPanels() + if markerData and markerData[i] then + local marker = markerData[i] + local markerText = marker.text + + if markerText then + -- Check if marker matches pattern: CALLSIGN + number + local upperText = string.upper(markerText) + local upperCallsign = string.upper(callsign) + local sequence = string.match(upperText, "^" .. upperCallsign .. "(%d+)") + + if sequence then + local seqNum = tonumber(sequence) + local pos = marker.pos + + table.insert(waypoints, { + sequence = seqNum, + coordinate = COORDINATE:NewFromVec3(pos), + markerId = marker.idx, + markerText = markerText + }) + + table.insert(markerIds, marker.idx) + + env.info(string.format("[TANKER] Found waypoint marker: %s at seq %d (ID: %d)", + markerText, seqNum, marker.idx)) + end + end + end + end + + -- Sort by sequence number + table.sort(waypoints, function(a, b) return a.sequence < b.sequence end) + + return waypoints, markerIds +end + +-- ============================================================================ + +--- Spawn a tanker directly from config (no Mission Editor template required) +--- @param config table Tanker configuration +--- @param coord COORDINATE Where to spawn +--- @param heading number Initial heading in degrees +--- @return GROUP The spawned tanker group +local function SpawnTankerFromConfig(config, coord, heading) + -- Generate unique group/unit IDs + local groupId = math.random(10000, 99999) + local unitId = math.random(10000, 99999) + + -- Ensure we have valid altitude (coord.y is altitude in meters MSL) + local spawnAlt = coord.y + env.info(string.format("[TANKER] Spawn altitude: %.1f meters (FL%03d)", spawnAlt, spawnAlt * 3.28084 / 100)) + + -- Create group data structure + local groupData = { + ["visible"] = false, + ["taskSelected"] = true, + ["route"] = { + ["points"] = { + [1] = { + ["alt"] = spawnAlt, + ["type"] = "Turning Point", + ["action"] = "Turning Point", + ["alt_type"] = "BARO", + ["speed"] = config.defaultSpeed * 0.514444, + ["task"] = { + ["id"] = "ComboTask", + ["params"] = { + ["tasks"] = { + [1] = { + ["id"] = "Tanker", + ["params"] = {} + } + } + } + }, + ["x"] = coord.x, + ["y"] = coord.z, + } + } + }, + ["hidden"] = false, + ["units"] = { + [1] = { + ["alt"] = spawnAlt, + ["alt_type"] = "BARO", + ["livery_id"] = config.livery, + ["skill"] = "High", + ["speed"] = config.defaultSpeed * 0.514444, + ["type"] = config.aircraftType, + ["unitId"] = unitId, + ["psi"] = -heading, -- Negative for correct heading + ["unitName"] = config.unitName, + ["x"] = coord.x, + ["y"] = coord.z, + ["heading"] = math.rad(heading), + ["onboard_num"] = "010", + }, + }, + ["groupId"] = groupId, + ["y"] = coord.z, + ["x"] = coord.x, + ["name"] = config.groupName, + ["task"] = "Refueling", + } + + -- Spawn the group using coalition.addGroup + local spawnedGroup = coalition.addGroup(country.id.USA, Group.Category.AIRPLANE, groupData) + + if spawnedGroup then + env.info(string.format("[TANKER] Spawned %s (ID: %d)", config.groupName, groupId)) + return GROUP:Find(spawnedGroup:getName()) + else + env.error(string.format("[TANKER] Failed to spawn %s", config.groupName)) + return nil + end +end + +--- Ensure default spawns immediately enter a holding pattern so they do not RTB +--- @param group GROUP The spawned tanker group +--- @param coord COORDINATE Center point for the orbit +--- @param config table Tanker configuration for speed/altitude +local function ApplyDefaultOrbitRoute(group, coord, config) + if not group or not coord or not config then + return + end + + local orbitCenter = coord:SetAltitude(config.defaultAltitude * 0.3048, true) + local orbitWP = orbitCenter:WaypointAirTurningPoint( + COORDINATE.WaypointAltType.BARO, + config.defaultSpeed * 0.514444, + config.defaultAltitude * 0.3048, + {}, + "DEFAULT-ORBIT" + ) + + orbitWP.task = { + id = "ComboTask", + params = { + tasks = { + { + id = "Tanker", + params = {} + }, + { + id = "Orbit", + params = { + pattern = "Circle", + speed = config.defaultSpeed * 0.514444, + altitude = config.defaultAltitude * 0.3048, + point = { + x = orbitCenter.x, + y = orbitCenter.z + } + } + } + } + } + } + + group:Route({ orbitWP }) + env.info(string.format("[TANKER] Applied default orbit for %s", config.displayName)) +end + +--- Create custom route tanker spawn +--- @param callsign string Callsign prefix used for markers +--- @param config table Tanker configuration +--- @param stateKey string State key for tracking +--- @param isEmergency boolean Whether this is an emergency spawn +--- @return boolean Success status +local function SpawnCustomRouteTanker(callsign, config, stateKey, isEmergency) + local state = TANKER_STATE[stateKey] + + -- Check if already active + if state.active then + MESSAGE:New(GetRandomMessage("ALREADY_ACTIVE", config.displayName), 10):ToBlue() + return false + end + + -- Scan for waypoint markers + local waypoints, markerIds = ScanForWaypointMarkers(callsign) + + -- Validate waypoint count + if #waypoints < ROUTE_CONFIG.minWaypoints then + MESSAGE:New(GetRandomMessage("TOO_FEW_WAYPOINTS", + ROUTE_CONFIG.minWaypoints, callsign, callsign), 15, "ERROR"):ToBlue() + return false + end + + if #waypoints > ROUTE_CONFIG.maxWaypoints then + MESSAGE:New(GetRandomMessage("TOO_MANY_WAYPOINTS", + ROUTE_CONFIG.maxWaypoints), 15, "ERROR"):ToBlue() + return false + end + + -- Build route description and validate waypoints + local routeDesc = "" + local routePoints = {} + local hasRTB = false + + for i, wp in ipairs(waypoints) do + local parsed = ParseWaypointMarker(wp.markerText, config.defaultAltitude, config.defaultSpeed) + + if parsed.rtb then + hasRTB = true + -- Find nearest friendly airbase from last waypoint position + local lastPos = #routePoints > 0 and routePoints[#routePoints].coord or wp.coordinate + local nearestAirbase = lastPos:GetClosestAirbase(Airbase.Category.AIRDROME, coalition.side.BLUE) + + if nearestAirbase then + local airbaseName = nearestAirbase:GetName() + local airbaseCoord = nearestAirbase:GetCoordinate() + routeDesc = routeDesc .. string.format("\n WP%d: RTB to %s", i, airbaseName) + + table.insert(routePoints, { + coord = airbaseCoord, + altitude = 0, -- Will land + speed = parsed.speed, + rtb = true, + airbase = nearestAirbase, + airbaseName = airbaseName + }) + + env.info(string.format("[TANKER] RTB destination: %s", airbaseName)) + else + routeDesc = routeDesc .. string.format("\n WP%d: RTB (no airbase found)", i) + env.warning("[TANKER] No friendly airbase found for RTB") + end + break -- RTB is terminal command + else + routeDesc = routeDesc .. string.format("\n WP%d: FL%03d @ %d kts", + i, math.floor(parsed.altitude / 100), parsed.speed) + + table.insert(routePoints, { + coord = wp.coordinate, + altitude = parsed.altitude, + speed = parsed.speed, + rtb = false + }) + end + end + + -- Confirm route to player + local emergencyText = isEmergency and " [EMERGENCY]" or "" + local routeMsg = GetRandomMessage("ROUTE_ACCEPTED", config.displayName, #routePoints, routeDesc) + if isEmergency then + routeMsg = GetRandomMessage("EMERGENCY_SPAWN", config.displayName) .. "\n" .. routeMsg + end + MESSAGE:New(routeMsg, 20):ToBlue() + + env.info(string.format("[TANKER] Spawning %s with custom route: %d waypoints", + config.displayName, #routePoints)) + + -- Debug: log route point data + for i, rp in ipairs(routePoints) do + env.info(string.format("[TANKER] RoutePoint %d: coord=%s, alt=%.0f, spd=%.0f, rtb=%s", + i, tostring(rp.coord), rp.altitude, rp.speed, tostring(rp.rtb))) + end + + -- Delete markers if configured + if ROUTE_CONFIG.deleteMarkersAfterUse then + for _, markerId in ipairs(markerIds) do + trigger.action.removeMark(markerId) + end + env.info(string.format("[TANKER] Deleted %d waypoint markers", #markerIds)) + end + + -- Spawn tanker with custom route + -- Calculate initial heading + local headingCoord + if routePoints[2] and routePoints[2].coord then + headingCoord = routePoints[2].coord + else + headingCoord = routePoints[1].coord + end + + local initialHeading = routePoints[1].coord:HeadingTo(headingCoord) + + -- Set the spawn coordinate with correct altitude (convert feet to meters) + local spawnCoord = routePoints[1].coord:SetAltitude(routePoints[1].altitude * 0.3048) + + local spawnedGroup = SpawnTankerFromConfig( + config, + spawnCoord, + initialHeading + ) + + if not spawnedGroup then + MESSAGE:New(GetRandomMessage("SPAWN_FAILURE", config.displayName), 10, "ERROR"):ToBlue() + return false + end + + -- Route the group through all waypoints + local taskRoute = {} + for i, rp in ipairs(routePoints) do + local wp + + -- RTB waypoint - land at airbase + if rp.rtb and rp.airbase then + wp = rp.coord:WaypointAirLanding( + rp.speed * 0.514444, + rp.airbase:GetDCSObject(), + {}, + "RTB" + ) + else + -- Normal waypoint + wp = rp.coord:WaypointAirFlyOverPoint( + COORDINATE.WaypointAltType.BARO, + rp.speed * 0.514444, -- Convert knots to m/s + rp.altitude * 0.3048, -- Convert feet to meters + {}, + "WP" .. i + ) + + -- Add tanker task to all waypoints + wp.task = { + id = "ComboTask", + params = { + tasks = { + { + id = "Tanker", + params = {} + } + } + } + } + end + + table.insert(taskRoute, wp) + end + + -- If last waypoint is not RTB, loop back to first waypoint to create continuous patrol + if not hasRTB and #routePoints > 1 then + local firstPoint = routePoints[1] + local loopWP = firstPoint.coord:WaypointAirFlyOverPoint( + COORDINATE.WaypointAltType.BARO, + firstPoint.speed * 0.514444, + firstPoint.altitude * 0.3048, + {}, + "LOOP-WP1" + ) + + -- Add tanker task to loop waypoint + loopWP.task = { + id = "ComboTask", + params = { + tasks = { + { + id = "Tanker", + params = {} + } + } + } + } + + table.insert(taskRoute, loopWP) + env.info(string.format("[TANKER] Added loop waypoint back to WP1 for continuous patrol")) + elseif not hasRTB and #routePoints == 1 then + -- Single waypoint - add circular orbit pattern + local singlePoint = routePoints[1] + local orbitWP = singlePoint.coord:WaypointAirTurningPoint( + COORDINATE.WaypointAltType.BARO, + singlePoint.speed * 0.514444, + singlePoint.altitude * 0.3048, + {}, + "ORBIT" + ) + orbitWP.task = { + id = "ComboTask", + params = { + tasks = { + { + id = "Tanker", + params = {} + }, + { + id = "Orbit", + params = { + pattern = "Circle", + speed = singlePoint.speed * 0.514444, + altitude = singlePoint.altitude * 0.3048 + } + } + } + } + } + table.insert(taskRoute, orbitWP) + env.info(string.format("[TANKER] Single waypoint - added circular orbit pattern")) + end + + -- Apply route to group + spawnedGroup:Route(taskRoute) + + -- Update state + local state = TANKER_STATE[stateKey] + state.active = true + state.group = spawnedGroup + state.fuelWarned = false + state.bingoWarned = false + + -- Announce spawn with details + AnnounceTankerInfo(config, true) + + -- Start fuel monitoring + StartFuelMonitor(stateKey, config) + + -- Update menus + UpdateTankerMenus() + + return true +end + +-- ============================================================================ +-- EVENT HANDLER +-- ============================================================================ + +BlueTankerEventHandler = EVENTHANDLER:New() + +function BlueTankerEventHandler:OnEventBirth(EventData) + local groupName = EventData.IniDCSGroupName + + if groupName and string.find(groupName, "TANKER 135") then + env.info(string.format("[TANKER] Birth event: %s", groupName)) + + -- Determine which tanker spawned + local stateKey, config + if string.find(groupName, "MPRS") then + stateKey = "KC135_MPRS" + config = TANKER_CONFIG.KC135_MPRS + else + stateKey = "KC135" + config = TANKER_CONFIG.KC135 + end + + -- Update state + local state = TANKER_STATE[stateKey] + state.active = true + state.group = GROUP:FindByName(groupName) + state.fuelWarned = false + state.bingoWarned = false + + -- Announce spawn with details + AnnounceTankerInfo(config, true) + + -- Start fuel monitoring + StartFuelMonitor(stateKey, config) + + -- Update menus + UpdateTankerMenus() + end +end + +function BlueTankerEventHandler:OnEventDead(EventData) + local groupName = EventData.IniDCSGroupName + + if groupName and string.find(groupName, "TANKER 135") then + env.info(string.format("[TANKER] Dead event: %s", groupName)) + + -- Determine which tanker died + local stateKey, config, spawnFunc + if string.find(groupName, "MPRS") then + stateKey = "KC135_MPRS" + config = TANKER_CONFIG.KC135_MPRS + spawnFunc = SpawnTankerMPRS + else + stateKey = "KC135" + config = TANKER_CONFIG.KC135 + spawnFunc = SpawnTanker + end + + MESSAGE:New(GetRandomMessage("DESTROYED", config.displayName), + 15, "ALERT"):ToBlue() + + -- Clean up and schedule respawn + CleanupTankerState(stateKey) + ScheduleRespawn(stateKey, config, spawnFunc) + + -- Update menus + UpdateTankerMenus() + end +end + +function BlueTankerEventHandler:OnEventCrash(EventData) + -- Treat crash same as dead + self:OnEventDead(EventData) +end + +function BlueTankerEventHandler:OnEventEngineShutdown(EventData) + local groupName = EventData.IniDCSGroupName + + if groupName and string.find(groupName, "TANKER 135") then + env.info(string.format("[TANKER] Engine shutdown event: %s", groupName)) + + -- Determine which tanker + local stateKey, config, spawnFunc + if string.find(groupName, "MPRS") then + stateKey = "KC135_MPRS" + config = TANKER_CONFIG.KC135_MPRS + spawnFunc = SpawnTankerMPRS + else + stateKey = "KC135" + config = TANKER_CONFIG.KC135 + spawnFunc = SpawnTanker + end + + MESSAGE:New(string.format("%s has returned to base", config.displayName), + 10):ToBlue() + + -- Clean up and schedule respawn + CleanupTankerState(stateKey) + ScheduleRespawn(stateKey, config, spawnFunc) + + -- Update menus + UpdateTankerMenus() + end +end + +function BlueTankerEventHandler:OnEventHit(EventData) + local groupName = EventData.IniDCSGroupName + + if groupName and string.find(groupName, "TANKER 135") then + local config = string.find(groupName, "MPRS") and TANKER_CONFIG.KC135_MPRS or TANKER_CONFIG.KC135 + + MESSAGE:New(GetRandomMessage("TAKING_FIRE", config.displayName), + 15, "WARNING"):ToBlue() + + env.info(string.format("[TANKER] %s hit by hostile fire", config.displayName)) + end +end + +-- ============================================================================ +-- SPAWN OBJECTS AND FUNCTIONS +-- ============================================================================ + +-- Function to spawn KC-135 +function SpawnTanker() + if TANKER_STATE.KC135.active then + MESSAGE:New(GetRandomMessage("ALREADY_ACTIVE", TANKER_CONFIG.KC135.displayName), 10):ToBlue() + return + end + + env.info("[TANKER] Spawning KC-135") + local spawnedGroup = SpawnTankerFromConfig( + TANKER_CONFIG.KC135, + DEFAULT_SPAWN_COORD, + 0 -- heading north + ) + + if spawnedGroup then + ApplyDefaultOrbitRoute(spawnedGroup, DEFAULT_SPAWN_COORD, TANKER_CONFIG.KC135) + TANKER_STATE.KC135.active = true + TANKER_STATE.KC135.group = spawnedGroup + AnnounceTankerInfo(TANKER_CONFIG.KC135, true) + + -- Start fuel monitoring + StartFuelMonitor("KC135", TANKER_CONFIG.KC135) + + UpdateTankerMenus() + else + MESSAGE:New(GetRandomMessage("SPAWN_FAILURE", TANKER_CONFIG.KC135.displayName), 10, "ERROR"):ToBlue() + end +end + +-- Function to spawn KC-135 MPRS +function SpawnTankerMPRS() + if TANKER_STATE.KC135_MPRS.active then + MESSAGE:New(GetRandomMessage("ALREADY_ACTIVE", TANKER_CONFIG.KC135_MPRS.displayName), 10):ToBlue() + return + end + + env.info("[TANKER] Spawning KC-135 MPRS") + local spawnedGroup = SpawnTankerFromConfig( + TANKER_CONFIG.KC135_MPRS, + DEFAULT_SPAWN_COORD, + 0 -- heading north + ) + + if spawnedGroup then + ApplyDefaultOrbitRoute(spawnedGroup, DEFAULT_SPAWN_COORD, TANKER_CONFIG.KC135_MPRS) + TANKER_STATE.KC135_MPRS.active = true + TANKER_STATE.KC135_MPRS.group = spawnedGroup + AnnounceTankerInfo(TANKER_CONFIG.KC135_MPRS, true) + + -- Start fuel monitoring + StartFuelMonitor("KC135_MPRS", TANKER_CONFIG.KC135_MPRS) + + UpdateTankerMenus() + else + MESSAGE:New(GetRandomMessage("SPAWN_FAILURE", TANKER_CONFIG.KC135_MPRS.displayName), 10, "ERROR"):ToBlue() + end +end + +-- Function to spawn KC-135 with custom route +function SpawnCustomTanker() + SpawnCustomRouteTanker( + TANKER_CONFIG.KC135.callsign, + TANKER_CONFIG.KC135, + "KC135", + false + ) +end + +-- Function to spawn KC-135 MPRS with custom route +function SpawnCustomTankerMPRS() + SpawnCustomRouteTanker( + TANKER_CONFIG.KC135_MPRS.callsign, + TANKER_CONFIG.KC135_MPRS, + "KC135_MPRS", + false + ) +end + +-- Function to spawn emergency KC-135 with custom route +function SpawnEmergencyTanker() + -- Use emergency respawn delay + local originalDelay = TANKER_CONFIG.KC135.respawnDelay + TANKER_CONFIG.KC135.respawnDelay = TANKER_CONFIG.KC135.emergencyRespawnDelay + + local success = SpawnCustomRouteTanker( + TANKER_CONFIG.KC135.callsign, + TANKER_CONFIG.KC135, + "KC135", + true + ) + + -- Restore original delay + TANKER_CONFIG.KC135.respawnDelay = originalDelay + + return success +end + +-- Function to spawn emergency KC-135 MPRS with custom route +function SpawnEmergencyTankerMPRS() + local originalDelay = TANKER_CONFIG.KC135_MPRS.respawnDelay + TANKER_CONFIG.KC135_MPRS.respawnDelay = TANKER_CONFIG.KC135_MPRS.emergencyRespawnDelay + + local success = SpawnCustomRouteTanker( + TANKER_CONFIG.KC135_MPRS.callsign, + TANKER_CONFIG.KC135_MPRS, + "KC135_MPRS", + true + ) + + TANKER_CONFIG.KC135_MPRS.respawnDelay = originalDelay + + return success +end + +-- Function to display tanker status +function ShowTankerStatus() + local msg = "=== TANKER STATUS ===\n\n" + + -- KC-135 Status + local kc135State = TANKER_STATE.KC135 + if kc135State.active and kc135State.group and kc135State.group:IsAlive() then + local fuel = kc135State.group:GetFuel() * 100 + local coord = kc135State.group:GetCoordinate() + local alt = coord:GetLandHeight() + coord.y + msg = msg .. string.format("%s: ACTIVE\n", TANKER_CONFIG.KC135.displayName) + msg = msg .. string.format(" Fuel: %.0f%%\n", fuel) + msg = msg .. string.format(" Altitude: FL%03d\n", math.floor(alt * 3.28084 / 100)) + if TANKER_CONFIG.KC135.tacan then + msg = msg .. string.format(" TACAN: %s\n", TANKER_CONFIG.KC135.tacan) + end + if TANKER_CONFIG.KC135.frequency then + msg = msg .. string.format(" Radio: %s MHz\n", TANKER_CONFIG.KC135.frequency) + end + else + msg = msg .. string.format("%s: NOT ACTIVE\n", TANKER_CONFIG.KC135.displayName) + end + + msg = msg .. "\n" + + -- KC-135 MPRS Status + local mprsState = TANKER_STATE.KC135_MPRS + if mprsState.active and mprsState.group and mprsState.group:IsAlive() then + local fuel = mprsState.group:GetFuel() * 100 + local coord = mprsState.group:GetCoordinate() + local alt = coord:GetLandHeight() + coord.y + msg = msg .. string.format("%s: ACTIVE\n", TANKER_CONFIG.KC135_MPRS.displayName) + msg = msg .. string.format(" Fuel: %.0f%%\n", fuel) + msg = msg .. string.format(" Altitude: FL%03d\n", math.floor(alt * 3.28084 / 100)) + if TANKER_CONFIG.KC135_MPRS.tacan then + msg = msg .. string.format(" TACAN: %s\n", TANKER_CONFIG.KC135_MPRS.tacan) + end + if TANKER_CONFIG.KC135_MPRS.frequency then + msg = msg .. string.format(" Radio: %s MHz\n", TANKER_CONFIG.KC135_MPRS.frequency) + end + else + msg = msg .. string.format("%s: NOT ACTIVE\n", TANKER_CONFIG.KC135_MPRS.displayName) + end + + MESSAGE:New(msg, 25):ToBlue() +end + +-- Function to show custom route help +function ShowCustomRouteHelp() + local msg = "╔════════════════════════════════════════════╗\n" + msg = msg .. "║ TANKER MANAGEMENT SYSTEM - GUIDE ║\n" + msg = msg .. "╚════════════════════════════════════════════╝\n\n" + + msg = msg .. "━━━ QUICK START ━━━\n\n" + msg = msg .. "1. SIMPLE SPAWN:\n" + msg = msg .. " • F10 → Tanker Management → Launch KC-135\n" + msg = msg .. " • Tanker spawns at default location (FL220)\n" + msg = msg .. " • Automatically orbits and provides refueling\n\n" + + msg = msg .. "2. CUSTOM ROUTE SPAWN:\n" + msg = msg .. " • Place numbered F10 map markers\n" + msg = msg .. " • Launch from Custom Route menu\n" + msg = msg .. " • Tanker follows your waypoints\n\n" + + msg = msg .. "━━━ AVAILABLE TANKERS ━━━\n\n" + msg = msg .. string.format("• %s (SHELL)\n", TANKER_CONFIG.KC135.displayName) + msg = msg .. string.format(" TACAN: %s | Radio: %s MHz\n", + TANKER_CONFIG.KC135.tacan or "N/A", TANKER_CONFIG.KC135.frequency or "N/A") + msg = msg .. string.format(" Marker Prefix: %s\n\n", TANKER_CONFIG.KC135.callsign) + + msg = msg .. string.format("• %s (ARCO)\n", TANKER_CONFIG.KC135_MPRS.displayName) + msg = msg .. string.format(" TACAN: %s | Radio: %s MHz\n", + TANKER_CONFIG.KC135_MPRS.tacan or "N/A", TANKER_CONFIG.KC135_MPRS.frequency or "N/A") + msg = msg .. string.format(" Marker Prefix: %s\n\n", TANKER_CONFIG.KC135_MPRS.callsign) + + msg = msg .. "━━━ CUSTOM ROUTE MARKERS ━━━\n\n" + msg = msg .. "BASIC USAGE:\n" + msg = msg .. " Place markers in sequence: SHELL1, SHELL2, SHELL3\n" + msg = msg .. " Minimum 2 waypoints required\n" + msg = msg .. " Defaults: FL220 @ 330 knots\n\n" + + msg = msg .. "ADVANCED SYNTAX:\n" + msg = msg .. " SHELL1:FL180 → Altitude override\n" + msg = msg .. " SHELL2::SP300 → Speed override\n" + msg = msg .. " SHELL3:FL200:SP280 → Both overrides\n" + msg = msg .. " SHELL4:RTB → Return to nearest base\n\n" + + msg = msg .. "EXAMPLES:\n" + msg = msg .. " Simple 3-point orbit:\n" + msg = msg .. " ARCO1, ARCO2, ARCO3\n\n" + msg = msg .. " High altitude route with RTB:\n" + msg = msg .. " SHELL1:FL280, SHELL2:FL280, SHELL3:RTB\n\n" + msg = msg .. " Low-level tanker track:\n" + msg = msg .. " ARCO1:FL120:SP250, ARCO2:FL120:SP250\n\n" + + msg = msg .. "━━━ REROUTING ACTIVE TANKERS ━━━\n\n" + msg = msg .. "Change an active tanker's route mid-mission:\n" + msg = msg .. " 1. Place new waypoint markers\n" + msg = msg .. " 2. F10 → Custom Route → Reroute Active Tanker\n" + msg = msg .. " 3. Tanker immediately follows new route\n\n" + + msg = msg .. "Use cases:\n" + msg = msg .. " • Reposition for different theater\n" + msg = msg .. " • Avoid threat areas\n" + msg = msg .. " • Send tanker home (use :RTB)\n\n" + + msg = msg .. "━━━ NOTES ━━━\n\n" + msg = msg .. "• Markers are auto-deleted after use\n" + msg = msg .. "• Tankers auto-respawn after 3 minutes if lost\n" + msg = msg .. "• Use Emergency Tanker for 1-minute respawn\n" + msg = msg .. "• RTB finds nearest friendly airbase & lands\n" + msg = msg .. "• Check Tanker Status for current position/fuel\n" + + MESSAGE:New(msg, 45):ToBlue() +end + +-- Function to reroute an active tanker with new waypoints +function RerouteTanker() + if not TANKER_STATE.KC135.active or not TANKER_STATE.KC135.group then + MESSAGE:New("KC-135 is not active! Spawn it first.", 10):ToBlue() + return + end + + -- Scan for waypoint markers + local waypoints, markerIds = ScanForWaypointMarkers(TANKER_CONFIG.KC135.callsign) + + if #waypoints < ROUTE_CONFIG.minWaypoints then + MESSAGE:New(string.format("Reroute requires at least %d waypoints!\nPlace markers: %s1, %s2, etc.", + ROUTE_CONFIG.minWaypoints, TANKER_CONFIG.KC135.callsign, TANKER_CONFIG.KC135.callsign), 15, "ERROR"):ToBlue() + return + end + + -- Build new route + local routePoints = {} + local routeDesc = "" + local hasRTB = false + + for i, wp in ipairs(waypoints) do + local parsed = ParseWaypointMarker(wp.markerText, TANKER_CONFIG.KC135.defaultAltitude, TANKER_CONFIG.KC135.defaultSpeed) + + if parsed.rtb then + hasRTB = true + local lastPos = #routePoints > 0 and routePoints[#routePoints].coord or TANKER_STATE.KC135.group:GetCoordinate() + local nearestAirbase = lastPos:GetClosestAirbase(Airbase.Category.AIRDROME, coalition.side.BLUE) + + if nearestAirbase then + local airbaseName = nearestAirbase:GetName() + local airbaseCoord = nearestAirbase:GetCoordinate() + routeDesc = routeDesc .. string.format("\n WP%d: RTB to %s", i, airbaseName) + + table.insert(routePoints, { + coord = airbaseCoord, + altitude = 0, + speed = parsed.speed, + rtb = true, + airbase = nearestAirbase, + airbaseName = airbaseName + }) + end + break + else + routeDesc = routeDesc .. string.format("\n WP%d: FL%03d @ %d kts", + i, math.floor(parsed.altitude / 100), parsed.speed) + table.insert(routePoints, { + coord = wp.coordinate, + altitude = parsed.altitude, + speed = parsed.speed, + rtb = false + }) + end + end + + -- Build task route + local taskRoute = {} + for i, rp in ipairs(routePoints) do + local wp + + if rp.rtb and rp.airbase then + wp = rp.coord:WaypointAirLanding( + rp.speed * 0.514444, + rp.airbase:GetDCSObject(), + {}, + "RTB" + ) + else + wp = rp.coord:WaypointAirFlyOverPoint( + COORDINATE.WaypointAltType.BARO, + rp.speed * 0.514444, + rp.altitude * 0.3048, + {}, + "WP" .. i + ) + + if not rp.rtb then + wp.task = { + id = "ComboTask", + params = { + tasks = { + {id = "Tanker", params = {}} + } + } + } + end + end + + table.insert(taskRoute, wp) + end + + -- Apply new route + TANKER_STATE.KC135.group:Route(taskRoute) + + MESSAGE:New(string.format("%s accepting new route with %d waypoints:%s", + TANKER_CONFIG.KC135.displayName, #routePoints, routeDesc), 20):ToBlue() + + -- Delete markers + if ROUTE_CONFIG.deleteMarkersAfterUse then + for _, markerId in ipairs(markerIds) do + trigger.action.removeMark(markerId) + end + end + + env.info(string.format("[TANKER] Rerouted %s with %d waypoints", TANKER_CONFIG.KC135.displayName, #routePoints)) +end + +-- Function to reroute KC-135 MPRS +function RerouteTankerMPRS() + if not TANKER_STATE.KC135_MPRS.active or not TANKER_STATE.KC135_MPRS.group then + MESSAGE:New("KC-135 MPRS is not active! Spawn it first.", 10):ToBlue() + return + end + + local waypoints, markerIds = ScanForWaypointMarkers(TANKER_CONFIG.KC135_MPRS.callsign) + + if #waypoints < ROUTE_CONFIG.minWaypoints then + MESSAGE:New(string.format("Reroute requires at least %d waypoints!\nPlace markers: %s1, %s2, etc.", + ROUTE_CONFIG.minWaypoints, TANKER_CONFIG.KC135_MPRS.callsign, TANKER_CONFIG.KC135_MPRS.callsign), 15, "ERROR"):ToBlue() + return + end + + local routePoints = {} + local routeDesc = "" + local hasRTB = false + + for i, wp in ipairs(waypoints) do + local parsed = ParseWaypointMarker(wp.markerText, TANKER_CONFIG.KC135_MPRS.defaultAltitude, TANKER_CONFIG.KC135_MPRS.defaultSpeed) + + if parsed.rtb then + hasRTB = true + local lastPos = #routePoints > 0 and routePoints[#routePoints].coord or TANKER_STATE.KC135_MPRS.group:GetCoordinate() + local nearestAirbase = lastPos:GetClosestAirbase(Airbase.Category.AIRDROME, coalition.side.BLUE) + + if nearestAirbase then + local airbaseName = nearestAirbase:GetName() + local airbaseCoord = nearestAirbase:GetCoordinate() + routeDesc = routeDesc .. string.format("\n WP%d: RTB to %s", i, airbaseName) + + table.insert(routePoints, { + coord = airbaseCoord, + altitude = 0, + speed = parsed.speed, + rtb = true, + airbase = nearestAirbase, + airbaseName = airbaseName + }) + end + break + else + routeDesc = routeDesc .. string.format("\n WP%d: FL%03d @ %d kts", + i, math.floor(parsed.altitude / 100), parsed.speed) + table.insert(routePoints, { + coord = wp.coordinate, + altitude = parsed.altitude, + speed = parsed.speed, + rtb = false + }) + end + end + + local taskRoute = {} + for i, rp in ipairs(routePoints) do + local wp + + if rp.rtb and rp.airbase then + wp = rp.coord:WaypointAirLanding( + rp.speed * 0.514444, + rp.airbase:GetDCSObject(), + {}, + "RTB" + ) + else + wp = rp.coord:WaypointAirFlyOverPoint( + COORDINATE.WaypointAltType.BARO, + rp.speed * 0.514444, + rp.altitude * 0.3048, + {}, + "WP" .. i + ) + + if not rp.rtb then + wp.task = { + id = "ComboTask", + params = { + tasks = { + {id = "Tanker", params = {}} + } + } + } + end + end + + table.insert(taskRoute, wp) + end + + TANKER_STATE.KC135_MPRS.group:Route(taskRoute) + + MESSAGE:New(string.format("%s accepting new route with %d waypoints:%s", + TANKER_CONFIG.KC135_MPRS.displayName, #routePoints, routeDesc), 20):ToBlue() + + if ROUTE_CONFIG.deleteMarkersAfterUse then + for _, markerId in ipairs(markerIds) do + trigger.action.removeMark(markerId) + end + end + + env.info(string.format("[TANKER] Rerouted %s with %d waypoints", TANKER_CONFIG.KC135_MPRS.displayName, #routePoints)) +end + +-- ============================================================================ +-- MISSION MENU SETUP +-- ============================================================================ + +-- Create mission menu for tanker requests +-- Integrates with MenuManager to place under "Mission Options" +-- This keeps CTLD at F2 and AFAC at F3 as intended +if MenuManager and MenuManager.CreateCoalitionMenu then + -- Use MenuManager to create menu under "Mission Options" + MENU_TANKER_ROOT = MenuManager.CreateCoalitionMenu(coalition.side.BLUE, "Tanker Operations") + env.info("[TANKER] Using MenuManager - menu created under Mission Options") +else + -- Fallback: create root menu if MenuManager not available + MENU_TANKER_ROOT = MENU_COALITION:New(coalition.side.BLUE, "Tanker Operations") + env.warning("[TANKER] MenuManager not found - creating root menu (load MenuManager first!)") +end + +-- Standard tanker spawns +MENU_KC135_LAUNCH = MENU_COALITION_COMMAND:New( + coalition.side.BLUE, + "Launch " .. TANKER_CONFIG.KC135.displayName, + MENU_TANKER_ROOT, + SpawnTanker +) + +MENU_KC135_MPRS_LAUNCH = MENU_COALITION_COMMAND:New( + coalition.side.BLUE, + "Launch " .. TANKER_CONFIG.KC135_MPRS.displayName, + MENU_TANKER_ROOT, + SpawnTankerMPRS +) + +-- Custom route submenu +local MENU_CUSTOM_ROUTE = MENU_COALITION:New( + coalition.side.BLUE, + "Custom Route", + MENU_TANKER_ROOT +) + +MENU_COALITION_COMMAND:New( + coalition.side.BLUE, + "How to Use Custom Routes", + MENU_CUSTOM_ROUTE, + ShowCustomRouteHelp +) + +MENU_COALITION_COMMAND:New( + coalition.side.BLUE, + string.format("Launch %s (%s markers)", TANKER_CONFIG.KC135.displayName, TANKER_CONFIG.KC135.callsign), + MENU_CUSTOM_ROUTE, + SpawnCustomTanker +) + +MENU_COALITION_COMMAND:New( + coalition.side.BLUE, + string.format("Launch %s (%s markers)", TANKER_CONFIG.KC135_MPRS.displayName, TANKER_CONFIG.KC135_MPRS.callsign), + MENU_CUSTOM_ROUTE, + SpawnCustomTankerMPRS +) + +-- Reroute submenu for changing active tanker routes +local MENU_REROUTE = MENU_COALITION:New( + coalition.side.BLUE, + "Reroute Active Tanker", + MENU_CUSTOM_ROUTE +) + +MENU_COALITION_COMMAND:New( + coalition.side.BLUE, + string.format("Reroute %s (%s markers)", TANKER_CONFIG.KC135.displayName, TANKER_CONFIG.KC135.callsign), + MENU_REROUTE, + RerouteTanker +) + +MENU_COALITION_COMMAND:New( + coalition.side.BLUE, + string.format("Reroute %s (%s markers)", TANKER_CONFIG.KC135_MPRS.displayName, TANKER_CONFIG.KC135_MPRS.callsign), + MENU_REROUTE, + RerouteTankerMPRS +) + +-- Emergency spawns submenu +local MENU_EMERGENCY = MENU_COALITION:New( + coalition.side.BLUE, + "Emergency Tanker", + MENU_TANKER_ROOT +) + +MENU_COALITION_COMMAND:New( + coalition.side.BLUE, + string.format("Emergency %s (%s markers)", TANKER_CONFIG.KC135.displayName, TANKER_CONFIG.KC135.callsign), + MENU_EMERGENCY, + SpawnEmergencyTanker +) + +MENU_COALITION_COMMAND:New( + coalition.side.BLUE, + string.format("Emergency %s (%s markers)", TANKER_CONFIG.KC135_MPRS.displayName, TANKER_CONFIG.KC135_MPRS.callsign), + MENU_EMERGENCY, + SpawnEmergencyTankerMPRS +) + +-- Status and info +MENU_COALITION_COMMAND:New( + coalition.side.BLUE, + "Tanker Status Report", + MENU_TANKER_ROOT, + ShowTankerStatus +) + +-- ============================================================================ +-- EVENT HANDLER REGISTRATION +-- ============================================================================ + +BlueTankerEventHandler:HandleEvent(EVENTS.Birth) +BlueTankerEventHandler:HandleEvent(EVENTS.Dead) +BlueTankerEventHandler:HandleEvent(EVENTS.Crash) +BlueTankerEventHandler:HandleEvent(EVENTS.EngineShutdown) +BlueTankerEventHandler:HandleEvent(EVENTS.Hit) + +env.info("[TANKER] Tanker Management System initialized") +MESSAGE:New("Tanker Management System online - Use F10 menu to request tankers", 15):ToBlue() \ No newline at end of file