mirror of
https://github.com/iTracerFacer/DCS_MissionDev.git
synced 2025-12-03 04:14:46 +00:00
added sling loads to the salvage system.
This commit is contained in:
parent
542a426028
commit
4a351a73dd
752
Moose_CTLD_Pure/MEDEVAC_Salvage_System_Guide.html
Normal file
752
Moose_CTLD_Pure/MEDEVAC_Salvage_System_Guide.html
Normal 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>< 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>
|
||||
@ -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
|
||||
-- =========================
|
||||
|
||||
@ -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.
Loading…
x
Reference in New Issue
Block a user