--- **Functional** -- Short Range Air Defense System -- -- === -- -- **SHORAD** - Short Range Air Defense System -- Controls a network of short range air/missile defense groups. -- -- === -- -- ## Missions: -- -- ### [SHORAD - Short Range Air Defense](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/master/SRD%20-%20SHORAD%20Defense) -- -- === -- -- ### Author : **applevangelist ** -- -- @module Functional.Shorad -- @image Functional.Shorad.jpg -- -- Date: July 2021 ------------------------------------------------------------------------- --- **SHORAD** class, extends Core.Base#BASE -- @type SHORAD -- @field #string ClassName -- @field #string name Name of this Shorad -- @field #boolean debug Set the debug state -- @field #string Prefixes String to be used to build the @{#Core.Set#SET_GROUP} -- @field #number Radius Shorad defense radius in meters -- @field Core.Set#SET_GROUP Groupset The set of Shorad groups -- @field Core.Set#SET_GROUP Samset The set of SAM groups to defend -- @field #string Coalition The coalition of this Shorad -- @field #number ActiveTimer How long a Shorad stays active after wake-up in seconds -- @field #table ActiveGroups Table for the timer function -- @field #string lid The log ID for the dcs.log -- @field #boolean DefendHarms Default true, intercept incoming HARMS -- @field #boolean DefendMavs Default true, intercept incoming AG-Missiles -- @field #number DefenseLowProb Default 70, minimum detection limit -- @field #number DefenseHighProb Default 90, maximim detection limit -- @field #boolean UseEmOnOff Decide if we are using Emission on/off (default) or AlarmState red/green. -- @extends Core.Base#BASE --- *Good friends are worth defending.* Mr Tushman, Wonder (the Movie) -- -- Simple Class for a more intelligent Short Range Air Defense System -- -- #SHORAD -- Moose derived missile intercepting short range defense system. -- Protects a network of SAM sites. Uses events to switch on the defense groups closest to the enemy. -- Easily integrated with @{Functional.Mantis#MANTIS} to complete the defensive system setup. -- -- ## Usage -- -- Set up a #SET_GROUP for the SAM sites to be protected: -- -- `local SamSet = SET_GROUP:New():FilterPrefixes("Red SAM"):FilterCoalitions("red"):FilterStart()` -- -- By default, SHORAD will defense against both HARMs and AG-Missiles with short to medium range. The default defense probability is 70-90%. -- When a missile is detected, SHORAD will activate defense groups in the given radius around the target for 10 minutes. It will *not* react to friendly fire. -- -- ### Start a new SHORAD system, parameters are: -- -- * Name: Name of this SHORAD. -- * ShoradPrefix: Filter for the Shorad #SET_GROUP. -- * Samset: The #SET_GROUP of SAM sites to defend. -- * Radius: Defense radius in meters. -- * ActiveTimer: Determines how many seconds the systems stay on red alert after wake-up call. -- * Coalition: Coalition, i.e. "blue", "red", or "neutral".* -- -- `myshorad = SHORAD:New("RedShorad", "Red SHORAD", SamSet, 25000, 600, "red")` -- -- ## Customize options -- -- * SHORAD:SwitchDebug(debug) -- * SHORAD:SwitchHARMDefense(onoff) -- * SHORAD:SwitchAGMDefense(onoff) -- * SHORAD:SetDefenseLimits(low,high) -- * SHORAD:SetActiveTimer(seconds) -- * SHORAD:SetDefenseRadius(meters) -- -- @field #SHORAD SHORAD = { ClassName = "SHORAD", name = "MyShorad", debug = false, Prefixes = "", Radius = 20000, Groupset = nil, Samset = nil, Coalition = nil, ActiveTimer = 600, --stay on 10 mins ActiveGroups = {}, lid = "", DefendHarms = true, DefendMavs = true, DefenseLowProb = 70, DefenseHighProb = 90, UseEmOnOff = false, } ----------------------------------------------------------------------- -- SHORAD System ----------------------------------------------------------------------- do -- TODO Complete list? --- Missile enumerators -- @field Harms SHORAD.Harms = { ["AGM_88"] = "AGM_88", ["AGM_45"] = "AGM_45", ["AGM_122"] = "AGM_122", ["AGM_84"] = "AGM_84", ["AGM_45"] = "AGM_45", ["ALARN"] = "ALARM", ["LD-10"] = "LD-10", ["X_58"] = "X_58", ["X_28"] = "X_28", ["X_25"] = "X_25", ["X_31"] = "X_31", ["Kh25"] = "Kh25", } --- TODO complete list? -- @field Mavs SHORAD.Mavs = { ["AGM"] = "AGM", ["C-701"] = "C-701", ["Kh25"] = "Kh25", ["Kh29"] = "Kh29", ["Kh31"] = "Kh31", ["Kh66"] = "Kh66", } --- Instantiates a new SHORAD object -- @param #SHORAD self -- @param #string Name Name of this SHORAD -- @param #string ShoradPrefix Filter for the Shorad #SET_GROUP -- @param Core.Set#SET_GROUP Samset The #SET_GROUP of SAM sites to defend -- @param #number Radius Defense radius in meters, used to switch on groups -- @param #number ActiveTimer Determines how many seconds the systems stay on red alert after wake-up call -- @param #string Coalition Coalition, i.e. "blue", "red", or "neutral" -- @param #boolean UseEmOnOff Use Emissions On/Off rather than Alarm State Red/Green (default: use Emissions switch) -- @retunr #SHORAD self function SHORAD:New(Name, ShoradPrefix, Samset, Radius, ActiveTimer, Coalition, UseEmOnOff) local self = BASE:Inherit( self, BASE:New() ) self:T({Name, ShoradPrefix, Samset, Radius, ActiveTimer, Coalition}) local GroupSet = SET_GROUP:New():FilterPrefixes(ShoradPrefix):FilterCoalitions(Coalition):FilterCategoryGround():FilterStart() self.name = Name or "MyShorad" self.Prefixes = ShoradPrefix or "SAM SHORAD" self.Radius = Radius or 20000 self.Coalition = Coalition or "blue" self.Samset = Samset or GroupSet self.ActiveTimer = ActiveTimer or 600 self.ActiveGroups = {} self.Groupset = GroupSet self.DefendHarms = true self.DefendMavs = true self.DefenseLowProb = 70 -- probability to detect a missile shot, low margin self.DefenseHighProb = 90 -- probability to detect a missile shot, high margin self.UseEmOnOff = UseEmOnOff or false -- Decide if we are using Emission on/off (default) or AlarmState red/green self:I("*** SHORAD - Started Version 0.2.8") -- Set the string id for output to DCS.log file. self.lid=string.format("SHORAD %s | ", self.name) self:_InitState() self:HandleEvent(EVENTS.Shot, self.HandleEventShot) return self end --- Initially set all groups to alarm state GREEN -- @param #SHORAD self function SHORAD:_InitState() self:T(self.lid .. " _InitState") local table = {} local set = self.Groupset self:T({set = set}) local aliveset = set:GetAliveSet() --#table for _,_group in pairs (aliveset) do if self.UseEmOnOff then --_group:SetAIOff() _group:EnableEmission(false) _group:OptionAlarmStateRed() --Wrapper.Group#GROUP else _group:OptionAlarmStateGreen() --Wrapper.Group#GROUP end _group:OptionDisperseOnAttack(30) end -- gather entropy for i=1,100 do math.random() end return self end --- Switch debug state on -- @param #SHORAD self -- @param #boolean debug Switch debug on (true) or off (false) function SHORAD:SwitchDebug(onoff) self:T( { onoff } ) if onoff then self:SwitchDebugOn() else self.SwitchDebugOff() end return self end --- Switch debug state on -- @param #SHORAD self function SHORAD:SwitchDebugOn() self.debug = true --tracing BASE:TraceOn() BASE:TraceClass("SHORAD") return self end --- Switch debug state off -- @param #SHORAD self function SHORAD:SwitchDebugOff() self.debug = false BASE:TraceOff() return self end --- Switch defense for HARMs -- @param #SHORAD self -- @param #boolean onoff function SHORAD:SwitchHARMDefense(onoff) self:T( { onoff } ) local onoff = onoff or true self.DefendHarms = onoff return self end --- Switch defense for AGMs -- @param #SHORAD self -- @param #boolean onoff function SHORAD:SwitchAGMDefense(onoff) self:T( { onoff } ) local onoff = onoff or true self.DefendMavs = onoff return self end --- Set defense probability limits -- @param #SHORAD self -- @param #number low Minimum detection limit, integer 1-100 -- @param #number high Maximum detection limit integer 1-100 function SHORAD:SetDefenseLimits(low,high) self:T( { low, high } ) local low = low or 70 local high = high or 90 if (low < 0) or (low > 100) or (low > high) then low = 70 end if (high < 0) or (high > 100) or (high < low ) then high = 90 end self.DefenseLowProb = low self.DefenseHighProb = high return self end --- Set the number of seconds a SHORAD site will stay active -- @param #SHORAD self -- @param #number seconds Number of seconds systems stay active function SHORAD:SetActiveTimer(seconds) self:T(self.lid .. " SetActiveTimer") local timer = seconds or 600 if timer < 0 then timer = 600 end self.ActiveTimer = timer return self end --- Set the number of meters for the SHORAD defense zone -- @param #SHORAD self -- @param #number meters Radius of the defense search zone in meters. #SHORADs in this range around a targeted group will go active function SHORAD:SetDefenseRadius(meters) self:T(self.lid .. " SetDefenseRadius") local radius = meters or 20000 if radius < 0 then radius = 20000 end self.Radius = radius return self end --- Set using Emission on/off instead of changing alarm state -- @param #SHORAD self -- @param #boolean switch Decide if we are changing alarm state or AI state function SHORAD:SetUsingEmOnOff(switch) self:T(self.lid .. " SetUsingEmOnOff") self.UseEmOnOff = switch or false return self end --- Check if a HARM was fired -- @param #SHORAD self -- @param #string WeaponName -- @return #boolean Returns true for a match function SHORAD:_CheckHarms(WeaponName) self:T(self.lid .. " _CheckHarms") self:T( { WeaponName } ) local hit = false if self.DefendHarms then for _,_name in pairs (SHORAD.Harms) do if string.find(WeaponName,_name,1) then hit = true end end end return hit end --- Check if an AGM was fired -- @param #SHORAD self -- @param #string WeaponName -- @return #boolean Returns true for a match function SHORAD:_CheckMavs(WeaponName) self:T(self.lid .. " _CheckMavs") self:T( { WeaponName } ) local hit = false if self.DefendMavs then for _,_name in pairs (SHORAD.Mavs) do if string.find(WeaponName,_name,1) then hit = true end end end return hit end --- Check the coalition of the attacker -- @param #SHORAD self -- @param #string Coalition name -- @return #boolean Returns false for a match function SHORAD:_CheckCoalition(Coalition) self:T(self.lid .. " _CheckCoalition") local owncoalition = self.Coalition local othercoalition = "" if Coalition == 0 then othercoalition = "neutral" elseif Coalition == 1 then othercoalition = "red" else othercoalition = "blue" end self:T({owncoalition = owncoalition, othercoalition = othercoalition}) if owncoalition ~= othercoalition then return true else return false end end --- Check if the missile is aimed at a SHORAD -- @param #SHORAD self -- @param #string TargetGroupName Name of the target group -- @return #boolean Returns true for a match, else false function SHORAD:_CheckShotAtShorad(TargetGroupName) self:T(self.lid .. " _CheckShotAtShorad") local tgtgrp = TargetGroupName local shorad = self.Groupset local shoradset = shorad:GetAliveSet() --#table local returnname = false for _,_groups in pairs (shoradset) do local groupname = _groups:GetName() if string.find(groupname, tgtgrp, 1) then returnname = true --_groups:RelocateGroundRandomInRadius(7,100,false,false) -- be a bit evasive end end return returnname end --- Check if the missile is aimed at a SAM site -- @param #SHORAD self -- @param #string TargetGroupName Name of the target group -- @return #boolean Returns true for a match, else false function SHORAD:_CheckShotAtSams(TargetGroupName) self:T(self.lid .. " _CheckShotAtSams") local tgtgrp = TargetGroupName local shorad = self.Samset --local shoradset = shorad:GetAliveSet() --#table local shoradset = shorad:GetSet() --#table local returnname = false for _,_groups in pairs (shoradset) do local groupname = _groups:GetName() if string.find(groupname, tgtgrp, 1) then returnname = true end end return returnname end --- Calculate if the missile shot is detected -- @param #SHORAD self -- @return #boolean Returns true for a detection, else false function SHORAD:_ShotIsDetected() self:T(self.lid .. " _ShotIsDetected") local IsDetected = false local DetectionProb = math.random(self.DefenseLowProb, self.DefenseHighProb) -- reference value local ActualDetection = math.random(1,100) -- value for this shot if ActualDetection <= DetectionProb then IsDetected = true end return IsDetected end --- Wake up #SHORADs in a zone with diameter Radius for ActiveTimer seconds -- @param #SHORAD self -- @param #string TargetGroup Name of the target group used to build the #ZONE -- @param #number Radius Radius of the #ZONE -- @param #number ActiveTimer Number of seconds to stay active -- @param #number TargetCat (optional) Category, i.e. Object.Category.UNIT or Object.Category.STATIC -- @usage Use this function to integrate with other systems, example -- -- local SamSet = SET_GROUP:New():FilterPrefixes("Blue SAM"):FilterCoalitions("blue"):FilterStart() -- myshorad = SHORAD:New("BlueShorad", "Blue SHORAD", SamSet, 22000, 600, "blue") -- myshorad:SwitchDebug(true) -- mymantis = MANTIS:New("BlueMantis","Blue SAM","Blue EWR",nil,"blue",false,"Blue Awacs") -- mymantis:AddShorad(myshorad,720) -- mymantis:Start() function SHORAD:WakeUpShorad(TargetGroup, Radius, ActiveTimer, TargetCat) self:T(self.lid .. " WakeUpShorad") self:T({TargetGroup, Radius, ActiveTimer, TargetCat}) local targetcat = TargetCat or Object.Category.UNIT local targetgroup = TargetGroup local targetvec2 = nil if targetcat == Object.Category.UNIT then targetvec2 = GROUP:FindByName(targetgroup):GetVec2() elseif targetcat == Object.Category.STATIC then targetvec2 = STATIC:FindByName(targetgroup,false):GetVec2() else local samset = self.Samset local sam = samset:GetRandom() targetvec2 = sam:GetVec2() end local targetzone = ZONE_RADIUS:New("Shorad",targetvec2,Radius) -- create a defense zone to check local groupset = self.Groupset --Core.Set#SET_GROUP local shoradset = groupset:GetAliveSet() --#table -- local function to switch off shorad again local function SleepShorad(group) local groupname = group:GetName() self.ActiveGroups[groupname] = nil if self.UseEmOnOff then group:EnableEmission(false) --group:SetAIOff() else group:OptionAlarmStateGreen() end local text = string.format("Sleeping SHORAD %s", group:GetName()) self:T(text) local m = MESSAGE:New(text,10,"SHORAD"):ToAllIf(self.debug) end -- go through set and find the one(s) to activate for _,_group in pairs (shoradset) do if _group:IsAnyInZone(targetzone) then local text = string.format("Waking up SHORAD %s", _group:GetName()) self:T(text) local m = MESSAGE:New(text,10,"SHORAD"):ToAllIf(self.debug) if self.UseEmOnOff then --_group:SetAIOn() _group:EnableEmission(true) end _group:OptionAlarmStateRed() local groupname = _group:GetName() if self.ActiveGroups[groupname] == nil then -- no timer yet for this group self.ActiveGroups[groupname] = { Timing = ActiveTimer } local endtime = timer.getTime() + (ActiveTimer * math.random(75,100) / 100 ) -- randomize wakeup a bit timer.scheduleFunction(SleepShorad, _group, endtime) end end end return self end --- Main function - work on the EventData -- @param #SHORAD self -- @param Core.Event#EVENTDATA EventData The event details table data set function SHORAD:HandleEventShot( EventData ) self:T( { EventData } ) self:T(self.lid .. " HandleEventShot") --local ShootingUnit = EventData.IniDCSUnit --local ShootingUnitName = EventData.IniDCSUnitName local ShootingWeapon = EventData.Weapon -- Identify the weapon fired local ShootingWeaponName = EventData.WeaponName -- return weapon type -- get firing coalition local weaponcoalition = EventData.IniGroup:GetCoalition() -- get detection probability if self:_CheckCoalition(weaponcoalition) then --avoid overhead on friendly fire local IsDetected = self:_ShotIsDetected() -- convert to text local DetectedText = "false" if IsDetected then DetectedText = "true" end local text = string.format("%s Missile Launched = %s | Detected probability state is %s", self.lid, ShootingWeaponName, DetectedText) self:T( text ) local m = MESSAGE:New(text,10,"Info"):ToAllIf(self.debug) -- if (self:_CheckHarms(ShootingWeaponName) or self:_CheckMavs(ShootingWeaponName)) and IsDetected then -- get target data local targetdata = EventData.Weapon:getTarget() -- Identify target local targetcat = targetdata:getCategory() -- Identify category self:T(string.format("Target Category (3=STATIC, 1=UNIT)= %s",tostring(targetcat))) local targetunit = nil if targetcat == Object.Category.UNIT then -- UNIT targetunit = UNIT:Find(targetdata) elseif targetcat == Object.Category.STATIC then -- STATIC targetunit = STATIC:Find(targetdata) end --local targetunitname = Unit.getName(targetdata) -- Unit name if targetunit and targetunit:IsAlive() then local targetunitname = targetunit:GetName() --local targetgroup = Unit.getGroup(Weapon.getTarget(ShootingWeapon)) --targeted group local targetgroup = nil local targetgroupname = "none" if targetcat == Object.Category.UNIT then targetgroup = targetunit:GetGroup() targetgroupname = targetgroup:GetName() -- group name elseif targetcat == Object.Category.STATIC then targetgroup = targetunit targetgroupname = targetunitname end local text = string.format("%s Missile Target = %s", self.lid, tostring(targetgroupname)) self:T( text ) local m = MESSAGE:New(text,10,"Info"):ToAllIf(self.debug) -- check if we or a SAM site are the target --local TargetGroup = EventData.TgtGroup -- Wrapper.Group#GROUP local shotatus = self:_CheckShotAtShorad(targetgroupname) --#boolean local shotatsams = self:_CheckShotAtSams(targetgroupname) --#boolean -- if being shot at, find closest SHORADs to activate if shotatsams or shotatus then self:T({shotatsams=shotatsams,shotatus=shotatus}) self:WakeUpShorad(targetgroupname, self.Radius, self.ActiveTimer, targetcat) end end end end end -- end ----------------------------------------------------------------------- -- SHORAD end -----------------------------------------------------------------------