mirror of
https://github.com/FlightControl-Master/MOOSE.git
synced 2026-07-21 06:04:49 +00:00
338ec34604
Added ships to MANTIS. NAVYGROUPs may be integrated. Need to be aware MANTIS doesn't change ROE. SEAD does not (by default) effect ships. Ships are individually identified, not as group. Group will react to jamming as a whole, will go live/sleep based upon longest range/best radar. No longer need prefixes for either ground or naval units.
4358 lines
204 KiB
Lua
4358 lines
204 KiB
Lua
--- **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" },
|
|
}
|
|
|
|
|
|
--- @type MANTIS.SamDataNaval
|
|
MANTIS.SamDataNaval = {
|
|
---------------------------------------------------------------------
|
|
-- CurrentHill (CH) surface combatants
|
|
---------------------------------------------------------------------
|
|
-- USA -- AEGIS / SM-family, S-band (SPY-1) or S-band AESA (SPY-6)
|
|
["Ticonderoga"] = { Range=150, Blindspot=4, Height=33, Type="Long", Radar="CH_Ticonderoga" },
|
|
["Ticonderoga CMP"] = { Range=150, Blindspot=4, Height=33, Type="Long", Radar="CH_Ticonderoga_CMP" },
|
|
["Arleigh Burke IIA"] = { Range=150, Blindspot=4, Height=33, Type="Long", Radar="CH_Arleigh_Burke_IIA" },
|
|
["Arleigh Burke III"] = { Range=180, Blindspot=3, Height=33, Type="Long", Radar="CH_Arleigh_Burke_III" },
|
|
["Constellation"] = { Range=120, Blindspot=3, Height=27, Type="Long", Radar="CH_Constellation" },
|
|
|
|
-- UK
|
|
["Type 45"] = { Range=120, Blindspot=3, Height=30, Type="Long", Radar="Type45" }, -- Aster 15/30, SAMPSON
|
|
["Type 26"] = { Range=25, Blindspot=1, Height=10, Type="Medium", Radar="CH_Type26" }, -- Sea Ceptor / CAMM
|
|
|
|
-- Germany
|
|
["F124 Sachsen"] = { Range=120, Blindspot=3, Height=24, Type="Long", Radar="CH_F124" }, -- SM-2/ESSM, APAR (X-band)
|
|
|
|
-- China
|
|
["Type 052D"] = { Range=100, Blindspot=5, Height=27, Type="Long", Radar="Type052D" }, -- HHQ-9
|
|
["Type 055"] = { Range=150, Blindspot=5, Height=30, Type="Long", Radar="Type055" }, -- HHQ-9B
|
|
["Type 054B"] = { Range=70, Blindspot=2, Height=20, Type="Medium", Radar="CH_Type054B" }, -- HHQ-16
|
|
["Type 056A"] = { Range=9, Blindspot=0.3, Height=6, Type="Point", Radar="CH_Type056A", Point="true" }, -- HHQ-10 (IR)
|
|
|
|
-- Russia
|
|
["Admiral Gorshkov"] = { Range=150, Blindspot=3, Height=30, Type="Long", Radar="CH_Admiral_Gorshkov" }, -- Poliment-Redut 9M96D
|
|
["Grigorovich AShM"] = { Range=50, Blindspot=2, Height=15, Type="Medium", Radar="CH_Grigorovich_AShM" }, -- Shtil-1
|
|
["Grigorovich LACM"] = { Range=50, Blindspot=2, Height=15, Type="Medium", Radar="CH_Grigorovich_LACM" },
|
|
["Steregushchiy"] = { Range=50, Blindspot=1.5, Height=20, Type="Medium", Radar="CH_Steregushchiy" }, -- Redut 9M96
|
|
["Gremyashchiy AShM"] = { Range=50, Blindspot=1.5, Height=20, Type="Medium", Radar="CH_Gremyashchiy_AShM" },
|
|
["Gremyashchiy LACM"] = { Range=50, Blindspot=1.5, Height=20, Type="Medium", Radar="CH_Gremyashchiy_LACM" },
|
|
["Karakurt AShM"] = { Range=20, Blindspot=1, Height=15, Type="Point", Radar="CH_Karakurt_AShM", Point="true" }, -- Pantsir-M
|
|
["Karakurt LACM"] = { Range=20, Blindspot=1, Height=15, Type="Point", Radar="CH_Karakurt_LACM", Point="true" },
|
|
|
|
-- Point-defense / MANPADS-only hulls (treated as close-in; also usable as EWR pickets)
|
|
["Type 022 FAC"] = { Range=5, Blindspot=0.2, Height=4, Type="Point", Radar="CH_Type022", Point="true" },
|
|
["IRGCN FAC AShM"] = { Range=5, Blindspot=0.2, Height=4, Type="Point", Radar="CH_IranFAC_MG", Point="true" }, -- MANPADS
|
|
["IRGCN FAC Igla"] = { Range=5, Blindspot=0.2, Height=4, Type="Point", Radar="CH_IranFAC_MG_AShM", Point="true" }, -- Igla
|
|
["Strb 90 FAC"] = { Range=4, Blindspot=0.2, Height=3, Type="Point", Radar="Strb90", Point="true" },
|
|
["Visby"] = { Range=4, Blindspot=0.2, Height=3, Type="Point", Radar="HSwMS_Visby", Point="true" }, -- gun/CIWS
|
|
|
|
---------------------------------------------------------------------
|
|
-- Base-game DCS surface combatants (type ids from pydcs / DCS export)
|
|
-- Only air-defence-capable hulls are listed. Pure-gun / no-AD hulls
|
|
-- (Leander gun refits, La Combattante, cargo, subs, WWII) are omitted
|
|
-- on purpose; add them as POINT if you want them to appear at all.
|
|
---------------------------------------------------------------------
|
|
-- USA
|
|
["CG Ticonderoga"] = { Range=120, Blindspot=3, Height=30, Type="Long", Radar="TICONDEROG" }, -- SM-2/SPY-1 NOTE: id has NO trailing 'E'
|
|
["DDG Arleigh Burke IIa"] = { Range=120, Blindspot=3, Height=30, Type="Long", Radar="USS_Arleigh_Burke_IIa" }, -- SM-2/SPY-1D
|
|
["FFG Perry"] = { Range=38, Blindspot=2, Height=15, Type="Medium", Radar="PERRY" }, -- SM-1MR (est. real range)
|
|
["CVN Roosevelt"] = { Range=15, Blindspot=1, Height=8, Type="Point", Radar="CVN_71", Point="true" }, -- NSSM/RAM
|
|
["CVN Lincoln"] = { Range=15, Blindspot=1, Height=8, Type="Point", Radar="CVN_72", Point="true" },
|
|
["CVN Washington"] = { Range=15, Blindspot=1, Height=8, Type="Point", Radar="CVN_73", Point="true" },
|
|
["CVN Stennis"] = { Range=15, Blindspot=1, Height=8, Type="Point", Radar="Stennis", Point="true" },
|
|
["CVN Truman"] = { Range=15, Blindspot=1, Height=8, Type="Point", Radar="CVN_75", Point="true" },
|
|
["CVN Vinson"] = { Range=15, Blindspot=1, Height=8, Type="Point", Radar="VINSON", Point="true" },
|
|
["LHA Tarawa"] = { Range=15, Blindspot=1, Height=8, Type="Point", Radar="LHA_Tarawa", Point="true" }, -- RAM/Sea Sparrow
|
|
-- Russia
|
|
["Cruiser Moskva"] = { Range=75, Blindspot=5, Height=25, Type="Long", Radar="MOSCOW" }, -- S-300F (SA-N-6)
|
|
["BC Pyotr Velikiy"] = { Range=150, Blindspot=5, Height=27, Type="Long", Radar="PIOTR" }, -- S-300F/FM
|
|
["Frigate Neustrashimy"] = { Range=12, Blindspot=1, Height=6, Type="Short", Radar="NEUSTRASH" }, -- Kinzhal (SA-N-9)
|
|
["Frigate Rezky"] = { Range=15, Blindspot=1, Height=5, Type="Short", Radar="REZKY" }, -- OSA-M (SA-N-4)
|
|
["Corvette Grisha"] = { Range=15, Blindspot=1, Height=5, Type="Short", Radar="ALBATROS" }, -- OSA-M (SA-N-4)
|
|
["Corvette Molniya"] = { Range=5, Blindspot=0.3, Height=3, Type="Point", Radar="MOLNIYA", Point="true" }, -- Strela
|
|
["CV Kuznetsov"] = { Range=12, Blindspot=1, Height=6, Type="Short", Radar="KUZNECOW" }, -- Kinzhal/Kashtan
|
|
["CV Kuznetsov 2017"] = { Range=12, Blindspot=1, Height=6, Type="Short", Radar="CV_1143_5" },
|
|
["Patrol Bykov TorM2KM"] = { Range=15, Blindspot=1, Height=6, Type="Short", Radar="CHAP_Project22160_TorM2KM" }, -- Tor
|
|
["Patrol Bykov"] = { Range=7, Blindspot=0.3, Height=4, Type="Point", Radar="CHAP_Project22160", Point="true" },
|
|
-- China
|
|
["Type 052C"] = { Range=100, Blindspot=5, Height=27, Type="Long", Radar="Type_052C" }, -- HHQ-9
|
|
["Type 052B"] = { Range=30, Blindspot=2, Height=15, Type="Medium", Radar="Type_052B" }, -- HHQ-16/Shtil
|
|
["Type 054A"] = { Range=45, Blindspot=2, Height=18, Type="Medium", Radar="Type_054A" }, -- HHQ-16
|
|
["Type 071 LPD"] = { Range=9, Blindspot=0.5, Height=6, Type="Point", Radar="Type_071", Point="true" }, -- HHQ-10
|
|
-- UK
|
|
["HMS Invincible"] = { Range=40, Blindspot=2, Height=18, Type="Medium", Radar="hms_invincible" }, -- Sea Dart
|
|
|
|
---------------------------------------------------------------------
|
|
-- desdemicabina-simulation 3.2 (Spanish/Australian navies)
|
|
-- AEGIS hulls: two ship_MK41_SM2 launchers (SM-2, 64 cells) + AEGIS-class
|
|
-- trackers (150km/30km alt, ECM_K 0.5). NOTE: this pack's airWeaponDist=45km
|
|
-- UNDERSTATES the fit -- Range below follows the SM-2 launcher (~100km), the
|
|
-- inverse of the Admiral pack's inflated values.
|
|
---------------------------------------------------------------------
|
|
["HMAS Hobart DDG-39"] = { Range=100, Blindspot=3, Height=30, Type="Long", Radar="DDG39" }, -- AEGIS SPY-1D(V), SM-2
|
|
["F100 Alvaro de Bazan"] = { Range=100, Blindspot=3, Height=30, Type="Long", Radar="F100" }, -- AEGIS SPY-1D, SM-2
|
|
["F105 Cristobal Colon"] = { Range=100, Blindspot=3, Height=30, Type="Long", Radar="F105" }, -- AEGIS SPY-1D, SM-2
|
|
["LHD Canberra L02"] = { Range=3, Blindspot=0.1, Height=2, Type="Point", Radar="L02", Point="true" }, -- 3x Phalanx only
|
|
["LPD Castilla L52"] = { Range=3, Blindspot=0.1, Height=2, Type="Point", Radar="L52", Point="true" }, -- 2x Phalanx (NO_SAM attr)
|
|
["LHD Juan Carlos I L61"] = { Range=1.5, Blindspot=0.1, Height=0.8, Type="Point", Radar="L61", Point="true" }, -- .50cal mounts only
|
|
|
|
---------------------------------------------------------------------
|
|
-- Admiral mod surface combatants
|
|
-- Worked example (USS America LHA-6). Read each field from the ship's
|
|
-- GT def file:
|
|
-- Radar <- GT.Name (the typeName; the essential, non-guessable key)
|
|
-- Range <- GT.airWeaponDist / 1000 (20000 -> 20)
|
|
-- Height <- launcher max_trg_alt / 1000 (15000 -> 15)
|
|
-- Blindspot <- launcher distanceMin / 1000 (400 -> 0.4)
|
|
-- Type <- from armament fit (Sea Sparrow => SHORT)
|
|
---------------------------------------------------------------------
|
|
["USS America LHA-6"] = { Range=20, Blindspot=0.4, Height=15, Type="Short", Radar="USS America LHA-6" }, -- Sea Sparrow + RAM + Phalanx
|
|
|
|
-- IMPORTANT: these Range values come from the actual weapon launcher's distanceMax
|
|
-- / the real system, NOT GT.airWeaponDist (which is unreliable here -- e.g. Blue
|
|
-- Ridge and Henry Kaiser both claim 100-150km with only CIWS/MGs).
|
|
-- LONG -- area SAM
|
|
["HMS Duncan (Type 45)"] = { Range=100, Blindspot=3, Height=30, Type="Long", Radar="HMS Duncan" }, -- Aster/Sea Viper (mod: MK41_SM2)
|
|
["Aquitaine D650 (FREMM)"] = { Range=120, Blindspot=3, Height=27, Type="Long", Radar="Aquitaine D650" }, -- SYLVER Aster 30 (distanceMax 120km)
|
|
["Normandie D651 (FREMM)"] = { Range=120, Blindspot=3, Height=27, Type="Long", Radar="D651_Normandie" }, -- SYLVER Aster 30
|
|
-- MEDIUM
|
|
["MEKO-200HN Hydra"] = { Range=50, Blindspot=2, Height=20, Type="Medium", Radar="MEKO_HN" }, -- MK41 (mod SM-2 / real ESSM)
|
|
["MEKO-200TN Barbaros"] = { Range=50, Blindspot=2, Height=20, Type="Medium", Radar="MEKO_TN" }, -- MK41 + SeaZenith CIWS
|
|
["Sovremenny Adm. Ushakov"] = { Range=46, Blindspot=2, Height=15, Type="Medium", Radar="Admiral Ushakov" }, -- SA-N-7 Gadfly
|
|
-- SHORT
|
|
["Udaloy II Chabanenko"] = { Range=12, Blindspot=1, Height=6, Type="Short", Radar="Udaloy II DDG Admiral Chabanenko" }, -- SA-N-9 Klinok
|
|
["USS San Antonio LPD-17"] = { Range=15, Blindspot=1, Height=8, Type="Short", Radar="USS San Antonio LPD-17" }, -- Sea Sparrow
|
|
-- POINT -- CIWS / RAM / gun / IR only (no area SAM); unjammable in the jammer table
|
|
["USS Oak Hill LSD-51"] = { Range=9, Blindspot=0.5, Height=6, Type="Point", Radar="USS_Oak_Hill LSD-51", Point="true" }, -- RAM + Phalanx
|
|
["USS Blue Ridge LCC-19"] = { Range=3, Blindspot=0.1, Height=2, Type="Point", Radar="USS_Blue_Ridge", Point="true" }, -- Phalanx only
|
|
["Sacramento AOE-1"] = { Range=9, Blindspot=0.5, Height=6, Type="Point", Radar="USS_Support_Ship_Sacramento_AOE-1", Point="true" }, -- RAM + Phalanx
|
|
["Henry Kaiser T-AO-187"] = { Range=1.5, Blindspot=0.1, Height=0.8,Type="Point", Radar="USNS_Henry_Kaiser", Point="true" }, -- .50cal only (negligible)
|
|
["HMS Albion L14"] = { Range=3, Blindspot=0.1, Height=2, Type="Point", Radar="HMS_Albion L14", Point="true" }, -- Phalanx only
|
|
["LPD Foudre"] = { Range=6, Blindspot=0.3, Height=4, Type="Point", Radar="LPD_Foundre", Point="true" }, -- Mistral (IR)
|
|
["Osa-I Missile Boat"] = { Range=4, Blindspot=0.2, Height=3, Type="Point", Radar="OSA_One", Point="true" }, -- AK-230 (NO_SAM)
|
|
["Osa-II Missile Boat"] = { Range=4, Blindspot=0.2, Height=3, Type="Point", Radar="OSA_Two", Point="true" }, -- AK-230
|
|
["IRIS Alvand"] = { Range=4, Blindspot=0.2, Height=3, Type="Point", Radar="IRIS_Alvand", Point="true" }, -- OtoBreda 40mm / 20mm
|
|
}
|
|
|
|
-----------------------------------------------------------------------
|
|
-- MANTIS Jammer Extension v2.0.0
|
|
-- Standoff Jamming (SOJ) aircraft support for MANTIS IADS networks
|
|
-- 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
|
|
|
|
|
|
--- @type MANTIS.JammerSAMParamsNaval
|
|
MANTIS.JammerSAMParamsNaval = {
|
|
-- Calibration note (research pass): high power-aperture warship radars
|
|
-- (SPY-1/SPY-6/SAMPSON/Poliment/HHQ-9B) are modeled HARDER to jam than their
|
|
-- ground peers -- lower peak/floor, mu pushed outward -- reflecting shipboard
|
|
-- prime power, large multi-face arrays, low sidelobes and sensor redundancy.
|
|
-- Documented ECM-weak legacy sets (SPG-51 CW illum, Type 909) slightly easier.
|
|
-- Navalized twins remain anchored to ground counterparts (Shtil=SA-11,
|
|
-- Klinok=Tor, OSA-M=SA-8, S-300F=SA-10). Open sources support ordering, not
|
|
-- exact percentages -- tune in-mission as needed.
|
|
-- USA AEGIS
|
|
["CH_Ticonderoga"] = { peak=17, mu=70, sigma_L=24, tail_dist=115, band="S", floor=2 },
|
|
["CH_Ticonderoga_CMP"] = { peak=17, mu=70, sigma_L=24, tail_dist=115, band="S", floor=2 },
|
|
["CH_Arleigh_Burke_IIA"] = { peak=17, mu=70, sigma_L=24, tail_dist=115, band="S", floor=2 },
|
|
["CH_Arleigh_Burke_III"] = { peak=14, mu=83, sigma_L=28, tail_dist=130, band="S", floor=2 }, -- SPY-6 AESA
|
|
["CH_Constellation"] = { peak=14, mu=63, sigma_L=22, tail_dist=100, band="S", floor=2 }, -- SPY-6(V)3
|
|
-- UK
|
|
["Type45"] = { peak=16, mu=62, sigma_L=24, tail_dist=110, band="S", floor=2 }, -- SAMPSON
|
|
["CH_Type26"] = { peak=18, mu=18, sigma_L=7, tail_dist=40, band="IJ", floor=2 }, -- CAMM active RH
|
|
-- Germany
|
|
["CH_F124"] = { peak=18, mu=62, sigma_L=24, tail_dist=110, band="IJ", floor=2 }, -- APAR X-band
|
|
-- China
|
|
["Type052D"] = { peak=20, mu=55, sigma_L=22, tail_dist=110, band="S", floor=2 },
|
|
["Type055"] = { peak=16, mu=65, sigma_L=24, tail_dist=120, band="S", floor=2 },
|
|
["CH_Type054B"] = { peak=22, mu=38, sigma_L=16, tail_dist=75, band="S", floor=3 },
|
|
["CH_Type056A"] = { peak=8, mu=5, sigma_L=2, tail_dist=12, band="OPT",floor=0 }, -- HHQ-10 IR
|
|
-- Russia
|
|
["CH_Admiral_Gorshkov"] = { peak=16, mu=80, sigma_L=30, tail_dist=145, band="S", floor=2 }, -- Poliment AESA
|
|
["CH_Grigorovich_AShM"] = { peak=50, mu=28, sigma_L=12, tail_dist=58, band="IJ", floor=3 }, -- Shtil-1
|
|
["CH_Grigorovich_LACM"] = { peak=50, mu=28, sigma_L=12, tail_dist=58, band="IJ", floor=3 },
|
|
["CH_Steregushchiy"] = { peak=22, mu=35, sigma_L=15, tail_dist=70, band="S", floor=2 }, -- Redut
|
|
["CH_Gremyashchiy_AShM"] = { peak=22, mu=35, sigma_L=15, tail_dist=70, band="S", floor=2 },
|
|
["CH_Gremyashchiy_LACM"] = { peak=22, mu=35, sigma_L=15, tail_dist=70, band="S", floor=2 },
|
|
["CH_Karakurt_AShM"] = { peak=10, mu=9, sigma_L=4, tail_dist=20, band="IJ", floor=0 }, -- Pantsir-M
|
|
["CH_Karakurt_LACM"] = { peak=10, mu=9, sigma_L=4, tail_dist=20, band="IJ", floor=0 },
|
|
-- CIWS / MANPADS-only hulls -> optical, essentially unjammable (floor 0)
|
|
["CH_Type022"] = { peak=3, mu=3, sigma_L=1, tail_dist=5, band="OPT",floor=0 },
|
|
["CH_IranFAC_MG"] = { peak=3, mu=3, sigma_L=1, tail_dist=5, band="OPT",floor=0 },
|
|
["CH_IranFAC_MG_AShM"] = { peak=3, mu=3, sigma_L=1, tail_dist=5, band="OPT",floor=0 },
|
|
["Strb90"] = { peak=3, mu=3, sigma_L=1, tail_dist=5, band="OPT",floor=0 },
|
|
["HSwMS_Visby"] = { peak=3, mu=3, sigma_L=1, tail_dist=5, band="OPT",floor=0 }, -- gun/CIWS: unjammable
|
|
|
|
-- Base-game DCS hulls (canonical entries; aliases set in the do-block below)
|
|
["TICONDEROG"] = { peak=17, mu=60, sigma_L=22, tail_dist=110, band="S", floor=2 }, -- SPY-1 (NO trailing E)
|
|
["USS_Arleigh_Burke_IIa"] = { peak=17, mu=60, sigma_L=22, tail_dist=110, band="S", floor=2 }, -- SPY-1D
|
|
["PERRY"] = { peak=33, mu=20, sigma_L=9, tail_dist=48, band="IJ", floor=3 }, -- SM-1 / SPG-51 (X-band illum)
|
|
["MOSCOW"] = { peak=32, mu=42, sigma_L=18, tail_dist=90, band="S", floor=3 }, -- S-300F
|
|
["PIOTR"] = { peak=28, mu=70, sigma_L=26, tail_dist=130, band="S", floor=3 }, -- S-300F/FM
|
|
["NEUSTRASH"] = { peak=28, mu=8, sigma_L=3.5,tail_dist=18, band="IJ", floor=2 }, -- Kinzhal
|
|
["REZKY"] = { peak=35, mu=8, sigma_L=3.5,tail_dist=18, band="IJ", floor=3 }, -- OSA-M
|
|
["MOLNIYA"] = { peak=3, mu=3, sigma_L=1, tail_dist=5, band="OPT",floor=0 }, -- Strela
|
|
["KUZNECOW"] = { peak=24, mu=8, sigma_L=3.5,tail_dist=18, band="IJ", floor=2 }, -- Kinzhal/Kashtan
|
|
["CHAP_Project22160_TorM2KM"] = { peak=28, mu=12, sigma_L=5, tail_dist=28, band="IJ", floor=3 }, -- Tor M2KM
|
|
["Type_052C"] = { peak=20, mu=55, sigma_L=22, tail_dist=110, band="S", floor=2 }, -- HHQ-9
|
|
["Type_052B"] = { peak=30, mu=20, sigma_L=10, tail_dist=48, band="S", floor=3 }, -- HHQ-16/Shtil
|
|
["Type_054A"] = { peak=26, mu=28, sigma_L=12, tail_dist=58, band="S", floor=3 }, -- HHQ-16
|
|
["hms_invincible"] = { peak=36, mu=25, sigma_L=11, tail_dist=52, band="IJ", floor=3 }, -- Sea Dart / Type 909
|
|
["CVN_71"] = { peak=8, mu=5, sigma_L=2, tail_dist=12, band="OPT",floor=0 }, -- NSSM/RAM (carrier point)
|
|
|
|
-- Admiral mod -- worked example (USS America LHA-6):
|
|
-- band <- radar band (Sea Sparrow Mk 95 illuminator = X-band => IJ)
|
|
-- floor <- inverse of GT ECM_K hint (ECM_K 0.65 = moderate => floor ~2-3)
|
|
-- mu <- engagement range in nm (20 km ~= 11 nm)
|
|
-- peak/sigma_L/tail_dist <- copy nearest short-IJ analog (Sea Sparrow ~ NASAMS/SA-11 class)
|
|
["USS America LHA-6"] = { peak=22, mu=11, sigma_L=5, tail_dist=25, band="IJ", floor=3 },
|
|
-- desdemicabina AEGIS hulls: SPY-1 family fingerprint (peak 17 / floor 2 / S),
|
|
-- mu scaled to the ~100km SM-2 envelope of these units.
|
|
["DDG39"] = { peak=17, mu=55, sigma_L=22, tail_dist=110, band="S", floor=2 }, -- SPY-1D(V)
|
|
["F100"] = { peak=17, mu=55, sigma_L=22, tail_dist=110, band="S", floor=2 }, -- SPY-1D
|
|
["F105"] = { peak=17, mu=55, sigma_L=22, tail_dist=110, band="S", floor=2 }, -- SPY-1D
|
|
-- Admiral SAM shooters (POINT hulls are aliased to the CIWS OPT entry in the do-block below)
|
|
["HMS Duncan"] = { peak=16, mu=54, sigma_L=22, tail_dist=110, band="S", floor=2 }, -- Aster (ECM_K 0.65)
|
|
["Aquitaine D650"] = { peak=16, mu=65, sigma_L=24, tail_dist=120, band="S", floor=2 }, -- Aster 30 (mod ECM_K 5.0 - see note)
|
|
["D651_Normandie"] = { peak=16, mu=65, sigma_L=24, tail_dist=120, band="S", floor=2 }, -- Aster 30
|
|
["MEKO_HN"] = { peak=22, mu=27, sigma_L=12, tail_dist=55, band="IJ", floor=2 }, -- ESSM/SM-2
|
|
["MEKO_TN"] = { peak=22, mu=27, sigma_L=12, tail_dist=55, band="IJ", floor=2 },
|
|
["Admiral Ushakov"] = { peak=48, mu=25, sigma_L=11, tail_dist=55, band="IJ", floor=3 }, -- SA-N-7 Gadfly
|
|
["Udaloy II DDG Admiral Chabanenko"] = { peak=28, mu=7, sigma_L=3, tail_dist=18, band="IJ", floor=3 }, -- SA-N-9 Klinok
|
|
["USS San Antonio LPD-17"] = { peak=22, mu=8, sigma_L=4, tail_dist=20, band="IJ", floor=3 }, -- Sea Sparrow
|
|
["LPD_Foundre"] = { peak=10, mu=3, sigma_L=2, tail_dist=10, band="OPT",floor=0 }, -- Mistral (IR, near-unjammable)
|
|
}
|
|
|
|
-- Naval jammer aliases: variants that share a hull/radar point to one curve,
|
|
-- so tuning the canonical entry updates all of them (mirrors JammerSAMParams).
|
|
do
|
|
local n = MANTIS.JammerSAMParamsNaval
|
|
-- US carriers + LHA + Type 071: point self-defence (NSSM/RAM/HHQ-10), optical-grade
|
|
n["CVN_72"] = n["CVN_71"]; n["CVN_73"] = n["CVN_71"]; n["CVN_75"] = n["CVN_71"]
|
|
n["Stennis"] = n["CVN_71"]; n["VINSON"] = n["CVN_71"]; n["LHA_Tarawa"] = n["CVN_71"]
|
|
n["Type_071"] = n["CVN_71"]
|
|
-- Grisha shares the SA-N-4 (OSA-M) curve with Rezky
|
|
n["ALBATROS"] = n["REZKY"]
|
|
-- Kuznetsov 2017 variant
|
|
n["CV_1143_5"] = n["KUZNECOW"]
|
|
-- Vasily Bykov base (gun) ~ Strela-grade optical
|
|
n["CHAP_Project22160"] = n["MOLNIYA"]
|
|
-- Admiral mod CIWS / RAM / gun-only hulls: all optical-grade, essentially unjammable.
|
|
-- Aliased to the base OPT curve so they resolve without cluttering the table.
|
|
n["USS_Oak_Hill LSD-51"] = n["MOLNIYA"] -- RAM + Phalanx
|
|
n["USS_Blue_Ridge"] = n["MOLNIYA"] -- Phalanx
|
|
n["USS_Support_Ship_Sacramento_AOE-1"] = n["MOLNIYA"] -- RAM + Phalanx
|
|
n["USNS_Henry_Kaiser"] = n["MOLNIYA"] -- .50cal
|
|
n["HMS_Albion L14"] = n["MOLNIYA"] -- Phalanx
|
|
n["OSA_One"] = n["MOLNIYA"] -- AK-230 (NO_SAM)
|
|
n["OSA_Two"] = n["MOLNIYA"] -- AK-230
|
|
n["IRIS_Alvand"] = n["MOLNIYA"] -- OtoBreda 40mm / 20mm
|
|
-- desdemicabina amphibs: CIWS / gun-only, optical-grade unjammable
|
|
n["L02"] = n["MOLNIYA"] -- 3x Phalanx
|
|
n["L52"] = n["MOLNIYA"] -- 2x Phalanx (NO_SAM)
|
|
n["L61"] = n["MOLNIYA"] -- .50cal only
|
|
end
|
|
|
|
|
|
-----------------------------------------------------------------------
|
|
-- MANTIS System
|
|
-----------------------------------------------------------------------
|
|
|
|
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)
|
|
-- Inject NAVYGROUP:SetMANTIS convenience API (safe if NAVYGROUP not loaded/already done)
|
|
MANTIS._InjectNavyGroupAPI()
|
|
|
|
|
|
-- 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.SEADNaval = false -- ships hard-kill inbound missiles; they do not go EMCON under SEAD (see SetNavalSEAD)
|
|
self.NavalSurfaceWakeup = true -- quiet ships go active when enemy surface combatants close (see SetNavalSurfaceWakeup)
|
|
self.NavalSurfaceWakeupRadius = nil -- meters; nil = each ship uses its own engagement radius
|
|
self.NavalPerUnit = true -- classify & switch every ship unit individually (see SetNavalPerUnit)
|
|
self.NavalAutonomy = true -- ship groups outside EWR radar coverage go alarm RED and search autonomously (see SetNavalAutonomy)
|
|
self.NavalAutonomyRadius = 150000 -- meters; a ship group within this distance of an alive EWR group counts as covered
|
|
self.engagerange = 95
|
|
self.autorelocate = false
|
|
self.autorelocateunits = { HQ = false, EWR = false}
|
|
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)
|
|
if self.NavalPerUnit and Group and Group:IsShip() and self._navalSAMs then
|
|
-- Per-unit naval: a hit wakes this group's managed radars individually
|
|
if not self.SuppressedGroups[Name] then
|
|
for _,_unit in pairs(Group:GetUnits() or {}) do
|
|
local uname = _unit:GetName()
|
|
if self._navalSAMs[uname] and self.SamStateTracker[uname] == "GREEN"
|
|
and not (self._jammerEnabled and self._jammedSAMs and self._jammedSAMs[uname]) then
|
|
self.SamStateTracker[uname] = "RED"
|
|
end
|
|
end
|
|
self:_ApplyNavalAlarmState() -- apply group alarm state immediately on hit
|
|
self:__RedState(1,Group)
|
|
end
|
|
return
|
|
end
|
|
local suppressed = self.SuppressedGroups[Name] or false
|
|
local jammed = self._jammerEnabled and self._jammedSAMs and self._jammedSAMs[Name] or false
|
|
if not suppressed and not jammed and self.SamStateTracker[Name] == "GREEN" then
|
|
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:IsShip() then include = false end -- MANTIS is anti-air: surface contacts are not engageable targets
|
|
if grp:GetCoalition() == self.coalition then include = false end
|
|
if coord and SamCoordinate and grp:IsHelicopter() then
|
|
dist = coord:Get2DDistance(SamCoordinate) or 0
|
|
if dist > self.ShoradActDistance then include = false end -- we do not want long range shooting at helos
|
|
end
|
|
if self.debug then
|
|
local text = "Looking at Group: "..grp:GetName() or "N/A"
|
|
text = text .. " Include = "..tostring(include)
|
|
MESSAGE:New(text,10,"MANTIS"):ToAllIf(self.verbose):ToLog()
|
|
end
|
|
local grpalt = grp:GetHeight(true)
|
|
if grpalt < height and grpalt > 10 and include == true then
|
|
table.insert(set,coord)
|
|
end
|
|
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 | Zonecheck %s", samstring, targetdistance/1000, rad/1000, inrange, tostring(zonecheck))
|
|
-- Naval per-unit state summary
|
|
if self._navalSAMs and next(self._navalSAMs) ~= nil then
|
|
local ntot, nred, njam = 0, 0, 0
|
|
local detail = ""
|
|
for uname,_ in pairs(self._navalSAMs) do
|
|
local u = UNIT:FindByName(uname)
|
|
if u and u:IsAlive() then
|
|
ntot = ntot + 1
|
|
local st = self.SamStateTracker[uname] or "GREEN"
|
|
if st == "RED" then nred = nred + 1 end
|
|
local jm = ""
|
|
if self._jammedSAMs and self._jammedSAMs[uname] then njam = njam + 1 jm = " (JAMMED)" end
|
|
local pk = ""
|
|
local parent = self._navalUnitParent and self._navalUnitParent[uname]
|
|
if parent and self._navalAutonomous and self._navalAutonomous[parent] then pk = " [AUTONOMOUS]" end
|
|
if self.verbose then detail = detail..string.format("\n %s: %s%s%s",uname,st,jm,pk) end
|
|
end
|
|
end
|
|
if ntot > 0 then
|
|
text = text..string.format("\nNaval units: %d | RED %d | GREEN %d | jammed %d",ntot,nred,ntot-nred,njam)..detail
|
|
end
|
|
end
|
|
local m = MESSAGE:New(text,10,"Check"):ToAllIf(self.debug)
|
|
self:T(self.lid..text)
|
|
end
|
|
-- 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 ARMCapacity
|
|
-- Ordered multi-table search: the mod-tagged table (if any) first, then all
|
|
-- others. This lets groups classify by radar TYPE NAME alone, so ground SAM
|
|
-- groups no longer need the designation (or even the mod tag) in their name.
|
|
local SearchTables
|
|
if mod then
|
|
SearchTables = { {self.SamDataHDS,"HDS"}, {self.SamData,nil}, {self.SamDataSMA,"SMA"}, {self.SamDataCH,"CHM"} }
|
|
elseif sma then
|
|
SearchTables = { {self.SamDataSMA,"SMA"}, {self.SamData,nil}, {self.SamDataHDS,"HDS"}, {self.SamDataCH,"CHM"} }
|
|
elseif chm then
|
|
SearchTables = { {self.SamDataCH,"CHM"}, {self.SamData,nil}, {self.SamDataHDS,"HDS"}, {self.SamDataSMA,"SMA"} }
|
|
else
|
|
SearchTables = { {self.SamData,nil}, {self.SamDataHDS,"HDS"}, {self.SamDataSMA,"SMA"}, {self.SamDataCH,"CHM"} }
|
|
end
|
|
self:T("Looking to auto-match for "..grpname)
|
|
for _,_unit in pairs(units) do
|
|
local unit = _unit -- Wrapper.Unit#UNIT
|
|
local typename = string.lower(unit:GetTypeName())
|
|
self:T(string.format("Matching typename: %s",typename))
|
|
-- Longest-match wins across ALL tables. This prevents a short Radar key in
|
|
-- one table (e.g. base "TorM2") from shadowing a more specific key in
|
|
-- another (e.g. CH "TorM2K"), and makes the typename path deterministic
|
|
-- (plain pairs() order no longer decides between overlapping keys).
|
|
-- Table order (mod-tagged table first) breaks exact-length ties.
|
|
local bestentry, bestidx, besttag, bestlen = nil, nil, nil, 0
|
|
for _,searchentry in ipairs(SearchTables) do
|
|
local SAMData, modtag = searchentry[1], searchentry[2]
|
|
if SAMData then
|
|
for idx,entry in pairs(SAMData) do
|
|
local _radar = string.lower(entry.Radar)
|
|
if #_radar > bestlen and string.find(typename,_radar,1,true) then
|
|
bestentry, bestidx, besttag, bestlen = entry, idx, modtag, #_radar
|
|
end
|
|
end
|
|
end
|
|
end
|
|
if bestentry then
|
|
local _entry = bestentry -- #MANTIS.SamData
|
|
type = _entry.Type
|
|
radiusscale = self.radiusscale[type]
|
|
range = _entry.Range * 1000 * radiusscale -- max firing range used as switch-on
|
|
height = _entry.Height * 1000 -- max firing height
|
|
blind = _entry.Blindspot * 100 -- blind spot range
|
|
ARMCapacity = _entry.ARMCapacity
|
|
self:T(string.format("Match: %s - %s",string.lower(_entry.Radar),type))
|
|
-- Stash the jammer curve by DESIGNATION (table key), so groups named
|
|
-- without a designation remain jammable. Reuses _ResolveJammerParams
|
|
-- (aliases, digit-boundary matching, mod bucketing) via a synthetic name.
|
|
self._samJammerParams = self._samJammerParams or {}
|
|
if self._samJammerParams[grpname] == nil then
|
|
local synth = bestidx
|
|
if besttag and not string.find(bestidx, besttag, 1, true) then
|
|
synth = bestidx .. " " .. besttag
|
|
end
|
|
local jp = self:_ResolveJammerParams(synth)
|
|
if jp then self._samJammerParams[grpname] = jp end
|
|
end
|
|
found = true
|
|
break
|
|
end
|
|
end
|
|
--- AAA or Point Defense
|
|
if not found then
|
|
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 for naval (ship) groups.
|
|
-- Matches the unit type name exactly against #MANTIS.SamDataNaval and, as a side
|
|
-- effect, stashes the hull's jammer curve in self._samJammerParams[grpname] so the
|
|
-- SOJ jammer extension can suppress it by type name (not group name).
|
|
-- @param #MANTIS self
|
|
-- @param #string grpname Name of the ship group
|
|
-- @return #number range Max firing range (m)
|
|
-- @return #number height Max firing height (m)
|
|
-- @return #string type #MANTIS.SamType
|
|
-- @return #number blind Blind spot
|
|
-- @return #number ARMCapacity
|
|
function MANTIS:_GetNavalSAMData(grpname)
|
|
self:T(self.lid.."_GetNavalSAMData for "..tostring(grpname))
|
|
self._navalSAMs = self._navalSAMs or {}
|
|
self._navalSAMs[grpname] = true
|
|
self._samJammerParams = self._samJammerParams or {}
|
|
local range = self.checkradius
|
|
local height = 3000
|
|
local type = MANTIS.SamType.POINT
|
|
local blind = 0
|
|
local ARMCapacity = 0
|
|
local group = GROUP:FindByName(grpname)
|
|
if not group then
|
|
self._samJammerParams[grpname] = nil
|
|
return range, height, type, blind, ARMCapacity
|
|
end
|
|
local units = group:GetUnits() or {}
|
|
for _,_unit in pairs(units) do
|
|
local typename = string.lower(_unit:GetTypeName())
|
|
for _,entry in pairs(self.SamDataNaval) do
|
|
if typename == string.lower(entry.Radar) then
|
|
type = entry.Type
|
|
local radiusscale = self.radiusscale[type] or 1
|
|
range = entry.Range * 1000 * radiusscale
|
|
height = entry.Height * 1000
|
|
blind = entry.Blindspot
|
|
ARMCapacity = entry.ARMCapacity or 0
|
|
self._samJammerParams[grpname] = self.JammerSAMParamsNaval[entry.Radar]
|
|
self:T(string.format("Naval match: %s -> %s (range %sm)", typename, tostring(type), tostring(range)))
|
|
return range, height, type, blind, ARMCapacity
|
|
end
|
|
end
|
|
end
|
|
-- Unmatched ship: conservative point-defence default (mirrors AAA fallback), no jammer curve.
|
|
self._samJammerParams[grpname] = nil
|
|
self:E(self.lid..string.format("*****Could not match naval radar data for %s! Defaulting to POINT.",grpname))
|
|
return 2000, 2000, MANTIS.SamType.POINT, 50, 0
|
|
end
|
|
|
|
--- [Internal] Classify a naval group PER UNIT. Each hull matching #MANTIS.SamDataNaval
|
|
-- becomes its own SAM-table record keyed by UNIT name, record[8] = parent group name.
|
|
-- @param #MANTIS self
|
|
-- @return #boolean handled True if at least one unit was classified.
|
|
function MANTIS:_BuildNavalUnitEntries(group, grpname, SAM_Tbl, SAM_Tbl_lg, SAM_Tbl_md, SAM_Tbl_sh, SAM_Tbl_pt, SEAD_Grps)
|
|
self:T(self.lid.."_BuildNavalUnitEntries for "..tostring(grpname))
|
|
self._navalSAMs = self._navalSAMs or {}
|
|
self._samJammerParams = self._samJammerParams or {}
|
|
local entries = 0
|
|
local seadadded = false
|
|
local units = group:GetUnits() or {}
|
|
for _,_unit in pairs(units) do
|
|
if _unit and _unit:IsAlive() then
|
|
local typename = string.lower(_unit:GetTypeName())
|
|
for _,entry in pairs(self.SamDataNaval) do
|
|
if typename == string.lower(entry.Radar) then
|
|
local unitname = _unit:GetName()
|
|
local type = entry.Type
|
|
local radiusscale = self.radiusscale[type] or 1
|
|
local range = entry.Range * 1000 * radiusscale
|
|
local height = entry.Height * 1000
|
|
local blind = entry.Blindspot
|
|
local coord = _unit:GetCoordinate()
|
|
local record = {unitname, coord, range, height, blind, type, nil, grpname}
|
|
table.insert( SAM_Tbl, record )
|
|
if type == MANTIS.SamType.LONG then table.insert( SAM_Tbl_lg, record )
|
|
elseif type == MANTIS.SamType.MEDIUM then table.insert( SAM_Tbl_md, record )
|
|
elseif type == MANTIS.SamType.SHORT then table.insert( SAM_Tbl_sh, record )
|
|
elseif type == MANTIS.SamType.POINT then table.insert( SAM_Tbl_pt, record ) end
|
|
self._navalSAMs[unitname] = true
|
|
self._navalUnitParent = self._navalUnitParent or {}
|
|
self._navalUnitParent[unitname] = grpname
|
|
self._samJammerParams[unitname] = self.JammerSAMParamsNaval[entry.Radar]
|
|
if self.SamStateTracker[unitname] == nil then self.SamStateTracker[unitname] = "GREEN" end
|
|
if self.SEADNaval and not seadadded then
|
|
table.insert( SEAD_Grps, grpname ) -- SEAD keys on GROUP names; add once
|
|
seadadded = true
|
|
end
|
|
entries = entries + 1
|
|
self:T(string.format("Naval per-unit match: %s (%s) -> %s",unitname,typename,tostring(type)))
|
|
break
|
|
end
|
|
end
|
|
end
|
|
end
|
|
return entries > 0
|
|
end
|
|
|
|
--- [Internal] Group ALARM-STATE arbiter for per-unit naval control.
|
|
-- DCS reliably honors only GROUP-level alarm state on ships, so per-unit
|
|
-- tracking decides intent and this function applies ONE alarm state per ship
|
|
-- group as the final word of each cycle:
|
|
-- RED if any managed unit is RED and not jammed (threat / hit / surface
|
|
-- wake), or the group is outside EWR radar coverage (autonomous
|
|
-- search, see SetNavalAutonomy) with at least one unjammed unit;
|
|
-- GREEN otherwise -- including full jamming suppression, which overrides
|
|
-- autonomy (a fully jammed group cannot search).
|
|
-- @param #MANTIS self
|
|
-- @return #MANTIS self
|
|
function MANTIS:_ApplyNavalAlarmState()
|
|
if not (self.NavalPerUnit and self._navalUnitParent) then return self end
|
|
local wants, seen, unjammed = {}, {}, {}
|
|
for uname, grpname in pairs(self._navalUnitParent) do
|
|
local u = UNIT:FindByName(uname)
|
|
if u and u:IsAlive() then
|
|
seen[grpname] = true
|
|
local jammed = self._jammerEnabled and self._jammedSAMs and self._jammedSAMs[uname]
|
|
if not jammed then unjammed[grpname] = true end
|
|
if self.SamStateTracker[uname] == "RED" and not jammed then
|
|
wants[grpname] = true
|
|
end
|
|
end
|
|
end
|
|
-- coverage sources: every alive EWR group (incl. EWR-role ships and AWACS ships in the EWR set)
|
|
local sources = {}
|
|
if self.NavalAutonomy then
|
|
-- Managed naval SAM groups cannot be their own coverage: when GREEN their
|
|
-- radars are dark, so a "BOTH"-role group must not self-license through
|
|
-- its EWR-set membership. Only UNMANAGED emitters count as sources:
|
|
-- land EWR, AWACS, and EWR-only ships (never state-switched, always
|
|
-- radiating).
|
|
local managed = {}
|
|
for _, gname in pairs(self._navalUnitParent) do managed[gname] = true end
|
|
self.EWR_Group:ForEachGroupAlive(
|
|
function(grp)
|
|
if not managed[grp:GetName()] then
|
|
local c = grp:GetCoordinate()
|
|
if c then table.insert(sources, c) end
|
|
end
|
|
end)
|
|
end
|
|
self._navalAutonomous = self._navalAutonomous or {}
|
|
self._navalGroupState = self._navalGroupState or {}
|
|
for grpname,_ in pairs(seen) do
|
|
local grp = GROUP:FindByName(grpname)
|
|
if grp and grp:IsAlive() then
|
|
local red = wants[grpname] or false
|
|
local autonomous = false
|
|
if self.NavalAutonomy and unjammed[grpname] and not red then
|
|
local covered = false
|
|
local gc = grp:GetCoordinate()
|
|
if gc then
|
|
for _,c in ipairs(sources) do
|
|
if c:Get2DDistance(gc) <= self.NavalAutonomyRadius then
|
|
covered = true
|
|
break
|
|
end
|
|
end
|
|
end
|
|
self:T(self.lid..string.format("%s EWR coverage: %s (%d unmanaged source(s), radius %dkm)",grpname,tostring(covered),#sources,self.NavalAutonomyRadius/1000))
|
|
if not covered then
|
|
red = true
|
|
autonomous = true
|
|
end
|
|
end
|
|
self._navalAutonomous[grpname] = autonomous or nil
|
|
local newstate = red and "RED" or "GREEN"
|
|
if self._navalGroupState[grpname] ~= newstate then
|
|
self._navalGroupState[grpname] = newstate
|
|
if red then
|
|
grp:OptionAlarmStateRed()
|
|
self:T(self.lid..grpname.." naval group -> alarm RED"..(autonomous and " (autonomous: no EWR coverage)" or ""))
|
|
else
|
|
grp:OptionAlarmStateGreen()
|
|
self:T(self.lid..grpname.." naval group -> alarm GREEN")
|
|
end
|
|
end
|
|
end
|
|
end
|
|
return self
|
|
end
|
|
|
|
|
|
--- [Internal] Function to get SAM firing data
|
|
-- @param #MANTIS self
|
|
-- @param #string grpname Name of the group
|
|
-- @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))
|
|
-- Naval hulls: match by exact unit type name against SamDataNaval and stash the
|
|
-- jammer curve; bypass the ground name-matching below.
|
|
do
|
|
local grp = GROUP:FindByName(grpname)
|
|
if grp and grp:IsShip() then
|
|
return self:_GetNavalSAMData(grpname)
|
|
end
|
|
end
|
|
local range = self.checkradius
|
|
local height = 3000
|
|
local type = MANTIS.SamType.MEDIUM
|
|
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() or _group:IsShip()) and _group:IsAlive() then
|
|
local group = _group -- Wrapper.Group#GROUP
|
|
-- DONE: add emissions on/off
|
|
if group:IsShip() and self.NavalPerUnit then
|
|
-- Per-unit naval control: ships are switched by GROUP ALARM STATE only,
|
|
-- the one mechanism DCS reliably honors on naval groups. Baseline is
|
|
-- GREEN; the alarm arbiter (_ApplyNavalAlarmState) raises the group RED
|
|
-- on threat, hit, surface contact, or loss of EWR coverage.
|
|
group:OptionAlarmStateGreen()
|
|
elseif self.UseEmOnOff then
|
|
group:OptionAlarmStateRed()
|
|
group:EnableEmission(false)
|
|
--group:SetAIOff()
|
|
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()
|
|
if group:IsShip() and self.NavalPerUnit
|
|
and self:_BuildNavalUnitEntries(group, grpname, SAM_Tbl, SAM_Tbl_lg, SAM_Tbl_md, SAM_Tbl_sh, SAM_Tbl_pt, SEAD_Grps) then
|
|
self:T(grpname.." handled as per-unit naval group")
|
|
else
|
|
local grprange,grpheight,type,blind,ARMCapacity = self:_GetSAMRange(grpname)
|
|
if ARMCapacity and ARMCapacity>0 then _group:SetProperty("ARMCapacity",ARMCapacity) end
|
|
table.insert( SAM_Tbl, {grpname, grpcoord, grprange, grpheight, blind, type, ARMCapacity})
|
|
--table.insert( SEAD_Grps, grpname )
|
|
if type == MANTIS.SamType.LONG then
|
|
table.insert( SAM_Tbl_lg, {grpname, grpcoord, grprange, grpheight, blind, type})
|
|
if (not group:IsShip()) or self.SEADNaval then
|
|
table.insert( SEAD_Grps, grpname )
|
|
end
|
|
self:T("SAM "..grpname.." is type LONG")
|
|
elseif type == MANTIS.SamType.MEDIUM then
|
|
table.insert( SAM_Tbl_md, {grpname, grpcoord, grprange, grpheight, blind, type})
|
|
if (not group:IsShip()) or self.SEADNaval then
|
|
table.insert( SEAD_Grps, grpname )
|
|
end
|
|
self:T("SAM "..grpname.." is type MEDIUM")
|
|
elseif type == MANTIS.SamType.SHORT then
|
|
table.insert( SAM_Tbl_sh, {grpname, grpcoord, grprange, grpheight, blind, type})
|
|
if (not group:IsShip()) or self.SEADNaval then
|
|
table.insert( SEAD_Grps, grpname )
|
|
end
|
|
self:T("SAM "..grpname.." is type SHORT")
|
|
elseif type == MANTIS.SamType.POINT then
|
|
table.insert( SAM_Tbl_pt, {grpname, grpcoord, grprange, grpheight, blind, type})
|
|
self:T("SAM "..grpname.." is type POINT")
|
|
if group:IsShip() then
|
|
if self.SEADNaval then
|
|
table.insert( SEAD_Grps, grpname ) -- naval point-defence -> SEAD only when opted in; never SHORAD scoot
|
|
end
|
|
else
|
|
self.ShoradGroupSet:Add(grpname,group)
|
|
if not self.autoshorad then
|
|
table.insert( SEAD_Grps, grpname )
|
|
end
|
|
end
|
|
end
|
|
end -- else: per-unit naval group handled above
|
|
self.SamStateTracker[grpname] = "GREEN"
|
|
end
|
|
end
|
|
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() or group:IsShip()) and group:IsAlive() then
|
|
local grpname = group:GetName()
|
|
local grpcoord = group:GetCoordinate()
|
|
if group:IsShip() and self.NavalPerUnit
|
|
and self:_BuildNavalUnitEntries(group, grpname, SAM_Tbl, SAM_Tbl_lg, SAM_Tbl_md, SAM_Tbl_sh, SAM_Tbl_pt, SEAD_Grps) then
|
|
self:T(grpname.." handled as per-unit naval group")
|
|
else
|
|
local grprange, grpheight,type,blind, ARMCapacity = self:_GetSAMRange(grpname)
|
|
-- TODO the below might stop working at some point after some hours, needs testing
|
|
--local radaralive = group:IsSAM()
|
|
if ARMCapacity and ARMCapacity>0 then _group:SetProperty("ARMCapacity",ARMCapacity) end
|
|
local radaralive = true
|
|
table.insert( SAM_Tbl, {grpname, grpcoord, grprange, grpheight, blind, type, ARMCapacity}) -- make the table lighter, as I don't really use the zone here
|
|
if type ~= MANTIS.SamType.POINT and ((not group:IsShip()) or self.SEADNaval) 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})
|
|
if group:IsShip() then
|
|
if self.SEADNaval then
|
|
table.insert( SEAD_Grps, grpname ) -- naval point-defence -> SEAD only when opted in; never SHORAD scoot
|
|
end
|
|
else
|
|
self.ShoradGroupSet:Add(grpname,group)
|
|
if self.autoshorad then
|
|
self.Shorad.Groupset = self.ShoradGroupSet
|
|
end
|
|
end
|
|
end
|
|
end -- else: per-unit naval group handled above
|
|
end
|
|
end
|
|
self.SAM_Table = SAM_Tbl
|
|
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 navalparent = _data[8] -- per-unit naval entry: name is a UNIT name
|
|
local radius = _data[3]
|
|
local height = _data[4]
|
|
local blind = _data[5] * 1.25 + 1
|
|
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 samunit = nil
|
|
local samgroup = nil
|
|
if navalparent then
|
|
samunit = UNIT:FindByName(name)
|
|
samgroup = (samunit and samunit:GetGroup()) or GROUP:FindByName(navalparent)
|
|
else
|
|
samgroup = GROUP:FindByName(name)
|
|
end
|
|
local samalive = false
|
|
if navalparent then samalive = (samunit ~= nil) and samunit:IsAlive() or false
|
|
elseif samgroup then samalive = samgroup:IsAlive() or false end
|
|
local IsInZone, Distance = self:_CheckObjectInZone(detset, samcoordinate, radius, height, dlink)
|
|
-- Naval surface wake-up: quiet ships activate when enemy surface combatants
|
|
-- close within the trigger radius. Jamming has precedence and holds them down.
|
|
if (not IsInZone) and self.NavalSurfaceWakeup and self._navalSAMs and self._navalSAMs[name]
|
|
and not (self._jammerEnabled and self._jammedSAMs and self._jammedSAMs[name]) then
|
|
local wakeradius = self.NavalSurfaceWakeupRadius or radius
|
|
if self:_EnemySurfaceInRange(samcoordinate, wakeradius) then
|
|
IsInZone = true
|
|
Distance = wakeradius -- keep distance-based proximity behaviors (smoke, SHORAD link) inert
|
|
end
|
|
end
|
|
local suppressed = self.SuppressedGroups[name] or (navalparent and self.SuppressedGroups[navalparent]) or false
|
|
local activeshorad = false
|
|
if self.Shorad and self.Shorad.ActiveGroups and self.Shorad.ActiveGroups[name] then
|
|
activeshorad = true
|
|
end
|
|
if samgroup:GetProperty("SHORAD_ACTIVE") == true and activeshorad == false then activeshorad = true end
|
|
if IsInZone and (not suppressed) and (not activeshorad) then --check any target in zone and not currently managed by SEAD
|
|
if samalive then
|
|
-- switch on SAM
|
|
local switch = false
|
|
if navalparent and switchedon < limit then
|
|
-- per-unit naval: mark THIS unit RED; the group alarm arbiter
|
|
-- (_ApplyNavalAlarmState) applies the group switch at cycle end
|
|
switchedon = switchedon + 1
|
|
switch = true
|
|
elseif self.UseEmOnOff and switchedon < limit then
|
|
-- DONE: add emissions on/off
|
|
samgroup:EnableEmission(true)
|
|
switchedon = switchedon + 1
|
|
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 (not navalparent) and self.SmokeDecoy == true and Distance < self.DetectAccousticRadius*1.5 then
|
|
self:T("Smoking")
|
|
self:_SmokeUnits(samgroup)
|
|
end
|
|
-- link in to SHORAD if available
|
|
-- DONE: Test integration fully
|
|
if self.ShoradLink and (not navalparent) and (Distance < self.ShoradActDistance or Distance < blind ) then -- don't give SHORAD position away too early; ships never SHORAD-link
|
|
local Shorad = self.Shorad --Functional.Shorad#SHORAD
|
|
local radius = self.checkradius
|
|
local ontime = self.ShoradTime
|
|
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 samalive and not suppressed and not activeshorad then
|
|
-- switch off SAM
|
|
if navalparent then
|
|
-- per-unit naval: tracker-only; group alarm state applied by the arbiter
|
|
elseif self.UseEmOnOff then
|
|
samgroup:EnableEmission(false)
|
|
else
|
|
samgroup:OptionAlarmStateGreen()
|
|
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
|
|
|
|
-- apply naval group alarm states as the final word of this cycle
|
|
self:_ApplyNavalAlarmState()
|
|
|
|
if self.debug or self.verbose then
|
|
local statusreport = GetReport()
|
|
MESSAGE:New(statusreport:Text(),10):ToAll():ToLog()
|
|
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 navalparent = _data[8] -- per-unit naval entry: name is a UNIT name
|
|
local samunit = nil
|
|
local samgroup = nil
|
|
if navalparent then
|
|
samunit = UNIT:FindByName(name)
|
|
samgroup = (samunit and samunit:GetGroup()) or GROUP:FindByName(navalparent)
|
|
else
|
|
samgroup = GROUP:FindByName(name)
|
|
end
|
|
local samalive = false
|
|
if navalparent then samalive = (samunit ~= nil) and samunit:IsAlive() or false
|
|
elseif samgroup then samalive = samgroup:IsAlive() or false end
|
|
if samalive then
|
|
if navalparent then
|
|
self.SamStateTracker[name] = "RED" -- arbiter raises the group
|
|
elseif self.UseEmOnOff then
|
|
-- DONE: add emissions on/off
|
|
--samgroup:SetAIOn()
|
|
samgroup:EnableEmission(true)
|
|
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._samJammerParams and self._samJammerParams[samName]) or 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]
|
|
local navalparent = _data[8] -- per-unit naval entry: name is a UNIT name
|
|
if self._jammedSAMs[name] and self.SamStateTracker[name] == "RED" then
|
|
local samunit = nil
|
|
local samgroup = nil
|
|
if navalparent then
|
|
samunit = UNIT:FindByName(name)
|
|
samgroup = (samunit and samunit:GetGroup()) or GROUP:FindByName(navalparent)
|
|
else
|
|
samgroup = GROUP:FindByName(name)
|
|
end
|
|
local samalive = false
|
|
if navalparent then samalive = (samunit ~= nil) and samunit:IsAlive() or false
|
|
elseif samgroup then samalive = samgroup:IsAlive() or false end
|
|
if samalive then
|
|
if navalparent then
|
|
-- per-unit naval: tracker set GREEN below; arbiter darkens the group if no unit remains RED
|
|
elseif self.UseEmOnOff then
|
|
samgroup:EnableEmission(false)
|
|
else
|
|
samgroup:OptionAlarmStateGreen()
|
|
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 Naval Extension: NAVYGROUP integration
|
|
-- Lets Ops.NavyGroup#NAVYGROUP objects (cruise control, missions, carrier ops) serve as
|
|
-- MANTIS-managed SAM shooters and/or EWR pickets:
|
|
-- mymantis:AddNavyGroup(navygroup, "SAM") -- managed shooter (or "EWR" = radiating picket)
|
|
-- navygroup:SetMANTIS(mymantis, "SAM") -- convenience, same effect
|
|
-- The ship group is added to the running set(s) regardless of name prefix; classification
|
|
-- and jamming resolve by hull type name as usual. MANTIS takes over emission/alarm
|
|
-- switching: the NAVYGROUP's default Emission/Alarmstate are neutralized so its
|
|
-- spawn-time option reset does not fight MANTIS.
|
|
-- ===================================================================================================
|
|
do
|
|
-- Guard MOOSE's ground relocation against ships. SEAD suppression (and any other
|
|
-- caller) invokes RelocateGroundRandomInRadius on suppressed SAM groups without a
|
|
-- category check; on a ship this clobbers the sailing route with a ground-formation
|
|
-- hop (and with onland=true can target a LAND point near a coast). Radar-off
|
|
-- evasion still applies to ships; only the ground move becomes a no-op.
|
|
if CONTROLLABLE and CONTROLLABLE.RelocateGroundRandomInRadius and not CONTROLLABLE._MANTISNavalRelocateGuard then
|
|
CONTROLLABLE._MANTISNavalRelocateGuard = true
|
|
local _origRelocate = CONTROLLABLE.RelocateGroundRandomInRadius
|
|
function CONTROLLABLE:RelocateGroundRandomInRadius( speed, radius, onroad, shortcut, formation, onland )
|
|
if self.IsShip and self:IsShip() then
|
|
return self
|
|
end
|
|
return _origRelocate( self, speed, radius, onroad, shortcut, formation, onland )
|
|
end
|
|
end
|
|
|
|
--- [Internal] Check if any enemy surface combatant is within radius of a coordinate.
|
|
-- Builds a live enemy-ship SET_GROUP lazily on first use.
|
|
-- @param #MANTIS self
|
|
-- @param Core.Point#COORDINATE coord Ship position.
|
|
-- @param #number radius Trigger radius in meters.
|
|
-- @return #boolean found
|
|
function MANTIS:_EnemySurfaceInRange(coord, radius)
|
|
if self.NavalThreatSet == nil then
|
|
local own = string.lower(self.Coalition or "")
|
|
local enemy = (own == "red" and "blue") or (own == "blue" and "red") or nil
|
|
if not enemy then
|
|
self:E(self.lid.."NavalSurfaceWakeup: cannot derive enemy coalition from "..tostring(self.Coalition).." - disabling.")
|
|
self.NavalSurfaceWakeup = false
|
|
self.NavalThreatSet = false
|
|
return false
|
|
end
|
|
self.NavalThreatSet = SET_GROUP:New():FilterCoalitions(enemy):FilterCategoryShip():FilterStart()
|
|
end
|
|
if self.NavalThreatSet == false or not coord then return false end
|
|
local found = false
|
|
self.NavalThreatSet:ForEachGroupAlive(
|
|
function(grp)
|
|
if not found then
|
|
local gc = grp:GetCoordinate()
|
|
if gc and gc:Get2DDistance(coord) <= radius then found = true end
|
|
end
|
|
end)
|
|
return found
|
|
end
|
|
|
|
--- Configure naval AUTONOMY outside EWR coverage. Default: ENABLED, 150km.
|
|
-- Ships mirror the ground-SAM umbrella concept: a ship group within CoverageKM
|
|
-- of any alive EWR group (including EWR-role ships or AWACS ships in the EWR
|
|
-- set) is managed normally -- alarm GREEN until MANTIS detects a threat inside
|
|
-- one of its units' envelopes. A group OUTSIDE coverage goes alarm RED and
|
|
-- searches/engages autonomously on its own radars until coverage is restored.
|
|
-- Full jamming suppression overrides autonomy and forces the group GREEN.
|
|
-- @param #MANTIS self
|
|
-- @param #boolean onoff Enable (true) or disable (false).
|
|
-- @param #number coveragekm (Optional) Coverage radius in KILOMETERS around each alive EWR (default 150).
|
|
-- @return #MANTIS self
|
|
function MANTIS:SetNavalAutonomy(onoff, coveragekm)
|
|
self.NavalAutonomy = onoff and true or false
|
|
if coveragekm then self.NavalAutonomyRadius = coveragekm * 1000 end
|
|
return self
|
|
end
|
|
|
|
--- Enable/disable PER-UNIT classification and switching for naval groups. Default: ENABLED.
|
|
-- Mixed-hull ship groups (e.g. a carrier escorted by destroyers) get one SAM entry PER
|
|
-- SHIP -- each hull with its own type, range, state and jamming curve, switched via
|
|
-- UNIT:EnableEmission while the group stays alarm RED (weapons hot). Disable to revert
|
|
-- to legacy group-level naval handling (first matched hull defines the whole group).
|
|
-- @param #MANTIS self
|
|
-- @param #boolean onoff Enable (true) or disable (false).
|
|
-- @return #MANTIS self
|
|
function MANTIS:SetNavalPerUnit(onoff)
|
|
self.NavalPerUnit = onoff and true or false
|
|
return self
|
|
end
|
|
|
|
--- Enable/disable the naval surface wake-up and optionally set a fixed trigger radius.
|
|
-- Default: ENABLED, radius = each ship's own engagement radius from SamDataNaval.
|
|
-- When enabled, a MANTIS-quiet ship goes active (RED / emissions on) whenever an
|
|
-- enemy surface combatant closes within the trigger radius, so anti-ship
|
|
-- engagements are not blocked by EMCON. Jamming still wins: a jammed ship stays
|
|
-- suppressed. Ships only -- ground SAM behavior is unchanged.
|
|
-- @param #MANTIS self
|
|
-- @param #boolean onoff Enable (true) or disable (false).
|
|
-- @param #number radiuskm (Optional) Fixed trigger radius in KILOMETERS for all ships.
|
|
-- @return #MANTIS self
|
|
function MANTIS:SetNavalSurfaceWakeup(onoff, radiuskm)
|
|
self.NavalSurfaceWakeup = onoff and true or false
|
|
self.NavalSurfaceWakeupRadius = radiuskm and radiuskm * 1000 or nil
|
|
return self
|
|
end
|
|
|
|
--- Include or exclude naval SAMs from SEAD emission-shutdown evasion. Default: EXCLUDED.
|
|
-- Surface combatants engage inbound ARMs/ASMs with hard-kill defenses (SAMs, CIWS);
|
|
-- going emissions-dark would disable exactly those defenses. Ground SAM behavior is
|
|
-- unchanged. Takes effect at the next SAM table refresh (every 3rd detection cycle).
|
|
-- @param #MANTIS self
|
|
-- @param #boolean onoff If true, ships use SEAD EMCON evasion like ground SAMs.
|
|
-- @return #MANTIS self
|
|
function MANTIS:SetNavalSEAD(onoff)
|
|
self.SEADNaval = onoff and true or false
|
|
-- SEAD:UpdateSet() only ever ADDS names. When switching OFF mid-mission we
|
|
-- must actively remove ship groups already registered with the SEAD
|
|
-- instance, or they would keep evading until mission end.
|
|
if (not self.SEADNaval) and self.mysead and self.mysead.SEADGroupPrefixes then
|
|
for name,_ in pairs(self.mysead.SEADGroupPrefixes) do
|
|
local grp = GROUP:FindByName(name)
|
|
if grp and grp:IsShip() then
|
|
self.mysead.SEADGroupPrefixes[name] = nil
|
|
end
|
|
end
|
|
end
|
|
return self
|
|
end
|
|
|
|
--- Add a NAVYGROUP to this MANTIS as SAM shooter and/or EWR picket.
|
|
-- IMPORTANT: MANTIS does not change ROE. NAVYGROUP defaults to Return Fire --
|
|
-- set NavyGroup:SwitchROE(ENUMS.ROE.WeaponFree) in your mission if the ship
|
|
-- should engage detected targets rather than only return fire.
|
|
-- @param #MANTIS self
|
|
-- @param Ops.NavyGroup#NAVYGROUP NavyGroup The navy group to add.
|
|
-- @param #string Role "SAM" (default) = managed shooter, or "EWR" = unmanaged, always-radiating sensor/picket (coverage source). "BOTH" is deprecated and treated as SAM.
|
|
-- @return #MANTIS self
|
|
function MANTIS:AddNavyGroup(NavyGroup, Role)
|
|
local role = string.upper(Role or "SAM")
|
|
if role == "BOTH" then
|
|
-- Deprecated: a MANAGED shooter cannot double as a sensor -- its radars
|
|
-- are dark whenever MANTIS holds it GREEN, so EWR membership senses
|
|
-- nothing and only pollutes EWR-alive counting and the advanced-mode
|
|
-- network ratio. Use a SEPARATE ship group with Role="EWR" as a
|
|
-- radiating picket instead.
|
|
self:E(self.lid.."AddNavyGroup: Role BOTH is deprecated -- treating as SAM. Use a separate EWR-role group as a radiating picket.")
|
|
role = "SAM"
|
|
end
|
|
if not (NavyGroup and NavyGroup.ClassName and string.find(NavyGroup.ClassName,"NAVYGROUP",1,true)) then
|
|
self:E(self.lid.."AddNavyGroup: object passed is not a NAVYGROUP!")
|
|
return self
|
|
end
|
|
local grp = NavyGroup:GetGroup()
|
|
if not (grp and grp:IsAlive()) then
|
|
self:E(self.lid.."AddNavyGroup: NAVYGROUP has no alive GROUP (late-activated? Call after activation).")
|
|
return self
|
|
end
|
|
-- Hand emission/alarm control to MANTIS. NAVYGROUP re-applies its default
|
|
-- Alarmstate/Emission at (re)spawn, which would fight MANTIS switching:
|
|
-- set defaults to Emission ON / Alarmstate AUTO so the reset is neutral.
|
|
if NavyGroup.SetDefaultEmission then NavyGroup:SetDefaultEmission(true) end
|
|
if NavyGroup.SetDefaultAlarmstate then NavyGroup:SetDefaultAlarmstate(0) end -- 0 = Auto
|
|
-- NOTE: MANTIS deliberately does NOT touch ROE -- that is the mission
|
|
-- designer's call. Be aware OPSGROUP's default ROE is Return Fire: a
|
|
-- MANTIS-managed NAVYGROUP will radiate when RED but hold its weapons
|
|
-- unless the mission sets e.g. NavyGroup:SwitchROE(ENUMS.ROE.WeaponFree).
|
|
self._navyGroups = self._navyGroups or {}
|
|
self._navyGroups[grp:GetName()] = NavyGroup
|
|
if role == "SAM" then
|
|
self.SAM_Group:AddGroup(grp)
|
|
-- If MANTIS is already up, classify the new ship immediately; otherwise
|
|
-- SetSAMStartState will pick it up at Start.
|
|
if self.SAM_Table then
|
|
self:_RefreshSAMTable()
|
|
end
|
|
elseif role == "EWR" then
|
|
self.EWR_Group:AddGroup(grp)
|
|
if self.Adv_EWR_Group then
|
|
self.Adv_EWR_Group:AddGroup(grp)
|
|
end
|
|
else
|
|
self:E(self.lid.."AddNavyGroup: unknown Role "..tostring(Role).." - use SAM or EWR.")
|
|
end
|
|
self:I(self.lid..string.format("Added NAVYGROUP %s as %s",grp:GetName(),role))
|
|
return self
|
|
end
|
|
|
|
--- [Internal, static] Inject NAVYGROUP:SetMANTIS. Idempotent; safe if NAVYGROUP absent.
|
|
function MANTIS._InjectNavyGroupAPI()
|
|
if NAVYGROUP and not NAVYGROUP.SetMANTIS then
|
|
--- Register this NAVYGROUP with a MANTIS instance as SAM shooter and/or EWR picket.
|
|
-- @param Ops.NavyGroup#NAVYGROUP self
|
|
-- @param Functional.Mantis#MANTIS Mantis The MANTIS instance.
|
|
-- @param #string Role "SAM" (default) = managed shooter, or "EWR" = unmanaged, always-radiating sensor/picket (coverage source). "BOTH" is deprecated and treated as SAM.
|
|
-- @return Ops.NavyGroup#NAVYGROUP self
|
|
function NAVYGROUP:SetMANTIS(Mantis, Role)
|
|
if Mantis and Mantis.AddNavyGroup then
|
|
Mantis:AddNavyGroup(self, Role)
|
|
else
|
|
self:E(self.lid.."SetMANTIS: no valid MANTIS instance passed!")
|
|
end
|
|
return self
|
|
end
|
|
end
|
|
end
|
|
-- Try immediately in case NAVYGROUP is already loaded (also runs from MANTIS:New).
|
|
MANTIS._InjectNavyGroupAPI()
|
|
end
|
|
|
|
-----------------------------------------------------------------------
|
|
-- MANTIS end
|
|
----------------------------------------------------------------------- |