Fixed several issues with smoke, and enhanced messages, menus, help systems.

This commit is contained in:
iTracerFacer 2025-11-09 10:52:38 -06:00
parent 2dedeb0b32
commit fb806bd926
4 changed files with 505 additions and 76 deletions

View File

@ -864,7 +864,7 @@ end
local function _getGroundSpeed(unit)
if not unit then return 0 end
local vel = unit:GetVelocity()
if not vel then return 0 end
if not vel or not vel.x or not vel.z then return 0 end
return math.sqrt(vel.x * vel.x + vel.z * vel.z)
end
@ -924,18 +924,27 @@ end
-- Helper: get nearest ACTIVE pickup zone (by configured list), respecting CTLD's active flags
function CTLD:_collectActivePickupDefs()
local out = {}
local added = {} -- Track added zone names to prevent duplicates
-- From config-defined zones
local defs = (self.Config and self.Config.Zones and self.Config.Zones.PickupZones) or {}
for _, z in ipairs(defs) do
local n = z.name
if (not n) or self._ZoneActive.Pickup[n] ~= false then table.insert(out, z) end
if (not n) or self._ZoneActive.Pickup[n] ~= false then
table.insert(out, z)
if n then added[n] = true end
end
end
-- From MOOSE zone objects if present
-- From MOOSE zone objects if present (skip if already added from config)
if self.PickupZones and #self.PickupZones > 0 then
for _, mz in ipairs(self.PickupZones) do
if mz and mz.GetName then
local n = mz:GetName()
if self._ZoneActive.Pickup[n] ~= false then table.insert(out, { name = n }) end
if self._ZoneActive.Pickup[n] ~= false and not added[n] then
table.insert(out, { name = n })
added[n] = true
end
end
end
end
@ -2438,6 +2447,48 @@ function CTLD:BuildGroupMenus(group)
-- Pop Smoke at MASH Zones
CMD('Pop Smoke at MASH Zones', medevacRoot, function() self:PopSmokeAtMASHZones(group) end)
-- Duplicate guide from Admin/Help -> Player Guides for quick access
MENU_GROUP_COMMAND:New(group, 'MASH & Salvage System - Guide', medevacRoot, function()
local lines = {}
table.insert(lines, 'MASH & Salvage System - Player Guide')
table.insert(lines, '')
table.insert(lines, 'What is it?')
table.insert(lines, '- MASH (Mobile Army Surgical Hospital) zones accept MEDEVAC crew deliveries.')
table.insert(lines, '- When ground vehicles are destroyed, crews spawn nearby and call for rescue.')
table.insert(lines, '- Rescuing crews and delivering them to MASH earns Salvage Points for your coalition.')
table.insert(lines, '- Salvage Points let you build out-of-stock items, keeping logistics flowing.')
table.insert(lines, '')
table.insert(lines, 'How MEDEVAC works:')
table.insert(lines, '- Vehicle destroyed → crew spawns after delay with invulnerability period.')
table.insert(lines, '- MEDEVAC request announced with grid coordinates and salvage value.')
table.insert(lines, '- Crews have a time limit (default 60 minutes); failure = crew KIA and vehicle lost.')
table.insert(lines, '- Fly to location, hover nearby, load troops normally - system detects MEDEVAC crew.')
table.insert(lines, '- Original vehicle respawns when crew is picked up (if enabled).')
table.insert(lines, '')
table.insert(lines, 'Delivering to MASH:')
table.insert(lines, '- Fly loaded crew to any MASH zone (fixed or mobile).')
table.insert(lines, '- Deploy troops inside MASH zone - salvage points awarded automatically.')
table.insert(lines, '- Coalition message shows points earned and new total.')
table.insert(lines, '')
table.insert(lines, 'Using Salvage Points:')
table.insert(lines, '- When crate requests fail (out of stock), salvage auto-applies if available.')
table.insert(lines, '- Each catalog item has a salvage cost (usually matches its value).')
table.insert(lines, '- Check current salvage: Coach & Nav -> MEDEVAC Status.')
table.insert(lines, '')
table.insert(lines, 'Mobile MASH:')
table.insert(lines, '- Build Mobile MASH crates to deploy field hospitals anywhere.')
table.insert(lines, '- Mobile MASH creates a new delivery zone with radio beacon.')
table.insert(lines, '- Multiple mobile MASHs can be deployed for forward operations.')
table.insert(lines, '')
table.insert(lines, 'Best practices:')
table.insert(lines, '- Monitor MEDEVAC requests: Coach & Nav -> Vectors to Nearest MEDEVAC Crew.')
table.insert(lines, '- Prioritize high-value vehicles (armor, AA) for maximum salvage.')
table.insert(lines, '- Deploy Mobile MASH near active combat zones to reduce delivery time.')
table.insert(lines, '- Coordinate with team: share MEDEVAC locations and salvage status.')
table.insert(lines, '- Watch for warnings: 15min and 5min alerts before crew timeout.')
MESSAGE:New(table.concat(lines, '\n'), 50):ToGroup(group)
end)
-- Admin/Settings submenu
local medevacAdminRoot = MENU_GROUP:New(group, 'Admin/Settings', medevacRoot)
CMD('Clear All MEDEVAC Missions', medevacAdminRoot, function() self:ClearAllMEDEVACMissions(group) end)
@ -2633,7 +2684,7 @@ function CTLD:BuildGroupMenus(group)
end)
-- Navigation -> Smoke Nearest Zone (Pickup/Drop/FOB)
CMD('Smoke Nearest Zone (Pickup/Drop/FOB)', navRoot, function()
CMD('Smoke Nearest Zone (Pickup/Drop/FOB/MASH)', navRoot, function()
local unit = group:GetUnit(1)
if not unit or not unit:IsAlive() then return end
@ -2663,12 +2714,22 @@ function CTLD:BuildGroupMenus(group)
end
end
return out
elseif kind == 'MASH' then
local out = {}
if CTLD._mashZones then
for name, data in pairs(CTLD._mashZones) do
if data and data.side == self.Side and data.zone then
table.insert(out, { name = name })
end
end
end
return out
end
return {}
end
local bestKind, bestZone, bestDist
for _, k in ipairs({ 'Pickup', 'Drop', 'FOB' }) do
for _, k in ipairs({ 'Pickup', 'Drop', 'FOB', 'MASH' }) do
local list = collectActive(k)
if list and #list > 0 then
local z, d = _nearestZonePoint(unit, list)
@ -2694,20 +2755,28 @@ function CTLD:BuildGroupMenus(group)
center = { x = center.x, y = center.y or 0, z = center.z }
end
-- Choose smoke color per kind (fallbacks if not configured)
local color = (bestKind == 'Pickup' and (self.Config.PickupZoneSmokeColor or trigger.smokeColor.Green))
or (bestKind == 'Drop' and trigger.smokeColor.Blue)
or trigger.smokeColor.White
-- Choose smoke color per kind
local color = trigger.smokeColor.Green -- default
if bestKind == 'Pickup' then
color = self.Config.PickupZoneSmokeColor or trigger.smokeColor.Green
elseif bestKind == 'Drop' then
color = trigger.smokeColor.Red
elseif bestKind == 'FOB' then
color = trigger.smokeColor.White
elseif bestKind == 'MASH' then
color = trigger.smokeColor.Orange
end
-- Apply smoke offset system (same as crate smoke)
-- Apply smoke offset system (use crate smoke config settings)
local smokeConfig = self.Config.CrateSmoke or {}
local smokePos = {
x = center.x,
y = land.getHeight({x = center.x, y = center.z}),
z = center.z
}
local offsetMeters = 5 -- Default offset
local offsetRandom = true
local offsetVertical = 2
local offsetMeters = tonumber(smokeConfig.OffsetMeters) or 5
local offsetRandom = (smokeConfig.OffsetRandom ~= false) -- default true
local offsetVertical = tonumber(smokeConfig.OffsetVertical) or 2
if offsetMeters > 0 then
local angle = 0
@ -2735,16 +2804,195 @@ function CTLD:BuildGroupMenus(group)
else
coord:SmokeGreen()
end
_msgGroup(group, string.format('Smoked nearest %s zone: %s', bestKind, bestZone:GetName()))
local distKm = bestDist / 1000
local distNm = bestDist / 1852
_msgGroup(group, string.format('Smoked nearest %s zone: %s (%.1f km / %.1f nm)', bestKind, bestZone:GetName(), distKm, distNm))
elseif trigger and trigger.action and trigger.action.smoke then
-- Fallback to trigger.action.smoke if MOOSE COORDINATE not available
trigger.action.smoke(smokePos, color)
_msgGroup(group, string.format('Smoked nearest %s zone: %s', bestKind, bestZone:GetName()))
local distKm = bestDist / 1000
local distNm = bestDist / 1852
_msgGroup(group, string.format('Smoked nearest %s zone: %s (%.1f km / %.1f nm)', bestKind, bestZone:GetName(), distKm, distNm))
else
_msgGroup(group, 'Smoke not available in this environment.')
end
end)
-- Smoke all nearby zones within range
CMD('Smoke All Nearby Zones (5km)', navRoot, function()
local unit = group:GetUnit(1)
if not unit or not unit:IsAlive() then return end
local maxRange = 5000 -- 5km in meters
-- Get unit position
local uname = unit:GetName()
local du = Unit.getByName and Unit.getByName(uname) or nil
if not du or not du:getPoint() then
_msgGroup(group, 'Unable to determine your position.')
return
end
local up = du:getPoint()
local ux, uz = up.x, up.z
-- Helper function to calculate distance and smoke a zone if in range
local function smokeZoneIfInRange(zoneName, zoneObj, zoneType, smokeColor)
if not zoneObj then return false end
-- Get zone center
local center
if self._getZoneCenterAndRadius then
center = select(1, self:_getZoneCenterAndRadius(zoneObj))
end
if not center and zoneObj.GetPointVec3 then
local v3 = zoneObj:GetPointVec3()
center = { x = v3.x, y = v3.y or 0, z = v3.z }
end
if not center then return false end
-- Calculate distance
local dx = center.x - ux
local dz = center.z - uz
local dist = math.sqrt(dx*dx + dz*dz)
if dist <= maxRange then
-- Apply smoke offset system
local smokeConfig = self.Config.CrateSmoke or {}
local smokePos = {
x = center.x,
y = land.getHeight({x = center.x, y = center.z}),
z = center.z
}
local offsetMeters = tonumber(smokeConfig.OffsetMeters) or 5
local offsetRandom = (smokeConfig.OffsetRandom ~= false)
local offsetVertical = tonumber(smokeConfig.OffsetVertical) or 2
if offsetMeters > 0 then
local angle = 0
if offsetRandom then
angle = math.random() * 2 * math.pi
end
smokePos.x = smokePos.x + offsetMeters * math.cos(angle)
smokePos.z = smokePos.z + offsetMeters * math.sin(angle)
end
smokePos.y = smokePos.y + offsetVertical
-- Spawn smoke
local coord = COORDINATE:New(smokePos.x, smokePos.y, smokePos.z)
if coord and coord.Smoke then
if smokeColor == trigger.smokeColor.Green then
coord:SmokeGreen()
elseif smokeColor == trigger.smokeColor.Red then
coord:SmokeRed()
elseif smokeColor == trigger.smokeColor.White then
coord:SmokeWhite()
elseif smokeColor == trigger.smokeColor.Orange then
coord:SmokeOrange()
elseif smokeColor == trigger.smokeColor.Blue then
coord:SmokeBlue()
else
coord:SmokeGreen()
end
else
trigger.action.smoke(smokePos, smokeColor)
end
return true, dist
end
return false, dist
end
-- Helper to get color name
local function getColorName(color)
if color == trigger.smokeColor.Green then return "Green"
elseif color == trigger.smokeColor.Red then return "Red"
elseif color == trigger.smokeColor.White then return "White"
elseif color == trigger.smokeColor.Orange then return "Orange"
elseif color == trigger.smokeColor.Blue then return "Blue"
else return "Unknown" end
end
local count = 0
local zones = {}
-- Check Pickup zones
local pickupDefs = self:_collectActivePickupDefs()
for _, def in ipairs(pickupDefs or {}) do
local mz = _findZone(def)
if mz then
-- Check for zone-specific smoke override, then fall back to config default
local zdef = self._ZoneDefs and self._ZoneDefs.PickupZones and self._ZoneDefs.PickupZones[def.name]
local smokeColor = (zdef and zdef.smoke) or self.Config.PickupZoneSmokeColor or trigger.smokeColor.Green
local smoked, dist = smokeZoneIfInRange(def.name, mz, 'Pickup', smokeColor)
if smoked then
count = count + 1
local zp = mz:GetPointVec3()
local brg = _bearingDeg({ x = ux, z = uz }, { x = zp.x, z = zp.z })
table.insert(zones, string.format('Pickup: %s - %.1f km @ %03d° (%s)', def.name, dist/1000, brg, getColorName(smokeColor)))
end
end
end
-- Check Drop zones
for _, mz in ipairs(self.DropZones or {}) do
if mz and mz.GetName then
local n = mz:GetName()
if (self._ZoneActive and self._ZoneActive.Drop and self._ZoneActive.Drop[n] ~= false) then
local smokeColor = trigger.smokeColor.Red
local smoked, dist = smokeZoneIfInRange(n, mz, 'Drop', smokeColor)
if smoked then
count = count + 1
local zp = mz:GetPointVec3()
local brg = _bearingDeg({ x = ux, z = uz }, { x = zp.x, z = zp.z })
table.insert(zones, string.format('Drop: %s - %.1f km @ %03d° (%s)', n, dist/1000, brg, getColorName(smokeColor)))
end
end
end
end
-- Check FOB zones
for _, mz in ipairs(self.FOBZones or {}) do
if mz and mz.GetName then
local n = mz:GetName()
if (self._ZoneActive and self._ZoneActive.FOB and self._ZoneActive.FOB[n] ~= false) then
local smokeColor = trigger.smokeColor.White
local smoked, dist = smokeZoneIfInRange(n, mz, 'FOB', smokeColor)
if smoked then
count = count + 1
local zp = mz:GetPointVec3()
local brg = _bearingDeg({ x = ux, z = uz }, { x = zp.x, z = zp.z })
table.insert(zones, string.format('FOB: %s - %.1f km @ %03d° (%s)', n, dist/1000, brg, getColorName(smokeColor)))
end
end
end
end
-- Check MASH zones
if CTLD._mashZones then
for name, data in pairs(CTLD._mashZones) do
if data and data.side == self.Side and data.zone then
local smokeColor = trigger.smokeColor.Orange
local smoked, dist = smokeZoneIfInRange(name, data.zone, 'MASH', smokeColor)
if smoked then
count = count + 1
local zp = data.zone:GetPointVec3()
local brg = _bearingDeg({ x = ux, z = uz }, { x = zp.x, z = zp.z })
table.insert(zones, string.format('MASH: %s - %.1f km @ %03d° (%s)', name, dist/1000, brg, getColorName(smokeColor)))
end
end
end
end
if count == 0 then
_msgGroup(group, string.format('No zones found within %.1f km.', maxRange/1000), 10)
else
local msg = string.format('Smoked %d zone(s) within %.1f km:\n%s', count, maxRange/1000, table.concat(zones, '\n'))
_msgGroup(group, msg, 15)
end
end)
-- Navigation -> MEDEVAC menu items (if MEDEVAC enabled)
if CTLD.MEDEVAC and CTLD.MEDEVAC.Enabled then
CMD('Vectors to Nearest MEDEVAC Crew', navRoot, function()
@ -2802,7 +3050,7 @@ function CTLD:BuildGroupMenus(group)
local nearestDist = math.huge
-- Find nearest MASH of same coalition
for _, mashData in ipairs(CTLD._mashZones or {}) do
for _, mashData in pairs(CTLD._mashZones or {}) do
if mashData.side == self.Side then
local dx = mashData.position.x - pos.x
local dz = mashData.position.z - pos.z
@ -2860,7 +3108,7 @@ function CTLD:BuildGroupMenus(group)
end
local salvage = CTLD._salvagePoints[self.Side] or 0
local mashCount = 0
for _, m in ipairs(CTLD._mashZones or {}) do
for _, m in pairs(CTLD._mashZones or {}) do
if m.side == self.Side then mashCount = mashCount + 1 end
end
msg = msg .. string.format('\n\nMEDEVAC:\nActive requests: %d\nMASH zones: %d\nSalvage points: %d',
@ -6481,7 +6729,7 @@ function CTLD:ShowSalvagePoints(group)
_msgGroup(group, table.concat(lines, '\n'), 20)
end
-- Vectors to nearest MEDEVAC
-- Vectors to nearest MEDEVAC (shows top 3 with time remaining)
function CTLD:VectorsToNearestMEDEVAC(group)
local cfg = CTLD.MEDEVAC
if not cfg or not cfg.Enabled then
@ -6496,44 +6744,69 @@ function CTLD:VectorsToNearestMEDEVAC(group)
if not pos then return end
local heading = unit:GetHeading() or 0
local isMetric = _getPlayerIsMetric(unit)
local nearest = nil
local nearestDist = math.huge
-- Collect all active MEDEVAC requests with distance
local requests = {}
for crewGroupName, data in pairs(CTLD._medevacCrews or {}) do
if data.side == self.Side and data.requestTime then
if data.side == self.Side and data.requestTime and not data.pickedUp then
local dist = math.sqrt((data.position.x - pos.x)^2 + (data.position.z - pos.z)^2)
if dist < nearestDist then
nearestDist = dist
nearest = data
end
table.insert(requests, {
data = data,
distance = dist
})
end
end
if not nearest then
if #requests == 0 then
_msgGroup(group, 'No active MEDEVAC requests.')
return
end
local dx = nearest.position.x - pos.x
local dz = nearest.position.z - pos.z
local bearing = math.deg(math.atan2(dz, dx))
if bearing < 0 then bearing = bearing + 360 end
local relativeBrg = bearing - heading
if relativeBrg < 0 then relativeBrg = relativeBrg + 360 end
if relativeBrg > 180 then relativeBrg = relativeBrg - 360 end
local distKm = nearestDist / 1000
local distNm = nearestDist / 1852
-- Sort by distance (closest first)
table.sort(requests, function(a, b) return a.distance < b.distance end)
-- Show top 3 (or fewer if less than 3 exist)
local lines = {}
table.insert(lines, string.format('MEDEVAC VECTORS: %s crew', nearest.vehicleType))
table.insert(lines, string.format('Bearing: %03d°', math.floor(bearing + 0.5)))
table.insert(lines, string.format('Relative: %+.0f°', relativeBrg))
table.insert(lines, string.format('Range: %.1f km / %.1f nm', distKm, distNm))
table.insert(lines, 'MEDEVAC VECTORS (nearest 3):')
table.insert(lines, '')
_msgGroup(group, table.concat(lines, '\n'), 15)
local maxShow = math.min(3, #requests)
for i = 1, maxShow do
local req = requests[i]
local data = req.data
local dist = req.distance
local dx = data.position.x - pos.x
local dz = data.position.z - pos.z
local bearing = math.deg(math.atan2(dz, dx))
if bearing < 0 then bearing = bearing + 360 end
local relativeBrg = bearing - heading
if relativeBrg < 0 then relativeBrg = relativeBrg + 360 end
if relativeBrg > 180 then relativeBrg = relativeBrg - 360 end
-- Calculate time remaining
local timeoutAt = data.spawnTime + (cfg.CrewTimeout or 3600)
local timeRemainSec = math.max(0, timeoutAt - timer.getTime())
local timeRemainMin = math.floor(timeRemainSec / 60)
-- Format distance
local distV, distU = _fmtRange(dist, isMetric)
-- Build message for this crew
table.insert(lines, string.format('#%d: %s crew', i, data.vehicleType))
table.insert(lines, string.format(' BRG %03d° (%+.0f° rel) | RNG %s %s',
math.floor(bearing + 0.5), relativeBrg, distV, distU))
table.insert(lines, string.format(' Time left: %d min | Salvage: %d pts',
timeRemainMin, data.salvageValue or 1))
if i < maxShow then
table.insert(lines, '')
end
end
_msgGroup(group, table.concat(lines, '\n'), 20)
end
-- List MASH locations
@ -6743,7 +7016,8 @@ function CTLD:_CreateMobileMASH(group, position, catalogDef)
}
if not CTLD._mashZones then CTLD._mashZones = {} end
table.insert(CTLD._mashZones, mashData)
-- Store mobile MASH with unique key (not array insert) to avoid duplicate iteration
CTLD._mashZones[mashId] = mashData
-- Draw on F10 map
local circleId = mashId .. '_circle'
@ -6820,29 +7094,26 @@ end
function CTLD:_RemoveMobileMASH(mashId)
if not CTLD._mashZones then return end
for i = #CTLD._mashZones, 1, -1 do
local mash = CTLD._mashZones[i]
if mash.id == mashId then
-- Stop scheduler
if mash.scheduler then
mash.scheduler:Stop()
end
-- Remove map drawings
if mash.circleId then trigger.action.removeMark(mash.circleId) end
if mash.textId then trigger.action.removeMark(mash.textId) end
-- Send destruction message
local msg = _fmtMsg(CTLD.Messages.medevac_mash_destroyed, {
mash_id = string.match(mashId, 'MOBILE_MASH_%d+_(%d+)') or '?'
})
trigger.action.outTextForCoalition(mash.side, msg, 20)
-- Remove from table
table.remove(CTLD._mashZones, i)
env.info(string.format('[Moose_CTLD][MobileMASH] Removed MASH %s', mashId))
break
local mash = CTLD._mashZones[mashId]
if mash then
-- Stop scheduler
if mash.scheduler then
mash.scheduler:Stop()
end
-- Remove map drawings
if mash.circleId then trigger.action.removeMark(mash.circleId) end
if mash.textId then trigger.action.removeMark(mash.textId) end
-- Send destruction message
local msg = _fmtMsg(CTLD.Messages.medevac_mash_destroyed, {
mash_id = string.match(mashId, 'MOBILE_MASH_%d+_(%d+)') or '?'
})
trigger.action.outTextForCoalition(mash.side, msg, 20)
-- Remove from table
CTLD._mashZones[mashId] = nil
env.info(string.format('[Moose_CTLD][MobileMASH] Removed MASH %s', mashId))
end
end

View File

@ -20,7 +20,7 @@
if _MOOSE_CTLD and _G.BASE then
local blueCfg = {
CoalitionSide = coalition.side.BLUE,
PickupZoneSmokeColor = trigger.smokeColor.Blue,
PickupZoneSmokeColor = trigger.smokeColor.Green,
AllowedAircraft = { -- transport-capable unit type names (case-sensitive as in DCS DB)
'UH-1H','Mi-8MTV2','Mi-24P','SA342M','SA342L','SA342Minigun','UH-60L','CH-47Fbl1','CH-47F','Mi-17','GazelleAI'
},
@ -32,7 +32,7 @@ local blueCfg = {
},
Zones = {
PickupZones = { { name = 'ALPHA', smoke = trigger.smokeColor.Blue, flag = 9001, activeWhen = 0 } },
PickupZones = { { name = 'ALPHA', flag = 9001, activeWhen = 0 } },
DropZones = { { name = 'BRAVO', flag = 9002, activeWhen = 0 } },
FOBZones = { { name = 'CHARLIE', flag = 9003, activeWhen = 0 } },
MASHZones = { { name = 'MASH Alpha', freq = '251.0 AM', radius = 500, flag = 9010, activeWhen = 0 } },
@ -47,7 +47,7 @@ ctldBlue = _MOOSE_CTLD:New(blueCfg)
local redCfg = {
CoalitionSide = coalition.side.RED,
PickupZoneSmokeColor = trigger.smokeColor.Red,
PickupZoneSmokeColor = trigger.smokeColor.Green,
AllowedAircraft = { -- transport-capable unit type names (case-sensitive as in DCS DB)
'UH-1H','Mi-8MTV2','Mi-24P','SA342M','SA342L','SA342Minigun','UH-60L','CH-47Fbl1','CH-47F','Mi-17','GazelleAI'
@ -60,7 +60,7 @@ local redCfg = {
},
Zones = {
PickupZones = { { name = 'DELTA', smoke = trigger.smokeColor.Red, flag = 9101, activeWhen = 0 } },
PickupZones = { { name = 'DELTA', flag = 9101, activeWhen = 0 } },
DropZones = { { name = 'ECHO', flag = 9102, activeWhen = 0 } },
FOBZones = { { name = 'FOXTROT', flag = 9103, activeWhen = 0 } },
MASHZones = { { name = 'MASH Bravo', freq = '252.0 AM', radius = 500, flag = 9111, activeWhen = 0 } },

Binary file not shown.

View File

@ -47,15 +47,26 @@ Below are the menu groups and the common actions youll see under each. Some o
- Deployment is blocked inside Pickup Zones when restrictions are enabled.
- Build
- Build Here: Consumes nearby crates (within the Build Radius) and spawns the unit/site at your position. Includes a “confirm within X seconds” safety and a cooldown between builds.
- Build Here: Consumes nearby crates (within the Build Radius) and spawns the unit/site at your position. Includes a "confirm within X seconds" safety and a cooldown between builds.
- Build (Advanced) → Buildable Near You
- Lists everything that can be built with crates youve dropped nearby (and optionally what youre carrying, depending on mission settings).
- Per item youll see:
- Lists everything that can be built with crates you've dropped nearby (and optionally what you're carrying, depending on mission settings).
- Per item you'll see:
- Build [Hold Position]: Spawns and orders the unit/site to hold.
- Build [Attack (N m)]: Spawns and orders mobile units to seek/attack within the configured radius. Static/unsuitable units will still hold.
- Refresh Buildable List: Re-scan nearby crates and update the list.
- FOB-only recipes can require building inside an FOB Zone when enabled (mission-specific rule).
- MEDEVAC (if enabled in mission)
- List Active MEDEVAC Requests: Shows all pending rescue missions with grid coordinates and time remaining
- Nearest MEDEVAC Location: Bearing and range to the closest MEDEVAC crew needing rescue
- Coalition Salvage Points: Display current salvage point balance for your coalition
- Vectors to Nearest MEDEVAC: Full details (bearing, range, time remaining) to nearest crew
- MASH Locations: Shows all active MASH (Mobile Army Surgical Hospital) zones where you can deliver crews
- Pop Smoke at Crew Locations: Marks all active crew locations with smoke for easier visual identification
- Pop Smoke at MASH Zones: Marks all MASH zones with smoke
- MASH & Salvage System - Guide: In-game quick reference for the MEDEVAC system (same as in Admin/Help -> Player Guides)
- Admin/Settings → Clear All MEDEVAC Missions: Debug/admin tool to reset all active MEDEVAC missions
### Logistics
[screenshot: Logistics -> Request Crate]
@ -136,10 +147,142 @@ Below are the menu groups and the common actions youll see under each. Some o
- Deploy armor and ATGM teams: Push objectives, ambush enemy convoys, or hold key terrain.
- Build EWR/JTAC: Improve situational awareness and targeting support.
- Establish FOBs: Create forward supply hubs to shorten flight times and increase the tempo of logistics.
- Rescue MEDEVAC crews: Save downed vehicle crews, earn salvage points, and keep friendly vehicles in the fight.
[screenshot: Example built SAM site]
Practical tip: Coordinate. One player can shuttle crates while others escort or build. FOBs multiply everyones effectiveness.
Practical tip: Coordinate. One player can shuttle crates while others escort or build. FOBs multiply everyone's effectiveness.
---
## MEDEVAC & Salvage System (Player Operations Guide)
The MEDEVAC (Medical Evacuation) and Salvage system adds a high-stakes rescue mission layer to logistics. When friendly ground vehicles are destroyed, their crews may survive and call for rescue. Successfully rescuing and delivering these crews to MASH zones earns your coalition Salvage Points—a critical resource that keeps logistics flowing even when supply zones run dry.
### What is MEDEVAC?
- **Vehicle destruction triggers rescue missions**: When a friendly ground vehicle (tank, APC, AA vehicle, etc.) is destroyed, the crew has a chance to survive and spawn near the wreck.
- **Time-limited rescue window**: Crews have a limited time (typically 60 minutes) to be rescued. If no one comes, they're KIA and the vehicle is permanently lost.
- **Coalition-wide benefit**: Any helicopter pilot can attempt the rescue. Successful delivery to MASH earns salvage points for the entire coalition.
### How the rescue workflow works
1. **Vehicle destroyed → Crew spawns** (after a delay, typically 5 minutes to let the battle clear)
- Crew spawns near the wreck with a small offset toward the nearest enemy
- Invulnerability period during announcement (crews can't be killed immediately)
- MEDEVAC request broadcast to coalition with grid coordinates and salvage value
- Map marker created (if enabled) showing location and time remaining
2. **Navigate to crew location**
- Use Operations → MEDEVAC → Vectors to Nearest MEDEVAC for bearing and range
- Or check Navigation → Vectors to Nearest MEDEVAC Crew
- Crews pop smoke when they detect approaching helicopters (typically within 8 km)
- Watch for humorous greeting messages when you get close!
3. **Load the crew**
- Hover nearby and load troops normally (Operations → Troop Transport → Load Troops)
- System automatically detects MEDEVAC crew and marks them as rescued
- **Original vehicle respawns at its death location** (if enabled), fully repaired and ready to fight
- You'll see a confirmation message with crew size and salvage value
4. **Deliver to MASH zone**
- Fly to any friendly MASH (Mobile Army Surgical Hospital) zone
- Use Operations → MEDEVAC → MASH Locations or Navigation → Vectors to Nearest MASH
- Deploy troops inside the MASH zone (Operations → Troop Transport → Deploy)
- **Salvage points automatically awarded** to your coalition
- Coalition-wide message announces the delivery, points earned, and new total
### Warning system
The mission keeps you informed of time-critical rescues:
- **15-minute warning**: "WARNING: [vehicle] crew at [grid] - rescue window expires in 15 minutes!"
- **5-minute warning**: "URGENT: [vehicle] crew at [grid] - rescue window expires in 5 minutes!"
- **Timeout**: If rescue window expires, crew is KIA and vehicle is permanently lost
### MASH Zones (Mobile Army Surgical Hospital)
**Fixed MASH zones** are pre-configured by the mission maker at friendly bases or FARPs. These are always active and visible on the map (use Admin/Help → Draw CTLD Zones to see them).
**Mobile MASH** can be built by players using MASH crates from the logistics catalog:
- Request and build Mobile MASH crates like any other unit
- Creates a new delivery zone with radio beacon
- Perfect for forward operations near active combat zones
- Multiple mobile MASHs can be deployed to reduce delivery times
- If destroyed, that MASH zone stops accepting deliveries
### Salvage Points: The economic engine
**Earning salvage**:
- Each vehicle type has a salvage value (typically 1 point per crew member)
- Deliver crews to MASH to earn points for your coalition
- Coalition-wide pool: everyone benefits from everyone's rescues
**Using salvage**:
- When you request crates and the supply zone is OUT OF STOCK, salvage automatically applies (if enabled)
- System consumes salvage points equal to the item's cost
- Lets you build critical items even when supply lines are exhausted
- Check current balance: Operations → MEDEVAC → Coalition Salvage Points
**Strategic value**:
- High-value vehicles (tanks, AA systems) typically award more salvage
- Prioritize rescues based on salvage value and proximity
- Mobile MASH deployment near combat zones multiplies salvage income
- Salvage can unlock mission-critical capabilities when inventory runs low
### Crew survival mechanics (mission-configurable)
- **Survival chance**: Configurable per coalition (default ~50%). Not every destroyed vehicle spawns a crew.
- **MANPADS chance**: Some crew members may spawn with anti-air weapons (default ~10%), providing limited self-defense
- **Crew size**: Varies by vehicle type (catalog-defined). Tanks typically have 3-4 crew, APCs 2-3.
- **Crew defense**: Crews will return fire if engaged during rescue (can be disabled)
- **Invulnerability**: Crews are typically immortal during the announcement delay and often remain protected until rescue to prevent instant death
### Best practices for MEDEVAC operations
1. **Monitor requests actively**: Use Operations → MEDEVAC → List Active MEDEVAC Requests to see all pending missions
2. **Prioritize by value and time**: High salvage + low time remaining = top priority
3. **Deploy Mobile MASH forward**: Reduce delivery time by placing MASH near active combat zones
4. **Coordinate with team**: Share MEDEVAC locations. One player can rescue while another delivers to MASH.
5. **Use smoke marking**: Operations → MEDEVAC → Pop Smoke at Crew Locations marks all crews with smoke
6. **Check salvage before major operations**: Know your coalition's salvage balance before pushing objectives
7. **Risk assessment**: Don't sacrifice your aircraft for low-value rescues in hot zones. Dead rescuer = no rescue.
### MEDEVAC menu quick reference (Operations → MEDEVAC)
- **List Active MEDEVAC Requests**: Overview of all pending rescues (grid, vehicle type, time left)
- **Nearest MEDEVAC Location**: Quick bearing/range to closest crew
- **Vectors to Nearest MEDEVAC**: Detailed navigation info with time remaining
- **Coalition Salvage Points**: Check current balance
- **MASH Locations**: Shows all active MASH zones (fixed and mobile)
- **Pop Smoke at Crew Locations**: Visual marking for all active crews
- **Pop Smoke at MASH Zones**: Visual marking for all delivery zones
- **MASH & Salvage System - Guide**: In-game reference (same content available in Admin/Help → Player Guides)
### MEDEVAC statistics (if enabled)
Some missions track detailed statistics available via Admin/Help → Show MEDEVAC Statistics:
- Crews spawned, rescued, delivered to MASH
- Timed out and killed in action
- Vehicles respawned
- Salvage earned, used, and current balance
[screenshot: MEDEVAC request message with grid coordinates]
[screenshot: Operations → MEDEVAC menu]
[screenshot: Mobile MASH deployed with beacon]
---
## How players influence the mission
- Build air defenses (SAM/AAA): Protect friendly FARPs/FOBs and deny enemy air.
- Deploy armor and ATGM teams: Push objectives, ambush enemy convoys, or hold key terrain.
- Build EWR/JTAC: Improve situational awareness and targeting support.
- Establish FOBs: Create forward supply hubs to shorten flight times and increase the tempo of logistics.
- Rescue MEDEVAC crews: Save downed vehicle crews, earn salvage points, and keep friendly vehicles in the fight.
[screenshot: Example built SAM site]
Practical tip: Coordinate. One player can shuttle crates while others escort or build. FOBs multiply everyone's effectiveness.
---
@ -175,10 +318,11 @@ Hint: See the shipped `init_mission_dual_coalition.lua` for a clean example of b
- Pickup (Supply): e.g., `ALPHA` (BLUE), `DELTA` (RED)
- Drop: e.g., `BRAVO` (BLUE), `ECHO` (RED)
- FOB: e.g., `CHARLIE` (BLUE), `FOXTROT` (RED)
- MASH (optional, for MEDEVAC): e.g., `MASH_BLUE_1`, `MASH_RED_1` (accepts crew deliveries for salvage points)
Use the names referenced by your init script. The example init uses flags to control active/inactive state.
[screenshot: Trigger zones for Pickup/Drop/FOB]
[screenshot: Trigger zones for Pickup/Drop/FOB/MASH]
### Frequently configured options (where to change)
@ -214,6 +358,20 @@ All of the following live under `CTLD.Config` in `Moose_CTLD.lua` or can be prov
- `AttackAI.VehicleSearchRadius`: How far spawned vehicles look for enemies when ordered to Attack
- `AttackAI.MoveSpeedKmh`: Movement speed for Attack orders
- MEDEVAC & Salvage system (optional feature)
- `MEDEVAC.Enabled`: Master switch for the rescue/salvage system
- `MEDEVAC.CrewSurvivalChance`: Per-coalition probability that destroyed vehicle crews survive (0.0-1.0)
- `MEDEVAC.ManPadSpawnChance`: Probability crews spawn with MANPADS for self-defense
- `MEDEVAC.CrewSpawnDelay`: Seconds after death before crew spawns (allows battle to clear)
- `MEDEVAC.CrewTimeout`: Max time for rescue before crew is KIA (default 3600 = 1 hour)
- `MEDEVAC.CrewImmortalDuringDelay`: Invulnerability during announcement delay
- `MEDEVAC.PopSmokeOnApproach`: Auto-smoke when helos get close (default true)
- `MEDEVAC.RespawnOnPickup`: Original vehicle respawns when crew is rescued (default true)
- `MEDEVAC.Salvage.Enabled`: Enable salvage points economy
- `MEDEVAC.Salvage.AutoApply`: Auto-use salvage for out-of-stock items (default true)
- `MEDEVAC.MapMarkers.Enabled`: Create F10 map markers for active MEDEVAC requests
- `Zones.MASHZones`: Configure fixed MASH delivery zones (Mobile MASH can be built by players)
- Menus
- `UseGroupMenus`: Per-group F10 menus (recommended)
- `UseCategorySubmenus`: Organize Request Crate/Recipe Info by category