mirror of
https://github.com/FlightControl-Master/MOOSE.git
synced 2025-08-15 10:47:21 +00:00
Correct module links. Replace references to missing images with MOOSE.JPG. Add default MOOSE.JPG where no image was previously set. Fix minor spelling error.
4087 lines
156 KiB
Lua
4087 lines
156 KiB
Lua
--- **Functional** - Range Practice.
|
|
--
|
|
-- ===
|
|
--
|
|
-- The RANGE class enables easy set up of bombing and strafing ranges within DCS World.
|
|
--
|
|
-- Implementation is based on the [Simple Range Script](https://forums.eagle.ru/showthread.php?t=157991) by [Ciribob](https://forums.eagle.ru/member.php?u=112175), which itself was motivated
|
|
-- by a script by SNAFU [see here](https://forums.eagle.ru/showthread.php?t=109174).
|
|
--
|
|
-- [476th - Air Weapons Range Objects mod](http://www.476vfightergroup.com/downloads.php?do=file&id=287) is highly recommended for this class.
|
|
--
|
|
-- **Main Features:**
|
|
--
|
|
-- * Impact points of bombs, rockets and missiles are recorded and distance to closest range target is measured and reported to the player.
|
|
-- * Number of hits on strafing passes are counted and reported. Also the percentage of hits w.r.t fired shots is evaluated.
|
|
-- * Results of all bombing and strafing runs are stored and top 10 results can be displayed.
|
|
-- * Range targets can be marked by smoke.
|
|
-- * Range can be illuminated by illumination bombs for night missions.
|
|
-- * Bomb, rocket and missile impact points can be marked by smoke.
|
|
-- * Direct hits on targets can trigger flares.
|
|
-- * Smoke and flare colors can be adjusted for each player via radio menu.
|
|
-- * Range information and weather at the range can be obtained via radio menu.
|
|
-- * Persistence: Bombing range results can be saved to disk and loaded the next time the mission is started.
|
|
-- * Range control voice overs (>40) for hit assessment.
|
|
--
|
|
-- ===
|
|
--
|
|
-- ## Youtube Videos:
|
|
--
|
|
-- * [MOOSE YouTube Channel](https://www.youtube.com/channel/UCjrA9j5LQoWsG4SpS8i79Qg)
|
|
-- * [MOOSE - On the Range - Demonstration Video](https://www.youtube.com/watch?v=kIXcxNB9_3M)
|
|
--
|
|
-- ===
|
|
--
|
|
-- ## Missions:
|
|
--
|
|
-- * [MAR - On the Range - MOOSE - SC](https://www.digitalcombatsimulator.com/en/files/3317765/) by shagrat
|
|
--
|
|
-- ===
|
|
--
|
|
-- ## Sound files: [MOOSE Sound Files](https://github.com/FlightControl-Master/MOOSE_SOUND/releases)
|
|
--
|
|
-- ===
|
|
--
|
|
-- ### Author: **[funkyfranky](https://forums.eagle.ru/member.php?u=115026)**
|
|
--
|
|
-- ### Contributions: [FlightControl](https://forums.eagle.ru/member.php?u=89536), [Ciribob](https://forums.eagle.ru/member.php?u=112175)
|
|
-- ### SRS Additions: Applevangelist
|
|
--
|
|
-- ===
|
|
-- @module Functional.Range
|
|
-- @image Range.JPG
|
|
|
|
--- RANGE class
|
|
-- @type RANGE
|
|
-- @field #string ClassName Name of the Class.
|
|
-- @field #boolean Debug If true, debug info is sent as messages on the screen.
|
|
-- @field #boolean verbose Verbosity level. Higher means more output to DCS log file.
|
|
-- @field #string id String id of range for output in DCS log.
|
|
-- @field #string rangename Name of the range.
|
|
-- @field Core.Point#COORDINATE location Coordinate of the range location.
|
|
-- @field #number rangeradius Radius of range defining its total size for e.g. smoking bomb impact points and sending radio messages. Default 5 km.
|
|
-- @field Core.Zone#ZONE rangezone MOOSE zone object of the range. For example, no bomb impacts are smoked if bombs fall outside of the range zone.
|
|
-- @field #table strafeTargets Table of strafing targets.
|
|
-- @field #table bombingTargets Table of targets to bomb.
|
|
-- @field #number nbombtargets Number of bombing targets.
|
|
-- @field #number nstrafetargets Number of strafing targets.
|
|
-- @field #boolean messages Globally enable/disable all messages to players.
|
|
-- @field #table MenuAddedTo Table for monitoring which players already got an F10 menu.
|
|
-- @field #table planes Table for administration.
|
|
-- @field #table strafeStatus Table containing the current strafing target a player as assigned to.
|
|
-- @field #table strafePlayerResults Table containing the strafing results of each player.
|
|
-- @field #table bombPlayerResults Table containing the bombing results of each player.
|
|
-- @field #table PlayerSettings Individual player settings.
|
|
-- @field #number dtBombtrack Time step [sec] used for tracking released bomb/rocket positions. Default 0.005 seconds.
|
|
-- @field #number BombtrackThreshold Bombs/rockets/missiles are only tracked if player-range distance is smaller than this threshold [m]. Default 25000 m.
|
|
-- @field #number Tmsg Time [sec] messages to players are displayed. Default 30 sec.
|
|
-- @field #string examinergroupname Name of the examiner group which should get all messages.
|
|
-- @field #boolean examinerexclusive If true, only the examiner gets messages. If false, clients and examiner get messages.
|
|
-- @field #number strafemaxalt Maximum altitude in meters AGL for registering for a strafe run. Default is 914 m = 3000 ft.
|
|
-- @field #number ndisplayresult Number of (player) results that a displayed. Default is 10.
|
|
-- @field Utilities.Utils#SMOKECOLOR BombSmokeColor Color id used for smoking bomb targets.
|
|
-- @field Utilities.Utils#SMOKECOLOR StrafeSmokeColor Color id used to smoke strafe targets.
|
|
-- @field Utilities.Utils#SMOKECOLOR StrafePitSmokeColor Color id used to smoke strafe pit approach boxes.
|
|
-- @field #number illuminationminalt Minimum altitude in meters AGL at which illumination bombs are fired. Default is 500 m.
|
|
-- @field #number illuminationmaxalt Maximum altitude in meters AGL at which illumination bombs are fired. Default is 1000 m.
|
|
-- @field #number scorebombdistance Distance from closest target up to which bomb hits are counted. Default 1000 m.
|
|
-- @field #number TdelaySmoke Time delay in seconds between impact of bomb and starting the smoke. Default 3 seconds.
|
|
-- @field #boolean eventmoose If true, events are handled by MOOSE. If false, events are handled directly by DCS eventhandler. Default true.
|
|
-- @field #boolean trackbombs If true (default), all bomb types are tracked and impact point to closest bombing target is evaluated.
|
|
-- @field #boolean trackrockets If true (default), all rocket types are tracked and impact point to closest bombing target is evaluated.
|
|
-- @field #boolean trackmissiles If true (default), all missile types are tracked and impact point to closest bombing target is evaluated.
|
|
-- @field #boolean defaultsmokebomb If true, initialize player settings to smoke bomb.
|
|
-- @field #boolean autosave If true, automatically save results every X seconds.
|
|
-- @field #number instructorfreq Frequency on which the range control transmitts.
|
|
-- @field Sound.RadioQueue#RADIOQUEUE instructor Instructor radio queue.
|
|
-- @field #number rangecontrolfreq Frequency on which the range control transmitts.
|
|
-- @field Sound.RadioQueue#RADIOQUEUE rangecontrol Range control radio queue.
|
|
-- @field #string rangecontrolrelayname Name of relay unit.
|
|
-- @field #string instructorrelayname Name of relay unit.
|
|
-- @field #string soundpath Path inside miz file where the sound files are located. Default is "Range Soundfiles/".
|
|
-- @field #boolean targetsheet If true, players can save their target sheets. Rangeboss will not work if targetsheets do not save.
|
|
-- @field #string targetpath Path where to save the target sheets.
|
|
-- @field #string targetprefix File prefix for target sheet files.
|
|
-- @field Sound.SRS#MSRS controlmsrs
|
|
-- @field Sound.SRS#MSRSQUEUE controlsrsQ
|
|
-- @field Sound.SRS#MSRS instructmsrs
|
|
-- @field Sound.SRS#MSRSQUEUE instructsrsQ
|
|
-- @extends Core.Fsm#FSM
|
|
|
|
--- *Don't only practice your art, but force your way into its secrets; art deserves that, for it and knowledge can raise man to the Divine.* - Ludwig van Beethoven
|
|
--
|
|
-- ===
|
|
--
|
|
-- 
|
|
--
|
|
-- # The Range Concept
|
|
--
|
|
-- The RANGE class enables a mission designer to easily set up practice ranges in DCS. A new RANGE object can be created with the @{#RANGE.New}(*rangename*) contructor.
|
|
-- The parameter *rangename* defines the name of the range. It has to be unique since this is also the name displayed in the radio menu.
|
|
--
|
|
-- Generally, a range consists of strafe pits and bombing targets. For strafe pits the number of hits for each pass is counted and tabulated.
|
|
-- For bombing targets, the distance from the impact point of the bomb, rocket or missile to the closest range target is measured and tabulated.
|
|
-- Each player can display his best results via a function in the radio menu or see the best best results from all players.
|
|
--
|
|
-- When all targets have been defined in the script, the range is started by the @{#RANGE.Start}() command.
|
|
--
|
|
-- **IMPORTANT**
|
|
--
|
|
-- Due to a DCS bug, it is not possible to directly monitor when a player enters a plane. So in a mission with client slots, it is vital that
|
|
-- a player first enters as spectator or hits ESC twice and **after that** jumps into the slot of his aircraft!
|
|
-- If that is not done, the script is not started correctly. This can be checked by looking at the radio menues. If the mission was entered correctly,
|
|
-- there should be an "On the Range" menu items in the "F10. Other..." menu.
|
|
--
|
|
-- # Strafe Pits
|
|
--
|
|
-- Each strafe pit can consist of multiple targets. Often one finds two or three strafe targets next to each other.
|
|
--
|
|
-- A strafe pit can be added to the range by the @{#RANGE.AddStrafePit}(*targetnames, boxlength, boxwidth, heading, inverseheading, goodpass, foulline*) function.
|
|
--
|
|
-- * The first parameter *targetnames* defines the target or targets. This can be a single item or a Table with the name(s) of @{Wrapper.Unit} or @{Wrapper.Static} objects defined in the mission editor.
|
|
-- * In order to perform a valid pass on the strafe pit, the pilot has to begin his run from the correct direction. Therefore, an "approach box" is defined in front
|
|
-- of the strafe targets. The parameters *boxlength* and *boxwidth* define the size of the box in meters, while the *heading* parameter defines the heading of the box FROM the target.
|
|
-- For example, if heading 120 is set, the approach box will start FROM the target and extend outwards on heading 120. A strafe run approach must then be flown apx. heading 300 TOWARDS the target.
|
|
-- If the parameter *heading* is passed as **nil**, the heading is automatically taken from the heading set in the ME for the first target unit.
|
|
-- * The parameter *inverseheading* turns the heading around by 180 degrees. This is useful when the default heading of strafe target units point in the wrong/opposite direction.
|
|
-- * The parameter *goodpass* defines the number of hits a pilot has to achieve during a run to be judged as a "good" pass.
|
|
-- * The last parameter *foulline* sets the distance from the pit targets to the foul line. Hit from closer than this line are not counted!
|
|
--
|
|
-- Another function to add a strafe pit is @{#RANGE.AddStrafePitGroup}(*group, boxlength, boxwidth, heading, inverseheading, goodpass, foulline*). Here,
|
|
-- the first parameter *group* is a MOOSE @{Wrapper.Group} object and **all** units in this group define **one** strafe pit.
|
|
--
|
|
-- Finally, a valid approach has to be performed below a certain maximum altitude. The default is 914 meters (3000 ft) AGL. This is a parameter valid for all
|
|
-- strafing pits of the range and can be adjusted by the @{#RANGE.SetMaxStrafeAlt}(maxalt) function.
|
|
--
|
|
-- # Bombing targets
|
|
--
|
|
-- One ore multiple bombing targets can be added to the range by the @{#RANGE.AddBombingTargets}(targetnames, goodhitrange, randommove) function.
|
|
--
|
|
-- * The first parameter *targetnames* defines the target or targets. This can be a single item or a Table with the name(s) of @{Wrapper.Unit} or @{Wrapper.Static} objects defined in the mission editor.
|
|
-- * The (optional) parameter *goodhitrange* specifies the radius in metres around the target within which a bomb/rocket hit is considered to be "good".
|
|
-- * If final (optional) parameter "*randommove*" can be enabled to create moving targets. If this parameter is set to true, the units of this bombing target will randomly move within the range zone.
|
|
-- Note that there might be quirks since DCS units can get stuck in buildings etc. So it might be safer to manually define a route for the units in the mission editor if moving targets are desired.
|
|
--
|
|
-- ## Adding Groups
|
|
--
|
|
-- Another possibility to add bombing targets is the @{#RANGE.AddBombingTargetGroup}(*group, goodhitrange, randommove*) function. Here the parameter *group* is a MOOSE @{Wrapper.Group} object
|
|
-- and **all** units in this group are defined as bombing targets.
|
|
--
|
|
-- ## Specifying Coordinates
|
|
--
|
|
-- It is also possible to specify coordinates rather than unit or static objects as bombing target locations. This has the advantage, that even when the unit/static object is dead, the specified
|
|
-- coordinate will still be a valid impact point. This can be done via the @{#RANGE.AddBombingTargetCoordinate}(*coord*, *name*, *goodhitrange*) function.
|
|
--
|
|
-- # Fine Tuning
|
|
--
|
|
-- Many range parameters have good default values. However, the mission designer can change these settings easily with the supplied user functions:
|
|
--
|
|
-- * @{#RANGE.SetMaxStrafeAlt}() sets the max altitude for valid strafing runs.
|
|
-- * @{#RANGE.SetMessageTimeDuration}() sets the duration how long (most) messages are displayed.
|
|
-- * @{#RANGE.SetDisplayedMaxPlayerResults}() sets the number of results displayed.
|
|
-- * @{#RANGE.SetRangeRadius}() defines the total range area.
|
|
-- * @{#RANGE.SetBombTargetSmokeColor}() sets the color used to smoke bombing targets.
|
|
-- * @{#RANGE.SetStrafeTargetSmokeColor}() sets the color used to smoke strafe targets.
|
|
-- * @{#RANGE.SetStrafePitSmokeColor}() sets the color used to smoke strafe pit approach boxes.
|
|
-- * @{#RANGE.SetSmokeTimeDelay}() sets the time delay between smoking bomb/rocket impact points after impact.
|
|
-- * @{#RANGE.TrackBombsON}() or @{#RANGE.TrackBombsOFF}() can be used to enable/disable tracking and evaluating of all bomb types a player fires.
|
|
-- * @{#RANGE.TrackRocketsON}() or @{#RANGE.TrackRocketsOFF}() can be used to enable/disable tracking and evaluating of all rocket types a player fires.
|
|
-- * @{#RANGE.TrackMissilesON}() or @{#RANGE.TrackMissilesOFF}() can be used to enable/disable tracking and evaluating of all missile types a player fires.
|
|
--
|
|
-- # Radio Menu
|
|
--
|
|
-- Each range gets a radio menu with various submenus where each player can adjust his individual settings or request information about the range or his scores.
|
|
--
|
|
-- The main range menu can be found at "F10. Other..." --> "F*X*. On the Range..." --> "F1. <Range Name>...".
|
|
--
|
|
-- The range menu contains the following submenus:
|
|
--
|
|
-- 
|
|
--
|
|
-- * "F1. Statistics...": Range results of all players and personal stats.
|
|
-- * "F2. Mark Targets": Mark range targets by smoke or flares.
|
|
-- * "F3. My Settings" Personal settings.
|
|
-- * "F4. Range Info": Information about the range, such as bearing and range.
|
|
--
|
|
-- ## F1 Statistics
|
|
--
|
|
-- 
|
|
--
|
|
-- ## F2 Mark Targets
|
|
--
|
|
-- 
|
|
--
|
|
-- ## F3 My Settings
|
|
--
|
|
-- 
|
|
--
|
|
-- ## F4 Range Info
|
|
--
|
|
-- 
|
|
--
|
|
-- # Voice Overs
|
|
--
|
|
-- Voice over sound files can be downloaded from the Moose Discord. Check the pinned messages in the *#func-range* channel.
|
|
--
|
|
-- Instructor radio will inform players when they enter or exit the range zone and provide the radio frequency of the range control for hit assessment.
|
|
-- This can be enabled via the @{#RANGE.SetInstructorRadio}(*frequency*) functions, where *frequency* is the AM frequency in MHz.
|
|
--
|
|
-- The range control can be enabled via the @{#RANGE.SetRangeControl}(*frequency*) functions, where *frequency* is the AM frequency in MHz.
|
|
--
|
|
-- By default, the sound files are placed in the "Range Soundfiles/" folder inside the mission (.miz) file. Another folder can be specified via the @{#RANGE.SetSoundfilesPath}(*path*) function.
|
|
--
|
|
-- ## Voice output via SRS
|
|
--
|
|
-- Alternatively, the voice output can be fully done via SRS, **no sound file additions needed**. Set up SRS with @{#RANGE.SetSRS}(). Range control and instructor frequencies and voices can then be
|
|
-- set via @{#RANGE.SetSRSRangeControl}() and @{#RANGE.SetSRSRangeInstructor}()
|
|
--
|
|
-- # Persistence
|
|
--
|
|
-- To automatically save bombing results to disk, use the @{#RANGE.SetAutosave}() function. Bombing results will be saved as csv file in your "Saved Games\DCS.openbeta\Logs" directory.
|
|
-- Each range has a separate file, which is named "RANGE-<*RangeName*>_BombingResults.csv".
|
|
--
|
|
-- The next time you start the mission, these results are also automatically loaded.
|
|
--
|
|
-- Strafing results are currently **not** saved.
|
|
--
|
|
-- # FSM Events
|
|
--
|
|
-- This class creates additional events that can be used by mission designers for custom reactions
|
|
--
|
|
-- * `EnterRange` when a player enters a range zone. See @{#RANGE.OnAfterEnterRange}
|
|
-- * `ExitRange` when a player leaves a range zone. See @{#RANGE.OnAfterExitRange}
|
|
-- * `Impact` on impact of a player's weapon on a bombing target. See @{#RANGE.OnAfterImpact}
|
|
-- * `RollingIn` when a player rolls in on a strafing target. See @{#RANGE.OnAfterRollingIn}
|
|
-- * `StrafeResult` when a player finishes a strafing run. See @{#RANGE.OnAfterStrafeResult}
|
|
--
|
|
-- # Examples
|
|
--
|
|
-- ## Goldwater Range
|
|
--
|
|
-- This example shows hot to set up the [Barry M. Goldwater range](https://en.wikipedia.org/wiki/Barry_M._Goldwater_Air_Force_Range).
|
|
-- It consists of two strafe pits each has two targets plus three bombing targets.
|
|
--
|
|
-- -- Strafe pits. Each pit can consist of multiple targets. Here we have two pits and each of the pits has two targets.
|
|
-- -- These are names of the corresponding units defined in the ME.
|
|
-- local strafepit_left={"GWR Strafe Pit Left 1", "GWR Strafe Pit Left 2"}
|
|
-- local strafepit_right={"GWR Strafe Pit Right 1", "GWR Strafe Pit Right 2"}
|
|
--
|
|
-- -- Table of bombing target names. Again these are the names of the corresponding units as defined in the ME.
|
|
-- local bombtargets={"GWR Bomb Target Circle Left", "GWR Bomb Target Circle Right", "GWR Bomb Target Hard"}
|
|
--
|
|
-- -- Create a range object.
|
|
-- GoldwaterRange=RANGE:New("Goldwater Range")
|
|
--
|
|
-- -- Distance between strafe target and foul line. You have to specify the names of the unit or static objects.
|
|
-- -- Note that this could also be done manually by simply measuring the distance between the target and the foul line in the ME.
|
|
-- GoldwaterRange:GetFoullineDistance("GWR Strafe Pit Left 1", "GWR Foul Line Left")
|
|
--
|
|
-- -- Add strafe pits. Each pit (left and right) consists of two targets. Where "nil" is used as input, the default value is used.
|
|
-- GoldwaterRange:AddStrafePit(strafepit_left, 3000, 300, nil, true, 30, 500)
|
|
-- GoldwaterRange:AddStrafePit(strafepit_right, nil, nil, nil, true, nil, 500)
|
|
--
|
|
-- -- Add bombing targets. A good hit is if the bomb falls less then 50 m from the target.
|
|
-- GoldwaterRange:AddBombingTargets(bombtargets, 50)
|
|
--
|
|
-- -- Start range.
|
|
-- GoldwaterRange:Start()
|
|
--
|
|
-- The [476th - Air Weapons Range Objects mod](http://www.476vfightergroup.com/downloads.php?do=file&id=287) is (implicitly) used in this example.
|
|
--
|
|
--
|
|
-- # Debugging
|
|
--
|
|
-- In case you have problems, it is always a good idea to have a look at your DCS log file. You find it in your "Saved Games" folder, so for example in
|
|
-- C:\Users\<yourname>\Saved Games\DCS\Logs\dcs.log
|
|
-- All output concerning the RANGE class should have the string "RANGE" in the corresponding line.
|
|
--
|
|
-- The verbosity of the output can be increased by adding the following lines to your script:
|
|
--
|
|
-- BASE:TraceOnOff(true)
|
|
-- BASE:TraceLevel(1)
|
|
-- BASE:TraceClass("RANGE")
|
|
--
|
|
-- To get even more output you can increase the trace level to 2 or even 3, c.f. @{BASE} for more details.
|
|
--
|
|
-- The function @{#RANGE.DebugON}() can be used to send messages on screen. It also smokes all defined strafe and bombing targets, the strafe pit approach boxes and the range zone.
|
|
--
|
|
-- Note that it can happen that the RANGE radio menu is not shown. Check that the range object is defined as a **global** variable rather than a local one.
|
|
-- The could avoid the lua garbage collection to accidentally/falsely deallocate the RANGE objects.
|
|
--
|
|
-- @field #RANGE
|
|
RANGE = {
|
|
ClassName = "RANGE",
|
|
Debug = false,
|
|
verbose = 0,
|
|
id = nil,
|
|
rangename = nil,
|
|
location = nil,
|
|
messages = true,
|
|
rangeradius = 5000,
|
|
rangezone = nil,
|
|
strafeTargets = {},
|
|
bombingTargets = {},
|
|
nbombtargets = 0,
|
|
nstrafetargets = 0,
|
|
MenuAddedTo = {},
|
|
planes = {},
|
|
strafeStatus = {},
|
|
strafePlayerResults = {},
|
|
bombPlayerResults = {},
|
|
PlayerSettings = {},
|
|
dtBombtrack = 0.005,
|
|
BombtrackThreshold = 25000,
|
|
Tmsg = 30,
|
|
examinergroupname = nil,
|
|
examinerexclusive = nil,
|
|
strafemaxalt = 914,
|
|
ndisplayresult = 10,
|
|
BombSmokeColor = SMOKECOLOR.Red,
|
|
StrafeSmokeColor = SMOKECOLOR.Green,
|
|
StrafePitSmokeColor = SMOKECOLOR.White,
|
|
illuminationminalt = 500,
|
|
illuminationmaxalt = 1000,
|
|
scorebombdistance = 1000,
|
|
TdelaySmoke = 3.0,
|
|
eventmoose = true,
|
|
trackbombs = true,
|
|
trackrockets = true,
|
|
trackmissiles = true,
|
|
defaultsmokebomb = true,
|
|
autosave = false,
|
|
instructorfreq = nil,
|
|
instructor = nil,
|
|
rangecontrolfreq = nil,
|
|
rangecontrol = nil,
|
|
soundpath = "Range Soundfiles/",
|
|
targetsheet = nil,
|
|
targetpath = nil,
|
|
targetprefix = nil,
|
|
}
|
|
|
|
--- Default range parameters.
|
|
-- @list Defaults
|
|
RANGE.Defaults = {
|
|
goodhitrange = 25,
|
|
strafemaxalt = 914,
|
|
dtBombtrack = 0.005,
|
|
Tmsg = 30,
|
|
ndisplayresult = 10,
|
|
rangeradius = 5000,
|
|
TdelaySmoke = 3.0,
|
|
boxlength = 3000,
|
|
boxwidth = 300,
|
|
goodpass = 20,
|
|
foulline = 610
|
|
}
|
|
|
|
--- Target type, i.e. unit, static, or coordinate.
|
|
-- @type RANGE.TargetType
|
|
-- @field #string UNIT Target is a unit.
|
|
-- @field #string STATIC Target is a static.
|
|
-- @field #string COORD Target is a coordinate.
|
|
RANGE.TargetType = {
|
|
UNIT = "Unit",
|
|
STATIC = "Static",
|
|
COORD = "Coordinate"
|
|
}
|
|
|
|
--- Player settings.
|
|
-- @type RANGE.PlayerData
|
|
-- @field #boolean smokebombimpact Smoke bomb impact points.
|
|
-- @field #boolean flaredirecthits Flare when player directly hits a target.
|
|
-- @field #number smokecolor Color of smoke.
|
|
-- @field #number flarecolor Color of flares.
|
|
-- @field #boolean messages Display info messages.
|
|
-- @field Wrapper.Client#CLIENT client Client object of player.
|
|
-- @field #string unitname Name of player aircraft unit.
|
|
-- @field #string playername Name of player.
|
|
-- @field #string airframe Aircraft type name.
|
|
-- @field #boolean inzone If true, player is inside the range zone.
|
|
|
|
--- Bomb target data.
|
|
-- @type RANGE.BombTarget
|
|
-- @field #string name Name of unit.
|
|
-- @field Wrapper.Unit#UNIT target Target unit.
|
|
-- @field Core.Point#COORDINATE coordinate Coordinate of the target.
|
|
-- @field #number goodhitrange Range in meters for a good hit.
|
|
-- @field #boolean move If true, unit move randomly.
|
|
-- @field #number speed Speed of unit.
|
|
-- @field #RANGE.TargetType type Type of target.
|
|
|
|
--- Strafe target data.
|
|
-- @type RANGE.StrafeTarget
|
|
-- @field #string name Name of the unit.
|
|
-- @field Core.Zone#ZONE_POLYGON polygon Polygon zone.
|
|
-- @field Core.Point#COORDINATE coordinate Center coordinate of the pit.
|
|
-- @field #number goodPass Number of hits for a good pass.
|
|
-- @field #table targets Table of target units.
|
|
-- @field #number foulline Foul line
|
|
-- @field #number smokepoints Number of smoke points.
|
|
-- @field #number heading Heading of pit.
|
|
|
|
--- Strafe status for player.
|
|
-- @type RANGE.StrafeStatus
|
|
-- @field #number hits Number of hits on target.
|
|
-- @field #number time Number of times.
|
|
-- @field #number ammo Amount of ammo.
|
|
-- @field #boolean pastfoulline If `true`, player passed foul line. Invalid pass.
|
|
-- @field #RANGE.StrafeTarget zone Strafe target.
|
|
|
|
--- Bomb target result.
|
|
-- @type RANGE.BombResult
|
|
-- @field #string name Name of closest target.
|
|
-- @field #number distance Distance in meters.
|
|
-- @field #number radial Radial in degrees.
|
|
-- @field #string weapon Name of the weapon.
|
|
-- @field #string quality Hit quality.
|
|
-- @field #string player Player name.
|
|
-- @field #string airframe Aircraft type of player.
|
|
-- @field #number time Time via timer.getAbsTime() in seconds of impact.
|
|
-- @field #string date OS date.
|
|
-- @field #number attackHdg Attack heading in degrees.
|
|
-- @field #number attackVel Attack velocity in knots.
|
|
-- @field #number attackAlt Attack altitude in feet.
|
|
-- @field #string clock Time of the run.
|
|
-- @field #string rangename Name of the range.
|
|
|
|
--- Strafe result.
|
|
-- @type RANGE.StrafeResult
|
|
-- @field #string player Player name.
|
|
-- @field #string airframe Aircraft type of player.
|
|
-- @field #number time Time via timer.getAbsTime() in seconds of impact.
|
|
-- @field #string date OS date.
|
|
-- @field #string name Name of the target pit.
|
|
-- @field #number roundsFired Number of rounds fired.
|
|
-- @field #number roundsHit Number of rounds that hit the target.
|
|
-- @field #number strafeAccuracy Accuracy of the run in percent.
|
|
-- @field #string clock Time of the run.
|
|
-- @field #string rangename Name of the range.
|
|
-- @field #boolean invalid Invalid pass.
|
|
|
|
--- Strafe result.
|
|
-- @type RANGE.StrafeResult
|
|
-- @field #string player Player name.
|
|
-- @field #string airframe Aircraft type of player.
|
|
-- @field #number time Time via timer.getAbsTime() in seconds of impact.
|
|
-- @field #string date OS date.
|
|
|
|
--- Sound file data.
|
|
-- @type RANGE.Soundfile
|
|
-- @field #string filename Name of the file
|
|
-- @field #number duration Duration in seconds.
|
|
|
|
--- Sound files.
|
|
-- @type RANGE.Sound
|
|
-- @field #RANGE.Soundfile RC0
|
|
-- @field #RANGE.Soundfile RC1
|
|
-- @field #RANGE.Soundfile RC2
|
|
-- @field #RANGE.Soundfile RC3
|
|
-- @field #RANGE.Soundfile RC4
|
|
-- @field #RANGE.Soundfile RC5
|
|
-- @field #RANGE.Soundfile RC6
|
|
-- @field #RANGE.Soundfile RC7
|
|
-- @field #RANGE.Soundfile RC8
|
|
-- @field #RANGE.Soundfile RC9
|
|
-- @field #RANGE.Soundfile RCAccuracy
|
|
-- @field #RANGE.Soundfile RCDegrees
|
|
-- @field #RANGE.Soundfile RCExcellentHit
|
|
-- @field #RANGE.Soundfile RCExcellentPass
|
|
-- @field #RANGE.Soundfile RCFeet
|
|
-- @field #RANGE.Soundfile RCFor
|
|
-- @field #RANGE.Soundfile RCGoodHit
|
|
-- @field #RANGE.Soundfile RCGoodPass
|
|
-- @field #RANGE.Soundfile RCHitsOnTarget
|
|
-- @field #RANGE.Soundfile RCImpact
|
|
-- @field #RANGE.Soundfile RCIneffectiveHit
|
|
-- @field #RANGE.Soundfile RCIneffectivePass
|
|
-- @field #RANGE.Soundfile RCInvalidHit
|
|
-- @field #RANGE.Soundfile RCLeftStrafePitTooQuickly
|
|
-- @field #RANGE.Soundfile RCPercent
|
|
-- @field #RANGE.Soundfile RCPoorHit
|
|
-- @field #RANGE.Soundfile RCPoorPass
|
|
-- @field #RANGE.Soundfile RCRollingInOnStrafeTarget
|
|
-- @field #RANGE.Soundfile RCTotalRoundsFired
|
|
-- @field #RANGE.Soundfile RCWeaponImpactedTooFar
|
|
-- @field #RANGE.Soundfile IR0
|
|
-- @field #RANGE.Soundfile IR1
|
|
-- @field #RANGE.Soundfile IR2
|
|
-- @field #RANGE.Soundfile IR3
|
|
-- @field #RANGE.Soundfile IR4
|
|
-- @field #RANGE.Soundfile IR5
|
|
-- @field #RANGE.Soundfile IR6
|
|
-- @field #RANGE.Soundfile IR7
|
|
-- @field #RANGE.Soundfile IR8
|
|
-- @field #RANGE.Soundfile IR9
|
|
-- @field #RANGE.Soundfile IRDecimal
|
|
-- @field #RANGE.Soundfile IRMegaHertz
|
|
-- @field #RANGE.Soundfile IREnterRange
|
|
-- @field #RANGE.Soundfile IRExitRange
|
|
RANGE.Sound = {
|
|
RC0 = { filename = "RC-0.ogg", duration = 0.60 },
|
|
RC1 = { filename = "RC-1.ogg", duration = 0.47 },
|
|
RC2 = { filename = "RC-2.ogg", duration = 0.43 },
|
|
RC3 = { filename = "RC-3.ogg", duration = 0.50 },
|
|
RC4 = { filename = "RC-4.ogg", duration = 0.58 },
|
|
RC5 = { filename = "RC-5.ogg", duration = 0.54 },
|
|
RC6 = { filename = "RC-6.ogg", duration = 0.61 },
|
|
RC7 = { filename = "RC-7.ogg", duration = 0.53 },
|
|
RC8 = { filename = "RC-8.ogg", duration = 0.34 },
|
|
RC9 = { filename = "RC-9.ogg", duration = 0.54 },
|
|
RCAccuracy = { filename = "RC-Accuracy.ogg", duration = 0.67 },
|
|
RCDegrees = { filename = "RC-Degrees.ogg", duration = 0.59 },
|
|
RCExcellentHit = { filename = "RC-ExcellentHit.ogg", duration = 0.76 },
|
|
RCExcellentPass = { filename = "RC-ExcellentPass.ogg", duration = 0.89 },
|
|
RCFeet = { filename = "RC-Feet.ogg", duration = 0.49 },
|
|
RCFor = { filename = "RC-For.ogg", duration = 0.64 },
|
|
RCGoodHit = { filename = "RC-GoodHit.ogg", duration = 0.52 },
|
|
RCGoodPass = { filename = "RC-GoodPass.ogg", duration = 0.62 },
|
|
RCHitsOnTarget = { filename = "RC-HitsOnTarget.ogg", duration = 0.88 },
|
|
RCImpact = { filename = "RC-Impact.ogg", duration = 0.61 },
|
|
RCIneffectiveHit = { filename = "RC-IneffectiveHit.ogg", duration = 0.86 },
|
|
RCIneffectivePass = { filename = "RC-IneffectivePass.ogg", duration = 0.99 },
|
|
RCInvalidHit = { filename = "RC-InvalidHit.ogg", duration = 2.97 },
|
|
RCLeftStrafePitTooQuickly = { filename = "RC-LeftStrafePitTooQuickly.ogg", duration = 3.09 },
|
|
RCPercent = { filename = "RC-Percent.ogg", duration = 0.56 },
|
|
RCPoorHit = { filename = "RC-PoorHit.ogg", duration = 0.54 },
|
|
RCPoorPass = { filename = "RC-PoorPass.ogg", duration = 0.68 },
|
|
RCRollingInOnStrafeTarget = { filename = "RC-RollingInOnStrafeTarget.ogg", duration = 1.38 },
|
|
RCTotalRoundsFired = { filename = "RC-TotalRoundsFired.ogg", duration = 1.22 },
|
|
RCWeaponImpactedTooFar = { filename = "RC-WeaponImpactedTooFar.ogg", duration = 3.73 },
|
|
IR0 = { filename = "IR-0.ogg", duration = 0.55 },
|
|
IR1 = { filename = "IR-1.ogg", duration = 0.41 },
|
|
IR2 = { filename = "IR-2.ogg", duration = 0.37 },
|
|
IR3 = { filename = "IR-3.ogg", duration = 0.41 },
|
|
IR4 = { filename = "IR-4.ogg", duration = 0.37 },
|
|
IR5 = { filename = "IR-5.ogg", duration = 0.43 },
|
|
IR6 = { filename = "IR-6.ogg", duration = 0.55 },
|
|
IR7 = { filename = "IR-7.ogg", duration = 0.43 },
|
|
IR8 = { filename = "IR-8.ogg", duration = 0.38 },
|
|
IR9 = { filename = "IR-9.ogg", duration = 0.55 },
|
|
IRDecimal = { filename = "IR-Decimal.ogg", duration = 0.54 },
|
|
IRMegaHertz = { filename = "IR-MegaHertz.ogg", duration = 0.87 },
|
|
IREnterRange = { filename = "IR-EnterRange.ogg", duration = 4.83 },
|
|
IRExitRange = { filename = "IR-ExitRange.ogg", duration = 3.10 },
|
|
}
|
|
|
|
--- Global list of all defined range names.
|
|
-- @field #table Names
|
|
RANGE.Names = {}
|
|
|
|
--- Main radio menu on group level.
|
|
-- @field #table MenuF10 Root menu table on group level.
|
|
RANGE.MenuF10 = {}
|
|
|
|
--- Main radio menu on mission level.
|
|
-- @field #table MenuF10Root Root menu on mission level.
|
|
RANGE.MenuF10Root = nil
|
|
|
|
--- Range script version.
|
|
-- @field #string version
|
|
RANGE.version = "2.5.0"
|
|
|
|
-- TODO list:
|
|
-- TODO: Verbosity level for messages.
|
|
-- TODO: Add option for default settings such as smoke off.
|
|
-- TODO: Add custom weapons, which can be specified by the user.
|
|
-- TODO: Check if units are still alive.
|
|
-- DONE: Add statics for strafe pits.
|
|
-- DONE: Add missiles.
|
|
-- DONE: Convert env.info() to self:T()
|
|
-- DONE: Add user functions.
|
|
-- DONE: Rename private functions, i.e. start with _functionname.
|
|
-- DONE: number of displayed results variable.
|
|
-- DONE: Add tire option for strafe pits. ==> No really feasible since tires are very small and cannot be seen.
|
|
-- DONE: Check that menu texts are short enough to be correctly displayed in VR.
|
|
|
|
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
|
|
|
--- RANGE contructor. Creates a new RANGE object.
|
|
-- @param #RANGE self
|
|
-- @param #string RangeName Name of the range. Has to be unique. Will we used to create F10 menu items etc.
|
|
-- @return #RANGE RANGE object.
|
|
function RANGE:New( RangeName )
|
|
|
|
-- Inherit BASE.
|
|
local self = BASE:Inherit( self, FSM:New() ) -- #RANGE
|
|
|
|
-- Get range name.
|
|
-- TODO: make sure that the range name is not given twice. This would lead to problems in the F10 radio menu.
|
|
self.rangename = RangeName or "Practice Range"
|
|
|
|
-- Log id.
|
|
self.id = string.format( "RANGE %s | ", self.rangename )
|
|
|
|
-- Debug info.
|
|
local text = string.format( "Script version %s - creating new RANGE object %s.", RANGE.version, self.rangename )
|
|
self:I( self.id .. text )
|
|
|
|
-- Defaults
|
|
self:SetDefaultPlayerSmokeBomb()
|
|
|
|
-- Start State.
|
|
self:SetStartState( "Stopped" )
|
|
|
|
---
|
|
-- Add FSM transitions.
|
|
-- From State --> Event --> To State
|
|
self:AddTransition("Stopped", "Start", "Running") -- Start RANGE script.
|
|
self:AddTransition("*", "Status", "*") -- Status of RANGE script.
|
|
self:AddTransition("*", "Impact", "*") -- Impact of bomb/rocket/missile.
|
|
self:AddTransition("*", "RollingIn", "*") -- Player rolling in on strafe target.
|
|
self:AddTransition("*", "StrafeResult", "*") -- Strafe result of player.
|
|
self:AddTransition("*", "EnterRange", "*") -- Player enters the range.
|
|
self:AddTransition("*", "ExitRange", "*") -- Player leaves the range.
|
|
self:AddTransition("*", "Save", "*") -- Save player results.
|
|
self:AddTransition("*", "Load", "*") -- Load player results.
|
|
|
|
------------------------
|
|
--- Pseudo Functions ---
|
|
------------------------
|
|
|
|
--- Triggers the FSM event "Start". Starts the RANGE. Initializes parameters and starts event handlers.
|
|
-- @function [parent=#RANGE] Start
|
|
-- @param #RANGE self
|
|
|
|
--- Triggers the FSM event "Start" after a delay. Starts the RANGE. Initializes parameters and starts event handlers.
|
|
-- @function [parent=#RANGE] __Start
|
|
-- @param #RANGE self
|
|
-- @param #number delay Delay in seconds.
|
|
|
|
--- Triggers the FSM event "Stop". Stops the RANGE and all its event handlers.
|
|
-- @param #RANGE self
|
|
|
|
--- Triggers the FSM event "Stop" after a delay. Stops the RANGE and all its event handlers.
|
|
-- @function [parent=#RANGE] __Stop
|
|
-- @param #RANGE self
|
|
-- @param #number delay Delay in seconds.
|
|
|
|
--- Triggers the FSM event "Status".
|
|
-- @function [parent=#RANGE] Status
|
|
-- @param #RANGE self
|
|
|
|
--- Triggers the FSM event "Status" after a delay.
|
|
-- @function [parent=#RANGE] __Status
|
|
-- @param #RANGE self
|
|
-- @param #number delay Delay in seconds.
|
|
|
|
--- Triggers the FSM event "Impact".
|
|
-- @function [parent=#RANGE] Impact
|
|
-- @param #RANGE self
|
|
-- @param #RANGE.BombResult result Data of bombing run.
|
|
-- @param #RANGE.PlayerData player Data of player settings etc.
|
|
|
|
--- Triggers the FSM delayed event "Impact".
|
|
-- @function [parent=#RANGE] __Impact
|
|
-- @param #RANGE self
|
|
-- @param #number delay Delay in seconds before the function is called.
|
|
-- @param #RANGE.BombResult result Data of the bombing run.
|
|
-- @param #RANGE.PlayerData player Data of player settings etc.
|
|
|
|
--- On after "Impact" event user function. Called when a bomb/rocket/missile impacted.
|
|
-- @function [parent=#RANGE] OnAfterImpact
|
|
-- @param #RANGE self
|
|
-- @param #string From From state.
|
|
-- @param #string Event Event.
|
|
-- @param #string To To state.
|
|
-- @param #RANGE.BombResult result Data of the bombing run.
|
|
-- @param #RANGE.PlayerData player Data of player settings etc.
|
|
|
|
|
|
--- Triggers the FSM event "RollingIn".
|
|
-- @function [parent=#RANGE] RollingIn
|
|
-- @param #RANGE self
|
|
-- @param #RANGE.PlayerData player Data of player settings etc.
|
|
-- @param #RANGE.StrafeTarget target Strafe target.
|
|
|
|
--- On after "RollingIn" event user function. Called when a player rolls in to a strafe taret.
|
|
-- @function [parent=#RANGE] OnAfterRollingIn
|
|
-- @param #RANGE self
|
|
-- @param #string From From state.
|
|
-- @param #string Event Event.
|
|
-- @param #string To To state.
|
|
-- @param #RANGE.PlayerData player Data of player settings etc.
|
|
-- @param #RANGE.StrafeTarget target Strafe target.
|
|
|
|
--- Triggers the FSM event "StrafeResult".
|
|
-- @function [parent=#RANGE] StrafeResult
|
|
-- @param #RANGE self
|
|
-- @param #RANGE.PlayerData player Data of player settings etc.
|
|
-- @param #RANGE.StrafeResult result Data of the strafing run.
|
|
|
|
--- On after "StrafeResult" event user function. Called when a player finished a strafing run.
|
|
-- @function [parent=#RANGE] OnAfterStrafeResult
|
|
-- @param #RANGE self
|
|
-- @param #string From From state.
|
|
-- @param #string Event Event.
|
|
-- @param #string To To state.
|
|
-- @param #RANGE.PlayerData player Data of player settings etc.
|
|
-- @param #RANGE.StrafeResult result Data of the strafing run.
|
|
|
|
--- Triggers the FSM event "EnterRange".
|
|
-- @function [parent=#RANGE] EnterRange
|
|
-- @param #RANGE self
|
|
-- @param #RANGE.PlayerData player Data of player settings etc.
|
|
|
|
--- Triggers the FSM delayed event "EnterRange".
|
|
-- @function [parent=#RANGE] __EnterRange
|
|
-- @param #RANGE self
|
|
-- @param #number delay Delay in seconds before the function is called.
|
|
-- @param #RANGE.PlayerData player Data of player settings etc.
|
|
|
|
--- On after "EnterRange" event user function. Called when a player enters the range zone.
|
|
-- @function [parent=#RANGE] OnAfterEnterRange
|
|
-- @param #RANGE self
|
|
-- @param #string From From state.
|
|
-- @param #string Event Event.
|
|
-- @param #string To To state.
|
|
-- @param #RANGE.PlayerData player Data of player settings etc.
|
|
|
|
--- Triggers the FSM event "ExitRange".
|
|
-- @function [parent=#RANGE] ExitRange
|
|
-- @param #RANGE self
|
|
-- @param #RANGE.PlayerData player Data of player settings etc.
|
|
|
|
--- Triggers the FSM delayed event "ExitRange".
|
|
-- @function [parent=#RANGE] __ExitRange
|
|
-- @param #RANGE self
|
|
-- @param #number delay Delay in seconds before the function is called.
|
|
-- @param #RANGE.PlayerData player Data of player settings etc.
|
|
|
|
--- On after "ExitRange" event user function. Called when a player leaves the range zone.
|
|
-- @function [parent=#RANGE] OnAfterExitRange
|
|
-- @param #RANGE self
|
|
-- @param #string From From state.
|
|
-- @param #string Event Event.
|
|
-- @param #string To To state.
|
|
-- @param #RANGE.PlayerData player Data of player settings etc.
|
|
|
|
-- Return object.
|
|
return self
|
|
end
|
|
|
|
--- Initializes number of targets and location of the range. Starts the event handlers.
|
|
-- @param #RANGE self
|
|
-- @param #string From From state.
|
|
-- @param #string Event Event.
|
|
-- @param #string To To state.
|
|
function RANGE:onafterStart()
|
|
|
|
-- Location/coordinate of range.
|
|
local _location = nil
|
|
|
|
-- Count bomb targets.
|
|
local _count = 0
|
|
for _, _target in pairs( self.bombingTargets ) do
|
|
_count = _count + 1
|
|
|
|
-- Get range location.
|
|
if _location == nil then
|
|
_location = self:_GetBombTargetCoordinate( _target )
|
|
end
|
|
end
|
|
self.nbombtargets = _count
|
|
|
|
-- Count strafing targets.
|
|
_count = 0
|
|
for _, _target in pairs( self.strafeTargets ) do
|
|
_count = _count + 1
|
|
|
|
for _, _unit in pairs( _target.targets ) do
|
|
if _location == nil then
|
|
_location = _unit:GetCoordinate()
|
|
end
|
|
end
|
|
end
|
|
self.nstrafetargets = _count
|
|
|
|
-- Location of the range. We simply take the first unit/target we find if it was not explicitly specified by the user.
|
|
if self.location == nil then
|
|
self.location = _location
|
|
end
|
|
|
|
if self.location == nil then
|
|
local text = string.format( "ERROR! No range location found. Number of strafe targets = %d. Number of bomb targets = %d.", self.nstrafetargets, self.nbombtargets )
|
|
self:E( self.id .. text )
|
|
return
|
|
end
|
|
|
|
-- Define a MOOSE zone of the range.
|
|
if self.rangezone == nil then
|
|
self.rangezone = ZONE_RADIUS:New( self.rangename, { x = self.location.x, y = self.location.z }, self.rangeradius )
|
|
end
|
|
|
|
-- Starting range.
|
|
local text = string.format( "Starting RANGE %s. Number of strafe targets = %d. Number of bomb targets = %d.", self.rangename, self.nstrafetargets, self.nbombtargets )
|
|
self:I( self.id .. text )
|
|
|
|
-- Event handling.
|
|
if self.eventmoose then
|
|
-- Events are handled my MOOSE.
|
|
self:T( self.id .. "Events are handled by MOOSE." )
|
|
self:HandleEvent( EVENTS.Birth )
|
|
self:HandleEvent( EVENTS.Hit )
|
|
self:HandleEvent( EVENTS.Shot )
|
|
else
|
|
-- Events are handled directly by DCS.
|
|
self:T( self.id .. "Events are handled directly by DCS." )
|
|
world.addEventHandler( self )
|
|
end
|
|
|
|
-- Make bomb target move randomly within the range zone.
|
|
for _, _target in pairs( self.bombingTargets ) do
|
|
|
|
-- Check if it is a static object.
|
|
-- local _static=self:_CheckStatic(_target.target:GetName())
|
|
local _static = _target.type == RANGE.TargetType.STATIC
|
|
|
|
if _target.move and _static == false and _target.speed > 1 then
|
|
local unit = _target.target -- Wrapper.Unit#UNIT
|
|
_target.target:PatrolZones( { self.rangezone }, _target.speed * 0.75, "Off road" )
|
|
end
|
|
|
|
end
|
|
|
|
-- Init range control.
|
|
if self.rangecontrolfreq and not self.useSRS then
|
|
|
|
-- Radio queue.
|
|
self.rangecontrol = RADIOQUEUE:New( self.rangecontrolfreq, nil, self.rangename )
|
|
self.rangecontrol.schedonce = true
|
|
|
|
-- Init numbers.
|
|
self.rangecontrol:SetDigit( 0, RANGE.Sound.RC0.filename, RANGE.Sound.RC0.duration, self.soundpath )
|
|
self.rangecontrol:SetDigit( 1, RANGE.Sound.RC1.filename, RANGE.Sound.RC1.duration, self.soundpath )
|
|
self.rangecontrol:SetDigit( 2, RANGE.Sound.RC2.filename, RANGE.Sound.RC2.duration, self.soundpath )
|
|
self.rangecontrol:SetDigit( 3, RANGE.Sound.RC3.filename, RANGE.Sound.RC3.duration, self.soundpath )
|
|
self.rangecontrol:SetDigit( 4, RANGE.Sound.RC4.filename, RANGE.Sound.RC4.duration, self.soundpath )
|
|
self.rangecontrol:SetDigit( 5, RANGE.Sound.RC5.filename, RANGE.Sound.RC5.duration, self.soundpath )
|
|
self.rangecontrol:SetDigit( 6, RANGE.Sound.RC6.filename, RANGE.Sound.RC6.duration, self.soundpath )
|
|
self.rangecontrol:SetDigit( 7, RANGE.Sound.RC7.filename, RANGE.Sound.RC7.duration, self.soundpath )
|
|
self.rangecontrol:SetDigit( 8, RANGE.Sound.RC8.filename, RANGE.Sound.RC8.duration, self.soundpath )
|
|
self.rangecontrol:SetDigit( 9, RANGE.Sound.RC9.filename, RANGE.Sound.RC9.duration, self.soundpath )
|
|
|
|
-- Set location where the messages are transmitted from.
|
|
self.rangecontrol:SetSenderCoordinate( self.location )
|
|
self.rangecontrol:SetSenderUnitName( self.rangecontrolrelayname )
|
|
|
|
-- Start range control radio queue.
|
|
self.rangecontrol:Start( 1, 0.1 )
|
|
|
|
-- Init range control.
|
|
if self.instructorfreq and not self.useSRS then
|
|
|
|
-- Radio queue.
|
|
self.instructor = RADIOQUEUE:New( self.instructorfreq, nil, self.rangename )
|
|
self.instructor.schedonce = true
|
|
|
|
-- Init numbers.
|
|
self.instructor:SetDigit( 0, RANGE.Sound.IR0.filename, RANGE.Sound.IR0.duration, self.soundpath )
|
|
self.instructor:SetDigit( 1, RANGE.Sound.IR1.filename, RANGE.Sound.IR1.duration, self.soundpath )
|
|
self.instructor:SetDigit( 2, RANGE.Sound.IR2.filename, RANGE.Sound.IR2.duration, self.soundpath )
|
|
self.instructor:SetDigit( 3, RANGE.Sound.IR3.filename, RANGE.Sound.IR3.duration, self.soundpath )
|
|
self.instructor:SetDigit( 4, RANGE.Sound.IR4.filename, RANGE.Sound.IR4.duration, self.soundpath )
|
|
self.instructor:SetDigit( 5, RANGE.Sound.IR5.filename, RANGE.Sound.IR5.duration, self.soundpath )
|
|
self.instructor:SetDigit( 6, RANGE.Sound.IR6.filename, RANGE.Sound.IR6.duration, self.soundpath )
|
|
self.instructor:SetDigit( 7, RANGE.Sound.IR7.filename, RANGE.Sound.IR7.duration, self.soundpath )
|
|
self.instructor:SetDigit( 8, RANGE.Sound.IR8.filename, RANGE.Sound.IR8.duration, self.soundpath )
|
|
self.instructor:SetDigit( 9, RANGE.Sound.IR9.filename, RANGE.Sound.IR9.duration, self.soundpath )
|
|
|
|
-- Set location where the messages are transmitted from.
|
|
self.instructor:SetSenderCoordinate( self.location )
|
|
self.instructor:SetSenderUnitName( self.instructorrelayname )
|
|
|
|
-- Start instructor radio queue.
|
|
self.instructor:Start( 1, 0.1 )
|
|
|
|
end
|
|
|
|
end
|
|
|
|
-- Load prev results.
|
|
if self.autosave then
|
|
self:Load()
|
|
end
|
|
|
|
-- Debug mode: smoke all targets and range zone.
|
|
if self.Debug then
|
|
self:_MarkTargetsOnMap()
|
|
self:_SmokeBombTargets()
|
|
self:_SmokeStrafeTargets()
|
|
self:_SmokeStrafeTargetBoxes()
|
|
self.rangezone:SmokeZone( SMOKECOLOR.White )
|
|
end
|
|
|
|
self:__Status( -60 )
|
|
end
|
|
|
|
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
|
-- User Functions
|
|
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
|
|
|
--- Set maximal strafing altitude. Player entering a strafe pit above that altitude are not registered for a valid pass.
|
|
-- @param #RANGE self
|
|
-- @param #number maxalt Maximum altitude in meters AGL. Default is 914 m = 3000 ft.
|
|
-- @return #RANGE self
|
|
function RANGE:SetMaxStrafeAlt( maxalt )
|
|
self.strafemaxalt = maxalt or RANGE.Defaults.strafemaxalt
|
|
return self
|
|
end
|
|
|
|
--- Set time interval for tracking bombs. A smaller time step increases accuracy but needs more CPU time.
|
|
-- @param #RANGE self
|
|
-- @param #number dt Time interval in seconds. Default is 0.005 s.
|
|
-- @return #RANGE self
|
|
function RANGE:SetBombtrackTimestep( dt )
|
|
self.dtBombtrack = dt or RANGE.Defaults.dtBombtrack
|
|
return self
|
|
end
|
|
|
|
--- Set time how long (most) messages are displayed.
|
|
-- @param #RANGE self
|
|
-- @param #number time Time in seconds. Default is 30 s.
|
|
-- @return #RANGE self
|
|
function RANGE:SetMessageTimeDuration( time )
|
|
self.Tmsg = time or RANGE.Defaults.Tmsg
|
|
return self
|
|
end
|
|
|
|
--- Automatically save player results to disc.
|
|
-- @param #RANGE self
|
|
-- @return #RANGE self
|
|
function RANGE:SetAutosaveOn()
|
|
self.autosave = true
|
|
return self
|
|
end
|
|
|
|
--- Switch off auto save player results.
|
|
-- @param #RANGE self
|
|
-- @return #RANGE self
|
|
function RANGE:SetAutosaveOff()
|
|
self.autosave = false
|
|
return self
|
|
end
|
|
|
|
--- Enable saving of player's target sheets and specify an optional directory path.
|
|
-- @param #RANGE self
|
|
-- @param #string path (Optional) Path where to save the target sheets.
|
|
-- @param #string prefix (Optional) Prefix for target sheet files. File name will be saved as *prefix_aircrafttype-0001.csv*, *prefix_aircrafttype-0002.csv*, etc.
|
|
-- @return #RANGE self
|
|
function RANGE:SetTargetSheet( path, prefix )
|
|
if io then
|
|
self.targetsheet = true
|
|
self.targetpath = path
|
|
self.targetprefix = prefix
|
|
else
|
|
self:E( self.lid .. "ERROR: io is not desanitized. Cannot save target sheet." )
|
|
end
|
|
return self
|
|
end
|
|
|
|
--- Set FunkMan socket. Bombing and strafing results will be send to your Discord bot.
|
|
-- **Requires running FunkMan program**.
|
|
-- @param #RANGE self
|
|
-- @param #number Port Port. Default `10042`.
|
|
-- @param #string Host Host. Default "127.0.0.1".
|
|
-- @return #RANGE self
|
|
function RANGE:SetFunkManOn(Port, Host)
|
|
|
|
self.funkmanSocket=SOCKET:New(Port, Host)
|
|
|
|
return self
|
|
end
|
|
|
|
--- Set messages to examiner. The examiner will receive messages from all clients.
|
|
-- @param #RANGE self
|
|
-- @param #string examinergroupname Name of the group of the examiner.
|
|
-- @param #boolean exclusively If true, messages are send exclusively to the examiner, i.e. not to the clients.
|
|
-- @return #RANGE self
|
|
function RANGE:SetMessageToExaminer( examinergroupname, exclusively )
|
|
self.examinergroupname = examinergroupname
|
|
self.examinerexclusive = exclusively
|
|
return self
|
|
end
|
|
|
|
--- Set max number of player results that are displayed.
|
|
-- @param #RANGE self
|
|
-- @param #number nmax Number of results. Default is 10.
|
|
-- @return #RANGE self
|
|
function RANGE:SetDisplayedMaxPlayerResults( nmax )
|
|
self.ndisplayresult = nmax or RANGE.Defaults.ndisplayresult
|
|
return self
|
|
end
|
|
|
|
--- Set range radius. Defines the area in which e.g. bomb impacts are smoked.
|
|
-- @param #RANGE self
|
|
-- @param #number radius Radius in km. Default 5 km.
|
|
-- @return #RANGE self
|
|
function RANGE:SetRangeRadius( radius )
|
|
self.rangeradius = radius * 1000 or RANGE.Defaults.rangeradius
|
|
return self
|
|
end
|
|
|
|
--- Set player setting whether bomb impact points are smoked or not.
|
|
-- @param #RANGE self
|
|
-- @param #boolean switch If true nor nil default is to smoke impact points of bombs.
|
|
-- @return #RANGE self
|
|
function RANGE:SetDefaultPlayerSmokeBomb( switch )
|
|
if switch == true or switch == nil then
|
|
self.defaultsmokebomb = true
|
|
else
|
|
self.defaultsmokebomb = false
|
|
end
|
|
return self
|
|
end
|
|
|
|
--- Set bomb track threshold distance. Bombs/rockets/missiles are only tracked if player-range distance is less than this distance. Default 25 km.
|
|
-- @param #RANGE self
|
|
-- @param #number distance Threshold distance in km. Default 25 km.
|
|
-- @return #RANGE self
|
|
function RANGE:SetBombtrackThreshold( distance )
|
|
self.BombtrackThreshold = (distance or 25) * 1000
|
|
return self
|
|
end
|
|
|
|
--- Set range location. If this is not done, one (random) unit position of the range is used to determine the location of the range.
|
|
-- The range location determines the position at which the weather data is evaluated.
|
|
-- @param #RANGE self
|
|
-- @param Core.Point#COORDINATE coordinate Coordinate of the range.
|
|
-- @return #RANGE self
|
|
function RANGE:SetRangeLocation( coordinate )
|
|
self.location = coordinate
|
|
return self
|
|
end
|
|
|
|
--- Set range zone. For example, no bomb impact points are smoked if a bomb falls outside of this zone.
|
|
-- If a zone is not explicitly specified, the range zone is determined by its location and radius.
|
|
-- @param #RANGE self
|
|
-- @param Core.Zone#ZONE zone MOOSE zone defining the range perimeters.
|
|
-- @return #RANGE self
|
|
function RANGE:SetRangeZone( zone )
|
|
self.rangezone = zone
|
|
return self
|
|
end
|
|
|
|
--- Set smoke color for marking bomb targets. By default bomb targets are marked by red smoke.
|
|
-- @param #RANGE self
|
|
-- @param Utilities.Utils#SMOKECOLOR colorid Color id. Default SMOKECOLOR.Red.
|
|
-- @return #RANGE self
|
|
function RANGE:SetBombTargetSmokeColor( colorid )
|
|
self.BombSmokeColor = colorid or SMOKECOLOR.Red
|
|
return self
|
|
end
|
|
|
|
--- Set score bomb distance.
|
|
-- @param #RANGE self
|
|
-- @param #number distance Distance in meters. Default 1000 m.
|
|
-- @return #RANGE self
|
|
function RANGE:SetScoreBombDistance( distance )
|
|
self.scorebombdistance = distance or 1000
|
|
return self
|
|
end
|
|
|
|
--- Set smoke color for marking strafe targets. By default strafe targets are marked by green smoke.
|
|
-- @param #RANGE self
|
|
-- @param Utilities.Utils#SMOKECOLOR colorid Color id. Default SMOKECOLOR.Green.
|
|
-- @return #RANGE self
|
|
function RANGE:SetStrafeTargetSmokeColor( colorid )
|
|
self.StrafeSmokeColor = colorid or SMOKECOLOR.Green
|
|
return self
|
|
end
|
|
|
|
--- Set smoke color for marking strafe pit approach boxes. By default strafe pit boxes are marked by white smoke.
|
|
-- @param #RANGE self
|
|
-- @param Utilities.Utils#SMOKECOLOR colorid Color id. Default SMOKECOLOR.White.
|
|
-- @return #RANGE self
|
|
function RANGE:SetStrafePitSmokeColor( colorid )
|
|
self.StrafePitSmokeColor = colorid or SMOKECOLOR.White
|
|
return self
|
|
end
|
|
|
|
--- Set time delay between bomb impact and starting to smoke the impact point.
|
|
-- @param #RANGE self
|
|
-- @param #number delay Time delay in seconds. Default is 3 seconds.
|
|
-- @return #RANGE self
|
|
function RANGE:SetSmokeTimeDelay( delay )
|
|
self.TdelaySmoke = delay or RANGE.Defaults.TdelaySmoke
|
|
return self
|
|
end
|
|
|
|
--- Enable debug modus.
|
|
-- @param #RANGE self
|
|
-- @return #RANGE self
|
|
function RANGE:DebugON()
|
|
self.Debug = true
|
|
return self
|
|
end
|
|
|
|
--- Disable debug modus.
|
|
-- @param #RANGE self
|
|
-- @return #RANGE self
|
|
function RANGE:DebugOFF()
|
|
self.Debug = false
|
|
return self
|
|
end
|
|
|
|
--- Disable ALL messages to players.
|
|
-- @param #RANGE self
|
|
-- @return #RANGE self
|
|
function RANGE:SetMessagesOFF()
|
|
self.messages = false
|
|
return self
|
|
end
|
|
|
|
--- Enable messages to players. This is the default
|
|
-- @param #RANGE self
|
|
-- @return #RANGE self
|
|
function RANGE:SetMessagesON()
|
|
self.messages = true
|
|
return self
|
|
end
|
|
|
|
--- Enables tracking of all bomb types. Note that this is the default setting.
|
|
-- @param #RANGE self
|
|
-- @return #RANGE self
|
|
function RANGE:TrackBombsON()
|
|
self.trackbombs = true
|
|
return self
|
|
end
|
|
|
|
--- Disables tracking of all bomb types.
|
|
-- @param #RANGE self
|
|
-- @return #RANGE self
|
|
function RANGE:TrackBombsOFF()
|
|
self.trackbombs = false
|
|
return self
|
|
end
|
|
|
|
--- Enables tracking of all rocket types. Note that this is the default setting.
|
|
-- @param #RANGE self
|
|
-- @return #RANGE self
|
|
function RANGE:TrackRocketsON()
|
|
self.trackrockets = true
|
|
return self
|
|
end
|
|
|
|
--- Disables tracking of all rocket types.
|
|
-- @param #RANGE self
|
|
-- @return #RANGE self
|
|
function RANGE:TrackRocketsOFF()
|
|
self.trackrockets = false
|
|
return self
|
|
end
|
|
|
|
--- Enables tracking of all missile types. Note that this is the default setting.
|
|
-- @param #RANGE self
|
|
-- @return #RANGE self
|
|
function RANGE:TrackMissilesON()
|
|
self.trackmissiles = true
|
|
return self
|
|
end
|
|
|
|
--- Disables tracking of all missile types.
|
|
-- @param #RANGE self
|
|
-- @return #RANGE self
|
|
function RANGE:TrackMissilesOFF()
|
|
self.trackmissiles = false
|
|
return self
|
|
end
|
|
|
|
--- Use SRS Simple-Text-To-Speech for transmissions. No sound files necessary.
|
|
-- @param #RANGE self
|
|
-- @param #string PathToSRS Path to SRS directory.
|
|
-- @param #number Port SRS port. Default 5002.
|
|
-- @param #number Coalition Coalition side, e.g. coalition.side.BLUE or coalition.side.RED
|
|
-- @param #number Frequency Frequency to use, defaults to 256 (same as rangecontrol)
|
|
-- @param #number Modulation Modulation to use, defaults to radio.modulation.AM
|
|
-- @param #number Volume Volume, between 0.0 and 1.0. Defaults to 1.0
|
|
-- @param #string PathToGoogleKey Path to Google TTS credentials.
|
|
-- @return #RANGE self
|
|
function RANGE:SetSRS(PathToSRS, Port, Coalition, Frequency, Modulation, Volume, PathToGoogleKey)
|
|
if PathToSRS then
|
|
|
|
self.useSRS=true
|
|
|
|
self.controlmsrs=MSRS:New(PathToSRS, Frequency or 256, Modulation or radio.modulation.AM, Volume or 1.0)
|
|
self.controlmsrs:SetPort(Port)
|
|
self.controlmsrs:SetCoalition(Coalition or coalition.side.BLUE)
|
|
self.controlmsrs:SetLabel("RANGEC")
|
|
self.controlsrsQ = MSRSQUEUE:New("CONTROL")
|
|
|
|
self.instructmsrs=MSRS:New(PathToSRS, Frequency or 305, Modulation or radio.modulation.AM, Volume or 1.0)
|
|
self.instructmsrs:SetPort(Port)
|
|
self.instructmsrs:SetCoalition(Coalition or coalition.side.BLUE)
|
|
self.instructmsrs:SetLabel("RANGEI")
|
|
self.instructsrsQ = MSRSQUEUE:New("INSTRUCT")
|
|
|
|
else
|
|
self:E(self.lid..string.format("ERROR: No SRS path specified!"))
|
|
end
|
|
return self
|
|
end
|
|
|
|
--- (SRS) Set range control frequency and voice.
|
|
-- @param #RANGE self
|
|
-- @param #number frequency Frequency in MHz. Default 256 MHz.
|
|
-- @param #number modulation Modulation, defaults to radio.modulation.AM.
|
|
-- @param #string voice Voice.
|
|
-- @param #string culture Culture, defaults to "en-US".
|
|
-- @param #string gender Gender, defaults to "female".
|
|
-- @param #string relayunitname Name of the unit used for transmission location.
|
|
-- @return #RANGE self
|
|
function RANGE:SetSRSRangeControl( frequency, modulation, voice, culture, gender, relayunitname )
|
|
self.rangecontrolfreq = frequency or 256
|
|
self.controlmsrs:SetFrequencies(self.rangecontrolfreq)
|
|
self.controlmsrs:SetModulations(modulation or radio.modulation.AM)
|
|
self.controlmsrs:SetVoice(voice)
|
|
self.controlmsrs:SetCulture(culture or "en-US")
|
|
self.controlmsrs:SetGender(gender or "female")
|
|
self.rangecontrol = true
|
|
if relayunitname then
|
|
local unit = UNIT:FindByName(relayunitname)
|
|
local Coordinate = unit:GetCoordinate()
|
|
self.rangecontrolrelayname = relayunitname
|
|
end
|
|
return self
|
|
end
|
|
|
|
--- (SRS) Set range instructor frequency and voice.
|
|
-- @param #RANGE self
|
|
-- @param #number frequency Frequency in MHz. Default 305 MHz.
|
|
-- @param #number modulation Modulation, defaults to radio.modulation.AM.
|
|
-- @param #string voice Voice.
|
|
-- @param #string culture Culture, defaults to "en-US".
|
|
-- @param #string gender Gender, defaults to "male".
|
|
-- @param #string relayunitname Name of the unit used for transmission location.
|
|
-- @return #RANGE self
|
|
function RANGE:SetSRSRangeInstructor( frequency, modulation, voice, culture, gender, relayunitname )
|
|
self.instructorfreq = frequency or 305
|
|
self.instructmsrs:SetFrequencies(self.instructorfreq)
|
|
self.instructmsrs:SetModulations(modulation or radio.modulation.AM)
|
|
self.instructmsrs:SetVoice(voice)
|
|
self.instructmsrs:SetCulture(culture or "en-US")
|
|
self.instructmsrs:SetGender(gender or "male")
|
|
self.instructor = true
|
|
if relayunitname then
|
|
local unit = UNIT:FindByName(relayunitname)
|
|
local Coordinate = unit:GetCoordinate()
|
|
self.instructmsrs:SetCoordinate(Coordinate)
|
|
self.instructorrelayname = relayunitname
|
|
end
|
|
return self
|
|
end
|
|
|
|
--- Enable range control and set frequency (non-SRS).
|
|
-- @param #RANGE self
|
|
-- @param #number frequency Frequency in MHz. Default 256 MHz.
|
|
-- @param #string relayunitname Name of the unit used for transmission.
|
|
-- @return #RANGE self
|
|
function RANGE:SetRangeControl( frequency, relayunitname )
|
|
self.rangecontrolfreq = frequency or 256
|
|
self.rangecontrolrelayname = relayunitname
|
|
return self
|
|
end
|
|
|
|
--- Enable instructor radio and set frequency (non-SRS).
|
|
-- @param #RANGE self
|
|
-- @param #number frequency Frequency in MHz. Default 305 MHz.
|
|
-- @param #string relayunitname Name of the unit used for transmission.
|
|
-- @return #RANGE self
|
|
function RANGE:SetInstructorRadio( frequency, relayunitname )
|
|
self.instructorfreq = frequency or 305
|
|
self.instructorrelayname = relayunitname
|
|
return self
|
|
end
|
|
|
|
--- Set sound files folder within miz file.
|
|
-- @param #RANGE self
|
|
-- @param #string path Path for sound files. Default "Range Soundfiles/". Mind the slash "/" at the end!
|
|
-- @return #RANGE self
|
|
function RANGE:SetSoundfilesPath( path )
|
|
self.soundpath = tostring( path or "Range Soundfiles/" )
|
|
self:I( self.id .. string.format( "Setting sound files path to %s", self.soundpath ) )
|
|
return self
|
|
end
|
|
|
|
--- Add new strafe pit. For a strafe pit, hits from guns are counted. One pit can consist of several units.
|
|
-- A strafe run approach is only valid if the player enters via a zone in front of the pit, which is defined by boxlength, boxwidth, and heading.
|
|
-- Furthermore, the player must not be too high and fly in the direction of the pit to make a valid target apporoach.
|
|
-- @param #RANGE self
|
|
-- @param #table targetnames Single or multiple (Table) unit or static names defining the strafe targets. The first target in the list determines the approach box origin (heading and box).
|
|
-- @param #number boxlength (Optional) Length of the approach box in meters. Default is 3000 m.
|
|
-- @param #number boxwidth (Optional) Width of the approach box in meters. Default is 300 m.
|
|
-- @param #number heading (Optional) Approach box heading in degrees (originating FROM the target). Default is the heading set in the ME for the first target unit
|
|
-- @param #boolean inverseheading (Optional) Use inverse heading (heading --> heading - 180 Degrees). Default is false.
|
|
-- @param #number goodpass (Optional) Number of hits for a "good" strafing pass. Default is 20.
|
|
-- @param #number foulline (Optional) Foul line distance. Hits from closer than this distance are not counted. Default is 610 m = 2000 ft. Set to 0 for no foul line.
|
|
-- @return #RANGE self
|
|
function RANGE:AddStrafePit( targetnames, boxlength, boxwidth, heading, inverseheading, goodpass, foulline )
|
|
self:F( { targetnames = targetnames, boxlength = boxlength, boxwidth = boxwidth, heading = heading, inverseheading = inverseheading, goodpass = goodpass, foulline = foulline } )
|
|
|
|
-- Create table if necessary.
|
|
if type( targetnames ) ~= "table" then
|
|
targetnames = { targetnames }
|
|
end
|
|
|
|
-- Make targets
|
|
local _targets = {}
|
|
local center = nil -- Wrapper.Unit#UNIT
|
|
local ntargets = 0
|
|
|
|
for _i, _name in ipairs( targetnames ) do
|
|
|
|
-- Check if we have a static or unit object.
|
|
local _isstatic = self:_CheckStatic( _name )
|
|
|
|
local unit = nil
|
|
if _isstatic == true then
|
|
|
|
-- Add static object.
|
|
self:T( self.id .. string.format( "Adding STATIC object %s as strafe target #%d.", _name, _i ) )
|
|
unit = STATIC:FindByName( _name, false )
|
|
|
|
elseif _isstatic == false then
|
|
|
|
-- Add unit object.
|
|
self:T( self.id .. string.format( "Adding UNIT object %s as strafe target #%d.", _name, _i ) )
|
|
unit = UNIT:FindByName( _name )
|
|
|
|
else
|
|
|
|
-- Neither unit nor static object with this name could be found.
|
|
local text = string.format( "ERROR! Could not find ANY strafe target object with name %s.", _name )
|
|
self:E( self.id .. text )
|
|
|
|
end
|
|
|
|
-- Add object to targets.
|
|
if unit then
|
|
table.insert( _targets, unit )
|
|
-- Define center as the first unit we find
|
|
if center == nil then
|
|
center = unit
|
|
end
|
|
ntargets = ntargets + 1
|
|
end
|
|
|
|
end
|
|
|
|
-- Check if at least one target could be found.
|
|
if ntargets == 0 then
|
|
local text = string.format( "ERROR! No strafe target could be found when calling RANGE:AddStrafePit() for range %s", self.rangename )
|
|
self:E( self.id .. text )
|
|
return
|
|
end
|
|
|
|
-- Approach box dimensions.
|
|
local l = boxlength or RANGE.Defaults.boxlength
|
|
local w = (boxwidth or RANGE.Defaults.boxwidth) / 2
|
|
|
|
-- Heading: either manually entered or automatically taken from unit heading.
|
|
local heading = heading or center:GetHeading()
|
|
|
|
-- Invert the heading since some units point in the "wrong" direction. In particular the strafe pit from 476th range objects.
|
|
if inverseheading ~= nil then
|
|
if inverseheading then
|
|
heading = heading - 180
|
|
end
|
|
end
|
|
if heading < 0 then
|
|
heading = heading + 360
|
|
end
|
|
if heading > 360 then
|
|
heading = heading - 360
|
|
end
|
|
|
|
-- Number of hits called a "good" pass.
|
|
goodpass = goodpass or RANGE.Defaults.goodpass
|
|
|
|
-- Foule line distance.
|
|
foulline = foulline or RANGE.Defaults.foulline
|
|
|
|
-- Coordinate of the range.
|
|
local Ccenter = center:GetCoordinate()
|
|
|
|
-- Name of the target defined as its unit name.
|
|
local _name = center:GetName()
|
|
|
|
-- Points defining the approach area.
|
|
local p = {}
|
|
p[#p + 1] = Ccenter:Translate( w, heading + 90 )
|
|
p[#p + 1] = p[#p]:Translate( l, heading )
|
|
p[#p + 1] = p[#p]:Translate( 2 * w, heading - 90 )
|
|
p[#p + 1] = p[#p]:Translate( -l, heading )
|
|
|
|
local pv2 = {}
|
|
for i, p in ipairs( p ) do
|
|
pv2[i] = { x = p.x, y = p.z }
|
|
end
|
|
|
|
-- Create polygon zone.
|
|
local _polygon = ZONE_POLYGON_BASE:New( _name, pv2 )
|
|
|
|
-- Create tires
|
|
-- _polygon:BoundZone()
|
|
|
|
local st = {} -- #RANGE.StrafeTarget
|
|
st.name = _name
|
|
st.polygon = _polygon
|
|
st.coordinate = Ccenter
|
|
st.goodPass = goodpass
|
|
st.targets = _targets
|
|
st.foulline = foulline
|
|
st.smokepoints = p
|
|
st.heading = heading
|
|
|
|
-- Add zone to table.
|
|
table.insert( self.strafeTargets, st )
|
|
|
|
-- Debug info
|
|
local text = string.format( "Adding new strafe target %s with %d targets: heading = %03d, box_L = %.1f, box_W = %.1f, goodpass = %d, foul line = %.1f", _name, ntargets, heading, l, w, goodpass, foulline )
|
|
self:T( self.id .. text )
|
|
|
|
return self
|
|
end
|
|
|
|
--- Add all units of a group as one new strafe target pit.
|
|
-- For a strafe pit, hits from guns are counted. One pit can consist of several units.
|
|
-- Note, an approach is only valid, if the player enters via a zone in front of the pit, which defined by boxlength and boxheading.
|
|
-- Furthermore, the player must not be too high and fly in the direction of the pit to make a valid target apporoach.
|
|
-- @param #RANGE self
|
|
-- @param Wrapper.Group#GROUP group MOOSE group of unit names defining the strafe target pit. The first unit in the group determines the approach zone (heading and box).
|
|
-- @param #number boxlength (Optional) Length of the approach box in meters. Default is 3000 m.
|
|
-- @param #number boxwidth (Optional) Width of the approach box in meters. Default is 300 m.
|
|
-- @param #number heading (Optional) Approach heading in Degrees. Default is heading of the unit as defined in the mission editor.
|
|
-- @param #boolean inverseheading (Optional) Take inverse heading (heading --> heading - 180 Degrees). Default is false.
|
|
-- @param #number goodpass (Optional) Number of hits for a "good" strafing pass. Default is 20.
|
|
-- @param #number foulline (Optional) Foul line distance. Hits from closer than this distance are not counted. Default 610 m = 2000 ft. Set to 0 for no foul line.
|
|
-- @return #RANGE self
|
|
function RANGE:AddStrafePitGroup( group, boxlength, boxwidth, heading, inverseheading, goodpass, foulline )
|
|
self:F( { group = group, boxlength = boxlength, boxwidth = boxwidth, heading = heading, inverseheading = inverseheading, goodpass = goodpass, foulline = foulline } )
|
|
|
|
if group and group:IsAlive() then
|
|
|
|
-- Get units of group.
|
|
local _units = group:GetUnits()
|
|
|
|
-- Make table of unit names.
|
|
local _names = {}
|
|
for _, _unit in ipairs( _units ) do
|
|
|
|
local _unit = _unit -- Wrapper.Unit#UNIT
|
|
|
|
if _unit and _unit:IsAlive() then
|
|
local _name = _unit:GetName()
|
|
table.insert( _names, _name )
|
|
end
|
|
|
|
end
|
|
|
|
-- Add strafe pit.
|
|
self:AddStrafePit( _names, boxlength, boxwidth, heading, inverseheading, goodpass, foulline )
|
|
end
|
|
|
|
return self
|
|
end
|
|
|
|
--- Add bombing target(s) to range.
|
|
-- @param #RANGE self
|
|
-- @param #table targetnames Single or multiple (Table) names of unit or static objects serving as bomb targets.
|
|
-- @param #number goodhitrange (Optional) Max distance from target unit (in meters) which is considered as a good hit. Default is 25 m.
|
|
-- @param #boolean randommove If true, unit will move randomly within the range. Default is false.
|
|
-- @return #RANGE self
|
|
function RANGE:AddBombingTargets( targetnames, goodhitrange, randommove )
|
|
self:F( { targetnames = targetnames, goodhitrange = goodhitrange, randommove = randommove } )
|
|
|
|
-- Create a table if necessary.
|
|
if type( targetnames ) ~= "table" then
|
|
targetnames = { targetnames }
|
|
end
|
|
|
|
-- Default range is 25 m.
|
|
goodhitrange = goodhitrange or RANGE.Defaults.goodhitrange
|
|
|
|
for _, name in pairs( targetnames ) do
|
|
|
|
-- Check if we have a static or unit object.
|
|
local _isstatic = self:_CheckStatic( name )
|
|
|
|
if _isstatic == true then
|
|
local _static = STATIC:FindByName( name )
|
|
self:T2( self.id .. string.format( "Adding static bombing target %s with hit range %d.", name, goodhitrange, false ) )
|
|
self:AddBombingTargetUnit( _static, goodhitrange )
|
|
elseif _isstatic == false then
|
|
local _unit = UNIT:FindByName( name )
|
|
self:T2( self.id .. string.format( "Adding unit bombing target %s with hit range %d.", name, goodhitrange, randommove ) )
|
|
self:AddBombingTargetUnit( _unit, goodhitrange, randommove )
|
|
else
|
|
self:E( self.id .. string.format( "ERROR! Could not find bombing target %s.", name ) )
|
|
end
|
|
|
|
end
|
|
|
|
return self
|
|
end
|
|
|
|
--- Add a unit or static object as bombing target.
|
|
-- @param #RANGE self
|
|
-- @param Wrapper.Positionable#POSITIONABLE unit Positionable (unit or static) of the strafe target.
|
|
-- @param #number goodhitrange Max distance from unit which is considered as a good hit.
|
|
-- @param #boolean randommove If true, unit will move randomly within the range. Default is false.
|
|
-- @return #RANGE self
|
|
function RANGE:AddBombingTargetUnit( unit, goodhitrange, randommove )
|
|
self:F( { unit = unit, goodhitrange = goodhitrange, randommove = randommove } )
|
|
|
|
-- Get name of positionable.
|
|
local name = unit:GetName()
|
|
|
|
-- Check if we have a static or unit object.
|
|
local _isstatic = self:_CheckStatic( name )
|
|
|
|
-- Default range is 25 m.
|
|
goodhitrange = goodhitrange or RANGE.Defaults.goodhitrange
|
|
|
|
-- Set randommove to false if it was not specified.
|
|
if randommove == nil or _isstatic == true then
|
|
randommove = false
|
|
end
|
|
|
|
-- Debug or error output.
|
|
if _isstatic == true then
|
|
self:I( self.id .. string.format( "Adding STATIC bombing target %s with good hit range %d. Random move = %s.", name, goodhitrange, tostring( randommove ) ) )
|
|
elseif _isstatic == false then
|
|
self:I( self.id .. string.format( "Adding UNIT bombing target %s with good hit range %d. Random move = %s.", name, goodhitrange, tostring( randommove ) ) )
|
|
else
|
|
self:E( self.id .. string.format( "ERROR! No bombing target with name %s could be found. Carefully check all UNIT and STATIC names defined in the mission editor!", name ) )
|
|
end
|
|
|
|
-- Get max speed of unit in km/h.
|
|
local speed = 0
|
|
if _isstatic == false then
|
|
speed = self:_GetSpeed( unit )
|
|
end
|
|
|
|
local target = {} -- #RANGE.BombTarget
|
|
target.name = name
|
|
target.target = unit
|
|
target.goodhitrange = goodhitrange
|
|
target.move = randommove
|
|
target.speed = speed
|
|
target.coordinate = unit:GetCoordinate()
|
|
if _isstatic then
|
|
target.type = RANGE.TargetType.STATIC
|
|
else
|
|
target.type = RANGE.TargetType.UNIT
|
|
end
|
|
|
|
-- Insert target to table.
|
|
table.insert( self.bombingTargets, target )
|
|
|
|
return self
|
|
end
|
|
|
|
--- Add a coordinate of a bombing target. This
|
|
-- @param #RANGE self
|
|
-- @param Core.Point#COORDINATE coord The coordinate.
|
|
-- @param #string name Name of target.
|
|
-- @param #number goodhitrange Max distance from unit which is considered as a good hit.
|
|
-- @return #RANGE self
|
|
function RANGE:AddBombingTargetCoordinate( coord, name, goodhitrange )
|
|
|
|
local target = {} -- #RANGE.BombTarget
|
|
target.name = name or "Bomb Target"
|
|
target.target = nil
|
|
target.goodhitrange = goodhitrange or RANGE.Defaults.goodhitrange
|
|
target.move = false
|
|
target.speed = 0
|
|
target.coordinate = coord
|
|
target.type = RANGE.TargetType.COORD
|
|
|
|
-- Insert target to table.
|
|
table.insert( self.bombingTargets, target )
|
|
|
|
return self
|
|
end
|
|
|
|
--- Add all units of a group as bombing targets.
|
|
-- @param #RANGE self
|
|
-- @param Wrapper.Group#GROUP group Group of bombing targets.
|
|
-- @param #number goodhitrange Max distance from unit which is considered as a good hit.
|
|
-- @param #boolean randommove If true, unit will move randomly within the range. Default is false.
|
|
-- @return #RANGE self
|
|
function RANGE:AddBombingTargetGroup( group, goodhitrange, randommove )
|
|
self:F( { group = group, goodhitrange = goodhitrange, randommove = randommove } )
|
|
|
|
if group then
|
|
|
|
local _units = group:GetUnits()
|
|
|
|
for _, _unit in pairs( _units ) do
|
|
if _unit and _unit:IsAlive() then
|
|
self:AddBombingTargetUnit( _unit, goodhitrange, randommove )
|
|
end
|
|
end
|
|
end
|
|
|
|
return self
|
|
end
|
|
|
|
--- Measures the foule line distance between two unit or static objects.
|
|
-- @param #RANGE self
|
|
-- @param #string namepit Name of the strafe pit target object.
|
|
-- @param #string namefoulline Name of the fould line distance marker object.
|
|
-- @return #number Foul line distance in meters.
|
|
function RANGE:GetFoullineDistance( namepit, namefoulline )
|
|
self:F( { namepit = namepit, namefoulline = namefoulline } )
|
|
|
|
-- Check if we have units or statics.
|
|
local _staticpit = self:_CheckStatic( namepit )
|
|
local _staticfoul = self:_CheckStatic( namefoulline )
|
|
|
|
-- Get the unit or static pit object.
|
|
local pit = nil
|
|
if _staticpit == true then
|
|
pit = STATIC:FindByName( namepit, false )
|
|
elseif _staticpit == false then
|
|
pit = UNIT:FindByName( namepit )
|
|
else
|
|
self:E( self.id .. string.format( "ERROR! Pit object %s could not be found in GetFoullineDistance function. Check the name in the ME.", namepit ) )
|
|
end
|
|
|
|
-- Get the unit or static foul line object.
|
|
local foul = nil
|
|
if _staticfoul == true then
|
|
foul = STATIC:FindByName( namefoulline, false )
|
|
elseif _staticfoul == false then
|
|
foul = UNIT:FindByName( namefoulline )
|
|
else
|
|
self:E( self.id .. string.format( "ERROR! Foul line object %s could not be found in GetFoullineDistance function. Check the name in the ME.", namefoulline ) )
|
|
end
|
|
|
|
-- Get the distance between the two objects.
|
|
local fouldist = 0
|
|
if pit ~= nil and foul ~= nil then
|
|
fouldist = pit:GetCoordinate():Get2DDistance( foul:GetCoordinate() )
|
|
else
|
|
self:E( self.id .. string.format( "ERROR! Foul line distance could not be determined. Check pit object name %s and foul line object name %s in the ME.", namepit, namefoulline ) )
|
|
end
|
|
|
|
self:T( self.id .. string.format( "Foul line distance = %.1f m.", fouldist ) )
|
|
return fouldist
|
|
end
|
|
|
|
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
|
-- Event Handling
|
|
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
|
|
|
--- General event handler.
|
|
-- @param #RANGE self
|
|
-- @param #table Event DCS event table.
|
|
function RANGE:onEvent( Event )
|
|
self:F3( Event )
|
|
|
|
if Event == nil or Event.initiator == nil then
|
|
self:T3( "Skipping onEvent. Event or Event.initiator unknown." )
|
|
return true
|
|
end
|
|
if Unit.getByName( Event.initiator:getName() ) == nil then
|
|
self:T3( "Skipping onEvent. Initiator unit name unknown." )
|
|
return true
|
|
end
|
|
|
|
local DCSiniunit = Event.initiator
|
|
local DCStgtunit = Event.target
|
|
local DCSweapon = Event.weapon
|
|
|
|
local EventData = {}
|
|
local _playerunit = nil
|
|
local _playername = nil
|
|
|
|
if Event.initiator then
|
|
EventData.IniUnitName = Event.initiator:getName()
|
|
EventData.IniDCSGroup = Event.initiator:getGroup()
|
|
EventData.IniGroupName = Event.initiator:getGroup():getName()
|
|
-- Get player unit and name. This returns nil,nil if the event was not fired by a player unit. And these are the only events we are interested in.
|
|
_playerunit, _playername = self:_GetPlayerUnitAndName( EventData.IniUnitName )
|
|
end
|
|
|
|
if Event.target then
|
|
EventData.TgtUnitName = Event.target:getName()
|
|
EventData.TgtUnit = UNIT:FindByName( EventData.TgtUnitName )
|
|
end
|
|
|
|
if Event.weapon then
|
|
EventData.Weapon = Event.weapon
|
|
EventData.weapon = Event.weapon
|
|
EventData.WeaponTypeName = Event.weapon:getTypeName()
|
|
end
|
|
|
|
-- Event info.
|
|
self:T3( self.id .. string.format( "EVENT: Event in onEvent with ID = %s", tostring( Event.id ) ) )
|
|
self:T3( self.id .. string.format( "EVENT: Ini unit = %s", tostring( EventData.IniUnitName ) ) )
|
|
self:T3( self.id .. string.format( "EVENT: Ini group = %s", tostring( EventData.IniGroupName ) ) )
|
|
self:T3( self.id .. string.format( "EVENT: Ini player = %s", tostring( _playername ) ) )
|
|
self:T3( self.id .. string.format( "EVENT: Tgt unit = %s", tostring( EventData.TgtUnitName ) ) )
|
|
self:T3( self.id .. string.format( "EVENT: Wpn type = %s", tostring( EventData.WeaponTypeName ) ) )
|
|
|
|
-- Call event Birth function.
|
|
if Event.id == world.event.S_EVENT_BIRTH and _playername then
|
|
self:OnEventBirth( EventData )
|
|
end
|
|
|
|
-- Call event Shot function.
|
|
if Event.id == world.event.S_EVENT_SHOT and _playername and Event.weapon then
|
|
self:OnEventShot( EventData )
|
|
end
|
|
|
|
-- Call event Hit function.
|
|
if Event.id == world.event.S_EVENT_HIT and _playername and DCStgtunit then
|
|
self:OnEventHit( EventData )
|
|
end
|
|
|
|
end
|
|
|
|
--- Range event handler for event birth.
|
|
-- @param #RANGE self
|
|
-- @param Core.Event#EVENTDATA EventData
|
|
function RANGE:OnEventBirth( EventData )
|
|
self:F( { eventbirth = EventData } )
|
|
|
|
local _unitName = EventData.IniUnitName
|
|
local _unit, _playername = self:_GetPlayerUnitAndName( _unitName )
|
|
|
|
self:T3( self.id .. "BIRTH: unit = " .. tostring( EventData.IniUnitName ) )
|
|
self:T3( self.id .. "BIRTH: group = " .. tostring( EventData.IniGroupName ) )
|
|
self:T3( self.id .. "BIRTH: player = " .. tostring( _playername ) )
|
|
|
|
if _unit and _playername then
|
|
|
|
local _uid = _unit:GetID()
|
|
local _group = _unit:GetGroup()
|
|
local _gid = _group:GetID()
|
|
local _callsign = _unit:GetCallsign()
|
|
|
|
-- Debug output.
|
|
local text = string.format( "Player %s, callsign %s entered unit %s (UID %d) of group %s (GID %d)", _playername, _callsign, _unitName, _uid, _group:GetName(), _gid )
|
|
self:T( self.id .. text )
|
|
|
|
-- Reset current strafe status.
|
|
self.strafeStatus[_uid] = nil
|
|
|
|
-- Add Menu commands after a delay of 0.1 seconds.
|
|
self:ScheduleOnce( 0.1, self._AddF10Commands, self, _unitName )
|
|
|
|
-- By default, some bomb impact points and do not flare each hit on target.
|
|
self.PlayerSettings[_playername] = {} -- #RANGE.PlayerData
|
|
self.PlayerSettings[_playername].smokebombimpact = self.defaultsmokebomb
|
|
self.PlayerSettings[_playername].flaredirecthits = false
|
|
self.PlayerSettings[_playername].smokecolor = SMOKECOLOR.Blue
|
|
self.PlayerSettings[_playername].flarecolor = FLARECOLOR.Red
|
|
self.PlayerSettings[_playername].delaysmoke = true
|
|
self.PlayerSettings[_playername].messages = true
|
|
self.PlayerSettings[_playername].client = CLIENT:FindByName( _unitName, nil, true )
|
|
self.PlayerSettings[_playername].unitname = _unitName
|
|
self.PlayerSettings[_playername].playername = _playername
|
|
self.PlayerSettings[_playername].airframe = EventData.IniUnit:GetTypeName()
|
|
self.PlayerSettings[_playername].inzone = false
|
|
|
|
-- Start check in zone timer.
|
|
if self.planes[_uid] ~= true then
|
|
self.timerCheckZone = TIMER:New( self._CheckInZone, self, EventData.IniUnitName ):Start( 1, 1 )
|
|
self.planes[_uid] = true
|
|
end
|
|
|
|
end
|
|
end
|
|
|
|
--- Range event handler for event hit.
|
|
-- @param #RANGE self
|
|
-- @param Core.Event#EVENTDATA EventData
|
|
function RANGE:OnEventHit( EventData )
|
|
self:F( { eventhit = EventData } )
|
|
|
|
-- Debug info.
|
|
self:T3( self.id .. "HIT: Ini unit = " .. tostring( EventData.IniUnitName ) )
|
|
self:T3( self.id .. "HIT: Ini group = " .. tostring( EventData.IniGroupName ) )
|
|
self:T3( self.id .. "HIT: Tgt target = " .. tostring( EventData.TgtUnitName ) )
|
|
|
|
-- Player info
|
|
local _unitName = EventData.IniUnitName
|
|
local _unit, _playername = self:_GetPlayerUnitAndName( _unitName )
|
|
if _unit == nil or _playername == nil then
|
|
return
|
|
end
|
|
|
|
-- Unit ID
|
|
local _unitID = _unit:GetID()
|
|
|
|
-- Target
|
|
local target = EventData.TgtUnit
|
|
local targetname = EventData.TgtUnitName
|
|
|
|
-- Current strafe target of player.
|
|
local _currentTarget = self.strafeStatus[_unitID] --#RANGE.StrafeStatus
|
|
|
|
-- Player has rolled in on a strafing target.
|
|
if _currentTarget and target:IsAlive() then
|
|
|
|
local playerPos = _unit:GetCoordinate()
|
|
local targetPos = target:GetCoordinate()
|
|
|
|
-- Loop over valid targets for this run.
|
|
for _, _target in pairs( _currentTarget.zone.targets ) do
|
|
|
|
-- Check the the target is the same that was actually hit.
|
|
if _target and _target:IsAlive() and _target:GetName() == targetname then
|
|
|
|
-- Get distance between player and target.
|
|
local dist = playerPos:Get2DDistance( targetPos )
|
|
|
|
if dist > _currentTarget.zone.foulline then
|
|
-- Increase hit counter of this run.
|
|
_currentTarget.hits = _currentTarget.hits + 1
|
|
|
|
-- Flare target.
|
|
if _unit and _playername and self.PlayerSettings[_playername].flaredirecthits then
|
|
targetPos:Flare( self.PlayerSettings[_playername].flarecolor )
|
|
end
|
|
else
|
|
-- Too close to the target.
|
|
if _currentTarget.pastfoulline == false and _unit and _playername then
|
|
local _d = _currentTarget.zone.foulline
|
|
-- DONE - SRS output
|
|
local text = string.format( "%s, Invalid hit!\nYou already passed foul line distance of %d m for target %s.", self:_myname( _unitName ), _d, targetname )
|
|
if self.useSRS then
|
|
local ttstext = string.format( "%s, Invalid hit! You already passed foul line distance of %d meters for target %s.", self:_myname( _unitName ), _d, targetname )
|
|
self.controlsrsQ:NewTransmission(ttstext,nil,self.controlmsrs,nil,2)
|
|
end
|
|
self:_DisplayMessageToGroup( _unit, text )
|
|
self:T2( self.id .. text )
|
|
_currentTarget.pastfoulline = true
|
|
end
|
|
end
|
|
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Bombing Targets
|
|
for _, _bombtarget in pairs( self.bombingTargets ) do
|
|
|
|
local _target = _bombtarget.target -- Wrapper.Positionable#POSITIONABLE
|
|
|
|
-- Check if one of the bomb targets was hit.
|
|
if _target and _target:IsAlive() and _bombtarget.name == targetname then
|
|
|
|
if _unit and _playername then
|
|
|
|
-- Flare target.
|
|
if self.PlayerSettings[_playername].flaredirecthits then
|
|
|
|
-- Position of target.
|
|
local targetPos = _target:GetCoordinate()
|
|
|
|
targetPos:Flare( self.PlayerSettings[_playername].flarecolor )
|
|
end
|
|
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
--- Range event handler for event shot (when a unit releases a rocket or bomb (but not a fast firing gun).
|
|
-- @param #RANGE self
|
|
-- @param #table weapon Weapon
|
|
function RANGE:_TrackWeapon(weapon)
|
|
|
|
end
|
|
|
|
--- Range event handler for event shot (when a unit releases a rocket or bomb (but not a fast firing gun).
|
|
-- @param #RANGE self
|
|
-- @param Core.Event#EVENTDATA EventData
|
|
function RANGE:OnEventShot( EventData )
|
|
self:F( { eventshot = EventData } )
|
|
|
|
-- Nil checks.
|
|
if EventData.Weapon == nil then
|
|
return
|
|
end
|
|
if EventData.IniDCSUnit == nil then
|
|
return
|
|
end
|
|
|
|
if EventData.IniPlayerName == nil then
|
|
return
|
|
end
|
|
|
|
-- Weapon data.
|
|
local _weapon = EventData.Weapon:getTypeName() -- should be the same as Event.WeaponTypeName
|
|
local _weaponStrArray = UTILS.Split( _weapon, "%." )
|
|
local _weaponName = _weaponStrArray[#_weaponStrArray]
|
|
|
|
-- Weapon descriptor.
|
|
local desc = EventData.Weapon:getDesc()
|
|
|
|
-- Weapon category: 0=SHELL, 1=MISSILE, 2=ROCKET, 3=BOMB (Weapon.Category.X)
|
|
local weaponcategory = desc.category
|
|
|
|
-- Debug info.
|
|
self:T( self.id .. "EVENT SHOT: Range " .. self.rangename )
|
|
self:T( self.id .. "EVENT SHOT: Ini unit = " .. EventData.IniUnitName )
|
|
self:T( self.id .. "EVENT SHOT: Ini group = " .. EventData.IniGroupName )
|
|
self:T( self.id .. "EVENT SHOT: Weapon type = " .. _weapon )
|
|
self:T( self.id .. "EVENT SHOT: Weapon name = " .. _weaponName )
|
|
self:T( self.id .. "EVENT SHOT: Weapon cate = " .. weaponcategory )
|
|
|
|
-- Tracking conditions for bombs, rockets and missiles.
|
|
local _bombs = weaponcategory == Weapon.Category.BOMB -- string.match(_weapon, "weapons.bombs")
|
|
local _rockets = weaponcategory == Weapon.Category.ROCKET -- string.match(_weapon, "weapons.nurs")
|
|
local _missiles = weaponcategory == Weapon.Category.MISSILE -- string.match(_weapon, "weapons.missiles") or _viggen
|
|
|
|
-- Check if any condition applies here.
|
|
local _track = (_bombs and self.trackbombs) or (_rockets and self.trackrockets) or (_missiles and self.trackmissiles)
|
|
|
|
-- Get unit name.
|
|
local _unitName = EventData.IniUnitName
|
|
|
|
-- Get player unit and name.
|
|
local _unit, _playername = self:_GetPlayerUnitAndName( _unitName )
|
|
|
|
-- Attack parameters.
|
|
local attackHdg=_unit:GetHeading()
|
|
local attackAlt=_unit:GetHeight()
|
|
local attackVel=_unit:GetVelocityKNOTS()
|
|
|
|
-- Set this to larger value than the threshold.
|
|
local dPR = self.BombtrackThreshold * 2
|
|
|
|
-- Distance player to range.
|
|
if _unit and _playername then
|
|
dPR = _unit:GetCoordinate():Get2DDistance( self.location )
|
|
self:T( self.id .. string.format( "Range %s, player %s, player-range distance = %d km.", self.rangename, _playername, dPR / 1000 ) )
|
|
end
|
|
|
|
-- Only track if distance player to range is < 25 km. Also check that a player shot. No need to track AI weapons.
|
|
if _track and dPR <= self.BombtrackThreshold and _unit and _playername then
|
|
|
|
-- Player data.
|
|
local playerData = self.PlayerSettings[_playername] -- #RANGE.PlayerData
|
|
|
|
-- Tracking info and init of last bomb position.
|
|
self:T( self.id .. string.format( "RANGE %s: Tracking %s - %s.", self.rangename, _weapon, EventData.weapon:getName() ) )
|
|
|
|
-- Init bomb position.
|
|
local _lastBombPos = { x = 0, y = 0, z = 0 } -- DCS#Vec3
|
|
|
|
-- Function monitoring the position of a bomb until impact.
|
|
local function trackBomb( _ordnance )
|
|
|
|
-- When the pcall returns a failure the weapon has hit.
|
|
local _status, _bombPos = pcall( function()
|
|
return _ordnance:getPoint()
|
|
end )
|
|
|
|
self:T2( self.id .. string.format( "Range %s: Bomb still in air: %s", self.rangename, tostring( _status ) ) )
|
|
if _status then
|
|
|
|
----------------------------
|
|
-- Weapon is still in air --
|
|
----------------------------
|
|
|
|
-- Remember this position.
|
|
_lastBombPos = { x = _bombPos.x, y = _bombPos.y, z = _bombPos.z }
|
|
|
|
-- Check again in ~0.005 seconds ==> 200 checks per second.
|
|
return timer.getTime() + self.dtBombtrack
|
|
else
|
|
|
|
-----------------------------
|
|
-- Bomb did hit the ground --
|
|
-----------------------------
|
|
|
|
-- Get closet target to last position.
|
|
local _closetTarget = nil -- #RANGE.BombTarget
|
|
local _distance = nil
|
|
local _closeCoord = nil --Core.Point#COORDINATE
|
|
local _hitquality = "POOR"
|
|
|
|
-- Get callsign.
|
|
local _callsign = self:_myname( _unitName )
|
|
|
|
-- Coordinate of impact point.
|
|
local impactcoord = COORDINATE:NewFromVec3( _lastBombPos )
|
|
|
|
-- Check if impact happened in range zone.
|
|
local insidezone = self.rangezone:IsCoordinateInZone( impactcoord )
|
|
|
|
-- Impact point of bomb.
|
|
if self.Debug then
|
|
impactcoord:MarkToAll( "Bomb impact point" )
|
|
end
|
|
|
|
-- Smoke impact point of bomb.
|
|
if playerData.smokebombimpact and insidezone then
|
|
if playerData.delaysmoke then
|
|
timer.scheduleFunction( self._DelayedSmoke, { coord = impactcoord, color = playerData.smokecolor }, timer.getTime() + self.TdelaySmoke )
|
|
else
|
|
impactcoord:Smoke( playerData.smokecolor )
|
|
end
|
|
end
|
|
|
|
-- Loop over defined bombing targets.
|
|
for _, _bombtarget in pairs( self.bombingTargets ) do
|
|
local bombtarget=_bombtarget --#RANGE.BombTarget
|
|
|
|
-- Get target coordinate.
|
|
local targetcoord = self:_GetBombTargetCoordinate( _bombtarget )
|
|
|
|
if targetcoord then
|
|
|
|
-- Distance between bomb and target.
|
|
local _temp = impactcoord:Get2DDistance( targetcoord )
|
|
|
|
-- Find closest target to last known position of the bomb.
|
|
if _distance == nil or _temp < _distance then
|
|
_distance = _temp
|
|
_closetTarget = bombtarget
|
|
_closeCoord = targetcoord
|
|
if _distance <= 1.53 then -- Rangeboss Edit
|
|
_hitquality = "SHACK" -- Rangeboss Edit
|
|
elseif _distance <= 0.5 * bombtarget.goodhitrange then -- Rangeboss Edit
|
|
_hitquality = "EXCELLENT"
|
|
elseif _distance <= bombtarget.goodhitrange then
|
|
_hitquality = "GOOD"
|
|
elseif _distance <= 2 * bombtarget.goodhitrange then
|
|
_hitquality = "INEFFECTIVE"
|
|
else
|
|
_hitquality = "POOR"
|
|
end
|
|
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Count if bomb fell less than ~1 km away from the target.
|
|
if _distance and _distance <= self.scorebombdistance then
|
|
-- Init bomb player results.
|
|
if not self.bombPlayerResults[_playername] then
|
|
self.bombPlayerResults[_playername] = {}
|
|
end
|
|
|
|
-- Local results.
|
|
local _results = self.bombPlayerResults[_playername]
|
|
|
|
local result = {} -- #RANGE.BombResult
|
|
result.command=SOCKET.DataType.BOMBRESULT
|
|
result.name = _closetTarget.name or "unknown"
|
|
result.distance = _distance
|
|
result.radial = _closeCoord:HeadingTo( impactcoord )
|
|
result.weapon = _weaponName or "unknown"
|
|
result.quality = _hitquality
|
|
result.player = playerData.playername
|
|
result.time = timer.getAbsTime()
|
|
result.clock = UTILS.SecondsToClock(result.time, true)
|
|
result.midate = UTILS.GetDCSMissionDate()
|
|
result.theatre = env.mission.theatre
|
|
result.airframe = playerData.airframe
|
|
result.roundsFired = 0 -- Rangeboss Edit
|
|
result.roundsHit = 0 -- Rangeboss Edit
|
|
result.roundsQuality = "N/A" -- Rangeboss Edit
|
|
result.rangename = self.rangename
|
|
result.attackHdg = attackHdg
|
|
result.attackVel = attackVel
|
|
result.attackAlt = attackAlt
|
|
|
|
-- Add to table.
|
|
table.insert( _results, result )
|
|
|
|
-- Call impact.
|
|
self:Impact( result, playerData )
|
|
|
|
elseif insidezone then
|
|
|
|
-- Send message.
|
|
-- DONE SRS message
|
|
local _message = string.format( "%s, weapon impacted too far from nearest range target (>%.1f km). No score!", _callsign, self.scorebombdistance / 1000 )
|
|
if self.useSRS then
|
|
local ttstext = string.format( "%s, weapon impacted too far from nearest range target, mor than %.1f kilometer. No score!", _callsign, self.scorebombdistance / 1000 )
|
|
self.controlsrsQ:NewTransmission(ttstext,nil,self.controlmsrs,nil,2)
|
|
end
|
|
self:_DisplayMessageToGroup( _unit, _message, nil, false )
|
|
|
|
if self.rangecontrol then
|
|
-- weapon impacted too far from the nearest target! No Score!
|
|
if self.useSRS then
|
|
self.controlsrsQ:NewTransmission(_message,nil,self.controlmsrs,nil,1)
|
|
else
|
|
self.rangecontrol:NewTransmission( RANGE.Sound.RCWeaponImpactedTooFar.filename, RANGE.Sound.RCWeaponImpactedTooFar.duration, self.soundpath, nil, nil, _message, self.subduration )
|
|
end
|
|
end
|
|
|
|
else
|
|
self:T( self.id .. "Weapon impacted outside range zone." )
|
|
end
|
|
|
|
-- Terminate the timer
|
|
self:T( self.id .. string.format( "Range %s, player %s: Terminating bomb track timer.", self.rangename, _playername ) )
|
|
return nil
|
|
|
|
end -- _status check
|
|
|
|
end -- end function trackBomb
|
|
|
|
-- Weapon is not yet "alife" just yet. Start timer in one second.
|
|
self:T( self.id .. string.format( "Range %s, player %s: Tracking of weapon starts in 0.1 seconds.", self.rangename, _playername ) )
|
|
timer.scheduleFunction( trackBomb, EventData.weapon, timer.getTime() + 0.1 )
|
|
|
|
end -- if _track (string.match) and player-range distance < threshold.
|
|
|
|
end
|
|
|
|
-----------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
|
-- FSM Functions
|
|
-----------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
|
|
|
--- Check spawn queue and spawn aircraft if necessary.
|
|
-- @param #RANGE self
|
|
-- @param #string From From state.
|
|
-- @param #string Event Event.
|
|
-- @param #string To To state.
|
|
function RANGE:onafterStatus( From, Event, To )
|
|
|
|
if self.verbose > 0 then
|
|
|
|
local fsmstate = self:GetState()
|
|
|
|
local text = string.format( "Range status: %s", fsmstate )
|
|
|
|
if self.instructor then
|
|
local alive = "N/A"
|
|
if self.instructorrelayname then
|
|
local relay = UNIT:FindByName( self.instructorrelayname )
|
|
if relay then
|
|
alive = tostring( relay:IsAlive() )
|
|
end
|
|
end
|
|
text = text .. string.format( ", Instructor %.3f MHz (Relay=%s alive=%s)", self.instructorfreq, tostring( self.instructorrelayname ), alive )
|
|
end
|
|
|
|
if self.rangecontrol then
|
|
local alive = "N/A"
|
|
if self.rangecontrolrelayname then
|
|
local relay = UNIT:FindByName( self.rangecontrolrelayname )
|
|
if relay then
|
|
alive = tostring( relay:IsAlive() )
|
|
end
|
|
end
|
|
text = text .. string.format( ", Control %.3f MHz (Relay=%s alive=%s)", self.rangecontrolfreq, tostring( self.rangecontrolrelayname ), alive )
|
|
end
|
|
|
|
-- Check range status.
|
|
self:I( self.id .. text )
|
|
|
|
end
|
|
|
|
-- Check player status.
|
|
self:_CheckPlayers()
|
|
|
|
-- Check back in ~10 seconds.
|
|
self:__Status( -10 )
|
|
end
|
|
|
|
--- Function called after player enters the range zone.
|
|
-- @param #RANGE self
|
|
-- @param #string From From state.
|
|
-- @param #string Event Event.
|
|
-- @param #string To To state.
|
|
-- @param #RANGE.PlayerData player Player data.
|
|
function RANGE:onafterEnterRange( From, Event, To, player )
|
|
|
|
if self.instructor and self.rangecontrol then
|
|
|
|
if self.useSRS then
|
|
local text = string.format("You entered the bombing range. For hit assessment, contact the range controller at %.3f MHz", self.rangecontrolfreq)
|
|
local ttstext = string.format("You entered the bombing range. For hit assessment, contact the range controller at %.3f mega hertz.", self.rangecontrolfreq)
|
|
local group = player.client:GetGroup()
|
|
self.instructsrsQ:NewTransmission(ttstext,nil,self.instructmsrs,nil,1,{group},text,10)
|
|
else
|
|
-- Range control radio frequency split.
|
|
local RF = UTILS.Split( string.format( "%.3f", self.rangecontrolfreq ), "." )
|
|
|
|
-- Radio message that player entered the range
|
|
-- You entered the bombing range. For hit assessment, contact the range controller at xy MHz
|
|
self.instructor:NewTransmission( RANGE.Sound.IREnterRange.filename, RANGE.Sound.IREnterRange.duration, self.soundpath )
|
|
self.instructor:Number2Transmission( RF[1] )
|
|
if tonumber( RF[2] ) > 0 then
|
|
self.instructor:NewTransmission( RANGE.Sound.IRDecimal.filename, RANGE.Sound.IRDecimal.duration, self.soundpath )
|
|
self.instructor:Number2Transmission( RF[2] )
|
|
end
|
|
self.instructor:NewTransmission( RANGE.Sound.IRMegaHertz.filename, RANGE.Sound.IRMegaHertz.duration, self.soundpath )
|
|
end
|
|
end
|
|
|
|
end
|
|
|
|
--- Function called after player leaves the range zone.
|
|
-- @param #RANGE self
|
|
-- @param #string From From state.
|
|
-- @param #string Event Event.
|
|
-- @param #string To To state.
|
|
-- @param #RANGE.PlayerData player Player data.
|
|
function RANGE:onafterExitRange( From, Event, To, player )
|
|
|
|
if self.instructor then
|
|
-- You left the bombing range zone. Have a nice day!
|
|
if self.useSRS then
|
|
local text = "You left the bombing range zone. Have a nice day!"
|
|
local group = player.client:GetGroup()
|
|
self.instructsrsQ:NewTransmission(text,nil,self.instructmsrs,nil,1,{group},text,10)
|
|
else
|
|
self.instructor:NewTransmission( RANGE.Sound.IRExitRange.filename, RANGE.Sound.IRExitRange.duration, self.soundpath )
|
|
end
|
|
end
|
|
|
|
end
|
|
|
|
--- Function called after bomb impact on range.
|
|
-- @param #RANGE self
|
|
-- @param #string From From state.
|
|
-- @param #string Event Event.
|
|
-- @param #string To To state.
|
|
-- @param #RANGE.BombResult result Result of bomb impact.
|
|
-- @param #RANGE.PlayerData player Player data table.
|
|
function RANGE:onafterImpact( From, Event, To, result, player )
|
|
|
|
-- Only display target name if there is more than one bomb target.
|
|
local targetname = nil
|
|
if #self.bombingTargets > 1 then
|
|
targetname = result.name
|
|
end
|
|
|
|
-- Send message to player.
|
|
local text = string.format( "%s, impact %03d° for %d ft", player.playername, result.radial, UTILS.MetersToFeet( result.distance ) )
|
|
if targetname then
|
|
text = text .. string.format( " from bulls of target %s.", targetname )
|
|
else
|
|
text = text .. "."
|
|
end
|
|
text = text .. string.format( " %s hit.", result.quality )
|
|
|
|
if self.rangecontrol then
|
|
|
|
if self.useSRS then
|
|
local group = player.client:GetGroup()
|
|
self.controlsrsQ:NewTransmission(text,nil,self.controlmsrs,nil,1,{group},text,10)
|
|
else
|
|
self.rangecontrol:NewTransmission( RANGE.Sound.RCImpact.filename, RANGE.Sound.RCImpact.duration, self.soundpath, nil, nil, text, self.subduration )
|
|
self.rangecontrol:Number2Transmission( string.format( "%03d", result.radial ), nil, 0.1 )
|
|
self.rangecontrol:NewTransmission( RANGE.Sound.RCDegrees.filename, RANGE.Sound.RCDegrees.duration, self.soundpath )
|
|
self.rangecontrol:NewTransmission( RANGE.Sound.RCFor.filename, RANGE.Sound.RCFor.duration, self.soundpath )
|
|
self.rangecontrol:Number2Transmission( string.format( "%d", UTILS.MetersToFeet( result.distance ) ) )
|
|
self.rangecontrol:NewTransmission( RANGE.Sound.RCFeet.filename, RANGE.Sound.RCFeet.duration, self.soundpath )
|
|
if result.quality == "POOR" then
|
|
self.rangecontrol:NewTransmission( RANGE.Sound.RCPoorHit.filename, RANGE.Sound.RCPoorHit.duration, self.soundpath, nil, 0.5 )
|
|
elseif result.quality == "INEFFECTIVE" then
|
|
self.rangecontrol:NewTransmission( RANGE.Sound.RCIneffectiveHit.filename, RANGE.Sound.RCIneffectiveHit.duration, self.soundpath, nil, 0.5 )
|
|
elseif result.quality == "GOOD" then
|
|
self.rangecontrol:NewTransmission( RANGE.Sound.RCGoodHit.filename, RANGE.Sound.RCGoodHit.duration, self.soundpath, nil, 0.5 )
|
|
elseif result.quality == "EXCELLENT" then
|
|
self.rangecontrol:NewTransmission( RANGE.Sound.RCExcellentHit.filename, RANGE.Sound.RCExcellentHit.duration, self.soundpath, nil, 0.5 )
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Unit.
|
|
if player.unitname and not self.useSRS then
|
|
|
|
-- Get unit.
|
|
local unit = UNIT:FindByName( player.unitname )
|
|
|
|
-- Send message.
|
|
self:_DisplayMessageToGroup( unit, text, nil, true )
|
|
self:T( self.id .. text )
|
|
end
|
|
|
|
-- Save results.
|
|
if self.autosave then
|
|
self:Save()
|
|
end
|
|
|
|
-- Send result to FunkMan, which creates fancy MatLab figures and sends them to Discord via a bot.
|
|
if self.funkmanSocket then
|
|
self.funkmanSocket:SendTable(result)
|
|
end
|
|
|
|
end
|
|
|
|
--- Function called after strafing run.
|
|
-- @param #RANGE self
|
|
-- @param #string From From state.
|
|
-- @param #string Event Event.
|
|
-- @param #string To To state.
|
|
-- @param #RANGE.PlayerData player Player data table.
|
|
-- @param #RANGE.StrafeResult result Result of run.
|
|
function RANGE:onafterStrafeResult( From, Event, To, player, result)
|
|
|
|
if self.funkmanSocket then
|
|
self.funkmanSocket:SendTable(result)
|
|
end
|
|
|
|
end
|
|
|
|
--- Function called before save event. Checks that io and lfs are desanitized.
|
|
-- @param #RANGE self
|
|
-- @param #string From From state.
|
|
-- @param #string Event Event.
|
|
-- @param #string To To state.
|
|
function RANGE:onbeforeSave( From, Event, To )
|
|
if io and lfs then
|
|
return true
|
|
else
|
|
self:E( self.id .. string.format( "WARNING: io and/or lfs not desanitized. Cannot save player results." ) )
|
|
return false
|
|
end
|
|
end
|
|
|
|
--- Function called after save.
|
|
-- @param #RANGE self
|
|
-- @param #string From From state.
|
|
-- @param #string Event Event.
|
|
-- @param #string To To state.
|
|
function RANGE:onafterSave( From, Event, To )
|
|
|
|
local function _savefile( filename, data )
|
|
local f = io.open( filename, "wb" )
|
|
if f then
|
|
f:write( data )
|
|
f:close()
|
|
self:I( self.id .. string.format( "Saving player results to file %s", tostring( filename ) ) )
|
|
else
|
|
self:E( self.id .. string.format( "ERROR: Could not save results to file %s", tostring( filename ) ) )
|
|
end
|
|
end
|
|
|
|
-- Path.
|
|
local path = lfs.writedir() .. [[Logs\]]
|
|
|
|
-- Set file name.
|
|
local filename = path .. string.format( "RANGE-%s_BombingResults.csv", self.rangename )
|
|
|
|
-- Header line.
|
|
local scores = "Name,Pass,Target,Distance,Radial,Quality,Weapon,Airframe,Mission Time"
|
|
|
|
-- Loop over all players.
|
|
for playername, results in pairs( self.bombPlayerResults ) do
|
|
|
|
-- Loop over player grades table.
|
|
for i, _result in pairs( results ) do
|
|
local result = _result -- #RANGE.BombResult
|
|
local distance = result.distance
|
|
local weapon = result.weapon
|
|
local target = result.name
|
|
local radial = result.radial
|
|
local quality = result.quality
|
|
local time = UTILS.SecondsToClock(result.time, true)
|
|
local airframe = result.airframe
|
|
local date = "n/a"
|
|
if os then
|
|
date = os.date()
|
|
end
|
|
scores = scores .. string.format( "\n%s,%d,%s,%.2f,%03d,%s,%s,%s,%s,%s", playername, i, target, distance, radial, quality, weapon, airframe, time, date )
|
|
end
|
|
end
|
|
|
|
_savefile( filename, scores )
|
|
end
|
|
|
|
--- Function called before save event. Checks that io and lfs are desanitized.
|
|
-- @param #RANGE self
|
|
-- @param #string From From state.
|
|
-- @param #string Event Event.
|
|
-- @param #string To To state.
|
|
function RANGE:onbeforeLoad( From, Event, To )
|
|
if io and lfs then
|
|
return true
|
|
else
|
|
self:E( self.id .. string.format( "WARNING: io and/or lfs not desanitized. Cannot load player results." ) )
|
|
return false
|
|
end
|
|
end
|
|
|
|
--- On after "Load" event. Loads results of all players from file.
|
|
-- @param #RANGE self
|
|
-- @param #string From From state.
|
|
-- @param #string Event Event.
|
|
-- @param #string To To state.
|
|
function RANGE:onafterLoad( From, Event, To )
|
|
|
|
--- Function that load data from a file.
|
|
local function _loadfile( filename )
|
|
local f = io.open( filename, "rb" )
|
|
if f then
|
|
-- self:I(self.id..string.format("Loading player results from file %s", tostring(filename)))
|
|
local data = f:read( "*all" )
|
|
f:close()
|
|
return data
|
|
else
|
|
self:E( self.id .. string.format( "WARNING: Could not load player results from file %s. File might not exist just yet.", tostring( filename ) ) )
|
|
return nil
|
|
end
|
|
end
|
|
|
|
-- Path in DCS log file.
|
|
local path = lfs.writedir() .. [[Logs\]]
|
|
|
|
-- Set file name.
|
|
local filename = path .. string.format( "RANGE-%s_BombingResults.csv", self.rangename )
|
|
|
|
-- Info message.
|
|
local text = string.format( "Loading player bomb results from file %s", filename )
|
|
self:I( self.id .. text )
|
|
|
|
-- Load asset data from file.
|
|
local data = _loadfile( filename )
|
|
|
|
if data then
|
|
|
|
-- Split by line break.
|
|
local results = UTILS.Split( data, "\n" )
|
|
|
|
-- Remove first header line.
|
|
table.remove( results, 1 )
|
|
|
|
-- Init player scores table.
|
|
self.bombPlayerResults = {}
|
|
|
|
-- Loop over all lines.
|
|
for _, _result in pairs( results ) do
|
|
|
|
-- Parameters are separated by commata.
|
|
local resultdata = UTILS.Split( _result, "," )
|
|
|
|
-- Grade table
|
|
local result = {} -- #RANGE.BombResult
|
|
|
|
-- Player name.
|
|
local playername = resultdata[1]
|
|
result.player = playername
|
|
|
|
-- Results data.
|
|
result.name = tostring( resultdata[3] )
|
|
result.distance = tonumber( resultdata[4] )
|
|
result.radial = tonumber( resultdata[5] )
|
|
result.quality = tostring( resultdata[6] )
|
|
result.weapon = tostring( resultdata[7] )
|
|
result.airframe = tostring( resultdata[8] )
|
|
result.time = UTILS.ClockToSeconds( resultdata[9] or "00:00:00" )
|
|
result.date = resultdata[10] or "n/a"
|
|
|
|
-- Create player array if necessary.
|
|
self.bombPlayerResults[playername] = self.bombPlayerResults[playername] or {}
|
|
|
|
-- Add result to table.
|
|
table.insert( self.bombPlayerResults[playername], result )
|
|
end
|
|
end
|
|
end
|
|
|
|
--- Save target sheet.
|
|
-- @param #RANGE self
|
|
-- @param #string _playername Player name.
|
|
-- @param #RANGE.StrafeResult result Results table.
|
|
function RANGE:_SaveTargetSheet( _playername, result ) -- RangeBoss Specific Function
|
|
|
|
--- Function that saves data to file
|
|
local function _savefile( filename, data )
|
|
local f = io.open( filename, "wb" )
|
|
if f then
|
|
f:write( data )
|
|
f:close()
|
|
else
|
|
env.info( "RANGEBOSS EDIT - could not save target sheet to file" )
|
|
-- self:E(self.lid..string.format("ERROR: could not save target sheet to file %s.\nFile may contain invalid characters.", tostring(filename)))
|
|
end
|
|
end
|
|
|
|
-- Set path or default.
|
|
local path = self.targetpath
|
|
if lfs then
|
|
path = path or lfs.writedir() .. [[Logs\]]
|
|
end
|
|
|
|
-- Create unused file name.
|
|
local filename = nil
|
|
for i = 1, 9999 do
|
|
|
|
-- Create file name
|
|
if self.targetprefix then
|
|
filename = string.format( "%s_%s-%04d.csv", self.targetprefix, result.airframe, i )
|
|
else
|
|
local name = UTILS.ReplaceIllegalCharacters( _playername, "_" )
|
|
filename = string.format( "RANGERESULTS-%s_Targetsheet-%s-%04d.csv", self.rangename, name, i )
|
|
end
|
|
|
|
-- Set path.
|
|
if path ~= nil then
|
|
filename = path .. "\\" .. filename
|
|
end
|
|
|
|
-- Check if file exists.
|
|
local _exists = UTILS.FileExists( filename )
|
|
if not _exists then
|
|
break
|
|
end
|
|
end
|
|
|
|
-- Header line
|
|
local data = "Name,Target,Rounds Fired,Rounds Hit,Rounds Quality,Airframe,Mission Time,OS Time\n"
|
|
|
|
local target = result.name
|
|
local airframe = result.airframe
|
|
local roundsFired = result.roundsFired
|
|
local roundsHit = result.roundsHit
|
|
local strafeResult = result.roundsQuality
|
|
local time = UTILS.SecondsToClock( result.time )
|
|
local date = "n/a"
|
|
if os then
|
|
date = os.date()
|
|
end
|
|
data = data .. string.format( "%s,%s,%d,%d,%s,%s,%s,%s", _playername, target, roundsFired, roundsHit, strafeResult, airframe, time, date )
|
|
|
|
-- Save file.
|
|
_savefile( filename, data )
|
|
end
|
|
|
|
-----------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
|
-- Display Messages
|
|
-----------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
|
|
|
--- Start smoking a coordinate with a delay.
|
|
-- @param #table _args Argements passed.
|
|
function RANGE._DelayedSmoke( _args )
|
|
_args.coord:Smoke(_args.color)
|
|
--trigger.action.smoke( _args.coord:GetVec3(), _args.color )
|
|
end
|
|
|
|
--- Display top 10 stafing results of a specific player.
|
|
-- @param #RANGE self
|
|
-- @param #string _unitName Name of the player unit.
|
|
function RANGE:_DisplayMyStrafePitResults( _unitName )
|
|
self:F( _unitName )
|
|
|
|
-- Get player unit and name
|
|
local _unit, _playername = self:_GetPlayerUnitAndName( _unitName )
|
|
|
|
if _unit and _playername then
|
|
|
|
-- Message header.
|
|
local _message = string.format( "My Top %d Strafe Pit Results:\n", self.ndisplayresult )
|
|
|
|
-- Get player results.
|
|
local _results = self.strafePlayerResults[_playername]
|
|
|
|
-- Create message.
|
|
if _results == nil then
|
|
-- No score yet.
|
|
_message = string.format( "%s: No Score yet.", _playername )
|
|
else
|
|
|
|
-- Sort results table wrt number of hits.
|
|
local _sort = function( a, b )
|
|
return a.roundsHit > b.roundsHit
|
|
end
|
|
table.sort( _results, _sort )
|
|
|
|
-- Prepare message of best results.
|
|
local _bestMsg = ""
|
|
local _count = 1
|
|
|
|
-- Loop over results
|
|
for _, _result in pairs( _results ) do
|
|
local result=_result --#RANGE.StrafeResult
|
|
|
|
-- Message text.
|
|
_message = _message .. string.format( "\n[%d] Hits %d - %s - %s", _count, result.roundsHit, result.name, result.roundsQuality )
|
|
|
|
-- Best result.
|
|
if _bestMsg == "" then
|
|
_bestMsg = string.format( "Hits %d - %s - %s", result.roundsHit, result.name, result.roundsQuality)
|
|
end
|
|
|
|
-- 10 runs
|
|
if _count == self.ndisplayresult then
|
|
break
|
|
end
|
|
|
|
-- Increase counter
|
|
_count = _count + 1
|
|
end
|
|
|
|
-- Message text.
|
|
_message = _message .. "\n\nBEST: " .. _bestMsg
|
|
end
|
|
|
|
-- Send message to group.
|
|
self:_DisplayMessageToGroup( _unit, _message, nil, true, true )
|
|
end
|
|
end
|
|
|
|
--- Display top 10 strafing results of all players.
|
|
-- @param #RANGE self
|
|
-- @param #string _unitName Name fo the player unit.
|
|
function RANGE:_DisplayStrafePitResults( _unitName )
|
|
self:F( _unitName )
|
|
|
|
-- Get player unit and name.
|
|
local _unit, _playername = self:_GetPlayerUnitAndName( _unitName )
|
|
|
|
-- Check if we have a unit which is a player.
|
|
if _unit and _playername then
|
|
|
|
-- Results table.
|
|
local _playerResults = {}
|
|
|
|
-- Message text.
|
|
local _message = string.format( "Strafe Pit Results - Top %d Players:\n", self.ndisplayresult )
|
|
|
|
-- Loop over player results.
|
|
for _playerName, _results in pairs( self.strafePlayerResults ) do
|
|
|
|
-- Get the best result of the player.
|
|
local _best = nil
|
|
for _, _result in pairs( _results ) do
|
|
if _best == nil or _result.roundsHit > _best.roundsHit then
|
|
_best = _result
|
|
end
|
|
end
|
|
|
|
-- Add best result to table.
|
|
if _best ~= nil then
|
|
local text = string.format( "%s: Hits %i - %s - %s", _playerName, _best.roundsHit, _best.name, _best.roundsQuality )
|
|
table.insert( _playerResults, { msg = text, hits = _best.roundsHit } )
|
|
end
|
|
|
|
end
|
|
|
|
-- Sort list!
|
|
local _sort = function( a, b )
|
|
return a.hits > b.hits
|
|
end
|
|
table.sort( _playerResults, _sort )
|
|
|
|
-- Add top 10 results.
|
|
for _i = 1, math.min( #_playerResults, self.ndisplayresult ) do
|
|
_message = _message .. string.format( "\n[%d] %s", _i, _playerResults[_i].msg )
|
|
end
|
|
|
|
-- In case there are no scores yet.
|
|
if #_playerResults < 1 then
|
|
_message = _message .. "No player scored yet."
|
|
end
|
|
|
|
-- Send message.
|
|
self:_DisplayMessageToGroup( _unit, _message, nil, true, true )
|
|
end
|
|
end
|
|
|
|
--- Display top 10 bombing run results of specific player.
|
|
-- @param #RANGE self
|
|
-- @param #string _unitName Name of the player unit.
|
|
function RANGE:_DisplayMyBombingResults( _unitName )
|
|
self:F( _unitName )
|
|
|
|
-- Get player unit and name.
|
|
local _unit, _playername = self:_GetPlayerUnitAndName( _unitName )
|
|
|
|
if _unit and _playername then
|
|
|
|
-- Init message.
|
|
local _message = string.format( "My Top %d Bombing Results:\n", self.ndisplayresult )
|
|
|
|
-- Results from player.
|
|
local _results = self.bombPlayerResults[_playername]
|
|
|
|
-- No score so far.
|
|
if _results == nil then
|
|
_message = _playername .. ": No Score yet."
|
|
else
|
|
|
|
-- Sort results wrt to distance.
|
|
local _sort = function( a, b )
|
|
return a.distance < b.distance
|
|
end
|
|
table.sort( _results, _sort )
|
|
|
|
-- Loop over results.
|
|
local _bestMsg = ""
|
|
for i, _result in pairs( _results ) do
|
|
local result = _result -- #RANGE.BombResult
|
|
|
|
-- Message with name, weapon and distance.
|
|
_message = _message .. "\n" .. string.format( "[%d] %d m %03d° - %s - %s - %s hit", i, result.distance, result.radial, result.name, result.weapon, result.quality )
|
|
|
|
-- Store best/first result.
|
|
if _bestMsg == "" then
|
|
_bestMsg = string.format( "%d m %03d° - %s - %s - %s hit", result.distance, result.radial, result.name, result.weapon, result.quality )
|
|
end
|
|
|
|
-- Best 10 runs only.
|
|
if i == self.ndisplayresult then
|
|
break
|
|
end
|
|
|
|
end
|
|
|
|
-- Message.
|
|
_message = _message .. "\n\nBEST: " .. _bestMsg
|
|
end
|
|
|
|
-- Send message.
|
|
self:_DisplayMessageToGroup( _unit, _message, nil, true, true )
|
|
end
|
|
end
|
|
|
|
--- Display best bombing results of top 10 players.
|
|
-- @param #RANGE self
|
|
-- @param #string _unitName Name of player unit.
|
|
function RANGE:_DisplayBombingResults( _unitName )
|
|
self:F( _unitName )
|
|
|
|
-- Results table.
|
|
local _playerResults = {}
|
|
|
|
-- Get player unit and name.
|
|
local _unit, _player = self:_GetPlayerUnitAndName( _unitName )
|
|
|
|
-- Check if we have a unit with a player.
|
|
if _unit and _player then
|
|
|
|
-- Message header.
|
|
local _message = string.format( "Bombing Results - Top %d Players:\n", self.ndisplayresult )
|
|
|
|
-- Loop over players.
|
|
for _playerName, _results in pairs( self.bombPlayerResults ) do
|
|
|
|
-- Find best result of player.
|
|
local _best = nil
|
|
for _, _result in pairs( _results ) do
|
|
if _best == nil or _result.distance < _best.distance then
|
|
_best = _result
|
|
end
|
|
end
|
|
|
|
-- Put best result of player into table.
|
|
if _best ~= nil then
|
|
local bestres = string.format( "%s: %d m - %s - %s - %s hit", _playerName, _best.distance, _best.name, _best.weapon, _best.quality )
|
|
table.insert( _playerResults, { msg = bestres, distance = _best.distance } )
|
|
end
|
|
|
|
end
|
|
|
|
-- Sort list of player results.
|
|
local _sort = function( a, b )
|
|
return a.distance < b.distance
|
|
end
|
|
table.sort( _playerResults, _sort )
|
|
|
|
-- Loop over player results.
|
|
for _i = 1, math.min( #_playerResults, self.ndisplayresult ) do
|
|
_message = _message .. string.format( "\n[%d] %s", _i, _playerResults[_i].msg )
|
|
end
|
|
|
|
-- In case there are no scores yet.
|
|
if #_playerResults < 1 then
|
|
_message = _message .. "No player scored yet."
|
|
end
|
|
|
|
-- Send message.
|
|
self:_DisplayMessageToGroup( _unit, _message, nil, true, true )
|
|
end
|
|
end
|
|
|
|
--- Report information like bearing and range from player unit to range.
|
|
-- @param #RANGE self
|
|
-- @param #string _unitname Name of the player unit.
|
|
function RANGE:_DisplayRangeInfo( _unitname )
|
|
self:F( _unitname )
|
|
|
|
-- Get player unit and player name.
|
|
local unit, playername = self:_GetPlayerUnitAndName( _unitname )
|
|
|
|
-- Check if we have a player.
|
|
if unit and playername then
|
|
self:I(playername)
|
|
-- Message text.
|
|
local text = ""
|
|
|
|
-- Current coordinates.
|
|
local coord = unit:GetCoordinate()
|
|
|
|
if self.location then
|
|
|
|
local settings = _DATABASE:GetPlayerSettings( playername ) or _SETTINGS -- Core.Settings#SETTINGS
|
|
|
|
-- Direction vector from current position (coord) to target (position).
|
|
local position = self.location -- Core.Point#COORDINATE
|
|
local bulls = position:ToStringBULLS( unit:GetCoalition(), settings )
|
|
local lldms = position:ToStringLLDMS( settings )
|
|
local llddm = position:ToStringLLDDM( settings )
|
|
local rangealt = position:GetLandHeight()
|
|
local vec3 = coord:GetDirectionVec3( position )
|
|
local angle = coord:GetAngleDegrees( vec3 )
|
|
local range = coord:Get2DDistance( position )
|
|
|
|
-- Bearing string.
|
|
local Bs = string.format( '%03d°', angle )
|
|
|
|
local texthit
|
|
if self.PlayerSettings[playername].flaredirecthits then
|
|
texthit = string.format( "Flare direct hits: ON (flare color %s)\n", self:_flarecolor2text( self.PlayerSettings[playername].flarecolor ) )
|
|
else
|
|
texthit = string.format( "Flare direct hits: OFF\n" )
|
|
end
|
|
local textbomb
|
|
if self.PlayerSettings[playername].smokebombimpact then
|
|
textbomb = string.format( "Smoke bomb impact points: ON (smoke color %s)\n", self:_smokecolor2text( self.PlayerSettings[playername].smokecolor ) )
|
|
else
|
|
textbomb = string.format( "Smoke bomb impact points: OFF\n" )
|
|
end
|
|
local textdelay
|
|
if self.PlayerSettings[playername].delaysmoke then
|
|
textdelay = string.format( "Smoke bomb delay: ON (delay %.1f seconds)", self.TdelaySmoke )
|
|
else
|
|
textdelay = string.format( "Smoke bomb delay: OFF" )
|
|
end
|
|
|
|
-- Player unit settings.
|
|
local trange = string.format( "%.1f km", range / 1000 )
|
|
local trangealt = string.format( "%d m", rangealt )
|
|
local tstrafemaxalt = string.format( "%d m", self.strafemaxalt )
|
|
if settings:IsImperial() then
|
|
trange = string.format( "%.1f NM", UTILS.MetersToNM( range ) )
|
|
trangealt = string.format( "%d feet", UTILS.MetersToFeet( rangealt ) )
|
|
tstrafemaxalt = string.format( "%d feet", UTILS.MetersToFeet( self.strafemaxalt ) )
|
|
end
|
|
|
|
-- Message.
|
|
text = text .. string.format( "Information on %s:\n", self.rangename )
|
|
text = text .. string.format( "-------------------------------------------------------\n" )
|
|
text = text .. string.format( "Bearing %s, Range %s\n", Bs, trange )
|
|
text = text .. string.format( "%s\n", bulls )
|
|
text = text .. string.format( "%s\n", lldms )
|
|
text = text .. string.format( "%s\n", llddm )
|
|
text = text .. string.format( "Altitude ASL: %s\n", trangealt )
|
|
text = text .. string.format( "Max strafing alt AGL: %s\n", tstrafemaxalt )
|
|
text = text .. string.format( "# of strafe targets: %d\n", self.nstrafetargets )
|
|
text = text .. string.format( "# of bomb targets: %d\n", self.nbombtargets )
|
|
if self.instructor then
|
|
local alive = "N/A"
|
|
if self.instructorrelayname then
|
|
local relay = UNIT:FindByName( self.instructorrelayname )
|
|
if relay then
|
|
--alive = tostring( relay:IsAlive() )
|
|
alive = relay:IsAlive() and "ok" or "N/A"
|
|
end
|
|
end
|
|
text = text .. string.format( "Instructor %.3f MHz (Relay=%s)\n", self.instructorfreq, alive )
|
|
end
|
|
if self.rangecontrol then
|
|
local alive = "N/A"
|
|
if self.rangecontrolrelayname then
|
|
local relay = UNIT:FindByName( self.rangecontrolrelayname )
|
|
if relay then
|
|
alive = tostring( relay:IsAlive() )
|
|
alive = relay:IsAlive() and "ok" or "N/A"
|
|
end
|
|
end
|
|
text = text .. string.format( "Control %.3f MHz (Relay=%s)\n", self.rangecontrolfreq, alive )
|
|
end
|
|
text = text .. texthit
|
|
text = text .. textbomb
|
|
text = text .. textdelay
|
|
|
|
-- Send message to player group.
|
|
self:_DisplayMessageToGroup( unit, text, nil, true, true )
|
|
|
|
-- Debug output.
|
|
self:T2( self.id .. text )
|
|
end
|
|
end
|
|
end
|
|
|
|
--- Display bombing target locations to player.
|
|
-- @param #RANGE self
|
|
-- @param #string _unitname Name of the player unit.
|
|
function RANGE:_DisplayBombTargets( _unitname )
|
|
self:F( _unitname )
|
|
|
|
-- Get player unit and player name.
|
|
local _unit, _playername = self:_GetPlayerUnitAndName( _unitname )
|
|
|
|
-- Check if we have a player.
|
|
if _unit and _playername then
|
|
|
|
-- Player settings.
|
|
local _settings = _DATABASE:GetPlayerSettings( _playername ) or _SETTINGS -- Core.Settings#SETTINGS
|
|
|
|
-- Message text.
|
|
local _text = "Bomb Target Locations:"
|
|
|
|
for _, _bombtarget in pairs( self.bombingTargets ) do
|
|
local bombtarget = _bombtarget -- #RANGE.BombTarget
|
|
|
|
-- Coordinate of bombtarget.
|
|
local coord = self:_GetBombTargetCoordinate( bombtarget )
|
|
|
|
if coord then
|
|
|
|
-- Get elevation
|
|
local elevation = coord:GetLandHeight()
|
|
local eltxt = string.format( "%d m", elevation )
|
|
if not _settings:IsMetric() then
|
|
elevation = UTILS.MetersToFeet( elevation )
|
|
eltxt = string.format( "%d ft", elevation )
|
|
end
|
|
|
|
local ca2g = coord:ToStringA2G( _unit, _settings )
|
|
_text = _text .. string.format( "\n- %s:\n%s @ %s", bombtarget.name or "unknown", ca2g, eltxt )
|
|
end
|
|
end
|
|
|
|
self:_DisplayMessageToGroup( _unit, _text, 120, true, true )
|
|
end
|
|
end
|
|
|
|
--- Display pit location and heading to player.
|
|
-- @param #RANGE self
|
|
-- @param #string _unitname Name of the player unit.
|
|
function RANGE:_DisplayStrafePits( _unitname )
|
|
self:F( _unitname )
|
|
|
|
-- Get player unit and player name.
|
|
local _unit, _playername = self:_GetPlayerUnitAndName( _unitname )
|
|
|
|
-- Check if we have a player.
|
|
if _unit and _playername then
|
|
|
|
-- Player settings.
|
|
local _settings = _DATABASE:GetPlayerSettings( _playername ) or _SETTINGS -- Core.Settings#SETTINGS
|
|
|
|
-- Message text.
|
|
local _text = "Strafe Target Locations:"
|
|
|
|
for _, _strafepit in pairs( self.strafeTargets ) do
|
|
local _target = _strafepit -- Wrapper.Positionable#POSITIONABLE
|
|
|
|
-- Pit parameters.
|
|
local coord = _strafepit.coordinate -- Core.Point#COORDINATE
|
|
local heading = _strafepit.heading
|
|
|
|
-- Turn heading around ==> approach heading.
|
|
if heading > 180 then
|
|
heading = heading - 180
|
|
else
|
|
heading = heading + 180
|
|
end
|
|
|
|
local mycoord = coord:ToStringA2G( _unit, _settings )
|
|
_text = _text .. string.format( "\n- %s: heading %03d°\n%s", _strafepit.name, heading, mycoord )
|
|
end
|
|
|
|
self:_DisplayMessageToGroup( _unit, _text, nil, true, true )
|
|
end
|
|
end
|
|
|
|
--- Report weather conditions at range. Temperature, QFE pressure and wind data.
|
|
-- @param #RANGE self
|
|
-- @param #string _unitname Name of the player unit.
|
|
function RANGE:_DisplayRangeWeather( _unitname )
|
|
self:F( _unitname )
|
|
|
|
-- Get player unit and player name.
|
|
local unit, playername = self:_GetPlayerUnitAndName( _unitname )
|
|
|
|
-- Check if we have a player.
|
|
if unit and playername then
|
|
|
|
-- Message text.
|
|
local text = ""
|
|
|
|
-- Current coordinates.
|
|
local coord = unit:GetCoordinate()
|
|
|
|
if self.location then
|
|
|
|
-- Get atmospheric data at range location.
|
|
local position = self.location -- Core.Point#COORDINATE
|
|
local T = position:GetTemperature()
|
|
local P = position:GetPressure()
|
|
local Wd, Ws = position:GetWind()
|
|
|
|
-- Get Beaufort wind scale.
|
|
local Bn, Bd = UTILS.BeaufortScale( Ws )
|
|
|
|
local WD = string.format( '%03d°', Wd )
|
|
local Ts = string.format( "%d°C", T )
|
|
|
|
local hPa2inHg = 0.0295299830714
|
|
local hPa2mmHg = 0.7500615613030
|
|
|
|
local settings = _DATABASE:GetPlayerSettings( playername ) or _SETTINGS -- Core.Settings#SETTINGS
|
|
local tT = string.format( "%d°C", T )
|
|
local tW = string.format( "%.1f m/s", Ws )
|
|
local tP = string.format( "%.1f mmHg", P * hPa2mmHg )
|
|
if settings:IsImperial() then
|
|
-- tT=string.format("%d°F", UTILS.CelsiusToFahrenheit(T))
|
|
tW = string.format( "%.1f knots", UTILS.MpsToKnots( Ws ) )
|
|
tP = string.format( "%.2f inHg", P * hPa2inHg )
|
|
end
|
|
|
|
-- Message text.
|
|
text = text .. string.format( "Weather Report at %s:\n", self.rangename )
|
|
text = text .. string.format( "--------------------------------------------------\n" )
|
|
text = text .. string.format( "Temperature %s\n", tT )
|
|
text = text .. string.format( "Wind from %s at %s (%s)\n", WD, tW, Bd )
|
|
text = text .. string.format( "QFE %.1f hPa = %s", P, tP )
|
|
else
|
|
text = string.format( "No range location defined for range %s.", self.rangename )
|
|
end
|
|
|
|
-- Send message to player group.
|
|
self:_DisplayMessageToGroup( unit, text, nil, true, true )
|
|
|
|
-- Debug output.
|
|
self:T2( self.id .. text )
|
|
else
|
|
self:T( self.id .. string.format( "ERROR! Could not find player unit in RangeInfo! Name = %s", _unitname ) )
|
|
end
|
|
end
|
|
|
|
-----------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
|
-- Timer Functions
|
|
|
|
--- Check status of players.
|
|
-- @param #RANGE self
|
|
-- @param #string _unitName Name of player unit.
|
|
function RANGE:_CheckPlayers()
|
|
|
|
for playername, _playersettings in pairs( self.PlayerSettings ) do
|
|
local playersettings = _playersettings -- #RANGE.PlayerData
|
|
|
|
local unitname = playersettings.unitname
|
|
local unit = UNIT:FindByName( unitname )
|
|
|
|
if unit and unit:IsAlive() then
|
|
|
|
if unit:IsInZone( self.rangezone ) then
|
|
|
|
------------------------------
|
|
-- Player INSIDE Range Zone --
|
|
------------------------------
|
|
|
|
if not playersettings.inzone then
|
|
playersettings.inzone = true
|
|
self:EnterRange( playersettings )
|
|
end
|
|
|
|
else
|
|
|
|
-------------------------------
|
|
-- Player OUTSIDE Range Zone --
|
|
-------------------------------
|
|
|
|
if playersettings.inzone == true then
|
|
playersettings.inzone = false
|
|
self:ExitRange( playersettings )
|
|
end
|
|
|
|
end
|
|
end
|
|
end
|
|
|
|
end
|
|
|
|
--- Check if player is inside a strafing zone. If he is, we start looking for hits. If he was and left the zone again, the result is stored.
|
|
-- @param #RANGE self
|
|
-- @param #string _unitName Name of player unit.
|
|
function RANGE:_CheckInZone( _unitName )
|
|
self:F2( _unitName )
|
|
|
|
-- Get player unit and name.
|
|
local _unit, _playername = self:_GetPlayerUnitAndName( _unitName )
|
|
local unitheading = 0 -- RangeBoss
|
|
|
|
if _unit and _playername then
|
|
|
|
-- Player data.
|
|
local playerData=self.PlayerSettings[_playername] -- #RANGE.PlayerData
|
|
|
|
--- Function to check if unit is in zone and facing in the right direction and is below the max alt.
|
|
local function checkme( targetheading, _zone )
|
|
local zone = _zone -- Core.Zone#ZONE
|
|
|
|
-- Heading check.
|
|
local unitheading = _unit:GetHeading()
|
|
local pitheading = targetheading - 180
|
|
local deltaheading = unitheading - pitheading
|
|
local towardspit = math.abs( deltaheading ) <= 90 or math.abs( deltaheading - 360 ) <= 90
|
|
|
|
if towardspit then
|
|
|
|
local vec3 = _unit:GetVec3()
|
|
local vec2 = { x = vec3.x, y = vec3.z } -- DCS#Vec2
|
|
local landheight = land.getHeight( vec2 )
|
|
local unitalt = vec3.y - landheight
|
|
|
|
if unitalt <= self.strafemaxalt then
|
|
local unitinzone = zone:IsVec2InZone( vec2 )
|
|
return unitinzone
|
|
end
|
|
end
|
|
|
|
return false
|
|
end
|
|
|
|
-- Current position of player unit.
|
|
local _unitID = _unit:GetID()
|
|
|
|
-- Currently strafing? (strafeStatus is nil if not)
|
|
local _currentStrafeRun = self.strafeStatus[_unitID] --#RANGE.StrafeStatus
|
|
|
|
if _currentStrafeRun then -- player has already registered for a strafing run.
|
|
|
|
-- Get the current approach zone and check if player is inside.
|
|
local zone = _currentStrafeRun.zone.polygon -- Core.Zone#ZONE_POLYGON_BASE
|
|
|
|
-- Check if unit in zone and facing the right direction.
|
|
local unitinzone = checkme( _currentStrafeRun.zone.heading, zone )
|
|
|
|
-- Check if player is in strafe zone and below max alt.
|
|
if unitinzone then
|
|
-- Still in zone, keep counting hits. Increase counter.
|
|
_currentStrafeRun.time = _currentStrafeRun.time + 1
|
|
|
|
else
|
|
|
|
-- Increase counter
|
|
_currentStrafeRun.time = _currentStrafeRun.time + 1
|
|
|
|
if _currentStrafeRun.time <= 3 then
|
|
|
|
-- Reset current run.
|
|
self.strafeStatus[_unitID] = nil
|
|
|
|
-- Message text.
|
|
local _msg = string.format( "%s left strafing zone %s too quickly. No Score.", _playername, _currentStrafeRun.zone.name )
|
|
|
|
-- Send message.
|
|
self:_DisplayMessageToGroup( _unit, _msg, nil, true )
|
|
|
|
if self.rangecontrol then
|
|
if self.useSRS then
|
|
local group = _unit:GetGroup()
|
|
local text = "You left the strafing zone too quickly! No score!"
|
|
--self.controlsrsQ:NewTransmission(text,nil,self.controlmsrs,nil,1,{group},text,10)
|
|
self.controlsrsQ:NewTransmission(text,nil,self.controlmsrs,nil,1)
|
|
else
|
|
-- You left the strafing zone too quickly! No score!
|
|
self.rangecontrol:NewTransmission( RANGE.Sound.RCLeftStrafePitTooQuickly.filename, RANGE.Sound.RCLeftStrafePitTooQuickly.duration, self.soundpath )
|
|
end
|
|
end
|
|
else
|
|
|
|
-- Get current ammo.
|
|
local _ammo = self:_GetAmmo( _unitName )
|
|
|
|
-- Result.
|
|
local _result = self.strafeStatus[_unitID] --#RANGE.StrafeStatus
|
|
|
|
local _sound = nil -- #RANGE.Soundfile
|
|
|
|
-- Calculate accuracy of run. Number of hits wrt number of rounds fired.
|
|
local shots = _result.ammo - _ammo
|
|
local accur = 0
|
|
if shots > 0 then
|
|
accur = _result.hits / shots * 100
|
|
if accur > 100 then
|
|
accur = 100
|
|
end
|
|
end
|
|
|
|
-- Results text and sound message.
|
|
local resulttext=""
|
|
if _result.pastfoulline == true then --
|
|
resulttext = "* INVALID - PASSED FOUL LINE *"
|
|
_sound = RANGE.Sound.RCPoorPass --
|
|
else
|
|
if accur >= 90 then
|
|
resulttext = "DEADEYE PASS"
|
|
_sound = RANGE.Sound.RCExcellentPass
|
|
elseif accur >= 75 then
|
|
resulttext = "EXCELLENT PASS"
|
|
_sound = RANGE.Sound.RCExcellentPass
|
|
elseif accur >= 50 then
|
|
resulttext = "GOOD PASS"
|
|
_sound = RANGE.Sound.RCGoodPass
|
|
elseif accur >= 25 then
|
|
resulttext = "INEFFECTIVE PASS"
|
|
_sound = RANGE.Sound.RCIneffectivePass
|
|
else
|
|
resulttext = "POOR PASS"
|
|
_sound = RANGE.Sound.RCPoorPass
|
|
end
|
|
end
|
|
|
|
-- Message text.
|
|
local _text = string.format( "%s, hits on target %s: %d", self:_myname( _unitName ), _result.zone.name, _result.hits )
|
|
local ttstext = string.format( "%s, hits on target %s: %d.", self:_myname( _unitName ), _result.zone.name, _result.hits )
|
|
if shots and accur then
|
|
_text = _text .. string.format( "\nTotal rounds fired %d. Accuracy %.1f %%.", shots, accur )
|
|
ttstext = ttstext .. string.format( ". Total rounds fired %d. Accuracy %.1f percent.", shots, accur )
|
|
end
|
|
_text = _text .. string.format( "\n%s", resulttext )
|
|
ttstext = ttstext .. string.format( " %s", resulttext )
|
|
|
|
-- Send message.
|
|
self:_DisplayMessageToGroup( _unit, _text )
|
|
|
|
-- Strafe result.
|
|
local result = {} -- #RANGE.StrafeResult
|
|
result.command=SOCKET.DataType.STRAFERESULT
|
|
result.player=_playername
|
|
result.name=_result.zone.name or "unknown"
|
|
result.time = timer.getAbsTime()
|
|
result.clock = UTILS.SecondsToClock(result.time)
|
|
result.midate = UTILS.GetDCSMissionDate()
|
|
result.theatre = env.mission.theatre
|
|
result.roundsFired = shots
|
|
result.roundsHit = _result.hits
|
|
result.roundsQuality = resulttext
|
|
result.strafeAccuracy = accur
|
|
result.rangename = self.rangename
|
|
result.airframe=playerData.airframe
|
|
result.invalid = _result.pastfoulline
|
|
|
|
-- Griger Results.
|
|
self:StrafeResult(playerData, result)
|
|
|
|
-- Save trap sheet.
|
|
if playerData and playerData.targeton and self.targetsheet then
|
|
self:_SaveTargetSheet( _playername, result )
|
|
end
|
|
|
|
-- Voice over.
|
|
if self.rangecontrol then
|
|
if self.useSRS then
|
|
self.controlsrsQ:NewTransmission(ttstext,nil,self.controlmsrs,nil,1)
|
|
else
|
|
self.rangecontrol:NewTransmission( RANGE.Sound.RCHitsOnTarget.filename, RANGE.Sound.RCHitsOnTarget.duration, self.soundpath )
|
|
self.rangecontrol:Number2Transmission( string.format( "%d", _result.hits ) )
|
|
if shots and accur then
|
|
self.rangecontrol:NewTransmission( RANGE.Sound.RCTotalRoundsFired.filename, RANGE.Sound.RCTotalRoundsFired.duration, self.soundpath, nil, 0.2 )
|
|
self.rangecontrol:Number2Transmission( string.format( "%d", shots ), nil, 0.2 )
|
|
self.rangecontrol:NewTransmission( RANGE.Sound.RCAccuracy.filename, RANGE.Sound.RCAccuracy.duration, self.soundpath, nil, 0.2 )
|
|
self.rangecontrol:Number2Transmission( string.format( "%d", UTILS.Round( accur, 0 ) ) )
|
|
self.rangecontrol:NewTransmission( RANGE.Sound.RCPercent.filename, RANGE.Sound.RCPercent.duration, self.soundpath )
|
|
end
|
|
self.rangecontrol:NewTransmission( _sound.filename, _sound.duration, self.soundpath, nil, 0.5 )
|
|
end
|
|
end
|
|
|
|
-- Set strafe status to nil.
|
|
self.strafeStatus[_unitID] = nil
|
|
|
|
-- Save stats so the player can retrieve them.
|
|
local _stats = self.strafePlayerResults[_playername] or {}
|
|
table.insert( _stats, result )
|
|
self.strafePlayerResults[_playername] = _stats
|
|
end
|
|
|
|
end
|
|
|
|
else
|
|
|
|
-- Check to see if we're in any of the strafing zones (first time).
|
|
for _, _targetZone in pairs( self.strafeTargets ) do
|
|
local target=_targetZone --#RANGE.StrafeTarget
|
|
|
|
-- Get the current approach zone and check if player is inside.
|
|
local zone = target.polygon -- Core.Zone#ZONE_POLYGON_BASE
|
|
|
|
-- Check if unit in zone and facing the right direction.
|
|
local unitinzone = checkme( target.heading, zone )
|
|
|
|
-- Player is inside zone.
|
|
if unitinzone then
|
|
|
|
-- Get ammo at the beginning of the run.
|
|
local _ammo = self:_GetAmmo( _unitName )
|
|
|
|
-- Init strafe status for this player.
|
|
self.strafeStatus[_unitID] = { hits = 0, zone = target, time = 1, ammo = _ammo, pastfoulline = false }
|
|
|
|
-- Rolling in!
|
|
local _msg = string.format( "%s, rolling in on strafe pit %s.", self:_myname( _unitName ), target.name )
|
|
|
|
if self.rangecontrol then
|
|
if self.useSRS then
|
|
self.controlsrsQ:NewTransmission(_msg,nil,self.controlmsrs,nil,1)
|
|
else
|
|
self.rangecontrol:NewTransmission( RANGE.Sound.RCRollingInOnStrafeTarget.filename, RANGE.Sound.RCRollingInOnStrafeTarget.duration, self.soundpath )
|
|
end
|
|
end
|
|
|
|
-- Send message.
|
|
self:_DisplayMessageToGroup( _unit, _msg, 10, true )
|
|
|
|
-- Trigger event that player is rolling in.
|
|
self:RollingIn(playerData, target)
|
|
|
|
-- We found our player. Skip remaining checks.
|
|
break
|
|
|
|
end -- unit in zone check
|
|
|
|
end -- loop over zones
|
|
end
|
|
end
|
|
|
|
end
|
|
|
|
-----------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
|
-- Menu Functions
|
|
-----------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
|
|
|
--- Add menu commands for player.
|
|
-- @param #RANGE self
|
|
-- @param #string _unitName Name of player unit.
|
|
function RANGE:_AddF10Commands( _unitName )
|
|
self:F( _unitName )
|
|
|
|
-- Get player unit and name.
|
|
local _unit, playername = self:_GetPlayerUnitAndName( _unitName )
|
|
|
|
-- Check for player unit.
|
|
if _unit and playername then
|
|
|
|
-- Get group and ID.
|
|
local group = _unit:GetGroup()
|
|
local _gid = group:GetID()
|
|
|
|
if group and _gid then
|
|
|
|
if not self.MenuAddedTo[_gid] then
|
|
|
|
-- Enable switch so we don't do this twice.
|
|
self.MenuAddedTo[_gid] = true
|
|
|
|
-- Range root menu path.
|
|
local _rangePath = nil
|
|
|
|
if RANGE.MenuF10Root then
|
|
|
|
-------------------
|
|
-- MISSION LEVEL --
|
|
-------------------
|
|
|
|
-- _rangePath = missionCommands.addSubMenuForGroup(_gid, self.rangename, RANGE.MenuF10Root)
|
|
_rangePath = MENU_GROUP:New( group, "On the Range" )
|
|
|
|
else
|
|
|
|
-----------------
|
|
-- GROUP LEVEL --
|
|
-----------------
|
|
|
|
-- Main F10 menu: F10/On the Range/<Range Name>/
|
|
if RANGE.MenuF10[_gid] == nil then
|
|
-- RANGE.MenuF10[_gid]=missionCommands.addSubMenuForGroup(_gid, "On the Range")
|
|
RANGE.MenuF10[_gid] = MENU_GROUP:New( group, "On the Range" )
|
|
end
|
|
-- _rangePath = missionCommands.addSubMenuForGroup(_gid, self.rangename, RANGE.MenuF10[_gid])
|
|
_rangePath = MENU_GROUP:New( group, self.rangename, RANGE.MenuF10[_gid] )
|
|
end
|
|
|
|
local _statsPath = MENU_GROUP:New( group, "Statistics", _rangePath )
|
|
local _markPath = MENU_GROUP:New( group, "Mark Targets", _rangePath )
|
|
local _settingsPath = MENU_GROUP:New( group, "My Settings", _rangePath )
|
|
local _infoPath = MENU_GROUP:New( group, "Range Info", _rangePath )
|
|
|
|
-- F10/On the Range/<Range Name>/My Settings/
|
|
local _mysmokePath = MENU_GROUP:New( group, "Smoke Color", _settingsPath )
|
|
local _myflarePath = MENU_GROUP:New( group, "Flare Color", _settingsPath )
|
|
|
|
-- F10/On the Range/<Range Name>/Mark Targets/
|
|
local _MoMap = MENU_GROUP_COMMAND:New( group, "Mark On Map", _markPath, self._MarkTargetsOnMap, self, _unitName )
|
|
local _IllRng = MENU_GROUP_COMMAND:New( group, "Illuminate Range", _markPath, self._IlluminateBombTargets, self, _unitName )
|
|
local _SSpit = MENU_GROUP_COMMAND:New( group, "Smoke Strafe Pits", _markPath, self._SmokeStrafeTargetBoxes, self, _unitName )
|
|
local _SStgts = MENU_GROUP_COMMAND:New( group, "Smoke Strafe Tgts", _markPath, self._SmokeStrafeTargets, self, _unitName )
|
|
local _SBtgts = MENU_GROUP_COMMAND:New( group, "Smoke Bomb Tgts", _markPath, self._SmokeBombTargets, self, _unitName )
|
|
-- F10/On the Range/<Range Name>/Stats/
|
|
local _AllSR = MENU_GROUP_COMMAND:New( group, "All Strafe Results", _statsPath, self._DisplayStrafePitResults, self, _unitName )
|
|
local _AllBR = MENU_GROUP_COMMAND:New( group, "All Bombing Results", _statsPath, self._DisplayBombingResults, self, _unitName )
|
|
local _MySR = MENU_GROUP_COMMAND:New( group, "My Strafe Results", _statsPath, self._DisplayMyStrafePitResults, self, _unitName )
|
|
local _MyBR = MENU_GROUP_COMMAND:New( group, "My Bomb Results", _statsPath, self._DisplayMyBombingResults, self, _unitName )
|
|
local _ResetST = MENU_GROUP_COMMAND:New( group, "Reset All Stats", _statsPath, self._ResetRangeStats, self, _unitName )
|
|
-- F10/On the Range/<Range Name>/My Settings/Smoke Color/
|
|
local _BlueSM = MENU_GROUP_COMMAND:New( group, "Blue Smoke", _mysmokePath, self._playersmokecolor, self, _unitName, SMOKECOLOR.Blue )
|
|
local _GrSM = MENU_GROUP_COMMAND:New( group, "Green Smoke", _mysmokePath, self._playersmokecolor, self, _unitName, SMOKECOLOR.Green )
|
|
local _OrSM = MENU_GROUP_COMMAND:New( group, "Orange Smoke", _mysmokePath, self._playersmokecolor, self, _unitName, SMOKECOLOR.Orange )
|
|
local _ReSM = MENU_GROUP_COMMAND:New( group, "Red Smoke", _mysmokePath, self._playersmokecolor, self, _unitName, SMOKECOLOR.Red )
|
|
local _WhSm = MENU_GROUP_COMMAND:New( group, "White Smoke", _mysmokePath, self._playersmokecolor, self, _unitName, SMOKECOLOR.White )
|
|
-- F10/On the Range/<Range Name>/My Settings/Flare Color/
|
|
local _GrFl = MENU_GROUP_COMMAND:New( group, "Green Flares", _myflarePath, self._playerflarecolor, self, _unitName, FLARECOLOR.Green )
|
|
local _ReFl = MENU_GROUP_COMMAND:New( group, "Red Flares", _myflarePath, self._playerflarecolor, self, _unitName, FLARECOLOR.Red )
|
|
local _WhFl = MENU_GROUP_COMMAND:New( group, "White Flares", _myflarePath, self._playerflarecolor, self, _unitName, FLARECOLOR.White )
|
|
local _YeFl = MENU_GROUP_COMMAND:New( group, "Yellow Flares", _myflarePath, self._playerflarecolor, self, _unitName, FLARECOLOR.Yellow )
|
|
-- F10/On the Range/<Range Name>/My Settings/
|
|
local _SmDe = MENU_GROUP_COMMAND:New( group, "Smoke Delay On/Off", _settingsPath, self._SmokeBombDelayOnOff, self, _unitName )
|
|
local _SmIm = MENU_GROUP_COMMAND:New( group, "Smoke Impact On/Off", _settingsPath, self._SmokeBombImpactOnOff, self, _unitName )
|
|
local _FlHi = MENU_GROUP_COMMAND:New( group, "Flare Hits On/Off", _settingsPath, self._FlareDirectHitsOnOff, self, _unitName )
|
|
local _AlMeA = MENU_GROUP_COMMAND:New( group, "All Messages On/Off", _settingsPath, self._MessagesToPlayerOnOff, self, _unitName )
|
|
local _TrpSh = MENU_GROUP_COMMAND:New( group, "Targetsheet On/Off", _settingsPath, self._TargetsheetOnOff, self, _unitName )
|
|
|
|
-- F10/On the Range/<Range Name>/Range Information
|
|
local _WeIn = MENU_GROUP_COMMAND:New( group, "General Info", _infoPath, self._DisplayRangeInfo, self, _unitName )
|
|
local _WeRe = MENU_GROUP_COMMAND:New( group, "Weather Report", _infoPath, self._DisplayRangeWeather, self, _unitName )
|
|
local _BoTgtgs = MENU_GROUP_COMMAND:New( group, "Bombing Targets", _infoPath, self._DisplayBombTargets, self, _unitName )
|
|
local _StrPits = MENU_GROUP_COMMAND:New( group, "Strafe Pits", _infoPath, self._DisplayStrafePits, self, _unitName ):Refresh()
|
|
end
|
|
else
|
|
self:E( self.id .. "Could not find group or group ID in AddF10Menu() function. Unit name: " .. _unitName or "N/A")
|
|
end
|
|
else
|
|
self:E( self.id .. "Player unit does not exist in AddF10Menu() function. Unit name: " .. _unitName or "N/A")
|
|
end
|
|
|
|
end
|
|
|
|
-----------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
|
-- Helper Functions
|
|
-----------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
|
|
|
--- Get the number of shells a unit currently has.
|
|
-- @param #RANGE self
|
|
-- @param #RANGE.BombTarget target Bomb target data.
|
|
-- @return Core.Point#COORDINATE Target coordinate.
|
|
function RANGE:_GetBombTargetCoordinate( target )
|
|
|
|
local coord = nil -- Core.Point#COORDINATE
|
|
|
|
if target.type == RANGE.TargetType.UNIT then
|
|
|
|
if not target.move then
|
|
-- Target should not move.
|
|
coord = target.coordinate
|
|
else
|
|
-- Moving target. Check if alive and get current position
|
|
if target.target and target.target:IsAlive() then
|
|
coord = target.target:GetCoordinate()
|
|
end
|
|
end
|
|
|
|
elseif target.type == RANGE.TargetType.STATIC then
|
|
|
|
-- Static targets dont move.
|
|
coord = target.coordinate
|
|
|
|
elseif target.type == RANGE.TargetType.COORD then
|
|
|
|
-- Coordinates dont move.
|
|
coord = target.coordinate
|
|
|
|
else
|
|
self:E( self.id .. "ERROR: Unknown target type." )
|
|
end
|
|
|
|
return coord
|
|
end
|
|
|
|
--- Get the number of shells a unit currently has.
|
|
-- @param #RANGE self
|
|
-- @param #string unitname Name of the player unit.
|
|
-- @return Number of shells left
|
|
function RANGE:_GetAmmo( unitname )
|
|
self:F2( unitname )
|
|
|
|
-- Init counter.
|
|
local ammo = 0
|
|
|
|
local unit, playername = self:_GetPlayerUnitAndName( unitname )
|
|
|
|
if unit and playername then
|
|
|
|
local has_ammo = false
|
|
|
|
local ammotable = unit:GetAmmo()
|
|
self:T2( { ammotable = ammotable } )
|
|
|
|
if ammotable ~= nil then
|
|
|
|
local weapons = #ammotable
|
|
self:T2( self.id .. string.format( "Number of weapons %d.", weapons ) )
|
|
|
|
for w = 1, weapons do
|
|
|
|
local Nammo = ammotable[w]["count"]
|
|
local Tammo = ammotable[w]["desc"]["typeName"]
|
|
|
|
-- We are specifically looking for shells here.
|
|
if string.match( Tammo, "shell" ) then
|
|
|
|
-- Add up all shells
|
|
ammo = ammo + Nammo
|
|
|
|
local text = string.format( "Player %s has %d rounds ammo of type %s", playername, Nammo, Tammo )
|
|
self:T( self.id .. text )
|
|
else
|
|
local text = string.format( "Player %s has %d ammo of type %s", playername, Nammo, Tammo )
|
|
self:T( self.id .. text )
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
return ammo
|
|
end
|
|
|
|
--- Mark targets on F10 map.
|
|
-- @param #RANGE self
|
|
-- @param #string _unitName Name of the player unit.
|
|
function RANGE:_MarkTargetsOnMap( _unitName )
|
|
self:F( _unitName )
|
|
|
|
-- Get group.
|
|
local group = nil -- Wrapper.Group#GROUP
|
|
if _unitName then
|
|
group = UNIT:FindByName( _unitName ):GetGroup()
|
|
end
|
|
|
|
-- Mark bomb targets.
|
|
for _, _bombtarget in pairs( self.bombingTargets ) do
|
|
local bombtarget = _bombtarget -- #RANGE.BombTarget
|
|
local coord = self:_GetBombTargetCoordinate( _bombtarget )
|
|
if group then
|
|
coord:MarkToGroup( string.format( "Bomb target %s:\n%s\n%s", bombtarget.name, coord:ToStringLLDMS(), coord:ToStringBULLS( group:GetCoalition() ) ), group )
|
|
else
|
|
coord:MarkToAll( string.format( "Bomb target %s", bombtarget.name ) )
|
|
end
|
|
end
|
|
|
|
-- Mark strafe targets.
|
|
for _, _strafepit in pairs( self.strafeTargets ) do
|
|
for _, _target in pairs( _strafepit.targets ) do
|
|
local _target = _target -- Wrapper.Positionable#POSITIONABLE
|
|
if _target and _target:IsAlive() then
|
|
local coord = _target:GetCoordinate() -- Core.Point#COORDINATE
|
|
if group then
|
|
-- coord:MarkToGroup("Strafe target ".._target:GetName(), group)
|
|
coord:MarkToGroup( string.format( "Strafe target %s:\n%s\n%s", _target:GetName(), coord:ToStringLLDMS(), coord:ToStringBULLS( group:GetCoalition() ) ), group )
|
|
else
|
|
coord:MarkToAll( "Strafe target " .. _target:GetName() )
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
if _unitName then
|
|
local _unit, _playername = self:_GetPlayerUnitAndName( _unitName )
|
|
local text = string.format( "%s, %s, range targets are now marked on F10 map.", self.rangename, _playername )
|
|
self:_DisplayMessageToGroup( _unit, text, 5 )
|
|
end
|
|
|
|
end
|
|
|
|
--- Illuminate targets. Fires illumination bombs at one random bomb and one random strafe target at a random altitude between 400 and 800 m.
|
|
-- @param #RANGE self
|
|
-- @param #string _unitName (Optional) Name of the player unit.
|
|
function RANGE:_IlluminateBombTargets( _unitName )
|
|
self:F( _unitName )
|
|
|
|
-- All bombing target coordinates.
|
|
local bomb = {}
|
|
|
|
for _, _bombtarget in pairs( self.bombingTargets ) do
|
|
local _target = _bombtarget.target -- Wrapper.Positionable#POSITIONABLE
|
|
local coord = self:_GetBombTargetCoordinate( _bombtarget )
|
|
if coord then
|
|
table.insert( bomb, coord )
|
|
end
|
|
end
|
|
|
|
if #bomb > 0 then
|
|
local coord = bomb[math.random( #bomb )] -- Core.Point#COORDINATE
|
|
local c = COORDINATE:New( coord.x, coord.y + math.random( self.illuminationminalt, self.illuminationmaxalt ), coord.z )
|
|
c:IlluminationBomb()
|
|
end
|
|
|
|
-- All strafe target coordinates.
|
|
local strafe = {}
|
|
|
|
for _, _strafepit in pairs( self.strafeTargets ) do
|
|
for _, _target in pairs( _strafepit.targets ) do
|
|
local _target = _target -- Wrapper.Positionable#POSITIONABLE
|
|
if _target and _target:IsAlive() then
|
|
local coord = _target:GetCoordinate() -- Core.Point#COORDINATE
|
|
table.insert( strafe, coord )
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Pick a random strafe target.
|
|
if #strafe > 0 then
|
|
local coord = strafe[math.random( #strafe )] -- Core.Point#COORDINATE
|
|
local c = COORDINATE:New( coord.x, coord.y + math.random( self.illuminationminalt, self.illuminationmaxalt ), coord.z )
|
|
c:IlluminationBomb()
|
|
end
|
|
|
|
if _unitName then
|
|
local _unit, _playername = self:_GetPlayerUnitAndName( _unitName )
|
|
local text = string.format( "%s, %s, range targets are illuminated.", self.rangename, _playername )
|
|
self:_DisplayMessageToGroup( _unit, text, 5 )
|
|
end
|
|
end
|
|
|
|
--- Reset player statistics.
|
|
-- @param #RANGE self
|
|
-- @param #string _unitName Name of the player unit.
|
|
function RANGE:_ResetRangeStats( _unitName )
|
|
self:F( _unitName )
|
|
|
|
-- Get player unit and name.
|
|
local _unit, _playername = self:_GetPlayerUnitAndName( _unitName )
|
|
|
|
if _unit and _playername then
|
|
self.strafePlayerResults[_playername] = nil
|
|
self.bombPlayerResults[_playername] = nil
|
|
local text = string.format( "%s, %s, your range stats were cleared.", self.rangename, _playername )
|
|
self:DisplayMessageToGroup( _unit, text, 5, false, true )
|
|
end
|
|
end
|
|
|
|
--- Display message to group.
|
|
-- @param #RANGE self
|
|
-- @param Wrapper.Unit#UNIT _unit Player unit.
|
|
-- @param #string _text Message text.
|
|
-- @param #number _time Duration how long the message is displayed.
|
|
-- @param #boolean _clear Clear up old messages.
|
|
-- @param #boolean display If true, display message regardless of player setting "Messages Off".
|
|
function RANGE:_DisplayMessageToGroup( _unit, _text, _time, _clear, display )
|
|
self:F( { unit = _unit, text = _text, time = _time, clear = _clear } )
|
|
|
|
-- Defaults
|
|
_time = _time or self.Tmsg
|
|
if _clear == nil or _clear == false then
|
|
_clear = false
|
|
else
|
|
_clear = true
|
|
end
|
|
|
|
-- Messages globally disabled.
|
|
if self.messages == false then
|
|
return
|
|
end
|
|
|
|
-- Check if unit is alive.
|
|
if _unit and _unit:IsAlive() then
|
|
|
|
-- Group ID.
|
|
local _gid = _unit:GetGroup():GetID()
|
|
local _grp = _unit:GetGroup()
|
|
|
|
-- Get playername and player settings
|
|
local _, playername = self:_GetPlayerUnitAndName( _unit:GetName() )
|
|
local playermessage = self.PlayerSettings[playername].messages
|
|
|
|
-- Send message to player if messages enabled and not only for the examiner.
|
|
if _gid and (playermessage == true or display) and (not self.examinerexclusive) then
|
|
local m = MESSAGE:New(_text,_time,nil,_clear):ToUnit(_unit)
|
|
end
|
|
|
|
-- Send message to examiner.
|
|
if self.examinergroupname ~= nil then
|
|
local _examinerid = GROUP:FindByName( self.examinergroupname )
|
|
if _examinerid then
|
|
local m = MESSAGE:New(_text,_time,nil,_clear):ToGroup(_examinerid)
|
|
end
|
|
end
|
|
end
|
|
|
|
end
|
|
|
|
--- Toggle status of smoking bomb impact points.
|
|
-- @param #RANGE self
|
|
-- @param #string unitname Name of the player unit.
|
|
function RANGE:_SmokeBombImpactOnOff( unitname )
|
|
self:F( unitname )
|
|
|
|
local unit, playername = self:_GetPlayerUnitAndName( unitname )
|
|
if unit and playername then
|
|
local text
|
|
if self.PlayerSettings[playername].smokebombimpact == true then
|
|
self.PlayerSettings[playername].smokebombimpact = false
|
|
text = string.format( "%s, %s, smoking impact points of bombs is now OFF.", self.rangename, playername )
|
|
else
|
|
self.PlayerSettings[playername].smokebombimpact = true
|
|
text = string.format( "%s, %s, smoking impact points of bombs is now ON.", self.rangename, playername )
|
|
end
|
|
self:_DisplayMessageToGroup( unit, text, 5, false, true )
|
|
end
|
|
|
|
end
|
|
|
|
--- Toggle status of time delay for smoking bomb impact points
|
|
-- @param #RANGE self
|
|
-- @param #string unitname Name of the player unit.
|
|
function RANGE:_SmokeBombDelayOnOff( unitname )
|
|
self:F( unitname )
|
|
|
|
local unit, playername = self:_GetPlayerUnitAndName( unitname )
|
|
if unit and playername then
|
|
local text
|
|
if self.PlayerSettings[playername].delaysmoke == true then
|
|
self.PlayerSettings[playername].delaysmoke = false
|
|
text = string.format( "%s, %s, delayed smoke of bombs is now OFF.", self.rangename, playername )
|
|
else
|
|
self.PlayerSettings[playername].delaysmoke = true
|
|
text = string.format( "%s, %s, delayed smoke of bombs is now ON.", self.rangename, playername )
|
|
end
|
|
self:_DisplayMessageToGroup( unit, text, 5, false, true )
|
|
end
|
|
|
|
end
|
|
|
|
--- Toggle display messages to player.
|
|
-- @param #RANGE self
|
|
-- @param #string unitname Name of the player unit.
|
|
function RANGE:_MessagesToPlayerOnOff( unitname )
|
|
self:F( unitname )
|
|
|
|
local unit, playername = self:_GetPlayerUnitAndName( unitname )
|
|
if unit and playername then
|
|
local text
|
|
if self.PlayerSettings[playername].messages == true then
|
|
text = string.format( "%s, %s, display of ALL messages is now OFF.", self.rangename, playername )
|
|
else
|
|
text = string.format( "%s, %s, display of ALL messages is now ON.", self.rangename, playername )
|
|
end
|
|
self:_DisplayMessageToGroup( unit, text, 5, false, true )
|
|
self.PlayerSettings[playername].messages = not self.PlayerSettings[playername].messages
|
|
end
|
|
|
|
end
|
|
|
|
--- Targetsheet saves if player on or off.
|
|
-- @param #RANGE self
|
|
-- @param #string _unitname Name of the player unit.
|
|
function RANGE:_TargetsheetOnOff( _unitname )
|
|
self:F2( _unitname )
|
|
|
|
-- Get player unit and player name.
|
|
local unit, playername = self:_GetPlayerUnitAndName( _unitname )
|
|
|
|
-- Check if we have a player.
|
|
if unit and playername then
|
|
|
|
-- Player data.
|
|
local playerData = self.PlayerSettings[playername] -- #RANGE.PlayerData
|
|
|
|
if playerData then
|
|
|
|
-- Check if option is enabled at all.
|
|
local text = ""
|
|
if self.targetsheet then
|
|
|
|
-- Invert current setting.
|
|
playerData.targeton = not playerData.targeton
|
|
|
|
-- Inform player.
|
|
if playerData and playerData.targeton == true then
|
|
text = string.format( "roger, your targetsheets are now SAVED." )
|
|
else
|
|
text = string.format( "affirm, your targetsheets are NOT SAVED." )
|
|
end
|
|
|
|
else
|
|
text = "negative, target sheet data recorder is broken on this range."
|
|
end
|
|
|
|
-- Message to player.
|
|
-- self:MessageToPlayer(playerData, text, nil, playerData.name, 5)
|
|
self:_DisplayMessageToGroup( unit, text, 5, false, false )
|
|
end
|
|
end
|
|
|
|
end
|
|
|
|
--- Toggle status of flaring direct hits of range targets.
|
|
-- @param #RANGE self
|
|
-- @param #string unitname Name of the player unit.
|
|
function RANGE:_FlareDirectHitsOnOff( unitname )
|
|
self:F( unitname )
|
|
|
|
local unit, playername = self:_GetPlayerUnitAndName( unitname )
|
|
if unit and playername then
|
|
local text
|
|
if self.PlayerSettings[playername].flaredirecthits == true then
|
|
self.PlayerSettings[playername].flaredirecthits = false
|
|
text = string.format( "%s, %s, flaring direct hits is now OFF.", self.rangename, playername )
|
|
else
|
|
self.PlayerSettings[playername].flaredirecthits = true
|
|
text = string.format( "%s, %s, flaring direct hits is now ON.", self.rangename, playername )
|
|
end
|
|
self:_DisplayMessageToGroup( unit, text, 5, false, true )
|
|
end
|
|
|
|
end
|
|
|
|
--- Mark bombing targets with smoke.
|
|
-- @param #RANGE self
|
|
-- @param #string unitname Name of the player unit.
|
|
function RANGE:_SmokeBombTargets( unitname )
|
|
self:F( unitname )
|
|
|
|
for _, _bombtarget in pairs( self.bombingTargets ) do
|
|
local _target = _bombtarget.target -- Wrapper.Positionable#POSITIONABLE
|
|
local coord = self:_GetBombTargetCoordinate( _bombtarget )
|
|
if coord then
|
|
coord:Smoke( self.BombSmokeColor )
|
|
end
|
|
end
|
|
|
|
if unitname then
|
|
local unit, playername = self:_GetPlayerUnitAndName( unitname )
|
|
local text = string.format( "%s, %s, bombing targets are now marked with %s smoke.", self.rangename, playername, self:_smokecolor2text( self.BombSmokeColor ) )
|
|
self:_DisplayMessageToGroup( unit, text, 5 )
|
|
end
|
|
|
|
end
|
|
|
|
--- Mark strafing targets with smoke.
|
|
-- @param #RANGE self
|
|
-- @param #string unitname Name of the player unit.
|
|
function RANGE:_SmokeStrafeTargets( unitname )
|
|
self:F( unitname )
|
|
|
|
for _, _target in pairs( self.strafeTargets ) do
|
|
_target.coordinate:Smoke( self.StrafeSmokeColor )
|
|
end
|
|
|
|
if unitname then
|
|
local unit, playername = self:_GetPlayerUnitAndName( unitname )
|
|
local text = string.format( "%s, %s, strafing tragets are now marked with %s smoke.", self.rangename, playername, self:_smokecolor2text( self.StrafeSmokeColor ) )
|
|
self:_DisplayMessageToGroup( unit, text, 5 )
|
|
end
|
|
|
|
end
|
|
|
|
--- Mark approach boxes of strafe targets with smoke.
|
|
-- @param #RANGE self
|
|
-- @param #string unitname Name of the player unit.
|
|
function RANGE:_SmokeStrafeTargetBoxes( unitname )
|
|
self:F( unitname )
|
|
|
|
for _, _target in pairs( self.strafeTargets ) do
|
|
local zone = _target.polygon -- Core.Zone#ZONE
|
|
zone:SmokeZone( self.StrafePitSmokeColor, 4 )
|
|
for _, _point in pairs( _target.smokepoints ) do
|
|
_point:SmokeOrange() -- Corners are smoked orange.
|
|
end
|
|
end
|
|
|
|
if unitname then
|
|
local unit, playername = self:_GetPlayerUnitAndName( unitname )
|
|
local text = string.format( "%s, %s, strafing pit approach boxes are now marked with %s smoke.", self.rangename, playername, self:_smokecolor2text( self.StrafePitSmokeColor ) )
|
|
self:_DisplayMessageToGroup( unit, text, 5 )
|
|
end
|
|
|
|
end
|
|
|
|
--- Sets the smoke color used to smoke players bomb impact points.
|
|
-- @param #RANGE self
|
|
-- @param #string _unitName Name of the player unit.
|
|
-- @param Utilities.Utils#SMOKECOLOR color ID of the smoke color.
|
|
function RANGE:_playersmokecolor( _unitName, color )
|
|
self:F( { unitname = _unitName, color = color } )
|
|
|
|
local _unit, _playername = self:_GetPlayerUnitAndName( _unitName )
|
|
if _unit and _playername then
|
|
self.PlayerSettings[_playername].smokecolor = color
|
|
local text = string.format( "%s, %s, your bomb impacts are now smoked in %s.", self.rangename, _playername, self:_smokecolor2text( color ) )
|
|
self:_DisplayMessageToGroup( _unit, text, 5 )
|
|
end
|
|
|
|
end
|
|
|
|
--- Sets the flare color used when player makes a direct hit on target.
|
|
-- @param #RANGE self
|
|
-- @param #string _unitName Name of the player unit.
|
|
-- @param Utilities.Utils#FLARECOLOR color ID of flare color.
|
|
function RANGE:_playerflarecolor( _unitName, color )
|
|
self:F( { unitname = _unitName, color = color } )
|
|
|
|
local _unit, _playername = self:_GetPlayerUnitAndName( _unitName )
|
|
if _unit and _playername then
|
|
self.PlayerSettings[_playername].flarecolor = color
|
|
local text = string.format( "%s, %s, your direct hits are now flared in %s.", self.rangename, _playername, self:_flarecolor2text( color ) )
|
|
self:_DisplayMessageToGroup( _unit, text, 5 )
|
|
end
|
|
|
|
end
|
|
|
|
--- Converts a smoke color id to text. E.g. SMOKECOLOR.Blue --> "blue".
|
|
-- @param #RANGE self
|
|
-- @param Utilities.Utils#SMOKECOLOR color Color Id.
|
|
-- @return #string Color text.
|
|
function RANGE:_smokecolor2text( color )
|
|
self:F( color )
|
|
|
|
local txt = ""
|
|
if color == SMOKECOLOR.Blue then
|
|
txt = "blue"
|
|
elseif color == SMOKECOLOR.Green then
|
|
txt = "green"
|
|
elseif color == SMOKECOLOR.Orange then
|
|
txt = "orange"
|
|
elseif color == SMOKECOLOR.Red then
|
|
txt = "red"
|
|
elseif color == SMOKECOLOR.White then
|
|
txt = "white"
|
|
else
|
|
txt = string.format( "unknown color (%s)", tostring( color ) )
|
|
end
|
|
|
|
return txt
|
|
end
|
|
|
|
--- Sets the flare color used to flare players direct target hits.
|
|
-- @param #RANGE self
|
|
-- @param Utilities.Utils#FLARECOLOR color Color Id.
|
|
-- @return #string Color text.
|
|
function RANGE:_flarecolor2text( color )
|
|
self:F( color )
|
|
|
|
local txt = ""
|
|
if color == FLARECOLOR.Green then
|
|
txt = "green"
|
|
elseif color == FLARECOLOR.Red then
|
|
txt = "red"
|
|
elseif color == FLARECOLOR.White then
|
|
txt = "white"
|
|
elseif color == FLARECOLOR.Yellow then
|
|
txt = "yellow"
|
|
else
|
|
txt = string.format( "unknown color (%s)", tostring( color ) )
|
|
end
|
|
|
|
return txt
|
|
end
|
|
|
|
--- Checks if a static object with a certain name exists. It also added it to the MOOSE data base, if it is not already in there.
|
|
-- @param #RANGE self
|
|
-- @param #string name Name of the potential static object.
|
|
-- @return #boolean Returns true if a static with this name exists. Retruns false if a unit with this name exists. Returns nil if neither unit or static exist.
|
|
function RANGE:_CheckStatic( name )
|
|
self:F2( name )
|
|
|
|
-- Get DCS static object.
|
|
local _DCSstatic = StaticObject.getByName( name )
|
|
|
|
if _DCSstatic and _DCSstatic:isExist() then
|
|
|
|
-- Static does exist at least in DCS. Check if it also in the MOOSE DB.
|
|
local _MOOSEstatic = STATIC:FindByName( name, false )
|
|
|
|
-- If static is not yet in MOOSE DB, we add it. Can happen for cargo statics!
|
|
if not _MOOSEstatic then
|
|
self:T( self.id .. string.format( "Adding DCS static to MOOSE database. Name = %s.", name ) )
|
|
_DATABASE:AddStatic( name )
|
|
end
|
|
|
|
return true
|
|
else
|
|
self:T3( self.id .. string.format( "No static object with name %s exists.", name ) )
|
|
end
|
|
|
|
-- Check if a unit has this name.
|
|
if UNIT:FindByName( name ) then
|
|
return false
|
|
else
|
|
self:T3( self.id .. string.format( "No unit object with name %s exists.", name ) )
|
|
end
|
|
|
|
-- If not unit or static exist, we return nil.
|
|
return nil
|
|
end
|
|
|
|
--- Get max speed of controllable.
|
|
-- @param #RANGE self
|
|
-- @param Wrapper.Controllable#CONTROLLABLE controllable
|
|
-- @return Maximum speed in km/h.
|
|
function RANGE:_GetSpeed( controllable )
|
|
self:F2( controllable )
|
|
|
|
-- Get DCS descriptors
|
|
local desc = controllable:GetDesc()
|
|
|
|
-- Get speed
|
|
local speed = 0
|
|
if desc then
|
|
speed = desc.speedMax * 3.6
|
|
self:T( { speed = speed } )
|
|
end
|
|
|
|
return speed
|
|
end
|
|
|
|
--- Returns the unit of a player and the player name. If the unit does not belong to a player, nil is returned.
|
|
-- @param #RANGE self
|
|
-- @param #string _unitName Name of the player unit.
|
|
-- @return Wrapper.Unit#UNIT Unit of player.
|
|
-- @return #string Name of the player.
|
|
-- @return nil If player does not exist.
|
|
function RANGE:_GetPlayerUnitAndName( _unitName )
|
|
self:F2( _unitName )
|
|
|
|
if _unitName ~= nil then
|
|
|
|
-- Get DCS unit from its name.
|
|
local DCSunit = Unit.getByName( _unitName )
|
|
|
|
if DCSunit then
|
|
|
|
local playername = DCSunit:getPlayerName()
|
|
local unit = UNIT:Find( DCSunit )
|
|
|
|
self:T2( { DCSunit = DCSunit, unit = unit, playername = playername } )
|
|
if DCSunit and unit and playername then
|
|
self:F2(playername)
|
|
return unit, playername
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
-- Return nil if we could not find a player.
|
|
return nil, nil
|
|
end
|
|
|
|
--- Returns a string which consists of the player name.
|
|
-- @param #RANGE self
|
|
-- @param #string unitname Name of the player unit.
|
|
function RANGE:_myname( unitname )
|
|
self:F2( unitname )
|
|
local pname = "Ghost 1 1"
|
|
local unit = UNIT:FindByName( unitname )
|
|
if unit and unit:IsAlive() then
|
|
local grp = unit:GetGroup()
|
|
if grp and grp:IsAlive() then
|
|
pname = grp:GetCustomCallSign(true,true)
|
|
end
|
|
end
|
|
return pname
|
|
end
|
|
|
|
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
|