diff --git a/CA BSA3.lua b/CA BSA3.lua new file mode 100644 index 0000000..384d5ed --- /dev/null +++ b/CA BSA3.lua @@ -0,0 +1,81 @@ +--[[ +Targets fot TITS v2.0Xx +]]-- + + +--[[ Add target parameters + function TITS.AddTarget( + _TargetName -- UNIQUE! Target name + , _DisplayName -- display name, accepts duplicates, if empty _TargetName will be used + , _Type -- [ unit | zone | static ] must be in lower case + , _Respawn -- Only for units. if true, unit can be respawned. Respwan will only happen when ALL the units in the group are destroyed + , _TrackImpact -- use target to calculate distance to impact + , _TrackStrafing -- count strafing hits on target + , _ReportHits -- reports splash damage from weapons + , _AllowSmokeDesignation -- Allow use of smoke designation for target + , _AllowLaserDesignation -- Allow use of laser designation for target + , _AllowIRDesignation -- Allow use of IR designation for target + , _AllowListCoordinates -- Allow list of target coordinates + , _uiNum1 -- Numeric value for custom UI use + , _uiNum2 -- Numeric value for custom UI use + , _uiNum3 -- Numeric value for custom UI use + , _uiBool1 -- Boolean value for custom UI use + , _uiBool2 -- Boolean value for custom UI use + , _uiBool3 -- Boolean value for custom UI use + , _uiText1 -- Text value for custom UI use + , _uiText2 -- Text value for custom UI use + , _uiText3 -- Text value for custom UI use + ) + +--]] + +UI.MagneticVar = 6 + +-- Load Targets +-- Display Track Track Report Designation Allow Smoke Allow Laser Allow IR Allow Nomark List UI UI UI UI UI UI UI UI UI +-- Name name Type Reswpan Impact Strafing Hits Zone Designation Designation Designation designation Coords. Num1 Num2 Num3 Bool1 Bool2 Bool3 Text1 Text2 Text3 + +TITS.AddTarget( 'Circle-A', '', 'unit', true, true, true, true, 1, false, false, false, true, true, 0, 0, 0, false, false, false, '', '', '' ) +TITS.AddTarget( 'Circle-B', '', 'unit', true, true, true, true, 1, false, false, false, true, true, 0, 0, 0, false, false, false, '', '', '' ) +TITS.AddTarget( 'Strafe Pit' ,'', 'unit', true, true, true, true, 1, false, false, false, true, true, 0, 0, 0, false, false, false, '', '', '' ) +TITS.AddTarget( 'Tank', '', 'unit', true, true, true, true, 1, true, true, true, true, false, 0, 0, 0, false, false, false, 'Left target', '', '' ) +TITS.AddTarget( 'Truck', '', 'unit', true, true, true, true, 1, true, true, true, true, false, 0, 0, 0, false, false, false, 'Right Target', '', '' ) + +TITS.AddTarget( 'Ground-23-1','Bridge-1', 'poly', true, true, true, true, 2, true, true, true, true, true, 0, 0, 0, false, false, false, 'It is oriented in the 147°/327° (mmagnetic) radial', '', '' ) +TITS.AddTarget( 'white','Goverment bldg.', 'unit', true, true, true, true, 2, true, true, true, true, true, 0, 0, 0, false, false, false, 'White Building.', 'North of the Bridge', '' ) +TITS.AddTarget( 'red', 'Factory', 'unit', true, true, true, true, 2, true, true, true, true, true, 0, 0, 0, false, false, false, 'Red Building', 'West of the Bridge', '' ) +TITS.AddTarget( 'wAr','Refinary', 'unit', true, true, true, true, 2, true, true, true, true, true, 0, 0, 0, false, false, false, 'White and Red Building', 'South of th Bridge', '' ) + + + + +-- Laod laser/ir designator list +-- name displayName zones allowLaser allowIR laser code + UI.AddDesignator( 'Predator' , '' , {1,2} , true , true , '1688' ) + + + +-- Engagement Zones +-- Name Type +UI.AddEngagementZone ( 'Engagement Zone', 'zone' ) + +--designation zones +UI.AddDesignationZone( 1, 'BSA' ) +UI.AddDesignationZone( 2, 'Structures' ) + + +-- Illumination Zones (circular ME zones) +-- Name Number of shells per round +UI.AddIlluminationZone( 'Illumination-1', 3 ) +UI.AddIlluminationZone( 'Illumination-2', 2 ) + +-- Confirm load +trigger.action.outText( "CA BSA 3 targets", 2 ) + + + + + + + + diff --git a/Range_UI.1.3.lua b/Range_UI.1.3.lua new file mode 100644 index 0000000..00dccd7 --- /dev/null +++ b/Range_UI.1.3.lua @@ -0,0 +1,2345 @@ +UI = {} +UI.Version = '1.3.03' + +--################################################################################################################################################### +-- ############# DCS - Range Control User Interface for Target Impact Tracker Script 2.0 ################# +-- ############# by Draken35 ################# +--################################################################################################################################################### +--[[ Requirements: + >>> MIST 4.5.107 or above + >>> TITS 2.3.00 or above +--]] +----------------------------------------------------------------------------------------------------------------------------------------------------- +--[[ Version history + 1.0.00 04/06/2022 Development starts + 1.0.01 05/03/2022 First release + 1.0.02 05/08/2022 -fixed Apache automatic coordinates system detection + 1.1.00 05/10/2022 - Uses TITS 2.1.x results API to report results instead of raw data tables + 1.1.01 05/11/2022 - Fixed issue with popup script and new result reports + - replaced UI.MaxWFindRPT by I.MinWFgrpRPT + 1.1.02 05/12/2022 - Added optional abort message when shooting before starting a pass + 1.1.10 05/14/2022 - No mark designation will display the UI.Text1,UI.Text2 and UI.Text3, so they can be used to give a talk-on for the target. + - Coordinates are now optional in the No-mark and laser/IR designation designation. Toggle option in designation menu + 1.2.00 05/15/2022 - Reorganization of Setting Menu + - Added optional Auto Pass Start and End + 1.2.01 05/18/2022 - Attack Radial added to designation + - Added release heading to reports + - Added reports based on designated target + - Fixed and readded Pass BDA report + 1.2.02 05/25/2022 - Fixed call to designated/grouped report from menu + 1.2.03 07/10/2022 - Range comms improved for MP games + - display on coordenates in 3 systems at the same time + 1.2.04 07/22/2022 - Fixed a crash in IR designation + 1.3.00 07/27/2022 - Added support for roll-in position tracking + - Added illumination zones + 1.3.01 07/30/2022 - Added talk-on fields to the laser/IR designation + 1.3.02 08/04/2022 - improved initialization of Auto pass start and end + 1.3.03 09/11/2022 - Lest we forget! + - Fixed issue with strafing reporting in missions with unlimited weapons. + A limitation is that it will only report passed with hits on target and it cannot report rate of hits.\ + - Fixed issue with automatic range clearance trigger for AI units spawned inside the zones + +--]] +----------------------------------------------------------------------------------------------------------------------------------------------------- +--[[ Pass Data return tables + Tables: + shellsFired[ShellType] = { + init = Shell Count at pass start + , fired = Shells fired during the pass + } + shellHits -- per target per shell type + shellHits[Target][ShellType] = Shell Hits Count + + weaponsFired -- and their release params & designated target at the time, updated by shot event if TITS.onPass[_unitName] == true + weaponsFired[WeaponID] = { + weaponType + , releasePitch + , releaseYaw + , releaseRoll + , releaseHeading + , releaseSpeed + , releasePosition + , timeStamp + , inFlight + } + + weaponHits -- per target per weapon , updated by hit event if TITS.onPass[_unitName] == true + weaponHits[WeaponID][Target] = timeStamp + + + impactData -- per target per weapon + impactData[WeaponID][Target] = { + targetPos + , impactPos + , impactDistance + , timeStamp + } + + + targetHealth -- per target. Health at the start and end of the pass + targetHealth[Target] = { + start_health + , end_health + } + + +--]] +--[[ Target List table + TargetList[name] = { + name -- UNIQUE! Target name + , displayName -- display name, accepts duplicates, if empty _TargetName will be used + , type -- allowed values [ 'unit' | 'zone' | 'static' ] must be in lower case + , respawn -- Only for units. if true, unit can be respawned. Respwan will only happen when ALL the units in the group are destroyed + , impact -- use target to calculate distance to impact + , strafing -- count strafing hits on target + , bda -- reports splash damage from weapons + , des_zone -- designation zone + , des_wp -- Allow use of smoke designation for target + , des_laser -- Allow use of laser designation for target + , des_ir -- Allow use of IR designation for target + , des_nomark -- Allow use of target designation with no marks (provides coordinates) + , list_coor -- Allow list of target coordinates + , uiNum1 -- Numeric value for custom UI use + , uiNum2 -- Numeric value for custom UI use + , uiNum3 -- Numeric value for custom UI use + , uiBool1 -- Boolean value for custom UI use + , uiBool2 -- Boolean value for custom UI use + , uiBool3 -- Boolean value for custom UI use + , uiText1 -- Text value for custom UI use + , uiText2 -- Text value for custom UI use + , uiText3 -- Text value for custom UI use + + } + +--]] + + +--################################################################################################################################################### +-- ############# Configuration section ################# +--################################################################################################################################################### + +UI.AutoPassStart = false +UI.AutoPassStartCoolDown = 30 -- seconds +UI.AutoPassEndTimeOut = 20 -- seconds +UI.AutoPassEnd = false -- If true, pass will automatically end if there aren't any weapons in the air and + -- UI.AutoPassEndTimeOut seconds had passed since the last shell was fired or weapon impact + -- was recorded + +UI.MagneticVar = 0 -- Magnetic variation for the map and date, used for designation attack radial calculation. East is + + +UI.SpeedUnitsDefault = 1 -- 1 = Knots, 2 = Mph, 3 = Kph +UI.DistanceUnitsDefault = 3 -- 1 = feet, 2 = yards, 3 = meters +UI.AltitudeUnitsDefault = 1 -- 1 = feet, 3 = meters ( 2 is not used) +UI.CoordinateFormatDefault = 2 -- 1 = MGRS, 2 = DMS, 3 = DM.mm +UI.DisplayReleaseDataDefault = true -- default for the option to display the release parameters in results +UI.DisplayAbortMsgDefault = true -- if true, display abort message when shooting before starting a pass +UI.DisplayCoordsDefault = true -- if true, display coordinates in designation messages +UI.desUseAttackRadialDefault = false -- add an attack radial requiment if true + +UI.DefaultResultReport = 0 -- 0 = Automatic + -- 1 = Impact results (closest/individual) + -- 2 = Impact results (closest/grouped) + -- 3 = BDA Summary + -- 4 = Impact results (designated/individual) + +UI.MinWFgrpRPT = 6 -- Minimun number of weapons fired in pass to display grouped report in automatic mode + +UI.minWPmarkDist = 50 -- meters. Minimum distance from target to mark using WP +UI.maxWPmarkDist = 150 -- meters. Maximum distance from target to mark using WP + + +UI.AutoSetCoordinates = true -- if true use player's unit type to set coordinates, else use MPT.CoordinateFormatDefault +UI.coordXtype = {} +UI.coordXtype['AV8BNA'] = 1 -- 1 = MGRS, 2 = DMS, 3 = DM.mm +UI.coordXtype['A-10C_2'] = 1 +UI.coordXtype['AH-64D_BLK_II'] = 1 +UI.coordXtype['F-16C_50'] = 3 +UI.coordXtype['FA-18C_hornet'] = 1 +UI.coordXtype['M-2000C'] = 3 + +UI.messageBeep = '204521__redoper__roger-beep.ogg' -- Message alert sound. leave empty for no sound + + +--################################################################################################################################################# +-- ############# Initialization of globals ############### +--################################################################################################################################################# + +UI.unitConfig = {} +UI.menuAddedToGroup = {} +UI.debugMarkcenter = false +UI.DesignationZones = {} +UI.DesignationZones[1] = { id = 1, name = 'Default'} +UI.Designation = { + Designator = '' -- unit name + , Designated = '' -- target name + , type = 0 -- 0 = none, 1 = WP , 2 = laser, 3 = IR, 4 = No mark, 23 = laser/IR + , zone = 1 + , briefing = '' + , ray1 = nil + , ray2 = nil + , AttackRadial = 0 + } +UI.Designators = {} +UI.engagementZones = {} +UI.illuminationZones = {} +UI.illuminating = false + +--################################################################################################################################################# +-- ############# UI code ############### +--################################################################################################################################################# + UI.EventHandler = {} +function UI.EventHandler:onEvent(_eventDCS) + + if _eventDCS == nil or _eventDCS.initiator == nil then + return true + end + + local status, err = pcall(function(_event) + + if (_event.id == world.event.S_EVENT_BIRTH or _event.id == world.event.S_EVENT_TAKEOFF + or _event.id == world.event.S_EVENT_ENGINE_STARTUP) and _event.initiator:getPlayerName() then + + UI.initUnit(_event.initiator:getName()) + + elseif ((_event.id == world.event.S_EVENT_SHOT) or (_event.id == world.event.S_EVENT_SHOOTING_START) ) and _event.initiator:getPlayerName() then -- id == 1 Shot + local _unitName = _event.initiator:getName() + local cfg = UI.unitConfig[_unitName] + if not TITS.onPass[_unitName] and cfg.abortMSG then + UI.messageToUnit(_unitName,"Abort! You are not cleared to fired!",{},10) + end + + end --_event.id == + -- ### End event processing + + return true + end, _eventDCS) -- local status, err = pcall(function(_event) + + if (not status) then + local msg = string.format("Target Impact Tracker UI (v%s): Error while handling event %s",UI.Version, err) + env.error(msg,false) + end +end +----------------------------------------------------------------------------------------------------------------------------------------------------- +function UI.initUnit(unitName) + local unit = Unit.getByName(unitName) + local groupID = unit:getGroup():getID() + + local rConfig = { + SpeedUOM = UI.SpeedUnitsDefault + , DistanceUOM = UI.DistanceUnitsDefault + , AltitudeUOM = UI.AltitudeUnitsDefault + , CoordFormat = UI.CoordinateFormatDefault + , DisplayReleaseData = UI.DisplayReleaseDataDefault + , rootPath = nil + , rollInPath = nil + , offPath = nil + , resultsRPT = UI.DefaultResultReport + , unitType = unit:getTypeName() + , abortMSG = UI.DisplayAbortMsgDefault + , desDisplayCoords = UI.DisplayCoordsDefault + , desUseAttackRadial = UI.desUseAttackRadialDefault + , lastPassEnd = timer.getTime() + , firstPass = true + } + + if UI.coordXtype[rConfig.unitType] and UI.AutoSetCoordinates then + rConfig.CoordFormat = UI.coordXtype[rConfig.unitType] + end + + + if UI.menuAddedToGroup[groupID] == {} then + UI.menuAddedToGroup[groupID] = false + end + if not UI.menuAddedToGroup[groupID] then + -- Add Menu + rConfig.rootPath = missionCommands.addSubMenuForGroup(groupID, "Range") -- Root level + + local _reportsPath = missionCommands.addSubMenuForGroup(groupID,"Reports", rConfig.rootPath) + missionCommands.addCommandForGroup(groupID,"List coordinates" , _reportsPath, UI.rptListCoord , unitName) + missionCommands.addCommandForGroup(groupID,"Results (closest/individual)" , _reportsPath, UI.rptImpactResults , unitName) + missionCommands.addCommandForGroup(groupID,"Results (closest/grouped)" , _reportsPath, UI.rptImpactResultsGrouped , unitName) + missionCommands.addCommandForGroup(groupID,"Results (designated/individual)" , _reportsPath, UI.rptImpactResultsDes , unitName) + missionCommands.addCommandForGroup(groupID,"Results (designated/grouped)" , _reportsPath, UI.rptImpactResultsDesGrouped , unitName) + missionCommands.addCommandForGroup(groupID,"BDA Pass summary" , _reportsPath, UI.rptBDAsummary , unitName) + missionCommands.addCommandForGroup(groupID,"Weapons in flight" , _reportsPath, UI.rptWeaponsInFlight , unitName) + --missionCommands.addCommandForGroup(groupID,"Debug: Inspect Data" , _reportsPath, UI.debugPassData , unitName) + local _designationPath = missionCommands.addSubMenuForGroup(groupID, "Designation", rConfig.rootPath) + missionCommands.addCommandForGroup(groupID,"New smoke (WP) designation" , _designationPath, UI.desWP , {unitName, true}) + missionCommands.addCommandForGroup(groupID,"Repeat smoke (WP) mark" , _designationPath, UI.desWP , {unitName, false}) + missionCommands.addCommandForGroup(groupID,"New Laser designation" , _designationPath, UI.desLaserIR , {unitName,'laser'}) + missionCommands.addCommandForGroup(groupID,"New IR pointer designation" , _designationPath, UI.desLaserIR , {unitName,'IR'}) + missionCommands.addCommandForGroup(groupID,"New Laser/IR designation" , _designationPath, UI.desLaserIR , {unitName,'laser/IR'}) + missionCommands.addCommandForGroup(groupID,"New No Mark designation" , _designationPath, UI.desNM , unitName) + missionCommands.addCommandForGroup(groupID,"Cancel designation" , _designationPath, UI.desCancel , unitName) + missionCommands.addCommandForGroup(groupID,"Repeat briefing" , _designationPath, UI.desRPTBrief , unitName) + ---------------------------------------------------------------------------------------- + local _settingsPath = missionCommands.addSubMenuForGroup(groupID, "Settings", rConfig.rootPath) + missionCommands.addCommandForGroup(groupID,"Display current settings" , _settingsPath, UI.Settings , {unitName,0}) + missionCommands.addCommandForGroup(groupID,"Toggle illumination" , _settingsPath, UI.Settings , {unitName,70}) + missionCommands.addCommandForGroup(groupID,"Respawn targets" , _settingsPath, UI.Settings , {unitName,66}) + missionCommands.addCommandForGroup(groupID,"Toggle abort message" , _settingsPath, UI.Settings , {unitName,1}) + missionCommands.addCommandForGroup(groupID,"Toggle display of Release data" , _settingsPath, UI.Settings , {unitName,2}) + + + local _settings6Path = missionCommands.addSubMenuForGroup(groupID, "Designation", _settingsPath) + local _designation1Path = missionCommands.addSubMenuForGroup(groupID, "Select designation zone", _settings6Path) + for _,z in pairs(UI.DesignationZones) do + local n = string.format('%s', z.name) + missionCommands.addCommandForGroup(groupID,n , _designation1Path, UI.desSetZone , {unitName,z.id}) + end + missionCommands.addCommandForGroup(groupID,"Toggle attack radial" , _settings6Path, UI.Settings , {unitName,31}) + missionCommands.addCommandForGroup(groupID,"Toggle coods. display" , _settings6Path, UI.Settings , {unitName,30}) + + local _settings1Path = missionCommands.addSubMenuForGroup(groupID, "Distance units", _settingsPath) + missionCommands.addCommandForGroup(groupID,"Distance: set to meters" , _settings1Path, UI.Settings , {unitName,3}) + missionCommands.addCommandForGroup(groupID,"Distance: set to feet" , _settings1Path, UI.Settings , {unitName,4}) + missionCommands.addCommandForGroup(groupID,"Distance: set to yards" , _settings1Path, UI.Settings , {unitName,5}) + local _settings2Path = missionCommands.addSubMenuForGroup(groupID, "Altitude units", _settingsPath) + missionCommands.addCommandForGroup(groupID,"Altitude: set to meters" , _settings2Path, UI.Settings , {unitName,6}) + missionCommands.addCommandForGroup(groupID,"Altitude: set to feet" , _settings2Path, UI.Settings , {unitName,7}) + local _settings3Path = missionCommands.addSubMenuForGroup(groupID, "Speed units", _settingsPath) + missionCommands.addCommandForGroup(groupID,"Speed: set to kph" , _settings3Path, UI.Settings , {unitName,8}) + missionCommands.addCommandForGroup(groupID,"Speed: set to knots" , _settings3Path, UI.Settings , {unitName,9}) + missionCommands.addCommandForGroup(groupID,"Speed: set to mph" , _settings3Path, UI.Settings , {unitName,10}) + --local _settings4Path = missionCommands.addSubMenuForGroup(groupID, "Coordinates format", _settingsPath) + -- missionCommands.addCommandForGroup(groupID,"Coord. format set to MGRS" , _settings4Path, UI.Settings , {unitName,11}) + -- missionCommands.addCommandForGroup(groupID,"Coord. format set to DMS" , _settings4Path, UI.Settings , {unitName,12}) + -- missionCommands.addCommandForGroup(groupID,"Coord. format set to DM.mm" , _settings4Path, UI.Settings , {unitName,13}) + local _settings5Path = missionCommands.addSubMenuForGroup(groupID, "Results report", _settingsPath) + missionCommands.addCommandForGroup(groupID,"Automatic selection" , _settings5Path, UI.Settings , {unitName,20}) + missionCommands.addCommandForGroup(groupID,"Impact results (individual)" , _settings5Path, UI.Settings , {unitName,21}) + missionCommands.addCommandForGroup(groupID,"Impact results (grouped)" , _settings5Path, UI.Settings , {unitName,22}) + missionCommands.addCommandForGroup(groupID,"BDA Pass summary" , _settings5Path, UI.Settings , {unitName,23}) + ---------------------------------------------------------------------------------------- + rConfig.rollInPath = missionCommands.addCommandForGroup(groupID,"Rolling in" , rConfig.rootPath , UI.passStartEnd, {unitName,'in'}) + UI.menuAddedToGroup[groupID] = true + + UI.unitConfig[unitName] = rConfig + + UI.Settings({unitName,0}) + + if UI.AutoPassStart then + -- launch pass end checker + local params = {unitName = unitName} + timer.scheduleFunction(UI.passStartCheck, params, timer.getTime() + 1 ) + end + + if UI.AutoPassEnd then + -- launch pass end checker + local params = {unitName = unitName} + timer.scheduleFunction(UI.passEndCheck, params, timer.getTime() + 1 ) + end + + end --if not UI.menuAddedToGroup[groupID] then + + + + + return true +end +----------------------------------------------------------------------------------------------------------------------------------------------------- +function UI.passStartCheck(params, time) + if not TITS.onPass[params.unitName] then + if UI.rowCount(UI.engagementZones) > 0 then + local inZone = false + for z,t in pairs(UI.engagementZones) do + local ut = mist.makeUnitTable({params.unitName}) + if t == 'zone' then + local u = mist.getUnitsInZones(ut, {z},'cylinder') + inZone = inZone or (UI.rowCount(u) > 0) + else + local p = mist.getGroupPoints(z) + local u = mist.getUnitsInPolygon(ut, p) + inZone = inZone or (UI.rowCount(u) > 0) + end + end -- for z,t in pairs(UI.engagementZones) do + if inZone then + local cfg = UI.unitConfig[params.unitName] + if (timer.getTime() - cfg.lastPassEnd ) >= UI.AutoPassStartCoolDown or cfg.firstPass then + cfg.firstPass = false + UI.passStartEnd({params.unitName, 'in'}) + end + end + end --if UI.rowCount(UI.engagementZones) > 0 then + end -- if not TITS.onPass[params.unitName] then + + return timer.getTime() + 1 +end -- function +----------------------------------------------------------------------------------------------------------------------------------------------------- +function UI.passEndCheck(params, time) + if TITS.onPass[params.unitName] then + local passData = TITS.passGetData(params.unitName) + local wf = TITS.getWeponsFired(params.unitName) + local sf = TITS.getShellsFired(params.unitName) + if wf.count > 0 or sf.total > 0 then + if not TITS.weaponsInFlight(params.unitName) then + local cTime = timer.getTime() + if (cTime - passData.lastShellFired) > UI.AutoPassEndTimeOut and + (cTime - passData.lastImpact) > UI.AutoPassEndTimeOut then + UI.passStartEnd({params.unitName, 'off'}) + end -- if (cTime - passData.lastShellFired) > UI.AutoPassEndTimeOut and + end -- if not TITS.weaponsInFlight(params.unitName) then + end -- if wf.count > 0 or sf.count > 0 then + end -- if TITS.onPass[params.unitName] then + return timer.getTime() + 1 +end -- function +----------------------------------------------------------------------------------------------------------------------------------------------------- +function UI.AddEngagementZone(_name, _type) + + UI.engagementZones[_name] = _type + +end -- function +----------------------------------------------------------------------------------------------------------------------------------------------------- +function UI.Settings(_args) + local unitName = _args[1] + local setting = _args[2] + local cfg = UI.unitConfig[unitName] + + if setting == 0 then -- Display settings + local _msg = '' + + _msg = string.format ('Flying: %s',cfg.unitType) + + if cfg.abortMSG then + _msg = string.format ('%s\nAbort message is ON',_msg) + else + _msg = string.format ('%s\nAbort message is OFF',_msg) + end + + if cfg.DisplayReleaseData then + _msg = string.format ('%s\nRelease data display is ON',_msg) + else + _msg = string.format ('%s\nRelease data display is OFF',_msg) + end + + if cfg.DistanceUOM == 3 then -- Distance meters + _msg = string.format ('%s\nDistance: Using meters',_msg) + elseif cfg.DistanceUOM == 2 then --Distance feet + _msg = string.format ('%s\nDistance: Using feet',_msg) + else -- Distance yards + _msg = string.format ('%s\nDistance: Using yards',_msg) + end + + if cfg.AltitudeUOM == 3 then -- Altitude meters + _msg = string.format ('%s\nAltitude: Using meters',_msg) + else + _msg = string.format ('%s\nAltitude: Using feet',_msg) + end + + if cfg.SpeedUOM == 3 then -- Speed kph + _msg = string.format ('%s\nSpeed: Using kph',_msg) + elseif cfg.SpeedUOM == 1 then -- Speed kt + _msg = string.format ('%s\nSpeed: Using knots',_msg) + else -- Speed mph + _msg = string.format ('%s\nSpeed: Using mph',_msg) + end + --[[ + if cfg.CoordFormat == 1 then -- Coordinate format MGRS + _msg = string.format ('%s\nCoordinate format: Using MGRS',_msg) + elseif cfg.CoordFormat == 2 then -- Coordinate format DMS + _msg = string.format ('%s\nCoordinate format: Using DMS',_msg) + else -- Coordinate format DM.mm + _msg = string.format ('%s\nCoordinate format: Using DM.mm',_msg) + end + --]] + if cfg.resultsRPT == 0 then + _msg = string.format ('%s\nResults using Automatic selection',_msg) + elseif cfg.resultsRPT == 1 then -- + _msg = string.format ('%s\nResults using Impact results (individual) report',_msg) + elseif cfg.resultsRPT == 2 then + _msg = string.format ('%s\nResults using Impact results (grouped) report',_msg) + elseif cfg.resultsRPT == 3 then + _msg = string.format ('%s\nResults using BDA Pass Summary report',_msg) + end + + _msg = string.format ('%s\nDesignation zone: %s',_msg,UI.DesignationZones[UI.Designation.zone].name) + + if cfg.desDisplayCoords then + _msg = string.format ('%s\nDisplay coordinates in designation msgs is ON',_msg) + else + _msg = string.format ('%s\nDisplay coordinates in designation msgs is OFF',_msg) + end + + if cfg.desUseAttackRadial then + _msg = string.format ('%s\nRequire attack radial in designation is ON',_msg) + else + _msg = string.format ('%s\nRequire attack radial in designation is OFF',_msg) + end + + UI.messageToUnit(unitName,_msg,{},10,false) + + + + elseif setting == 1 then -- Toggle display of Abort message + cfg.abortMSG = not cfg.abortMSG + if cfg.abortMSG then + UI.messageToUnit(unitName,'Abort message is ON',false) + else + UI.messageToUnit(unitName,'Abort message is OFF',false) + end + + elseif setting == 2 then -- Toggle display of Release data + cfg.DisplayReleaseData = not cfg.DisplayReleaseData + if cfg.DisplayReleaseData then + UI.messageToUnit(unitName,'Release data display is ON',false) + else + UI.messageToUnit(unitName,'Release data display is OFF',false) + end + -- Distancce + elseif setting == 3 then -- Distance meters + cfg.DistanceUOM = 3 -- 1 = feet, 2 = yards, 3 = meters + UI.messageToUnit(unitName,'Distance: Using meters',false) + elseif setting == 4 then --Distance feet + cfg.DistanceUOM = 1 -- 1 = feet, 2 = yards, 3 = meters + UI.messageToUnit(unitName,'Distance: Using feet',false) + elseif setting == 5 then -- Distance yards + cfg.DistanceUOM = 2 --1 = feet, 2 = yards, 3 = meters + UI.messageToUnit(unitName,'Distance: Using yards',false) + -- Altitude + elseif setting == 6 then -- Altitude meters + cfg.AltitudeUOM = 3 -- 1 = feet, 3 = meters + UI.messageToUnit(unitName,'Altitude: Using meters',false) + elseif setting == 7 then -- Altitude feet + cfg.AltitudeUOM = 1 -- 1 = feet, 3 = meters + UI.messageToUnit(unitName,'Altitude: Using feet',false) + -- Speed + elseif setting == 8 then -- Speed kph + cfg.SpeedUOM = 3 -- 1 = Knots, 2 = Mph, 3 = Kph + UI.messageToUnit(unitName,'Speed: Using kph',false) + elseif setting == 9 then -- Speed kt + cfg.SpeedUOM= 1 -- 1 = Knots, 2 = Mph, 3 = Kph + UI.messageToUnit(unitName,'Speed: Using knots',false) + elseif setting == 10 then -- Speed mph + cfg.SpeedUOM = 2 -- 1 = Knots, 2 = Mph, 3 = Kph + UI.messageToUnit(unitName,'Speed: Using mph',false) + -- Coordinate format + --[[ + elseif setting == 11 then -- Coordinate format MGRS + cfg.CoordFormat = 1 -- 1 = MGRS, 2 = DMS, 3 = DM.mm + UI.messageToUnit(unitName,'Coordinate format: Using MGRS',false) + elseif setting == 12 then -- Coordinate format DMS + cfg.CoordFormat = 2 -- 1 = MGRS, 2 = DMS, 3 = DM.mm + UI.messageToUnit(unitName,'Coordinate format: Using DMS',false) + elseif setting == 13 then -- Coordinate format DM.mm + cfg.CoordFormat = 3 -- 1 = MGRS, 2 = DMS, 3 = DM.mm + UI.messageToUnit(unitName,'Coordinate format: Using DM.mm',false) + --]] + elseif setting == 20 then -- Results reports + cfg.resultsRPT = 0 + UI.messageToUnit(unitName,'Results using Automatic selection',false) + elseif setting == 21 then -- Results reports + cfg.resultsRPT = 1 + UI.messageToUnit(unitName,'Results using Impact results (individual) report',false) + elseif setting == 22 then -- Results reports + cfg.resultsRPT = 2 + UI.messageToUnit(unitName,'Results using Impact results (grouped) report',false) + elseif setting == 23 then -- Results reports + cfg.resultsRPT = 3 + UI.messageToUnit(unitName,'Results using BDA Pass Summary report',false) + elseif setting == 30 then -- Toggle coords display in designation + cfg.desDisplayCoords = not cfg.desDisplayCoords + if cfg.desDisplayCoords then + UI.messageToUnit(unitName,'Display coordinates in designation msgs is ON',false) + else + UI.messageToUnit(unitName,'Display coordinates in designation msgs is OFF',false) + end + elseif setting == 31 then -- Toggle coords display in designation + cfg.desUseAttackRadial = not cfg.desUseAttackRadial + if cfg.desUseAttackRadial then + UI.messageToUnit(unitName,'Require attack radial in designation is ON',false) + else + UI.messageToUnit(unitName,'Require attack radial in designation is OFF',false) + end + elseif setting == 66 then -- respawn test + for u,_ in pairs(TITS.TargetList) do + TITS.respawnTarget(u) + end -- for u,v in pairs(TITS.TargetList) do + + elseif setting == 70 then -- toggle illumination + UI.illuminating = not UI.illuminating + if UI.illuminating then + trigger.action.outText( 'Starting illumination', 10 ) + timer.scheduleFunction(UI.illuminate, {}, timer.getTime() + 1 ) + else + trigger.action.outText( 'Stoping illumination', 10 ) + end + end -- main if + + + return true +end +----------------------------------------------------------------------------------------------------------------------------------------------------- +function UI.messageToUnit(_unitName,_messageText,_soundTable,_displayTime,_displayToAll) + --[[ + local msg = {} + msg.text = _messageText + msg.msgFor = {units = {_unitName}} + + if TITS.messageBeep ~= '' then + msg.sound = TITS.messageBeep + end + + if _soundTable ~= nil then + msg.multSound = _soundTable + end + + if _displayTime == nil then + _displayTime = 5 + end + + msg.displayTime = _displayTime + + mist.message.add(msg) + --]] + + + + -- bypass mist because of message history spamming + if _displayToAll == nil then + _displayToAll = true + end + if _displayTime == nil then + _displayTime = 5 + end + local _unit = Unit.getByName(_unitName) + local _groupId = _unit:getGroup():getID() + local _callSign = _unit:getCallsign() + if UI.messageBeep ~= '' then + if _displayToAll then + trigger.action.outSound(UI.messageBeep) + else + trigger.action.outSoundForGroup(_groupId,UI.messageBeep) + end + end + local _msg = string.format('(%s): %s',_callSign,_messageText) + if _displayToAll then + trigger.action.outText( _msg, _displayTime) + else + trigger.action.outTextForGroup(_groupId, _msg, _displayTime) + end + + return true +end +----------------------------------------------------------------------------------------------------------------------------------------------------- +function UI.passStartEnd(_args) + local unitName = _args[1] + local value = _args[2] + --local groupID = Unit.getByName(unitName):getGroup():getID() + local unit = Unit.getByName(unitName) + local groupID = unit:getGroup():getID() + + if unit:getPlayerName() then + if value == 'in' then + + UI.messageToUnit(unitName,'Cleared in hot',{},10) + + -- reset prior attack results + TITS.passInit(unitName) + TITS.passStart(unitName) + + UI.toggleAttackMenu(unitName, "in") + + if pcall(function() return PTC.Version end ) then + PTC.passTargetsUP = 0 + PTC.passTargetsHIT = 0 + end + + else -- value == 'off' + local cfg = UI.unitConfig[unitName] + local canEnd = TITS.passEnd(unitName) + cfg.lastPassEnd = timer.getTime() + if canEnd then + local cfg = UI.unitConfig[unitName] + UI.messageToUnit(unitName,'Standby for report',{},10) + -- Show results + if cfg.resultsRPT == 0 then -- automatic + local passData = TITS.passGetData(unitName) + local wf = passData.weaponsFired + if UI.rowCount(wf) >= UI.MinWFgrpRPT then + if UI.Designation.type ~=0 and UI.Designation.Designated ~= '' then + UI.rptImpactResultsDesGrouped(unitName) + else + UI.rptImpactResultsGrouped(unitName) + end + else + if UI.Designation.type ~=0 and UI.Designation.Designated ~= '' then + UI.rptImpactResultsDes(unitName) + else + UI.rptImpactResults(unitName) + end + end + elseif cfg.resultsRPT == 1 then + UI.rptImpactResults(unitName) + elseif cfg.resultsRPT == 2 then + UI.rptImpactResultsGrouped(unitName) + elseif cfg.resultsRPT == 3 then + UI.rptBDAsummary(unitName) + end -- if UI.DefaultResultReport + UI.toggleAttackMenu(unitName, "off") + + else + UI.messageToUnit(unitName,'Standby, weapons still in the air',{},10) + end + end + end --if unit:getPlayerName() then + + return true +end +----------------------------------------------------------------------------------------------------------------------------------------------------- +function UI.toggleAttackMenu(unitName, action) + local unit = Unit.getByName(unitName) + local groupID = unit:getGroup():getID() + local cfg = UI.unitConfig[unitName] + + if action == 'in' then + missionCommands.removeItemForGroup(groupID, cfg.rollInPath) + cfg.offPath = missionCommands.addCommandForGroup(groupID,"Off - attack completed" , cfg.rootPath, UI.passStartEnd , {unitName,'off'}) + else + missionCommands.removeItemForGroup(groupID, cfg.offPath) + cfg.rollInPath= missionCommands.addCommandForGroup(groupID,"Rolling in" , cfg.rootPath, UI.passStartEnd , {unitName,'in'}) + end -- if action... + + return true +end +----------------------------------------------------------------------------------------------------------------------------------------------------- +function UI.debugPassData(_unitName) + local d = TITS.passGetData(_unitName) + + --local unit = Unit.getByName(_unitName) + --local ammoTable = unit:getAmmo() + + --trigger.action.outText("ammoTable", 1) + --trigger.action.outText(mist.utils.tableShow(ammoTable), 1) + + --trigger.action.outText("TargetList", 1) + --trigger.action.outText(mist.utils.tableShow(TITS.TargetList), 1) + + --trigger.action.outText("targetHealth", 1) + --trigger.action.outText(mist.utils.tableShow(d.targetHealth), 1) + + --trigger.action.outText("pass data", 1) + --trigger.action.outText(mist.utils.tableShow(d), 1) + + trigger.action.outText("shellsFired", 1) + trigger.action.outText(mist.utils.tableShow(d.shellsFired), 1) + + trigger.action.outText("shellHits", 1) + trigger.action.outText(mist.utils.tableShow(d.shellHits), 1) + + trigger.action.outText("shellDisplayName", 1) + trigger.action.outText(mist.utils.tableShow(d.shellDisplayName), 1) + + trigger.action.outText("getShellsFired", 1) + local shellsFired = TITS.getShellsFired(_unitName) + trigger.action.outText(mist.utils.tableShow(shellsFired), 1) + + + --trigger.action.outText("weaponHits", 1) + --trigger.action.outText(mist.utils.tableShow(d.weaponHits), 1) + + --trigger.action.outText("weaponsFired", 1) + --trigger.action.outText(mist.utils.tableShow(d.weaponsFired), 1) + + --trigger.action.outText("impactData", 1) + --trigger.action.outText(mist.utils.tableShow(d.impactData), 1) + + --trigger.action.outText("groups", 1) + --trigger.action.outText(mist.utils.tableShow(d.groups), 1) + + return true +end -- function +----------------------------------------------------------------------------------------------------------------------------------------------------- +function UI.rptWeaponsInFlight(_unitName) + local passData = TITS.passGetData(_unitName) + local weaponsFired = passData.weaponsFired + local rpt = '' + local t = timer.getTime() + for k,v in pairs(weaponsFired) do + if v.inFlight then + et = t - v.timeStamp + rpt = string.format('%s\n %s (%s s)',rpt, v.weaponType,mist.utils.round(et,0)) + end + end + + if rpt == '' then + rpt = 'No weapons in flight' + else + rpt = string.format('Weapons in flight:%s',rpt) + end + UI.messageToUnit(_unitName,rpt,{},30) +end -- function +----------------------------------------------------------------------------------------------------------------------------------------------------- +function UI.rptBDAsummary(_unitName) + if TITS.onPass[_unitName] then + UI.messageToUnit(_unitName,'Data not available yet, finish the pass',{},5) + else + local passData = TITS.passGetData(_unitName) + local shellHits = passData.shellHits + local weaponHits = passData.weaponHits + local weaponsFired = passData.weaponsFired + local targetHealth = passData.targetHealth + local rptHitsData = {} + local rptTargetsHit = {} + local config = UI.unitConfig[_unitName] + + + -- Get targets hit by shells and count of hits per shell type + if #shellHits > 0 then + for target,_ in pairs(shellHits) do + if not rptTargetsHit[target] then + rptTargetsHit[target] = true + end + for shellType,count in pairs(shellHits[target]) do + if not rptHitsData[target] then + rptHitsData[target] = {} + end -- if not rptHitsData[target] then + if not rptHitsData[target][shellType] then + rptHitsData[target][shellType] = count + else + rptHitsData[target][shellType] = rptHitsData[target][shellType] + count + end + end -- for _,shellType in pairs(shellHits[target]) do + end -- for target,_ in pairs(shellHits) do + end -- if #shellHits > 0 then + + -- Get targets hit by weapons and count of hits by weapon type + if UI.rowCount(weaponHits) > 0 then + for weaponID, _ in pairs(weaponHits) do + local weaponType = weaponsFired[weaponID].weaponType + for target,_ in pairs (weaponHits[weaponID]) do + if not rptTargetsHit[target] then + rptTargetsHit[target] = true + end + if not rptHitsData[target] then + rptHitsData[target] = {} + end -- if not rptHitsData[target] then + if not rptHitsData[target][weaponType] then + rptHitsData[target][weaponType] = 1 + else + rptHitsData[target][weaponType] = rptHitsData[target][weaponType] + 1 + end + end -- for target,_ in pairs (weaponHits[weaponID] do + end -- for weaponID, _ in pairs(weaponHits) do + end -- if #weaponHits > 0 then + + +----- Show number of rows per table + + -- Process the data and generate report + local rpt = '' + if UI.rowCount(rptTargetsHit) > 0 then + for target,_ in pairs(rptTargetsHit) do + local section = '' + local t_row = TITS.TargetList[target] + local t_health = targetHealth[target] + local hits = rptHitsData[target] + + local max_h = TITS.getMaxTargetLife(target) + local sp_h = t_health.start_health + local ep_h = t_health.end_health + + local pass_delta = (sp_h - ep_h)*100/max_h + local overall_delta = ep_h*100/max_h + + local t_status = '' + if overall_delta >= 0 and overall_delta < 10 then + t_status = 'destroyed' + elseif overall_delta >= 10 and overall_delta < 25 then + t_status = 'heavily damaged' + elseif overall_delta >= 25 and overall_delta < 50 then + t_status = 'moderately damaged' + elseif overall_delta >= 50 and overall_delta < 75 then + t_status = 'slightly damaged' + else + t_status = 'operational' + end -- if overall_delta >= 0 and overall_delta < 10 then + + section = string.format(" >>> %s is %s (pass damage: %s%%)",t_row.displayName,t_status,mist.utils.round(pass_delta,1)) + -- hit details? + + rpt = string.format("%s\n%s",rpt,section) + end -- for target,_ in pairs(rptHitsData) do + end --if #rptHitsData > 0 then + + if rpt == '' then + rpt = 'No targets hit in this pass' + end + if TITS.onPass[_unitName] then + rpt = string.format('Pass BDA Summary (partial:)\n%s',rpt) + else + rpt = string.format('Pass BDA Summary:\n%s',rpt) + end + UI.messageToUnit(_unitName,rpt,{},30) + + end --if TITS.onPass[_unitName] then +end -- function +----------------------------------------------------------------------------------------------------------------------------------------------------- +function UI.rptListCoord(_unitName) + local _msg = '' + local cfg = UI.unitConfig[_unitName] + + for t,v in pairs(TITS.TargetList) do + if v.list_coor then + local pos = TITS.getTargetPos(t) + --local _coord, alt, auom = UI.getCoords(_unitName,pos) + local _coord_mgrs,_coord_dms,_coord_dm, alt, auom = UI.getCoords(_unitName,pos) + + _msg = _msg..string.format('[%s]-> Elev: %i%s MSL\n %s\n %s\n %s\n',v.displayName,alt,auom,_coord_mgrs,_coord_dms,_coord_dm) + end + end -- if v.list_coor then + if _msg ~= '' then + _msg = string.format('Coordinates:\n%s',_msg) + else + _msg = 'Coordinates not available' + end + UI.messageToUnit(_unitName, _msg,{},30) + return true +end -- function +----------------------------------------------------------------------------------------------------------------------------------------------------- +function UI.rptImpactResults(_unitName) + local config = UI.unitConfig[_unitName] + local weaponsFired = TITS.getWeponsFired(_unitName) + local shellsFired = TITS.getShellsFired(_unitName) + local shellHits = TITS.getShellHits(_unitName) + local title = '\nIndividual Impact Report (closest target)' + + if TITS.onPass[_unitName] then + title = title..' (partial)' + end + + if weaponsFired.count > 0 or shellsFired.total > 0 or shellHits.total > 0 then + local rpt = '' + local sec = '' + -- + -- Shell Hits ========================================================== + -- + if shellsFired.total > 0 or shellHits.total > 0 then + + local ROT = 0 + if shellsFired.total > 0 then + ROT = mist.utils.round(100*shellHits.total/shellsFired.total,1) + sec = string.format(' %s Rounds fired / hits: %s%%\n',shellsFired.total,ROT) + end + + + --loop thru shells + for _, d in pairs(shellHits.list) do + local targetName = TITS.TargetList[d.target].displayName + if UI.Designation.type ~=0 and UI.Designation.Designated ~= '' then + if d.target == UI.Designation.Designated then + sec = string.format('%s -> %s hit %s rnds: %s - On Target!',sec,targetName, d.hits, d.shellType) + else + sec = string.format('%s -> %s hit %s rnds: %s - Wrong target!',sec,targetName, d.hits, d.shellType) + end + if config.desUseAttackRadial and UI.Designation.Designated == d.target then + local delta = mist.utils.round((shellsFired.shootHeading - UI.MagneticVar) - UI.Designation.AttackRadial, 0) + if delta >= 180 then + delta = 360-delta + end + if delta == 0 then + sec = sec..' \n' + else + sec = string.format('%s \n', sec,delta) + end + end + + + else + sec = string.format('%s -> %s hit %s rnds: %s\n',sec,targetName, d.hits, d.shellType) + end + end -- for shellType, count in pairs(shellsFired.list) do + + end --if shellsFired.total > 0 then + + rpt = sec + sec = '' + + -- + -- Weapon Hits ========================================================== + -- + if weaponsFired.count > 0 then + for _,wID in pairs(weaponsFired.list) do + local weaponData = TITS.getWeaponData(_unitName, wID) + local targetLN = string.format(" -> %s :",weaponData.weaponType) + + if weaponData.miss then + local missDist,missDistUOM = UI.convertDistance(_unitName,TITS.MaxMissDistance) + missDist = mist.utils.round(missDist,0) + targetLN = string.format("%s no targets within %s%s from the impact",targetLN,missDist,missDistUOM) + else + + local ct = weaponData.closestTarget + local ctd = weaponData.impacts[ct] + local ctDN= TITS.TargetList[ct].displayName + local hitTXT = 'No effect' + if ctd.hit then + hitTXT ='Good effect' + end + if UI.Designation.type ~=0 and UI.Designation.Designated ~= '' then + if UI.Designation.Designated == ct then + hitTXT =hitTXT..' on designated target!' + else + hitTXT =hitTXT..' on wrong target!' + end + if config.desUseAttackRadial and UI.Designation.Designated == ct then + local delta = mist.utils.round((weaponData.releaseHeading - UI.MagneticVar) - UI.Designation.AttackRadial, 0) + if delta >= 180 then + delta = 360-delta + end + if delta == 0 then + hitTXT = hitTXT..' ' + else + hitTXT = string.format('%s ', hitTXT,delta) + end + end + + else + hitTXT =hitTXT..' on target!' + end + targetLN = string.format("%s %s\n",targetLN,hitTXT) + -- Impact data + local dist,distUOM = UI.convertDistance(_unitName,ctd.distance) + dist = mist.utils.round(dist,0) + + local direction = UI.getRelativeDirection(weaponData.releaseHeading ,ctd.targetPos, ctd.impactPos) + direction = UI.getClockDirection(direction) + + local slantRange = mist.utils.get3DDist(weaponData.releasePosition, ctd.targetPos) + + local angDist,angDistUOM = UI.convertAngDistance(_unitName,ctd.distance/(slantRange * 0.1)) + angDist = mist.utils.round(angDist,2) + + targetLN = string.format("%s %s %s from %s @ %s o'clock (%s %s)\n" + , targetLN + , dist + , distUOM + , ctDN + , direction + , angDist + , angDistUOM + ) + --Release params + if config.DisplayReleaseData then + local pos = weaponData.releasePosition + local height = land.getHeight({x = pos.x, y = pos.z}) + local releaseAlt = pos.y - height -- in meters. Release AGL + local alt, altUOM = UI.convertAltitude(_unitName,releaseAlt) + alt = mist.utils.round(alt,0) + local speed, speedUOM = UI.convertSpeed(_unitName,weaponData.releaseSpeed) + speed = mist.utils.round(speed,0) + local slant,slantUOM = UI.convertDistance(_unitName,slantRange) + slant = mist.utils.round(slant,0) + + local pitch = mist.utils.round(weaponData.releasePitch,1) + local roll = mist.utils.round(weaponData.releaseRoll,1) + local yaw = mist.utils.round(weaponData.releaseYaw,1) + + local RIdistance,RIdistUOM = UI.convertDistance(_unitName,weaponData.rollInDistance) + local RIat,RIatUOM = UI.convertAltitude(_unitName,weaponData.rollInAltitudeAT) + RIdistance = mist.utils.round(RIdistance,0) + RIat = mist.utils.round(RIat,0) + + local relP = string.format(" Rel. Params: Alt: %s%s AGL Speed: %s%s Hdg:%s°\n" + + , alt , altUOM + , speed , speedUOM + ,mist.utils.round(weaponData.releaseHeading,0) + ) + relP = string.format("%s S.RNG: %s%s P: %s R:%s Y:%s\n" + , relP + , slant , slantUOM + , pitch + , roll + , yaw + ) + + relP = string.format("%s Roll-in: Dist: %s%s Alt: %s%s (above target)\n",relP,RIdistance,RIdistUOM,RIat,RIatUOM) + + targetLN = string.format("%s%s",targetLN,relP) + end -- if cfg.DisplayReleaseData then + + end -- if weaponData.miss then + + sec = sec..targetLN + end -- for _,wID in pairs(weaponsFired.list) do + end --if weaponsFired.count > 0 + + if rpt ~= '' then + rpt = string.format ('%s\n%s',rpt,sec) + else + rpt = sec + end + sec = '' + -- + -- Popups Hits ========================================================== + -- + if pcall(function() return PTC.Version end ) then + if PTC.passTargetsUP > 0 then + sec = string.format("%s Popups hit: %s/%s",sec,PTC.passTargetsHIT ,PTC.passTargetsUP ) + end + end + + if rpt ~= '' then + rpt = string.format ('%s\n%s',rpt,sec) + else + rpt = sec + end + sec = '' + -- + -- Show report ========================================================== + -- + rpt = string.format ('%s\n%s',title,rpt) + UI.messageToUnit(_unitName,rpt,{},30) + else -- if weaponsFired.count > 0 or shellsFired.total > 0 then + UI.messageToUnit(_unitName,title..'\nThere is nothing to report.',{},5) + end -- if weaponsFired.count > 0 or shellsFired.total > 0 then + +end --function +----------------------------------------------------------------------------------------------------------------------------------------------------- +function UI.rptImpactResultsGrouped(_unitName) + local config = UI.unitConfig[_unitName] + local weaponsFired = TITS.getWeponsFired(_unitName) + local shellsFired = TITS.getShellsFired(_unitName) + local shellHits = TITS.getShellHits(_unitName) + local title = '\nGrouped Impact Report (closest target)' + + if TITS.onPass[_unitName] then + title = title..' (partial)' + end + + if weaponsFired.count > 0 or shellsFired.total > 0 or shellHits.total > 0 then + local rpt = '' + local sec = '' + -- + -- Shell Hits ========================================================== + -- + if shellsFired.total > 0 or shellHits.total > 0 then + + local ROT = 0 + if shellsFired.total > 0 then + ROT = mist.utils.round(100*shellHits.total/shellsFired.total,1) + sec = string.format(' %s Rounds fired / hits: %s%%\n',shellsFired.total,ROT) + end + + --loop thru shells + for _, d in pairs(shellHits.list) do + local targetName = TITS.TargetList[d.target].displayName + if UI.Designation.type ~=0 and UI.Designation.Designated ~= '' then + if d.target == UI.Designation.Designated then + sec = string.format('%s -> %s hit %s rnds: %s - On Target!',sec,targetName, d.hits, d.shellType) + else + sec = string.format('%s -> %s hit %s rnds: %s - Wrong target!',sec,targetName, d.hits, d.shellType) + end + + if config.desUseAttackRadial and UI.Designation.Designated == d.target then + local delta = mist.utils.round((shellsFired.shootHeading - UI.MagneticVar) - UI.Designation.AttackRadial, 0) + if delta >= 180 then + delta = 360-delta + end + if delta == 0 then + sec = sec..' \n' + else + sec = string.format('%s \n', sec,delta) + end + end + + + else + sec = string.format('%s -> %s hit %s rnds: %s\n',sec,targetName, d.hits, d.shellType) + end + end -- for shellType, count in pairs(shellsFired.list) do + + end --if shellsFired.total > 0 then + + rpt = sec + sec = '' + + -- + -- Groups ========================================================== + -- + local groups = TITS.getGroups(_unitName) + for _ , groupID in pairs(groups.list) do + local groupData = TITS.getGroupData(_unitName, groupID) + local targetLN = string.format(" -> %s :",groupData.weaponType) + + if groupData.miss then + local missDist,missDistUOM = UI.convertDistance(_unitName,TITS.MaxMissDistance) + missDist = mist.utils.round(missDist,0) + targetLN = string.format("%s no targets within %s%s from the impact",targetLN,missDist,missDistUOM) + else + + local ct = groupData.closestTarget + local ctDN= TITS.TargetList[ct].displayName + local hitTXT = 'No effect' + if groupData.closestTargetHit then + hitTXT ='Good effect' + end + if UI.Designation.type ~=0 and UI.Designation.Designated ~= '' then + if UI.Designation.Designated == ct then + hitTXT =hitTXT..' on designated target!' + else + hitTXT =hitTXT..' on wrong target!' + end + + if config.desUseAttackRadial and UI.Designation.Designated == ct then + local delta = mist.utils.round((weaponData.releaseHeading - UI.MagneticVar) - UI.Designation.AttackRadial, 0) + if delta >= 180 then + delta = 360-delta + end + if delta == 0 then + hitTXT = hitTXT..' ' + else + hitTXT = string.format('%s ', hitTXT,delta) + end + end + + + else + hitTXT =hitTXT..' on target!' + end + + targetLN = string.format("%s %s (%s/%s hits)\n",targetLN,hitTXT,groupData.closestTargetHitCount, groupData.weaponCount) + -- Impact data + local dist,distUOM = UI.convertDistance(_unitName,groupData.closestTargetDist) + dist = mist.utils.round(dist,0) + + local direction = UI.getRelativeDirection(groupData.releaseHeading ,groupData.closestTargetPos, groupData.impactPos) + direction = UI.getClockDirection(direction) + + local slantRange = mist.utils.get3DDist(groupData.releasePosition, groupData.closestTargetPos) + + local angDist,angDistUOM = UI.convertAngDistance(_unitName,groupData.closestTargetDist/(slantRange * 0.1)) + angDist = mist.utils.round(angDist,2) + + local grpSize,grpSizeUOM = UI.convertAngDistance(_unitName,groupData.size) + grpSize = mist.utils.round(grpSize,2) + + + -- hit count / weapon count + + targetLN = string.format("%s %s %s from %s @ %s o'clock (%s %s)\n" + , targetLN + , dist + , distUOM + , ctDN + , direction + , angDist + , angDistUOM + ) + targetLN = string.format("%s Group size: %s%s\n" + , targetLN + , grpSize + , grpSizeUOM + ) + --Release params + if config.DisplayReleaseData then + local pos = groupData.releasePosition + local height = land.getHeight({x = pos.x, y = pos.z}) + local releaseAlt = pos.y - height -- in meters. Release AGL + local alt, altUOM = UI.convertAltitude(_unitName,releaseAlt) + alt = mist.utils.round(alt,0) + local speed, speedUOM = UI.convertSpeed(_unitName,groupData.releaseSpeed) + speed = mist.utils.round(speed,0) + local slant,slantUOM = UI.convertDistance(_unitName,slantRange) + slant = mist.utils.round(slant,0) + + local pitch = mist.utils.round(groupData.releasePitch,1) + local roll = mist.utils.round(groupData.releaseRoll,1) + local yaw = mist.utils.round(groupData.releaseYaw,1) + + local RIdistance,RIdistUOM = UI.convertDistance(_unitName,groupData.rollInDistance) + local RIat,RIatUOM = UI.convertAltitude(_unitName,groupData.rollInAltitudeAT) + RIdistance = mist.utils.round(RIdistance,0) + RIat = mist.utils.round(RIat,0) + + + local relP = string.format(" Rel. Params: Alt: %s%s AGL Speed: %s%s Hdg:%s°\n" + , alt , altUOM + , speed , speedUOM + ,mist.utils.round(groupData.releaseHeading,0) + ) + relP = string.format("%s S.RNG: %s%s P: %s R:%s Y:%s\n" + , relP + , slant , slantUOM + , pitch + , roll + , yaw + ) + + relP = string.format("%s Roll-in: Dist: %s%s Alt: %s%s (above target)\n",relP,RIdistance,RIdistUOM,RIat,RIatUOM) + + targetLN = string.format("%s%s",targetLN,relP) + end -- if cfg.DisplayReleaseData then + + end -- if weaponData.miss then + + sec = sec..targetLN + + end -- for _,groupID in pairs(groups) do + + + -- + -- Popups Hits ========================================================== + -- + if pcall(function() return PTC.Version end ) then + if PTC.passTargetsUP > 0 then + sec = string.format("%s Popups hit: %s/%s",sec,PTC.passTargetsHIT ,PTC.passTargetsUP ) + end + end + + if rpt ~= '' then + rpt = string.format ('%s\n%s',rpt,sec) + else + rpt = sec + end + sec = '' + -- + -- Show report ========================================================== + -- + rpt = string.format ('%s\n%s',title,rpt) + UI.messageToUnit(_unitName,rpt,{},30) + else -- if weaponsFired.count > 0 or shellsFired.total > 0 then + UI.messageToUnit(_unitName,title..'\nThere is nothing to report.',{},5) + end -- if weaponsFired.count > 0 or shellsFired.total > 0 then + + + +end -- function +----------------------------------------------------------------------------------------------------------------------------------------------------- +function UI.rptImpactResultsDes(_unitName) + local config = UI.unitConfig[_unitName] + local weaponsFired = TITS.getWeponsFired(_unitName) + local shellsFired = TITS.getShellsFired(_unitName) + local shellHits = TITS.getShellHits(_unitName) + local title = '\nIndividual Impact Report (designated target)' + + if TITS.onPass[_unitName] then + title = title..' (partial)' + end + + if UI.Designation.type ~=0 and UI.Designation.Designated ~= '' then + if weaponsFired.count > 0 or shellsFired.total > 0 or shellHits.total > 0 then + local rpt = '' + local sec = '' + -- Header + local ct = UI.Designation.Designated + local ctDN= TITS.TargetList[ct].displayName + rpt = string.format(' Target: %s',ctDN) + if config.desUseAttackRadial then + rpt = string.format('%s / Attack radial: %s°',rpt,UI.Designation.AttackRadial) + end + + -- + -- Shell Hits ========================================================== + -- + if shellsFired.total > 0 or shellHits.total > 0 then + local ROT = 0 + if shellsFired.total > 0 then + ROT = mist.utils.round(100*shellHits.total/shellsFired.total,1) + sec = string.format(' %s Rounds fired / hits: %s%%\n',shellsFired.total,ROT) + end + + --loop thru shells + for _, d in pairs(shellHits.list) do + local targetName = TITS.TargetList[d.target].displayName + if UI.Designation.type ~=0 and UI.Designation.Designated ~= '' then + if d.target == UI.Designation.Designated then + sec = string.format('%s -> %s hit %s rnds: %s - On Target!',sec,targetName, d.hits, d.shellType) + else + sec = string.format('%s -> %s hit %s rnds: %s - Wrong target!',sec,targetName, d.hits, d.shellType) + end + if config.desUseAttackRadial and UI.Designation.Designated == d.target then + local delta = mist.utils.round((shellsFired.shootHeading - UI.MagneticVar) - UI.Designation.AttackRadial, 0) + if delta >= 180 then + delta = 360-delta + end + if delta == 0 then + sec = sec..' \n' + else + sec = string.format('%s \n', sec,delta) + end + end + + + else + sec = string.format('%s -> %s hit %s rnds: %s\n',sec,targetName, d.hits, d.shellType) + end + end -- for shellType, count in pairs(shellsFired.list) do + + end --if shellsFired.total > 0 then + + if sec ~= '' then + if rpt ~= '' then + rpt = string.format ('%s\n%s',rpt,sec) + else + rpt = sec + end + sec = '' + end + + -- + -- Weapon Hits ========================================================== + -- + if weaponsFired.count > 0 then + for _,wID in pairs(weaponsFired.list) do + local weaponData = TITS.getWeaponData(_unitName, wID) + local targetLN = string.format(" -> %s :",weaponData.weaponType) + local missDist,missDistUOM = UI.convertDistance(_unitName,TITS.MaxMissDistance) + if weaponData.miss then + missDist = mist.utils.round(missDist,0) + targetLN = string.format("%s no targets within %s%s from the impact",targetLN,missDist,missDistUOM) + else + local ctd = weaponData.impacts[ct] + local hitTXT = 'No effect' + + if ctd then + + if ctd.hit then + hitTXT ='Good effect' + end + + if config.desUseAttackRadial and UI.Designation.Designated == ct then + local delta = mist.utils.round((weaponData.releaseHeading - UI.MagneticVar) - UI.Designation.AttackRadial, 0) + if delta >= 180 then + delta = 360-delta + end + if delta == 0 then + hitTXT = hitTXT..' ' + else + hitTXT = string.format('%s ', hitTXT,delta) + end + end + + + targetLN = string.format("%s %s\n",targetLN,hitTXT) + -- Impact data + local dist,distUOM = UI.convertDistance(_unitName,ctd.distance) + dist = mist.utils.round(dist,0) + + local direction = UI.getRelativeDirection(weaponData.releaseHeading ,ctd.targetPos, ctd.impactPos) + direction = UI.getClockDirection(direction) + + local slantRange = mist.utils.get3DDist(weaponData.releasePosition, ctd.targetPos) + + local angDist,angDistUOM = UI.convertAngDistance(_unitName,ctd.distance/(slantRange * 0.1)) + angDist = mist.utils.round(angDist,2) + + + targetLN = string.format("%s %s %s from target @ %s o'clock (%s %s)\n" + , targetLN + , dist + , distUOM + , direction + , angDist + , angDistUOM + ) + + --Release params + if config.DisplayReleaseData then + local pos = weaponData.releasePosition + local height = land.getHeight({x = pos.x, y = pos.z}) + local releaseAlt = pos.y - height -- in meters. Release AGL + local alt, altUOM = UI.convertAltitude(_unitName,releaseAlt) + alt = mist.utils.round(alt,0) + local speed, speedUOM = UI.convertSpeed(_unitName,weaponData.releaseSpeed) + speed = mist.utils.round(speed,0) + local slant,slantUOM = UI.convertDistance(_unitName,slantRange) + slant = mist.utils.round(slant,0) + + local pitch = mist.utils.round(weaponData.releasePitch,1) + local roll = mist.utils.round(weaponData.releaseRoll,1) + local yaw = mist.utils.round(weaponData.releaseYaw,1) + + local relP = string.format(" Rel. Params: Alt: %s%s AGL Speed: %s%s Hdg:%s°\n" + , alt , altUOM + , speed , speedUOM + ,mist.utils.round(weaponData.releaseHeading,0) + ) + relP = string.format("%s S.RNG: %s%s P: %s R:%s Y:%s\n" + , relP + , slant , slantUOM + , pitch + , roll + , yaw + ) + + targetLN = string.format("%s%s",targetLN,relP) + end -- if cfg.DisplayReleaseData then + else -- if ctd then + targetLN = string.format("%s no impact within %s%s of the designated target",targetLN,missDist,missDistUOM) + end -- if ctd then + end -- if weaponData.miss then + + sec = sec..targetLN + end -- for _,wID in pairs(weaponsFired.list) do + end --if weaponsFired.count > 0 + + if rpt ~= '' then + rpt = string.format ('%s\n%s',rpt,sec) + else + rpt = sec + end + sec = '' + -- + -- Popups Hits ========================================================== + -- + if pcall(function() return PTC.Version end ) then + if PTC.passTargetsUP > 0 then + sec = string.format("%s Popups hit: %s/%s",sec,PTC.passTargetsHIT ,PTC.passTargetsUP ) + end + end + + if rpt ~= '' then + rpt = string.format ('%s\n%s',rpt,sec) + else + rpt = sec + end + sec = '' + -- + -- Show report ========================================================== + -- + rpt = string.format ('%s\n%s',title,rpt) + UI.messageToUnit(_unitName,rpt,{},30) + else -- if weaponsFired.count > 0 or shellsFired.total > 0 then + UI.messageToUnit(_unitName,title..'\nThere is nothing to report.',{},5) + end -- if weaponsFired.count > 0 or shellsFired.total > 0 then + + else + UI.messageToUnit(_unitName,title..'\nThere is no designated target.',{},5) + end --if UI.Designation.type ~=0 and UI.Designation.Designated ~= '' then + +end --function +----------------------------------------------------------------------------------------------------------------------------------------------------- +function UI.rptImpactResultsDesGrouped(_unitName) + local config = UI.unitConfig[_unitName] + local weaponsFired = TITS.getWeponsFired(_unitName) + local shellsFired = TITS.getShellsFired(_unitName) + local shellHits = TITS.getShellHits(_unitName) + local title = '\nGrouped Impact Report (designated target)' + + if TITS.onPass[_unitName] then + title = title..' (partial)' + end + + if UI.Designation.type ~=0 and UI.Designation.Designated ~= '' then + if weaponsFired.count > 0 or shellsFired.total > 0 or shellHits.total > 0 then + local rpt = '' + local sec = '' + -- Header + local ct = UI.Designation.Designated + local ctDN= TITS.TargetList[ct].displayName + rpt = string.format(' Target: %s',ctDN) + if config.desUseAttackRadial then + rpt = string.format('%s / Attack radial: %s°',rpt,UI.Designation.AttackRadial) + end + + -- + -- Shell Hits ========================================================== + -- + if shellsFired.total > 0 or shellHits.total > 0 then + local ROT = 0 + if shellsFired.total > 0 then + ROT = mist.utils.round(100*shellHits.total/shellsFired.total,1) + sec = string.format(' %s Rounds fired / hits: %s%%\n',shellsFired.total,ROT) + end + + --loop thru shells + for _, d in pairs(shellHits.list) do + local targetName = TITS.TargetList[d.target].displayName + if UI.Designation.type ~=0 and UI.Designation.Designated ~= '' then + if d.target == UI.Designation.Designated then + sec = string.format('%s -> %s hit %s rnds: %s - On Target!',sec,targetName, d.hits, d.shellType) + else + sec = string.format('%s -> %s hit %s rnds: %s - Wrong target!',sec,targetName, d.hits, d.shellType) + end + if config.desUseAttackRadial and UI.Designation.Designated == d.target then + local delta = mist.utils.round((shellsFired.shootHeading - UI.MagneticVar) - UI.Designation.AttackRadial, 0) + if delta >= 180 then + delta = 360-delta + end + if delta == 0 then + sec = sec..' \n' + else + sec = string.format('%s \n', sec,delta) + end + end + + + else + sec = string.format('%s -> %s hit %s rnds: %s\n',sec,targetName, d.hits, d.shellType) + end + end -- for shellType, count in pairs(shellsFired.list) do + + end --if shellsFired.total > 0 then + + if sec ~= '' then + if rpt ~= '' then + rpt = string.format ('%s\n%s',rpt,sec) + else + rpt = sec + end + sec = '' + end + + -- + -- Groups ========================================================== + -- + if weaponsFired.count > 0 then + local groups = TITS.getGroups(_unitName) + for _,groupID in pairs(groups.list) do + local groupData = TITS.getGroupData(_unitName, groupID) + local targetLN = string.format(" -> %s :",groupData.weaponType) + local missDist,missDistUOM = UI.convertDistance(_unitName,TITS.MaxMissDistance) + if groupData.miss then + missDist = mist.utils.round(missDist,0) + targetLN = string.format("%s no targets within %s%s from the impact",targetLN,missDist,missDistUOM) + else + + -- get all the weapons in the group with an impact on the target + local ctd = {} + local ctPos = {} + local ctHits = 0 + for _,row in pairs(groupData.TargetImpacts) do + if row.target == UI.Designation.Designated then + table.insert(ctd,row.targetPos) + ctPos =row.targetPos + if row.hit then + ctHits = ctHits + 1 + end + end + end + + + --local ctd = weaponData.impacts[ct] + local hitTXT = 'No effect' + + if UI.rowCount(ctd) > 0 then + + if UI.inTable(UI.Designation.Designated,groupData.targetsHit) then + hitTXT ='Good effect' + end + + if config.desUseAttackRadial and UI.Designation.Designated == ct then + local delta = mist.utils.round((groupData.releaseHeading - UI.MagneticVar) - UI.Designation.AttackRadial, 0) + if delta >= 180 then + delta = 360-delta + end + if delta == 0 then + hitTXT = hitTXT..' ' + else + hitTXT = string.format('%s ', hitTXT,delta) + end + end + + targetLN = string.format("%s %s (%s/%s hits)\n",targetLN,hitTXT,ctHits, groupData.weaponCount) + --targetLN = string.format("%s %s\n",targetLN,hitTXT) + -- Impact data from group center + local distance = mist.utils.get3DDist(groupData.impactPos,ctPos) + + local dist,distUOM = UI.convertDistance(_unitName,distance) + dist = mist.utils.round(dist,0) + + local direction = UI.getRelativeDirection(groupData.releaseHeading ,ctPos, groupData.impactPos) + direction = UI.getClockDirection(direction) + + local slantRange = mist.utils.get3DDist(groupData.releasePosition, ctPos) + + local angDist,angDistUOM = UI.convertAngDistance(_unitName,distance/(slantRange * 0.1)) + angDist = mist.utils.round(angDist,2) + + local grpSize,grpSizeUOM = UI.convertAngDistance(_unitName,groupData.size) + grpSize = mist.utils.round(grpSize,2) + + targetLN = string.format("%s %s %s from target @ %s o'clock (%s %s)\n" + , targetLN + , dist + , distUOM + , direction + , angDist + , angDistUOM + ) + targetLN = string.format("%s Group size: %s%s\n" + , targetLN + , grpSize + , grpSizeUOM + ) + --Release params + if config.DisplayReleaseData then + local pos = groupData.releasePosition + local height = land.getHeight({x = pos.x, y = pos.z}) + local releaseAlt = pos.y - height -- in meters. Release AGL + local alt, altUOM = UI.convertAltitude(_unitName,releaseAlt) + alt = mist.utils.round(alt,0) + local speed, speedUOM = UI.convertSpeed(_unitName,groupData.releaseSpeed) + speed = mist.utils.round(speed,0) + local slant,slantUOM = UI.convertDistance(_unitName,slantRange) + slant = mist.utils.round(slant,0) + + local pitch = mist.utils.round(groupData.releasePitch,1) + local roll = mist.utils.round(groupData.releaseRoll,1) + local yaw = mist.utils.round(groupData.releaseYaw,1) + + local relP = string.format(" Rel. Params: Alt: %s%s AGL Speed: %s%s Hdg:%s°\n" + , alt , altUOM + , speed , speedUOM + , mist.utils.round(groupData.releaseHeading,0) + ) + relP = string.format("%s S.RNG: %s%s P: %s R:%s Y:%s\n" + , relP + , slant , slantUOM + , pitch + , roll + , yaw + ) + + targetLN = string.format("%s%s",targetLN,relP) + end -- if cfg.DisplayReleaseData then + else -- if ctd then + targetLN = string.format("%s no impact within %s%s of the designated target",targetLN,missDist,missDistUOM) + end -- if ctd then + end -- if weaponData.miss then + + sec = sec..targetLN + end -- for _,wID in pairs(weaponsFired.list) do + end --if weaponsFired.count > 0 + + if rpt ~= '' then + rpt = string.format ('%s\n%s',rpt,sec) + else + rpt = sec + end + sec = '' + -- + -- Popups Hits ========================================================== + -- + if pcall(function() return PTC.Version end ) then + if PTC.passTargetsUP > 0 then + sec = string.format("%s Popups hit: %s/%s",sec,PTC.passTargetsHIT ,PTC.passTargetsUP ) + end + end + + if rpt ~= '' then + rpt = string.format ('%s\n%s',rpt,sec) + else + rpt = sec + end + sec = '' + -- + -- Show report ========================================================== + -- + rpt = string.format ('%s\n%s',title,rpt) + UI.messageToUnit(_unitName,rpt,{},30) + else -- if weaponsFired.count > 0 or shellsFired.total > 0 then + UI.messageToUnit(_unitName,title..'\nThere is nothing to report.',{},5) + end -- if weaponsFired.count > 0 or shellsFired.total > 0 then + + else + UI.messageToUnit(_unitName,title..'\nThere is no designated target.',{},5) + end --if UI.Designation.type ~=0 and UI.Designation.Designated ~= '' then + +end --function + +-- +--################################################################################################################################################# +-- ############# Illumination ############### +--################################################################################################################################################# +-- +function UI.AddIlluminationZone(_zoneName, _Rounds) + UI.illuminationZones[_zoneName] = _Rounds +end -- function +----------------------------------------------------------------------------------------------------------------------------------------------------- +function UI.illuminate(params, time) + if UI.illuminating then + if UI.rowCount(UI.illuminationZones) > 0 then + for z,r in pairs(UI.illuminationZones) do + for i=1,r do + local pos = mist.utils.makeVec3(mist.getRandomPointInZone(z),mist.random(450,550)) + trigger.action.illuminationBomb(pos,7500) + end + end + end + return timer.getTime() + 150 + else + return nil + end +end -- function +-------------------- +-- +--################################################################################################################################################# +-- ############# Designation ############### +--################################################################################################################################################# +-- +function UI.AddDesignationZone(_ID, _Name) + UI.DesignationZones[_ID] = { id = _ID, name = _Name} +end -- function +----------------------------------------------------------------------------------------------------------------------------------------------------- +function UI.AddDesignator(_name, _displayName, _zones, _allowLaser, _allowIR, _code) + local dn = _displayName + if _displayName == '' then + dn = _name + end + UI.Designators[_name] = { + name = _name + , displayName = dn + , zones = _zones + , allowLaser = _allowLaser + , allowIR = _allowIR + , code = _code + + } +end -- function +----------------------------------------------------------------------------------------------------------------------------------------------------- +function UI.desSetZone(_args) + + local unitName = _args[1] + local zoneID = _args[2] + + UI.Designation.zone = zoneID + local z = UI.DesignationZones[zoneID] + UI.messageToUnit(unitName,string.format('Designation zone set to: %s',z.name)) + +end -- function +----------------------------------------------------------------------------------------------------------------------------------------------------- +function UI.desWP(_args) + local _unitName = _args[1] + local isNew = _args[2] + local ERROR = 0 + local targetList = {} + local n = 0 -- Number of designatable targets in zone + local tgt -- selectec target + local cfg = UI.unitConfig[_unitName] + -- Get WP designatable targets in the selected zone + if isNew then + for _,row in pairs(TITS.TargetList) do + if row.des_zone == UI.Designation.zone and row.des_wp and TITS.getTargetLife(row.name) > 0 then + n = n + 1 + targetList[n] = row.name + end + end -- for t,row in pairs(TITS.TargetList) do + if n == 0 then + ERROR = 1 -- No designatable targets in zone + else + local r = mist.random(1,n) + tgt = targetList[r] + end + else -- if isNew or UI.Designation.Designated == '' then + tgt = UI.Designation.Designated + if TITS.getTargetLife(tgt) <= 0 then + ERROR = 2 -- prior designated target is dead + end + end -- if isNew then + if ERROR == 0 then -- Designate the target + --select a random target + local tgtPos = TITS.getTargetPos(tgt) + local markPos = mist.utils.makeVec3GL(mist.getRandPointInCircle(tgtPos,UI.maxWPmarkDist,UI.minWPmarkDist,0,360)) + local _dir = mist.utils.toDegree(mist.utils.getDir(mist.vec.sub(tgtPos, markPos))) + local _dist = mist.utils.get2DDist(markPos,tgtPos) + local _direction = '' + local oTarget = TITS.TargetList[tgt] + + if _dir >= 0 and _dir < 22 then + _direction = 'North' + elseif _dir >= 22 and _dir < 68 then + _direction = 'North East' + elseif _dir >= 68 and _dir < 113 then + _direction = 'East' + elseif _dir >= 113 and _dir < 158 then + _direction = 'South East' + elseif _dir >= 158 and _dir < 203 then + _direction = 'South' + elseif _dir >= 203 and _dir < 248 then + _direction = 'South West' + elseif _dir >= 248 and _dir < 293 then + _direction = 'West' + elseif _dir >= 293 and _dir < 338 then + _direction = 'North West' + elseif _dir >= 338 and _dir <= 360 then + _direction = 'North' + end + + local ar_brief = '' + if cfg.desUseAttackRadial then + UI.Designation.AttackRadial = mist.random(1,359) + ar_brief = string.format('\nAttack radial %s° (magnetic)',UI.Designation.AttackRadial) + end + + -- prepare the briefing + local dist,distUOM = UI.convertDistance(_unitName,_dist) + dist = mist.utils.round(dist,0) + if distUOM == 'ft' then + dist = math.floor(dist / 25) * 25 + else + dist = math.floor(dist / 10) * 10 + end + + local briefing = string.format("%s marked with WP\n%i %s, %s from the mark%s",oTarget.displayName, dist,distUOM, _direction,ar_brief) + + UI.Designation.Designator = '' -- unit name + UI.Designation.Designated = tgt + UI.Designation.type = 1 -- 0 = none, 1 = WP , 2 = laser, 3 = IR + UI.Designation.briefing = briefing + + trigger.action.smoke(markPos, trigger.smokeColor.White ) + UI.messageToUnit(_unitName,briefing,{},45) + else + local errorMSG ='' + if ERROR == 1 then + errorMSG = string.format('There are no designatable targets in zone %s',UI.DesignationZones[UI.Designation.zone].name ) + elseif ERROR == 2 then + errorMSG ='Target is dead. Select a new one' + end + UI.messageToUnit(_unitName,errorMSG,{},45) + + UI.Designation.Designator = '' -- unit name + UI.Designation.Designated = '' + UI.Designation.type = 0 -- 0 = none, 1 = WP , 2 = laser, 3 = IR + UI.Designation.briefing = '' + + + end --if not ERROR then + + +end -- function +----------------------------------------------------------------------------------------------------------------------------------------------------- +function UI.desNM(_unitName) + local config = UI.unitConfig[_unitName] + local ERROR = 0 + local targetList = {} + local n = 0 -- Number of designatable targets in zone + local tgt -- selectec target + -- Get WP designatable targets in the selected zone + + for _,row in pairs(TITS.TargetList) do + if row.des_zone == UI.Designation.zone and row.des_nomark and TITS.getTargetLife(row.name) > 0 then + n = n + 1 + targetList[n] = row.name + end + end -- for t,row in pairs(TITS.TargetList) do + if n == 0 then + ERROR = 1 -- No designatable targets in zone + else + local r = mist.random(1,n) + tgt = targetList[r] + end + + + if ERROR == 0 then -- Designate the target + -- prepare the briefing + local oTarget = TITS.TargetList[tgt] + local cfg = UI.unitConfig[_unitName] + + local ar_brief = '' + if cfg.desUseAttackRadial then + UI.Designation.AttackRadial = mist.random(1,359) + ar_brief = string.format('\nAttack radial %s° (magnetic)',UI.Designation.AttackRadial) + end + + + local briefing = '' + if config.desDisplayCoords then + local pos = TITS.getTargetPos(tgt) + --local _coord, alt, auom = UI.getCoords(_unitName,pos) + local _coord_mgrs,_coord_dms,_coord_dm, alt, auom = UI.getCoords(_unitName,pos) + --Elev: %i%s MSL\n %s\n %s\n %s\n' + briefing = string.format("\nYour target is %s at Elev: %i%s MSL\n %s\n %s\n %s\n",oTarget.displayName,alt,auom,_coord_mgrs,_coord_dms,_coord_dm) + else + briefing = string.format("\nYour target is %s",oTarget.displayName) + end + + briefing = string.format("%s%s",briefing,ar_brief) + + if oTarget.uiText1 ~= '' then + briefing = string.format('%s\n%s',briefing,oTarget.uiText1) + end + + if oTarget.uiText2 ~= '' then + briefing = string.format('%s\n%s',briefing,oTarget.uiText2) + end + + if oTarget.uiText3 ~= '' then + briefing = string.format('%s\n%s',briefing,oTarget.uiText3) + end + + UI.Designation.Designator = '' -- unit name + UI.Designation.Designated = tgt + UI.Designation.type = 4 -- 0 = none, 1 = WP , 2 = laser, 3 = IR, 4 = No mark + UI.Designation.briefing = briefing + + UI.messageToUnit(_unitName,briefing,{},45) + else + local errorMSG ='' + if ERROR == 1 then + errorMSG = string.format('There are no designatable targets in zone %s',UI.DesignationZones[UI.Designation.zone].name ) + end + UI.messageToUnit(_unitName,errorMSG,{},45) + + UI.Designation.Designator = '' -- unit name + UI.Designation.Designated = '' + UI.Designation.type = 0 -- 0 = none, 1 = WP , 2 = laser, 3 = IR + UI.Designation.briefing = '' + + + end --if not ERROR then + + +end -- function +----------------------------------------------------------------------------------------------------------------------------------------------------- +function UI.desRPTBrief(_unitName) + if UI.Designation.type ~= 0 then + UI.messageToUnit(_unitName,UI.Designation.briefing,{},45) + else + UI.messageToUnit(_unitName,'No targets designated',{},45) + end +end -- function +----------------------------------------------------------------------------------------------------------------------------------------------------- +function UI.desCancel(_unitName) + + UI.Designation.Designator = '' -- unit name + UI.Designation.Designated = '' + UI.Designation.type = 0 -- 0 = none, 1 = WP , 2 = laser, 3 = IR, 4 = No mark, 23 = laser/IR + UI.Designation.briefing = '' + UI.Designation.spotON = false + UI.messageToUnit(_unitName,'Designation cancelled',{},5) + +end -- function +----------------------------------------------------------------------------------------------------------------------------------------------------- +function UI.desLaserIR(_args) + local _unitName = _args[1] + local desType = _args[2] -- laser | IR + local designators = {} + local targets = {} + local Nt = 0 + local Nd = 0 + local cfg = UI.unitConfig[_unitName] + + if UI.Designation.type == 2 or UI.Designation.type == 3 or UI.Designation.type == 23 then + UI.messageToUnit(_unitName,'There is a prior laser/IR designation. Cancel it first',{},15) + return false + end + + -- get designatable targets is zone + for _,row in pairs(TITS.TargetList) do + if row.des_zone == UI.Designation.zone and TITS.getTargetLife(row.name) > 0 and + ((row.des_laser and desType == 'laser') or (row.des_ir and desType == 'IR') or (desType == 'laser/IR' and row.des_laser and row.des_ir ) ) then + Nt = Nt + 1 + targets[Nt] = row.name + end + end -- for t,row in pairs(TITS.TargetList) do + + -- get designators in zone + for _,row in pairs(UI.Designators) do + if UI.inTable(UI.Designation.zone, row.zones ) and ((row.allowLaser and desType == 'laser') or (row.allowIR and desType == 'IR') or (desType == 'laser/IR' and row.allowLaser and row.allowIR ) ) then + -- check designator life + local u = Unit.getByName(row.name) + if u then + Nd = Nd + 1 + designators[Nd] = row.name + end + end + end -- for t,row in pairs(TITS.TargetList) do + if Nt > 0 and Nd > 0 then + local Rt = mist.random(1,Nt) + local Rd = mist.random(1,Nd) + UI.Designation.Designator = designators[Rd] + UI.Designation.Designated = targets[Rt] + local designator = Unit.getByName(UI.Designation.Designator ) + + local desPos = TITS.getTargetPos(UI.Designation.Designated) + desPos = { x = desPos.x, y = desPos.y + 2.0, z = desPos.z } + if desType == 'laser' then + UI.Designation.type = 2 + UI.Designation.ray1 = Spot.createLaser(designator, {x = 0, y = 1, z = 0}, desPos , UI.Designators[UI.Designation.Designator].code) + elseif desType == 'IR' then + UI.Designation.type = 3 + UI.Designation.ray1 = Spot.createInfraRed(designator, {x = 0, y = 1, z = 0}, desPos) + else + UI.Designation.type = 23 + UI.Designation.ray1 = Spot.createLaser(designator, {x = 0, y = 1, z = 0}, desPos, UI.Designators[UI.Designation.Designator].code) + UI.Designation.ray2 = Spot.createInfraRed(designator, {x = 0, y = 1, z = 0}, desPos) + end + local function updateRay() + if UI.Designation.type == 2 or UI.Designation.type == 3 then + local pos = TITS.getTargetPos(UI.Designation.Designated) + pos = { x = pos.x, y = pos.y + 2.0, z = pos.z } + UI.Designation.ray1:setPoint(pos) + timer.scheduleFunction(updateRay, {}, timer.getTime() + 0.5) + elseif UI.Designation.type == 23 then + local pos = TITS.getTargetPos(UI.Designation.Designated) + pos = { x = pos.x, y = pos.y + 2.0, z = pos.z } + UI.Designation.ray1:setPoint(pos) + UI.Designation.ray2:setPoint(pos) + timer.scheduleFunction(updateRay, {}, timer.getTime() + 0.5) + else + UI.Designation.ray1:destroy() + if UI.Designation.ray2 then + UI.Designation.ray2:destroy() + end + end + end + timer.scheduleFunction(updateRay, {}, timer.getTime() + 0.5) + -- prepare briefing + + local ar_brief = '' + if cfg.desUseAttackRadial then + UI.Designation.AttackRadial = mist.random(1,359) + ar_brief = string.format('\nAttack radial %s° (magnetic)',UI.Designation.AttackRadial) + end + + + local dDN = UI.Designators[UI.Designation.Designator].displayName + local t = TITS.TargetList[UI.Designation.Designated] + local brief = string.format('%s designated by %s from %s', t.displayName, desType, dDN) + if UI.Designation.type == 2 or UI.Designation.type == 23 then + brief = string.format('%s with code: %s',brief,UI.Designators[UI.Designation.Designator].code) + end + + if cfg.desDisplayCoords then + local pos = TITS.getTargetPos(UI.Designation.Designated) + --local _coord, alt, auom = UI.getCoords(_unitName,pos) + local _coord_mgrs,_coord_dms,_coord_dm, alt, auom = UI.getCoords(_unitName,pos) + brief = string.format('%s\ntarget coords: Elev: %i%s MSL\n %s\n %s\n %s',brief,alt,auom, _coord_mgrs,_coord_dms,_coord_dm) + end + brief = string.format('%s%s',brief,ar_brief) + + local oTarget = TITS.TargetList[UI.Designation.Designated] + if oTarget.uiText1 ~= '' then + brief = string.format('%s\n%s',brief,oTarget.uiText1) + end + + if oTarget.uiText2 ~= '' then + brief = string.format('%s\n%s',brief,oTarget.uiText2) + end + + if oTarget.uiText3 ~= '' then + brief = string.format('%s\n%s',brief,oTarget.uiText3) + end + + + UI.messageToUnit(_unitName,brief,{},45) + + else -- if Nt > 0 and Nd > 0 then + local errorMSG = string.format('There are no designatable targets or designators in zone %s',UI.DesignationZones[UI.Designation.zone].name ) + UI.messageToUnit(_unitName,errorMSG,{},45) + end + return true +end -- function +----------------------------------------------------------------------------------------------------------------------------------------------------- + +-- +--################################################################################################################################################# +-- ############# Utility ############### +--################################################################################################################################################# +-- +function UI.getCoords(_unitName,pos) + local lat, lon, alt = coord.LOtoLL(pos) + + --MGRS + local _coord_mgrs = mist.tostringMGRS(coord.LLtoMGRS(coord.LOtoLL(pos)), 5):gsub(string.char(9),' ') + -- DMS + local _coord_dms = mist.tostringLL(lat, lon, 3, true):gsub(string.char(9),' ') + -- DM.mm + local _coord_dm = mist.tostringLL(lat, lon, 4):gsub(string.char(9),' ') + local a,auom = UI.convertAltitude(_unitName,alt) + return _coord_mgrs,_coord_dms,_coord_dm, a, auom +--[[ + local cfg = UI.unitConfig[_unitName] + local _coord = '' + local lat, lon, alt = coord.LOtoLL(pos) + + + if cfg.CoordFormat == 1 then --MGRS + _coord = mist.tostringMGRS(coord.LLtoMGRS(coord.LOtoLL(pos)), 5) + elseif cfg.CoordFormat == 2 then -- DMS + _coord = mist.tostringLL(lat, lon, 2, true) + else -- DM.mm + _coord = mist.tostringLL(lat, lon, 2) + end + + _coord = _coord:gsub(string.char(9),' ') + local a,auom = UI.convertAltitude(_unitName,alt) + return _coord, a, auom +--]] +end -- function +----------------------------------------------------------------------------------------------------------------------------------------------------- +function UI.convertSpeed(_unitName,_speed) + local s + local u + local cfg = UI.unitConfig[_unitName] + -- 1 = Knots, 2 = Mph, 3 = Kph + -- _speed in m/s + if cfg.SpeedUOM == 1 then + s = _speed * 1.943844 + u = 'kt' + elseif cfg.SpeedUOM == 2 then + s = _speed * 2.23694 + u = 'mph' + else + s = _speed * 36 + u = 'kph' + end + return s,u +end +----------------------------------------------------------------------------------------------------------------------------------------------------- +function UI.convertDistance(_unitName,_distance) + local s + local u + local cfg = UI.unitConfig[_unitName] + -- 1 = feet, 2 = yards, 3 = meters + -- _distance in meters + if cfg.DistanceUOM == 1 then + s = _distance * 3.28084 + u = 'ft' + elseif cfg.DistanceUOM == 2 then + s = _distance * 1.093613 + u = 'yd' + else + s = _distance + u = 'm' + end + return s,u +end +----------------------------------------------------------------------------------------------------------------------------------------------------- +function UI.convertAngDistance(_unitName,_distance) + local s + local u + local cfg = UI.unitConfig[_unitName] + -- 1 = feet, 2 = yards, 3 = meters + -- _distance in meters + if cfg.DistanceUOM == 1 or cfg.DistanceUOM == 2 then + s = _distance * 3.4377492368197 + u = 'MOA' + else + s = _distance + u = 'mils' + end + return s,u +end +----------------------------------------------------------------------------------------------------------------------------------------------------- +function UI.convertAltitude(_unitName,_altitude) + local s + local u + local cfg = UI.unitConfig[_unitName] + -- 1 = feet, 2 = yards, 3 = meters + -- _distance in meters + if cfg.AltitudeUOM == 1 then + s = _altitude * 3.28084 + u = 'ft' + else + s = _altitude + u = 'm' + end + return s,u +end +----------------------------------------------------------------------------------------------------------------------------------------------------- +function UI.getRelativeDirection(_refHeading,_point1, _point2) + local tgtBearing + local xUnit = _point1.x + local yUnit = _point1.z + local xZone = _point2.x + local yZone = _point2.z + + local xDiff = xUnit - xZone + local yDiff = yUnit - yZone + + local tgtAngle = math.deg(math.atan(yDiff/xDiff)) + + if xDiff > 0 then + tgtBearing = 180 + tgtAngle + end + + if xDiff < 0 and tgtAngle > 0 then + tgtBearing = tgtAngle + end + + if xDiff < 0 and tgtAngle < 0 then + tgtBearing = 360 + tgtAngle + end + + tgtBearing = tgtBearing - _refHeading + if tgtBearing > 360 then + tgtBearing = tgtBearing - 360 + end + if tgtBearing < 0 then + tgtBearing = 360 + tgtBearing + end + + return tgtBearing +end +----------------------------------------------------------------------------------------------------------------------------------------------------- +function UI.getClockDirection(_direction) + -- by cfrag + if not _direction then return 0 end + while _direction < 0 do + _direction = _direction + 360 + end + + if _direction < 15 then -- special case 12 o'clock past 12 o'clock + return 12 + end + + _direction = _direction + 15 -- add offset so we get all other times correct + return math.floor(_direction/30) + +end +----------------------------------------------------------------------------------------------------------------------------------------------------- +function UI.getDirection(_point1, _point2) + local tgtBearing + local xUnit = _point1.x + local yUnit = _point1.z + local xZone = _point2.x + local yZone = _point2.z + + local xDiff = xUnit - xZone + local yDiff = yUnit - yZone + + local tgtAngle = math.deg(math.atan(yDiff/xDiff)) + + if xDiff > 0 then + tgtBearing = 180 + tgtAngle + end + + if xDiff < 0 and tgtAngle > 0 then + tgtBearing = tgtAngle + end + + if xDiff < 0 and tgtAngle < 0 then + tgtBearing = 360 + tgtAngle + end + + + return tgtBearing +end +----------------------------------------------------------------------------------------------------------------------------------------------------- +function UI.rowCount(t) + local r = 0 + if t then + for _,_ in pairs(t) do + r = r + 1 + end + end + return r +end -- function +----------------------------------------------------------------------------------------------------------------------------------------------------- +function UI.inTable(v,t) + local r = true + for _,j in pairs(t) do + if j == v then + return true + end + end + return false + +end -- function +----------------------------------------------------------------------------------------------------------------------------------------------------- + +-- +--################################################################################################################################################# +-- ############# Main body ############### +--################################################################################################################################################# +-- +do + -- load event handler + world.addEventHandler(UI.EventHandler) + + trigger.action.outText( 'Range UI - v'..UI.Version, 1 ) +end \ No newline at end of file diff --git a/TargetImpactTracker-2.3.lua b/TargetImpactTracker-2.3.lua new file mode 100644 index 0000000..239374e --- /dev/null +++ b/TargetImpactTracker-2.3.lua @@ -0,0 +1,1310 @@ +TITS = {} +TITS.Version = '2.3.01' + + +--################################################################################################################################################### +-- ############# DCS - Target Impact Tracker Script ################# +-- ############# by Draken35 ################# +--################################################################################################################################################### +--[[ Requirements: + >>> MIST 4.5.107 or above + +--]] +----------------------------------------------------------------------------------------------------------------------------------------------------- +--[[ Version history + 2.0.00 03/04/2022 Development starts + 2.0.01 05/03/2022 First release + 2.0.02 05/04/2022 Added uncorrected impact position and warhead category to weaponsFire + 2.1.00 05/10/2022 - Fired weapons grouping improvements + - Results API + - Added check on getAmmo to prevent ED bug ( getAmmo returns only 1 shell type for combat mix - A10C) + from crashing the script. + 2.1.01 05/11/2022 - Fixed "no effect" message when target is killed by the weapon + (dead objects are not triggering hit event at this time) + 2.1.02 05/12/2022 - Fixed missing shell count when using "combat mix" type of loads + 2.2.00 05/15/2022 - Shell Burst recording + - new fired shell counting method / remove need for pass end + - added capture of shells display name and use it in the API if available + 2.2.01 05/16/2022 - Added release data for last shell burst in pass + 2.2.02 05/18/2022 - Added TargetImapct table to getGroupData + 2.3.00 07/27/2022 - Added roll-in position tracking + - added polygonal zone targets types + 2.3.01 09/10/2022 - Fixed issue with recording strafing hits with unlimited ammo (star shooting event is not longer populating weapon name - ED issue) +--]] +----------------------------------------------------------------------------------------------------------------------------------------------------- +--[[ Results API + + count,{} = TITS.getWeponsFired(_unitName) + returns simple table of weapons IDs fired in the pass + + + + + + + + + TITS.getWeponsInFlight() + returns simple table of weapons IDs fired in the pass + + TITS.getWeaponData(WeaponID) + returns a complex object with the following data for the specific WeaponID: + weaponType + , releasePitch + , releaseYaw + , releaseRoll + , releaseHeading + , releaseSpeed + , releasePosition + , timeStamp + , inFlight + , unImpactPos -- uncorrected position + , avgImpactPos -- average corrected position + , group + , targetsHitList -- List of target hit by the weapon + , closestTargetHit -- complex object with: + { + target + , distance (from corrected position to target) + , bearing (true degrees, from corrected position to target) + , slantRange (from release position to target) + + } + , TargetsHit -- complex object list with: + { + target + , distance (from corrected position to target) + , bearing (true degrees, from corrected position to target) + , slantRange (from release position to target) + + } + + TITS.getWeaponGroupsFired() + returns complex list of + { + groupId + , weaponType + , quantityFired + } + + TITS.getWeaponGroupsFiredData() + returns complex list with + { + groupId + , weaponType + , quantityFired + , avgReleasePitch + , avgReleaseYaw + , avgReleaseRoll + , avgReleaseHeading + , avgReleaseSpeed + , avgReleasePosition + , groupPosition -- group center + , weaponList -- list of id of weapons in group + , targetsHitList -- List of target hit by the weapon + , closestTargetHit -- complex object with: + { + target + , distance (from group position to target) + , bearing (true degrees, from group position to target) + , slantRange (from avg release position to target) + + } + , TargetsHit -- complex object list with: + { + target + , distance (from group position to target) + , bearing (true degrees, from group position to target) + , slantRange (from avg release position to target) + + } + } + +--]] + +--[[ Pass Data return tables + Tables: + shellsFired -- initial count of rounds per shell type at pass Start, updated to rounds fired in the pass at pass End + shellsFired[ShellType] = { + init = Shell Count at pass start + , fired = Shells fired during the pass + } + + + shellHits -- per target per shell type + shellHits[Target][ShellType] = Shell Hits Count + + weaponsFired -- and their release params & designated target at the time, updated by shot event if TITS.onPass[_unitName] == true + weaponsFired[WeaponID] = { + weaponType + , releasePitch + , releaseYaw + , releaseRoll + , releaseHeading + , releaseSpeed + , releasePosition + , timeStamp + , inFlight + , impactPos -- uncorrected + , group + } + + weaponHits -- per target per weapon , updated by hit event if TITS.onPass[_unitName] == true + weaponHits[WeaponID][Target] = timeStamp + + + impactData -- per target per weapon + impactData[WeaponID][Target] = { + targetPosition + , impactPosition + , impactDistance + , timeStamp + } + + + targetHealth -- per target. Health at the start and end of the pass + targetHealth[Target] = { + start_health + , end_health + } + + +--]] +--[[ Target List table + TargetList[name] = { + name -- UNIQUE! Target name + , displayName -- display name, accepts duplicates, if empty _TargetName will be used + , type -- allowed values [ 'unit' | 'zone' | 'static' ] must be in lower case + , respawn -- Only for units. if true, unit can be respawned. Respwan will only happen when ALL the units in the group are destroyed + , impact -- use target to calculate distance to impact + , strafing -- count strafing hits on target + , bda -- reports splash damage from weapons + , des_zone -- designation zone + , des_wp -- Allow use of smoke designation for target + , des_laser -- Allow use of laser designation for target + , des_ir -- Allow use of IR designation for target + , des_nomark -- Allow use of target designation with no marks (provides coordinates) + , list_coor -- Allow list of target coordinates + , uiNum1 -- Numeric value for custom UI use + , uiNum2 -- Numeric value for custom UI use + , uiNum3 -- Numeric value for custom UI use + , uiBool1 -- Boolean value for custom UI use + , uiBool2 -- Boolean value for custom UI use + , uiBool3 -- Boolean value for custom UI use + , uiText1 -- Text value for custom UI use + , uiText2 -- Text value for custom UI use + , uiText3 -- Text value for custom UI use + + } + +--]] + + +--################################################################################################################################################### +-- ############# Configuration section ################# +--################################################################################################################################################### + TITS.TargetTrackingInterval = 1 -- in seconds. Time in between target checks + TITS.WeaponTrackingInterval = 0.001 -- in seconds. Time in between weapon tracking checks + TITS.MaxMissDistance = 1000 -- meters. Any impact over this distance from the closest target will be considered as Miss + TITS.MaxCorrectionDistance = 15 -- meters + TITS.weaponGroupInterval = 1.5 -- in seconds. Minimun time between weapons shots to be considered a new group + +--################################################################################################################################################# +-- ############# Initialization of globals ############### +--################################################################################################################################################# + TITS.TargetList = {} + TITS.PassData = {} + TITS.onPass = {} + TITS.lastOSclock = timer.getTime() + TITS.avgTrackTic = 0 + + +--################################################################################################################################################# +-- ############# Event Handler & weapon tracking ############### +--################################################################################################################################################# + TITS.coreEventHandler = {} + function TITS.coreEventHandler:onEvent(_eventDCS) + + if _eventDCS == nil or _eventDCS.initiator == nil then + return true + end + + local status, err = pcall(function(_event) + + if (_event.id == world.event.S_EVENT_SHOT) and _event.initiator:getPlayerName() then -- id == 1 Shot + + TITS.ShotEvent(_event) + + elseif _event.id == world.event.S_EVENT_HIT and _event.initiator:getPlayerName() then -- id == 2 Hit + + TITS.HitEvent(_event) + + elseif _event.id == world.event.S_EVENT_SHOOTING_START and _event.initiator:getPlayerName() then -- id == 2 Hit + + TITS.ShootingStart(_event) + + elseif _event.id == world.event.S_EVENT_SHOOTING_END and _event.initiator:getPlayerName() then -- id == 2 Hit + + TITS.ShootingEnd(_event) + + end --if (_event.id == world.event.S_EVENT_SHOT) and _event.initiator:getPlayerName() then + -- ### End event processing + + return true + end, _eventDCS) -- local status, err = pcall(function(_event) + + if (not status) then + local msg = string.format("Target Impact Tracker (v%s): Error while handling event %s",TITS.Version, err) + env.error(msg,false) + end + end + --============================================================================================================================================= + function TITS.ShotEvent(_event) + local _weapon = _event.weapon + local _unitName = _event.initiator:getName() + local data = TITS.PassData[_unitName] + -- recond weapon fired and release data + + if TITS.onPass[_unitName] then + + if -- check for new group + (data.lastWPNfired ~= _weapon:getTypeName()) or + (timer.getTime() - data.lastWPNts) > TITS.weaponGroupInterval + then + data.lastWPNfired = _weapon:getTypeName() + data.lastWPNts = timer.getTime() + data.groupId = data.groupId + 1 + data.groups[data.groupId] = {} + end + + + table.insert(data.groups[data.groupId],_weapon:getName()) + + + local r = { + weaponType = _weapon:getTypeName() + , releasePitch = mist.utils.toDegree(mist.getClimbAngle(_event.initiator)) + , releaseYaw = mist.utils.toDegree(mist.getYaw(_event.initiator)) + , releaseRoll = mist.utils.toDegree(mist.getRoll(_event.initiator)) + , releaseHeading = mist.utils.toDegree(mist.getHeading(_event.initiator)) + , releaseSpeed = mist.vec.mag(_event.initiator:getVelocity()) + , releasePosition = _event.initiator:getPosition().p + , timeStamp = timer.getTime() + , inFlight = true + , impactPos = {} + , groupId = data.groupId + } + + data.weaponsFired[_weapon:getName()] = r + + -- start tracking weapon + local params = {} + params.unitName = _event.initiator:getName() + params.launchPos = _event.initiator:getPosition().p + params.weapon = _weapon + params.weaponID = _weapon:getName() + params.weaponLastPoint = _weapon:getPoint() + params.weaponLastVelocity = _weapon:getVelocity() + + TITS.lastOSclock = timer.getTime() + TITS.avgTrackTic = 0 + timer.scheduleFunction(TITS.TrackWeapon, params, timer.getTime() + TITS.WeaponTrackingInterval ) + end --if TITS.onPass[_unitName] then + end -- function + --============================================================================================================================================= + function TITS.HitEvent(_event) + local unitName = _event.initiator:getName() + local eventTarget = _event.target + local shellType = _event.weapon:getTypeName() + local isStrafingWeapon = string.find(_event.weapon:getTypeName():lower(), "weapons.shell") + local targetName = '' + local data = TITS.PassData[unitName] + + if eventTarget then + targetName = eventTarget:getName() + end + + if targetName ~= '' and TITS.onPass[unitName] then + if isStrafingWeapon then + local ts = data.shellHits[targetName] + if not ts then + target = TITS.TargetList[targetName] + if target then + if target.strafing then + data.shellHits[targetName] = {} + ts = data.shellHits[targetName] + end + end + end + if ts then + if ts[shellType] then + data.shellHits[targetName][shellType] = data.shellHits[targetName][shellType] + 1 + else + data.shellHits[targetName][shellType] = 1 + end + end + local desc = _event.weapon:getDesc() + if desc then + data.shellDisplayName[shellType] = desc.displayName + end + else -- it goes big boom + local target = TITS.getTargetObj(targetName) + if target then + -- record the target that got splashed by the weapon + local t = TITS.TargetList[targetName] + if t.bda then + if not data.weaponHits[_event.weapon:getName()] then + data.weaponHits[_event.weapon:getName()] = {} + end + + data.weaponHits[_event.weapon:getName()][targetName] = timer.getTime() + end -- if t.bda then + end -- if target then + end -- if isStrafingWeapon then + end -- if targetName ~= '' and TITS.onPass[unitName] then + + return true + end -- function + --============================================================================================================================================= + function TITS.ShootingStart(_event) + local _unitName = _event.initiator:getName() + local unit = Unit.getByName(_unitName) + local data = TITS.PassData[_unitName] + local ammoTable = unit:getAmmo() + + for _,v in pairs (ammoTable) do + local t = v.desc + if string.find(t.typeName:lower(), "weapons.shell") then + + if _event.weapon_name then + if data.shellsFired[_event.weapon_name] then + data.shellsFired[_event.weapon_name].init = v.count + else + data.shellsFired[_event.weapon_name] = { + init = v.count + , fired = 0 + } + end + else + if data.shellsFired[t.typeName] then + data.shellsFired[t.typeName].init = v.count + else + data.shellsFired[t.typeName] = { + init = v.count + , fired = 0 + } + end + end -- if _event.weapon_name then + end--if string.find(t.typeName:lower(), "weapons.shell") then + end -- for _,v in pairs (ammoTable) do + + + data.LBreleasePitch = mist.utils.toDegree(mist.getClimbAngle(_event.initiator)) + data.LBreleaseYaw = mist.utils.toDegree(mist.getYaw(_event.initiator)) + data.LBreleaseRoll = mist.utils.toDegree(mist.getRoll(_event.initiator)) + data.LBreleaseHeading = mist.utils.toDegree(mist.getHeading(_event.initiator)) + data.LBreleaseSpeed = mist.vec.mag(_event.initiator:getVelocity()) + data.LBreleasePosition = _event.initiator:getPosition().p + + + end -- function + --============================================================================================================================================= + function TITS.ShootingEnd(_event) + local _unitName = _event.initiator:getName() + local unit = Unit.getByName(_unitName) + local data = TITS.PassData[_unitName] + local ammoTable = unit:getAmmo() + local shellFounds = false + + data.lastShellFired = timer.getTime() + + if ammoTable then + for _,v in pairs (ammoTable) do + local t = v.desc + if string.find(t.typeName:lower(), "weapons.shell") then + shellFounds = true + d = data.shellsFired[t.typeName] + data.shellDisplayName[t.typeName] = t.displayName + if d then + d.fired = d.init - v.count + end + end + end -- for _,v in pairs (ammoTable) do + end + + if not ammoTable or not shellFounds then + for _,d in pairs(data.shellsFired) do + d.fired = d.init + end + end + + end -- function + --============================================================================================================================================= + function TITS.TrackWeapon(params,time) + local OSclock = timer.getTime() + TITS.avgTrackTic = (TITS.avgTrackTic + (OSclock-TITS.lastOSclock))/2 + TITS.lastOSclock = OSclock + + local _status,_weaponData, _weaponVel = pcall(function() + return {params.weapon:getPoint(),params.weapon:getVelocity()} + end) + if _status then + params.weaponLastPoint = _weaponData[1] + params.weaponLastVelocity = _weaponData[2] + return timer.getTime() + TITS.WeaponTrackingInterval -- keep tracking + else -- if _status then (weapon not longer exists) + local data = TITS.PassData[params.unitName] + data.lastImpact = timer.getTime() + + data.weaponsFired[params.weaponID].inFlight = false + for targetName,targetData in pairs (TITS.TargetList) do + if targetData.detected and targetData.impact then + if (not targetData.dead) or ((targetData.dead) and ((targetData.dead >= data.timestamp))) then + local targetPos = TITS.getTargetPos(targetName) + local targetVel = TITS.getTargetVel(targetName) + local ImpactPos = params.weaponLastPoint + data.weaponsFired[params.weaponID].impactPos = ImpactPos + local weaponVel = params.weaponLastVelocity + local impactDistance = mist.utils.get3DDist(ImpactPos,targetPos) + if impactDistance <= TITS.MaxMissDistance then + + ImpactPos = mist.vec.add(ImpactPos ,mist.vec.scalar_mult(weaponVel, TITS.WeaponTrackingInterval )) + + if impactDistance <= TITS.MaxCorrectionDistance then -- weapon hit the target's collision box, correct the impact + if params.weaponLastVelocity.y ~= 0 then + -- correct Weapon and Target position + local deltaY = math.abs(ImpactPos.y - targetPos.y) + local flightTime = math.abs(deltaY / weaponVel.y) + ImpactPos = mist.vec.add(ImpactPos ,mist.vec.scalar_mult(weaponVel , flightTime )) + targetPos = mist.vec.add(targetPos ,mist.vec.scalar_mult(targetVel, flightTime )) + end -- params.weaponLastVelocity.y ~= 0 + impactDistance = mist.utils.get3DDist(ImpactPos,targetPos) + + end -- if impactDistance <= TITS.MaxCorrectionDistance then + + -- check if the target is dead, and if so, record a hit + if TITS.getTargetLife(targetName) == 0 then + + if not data.weaponHits[params.weaponID] then + data.weaponHits[params.weaponID] = {} + end + data.weaponHits[params.weaponID][targetName] = timer.getTime() + end + --record the impact + if not data.impactData[params.weaponID] then + data.impactData[params.weaponID] = {} + end + + data.impactData[params.weaponID][targetName] = { + targetPos = targetPos + , impactPos = ImpactPos + , impactDistance = impactDistance + , timestamp = timer.getTime() + } + -- see if target is a zone and the impact is with the radius + if targetData.type == 'zone' and targetData.bda then + local zone = trigger.misc.getZone(targetName) + if impactDistance <= zone.radius then + + if not data.weaponHits[params.weaponID] then + data.weaponHits[params.weaponID] = {} + end + data.weaponHits[params.weaponID][targetName] = timer.getTime() + end + end -- if targetData.type = 'zone' then + if targetData.type == 'poly' and targetData.bda then + if mist.pointInPolygon(ImpactPos,TITS.getTargetObj(targetName), 1000) then + + if not data.weaponHits[params.weaponID] then + data.weaponHits[params.weaponID] = {} + end + data.weaponHits[params.weaponID][targetName] = timer.getTime() + end + end -- if targetData.type = 'poly' then + + end -- if impactDistance <= TITS.MaxMissDistance then + end --if (not targetData.dead) or ((targetData.dead) and ((targetData.dead >= data.timestamp))) then + end -- if targetData.detected and targetData.impact then + end -- for targetName,targetData in pairs (TITS.TargetList) do + end -- if _status then + + end -- function +-- +--################################################################################################################################################# +-- ############# target functions ############### +--################################################################################################################################################# +-- + function TITS.AddTarget( + _TargetName -- UNIQUE! Target name + , _DisplayName -- display name, accepts duplicates, if empty _TargetName will be used + , _Type -- [ unit | zone | static ] must be in lower case + , _Respawn -- Only for units. if true, unit can be respawned. Respwan will only happen when ALL the units in the group are destroyed + , _TrackImpact -- use target to calculate distance to impact + , _TrackStrafing -- count strafing hits on target + , _ReportHits -- reports splash damage from weapons + , _desZone -- designation zone + , _AllowSmokeDesignation -- Allow use of smoke designation for target + , _AllowLaserDesignation -- Allow use of laser designation for target + , _AllowIRDesignation -- Allow use of IR designation for target + , _AllowNoMarkDesignation -- Allow use of target designation with no marks (provides coordinates) + , _AllowListCoordinates -- Allow list of target coordinates + , _uiNum1 -- Numeric value for custom UI use + , _uiNum2 -- Numeric value for custom UI use + , _uiNum3 -- Numeric value for custom UI use + , _uiBool1 -- Boolean value for custom UI use + , _uiBool2 -- Boolean value for custom UI use + , _uiBool3 -- Boolean value for custom UI use + , _uiText1 -- Text value for custom UI use + , _uiText2 -- Text value for custom UI use + , _uiText3 -- Text value for custom UI use + ) + + local dn = _DisplayName + if dn == '' then + dn = _TargetName + end + + local life0 = 100 + if _Type == 'static' then + local obj = StaticObject.getByName(_TargetName) + if obj then + life0 = obj:getLife() + end + end + + local gpn = '' + if _Type == 'unit' then + local u = Unit.getByName(_TargetName) + gpn = u:getGroup():getName() + end + + + local v = { + name = _TargetName, + displayName = dn, + type = _Type, + respawn = _Respawn, + impact = _TrackImpact, + strafing = _TrackStrafing, + bda = _ReportHits, + des_zone = _desZone, + des_wp = _AllowSmokeDesignation, + des_laser = _AllowLaserDesignation, + des_ir = _AllowIRDesignation, + des_nomark = _AllowNoMarkDesignation, + list_coor = _AllowListCoordinates, + uiNum1 = _uiNum1, + uiNum2 = _uiNum2, + uiNum3 = _uiNum3, + uiBool1 = _uiBool1, + uiBool2 = _uiBool2, + uiBool3 = _uiBool3, + uiText1 = _uiText1, + uiText2 = _uiText2, + uiText3 = _uiText3, + + + -- internal use attributes + detected = nil,-- Detected = target detected as alive + dead = nil,-- Dead = dead detection time + life0 = life0 , -- initial life for statics & zones + groupName = gpn, + lastPos = {}, + lastVel = {} + } + TITS.TargetList[_TargetName] = v + + end -- function + --============================================================================================================================================= + function TITS.respawnTarget(_TargetName, _override) + local tgt = TITS.TargetList[_TargetName] + + if tgt then + if tgt.type == 'unit' and (tgt.respawn or _override) and tgt.dead then + if not Group.getByName(tgt.groupName) then + mist.respawnGroup(tgt.groupName, true) + tgt.dead = nil + tgt.detected = timer.getTime() + end + end + end + end -- function + --============================================================================================================================================= + function TITS.getTargetObj(_TargetName) + local tgt = TITS.TargetList[_TargetName] + local obj = nil + + if tgt.type == 'unit' then + obj = Unit.getByName(_TargetName) + elseif tgt.type == 'static' then + obj = StaticObject.getByName(_TargetName) + elseif tgt.type == 'zone' then + obj = trigger.misc.getZone(_TargetName) + elseif tgt.type == 'poly' then + local u = Unit.getByName(_TargetName) + local g = u:getGroup() + obj = mist.getGroupPoints(g:getName()) + end + return obj + end -- function + --============================================================================================================================================= + function TITS.getTargetLife(_TargetName) + local tgt = TITS.TargetList[_TargetName] + local obj = TITS.getTargetObj(_TargetName) + local life = 0 + + if obj then + if tgt.type == 'unit' or tgt.type == 'static' then + life = obj:getLife() + elseif tgt.type == 'zone' then + life = 100 + elseif tgt.type == 'poly' then + life = 100 + end + end + return life + end -- function + --============================================================================================================================================= + function TITS.getMaxTargetLife(_TargetName) + local tgt = TITS.TargetList[_TargetName] + local obj = TITS.getTargetObj(_TargetName) + local life = 0 + + if obj then + if tgt.type == 'unit' then + life = obj:getLife0() + elseif tgt.type == 'static' or tgt.type == 'zone' then + life = tgt.life0 + end + end + return life + end -- function + --============================================================================================================================================= + function TITS.targetExists(_TargetName) + local tgt = TITS.TargetList[_TargetName] + local obj = TITS.getTargetObj(_TargetName) + local exists = false + + if obj then + if tgt.type == 'unit' or tgt.type == 'static' then + exists = obj:isExist() + elseif tgt.type == 'zone' then + exists = true + elseif tgt.type == 'poly' then + exists = true + end + end + return exists + end -- function + --============================================================================================================================================= + function TITS.getTargetPos(_TargetName) + local tgt = TITS.TargetList[_TargetName] + local obj = TITS.getTargetObj(_TargetName) + local Pos = nil + + if obj then + if tgt.type == 'unit' or tgt.type == 'static' then + Pos = obj:getPoint() + elseif tgt.type == 'zone' then + Pos = mist.utils.makeVec3GL(obj.point) + elseif tgt.type == 'poly' then + local n = 0 + Pos = {x=0,y=0,z=0} + for _,p in pairs(obj) do + n = n + 1 + Pos = mist.vec.add(Pos, mist.utils.makeVec3GL(p)) + end + if n > 0 then + Pos = mist.vec.scalar_mult(Pos , 1 / n) + end + + end + else -- if obj then + -- target is dead + Pos = tgt.lastPos + end -- if obj then + return Pos + + end -- function + --============================================================================================================================================= + function TITS.getTargetVel(_TargetName) + local tgt = TITS.TargetList[_TargetName] + local obj = TITS.getTargetObj(_TargetName) + local Vel = nil + + if obj then + if tgt.type == 'unit' or tgt.type == 'static' then + Vel = obj:getVelocity() + elseif tgt.type == 'zone' then + Vel = { x=0, y=0, z=0} + elseif tgt.type == 'poly' then + Vel = { x=0, y=0, z=0} + end + else -- if obj then + -- target is dead + Vel = tgt.lastVel + end -- if obj then + return Vel + end -- function + --============================================================================================================================================= + function TITS.trackTargets(_params,time) + for k,v in pairs(TITS.TargetList) do + if v.type ~='zone' and v.type ~='poly' then + local o = TITS.getTargetObj(k) + local life = 0 + if o then + life = TITS.getTargetLife(k) + end + if life > 0 then + -- target is alive + if v.detected == nil then + -- target detected + v.detected = timer.getTime() + end -- if v.Detected == nil then + -- if v.lastPos == nil or v.lastVel == nil or v.canMove then + if v.type == 'unit' then + v.lastPos = TITS.getTargetPos(k) + v.lastVel = TITS.getTargetVel(k) + end --if v.type == 'unit' then + -- end -- if v.lastPos == nil or v.lastVel == nil or v.canMove then + else -- if o then + -- target is dead + if v.detected and v.dead == nil then + v.dead = timer.getTime( ) + end -- if v.Detected and v.Dead == nil then + end -- if o then + else + if v.detected == nil then + -- target detected + v.detected = timer.getTime() + end -- if v.Detected == nil then + end -- if v.type ~='zone' then + end -- for k,v in pairs(TITS.TargetList) do + + return timer.getTime() + TITS.TargetTrackingInterval -- keep tracking + end -- function +-- +--################################################################################################################################################# +-- ############# Pass functions ############### +--################################################################################################################################################# +-- + function TITS.passInit(_unitName) + local unit = Unit.getByName(_unitName) + TITS.PassData[_unitName] = {} + + local r = { + shellsFired = {} -- initial count of rounds per shell type at pass Start, updated to rounds fired in the pass at pass End + , shellHits = {} -- per target per shell type + , shellDisplayName = {} -- display name per shell type + , weaponsFired = {} -- and their release params & designated target at the time, updated by shot event if TITS.onPass[_unitName] == true + , weaponHits = {} -- per target per weapon , updated by hit event if TITS.onPass[_unitName] == true + , impactData = {} -- per target per weapon & slant range from launch position + , targetHealth = {} -- per target at this time + , timestamp = timer.getTime() -- Pass start time + , groupId = 0 -- fired weapons group counter + , lastWPNfired = '' -- last weapon TYPE fired + , lastWPNts = 0 -- Last weapon fired time stamp + , groups = {} -- groups indexing + , lastShellFired = timer.getTime() -- to control Auto pass end in the UI + , lastImpact = timer.getTime() -- to control Auto pass end in the UI + , LBreleasePitch = 0 + , LBreleaseYaw = 0 + , LBreleaseRoll = 0 + , LBreleaseHeading = 0 + , LBreleaseSpeed = {} + , LBreleasePosition = {} + , rollInPosition = unit:getPosition().p + + + } + + + + -- init shellsFired + --[[ + local unit = Unit.getByName(_unitName) + local ammoTable = unit:getAmmo() + for _,v in pairs (ammoTable) do + local t = v.desc + if string.find(t.typeName:lower(), "weapons.shell") then + local c = { init = v.count, fired = 0 } + r.shellsFired[t.typeName] = c + end + end -- for _,v in pairs (ammoTable) do + ]]-- + -- init targetHealth & shellHits + for k,targetData in pairs (TITS.TargetList) do + local th = TITS.getTargetLife(k) + local h = { + start_health = th + , end_health = th + } + r.targetHealth[k] = h + + --[[ shellHits + if targetData.strafing and targetData.type ~= 'zone' then --and TITS.getTargetLife(k) > 0 then + r.shellHits[k] = {} + for v,_ in pairs(r.shellsFired) do + r.shellHits[k][v] = 0 + end -- for v,_ in pairs(r.shellsFired) do + end -- if k.strafing then + --]] + end -- for k,_ in pairs (TITS.TargetList) do + + TITS.PassData[_unitName] = r + end -- function + --============================================================================================================================================= + function TITS.passStart(_unitName) + TITS.onPass[_unitName] = true + end -- function + --============================================================================================================================================= + function TITS.passEnd(_unitName) + local weponsInFlight = false + local d = TITS.PassData[_unitName] + + if d.weaponsFired then + for _,y in pairs(d.weaponsFired) do + weponsInFlight = weponsInFlight or y.inFlight + end -- for _,y in pairs(d.weaponsFired) do + end + if not weponsInFlight then + TITS.onPass[_unitName] = false + -- update PassData shellsFired + local unit = Unit.getByName(_unitName) + local ammoTable = unit:getAmmo() + local r = d.shellsFired + local ShellsFounds = false + if ammoTable then + for _,v in pairs (ammoTable) do + local t = v.desc + if string.find(t.typeName:lower(), "weapons.shell") then + ShellsFounds = true + local c = r[t.typeName] + if c then + c.fired = c.init - v.count -- rounds fired + else + if r[1] then + r[1].fired = r[1].init - v.count + else + r[t.typeName] = {fired = 0} + end + local msg = string.format("Target Impact Tracker (v%s): %s ammo type load error",TITS.Version, t.typeName) + env.error(msg,false) + end + end + end -- for _,v in pairs (ammoTable) do + end + if not ammoTable or not ShellsFounds then + -- all anmo was used + for _,v in pairs(r) do + v.fired = v.init + end + end + + -- update target health + for k,targetData in pairs (TITS.TargetList) do + local th = TITS.getTargetLife(k) + local h = d.targetHealth[k] + h.end_health = th + d.targetHealth[k] = h + end -- for k,targetData in pairs (TITS.TargetList) do + end + return not weponsInFlight + end -- function + --============================================================================================================================================= + function TITS.passGetData(_unitName) + local r = TITS.PassData + if r ~= {} then + r = TITS.PassData[_unitName] + end + return r + end -- function + --============================================================================================================================================= + function TITS.weaponsInFlight(_unitName) + local weponsInFlight = false + local d = TITS.PassData[_unitName] + + if d.weaponsFired then + for _,y in pairs(d.weaponsFired) do + weponsInFlight = weponsInFlight or y.inFlight + end -- for _,y in pairs(d.weaponsFired) do + end + return weponsInFlight + end -- function +-- +--################################################################################################################################################# +-- ############# Results API ############### +--################################################################################################################################################# +-- + function TITS.getWeponsFired(_unitName) + local wftable = {} + local count = 0 + + + local data = TITS.passGetData(_unitName) + if TITS.rowCount(data) > 0 then + local wf = data.weaponsFired + local r = {} + + if TITS.rowCount(wf) > 0 then + for wID,_ in pairs(wf) do + count = count + 1 + wftable[wID] = wID + end + end + + end --if data ~= {} then + r = {count = count, list = wftable} + return r + + end -- function +--============================================================================================================================================= + function TITS.getWeaponData(_unitName, _weaponID) + --[[ + weaponType + , releasePitch + , releaseYaw + , releaseRoll + , releaseHeading + , releaseSpeed + , releasePosition + , inFlight + , impactPos -- uncorrected + , group + , impacts[target] + target + , targetPos + , hit + , distance + , impactPos -- corrected + , closestTarget + , miss -- no impacts close to targets + , avgImpactPos -- corrected + , targetsHit + , rollInDistance -- to closest target + , rollInAltitudeAT -- Altitude above closest target + --]] + local data = TITS.passGetData(_unitName) + local wData = data.weaponsFired[_weaponID] + local wHits= data.weaponHits[_weaponID] + local r = {} + if wData then + r = { + weaponType = wData.weaponType + , releasePitch = wData.releasePitch + , releaseYaw = wData.releaseYaw + , releaseRoll = wData.releaseRoll + , releaseHeading = wData.releaseHeading + , releaseSpeed = wData.releaseSpeed + , releasePosition = wData.releasePosition + , inFlight = wData.inFlight + , impactPos = wData.impactPos -- uncorrected + , group = wData.group + , impacts = {} + , closestTarget = '' + , miss = true + , avgImpactPos = {x=0,y=0,z=0} + , targetsHit = {} + , rollInDistance = 0 + , rollInAltitudeAT = 0 + } + -- get impacts and find closest target + local impactData = data.impactData[_weaponID] + local minDist = TITS.MaxMissDistance * 2 + + if impactData then + r.miss = false + local impactCount = 0 + for target, irow in pairs(impactData) do + local hit = false + impactCount = impactCount + 1 + r.avgImpactPos = mist.vec.add(r.avgImpactPos , irow.impactPos) + if irow.impactDistance <= minDist then + minDist = irow.impactDistance + r.closestTarget = target + end + if wHits then + if wHits[target] then + hit = true + table.insert(r.targetsHit,target) + end + end + r.impacts[target] = { + target = target + , targetPos = irow.targetPos + , hit = hit + , distance = irow.impactDistance + , impactPos = irow.impactPos + } + + end -- for target, irow in pairs(impactData) + r.avgImpactPos = mist.vec.scalar_mult(r.avgImpactPos , 1/impactCount) + if r.closestTarget ~= '' then + local tp = TITS.getTargetPos(r.closestTarget) + r.rollInDistance = mist.utils.get2DDist(data.rollInPosition,tp) + r.rollInAltitudeAT = data.rollInPosition.y - tp.y + end + end -- if impactData then + + end -- if wData then + + return r + end -- function +--============================================================================================================================================= + function TITS.getShellsFired(_unitName) + local shtable = {} + local total = 0 + local data = TITS.passGetData(_unitName) + if TITS.rowCount(data) > 0 then + local sf = data.shellsFired + local r = {} + + if TITS.rowCount(sf) > 0 then + for shellType,s in pairs(sf) do + if s.fired then + total = total + s.fired + shtable[shellType] = s.fired + end + end + end + + end --if data ~= {} then + r = {total = total, list = shtable, shootHeading = data.LBreleaseHeading} + return r + + end -- function +--============================================================================================================================================= + function TITS.getShellHits(_unitName) + local shtable = {} + local targets = {} + local r = {} + local total = 0 + local data = TITS.passGetData(_unitName) + if TITS.rowCount(data) > 0 then + local sh = data.shellHits + + if TITS.rowCount(sh) > 0 then + for target,sData in pairs(sh) do + if not TITS.inTable(target,targets) then + table.insert(targets,target) + end + for shellType,count in pairs(sData) do + if count > 0 then + total = total + count + if data.shellDisplayName[shellType] then + table.insert(shtable, {target = target, shellType = data.shellDisplayName[shellType], hits = count}) + else + shellType = shellType:gsub("weapons.shells.", "") + table.insert(shtable, {target = target, shellType = shellType, hits = count}) + end + end + end -- for shellType,count in pairs(sData) do + end -- for target,sData in pairs(sh) do + end + end --if data ~= {} then + r = {total = total, targets = targets, list = shtable} + return r + + end -- function +--============================================================================================================================================= + function TITS.getGroups(_unitName) + local grptable = {} + local count = 0 + local r = {} + local data = TITS.passGetData(_unitName) + + if TITS.rowCount(data) > 0 then + local groups = data.groups + + + if TITS.rowCount(groups) > 0 then + for gID,_ in pairs(groups) do + count = count + 1 + grptable[gID] = gID + end + end + + end --if data ~= {} then + r = {count = count, list = grptable} + return r + + end --function +--============================================================================================================================================= + function TITS.getGroupData(_unitName, _groupID) + --[[ + GroupID + , weaponType + , releasePitch + , releaseYaw + , releaseRoll + , releaseHeading + , releaseSpeed + , releasePosition + , impactPos -- Average from weapon impacts + , weapons {id list} + , weaponPos {avgImpactPos list} + , closestTarget + , closestTargetPos + , closestTargetHit + , miss -- no impacts close to targets + , avgImpactPos -- corrected + , targetsHit + , size (max dist ) + , TargetImpacts + + --]] + local r = {} + local data = TITS.passGetData(_unitName) + + if TITS.rowCount(data) > 0 then + local weapons = data.groups[_groupID] + local impactCount = 0 + local weaponCount = 0 + local targetData = {} + + r = { + groupId = _groupID + , weaponType = '' + , weaponCount = 0 + , releasePitch = 0 + , releaseYaw = 0 + , releaseRoll = 0 + , releaseHeading = 0 + , releaseSpeed = 0 + , releasePosition = {x=0,y=0,z=0} + , impactPos = {x=0,y=0,z=0} -- center + , weapons = weapons + , weaponsPos = {} + , closestTarget = '' + , closestTargetPos = {} + , closestTargetHit = false + , closestTargetDist = 2 * TITS.MaxMissDistance + , closestTargetHitCount = 0 + , miss = true + , avgImpactPos = {x=0,y=0,z=0} + , targetsHit = {} + , size = 0 + , TargetImpacts ={} + , rollInDistance = 0 + , rollInAltitudeAT = 0 + + } + + for _, weaponID in pairs(weapons) do + local wd = TITS.getWeaponData(_unitName, weaponID) + + weaponCount = weaponCount + 1 + r.weaponType = wd.weaponType + r.releasePitch = r.releasePitch + wd.releasePitch + r.releaseYaw = r.releaseYaw + wd.releaseYaw + r.releaseRoll = r.releaseRoll + wd.releaseRoll + r.releaseHeading = r.releaseHeading + wd.releaseHeading + r.releaseSpeed = r.releaseSpeed + wd.releaseSpeed + r.releasePosition = mist.vec.add(r.releasePosition,wd.releasePosition) + r.impactPos = mist.vec.add(r.impactPos,wd.avgImpactPos) + table.insert(r.weaponsPos,wd.avgImpactPos) + + if not wd.miss then + r.miss = false + for _,impact in pairs(wd.impacts) do + table.insert(targetData,impact) + if impact.hit and not TITS.inTable(impact.target,r.targetsHit) then + table.insert(r.targetsHit,impact.target) + end + end --for _,impact in pairs(wd.impacts) do + end -- if not wd.miss then + + end -- for _,weaponID in pairs(weapons) do + r.TargetImpacts = targetData + -- averages + r.weaponCount = weaponCount + if weaponCount > 0 then + r.releasePitch = r.releasePitch / weaponCount + r.releaseYaw = r.releaseYaw / weaponCount + r.releaseRoll = r.releaseRoll / weaponCount + r.releaseHeading = r.releaseHeading / weaponCount + r.releaseSpeed = r.releaseSpeed / weaponCount + r.releasePosition = mist.vec.scalar_mult(r.releasePosition , 1/weaponCount) + r.impactPos = mist.vec.scalar_mult(r.impactPos , 1/weaponCount) + if not r.miss then + -- get closestTarget + for _,t in pairs(targetData) do + local d = mist.utils.get3DDist(r.impactPos, t.targetPos) + if d <= r.closestTargetDist then + r.closestTargetDist = d + r.closestTarget = t.target + r.closestTargetPos = t.targetPos + r.closestTargetHit = t.hit + end + end -- for _,t in pairs(targetData) do + + if r.closestTarget ~= '' then + local tp = TITS.getTargetPos(r.closestTarget) + r.rollInDistance = mist.utils.get2DDist(data.rollInPosition,tp) + r.rollInAltitudeAT = data.rollInPosition.y - tp.y + end + + end -- if not r.miss then + --get size + for i = 1, weaponCount-1, 1 do + for j = i+1, weaponCount, 1 do + local d = mist.utils.get3DDist(r.weaponsPos[i],r.weaponsPos[j]) + if d > r.size then + r.size = d + end + end --for j = i+1, weaponCount, 1 do + end -- for i = 1, weaponCount, 1 do + + for _, weaponID in pairs(weapons) do + local wd = TITS.getWeaponData(_unitName, weaponID) + if TITS.inTable(r.closestTarget,wd.targetsHit) then + r.closestTargetHitCount = r.closestTargetHitCount + 1 + end + end --for _, weaponID in pairs(weapons) do + + end -- if weaponCount > 0 then + + + end --if data ~= {} then + + return r + + end --function +-- +--################################################################################################################################################# +-- ############# utils ############### +--################################################################################################################################################# +-- + function TITS.rowCount(t) + local r = 0 + if t then + for _,_ in pairs(t) do + r = r + 1 + end + end + return r + end -- function +--============================================================================================================================================= + function TITS.inTable(v,t) + local r = true + for _,j in pairs(t) do + if j == v then + return true + end + end + return false + + end -- function +-- +--################################################################################################################################################# +-- ############# Main body ############### +--################################################################################################################################################# +-- +do + -- load event handler + world.addEventHandler(TITS.coreEventHandler) + -- start tracking the targets + timer.scheduleFunction(TITS.trackTargets,{},timer.getTime() + TITS.TargetTrackingInterval) + + trigger.action.outText( 'Target Impact Tracker Script- v'..TITS.Version, 1 ) +end \ No newline at end of file diff --git a/TargetImpactTracker.lua b/TargetImpactTracker.lua new file mode 100644 index 0000000..d548a30 --- /dev/null +++ b/TargetImpactTracker.lua @@ -0,0 +1,662 @@ +--[[ + + DCS - Target Impact Tracker Script - Range Control + by Draken35 + + Requires MIST 4.4.90 or above + + Version history + 1.00 10/04/2021 Initial release / Development start + + + +--]] + +TITS = {} +trigger.action.outText( 'Target Impact Tracker Script- V1.00', 2 ) + + +--############################################################################################################################################ +-- ############# Configuration section ############# +--############################################################################################################################################ +TITS.MaxMissDistance = 1000 -- Any impact over this distance from the closest target will be considered as Miss + +TITS.TargetList = {} -- These are the targets that the script handles + -- for the unit, use the UNIT name, not the group. + -- Only units can be strafed and have a bda (hit) + -- target names need to be UNIQUE. It is also case sensitive. + -- impact = true will consider the target when calculating the closest target to the weapon impact. If false it will not be considered. + -- strafing = true will allow the target to count strafing hits. If false, it will not + -- bda = true will make the script check if the target was in the weapon blast radio and was "hit" by the blast. If false, it will not + -- The existance or type of the targets IS NOT VALIDATED by the script to save some CPU cycles. Make sure the list match the mission + + table.insert(TITS.TargetList, { name = 'Circle A', type = 'unit', impact =true, strafing = true, bda = true }) + table.insert(TITS.TargetList, { name = 'Moving Target', type = 'unit', impact =true, strafing = true, bda = true }) + table.insert(TITS.TargetList, { name = 'Circle B', type = 'zone', impact =true, strafing = false, bda = false }) + table.insert(TITS.TargetList, { name = 'Blue Sealand', type = 'unit', impact =false, strafing = true, bda = true }) + table.insert(TITS.TargetList, { name = 'Brown Sealand', type = 'unit', impact =false, strafing = true, bda = true }) + table.insert(TITS.TargetList, { name = 'Green Sealand', type = 'unit', impact =false, strafing = true, bda = true }) + table.insert(TITS.TargetList, { name = 'Tan Sealand', type = 'unit', impact =false, strafing = true, bda = true }) + table.insert(TITS.TargetList, { name = 'Strafe pit', type = 'unit', impact =false, strafing = true, bda = false }) + + +--############################################################################################################################################ +-- ############# Sound Files ############# +--############################################################################################################################################ + TITS.messageBeep = '204521__redoper__roger-beep.ogg' -- leave empty for no sound + + + +--############################################################################################################################################ +-- ############# Code section ############# +--############################################################################################################################################ +-- Initialization of globals + +TITS.eventHandler = {} +TITS.menuAddedToGroup = {} +TITS.attackingUnits = {} +TITS.isStrafingAttack = {} +TITS.reportWeaponsDistance = {} -- report non-strafing hits +TITS.reportWeaponsHits = {} -- report non-strafing hits +TITS.strafingHitCounter = {} +TITS.weponsHit = {} +TITS.rootPath = {} +TITS.rollInPath = {} +TITS.offPath = {} +TITS.lastAbortMSG = {} +TITS.passCounter = {} +TITS.uom ={} -- 1 metric (kph), 2 imperial (knots) , 3 imperial (mph) +TITS.DisplayDBA = {} +TITS.DisplayReleaseData = {} + + +--############################################################################################################################################ +function TITS.eventHandler:onEvent(_eventDCS) + + if _eventDCS == nil or _eventDCS.initiator == nil then + return true + end + + local status, err = pcall(function(_event) + + + -- ### player entered unit _event.id == 15 + if _event.id == 15 then -- id == 15 Enter + + TITS.EnterUnitEvent(_event) + + elseif (_event.id == world.event.S_EVENT_SHOT) and _event.initiator:getPlayerName() then -- id == 1 Shot + + TITS.ShotEvent(_event) + + elseif _event.id == world.event.S_EVENT_HIT and _event.initiator:getPlayerName() then -- id == 2 Hit + + TITS.HitEvent(_event) + + elseif _event.id == world.event.S_EVENT_SHOOTING_START and _event.initiator:getPlayerName() then -- id == 23 Start Shooting + + TITS.StartShootingEvent(_event) + + end -- if _event.id == S_EVENT_PLAYER_ENTER_UNIT then --player entered unit + -- ### End event processing + + return true + end, _eventDCS) + + if (not status) then + env.error(string.format("Target Impact Tracker: Error while handling event %s", err),false) + trigger.action.outText( string.format("Target Impact Tracker: Error while handling event %s", err), 30 ) + end +end +--############################################################################################################################################ +function TITS.EnterUnitEvent(_event) + local groupID = _event.initiator:getGroup():getID() + local unitName = _event.initiator:getName() + + -- inits + TITS.attackingUnits[unitName] = false + TITS.reportWeaponsHits[unitName] = false + TITS.lastAbortMSG[unitName] = timer.getTime() - 5 + TITS.passCounter[unitName] = 0 + TITS.uom[unitName] = 2 -- 1 metric (kph), 2 imperial (knots) , 3 imperial (mph) + TITS.DisplayDBA[unitName] = true + TITS.DisplayReleaseData[unitName] = true + + -- Menu + if not TITS.menuAddedToGroup[groupID] then + TITS.rootPath[unitName] = missionCommands.addSubMenuForGroup(groupID, "Range") + local _reportsPath = missionCommands.addSubMenuForGroup(groupID, "Reports", TITS.rootPath[unitName]) + missionCommands.addCommandForGroup(groupID,"Attack results" , _reportsPath, TITS.ReportAttackResults , unitName) + + local _settingsPath = missionCommands.addSubMenuForGroup(groupID, "Settings", TITS.rootPath[unitName]) + missionCommands.addCommandForGroup(groupID,"Toggle non strafing hits (BDA) report" , _settingsPath, TITS.Settings , {unitName,1}) + missionCommands.addCommandForGroup(groupID,"Toggle display of Release data" , _settingsPath, TITS.Settings , {unitName,2}) + missionCommands.addCommandForGroup(groupID,"Use metric units" , _settingsPath, TITS.Settings , {unitName,3}) + missionCommands.addCommandForGroup(groupID,"Use imperial units (knots)" , _settingsPath, TITS.Settings , {unitName,4}) + missionCommands.addCommandForGroup(groupID,"Use imperial units (mph)" , _settingsPath, TITS.Settings , {unitName,5}) + + TITS.rollInPath[unitName] = missionCommands.addCommandForGroup(groupID,"Rolling in" , TITS.rootPath[unitName], TITS.ReportAttack , {unitName,'in'}) + + end -- if not TITS.menuAddedToGroup[groupID] then + return true +end +--############################################################################################################################################ +function TITS.ShotEvent(_event) + local _weapon = _event.weapon + local _target = _weapon:getTarget() + local unitName = _event.initiator:getName() + + if not TITS.attackingUnits[unitName] then + local t = timer.getTime() + if (t - TITS.lastAbortMSG[unitName]) > 5 then + TITS.messageToUnit(unitName,'Abort! Abort! Abort!. You are not cleared to employ weapons.',{},10) + TITS.lastAbortMSG[unitName] = t + end + else + local params = {} + params.unitName = _event.initiator:getName() + params.releaseHeading = mist.utils.toDegree(mist.getHeading(_event.initiator)) -- in degrees. Players release heading + local pos = _event.initiator:getPosition().p + local height = land.getHeight({x = pos.x, y = pos.z}) + params.releaseAlt = pos.y - height -- in meters. Release AGL + params.releasePitch = mist.utils.toDegree(mist.getClimbAngle(_event.initiator)) -- in degrees. Release dive angle + params.releaseRoll = mist.utils.toDegree(mist.getRoll(_event.initiator)) -- in degrees. + params.releaseYaw = mist.utils.toDegree(mist.getYaw(_event.initiator)) -- in degrees. + params.releaseSpeed = mist.vec.mag(_event.initiator:getVelocity()) -- meters/second + params.weapon = _weapon + params.weponName = _weapon:getTypeName() + params.weaponID = _weapon:getName() + params.weaponLastPoint = _weapon:getPoint() + + + timer.scheduleFunction(TITS.TrackWeapon, params, timer.getTime() + 0.001 ) + end + return true +end +--############################################################################################################################################ +function TITS.StartShootingEvent(_event) + local unitName = _event.initiator:getName() + + + if not TITS.attackingUnits[unitName] then + local t = timer.getTime() + if (t - TITS.lastAbortMSG[unitName]) > 5 then + TITS.messageToUnit(unitName,'Abort! Abort! Abort!. You are not cleared to employ weapons.',{},10) + TITS.lastAbortMSG[unitName] = t + end + else + TITS.isStrafingAttack[unitName] = true + end + return true +end +--############################################################################################################################################ +function TITS.HitEvent(_event) + local unitName = _event.initiator:getName() + local eventTarget = _event.target + local isStrafingWeapon = string.find(_event.weapon:getTypeName():lower(), "weapons.shell") + local targetName = '' + + if eventTarget then + targetName = eventTarget:getName() + end + + + if TITS.attackingUnits[unitName] and targetName ~= '' then + if isStrafingWeapon then + local list = TITS.strafingHitCounter[unitName] + for t,v in pairs(list) do + + if t == targetName then + + TITS.strafingHitCounter[unitName][targetName] = TITS.strafingHitCounter[unitName][targetName] + 1 + break + end + end + else -- it goes big boom + local target = TITS.getTarget(targetName) + if target then + if target.bda then + -- record the target that got splashed by the weapon BDA + local r = { + weaponID = _event.weapon:getName() + , weaponType = _event.weapon:getTypeName() + , Target = targetName + } + table.insert(TITS.reportWeaponsHits[unitName],r) + + end + end + end -- if isStrafingWeapon then + end + + return true +end +--############################################################################################################################################ +function TITS.ReportAttack(_args) + local unitName = _args[1] + local value = _args[2] + local groupID = Unit.getByName(unitName):getGroup():getID() + + TITS.passCounter[unitName] = TITS.passCounter[unitName] + 1 + + TITS.attackingUnits[unitName] = (value == 'in') + if TITS.attackingUnits[unitName] then + TITS.messageToUnit(unitName,'Roger. Cleared in hot',{},10) + -- reset non-strafing results + TITS.ResetStrafingHitCounter(unitName) + TITS.reportWeaponsDistance[unitName] = {} + TITS.reportWeaponsHits[unitName] = {} + missionCommands.removeItemForGroup(groupID, TITS.rollInPath[unitName]) + TITS.offPath[unitName] = missionCommands.addCommandForGroup(groupID,"Off - attack completed" , TITS.rootPath[unitName], TITS.ReportAttack , {unitName,'off'}) + else + TITS.messageToUnit(unitName,'Roger. Standby for report',{},10) + + TITS.ReportAttackResults(unitName) + + missionCommands.removeItemForGroup(groupID, TITS.offPath[unitName]) + TITS.rollInPath[unitName] = missionCommands.addCommandForGroup(groupID,"Rolling in" , TITS.rootPath[unitName], TITS.ReportAttack , {unitName,'in'}) + end + return true +end +--############################################################################################################################################ +function TITS.ReportAttackResults(_unitName) + local unit = Unit.getByName(_unitName) + local player = unit:getPlayerName() + local dist_uom + + if TITS.uom[_unitName] == 1 then + dist_uom = 'm' + elseif TITS.uom[_unitName] == 2 then + dist_uom = 'ft' + else + dist_uom = 'ft' + end + -- Strafing results + local strafingResults = '' + local unitTargetList = TITS.strafingHitCounter[_unitName] + if TITS.strafingHitCounter[_unitName] ~= nil then + for t,v in pairs(unitTargetList) do + if v > 0 then + local target = TITS.getTarget(t) + if target then + strafingResults = strafingResults..string.format("\n%s, %i hit(s)",target.name,v) + end + end + end + end + + if TITS.isStrafingAttack[_unitName] then + if strafingResults == '' then + strafingResults = 'no targets hit' + end + strafingResults = 'Strafing attack: '.. strafingResults + end + +-- Impact distace results and BDA + + local distanceResult = '' + + for _,r in pairs(TITS.reportWeaponsDistance[_unitName]) do + + local impact = 'Miss' + local targetsHit = '' + if r.impactDistance <= TITS.MaxMissDistance then + impact = string.format("%s %s of %s @ %s o'clock" + , math.floor(TITS.convertDistance(_unitName,r.impactDistance)) + , dist_uom + , r.closestTarget + , r.impactDirection + ) + end + ------ BDA + local targetsHit = '' + for _,h in pairs(TITS.reportWeaponsHits[_unitName]) do + if h.weaponID == r.weaponID then + if not string.find(targetsHit, h.Target) then + if targetsHit ~= '' then + targetsHit = targetsHit..', ' + end + targetsHit = targetsHit..h.Target + end + end + end + if targetsHit == '' then + targetsHit = 'none' + end + + local tempStr = string.format( ">>> %s : %s" + , r.weaponType + , impact + ) + if TITS.DisplayDBA[_unitName] then + tempStr = string.format("%s\n Targets hit: %s",tempStr,targetsHit) + end + + + -- Release parameters + local alt = string.format("%s",math.floor(TITS.convertDistance(_unitName,r.releaseAlt )+0.5)) + local Speed = string.format("%s",math.floor(TITS.convertSpeed(_unitName,r.releaseSpeed )+0.5)) + if TITS.uom[_unitName] == 1 then + alt = alt..' m' + Speed = Speed..' kph' + elseif TITS.uom[_unitName] == 2 then + alt = alt..' ft' + Speed = Speed..' knots' + else + alt = alt..' ft' + Speed = Speed..' mph' + end + local Pitch = string.format("%s°",math.floor((r.releasePitch+0.05) *100 )/100) + local Roll = string.format("%s°",math.floor((r.releaseRoll +0.05) *100 )/100) + local Yaw = string.format("%s°",math.floor((r.releaseYaw +0.05) *100 )/100) + local relP = string.format(" Release Parameters:\n Alt: %s AGL Speed: %s\n P: %s R:%s Y:%s" + , alt + , Speed + , Pitch + , Roll + , Yaw + ) + + if TITS.DisplayReleaseData[_unitName] then + tempStr = string.format("%s\n%s",tempStr,relP) + end + + if tempStr ~= '' then + distanceResult = distanceResult..'\n'..tempStr + end + end + + + local results = '' + if strafingResults ~= '' then + results = strafingResults..'\n\n' + end + if distanceResult ~= '' then + results = results..distanceResult..'\n\n' + end + + TITS.messageToUnit(_unitName,results,{},45) + return true +end +--############################################################################################################################################ +function TITS.messageToUnit(_unitName,_messageText,_soundTable,_displayTime) + local msg = {} + + msg.text = _messageText + msg.msgFor = {units = {_unitName}} + + if TITS.messageBeep ~= '' then + msg.sound = TITS.messageBeep + end + + if _soundTable ~= nil then + msg.multSound = _soundTable + end + + if _displayTime == nil then + _displayTime = 5 + end + msg.displayTime = _displayTime + + mist.message.add(msg) + + return true +end +--############################################################################################################################################ +function TITS.Settings(_args) + local unitName = _args[1] + local setting = _args[2] + + if setting == 1 then -- Toggle non strafing hits report + TITS.DisplayDBA[unitName] = not TITS.DisplayDBA[unitName] + if TITS.DisplayDBA[unitName] then + TITS.messageToUnit(unitName,'Non-strafing hits (BDA) reports is ON') + else + TITS.messageToUnit(unitName,'Non-strafing hits (BDA) reports is OFF') + end + elseif setting == 2 then -- Toggle display of Release data + TITS.DisplayReleaseData[unitName] = not TITS.DisplayReleaseData[unitName] + if TITS.DisplayReleaseData[unitName] then + TITS.messageToUnit(unitName,'Release data display is ON') + else + TITS.messageToUnit(unitName,'Release data display is OFF') + end + elseif setting == 3 then -- use metric units + TITS.uom[unitName] = 1 -- 1 metric (kph), 2 imperial (knots) , 3 imperial (mph) + TITS.messageToUnit(unitName,'Using metric units') + elseif setting == 4 then -- use imperial units(knots) + TITS.uom[unitName] = 2 -- 1 metric (kph), 2 imperial (knots) , 3 imperial (mph) + TITS.messageToUnit(unitName,'Using imperial units (knots)') + elseif setting == 5 then -- use imperial units(mph + TITS.uom[unitName] = 3 -- 1 metric (kph), 2 imperial (knots) , 3 imperial (mph) + TITS.messageToUnit(unitName,'Using imperial units (mph)') + end -- main if + return true +end +--############################################################################################################################################ +function TITS.ResetStrafingHitCounter(_unitName) + + TITS.isStrafingAttack[_unitName] = false + for _,t in pairs(TITS.TargetList) do + if t.strafing and t.type == 'unit' then + u = Unit.getByName(t.name) + if u then + if TITS.strafingHitCounter[_unitName] == nil then + TITS.strafingHitCounter[_unitName] = {} + end + TITS.strafingHitCounter[_unitName][t.name] = 0 + -- else ignore it, doesn't exists + end -- if u then + end -- if t.strafing and type == 'unit'then + + end + + return true +end +--############################################################################################################################################ +function TITS.getTarget(_name) + for _,t in pairs(TITS.TargetList) do + if t.name == _name then + return t + end + end + return nil +end +--############################################################################################################################################ +function TITS.TrackWeapon(params,time) + + local _status,_weaponPos = pcall(function() + return params.weapon:getPoint() + end) + if _status then + -- weapon still in fly, update last point + params.weaponLastPoint = _weaponPos + return timer.getTime() + 0.001 -- keep tracking + else -- wepon hit + local target + local targetPos + local tempTarget + local tempDist = nil + local tempPos + -- weapon had no valid target so look for the closest to impact + for t,v in pairs(TITS.TargetList) do + if v.impact then -- impact tracking target + local oTarget + local oTargetPos + + if v.type == 'unit' then + oTarget = Unit.getByName(v.name) + oTargetPos = oTarget:getPoint() + else -- it's a zone + oTarget = trigger.misc.getZone(v.name) + oTargetPos = oTarget.point + end -- if v.type == 'unit' then + local d = math.floor(TITS.getDistance(params.weaponLastPoint,oTargetPos)) -- in meters + if tempDist then + if d < tempDist then + tempTarget = v.name + tempPos = oTargetPos + tempDist = d + end -- if d < tempDist then + else -- if tempDist then + tempTarget = v.name + tempPos = oTargetPos + tempDist = d + end -- if not tempDist then + end --if v.impact then -- impact tracking target + end -- for t,v in pairs(TITS.TargetList) do + target = tempTarget + targetPos = tempPos + + + local _impactDistance = math.floor(TITS.getDistance(params.weaponLastPoint,targetPos)) -- in meters + local _direction = TITS.getRelativeDirection(params.releaseHeading ,targetPos,params.weaponLastPoint) + _direction = TITS.getClockDirection(_direction) + + -- store the data + + local r = { + weaponID = params.weaponID + , weaponType = params.weponName + , intendedTarget = 'unk' + , closestTarget = target + , impactDistance = _impactDistance + , impactDirection = _direction + , releaseHeading = params.releaseHeading + , releaseAlt = params.releaseAlt + , releaseSpeed = params.releaseSpeed + , releasePitch = params.releasePitch + , releaseRoll = params.releaseRoll + , releaseYaw = params.releaseYaw + } + if params.weponHasTarget then + r.intendedTarget = params.target:getName() + end + table.insert(TITS.reportWeaponsDistance[params.unitName],r) + return nil + end +end +--############################################################################################################################################ +function TITS.convertSpeed(_unitName,_speed) + local s + -- 1 metric (kph), 2 imperial (knots) , 3 imperial (mph) + if TITS.uom[_unitName] == 1 then + s = _speed * 3.6 + elseif TITS.uom[_unitName] == 2 then + s = _speed * 1.943844 + else + s = _speed * 2.23694 + end + return s +end +--############################################################################################################################################ +function TITS.convertDistance(_unitName,_distance) + local s + -- 1 metric (kph), 2 imperial (knots) , 3 imperial (mph) + if TITS.uom[_unitName] == 1 then + s = _distance + else + s = _distance * 3.28084 + end + return s +end +--############################################################################################################################################ +function TITS.getDistance(_point1, _point2) + + local xUnit = _point1.x + local yUnit = _point1.z + local xZone = _point2.x + local yZone = _point2.z + + local xDiff = xUnit - xZone + local yDiff = yUnit - yZone + + return math.sqrt(xDiff * xDiff + yDiff * yDiff) +end +--############################################################################################################################################ +function TITS.getRelativeDirection(_refHeading,_point1, _point2) + local tgtBearing + local xUnit = _point1.x + local yUnit = _point1.z + local xZone = _point2.x + local yZone = _point2.z + + local xDiff = xUnit - xZone + local yDiff = yUnit - yZone + + local tgtAngle = math.deg(math.atan(yDiff/xDiff)) + + if xDiff > 0 then + tgtBearing = 180 + tgtAngle + end + + if xDiff < 0 and tgtAngle > 0 then + tgtBearing = tgtAngle + end + + if xDiff < 0 and tgtAngle < 0 then + tgtBearing = 360 + tgtAngle + end + + tgtBearing = tgtBearing - _refHeading + if tgtBearing > 360 then + tgtBearing = tgtBearing - 360 + end + if tgtBearing < 0 then + tgtBearing = 360 + tgtBearing + end + + return tgtBearing +end +--############################################################################################################################################ +function TITS.getClockDirection(_direction) + -- by cfrag + if not _direction then return 0 end + while _direction < 0 do + _direction = _direction + 360 + end + + if _direction < 15 then -- special case 12 o'clock past 12 o'clock + return 12 + end + + _direction = _direction + 15 -- add offset so we get all other times correct + return math.floor(_direction/30) + +end +--############################################################################################################################################ +function TITS.getDirection(_point1, _point2) + local tgtBearing + local xUnit = _point1.x + local yUnit = _point1.z + local xZone = _point2.x + local yZone = _point2.z + + local xDiff = xUnit - xZone + local yDiff = yUnit - yZone + + local tgtAngle = math.deg(math.atan(yDiff/xDiff)) + + if xDiff > 0 then + tgtBearing = 180 + tgtAngle + end + + if xDiff < 0 and tgtAngle > 0 then + tgtBearing = tgtAngle + end + + if xDiff < 0 and tgtAngle < 0 then + tgtBearing = 360 + tgtAngle + end + + + env.info("xDiff "..xDiff.." yDiff "..yDiff) + return tgtBearing +end +--############################################################################################################################################ + + + +--############################################################################################################################################ +-- Main Body +world.addEventHandler(TITS.eventHandler) \ No newline at end of file