diff --git a/DCS_Kola/Operation_Polar_Shield/F99th-Operation_Polar_Shield_1.2.0.miz b/DCS_Kola/Operation_Polar_Shield/F99th-Operation_Polar_Shield_1.2.0.miz index 7bb2f8b..98c524d 100644 Binary files a/DCS_Kola/Operation_Polar_Shield/F99th-Operation_Polar_Shield_1.2.0.miz and b/DCS_Kola/Operation_Polar_Shield/F99th-Operation_Polar_Shield_1.2.0.miz differ diff --git a/DCS_Kola/Operation_Polar_Shield/F99th-Operation_Polar_Shield_1.2.1.miz b/DCS_Kola/Operation_Polar_Shield/F99th-Operation_Polar_Shield_1.2.1.miz new file mode 100644 index 0000000..e56ee48 Binary files /dev/null and b/DCS_Kola/Operation_Polar_Shield/F99th-Operation_Polar_Shield_1.2.1.miz differ diff --git a/DCS_Kola/Operation_Polar_Shield/Moose_CTLD_Init_DualCoalitions.lua b/DCS_Kola/Operation_Polar_Shield/Moose_CTLD_Init_DualCoalitions.lua index 826626c..e2ac21c 100644 --- a/DCS_Kola/Operation_Polar_Shield/Moose_CTLD_Init_DualCoalitions.lua +++ b/DCS_Kola/Operation_Polar_Shield/Moose_CTLD_Init_DualCoalitions.lua @@ -37,6 +37,7 @@ ctldBlue = _MOOSE_CTLD:New({ }, --DropZones = { { name = 'BRAVO', flag = 9002, activeWhen = 0 } }, --FOBZones = { { name = 'CHARLIE', flag = 9003, activeWhen = 0 } }, + --MASHZones = { { name = 'MASH Alpha', freq = '251.0 AM', radius = 500, flag = 9010, activeWhen = 0 } }, }, BuildRequiresGroundCrates = true, }) @@ -62,6 +63,7 @@ ctldRed = _MOOSE_CTLD:New({ }, --DropZones = { { name = 'ECHO', flag = 9102, activeWhen = 0 } }, --FOBZones = { { name = 'FOXTROT', flag = 9103, activeWhen = 0 } }, + --MASHZones = { { name = 'MASH Bravo', freq = '252.0 AM', radius = 500, flag = 9111, activeWhen = 0 } }, }, BuildRequiresGroundCrates = true, }) diff --git a/DCS_Kola/Operation_Polar_Shield/Moose_CaptureZones.lua b/DCS_Kola/Operation_Polar_Shield/Moose_CaptureZones.lua index 38049bc..ed27301 100644 --- a/DCS_Kola/Operation_Polar_Shield/Moose_CaptureZones.lua +++ b/DCS_Kola/Operation_Polar_Shield/Moose_CaptureZones.lua @@ -1,5 +1,29 @@ -- Setup Capture Missions & Zones +-- ================================================================================ +-- MESSAGE AND TIMING CONFIGURATION +-- Control how often messages are sent and how long they are displayed +-- ================================================================================ +local MESSAGE_CONFIG = { + -- Zone status broadcast frequency (in seconds) + STATUS_BROADCAST_FREQUENCY = 3602, -- Default: 3600 seconds (1 hour) + STATUS_BROADCAST_START_DELAY = 10, -- Default: 10 seconds initial delay + + -- Zone color verification frequency (in seconds) + COLOR_VERIFICATION_FREQUENCY = 240, -- Default: 240 seconds (4 minutes) + COLOR_VERIFICATION_START_DELAY = 60, -- Default: 60 seconds initial delay + + -- Tactical marker update frequency (in seconds) + TACTICAL_UPDATE_FREQUENCY = 180, -- Default: 180 seconds (3 minutes) + TACTICAL_UPDATE_START_DELAY = 30, -- Default: 30 seconds initial delay + + -- Message display durations (in seconds) + STATUS_MESSAGE_DURATION = 15, -- Default: 15 seconds + VICTORY_MESSAGE_DURATION = 300, -- Default: 300 seconds + CAPTURE_MESSAGE_DURATION = 15, -- Default: 15 seconds + ATTACK_MESSAGE_DURATION = 15, -- Default: 15 seconds +} + -- ================================================================================ -- ZONE COLOR CONFIGURATION -- Mission makers can easily customize zone colors here @@ -471,16 +495,16 @@ local function OnEnterGuarded(ZoneCapture, From, Event, To) ZoneCapture:UndrawZone() local color = ZONE_COLORS.BLUE_CAPTURED ZoneCapture:DrawZone(-1, color, 0.5, color, 0.2, 2, true) - US_CC:MessageTypeToCoalition( string.format( "%s is under protection of the USA", ZoneCapture:GetZoneName() ), MESSAGE.Type.Information ) - RU_CC:MessageTypeToCoalition( string.format( "%s is under protection of the USA", ZoneCapture:GetZoneName() ), MESSAGE.Type.Information ) + US_CC:MessageTypeToCoalition( string.format( "%s is under protection of the USA", ZoneCapture:GetZoneName() ), MESSAGE.Type.Information, MESSAGE_CONFIG.CAPTURE_MESSAGE_DURATION ) + RU_CC:MessageTypeToCoalition( string.format( "%s is under protection of the USA", ZoneCapture:GetZoneName() ), MESSAGE.Type.Information, MESSAGE_CONFIG.CAPTURE_MESSAGE_DURATION ) else ZoneCapture:Smoke( SMOKECOLOR.Red ) -- Update zone visual markers to RED (captured) ZoneCapture:UndrawZone() local color = ZONE_COLORS.RED_CAPTURED ZoneCapture:DrawZone(-1, color, 0.5, color, 0.2, 2, true) - RU_CC:MessageTypeToCoalition( string.format( "%s is under protection of Russia", ZoneCapture:GetZoneName() ), MESSAGE.Type.Information ) - US_CC:MessageTypeToCoalition( string.format( "%s is under protection of Russia", ZoneCapture:GetZoneName() ), MESSAGE.Type.Information ) + RU_CC:MessageTypeToCoalition( string.format( "%s is under protection of Russia", ZoneCapture:GetZoneName() ), MESSAGE.Type.Information, MESSAGE_CONFIG.CAPTURE_MESSAGE_DURATION ) + US_CC:MessageTypeToCoalition( string.format( "%s is under protection of Russia", ZoneCapture:GetZoneName() ), MESSAGE.Type.Information, MESSAGE_CONFIG.CAPTURE_MESSAGE_DURATION ) end -- Create/update tactical information marker CreateTacticalInfoMarker(ZoneCapture) @@ -493,8 +517,8 @@ local function OnEnterEmpty(ZoneCapture) ZoneCapture:UndrawZone() local color = ZONE_COLORS.EMPTY ZoneCapture:DrawZone(-1, color, 0.5, color, 0.2, 2, true) - US_CC:MessageTypeToCoalition( string.format( "%s is unprotected, and can be captured!", ZoneCapture:GetZoneName() ), MESSAGE.Type.Information ) - RU_CC:MessageTypeToCoalition( string.format( "%s is unprotected, and can be captured!", ZoneCapture:GetZoneName() ), MESSAGE.Type.Information ) + US_CC:MessageTypeToCoalition( string.format( "%s is unprotected, and can be captured!", ZoneCapture:GetZoneName() ), MESSAGE.Type.Information, MESSAGE_CONFIG.CAPTURE_MESSAGE_DURATION ) + RU_CC:MessageTypeToCoalition( string.format( "%s is unprotected, and can be captured!", ZoneCapture:GetZoneName() ), MESSAGE.Type.Information, MESSAGE_CONFIG.CAPTURE_MESSAGE_DURATION ) -- Create/update tactical information marker CreateTacticalInfoMarker(ZoneCapture) end @@ -507,12 +531,12 @@ local function OnEnterAttacked(ZoneCapture) local color if Coalition == coalition.side.BLUE then color = ZONE_COLORS.BLUE_ATTACKED -- Light blue for Blue zone under attack - US_CC:MessageTypeToCoalition( string.format( "%s is under attack by Russia", ZoneCapture:GetZoneName() ), MESSAGE.Type.Information ) - RU_CC:MessageTypeToCoalition( string.format( "We are attacking %s", ZoneCapture:GetZoneName() ), MESSAGE.Type.Information ) + US_CC:MessageTypeToCoalition( string.format( "%s is under attack by Russia", ZoneCapture:GetZoneName() ), MESSAGE.Type.Information, MESSAGE_CONFIG.ATTACK_MESSAGE_DURATION ) + RU_CC:MessageTypeToCoalition( string.format( "We are attacking %s", ZoneCapture:GetZoneName() ), MESSAGE.Type.Information, MESSAGE_CONFIG.ATTACK_MESSAGE_DURATION ) else color = ZONE_COLORS.RED_ATTACKED -- Orange for Red zone under attack - RU_CC:MessageTypeToCoalition( string.format( "%s is under attack by the USA", ZoneCapture:GetZoneName() ), MESSAGE.Type.Information ) - US_CC:MessageTypeToCoalition( string.format( "We are attacking %s", ZoneCapture:GetZoneName() ), MESSAGE.Type.Information ) + RU_CC:MessageTypeToCoalition( string.format( "%s is under attack by the USA", ZoneCapture:GetZoneName() ), MESSAGE.Type.Information, MESSAGE_CONFIG.ATTACK_MESSAGE_DURATION ) + US_CC:MessageTypeToCoalition( string.format( "We are attacking %s", ZoneCapture:GetZoneName() ), MESSAGE.Type.Information, MESSAGE_CONFIG.ATTACK_MESSAGE_DURATION ) end ZoneCapture:DrawZone(-1, color, 0.5, color, 0.2, 2, true) -- Create/update tactical information marker @@ -830,7 +854,7 @@ local function BroadcastZoneStatus() local fullMessage = reportMessage .. detailMessage - US_CC:MessageTypeToCoalition( fullMessage, MESSAGE.Type.Information, 15 ) + US_CC:MessageTypeToCoalition( fullMessage, MESSAGE.Type.Information, MESSAGE_CONFIG.STATUS_MESSAGE_DURATION ) log("[ZONE STATUS] " .. reportMessage:gsub("\n", " | ")) @@ -846,17 +870,17 @@ local ZoneMonitorScheduler = SCHEDULER:New( nil, function() US_CC:MessageTypeToCoalition( string.format("APPROACHING VICTORY! %d more zone(s) needed for complete success!", status.total - status.blue), - MESSAGE.Type.Information, 10 + MESSAGE.Type.Information, MESSAGE_CONFIG.VICTORY_MESSAGE_DURATION ) RU_CC:MessageTypeToCoalition( string.format("CRITICAL SITUATION! Only %d zone(s) remain under our control!", status.red), - MESSAGE.Type.Information, 10 + MESSAGE.Type.Information, MESSAGE_CONFIG.VICTORY_MESSAGE_DURATION ) end -end, {}, 10, 300 ) -- Start after 10 seconds, repeat every 300 seconds (5 minutes) +end, {}, MESSAGE_CONFIG.STATUS_BROADCAST_START_DELAY, MESSAGE_CONFIG.STATUS_BROADCAST_FREQUENCY ) -- Configurable timing -- Periodic zone color verification system (every 2 minutes) local ZoneColorVerificationScheduler = SCHEDULER:New( nil, function() @@ -893,7 +917,7 @@ local ZoneColorVerificationScheduler = SCHEDULER:New( nil, function() end end -end, {}, 60, 240 ) -- Start after 60 seconds, repeat every 240 seconds (4 minutes) +end, {}, MESSAGE_CONFIG.COLOR_VERIFICATION_START_DELAY, MESSAGE_CONFIG.COLOR_VERIFICATION_FREQUENCY ) -- Configurable timing -- Periodic tactical marker update system with change detection (every 3 minutes) local __lastForceCountsByZone = {} @@ -916,7 +940,7 @@ local TacticalMarkerUpdateScheduler = SCHEDULER:New( nil, function() end end -end, {}, 30, 180 ) -- Start after 30 seconds, repeat every 180 seconds (3 minutes) +end, {}, MESSAGE_CONFIG.TACTICAL_UPDATE_START_DELAY, MESSAGE_CONFIG.TACTICAL_UPDATE_FREQUENCY ) -- Configurable timing -- Function to refresh all zone colors based on current ownership local function RefreshAllZoneColors() diff --git a/DCS_Kola/Operation_Polar_Shield/Moose_TADC_Load2nd.lua b/DCS_Kola/Operation_Polar_Shield/Moose_TADC_Load2nd.lua index f67e6b4..bc49db4 100644 --- a/DCS_Kola/Operation_Polar_Shield/Moose_TADC_Load2nd.lua +++ b/DCS_Kola/Operation_Polar_Shield/Moose_TADC_Load2nd.lua @@ -140,8 +140,8 @@ local TADC_SETTINGS = { -- Timing settings (applies to both coalitions) checkInterval = 30, -- How often to scan for threats (seconds) monitorInterval = 30, -- How often to check interceptor status (seconds) - statusReportInterval = 120, -- How often to report airbase status (seconds) - squadronSummaryInterval = 600, -- How often to broadcast squadron summary (seconds) + statusReportInterval = 1805, -- How often to report airbase status (seconds) + squadronSummaryInterval = 1800, -- How often to broadcast squadron summary (seconds) cargoCheckInterval = 15, -- How often to check for cargo deliveries (seconds) -- RED Coalition Settings diff --git a/Moose_CTLD_Pure/Moose_CTLD.lua b/Moose_CTLD_Pure/Moose_CTLD.lua index 367e5cb..52f677f 100644 --- a/Moose_CTLD_Pure/Moose_CTLD.lua +++ b/Moose_CTLD_Pure/Moose_CTLD.lua @@ -291,6 +291,7 @@ CTLD.Config = { DrawPickupZones = true, -- draw Pickup/Supply zones as shaded circles with labels DrawDropZones = true, -- optionally draw Drop zones DrawFOBZones = true, -- optionally draw FOB zones + DrawMASHZones = true, -- optionally draw MASH (medical) zones FontSize = 18, -- label text size ReadOnly = true, -- prevent clients from removing the shapes ForAll = false, -- if true, draw shapes to all (-1) instead of coalition only (useful for testing/briefing) @@ -300,12 +301,14 @@ CTLD.Config = { Pickup = {0, 1, 0, 0.15}, -- light green fill for Pickup zones Drop = {0, 0, 0, 0.25}, -- black fill for Drop zones FOB = {1, 1, 0, 0.15}, -- yellow fill for FOB zones + MASH = {1, 0.75, 0.8, 0.25}, -- pink fill for MASH zones }, 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 + MASH = 1, -- solid }, -- Label placement tuning (simple): -- Effective extra offset from the circle edge = r * LabelOffsetRatio + LabelOffsetFromEdge @@ -317,6 +320,7 @@ CTLD.Config = { Pickup = 'Supply Zone', Drop = 'Drop Zone', FOB = 'FOB Zone', + MASH = 'MASH', } }, @@ -331,7 +335,7 @@ CTLD.Config = { -- Inventory system (per pickup zone and FOBs) Inventory = { Enabled = true, -- master switch for per-location stock control - FOBStockFactor = 0.25, -- starting stock at newly built FOBs relative to pickup-zone initialStock + FOBStockFactor = 0.50, -- starting stock at newly built FOBs relative to pickup-zone initialStock ShowStockInMenu = true, -- if true, append simple stock hints to menu labels (per current nearest zone) HideZeroStockMenu = false, -- removed: previously created an "In Stock Here" submenu; now disabled by default }, @@ -344,6 +348,16 @@ CTLD.Config = { -- If no catalog is loaded, empty table is used (and fallback logic applies) TroopTypes = {}, }, + + -- Zones (Supply/Pickup, Drop, FOB, MASH) + -- Mission makers should populate these arrays with zone definitions + -- Each zone entry can be: { name = 'ZoneName' } or { name = 'ZoneName', flag = 9001, activeWhen = 0, smoke = color, radius = meters } + Zones = { + PickupZones = {}, -- Supply zones where crates/troops can be requested + DropZones = {}, -- Optional Drop/AO zones + FOBZones = {}, -- FOB zones (restrict FOB building to these if RestrictFOBToZones = true) + MASHZones = {}, -- Medical zones for MEDEVAC crew delivery (MASH = Mobile Army Surgical Hospital) + }, } -- #endregion Config @@ -430,12 +444,6 @@ CTLD.MEDEVAC = { { time = 300, message = 'URGENT MEDEVAC: {crew} at {grid} will be KIA in 5 minutes!' }, }, - -- MASH Zones (fixed, defined in mission editor) - MASHZones = { - -- Example: { name = 'MASH Alpha', freq = 251.0, radius = 500 }, - -- Mission makers add their zones here or via CTLD:AddMASHZone() - }, - MASHZoneRadius = 500, -- default radius for MASH zones MASHZoneColors = { border = {1, 1, 0, 0.85}, -- yellow border @@ -852,6 +860,7 @@ function CTLD:_getZoneCenterAndRadius(mz) 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] + or self._ZoneDefs.MASHZones and self._ZoneDefs.MASHZones[name] if d and d.radius then r = d.radius end end r = r or (mz.GetRadius and mz:GetRadius()) or 150 @@ -859,7 +868,7 @@ function CTLD:_getZoneCenterAndRadius(mz) end -- Draw a circle and label for a zone on the F10 map for this coalition. --- kind: 'Pickup' | 'Drop' | 'FOB' +-- kind: 'Pickup' | 'Drop' | 'FOB' | 'MASH' 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 {} @@ -898,7 +907,7 @@ function CTLD:ClearMapDrawings() if ids.text then pcall(trigger.action.removeMark, ids.text) end end end - self._MapMarkup = { Pickup = {}, Drop = {}, FOB = {} } + self._MapMarkup = { Pickup = {}, Drop = {}, FOB = {}, MASH = {} } end function CTLD:_removeZoneDrawing(kind, zname) @@ -912,14 +921,14 @@ end -- Public: set a specific zone active/inactive by kind and name function CTLD:SetZoneActive(kind, name, active, silent) if not (kind and name) then return end - local k = (kind == 'Pickup' or kind == 'Drop' or kind == 'FOB') and kind or nil + local k = (kind == 'Pickup' or kind == 'Drop' or kind == 'FOB' or kind == 'MASH') and kind or nil if not k then return end - self._ZoneActive = self._ZoneActive or { Pickup = {}, Drop = {}, FOB = {} } + self._ZoneActive = self._ZoneActive or { Pickup = {}, Drop = {}, FOB = {}, MASH = {} } 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 list = (k=='Pickup' and self.PickupZones) or (k=='Drop' and self.DropZones) or (k=='FOB' and self.FOBZones) or (k=='MASH' and self.MASHZones) 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 @@ -1002,6 +1011,17 @@ function CTLD:DrawZonesOnMap() end end end + if md.DrawMASHZones then + for _,mz in ipairs(self.MASHZones or {}) do + local name = mz:GetName() + if self._ZoneActive.MASH[name] ~= false then + opts.LabelPrefix = (md.LabelPrefixes and md.LabelPrefixes.MASH) or 'MASH' + opts.LineType = (md.LineTypes and md.LineTypes.MASH) or md.LineType or 1 + opts.FillColor = (md.FillColors and md.FillColors.MASH) or nil + self:_drawZoneCircleAndLabel('MASH', mz, opts) + end + end + end end -- Unit preference detection and unit-aware formatting @@ -1454,6 +1474,7 @@ function CTLD:New(cfg) 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) + pushFromZones('MASH', o.Config.Zones and o.Config.Zones.MASHZones) o._BindingsMerged = merged if o._BindingsMerged and #o._BindingsMerged > 0 then @@ -1532,8 +1553,9 @@ function CTLD:InitZones() self.PickupZones = {} self.DropZones = {} self.FOBZones = {} - self._ZoneDefs = { PickupZones = {}, DropZones = {}, FOBZones = {} } - self._ZoneActive = { Pickup = {}, Drop = {}, FOB = {} } + self.MASHZones = {} + self._ZoneDefs = { PickupZones = {}, DropZones = {}, FOBZones = {}, MASHZones = {} } + self._ZoneActive = { Pickup = {}, Drop = {}, FOB = {}, MASH = {} } for _,z in ipairs(self.Config.Zones.PickupZones or {}) do local mz = _findZone(z) if mz then @@ -1561,6 +1583,15 @@ function CTLD:InitZones() if self._ZoneActive.FOB[name] == nil then self._ZoneActive.FOB[name] = (z.active ~= false) end end end + for _,z in ipairs(self.Config.Zones.MASHZones or {}) do + local mz = _findZone(z) + if mz then + table.insert(self.MASHZones, mz) + local name = mz:GetName() + self._ZoneDefs.MASHZones[name] = z + if self._ZoneActive.MASH[name] == nil then self._ZoneActive.MASH[name] = (z.active ~= false) end + end + end end -- Validate configured zone names exist in the mission; warn coalition if any are missing. @@ -1593,9 +1624,9 @@ function CTLD:ValidateZones() return s end - local missing = { Pickup = {}, Drop = {}, FOB = {} } - local found = { Pickup = {}, Drop = {}, FOB = {} } - local coords = { Pickup = 0, Drop = 0, FOB = 0 } + local missing = { Pickup = {}, Drop = {}, FOB = {}, MASH = {} } + local found = { Pickup = {}, Drop = {}, FOB = {}, MASH = {} } + local coords = { Pickup = 0, Drop = 0, FOB = 0, MASH = 0 } for _,z in ipairs(self.Config.Zones.PickupZones or {}) do if z.name then @@ -1618,6 +1649,13 @@ function CTLD:ValidateZones() coords.FOB = coords.FOB + 1 end end + for _,z in ipairs(self.Config.Zones.MASHZones or {}) do + if z.name then + if zoneExistsByName(z.name) then table.insert(found.MASH, z.name) else table.insert(missing.MASH, z.name) end + elseif z.coord then + coords.MASH = coords.MASH + 1 + end + end -- Log a concise summary to dcs.log local sideStr = sideToStr(self.Side) @@ -1630,8 +1668,11 @@ function CTLD:ValidateZones() env.info(string.format('[Moose_CTLD][ZoneValidation][%s] FOB : configured=%d (named=%d, coord=%d) found=%d missing=%d', sideStr, #(self.Config.Zones.FOBZones or {}), #found.FOB + #missing.FOB, coords.FOB, #found.FOB, #missing.FOB)) + env.info(string.format('[Moose_CTLD][ZoneValidation][%s] MASH : configured=%d (named=%d, coord=%d) found=%d missing=%d', + sideStr, + #(self.Config.Zones.MASHZones or {}), #found.MASH + #missing.MASH, coords.MASH, #found.MASH, #missing.MASH)) - local anyMissing = (#missing.Pickup > 0) or (#missing.Drop > 0) or (#missing.FOB > 0) + local anyMissing = (#missing.Pickup > 0) or (#missing.Drop > 0) or (#missing.FOB > 0) or (#missing.MASH > 0) if anyMissing then if #missing.Pickup > 0 then local msg = 'CTLD config warning: Missing Pickup Zones: '..join(missing.Pickup) @@ -1645,6 +1686,10 @@ function CTLD:ValidateZones() local msg = 'CTLD config warning: Missing FOB Zones: '..join(missing.FOB) _msgCoalition(self.Side, msg); env.info('[Moose_CTLD][ZoneValidation]['..sideStr..'] '..msg) end + if #missing.MASH > 0 then + local msg = 'CTLD config warning: Missing MASH Zones: '..join(missing.MASH) + _msgCoalition(self.Side, msg); env.info('[Moose_CTLD][ZoneValidation]['..sideStr..'] '..msg) + end else env.info(string.format('[Moose_CTLD][ZoneValidation][%s] All configured zone names resolved successfully.', sideStr)) end @@ -2010,6 +2055,9 @@ function CTLD:BuildGroupMenus(group) end end) + -- Logistics -> Show Inventory at Nearest Pickup Zone/FOB + CMD('Show Inventory at Nearest Zone', logRoot, function() self:ShowNearestZoneInventory(group) end) + -- Field Tools CMD('Create Drop Zone (AO)', toolsRoot, function() self:CreateDropZoneAtGroup(group) end) local smokeRoot = MENU_GROUP:New(group, 'Smoke My Location', toolsRoot) @@ -3730,6 +3778,110 @@ function CTLD:DropLoadedCrates(group, howMany) _msgGroup(group, string.format('Reminder: Dropped crates will despawn after %d mins to prevent clutter.', mins)) end end + +-- Show inventory at the nearest pickup zone/FOB +function CTLD:ShowNearestZoneInventory(group) + local unit = group:GetUnit(1) + if not unit or not unit:IsAlive() then return end + + -- Find nearest active pickup zone + local zone, dist = self:_nearestActivePickupZone(unit) + + if not zone then + _msgGroup(group, 'No active pickup zones found nearby. Move closer to a supply zone.') + return + end + + local zoneName = zone:GetName() + local isMetric = _getPlayerIsMetric(unit) + local rngV, rngU = _fmtRange(dist, isMetric) + + -- Get inventory for this zone + local stockTbl = CTLD._stockByZone[zoneName] or {} + + -- Build the inventory display + local lines = {} + table.insert(lines, string.format('Inventory at %s', zoneName)) + table.insert(lines, string.format('Distance: %.1f %s', rngV, rngU)) + table.insert(lines, '') + + -- Check if inventory system is enabled + local invEnabled = self.Config.Inventory and self.Config.Inventory.Enabled ~= false + if not invEnabled then + table.insert(lines, 'Inventory tracking is disabled - all items available.') + _msgGroup(group, table.concat(lines, '\n'), 20) + return + end + + -- Count total items and organize by category + local totalItems = 0 + local byCategory = {} + + for key, count in pairs(stockTbl) do + if count > 0 then + local def = self.Config.CrateCatalog[key] + if def and ((not def.side) or def.side == self.Side) then + local cat = (def.menuCategory or 'Other') + byCategory[cat] = byCategory[cat] or {} + table.insert(byCategory[cat], { + key = key, + name = def.menu or def.description or key, + count = count, + isRecipe = (type(def.requires) == 'table') + }) + totalItems = totalItems + count + end + end + end + + if totalItems == 0 then + table.insert(lines, 'No items in stock at this location.') + table.insert(lines, 'Request resupply or move to another zone.') + else + table.insert(lines, string.format('Total items in stock: %d', totalItems)) + table.insert(lines, '') + + -- Sort categories for consistent display + local categories = {} + for cat, _ in pairs(byCategory) do + table.insert(categories, cat) + end + table.sort(categories) + + -- Display items by category + for _, cat in ipairs(categories) do + table.insert(lines, string.format('-- %s --', cat)) + local items = byCategory[cat] + -- Sort items by name + table.sort(items, function(a, b) return a.name < b.name end) + for _, item in ipairs(items) do + if item.isRecipe then + -- For recipes, calculate available bundles + local def = self.Config.CrateCatalog[item.key] + local bundles = math.huge + if def and def.requires then + for reqKey, qty in pairs(def.requires) do + local have = tonumber(stockTbl[reqKey] or 0) or 0 + local need = tonumber(qty or 0) or 0 + if need > 0 then + bundles = math.min(bundles, math.floor(have / need)) + end + end + end + if bundles == math.huge then bundles = 0 end + table.insert(lines, string.format(' %s: %d bundle%s', item.name, bundles, (bundles == 1 and '' or 's'))) + else + table.insert(lines, string.format(' %s: %d', item.name, item.count)) + end + end + table.insert(lines, '') + end + end + + -- Display the inventory + _msgGroup(group, table.concat(lines, '\n'), 30) +end + -- #endregion Loaded crate management -- ========================= @@ -4865,59 +5017,22 @@ function CTLD:_InitMASHZones() local cfg = CTLD.MEDEVAC if not cfg or not cfg.Enabled then return end - -- Load fixed MASH zones from config - for _, zoneConfig in ipairs(cfg.MASHZones or {}) do - if zoneConfig.name then - local zone = ZONE:FindByName(zoneConfig.name) - if zone then - CTLD._mashZones[zoneConfig.name] = { - zone = zone, - side = self.Side, - isMobile = false, - radius = zoneConfig.radius or cfg.MASHZoneRadius or 500, - freq = zoneConfig.freq - } - env.info('[Moose_CTLD][MEDEVAC] Registered MASH zone: '..zoneConfig.name) - else - env.info('[Moose_CTLD][MEDEVAC] WARNING: MASH zone not found in mission: '..zoneConfig.name) - end - end - end + -- Fixed MASH zones are now initialized via InitZones() in the standard Zones structure + -- This function now focuses on setting up mobile MASH tracking and announcements - -- Draw MASH zones on map - if cfg.MapMarkers and cfg.MapMarkers.Enabled then - self:_DrawMASHZones() - end -end - --- Draw MASH zones on F10 map -function CTLD:_DrawMASHZones() - local cfg = CTLD.MEDEVAC - if not cfg or not cfg.Enabled then return end - - local md = self.Config.MapDraw - if not md or not md.Enabled then return end - - for zoneName, mashData in pairs(CTLD._mashZones) do - if mashData.side == self.Side then - local zone = mashData.zone - if zone then - local p, r = self:_getZoneCenterAndRadius(zone) - if p and r then - local circleId = _nextMarkupId() - local textId = _nextMarkupId() - - local borderColor = cfg.MASHZoneColors.border or {1, 1, 0, 0.85} - local fillColor = cfg.MASHZoneColors.fill or {1, 0.75, 0.8, 0.25} - - trigger.action.circleToCoalition(self.Side, circleId, p, r, borderColor, fillColor, 1, true, "") - - local label = string.format('MASH: %s', zoneName) - local textPos = {x = p.x, y = 0, z = p.z - r - 50} - trigger.action.textToCoalition(self.Side, textId, textPos, {1,1,1,0.9}, {0,0,0,0}, 18, true, label) - end - end - end + -- Register fixed MASH zones in the global _mashZones table for delivery detection + -- (mobile MASH zones will be added dynamically when built) + for _, mz in ipairs(self.MASHZones or {}) do + local name = mz:GetName() + local zdef = self._ZoneDefs.MASHZones[name] + CTLD._mashZones[name] = { + zone = mz, + side = self.Side, + isMobile = false, + radius = (zdef and zdef.radius) or cfg.MASHZoneRadius or 500, + freq = (zdef and zdef.freq) or nil + } + env.info('[Moose_CTLD][MEDEVAC] Registered fixed MASH zone: '..name) end end diff --git a/Moose_CTLD_Pure/Moose_CTLD_Init_DualCoalitions.lua b/Moose_CTLD_Pure/Moose_CTLD_Init_DualCoalitions.lua index 919dddf..1eb5afa 100644 --- a/Moose_CTLD_Pure/Moose_CTLD_Init_DualCoalitions.lua +++ b/Moose_CTLD_Pure/Moose_CTLD_Init_DualCoalitions.lua @@ -30,6 +30,7 @@ ctldBlue = _MOOSE_CTLD:New({ PickupZones = { { name = 'ALPHA', smoke = trigger.smokeColor.Blue, flag = 9001, activeWhen = 0 } }, DropZones = { { name = 'BRAVO', flag = 9002, activeWhen = 0 } }, FOBZones = { { name = 'CHARLIE', flag = 9003, activeWhen = 0 } }, + --MASHZones = { { name = 'MASH Alpha', freq = '251.0 AM', radius = 500, flag = 9010, activeWhen = 0 } }, }, BuildRequiresGroundCrates = true, }) @@ -47,6 +48,7 @@ ctldRed = _MOOSE_CTLD:New({ PickupZones = { { name = 'DELTA', smoke = trigger.smokeColor.Red, flag = 9101, activeWhen = 0 } }, DropZones = { { name = 'ECHO', flag = 9102, activeWhen = 0 } }, FOBZones = { { name = 'FOXTROT', flag = 9103, activeWhen = 0 } }, + --MASHZones = { { name = 'MASH Bravo', freq = '252.0 AM', radius = 500, flag = 9111, activeWhen = 0 } }, }, BuildRequiresGroundCrates = true, })