Changed some menus around and added "Build All Here" feature to build all nearyb crates.. no delays.. all crates.

This commit is contained in:
iTracerFacer 2025-11-22 11:31:16 -06:00
parent 270f8d5a3c
commit 70becf7170
2 changed files with 232 additions and 89 deletions

View File

@ -18,7 +18,7 @@
-- #region Config -- #region Config
local CTLD = {} local CTLD = {}
CTLD.Version = '1.0.1' CTLD.Version = '1.0.2'
CTLD.__index = CTLD CTLD.__index = CTLD
CTLD._lastSalvageInterval = CTLD._lastSalvageInterval or 0 CTLD._lastSalvageInterval = CTLD._lastSalvageInterval or 0
CTLD._playerUnitPrefs = CTLD._playerUnitPrefs or {} CTLD._playerUnitPrefs = CTLD._playerUnitPrefs or {}
@ -122,6 +122,42 @@ CTLD.Messages = {
medevac_crew_delivered_mash = "{player} delivered {vehicle} crew to MASH. Earned {salvage} salvage points! Coalition total: {total}.", medevac_crew_delivered_mash = "{player} delivered {vehicle} crew to MASH. Earned {salvage} salvage points! Coalition total: {total}.",
medevac_crew_timeout = "MEDEVAC FAILED: {vehicle} crew at {grid} KIA - no rescue attempted. Vehicle lost.", medevac_crew_timeout = "MEDEVAC FAILED: {vehicle} crew at {grid} KIA - no rescue attempted. Vehicle lost.",
medevac_crew_killed = "MEDEVAC FAILED: {vehicle} crew killed in action. Vehicle lost.", medevac_crew_killed = "MEDEVAC FAILED: {vehicle} crew killed in action. Vehicle lost.",
medevac_crew_killed_lines = {
"Stranded Crew ({vehicle} @ {grid}): We're taking heavy fire—where's that bird— *static*",
"Stranded Crew ({vehicle} @ {grid}): Ahh— we're under fire— we can't h— *boom*",
"Stranded Crew ({vehicle} @ {grid}): They're right on top of us— we won't ho— *static*",
"Stranded Crew ({vehicle} @ {grid}): Rounds incoming— this is it boys— *explosion*",
"Stranded Crew ({vehicle} @ {grid}): If you hear this, we didn't make— *static*",
"Stranded Crew ({vehicle} @ {grid}): No more cover! We can't hold— *boom*",
"Stranded Crew ({vehicle} @ {grid}): This is it— tell them we tried— *static*",
"Stranded Crew ({vehicle} @ {grid}): They're all around us— oh God— *explosion*",
"Stranded Crew ({vehicle} @ {grid}): Taking direct hits— we're not walking out of— *static*",
"Stranded Crew ({vehicle} @ {grid}): Where's that ride— we're pinned— we can't— *boom*",
-- New Mo-blame lines
"Stranded Crew ({vehicle} @ {grid}): Mo said this LZ was 'low threat'—remind him how that turned out— *static*",
"Stranded Crew ({vehicle} @ {grid}): Copy, still no bird—did Mo forget to file the MEDEVAC again?— *boom*",
"Stranded Crew ({vehicle} @ {grid}): We only took this route because Mo said it was 'shortcut friendly'— *static*",
"Stranded Crew ({vehicle} @ {grid}): Tell Mo his wont be hot for long brief was a lie— *explosion*",
"Stranded Crew ({vehicle} @ {grid}): Command, if Mo planned this exfil, wed like a refund— *static*",
"Stranded Crew ({vehicle} @ {grid}): Mo promised wed be home for chow—guess were staying for artillery instead— *boom*",
"Stranded Crew ({vehicle} @ {grid}): Were still at {grid}—unless Mo optimized the coordinates again— *static*",
"Stranded Crew ({vehicle} @ {grid}): Whoever let Mo pick this landing zone owes us new helmets— *explosion*",
"Stranded Crew ({vehicle} @ {grid}): Be advised, Mos safe corridor is mostly explosions right now— *static*",
"Stranded Crew ({vehicle} @ {grid}): If the birds lost, check if Mo touched the map— *boom*",
"Stranded Crew ({vehicle} @ {grid}): This is what we get for trusting Mos weather call— *static*",
"Stranded Crew ({vehicle} @ {grid}): Mo swore enemy armor never comes this far—theyre waving at us— *explosion*",
"Stranded Crew ({vehicle} @ {grid}): Were painted from every angle—next time, maybe dont let Mo do the route card— *static*",
"Stranded Crew ({vehicle} @ {grid}): If Mo filed our grid as a coffee break, were gonna haunt him— *boom*",
"Stranded Crew ({vehicle} @ {grid}): Radio check—anyone but Mo on comms? Wed like a real pickup— *static*",
"Stranded Crew ({vehicle} @ {grid}): Mo said in and out, easy day—were on hour one of not easy— *explosion*",
"Stranded Crew ({vehicle} @ {grid}): Still no rotors—ask Mo if he scheduled this MEDEVAC for tomorrow— *static*",
"Stranded Crew ({vehicle} @ {grid}): If Mos running the flight schedule, tell him were fresh out of patience— *boom*",
"Stranded Crew ({vehicle} @ {grid}): Mo briefed minimal contact—were currently meeting the entire enemy battalion— *static*",
"Stranded Crew ({vehicle} @ {grid}): Rescue bird, if youre circling, thats Mos navigation, not ours— *explosion*",
"Stranded Crew ({vehicle} @ {grid}): We popped smoke twice—maybe Mo told them to ignore it— *static*",
"Stranded Crew ({vehicle} @ {grid}): If they ask how we got stuck here, just say Mo—theyll understand— *boom*",
"Stranded Crew ({vehicle} @ {grid}): Mo said whats the worst that could happen—tell him hes about to find out— *static*",
},
medevac_no_requests = "No active MEDEVAC requests.", medevac_no_requests = "No active MEDEVAC requests.",
medevac_vectors = "MEDEVAC: {vehicle} crew bearing {brg}°, range {rng} {rng_u}. Time remaining: {time_remain} mins.", medevac_vectors = "MEDEVAC: {vehicle} crew bearing {brg}°, range {rng} {rng_u}. Time remaining: {time_remain} mins.",
medevac_salvage_status = "Coalition Salvage Points: {points}. Use salvage to build out-of-stock items.", medevac_salvage_status = "Coalition Salvage Points: {points}. Use salvage to build out-of-stock items.",
@ -404,7 +440,7 @@ CTLD.Config = {
BuildConfirmEnabled = false, -- require a second confirmation within a short window before building BuildConfirmEnabled = false, -- require a second confirmation within a short window before building
BuildConfirmWindowSeconds = 30, -- seconds allowed between first and second "Build Here" press BuildConfirmWindowSeconds = 30, -- seconds allowed between first and second "Build Here" press
BuildCooldownEnabled = true, -- impose a cooldown before allowing another build by the same group BuildCooldownEnabled = true, -- impose a cooldown before allowing another build by the same group
BuildCooldownSeconds = 60, -- seconds of cooldown after a successful build per group BuildCooldownSeconds = 0, -- seconds of cooldown after a successful build per group
-- === Pickup & Drop Zone Rules === -- === Pickup & Drop Zone Rules ===
RequirePickupZoneForCrateRequest = true, -- enforce that crate requests must be near a Supply (Pickup) Zone RequirePickupZoneForCrateRequest = true, -- enforce that crate requests must be near a Supply (Pickup) Zone
@ -1025,6 +1061,7 @@ CTLD.MEDEVAC = {
CrewImmortalDuringDelay = true, -- 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) 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) CrewImmortalAfterAnnounce = false, -- if true, crew stays immortal even after announcing mission (easier gameplay)
KeepCrewInvisibleForLifetime = true, -- if true, keep crew invisible to AI for entire mission lifetime
-- Smoke signals -- Smoke signals
PopSmokeOnSpawn = true, -- crew pops smoke when they first spawn PopSmokeOnSpawn = true, -- crew pops smoke when they first spawn
@ -5531,7 +5568,14 @@ function CTLD:BuildGroupMenus(group)
end) end)
local buildRoot = MENU_GROUP:New(group, 'Build Menu', logRoot) local buildRoot = MENU_GROUP:New(group, 'Build Menu', logRoot)
CMD('Build Here', buildRoot, function() self:BuildAtGroup(group) end) local cd = tonumber(self.Config.BuildCooldownSeconds) or 0
local buildHereLabel
if cd <= 0 then
buildHereLabel = 'Build All Here'
else
buildHereLabel = string.format('Build Here (w/%ds throttle)', cd)
end
CMD(buildHereLabel, buildRoot, function() self:BuildAtGroup(group) end)
self:_BuildOrRefreshBuildAdvancedMenu(group, buildRoot) self:_BuildOrRefreshBuildAdvancedMenu(group, buildRoot)
MENU_GROUP_COMMAND:New(group, 'Refresh Buildable List', buildRoot, function() MENU_GROUP_COMMAND:New(group, 'Refresh Buildable List', buildRoot, function()
self:_BuildOrRefreshBuildAdvancedMenu(group, buildRoot) self:_BuildOrRefreshBuildAdvancedMenu(group, buildRoot)
@ -6473,13 +6517,22 @@ function CTLD:_BuildOrRefreshBuildAdvancedMenu(group, rootMenu)
for _,it in ipairs(items) do for _,it in ipairs(items) do
local label = (it.def and (it.def.menu or it.def.description)) or it.key local label = (it.def and (it.def.menu or it.def.description)) or it.key
local perItem = MENU_GROUP:New(group, label, dynRoot) local perItem = MENU_GROUP:New(group, label, dynRoot)
local cd = tonumber(self.Config.BuildCooldownSeconds) or 0
local holdTitle, attackTitle
if cd <= 0 then
holdTitle = 'Build All [Hold Position]'
attackTitle = string.format('Build All [Attack (%dm)]', (self.Config.AttackAI and self.Config.AttackAI.VehicleSearchRadius) or 5000)
else
holdTitle = string.format('Build (w/%ds throttle) [Hold Position]', cd)
attackTitle = string.format('Build (w/%ds throttle) [Attack (%dm)]', cd, (self.Config.AttackAI and self.Config.AttackAI.VehicleSearchRadius) or 5000)
end
-- Hold Position -- Hold Position
CMD('Build [Hold Position]', perItem, function() CMD(holdTitle, perItem, function()
self:BuildSpecificAtGroup(group, it.key, { behavior = 'defend' }) self:BuildSpecificAtGroup(group, it.key, { behavior = 'defend' })
end) end)
-- Attack variant (render even if canAttackMove=false; we message accordingly) -- Attack variant (render even if canAttackMove=false; we message accordingly)
local vr = (self.Config.AttackAI and self.Config.AttackAI.VehicleSearchRadius) or 5000 local vr = (self.Config.AttackAI and self.Config.AttackAI.VehicleSearchRadius) or 5000
CMD(string.format('Build [Attack (%dm)]', vr), perItem, function() CMD(attackTitle, perItem, function()
if it.def and it.def.canAttackMove == false then if it.def and it.def.canAttackMove == false then
MESSAGE:New('This unit is static or not suited to move; it will hold position.', 8):ToGroup(group) MESSAGE:New('This unit is static or not suited to move; it will hold position.', 8):ToGroup(group)
self:BuildSpecificAtGroup(group, it.key, { behavior = 'defend' }) self:BuildSpecificAtGroup(group, it.key, { behavior = 'defend' })
@ -6499,11 +6552,14 @@ function CTLD:BuildSpecificAtGroup(group, recipeKey, opts)
local now = timer.getTime() local now = timer.getTime()
local gname = group:GetName() local gname = group:GetName()
if self.Config.BuildCooldownEnabled then if self.Config.BuildCooldownEnabled then
local last = CTLD._buildCooldown[gname] local cd = tonumber(self.Config.BuildCooldownSeconds) or 0
if last and (now - last) < (self.Config.BuildCooldownSeconds or 60) then if cd > 0 then
local rem = math.max(0, math.ceil((self.Config.BuildCooldownSeconds or 60) - (now - last))) local last = CTLD._buildCooldown[gname]
_msgGroup(group, string.format('Build on cooldown. Try again in %ds.', rem)) if last and (now - last) < cd then
return local rem = math.max(0, math.ceil(cd - (now - last)))
_msgGroup(group, string.format('Build on cooldown. Try again in %ds.', rem))
return
end
end end
end end
if self.Config.BuildConfirmEnabled then if self.Config.BuildConfirmEnabled then
@ -6721,84 +6777,142 @@ function CTLD:BuildSpecificAtGroup(group, recipeKey, opts)
return return
end end
-- Verify counts and build -- Verify counts and build (supports multi-build when cooldown is zero)
if type(def.requires) == 'table' then if type(def.requires) == 'table' then
for reqKey,qty in pairs(def.requires) do if (counts[reqKey] or 0) < (qty or 0) then _eventSend(self, group, nil, 'build_insufficient_crates', { build = def.description or recipeKey }); return end end local cd = tonumber(self.Config.BuildCooldownSeconds) or 0
local gdata = def.build({ x = spawnAt.x, z = spawnAt.z }, hdgDeg, def.side or self.Side) local maxCopies = 1
_eventSend(self, group, nil, 'build_started', { build = def.description or recipeKey }) if cd <= 0 then
local g = _coalitionAddGroup(def.side or self.Side, def.category or Group.Category.GROUND, gdata, self.Config) maxCopies = math.huge
if not g then _eventSend(self, group, nil, 'build_failed', { reason = 'DCS group spawn error' }); return end for reqKey,qty in pairs(def.requires) do
if self.Config.JTAC and self.Config.JTAC.Verbose then if (qty or 0) > 0 then
_logInfo(string.format('JTAC pre: post-build (composite) key=%s group=%s', tostring(recipeKey), tostring(g:getName()))) local available = counts[reqKey] or 0
end local copiesForKey = math.floor(available / (qty or 1))
self:_maybeRegisterJTAC(recipeKey, def, g) if copiesForKey < maxCopies then maxCopies = copiesForKey end
for reqKey,qty in pairs(def.requires) do consumeCrates(reqKey, qty or 0) end end
_eventSend(self, nil, self.Side, 'build_success_coalition', { build = def.description or recipeKey, player = _playerNameFromGroup(group) })
if def.isFOB then pcall(function() self:_CreateFOBPickupZone({ x = spawnAt.x, z = spawnAt.z }, def, hdg) end) end
if def.isMobileMASH then
_logDebug(string.format('[MobileMASH] BuildSpecificAtGroup invoking _CreateMobileMASH for key %s at (%.1f, %.1f)', tostring(recipeKey), spawnAt.x or -1, spawnAt.z or -1))
local ok, err = pcall(function() self:_CreateMobileMASH(g, { x = spawnAt.x, z = spawnAt.z }, def) end)
if not ok then
_logError(string.format('[MobileMASH] _CreateMobileMASH invocation failed: %s', tostring(err)))
end end
end if maxCopies < 1 or maxCopies == math.huge then
-- behavior _eventSend(self, group, nil, 'build_insufficient_crates', { build = def.description or recipeKey })
local behavior = opts and opts.behavior or nil return
if behavior == 'attack' and (def.canAttackMove ~= false) and self.Config.AttackAI and self.Config.AttackAI.Enabled then
local t = self:_assignAttackBehavior(g:getName(), spawnAt, true)
local isMetric = _getPlayerIsMetric(group:GetUnit(1))
if t and t.kind == 'base' then
local brg = _bearingDeg(spawnAt, t.point)
local v, u = _fmtRange(t.dist or 0, isMetric)
_eventSend(self, nil, self.Side, 'attack_base_announce', { unit_name = g:getName(), player = _playerNameFromGroup(group), base_name = t.name, brg = brg, rng = v, rng_u = u })
elseif t and t.kind == 'enemy' then
local brg = _bearingDeg(spawnAt, t.point)
local v, u = _fmtRange(t.dist or 0, isMetric)
_eventSend(self, nil, self.Side, 'attack_enemy_announce', { unit_name = g:getName(), player = _playerNameFromGroup(group), enemy_type = t.etype or 'unit', brg = brg, rng = v, rng_u = u })
else
local v, u = _fmtRange((self.Config.AttackAI and self.Config.AttackAI.VehicleSearchRadius) or 5000, isMetric)
_eventSend(self, nil, self.Side, 'attack_no_targets', { unit_name = g:getName(), player = _playerNameFromGroup(group), rng = v, rng_u = u })
end end
elseif behavior == 'attack' and def.canAttackMove == false then else
MESSAGE:New('This unit is static or not suited to move; it will hold position.', 8):ToGroup(group) for reqKey,qty in pairs(def.requires) do
if (counts[reqKey] or 0) < (qty or 0) then
_eventSend(self, group, nil, 'build_insufficient_crates', { build = def.description or recipeKey })
return
end
end
maxCopies = 1
end end
if self.Config.BuildCooldownEnabled then CTLD._buildCooldown[gname] = now end
local built = 0
while built < maxCopies do
local gdata = def.build({ x = spawnAt.x, z = spawnAt.z }, hdgDeg, def.side or self.Side)
_eventSend(self, group, nil, 'build_started', { build = def.description or recipeKey })
local g = _coalitionAddGroup(def.side or self.Side, def.category or Group.Category.GROUND, gdata, self.Config)
if not g then
_eventSend(self, group, nil, 'build_failed', { reason = 'DCS group spawn error' })
break
end
if self.Config.JTAC and self.Config.JTAC.Verbose then
_logInfo(string.format('JTAC pre: post-build (composite) key=%s group=%s', tostring(recipeKey), tostring(g:getName())))
end
self:_maybeRegisterJTAC(recipeKey, def, g)
for reqKey,qty in pairs(def.requires) do
consumeCrates(reqKey, qty or 0)
counts[reqKey] = (counts[reqKey] or 0) - (qty or 0)
end
_eventSend(self, nil, self.Side, 'build_success_coalition', { build = def.description or recipeKey, player = _playerNameFromGroup(group) })
if def.isFOB then pcall(function() self:_CreateFOBPickupZone({ x = spawnAt.x, z = spawnAt.z }, def, hdg) end) end
if def.isMobileMASH then
_logDebug(string.format('[MobileMASH] BuildSpecificAtGroup invoking _CreateMobileMASH for key %s at (%.1f, %.1f)', tostring(recipeKey), spawnAt.x or -1, spawnAt.z or -1))
local ok, err = pcall(function() self:_CreateMobileMASH(g, { x = spawnAt.x, z = spawnAt.z }, def) end)
if not ok then
_logError(string.format('[MobileMASH] _CreateMobileMASH invocation failed: %s', tostring(err)))
end
end
-- behavior (applied for each built group)
local behavior = opts and opts.behavior or nil
if behavior == 'attack' and (def.canAttackMove ~= false) and self.Config.AttackAI and self.Config.AttackAI.Enabled then
local t = self:_assignAttackBehavior(g:getName(), spawnAt, true)
local isMetric = _getPlayerIsMetric(group:GetUnit(1))
if t and t.kind == 'base' then
local brg = _bearingDeg(spawnAt, t.point)
local v, u = _fmtRange(t.dist or 0, isMetric)
_eventSend(self, nil, self.Side, 'attack_base_announce', { unit_name = g:getName(), player = _playerNameFromGroup(group), base_name = t.name, brg = brg, rng = v, rng_u = u })
elseif t and t.kind == 'enemy' then
local brg = _bearingDeg(spawnAt, t.point)
local v, u = _fmtRange(t.dist or 0, isMetric)
_eventSend(self, nil, self.Side, 'attack_enemy_announce', { unit_name = g:getName(), player = _playerNameFromGroup(group), enemy_type = t.etype or 'unit', brg = brg, rng = v, rng_u = u })
else
local v, u = _fmtRange((self.Config.AttackAI and self.Config.AttackAI.VehicleSearchRadius) or 5000, isMetric)
_eventSend(self, nil, self.Side, 'attack_no_targets', { unit_name = g:getName(), player = _playerNameFromGroup(group), rng = v, rng_u = u })
end
elseif behavior == 'attack' and def.canAttackMove == false then
MESSAGE:New('This unit is static or not suited to move; it will hold position.', 8):ToGroup(group)
end
built = built + 1
if cd > 0 then break end
end
if self.Config.BuildCooldownEnabled and cd > 0 then CTLD._buildCooldown[gname] = now end
return return
else else
-- single-key -- single-key
local need = def.required or 1 local need = def.required or 1
if (counts[recipeKey] or 0) < need then _eventSend(self, group, nil, 'build_insufficient_crates', { build = def.description or recipeKey }); return end local cd = tonumber(self.Config.BuildCooldownSeconds) or 0
local gdata = def.build({ x = spawnAt.x, z = spawnAt.z }, hdgDeg, def.side or self.Side) local maxCopies = 1
_eventSend(self, group, nil, 'build_started', { build = def.description or recipeKey }) if cd <= 0 then
local g = _coalitionAddGroup(def.side or self.Side, def.category or Group.Category.GROUND, gdata, self.Config) local available = counts[recipeKey] or 0
if not g then _eventSend(self, group, nil, 'build_failed', { reason = 'DCS group spawn error' }); return end maxCopies = math.floor(available / need)
if self.Config.JTAC and self.Config.JTAC.Verbose then if maxCopies < 1 then
_logInfo(string.format('JTAC pre: post-build (single) key=%s group=%s', tostring(recipeKey), tostring(g:getName()))) _eventSend(self, group, nil, 'build_insufficient_crates', { build = def.description or recipeKey })
end return
self:_maybeRegisterJTAC(recipeKey, def, g)
consumeCrates(recipeKey, need)
_eventSend(self, nil, self.Side, 'build_success_coalition', { build = def.description or recipeKey, player = _playerNameFromGroup(group) })
-- behavior
local behavior = opts and opts.behavior or nil
if behavior == 'attack' and (def.canAttackMove ~= false) and self.Config.AttackAI and self.Config.AttackAI.Enabled then
local t = self:_assignAttackBehavior(g:getName(), spawnAt, true)
local isMetric = _getPlayerIsMetric(group:GetUnit(1))
if t and t.kind == 'base' then
local brg = _bearingDeg(spawnAt, t.point)
local v, u = _fmtRange(t.dist or 0, isMetric)
_eventSend(self, nil, self.Side, 'attack_base_announce', { unit_name = g:getName(), player = _playerNameFromGroup(group), base_name = t.name, brg = brg, rng = v, rng_u = u })
elseif t and t.kind == 'enemy' then
local brg = _bearingDeg(spawnAt, t.point)
local v, u = _fmtRange(t.dist or 0, isMetric)
_eventSend(self, nil, self.Side, 'attack_enemy_announce', { unit_name = g:getName(), player = _playerNameFromGroup(group), enemy_type = t.etype or 'unit', brg = brg, rng = v, rng_u = u })
else
local v, u = _fmtRange((self.Config.AttackAI and self.Config.AttackAI.VehicleSearchRadius) or 5000, isMetric)
_eventSend(self, nil, self.Side, 'attack_no_targets', { unit_name = g:getName(), player = _playerNameFromGroup(group), rng = v, rng_u = u })
end end
elseif behavior == 'attack' and def.canAttackMove == false then else
MESSAGE:New('This unit is static or not suited to move; it will hold position.', 8):ToGroup(group) if (counts[recipeKey] or 0) < need then _eventSend(self, group, nil, 'build_insufficient_crates', { build = def.description or recipeKey }); return end
maxCopies = 1
end end
if self.Config.BuildCooldownEnabled then CTLD._buildCooldown[gname] = now end
local built = 0
while built < maxCopies do
local gdata = def.build({ x = spawnAt.x, z = spawnAt.z }, hdgDeg, def.side or self.Side)
_eventSend(self, group, nil, 'build_started', { build = def.description or recipeKey })
local g = _coalitionAddGroup(def.side or self.Side, def.category or Group.Category.GROUND, gdata, self.Config)
if not g then _eventSend(self, group, nil, 'build_failed', { reason = 'DCS group spawn error' }); break end
if self.Config.JTAC and self.Config.JTAC.Verbose then
_logInfo(string.format('JTAC pre: post-build (single) key=%s group=%s', tostring(recipeKey), tostring(g:getName())))
end
self:_maybeRegisterJTAC(recipeKey, def, g)
consumeCrates(recipeKey, need)
counts[recipeKey] = (counts[recipeKey] or 0) - need
_eventSend(self, nil, self.Side, 'build_success_coalition', { build = def.description or recipeKey, player = _playerNameFromGroup(group) })
-- behavior
local behavior = opts and opts.behavior or nil
if behavior == 'attack' and (def.canAttackMove ~= false) and self.Config.AttackAI and self.Config.AttackAI.Enabled then
local t = self:_assignAttackBehavior(g:getName(), spawnAt, true)
local isMetric = _getPlayerIsMetric(group:GetUnit(1))
if t and t.kind == 'base' then
local brg = _bearingDeg(spawnAt, t.point)
local v, u = _fmtRange(t.dist or 0, isMetric)
_eventSend(self, nil, self.Side, 'attack_base_announce', { unit_name = g:getName(), player = _playerNameFromGroup(group), base_name = t.name, brg = brg, rng = v, rng_u = u })
elseif t and t.kind == 'enemy' then
local brg = _bearingDeg(spawnAt, t.point)
local v, u = _fmtRange(t.dist or 0, isMetric)
_eventSend(self, nil, self.Side, 'attack_enemy_announce', { unit_name = g:getName(), player = _playerNameFromGroup(group), enemy_type = t.etype or 'unit', brg = brg, rng = v, rng_u = u })
else
local v, u = _fmtRange((self.Config.AttackAI and self.Config.AttackAI.VehicleSearchRadius) or 5000, isMetric)
_eventSend(self, nil, self.Side, 'attack_no_targets', { unit_name = g:getName(), player = _playerNameFromGroup(group), rng = v, rng_u = u })
end
elseif behavior == 'attack' and def.canAttackMove == false then
MESSAGE:New('This unit is static or not suited to move; it will hold position.', 8):ToGroup(group)
end
built = built + 1
if cd > 0 then break end
end
if self.Config.BuildCooldownEnabled and cd > 0 then CTLD._buildCooldown[gname] = now end
return return
end end
end end
@ -10099,6 +10213,30 @@ function CTLD:InitMEDEVAC()
end end
end end
end end
-- Next check if this death wiped out an active MEDEVAC crew group
-- Be defensive: unit may be a Moose wrapper or raw DCS unit, and
-- not all objects will expose a "GetGroup" method.
local dcsGroup = (unit and unit.GetGroup and unit:GetGroup()) or (unit and unit.getGroup and unit:getGroup()) or nil
if dcsGroup then
local gName = dcsGroup:GetName()
if gName and CTLD._medevacCrews[gName] then
local anyAlive = false
local units = dcsGroup:GetUnits()
if units then
for _, u in ipairs(units) do
if u and u:IsAlive() then
anyAlive = true
break
end
end
end
if not anyAlive then
selfref:_RemoveMEDEVACCrew(gName, 'killed')
return
end
end
end
end end
-- Normal death processing for vehicle spawning MEDEVAC crews -- Normal death processing for vehicle spawning MEDEVAC crews
@ -10571,19 +10709,9 @@ function CTLD:_SpawnMEDEVACCrew(eventData, catalogEntry)
-- Crew survived! Now announce to players and make mission available -- Crew survived! Now announce to players and make mission available
_logVerbose(string.format('[MEDEVAC] Crew %s survived, announcing mission', crewGroupName)) _logVerbose(string.format('[MEDEVAC] Crew %s survived, announcing mission', crewGroupName))
-- Make crew visible again (remove invisibility) and optionally remove immortality -- Optionally remove immortality after announcement; visibility is controlled by KeepCrewInvisibleForLifetime
local crewController = g:getController() local crewController = g:getController()
if crewController then if crewController then
-- Always make crew visible when they announce
if cfg.CrewInvisibleDuringDelay then
local setVisible = {
id = 'SetInvisible',
params = { value = false }
}
Controller.setCommand(crewController, setVisible)
_logVerbose('[MEDEVAC] Crew is now visible to AI')
end
-- Remove immortality unless config says to keep it -- Remove immortality unless config says to keep it
if cfg.CrewImmortalDuringDelay and not cfg.CrewImmortalAfterAnnounce then if cfg.CrewImmortalDuringDelay and not cfg.CrewImmortalAfterAnnounce then
local setMortal = { local setMortal = {
@ -10819,6 +10947,21 @@ function CTLD:_RemoveMEDEVACCrew(crewGroupName, reason)
CTLD._medevacStats[self.Side].timedOut = (CTLD._medevacStats[self.Side].timedOut or 0) + 1 CTLD._medevacStats[self.Side].timedOut = (CTLD._medevacStats[self.Side].timedOut or 0) + 1
end end
elseif reason == 'killed' then elseif reason == 'killed' then
local grid = self:_GetMGRSString(data.position)
local lines = CTLD.Messages.medevac_crew_killed_lines
if lines and #lines > 0 then
local line = lines[math.random(1, #lines)]
_msgCoalition(self.Side, _fmtTemplate(line, {
vehicle = data.vehicleType,
grid = grid,
}), 15)
else
_msgCoalition(self.Side, _fmtTemplate(CTLD.Messages.medevac_crew_killed, {
vehicle = data.vehicleType,
grid = grid,
}), 15)
end
-- Track statistics -- Track statistics
if CTLD.MEDEVAC and CTLD.MEDEVAC.Statistics and CTLD.MEDEVAC.Statistics.Enabled then if CTLD.MEDEVAC and CTLD.MEDEVAC.Statistics and CTLD.MEDEVAC.Statistics.Enabled then
CTLD._medevacStats[self.Side].killed = (CTLD._medevacStats[self.Side].killed or 0) + 1 CTLD._medevacStats[self.Side].killed = (CTLD._medevacStats[self.Side].killed or 0) + 1

Binary file not shown.