diff --git a/Moose_CTLD_Pure/Moose_CTLD.lua b/Moose_CTLD_Pure/Moose_CTLD.lua index 684a442..65873b1 100644 --- a/Moose_CTLD_Pure/Moose_CTLD.lua +++ b/Moose_CTLD_Pure/Moose_CTLD.lua @@ -37,6 +37,18 @@ CTLD.Config = { Debug = false, PickupZoneSmokeColor = trigger.smokeColor.Green, -- default smoke color when spawning crates at pickup zones + -- Hover pickup configuration (Ciribob-style inspired) + HoverPickup = { + Enabled = true, -- if true, auto-load the nearest crate when hovering close enough for a duration + Height = 3, -- meters AGL threshold for hover pickup + Radius = 15, -- meters horizontal distance to crate to consider for pickup + AutoPickupDistance = 25, -- meters max search distance for candidate crates + Duration = 3, -- seconds of continuous hover before loading occurs + MaxCratesPerLoad = 6, -- maximum crates the aircraft can carry simultaneously + RequireLowSpeed = true, -- require near-stationary hover + MaxSpeedMPS = 5 -- max allowed speed in m/s for hover pickup + }, + Zones = { -- Optional: supply by name (ME trigger zones) or define coordinates inline PickupZones = { -- Examples: @@ -147,6 +159,9 @@ CTLD.Config = { CTLD._instances = CTLD._instances or {} CTLD._crates = {} -- [crateName] = { key, zone, side, spawnTime, point } CTLD._troopsLoaded = {} -- [groupName] = { count, typeKey } +CTLD._loadedCrates = {} -- [groupName] = { total=n, byKey = { key -> count } } +CTLD._hoverState = {} -- [unitName] = { targetCrate=name, startTime=t } +CTLD._unitLast = {} -- [unitName] = { x, z, t } -- ========================= -- Utilities @@ -290,6 +305,13 @@ function CTLD:New(cfg) end, {}, 10, 10) -- check every 10 seconds end + -- Optional: hover pickup scanner + if o.Config.HoverPickup and o.Config.HoverPickup.Enabled then + o.HoverSched = SCHEDULER:New(nil, function() + o:ScanHoverPickup() + end, {}, 0.5, 0.5) + end + table.insert(CTLD._instances, o) _msgCoalition(o.Side, string.format('CTLD %s initialized for coalition', CTLD.Version)) return o @@ -389,6 +411,14 @@ function CTLD:BuildGroupMenus(group) self:BuildAtGroup(group) end) + -- Crate management (loaded crates) + MENU_GROUP_COMMAND:New(group, 'Drop One Loaded Crate', root, function() + self:DropLoadedCrates(group, 1) + end) + MENU_GROUP_COMMAND:New(group, 'Drop All Loaded Crates', root, function() + self:DropLoadedCrates(group, -1) + end) + return root end @@ -486,9 +516,26 @@ function CTLD:BuildAtGroup(group) counts[c.meta.key] = (counts[c.meta.key] or 0) + 1 end + -- Include loaded crates carried by this group + local gname = group:GetName() + local carried = CTLD._loadedCrates[gname] + if carried and carried.byKey then + for k,v in pairs(carried.byKey) do + counts[k] = (counts[k] or 0) + v + end + end + -- Helper to consume crates of a given key/qty local function consumeCrates(key, qty) local removed = 0 + -- Consume from loaded crates first + if carried and carried.byKey and (carried.byKey[key] or 0) > 0 then + local take = math.min(qty, carried.byKey[key]) + carried.byKey[key] = carried.byKey[key] - take + if carried.byKey[key] <= 0 then carried.byKey[key] = nil end + carried.total = math.max(0, (carried.total or 0) - take) + removed = removed + take + end for _,c in ipairs(nearby) do if removed >= qty then break end if c.meta.key == key then @@ -559,6 +606,128 @@ function CTLD:BuildAtGroup(group) _msgGroup(group, 'Insufficient crates to build any asset here') end +-- ========================= +-- Loaded crate management +-- ========================= +function CTLD:_addLoadedCrate(group, crateKey) + local gname = group:GetName() + CTLD._loadedCrates[gname] = CTLD._loadedCrates[gname] or { total = 0, byKey = {} } + local lc = CTLD._loadedCrates[gname] + lc.total = lc.total + 1 + lc.byKey[crateKey] = (lc.byKey[crateKey] or 0) + 1 +end + +function CTLD:DropLoadedCrates(group, howMany) + local gname = group:GetName() + local lc = CTLD._loadedCrates[gname] + if not lc or (lc.total or 0) == 0 then _msgGroup(group, 'No loaded crates to drop') 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 toDrop = (howMany and howMany > 0) and howMany or lc.total + -- Drop in key order + for k,count in pairs(BASE:DeepCopy(lc.byKey)) do + if toDrop <= 0 then break end + local dropNow = math.min(count, toDrop) + for i=1,dropNow do + local cname = string.format('CTLD_CRATE_%s_%d', k, math.random(100000,999999)) + local cat = self.Config.CrateCatalog[k] + _spawnStaticCargo(self.Side, here, (cat and cat.dcsCargoType) or 'uh1h_cargo', cname) + CTLD._crates[cname] = { key = k, side = self.Side, spawnTime = timer.getTime(), point = { x = here.x, z = here.z } } + lc.byKey[k] = lc.byKey[k] - 1 + if lc.byKey[k] <= 0 then lc.byKey[k] = nil end + lc.total = lc.total - 1 + toDrop = toDrop - 1 + if toDrop <= 0 then break end + end + end + _msgGroup(group, 'Dropped loaded crates') +end + +-- ========================= +-- Hover pickup scanner +-- ========================= +function CTLD:ScanHoverPickup() + local hp = self.Config.HoverPickup or {} + if not hp.Enabled then return end + -- iterate all groups that have menus (active transports) + for gname,root in pairs(self.MenusByGroup or {}) do + local group = GROUP:FindByName(gname) + if group and group:IsAlive() then + local unit = group:GetUnit(1) + if unit and unit:IsAlive() then + -- Allowed type check + local typ = _getUnitType(unit) + if _isIn(self.Config.AllowedAircraft, typ) then + local p3 = unit:GetPointVec3() + local agl = 0 + if land and land.getHeight then + agl = math.max(0, p3.y - land.getHeight({ x = p3.x, y = p3.z })) + else + agl = p3.y -- fallback + end + -- speed estimate + local uname = unit:GetName() + local now = timer.getTime() + local last = CTLD._unitLast[uname] + local speed = 0 + if last and (now > (last.t or 0)) then + local dx = (p3.x - last.x) + local dz = (p3.z - last.z) + local dt = now - last.t + if dt > 0 then speed = math.sqrt(dx*dx + dz*dz) / dt end + end + CTLD._unitLast[uname] = { x = p3.x, z = p3.z, t = now } + + local speedOK = (not hp.RequireLowSpeed) or (speed <= (hp.MaxSpeedMPS or 5)) + local heightOK = agl <= (hp.Height or 3) + if speedOK and heightOK then + -- find nearest crate within AutoPickupDistance + local bestName, bestMeta, bestd + local maxd = hp.AutoPickupDistance or hp.Radius or 25 + for name,meta in pairs(CTLD._crates) do + if meta.side == self.Side then + local dx = (meta.point.x - p3.x) + local dz = (meta.point.z - p3.z) + local d = math.sqrt(dx*dx + dz*dz) + if d <= maxd and ((not bestd) or d < bestd) then + bestName, bestMeta, bestd = name, meta, d + end + end + end + if bestName and bestMeta and bestd <= (hp.Radius or maxd) then + -- check capacity + local carried = CTLD._loadedCrates[gname] + local total = carried and carried.total or 0 + if total < (hp.MaxCratesPerLoad or 6) then + local hs = CTLD._hoverState[uname] + if not hs or hs.targetCrate ~= bestName then + CTLD._hoverState[uname] = { targetCrate = bestName, startTime = now } + else + if (now - hs.startTime) >= (hp.Duration or 3) then + -- load it + local obj = StaticObject.getByName(bestName) + if obj then obj:destroy() end + CTLD._crates[bestName] = nil + self:_addLoadedCrate(group, bestMeta.key) + _msgGroup(group, string.format('Loaded %s crate', tostring(bestMeta.key))) + CTLD._hoverState[uname] = nil + end + end + end + else + CTLD._hoverState[uname] = nil + end + else + CTLD._hoverState[uname] = nil + end + end + end + end + end +end + -- ========================= -- Troops -- ========================= diff --git a/Moose_CTLD_Pure/catalogs/CrateCatalog_CTLD_Extract.lua b/Moose_CTLD_Pure/catalogs/Moose_CTLD_Catalog.lua similarity index 100% rename from Moose_CTLD_Pure/catalogs/CrateCatalog_CTLD_Extract.lua rename to Moose_CTLD_Pure/catalogs/Moose_CTLD_Catalog.lua diff --git a/Moose_CTLD_Pure/init_example.lua b/Moose_CTLD_Pure/init_example.lua deleted file mode 100644 index 24c947c..0000000 --- a/Moose_CTLD_Pure/init_example.lua +++ /dev/null @@ -1,44 +0,0 @@ --- 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() diff --git a/Moose_CTLD_Pure/init_mission_dual_coalition.lua b/Moose_CTLD_Pure/init_mission_dual_coalition.lua index fabae9d..825e605 100644 --- a/Moose_CTLD_Pure/init_mission_dual_coalition.lua +++ b/Moose_CTLD_Pure/init_mission_dual_coalition.lua @@ -11,32 +11,36 @@ -- RED : PICKUP_RED_MAIN, DROP_RED_1, FOB_RED_A -- Adjust names below if you use different zone names. + -- Create CTLD for BLUE ctldBlue = _MOOSE_CTLD:New({ - CoalitionSide = coalition.side.BLUE, - UseCategorySubmenus = true, - UseBuiltinCatalog = false, -- rely on external catalog (recommended) - RestrictFOBToZones = true, - AutoBuildFOBInZones = true, - Zones = { - PickupZones = { { name = 'PICKUP_BLUE_MAIN' } }, - DropZones = { { name = 'DROP_BLUE_1' } }, - FOBZones = { { name = 'FOB_BLUE_A' } }, - }, + CoalitionSide = coalition.side.BLUE, + PickupZoneSmokeColor = trigger.smokeColor.Blue, + 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' + }, + + Zones = { + PickupZones = { { name = 'PICKUP_BLUE_MAIN', smoke = trigger.smokeColor.Blue } }, + --DropZones = { { name = 'DROP_BLUE_1' } }, + -- FOBZones = { { name = 'FOB_BLUE_A' } }, + }, }) -- Create CTLD for RED ctldRed = _MOOSE_CTLD:New({ - CoalitionSide = coalition.side.RED, - UseCategorySubmenus = true, - UseBuiltinCatalog = false, -- rely on external catalog (recommended) - RestrictFOBToZones = true, - AutoBuildFOBInZones = true, - Zones = { - PickupZones = { { name = 'PICKUP_RED_MAIN' } }, - DropZones = { { name = 'DROP_RED_1' } }, - FOBZones = { { name = 'FOB_RED_A' } }, - }, + CoalitionSide = coalition.side.RED, + PickupZoneSmokeColor = trigger.smokeColor.Red, + 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' + + }, + + Zones = { + PickupZones = { { name = 'PICKUP_RED_MAIN', smoke = trigger.smokeColor.Red } }, + --DropZones = { { name = 'DROP_RED_1' } }, + -- FOBZones = { { name = 'FOB_RED_A' } }, + }, }) -- If the external catalog was loaded (as _CTLD_EXTRACTED_CATALOG), both instances auto-merged it diff --git a/Moose_CTLD_Pure/init_mission_example.lua b/Moose_CTLD_Pure/init_mission_example.lua deleted file mode 100644 index 4830393..0000000 --- a/Moose_CTLD_Pure/init_mission_example.lua +++ /dev/null @@ -1,46 +0,0 @@ --- init_mission_example.lua --- Use this with Mission Editor triggers as DO SCRIPT (no paths) --- Load order (Mission Start): --- Option A (catalog first): --- 1) DO SCRIPT FILE: Moose.lua --- 2) DO SCRIPT FILE: Moose_CTLD_Pure/catalogs/CrateCatalog_CTLD_Extract.lua -- sets _CTLD_EXTRACTED_CATALOG --- 3) DO SCRIPT FILE: Moose_CTLD_Pure/Moose_CTLD.lua --- 4) DO SCRIPT FILE: Moose_CTLD_Pure/Moose_CTLD_FAC.lua (optional) --- 5) DO SCRIPT: paste the block below --- Option B (catalog after CTLD file): --- 1) DO SCRIPT FILE: Moose.lua --- 2) DO SCRIPT FILE: Moose_CTLD_Pure/Moose_CTLD.lua --- 3) DO SCRIPT FILE: Moose_CTLD_Pure/catalogs/CrateCatalog_CTLD_Extract.lua -- still fine: load before ctld:New() --- 4) DO SCRIPT FILE: Moose_CTLD_Pure/Moose_CTLD_FAC.lua (optional) --- 5) DO SCRIPT: paste the block below - --- create CTLD instance for BLUE -ctld = _MOOSE_CTLD:New({ - CoalitionSide = coalition.side.BLUE, - UseCategorySubmenus = true, - -- Set to false if you want ONLY the external catalog and no built-in examples - -- UseBuiltinCatalog = false, - RestrictFOBToZones = true, - AutoBuildFOBInZones = true, - Zones = { - PickupZones = { { name = 'PICKUP_BLUE_MAIN' } }, - DropZones = { { name = 'DROP_BLUE_1' } }, - FOBZones = { { name = 'FOB_ALPHA' } }, - }, -}) - --- No manual merge needed: Moose_CTLD auto-detects and merges global catalogs --- supported global names: _CTLD_EXTRACTED_CATALOG, CTLD_CATALOG, MOOSE_CTLD_CATALOG - --- set allowed transports (optional override) --- ctld:SetAllowedAircraft({'UH-1H','Mi-8MTV2','Mi-24P'}) - --- FAC/RECCE (optional) -if _MOOSE_CTLD_FAC then - fac = _MOOSE_CTLD_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() -end