Updated CTLD. Fixed FARP Upgrade not working. Fixed claimin cargo by putting a salvage zone on top of it.

This commit is contained in:
iTracerFacer 2025-11-29 16:07:29 -06:00
parent 70842b241d
commit 6994ca0642
8 changed files with 23534 additions and 249 deletions

8226
CTLD.lua Normal file

File diff suppressed because it is too large Load Diff

View File

@ -182,13 +182,13 @@ CTLD.Messages = {
-- FARP System messages -- FARP System messages
farp_upgrade_started = "Upgrading FOB to FARP Stage {stage}... Building in progress.", farp_upgrade_started = "Upgrading FOB to FARP Stage {stage}... Building in progress.",
farp_upgrade_complete = "{player} upgraded FOB to FARP Stage {stage}! Services available: {services}", farp_upgrade_complete = "{player} upgraded FOB to FARP Stage {stage}!\nFARP Services: {services}\nFOB still active for logistics and troop operations.",
farp_upgrade_insufficient_salvage = "Insufficient salvage to upgrade to FARP Stage {stage}. Need {need} points (have {current}). Deliver crews to MASH or sling-load salvage!", farp_upgrade_insufficient_salvage = "Insufficient salvage to upgrade to FARP Stage {stage}. Need {need} points (have {current}). Deliver crews to MASH or sling-load salvage!",
farp_status = "FOB Status: FARP Stage {stage}/{max_stage}\nServices: {services}\nNext upgrade: {next_cost} salvage (Stage {next_stage})", farp_status = "FOB + FARP Status: Stage {stage}/{max_stage}\nFARP Services: {services}\nFOB logistics: ACTIVE\nNext upgrade: {next_cost} salvage (Stage {next_stage})",
farp_status_maxed = "FOB Status: FARP Stage {stage}/{max_stage} (FULLY UPGRADED)\nServices: {services}", farp_status_maxed = "FOB + FARP Status: Stage {stage}/{max_stage} (FULLY UPGRADED)\nFARP Services: {services}\nFOB logistics: ACTIVE",
farp_not_at_fob = "You must be near a FOB Pickup Zone to upgrade it to a FARP.", farp_not_at_fob = "You must be near a FOB Pickup Zone to upgrade it to a FARP.",
farp_already_maxed = "This FOB is already at maximum FARP stage (Stage 3).", farp_already_maxed = "This FOB is already at maximum FARP stage (Stage 3).",
farp_service_available = "FARP services available: Rearm, Refuel, Repair for ground vehicles and helicopters within {radius}m.", farp_service_available = "FARP services available: Rearm, Refuel, Repair for ground vehicles and helicopters within {radius}m. FOB logistics remain active.",
slingload_salvage_warn_5min = "SALVAGE URGENT: Crate {id} at {grid} expires in 5 minutes!", slingload_salvage_warn_5min = "SALVAGE URGENT: Crate {id} at {grid} expires in 5 minutes!",
slingload_salvage_hooked_in_zone = "Salvage crate {id} is inside {zone}. Release the sling to complete delivery.", slingload_salvage_hooked_in_zone = "Salvage crate {id} is inside {zone}. Release the sling to complete delivery.",
slingload_salvage_wrong_zone = "Salvage crate {id} is sitting in {zone_type} zone {zone}. Take it to an active Salvage zone for credit.", slingload_salvage_wrong_zone = "Salvage crate {id} is sitting in {zone_type} zone {zone}. Take it to an active Salvage zone for credit.",
@ -621,16 +621,16 @@ CTLD.Config = {
-- Spawn probability when enemy ground units die -- Spawn probability when enemy ground units die
SpawnChance = { SpawnChance = {
[coalition.side.BLUE] = 0.20, -- 20% chance when BLUE unit dies (RED can collect the salvage) [coalition.side.BLUE] = 0.10, -- 20% chance when BLUE unit dies (RED can collect the salvage)
[coalition.side.RED] = 0.20, -- 20% chance when RED unit dies (BLUE can collect the salvage) [coalition.side.RED] = 0.10, -- 20% chance when RED unit dies (BLUE can collect the salvage)
}, },
-- Weight classes with spawn probabilities and reward rates -- Weight classes with spawn probabilities and reward rates
WeightClasses = { WeightClasses = {
{ name = 'Light', min = 500, max = 1000, probability = 0.50, rewardPer500kg = 2 }, -- Huey-capable { name = 'Light', min = 500, max = 1000, probability = 0.50, rewardPer500kg = 0.5 }, -- 1-2 pts (reduced from 2)
{ name = 'Medium', min = 2501, max = 5000, probability = 0.30, rewardPer500kg = 3 }, -- Hip/Mi-8 { name = 'Medium', min = 2501, max = 5000, probability = 0.30, rewardPer500kg = 1 }, -- 5-10 pts (reduced from 3)
{ name = 'Heavy', min = 5001, max = 8000, probability = 0.15, rewardPer500kg = 5 }, -- Large helos { name = 'Heavy', min = 5001, max = 8000, probability = 0.15, rewardPer500kg = 1.5 }, -- 15-24 pts (reduced from 5)
{ name = 'SuperHeavy', min = 8001, max = 12000, probability = 0.05, rewardPer500kg = 8 }, -- Chinook only { name = 'SuperHeavy', min = 8001, max = 12000, probability = 0.05, rewardPer500kg = 2 }, -- 32-48 pts (reduced from 8)
}, },
-- Condition-based reward multipliers (based on crate health when delivered) -- Condition-based reward multipliers (based on crate health when delivered)
@ -713,134 +713,76 @@ CTLD.FARPConfig = {
-- Format: { type = "DCS_Static_Name", x = offset_x, z = offset_z, heading = degrees, height = 0 } -- Format: { type = "DCS_Static_Name", x = offset_x, z = offset_z, heading = degrees, height = 0 }
-- Positions are relative to FOB center point -- Positions are relative to FOB center point
StageLayouts = { StageLayouts = {
-- Stage 1: Basic FARP Pad (3 salvage) -- Stage 1: Basic FARP Pad (3 salvage) - Inner ring 30-60m
[1] = { [1] = {
{ type = "FARP CP Blindage", x = 0, z = 25, heading = 180 }, { type = "FARP CP Blindage", x = 0, z = 40, heading = 0 },
{ type = "FARP Tent", x = 17.3, z = 10, heading = 240 }, { type = "FARP Tent", x = 45, z = 20, heading = 30 },
{ type = "FARP Tent", x = -17.3, z = 10, heading = 120 }, { type = "FARP Tent", x = -45, z = 20, heading = 330 },
{ type = "container_20ft", x = 15.4, z = -6.2, heading = 90 }, { type = "container_20ft", x = 35, z = -30, heading = 338 },
{ type = "container_20ft", x = -15.4, z = -6.2, heading = 90 }, { type = "container_20ft", x = -35, z = -30, heading = 202 },
{ type = "Windsock", x = 0, z = 30, heading = 0 }, { type = "Windsock", x = 0, z = 60, heading = 0 },
{ type = "FARP Ammo Dump Coating", x = 13, z = -10.6, heading = 30 }, { type = "FARP Ammo Dump Coating", x = 30, z = -25, heading = 219 },
{ type = "FARP Ammo Dump Coating", x = -13, z = -10.6, heading = 330 }, { type = "FARP Ammo Dump Coating", x = -30, z = -25, heading = 141 },
{ type = ".Ammunition depot", x = 17, z = 12, heading = 0 }, { type = "FARP Fuel Depot", x = 40, z = 30, heading = 30 },
{ type = ".Ammunition depot", x = -17, z = 12, heading = 0 }, { type = "FARP Fuel Depot", x = -40, z = 30, heading = 330 },
{ type = "BarrelCargo", x = 8, z = -18, heading = 0 }, { type = "GeneratorF", x = 50, z = -10, heading = 278 },
{ type = "BarrelCargo", x = -8, z = -18, heading = 0 }, { type = "container_20ft", x = -50, z = -10, heading = 98 },
{ type = "BarrelCargo", x = 12, z = 20, heading = 0 },
{ type = "BarrelCargo", x = -12, z = 20, heading = 0 },
{ type = "GeneratorF", x = 22, z = -3, heading = 270 },
{ type = "Sandbox", x = 10, z = -16, heading = 0 },
{ type = "Sandbox", x = -10, z = -16, heading = 0 },
{ type = "Sandbox", x = 10, z = 16, heading = 0 },
{ type = "Sandbox", x = -10, z = 16, heading = 0 },
{ type = "Sandbox", x = 18, z = 0, heading = 0 },
{ type = "Sandbox", x = -18, z = 0, heading = 0 },
}, },
-- Stage 2: Operational FARP - adds fuel capability (5 more salvage) -- Stage 2: Operational FARP - adds fuel capability (5 more salvage) - Middle ring 80-130m
[2] = { [2] = {
{ type = "M978 HEMTT Tanker", x = 90, z = 0, heading = 270 },
{ type = "M978 HEMTT Tanker", x = -90, z = 0, heading = 90 },
{ type = "FARP Fuel Depot", x = 95, z = -40, heading = 281 },
{ type = "FARP Fuel Depot", x = -95, z = -40, heading = 79 },
{ type = "FARP Tent", x = 80, z = 60, heading = 39 },
{ type = "FARP Tent", x = -80, z = 60, heading = 321 },
{ type = "container_40ft", x = 0, z = -100, heading = 180 },
{ type = "container_40ft", x = 50, z = -100, heading = 180 },
{ type = "FARP Ammo Dump Coating", x = 85, z = 70, heading = 30 },
{ type = "FARP Ammo Dump Coating", x = -85, z = 70, heading = 330 },
{ type = "Ural-375 PBU", x = 105, z = -30, heading = 247 },
{ type = "Electric power box", x = 90, z = 50, heading = 36 },
{ type = "Electric power box", x = -90, z = 50, heading = 324 },
{ type = "GeneratorF", x = 0, z = -85, heading = 180 },
},
-- Stage 3: Full Forward Airbase - adds ammo and comms (8 more salvage) - Outer ring 150-250m
[3] = {
{ type = "FARP", x = 0, z = 0, heading = 0 },
-- Service objects near pad for scripted services
{ type = "M978 HEMTT Tanker", x = 35, z = 0, heading = 270 }, { type = "M978 HEMTT Tanker", x = 35, z = 0, heading = 270 },
{ type = "M978 HEMTT Tanker", x = -35, z = 0, heading = 90 }, { type = "M978 HEMTT Tanker", x = -35, z = 0, heading = 90 },
{ type = "FARP Fuel Depot", x = 40, z = -8, heading = 0 }, { type = "FARP Fuel Depot", x = 30, z = 25, heading = 315 },
{ type = "FARP Fuel Depot", x = -40, z = -8, heading = 0 }, { type = "FARP Fuel Depot", x = -30, z = 25, heading = 45 },
{ type = "FARP Tent", x = 26.5, z = 21.7, heading = 210 }, { type = "FARP Ammo Dump Coating", x = 40, z = -20, heading = 282 },
{ type = "FARP Tent", x = -26.5, z = 21.7, heading = 150 }, { type = "FARP Ammo Dump Coating", x = -40, z = -20, heading = 78 },
{ type = "container_40ft", x = 0, z = -35, heading = 0 }, -- Extended support structures at outer ring
{ type = "container_40ft", x = 8, z = -35, heading = 0 }, { type = "FARP CP Blindage", x = 0, z = 150, heading = 0 },
{ type = "Hesco_wallperimeter_7", x = 0, z = 42, heading = 0 }, { type = "Shelter", x = 0, z = -160, heading = 180 },
{ type = "Hesco_wallperimeter_7", x = 30, z = 30, heading = 315 }, { type = "SKP-11", x = 0, z = 180, heading = 0 },
{ type = "Hesco_wallperimeter_7", x = -30, z = 30, heading = 45 }, { type = "ZiL-131 APA-80", x = 50, z = 165, heading = 8 },
{ type = "Red_Flag", x = 0, z = 45, heading = 0 }, { type = "FARP Tent", x = 170, z = 0, heading = 270 },
{ type = "Red_Flag", x = 45, z = 0, heading = 0 }, { type = "FARP Tent", x = -170, z = 0, heading = 90 },
{ type = "Red_Flag", x = -45, z = 0, heading = 0 }, { type = "FARP Tent", x = 120, z = 120, heading = 315 },
{ type = "Red_Flag", x = 0, z = -45, heading = 0 }, { type = "FARP Tent", x = -120, z = 120, heading = 45 },
{ type = "Ural-375 PBU", x = 32.9, z = -13.1, heading = 225 }, { type = "FARP Ammo Dump Coating", x = 160, z = 80, heading = 30 },
{ type = "Electric power box", x = 28, z = 20, heading = 0 }, { type = "FARP Ammo Dump Coating", x = -160, z = 80, heading = 330 },
{ type = "Electric power box", x = -28, z = 20, heading = 0 }, { type = "container_20ft", x = -140, z = -130, heading = 128 },
{ type = "Landmine pot", x = 36, z = 8, heading = 0 }, { type = "container_20ft", x = -150, z = -120, heading = 133 },
{ type = "Landmine pot", x = -36, z = 8, heading = 0 }, { type = "container_20ft", x = 140, z = -130, heading = 52 },
{ type = "Landmine pot", x = 30, z = -25, heading = 0 }, { type = "container_20ft", x = 150, z = -120, heading = 47 },
{ type = "Landmine pot", x = -30, z = -25, heading = 0 }, { type = "GeneratorF", x = 155, z = -140, heading = 234 },
{ type = "Landmine pot", x = 20, z = 30, heading = 0 }, { type = "GeneratorF", x = -155, z = -140, heading = 126 },
{ type = "Landmine pot", x = -20, z = 30, heading = 0 }, { type = "UAZ-469", x = 70, z = 160, heading = 14 },
{ type = "Tetrapod", x = 42, z = 15, heading = 0 }, { type = "UAZ-469", x = -70, z = 160, heading = 346 },
{ type = "Tetrapod", x = -42, z = 15, heading = 0 }, { type = "Ural-375", x = -125, z = -135, heading = 125 },
{ type = "Tetrapod", x = 42, z = -15, heading = 0 }, { type = "Sandbox", x = 350, z = 0, heading = 270 },
{ type = "Tetrapod", x = -42, z = -15, heading = 0 }, { type = "Sandbox", x = -350, z = 0, heading = 90 },
{ type = "Tetrapod", x = 15, z = 42, heading = 0 }, { type = "Sandbox", x = 247, z = 247, heading = 315 },
{ type = "Tetrapod", x = -15, z = 42, heading = 0 }, { type = "Sandbox", x = -247, z = 247, heading = 45 },
{ type = "Tetrapod", x = 15, z = -42, heading = 0 }, { type = "Sandbox", x = 247, z = -247, heading = 225 },
{ type = "Tetrapod", x = -15, z = -42, heading = 0 }, { type = "Sandbox", x = -247, z = -247, heading = 135 },
{ type = "FARP Command Post", x = 0, z = 25, heading = 180 },
},
-- Stage 3: Full Forward Airbase - adds ammo and comms (8 more salvage)
[3] = {
{ type = "M939 Heavy", x = 38.9, z = -21.9, heading = 225 },
{ type = "M939 Heavy", x = -38.9, z = -21.9, heading = 135 },
{ type = "Shelter", x = 0, z = -50, heading = 0 },
{ type = "FARP Ammo Dump Coating", x = 48, z = -10, heading = 0 },
{ type = "FARP Ammo Dump Coating", x = -48, z = -10, heading = 0 },
{ type = "FARP Ammo Dump Coating", x = 45, z = -20, heading = 0 },
{ type = "FARP Ammo Dump Coating", x = -45, z = -20, heading = 0 },
{ type = "SKP-11", x = 0, z = 55, heading = 180 },
{ type = "ZiL-131 APA-80", x = 8, z = 52, heading = 180 },
{ type = "Hesco_wallperimeter_1", x = 52, z = 30, heading = 0 },
{ type = "Hesco_wallperimeter_1", x = -52, z = 30, heading = 0 },
{ type = "Hesco_wallperimeter_1", x = 52, z = -30, heading = 0 },
{ type = "Hesco_wallperimeter_1", x = -52, z = -30, heading = 0 },
{ type = "Hesco_wallperimeter_1", x = 30, z = 52, heading = 90 },
{ type = "Hesco_wallperimeter_1", x = -30, z = 52, heading = 90 },
{ type = "Hesco_wallperimeter_1", x = 30, z = -52, heading = 90 },
{ type = "Hesco_wallperimeter_1", x = -30, z = -52, heading = 90 },
{ type = "Hesco_wallperimeter_1", x = 45, z = 40, heading = 45 },
{ type = "Hesco_wallperimeter_1", x = -45, z = 40, heading = 315 },
{ type = "Hesco_wallperimeter_1", x = 45, z = -40, heading = 135 },
{ type = "Hesco_wallperimeter_1", x = -45, z = -40, heading = 225 },
{ type = "FARP Tent", x = 52, z = 0, heading = 270 },
{ type = "FARP Tent", x = -52, z = 0, heading = 90 },
{ type = "FARP Tent", x = 36.8, z = 36.8, heading = 225 },
{ type = "container_20ft", x = -30, z = -38, heading = 45 },
{ type = "container_20ft", x = -35, z = -33, heading = 45 },
{ type = "container_20ft", x = -25, z = -43, heading = 45 },
{ type = "container_20ft", x = -33, z = -45, heading = 135 },
{ type = "GeneratorF", x = 43.1, z = -31.4, heading = 225 },
{ type = "UAZ-469", x = 12, z = 48, heading = 200 },
{ type = "UAZ-469", x = 16, z = 50, heading = 170 },
{ type = "Ural-375", x = -25, z = -35, heading = 45 },
{ type = "Landmine pot", x = 50, z = 15, heading = 0 },
{ type = "Landmine pot", x = -50, z = 15, heading = 0 },
{ type = "Landmine pot", x = 50, z = -15, heading = 0 },
{ type = "Landmine pot", x = -50, z = -15, heading = 0 },
{ type = "Landmine pot", x = 15, z = 50, heading = 0 },
{ type = "Landmine pot", x = -15, z = 50, heading = 0 },
{ type = "Landmine pot", x = 40, z = 30, heading = 0 },
{ type = "Landmine pot", x = -40, z = 30, heading = 0 },
{ type = "Landmine pot", x = 30, z = -40, heading = 0 },
{ type = "Landmine pot", x = -30, z = -40, heading = 0 },
{ type = "Landmine pot", x = 45, z = 25, heading = 0 },
{ type = "Landmine pot", x = -45, z = 25, heading = 0 },
{ type = "billboard_motorized rifle troops", x = 0, z = -58, heading = 0 },
{ type = "Sandbox", x = 38, z = -8, heading = 0 },
{ type = "Sandbox", x = -38, z = -8, heading = 0 },
{ type = "Sandbox", x = 38, z = 8, heading = 0 },
{ type = "Sandbox", x = -38, z = 8, heading = 0 },
{ type = "Black_Tyre", x = 20, z = -48, heading = 0 },
{ type = "Black_Tyre", x = -20, z = -48, heading = 0 },
{ type = "Black_Tyre", x = 24, z = -46, heading = 0 },
{ type = "Black_Tyre", x = -24, z = -46, heading = 0 },
{ type = "Black_Tyre", x = 28, z = -44, heading = 0 },
{ type = "Black_Tyre", x = -28, z = -44, heading = 0 },
{ type = "Black_Tyre", x = 48, z = 20, heading = 0 },
{ type = "Black_Tyre", x = -48, z = 20, heading = 0 },
{ type = "WatchTower", x = -38.9, z = 38.9, heading = 225 },
{ type = "warning_board_c", x = 0, z = 48, heading = 180 },
{ type = "warning_board_c", x = 48, z = 0, heading = 270 },
{ type = "warning_board_c", x = -48, z = 0, heading = 90 },
{ type = "warning_board_c", x = 35, z = -35, heading = 45 },
{ type = "warning_board_c", x = -35, z = -35, heading = 315 },
{ type = "warning_board_c", x = 35, z = 35, heading = 225 },
}, },
}, },
} }
@ -859,8 +801,8 @@ CTLD.HoverCoachConfig = {
arrivalDist = 1000, -- m: start guidance "You're close…" arrivalDist = 1000, -- m: start guidance "You're close…"
closeDist = 100, -- m: reduce speed / set AGL guidance closeDist = 100, -- m: reduce speed / set AGL guidance
precisionDist = 8, -- m: start precision hints precisionDist = 8, -- m: start precision hints
captureHoriz = 10, -- m: horizontal sweet spot radius captureHoriz = 15, -- m: horizontal sweet spot radius
captureVert = 10, -- m: vertical sweet spot tolerance around AGL window captureVert = 15, -- m: vertical sweet spot tolerance around AGL window
aglMin = 5, -- m: hover window min AGL aglMin = 5, -- m: hover window min AGL
aglMax = 20, -- m: hover window max AGL aglMax = 20, -- m: hover window max AGL
maxGS = 8/3.6, -- m/s: 8 km/h for precision, used for errors maxGS = 8/3.6, -- m/s: 8 km/h for precision, used for errors
@ -4184,6 +4126,36 @@ function CTLD:ClearMapDrawings()
self._MapMarkup = { Pickup = {}, Drop = {}, FOB = {}, MASH = {}, SalvageDrop = {} } self._MapMarkup = { Pickup = {}, Drop = {}, FOB = {}, MASH = {}, SalvageDrop = {} }
end end
function CTLD:_updateMobileMASHDrawing(mashId)
local data = CTLD._mashZones and CTLD._mashZones[mashId]
if not data or data.side ~= self.Side or not data.isMobile then return end
if not (self.Config.MapDraw and self.Config.MapDraw.Enabled and self.Config.MapDraw.DrawMASHZones) then return end
local zoneName = data.displayName or mashId
if self._ZoneActive.MASH[zoneName] == false then return end
-- Remove old drawing
self:_removeZoneDrawing('MASH', zoneName)
-- Redraw at new position
local md = self.Config.MapDraw
local opts = {
OutlineColor = md.OutlineColor,
LineType = (md.LineTypes and md.LineTypes.MASH) or md.LineType or 1,
FillColor = (md.FillColors and md.FillColors.MASH) or nil,
FontSize = md.FontSize,
ReadOnly = (md.ReadOnly ~= false),
LabelPrefix = (md.LabelPrefixes and md.LabelPrefixes.MASH) or 'MASH',
LabelOffsetX = md.LabelOffsetX,
LabelOffsetFromEdge = md.LabelOffsetFromEdge,
LabelOffsetRatio = md.LabelOffsetRatio,
ForAll = (md.ForAll == true),
}
if data.zone then
self:_drawZoneCircleAndLabel('MASH', data.zone, opts)
end
end
function CTLD:_removeZoneDrawing(kind, zname) function CTLD:_removeZoneDrawing(kind, zname)
if not (self._MapMarkup and self._MapMarkup[kind] and self._MapMarkup[kind][zname]) then return end if not (self._MapMarkup and self._MapMarkup[kind] and self._MapMarkup[kind][zname]) then return end
local ids = self._MapMarkup[kind][zname] local ids = self._MapMarkup[kind][zname]
@ -4310,13 +4282,14 @@ function CTLD:DrawZonesOnMap()
local v2 = (VECTOR2 and VECTOR2.New) and VECTOR2:New(pos.x, pos.z) or { x = pos.x, y = pos.z } local v2 = (VECTOR2 and VECTOR2.New) and VECTOR2:New(pos.x, pos.z) or { x = pos.x, y = pos.z }
zoneObj = ZONE_RADIUS:New(zoneName, v2, data.radius or 500) zoneObj = ZONE_RADIUS:New(zoneName, v2, data.radius or 500)
else else
local posCopy = { x = pos.x, z = pos.z } -- Create zone that references data.position directly for live updates
zoneObj = {} zoneObj = {}
function zoneObj:GetName() function zoneObj:GetName()
return zoneName return zoneName
end end
function zoneObj:GetPointVec3() function zoneObj:GetPointVec3()
return { x = posCopy.x, y = 0, z = posCopy.z } local currentPos = data.position or { x = 0, z = 0 }
return { x = currentPos.x, y = 0, z = currentPos.z }
end end
function zoneObj:GetRadius() function zoneObj:GetRadius()
return data.radius or 500 return data.radius or 500
@ -7179,9 +7152,10 @@ function CTLD:BuildSpecificAtGroup(group, recipeKey, opts)
counts[reqKey] = (counts[reqKey] or 0) - (qty or 0) counts[reqKey] = (counts[reqKey] or 0) - (qty or 0)
end 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) })
_logInfo(string.format('[BUILD_DEBUG] Built key=%s desc=%s isFOB=%s isMobileMASH=%s', tostring(recipeKey), tostring(def.description), tostring(def.isFOB), tostring(def.isMobileMASH)))
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
if def.isMobileMASH then if def.isMobileMASH then
_logDebug(string.format('[MobileMASH] BuildSpecificAtGroup invoking _CreateMobileMASH for key %s at (%.1f, %.1f)', tostring(recipeKey), spawnAt.x or -1, spawnAt.z or -1)) _logInfo(string.format('[MobileMASH] BuildSpecificAtGroup invoking _CreateMobileMASH for key %s at (%.1f, %.1f)', tostring(recipeKey), spawnAt.x or -1, spawnAt.z or -1))
local ok, err = pcall(function() self:_CreateMobileMASH(g, { x = spawnAt.x, z = spawnAt.z }, def) end) local ok, err = pcall(function() self:_CreateMobileMASH(g, { x = spawnAt.x, z = spawnAt.z }, def) end)
if not ok then if not ok then
_logError(string.format('[MobileMASH] _CreateMobileMASH invocation failed: %s', tostring(err))) _logError(string.format('[MobileMASH] _CreateMobileMASH invocation failed: %s', tostring(err)))
@ -8785,6 +8759,14 @@ function CTLD:BuildAtGroup(group, opts)
self:_CreateFOBPickupZone({ x = actualSpawn.x, z = actualSpawn.z }, cat, hdg) self:_CreateFOBPickupZone({ x = actualSpawn.x, z = actualSpawn.z }, cat, hdg)
end) end)
end end
-- If this was a Mobile MASH, create the tracking zone
if cat.isMobileMASH then
_logInfo(string.format('[MobileMASH] BuildAtGroup invoking _CreateMobileMASH for key %s at (%.1f, %.1f)', tostring(recipeKey), actualSpawn.x or -1, actualSpawn.z or -1))
local ok, err = pcall(function() self:_CreateMobileMASH(g, { x = actualSpawn.x, z = actualSpawn.z }, cat) end)
if not ok then
_logError(string.format('[MobileMASH] _CreateMobileMASH invocation failed: %s', tostring(err)))
end
end
-- Assign optional behavior for built vehicles/groups -- Assign optional behavior for built vehicles/groups
local behavior = opts and opts.behavior or nil local behavior = opts and opts.behavior or nil
if behavior == 'attack' and self.Config.AttackAI and self.Config.AttackAI.Enabled then if behavior == 'attack' and self.Config.AttackAI and self.Config.AttackAI.Enabled then
@ -9690,7 +9672,27 @@ function CTLD:ScanGroundAutoLoad()
if groundCfg.RequirePickupZone then if groundCfg.RequirePickupZone then
local inPickupZone = self:_isUnitInsidePickupZone(unit, true) local inPickupZone = self:_isUnitInsidePickupZone(unit, true)
if not inPickupZone and groundCfg.AllowInFOBZones then -- Explicitly exclude FARP service zones from auto-load
local inFARPZone = false
local unitPoint = unit:GetPointVec3()
for farpZoneName, farpInfo in pairs(CTLD._farpZones or {}) do
local farpZone = farpInfo.zone
if farpZone then
local farpPoint = farpZone:GetPointVec3()
local dx = (farpPoint.x - unitPoint.x)
local dz = (farpPoint.z - unitPoint.z)
local d = math.sqrt(dx*dx + dz*dz)
local farpRadius = self:_getZoneRadius(farpZone)
if d <= farpRadius then
inFARPZone = true
break
end
end
end
if inFARPZone then
inValidZone = false -- Never auto-load in FARP zones
elseif not inPickupZone and groundCfg.AllowInFOBZones then
-- Check FOB zones too -- Check FOB zones too
for _, fobZone in ipairs(self.FOBZones or {}) do for _, fobZone in ipairs(self.FOBZones or {}) do
local fname = fobZone:GetName() local fname = fobZone:GetName()
@ -10700,20 +10702,20 @@ function CTLD:FindNearestFOBZone(point)
end end
-- Spawn static objects for a FARP stage -- Spawn static objects for a FARP stage
function CTLD:SpawnFARPStatics(zoneName, stage, centerPoint, coalition) function CTLD:SpawnFARPStatics(zoneName, stage, centerPoint, coalitionId)
if not (CTLD.FARPConfig and CTLD.FARPConfig.StageLayouts[stage]) then if not (CTLD.FARPConfig and CTLD.FARPConfig.StageLayouts[stage]) then
_logError(string.format('Invalid FARP stage %d or missing layout config', stage)) _logError(string.format('Invalid FARP stage %d or missing layout config', stage))
return false return false
end end
local layout = CTLD.FARPConfig.StageLayouts[stage] local layout = CTLD.FARPConfig.StageLayouts[stage]
local farpData = CTLD._farpData[zoneName] or { stage = 0, statics = {}, coalition = coalition } local farpData = CTLD._farpData[zoneName] or { stage = 0, statics = {}, coalition = coalitionId }
_logInfo(string.format('Spawning FARP Stage %d statics for zone %s (coalition %d)', stage, zoneName, coalition)) _logInfo(string.format('Spawning FARP Stage %d statics for zone %s (coalition %d)', stage, zoneName, coalitionId))
-- Get coalition name for DCS -- Get coalition name for DCS
-- Note: 'coalition' parameter is a number (1=red, 2=blue), not the coalition table -- Note: 'coalitionId' parameter is a number (1=red, 2=blue), not the coalition table
local coalitionName = (coalition == 2) and 'blue' or 'red' local coalitionName = (coalitionId == 2) and 'blue' or 'red'
for _, obj in ipairs(layout) do for _, obj in ipairs(layout) do
-- Calculate world position from relative offset -- Calculate world position from relative offset
@ -10724,6 +10726,20 @@ function CTLD:SpawnFARPStatics(zoneName, stage, centerPoint, coalition)
-- Generate unique name -- Generate unique name
local staticName = string.format('FARP_%s_S%d_%s_%d', zoneName, stage, obj.type:gsub('%s+', '_'), math.random(10000, 99999)) local staticName = string.format('FARP_%s_S%d_%s_%d', zoneName, stage, obj.type:gsub('%s+', '_'), math.random(10000, 99999))
-- Determine category and shape_name based on object type
local category = "Fortifications"
local shapeName = ""
local linkUnit = nil
local linkOffset = false
local callsignID = math.random(1, 99)
if obj.type == "FARP" then
category = "Heliports"
shapeName = "FARP"
linkUnit = 0
linkOffset = true
end
-- Create static object data -- Create static object data
local staticData = { local staticData = {
["type"] = obj.type, ["type"] = obj.type,
@ -10731,15 +10747,24 @@ function CTLD:SpawnFARPStatics(zoneName, stage, centerPoint, coalition)
["heading"] = math.rad(obj.heading or 0), ["heading"] = math.rad(obj.heading or 0),
["x"] = worldX, ["x"] = worldX,
["y"] = worldZ, ["y"] = worldZ,
["category"] = "Fortifications", ["category"] = category,
["canCargo"] = false, ["canCargo"] = false,
["shape_name"] = "", ["shape_name"] = shapeName,
["rate"] = 100, ["rate"] = 100,
} }
-- Add FARP-specific data
if obj.type == "FARP" then
staticData["linkUnit"] = linkUnit
staticData["linkOffset"] = linkOffset
staticData["callsign_id"] = callsignID
staticData["frequencyList"] = {127.5, 129.5, 121.5}
staticData["modulation"] = 0
end
-- Spawn the static -- Spawn the static
local success, staticObj = pcall(function() local success, staticObj = pcall(function()
return coalition.addStaticObject(coalition, staticData) return coalition.addStaticObject(coalitionId, staticData)
end) end)
if success and staticObj then if success and staticObj then
@ -10751,7 +10776,7 @@ function CTLD:SpawnFARPStatics(zoneName, stage, centerPoint, coalition)
end end
farpData.stage = stage farpData.stage = stage
farpData.coalition = coalition farpData.coalition = coalitionId
CTLD._farpData[zoneName] = farpData CTLD._farpData[zoneName] = farpData
_logInfo(string.format('FARP Stage %d complete for zone %s - spawned %d statics', stage, zoneName, #farpData.statics)) _logInfo(string.format('FARP Stage %d complete for zone %s - spawned %d statics', stage, zoneName, #farpData.statics))
@ -10808,7 +10833,8 @@ function CTLD:UpgradeFARP(group, zoneName)
end end
local center = zone:GetVec2() local center = zone:GetVec2()
local centerPoint = { x = center.x, z = center.y } -- Offset FARP 80m north from FOB zone center to avoid spawned trucks
local centerPoint = { x = center.x, z = center.y + 80 }
-- Deduct salvage -- Deduct salvage
CTLD._salvagePoints[self.Side] = currentSalvage - upgradeCost CTLD._salvagePoints[self.Side] = currentSalvage - upgradeCost
@ -10834,7 +10860,7 @@ function CTLD:UpgradeFARP(group, zoneName)
services = table.concat(services, ', ') services = table.concat(services, ', ')
}) })
-- Create or update FARP service zone -- Create or update FARP service zone (use same offset centerPoint)
self:CreateFARPServiceZone(zoneName, centerPoint, nextStage) self:CreateFARPServiceZone(zoneName, centerPoint, nextStage)
_logInfo(string.format('%s upgraded FOB %s to FARP Stage %d (cost: %d salvage)', _logInfo(string.format('%s upgraded FOB %s to FARP Stage %d (cost: %d salvage)',
@ -10857,6 +10883,13 @@ function CTLD:CreateFARPServiceZone(zoneName, centerPoint, stage)
local v2 = (VECTOR2 and VECTOR2.New) and VECTOR2:New(centerPoint.x, centerPoint.z) or { x = centerPoint.x, y = centerPoint.z } local v2 = (VECTOR2 and VECTOR2.New) and VECTOR2:New(centerPoint.x, centerPoint.z) or { x = centerPoint.x, y = centerPoint.z }
local serviceZone = ZONE_RADIUS:New(farpZoneName, v2, radius) local serviceZone = ZONE_RADIUS:New(farpZoneName, v2, radius)
-- Enable scanning for the zone (scan for units and helicopters)
if serviceZone.Scan then
pcall(function()
serviceZone:Scan({Object.Category.UNIT, Object.Category.STATIC})
end)
end
CTLD._farpZones[farpZoneName] = { CTLD._farpZones[farpZoneName] = {
zone = serviceZone, zone = serviceZone,
side = self.Side, side = self.Side,
@ -10876,56 +10909,115 @@ function CTLD:StartFARPServices(farpZoneName)
if not farpInfo then return end if not farpInfo then return end
local selfref = self local selfref = self
CTLD._farpServiceState = CTLD._farpServiceState or {}
-- Service scheduler runs every 5 seconds -- Service scheduler runs every 2 seconds
SCHEDULER:New(nil, function() SCHEDULER:New(nil, function()
local zone = farpInfo.zone local zone = farpInfo.zone
if not zone then return end if not zone then return end
local stage = farpInfo.stage local stage = farpInfo.stage
local units = zone:GetScannedUnits() local now = timer.getTime()
for _, unit in ipairs(units or {}) do -- Scan zone for units
local zoneVec3 = zone:GetPointVec3()
local radius = selfref:_getZoneRadius(zone)
local volS = {
id = world.VolumeType.SPHERE,
params = {
point = zoneVec3,
radius = radius
}
}
local foundUnits = {}
world.searchObjects(Object.Category.UNIT, volS, function(obj)
local unit = UNIT:Find(obj)
if unit and unit:IsAlive() then if unit and unit:IsAlive() then
local unitCoalition = unit:GetCoalition() local unitCoalition = unit:GetCoalition()
-- Only service friendly units -- Only service friendly units
if unitCoalition == farpInfo.side then if unitCoalition == farpInfo.side then
local unitType = unit:GetTypeName() if unit:IsAir() or unit:IsGround() then
local group = unit:GetGroup() table.insert(foundUnits, unit)
end
end
end
return true
end)
for _, unit in ipairs(foundUnits) do
local dcsUnit = unit:GetDCSObject()
if dcsUnit then
local uname = unit:GetName()
local agl = unit:GetAltitude() - land.getHeight(unit:GetPointVec2())
local vel = unit:GetVelocityKMH()
-- Check if aircraft is on ground (low AGL and low speed)
local onGround = (agl < 5) and (vel < 5)
if onGround then
CTLD._farpServiceState[uname] = CTLD._farpServiceState[uname] or { lastService = 0 }
local state = CTLD._farpServiceState[uname]
-- Service helicopters and ground vehicles -- Service every 3 seconds to avoid spam
if group and (unit:IsHelicopter() or unit:IsGround()) then if (now - state.lastService) >= 3 then
local serviced = false
-- Stage 2+: Refuel -- Stage 2+: Refuel
if stage >= 2 then if stage >= 2 then
-- Trigger refuel (DCS built-in command) local fuel = dcsUnit:getFuel()
pcall(function() if fuel < 0.95 then
local controller = unit:GetUnit():getController() -- Add 10% fuel per service tick (takes ~30 seconds for full refuel)
if controller then dcsUnit:setFuel(math.min(1.0, fuel + 0.10))
controller:setCommand({ serviced = true
id = 'RefuelInFlight', end
params = {}
})
end
end)
end end
-- Stage 3: Rearm and Repair -- Stage 3: Rearm
if stage >= 3 then if stage >= 3 then
pcall(function() pcall(function()
local dcsUnit = unit:GetUnit() -- Rearm all pylons to full
if dcsUnit then local ammo = dcsUnit:getAmmo()
-- Note: DCS doesn't have direct Lua API for ground rearm/repair if ammo then
-- This simulates the presence of the service zone for _, wpn in ipairs(ammo) do
-- In practice, DCS may auto-service units near FARP statics if wpn.count then
-- Set to full ammo (this is a workaround - DCS has limited rearm API)
dcsUnit:setAmmo(wpn.type_name, wpn.count + 100)
end
end
serviced = true
end end
end) end)
-- Repair (restore hit points)
local life = dcsUnit:getLife()
local life0 = dcsUnit:getLife0()
if life and life0 and life < life0 then
-- Repair 20% per tick (takes ~15 seconds for full repair)
dcsUnit:setLife(math.min(life0, life + (life0 * 0.20)))
serviced = true
end
end end
if serviced then
state.lastService = now
local gname = unit:GetGroup():GetName()
if stage >= 3 then
MESSAGE:New(string.format('FARP: Servicing %s (Refuel/Rearm/Repair)', unit:GetTypeName()), 5):ToGroup(GROUP:FindByName(gname))
else
MESSAGE:New(string.format('FARP: Refueling %s', unit:GetTypeName()), 5):ToGroup(GROUP:FindByName(gname))
end
end
end
else
-- Aircraft not on ground, reset service timer
if CTLD._farpServiceState[uname] then
CTLD._farpServiceState[uname].lastService = 0
end end
end end
end end
end end
end, {}, 0, 5) -- Start immediately, repeat every 5 seconds end, {}, 0, 2) -- Start immediately, repeat every 2 seconds
_logDebug(string.format('FARP service scheduler started for %s', farpZoneName)) _logDebug(string.format('FARP service scheduler started for %s', farpZoneName))
end end
@ -11121,7 +11213,12 @@ function CTLD:InitMEDEVAC()
-- Initialize salvage pools -- Initialize salvage pools
if CTLD.MEDEVAC.Salvage and CTLD.MEDEVAC.Salvage.Enabled then if CTLD.MEDEVAC.Salvage and CTLD.MEDEVAC.Salvage.Enabled then
CTLD._salvagePoints[self.Side] = CTLD._salvagePoints[self.Side] or 0 local before = CTLD._salvagePoints[self.Side]
-- Check instance config for InitialSalvage override
local initialValue = (self.Config.MEDEVAC and self.Config.MEDEVAC.InitialSalvage) or 0
CTLD._salvagePoints[self.Side] = CTLD._salvagePoints[self.Side] or initialValue
local after = CTLD._salvagePoints[self.Side]
env.info(string.format('[InitMEDEVAC] Side=%s Salvage BEFORE=%s InitialValue=%s AFTER=%s', tostring(self.Side), tostring(before), tostring(initialValue), tostring(after)))
end end
-- Setup event handler for unit deaths -- Setup event handler for unit deaths
@ -12937,8 +13034,11 @@ function CTLD:_TryUseSalvageForCrate(group, crateKey, catalogEntry)
if not cfg or not cfg.Enabled then return false end if not cfg or not cfg.Enabled then return false end
if not cfg.AutoApply then return false end if not cfg.AutoApply then return false end
-- Check if item has salvage value -- Check if item has salvage value (use same fallback logic as MEDEVAC)
local salvageCost = (catalogEntry and catalogEntry.salvageValue) or 0 local salvageCost = catalogEntry.salvageValue
if not salvageCost then
salvageCost = catalogEntry.required or cfg.DefaultValue or 1
end
if salvageCost <= 0 then return false end if salvageCost <= 0 then return false end
-- Check if we have enough salvage -- Check if we have enough salvage
@ -12981,7 +13081,12 @@ function CTLD:_CanUseSalvageForCrate(crateKey, catalogEntry, quantity)
if not cfg.AutoApply then return false end if not cfg.AutoApply then return false end
quantity = quantity or 1 quantity = quantity or 1
local salvageCost = ((catalogEntry and catalogEntry.salvageValue) or 0) * quantity -- Check if item has salvage value (use same fallback logic as MEDEVAC)
local salvageCost = catalogEntry.salvageValue
if not salvageCost then
salvageCost = catalogEntry.required or cfg.DefaultValue or 1
end
salvageCost = salvageCost * quantity
if salvageCost <= 0 then return false end if salvageCost <= 0 then return false end
local available = CTLD._salvagePoints[self.Side] or 0 local available = CTLD._salvagePoints[self.Side] or 0
@ -13266,6 +13371,7 @@ function CTLD:ShowSalvagePoints(group)
end end
local salvage = CTLD._salvagePoints[self.Side] or 0 local salvage = CTLD._salvagePoints[self.Side] or 0
env.info('ShowSalvagePoints: self.Side = ' .. tostring(self.Side) .. ', CTLD._salvagePoints[self.Side] = ' .. tostring(CTLD._salvagePoints and CTLD._salvagePoints[self.Side] or 'nil') .. ', salvage = ' .. tostring(salvage))
local lines = {} local lines = {}
table.insert(lines, '=== Coalition Salvage Points ===') table.insert(lines, '=== Coalition Salvage Points ===')
@ -13641,18 +13747,19 @@ end
-- Create a Mobile MASH zone and start announcements -- Create a Mobile MASH zone and start announcements
function CTLD:_CreateMobileMASH(group, position, catalogDef) function CTLD:_CreateMobileMASH(group, position, catalogDef)
local cfg = CTLD.MEDEVAC _logInfo('[MobileMASH] _CreateMobileMASH called')
local cfg = self.Config.MEDEVAC
if not cfg or not cfg.Enabled then if not cfg or not cfg.Enabled then
_logDebug('[MobileMASH] Config missing or MEDEVAC disabled; aborting mobile deployment') _logInfo('[MobileMASH] Config missing or MEDEVAC disabled; aborting mobile deployment')
return return
end end
if not cfg.MobileMASH or not cfg.MobileMASH.Enabled then if not cfg.MobileMASH or not cfg.MobileMASH.Enabled then
_logDebug('[MobileMASH] MobileMASH feature disabled in config; aborting') _logInfo('[MobileMASH] MobileMASH feature disabled in config; aborting')
return return
end end
if not position or not position.x or not position.z then if not position or not position.x or not position.z then
_logError('[MobileMASH] Missing build position; aborting Mobile MASH deployment') _logInfo('[MobileMASH] Missing build position; aborting Mobile MASH deployment')
return return
end end
@ -13661,7 +13768,7 @@ function CTLD:_CreateMobileMASH(group, position, catalogDef)
local okPreview, namePreview = pcall(function() return group:getName() end) local okPreview, namePreview = pcall(function() return group:getName() end)
if okPreview and namePreview and namePreview ~= '' then groupNamePreview = namePreview end if okPreview and namePreview and namePreview ~= '' then groupNamePreview = namePreview end
end end
_logVerbose(string.format('[MobileMASH] Build requested for group %s at (%.1f, %.1f)', groupNamePreview, position.x or 0, position.z or 0)) _logInfo(string.format('[MobileMASH] Build requested for group %s at (%.1f, %.1f)', groupNamePreview, position.x or 0, position.z or 0))
local function safeGetName(g) local function safeGetName(g)
if not g then return nil end if not g then return nil end
@ -13681,12 +13788,12 @@ function CTLD:_CreateMobileMASH(group, position, catalogDef)
_logError('[MobileMASH] Unable to determine coalition side; aborting Mobile MASH deployment') _logError('[MobileMASH] Unable to determine coalition side; aborting Mobile MASH deployment')
return return
end end
_logDebug(string.format('[MobileMASH] Using coalition side %s (%s)', tostring(side), tostring(catalogDef.side or self.Side))) _logInfo(string.format('[MobileMASH] Using coalition side %s (%s)', tostring(side), tostring(catalogDef.side or self.Side)))
CTLD._mobileMASHCounter = CTLD._mobileMASHCounter or { [coalition.side.BLUE] = 0, [coalition.side.RED] = 0 } CTLD._mobileMASHCounter = CTLD._mobileMASHCounter or { [coalition.side.BLUE] = 0, [coalition.side.RED] = 0 }
CTLD._mobileMASHCounter[side] = (CTLD._mobileMASHCounter[side] or 0) + 1 CTLD._mobileMASHCounter[side] = (CTLD._mobileMASHCounter[side] or 0) + 1
local index = CTLD._mobileMASHCounter[side] local index = CTLD._mobileMASHCounter[side]
_logDebug(string.format('[MobileMASH] Assigned deployment index %d for side %s', index, tostring(side))) _logInfo(string.format('[MobileMASH] Assigned deployment index %d for side %s', index, tostring(side)))
local mashId = string.format('MOBILE_MASH_%d_%d', side, index) local mashId = string.format('MOBILE_MASH_%d_%d', side, index)
local displayName local displayName
@ -13695,13 +13802,13 @@ function CTLD:_CreateMobileMASH(group, position, catalogDef)
else else
displayName = string.format('Mobile MASH %d', index) displayName = string.format('Mobile MASH %d', index)
end end
_logDebug(string.format('[MobileMASH] mashId=%s displayName=%s recipeDesc=%s', mashId, tostring(displayName), tostring(catalogDef.description))) _logInfo(string.format('[MobileMASH] mashId=%s displayName=%s recipeDesc=%s', mashId, tostring(displayName), tostring(catalogDef.description)))
local initialPos = { x = position.x, z = position.z } local initialPos = { x = position.x, z = position.z }
local radius = cfg.MobileMASH.ZoneRadius or 500 local radius = cfg.MobileMASH.ZoneRadius or 500
local beaconFreq = cfg.MobileMASH.BeaconFrequency or '30.0 FM' local beaconFreq = cfg.MobileMASH.BeaconFrequency or '30.0 FM'
local mashGroupName = safeGetName(group) local mashGroupName = safeGetName(group)
_logDebug(string.format('[MobileMASH] Initial position (%.1f, %.1f) radius %.1f freq %s groupName=%s', initialPos.x or 0, initialPos.z or 0, radius, tostring(beaconFreq), tostring(mashGroupName))) _logInfo(string.format('[MobileMASH] Initial position (%.1f, %.1f) radius %.1f freq %s groupName=%s', initialPos.x or 0, initialPos.z or 0, radius, tostring(beaconFreq), tostring(mashGroupName)))
local function buildZoneObject(name, r, pos) local function buildZoneObject(name, r, pos)
if ZONE_RADIUS and VECTOR2 and VECTOR2.New then if ZONE_RADIUS and VECTOR2 and VECTOR2.New then
@ -13844,7 +13951,7 @@ function CTLD:_CreateMobileMASH(group, position, catalogDef)
} }
CTLD._mashZones[mashId] = mashData CTLD._mashZones[mashId] = mashData
_logDebug(string.format('[MobileMASH] Registered mashId=%s displayName=%s zoneRadius=%.1f freq=%s', mashId, displayName, radius, tostring(beaconFreq))) _logInfo(string.format('[MobileMASH] Registered mashId=%s displayName=%s zoneRadius=%.1f freq=%s', mashId, displayName, radius, tostring(beaconFreq)))
self._ZoneDefs = self._ZoneDefs or { PickupZones = {}, DropZones = {}, FOBZones = {}, MASHZones = {} } self._ZoneDefs = self._ZoneDefs or { PickupZones = {}, DropZones = {}, FOBZones = {}, MASHZones = {} }
self._ZoneDefs.MASHZones = self._ZoneDefs.MASHZones or {} self._ZoneDefs.MASHZones = self._ZoneDefs.MASHZones or {}
@ -13857,31 +13964,31 @@ function CTLD:_CreateMobileMASH(group, position, catalogDef)
-- Add zone to MASHZones array so it's recognized by the system -- Add zone to MASHZones array so it's recognized by the system
self.MASHZones = self.MASHZones or {} self.MASHZones = self.MASHZones or {}
table.insert(self.MASHZones, zoneObj) table.insert(self.MASHZones, zoneObj)
_logDebug(string.format('[MobileMASH] Added zone to MASHZones array, total count: %d', #self.MASHZones)) _logInfo(string.format('[MobileMASH] Added zone to MASHZones array, total count: %d', #self.MASHZones))
local md = self.Config and self.Config.MapDraw or {} -- Add to MEDEVAC zones if MEDEVAC is active
if md.Enabled then if self.MEDEVAC and self.MEDEVAC.AddZone then
local ok, err = pcall(function() self:DrawZonesOnMap() end) local ok, err = pcall(function()
if not ok then self.MEDEVAC:AddZone(displayName, zoneObj)
_logError(string.format('DrawZonesOnMap failed after Mobile MASH creation: %s', tostring(err))) end)
if ok then
_logInfo(string.format('[MobileMASH] Added zone to MEDEVAC system: %s', displayName))
-- Refresh MEDEVAC menu to include the new zone
pcall(function() self.MEDEVAC:__Start(1) end)
else
_logDebug(string.format('[MobileMASH] Could not add to MEDEVAC system: %s', tostring(err)))
end end
else end
local circleId = _nextMarkupId()
local textId = _nextMarkupId()
local p = { x = initialPos.x, y = 0, z = initialPos.z }
local colors = cfg.MASHZoneColors or {} -- Auto-draw the new zone on the map using the update function
local borderColor = colors.border or {1, 1, 0, 0.85} -- This ensures the drawing is tracked properly and can be updated/removed later
local fillColor = colors.fill or {1, 0.75, 0.8, 0.25} local md = self.Config and self.Config.MapDraw or {}
if md.Enabled and md.DrawMASHZones then
trigger.action.circleToCoalition(side, circleId, p, radius, borderColor, fillColor, 1, true, "") _logInfo('[MobileMASH] Drawing new Mobile MASH zone on map')
local ok, err = pcall(function() self:_updateMobileMASHDrawing(mashId) end)
local textPos = { x = p.x, y = 0, z = p.z - radius - 50 } if not ok then
trigger.action.textToCoalition(side, textId, textPos, {1,1,1,0.9}, {0,0,0,0}, 18, true, displayName) _logError(string.format('_updateMobileMASHDrawing failed after Mobile MASH creation: %s', tostring(err)))
end
mashData.circleId = circleId
mashData.textId = textId
_logDebug(string.format('[MobileMASH] Drawn map circleId=%d textId=%d', circleId, textId))
end end
local gridStr = self:_GetMGRSString(initialPos) local gridStr = self:_GetMGRSString(initialPos)
@ -13930,7 +14037,9 @@ function CTLD:_CreateMobileMASH(group, position, catalogDef)
-- Create a separate frequent position update scheduler for mobile MASH tracking -- Create a separate frequent position update scheduler for mobile MASH tracking
-- This ensures the zone follows the vehicle even if announcements are infrequent -- This ensures the zone follows the vehicle even if announcements are infrequent
local ctldInstance = self local ctldInstance = self
local positionUpdateInterval = 5 -- Update position every 5 seconds local positionUpdateInterval = 15 -- Update position every 15 seconds
local mapRedrawInterval = 15 -- Redraw map every 15 seconds
local updatesSinceRedraw = 0
local posScheduler = SCHEDULER:New(nil, function() local posScheduler = SCHEDULER:New(nil, function()
local ok, err = pcall(function() local ok, err = pcall(function()
if not groupIsAlive() then if not groupIsAlive() then
@ -13949,13 +14058,20 @@ function CTLD:_CreateMobileMASH(group, position, catalogDef)
end end
end end
_logDebug(string.format('[MobileMASH] Position updated for %s at (%.1f, %.1f)', displayName, vec3.x, vec3.z)) _logDebug(string.format('[MobileMASH] Position updated for %s at (%.1f, %.1f)', displayName, vec3.x, vec3.z))
-- Redraw map only every 120 seconds
updatesSinceRedraw = updatesSinceRedraw + positionUpdateInterval
if updatesSinceRedraw >= mapRedrawInterval then
pcall(function() ctldInstance:_updateMobileMASHDrawing(mashId) end)
updatesSinceRedraw = 0
end
end end
end) end)
if not ok then _logError('Mobile MASH position update scheduler error: '..tostring(err)) end if not ok then _logError('Mobile MASH position update scheduler error: '..tostring(err)) end
end, {}, positionUpdateInterval, positionUpdateInterval) end, {}, positionUpdateInterval, positionUpdateInterval)
mashData.positionScheduler = posScheduler mashData.positionScheduler = posScheduler
_logDebug(string.format('[MobileMASH] Position update scheduler started every %ds', positionUpdateInterval)) _logDebug(string.format('[MobileMASH] Position update scheduler started every %ds (map redraw every %ds)', positionUpdateInterval, mapRedrawInterval))
if EVENTHANDLER then if EVENTHANDLER then
local ctldInstance = self local ctldInstance = self
@ -14025,6 +14141,12 @@ function CTLD:_RemoveMobileMASH(mashId)
end end
end end
-- Remove from MEDEVAC system if possible
if self.MEDEVAC and self.MEDEVAC.RemoveZone then
pcall(function() self.MEDEVAC:RemoveZone(name) end)
_logDebug(string.format('[MobileMASH] Attempted to remove zone from MEDEVAC system: %s', name))
end
-- Send destruction message -- Send destruction message
local msg = _fmtTemplate(CTLD.Messages.medevac_mash_destroyed, { local msg = _fmtTemplate(CTLD.Messages.medevac_mash_destroyed, {
mash_id = string.match(mashId, 'MOBILE_MASH_%d+_(%d+)') or '?' mash_id = string.match(mashId, 'MOBILE_MASH_%d+_(%d+)') or '?'
@ -14700,6 +14822,19 @@ function CTLD:CreateSalvageZoneAtGroup(group)
local coord = COORDINATE:NewFromVec3(pos) local coord = COORDINATE:NewFromVec3(pos)
local radius = cfg.DefaultZoneRadius or 300 local radius = cfg.DefaultZoneRadius or 300
-- Check for nearby salvage cargo (prevent zone placement within 1km of cargo)
local minDistance = 1000 -- 1km
for crateName, meta in pairs(CTLD._salvageCrates or {}) do
if meta and meta.position then
local cratePos = meta.position
local distance = math.sqrt((pos.x - cratePos.x)^2 + (pos.z - cratePos.z)^2)
if distance < minDistance then
_msgGroup(group, string.format('Cannot create salvage zone within %.0fm of existing salvage cargo. Nearest cargo is %.0fm away.', minDistance, distance))
return
end
end
end
self._DynamicSalvageZones = self._DynamicSalvageZones or {} self._DynamicSalvageZones = self._DynamicSalvageZones or {}
self._DynamicSalvageQueue = self._DynamicSalvageQueue or {} self._DynamicSalvageQueue = self._DynamicSalvageQueue or {}

View File

@ -25,18 +25,29 @@ local blueCfg = {
'UH-1H','Mi-8MTV2','Mi-24P','SA342M','SA342L','SA342Minigun','UH-60L','CH-47Fbl1','CH-47F','Mi-17','GazelleAI' 'UH-1H','Mi-8MTV2','Mi-24P','SA342M','SA342L','SA342Minigun','UH-60L','CH-47Fbl1','CH-47F','Mi-17','GazelleAI'
}, },
-- Optional: drive zone activation from mission flags (preferred: set per-zone below via flag/activeWhen) -- Optional: drive zone activation from mission flags (preferred: set per-zone below via flag/activeWhen)
MapDraw = { MEDEVAC = {
Enabled = true, Enabled = true,
DrawMASHZones = true, -- Enable MASH zone drawing InitialSalvage = 25, -- Starting salvage points for this coalition
}, MobileMASH = {
Enabled = true,
Zones = { ZoneRadius = 300,
BeaconFrequency = '32.0 FM',
AnnouncementInterval = 300, -- Announce position every 5 minutes
},
},
MapDraw = {
Enabled = true,
DrawMASHZones = true, -- Enable MASH zone drawing
},
Zones = {
PickupZones = { { name = 'ALPHA', flag = 9001, activeWhen = 0 } }, PickupZones = { { name = 'ALPHA', flag = 9001, activeWhen = 0 } },
DropZones = { { name = 'BRAVO', flag = 9002, activeWhen = 0 } }, DropZones = { { name = 'BRAVO', flag = 9002, activeWhen = 0 } },
FOBZones = { { name = 'CHARLIE', flag = 9003, activeWhen = 0 } }, FOBZones = { { name = 'CHARLIE', flag = 9003, activeWhen = 0 } },
MASHZones = { { name = 'MASH Alpha', freq = '251.0 AM', radius = 500, flag = 9010, activeWhen = 0 } }, MASHZones = { { name = 'MASH Alpha', freq = '251.0 AM', radius = 300, flag = 9010, activeWhen = 0 } },
SalvageDropZones = { { name = 'S1', flag = 9020, radius = 500, activeWhen = 0 } }, SalvageDropZones = { { name = 'S1', flag = 9020, radius = 300, activeWhen = 0 } },
}, },
BuildRequiresGroundCrates = true, BuildRequiresGroundCrates = true,
} }
@ -45,6 +56,7 @@ if blueCfg.Zones and blueCfg.Zones.MASHZones and blueCfg.Zones.MASHZones[1] then
env.info('[DEBUG] blueCfg.Zones.MASHZones[1].name: ' .. tostring(blueCfg.Zones.MASHZones[1].name)) env.info('[DEBUG] blueCfg.Zones.MASHZones[1].name: ' .. tostring(blueCfg.Zones.MASHZones[1].name))
end end
ctldBlue = _MOOSE_CTLD:New(blueCfg) ctldBlue = _MOOSE_CTLD:New(blueCfg)
env.info('[CTLD_INIT] After BLUE init, salvage = ' .. tostring((CTLD._salvagePoints and CTLD._salvagePoints[coalition.side.BLUE]) or 'nil'))
local redCfg = { local redCfg = {
CoalitionSide = coalition.side.RED, CoalitionSide = coalition.side.RED,
@ -55,17 +67,28 @@ local redCfg = {
}, },
-- Optional: drive zone activation for RED via per-zone flag/activeWhen -- Optional: drive zone activation for RED via per-zone flag/activeWhen
MapDraw = { MEDEVAC = {
Enabled = true, Enabled = true,
DrawMASHZones = true, -- Enable MASH zone drawing InitialSalvage = 25, -- Starting salvage points for this coalition
}, MobileMASH = {
Enabled = true,
Zones = { ZoneRadius = 300,
BeaconFrequency = '30.0 FM',
AnnouncementInterval = 1800, -- Announce position every 30 minutes
},
},
MapDraw = {
Enabled = true,
DrawMASHZones = true, -- Enable MASH zone drawing
},
Zones = {
PickupZones = { { name = 'DELTA', flag = 9101, activeWhen = 0 } }, PickupZones = { { name = 'DELTA', flag = 9101, activeWhen = 0 } },
DropZones = { { name = 'ECHO', flag = 9102, activeWhen = 0 } }, DropZones = { { name = 'ECHO', flag = 9102, activeWhen = 0 } },
FOBZones = { { name = 'FOXTROT', flag = 9103, activeWhen = 0 } }, FOBZones = { { name = 'FOXTROT', flag = 9103, activeWhen = 0 } },
MASHZones = { { name = 'MASH Bravo', freq = '252.0 AM', radius = 500, flag = 9111, activeWhen = 0 } }, MASHZones = { { name = 'MASH Bravo', freq = '252.0 AM', radius = 300, flag = 9111, activeWhen = 0 } },
SalvageDropZones = { { name = 'S2', flag = 9020, radius = 500, activeWhen = 0 } }, SalvageDropZones = { { name = 'S2', flag = 9020, radius = 300, activeWhen = 0 } },
}, },
BuildRequiresGroundCrates = true, BuildRequiresGroundCrates = true,
} }
@ -74,6 +97,7 @@ if redCfg.Zones and redCfg.Zones.MASHZones and redCfg.Zones.MASHZones[1] then
env.info('[DEBUG] redCfg.Zones.MASHZones[1].name: ' .. tostring(redCfg.Zones.MASHZones[1].name)) env.info('[DEBUG] redCfg.Zones.MASHZones[1].name: ' .. tostring(redCfg.Zones.MASHZones[1].name))
end end
ctldRed = _MOOSE_CTLD:New(redCfg) ctldRed = _MOOSE_CTLD:New(redCfg)
env.info('[CTLD_INIT] After RED init, salvage = ' .. tostring((CTLD._salvagePoints and CTLD._salvagePoints[coalition.side.RED]) or 'nil'))
-- Merge catalog into both CTLD instances if catalog was loaded -- Merge catalog into both CTLD instances if catalog was loaded
env.info('[init_mission_dual_coalition] Checking for catalog: '..((_CTLD_EXTRACTED_CATALOG and 'FOUND') or 'NOT FOUND')) env.info('[init_mission_dual_coalition] Checking for catalog: '..((_CTLD_EXTRACTED_CATALOG and 'FOUND') or 'NOT FOUND'))
@ -93,6 +117,7 @@ else
env.info('[init_mission_dual_coalition] WARNING: _CTLD_EXTRACTED_CATALOG not found - catalog not loaded!') env.info('[init_mission_dual_coalition] WARNING: _CTLD_EXTRACTED_CATALOG not found - catalog not loaded!')
env.info('[init_mission_dual_coalition] Available globals: '..((_G._CTLD_EXTRACTED_CATALOG and 'in _G') or 'not in _G')) env.info('[init_mission_dual_coalition] Available globals: '..((_G._CTLD_EXTRACTED_CATALOG and 'in _G') or 'not in _G'))
end end
env.info('[CTLD_INIT] End of init - BLUE salvage: ' .. tostring(CTLD._salvagePoints and CTLD._salvagePoints[coalition.side.BLUE] or 'nil') .. ', RED salvage: ' .. tostring(CTLD._salvagePoints and CTLD._salvagePoints[coalition.side.RED] or 'nil'))
else else
env.info('[init_mission_dual_coalition] Moose or CTLD missing; skipping CTLD init') env.info('[init_mission_dual_coalition] Moose or CTLD missing; skipping CTLD init')
end end

BIN
Moose_CTLD_NoBattle.miz Normal file

Binary file not shown.

Binary file not shown.

View File

@ -268,20 +268,14 @@ cat['BLUE_MQ9'] = { menuCategory='Drones', menu='MQ-9 Reaper - JTA
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' } } 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'] = { hidden=true, description='FOB small crate', dcsCargoType='container_cargo', required=1, initialStock=12, side=nil, category=Group.Category.GROUND, build=function(point, headingDeg) cat['FOB_SMALL'] = { hidden=true, description='FOB small crate', dcsCargoType='container_cargo', required=1, initialStock=12, side=nil, category=Group.Category.GROUND }
-- spawns a harmless placeholder truck for visibility; consumed by FOB_SITE build
return singleUnit('Ural-375')(point, headingDeg)
end }
cat['FOB_SITE'] = { menuCategory='Support', menu='FOB Crates - All', description='FOB Site', isFOB=true, dcsCargoType='container_cargo', requires={ FOB_SMALL=3 }, initialStock=0, side=nil, category=Group.Category.GROUND, cat['FOB_SITE'] = { menuCategory='Support', menu='FOB Crates - All', description='FOB Site', isFOB=true, dcsCargoType='container_cargo', requires={ FOB_SMALL=3 }, initialStock=0, side=nil, category=Group.Category.GROUND,
build=multiUnits({ {type='HEMTT TFFT'}, {type='Ural-375 PBU', dx=10, dz=8}, {type='Ural-375', dx=-10, dz=8} }) } build=multiUnits({ {type='HEMTT TFFT'}, {type='Ural-375 PBU', dx=10, dz=8}, {type='Ural-375', dx=-10, dz=8} }) }
-- Mobile MASH (Support) — three crates build a Mobile MASH unit -- Mobile MASH (Support) — three crates build a Mobile MASH unit
cat['MOBILE_MASH_SMALL'] = { hidden=true, description='Mobile MASH crate', dcsCargoType='container_cargo', required=1, initialStock=6, side=nil, category=Group.Category.GROUND, build=function(point, headingDeg) cat['MOBILE_MASH_SMALL'] = { hidden=true, description='Mobile MASH crate', dcsCargoType='container_cargo', required=1, initialStock=3, side=nil, category=Group.Category.GROUND }
-- spawns placeholder truck for visibility; consumed by MOBILE_MASH build cat['BLUE_MOBILE_MASH'] = { menuCategory='Support', menu='Mobile MASH - All', description='Blue Mobile MASH Unit', isMobileMASH=true, dcsCargoType='container_cargo', requires={ MOBILE_MASH_SMALL=3 }, initialStock=0, side=BLUE, category=Group.Category.GROUND, build=multiUnits({ {type='M-113'}, {type='M-113', dx=10, dz=8}, {type='M-113', dx=-10, dz=8} }) }
return singleUnit('Ural-375')(point, headingDeg) cat['RED_MOBILE_MASH'] = { menuCategory='Support', menu='Mobile MASH - All', description='Red Mobile MASH Unit', isMobileMASH=true, dcsCargoType='container_cargo', requires={ MOBILE_MASH_SMALL=3 }, initialStock=0, side=RED, category=Group.Category.GROUND, build=multiUnits({ {type='BTR_D'}, {type='BTR_D', dx=10, dz=8}, {type='BTR_D', dx=-10, dz=8} }) }
end }
cat['BLUE_MOBILE_MASH'] = { menuCategory='Support', menu='Mobile MASH - All', description='Blue Mobile MASH Unit', isMobileMASH=true, dcsCargoType='container_cargo', requires={ MOBILE_MASH_SMALL=3 }, initialStock=0, side=BLUE, category=Group.Category.GROUND, build=singleUnit('M-113') }
cat['RED_MOBILE_MASH'] = { menuCategory='Support', menu='Mobile MASH - All', description='Red Mobile MASH Unit', isMobileMASH=true, dcsCargoType='container_cargo', requires={ MOBILE_MASH_SMALL=3 }, initialStock=0, side=RED, category=Group.Category.GROUND, build=singleUnit('BTR_D') }
-- ========================= -- =========================
-- Troop Type Definitions -- Troop Type Definitions

View File

@ -268,20 +268,14 @@ cat['BLUE_MQ9'] = { menuCategory='Drones', menu='MQ-9 Reaper - JTA
cat['RED_WINGLOONG'] = { menuCategory='Drones', menu='WingLoong-I - JTAC', description='WingLoong-I JTAC', dcsCargoType='container_cargo', required=1, initialStock=1, side=RED, category=Group.Category.AIRPLANE, build=singleAirUnit('WingLoong-I'), roles={'JTAC'}, jtac={ platform='air' } } cat['RED_WINGLOONG'] = { menuCategory='Drones', menu='WingLoong-I - JTAC', description='WingLoong-I JTAC', dcsCargoType='container_cargo', required=1, initialStock=1, 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'] = { hidden=true, description='FOB small crate', dcsCargoType='container_cargo', required=1, initialStock=6, side=nil, category=Group.Category.GROUND, build=function(point, headingDeg) cat['FOB_SMALL'] = { hidden=true, description='FOB small crate', dcsCargoType='container_cargo', required=1, initialStock=6, side=nil, category=Group.Category.GROUND }
-- spawns a harmless placeholder truck for visibility; consumed by FOB_SITE build
return singleUnit('Ural-375')(point, headingDeg)
end }
cat['FOB_SITE'] = { menuCategory='Support', menu='FOB Crates - All', description='FOB Site', isFOB=true, dcsCargoType='container_cargo', requires={ FOB_SMALL=3 }, initialStock=0, side=nil, category=Group.Category.GROUND, cat['FOB_SITE'] = { menuCategory='Support', menu='FOB Crates - All', description='FOB Site', isFOB=true, dcsCargoType='container_cargo', requires={ FOB_SMALL=3 }, initialStock=0, side=nil, category=Group.Category.GROUND,
build=multiUnits({ {type='HEMTT TFFT'}, {type='Ural-375 PBU', dx=10, dz=8}, {type='Ural-375', dx=-10, dz=8} }) } build=multiUnits({ {type='HEMTT TFFT'}, {type='Ural-375 PBU', dx=10, dz=8}, {type='Ural-375', dx=-10, dz=8} }) }
-- Mobile MASH (Support) — three crates build a Mobile MASH unit -- Mobile MASH (Support) — three crates build a Mobile MASH unit
cat['MOBILE_MASH_SMALL'] = { hidden=true, description='Mobile MASH crate', dcsCargoType='container_cargo', required=1, initialStock=3, side=nil, category=Group.Category.GROUND, build=function(point, headingDeg) cat['MOBILE_MASH_SMALL'] = { hidden=true, description='Mobile MASH crate', dcsCargoType='container_cargo', required=1, initialStock=3, side=nil, category=Group.Category.GROUND }
-- spawns placeholder truck for visibility; consumed by MOBILE_MASH build cat['BLUE_MOBILE_MASH'] = { menuCategory='Support', menu='Mobile MASH - All', description='Blue Mobile MASH Unit', isMobileMASH=true, dcsCargoType='container_cargo', requires={ MOBILE_MASH_SMALL=3 }, initialStock=0, side=BLUE, category=Group.Category.GROUND, build=multiUnits({ {type='M-113'}, {type='M-113', dx=10, dz=8}, {type='M-113', dx=-10, dz=8} }) }
return singleUnit('Ural-375')(point, headingDeg) cat['RED_MOBILE_MASH'] = { menuCategory='Support', menu='Mobile MASH - All', description='Red Mobile MASH Unit', isMobileMASH=true, dcsCargoType='container_cargo', requires={ MOBILE_MASH_SMALL=3 }, initialStock=0, side=RED, category=Group.Category.GROUND, build=multiUnits({ {type='BTR_D'}, {type='BTR_D', dx=10, dz=8}, {type='BTR_D', dx=-10, dz=8} }) }
end }
cat['BLUE_MOBILE_MASH'] = { menuCategory='Support', menu='Mobile MASH - All', description='Blue Mobile MASH Unit', isMobileMASH=true, dcsCargoType='container_cargo', requires={ MOBILE_MASH_SMALL=3 }, initialStock=0, side=BLUE, category=Group.Category.GROUND, build=singleUnit('M-113') }
cat['RED_MOBILE_MASH'] = { menuCategory='Support', menu='Mobile MASH - All', description='Red Mobile MASH Unit', isMobileMASH=true, dcsCargoType='container_cargo', requires={ MOBILE_MASH_SMALL=3 }, initialStock=0, side=RED, category=Group.Category.GROUND, build=singleUnit('BTR_D') }
-- ========================= -- =========================
-- Troop Type Definitions -- Troop Type Definitions

File diff suppressed because it is too large Load Diff