Configured messages to be one table for simplicity.

This commit is contained in:
iTracerFacer
2025-11-04 10:57:24 -06:00
parent d74226ee40
commit 2d7ec65c65

View File

@@ -46,22 +46,57 @@ CTLD.HoverCoachConfig = {
generic = 3.0, -- s between non-coach messages generic = 3.0, -- s between non-coach messages
repeatSame = 6.0 -- s before repeating same message key repeatSame = 6.0 -- s before repeating same message key
}, },
}
messages = { -- General CTLD event messages (non-hover). Tweak freely.
-- Placeholders: {id}, {type}, {brg}, {rng}, {rng_u}, {gs}, {gs_u}, {agl}, {agl_u}, {hints} CTLD.Messages = {
spawned = "Crates live! {type} [{id}]. Bearing {brg}° range {rng} {rng_u}. Call for vectors if you need a hand.", -- Crates
arrival = "Youre close—nice and easy. Hover at 520 meters.", crate_spawn_requested = "Request received—spawning {type} crate at {zone}.",
close = "Reduce speed below 15 km/h and set 520 m AGL.", pickup_zone_required = "Move within {zone_dist} {zone_dist_u} of a Supply Zone to request crates.",
coach = "{hints} GS {gs} {gs_u}.", crate_re_marked = "Re-marking crate {id} with {mark}.",
tooFast = "Too fast for pickup: GS {gs} {gs_u}. Reduce below 8 km/h.", crate_expired = "Crate {id} expired and was removed.",
tooHigh = "Too high: AGL {agl} {agl_u}. Target 520 m.", crate_max_capacity = "Max load reached ({total}). Drop or build before picking up more.",
tooLow = "Too low: AGL {agl} {agl_u}. Maintain at least 5 m.", crate_spawned = "Crates live! {type} [{id}]. Bearing {brg}° range {rng} {rng_u}. Call for vectors if you need a hand.",
drift = "Outside pickup window. Re-center within 25 m.",
hold = "Oooh, right there! HOLD POSITION…", -- Drops
loaded = "Crate is hooked! Nice flying!", drop_initiated = "Dropping {count} crate(s) here…",
hoverLost = "Movement detected—recover hover to load.", dropped_crates = "Dropped {count} crate(s) at your location.",
abort = "Hover lost. Reacquire within 25 m, GS < 8 km/h, AGL 520 m.", no_loaded_crates = "No loaded crates to drop.",
}
-- Build
build_insufficient_crates = "Insufficient crates to build {build}.",
build_requires_ground = "You have {total} crate(s) onboard—drop them first to build here.",
build_started = "Building {build} at your position…",
build_success = "{build} deployed to the field!",
build_success_coalition = "{player} deployed {build} to the field!",
build_failed = "Build failed: {reason}.",
fob_restricted = "FOB building is restricted to designated FOB zones.",
auto_fob_built = "FOB auto-built at {zone}.",
-- Troops
troops_loaded = "Loaded {count} troops—ready to deploy.",
troops_unloaded = "Deployed {count} troops.",
troops_unloaded_coalition = "{player} deployed {count} troops.",
no_troops = "No troops onboard.",
troops_deploy_failed = "Deploy failed: {reason}.",
-- Coach & nav
vectors_to_crate = "Nearest crate {id}: bearing {brg}°, range {rng} {rng_u}.",
coach_enabled = "Hover Coach enabled.",
coach_disabled = "Hover Coach disabled.",
-- Hover Coach guidance
coach_arrival = "Youre close—nice and easy. Hover at 520 meters.",
coach_close = "Reduce speed below 15 km/h and set 520 m AGL.",
coach_hint = "{hints} GS {gs} {gs_u}.",
coach_too_fast = "Too fast for pickup: GS {gs} {gs_u}. Reduce below 8 km/h.",
coach_too_high = "Too high: AGL {agl} {agl_u}. Target 520 m.",
coach_too_low = "Too low: AGL {agl} {agl_u}. Maintain at least 5 m.",
coach_drift = "Outside pickup window. Re-center within 25 m.",
coach_hold = "Oooh, right there! HOLD POSITION…",
coach_loaded = "Crate is hooked! Nice flying!",
coach_hover_lost = "Movement detected—recover hover to load.",
coach_abort = "Hover lost. Reacquire within 25 m, GS < 8 km/h, AGL 520 m.",
} }
CTLD.Config = { CTLD.Config = {
@@ -209,6 +244,7 @@ CTLD._loadedCrates = {} -- [groupName] = { total=n, byKey = { key -> count }
CTLD._hoverState = {} -- [unitName] = { targetCrate=name, startTime=t } CTLD._hoverState = {} -- [unitName] = { targetCrate=name, startTime=t }
CTLD._unitLast = {} -- [unitName] = { x, z, t } CTLD._unitLast = {} -- [unitName] = { x, z, t }
CTLD._coachState = {} -- [unitName] = { lastKeyTimes = {key->time}, lastHint = "", phase = "", lastPhaseMsg = 0, target = crateName, holdStart = nil } CTLD._coachState = {} -- [unitName] = { lastKeyTimes = {key->time}, lastHint = "", phase = "", lastPhaseMsg = 0, target = crateName, holdStart = nil }
CTLD._msgState = { } -- messaging throttle state: [scopeKey] = { lastKeyTimes = { key -> time } }
-- ========================= -- =========================
-- Utilities -- Utilities
@@ -360,19 +396,26 @@ local function _projectToBodyFrame(dx, dz, hdg)
return right, fwd return right, fwd
end end
local function _playerNameFromGroup(group)
if not group then return 'Player' end
local unit = group:GetUnit(1)
local pname = unit and unit.GetPlayerName and unit:GetPlayerName()
if pname and pname ~= '' then return pname end
return group:GetName() or 'Player'
end
local function _coachSend(self, group, unitName, key, data, isCoach) local function _coachSend(self, group, unitName, key, data, isCoach)
local cfg = CTLD.HoverCoachConfig local cfg = CTLD.HoverCoachConfig or {}
if not (cfg and cfg.enabled) then return end
local now = timer.getTime() local now = timer.getTime()
CTLD._coachState[unitName] = CTLD._coachState[unitName] or { lastKeyTimes = {} } CTLD._coachState[unitName] = CTLD._coachState[unitName] or { lastKeyTimes = {} }
local st = CTLD._coachState[unitName] local st = CTLD._coachState[unitName]
local last = st.lastKeyTimes[key] or 0 local last = st.lastKeyTimes[key] or 0
local minGap = isCoach and (cfg.throttle.coachUpdate or 1.5) or (cfg.throttle.generic or 3.0) local minGap = isCoach and ((cfg.throttle and cfg.throttle.coachUpdate) or 1.5) or ((cfg.throttle and cfg.throttle.generic) or 3.0)
local repeatGap = cfg.throttle.repeatSame or (minGap * 2) local repeatGap = (cfg.throttle and cfg.throttle.repeatSame) or (minGap * 2)
if last > 0 and (now - last) < minGap then return end if last > 0 and (now - last) < minGap then return end
-- prevent repeat spam of identical key too fast (only after first send) -- prevent repeat spam of identical key too fast (only after first send)
if last > 0 and (now - last) < repeatGap then return end if last > 0 and (now - last) < repeatGap then return end
local tpl = cfg.messages[key] local tpl = CTLD.Messages and CTLD.Messages[key]
if not tpl then return end if not tpl then return end
local text = _fmtTemplate(tpl, data) local text = _fmtTemplate(tpl, data)
if text and text ~= '' then if text and text ~= '' then
@@ -381,6 +424,27 @@ local function _coachSend(self, group, unitName, key, data, isCoach)
end end
end end
local function _eventSend(self, group, side, key, data)
local tpl = CTLD.Messages and CTLD.Messages[key]
if not tpl then return end
local now = timer.getTime()
local scopeKey
if group then scopeKey = 'GRP:'..group:GetName() else scopeKey = 'COAL:'..tostring(side or self.Side) end
CTLD._msgState[scopeKey] = CTLD._msgState[scopeKey] or { lastKeyTimes = {} }
local st = CTLD._msgState[scopeKey]
local last = st.lastKeyTimes[key] or 0
local cfg = CTLD.HoverCoachConfig
local minGap = (cfg and cfg.throttle and cfg.throttle.generic) or 3.0
local repeatGap = (cfg and cfg.throttle and cfg.throttle.repeatSame) or (minGap * 2)
if last > 0 and (now - last) < minGap then return end
if last > 0 and (now - last) < repeatGap then return end
local text = _fmtTemplate(tpl, data)
if not text or text == '' then return end
if group then _msgGroup(group, text) else _msgCoalition(side or self.Side, text) end
st.lastKeyTimes[key] = now
end
-- Determine an approximate radius for a ZONE. Tries MOOSE radius, then trigger zone radius, then configured radius. -- Determine an approximate radius for a ZONE. Tries MOOSE radius, then trigger zone radius, then configured radius.
function CTLD:_getZoneRadius(zone) function CTLD:_getZoneRadius(zone)
if zone and zone.Radius then return zone.Radius end if zone and zone.Radius then return zone.Radius end
@@ -565,6 +629,70 @@ function CTLD:BuildGroupMenus(group)
self:DropLoadedCrates(group, -1) self:DropLoadedCrates(group, -1)
end) end)
-- Coach & Navigation utilities
local navRoot = MENU_GROUP:New(group, 'Coach & Nav', root)
local gname = group:GetName()
MENU_GROUP_COMMAND:New(group, 'Hover Coach: Enable', navRoot, function()
CTLD._coachOverride = CTLD._coachOverride or {}
CTLD._coachOverride[gname] = true
_eventSend(self, group, nil, 'coach_enabled', {})
end)
MENU_GROUP_COMMAND:New(group, 'Hover Coach: Disable', navRoot, function()
CTLD._coachOverride = CTLD._coachOverride or {}
CTLD._coachOverride[gname] = false
_eventSend(self, group, nil, 'coach_disabled', {})
end)
MENU_GROUP_COMMAND:New(group, 'Request Vectors to Nearest Crate', navRoot, function()
local unit = group:GetUnit(1)
if not unit or not unit:IsAlive() then return end
local p = unit:GetPointVec3()
local here = { x = p.x, z = p.z }
-- find nearest same-side crate
local bestName, bestMeta, bestd
for name,meta in pairs(CTLD._crates) do
if meta.side == self.Side then
local dx = (meta.point.x - here.x)
local dz = (meta.point.z - here.z)
local d = math.sqrt(dx*dx + dz*dz)
if (not bestd) or d < bestd then
bestName, bestMeta, bestd = name, meta, d
end
end
end
if bestName and bestMeta then
local brg = _bearingDeg(here, bestMeta.point)
local isMetric = _getPlayerIsMetric(unit)
local rngV, rngU = _fmtRange(bestd, isMetric)
_eventSend(self, group, nil, 'vectors_to_crate', { id = bestName, brg = brg, rng = rngV, rng_u = rngU })
else
_msgGroup(group, 'No friendly crates found.')
end
end)
MENU_GROUP_COMMAND:New(group, 'Re-mark Nearest Crate (Smoke)', navRoot, function()
local unit = group:GetUnit(1)
if not unit or not unit:IsAlive() then return end
local p = unit:GetPointVec3()
local here = { x = p.x, z = p.z }
local bestName, bestMeta, bestd
for name,meta in pairs(CTLD._crates) do
if meta.side == self.Side then
local dx = (meta.point.x - here.x)
local dz = (meta.point.z - here.z)
local d = math.sqrt(dx*dx + dz*dz)
if (not bestd) or d < bestd then
bestName, bestMeta, bestd = name, meta, d
end
end
end
if bestName and bestMeta then
local zdef = { smoke = self.Config.PickupZoneSmokeColor }
trigger.action.smoke({ x = bestMeta.point.x, z = bestMeta.point.z }, (zdef and zdef.smoke) or self.Config.PickupZoneSmokeColor)
_eventSend(self, group, nil, 'crate_re_marked', { id = bestName, mark = 'smoke' })
else
_msgGroup(group, 'No friendly crates found to mark.')
end
end)
return root return root
end end
@@ -589,6 +717,8 @@ function CTLD:RequestCrateForGroup(group, crateKey)
local zone, dist = _nearestZonePoint(unit, self.Config.Zones.PickupZones) local zone, dist = _nearestZonePoint(unit, self.Config.Zones.PickupZones)
local spawnPoint local spawnPoint
local maxd = (self.Config.PickupZoneMaxDistance or 10000) local maxd = (self.Config.PickupZoneMaxDistance or 10000)
-- Announce request
_eventSend(self, group, nil, 'crate_spawn_requested', { type = tostring(crateKey), zone = zone and zone:GetName() or 'nearest zone' })
if zone and dist <= maxd then if zone and dist <= maxd then
spawnPoint = zone:GetPointVec3() spawnPoint = zone:GetPointVec3()
-- if pickup zone has smoke configured, mark it -- if pickup zone has smoke configured, mark it
@@ -600,7 +730,9 @@ function CTLD:RequestCrateForGroup(group, crateKey)
else else
-- Either require a pickup zone proximity, or fallback to near-aircraft spawn (legacy behavior) -- Either require a pickup zone proximity, or fallback to near-aircraft spawn (legacy behavior)
if self.Config.RequirePickupZoneForCrateRequest then if self.Config.RequirePickupZoneForCrateRequest then
_msgGroup(group, string.format('You are not close enough to a Supply Zone to request resources. Move within %.1f km.', (maxd or 10000)/1000)) local isMetric = _getPlayerIsMetric(unit)
local v, u = _fmtRange(math.max(0, dist - maxd), isMetric)
_eventSend(self, group, nil, 'pickup_zone_required', { zone_dist = v, zone_dist_u = u })
return return
else else
-- fallback: spawn near aircraft current position (safe offset) -- fallback: spawn near aircraft current position (safe offset)
@@ -615,6 +747,7 @@ function CTLD:RequestCrateForGroup(group, crateKey)
side = self.Side, side = self.Side,
spawnTime = timer.getTime(), spawnTime = timer.getTime(),
point = { x = spawnPoint.x, z = spawnPoint.z }, point = { x = spawnPoint.x, z = spawnPoint.z },
requester = group:GetName(),
} }
-- Immersive spawn message with bearing/range per player units -- Immersive spawn message with bearing/range per player units
do do
@@ -632,7 +765,7 @@ function CTLD:RequestCrateForGroup(group, crateKey)
rng = rngV, rng = rngV,
rng_u = rngU, rng_u = rngU,
} }
_coachSend(self, group, unit:GetName(), 'spawned', data, false) _eventSend(self, group, nil, 'crate_spawned', data)
end end
end end
@@ -658,6 +791,14 @@ function CTLD:CleanupCrates()
if obj then obj:destroy() end if obj then obj:destroy() end
CTLD._crates[name] = nil CTLD._crates[name] = nil
if self.Config.Debug then env.info('[CTLD] Cleaned up crate '..name) end if self.Config.Debug then env.info('[CTLD] Cleaned up crate '..name) end
-- Notify requester group if still around; else coalition
local gname = meta.requester
local group = gname and GROUP:FindByName(gname) or nil
if group and group:IsAlive() then
_eventSend(self, group, nil, 'crate_expired', { id = name })
else
_eventSend(self, nil, self.Side, 'crate_expired', { id = name })
end
end end
end end
end end
@@ -678,7 +819,10 @@ function CTLD:BuildAtGroup(group)
if c.meta.side == self.Side then table.insert(filtered, c) end if c.meta.side == self.Side then table.insert(filtered, c) end
end end
nearby = filtered nearby = filtered
if #nearby == 0 then _msgGroup(group, 'No crates within '..radius..'m') return end if #nearby == 0 then
_eventSend(self, group, nil, 'build_insufficient_crates', { build = 'asset' })
return
end
-- Count by key -- Count by key
local counts = {} local counts = {}
@@ -736,13 +880,14 @@ function CTLD:BuildAtGroup(group)
if ok then if ok then
local hdg = unit:GetHeading() local hdg = unit:GetHeading()
local gdata = cat.build({ x = here.x, z = here.z }, math.deg(hdg), cat.side or self.Side) local gdata = cat.build({ x = here.x, z = here.z }, math.deg(hdg), cat.side or self.Side)
_eventSend(self, group, nil, 'build_started', { build = cat.description or recipeKey })
local g = _coalitionAddGroup(cat.side or self.Side, cat.category or Group.Category.GROUND, gdata) local g = _coalitionAddGroup(cat.side or self.Side, cat.category or Group.Category.GROUND, gdata)
if g then if g then
for reqKey,qty in pairs(cat.requires) do consumeCrates(reqKey, qty) end for reqKey,qty in pairs(cat.requires) do consumeCrates(reqKey, qty) end
_msgGroup(group, string.format('Built %s at your location', cat.description or recipeKey)) _eventSend(self, nil, self.Side, 'build_success_coalition', { build = cat.description or recipeKey, player = _playerNameFromGroup(group) })
return return
else else
_msgGroup(group, 'Build failed: DCS group spawn error') _eventSend(self, group, nil, 'build_failed', { reason = 'DCS group spawn error' })
return return
end end
end end
@@ -759,13 +904,14 @@ function CTLD:BuildAtGroup(group)
else else
local hdg = unit:GetHeading() local hdg = unit:GetHeading()
local gdata = cat.build({ x = here.x, z = here.z }, math.deg(hdg), cat.side or self.Side) local gdata = cat.build({ x = here.x, z = here.z }, math.deg(hdg), cat.side or self.Side)
_eventSend(self, group, nil, 'build_started', { build = cat.description or key })
local g = _coalitionAddGroup(cat.side or self.Side, cat.category or Group.Category.GROUND, gdata) local g = _coalitionAddGroup(cat.side or self.Side, cat.category or Group.Category.GROUND, gdata)
if g then if g then
consumeCrates(key, cat.required or 1) consumeCrates(key, cat.required or 1)
_msgGroup(group, string.format('Built %s at your location', cat.description or key)) _eventSend(self, nil, self.Side, 'build_success_coalition', { build = cat.description or key, player = _playerNameFromGroup(group) })
return return
else else
_msgGroup(group, 'Build failed: DCS group spawn error') _eventSend(self, group, nil, 'build_failed', { reason = 'DCS group spawn error' })
return return
end end
end end
@@ -773,7 +919,7 @@ function CTLD:BuildAtGroup(group)
end end
if fobBlocked then if fobBlocked then
_msgGroup(group, 'FOB building is restricted to designated FOB zones. Move inside a FOB zone to build.') _eventSend(self, group, nil, 'fob_restricted', {})
return return
end end
@@ -781,12 +927,11 @@ function CTLD:BuildAtGroup(group)
if self.Config.BuildRequiresGroundCrates == true then if self.Config.BuildRequiresGroundCrates == true then
local carried = CTLD._loadedCrates[gname] local carried = CTLD._loadedCrates[gname]
if carried and (carried.total or 0) > 0 then if carried and (carried.total or 0) > 0 then
_msgGroup(group, string.format('Insufficient ground crates to build here. You have %d loaded crate(s) onboard — drop them first (F10 -> Drop Loaded Crates).', carried.total)) _eventSend(self, group, nil, 'build_requires_ground', { total = carried.total })
return return
end end
end end
_eventSend(self, group, nil, 'build_insufficient_crates', { build = 'asset' })
_msgGroup(group, 'Insufficient crates to build any asset here')
end end
-- ========================= -- =========================
@@ -803,12 +948,15 @@ end
function CTLD:DropLoadedCrates(group, howMany) function CTLD:DropLoadedCrates(group, howMany)
local gname = group:GetName() local gname = group:GetName()
local lc = CTLD._loadedCrates[gname] local lc = CTLD._loadedCrates[gname]
if not lc or (lc.total or 0) == 0 then _msgGroup(group, 'No loaded crates to drop') return end if not lc or (lc.total or 0) == 0 then _eventSend(self, group, nil, 'no_loaded_crates', {}) return end
local unit = group:GetUnit(1) local unit = group:GetUnit(1)
if not unit or not unit:IsAlive() then return end if not unit or not unit:IsAlive() then return end
local p = unit:GetPointVec3() local p = unit:GetPointVec3()
local here = { x = p.x, z = p.z } local here = { x = p.x, z = p.z }
local toDrop = (howMany and howMany > 0) and howMany or lc.total local initialTotal = lc.total or 0
local requested = (howMany and howMany > 0) and howMany or initialTotal
local toDrop = math.min(requested, initialTotal)
_eventSend(self, group, nil, 'drop_initiated', { count = toDrop })
-- Drop in key order -- Drop in key order
for k,count in pairs(BASE:DeepCopy(lc.byKey)) do for k,count in pairs(BASE:DeepCopy(lc.byKey)) do
if toDrop <= 0 then break end if toDrop <= 0 then break end
@@ -825,7 +973,8 @@ function CTLD:DropLoadedCrates(group, howMany)
if toDrop <= 0 then break end if toDrop <= 0 then break end
end end
end end
_msgGroup(group, 'Dropped loaded crates') local actualDropped = initialTotal - (lc.total or 0)
_eventSend(self, group, nil, 'dropped_crates', { count = actualDropped })
end end
-- ========================= -- =========================
@@ -877,16 +1026,21 @@ function CTLD:ScanHoverPickup()
end end
end end
-- Resolve per-group coach enable override
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 coachCfg.enabled and bestName and bestMeta then if coachEnabled and bestName and bestMeta then
local isMetric = _getPlayerIsMetric(unit) local isMetric = _getPlayerIsMetric(unit)
-- Arrival phase -- Arrival phase
if bestd <= (coachCfg.thresholds.arrivalDist or 1000) then if bestd <= (coachCfg.thresholds.arrivalDist or 1000) then
_coachSend(self, group, uname, 'arrival', {}, false) _coachSend(self, group, uname, 'coach_arrival', {}, false)
end end
-- Close-in -- Close-in
if bestd <= (coachCfg.thresholds.closeDist or 100) then if bestd <= (coachCfg.thresholds.closeDist or 100) then
_coachSend(self, group, uname, 'close', {}, false) _coachSend(self, group, uname, 'coach_close', {}, false)
end end
-- Precision phase -- Precision phase
@@ -926,7 +1080,7 @@ function CTLD:ScanHoverPickup()
local gsV, gsU = _fmtSpeed(gs, isMetric) local gsV, gsU = _fmtSpeed(gs, isMetric)
local data = { hints = (hints ~= '' and (hints..'.') or ''), gs = gsV, gs_u = gsU } local data = { hints = (hints ~= '' and (hints..'.') or ''), gs = gsV, gs_u = gsU }
_coachSend(self, group, uname, 'coach', data, true) _coachSend(self, group, uname, 'coach_hint', data, true)
-- Error prompts (dominant one) -- Error prompts (dominant one)
local maxGS = coachCfg.thresholds.maxGS or (8/3.6) local maxGS = coachCfg.thresholds.maxGS or (8/3.6)
@@ -934,13 +1088,13 @@ function CTLD:ScanHoverPickup()
local aglMaxT = aglMax local aglMaxT = aglMax
if gs > maxGS then if gs > maxGS then
local v, u = _fmtSpeed(gs, isMetric) local v, u = _fmtSpeed(gs, isMetric)
_coachSend(self, group, uname, 'tooFast', { gs = v, gs_u = u }, false) _coachSend(self, group, uname, 'coach_too_fast', { gs = v, gs_u = u }, false)
elseif agl > aglMaxT then elseif agl > aglMaxT then
local v, u = _fmtAGL(agl, isMetric) local v, u = _fmtAGL(agl, isMetric)
_coachSend(self, group, uname, 'tooHigh', { agl = v, agl_u = u }, false) _coachSend(self, group, uname, 'coach_too_high', { agl = v, agl_u = u }, false)
elseif agl < aglMinT then elseif agl < aglMinT then
local v, u = _fmtAGL(agl, isMetric) local v, u = _fmtAGL(agl, isMetric)
_coachSend(self, group, uname, 'tooLow', { 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
@@ -973,7 +1127,7 @@ function CTLD:ScanHoverPickup()
local hs = CTLD._hoverState[uname] local hs = CTLD._hoverState[uname]
if not hs or hs.targetCrate ~= bestName then if not hs or hs.targetCrate ~= bestName then
CTLD._hoverState[uname] = { targetCrate = bestName, startTime = now } CTLD._hoverState[uname] = { targetCrate = bestName, startTime = now }
if coachCfg.enabled then _coachSend(self, group, uname, 'hold', {}, false) end if coachCfg.enabled then _coachSend(self, group, uname, 'coach_hold', {}, false) end
else else
-- stability hold timer -- stability hold timer
local holdNeeded = coachCfg.enabled and (coachCfg.thresholds.stabilityHold or (hp.Duration or 3)) or (hp.Duration or 3) local holdNeeded = coachCfg.enabled and (coachCfg.thresholds.stabilityHold or (hp.Duration or 3)) or (hp.Duration or 3)
@@ -983,8 +1137,8 @@ function CTLD:ScanHoverPickup()
if obj then obj:destroy() end if obj then obj:destroy() end
CTLD._crates[bestName] = nil CTLD._crates[bestName] = nil
self:_addLoadedCrate(group, bestMeta.key) self:_addLoadedCrate(group, bestMeta.key)
if coachCfg.enabled then if coachEnabled then
_coachSend(self, group, uname, 'loaded', {}, false) _coachSend(self, group, uname, 'coach_loaded', {}, false)
else else
_msgGroup(group, string.format('Loaded %s crate', tostring(bestMeta.key))) _msgGroup(group, string.format('Loaded %s crate', tostring(bestMeta.key)))
end end
@@ -994,13 +1148,13 @@ function CTLD:ScanHoverPickup()
end end
else else
-- lost precision window -- lost precision window
if coachCfg.enabled then _coachSend(self, group, uname, 'hoverLost', {}, 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 coachCfg.enabled then _coachSend(self, group, uname, '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
@@ -1023,13 +1177,13 @@ function CTLD:LoadTroops(group, opts)
count = capacity, count = capacity,
typeKey = 'RIFLE', typeKey = 'RIFLE',
} }
_msgGroup(group, string.format('Loaded %d troops (virtual). Use Unload Troops to deploy.', capacity)) _eventSend(self, group, nil, 'troops_loaded', { count = capacity })
end end
function CTLD:UnloadTroops(group) function CTLD:UnloadTroops(group)
local gname = group:GetName() local gname = group:GetName()
local load = CTLD._troopsLoaded[gname] local load = CTLD._troopsLoaded[gname]
if not load or (load.count or 0) == 0 then _msgGroup(group, 'No troops onboard') return end if not load or (load.count or 0) == 0 then _eventSend(self, group, nil, 'no_troops', {}) return end
local unit = group:GetUnit(1) local unit = group:GetUnit(1)
if not unit or not unit:IsAlive() then return end if not unit or not unit:IsAlive() then return end
@@ -1053,9 +1207,9 @@ function CTLD:UnloadTroops(group)
local spawned = _coalitionAddGroup(self.Side, Group.Category.GROUND, groupData) local spawned = _coalitionAddGroup(self.Side, Group.Category.GROUND, groupData)
if spawned then if spawned then
CTLD._troopsLoaded[gname] = nil CTLD._troopsLoaded[gname] = nil
_msgGroup(group, string.format('Deployed %d troops', #units)) _eventSend(self, nil, self.Side, 'troops_unloaded_coalition', { count = #units, player = _playerNameFromGroup(group) })
else else
_msgGroup(group, 'Deploy failed: DCS group spawn error') _eventSend(self, group, nil, 'troops_deploy_failed', { reason = 'DCS group spawn error' })
end end
end end