mirror of
https://github.com/iTracerFacer/DCS_MissionDev.git
synced 2025-12-03 04:14:46 +00:00
Working slingload crates adds to salvage if brought to salvage zone.
This commit is contained in:
parent
9520db4e2b
commit
07b5635eaa
@ -23,6 +23,9 @@ CTLD.__index = CTLD
|
||||
CTLD._lastSalvageInterval = CTLD._lastSalvageInterval or 0
|
||||
CTLD._playerUnitPrefs = CTLD._playerUnitPrefs or {}
|
||||
|
||||
local _msgGroup, _msgCoalition
|
||||
local _log, _logError, _logInfo, _logVerbose, _logDebug, _logImmediate
|
||||
|
||||
-- General CTLD event messages (non-hover). Tweak freely.
|
||||
CTLD.Messages = {
|
||||
-- Crates
|
||||
@ -34,7 +37,7 @@ CTLD.Messages = {
|
||||
crate_max_capacity = "Max load reached ({total}). Drop or build before picking up more.",
|
||||
crate_aircraft_capacity = "Aircraft capacity reached ({current}/{max} crates). Your {aircraft} can only carry {max} crates.",
|
||||
troop_aircraft_capacity = "Aircraft capacity reached. Your {aircraft} can only carry {max} troops (you need {count}).",
|
||||
crate_spawned = "Crate’s live! {type} [{id}]. Bearing {brg}° range {rng} {rng_u}. Call for vectors if you need a hand.",
|
||||
crate_spawned = "Crate’s live! {type} [{id}]. Bearing {brg}° range {rng} {rng_u}.\nCall for vectors if you need a hand.\n\nTo load: HOVER within 25m at 5-20m AGL, or LAND within 35m and hold for 25s.",
|
||||
|
||||
-- Drops
|
||||
drop_initiated = "Dropping {count} crate(s) here…",
|
||||
@ -140,6 +143,8 @@ CTLD.Messages = {
|
||||
slingload_salvage_zone_deactivated = "Salvage Collection Zone '{zone}' is now INACTIVE.",
|
||||
slingload_salvage_warn_30min = "SALVAGE REMINDER: Crate {id} at {grid} expires in 30 minutes. Weight: {weight}kg.",
|
||||
slingload_salvage_warn_5min = "SALVAGE URGENT: Crate {id} at {grid} expires in 5 minutes!",
|
||||
slingload_salvage_hooked_in_zone = "Salvage crate {id} is inside {zone}. Release the sling to complete delivery.",
|
||||
slingload_salvage_wrong_zone = "Salvage crate {id} is sitting in {zone_type} zone {zone}. Take it to an active Salvage zone for credit.",
|
||||
slingload_salvage_received_quips = {
|
||||
"{player}: Leroy just whispered that's the smoothest receiving job he's seen since Jenkins tried the backwards hover.",
|
||||
"Jenkins radios {player}, 'Keep receiving cargo like that and Mo might finally stop polishing his hook.'",
|
||||
@ -313,10 +318,10 @@ CTLD.Config = {
|
||||
MessageDuration = 15, -- seconds for on-screen messages
|
||||
|
||||
-- Debug toggles for detailed crate proximity logging (useful when tuning hover coach / ground autoload)
|
||||
DebugHoverCrates = true,
|
||||
DebugHoverCrates = false,
|
||||
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,
|
||||
DebugGroundCrates = false,
|
||||
DebugGroundCratesInterval = 2.0, -- seconds between ground debug log bursts (per aircraft)
|
||||
DebugGroundCratesStep = 10, -- log again when nearest crate distance changes by this many meters
|
||||
|
||||
@ -560,8 +565,8 @@ CTLD.Config = {
|
||||
|
||||
-- Spawn probability when enemy ground units die
|
||||
SpawnChance = {
|
||||
[coalition.side.BLUE] = 0.15, -- 15% chance when BLUE unit dies (RED can collect the salvage)
|
||||
[coalition.side.RED] = 0.15, -- 90% chance when RED unit dies (BLUE can collect the salvage)
|
||||
[coalition.side.BLUE] = 0.20, -- 20% chance when BLUE unit dies (RED can collect the salvage)
|
||||
[coalition.side.RED] = 0.20, -- 20% chance when RED unit dies (BLUE can collect the salvage)
|
||||
},
|
||||
|
||||
-- Weight classes with spawn probabilities and reward rates
|
||||
@ -587,7 +592,7 @@ CTLD.Config = {
|
||||
SmokeDuration = 120, -- 2 minutes
|
||||
SmokeColor = trigger.smokeColor.Orange,
|
||||
MaxActiveCrates = 40, -- hard cap on simultaneously spawned salvage crates per coalition
|
||||
AdaptiveIntervals = { idle = 10, low = 10, medium = 15, high = 20 },
|
||||
AdaptiveIntervals = { idle = 10, low = 20, medium = 25, high = 30 },
|
||||
|
||||
-- Spawn restrictions
|
||||
MinSpawnDistance = 25, -- meters from death location
|
||||
@ -1017,7 +1022,7 @@ CTLD.MEDEVAC = {
|
||||
CrewDefendSelf = true, -- crews will return fire if engaged
|
||||
|
||||
-- Crew protection during announcement delay
|
||||
CrewImmortalDuringDelay = false, -- make crew immortal (invulnerable) during announcement delay to prevent early death
|
||||
CrewImmortalDuringDelay = true, -- make crew immortal (invulnerable) during announcement delay to prevent early death
|
||||
CrewInvisibleDuringDelay = true, -- make crew invisible to AI during announcement delay (won't be targeted by enemy)
|
||||
CrewImmortalAfterAnnounce = false, -- if true, crew stays immortal even after announcing mission (easier gameplay)
|
||||
|
||||
@ -1846,8 +1851,167 @@ CTLD._salvageCrates = CTLD._salvageCrates or {} -- [crateName] = { side, weigh
|
||||
CTLD._salvageDropZones = CTLD._salvageDropZones or {} -- [zoneName] = { zone, side, active }
|
||||
CTLD._salvageStats = CTLD._salvageStats or { -- [coalition.side] = { spawned, delivered, expired, totalWeight, totalReward }
|
||||
[coalition.side.BLUE] = { spawned = 0, delivered = 0, expired = 0, totalWeight = 0, totalReward = 0 },
|
||||
[coalition.side.RED] = { spawned = 0, delivered = 0, expired = 0, totalWeight = 0, totalReward = 0 },
|
||||
[coalition.side.RED] = { spawned = 0, delivered = 0, expired = 0, totalWeight = 0, totalReward = 0 },
|
||||
}
|
||||
-- One-shot timer tracking for cleanup
|
||||
CTLD._pendingTimers = CTLD._pendingTimers or {} -- [timerId] = true
|
||||
|
||||
local function _distanceXZ(a, b)
|
||||
if not a or not b then return math.huge end
|
||||
local dx = (a.x or 0) - (b.x or 0)
|
||||
local dz = (a.z or 0) - (b.z or 0)
|
||||
return math.sqrt(dx * dx + dz * dz)
|
||||
end
|
||||
|
||||
local function _buildSphereVolume(point, radius)
|
||||
local px = (point and point.x) or 0
|
||||
local pz = (point and point.z) or 0
|
||||
local py = (point and (point.y or point.alt))
|
||||
if py == nil and land and land.getHeight then
|
||||
local ok, h = pcall(land.getHeight, { x = px, y = pz })
|
||||
if ok and type(h) == 'number' then py = h end
|
||||
end
|
||||
py = py or 0
|
||||
local volId = (world and world.VolumeType and world.VolumeType.SPHERE) or 0
|
||||
return {
|
||||
id = volId,
|
||||
params = {
|
||||
point = { x = px, y = py, z = pz },
|
||||
radius = radius or 0,
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
-- Check if a crate is being sling-loaded by scanning for nearby helicopters
|
||||
-- Static objects don't have inAir() method, so we check if any unit is carrying it
|
||||
local function _isCrateHooked(crateObj)
|
||||
if not crateObj then return false end
|
||||
|
||||
-- For dynamic objects (vehicles), inAir() works
|
||||
if crateObj.inAir then
|
||||
local ok, result = pcall(function() return crateObj:inAir() end)
|
||||
if ok and result then return true end
|
||||
end
|
||||
|
||||
-- For static objects: check if the crate itself is elevated above ground
|
||||
-- This indicates it's actually being carried, not just near a helicopter
|
||||
local cratePos = crateObj:getPoint()
|
||||
if not cratePos then return false end
|
||||
|
||||
-- Get ground height at crate position
|
||||
local landHeight = land.getHeight({x = cratePos.x, y = cratePos.z})
|
||||
if not landHeight then landHeight = 0 end
|
||||
|
||||
-- If crate is more than 2 meters above ground, it's being carried
|
||||
-- (accounts for terrain variations and crate size)
|
||||
local heightAboveGround = cratePos.y - landHeight
|
||||
if heightAboveGround > 2 then
|
||||
return true
|
||||
end
|
||||
|
||||
return false
|
||||
end
|
||||
|
||||
local function _fmtTemplate(tpl, data)
|
||||
if not tpl or tpl == '' then return '' end
|
||||
-- Support placeholder keys with underscores (e.g., {zone_dist_u})
|
||||
return (tpl:gsub('{([%w_]+)}', function(k)
|
||||
local v = data and data[k]
|
||||
-- If value is missing, leave placeholder intact to aid debugging
|
||||
if v == nil then return '{'..k..'}' end
|
||||
return tostring(v)
|
||||
end))
|
||||
end
|
||||
|
||||
function CTLD:_FindNearestFriendlyTransport(position, side, radius)
|
||||
if not position or not side then return nil end
|
||||
radius = radius or 600
|
||||
local bestGroupName
|
||||
local bestDist = math.huge
|
||||
local sphere = _buildSphereVolume(position, radius)
|
||||
world.searchObjects(Object.Category.UNIT, sphere, function(obj)
|
||||
if not obj or (not obj.isExist or not obj:isExist()) then return true end
|
||||
if not obj.getCoalition or obj:getCoalition() ~= side then return true end
|
||||
local grp = obj.getGroup and obj:getGroup()
|
||||
if not grp then return true end
|
||||
local grpName = grp.getName and grp:getName()
|
||||
if not grpName then return true end
|
||||
local objPos = obj.getPoint and obj:getPoint()
|
||||
local dist = _distanceXZ(position, objPos)
|
||||
if dist < bestDist then
|
||||
bestDist = dist
|
||||
bestGroupName = grpName
|
||||
end
|
||||
return true
|
||||
end)
|
||||
if not bestGroupName then return nil end
|
||||
local mooseGrp = GROUP:FindByName(bestGroupName)
|
||||
return mooseGrp
|
||||
end
|
||||
|
||||
function CTLD:_SendSalvageHint(meta, messageKey, data, position, cooldown)
|
||||
if not meta or not messageKey then return end
|
||||
cooldown = cooldown or 10
|
||||
meta.hintCooldowns = meta.hintCooldowns or {}
|
||||
local hintKey = messageKey
|
||||
if data and data.zone then hintKey = hintKey .. ':' .. data.zone end
|
||||
local now = timer.getTime()
|
||||
local last = meta.hintCooldowns[hintKey] or 0
|
||||
if (now - last) < cooldown then return end
|
||||
meta.hintCooldowns[hintKey] = now
|
||||
|
||||
local template = self.Messages and self.Messages[messageKey]
|
||||
if not template then return end
|
||||
local text = _fmtTemplate(template, data or {})
|
||||
if not text or text == '' then return end
|
||||
|
||||
local recipient = self:_FindNearestFriendlyTransport(position, meta.side, 700)
|
||||
local recipientLabel
|
||||
if recipient then
|
||||
_msgGroup(recipient, text)
|
||||
recipientLabel = recipient.GetName and recipient:GetName() or 'nearest transport'
|
||||
else
|
||||
_msgCoalition(meta.side, text)
|
||||
recipientLabel = string.format('coalition-%s', meta.side == coalition.side.BLUE and 'BLUE' or 'RED')
|
||||
end
|
||||
|
||||
local zoneLabel = (data and data.zone) and (' zone '..tostring(data.zone)) or ''
|
||||
_logInfo(string.format('[SlingLoadSalvage] Hint %s -> %s for crate %s%s',
|
||||
messageKey,
|
||||
recipientLabel or 'unknown recipient',
|
||||
data and data.id or 'unknown',
|
||||
zoneLabel))
|
||||
end
|
||||
|
||||
function CTLD:_CheckCrateZoneHints(crateName, meta, cratePos)
|
||||
if not meta or not cratePos then return end
|
||||
local zoneSets = {
|
||||
{ list = self.PickupZones, active = self._ZoneActive and self._ZoneActive.Pickup, label = 'Pickup' },
|
||||
{ list = self.FOBZones, active = self._ZoneActive and self._ZoneActive.FOB, label = 'FOB' },
|
||||
{ list = self.DropZones, active = self._ZoneActive and self._ZoneActive.Drop, label = 'Drop' },
|
||||
}
|
||||
|
||||
for _, entry in ipairs(zoneSets) do
|
||||
local zones = entry.list or {}
|
||||
if #zones > 0 then
|
||||
for _, zone in ipairs(zones) do
|
||||
local zoneName = zone:GetName()
|
||||
local isActive = true
|
||||
if entry.active and zoneName then
|
||||
isActive = (entry.active[zoneName] ~= false)
|
||||
end
|
||||
if isActive and zone:IsVec3InZone(cratePos) then
|
||||
self:_SendSalvageHint(meta, 'slingload_salvage_wrong_zone', {
|
||||
id = crateName,
|
||||
zone = zoneName,
|
||||
zone_type = entry.label,
|
||||
}, cratePos, 15)
|
||||
return
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- #endregion State
|
||||
|
||||
@ -2282,12 +2446,12 @@ local function _jtacTargetScoreProfiled(unit, profile)
|
||||
return math.floor(base * mult + 0.5)
|
||||
end
|
||||
|
||||
local function _msgGroup(group, text, t)
|
||||
_msgGroup = function(group, text, t)
|
||||
if not group then return end
|
||||
MESSAGE:New(text, t or CTLD.Config.MessageDuration):ToGroup(group)
|
||||
end
|
||||
|
||||
local function _msgCoalition(side, text, t)
|
||||
_msgCoalition = function(side, text, t)
|
||||
MESSAGE:New(text, t or CTLD.Config.MessageDuration):ToCoalition(side)
|
||||
end
|
||||
|
||||
@ -2308,7 +2472,7 @@ local _logLevelLabels = {
|
||||
[LOG_DEBUG] = 'DEBUG',
|
||||
}
|
||||
|
||||
local function _log(level, msg)
|
||||
_log = function(level, msg)
|
||||
local logLevel = CTLD.Config and CTLD.Config.LogLevel or LOG_INFO
|
||||
if level > logLevel or level == LOG_NONE then return end
|
||||
local label = _logLevelLabels[level] or tostring(level)
|
||||
@ -2320,14 +2484,14 @@ local function _log(level, msg)
|
||||
end
|
||||
end
|
||||
|
||||
local function _logError(msg) _log(LOG_ERROR, msg) end
|
||||
local function _logInfo(msg) _log(LOG_INFO, msg) end
|
||||
_logError = function(msg) _log(LOG_ERROR, msg) end
|
||||
_logInfo = function(msg) _log(LOG_INFO, msg) end
|
||||
-- Treat VERBOSE as DEBUG-only to reduce noise unless LogLevel is 4
|
||||
local function _logVerbose(msg) _log(LOG_DEBUG, msg) end
|
||||
local function _logDebug(msg) _log(LOG_DEBUG, msg) end
|
||||
_logVerbose = function(msg) _log(LOG_DEBUG, msg) end
|
||||
_logDebug = function(msg) _log(LOG_DEBUG, msg) end
|
||||
|
||||
-- Emits tagged messages regardless of configured LogLevel (used by explicit debug toggles)
|
||||
local function _logImmediate(tag, msg)
|
||||
_logImmediate = function(tag, msg)
|
||||
local text = string.format('[Moose_CTLD][%s] %s', tag or 'DEBUG', tostring(msg))
|
||||
if env and env.info then
|
||||
env.info(text)
|
||||
@ -2944,6 +3108,35 @@ function CTLD:_cancelSchedule(key)
|
||||
end
|
||||
end
|
||||
|
||||
-- Track one-shot timers for cleanup
|
||||
local function _trackOneShotTimer(id)
|
||||
if id and CTLD._pendingTimers then
|
||||
CTLD._pendingTimers[id] = true
|
||||
end
|
||||
return id
|
||||
end
|
||||
|
||||
-- Clean up one-shot timers when they execute
|
||||
local function _wrapOneShotCallback(callback)
|
||||
return function(...)
|
||||
local result = callback(...)
|
||||
-- If callback returns a time, it's recurring - don't remove
|
||||
if not result or type(result) ~= 'number' then
|
||||
local trackedId = nil
|
||||
for id, _ in pairs(CTLD._pendingTimers or {}) do
|
||||
if id == callback then
|
||||
trackedId = id
|
||||
break
|
||||
end
|
||||
end
|
||||
if trackedId and CTLD._pendingTimers then
|
||||
CTLD._pendingTimers[trackedId] = nil
|
||||
end
|
||||
end
|
||||
return result
|
||||
end
|
||||
end
|
||||
|
||||
local function _removeMenuHandle(menu)
|
||||
if not menu or type(menu) ~= 'table' then return end
|
||||
|
||||
@ -3017,6 +3210,9 @@ local function _clearPerGroupCaches(groupName)
|
||||
if CTLD._msgState then
|
||||
CTLD._msgState['GRP:'..groupName] = nil
|
||||
end
|
||||
|
||||
-- Note: One-shot timers are now self-cleaning via wrapper, but we log for visibility
|
||||
_logVerbose(string.format('[CTLD] Cleared caches for group %s', groupName))
|
||||
end
|
||||
|
||||
local function _clearPerUnitCachesForGroup(group)
|
||||
@ -3210,6 +3406,8 @@ function CTLD:_ensurePeriodicGC()
|
||||
if self._schedules and self._schedules.periodicGC then return end
|
||||
|
||||
local function gcTick()
|
||||
local now = timer.getTime()
|
||||
|
||||
-- Coach state: remove units that no longer exist
|
||||
if CTLD and CTLD._coachState then
|
||||
for uname, _ in pairs(CTLD._coachState) do
|
||||
@ -3231,7 +3429,6 @@ function CTLD:_ensurePeriodicGC()
|
||||
|
||||
-- Smoke meta: prune crates without points or exceeded duration
|
||||
if CTLD and CTLD._crates then
|
||||
local now = timer.getTime()
|
||||
for name, meta in pairs(CTLD._crates) do
|
||||
if meta and meta._smoke then
|
||||
local s = meta._smoke
|
||||
@ -3242,6 +3439,68 @@ function CTLD:_ensurePeriodicGC()
|
||||
end
|
||||
end
|
||||
|
||||
-- Enforce MEDEVAC crew limit (keep last 100 requests)
|
||||
if CTLD and CTLD._medevacCrews then
|
||||
local crewCount = 0
|
||||
local crewList = {}
|
||||
for crewName, crewData in pairs(CTLD._medevacCrews) do
|
||||
crewCount = crewCount + 1
|
||||
table.insert(crewList, { name = crewName, time = crewData.requestTime or 0 })
|
||||
end
|
||||
if crewCount > 100 then
|
||||
table.sort(crewList, function(a, b) return a.time < b.time end)
|
||||
local toRemove = crewCount - 100
|
||||
for i = 1, toRemove do
|
||||
local crewName = crewList[i].name
|
||||
local crewData = CTLD._medevacCrews[crewName]
|
||||
if crewData and crewData.markerID then
|
||||
pcall(function() trigger.action.removeMark(crewData.markerID) end)
|
||||
end
|
||||
CTLD._medevacCrews[crewName] = nil
|
||||
end
|
||||
env.info(string.format('[CTLD][GC] Pruned %d old MEDEVAC crew entries', toRemove))
|
||||
end
|
||||
end
|
||||
|
||||
-- Enforce salvage crate limit (keep last 50 active crates per side)
|
||||
if CTLD and CTLD._salvageCrates then
|
||||
local crateBySide = { [coalition.side.BLUE] = {}, [coalition.side.RED] = {} }
|
||||
for crateName, crateData in pairs(CTLD._salvageCrates) do
|
||||
if crateData.side then
|
||||
table.insert(crateBySide[crateData.side], { name = crateName, time = crateData.spawnTime or 0 })
|
||||
end
|
||||
end
|
||||
for side, crates in pairs(crateBySide) do
|
||||
if #crates > 50 then
|
||||
table.sort(crates, function(a, b) return a.time < b.time end)
|
||||
local toRemove = #crates - 50
|
||||
for i = 1, toRemove do
|
||||
local crateName = crates[i].name
|
||||
local crateData = CTLD._salvageCrates[crateName]
|
||||
if crateData and crateData.staticObject and crateData.staticObject.destroy then
|
||||
pcall(function() crateData.staticObject:destroy() end)
|
||||
end
|
||||
CTLD._salvageCrates[crateName] = nil
|
||||
end
|
||||
local sideName = (side == coalition.side.BLUE and 'BLUE') or 'RED'
|
||||
env.info(string.format('[CTLD][GC] Pruned %d old %s salvage crates', toRemove, sideName))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Clean up stale pending timer references
|
||||
if CTLD and CTLD._pendingTimers then
|
||||
local timerCount = 0
|
||||
for _ in pairs(CTLD._pendingTimers) do
|
||||
timerCount = timerCount + 1
|
||||
end
|
||||
if timerCount > 200 then
|
||||
-- If we have too many pending timers, clear old ones (they may have fired already)
|
||||
env.info(string.format('[CTLD][GC] Clearing %d stale timer references', timerCount))
|
||||
CTLD._pendingTimers = {}
|
||||
end
|
||||
end
|
||||
|
||||
return timer.getTime() + 300 -- every 5 minutes
|
||||
end
|
||||
|
||||
@ -3255,6 +3514,7 @@ function CTLD:_ensureBackgroundTasks()
|
||||
self:_ensureGlobalSmokeTicker()
|
||||
self:_ensurePeriodicGC()
|
||||
self:_ensureAdaptiveBackgroundLoop()
|
||||
self:_ensureSlingLoadEventHandler()
|
||||
self:_startHourlyDiagnostics()
|
||||
end
|
||||
|
||||
@ -3381,6 +3641,12 @@ function CTLD:_ensureAdaptiveBackgroundLoop()
|
||||
self:_registerSchedule('backgroundLoop', id)
|
||||
end
|
||||
|
||||
function CTLD:_ensureSlingLoadEventHandler()
|
||||
-- No event handler needed - we use inAir() checks like original CTLD.lua
|
||||
if self._slingHandlerRegistered then return end
|
||||
self._slingHandlerRegistered = true
|
||||
end
|
||||
|
||||
function CTLD:_startHourlyDiagnostics()
|
||||
if not (timer and timer.scheduleFunction) then return end
|
||||
if self._schedules and self._schedules.hourlyDiagnostics then return end
|
||||
@ -3394,8 +3660,21 @@ function CTLD:_startHourlyDiagnostics()
|
||||
or (self.Side == coalition.side.RED and 'RED')
|
||||
or (self.Side == coalition.side.NEUTRAL and 'NEUTRAL')
|
||||
or tostring(self.Side)
|
||||
|
||||
-- Comprehensive memory usage stats
|
||||
local cratesCount = _countTableEntries(CTLD._crates)
|
||||
local medevacCrewsCount = _countTableEntries(CTLD._medevacCrews)
|
||||
local salvageCratesCount = _countTableEntries(CTLD._salvageCrates)
|
||||
local coachStateCount = _countTableEntries(CTLD._coachState)
|
||||
local hoverStateCount = _countTableEntries(CTLD._hoverState)
|
||||
local pendingTimersCount = _countTableEntries(CTLD._pendingTimers)
|
||||
local msgStateCount = _countTableEntries(CTLD._msgState)
|
||||
|
||||
env.info(string.format('[CTLD][SoakTest][%s] salvageZones=%d dynamicZones=%d menus=%d',
|
||||
sideLabel, salvageCount, dynamicCount, menuCount))
|
||||
env.info(string.format('[CTLD][Memory][%s] crates=%d medevacCrews=%d salvageCrates=%d coachState=%d hoverState=%d pendingTimers=%d msgState=%d',
|
||||
sideLabel, cratesCount, medevacCrewsCount, salvageCratesCount, coachStateCount, hoverStateCount, pendingTimersCount, msgStateCount))
|
||||
|
||||
return timer.getTime() + 3600
|
||||
end
|
||||
|
||||
@ -3955,17 +4234,6 @@ local function _fmtAGL(meters, isMetric)
|
||||
return _fmtDistance(meters, isMetric)
|
||||
end
|
||||
|
||||
local function _fmtTemplate(tpl, data)
|
||||
if not tpl or tpl == '' then return '' end
|
||||
-- Support placeholder keys with underscores (e.g., {zone_dist_u})
|
||||
return (tpl:gsub('{([%w_]+)}', function(k)
|
||||
local v = data and data[k]
|
||||
-- If value is missing, leave placeholder intact to aid debugging
|
||||
if v == nil then return '{'..k..'}' end
|
||||
return tostring(v)
|
||||
end))
|
||||
end
|
||||
|
||||
-- Coalition utility: return opposite side (BLUE<->RED); NEUTRAL returns RED by default
|
||||
local function _enemySide(side)
|
||||
if coalition and coalition.side then
|
||||
@ -4570,6 +4838,7 @@ function CTLD:InitZones()
|
||||
if mz then
|
||||
table.insert(self.SalvageDropZones, mz)
|
||||
local name = mz:GetName()
|
||||
if z and z.side == nil then z.side = self.Side end
|
||||
self._ZoneDefs.SalvageDropZones[name] = z
|
||||
if self._ZoneActive.SalvageDrop[name] == nil then self._ZoneActive.SalvageDrop[name] = (z.active ~= false) end
|
||||
end
|
||||
@ -6117,7 +6386,8 @@ function CTLD:_BuildOrRefreshInStockMenu(group, rootMenu)
|
||||
MENU_GROUP_COMMAND:New(group, label, inRoot, function()
|
||||
self:RequestCrateForGroup(group, it.key)
|
||||
-- After requesting, refresh to reflect the decremented stock
|
||||
timer.scheduleFunction(function() self:_BuildOrRefreshInStockMenu(group) end, {}, timer.getTime() + 0.1)
|
||||
local id = timer.scheduleFunction(function() self:_BuildOrRefreshInStockMenu(group) end, {}, timer.getTime() + 0.1)
|
||||
_trackOneShotTimer(id)
|
||||
end)
|
||||
end
|
||||
end
|
||||
@ -8148,12 +8418,13 @@ function CTLD:_scheduleLoadedCrateMenuRefresh(group)
|
||||
local state = self._loadedCrateMenus[gname]
|
||||
if not state or not state.parent then return end
|
||||
local ctld = self
|
||||
timer.scheduleFunction(function()
|
||||
local id = timer.scheduleFunction(function()
|
||||
local g = GROUP:FindByName(gname)
|
||||
if not g then return end
|
||||
ctld:_BuildOrRefreshLoadedCrateMenu(g, state.parent)
|
||||
return
|
||||
end, {}, timer.getTime() + 0.1)
|
||||
_trackOneShotTimer(id)
|
||||
end
|
||||
|
||||
function CTLD:DropLoadedCrates(group, howMany, crateKey)
|
||||
@ -9786,7 +10057,7 @@ function CTLD:InitMEDEVAC()
|
||||
if crewData.invulnerable and now < crewData.invulnerableUntil then
|
||||
_logVerbose(string.format('[MEDEVAC] Invulnerable crew member %s killed, respawning...', unitName))
|
||||
-- Respawn this crew member
|
||||
timer.scheduleFunction(function()
|
||||
local id = timer.scheduleFunction(function()
|
||||
local grp = Group.getByName(crewGroupName)
|
||||
if grp and grp:isExist() then
|
||||
local cfg = CTLD.MEDEVAC
|
||||
@ -9822,6 +10093,7 @@ function CTLD:InitMEDEVAC()
|
||||
_logVerbose(string.format('[MEDEVAC] Respawned invulnerable crew member %s', unitName))
|
||||
end
|
||||
end, nil, timer.getTime() + 1)
|
||||
_trackOneShotTimer(id)
|
||||
return -- Don't process as normal death
|
||||
end
|
||||
end
|
||||
@ -10128,7 +10400,7 @@ function CTLD:_SpawnMEDEVACCrew(eventData, catalogEntry)
|
||||
|
||||
_logVerbose(string.format('[MEDEVAC] Crew will spawn in %d seconds after battle clears', spawnDelay))
|
||||
|
||||
timer.scheduleFunction(function()
|
||||
local spawnTimerId = timer.scheduleFunction(function()
|
||||
-- Now spawn the crew after battle has cleared
|
||||
local crewGroupName = string.format('MEDEVAC_Crew_%s_%d', unitType, math.random(100000, 999999))
|
||||
local crewUnitType = catalogEntry.crewType or cfg.CrewUnitTypes[selfref.Side] or ((selfref.Side == coalition.side.BLUE) and 'Soldier M4' or 'Infantry AK')
|
||||
@ -10287,7 +10559,7 @@ function CTLD:_SpawnMEDEVACCrew(eventData, catalogEntry)
|
||||
local announceDelay = cfg.CrewAnnouncementDelay or 60
|
||||
_logVerbose(string.format('[MEDEVAC] Will announce mission in %d seconds if crew survives', announceDelay))
|
||||
|
||||
timer.scheduleFunction(function()
|
||||
local announceTimerId = timer.scheduleFunction(function()
|
||||
-- Check if crew still exists
|
||||
local g = Group.getByName(crewGroupName)
|
||||
if not g or not g:isExist() then
|
||||
@ -10359,8 +10631,10 @@ function CTLD:_SpawnMEDEVACCrew(eventData, catalogEntry)
|
||||
end
|
||||
|
||||
end, nil, timer.getTime() + announceDelay)
|
||||
_trackOneShotTimer(announceTimerId)
|
||||
|
||||
end, nil, timer.getTime() + spawnDelay)
|
||||
_trackOneShotTimer(spawnTimerId)
|
||||
|
||||
end
|
||||
|
||||
@ -11310,18 +11584,19 @@ function CTLD:_HandleMEDEVACPickup(rescueGroup, crewGroupName, crewData)
|
||||
if #loadingMsgs > 0 then
|
||||
local messageCount = math.floor(loadingDuration / loadingMsgInterval)
|
||||
for i = 1, messageCount do
|
||||
timer.scheduleFunction(function()
|
||||
local msgId = timer.scheduleFunction(function()
|
||||
local g = GROUP:FindByName(gname)
|
||||
if g and g:IsAlive() then
|
||||
local randomLoadingMsg = loadingMsgs[math.random(1, #loadingMsgs)]
|
||||
_msgGroup(g, randomLoadingMsg, loadingMsgInterval - 0.5)
|
||||
end
|
||||
end, nil, timer.getTime() + (i * loadingMsgInterval))
|
||||
_trackOneShotTimer(msgId)
|
||||
end
|
||||
end
|
||||
|
||||
-- Schedule final completion after loading duration
|
||||
timer.scheduleFunction(function()
|
||||
local completionId = timer.scheduleFunction(function()
|
||||
local g = GROUP:FindByName(gname)
|
||||
if g and g:IsAlive() then
|
||||
-- Show completion message
|
||||
@ -11343,9 +11618,10 @@ function CTLD:_HandleMEDEVACPickup(rescueGroup, crewGroupName, crewData)
|
||||
|
||||
-- Respawn vehicle if enabled
|
||||
if cfg.RespawnOnPickup then
|
||||
timer.scheduleFunction(function()
|
||||
local respawnId = timer.scheduleFunction(function()
|
||||
self:_RespawnMEDEVACVehicle(crewData)
|
||||
end, nil, timer.getTime() + 2) -- 2 second delay for realism
|
||||
_trackOneShotTimer(respawnId)
|
||||
end
|
||||
|
||||
-- Mark crew as picked up (for MASH delivery tracking)
|
||||
@ -11354,6 +11630,7 @@ function CTLD:_HandleMEDEVACPickup(rescueGroup, crewGroupName, crewData)
|
||||
|
||||
_logVerbose(string.format('[MEDEVAC] Crew %s picked up by %s', crewGroupName, gname))
|
||||
end, nil, timer.getTime() + loadingDuration)
|
||||
_trackOneShotTimer(completionId)
|
||||
end
|
||||
|
||||
-- Respawn the vehicle at original death location
|
||||
@ -11499,7 +11776,7 @@ function CTLD:_DeliverMEDEVACCrewToMASH(group, crewGroupName, crewData)
|
||||
CTLD._salvagePoints[self.Side] = (CTLD._salvagePoints[self.Side] or 0) + crewData.salvageValue
|
||||
|
||||
-- Message to coalition (shown after brief delay to let unload message be seen)
|
||||
timer.scheduleFunction(function()
|
||||
local msgId = timer.scheduleFunction(function()
|
||||
_msgCoalition(self.Side, _fmtTemplate(CTLD.Messages.medevac_crew_delivered_mash, {
|
||||
player = _playerNameFromGroup(group),
|
||||
vehicle = crewData.vehicleType,
|
||||
@ -11507,6 +11784,7 @@ function CTLD:_DeliverMEDEVACCrewToMASH(group, crewGroupName, crewData)
|
||||
total = CTLD._salvagePoints[self.Side]
|
||||
}), 15)
|
||||
end, nil, timer.getTime() + 3)
|
||||
_trackOneShotTimer(msgId)
|
||||
|
||||
-- Track statistics
|
||||
if CTLD.MEDEVAC and CTLD.MEDEVAC.Statistics and CTLD.MEDEVAC.Statistics.Enabled then
|
||||
@ -13077,32 +13355,28 @@ function CTLD:_CheckSlingLoadSalvageCrates()
|
||||
if zoneDef and zoneDef.side == meta.side and (self._ZoneActive.SalvageDrop[zoneName] ~= false) then
|
||||
-- cratePos is a DCS Vec3 table, so use the direct Vec3 helper to avoid GetVec2 calls
|
||||
if zone:IsVec3InZone(cratePos) then
|
||||
-- Treat zero/negative cargo weight as "resting on the ground" (DCS returns >0 only while hooked)
|
||||
local crateHooked = false
|
||||
local weightReading = nil
|
||||
if meta.staticObject.getCargoWeight then
|
||||
local ok, cargoWeight = pcall(function()
|
||||
return meta.staticObject:getCargoWeight()
|
||||
end)
|
||||
if ok then
|
||||
weightReading = cargoWeight or 0
|
||||
crateHooked = (weightReading > 0)
|
||||
else
|
||||
_logDebug(string.format('[SlingLoadSalvage] getCargoWeight failed for %s: %s', crateName, tostring(cargoWeight)))
|
||||
end
|
||||
end
|
||||
|
||||
-- Simple CTLD.lua style: just check if crate is in air
|
||||
local crateHooked = _isCrateHooked(meta.staticObject)
|
||||
|
||||
if not crateHooked then
|
||||
-- Crate is on the ground in the zone - deliver it!
|
||||
_logInfo(string.format('[SlingLoadSalvage] Delivering %s', crateName))
|
||||
self:_DeliverSlingLoadSalvageCrate(crateName, meta, zoneName)
|
||||
table.insert(cratesToRemove, crateName)
|
||||
break
|
||||
else
|
||||
_logVerbose(string.format('[SlingLoadSalvage] Crate %s inside %s but still hooked (weight reading %.1f) – waiting for release',
|
||||
crateName, zoneName, weightReading or -1))
|
||||
-- Crate is still hooked - send hint and wait for release
|
||||
self:_SendSalvageHint(meta, 'slingload_salvage_hooked_in_zone', {
|
||||
id = crateName,
|
||||
zone = zoneName,
|
||||
}, cratePos, 8)
|
||||
_logDebug(string.format('[SlingLoadSalvage] Crate %s still hooked, waiting for release', crateName))
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
-- Provide guidance if crate is lingering inside other zone types
|
||||
self:_CheckCrateZoneHints(crateName, meta, cratePos)
|
||||
end
|
||||
else
|
||||
-- Crate no longer exists (destroyed or removed)
|
||||
@ -13168,10 +13442,7 @@ function CTLD:_DeliverSlingLoadSalvageCrate(crateName, meta, zoneName)
|
||||
local nearbyUnits = {}
|
||||
|
||||
-- Search for units in the zone
|
||||
local sphere = {
|
||||
point = zonePos,
|
||||
radius = radius,
|
||||
}
|
||||
local sphere = _buildSphereVolume(zonePos, radius)
|
||||
|
||||
local foundUnits = {}
|
||||
world.searchObjects(Object.Category.UNIT, sphere, function(obj)
|
||||
|
||||
Binary file not shown.
Loading…
x
Reference in New Issue
Block a user