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

@@ -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()