From edf70e971cd8769ee3a8b705012378caf4adcdff Mon Sep 17 00:00:00 2001 From: Geoff Franks Date: Wed, 10 Jun 2026 21:02:40 -0400 Subject: [PATCH 01/18] fix(Spawn): air-spawn takeoff event never fired (wrong arg packing) SPAWN:SpawnAtAirbase scheduled the synthetic takeoff event for air-spawned groups with: self:ScheduleOnce(5, BASE.CreateEventTakeoff, {GroupSpawned, timer.getTime(), UnitSpawned:GetDCSObject()}) BASE:ScheduleOnce(Start, SchedulerFunction, ...) forwards its trailing arguments to the scheduled function as varargs (it wraps `...` in a table internally). Wrapping the arguments in a table here made that table argument number one, so CreateEventTakeoff ran with the {group, time, dcs} table as `self`. A plain table has no :F(), the very first line errored, and the S_EVENT_TAKEOFF event was never raised. Air-spawned AI_A2A_DISPATCHER GCI defenders therefore never activated (observed: zero QRA scrambled with takeoff = Air). The sibling call site in SpawnAtParkingSpot is correct because it uses SCHEDULER:New(nil, fn, {args}, 5), which DOES take a single argument table. Drop the braces so the args pass as varargs, matching CreateEventTakeoff's (self=group, EventTime, Initiator) signature. Co-Authored-By: Claude Opus 4.8 --- Moose Development/Moose/Core/Spawn.lua | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/Moose Development/Moose/Core/Spawn.lua b/Moose Development/Moose/Core/Spawn.lua index 3e5ffea8a..cfbbd79b8 100644 --- a/Moose Development/Moose/Core/Spawn.lua +++ b/Moose Development/Moose/Core/Spawn.lua @@ -2430,8 +2430,13 @@ function SPAWN:SpawnAtAirbase( SpawnAirbase, Takeoff, TakeoffAltitude, TerminalT -- When spawned in the air, we need to generate a Takeoff Event. if Takeoff == GROUP.Takeoff.Air then for UnitID, UnitSpawned in pairs( GroupSpawned:GetUnits() ) do - --SCHEDULER:New( nil, BASE.CreateEventTakeoff, { GroupSpawned, timer.getTime(), UnitSpawned:GetDCSObject() }, 5 ) --No need to create a new SCHEDULER instance every time! - self:ScheduleOnce(5, BASE.CreateEventTakeoff, {GroupSpawned, timer.getTime(), UnitSpawned:GetDCSObject()}) + -- BASE:ScheduleOnce forwards its trailing args to the scheduled function as + -- varargs, so they must NOT be wrapped in a table here (unlike SCHEDULER:New, + -- which takes a single argument table). Passing a table made it argument #1, + -- i.e. CreateEventTakeoff ran with that table as `self`, so the takeoff event + -- never fired and air-spawned groups (e.g. AI_A2A_DISPATCHER GCI defenders) + -- never activated. + self:ScheduleOnce(5, BASE.CreateEventTakeoff, GroupSpawned, timer.getTime(), UnitSpawned:GetDCSObject()) end end From 7e9dd9deff401d5badd0a1cc9fc605951dd107f5 Mon Sep 17 00:00:00 2001 From: Applevangelist Date: Tue, 23 Jun 2026 12:29:45 +0200 Subject: [PATCH 02/18] #EASYGCICAP - fall back to GCI behaviour if no CAP Points have been defined. --- Moose Development/Moose/Ops/EasyGCICAP.lua | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/Moose Development/Moose/Ops/EasyGCICAP.lua b/Moose Development/Moose/Ops/EasyGCICAP.lua index 1b2afb44b..8166e5d8e 100644 --- a/Moose Development/Moose/Ops/EasyGCICAP.lua +++ b/Moose Development/Moose/Ops/EasyGCICAP.lua @@ -135,6 +135,7 @@ -- local mywing = EASYGCICAP:New("Blue CAP Operations",AIRBASE.Caucasus.Kutaisi,"blue","Blue EWR") -- -- -- Add a CAP patrol point belonging to our airbase, we'll be at 30k ft doing 400 kn, initial direction 90 degrees (East), leg 20NM +-- -- NOTE - Skip this function and do not create CAP Points if you want GCI behaviour only. -- mywing:AddPatrolPointCAP(AIRBASE.Caucasus.Kutaisi,ZONE:FindByName("Blue Zone 1"):GetCoordinate(),30000,400,90,20) -- -- -- Add a Squadron with template "Blue Sq1 M2000c", 20 airframes, skill good, Modex starting with 102 and skin "Vendee Jeanne" @@ -286,7 +287,7 @@ EASYGCICAP = { --- EASYGCICAP class version. -- @field #string version -EASYGCICAP.version="0.1.36" +EASYGCICAP.version="0.1.37" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- @@ -754,6 +755,15 @@ end function EASYGCICAP:_AddAirwing(Airbasename, Alias) self:T(self.lid.."_AddAirwing "..Airbasename) + -- Gather Some Stats + local function counttable(tbl) + local count = 0 + for _,_data in pairs(tbl) do + count = count + 1 + end + return count + end + local CapFormation = self.CapFormation local DespawnAfterLanding = self.DespawnAfterLanding local DespawnAfterHolding = self.DespawnAfterHolding @@ -772,7 +782,11 @@ function EASYGCICAP:_AddAirwing(Airbasename, Alias) CAP_Wing:SetMarker(false) CAP_Wing:SetAirbase(AIRBASE:FindByName(Airbasename)) CAP_Wing:SetRespawnAfterDestroyed() - CAP_Wing:SetNumberCAP(self.capgrouping) + + --- #DONE avoid wings with no CAP points starting CAP anyhow; AirWing uses this to start CAP and creates points when there are none. + if counttable(self.ManagedCP) >0 then + CAP_Wing:SetNumberCAP(self.capgrouping) + end CAP_Wing:SetCapCloseRaceTrack(true) if self.showpatrolpointmarks then @@ -1039,7 +1053,7 @@ function EASYGCICAP:_SetCAPPatrolPoints() MESSAGE:New(self.lid.."You are trying to create a CAP point for which there is no wing! "..tostring(data.AirbaseName),30,"CHECK"):ToAllIf(self.debug):ToLog() return end - local Wing = self.wings[data.AirbaseName][1] -- Ops.Airwing#AIRWING + local Wing = self.wings[data.AirbaseName][1] -- Ops.AirWing#AIRWING local Coordinate = data.Coordinate local Altitude = data.Altitude local Speed = data.Speed From f769a0deb55e4b9bce3dc0fd4b4bbd248d8d5663 Mon Sep 17 00:00:00 2001 From: leka1986 <83298840+leka1986@users.noreply.github.com> Date: Wed, 24 Jun 2026 01:56:31 +0200 Subject: [PATCH 03/18] Update Utils.lua Added KA-50_3 door check. --- Moose Development/Moose/Utilities/Utils.lua | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Moose Development/Moose/Utilities/Utils.lua b/Moose Development/Moose/Utilities/Utils.lua index b75fe8935..d4e933658 100644 --- a/Moose Development/Moose/Utilities/Utils.lua +++ b/Moose Development/Moose/Utilities/Utils.lua @@ -2378,6 +2378,11 @@ function UTILS.IsLoadingDoorOpen( unit_name ) BASE:T(unit_name .. " front door(s) are open or doors removed") return true end + + if type_name == "Ka-50_3" and unit:getDrawArgumentValue(38) == 1 then + BASE:T(unit_name .. " cockpit door is open") + return true + end if type_name=="C-130J-30" and (unit:getDrawArgumentValue(86)==1)then BASE:T(unit_name.." rear doors are open") From 8c60a8c678954592cb4f780acdfeec6e031e33a2 Mon Sep 17 00:00:00 2001 From: Thomas <72444570+Applevangelist@users.noreply.github.com> Date: Fri, 26 Jun 2026 12:31:54 +0200 Subject: [PATCH 04/18] Update ScheduleDispatcher.lua Undoing previous change as some delayed event calls stopped working, probably FSM. Need to find the reason for that. --- Moose Development/Moose/Core/ScheduleDispatcher.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Moose Development/Moose/Core/ScheduleDispatcher.lua b/Moose Development/Moose/Core/ScheduleDispatcher.lua index 0d6fa6df5..05fe6d25f 100644 --- a/Moose Development/Moose/Core/ScheduleDispatcher.lua +++ b/Moose Development/Moose/Core/ScheduleDispatcher.lua @@ -246,11 +246,11 @@ function SCHEDULEDISPATCHER:AddSchedule( Scheduler, ScheduleFunction, ScheduleAr -- self:T3( { Repeat = CallID, CurrentTime, ScheduleTime, ScheduleArguments } ) return ScheduleTime -- returns the next time the function needs to be called. else - self:_Reclaim( Scheduler, CallID ) + self:Stop( Scheduler, CallID ) end else - self:_Reclaim( Scheduler, CallID ) + self:Stop( Scheduler, CallID ) end else self:I( "<<<>" .. Name .. ":" .. Line .. " (" .. Source .. ")" ) From 626b12f486db981dadd71721044cb9f530c2eb9a Mon Sep 17 00:00:00 2001 From: Applevangelist Date: Sat, 27 Jun 2026 10:57:27 +0200 Subject: [PATCH 05/18] #MSRS #HOUND corrected frequency formatting to allow 1000ths of a frequency --- Moose Development/Moose/Sound/SRS.lua | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Moose Development/Moose/Sound/SRS.lua b/Moose Development/Moose/Sound/SRS.lua index d048bc844..d69a703f7 100644 --- a/Moose Development/Moose/Sound/SRS.lua +++ b/Moose Development/Moose/Sound/SRS.lua @@ -2108,7 +2108,7 @@ function MSRS:_HoundTextToSpeech(Message,Frequencies,Modulations,Volume,Label,Co local ffs = {} for _,_f in pairs(Frequencies) do - table.insert(ffs,string.format("%.1f",_f)) + table.insert(ffs,string.format("%.3f",_f)) end local freqs = table.concat(ffs, ",") @@ -2214,7 +2214,7 @@ function MSRS:_HoundTestTone(Frequencies, Modulations, Coalition) local ffs = {} for _,_f in pairs(Frequencies or self.frequencies) do - table.insert(ffs,string.format("%.1f",_f)) + table.insert(ffs,string.format("%.3f",_f)) end local freqs = table.concat(ffs, ",") @@ -2291,7 +2291,7 @@ function MSRS:RadioJammerOn(Frequencies, Modulations, Coalition, Noisetype, Volu local ffs = {} for _,_f in pairs(Frequencies or self.frequencies) do - table.insert(ffs,string.format("%.1f",_f)) + table.insert(ffs,string.format("%.3f",_f)) end local freqs = table.concat(ffs, ",") From 98ed7d50c19343a2409304a44de55d2b3ff63ca2 Mon Sep 17 00:00:00 2001 From: Applevangelist Date: Sun, 28 Jun 2026 09:30:45 +0200 Subject: [PATCH 06/18] #EASYGCICAP * Added `SetDefaultINTERCEPTAlt()` --- Moose Development/Moose/Ops/EasyGCICAP.lua | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/Moose Development/Moose/Ops/EasyGCICAP.lua b/Moose Development/Moose/Ops/EasyGCICAP.lua index 8166e5d8e..d1d499ddb 100644 --- a/Moose Development/Moose/Ops/EasyGCICAP.lua +++ b/Moose Development/Moose/Ops/EasyGCICAP.lua @@ -191,9 +191,10 @@ -- * @{#EASYGCICAP.SetDefaultResurrection}: Set how many seconds the AirWing stays inoperable after the AirWing STATIC HQ ist destroyed, default 900 secs. -- * @{#EASYGCICAP.SetDefaultCAPSpeed}: Set how many knots the CAP flights should do (will be altitude corrected), default 300 kn. -- * @{#EASYGCICAP.SetDefaultCAPAlt}: Set at which altitude (ASL) the CAP planes will fly, default 25,000 ft. +-- * @{#EASYGCICAP.SetDefaultINTERCEPTAlt}: Set at which altitude (ASL) the Intercept planes will fly, default 25,000 ft. -- * @{#EASYGCICAP.SetDefaultCAPDirection}: Set the initial direction from the CAP point the planes will fly in degrees, default is 90°. -- * @{#EASYGCICAP.SetDefaultCAPLeg}: Set the length of the CAP leg, default is 15 NM. --- * @{#EASYGCICAP.SetDefaultCAPGrouping}: Set how many planes will be spawned per mission (CVAP/GCI), defaults to 2. +-- * @{#EASYGCICAP.SetDefaultCAPGrouping}: Set how many planes will be spawned per mission (CAP/GCI), defaults to 2. -- * @{#EASYGCICAP.SetDefaultMissionRange}: Set how many NM the planes can go from the home base, defaults to 100. -- * @{#EASYGCICAP.SetDefaultNumberAlert5Standby}: Set how many planes will be spawned on cold standby (Alert5), default 2. -- * @{#EASYGCICAP.SetDefaultEngageRange}: Set max engage range for CAP flights if they detect intruders, defaults to 50. @@ -287,7 +288,7 @@ EASYGCICAP = { --- EASYGCICAP class version. -- @field #string version -EASYGCICAP.version="0.1.37" +EASYGCICAP.version="0.1.38" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- @@ -588,6 +589,17 @@ function EASYGCICAP:SetDefaultCAPAlt(Altitude) return self end +--- Set default INTERCEPT Altitude in feet +-- @param #EASYGCICAP self +-- @param #number Altitude (Optional) Altitude defaults to 25000 +-- @return #EASYGCICAP self +function EASYGCICAP:SetDefaultINTERCEPTAlt(Altitude) + self:T(self.lid.."SetDefaultINTERCEPTAlt") + self.interceptalt = Altitude or 25000 + return self +end +-- + --- Set default CAP lieg initial direction in degrees -- @param #EASYGCICAP self -- @param #number Direction (Optional) Direction defaults to 90 (East) @@ -1519,6 +1531,7 @@ function EASYGCICAP:_AssignIntercept(Cluster) local overhead = self.overhead local capspeed = self.capspeed + 100 local capalt = self.capalt + local interalt = self.interceptalt or self.capalt local maxsize = self.maxinterceptsize local repeatsonfailure = self.repeatsonfailure @@ -1606,6 +1619,7 @@ function EASYGCICAP:_AssignIntercept(Cluster) :SetRepeatOnFailure(repeats) :SetMissionSpeed(UTILS.KnotsToAltKIAS(capspeed,capalt)) :SetMissionAltitude(capalt) + :SetEngageAltitude(interalt) if nogozoneset:Count() > 0 then InterceptAuftrag:AddConditionSuccess( From af4f20af67542755f1c63440b0f107670b65dfcf Mon Sep 17 00:00:00 2001 From: leka1986 <83298840+leka1986@users.noreply.github.com> Date: Sun, 12 Jul 2026 13:30:33 +0200 Subject: [PATCH 07/18] Fix aircraft model names for Lancer and Kiowa --- Moose Development/Moose/Utilities/Enums.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Moose Development/Moose/Utilities/Enums.lua b/Moose Development/Moose/Utilities/Enums.lua index 62fe643d7..2ca2f6c79 100644 --- a/Moose Development/Moose/Utilities/Enums.lua +++ b/Moose Development/Moose/Utilities/Enums.lua @@ -523,7 +523,7 @@ ENUMS.ReportingName = Tornado = "Tornado", -- Transport / Bomber / Others Atlas = "A400", - Lancer = "B1-B", + Lancer = "B-1B", Stratofortress = "B-52H", Herc = "C-130", Hercules = "C-130J-30", @@ -568,7 +568,7 @@ ENUMS.ReportingName = Apache = "AH-64", Chinook = "CH-47", Sea_Stallion = "CH-53", - Kiowa = "OH-58", + Kiowa = "OH58D", Seahawk = "SH-60", Blackhawk = "UH-60", Sea_King = "S-61", From 3d79a16d3a831021d802d30fe1973a53de93205d Mon Sep 17 00:00:00 2001 From: Shafik Date: Mon, 13 Jul 2026 11:02:42 +0300 Subject: [PATCH 08/18] [FIX] "Parameter #1 (group id) missed" refreshing menus for dead groups --- Moose Development/Moose/Ops/CTLD.lua | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/Moose Development/Moose/Ops/CTLD.lua b/Moose Development/Moose/Ops/CTLD.lua index 2b7bc20fa..28e06c80e 100644 --- a/Moose Development/Moose/Ops/CTLD.lua +++ b/Moose Development/Moose/Ops/CTLD.lua @@ -6171,11 +6171,11 @@ function CTLD:_BuildObjectFromCrates(Group,Unit,Build,Repair,RepairLocation,Mult local ctype = Build.Type -- #CTLD_CARGO.Enum local canmove = false if ctype == CTLD_CARGO.Enum.VEHICLE then canmove = true end - if ctype == CTLD_CARGO.Enum.STATIC then - return self + if ctype == CTLD_CARGO.Enum.STATIC then + return self end local temptable = Build.Template or {} - if type(temptable) == "string" then + if type(temptable) == "string" then temptable = {temptable} end local zone = nil -- Core.Zone#ZONE_RADIUS @@ -6217,8 +6217,11 @@ function CTLD:_BuildObjectFromCrates(Group,Unit,Build,Repair,RepairLocation,Mult self:__CratesBuild(1,Group,Unit,self.DroppedTroops[self.TroopCounter]) end end -- template loop - self:_RefreshLoadCratesMenu(Group, Unit) - self:_RefreshPackMenus(Group, Unit) + + if Group and Group:IsAlive() and Group:GetID() then + self:_RefreshLoadCratesMenu(Group, Unit) + self:_RefreshPackMenus(Group, Unit) + end else self:T(self.lid.."Group KIA while building!") end From 338ec34604ba55cf441284d2b7ea9f4bc407867e Mon Sep 17 00:00:00 2001 From: patpatpowercat <56987247+patpatpowercat@users.noreply.github.com> Date: Tue, 14 Jul 2026 21:47:47 -0700 Subject: [PATCH 09/18] Update Mantis.lua Added ships to MANTIS. NAVYGROUPs may be integrated. Need to be aware MANTIS doesn't change ROE. SEAD does not (by default) effect ships. Ships are individually identified, not as group. Group will react to jamming as a whole, will go live/sleep based upon longest range/best radar. No longer need prefixes for either ground or naval units. --- Moose Development/Moose/Functional/Mantis.lua | 908 +++++++++++++++++- 1 file changed, 858 insertions(+), 50 deletions(-) diff --git a/Moose Development/Moose/Functional/Mantis.lua b/Moose Development/Moose/Functional/Mantis.lua index 91f728ac3..858b9e302 100644 --- a/Moose Development/Moose/Functional/Mantis.lua +++ b/Moose Development/Moose/Functional/Mantis.lua @@ -524,6 +524,137 @@ MANTIS.SamDataCH = { ["Lvkv9040M CHM"] = { Range=2, Blindspot=0.1, Height=1.2, Type="Point", Radar="LvKv9040",Point="true" }, } + +--- @type MANTIS.SamDataNaval +MANTIS.SamDataNaval = { + --------------------------------------------------------------------- + -- CurrentHill (CH) surface combatants + --------------------------------------------------------------------- + -- USA -- AEGIS / SM-family, S-band (SPY-1) or S-band AESA (SPY-6) + ["Ticonderoga"] = { Range=150, Blindspot=4, Height=33, Type="Long", Radar="CH_Ticonderoga" }, + ["Ticonderoga CMP"] = { Range=150, Blindspot=4, Height=33, Type="Long", Radar="CH_Ticonderoga_CMP" }, + ["Arleigh Burke IIA"] = { Range=150, Blindspot=4, Height=33, Type="Long", Radar="CH_Arleigh_Burke_IIA" }, + ["Arleigh Burke III"] = { Range=180, Blindspot=3, Height=33, Type="Long", Radar="CH_Arleigh_Burke_III" }, + ["Constellation"] = { Range=120, Blindspot=3, Height=27, Type="Long", Radar="CH_Constellation" }, + + -- UK + ["Type 45"] = { Range=120, Blindspot=3, Height=30, Type="Long", Radar="Type45" }, -- Aster 15/30, SAMPSON + ["Type 26"] = { Range=25, Blindspot=1, Height=10, Type="Medium", Radar="CH_Type26" }, -- Sea Ceptor / CAMM + + -- Germany + ["F124 Sachsen"] = { Range=120, Blindspot=3, Height=24, Type="Long", Radar="CH_F124" }, -- SM-2/ESSM, APAR (X-band) + + -- China + ["Type 052D"] = { Range=100, Blindspot=5, Height=27, Type="Long", Radar="Type052D" }, -- HHQ-9 + ["Type 055"] = { Range=150, Blindspot=5, Height=30, Type="Long", Radar="Type055" }, -- HHQ-9B + ["Type 054B"] = { Range=70, Blindspot=2, Height=20, Type="Medium", Radar="CH_Type054B" }, -- HHQ-16 + ["Type 056A"] = { Range=9, Blindspot=0.3, Height=6, Type="Point", Radar="CH_Type056A", Point="true" }, -- HHQ-10 (IR) + + -- Russia + ["Admiral Gorshkov"] = { Range=150, Blindspot=3, Height=30, Type="Long", Radar="CH_Admiral_Gorshkov" }, -- Poliment-Redut 9M96D + ["Grigorovich AShM"] = { Range=50, Blindspot=2, Height=15, Type="Medium", Radar="CH_Grigorovich_AShM" }, -- Shtil-1 + ["Grigorovich LACM"] = { Range=50, Blindspot=2, Height=15, Type="Medium", Radar="CH_Grigorovich_LACM" }, + ["Steregushchiy"] = { Range=50, Blindspot=1.5, Height=20, Type="Medium", Radar="CH_Steregushchiy" }, -- Redut 9M96 + ["Gremyashchiy AShM"] = { Range=50, Blindspot=1.5, Height=20, Type="Medium", Radar="CH_Gremyashchiy_AShM" }, + ["Gremyashchiy LACM"] = { Range=50, Blindspot=1.5, Height=20, Type="Medium", Radar="CH_Gremyashchiy_LACM" }, + ["Karakurt AShM"] = { Range=20, Blindspot=1, Height=15, Type="Point", Radar="CH_Karakurt_AShM", Point="true" }, -- Pantsir-M + ["Karakurt LACM"] = { Range=20, Blindspot=1, Height=15, Type="Point", Radar="CH_Karakurt_LACM", Point="true" }, + + -- Point-defense / MANPADS-only hulls (treated as close-in; also usable as EWR pickets) + ["Type 022 FAC"] = { Range=5, Blindspot=0.2, Height=4, Type="Point", Radar="CH_Type022", Point="true" }, + ["IRGCN FAC AShM"] = { Range=5, Blindspot=0.2, Height=4, Type="Point", Radar="CH_IranFAC_MG", Point="true" }, -- MANPADS + ["IRGCN FAC Igla"] = { Range=5, Blindspot=0.2, Height=4, Type="Point", Radar="CH_IranFAC_MG_AShM", Point="true" }, -- Igla + ["Strb 90 FAC"] = { Range=4, Blindspot=0.2, Height=3, Type="Point", Radar="Strb90", Point="true" }, + ["Visby"] = { Range=4, Blindspot=0.2, Height=3, Type="Point", Radar="HSwMS_Visby", Point="true" }, -- gun/CIWS + + --------------------------------------------------------------------- + -- Base-game DCS surface combatants (type ids from pydcs / DCS export) + -- Only air-defence-capable hulls are listed. Pure-gun / no-AD hulls + -- (Leander gun refits, La Combattante, cargo, subs, WWII) are omitted + -- on purpose; add them as POINT if you want them to appear at all. + --------------------------------------------------------------------- + -- USA + ["CG Ticonderoga"] = { Range=120, Blindspot=3, Height=30, Type="Long", Radar="TICONDEROG" }, -- SM-2/SPY-1 NOTE: id has NO trailing 'E' + ["DDG Arleigh Burke IIa"] = { Range=120, Blindspot=3, Height=30, Type="Long", Radar="USS_Arleigh_Burke_IIa" }, -- SM-2/SPY-1D + ["FFG Perry"] = { Range=38, Blindspot=2, Height=15, Type="Medium", Radar="PERRY" }, -- SM-1MR (est. real range) + ["CVN Roosevelt"] = { Range=15, Blindspot=1, Height=8, Type="Point", Radar="CVN_71", Point="true" }, -- NSSM/RAM + ["CVN Lincoln"] = { Range=15, Blindspot=1, Height=8, Type="Point", Radar="CVN_72", Point="true" }, + ["CVN Washington"] = { Range=15, Blindspot=1, Height=8, Type="Point", Radar="CVN_73", Point="true" }, + ["CVN Stennis"] = { Range=15, Blindspot=1, Height=8, Type="Point", Radar="Stennis", Point="true" }, + ["CVN Truman"] = { Range=15, Blindspot=1, Height=8, Type="Point", Radar="CVN_75", Point="true" }, + ["CVN Vinson"] = { Range=15, Blindspot=1, Height=8, Type="Point", Radar="VINSON", Point="true" }, + ["LHA Tarawa"] = { Range=15, Blindspot=1, Height=8, Type="Point", Radar="LHA_Tarawa", Point="true" }, -- RAM/Sea Sparrow + -- Russia + ["Cruiser Moskva"] = { Range=75, Blindspot=5, Height=25, Type="Long", Radar="MOSCOW" }, -- S-300F (SA-N-6) + ["BC Pyotr Velikiy"] = { Range=150, Blindspot=5, Height=27, Type="Long", Radar="PIOTR" }, -- S-300F/FM + ["Frigate Neustrashimy"] = { Range=12, Blindspot=1, Height=6, Type="Short", Radar="NEUSTRASH" }, -- Kinzhal (SA-N-9) + ["Frigate Rezky"] = { Range=15, Blindspot=1, Height=5, Type="Short", Radar="REZKY" }, -- OSA-M (SA-N-4) + ["Corvette Grisha"] = { Range=15, Blindspot=1, Height=5, Type="Short", Radar="ALBATROS" }, -- OSA-M (SA-N-4) + ["Corvette Molniya"] = { Range=5, Blindspot=0.3, Height=3, Type="Point", Radar="MOLNIYA", Point="true" }, -- Strela + ["CV Kuznetsov"] = { Range=12, Blindspot=1, Height=6, Type="Short", Radar="KUZNECOW" }, -- Kinzhal/Kashtan + ["CV Kuznetsov 2017"] = { Range=12, Blindspot=1, Height=6, Type="Short", Radar="CV_1143_5" }, + ["Patrol Bykov TorM2KM"] = { Range=15, Blindspot=1, Height=6, Type="Short", Radar="CHAP_Project22160_TorM2KM" }, -- Tor + ["Patrol Bykov"] = { Range=7, Blindspot=0.3, Height=4, Type="Point", Radar="CHAP_Project22160", Point="true" }, + -- China + ["Type 052C"] = { Range=100, Blindspot=5, Height=27, Type="Long", Radar="Type_052C" }, -- HHQ-9 + ["Type 052B"] = { Range=30, Blindspot=2, Height=15, Type="Medium", Radar="Type_052B" }, -- HHQ-16/Shtil + ["Type 054A"] = { Range=45, Blindspot=2, Height=18, Type="Medium", Radar="Type_054A" }, -- HHQ-16 + ["Type 071 LPD"] = { Range=9, Blindspot=0.5, Height=6, Type="Point", Radar="Type_071", Point="true" }, -- HHQ-10 + -- UK + ["HMS Invincible"] = { Range=40, Blindspot=2, Height=18, Type="Medium", Radar="hms_invincible" }, -- Sea Dart + + --------------------------------------------------------------------- + -- desdemicabina-simulation 3.2 (Spanish/Australian navies) + -- AEGIS hulls: two ship_MK41_SM2 launchers (SM-2, 64 cells) + AEGIS-class + -- trackers (150km/30km alt, ECM_K 0.5). NOTE: this pack's airWeaponDist=45km + -- UNDERSTATES the fit -- Range below follows the SM-2 launcher (~100km), the + -- inverse of the Admiral pack's inflated values. + --------------------------------------------------------------------- + ["HMAS Hobart DDG-39"] = { Range=100, Blindspot=3, Height=30, Type="Long", Radar="DDG39" }, -- AEGIS SPY-1D(V), SM-2 + ["F100 Alvaro de Bazan"] = { Range=100, Blindspot=3, Height=30, Type="Long", Radar="F100" }, -- AEGIS SPY-1D, SM-2 + ["F105 Cristobal Colon"] = { Range=100, Blindspot=3, Height=30, Type="Long", Radar="F105" }, -- AEGIS SPY-1D, SM-2 + ["LHD Canberra L02"] = { Range=3, Blindspot=0.1, Height=2, Type="Point", Radar="L02", Point="true" }, -- 3x Phalanx only + ["LPD Castilla L52"] = { Range=3, Blindspot=0.1, Height=2, Type="Point", Radar="L52", Point="true" }, -- 2x Phalanx (NO_SAM attr) + ["LHD Juan Carlos I L61"] = { Range=1.5, Blindspot=0.1, Height=0.8, Type="Point", Radar="L61", Point="true" }, -- .50cal mounts only + + --------------------------------------------------------------------- + -- Admiral mod surface combatants + -- Worked example (USS America LHA-6). Read each field from the ship's + -- GT def file: + -- Radar <- GT.Name (the typeName; the essential, non-guessable key) + -- Range <- GT.airWeaponDist / 1000 (20000 -> 20) + -- Height <- launcher max_trg_alt / 1000 (15000 -> 15) + -- Blindspot <- launcher distanceMin / 1000 (400 -> 0.4) + -- Type <- from armament fit (Sea Sparrow => SHORT) + --------------------------------------------------------------------- + ["USS America LHA-6"] = { Range=20, Blindspot=0.4, Height=15, Type="Short", Radar="USS America LHA-6" }, -- Sea Sparrow + RAM + Phalanx + + -- IMPORTANT: these Range values come from the actual weapon launcher's distanceMax + -- / the real system, NOT GT.airWeaponDist (which is unreliable here -- e.g. Blue + -- Ridge and Henry Kaiser both claim 100-150km with only CIWS/MGs). + -- LONG -- area SAM + ["HMS Duncan (Type 45)"] = { Range=100, Blindspot=3, Height=30, Type="Long", Radar="HMS Duncan" }, -- Aster/Sea Viper (mod: MK41_SM2) + ["Aquitaine D650 (FREMM)"] = { Range=120, Blindspot=3, Height=27, Type="Long", Radar="Aquitaine D650" }, -- SYLVER Aster 30 (distanceMax 120km) + ["Normandie D651 (FREMM)"] = { Range=120, Blindspot=3, Height=27, Type="Long", Radar="D651_Normandie" }, -- SYLVER Aster 30 + -- MEDIUM + ["MEKO-200HN Hydra"] = { Range=50, Blindspot=2, Height=20, Type="Medium", Radar="MEKO_HN" }, -- MK41 (mod SM-2 / real ESSM) + ["MEKO-200TN Barbaros"] = { Range=50, Blindspot=2, Height=20, Type="Medium", Radar="MEKO_TN" }, -- MK41 + SeaZenith CIWS + ["Sovremenny Adm. Ushakov"] = { Range=46, Blindspot=2, Height=15, Type="Medium", Radar="Admiral Ushakov" }, -- SA-N-7 Gadfly + -- SHORT + ["Udaloy II Chabanenko"] = { Range=12, Blindspot=1, Height=6, Type="Short", Radar="Udaloy II DDG Admiral Chabanenko" }, -- SA-N-9 Klinok + ["USS San Antonio LPD-17"] = { Range=15, Blindspot=1, Height=8, Type="Short", Radar="USS San Antonio LPD-17" }, -- Sea Sparrow + -- POINT -- CIWS / RAM / gun / IR only (no area SAM); unjammable in the jammer table + ["USS Oak Hill LSD-51"] = { Range=9, Blindspot=0.5, Height=6, Type="Point", Radar="USS_Oak_Hill LSD-51", Point="true" }, -- RAM + Phalanx + ["USS Blue Ridge LCC-19"] = { Range=3, Blindspot=0.1, Height=2, Type="Point", Radar="USS_Blue_Ridge", Point="true" }, -- Phalanx only + ["Sacramento AOE-1"] = { Range=9, Blindspot=0.5, Height=6, Type="Point", Radar="USS_Support_Ship_Sacramento_AOE-1", Point="true" }, -- RAM + Phalanx + ["Henry Kaiser T-AO-187"] = { Range=1.5, Blindspot=0.1, Height=0.8,Type="Point", Radar="USNS_Henry_Kaiser", Point="true" }, -- .50cal only (negligible) + ["HMS Albion L14"] = { Range=3, Blindspot=0.1, Height=2, Type="Point", Radar="HMS_Albion L14", Point="true" }, -- Phalanx only + ["LPD Foudre"] = { Range=6, Blindspot=0.3, Height=4, Type="Point", Radar="LPD_Foundre", Point="true" }, -- Mistral (IR) + ["Osa-I Missile Boat"] = { Range=4, Blindspot=0.2, Height=3, Type="Point", Radar="OSA_One", Point="true" }, -- AK-230 (NO_SAM) + ["Osa-II Missile Boat"] = { Range=4, Blindspot=0.2, Height=3, Type="Point", Radar="OSA_Two", Point="true" }, -- AK-230 + ["IRIS Alvand"] = { Range=4, Blindspot=0.2, Height=3, Type="Point", Radar="IRIS_Alvand", Point="true" }, -- OtoBreda 40mm / 20mm +} + ----------------------------------------------------------------------- -- MANTIS Jammer Extension v2.0.0 -- Standoff Jamming (SOJ) aircraft support for MANTIS IADS networks @@ -705,6 +836,119 @@ do end +--- @type MANTIS.JammerSAMParamsNaval +MANTIS.JammerSAMParamsNaval = { + -- Calibration note (research pass): high power-aperture warship radars + -- (SPY-1/SPY-6/SAMPSON/Poliment/HHQ-9B) are modeled HARDER to jam than their + -- ground peers -- lower peak/floor, mu pushed outward -- reflecting shipboard + -- prime power, large multi-face arrays, low sidelobes and sensor redundancy. + -- Documented ECM-weak legacy sets (SPG-51 CW illum, Type 909) slightly easier. + -- Navalized twins remain anchored to ground counterparts (Shtil=SA-11, + -- Klinok=Tor, OSA-M=SA-8, S-300F=SA-10). Open sources support ordering, not + -- exact percentages -- tune in-mission as needed. + -- USA AEGIS + ["CH_Ticonderoga"] = { peak=17, mu=70, sigma_L=24, tail_dist=115, band="S", floor=2 }, + ["CH_Ticonderoga_CMP"] = { peak=17, mu=70, sigma_L=24, tail_dist=115, band="S", floor=2 }, + ["CH_Arleigh_Burke_IIA"] = { peak=17, mu=70, sigma_L=24, tail_dist=115, band="S", floor=2 }, + ["CH_Arleigh_Burke_III"] = { peak=14, mu=83, sigma_L=28, tail_dist=130, band="S", floor=2 }, -- SPY-6 AESA + ["CH_Constellation"] = { peak=14, mu=63, sigma_L=22, tail_dist=100, band="S", floor=2 }, -- SPY-6(V)3 + -- UK + ["Type45"] = { peak=16, mu=62, sigma_L=24, tail_dist=110, band="S", floor=2 }, -- SAMPSON + ["CH_Type26"] = { peak=18, mu=18, sigma_L=7, tail_dist=40, band="IJ", floor=2 }, -- CAMM active RH + -- Germany + ["CH_F124"] = { peak=18, mu=62, sigma_L=24, tail_dist=110, band="IJ", floor=2 }, -- APAR X-band + -- China + ["Type052D"] = { peak=20, mu=55, sigma_L=22, tail_dist=110, band="S", floor=2 }, + ["Type055"] = { peak=16, mu=65, sigma_L=24, tail_dist=120, band="S", floor=2 }, + ["CH_Type054B"] = { peak=22, mu=38, sigma_L=16, tail_dist=75, band="S", floor=3 }, + ["CH_Type056A"] = { peak=8, mu=5, sigma_L=2, tail_dist=12, band="OPT",floor=0 }, -- HHQ-10 IR + -- Russia + ["CH_Admiral_Gorshkov"] = { peak=16, mu=80, sigma_L=30, tail_dist=145, band="S", floor=2 }, -- Poliment AESA + ["CH_Grigorovich_AShM"] = { peak=50, mu=28, sigma_L=12, tail_dist=58, band="IJ", floor=3 }, -- Shtil-1 + ["CH_Grigorovich_LACM"] = { peak=50, mu=28, sigma_L=12, tail_dist=58, band="IJ", floor=3 }, + ["CH_Steregushchiy"] = { peak=22, mu=35, sigma_L=15, tail_dist=70, band="S", floor=2 }, -- Redut + ["CH_Gremyashchiy_AShM"] = { peak=22, mu=35, sigma_L=15, tail_dist=70, band="S", floor=2 }, + ["CH_Gremyashchiy_LACM"] = { peak=22, mu=35, sigma_L=15, tail_dist=70, band="S", floor=2 }, + ["CH_Karakurt_AShM"] = { peak=10, mu=9, sigma_L=4, tail_dist=20, band="IJ", floor=0 }, -- Pantsir-M + ["CH_Karakurt_LACM"] = { peak=10, mu=9, sigma_L=4, tail_dist=20, band="IJ", floor=0 }, + -- CIWS / MANPADS-only hulls -> optical, essentially unjammable (floor 0) + ["CH_Type022"] = { peak=3, mu=3, sigma_L=1, tail_dist=5, band="OPT",floor=0 }, + ["CH_IranFAC_MG"] = { peak=3, mu=3, sigma_L=1, tail_dist=5, band="OPT",floor=0 }, + ["CH_IranFAC_MG_AShM"] = { peak=3, mu=3, sigma_L=1, tail_dist=5, band="OPT",floor=0 }, + ["Strb90"] = { peak=3, mu=3, sigma_L=1, tail_dist=5, band="OPT",floor=0 }, + ["HSwMS_Visby"] = { peak=3, mu=3, sigma_L=1, tail_dist=5, band="OPT",floor=0 }, -- gun/CIWS: unjammable + + -- Base-game DCS hulls (canonical entries; aliases set in the do-block below) + ["TICONDEROG"] = { peak=17, mu=60, sigma_L=22, tail_dist=110, band="S", floor=2 }, -- SPY-1 (NO trailing E) + ["USS_Arleigh_Burke_IIa"] = { peak=17, mu=60, sigma_L=22, tail_dist=110, band="S", floor=2 }, -- SPY-1D + ["PERRY"] = { peak=33, mu=20, sigma_L=9, tail_dist=48, band="IJ", floor=3 }, -- SM-1 / SPG-51 (X-band illum) + ["MOSCOW"] = { peak=32, mu=42, sigma_L=18, tail_dist=90, band="S", floor=3 }, -- S-300F + ["PIOTR"] = { peak=28, mu=70, sigma_L=26, tail_dist=130, band="S", floor=3 }, -- S-300F/FM + ["NEUSTRASH"] = { peak=28, mu=8, sigma_L=3.5,tail_dist=18, band="IJ", floor=2 }, -- Kinzhal + ["REZKY"] = { peak=35, mu=8, sigma_L=3.5,tail_dist=18, band="IJ", floor=3 }, -- OSA-M + ["MOLNIYA"] = { peak=3, mu=3, sigma_L=1, tail_dist=5, band="OPT",floor=0 }, -- Strela + ["KUZNECOW"] = { peak=24, mu=8, sigma_L=3.5,tail_dist=18, band="IJ", floor=2 }, -- Kinzhal/Kashtan + ["CHAP_Project22160_TorM2KM"] = { peak=28, mu=12, sigma_L=5, tail_dist=28, band="IJ", floor=3 }, -- Tor M2KM + ["Type_052C"] = { peak=20, mu=55, sigma_L=22, tail_dist=110, band="S", floor=2 }, -- HHQ-9 + ["Type_052B"] = { peak=30, mu=20, sigma_L=10, tail_dist=48, band="S", floor=3 }, -- HHQ-16/Shtil + ["Type_054A"] = { peak=26, mu=28, sigma_L=12, tail_dist=58, band="S", floor=3 }, -- HHQ-16 + ["hms_invincible"] = { peak=36, mu=25, sigma_L=11, tail_dist=52, band="IJ", floor=3 }, -- Sea Dart / Type 909 + ["CVN_71"] = { peak=8, mu=5, sigma_L=2, tail_dist=12, band="OPT",floor=0 }, -- NSSM/RAM (carrier point) + + -- Admiral mod -- worked example (USS America LHA-6): + -- band <- radar band (Sea Sparrow Mk 95 illuminator = X-band => IJ) + -- floor <- inverse of GT ECM_K hint (ECM_K 0.65 = moderate => floor ~2-3) + -- mu <- engagement range in nm (20 km ~= 11 nm) + -- peak/sigma_L/tail_dist <- copy nearest short-IJ analog (Sea Sparrow ~ NASAMS/SA-11 class) + ["USS America LHA-6"] = { peak=22, mu=11, sigma_L=5, tail_dist=25, band="IJ", floor=3 }, + -- desdemicabina AEGIS hulls: SPY-1 family fingerprint (peak 17 / floor 2 / S), + -- mu scaled to the ~100km SM-2 envelope of these units. + ["DDG39"] = { peak=17, mu=55, sigma_L=22, tail_dist=110, band="S", floor=2 }, -- SPY-1D(V) + ["F100"] = { peak=17, mu=55, sigma_L=22, tail_dist=110, band="S", floor=2 }, -- SPY-1D + ["F105"] = { peak=17, mu=55, sigma_L=22, tail_dist=110, band="S", floor=2 }, -- SPY-1D + -- Admiral SAM shooters (POINT hulls are aliased to the CIWS OPT entry in the do-block below) + ["HMS Duncan"] = { peak=16, mu=54, sigma_L=22, tail_dist=110, band="S", floor=2 }, -- Aster (ECM_K 0.65) + ["Aquitaine D650"] = { peak=16, mu=65, sigma_L=24, tail_dist=120, band="S", floor=2 }, -- Aster 30 (mod ECM_K 5.0 - see note) + ["D651_Normandie"] = { peak=16, mu=65, sigma_L=24, tail_dist=120, band="S", floor=2 }, -- Aster 30 + ["MEKO_HN"] = { peak=22, mu=27, sigma_L=12, tail_dist=55, band="IJ", floor=2 }, -- ESSM/SM-2 + ["MEKO_TN"] = { peak=22, mu=27, sigma_L=12, tail_dist=55, band="IJ", floor=2 }, + ["Admiral Ushakov"] = { peak=48, mu=25, sigma_L=11, tail_dist=55, band="IJ", floor=3 }, -- SA-N-7 Gadfly + ["Udaloy II DDG Admiral Chabanenko"] = { peak=28, mu=7, sigma_L=3, tail_dist=18, band="IJ", floor=3 }, -- SA-N-9 Klinok + ["USS San Antonio LPD-17"] = { peak=22, mu=8, sigma_L=4, tail_dist=20, band="IJ", floor=3 }, -- Sea Sparrow + ["LPD_Foundre"] = { peak=10, mu=3, sigma_L=2, tail_dist=10, band="OPT",floor=0 }, -- Mistral (IR, near-unjammable) +} + +-- Naval jammer aliases: variants that share a hull/radar point to one curve, +-- so tuning the canonical entry updates all of them (mirrors JammerSAMParams). +do + local n = MANTIS.JammerSAMParamsNaval + -- US carriers + LHA + Type 071: point self-defence (NSSM/RAM/HHQ-10), optical-grade + n["CVN_72"] = n["CVN_71"]; n["CVN_73"] = n["CVN_71"]; n["CVN_75"] = n["CVN_71"] + n["Stennis"] = n["CVN_71"]; n["VINSON"] = n["CVN_71"]; n["LHA_Tarawa"] = n["CVN_71"] + n["Type_071"] = n["CVN_71"] + -- Grisha shares the SA-N-4 (OSA-M) curve with Rezky + n["ALBATROS"] = n["REZKY"] + -- Kuznetsov 2017 variant + n["CV_1143_5"] = n["KUZNECOW"] + -- Vasily Bykov base (gun) ~ Strela-grade optical + n["CHAP_Project22160"] = n["MOLNIYA"] + -- Admiral mod CIWS / RAM / gun-only hulls: all optical-grade, essentially unjammable. + -- Aliased to the base OPT curve so they resolve without cluttering the table. + n["USS_Oak_Hill LSD-51"] = n["MOLNIYA"] -- RAM + Phalanx + n["USS_Blue_Ridge"] = n["MOLNIYA"] -- Phalanx + n["USS_Support_Ship_Sacramento_AOE-1"] = n["MOLNIYA"] -- RAM + Phalanx + n["USNS_Henry_Kaiser"] = n["MOLNIYA"] -- .50cal + n["HMS_Albion L14"] = n["MOLNIYA"] -- Phalanx + n["OSA_One"] = n["MOLNIYA"] -- AK-230 (NO_SAM) + n["OSA_Two"] = n["MOLNIYA"] -- AK-230 + n["IRIS_Alvand"] = n["MOLNIYA"] -- OtoBreda 40mm / 20mm + -- desdemicabina amphibs: CIWS / gun-only, optical-grade unjammable + n["L02"] = n["MOLNIYA"] -- 3x Phalanx + n["L52"] = n["MOLNIYA"] -- 2x Phalanx (NO_SAM) + n["L61"] = n["MOLNIYA"] -- .50cal only +end + + ----------------------------------------------------------------------- -- MANTIS System ----------------------------------------------------------------------- @@ -741,6 +985,8 @@ do -- mybluemantis:Start() -- function MANTIS:New(name,samprefix,ewrprefix,hq,Coalition,dynamic,awacs, EmOnOff, Padding, Zones) + -- Inject NAVYGROUP:SetMANTIS convenience API (safe if NAVYGROUP not loaded/already done) + MANTIS._InjectNavyGroupAPI() -- Inherit everything from BASE class. @@ -771,6 +1017,12 @@ do self.grouping = 5000 self.acceptrange = 80000 self.detectinterval = 30 + self.SEADNaval = false -- ships hard-kill inbound missiles; they do not go EMCON under SEAD (see SetNavalSEAD) + self.NavalSurfaceWakeup = true -- quiet ships go active when enemy surface combatants close (see SetNavalSurfaceWakeup) + self.NavalSurfaceWakeupRadius = nil -- meters; nil = each ship uses its own engagement radius + self.NavalPerUnit = true -- classify & switch every ship unit individually (see SetNavalPerUnit) + self.NavalAutonomy = true -- ship groups outside EWR radar coverage go alarm RED and search autonomously (see SetNavalAutonomy) + self.NavalAutonomyRadius = 150000 -- meters; a ship group within this distance of an alive EWR group counts as covered self.engagerange = 95 self.autorelocate = false self.autorelocateunits = { HQ = false, EWR = false} @@ -1086,6 +1338,21 @@ do end local function SwitchSAMOn(Name,Group) + if self.NavalPerUnit and Group and Group:IsShip() and self._navalSAMs then + -- Per-unit naval: a hit wakes this group's managed radars individually + if not self.SuppressedGroups[Name] then + for _,_unit in pairs(Group:GetUnits() or {}) do + local uname = _unit:GetName() + if self._navalSAMs[uname] and self.SamStateTracker[uname] == "GREEN" + and not (self._jammerEnabled and self._jammedSAMs and self._jammedSAMs[uname]) then + self.SamStateTracker[uname] = "RED" + end + end + self:_ApplyNavalAlarmState() -- apply group alarm state immediately on hit + self:__RedState(1,Group) + end + return + end local suppressed = self.SuppressedGroups[Name] or false local jammed = self._jammerEnabled and self._jammedSAMs and self._jammedSAMs[Name] or false if not suppressed and not jammed and self.SamStateTracker[Name] == "GREEN" then @@ -1790,6 +2057,7 @@ do local dist = 0 local include = true if grp:IsGround() then include = false end + if grp:IsShip() then include = false end -- MANTIS is anti-air: surface contacts are not engageable targets if grp:GetCoalition() == self.coalition then include = false end if coord and SamCoordinate and grp:IsHelicopter() then dist = coord:Get2DDistance(SamCoordinate) or 0 @@ -1853,7 +2121,29 @@ do if targetdistance <= rad then inrange = "true" end - local text = string.format("Checking SAM at %s | Tgtdist %.1fkm | Rad %.1fkm | Inrange %s", samstring, targetdistance/1000, rad/1000, inrange) + local text = string.format("Checking SAM at %s | Tgtdist %.1fkm | Rad %.1fkm | Inrange %s | Zonecheck %s", samstring, targetdistance/1000, rad/1000, inrange, tostring(zonecheck)) + -- Naval per-unit state summary + if self._navalSAMs and next(self._navalSAMs) ~= nil then + local ntot, nred, njam = 0, 0, 0 + local detail = "" + for uname,_ in pairs(self._navalSAMs) do + local u = UNIT:FindByName(uname) + if u and u:IsAlive() then + ntot = ntot + 1 + local st = self.SamStateTracker[uname] or "GREEN" + if st == "RED" then nred = nred + 1 end + local jm = "" + if self._jammedSAMs and self._jammedSAMs[uname] then njam = njam + 1 jm = " (JAMMED)" end + local pk = "" + local parent = self._navalUnitParent and self._navalUnitParent[uname] + if parent and self._navalAutonomous and self._navalAutonomous[parent] then pk = " [AUTONOMOUS]" end + if self.verbose then detail = detail..string.format("\n %s: %s%s%s",uname,st,jm,pk) end + end + end + if ntot > 0 then + text = text..string.format("\nNaval units: %d | RED %d | GREEN %d | jammed %d",ntot,nred,ntot-nred,njam)..detail + end + end local m = MESSAGE:New(text,10,"Check"):ToAllIf(self.debug) self:T(self.lid..text) end @@ -1988,37 +2278,66 @@ do local blind = 0 local group = GROUP:FindByName(grpname) -- Wrapper.Group#GROUP local units = group:GetUnits() - local SAMData = self.SamData local ARMCapacity + -- Ordered multi-table search: the mod-tagged table (if any) first, then all + -- others. This lets groups classify by radar TYPE NAME alone, so ground SAM + -- groups no longer need the designation (or even the mod tag) in their name. + local SearchTables if mod then - SAMData = self.SamDataHDS + SearchTables = { {self.SamDataHDS,"HDS"}, {self.SamData,nil}, {self.SamDataSMA,"SMA"}, {self.SamDataCH,"CHM"} } elseif sma then - SAMData = self.SamDataSMA + SearchTables = { {self.SamDataSMA,"SMA"}, {self.SamData,nil}, {self.SamDataHDS,"HDS"}, {self.SamDataCH,"CHM"} } elseif chm then - SAMData = self.SamDataCH + SearchTables = { {self.SamDataCH,"CHM"}, {self.SamData,nil}, {self.SamDataHDS,"HDS"}, {self.SamDataSMA,"SMA"} } + else + SearchTables = { {self.SamData,nil}, {self.SamDataHDS,"HDS"}, {self.SamDataSMA,"SMA"}, {self.SamDataCH,"CHM"} } end self:T("Looking to auto-match for "..grpname) for _,_unit in pairs(units) do local unit = _unit -- Wrapper.Unit#UNIT local typename = string.lower(unit:GetTypeName()) self:T(string.format("Matching typename: %s",typename)) - for idx,entry in pairs(SAMData) do - local _entry = entry -- #MANTIS.SamData - local _radar = string.lower(_entry.Radar) - self:T(string.format("Trying typename: %s",_radar)) - if string.find(typename,_radar,1,true) then - type = _entry.Type - radiusscale = self.radiusscale[type] - range = _entry.Range * 1000 * radiusscale -- max firing range used as switch-on - height = _entry.Height * 1000 -- max firing height - blind = _entry.Blindspot * 100 -- blind spot range - ARMCapacity = _entry.ARMCapacity - self:T(string.format("Match: %s - %s",_radar,type)) - found = true - break + -- Longest-match wins across ALL tables. This prevents a short Radar key in + -- one table (e.g. base "TorM2") from shadowing a more specific key in + -- another (e.g. CH "TorM2K"), and makes the typename path deterministic + -- (plain pairs() order no longer decides between overlapping keys). + -- Table order (mod-tagged table first) breaks exact-length ties. + local bestentry, bestidx, besttag, bestlen = nil, nil, nil, 0 + for _,searchentry in ipairs(SearchTables) do + local SAMData, modtag = searchentry[1], searchentry[2] + if SAMData then + for idx,entry in pairs(SAMData) do + local _radar = string.lower(entry.Radar) + if #_radar > bestlen and string.find(typename,_radar,1,true) then + bestentry, bestidx, besttag, bestlen = entry, idx, modtag, #_radar + end + end end end - if found then break end + if bestentry then + local _entry = bestentry -- #MANTIS.SamData + type = _entry.Type + radiusscale = self.radiusscale[type] + range = _entry.Range * 1000 * radiusscale -- max firing range used as switch-on + height = _entry.Height * 1000 -- max firing height + blind = _entry.Blindspot * 100 -- blind spot range + ARMCapacity = _entry.ARMCapacity + self:T(string.format("Match: %s - %s",string.lower(_entry.Radar),type)) + -- Stash the jammer curve by DESIGNATION (table key), so groups named + -- without a designation remain jammable. Reuses _ResolveJammerParams + -- (aliases, digit-boundary matching, mod bucketing) via a synthetic name. + self._samJammerParams = self._samJammerParams or {} + if self._samJammerParams[grpname] == nil then + local synth = bestidx + if besttag and not string.find(bestidx, besttag, 1, true) then + synth = bestidx .. " " .. besttag + end + local jp = self:_ResolveJammerParams(synth) + if jp then self._samJammerParams[grpname] = jp end + end + found = true + break + end end --- AAA or Point Defense if not found then @@ -2037,6 +2356,188 @@ do return range, height, type, blind, ARMCapacity end + --- [Internal] Function to get SAM firing data for naval (ship) groups. + -- Matches the unit type name exactly against #MANTIS.SamDataNaval and, as a side + -- effect, stashes the hull's jammer curve in self._samJammerParams[grpname] so the + -- SOJ jammer extension can suppress it by type name (not group name). + -- @param #MANTIS self + -- @param #string grpname Name of the ship group + -- @return #number range Max firing range (m) + -- @return #number height Max firing height (m) + -- @return #string type #MANTIS.SamType + -- @return #number blind Blind spot + -- @return #number ARMCapacity + function MANTIS:_GetNavalSAMData(grpname) + self:T(self.lid.."_GetNavalSAMData for "..tostring(grpname)) + self._navalSAMs = self._navalSAMs or {} + self._navalSAMs[grpname] = true + self._samJammerParams = self._samJammerParams or {} + local range = self.checkradius + local height = 3000 + local type = MANTIS.SamType.POINT + local blind = 0 + local ARMCapacity = 0 + local group = GROUP:FindByName(grpname) + if not group then + self._samJammerParams[grpname] = nil + return range, height, type, blind, ARMCapacity + end + local units = group:GetUnits() or {} + for _,_unit in pairs(units) do + local typename = string.lower(_unit:GetTypeName()) + for _,entry in pairs(self.SamDataNaval) do + if typename == string.lower(entry.Radar) then + type = entry.Type + local radiusscale = self.radiusscale[type] or 1 + range = entry.Range * 1000 * radiusscale + height = entry.Height * 1000 + blind = entry.Blindspot + ARMCapacity = entry.ARMCapacity or 0 + self._samJammerParams[grpname] = self.JammerSAMParamsNaval[entry.Radar] + self:T(string.format("Naval match: %s -> %s (range %sm)", typename, tostring(type), tostring(range))) + return range, height, type, blind, ARMCapacity + end + end + end + -- Unmatched ship: conservative point-defence default (mirrors AAA fallback), no jammer curve. + self._samJammerParams[grpname] = nil + self:E(self.lid..string.format("*****Could not match naval radar data for %s! Defaulting to POINT.",grpname)) + return 2000, 2000, MANTIS.SamType.POINT, 50, 0 + end + + --- [Internal] Classify a naval group PER UNIT. Each hull matching #MANTIS.SamDataNaval + -- becomes its own SAM-table record keyed by UNIT name, record[8] = parent group name. + -- @param #MANTIS self + -- @return #boolean handled True if at least one unit was classified. + function MANTIS:_BuildNavalUnitEntries(group, grpname, SAM_Tbl, SAM_Tbl_lg, SAM_Tbl_md, SAM_Tbl_sh, SAM_Tbl_pt, SEAD_Grps) + self:T(self.lid.."_BuildNavalUnitEntries for "..tostring(grpname)) + self._navalSAMs = self._navalSAMs or {} + self._samJammerParams = self._samJammerParams or {} + local entries = 0 + local seadadded = false + local units = group:GetUnits() or {} + for _,_unit in pairs(units) do + if _unit and _unit:IsAlive() then + local typename = string.lower(_unit:GetTypeName()) + for _,entry in pairs(self.SamDataNaval) do + if typename == string.lower(entry.Radar) then + local unitname = _unit:GetName() + local type = entry.Type + local radiusscale = self.radiusscale[type] or 1 + local range = entry.Range * 1000 * radiusscale + local height = entry.Height * 1000 + local blind = entry.Blindspot + local coord = _unit:GetCoordinate() + local record = {unitname, coord, range, height, blind, type, nil, grpname} + table.insert( SAM_Tbl, record ) + if type == MANTIS.SamType.LONG then table.insert( SAM_Tbl_lg, record ) + elseif type == MANTIS.SamType.MEDIUM then table.insert( SAM_Tbl_md, record ) + elseif type == MANTIS.SamType.SHORT then table.insert( SAM_Tbl_sh, record ) + elseif type == MANTIS.SamType.POINT then table.insert( SAM_Tbl_pt, record ) end + self._navalSAMs[unitname] = true + self._navalUnitParent = self._navalUnitParent or {} + self._navalUnitParent[unitname] = grpname + self._samJammerParams[unitname] = self.JammerSAMParamsNaval[entry.Radar] + if self.SamStateTracker[unitname] == nil then self.SamStateTracker[unitname] = "GREEN" end + if self.SEADNaval and not seadadded then + table.insert( SEAD_Grps, grpname ) -- SEAD keys on GROUP names; add once + seadadded = true + end + entries = entries + 1 + self:T(string.format("Naval per-unit match: %s (%s) -> %s",unitname,typename,tostring(type))) + break + end + end + end + end + return entries > 0 + end + + --- [Internal] Group ALARM-STATE arbiter for per-unit naval control. + -- DCS reliably honors only GROUP-level alarm state on ships, so per-unit + -- tracking decides intent and this function applies ONE alarm state per ship + -- group as the final word of each cycle: + -- RED if any managed unit is RED and not jammed (threat / hit / surface + -- wake), or the group is outside EWR radar coverage (autonomous + -- search, see SetNavalAutonomy) with at least one unjammed unit; + -- GREEN otherwise -- including full jamming suppression, which overrides + -- autonomy (a fully jammed group cannot search). + -- @param #MANTIS self + -- @return #MANTIS self + function MANTIS:_ApplyNavalAlarmState() + if not (self.NavalPerUnit and self._navalUnitParent) then return self end + local wants, seen, unjammed = {}, {}, {} + for uname, grpname in pairs(self._navalUnitParent) do + local u = UNIT:FindByName(uname) + if u and u:IsAlive() then + seen[grpname] = true + local jammed = self._jammerEnabled and self._jammedSAMs and self._jammedSAMs[uname] + if not jammed then unjammed[grpname] = true end + if self.SamStateTracker[uname] == "RED" and not jammed then + wants[grpname] = true + end + end + end + -- coverage sources: every alive EWR group (incl. EWR-role ships and AWACS ships in the EWR set) + local sources = {} + if self.NavalAutonomy then + -- Managed naval SAM groups cannot be their own coverage: when GREEN their + -- radars are dark, so a "BOTH"-role group must not self-license through + -- its EWR-set membership. Only UNMANAGED emitters count as sources: + -- land EWR, AWACS, and EWR-only ships (never state-switched, always + -- radiating). + local managed = {} + for _, gname in pairs(self._navalUnitParent) do managed[gname] = true end + self.EWR_Group:ForEachGroupAlive( + function(grp) + if not managed[grp:GetName()] then + local c = grp:GetCoordinate() + if c then table.insert(sources, c) end + end + end) + end + self._navalAutonomous = self._navalAutonomous or {} + self._navalGroupState = self._navalGroupState or {} + for grpname,_ in pairs(seen) do + local grp = GROUP:FindByName(grpname) + if grp and grp:IsAlive() then + local red = wants[grpname] or false + local autonomous = false + if self.NavalAutonomy and unjammed[grpname] and not red then + local covered = false + local gc = grp:GetCoordinate() + if gc then + for _,c in ipairs(sources) do + if c:Get2DDistance(gc) <= self.NavalAutonomyRadius then + covered = true + break + end + end + end + self:T(self.lid..string.format("%s EWR coverage: %s (%d unmanaged source(s), radius %dkm)",grpname,tostring(covered),#sources,self.NavalAutonomyRadius/1000)) + if not covered then + red = true + autonomous = true + end + end + self._navalAutonomous[grpname] = autonomous or nil + local newstate = red and "RED" or "GREEN" + if self._navalGroupState[grpname] ~= newstate then + self._navalGroupState[grpname] = newstate + if red then + grp:OptionAlarmStateRed() + self:T(self.lid..grpname.." naval group -> alarm RED"..(autonomous and " (autonomous: no EWR coverage)" or "")) + else + grp:OptionAlarmStateGreen() + self:T(self.lid..grpname.." naval group -> alarm GREEN") + end + end + end + end + return self + end + + --- [Internal] Function to get SAM firing data -- @param #MANTIS self -- @param #string grpname Name of the group @@ -2046,6 +2547,14 @@ do -- @return #number blind "blind" spot function MANTIS:_GetSAMRange(grpname) self:T(self.lid.."_GetSAMRange for "..tostring(grpname)) + -- Naval hulls: match by exact unit type name against SamDataNaval and stash the + -- jammer curve; bypass the ground name-matching below. + do + local grp = GROUP:FindByName(grpname) + if grp and grp:IsShip() then + return self:_GetNavalSAMData(grpname) + end + end local range = self.checkradius local height = 3000 local type = MANTIS.SamType.MEDIUM @@ -2122,10 +2631,16 @@ do local engagerange = self.engagerange -- firing range in % of max --cycle through groups and set alarm state etc for _i,_group in pairs (SAM_Grps) do - if _group:IsGround() and _group:IsAlive() then + if (_group:IsGround() or _group:IsShip()) and _group:IsAlive() then local group = _group -- Wrapper.Group#GROUP -- DONE: add emissions on/off - if self.UseEmOnOff then + if group:IsShip() and self.NavalPerUnit then + -- Per-unit naval control: ships are switched by GROUP ALARM STATE only, + -- the one mechanism DCS reliably honors on naval groups. Baseline is + -- GREEN; the alarm arbiter (_ApplyNavalAlarmState) raises the group RED + -- on threat, hit, surface contact, or loss of EWR coverage. + group:OptionAlarmStateGreen() + elseif self.UseEmOnOff then group:OptionAlarmStateRed() group:EnableEmission(false) --group:SetAIOff() @@ -2135,30 +2650,47 @@ do group:OptionEngageRange(engagerange) --default engagement will be 95% of firing range local grpname = group:GetName() local grpcoord = group:GetCoordinate() + if group:IsShip() and self.NavalPerUnit + and self:_BuildNavalUnitEntries(group, grpname, SAM_Tbl, SAM_Tbl_lg, SAM_Tbl_md, SAM_Tbl_sh, SAM_Tbl_pt, SEAD_Grps) then + self:T(grpname.." handled as per-unit naval group") + else local grprange,grpheight,type,blind,ARMCapacity = self:_GetSAMRange(grpname) if ARMCapacity and ARMCapacity>0 then _group:SetProperty("ARMCapacity",ARMCapacity) end table.insert( SAM_Tbl, {grpname, grpcoord, grprange, grpheight, blind, type, ARMCapacity}) --table.insert( SEAD_Grps, grpname ) if type == MANTIS.SamType.LONG then table.insert( SAM_Tbl_lg, {grpname, grpcoord, grprange, grpheight, blind, type}) - table.insert( SEAD_Grps, grpname ) + if (not group:IsShip()) or self.SEADNaval then + table.insert( SEAD_Grps, grpname ) + end self:T("SAM "..grpname.." is type LONG") elseif type == MANTIS.SamType.MEDIUM then table.insert( SAM_Tbl_md, {grpname, grpcoord, grprange, grpheight, blind, type}) - table.insert( SEAD_Grps, grpname ) + if (not group:IsShip()) or self.SEADNaval then + table.insert( SEAD_Grps, grpname ) + end self:T("SAM "..grpname.." is type MEDIUM") elseif type == MANTIS.SamType.SHORT then table.insert( SAM_Tbl_sh, {grpname, grpcoord, grprange, grpheight, blind, type}) - table.insert( SEAD_Grps, grpname ) + if (not group:IsShip()) or self.SEADNaval then + table.insert( SEAD_Grps, grpname ) + end self:T("SAM "..grpname.." is type SHORT") elseif type == MANTIS.SamType.POINT then table.insert( SAM_Tbl_pt, {grpname, grpcoord, grprange, grpheight, blind, type}) self:T("SAM "..grpname.." is type POINT") - self.ShoradGroupSet:Add(grpname,group) - if not self.autoshorad then - table.insert( SEAD_Grps, grpname ) - end + if group:IsShip() then + if self.SEADNaval then + table.insert( SEAD_Grps, grpname ) -- naval point-defence -> SEAD only when opted in; never SHORAD scoot + end + else + self.ShoradGroupSet:Add(grpname,group) + if not self.autoshorad then + table.insert( SEAD_Grps, grpname ) + end + end end + end -- else: per-unit naval group handled above self.SamStateTracker[grpname] = "GREEN" end end @@ -2198,16 +2730,20 @@ do for _i,_group in pairs (SAM_Grps) do local group = _group -- Wrapper.Group#GROUP group:OptionEngageRange(engagerange) --engagement will be 95% of firing range - if group:IsGround() and group:IsAlive() then + if (group:IsGround() or group:IsShip()) and group:IsAlive() then local grpname = group:GetName() local grpcoord = group:GetCoordinate() + if group:IsShip() and self.NavalPerUnit + and self:_BuildNavalUnitEntries(group, grpname, SAM_Tbl, SAM_Tbl_lg, SAM_Tbl_md, SAM_Tbl_sh, SAM_Tbl_pt, SEAD_Grps) then + self:T(grpname.." handled as per-unit naval group") + else local grprange, grpheight,type,blind, ARMCapacity = self:_GetSAMRange(grpname) -- TODO the below might stop working at some point after some hours, needs testing --local radaralive = group:IsSAM() if ARMCapacity and ARMCapacity>0 then _group:SetProperty("ARMCapacity",ARMCapacity) end local radaralive = true table.insert( SAM_Tbl, {grpname, grpcoord, grprange, grpheight, blind, type, ARMCapacity}) -- make the table lighter, as I don't really use the zone here - if type ~= MANTIS.SamType.POINT then + if type ~= MANTIS.SamType.POINT and ((not group:IsShip()) or self.SEADNaval) then table.insert( SEAD_Grps, grpname ) end if type == MANTIS.SamType.LONG and radaralive then @@ -2222,11 +2758,18 @@ do elseif type == MANTIS.SamType.POINT or (not radaralive) then table.insert( SAM_Tbl_pt, {grpname, grpcoord, grprange, grpheight, blind, type}) self:T({grpname,grprange, grpheight}) - self.ShoradGroupSet:Add(grpname,group) - if self.autoshorad then - self.Shorad.Groupset = self.ShoradGroupSet + if group:IsShip() then + if self.SEADNaval then + table.insert( SEAD_Grps, grpname ) -- naval point-defence -> SEAD only when opted in; never SHORAD scoot + end + else + self.ShoradGroupSet:Add(grpname,group) + if self.autoshorad then + self.Shorad.Groupset = self.ShoradGroupSet + end end end + end -- else: per-unit naval group handled above end end self.SAM_Table = SAM_Tbl @@ -2414,6 +2957,7 @@ function MANTIS:SeadAllowSuppression(targetGroup, targetName, attackerGroup, wea for _,_data in pairs (samset) do local samcoordinate = _data[2] local name = _data[1] + local navalparent = _data[8] -- per-unit naval entry: name is a UNIT name local radius = _data[3] local height = _data[4] local blind = _data[5] * 1.25 + 1 @@ -2421,19 +2965,44 @@ function MANTIS:SeadAllowSuppression(targetGroup, targetName, attackerGroup, wea if not shortsam then shortsam = (_data[6] == MANTIS.SamType.POINT) and true or false end - local samgroup = GROUP:FindByName(name) + local samunit = nil + local samgroup = nil + if navalparent then + samunit = UNIT:FindByName(name) + samgroup = (samunit and samunit:GetGroup()) or GROUP:FindByName(navalparent) + else + samgroup = GROUP:FindByName(name) + end + local samalive = false + if navalparent then samalive = (samunit ~= nil) and samunit:IsAlive() or false + elseif samgroup then samalive = samgroup:IsAlive() or false end local IsInZone, Distance = self:_CheckObjectInZone(detset, samcoordinate, radius, height, dlink) - local suppressed = self.SuppressedGroups[name] or false + -- Naval surface wake-up: quiet ships activate when enemy surface combatants + -- close within the trigger radius. Jamming has precedence and holds them down. + if (not IsInZone) and self.NavalSurfaceWakeup and self._navalSAMs and self._navalSAMs[name] + and not (self._jammerEnabled and self._jammedSAMs and self._jammedSAMs[name]) then + local wakeradius = self.NavalSurfaceWakeupRadius or radius + if self:_EnemySurfaceInRange(samcoordinate, wakeradius) then + IsInZone = true + Distance = wakeradius -- keep distance-based proximity behaviors (smoke, SHORAD link) inert + end + end + local suppressed = self.SuppressedGroups[name] or (navalparent and self.SuppressedGroups[navalparent]) or false local activeshorad = false if self.Shorad and self.Shorad.ActiveGroups and self.Shorad.ActiveGroups[name] then activeshorad = true end if samgroup:GetProperty("SHORAD_ACTIVE") == true and activeshorad == false then activeshorad = true end if IsInZone and (not suppressed) and (not activeshorad) then --check any target in zone and not currently managed by SEAD - if samgroup:IsAlive() then + if samalive then -- switch on SAM local switch = false - if self.UseEmOnOff and switchedon < limit then + if navalparent and switchedon < limit then + -- per-unit naval: mark THIS unit RED; the group alarm arbiter + -- (_ApplyNavalAlarmState) applies the group switch at cycle end + switchedon = switchedon + 1 + switch = true + elseif self.UseEmOnOff and switchedon < limit then -- DONE: add emissions on/off samgroup:EnableEmission(true) switchedon = switchedon + 1 @@ -2448,13 +3017,13 @@ function MANTIS:SeadAllowSuppression(targetGroup, targetName, attackerGroup, wea self:__RedState(1,samgroup) end -- DONE Restrict on Distance - if shortsam == true and self.SmokeDecoy == true and Distance < self.DetectAccousticRadius*1.5 then + if shortsam == true and (not navalparent) and self.SmokeDecoy == true and Distance < self.DetectAccousticRadius*1.5 then self:T("Smoking") self:_SmokeUnits(samgroup) end -- link in to SHORAD if available -- DONE: Test integration fully - if self.ShoradLink and (Distance < self.ShoradActDistance or Distance < blind ) then -- don't give SHORAD position away too early + if self.ShoradLink and (not navalparent) and (Distance < self.ShoradActDistance or Distance < blind ) then -- don't give SHORAD position away too early; ships never SHORAD-link local Shorad = self.Shorad --Functional.Shorad#SHORAD local radius = self.checkradius local ontime = self.ShoradTime @@ -2469,9 +3038,11 @@ function MANTIS:SeadAllowSuppression(targetGroup, targetName, attackerGroup, wea end end --end alive else - if samgroup:IsAlive() and not suppressed and not activeshorad then + if samalive and not suppressed and not activeshorad then -- switch off SAM - if self.UseEmOnOff then + if navalparent then + -- per-unit naval: tracker-only; group alarm state applied by the arbiter + elseif self.UseEmOnOff then samgroup:EnableEmission(false) else samgroup:OptionAlarmStateGreen() @@ -2570,6 +3141,9 @@ function MANTIS:SeadAllowSuppression(targetGroup, targetName, attackerGroup, wea return statusreport end + -- apply naval group alarm states as the final word of this cycle + self:_ApplyNavalAlarmState() + if self.debug or self.verbose then local statusreport = GetReport() MESSAGE:New(statusreport:Text(),10):ToAll():ToLog() @@ -2605,9 +3179,22 @@ function MANTIS:SeadAllowSuppression(targetGroup, targetName, attackerGroup, wea local samset = self:_GetSAMTable() -- table of i.1=names, i.2=coordinates for _,_data in pairs (samset) do local name = _data[1] - local samgroup = GROUP:FindByName(name) - if samgroup:IsAlive() then - if self.UseEmOnOff then + local navalparent = _data[8] -- per-unit naval entry: name is a UNIT name + local samunit = nil + local samgroup = nil + if navalparent then + samunit = UNIT:FindByName(name) + samgroup = (samunit and samunit:GetGroup()) or GROUP:FindByName(navalparent) + else + samgroup = GROUP:FindByName(name) + end + local samalive = false + if navalparent then samalive = (samunit ~= nil) and samunit:IsAlive() or false + elseif samgroup then samalive = samgroup:IsAlive() or false end + if samalive then + if navalparent then + self.SamStateTracker[name] = "RED" -- arbiter raises the group + elseif self.UseEmOnOff then -- DONE: add emissions on/off --samgroup:SetAIOn() samgroup:EnableEmission(true) @@ -3236,7 +3823,7 @@ function MANTIS:SeadAllowSuppression(targetGroup, targetName, attackerGroup, wea end end for samName, samCoord in pairs(allSAMs) do - local params = self:_ResolveJammerParams(samName) + local params = (self._samJammerParams and self._samJammerParams[samName]) or self:_ResolveJammerParams(samName) if params then local survival = 1.0 for _, jammer in ipairs(self._jammerSnapshot) do @@ -3508,10 +4095,23 @@ function MANTIS:SeadAllowSuppression(targetGroup, targetName, attackerGroup, wea if self._jammerEnabled and self._jammedSAMs then for _, _data in pairs(samset) do local name = _data[1] + local navalparent = _data[8] -- per-unit naval entry: name is a UNIT name if self._jammedSAMs[name] and self.SamStateTracker[name] == "RED" then - local samgroup = GROUP:FindByName(name) - if samgroup and samgroup:IsAlive() then - if self.UseEmOnOff then + local samunit = nil + local samgroup = nil + if navalparent then + samunit = UNIT:FindByName(name) + samgroup = (samunit and samunit:GetGroup()) or GROUP:FindByName(navalparent) + else + samgroup = GROUP:FindByName(name) + end + local samalive = false + if navalparent then samalive = (samunit ~= nil) and samunit:IsAlive() or false + elseif samgroup then samalive = samgroup:IsAlive() or false end + if samalive then + if navalparent then + -- per-unit naval: tracker set GREEN below; arbiter darkens the group if no unit remains RED + elseif self.UseEmOnOff then samgroup:EnableEmission(false) else samgroup:OptionAlarmStateGreen() @@ -3545,6 +4145,214 @@ function MANTIS:SeadAllowSuppression(targetGroup, targetName, attackerGroup, wea end end + +-- =================================================================================================== +-- MANTIS Naval Extension: NAVYGROUP integration +-- Lets Ops.NavyGroup#NAVYGROUP objects (cruise control, missions, carrier ops) serve as +-- MANTIS-managed SAM shooters and/or EWR pickets: +-- mymantis:AddNavyGroup(navygroup, "SAM") -- managed shooter (or "EWR" = radiating picket) +-- navygroup:SetMANTIS(mymantis, "SAM") -- convenience, same effect +-- The ship group is added to the running set(s) regardless of name prefix; classification +-- and jamming resolve by hull type name as usual. MANTIS takes over emission/alarm +-- switching: the NAVYGROUP's default Emission/Alarmstate are neutralized so its +-- spawn-time option reset does not fight MANTIS. +-- =================================================================================================== +do + -- Guard MOOSE's ground relocation against ships. SEAD suppression (and any other + -- caller) invokes RelocateGroundRandomInRadius on suppressed SAM groups without a + -- category check; on a ship this clobbers the sailing route with a ground-formation + -- hop (and with onland=true can target a LAND point near a coast). Radar-off + -- evasion still applies to ships; only the ground move becomes a no-op. + if CONTROLLABLE and CONTROLLABLE.RelocateGroundRandomInRadius and not CONTROLLABLE._MANTISNavalRelocateGuard then + CONTROLLABLE._MANTISNavalRelocateGuard = true + local _origRelocate = CONTROLLABLE.RelocateGroundRandomInRadius + function CONTROLLABLE:RelocateGroundRandomInRadius( speed, radius, onroad, shortcut, formation, onland ) + if self.IsShip and self:IsShip() then + return self + end + return _origRelocate( self, speed, radius, onroad, shortcut, formation, onland ) + end + end + + --- [Internal] Check if any enemy surface combatant is within radius of a coordinate. + -- Builds a live enemy-ship SET_GROUP lazily on first use. + -- @param #MANTIS self + -- @param Core.Point#COORDINATE coord Ship position. + -- @param #number radius Trigger radius in meters. + -- @return #boolean found + function MANTIS:_EnemySurfaceInRange(coord, radius) + if self.NavalThreatSet == nil then + local own = string.lower(self.Coalition or "") + local enemy = (own == "red" and "blue") or (own == "blue" and "red") or nil + if not enemy then + self:E(self.lid.."NavalSurfaceWakeup: cannot derive enemy coalition from "..tostring(self.Coalition).." - disabling.") + self.NavalSurfaceWakeup = false + self.NavalThreatSet = false + return false + end + self.NavalThreatSet = SET_GROUP:New():FilterCoalitions(enemy):FilterCategoryShip():FilterStart() + end + if self.NavalThreatSet == false or not coord then return false end + local found = false + self.NavalThreatSet:ForEachGroupAlive( + function(grp) + if not found then + local gc = grp:GetCoordinate() + if gc and gc:Get2DDistance(coord) <= radius then found = true end + end + end) + return found + end + + --- Configure naval AUTONOMY outside EWR coverage. Default: ENABLED, 150km. + -- Ships mirror the ground-SAM umbrella concept: a ship group within CoverageKM + -- of any alive EWR group (including EWR-role ships or AWACS ships in the EWR + -- set) is managed normally -- alarm GREEN until MANTIS detects a threat inside + -- one of its units' envelopes. A group OUTSIDE coverage goes alarm RED and + -- searches/engages autonomously on its own radars until coverage is restored. + -- Full jamming suppression overrides autonomy and forces the group GREEN. + -- @param #MANTIS self + -- @param #boolean onoff Enable (true) or disable (false). + -- @param #number coveragekm (Optional) Coverage radius in KILOMETERS around each alive EWR (default 150). + -- @return #MANTIS self + function MANTIS:SetNavalAutonomy(onoff, coveragekm) + self.NavalAutonomy = onoff and true or false + if coveragekm then self.NavalAutonomyRadius = coveragekm * 1000 end + return self + end + + --- Enable/disable PER-UNIT classification and switching for naval groups. Default: ENABLED. + -- Mixed-hull ship groups (e.g. a carrier escorted by destroyers) get one SAM entry PER + -- SHIP -- each hull with its own type, range, state and jamming curve, switched via + -- UNIT:EnableEmission while the group stays alarm RED (weapons hot). Disable to revert + -- to legacy group-level naval handling (first matched hull defines the whole group). + -- @param #MANTIS self + -- @param #boolean onoff Enable (true) or disable (false). + -- @return #MANTIS self + function MANTIS:SetNavalPerUnit(onoff) + self.NavalPerUnit = onoff and true or false + return self + end + + --- Enable/disable the naval surface wake-up and optionally set a fixed trigger radius. + -- Default: ENABLED, radius = each ship's own engagement radius from SamDataNaval. + -- When enabled, a MANTIS-quiet ship goes active (RED / emissions on) whenever an + -- enemy surface combatant closes within the trigger radius, so anti-ship + -- engagements are not blocked by EMCON. Jamming still wins: a jammed ship stays + -- suppressed. Ships only -- ground SAM behavior is unchanged. + -- @param #MANTIS self + -- @param #boolean onoff Enable (true) or disable (false). + -- @param #number radiuskm (Optional) Fixed trigger radius in KILOMETERS for all ships. + -- @return #MANTIS self + function MANTIS:SetNavalSurfaceWakeup(onoff, radiuskm) + self.NavalSurfaceWakeup = onoff and true or false + self.NavalSurfaceWakeupRadius = radiuskm and radiuskm * 1000 or nil + return self + end + + --- Include or exclude naval SAMs from SEAD emission-shutdown evasion. Default: EXCLUDED. + -- Surface combatants engage inbound ARMs/ASMs with hard-kill defenses (SAMs, CIWS); + -- going emissions-dark would disable exactly those defenses. Ground SAM behavior is + -- unchanged. Takes effect at the next SAM table refresh (every 3rd detection cycle). + -- @param #MANTIS self + -- @param #boolean onoff If true, ships use SEAD EMCON evasion like ground SAMs. + -- @return #MANTIS self + function MANTIS:SetNavalSEAD(onoff) + self.SEADNaval = onoff and true or false + -- SEAD:UpdateSet() only ever ADDS names. When switching OFF mid-mission we + -- must actively remove ship groups already registered with the SEAD + -- instance, or they would keep evading until mission end. + if (not self.SEADNaval) and self.mysead and self.mysead.SEADGroupPrefixes then + for name,_ in pairs(self.mysead.SEADGroupPrefixes) do + local grp = GROUP:FindByName(name) + if grp and grp:IsShip() then + self.mysead.SEADGroupPrefixes[name] = nil + end + end + end + return self + end + + --- Add a NAVYGROUP to this MANTIS as SAM shooter and/or EWR picket. + -- IMPORTANT: MANTIS does not change ROE. NAVYGROUP defaults to Return Fire -- + -- set NavyGroup:SwitchROE(ENUMS.ROE.WeaponFree) in your mission if the ship + -- should engage detected targets rather than only return fire. + -- @param #MANTIS self + -- @param Ops.NavyGroup#NAVYGROUP NavyGroup The navy group to add. + -- @param #string Role "SAM" (default) = managed shooter, or "EWR" = unmanaged, always-radiating sensor/picket (coverage source). "BOTH" is deprecated and treated as SAM. + -- @return #MANTIS self + function MANTIS:AddNavyGroup(NavyGroup, Role) + local role = string.upper(Role or "SAM") + if role == "BOTH" then + -- Deprecated: a MANAGED shooter cannot double as a sensor -- its radars + -- are dark whenever MANTIS holds it GREEN, so EWR membership senses + -- nothing and only pollutes EWR-alive counting and the advanced-mode + -- network ratio. Use a SEPARATE ship group with Role="EWR" as a + -- radiating picket instead. + self:E(self.lid.."AddNavyGroup: Role BOTH is deprecated -- treating as SAM. Use a separate EWR-role group as a radiating picket.") + role = "SAM" + end + if not (NavyGroup and NavyGroup.ClassName and string.find(NavyGroup.ClassName,"NAVYGROUP",1,true)) then + self:E(self.lid.."AddNavyGroup: object passed is not a NAVYGROUP!") + return self + end + local grp = NavyGroup:GetGroup() + if not (grp and grp:IsAlive()) then + self:E(self.lid.."AddNavyGroup: NAVYGROUP has no alive GROUP (late-activated? Call after activation).") + return self + end + -- Hand emission/alarm control to MANTIS. NAVYGROUP re-applies its default + -- Alarmstate/Emission at (re)spawn, which would fight MANTIS switching: + -- set defaults to Emission ON / Alarmstate AUTO so the reset is neutral. + if NavyGroup.SetDefaultEmission then NavyGroup:SetDefaultEmission(true) end + if NavyGroup.SetDefaultAlarmstate then NavyGroup:SetDefaultAlarmstate(0) end -- 0 = Auto + -- NOTE: MANTIS deliberately does NOT touch ROE -- that is the mission + -- designer's call. Be aware OPSGROUP's default ROE is Return Fire: a + -- MANTIS-managed NAVYGROUP will radiate when RED but hold its weapons + -- unless the mission sets e.g. NavyGroup:SwitchROE(ENUMS.ROE.WeaponFree). + self._navyGroups = self._navyGroups or {} + self._navyGroups[grp:GetName()] = NavyGroup + if role == "SAM" then + self.SAM_Group:AddGroup(grp) + -- If MANTIS is already up, classify the new ship immediately; otherwise + -- SetSAMStartState will pick it up at Start. + if self.SAM_Table then + self:_RefreshSAMTable() + end + elseif role == "EWR" then + self.EWR_Group:AddGroup(grp) + if self.Adv_EWR_Group then + self.Adv_EWR_Group:AddGroup(grp) + end + else + self:E(self.lid.."AddNavyGroup: unknown Role "..tostring(Role).." - use SAM or EWR.") + end + self:I(self.lid..string.format("Added NAVYGROUP %s as %s",grp:GetName(),role)) + return self + end + + --- [Internal, static] Inject NAVYGROUP:SetMANTIS. Idempotent; safe if NAVYGROUP absent. + function MANTIS._InjectNavyGroupAPI() + if NAVYGROUP and not NAVYGROUP.SetMANTIS then + --- Register this NAVYGROUP with a MANTIS instance as SAM shooter and/or EWR picket. + -- @param Ops.NavyGroup#NAVYGROUP self + -- @param Functional.Mantis#MANTIS Mantis The MANTIS instance. + -- @param #string Role "SAM" (default) = managed shooter, or "EWR" = unmanaged, always-radiating sensor/picket (coverage source). "BOTH" is deprecated and treated as SAM. + -- @return Ops.NavyGroup#NAVYGROUP self + function NAVYGROUP:SetMANTIS(Mantis, Role) + if Mantis and Mantis.AddNavyGroup then + Mantis:AddNavyGroup(self, Role) + else + self:E(self.lid.."SetMANTIS: no valid MANTIS instance passed!") + end + return self + end + end + end + -- Try immediately in case NAVYGROUP is already loaded (also runs from MANTIS:New). + MANTIS._InjectNavyGroupAPI() +end + ----------------------------------------------------------------------- -- MANTIS end ------------------------------------------------------------------------ +----------------------------------------------------------------------- \ No newline at end of file From 55e048b322aee6e1b9fc7a14797405ce20ce613d Mon Sep 17 00:00:00 2001 From: leka1986 Date: Wed, 15 Jul 2026 18:39:46 +0200 Subject: [PATCH 10/18] Add group option change event support --- Moose Development/Moose/Core/Base.lua | 22 ++++++++++++++++++ Moose Development/Moose/Core/Event.lua | 32 ++++++++++++++++++++------ Moose Development/Moose/DCS.lua | 3 ++- 3 files changed, 49 insertions(+), 8 deletions(-) diff --git a/Moose Development/Moose/Core/Base.lua b/Moose Development/Moose/Core/Base.lua index 95d8858b7..6ec0a7d05 100644 --- a/Moose Development/Moose/Core/Base.lua +++ b/Moose Development/Moose/Core/Base.lua @@ -735,6 +735,12 @@ do -- Event Handling -- @function [parent=#BASE] OnEventPlayerEnterAircraft -- @param #BASE self -- @param Core.Event#EVENTDATA EventData The EventData structure. + + --- Occurs when an option is changed for an AI group. + -- The event identifies the initiating group, but does not provide the option ID or value. + -- @function [parent=#BASE] OnEventGroupChangeOption + -- @param #BASE self + -- @param Core.Event#EVENTDATA EventData The EventData structure. --- Occurs when a player creates a dynamic cargo object from the F8 ground crew menu. -- *** NOTE *** this is a workarounf for DCS not creating these events as of Aug 2024. @@ -880,6 +886,22 @@ end world.onEvent(Event) end + + --- Creation of a `S_EVENT_GROUP_CHANGE_OPTION` event. + -- @param #BASE self + -- @param DCS#Time EventTime The time stamp of the event. + -- @param DCS#Group Initiator The initiating group of the event. + function BASE:CreateEventGroupChangeOption( EventTime, Initiator ) + self:F( { EventTime, Initiator } ) + + local Event = { + id = EVENTS.GroupChangeOption, + time = EventTime, + initiator = Initiator, + } + + world.onEvent( Event ) + end --- Creation of a S_EVENT_NEW_DYNAMIC_CARGO event. -- @param #BASE self diff --git a/Moose Development/Moose/Core/Event.lua b/Moose Development/Moose/Core/Event.lua index 267a46bdd..71270e026 100644 --- a/Moose Development/Moose/Core/Event.lua +++ b/Moose Development/Moose/Core/Event.lua @@ -278,6 +278,7 @@ EVENTS = { SimulationUnfreeze = world.event.S_EVENT_SIMULATION_UNFREEZE or -1, HumanAircraftRepairStart = world.event.S_EVENT_HUMAN_AIRCRAFT_REPAIR_START or -1, HumanAircraftRepairFinish = world.event.S_EVENT_HUMAN_AIRCRAFT_REPAIR_FINISH or -1, + GroupChangeOption = world.event.S_EVENT_GROUP_CHANGE_OPTION or -1, -- dynamic cargo NewDynamicCargo = world.event.S_EVENT_NEW_DYNAMIC_CARGO or -1, DynamicCargoLoaded = world.event.S_EVENT_DYNAMIC_CARGO_LOADED or -1, @@ -291,23 +292,25 @@ EVENTS = { -- -- * A (Object.Category.)UNIT : A UNIT object type is involved in the Event. -- * A (Object.Category.)STATIC : A STATIC object type is involved in the Event. +-- * A GROUP : A DCS Group object is involved in the Event. -- -- @type EVENTDATA -- @field #number id The identifier of the event. -- --- @field DCS#Unit initiator (UNIT/STATIC/SCENERY) The initiating @{DCS#Unit} or @{DCS#StaticObject}. +-- @field DCS#Unit initiator (UNIT/STATIC/SCENERY/GROUP) The initiating @{DCS#Unit} or @{DCS#StaticObject}. For `S_EVENT_GROUP_CHANGE_OPTION`, this is a DCS Group. -- @field DCS#Object.Category IniObjectCategory (UNIT/STATIC/SCENERY) The initiator object category ( Object.Category.UNIT or Object.Category.STATIC ). -- @field DCS#Unit IniDCSUnit (UNIT/STATIC) The initiating @{DCS#Unit} or @{DCS#StaticObject}. -- @field #string IniDCSUnitName (UNIT/STATIC) The initiating Unit name. -- @field Wrapper.Unit#UNIT IniUnit (UNIT/STATIC) The initiating MOOSE wrapper @{Wrapper.Unit#UNIT} of the initiator Unit object. -- @field #string IniUnitName (UNIT/STATIC) The initiating UNIT name (same as IniDCSUnitName). --- @field DCS#Group IniDCSGroup (UNIT) The initiating {DCSGroup#Group}. --- @field #string IniDCSGroupName (UNIT) The initiating Group name. --- @field Wrapper.Group#GROUP IniGroup (UNIT) The initiating MOOSE wrapper @{Wrapper.Group#GROUP} of the initiator Group object. --- @field #string IniGroupName UNIT) The initiating GROUP name (same as IniDCSGroupName). +-- @field DCS#Group IniDCSGroup (UNIT/GROUP) The initiating @{DCS#Group}. +-- @field #string IniDCSGroupName (UNIT/GROUP) The initiating Group name. +-- @field Wrapper.Group#GROUP IniGroup (UNIT/GROUP) The initiating MOOSE wrapper @{Wrapper.Group#GROUP} of the initiator Group object. +-- @field #string IniGroupName (UNIT/GROUP) The initiating GROUP name (same as IniDCSGroupName). +-- @field DCS#Group.Category IniGroupCategory (GROUP) The category of the initiating group. -- @field #string IniPlayerName (UNIT) The name of the initiating player in case the Unit is a client or player slot. -- @field #string IniPlayerUCID (UNIT) The UCID of the initiating player in case the Unit is a client or player slot and on a multi-player server. --- @field DCS#coalition.side IniCoalition (UNIT) The coalition of the initiator. +-- @field DCS#coalition.side IniCoalition (UNIT/GROUP) The coalition of the initiator. -- @field DCS#Unit.Category IniCategory (UNIT) The category of the initiator. -- @field #string IniTypeName (UNIT) The type name of the initiator. -- @@ -734,6 +737,12 @@ local _EVENTMETA = { Side = "I", Event = "OnEventHumanAircraftRepairFinish", Text = "S_EVENT_HUMAN_AIRCRAFT_REPAIR_FINISH" + }, + [EVENTS.GroupChangeOption] = { + Order = 1, + Side = "I", + Event = "OnEventGroupChangeOption", + Text = "S_EVENT_GROUP_CHANGE_OPTION" }, -- dynamic cargo [EVENTS.NewDynamicCargo] = { @@ -1234,7 +1243,16 @@ function EVENT:onEvent( Event ) if Event.initiator then - Event.IniObjectCategory = Object.getCategory(Event.initiator) + if Event.id == EVENTS.GroupChangeOption then + Event.IniDCSGroup = Event.initiator + Event.IniDCSGroupName = Group.getName(Event.initiator) + Event.IniGroupName = Event.IniDCSGroupName + Event.IniGroup = GROUP:FindByName(Event.IniDCSGroupName) + Event.IniCoalition = Group.getCoalition(Event.initiator) + Event.IniGroupCategory = Group.getCategory(Event.initiator) + else + Event.IniObjectCategory = Object.getCategory(Event.initiator) + end if Event.IniObjectCategory == Object.Category.STATIC then --- diff --git a/Moose Development/Moose/DCS.lua b/Moose Development/Moose/DCS.lua index f726caf31..ddbfd0e8f 100644 --- a/Moose Development/Moose/DCS.lua +++ b/Moose Development/Moose/DCS.lua @@ -87,7 +87,8 @@ do -- world -- @field S_EVENT_SIMULATION_UNFREEZE = 58 -- @field S_EVENT_HUMAN_AIRCRAFT_REPAIR_START = 59 -- @field S_EVENT_HUMAN_AIRCRAFT_REPAIR_FINISH = 60 - -- @field S_EVENT_MAX = 61 + -- @field S_EVENT_GROUP_CHANGE_OPTION = 61 + -- @field S_EVENT_MAX = 62 --- The birthplace enumerator is used to define where an aircraft or helicopter has spawned in association with birth events. -- @type world.BirthPlace From b1f86c702d80bae0d2acb143c6388ced85d97b73 Mon Sep 17 00:00:00 2001 From: shafik Date: Thu, 16 Jul 2026 19:48:35 +0200 Subject: [PATCH 11/18] [ADDED] F-100 weapons enums --- Moose Development/Moose/Utilities/Enums.lua | 41 +++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/Moose Development/Moose/Utilities/Enums.lua b/Moose Development/Moose/Utilities/Enums.lua index 62fe643d7..e7da59714 100644 --- a/Moose Development/Moose/Utilities/Enums.lua +++ b/Moose Development/Moose/Utilities/Enums.lua @@ -2126,6 +2126,47 @@ ENUMS.Storage.weapons.containers.UH60_GAU19_RIGHT = 'weapons.containers.{UH60_GA ENUMS.Storage.weapons.containers.UH60_M230_RIGHT = 'weapons.containers.{UH60_M230_RIGHT}' ENUMS.Storage.weapons.containers.UH60L_M2_GUNNER = 'weapons.containers.{UH60L_M2_GUNNER}' + +ENUMS.Storage.weapons.adapters.GD_F100_SUU_13 = 'weapons.adapters.GD_F100_SUU-13' +ENUMS.Storage.weapons.containers.GPU_5_POD = 'weapons.containers.{GPU_5_POD}' +ENUMS.Storage.weapons.bombs.BOLT_117 = 'weapons.bombs.BOLT-117' +ENUMS.Storage.weapons.shells.F100_M39_20_TP_T = 'weapons.shells.F100_M39_20_TP_T' +ENUMS.Storage.weapons.gunmounts.M_39_F_100 = 'weapons.gunmounts.M-39 F-100' +ENUMS.Storage.weapons.droptanks.GD_F100_TANK_275_L = 'weapons.droptanks.GD_F100_TANK_275_L' +ENUMS.Storage.weapons.bombs.BLU_4B_R_GROUP_R = 'weapons.bombs.BLU-4B_R_GROUP_R' +ENUMS.Storage.weapons.containers.F100_CAMERA_P2 = 'weapons.containers.F100_CAMERA_P2' +ENUMS.Storage.weapons.containers.HB_ORD_SUU_7 = 'weapons.containers.HB_ORD_SUU_7' +ENUMS.Storage.weapons.adapters.SidewinderPylon = 'weapons.adapters.SidewinderPylon' +ENUMS.Storage.weapons.adapters.apu_470 = 'weapons.adapters.apu_470' +ENUMS.Storage.weapons.droptanks.GD_F100_TANK_335_R = 'weapons.droptanks.GD_F100_TANK_335_R' +ENUMS.Storage.weapons.bombs.BLU_3B_R_GROUP_R = 'weapons.bombs.BLU-3B_R_GROUP_R' +ENUMS.Storage.weapons.droptanks.GD_F100_TANK_335_L = 'weapons.droptanks.GD_F100_TANK_335_L' +ENUMS.Storage.weapons.adapters.mbdz_u2t = 'weapons.adapters.mbdz_u2t' +ENUMS.Storage.weapons.containers.ZellRocket = 'weapons.containers.ZellRocket' +ENUMS.Storage.weapons.containers.apu_73 = 'weapons.containers.apu-73' +ENUMS.Storage.weapons.gunmounts.GAU_13 = 'weapons.gunmounts.GAU-13' +ENUMS.Storage.weapons.shells.F100_M39_20_TP = 'weapons.shells.F100_M39_20_TP' +ENUMS.Storage.weapons.shells.F100_M39_20_API = 'weapons.shells.F100_M39_20_API' +ENUMS.Storage.weapons.adapters.bdz_umk2b = 'weapons.adapters.bdz_umk2b' +ENUMS.Storage.weapons.droptanks.GD_F100_TANK_450 = 'weapons.droptanks.GD_F100_TANK_450' +ENUMS.Storage.weapons.droptanks.GD_F100_TANK_275_R = 'weapons.droptanks.GD_F100_TANK_275_R' +ENUMS.Storage.weapons.bombs.BLU_18_B_GROUP = 'weapons.bombs.BLU-18/B_GROUP' +ENUMS.Storage.weapons.containers.ZELL_Booster = 'weapons.containers.ZELL Booster' +ENUMS.Storage.weapons.adapters.apu_60 = 'weapons.adapters.apu_60' +ENUMS.Storage.weapons.gunmounts.GPU_5_POD = 'weapons.gunmounts.{GPU_5_POD}' +ENUMS.Storage.weapons.containers.F100_CAMERA_KA71A = 'weapons.containers.F100_CAMERA_KA71A' +ENUMS.Storage.weapons.bombs.BLU_3_R_GROUP_R = 'weapons.bombs.BLU-3_R_GROUP_R' +ENUMS.Storage.weapons.shells.F100_M39_20_HEI = 'weapons.shells.F100_M39_20_HEI' +ENUMS.Storage.weapons.droptanks.GD_F100_TANK_200 = 'weapons.droptanks.GD_F100_TANK_200' +ENUMS.Storage.weapons.adapters.apu_73 = 'weapons.adapters.apu-73' +ENUMS.Storage.weapons.gunmounts.AKAN_NO_TRC = 'weapons.gunmounts.AKAN_NO_TRC' +ENUMS.Storage.weapons.gunmounts.AKAN = 'weapons.gunmounts.AKAN' +ENUMS.Storage.weapons.shells.F100_M39_20_HEI_T = 'weapons.shells.F100_M39_20_HEI_T' +ENUMS.Storage.weapons.shells.F100_M39_20_API_T = 'weapons.shells.F100_M39_20_API_T' +ENUMS.Storage.weapons.containers.GD_F100_STRIKE_CAMERA = 'weapons.containers.GD_F100_STRIKE_CAMERA' +ENUMS.Storage.weapons.bombs.BLU_49_B = 'weapons.bombs.BLU-49/B' +ENUMS.Storage.weapons.containers.GD_F100_SUU_13 = 'weapons.containers.GD_F100_SUU-13' + --- -- @type ENUMS.FARPType -- @field #string FARP From 64dea179828ee95605c0c171011a530688e06126 Mon Sep 17 00:00:00 2001 From: shafik Date: Thu, 16 Jul 2026 19:58:36 +0200 Subject: [PATCH 12/18] [FIXED] Hardcoded "blue" coalition in SET filters (Doesn't work for redfor in PvP). [FIXED] Unnecessary new COORDINATE objects that don't hold reference (used only for distance calc). --- Moose Development/Moose/Ops/CTLD.lua | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/Moose Development/Moose/Ops/CTLD.lua b/Moose Development/Moose/Ops/CTLD.lua index 2b7bc20fa..0a967fda0 100644 --- a/Moose Development/Moose/Ops/CTLD.lua +++ b/Moose Development/Moose/Ops/CTLD.lua @@ -3747,16 +3747,16 @@ function CTLD:_C130GetUnits(Group, Unit, Name) return self end - local coord = Unit:GetCoordinate() or Group:GetCoordinate() + local coord = Unit:GetCoord() or Group:GetCoord() local capabilities = self:_GetUnitCapabilities(Unit) local innerDist = (capabilities.length and capabilities.length/2) or 15 local maxUnitsNearby = self.maxUnitsNearby or 3 local searchRadius = self.UnitDistance or 90 local checkZone = ZONE_RADIUS:New("CTLD_C130UnitsZone",coord:GetVec2(),searchRadius,false) - local nearGroups = SET_GROUP:New():FilterCoalitions("blue"):FilterZones({checkZone}):FilterOnce() + local nearGroups = SET_GROUP:New():FilterCoalitions(self.coalitiontxt):FilterZones({checkZone}):FilterOnce() local nearbyCount = 0 for _,gr in pairs(nearGroups.Set) do - local gc = gr:GetCoordinate() + local gc = gr:GetCoord() if gc then local dist = coord:Get2DDistance(gc) if dist > innerDist then @@ -3925,7 +3925,7 @@ function CTLD:_GetCrates(Group, Unit, Cargo, number, drop, pack, quiet, suppress -- Check cargo location if available local location = Cargo:GetLocation() if location then - local unitcoord = Unit:GetCoordinate() or Group:GetCoordinate() + local unitcoord = Unit:GetCoord() or Group:GetCoord() if unitcoord then if not location:IsCoordinateInZone(unitcoord) then -- no we're not at the right spot @@ -4136,8 +4136,8 @@ function CTLD:_GetCrates(Group, Unit, Cargo, number, drop, pack, quiet, suppress if type(ship) == "string" then self:T("Spawning on ship "..ship) local Ship = UNIT:FindByName(ship) - local shipcoord = Ship:GetCoordinate() - local unitcoord = Unit:GetCoordinate() + local shipcoord = Ship:GetCoord() + local unitcoord = Unit:GetCoord() local dist = shipcoord:Get2DDistance(unitcoord) dist = dist - (20 + math.random(1, 10)) local halfwidth = (width or 20) / 2 @@ -4395,11 +4395,11 @@ function CTLD:_C130RemoveUnitsNearby(_group,_unit) local innerDist = (capabilities.length and capabilities.length/2) or 15 local finddist = self.PackDistance or (self.CrateDistance or 35) local zone = ZONE_RADIUS:New("CTLD_C130RemoveZone",location:GetVec2(),finddist,false) - local nearestGroups = SET_GROUP:New():FilterCoalitions("blue"):FilterZones({zone}):FilterOnce() + local nearestGroups = SET_GROUP:New():FilterCoalitions(self.coalitiontxt):FilterZones({zone}):FilterOnce() local removedAny = false local removedTable = {} for _, gr in pairs(nearestGroups.Set) do - local gc = gr:GetCoordinate() + local gc = gr:GetCoord() if gc then local dist = location:Get2DDistance(gc) if dist > innerDist then @@ -5683,18 +5683,18 @@ end -- @return #number Number function CTLD:_FindPackableGroupsNearby(Group, Unit) self:T(self.lid .. " _FindPackableGroupsNearby") - local location = Group:GetCoordinate() + local location = Group:GetCoord() if not location then return {}, 0 end local capabilities = self:_GetUnitCapabilities(Unit) local innerDist = (capabilities.length and capabilities.length/2) or 15 local finddist = self.PackDistance or (self.CrateDistance or 35) local zone = ZONE_RADIUS:New("CTLD_PackableZone", location:GetVec2(), finddist, false) - local nearestGroups = SET_GROUP:New():FilterCoalitions("blue"):FilterZones({zone}):FilterOnce() + local nearestGroups = SET_GROUP:New():FilterCoalitions(self.coalitiontxt):FilterZones({zone}):FilterOnce() local packable = {} for _, gr in pairs(nearestGroups.Set) do if gr and gr:GetName() ~= Group:GetName() then - local gc = gr:GetCoordinate() + local gc = gr:GetCoord() if gc then local dist = location:Get2DDistance(gc) if dist > innerDist and dist <= finddist then @@ -9008,7 +9008,7 @@ function CTLD:IsUnitInZone(Unit,Zonetype) local zoneret = nil local zonewret = nil local zonenameret = nil - local unitcoord = Unit:GetCoordinate() + local unitcoord = Unit:GetCoord() if not unitcoord then if Zonetype == CTLD.CargoZoneType.SHIP then return false, nil, nil, 1000000, nil @@ -9029,7 +9029,7 @@ function CTLD:IsUnitInZone(Unit,Zonetype) self:T("Checking Type Ship: "..zonename) local ZoneUNIT = UNIT:FindByName(zonename) if not ZoneUNIT then return false end - zonecoord = ZoneUNIT:GetCoordinate() + zonecoord = ZoneUNIT:GetCoord() zoneradius = czone.shiplength zonewidth = czone.shipwidth zone = ZONE_UNIT:New( ZoneUNIT:GetName(), ZoneUNIT, zoneradius/2) From a5880b89c19f5281aa44f874638890dd838dc8f1 Mon Sep 17 00:00:00 2001 From: leka1986 Date: Wed, 22 Jul 2026 05:07:21 +0200 Subject: [PATCH 13/18] Fix escort, cargo, and MANTIS behavior --- Moose Development/Moose/Functional/Mantis.lua | 94 +++++++++++-------- Moose Development/Moose/Ops/Auftrag.lua | 8 +- Moose Development/Moose/Ops/CTLD.lua | 8 +- 3 files changed, 66 insertions(+), 44 deletions(-) diff --git a/Moose Development/Moose/Functional/Mantis.lua b/Moose Development/Moose/Functional/Mantis.lua index 858b9e302..a85774d7e 100644 --- a/Moose Development/Moose/Functional/Mantis.lua +++ b/Moose Development/Moose/Functional/Mantis.lua @@ -2043,35 +2043,31 @@ do -- @param #MANTIS self -- @param #number height -- @param Core.Point#COORDINATE SamCoordinate + -- @param #table contactSnapshot Contact facts captured once for the current check. -- @return #table set - function MANTIS:_PreFilterHeight(height,SamCoordinate) + function MANTIS:_PreFilterHeight(height,SamCoordinate,contactSnapshot) self:T(self.lid.."_PreFilterHeight") local set = {} - local dlink = self.Detection -- Ops.Intel#INTEL_DLINK - local detectedgroups = dlink:GetContactTable() - for _,_contact in pairs(detectedgroups) do - local contact = _contact -- Ops.Intel#INTEL.Contact + for _,contact in pairs(contactSnapshot) do local grp = contact.group -- Wrapper.Group#GROUP - if grp:IsAlive() then - local coord = grp:GetCoordinate() - local dist = 0 - local include = true - if grp:IsGround() then include = false end - if grp:IsShip() then include = false end -- MANTIS is anti-air: surface contacts are not engageable targets - if grp:GetCoalition() == self.coalition then include = false end - if coord and SamCoordinate and grp:IsHelicopter() then - dist = coord:Get2DDistance(SamCoordinate) or 0 - if dist > self.ShoradActDistance then include = false end -- we do not want long range shooting at helos - end - if self.debug then - local text = "Looking at Group: "..grp:GetName() or "N/A" - text = text .. " Include = "..tostring(include) - MESSAGE:New(text,10,"MANTIS"):ToAllIf(self.verbose):ToLog() - end - local grpalt = grp:GetHeight(true) - if grpalt < height and grpalt > 10 and include == true then - table.insert(set,coord) - end + local coord = contact.coordinate + local dist = 0 + local include = true + if contact.isGround then include = false end + if contact.isShip then include = false end -- MANTIS is anti-air: surface contacts are not engageable targets + if contact.coalition == self.coalition then include = false end + if coord and SamCoordinate and contact.isHelicopter then + dist = coord:Get2DDistance(SamCoordinate) or 0 + if dist > self.ShoradActDistance then include = false end -- we do not want long range shooting at helos + end + if self.debug then + local text = "Looking at Group: "..grp:GetName() or "N/A" + text = text .. " Include = "..tostring(include) + MESSAGE:New(text,10,"MANTIS"):ToAllIf(self.verbose):ToLog() + end + local grpalt = contact.height + if grpalt < height and grpalt > 10 and include == true then + table.insert(set,coord) end end return set @@ -2084,16 +2080,17 @@ do -- @param #number radius Radius to check. -- @param #number height Height to check. -- @param #boolean dlink Data from DLINK. + -- @param #table contactSnapshot Contact facts captured once for the current check. -- @return #boolean True if in any zone, else false -- @return #number Distance Target distance in meters or zero when no object is in zone - function MANTIS:_CheckObjectInZone(dectset, samcoordinate, radius, height, dlink) + function MANTIS:_CheckObjectInZone(dectset, samcoordinate, radius, height, dlink, contactSnapshot) self:T(self.lid.."_CheckObjectInZone") -- check if non of the coordinate is in the given defense zone local rad = radius or self.checkradius local set = dectset if dlink then -- DEBUG - set = self:_PreFilterHeight(height,samcoordinate) + set = self:_PreFilterHeight(height,samcoordinate,contactSnapshot) end --self.friendlyset -- Core.Set#SET_GROUP if self.checkforfriendlies == true and self.friendlyset == nil then @@ -2732,7 +2729,8 @@ do group:OptionEngageRange(engagerange) --engagement will be 95% of firing range if (group:IsGround() or group:IsShip()) and group:IsAlive() then local grpname = group:GetName() - local grpcoord = group:GetCoordinate() + local grpcoord = group:GetCoord() + if grpcoord then grpcoord.Heading = group:GetHeading() or 0 end if group:IsShip() and self.NavalPerUnit and self:_BuildNavalUnitEntries(group, grpname, SAM_Tbl, SAM_Tbl_lg, SAM_Tbl_md, SAM_Tbl_sh, SAM_Tbl_pt, SEAD_Grps) then self:T(grpname.." handled as per-unit naval group") @@ -2944,10 +2942,11 @@ function MANTIS:SeadAllowSuppression(targetGroup, targetName, attackerGroup, wea -- @param #table detset Table of COORDINATES -- @param #boolean dlink Using DLINK -- @param #number limit of SAM sites to go active on a contact + -- @param #table contactSnapshot Contact facts captured once for the current check. -- @return #number instatusred -- @return #number instatusgreen -- @return #number activeshorads - function MANTIS:_CheckLoop(samset,detset,dlink,limit) + function MANTIS:_CheckLoop(samset,detset,dlink,limit,contactSnapshot) self:T(self.lid .. "CheckLoop " .. #detset .. " Coordinates") local switchedon = 0 local instatusred = 0 @@ -2976,7 +2975,7 @@ function MANTIS:SeadAllowSuppression(targetGroup, targetName, attackerGroup, wea local samalive = false if navalparent then samalive = (samunit ~= nil) and samunit:IsAlive() or false elseif samgroup then samalive = samgroup:IsAlive() or false end - local IsInZone, Distance = self:_CheckObjectInZone(detset, samcoordinate, radius, height, dlink) + local IsInZone, Distance = self:_CheckObjectInZone(detset, samcoordinate, radius, height, dlink, contactSnapshot) -- Naval surface wake-up: quiet ships activate when enemy surface combatants -- close within the trigger radius. Jamming has precedence and holds them down. if (not IsInZone) and self.NavalSurfaceWakeup and self._navalSAMs and self._navalSAMs[name] @@ -3095,22 +3094,41 @@ function MANTIS:SeadAllowSuppression(targetGroup, targetName, attackerGroup, wea self:_RefreshSAMTable() end self.checkcounter = self.checkcounter + 1 + local contactSnapshot = nil + if dlink then + contactSnapshot = {} + for _,contact in pairs(detection:GetContactTable()) do + local grp = contact.group -- Wrapper.Group#GROUP + if grp:IsAlive() then + local coord = grp:GetCoord() + contactSnapshot[#contactSnapshot + 1] = { + group = grp, + coordinate = coord, + height = grp:GetHeight(true), + coalition = grp:GetCoalition(), + isGround = grp:IsGround(), + isShip = grp:IsShip(), + isHelicopter = grp:IsHelicopter(), + } + end + end + end local instatusred = 0 local instatusgreen = 0 local activeshorads = 0 -- switch SAMs on/off if (n)one of the detected groups is inside their reach if self.automode then local samset = self.SAM_Table_Long -- table of i.1=names, i.2=coordinates, i.3=firing range, i.4=firing height - local instatusredl, instatusgreenl, activeshoradsl = self:_CheckLoop(samset,detset,dlink,self.maxlongrange) + local instatusredl, instatusgreenl, activeshoradsl = self:_CheckLoop(samset,detset,dlink,self.maxlongrange,contactSnapshot) local samset = self.SAM_Table_Medium -- table of i.1=names, i.2=coordinates, i.3=firing range, i.4=firing height - local instatusredm, instatusgreenm, activeshoradsm = self:_CheckLoop(samset,detset,dlink,self.maxmidrange) + local instatusredm, instatusgreenm, activeshoradsm = self:_CheckLoop(samset,detset,dlink,self.maxmidrange,contactSnapshot) local samset = self.SAM_Table_Short -- table of i.1=names, i.2=coordinates, i.3=firing range, i.4=firing height - local instatusreds, instatusgreens, activeshoradss = self:_CheckLoop(samset,detset,dlink,self.maxshortrange) + local instatusreds, instatusgreens, activeshoradss = self:_CheckLoop(samset,detset,dlink,self.maxshortrange,contactSnapshot) local samset = self.SAM_Table_PointDef -- table of i.1=names, i.2=coordinates, i.3=firing range, i.4=firing height - local instatusred, instatusgreen, activeshorads = self:_CheckLoop(samset,detset,dlink,self.maxpointdefrange) + local instatusred, instatusgreen, activeshorads = self:_CheckLoop(samset,detset,dlink,self.maxpointdefrange,contactSnapshot) else local samset = self:_GetSAMTable() -- table of i.1=names, i.2=coordinates, i.3=firing range, i.4=firing height - local instatusred, instatusgreen, activeshorads = self:_CheckLoop(samset,detset,dlink,self.maxclassic) + local instatusred, instatusgreen, activeshorads = self:_CheckLoop(samset,detset,dlink,self.maxclassic,contactSnapshot) end local function GetReport() @@ -4090,8 +4108,8 @@ function MANTIS:SeadAllowSuppression(targetGroup, targetName, attackerGroup, wea end --- [Internal] Override: _CheckLoop with jammer suppression. - function MANTIS:_CheckLoop(samset, detset, dlink, limit) - local r, g, s = self:_CheckLoopOriginal(samset, detset, dlink, limit) + function MANTIS:_CheckLoop(samset, detset, dlink, limit, contactSnapshot) + local r, g, s = self:_CheckLoopOriginal(samset, detset, dlink, limit, contactSnapshot) if self._jammerEnabled and self._jammedSAMs then for _, _data in pairs(samset) do local name = _data[1] @@ -4355,4 +4373,4 @@ end ----------------------------------------------------------------------- -- MANTIS end ------------------------------------------------------------------------ \ No newline at end of file +----------------------------------------------------------------------- diff --git a/Moose Development/Moose/Ops/Auftrag.lua b/Moose Development/Moose/Ops/Auftrag.lua index 78c0e777d..6aa1f063d 100644 --- a/Moose Development/Moose/Ops/Auftrag.lua +++ b/Moose Development/Moose/Ops/Auftrag.lua @@ -6466,7 +6466,13 @@ function AUFTRAG:GetDCSMissionTask(MissionGroup) -- ESCORT Mission -- -------------------- - local DCStask=CONTROLLABLE.TaskEscort(nil, self.engageTarget:GetObject(), self.escortVec3, nil, self.engageMaxDistance, self.engageTargetTypes) + local FollowGroup=self.engageTarget:GetObject() + local DCStask=nil + if next(self.engageTargetTypes)==nil then + DCStask=FollowGroup:TaskFollow(FollowGroup, self.escortVec3) + else + DCStask=CONTROLLABLE.TaskEscort(nil, FollowGroup, self.escortVec3, nil, self.engageMaxDistance, self.engageTargetTypes) + end table.insert(DCStasks, DCStask) diff --git a/Moose Development/Moose/Ops/CTLD.lua b/Moose Development/Moose/Ops/CTLD.lua index 6e6226502..a97619c43 100644 --- a/Moose Development/Moose/Ops/CTLD.lua +++ b/Moose Development/Moose/Ops/CTLD.lua @@ -2748,16 +2748,14 @@ function CTLD:_EventHandler(EventData) loaded = self.Loaded_Cargo[unitname] -- #CTLD.LoadedCargo loaded.Cratesloaded = loaded.Cratesloaded - 1 if loaded.Cratesloaded < 0 then loaded.Cratesloaded = 0 end - -- TODO zap cargo from list local Loaded = {} for _,_item in pairs (loaded.Cargo or {}) do self:T(self.lid.."UNLOAD checking: ".._item:GetName()) self:T(self.lid.."UNLOAD state: ".. tostring(_item:WasDropped())) - if _item and _item:GetType() == CTLD_CARGO.Enum.GCLOADABLE and event.IniDynamicCargoName and event.IniDynamicCargoName ~= _item:GetName() and not _item:WasDropped() then + local unloadedItem = _item and _item:GetType() == CTLD_CARGO.Enum.GCLOADABLE and event.IniDynamicCargoName and event.IniDynamicCargoName == _item:GetName() + if not unloadedItem then table.insert(Loaded,_item) - else - table.insert(Loaded,_item) - end + end end loaded.Cargo = nil loaded.Cargo = Loaded From 6e09ea7089124842f011d7d082238ce456488a1b Mon Sep 17 00:00:00 2001 From: Thomas <72444570+Applevangelist@users.noreply.github.com> Date: Thu, 23 Jul 2026 15:03:47 +0200 Subject: [PATCH 14/18] Update Airboss.lua Add F14BU --- Moose Development/Moose/Ops/Airboss.lua | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Moose Development/Moose/Ops/Airboss.lua b/Moose Development/Moose/Ops/Airboss.lua index db2419d7d..142a35991 100644 --- a/Moose Development/Moose/Ops/Airboss.lua +++ b/Moose Development/Moose/Ops/Airboss.lua @@ -45,7 +45,7 @@ -- **Supported Aircraft:** -- -- * [F/A-18C Hornet Lot 20](https://forums.eagle.ru/forumdisplay.php?f=557) (Player & AI) --- * [F-14A/B/A Early Tomcat](https://forums.eagle.ru/forumdisplay.php?f=395) (Player & AI) +-- * [F-14A/B/BU/A Early Tomcat](https://forums.eagle.ru/forumdisplay.php?f=395) (Player & AI) -- * [A-4E Skyhawk Community Mod](https://forums.eagle.ru/showthread.php?t=224989) (Player & AI) -- * [AV-8B N/A Harrier](https://forums.eagle.ru/forumdisplay.php?f=555) (Player & AI) -- * [T-45C Goshawk](https://forum.dcs.world/topic/203816-vnao-t-45-goshawk/) (VNAO mod) (Player & AI) @@ -1276,6 +1276,7 @@ AIRBOSS = { -- @field #string A4EC A-4E Community mod. -- @field #string HORNET F/A-18C Lot 20 Hornet by Eagle Dynamics. -- @field #string F14A F-14A by Heatblur. +-- @field #string F14BU F-14BU by Heatblur. -- @field #string F14A_Early F-14A-135-GR-Early by Heatblur. -- @field #string F14B F-14B by Heatblur. -- @field #string F14A_AI F-14A Tomcat (AI). @@ -1297,6 +1298,7 @@ AIRBOSS.AircraftCarrier={ F14A="F-14A-135-GR", F14A_Early="F-14A-135-GR-Early", F14B="F-14B", + F14BU="F-14BU", F14A_AI="F-14A", FA18C="F/A-18C", T45C="T-45", @@ -19394,4 +19396,4 @@ function AIRBOSS:onafterLSOGrade(From, Event, To, playerData, grade) self.funkmanSocket:SendTable(result) end -end \ No newline at end of file +end From fc1059c5d6784d696650a97f77a838593a47e90c Mon Sep 17 00:00:00 2001 From: shafik Date: Thu, 23 Jul 2026 17:25:41 +0200 Subject: [PATCH 15/18] CTLD Enhancements --- Moose Development/Moose/Ops/CTLD.lua | 91 +++++++++++++++------------- 1 file changed, 48 insertions(+), 43 deletions(-) diff --git a/Moose Development/Moose/Ops/CTLD.lua b/Moose Development/Moose/Ops/CTLD.lua index 6e6226502..426c84db9 100644 --- a/Moose Development/Moose/Ops/CTLD.lua +++ b/Moose Development/Moose/Ops/CTLD.lua @@ -183,6 +183,7 @@ do -- my_ctld.placeCratesAhead = false -- place crates straight ahead of the helicopter, in a random way. If true, crates are more neatly sorted. -- my_ctld.nobuildinloadzones = true -- forbid players to build stuff in LOAD zones if set to `true` -- my_ctld.movecratesbeforebuild = true -- crates must be moved once before they can be build. Set to false for direct builds. +-- my_ctld.useaircraftheadingforbuilds = false -- Matches the heading of the spawned groups from a crate build to the aircraft heading when starting the build. Default is false for template heading. -- my_ctld.surfacetypes = {land.SurfaceType.LAND,land.SurfaceType.ROAD,land.SurfaceType.RUNWAY,land.SurfaceType.SHALLOW_WATER} -- surfaces for loading back objects. -- my_ctld.nobuildmenu = false -- if set to true effectively enforces to have engineers build/repair stuff for you. -- my_ctld.RadioSound = "beacon.ogg" -- -- this sound will be hearable if you tune in the beacon frequency. Add the sound file to your miz. @@ -2037,7 +2038,7 @@ function CTLD:_C130DcAutoRegisterEntry(SetId, Cargo) local pos = Cargo:GetPositionable() local pname = pos and pos.GetName and pos:GetName() or nil - local pcoord = pos and pos.GetCoordinate and pos:GetCoordinate() or nil + local pcoord = pos and pos.GetCoord and pos:GetCoord() or nil local entryId = string.format("%s#%d", SetId, #setData.entries + 1) local entry = { id = entryId, @@ -3125,14 +3126,14 @@ end function CTLD:_FindRepairNearby(Group, Unit, Repairtype) self:T(self.lid .. " _FindRepairNearby") --self:T({Group:GetName(),Unit:GetName(),Repairtype}) - local unitcoord = Unit:GetCoordinate() + local unitcoord = Unit:GetCoord() -- find nearest group of deployed groups local nearestGroup = nil local nearestGroupIndex = -1 local nearestDistance = 10000 for k,v in pairs(self.DroppedTroops) do - local distance = self:_GetDistance(v:GetCoordinate(),unitcoord) + local distance = self:_GetDistance(v:GetCoord(),unitcoord) local unit = v:GetUnit(1) -- Wrapper.Unit#UNIT local desc = unit:GetDesc() or nil if distance < nearestDistance and distance ~= -1 and not desc.attributes.Infantry then @@ -3277,7 +3278,7 @@ end local capabilities = self:_GetUnitCapabilities(Unit) local cantroops = capabilities.troops -- #boolean local trooplimit = capabilities.trooplimit -- #number - local unitcoord = unit:GetCoordinate() + local unitcoord = unit:GetCoord() -- find nearest group of deployed troops local nearestGroup = nil @@ -3288,7 +3289,7 @@ end local distancekeys = {} local extractdistance = self.CrateDistance * self.ExtractFactor for k,v in pairs(self.DroppedTroops) do - local distance = self:_GetDistance(v:GetCoordinate(),unitcoord) + local distance = self:_GetDistance(v:GetCoord(),unitcoord) local TNow = timer.getTime() local vtime = v.ExtractTime or TNow-310 if distance <= extractdistance and distance ~= -1 and (TNow - vtime > 300) then @@ -3376,7 +3377,7 @@ end self:_UpdateUnitCargoMass(Unit) local groupname = nearestGroup:GetName() self:__TroopsExtracted(running,Group, Unit, nearestGroup, groupname) - local coord = Unit:GetCoordinate() or Group:GetCoordinate() -- Core.Point#COORDINATE + local coord = Unit:GetCoord() or Group:GetCoord() -- Core.Point#COORDINATE local Point if coord then local heading = unit:GetHeading() or 0 @@ -3806,6 +3807,7 @@ function CTLD:_C130GetUnits(Group, Unit, Name) if canmove then SPAWN:NewWithAlias(_template,alias) :InitRandomizeUnits(true,10,2) + :InitCoalition(self.coalition) :InitValidateAndRepositionGroundUnits(self.validateAndRepositionUnits, 70) :InitDelayOff() :OnSpawnGroup(function(grp,TimeStamp) @@ -3819,6 +3821,7 @@ function CTLD:_C130GetUnits(Group, Unit, Name) SPAWN:NewWithAlias(_template,alias) :InitRandomizeUnits(true,10,2) :InitDelayOff() + :InitCoalition(self.coalition) :InitValidateAndRepositionGroundUnits(self.validateAndRepositionUnits, 70) :OnSpawnGroup(function(grp,TimeStamp) grp.spawntime = TimeStamp or timer.getTime() @@ -3972,7 +3975,7 @@ function CTLD:_GetCrates(Group, Unit, Cargo, number, drop, pack, quiet, suppress end -- get position and heading of heli - local position = Unit:GetCoordinate() + local position = Unit:GetCoord() local heading = Unit:GetHeading() + 1 local height = Unit:GetHeight() local droppedcargo = {} @@ -4389,7 +4392,7 @@ end function CTLD:_C130RemoveUnitsNearby(_group,_unit) self:T(self.lid .. " _C130RemoveUnitsNearby") if not _group or not _unit then return self end - local location = _group:GetCoordinate() + local location = _group:GetCoord() if not location then return self end local capabilities = self:_GetUnitCapabilities(_unit) local innerDist = (capabilities.length and capabilities.length/2) or 15 @@ -4533,7 +4536,7 @@ end function CTLD:_FindCratesNearby( _group, _unit, _dist, _ignoreweight, ignoretype, ignoreHercInner) self:T(self.lid .. " _FindCratesNearby") local finddist = _dist - local location = _group:GetCoordinate() + local location = _group:GetCoord() local existingcrates = self.Spawned_Cargo -- #table -- cycle local index = 0 @@ -5159,7 +5162,6 @@ function CTLD:_UnloadTroops(Group, Unit) -- unload troops local name = cargo:GetName() or "none" local temptable = cargo:GetTemplates() or {} - local position = Group:GetCoordinate() local zoneradius = self.troopdropzoneradius or 100 -- drop zone radius local factor = 1 if IsHerc then @@ -5196,6 +5198,7 @@ function CTLD:_UnloadTroops(Group, Unit) local Positions = self:_GetUnitPositions(randomcoord,rad,heading,_template) self.DroppedTroops[self.TroopCounter] = SPAWN:NewWithAlias(_template,alias) :InitDelayOff() + :InitCoalition(self.coalition) :InitSetUnitAbsolutePositions(Positions) :InitValidateAndRepositionGroundUnits(self.validateAndRepositionUnits) :OnSpawnGroup(function(grp) grp.spawntime = timer.getTime() end) @@ -5506,8 +5509,8 @@ function CTLD:_BuildCrates(Group, Unit,Engineering,MultiDrop,NotifyGroup) local required = Crate:GetCratesNeeded() local template = Crate:GetTemplates() local ctype = Crate:GetType() - local ccoord = Crate:GetPositionable():GetCoordinate() -- Core.Point#COORDINATE - local distToUnit=Unit and ccoord:Get2DDistance(Unit:GetCoordinate())or 0 + local ccoord = Crate:GetPositionable():GetCoord() -- Core.Point#COORDINATE + local distToUnit=Unit and ccoord:Get2DDistance(Unit:GetCoord())or 0 local isHercDrop=Crate:WasDropped(true) if not isHercDrop and distToUnit>baseDist then elseif self:IsC130J(Unit) and distToUnit<15 then @@ -5526,6 +5529,7 @@ function CTLD:_BuildCrates(Group, Unit,Engineering,MultiDrop,NotifyGroup) object.Found = 1 object.Template = template object.CanBuild = false + object.Heading = Unit:GetHeading() object.Type = ctype -- #CTLD_CARGO.Enum object.Coord = ccoord:GetVec2() buildables[name] = object @@ -5611,7 +5615,7 @@ function CTLD:_BuildCrates(Group, Unit,Engineering,MultiDrop,NotifyGroup) local sep = self.buildPairSeparation or 25 local hdg = (Unit:GetHeading()+180)%360 local lat = (hdg+90)%360 - local base = Unit:GetCoordinate():Translate(20,hdg) + local base = Unit:GetCoord():Translate(20,hdg) if full == 1 then local cratesNow, numberNow = self:_FindCratesNearby(Group,Unit, finddist,true,true, not Engineering) @@ -5645,7 +5649,7 @@ function CTLD:_BuildCrates(Group, Unit,Engineering,MultiDrop,NotifyGroup) self:_RefreshPackMenus(Group,Unit) local off = start + (n-1)*sep local coord = base:Translate(off,lat):GetVec2() - local b = { Name=build.Name, Required=build.Required, Template=build.Template, CanBuild=true, Type=build.Type, Coord=coord } + local b = { Name=build.Name, Required=build.Required, Template=build.Template, CanBuild=true, Type=build.Type, Coord=coord, Heading=build.Heading } if self.buildtime and self.buildtime > 0 then local buildtimer = TIMER:New(self._BuildObjectFromCrates,self,Group,Unit,b,false,Group:GetCoordinate(),MultiDrop) buildtimer:Start(self.buildtime) @@ -5827,12 +5831,12 @@ function CTLD:_LoadPackedCratesByIds(Group, Unit, crateIds, cargoName) local matchingCrates = {} local finddist = self.CrateDistance or 35 - local location = Group:GetCoordinate() + local location = Group:GetCoord() for _, crateObj in pairs(self.Spawned_Cargo or {}) do if crateObj and idLookup[crateObj:GetID()] then local pos = crateObj:GetPositionable() if pos and pos:IsAlive() then - local dist = location:Get2DDistance(pos:GetCoordinate()) + local dist = location:Get2DDistance(pos:GetCoord()) if dist <= finddist then matchingCrates[#matchingCrates + 1] = crateObj end @@ -5942,12 +5946,12 @@ function CTLD:_RemovePackedCratesByIds(Group, Unit, crateIds) local crates = {} local finddist = self.CrateDistance or 35 - local location = Group:GetCoordinate() + local location = Group:GetCoord() for _, entry in pairs(self.Spawned_Cargo or {}) do if entry and idLookup[entry:GetID()] then local pos = entry:GetPositionable() if pos and pos:IsAlive() then - local dist = location:Get2DDistance(pos:GetCoordinate()) + local dist = location:Get2DDistance(pos:GetCoord()) if dist <= finddist then crates[#crates + 1] = entry end @@ -6197,24 +6201,24 @@ function CTLD:_BuildObjectFromCrates(Group,Unit,Build,Repair,RepairLocation,Mult for _,_template in pairs(temptable) do self.TroopCounter = self.TroopCounter + 1 local alias = string.format("%s-%d", _template, math.random(1,100000)) + local spawn = SPAWN:NewWithAlias(_template,alias) + :InitDelayOff() + :InitCoalition(self.coalition) + :InitValidateAndRepositionGroundUnits(self.validateAndRepositionUnits) + :OnSpawnGroup(function(grp) grp.spawntime = timer.getTime() end) if canmove then - self.DroppedTroops[self.TroopCounter] = SPAWN:NewWithAlias(_template,alias) - --:InitRandomizeUnits(true,20,2) - :InitDelayOff() - :InitValidateAndRepositionGroundUnits(self.validateAndRepositionUnits) - :OnSpawnGroup(function(grp) grp.spawntime = timer.getTime() end) - :SpawnFromVec2(randomcoord) - else -- don't random position of e.g. SAM units build as FOB - self.DroppedTroops[self.TroopCounter] = SPAWN:NewWithAlias(_template,alias) - :InitDelayOff() - :InitValidateAndRepositionGroundUnits(self.validateAndRepositionUnits) - :OnSpawnGroup(function(grp) grp.spawntime = timer.getTime() end) - :SpawnFromVec2(randomcoord) + --spawn:InitRandomizeUnits(true,20,2) end + if Build.Heading and self.buildcrateswithaircraftheading then + spawn:InitGroupHeading(Build.Heading) + end + local spawnedGroup = spawn:SpawnFromVec2(randomcoord) + self.DroppedTroops[self.TroopCounter] = spawnedGroup + if Repair then - self:__CratesRepaired(1,Group,Unit,self.DroppedTroops[self.TroopCounter]) + self:__CratesRepaired(1,Group,Unit,spawnedGroup) else - self:__CratesBuild(1,Group,Unit,self.DroppedTroops[self.TroopCounter]) + self:__CratesBuild(1,Group,Unit,spawnedGroup) end end -- template loop @@ -6244,14 +6248,11 @@ end -- @param Wrapper.Group#GROUP Group The Group to move. function CTLD:_MoveGroupToZone(Group) self:T(self.lid .. " _MoveGroupToZone") - local groupname = Group:GetName() or "none" - local groupcoord = Group:GetCoordinate() -- Get closest zone of type local outcome, name, zone, distance = self:IsUnitInZone(Group,CTLD.CargoZoneType.MOVE) self:T({canmove=outcome, name=name, zone=zone, dist=distance,max=self.movetroopsdistance}) if (distance <= self.movetroopsdistance) and outcome == true and zone~= nil then -- yes, we can ;) - local groupname = Group:GetName() local zonecoord = zone:GetRandomCoordinate(20,125) -- Core.Point#COORDINATE local formation = self:_GetVehicleFormation() --local coordinate = zonecoord:GetVec2() @@ -7934,6 +7935,7 @@ function CTLD:_UnloadSingleTroopByID(Group, Unit, chunkID, qty) local Positions = self:_GetUnitPositions(randomcoord, rad, heading, _template) self.DroppedTroops[self.TroopCounter] = SPAWN:NewWithAlias(_template, alias) :InitDelayOff() + :InitCoalition(self.coalition) :InitSetUnitAbsolutePositions(Positions) :InitValidateAndRepositionGroundUnits(self.validateAndRepositionUnits) :OnSpawnGroup(function(grp) grp.spawntime = timer.getTime() end) @@ -9081,7 +9083,7 @@ function CTLD:SmokePositionNow(Unit, Flare, SmokeColor) end local FlareColor = self.FlareColor or FLARECOLOR.Red -- table of #CTLD.CargoZone table - local unitcoord = Unit:GetCoordinate() -- Core.Point#COORDINATE + local unitcoord = Unit:GetCoord() -- Core.Point#COORDINATE local Group = Unit:GetGroup() if Flare then unitcoord:Flare(FlareColor, 90) @@ -9100,7 +9102,7 @@ end function CTLD:SmokeZoneNearBy(Unit, Flare) self:T(self.lid .. " SmokeZoneNearBy") -- table of #CTLD.CargoZone table - local unitcoord = Unit:GetCoordinate() + local unitcoord = Unit:GetCoord() local Group = Unit:GetGroup() local smokedistance = self.smokedistance local smoked = false @@ -9237,7 +9239,7 @@ end -- get speed and height local uspeed = Unit:GetVelocityMPS() local uheight = Unit:GetHeight() - local ucoord = Unit:GetCoordinate() + local ucoord = Unit:GetCoord() if not ucoord then return false end @@ -9266,7 +9268,7 @@ end -- get speed and height local uspeed = Unit:GetVelocityMPS() local uheight = Unit:GetHeight() - local ucoord = Unit:GetCoordinate() + local ucoord = Unit:GetCoord() if not ucoord then return false end @@ -9358,7 +9360,7 @@ end minheight = 5.1 -- herc is 5m AGL on the ground end local uheight = Unit:GetHeight() - local ucoord = Unit:GetCoordinate() + local ucoord = Unit:GetCoord() if not ucoord then return false end @@ -10144,6 +10146,7 @@ end local alias = string.format("%s-%d", _template, math.random(1,100000)) self.DroppedTroops[self.TroopCounter] = SPAWN:NewWithAlias(_template,alias) :InitRandomizeUnits(randompositions,20,2) + :InitCoalition(self.coalition) :InitValidateAndRepositionGroundUnits(self.validateAndRepositionUnits) :InitDelayOff() :OnSpawnGroup(function(grp,TimeStamp) grp.spawntime = TimeStamp or timer.getTime() end,TimeStamp) @@ -10298,6 +10301,7 @@ end if canmove then self.DroppedTroops[self.TroopCounter] = SPAWN:NewWithAlias(_template,alias) :InitRandomizeUnits(true,20,2) + :InitCoalition(self.coalition) :InitValidateAndRepositionGroundUnits(self.validateAndRepositionUnits) :InitDelayOff() :OnSpawnGroup(function(grp,TimeStamp) grp.spawntime = TimeStamp or timer.getTime() end,TimeStamp) @@ -10305,6 +10309,7 @@ end else -- don't random position of e.g. SAM units build as FOB self.DroppedTroops[self.TroopCounter] = SPAWN:NewWithAlias(_template,alias) :InitDelayOff() + :InitCoalition(self.coalition) :InitValidateAndRepositionGroundUnits(self.validateAndRepositionUnits) :OnSpawnGroup(function(grp,TimeStamp) grp.spawntime = TimeStamp or timer.getTime() end,TimeStamp) :SpawnFromVec2(randomcoord) @@ -10586,7 +10591,7 @@ end self:T({From, Event, To}) if Unit and Unit:IsPlayer() and self.PlayerTaskQueue then local playername = Unit:GetPlayerName() - local dropcoord = Troops:GetCoordinate() or COORDINATE:New(0,0,0) + local dropcoord = Troops:GetCoord() or COORDINATE:New(0,0,0) local dropvec2 = dropcoord:GetVec2() self.PlayerTaskQueue:ForEach( function (Task) @@ -10643,7 +10648,7 @@ end for _,_cargo in pairs(Cargotable) do local Vehicle = _cargo.Positionable if Vehicle then - local dropcoord = Vehicle:GetCoordinate() or COORDINATE:New(0,0,0) + local dropcoord = Vehicle:GetCoord() or COORDINATE:New(0,0,0) local dropvec2 = dropcoord:GetVec2() self.PlayerTaskQueue:ForEach( function (Task) @@ -10708,7 +10713,7 @@ end self:T({From, Event, To}) if Unit and Unit:IsPlayer() and self.PlayerTaskQueue then local playername = Unit:GetPlayerName() - local dropcoord = Vehicle:GetCoordinate() or COORDINATE:New(0,0,0) + local dropcoord = Vehicle:GetCoord() or COORDINATE:New(0,0,0) local dropvec2 = dropcoord:GetVec2() self.PlayerTaskQueue:ForEach( function (Task) From 2f229c1d8c1e33247aee0cf5d3ac23a1ae752ae3 Mon Sep 17 00:00:00 2001 From: shafik Date: Thu, 23 Jul 2026 17:51:51 +0200 Subject: [PATCH 16/18] [ADDED] New equipment to ENUMS (GBU-24E + more) --- Moose Development/Moose/Utilities/Enums.lua | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/Moose Development/Moose/Utilities/Enums.lua b/Moose Development/Moose/Utilities/Enums.lua index 9001858c1..1953f03e5 100644 --- a/Moose Development/Moose/Utilities/Enums.lua +++ b/Moose Development/Moose/Utilities/Enums.lua @@ -2165,7 +2165,16 @@ ENUMS.Storage.weapons.shells.F100_M39_20_HEI_T = 'weapons.shells.F100_M39_20_HEI ENUMS.Storage.weapons.shells.F100_M39_20_API_T = 'weapons.shells.F100_M39_20_API_T' ENUMS.Storage.weapons.containers.GD_F100_STRIKE_CAMERA = 'weapons.containers.GD_F100_STRIKE_CAMERA' ENUMS.Storage.weapons.bombs.BLU_49_B = 'weapons.bombs.BLU-49/B' + +ENUMS.Storage.weapons.bombs.GBU_24E = 'weapons.bombs.GBU_24E' ENUMS.Storage.weapons.containers.GD_F100_SUU_13 = 'weapons.containers.GD_F100_SUU-13' +ENUMS.Storage.weapons.gunmounts.AKAN = 'weapons.gunmounts.{AKAN}' +ENUMS.Storage.weapons.gunmounts.AKAN_NO_TRC = 'weapons.gunmounts.{AKAN_NO_TRC}' +ENUMS.Storage.weapons.gunmounts.SUU_23_POD_Wing = 'weapons.gunmounts.{SUU_23_POD_Wing}' +ENUMS.Storage.weapons.containers.SUU_23_POD_Wing = 'weapons.containers.{SUU_23_POD_Wing}' +ENUMS.Storage.weapons.containers.SUU_23_POD_Centerline = 'weapons.containers.{SUU_23_POD_Centerline}' +ENUMS.Storage.weapons.gunmounts.SUU_23_POD_Centerline = 'weapons.gunmounts.{SUU_23_POD_Centerline}' +ENUMS.Storage.weapons.containers.HB_F_4E_BRU_42 = 'weapons.containers.HB_F-4E_BRU-42' --- -- @type ENUMS.FARPType From d9a6f5daafea47b476d8fac85de346c7cbfc03f0 Mon Sep 17 00:00:00 2001 From: patpatpowercat <56987247+patpatpowercat@users.noreply.github.com> Date: Sat, 25 Jul 2026 22:03:47 -0700 Subject: [PATCH 17/18] Update Airboss.lua Updated to fully add F-14BU. Added _IsTomcat to combine all together rather than have to update each individually multiple locations. --- Moose Development/Moose/Ops/Airboss.lua | 74 ++++++++++++++++++------- 1 file changed, 53 insertions(+), 21 deletions(-) diff --git a/Moose Development/Moose/Ops/Airboss.lua b/Moose Development/Moose/Ops/Airboss.lua index 142a35991..304233b81 100644 --- a/Moose Development/Moose/Ops/Airboss.lua +++ b/Moose Development/Moose/Ops/Airboss.lua @@ -45,7 +45,7 @@ -- **Supported Aircraft:** -- -- * [F/A-18C Hornet Lot 20](https://forums.eagle.ru/forumdisplay.php?f=557) (Player & AI) --- * [F-14A/B/BU/A Early Tomcat](https://forums.eagle.ru/forumdisplay.php?f=395) (Player & AI) +-- * [F-14A/B/B(U)/A Early Tomcat](https://forums.eagle.ru/forumdisplay.php?f=395) (Player & AI) -- * [A-4E Skyhawk Community Mod](https://forums.eagle.ru/showthread.php?t=224989) (Player & AI) -- * [AV-8B N/A Harrier](https://forums.eagle.ru/forumdisplay.php?f=555) (Player & AI) -- * [T-45C Goshawk](https://forum.dcs.world/topic/203816-vnao-t-45-goshawk/) (VNAO mod) (Player & AI) @@ -5510,7 +5510,7 @@ function AIRBOSS:_GetAircraftAoA( playerData ) local goshawk = playerData.actype == AIRBOSS.AircraftCarrier.T45C local skyhawk = playerData.actype == AIRBOSS.AircraftCarrier.A4EC local harrier = playerData.actype == AIRBOSS.AircraftCarrier.AV8B - local tomcat = playerData.actype == AIRBOSS.AircraftCarrier.F14A or playerData.actype == AIRBOSS.AircraftCarrier.F14B or playerData.actype == AIRBOSS.AircraftCarrier.F14A_Early + local tomcat = self:_IsTomcat( playerData.actype ) local corsair = playerData.actype == AIRBOSS.AircraftCarrier.CORSAIR or playerData.actype == AIRBOSS.AircraftCarrier.CORSAIR_CW -- Table with AoA values. @@ -5573,6 +5573,18 @@ function AIRBOSS:_GetAircraftAoA( playerData ) aoa.OnSpeedMin = 9.5 aoa.Fast = 8.0 aoa.FAST = 7.5 + else + -- Unknown carrier-capable type: _IsCarrierAircraft() is enum-driven, so a type can + -- reach here with no AoA branch. Returning an empty table nil-propagates into + -- _GetAircraftParameters() and the AoA waveoff check. Fall back to Hornet values. + self:E( self.lid .. string.format( "ERROR: No AoA parameters defined for aircraft type %s! Falling back to F/A-18C values.", tostring( playerData.actype ) ) ) + aoa.SLOW = 9.8 + aoa.Slow = 9.3 + aoa.OnSpeedMax = 8.8 + aoa.OnSpeed = 8.1 + aoa.OnSpeedMin = 7.4 + aoa.Fast = 6.9 + aoa.FAST = 6.3 end return aoa @@ -5589,7 +5601,7 @@ function AIRBOSS:_AoAUnit2Deg( playerData, aoaunits ) local degrees = aoaunits -- Check aircraft type of player. - if playerData.actype == AIRBOSS.AircraftCarrier.F14A or playerData.actype == AIRBOSS.AircraftCarrier.F14B or playerData.actype == AIRBOSS.AircraftCarrier.F14A_Early then + if self:_IsTomcat( playerData.actype ) then ------------- -- F-14A/B -- @@ -5632,7 +5644,7 @@ function AIRBOSS:_AoADeg2Units( playerData, degrees ) local aoaunits = degrees -- Check aircraft type of player. - if playerData.actype == AIRBOSS.AircraftCarrier.F14A or playerData.actype == AIRBOSS.AircraftCarrier.F14B or playerData.actype == AIRBOSS.AircraftCarrier.F14A_Early then + if self:_IsTomcat( playerData.actype ) then ------------- -- F-14A/B -- @@ -5682,7 +5694,7 @@ function AIRBOSS:_GetAircraftParameters( playerData, step ) or playerData.actype == AIRBOSS.AircraftCarrier.RHINOF or playerData.actype == AIRBOSS.AircraftCarrier.GROWLER local skyhawk = playerData.actype == AIRBOSS.AircraftCarrier.A4EC - local tomcat = playerData.actype == AIRBOSS.AircraftCarrier.F14A or playerData.actype == AIRBOSS.AircraftCarrier.F14B + local tomcat = self:_IsTomcat( playerData.actype ) local harrier = playerData.actype == AIRBOSS.AircraftCarrier.AV8B local goshawk = playerData.actype == AIRBOSS.AircraftCarrier.T45C local corsair = playerData.actype == AIRBOSS.AircraftCarrier.CORSAIR or playerData.actype == AIRBOSS.AircraftCarrier.CORSAIR_CW @@ -6615,9 +6627,7 @@ function AIRBOSS:_RefuelAI( flight ) local refuelac=false local actype=flight.group:GetTypeName() if actype==AIRBOSS.AircraftCarrier.AV8B or - actype==AIRBOSS.AircraftCarrier.F14A or - actype==AIRBOSS.AircraftCarrier.F14B or - actype==AIRBOSS.AircraftCarrier.F14A_AI or + self:_IsTomcat( actype, true ) or actype==AIRBOSS.AircraftCarrier.HORNET or actype==AIRBOSS.AircraftCarrier.RHINOE or actype==AIRBOSS.AircraftCarrier.RHINOF or @@ -6727,7 +6737,7 @@ function AIRBOSS:_LandAI( flight ) Speed = UTILS.KnotsToKmph( 200 ) elseif flight.actype == AIRBOSS.AircraftCarrier.E2D or flight.actype == AIRBOSS.AircraftCarrier.C2A then Speed = UTILS.KnotsToKmph( 150 ) - elseif flight.actype == AIRBOSS.AircraftCarrier.F14A_AI or flight.actype == AIRBOSS.AircraftCarrier.F14A or flight.actype == AIRBOSS.AircraftCarrier.F14B then + elseif self:_IsTomcat( flight.actype, true ) then Speed = UTILS.KnotsToKmph( 175 ) elseif flight.actype == AIRBOSS.AircraftCarrier.S3B or flight.actype == AIRBOSS.AircraftCarrier.S3BTANKER then Speed = UTILS.KnotsToKmph( 140 ) @@ -8202,7 +8212,7 @@ function AIRBOSS:_CheckPlayerStatus() or playerData.actype == AIRBOSS.AircraftCarrier.RHINOE or playerData.actype == AIRBOSS.AircraftCarrier.RHINOF or playerData.actype == AIRBOSS.AircraftCarrier.GROWLER - local tomcat = playerData.actype == AIRBOSS.AircraftCarrier.F14A or playerData.actype == AIRBOSS.AircraftCarrier.F14B + local tomcat = self:_IsTomcat( playerData.actype ) -- VNAO Edit - Added wrapped up call to LSO grading Hornet if playerData.step==AIRBOSS.PatternStep.WAKE and hornet then-- VNAO Edit - Added @@ -9457,7 +9467,7 @@ function AIRBOSS:_Initial( playerData ) -- Hook down for students. if playerData.difficulty == AIRBOSS.Difficulty.EASY and playerData.actype ~= AIRBOSS.AircraftCarrier.AV8B then - if playerData.actype == AIRBOSS.AircraftCarrier.F14A or playerData.actype == AIRBOSS.AircraftCarrier.F14B then + if self:_IsTomcat( playerData.actype ) then hint = hint .. " - Hook down, SAS on, Wing Sweep 68°!" else hint = hint .. " - Hook down!" @@ -9622,8 +9632,7 @@ function AIRBOSS:_DirtyUp( playerData ) -- Radio call "Say/Fly needles". Delayed by 10/15 seconds. if playerData.actype == AIRBOSS.AircraftCarrier.HORNET - or playerData.actype == AIRBOSS.AircraftCarrier.F14A - or playerData.actype == AIRBOSS.AircraftCarrier.F14B + or self:_IsTomcat( playerData.actype ) or playerData.actype == AIRBOSS.AircraftCarrier.RHINOE or playerData.actype == AIRBOSS.AircraftCarrier.RHINOF or playerData.actype == AIRBOSS.AircraftCarrier.GROWLER @@ -10817,7 +10826,7 @@ function AIRBOSS:_Trapped( playerData ) or playerData.actype == AIRBOSS.AircraftCarrier.RHINOF or playerData.actype == AIRBOSS.AircraftCarrier.GROWLER then dcorr = 100 - elseif playerData.actype == AIRBOSS.AircraftCarrier.F14A or playerData.actype == AIRBOSS.AircraftCarrier.F14B then + elseif self:_IsTomcat( playerData.actype ) then -- TODO: Check Tomcat. dcorr = 100 elseif playerData.actype == AIRBOSS.AircraftCarrier.A4EC then @@ -11630,7 +11639,7 @@ function AIRBOSS:_AttitudeMonitor( playerData ) local unitClient = Unit.getByName(unit:GetName()) -- VNAO Edit - Added local hornet = playerData.actype == AIRBOSS.AircraftCarrier.HORNET -- VNAO Edit - Added - local tomcat = playerData.actype == AIRBOSS.AircraftCarrier.F14A or playerData.actype == AIRBOSS.AircraftCarrier.F14B or playerData.actype == AIRBOSS.AircraftCarrier.F14A_Early -- VNAO Edit - Added + local tomcat = self:_IsTomcat( playerData.actype ) -- VNAO Edit - Added if hornet then -- VNAO Edit - Added local nozzlePosL = 0 -- VNAO Edit - Added @@ -11863,7 +11872,7 @@ function AIRBOSS:_NozzleArgumentLeft( unit ) -- VNAO Edit - Added else -- VNAO Edit - Added nozzlePosL = 0 -- VNAO Edit - Added end -- VNAO Edit - Added - elseif typeName == "F-14A-135-GR" or typeName == "F-14B" or typeName == "F-14A-135-GR-Early" then -- VNAO Edit - Added + elseif self:_IsTomcat( typeName ) then -- VNAO Edit - Added nozzlePosL = unitClient:getDrawArgumentValue(434) -- VNAO Edit - Added end -- VNAO Edit - Added @@ -11888,7 +11897,7 @@ function AIRBOSS:_NozzleArgumentRight( unit ) -- VNAO Edit - Added else -- VNAO Edit - Added nozzlePosR = 0 -- VNAO Edit - Added end -- VNAO Edit - Added - elseif typeName == "F-14A-135-GR" or typeName == "F-14B" or typeName == "F-14A-135-GR-Early" then -- VNAO Edit - Added + elseif self:_IsTomcat( typeName ) then -- VNAO Edit - Added nozzlePosR = unitClient:getDrawArgumentValue(433) -- VNAO Edit - Added end -- VNAO Edit - Added return nozzlePosR -- VNAO Edit - Added @@ -13151,7 +13160,7 @@ function AIRBOSS:_Flightdata2Text( playerData, groovestep ) local hornet = playerData.actype == AIRBOSS.AircraftCarrier.HORNET-- VNAO Edit - Added - local tomcat = playerData.actype == AIRBOSS.AircraftCarrier.F14A or playerData.actype == AIRBOSS.AircraftCarrier.F14B-- VNAO Edit - Added + local tomcat = self:_IsTomcat( playerData.actype )-- VNAO Edit - Added if hornet then-- VNAO Edit - Added if Lnoz > 0.6 and Rnoz > 0.6 then -- VNAO Edit - Added check them both, it's possilbe there could be a single engine landing and one is in idle perhaps? @@ -13667,7 +13676,7 @@ function AIRBOSS:_StepHint( playerData, step ) -- Late break. if step == AIRBOSS.PatternStep.LATEBREAK then - if playerData.actype == AIRBOSS.AircraftCarrier.F14A or playerData.actype == AIRBOSS.AircraftCarrier.F14B then + if self:_IsTomcat( playerData.actype ) then hint = hint .. "\nWing Sweep 20°, Gear DOWN < 280 KIAS." end end @@ -13676,7 +13685,7 @@ function AIRBOSS:_StepHint( playerData, step ) if step == AIRBOSS.PatternStep.ABEAM then if playerData.actype == AIRBOSS.AircraftCarrier.AV8B then hint = hint .. "\nNozzles 50°-60°. Antiskid OFF. Lights OFF." - elseif playerData.actype == AIRBOSS.AircraftCarrier.F14A or playerData.actype == AIRBOSS.AircraftCarrier.F14B then + elseif self:_IsTomcat( playerData.actype ) then hint = hint .. "\nSlats/Flaps EXTENDED < 225 KIAS. DLC SELECTED. Auto Throttle IF DESIRED." else hint = hint .. "\nDirty up! Gear DOWN, flaps DOWN. Check hook down." @@ -15111,7 +15120,7 @@ function AIRBOSS:_GetACNickname( actype ) nickname = "Hawkeye" elseif actype == AIRBOSS.AircraftCarrier.C2A then nickname = "Greyhound" - elseif actype == AIRBOSS.AircraftCarrier.F14A_AI or actype == AIRBOSS.AircraftCarrier.F14A or actype == AIRBOSS.AircraftCarrier.F14B or actype == AIRBOSS.AircraftCarrier.F14A_Early then + elseif self:_IsTomcat( actype, true ) then nickname = "Tomcat" elseif actype == AIRBOSS.AircraftCarrier.FA18C or actype == AIRBOSS.AircraftCarrier.HORNET then nickname = "Hornet" @@ -15248,6 +15257,29 @@ function AIRBOSS:_GetGoodBadScore( playerData ) return lowscore, badscore end +--- Check whether an aircraft type name is any F-14 Tomcat variant. +-- Central predicate so that adding a new Tomcat variant only requires editing +-- this function and the AIRBOSS.AircraftCarrier enum. +-- @param #AIRBOSS self +-- @param #string actype Aircraft type name, e.g. from `unit:GetTypeName()` or `playerData.actype`. +-- @param #boolean IncludeAI If true, also match the AI-only F-14A (AIRBOSS.AircraftCarrier.F14A_AI). +-- @return #boolean If true, actype is an F-14 variant. +function AIRBOSS:_IsTomcat( actype, IncludeAI ) + + if actype == AIRBOSS.AircraftCarrier.F14A + or actype == AIRBOSS.AircraftCarrier.F14B + or actype == AIRBOSS.AircraftCarrier.F14BU + or actype == AIRBOSS.AircraftCarrier.F14A_Early then + return true + end + + if IncludeAI and actype == AIRBOSS.AircraftCarrier.F14A_AI then + return true + end + + return false +end + --- Check if aircraft is capable of landing on this aircraft carrier. -- @param #AIRBOSS self -- @param Wrapper.Unit#UNIT unit Aircraft unit. (Will also work with groups as given parameter.) From f66c77b027d66cb87bf66e949098dfb7ab39878b Mon Sep 17 00:00:00 2001 From: Shafik Date: Mon, 27 Jul 2026 10:44:44 +0300 Subject: [PATCH 18/18] [FIXED] `CTLD.useaircraftheadingforbuilds` --- Moose Development/Moose/Ops/CTLD.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Moose Development/Moose/Ops/CTLD.lua b/Moose Development/Moose/Ops/CTLD.lua index 971e35e36..44ab8a1de 100644 --- a/Moose Development/Moose/Ops/CTLD.lua +++ b/Moose Development/Moose/Ops/CTLD.lua @@ -6207,7 +6207,7 @@ function CTLD:_BuildObjectFromCrates(Group,Unit,Build,Repair,RepairLocation,Mult if canmove then --spawn:InitRandomizeUnits(true,20,2) end - if Build.Heading and self.buildcrateswithaircraftheading then + if Build.Heading and self.useaircraftheadingforbuilds then spawn:InitGroupHeading(Build.Heading) end local spawnedGroup = spawn:SpawnFromVec2(randomcoord)