diff --git a/DCS_Kola/Operation_Polar_Shield/F99th-Operation_Polar_Shield_1.1.5.miz b/DCS_Kola/Operation_Polar_Shield/F99th-Operation_Polar_Shield_1.1.5.miz index 86a9809..35ba19c 100644 Binary files a/DCS_Kola/Operation_Polar_Shield/F99th-Operation_Polar_Shield_1.1.5.miz and b/DCS_Kola/Operation_Polar_Shield/F99th-Operation_Polar_Shield_1.1.5.miz differ diff --git a/DCS_Kola/Operation_Polar_Shield/F99th-Operation_Polar_Shield_1.1.6.miz b/DCS_Kola/Operation_Polar_Shield/F99th-Operation_Polar_Shield_1.1.6.miz new file mode 100644 index 0000000..79fe9ad Binary files /dev/null and b/DCS_Kola/Operation_Polar_Shield/F99th-Operation_Polar_Shield_1.1.6.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 new file mode 100644 index 0000000..360116e --- /dev/null +++ b/DCS_Kola/Operation_Polar_Shield/Moose_CTLD_Init_DualCoalitions.lua @@ -0,0 +1,86 @@ +-- init_mission_dual_coalition.lua +-- Use in Mission Editor with DO SCRIPT FILE load order: +-- 1) Moose.lua +-- 2) Moose_CTLD_Pure/Moose_CTLD.lua +-- 3) Moose_CTLD_Pure/catalogs/CrateCatalog_CTLD_Extract.lua -- optional but recommended catalog with BLUE+RED items (_CTLD_EXTRACTED_CATALOG) +-- 4) Moose_CTLD_Pure/Moose_CTLD_FAC.lua -- optional FAC/RECCE support +-- 5) DO SCRIPT: dofile on this file OR paste the block below directly +-- +-- Zones you should create in the Mission Editor (as trigger zones): +-- BLUE: PICKUP_BLUE_MAIN, DROP_BLUE_1, FOB_BLUE_A +-- RED : PICKUP_RED_MAIN, DROP_RED_1, FOB_RED_A +-- Adjust names below if you use different zone names. + + +-- Create CTLD instances only if Moose and CTLD are available +if _MOOSE_CTLD and _G.BASE then +ctldBlue = _MOOSE_CTLD:New({ + 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','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 = 'Luostari Supply', smoke = trigger.smokeColor.Blue, flag = 9001, activeWhen = 0 }, + { name = 'Koshka Supply', smoke = trigger.smokeColor.Blue, flag = 9004, activeWhen = 0 }, + { name = 'Ivalo Supply', smoke = trigger.smokeColor.Blue, flag = 9005, activeWhen = 0 }, + { name = 'Alakurtti Supply', smoke = trigger.smokeColor.Blue, flag = 9006, activeWhen = 0 }, + { name = 'Dallas FARP Supply', smoke = trigger.smokeColor.Blue, flag = 9007, activeWhen = 0 }, + { name = 'Paris FARP Supply', smoke = trigger.smokeColor.Blue, flag = 9008, activeWhen = 0 }, + { name = 'London FARP Supply', smoke = trigger.smokeColor.Blue, flag = 9009, activeWhen = 0}, + }, + --DropZones = { { name = 'BRAVO', flag = 9002, activeWhen = 0 } }, + --FOBZones = { { name = 'CHARLIE', flag = 9003, activeWhen = 0 } }, + }, + BuildRequiresGroundCrates = true, +}) + +ctldRed = _MOOSE_CTLD:New({ + 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','UH-60L','CH-47Fbl1','CH-47F','Mi-17','GazelleAI' + + }, + -- Optional: drive zone activation for RED via per-zone flag/activeWhen + + Zones = { + PickupZones = { { name = 'Luostari Supply', smoke = trigger.smokeColor.Red, flag = 9101, activeWhen = 0 }, + { name = 'Severomorsk-1 Supply', smoke = trigger.smokeColor.Red, flag = 9104, activeWhen = 0 }, + { name = 'Severomorsk-3 Supply', smoke = trigger.smokeColor.Red, flag = 9105, activeWhen = 0 }, + { name = 'Alakurtti Supply', smoke = trigger.smokeColor.Red, flag = 9106, activeWhen = 0 }, + { name = 'Murmansk Supply', smoke = trigger.smokeColor.Red, flag = 9107, activeWhen = 0 }, + { name = 'Olenya Supply', smoke = trigger.smokeColor.Red, flag = 9108, activeWhen = 0 }, + { name = 'Monchegorsk Supply', smoke = trigger.smokeColor.Red, flag = 9109, activeWhen = 0}, + { name = 'Afrikanda Supply', smoke = trigger.smokeColor.Red, flag = 9110, activeWhen = 0 }, + }, + --DropZones = { { name = 'ECHO', flag = 9102, activeWhen = 0 } }, + --FOBZones = { { name = 'FOXTROT', flag = 9103, activeWhen = 0 } }, + }, + BuildRequiresGroundCrates = true, +}) +else + env.info('[init_mission_dual_coalition] Moose or CTLD missing; skipping CTLD init') +end + + +-- Optional: FAC/RECCE for both sides (requires Moose_CTLD_FAC.lua) +if _MOOSE_CTLD_FAC and _G.BASE and ctldBlue and ctldRed then + facBlue = _MOOSE_CTLD_FAC:New(ctldBlue, { + CoalitionSide = coalition.side.BLUE, + Arty = { Enabled = false }, + }) + -- facBlue:AddRecceZone({ name = 'RECCE_BLUE_1' }) + facBlue:Run() + + facRed = _MOOSE_CTLD_FAC:New(ctldRed, { + CoalitionSide = coalition.side.RED, + Arty = { Enabled = false }, + }) + -- facRed:AddRecceZone({ name = 'RECCE_RED_1' }) + facRed:Run() +else + env.info('[init_mission_dual_coalition] FAC not initialized (missing Moose/CTLD/FAC or CTLD not created)') +end diff --git a/Moose_CTLD_Pure/Moose_CTLD.lua b/Moose_CTLD_Pure/Moose_CTLD.lua index 4ced05c..b608e4b 100644 --- a/Moose_CTLD_Pure/Moose_CTLD.lua +++ b/Moose_CTLD_Pure/Moose_CTLD.lua @@ -172,6 +172,7 @@ CTLD.Messages = { -- Zone restrictions drop_forbidden_in_pickup = "Cannot drop crates inside a Supply Zone. Move outside the zone boundary.", troop_deploy_forbidden_in_pickup = "Cannot deploy troops inside a Supply Zone. Move outside the zone boundary.", + drop_zone_too_close_to_pickup = "Drop Zone creation blocked: too close to Supply Zone {zone} (need at least {need} {need_u}; current {dist} {dist_u}). Fly further away and try again.", } -- #endregion Messaging @@ -209,6 +210,8 @@ CTLD.Config = { -- Dynamic Drop Zone settings DropZoneRadius = 250, -- meters: radius used when creating a Drop Zone via the admin menu at player position + MinDropZoneDistanceFromPickup = 10000, -- meters: minimum distance from nearest Pickup Zone required to create a dynamic Drop Zone (0 to disable) + MinDropDistanceActivePickupOnly = true, -- when true, only ACTIVE pickup zones are considered for the minimum distance check -- Attack/Defend AI behavior for deployed troops and built vehicles AttackAI = { @@ -284,6 +287,7 @@ CTLD.Config = { }, +} -- #endregion Config -- #region State @@ -356,7 +360,7 @@ local function _nearestZonePoint(unit, list) local up = du:getPoint() local ux, uz = up.x, up.z - local best, bestd = nil, 1e12 + local best, bestd = nil, nil for _, z in ipairs(list or {}) do local mz = _findZone(z) local zx, zz @@ -380,6 +384,7 @@ local function _nearestZonePoint(unit, list) if d < bestd then best, bestd = mz, d end end end + if not best then return nil, nil end return best, bestd end @@ -400,16 +405,28 @@ function CTLD:_isUnitInsidePickupZone(unit, activeOnly) end -- Helper: get nearest ACTIVE pickup zone (by configured list), respecting CTLD's active flags -function CTLD:_nearestActivePickupZone(unit) - 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 +function CTLD:_collectActivePickupDefs() + local out = {} + -- From config-defined zones + local defs = (self.Config and self.Config.Zones and 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 _nearestZonePoint(unit, _activePickupDefs()) + -- From MOOSE zone objects if present + if self.PickupZones and #self.PickupZones > 0 then + for _, mz in ipairs(self.PickupZones) do + if mz and mz.GetName then + local n = mz:GetName() + if self._ZoneActive.Pickup[n] ~= false then table.insert(out, { name = n }) end + end + end + end + return out +end + +function CTLD:_nearestActivePickupZone(unit) + return _nearestZonePoint(unit, self:_collectActivePickupDefs()) end local function _coalitionAddGroup(side, category, groupData) @@ -2458,17 +2475,9 @@ 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 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 zone, dist = self:_nearestActivePickupZone(unit) + local defs = self:_collectActivePickupDefs() + local hasPickupZones = (#defs > 0) local spawnPoint local maxd = (self.Config.PickupZoneMaxDistance or 10000) -- Announce request @@ -2480,7 +2489,7 @@ function CTLD:RequestCrateForGroup(group, crateKey) return end - if zone and dist <= maxd then + if zone and dist and dist <= maxd then -- Compute a random spawn point within the pickup zone to avoid stacking crates local center = zone:GetPointVec3() local rZone = self:_getZoneRadius(zone) @@ -3158,11 +3167,29 @@ function CTLD:LoadTroops(group, opts) _eventSend(self, group, nil, 'no_pickup_zones', {}) return end - 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 + local zone, dist = self:_nearestActivePickupZone(unit) + if not zone or not dist then + -- No active pickup zone resolvable; provide helpful vectors to nearest configured zone if any + local list = {} + if self.Config and self.Config.Zones and self.Config.Zones.PickupZones then + for _, z in ipairs(self.Config.Zones.PickupZones) do table.insert(list, z) end + elseif self.PickupZones and #self.PickupZones > 0 then + for _, mz in ipairs(self.PickupZones) do if mz and mz.GetName then table.insert(list, { name = mz:GetName() }) end end + end + local fbZone, fbDist = _nearestZonePoint(unit, list) + if fbZone and fbDist then + local isMetric = _getPlayerIsMetric(unit) + local rZone = self:_getZoneRadius(fbZone) or 0 + local delta = math.max(0, fbDist - rZone) + local v, u = _fmtRange(delta, isMetric) + local up = unit:GetPointVec3(); local zp = fbZone:GetPointVec3() + local brg = _bearingDeg({ x = up.x, z = up.z }, { x = zp.x, z = zp.z }) + _eventSend(self, group, nil, 'troop_pickup_zone_required', { zone_dist = v, zone_dist_u = u, zone_brg = brg }) + else + _eventSend(self, group, nil, 'no_pickup_zones', {}) + end + return end - local zone, dist = _nearestZonePoint(unit, activeDefs) local inside = false if zone then local rZone = self:_getZoneRadius(zone) or 0 @@ -3170,8 +3197,8 @@ function CTLD:LoadTroops(group, opts) end if not inside then local isMetric = _getPlayerIsMetric(unit) - local rZone = (zone and (self:_getZoneRadius(zone) or 0)) or 0 - local delta = math.max(0, (dist or 0) - rZone) + local rZone = (self:_getZoneRadius(zone) or 0) + local delta = (dist and rZone) and math.max(0, dist - rZone) or 0 local v, u = _fmtRange(delta, isMetric) -- Bearing from player to zone center local up = unit:GetPointVec3() @@ -3403,12 +3430,44 @@ function CTLD:CreateDropZoneAtGroup(group) if not group or not group:IsAlive() then return end local unit = group:GetUnit(1) if not unit or not unit:IsAlive() then return end - -- Prevent creating a Drop Zone too close to an active Pickup Zone to avoid overlap/confusion - local inside, pz, dist, pr = self:_isUnitInsidePickupZone(unit, true) + -- Prevent creating a Drop Zone inside or too close to a Pickup Zone + -- 1) Block if inside a (potentially active-only) pickup zone + local activeOnlyForInside = (self.Config and self.Config.ForbidChecksActivePickupOnly ~= false) + local inside, pz, distInside, pr = self:_isUnitInsidePickupZone(unit, activeOnlyForInside) if inside then - _msgGroup(group, string.format('Too close to Pickup Zone %s (%.0fm). Move away and try again.', (pz and pz.GetName and pz:GetName()) or '(pickup)', dist or 0), 10) + local isMetric = _getPlayerIsMetric(unit) + local curV, curU = _fmtRange(distInside or 0, isMetric) + local needV, needU = _fmtRange(self.Config.MinDropZoneDistanceFromPickup or 10000, isMetric) + _eventSend(self, group, nil, 'drop_zone_too_close_to_pickup', { + zone = (pz and pz.GetName and pz:GetName()) or '(pickup)', + need = needV, need_u = needU, + dist = curV, dist_u = curU, + }) return end + -- 2) Enforce a minimum distance from the nearest pickup zone (configurable) + local minD = tonumber(self.Config and self.Config.MinDropZoneDistanceFromPickup) or 0 + if minD > 0 then + local considerActive = (self.Config and self.Config.MinDropDistanceActivePickupOnly ~= false) + local nearestZone, nearestDist + if considerActive then + nearestZone, nearestDist = self:_nearestActivePickupZone(unit) + else + local list = (self.Config and self.Config.Zones and self.Config.Zones.PickupZones) or {} + nearestZone, nearestDist = _nearestZonePoint(unit, list) + end + if nearestZone and nearestDist and nearestDist < minD then + local isMetric = _getPlayerIsMetric(unit) + local needV, needU = _fmtRange(minD, isMetric) + local curV, curU = _fmtRange(nearestDist, isMetric) + _eventSend(self, group, nil, 'drop_zone_too_close_to_pickup', { + zone = (nearestZone and nearestZone.GetName and nearestZone:GetName()) or '(pickup)', + need = needV, need_u = needU, + dist = curV, dist_u = curU, + }) + return + end + end local p = unit:GetPointVec3() local baseName = group:GetName() or 'GROUP' local safe = tostring(baseName):gsub('%W', '') diff --git a/Moose_CTLD_Pure/Moose_CTLD_Pure.miz b/Moose_CTLD_Pure/Moose_CTLD_Pure.miz index 3ba50bd..86c60a5 100644 Binary files a/Moose_CTLD_Pure/Moose_CTLD_Pure.miz and b/Moose_CTLD_Pure/Moose_CTLD_Pure.miz differ