diff --git a/Moose Development/Moose/Functional/CarrierTrainer.lua b/Moose Development/Moose/Functional/CarrierTrainer.lua index 3f6975264..cdd8ba769 100644 --- a/Moose Development/Moose/Functional/CarrierTrainer.lua +++ b/Moose Development/Moose/Functional/CarrierTrainer.lua @@ -14,8 +14,9 @@ -- * Automatic TACAN and ICLS channel setting. -- * Different radio channels for LSO and airboss calls. -- * F10 radio menu including carrier info (weather, radio frequencies, TACAN/ICLS channels, pilot grades). +-- * Multiple carriers supported. -- --- Please not that his class is work in progress and in an **alpha** stage. +-- **PLEASE NOTE** that his class is work in progress and in an **alpha** stage. -- At the moment training parameters are optimized for F/A-18C Hornet as aircraft and USS Stennis as carrier. -- Other aircraft and carriers **might** be possible in future but would need a different set of parameters. -- @@ -44,7 +45,7 @@ -- @field Core.Radio#RADIO Carrierradio Radio for carrier calls. -- @field #AIRBOSS.RadioCalls radiocall LSO and Airboss call sound files and texts. -- @field Core.Zone#ZONE_UNIT startZone Zone in which the pattern approach starts. --- @field Core.Zone#ZONE_UNIT carrierZone Large zone around the carrier to welcome players. +-- @field Core.Zone#ZONE_UNIT carrierZone Carrier controlled area (CCA), i.e. a zone of 50 NM radius around the carrier. -- @field Core.Zone#ZONE_UNIT registerZone Zone behind the carrier to register for a new approach. -- @field Core.Zone#ZONE_UNIT zoneHolding Zone where aircraft are holding before entering the landing pattern. -- @field #table players Table of players. @@ -60,10 +61,7 @@ -- @field #AIRBOSS.Checkpoint C3Descent4k Case III descent at 4000 ft/min right after leaving holding pattern. -- @field #AIRBOSS.Checkpoint C3Descent2k Case III descent at 2000 ft/min at 5000 ft plattform. -- @field #AIRBOSS.Checkpoint C3DirtyUp Case III dirty up and on speed position at 1200 ft and 10-12 NM from the carrier. --- @field #AIRBOSS.Checkpoint C3BullsEye Case III intercept glideslope and follow ICLS "bullseye". --- @field #number rwyangle Angle of the runway wrt to carrier "nose". For the Stennis ~ -10 degrees. --- @field #number sterndist Distance in meters from carrier coordinate to the end of the deck. --- @field #number deckheight Height of the deck in meters. +-- @field #AIRBOSS.Checkpoint C3BullsEye Case III intercept glideslope and follow ICLS "bullseye". -- @field #number case Recovery case I, II or III. -- @field #table Qmarshal Queue of marshalling aircraft groups. -- @field #table Qpattern Queue of aircraft groups in the landing pattern. @@ -89,7 +87,7 @@ AIRBOSS = { Debug = true, carrier = nil, carriertype = nil, - carrierparam = nil, + carrierparam = {}, alias = nil, airbase = nil, beacon = nil, @@ -119,7 +117,6 @@ AIRBOSS = { C3Descent2k = {}, C3DirtyUp = {}, C3BullsEye = {}, - radiocall = nil, case = 1, Qpattern = {}, Qmarshal = {}, @@ -164,7 +161,7 @@ AIRBOSS.CarrierType={ -- @type AIRBOSS.PatternStep AIRBOSS.PatternStep={ UNDEFINED="Undefined", - UNREGISTERED="Unregistered", + COMMENCING="Commencing", HOLDING="Holding", DESCENT4K="Descent 4000 ft/min", DESCENT2K="Descent 2000 ft/min", @@ -212,11 +209,13 @@ AIRBOSS.PatternStep={ -- @type AIRBOSS.Soundfile -- @field #AIRBOSS.RadioSound RIGHTFORLINEUP -- @field #AIRBOSS.RadioSound COMELEFT --- @field #AIRBOSS.RadioSound --- @field #AIRBOSS.RadioSound --- @field #AIRBOSS.RadioSound --- @field #AIRBOSS.RadioSound --- @field #AIRBOSS.RadioSound +-- @field #AIRBOSS.RadioSound HIGH +-- @field #AIRBOSS.RadioSound POWER +-- @field #AIRBOSS.RadioSound CALLTHEBALL +-- @field #AIRBOSS.RadioSound ROGERBALL +-- @field #AIRBOSS.RadioSound WAVEOFF +-- @field #AIRBOSS.RadioSound BOLTER +-- @field #AIRBOSS.RadioSound LONGINGROOVE AIRBOSS.Soundfile={ RIGHTFORLINEUP={ normal="LSO - RightLineUp(L).ogg", @@ -328,6 +327,7 @@ AIRBOSS.GroovePos={ -- @field Wrapper.Group#GROUP group Aircraft group the player is in. -- @field #string callsign Callsign of player. -- @field #string difficulty Difficulty level. +-- @field #string step Coming pattern step. -- @field #number passes Number of passes. -- @field #boolean attitudemonitor If true, display aircraft attitude and other parameters constantly. -- @field #table debrief Debrief analysis of the current step of this pass. @@ -380,13 +380,14 @@ AIRBOSS.MenuF10={} --- Airboss class version. -- @field #string version -AIRBOSS.version="0.2.5w" +AIRBOSS.version="0.2.6" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- --- TODO: Handle crash event. Delete ac from queue, send rescue helo, stop carrier? +-- TODO: Handle crash event. Delete A/C from queue, send rescue helo, stop carrier? +-- TODO: Add aircraft numbers in queue to carrier info F10 radio output. -- TODO: Transmission via radio. -- TODO: Get board numbers. -- TODO: Get fuel state in pounds. @@ -444,9 +445,11 @@ function AIRBOSS:New(carriername, alias) -- Set up Airboss radio. self.Carrierradio=RADIO:New(self.carrier) + self:SetCarrierradio() -- Set up LSO radio. self.LSOradio=RADIO:New(self.carrier) + self:SetLSOradio() -- Init carrier parameters. if self.carriertype==AIRBOSS.CarrierType.STENNIS then @@ -465,11 +468,11 @@ function AIRBOSS:New(carriername, alias) return nil end - -- Zone 5 km astern and 100 m starboard of the carrier with radius of 2.5 km. - self.registerZone = ZONE_UNIT:New("registerZone", self.carrier, 2.5*1000, {dx = -5000, dy = 100, relative_to_unit=true}) + -- Zone 3 NM astern and 100 m starboard of the carrier with radius of 2.0 km. + self.zoneInitial=ZONE_UNIT:New("registerZone", self.carrier, 2.0*1000, {dx=-UTILS.NMToMeters(3), dy=100, relative_to_unit=true}) -- Zone 2 km astern and 100 m starboard of the carrier with a radius of 1 km. - self.startZone = ZONE_UNIT:New("startZone", self.carrier, 1.0*1000, {dx = -2000, dy = 100, relative_to_unit=true}) + self.startZone = ZONE_UNIT:New("startZone", self.carrier, 1.0*1000, {dx=-2000, dy=100, relative_to_unit=true}) -- Zone around the carrier with a radius of 30 km. self:SetCarrierControlledZone() @@ -477,6 +480,13 @@ function AIRBOSS:New(carriername, alias) -- Default recovery case. self:SetRecoveryCase(1) + env.info("FF sound files:") + for _name,_sound in pairs(AIRBOSS.Soundfile) do + local sound=_sound --#AIRBOSS.RadioSound + self:I{name=_name,sound=_sound} + self.radiocall[_name]=sound + end + ----------------------- --- FSM Transitions --- ----------------------- @@ -601,22 +611,26 @@ end --- Set LSO radio frequency. -- @param #AIRBOSS self --- @param #number freq Frequency in MHz. Default 264 MHz. +-- @param #number frequency Frequency in MHz. Default 264 MHz. +-- @param #string modulation Modulation, i.e. "AM" (default) or "FM". -- @return #AIRBOSS self -function AIRBOSS:SetLSOradio(freq) +function AIRBOSS:SetLSOradio(frequency, modulation) - self.LSOfreq=freq or 264 + self.LSOfreq=frequency or 264 + self.LSOmodulation=modulation or "AM" return self end --- Set carrier radio frequency. -- @param #AIRBOSS self --- @param #number freq Frequency in MHz. Default 305. +-- @param #number frequency Frequency in MHz. Default 305 MHz. +-- @param #string modulation Modulation, i.e. "AM" (default) or "FM". -- @return #AIRBOSS self -function AIRBOSS:SetCarrierradio(freq) +function AIRBOSS:SetCarrierradio(frequency, modulation) - self.Carrierfreq=freq or 305 + self.Carrierfreq=frequency or 305 + self.Carrriermodulation=modulation or "AM" return self end @@ -687,12 +701,12 @@ function AIRBOSS:onafterStatus(From, Event, To) if startrecovery==true then self:Recover() end - - local text=string.format("AIRBOSS %s: Status %s.", self.alias, self:GetState()) - self:I(text) -- Update marshal and pattern queue every 30 seconds. if time-self.Tqueue>30 then + + local text=string.format("AIRBOSS %s: Status %s.", self.alias, self:GetState()) + self:I(text) -- Scan carrier zone for new aircraft. self:_ScanCarrierZone() @@ -708,7 +722,7 @@ function AIRBOSS:onafterStatus(From, Event, To) self:_CheckPlayerStatus() -- Call status again in one second. - self:__Status(-1) + self:__Status(-0.5) end --- Check if recovery times. @@ -850,7 +864,7 @@ function AIRBOSS:_PrintQueue(queue, name) end ---- Check if new aircraft arrived +--- Scan carrier zone for (new) units. -- @param #AIRBOSS self function AIRBOSS:_ScanCarrierZone() --env.info("FF Scanning Carrier Zone") @@ -874,6 +888,7 @@ function AIRBOSS:_ScanCarrierZone() -- Check if this an aircraft and that it is airborn and closing in. if unit:IsAir() and unit:InAir() and unit:IsInZone(zsma)then -- TODO: check for correct aircraft types and also helos! + -- TODO: check for right coalition. local group=unit:GetGroup() local unitname=unit:GetName() @@ -1011,7 +1026,7 @@ function AIRBOSS:_MarshalAI(group) end -- Pattern altitude. - local Altitude=UTILS.FeetToMeters((nstacks+angels0)*1000) + local Altitude=UTILS.FeetToMeters((nstacks+angels0)*1000) -- Add group to marshal stack. self:_AddMarshallGroup(group, nstacks+1, Altitude) @@ -1077,7 +1092,7 @@ function AIRBOSS:_CollapseMarshalStack() -- Set player step to 0. if flight.ai==false then local playerData=self:_GetPlayerDataGroup(flight.group) - playerData.step=AIRBOSS.PatternStep.UNREGISTERED + playerData.step=AIRBOSS.PatternStep.COMMENCING end -- Time stamp. @@ -1168,6 +1183,7 @@ function AIRBOSS:_CheckPlayerStatus() -- Player unit. local unit=playerData.unit + -- Check if unit is alive and in air. if unit:IsAlive() then -- Display aircraft attitude and other parameters as message text. @@ -1175,7 +1191,7 @@ function AIRBOSS:_CheckPlayerStatus() self:_DetailedPlayerStatus(playerData) end - -- Check if player is in carrier controlled zone. + -- Check if player is in carrier controlled area (zone with R=50 NM around the carrier). if unit:IsInZone(self.carrierZone) then -- Check if player was previously not inside the zone. @@ -1194,26 +1210,48 @@ function AIRBOSS:_CheckPlayerStatus() end - if playerData.step==0 and unit:InAir() then + if playerData.step==AIRBOSS.PatternStep.UNDEFINED then - -- New approach. - self:_NewRound(playerData) + self:I("Player status undefined. Waiting for next step.") - -- Jump to Groove for testing. + -- Jump directly to CASE I straight in approach. + playerData.step=AIRBOSS.PatternStep.COMMENCING + + -- Jump to final/groove for testing. if self.groovedebug then - playerData.step=90 + playerData.step=AIRBOSS.PatternStep.FINAL self.groovedebug=false end + + elseif playerData.step==AIRBOSS.PatternStep.COMMENCING and unit:InAir() then + + -- New approach. + self:_Commencing(playerData) + elseif playerData.step==AIRBOSS.PatternStep.HOLDING then + -- TODO: holding check. + elseif playerData.step==AIRBOSS.PatternStep.DESCENT4K then + -- CASE III: Initial descent with 4000 ft/min. + self:_Descent4k(playerData) + elseif playerData.step==AIRBOSS.PatternStep.DESCENT2K then + -- CASE III: Player has reached 5k "Platform". + self:_Descent2k(playerData) + elseif playerData.step==AIRBOSS.PatternStep.DIRTYUP then + -- CASE III: Player has descended to 1200 ft and is going level from now on. + self:_DirtyUp(playerData) + elseif playerData.step==AIRBOSS.PatternStep.BULLSEYE then + -- CASE III: Player has intercepted the glide slope and should follow "Bullseye" (ICLS). + self:_BullsEye(playerData) + elseif playerData.step==AIRBOSS.PatternStep.INITIAL then -- Player is at the initial position entering the landing pattern. @@ -1254,7 +1292,7 @@ function AIRBOSS:_CheckPlayerStatus() elseif playerData.step==AIRBOSS.PatternStep.FINAL then - -- Entering the groove. + -- Turn to final and enter the groove. self:_Final(playerData) elseif playerData.step==AIRBOSS.PatternStep.GROOVE_XX or @@ -1338,7 +1376,7 @@ function AIRBOSS:OnEventBirth(EventData) self.players[_playername]=self:_InitPlayer(_unitName) -- Start in the groove for debugging. - self.groovedebug=false + self.groovedebug=true end end @@ -1362,43 +1400,50 @@ function AIRBOSS:OnEventLand(EventData) local _group=_unit:GetGroup() local _callsign=_unit:GetCallsign() - -- Debug output. - local text=string.format("Player %s, callsign %s unit %s (ID=%d) of group %s landed.", _playername, _callsign, _unitName, _uid, _group:GetName()) - self:T(self.lid..text) - MESSAGE:New(text, 5):ToAllIf(self.Debug) + -- This would be the closest airbase. + local airbase=EventData.Place + local airbasename=tostring(airbase:GetName()) - -- Player data. - local playerData=self.players[_playername] --#AIRBOSS.PlayerData + -- Check if player landed on the right airbase. + if airbasename==self.airbase:GetName() then - -- Coordinate at landing event - local coord=playerData.unit:GetCoordinate() - - -- Debug mark of player landing coord. - local lp=coord:MarkToAll("Landing coord.") - coord:SmokeGreen() - - -- Debug marks of wires. - local w1=self.carrier:GetCoordinate():Translate(-104, 0):MarkToAll("Wire 1") - local w2=self.carrier:GetCoordinate():Translate( -92, 0):MarkToAll("Wire 2") - local w3=self.carrier:GetCoordinate():Translate( -80, 0):MarkToAll("Wire 3") - local w4=self.carrier:GetCoordinate():Translate( -68, 0):MarkToAll("Wire 4") - - -- We did land. - env.info("FF landed") - playerData.landed=true - - -- Unkonwn step. - playerData.step=AIRBOSS.PatternStep.UNDEFINED - - --TODO: maybe check that we actually landed on the right carrier. - - -- Call trapped function in 3 seconds to make sure we did not bolter. - SCHEDULER:New(nil, self._Trapped,{self, playerData, coord}, 3) + -- Debug output. + local text=string.format("Player %s, callsign %s unit %s (ID=%d) of group %s landed at airbase %s", _playername, _callsign, _unitName, _uid, _group:GetName(), airbasename) + self:T(self.lid..text) + MESSAGE:New(text, 5):ToAllIf(self.Debug) - end - - if self:_InQueue(self.Qpattern, EventData.IniGroup) then - self:_RemoveQueue(self.Qpattern, EventData.IniGroup) + -- Player data. + local playerData=self.players[_playername] --#AIRBOSS.PlayerData + + -- Coordinate at landing event + local coord=playerData.unit:GetCoordinate() + + -- Debug mark of player landing coord. + local lp=coord:MarkToAll("Landing coord.") + coord:SmokeGreen() + + -- Debug marks of wires. + local w1=self.carrier:GetCoordinate():Translate(self.carrierparam.wire1, 0):MarkToAll("Wire 1") + local w2=self.carrier:GetCoordinate():Translate(self.carrierparam.wire2, 0):MarkToAll("Wire 2") + local w3=self.carrier:GetCoordinate():Translate(self.carrierparam.wire3, 0):MarkToAll("Wire 3") + local w4=self.carrier:GetCoordinate():Translate(self.carrierparam.wire4, 0):MarkToAll("Wire 4") + + -- We did land. + env.info("FF landed") + playerData.landed=true + + -- Unkonwn step. + playerData.step=AIRBOSS.PatternStep.UNDEFINED + + -- Call trapped function in 3 seconds to make sure we did not bolter. + SCHEDULER:New(nil, self._Trapped,{self, playerData, coord}, 3) + + end + + if self:_InQueue(self.Qpattern, EventData.IniGroup) then + self:_RemoveQueue(self.Qpattern, EventData.IniGroup) + end + end end @@ -1482,7 +1527,7 @@ function AIRBOSS:_InitPlayer(unitname) playerData.inbigzone=playerData.unit:IsInZone(self.carrierZone) -- Init stuff for this round. - playerData=self:_InitNewRound(playerData) + playerData=self:_InitNewApproach(playerData) -- Return player data table. return playerData @@ -1495,9 +1540,11 @@ end -- @param #AIRBOSS self -- @param #AIRBOSS.PlayerData playerData Player data. -- @return #AIRBOSS.PlayerData Initialized player data. -function AIRBOSS:_InitNewRound(playerData) - self:I(self.lid..string.format("New round for player %s.", playerData.callsign)) - playerData.step=0 +function AIRBOSS:_InitNewApproach(playerData) + self:I(self.lid..string.format("New approach of player %s.", playerData.callsign)) + + playerData.step=AIRBOSS.PatternStep.UNDEFINED + playerData.groove={} playerData.debrief={} playerData.patternwo=false @@ -1507,23 +1554,30 @@ function AIRBOSS:_InitNewRound(playerData) playerData.boltered=false playerData.landed=false playerData.Tlso=timer.getTime() + return playerData end ---- Initialize player data. +--- Commence approach. -- @param #AIRBOSS self -- @param #AIRBOSS.PlayerData playerData Player data. -function AIRBOSS:_NewRound(playerData) - - if playerData.unit:IsInZone(self.registerZone) then - local text="Cleared for approach." - self:_SendMessageToPlayer(text, 10, playerData) +function AIRBOSS:_Commencing(playerData) + + local text="Commencing." + self:_SendMessageToPlayer(text, 10, playerData) - self:_InitNewRound(playerData) - - -- Next step: start of pattern. - playerData.step=1 + -- Initialize player data for new approach. + self:_InitNewApproach(playerData) + + -- Next step: depends on case recovery. + if self.case==1 then + -- CASE I: Player has to fly to the initial which is 3 NM DME astern of the boat. + playerData.step=AIRBOSS.PatternStep.INITIAL + else + -- CASE III: Player has to start the descent at 4000 ft/min. + playerData.step=AIRBOSS.PatternStep.DESCENT4K end + end --- Start pattern when player enters the initial zone. @@ -1531,8 +1585,8 @@ end -- @param #AIRBOSS.PlayerData playerData Player data table. function AIRBOSS:_Initial(playerData) - -- Check if player is in start zone and about to enter the pattern. - if playerData.unit:IsInZone(self.startZone) then + -- Check if player is in initial zone and entering the CASE I pattern. + if playerData.unit:IsInZone(self.zoneInitial) then -- Inform player. local hint = string.format("Entering the pattern.") @@ -1541,7 +1595,7 @@ function AIRBOSS:_Initial(playerData) end -- Send message. - self:_SendMessageToPlayer(hint, 8, playerData) + self:_SendMessageToPlayer(hint, 10, playerData) -- Next step: upwind. playerData.step=AIRBOSS.PatternStep.UPWIND @@ -1790,12 +1844,9 @@ function AIRBOSS:_CheckForLongDownwind(playerData) -- Check we are not too far out w.r.t back of the boat. if X