diff --git a/Moose_CTLD_Pure/Moose_CTLD.lua b/Moose_CTLD_Pure/Moose_CTLD.lua index 72737f1..48f14bd 100644 --- a/Moose_CTLD_Pure/Moose_CTLD.lua +++ b/Moose_CTLD_Pure/Moose_CTLD.lua @@ -166,6 +166,33 @@ CTLD.Config = { RequirePickupZoneForCrateRequest = true, -- enforce that crate requests must be near a Supply (Pickup) Zone RequirePickupZoneForTroopLoad = true, -- if true, troops can only be loaded while inside a Supply (Pickup) Zone PickupZoneMaxDistance = 10000, -- meters; nearest pickup zone must be within this distance to allow a request + + -- Optional: draw zones on the F10 map using trigger.action.* markup (ME Draw-like) + MapDraw = { + Enabled = true, -- master switch for any map drawings created by this script + DrawPickupZones = true, -- draw Pickup/Supply zones as shaded circles with labels + DrawDropZones = false, -- optionally draw Drop zones + DrawFOBZones = false, -- optionally draw FOB zones + OutlineColor = {0, 1, 0, 0.85}, -- RGBA 0..1 for outlines (default green) + FillColor = {0, 1, 0, 0.15}, -- RGBA 0..1 for fill (light green shade) + LineType = 1, -- default line type if per-kind is not set (0 None, 1 Solid, 2 Dashed, 3 Dotted, 4 DotDash, 5 LongDash, 6 TwoDash) + LineTypes = { -- override border style per zone kind + Pickup = 3, -- dotted + Drop = 2, -- dashed + FOB = 4, -- dot-dash + }, + FontSize = 18, -- label text size + ReadOnly = true, -- prevent clients from removing the shapes + LabelPrefix = 'Pickup Zone' -- prefix for labels; zone name will be appended + }, + + -- Optional: bindings to mission flags to activate/deactivate zones via ME triggers + -- Each entry: { kind = 'Pickup'|'Drop'|'FOB', name = 'Zone Name', flag = 1001, activeWhen = 1 } + ZoneEventBindings = { + -- Example: + -- { kind = 'Pickup', name = 'Pickup Zone 1', flag = 9001, activeWhen = 1 }, + -- { kind = 'Drop', name = 'DZ_WEST', flag = 9002, activeWhen = 1 }, + }, -- Crate spawn placement within pickup zones PickupZoneSpawnRandomize = true, -- if true, spawn crates at a random point within the pickup zone (avoids stacking) PickupZoneSpawnEdgeBuffer = 10, -- meters: keep spawns at least this far inside the zone edge @@ -306,6 +333,7 @@ CTLD._coachState = {} -- [unitName] = { lastKeyTimes = {key->time}, lastHi CTLD._msgState = { } -- messaging throttle state: [scopeKey] = { lastKeyTimes = { key -> time } } CTLD._buildConfirm = {} -- [groupName] = time of first build request (awaiting confirmation) CTLD._buildCooldown = {} -- [groupName] = time of last successful build +CTLD._NextMarkupId = 10000 -- global-ish id generator shared by instances for map drawings -- #endregion State @@ -404,6 +432,167 @@ local function _vec3FromUnit(unit) return { x = p.x, y = p.y, z = p.z } end +-- Unique id generator for map markups (lines/circles/text) +local function _nextMarkupId() + CTLD._NextMarkupId = (CTLD._NextMarkupId or 10000) + 1 + return CTLD._NextMarkupId +end + +-- Resolve a zone's center (vec3) and radius (meters). +-- Accepts a MOOSE ZONE object returned by _findZone/ZONE:FindByName/ZONE_RADIUS:New +function CTLD:_getZoneCenterAndRadius(mz) + if not mz then return nil, nil end + local name = mz.GetName and mz:GetName() or nil + -- Prefer Mission Editor zone data if available + if name and trigger and trigger.misc and trigger.misc.getZone then + local z = trigger.misc.getZone(name) + if z and z.point and z.radius then + local p = { x = z.point.x, y = z.point.y or 0, z = z.point.z } + return p, z.radius + end + end + -- Fall back to MOOSE zone center + local pv = mz.GetPointVec3 and mz:GetPointVec3(mz) or nil + local p = pv and { x = pv.x, y = pv.y or 0, z = pv.z } or nil + -- Try to fetch a configured radius from our zone defs + local r + if name and self._ZoneDefs then + local d = self._ZoneDefs.PickupZones and self._ZoneDefs.PickupZones[name] + or self._ZoneDefs.DropZones and self._ZoneDefs.DropZones[name] + or self._ZoneDefs.FOBZones and self._ZoneDefs.FOBZones[name] + if d and d.radius then r = d.radius end + end + r = r or (mz.GetRadius and mz:GetRadius()) or 150 + return p, r +end + +-- Draw a circle and label for a zone on the F10 map for this coalition. +-- kind: 'Pickup' | 'Drop' | 'FOB' +function CTLD:_drawZoneCircleAndLabel(kind, mz, opts) + if not (trigger and trigger.action and trigger.action.circleToAll and trigger.action.textToAll) then return end + opts = opts or {} + local p, r = self:_getZoneCenterAndRadius(mz) + if not p or not r then return end + local side = self.Side + local outline = opts.OutlineColor or {0,1,0,0.85} + local fill = opts.FillColor or {0,1,0,0.15} + local lineType = opts.LineType or 1 + local readOnly = (opts.ReadOnly ~= false) + local fontSize = opts.FontSize or 18 + local labelPrefix = opts.LabelPrefix or 'Zone' + local zname = (mz.GetName and mz:GetName()) or '(zone)' + local circleId = _nextMarkupId() + local textId = _nextMarkupId() + trigger.action.circleToAll(side, circleId, p, r, outline, fill, lineType, readOnly) + local label = string.format('%s: %s', labelPrefix, zname) + -- Offset text slightly north of center so it’s readable above the circle center + local textPos = { x = p.x, y = 0, z = p.z - (r + 20) } + trigger.action.textToAll(side, textId, textPos, {1,1,1,0.9}, {0,0,0,0}, fontSize, readOnly, label) + -- Track ids so they can be cleared later + self._MapMarkup = self._MapMarkup or { Pickup = {}, Drop = {}, FOB = {} } + self._MapMarkup[kind] = self._MapMarkup[kind] or {} + self._MapMarkup[kind][zname] = { circle = circleId, text = textId } +end + +function CTLD:ClearMapDrawings() + if not (self._MapMarkup and trigger and trigger.action and trigger.action.removeMark) then return end + for _, byName in pairs(self._MapMarkup) do + for _, ids in pairs(byName) do + if ids.circle then pcall(trigger.action.removeMark, ids.circle) end + if ids.text then pcall(trigger.action.removeMark, ids.text) end + end + end + self._MapMarkup = { Pickup = {}, Drop = {}, FOB = {} } +end + +function CTLD:_removeZoneDrawing(kind, zname) + if not (self._MapMarkup and self._MapMarkup[kind] and self._MapMarkup[kind][zname]) then return end + local ids = self._MapMarkup[kind][zname] + if ids.circle then pcall(trigger.action.removeMark, ids.circle) end + if ids.text then pcall(trigger.action.removeMark, ids.text) end + self._MapMarkup[kind][zname] = nil +end + +-- Public: set a specific zone active/inactive by kind and name +function CTLD:SetZoneActive(kind, name, active) + if not (kind and name) then return end + local k = (kind == 'Pickup' or kind == 'Drop' or kind == 'FOB') and kind or nil + if not k then return end + self._ZoneActive = self._ZoneActive or { Pickup = {}, Drop = {}, FOB = {} } + self._ZoneActive[k][name] = (active ~= false) + -- Update drawings for this one zone only + if self.Config.MapDraw and self.Config.MapDraw.Enabled then + -- Find the MOOSE zone object by name + local list = (k=='Pickup' and self.PickupZones) or (k=='Drop' and self.DropZones) or (k=='FOB' and self.FOBZones) or {} + local mz + for _,z in ipairs(list or {}) do if z and z.GetName and z:GetName() == name then mz = z break end end + if self._ZoneActive[k][name] then + if mz then + local md = self.Config.MapDraw + local opts = { + OutlineColor = md.OutlineColor, + FillColor = md.FillColor, + LineType = (md.LineTypes and md.LineTypes[k]) or md.LineType or 1, + FontSize = md.FontSize, + ReadOnly = (md.ReadOnly ~= false), + LabelPrefix = (k=='Pickup' and (md.LabelPrefix or 'Pickup Zone')) or (k..' Zone') + } + self:_drawZoneCircleAndLabel(k, mz, opts) + end + else + self:_removeZoneDrawing(k, name) + end + end + -- Optional messaging + local stateStr = self._ZoneActive[k][name] and 'ACTIVATED' or 'DEACTIVATED' + env.info(string.format('[Moose_CTLD] Zone %s %s (%s)', tostring(name), stateStr, k)) +end + +function CTLD:DrawZonesOnMap() + local md = self.Config and self.Config.MapDraw or {} + if not md.Enabled then return end + -- Clear previous drawings before re-drawing + self:ClearMapDrawings() + local opts = { + OutlineColor = md.OutlineColor, + FillColor = md.FillColor, + LineType = md.LineType, + FontSize = md.FontSize, + ReadOnly = (md.ReadOnly ~= false), + LabelPrefix = md.LabelPrefix or 'Zone' + } + if md.DrawPickupZones then + for _,mz in ipairs(self.PickupZones or {}) do + local name = mz:GetName() + if self._ZoneActive.Pickup[name] ~= false then + opts.LabelPrefix = md.LabelPrefix or 'Pickup Zone' + opts.LineType = (md.LineTypes and md.LineTypes.Pickup) or md.LineType or 1 + self:_drawZoneCircleAndLabel('Pickup', mz, opts) + end + end + end + if md.DrawDropZones then + for _,mz in ipairs(self.DropZones or {}) do + local name = mz:GetName() + if self._ZoneActive.Drop[name] ~= false then + opts.LabelPrefix = 'Drop Zone' + opts.LineType = (md.LineTypes and md.LineTypes.Drop) or md.LineType or 1 + self:_drawZoneCircleAndLabel('Drop', mz, opts) + end + end + end + if md.DrawFOBZones then + for _,mz in ipairs(self.FOBZones or {}) do + local name = mz:GetName() + if self._ZoneActive.FOB[name] ~= false then + opts.LabelPrefix = 'FOB Zone' + opts.LineType = (md.LineTypes and md.LineTypes.FOB) or md.LineType or 1 + self:_drawZoneCircleAndLabel('FOB', mz, opts) + end + end + end +end + -- Unit preference detection and unit-aware formatting local function _getPlayerIsMetric(unit) local ok, isMetric = pcall(function() @@ -594,6 +783,51 @@ function CTLD:New(cfg) o:InitZones() -- Validate configured zones and warn if missing o:ValidateZones() + -- Optional: draw configured zones on the F10 map + if o.Config.MapDraw and o.Config.MapDraw.Enabled then + -- Defer a tiny bit to ensure mission environment is fully up + timer.scheduleFunction(function() + pcall(function() o:DrawZonesOnMap() end) + end, {}, timer.getTime() + 1) + end + -- Optional: bind zone activation to mission flags (merge from config table and per-zone flag fields) + do + local merged = {} + -- Collect from explicit bindings (backward compatible) + if o.Config.ZoneEventBindings then + for _,b in ipairs(o.Config.ZoneEventBindings) do table.insert(merged, b) end + end + -- Collect from per-zone entries (preferred) + local function pushFromZones(kind, list) + for _,z in ipairs(list or {}) do + if z and z.name and z.flag then + table.insert(merged, { kind = kind, name = z.name, flag = z.flag, activeWhen = z.activeWhen or 1 }) + end + end + end + pushFromZones('Pickup', o.Config.Zones and o.Config.Zones.PickupZones) + pushFromZones('Drop', o.Config.Zones and o.Config.Zones.DropZones) + pushFromZones('FOB', o.Config.Zones and o.Config.Zones.FOBZones) + + o._BindingsMerged = merged + if o._BindingsMerged and #o._BindingsMerged > 0 then + o._ZoneFlagState = {} + o.ZoneFlagSched = SCHEDULER:New(nil, function() + for _,b in ipairs(o._BindingsMerged) do + if b and b.flag and b.kind and b.name then + local val = (trigger and trigger.misc and trigger.misc.getUserFlag) and trigger.misc.getUserFlag(b.flag) or 0 + local activeWhen = (b.activeWhen ~= nil) and b.activeWhen or 1 + local shouldBeActive = (val == activeWhen) + local key = tostring(b.kind)..'|'..tostring(b.name) + if o._ZoneFlagState[key] ~= shouldBeActive then + o._ZoneFlagState[key] = shouldBeActive + o:SetZoneActive(b.kind, b.name, shouldBeActive) + end + end + end + end, {}, 1, 1) + end + end o:InitMenus() -- Periodic cleanup for crates @@ -625,17 +859,33 @@ function CTLD:InitZones() self.DropZones = {} self.FOBZones = {} self._ZoneDefs = { PickupZones = {}, DropZones = {}, FOBZones = {} } + self._ZoneActive = { Pickup = {}, Drop = {}, FOB = {} } 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 + if mz then + table.insert(self.PickupZones, mz) + local name = mz:GetName() + self._ZoneDefs.PickupZones[name] = z + if self._ZoneActive.Pickup[name] == nil then self._ZoneActive.Pickup[name] = (z.active ~= false) end + 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 + if mz then + table.insert(self.DropZones, mz) + local name = mz:GetName() + self._ZoneDefs.DropZones[name] = z + if self._ZoneActive.Drop[name] == nil then self._ZoneActive.Drop[name] = (z.active ~= false) end + 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 + if mz then + table.insert(self.FOBZones, mz) + local name = mz:GetName() + self._ZoneDefs.FOBZones[name] = z + if self._ZoneActive.FOB[name] == nil then self._ZoneActive.FOB[name] = (z.active ~= false) end + end end end @@ -863,11 +1113,17 @@ function CTLD:BuildGroupMenus(group) -- Prefer configured pickup zones list; fallback to runtime zones converted to name list local list = nil if self.Config and self.Config.Zones and self.Config.Zones.PickupZones then - list = self.Config.Zones.PickupZones + list = {} + for _,z in ipairs(self.Config.Zones.PickupZones) do + if (not z.name) or self._ZoneActive.Pickup[z.name] ~= false then table.insert(list, z) end + end elseif self.PickupZones and #self.PickupZones > 0 then list = {} for _,mz in ipairs(self.PickupZones) do - if mz and mz.GetName then table.insert(list, { name = mz:GetName() }) end + if mz and mz.GetName then + local n = mz:GetName() + if self._ZoneActive.Pickup[n] ~= false then table.insert(list, { name = n }) end + end end else list = {} @@ -949,6 +1205,14 @@ function CTLD:InitCoalitionAdminMenu() MENU_COALITION_COMMAND:New(self.Side, 'Show Coalition Summary', root, function() self:ShowCoalitionSummary() end) + MENU_COALITION_COMMAND:New(self.Side, 'Draw CTLD Zones on Map', root, function() + self:DrawZonesOnMap() + _msgCoalition(self.Side, 'CTLD zones drawn on F10 map.', 8) + end) + MENU_COALITION_COMMAND:New(self.Side, 'Clear CTLD Map Drawings', root, function() + self:ClearMapDrawings() + _msgCoalition(self.Side, 'CTLD map drawings cleared.', 8) + end) self.AdminMenu = root end -- #endregion Menus @@ -1062,8 +1326,17 @@ function CTLD:RequestCrateForGroup(group, 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 hasPickupZones = (self.PickupZones and #self.PickupZones > 0) or (self.Config.Zones and self.Config.Zones.PickupZones and #self.Config.Zones.PickupZones > 0) + local function _activePickupDefs() + local defs, out = self.Config.Zones.PickupZones or {}, {} + for _,z in ipairs(defs) do + local n = z.name + if (not n) or self._ZoneActive.Pickup[n] ~= false then table.insert(out, z) end + end + return out + end + local zone, dist = _nearestZonePoint(unit, _activePickupDefs()) + local hasPickupZones = ((self.PickupZones and #self.PickupZones > 0) or (self.Config.Zones and self.Config.Zones.PickupZones and #self.Config.Zones.PickupZones > 0)) + and (next(self._ZoneActive.Pickup) ~= nil) local spawnPoint local maxd = (self.Config.PickupZoneMaxDistance or 10000) -- Announce request @@ -1620,7 +1893,11 @@ function CTLD:LoadTroops(group, opts) _eventSend(self, group, nil, 'no_pickup_zones', {}) return end - local zone, dist = _nearestZonePoint(unit, self.Config.Zones.PickupZones) + local activeDefs = {} + for _,z in ipairs(self.Config.Zones.PickupZones or {}) do + if (not z.name) or self._ZoneActive.Pickup[z.name] ~= false then table.insert(activeDefs, z) end + end + local zone, dist = _nearestZonePoint(unit, activeDefs) local inside = false if zone then local rZone = self:_getZoneRadius(zone) or 0 diff --git a/Moose_CTLD_Pure/init_mission_dual_coalition.lua b/Moose_CTLD_Pure/init_mission_dual_coalition.lua index d28b733..9e29af1 100644 --- a/Moose_CTLD_Pure/init_mission_dual_coalition.lua +++ b/Moose_CTLD_Pure/init_mission_dual_coalition.lua @@ -20,12 +20,13 @@ ctldBlue = _MOOSE_CTLD:New({ 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' }, + -- Optional: drive zone activation from mission flags (preferred: set per-zone below via flag/activeWhen) - Zones = { - PickupZones = { { name = 'Blue_PickupZone_1', smoke = trigger.smokeColor.Blue } }, - --DropZones = { { name = 'DROP_BLUE_1' } }, - -- FOBZones = { { name = 'FOB_BLUE_A' } }, - }, + Zones = { + PickupZones = { { name = 'Blue_PickupZone_1', smoke = trigger.smokeColor.Blue, flag = 9001, activeWhen = 1 } }, + --DropZones = { { name = 'DROP_BLUE_1', flag = 9002, activeWhen = 1 } }, + --FOBZones = { { name = 'FOB_BLUE_A', flag = 9003, activeWhen = 1 } }, + }, BuildRequiresGroundCrates = true, }) @@ -36,12 +37,13 @@ ctldRed = _MOOSE_CTLD:New({ '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' }, + -- Optional: drive zone activation for RED via per-zone flag/activeWhen - Zones = { - PickupZones = { { name = 'Red_PickupZone_1', smoke = trigger.smokeColor.Red } }, - --DropZones = { { name = 'DROP_RED_1' } }, - -- FOBZones = { { name = 'FOB_RED_A' } }, - }, + Zones = { + PickupZones = { { name = 'Red_PickupZone_1', smoke = trigger.smokeColor.Red, flag = 9101, activeWhen = 1 } }, + --DropZones = { { name = 'DROP_RED_1', flag = 9102, activeWhen = 1 } }, + --FOBZones = { { name = 'FOB_RED_A', flag = 9103, activeWhen = 1 } }, + }, BuildRequiresGroundCrates = true, }) else