--- **Functional** - Modular, Automatic and Network capable Targeting and Interception System for Air Defenses. -- -- === -- -- ## Features: -- -- * Moose derived Modular, Automatic and Network capable Targeting and Interception System. -- * Controls a network of SAM sites. Uses detection to switch on the AA site closest to the enemy. -- * Automatic mode (default since 0.8) can set-up your SAM site network automatically for you. -- * Leverage evasiveness from SEAD, leverage attack range setting. -- -- === -- -- ## Missions: -- -- ### [MANTIS - Modular, Automatic and Network capable Targeting and Interception System](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/master/Functional/Mantis) -- -- === -- -- ### Author : **applevangelist ** -- -- @module Functional.Mantis -- @image Functional.Mantis.jpg -- -- Last Update: January 2026 ------------------------------------------------------------------------- --- **MANTIS** class, extends Core.Base#BASE -- @type MANTIS -- @field #string ClassName -- @field #string name Name of this Mantis -- @field #string SAM_Templates_Prefix Prefix to build the #SET_GROUP for SAM sites -- @field Core.Set#SET_GROUP SAM_Group The SAM #SET_GROUP -- @field #string EWR_Templates_Prefix Prefix to build the #SET_GROUP for EWR group -- @field Core.Set#SET_GROUP EWR_Group The EWR #SET_GROUP -- @field Core.Set#SET_GROUP Adv_EWR_Group The EWR #SET_GROUP used for advanced mode -- @field #string HQ_Template_CC The ME name of the HQ object -- @field Wrapper.Group#GROUP HQ_CC The #GROUP object of the HQ -- @field #table SAM_Table Table of SAM sites -- @field #string lid Prefix for logging -- @field Functional.Detection#DETECTION_AREAS Detection The #DETECTION_AREAS object for EWR -- @field Functional.Detection#DETECTION_AREAS AWACS_Detection The #DETECTION_AREAS object for AWACS -- @field #boolean debug Switch on extra messages -- @field #boolean verbose Switch on extra logging -- @field #number checkradius Radius of the SAM sites -- @field #number grouping Radius to group detected objects -- @field #number acceptrange Radius of the EWR detection -- @field #number detectinterval Interval in seconds for the target detection -- @field #number engagerange Firing engage range of the SAMs, see [https://wiki.hoggitworld.com/view/DCS_option_engagementRange] -- @field #boolean autorelocate Relocate HQ and EWR groups in random intervals. Note: You need to select units for this which are *actually mobile* -- @field #boolean advanced Use advanced mode, will decrease reactivity of MANTIS, if HQ and/or EWR network dies. Set SAMs to RED state if both are dead. Requires usage of an HQ object -- @field #number adv_ratio Percentage to use for advanced mode, defaults to 100% -- @field #number adv_state Advanced mode state tracker -- @field #boolean advAwacs Boolean switch to use Awacs as a separate detection stream -- @field #number awacsrange Detection range of an optional Awacs unit -- @field #boolean UseEmOnOff Decide if we are using Emissions on/off (true) or AlarmState red/green (default) -- @field Functional.Shorad#SHORAD Shorad SHORAD Object, if available -- @field #boolean ShoradLink If true, #MANTIS has #SHORAD enabled -- @field #number ShoradTime Timer in seconds, how long #SHORAD will be active after a detection inside of the defense range -- @field #number ShoradActDistance Distance of an attacker in meters from a Mantis SAM site, on which Shorad will be switched on. Useful to not give away Shorad sites too early. Default 15km. Should be smaller than checkradius. -- @field #boolean checkforfriendlies If true, do not activate a SAM installation if a friendly aircraft is in firing range. -- @field #table FilterZones Table of Core.Zone#ZONE Zones Consider SAM groups in this zone(s) only for this MANTIS instance, must be handed as #table of Zone objects. -- @field #boolean SmokeDecoy If true, smoke short range SAM units as decoy if a plane is in firing range. -- @field #number SmokeDecoyColor Color to use, defaults to SMOKECOLOR.White. -- @field #number checkcounter Counter for SAM Table refreshes. -- @field #number DLinkCacheTime Seconds after which cached contacts in DLink will decay. -- @field #boolean logsamstatus Log SAM status in dcs.log every cycle if true. -- @field #boolean DetectAccoustic Set if we can also detect units accousticly. -- @field #number DetectAccousticRadius We can hear in this range. -- @field #table DetectAccousticCategories We can hear these categories. -- @extends Core.Base#BASE --- *The worst thing that can happen to a good cause is, not to be skillfully attacked, but to be ineptly defended.* - Frédéric Bastiat -- -- Moose class for a more intelligent Air Defense System -- -- # MANTIS -- -- * Moose derived Modular, Automatic and Network capable Targeting and Interception System. -- * Controls a network of SAM sites. Uses detection to switch on the SAM site closest to the enemy. -- * **Automatic mode** (default) will set-up your SAM site network automatically for you. -- * Leverage evasiveness from SEAD, leverage attack range setting. -- * Automatic setup of SHORAD based on groups of the class "short-range". -- -- # 0. Base considerations and naming conventions -- -- **Before** you start to set up your SAM sites in the mission editor, please think of naming conventions. This is especially critical to make -- eveything work as intended, also if you have both a blue and a red installation! -- -- You need three **non-overlapping** "name spaces" for everything to work properly: -- -- * SAM sites, e.g. each **group name** begins with "Red SAM" -- * EWR network and AWACS, e.g. each **group name** begins with "Red EWR" and *not* e.g. "Red SAM EWR" (overlap with "Red SAM"), "Red EWR Awacs" will be found by "Red EWR" -- * SHORAD, e.g. each **group name** begins with "Red SHORAD" and *not" e.g. just "SHORAD" because you might also have "Blue SHORAD" -- * Point Defense, e.g. each **group name** begins with "Red AAA" and *not" e.g. just "AAA" because you might also have "Blue AAA" -- -- It's important to get this right because of the nature of the filter-system in @{Core.Set#SET_GROUP}. Filters are "greedy", that is they -- will match *any* string that contains the search string - hence we need to avoid that SAMs, EWR and SHORAD step on each other\'s toes. -- -- Second, for auto-mode to work, the SAMs need the **SAM Type Name** in their group name, as MANTIS will determine their capabilities from this. -- This is case-sensitive, so "sa-11" is not equal to "SA-11" is not equal to "Sa-11"! -- -- Known SAM types at the time of writing are: -- -- * Avenger -- * Chaparral -- * Hawk -- * Linebacker -- * NASAMS -- * Patriot -- * Rapier -- * Roland -- * IRIS-T SLM -- * Pantsir S1 -- * TOR M2 -- * C-RAM -- * Silkworm (though strictly speaking this is a surface to ship missile) -- * SA-2, SA-3, SA-5, SA-6, SA-7, SA-8, SA-9, SA-10, SA-11, SA-13, SA-15, SA-19, S-300VM, S-300V4, S-400 (SA-21) -- * From IDF mod: STUNNER IDFA, TAMIR IDFA (Note all caps!) -- * From HDS (see note on HDS below): SA-2, SA-3, SA-10B, SA-10C, SA-12, SA-17, SA-20A, SA-20B, SA-23, HQ-2, SAMP/T Block 1, SAMP/T Block 1INT, SAMP/T Block2 -- * Other Mods: Nike -- -- * From SMA: RBS98M, RBS70, RBS90, RBS90M, RBS103A, RBS103B, RBS103AM, RBS103BM, Lvkv9040M -- **NOTE** If you are using the Swedish Military Assets (SMA), please note that the **group name** for RBS-SAM types also needs to contain the keyword "SMA" -- -- * From CH: 2S38, PantsirS1, PantsirS2, PGL-625, HQ-17A, M903PAC2, M903PAC3, TorM2, TorM2K, TorM2M, NASAMS3-AMRAAMER, NASAMS3-AIM9X2, C-RAM, PGZ-09, S350-9M100, S350-9M96D -- **NOTE** If you are using the Military Assets by Currenthill (CH), please note that the **group name** for CH-SAM types also needs to contain the keyword "CHM" -- -- Following the example started above, an SA-6 site group name should start with "Red SAM SA-6" then, or a blue Patriot installation with e.g. "Blue SAM Patriot". -- **NOTE** If you are using the High-Digit-Sam Mod, please note that the **group name** for the following SAM types also needs to contain the keyword "HDS": -- -- * SA-2 (with V759 missile, e.g. "Red SAM SA-2 HDS") -- * SA-2 (with HQ-2 launcher, use HQ-2 in the group name, e.g. "Red SAM HQ-2" ) -- * SA-3 (with V601P missile, e.g. "Red SAM SA-3 HDS") -- * SA-10B (overlap with other SA-10 types, e.g. "Red SAM SA-10B HDS" with 5P85CE launcher) -- * SA-10C (overlap with other SA-10 types, e.g. "Red SAM SA-10C HDS" with 5P85SE launcher) -- * SA-12 (e.g. "Red SAM SA-12 HDS") -- * SA-23 (e.g. "Red SAM SA-23 HDS") -- * SAMP/T (launcher dependent range, e.g. "Blue SAM SAMPT Block 1 HDS" for Block 1, "Blue SAM SAMPT Block 1INT HDS", "Blue SAM SAMPT Block 2 HDS") -- -- The other HDS types work like the rest of the known SAM systems. -- -- # 0.1 Set-up in the mission editor -- -- Set up your SAM sites in the mission editor. Name the groups using a systematic approach like above.Can be e.g. AWACS or a combination of AWACS and Search Radars like e.g. EWR 1L13 etc. -- Search Radars usually have "SR" or "STR" in their names. Use the encyclopedia in the mission editor to inform yourself. -- Set up your SHORAD systems. They need to be **close** to (i.e. around) the SAM sites to be effective. Use **one unit ** per group (multiple groups) for the SAM location. -- Else, evasive manoevers might club up all defenders in one place. Red SA-15 TOR systems offer a good missile defense. -- -- [optional] Set up your HQ. Can be any group, e.g. a command vehicle. -- -- # 1. Basic tactical considerations when setting up your SAM sites -- -- ## 1.1 Radar systems and AWACS -- -- Typically, your setup should consist of EWR (early warning) radars to detect and track targets, accompanied by AWACS if your scenario forsees that. Ensure that your EWR radars have a good coverage of the area you want to track. -- **Location** is of highest importance here. Whilst AWACS in DCS has almost the "all seeing eye", EWR don't have that. Choose your location wisely, against a mountain backdrop or inside a valley even the best EWR system -- doesn't work well. Prefer higher-up locations with a good view; use F7 in-game to check where you actually placed your EWR and have a look around. Apart from the obvious choice, do also consider other radar units -- for this role, most have "SR" (search radar) or "STR" (search and track radar) in their names, use the encyclopedia to see what they actually do. -- **HINT** Set at least one EWR on invisible and immortal so MANTIS doesn't stop working. -- -- ## 1.2 SAM sites -- -- Typically your SAM should cover all attack ranges. The closer the enemy gets, the more systems you will need to deploy to defend your location. Use a combination of long-range systems like the SA-5/10/11, midrange like SA-6 and short-range like -- SA-2 for defense (Patriot, Hawk, Gepard, Blindfire for the blue side). For close-up defense and defense against HARMs or low-flying aircraft, helicopters it is also advisable to deploy SA-15 TOR systems, Shilka, Strela and Tunguska units, as well as manpads (Think Gepard, Avenger, Chaparral, -- Linebacker, Roland systems for the blue side). If possible, overlap ranges for mutual coverage. -- -- ## 1.3 Typical problems -- -- Often times, people complain because the detection cannot "see" oncoming targets and/or Mantis switches on too late. Three typial problems here are -- -- * bad placement of radar units, -- * overestimation how far units can "see" and -- * not taking into account that a SAM site will take (e.g for a SA-6) 30-40 seconds between switching on, acquiring the target and firing. -- -- An attacker doing 350knots will cover ca 180meters/second or thus more than 6km until the SA-6 fires. Use triggers zones and the ruler in the mission editor to understand distances and zones. Take into account that the ranges given by the circles -- in the mission editor are absolute maximum ranges; in-game this is rather 50-75% of that depending on the system. Fiddle with placement and options to see what works best for your scenario, and remember **everything in here is in meters**. -- -- # 2. Start up your MANTIS with a basic setting -- -- myredmantis = MANTIS:New("myredmantis","Red SAM","Red EWR",nil,"red",false) -- myredmantis:Start() -- -- Use -- -- * MANTIS:SetEWRGrouping(radius) [classic mode] -- * MANTIS:SetSAMRadius(radius) [classic mode] -- * MANTIS:SetDetectInterval(interval) [classic & auto modes] -- * MANTIS:SetAutoRelocate(hq, ewr) [classic & auto modes] -- -- before starting #MANTIS to fine-tune your setup. -- -- If you want to use a separate AWACS unit to support your EWR system, use e.g. the following setup: -- -- mybluemantis = MANTIS:New("bluemantis","Blue SAM","Blue EWR",nil,"blue",false,"Blue Awacs") -- mybluemantis:Start() -- -- ## 2.1 Auto mode features -- -- ### 2.1.1 You can add Accept-, Reject- and Conflict-Zones to your setup, e.g. to consider borders or de-militarized zones: -- -- -- Parameters are tables of Core.Zone#ZONE objects! -- -- This is effectively a 3-stage filter allowing for zone overlap. A coordinate is accepted first when -- -- it is inside any AcceptZone. Then RejectZones are checked, which enforces both borders, but also overlaps of -- -- Accept- and RejectZones. Last, if it is inside a conflict zone, it is accepted. -- mybluemantis:AddZones(AcceptZones,RejectZones,ConflictZones) -- -- -- ### 2.1.2 Change the number of long-, mid- and short-range, point defense systems going live on a detected target: -- -- -- parameters are numbers. Defaults are 1,2,2,6,6 respectively -- mybluemantis:SetMaxActiveSAMs(Short,Mid,Long,Classic,Point) -- -- ### 2.1.3 SHORAD/Point defense will automatically be added from SAM sites of type "point" or if the range is less than 5km or if the type is AAA. -- -- ### 2.1.4 Advanced features -- -- -- Option to set the scale of the activation range, i.e. don't activate at the fringes of max range, defaults below. -- -- also see engagerange below. -- self.radiusscale[MANTIS.SamType.LONG] = 1.1 -- self.radiusscale[MANTIS.SamType.MEDIUM] = 1.2 -- self.radiusscale[MANTIS.SamType.SHORT] = 1.3 -- self.radiusscale[MANTIS.SamType.POINT] = 1.4 -- -- ### 2.1.5 Friendlies check in firing range -- -- -- For some scenarios, like Cold War, it might be useful not to activate SAMs if friendly aircraft are around to avoid death by friendly fire. -- mybluemantis.checkforfriendlies = true -- -- ### 2.1.6 Shoot & Scoot -- -- -- Option to make the (driveable) SHORAD units drive around and shuffle positions -- -- We use a SET_ZONE for that, number of zones to consider defaults to three, Random is true for random coordinates and Formation is e.g. "Vee". -- mybluemantis:AddScootZones(ZoneSet, Number, Random, Formation) -- -- # 3. Default settings [both modes unless stated otherwise] -- -- By default, the following settings are active: -- -- * SAM_Templates_Prefix = "Red SAM" - SAM site group names in the mission editor begin with "Red SAM" -- * EWR_Templates_Prefix = "Red EWR" - EWR group names in the mission editor begin with "Red EWR" - can also be combined with an AWACS unit -- * [classic mode] checkradius = 25000 (meters) - SAMs will engage enemy flights, if they are within a 25km around each SAM site - `MANTIS:SetSAMRadius(radius)` -- * grouping = 5000 (meters) - Detection (EWR) will group enemy flights to areas of 5km for tracking - `MANTIS:SetEWRGrouping(radius)` -- * detectinterval = 30 (seconds) - MANTIS will decide every 30 seconds which SAM to activate - `MANTIS:SetDetectInterval(interval)` -- * engagerange = 95 (percent) - SAMs will only fire if flights are inside of a 95% radius of their max firerange - `MANTIS:SetSAMRange(range)` -- * dynamic = false - Group filtering is set to once, i.e. newly added groups will not be part of the setup by default - `MANTIS:New(name,samprefix,ewrprefix,hq,coalition,dynamic)` -- * autorelocate = false - HQ and (mobile) EWR system will not relocate in random intervals between 30mins and 1 hour - `MANTIS:SetAutoRelocate(hq, ewr)` -- * debug = false - Debugging reports on screen are set to off - `MANTIS:Debug(onoff)` -- -- # 4. Advanced Mode -- -- Advanced mode will *decrease* reactivity of MANTIS, if HQ and/or EWR network dies. Awacs is counted as one EWR unit. It will set SAMs to RED state if both are dead. Requires usage of an **HQ** object and the **dynamic** option. -- -- E.g. mymantis:SetAdvancedMode( true, 90 ) -- -- Use this option if you want to make use of or allow advanced SEAD tactics. -- -- # 5. Integrated SEAD -- -- MANTIS is using @{Functional.Sead#SEAD} internally to both detect and evade HARM attacks. No extra efforts needed to set this up! -- Once a HARM attack is detected, MANTIS (via SEAD) will shut down the radars of the attacked SAM site and take evasive action by moving the SAM -- vehicles around (*if they are __drivable__*, that is). There's a component of randomness in detection and evasion, which is based on the -- skill set of the SAM set (the higher the skill, the more likely). When a missile is fired from far away, the SAM will stay active for a -- period of time to stay defensive, before it takes evasive actions. -- -- You can link into the SEAD driven events of MANTIS like so: -- -- function mymantis:OnAfterSeadSuppressionPlanned(From, Event, To, Group, Name, SuppressionStartTime, SuppressionEndTime) -- -- your code here - SAM site shutdown and evasion planned, but not yet executed -- -- Time entries relate to timer.getTime() - see https://wiki.hoggitworld.com/view/DCS_func_getTime -- end -- -- function mymantis:OnAfterSeadSuppressionStart(From, Event, To, Group, Name) -- -- your code here - SAM site is emissions off and possibly moving -- end -- -- function mymantis:OnAfterSeadSuppressionEnd(From, Event, To, Group, Name) -- -- your code here - SAM site is back online -- end -- -- @field #MANTIS MANTIS = { ClassName = "MANTIS", name = "mymantis", version = "0.9.44", SAM_Templates_Prefix = "", SAM_Group = nil, EWR_Templates_Prefix = "", EWR_Group = nil, Adv_EWR_Group = nil, HQ_Template_CC = "", HQ_CC = nil, SAM_Table = {}, SAM_Table_Long = {}, SAM_Table_Medium = {}, SAM_Table_Short = {}, SAM_Table_PointDef = {}, lid = "", Detection = nil, AWACS_Detection = nil, debug = false, checkradius = 25000, grouping = 5000, acceptrange = 80000, detectinterval = 30, engagerange = 95, autorelocate = false, advanced = false, adv_ratio = 100, adv_state = 0, AWACS_Prefix = "", advAwacs = false, verbose = false, awacsrange = 250000, Shorad = nil, ShoradLink = false, ShoradTime = 600, ShoradActDistance = 25000, UseEmOnOff = false, TimeStamp = 0, state2flag = false, SamStateTracker = {}, DLink = false, DLTimeStamp = 0, Padding = 10, SuppressedGroups = {}, automode = true, autoshorad = true, ShoradGroupSet = nil, checkforfriendlies = false, SmokeDecoy = false, SmokeDecoyColor = SMOKECOLOR.White, checkcounter = 1, DLinkCacheTime = 120, logsamstatus = false, DetectAccoustic = false, DetectAccousticRadius = 2000, DetectAccousticCategories = {Unit.Category.HELICOPTER}, ARMWeaponSeen = {}, InboundARMs = {}, } --- Advanced state enumerator -- @type MANTIS.AdvancedState MANTIS.AdvancedState = { GREEN = 0, AMBER = 1, RED = 2, } --- SAM Type -- @type MANTIS.SamType MANTIS.SamType = { SHORT = "Short", MEDIUM = "Medium", LONG = "Long", POINT = "Point", } --- SAM Radiusscale -- @type MANTIS.radiusscale MANTIS.radiusscale = {} MANTIS.radiusscale[MANTIS.SamType.LONG] = 1.1 MANTIS.radiusscale[MANTIS.SamType.MEDIUM] = 1.2 MANTIS.radiusscale[MANTIS.SamType.SHORT] = 1.75 MANTIS.radiusscale[MANTIS.SamType.POINT] = 3 --- SAM data -- @type MANTIS.SamData -- @field #number Range Max firing range in km -- @field #number Blindspot no-firing range (green circle) -- @field #number Height Max firing height in km -- @field #string Type #MANTIS.SamType of SAM, i.e. SHORT, MEDIUM or LONG (range) -- @field #string Radar Radar typename on unit level (used as key) -- @field #string Point Point defense capable -- @field ARMCapacit ARMCapacity ie how many (H)ARMs the system can defend at the same time MANTIS.SamData = { ["Hawk"] = { Range=35, Blindspot=0, Height=12, Type="Medium", Radar="Hawk" }, -- measures in km ["NASAMS"] = { Range=14, Blindspot=0, Height=7, Type="Short", Radar="NSAMS", ARMCapacity=1 }, -- AIM 120B ["Patriot"] = { Range=99, Blindspot=0, Height=25, Type="Long", Radar="Patriot str" }, ["Rapier"] = { Range=10, Blindspot=0, Height=3, Type="Short", Radar="rapier" }, ["SA-2"] = { Range=40, Blindspot=7, Height=25, Type="Medium", Radar="S_75M_Volhov" }, ["SA-3"] = { Range=18, Blindspot=6, Height=18, Type="Short", Radar="5p73 s-125 ln" }, ["SA-5"] = { Range=250, Blindspot=7, Height=40, Type="Long", Radar="5N62V" }, ["SA-6"] = { Range=25, Blindspot=0, Height=8, Type="Medium", Radar="1S91" }, ["SA-10"] = { Range=119, Blindspot=0, Height=18, Type="Long" , Radar="S-300PS 4", ARMCapacity=4}, ["SA-11"] = { Range=35, Blindspot=0, Height=20, Type="Medium", Radar="SA-11" }, ["Roland"] = { Range=6, Blindspot=0, Height=5, Type="Short", Radar="Roland", ARMCapacity=1 }, ["Gepard"] = { Range=5, Blindspot=0, Height=4, Type="Point", Radar="Gepard" }, ["HQ-7"] = { Range=12, Blindspot=0, Height=3, Type="Short", Radar="HQ-7" }, ["SA-9"] = { Range=4, Blindspot=0, Height=3, Type="Point", Radar="Strela", Point="true" }, ["SA-8"] = { Range=10, Blindspot=0, Height=5, Type="Short", Radar="Osa 9A33" }, ["SA-19"] = { Range=8, Blindspot=0, Height=3, Type="Short", Radar="Tunguska" }, ["SA-15"] = { Range=11, Blindspot=0, Height=6, Type="Point", Radar="Tor 9A331", Point="true", ARMCapacity=2 }, ["SA-13"] = { Range=5, Blindspot=0, Height=3, Type="Point", Radar="Strela", Point="true" }, ["Avenger"] = { Range=4, Blindspot=0, Height=3, Type="Short", Radar="Avenger" }, ["Chaparral"] = { Range=8, Blindspot=0, Height=3, Type="Short", Radar="Chaparral" }, ["Linebacker"] = { Range=4, Blindspot=0, Height=3, Type="Point", Radar="Linebacker", Point="true" }, ["Silkworm"] = { Range=90, Blindspot=1, Height=0.2, Type="Long", Radar="Silkworm" }, ["C-RAM"] = { Range=2, Blindspot=0, Height=2, Type="Point", Radar="HEMTT_C-RAM_Phalanx", Point="true" }, -- units from HDS Mod, multi launcher options is tricky ["SA-10B"] = { Range=75, Blindspot=0, Height=18, Type="Medium" , Radar="SA-10B", ARMCapacity=4}, ["SA-17"] = { Range=50, Blindspot=3, Height=50, Type="Medium", Radar="SA-17", ARMCapacity=4 }, ["SA-20A"] = { Range=150, Blindspot=5, Height=27, Type="Long" , Radar="S-300PMU1", ARMCapacity=16}, ["SA-20B"] = { Range=200, Blindspot=4, Height=27, Type="Long" , Radar="S-300PMU2", ARMCapacity=18}, ["S-300VM"] = { Range=200, Blindspot=5, Height=30, Type="Long" , Radar="9S32ME", ARMCapacity=4}, -- SA-23 Gladiator/Giant ["S-300V4"] = { Range=380, Blindspot=5, Height=30, Type="Long" , Radar="9S32M-1E", ARMCapacity=4}, -- SA-23B ["S-400"] = { Range=380, Blindspot=5, Height=30, Type="Long" , Radar="92N6E", ARMCapacity=4}, -- SA-21 Growler ["HQ-2"] = { Range=50, Blindspot=6, Height=35, Type="Medium", Radar="HQ_2_Guideline_LN" }, ["TAMIR IDFA"] = { Range=20, Blindspot=0.6, Height=12.3, Type="Short", Radar="IRON_DOME_LN" }, ["STUNNER IDFA"] = { Range=250, Blindspot=1, Height=45, Type="Long", Radar="DAVID_SLING_LN" }, ["Nike"] = { Range=155, Blindspot=6, Height=30, Type="Long", Radar="HIPAR" }, ["Dog Ear"] = { Range=11, Blindspot=0, Height=9, Type="Point", Radar="Dog Ear", Point="true" }, -- CH Added to DCS core 2.9.19.x ["Pantsir S1"] = { Range=20, Blindspot=1.2, Height=15, Type="Point", Radar="PantsirS1" , Point="true", ARMCapacity=3 }, ["Tor M2"] = { Range=12, Blindspot=1, Height=10, Type="Point", Radar="TorM2", Point="true", ARMCapacity=4 }, ["IRIS-T SLM"] = { Range=40, Blindspot=0.5, Height=20, Type="Medium", Radar="CH_IRIST_SLM", ARMCapacity=12 }, -- 4 per starter, usually 3 starters in a battery ["SON-9"] = { Range=20, Blindspot=0, Height=14, Type="Point", Radar="SON_9", Point="true" }, -- Fire Can FCR for S-60/KS-19 AAA } --- SAM data HDS -- @type MANTIS.SamDataHDS -- @field #number Range Max firing range in km -- @field #number Blindspot no-firing range (green circle) -- @field #number Height Max firing height in km -- @field #string Type #MANTIS.SamType of SAM, i.e. SHORT, MEDIUM or LONG (range) -- @field #string Radar Radar typename on unit level (used as key) -- @field #string Point Point defense capable MANTIS.SamDataHDS = { -- units from HDS Mod, multi launcher options is tricky -- group name MUST contain HDS to ID launcher type correctly! ["SA-2 HDS"] = { Range=56, Blindspot=7, Height=30, Type="Medium", Radar="V759" }, ["SA-3 HDS"] = { Range=20, Blindspot=6, Height=30, Type="Short", Radar="V-601P" }, ["SA-10B HDS"] = { Range=90, Blindspot=5, Height=25, Type="Long" , Radar="5P85CE ln", ARMCapacity=8}, -- V55RUD ["SA-10C HDS"] = { Range=75, Blindspot=5, Height=25, Type="Long" , Radar="5P85SE ln", ARMCapacity=3}, -- V55RUD ["SA-17 HDS"] = { Range=50, Blindspot=3, Height=50, Type="Medium", Radar="SA-17", ARMCapacity=4 }, ["SA-12 HDS"] = { Range=100, Blindspot=6, Height=30, Type="Long" , Radar="S-300V 9A82 l", ARMCapacity=12}, ["SA-23 HDS"] = { Range=200, Blindspot=1, Height=50, Type="Long", Radar="S-300VM 9A82ME", ARMCapacity=14 }, ["HQ-2 HDS"] = { Range=50, Blindspot=6, Height=35, Type="Medium", Radar="HQ_2_Guideline_LN" }, ["SAMPT Block 1 HDS"] = { Range=120, Blindspot=1, Height=20, Type="long", Radar="SAMPT_MLT_Blk1" }, -- Block 1 Launcher ["SAMPT Block 1INT HDS"] = { Range=150, Blindspot=1, Height=25, Type="long", Radar="SAMPT_MLT_Blk1NT" }, -- Block 1-INT Launcher ["SAMPT Block 2 HDS"] = { Range=200, Blindspot=10, Height=70, Type="long", Radar="SAMPT_MLT_Blk2" }, -- Block 2 Launcher } --- SAM data SMA -- @type MANTIS.SamDataSMA -- @field #number Range Max firing range in km -- @field #number Blindspot no-firing range (green circle) -- @field #number Height Max firing height in km -- @field #string Type #MANTIS.SamType of SAM, i.e. SHORT, MEDIUM or LONG (range) -- @field #string Radar Radar typename on unit level (used as key) -- @field #string Point Point defense capable MANTIS.SamDataSMA = { -- units from SMA Mod (Sweedish Military Assets) -- https://forum.dcs.world/topic/295202-swedish-military-assets-for-dcs-by-currenthill/ -- group name MUST contain SMA to ID launcher type correctly! ["RBS98M SMA"] = { Range=20, Blindspot=0.2, Height=8, Type="Short", Radar="RBS-98" }, ["RBS70 SMA"] = { Range=8, Blindspot=0.25, Height=6, Type="Short", Radar="RBS-70" }, ["RBS70M SMA"] = { Range=8, Blindspot=0.25, Height=6, Type="Short", Radar="BV410_RBS70" }, ["RBS90 SMA"] = { Range=8, Blindspot=0.25, Height=6, Type="Short", Radar="RBS-90" }, ["RBS90M SMA"] = { Range=8, Blindspot=0.25, Height=6, Type="Short", Radar="BV410_RBS90" }, ["RBS103A SMA"] = { Range=160, Blindspot=1, Height=36, Type="Long", Radar="LvS-103_Lavett103_Rb103A" }, ["RBS103B SMA"] = { Range=120, Blindspot=3, Height=24.5, Type="Long", Radar="LvS-103_Lavett103_Rb103B" }, ["RBS103AM SMA"] = { Range=160, Blindspot=1, Height=36, Type="Long", Radar="LvS-103_Lavett103_HX_Rb103A" }, ["RBS103BM SMA"] = { Range=120, Blindspot=3, Height=24.5, Type="Long", Radar="LvS-103_Lavett103_HX_Rb103B" }, ["Lvkv9040M SMA"] = { Range=2, Blindspot=0.1, Height=1.2, Type="Point", Radar="LvKv9040",Point="true" }, } --- SAM data CH -- @type MANTIS.SamDataCH -- @field #number Range Max firing range in km -- @field #number Blindspot no-firing range (green circle) -- @field #number Height Max firing height in km -- @field #string Type #MANTIS.SamType of SAM, i.e. SHORT, MEDIUM or LONG (range) -- @field #string Radar Radar typename on unit level (used as key) -- @field #string Point Point defense capable MANTIS.SamDataCH = { -- units from CH (Military Assets by Currenthill) -- https://www.currenthill.com/ -- group name MUST contain CHM to ID launcher type correctly! ["2S38 CHM"] = { Range=6, Blindspot=0.1, Height=4.5, Type="Short", Radar="2S38" }, ["PantsirS1 CHM"] = { Range=20, Blindspot=1.2, Height=15, Type="Point", Radar="PantsirS1", Point="true", ARMCapacity=3 }, ["PantsirS2 CHM"] = { Range=30, Blindspot=1.2, Height=18, Type="Medium", Radar="PantsirS2", ARMCapacity=4 }, ["PGL-625 CHM"] = { Range=10, Blindspot=1, Height=5, Type="Short", Radar="PGL_625" }, ["HQ-17A CHM"] = { Range=15, Blindspot=1.5, Height=10, Type="Short", Radar="HQ17A" }, ["M903PAC2 CHM"] = { Range=120, Blindspot=3, Height=24.5, Type="Long", Radar="MIM104_M903_PAC2" }, ["M903PAC3 CHM"] = { Range=160, Blindspot=1, Height=40, Type="Long", Radar="MIM104_M903_PAC3" }, ["TorM2 CHM"] = { Range=12, Blindspot=1, Height=10, Type="Point", Radar="TorM2", Point="true", ARMCapacity=3 }, ["TorM2K CHM"] = { Range=12, Blindspot=1, Height=10, Type="Point", Radar="TorM2K", Point="true", ARMCapacity=4 }, ["TorM2M CHM"] = { Range=16, Blindspot=1, Height=10, Type="Point", Radar="TorM2M", Point="true", ARMCapacity=4 }, ["NASAMS3-AMRAAMER CHM"] = { Range=50, Blindspot=2, Height=35.7, Type="Medium", Radar="CH_NASAMS3_LN_AMRAAM_ER" }, ["NASAMS3-AIM9X2 CHM"] = { Range=20, Blindspot=0.2, Height=18, Type="Short", Radar="CH_NASAMS3_LN_AIM9X2" }, ["C-RAM CHM"] = { Range=2, Blindspot=0, Height=2, Type="Point", Radar="CH_Centurion_C_RAM", Point="true" }, ["PGZ-09 CHM"] = { Range=4, Blindspot=0.5, Height=3, Type="Point", Radar="CH_PGZ09", Point="true" }, ["S350-9M100 CHM"] = { Range=15, Blindspot=1, Height=8, Type="Short", Radar="CH_S350_50P6_9M100", ARMCapacity=20 }, ["S350-9M96D CHM"] = { Range=150, Blindspot=2.5, Height=30, Type="Long", Radar="CH_S350_50P6_9M96D", ARMCapacity=20 }, ["LAV-AD CHM"] = { Range=8, Blindspot=0.16, Height=4.8, Type="Short", Radar="CH_LAVAD" }, ["HQ-22 CHM"] = { Range=170, Blindspot=5, Height=27, Type="Long", Radar="CH_HQ22_LN" }, ["PGZ-95 CHM"] = { Range=2.5, Blindspot=0.5, Height=2, Type="Point", Radar="CH_PGZ95",Point="true" }, ["LD-3000 CHM"] = { Range=2.5, Blindspot=0.1, Height=3, Type="Point", Radar="CH_LD3000_stationary", Point="true" }, ["LD-3000M CHM"] = { Range=2.5, Blindspot=0.1, Height=3, Type="Point", Radar="CH_LD3000", Point="true" }, ["FlaRakRad CHM"] = { Range=8, Blindspot=1.5, Height=6, Type="Short", Radar="CH_FlaRakRad" }, ["IRIS-T SLM CHM"] = { Range=40, Blindspot=0.5, Height=20, Type="Medium", Radar="CH_IRIST_SLM" }, ["M903PAC2KAT1 CHM"] = { Range=120, Blindspot=3, Height=24.5, Type="Long", Radar="CH_MIM104_M903_PAC2_KAT1" }, ["Skynex CHM"] = { Range=3.5, Blindspot=0.1, Height=3.5, Type="Point", Radar="CH_SkynexHX", Point="true" }, ["Skyshield CHM"] = { Range=3.5, Blindspot=0.1, Height=3.5, Type="Point", Radar="CH_Skyshield_Gun", Point="true" }, ["WieselOzelot CHM"] = { Range=8, Blindspot=0.16, Height=4.8, Type="Short", Radar="CH_Wiesel2Ozelot" }, ["BukM3-9M317M CHM"] = { Range=70, Blindspot=0.25, Height=35, Type="Medium", Radar="CH_BukM3_9A317M", ARMCapacity=20 }, ["BukM3-9M317MA CHM"] = { Range=70, Blindspot=0.25, Height=35, Type="Medium", Radar="CH_BukM3_9A317MA", ARMCapacity=20 }, ["SkySabre CHM"] = { Range=30, Blindspot=0.5, Height=10, Type="Medium", Radar="CH_SkySabreLN" }, ["Stormer CHM"] = { Range=7.5, Blindspot=0.3, Height=7, Type="Short", Radar="CH_StormerHVM" }, ["THAAD CHM"] = { Range=200, Blindspot=40, Height=150, Type="Long", Radar="CH_THAAD_M1120" }, ["USInfantryFIM92K CHM"] = { Range=8, Blindspot=0.16, Height=4.8, Type="Short", Radar="CH_USInfantry_FIM92" }, ["RBS98M CHM"] = { Range=20, Blindspot=0.2, Height=8, Type="Short", Radar="RBS-98" }, ["RBS70 CHM"] = { Range=8, Blindspot=0.25, Height=6, Type="Short", Radar="RBS-70" }, ["RBS70M CHM"] = { Range=8, Blindspot=0.25, Height=6, Type="Short", Radar="BV410_RBS70" }, ["RBS90 CHM"] = { Range=8, Blindspot=0.25, Height=6, Type="Short", Radar="RBS-90" }, ["RBS90M CHM"] = { Range=8, Blindspot=0.25, Height=6, Type="Short", Radar="BV410_RBS90" }, ["RBS103A CHM"] = { Range=160, Blindspot=1, Height=36, Type="Long", Radar="LvS-103_Lavett103_Rb103A" }, ["RBS103B CHM"] = { Range=120, Blindspot=3, Height=24.5, Type="Long", Radar="LvS-103_Lavett103_Rb103B" }, ["RBS103AM CHM"] = { Range=160, Blindspot=1, Height=36, Type="Long", Radar="LvS-103_Lavett103_HX_Rb103A" }, ["RBS103BM CHM"] = { Range=120, Blindspot=3, Height=24.5, Type="Long", Radar="LvS-103_Lavett103_HX_Rb103B" }, ["Lvkv9040M CHM"] = { Range=2, Blindspot=0.1, Height=1.2, Type="Point", Radar="LvKv9040",Point="true" }, } ----------------------------------------------------------------------- -- MANTIS Jammer Extension v2.0.0 -- Standoff Jamming (SOJ) aircraft support for MANTIS IADS networks -- Physics-based jamming: asymmetric Gaussian rise + exponential decay -- Compatible with v7 jammer curves ----------------------------------------------------------------------- --- Jammer Loadout Configurations (v8: Max 3 Pods) -- @type MANTIS.JammerLoadouts MANTIS.JammerLoadouts = { ["1xALQ99"]={name="1x AN/ALQ-99",description="Baseline single pod. Full 64MHz-20GHz spectrum.",mult_LOW=1.12,mult_S=1.10,mult_IJ=1.12,mult_OPT=1.00,bt_mod=1.00,range_mod=1.00,tier="ALQ99"}, ["2xALQ99"]={name="2x AN/ALQ-99",description="Two pods. Improved ERP with log stacking.",mult_LOW=1.40,mult_S=1.36,mult_IJ=1.40,mult_OPT=1.02,bt_mod=1.00,range_mod=1.03,tier="ALQ99"}, ["3xALQ99"]={name="3x AN/ALQ-99 (Maximum Legacy)",description="Max legacy barrage. Highest broadband ERP.",mult_LOW=1.58,mult_S=1.52,mult_IJ=1.58,mult_OPT=1.04,bt_mod=1.00,range_mod=1.06,tier="ALQ99"}, ["1xALQ249"]={name="1x AN/ALQ-249 (AESA)",description="AESA 2-18GHz. High peak+wider window. Blind <2GHz.",mult_LOW=0.15,mult_S=1.82,mult_IJ=1.70,mult_OPT=1.00,bt_mod=1.28,range_mod=1.16,tier="ALQ249"}, ["2xALQ249"]={name="2x AN/ALQ-249",description="Two AESA pods. Strong S/IJ dominance.",mult_LOW=0.15,mult_S=2.30,mult_IJ=2.15,mult_OPT=1.00,bt_mod=1.38,range_mod=1.26,tier="ALQ249"}, ["3xALQ249"]={name="3x AN/ALQ-249 (Maximum AESA)",description="Max AESA ERP. Extreme S/IJ. No LOW coverage.",mult_LOW=0.15,mult_S=2.75,mult_IJ=2.58,mult_OPT=1.00,bt_mod=1.48,range_mod=1.35,tier="ALQ249"}, ["1xALQ99_1xALQ249"]={name="1x ALQ-99 + 1x ALQ-249",description="Balanced coverage. AESA S/IJ + ALQ-99 LOW-band.",mult_LOW=1.14,mult_S=1.90,mult_IJ=1.80,mult_OPT=1.01,bt_mod=1.22,range_mod=1.12,tier="Mixed"}, ["1xALQ99_2xALQ249"]={name="1x ALQ-99 + 2x ALQ-249 [Recommended]",description="Best all-around. High AESA ERP + LOW coverage.",mult_LOW=1.10,mult_S=2.38,mult_IJ=2.22,mult_OPT=1.01,bt_mod=1.35,range_mod=1.24,tier="Mixed"}, ["2xALQ99_1xALQ249"]={name="2x ALQ-99 + 1x ALQ-249",description="Strong LOW-band + AESA S/IJ boost.",mult_LOW=1.42,mult_S=2.10,mult_IJ=2.04,mult_OPT=1.03,bt_mod=1.25,range_mod=1.15,tier="Mixed"}, } --- Tiered loadout keys for F10 menu (v8: 3/3/3) -- @type MANTIS.JammerLoadoutTiers MANTIS.JammerLoadoutTiers = { ALQ99 = {"1xALQ99","2xALQ99","3xALQ99"}, ALQ249 = {"1xALQ249","2xALQ249","3xALQ249"}, Mixed = {"1xALQ99_1xALQ249","1xALQ99_2xALQ249","2xALQ99_1xALQ249"}, } --- Jitter configuration: ±10% per evaluation (EW Fundamentals Ch.9) -- @type MANTIS.JammerJitterPercent MANTIS.JammerJitterPercent = 0.10 --- Jammer SAM Parameters — v9 curves (v8.1) {peak%, mu_nm, sigma_L, tail_dist, band, floor%} -- v9 changes from v8: -- * Width rework: curves now track radar detection/track envelope (not just missile range) -- * Long-range systems widened (SA-5, SA-21, SA-23, Nike, PAC-2, S-300/400 family) -- * Short-range systems tightened (SA-19, SON-9, HQ-7, Rapier) -- * HAWK widened to match CW illuminator envelope (~90nm) -- floor: residual effectiveness inside burnthrough (now tapers past mu with R^2 falloff) -- 5 = legacy radar, no ECCM | 3 = moderate ECCM | 2 = advanced ECCM/AESA | 0 = optical/IR -- @type MANTIS.JammerSAMParams MANTIS.JammerSAMParams = { -- Standard SamData (canonical entries — aliases below point to these) ["Nike"] ={peak=78,mu=55,sigma_L=22,tail_dist=110,band="S", floor=5}, ["Hawk"] ={peak=30,mu=22,sigma_L=9, tail_dist=55, band="IJ", floor=3}, ["SA-2"] ={peak=75,mu=40,sigma_L=16,tail_dist=90, band="S", floor=5}, ["SA-3"] ={peak=45,mu=22,sigma_L=10,tail_dist=52, band="IJ", floor=5}, ["SA-5"] ={peak=52,mu=75,sigma_L=26,tail_dist=145,band="S", floor=5}, ["SA-6"] ={peak=33,mu=18,sigma_L=8, tail_dist=48, band="IJ", floor=3}, ["SA-8"] ={peak=38,mu=10,sigma_L=4, tail_dist=24, band="IJ", floor=3}, ["SA-9"] ={peak=3, mu=3, sigma_L=1, tail_dist=5, band="OPT",floor=0}, ["SA-10"] ={peak=32,mu=50,sigma_L=20,tail_dist=95, band="S", floor=3}, ["SA-10B"] ={peak=30,mu=52,sigma_L=20,tail_dist=100,band="S", floor=3}, ["SA-11"] ={peak=52,mu=28,sigma_L=12,tail_dist=58, band="IJ", floor=3}, ["SA-13"] ={peak=3, mu=3, sigma_L=1, tail_dist=5, band="OPT",floor=0}, ["SA-15"] ={peak=30,mu=11,sigma_L=5, tail_dist=22, band="IJ", floor=3}, ["SA-17"] ={peak=24,mu=32,sigma_L=14,tail_dist=68, band="IJ", floor=2}, ["SA-19"] ={peak=25,mu=8, sigma_L=3.5,tail_dist=16,band="IJ", floor=2}, ["SA-20A"] ={peak=22,mu=65,sigma_L=24,tail_dist=115,band="S", floor=2}, ["SA-20B"] ={peak=20,mu=68,sigma_L=24,tail_dist=120,band="S", floor=2}, ["S-300VM"] ={peak=16,mu=80,sigma_L=30,tail_dist=135,band="S", floor=2}, -- SA-23 canonical ["S-300V4"] ={peak=14,mu=85,sigma_L=30,tail_dist=145,band="S", floor=2}, -- SA-23B canonical ["S-400"] ={peak=18,mu=80,sigma_L=30,tail_dist=150,band="S", floor=2}, -- SA-21 canonical ["NASAMS"] ={peak=25,mu=28,sigma_L=12,tail_dist=52, band="IJ", floor=2}, ["Patriot"] ={peak=32,mu=62,sigma_L=24,tail_dist=110,band="S", floor=3}, ["Rapier"] ={peak=12,mu=6, sigma_L=2.5,tail_dist=14,band="IJ", floor=0}, ["Gepard"] ={peak=18,mu=6, sigma_L=2, tail_dist=18, band="IJ", floor=0}, ["Roland"] ={peak=35,mu=5, sigma_L=2, tail_dist=12, band="IJ", floor=3}, ["HQ-7"] ={peak=38,mu=8, sigma_L=3.5,tail_dist=18,band="IJ", floor=3}, ["HQ-2"] ={peak=70,mu=38,sigma_L=15,tail_dist=80, band="S", floor=5}, ["C-RAM"] ={peak=3, mu=3, sigma_L=1, tail_dist=5, band="OPT",floor=0}, ["Avenger"] ={peak=3, mu=3, sigma_L=1, tail_dist=5, band="OPT",floor=0}, ["Chaparral"] ={peak=3, mu=3, sigma_L=1, tail_dist=5, band="OPT",floor=0}, ["Linebacker"] ={peak=3, mu=3, sigma_L=1, tail_dist=5, band="OPT",floor=0}, ["Silkworm"] ={peak=35,mu=20,sigma_L=8, tail_dist=40, band="IJ", floor=3}, ["Dog Ear"] ={peak=40,mu=10,sigma_L=4, tail_dist=20, band="IJ", floor=5}, ["Pantsir S1"] ={peak=10,mu=9, sigma_L=4, tail_dist=20, band="IJ", floor=0}, -- SA-22 canonical ["Tor M2"] ={peak=28,mu=14,sigma_L=6, tail_dist=30, band="IJ", floor=3}, ["IRIS-T SLM"] ={peak=18,mu=22,sigma_L=10,tail_dist=50, band="IJ", floor=2}, ["SON-9"] ={peak=48,mu=14,sigma_L=6, tail_dist=30, band="IJ", floor=5}, -- IDF Mod ["TAMIR IDFA"] ={peak=19,mu=25,sigma_L=12,tail_dist=52,band="S", floor=2}, ["STUNNER IDFA"]={peak=16,mu=45,sigma_L=18,tail_dist=80,band="S", floor=2}, -- HDS Mod (canonical for keys with no base equivalent) ["SA-10C HDS"] ={peak=30,mu=50,sigma_L=20,tail_dist=92, band="S", floor=3}, ["SA-12 HDS"] ={peak=35,mu=50,sigma_L=18,tail_dist=90, band="S", floor=3}, ["SAMPT Block 1 HDS"] ={peak=28,mu=45,sigma_L=18,tail_dist=85, band="S", floor=3}, ["SAMPT Block 1INT HDS"]={peak=26,mu=48,sigma_L=18,tail_dist=88,band="S", floor=3}, ["SAMPT Block 2 HDS"] ={peak=22,mu=52,sigma_L=20,tail_dist=92, band="S", floor=2}, -- SMA Mod (canonical for SMA-only keys) ["RBS70 SMA"] ={peak=5, mu=4, sigma_L=2, tail_dist=10,band="OPT",floor=0}, -- canonical for RBS70 family ["RBS90 SMA"] ={peak=5, mu=4, sigma_L=2, tail_dist=10,band="OPT",floor=0}, -- canonical for RBS90 family ["RBS98M SMA"] ={peak=25,mu=12,sigma_L=5, tail_dist=25,band="IJ", floor=3}, -- canonical ["RBS103A SMA"] ={peak=20,mu=55,sigma_L=20,tail_dist=90,band="S", floor=2}, -- canonical ["RBS103B SMA"] ={peak=22,mu=45,sigma_L=18,tail_dist=80,band="S", floor=2}, -- canonical ["Lvkv9040M SMA"]={peak=15,mu=3, sigma_L=1, tail_dist=8, band="OPT",floor=0}, -- canonical -- CH Mod (canonical for CHM-only keys) ["2S38 CHM"] ={peak=8, mu=3, sigma_L=1, tail_dist=8, band="OPT",floor=0}, ["PGL-625 CHM"] ={peak=12,mu=6, sigma_L=2.5,tail_dist=14,band="OPT",floor=0}, -- Type-08 ["HQ-17A CHM"] ={peak=30,mu=11,sigma_L=5, tail_dist=24, band="IJ", floor=3}, ["M903PAC3 CHM"] ={peak=15,mu=58,sigma_L=22,tail_dist=105,band="S", floor=2}, ["TorM2M CHM"] ={peak=26,mu=16,sigma_L=6, tail_dist=32, band="IJ", floor=3}, -- variant ["NASAMS3-AMRAAMER CHM"] ={peak=20,mu=35,sigma_L=14,tail_dist=65, band="IJ", floor=2}, ["NASAMS3-AIM9X2 CHM"] ={peak=10,mu=15,sigma_L=6, tail_dist=30, band="IJ", floor=0}, ["PGZ-09 CHM"] ={peak=22,mu=5, sigma_L=2, tail_dist=14, band="IJ", floor=0}, -- Type-09 ["PGZ-95 CHM"] ={peak=15,mu=4, sigma_L=2, tail_dist=10, band="OPT",floor=0}, ["S350-9M100 CHM"] ={peak=18,mu=35,sigma_L=15,tail_dist=78, band="IJ", floor=2}, -- SA-28 canonical ["HQ-22 CHM"] ={peak=20,mu=70,sigma_L=26,tail_dist=120,band="S", floor=2}, ["IRIS-T SLM CHM"] ={peak=18,mu=22,sigma_L=10,tail_dist=50, band="IJ", floor=2}, -- will be aliased ["Skynex CHM"] ={peak=12,mu=4, sigma_L=2, tail_dist=10, band="OPT",floor=0}, ["Skyshield CHM"] ={peak=12,mu=4, sigma_L=2, tail_dist=10, band="OPT",floor=0}, ["BukM3-9M317M CHM"] ={peak=22,mu=38,sigma_L=16,tail_dist=75, band="IJ", floor=2}, -- SA-27 canonical ["SkySabre CHM"] ={peak=18,mu=18,sigma_L=7, tail_dist=40, band="IJ", floor=2}, ["Stormer CHM"] ={peak=10,mu=5, sigma_L=2, tail_dist=12, band="OPT",floor=0}, ["THAAD CHM"] ={peak=10,mu=90,sigma_L=38,tail_dist=140,band="IJ", floor=0}, ["WieselOzelot CHM"] ={peak=3, mu=3, sigma_L=1, tail_dist=5, band="OPT",floor=0}, ["USInfantryFIM92K CHM"] ={peak=3, mu=3, sigma_L=1, tail_dist=5, band="OPT",floor=0}, } -- Alias entries: point alias keys to the canonical entry's table. -- Modifying any canonical entry above automatically updates all its aliases. -- This prevents parameter drift when updating values. do local p = MANTIS.JammerSAMParams -- NATO designation aliases p["SA-21"] = p["S-400"] -- SA-21 Growler p["SA-22"] = p["Pantsir S1"] -- SA-22 Greyhound p["SA-23"] = p["S-300VM"] -- SA-23 Gladiator p["SA-23B"] = p["S-300V4"] -- SA-23B Antey-2500 (improved) p["SA-27"] = p["BukM3-9M317M CHM"] -- SA-27 Gollum (Buk-M3) p["SA-28"] = p["S350-9M100 CHM"] -- SA-28 Vityaz (S-350) -- Buk-M3 variants (same radar) p["BukM3-9M317MA CHM"] = p["BukM3-9M317M CHM"] -- S-350 variants (same radar) p["S350-9M96D CHM"] = p["S350-9M100 CHM"] -- Pantsir variants p["PantsirS1 CHM"] = p["Pantsir S1"] p["PantsirS2 CHM"] = p["Pantsir S1"] -- Tor-M2 variants p["TorM2 CHM"] = p["Tor M2"] p["TorM2K CHM"] = p["Tor M2"] -- Patriot PAC-2 (same MPQ-65 radar) p["M903PAC2 CHM"] = p["Patriot"] p["M903PAC2KAT1 CHM"] = p["Patriot"] -- IRIS-T (base and CHM are same system) p["IRIS-T SLM CHM"] = p["IRIS-T SLM"] -- HDS variants (same radar as base entry) p["SA-2 HDS"] = p["SA-2"] p["SA-3 HDS"] = p["SA-3"] p["SA-10B HDS"] = p["SA-10B"] p["SA-17 HDS"] = p["SA-17"] p["SA-23 HDS"] = p["S-300VM"] p["HQ-2 HDS"] = p["HQ-2"] -- Roland / FlaRakRad (same system) p["FlaRakRad CHM"] = p["Roland"] -- Phalanx-class CIWS (Ka-band, essentially unjammable) p["C-RAM CHM"] = p["C-RAM"] p["LD-3000 CHM"] = p["C-RAM"] p["LD-3000M CHM"] = p["C-RAM"] -- SMA / CHM Swedish duplicates (CH mod re-implements SMA assets) p["RBS70 CHM"] = p["RBS70 SMA"] p["RBS70M SMA"] = p["RBS70 SMA"] p["RBS70M CHM"] = p["RBS70 SMA"] p["RBS90 CHM"] = p["RBS90 SMA"] p["RBS90M SMA"] = p["RBS90 SMA"] p["RBS90M CHM"] = p["RBS90 SMA"] p["RBS98M CHM"] = p["RBS98M SMA"] p["RBS103A CHM"] = p["RBS103A SMA"] p["RBS103AM SMA"] = p["RBS103A SMA"] p["RBS103AM CHM"] = p["RBS103A SMA"] p["RBS103B CHM"] = p["RBS103B SMA"] p["RBS103BM SMA"] = p["RBS103B SMA"] p["RBS103BM CHM"] = p["RBS103B SMA"] p["Lvkv9040M CHM"]= p["Lvkv9040M SMA"] -- IR MANPADS (all near-zero, different systems but share params) p["LAV-AD CHM"] = p["SA-9"] -- NOTE: SA-13, Avenger, Chaparral, Linebacker kept separate (different real systems) end ----------------------------------------------------------------------- -- MANTIS System ----------------------------------------------------------------------- do --- Function to instantiate a new object of class MANTIS --@param #MANTIS self --@param #string name Name of this MANTIS for reporting --@param #string samprefix Prefixes for the SAM groups from the ME, e.g. all groups starting with "Red Sam..." --@param #string ewrprefix Prefixes for the EWR groups from the ME, e.g. all groups starting with "Red EWR..." --@param #string hq Group name of your HQ (optional) --@param #string Coalition Coalition side of your setup, e.g. "blue", "red" or "neutral" --@param #boolean dynamic Use constant (true) filtering or just filter once (false, default) (optional) --@param #string awacs Group name of your Awacs (optional) --@param #boolean EmOnOff Make MANTIS switch Emissions on and off instead of changing the alarm state between RED and GREEN (optional) --@param #number Padding For #SEAD - Extra number of seconds to add to radar switch-back-on time (optional) --@param #table Zones Table of Core.Zone#ZONE Zones Consider SAM groups in this zone(s) only for this MANTIS instance, must be handed as #table of Zone objects --@return #MANTIS self --@usage Start up your MANTIS with a basic setting -- -- myredmantis = MANTIS:New("myredmantis","Red SAM","Red EWR",nil,"red",false) -- myredmantis:Start() -- -- [optional] Use -- -- myredmantis:SetDetectInterval(interval) -- myredmantis:SetAutoRelocate(hq, ewr) -- -- before starting #MANTIS to fine-tune your setup. -- -- If you want to use a separate AWACS unit (default detection range: 250km) to support your EWR system, use e.g. the following setup: -- -- mybluemantis = MANTIS:New("bluemantis","Blue SAM","Blue EWR",nil,"blue",false,"Blue Awacs") -- mybluemantis:Start() -- function MANTIS:New(name,samprefix,ewrprefix,hq,Coalition,dynamic,awacs, EmOnOff, Padding, Zones) -- Inherit everything from BASE class. local self = BASE:Inherit(self, FSM:New()) -- #MANTIS -- DONE: Create some user functions for these -- DONE: Make HQ useful -- DONE: Set SAMs to auto if EWR dies -- DONE: Refresh SAM table in dynamic mode -- DONE: Treat Awacs separately, since they might be >80km off site -- DONE: Allow tables of prefixes for the setup -- DONE: Auto-Mode with range setups for various known SAM types. -- DONE: Added reaction on HIT and UNIT LOST events. self.name = name or "mymantis" self.SAM_Templates_Prefix = samprefix or "Red SAM" self.EWR_Templates_Prefix = ewrprefix or "Red EWR" self.HQ_Template_CC = hq or nil self.Coalition = Coalition or "red" self.coalition = Coalition == "blue" and coalition.side.BLUE or coalition.side.RED self.SAM_Table = {} self.SAM_Table_Long = {} self.SAM_Table_Medium = {} self.SAM_Table_Short = {} self.SAM_Table_PointDef = {} self.dynamic = dynamic or false self.checkradius = 25000 self.grouping = 5000 self.acceptrange = 80000 self.detectinterval = 30 self.engagerange = 95 self.autorelocate = false self.autorelocateunits = { HQ = false, EWR = false} self.advanced = false self.adv_ratio = 100 self.adv_state = 0 self.verbose = false self.Adv_EWR_Group = nil self.AWACS_Prefix = awacs or nil self.awacsrange = 250000 --DONE: 250km, User Function to change self.Shorad = nil self.ShoradLink = false self.ShoradTime = 600 self.ShoradActDistance = 25000 self.TimeStamp = timer.getAbsTime() self.relointerval = math.random(1800,3600) -- random between 30 and 60 mins self.state2flag = false self.SamStateTracker = {} -- table to hold alert states, so we don't trigger state changes twice in adv mode self.DLink = false self.Padding = Padding or 10 self.SuppressedGroups = {} -- 0.8 additions self.automode = true --self.SAMCheckRanges = {} self.usezones = false self.AcceptZones = {} self.RejectZones = {} self.ConflictZones = {} self.maxlongrange = 1 self.maxmidrange = 2 self.maxshortrange = 2 self.maxpointdefrange = 6 self.maxclassic = 6 self.autoshorad = true self.ShoradGroupSet = SET_GROUP:New() -- Core.Set#SET_GROUP self.FilterZones = Zones self.LastThreatEval = {} self.InboundARMs = {} self.SkateZones = nil self.SkateNumber = 3 self.shootandscoot = false self.SmokeDecoy = false self.SmokeDecoyColor = SMOKECOLOR.White self.UseEmOnOff = true if EmOnOff == false then self.UseEmOnOff = false end if type(awacs) == "string" then self.advAwacs = true else self.advAwacs = false end self:SetDLinkCacheTime() -- Set the string id for output to DCS.log file. self.lid=string.format("MANTIS %s | ", self.name) -- Debug trace. if self.debug then BASE:TraceOnOff(true) BASE:TraceClass(self.ClassName) --BASE:TraceClass("SEAD") BASE:TraceLevel(1) end self.ewr_templates = {} if type(samprefix) ~= "table" then self.SAM_Templates_Prefix = {samprefix} end if type(ewrprefix) ~= "table" then self.EWR_Templates_Prefix = {ewrprefix} end for _,_group in pairs (self.SAM_Templates_Prefix) do table.insert(self.ewr_templates,_group) end for _,_group in pairs (self.EWR_Templates_Prefix) do table.insert(self.ewr_templates,_group) end if self.advAwacs then table.insert(self.ewr_templates,awacs) end self.logsamstatus = false self:T({self.ewr_templates}) self.SAM_Group = SET_GROUP:New():FilterPrefixes(self.SAM_Templates_Prefix):FilterCoalitions(self.Coalition) self.EWR_Group = SET_GROUP:New():FilterPrefixes(self.ewr_templates):FilterCoalitions(self.Coalition) if self.FilterZones then self.SAM_Group:FilterZones(self.FilterZones) end if self.dynamic then -- Set SAM SET_GROUP self.SAM_Group:FilterStart() -- Set EWR SET_GROUP self.EWR_Group:FilterStart() else -- Set SAM SET_GROUP self.SAM_Group:FilterOnce() -- Set EWR SET_GROUP self.EWR_Group:FilterOnce() end -- set up CC if self.HQ_Template_CC then self.HQ_CC = GROUP:FindByName(self.HQ_Template_CC) end -- counter for SAM table updates self.checkcounter = 1 self:I(string.format("***** Starting MANTIS Version %s *****", self.version)) --- FSM Functions --- -- Start State. self:SetStartState("Stopped") -- Add FSM transitions. -- From State --> Event --> To State self:AddTransition("Stopped", "Start", "Running") -- Start FSM. self:AddTransition("*", "Status", "*") -- MANTIS status update. self:AddTransition("*", "Relocating", "*") -- MANTIS HQ and EWR are relocating. self:AddTransition("*", "GreenState", "*") -- MANTIS A SAM switching to GREEN state. self:AddTransition("*", "RedState", "*") -- MANTIS A SAM switching to RED state. self:AddTransition("*", "AdvStateChange", "*") -- MANTIS advanced mode state change. self:AddTransition("*", "ShoradActivated", "*") -- MANTIS woke up a connected SHORAD. self:AddTransition("*", "SeadSuppressionStart", "*") -- SEAD has switched off one group. self:AddTransition("*", "SeadSuppressionEnd", "*") -- SEAD has switched on one group. self:AddTransition("*", "SeadSuppressionPlanned", "*") -- SEAD has planned a suppression. self:AddTransition("*", "SAMUnitHit", "*") -- A SAM unit was hit self:AddTransition("*", "SAMUnitLost", "*") -- A SAM Unit was lost self:AddTransition("*", "Stop", "Stopped") -- Stop FSM. ------------------------ --- Pseudo Functions --- ------------------------ --- Triggers the FSM event "Start". Starts the MANTIS. Initializes parameters and starts event handlers. -- @function [parent=#MANTIS] Start -- @param #MANTIS self --- Triggers the FSM event "Start" after a delay. Starts the MANTIS. Initializes parameters and starts event handlers. -- @function [parent=#MANTIS] __Start -- @param #MANTIS self -- @param #number delay Delay in seconds. --- Triggers the FSM event "Stop". Stops the MANTIS and all its event handlers. -- @param #MANTIS self --- Triggers the FSM event "Stop" after a delay. Stops the MANTIS and all its event handlers. -- @function [parent=#MANTIS] __Stop -- @param #MANTIS self -- @param #number delay Delay in seconds. --- Triggers the FSM event "Status". -- @function [parent=#MANTIS] Status -- @param #MANTIS self --- Triggers the FSM event "Status" after a delay. -- @function [parent=#MANTIS] __Status -- @param #MANTIS self -- @param #number delay Delay in seconds. --- On After "Relocating" event. HQ and/or EWR moved. -- @function [parent=#MANTIS] OnAfterRelocating -- @param #MANTIS self -- @param #string From The From State -- @param #string Event The Event -- @param #string To The To State -- @return #MANTIS self --- On After "GreenState" event. A SAM group was switched to GREEN alert. -- @function [parent=#MANTIS] OnAfterGreenState -- @param #MANTIS self -- @param #string From The From State -- @param #string Event The Event -- @param #string To The To State -- @param Wrapper.Group#GROUP Group The GROUP object whose state was changed -- @return #MANTIS self --- On After "RedState" event. A SAM group was switched to RED alert. -- @function [parent=#MANTIS] OnAfterRedState -- @param #MANTIS self -- @param #string From The From State -- @param #string Event The Event -- @param #string To The To State -- @param Wrapper.Group#GROUP Group The GROUP object whose state was changed -- @return #MANTIS self --- On After "AdvStateChange" event. Advanced state changed, influencing detection speed. -- @function [parent=#MANTIS] OnAfterAdvStateChange -- @param #MANTIS self -- @param #string From The From State -- @param #string Event The Event -- @param #string To The To State -- @param #number Oldstate Old state - 0 = green, 1 = amber, 2 = red -- @param #number Newstate New state - 0 = green, 1 = amber, 2 = red -- @param #number Interval Calculated detection interval based on state and advanced feature setting -- @return #MANTIS self --- On After "ShoradActivated" event. Mantis has activated a SHORAD. -- @function [parent=#MANTIS] OnAfterShoradActivated -- @param #MANTIS self -- @param #string From The From State -- @param #string Event The Event -- @param #string To The To State -- @param #string Name Name of the GROUP which SHORAD shall protect -- @param #number Radius Radius around the named group to find SHORAD groups -- @param #number Ontime Seconds the SHORAD will stay active --- On After "SeadSuppressionPlanned" event. Mantis has planned to switch off a site to defend SEAD attack. -- @function [parent=#MANTIS] OnAfterSeadSuppressionPlanned -- @param #MANTIS self -- @param #string From The From State -- @param #string Event The Event -- @param #string To The To State -- @param Wrapper.Group#GROUP Group The suppressed GROUP object -- @param #string Name Name of the suppressed group -- @param #number SuppressionStartTime Model start time of the suppression from `timer.getTime()` -- @param #number SuppressionEndTime Model end time of the suppression from `timer.getTime()` -- @param Wrapper.Group#GROUP Attacker The attacking GROUP object --- On After "SeadSuppressionStart" event. Mantis has switched off a site to defend a SEAD attack. -- @function [parent=#MANTIS] OnAfterSeadSuppressionStart -- @param #MANTIS self -- @param #string From The From State -- @param #string Event The Event -- @param #string To The To State -- @param Wrapper.Group#GROUP Group The suppressed GROUP object -- @param #string Name Name of the suppressed group -- @param Wrapper.Group#GROUP Attacker The attacking GROUP object --- On After "SeadSuppressionEnd" event. Mantis has switched on a site after a SEAD attack. -- @function [parent=#MANTIS] OnAfterSeadSuppressionEnd -- @param #MANTIS self -- @param #string From The From State -- @param #string Event The Event -- @param #string To The To State -- @param Wrapper.Group#GROUP Group The suppressed GROUP object -- @param #string Name Name of the suppressed group --- On After "SAMUnitHit" event. A SAM Unit was hit. -- @function [parent=#MANTIS] OnAfterSeadSuppressionEnd -- @param #MANTIS self -- @param #string From The From State -- @param #string Event The Event -- @param #string To The To State -- @param Wrapper.Group#GROUP Group The GROUP of the hit UNIT object -- @param #string Name Name of the suppressed group --- On After "SAMUnitLoast" event. A SAM Unit was lost. -- @function [parent=#MANTIS] OnAfterSeadSuppressionEnd -- @param #MANTIS self -- @param #string From The From State -- @param #string Event The Event -- @param #string To The To State -- @param Wrapper.Group#GROUP Group The GROUP of the lost UNIT object -- @param #string Name Name of the suppressed group return self end ----------------------------------------------------------------------- -- MANTIS helper functions ----------------------------------------------------------------------- --- Set to accept accoustic detection. Set this *before* MANTIS starts! -- @param #MANTIS self -- @param #number Radius (Optional) Radius in which we can "hear" units. Defaults to 2000 meters. -- @param #table UnitCategories (Optional) Set what Unit Categories we can "hear". Defaults to `{Unit.Category.HELICOPTER}` -- @return #MANTIS self function MANTIS:SetAccousticDetectionOn(Radius,UnitCategories) self.DetectAccoustic = true self.DetectAccousticRadius = Radius or 2000 self.DetectAccousticCategories = UnitCategories or {Unit.Category.HELICOPTER} return self end --- Switch off accoustic detection. -- @param #MANTIS self -- @return #MANTIS self function MANTIS:SetAccousticDetectionOff() self.DetectAccoustic = false return self end --- [Internal] Function to manage hits on SAM units -- @param #MANTIS self -- @param Core.Event#EVENTDATA EventData The EVENT data -- @return #MANTIS self function MANTIS:_EventHandler(EventData) self:T(self.lid .. "_EventHandler") local function IsOneOfOurs(name) for _,_name in pairs(self.ewr_templates) do if string.find(name,_name,1,true) then return true end end return false end local function SwitchSAMOn(Name,Group) 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 self.SamStateTracker[Name] = "RED" if self.UseEmOnOff then -- DONE: add emissions on/off Group:EnableEmission(true) elseif (not self.UseEmOnOff) then Group:OptionAlarmStateRed() end self:__RedState(1,Group) if self.SmokeDecoy == true then self:_SmokeUnits(Group) end end end local coordinate -- Core.Point#COORDINATE local Name -- #string local Group -- Wrapper.Group#GROUP local lasthit = 0 local firsthit = false local alerton = false -- Check if we can get a location local data = EventData -- Core.Event#EVENTDATA if data.id == EVENTS.Hit then -- Unit hit, one of ours? if data.TgtGroupName and IsOneOfOurs(data.TgtGroupName) then self:T("Unit hit in group: "..data.TgtGroupName) if data.TgtGroup then lasthit = data.TgtGroup:GetProperty("MANTIS_LASTHIT") firsthit = (lasthit==nil) and true or false if firsthit == true then alerton = true end if lasthit ~= nil and timer.getTime()-lasthit > self.ShoradTime then alerton = true end coordinate = data.TgtGroup:GetCoordinate() Name = data.TgtGroupName Group = data.TgtGroup if alerton == true then self:__SAMUnitHit(1,Group,Name) SwitchSAMOn(Name,Group) end if coordinate and self.debug then local text = coordinate:ToStringMGRS() self:I("Location: "..text) end end end end if data.id == EVENTS.UnitLost then if data.IniGroupName and IsOneOfOurs(data.IniGroupName) then self:T("Unit lost in group: "..data.IniGroupName) if data.IniGroup then lasthit = data.IniGroup:GetProperty("MANTIS_LASTHIT") firsthit = (lasthit==nil) and true or false if firsthit == true then alerton = true end if lasthit ~= nil and timer.getTime()-lasthit > self.ShoradTime then alerton = true end coordinate = data.IniGroup:GetCoordinate() Name = data.IniGroupName Group = data.IniGroup alerton = true SwitchSAMOn(Name,Group) self:__SAMUnitLost(1,Group,Name) if coordinate and self.debug then local text = coordinate:ToStringMGRS() self:I("Location: "..text) end end end end if firsthit == true or alerton == true then Group:SetProperty("MANTIS_LASTHIT",timer.getTime()) end if coordinate ~= nil and Name ~= nil and Group ~=nil and alerton == true then if self.ShoradLink then self:T("Shorad activated for: "..Name) local Shorad = self.Shorad -- Functional.Shorad#SHORAD local radius = self.checkradius local ontime = self.ShoradTime Shorad:WakeUpShorad(Name, radius, ontime, nil, true) self:__ShoradActivated(1,Name, radius, ontime) end if self.autorelocate and Group then Group:RelocateGroundRandomInRadius(20,500,true,true,nil,true) end end return self end --- [Internal] Function to get the self.SAM_Table -- @param #MANTIS self -- @return #table table function MANTIS:_GetSAMTable() self:T(self.lid .. "GetSAMTable") return self.SAM_Table end --- [Internal] Function to set the self.SAM_Table -- @param #MANTIS self -- @return #MANTIS self function MANTIS:_SetSAMTable(table) self:T(self.lid .. "SetSAMTable") self.SAM_Table = table return self end --- Function to set the grouping radius of the detection in meters -- @param #MANTIS self -- @param #number radius Radius upon which detected objects will be grouped function MANTIS:SetEWRGrouping(radius) self:T(self.lid .. "SetEWRGrouping") local radius = radius or 5000 self.grouping = radius return self end --- Add a SET_ZONE of zones for Shoot&Scoot - SHORAD units will move around -- @param #MANTIS self -- @param Core.Set#SET_ZONE ZoneSet Set of zones to be used. Units will move around to the next (random) zone between 100m and 3000m away. -- @param #number Number (Optional) Number of closest zones to be considered, defaults to 3. -- @param #boolean Random If true, use a random coordinate inside the next zone to scoot to. -- @param #string Formation (Optional) Formation to use, defaults to "Cone". See mission editor dropdown for options. -- @return #MANTIS self function MANTIS:AddScootZones(ZoneSet, Number, Random, Formation) self:T(self.lid .. " AddScootZones") self.SkateZones = ZoneSet self.SkateNumber = Number or 3 self.shootandscoot = true self.ScootRandom = Random self.ScootFormation = Formation or "Cone" return self end --- Function to set accept and reject zones. -- @param #MANTIS self -- @param #table AcceptZones Table of @{Core.Zone#ZONE} objects -- @param #table RejectZones Table of @{Core.Zone#ZONE} objects -- @param #table ConflictZones Table of @{Core.Zone#ZONE} objects -- @return #MANTIS self -- @usage -- Parameters are **tables of Core.Zone#ZONE** objects! -- This is effectively a 3-stage filter allowing for zone overlap. A coordinate is accepted first when -- it is inside any AcceptZone. Then RejectZones are checked, which enforces both borders, but also overlaps of -- Accept- and RejectZones. Last, if it is inside a conflict zone, it is accepted. function MANTIS:AddZones(AcceptZones,RejectZones, ConflictZones) self:T(self.lid .. "AddZones") self.AcceptZones = AcceptZones or {} self.RejectZones = RejectZones or {} self.ConflictZones = ConflictZones or {} self.AcceptZonesNo = UTILS.TableLength(self.AcceptZones) self.RejectZonesNo = UTILS.TableLength(self.RejectZones) self.ConflictZonesNo = UTILS.TableLength(self.ConflictZones) self:T(string.format("AcceptZonesNo = %d | RejectZonesNo = %d | ConflictZonesNo = %d",self.AcceptZonesNo,self.RejectZonesNo,self.ConflictZonesNo)) if self.AcceptZonesNo > 0 or self.RejectZonesNo > 0 or self.ConflictZonesNo > 0 then self.usezones = true end return self end --- Add a single reject zone to MANTIS. -- @param #MANTIS self -- @param Core.Zone#ZONE Zone The zone to be added -- @return #MANTIS self function MANTIS:AddRejectZone(Zone) if Zone and Zone:IsInstanceOf("ZONE_BASE") then table.insert(self.RejectZones,Zone) self.usezones = true end return self end --- Add a single accept zone to MANTIS. -- @param #MANTIS self -- @param Core.Zone#ZONE Zone The zone to be added -- @return #MANTIS self function MANTIS:AddAcceptZone(Zone) if Zone and Zone:IsInstanceOf("ZONE_BASE") then table.insert(self.AcceptZones,Zone) self.usezones = true end return self end --- Add a single conflict zone to MANTIS. -- @param #MANTIS self -- @param Core.Zone#ZONE Zone The zone to be added -- @return #MANTIS self function MANTIS:AddConflictZone(Zone) if Zone and Zone:IsInstanceOf("ZONE_BASE") then table.insert(self.ConflictZones,Zone) self.usezones = true end return self end --- Function to set corridor zones. -- @param #MANTIS self -- @param Core.Set#SET_ZONE CorridorZones Can be handed in as SET\_ZONE or single ZONE object. -- @return #MANTIS self function MANTIS:SetCorridorZones(CorridorZones) self:T(self.lid .. "SetCorridorZones") if CorridorZones and CorridorZones:IsInstanceOf("SET_ZONE") then self.corridorzones = CorridorZones self.usecorridors = true elseif CorridorZones and CorridorZones:IsInstanceOf("ZONE_BASE") then if not self.corridorzones then self.corridorzones = SET_ZONE:New() end self.corridorzones:AddZone(CorridorZones) self.usecorridors = true end if self.intelset then for _,_intel in pairs(self.intelset) do _intel:SetCorridorZones(self.corridorzones) end end return self end --- Function to add one corridor zone. -- @param #MANTIS self -- @param Core.Zone#ZONE CorridorZone The ZONE object to be added. -- @return #MANTIS self function MANTIS:AddCorridorZone(CorridorZone) self:T(self.lid .. "AddCorridorZone") self:SetCorridorZones(CorridorZone) return self end --- Function to set corridor zone floor and ceiling in FEET. -- @param #MANTIS self -- @param #number Floor Floor altitude ASL in feet. -- @param #number Ceiling Ceiling altitude ASL in feet. -- @return #MANTIS self function MANTIS:SetCorridorZoneFloorAndCeiling(Floor,Ceiling) self.corridorfloor = UTILS.FeetToMeters(Floor) self.corridorceiling = UTILS.FeetToMeters(Ceiling) if self.intelset then for _,_intel in pairs(self.intelset) do _intel:SetCorridorLimits(self.corridorfloor,self.corridorceiling) end end return self end --- Function to set corridor zone floor and ceiling in METERS. -- @param #MANTIS self -- @param #number Floor Floor altitude ASL in meters. -- @param #number Ceiling Ceiling altitude ASL in meters. -- @return #MANTIS self function MANTIS:SetCorridorZoneFloorAndCeilingMeters(Floor,Ceiling) self.corridorfloor = Floor self.corridorceiling = Ceiling if self.intelset then for _,_intel in pairs(self.intelset) do _intel:SetCorridorLimits(self.corridorfloor,self.corridorceiling) end end return self end --- Function to set the detection radius of the EWR in meters. (Deprecated, SAM range is used) -- @param #MANTIS self -- @param #number radius Radius of the EWR detection zone function MANTIS:SetEWRRange(radius) self:T(self.lid .. "SetEWRRange") return self end --- Function to set switch-on/off zone for the SAM sites in meters. Overwritten per SAM in automode. -- @param #MANTIS self -- @param #number radius Radius of the firing zone in classic mode function MANTIS:SetSAMRadius(radius) self:T(self.lid .. "SetSAMRadius") local radius = radius or 25000 self.checkradius = radius return self end --- Function to set SAM firing engage range, 0-100 percent, e.g. 85 -- @param #MANTIS self -- @param #number range Percent of the max fire range function MANTIS:SetSAMRange(range) self:T(self.lid .. "SetSAMRange") local range = range or 95 if range < 0 or range > 100 then range = 95 end self.engagerange = range return self end --- Function to set Short Range SAMs to spit out smoke as decoy, if an enemy plane is in range. -- @param #MANTIS self -- @param #boolean Onoff Set to true for on and nil/false for off. -- @param #number Color (Optional) Color to use, defaults to `SMOKECOLOR.White` function MANTIS:SetSmokeDecoy(Onoff,Color) self.SmokeDecoy = Onoff self.SmokeDecoyColor = Color or SMOKECOLOR.White return self end --- Function to set number of SAMs going active on a valid, detected thread -- @param #MANTIS self -- @param #number Short (Optional) Number of short-range systems activated, defaults to 2. -- @param #number Mid (Optional) Number of mid-range systems activated, defaults to 2. -- @param #number Long (Optional) Number of long-range systems activated, defaults to 1. -- @param #number Classic (Optional) (non-automode) Number of overall systems activated, defaults to 6. -- @param #number Point (Optional) Number of point defense and AAA systems activated, defaults to 6. -- @return #MANTIS self function MANTIS:SetMaxActiveSAMs(Short,Mid,Long,Classic,Point) self:T(self.lid .. "SetMaxActiveSAMs") self.maxclassic = Classic or 6 self.maxlongrange = Long or 1 self.maxmidrange = Mid or 2 self.maxshortrange = Short or 2 self.maxpointdefrange= Point or 6 return self end --- Function to set a new SAM firing engage range, use this method to adjust range while running MANTIS, e.g. for different setups day and night -- @param #MANTIS self -- @param #number range Percent of the max fire range function MANTIS:SetNewSAMRangeWhileRunning(range) self:T(self.lid .. "SetNewSAMRangeWhileRunning") local range = range or 95 if range < 0 or range > 100 then range = 95 end self.engagerange = range self:_RefreshSAMTable() self.mysead.EngagementRange = range return self end --- Function to set switch-on/off the debug state -- @param #MANTIS self -- @param #boolean onoff Set true to switch on function MANTIS:Debug(onoff) self:T(self.lid .. "SetDebug") local onoff = onoff or false self.debug = onoff if onoff then -- Debug trace. BASE:TraceOn() BASE:TraceClass("MANTIS") BASE:TraceLevel(1) else BASE:TraceOff() end return self end --- Function to get the HQ object for further use -- @param #MANTIS self -- @return Wrapper.Group#GROUP The HQ #GROUP object or *nil* if it doesn't exist function MANTIS:GetCommandCenter() self:T(self.lid .. "GetCommandCenter") if self.HQ_CC then return self.HQ_CC else return nil end end --- Function to set separate AWACS detection instance -- @param #MANTIS self -- @param #string prefix Name of the AWACS group in the mission editor function MANTIS:SetAwacs(prefix) self:T(self.lid .. "SetAwacs") if prefix ~= nil then if type(prefix) == "string" then self.AWACS_Prefix = prefix self.advAwacs = true end end return self end --- Function to set AWACS detection range. Defaults to 250.000m (250km) - use **before** starting your Mantis! -- @param #MANTIS self -- @param #number range Detection range of the AWACS group function MANTIS:SetAwacsRange(range) self:T(self.lid .. "SetAwacsRange") local range = range or 250000 self.awacsrange = range return self end --- Function to set the HQ object for further use -- @param #MANTIS self -- @param Wrapper.Group#GROUP group The #GROUP object to be set as HQ function MANTIS:SetCommandCenter(group) self:T(self.lid .. "SetCommandCenter") local group = group or nil if group ~= nil then if type(group) == "string" then self.HQ_CC = GROUP:FindByName(group) self.HQ_Template_CC = group else self.HQ_CC = group self.HQ_Template_CC = group:GetName() end end return self end --- Function to set how long INTEL DLINK remembers contacts. -- @param #MANTIS self -- @param #number seconds Remember this many seconds, at least 5 seconds. -- @return #MANTIS self function MANTIS:SetDLinkCacheTime(seconds) self.DLinkCacheTime = math.abs(seconds or 120) if self.DLinkCacheTime < 5 then self.DLinkCacheTime = 5 end return self end --- Function to set the detection interval -- @param #MANTIS self -- @param #number interval The interval in seconds function MANTIS:SetDetectInterval(interval) self:T(self.lid .. "SetDetectInterval") local interval = interval or 30 self.detectinterval = interval return self end --- Function to set Advanded Mode -- @param #MANTIS self -- @param #boolean onoff If true, will activate Advanced Mode -- @param #number ratio (Optional) Percentage to use for advanced mode, defaults to 100% -- @usage Advanced mode will *decrease* reactivity of MANTIS, if HQ and/or EWR network dies. Set SAMs to RED state if both are dead. Requires usage of an **HQ** object and the **dynamic** option. -- E.g. `mymantis:SetAdvancedMode(true, 90)` function MANTIS:SetAdvancedMode(onoff, ratio) self:T(self.lid .. "SetAdvancedMode") --self:T({onoff, ratio}) local onoff = onoff or false local ratio = ratio or 100 if (type(self.HQ_Template_CC) == "string") and onoff and self.dynamic then self.adv_ratio = ratio self.advanced = true self.adv_state = 0 self.Adv_EWR_Group = SET_GROUP:New():FilterPrefixes(self.EWR_Templates_Prefix):FilterCoalitions(self.Coalition):FilterStart() self:I(string.format("***** Starting Advanced Mode MANTIS Version %s *****", self.version)) else local text = self.lid.." Advanced Mode requires a HQ and dynamic to be set. Revisit your MANTIS:New() statement to add both." local m= MESSAGE:New(text,10,"MANTIS",true):ToAll() self:E(text) end return self end --- Set using Emissions on/off instead of changing alarm state -- @param #MANTIS self -- @param #boolean switch Decide if we are changing alarm state or Emission state function MANTIS:SetUsingEmOnOff(switch) self:T(self.lid .. "SetUsingEmOnOff") self.UseEmOnOff = switch or false return self end --- Set using your own #INTEL_DLINK object instead of #DETECTION -- @param #MANTIS self -- @param Ops.Intel#INTEL_DLINK DLink The data link object to be used. function MANTIS:SetUsingDLink(DLink) self:T(self.lid .. "SetUsingDLink") self.DLink = true self.Detection = DLink self.DLTimeStamp = timer.getAbsTime() return self end --- [Internal] Function to check if HQ is alive -- @param #MANTIS self -- @return #boolean True if HQ is alive, else false function MANTIS:_CheckHQState() self:T(self.lid .. "CheckHQState") local text = self.lid.." Checking HQ State" local m= MESSAGE:New(text,10,"MANTIS"):ToAllIf(self.debug) if self.verbose then self:I(text) end -- start check if self.advanced then local hq = self.HQ_Template_CC local hqgrp = GROUP:FindByName(hq) if hqgrp then if hqgrp:IsAlive() then -- ok we're on, hq exists and as alive --self:T(self.lid.." HQ is alive!") return true else --self:T(self.lid.." HQ is dead!") return false end end end return self end --- [Internal] Function to check if EWR is (at least partially) alive -- @param #MANTIS self -- @return #boolean True if EWR is alive, else false function MANTIS:_CheckEWRState() self:T(self.lid .. "CheckEWRState") local text = self.lid.." Checking EWR State" --self:T(text) local m= MESSAGE:New(text,10,"MANTIS"):ToAllIf(self.debug) if self.verbose then self:I(text) end -- start check if self.advanced then local EWR_Group = self.Adv_EWR_Group --local EWR_Set = EWR_Group.Set local nalive = EWR_Group:CountAlive() if self.advAwacs then local awacs = GROUP:FindByName(self.AWACS_Prefix) if awacs ~= nil then if awacs:IsAlive() then nalive = nalive+1 end end end --self:T(self.lid..string.format(" No of EWR alive is %d", nalive)) if nalive > 0 then return true else return false end end return self end --- [Internal] Check if any EWR or AWACS is still alive -- @param #MANTIS self -- @return #boolean outcome function MANTIS:_CheckAnyEWRAlive() self:T(self.lid .. "_CheckAnyEWRAlive") local alive = false if self.EWR_Group:CountAlive() > 0 then alive = true end if not alive and self.AWACS_Prefix then local awacs = GROUP:FindByName(self.AWACS_Prefix) if awacs and awacs:IsAlive() then alive = true end end return alive end --- [Internal] Function to determine state of the advanced mode -- @param #MANTIS self -- @return #number Newly calculated interval -- @return #number Previous state for tracking 0, 1, or 2 function MANTIS:_CalcAdvState() self:T(self.lid .. "CalcAdvState") local m=MESSAGE:New(self.lid.." Calculating Advanced State",10,"MANTIS"):ToAllIf(self.debug) if self.verbose then self:I(self.lid.." Calculating Advanced State") end -- start check local currstate = self.adv_state -- save curr state for comparison later local EWR_State = self:_CheckEWRState() local HQ_State = self:_CheckHQState() -- set state if EWR_State and HQ_State then -- both alive self.adv_state = 0 --everything is fine elseif EWR_State or HQ_State then -- one alive self.adv_state = 1 --slow down level 1 else -- none alive self.adv_state = 2 --slow down level 2 end -- calculate new detectioninterval local interval = self.detectinterval -- e.g. 30 local ratio = self.adv_ratio / 100 -- e.g. 80/100 = 0.8 ratio = ratio * self.adv_state -- e.g 0.8*2 = 1.6 local newinterval = interval + (interval * ratio) -- e.g. 30+(30*1.6) = 78 if self.debug or self.verbose then local text = self.lid..string.format(" Calculated OldState/NewState/Interval: %d / %d / %d", currstate, self.adv_state, newinterval) --self:T(text) local m=MESSAGE:New(text,10,"MANTIS"):ToAllIf(self.debug) if self.verbose then self:I(text) end end return newinterval, currstate end --- Function to set autorelocation for HQ and EWR objects. Note: Units must be actually mobile in DCS! -- @param #MANTIS self -- @param #boolean hq If true, will relocate HQ object -- @param #boolean ewr If true, will relocate EWR objects function MANTIS:SetAutoRelocate(hq, ewr) self:T(self.lid .. "SetAutoRelocate") --self:T({hq, ewr}) local hqrel = hq or false local ewrel = ewr or false if hqrel or ewrel then self.autorelocate = true self.autorelocateunits = { HQ = hqrel, EWR = ewrel } --self:T({self.autorelocate, self.autorelocateunits}) end return self end --- [Internal] Function to execute the relocation -- @param #MANTIS self -- @return #MANTIS self function MANTIS:_RelocateGroups() self:T(self.lid .. "RelocateGroups") local text = self.lid.." Relocating Groups" local m= MESSAGE:New(text,10,"MANTIS",true):ToAllIf(self.debug) if self.verbose then self:I(text) end if self.autorelocate then -- relocate HQ local HQGroup = self.HQ_CC if self.autorelocateunits.HQ and self.HQ_CC and HQGroup:IsAlive() then --only relocate if HQ exists local _hqgrp = self.HQ_CC --self:T(self.lid.." Relocating HQ") local text = self.lid.." Relocating HQ" --local m= MESSAGE:New(text,10,"MANTIS"):ToAll() _hqgrp:RelocateGroundRandomInRadius(20,500,true,true,nil,true) end --relocate EWR -- TODO: maybe dependent on AlarmState? Observed: SA11 SR only relocates if no objects in reach if self.autorelocateunits.EWR then -- get EWR Group local EWR_GRP = SET_GROUP:New():FilterPrefixes(self.EWR_Templates_Prefix):FilterCoalitions(self.Coalition):FilterOnce() local EWR_Grps = EWR_GRP.Set --table of objects in SET_GROUP for _,_grp in pairs (EWR_Grps) do if _grp:IsAlive() and _grp:IsGround() then --self:T(self.lid.." Relocating EWR ".._grp:GetName()) local text = self.lid.." Relocating EWR ".._grp:GetName() local m= MESSAGE:New(text,10,"MANTIS"):ToAllIf(self.debug) if self.verbose then self:I(text) end _grp:RelocateGroundRandomInRadius(20,500,true,true,nil,true) end end end end return self end --- [Internal] Function to check accept and reject zones -- @param #MANTIS self -- @param Core.Point#COORDINATE coord The coordinate to check -- @return #boolean outcome function MANTIS:_CheckCoordinateInZones(coord) -- DEBUG self:T(self.lid.."_CheckCoordinateInZones") local inzone = false -- acceptzones self:T(string.format("AcceptZonesNo = %d | RejectZonesNo = %d | ConflictZonesNo = %d",self.AcceptZonesNo,self.RejectZonesNo,self.ConflictZonesNo)) if self.AcceptZonesNo > 0 then for _,_zone in pairs(self.AcceptZones) do local zone = _zone -- Core.Zone#ZONE if zone:IsCoordinateInZone(coord) then inzone = true self:T(self.lid.."Target coord in Accept Zone!") break end end end -- rejectzones if self.RejectZonesNo > 0 then for _,_zone in pairs(self.RejectZones) do local zone = _zone -- Core.Zone#ZONE if zone:IsCoordinateInZone(coord) then inzone = false self:T(self.lid.."Target coord in Reject Zone!") break end end end -- conflictzones if self.ConflictZonesNo > 0 then for _,_zone in pairs(self.ConflictZones) do local zone = _zone -- Core.Zone#ZONE if zone:IsCoordinateInZone(coord) then inzone = true self:T(self.lid.."Target coord in Conflict Zone!") break end end end return inzone end --- [Internal] Function to prefilter height based and check for Helo activity. -- @param #MANTIS self -- @param #number height -- @param Core.Point#COORDINATE SamCoordinate -- @return #table set function MANTIS:_PreFilterHeight(height,SamCoordinate) 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 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: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 end end return set end --- [Internal] Function to check if any object is in the given SAM zone -- @param #MANTIS self -- @param #table dectset Table of coordinates of detected items -- @param Core.Point#COORDINATE samcoordinate Coordinate object. -- @param #number radius Radius to check. -- @param #number height Height to check. -- @param #boolean dlink Data from DLINK. -- @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) 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) end --self.friendlyset -- Core.Set#SET_GROUP if self.checkforfriendlies == true and self.friendlyset == nil then self.friendlyset = SET_GROUP:New():FilterCoalitions(self.Coalition):FilterCategories({"plane","helicopter"}):FilterFunction(function(grp) if grp and grp:InAir() then return true else return false end end):FilterStart() end for _,_coord in pairs (set) do local coord = _coord -- get current coord to check -- output for cross-check local targetdistance = samcoordinate:DistanceFromPointVec2(coord) if not targetdistance then targetdistance = samcoordinate:Get2DDistance(coord) end -- check accept/reject zones local zonecheck = true self:T("self.usezones = "..tostring(self.usezones)) if self.usezones then -- DONE zonecheck = self:_CheckCoordinateInZones(coord) end if self.verbose and self.debug then --local dectstring = coord:ToStringLLDMS() local samstring = samcoordinate:ToStringMGRS({MGRS_Accuracy=0}) samstring = string.gsub(samstring,"%s","") local inrange = "false" 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 m = MESSAGE:New(text,10,"Check"):ToAllIf(self.debug) self:T(self.lid..text) end -- friendlies around? local nofriendlies = true if self.checkforfriendlies == true then local closestfriend, distance = self.friendlyset:GetClosestGroup(samcoordinate) if closestfriend and distance and distance < rad then nofriendlies = false end end -- end output to cross-check if targetdistance <= rad and zonecheck == true and nofriendlies == true then return true, targetdistance end end return false, 0 end --- [Internal] Function to start the detection via EWR groups - if INTEL isn\'t available -- @param #MANTIS self -- @return Functional.Detection #DETECTION_AREAS The running detection set function MANTIS:StartDetection() self:T(self.lid.."Starting Detection") -- start detection local groupset = self.EWR_Group local grouping = self.grouping or 5000 --local acceptrange = self.acceptrange or 80000 local interval = self.detectinterval or 20 local MANTISdetection = DETECTION_AREAS:New( groupset, grouping ) --[Internal] Grouping detected objects to 5000m zones MANTISdetection:FilterCategories({ Unit.Category.AIRPLANE, Unit.Category.HELICOPTER }) --MANTISdetection:SetAcceptRange(acceptrange) -- deprecated - in range of SAMs is used anyway MANTISdetection:SetRefreshTimeInterval(interval) MANTISdetection:__Start(2) return MANTISdetection end --- [Internal] Function to start the detection with INTEL via EWR groups -- @param #MANTIS self -- @return Ops.Intel#INTEL_DLINK The running detection set function MANTIS:StartIntelDetection() self:T(self.lid.."Starting Intel Detection") -- DEBUG -- start detection local groupset = self.EWR_Group local samset = self.SAM_Group self.intelset = {} local IntelOne = INTEL:New(groupset,self.coalition,self.name.." IntelOne") IntelOne.DetectAccoustic = self.DetectAccoustic IntelOne.DetectAccousticRadius = self.DetectAccousticRadius or 2000 IntelOne.DetectAccousticUnitTypes = self.DetectAccousticCategories or {Unit.Category.HELICOPTER} --IntelOne:SetClusterAnalysis(true,true,true) if self.usecorridors == true then IntelOne:SetCorridorZones(self.corridorzones) if self.corridorfloor or self.corridorceiling then IntelOne:SetCorridorLimits(self.corridorfloor,self.corridorceiling) end end IntelOne:Start() local IntelTwo = INTEL:New(samset,self.coalition,self.name.." IntelTwo") IntelTwo.DetectAccoustic = self.DetectAccoustic IntelTwo.DetectAccousticRadius = self.DetectAccousticRadius or 2000 IntelTwo.DetectAccousticUnitTypes = self.DetectAccousticCategories or {Unit.Category.HELICOPTER} --IntelTwo:SetClusterAnalysis(true,true,true) if self.usecorridors == true then IntelTwo:SetCorridorZones(self.corridorzones) if self.corridorfloor or self.corridorceiling then IntelTwo:SetCorridorLimits(self.corridorfloor,self.corridorceiling) end end IntelTwo:Start() local CacheTime = self.DLinkCacheTime or 120 local IntelDlink = INTEL_DLINK:New({IntelOne,IntelTwo},self.name.." DLINK",22,CacheTime) IntelDlink:__Start(1) self:SetUsingDLink(IntelDlink) table.insert(self.intelset, IntelOne) table.insert(self.intelset, IntelTwo) return IntelDlink end --- [Internal] Function to start the detection via AWACS if defined as separate (classic) -- @param #MANTIS self -- @return Functional.Detection #DETECTION_AREAS The running detection set function MANTIS:StartAwacsDetection() self:T(self.lid.."Starting Awacs Detection") -- start detection local group = self.AWACS_Prefix local groupset = SET_GROUP:New():FilterPrefixes(group):FilterCoalitions(self.Coalition):FilterStart() local grouping = self.grouping or 5000 --local acceptrange = self.acceptrange or 80000 local interval = self.detectinterval or 60 --@param Functional.Detection #DETECTION_AREAS _MANTISdetection [Internal] The MANTIS detection object local MANTISAwacs = DETECTION_AREAS:New( groupset, grouping ) --[Internal] Grouping detected objects to 5000m zones MANTISAwacs:FilterCategories({ Unit.Category.AIRPLANE, Unit.Category.HELICOPTER }) MANTISAwacs:SetAcceptRange(self.awacsrange) --250km MANTISAwacs:SetRefreshTimeInterval(interval) MANTISAwacs:Start() return MANTISAwacs end --- [Internal] Function to get SAM firing data from units types. -- @param #MANTIS self -- @param #string grpname Name of the group -- @param #boolean mod HDS mod flag -- @param #boolean sma SMA mod flag -- @param #boolean chm CH mod flag -- @return #number range Max firing range -- @return #number height Max firing height -- @return #string type Long, medium or short range -- @return #number blind "blind" spot function MANTIS:_GetSAMDataFromUnits(grpname,mod,sma,chm) self:T(self.lid.."_GetSAMDataFromUnits") local found = false local range = self.checkradius local height = 3000 local type = MANTIS.SamType.MEDIUM local radiusscale = self.radiusscale[type] local blind = 0 local group = GROUP:FindByName(grpname) -- Wrapper.Group#GROUP local units = group:GetUnits() local SAMData = self.SamData local ARMCapacity if mod then SAMData = self.SamDataHDS elseif sma then SAMData = self.SamDataSMA elseif chm then SAMData = self.SamDataCH 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 end end if found then break end end --- AAA or Point Defense if not found then local grp = GROUP:FindByName(grpname) if (grp and grp:IsAlive() and grp:IsAAA()) or string.find(grpname,"AAA",1,true) then range = 2000 height = 2000 blind = 50 type = MANTIS.SamType.POINT found = true end end if not found then self:E(self.lid .. string.format("*****Could not match radar data for %s! Will default to midrange values!",grpname)) end return range, height, type, blind, ARMCapacity end --- [Internal] Function to get SAM firing data -- @param #MANTIS self -- @param #string grpname Name of the group -- @return #number range Max firing range -- @return #number height Max firing height -- @return #string type Long, medium or short range -- @return #number blind "blind" spot function MANTIS:_GetSAMRange(grpname) self:T(self.lid.."_GetSAMRange for "..tostring(grpname)) local range = self.checkradius local height = 3000 local type = MANTIS.SamType.MEDIUM local radiusscale = self.radiusscale[type] local blind = 0 local found = false local HDSmod = false local SMAMod = false local CHMod = false local ARMCapacity = 0 if string.find(grpname,"HDS",1,true) then HDSmod = true elseif string.find(grpname,"SMA",1,true) then SMAMod = true elseif string.find(grpname,"CHM",1,true) then CHMod = true end --if self.automode then for idx,entry in pairs(self.SamData) do self:T2("ID = " .. idx) if string.find(grpname,idx,1,true) then local _entry = entry -- #MANTIS.SamData type = _entry.Type radiusscale = self.radiusscale[type] range = _entry.Range * 1000 * radiusscale -- max firing range height = _entry.Height * 1000 -- max firing height blind = _entry.Blindspot ARMCapacity = _entry.ARMCapacity or 0 self:T("Matching Groupname = " .. grpname .. " Range= " .. range) found = true break end end --end --- Secondary - AAA or Point Defense if not found then local grp = GROUP:FindByName(grpname) if (grp and grp:IsAlive() and grp:IsAAA()) or string.find(grpname,"AAA",1,true) then range = 2000 height = 2000 blind = 50 type = MANTIS.SamType.POINT found = true end end --- Tertiary filter if not found if (not found) or HDSmod or SMAMod or CHMod then range, height, type, blind, ARMCapacity = self:_GetSAMDataFromUnits(grpname,HDSmod,SMAMod,CHMod) elseif not found then self:E(self.lid .. string.format("*****Could not match radar data for %s! Will default to midrange values!",grpname)) end if found and string.find(grpname,"SHORAD",1,true) then type = MANTIS.SamType.POINT -- force short on match end return range, height, type, blind, ARMCapacity end --- [Internal] Function to set the SAM start state -- @param #MANTIS self -- @return #MANTIS self function MANTIS:SetSAMStartState() -- DONE: if using dynamic filtering, update SAM_Table and the (active) SEAD groups, pull req #1405/#1406 -- DONE: Auto mode self:T(self.lid.."Setting SAM Start States") -- get SAM Group local SAM_SET = self.SAM_Group local SAM_Grps = SAM_SET.Set --table of objects local SAM_Tbl = {} -- table of SAM defense zones local SAM_Tbl_lg = {} -- table of long range SAM defense zones local SAM_Tbl_md = {} -- table of mid range SAM defense zones local SAM_Tbl_sh = {} -- table of short range SAM defense zones local SAM_Tbl_pt = {} -- table of point defense/AAA local SEAD_Grps = {} -- table of SAM names to make evasive 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 local group = _group -- Wrapper.Group#GROUP -- DONE: add emissions on/off if self.UseEmOnOff then group:OptionAlarmStateRed() group:EnableEmission(false) --group:SetAIOff() else group:OptionAlarmStateGreen() -- AI off end group:OptionEngageRange(engagerange) --default engagement will be 95% of firing range local grpname = group:GetName() local grpcoord = group:GetCoordinate() 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 ) 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 ) 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 ) 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 end self.SamStateTracker[grpname] = "GREEN" end end self.SAM_Table = SAM_Tbl self.SAM_Table_Long = SAM_Tbl_lg self.SAM_Table_Medium = SAM_Tbl_md self.SAM_Table_Short = SAM_Tbl_sh self.SAM_Table_PointDef = SAM_Tbl_pt -- make SAMs evasive local mysead = SEAD:New( SEAD_Grps, self.Padding ) -- Functional.Sead#SEAD mysead:SetEngagementRange(engagerange) mysead:AddCallBack(self) if self.UseEmOnOff then mysead:SwitchEmissions(true) end self.mysead = mysead return self end --- [Internal] Function to update SAM table and SEAD state -- @param #MANTIS self -- @return #MANTIS self function MANTIS:_RefreshSAMTable() self:T(self.lid.."RefreshSAMTable") -- Requires SEAD 0.2.2 or better -- get SAM Group local SAM_SET = self.SAM_Group local SAM_Grps = SAM_SET.Set --table of objects local SAM_Tbl = {} -- table of SAM defense zones local SAM_Tbl_lg = {} -- table of long range SAM defense zones local SAM_Tbl_md = {} -- table of mid range SAM defense zones local SAM_Tbl_sh = {} -- table of short range SAM defense zones local SAM_Tbl_pt = {} -- table of point defense/AAA local SEAD_Grps = {} -- table of SAM names to make evasive 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 local group = _group -- Wrapper.Group#GROUP group:OptionEngageRange(engagerange) --engagement will be 95% of firing range if group:IsGround() and group:IsAlive() then local grpname = group:GetName() local grpcoord = group:GetCoordinate() 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 table.insert( SEAD_Grps, grpname ) end if type == MANTIS.SamType.LONG and radaralive then table.insert( SAM_Tbl_lg, {grpname, grpcoord, grprange, grpheight, blind, type}) self:T({grpname,grprange, grpheight}) elseif type == MANTIS.SamType.MEDIUM and radaralive then table.insert( SAM_Tbl_md, {grpname, grpcoord, grprange, grpheight, blind, type}) self:T({grpname,grprange, grpheight}) elseif type == MANTIS.SamType.SHORT and radaralive then table.insert( SAM_Tbl_sh, {grpname, grpcoord, grprange, grpheight, blind, type}) self:T({grpname,grprange, grpheight}) 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 end end end end self.SAM_Table = SAM_Tbl self.SAM_Table_Long = SAM_Tbl_lg self.SAM_Table_Medium = SAM_Tbl_md self.SAM_Table_Short = SAM_Tbl_sh self.SAM_Table_PointDef = SAM_Tbl_pt -- make SAMs evasive if self.mysead ~= nil then local mysead = self.mysead mysead:UpdateSet( SEAD_Grps ) end return self end --- Function to link up #MANTIS with a #SHORAD installation -- @param #MANTIS self -- @param Functional.Shorad#SHORAD Shorad The #SHORAD object -- @param #number Shoradtime Number of seconds #SHORAD stays active post wake-up function MANTIS:AddShorad(Shorad,Shoradtime) self:T(self.lid.."AddShorad") local Shorad = Shorad or nil local ShoradTime = Shoradtime or 600 local ShoradLink = true if Shorad:IsInstanceOf("SHORAD") then self.ShoradLink = ShoradLink self.Shorad = Shorad --#SHORAD self.ShoradTime = Shoradtime -- #number end return self end --- Function to unlink #MANTIS from a #SHORAD installation -- @param #MANTIS self function MANTIS:RemoveShorad() self:T(self.lid.."RemoveShorad") self.ShoradLink = false return self end --- [Internal] Function to smoke a group in decoy. -- @param #MANTIS self -- @param Wrapper.Group#GROUP Group -- @return #MANTIS self function MANTIS:_SmokeUnits(Group) self:T("Smoking") local LastSmoketime=Group:GetProperty("MANTIS_LASTSMOKE_TIME") or 0 local TNow = timer.getTime() if TNow - LastSmoketime > 290 then -- Smoking lasts 5 minutes Group:SetProperty("MANTIS_LASTSMOKE_TIME",TNow) local units = Group:GetUnits() or {} local smoke = self.SmokeDecoyColor or SMOKECOLOR.White for _,unit in pairs(units) do if unit and unit:IsAlive() then unit:GetCoordinate():Smoke(smoke) end end end return self end ----------------------------------------------------------------------- -- MANTIS main functions ----------------------------------------------------------------------- --- [Internal] Check if a system can and should defend for HARMs itself -- @param #MANTIS self -- @param Wrapper.Group#GROUP targetGroup -- @param #string targetName -- @param Wrapper.Group#GROUP attackerGroup -- @param #string weaponName -- @param Wrapper.Weapon#WEAPON weaponWrapper -- @param #number tti -- @param #number delay -- @return #boolean Outcome function MANTIS:SeadAllowSuppression(targetGroup, targetName, attackerGroup, weaponName, weaponWrapper, tti, delay) self:T(self.lid.."SeadAllowSuppression") --- Thanks to @Goon Jan 2026 ---------------------------------------------------------------- -- LOG INCOMING REQUEST ---------------------------------------------------------------- self:T(string.format("MANTIS:SeadAllowSuppression REQUEST | target=%s | weapon=%s | tti=%s | delay=%s",tostring(targetName), tostring(weaponName),tostring(tti),tostring(delay))) ---------------------------------------------------------------- -- JAMMER CHECK: if SAM is currently jammed, deny SEAD suppression. -- The jammer is already keeping the SAM off. SEAD suppression would -- cause SuppressionStop to briefly re-enable the radar and relocate -- the SAM (potentially out of jammer range). Let the jammer handle it. ---------------------------------------------------------------- if self._jammerEnabled and self._jammedSAMs and self._jammedSAMs[targetName] then self:T(string.format("MANTIS:SeadAllowSuppression DECISION -> DENIED (JAMMED %.0f%%) | target=%s", self._jammedSAMs[targetName] * 100, tostring(targetName))) return false end ---------------------------------------------------------------- -- LOOK UP ARM CAPACITY FOR THIS SAM ---------------------------------------------------------------- local armcap = targetGroup:GetProperty("ARMCapacity") if not armcap then for _, sam in pairs(self.SAM_Table or {}) do if sam[1] == targetName then armcap = sam[7] -- ARMCapacity break end end end self:T(string.format("MANTIS:SeadAllowSuppression SAM DATA | target=%s | ARMCapacity=%s",tostring(targetName),armcap and tostring(armcap) or "nil")) ---------------------------------------------------------------- -- TRACK SEAD THREATS (PER TARGET) ---------------------------------------------------------------- local THREAT_WINDOW = 0.1 -- seconds self.LastThreatEval = self.LastThreatEval or {} self.InboundARMs = self.InboundARMs or {} local now = timer.getTime() local last = self.LastThreatEval[targetName] or 0 if (now - last) >= THREAT_WINDOW then self.InboundARMs[targetName] = (self.InboundARMs[targetName] or 0) + 1 self.LastThreatEval[targetName] = now self:T(string.format("MANTIS:SeadAllowSuppression NEW threat accepted | Δt=%.3f",now - last)) else self:T(string.format("MANTIS:SeadAllowSuppression duplicate evaluation ignored | Δt=%.3f",now - last)) end local inbound = self.InboundARMs[targetName] or 0 self:T(string.format("MANTIS:SeadAllowSuppression THREAT COUNT | target=%s | inboundThreats=%d",tostring(targetName),inbound)) ---------------------------------------------------------------- -- DECISION GATE ---------------------------------------------------------------- -- No missiles left over → legacy behavior if targetGroup and targetGroup:IsAlive() then local AmmotT, AmmoS, _, _,AmmoM = targetGroup:GetAmmunition() -- TODO Check C-RAM probably needs an exception as it is a gun, need to check its effectiveness if AmmoM and AmmoM == 0 then self:T(string.format("MANTIS:SeadAllowSuppression DECISION -> APPROVED (no MISSILES) | target=%s",tostring(targetName))) return true end end -- No ARM capacity defined → legacy behavior if (not armcap) or armcap == 0 then self:T(string.format("MANTIS:SeadAllowSuppression DECISION -> APPROVED (no ARMCAP) | target=%s",tostring(targetName))) return true end -- Suppress only once enough threats accumulated if inbound >= armcap then self:T(string.format("MANTIS:SeadAllowSuppression DECISION -> APPROVED (inbound %d >= cap %d) | target=%s",inbound,armcap,tostring(targetName))) return true end self:T(string.format("MANTIS:SeadAllowSuppression DECISION -> DENIED (inbound %d < cap %d) | target=%s",inbound,armcap,tostring(targetName))) return false end --- [Internal] Check detection function -- @param #MANTIS self -- @param #table samset Table of SAM data -- @param #table detset Table of COORDINATES -- @param #boolean dlink Using DLINK -- @param #number limit of SAM sites to go active on a contact -- @return #number instatusred -- @return #number instatusgreen -- @return #number activeshorads function MANTIS:_CheckLoop(samset,detset,dlink,limit) self:T(self.lid .. "CheckLoop " .. #detset .. " Coordinates") local switchedon = 0 local instatusred = 0 local instatusgreen = 0 local activeshorads = 0 local SEADactive = 0 for _,_data in pairs (samset) do local samcoordinate = _data[2] local name = _data[1] local radius = _data[3] local height = _data[4] local blind = _data[5] * 1.25 + 1 local shortsam = (_data[6] ~= MANTIS.SamType.LONG) and true or false if not shortsam then shortsam = (_data[6] == MANTIS.SamType.POINT) and true or false end local samgroup = GROUP:FindByName(name) local IsInZone, Distance = self:_CheckObjectInZone(detset, samcoordinate, radius, height, dlink) local suppressed = self.SuppressedGroups[name] 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 -- switch on SAM local switch = false if self.UseEmOnOff and switchedon < limit then -- DONE: add emissions on/off samgroup:EnableEmission(true) switchedon = switchedon + 1 switch = true elseif (not self.UseEmOnOff) and switchedon < limit then samgroup:OptionAlarmStateRed() switchedon = switchedon + 1 switch = true end if self.SamStateTracker[name] ~= "RED" and switch then self.SamStateTracker[name] = "RED" self:__RedState(1,samgroup) end -- DONE Restrict on Distance if shortsam == true 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 local Shorad = self.Shorad --Functional.Shorad#SHORAD local radius = self.checkradius local ontime = self.ShoradTime Shorad:WakeUpShorad(name, radius, ontime) self:__ShoradActivated(1,name, radius, ontime) end -- debug output if (self.debug or self.verbose) and switch then local text = string.format("SAM %s in alarm state RED!", name) --local m=MESSAGE:New(text,10,"MANTIS"):ToAllIf(self.debug if self.verbose then self:I(self.lid..text) end end end --end alive else if samgroup:IsAlive() and not suppressed and not activeshorad then -- switch off SAM if self.UseEmOnOff then samgroup:EnableEmission(false) else samgroup:OptionAlarmStateGreen() end if self.SamStateTracker[name] ~= "GREEN" then self.SamStateTracker[name] = "GREEN" self:__GreenState(1,samgroup) end if self.debug or self.verbose then local text = string.format("SAM %s in alarm state GREEN!", name) --local m=MESSAGE:New(text,10,"MANTIS"):ToAllIf(self.debug) if self.verbose then self:I(self.lid..text) end end end --end alive end --end check end --for loop --[[ if self.debug or self.verbose or self.logsamstatus then for _,_status in pairs(self.SamStateTracker) do if _status == "GREEN" then instatusgreen=instatusgreen+1 elseif _status == "RED" then instatusred=instatusred+1 end end if self.Shorad then for _,_name in pairs(self.Shorad.ActiveGroups or {}) do activeshorads=activeshorads+1 end end end self:T(self.lid..string.format("SAM State Count: GREEN %d | RED %d | SHORAD %d",instatusred, instatusgreen, activeshorads)) --]] return instatusred, instatusgreen, activeshorads end --- [Internal] Check detection function -- @param #MANTIS self -- @param Functional.Detection#DETECTION_AREAS detection Detection object -- @param #boolean dlink -- @param #boolean reporttolog -- @return #MANTIS self function MANTIS:_Check(detection,dlink,reporttolog) self:T(self.lid .. "Check") --get detected set local detset = detection:GetDetectedItemCoordinates() --self:T("Check:", {detset}) -- update SAM Table evey 3 runs if self.checkcounter%3 == 0 then self:_RefreshSAMTable() end self.checkcounter = self.checkcounter + 1 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 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 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 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) 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) end local function GetReport() if self.debug or self.verbose or self.logsamstatus then for _,_status in pairs(self.SamStateTracker) do if _status == "GREEN" then instatusgreen=instatusgreen+1 elseif _status == "RED" then instatusred=instatusred+1 end end if self.Shorad then for _,_name in pairs(self.Shorad.ActiveGroups or {}) do activeshorads=activeshorads+1 end end end local statusreport = REPORT:New("\nMANTIS Status "..self.name) statusreport:Add("+-----------------------------+") statusreport:Add(string.format("+ SAM in RED State: %2d",instatusred)) statusreport:Add(string.format("+ SAM in GREEN State: %2d",instatusgreen)) if self.Shorad then statusreport:Add(string.format("+ SHORAD active: %2d",activeshorads)) end statusreport:Add("+-----------------------------+") return statusreport end if self.debug or self.verbose then local statusreport = GetReport() MESSAGE:New(statusreport:Text(),10):ToAll():ToLog() elseif reporttolog == true then local statusreport = GetReport() MESSAGE:New(statusreport:Text(),10):ToLog() end return self end --- [Internal] Relocation relay function -- @param #MANTIS self -- @return #MANTIS self function MANTIS:_Relocate() self:T(self.lid .. "Relocate") self:_RelocateGroups() return self end --- [Internal] Check advanced state -- @param #MANTIS self -- @return #MANTIS self function MANTIS:_CheckAdvState() self:T(self.lid .. "CheckAdvSate") local interval, oldstate = self:_CalcAdvState() local newstate = self.adv_state if newstate ~= oldstate then -- deal with new state self:__AdvStateChange(1,oldstate,newstate,interval) if newstate == 2 then -- switch alarm state RED self.state2flag = true 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 -- DONE: add emissions on/off --samgroup:SetAIOn() samgroup:EnableEmission(true) else samgroup:OptionAlarmStateRed() end end -- end alive end -- end for loop elseif newstate <= 1 then -- change MantisTimer to slow down or speed up self.detectinterval = interval self.state2flag = false end end -- end newstate vs oldstate return self end --- [Internal] Check DLink state -- @param #MANTIS self -- @return #MANTIS self function MANTIS:_CheckDLinkState() self:T(self.lid .. "_CheckDLinkState") local dlink = self.Detection -- Ops.Intel#INTEL_DLINK local TS = timer.getAbsTime() if not dlink:Is("Running") and (TS - self.DLTimeStamp > 29) then self.DLink = false self.Detection = self:StartDetection() -- fall back self:I(self.lid .. "Intel DLink not running - switching back to single detection!") end end --- [Internal] Function to set start state -- @param #MANTIS self -- @param #string From The From State -- @param #string Event The Event -- @param #string To The To State -- @return #MANTIS self function MANTIS:onafterStart(From, Event, To) self:T({From, Event, To}) self:T(self.lid.."Starting MANTIS") self:SetSAMStartState() if not INTEL then self.Detection = self:StartDetection() else self.Detection = self:StartIntelDetection() end if self.autoshorad then self.Shorad = SHORAD:New(self.name.."-SHORAD","SHORAD",self.SAM_Group,self.ShoradActDistance,self.ShoradTime,self.Coalition,self.UseEmOnOff,self.SmokeDecoy,self.SmokeDecoyColor) self.Shorad:SetDefenseLimits(80,95) self.ShoradLink = true self.Shorad.Groupset=self.ShoradGroupSet self.Shorad.debug = self.debug self.Shorad:AddCallBack(self) end if self.shootandscoot and self.SkateZones and self.Shorad then self.Shorad:AddScootZones(self.SkateZones,self.SkateNumber or 3,self.ScootRandom,self.ScootFormation) end self:HandleEvent(EVENTS.Hit,self._EventHandler) self:HandleEvent(EVENTS.UnitLost,self._EventHandler) self:__Status(-math.random(1,10)) return self end --- [Internal] Before status function for MANTIS -- @param #MANTIS self -- @param #string From The From State -- @param #string Event The Event -- @param #string To The To State -- @return #MANTIS self function MANTIS:onbeforeStatus(From, Event, To) self:T({From, Event, To}) -- check detection if not self.state2flag then self:_Check(self.Detection,self.DLink,self.logsamstatus) end local EWRAlive = self:_CheckAnyEWRAlive() local function FindSAMSRTR() for i=1,1000 do local randomsam = self.SAM_Group:GetRandom() if randomsam and randomsam:IsAlive() then if randomsam:IsSAM() then return randomsam end end end end -- Switch on a random SR/TR if no EWR left over if not EWRAlive then local randomsam = FindSAMSRTR() -- Wrapper.Group#GROUP if randomsam and randomsam:IsAlive() then if self.UseEmOnOff then randomsam:EnableEmission(true) else randomsam:OptionAlarmStateRed() end local name = randomsam:GetName() if self.SamStateTracker[name] ~= "RED" then self:__RedState(1,randomsam) self.SamStateTracker[name] = "RED" end end end -- relocate HQ and EWR if self.autorelocate then local relointerval = self.relointerval local thistime = timer.getAbsTime() local timepassed = thistime - self.TimeStamp local halfintv = math.floor(timepassed / relointerval) if halfintv >= 1 then self.TimeStamp = timer.getAbsTime() self:_Relocate() self:__Relocating(1) end end -- advanced state check if self.advanced then self:_CheckAdvState() end -- check DLink state if self.DLink then self:_CheckDLinkState() end return self end --- [Internal] Status function for MANTIS -- @param #MANTIS self -- @param #string From The From State -- @param #string Event The Event -- @param #string To The To State -- @return #MANTIS self function MANTIS:onafterStatus(From,Event,To) self:T({From, Event, To}) -- Display some states if self.debug and self.verbose then self:I(self.lid .. "Status Report") for _name,_state in pairs(self.SamStateTracker) do self:I(string.format("Site %s | Status %s",_name,_state)) end end local interval = self.detectinterval * -1 self:__Status(interval) return self end --- [Internal] Function to stop MANTIS -- @param #MANTIS self -- @param #string From The From State -- @param #string Event The Event -- @param #string To The To State -- @return #MANTIS self function MANTIS:onafterStop(From, Event, To) self:T({From, Event, To}) return self end --- [Internal] Function triggered by Event Relocating -- @param #MANTIS self -- @param #string From The From State -- @param #string Event The Event -- @param #string To The To State -- @return #MANTIS self function MANTIS:onafterRelocating(From, Event, To) self:T({From, Event, To}) return self end --- [Internal] Function triggered by Event GreenState -- @param #MANTIS self -- @param #string From The From State -- @param #string Event The Event -- @param #string To The To State -- @param Wrapper.Group#GROUP Group The GROUP object whose state was changed -- @return #MANTIS self function MANTIS:onafterGreenState(From, Event, To, Group) self:T({From, Event, To, Group:GetName()}) return self end --- [Internal] Function triggered by Event RedState -- @param #MANTIS self -- @param #string From The From State -- @param #string Event The Event -- @param #string To The To State -- @param Wrapper.Group#GROUP Group The GROUP object whose state was changed -- @return #MANTIS self function MANTIS:onafterRedState(From, Event, To, Group) self:T({From, Event, To, Group:GetName()}) return self end --- [Internal] Function triggered by Event AdvStateChange -- @param #MANTIS self -- @param #string From The From State -- @param #string Event The Event -- @param #string To The To State -- @param #number Oldstate Old state - 0 = green, 1 = amber, 2 = red -- @param #number Newstate New state - 0 = green, 1 = amber, 2 = red -- @param #number Interval Calculated detection interval based on state and advanced feature setting -- @return #MANTIS self function MANTIS:onafterAdvStateChange(From, Event, To, Oldstate, Newstate, Interval) self:T({From, Event, To, Oldstate, Newstate, Interval}) return self end --- [Internal] Function triggered by Event ShoradActivated -- @param #MANTIS self -- @param #string From The From State -- @param #string Event The Event -- @param #string To The To State -- @param #string Name Name of the GROUP which SHORAD shall protect -- @param #number Radius Radius around the named group to find SHORAD groups -- @param #number Ontime Seconds the SHORAD will stay active function MANTIS:onafterShoradActivated(From, Event, To, Name, Radius, Ontime) self:T({From, Event, To, Name, Radius, Ontime}) return self end --- [Internal] Function triggered by Event SeadSuppressionStart -- @param #MANTIS self -- @param #string From The From State -- @param #string Event The Event -- @param #string To The To State -- @param Wrapper.Group#GROUP Group The suppressed GROUP object -- @param #string Name Name of the suppressed group -- @param Wrapper.Group#GROUP Attacker The attacking GROUP object function MANTIS:onafterSeadSuppressionStart(From, Event, To, Group, Name, Attacker) self:T({From, Event, To, Name}) self.SuppressedGroups[Name] = true if self.ShoradLink then local Shorad = self.Shorad local radius = self.checkradius local ontime = self.ShoradTime Shorad:WakeUpShorad(Name, radius, ontime, nil, true) self:__ShoradActivated(1,Name, radius, ontime) end return self end --- [Internal] Function triggered by Event SeadSuppressionEnd -- @param #MANTIS self -- @param #string From The From State -- @param #string Event The Event -- @param #string To The To State -- @param Wrapper.Group#GROUP Group The suppressed GROUP object -- @param #string Name Name of the suppressed group function MANTIS:onafterSeadSuppressionEnd(From, Event, To, Group, Name) self:T({From, Event, To, Name}) self.SuppressedGroups[Name] = false self.InboundARMs[Name] = 0 return self end --- [Internal] Function triggered by Event SeadSuppressionPlanned -- @param #MANTIS self -- @param #string From The From State -- @param #string Event The Event -- @param #string To The To State -- @param Wrapper.Group#GROUP Group The suppressed GROUP object -- @param #string Name Name of the suppressed group -- @param #number SuppressionStartTime Model start time of the suppression from `timer.getTime()` -- @param #number SuppressionEndTime Model end time of the suppression from `timer.getTime()` -- @param Wrapper.Group#GROUP Attacker The attacking GROUP object function MANTIS:onafterSeadSuppressionPlanned(From, Event, To, Group, Name, SuppressionStartTime, SuppressionEndTime, Attacker) self:T({From, Event, To, Name}) return self end ----------------------------------------------------------------------- -- MANTIS Jammer Extension v2.0.0 - Functions ----------------------------------------------------------------------- --- Add client jammer aircraft support. Call BEFORE :Start(). -- @param #MANTIS self -- @param Core.Set#SET_GROUP clientSet SET_GROUP of client jammer aircraft, or a prefix string -- @param #string defaultLoadout Default loadout key (default "1xALQ99_2xALQ249") -- @return #MANTIS self function MANTIS:AddJammer(clientSet, defaultLoadout) self:T(self.lid .. "AddJammer") self._jammerEnabled = true -- Allow string prefix shorthand if type(clientSet) == "string" then clientSet = SET_GROUP:New():FilterPrefixes(clientSet):FilterActive(true):FilterStart() end self._jammerClientSet = clientSet self._jammerDefaultLoadout = defaultLoadout or "1xALQ99_2xALQ249" self._jammerAircraft = self._jammerAircraft or {} self._jammerAISets = self._jammerAISets or {} self._jammerSnapshot = self._jammerSnapshot or {} self._jammedSAMs = self._jammedSAMs or {} self._jammerMenusBuilt = self._jammerMenusBuilt or {} if not self._jammerHasTransitions then self:AddTransition("*", "JammerSuppression", "*") self:AddTransition("*", "JammerActivated", "*") self:AddTransition("*", "JammerDeactivated", "*") self._jammerHasTransitions = true end -- Start fast client menu scheduler (5s interval) so menus appear quickly -- instead of waiting for the next MANTIS Status cycle (~30s) if not self._jammerMenuScheduler then self._jammerMenuScheduler = SCHEDULER:New(nil, function() if not self._jammerEnabled then return end if self._jammerClientSet then self:_IterateJammerSet(self._jammerClientSet, function(unit, group) local unitName = unit:GetName() if not self._jammerAircraft[unitName] then self._jammerAircraft[unitName] = { loadout = self._jammerDefaultLoadout, active = false, isClient = unit:IsPlayer(), hasLoadout = false, } elseif not self._jammerAircraft[unitName].isClient and unit:IsPlayer() then self._jammerAircraft[unitName].isClient = true end if self._jammerAircraft[unitName].isClient and not self._jammerMenusBuilt[unitName] then self:_SetupJammerMenu(unit, group) end end) end end, {}, 2, 5) -- start in 2s, repeat every 5s end self:I(string.format("%sJammer configured | default=%s", self.lid, self._jammerDefaultLoadout)) return self end --- Add AI jammer aircraft. Call BEFORE :Start(). Can be called multiple times. -- AI jammers start OFF; use SetJammerActive(unitName, true) to activate via script. -- @param #MANTIS self -- @param Core.Set#SET_GROUP aiSet SET_GROUP or prefix string for AI jammer aircraft -- @param #string loadout Loadout key for these AI aircraft (default "1xALQ99_2xALQ249") -- @return #MANTIS self function MANTIS:AddJammerAI(aiSet, loadout) self:T(self.lid .. "AddJammerAI") self._jammerEnabled = true self._jammerAircraft = self._jammerAircraft or {} self._jammerAISets = self._jammerAISets or {} self._jammerSnapshot = self._jammerSnapshot or {} self._jammedSAMs = self._jammedSAMs or {} self._jammerMenusBuilt = self._jammerMenusBuilt or {} local actualSet = aiSet if type(aiSet) == "string" then actualSet = SET_GROUP:New():FilterPrefixes(aiSet):FilterActive(true):FilterStart() end table.insert(self._jammerAISets, { set = actualSet, loadout = loadout or "1xALQ99_2xALQ249" }) if not self._jammerHasTransitions then self:AddTransition("*", "JammerSuppression", "*") self:AddTransition("*", "JammerActivated", "*") self:AddTransition("*", "JammerDeactivated", "*") self._jammerHasTransitions = true end self:I(string.format("%sJammer AI configured | loadout=%s", self.lid, loadout or "1xALQ99_2xALQ249")) return self end --- [Internal] Core jamming probability curve (v9 / v8.1 engine). -- Features: decoupled bt_mod/range_mod, tapering residual floor, ±10% jitter. -- @param #MANTIS self -- @param #number d Distance in nautical miles -- @param #table params {peak, mu, sigma_L, tail_dist, band, floor} -- @param #string loadoutKey Key into JammerLoadouts -- @return #number Jamming probability as fraction (0.0-0.95) function MANTIS:_JamGaussianExp(d, params, loadoutKey) if d < 0 or d > 200 then return 0 end if not params then return 0 end local cfg = self.JammerLoadouts[loadoutKey] if not cfg then return 0 end local peak, mu, sigma_L, tail_dist, band = params.peak, params.mu, params.sigma_L, params.tail_dist, params.band local floor = params.floor or 0 local bm if band == "LOW" then bm = cfg.mult_LOW elseif band == "S" then bm = cfg.mult_S elseif band == "IJ" then bm = cfg.mult_IJ else bm = cfg.mult_OPT end -- Decoupled window modifiers local eff_sigma_L = sigma_L / cfg.bt_mod local eff_tail_dist = tail_dist * cfg.range_mod local eff_peak = math.min(95, peak * bm) local raw if d < mu then raw = eff_peak * math.exp(-0.5 * ((d - mu) / eff_sigma_L) ^ 2) else local lambda = math.log(100.0) / eff_tail_dist raw = eff_peak * math.exp(-lambda * (d - mu)) end -- Tapering residual floor (v9): floor full inside engagement band (d <= mu), -- attenuates with R^2-like falloff past mu over 2.5x the main decay distance. -- Prevents artificial hard cliff at 200nm cap; matches real radar/jammer physics. local eff_floor = floor * bm if eff_floor > 0 and d > mu then local floorLambda = math.log(100.0) / (eff_tail_dist * 2.5) eff_floor = eff_floor * math.exp(-floorLambda * (d - mu)) end raw = math.max(eff_floor, raw) -- Stochastic jitter ±10% applied AFTER floor (v9), so residual also varies. -- Prevents gameable flat long-range value. Reflects S/N, RCS, atmospheric fluctuation. local jitter = 1.0 + (math.random() * 2 - 1) * (self.JammerJitterPercent or 0.10) raw = raw * jitter return math.max(0, math.min(95, raw)) / 100.0 end --- [Internal] Build cached sorted key lists for the resolver. -- Called once on first use. Avoids re-scanning/re-sorting JammerSAMParams every cycle. -- @param #MANTIS self -- @return #MANTIS self function MANTIS:_BuildJammerKeyCache() self._jammerSortedHDS = {} self._jammerSortedSMA = {} self._jammerSortedCHM = {} self._jammerSortedBase = {} for key, _ in pairs(self.JammerSAMParams) do if string.find(key, "HDS", 1, true) then table.insert(self._jammerSortedHDS, key) elseif string.find(key, "SMA", 1, true) then table.insert(self._jammerSortedSMA, key) elseif string.find(key, "CHM", 1, true) then table.insert(self._jammerSortedCHM, key) else table.insert(self._jammerSortedBase, key) end end local byLenDesc = function(a, b) return #a > #b end table.sort(self._jammerSortedHDS, byLenDesc) table.sort(self._jammerSortedSMA, byLenDesc) table.sort(self._jammerSortedCHM, byLenDesc) table.sort(self._jammerSortedBase, byLenDesc) self._jammerResolverCache = {} return self end --- [Internal] Resolve a SAM group name to its jammer parameters. -- Uses cached sorted key lists and per-name memoization for performance. -- Substring matching with digit-boundary protection prevents false matches -- (e.g. "SA-22 Pantsir" no longer matches "SA-2"). -- @param #MANTIS self -- @param #string grpname The SAM group name -- @return #table params or nil function MANTIS:_ResolveJammerParams(grpname) if not grpname then return nil end -- Lazy-build cache on first use if not self._jammerSortedBase then self:_BuildJammerKeyCache() end -- Memoization: same group name resolves to same params (params don't change at runtime) local cached = self._jammerResolverCache[grpname] if cached ~= nil then if cached == false then return nil end -- false sentinel = "no match" return cached end -- Helper: check key match with digit-boundary protection local function safeMatch(name, key) local startPos, endPos = string.find(name, key, 1, true) if not startPos then return false end local nextChar = string.sub(name, endPos + 1, endPos + 1) if nextChar == "" then return true end if string.match(nextChar, "%d") then return false end return true end -- Detect mod type and select pre-sorted key list local keyList if string.find(grpname, "HDS", 1, true) then keyList = self._jammerSortedHDS elseif string.find(grpname, "SMA", 1, true) then keyList = self._jammerSortedSMA elseif string.find(grpname, "CHM", 1, true) then keyList = self._jammerSortedCHM end -- First pass: mod-specific keys if keyList then for i = 1, #keyList do if safeMatch(grpname, keyList[i]) then local params = self.JammerSAMParams[keyList[i]] self._jammerResolverCache[grpname] = params return params end end end -- Second pass: base keys local baseKeys = self._jammerSortedBase for i = 1, #baseKeys do if safeMatch(grpname, baseKeys[i]) then local params = self.JammerSAMParams[baseKeys[i]] self._jammerResolverCache[grpname] = params return params end end self._jammerResolverCache[grpname] = false -- cache "no match" too return nil end --- [Internal] Count table entries function MANTIS:_CountTable(t) local c = 0 for _ in pairs(t) do c = c + 1 end return c end --- [Internal] Iterate any supported SET type, calling fn(unit, group) for each alive unit. -- Supports SET_GROUP (via ForEachGroupAlive) and SET_CLIENT/SET_PLAYER (via ForEachClient). -- Note: callback receives ALL alive units — caller must check InAir() for snapshot logic. -- Menu setup happens for ground units too so clients see the menu before takeoff. -- @param #MANTIS self -- @param set The SET object (SET_GROUP, SET_CLIENT, or SET_PLAYER) -- @param #function fn Callback receiving (unit, group) -- @return #MANTIS self function MANTIS:_IterateJammerSet(set, fn) if not set then return self end if type(set.ForEachClient) == "function" then set:ForEachClient(function(client) if not client then return end local unit = client:GetClientGroupUnit() if not unit or not unit:IsAlive() then return end local group = unit:GetGroup() if not group then return end fn(unit, group) end) elseif type(set.ForEachGroupAlive) == "function" then set:ForEachGroupAlive(function(group) if not group then return end local units = group:GetUnits() if not units then return end for _, unit in pairs(units) do if unit and unit:IsAlive() then fn(unit, group) end end end) else self:E(self.lid .. "ERROR: jammer set is not SET_GROUP, SET_CLIENT, or SET_PLAYER (no ForEachGroupAlive or ForEachClient method).") end return self end --- [Internal] Update jammer aircraft states and build snapshot. -- @param #MANTIS self -- @return #MANTIS self function MANTIS:_UpdateJammers() if not self._jammerEnabled then return self end self._jammerSnapshot = {} -- Process client aircraft if self._jammerClientSet then self:_IterateJammerSet(self._jammerClientSet, function(unit, group) local unitName = unit:GetName() if not self._jammerAircraft[unitName] then self._jammerAircraft[unitName] = { loadout = self._jammerDefaultLoadout, active = false, isClient = unit:IsPlayer(), hasLoadout = false, } else -- Re-check IsPlayer in case a human took the slot after first detection if not self._jammerAircraft[unitName].isClient and unit:IsPlayer() then self._jammerAircraft[unitName].isClient = true end end -- Setup menu for clients (works on ground or airborne) if self._jammerAircraft[unitName].isClient and not self._jammerMenusBuilt[unitName] then self:_SetupJammerMenu(unit, group) end -- Snapshot only includes airborne armed jammers local state = self._jammerAircraft[unitName] if state.active and unit:InAir() then local coord = unit:GetCoordinate() if coord then table.insert(self._jammerSnapshot, { coord = coord, loadout = state.loadout, name = unitName }) end end end) end -- Process AI aircraft for _, aiEntry in ipairs(self._jammerAISets or {}) do self:_IterateJammerSet(aiEntry.set, function(unit, group) local unitName = unit:GetName() if not self._jammerAircraft[unitName] then self._jammerAircraft[unitName] = { loadout = aiEntry.loadout, active = false, isClient = false, hasLoadout = true, } end -- Snapshot only includes airborne armed jammers local state = self._jammerAircraft[unitName] if state.active and unit:InAir() then local coord = unit:GetCoordinate() if coord then table.insert(self._jammerSnapshot, { coord = coord, loadout = state.loadout, name = unitName }) end end end) end -- Clean up dead units only (keep ground-bound clients so menus persist) -- Respawn detection: when client respawns, slot may briefly disappear then reappear local toRemove = {} for unitName, state in pairs(self._jammerAircraft) do local unit = UNIT:FindByName(unitName) if not unit or not unit:IsAlive() then table.insert(toRemove, unitName) end end for _, unitName in ipairs(toRemove) do if self._jammerAircraft[unitName] and self._jammerAircraft[unitName].active then self:__JammerDeactivated(1, unitName) end self._jammerAircraft[unitName] = nil self._jammerMenusBuilt[unitName] = nil end return self end --- [Internal] Compute which SAMs are jammed this cycle. -- @param #MANTIS self -- @return #MANTIS self function MANTIS:_ComputeJammedSAMs() self._jammedSAMs = {} if not self._jammerEnabled then return self end if #self._jammerSnapshot == 0 then return self end local M_TO_NM = 1.0 / 1852.0 local allSAMs = {} local tables = self.automode and { self.SAM_Table_Long, self.SAM_Table_Medium, self.SAM_Table_Short, self.SAM_Table_PointDef } or { self.SAM_Table } for _, samTable in ipairs(tables) do for _, _data in pairs(samTable) do if not allSAMs[_data[1]] then allSAMs[_data[1]] = _data[2] end end end for samName, samCoord in pairs(allSAMs) do local params = self:_ResolveJammerParams(samName) if params then local survival = 1.0 for _, jammer in ipairs(self._jammerSnapshot) do local distNM = samCoord:Get2DDistance(jammer.coord) * M_TO_NM local pJam = self:_JamGaussianExp(distNM, params, jammer.loadout) if pJam > 0 then survival = survival * (1.0 - pJam) end end local combinedProb = 1.0 - survival if combinedProb > 0 and math.random() < combinedProb then self._jammedSAMs[samName] = combinedProb end end end -- Cycle debug logging (mirrors JammerDebug output, gated on MANTIS debug/verbose flags) if self.debug or self.verbose then local activeCount = #(self._jammerSnapshot or {}) local jamCount = self:_CountTable(self._jammedSAMs) -- Only log if there's something interesting to report if activeCount > 0 or jamCount > 0 then local lines = {} table.insert(lines, string.format("%sJammer cycle: %d active aircraft, %d SAMs jammed", self.lid, activeCount, jamCount)) -- List active jammers for _, jammer in ipairs(self._jammerSnapshot) do local cfg = self.JammerLoadouts[jammer.loadout] table.insert(lines, string.format(" ACTIVE: %s | loadout=%s", jammer.name, cfg and cfg.name or jammer.loadout)) end -- List jammed SAMs for samName, prob in pairs(self._jammedSAMs) do table.insert(lines, string.format(" JAMMED: %s @ %.0f%%", samName, prob * 100)) end local text = table.concat(lines, "\n") self:I(text) if self.debug then MESSAGE:New(text, 10, "MANTIS"):ToAll() end end end return self end --- [Internal] Set up tiered F10 jammer menu for a player unit. -- @param #MANTIS self -- @param Wrapper.Unit#UNIT unit -- @param Wrapper.Group#GROUP group -- @return #MANTIS self function MANTIS:_SetupJammerMenu(unit, group) local unitName = unit:GetName() local groupName = group:GetName() if self._jammerMenusBuilt[unitName] then return self end self._jammerMenusBuilt[unitName] = true -- If this group already has a menu tree (e.g. respawn), remove it first if self._jammerGroupMenus and self._jammerGroupMenus[groupName] then self._jammerGroupMenus[groupName]:Remove() self._jammerGroupMenus[groupName] = nil end self._jammerGroupMenus = self._jammerGroupMenus or {} local rootMenu = MENU_GROUP:New(group, "Jammer Controls") self._jammerGroupMenus[groupName] = rootMenu -- Tiered loadout submenus local alq99Menu = MENU_GROUP:New(group, "ALQ-99 Loadouts", rootMenu) local alq249Menu = MENU_GROUP:New(group, "ALQ-249 Loadouts", rootMenu) local mixedMenu = MENU_GROUP:New(group, "Mixed Loadouts", rootMenu) local tierMenus = { ALQ99 = alq99Menu, ALQ249 = alq249Menu, Mixed = mixedMenu } for tierName, keys in pairs(self.JammerLoadoutTiers) do local parentMenu = tierMenus[tierName] for _, loadoutKey in ipairs(keys) do local cfg = self.JammerLoadouts[loadoutKey] if cfg then MENU_GROUP_COMMAND:New(group, cfg.name, parentMenu, self._JammerMenuSetLoadout, self, unitName, loadoutKey, group, rootMenu) end end end MESSAGE:New("JAMMER ONLINE\nSelect a loadout from the Jammer Controls menu.", 15, "JAMMER"):ToGroup(group) return self end --- [Internal] F10 callback: Set loadout and show Music toggle function MANTIS:_JammerMenuSetLoadout(unitName, loadoutKey, group, rootMenu) local state = self._jammerAircraft[unitName] if not state then return end state.loadout = loadoutKey state.hasLoadout = true local cfg = self.JammerLoadouts[loadoutKey] MESSAGE:New(string.format("LOADOUT SELECTED: %s\n%s", cfg and cfg.name or loadoutKey, cfg and cfg.description or ""), 12, "JAMMER"):ToGroup(group) -- Add Music toggle if not already present if not state._musicMenuAdded then state._musicMenuAdded = true state._musicOnMenu = nil state._musicOffMenu = nil self:_ShowMusicOn(unitName, group, rootMenu) end end --- [Internal] Show "Music On" menu entry (jammer is OFF, click to turn ON) function MANTIS:_ShowMusicOn(unitName, group, rootMenu) local state = self._jammerAircraft[unitName] if not state then return end if state._musicOffMenu then state._musicOffMenu:Remove() state._musicOffMenu = nil end state._musicOnMenu = MENU_GROUP_COMMAND:New(group, "Music On", rootMenu, function() local s = self._jammerAircraft[unitName] if not s then return end s.active = true MESSAGE:New(string.format("JAMMER ACTIVE\n%s", self.JammerLoadouts[s.loadout] and self.JammerLoadouts[s.loadout].name or s.loadout), 10, "JAMMER"):ToGroup(group) self:__JammerActivated(1, unitName, s.loadout) self:_ShowMusicOff(unitName, group, rootMenu) end) end --- [Internal] Show "Music Off" menu entry (jammer is ON, click to turn OFF) function MANTIS:_ShowMusicOff(unitName, group, rootMenu) local state = self._jammerAircraft[unitName] if not state then return end if state._musicOnMenu then state._musicOnMenu:Remove() state._musicOnMenu = nil end state._musicOffMenu = MENU_GROUP_COMMAND:New(group, "Music Off", rootMenu, function() local s = self._jammerAircraft[unitName] if not s then return end s.active = false MESSAGE:New("JAMMER SAFE", 10, "JAMMER"):ToGroup(group) self:__JammerDeactivated(1, unitName) self:_ShowMusicOn(unitName, group, rootMenu) end) end --- On After "JammerSuppression" event function MANTIS:onafterJammerSuppression(From, Event, To, Group, Name, Probability) self:T({From, Event, To, Name, Probability}) return self end --- On After "JammerActivated" event function MANTIS:onafterJammerActivated(From, Event, To, UnitName, Loadout) self:T({From, Event, To, UnitName, Loadout}) return self end --- On After "JammerDeactivated" event function MANTIS:onafterJammerDeactivated(From, Event, To, UnitName) self:T({From, Event, To, UnitName}) return self end --- Get table of currently jammed SAM group names. -- @param #MANTIS self -- @return #table {samName = probability} function MANTIS:GetJammedSAMs() return self._jammedSAMs or {} end --- Get number of active jammer aircraft. -- @param #MANTIS self -- @return #number function MANTIS:GetActiveJammerCount() return self._jammerSnapshot and #self._jammerSnapshot or 0 end --- Print a debug report of the jammer system to dcs.log and screen. -- Lists registered aircraft, their states, snapshot count, and currently -- jammed SAMs with their probabilities. Use this to diagnose menu issues, -- missing aircraft, or unexpected SAM matching. -- @param #MANTIS self -- @param #boolean toScreen If true, also display report on screen (default false) -- @return #MANTIS self function MANTIS:JammerDebug(toScreen) local lines = {} table.insert(lines, "=== MANTIS JAMMER DEBUG REPORT ===") table.insert(lines, string.format("Enabled: %s | Default loadout: %s", tostring(self._jammerEnabled), tostring(self._jammerDefaultLoadout))) table.insert(lines, string.format("Has client set: %s | AI sets: %d", tostring(self._jammerClientSet ~= nil), #(self._jammerAISets or {}))) -- Aircraft state local count = 0 for unitName, state in pairs(self._jammerAircraft or {}) do count = count + 1 table.insert(lines, string.format(" [%s] active=%s isClient=%s loadout=%s hasLoadout=%s", unitName, tostring(state.active), tostring(state.isClient), tostring(state.loadout), tostring(state.hasLoadout))) end table.insert(lines, string.format("Total tracked aircraft: %d", count)) table.insert(lines, string.format("Active jammer snapshot: %d aircraft airborne+armed", #(self._jammerSnapshot or {}))) -- Jammed SAMs local jcount = 0 for samName, prob in pairs(self._jammedSAMs or {}) do jcount = jcount + 1 table.insert(lines, string.format(" JAMMED: %s @ %.0f%%", samName, prob * 100)) end table.insert(lines, string.format("Currently jammed SAMs: %d", jcount)) table.insert(lines, "=== END REPORT ===") local report = table.concat(lines, "\n") self:I(report) if toScreen then MESSAGE:New(report, 30, "JAMMER DEBUG"):ToAll() end return self end --- Test the SAM resolver against a hypothetical group name. -- Useful for verifying naming convention before mission start. -- @param #MANTIS self -- @param #string testName The hypothetical SAM group name to test -- @return #string Matched JammerSAMParams key, or "NO MATCH" function MANTIS:JammerTestResolver(testName) local params = self:_ResolveJammerParams(testName) if not params then self:I(string.format("[JammerTestResolver] '%s' -> NO MATCH", testName)) return "NO MATCH" end -- Find which key matched for key, p in pairs(self.JammerSAMParams) do if p == params then self:I(string.format("[JammerTestResolver] '%s' -> '%s' {peak=%d, mu=%d, band=%s, floor=%d}", testName, key, params.peak, params.mu, params.band, params.floor or 0)) return key end end return "MATCHED (key unknown)" end --- Manually set a jammer aircraft loadout (AI scripted control). -- @param #MANTIS self -- @param #string unitName DCS unit name -- @param #string loadoutKey Key into JammerLoadouts -- @return #MANTIS self function MANTIS:SetJammerLoadout(unitName, loadoutKey) if self._jammerAircraft and self._jammerAircraft[unitName] then self._jammerAircraft[unitName].loadout = loadoutKey self._jammerAircraft[unitName].hasLoadout = true end return self end --- Manually activate/deactivate a jammer aircraft (AI scripted control). -- @param #MANTIS self -- @param #string unitName DCS unit name -- @param #boolean active true=on, false=off -- @return #MANTIS self function MANTIS:SetJammerActive(unitName, active) if self._jammerAircraft and self._jammerAircraft[unitName] then local wasActive = self._jammerAircraft[unitName].active self._jammerAircraft[unitName].active = active if active and not wasActive then self:__JammerActivated(1, unitName, self._jammerAircraft[unitName].loadout) elseif not active and wasActive then self:__JammerDeactivated(1, unitName) end end return self end ----------------------------------------------------------------------- -- MANTIS Jammer Extension - Hook Overrides ----------------------------------------------------------------------- -- Guard: only save originals once (prevents infinite recursion if file loaded twice) if not MANTIS._CheckLoopOriginal then MANTIS._CheckLoopOriginal = MANTIS._CheckLoop end if not MANTIS._onbeforeStatusOriginal then MANTIS._onbeforeStatusOriginal = MANTIS.onbeforeStatus end --- [Internal] Override: _CheckLoop with jammer suppression. function MANTIS:_CheckLoop(samset, detset, dlink, limit) local r, g, s = self:_CheckLoopOriginal(samset, detset, dlink, limit) if self._jammerEnabled and self._jammedSAMs then for _, _data in pairs(samset) do local name = _data[1] 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 samgroup:EnableEmission(false) else samgroup:OptionAlarmStateGreen() end self.SamStateTracker[name] = "GREEN" self:__JammerSuppression(1, samgroup, name, self._jammedSAMs[name]) if self.ShoradLink then local Shorad = self.Shorad local shoradradius = self.checkradius local ontime = self.ShoradTime Shorad:WakeUpShorad(name, shoradradius, ontime, nil, true) self:__ShoradActivated(1, name, shoradradius, ontime) end if self.debug or self.verbose then self:T(string.format("%sJAMMED: %s forced GREEN (%.1f%%)", self.lid, name, self._jammedSAMs[name] * 100)) end end end end end return r, g, s end --- [Internal] Override: onbeforeStatus with jammer update. function MANTIS:onbeforeStatus(From, Event, To) if self._jammerEnabled then self:_UpdateJammers() self:_ComputeJammedSAMs() end return self:_onbeforeStatusOriginal(From, Event, To) end end ----------------------------------------------------------------------- -- MANTIS end -----------------------------------------------------------------------