Reorganized the CTLD configuration block in Moose_CTLD.lua so related settings sit together under clear section headers for mission makers.

Introduced an “Instance & Access” section for coalition/aircraft gates, then grouped runtime/logging, menu/catalog, transport capacity, deployment rules, build controls, pickup/drop logic, autonomous assets, combat automation, visual aids, inventory, and zone tables.
Kept every existing setting (JTAC, drone spawn, inventory, etc.) while tightening inline comments to explain practical effects without altering defaults.
This commit is contained in:
iTracerFacer 2025-11-10 07:50:32 -06:00
parent df276544c5
commit c2e549c9c1
2 changed files with 571 additions and 90 deletions

View File

@ -153,6 +153,12 @@ CTLD.Messages = {
attack_base_announce = "{unit_name} deployed by {player} is moving to capture {base_name} at {brg}°, {rng} {rng_u}.", attack_base_announce = "{unit_name} deployed by {player} is moving to capture {base_name} at {brg}°, {rng} {rng_u}.",
attack_no_targets = "{unit_name} deployed by {player} found no targets within {rng} {rng_u}. Holding position.", attack_no_targets = "{unit_name} deployed by {player} found no targets within {rng} {rng_u}. Holding position.",
jtac_onstation = "JTAC {jtac} on station. CODE {code}.",
jtac_new_target = "JTAC {jtac} lasing {target}. CODE {code}. POS {grid}.",
jtac_target_lost = "JTAC {jtac} lost target. Reacquiring.",
jtac_target_destroyed = "JTAC {jtac} reports target destroyed.",
jtac_idle = "JTAC {jtac} scanning for targets.",
-- Zone restrictions -- Zone restrictions
drop_forbidden_in_pickup = "Cannot drop crates inside a Supply Zone. Move outside the zone boundary.", drop_forbidden_in_pickup = "Cannot drop crates inside a Supply Zone. Move outside the zone boundary.",
troop_deploy_forbidden_in_pickup = "Cannot deploy troops inside a Supply Zone. Move outside the zone boundary.", troop_deploy_forbidden_in_pickup = "Cannot deploy troops inside a Supply Zone. Move outside the zone boundary.",
@ -185,20 +191,39 @@ CTLD.Messages = {
-- #endregion Messaging -- #endregion Messaging
CTLD.Config = { CTLD.Config = {
-- === Instance & Access ===
CoalitionSide = coalition.side.BLUE, -- default coalition this instance serves (menus created for this side) CoalitionSide = coalition.side.BLUE, -- default coalition this instance serves (menus created for this side)
CountryId = nil, -- optional explicit country id for spawned groups; falls back per coalition CountryId = nil, -- optional explicit country id for spawned groups; falls back per coalition
AllowedAircraft = { -- transport-capable unit type names (case-sensitive as in DCS DB) AllowedAircraft = { -- transport-capable unit type names (case-sensitive as in DCS DB)
'UH-1H','Mi-8MTV2','Mi-24P','SA342M','SA342L','SA342Minigun','Ka-50','Ka-50_3','AH-64D_BLK_II','UH-60L','CH-47Fbl1','CH-47F','Mi-17','GazelleAI' 'UH-1H','Mi-8MTV2','Mi-24P','SA342M','SA342L','SA342Minigun','Ka-50','Ka-50_3','AH-64D_BLK_II','UH-60L','CH-47Fbl1','CH-47F','Mi-17','GazelleAI'
}, },
-- === Runtime & Messaging ===
-- Logging control: set the desired level of detail for env.info logging to DCS.log -- Logging control: set the desired level of detail for env.info logging to DCS.log
-- 0 = NONE - No logging at all (production servers) -- 0 = NONE - No logging at all (production servers)
-- 1 = ERROR - Only critical errors and warnings -- 1 = ERROR - Only critical errors and warnings
-- 2 = INFO - Important state changes, initialization, cleanup (default for production) -- 2 = INFO - Important state changes, initialization, cleanup (default for production)
-- 3 = VERBOSE - Detailed operational info (zone validation, menus, builds, MEDEVAC events) -- 3 = VERBOSE - Detailed operational info (zone validation, menus, builds, MEDEVAC events)
-- 4 = DEBUG - Everything including hover checks, crate pickups, detailed troop spawns -- 4 = DEBUG - Everything including hover checks, crate pickups, detailed troop spawns
LogLevel = 4, LogLevel = 4,
MessageDuration = 15, -- seconds for on-screen messages
Debug = false, -- leave false for production; enables extra debug output and draws when true
-- === Menu & Catalog ===
UseGroupMenus = true, -- if true, F10 menus per player group; otherwise coalition-wide (leave this alone)
CreateMenuAtMissionStart = false, -- creates empty root menu at mission start to reserve F10 position (populated on player spawn)
RootMenuName = 'CTLD', -- name for the root F10 menu; menu ordering depends on script load order in mission editor
UseCategorySubmenus = true, -- if true, organize crate requests by category submenu (menuCategory)
UseBuiltinCatalog = false, -- start with the shipped catalog (true) or expect mission to load its own (false)
-- === Transport Capacity ===
-- Default capacities for aircraft not listed in AircraftCapacities table
-- Used as fallback for any transport aircraft without specific limits defined
DefaultCapacity = {
maxCrates = 4, -- reasonable middle ground
maxTroops = 12, -- moderate squad size
maxWeightKg = 2000, -- default weight capacity in kg (omit to disable weight modeling)
},
-- Per-aircraft capacity limits (realistic cargo/troop capacities) -- Per-aircraft capacity limits (realistic cargo/troop capacities)
-- Set maxCrates = 0 and maxTroops = 0 for attack helicopters with no cargo capability -- Set maxCrates = 0 and maxTroops = 0 for attack helicopters with no cargo capability
-- If an aircraft type is not listed here, it will use DefaultCapacity values -- If an aircraft type is not listed here, it will use DefaultCapacity values
@ -211,21 +236,21 @@ CTLD.Config = {
['SA342L'] = { maxCrates = 1, maxTroops = 3, maxWeightKg = 400 }, ['SA342L'] = { maxCrates = 1, maxTroops = 3, maxWeightKg = 400 },
['SA342Minigun'] = { maxCrates = 1, maxTroops = 3, maxWeightKg = 400 }, ['SA342Minigun'] = { maxCrates = 1, maxTroops = 3, maxWeightKg = 400 },
['GazelleAI'] = { maxCrates = 1, maxTroops = 3, maxWeightKg = 400 }, ['GazelleAI'] = { maxCrates = 1, maxTroops = 3, maxWeightKg = 400 },
-- Attack Helicopters (no cargo capacity - combat only) -- Attack Helicopters (no cargo capacity - combat only)
['Ka-50'] = { maxCrates = 0, maxTroops = 0, maxWeightKg = 0 }, -- Black Shark - single seat attack ['Ka-50'] = { maxCrates = 0, maxTroops = 0, maxWeightKg = 0 }, -- Black Shark - single seat attack
['Ka-50_3'] = { maxCrates = 0, maxTroops = 0, maxWeightKg = 0 }, -- Black Shark 3 ['Ka-50_3'] = { maxCrates = 0, maxTroops = 0, maxWeightKg = 0 }, -- Black Shark 3
['AH-64D_BLK_II'] = { maxCrates = 0, maxTroops = 0, maxWeightKg = 0 }, -- Apache - attack/recon only ['AH-64D_BLK_II'] = { maxCrates = 0, maxTroops = 0, maxWeightKg = 0 }, -- Apache - attack/recon only
['Mi-24P'] = { maxCrates = 2, maxTroops = 8, maxWeightKg = 1000 }, -- Hind - attack helo but has small troop bay ['Mi-24P'] = { maxCrates = 2, maxTroops = 8, maxWeightKg = 1000 }, -- Hind - attack helo but has small troop bay
-- Light Utility Helicopters (moderate capacity) -- Light Utility Helicopters (moderate capacity)
['UH-1H'] = { maxCrates = 3, maxTroops = 11, maxWeightKg = 1800 }, -- Huey - classic light transport ['UH-1H'] = { maxCrates = 3, maxTroops = 11, maxWeightKg = 1800 }, -- Huey - classic light transport
-- Medium Transport Helicopters (good capacity) -- Medium Transport Helicopters (good capacity)
['Mi-8MTV2'] = { maxCrates = 5, maxTroops = 24, maxWeightKg = 4000 }, -- Hip - Russian medium transport ['Mi-8MTV2'] = { maxCrates = 5, maxTroops = 24, maxWeightKg = 4000 }, -- Hip - Russian medium transport
['Mi-17'] = { maxCrates = 5, maxTroops = 24, maxWeightKg = 4000 }, -- Hip variant ['Mi-17'] = { maxCrates = 5, maxTroops = 24, maxWeightKg = 4000 }, -- Hip variant
['UH-60L'] = { maxCrates = 4, maxTroops = 11, maxWeightKg = 4000 }, -- Black Hawk - medium utility ['UH-60L'] = { maxCrates = 4, maxTroops = 11, maxWeightKg = 4000 }, -- Black Hawk - medium utility
-- Heavy Lift Helicopters (maximum capacity) -- Heavy Lift Helicopters (maximum capacity)
['CH-47Fbl1'] = { maxCrates = 10, maxTroops = 33, maxWeightKg = 12000 }, -- Chinook - heavy lift beast ['CH-47Fbl1'] = { maxCrates = 10, maxTroops = 33, maxWeightKg = 12000 }, -- Chinook - heavy lift beast
['CH-47F'] = { maxCrates = 10, maxTroops = 33, maxWeightKg = 12000 }, -- Chinook variant ['CH-47F'] = { maxCrates = 10, maxTroops = 33, maxWeightKg = 12000 }, -- Chinook variant
@ -235,83 +260,103 @@ CTLD.Config = {
['C-130'] = { maxCrates = 20, maxTroops = 92, maxWeightKg = 20000, requireGround = true, maxGroundSpeed = 1.0 }, -- C-130 Hercules - tactical airlifter (must be fully stopped) ['C-130'] = { maxCrates = 20, maxTroops = 92, maxWeightKg = 20000, requireGround = true, maxGroundSpeed = 1.0 }, -- C-130 Hercules - tactical airlifter (must be fully stopped)
['C-17A'] = { maxCrates = 30, maxTroops = 150, maxWeightKg = 77500, requireGround = true, maxGroundSpeed = 1.0 }, -- C-17 Globemaster III - strategic airlifter ['C-17A'] = { maxCrates = 30, maxTroops = 150, maxWeightKg = 77500, requireGround = true, maxGroundSpeed = 1.0 }, -- C-17 Globemaster III - strategic airlifter
}, },
-- Default capacities for aircraft not listed in AircraftCapacities table
-- Used as fallback for any transport aircraft without specific limits defined
DefaultCapacity = {
maxCrates = 4, -- reasonable middle ground
maxTroops = 12, -- moderate squad size
maxWeightKg = 2000, -- default weight capacity in kg (omit to disable weight modeling)
},
UseGroupMenus = true, -- if true, F10 menus per player group; otherwise coalition-wide
CreateMenuAtMissionStart = false, -- if true with UseGroupMenus=true, creates empty root menu at mission start to reserve F10 position (populated on player spawn)
RootMenuName = 'CTLD', -- Name for the root F10 menu. Note: Menu ordering depends on script load order in mission editor.
UseCategorySubmenus = true, -- if true, organize crate requests by category submenu (menuCategory)
UseBuiltinCatalog = false, -- if false, starts with an empty catalog; intended when you preload a global catalog and want only that
-- Safety offsets to avoid spawning units too close to player aircraft
BuildSpawnOffset = 40, -- meters: shift build point forward from the aircraft to avoid rotor/ground collisions (0 = spawn centered on aircraft)
TroopSpawnOffset = 40, -- meters: shift troop unload point forward from the aircraft
-- Air-spawn settings for CTLD-built drones (AIRPLANE category entries in the catalog like MQ-9 / WingLoong)
DroneAirSpawn = {
Enabled = true, -- when true, AIRPLANE catalog items that opt-in can spawn in the air at a set altitude
AltitudeMeters = 3048, -- default spawn altitude ASL (meters) - 10,000 feet
SpeedMps = 120 -- default initial speed in m/s
},
DropCrateForwardOffset = 35, -- meters: drop loaded crates this far in front of the aircraft (instead of directly under)
RestrictFOBToZones = false, -- if true, recipes marked isFOB only build inside configured FOBZones
AutoBuildFOBInZones = false, -- if true, CTLD auto-builds FOB recipes when required crates are inside a FOB zone
BuildRadius = 60, -- meters around build point to collect crates
CrateLifetime = 3600, -- seconds before crates auto-clean up; 0 = disable
MessageDuration = 15, -- seconds for on-screen messages
Debug = false,
-- === Loading & Deployment Rules ===
-- Ground requirements for loading (realistic behavior) RequireGroundForTroopLoad = true, -- must be landed to load troops (prevents loading while hovering)
RequireGroundForTroopLoad = true, -- if true, must be landed to load troops (prevents loading while hovering) RequireGroundForVehicleLoad = true, -- must be landed to load vehicles (C-130/large transports)
RequireGroundForVehicleLoad = true, -- if true, must be landed to load vehicles (C-130/large transports) MaxGroundSpeedForLoading = 2.0, -- meters/second limit while loading (roughly 4 knots)
MaxGroundSpeedForLoading = 2.0, -- meters/second: max ground speed allowed for loading (prevents loading while taxiing fast; ~4 knots)
-- Fast-rope deployment (allows troop unload while hovering at safe altitude) -- Fast-rope deployment (allows troop unload while hovering at safe altitude)
EnableFastRope = true, -- if true, troops can fast-rope from hovering helicopters EnableFastRope = true, -- if true, troops can fast-rope from hovering helicopters
FastRopeMaxHeight = 20, -- meters AGL: maximum altitude for fast-rope deployment FastRopeMaxHeight = 20, -- meters AGL: maximum altitude for fast-rope deployment
FastRopeMinHeight = 5, -- meters AGL: minimum altitude for fast-rope deployment (too low = collision risk) FastRopeMinHeight = 5, -- meters AGL: minimum altitude for fast-rope deployment (too low = collision risk)
-- Safety offsets to avoid spawning units too close to player aircraft
BuildSpawnOffset = 40, -- meters: shift build point forward from the aircraft (0 = spawn centered on aircraft)
TroopSpawnOffset = 40, -- meters: shift troop unload point forward from the aircraft
DropCrateForwardOffset = 35, -- meters: drop loaded crates this far in front of the aircraft
-- === Build & Crate Handling ===
BuildRequiresGroundCrates = true, -- required crates must be on the ground (not still carried)
BuildRadius = 60, -- meters around build point to collect crates
RestrictFOBToZones = false, -- only allow FOB recipes inside configured FOBZones
AutoBuildFOBInZones = false, -- auto-build FOB recipes when required crates are inside a FOB zone
CrateLifetime = 3600, -- seconds before crates auto-clean up; 0 = disable
-- Build safety -- Build safety
BuildConfirmEnabled = false, -- require a second confirmation within a short window before building BuildConfirmEnabled = false, -- require a second confirmation within a short window before building
BuildConfirmWindowSeconds = 30, -- seconds allowed between first and second "Build Here" press BuildConfirmWindowSeconds = 30, -- seconds allowed between first and second "Build Here" press
BuildCooldownEnabled = true, -- after a successful build, impose a cooldown before allowing another build by the same group BuildCooldownEnabled = true, -- impose a cooldown before allowing another build by the same group
BuildCooldownSeconds = 60, -- seconds of cooldown after a successful build per group BuildCooldownSeconds = 60, -- seconds of cooldown after a successful build per group
-- === Pickup & Drop Zone Rules ===
RequirePickupZoneForCrateRequest = true, -- enforce that crate requests must be near a Supply (Pickup) Zone
RequirePickupZoneForTroopLoad = true, -- troops can only be loaded while inside a Supply (Pickup) Zone
PickupZoneMaxDistance = 10000, -- meters; nearest pickup zone must be within this distance to allow a request
ForbidDropsInsidePickupZones = true, -- block crate drops while inside a Pickup Zone
ForbidTroopDeployInsidePickupZones = true, -- block troop deploy while inside a Pickup Zone
ForbidChecksActivePickupOnly = true, -- when true, restriction applies only to ACTIVE pickup zones; false blocks all configured pickup zones
-- Dynamic Drop Zone settings
DropZoneRadius = 250, -- meters: radius used when creating a Drop Zone via the admin menu at player position
MinDropZoneDistanceFromPickup = 2000, -- meters: minimum distance from nearest Pickup Zone required to create a dynamic Drop Zone (0 to disable)
MinDropDistanceActivePickupOnly = true, -- when true, only ACTIVE pickup zones are considered for the minimum distance check
-- === Pickup Zone Spawn Placement ===
PickupZoneSpawnRandomize = true, -- spawn crates at a random point within the pickup zone (avoids stacking)
PickupZoneSpawnEdgeBuffer = 10, -- meters: keep spawns at least this far inside the zone edge
PickupZoneSpawnMinOffset = 100, -- meters: keep spawns at least this far from the exact center
CrateSpawnMinSeparation = 7, -- meters: try not to place a new crate closer than this to an existing one
CrateSpawnSeparationTries = 6, -- attempts to find a non-overlapping position before accepting best effort
PickupZoneSmokeColor = trigger.smokeColor.Green, -- default smoke color when spawning crates at pickup zones PickupZoneSmokeColor = trigger.smokeColor.Green, -- default smoke color when spawning crates at pickup zones
-- Crate Smoke Settings -- Crate Smoke Settings
-- NOTE: Individual smoke effects last ~5 minutes (DCS hardcoded, cannot be changed) -- NOTE: Individual smoke effects last ~5 minutes (DCS hardcoded, cannot be changed)
-- These settings control whether/how often NEW smoke is spawned, not how long each smoke lasts -- These settings control whether/how often NEW smoke is spawned, not how long each smoke lasts
CrateSmoke = { CrateSmoke = {
Enabled = true, -- if true, spawn smoke when crates are created; if false, no smoke at all Enabled = true, -- spawn smoke when crates are created; if false, no smoke at all
AutoRefresh = false, -- if true, automatically spawn new smoke every RefreshInterval seconds (creates continuous smoke) AutoRefresh = false, -- automatically spawn new smoke every RefreshInterval seconds
RefreshInterval = 240, -- seconds: how often to spawn new smoke (only used if AutoRefresh = true; 240s = 4min recommended) RefreshInterval = 240, -- seconds: how often to spawn new smoke (only used if AutoRefresh = true)
MaxRefreshDuration = 600, -- seconds: stop auto-refresh after this long (safety limit; 600s = 10min; set high or disable AutoRefresh for one-time smoke) MaxRefreshDuration = 600, -- seconds: stop auto-refresh after this long (safety limit)
OffsetMeters = 0, -- meters: horizontal offset from crate so helicopters don't hover in smoke (0 = directly on crate) OffsetMeters = 0, -- meters: horizontal offset from crate so helicopters don't hover in smoke
OffsetRandom = true, -- if true, randomize horizontal offset direction; if false, always offset north OffsetRandom = true, -- if true, randomize horizontal offset direction; if false, always offset north
OffsetVertical = 20, -- meters: vertical offset above ground level (helps smoke be more visible; 2-3 recommended) OffsetVertical = 20, -- meters: vertical offset above ground level (helps smoke be more visible)
}, },
RequirePickupZoneForCrateRequest = true, -- enforce that crate requests must be near a Supply (Pickup) Zone
RequirePickupZoneForTroopLoad = true, -- if true, troops can only be loaded while inside a Supply (Pickup) Zone
PickupZoneMaxDistance = 10000, -- meters; nearest pickup zone must be within this distance to allow a request
-- Safety rules around Supply (Pickup) Zones
ForbidDropsInsidePickupZones = true, -- if true, players cannot drop crates while inside a Pickup Zone
ForbidTroopDeployInsidePickupZones = true, -- if true, players cannot deploy troops while inside a Pickup Zone
ForbidChecksActivePickupOnly = true, -- when true, restriction applies only to ACTIVE pickup zones; set false to block inside any configured pickup zone
-- Dynamic Drop Zone settings -- === Autonomous Assets ===
DropZoneRadius = 250, -- meters: radius used when creating a Drop Zone via the admin menu at player position -- Air-spawn settings for CTLD-built drones (AIRPLANE catalog entries like MQ-9 / WingLoong)
MinDropZoneDistanceFromPickup = 2000, -- meters: minimum distance from nearest Pickup Zone required to create a dynamic Drop Zone (0 to disable) DroneAirSpawn = {
MinDropDistanceActivePickupOnly = true, -- when true, only ACTIVE pickup zones are considered for the minimum distance check Enabled = true, -- when true, AIRPLANE catalog items that opt-in can spawn in the air at a set altitude
AltitudeMeters = 5000, -- default spawn altitude ASL (meters)
-- Attack/Defend AI behavior for deployed troops and built vehicles SpeedMps = 120 -- default initial speed in m/s
},
JTAC = {
Enabled = true,
AutoLase = {
Enabled = true,
SearchRadius = 8000, -- meters to scan for enemy targets
RefreshSeconds = 15, -- seconds between active target updates
IdleRescanSeconds = 30, -- seconds between scans when no target locked
LostRetrySeconds = 10, -- wait before trying to reacquire after transport/line-of-sight loss
TransportHoldSeconds = 10, -- defer lase loop while JTAC is in transport (group empty)
},
Smoke = {
Enabled = true,
ColorBlue = trigger.smokeColor.Orange,
ColorRed = trigger.smokeColor.Green,
RefreshSeconds = 300, -- seconds between smoke refreshes on active targets
OffsetMeters = 5, -- random offset radius for smoke placement
},
LaserCodes = { '1688','1677','1666','1113','1115','1111' },
LockType = 'all', -- 'all' | 'vehicle' | 'troop'
Announcements = {
Enabled = true,
Duration = 10,
},
},
-- === Combat Automation ===
AttackAI = { AttackAI = {
Enabled = true, -- master switch for attack behavior Enabled = true, -- master switch for attack behavior
TroopSearchRadius = 3000, -- meters: when deploying troops with Attack, search radius for targets/bases TroopSearchRadius = 3000, -- meters: when deploying troops with Attack, search radius for targets/bases
@ -320,14 +365,15 @@ CTLD.Config = {
TroopAdvanceSpeedKmh = 20, -- movement speed for troops when ordered to attack TroopAdvanceSpeedKmh = 20, -- movement speed for troops when ordered to attack
VehicleAdvanceSpeedKmh = 35, -- movement speed for vehicles when ordered to attack VehicleAdvanceSpeedKmh = 35, -- movement speed for vehicles when ordered to attack
}, },
-- === Visual Aids ===
-- Optional: draw zones on the F10 map using trigger.action.* markup (ME Draw-like) -- Optional: draw zones on the F10 map using trigger.action.* markup (ME Draw-like)
MapDraw = { MapDraw = {
Enabled = true, -- master switch for any map drawings created by this script Enabled = true, -- master switch for any map drawings created by this script
DrawPickupZones = true, -- draw Pickup/Supply zones as shaded circles with labels DrawPickupZones = true, -- draw Pickup/Supply zones as shaded circles with labels
DrawDropZones = true, -- optionally draw Drop zones DrawDropZones = true, -- optionally draw Drop zones
DrawFOBZones = true, -- optionally draw FOB zones DrawFOBZones = true, -- optionally draw FOB zones
DrawMASHZones = true, -- optionally draw MASH (medical) zones DrawMASHZones = true, -- optionally draw MASH (medical) zones
FontSize = 18, -- label text size FontSize = 18, -- label text size
ReadOnly = true, -- prevent clients from removing the shapes ReadOnly = true, -- prevent clients from removing the shapes
ForAll = false, -- if true, draw shapes to all (-1) instead of coalition only (useful for testing/briefing) ForAll = false, -- if true, draw shapes to all (-1) instead of coalition only (useful for testing/briefing)
@ -360,32 +406,24 @@ CTLD.Config = {
} }
}, },
-- Crate spawn placement within pickup zones -- === Inventory & Troops ===
PickupZoneSpawnRandomize = true, -- if true, spawn crates at a random point within the pickup zone (avoids stacking)
PickupZoneSpawnEdgeBuffer = 10, -- meters: keep spawns at least this far inside the zone edge
PickupZoneSpawnMinOffset = 100, -- meters: keep spawns at least this far from the exact center
CrateSpawnMinSeparation = 7, -- meters: try not to place a new crate closer than this to an existing one
CrateSpawnSeparationTries = 6, -- attempts to find a non-overlapping position before accepting best effort
BuildRequiresGroundCrates = true, -- if true, all required crates must be on the ground (won't count/consume carried crates)
-- Inventory system (per pickup zone and FOBs) -- Inventory system (per pickup zone and FOBs)
Inventory = { Inventory = {
Enabled = true, -- master switch for per-location stock control Enabled = true, -- master switch for per-location stock control
FOBStockFactor = 0.50, -- starting stock at newly built FOBs relative to pickup-zone initialStock FOBStockFactor = 0.50, -- starting stock at newly built FOBs relative to pickup-zone initialStock
ShowStockInMenu = true, -- if true, append simple stock hints to menu labels (per current nearest zone) ShowStockInMenu = true, -- append simple stock hints to menu labels (per current nearest zone)
HideZeroStockMenu = false, -- removed: previously created an "In Stock Here" submenu; now disabled by default HideZeroStockMenu = false, -- removed: previously created an "In Stock Here" submenu; now disabled by default
}, },
-- Troop type presets (menu-driven loadable teams) -- Troop type presets (menu-driven loadable teams)
Troops = { Troops = {
-- Default troop type to use when no specific type is chosen DefaultType = 'AS', -- default troop type to use when no specific type is chosen
DefaultType = 'AS',
-- Team definitions: loaded from catalog via _CTLD_TROOP_TYPES global -- Team definitions: loaded from catalog via _CTLD_TROOP_TYPES global
-- If no catalog is loaded, empty table is used (and fallback logic applies) -- If no catalog is loaded, empty table is used (and fallback logic applies)
TroopTypes = {}, TroopTypes = {},
}, },
-- Zones (Supply/Pickup, Drop, FOB, MASH) -- === Zone Tables ===
-- Mission makers should populate these arrays with zone definitions -- Mission makers should populate these arrays with zone definitions
-- Each zone entry can be: { name = 'ZoneName' } or { name = 'ZoneName', flag = 9001, activeWhen = 0, smoke = color, radius = meters } -- Each zone entry can be: { name = 'ZoneName' } or { name = 'ZoneName', flag = 9001, activeWhen = 0, smoke = color, radius = meters }
Zones = { Zones = {
@ -1098,6 +1136,11 @@ CTLD._spatialGridSize = 500 -- meters per grid cell (tunable based on hover pic
-- Inventory state -- Inventory state
CTLD._stockByZone = CTLD._stockByZone or {} -- [zoneName] = { [crateKey] = count } CTLD._stockByZone = CTLD._stockByZone or {} -- [zoneName] = { [crateKey] = count }
CTLD._inStockMenus = CTLD._inStockMenus or {} -- per-group filtered menu handles CTLD._inStockMenus = CTLD._inStockMenus or {} -- per-group filtered menu handles
CTLD._jtacReservedCodes = CTLD._jtacReservedCodes or {
[coalition.side.BLUE] = {},
[coalition.side.RED] = {},
[coalition.side.NEUTRAL] = {},
}
-- MEDEVAC state -- MEDEVAC state
CTLD._medevacCrews = CTLD._medevacCrews or {} -- [crewGroupName] = { vehicleType, side, spawnTime, position, salvageValue, markerID, originalHeading, requestTime, warningsSent } CTLD._medevacCrews = CTLD._medevacCrews or {} -- [crewGroupName] = { vehicleType, side, spawnTime, position, salvageValue, markerID, originalHeading, requestTime, warningsSent }
CTLD._salvagePoints = CTLD._salvagePoints or {} -- [coalition.side] = points (global pool) CTLD._salvagePoints = CTLD._salvagePoints or {} -- [coalition.side] = points (global pool)
@ -1180,6 +1223,67 @@ local function _isIn(list, value)
return false return false
end end
local function _vec3(x, y, z)
return { x = x, y = y, z = z }
end
local function _distance3d(a, b)
if not a or not b then return math.huge end
local dx = (a.x or 0) - (b.x or 0)
local dy = (a.y or 0) - (b.y or 0)
local dz = (a.z or 0) - (b.z or 0)
return math.sqrt(dx * dx + dy * dy + dz * dz)
end
local function _unitHasAttribute(unit, attr)
if not unit or not attr then return false end
local ok, res = pcall(function() return unit:hasAttribute(attr) end)
return ok and res == true
end
local function _isDcsInfantry(unit)
if not unit then return false end
local tn = string.lower(unit:getTypeName() or '')
if tn:find('infantry') or tn:find('soldier') or tn:find('paratrooper') or tn:find('manpad') then
return true
end
return _unitHasAttribute(unit, 'Infantry')
end
local function _hasLineOfSight(fromPos, toPos)
if not (fromPos and toPos) then return false end
local p1 = _vec3(fromPos.x, (fromPos.y or 0) + 2.0, fromPos.z)
local p2 = _vec3(toPos.x, (toPos.y or 0) + 2.0, toPos.z)
local ok, visible = pcall(function() return land.isVisible(p1, p2) end)
return ok and visible == true
end
local function _jtacTargetScore(unit)
if not unit then return -1 end
if _unitHasAttribute(unit, 'SAM SR') or _unitHasAttribute(unit, 'SAM TR') or _unitHasAttribute(unit, 'SAM CC') or _unitHasAttribute(unit, 'SAM LN') then
return 120
end
if _unitHasAttribute(unit, 'Air Defence') or _unitHasAttribute(unit, 'AAA') then
return 100
end
if _unitHasAttribute(unit, 'IR Guided SAM') or _unitHasAttribute(unit, 'SAM') then
return 95
end
if _unitHasAttribute(unit, 'Artillery') or _unitHasAttribute(unit, 'MLRS') then
return 80
end
if _unitHasAttribute(unit, 'Armor') or _unitHasAttribute(unit, 'Tanks') then
return 70
end
if _unitHasAttribute(unit, 'APC') or _unitHasAttribute(unit, 'IFV') then
return 60
end
if _isDcsInfantry(unit) then
return 20
end
return 40
end
local function _msgGroup(group, text, t) local function _msgGroup(group, text, t)
if not group then return end if not group then return end
MESSAGE:New(text, t or CTLD.Config.MessageDuration):ToGroup(group) MESSAGE:New(text, t or CTLD.Config.MessageDuration):ToGroup(group)
@ -2291,6 +2395,7 @@ function CTLD:New(cfg)
o.Config.CountryId = o.CountryId o.Config.CountryId = o.CountryId
o.MenuRoots = {} o.MenuRoots = {}
o.MenusByGroup = {} o.MenusByGroup = {}
o._jtacRegistry = {}
-- If caller disabled builtin catalog, clear it before merging any globals -- If caller disabled builtin catalog, clear it before merging any globals
if o.Config.UseBuiltinCatalog == false then if o.Config.UseBuiltinCatalog == false then
@ -2441,6 +2546,18 @@ function CTLD:New(cfg)
end, {}, checkInterval, checkInterval) end, {}, checkInterval, checkInterval)
end end
if o.Config.JTAC and o.Config.JTAC.Enabled then
local jtacInterval = 5
if o.Config.JTAC.AutoLase then
local refresh = tonumber(o.Config.JTAC.AutoLase.RefreshSeconds) or 15
local idle = tonumber(o.Config.JTAC.AutoLase.IdleRescanSeconds) or 30
jtacInterval = math.max(2, math.min(refresh, idle, 10))
end
o.JTACSched = SCHEDULER:New(nil, function()
o:_tickJTACs()
end, {}, jtacInterval, jtacInterval)
end
table.insert(CTLD._instances, o) table.insert(CTLD._instances, o)
_msgCoalition(o.Side, string.format('CTLD %s initialized for coalition', CTLD.Version)) _msgCoalition(o.Side, string.format('CTLD %s initialized for coalition', CTLD.Version))
return o return o
@ -4100,6 +4217,7 @@ function CTLD:BuildSpecificAtGroup(group, recipeKey, opts)
_eventSend(self, group, nil, 'build_started', { build = def.description or recipeKey }) _eventSend(self, group, nil, 'build_started', { build = def.description or recipeKey })
local g = _coalitionAddGroup(def.side or self.Side, def.category or Group.Category.GROUND, gdata, self.Config) local g = _coalitionAddGroup(def.side or self.Side, def.category or Group.Category.GROUND, gdata, self.Config)
if not g then _eventSend(self, group, nil, 'build_failed', { reason = 'DCS group spawn error' }); return end if not g then _eventSend(self, group, nil, 'build_failed', { reason = 'DCS group spawn error' }); return end
self:_maybeRegisterJTAC(recipeKey, def, g)
for reqKey,qty in pairs(def.requires) do consumeCrates(reqKey, qty or 0) end for reqKey,qty in pairs(def.requires) do consumeCrates(reqKey, qty or 0) end
_eventSend(self, nil, self.Side, 'build_success_coalition', { build = def.description or recipeKey, player = _playerNameFromGroup(group) }) _eventSend(self, nil, self.Side, 'build_success_coalition', { build = def.description or recipeKey, player = _playerNameFromGroup(group) })
if def.isFOB then pcall(function() self:_CreateFOBPickupZone({ x = spawnAt.x, z = spawnAt.z }, def, hdg) end) end if def.isFOB then pcall(function() self:_CreateFOBPickupZone({ x = spawnAt.x, z = spawnAt.z }, def, hdg) end) end
@ -4134,6 +4252,7 @@ function CTLD:BuildSpecificAtGroup(group, recipeKey, opts)
_eventSend(self, group, nil, 'build_started', { build = def.description or recipeKey }) _eventSend(self, group, nil, 'build_started', { build = def.description or recipeKey })
local g = _coalitionAddGroup(def.side or self.Side, def.category or Group.Category.GROUND, gdata, self.Config) local g = _coalitionAddGroup(def.side or self.Side, def.category or Group.Category.GROUND, gdata, self.Config)
if not g then _eventSend(self, group, nil, 'build_failed', { reason = 'DCS group spawn error' }); return end if not g then _eventSend(self, group, nil, 'build_failed', { reason = 'DCS group spawn error' }); return end
self:_maybeRegisterJTAC(recipeKey, def, g)
consumeCrates(recipeKey, need) consumeCrates(recipeKey, need)
_eventSend(self, nil, self.Side, 'build_success_coalition', { build = def.description or recipeKey, player = _playerNameFromGroup(group) }) _eventSend(self, nil, self.Side, 'build_success_coalition', { build = def.description or recipeKey, player = _playerNameFromGroup(group) })
-- behavior -- behavior
@ -4161,6 +4280,357 @@ function CTLD:BuildSpecificAtGroup(group, recipeKey, opts)
end end
end end
function CTLD:_definitionIsJTAC(def)
if not def then return false end
if def.isJTAC == true then return true end
if type(def.jtac) == 'table' and def.jtac.enabled ~= false then return true end
if type(def.roles) == 'table' then
for _, role in ipairs(def.roles) do
if tostring(role):upper() == 'JTAC' then
return true
end
end
end
return false
end
function CTLD:_maybeRegisterJTAC(recipeKey, def, dcsGroup)
if not (self.Config.JTAC and self.Config.JTAC.Enabled) then return end
if not self:_definitionIsJTAC(def) then return end
if not dcsGroup then return end
self:_registerJTACGroup(recipeKey, def, dcsGroup)
end
function CTLD:_reserveJTACCode(side, groupName)
local pool = self.Config.JTAC and self.Config.JTAC.LaserCodes or { '1688' }
if not CTLD._jtacReservedCodes then
CTLD._jtacReservedCodes = { [coalition.side.BLUE] = {}, [coalition.side.RED] = {}, [coalition.side.NEUTRAL] = {} }
end
CTLD._jtacReservedCodes[side] = CTLD._jtacReservedCodes[side] or {}
for _, code in ipairs(pool) do
code = tostring(code)
if not CTLD._jtacReservedCodes[side][code] then
CTLD._jtacReservedCodes[side][code] = groupName
return code
end
end
local fallback = tostring(pool[1] or '1688')
_logVerbose(string.format('JTAC laser code pool exhausted for side %s, reusing %s', tostring(side), fallback))
return fallback
end
function CTLD:_releaseJTACCode(side, code, groupName)
if not code then return end
code = tostring(code)
if CTLD._jtacReservedCodes and CTLD._jtacReservedCodes[side] then
if CTLD._jtacReservedCodes[side][code] == groupName then
CTLD._jtacReservedCodes[side][code] = nil
end
end
end
function CTLD:_registerJTACGroup(recipeKey, def, dcsGroup)
if not (dcsGroup and dcsGroup.getName) then return end
local groupName = dcsGroup:getName()
if not groupName then return end
self:_cleanupJTACEntry(groupName) -- ensure stale entry cleared
local side = dcsGroup:getCoalition() or self.Side
local code = self:_reserveJTACCode(side, groupName)
local platform = 'ground'
if def and def.jtac and def.jtac.platform then
platform = tostring(def.jtac.platform)
elseif def and def.category == Group.Category.AIRPLANE then
platform = 'air'
end
local cfgSmoke = self.Config.JTAC and self.Config.JTAC.Smoke or {}
local smokeColor = (side == coalition.side.BLUE) and cfgSmoke.ColorBlue or cfgSmoke.ColorRed
local entry = {
groupName = groupName,
recipeKey = recipeKey,
def = def,
side = side,
code = code,
platform = platform,
smokeColor = smokeColor,
nextScan = timer.getTime() + 2,
smokeNext = 0,
lockType = def and def.jtac and def.jtac.lock,
}
local friendlyName = (def and self:_friendlyNameForKey(recipeKey)) or groupName
entry.displayName = friendlyName
entry.lastState = 'onstation'
self._jtacRegistry[groupName] = entry
self:_announceJTAC('jtac_onstation', entry, {
jtac = friendlyName,
code = code,
})
_logInfo(string.format('JTAC %s registered (code %s)', groupName, code))
end
function CTLD:_announceJTAC(msgKey, entry, payload)
if not entry then return end
local cfg = self.Config.JTAC and self.Config.JTAC.Announcements
if not (cfg and cfg.Enabled ~= false) then return end
local tpl = CTLD.Messages[msgKey]
if not tpl then return end
local data = payload or {}
data.jtac = data.jtac or entry.displayName or entry.groupName
data.code = data.code or entry.code
local text = _fmtTemplate(tpl, data)
if text and text ~= '' then
_msgCoalition(entry.side or self.Side, text, cfg.Duration or self.Config.MessageDuration)
end
end
function CTLD:_cleanupJTACEntry(groupName)
local entry = self._jtacRegistry and self._jtacRegistry[groupName]
if not entry then return end
self:_cancelJTACSpots(entry)
self:_releaseJTACCode(entry.side or self.Side, entry.code, groupName)
self._jtacRegistry[groupName] = nil
end
function CTLD:_cancelJTACSpots(entry)
if not entry then return end
if entry.laserSpot then
pcall(function() Spot.destroy(entry.laserSpot) end)
entry.laserSpot = nil
end
if entry.irSpot then
pcall(function() Spot.destroy(entry.irSpot) end)
entry.irSpot = nil
end
end
function CTLD:_tickJTACs()
if not self._jtacRegistry then return end
if not next(self._jtacRegistry) then return end
local now = timer.getTime()
for groupName, entry in pairs(self._jtacRegistry) do
if not entry.nextScan or now >= entry.nextScan then
local ok, err = pcall(function()
self:_processJTACEntry(groupName, entry, now)
end)
if not ok then
_logError(string.format('JTAC tick error for %s: %s', tostring(groupName), tostring(err)))
entry.nextScan = now + 10
end
end
end
end
function CTLD:_processJTACEntry(groupName, entry, now)
local cfg = self.Config.JTAC or {}
local autoCfg = cfg.AutoLase or {}
if autoCfg.Enabled == false then
self:_cancelJTACSpots(entry)
entry.nextScan = now + 30
return
end
local group = Group.getByName(groupName)
if not group or not group:isExist() then
self:_cleanupJTACEntry(groupName)
return
end
local units = group:getUnits() or {}
if #units == 0 then
self:_cancelJTACSpots(entry)
entry.nextScan = now + (autoCfg.TransportHoldSeconds or 10)
return
end
local jtacUnit = units[1]
if not jtacUnit or jtacUnit:getLife() <= 0 or not jtacUnit:isActive() then
self:_cleanupJTACEntry(groupName)
return
end
entry.jtacUnitName = entry.jtacUnitName or jtacUnit:getName()
entry.displayName = entry.displayName or entry.jtacUnitName or groupName
local jtacPoint = jtacUnit:getPoint()
local searchRadius = tonumber(autoCfg.SearchRadius) or 8000
local current = entry.currentTarget
local targetUnit = nil
local targetStatus = nil
if current and current.name then
local candidate = Unit.getByName(current.name)
if candidate and candidate:isExist() and candidate:getLife() > 0 then
local tgtPoint = candidate:getPoint()
local dist = _distance3d(tgtPoint, jtacPoint)
if dist <= searchRadius and _hasLineOfSight(jtacPoint, tgtPoint) then
targetUnit = candidate
current.lastSeen = now
current.distance = dist
else
targetStatus = 'lost'
end
else
targetStatus = 'destroyed'
end
if targetStatus then
if targetStatus == 'destroyed' then
if entry.lastState ~= 'destroyed' then
self:_announceJTAC('jtac_target_destroyed', entry, {
jtac = entry.displayName,
target = current.label or current.name,
code = entry.code,
})
entry.lastState = 'destroyed'
end
else
if entry.lastState ~= 'lost' then
self:_announceJTAC('jtac_target_lost', entry, {
jtac = entry.displayName,
target = current.label or current.name,
})
entry.lastState = 'lost'
end
end
entry.currentTarget = nil
targetUnit = nil
self:_cancelJTACSpots(entry)
entry.nextScan = now + (targetStatus == 'lost' and (autoCfg.LostRetrySeconds or 10) or 5)
end
end
if not targetUnit then
local lockPref = entry.lockType or cfg.LockType or 'all'
local selection = self:_findJTACNewTarget(entry, jtacPoint, searchRadius, lockPref)
if selection then
targetUnit = selection.unit
entry.currentTarget = {
name = targetUnit:getName(),
label = targetUnit:getTypeName(),
firstSeen = now,
lastSeen = now,
distance = selection.distance,
}
local grid = self:_GetMGRSString(targetUnit:getPoint())
local newState = 'target:'..(entry.currentTarget.name or '')
if entry.lastState ~= newState then
self:_announceJTAC('jtac_new_target', entry, {
jtac = entry.displayName,
target = targetUnit:getTypeName(),
code = entry.code,
grid = grid,
})
entry.lastState = newState
end
end
end
if targetUnit then
self:_updateJTACSpots(entry, jtacUnit, targetUnit)
entry.nextScan = now + (autoCfg.RefreshSeconds or 15)
else
self:_cancelJTACSpots(entry)
entry.nextScan = now + (autoCfg.IdleRescanSeconds or 30)
if entry.lastState ~= 'idle' then
self:_announceJTAC('jtac_idle', entry, {
jtac = entry.displayName,
})
entry.lastState = 'idle'
end
end
end
function CTLD:_findJTACNewTarget(entry, jtacPoint, radius, lockType)
local enemy = _enemySide(entry and entry.side or self.Side)
local best
local lock = (lockType or 'all'):lower()
local ok, groups = pcall(function()
return coalition.getGroups(enemy, Group.Category.GROUND) or {}
end)
if not ok then
groups = {}
end
for _, grp in ipairs(groups) do
if grp and grp:isExist() then
local units = grp:getUnits()
if units then
for _, unit in ipairs(units) do
if unit and unit:isExist() and unit:isActive() and unit:getLife() > 0 then
local skip = false
if lock == 'troop' and not _isDcsInfantry(unit) then skip = true end
if lock == 'vehicle' and _isDcsInfantry(unit) then skip = true end
if not skip then
local pos = unit:getPoint()
local dist = _distance3d(pos, jtacPoint)
if dist <= radius and _hasLineOfSight(jtacPoint, pos) then
local score = _jtacTargetScore(unit)
if not best or score > best.score or (score == best.score and dist < best.distance) then
best = { unit = unit, score = score, distance = dist }
end
end
end
end
end
end
end
end
return best
end
function CTLD:_updateJTACSpots(entry, jtacUnit, targetUnit)
if not (entry and jtacUnit and targetUnit) then return end
local codeNumber = tonumber(entry.code) or 1688
local targetPoint = targetUnit:getPoint()
targetPoint = _vec3(targetPoint.x, targetPoint.y + 2.0, targetPoint.z)
local origin = { x = 0, y = 2.0, z = 0 }
if not entry.laserSpot or not entry.irSpot then
local ok, res = pcall(function()
local spots = {}
spots.ir = Spot.createInfraRed(jtacUnit, origin, targetPoint)
spots.laser = Spot.createLaser(jtacUnit, origin, targetPoint, codeNumber)
return spots
end)
if ok and res then
entry.irSpot = entry.irSpot or res.ir
entry.laserSpot = entry.laserSpot or res.laser
else
_logError(string.format('JTAC spot create failed for %s: %s', tostring(entry.groupName), tostring(res)))
end
else
pcall(function()
if entry.laserSpot and entry.laserSpot.setPoint then entry.laserSpot:setPoint(targetPoint) end
if entry.irSpot and entry.irSpot.setPoint then entry.irSpot:setPoint(targetPoint) end
end)
end
local smokeCfg = self.Config.JTAC and self.Config.JTAC.Smoke or {}
if smokeCfg.Enabled then
local now = timer.getTime()
if not entry.smokeNext or now >= entry.smokeNext then
local color = entry.smokeColor or smokeCfg.ColorBlue or trigger.smokeColor.White
local pos = targetUnit:getPoint()
local offset = tonumber(smokeCfg.OffsetMeters) or 0
if offset > 0 then
local ang = math.random() * math.pi * 2
pos.x = pos.x + math.cos(ang) * offset
pos.z = pos.z + math.sin(ang) * offset
end
pcall(function()
trigger.action.smoke({ x = pos.x, y = pos.y, z = pos.z }, color)
end)
entry.smokeNext = now + (smokeCfg.RefreshSeconds or 300)
end
end
end
function CTLD:BuildCoalitionMenus(root) function CTLD:BuildCoalitionMenus(root)
-- Optional: implement coalition-level crate spawns at pickup zones -- Optional: implement coalition-level crate spawns at pickup zones
for key,_ in pairs(self.Config.CrateCatalog) do for key,_ in pairs(self.Config.CrateCatalog) do
@ -8048,6 +8518,17 @@ function CTLD:Cleanup()
CTLD._msgState = {} CTLD._msgState = {}
CTLD._buildConfirm = {} CTLD._buildConfirm = {}
CTLD._buildCooldown = {} CTLD._buildCooldown = {}
CTLD._jtacReservedCodes = { [coalition.side.BLUE] = {}, [coalition.side.RED] = {}, [coalition.side.NEUTRAL] = {} }
if self.JTACSched then
pcall(function() self.JTACSched:Stop() end)
self.JTACSched = nil
end
if self._jtacRegistry then
for groupName in pairs(self._jtacRegistry) do
self:_cleanupJTACEntry(groupName)
end
self._jtacRegistry = {}
end
_logInfo('Cleanup complete') _logInfo('Cleanup complete')
end end

View File

@ -119,13 +119,13 @@ cat['RED_T72B3'] = { menuCategory='Combat Vehicles', menu='T-72B3',
cat['RED_T90M'] = { menuCategory='Combat Vehicles', menu='T-90M', description='T-90M', dcsCargoType='container_cargo', required=3, initialStock=8, side=RED, category=Group.Category.GROUND, build=singleUnit('CHAP_T90M'), unitType='CHAP_T90M', MEDEVAC=true, salvageValue=3, crewSize=3 } cat['RED_T90M'] = { menuCategory='Combat Vehicles', menu='T-90M', description='T-90M', dcsCargoType='container_cargo', required=3, initialStock=8, side=RED, category=Group.Category.GROUND, build=singleUnit('CHAP_T90M'), unitType='CHAP_T90M', MEDEVAC=true, salvageValue=3, crewSize=3 }
-- Support (BLUE) -- Support (BLUE)
cat['BLUE_MRAP_JTAC'] = { menuCategory='Support', menu='MRAP - JTAC', description='JTAC MRAP', dcsCargoType='container_cargo', required=1, initialStock=12, side=BLUE, category=Group.Category.GROUND, build=singleUnit('MaxxPro_MRAP'), MEDEVAC=true, salvageValue=1, crewSize=4 } cat['BLUE_MRAP_JTAC'] = { menuCategory='Support', menu='MRAP - JTAC', description='JTAC MRAP', dcsCargoType='container_cargo', required=1, initialStock=12, side=BLUE, category=Group.Category.GROUND, build=singleUnit('MaxxPro_MRAP'), MEDEVAC=true, salvageValue=1, crewSize=4, roles={'JTAC'}, jtac={ platform='ground' } }
cat['BLUE_M818_AMMO'] = { menuCategory='Support', menu='M-818 Ammo Truck', description='M-818 Ammo Truck', dcsCargoType='container_cargo', required=1, initialStock=12, side=BLUE, category=Group.Category.GROUND, build=singleUnit('M 818'), salvageValue=1, crewSize=2 } cat['BLUE_M818_AMMO'] = { menuCategory='Support', menu='M-818 Ammo Truck', description='M-818 Ammo Truck', dcsCargoType='container_cargo', required=1, initialStock=12, side=BLUE, category=Group.Category.GROUND, build=singleUnit('M 818'), salvageValue=1, crewSize=2 }
cat['BLUE_M978_TANKER'] = { menuCategory='Support', menu='M-978 Tanker', description='M-978 Tanker', dcsCargoType='container_cargo', required=1, initialStock=10, side=BLUE, category=Group.Category.GROUND, build=singleUnit('M978 HEMTT Tanker'), salvageValue=1, crewSize=2 } cat['BLUE_M978_TANKER'] = { menuCategory='Support', menu='M-978 Tanker', description='M-978 Tanker', dcsCargoType='container_cargo', required=1, initialStock=10, side=BLUE, category=Group.Category.GROUND, build=singleUnit('M978 HEMTT Tanker'), salvageValue=1, crewSize=2 }
cat['BLUE_EWR_FPS117'] = { menuCategory='Support', menu='EWR Radar FPS-117', description='EWR Radar FPS-117', dcsCargoType='container_cargo', required=1, initialStock=6, side=BLUE, category=Group.Category.GROUND, build=singleUnit('FPS-117'), salvageValue=1, crewSize=3 } cat['BLUE_EWR_FPS117'] = { menuCategory='Support', menu='EWR Radar FPS-117', description='EWR Radar FPS-117', dcsCargoType='container_cargo', required=1, initialStock=6, side=BLUE, category=Group.Category.GROUND, build=singleUnit('FPS-117'), salvageValue=1, crewSize=3 }
-- Support (RED) -- Support (RED)
cat['RED_TIGR_JTAC'] = { menuCategory='Support', menu='Tigr - JTAC', description='JTAC Tigr', dcsCargoType='container_cargo', required=1, initialStock=12, side=RED, category=Group.Category.GROUND, build=singleUnit('Tigr_233036'), MEDEVAC=true, salvageValue=1, crewSize=4 } cat['RED_TIGR_JTAC'] = { menuCategory='Support', menu='Tigr - JTAC', description='JTAC Tigr', dcsCargoType='container_cargo', required=1, initialStock=12, side=RED, category=Group.Category.GROUND, build=singleUnit('Tigr_233036'), MEDEVAC=true, salvageValue=1, crewSize=4, roles={'JTAC'}, jtac={ platform='ground' } }
cat['RED_URAL4320_AMMO'] = { menuCategory='Support', menu='Ural-4320-31 Ammo Truck', description='Ural-4320-31 Ammo Truck', dcsCargoType='container_cargo', required=1, initialStock=12, side=RED, category=Group.Category.GROUND, build=singleUnit('Ural-4320-31'), salvageValue=1, crewSize=2 } cat['RED_URAL4320_AMMO'] = { menuCategory='Support', menu='Ural-4320-31 Ammo Truck', description='Ural-4320-31 Ammo Truck', dcsCargoType='container_cargo', required=1, initialStock=12, side=RED, category=Group.Category.GROUND, build=singleUnit('Ural-4320-31'), salvageValue=1, crewSize=2 }
cat['RED_ATZ10_TANKER'] = { menuCategory='Support', menu='ATZ-10 Refueler', description='ATZ-10 Refueler', dcsCargoType='container_cargo', required=1, initialStock=10, side=RED, category=Group.Category.GROUND, build=singleUnit('ATZ-10'), salvageValue=1, crewSize=2 } cat['RED_ATZ10_TANKER'] = { menuCategory='Support', menu='ATZ-10 Refueler', description='ATZ-10 Refueler', dcsCargoType='container_cargo', required=1, initialStock=10, side=RED, category=Group.Category.GROUND, build=singleUnit('ATZ-10'), salvageValue=1, crewSize=2 }
cat['RED_EWR_1L13'] = { menuCategory='Support', menu='EWR Radar 1L13', description='EWR Radar 1L13', dcsCargoType='container_cargo', required=1, initialStock=6, side=RED, category=Group.Category.GROUND, build=singleUnit('1L13 EWR'), salvageValue=1, crewSize=3 } cat['RED_EWR_1L13'] = { menuCategory='Support', menu='EWR Radar 1L13', description='EWR Radar 1L13', dcsCargoType='container_cargo', required=1, initialStock=6, side=RED, category=Group.Category.GROUND, build=singleUnit('1L13 EWR'), salvageValue=1, crewSize=3 }
@ -233,8 +233,8 @@ cat['RED_BUK_REPAIR'] = { menuCategory='SAM long range', menu='BUK Repai
end } end }
-- Drones (JTAC) -- Drones (JTAC)
cat['BLUE_MQ9'] = { menuCategory='Drones', menu='MQ-9 Reaper - JTAC', description='MQ-9 JTAC', dcsCargoType='container_cargo', required=1, initialStock=3, side=BLUE, category=Group.Category.AIRPLANE, build=singleAirUnit('MQ-9 Reaper') } cat['BLUE_MQ9'] = { menuCategory='Drones', menu='MQ-9 Reaper - JTAC', description='MQ-9 JTAC', dcsCargoType='container_cargo', required=1, initialStock=3, side=BLUE, category=Group.Category.AIRPLANE, build=singleAirUnit('MQ-9 Reaper'), roles={'JTAC'}, jtac={ platform='air' } }
cat['RED_WINGLOONG'] = { menuCategory='Drones', menu='WingLoong-I - JTAC', description='WingLoong-I JTAC', dcsCargoType='container_cargo', required=1, initialStock=3, side=RED, category=Group.Category.AIRPLANE, build=singleAirUnit('WingLoong-I') } cat['RED_WINGLOONG'] = { menuCategory='Drones', menu='WingLoong-I - JTAC', description='WingLoong-I JTAC', dcsCargoType='container_cargo', required=1, initialStock=3, side=RED, category=Group.Category.AIRPLANE, build=singleAirUnit('WingLoong-I'), roles={'JTAC'}, jtac={ platform='air' } }
-- FOB crates (Support) — three small crates build a FOB site -- FOB crates (Support) — three small crates build a FOB site
cat['FOB_SMALL'] = { menuCategory='Support', menu='FOB Crate - Small', description='FOB small crate', dcsCargoType='container_cargo', required=1, initialStock=12, side=nil, category=Group.Category.GROUND, build=function(point, headingDeg) cat['FOB_SMALL'] = { menuCategory='Support', menu='FOB Crate - Small', description='FOB small crate', dcsCargoType='container_cargo', required=1, initialStock=12, side=nil, category=Group.Category.GROUND, build=function(point, headingDeg)