Too many changes to list accross to many files. Save more often dummy. :)

This commit is contained in:
iTracerFacer 2025-11-04 08:32:11 -06:00
parent 853a3114ee
commit b70d208f38
17 changed files with 134378 additions and 285 deletions

View File

@ -1,155 +0,0 @@
# Dual Coalition Zone Capture - Full Analysis & Changes
## Summary
The script has been refactored to provide **complete parity between RED and BLUE coalitions**. Both sides now have equal access to all features, information, and victory conditions.
---
## ✅ Changes Made for Full Dual-Coalition Support
### 1. **Tactical Markers** (Lines ~390-440)
**BEFORE:** Only BLUE coalition received tactical markers
**AFTER:** Both RED and BLUE receive separate tactical markers
- Each coalition sees enemy unit positions (when ≤10 units)
- Markers are coalition-specific and read-only
- Uses `TacticalMarkerID_BLUE` and `TacticalMarkerID_RED` for tracking
### 2. **Victory Conditions** (Lines ~490-600)
**BEFORE:** Only checked for BLUE victory
**AFTER:** Both coalitions can win
- BLUE Victory: All zones captured → "BLUE_VICTORY" flag set
- RED Victory: All zones captured → "RED_VICTORY" flag set
- Each side gets appropriate celebration effects (smoke colors, flares)
- Proper victory/defeat messages for both sides
### 3. **Zone Status Reports** (Lines ~710-740)
**BEFORE:** Only BLUE received status broadcasts
**AFTER:** Both coalitions receive status reports
- Each coalition sees their specific victory progress percentage
- Same zone ownership data, customized messaging per coalition
### 4. **Victory Progress Monitoring** (Lines ~745-780)
**BEFORE:** Only warned BLUE when approaching victory
**AFTER:** Both sides get symmetric warnings
- BLUE approaching victory (80%+) → BLUE gets encouragement, RED gets warning
- RED approaching victory (80%+) → RED gets encouragement, BLUE gets warning
### 5. **F10 Radio Menu Commands** (Lines ~840-900)
**BEFORE:** Only BLUE had F10 menu access
**AFTER:** Both coalitions have identical F10 menus
- "Get Zone Status Report" - Shows current zone ownership
- "Check Victory Progress" - Shows their specific progress percentage
- "Refresh Zone Colors" - Forces zone border redraw
### 6. **Zone Color Refresh Messages** (Line ~835)
**BEFORE:** Only BLUE notified when colors refreshed
**AFTER:** Both coalitions receive confirmation message
### 7. **Mission Definitions** (Lines 52-88)
**BEFORE:** Only BLUE mission defined
**AFTER:** Both coalitions have missions
- BLUE: "Capture the Airfields" (offensive mission)
- RED: "Defend the Motherland" (defensive mission)
---
## 🎯 Features Now Available to BOTH Coalitions
| Feature | BLUE | RED |
|---------|------|-----|
| Mission Objectives | ✅ | ✅ |
| Tactical Markers (enemy positions) | ✅ | ✅ |
| Zone Status Reports | ✅ | ✅ |
| Victory Progress Tracking | ✅ | ✅ |
| Victory Conditions | ✅ | ✅ |
| F10 Menu Commands | ✅ | ✅ |
| Zone Color Indicators | ✅ | ✅ |
| Capture/Attack/Guard Messages | ✅ | ✅ |
---
## 🔧 Configuration
Mission makers can now set up asymmetric scenarios by configuring the `ZONE_CONFIG` table:
```lua
local ZONE_CONFIG = {
RED = {
"Kilpyavr",
"Severomorsk-1",
-- ... more zones
},
BLUE = {
"Banak", -- Example: BLUE starting zone
"Kirkenes"
},
NEUTRAL = {
"Contested Valley" -- Starts empty
}
}
```
---
## 🎮 Gameplay Impact
### Balanced Competition
- Both sides can now win by capturing all zones
- Victory celebrations are coalition-specific (blue/red smoke & flares)
- Mission end triggers coalition-specific flags
### Equal Information Access
- Both coalitions see enemy positions in contested zones
- Both receive periodic status updates
- Both have access to F10 menu commands
### Symmetric Design
- All event handlers work equally for both sides
- Messages are dynamically generated based on zone ownership
- No hardcoded coalition bias anywhere in the code
---
## 📝 Technical Notes
### Global Variables Used
- `US_CC` - BLUE coalition command center
- `RU_CC` - RED coalition command center
- Both must be defined before loading this script
### User Flags Set on Victory
- `BLUE_VICTORY` = 1 when BLUE wins
- `RED_VICTORY` = 1 when RED wins
### Storage Structure
- `zoneCaptureObjects[]` - Array of zone capture objects
- `zoneNames[]` - Array of zone names
- `zoneMetadata{}` - Dictionary with coalition info
- All zones accessible via table iteration (no global zone variables)
---
## 🚀 Migration from Old Script
If migrating from `Moose_CaptureZones.lua`:
1. **Update zone configuration** - Move zone names to `ZONE_CONFIG` table
2. **Remove manual zone creation** - The loop handles it now
3. **No code changes needed** for existing trigger zones in mission editor
4. **F10 menus now available** to RED players automatically
---
## ✨ Benefits of Refactoring
1. **Easy to configure** - Simple table instead of repetitive code
2. **Coalition agnostic** - Works equally for RED/BLUE/NEUTRAL
3. **Maintainable** - Zone logic centralized in loops
4. **Extensible** - Easy to add new features for both sides
5. **Balanced** - True dual-coalition gameplay
---
*Last Updated: Analysis completed with full dual-coalition parity*

View File

@ -893,20 +893,30 @@ local ZoneColorVerificationScheduler = SCHEDULER:New( nil, function()
end
end
end, {}, 60, 120 ) -- Start after 60 seconds, repeat every 120 seconds (2 minutes)
end, {}, 60, 240 ) -- Start after 60 seconds, repeat every 240 seconds (4 minutes)
-- Periodic tactical marker update system (every 1 minute)
-- Periodic tactical marker update system with change detection (every 3 minutes)
local __lastForceCountsByZone = {}
local TacticalMarkerUpdateScheduler = SCHEDULER:New( nil, function()
log("[TACTICAL] Running periodic tactical marker update...")
-- Update tactical markers for all zones
log("[TACTICAL] Running periodic tactical marker update (change-detected)...")
for i, zoneCapture in ipairs(zoneCaptureObjects) do
if zoneCapture then
CreateTacticalInfoMarker(zoneCapture)
local zoneName = zoneNames and zoneNames[i] or (zoneCapture.GetZoneName and zoneCapture:GetZoneName()) or ("Zone " .. i)
local counts = GetZoneForceStrengths(zoneCapture)
local last = __lastForceCountsByZone[zoneName]
local changed = (not last) or (last.red ~= counts.red) or (last.blue ~= counts.blue) or (last.neutral ~= counts.neutral)
if changed then
__lastForceCountsByZone[zoneName] = { red = counts.red, blue = counts.blue, neutral = counts.neutral }
CreateTacticalInfoMarker(zoneCapture)
else
-- unchanged: skip marker churn
end
end
end
end, {}, 30, 60 ) -- Start after 30 seconds, repeat every 60 seconds (1 minute)
end, {}, 30, 180 ) -- Start after 30 seconds, repeat every 180 seconds (3 minutes)
-- Function to refresh all zone colors based on current ownership
local function RefreshAllZoneColors()

View File

@ -1,4 +1,4 @@
--[[
.--[[
Simple AFAC System v1.0
========================
@ -86,8 +86,8 @@ AFAC.Config = {
mapMarkerDuration = 120,
smokeInterval = 300,
-- Target update intervals
autoUpdateInterval = 1.0,
-- Target update intervals (throttled to reduce per-second scanning)
autoUpdateInterval = 2.5,
manualScanRange = 18520,
-- Debug mode
@ -962,10 +962,10 @@ timer.scheduleFunction(
trigger.action.outText("Simple AFAC System v1.0 loaded successfully!", 10)
AFAC.log("AFAC Script fully loaded and initialized")
-- Test message every 30 seconds to verify script is running
function AFAC.heartbeat()
AFAC.log("AFAC System heartbeat - script is running")
timer.scheduleFunction(AFAC.heartbeat, nil, timer.getTime() + 30)
end
timer.scheduleFunction(AFAC.heartbeat, nil, timer.getTime() + 30)
-- Heartbeat disabled in production to avoid periodic UI/network churn
-- To re-enable for debugging, uncomment lines below:
-- function AFAC.heartbeat()
-- AFAC.log("AFAC System heartbeat - script is running")
-- timer.scheduleFunction(AFAC.heartbeat, nil, timer.getTime() + 30)
-- end
-- timer.scheduleFunction(AFAC.heartbeat, nil, timer.getTime() + 30)

View File

@ -31,6 +31,19 @@ local CVN_TACAN_Name = "CVN" -- put the 3-letter TACAN identifier you want to
local CVN_RecoveryWindowTime = 20 -- time in minutes for how long recovery will be open, feel free to change the number
-- Simple per-event cooldown to limit chat/sound spam (reduces network/UI churn)
local __cvnLastMsg = {}
local function sendThrottled(key, cooldownSec, sendFn)
local now = timer.getTime()
local last = __cvnLastMsg[key] or 0
if (now - last) >= (cooldownSec or 30) then
__cvnLastMsg[key] = now
local ok, err = pcall(sendFn)
if not ok then env.info("[CVN] sendThrottled error for key "..tostring(key)..": "..tostring(err)) end
end
end
-- Functions to move ship to designated waypoint/zones
local msgCVNPatrol = "Sending Carrier Group to Patrol Zone: "
@ -38,8 +51,10 @@ local msgCVNPatrol = "Sending Carrier Group to Patrol Zone: "
function SetCVNPatrolZone(zoneNumber)
if PatrolZones[zoneNumber] then
SetCVNActivePatrolZone = zoneNumber
MESSAGE:New(msgCVNPatrol .. SetCVNActivePatrolZone .. " (Waypoints " .. PatrolZones[zoneNumber].startWP .. "-" .. PatrolZones[zoneNumber].endWP .. ")", msgTime):ToBlue()
USERSOUND:New("ping.ogg"):ToCoalition(coalition.side.BLUE)
sendThrottled("CVN_SetPatrolZone", 30, function()
MESSAGE:New(msgCVNPatrol .. SetCVNActivePatrolZone .. " (Waypoints " .. PatrolZones[zoneNumber].startWP .. "-" .. PatrolZones[zoneNumber].endWP .. ")", msgTime):ToBlue()
USERSOUND:New("ping.ogg"):ToCoalition(coalition.side.BLUE)
end)
BlueCVNGroup:GotoWaypoint(PatrolZones[zoneNumber].startWP)
else
MESSAGE:New("Invalid patrol zone: " .. zoneNumber, msgTime):ToBlue()
@ -93,8 +108,10 @@ function start_recovery()
if BlueCVNGroup:IsSteamingIntoWind() == true then
local unitName = CVN_beacon_unit and CVN_beacon_unit:GetName() or "CVN-72 Abraham Lincoln"
MESSAGE:New(unitName .. " is currently launching/recovering aircraft, currently active recovery window closes at time " .. timerecovery_end, msgTime, "CVNNAVINFO",false):ToBlue()
USERSOUND:New("ping.ogg"):ToCoalition(coalition.side.BLUE)
sendThrottled("CVN_RecoveryActive", 60, function()
MESSAGE:New(unitName .. " is currently launching/recovering aircraft, currently active recovery window closes at time " .. timerecovery_end, msgTime, "CVNNAVINFO",false):ToBlue()
USERSOUND:New("ping.ogg"):ToCoalition(coalition.side.BLUE)
end)
else
local timenow=timer.getAbsTime( )
local timeend=timenow+CVN_RecoveryWindowTime*60 -- this sets the recovery window to 45 minutes, you can change the numbers as you wish
@ -102,8 +119,10 @@ function start_recovery()
timerecovery_end = UTILS.SecondsToClock(timeend,true)
BlueCVNGroup:AddTurnIntoWind(timerecovery_start,timerecovery_end,25,true,-9)
local unitName = CVN_beacon_unit and CVN_beacon_unit:GetName() or "CVN-72 Abraham Lincoln"
MESSAGE:New(unitName.." is turning into the wind to begin " .. CVN_RecoveryWindowTime .. " mins of aircraft operations.\nLaunch/Recovery Window will be open from time " .. timerecovery_start .. " until " .. timerecovery_end, msgTime, "CVNNAVINFO",false):ToBlue()
USERSOUND:New("ping.ogg"):ToCoalition(coalition.side.BLUE)
sendThrottled("CVN_RecoveryStart", 60, function()
MESSAGE:New(unitName.." is turning into the wind to begin " .. CVN_RecoveryWindowTime .. " mins of aircraft operations.\nLaunch/Recovery Window will be open from time " .. timerecovery_start .. " until " .. timerecovery_end, msgTime, "CVNNAVINFO",false):ToBlue()
USERSOUND:New("ping.ogg"):ToCoalition(coalition.side.BLUE)
end)
end
end
@ -135,8 +154,10 @@ end
function ResetAwacs()
if BlueAwacs then
BlueAwacs:Destroy()
MESSAGE:New("Resetting AWACS...", msgTime, "CVNNAVINFO",false):ToBlue()
USERSOUND:New("ping.ogg"):ToCoalition(coalition.side.BLUE)
sendThrottled("CVN_ResetAWACS", 30, function()
MESSAGE:New("Resetting AWACS...", msgTime, "CVNNAVINFO",false):ToBlue()
USERSOUND:New("ping.ogg"):ToCoalition(coalition.side.BLUE)
end)
else
MESSAGE:New("No AWACS to reset - AWACS functionality not available", msgTime):ToBlue()
end
@ -193,8 +214,10 @@ function BlueCVNGroup:OnAfterPassingWaypoint(From, Event, To, Waypoint)
-- Debug info.
local unitName = CVN_beacon_unit and CVN_beacon_unit:GetName() or "CVN-72 Abraham Lincoln"
local text=string.format(unitName.." passed waypoint ID=%d (Index=%d) %d times", waypoint.uid, BlueCVNGroup:GetWaypointIndex(waypoint.uid), waypoint.npassed)
MESSAGE:New(text, msgTime):ToBlue()
USERSOUND:New("ping.ogg"):ToCoalition(coalition.side.BLUE)
sendThrottled("CVN_PassingWaypoint", 20, function()
MESSAGE:New(text, msgTime):ToBlue()
USERSOUND:New("ping.ogg"):ToCoalition(coalition.side.BLUE)
end)
env.info(text)
-- Dynamic patrol logic based on current patrol zone
@ -206,13 +229,13 @@ function BlueCVNGroup:OnAfterPassingWaypoint(From, Event, To, Waypoint)
if currentWaypointID == currentZone.endWP then
BlueCVNGroup:GotoWaypoint(currentZone.startWP)
local patrolText = string.format("Patrolling: Going to waypoint %d (Zone %d)", currentZone.startWP, SetCVNActivePatrolZone)
MESSAGE:New(patrolText, msgTime):ToBlue()
sendThrottled("CVN_PatrolGoto", 10, function() MESSAGE:New(patrolText, msgTime):ToBlue() end)
env.info(patrolText)
-- If we reached the start waypoint of current patrol zone, go to end waypoint
elseif currentWaypointID == currentZone.startWP then
BlueCVNGroup:GotoWaypoint(currentZone.endWP)
local patrolText = string.format("Patrolling: Going to waypoint %d (Zone %d)", currentZone.endWP, SetCVNActivePatrolZone)
MESSAGE:New(patrolText, msgTime):ToBlue()
sendThrottled("CVN_PatrolGoto", 10, function() MESSAGE:New(patrolText, msgTime):ToBlue() end)
env.info(patrolText)
end
end
@ -223,8 +246,10 @@ end
function BlueCVNGroup:OnAfterCruise(From, Event, To)
local unitName = CVN_beacon_unit and CVN_beacon_unit:GetName() or "CVN-72 Abraham Lincoln"
local text=unitName.." is cruising straight and steady."
MESSAGE:New(text, msgTime):ToBlue()
USERSOUND:New("ping.ogg"):ToCoalition(coalition.side.BLUE)
sendThrottled("CVN_Cruise", 30, function()
MESSAGE:New(text, msgTime):ToBlue()
USERSOUND:New("ping.ogg"):ToCoalition(coalition.side.BLUE)
end)
env.info(text)
end
@ -232,8 +257,10 @@ end
function BlueCVNGroup:OnAfterTurningStarted(From, Event, To)
local unitName = CVN_beacon_unit and CVN_beacon_unit:GetName() or "CVN-72 Abraham Lincoln"
local text=unitName.." has started turning!"
MESSAGE:New(text, msgTime):ToBlue()
USERSOUND:New("ping.ogg"):ToCoalition(coalition.side.BLUE)
sendThrottled("CVN_TurningStarted", 30, function()
MESSAGE:New(text, msgTime):ToBlue()
USERSOUND:New("ping.ogg"):ToCoalition(coalition.side.BLUE)
end)
env.info(text)
end
@ -241,8 +268,10 @@ end
function BlueCVNGroup:OnAfterTurningStopped(From, Event, To)
local unitName = CVN_beacon_unit and CVN_beacon_unit:GetName() or "CVN-72 Abraham Lincoln"
local text=unitName.." has stopped turning..proceeding to next waypoint."
MESSAGE:New(text, msgTime):ToBlue()
USERSOUND:New("ping.ogg"):ToCoalition(coalition.side.BLUE)
sendThrottled("CVN_TurningStopped", 30, function()
MESSAGE:New(text, msgTime):ToBlue()
USERSOUND:New("ping.ogg"):ToCoalition(coalition.side.BLUE)
end)
env.info(text)
end
@ -262,8 +291,10 @@ BlueCVNGroup:SetCheckZones(ZoneSet)
function BlueCVNGroup:OnAfterEnterZone(From, Event, To, Zone)
local unitName = CVN_beacon_unit and CVN_beacon_unit:GetName() or "CVN-72 Abraham Lincoln"
local text=string.format(unitName.." has entered patrol zone %s.", Zone:GetName())
MESSAGE:New(text, msgTime):ToBlue()
USERSOUND:New("ping.ogg"):ToCoalition(coalition.side.BLUE)
sendThrottled("CVN_EnterZone", 20, function()
MESSAGE:New(text, msgTime):ToBlue()
USERSOUND:New("ping.ogg"):ToCoalition(coalition.side.BLUE)
end)
env.info(text)
end
@ -271,7 +302,9 @@ end
function BlueCVNGroup:OnAfterLeaveZone(From, Event, To, Zone)
local unitName = CVN_beacon_unit and CVN_beacon_unit:GetName() or "CVN-72 Abraham Lincoln"
local text=string.format(unitName.." left patrol zone %s.", Zone:GetName())
MESSAGE:New(text, msgTime):ToBlue()
USERSOUND:New("ping.ogg"):ToCoalition(coalition.side.BLUE)
sendThrottled("CVN_LeaveZone", 20, function()
MESSAGE:New(text, msgTime):ToBlue()
USERSOUND:New("ping.ogg"):ToCoalition(coalition.side.BLUE)
end)
env.info(text)
end

View File

@ -19,6 +19,13 @@ REQUIRES:
]]
-- Single-run guard to prevent duplicate dispatcher loops if script is reloaded
if _G.__TDAC_DISPATCHER_RUNNING then
env.info("[TDAC] CargoDispatcher already running; aborting duplicate load")
return
end
_G.__TDAC_DISPATCHER_RUNNING = true
--[[
GLOBAL STATE AND CONFIGURATION
--------------------------------------------------------------------------

View File

@ -5,6 +5,7 @@ trigger.action.outText("OnBirthMessage script is loading...", 10)
-- Player preferences storage
local playerWelcomeSettings = {}
local processedPlayers = {} -- Track players to prevent double processing
local DUP_TTL_SECONDS = 10 -- window to ignore duplicate events for the same player
-- F10 Menu Functions
local function enableWelcomeMessage(playerUnitID, playerName)
@ -77,19 +78,20 @@ function onPlayerJoin:onEvent(event)
if (event.id == world.event.S_EVENT_BIRTH or event.id == world.event.S_EVENT_ENGINE_STARTUP) then
env.info("OnBirthMessage: Correct event type detected")
if event.initiator then
if event.initiator then
env.info("OnBirthMessage: Initiator exists")
local playerName = event.initiator:getPlayerName()
if playerName then
env.info("OnBirthMessage: Player name found: " .. playerName)
-- Check if we've already processed this player to prevent doubles
local playerKey = playerName .. "_" .. event.id
if processedPlayers[playerKey] then
env.info("OnBirthMessage: Already processed " .. playerName .. " for event " .. event.id .. " - skipping")
-- Check if we've already processed this player to prevent doubles (within TTL)
local now = (event.time or timer.getTime())
local last = processedPlayers[playerName]
if last and (now - last) < DUP_TTL_SECONDS then
env.info("OnBirthMessage: Duplicate event for " .. playerName .. " within TTL - skipping")
return
end
processedPlayers[playerKey] = true
processedPlayers[playerName] = now
-- Add error handling to prevent script crashes
local success, errorMsg = pcall(function()

File diff suppressed because it is too large Load Diff

132469
Moose_.lua.backup Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,552 @@
-- Moose_CTLD.lua
-- Pure-MOOSE, template-free CTLD-style logistics & troop transport
-- Drop-in script: no MIST, no mission editor templates required
-- Dependencies: Moose.lua must be loaded before this script
-- Author: Copilot (generated)
-- Contract
-- Inputs: Config table or defaults. No ME templates needed. Zones may be named ME trigger zones or provided via coordinates in config.
-- Outputs: F10 menus for helo/transport groups; crate spawning/building; troop load/unload; optional JTAC hookup (via FAC module);
-- Error modes: missing Moose -> abort; unknown crate key -> message; spawn blocked in enemy airbase; zone missing -> message.
if not _G.Moose or not _G.BASE then
env.info('[Moose_CTLD] Moose not detected (BASE class missing). Ensure Moose.lua is loaded before Moose_CTLD.lua')
end
local CTLD = {}
CTLD.__index = CTLD
-- =========================
-- Defaults and State
-- =========================
CTLD.Version = '0.1.0-alpha'
CTLD.Config = {
CoalitionSide = coalition.side.BLUE, -- default coalition this instance serves (menus created for this side)
AllowedAircraft = { -- transport-capable unit type names (case-sensitive as in DCS DB)
'UH-1H','Mi-8MTV2','Mi-24P','SA342M','SA342L','SA342Minigun','Ka-50','Ka-50_3','AH-64D_BLK_II','UH-60L','CH-47Fbl1','CH-47F','Mi-17','GazelleAI'
},
UseGroupMenus = true, -- if true, F10 menus per player group; otherwise coalition-wide
UseCategorySubmenus = true, -- if true, organize crate requests by category submenu (menuCategory)
BuildRadius = 60, -- meters around build point to collect crates
CrateLifetime = 3600, -- seconds before crates auto-clean
MessageDuration = 15, -- seconds for on-screen messages
Debug = false,
PickupZoneSmokeColor = trigger.smokeColor.Green, -- default smoke color when spawning crates at pickup zones
Zones = { -- Optional: supply by name (ME trigger zones) or define coordinates inline
PickupZones = {
-- examples:
-- { name = 'PICKUP_BLUE_MAIN' },
-- { name = 'Pickup-West', smoke = trigger.smokeColor.Green },
-- { coord = { x = 12345, y = 0, z = 67890 }, radius = 150, name = 'ScriptPickup1' },
},
DropZones = {
-- { name = 'DROP_BLUE_1' },
},
FOBZones = {
-- optional: where FOB crates can unpack to spawn FARP/FOB assets
},
},
-- Crate catalog: key -> crate properties and build recipe
-- No ME templates; unit compositions are defined directly here.
CrateCatalog = {
-- Example: MANPADS team requiring 2 crates
MANPADS = {
description = '2x Crates -> MANPADS team',
weight = 120, -- affects sling/limits only informationally (no physics in script)
dcsCargoType = 'uh1h_cargo', -- static cargo type (can adjust per DCS version); user-tunable
required = 2, -- number of crates to assemble
side = coalition.side.BLUE,
category = Group.Category.GROUND,
build = function(point, headingDeg)
local u1 = { type = 'Soldier stinger', name = string.format('CTLD-MANPADS-%d', math.random(100000,999999)),
x = point.x, y = point.z, heading = math.rad(headingDeg or 0) }
local group = {
visible = false, lateActivation = false, tasks = {}, task = 'Ground Nothing',
units = { u1 }, route = { }, name = string.format('CTLD_MANPADS_%d', math.random(100000,999999))
}
return group
end,
},
-- Example: AAA site needing 3 crates
AAA = {
description = '3x Crates -> ZU-23 site',
weight = 400,
dcsCargoType = 'container_cargo',
required = 3,
side = coalition.side.BLUE,
category = Group.Category.GROUND,
build = function(point, headingDeg)
local hdg = math.rad(headingDeg or 0)
local function offset(dx, dz) return { x = point.x + dx, z = point.z + dz } end
local units = {
{ type='ZU-23 Emplacement', name=string.format('CTLD-ZU23-%d', math.random(100000,999999)), x=point.x, y=point.z, heading=hdg },
{ type='Ural-375', name=string.format('CTLD-TRK-%d', math.random(100000,999999)), x=offset(15, 12).x, y=offset(15, 12).z, heading=hdg },
{ type='Infantry AK', name=string.format('CTLD-INF-%d', math.random(100000,999999)), x=offset(-12,-15).x, y=offset(-12,-15).z, heading=hdg },
}
return { visible=false, lateActivation=false, tasks={}, task='Ground Nothing', units=units, route={}, name=string.format('CTLD_AAA_%d', math.random(100000,999999)) }
end,
},
-- Example: FARP/FOB build from 4 crates (spawns helipads + support statics/vehicles)
FOB = {
description = '4x Crates -> FARP/FOB',
weight = 500,
dcsCargoType = 'container_cargo',
required = 4,
side = coalition.side.BLUE,
category = Group.Category.GROUND,
build = function(point, headingDeg)
local heading = math.rad(headingDeg or 0)
-- Spawn statics that provide FARP services
local function addStatic(typeName, dx, dz, nameSuffix)
local p = { x = point.x + dx, z = point.z + dz }
local st = {
name = string.format('CTLD-FOB-%s-%d', nameSuffix, math.random(100000,999999)),
type = typeName,
x = p.x, y = p.z,
heading = heading,
}
coalition.addStaticObject(coalition.side.BLUE, st)
end
-- Common FARP layout
addStatic('FARP', 0, 0, 'PAD')
addStatic('FARP Ammo Dump Coating', 15, 10, 'AMMO')
addStatic('FARP Fuel Depot', -15, 10, 'FUEL')
addStatic('FARP Tent', 10, -12, 'TENT1')
addStatic('FARP Tent', -10, -12, 'TENT2')
-- Ground support vehicles to enable rearm/refuel/repair
local units = {
{ type='HEMTT TFFT', name=string.format('CTLD-FOB-FUEL-%d', math.random(100000,999999)), x=point.x + 20, y=point.z + 15, heading=heading },
{ type='Ural-375 PBU', name=string.format('CTLD-FOB-REPAIR-%d', math.random(100000,999999)), x=point.x - 20, y=point.z + 15, heading=heading },
{ type='Ural-375', name=string.format('CTLD-FOB-AMMO-%d', math.random(100000,999999)), x=point.x, y=point.z - 18, heading=heading },
}
return { visible=false, lateActivation=false, tasks={}, task='Ground Nothing', units=units, route={}, name=string.format('CTLD_FOB_%d', math.random(100000,999999)) }
end,
},
},
}
-- Internal state tables
CTLD._instances = CTLD._instances or {}
CTLD._crates = {} -- [crateName] = { key, zone, side, spawnTime, point }
CTLD._troopsLoaded = {} -- [groupName] = { count, typeKey }
-- =========================
-- Utilities
-- =========================
local function _isIn(list, value)
for _,v in ipairs(list or {}) do if v == value then return true end end
return false
end
local function _msgGroup(group, text, t)
if not group then return end
MESSAGE:New(text, t or CTLD.Config.MessageDuration):ToGroup(group)
end
local function _msgCoalition(side, text, t)
MESSAGE:New(text, t or CTLD.Config.MessageDuration):ToCoalition(side)
end
local function _findZone(z)
if z.name then
local mz = ZONE:FindByName(z.name)
if mz then return mz end
end
if z.coord then
local r = z.radius or 150
local v = VECTOR2:New(z.coord.x, z.coord.z)
return ZONE_RADIUS:New(z.name or ('CTLD_ZONE_'..math.random(10000,99999)), v, r)
end
return nil
end
local function _getUnitType(unit)
local ud = unit and unit:GetDesc() or nil
return ud and ud.typeName or unit and unit:GetTypeName()
end
local function _nearestZonePoint(unit, list)
if not unit or not unit:IsAlive() then return nil end
local p3 = unit:GetPointVec3()
local best, bestd = nil, 1e12
for _,z in ipairs(list or {}) do
local mz = _findZone(z)
if mz then
local d = p3:DistanceFromPoint(mz:GetPointVec3())
if d < bestd then best, bestd = mz, d end
end
end
return best, bestd
end
local function _coalitionAddGroup(side, category, groupData)
-- Enforce side/category in groupData just to be safe
groupData.category = category
return coalition.addGroup(side, category, groupData)
end
local function _spawnStaticCargo(side, point, cargoType, name)
local static = {
name = name,
type = cargoType,
x = point.x,
y = point.z,
heading = 0,
canCargo = true,
}
return coalition.addStaticObject(side, static)
end
local function _vec3FromUnit(unit)
local p = unit:GetPointVec3()
return { x = p.x, y = p.y, z = p.z }
end
-- =========================
-- Construction
-- =========================
function CTLD:New(cfg)
local o = setmetatable({}, self)
o.Config = BASE:DeepCopy(CTLD.Config)
if cfg then o.Config = BASE:Inherit(o.Config, cfg) end
o.Side = o.Config.CoalitionSide
o.MenuRoots = {}
o.MenusByGroup = {}
o:InitZones()
o:InitMenus()
-- Periodic cleanup for crates
o.Sched = SCHEDULER:New(nil, function()
o:CleanupCrates()
end, {}, 60, 60)
table.insert(CTLD._instances, o)
_msgCoalition(o.Side, string.format('CTLD %s initialized for coalition', CTLD.Version))
return o
end
function CTLD:InitZones()
self.PickupZones = {}
self.DropZones = {}
self.FOBZones = {}
self._ZoneDefs = { PickupZones = {}, DropZones = {}, FOBZones = {} }
for _,z in ipairs(self.Config.Zones.PickupZones or {}) do
local mz = _findZone(z)
if mz then table.insert(self.PickupZones, mz); self._ZoneDefs.PickupZones[mz:GetName()] = z end
end
for _,z in ipairs(self.Config.Zones.DropZones or {}) do
local mz = _findZone(z)
if mz then table.insert(self.DropZones, mz); self._ZoneDefs.DropZones[mz:GetName()] = z end
end
for _,z in ipairs(self.Config.Zones.FOBZones or {}) do
local mz = _findZone(z)
if mz then table.insert(self.FOBZones, mz); self._ZoneDefs.FOBZones[mz:GetName()] = z end
end
end
-- =========================
-- Menus
-- =========================
function CTLD:InitMenus()
if self.Config.UseGroupMenus then
self:WireBirthHandler()
else
self.MenuRoot = MENU_COALITION:New(self.Side, 'CTLD')
self:BuildCoalitionMenus(self.MenuRoot)
end
end
function CTLD:WireBirthHandler()
local handler = EVENTHANDLER:New()
handler:HandleEvent(EVENTS.Birth)
local selfref = self
function handler:OnEventBirth(eventData)
local unit = eventData.IniUnit
if not unit or not unit:IsAlive() then return end
if unit:GetCoalition() ~= selfref.Side then return end
local typ = _getUnitType(unit)
if not _isIn(selfref.Config.AllowedAircraft, typ) then return end
local grp = unit:GetGroup()
if not grp then return end
local gname = grp:GetName()
if selfref.MenusByGroup[gname] then return end
selfref.MenusByGroup[gname] = selfref:BuildGroupMenus(grp)
_msgGroup(grp, 'CTLD menu available (F10)')
end
self.BirthHandler = handler
end
function CTLD:BuildGroupMenus(group)
local root = MENU_GROUP:New(group, 'CTLD')
-- Request crate submenu per catalog entry
local reqRoot = MENU_GROUP:New(group, 'Request Crate', root)
if self.Config.UseCategorySubmenus then
local submenus = {}
local function getSubmenu(catLabel)
if not submenus[catLabel] then
submenus[catLabel] = MENU_GROUP:New(group, catLabel, reqRoot)
end
return submenus[catLabel]
end
for key,def in pairs(self.Config.CrateCatalog) do
local label = (def and (def.menu or def.description)) or key
local sideOk = (not def.side) or def.side == self.Side
if sideOk then
local catLabel = (def and def.menuCategory) or 'Other'
local parent = getSubmenu(catLabel)
MENU_GROUP_COMMAND:New(group, label, parent, function()
self:RequestCrateForGroup(group, key)
end)
end
end
else
for key,def in pairs(self.Config.CrateCatalog) do
local label = (def and (def.menu or def.description)) or key
local sideOk = (not def.side) or def.side == self.Side
if sideOk then
MENU_GROUP_COMMAND:New(group, label, reqRoot, function()
self:RequestCrateForGroup(group, key)
end)
end
end
end
-- Troops
MENU_GROUP_COMMAND:New(group, 'Load Troops', root, function() self:LoadTroops(group) end)
MENU_GROUP_COMMAND:New(group, 'Unload Troops', root, function() self:UnloadTroops(group) end)
-- Build
MENU_GROUP_COMMAND:New(group, 'Build Here', root, function()
self:BuildAtGroup(group)
end)
return root
end
function CTLD:BuildCoalitionMenus(root)
-- Optional: implement coalition-level crate spawns at pickup zones
for key,_ in pairs(self.Config.CrateCatalog) do
MENU_COALITION_COMMAND:New(self.Side, 'Spawn '..key..' at nearest Pickup Zone', root, function()
-- Not group-context; skip here
_msgCoalition(self.Side, 'Group menus recommended for crate requests')
end)
end
end
-- =========================
-- Crates
-- =========================
function CTLD:RequestCrateForGroup(group, crateKey)
local cat = self.Config.CrateCatalog[crateKey]
if not cat then _msgGroup(group, 'Unknown crate type: '..tostring(crateKey)) return end
local unit = group:GetUnit(1)
if not unit or not unit:IsAlive() then return end
local zone, dist = _nearestZonePoint(unit, self.Config.Zones.PickupZones)
local spawnPoint
if zone and dist < 10000 then
spawnPoint = zone:GetPointVec3()
-- if pickup zone has smoke configured, mark it
local zdef = self._ZoneDefs.PickupZones[zone:GetName()]
local smokeColor = (zdef and zdef.smoke) or self.Config.PickupZoneSmokeColor
if smokeColor then
trigger.action.smoke({ x = spawnPoint.x, z = spawnPoint.z }, smokeColor)
end
else
-- fallback: spawn near aircraft current position (safe offset)
local p = unit:GetPointVec3()
spawnPoint = POINT_VEC3:New(p.x + 10, p.y, p.z + 10)
end
local cname = string.format('CTLD_CRATE_%s_%d', crateKey, math.random(100000,999999))
_spawnStaticCargo(self.Side, { x = spawnPoint.x, z = spawnPoint.z }, cat.dcsCargoType or 'uh1h_cargo', cname)
CTLD._crates[cname] = {
key = crateKey,
side = self.Side,
spawnTime = timer.getTime(),
point = { x = spawnPoint.x, z = spawnPoint.z },
}
_msgGroup(group, string.format('Spawned crate %s at %s', crateKey, zone and zone:GetName() or 'current pos'))
end
function CTLD:GetNearbyCrates(point, radius)
local result = {}
for name,meta in pairs(CTLD._crates) do
local dx = (meta.point.x - point.x)
local dz = (meta.point.z - point.z)
local d = math.sqrt(dx*dx + dz*dz)
if d <= radius then
table.insert(result, { name = name, meta = meta })
end
end
return result
end
function CTLD:CleanupCrates()
local now = timer.getTime()
local life = self.Config.CrateLifetime
for name,meta in pairs(CTLD._crates) do
if now - (meta.spawnTime or now) > life then
local obj = StaticObject.getByName(name)
if obj then obj:destroy() end
CTLD._crates[name] = nil
if self.Config.Debug then env.info('[CTLD] Cleaned up crate '..name) end
end
end
end
-- =========================
-- Build logic
-- =========================
function CTLD:BuildAtGroup(group)
local unit = group:GetUnit(1)
if not unit or not unit:IsAlive() then return end
local p = unit:GetPointVec3()
local here = { x = p.x, z = p.z }
local radius = self.Config.BuildRadius
local nearby = self:GetNearbyCrates(here, radius)
if #nearby == 0 then _msgGroup(group, 'No crates within '..radius..'m') return end
-- Count by key
local counts = {}
for _,c in ipairs(nearby) do
counts[c.meta.key] = (counts[c.meta.key] or 0) + 1
end
-- Helper to consume crates of a given key/qty
local function consumeCrates(key, qty)
local removed = 0
for _,c in ipairs(nearby) do
if removed >= qty then break end
if c.meta.key == key then
local obj = StaticObject.getByName(c.name)
if obj then obj:destroy() end
CTLD._crates[c.name] = nil
removed = removed + 1
end
end
end
-- Try composite recipes first (requires is a map of key->qty)
for recipeKey,cat in pairs(self.Config.CrateCatalog) do
if type(cat.requires) == 'table' and cat.build then
local ok = true
for reqKey,qty in pairs(cat.requires) do
if (counts[reqKey] or 0) < qty then ok = false; break end
end
if ok then
local hdg = unit:GetHeading()
local gdata = cat.build({ x = here.x, z = here.z }, math.deg(hdg))
local g = _coalitionAddGroup(cat.side or self.Side, cat.category or Group.Category.GROUND, gdata)
if g then
for reqKey,qty in pairs(cat.requires) do consumeCrates(reqKey, qty) end
_msgGroup(group, string.format('Built %s at your location', cat.description or recipeKey))
return
else
_msgGroup(group, 'Build failed: DCS group spawn error')
return
end
end
end
end
-- Then single-key recipes
for key,count in pairs(counts) do
local cat = self.Config.CrateCatalog[key]
if cat and cat.build and (not cat.requires) and count >= (cat.required or 1) then
local hdg = unit:GetHeading()
local gdata = cat.build({ x = here.x, z = here.z }, math.deg(hdg))
local g = _coalitionAddGroup(cat.side or self.Side, cat.category or Group.Category.GROUND, gdata)
if g then
consumeCrates(key, cat.required or 1)
_msgGroup(group, string.format('Built %s at your location', cat.description or key))
return
else
_msgGroup(group, 'Build failed: DCS group spawn error')
return
end
end
end
_msgGroup(group, 'Insufficient crates to build any asset here')
end
-- =========================
-- Troops
-- =========================
function CTLD:LoadTroops(group, opts)
local gname = group:GetName()
local unit = group:GetUnit(1)
if not unit or not unit:IsAlive() then return end
local capacity = 6 -- simple default; can be adjusted per type later
CTLD._troopsLoaded[gname] = {
count = capacity,
typeKey = 'RIFLE',
}
_msgGroup(group, string.format('Loaded %d troops (virtual). Use Unload Troops to deploy.', capacity))
end
function CTLD:UnloadTroops(group)
local gname = group:GetName()
local load = CTLD._troopsLoaded[gname]
if not load or (load.count or 0) == 0 then _msgGroup(group, 'No troops onboard') return end
local unit = group:GetUnit(1)
if not unit or not unit:IsAlive() then return end
local p = unit:GetPointVec3()
local here = { x = p.x, z = p.z }
local hdg = unit:GetHeading()
local count = load.count
-- Spawn a simple infantry fireteam
local units = {}
for i=1, math.min(count, 8) do
table.insert(units, {
type = 'Infantry AK', name = string.format('CTLD-TROOP-%d', math.random(100000,999999)),
x = here.x + i*1.5, y = here.z + (i%2==0 and 2 or -2), heading = hdg
})
end
local groupData = {
visible=false, lateActivation=false, tasks={}, task='Ground Nothing',
units=units, route={}, name=string.format('CTLD_TROOPS_%d', math.random(100000,999999))
}
local spawned = _coalitionAddGroup(self.Side, Group.Category.GROUND, groupData)
if spawned then
CTLD._troopsLoaded[gname] = nil
_msgGroup(group, string.format('Deployed %d troops', #units))
else
_msgGroup(group, 'Deploy failed: DCS group spawn error')
end
end
-- =========================
-- Public helpers
-- =========================
function CTLD:RegisterCrate(key, def)
self.Config.CrateCatalog[key] = def
end
function CTLD:MergeCatalog(tbl)
for k,v in pairs(tbl or {}) do self.Config.CrateCatalog[k] = v end
end
function CTLD:AddPickupZone(z)
local mz = _findZone(z)
if mz then table.insert(self.PickupZones, mz); table.insert(self.Config.Zones.PickupZones, z) end
end
function CTLD:AddDropZone(z)
local mz = _findZone(z)
if mz then table.insert(self.DropZones, mz); table.insert(self.Config.Zones.DropZones, z) end
end
function CTLD:SetAllowedAircraft(list)
self.Config.AllowedAircraft = BASE:DeepCopy(list)
end
-- =========================
-- Return factory
-- =========================
_MOOSE_CTLD = CTLD
return CTLD

View File

@ -0,0 +1,207 @@
-- Moose_CTLD_FAC.lua
-- FAC/RECCE features integrated with pure-MOOSE CTLD
-- Provides: recce zones, auto target marking (smoke/illum), JTAC auto-lase bootstrap, optional artillery mark tasks
if not _G.Moose or not _G.BASE then
env.info('[Moose_CTLD_FAC] Moose not detected. Ensure Moose.lua is loaded before this script.')
end
local FAC = {}
FAC.__index = FAC
FAC.Version = '0.1.0-alpha'
FAC.Config = {
CoalitionSide = coalition.side.BLUE,
ScanInterval = 20, -- seconds between scans
MarkSmokeColor = trigger.smokeColor.Red,
MarkIllum = false, -- drop illumination at night if true
MarkText = true, -- place map marks with target info
DetectionRadius = 5000, -- meters within zone
MinReportSeparation = 400, -- meters between subsequent marks to reduce spam
UseGroupMenus = true,
Debug = false,
Arty = { -- optional artillery support
Enabled = true,
Groups = { -- names of friendly artillery groups to use for marking
-- 'BLUE_ARTY_1',
},
Rounds = 3,
Spread = 120, -- meters randomization around mark point
}
}
FAC._lastMarks = {} -- [zoneName] = { lastPoint = {x,z} }
function FAC:New(ctld, cfg)
local o = setmetatable({}, self)
o.CTLD = ctld
o.Config = BASE:DeepCopy(FAC.Config)
if cfg then o.Config = BASE:Inherit(o.Config, cfg) end
o.Side = o.Config.CoalitionSide
o.Zones = {}
o.MenusByGroup = {}
if o.Config.UseGroupMenus then o:WireBirthHandler() end
return o
end
function FAC:WireBirthHandler()
local handler = EVENTHANDLER:New()
handler:HandleEvent(EVENTS.Birth)
local selfref = self
function handler:OnEventBirth(eventData)
local unit = eventData.IniUnit
if not unit or not unit:IsAlive() then return end
if unit:GetCoalition() ~= selfref.Side then return end
local grp = unit:GetGroup()
if not grp then return end
local gname = grp:GetName()
if selfref.MenusByGroup[gname] then return end
-- Simple menu: FAC actions
local root = MENU_GROUP:New(grp, 'FAC/RECCE')
MENU_GROUP_COMMAND:New(grp, 'List Recce Zones', root, function() selfref:MenuListZones(grp) end)
MENU_GROUP_COMMAND:New(grp, 'Mark Contacts (all zones)', root, function() selfref:ForceScanAll(grp) end)
selfref.MenusByGroup[gname] = root
MESSAGE:New('FAC/RECCE menu available (F10)', 10):ToGroup(grp)
end
self.BirthHandler = handler
end
function FAC:AddRecceZone(def)
-- def: { name='ZONE_NAME' } or { coord={x,y,z}, radius=NN, name='Recce1' }
local z
if def.name then
z = ZONE:FindByName(def.name)
end
if not z and def.coord then
local r = def.radius or 5000
z = ZONE_RADIUS:New(def.name or ('FAC_ZONE_'..math.random(10000,99999)), VECTOR2:New(def.coord.x, def.coord.z), r)
end
if not z then return nil end
local Z = {
Zone = z,
Name = z:GetName(),
Detector = self:CreateDetector(z),
LastScan = 0,
}
table.insert(self.Zones, Z)
return Z
end
function FAC:CreateDetector(zone)
-- Detection in areas using Moose detection classes
local enemySide = (self.Side == coalition.side.BLUE) and coalition.side.RED or coalition.side.BLUE
local setEnemies = SET_GROUP:New():FilterCoalitions(enemySide):FilterCategoryGround():FilterStart()
local det = DETECTION_AREAS:New(setEnemies, zone:GetRadius())
det:BoundZone(zone)
return det
end
function FAC:MenuListZones(group)
local names = {}
for _,Z in ipairs(self.Zones) do table.insert(names, Z.Name) end
MESSAGE:New('Recce zones: '..(table.concat(names, ', '):gsub('^%s+$','none')), 15):ToGroup(group)
end
function FAC:ForceScanAll(group)
for _,Z in ipairs(self.Zones) do self:ScanZone(Z, group) end
end
function FAC:Run()
-- schedule periodic scanning
if self.Sched then self.Sched:Stop() end
self.Sched = SCHEDULER:New(nil, function()
for _,Z in ipairs(self.Zones) do self:ScanZone(Z) end
end, {}, 5, self.Config.ScanInterval)
end
local function _p3(v2)
return { x = v2.x, y = land.getHeight({x=v2.x, y=v2.y}), z = v2.y }
end
function FAC:ScanZone(Z, notifyGroup)
local now = timer.getTime()
local det = Z.Detector
det:DetectionUpdate()
local reports = det:GetDetectedItems()
if not reports or #reports == 0 then
if notifyGroup then MESSAGE:New('No contacts detected in '..Z.Name, 10):ToGroup(notifyGroup) end
return
end
local enemySide = (self.Side == coalition.side.BLUE) and coalition.side.RED or coalition.side.BLUE
for _,rep in ipairs(reports) do
local contact = rep.object -- wrapper around GROUP or UNIT
local pos2 = rep.point -- vec2
if pos2 then
local markPoint = { x = pos2.x, z = pos2.y }
local allow = true
local last = FAC._lastMarks[Z.Name]
if last then
local dx = (markPoint.x - last.x); local dz = (markPoint.z - last.z)
if math.sqrt(dx*dx+dz*dz) < self.Config.MinReportSeparation then allow = false end
end
if allow then
FAC._lastMarks[Z.Name] = { x = markPoint.x, z = markPoint.z }
self:MarkTarget(Z, markPoint, rep, enemySide)
if notifyGroup then MESSAGE:New(string.format('Marked contact in %s', Z.Name), 10):ToGroup(notifyGroup) end
end
end
end
end
function FAC:MarkTarget(Z, point, rep, enemySide)
-- Smoke
trigger.action.smoke(point, self.Config.MarkSmokeColor)
-- Map mark
if self.Config.MarkText then
local txt = string.format('FAC: %s at %s', rep.type or 'Contact', coord.LLtoString(coord.LOtoLL(point), 0))
trigger.action.markToCoalition(math.random(100000,999999), txt, point, self.Side, true)
end
-- Optional arty marking
if self.Config.Arty.Enabled then
self:ArtyMark(point)
end
end
function FAC:ArtyMark(point)
local rounds = self.Config.Arty.Rounds or 1
for _,gname in ipairs(self.Config.Arty.Groups or {}) do
local g = Group.getByName(gname)
if g and g:isExist() then
local ctrl = g:getController()
if ctrl then
for i=1,rounds do
local spread = self.Config.Arty.Spread or 0
local tgt = { x = point.x + math.random(-spread, spread), y = point.z + math.random(-spread, spread) }
local task = {
id = 'FireAtPoint',
params = {
point = { x = tgt.x, y = land.getHeight({x=tgt.x, y=tgt.y}), z = tgt.y },
expendQty = 1,
dispersion = 50,
attackQty = 1,
weaponType = 0,
}
}
ctrl:setTask(task)
end
end
end
end
end
-- Bootstrap a JTAC on a spawned unit/group via MOOSE FAC_AUTO
function FAC:StartJTACOnGroup(groupName, code, smoke)
local grp = GROUP:FindByName(groupName)
if not grp then return nil end
local fac = FAC_AUTO:New(grp)
fac:SetLaser(true, code or 1688)
fac:SetSmoke(smoke or self.Config.MarkSmokeColor)
fac:SetDetectVehicles()
fac:Start()
return fac
end
_MOOSE_CTLD_FAC = FAC
return FAC

106
Moose_CTLD_Pure/README.md Normal file
View File

@ -0,0 +1,106 @@
# Moose_CTLD_Pure
Pure-MOOSE CTLD-style logistics and FAC/RECCE without MIST or mission editor templates. Drop-in, config-driven.
## What this is
- Logistics and troop transport similar to popular CTLD scripts, implemented directly on MOOSE.
- No MIST. No mission editor templates. Unit compositions are defined in config tables.
- Optional FAC/RECCE module that auto-marks targets in zones and can drive artillery marking and JTAC auto-lase.
## Quick start
1) Load `Moose.lua` first, then include these files (order matters):
- `Moose_CTLD_Pure/Moose_CTLD.lua`
- `Moose_CTLD_Pure/Moose_CTLD_FAC.lua` (optional, for FAC/RECCE)
2) Initialize CTLD with minimal config:
```lua
local CTLD = dofile(lfs.writedir()..[[Scripts\Moose_CTLD_Pure\Moose_CTLD.lua]])
local ctld = CTLD:New({
CoalitionSide = coalition.side.BLUE,
Zones = {
PickupZones = { { name = 'PICKUP_BLUE_MAIN' } },
DropZones = { { name = 'DROP_BLUE_1' } },
},
})
-- Optionally load a larger crate catalog extracted from your CTLD.lua
local extracted = dofile(lfs.writedir()..[[Scripts\Moose_CTLD_Pure\catalogs\CrateCatalog_CTLD_Extract.lua]])
ctld:MergeCatalog(extracted)
```
- If you don't have ME trigger zones, define by coordinates:
```lua
Zones = {
PickupZones = {
{ coord = { x=123456, y=0, z=654321 }, radius=150, name='ScriptPickup1' },
}
}
```
3) (Optional) FAC/RECCE:
```lua
local FAC = dofile(lfs.writedir()..[[Scripts\Moose_CTLD_Pure\Moose_CTLD_FAC.lua]])
local fac = FAC:New(ctld, {
CoalitionSide = coalition.side.BLUE,
Arty = { Enabled = true, Groups = { 'BLUE_ARTY_1' }, Rounds = 3, Spread = 100 },
})
fac:AddRecceZone({ name = 'RECCE_ZONE_1' })
fac:Run()
```
4) In mission, pilots of allowed aircraft (configured in `AllowedAircraft`) will see F10 menus:
- CTLD > Request Crate > [Type]
- CTLD > Load Troops / Unload Troops
- CTLD > Build Here
- FAC/RECCE > List Recce Zones / Mark Contacts (all zones)
## Configuring crates and builds (no templates)
Edit `CrateCatalog` in `Moose_CTLD.lua`. Each entry defines:
- `required`: how many crates to assemble
- `weight`: informational
- `dcsCargoType`: DCS static cargo type string (e.g., `uh1h_cargo`, `container_cargo`); tweak per map/mods
- `build(point, headingDeg)`: function returning a DCS group table for `coalition.addGroup`
Example snippet:
```lua
CrateCatalog = {
MANPADS = {
description = '2x Crates -> MANPADS team',
weight = 120,
dcsCargoType = 'uh1h_cargo',
required = 2,
side = coalition.side.BLUE,
category = Group.Category.GROUND,
build = function(point, headingDeg)
return { visible=false, lateActivation=false, units={
{ type='Soldier stinger', name='CTLD-MANPADS-1', x=point.x, y=point.z, heading=math.rad(headingDeg or 0) }
} }
end,
},
}
```
## Notes and limitations
- This avoids sling-load event dependency by using a player command "Build Here" that consumes nearby crates within a radius. You can still sling crates physically.
- Cargo static types vary across DCS versions. If a spawned crate isnt sling-loadable, change `dcsCargoType` strings in `CrateCatalog` (e.g., `uh1h_cargo`, `container_cargo`, `ammo_cargo`, `container_20ft` depending on map/version).
- Troops are virtually loaded and spawned on unload. Adjust capacity logic if you want type-based capacities.
- FAC/RECCE detection leverages Moose `DETECTION_AREAS`. Tweak `ScanInterval`, `DetectionRadius`, and `MinReportSeparation` to balance spam/performance.
- Artillery marking uses `Controller.setTask('FireAtPoint')` on configured groups. Ensure those groups exist and are artillery-capable.
- JTAC Auto-Lase helper provided: `fac:StartJTACOnGroup(groupName, laserCode, smokeColor)` uses `FAC_AUTO`.
## Extending
- Add radio beacons, FOB build recipes, fuel/ammo crates, and CSAR hooks by registering more `CrateCatalog` entries and/or adding helper methods.
- To support per-airframe capacities and sling-only rules, extend `AllowedAircraft` and add a type->capacity map.
## Changelog
- 0.1.0-alpha: Initial release: CTLD crate/troops/build, FAC recce zones + arty mark + JTAC bootstrap.

View File

@ -0,0 +1,166 @@
-- CrateCatalog_CTLD_Extract.lua
-- Auto-generated from CTLD.lua (Operation_Polar_Shield) spawnableCrates config
-- Returns a table of crate definitions suitable for CTLD:MergeCatalog()
-- Notes:
-- - Each entry has keys: description/menu, dcsCargoType, required or requires (composite), side, category, build(point, headingDeg)
-- - Single-unit entries spawn one unit by DCS type. Composite "SITE" entries spawn a multi-unit group approximating system components.
local function singleUnit(unitType)
return function(point, headingDeg)
local name = string.format('%s-%d', unitType, math.random(100000,999999))
local hdg = math.rad(headingDeg or 0)
return {
visible=false, lateActivation=false, tasks={}, task='Ground Nothing', route={},
units={ { type=unitType, name=name, x=point.x, y=point.z, heading=hdg } },
name = 'CTLD_'..name
}
end
end
local function multiUnits(units)
-- units: array of { type, dx, dz }
return function(point, headingDeg)
local hdg = math.rad(headingDeg or 0)
local function off(dx, dz) return { x = point.x + dx, z = point.z + dz } end
local list = {}
for i,u in ipairs(units) do
local p = off(u.dx or 0, u.dz or 3*i)
table.insert(list, {
type = u.type, name = string.format('CTLD-%s-%d', u.type, math.random(100000,999999)),
x = p.x, y = p.z, heading = hdg
})
end
return { visible=false, lateActivation=false, tasks={}, task='Ground Nothing', route={}, units=list, name=string.format('CTLD_SITE_%d', math.random(100000,999999)) }
end
end
local BLUE = coalition.side.BLUE
local RED = coalition.side.RED
local cat = {}
-- Combat Vehicles (BLUE)
cat['BLUE_M1128_STRYKER_MGS'] = { menuCategory='Combat Vehicles', menu='M1128 Stryker MGS', description='M1128 Stryker MGS', dcsCargoType='container_cargo', required=1, side=BLUE, category=Group.Category.GROUND, build=singleUnit('M1128 Stryker MGS') }
cat['BLUE_M60A3_PATTON'] = { menuCategory='Combat Vehicles', menu='M-60A3 Patton', description='M-60A3 Patton', dcsCargoType='container_cargo', required=1, side=BLUE, category=Group.Category.GROUND, build=singleUnit('M-60') }
cat['BLUE_HMMWV_TOW'] = { menuCategory='Combat Vehicles', menu='Humvee - TOW', description='Humvee - TOW', dcsCargoType='container_cargo', required=1, side=BLUE, category=Group.Category.GROUND, build=singleUnit('M1045 HMMWV TOW') }
cat['BLUE_M1134_STRYKER_ATGM']= { menuCategory='Combat Vehicles', menu='M1134 Stryker ATGM',description='M1134 Stryker ATGM',dcsCargoType='container_cargo', required=1, side=BLUE, category=Group.Category.GROUND, build=singleUnit('M1134 Stryker ATGM') }
cat['BLUE_LAV25'] = { menuCategory='Combat Vehicles', menu='LAV-25', description='LAV-25', dcsCargoType='container_cargo', required=1, side=BLUE, category=Group.Category.GROUND, build=singleUnit('LAV-25') }
cat['BLUE_M2A2_BRADLEY'] = { menuCategory='Combat Vehicles', menu='M2A2 Bradley', description='M2A2 Bradley', dcsCargoType='container_cargo', required=1, side=BLUE, category=Group.Category.GROUND, build=singleUnit('M-2 Bradley') }
cat['BLUE_VAB_MEPHISTO'] = { menuCategory='Combat Vehicles', menu='ATGM VAB Mephisto', description='ATGM VAB Mephisto', dcsCargoType='container_cargo', required=1, side=BLUE, category=Group.Category.GROUND, build=singleUnit('VAB_Mephisto') }
cat['BLUE_M1A2C_ABRAMS'] = { menuCategory='Combat Vehicles', menu='M1A2C Abrams', description='M1A2C Abrams', dcsCargoType='container_cargo', required=1, side=BLUE, category=Group.Category.GROUND, build=singleUnit('M1A2C_SEP_V3') }
-- Combat Vehicles (RED)
cat['RED_BTR82A'] = { menuCategory='Combat Vehicles', menu='BTR-82A', description='BTR-82A', dcsCargoType='container_cargo', required=1, side=RED, category=Group.Category.GROUND, build=singleUnit('BTR-82A') }
cat['RED_BRDM2'] = { menuCategory='Combat Vehicles', menu='BRDM-2', description='BRDM-2', dcsCargoType='container_cargo', required=1, side=RED, category=Group.Category.GROUND, build=singleUnit('BRDM-2') }
cat['RED_BMP3'] = { menuCategory='Combat Vehicles', menu='BMP-3', description='BMP-3', dcsCargoType='container_cargo', required=1, side=RED, category=Group.Category.GROUND, build=singleUnit('BMP-3') }
cat['RED_T55'] = { menuCategory='Combat Vehicles', menu='T-55', description='T-55', dcsCargoType='container_cargo', required=1, side=RED, category=Group.Category.GROUND, build=singleUnit('T-55') }
cat['RED_T72B3'] = { menuCategory='Combat Vehicles', menu='T-72B3', description='T-72B3', dcsCargoType='container_cargo', required=1, side=RED, category=Group.Category.GROUND, build=singleUnit('T-72B3') }
-- Support (BLUE)
cat['BLUE_MRAP_JTAC'] = { menuCategory='Support', menu='MRAP - JTAC', description='JTAC MRAP', dcsCargoType='container_cargo', required=1, side=BLUE, category=Group.Category.GROUND, build=singleUnit('MaxxPro_MRAP') }
cat['BLUE_M818_AMMO'] = { menuCategory='Support', menu='M-818 Ammo Truck', description='M-818 Ammo Truck', dcsCargoType='container_cargo', required=1, side=BLUE, category=Group.Category.GROUND, build=singleUnit('M 818') }
cat['BLUE_M978_TANKER'] = { menuCategory='Support', menu='M-978 Tanker', description='M-978 Tanker', dcsCargoType='container_cargo', required=1, side=BLUE, category=Group.Category.GROUND, build=singleUnit('M978 HEMTT Tanker') }
cat['BLUE_EWR_FPS117'] = { menuCategory='Support', menu='EWR Radar FPS-117', description='EWR Radar FPS-117', dcsCargoType='container_cargo', required=1, side=BLUE, category=Group.Category.GROUND, build=singleUnit('FPS-117') }
-- Support (RED)
cat['RED_TIGR_JTAC'] = { menuCategory='Support', menu='Tigr - JTAC', description='JTAC Tigr', dcsCargoType='container_cargo', required=1, side=RED, category=Group.Category.GROUND, build=singleUnit('Tigr_233036') }
cat['RED_URAL4320_AMMO'] = { menuCategory='Support', menu='Ural-4320-31 Ammo Truck', description='Ural-4320-31 Ammo Truck', dcsCargoType='container_cargo', required=1, side=RED, category=Group.Category.GROUND, build=singleUnit('Ural-4320-31') }
cat['RED_ATZ10_TANKER'] = { menuCategory='Support', menu='ATZ-10 Refueler', description='ATZ-10 Refueler', dcsCargoType='container_cargo', required=1, side=RED, category=Group.Category.GROUND, build=singleUnit('ATZ-10') }
cat['RED_EWR_1L13'] = { menuCategory='Support', menu='EWR Radar 1L13', description='EWR Radar 1L13', dcsCargoType='container_cargo', required=1, side=RED, category=Group.Category.GROUND, build=singleUnit('1L13 EWR') }
-- Artillery (BLUE)
cat['BLUE_MLRS'] = { menuCategory='Artillery', menu='MLRS', description='MLRS', dcsCargoType='container_cargo', required=2, side=BLUE, category=Group.Category.GROUND, build=singleUnit('MLRS') }
cat['BLUE_SMERCH_CM'] = { menuCategory='Artillery', menu='Smerch_CM', description='Smerch (CM)', dcsCargoType='container_cargo', required=2, side=BLUE, category=Group.Category.GROUND, build=singleUnit('Smerch') }
cat['BLUE_L118_105MM'] = { menuCategory='Artillery', menu='L118 Light Artillery 105mm', description='L118 105mm', dcsCargoType='container_cargo', required=1, side=BLUE, category=Group.Category.GROUND, build=singleUnit('L118_Unit') }
cat['BLUE_SMERCH_HE'] = { menuCategory='Artillery', menu='Smerch_HE', description='Smerch (HE)', dcsCargoType='container_cargo', required=2, side=BLUE, category=Group.Category.GROUND, build=singleUnit('Smerch_HE') }
cat['BLUE_M109'] = { menuCategory='Artillery', menu='M-109', description='M-109', dcsCargoType='container_cargo', required=2, side=BLUE, category=Group.Category.GROUND, build=singleUnit('M-109') }
-- Artillery (RED)
cat['RED_GVOZDika'] = { menuCategory='Artillery', menu='SAU Gvozdika', description='SAU Gvozdika', dcsCargoType='container_cargo', required=2, side=RED, category=Group.Category.GROUND, build=singleUnit('SAU Gvozdika') }
cat['RED_2S19_MSTA'] = { menuCategory='Artillery', menu='SPH 2S19 Msta', description='SPH 2S19 Msta', dcsCargoType='container_cargo', required=2, side=RED, category=Group.Category.GROUND, build=singleUnit('SAU Msta') }
cat['RED_URAGAN_BM27'] = { menuCategory='Artillery', menu='Uragan_BM-27', description='Uragan BM-27', dcsCargoType='container_cargo', required=2, side=RED, category=Group.Category.GROUND, build=singleUnit('Uragan_BM-27') }
cat['RED_BM21_GRAD'] = { menuCategory='Artillery', menu='BM-21 Grad Ural', description='BM-21 Grad Ural', dcsCargoType='container_cargo', required=2, side=RED, category=Group.Category.GROUND, build=singleUnit('Grad-URAL') }
cat['RED_PLZ05'] = { menuCategory='Artillery', menu='PLZ-05 Mobile Artillery', description='PLZ-05', dcsCargoType='container_cargo', required=2, side=RED, category=Group.Category.GROUND, build=singleUnit('PLZ05') }
-- AAA (BLUE)
cat['BLUE_GEPARD'] = { menuCategory='AAA', menu='Gepard AAA', description='Gepard AAA', dcsCargoType='container_cargo', required=1, side=BLUE, category=Group.Category.GROUND, build=singleUnit('Gepard') }
cat['BLUE_CRAM'] = { menuCategory='AAA', menu='LPWS C-RAM', description='LPWS C-RAM', dcsCargoType='container_cargo', required=1, side=BLUE, category=Group.Category.GROUND, build=singleUnit('HEMTT_C-RAM_Phalanx') }
cat['BLUE_VULCAN_M163'] = { menuCategory='AAA', menu='SPAAA Vulcan M163', description='Vulcan M163', dcsCargoType='container_cargo', required=1, side=BLUE, category=Group.Category.GROUND, build=singleUnit('Vulcan') }
cat['BLUE_BOFORS40'] = { menuCategory='AAA', menu='Bofors 40mm', description='Bofors 40mm', dcsCargoType='container_cargo', required=1, side=BLUE, category=Group.Category.GROUND, build=singleUnit('bofors40') }
-- AAA (RED)
cat['RED_URAL_ZU23'] = { menuCategory='AAA', menu='Ural-375 ZU-23', description='Ural-375 ZU-23', dcsCargoType='container_cargo', required=1, side=RED, category=Group.Category.GROUND, build=singleUnit('Ural-375 ZU-23') }
cat['RED_SHILKA'] = { menuCategory='AAA', menu='ZSU-23-4 Shilka', description='ZSU-23-4 Shilka', dcsCargoType='container_cargo', required=1, side=RED, category=Group.Category.GROUND, build=singleUnit('ZSU-23-4 Shilka') }
cat['RED_ZSU57_2'] = { menuCategory='AAA', menu='ZSU_57_2', description='ZSU_57_2', dcsCargoType='container_cargo', required=1, side=RED, category=Group.Category.GROUND, build=singleUnit('ZSU_57_2') }
-- SAM short range (BLUE)
cat['BLUE_M1097_AVENGER'] = { menuCategory='SAM short range', menu='M1097 Avenger', description='M1097 Avenger', dcsCargoType='container_cargo', required=2, side=BLUE, category=Group.Category.GROUND, build=singleUnit('M1097 Avenger') }
cat['BLUE_M48_CHAPARRAL'] = { menuCategory='SAM short range', menu='M48 Chaparral', description='M48 Chaparral', dcsCargoType='container_cargo', required=2, side=BLUE, category=Group.Category.GROUND, build=singleUnit('M48 Chaparral') }
cat['BLUE_ROLAND_ADS'] = { menuCategory='SAM short range', menu='Roland ADS', description='Roland ADS', dcsCargoType='container_cargo', required=2, side=BLUE, category=Group.Category.GROUND, build=singleUnit('Roland ADS') }
cat['BLUE_M6_LINEBACKER'] = { menuCategory='SAM short range', menu='M6 Linebacker', description='M6 Linebacker', dcsCargoType='container_cargo', required=1, side=BLUE, category=Group.Category.GROUND, build=singleUnit('M6 Linebacker') }
-- Rapier components and site
cat['BLUE_RAPIER_LN'] = { menuCategory='SAM short range', menu='Rapier Launcher', description='Rapier Launcher', dcsCargoType='container_cargo', required=1, side=BLUE, category=Group.Category.GROUND, build=singleUnit('rapier_fsa_launcher') }
cat['BLUE_RAPIER_SR'] = { menuCategory='SAM short range', menu='Rapier SR', description='Rapier SR', dcsCargoType='container_cargo', required=1, side=BLUE, category=Group.Category.GROUND, build=singleUnit('rapier_fsa_blindfire_radar') }
cat['BLUE_RAPIER_TR'] = { menuCategory='SAM short range', menu='Rapier Tracker', description='Rapier Tracker', dcsCargoType='container_cargo', required=1, side=BLUE, category=Group.Category.GROUND, build=singleUnit('rapier_fsa_optical_tracker_unit') }
cat['BLUE_RAPIER_SITE'] = { menuCategory='SAM short range', menu='Rapier - All crates', description='Rapier Site', dcsCargoType='container_cargo', requires={ BLUE_RAPIER_LN=1, BLUE_RAPIER_SR=1, BLUE_RAPIER_TR=1 }, side=BLUE, category=Group.Category.GROUND,
build=multiUnits({ {type='rapier_fsa_launcher'}, {type='rapier_fsa_blindfire_radar', dx=12, dz=6}, {type='rapier_fsa_optical_tracker_unit', dx=-12, dz=6} }) }
-- SAM short range (RED)
cat['RED_OSA_9K33'] = { menuCategory='SAM short range', menu='9K33 Osa', description='9K33 Osa', dcsCargoType='container_cargo', required=2, side=RED, category=Group.Category.GROUND, build=singleUnit('Osa 9A33 ln') }
cat['RED_STRELA1_9P31'] = { menuCategory='SAM short range', menu='9P31 Strela-1', description='9P31 Strela-1', dcsCargoType='container_cargo', required=2, side=RED, category=Group.Category.GROUND, build=singleUnit('Strela-1 9P31') }
cat['RED_TUNGUSKA_2S6'] = { menuCategory='SAM short range', menu='2K22 Tunguska', description='2K22 Tunguska', dcsCargoType='container_cargo', required=2, side=RED, category=Group.Category.GROUND, build=singleUnit('2S6 Tunguska') }
cat['RED_STRELA10M3'] = { menuCategory='SAM short range', menu='SA-13 Strela-10M3', description='SA-13 Strela-10M3', dcsCargoType='container_cargo', required=2, side=RED, category=Group.Category.GROUND, build=singleUnit('Strela-10M3') }
-- HQ-7 components and site
cat['RED_HQ7_LN'] = { menuCategory='SAM short range', menu='HQ-7_Launcher', description='HQ-7 Launcher', dcsCargoType='container_cargo', required=2, side=RED, category=Group.Category.GROUND, build=singleUnit('HQ-7_LN_SP') }
cat['RED_HQ7_STR'] = { menuCategory='SAM short range', menu='HQ-7_STR_SP', description='HQ-7 STR', dcsCargoType='container_cargo', required=1, side=RED, category=Group.Category.GROUND, build=singleUnit('HQ-7_STR_SP') }
cat['RED_HQ7_SITE'] = { menuCategory='SAM short range', menu='HQ-7 - All crates', description='HQ-7 Site', dcsCargoType='container_cargo', requires={ RED_HQ7_LN=1, RED_HQ7_STR=1 }, side=RED, category=Group.Category.GROUND,
build=multiUnits({ {type='HQ-7_LN_SP'}, {type='HQ-7_STR_SP', dx=10, dz=8} }) }
-- SAM mid range (BLUE) HAWK + NASAMS
cat['BLUE_HAWK_LN'] = { menuCategory='SAM mid range', menu='HAWK Launcher', description='HAWK Launcher', dcsCargoType='container_cargo', required=1, side=BLUE, category=Group.Category.GROUND, build=singleUnit('Hawk ln') }
cat['BLUE_HAWK_SR'] = { menuCategory='SAM mid range', menu='HAWK Search Radar', description='HAWK SR', dcsCargoType='container_cargo', required=1, side=BLUE, category=Group.Category.GROUND, build=singleUnit('Hawk sr') }
cat['BLUE_HAWK_TR'] = { menuCategory='SAM mid range', menu='HAWK Track Radar', description='HAWK TR', dcsCargoType='container_cargo', required=1, side=BLUE, category=Group.Category.GROUND, build=singleUnit('Hawk tr') }
cat['BLUE_HAWK_PCP'] = { menuCategory='SAM mid range', menu='HAWK PCP', description='HAWK PCP', dcsCargoType='container_cargo', required=1, side=BLUE, category=Group.Category.GROUND, build=singleUnit('Hawk pcp') }
cat['BLUE_HAWK_CWAR'] = { menuCategory='SAM mid range', menu='HAWK CWAR', description='HAWK CWAR', dcsCargoType='container_cargo', required=1, side=BLUE, category=Group.Category.GROUND, build=singleUnit('Hawk cwar') }
cat['BLUE_HAWK_SITE'] = { menuCategory='SAM mid range', menu='HAWK - All crates', description='HAWK Site', dcsCargoType='container_cargo', requires={ BLUE_HAWK_LN=1, BLUE_HAWK_SR=1, BLUE_HAWK_TR=1, BLUE_HAWK_PCP=1, BLUE_HAWK_CWAR=1 }, side=BLUE, category=Group.Category.GROUND,
build=multiUnits({ {type='Hawk ln'}, {type='Hawk sr', dx=12, dz=8}, {type='Hawk tr', dx=-12, dz=8}, {type='Hawk pcp', dx=18, dz=12}, {type='Hawk cwar', dx=-18, dz=12} }) }
cat['BLUE_NASAMS_LN'] = { menuCategory='SAM mid range', menu='NASAMS Launcher 120C', description='NASAMS LN 120C', dcsCargoType='container_cargo', required=1, side=BLUE, category=Group.Category.GROUND, build=singleUnit('NASAMS_LN_C') }
cat['BLUE_NASAMS_RADAR'] = { menuCategory='SAM mid range', menu='NASAMS Search/Track Radar', description='NASAMS Radar', dcsCargoType='container_cargo', required=1, side=BLUE, category=Group.Category.GROUND, build=singleUnit('NASAMS_Radar_MPQ64F1') }
cat['BLUE_NASAMS_CP'] = { menuCategory='SAM mid range', menu='NASAMS Command Post', description='NASAMS CP', dcsCargoType='container_cargo', required=1, side=BLUE, category=Group.Category.GROUND, build=singleUnit('NASAMS_Command_Post') }
cat['BLUE_NASAMS_SITE'] = { menuCategory='SAM mid range', menu='NASAMS - All crates', description='NASAMS Site', dcsCargoType='container_cargo', requires={ BLUE_NASAMS_LN=1, BLUE_NASAMS_RADAR=1, BLUE_NASAMS_CP=1 }, side=BLUE, category=Group.Category.GROUND,
build=multiUnits({ {type='NASAMS_LN_C'}, {type='NASAMS_Radar_MPQ64F1', dx=12, dz=8}, {type='NASAMS_Command_Post', dx=-12, dz=8} }) }
-- SAM mid range (RED) KUB
cat['RED_KUB_LN'] = { menuCategory='SAM mid range', menu='KUB Launcher', description='KUB Launcher', dcsCargoType='container_cargo', required=1, side=RED, category=Group.Category.GROUND, build=singleUnit('Kub 2P25 ln') }
cat['RED_KUB_RADAR'] = { menuCategory='SAM mid range', menu='KUB Radar', description='KUB Radar', dcsCargoType='container_cargo', required=1, side=RED, category=Group.Category.GROUND, build=singleUnit('Kub 1S91 str') }
cat['RED_KUB_SITE'] = { menuCategory='SAM mid range', menu='KUB - All crates', description='KUB Site', dcsCargoType='container_cargo', requires={ RED_KUB_LN=1, RED_KUB_RADAR=1 }, side=RED, category=Group.Category.GROUND,
build=multiUnits({ {type='Kub 2P25 ln'}, {type='Kub 1S91 str', dx=12, dz=8} }) }
-- SAM long range (BLUE) Patriot
cat['BLUE_PATRIOT_LN'] = { menuCategory='SAM long range', menu='Patriot Launcher', description='Patriot Launcher', dcsCargoType='container_cargo', required=1, side=BLUE, category=Group.Category.GROUND, build=singleUnit('Patriot ln') }
cat['BLUE_PATRIOT_RADAR'] = { menuCategory='SAM long range', menu='Patriot Radar', description='Patriot Radar', dcsCargoType='container_cargo', required=1, side=BLUE, category=Group.Category.GROUND, build=singleUnit('Patriot str') }
cat['BLUE_PATRIOT_ECS'] = { menuCategory='SAM long range', menu='Patriot ECS', description='Patriot ECS', dcsCargoType='container_cargo', required=1, side=BLUE, category=Group.Category.GROUND, build=singleUnit('Patriot ECS') }
cat['BLUE_PATRIOT_SITE'] = { menuCategory='SAM long range', menu='Patriot - All crates', description='Patriot Site', dcsCargoType='container_cargo', requires={ BLUE_PATRIOT_LN=1, BLUE_PATRIOT_RADAR=1, BLUE_PATRIOT_ECS=1 }, side=BLUE, category=Group.Category.GROUND,
build=multiUnits({ {type='Patriot ln'}, {type='Patriot str', dx=14, dz=10}, {type='Patriot ECS', dx=-14, dz=10} }) }
-- SAM long range (RED) BUK
cat['RED_BUK_LN'] = { menuCategory='SAM long range', menu='BUK Launcher', description='BUK Launcher', dcsCargoType='container_cargo', required=1, side=RED, category=Group.Category.GROUND, build=singleUnit('SA-11 Buk LN 9A310M1') }
cat['RED_BUK_SR'] = { menuCategory='SAM long range', menu='BUK Search Radar', description='BUK Search Radar', dcsCargoType='container_cargo', required=1, side=RED, category=Group.Category.GROUND, build=singleUnit('SA-11 Buk SR 9S18M1') }
cat['RED_BUK_CC'] = { menuCategory='SAM long range', menu='BUK CC Radar', description='BUK CC Radar', dcsCargoType='container_cargo', required=1, side=RED, category=Group.Category.GROUND, build=singleUnit('SA-11 Buk CC 9S470M1') }
cat['RED_BUK_SITE'] = { menuCategory='SAM long range', menu='BUK - All crates', description='BUK Site', dcsCargoType='container_cargo', requires={ RED_BUK_LN=1, RED_BUK_SR=1, RED_BUK_CC=1 }, side=RED, category=Group.Category.GROUND,
build=multiUnits({ {type='SA-11 Buk LN 9A310M1'}, {type='SA-11 Buk SR 9S18M1', dx=12, dz=8}, {type='SA-11 Buk CC 9S470M1', dx=-12, dz=8} }) }
-- Drones (JTAC)
cat['BLUE_MQ9'] = { menuCategory='Drones', menu='MQ-9 Reaper - JTAC', description='MQ-9 JTAC', dcsCargoType='container_cargo', required=1, side=BLUE, category=Group.Category.AIRPLANE, build=singleUnit('MQ-9 Reaper') }
cat['RED_WINGLOONG'] = { menuCategory='Drones', menu='WingLoong-I - JTAC', description='WingLoong-I JTAC', dcsCargoType='container_cargo', required=1, side=RED, category=Group.Category.AIRPLANE, build=singleUnit('WingLoong-I') }
-- FOB crates (Support) — three small crates build a FOB site
cat['FOB_SMALL'] = { menuCategory='Support', menu='FOB Crate - Small', description='FOB small crate', dcsCargoType='container_cargo', required=1, side=nil, category=Group.Category.GROUND, build=function(point, headingDeg)
-- spawns a harmless placeholder truck for visibility; consumed by FOB_SITE build
return singleUnit('Ural-375')(point, headingDeg)
end }
cat['FOB_SITE'] = { menuCategory='Support', menu='FOB Crates - All', description='FOB Site', dcsCargoType='container_cargo', requires={ FOB_SMALL=3 }, side=nil, category=Group.Category.GROUND,
build=multiUnits({ {type='HEMTT TFFT'}, {type='Ural-375 PBU', dx=10, dz=8}, {type='Ural-375', dx=-10, dz=8} }) }
return cat

View File

@ -0,0 +1,44 @@
-- init_example.lua (optional dev harness snippet)
-- Load Moose before this. Then do:
-- dofile(lfs.writedir()..[[Scripts\Moose_CTLD_Pure\init_example.lua]])
local CTLD = dofile(lfs.writedir()..[[Scripts\Moose_CTLD_Pure\Moose_CTLD.lua]])
local ctld = CTLD:New({
CoalitionSide = coalition.side.BLUE,
Zones = {
PickupZones = { { name = 'PICKUP_BLUE_MAIN' } },
DropZones = { { name = 'DROP_BLUE_1' } },
},
})
-- Load the extracted CTLD-based crate catalog
local extracted = dofile(lfs.writedir()..[[Scripts\Moose_CTLD_Pure\catalogs\CrateCatalog_CTLD_Extract.lua]])
ctld:MergeCatalog(extracted)
-- Register a SAM site built from 4 crates
ctld:RegisterCrate('IR-SAM', {
description = '4x crates -> IR SAM site',
weight = 300,
dcsCargoType = 'container_cargo',
required = 4,
side = coalition.side.BLUE,
category = Group.Category.GROUND,
build = function(point, headingDeg)
local hdg = math.rad(headingDeg or 0)
local function off(dx, dz) return { x=point.x+dx, z=point.z+dz } end
local units = {
{ type='Strela-10M3', name='CTLD-SAM-1', x=point.x, y=point.z, heading=hdg },
{ type='Ural-375', name='CTLD-SAM-SUP', x=off(10,12).x, y=off(10,12).z, heading=hdg },
}
return { visible=false, lateActivation=false, units=units, name='CTLD_SAM_'..math.random(100000,999999) }
end,
})
-- FAC/RECCE setup
local FAC = dofile(lfs.writedir()..[[Scripts\Moose_CTLD_Pure\Moose_CTLD_FAC.lua]])
local fac = FAC:New(ctld, {
CoalitionSide = coalition.side.BLUE,
Arty = { Enabled=true, Groups={'BLUE_ARTY_1'}, Rounds=2, Spread=80 },
})
fac:AddRecceZone({ name = 'RECCE_ZONE_1' })
fac:Run()

View File

@ -0,0 +1,83 @@
# Download latest Moose_.lua from GitHub
[CmdletBinding()]
param(
# Destination path for Moose_.lua. Defaults to repo root alongside Patch-MooseMissions folder
[Parameter(Mandatory=$false)]
[string]$MooseLuaPath = (Join-Path (Split-Path $PSScriptRoot -Parent) 'Moose_.lua'),
# Use -Force to actually download; otherwise runs in WhatIf/preview mode
[Parameter(Mandatory=$false)]
[switch]$Force
)
# Determine WhatIf mode (preview by default)
$runInWhatIfMode = -not $Force
# Ensure destination directory exists (avoid null/invalid path issues)
if ([string]::IsNullOrWhiteSpace($MooseLuaPath)) {
throw "Destination path for Moose_.lua is empty. Provide -MooseLuaPath."
}
$destDir = Split-Path -Path $MooseLuaPath -Parent
if (-not [string]::IsNullOrWhiteSpace($destDir) -and -not (Test-Path $destDir)) {
New-Item -Path $destDir -ItemType Directory -Force -WhatIf:$false | Out-Null
}
# Enable TLS 1.2 for GitHub downloads on older environments
try {
[System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor [System.Net.SecurityProtocolType]::Tls12
} catch {}
if (-not $runInWhatIfMode) {
Write-Host "Downloading latest Moose_.lua from GitHub..." -ForegroundColor Yellow
$mooseGitHubUrl = "https://raw.githubusercontent.com/FlightControl-Master/MOOSE_INCLUDE/develop/Moose_Include_Static/Moose_.lua"
$backupPath = "$MooseLuaPath.backup"
try {
# Create a single backup of existing Moose_.lua if it exists and backup doesn't exist yet
if ((Test-Path $MooseLuaPath) -and -not (Test-Path $backupPath)) {
Write-Host " Creating backup: $backupPath" -ForegroundColor Gray
Copy-Item $MooseLuaPath $backupPath -Force -WhatIf:$false
}
# Download the file
$ProgressPreference = 'SilentlyContinue' # Speeds up Invoke-WebRequest
Invoke-WebRequest -Uri $mooseGitHubUrl -OutFile $MooseLuaPath -ErrorAction Stop
$ProgressPreference = 'Continue'
# Verify the download
if (Test-Path $MooseLuaPath) {
$fileSize = (Get-Item $MooseLuaPath).Length
Write-Host " SUCCESS: Downloaded Moose_.lua ($([math]::Round($fileSize/1MB, 2)) MB)" -ForegroundColor Green
} else {
throw "Downloaded file not found after download"
}
}
catch {
Write-Error "Failed to download Moose_.lua from GitHub: $_"
Write-Host " URL: $mooseGitHubUrl" -ForegroundColor Red
# Check if we have a backup or existing file to use
if (Test-Path $MooseLuaPath) {
Write-Warning "Using existing Moose_.lua file instead"
} else {
Write-Error "No Moose_.lua file available. Cannot proceed."
exit 1
}
}
Write-Host ""
} else {
Write-Host "WHATIF: Would download latest Moose_.lua from GitHub" -ForegroundColor Magenta
Write-Host " URL: https://raw.githubusercontent.com/FlightControl-Master/MOOSE_INCLUDE/develop/Moose_Include_Static/Moose_.lua" -ForegroundColor Gray
Write-Host " Destination: $MooseLuaPath" -ForegroundColor Gray
# Still need to verify file exists for WhatIf to show what would be patched
if (-not (Test-Path $MooseLuaPath)) {
Write-Warning "Moose_.lua not found at $MooseLuaPath - run with -Force to download it"
Write-Warning "Continuing with WhatIf to show which missions would be processed..."
}
Write-Host ""
}

View File

@ -25,6 +25,27 @@ PowerShell script to automatically patch DCS mission files (.miz) with updated L
## Usage
### Get the latest Moose_.lua
Use the helper script to fetch the latest Moose_.lua into the repo root (C:\DCS_MissionDev\Moose_.lua by default):
```powershell
# Preview (no changes)
./Download-MooseInclude.ps1
# Actually download
./Download-MooseInclude.ps1 -Force
# Optional: choose a different destination
./Download-MooseInclude.ps1 -Force -MooseLuaPath 'D:\Scripts\Moose_.lua'
```
If your system blocks script execution, temporarily allow scripts for the current session only:
```powershell
Set-ExecutionPolicy -Scope Process -ExecutionPolicy Bypass
```
### Basic Usage
Update a mission with automatic version increment: