--- **Ops** - Allow a player in a helo like the Gazelle, KA-50 to recon and lase ground targets. -- -- ## Features: -- -- * Allow a player in a helicopter to detect, smoke, flare, lase and report ground units to others. -- * Implements visual detection from the helo -- * Implements optical detection via the Gazelle Vivianne system and lasing -- * KA-50 BlackShark basic support -- * Everyone else gets visual detection only -- * Upload target info to a PLAYERTASKCONTROLLER Instance -- -- === -- -- # Demo Missions -- -- ### Demo missions can be found on [github](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/develop/). -- -- === -- -- -- ### Authors: -- -- * Applevangelist (Design & Programming) -- -- === -- -- @module Ops.PlayerRecce -- @image Ops_PlayerRecce.png ------------------------------------------------------------------------------------------------------------------- -- PLAYERRECCE -- TODO: PLAYERRECCE -- DONE: No messages when no targets to flare or smoke -- DONE: Smoke not all targets -- DONE: Messages to Attack Group, use client settings -- DONE: Lasing dist 8km -- DONE: Reference Point RP -- DONE: Sort for multiple targets in one direction -- DONE: Targets with forget timeout, also report ------------------------------------------------------------------------------------------------------------------- --- PLAYERRECCE class. -- @type PLAYERRECCE -- @field #string ClassName Name of the class. -- @field #boolean verbose Switch verbosity. -- @field #string lid Class id string for output to DCS log file. -- @field #string version -- @field #table ViewZone -- @field #table ViewZoneVisual -- @field #table ViewZoneLaser -- @field #table LaserFOV -- @field #table LaserTarget -- @field Core.Set#SET_CLIENT PlayerSet -- @field #string Name -- @field #number Coalition -- @field #string CoalitionName -- @field #boolean debug -- @field #table LaserSpots -- @field #table UnitLaserCodes -- @field #table LaserCodes -- @field #table ClientMenus -- @field #table OnStation -- @field #number minthreatlevel -- @field #number lasingtime -- @field #table AutoLase -- @field Core.Set#SET_CLIENT AttackSet -- @field #boolean TransmitOnlyWithPlayers -- @field Sound.SRS#MSRS SRS -- @field Sound.SRS#MSRSQUEUE SRSQueue -- @field #boolean UseController -- @field Ops.PlayerTask#PLAYERTASKCONTROLLER Controller -- @field #boolean ShortCallsign -- @field #boolean Keepnumber -- @field #table CallsignTranslations -- @field Core.Point#COORDINATE ReferencePoint -- @field #string RPName -- @field Wrapper.Marker#MARKER RPMarker -- @field #number TForget -- @field Utilities.FiFo#FIFO TargetCache -- @field #boolean smokeownposition -- @field #table SmokeOwn -- @field #boolean smokeaveragetargetpos -- @extends Core.Fsm#FSM --- -- -- *It is our attitude at the beginning of a difficult task which, more than anything else, which will affect its successful outcome.* (William James) -- -- === -- -- # PLAYERRECCE -- -- * Allow a player in a helicopter to detect, smoke, flare, lase and report ground units to others. -- * Implements visual detection from the helo -- * Implements optical detection via the Gazelle Vivianne system and lasing -- * KA-50 BlackShark basic support -- * Everyone else gets visual detection only -- * Upload target info to a PLAYERTASKCONTROLLER Instance -- -- If you have questions or suggestions, please visit the [MOOSE Discord](https://discord.gg/AeYAkHP) channel. -- -- -- @field #PLAYERRECCE PLAYERRECCE = { ClassName = "PLAYERRECCE", verbose = true, lid = nil, version = "0.1.23", ViewZone = {}, ViewZoneVisual = {}, ViewZoneLaser = {}, LaserFOV = {}, LaserTarget = {}, PlayerSet = nil, debug = false, LaserSpots = {}, UnitLaserCodes = {}, LaserCodes = {}, ClientMenus = {}, OnStation = {}, minthreatlevel = 0, lasingtime = 60, AutoLase = {}, AttackSet = nil, TransmitOnlyWithPlayers = true, UseController = false, Controller = nil, ShortCallsign = true, Keepnumber = true, CallsignTranslations = nil, ReferencePoint = nil, TForget = 600, TargetCache = nil, smokeownposition = false, SmokeOwn = {}, smokeaveragetargetpos = false, } --- -- @type PlayerRecceDetected -- @field #boolean detected -- @field Wrapper.Client#CLIENT recce -- @field #string playername -- @field #number timestamp --- -- @type LaserRelativePos -- @field #string typename Unit type name PLAYERRECCE.LaserRelativePos = { ["SA342M"] = { x = 1.7, y = 1.2, z = 0 }, ["SA342Mistral"] = { x = 1.7, y = 1.2, z = 0 }, ["SA342Minigun"] = { x = 1.7, y = 1.2, z = 0 }, ["SA342L"] = { x = 1.7, y = 1.2, z = 0 }, ["Ka-50"] = { x = 6.1, y = -0.85 , z = 0 }, ["Ka-50_3"] = { x = 6.1, y = -0.85 , z = 0 } } --- -- @type MaxViewDistance -- @field #string typename Unit type name PLAYERRECCE.MaxViewDistance = { ["SA342M"] = 8000, ["SA342Mistral"] = 8000, ["SA342Minigun"] = 8000, ["SA342L"] = 8000, ["Ka-50"] = 8000, ["Ka-50_3"] = 8000, } --- -- @type Cameraheight -- @field #string typename Unit type name PLAYERRECCE.Cameraheight = { ["SA342M"] = 2.85, ["SA342Mistral"] = 2.85, ["SA342Minigun"] = 2.85, ["SA342L"] = 2.85, ["Ka-50"] = 0.5, ["Ka-50_3"] = 0.5, } --- -- @type CanLase -- @field #string typename Unit type name PLAYERRECCE.CanLase = { ["SA342M"] = true, ["SA342Mistral"] = true, ["SA342Minigun"] = false, -- no optics ["SA342L"] = true, ["Ka-50"] = true, ["Ka-50_3"] = true, } --- -- @type SmokeColor -- @field #string color PLAYERRECCE.SmokeColor = { ["highsmoke"] = SMOKECOLOR.Orange, ["medsmoke"] = SMOKECOLOR.White, ["lowsmoke"] = SMOKECOLOR.Green, ["lasersmoke"] = SMOKECOLOR.Red, ["ownsmoke"] = SMOKECOLOR.Blue, } --- -- @type FlareColor -- @field #string color PLAYERRECCE.FlareColor = { ["highflare"] =FLARECOLOR.Yellow, ["medflare"] = FLARECOLOR.White, ["lowflare"] = FLARECOLOR.Green, ["laserflare"] = FLARECOLOR.Red, ["ownflare"] = FLARECOLOR.Green, } --- Create and run a new PlayerRecce instance. -- @param #PLAYERRECCE self -- @param #string Name The name of this instance -- @param #number Coalition, e.g. coalition.side.BLUE -- @param Core.Set#SET_CLIENT PlayerSet The set of pilots working as recce -- @return #PLAYERRECCE self function PLAYERRECCE:New(Name, Coalition, PlayerSet) -- Inherit everything from FSM class. local self=BASE:Inherit(self, FSM:New()) -- #PLAYERRECCE self.Name = Name or "Blue FACA" self.Coalition = Coalition or coalition.side.BLUE self.CoalitionName = UTILS.GetCoalitionName(Coalition) self.PlayerSet = PlayerSet self.lid=string.format("PlayerForwardController %s %s | ", self.Name, self.version) self:SetLaserCodes( { 1688, 1130, 4785, 6547, 1465, 4578 } ) -- set self.LaserCodes self.lasingtime = 60 self.minthreatlevel = 0 self.TForget = 600 self.TargetCache = FIFO:New() -- FSM start state is STOPPED. self:SetStartState("Stopped") self:AddTransition("Stopped", "Start", "Running") self:AddTransition("*", "Status", "*") self:AddTransition("*", "RecceOnStation", "*") self:AddTransition("*", "RecceOffStation", "*") self:AddTransition("*", "TargetDetected", "*") self:AddTransition("*", "TargetsSmoked", "*") self:AddTransition("*", "TargetsFlared", "*") self:AddTransition("*", "Illumination", "*") self:AddTransition("*", "TargetLasing", "*") self:AddTransition("*", "TargetLOSLost", "*") self:AddTransition("*", "TargetReport", "*") self:AddTransition("*", "TargetReportSent", "*") self:AddTransition("*", "Shack", "*") self:AddTransition("Running", "Stop", "Stopped") -- Player Events self:HandleEvent(EVENTS.PlayerLeaveUnit, self._EventHandler) self:HandleEvent(EVENTS.Ejection, self._EventHandler) self:HandleEvent(EVENTS.Crash, self._EventHandler) self:HandleEvent(EVENTS.PilotDead, self._EventHandler) self:HandleEvent(EVENTS.PlayerEnterAircraft, self._EventHandler) self:__Start(-1) local starttime = math.random(5,10) self:__Status(-starttime) self:I(self.lid.." Started.") ------------------------ --- Pseudo Functions --- ------------------------ --- Triggers the FSM event "Start". Starts the PLAYERRECCE. Note: Start() is called automatically after New(). -- @function [parent=#PLAYERRECCE] Start -- @param #PLAYERRECCE self --- Triggers the FSM event "Start" after a delay. Starts the PLAYERRECCE. Note: Start() is called automatically after New(). -- @function [parent=#PLAYERRECCE] __Start -- @param #PLAYERRECCE self -- @param #number delay Delay in seconds. --- Triggers the FSM event "Stop". Stops the PLAYERRECCE and all its event handlers. -- @param #PLAYERRECCE self --- Triggers the FSM event "Stop" after a delay. Stops the PLAYERRECCE and all its event handlers. -- @function [parent=#PLAYERRECCE] __Stop -- @param #PLAYERRECCE self -- @param #number delay Delay in seconds. --- FSM Function OnAfterRecceOnStation. Recce came on station. -- @function [parent=#PLAYERRECCE] OnAfterRecceOnStation -- @param #PLAYERRECCE self -- @param #string From State. -- @param #string Event Trigger. -- @param #string To State. -- @param Wrapper.Client#CLIENT Client -- @param #string Playername -- @return #PLAYERRECCE self --- FSM Function OnAfterRecceOffStation. Recce went off duty. -- @function [parent=#PLAYERRECCE] OnAfterRecceOffStation -- @param #PLAYERRECCE self -- @param #string From State. -- @param #string Event Trigger. -- @param #string To State. -- @param Wrapper.Client#CLIENT Client -- @param #string Playername -- @return #PLAYERRECCE self --- FSM Function OnAfterTargetDetected. Targets detected. -- @function [parent=#PLAYERRECCE] OnAfterTargetDetected -- @param #PLAYERRECCE self -- @param #string From State. -- @param #string Event Trigger. -- @param #string To State. -- @param #table Targetsbyclock #table with index 1..12 containing a #table of Wrapper.Unit#UNIT objects each. -- @param Wrapper.Client#CLIENT Client -- @param #string Playername -- @return #PLAYERRECCE self --- FSM Function OnAfterTargetsSmoked. Smoke grenade shot. -- @function [parent=#PLAYERRECCE] OnAfterTargetsSmoked -- @param #PLAYERRECCE self -- @param #string From State. -- @param #string Event Trigger. -- @param #string To State. -- @param Wrapper.Client#CLIENT Client -- @param #string Playername -- @param Core.Set#SET_UNIT TargetSet -- @return #PLAYERRECCE self --- FSM Function OnAfterTargetsFlared. Flares shot. -- @function [parent=#PLAYERRECCE] OnAfterTargetsFlared -- @param #PLAYERRECCE self -- @param #string From State. -- @param #string Event Trigger. -- @param #string To State. -- @param Wrapper.Client#CLIENT Client -- @param #string Playername -- @param Core.Set#SET_UNIT TargetSet -- @return #PLAYERRECCE self --- FSM Function OnAfterIllumination. Illumination rocket shot. -- @function [parent=#PLAYERRECCE] OnAfterIllumination -- @param #PLAYERRECCE self -- @param #string From State. -- @param #string Event Trigger. -- @param #string To State. -- @param Wrapper.Client#CLIENT Client -- @param #string Playername -- @param Core.Set#SET_UNIT TargetSet -- @return #PLAYERRECCE self --- FSM Function OnAfterTargetLasing. Lasing a new target. -- @function [parent=#PLAYERRECCE] OnAfterTargetLasing -- @param #PLAYERRECCE self -- @param #string From State. -- @param #string Event Trigger. -- @param #string To State. -- @param Wrapper.Client#CLIENT Client -- @param Wrapper.Unit#UNIT Target -- @param #number Lasercode -- @param #number Lasingtime -- @return #PLAYERRECCE self --- FSM Function OnAfterTargetLOSLost. Lost LOS on lased target. -- @function [parent=#PLAYERRECCE] OnAfterTargetLOSLost -- @param #PLAYERRECCE self -- @param #string From State. -- @param #string Event Trigger. -- @param #string To State. -- @param Wrapper.Client#CLIENT Client -- @param Wrapper.Unit#UNIT Target -- @return #PLAYERRECCE self --- FSM Function OnAfterTargetReport. Laser target report sent. -- @function [parent=#PLAYERRECCE] OnAfterTargetReport -- @param #PLAYERRECCE self -- @param #string From State. -- @param #string Event Trigger. -- @param #string To State. -- @param Wrapper.Client#CLIENT Client -- @param Core.Set#SET_UNIT TargetSet -- @param Wrapper.Unit#UNIT Target Target currently lased -- @param #string Text -- @return #PLAYERRECCE self --- FSM Function OnAfterTargetReportSent. All targets report sent. -- @function [parent=#PLAYERRECCE] OnAfterTargetReportSent -- @param #PLAYERRECCE self -- @param #string From State. -- @param #string Event Trigger. -- @param #string To State. -- @param Wrapper.Client#CLIENT Client Client sending the report -- @param #string Playername Player name -- @param Core.Set#SET_UNIT TargetSet Set of targets -- @return #PLAYERRECCE self --- FSM Function OnAfterShack. Lased target has been destroyed. -- @function [parent=#PLAYERRECCE] OnAfterShack -- @param #PLAYERRECCE self -- @param #string From State. -- @param #string Event Trigger. -- @param #string To State. -- @param Wrapper.Client#CLIENT Client -- @param Wrapper.Unit#UNIT Target The destroyed target (if obtainable) -- @return #PLAYERRECCE self return self end ------------------------------------------------------------------------------------------ -- TODO: Functions ------------------------------------------------------------------------------------------ --- [Internal] Event handling -- @param #PLAYERRECCE self -- @param Core.Event#EVENTDATA EventData -- @return #PLAYERRECCE self function PLAYERRECCE:_EventHandler(EventData) self:T(self.lid.."_EventHandler: "..EventData.id) if EventData.id == EVENTS.PlayerLeaveUnit or EventData.id == EVENTS.Ejection or EventData.id == EVENTS.Crash or EventData.id == EVENTS.PilotDead then if EventData.IniPlayerName then self:T(self.lid.."Event for player: "..EventData.IniPlayerName) if self.ClientMenus[EventData.IniPlayerName] then self.ClientMenus[EventData.IniPlayerName]:Remove() end self.ClientMenus[EventData.IniPlayerName] = nil self.LaserSpots[EventData.IniPlayerName] = nil self.OnStation[EventData.IniPlayerName] = false self.LaserFOV[EventData.IniPlayerName] = nil self.UnitLaserCodes[EventData.IniPlayerName] = nil self.LaserTarget[EventData.IniPlayerName] = nil self.AutoLase[EventData.IniPlayerName] = false if self.ViewZone[EventData.IniPlayerName] then self.ViewZone[EventData.IniPlayerName]:UndrawZone() end if self.ViewZoneLaser[EventData.IniPlayerName] then self.ViewZoneLaser[EventData.IniPlayerName]:UndrawZone() end if self.ViewZoneVisual[EventData.IniPlayerName] then self.ViewZoneVisual[EventData.IniPlayerName]:UndrawZone() end end elseif EventData.id == EVENTS.PlayerEnterAircraft and EventData.IniCoalition == self.Coalition then if EventData.IniPlayerName then self:T(self.lid.."Event for player: "..EventData.IniPlayerName) self.UnitLaserCodes[EventData.IniPlayerName] = 1688 self.ClientMenus[EventData.IniPlayerName] = nil self.LaserSpots[EventData.IniPlayerName] = nil self.OnStation[EventData.IniPlayerName] = false self.LaserFOV[EventData.IniPlayerName] = nil self.UnitLaserCodes[EventData.IniPlayerName] = nil self.LaserTarget[EventData.IniPlayerName] = nil self.AutoLase[EventData.IniPlayerName] = false if self.ViewZone[EventData.IniPlayerName] then self.ViewZone[EventData.IniPlayerName]:UndrawZone() end if self.ViewZoneLaser[EventData.IniPlayerName] then self.ViewZoneLaser[EventData.IniPlayerName]:UndrawZone() end if self.ViewZoneVisual[EventData.IniPlayerName] then self.ViewZoneVisual[EventData.IniPlayerName]:UndrawZone() end self:_BuildMenus() end end return self end --- (Internal) Function to determine clockwise direction to target. -- @param #PLAYERRECCE self -- @param Wrapper.Unit#UNIT unit The Helicopter -- @param Wrapper.Unit#UNIT target The downed Group -- @return #number direction function PLAYERRECCE:_GetClockDirection(unit, target) self:T(self.lid .. " _GetClockDirection") local _playerPosition = unit:GetCoordinate() -- get position of helicopter local _targetpostions = target:GetCoordinate() -- get position of downed pilot local _heading = unit:GetHeading() -- heading --self:I("Heading = ".._heading) local DirectionVec3 = _playerPosition:GetDirectionVec3( _targetpostions ) local Angle = _playerPosition:GetAngleDegrees( DirectionVec3 ) --self:I("Angle = "..Angle) local clock = 12 local hours = 0 if _heading and Angle then clock = 12 --if angle == 0 then angle = 360 end clock = _heading-Angle hours = (clock/30)*-1 --self:I("hours = "..hours) clock = 12+hours clock = UTILS.Round(clock,0) if clock > 12 then clock = clock-12 end if clock == 0 then clock = 12 end end --self:I("Clock ="..clock) return clock end --- [User] Set a table of possible laser codes. -- Each new RECCE can select a code from this table, default is { 1688, 1130, 4785, 6547, 1465, 4578 }. -- @param #PLAYERRECCE self -- @param #list<#number> LaserCodes -- @return #PLAYERRECCE function PLAYERRECCE:SetLaserCodes( LaserCodes ) self.LaserCodes = ( type( LaserCodes ) == "table" ) and LaserCodes or { LaserCodes } return self end --- [User] Set a reference point coordinate for A2G Operations. Will be used in coordinate references. -- @param #PLAYERRECCE self -- @param Core.Point#COORDINATE Coordinate Coordinate of the RP -- @param #string Name Name of the RP -- @return #PLAYERRECCE function PLAYERRECCE:SetReferencePoint(Coordinate,Name) self.ReferencePoint = Coordinate self.RPName = Name if self.RPMarker then self.RPMarker:Remove() end local llddm = Coordinate:ToStringLLDDM() local lldms = Coordinate:ToStringLLDMS() local mgrs = Coordinate:ToStringMGRS() local text = string.format("%s RP %s\n%s\n%s\n%s",self.Name,Name,llddm,lldms,mgrs) self.RPMarker = MARKER:New(Coordinate,text) self.RPMarker:ReadOnly() self.RPMarker:ToCoalition(self.Coalition) return self end --- [User] Set PlayerTaskController. Allows to upload target reports to the controller, in turn creating tasks for other players. -- @param #PLAYERRECCE self -- @param Ops.PlayerTask#PLAYERTASKCONTROLLER Controller -- @return #PLAYERRECCE function PLAYERRECCE:SetPlayerTaskController(Controller) self.UseController = true self.Controller = Controller return self end --- [User] Set a set of clients which will receive target reports -- @param #PLAYERRECCE self -- @param Core.Set#SET_CLIENT AttackSet -- @return #PLAYERRECCE function PLAYERRECCE:SetAttackSet(AttackSet) self.AttackSet = AttackSet return self end ---[Internal] Check Gazelle camera in on -- @param #PLAYERRECCE self -- @param Wrapper.Client#CLIENT client -- @param #string playername -- @return #boolean OnOff function PLAYERRECCE:_CameraOn(client,playername) local camera = true local unit = client -- Wrapper.Unit#UNIT if unit and unit:IsAlive() then local typename = unit:GetTypeName() if string.find(typename,"SA342") then local dcsunit = Unit.getByName(client:GetName()) local vivihorizontal = dcsunit:getDrawArgumentValue(215) or 0 -- (not in MiniGun) 1 to -1 -- zero is straight ahead, 1/-1 = 180 deg if vivihorizontal < -0.7 or vivihorizontal > 0.7 then camera = false end elseif string.find(typename,"Ka-50") then camera = true end end return camera end --- [Internal] Get the view parameters from a Gazelle camera -- @param #PLAYERRECCE self -- @param Wrapper.Unit#UNIT Gazelle -- @return #number cameraheading in degrees. -- @return #number cameranodding in degrees. -- @return #number maxview in meters. -- @return #boolean cameraison If true, camera is on, else off. function PLAYERRECCE:_GetGazelleVivianneSight(Gazelle) self:T(self.lid.."GetGazelleVivianneSight") local unit = Gazelle -- Wrapper.Unit#UNIT if unit and unit:IsAlive() then local dcsunit = Unit.getByName(Gazelle:GetName()) local vivihorizontal = dcsunit:getDrawArgumentValue(215) or 0 -- (not in MiniGun) 1 to -1 -- zero is straight ahead, 1/-1 = 180 deg, local vivivertical = dcsunit:getDrawArgumentValue(216) or 0 -- L/Mistral/Minigun model has no 216, ca 10deg up (=1) and down (=-1) -- vertical model limits 1.53846, -1.10731 local vivioff = false -- -1 = -180, 1 = 180 -- Actual model view -0,66 to 0,66 -- Nick view 1.53846, -1.10731 for - 30° to +45° if vivihorizontal < -0.67 then -- model end vivihorizontal = -0.67 vivioff = false --return 0,0,0,false elseif vivihorizontal > 0.67 then -- vivi off vivihorizontal = 0.67 vivioff = true return 0,0,0,false end vivivertical = vivivertical / 1.10731 -- normalize local horizontalview = vivihorizontal * -180 local verticalview = vivivertical * 30 -- ca +/- 30° --self:I(string.format("vivihorizontal=%.5f | vivivertical=%.5f",vivihorizontal,vivivertical)) --self:I(string.format("horizontal=%.5f | vertical=%.5f",horizontalview,verticalview)) local heading = unit:GetHeading() local viviheading = (heading+horizontalview)%360 local maxview = self:_GetActualMaxLOSight(unit,viviheading, verticalview,vivioff) --self:I(string.format("maxview=%.5f",maxview)) -- visual skew local factor = 3.15 self.GazelleViewFactors = { [1]=1.18, [2]=1.32, [3]=1.46, [4]=1.62, [5]=1.77, [6]=1.85, [7]=2.05, [8]=2.05, [9]=2.3, [10]=2.3, [11]=2.27, [12]=2.27, [13]=2.43, } local lfac = UTILS.Round(maxview,-2) if lfac <= 1300 then --factor = self.GazelleViewFactors[lfac/100] factor = 3.15 maxview = math.ceil((maxview*factor)/100)*100 end if maxview > 8000 then maxview = 8000 end --self:I(string.format("corrected maxview=%.5f",maxview)) return viviheading, verticalview,maxview, not vivioff end return 0,0,0,false end --- [Internal] Get the max line of sight based on unit head and camera nod via trigonometry. Returns 0 if camera is off. -- @param #PLAYERRECCE self -- @param Wrapper.Unit#UNIT unit The unit which LOS we want -- @param #number vheading Heading where the unit or camera is looking -- @param #number vnod Nod down in degrees -- @param #boolean vivoff Camera on or off -- @return #number maxview Max view distance in meters function PLAYERRECCE:_GetActualMaxLOSight(unit,vheading, vnod, vivoff) self:T(self.lid.."_GetActualMaxLOSight") if vivoff then return 0 end --if vnod < -0.03 then vnod = -0.03 end local maxview = 0 if unit and unit:IsAlive() then local typename = unit:GetTypeName() maxview = self.MaxViewDistance[typename] or 8000 local CamHeight = self.Cameraheight[typename] or 0 if vnod < 0 then -- Looking down -- determine max distance we're looking at local beta = 90 local gamma = math.floor(90-vnod) local alpha = math.floor(180-beta-gamma) local a = unit:GetHeight()-unit:GetCoordinate():GetLandHeight()+CamHeight local b = a / math.sin(math.rad(alpha)) local c = b * math.sin(math.rad(gamma)) maxview = c*1.2 -- +20% end end return math.abs(maxview) end --- [User] Set callsign options for TTS output. See @{Wrapper.Group#GROUP.GetCustomCallSign}() on how to set customized callsigns. -- @param #PLAYERRECCE self -- @param #boolean ShortCallsign If true, only call out the major flight number -- @param #boolean Keepnumber If true, keep the **customized callsign** in the #GROUP name for players as-is, no amendments or numbers. -- @param #table CallsignTranslations (optional) Table to translate between DCS standard callsigns and bespoke ones. Does not apply if using customized -- callsigns from playername or group name. -- @return #PLAYERRECCE self function PLAYERRECCE:SetCallSignOptions(ShortCallsign,Keepnumber,CallsignTranslations) if not ShortCallsign or ShortCallsign == false then self.ShortCallsign = false else self.ShortCallsign = true end self.Keepnumber = Keepnumber or false self.CallsignTranslations = CallsignTranslations return self end --- [Internal] Build a ZONE_POLYGON from a given viewport of a unit -- @param #PLAYERRECCE self -- @param Wrapper.Unit#UNIT unit The unit which is looking -- @param #number vheading Heading where the unit or camera is looking -- @param #number minview Min line of sight - for lasing -- @param #number maxview Max line of sight -- @param #number angle Angle left/right to be added to heading to form a triangle -- @param #boolean camon Camera is switched on -- @param #boolean laser Zone is for lasing -- @return Core.Zone#ZONE_POLYGON ViewZone or nil if camera is off function PLAYERRECCE:_GetViewZone(unit, vheading, minview, maxview, angle, camon, laser) self:T(self.lid.."_GetViewZone") local viewzone = nil if not camon then return nil end if unit and unit:IsAlive() then local unitname = unit:GetName() if not laser then -- Triangle local startpos = unit:GetCoordinate() local heading1 = (vheading+angle)%360 local heading2 = (vheading-angle)%360 local pos1 = startpos:Translate(maxview,heading1) local pos2 = startpos:Translate(maxview,heading2) local array = {} table.insert(array,startpos:GetVec2()) table.insert(array,pos1:GetVec2()) table.insert(array,pos2:GetVec2()) viewzone = ZONE_POLYGON:NewFromPointsArray(unitname,array) else -- Square local startp = unit:GetCoordinate() local heading1 = (vheading+90)%360 local heading2 = (vheading-90)%360 self:T({heading1,heading2}) local startpos = startp:Translate(minview,vheading) local pos1 = startpos:Translate(12.5,heading1) local pos2 = startpos:Translate(12.5,heading2) local pos3 = pos1:Translate(maxview,vheading) local pos4 = pos2:Translate(maxview,vheading) local array = {} table.insert(array,pos1:GetVec2()) table.insert(array,pos2:GetVec2()) table.insert(array,pos4:GetVec2()) table.insert(array,pos3:GetVec2()) viewzone = ZONE_POLYGON:NewFromPointsArray(unitname,array) end end return viewzone end --- [Internal] --@param #PLAYERRECCE self --@param Wrapper.Client#CLIENT client --@return Core.Set#SET_UNIT Set of targets, can be empty! --@return #number count Count of targets function PLAYERRECCE:_GetKnownTargets(client) self:T(self.lid.."_GetKnownTargets") local finaltargets = SET_UNIT:New() local targets = self.TargetCache:GetDataTable() local playername = client:GetPlayerName() for _,_target in pairs(targets) do local targetdata = _target.PlayerRecceDetected -- Ops.PlayerRecce#PLAYERRECCE.PlayerRecceDetected if targetdata.playername == playername then finaltargets:Add(_target:GetName(),_target) end end return finaltargets,finaltargets:CountAlive() end --- [Internal] --@param #PLAYERRECCE self --@return #PLAYERRECCE self function PLAYERRECCE:_CleanupTargetCache() self:T(self.lid.."_CleanupTargetCache") local cleancache = FIFO:New() self.TargetCache:ForEach( function(unit) local pull = false if unit and unit:IsAlive() and unit:GetLife() > 1 then if unit.PlayerRecceDetected and unit.PlayerRecceDetected.timestamp then local TNow = timer.getTime() if TNow-unit.PlayerRecceDetected.timestamp > self.TForget then -- Forget this unit pull = true unit.PlayerRecceDetected=nil end else -- no timestamp pull = true end else -- dead pull = true end if not pull then cleancache:Push(unit,unit:GetName()) end end ) self.TargetCache = nil self.TargetCache = cleancache return self end --- [Internal] --@param #PLAYERRECCE self --@param Wrapper.Unit#UNIT unit The FACA unit --@param #boolean camera If true, use the unit's camera for targets in sight --@param #laser laser Use laser zone --@return Core.Set#SET_UNIT Set of targets, can be empty! --@return #number count Count of targets function PLAYERRECCE:_GetTargetSet(unit,camera,laser) self:T(self.lid.."_GetTargetSet") local finaltargets = SET_UNIT:New() local finalcount = 0 local minview = 0 local typename = unit:GetTypeName() local playername = unit:GetPlayerName() local maxview = self.MaxViewDistance[typename] or 5000 local heading,nod,maxview,angle = 0,30,8000,10 local camon = false local name = unit:GetName() if string.find(typename,"SA342") and camera then heading,nod,maxview,camon = self:_GetGazelleVivianneSight(unit) angle=10 -- Model nod and actual TV view don't compute maxview = self.MaxViewDistance[typename] or 5000 elseif string.find(typename,"Ka-50") and camera then heading = unit:GetHeading() nod,maxview,camon = 10,1000,true angle = 10 maxview = self.MaxViewDistance[typename] or 5000 else -- visual heading = unit:GetHeading() nod,maxview,camon = 10,1000,true angle = 45 end if laser then -- get min/max values if not self.LaserFOV[playername] then minview = 100 maxview = 2000 self.LaserFOV[playername] = { min=100, max=2000, } else minview = self.LaserFOV[playername].min maxview = self.LaserFOV[playername].max end end local zone = self:_GetViewZone(unit,heading,minview,maxview,angle,camon,laser) if zone then local redcoalition = "red" if self.Coalition == coalition.side.RED then redcoalition = "blue" end -- determine what we can see local startpos = unit:GetCoordinate() local targetset = SET_UNIT:New():FilterCategories("ground"):FilterActive(true):FilterZones({zone}):FilterCoalitions(redcoalition):FilterOnce() self:T("Prefilter Target Count = "..targetset:CountAlive()) -- TODO - Threat level filter? -- TODO - Min distance from unit? targetset:ForEach( function(_unit) local _unit = _unit -- Wrapper.Unit#UNIT local _unitpos = _unit:GetCoordinate() if startpos:IsLOS(_unitpos) and _unit:IsAlive() and _unit:GetLife()>1 then self:T("Adding to final targets: ".._unit:GetName()) finaltargets:Add(_unit:GetName(),_unit) end end ) finalcount = finaltargets:CountAlive() self:T(string.format("%s Unit: %s | Targets in view %s",self.lid,name,finalcount)) end return finaltargets, finalcount, zone end ---[Internal] --@param #PLAYERRECCE self --@param Core.Set#SET_UNIT targetset Set of targets, can be empty! --@return Wrapper.Unit#UNIT Target or nil function PLAYERRECCE:_GetHVTTarget(targetset) self:T(self.lid.."_GetHVTTarget") -- sort units local unitsbythreat = {} local minthreat = self.minthreatlevel or 0 for _,_unit in pairs(targetset.Set) do local unit = _unit -- Wrapper.Unit#UNIT if unit and unit:IsAlive() and unit:GetLife() >1 then local threat = unit:GetThreatLevel() if threat >= minthreat then -- prefer radar units if unit:HasAttribute("RADAR_BAND1_FOR_ARM") or unit:HasAttribute("RADAR_BAND2_FOR_ARM") or unit:HasAttribute("Optical Tracker") then threat = 11 end table.insert(unitsbythreat,{unit,threat}) end end end table.sort(unitsbythreat, function(a,b) local aNum = a[2] -- Coin value of a local bNum = b[2] -- Coin value of b return aNum > bNum -- Return their comparisons, < for ascending, > for descending end) if unitsbythreat[1] and unitsbythreat[1][1] then return unitsbythreat[1][1] else return nil end end --- [Internal] --@param #PLAYERRECCE self --@param Wrapper.Client#CLIENT client The FACA unit --@param Core.Set#SET_UNIT targetset Set of targets, can be empty! --@return #PLAYERRECCE self function PLAYERRECCE:_LaseTarget(client,targetset) self:T(self.lid.."_LaseTarget") -- get one target local target = self:_GetHVTTarget(targetset) -- Wrapper.Unit#UNIT local playername = client:GetPlayerName() local laser = nil -- Core.Spot#SPOT -- set laser if not self.LaserSpots[playername] then laser = SPOT:New(client) if not self.UnitLaserCodes[playername] then self.UnitLaserCodes[playername] = 1688 end laser.LaserCode = self.UnitLaserCodes[playername] or 1688 self.LaserSpots[playername] = laser else laser = self.LaserSpots[playername] end -- old target if self.LaserTarget[playername] then -- still looking at target? local target=self.LaserTarget[playername] -- Ops.Target#TARGET local oldtarget = target:GetObject() --or laser.Target self:T("Targetstate: "..target:GetState()) self:T("Laser State: "..tostring(laser:IsLasing())) if (not oldtarget) or targetset:IsNotInSet(oldtarget) or target:IsDead() or target:IsDestroyed() then -- lost LOS or dead laser:LaseOff() if target:IsDead() or target:IsDestroyed() or target:GetLife() < 2 then self:__Shack(-1,client,oldtarget) --self.LaserTarget[playername] = nil else self:__TargetLOSLost(-1,client,oldtarget) --self.LaserTarget[playername] = nil end self.LaserTarget[playername] = nil oldtarget = nil self.LaserSpots[playername] = nil elseif oldtarget and laser and (not laser:IsLasing()) then --laser:LaseOff() self:T("Switching laser back on ..") local lasercode = self.UnitLaserCodes[playername] or laser.LaserCode or 1688 local lasingtime = self.lasingtime or 60 --local targettype = target:GetTypeName() laser:LaseOn(oldtarget,lasercode,lasingtime) --self:__TargetLasing(-1,client,oldtarget,lasercode,lasingtime) else -- we should not be here... self:T("Target alive and laser is on!") --self.LaserSpots[playername] = nil end -- new target elseif (not laser:IsLasing()) and target then local relativecam = self.LaserRelativePos[client:GetTypeName()] laser:SetRelativeStartPosition(relativecam) local lasercode = self.UnitLaserCodes[playername] or laser.LaserCode or 1688 local lasingtime = self.lasingtime or 60 --local targettype = target:GetTypeName() laser:LaseOn(target,lasercode,lasingtime) self.LaserTarget[playername] = TARGET:New(target) --self.LaserTarget[playername].TStatus = 9 self:__TargetLasing(-1,client,target,lasercode,lasingtime) end return self end --- [Internal] -- @param #PLAYERRECCE self -- @param Wrapper.Client#CLIENT client -- @param Wrapper.Group#GROUP group -- @param #string playername -- @return #PLAYERRECCE self function PLAYERRECCE:_SetClientLaserCode(client,group,playername,code) self:T(self.lid.."_SetClientLaserCode") self.UnitLaserCodes[playername] = code or 1688 if self.ClientMenus[playername] then self.ClientMenus[playername]:Remove() self.ClientMenus[playername]=nil end self:_BuildMenus() return self end --- [Internal] -- @param #PLAYERRECCE self -- @param Wrapper.Client#CLIENT client -- @param Wrapper.Group#GROUP group -- @param #string playername -- @return #PLAYERRECCE self function PLAYERRECCE:_SwitchOnStation(client,group,playername) self:T(self.lid.."_SwitchOnStation") if not self.OnStation[playername] then self.OnStation[playername] = true self:__RecceOnStation(-1,client,playername) else self.OnStation[playername] = false self:__RecceOffStation(-1,client,playername) end if self.ClientMenus[playername] then self.ClientMenus[playername]:Remove() self.ClientMenus[playername]=nil end self:_BuildMenus(client) return self end --- [Internal] -- @param #PLAYERRECCE self -- @param Wrapper.Client#CLIENT client -- @param Wrapper.Group#GROUP group -- @param #string playername -- @return #PLAYERRECCE self function PLAYERRECCE:_SwitchSmoke(client,group,playername) self:T(self.lid.."_SwitchLasing") if not self.SmokeOwn[playername] then self.SmokeOwn[playername] = true MESSAGE:New("Smoke self is now ON",10,self.Name or "FACA"):ToClient(client) else self.SmokeOwn[playername] = false MESSAGE:New("Smoke self is now OFF",10,self.Name or "FACA"):ToClient(client) end if self.ClientMenus[playername] then self.ClientMenus[playername]:Remove() self.ClientMenus[playername]=nil end self:_BuildMenus(client) return self end --- [Internal] -- @param #PLAYERRECCE self -- @param Wrapper.Client#CLIENT client -- @param Wrapper.Group#GROUP group -- @param #string playername -- @return #PLAYERRECCE self function PLAYERRECCE:_SwitchLasing(client,group,playername) self:T(self.lid.."_SwitchLasing") if not self.AutoLase[playername] then self.AutoLase[playername] = true MESSAGE:New("Lasing is now ON",10,self.Name or "FACA"):ToClient(client) else self.AutoLase[playername] = false if self.LaserSpots[playername] then local laser = self.LaserSpots[playername] -- Core.Spot#SPOT if laser:IsLasing() then laser:LaseOff() end self.LaserSpots[playername] = nil end MESSAGE:New("Lasing is now OFF",10,self.Name or "FACA"):ToClient(client) end if self.ClientMenus[playername] then self.ClientMenus[playername]:Remove() self.ClientMenus[playername]=nil end self:_BuildMenus(client) return self end --- [Internal] -- @param #PLAYERRECCE self -- @param Wrapper.Client#CLIENT client -- @param Wrapper.Group#GROUP group -- @param #string playername -- @param #number mindist -- @param #number maxdist -- @return #PLAYERRECCE self function PLAYERRECCE:_SwitchLasingDist(client,group,playername,mindist,maxdist) self:T(self.lid.."_SwitchLasingDist") local mind = mindist or 100 local maxd = maxdist or 2000 if not self.LaserFOV[playername] then self.LaserFOV[playername] = { min=mind, max=maxd, } else self.LaserFOV[playername].min=mind self.LaserFOV[playername].max=maxd end MESSAGE:New(string.format("Laser distance set to %d-%dm!",mindist,maxdist),10,"FACA"):ToClient(client) if self.ClientMenus[playername] then self.ClientMenus[playername]:Remove() self.ClientMenus[playername]=nil end self:_BuildMenus(client) return self end --- [Internal] -- @param #PLAYERRECCE self -- @param Wrapper.Client#CLIENT client -- @param Wrapper.Group#GROUP group -- @param #string playername -- @return #PLAYERRECCE self function PLAYERRECCE:_WIP(client,group,playername) self:T(self.lid.."_WIP") return self end --- [Internal] -- @param #PLAYERRECCE self -- @param Wrapper.Client#CLIENT client -- @param Wrapper.Group#GROUP group -- @param #string playername -- @return #PLAYERRECCE self function PLAYERRECCE:_SmokeTargets(client,group,playername) self:T(self.lid.."_SmokeTargets") local cameraset = self:_GetTargetSet(client,true) -- Core.Set#SET_UNIT local visualset = self:_GetTargetSet(client,false) -- Core.Set#SET_UNIT if cameraset:CountAlive() > 0 or visualset:CountAlive() > 0 then self:__TargetsSmoked(-1,client,playername,cameraset) else return self end local highsmoke = self.SmokeColor.highsmoke local medsmoke = self.SmokeColor.medsmoke local lowsmoke = self.SmokeColor.lowsmoke local lasersmoke = self.SmokeColor.lasersmoke local laser = self.LaserSpots[playername] -- Core.Spot#SPOT -- laser targer gets extra smoke if laser and laser.Target and laser.Target:IsAlive() then laser.Target:GetCoordinate():Smoke(lasersmoke) end local coord = visualset:GetCoordinate() if coord and self.smokeaveragetargetpos then coord:SetAtLandheight() coord:Smoke(medsmoke) else -- smoke everything for _,_unit in pairs(visualset.Set) do local unit = _unit --Wrapper.Unit#UNIT if unit and unit:IsAlive() then local coord = unit:GetCoordinate() local threat = unit:GetThreatLevel() if coord then local color = lowsmoke if threat > 7 then color = highsmoke elseif threat > 2 then color = medsmoke end coord:Smoke(color) end end end end if self.SmokeOwn[playername] then local cc = client:GetVec2() -- don't smoke mid-air local lc = COORDINATE:NewFromVec2(cc,1) local color = self.SmokeColor.ownsmoke lc:Smoke(color) end return self end --- [Internal] -- @param #PLAYERRECCE self -- @param Wrapper.Client#CLIENT client -- @param Wrapper.Group#GROUP group -- @param #string playername -- @return #PLAYERRECCE self function PLAYERRECCE:_FlareTargets(client,group,playername) self:T(self.lid.."_FlareTargets") local cameraset = self:_GetTargetSet(client,true) -- Core.Set#SET_UNIT local visualset = self:_GetTargetSet(client,false) -- Core.Set#SET_UNIT cameraset:AddSet(visualset) if cameraset:CountAlive() > 0 then self:__TargetsFlared(-1,client,playername,cameraset) end local highsmoke = self.FlareColor.highflare local medsmoke = self.FlareColor.medflare local lowsmoke = self.FlareColor.lowflare local lasersmoke = self.FlareColor.laserflare local laser = self.LaserSpots[playername] -- Core.Spot#SPOT -- laser targer gets extra smoke if laser and laser.Target and laser.Target:IsAlive() then laser.Target:GetCoordinate():Flare(lasersmoke) if cameraset:IsInSet(laser.Target) then cameraset:Remove(laser.Target:GetName(),true) end end -- smoke everything else for _,_unit in pairs(cameraset.Set) do local unit = _unit --Wrapper.Unit#UNIT if unit and unit:IsAlive() then local coord = unit:GetCoordinate() local threat = unit:GetThreatLevel() if coord then local color = lowsmoke if threat > 7 then color = highsmoke elseif threat > 2 then color = medsmoke end coord:Flare(color) end end end return self end --- [Internal] -- @param #PLAYERRECCE self -- @param Wrapper.Client#CLIENT client -- @param Wrapper.Group#GROUP group -- @param #string playername -- @return #PLAYERRECCE self function PLAYERRECCE:_IlluTargets(client,group,playername) self:T(self.lid.."_IlluTargets") local totalset, count = self:_GetKnownTargets(client) -- Core.Set#SET_UNIT if count > 0 then local coord = totalset:GetCoordinate() -- Core.Point#COORDINATE coord.y = coord.y + 200 coord:IlluminationBomb(nil,1) self:__Illumination(1,client,playername,totalset) end return self end --- [Internal] -- @param #PLAYERRECCE self -- @param Wrapper.Client#CLIENT client -- @param Wrapper.Group#GROUP group -- @param #string playername -- @return #PLAYERRECCE self function PLAYERRECCE:_UploadTargets(client,group,playername) self:T(self.lid.."_UploadTargets") --local targetset, number = self:_GetTargetSet(client,true) --local vtargetset, vnumber = self:_GetTargetSet(client,false) local totalset, count = self:_GetKnownTargets(client) --local totalset = SET_UNIT:New() -- totalset:AddSet(targetset) --totalset:AddSet(vtargetset) if count > 0 then self.Controller:AddTarget(totalset) self:__TargetReportSent(1,client,playername,totalset) end return self end --- [Internal] -- @param #PLAYERRECCE self -- @param Wrapper.Client#CLIENT client -- @param Wrapper.Group#GROUP group -- @param #string playername -- @return #PLAYERRECCE self function PLAYERRECCE:_ReportLaserTargets(client,group,playername) self:T(self.lid.."_ReportLaserTargets") local targetset, number = self:_GetTargetSet(client,true,true) if number > 0 and self.AutoLase[playername] then local Settings = ( client and _DATABASE:GetPlayerSettings( playername ) ) or _SETTINGS local target = self:_GetHVTTarget(targetset) -- the one we're lasing local ThreatLevel = target:GetThreatLevel() or 1 local ThreatLevelText = "high" if ThreatLevel > 3 and ThreatLevel < 8 then ThreatLevelText = "medium" elseif ThreatLevel <= 3 then ThreatLevelText = "low" end local ThreatGraph = "[" .. string.rep( "■", ThreatLevel ) .. string.rep( "□", 10 - ThreatLevel ) .. "]: "..ThreatLevel local report = REPORT:New("Lasing Report") report:Add(string.rep("-",15)) report:Add("Target type: "..target:GetTypeName() or "unknown") report:Add("Threat Level: "..ThreatGraph.." ("..ThreatLevelText..")") if not self.ReferencePoint then report:Add("Location: "..client:GetCoordinate():ToStringBULLS(self.Coalition,Settings)) else report:Add("Location: "..client:GetCoordinate():ToStringFromRPShort(self.ReferencePoint,self.RPName,client,Settings)) end report:Add("Laser Code: "..self.UnitLaserCodes[playername] or 1688) report:Add(string.rep("-",15)) local text = report:Text() self:__TargetReport(1,client,targetset,target,text) else local report = REPORT:New("Lasing Report") report:Add(string.rep("-",15)) report:Add("N O T A R G E T S") report:Add(string.rep("-",15)) local text = report:Text() self:__TargetReport(1,client,nil,nil,text) end return self end --- [Internal] -- @param #PLAYERRECCE self -- @param Wrapper.Client#CLIENT client -- @param Wrapper.Group#GROUP group -- @param #string playername -- @return #PLAYERRECCE self function PLAYERRECCE:_ReportVisualTargets(client,group,playername) self:T(self.lid.."_ReportVisualTargets") local targetset, number = self:_GetKnownTargets(client) if number > 0 then local Settings = ( client and _DATABASE:GetPlayerSettings( playername ) ) or _SETTINGS local ThreatLevel = targetset:CalculateThreatLevelA2G() local ThreatLevelText = "high" if ThreatLevel > 3 and ThreatLevel < 8 then ThreatLevelText = "medium" elseif ThreatLevel <= 3 then ThreatLevelText = "low" end local ThreatGraph = "[" .. string.rep( "■", ThreatLevel ) .. string.rep( "□", 10 - ThreatLevel ) .. "]: "..ThreatLevel local report = REPORT:New("Target Report") report:Add(string.rep("-",15)) report:Add("Target count: "..number) report:Add("Threat Level: "..ThreatGraph.." ("..ThreatLevelText..")") if not self.ReferencePoint then report:Add("Location: "..client:GetCoordinate():ToStringBULLS(self.Coalition,Settings)) else report:Add("Location: "..client:GetCoordinate():ToStringFromRPShort(self.ReferencePoint,self.RPName,client,Settings)) end report:Add(string.rep("-",15)) local text = report:Text() self:__TargetReport(1,client,targetset,nil,text) else local report = REPORT:New("Target Report") report:Add(string.rep("-",15)) report:Add("N O T A R G E T S") report:Add(string.rep("-",15)) local text = report:Text() self:__TargetReport(1,client,nil,nil,text) end return self end --- [Internal] Build Menus -- @param #PLAYERRECCE self -- @param Wrapper.Client#CLIENT Client (optional) Client object -- @return #PLAYERRECCE self function PLAYERRECCE:_BuildMenus(Client) self:T(self.lid.."_BuildMenus") local clients = self.PlayerSet -- Core.Set#SET_CLIENT local clientset = clients:GetSetObjects() if Client then clientset = {Client} end for _,_client in pairs(clientset) do local client = _client -- Wrapper.Client#CLIENT if client and client:IsAlive() then local playername = client:GetPlayerName() if not self.UnitLaserCodes[playername] then self:_SetClientLaserCode(nil,nil,playername,1688) end if self.SmokeOwn[playername] == nil then self.SmokeOwn[playername] = self.smokeownposition end local group = client:GetGroup() if not self.ClientMenus[playername] then local canlase = self.CanLase[client:GetTypeName()] self.ClientMenus[playername] = MENU_GROUP:New(group,self.MenuName or self.Name or "RECCE") local txtonstation = self.OnStation[playername] and "ON" or "OFF" local text = string.format("Switch On-Station (%s)",txtonstation) local onstationmenu = MENU_GROUP_COMMAND:New(group,text,self.ClientMenus[playername],self._SwitchOnStation,self,client,group,playername) if self.OnStation[playername] then local smoketopmenu = MENU_GROUP:New(group,"Visual Markers",self.ClientMenus[playername]) local smokemenu = MENU_GROUP_COMMAND:New(group,"Smoke Targets",smoketopmenu,self._SmokeTargets,self,client,group,playername) local flaremenu = MENU_GROUP_COMMAND:New(group,"Flare Targets",smoketopmenu,self._FlareTargets,self,client,group,playername) local illumenu = MENU_GROUP_COMMAND:New(group,"Illuminate Area",smoketopmenu,self._IlluTargets,self,client,group,playername) local ownsm = self.SmokeOwn[playername] and "ON" or "OFF" local owntxt = string.format("Switch smoke self (%s)",ownsm) local ownsmoke = MENU_GROUP_COMMAND:New(group,owntxt,smoketopmenu,self._SwitchSmoke,self,client,group,playername) if canlase then local txtonstation = self.AutoLase[playername] and "ON" or "OFF" local text = string.format("Switch Lasing (%s)",txtonstation) local lasemenu = MENU_GROUP_COMMAND:New(group,text,self.ClientMenus[playername],self._SwitchLasing,self,client,group,playername) local lasedist = MENU_GROUP:New(group,"Set Laser Distance",self.ClientMenus[playername]) local mindist = 100 local maxdist = 2000 if self.LaserFOV[playername] and self.LaserFOV[playername].max then maxdist = self.LaserFOV[playername].max end local laselist={} for i=2,8 do local dist1 = (i*1000)-1000 local dist2 = i*1000 dist1 = dist1 == 1000 and 100 or dist1 local text = string.format("%d-%dm",dist1,dist2) if dist2 == maxdist then text = text .. " (*)" end laselist[i] = MENU_GROUP_COMMAND:New(group,text,lasedist,self._SwitchLasingDist,self,client,group,playername,dist1,dist2) end end local targetmenu = MENU_GROUP:New(group,"Target Report",self.ClientMenus[playername]) if canlase then local reportL = MENU_GROUP_COMMAND:New(group,"Laser Target",targetmenu,self._ReportLaserTargets,self,client,group,playername) end local reportV = MENU_GROUP_COMMAND:New(group,"Visual Targets",targetmenu,self._ReportVisualTargets,self,client,group,playername) if self.UseController then local text = string.format("Target Upload to %s",self.Controller.MenuName or self.Controller.Name) local upload = MENU_GROUP_COMMAND:New(group,text,targetmenu,self._UploadTargets,self,client,group,playername) end if canlase then local lasecodemenu = MENU_GROUP:New(group,"Set Laser Code",self.ClientMenus[playername]) local codemenu = {} for _,_code in pairs(self.LaserCodes) do --self._SetClientLaserCode,self,client,group,playername) if _code == self.UnitLaserCodes[playername] then _code = tostring(_code).."(*)" end codemenu[playername.._code] = MENU_GROUP_COMMAND:New(group,tostring(_code),lasecodemenu,self._SetClientLaserCode,self,client,group,playername,_code) end end end end end end return self end --- [Internal] -- @param #PLAYERRECCE self -- @param Core.Set#SET_UNIT targetset -- @param Wrapper.Client#CLIENT client -- @param #string playername -- @return #PLAYERRECCE self function PLAYERRECCE:_CheckNewTargets(targetset,client,playername) self:T(self.lid.."_CheckNewTargets") local tempset = SET_UNIT:New() targetset:ForEach( function(unit) if unit and unit:IsAlive() then self:T("Report unit: "..unit:GetName()) if not unit.PlayerRecceDetected then self:T("New unit: "..unit:GetName()) unit.PlayerRecceDetected = { detected = true, recce = client, playername = playername, timestamp = timer.getTime() } --self:TargetDetected(unit,client,playername) tempset:Add(unit:GetName(),unit) if not self.TargetCache:HasUniqueID(unit:GetName()) then self.TargetCache:Push(unit,unit:GetName()) end end if unit.PlayerRecceDetected and unit.PlayerRecceDetected.timestamp then local TNow = timer.getTime() if TNow-unit.PlayerRecceDetected.timestamp > self.TForget then unit.PlayerRecceDetected = { detected = true, recce = client, playername = playername, timestamp = timer.getTime() } if not self.TargetCache:HasUniqueID(unit:GetName()) then self.TargetCache:Push(unit,unit:GetName()) end tempset:Add(unit:GetName(),unit) end end end end ) local targetsbyclock = {} for i=1,12 do targetsbyclock[i] = {} end tempset:ForEach( function (object) local obj=object -- Wrapper.Unit#UNIT local clock = self:_GetClockDirection(client,obj) table.insert(targetsbyclock[clock],obj) end ) self:T("Known target Count: "..self.TargetCache:Count()) if tempset:CountAlive() > 0 then self:TargetDetected(targetsbyclock,client,playername) end return self end --- [User] Set SRS TTS details - see @{Sound.SRS} for details -- @param #PLAYERRECCE self -- @param #number Frequency Frequency to be used. Can also be given as a table of multiple frequencies, e.g. 271 or {127,251}. There needs to be exactly the same number of modulations! -- @param #number Modulation Modulation to be used. Can also be given as a table of multiple modulations, e.g. radio.modulation.AM or {radio.modulation.FM,radio.modulation.AM}. There needs to be exactly the same number of frequencies! -- @param #string PathToSRS Defaults to "C:\\Program Files\\DCS-SimpleRadio-Standalone" -- @param #string Gender (Optional) Defaults to "male" -- @param #string Culture (Optional) Defaults to "en-US" -- @param #number Port (Optional) Defaults to 5002 -- @param #string Voice (Optional) Use a specifc voice with the @{Sound.SRS#SetVoice} function, e.g, `:SetVoice("Microsoft Hedda Desktop")`. -- Note that this must be installed on your windows system. Can also be Google voice types, if you are using Google TTS. -- @param #number Volume (Optional) Volume - between 0.0 (silent) and 1.0 (loudest) -- @param #string PathToGoogleKey (Optional) Path to your google key if you want to use google TTS -- @return #PLAYERRECCE self function PLAYERRECCE:SetSRS(Frequency,Modulation,PathToSRS,Gender,Culture,Port,Voice,Volume,PathToGoogleKey) self:T(self.lid.."SetSRS") self.PathToSRS = PathToSRS or MSRS.path or "C:\\Program Files\\DCS-SimpleRadio-Standalone" -- self.Gender = Gender or MSRS.gender or "male" -- self.Culture = Culture or MSRS.culture or "en-US" -- self.Port = Port or MSRS.port or 5002 -- self.Voice = Voice or MSRS.voice -- self.PathToGoogleKey = PathToGoogleKey -- self.Volume = Volume or 1.0 -- self.UseSRS = true self.Frequency = Frequency or {127,251} -- self.BCFrequency = self.Frequency self.Modulation = Modulation or {radio.modulation.FM,radio.modulation.AM} -- self.BCModulation = self.Modulation -- set up SRS self.SRS=MSRS:New(self.PathToSRS,self.Frequency,self.Modulation) self.SRS:SetCoalition(self.Coalition) self.SRS:SetLabel(self.MenuName or self.Name) self.SRS:SetGender(self.Gender) self.SRS:SetCulture(self.Culture) self.SRS:SetPort(self.Port) self.SRS:SetVolume(self.Volume) if self.PathToGoogleKey then self.SRS:SetProviderOptionsGoogle(self.PathToGoogleKey,self.PathToGoogleKey) self.SRS:SetProvider(MSRS.Provider.GOOGLE) end -- Pre-configured Google? if (not PathToGoogleKey) and self.SRS:GetProvider() == MSRS.Provider.GOOGLE then self.PathToGoogleKey = MSRS.poptions.gcloud.credentials self.Voice = Voice or MSRS.poptions.gcloud.voice end self.SRS:SetVoice(self.Voice) self.SRSQueue = MSRSQUEUE:New(self.MenuName or self.Name) self.SRSQueue:SetTransmitOnlyWithPlayers(self.TransmitOnlyWithPlayers) return self end --- [User] For SRS - Switch to only transmit if there are players on the server. -- @param #PLAYERRECCE self -- @param #boolean Switch If true, only send SRS if there are alive Players. -- @return #PLAYERRECCE self function PLAYERRECCE:SetTransmitOnlyWithPlayers(Switch) self.TransmitOnlyWithPlayers = Switch if self.SRSQueue then self.SRSQueue:SetTransmitOnlyWithPlayers(Switch) end return self end --- [User] Set the top menu name to a custom string. -- @param #PLAYERRECCE self -- @param #string Name The name to use as the top menu designation. -- @return #PLAYERRECCE self function PLAYERRECCE:SetMenuName(Name) self:T(self.lid.."SetMenuName: "..Name) self.MenuName = Name return self end --- [User] Enable smoking of own position -- @param #PLAYERRECCE self -- @return #PLAYERRECCE self function PLAYERRECCE:EnableSmokeOwnPosition() self:T(self.lid.."EnableSmokeOwnPosition") self.smokeownposition = true return self end --- [User] Disable smoking of own position -- @param #PLAYERRECCE self -- @return #PLAYERRECCE function PLAYERRECCE:DisableSmokeOwnPosition() self:T(self.lid.."DisableSmokeOwnPosition") self.smokeownposition = false return self end --- [User] Enable smoking of average target positions, instead of all targets visible. Loses smoke per threatlevel -- each is med threat. Default is - smoke all positions. -- @param #PLAYERRECCE self -- @return #PLAYERRECCE self function PLAYERRECCE:EnableSmokeAverageTargetPosition() self:T(self.lid.."ENableSmokeOwnPosition") self.smokeaveragetargetpos = true return self end --- [User] Disable smoking of average target positions, instead of all targets visible. Default is - smoke all positions. -- @param #PLAYERRECCE self -- @return #PLAYERRECCE function PLAYERRECCE:DisableSmokeAverageTargetPosition() self:T(self.lid.."DisableSmokeAverageTargetPosition") self.smokeaveragetargetpos = false return self end --- [Internal] Get text for text-to-speech. -- Numbers are spaced out, e.g. "Heading 180" becomes "Heading 1 8 0 ". -- @param #PLAYERRECCE self -- @param #string text Original text. -- @return #string Spoken text. function PLAYERRECCE:_GetTextForSpeech(text) -- Space out numbers. text=string.gsub(text,"%d","%1 ") -- get rid of leading or trailing spaces text=string.gsub(text,"^%s*","") text=string.gsub(text,"%s*$","") text=string.gsub(text,"0","zero") text=string.gsub(text,"9","niner") text=string.gsub(text," "," ") return text end ------------------------------------------------------------------------------------------ -- TODO: FSM Functions ------------------------------------------------------------------------------------------ --- [Internal] Status Loop -- @param #PLAYERRECCE self -- @param #string From -- @param #string Event -- @param #string To -- @return #PLAYERRECCE self function PLAYERRECCE:onafterStatus(From, Event, To) self:T({From, Event, To}) if not self.timestamp then self.timestamp = timer.getTime() else local tNow = timer.getTime() if tNow - self.timestamp >= 60 then self:_CleanupTargetCache() self.timestamp = timer.getTime() end end self:_BuildMenus() self.PlayerSet:ForEachClient( function(Client) local client = Client -- Wrapper.Client#CLIENT local playername = client:GetPlayerName() local cameraison = self:_CameraOn(client,playername) if client and client:IsAlive() and self.OnStation[playername] then --- local targetset, targetcount, tzone = nil,0,nil local laserset, lzone = nil,nil local vistargetset, vistargetcount, viszone = nil,0,nil -- targets on camera if cameraison then targetset, targetcount, tzone = self:_GetTargetSet(client,true) if targetset then if self.ViewZone[playername] then self.ViewZone[playername]:UndrawZone() end if self.debug and tzone then self.ViewZone[playername]=tzone:DrawZone(self.Coalition,{0,0,1},nil,nil,nil,1) end end self:T({targetcount=targetcount}) end -- lase targets on camera if self.AutoLase[playername] and cameraison then laserset, targetcount, lzone = self:_GetTargetSet(client,true,true) if targetcount > 0 or self.LaserTarget[playername] then if self.CanLase[client:GetTypeName()] then -- DONE move to lase at will self:_LaseTarget(client,laserset) end end if lzone then if self.ViewZoneLaser[playername] then self.ViewZoneLaser[playername]:UndrawZone() end if self.debug and tzone then self.ViewZoneLaser[playername]=lzone:DrawZone(self.Coalition,{0,1,0},nil,nil,nil,1) end end self:T({lasercount=targetcount}) end -- visual targets vistargetset, vistargetcount, viszone = self:_GetTargetSet(client,false) if vistargetset then if self.ViewZoneVisual[playername] then self.ViewZoneVisual[playername]:UndrawZone() end if self.debug and viszone then self.ViewZoneVisual[playername]=viszone:DrawZone(self.Coalition,{1,0,0},nil,nil,nil,3) end end self:T({visualtargetcount=vistargetcount}) if targetset then vistargetset:AddSet(targetset) end if laserset then vistargetset:AddSet(laserset) end if not cameraison and self.debug then if self.ViewZoneLaser[playername] then self.ViewZoneLaser[playername]:UndrawZone() end if self.ViewZone[playername] then self.ViewZone[playername]:UndrawZone() end end self:_CheckNewTargets(vistargetset,client,playername) end end ) self:__Status(-10) return self end --- [Internal] Recce on station -- @param #PLAYERRECCE self -- @param #string From -- @param #string Event -- @param #string To -- @param Wrapper.Client#CLIENT Client -- @param #string Playername -- @return #PLAYERRECCE self function PLAYERRECCE:onafterRecceOnStation(From, Event, To, Client, Playername) self:T({From, Event, To}) local callsign = Client:GetGroup():GetCustomCallSign(self.ShortCallsign,self.Keepnumber,self.CallsignTranslations) local coord = Client:GetCoordinate() local coordtext = coord:ToStringBULLS(self.Coalition) if self.ReferencePoint then local Settings = Client and _DATABASE:GetPlayerSettings(Playername) or _SETTINGS -- Core.Settings#SETTINGS coordtext = coord:ToStringFromRPShort(self.ReferencePoint,self.RPName,Client,Settings) end local text1 = "Party time!" local text2 = string.format("All stations, FACA %s on station\nat %s!",callsign, coordtext) local text2tts = string.format("All stations, FACA %s on station at %s!",callsign, coordtext) text2tts = self:_GetTextForSpeech(text2tts) if self.debug then self:T(text2.."\n"..text2tts) end if self.UseSRS then local grp = Client:GetGroup() local coord = grp:GetCoordinate() if coord then self.SRS:SetCoordinate(coord) end self.SRSQueue:NewTransmission(text1,nil,self.SRS,nil,2) self.SRSQueue:NewTransmission(text2tts,nil,self.SRS,nil,3) MESSAGE:New(text2,10,self.Name or "FACA"):ToCoalition(self.Coalition) else MESSAGE:New(text1,10,self.Name or "FACA"):ToClient(Client) MESSAGE:New(text2,10,self.Name or "FACA"):ToCoalition(self.Coalition) end return self end --- [Internal] Recce off station -- @param #PLAYERRECCE self -- @param #string From -- @param #string Event -- @param #string To -- @param Wrapper.Client#CLIENT Client -- @param #string Playername -- @return #PLAYERRECCE self function PLAYERRECCE:onafterRecceOffStation(From, Event, To, Client, Playername) self:T({From, Event, To}) local callsign = Client:GetGroup():GetCustomCallSign(self.ShortCallsign,self.Keepnumber,self.CallsignTranslations) local coord = Client:GetCoordinate() local coordtext = coord:ToStringBULLS(self.Coalition) if self.ReferencePoint then local Settings = Client and _DATABASE:GetPlayerSettings(Playername) or _SETTINGS -- Core.Settings#SETTINGS coordtext = coord:ToStringFromRPShort(self.ReferencePoint,self.RPName,Client,Settings) end local text = string.format("All stations, FACA %s leaving station\nat %s, good bye!",callsign, coordtext) local texttts = string.format("All stations, FACA %s leaving station at %s, good bye!",callsign, coordtext) texttts = self:_GetTextForSpeech(texttts) if self.debug then self:T(text.."\n"..texttts) end local text1 = "Going home!" if self.UseSRS then local grp = Client:GetGroup() local coord = grp:GetCoordinate() if coord then self.SRS:SetCoordinate(coord) end self.SRSQueue:NewTransmission(text1,nil,self.SRS,nil,2) self.SRSQueue:NewTransmission(texttts,nil,self.SRS,nil,3) MESSAGE:New(text,10,self.Name or "FACA"):ToCoalition(self.Coalition) else MESSAGE:New(text,10,self.Name or "FACA"):ToCoalition(self.Coalition) end return self end --- [Internal] Target Detected -- @param #PLAYERRECCE self -- @param #string From -- @param #string Event -- @param #string To -- @param #table Targetsbyclock. #table with index 1..12 containing a #table of Wrapper.Unit#UNIT objects each. -- @param Wrapper.Client#CLIENT Client -- @param #string Playername -- @return #PLAYERRECCE self function PLAYERRECCE:onafterTargetDetected(From, Event, To, Targetsbyclock, Client, Playername) self:T({From, Event, To}) local dunits = "meters" local Settings = Client and _DATABASE:GetPlayerSettings(Playername) or _SETTINGS -- Core.Settings#SETTINGS local clientcoord = Client:GetCoordinate() for i=1,12 do local targets = Targetsbyclock[i] --#table local targetno = #targets if targetno == 1 then -- only one local targetdistance = clientcoord:Get2DDistance(targets[1]:GetCoordinate()) or 100 local Threatlvl = targets[1]:GetThreatLevel() local ThreatTxt = "Low" if Threatlvl >=7 then ThreatTxt = "Medium" elseif Threatlvl >=3 then ThreatTxt = "High" end if Settings:IsMetric() then targetdistance = UTILS.Round(targetdistance,-2) if targetdistance >= 1000 then targetdistance = UTILS.Round(targetdistance/1000,0) dunits = "kilometer" end else if UTILS.MetersToNM(targetdistance) >=1 then targetdistance = UTILS.Round(UTILS.MetersToNM(targetdistance),0) dunits = "miles" else targetdistance = UTILS.Round(UTILS.MetersToFeet(targetdistance),-2) dunits = "feet" end end local text = string.format("Target! %s! %s o\'clock, %d %s!", ThreatTxt,i, targetdistance, dunits) local ttstext = string.format("Target! %s! %s oh clock, %d %s!", ThreatTxt, i, targetdistance, dunits) if self.UseSRS then local grp = Client:GetGroup() if clientcoord then self.SRS:SetCoordinate(clientcoord) end self.SRSQueue:NewTransmission(ttstext,nil,self.SRS,nil,1,{grp},text,10) else MESSAGE:New(text,10,self.Name or "FACA"):ToClient(Client) end elseif targetno > 1 then -- multiple local function GetNearest(TTable) local distance = 10000000 for _,_unit in pairs(TTable) do local dist = clientcoord:Get2DDistance(_unit:GetCoordinate()) or 100 if dist < distance then distance = dist end end return distance end local targetdistance = GetNearest(targets) if Settings:IsMetric() then targetdistance = UTILS.Round(targetdistance,-2) if targetdistance >= 1000 then targetdistance = UTILS.Round(targetdistance/1000,0) dunits = "kilometer" end else if UTILS.MetersToNM(targetdistance) >=1 then targetdistance = UTILS.Round(UTILS.MetersToNM(targetdistance),0) dunits = "miles" else targetdistance = UTILS.Round(UTILS.MetersToFeet(targetdistance),-2) dunits = "feet" end end local text = string.format(" %d targets! %s o\'clock, %d %s!", targetno, i, targetdistance, dunits) local ttstext = string.format("%d targets! %s oh clock, %d %s!", targetno, i, targetdistance, dunits) if self.UseSRS then local grp = Client:GetGroup() local coord = grp:GetCoordinate() if coord then self.SRS:SetCoordinate(coord) end self.SRSQueue:NewTransmission(ttstext,nil,self.SRS,nil,1,{grp},text,10) else MESSAGE:New(text,10,self.Name or "FACA"):ToClient(Client) end end end return self end --- [Internal] Targets Illuminated -- @param #PLAYERRECCE self -- @param #string From -- @param #string Event -- @param #string To -- @param Wrapper.Client#CLIENT Client -- @param #string Playername -- @param Core.Set#SET_UNIT TargetSet -- @return #PLAYERRECCE self function PLAYERRECCE:onafterIllumination(From, Event, To, Client, Playername, TargetSet) self:T({From, Event, To}) local callsign = Client:GetGroup():GetCustomCallSign(self.ShortCallsign,self.Keepnumber,self.CallsignTranslations) local coord = Client:GetCoordinate() local coordtext = coord:ToStringBULLS(self.Coalition) if self.AttackSet then for _,_client in pairs(self.AttackSet.Set) do local client = _client --Wrapper.Client#CLIENT if client and client:IsAlive() then local Settings = client and _DATABASE:GetPlayerSettings(client:GetPlayerName()) or _SETTINGS local coordtext = coord:ToStringA2G(client,Settings) if self.ReferencePoint then coordtext = coord:ToStringFromRPShort(self.ReferencePoint,self.RPName,client,Settings) end local text = string.format("All stations, %s fired illumination\nat %s!",callsign, coordtext) MESSAGE:New(text,15,self.Name or "FACA"):ToClient(client) end end end local text = "Sunshine!" local ttstext = "Sunshine!" if self.UseSRS then local grp = Client:GetGroup() local coord = grp:GetCoordinate() if coord then self.SRS:SetCoordinate(coord) end self.SRSQueue:NewTransmission(ttstext,nil,self.SRS,nil,1,{grp},text,10) else MESSAGE:New(text,10,self.Name or "FACA"):ToClient(Client) end return self end --- [Internal] Targets Smoked -- @param #PLAYERRECCE self -- @param #string From -- @param #string Event -- @param #string To -- @param Wrapper.Client#CLIENT Client -- @param #string Playername -- @param Core.Set#SET_UNIT TargetSet -- @return #PLAYERRECCE self function PLAYERRECCE:onafterTargetsSmoked(From, Event, To, Client, Playername, TargetSet) self:T({From, Event, To}) local callsign = Client:GetGroup():GetCustomCallSign(self.ShortCallsign,self.Keepnumber,self.CallsignTranslations) local coord = Client:GetCoordinate() local coordtext = coord:ToStringBULLS(self.Coalition) if self.AttackSet then for _,_client in pairs(self.AttackSet.Set) do local client = _client --Wrapper.Client#CLIENT if client and client:IsAlive() then local Settings = client and _DATABASE:GetPlayerSettings(client:GetPlayerName()) or _SETTINGS local coordtext = coord:ToStringA2G(client,Settings) if self.ReferencePoint then coordtext = coord:ToStringFromRPShort(self.ReferencePoint,self.RPName,client,Settings) end local text = string.format("All stations, %s smoked targets\nat %s!",callsign, coordtext) MESSAGE:New(text,15,self.Name or "FACA"):ToClient(client) end end end local text = "Smoke on!" local ttstext = "Smoke and Mirrors!" if self.UseSRS then local grp = Client:GetGroup() local coord = grp:GetCoordinate() if coord then self.SRS:SetCoordinate(coord) end self.SRSQueue:NewTransmission(ttstext,nil,self.SRS,nil,1,{grp},text,10) else MESSAGE:New(text,10,self.Name or "FACA"):ToClient(Client) end return self end --- [Internal] Targets Flared -- @param #PLAYERRECCE self -- @param #string From -- @param #string Event -- @param #string To -- @param Wrapper.Client#CLIENT Client -- @param #string Playername -- @param Core.Set#SET_UNIT TargetSet -- @return #PLAYERRECCE self function PLAYERRECCE:onafterTargetsFlared(From, Event, To, Client, Playername, TargetSet) self:T({From, Event, To}) local callsign = Client:GetGroup():GetCustomCallSign(self.ShortCallsign,self.Keepnumber,self.CallsignTranslations) local coord = Client:GetCoordinate() local coordtext = coord:ToStringBULLS(self.Coalition) if self.AttackSet then for _,_client in pairs(self.AttackSet.Set) do local client = _client --Wrapper.Client#CLIENT if client and client:IsAlive() then local Settings = client and _DATABASE:GetPlayerSettings(client:GetPlayerName()) or _SETTINGS if self.ReferencePoint then coordtext = coord:ToStringFromRPShort(self.ReferencePoint,self.RPName,client,Settings) end local coordtext = coord:ToStringA2G(client,Settings) local text = string.format("All stations, %s flared targets\nat %s!",callsign, coordtext) MESSAGE:New(text,15,self.Name or "FACA"):ToClient(client) end end end local text = "Fireworks!" local ttstext = "Fire works!" if self.UseSRS then local grp = Client:GetGroup() local coord = grp:GetCoordinate() if coord then self.SRS:SetCoordinate(coord) end self.SRSQueue:NewTransmission(ttstext,nil,self.SRS,nil,1,{grp},text,10) else MESSAGE:New(text,10,self.Name or "FACA"):ToClient(Client) end return self end --- [Internal] Target lasing -- @param #PLAYERRECCE self -- @param #string From -- @param #string Event -- @param #string To -- @param Wrapper.Client#CLIENT Client -- @param Wrapper.Unit#UNIT Target -- @param #number Lasercode -- @param #number Lasingtime -- @return #PLAYERRECCE self function PLAYERRECCE:onafterTargetLasing(From, Event, To, Client, Target, Lasercode, Lasingtime) self:T({From, Event, To}) local callsign = Client:GetGroup():GetCustomCallSign(self.ShortCallsign,self.Keepnumber,self.CallsignTranslations) local Settings = ( Client and _DATABASE:GetPlayerSettings( Client:GetPlayerName() ) ) or _SETTINGS local coord = Client:GetCoordinate() local coordtext = coord:ToStringBULLS(self.Coalition,Settings) if self.ReferencePoint then coordtext = coord:ToStringFromRPShort(self.ReferencePoint,self.RPName,Client,Settings) end local targettype = Target:GetTypeName() if self.AttackSet then for _,_client in pairs(self.AttackSet.Set) do local client = _client --Wrapper.Client#CLIENT if client and client:IsAlive() then local Settings = client and _DATABASE:GetPlayerSettings(client:GetPlayerName()) or _SETTINGS if self.ReferencePoint then coordtext = coord:ToStringFromRPShort(self.ReferencePoint,self.RPName,client,Settings) end local coordtext = coord:ToStringA2G(client,Settings) local text = string.format("All stations, %s lasing %s\nat %s!\nCode %d, Duration %d plus seconds!",callsign, targettype, coordtext, Lasercode, Lasingtime) MESSAGE:New(text,15,self.Name or "FACA"):ToClient(client) end end end local text = "Lasing!" local ttstext = "Laser on!" if self.UseSRS then local grp = Client:GetGroup() local coord = grp:GetCoordinate() if coord then self.SRS:SetCoordinate(coord) end self.SRSQueue:NewTransmission(ttstext,nil,self.SRS,nil,1,{grp},text,10) else MESSAGE:New(text,10,self.Name or "FACA"):ToClient(Client) end return self end --- [Internal] Lased target destroyed -- @param #PLAYERRECCE self -- @param #string From -- @param #string Event -- @param #string To -- @param Wrapper.Client#CLIENT Client -- @param Wrapper.Unit#UNIT Target -- @return #PLAYERRECCE self function PLAYERRECCE:onafterShack(From, Event, To, Client, Target) self:T({From, Event, To}) local callsign = Client:GetGroup():GetCustomCallSign(self.ShortCallsign,self.Keepnumber,self.CallsignTranslations) local Settings = ( Client and _DATABASE:GetPlayerSettings( Client:GetPlayerName() ) ) or _SETTINGS local coord = Client:GetCoordinate() local coordtext = coord:ToStringBULLS(self.Coalition,Settings) if self.ReferencePoint then coordtext = coord:ToStringFromRPShort(self.ReferencePoint,self.RPName,Client,Settings) end local targettype = "target" if self.AttackSet then for _,_client in pairs(self.AttackSet.Set) do local client = _client --Wrapper.Client#CLIENT if client and client:IsAlive() then local Settings = client and _DATABASE:GetPlayerSettings(client:GetPlayerName()) or _SETTINGS if self.ReferencePoint then coordtext = coord:ToStringFromRPShort(self.ReferencePoint,self.RPName,client,Settings) end local coordtext = coord:ToStringA2G(client,Settings) local text = string.format("All stations, %s good hit on %s\nat %s!",callsign, targettype, coordtext) MESSAGE:New(text,15,self.Name or "FACA"):ToClient(client) end end end local text = "Shack!" local ttstext = "Shack!" if self.UseSRS then local grp = Client:GetGroup() local coord = grp:GetCoordinate() if coord then self.SRS:SetCoordinate(coord) end self.SRSQueue:NewTransmission(ttstext,nil,self.SRS,nil,1) else MESSAGE:New(text,10,self.Name or "FACA"):ToClient(Client) end return self end --- [Internal] Laser lost LOS -- @param #PLAYERRECCE self -- @param #string From -- @param #string Event -- @param #string To -- @param Wrapper.Client#CLIENT Client -- @param Wrapper.Unit#UNIT Target -- @return #PLAYERRECCE self function PLAYERRECCE:onafterTargetLOSLost(From, Event, To, Client, Target) self:T({From, Event, To}) local callsign = Client:GetGroup():GetCustomCallSign(self.ShortCallsign,self.Keepnumber,self.CallsignTranslations) local Settings = ( Client and _DATABASE:GetPlayerSettings( Client:GetPlayerName() ) ) or _SETTINGS local coord = Client:GetCoordinate() local coordtext = coord:ToStringBULLS(self.Coalition,Settings) if self.ReferencePoint then coordtext = coord:ToStringFromRPShort(self.ReferencePoint,self.RPName,Client,Settings) end local targettype = "target" --Target:GetTypeName() if self.AttackSet then for _,_client in pairs(self.AttackSet.Set) do local client = _client --Wrapper.Client#CLIENT if client and client:IsAlive() then local Settings = client and _DATABASE:GetPlayerSettings(client:GetPlayerName()) or _SETTINGS if self.ReferencePoint then coordtext = coord:ToStringFromRPShort(self.ReferencePoint,self.RPName,client,Settings) end local coordtext = coord:ToStringA2G(client,Settings) local text = string.format("All stations, %s lost sight of %s\nat %s!",callsign, targettype, coordtext) MESSAGE:New(text,15,self.Name or "FACA"):ToClient(client) end end end local text = "Lost LOS!" local ttstext = "Lost L O S!" if self.UseSRS then local grp = Client:GetGroup() local coord = grp:GetCoordinate() if coord then self.SRS:SetCoordinate(coord) end self.SRSQueue:NewTransmission(ttstext,nil,self.SRS,nil,1,{grp},text,10) else MESSAGE:New(text,10,self.Name or "FACA"):ToClient(Client) end return self end --- [Internal] Target report -- @param #PLAYERRECCE self -- @param #string From -- @param #string Event -- @param #string To -- @param Wrapper.Client#CLIENT Client -- @param Core.Set#SET_UNIT TargetSet -- @param Wrapper.Unit#UNIT Target -- @param #string Text -- @return #PLAYERRECCE self function PLAYERRECCE:onafterTargetReport(From, Event, To, Client, TargetSet, Target, Text) self:T({From, Event, To}) MESSAGE:New(Text,45,self.Name or "FACA"):ToClient(Client) if self.AttackSet then -- send message to AttackSet for _,_client in pairs(self.AttackSet.Set) do local client = _client -- Wrapper.Client#CLIENT if client and client:IsAlive() and client ~= Client then MESSAGE:New(Text,45,self.Name or "FACA"):ToClient(client) end end end return self end --- [Internal] Target data upload -- @param #PLAYERRECCE self -- @param #string From -- @param #string Event -- @param #string To -- @param Wrapper.Client#CLIENT Client Client sending the report -- @param #string Playername Player name -- @param Core.Set#SET_UNIT TargetSet Set of targets -- @return #PLAYERRECCE self function PLAYERRECCE:onafterTargetReportSent(From, Event, To, Client, Playername, TargetSet) self:T({From, Event, To}) local text = "Upload completed!" if self.UseSRS then local grp = Client:GetGroup() local coord = grp:GetCoordinate() if coord then self.SRS:SetCoordinate(coord) end self.SRSQueue:NewTransmission(text,nil,self.SRS,nil,1,{grp},text,10) else MESSAGE:New(text,10,self.Name or "FACA"):ToClient(Client) end return self end --- [Internal] Stop -- @param #PLAYERRECCE self -- @param #string From -- @param #string Event -- @param #string To -- @return #PLAYERRECCE self function PLAYERRECCE:onafterStop(From, Event, To) self:I({From, Event, To}) -- Player Events self:UnHandleEvent(EVENTS.PlayerLeaveUnit) self:UnHandleEvent(EVENTS.Ejection) self:UnHandleEvent(EVENTS.Crash) self:UnHandleEvent(EVENTS.PilotDead) self:UnHandleEvent(EVENTS.PlayerEnterAircraft) return self end ------------------------------------------------------------------------------------------ -- TODO: END PLAYERRECCE ------------------------------------------------------------------------------------------