added sling loads to the salvage system.

This commit is contained in:
iTracerFacer 2025-11-11 01:13:07 -06:00
parent 542a426028
commit 4a351a73dd
4 changed files with 1447 additions and 7 deletions

View File

@ -0,0 +1,752 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>MOOSE CTLD: MEDEVAC & Salvage System Guide</title>
<style>
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
line-height: 1.6;
color: #e0e0e0;
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
margin: 0;
padding: 20px;
}
.container {
max-width: 1200px;
margin: 0 auto;
background: rgba(26, 26, 46, 0.95);
padding: 30px;
border-radius: 10px;
box-shadow: 0 0 30px rgba(0, 0, 0, 0.5);
}
h1 {
color: #ff6b6b;
text-align: center;
font-size: 2.5em;
margin-bottom: 10px;
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.5);
border-bottom: 3px solid #ff6b6b;
padding-bottom: 15px;
}
h2 {
color: #4ecdc4;
font-size: 1.8em;
margin-top: 40px;
margin-bottom: 20px;
border-left: 5px solid #4ecdc4;
padding-left: 15px;
text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.5);
}
h3 {
color: #ffe66d;
font-size: 1.3em;
margin-top: 25px;
margin-bottom: 15px;
text-transform: uppercase;
letter-spacing: 1px;
}
.subtitle {
text-align: center;
color: #a8dadc;
font-size: 1.2em;
margin-bottom: 30px;
font-style: italic;
}
.overview-box {
background: rgba(78, 205, 196, 0.1);
border-left: 4px solid #4ecdc4;
padding: 20px;
margin: 20px 0;
border-radius: 5px;
}
.menu-tree {
background: #0f0f1e;
padding: 20px;
border-radius: 8px;
font-family: 'Courier New', monospace;
font-size: 0.9em;
line-height: 1.8;
overflow-x: auto;
border: 2px solid #4ecdc4;
margin: 20px 0;
}
.menu-tree .highlight {
color: #ff6b6b;
font-weight: bold;
}
table {
width: 100%;
border-collapse: collapse;
margin: 20px 0;
background: rgba(15, 15, 30, 0.8);
border-radius: 8px;
overflow: hidden;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.3);
}
th {
background: linear-gradient(135deg, #4ecdc4 0%, #3ab4aa 100%);
color: #0f0f1e;
padding: 15px;
text-align: left;
font-weight: bold;
text-transform: uppercase;
letter-spacing: 0.5px;
}
td {
padding: 12px 15px;
border-bottom: 1px solid rgba(78, 205, 196, 0.2);
}
tr:hover {
background: rgba(78, 205, 196, 0.1);
}
.step-list {
background: rgba(255, 107, 107, 0.1);
border-left: 4px solid #ff6b6b;
padding: 15px 15px 15px 40px;
margin: 15px 0;
border-radius: 5px;
}
.step-list li {
margin: 10px 0;
padding-left: 10px;
}
.pro-tip {
background: rgba(255, 230, 109, 0.15);
border: 2px solid #ffe66d;
padding: 15px;
margin: 20px 0;
border-radius: 8px;
position: relative;
}
.pro-tip::before {
content: "💡 PRO TIP";
color: #ffe66d;
font-weight: bold;
display: block;
margin-bottom: 10px;
font-size: 1.1em;
}
.warning-box {
background: rgba(255, 107, 107, 0.15);
border: 2px solid #ff6b6b;
padding: 15px;
margin: 20px 0;
border-radius: 8px;
}
.warning-box::before {
content: "⚠️ IMPORTANT";
color: #ff6b6b;
font-weight: bold;
display: block;
margin-bottom: 10px;
font-size: 1.1em;
}
.badge {
display: inline-block;
padding: 5px 12px;
background: #4ecdc4;
color: #0f0f1e;
border-radius: 20px;
font-size: 0.85em;
font-weight: bold;
margin-left: 10px;
}
.badge-salvage1 {
background: #ff6b6b;
}
.badge-salvage2 {
background: #ffe66d;
color: #0f0f1e;
}
code {
background: rgba(78, 205, 196, 0.2);
padding: 2px 8px;
border-radius: 4px;
font-family: 'Courier New', monospace;
color: #4ecdc4;
}
.scenario-box {
background: rgba(15, 15, 30, 0.8);
padding: 15px;
margin: 15px 0;
border-radius: 8px;
border-left: 4px solid #ffe66d;
}
.scenario-box h4 {
color: #ffe66d;
margin-top: 0;
font-size: 1.1em;
}
.quick-ref {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 20px;
margin: 20px 0;
}
.quick-ref-card {
background: rgba(15, 15, 30, 0.8);
padding: 20px;
border-radius: 8px;
border-top: 4px solid #4ecdc4;
}
.quick-ref-card h4 {
color: #4ecdc4;
margin-top: 0;
font-size: 1.2em;
}
.comparison {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
margin: 20px 0;
}
.comparison > div {
background: rgba(15, 15, 30, 0.8);
padding: 20px;
border-radius: 8px;
}
.comparison .medevac {
border-top: 4px solid #ff6b6b;
}
.comparison .slingload {
border-top: 4px solid #ffe66d;
}
ul, ol {
margin: 10px 0;
}
li {
margin: 8px 0;
}
.footer {
text-align: center;
margin-top: 50px;
padding-top: 20px;
border-top: 2px solid rgba(78, 205, 196, 0.3);
color: #a8dadc;
font-style: italic;
}
@media print {
body {
background: white;
color: black;
}
.container {
background: white;
box-shadow: none;
}
}
</style>
</head>
<body>
<div class="container">
<h1>🚁 MOOSE CTLD: COMPLETE MEDEVAC & SALVAGE SYSTEM GUIDE 📦</h1>
<p class="subtitle">Dynamic Battlefield Economy & Rescue Operations</p>
<div class="overview-box">
<h3>OVERVIEW</h3>
<p>The CTLD system features a comprehensive salvage economy with <strong>TWO distinct methods</strong> for earning salvage points: <strong>MEDEVAC crew rescue missions</strong> and <strong>Sling-Load salvage crate recovery</strong>. Both systems feed into a shared coalition salvage pool that can be used to build out-of-stock equipment.</p>
</div>
<h2>📋 Complete Menu Structure</h2>
<div class="menu-tree">
F10 > CTLD
├─── Operations
│ ├─── Troop Transport
│ │ ├─── Load Troops
│ │ ├─── Load Troops (Type)
│ │ │ ├─── [Assault Squad]
│ │ │ ├─── [MANPADS Team]
│ │ │ ├─── [AT Team]
│ │ │ └─── [Mortar Team]
│ │ ├─── Unload Troops
│ │ └─── Unload Troops (Attack Mode)
│ │
│ ├─── Build
│ │ ├─── Build Here
│ │ ├─── Build (Advanced)
│ │ │ ├─── [Category] > [Item]
│ │ │ └─── Build Here (Attack Mode)
│ │ └─── Build (Attack Mode)
│ │
│ └─── <span class="highlight">MEDEVAC ⭐</span>
│ ├─── List Active Missions
│ ├─── Vectors to Nearest Crew
│ ├─── Coalition Salvage Points
│ └─── Admin/Settings
│ └─── Clear All MEDEVAC Missions
├─── Logistics
│ ├─── Request Crate
│ │ └─── [Category] > [Item]
│ │
│ ├─── Recipe Info
│ │ └─── [Category] > [Item Details]
│ │
│ ├─── Crate Management
│ │ ├─── List Nearby Crates
│ │ ├─── Drop All Loaded Crates
│ │ ├─── Drop Single Crate
│ │ └─── Re-mark Crate (Smoke)
│ │
│ └─── Show Inventory at Nearest Zone
├─── Field Tools
│ ├─── Create Drop Zone (AO)
│ │
│ ├─── <span class="highlight">Salvage Collection Zones ⭐</span>
│ │ ├─── Create Salvage Zone Here
│ │ └─── Show Active Salvage Zones
│ │
│ └─── Smoke My Location
│ ├─── Green / Red / White / Orange / Blue
├─── Navigation
│ ├─── Request Vectors to Nearest Crate
│ ├─── <span class="highlight">Vectors to Nearest Salvage Crate ⭐</span>
│ ├─── Vectors to Nearest Pickup Zone
│ └─── Hover Coach (Enable/Disable)
└─── Admin/Help
├─── Player Guides
│ ├─── Quick Start Guide
│ ├─── Hover Pickup Tutorial
│ ├─── Build System Guide
│ └─── JTAC Operations
└─── Show CTLD Status
</div>
<h2>🚑 SALVAGE METHOD #1: MEDEVAC RESCUE <span class="badge badge-salvage1">CREW RECOVERY</span></h2>
<p><strong>CONCEPT:</strong> Rescue stranded vehicle crews and deliver them to MASH zones</p>
<h3>How It Works</h3>
<ol class="step-list">
<li>When friendly vehicles are destroyed, there's a <strong>% chance the crew survives</strong><br>
<em>(Default: 50% survival chance, configurable per coalition)</em></li>
<li>After a <strong>5-minute delay</strong> (battle clearance), surviving crew spawns near the wreck with a MEDEVAC mission announcement</li>
<li>Crew will:
<ul>
<li>Pop smoke when rescue helicopter approaches (8km detection)</li>
<li>Send colorful radio messages ("Follow the smoke!")</li>
<li>Wait up to <strong>1 hour</strong> before being declared KIA</li>
<li>May include MANPADS soldier for self-defense (10% chance)</li>
</ul>
</li>
<li>Load crew via normal troop pickup (land near crew, auto-loads)</li>
<li>Fly to any MASH (Mobile Army Surgical Hospital) zone</li>
<li>Land in MASH zone and wait <strong>15 seconds</strong> for automatic crew offload</li>
<li>Salvage points awarded based on vehicle value!</li>
</ol>
<h3>Salvage Value Scale</h3>
<p>Value is determined by the destroyed vehicle type (from catalog):</p>
<table>
<tr>
<th>Vehicle Type</th>
<th>Salvage Points</th>
<th>Examples</th>
</tr>
<tr>
<td>Light Vehicles</td>
<td>1-5 points</td>
<td>Humvees, trucks, light armor</td>
</tr>
<tr>
<td>Medium Vehicles</td>
<td>5-15 points</td>
<td>APCs, IFVs, light tanks</td>
</tr>
<tr>
<td>Heavy Vehicles</td>
<td>15-30 points</td>
<td>MBTs, heavy armor</td>
</tr>
<tr>
<td>Special Assets</td>
<td>30-50 points</td>
<td>SAMs, artillery, C2 vehicles</td>
</tr>
</table>
<div class="pro-tip">
<strong>Example:</strong> Rescue a T-90 crew = ~25 salvage points
</div>
<h3>MEDEVAC F10 Menu Commands</h3>
<ul>
<li><code>F10 > Operations > MEDEVAC > List Active Missions</code> - Shows all pending rescues with locations</li>
<li><code>F10 > Operations > MEDEVAC > Vectors to Nearest Crew</code> - Bearing/distance to closest MEDEVAC</li>
<li><code>F10 > Operations > MEDEVAC > Coalition Salvage Points</code> - Check your coalition's total salvage balance</li>
</ul>
<h2>📦 SALVAGE METHOD #2: SLING-LOAD RECOVERY <span class="badge badge-salvage2">EQUIPMENT SALVAGE</span></h2>
<p><strong>CONCEPT:</strong> Recover enemy equipment wreckage via DCS sling-load mechanics</p>
<h3>How It Works</h3>
<ol class="step-list">
<li>When <strong>ENEMY</strong> ground units die, there's a <strong>15% chance</strong> (configurable) to spawn a physical cargo crate near the wreck for <strong>YOUR coalition</strong> to collect</li>
<li>Crate naming:
<ul>
<li><code>SALVAGE-B-XXXXXX</code> (Blue coalition can collect, spawns from RED deaths)</li>
<li><code>SALVAGE-R-XXXXXX</code> (Red coalition can collect, spawns from BLUE deaths)</li>
</ul>
</li>
<li>Orange smoke marks crate location for 2 minutes after spawn</li>
<li>Crate has a random weight class (determines helicopter requirement & reward)</li>
<li>Use standard <strong>DCS F6 RADIO MENU</strong> sling-load to hook the crate</li>
<li>Fly to a Salvage Collection Zone (create via F10 > Field Tools menu)</li>
<li>Land/drop crate inside the zone boundary</li>
<li>Automatic detection awards salvage points based on weight + condition!</li>
<li>Crate expires after <strong>3 HOURS</strong> if not collected (warnings at 30min & 5min)</li>
</ol>
<div class="warning-box">
<strong>NOT hover-pickup!</strong> This is pure DCS sling-load mechanics using F6 RADIO MENU!
</div>
<h3>Weight Classes & Reward Matrix</h3>
<p>Crate weight determines both helicopter requirements and base reward value:</p>
<table>
<tr>
<th>Weight Class</th>
<th>Weight Range</th>
<th>Helicopter</th>
<th>Base Reward</th>
<th>Spawn Chance</th>
</tr>
<tr>
<td><strong>Light</strong></td>
<td>1500-2500 kg</td>
<td>UH-1H Huey, UH-60</td>
<td>2 pts/500kg</td>
<td>50%</td>
</tr>
<tr>
<td><strong>Medium</strong></td>
<td>2501-5000 kg</td>
<td>Mi-8 Hip, Ka-50</td>
<td>3 pts/500kg</td>
<td>30%</td>
</tr>
<tr>
<td><strong>Heavy</strong></td>
<td>5001-8000 kg</td>
<td>Large Helos</td>
<td>5 pts/500kg</td>
<td>15%</td>
</tr>
<tr>
<td><strong>Super Heavy</strong></td>
<td>8001-12000 kg</td>
<td>CH-47 Chinook ONLY</td>
<td>8 pts/500kg</td>
<td>5%</td>
</tr>
</table>
<h3>Reward Calculation Formula</h3>
<div class="pro-tip">
<strong>Final Reward = (Weight ÷ 500) × Base Multiplier × Condition Bonus</strong>
<br><br>
<strong>Example Light Crate (2000kg):</strong>
<ul style="margin-top: 10px;">
<li>Base: (2000 ÷ 500) × 2 = <strong>8 points</strong></li>
<li>If Undamaged: 8 × 1.5 = <strong>12 points</strong></li>
<li>If Damaged: 8 × 1.0 = <strong>8 points</strong></li>
<li>If Heavy Damage: 8 × 0.5 = <strong>4 points</strong></li>
</ul>
</div>
<h3>⚠️ Condition-Based Multipliers - FLY CAREFULLY!</h3>
<p>Crate health affects your reward! Damage reduces salvage value:</p>
<table>
<tr>
<th>Condition</th>
<th>Health Range</th>
<th>Multiplier</th>
<th>Example (8pt base)</th>
</tr>
<tr style="background: rgba(78, 205, 196, 0.15);">
<td><strong>UNDAMAGED ✓</strong></td>
<td>≥ 90% health</td>
<td><strong>1.5x</strong></td>
<td><strong>12 points (+50% BONUS!)</strong></td>
</tr>
<tr>
<td>Damaged</td>
<td>50-89% health</td>
<td>1.0x</td>
<td>8 points (normal)</td>
</tr>
<tr style="background: rgba(255, 107, 107, 0.1);">
<td><strong>Heavy Damage ⚠</strong></td>
<td>&lt; 50% health</td>
<td><strong>0.5x</strong></td>
<td><strong>4 points (-50% penalty)</strong></td>
</tr>
<tr style="background: rgba(255, 107, 107, 0.2);">
<td><strong>DESTROYED ✗</strong></td>
<td>0% health</td>
<td><strong>0x</strong></td>
<td><strong>0 points (crate lost)</strong></td>
</tr>
</table>
<div class="pro-tip">
Smooth flying = 50% bonus! Crash landing = 50% penalty!
</div>
<h3>Example Salvage Scenarios</h3>
<div class="scenario-box">
<h4>Scenario A: Light Crate, Perfect Delivery</h4>
<ul>
<li>Crate: 2000kg Light class</li>
<li>Flown carefully, no damage</li>
<li><strong>Reward: (2000÷500) × 2 × 1.5 = 12 salvage points</strong></li>
</ul>
</div>
<div class="scenario-box">
<h4>Scenario B: Medium Crate, Rough Landing</h4>
<ul>
<li>Crate: 4000kg Medium class</li>
<li>Damaged during transport (60% health)</li>
<li><strong>Reward: (4000÷500) × 3 × 1.0 = 24 salvage points</strong></li>
</ul>
</div>
<div class="scenario-box">
<h4>Scenario C: Super Heavy Crate, Crashed</h4>
<ul>
<li>Crate: 10,000kg Super Heavy (Chinook required!)</li>
<li>Heavy damage (40% health remaining)</li>
<li><strong>Reward: (10000÷500) × 8 × 0.5 = 80 salvage points</strong></li>
<li><em>(Would be 160 if undamaged!)</em></li>
</ul>
</div>
<div class="scenario-box">
<h4>Scenario D: Heavy Crate, Destroyed</h4>
<ul>
<li>Crate: 7000kg Heavy class</li>
<li>Crate destroyed in crash</li>
<li><strong>Reward: 0 points (crate removed from mission)</strong></li>
</ul>
</div>
<h3>Sling-Load Salvage F10 Menu Commands</h3>
<ul>
<li><code>F10 > Field Tools > Salvage Collection Zones > Create Salvage Zone Here</code> - Spawns a 300m collection zone at your position</li>
<li><code>F10 > Field Tools > Salvage Collection Zones > Show Active Salvage Zones</code> - Lists all active salvage drop-off points</li>
<li><code>F10 > Navigation > Vectors to Nearest Salvage Crate</code> - Bearing/distance/weight/value info</li>
</ul>
<h3>Spawn Restrictions</h3>
<p>Salvage crates will <strong>NOT</strong> spawn:</p>
<ul>
<li>Within 1000m of active pickup zones (prevents clutter)</li>
<li>Within 1km of airbases (avoids spawn on runways)</li>
<li>10-25 meters from wreck location (random placement)</li>
</ul>
<h3>Lifecycle & Warnings</h3>
<ul>
<li><strong>Spawn:</strong> Orange smoke + coalition announcement (grid, weight, estimated value)</li>
<li><strong>30 Minutes Remaining:</strong> First warning message</li>
<li><strong>5 Minutes Remaining:</strong> Urgent warning message</li>
<li><strong>Expiration:</strong> Crate removed + expiration message</li>
<li><strong>Total Lifetime:</strong> 3 HOURS (10,800 seconds, configurable)</li>
</ul>
<h2>💰 Using Salvage Points</h2>
<h3>What Are Salvage Points?</h3>
<p>Salvage points are a <strong>coalition-wide resource pool</strong> that allows you to build equipment that is normally out-of-stock at your current location.</p>
<h3>When Salvage Is Used</h3>
<p>Salvage auto-applies when:</p>
<ol>
<li>You request a crate that is <strong>OUT OF STOCK</strong> at nearest supply zone</li>
<li>Coalition has enough salvage points to cover the cost</li>
<li>Cost = item's required crate count (e.g., M1 Abrams = 3 crates = 3 salvage)</li>
</ol>
<h3>Salvage Balance</h3>
<p>Check your coalition's salvage point balance:</p>
<p><code>F10 > Operations > MEDEVAC > Coalition Salvage Points</code></p>
<p><em>Or build/request menu will show salvage balance when out-of-stock</em></p>
<h2>🎯 Strategic Considerations</h2>
<div class="comparison">
<div class="medevac">
<h4 style="color: #ff6b6b;">MEDEVAC Advantages</h4>
<ul>
<li>✓ More consistent rewards (vehicle value-based)</li>
<li>✓ Easier execution (normal troop pickup + land at MASH)</li>
<li>✓ Lower skill requirement</li>
<li>✓ Supports role-play/immersion</li>
<li>✓ No condition penalties</li>
</ul>
</div>
<div class="slingload">
<h4 style="color: #ffe66d;">Sling-Load Salvage Advantages</h4>
<ul>
<li>✓ Higher potential rewards (up to 160pts for perfect Chinook delivery!)</li>
<li>✓ More frequent opportunities (every enemy kill = 15% chance)</li>
<li>✓ Skill-based system (rewards good flying)</li>
<li>✓ Can be done solo or coordinated</li>
<li>✓ Creates dynamic battlefield scavenging gameplay</li>
</ul>
</div>
</div>
<h3>Combined Strategy</h3>
<p>Smart coalitions will:</p>
<ul>
<li>Assign dedicated MEDEVAC pilots for steady income</li>
<li>Have salvage scavengers follow the front line for crate collection</li>
<li>Prioritize high-value targets for maximum salvage spawns</li>
<li>Practice smooth sling-load flying for condition bonuses</li>
<li>Coordinate Chinook pilots for Super Heavy crate recovery</li>
</ul>
<h2>🚀 Quick Reference</h2>
<div class="quick-ref">
<div class="quick-ref-card">
<h4>MEDEVAC Quick Steps</h4>
<ol>
<li>Listen for MEDEVAC announcement (friendly vehicle crew spawned)</li>
<li><code>F10 > Ops > MEDEVAC > Vectors to Nearest Crew</code></li>
<li>Fly to location, follow smoke</li>
<li>Land near crew (auto-loads like troops)</li>
<li>Fly to MASH zone</li>
<li>Land and wait 15 seconds</li>
<li>Salvage awarded! Vehicle respawns shortly after.</li>
</ol>
</div>
<div class="quick-ref-card">
<h4>Sling-Load Salvage Quick Steps</h4>
<ol>
<li>Listen for salvage spawn announcement (enemy died → crate spawned)</li>
<li><code>F10 > Navigation > Vectors to Nearest Salvage Crate</code></li>
<li>Fly to location (orange smoke = crate)</li>
<li>Use <strong>DCS F6 RADIO MENU > Sling Load > Hook Cargo</strong></li>
<li>Fly carefully to Salvage Collection Zone</li>
<li>Land or drop crate inside zone</li>
<li>Salvage awarded based on weight + condition!</li>
</ol>
</div>
</div>
<h3>Key Differences</h3>
<table>
<tr>
<th>Feature</th>
<th>MEDEVAC</th>
<th>Sling-Load Salvage</th>
</tr>
<tr>
<td>Pickup Method</td>
<td>Hover pickup OR land → auto-loads troops</td>
<td>DCS F6 menu sling-load ONLY (not CTLD hover pickup!)</td>
</tr>
<tr>
<td>Reward Type</td>
<td>Fixed value per vehicle type</td>
<td>Variable value based on weight + flying skill</td>
</tr>
<tr>
<td>Time Window</td>
<td>1-hour window before crew KIA</td>
<td>3-hour window before crate expires</td>
</tr>
<tr>
<td>Skill Level</td>
<td>Easy to Medium</td>
<td>Medium to Hard (condition bonuses)</td>
</tr>
</table>
<h2>🔧 Troubleshooting</h2>
<h3>"I can't sling-load the salvage crate!"</h3>
<ul>
<li>Use DCS F6 RADIO MENU, not F10 CTLD hover pickup</li>
<li>Make sure helicopter supports sling-load (Huey, Hip, Chinook, etc.)</li>
<li>Check crate weight vs. helicopter capacity</li>
</ul>
<h3>"Crate disappeared before I got there!"</h3>
<ul>
<li>Crates expire after 3 hours</li>
<li>Check <code>F10 > Navigation > Vectors</code> for time remaining</li>
<li>Warnings sent at 30min and 5min</li>
</ul>
<h3>"I didn't get full reward for my delivery!"</h3>
<ul>
<li>Check crate health - damage reduces reward by up to 50%</li>
<li>Fly smoothly, avoid crashes, gentle landings</li>
<li>Undamaged crates give 50% BONUS!</li>
</ul>
<h3>"No MEDEVAC missions spawning!"</h3>
<ul>
<li>Check crew survival chance settings (default 50%)</li>
<li>Only friendly vehicle deaths spawn MEDEVAC</li>
<li>5-minute delay after death before crew spawns</li>
</ul>
<h3>"Where do I create Salvage Collection Zones?"</h3>
<ul>
<li><code>F10 > Field Tools > Salvage Collection Zones > Create Salvage Zone Here</code></li>
<li>Zone spawns at your current position with 300m radius</li>
</ul>
<h2>⚙️ Configuration Notes</h2>
<p>Mission makers can adjust:</p>
<ul>
<li>MEDEVAC crew survival chance (default 50% per coalition)</li>
<li>Sling-load salvage spawn chance (default 15% per coalition)</li>
<li>Crate lifetime (default 3 hours)</li>
<li>Weight class probabilities and reward rates</li>
<li>Condition multipliers</li>
<li>MANPADS spawn chance with crews (default 10%)</li>
<li>Spawn restrictions and distances</li>
</ul>
<p>All settings are per-coalition and fully configurable via the CTLD config table.</p>
<div class="footer">
<p><strong>System Design:</strong> F99th Squadron + AI Collaboration</p>
<p><strong>Implementation:</strong> MOOSE Framework + CTLD Module</p>
<p><strong>Concept Inspiration:</strong> Real-world combat salvage & rescue operations</p>
<p><strong>Gameplay Balance:</strong> Community tested & refined</p>
<br>
<p style="font-size: 1.2em; color: #4ecdc4;">Fly safe. Rescue smart. Salvage everything. 🚁📦</p>
</div>
</div>
</body>
</html>

View File

@ -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
-- =========================

View File

@ -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,
}

Binary file not shown.