From 9795d5655f89ae3a248c8f11caf60217496e32ba Mon Sep 17 00:00:00 2001 From: Frank Date: Sun, 9 Dec 2018 12:01:15 +0100 Subject: [PATCH] Improvements and Fixes from FF/Develop --- Moose Development/Moose/AI/AI_Formation.lua | 44 +- Moose Development/Moose/Core/Point.lua | 34 +- Moose Development/Moose/Core/Radio.lua | 431 +++++++++++++----- Moose Development/Moose/Core/Set.lua | 4 +- Moose Development/Moose/Core/SpawnStatic.lua | 43 ++ Moose Development/Moose/Core/UserFlag.lua | 2 +- Moose Development/Moose/Core/UserSound.lua | 12 +- Moose Development/Moose/Core/Zone.lua | 43 +- .../Moose/Functional/Artillery.lua | 182 ++++---- .../Moose/Functional/Detection.lua | 2 +- Moose Development/Moose/Functional/RAT.lua | 23 +- Moose Development/Moose/Functional/Range.lua | 27 +- Moose Development/Moose/Utilities/Utils.lua | 110 ++++- .../Moose/Wrapper/Controllable.lua | 190 +++++++- Moose Development/Moose/Wrapper/Group.lua | 16 +- .../Moose/Wrapper/Positionable.lua | 15 +- Moose Development/Moose/Wrapper/Unit.lua | 22 +- 17 files changed, 911 insertions(+), 289 deletions(-) diff --git a/Moose Development/Moose/AI/AI_Formation.lua b/Moose Development/Moose/AI/AI_Formation.lua index 489e69cf3..c02096609 100644 --- a/Moose Development/Moose/AI/AI_Formation.lua +++ b/Moose Development/Moose/AI/AI_Formation.lua @@ -36,6 +36,7 @@ -- @field #boolean ReportTargets If true, nearby targets are reported. -- @Field DCSTypes#AI.Option.Air.val.ROE OptionROE Which ROE is set to the FollowGroup. -- @field DCSTypes#AI.Option.Air.val.REACTION_ON_THREAT OptionReactionOnThreat Which REACTION_ON_THREAT is set to the FollowGroup. +-- @field #number dtFollow Time step between position updates. --- Build large formations, make AI follow a @{Wrapper.Client#CLIENT} (player) leader or a @{Wrapper.Unit#UNIT} (AI) leader. @@ -106,6 +107,7 @@ AI_FORMATION = { FollowScheduler = nil, OptionROE = AI.Option.Air.val.ROE.OPEN_FIRE, OptionReactionOnThreat = AI.Option.Air.val.REACTION_ON_THREAT.ALLOW_ABORT_MISSION, + dtFollow = 0.5, } --- AI_FORMATION.Mode class @@ -125,6 +127,7 @@ AI_FORMATION = { -- @param Wrapper.Unit#UNIT FollowUnit The UNIT leading the FolllowGroupSet. -- @param Core.Set#SET_GROUP FollowGroupSet The group AI escorting the FollowUnit. -- @param #string FollowName Name of the escort. +-- @param #string FollowBriefing Briefing. -- @return #AI_FORMATION self function AI_FORMATION:New( FollowUnit, FollowGroupSet, FollowName, FollowBriefing ) --R2.1 local self = BASE:Inherit( self, FSM_SET:New( FollowGroupSet ) ) @@ -139,7 +142,7 @@ function AI_FORMATION:New( FollowUnit, FollowGroupSet, FollowName, FollowBriefin self:AddTransition( "*", "Stop", "Stopped" ) - self:AddTransition( "None", "Start", "Following" ) + self:AddTransition( {"None", "Stopped"}, "Start", "Following" ) self:AddTransition( "*", "FormationLine", "*" ) --- FormationLine Handler OnBefore for AI_FORMATION @@ -620,6 +623,16 @@ function AI_FORMATION:New( FollowUnit, FollowGroupSet, FollowName, FollowBriefin return self end + +--- Set time interval between updates of the formation. +-- @param #AI_FORMATION self +-- @param #number dt Time step in seconds between formation updates. Default is every 0.5 seconds. +-- @return #AI_FORMATION +function AI_FORMATION:SetFollowTimeInterval(dt) --R2.1 + self.dtFollow=dt or 0.5 + return self +end + --- This function is for test, it will put on the frequency of the FollowScheduler a red smoke at the direction vector calculated for the escort to fly to. -- This allows to visualize where the escort is flying to. -- @param #AI_FORMATION self @@ -893,7 +906,30 @@ function AI_FORMATION:SetFlightRandomization( FlightRandomization ) --R2.1 end ---- @param Follow#AI_FORMATION self +--- Stop function. Formation will not be updated any more. +-- @param #AI_FORMATION self +-- @param Core.Set#SET_GROUP FollowGroupSet The following set of groups. +-- @param #string From From state. +-- @param #string Event Event. +-- @pram #string To The to state. +function AI_FORMATION:onafterStop(FollowGroupSet, From, Event, To) --R2.1 + self:E("Stopping formation.") +end + +--- Follow event fuction. Check if coming from state "stopped". If so the transition is rejected. +-- @param #AI_FORMATION self +-- @param Core.Set#SET_GROUP FollowGroupSet The following set of groups. +-- @param #string From From state. +-- @param #string Event Event. +-- @pram #string To The to state. +function AI_FORMATION:onbeforeFollow( FollowGroupSet, From, Event, To ) --R2.1 + if From=="Stopped" then + return false -- Deny transition. + end + return true +end + +--- @param #AI_FORMATION self function AI_FORMATION:onenterFollowing( FollowGroupSet ) --R2.1 self:F( ) @@ -1032,8 +1068,8 @@ function AI_FORMATION:onenterFollowing( FollowGroupSet ) --R2.1 end, self, ClientUnit, CT1, CV1, CT2, CV2 ) - - self:__Follow( -0.5 ) + + self:__Follow( -self.dtFollow ) end end diff --git a/Moose Development/Moose/Core/Point.lua b/Moose Development/Moose/Core/Point.lua index 528ac9a84..366ad03e1 100644 --- a/Moose Development/Moose/Core/Point.lua +++ b/Moose Development/Moose/Core/Point.lua @@ -342,18 +342,18 @@ do -- COORDINATE return x - Precision <= self.x and x + Precision >= self.x and z - Precision <= self.z and z + Precision >= self.z end - --- Returns if the 2 coordinates are at the same 2D position. + --- Scan/find objects (units, statics, scenery) within a certain radius around the coordinate using the world.searchObjects() DCS API function. -- @param #COORDINATE self -- @param #number radius (Optional) Scan radius in meters. Default 100 m. -- @param #boolean scanunits (Optional) If true scan for units. Default true. -- @param #boolean scanstatics (Optional) If true scan for static objects. Default true. -- @param #boolean scanscenery (Optional) If true scan for scenery objects. Default false. - -- @return True if units were found. - -- @return True if statics were found. - -- @return True if scenery objects were found. - -- @return Unit objects found. - -- @return Static objects found. - -- @return Scenery objects found. + -- @return #boolean True if units were found. + -- @return #boolean True if statics were found. + -- @return #boolean True if scenery objects were found. + -- @return #table Table of MOOSE @[#Wrapper.Unit#UNIT} objects found. + -- @return #table Table of DCS static objects found. + -- @return #table Table of DCS scenery objects found. function COORDINATE:ScanObjects(radius, scanunits, scanstatics, scanscenery) self:F(string.format("Scanning in radius %.1f m.", radius)) @@ -405,18 +405,17 @@ do -- COORDINATE local ObjectCategory = ZoneObject:getCategory() -- Check for unit or static objects - --if (ObjectCategory == Object.Category.UNIT and ZoneObject:isExist() and ZoneObject:isActive()) then - if (ObjectCategory == Object.Category.UNIT and ZoneObject:isExist()) then + if ObjectCategory==Object.Category.UNIT and ZoneObject:isExist() then table.insert(Units, UNIT:Find(ZoneObject)) gotunits=true - elseif (ObjectCategory == Object.Category.STATIC and ZoneObject:isExist()) then + elseif ObjectCategory==Object.Category.STATIC and ZoneObject:isExist() then table.insert(Statics, ZoneObject) gotstatics=true - elseif ObjectCategory == Object.Category.SCENERY then + elseif ObjectCategory==Object.Category.SCENERY then table.insert(Scenery, ZoneObject) gotscenery=true @@ -460,12 +459,12 @@ do -- COORDINATE --- Add a Distance in meters from the COORDINATE orthonormal plane, with the given angle, and calculate the new COORDINATE. -- @param #COORDINATE self -- @param DCS#Distance Distance The Distance to be added in meters. - -- @param DCS#Angle Angle The Angle in degrees. - -- @return #COORDINATE The new calculated COORDINATE. + -- @param DCS#Angle Angle The Angle in degrees. Defaults to 0 if not specified (nil). + -- @return Core.Point#COORDINATE The new calculated COORDINATE. function COORDINATE:Translate( Distance, Angle ) local SX = self.x local SY = self.z - local Radians = Angle / 180 * math.pi + local Radians = (Angle or 0) / 180 * math.pi local TX = Distance * math.cos( Radians ) + SX local TY = Distance * math.sin( Radians ) + SY @@ -1121,6 +1120,9 @@ do -- COORDINATE --- Build a Waypoint Air "Landing". -- @param #COORDINATE self -- @param DCS#Speed Speed Airspeed in km/h. + -- @param Wrapper.Airbase#AIRBASE airbase The airbase for takeoff and landing points. + -- @param #table DCSTasks A table of @{DCS#Task} items which are executed at the waypoint. + -- @param #string description A text description of the waypoint, which will be shown on the F10 map. -- @return #table The route point. -- @usage -- @@ -1129,8 +1131,8 @@ do -- COORDINATE -- LandingWaypoint = LandingCoord:WaypointAirLanding( 60 ) -- HeliGroup:Route( { LandWaypoint }, 1 ) -- Start landing the helicopter in one second. -- - function COORDINATE:WaypointAirLanding( Speed ) - return self:WaypointAir( nil, COORDINATE.WaypointType.Land, COORDINATE.WaypointAction.Landing, Speed ) + function COORDINATE:WaypointAirLanding( Speed, airbase, DCSTasks, description ) + return self:WaypointAir(nil, COORDINATE.WaypointType.Land, COORDINATE.WaypointAction.Landing, Speed, nil, airbase, DCSTasks, description) end diff --git a/Moose Development/Moose/Core/Radio.lua b/Moose Development/Moose/Core/Radio.lua index 954fc51a9..662b7b94f 100644 --- a/Moose Development/Moose/Core/Radio.lua +++ b/Moose Development/Moose/Core/Radio.lua @@ -9,12 +9,12 @@ -- -- The Radio contains 2 classes : RADIO and BEACON -- --- What are radio communications in DCS ? +-- What are radio communications in DCS? -- -- * Radio transmissions consist of **sound files** that are broadcasted on a specific **frequency** (e.g. 115MHz) and **modulation** (e.g. AM), -- * They can be **subtitled** for a specific **duration**, the **power** in Watts of the transmiter's antenna can be set, and the transmission can be **looped**. -- --- How to supply DCS my own Sound Files ? +-- How to supply DCS my own Sound Files? -- -- * Your sound files need to be encoded in **.ogg** or .wav, -- * Your sound files should be **as tiny as possible**. It is suggested you encode in .ogg with low bitrate and sampling settings, @@ -23,7 +23,7 @@ -- -- Due to weird DCS quirks, **radio communications behave differently** if sent by a @{Wrapper.Unit#UNIT} or a @{Wrapper.Group#GROUP} or by any other @{Wrapper.Positionable#POSITIONABLE} -- --- * If the transmitter is a @{Wrapper.Unit#UNIT} or a @{Wrapper.Group#GROUP}, DCS will set the power of the transmission automatically, +-- * If the transmitter is a @{Wrapper.Unit#UNIT} or a @{Wrapper.Group#GROUP}, DCS will set the power of the transmission automatically, -- * If the transmitter is any other @{Wrapper.Positionable#POSITIONABLE}, the transmisison can't be subtitled or looped. -- -- Note that obviously, the **frequency** and the **modulation** of the transmission are important only if the players are piloting an **Advanced System Modelling** enabled aircraft, @@ -33,7 +33,7 @@ -- -- === -- --- ### Author: Hugues "Grey_Echo" Bousquet +-- ### Authors: Hugues "Grey_Echo" Bousquet, funkyfranky -- -- @module Core.Radio -- @image Core_Radio.JPG @@ -66,24 +66,25 @@ -- * @{#RADIO.SetPower}() : Sets the power of the antenna in Watts -- * @{#RADIO.NewGenericTransmission}() : Shortcut to set all the relevant parameters in one method call -- --- What is this power thing ? +-- What is this power thing? -- -- * If your transmission is sent by a @{Wrapper.Positionable#POSITIONABLE} other than a @{Wrapper.Unit#UNIT} or a @{Wrapper.Group#GROUP}, you can set the power of the antenna, -- * Otherwise, DCS sets it automatically, depending on what's available on your Unit, --- * If the player gets **too far** from the transmiter, or if the antenna is **too weak**, the transmission will **fade** and **become noisyer**, +-- * If the player gets **too far** from the transmitter, or if the antenna is **too weak**, the transmission will **fade** and **become noisyer**, -- * This an automated DCS calculation you have no say on, --- * For reference, a standard VOR station has a 100W antenna, a standard AA TACAN has a 120W antenna, and civilian ATC's antenna usually range between 300 and 500W, +-- * For reference, a standard VOR station has a 100 W antenna, a standard AA TACAN has a 120 W antenna, and civilian ATC's antenna usually range between 300 and 500 W, -- * Note that if the transmission has a subtitle, it will be readable, regardless of the quality of the transmission. -- -- @type RADIO --- @field Positionable#POSITIONABLE Positionable The transmiter --- @field #string FileName Name of the sound file --- @field #number Frequency Frequency of the transmission in Hz --- @field #number Modulation Modulation of the transmission (either radio.modulation.AM or radio.modulation.FM) --- @field #string Subtitle Subtitle of the transmission --- @field #number SubtitleDuration Duration of the Subtitle in seconds --- @field #number Power Power of the antenna is Watts --- @field #boolean Loop (default true) +-- @field Wrapper.Controllable#CONTROLLABLE Positionable The @{#CONTROLLABLE} that will transmit the radio calls. +-- @field #string FileName Name of the sound file played. +-- @field #number Frequency Frequency of the transmission in Hz. +-- @field #number Modulation Modulation of the transmission (either radio.modulation.AM or radio.modulation.FM). +-- @field #string Subtitle Subtitle of the transmission. +-- @field #number SubtitleDuration Duration of the Subtitle in seconds. +-- @field #number Power Power of the antenna is Watts. +-- @field #boolean Loop Transmission is repeated (default true). +-- @field #string alias Name of the radio transmitter. -- @extends Core.Base#BASE RADIO = { ClassName = "RADIO", @@ -93,19 +94,19 @@ RADIO = { Subtitle = "", SubtitleDuration = 0, Power = 100, - Loop = true, + Loop = false, + alias=nil, } ---- Create a new RADIO Object. This doesn't broadcast a transmission, though, use @{#RADIO.Broadcast} to actually broadcast --- If you want to create a RADIO, you probably should use @{Wrapper.Positionable#POSITIONABLE.GetRadio}() instead +--- Create a new RADIO Object. This doesn't broadcast a transmission, though, use @{#RADIO.Broadcast} to actually broadcast. +-- If you want to create a RADIO, you probably should use @{Wrapper.Positionable#POSITIONABLE.GetRadio}() instead. -- @param #RADIO self -- @param Wrapper.Positionable#POSITIONABLE Positionable The @{Positionable} that will receive radio capabilities. --- @return #RADIO Radio --- @return #nil If Positionable is invalid +-- @return #RADIO The RADIO object or #nil if Positionable is invalid. function RADIO:New(Positionable) + + -- Inherit base local self = BASE:Inherit( self, BASE:New() ) -- Core.Radio#RADIO - - self.Loop = true -- default Loop to true (not sure the above RADIO definition actually is working) self:F(Positionable) if Positionable:GetPointVec2() then -- It's stupid, but the only way I found to make sure positionable is valid @@ -113,11 +114,27 @@ function RADIO:New(Positionable) return self end - self:E({"The passed positionable is invalid, no RADIO created", Positionable}) + self:E({error="The passed positionable is invalid, no RADIO created!", positionable=Positionable}) return nil end ---- Check validity of the filename passed and sets RADIO.FileName +--- Set alias of the transmitter. +-- @param #RADIO self +-- @param #string alias Name of the radio transmitter. +-- @return #RADIO self +function RADIO:SetAlias(alias) + self.alias=tostring(alias) + return self +end + +--- Get alias of the transmitter. +-- @param #RADIO self +-- @return #string Name of the transmitter. +function RADIO:GetAlias() + return tostring(self.alias) +end + +--- Set the file name for the radio transmission. -- @param #RADIO self -- @param #string FileName File name of the sound file (i.e. "Noise.ogg") -- @return #RADIO self @@ -125,49 +142,63 @@ function RADIO:SetFileName(FileName) self:F2(FileName) if type(FileName) == "string" then + if FileName:find(".ogg") or FileName:find(".wav") then if not FileName:find("l10n/DEFAULT/") then FileName = "l10n/DEFAULT/" .. FileName end + self.FileName = FileName return self end end - self:E({"File name invalid. Maybe something wrong with the extension ?", self.FileName}) + self:E({"File name invalid. Maybe something wrong with the extension?", FileName}) return self end ---- Check validity of the frequency passed and sets RADIO.Frequency +--- Set the frequency for the radio transmission. +-- If the transmitting positionable is a unit or group, this also set the command "SetFrequency" with the defined frequency and modulation. -- @param #RADIO self --- @param #number Frequency in MHz (Ranges allowed for radio transmissions in DCS : 30-88 / 108-152 / 225-400MHz) +-- @param #number Frequency Frequency in MHz. Ranges allowed for radio transmissions in DCS : 30-88 / 108-152 / 225-400MHz. -- @return #RADIO self function RADIO:SetFrequency(Frequency) self:F2(Frequency) + if type(Frequency) == "number" then + -- If frequency is in range if (Frequency >= 30 and Frequency < 88) or (Frequency >= 108 and Frequency < 152) or (Frequency >= 225 and Frequency < 400) then - self.Frequency = Frequency * 1000000 -- Conversion in Hz + + -- Convert frequency from MHz to Hz + self.Frequency = Frequency * 1000000 + -- If the RADIO is attached to a UNIT or a GROUP, we need to send the DCS Command "SetFrequency" to change the UNIT or GROUP frequency if self.Positionable.ClassName == "UNIT" or self.Positionable.ClassName == "GROUP" then - self.Positionable:SetCommand({ + + local commandSetFrequency={ id = "SetFrequency", params = { - frequency = self.Frequency, + frequency = self.Frequency, modulation = self.Modulation, } - }) + } + + self:T2(commandSetFrequency) + self.Positionable:SetCommand(commandSetFrequency) end + return self end end - self:E({"Frequency is outside of DCS Frequency ranges (30-80, 108-152, 225-400). Frequency unchanged.", self.Frequency}) + + self:E({"Frequency is outside of DCS Frequency ranges (30-80, 108-152, 225-400). Frequency unchanged.", Frequency}) return self end ---- Check validity of the frequency passed and sets RADIO.Modulation +--- Set AM or FM modulation of the radio transmitter. -- @param #RADIO self --- @param #number Modulation either radio.modulation.AM or radio.modulation.FM +-- @param #number Modulation Modulation is either radio.modulation.AM or radio.modulation.FM. -- @return #RADIO self function RADIO:SetModulation(Modulation) self:F2(Modulation) @@ -183,23 +214,24 @@ end --- Check validity of the power passed and sets RADIO.Power -- @param #RADIO self --- @param #number Power in W +-- @param #number Power Power in W. -- @return #RADIO self function RADIO:SetPower(Power) self:F2(Power) + if type(Power) == "number" then self.Power = math.floor(math.abs(Power)) --TODO Find what is the maximum power allowed by DCS and limit power to that - return self + else + self:E({"Power is invalid. Power unchanged.", self.Power}) end - self:E({"Power is invalid. Power unchanged.", self.Power}) + return self end ---- Check validity of the loop passed and sets RADIO.Loop +--- Set message looping on or off. -- @param #RADIO self --- @param #boolean Loop +-- @param #boolean Loop If true, message is repeated indefinitely. -- @return #RADIO self --- @usage function RADIO:SetLoop(Loop) self:F2(Loop) if type(Loop) == "boolean" then @@ -232,13 +264,12 @@ function RADIO:SetSubtitle(Subtitle, SubtitleDuration) self:E({"Subtitle is invalid. Subtitle reset.", self.Subtitle}) end if type(SubtitleDuration) == "number" then - if math.floor(math.abs(SubtitleDuration)) == SubtitleDuration then - self.SubtitleDuration = SubtitleDuration - return self - end + self.SubtitleDuration = SubtitleDuration + else + self.SubtitleDuration = 0 + self:E({"SubtitleDuration is invalid. SubtitleDuration reset.", self.SubtitleDuration}) end - self.SubtitleDuration = 0 - self:E({"SubtitleDuration is invalid. SubtitleDuration reset.", self.SubtitleDuration}) + return self end --- Create a new transmission, that is to say, populate the RADIO with relevant data @@ -246,10 +277,10 @@ end -- but it will work with a UNIT or a GROUP anyway. -- Only the #RADIO and the Filename are mandatory -- @param #RADIO self --- @param #string FileName --- @param #number Frequency in MHz --- @param #number Modulation either radio.modulation.AM or radio.modulation.FM --- @param #number Power in W +-- @param #string FileName Name of the sound file that will be transmitted. +-- @param #number Frequency Frequency in MHz. +-- @param #number Modulation Modulation of frequency, which is either radio.modulation.AM or radio.modulation.FM. +-- @param #number Power Power in W. -- @return #RADIO self function RADIO:NewGenericTransmission(FileName, Frequency, Modulation, Power, Loop) self:F({FileName, Frequency, Modulation, Power}) @@ -269,31 +300,43 @@ end -- but it will work for any @{Wrapper.Positionable#POSITIONABLE}. -- Only the RADIO and the Filename are mandatory. -- @param #RADIO self --- @param #string FileName --- @param #string Subtitle --- @param #number SubtitleDuration in s --- @param #number Frequency in MHz --- @param #number Modulation either radio.modulation.AM or radio.modulation.FM --- @param #boolean Loop +-- @param #string FileName Name of sound file. +-- @param #string Subtitle Subtitle to be displayed with sound file. +-- @param #number SubtitleDuration Duration of subtitle display in seconds. +-- @param #number Frequency Frequency in MHz. +-- @param #number Modulation Modulation which can be either radio.modulation.AM or radio.modulation.FM +-- @param #boolean Loop If true, loop message. -- @return #RADIO self function RADIO:NewUnitTransmission(FileName, Subtitle, SubtitleDuration, Frequency, Modulation, Loop) self:F({FileName, Subtitle, SubtitleDuration, Frequency, Modulation, Loop}) + -- Set file name. self:SetFileName(FileName) - local Duration = 5 - if SubtitleDuration then Duration = SubtitleDuration end - -- SubtitleDuration argument was missing, adding it - if Subtitle then self:SetSubtitle(Subtitle, Duration) end - -- self:SetSubtitleDuration is non existent, removing faulty line - -- if SubtitleDuration then self:SetSubtitleDuration(SubtitleDuration) end - if Frequency then self:SetFrequency(Frequency) end - if Modulation then self:SetModulation(Modulation) end - if Loop then self:SetLoop(Loop) end + + -- Set modulation AM/FM. + if Modulation then + self:SetModulation(Modulation) + end + + -- Set frequency. + if Frequency then + self:SetFrequency(Frequency) + end + + -- Set subtitle. + if Subtitle then + self:SetSubtitle(Subtitle, SubtitleDuration or 0) + end + + -- Set Looping. + if Loop then + self:SetLoop(Loop) + end return self end ---- Actually Broadcast the transmission +--- Broadcast the transmission. -- * The Radio has to be populated with the new transmission before broadcasting. -- * Please use RADIO setters or either @{#RADIO.NewGenericTransmission} or @{#RADIO.NewUnitTransmission} -- * This class is in fact pretty smart, it determines the right DCS function to use depending on the type of POSITIONABLE @@ -302,31 +345,38 @@ end -- * If your POSITIONABLE is a UNIT or a GROUP, the Power is ignored. -- * If your POSITIONABLE is not a UNIT or a GROUP, the Subtitle, SubtitleDuration are ignored -- @param #RADIO self +-- @param #boolean viatrigger Use trigger.action.radioTransmission() in any case, i.e. also for UNITS and GROUPS. -- @return #RADIO self -function RADIO:Broadcast() - self:F() +function RADIO:Broadcast(viatrigger) + self:F({viatrigger=viatrigger}) - -- If the POSITIONABLE is actually a UNIT or a GROUP, use the more complicated DCS command system - if self.Positionable.ClassName == "UNIT" or self.Positionable.ClassName == "GROUP" then - self:T2("Broadcasting from a UNIT or a GROUP") - self.Positionable:SetCommand({ + -- If the POSITIONABLE is actually a UNIT or a GROUP, use the more complicated DCS command system. + if (self.Positionable.ClassName=="UNIT" or self.Positionable.ClassName=="GROUP") and (not viatrigger) then + self:T("Broadcasting from a UNIT or a GROUP") + + local commandTransmitMessage={ id = "TransmitMessage", params = { file = self.FileName, duration = self.SubtitleDuration, subtitle = self.Subtitle, loop = self.Loop, - } - }) + }} + + self:T3(commandTransmitMessage) + self.Positionable:SetCommand(commandTransmitMessage) else -- If the POSITIONABLE is anything else, we revert to the general singleton function -- I need to give it a unique name, so that the transmission can be stopped later. I use the class ID - self:T2("Broadcasting from a POSITIONABLE") + self:T("Broadcasting from a POSITIONABLE") trigger.action.radioTransmission(self.FileName, self.Positionable:GetPositionVec3(), self.Modulation, self.Loop, self.Frequency, self.Power, tostring(self.ID)) end + return self end + + --- Stops a transmission -- This function is especially usefull to stop the broadcast of looped transmissions -- @param #RADIO self @@ -335,10 +385,10 @@ function RADIO:StopBroadcast() self:F() -- If the POSITIONABLE is a UNIT or a GROUP, stop the transmission with the DCS "StopTransmission" command if self.Positionable.ClassName == "UNIT" or self.Positionable.ClassName == "GROUP" then - self.Positionable:SetCommand({ - id = "StopTransmission", - params = {} - }) + + local commandStopTransmission={id="StopTransmission", params={}} + + self.Positionable:SetCommand(commandStopTransmission) else -- Else, we use the appropriate singleton funciton trigger.action.stopRadioTransmission(tostring(self.ID)) @@ -364,22 +414,86 @@ end -- Use @{#BEACON:StopRadioBeacon}() to stop it. -- -- @type BEACON +-- @field #string ClassName Name of the class "BEACON". +-- @field Wrapper.Controllable#CONTROLLABLE Positionable The @{#CONTROLLABLE} that will receive radio capabilities. -- @extends Core.Base#BASE BEACON = { ClassName = "BEACON", + Positionable = nil, } ---- Create a new BEACON Object. This doesn't activate the beacon, though, use @{#BEACON.AATACAN} or @{#BEACON.Generic} +--- Beacon types supported by DCS. +-- @type BEACON.Type +-- @field #number NULL +-- @field #number VOR +-- @field #number DME +-- @field #number VOR_DME +-- @field #number TACAN +-- @field #number VORTAC +-- @field #number RSBN +-- @field #number BROADCAST_STATION +-- @field #number HOMER +-- @field #number AIRPORT_HOMER +-- @field #number AIRPORT_HOMER_WITH_MARKER +-- @field #number ILS_FAR_HOMER +-- @field #number ILS_NEAR_HOMER +-- @field #number ILS_LOCALIZER +-- @field #number ILS_GLIDESLOPE +-- @field #number NAUTICAL_HOMER +-- @field #number ICLS +BEACON.Type={ + NULL = 0, + VOR = 1, + DME = 2, + VOR_DME = 3, + TACAN = 4, + VORTAC = 5, + RSBN = 32, + BROADCAST_STATION = 1024, + HOMER = 8, + AIRPORT_HOMER = 4104, + AIRPORT_HOMER_WITH_MARKER = 4136, + ILS_FAR_HOMER = 16408, + ILS_NEAR_HOMER = 16456, + ILS_LOCALIZER = 16640, + ILS_GLIDESLOPE = 16896, + NAUTICAL_HOMER = 32776, + ICLS = 131584, +} + +--- Beacon systems supported by DCS. +-- @type BEACON.System +-- @field #number PAR_10 +-- @field #number RSBN_5 +-- @field #number TACAN +-- @field #number TACAN_TANKER +-- @field #number ILS_LOCALIZER +-- @field #number ILS_GLIDESLOPE +-- @field #number BROADCAST_STATION +BEACON.System={ + PAR_10 = 1, + RSBN_5 = 2, + TACAN = 3, + TACAN_TANKER = 4, + ILS_LOCALIZER = 5, + ILS_GLIDESLOPE = 6, + BROADCAST_STATION = 7, +} + +--- Create a new BEACON Object. This doesn't activate the beacon, though, use @{#BEACON.ActivateTACAN} etc. -- If you want to create a BEACON, you probably should use @{Wrapper.Positionable#POSITIONABLE.GetBeacon}() instead. -- @param #BEACON self -- @param Wrapper.Positionable#POSITIONABLE Positionable The @{Positionable} that will receive radio capabilities. --- @return #BEACON Beacon --- @return #nil If Positionable is invalid +-- @return #BEACON Beacon object or #nil if the positionable is invalid. function BEACON:New(Positionable) - local self = BASE:Inherit(self, BASE:New()) + + -- Inherit BASE. + local self=BASE:Inherit(self, BASE:New()) --#BEACON + -- Debug. self:F(Positionable) + -- Set positionable. if Positionable:GetPointVec2() then -- It's stupid, but the only way I found to make sure positionable is valid self.Positionable = Positionable return self @@ -390,44 +504,95 @@ function BEACON:New(Positionable) end ---- Converts a TACAN Channel/Mode couple into a frequency in Hz +--- Activates a TACAN BEACON. -- @param #BEACON self --- @param #number TACANChannel --- @param #string TACANMode --- @return #number Frequecy --- @return #nil if parameters are invalid -function BEACON:_TACANToFrequency(TACANChannel, TACANMode) - self:F3({TACANChannel, TACANMode}) - - if type(TACANChannel) ~= "number" then - if TACANMode ~= "X" and TACANMode ~= "Y" then - return nil -- error in arguments - end +-- @param #number Channel TACAN channel, i.e. the "10" part in "10Y". +-- @param #string Mode TACAN mode, i.e. the "Y" part in "10Y". +-- @param #string Message The Message that is going to be coded in Morse and broadcasted by the beacon. +-- @param #boolean Bearing If true, beacon provides bearing information. If false (or nil), only distance information is available. +-- @param #number Duration How long will the beacon last in seconds. Omit for forever. +-- @return #BEACON self +-- @usage +-- -- Let's create a TACAN Beacon for a tanker +-- local myUnit = UNIT:FindByName("MyUnit") +-- local myBeacon = myUnit:GetBeacon() -- Creates the beacon +-- +-- myBeacon:TACAN(20, "Y", "TEXACO", true) -- Activate the beacon +function BEACON:ActivateTACAN(Channel, Mode, Message, Bearing, Duration) + self:T({channel=Channel, mode=Mode, callsign=Message, bearing=Bearing, duration=Duration}) + + -- Get frequency. + local Frequency=UTILS.TACANToFrequency(Channel, Mode) + + -- Check. + if not Frequency then + self:E({"The passed TACAN channel is invalid, the BEACON is not emitting"}) + return self end --- This code is largely based on ED's code, in DCS World\Scripts\World\Radio\BeaconTypes.lua, line 137. --- I have no idea what it does but it seems to work - local A = 1151 -- 'X', channel >= 64 - local B = 64 -- channel >= 64 + -- Beacon type. + local Type=BEACON.Type.TACAN - if TACANChannel < 64 then - B = 1 - end + -- Beacon system. + local System=BEACON.System.TACAN - if TACANMode == 'Y' then - A = 1025 - if TACANChannel < 64 then - A = 1088 - end - else -- 'X' - if TACANChannel < 64 then - A = 962 + -- Check if unit is an aircraft and set system accordingly. + local AA=self.Positionable:IsAir() + if AA then + System=BEACON.System.TACAN_TANKER + -- Check if "Y" mode is selected for aircraft. + if Mode~="Y" then + self:E({"WARNING: The POSITIONABLE you want to attach the AA Tacan Beacon is an aircraft: Mode should Y !The BEACON is not emitting.", self.Positionable}) end end - return (A + TACANChannel - B) * 1000000 + -- Attached unit. + local UnitID=self.Positionable:GetID() + + -- Debug. + self:T({"TACAN BEACON started!"}) + + -- Start beacon. + self.Positionable:CommandActivateBeacon(Type, System, Frequency, UnitID, Channel, Mode, AA, Message, Bearing) + + -- Stop sheduler. + if Duration then + self.Positionable:DeactivateBeacon(Duration) + end + + return self end +--- Activates an ICLS BEACON. The unit the BEACON is attached to should be an aircraft carrier supporting this system. +-- @param #BEACON self +-- @param #number Channel ICLS channel. +-- @param #string Callsign The Message that is going to be coded in Morse and broadcasted by the beacon. +-- @param #number Duration How long will the beacon last in seconds. Omit for forever. +-- @return #BEACON self +function BEACON:ActivateICLS(Channel, Callsign, Duration) + self:F({Channel=Channel, Callsign=Callsign, Duration=Duration}) + + -- Attached unit. + local UnitID=self.Positionable:GetID() + + -- Debug + self:T2({"ICLS BEACON started!"}) + + -- Start beacon. + self.Positionable:CommandActivateICLS(Channel, UnitID, Callsign) + + -- Stop sheduler + if Duration then -- Schedule the stop of the BEACON if asked by the MD + self.Positionable:DeactivateBeacon(Duration) + end + + return self +end + + + + + --- Activates a TACAN BEACON on an Aircraft. -- @param #BEACON self @@ -480,7 +645,7 @@ function BEACON:AATACAN(TACANChannel, Message, Bearing, BeaconDuration) }) if BeaconDuration then -- Schedule the stop of the BEACON if asked by the MD - SCHEDULER:New( nil, + SCHEDULER:New(nil, function() self:StopAATACAN() end, {}, BeaconDuration) @@ -591,4 +756,44 @@ function BEACON:StopRadioBeacon() self:F() -- The unique name of the transmission is the class ID trigger.action.stopRadioTransmission(tostring(self.ID)) -end \ No newline at end of file + return self +end + +--- Converts a TACAN Channel/Mode couple into a frequency in Hz +-- @param #BEACON self +-- @param #number TACANChannel +-- @param #string TACANMode +-- @return #number Frequecy +-- @return #nil if parameters are invalid +function BEACON:_TACANToFrequency(TACANChannel, TACANMode) + self:F3({TACANChannel, TACANMode}) + + if type(TACANChannel) ~= "number" then + if TACANMode ~= "X" and TACANMode ~= "Y" then + return nil -- error in arguments + end + end + +-- This code is largely based on ED's code, in DCS World\Scripts\World\Radio\BeaconTypes.lua, line 137. +-- I have no idea what it does but it seems to work + local A = 1151 -- 'X', channel >= 64 + local B = 64 -- channel >= 64 + + if TACANChannel < 64 then + B = 1 + end + + if TACANMode == 'Y' then + A = 1025 + if TACANChannel < 64 then + A = 1088 + end + else -- 'X' + if TACANChannel < 64 then + A = 962 + end + end + + return (A + TACANChannel - B) * 1000000 +end + diff --git a/Moose Development/Moose/Core/Set.lua b/Moose Development/Moose/Core/Set.lua index 0864fb6c1..6acb294ab 100644 --- a/Moose Development/Moose/Core/Set.lua +++ b/Moose Development/Moose/Core/Set.lua @@ -409,9 +409,9 @@ do -- SET_BASE for ObjectID, ObjectData in pairs( self.Set ) do if NearestObject == nil then NearestObject = ObjectData - ClosestDistance = PointVec2:DistanceFromPointVec2( ObjectData:GetVec2() ) + ClosestDistance = PointVec2:DistanceFromPointVec2( ObjectData:GetCoordinate() ) else - local Distance = PointVec2:DistanceFromPointVec2( ObjectData:GetVec2() ) + local Distance = PointVec2:DistanceFromPointVec2( ObjectData:GetCoordinate() ) if Distance < ClosestDistance then NearestObject = ObjectData ClosestDistance = Distance diff --git a/Moose Development/Moose/Core/SpawnStatic.lua b/Moose Development/Moose/Core/SpawnStatic.lua index 0081195a5..e5a7b4e59 100644 --- a/Moose Development/Moose/Core/SpawnStatic.lua +++ b/Moose Development/Moose/Core/SpawnStatic.lua @@ -195,6 +195,49 @@ function SPAWNSTATIC:SpawnFromPointVec2( PointVec2, Heading, NewName ) --R2.1 end +--- Creates a new @{Static} from a COORDINATE. +-- @param #SPAWNSTATIC self +-- @param Core.Point#COORDINATE Coordinate The 3D coordinate where to spawn the static. +-- @param #number Heading (Optional) Heading The heading of the static, which is a number in degrees from 0 to 360. Default is 0 degrees. +-- @param #string NewName (Optional) The name of the new static. +-- @return #SPAWNSTATIC +function SPAWNSTATIC:SpawnFromCoordinate(Coordinate, Heading, NewName) --R2.4 + self:F( { PointVec2, Heading, NewName } ) + + local StaticTemplate, CoalitionID, CategoryID, CountryID = _DATABASE:GetStaticGroupTemplate( self.SpawnTemplatePrefix ) + + if StaticTemplate then + + Heading=Heading or 0 + + local StaticUnitTemplate = StaticTemplate.units[1] + + StaticUnitTemplate.x = Coordinate.x + StaticUnitTemplate.y = Coordinate.z + StaticUnitTemplate.alt = Coordinate.y + + StaticTemplate.route = nil + StaticTemplate.groupId = nil + + StaticTemplate.name = NewName or string.format("%s#%05d", self.SpawnTemplatePrefix, self.SpawnIndex ) + StaticUnitTemplate.name = StaticTemplate.name + StaticUnitTemplate.heading = ( Heading / 180 ) * math.pi + + _DATABASE:_RegisterStaticTemplate( StaticTemplate, CoalitionID, CategoryID, CountryID) + + self:F({StaticTemplate = StaticTemplate}) + + local Static = coalition.addStaticObject( self.CountryID or CountryID, StaticTemplate.units[1] ) + + self.SpawnIndex = self.SpawnIndex + 1 + + return _DATABASE:FindStatic(Static:getName()) + end + + return nil +end + + --- Respawns the original @{Static}. -- @param #SPAWNSTATIC self -- @return #SPAWNSTATIC diff --git a/Moose Development/Moose/Core/UserFlag.lua b/Moose Development/Moose/Core/UserFlag.lua index 88c1d0f60..bef5cefff 100644 --- a/Moose Development/Moose/Core/UserFlag.lua +++ b/Moose Development/Moose/Core/UserFlag.lua @@ -70,7 +70,7 @@ do -- UserFlag -- local BlueVictory = USERFLAG:New( "VictoryBlue" ) -- local BlueVictoryValue = BlueVictory:Get() -- Get the UserFlag VictoryBlue value. -- - function USERFLAG:Get( Number ) --R2.3 + function USERFLAG:Get() --R2.3 return trigger.misc.getUserFlag( self.UserFlagName ) end diff --git a/Moose Development/Moose/Core/UserSound.lua b/Moose Development/Moose/Core/UserSound.lua index a0547a5cf..b0f6fb393 100644 --- a/Moose Development/Moose/Core/UserSound.lua +++ b/Moose Development/Moose/Core/UserSound.lua @@ -118,15 +118,21 @@ do -- UserSound --- Play the usersound to the given @{Wrapper.Group}. -- @param #USERSOUND self -- @param Wrapper.Group#GROUP Group The @{Wrapper.Group} to play the usersound to. + -- @param #number Delay (Optional) Delay in seconds, before the sound is played. Default 0. -- @return #USERSOUND The usersound instance. -- @usage -- local BlueVictory = USERSOUND:New( "BlueVictory.ogg" ) -- local PlayerGroup = GROUP:FindByName( "PlayerGroup" ) -- Search for the active group named "PlayerGroup", that contains a human player. -- BlueVictory:ToGroup( PlayerGroup ) -- Play the sound that Blue has won to the player group. -- - function USERSOUND:ToGroup( Group ) --R2.3 - - trigger.action.outSoundForGroup( Group:GetID(), self.UserSoundFileName ) + function USERSOUND:ToGroup( Group, Delay ) --R2.3 + + Delay=Delay or 0 + if Delay>0 then + SCHEDULER:New(nil, USERSOUND.ToGroup,{self, Group}, Delay) + else + trigger.action.outSoundForGroup( Group:GetID(), self.UserSoundFileName ) + end return self end diff --git a/Moose Development/Moose/Core/Zone.lua b/Moose Development/Moose/Core/Zone.lua index 9b5b6827f..e0971ee06 100644 --- a/Moose Development/Moose/Core/Zone.lua +++ b/Moose Development/Moose/Core/Zone.lua @@ -1398,16 +1398,15 @@ end --- Smokes the zone boundaries in a color. -- @param #ZONE_POLYGON_BASE self -- @param Utilities.Utils#SMOKECOLOR SmokeColor The smoke color. +-- @param #number Segments (Optional) Number of segments within boundary line. Default 10. -- @return #ZONE_POLYGON_BASE self -function ZONE_POLYGON_BASE:SmokeZone( SmokeColor ) +function ZONE_POLYGON_BASE:SmokeZone( SmokeColor, Segments ) self:F2( SmokeColor ) - local i - local j - local Segments = 10 + Segments=Segments or 10 - i = 1 - j = #self._.Polygon + local i=1 + local j=#self._.Polygon while i <= #self._.Polygon do self:T( { i, j, self._.Polygon[i], self._.Polygon[j] } ) @@ -1428,6 +1427,38 @@ function ZONE_POLYGON_BASE:SmokeZone( SmokeColor ) end +--- Flare the zone boundaries in a color. +-- @param #ZONE_POLYGON_BASE self +-- @param Utilities.Utils#FLARECOLOR FlareColor The flare color. +-- @param #number Segments (Optional) Number of segments within boundary line. Default 10. +-- @return #ZONE_POLYGON_BASE self +function ZONE_POLYGON_BASE:FlareZone( FlareColor, Segments ) + self:F2(FlareColor) + + Segments=Segments or 10 + + local i=1 + local j=#self._.Polygon + + while i <= #self._.Polygon do + self:T( { i, j, self._.Polygon[i], self._.Polygon[j] } ) + + local DeltaX = self._.Polygon[j].x - self._.Polygon[i].x + local DeltaY = self._.Polygon[j].y - self._.Polygon[i].y + + for Segment = 0, Segments do -- We divide each line in 5 segments and smoke a point on the line. + local PointX = self._.Polygon[i].x + ( Segment * DeltaX / Segments ) + local PointY = self._.Polygon[i].y + ( Segment * DeltaY / Segments ) + POINT_VEC2:New( PointX, PointY ):Flare(FlareColor) + end + j = i + i = i + 1 + end + + return self +end + + --- Returns if a location is within the zone. diff --git a/Moose Development/Moose/Functional/Artillery.lua b/Moose Development/Moose/Functional/Artillery.lua index 8509928fb..637560d03 100644 --- a/Moose Development/Moose/Functional/Artillery.lua +++ b/Moose Development/Moose/Functional/Artillery.lua @@ -216,7 +216,7 @@ -- One way to determin which types of ammo the unit carries, one can use the debug mode of the arty class via @{#ARTY.SetDebugON}(). -- In debug mode, the all ammo types of the group are printed to the monitor as message and can be found in the DCS.log file. -- --- ## Empoying Selected Weapons +-- ## Employing Selected Weapons -- -- If an ARTY group carries multiple weapons, which can be used for artillery task, a certain weapon type can be selected to attack the target. -- This is done via the *weapontype* parameter of the @{#ARTY.AssignTargetCoord}(..., *weapontype*, ...) function. @@ -674,11 +674,13 @@ ARTY.id="ARTY | " --- Arty script version. -- @field #string version -ARTY.version="1.0.6" +ARTY.version="1.0.7" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list: +-- TODO: Add hit event and make the arty group relocate. +-- TODO: Handle rearming for ships. How? -- DONE: Delete targets from queue user function. -- DONE: Delete entire target queue user function. -- DONE: Add weapon types. Done but needs improvements. @@ -697,11 +699,9 @@ ARTY.version="1.0.6" -- DONE: Add command move to make arty group move. -- DONE: remove schedulers for status event. -- DONE: Improve handling of special weapons. When winchester if using selected weapons? --- TODO: Handle rearming for ships. How? -- DONE: Make coordinate after rearming general, i.e. also work after the group has moved to anonther location. -- DONE: Add set commands via markers. E.g. set rearming place. -- DONE: Test stationary types like mortas ==> rearming etc. --- TODO: Add hit event and make the arty group relocate. -- DONE: Add illumination and smoke. --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -2878,7 +2878,7 @@ function ARTY:onafterCeaseFire(Controllable, From, Event, To, target) self.Controllable:ClearTasks() else - self:E(ARTY.id.."ERROR: No target in cease fire for group %s.", self.groupname) + self:E(ARTY.id..string.format("ERROR: No target in cease fire for group %s.", self.groupname)) end -- Set number of shots to zero. @@ -4253,101 +4253,116 @@ end -- @param #ARTY self function ARTY:_CheckTargetsInRange() + local targets2delete={} + for i=1,#self.targets do local _target=self.targets[i] self:T3(ARTY.id..string.format("Before: Target %s - in range = %s", _target.name, tostring(_target.inrange))) -- Check if target is in range. - local _inrange,_toofar,_tooclose=self:_TargetInRange(_target) + local _inrange,_toofar,_tooclose,_remove=self:_TargetInRange(_target) self:T3(ARTY.id..string.format("Inbetw: Target %s - in range = %s, toofar = %s, tooclose = %s", _target.name, tostring(_target.inrange), tostring(_toofar), tostring(_tooclose))) - -- Init default for assigning moves into range. - local _movetowards=false - local _moveaway=false + if _remove then - if _target.inrange==nil then - - -- First time the check is performed. We call the function again and send a message. - _target.inrange,_toofar,_tooclose=self:_TargetInRange(_target, self.report or self.Debug) + -- The ARTY group is immobile and not cargo but the target is not in range! + table.insert(targets2delete, _target.name) - -- Send group towards/away from target. - if _toofar then - _movetowards=true - elseif _tooclose then - _moveaway=true - end + else - elseif _target.inrange==true then - - -- Target was in range at previous check... - - if _toofar then --...but is now too far away. - _movetowards=true - elseif _tooclose then --...but is now too close. - _moveaway=true - end - - elseif _target.inrange==false then - - -- Target was out of range at previous check. + -- Init default for assigning moves into range. + local _movetowards=false + local _moveaway=false - if _inrange then - -- Inform coalition that target is now in range. - local text=string.format("%s, target %s is now in range.", self.alias, _target.name) - self:T(ARTY.id..text) - MESSAGE:New(text,10):ToCoalitionIf(self.Controllable:GetCoalition(), self.report or self.Debug) - end - - end - - -- Assign a relocation command so that the unit will be in range of the requested target. - if self.autorelocate and (_movetowards or _moveaway) then - - -- Get current position. - local _from=self.Controllable:GetCoordinate() - local _dist=_from:Get2DDistance(_target.coord) + if _target.inrange==nil then - if _dist<=self.autorelocatemaxdist then - - local _tocoord --Core.Point#COORDINATE - local _name="" - local _safetymargin=500 - - if _movetowards then + -- First time the check is performed. We call the function again and send a message. + _target.inrange,_toofar,_tooclose=self:_TargetInRange(_target, self.report or self.Debug) - -- Target was in range on previous check but now we are too far away. - local _waytogo=_dist-self.maxrange+_safetymargin - local _heading=self:_GetHeading(_from,_target.coord) - _tocoord=_from:Translate(_waytogo, _heading) - _name=string.format("%s, relocation to within max firing range of target %s", self.alias, _target.name) - - elseif _moveaway then - - -- Target was in range on previous check but now we are too far away. - local _waytogo=_dist-self.minrange+_safetymargin - local _heading=self:_GetHeading(_target.coord,_from) - _tocoord=_from:Translate(_waytogo, _heading) - _name=string.format("%s, relocation to within min firing range of target %s", self.alias, _target.name) - + -- Send group towards/away from target. + if _toofar then + _movetowards=true + elseif _tooclose then + _moveaway=true end - - -- Send info message. - MESSAGE:New(_name.." assigned.", 10):ToCoalitionIf(self.Controllable:GetCoalition(), self.report or self.Debug) - - -- Assign relocation move. - self:AssignMoveCoord(_tocoord, nil, nil, self.autorelocateonroad, false, _name, true) + + elseif _target.inrange==true then + + -- Target was in range at previous check... + + if _toofar then --...but is now too far away. + _movetowards=true + elseif _tooclose then --...but is now too close. + _moveaway=true + end + + elseif _target.inrange==false then + + -- Target was out of range at previous check. + if _inrange then + -- Inform coalition that target is now in range. + local text=string.format("%s, target %s is now in range.", self.alias, _target.name) + self:T(ARTY.id..text) + MESSAGE:New(text,10):ToCoalitionIf(self.Controllable:GetCoalition(), self.report or self.Debug) + end + end + + -- Assign a relocation command so that the unit will be in range of the requested target. + if self.autorelocate and (_movetowards or _moveaway) then + + -- Get current position. + local _from=self.Controllable:GetCoordinate() + local _dist=_from:Get2DDistance(_target.coord) + + if _dist<=self.autorelocatemaxdist then + + local _tocoord --Core.Point#COORDINATE + local _name="" + local _safetymargin=500 + + if _movetowards then + + -- Target was in range on previous check but now we are too far away. + local _waytogo=_dist-self.maxrange+_safetymargin + local _heading=self:_GetHeading(_from,_target.coord) + _tocoord=_from:Translate(_waytogo, _heading) + _name=string.format("%s, relocation to within max firing range of target %s", self.alias, _target.name) + elseif _moveaway then + + -- Target was in range on previous check but now we are too far away. + local _waytogo=_dist-self.minrange+_safetymargin + local _heading=self:_GetHeading(_target.coord,_from) + _tocoord=_from:Translate(_waytogo, _heading) + _name=string.format("%s, relocation to within min firing range of target %s", self.alias, _target.name) + + end + + -- Send info message. + MESSAGE:New(_name.." assigned.", 10):ToCoalitionIf(self.Controllable:GetCoalition(), self.report or self.Debug) + + -- Assign relocation move. + self:AssignMoveCoord(_tocoord, nil, nil, self.autorelocateonroad, false, _name, true) + + end + + end + + -- Update value. + _target.inrange=_inrange + + self:T3(ARTY.id..string.format("After: Target %s - in range = %s", _target.name, tostring(_target.inrange))) end - - -- Update value. - _target.inrange=_inrange - - self:T3(ARTY.id..string.format("After: Target %s - in range = %s", _target.name, tostring(_target.inrange))) - end + + -- Remove targets not in range. + for _,targetname in pairs(targets2delete) do + self:RemoveTarget(targetname) + end + end --- Check all normal (untimed) targets and return the target with the highest priority which has been engaged the fewest times. @@ -4728,6 +4743,7 @@ end -- @return #boolean True if target is in range, false otherwise. -- @return #boolean True if ARTY group is too far away from the target, i.e. distance > max firing range. -- @return #boolean True if ARTY group is too close to the target, i.e. distance < min finring range. +-- @return #boolean True if target should be removed since ARTY group is immobile and not cargo. function ARTY:_TargetInRange(target, message) self:F3(target) @@ -4763,11 +4779,13 @@ function ARTY:_TargetInRange(target, message) end -- Remove target if ARTY group cannot move, e.g. Mortas. No chance to be ever in range - unless they are cargo. + local _remove=false if not (self.ismobile or self.iscargo) and _inrange==false then - self:RemoveTarget(target.name) + --self:RemoveTarget(target.name) + _remove=true end - return _inrange,_toofar,_tooclose + return _inrange,_toofar,_tooclose,_remove end --- Get the weapon type name, which should be used to attack the target. diff --git a/Moose Development/Moose/Functional/Detection.lua b/Moose Development/Moose/Functional/Detection.lua index 7402729ab..fd3029b5a 100644 --- a/Moose Development/Moose/Functional/Detection.lua +++ b/Moose Development/Moose/Functional/Detection.lua @@ -1012,7 +1012,7 @@ do -- DETECTION_BASE --- Set the parameters to calculate to optimal intercept point. -- @param #DETECTION_BASE self -- @param #boolean Intercept Intercept is true if an intercept point is calculated. Intercept is false if it is disabled. The default Intercept is false. - -- @param #number IntereptDelay If Intercept is true, then InterceptDelay is the average time it takes to get airplanes airborne. + -- @param #number InterceptDelay If Intercept is true, then InterceptDelay is the average time it takes to get airplanes airborne. -- @return #DETECTION_BASE self function DETECTION_BASE:SetIntercept( Intercept, InterceptDelay ) self:F2() diff --git a/Moose Development/Moose/Functional/RAT.lua b/Moose Development/Moose/Functional/RAT.lua index 5b1616c81..9a1c9306b 100644 --- a/Moose Development/Moose/Functional/RAT.lua +++ b/Moose Development/Moose/Functional/RAT.lua @@ -5435,7 +5435,7 @@ function RAT:_ATCInit(airports_map) if not RAT.ATC.init then local text text="Starting RAT ATC.\nSimultanious = "..RAT.ATC.Nclearance.."\n".."Delay = "..RAT.ATC.delay - self:T(RAT.id..text) + BASE:T(RAT.id..text) RAT.ATC.init=true for _,ap in pairs(airports_map) do local name=ap:GetName() @@ -5458,7 +5458,7 @@ end -- @param #string name Group name of the flight. -- @param #string dest Name of the destination airport. function RAT:_ATCAddFlight(name, dest) - self:T(string.format("%sATC %s: Adding flight %s with destination %s.", RAT.id, dest, name, dest)) + BASE:T(string.format("%sATC %s: Adding flight %s with destination %s.", RAT.id, dest, name, dest)) RAT.ATC.flight[name]={} RAT.ATC.flight[name].destination=dest RAT.ATC.flight[name].Tarrive=-1 @@ -5483,7 +5483,7 @@ end -- @param #string name Group name of the flight. -- @param #number time Time the fight first registered. function RAT:_ATCRegisterFlight(name, time) - self:T(RAT.id.."Flight ".. name.." registered at ATC for landing clearance.") + BASE:T(RAT.id.."Flight ".. name.." registered at ATC for landing clearance.") RAT.ATC.flight[name].Tarrive=time RAT.ATC.flight[name].holding=0 end @@ -5514,7 +5514,7 @@ function RAT:_ATCStatus() -- Aircraft is holding. local text=string.format("ATC %s: Flight %s is holding for %i:%02d. %s.", dest, name, hold/60, hold%60, busy) - self:T(RAT.id..text) + BASE:T(RAT.id..text) elseif hold==RAT.ATC.onfinal then @@ -5522,7 +5522,7 @@ function RAT:_ATCStatus() local Tfinal=Tnow-RAT.ATC.flight[name].Tonfinal local text=string.format("ATC %s: Flight %s is on final. Waiting %i:%02d for landing event.", dest, name, Tfinal/60, Tfinal%60) - self:T(RAT.id..text) + BASE:T(RAT.id..text) elseif hold==RAT.ATC.unregistered then @@ -5530,7 +5530,7 @@ function RAT:_ATCStatus() --self:T(string.format("ATC %s: Flight %s is not registered yet (hold %d).", dest, name, hold)) else - self:E(RAT.id.."ERROR: Unknown holding time in RAT:_ATCStatus().") + BASE:E(RAT.id.."ERROR: Unknown holding time in RAT:_ATCStatus().") end end @@ -5572,12 +5572,12 @@ function RAT:_ATCCheck() -- Debug message. local text=string.format("ATC %s: Flight %s runway is busy. You are #%d of %d in landing queue. Your holding time is %i:%02d.", name, flight,qID, nqueue, RAT.ATC.flight[flight].holding/60, RAT.ATC.flight[flight].holding%60) - self:T(RAT.id..text) + BASE:T(RAT.id..text) else local text=string.format("ATC %s: Flight %s was cleared for landing. Your holding time was %i:%02d.", name, flight, RAT.ATC.flight[flight].holding/60, RAT.ATC.flight[flight].holding%60) - self:T(RAT.id..text) + BASE:T(RAT.id..text) -- Clear flight for landing. RAT:_ATCClearForLanding(name, flight) @@ -5705,12 +5705,7 @@ function RAT:_ATCQueue() for k,v in ipairs(_queue) do table.insert(RAT.ATC.airport[airport].queue, v[1]) end - - --fvh - --for k,v in ipairs(RAT.ATC.airport[airport].queue) do - --print(string.format("queue #%02i flight \"%s\" holding %d seconds",k, v, RAT.ATC.flight[v].holding)) - --end - + end end diff --git a/Moose Development/Moose/Functional/Range.lua b/Moose Development/Moose/Functional/Range.lua index 95b800521..394f134f2 100644 --- a/Moose Development/Moose/Functional/Range.lua +++ b/Moose Development/Moose/Functional/Range.lua @@ -276,7 +276,7 @@ RANGE.id="RANGE | " --- Range script version. -- @field #string version -RANGE.version="1.2.1" +RANGE.version="1.2.3" --TODO list: --TODO: Add custom weapons, which can be specified by the user. @@ -460,9 +460,10 @@ function RANGE:SetBombtrackThreshold(distance) self.BombtrackThreshold=distance*1000 or 25*1000 end ---- Set range location. If this is not done, one (random) unit position of the range is used to determine the center of the range. +--- 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 center of the range. +-- @param Core.Point#COORDINATE coordinate Coordinate of the range. function RANGE:SetRangeLocation(coordinate) self.location=coordinate end @@ -471,7 +472,7 @@ end -- 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. -function RANGE:SetRangeLocation(zone) +function RANGE:SetRangeZone(zone) self.rangezone=zone end @@ -1163,11 +1164,19 @@ function RANGE:OnEventShot(EventData) -- Coordinate of impact point. local impactcoord=COORDINATE:NewFromVec3(_lastBombPos) + -- Check if impact happend in range zone. + local insidezone=self.rangezone:IsCoordinateInZone(impactcoord) + -- Distance from range. We dont want to smoke targets outside of the range. local impactdist=impactcoord:Get2DDistance(self.location) + -- Impact point of bomb. + if self.Debug then + impactcoord:MarkToAll("Bomb impact point") + end + -- Smoke impact point of bomb. - if self.PlayerSettings[_playername].smokebombimpact and impactdist1 then @@ -680,3 +712,77 @@ function UTILS.VecCross(a, b) return {x=a.y*b.z - a.z*b.y, y=a.z*b.x - a.x*b.z, z=a.x*b.y - a.y*b.x} end +--- Converts a TACAN Channel/Mode couple into a frequency in Hz. +-- @param #number TACANChannel The TACAN channel, i.e. the 10 in "10X". +-- @param #string TACANMode The TACAN mode, i.e. the "X" in "10X". +-- @return #number Frequency in Hz or #nil if parameters are invalid. +function UTILS.TACANToFrequency(TACANChannel, TACANMode) + + if type(TACANChannel) ~= "number" then + return nil -- error in arguments + end + if TACANMode ~= "X" and TACANMode ~= "Y" then + return nil -- error in arguments + end + +-- This code is largely based on ED's code, in DCS World\Scripts\World\Radio\BeaconTypes.lua, line 137. +-- I have no idea what it does but it seems to work + local A = 1151 -- 'X', channel >= 64 + local B = 64 -- channel >= 64 + + if TACANChannel < 64 then + B = 1 + end + + if TACANMode == 'Y' then + A = 1025 + if TACANChannel < 64 then + A = 1088 + end + else -- 'X' + if TACANChannel < 64 then + A = 962 + end + end + + return (A + TACANChannel - B) * 1000000 +end + + +--- Returns the DCS map/theatre as optained by env.mission.theatre +-- @return #string DCS map name . +function UTILS.GetDCSMap() + return env.mission.theatre +end + +--- Returns the magnetic declination of the map. +-- Returned values for the current maps are: +-- +-- * Caucasus +6 (East), year ~ 2011 +-- * NTTR +12 (East), year ~ 2011 +-- * Normandy -10 (West), year ~ 1944 +-- * Persian Gulf +2 (East), year ~ 2011 +-- @param #string map (Optional) Map for which the declination is returned. Default is from env.mission.theatre +-- @return #number Declination in degrees. +function UTILS.GetMagneticDeclination(map) + + -- Map. + map=map or UTILS.GetDCSMap() + + local declination=0 + if map==DCSMAP.Caucasus then + declination=6 + elseif map==DCSMAP.NTTR then + declination=12 + elseif map==DCSMAP.Normandy then + declination=-10 + elseif map==DCSMAP.PersianGulf then + declination=2 + else + declination=0 + end + + return declination +end + + diff --git a/Moose Development/Moose/Wrapper/Controllable.lua b/Moose Development/Moose/Wrapper/Controllable.lua index 1fb50baa5..91bc437a4 100644 --- a/Moose Development/Moose/Wrapper/Controllable.lua +++ b/Moose Development/Moose/Wrapper/Controllable.lua @@ -342,16 +342,26 @@ function CONTROLLABLE:PushTask( DCSTask, WaitTime ) local DCSControllable = self:GetDCSObject() if DCSControllable then - local Controller = self:_GetController() + + local DCSControllableName = self:GetName() -- When a controllable SPAWNs, it takes about a second to get the controllable in the simulator. Setting tasks to unspawned controllables provides unexpected results. -- Therefore we schedule the functions to set the mission and options for the Controllable. - -- Controller:pushTask( DCSTask ) + -- Controller:pushTask( DCSTask ) + + local function PushTask( Controller, DCSTask ) + if self and self:IsAlive() then + local Controller = self:_GetController() + Controller:pushTask( DCSTask ) + else + BASE:E( { DCSControllableName .. " is not alive anymore.", DCSTask = DCSTask } ) + end + end - if WaitTime then - self.TaskScheduler:Schedule( Controller, Controller.pushTask, { DCSTask }, WaitTime ) + if not WaitTime or WaitTime == 0 then + PushTask( self, DCSTask ) else - Controller:pushTask( DCSTask ) + self.TaskScheduler:Schedule( self, PushTask, { DCSTask }, WaitTime ) end return self @@ -362,7 +372,7 @@ end --- Clearing the Task Queue and Setting the Task on the queue from the controllable. -- @param #CONTROLLABLE self --- @param #DCS.Task DCSTask DCS Task array. +-- @param DCS#Task DCSTask DCS Task array. -- @param #number WaitTime Time in seconds, before the task is set. -- @return Wrapper.Controllable#CONTROLLABLE self function CONTROLLABLE:SetTask( DCSTask, WaitTime ) @@ -540,9 +550,9 @@ end ---- Executes a command action +--- Executes a command action for the CONTROLLABLE. -- @param #CONTROLLABLE self --- @param DCS#Command DCSCommand +-- @param DCS#Command DCSCommand The command to be executed. -- @return #CONTROLLABLE self function CONTROLLABLE:SetCommand( DCSCommand ) self:F2( DCSCommand ) @@ -630,9 +640,122 @@ function CONTROLLABLE:StartUncontrolled(delay) return self end +--- Give the CONTROLLABLE the command to activate a beacon. See [DCS_command_activateBeacon](https://wiki.hoggitworld.com/view/DCS_command_activateBeacon) on Hoggit. +-- For specific beacons like TACAN use the more convenient @{#BEACON} class. +-- Note that a controllable can only have one beacon activated at a time with the execption of ICLS. +-- @param #CONTROLLABLE self +-- @param Core.Radio#BEACON.Type Type Beacon type (VOR, DME, TACAN, RSBN, ILS etc). +-- @param Core.Radio#BEACON.System System Beacon system (VOR, DME, TACAN, RSBN, ILS etc). +-- @param #number Frequency Frequency in Hz the beacon is running on. Use @{#UTILS.TACANToFrequency} to generate a frequency for TACAN beacons. +-- @param #number UnitID The ID of the unit the beacon is attached to. Usefull if more units are in one group. +-- @param #number Channel Channel the beacon is using. For, e.g. TACAN beacons. +-- @param #string ModeChannel The TACAN mode of the beacon, i.e. "X" or "Y". +-- @param #boolean AA If true, create and Air-Air beacon. IF nil, automatically set if CONTROLLABLE depending on whether unit is and aircraft or not. +-- @param #string Callsign Morse code identification callsign. +-- @param #boolean Bearing If true, beacon provides bearing information - if supported by the unit the beacon is attached to. +-- @param #number Delay (Optional) Delay in seconds before the beacon is activated. +-- @return #CONTROLLABLE self +function CONTROLLABLE:CommandActivateBeacon(Type, System, Frequency, UnitID, Channel, ModeChannel, AA, Callsign, Bearing, Delay) + + AA=AA or self:IsAir() + UnitID=UnitID or self:GetID() + + -- Command + local CommandActivateBeacon= { + id = "ActivateBeacon", + params = { + ["type"] = Type, + ["system"] = System, + ["frequency"] = Frequency, + ["unitId"] = UnitID, + ["channel"] = Channel, + ["modeChannel"] = ModeChannel, + ["AA"] = AA, + ["callsign"] = Callsign, + ["bearing"] = Bearing, + } + } + + if Delay and Delay>0 then + SCHEDULER:New(nil, self.CommandActivateBeacon, {self, Type, System, Frequency, UnitID, Channel, ModeChannel, AA, Callsign, Bearing}, Delay) + else + self:SetCommand(CommandActivateBeacon) + end + + return self +end + +--- Activate ICLS system of the CONTROLLABLE. The controllable should be an aircraft carrier! +-- @param #CONTROLLABLE self +-- @param #number Channel ICLS channel. +-- @param #number UnitID The ID of the unit the ICLS system is attached to. Useful if more units are in one group. +-- @param #string Callsign Morse code identification callsign. +-- @param #number Delay (Optional) Delay in seconds before the ICLS is deactivated. +-- @return #CONTROLLABLE self +function CONTROLLABLE:CommandActivateICLS(Channel, UnitID, Callsign, Delay) + self:F() + + -- Command to activate ICLS system. + local CommandActivateICLS= { + id = "ActivateICLS", + params= { + ["type"] = BEACON.Type.ICLS, + ["channel"] = Channel, + ["unitId"] = UnitID, + ["callsign"] = Callsign, + } + } + + if Delay and Delay>0 then + SCHEDULER:New(nil, self.CommandActivateICLS, {self}, Delay) + else + self:SetCommand(CommandActivateICLS) + end + + return self +end + + +--- Deactivate the active beacon of the CONTROLLABLE. +-- @param #CONTROLLABLE self +-- @param #number Delay (Optional) Delay in seconds before the beacon is deactivated. +-- @return #CONTROLLABLE self +function CONTROLLABLE:CommandDeactivateBeacon(Delay) + self:F() + + -- Command to deactivate + local CommandDeactivateBeacon={id='DeactivateBeacon', params={}} + + if Delay and Delay>0 then + SCHEDULER:New(nil, self.CommandActivateBeacon, {self}, Delay) + else + self:SetCommand(CommandDeactivateBeacon) + end + + return self +end + +--- Deactivate the ICLS of the CONTROLLABLE. +-- @param #CONTROLLABLE self +-- @param #number Delay (Optional) Delay in seconds before the ICLS is deactivated. +-- @return #CONTROLLABLE self +function CONTROLLABLE:CommandDeactivateICLS(Delay) + self:F() + + -- Command to deactivate + local CommandDeactivateICLS={id='DeactivateICLS', params={}} + + if Delay and Delay>0 then + SCHEDULER:New(nil, self.CommandDeactivateICLS, {self}, Delay) + else + self:SetCommand(CommandDeactivateICLS) + end + + return self +end + + -- TASKS FOR AIR CONTROLLABLES - - --- (AIR) Attack a Controllable. -- @param #CONTROLLABLE self -- @param Wrapper.Controllable#CONTROLLABLE AttackGroup The Controllable to be attacked. @@ -870,6 +993,38 @@ function CONTROLLABLE:TaskOrbitCircleAtVec2( Point, Altitude, Speed ) return DCSTask end +--- (AIR) Orbit at a position with at a given altitude and speed. Optionally, a race track pattern can be specified. +-- @param #CONTROLLABLE self +-- @param Core.Point#COORDINATE Coord Coordinate at which the CONTROLLABLE orbits. +-- @param #number Altitude Altitude in meters of the orbit pattern. +-- @param #number Speed Speed [m/s] flying the orbit pattern +-- @param Core.Point#COORDINATE CoordRaceTrack (Optional) If this coordinate is specified, the CONTROLLABLE will fly a race-track pattern using this and the initial coordinate. +-- @return #CONTROLLABLE self +function CONTROLLABLE:TaskOrbit(Coord, Altitude, Speed, CoordRaceTrack) + + local Pattern=AI.Task.OrbitPattern.CIRCLE + + local P1=Coord:GetVec2() + local P2=nil + if CoordRaceTrack then + Pattern=AI.Task.OrbitPattern.RACE_TRACK + P2=CoordRaceTrack:GetVec2() + end + + local Task = { + id = 'Orbit', + params = { + pattern = Pattern, + point = P1, + point2 = P2, + speed = Speed, + altitude = Altitude, + } + } + + return Task +end + --- (AIR) Orbit at the current position of the first unit of the controllable at a specified alititude. -- @param #CONTROLLABLE self -- @param #number Altitude The altitude [m] to hold the position. @@ -958,11 +1113,7 @@ function CONTROLLABLE:TaskRefueling() -- params = {} -- } - local DCSTask - DCSTask = { id = 'Refueling', - params = { - }, - }, + local DCSTask={id='Refueling', params={}} self:T3( { DCSTask } ) return DCSTask @@ -2101,7 +2252,7 @@ do -- Route methods FromCoordinate = FromCoordinate or self:GetCoordinate() -- Get path and path length on road including the end points (From and To). - local PathOnRoad, LengthOnRoad=FromCoordinate:GetPathOnRoad(ToCoordinate, true) + local PathOnRoad, LengthOnRoad, GotPath =FromCoordinate:GetPathOnRoad(ToCoordinate, true) -- Get the length only(!) on the road. local _,LengthRoad=FromCoordinate:GetPathOnRoad(ToCoordinate, false) @@ -2113,7 +2264,7 @@ do -- Route methods -- Calculate the direct distance between the initial and final points. local LengthDirect=FromCoordinate:Get2DDistance(ToCoordinate) - if PathOnRoad then + if GotPath then -- Off road part of the rout: Total=OffRoad+OnRoad. LengthOffRoad=LengthOnRoad-LengthRoad @@ -2136,7 +2287,7 @@ do -- Route methods local canroad=false -- Check if a valid path on road could be found. - if PathOnRoad and LengthDirect > 2000 then -- if the length of the movement is less than 1 km, drive directly. + if GotPath and LengthDirect > 2000 then -- if the length of the movement is less than 1 km, drive directly. -- Check whether the road is very long compared to direct path. if LongRoad and Shortcut then @@ -3024,6 +3175,3 @@ function CONTROLLABLE:IsAirPlane() return nil end - - --- Message APIs \ No newline at end of file diff --git a/Moose Development/Moose/Wrapper/Group.lua b/Moose Development/Moose/Wrapper/Group.lua index e6c6648fd..c93866343 100644 --- a/Moose Development/Moose/Wrapper/Group.lua +++ b/Moose Development/Moose/Wrapper/Group.lua @@ -325,7 +325,7 @@ end -- So all event listeners will catch the destroy event of this group for each unit in the group. -- To raise these events, provide the `GenerateEvent` parameter. -- @param #GROUP self --- @param #boolean GenerateEvent true if you want to generate a crash or dead event for each unit. +-- @param #boolean GenerateEvent If true, a crash or dead event for each unit is generated. If false, if no event is triggered. If nil, a RemoveUnit event is triggered. -- @usage -- -- Air unit example: destroy the Helicopter and generate a S_EVENT_CRASH for each unit in the Helicopter group. -- Helicopter = GROUP:FindByName( "Helicopter" ) @@ -1482,6 +1482,17 @@ function GROUP:Respawn( Template, Reset ) if not Template then Template = self:GetTemplate() end + + -- Get correct heading. + local function _Heading(course) + local h + if course<=180 then + h=math.rad(course) + else + h=-math.rad(360-course) + end + return h + end if self:IsAlive() then local Zone = self.InitRespawnZone -- Core.Zone#ZONE @@ -1515,7 +1526,8 @@ function GROUP:Respawn( Template, Reset ) Template.units[UnitID].alt = self.InitRespawnHeight and self.InitRespawnHeight or GroupUnitVec3.y Template.units[UnitID].x = ( Template.units[UnitID].x - From.x ) + GroupUnitVec3.x -- Keep the original x position of the template and translate to the new position. Template.units[UnitID].y = ( Template.units[UnitID].y - From.y ) + GroupUnitVec3.z -- Keep the original z position of the template and translate to the new position. - Template.units[UnitID].heading = self.InitRespawnHeading and self.InitRespawnHeading or GroupUnit:GetHeading() + Template.units[UnitID].heading = _Heading(self.InitRespawnHeading and self.InitRespawnHeading or GroupUnit:GetHeading()) + Template.units[UnitID].psi = -Template.units[UnitID].heading self:F( { UnitID, Template.units[UnitID], Template.units[UnitID] } ) end end diff --git a/Moose Development/Moose/Wrapper/Positionable.lua b/Moose Development/Moose/Wrapper/Positionable.lua index fef546f38..137d4ab4c 100644 --- a/Moose Development/Moose/Wrapper/Positionable.lua +++ b/Moose Development/Moose/Wrapper/Positionable.lua @@ -706,8 +706,8 @@ end --- Returns the unit's climb or descent angle. -- @param Wrapper.Positionable#POSITIONABLE self --- @return #number Climb or descent angle in degrees. -function POSITIONABLE:GetClimbAnge() +-- @return #number Climb or descent angle in degrees. Or 0 if velocity vector norm is zero (or nil). Or nil, if the position of the POSITIONABLE returns nil. +function POSITIONABLE:GetClimbAngle() -- Get position of the unit. local unitpos = self:GetPosition() @@ -719,10 +719,17 @@ function POSITIONABLE:GetClimbAnge() if unitvel and UTILS.VecNorm(unitvel)~=0 then - return math.asin(unitvel.y/UTILS.VecNorm(unitvel)) - + -- Calculate climb angle. + local angle=math.asin(unitvel.y/UTILS.VecNorm(unitvel)) + + -- Return angle in degrees. + return math.deg(angle) + else + return 0 end end + + return nil end --- Returns the pitch angle of a unit. diff --git a/Moose Development/Moose/Wrapper/Unit.lua b/Moose Development/Moose/Wrapper/Unit.lua index d24a0b0b0..d81a6c01c 100644 --- a/Moose Development/Moose/Wrapper/Unit.lua +++ b/Moose Development/Moose/Wrapper/Unit.lua @@ -902,29 +902,31 @@ end function UNIT:InAir() self:F2( self.UnitName ) + -- Get DCS unit object. local DCSUnit = self:GetDCSObject() --DCS#Unit if DCSUnit then --- Implementation of workaround. The original code is below. --- This to simulate the landing on buildings. - - local UnitInAir = true + -- Get DCS result of whether unit is in air or not. + local UnitInAir = DCSUnit:inAir() + + -- Get unit category. local UnitCategory = DCSUnit:getDesc().category - if UnitCategory == Unit.Category.HELICOPTER then + + -- If DCS says that it is in air, check if this is really the case, since we might have landed on a building where inAir()=true but actually is not. + -- This is a workaround since DCS currently does not acknoledge that helos land on buildings. + -- Note however, that the velocity check will fail if the ground is moving, e.g. on an aircraft carrier! + if UnitInAir==true and UnitCategory == Unit.Category.HELICOPTER then local VelocityVec3 = DCSUnit:getVelocity() - local Velocity = ( VelocityVec3.x ^ 2 + VelocityVec3.y ^ 2 + VelocityVec3.z ^ 2 ) ^ 0.5 -- in meters / sec + local Velocity = UTILS.VecNorm(VelocityVec3) local Coordinate = DCSUnit:getPoint() local LandHeight = land.getHeight( { x = Coordinate.x, y = Coordinate.z } ) local Height = Coordinate.y - LandHeight if Velocity < 1 and Height <= 60 then UnitInAir = false end - else - UnitInAir = DCSUnit:inAir() end - - + self:T3( UnitInAir ) return UnitInAir end