diff --git a/Moose_CTLD_Pure/MEDEVAC_Salvage_System_Guide.html b/Moose_CTLD_Pure/MEDEVAC_Salvage_System_Guide.html
new file mode 100644
index 0000000..4ace6f5
--- /dev/null
+++ b/Moose_CTLD_Pure/MEDEVAC_Salvage_System_Guide.html
@@ -0,0 +1,752 @@
+
+
+
+
+
๐ MOOSE CTLD: COMPLETE MEDEVAC & SALVAGE SYSTEM GUIDE ๐ฆ
+
Dynamic Battlefield Economy & Rescue Operations
+
+
+
OVERVIEW
+
The CTLD system features a comprehensive salvage economy with TWO distinct methods for earning salvage points: MEDEVAC crew rescue missions and Sling-Load salvage crate recovery. Both systems feed into a shared coalition salvage pool that can be used to build out-of-stock equipment.
+
+
+
๐ Complete Menu Structure
+
+
+
๐ SALVAGE METHOD #1: MEDEVAC RESCUE CREW RECOVERY
+
+
CONCEPT: Rescue stranded vehicle crews and deliver them to MASH zones
+
+
How It Works
+
+ - When friendly vehicles are destroyed, there's a % chance the crew survives
+ (Default: 50% survival chance, configurable per coalition)
+
+ - After a 5-minute delay (battle clearance), surviving crew spawns near the wreck with a MEDEVAC mission announcement
+
+ - Crew will:
+
+ - Pop smoke when rescue helicopter approaches (8km detection)
+ - Send colorful radio messages ("Follow the smoke!")
+ - Wait up to 1 hour before being declared KIA
+ - May include MANPADS soldier for self-defense (10% chance)
+
+
+
+ - Load crew via normal troop pickup (land near crew, auto-loads)
+
+ - Fly to any MASH (Mobile Army Surgical Hospital) zone
+
+ - Land in MASH zone and wait 15 seconds for automatic crew offload
+
+ - Salvage points awarded based on vehicle value!
+
+
+
Salvage Value Scale
+
Value is determined by the destroyed vehicle type (from catalog):
+
+
+ | Vehicle Type |
+ Salvage Points |
+ Examples |
+
+
+ | Light Vehicles |
+ 1-5 points |
+ Humvees, trucks, light armor |
+
+
+ | Medium Vehicles |
+ 5-15 points |
+ APCs, IFVs, light tanks |
+
+
+ | Heavy Vehicles |
+ 15-30 points |
+ MBTs, heavy armor |
+
+
+ | Special Assets |
+ 30-50 points |
+ SAMs, artillery, C2 vehicles |
+
+
+
+
+ Example: Rescue a T-90 crew = ~25 salvage points
+
+
+
MEDEVAC F10 Menu Commands
+
+ F10 > Operations > MEDEVAC > List Active Missions - Shows all pending rescues with locations
+ F10 > Operations > MEDEVAC > Vectors to Nearest Crew - Bearing/distance to closest MEDEVAC
+ F10 > Operations > MEDEVAC > Coalition Salvage Points - Check your coalition's total salvage balance
+
+
+
๐ฆ SALVAGE METHOD #2: SLING-LOAD RECOVERY EQUIPMENT SALVAGE
+
+
CONCEPT: Recover enemy equipment wreckage via DCS sling-load mechanics
+
+
How It Works
+
+ - When ENEMY ground units die, there's a 15% chance (configurable) to spawn a physical cargo crate near the wreck for YOUR coalition to collect
+
+ - Crate naming:
+
+ SALVAGE-B-XXXXXX (Blue coalition can collect, spawns from RED deaths)
+ SALVAGE-R-XXXXXX (Red coalition can collect, spawns from BLUE deaths)
+
+
+
+ - Orange smoke marks crate location for 2 minutes after spawn
+
+ - Crate has a random weight class (determines helicopter requirement & reward)
+
+ - Use standard DCS F6 RADIO MENU sling-load to hook the crate
+
+ - Fly to a Salvage Collection Zone (create via F10 > Field Tools menu)
+
+ - Land/drop crate inside the zone boundary
+
+ - Automatic detection awards salvage points based on weight + condition!
+
+ - Crate expires after 3 HOURS if not collected (warnings at 30min & 5min)
+
+
+
+ NOT hover-pickup! This is pure DCS sling-load mechanics using F6 RADIO MENU!
+
+
+
Weight Classes & Reward Matrix
+
Crate weight determines both helicopter requirements and base reward value:
+
+
+ | Weight Class |
+ Weight Range |
+ Helicopter |
+ Base Reward |
+ Spawn Chance |
+
+
+ | Light |
+ 1500-2500 kg |
+ UH-1H Huey, UH-60 |
+ 2 pts/500kg |
+ 50% |
+
+
+ | Medium |
+ 2501-5000 kg |
+ Mi-8 Hip, Ka-50 |
+ 3 pts/500kg |
+ 30% |
+
+
+ | Heavy |
+ 5001-8000 kg |
+ Large Helos |
+ 5 pts/500kg |
+ 15% |
+
+
+ | Super Heavy |
+ 8001-12000 kg |
+ CH-47 Chinook ONLY |
+ 8 pts/500kg |
+ 5% |
+
+
+
+
Reward Calculation Formula
+
+
Final Reward = (Weight รท 500) ร Base Multiplier ร Condition Bonus
+
+
Example Light Crate (2000kg):
+
+ - Base: (2000 รท 500) ร 2 = 8 points
+ - If Undamaged: 8 ร 1.5 = 12 points
+ - If Damaged: 8 ร 1.0 = 8 points
+ - If Heavy Damage: 8 ร 0.5 = 4 points
+
+
+
+
โ ๏ธ Condition-Based Multipliers - FLY CAREFULLY!
+
Crate health affects your reward! Damage reduces salvage value:
+
+
+ | Condition |
+ Health Range |
+ Multiplier |
+ Example (8pt base) |
+
+
+ | UNDAMAGED โ |
+ โฅ 90% health |
+ 1.5x |
+ 12 points (+50% BONUS!) |
+
+
+ | Damaged |
+ 50-89% health |
+ 1.0x |
+ 8 points (normal) |
+
+
+ | Heavy Damage โ |
+ < 50% health |
+ 0.5x |
+ 4 points (-50% penalty) |
+
+
+ | DESTROYED โ |
+ 0% health |
+ 0x |
+ 0 points (crate lost) |
+
+
+
+
+ Smooth flying = 50% bonus! Crash landing = 50% penalty!
+
+
+
Example Salvage Scenarios
+
+
+
Scenario A: Light Crate, Perfect Delivery
+
+ - Crate: 2000kg Light class
+ - Flown carefully, no damage
+ - Reward: (2000รท500) ร 2 ร 1.5 = 12 salvage points
+
+
+
+
+
Scenario B: Medium Crate, Rough Landing
+
+ - Crate: 4000kg Medium class
+ - Damaged during transport (60% health)
+ - Reward: (4000รท500) ร 3 ร 1.0 = 24 salvage points
+
+
+
+
+
Scenario C: Super Heavy Crate, Crashed
+
+ - Crate: 10,000kg Super Heavy (Chinook required!)
+ - Heavy damage (40% health remaining)
+ - Reward: (10000รท500) ร 8 ร 0.5 = 80 salvage points
+ - (Would be 160 if undamaged!)
+
+
+
+
+
Scenario D: Heavy Crate, Destroyed
+
+ - Crate: 7000kg Heavy class
+ - Crate destroyed in crash
+ - Reward: 0 points (crate removed from mission)
+
+
+
+
Sling-Load Salvage F10 Menu Commands
+
+ F10 > Field Tools > Salvage Collection Zones > Create Salvage Zone Here - Spawns a 300m collection zone at your position
+ F10 > Field Tools > Salvage Collection Zones > Show Active Salvage Zones - Lists all active salvage drop-off points
+ F10 > Navigation > Vectors to Nearest Salvage Crate - Bearing/distance/weight/value info
+
+
+
Spawn Restrictions
+
Salvage crates will NOT spawn:
+
+ - Within 1000m of active pickup zones (prevents clutter)
+ - Within 1km of airbases (avoids spawn on runways)
+ - 10-25 meters from wreck location (random placement)
+
+
+
Lifecycle & Warnings
+
+ - Spawn: Orange smoke + coalition announcement (grid, weight, estimated value)
+ - 30 Minutes Remaining: First warning message
+ - 5 Minutes Remaining: Urgent warning message
+ - Expiration: Crate removed + expiration message
+ - Total Lifetime: 3 HOURS (10,800 seconds, configurable)
+
+
+
๐ฐ Using Salvage Points
+
+
What Are Salvage Points?
+
Salvage points are a coalition-wide resource pool that allows you to build equipment that is normally out-of-stock at your current location.
+
+
When Salvage Is Used
+
Salvage auto-applies when:
+
+ - You request a crate that is OUT OF STOCK at nearest supply zone
+ - Coalition has enough salvage points to cover the cost
+ - Cost = item's required crate count (e.g., M1 Abrams = 3 crates = 3 salvage)
+
+
+
Salvage Balance
+
Check your coalition's salvage point balance:
+
F10 > Operations > MEDEVAC > Coalition Salvage Points
+
Or build/request menu will show salvage balance when out-of-stock
+
+
๐ฏ Strategic Considerations
+
+
+
+
MEDEVAC Advantages
+
+ - โ More consistent rewards (vehicle value-based)
+ - โ Easier execution (normal troop pickup + land at MASH)
+ - โ Lower skill requirement
+ - โ Supports role-play/immersion
+ - โ No condition penalties
+
+
+
+
Sling-Load Salvage Advantages
+
+ - โ Higher potential rewards (up to 160pts for perfect Chinook delivery!)
+ - โ More frequent opportunities (every enemy kill = 15% chance)
+ - โ Skill-based system (rewards good flying)
+ - โ Can be done solo or coordinated
+ - โ Creates dynamic battlefield scavenging gameplay
+
+
+
+
+
Combined Strategy
+
Smart coalitions will:
+
+ - Assign dedicated MEDEVAC pilots for steady income
+ - Have salvage scavengers follow the front line for crate collection
+ - Prioritize high-value targets for maximum salvage spawns
+ - Practice smooth sling-load flying for condition bonuses
+ - Coordinate Chinook pilots for Super Heavy crate recovery
+
+
+
๐ Quick Reference
+
+
+
+
MEDEVAC Quick Steps
+
+ - Listen for MEDEVAC announcement (friendly vehicle crew spawned)
+ F10 > Ops > MEDEVAC > Vectors to Nearest Crew
+ - Fly to location, follow smoke
+ - Land near crew (auto-loads like troops)
+ - Fly to MASH zone
+ - Land and wait 15 seconds
+ - Salvage awarded! Vehicle respawns shortly after.
+
+
+
+
+
Sling-Load Salvage Quick Steps
+
+ - Listen for salvage spawn announcement (enemy died โ crate spawned)
+ F10 > Navigation > Vectors to Nearest Salvage Crate
+ - Fly to location (orange smoke = crate)
+ - Use DCS F6 RADIO MENU > Sling Load > Hook Cargo
+ - Fly carefully to Salvage Collection Zone
+ - Land or drop crate inside zone
+ - Salvage awarded based on weight + condition!
+
+
+
+
+
Key Differences
+
+
+ | Feature |
+ MEDEVAC |
+ Sling-Load Salvage |
+
+
+ | Pickup Method |
+ Hover pickup OR land โ auto-loads troops |
+ DCS F6 menu sling-load ONLY (not CTLD hover pickup!) |
+
+
+ | Reward Type |
+ Fixed value per vehicle type |
+ Variable value based on weight + flying skill |
+
+
+ | Time Window |
+ 1-hour window before crew KIA |
+ 3-hour window before crate expires |
+
+
+ | Skill Level |
+ Easy to Medium |
+ Medium to Hard (condition bonuses) |
+
+
+
+
๐ง Troubleshooting
+
+
"I can't sling-load the salvage crate!"
+
+ - Use DCS F6 RADIO MENU, not F10 CTLD hover pickup
+ - Make sure helicopter supports sling-load (Huey, Hip, Chinook, etc.)
+ - Check crate weight vs. helicopter capacity
+
+
+
"Crate disappeared before I got there!"
+
+ - Crates expire after 3 hours
+ - Check
F10 > Navigation > Vectors for time remaining
+ - Warnings sent at 30min and 5min
+
+
+
"I didn't get full reward for my delivery!"
+
+ - Check crate health - damage reduces reward by up to 50%
+ - Fly smoothly, avoid crashes, gentle landings
+ - Undamaged crates give 50% BONUS!
+
+
+
"No MEDEVAC missions spawning!"
+
+ - Check crew survival chance settings (default 50%)
+ - Only friendly vehicle deaths spawn MEDEVAC
+ - 5-minute delay after death before crew spawns
+
+
+
"Where do I create Salvage Collection Zones?"
+
+ F10 > Field Tools > Salvage Collection Zones > Create Salvage Zone Here
+ - Zone spawns at your current position with 300m radius
+
+
+
โ๏ธ Configuration Notes
+
Mission makers can adjust:
+
+ - MEDEVAC crew survival chance (default 50% per coalition)
+ - Sling-load salvage spawn chance (default 15% per coalition)
+ - Crate lifetime (default 3 hours)
+ - Weight class probabilities and reward rates
+ - Condition multipliers
+ - MANPADS spawn chance with crews (default 10%)
+ - Spawn restrictions and distances
+
+
All settings are per-coalition and fully configurable via the CTLD config table.
+
+
+
+
+
\ No newline at end of file
diff --git a/Moose_CTLD_Pure/Moose_CTLD.lua b/Moose_CTLD_Pure/Moose_CTLD.lua
index 35d4356..8215918 100644
--- a/Moose_CTLD_Pure/Moose_CTLD.lua
+++ b/Moose_CTLD_Pure/Moose_CTLD.lua
@@ -128,6 +128,19 @@ CTLD.Messages = {
medevac_crew_warn_15min = "WARNING: {vehicle} crew at {grid} - rescue window expires in 15 minutes!",
medevac_crew_warn_5min = "URGENT: {vehicle} crew at {grid} - rescue window expires in 5 minutes!",
medevac_unload_hold = "MEDEVAC: Stay grounded in the MASH zone for {seconds} seconds to offload casualties.",
+
+ -- Sling-Load Salvage messages
+ slingload_salvage_spawned = "SALVAGE OPPORTUNITY: Enemy wreckage at {grid}. Weight: {weight}kg, Est. Value: {reward}pts. {time_remain} to collect.",
+ slingload_salvage_delivered = "{player} delivered {weight}kg salvage for {reward} points ({condition})! Coalition total: {total}",
+ slingload_salvage_expired = "SALVAGE LOST: Crate {id} at {grid} deteriorated.",
+ slingload_salvage_damaged = "CAUTION: Salvage crate damaged in transit. Value reduced to {reward}pts.",
+ slingload_salvage_vectors = "Nearest salvage crate {id}: bearing {brg}ยฐ, range {rng} {rng_u}. Weight: {weight}kg, Value: {reward}pts.",
+ slingload_salvage_no_crates = "No active salvage crates available.",
+ slingload_salvage_zone_created = "Salvage Collection Zone '{zone}' created at your position (radius: {radius}m).",
+ slingload_salvage_zone_activated = "Salvage Collection Zone '{zone}' is now ACTIVE.",
+ 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!",
medevac_unload_aborted = "MEDEVAC: Unload aborted - {reason}. Land and hold for {seconds} seconds.",
-- Mobile MASH messages
@@ -248,7 +261,7 @@ CTLD.Config = {
ForbidChecksActivePickupOnly = true, -- when true, restriction applies only to ACTIVE pickup zones; false blocks all configured pickup zones
-- Dynamic Drop Zone settings
- DropZoneRadius = 250, -- meters: radius used when creating a Drop Zone via the admin menu at player position
+ DropZoneRadius = 500, -- meters: radius used when creating a Drop Zone via the admin menu at player position
MinDropZoneDistanceFromPickup = 2000, -- meters: minimum distance from nearest Pickup Zone required to create a dynamic Drop Zone (0 to disable)
MinDropDistanceActivePickupOnly = true, -- when true, only ACTIVE pickup zones are considered for the minimum distance check
@@ -325,6 +338,7 @@ CTLD.Config = {
DrawDropZones = true, -- optionally draw Drop zones
DrawFOBZones = true, -- optionally draw FOB zones
DrawMASHZones = true, -- optionally draw MASH (medical) zones
+ DrawSalvageZones = true, -- optionally draw Salvage Collection zones
FontSize = 18, -- label text size
ReadOnly = true, -- prevent clients from removing the shapes
ForAll = false, -- if true, draw shapes to all (-1) instead of coalition only (useful for testing/briefing)
@@ -335,6 +349,7 @@ CTLD.Config = {
Drop = {0, 0, 0, 0.25}, -- black fill for Drop zones
FOB = {1, 1, 0, 0.15}, -- yellow fill for FOB zones
MASH = {1, 0.75, 0.8, 0.25}, -- pink fill for MASH zones
+ SalvageDrop = {1, 0, 1, 0.15}, -- magenta fill for Salvage zones
},
LineType = 1, -- default line type if per-kind is not set (0 None, 1 Solid, 2 Dashed, 3 Dotted, 4 DotDash, 5 LongDash, 6 TwoDash)
LineTypes = { -- override border style per zone kind
@@ -342,6 +357,7 @@ CTLD.Config = {
Drop = 2, -- dashed
FOB = 4, -- dot-dash
MASH = 1, -- solid
+ SalvageDrop = 2, -- dashed
},
-- Label placement tuning (simple):
-- Effective extra offset from the circle edge = r * LabelOffsetRatio + LabelOffsetFromEdge
@@ -354,6 +370,7 @@ CTLD.Config = {
Drop = 'Drop Zone',
FOB = 'FOB Zone',
MASH = 'MASH Zone',
+ SalvageDrop = 'Salvage Collection Zone',
}
},
@@ -382,6 +399,66 @@ CTLD.Config = {
DropZones = {}, -- Optional Drop/AO zones
FOBZones = {}, -- FOB zones (restrict FOB building to these if RestrictFOBToZones = true)
MASHZones = {}, -- Medical zones for MEDEVAC crew delivery (MASH = Mobile Army Surgical Hospital)
+ SalvageDropZones = {}, -- Salvage collection zones for sling-load salvage delivery
+ },
+
+ -- === Sling-Load Salvage System ===
+ -- Spawn salvageable crates when enemy units are destroyed; deliver to collection zones for rewards
+ SlingLoadSalvage = {
+ Enabled = true,
+
+ -- Spawn probability when enemy ground units die
+ SpawnChance = {
+ [coalition.side.BLUE] = 0.90, -- 90% chance when BLUE unit dies (RED can collect the salvage)
+ [coalition.side.RED] = 0.90, -- 90% chance when RED unit dies (BLUE can collect the salvage)
+ },
+
+ -- Weight classes with spawn probabilities and reward rates
+ WeightClasses = {
+ { name = 'Light', min = 1500, max = 2500, probability = 0.50, rewardPer500kg = 2 }, -- Huey-capable
+ { name = 'Medium', min = 2501, max = 5000, probability = 0.30, rewardPer500kg = 3 }, -- Hip/Mi-8
+ { name = 'Heavy', min = 5001, max = 8000, probability = 0.15, rewardPer500kg = 5 }, -- Large helos
+ { name = 'SuperHeavy', min = 8001, max = 12000, probability = 0.05, rewardPer500kg = 8 }, -- Chinook only
+ },
+
+ -- Condition-based reward multipliers (based on crate health when delivered)
+ ConditionMultipliers = {
+ Undamaged = 1.5, -- >= 90% health
+ Damaged = 1.0, -- 50-89% health
+ HeavyDamage = 0.5, -- < 50% health
+ },
+
+ CrateLifetime = 10800, -- 3 hours (seconds)
+ WarningTimes = { 1800, 300 }, -- Warn at 30min and 5min remaining
+
+ -- Visual indicators
+ SpawnSmoke = false,
+ SmokeDuration = 120, -- 2 minutes
+ SmokeColor = trigger.smokeColor.Orange,
+
+ -- Spawn restrictions
+ MinSpawnDistance = 25, -- meters from death location
+ MaxSpawnDistance = 45, -- meters from death location
+ NoSpawnNearPickupZones = true,
+ NoSpawnNearPickupZoneDistance = 1000, -- meters
+ NoSpawnNearAirbasesKm = 1,
+
+ DetectionInterval = 5, -- seconds between salvage zone checks
+
+ -- Cargo static types (DCS sling-loadable cargo)
+ CargoTypes = {
+ 'container_cargo',
+ 'ammo_cargo',
+ 'fueltank_cargo',
+ 'barrels_cargo',
+ },
+
+ -- Salvage Collection Zone defaults
+ DefaultZoneRadius = 300,
+ ZoneColors = {
+ border = {1, 0.5, 0, 0.85}, -- orange border
+ fill = {1, 0.5, 0, 0.15}, -- light orange fill
+ },
},
}
-- #endregion Config
@@ -1215,6 +1292,16 @@ CTLD.MEDEVAC = {
TrackByPlayer = false, -- if true, track per-player stats (not yet implemented)
},
}
+
+-- =========================
+-- Sling-Load Salvage Configuration (MOVED)
+-- =========================
+-- #region SlingLoadSalvage Config
+-- NOTE: SlingLoadSalvage configuration has been MOVED into CTLD.Config.SlingLoadSalvage
+-- so that it properly gets copied to each CTLD instance via DeepCopy/DeepMerge.
+-- The old CTLD.SlingLoadSalvage global definition here is removed to avoid confusion.
+-- See CTLD.Config.SlingLoadSalvage above for the actual configuration.
+-- #endregion SlingLoadSalvage Config
--===================================================================================================================================================
-- #endregion MEDEVAC Config
@@ -1257,6 +1344,14 @@ CTLD._medevacUnloadStates = CTLD._medevacUnloadStates or {} -- [groupName] = { s
CTLD._medevacLoadStates = CTLD._medevacLoadStates or {} -- [groupName] = { startTime, delay, crewGroupName, crewData, holdAnnounced, nextReminder }
CTLD._medevacEnrouteStates = CTLD._medevacEnrouteStates or {} -- [groupName] = { nextSend, lastIndex }
+-- Sling-Load Salvage state
+CTLD._salvageCrates = CTLD._salvageCrates or {} -- [crateName] = { side, weight, spawnTime, position, initialHealth, rewardValue, warningsSent, staticObject, crateClass }
+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 },
+}
+
-- #endregion State
-- =========================
@@ -2564,6 +2659,17 @@ function CTLD:DrawZonesOnMap()
end
end
end
+ if md.DrawSalvageZones then
+ for _,mz in ipairs(self.SalvageDropZones or {}) do
+ local name = mz:GetName()
+ if self._ZoneActive.SalvageDrop[name] ~= false then
+ opts.LabelPrefix = (md.LabelPrefixes and md.LabelPrefixes.SalvageDrop) or 'Salvage Zone'
+ opts.LineType = (md.LineTypes and md.LineTypes.SalvageDrop) or md.LineType or 1
+ opts.FillColor = (md.FillColors and md.FillColors.SalvageDrop) or self.Config.SlingLoadSalvage.ZoneColors.fill
+ self:_drawZoneCircleAndLabel('SalvageDrop', mz, opts)
+ end
+ end
+ end
end
-- Unit preference detection and unit-aware formatting
@@ -3032,6 +3138,7 @@ function CTLD:New(cfg)
pushFromZones('Drop', o.Config.Zones and o.Config.Zones.DropZones)
pushFromZones('FOB', o.Config.Zones and o.Config.Zones.FOBZones)
pushFromZones('MASH', o.Config.Zones and o.Config.Zones.MASHZones)
+ pushFromZones('SalvageDrop', o.Config.Zones and o.Config.Zones.SalvageDropZones)
o._BindingsMerged = merged
if o._BindingsMerged and #o._BindingsMerged > 0 then
@@ -3137,8 +3244,9 @@ function CTLD:InitZones()
self.DropZones = {}
self.FOBZones = {}
self.MASHZones = {}
- self._ZoneDefs = { PickupZones = {}, DropZones = {}, FOBZones = {}, MASHZones = {} }
- self._ZoneActive = { Pickup = {}, Drop = {}, FOB = {}, MASH = {} }
+ self.SalvageDropZones = {}
+ self._ZoneDefs = { PickupZones = {}, DropZones = {}, FOBZones = {}, MASHZones = {}, SalvageDropZones = {} }
+ self._ZoneActive = { Pickup = {}, Drop = {}, FOB = {}, MASH = {}, SalvageDrop = {} }
for _,z in ipairs(self.Config.Zones.PickupZones or {}) do
local mz = _findZone(z)
if mz then
@@ -3175,6 +3283,15 @@ function CTLD:InitZones()
if self._ZoneActive.MASH[name] == nil then self._ZoneActive.MASH[name] = (z.active ~= false) end
end
end
+ for _,z in ipairs(self.Config.Zones.SalvageDropZones or {}) do
+ local mz = _findZone(z)
+ if mz then
+ table.insert(self.SalvageDropZones, mz)
+ local name = mz:GetName()
+ self._ZoneDefs.SalvageDropZones[name] = z
+ if self._ZoneActive.SalvageDrop[name] == nil then self._ZoneActive.SalvageDrop[name] = (z.active ~= false) end
+ end
+ end
end
-- Validate configured zone names exist in the mission; warn coalition if any are missing.
@@ -3207,9 +3324,9 @@ function CTLD:ValidateZones()
return s
end
- local missing = { Pickup = {}, Drop = {}, FOB = {}, MASH = {} }
- local found = { Pickup = {}, Drop = {}, FOB = {}, MASH = {} }
- local coords = { Pickup = 0, Drop = 0, FOB = 0, MASH = 0 }
+ local missing = { Pickup = {}, Drop = {}, FOB = {}, MASH = {}, SalvageDrop = {} }
+ local found = { Pickup = {}, Drop = {}, FOB = {}, MASH = {}, SalvageDrop = {} }
+ local coords = { Pickup = 0, Drop = 0, FOB = 0, MASH = 0, SalvageDrop = 0 }
for _,z in ipairs(self.Config.Zones.PickupZones or {}) do
if z.name then
@@ -3239,6 +3356,13 @@ function CTLD:ValidateZones()
coords.MASH = coords.MASH + 1
end
end
+ for _,z in ipairs(self.Config.Zones.SalvageDropZones or {}) do
+ if z.name then
+ if zoneExistsByName(z.name) then table.insert(found.SalvageDrop, z.name) else table.insert(missing.SalvageDrop, z.name) end
+ elseif z.coord then
+ coords.SalvageDrop = coords.SalvageDrop + 1
+ end
+ end
-- Log a concise summary to dcs.log
local sideStr = sideToStr(self.Side)
@@ -3254,8 +3378,11 @@ function CTLD:ValidateZones()
_logVerbose(string.format('[ZoneValidation][%s] MASH : configured=%d (named=%d, coord=%d) found=%d missing=%d',
sideStr,
#(self.Config.Zones.MASHZones or {}), #found.MASH + #missing.MASH, coords.MASH, #found.MASH, #missing.MASH))
+ _logVerbose(string.format('[ZoneValidation][%s] Salvage: configured=%d (named=%d, coord=%d) found=%d missing=%d',
+ sideStr,
+ #(self.Config.Zones.SalvageDropZones or {}), #found.SalvageDrop + #missing.SalvageDrop, coords.SalvageDrop, #found.SalvageDrop, #missing.SalvageDrop))
- local anyMissing = (#missing.Pickup > 0) or (#missing.Drop > 0) or (#missing.FOB > 0) or (#missing.MASH > 0)
+ local anyMissing = (#missing.Pickup > 0) or (#missing.Drop > 0) or (#missing.FOB > 0) or (#missing.MASH > 0) or (#missing.SalvageDrop > 0)
if anyMissing then
if #missing.Pickup > 0 then
local msg = 'CTLD config warning: Missing Pickup Zones: '..join(missing.Pickup)
@@ -3273,6 +3400,10 @@ function CTLD:ValidateZones()
local msg = 'CTLD config warning: Missing MASH Zones: '..join(missing.MASH)
_msgCoalition(self.Side, msg); _logError('[ZoneValidation]['..sideStr..'] '..msg)
end
+ if #missing.SalvageDrop > 0 then
+ local msg = 'CTLD config warning: Missing Salvage Drop Zones: '..join(missing.SalvageDrop)
+ _msgCoalition(self.Side, msg); _logError('[ZoneValidation]['..sideStr..'] '..msg)
+ end
else
_logVerbose(string.format('[ZoneValidation][%s] All configured zone names resolved successfully.', sideStr))
end
@@ -3765,6 +3896,15 @@ function CTLD:BuildGroupMenus(group)
-- Field Tools
CMD('Create Drop Zone (AO)', toolsRoot, function() self:CreateDropZoneAtGroup(group) end)
+
+ -- Salvage Collection Zones submenu
+ if self.Config.SlingLoadSalvage and self.Config.SlingLoadSalvage.Enabled then
+ local salvageZoneRoot = MENU_GROUP:New(group, 'Salvage Collection Zones', toolsRoot)
+ CMD('Create Salvage Zone Here', salvageZoneRoot, function() self:CreateSalvageZoneAtGroup(group) end)
+ CMD('Show Active Salvage Zones', salvageZoneRoot, function() self:ShowActiveSalvageZones(group) end)
+ -- Dynamic per-zone management will be added by _rebuildSalvageZoneMenus
+ end
+
local smokeRoot = MENU_GROUP:New(group, 'Smoke My Location', toolsRoot)
local function smokeHere(color)
local unit = group:GetUnit(1)
@@ -3806,6 +3946,12 @@ function CTLD:BuildGroupMenus(group)
_msgGroup(group, 'No friendly crates found.')
end
end)
+
+ -- Sling-Load Salvage vectors
+ if self.Config.SlingLoadSalvage and self.Config.SlingLoadSalvage.Enabled then
+ CMD('Vectors to Nearest Salvage Crate', navRoot, function() self:ShowNearestSalvageCrate(group) end)
+ end
+
CMD('Vectors to Nearest Pickup Zone', navRoot, function()
local unit = group:GetUnit(1)
if not unit or not unit:IsAlive() then return end
@@ -7404,6 +7550,26 @@ function CTLD:InitMEDEVAC()
_logDebug(string.format('[MEDEVAC] OnEventDead: %s not found in catalog', unitType))
end
end
+
+ -- Sling-Load Salvage: Check if we should spawn a salvage crate for the OPPOSING coalition
+ if selfref.Config.SlingLoadSalvage and selfref.Config.SlingLoadSalvage.Enabled then
+ -- Get unit position
+ local unitPos = nil
+ if eventData.initiator and eventData.initiator.getPoint then
+ local success, point = pcall(function() return eventData.initiator:getPoint() end)
+ if success and point then
+ unitPos = point
+ end
+ end
+
+ if unitPos then
+ -- Determine enemy coalition (who can collect this salvage)
+ local enemySide = (selfref.Side == coalition.side.BLUE) and coalition.side.RED or coalition.side.BLUE
+ selfref:_SpawnSlingLoadSalvageCrate(unitPos, unitType, enemySide, eventData)
+ else
+ _logDebug('[SlingLoadSalvage] Could not get unit position for salvage spawn')
+ end
+ end
end
self.MEDEVACHandler = handler
@@ -7441,6 +7607,15 @@ function CTLD:InitMEDEVAC()
selfref:_CheckMEDEVACTimeouts()
end, {}, 30, 30)
+ -- Start sling-load salvage crate checker (runs every 5 seconds by default)
+ if self.Config.SlingLoadSalvage and self.Config.SlingLoadSalvage.Enabled then
+ local interval = self.Config.SlingLoadSalvage.DetectionInterval or 5
+ self.SalvageSched = SCHEDULER:New(nil, function()
+ selfref:_CheckSlingLoadSalvageCrates()
+ end, {}, interval, interval)
+ _logInfo('Sling-Load Salvage system initialized for coalition '..tostring(self.Side))
+ end
+
-- Initialize MASH zones from config
self:_InitMASHZones()
@@ -10189,6 +10364,14 @@ function CTLD:Cleanup()
-- Stop any MEDEVAC timeout checkers or other schedulers
-- (If you add schedulers in the future, stop them here)
+ if self.MEDEVACSched then
+ pcall(function() self.MEDEVACSched:Stop() end)
+ self.MEDEVACSched = nil
+ end
+ if self.SalvageSched then
+ pcall(function() self.SalvageSched:Stop() end)
+ self.SalvageSched = nil
+ end
-- Clear spatial grid
CTLD._spatialGrid = {}
@@ -10206,6 +10389,16 @@ function CTLD:Cleanup()
CTLD._buildConfirm = {}
CTLD._buildCooldown = {}
CTLD._jtacReservedCodes = { [coalition.side.BLUE] = {}, [coalition.side.RED] = {}, [coalition.side.NEUTRAL] = {} }
+
+ -- Clear salvage state
+ if CTLD._salvageCrates then
+ for crateName, meta in pairs(CTLD._salvageCrates) do
+ if meta.staticObject and meta.staticObject.destroy then
+ pcall(function() meta.staticObject:destroy() end)
+ end
+ end
+ CTLD._salvageCrates = {}
+ end
if self.JTACSched then
pcall(function() self.JTACSched:Stop() end)
self.JTACSched = nil
@@ -10245,6 +10438,499 @@ end
-- #endregion Public helpers
+-- =========================
+-- Sling-Load Salvage System
+-- =========================
+-- #region SlingLoadSalvage
+
+-- Spawn a salvage crate when an enemy ground unit dies
+function CTLD:_SpawnSlingLoadSalvageCrate(unitPos, unitTypeName, enemySide, eventData)
+ local cfg = self.Config.SlingLoadSalvage
+ if not cfg or not cfg.Enabled then return end
+
+ -- Check spawn chance for this coalition
+ local spawnChance = cfg.SpawnChance[enemySide] or 0.15
+ if math.random() > spawnChance then
+ _logVerbose(string.format('[SlingLoadSalvage] Spawn roll failed (%.2f chance)', spawnChance))
+ return
+ end
+
+ -- Check spawn restrictions
+ if cfg.NoSpawnNearPickupZones then
+ local minDist = cfg.NoSpawnNearPickupZoneDistance or 1000
+ for _, zone in ipairs(self.PickupZones or {}) do
+ local zoneName = zone:GetName()
+ if zoneName and (self._ZoneActive.Pickup[zoneName] ~= false) then
+ local zonePos = zone:GetPointVec3()
+ local dist = math.sqrt((unitPos.x - zonePos.x)^2 + (unitPos.z - zonePos.z)^2)
+ if dist < minDist then
+ _logVerbose('[SlingLoadSalvage] Too close to pickup zone, aborting spawn')
+ return
+ end
+ end
+ end
+ end
+
+ if cfg.NoSpawnNearAirbasesKm and cfg.NoSpawnNearAirbasesKm > 0 then
+ local airbases = coalition.getAirbases(enemySide)
+ if airbases then
+ local minDistKm = cfg.NoSpawnNearAirbasesKm * 1000
+ for _, ab in ipairs(airbases) do
+ local abPos = ab:getPoint()
+ local dist = math.sqrt((unitPos.x - abPos.x)^2 + (unitPos.z - abPos.z)^2)
+ if dist < minDistKm then
+ _logVerbose('[SlingLoadSalvage] Too close to airbase, aborting spawn')
+ return
+ end
+ end
+ end
+ end
+
+ -- Select weight class
+ local totalProb = 0
+ for _, wc in ipairs(cfg.WeightClasses) do
+ totalProb = totalProb + wc.probability
+ end
+ local roll = math.random() * totalProb
+ local cumulative = 0
+ local selectedClass = cfg.WeightClasses[1] -- fallback
+ for _, wc in ipairs(cfg.WeightClasses) do
+ cumulative = cumulative + wc.probability
+ if roll <= cumulative then
+ selectedClass = wc
+ break
+ end
+ end
+
+ local weight = math.random(selectedClass.min, selectedClass.max)
+ local rewardValue = math.floor((weight / 500) * selectedClass.rewardPer500kg)
+
+ -- Calculate spawn position
+ local minDist = cfg.MinSpawnDistance or 10
+ local maxDist = cfg.MaxSpawnDistance or 25
+ local distance = minDist + math.random() * (maxDist - minDist)
+ local angle = math.random() * 2 * math.pi
+ local spawnPos = {
+ x = unitPos.x + math.cos(angle) * distance,
+ z = unitPos.z + math.sin(angle) * distance
+ }
+
+ -- Get land height
+ local landHeight = land.getHeight({ x = spawnPos.x, y = spawnPos.z })
+
+ -- Select cargo type based on weight
+ local cargoType
+ if weight < 1500 then
+ -- Light: barrels or ammo pallets
+ local lightTypes = { 'barrels_cargo', 'ammo_cargo' }
+ cargoType = lightTypes[math.random(1, #lightTypes)]
+ elseif weight < 2500 then
+ -- Medium: fuel tanks or containers
+ local mediumTypes = { 'fueltank_cargo', 'container_cargo', 'ammo_cargo' }
+ cargoType = mediumTypes[math.random(1, #mediumTypes)]
+ else
+ -- Heavy: large containers only
+ cargoType = 'container_cargo'
+ end
+
+ -- Create unique crate name
+ local sidePrefix = (enemySide == coalition.side.BLUE) and 'R' or 'B'
+ local crateName = string.format('SALVAGE-%s-%06d', sidePrefix, math.random(100000, 999999))
+
+ -- Spawn the static cargo
+ local countryId = self.CountryId
+ if eventData and eventData.initiator and eventData.initiator.getCountry then
+ local success, result = pcall(function() return eventData.initiator:getCountry() end)
+ if success and result then
+ countryId = result
+ end
+ end
+
+ local staticData = {
+ ['type'] = cargoType,
+ ['name'] = crateName,
+ ['x'] = spawnPos.x,
+ ['y'] = spawnPos.z,
+ ['heading'] = math.random() * 2 * math.pi,
+ ['canCargo'] = true,
+ ['mass'] = weight,
+ }
+
+ local success, staticObj = pcall(function()
+ return coalition.addStaticObject(countryId, staticData)
+ end)
+
+ if not success or not staticObj then
+ _logError('[SlingLoadSalvage] Failed to spawn salvage crate: ' .. tostring(staticObj))
+ return
+ end
+
+ -- Store crate metadata
+ CTLD._salvageCrates[crateName] = {
+ side = enemySide,
+ weight = weight,
+ spawnTime = timer.getTime(),
+ position = spawnPos,
+ initialHealth = 1.0,
+ rewardValue = rewardValue,
+ warningsSent = {},
+ staticObject = staticObj,
+ crateClass = selectedClass.name,
+ }
+
+ -- Update stats
+ if not CTLD._salvageStats[enemySide] then
+ CTLD._salvageStats[enemySide] = { spawned = 0, delivered = 0, expired = 0, totalWeight = 0, totalReward = 0 }
+ end
+ CTLD._salvageStats[enemySide].spawned = CTLD._salvageStats[enemySide].spawned + 1
+
+ -- Spawn smoke if enabled
+ if cfg.SpawnSmoke then
+ local smokePos = { x = spawnPos.x, y = landHeight, z = spawnPos.z }
+ trigger.action.smoke(smokePos, cfg.SmokeColor or trigger.smokeColor.Orange)
+ end
+
+ -- Calculate expiration time
+ local lifetime = cfg.CrateLifetime or 10800
+ local timeRemainMin = math.floor(lifetime / 60)
+ local grid = self:_GetMGRSString(spawnPos)
+
+ -- Announce to coalition
+ local msg = _fmtTemplate(self.Messages.slingload_salvage_spawned, {
+ grid = grid,
+ weight = weight,
+ reward = rewardValue,
+ time_remain = timeRemainMin,
+ })
+ _msgCoalition(enemySide, msg)
+
+ _logInfo(string.format('[SlingLoadSalvage] Spawned %s: weight=%dkg, reward=%dpts at %s',
+ crateName, weight, rewardValue, grid))
+end
+
+-- Check salvage crates for delivery and cleanup
+function CTLD:_CheckSlingLoadSalvageCrates()
+ local cfg = self.Config.SlingLoadSalvage
+ if not cfg or not cfg.Enabled then return end
+
+ local now = timer.getTime()
+ local cratesToRemove = {}
+
+ for crateName, meta in pairs(CTLD._salvageCrates) do
+ if meta.side == self.Side then
+ local elapsed = now - meta.spawnTime
+ local lifetime = cfg.CrateLifetime or 10800
+
+ -- Check for expiration
+ if elapsed >= lifetime then
+ table.insert(cratesToRemove, crateName)
+
+ -- Update stats
+ CTLD._salvageStats[meta.side].expired = CTLD._salvageStats[meta.side].expired + 1
+
+ -- Announce expiration
+ local grid = self:_GetMGRSString(meta.position)
+ local msg = _fmtTemplate(self.Messages.slingload_salvage_expired, {
+ id = crateName,
+ grid = grid,
+ })
+ _msgCoalition(meta.side, msg)
+
+ -- Remove the static object
+ if meta.staticObject and meta.staticObject.destroy then
+ pcall(function() meta.staticObject:destroy() end)
+ end
+
+ _logVerbose(string.format('[SlingLoadSalvage] Crate %s expired', crateName))
+
+ else
+ -- Check for warnings
+ local remaining = lifetime - elapsed
+ for _, warnTime in ipairs(cfg.WarningTimes or { 1800, 300 }) do
+ if remaining <= warnTime and not meta.warningsSent[warnTime] then
+ meta.warningsSent[warnTime] = true
+ local grid = self:_GetMGRSString(meta.position)
+ local msgKey = (warnTime >= 1800) and 'slingload_salvage_warn_30min' or 'slingload_salvage_warn_5min'
+ local msg = _fmtTemplate(self.Messages[msgKey], {
+ id = crateName,
+ grid = grid,
+ weight = meta.weight,
+ })
+ _msgCoalition(meta.side, msg)
+ end
+ end
+
+ -- Check if crate is in a salvage zone
+ if meta.staticObject and meta.staticObject:isExist() then
+ local cratePos = meta.staticObject:getPoint()
+ if cratePos then
+ -- Check all salvage zones for this coalition
+ for _, zone in ipairs(self.SalvageDropZones or {}) do
+ local zoneName = zone:GetName()
+ local zoneDef = self._ZoneDefs.SalvageDropZones[zoneName]
+
+ if zoneDef and zoneDef.side == meta.side and (self._ZoneActive.SalvageDrop[zoneName] ~= false) then
+ if zone:IsPointVec3InZone(cratePos) then
+ -- Check if crate is sling-loaded (has a parent)
+ local isLoaded = false
+ if meta.staticObject.getCargoDisplayName then
+ -- Crate is NOT on the ground if it's being carried
+ -- We detect delivery when crate is IN zone AND on ground (not sling-loaded)
+ local cargoWeight = meta.staticObject:getCargoWeight()
+ if cargoWeight and cargoWeight > 0 then
+ -- Crate exists and is on ground in zone - DELIVER IT
+ self:_DeliverSlingLoadSalvageCrate(crateName, meta, zoneName)
+ table.insert(cratesToRemove, crateName)
+ break
+ end
+ else
+ -- Fallback: just check if in zone
+ self:_DeliverSlingLoadSalvageCrate(crateName, meta, zoneName)
+ table.insert(cratesToRemove, crateName)
+ break
+ end
+ end
+ end
+ end
+ end
+ else
+ -- Crate no longer exists (destroyed or removed)
+ table.insert(cratesToRemove, crateName)
+ _logVerbose(string.format('[SlingLoadSalvage] Crate %s no longer exists', crateName))
+ end
+ end
+ end
+ end
+
+ -- Remove processed crates
+ for _, crateName in ipairs(cratesToRemove) do
+ CTLD._salvageCrates[crateName] = nil
+ end
+end
+
+-- Deliver a salvage crate and award points
+function CTLD:_DeliverSlingLoadSalvageCrate(crateName, meta, zoneName)
+ local cfg = self.Config.SlingLoadSalvage
+
+ -- Check crate health for condition multiplier
+ local healthRatio = 1.0
+ if meta.staticObject and meta.staticObject.getLife then
+ local success, currentLife = pcall(function() return meta.staticObject:getLife() end)
+ if success and currentLife then
+ local success2, maxLife = pcall(function() return meta.staticObject:getLife0() end)
+ if success2 and maxLife and maxLife > 0 then
+ healthRatio = currentLife / maxLife
+ end
+ end
+ end
+
+ -- Determine condition multiplier
+ local conditionMult = cfg.ConditionMultipliers.Damaged or 1.0
+ local conditionLabel = "Damaged"
+ if healthRatio >= 0.9 then
+ conditionMult = cfg.ConditionMultipliers.Undamaged or 1.5
+ conditionLabel = "Undamaged"
+ elseif healthRatio < 0.5 then
+ conditionMult = cfg.ConditionMultipliers.HeavyDamage or 0.5
+ conditionLabel = "Heavy Damage"
+ end
+
+ -- Calculate final reward
+ local finalReward = math.floor(meta.rewardValue * conditionMult)
+
+ -- Award salvage points
+ CTLD._salvagePoints[meta.side] = (CTLD._salvagePoints[meta.side] or 0) + finalReward
+
+ -- Update stats
+ CTLD._salvageStats[meta.side].delivered = CTLD._salvageStats[meta.side].delivered + 1
+ CTLD._salvageStats[meta.side].totalWeight = CTLD._salvageStats[meta.side].totalWeight + meta.weight
+ CTLD._salvageStats[meta.side].totalReward = CTLD._salvageStats[meta.side].totalReward + finalReward
+
+ -- Find the player who delivered (nearest transport helo in zone)
+ local playerName = "Unknown Pilot"
+ local deliveryUnit = nil
+ for _, zone in ipairs(self.SalvageDropZones or {}) do
+ if zone:GetName() == zoneName then
+ -- Find nearby friendly helicopters
+ local zonePos = zone:GetPointVec3()
+ local radius = self:_getZoneRadius(zone) or 300
+ local nearbyUnits = {}
+
+ -- Search for units in the zone
+ local sphere = {
+ point = zonePos,
+ radius = radius,
+ }
+
+ local foundUnits = {}
+ world.searchObjects(Object.Category.UNIT, sphere, function(obj)
+ if obj and obj:isExist() and obj.getCoalition then
+ local objCoal = obj:getCoalition()
+ if objCoal == meta.side and obj.getGroup then
+ local grp = obj:getGroup()
+ if grp then
+ local grpName = grp:getName()
+ table.insert(foundUnits, { unit = obj, group = grp, groupName = grpName })
+ end
+ end
+ end
+ return true
+ end)
+
+ -- Find player name from group
+ if #foundUnits > 0 then
+ deliveryUnit = foundUnits[1].unit
+ local grpName = foundUnits[1].groupName
+ if grpName then
+ -- Try to extract player name from group
+ local mooseGrp = GROUP:FindByName(grpName)
+ if mooseGrp then
+ local unit1 = mooseGrp:GetUnit(1)
+ if unit1 then
+ local pName = unit1:GetPlayerName()
+ if pName and pName ~= '' then
+ playerName = pName
+ else
+ playerName = grpName
+ end
+ end
+ end
+ end
+ end
+ break
+ end
+ end
+
+ -- Announce delivery
+ local msg = _fmtTemplate(self.Messages.slingload_salvage_delivered, {
+ player = playerName,
+ weight = meta.weight,
+ reward = finalReward,
+ condition = conditionLabel,
+ total = CTLD._salvagePoints[meta.side],
+ })
+ _msgCoalition(meta.side, msg)
+
+ -- Remove the crate
+ if meta.staticObject and meta.staticObject.destroy then
+ pcall(function() meta.staticObject:destroy() end)
+ end
+
+ _logInfo(string.format('[SlingLoadSalvage] %s delivered %s: %dkg, %dpts (%s), total=%d',
+ playerName, crateName, meta.weight, finalReward, conditionLabel, CTLD._salvagePoints[meta.side]))
+end
+
+-- Menu: Create Salvage Zone at group position
+function CTLD:CreateSalvageZoneAtGroup(group)
+ local cfg = self.Config.SlingLoadSalvage
+ if not cfg or not cfg.Enabled then
+ _msgGroup(group, 'Sling-Load Salvage system is disabled.')
+ return
+ end
+
+ local unit = group:GetUnit(1)
+ if not unit or not unit:IsAlive() then return end
+
+ local pos = unit:GetPointVec3()
+ local coord = COORDINATE:NewFromVec3(pos)
+ local radius = cfg.DefaultZoneRadius or 300
+
+ -- Generate unique zone name
+ local zoneName = string.format('SalvageZone-%s-%d', (self.Side == coalition.side.BLUE and 'BLUE' or 'RED'),
+ math.random(1000, 9999))
+
+ -- Create MOOSE zone
+ local zone = ZONE_RADIUS:New(zoneName, coord:GetVec2(), radius)
+
+ -- Add to instance zones
+ table.insert(self.SalvageDropZones, zone)
+ self._ZoneDefs.SalvageDropZones[zoneName] = { name = zoneName, side = self.Side, active = true }
+ self._ZoneActive.SalvageDrop[zoneName] = true
+
+ -- Announce
+ local msg = _fmtTemplate(self.Messages.slingload_salvage_zone_created, {
+ zone = zoneName,
+ radius = radius,
+ })
+ _msgGroup(group, msg)
+
+ _logInfo(string.format('[SlingLoadSalvage] Created zone %s at %s', zoneName, coord:ToStringLLDMS()))
+end
+
+-- Menu: Show active salvage zones
+function CTLD:ShowActiveSalvageZones(group)
+ local cfg = self.Config.SlingLoadSalvage
+ if not cfg or not cfg.Enabled then return end
+
+ local activeZones = {}
+ for _, zone in ipairs(self.SalvageDropZones or {}) do
+ local zoneName = zone:GetName()
+ if self._ZoneActive.SalvageDrop[zoneName] ~= false then
+ local zoneDef = self._ZoneDefs.SalvageDropZones[zoneName]
+ if zoneDef and zoneDef.side == self.Side then
+ table.insert(activeZones, zoneName)
+ end
+ end
+ end
+
+ if #activeZones == 0 then
+ _msgGroup(group, 'No active Salvage Collection Zones configured.')
+ else
+ local msg = 'Active Salvage Collection Zones:\n' .. table.concat(activeZones, '\n')
+ _msgGroup(group, msg)
+ end
+end
+
+-- Menu: Show nearest salvage crate vectors
+function CTLD:ShowNearestSalvageCrate(group)
+ local cfg = self.Config.SlingLoadSalvage
+ if not cfg or not cfg.Enabled then return end
+
+ local unit = group:GetUnit(1)
+ if not unit or not unit:IsAlive() then return end
+
+ local pos = unit:GetPointVec3()
+ local here = { x = pos.x, z = pos.z }
+
+ local nearestName, nearestMeta, nearestDist = nil, nil, math.huge
+ for crateName, meta in pairs(CTLD._salvageCrates) do
+ if meta.side == self.Side then
+ local dx = meta.position.x - here.x
+ local dz = meta.position.z - here.z
+ local dist = math.sqrt(dx*dx + dz*dz)
+ if dist < nearestDist then
+ nearestDist = dist
+ nearestName = crateName
+ nearestMeta = meta
+ end
+ end
+ end
+
+ if not nearestName then
+ local msg = self.Messages.slingload_salvage_no_crates or 'No active salvage crates available.'
+ _msgGroup(group, msg)
+ return
+ end
+
+ local brg = _bearingDeg(here, nearestMeta.position)
+ local isMetric = _getPlayerIsMetric(unit)
+ local rng, rngU = _fmtRange(nearestDist, isMetric)
+
+ local msg = _fmtTemplate(self.Messages.slingload_salvage_vectors, {
+ id = nearestName,
+ brg = brg,
+ rng = rng,
+ rng_u = rngU,
+ weight = nearestMeta.weight,
+ reward = nearestMeta.rewardValue,
+ })
+ _msgGroup(group, msg)
+end
+
+-- #endregion SlingLoadSalvage
+
+-- #endregion Public helpers
+
-- =========================
-- Return factory
-- =========================
diff --git a/Moose_CTLD_Pure/Moose_CTLD_Init_DualCoalitions.lua b/Moose_CTLD_Pure/Moose_CTLD_Init_DualCoalitions.lua
index e267c13..0afdd12 100644
--- a/Moose_CTLD_Pure/Moose_CTLD_Init_DualCoalitions.lua
+++ b/Moose_CTLD_Pure/Moose_CTLD_Init_DualCoalitions.lua
@@ -36,6 +36,7 @@ local blueCfg = {
DropZones = { { name = 'BRAVO', flag = 9002, activeWhen = 0 } },
FOBZones = { { name = 'CHARLIE', flag = 9003, activeWhen = 0 } },
MASHZones = { { name = 'MASH Alpha', freq = '251.0 AM', radius = 500, flag = 9010, activeWhen = 0 } },
+ SalvageDropZones = { { name = 'S1', flag = 9020, radius = 500, activeWhen = 0 } },
},
BuildRequiresGroundCrates = true,
}
@@ -64,6 +65,7 @@ local redCfg = {
DropZones = { { name = 'ECHO', flag = 9102, activeWhen = 0 } },
FOBZones = { { name = 'FOXTROT', flag = 9103, activeWhen = 0 } },
MASHZones = { { name = 'MASH Bravo', freq = '252.0 AM', radius = 500, flag = 9111, activeWhen = 0 } },
+ SalvageDropZones = { { name = 'S2', flag = 9020, radius = 500, activeWhen = 0 } },
},
BuildRequiresGroundCrates = true,
}
diff --git a/Moose_CTLD_Pure/Moose_CTLD_Pure.miz b/Moose_CTLD_Pure/Moose_CTLD_Pure.miz
index 06dcf22..8b93d48 100644
Binary files a/Moose_CTLD_Pure/Moose_CTLD_Pure.miz and b/Moose_CTLD_Pure/Moose_CTLD_Pure.miz differ