diff --git a/DCS_Kola/Operation_Polar_Shield/F99th-Operation_Polar_Shield_1.2.9.miz b/DCS_Kola/Operation_Polar_Shield/F99th-Operation_Polar_Shield_1.2.9.miz index 544e12c..541ff64 100644 Binary files a/DCS_Kola/Operation_Polar_Shield/F99th-Operation_Polar_Shield_1.2.9.miz and b/DCS_Kola/Operation_Polar_Shield/F99th-Operation_Polar_Shield_1.2.9.miz differ diff --git a/DCS_Kola/Operation_Polar_Shield/F99th-Operation_Polar_Shield_1.3.0.miz b/DCS_Kola/Operation_Polar_Shield/F99th-Operation_Polar_Shield_1.3.0.miz new file mode 100644 index 0000000..ea0c412 Binary files /dev/null and b/DCS_Kola/Operation_Polar_Shield/F99th-Operation_Polar_Shield_1.3.0.miz differ diff --git a/Moose_.lua b/Moose_.lua index fc76658..6a99c26 100644 --- a/Moose_.lua +++ b/Moose_.lua @@ -1,4 +1,4 @@ -env.info('*** MOOSE GITHUB Commit Hash ID: 2025-11-11T12:57:41+01:00-d7b0b3c898fb636dd8b728721e247763383a5bdb ***') +env.info('*** MOOSE GITHUB Commit Hash ID: 2025-11-14T17:27:02+01:00-cdbf1e147e76dcfab3d1bc471edf593a0e92182a ***') if not MOOSE_DEVELOPMENT_FOLDER then MOOSE_DEVELOPMENT_FOLDER='Scripts' end @@ -31462,6 +31462,7 @@ self.Life0=Life0 else self:E(string.format("Static object %s does not exist!",tostring(self.StaticName))) end +self._vec3=self:GetVec3() return self end function STATIC:GetLife0() @@ -31474,6 +31475,20 @@ return DCSStatic:getLife()or 1 end return nil end +function STATIC:GetVec2Cached() +local vec2=self:GetVec2() +if not vec2 and self._vec3 then +vec2={x=self._vec3.x,y=self._vec3.z} +end +return vec2 +end +function STATIC:GetVec3Cached() +local vec3=self:GetVec3() +if not vec3 and self._vec3 then +vec3=self._vec3 +end +return vec3 +end function STATIC:Find(DCSStatic) local StaticName=DCSStatic:getName() local StaticFound=_DATABASE:FindStatic(StaticName) @@ -31539,6 +31554,7 @@ SCHEDULER:New(nil,self.SpawnAt,{self,Coordinate,Heading},Delay) else local SpawnStatic=SPAWNSTATIC:NewFromStatic(self.StaticName) SpawnStatic:SpawnFromPointVec2(Coordinate,Heading,self.StaticName) +self._vec3=self:GetVec3() end return self end @@ -31549,6 +31565,7 @@ else CountryID=CountryID or self:GetCountry() local SpawnStatic=SPAWNSTATIC:NewFromStatic(self.StaticName,CountryID) SpawnStatic:Spawn(nil,self.StaticName) +self._vec3=self:GetVec3() end return self end @@ -31558,6 +31575,7 @@ SCHEDULER:New(nil,self.ReSpawnAt,{self,Coordinate,Heading},Delay) else local SpawnStatic=SPAWNSTATIC:NewFromStatic(self.StaticName,self:GetCountry()) SpawnStatic:SpawnFromCoordinate(Coordinate,Heading,self.StaticName) +self._vec3=self:GetVec3() end return self end @@ -134336,6 +134354,16 @@ local Vector=NavFix.vector:Translate(UTILS.NMToMeters(Distance),Bearing,true) self=NAVFIX:NewFromVector(Name,Type,Vector) return self end +function NAVFIX:NewFromBeacon(Beacon) +local frequency,unit=BEACONS:_GetFrequency(Beacon.frequency) +frequency=string.format("%.3f",frequency) +if Beacon.typeName=="TACAN"then +frequency=Beacon.channel +unit="X" +end +self=NAVFIX:NewFromVector(string.format("%s %s %s",Beacon.typeName,frequency,unit),Beacon.typeName,Beacon.vec3) +return self +end function NAVFIX:SetIntermediateFix(IntermediateFix) self.isIF=IntermediateFix return self @@ -134549,9 +134577,18 @@ return closest end function BEACONS:GetBeacons(TypeID) local beacons={} +local keys={} +if TypeID~=nil and type(TypeID)~="table"then +TypeID={TypeID} +end +for _,_typeid in pairs(TypeID or{})do +if _typeid~=nil then +keys[_typeid]=_typeid +end +end for _,_beacon in pairs(self.beacons)do local bc=_beacon -if TypeID==nil or TypeID==bc.type then +if TypeID==nil or keys[bc.type]~=nil then table.insert(beacons,bc) end end diff --git a/Moose_CTLD_Pure/Moose_CTLD.lua b/Moose_CTLD_Pure/Moose_CTLD.lua index e03b7d8..bc90c2d 100644 --- a/Moose_CTLD_Pure/Moose_CTLD.lua +++ b/Moose_CTLD_Pure/Moose_CTLD.lua @@ -4516,10 +4516,10 @@ function CTLD:BuildGroupMenus(group) CMD('Show Onboard Manifest', logRoot, function() self:ShowOnboardManifest(group) end) local reqRoot = MENU_GROUP:New(group, 'Request Crate', logRoot) - local crateMgmt = MENU_GROUP:New(group, 'Crate Management', logRoot) - CMD('Drop One Loaded Crate', crateMgmt, function() self:DropLoadedCrates(group, 1) end) - CMD('Drop All Loaded Crates', crateMgmt, function() self:DropLoadedCrates(group, -1) end) - self:_BuildOrRefreshLoadedCrateMenu(group, crateMgmt) + local crateMgmt = MENU_GROUP:New(group, 'Crate Management', logRoot) + CMD('Drop One Loaded Crate', crateMgmt, function() self:DropLoadedCrates(group, 1) end) + CMD('Drop All Loaded Crates', crateMgmt, function() self:DropLoadedCrates(group, -1) end) + self:_BuildOrRefreshLoadedCrateMenu(group, crateMgmt) CMD('Re-mark Nearest Crate (Smoke)', crateMgmt, function() local unit = group:GetUnit(1) if not unit or not unit:IsAlive() then return end @@ -4537,16 +4537,13 @@ function CTLD:BuildGroupMenus(group) end end if bestName and bestMeta then - local zdef = { smoke = self.Config.PickupZoneSmokeColor } local sx, sz = bestMeta.point.x, bestMeta.point.z local sy = 0 if land and land.getHeight then - -- land.getHeight expects Vec2 where y is z local ok, h = pcall(land.getHeight, { x = sx, y = sz }) if ok and type(h) == 'number' then sy = h end end - -- Use new smoke helper with crate ID for refresh scheduling - local smokeColor = (zdef and zdef.smoke) or self.Config.PickupZoneSmokeColor + local smokeColor = self.Config.PickupZoneSmokeColor _spawnCrateSmoke({ x = sx, y = sy, z = sz }, smokeColor, self.Config.CrateSmoke, bestName) _eventSend(self, group, nil, 'crate_re_marked', { id = bestName, mark = 'smoke' }) else @@ -4564,115 +4561,122 @@ function CTLD:BuildGroupMenus(group) local infoRoot = MENU_GROUP:New(group, 'Recipe Info', logRoot) if self.Config.UseCategorySubmenus then - local submenus = {} - local function getSubmenu(catLabel) - if not submenus[catLabel] then - submenus[catLabel] = MENU_GROUP:New(group, catLabel, reqRoot) + local reqSubmenus = {} + local function getRequestSub(catLabel) + if not reqSubmenus[catLabel] then + reqSubmenus[catLabel] = MENU_GROUP:New(group, catLabel, reqRoot) + end + return reqSubmenus[catLabel] end - return submenus[catLabel] - end - local infoSubs = {} - local function getInfoSub(catLabel) - if not infoSubs[catLabel] then - infoSubs[catLabel] = MENU_GROUP:New(group, catLabel, infoRoot) + + local infoSubs = {} + local function getInfoSub(catLabel) + if not infoSubs[catLabel] then + infoSubs[catLabel] = MENU_GROUP:New(group, catLabel, infoRoot) + end + return infoSubs[catLabel] end - return infoSubs[catLabel] - end - local replacementQueue = {} - for key,def in pairs(self.Config.CrateCatalog) do - if not (def and def.hidden) then - local label = self:_formatMenuLabelWithCrates(key, def) - 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) - if def and type(def.requires) == 'table' then - -- Composite recipe: request full bundle of component crates - CMD(label, parent, function() self:RequestRecipeBundleForGroup(group, key) end) - for reqKey,_ in pairs(def.requires) do - local compDef = self.Config.CrateCatalog[reqKey] - local compSideOk = (not compDef) or (not compDef.side) or compDef.side == self.Side - if compDef and compDef.hidden and compSideOk then - local queue = replacementQueue[catLabel] - if not queue then - queue = { list = {}, seen = {} } - replacementQueue[catLabel] = queue - end - if not queue.seen[reqKey] then - queue.seen[reqKey] = true - table.insert(queue.list, { key = reqKey, def = compDef }) + + local replacementQueue = {} + for key,def in pairs(self.Config.CrateCatalog) do + if not (def and def.hidden) then + local sideOk = (not def.side) or def.side == self.Side + if sideOk then + local catLabel = (def and def.menuCategory) or 'Other' + local reqParent = getRequestSub(catLabel) + local label = self:_formatMenuLabelWithCrates(key, def) + + if def and type(def.requires) == 'table' then + CMD(label, reqParent, function() self:RequestRecipeBundleForGroup(group, key) end) + for reqKey,_ in pairs(def.requires) do + local compDef = self.Config.CrateCatalog[reqKey] + local compSideOk = (not compDef) or (not compDef.side) or compDef.side == self.Side + if compDef and compDef.hidden and compSideOk then + local queue = replacementQueue[catLabel] + if not queue then + queue = { list = {}, seen = {} } + replacementQueue[catLabel] = queue + end + if not queue.seen[reqKey] then + queue.seen[reqKey] = true + table.insert(queue.list, { key = reqKey, def = compDef }) + end end end + else + CMD(label, reqParent, function() self:RequestCrateForGroup(group, key) end) end - else - CMD(label, parent, function() self:RequestCrateForGroup(group, key) end) + + local infoParent = getInfoSub(catLabel) + CMD((def and (def.menu or def.description)) or key, infoParent, function() + local text = self:_formatRecipeInfo(key, def) + _msgGroup(group, text) + end) end - local infoParent = getInfoSub(catLabel) - CMD((def and (def.menu or def.description)) or key, infoParent, function() - local text = self:_formatRecipeInfo(key, def) - _msgGroup(group, text) - end) end end - end - for catLabel,queue in pairs(replacementQueue) do - if queue and queue.list and #queue.list > 0 then - table.sort(queue.list, function(a,b) + + for catLabel,queue in pairs(replacementQueue) do + if queue and queue.list and #queue.list > 0 then + table.sort(queue.list, function(a,b) + local la = (a.def and (a.def.menu or a.def.description)) or a.key + local lb = (b.def and (b.def.menu or b.def.description)) or b.key + return tostring(la) < tostring(lb) + end) + local reqParent = getRequestSub(catLabel) + local replMenu = MENU_GROUP:New(group, 'Replacement Crates', reqParent) + for _,entry in ipairs(queue.list) do + local replLabel = string.format('Replacement: %s', self:_formatMenuLabelWithCrates(entry.key, entry.def)) + CMD(replLabel, replMenu, function() self:RequestCrateForGroup(group, entry.key) end) + end + end + end + else + local replacementList = {} + local replacementSeen = {} + for key,def in pairs(self.Config.CrateCatalog) do + if not (def and def.hidden) then + local sideOk = (not def.side) or def.side == self.Side + if sideOk then + local label = self:_formatMenuLabelWithCrates(key, def) + if def and type(def.requires) == 'table' then + CMD(label, reqRoot, function() self:RequestRecipeBundleForGroup(group, key) end) + for reqKey,_ in pairs(def.requires) do + local compDef = self.Config.CrateCatalog[reqKey] + local compSideOk = (not compDef) or (not compDef.side) or compDef.side == self.Side + if compDef and compDef.hidden and compSideOk and not replacementSeen[reqKey] then + replacementSeen[reqKey] = true + table.insert(replacementList, { key = reqKey, def = compDef }) + end + end + else + CMD(label, reqRoot, function() self:RequestCrateForGroup(group, key) end) + end + + CMD((def and (def.menu or def.description)) or key, infoRoot, function() + local text = self:_formatRecipeInfo(key, def) + _msgGroup(group, text) + end) + end + end + end + + if #replacementList > 0 then + table.sort(replacementList, function(a,b) local la = (a.def and (a.def.menu or a.def.description)) or a.key local lb = (b.def and (b.def.menu or b.def.description)) or b.key return tostring(la) < tostring(lb) end) - local parent = getSubmenu(catLabel) - local replMenu = MENU_GROUP:New(group, 'Replacement Crates', parent) - for _,entry in ipairs(queue.list) do + local replMenu = MENU_GROUP:New(group, 'Replacement Crates', reqRoot) + for _,entry in ipairs(replacementList) do local replLabel = string.format('Replacement: %s', self:_formatMenuLabelWithCrates(entry.key, entry.def)) CMD(replLabel, replMenu, function() self:RequestCrateForGroup(group, entry.key) end) end end end - else - local replacementList = {} - local replacementSeen = {} - for key,def in pairs(self.Config.CrateCatalog) do - if not (def and def.hidden) then - local label = self:_formatMenuLabelWithCrates(key, def) - local sideOk = (not def.side) or def.side == self.Side - if sideOk then - if def and type(def.requires) == 'table' then - CMD(label, reqRoot, function() self:RequestRecipeBundleForGroup(group, key) end) - for reqKey,_ in pairs(def.requires) do - local compDef = self.Config.CrateCatalog[reqKey] - local compSideOk = (not compDef) or (not compDef.side) or compDef.side == self.Side - if compDef and compDef.hidden and compSideOk and not replacementSeen[reqKey] then - replacementSeen[reqKey] = true - table.insert(replacementList, { key = reqKey, def = compDef }) - end - end - else - CMD(label, reqRoot, function() self:RequestCrateForGroup(group, key) end) - end - CMD((def and (def.menu or def.description)) or key, infoParent, function() - local text = self:_formatRecipeInfo(key, def) - _msgGroup(group, text) - end) - end - end - end - if #replacementList > 0 then - -- Logistics -> Show Inventory at Nearest Pickup Zone/FOB - CMD('Show Inventory at Nearest Zone', logRoot, function() self:ShowNearestZoneInventory(group) end) - end - -- Use new smoke helper with crate ID for refresh scheduling - local smokeColor = (zdef and zdef.smoke) or self.Config.PickupZoneSmokeColor - _spawnCrateSmoke({ x = sx, y = sy, z = sz }, smokeColor, self.Config.CrateSmoke, bestName) - _eventSend(self, group, nil, 'crate_re_marked', { id = bestName, mark = 'smoke' }) - else - _msgGroup(group, 'No friendly crates found to mark.') - end - end) - -- Logistics -> Show Inventory at Nearest Pickup Zone/FOB - CMD('Show Inventory at Nearest Zone', logRoot, function() self:ShowNearestZoneInventory(group) 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) @@ -7709,22 +7713,32 @@ function CTLD:ScanHoverPickup() -- Group doesn't exist or is dead, remove from tracking _removeFromSpatialGrid(troopGroupName, troopMeta.point, 'troops') CTLD._deployedTroops[troopGroupName] = nil + end + end + end + local coachEnabled = coachCfg.enabled if CTLD._coachOverride and CTLD._coachOverride[gname] ~= nil then coachEnabled = CTLD._coachOverride[gname] end + -- If coach is on, provide phased guidance if coachEnabled and bestName and bestMeta then + local thresholds = coachCfg.thresholds or {} local isMetric = _getPlayerIsMetric(unit) + -- Arrival phase - if bestd <= (coachCfg.thresholds.arrivalDist or 1000) then + if bestd <= (thresholds.arrivalDist or 1000) then _coachSend(self, group, uname, 'coach_arrival', {}, false) end + + -- Close-in guidance + if bestd <= (thresholds.closeDist or 100) then _coachSend(self, group, uname, 'coach_close', {}, false) end - -- Precision phase - if bestd <= (coachCfg.thresholds.precisionDist or 30) then + -- Precision phase + if bestd <= (thresholds.precisionDist or 30) then local hdg, _ = _headingRadDeg(unit) local dx = (bestMeta.point.x - p3.x) local dz = (bestMeta.point.z - p3.z) @@ -7745,8 +7759,8 @@ function CTLD:ScanHoverPickup() -- Vertical hint against AGL window local vHint - local aglMin = coachCfg.thresholds.aglMin or 5 - local aglMax = coachCfg.thresholds.aglMax or 20 + local aglMin = thresholds.aglMin or 5 + local aglMax = thresholds.aglMax or 20 if agl < aglMin then local dv, du = _fmtAGL(aglMin - agl, isMetric) vHint = string.format("Up %d %s", dv, du) @@ -7763,7 +7777,7 @@ function CTLD:ScanHoverPickup() _coachSend(self, group, uname, 'coach_hint', data, true) -- Error prompts (dominant one) - local maxGS = coachCfg.thresholds.maxGS or (8/3.6) + local maxGS = thresholds.maxGS or (8/3.6) local aglMinT = aglMin local aglMaxT = aglMax if gs > maxGS then diff --git a/Moose_CTLD_Pure/Moose_CTLD_Pure.miz b/Moose_CTLD_Pure/Moose_CTLD_Pure.miz index 627b53e..bbcfc6b 100644 Binary files a/Moose_CTLD_Pure/Moose_CTLD_Pure.miz and b/Moose_CTLD_Pure/Moose_CTLD_Pure.miz differ