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] 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