mirror of
https://github.com/iTracerFacer/DCS_MissionDev.git
synced 2025-12-03 04:14:46 +00:00
Added AutoGroundLoading while in pickup/fob zone. Land near crates, and wait for process to complete. No menu needed.
This commit is contained in:
parent
4d74bb935e
commit
c6ebc0cc11
@ -11,6 +11,9 @@
|
|||||||
-- Inputs: Config table or defaults. No ME templates needed. Zones may be named ME trigger zones or provided via coordinates in config.
|
-- Inputs: Config table or defaults. No ME templates needed. Zones may be named ME trigger zones or provided via coordinates in config.
|
||||||
-- Outputs: F10 menus for helo/transport groups; crate spawning/building; troop load/unload; optional JTAC hookup (via FAC module);
|
-- Outputs: F10 menus for helo/transport groups; crate spawning/building; troop load/unload; optional JTAC hookup (via FAC module);
|
||||||
-- Error modes: missing Moose -> abort; unknown crate key -> message; spawn blocked in enemy airbase; zone missing -> message.
|
-- Error modes: missing Moose -> abort; unknown crate key -> message; spawn blocked in enemy airbase; zone missing -> message.
|
||||||
|
--
|
||||||
|
-- Orignal Author of CTLD: Ciribob
|
||||||
|
-- Moose adaptation: Lathe, Copilot, F99th-TracerFacer
|
||||||
|
|
||||||
-- #region Config
|
-- #region Config
|
||||||
|
|
||||||
@ -62,6 +65,14 @@ CTLD.Messages = {
|
|||||||
troop_unload_altitude_too_high = "Too high for fast-rope deployment. Maximum: {max_agl} m AGL (current: {current_agl} m). Land or descend.",
|
troop_unload_altitude_too_high = "Too high for fast-rope deployment. Maximum: {max_agl} m AGL (current: {current_agl} m). Land or descend.",
|
||||||
troop_unload_altitude_too_low = "Too low for safe fast-rope. Minimum: {min_agl} m AGL (current: {current_agl} m). Climb or land.",
|
troop_unload_altitude_too_low = "Too low for safe fast-rope. Minimum: {min_agl} m AGL (current: {current_agl} m). Climb or land.",
|
||||||
|
|
||||||
|
-- Ground auto-load
|
||||||
|
ground_load_started = "Ground loading: Hold position for {seconds}s to load {count} crate(s)...",
|
||||||
|
ground_load_progress = "Loading crates... {remaining}s remaining. Hold position.",
|
||||||
|
ground_load_complete = "Ground load complete! Loaded {count} crate(s).",
|
||||||
|
ground_load_aborted = "Ground load aborted: aircraft moved or lifted off.",
|
||||||
|
ground_load_no_zone = "Ground auto-load requires being inside a Pickup Zone. Nearest zone: {zone_dist} {zone_dist_u} at {zone_brg}°.",
|
||||||
|
ground_load_no_crates = "No crates within {radius}m to load.",
|
||||||
|
|
||||||
-- Coach & nav
|
-- Coach & nav
|
||||||
vectors_to_crate = "Nearest crate {id}: bearing {brg}°, range {rng} {rng_u}.",
|
vectors_to_crate = "Nearest crate {id}: bearing {brg}°, range {rng} {rng_u}.",
|
||||||
vectors_to_pickup_zone = "Nearest supply zone {zone}: bearing {brg}°, range {rng} {rng_u}.",
|
vectors_to_pickup_zone = "Nearest supply zone {zone}: bearing {brg}°, range {rng} {rng_u}.",
|
||||||
@ -159,6 +170,14 @@ CTLD.Config = {
|
|||||||
LogLevel = 1, -- lowered from DEBUG (4) to INFO (2) for production performance
|
LogLevel = 1, -- lowered from DEBUG (4) to INFO (2) for production performance
|
||||||
MessageDuration = 15, -- seconds for on-screen messages
|
MessageDuration = 15, -- seconds for on-screen messages
|
||||||
|
|
||||||
|
-- Debug toggles for detailed crate proximity logging (useful when tuning hover coach / ground autoload)
|
||||||
|
DebugHoverCrates = true,
|
||||||
|
DebugHoverCratesInterval = 1.0, -- seconds between hover debug log bursts (per aircraft)
|
||||||
|
DebugHoverCratesStep = 25, -- log again when nearest crate distance changes by this many meters
|
||||||
|
DebugGroundCrates = true,
|
||||||
|
DebugGroundCratesInterval = 2.0, -- seconds between ground debug log bursts (per aircraft)
|
||||||
|
DebugGroundCratesStep = 10, -- log again when nearest crate distance changes by this many meters
|
||||||
|
|
||||||
-- === Menu & Catalog ===
|
-- === Menu & Catalog ===
|
||||||
UseGroupMenus = true, -- if true, F10 menus per player group; otherwise coalition-wide (leave this alone)
|
UseGroupMenus = true, -- if true, F10 menus per player group; otherwise coalition-wide (leave this alone)
|
||||||
CreateMenuAtMissionStart = false, -- creates empty root menu at mission start to reserve F10 position (populated on player spawn)
|
CreateMenuAtMissionStart = false, -- creates empty root menu at mission start to reserve F10 position (populated on player spawn)
|
||||||
@ -471,8 +490,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 = 8, -- m: horizontal sweet spot radius
|
captureHoriz = 10, -- m: horizontal sweet spot radius
|
||||||
captureVert = 8, -- m: vertical sweet spot tolerance around AGL window
|
captureVert = 10, -- 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
|
||||||
@ -489,6 +508,21 @@ CTLD.HoverCoachConfig = {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
-- =========================
|
||||||
|
-- Ground Auto-Load Configuration
|
||||||
|
-- =========================
|
||||||
|
-- Automatic crate loading while landed (for pilots who prefer not to hover)
|
||||||
|
CTLD.GroundAutoLoadConfig = {
|
||||||
|
Enabled = true, -- master switch for ground auto-load feature
|
||||||
|
LoadDelay = 25, -- seconds to hold position on ground before auto-loading
|
||||||
|
GroundContactAGL = 3.5, -- meters AGL considered "on the ground" (matches MEDEVAC)
|
||||||
|
MaxGroundSpeed = 2.0, -- m/s maximum ground speed during loading (~4 knots)
|
||||||
|
SearchRadius = 20, -- meters to search for nearby crates
|
||||||
|
AbortGrace = 2, -- seconds of movement/liftoff tolerated before aborting
|
||||||
|
RequirePickupZone = true, -- MUST be inside a pickup zone to auto-load (prevents drop/re-pickup loops)
|
||||||
|
AllowInFOBZones = true, -- also allow auto-load in FOB zones (once built)
|
||||||
|
}
|
||||||
|
|
||||||
-- =========================
|
-- =========================
|
||||||
-- MEDEVAC Configuration
|
-- MEDEVAC Configuration
|
||||||
-- =========================
|
-- =========================
|
||||||
@ -1823,6 +1857,72 @@ local function _logInfo(msg) _log(LOG_INFO, msg) end
|
|||||||
local function _logVerbose(msg) _log(LOG_DEBUG, msg) end
|
local function _logVerbose(msg) _log(LOG_DEBUG, msg) end
|
||||||
local function _logDebug(msg) _log(LOG_DEBUG, msg) end
|
local function _logDebug(msg) _log(LOG_DEBUG, msg) end
|
||||||
|
|
||||||
|
-- Emits tagged messages regardless of configured LogLevel (used by explicit debug toggles)
|
||||||
|
local function _logImmediate(tag, msg)
|
||||||
|
local text = string.format('[Moose_CTLD][%s] %s', tag or 'DEBUG', tostring(msg))
|
||||||
|
if env and env.info then
|
||||||
|
env.info(text)
|
||||||
|
else
|
||||||
|
print(text)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function _debugCrateSight(kind, params)
|
||||||
|
if not params or not params.unit then return end
|
||||||
|
CTLD._debugSightState = CTLD._debugSightState or {}
|
||||||
|
local key = string.format('%s:%s', kind, params.unit)
|
||||||
|
local state = CTLD._debugSightState[key] or {}
|
||||||
|
local now = params.now or timer.getTime()
|
||||||
|
local interval = params.interval or 1.0
|
||||||
|
local step = params.step or 10.0
|
||||||
|
local name = params.name or 'none'
|
||||||
|
local distance = params.distance or math.huge
|
||||||
|
local crateCount = params.count or 0
|
||||||
|
local troopCount = params.troops or 0
|
||||||
|
local shouldLog = false
|
||||||
|
|
||||||
|
if not state.lastTime or interval <= 0 or (now - state.lastTime) >= interval then
|
||||||
|
shouldLog = true
|
||||||
|
end
|
||||||
|
if state.lastName ~= name then
|
||||||
|
shouldLog = true
|
||||||
|
end
|
||||||
|
if state.lastCount ~= crateCount or state.lastTroops ~= troopCount then
|
||||||
|
shouldLog = true
|
||||||
|
end
|
||||||
|
if distance ~= math.huge then
|
||||||
|
if not state.lastDist or math.abs(distance - state.lastDist) >= step then
|
||||||
|
shouldLog = true
|
||||||
|
end
|
||||||
|
elseif state.lastDist ~= math.huge then
|
||||||
|
shouldLog = true
|
||||||
|
end
|
||||||
|
|
||||||
|
if not shouldLog then return end
|
||||||
|
|
||||||
|
local distText = (distance ~= math.huge) and string.format('d=%.1f', distance) or 'd=n/a'
|
||||||
|
local summaryParts = { string.format('%d crate(s)', crateCount) }
|
||||||
|
if troopCount and troopCount > 0 then
|
||||||
|
table.insert(summaryParts, string.format('%d troop group(s)', troopCount))
|
||||||
|
end
|
||||||
|
local summary = table.concat(summaryParts, ', ')
|
||||||
|
local noteParts = {}
|
||||||
|
if params.radius then table.insert(noteParts, string.format('radius=%dm', math.floor(params.radius))) end
|
||||||
|
if params.note then table.insert(noteParts, params.note) end
|
||||||
|
local noteText = (#noteParts > 0) and (' [' .. table.concat(noteParts, ' ') .. ']') or ''
|
||||||
|
local targetLabel = params.targetLabel or 'nearest'
|
||||||
|
local typeHint = params.typeHint and (' type=' .. params.typeHint) or ''
|
||||||
|
_logImmediate(kind, string.format('Unit %s tracking %s; %s=%s %s%s%s',
|
||||||
|
params.unit, summary, targetLabel, name, distText, typeHint, noteText))
|
||||||
|
|
||||||
|
state.lastTime = now
|
||||||
|
state.lastName = name
|
||||||
|
state.lastDist = distance
|
||||||
|
state.lastCount = crateCount
|
||||||
|
state.lastTroops = troopCount
|
||||||
|
CTLD._debugSightState[key] = state
|
||||||
|
end
|
||||||
|
|
||||||
function CTLD:_collectEntryUnitTypes(entry)
|
function CTLD:_collectEntryUnitTypes(entry)
|
||||||
local collected = {}
|
local collected = {}
|
||||||
local seen = {}
|
local seen = {}
|
||||||
@ -2700,6 +2800,16 @@ function CTLD:_startHoverScheduler()
|
|||||||
end, {}, startDelay, interval)
|
end, {}, startDelay, interval)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
function CTLD:_startGroundLoadScheduler()
|
||||||
|
local groundCfg = CTLD.GroundAutoLoadConfig or {}
|
||||||
|
if not groundCfg.Enabled or self.GroundLoadSched then return end
|
||||||
|
local interval = 1.0 -- check every second for ground load conditions
|
||||||
|
self.GroundLoadSched = SCHEDULER:New(nil, function()
|
||||||
|
local ok, err = pcall(function() self:ScanGroundAutoLoad() end)
|
||||||
|
if not ok then _logError('GroundLoadSched ScanGroundAutoLoad error: '..tostring(err)) end
|
||||||
|
end, {}, interval, interval)
|
||||||
|
end
|
||||||
|
|
||||||
-- Adaptive background loop consolidating salvage checks and periodic pruning
|
-- Adaptive background loop consolidating salvage checks and periodic pruning
|
||||||
function CTLD:_ensureAdaptiveBackgroundLoop()
|
function CTLD:_ensureAdaptiveBackgroundLoop()
|
||||||
if self._schedules and self._schedules.backgroundLoop then return end
|
if self._schedules and self._schedules.backgroundLoop then return end
|
||||||
@ -3724,6 +3834,10 @@ function CTLD:New(cfg)
|
|||||||
o._DynamicSalvageQueue = {}
|
o._DynamicSalvageQueue = {}
|
||||||
o._jtacRegistry = {}
|
o._jtacRegistry = {}
|
||||||
|
|
||||||
|
-- Ground auto-load state tracking
|
||||||
|
CTLD._groundLoadState = CTLD._groundLoadState or {}
|
||||||
|
CTLD._groundLoadTimers = CTLD._groundLoadTimers or {}
|
||||||
|
|
||||||
-- If caller disabled builtin catalog, clear it before merging any globals
|
-- If caller disabled builtin catalog, clear it before merging any globals
|
||||||
if o.Config.UseBuiltinCatalog == false then
|
if o.Config.UseBuiltinCatalog == false then
|
||||||
o.Config.CrateCatalog = {}
|
o.Config.CrateCatalog = {}
|
||||||
@ -3874,6 +3988,13 @@ function CTLD:New(cfg)
|
|||||||
o:_startHoverScheduler()
|
o:_startHoverScheduler()
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- Optional: ground auto-load scanner
|
||||||
|
local groundCfg = CTLD.GroundAutoLoadConfig or {}
|
||||||
|
if groundCfg.Enabled then
|
||||||
|
o.GroundLoadSched = nil
|
||||||
|
o:_startGroundLoadScheduler()
|
||||||
|
end
|
||||||
|
|
||||||
-- MEDEVAC auto-pickup and auto-unload scheduler
|
-- MEDEVAC auto-pickup and auto-unload scheduler
|
||||||
if CTLD.MEDEVAC and CTLD.MEDEVAC.Enabled then
|
if CTLD.MEDEVAC and CTLD.MEDEVAC.Enabled then
|
||||||
local checkInterval = (CTLD.MEDEVAC.AutoPickup and CTLD.MEDEVAC.AutoPickup.CheckInterval) or 3
|
local checkInterval = (CTLD.MEDEVAC.AutoPickup and CTLD.MEDEVAC.AutoPickup.CheckInterval) or 3
|
||||||
@ -7759,30 +7880,46 @@ function CTLD:ScanHoverPickup()
|
|||||||
local p3 = unit:GetPointVec3()
|
local p3 = unit:GetPointVec3()
|
||||||
local ground = land and land.getHeight and land.getHeight({ x = p3.x, y = p3.z }) or 0
|
local ground = land and land.getHeight and land.getHeight({ x = p3.x, y = p3.z }) or 0
|
||||||
local agl = math.max(0, p3.y - ground)
|
local agl = math.max(0, p3.y - ground)
|
||||||
-- speeds (ground/vertical)
|
|
||||||
local last = CTLD._unitLast[uname]
|
-- Skip hover coaching if on the ground (let ground auto-load handle it)
|
||||||
local gs, vs = 0, 0
|
local groundCfg = CTLD.GroundAutoLoadConfig or {}
|
||||||
if last and (now > (last.t or 0)) then
|
local groundContactAGL = groundCfg.GroundContactAGL or 3.5
|
||||||
local dt = now - last.t
|
if agl <= groundContactAGL then
|
||||||
if dt > 0 then
|
-- On ground, clear hover state and skip hover logic
|
||||||
local dx = (p3.x - last.x)
|
CTLD._hoverState[uname] = nil
|
||||||
local dz = (p3.z - last.z)
|
-- Also, when firmly landed, suppress any completed ground-load state
|
||||||
gs = math.sqrt(dx*dx + dz*dz) / dt
|
-- so returning to this spot without crates won't keep retriggering.
|
||||||
if last.agl then vs = (agl - last.agl) / dt end
|
if CTLD._groundLoadState and CTLD._groundLoadState[uname] and CTLD._groundLoadState[uname].completed then
|
||||||
|
CTLD._groundLoadState[uname] = nil
|
||||||
end
|
end
|
||||||
end
|
else
|
||||||
CTLD._unitLast[uname] = { x = p3.x, z = p3.z, t = now, agl = agl }
|
-- speeds (ground/vertical)
|
||||||
|
local last = CTLD._unitLast[uname]
|
||||||
|
local gs, vs = 0, 0
|
||||||
|
if last and (now > (last.t or 0)) then
|
||||||
|
local dt = now - last.t
|
||||||
|
if dt > 0 then
|
||||||
|
local dx = (p3.x - last.x)
|
||||||
|
local dz = (p3.z - last.z)
|
||||||
|
gs = math.sqrt(dx*dx + dz*dz) / dt
|
||||||
|
if last.agl then vs = (agl - last.agl) / dt end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
CTLD._unitLast[uname] = { x = p3.x, z = p3.z, t = now, agl = agl }
|
||||||
|
|
||||||
-- Use spatial indexing to find nearby crates/troops efficiently
|
-- Use spatial indexing to find nearby crates/troops efficiently
|
||||||
local maxd = coachCfg.autoPickupDistance or 25
|
local maxd = coachCfg.autoPickupDistance or 25
|
||||||
local nearby = _getNearbyFromSpatialGrid(p3.x, p3.z, maxd)
|
local nearby = _getNearbyFromSpatialGrid(p3.x, p3.z, maxd)
|
||||||
|
|
||||||
local bestName, bestMeta, bestd
|
local friendlyCrateCount = 0
|
||||||
local bestType = 'crate'
|
local friendlyTroopCount = 0
|
||||||
|
local bestName, bestMeta, bestd
|
||||||
|
local bestType = 'crate'
|
||||||
|
|
||||||
-- Search nearby crates from spatial grid
|
-- Search nearby crates from spatial grid
|
||||||
for name, meta in pairs(nearby.crates) do
|
for name, meta in pairs(nearby.crates or {}) do
|
||||||
if meta.side == self.Side then
|
if meta.side == self.Side then
|
||||||
|
friendlyCrateCount = friendlyCrateCount + 1
|
||||||
local dx = (meta.point.x - p3.x)
|
local dx = (meta.point.x - p3.x)
|
||||||
local dz = (meta.point.z - p3.z)
|
local dz = (meta.point.z - p3.z)
|
||||||
local d = math.sqrt(dx*dx + dz*dz)
|
local d = math.sqrt(dx*dx + dz*dz)
|
||||||
@ -7794,8 +7931,9 @@ function CTLD:ScanHoverPickup()
|
|||||||
end
|
end
|
||||||
|
|
||||||
-- Search nearby deployed troops from spatial grid
|
-- Search nearby deployed troops from spatial grid
|
||||||
for troopGroupName, troopMeta in pairs(nearby.troops) do
|
for troopGroupName, troopMeta in pairs(nearby.troops or {}) do
|
||||||
if troopMeta.side == self.Side then
|
if troopMeta.side == self.Side then
|
||||||
|
friendlyTroopCount = friendlyTroopCount + 1
|
||||||
local troopGroup = GROUP:FindByName(troopGroupName)
|
local troopGroup = GROUP:FindByName(troopGroupName)
|
||||||
if troopGroup and troopGroup:IsAlive() then
|
if troopGroup and troopGroup:IsAlive() then
|
||||||
local troopPos = troopGroup:GetCoordinate()
|
local troopPos = troopGroup:GetCoordinate()
|
||||||
@ -7817,13 +7955,37 @@ function CTLD:ScanHoverPickup()
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
local coachEnabled = coachCfg.enabled
|
if CTLD.Config and CTLD.Config.DebugHoverCrates and (friendlyCrateCount > 0 or friendlyTroopCount > 0) then
|
||||||
if CTLD._coachOverride and CTLD._coachOverride[gname] ~= nil then
|
local debugLabel = bestName or 'none'
|
||||||
coachEnabled = CTLD._coachOverride[gname]
|
local debugType = bestType
|
||||||
end
|
local debugDist = bestd
|
||||||
|
if not bestName and friendlyCrateCount > 0 then
|
||||||
|
debugLabel = 'pending'
|
||||||
|
debugType = 'crate'
|
||||||
|
debugDist = math.huge
|
||||||
|
end
|
||||||
|
_debugCrateSight('HoverDebug', {
|
||||||
|
unit = uname,
|
||||||
|
now = now,
|
||||||
|
interval = CTLD.Config.DebugHoverCratesInterval,
|
||||||
|
step = CTLD.Config.DebugHoverCratesStep,
|
||||||
|
name = debugLabel,
|
||||||
|
distance = debugDist,
|
||||||
|
count = friendlyCrateCount,
|
||||||
|
troops = friendlyTroopCount,
|
||||||
|
radius = maxd,
|
||||||
|
typeHint = debugType,
|
||||||
|
note = string.format('coachGS<=%.1f', coachCfg.thresholds.captureGS or (4/3.6)),
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
local coachEnabled = coachCfg.enabled
|
||||||
|
if CTLD._coachOverride and CTLD._coachOverride[gname] ~= nil then
|
||||||
|
coachEnabled = CTLD._coachOverride[gname]
|
||||||
|
end
|
||||||
|
|
||||||
-- If coach is on, provide phased guidance
|
-- If coach is on, provide phased guidance
|
||||||
if coachEnabled and bestName and bestMeta then
|
if coachEnabled and bestName and bestMeta then
|
||||||
local thresholds = coachCfg.thresholds or {}
|
local thresholds = coachCfg.thresholds or {}
|
||||||
local isMetric = _getPlayerIsMetric(unit)
|
local isMetric = _getPlayerIsMetric(unit)
|
||||||
|
|
||||||
@ -7891,19 +8053,19 @@ function CTLD:ScanHoverPickup()
|
|||||||
_coachSend(self, group, uname, 'coach_too_low', { agl = v, agl_u = u }, false)
|
_coachSend(self, group, uname, 'coach_too_low', { agl = v, agl_u = u }, false)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Auto-load logic using capture thresholds
|
-- Auto-load logic using capture thresholds
|
||||||
local capGS = coachCfg.thresholds.captureGS or (4/3.6)
|
local capGS = coachCfg.thresholds.captureGS or (4/3.6)
|
||||||
local aglMin = coachCfg.thresholds.aglMin or 5
|
local aglMin = coachCfg.thresholds.aglMin or 5
|
||||||
local aglMax = coachCfg.thresholds.aglMax or 20
|
local aglMax = coachCfg.thresholds.aglMax or 20
|
||||||
local speedOK = gs <= capGS
|
local speedOK = gs <= capGS
|
||||||
local heightOK = (agl >= aglMin and agl <= aglMax)
|
local heightOK = (agl >= aglMin and agl <= aglMax)
|
||||||
|
|
||||||
if bestName and bestMeta and speedOK and heightOK then
|
if bestName and bestMeta and speedOK and heightOK then
|
||||||
local withinRadius = bestd <= (coachCfg.thresholds.captureHoriz or 2)
|
local withinRadius = bestd <= (coachCfg.thresholds.captureHoriz or 2)
|
||||||
|
|
||||||
if withinRadius then
|
if withinRadius then
|
||||||
local carried = CTLD._loadedCrates[gname]
|
local carried = CTLD._loadedCrates[gname]
|
||||||
local total = carried and carried.total or 0
|
local total = carried and carried.total or 0
|
||||||
local currentWeight = carried and carried.totalWeightKg or 0
|
local currentWeight = carried and carried.totalWeightKg or 0
|
||||||
@ -7956,7 +8118,7 @@ function CTLD:ScanHoverPickup()
|
|||||||
end
|
end
|
||||||
|
|
||||||
-- Check both count AND weight limits
|
-- Check both count AND weight limits
|
||||||
if countOK and weightOK then
|
if countOK and weightOK then
|
||||||
local hs = CTLD._hoverState[uname]
|
local hs = CTLD._hoverState[uname]
|
||||||
if not hs or hs.targetCrate ~= bestName or hs.targetType ~= bestType then
|
if not hs or hs.targetCrate ~= bestName or hs.targetType ~= bestType then
|
||||||
CTLD._hoverState[uname] = { targetCrate = bestName, targetType = bestType, startTime = now }
|
CTLD._hoverState[uname] = { targetCrate = bestName, targetType = bestType, startTime = now }
|
||||||
@ -7967,16 +8129,14 @@ function CTLD:ScanHoverPickup()
|
|||||||
if (now - hs.startTime) >= holdNeeded then
|
if (now - hs.startTime) >= holdNeeded then
|
||||||
-- load it
|
-- load it
|
||||||
if bestType == 'crate' then
|
if bestType == 'crate' then
|
||||||
local obj = StaticObject.getByName(bestName)
|
-- Use shared loading function
|
||||||
if obj then obj:destroy() end
|
local success = self:_loadCrateIntoAircraft(group, bestName, bestMeta)
|
||||||
_cleanupCrateSmoke(bestName) -- Clean up smoke refresh schedule
|
if success then
|
||||||
_removeFromSpatialGrid(bestName, bestMeta.point, 'crate') -- Remove from spatial index
|
if coachEnabled then
|
||||||
CTLD._crates[bestName] = nil
|
_coachSend(self, group, uname, 'coach_loaded', {}, false)
|
||||||
self:_addLoadedCrate(group, bestMeta.key)
|
else
|
||||||
if coachEnabled then
|
_msgGroup(group, string.format('Loaded %s crate', tostring(bestMeta.key)))
|
||||||
_coachSend(self, group, uname, 'coach_loaded', {}, false)
|
end
|
||||||
else
|
|
||||||
_msgGroup(group, string.format('Loaded %s crate', tostring(bestMeta.key)))
|
|
||||||
end
|
end
|
||||||
elseif bestType == 'troops' then
|
elseif bestType == 'troops' then
|
||||||
-- Pick up the troop group
|
-- Pick up the troop group
|
||||||
@ -8024,7 +8184,7 @@ function CTLD:ScanHoverPickup()
|
|||||||
CTLD._hoverState[uname] = nil
|
CTLD._hoverState[uname] = nil
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
-- Aircraft at capacity - notify player with weight/count info
|
-- Aircraft at capacity - notify player with weight/count info
|
||||||
local aircraftType = _getUnitType(unit) or 'aircraft'
|
local aircraftType = _getUnitType(unit) or 'aircraft'
|
||||||
if not weightOK then
|
if not weightOK then
|
||||||
@ -8039,18 +8199,19 @@ function CTLD:ScanHoverPickup()
|
|||||||
_eventSend(self, group, nil, 'troop_aircraft_capacity', { count = bestMeta.count or 0, max = maxTroops, aircraft = aircraftType })
|
_eventSend(self, group, nil, 'troop_aircraft_capacity', { count = bestMeta.count or 0, max = maxTroops, aircraft = aircraftType })
|
||||||
end
|
end
|
||||||
CTLD._hoverState[uname] = nil
|
CTLD._hoverState[uname] = nil
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
-- lost precision window
|
-- lost precision window
|
||||||
if coachEnabled then _coachSend(self, group, uname, 'coach_hover_lost', {}, false) end
|
if coachEnabled then _coachSend(self, group, uname, 'coach_hover_lost', {}, false) end
|
||||||
CTLD._hoverState[uname] = nil
|
CTLD._hoverState[uname] = nil
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
-- reset hover state when outside primary envelope
|
-- reset hover state when outside primary envelope
|
||||||
if CTLD._hoverState[uname] then
|
if CTLD._hoverState[uname] then
|
||||||
if coachEnabled then _coachSend(self, group, uname, 'coach_abort', {}, false) end
|
if coachEnabled then _coachSend(self, group, uname, 'coach_abort', {}, false) end
|
||||||
end
|
end
|
||||||
CTLD._hoverState[uname] = nil
|
CTLD._hoverState[uname] = nil
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@ -8059,6 +8220,316 @@ function CTLD:ScanHoverPickup()
|
|||||||
end
|
end
|
||||||
-- #endregion Hover pickup scanner
|
-- #endregion Hover pickup scanner
|
||||||
|
|
||||||
|
-- =========================
|
||||||
|
-- Ground Auto-Load Scanner
|
||||||
|
-- =========================
|
||||||
|
-- #region Ground auto-load scanner
|
||||||
|
function CTLD:ScanGroundAutoLoad()
|
||||||
|
local groundCfg = CTLD.GroundAutoLoadConfig or { Enabled = false }
|
||||||
|
if not groundCfg.Enabled then return end
|
||||||
|
|
||||||
|
local now = timer.getTime()
|
||||||
|
|
||||||
|
-- Iterate all groups that have menus (active transports)
|
||||||
|
for gname, _ in pairs(self.MenusByGroup or {}) do
|
||||||
|
local group = GROUP:FindByName(gname)
|
||||||
|
if group and group:IsAlive() then
|
||||||
|
local unit = group:GetUnit(1)
|
||||||
|
if unit and unit:IsAlive() then
|
||||||
|
local typ = _getUnitType(unit)
|
||||||
|
if _isIn(self.Config.AllowedAircraft, typ) then
|
||||||
|
local uname = unit:GetName()
|
||||||
|
|
||||||
|
-- Ensure ground-load state table exists
|
||||||
|
CTLD._groundLoadState = CTLD._groundLoadState or {}
|
||||||
|
|
||||||
|
-- Check if already carrying crates (skip if at capacity)
|
||||||
|
local carried = CTLD._loadedCrates[gname]
|
||||||
|
local currentCount = carried and carried.total or 0
|
||||||
|
local capacity = _getAircraftCapacity(unit)
|
||||||
|
|
||||||
|
if currentCount < capacity.maxCrates then
|
||||||
|
-- Check basic requirements: on ground, low speed
|
||||||
|
local agl = _getUnitAGL(unit)
|
||||||
|
local gs = _getGroundSpeed(unit)
|
||||||
|
local onGround = (agl <= (groundCfg.GroundContactAGL or 3.5))
|
||||||
|
local slowEnough = (gs <= (groundCfg.MaxGroundSpeed or 2.0))
|
||||||
|
|
||||||
|
-- If a previous ground auto-load completed while we remain effectively stationary
|
||||||
|
-- on the ground, avoid auto-starting again until the aircraft has moved or lifted off.
|
||||||
|
local state = CTLD._groundLoadState[uname]
|
||||||
|
local canProcess = true
|
||||||
|
if state and state.completed and onGround and slowEnough then
|
||||||
|
local p3 = unit:GetPointVec3()
|
||||||
|
local sx = state.completedPosition and state.completedPosition.x or state.startPosition and state.startPosition.x or p3.x
|
||||||
|
local sz = state.completedPosition and state.completedPosition.z or state.startPosition and state.startPosition.z or p3.z
|
||||||
|
local dx = (p3.x - sx)
|
||||||
|
local dz = (p3.z - sz)
|
||||||
|
local moved = math.sqrt(dx*dx + dz*dz)
|
||||||
|
-- Require a small reposition (e.g., taxi or liftoff) before new auto-load cycles
|
||||||
|
if moved < 20 then
|
||||||
|
canProcess = false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if onGround and slowEnough and canProcess then
|
||||||
|
-- Check zone requirement
|
||||||
|
local inValidZone = false
|
||||||
|
if groundCfg.RequirePickupZone then
|
||||||
|
local inPickupZone = self:_isUnitInsidePickupZone(unit, true)
|
||||||
|
|
||||||
|
if not inPickupZone and groundCfg.AllowInFOBZones then
|
||||||
|
-- Check FOB zones too
|
||||||
|
for _, fobZone in ipairs(self.FOBZones or {}) do
|
||||||
|
local fname = fobZone:GetName()
|
||||||
|
if self._ZoneActive.FOB[fname] ~= false then
|
||||||
|
local fobPoint = fobZone:GetPointVec3()
|
||||||
|
local unitPoint = unit:GetPointVec3()
|
||||||
|
local dx = (fobPoint.x - unitPoint.x)
|
||||||
|
local dz = (fobPoint.z - unitPoint.z)
|
||||||
|
local d = math.sqrt(dx*dx + dz*dz)
|
||||||
|
local fobRadius = self:_getZoneRadius(fobZone)
|
||||||
|
if d <= fobRadius then
|
||||||
|
inValidZone = true
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
else
|
||||||
|
inValidZone = inPickupZone
|
||||||
|
end
|
||||||
|
else
|
||||||
|
inValidZone = true -- no zone requirement
|
||||||
|
end
|
||||||
|
|
||||||
|
if inValidZone then
|
||||||
|
-- Find nearby crates
|
||||||
|
local p3 = unit:GetPointVec3()
|
||||||
|
local searchRadius = groundCfg.SearchRadius or 50
|
||||||
|
local nearby = _getNearbyFromSpatialGrid(p3.x, p3.z, searchRadius)
|
||||||
|
|
||||||
|
local bestGroundCrate, bestGroundDist = nil, math.huge
|
||||||
|
local loadableCrates = {}
|
||||||
|
for name, meta in pairs(nearby.crates or {}) do
|
||||||
|
if meta.side == self.Side then
|
||||||
|
local dx = (meta.point.x - p3.x)
|
||||||
|
local dz = (meta.point.z - p3.z)
|
||||||
|
local d = math.sqrt(dx*dx + dz*dz)
|
||||||
|
if d <= searchRadius then
|
||||||
|
local entry = { name = name, meta = meta, dist = d }
|
||||||
|
table.insert(loadableCrates, entry)
|
||||||
|
if d < bestGroundDist then
|
||||||
|
bestGroundCrate, bestGroundDist = entry, d
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if CTLD.Config and CTLD.Config.DebugGroundCrates then
|
||||||
|
local shouldReport = true
|
||||||
|
local gst = CTLD._groundLoadState and CTLD._groundLoadState[uname]
|
||||||
|
if gst and gst.reportHold then
|
||||||
|
-- only emit periodic heartbeat during hold
|
||||||
|
local last = gst.reportLast or 0
|
||||||
|
if (now - last) < (CTLD.Config.DebugGroundCratesInterval or 2.0) then
|
||||||
|
shouldReport = false
|
||||||
|
else
|
||||||
|
CTLD._groundLoadState[uname].reportLast = now
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if shouldReport then
|
||||||
|
_debugCrateSight('GroundDebug', {
|
||||||
|
unit = uname,
|
||||||
|
now = now,
|
||||||
|
interval = CTLD.Config.DebugGroundCratesInterval,
|
||||||
|
step = CTLD.Config.DebugGroundCratesStep,
|
||||||
|
name = bestGroundCrate and bestGroundCrate.name or 'none',
|
||||||
|
distance = (bestGroundDist ~= math.huge) and bestGroundDist or nil,
|
||||||
|
count = #loadableCrates,
|
||||||
|
troops = 0,
|
||||||
|
radius = searchRadius,
|
||||||
|
typeHint = 'crate',
|
||||||
|
note = string.format('freeSlots=%d', math.max(0, capacity.maxCrates - currentCount))
|
||||||
|
})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if #loadableCrates > 0 then
|
||||||
|
-- Sort by distance, load closest first (up to capacity)
|
||||||
|
table.sort(loadableCrates, function(a, b) return a.dist < b.dist end)
|
||||||
|
|
||||||
|
-- Determine how many we can load
|
||||||
|
local canLoad = math.min(#loadableCrates, capacity.maxCrates - currentCount)
|
||||||
|
local cratesToLoad = {}
|
||||||
|
for i = 1, canLoad do
|
||||||
|
table.insert(cratesToLoad, loadableCrates[i])
|
||||||
|
end
|
||||||
|
|
||||||
|
if #cratesToLoad > 0 then
|
||||||
|
-- Ground load state machine
|
||||||
|
local state = CTLD._groundLoadState[uname]
|
||||||
|
if not state or not state.loading then
|
||||||
|
-- Start new load sequence
|
||||||
|
CTLD._groundLoadState[uname] = {
|
||||||
|
loading = true,
|
||||||
|
startTime = now,
|
||||||
|
cratesToLoad = cratesToLoad,
|
||||||
|
startPosition = { x = p3.x, z = p3.z },
|
||||||
|
lastCheckTime = now,
|
||||||
|
}
|
||||||
|
_coachSend(self, group, uname, 'ground_load_started', {
|
||||||
|
seconds = groundCfg.LoadDelay,
|
||||||
|
count = #cratesToLoad
|
||||||
|
}, false)
|
||||||
|
else
|
||||||
|
-- Validate that crates in state still exist
|
||||||
|
local validCrates = {}
|
||||||
|
for _, crateInfo in ipairs(state.cratesToLoad or {}) do
|
||||||
|
-- Check if crate still exists in CTLD._crates
|
||||||
|
if CTLD._crates[crateInfo.name] then
|
||||||
|
table.insert(validCrates, crateInfo)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- If no valid crates remain, reset state
|
||||||
|
if #validCrates == 0 then
|
||||||
|
CTLD._groundLoadState[uname] = nil
|
||||||
|
else
|
||||||
|
-- Update state with only valid crates
|
||||||
|
state.cratesToLoad = validCrates
|
||||||
|
|
||||||
|
-- Continue existing sequence
|
||||||
|
local elapsed = now - state.startTime
|
||||||
|
local remaining = (groundCfg.LoadDelay or 15) - elapsed
|
||||||
|
|
||||||
|
-- Check if moved too much
|
||||||
|
local dx = (p3.x - state.startPosition.x)
|
||||||
|
local dz = (p3.z - state.startPosition.z)
|
||||||
|
local moved = math.sqrt(dx*dx + dz*dz)
|
||||||
|
if moved > 10 then -- moved more than 10m
|
||||||
|
_coachSend(self, group, uname, 'ground_load_aborted', {}, false)
|
||||||
|
CTLD._groundLoadState[uname] = nil
|
||||||
|
else
|
||||||
|
-- Progress message every 5 seconds
|
||||||
|
if (now - state.lastCheckTime) >= 5 and remaining > 1 then
|
||||||
|
_coachSend(self, group, uname, 'ground_load_progress', {
|
||||||
|
remaining = math.ceil(remaining)
|
||||||
|
}, false)
|
||||||
|
state.lastCheckTime = now
|
||||||
|
state.reportHold = true
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Check if time elapsed
|
||||||
|
if remaining <= 0 then
|
||||||
|
-- Load the crates!
|
||||||
|
local loadedCount = 0
|
||||||
|
for _, crateInfo in ipairs(state.cratesToLoad) do
|
||||||
|
local success = self:_loadCrateIntoAircraft(group, crateInfo.name, crateInfo.meta)
|
||||||
|
if success then
|
||||||
|
loadedCount = loadedCount + 1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if loadedCount > 0 then
|
||||||
|
_coachSend(self, group, uname, 'ground_load_complete', {
|
||||||
|
count = loadedCount
|
||||||
|
}, false)
|
||||||
|
-- Mark completion and remember where it happened so we don't
|
||||||
|
-- immediately restart another cycle while still parked.
|
||||||
|
CTLD._groundLoadState[uname] = {
|
||||||
|
completed = true,
|
||||||
|
completedTime = now,
|
||||||
|
completedPosition = { x = p3.x, z = p3.z },
|
||||||
|
reportHold = false,
|
||||||
|
}
|
||||||
|
else
|
||||||
|
-- Nothing actually loaded; clear state fully.
|
||||||
|
CTLD._groundLoadState[uname] = nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end -- end of validCrates > 0
|
||||||
|
end -- end of state exists check
|
||||||
|
else
|
||||||
|
CTLD._groundLoadState[uname] = nil
|
||||||
|
end
|
||||||
|
else
|
||||||
|
CTLD._groundLoadState[uname] = nil
|
||||||
|
end
|
||||||
|
else
|
||||||
|
-- Not in a valid zone
|
||||||
|
local state = CTLD._groundLoadState[uname]
|
||||||
|
if state and state.sentZoneWarning ~= true then
|
||||||
|
-- Send zone requirement message (once)
|
||||||
|
local nearestZone, nearestDist = self:_nearestActivePickupZone(unit)
|
||||||
|
if nearestZone and nearestDist then
|
||||||
|
local isMetric = _getPlayerIsMetric(unit)
|
||||||
|
local brg = _bearingTo(unit, nearestZone)
|
||||||
|
local distV, distU = _fmtDistance(nearestDist, isMetric)
|
||||||
|
_coachSend(self, group, uname, 'ground_load_no_zone', {
|
||||||
|
zone_dist = distV,
|
||||||
|
zone_dist_u = distU,
|
||||||
|
zone_brg = brg
|
||||||
|
}, false)
|
||||||
|
end
|
||||||
|
if not state then
|
||||||
|
CTLD._groundLoadState[uname] = { sentZoneWarning = true }
|
||||||
|
else
|
||||||
|
state.sentZoneWarning = true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
else
|
||||||
|
-- Not on ground or moving too fast, reset state
|
||||||
|
local state = CTLD._groundLoadState[uname]
|
||||||
|
if state and state.loading then
|
||||||
|
-- Was loading but now lifted off or moved
|
||||||
|
_coachSend(self, group, uname, 'ground_load_aborted', {}, false)
|
||||||
|
end
|
||||||
|
CTLD._groundLoadState[uname] = nil
|
||||||
|
end
|
||||||
|
else
|
||||||
|
-- At capacity, no need to check
|
||||||
|
CTLD._groundLoadState[uname] = nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Helper: Load a crate into an aircraft (shared by hover and ground load)
|
||||||
|
function CTLD:_loadCrateIntoAircraft(group, crateName, crateMeta)
|
||||||
|
if not group or not crateName or not crateMeta then return false end
|
||||||
|
|
||||||
|
local gname = group:GetName()
|
||||||
|
local unit = group:GetUnit(1)
|
||||||
|
if not unit or not unit:IsAlive() then return false end
|
||||||
|
|
||||||
|
-- Destroy the static object first
|
||||||
|
local obj = StaticObject.getByName(crateName)
|
||||||
|
if obj then obj:destroy() end
|
||||||
|
|
||||||
|
-- Clean up crate smoke refresh schedule
|
||||||
|
_cleanupCrateSmoke(crateName)
|
||||||
|
|
||||||
|
-- Remove from spatial grid and tracking
|
||||||
|
_removeFromSpatialGrid(crateName, crateMeta.point, 'crate')
|
||||||
|
CTLD._crates[crateName] = nil
|
||||||
|
|
||||||
|
-- Add to loaded crates using existing method (maintains consistency with rest of code)
|
||||||
|
self:_addLoadedCrate(group, crateMeta.key)
|
||||||
|
|
||||||
|
-- Update inventory if enabled
|
||||||
|
if self.Config.Inventory and self.Config.Inventory.Enabled and crateMeta.spawnZone then
|
||||||
|
pcall(function() self:_updateInventoryOnPickup(crateMeta.spawnZone, crateMeta.key) end)
|
||||||
|
end
|
||||||
|
|
||||||
|
_logDebug(string.format('[Load] Loaded crate %s (%s) into %s', crateName, crateMeta.key, gname))
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
-- #endregion Ground auto-load scanner
|
||||||
|
|
||||||
-- =========================
|
-- =========================
|
||||||
-- Troops
|
-- Troops
|
||||||
-- =========================
|
-- =========================
|
||||||
@ -8166,7 +8637,107 @@ function CTLD:LoadTroops(group, opts)
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Enforce pickup zone requirement for troop loading (inside zone)
|
-- Check for nearby deployed troops first (allow pickup regardless of zone restrictions)
|
||||||
|
local p3 = unit:GetPointVec3()
|
||||||
|
local maxPickupDistance = 25 -- meters for ground-based troop pickup (matches hover pickup distance)
|
||||||
|
local nearby = _getNearbyFromSpatialGrid(p3.x, p3.z, maxPickupDistance)
|
||||||
|
|
||||||
|
local nearbyTroopGroup = nil
|
||||||
|
local nearbyTroopMeta = nil
|
||||||
|
local nearbyTroopDist = nil
|
||||||
|
|
||||||
|
-- Search for nearby deployed troops
|
||||||
|
for troopGroupName, troopMeta in pairs(nearby.troops) do
|
||||||
|
if troopMeta.side == self.Side then
|
||||||
|
local troopGroup = GROUP:FindByName(troopGroupName)
|
||||||
|
if troopGroup and troopGroup:IsAlive() then
|
||||||
|
local troopPos = troopGroup:GetCoordinate()
|
||||||
|
if troopPos then
|
||||||
|
local tp = troopPos:GetVec3()
|
||||||
|
local dx = (tp.x - p3.x)
|
||||||
|
local dz = (tp.z - p3.z)
|
||||||
|
local d = math.sqrt(dx*dx + dz*dz)
|
||||||
|
if d <= maxPickupDistance and ((not nearbyTroopDist) or d < nearbyTroopDist) then
|
||||||
|
nearbyTroopGroup = troopGroup
|
||||||
|
nearbyTroopMeta = troopMeta
|
||||||
|
nearbyTroopDist = d
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- If we found nearby deployed troops, pick them up (bypass zone requirement)
|
||||||
|
if nearbyTroopGroup and nearbyTroopMeta then
|
||||||
|
-- Load the deployed troops
|
||||||
|
local troopCount = nearbyTroopMeta.count or 0
|
||||||
|
local typeKey = nearbyTroopMeta.typeKey or 'AS'
|
||||||
|
local troopWeight = nearbyTroopMeta.weightKg or 0
|
||||||
|
|
||||||
|
-- Check aircraft capacity
|
||||||
|
local capacity = _getAircraftCapacity(unit)
|
||||||
|
local maxTroops = capacity.maxTroops
|
||||||
|
local maxWeight = capacity.maxWeightKg or 0
|
||||||
|
|
||||||
|
local currentTroops = CTLD._troopsLoaded[gname]
|
||||||
|
local currentTroopCount = currentTroops and currentTroops.count or 0
|
||||||
|
local totalTroopCount = currentTroopCount + troopCount
|
||||||
|
|
||||||
|
local carried = CTLD._loadedCrates[gname]
|
||||||
|
local currentWeight = carried and carried.totalWeightKg or 0
|
||||||
|
local wouldBeWeight = currentWeight + troopWeight
|
||||||
|
|
||||||
|
-- Check capacity
|
||||||
|
if totalTroopCount > maxTroops then
|
||||||
|
local aircraftType = _getUnitType(unit) or 'aircraft'
|
||||||
|
_msgGroup(group, string.format('Troop capacity exceeded! Current: %d, Adding: %d, Max: %d for %s',
|
||||||
|
currentTroopCount, troopCount, maxTroops, aircraftType))
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
if maxWeight > 0 and wouldBeWeight > maxWeight then
|
||||||
|
local aircraftType = _getUnitType(unit) or 'aircraft'
|
||||||
|
_msgGroup(group, string.format('Weight capacity exceeded! Current: %dkg, Troops: %dkg, Max: %dkg for %s',
|
||||||
|
math.floor(currentWeight), math.floor(troopWeight), math.floor(maxWeight), aircraftType))
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Load the troops and remove from deployed tracking
|
||||||
|
if currentTroops then
|
||||||
|
local troopTypes = currentTroops.troopTypes or { { typeKey = currentTroops.typeKey, count = currentTroops.count } }
|
||||||
|
table.insert(troopTypes, { typeKey = typeKey, count = troopCount })
|
||||||
|
|
||||||
|
CTLD._troopsLoaded[gname] = {
|
||||||
|
count = totalTroopCount,
|
||||||
|
typeKey = 'Mixed',
|
||||||
|
troopTypes = troopTypes,
|
||||||
|
weightKg = currentTroops.weightKg + troopWeight,
|
||||||
|
}
|
||||||
|
else
|
||||||
|
CTLD._troopsLoaded[gname] = {
|
||||||
|
count = troopCount,
|
||||||
|
typeKey = typeKey,
|
||||||
|
troopTypes = { { typeKey = typeKey, count = troopCount } },
|
||||||
|
weightKg = troopWeight,
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Remove from deployed tracking
|
||||||
|
local troopGroupName = nearbyTroopGroup:GetName()
|
||||||
|
_removeFromSpatialGrid(troopGroupName, nearbyTroopMeta.point, 'troops')
|
||||||
|
CTLD._deployedTroops[troopGroupName] = nil
|
||||||
|
|
||||||
|
-- Destroy the troop group
|
||||||
|
nearbyTroopGroup:Destroy()
|
||||||
|
|
||||||
|
self:_refreshLoadedTroopSummaryForGroup(gname)
|
||||||
|
_eventSend(self, group, nil, 'troops_loaded', { count = totalTroopCount })
|
||||||
|
_msgGroup(group, string.format('Picked up %d deployed troops (total onboard: %d)', troopCount, totalTroopCount))
|
||||||
|
|
||||||
|
return -- Successfully picked up deployed troops, exit function
|
||||||
|
end
|
||||||
|
|
||||||
|
-- No nearby deployed troops found, enforce pickup zone requirement for spawning new troops
|
||||||
if self.Config.RequirePickupZoneForTroopLoad then
|
if self.Config.RequirePickupZoneForTroopLoad then
|
||||||
local hasPickupZones = (self.PickupZones and #self.PickupZones > 0) or (self.Config.Zones and self.Config.Zones.PickupZones and #self.Config.Zones.PickupZones > 0)
|
local hasPickupZones = (self.PickupZones and #self.PickupZones > 0) or (self.Config.Zones and self.Config.Zones.PickupZones and #self.Config.Zones.PickupZones > 0)
|
||||||
if not hasPickupZones then
|
if not hasPickupZones then
|
||||||
|
|||||||
@ -931,8 +931,56 @@ function FAC:_buildObserverMenu(group)
|
|||||||
CMD('Show Active FAC/RECCE Controllers', function() self:_showFacStatus(group) end)
|
CMD('Show Active FAC/RECCE Controllers', function() self:_showFacStatus(group) end)
|
||||||
CMD('Show FAC Codes In Use', function() self:_showCodesCoalition() end)
|
CMD('Show FAC Codes In Use', function() self:_showCodesCoalition() end)
|
||||||
CMD('FAC/RECCE Help', function()
|
CMD('FAC/RECCE Help', function()
|
||||||
local msg = 'FAC/RECCE controls are available from aircraft configured as FAC platforms. Join an AFAC/RECCE group or use one of the approved aircraft types to access full targeting tools.'
|
local types = self.Config.facACTypes or {}
|
||||||
MESSAGE:New(msg, 12):ToGroup(group)
|
local typeList = (#types > 0) and table.concat(types, ', ') or 'see mission briefing'
|
||||||
|
|
||||||
|
local laserCodes = self.Config.FAC_laser_codes or {'1688'}
|
||||||
|
local defaultCode = laserCodes[1] or '1688'
|
||||||
|
local allCodes = table.concat(laserCodes, ', ')
|
||||||
|
|
||||||
|
local maxDist = tostring(self.Config.FAC_maxDistance or 18520)
|
||||||
|
local rootName = self.Config.RootMenuName or 'FAC/RECCE'
|
||||||
|
local markerDefault = self.Config.MarkerDefault or 'FLARES'
|
||||||
|
|
||||||
|
local msg = table.concat({
|
||||||
|
'FAC/RECCE Overview:',
|
||||||
|
'',
|
||||||
|
'- This module lets certain aircraft act as an airborne JTAC / artillery spotter.',
|
||||||
|
'- To get the FAC menu, you must be in a group named with AFAC/RECCE/RECON,',
|
||||||
|
' or flying one of the approved FAC aircraft types (' .. typeList .. ').',
|
||||||
|
'',
|
||||||
|
'Basic Usage:',
|
||||||
|
'- Open the F10 radio menu and look for "' .. rootName .. '".',
|
||||||
|
'- Use "Auto Laze ON" to have the module automatically search for and lase nearby enemy targets.',
|
||||||
|
'- Use "Scan for Close Targets" then "Select Found Target" to manually pick a target from a list.',
|
||||||
|
'- Use "RECCE: Sweep & Mark" to scan a larger area and drop map markers on detected contacts.',
|
||||||
|
'',
|
||||||
|
'Laser Codes:',
|
||||||
|
'- Default FAC laser code: ' .. defaultCode .. '.',
|
||||||
|
'- Allowed codes: ' .. allCodes .. '.',
|
||||||
|
'- Use the "Laser Code" submenu to change your code if another FAC is already using it.',
|
||||||
|
'- The module will try to avoid code conflicts and will notify you if a different code is assigned.',
|
||||||
|
'',
|
||||||
|
'Markers & Smoke:',
|
||||||
|
'- Default marker type: ' .. markerDefault .. '.',
|
||||||
|
'- FAC can mark the current target with smoke or flares in different colors.',
|
||||||
|
'- Use the "Marker" submenu to choose SMOKE or FLARES and a color for your marks.',
|
||||||
|
'',
|
||||||
|
'Range & Line of Sight:',
|
||||||
|
'- FAC search range is about ' .. maxDist .. ' meters (~10 NM).',
|
||||||
|
'- Targets must be within line-of-sight; hills and terrain can block detection and lasing.',
|
||||||
|
'',
|
||||||
|
'Artillery & Air Support:',
|
||||||
|
'- The "Artillery" and "Air/Naval" menus look for AI units on your side that can fire on the target.',
|
||||||
|
'- If no suitable unit is in range or has ammo, the module will tell you.',
|
||||||
|
'- Guided/air/naval options require appropriate AI aircraft or ships placed by the mission designer.',
|
||||||
|
'',
|
||||||
|
'If you do not see FAC menus:',
|
||||||
|
'- Check that your group name contains AFAC/RECCE/RECON, or you are flying a supported FAC aircraft type.',
|
||||||
|
'- Make sure Moose.lua, Moose_CTLD.lua, and Moose_CTLD_FAC.lua are all loaded in the mission (in that order).',
|
||||||
|
}, '\n')
|
||||||
|
|
||||||
|
MESSAGE:New(msg, 30):ToGroup(group)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
return { root = root }
|
return { root = root }
|
||||||
|
|||||||
Binary file not shown.
Loading…
x
Reference in New Issue
Block a user