diff --git a/Moose Development/Moose/Ops/Airboss.lua b/Moose Development/Moose/Ops/Airboss.lua index 635db4268..64aad99e9 100644 --- a/Moose Development/Moose/Ops/Airboss.lua +++ b/Moose Development/Moose/Ops/Airboss.lua @@ -8,7 +8,6 @@ -- * Supports human pilots as well as AI flight groups. -- * Automatic LSO grading. -- * Different skill levels from tipps on-the-fly for students to complete ziplip for pros. --- * Rescue helo option. -- * Recovery tanker option. -- * Voice overs for LSO and AIRBOSS calls. Can easily be customized by users. -- * Automatic TACAN and ICLS channel setting. @@ -49,9 +48,6 @@ -- @field Core.Zone#ZONE_UNIT zoneCCA Carrier controlled area (CCA), i.e. a zone of 50 NM radius around the carrier. -- @field Core.Zone#ZONE_UNIT zoneCCZ Carrier controlled zone (CCZ), i.e. a zone of 5 NM radius around the carrier. -- @field Core.Zone#ZONE_UNIT zoneInitial Zone usually 3 NM astern of carrier where pilots start their CASE I pattern. --- @field Core.Zone#ZONE_UNIT zonePlatform Zone astern the carrier where pilots should hit 5000 ft in CASE II/III. --- @field Core.Zone#ZONE_UNIT zoneDirtyup Zone astern the carrier where pilots should hit 1200 ft and dirty up. --- @field Core.Zone#ZONE_UNIT zoneBullseye Zone astern the carrier where pilots should intercept the glide slope. -- @field #table players Table of players. -- @field #table menuadded Table of units where the F10 radio menu was added. -- @field #AIRBOSS.Checkpoint Upwind Upwind checkpoint. @@ -70,7 +66,7 @@ -- @field #table flights List of all flights in the CCA. -- @field #table Qmarshal Queue of marshalling aircraft groups. -- @field #table Qpattern Queue of aircraft groups in the landing pattern. --- @field Ops.RescueHelo#RESCUEHELO rescuehelo Rescue helo flying in close formation with the carrier. +-- @field #number Nmaxpattern Max number of aircraft in landing pattern. -- @field Ops.RecoveryTanker#RECOVERYTANKER tanker Recovery tanker flying overhead of carrier. -- @field Functional.Warehouse#WAREHOUSE warehouse Warehouse object of the carrier. -- @field #table recoverytime List of time intervals when aircraft are recovered. @@ -130,9 +126,6 @@ AIRBOSS = { zoneCCA = nil, zoneCCZ = nil, zoneInitial = nil, - zonePlatform = nil, - zoneDirtyup = nil, - zoneBullseye = nil, players = {}, menuadded = {}, Upwind = {}, @@ -152,11 +145,10 @@ AIRBOSS = { Qpattern = {}, Qmarshal = {}, Nmaxpattern = nil, - rescuehelo = nil, tanker = nil, warehouse = nil, recoverytime = {}, - holdingoffset= 0, + holdingoffset= nil, } --- Player aircraft types capable of landing on carriers. @@ -466,7 +458,7 @@ AIRBOSS.MenuF10={} --- Airboss class version. -- @field #string version -AIRBOSS.version="0.3.5w" +AIRBOSS.version="0.3.7" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list @@ -476,17 +468,17 @@ AIRBOSS.version="0.3.5w" -- TODO: Add radio transmission queue for LSO and airboss. -- TODO: Get correct wire when trapped. -- TODO: Add radio check (LSO, AIRBOSS) to F10 radio menu. --- TODO: Right pattern step after bolter/wo/patternWO? --- TODO: Handle crash event. Delete A/C from queue, send rescue helo, stop carrier? --- TODO: Get fuel state in pounds. -- TODO: Add user functions. -- TODO: Generalize parameters for other carriers. -- TODO: Generalize parameters for other aircraft. --- TODO: CASE II. --- TODO: CASE III. -- TODO: Foul deck check. -- TODO: Persistence of results. -- TODO: Strike group with helo bringing cargo etc. +-- TODO: Right pattern step after bolter/wo/patternWO? +-- TODO: CASE II. +-- TODO: CASE III. +-- DONE: Handle crash event. Delete A/C from queue, send rescue helo. +-- DONE: Get fuel state in pounds. (working for the hornet, did not check others) -- DONE: Add aircraft numbers in queue to carrier info F10 radio output. -- DONE: Monitor holding of players/AI in zoneHolding. -- DONE: Transmission via radio. @@ -540,7 +532,9 @@ function AIRBOSS:New(carriername, alias) -- Create carrier beacon. self.beacon=BEACON:New(self.carrier) - + + -- Defaults: + -- Set up Airboss radio. self.Carrierradio=RADIO:New(self.carrier) self.Carrierradio:SetAlias("AIRBOSS") @@ -551,6 +545,28 @@ function AIRBOSS:New(carriername, alias) self.LSOradio:SetAlias("LSO") self:SetLSOradio() + -- Set ICSL to channel 1. + self:SetICLS() + + -- Set TACAN to channel 74X + self:SetTACAN() + + -- Set max aircraft in landing pattern. + self:SetMaxLandingPattern(1) + + -- Set holding offset to 0 degrees. + self:SetHoldingOffsetAngle(30) + + -- Default recovery case. + self:SetRecoveryCase(1) + + -- CCA 50 NM radius zone around the carrier. + self:SetCarrierControlledArea() + + -- CCZ 5 NM radius zone around the carrier. + self:SetCarrierControlledZone() + + -- Init carrier parameters. if self.carriertype==AIRBOSS.CarrierType.STENNIS then self:_InitStennis() @@ -571,11 +587,6 @@ function AIRBOSS:New(carriername, alias) -- CASE I/II moving zone: Zone 3 NM astern and 100 m starboard of the carrier with radius of 0.5 km. self.zoneInitial=ZONE_UNIT:New("Initial Zone", self.carrier, 0.5*1000, {dx=-UTILS.NMToMeters(3), dy=100, relative_to_unit=true}) - -- CASE II/III moving zones. - local angle=180+self.carrierparam.rwyangle - self.zonePlatform = ZONE_UNIT:New("Platform Zone", self.carrier, 1.5*1000, {rho=UTILS.NMToMeters(20), theta=angle, relative_to_unit=true}) - self.zoneDirtyup = ZONE_UNIT:New("Dirty Up Zone", self.carrier, 1.5*1000, {rho=UTILS.NMToMeters(10), theta=angle, relative_to_unit=true}) - self.zoneBullseye = ZONE_UNIT:New("Bulleye Zone", self.carrier, 1.5*1000, {rho=UTILS.NMToMeters( 3), theta=angle, relative_to_unit=true}) -- Smoke zones. if self.Debug then @@ -583,18 +594,17 @@ function AIRBOSS:New(carriername, alias) --self.zonePlatform:SmokeZone(SMOKECOLOR.Orange, 90) --self.zoneDirtyup:SmokeZone(SMOKECOLOR.Blue, 90) --self.zoneBullseye:SmokeZone(SMOKECOLOR.Red, 90) - --local zp=self:_GetCase23ValidZone():SmokeZone(SMOKECOLOR.Green, 45) + --self.zoneInitial:SmokeZone(SMOKECOLOR.White, 90) + local case=3 + self:_GetZoneBullseye(case):SmokeZone(SMOKECOLOR.White, 45) + self:_GetZoneDirtyUp(case):SmokeZone(SMOKECOLOR.Orange, 45) + self:_GetZoneArcIn(case):SmokeZone(SMOKECOLOR.Blue, 45) + self:_GetZoneArcOut(case):SmokeZone(SMOKECOLOR.Blue, 45) + self:_GetZonePlatform(case):SmokeZone(SMOKECOLOR.Red, 45) + self:_GetZoneCorridor(case):SmokeZone(SMOKECOLOR.Green, 45) + end - -- CCA 50 NM radius zone around the carrier. - self:SetCarrierControlledArea() - - -- CCZ 5 NM radius zone around the carrier. - self:SetCarrierControlledZone() - - -- Default recovery case. - self:SetRecoveryCase(1) - -- Init default sound files. for _name,_sound in pairs(AIRBOSS.Soundfile) do local sound=_sound --#AIRBOSS.RadioSound @@ -620,6 +630,7 @@ function AIRBOSS:New(carriername, alias) self:AddTransition("*", "Idle", "Idle") -- Carrier is idleing. self:AddTransition("Idle", "Recover", "Recovering") -- Recover aircraft. self:AddTransition("*", "Status", "*") -- Update status of players and queues. + self:AddTransition("*", "Case", "*") -- Switch to another case recovery. self:AddTransition("*", "Stop", "Stopped") -- Stop AIRBOSS script. @@ -653,6 +664,20 @@ function AIRBOSS:New(carriername, alias) -- @param #number delay Delay in seconds. + --- Triggers the FSM event "Case" that switches the recovery case. + -- @function [parent=#AIRBOSS] Case + -- @param #AIRBOSS self + -- @param #number OldCase Old recovery case. + -- @param #number NewCase New recovery case. + + --- Triggers the delayed FSM event "Case" that switches the recovery case + -- @function [parent=#AIRBOSS] __Case + -- @param #AIRBOSS self + -- @param #number delay Delay in seconds. + -- @param #number OldCase Old recovery case. + -- @param #number NewCase New recovery case. + + --- Triggers the FSM event "Stop" that stops the airboss. Event handlers are stopped. -- @function [parent=#AIRBOSS] Stop -- @param #AIRBOSS self @@ -708,6 +733,18 @@ function AIRBOSS:SetRecoveryCase(case) return self end +--- Set holding pattern offset from final bearing for Case II/III recoveries. +-- Usually, this is +-15 or +-30 degrees. +-- @param #AIRBOSS self +-- @param #number offset Offset angle in degrees. Default 0. +-- @return #AIRBOSS self +function AIRBOSS:SetHoldingOffsetAngle(offset) + + self.holdingoffset=offset or 0 + + return self +end + --- Add recovery time slot. -- @param #AIRBOSS self -- @param #string starttime Start time, e.g. "8:00" for eight o'clock. @@ -797,12 +834,12 @@ function AIRBOSS:SetCarrierradio(frequency, modulation) end ---- Define rescue helicopter associated with the carrier. +--- Set number of aircraft units which can be in the landing pattern before the pattern is full. -- @param #AIRBOSS self --- @param Ops.RescueHelo#RESCUEHELO rescuehelo Rescue helo object. +-- @param #number nmax Max number. Default 4. -- @return #ARIBOSS self -function AIRBOSS:SetRescueHelo(rescuehelo) - self.rescuehelo=rescuehelo +function AIRBOSS:SetMaxLandingPattern(nmax) + self.Nmaxpattern=nmax or 4 return self end @@ -841,7 +878,7 @@ function AIRBOSS:IsIdle() end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- --- FSM states +-- FSM event functions ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- --- On after Start event. Starts the warehouse. Addes event handlers and schedules status updates of reqests and queue. @@ -872,12 +909,12 @@ function AIRBOSS:onafterStart(From, Event, To) self:HandleEvent(EVENTS.Birth) self:HandleEvent(EVENTS.Land) self:HandleEvent(EVENTS.Crash) - --self:HandleEvent(EVENTS.Ejection) + self:HandleEvent(EVENTS.Ejection) -- Time stamp for checking queues. self.Tqueue=timer.getTime() - -- Init status check + -- Start status check in 1 second. self:__Status(1) end @@ -993,6 +1030,44 @@ function AIRBOSS:_CheckRecoveryTimes() end +--- On before "Case" event. +-- @param #AIRBOSS self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #number OldCase The old (current) case. +-- @param #number NewCase The new case. +-- @return #boolean If true, switching to new case recovery is allowed. +function AIRBOSS:onbeforeCase(From, Event, To, OldCase, NewCase) + if NewCase==self.case then + -- Old=New ==> no switch necessary + return false + end + + if NewCase<1 or NewCase>3 then + self:E(self.lid.."ERROR: new case is not 1, 2 or 3 but %s", tostring(NewCase)) + return false + end + + return true +end + +--- On after "Case" event. +-- @param #AIRBOSS self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #number OldCase The old (current) case. +-- @param #number NewCase The new case. +function AIRBOSS:onbeforeCase(From, Event, To, OldCase, NewCase) + + self.case=NewCase + + + +end + + --- On before "Recover" event. -- @param #AIRBOSS self -- @param #string From From state. @@ -1004,7 +1079,7 @@ function AIRBOSS:onbeforeRecover(From, Event, To) end ---- On after Stop event. Unhandle events and stop status updates. +--- On after Stop event. Unhandle events. -- @param #AIRBOSS self -- @param #string From From state. -- @param #string Event Event. @@ -1013,6 +1088,7 @@ function AIRBOSS:onafterStop(From, Event, To) self:UnHandleEvent(EVENTS.Birth) self:UnHandleEvent(EVENTS.Land) self:UnHandleEvent(EVENTS.Crash) + self:UnHandleEvent(EVENTS.Ejection) end ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -1188,10 +1264,14 @@ end -- @return #number Speed in m/s or nil. function AIRBOSS:_GetAircraftParameters(playerData, step) + -- Get parameters depended on step. step=step or playerData.step + + -- Get AC type. local hornet=playerData.actype==AIRBOSS.AircraftCarrier.HORNET local skyhawk=playerData.actype==AIRBOSS.AircraftCarrier.A4EC + -- Return values. local alt local aoa local dist @@ -1281,7 +1361,7 @@ function AIRBOSS:_GetAircraftParameters(playerData, step) alt=UTILS.FeetToMeters(500) end - aoa=8.1 + aoa=8.1 elseif step==AIRBOSS.PatternStep.WAKE then @@ -1328,7 +1408,7 @@ function AIRBOSS:_CheckQueue() local nmarshal,_=self:_GetQueueInfo(self.Qmarshal) -- Check if there are flights in marshal strack and if the pattern is free. - if nmarshal>0 and npattern<1 then + if nmarshal>0 and npattern stay in pattern and try again. - playerData.step=AIRBOSS.PatternStep.COMMENCING - elseif playerData.patternwo then - -- CASE I pattern wave off. - -- Ask again? Back to marshal. - playerData.step=AIRBOSS.PatternStep.COMMENCING - end - - elseif flight.case==2 then - - - - elseif flight.case==3 then - - end - - else - end - ]] - - playerData.step=AIRBOSS.PatternStep.COMMENCING - - + elseif playerData.landed and not playerData.unit:InAir() then -- Remove player unit from flight and all queues. self:_RemoveUnitFromFlight(playerData.unit) -- Message to player. - self:MessageToPlayer(playerData, "Welcome to the carrier!", "LSO", nil, 10) + self:MessageToPlayer(playerData, "Welcome on board!", "LSO", nil, 10) else @@ -4731,6 +5035,56 @@ function AIRBOSS:_IsHuman(group) return false end +--- Get fuel state in pounds. +-- @param #AIRBOSS self +-- @param Wrapper.Unit#UNIT unit The unit for which the mass is determined. +-- @return #number Fuel state in pounds. +function AIRBOSS:_GetFuelState(unit) + + -- Get relative fuel [0,1]. + local fuel=unit:GetFuel() + + -- Get max weight of fuel in kg. + local maxfuel=self:_GetUnitMasses(unit) + + -- Fuel state, i.e. what let's + local fuelstate=fuel*maxfuel + + -- Debug info. + self:I(self.lid..string.format("Unit %s fuel state = %.1f kg = %.1f lbs", unit:GetName(), fuelstate, UTILS.kg2lbs(fuelstate))) + + return UTILS.kg2lbs(fuelstate) +end + +--- Get unit masses especially fuel from DCS descriptor values. +-- @param #AIRBOSS self +-- @param Wrapper.Unit#UNIT unit The unit for which the mass is determined. +-- @return #number Mass of fuel in kg. +-- @return #number Empty weight of unit in kg. +-- @return #number Max weight of unit in kg. +-- @return #number Max cargo weight in kg. +function AIRBOSS:_GetUnitMasses(unit) + + -- Get DCS descriptors table. + local Desc=unit:GetDesc() + + -- Mass of fuel in kg. + local massfuel=Desc.fuelMassMax or 0 + + -- Mass of empty unit in km. + local massempty=Desc.massEmpty or 0 + + -- Max weight of unit in kg. + local massmax=Desc.massMax or 0 + + -- Rest is cargo. + local masscargo=massmax-massfuel-massempty + + -- Debug info. + self:I(self.lid..string.format("Unit %s mass fuel=%.1f kg, empty=%.1f kg, max=%.1f kg, cargo=%.1f kg", unit:GetName(), massfuel, massempty, massmax, masscargo)) + + return massfuel, massempty, massmax, masscargo +end --- Get player data from unit object -- @param #AIRBOSS self @@ -5025,6 +5379,7 @@ function AIRBOSS:_RequestCommence(_unitName) -- Get stack value. local stack=playerData.flag:Get() + -- Check if player is in the lowest stack. if stack>1 then -- We are in a higher stack. text="Negative ghostrider, it's not your turn yet!" @@ -5033,8 +5388,8 @@ function AIRBOSS:_RequestCommence(_unitName) -- Number of aircraft currently in pattern. local _,npattern=self:_GetQueueInfo(self.Qpattern) - -- TODO: set nmax for pattern. Should be ~6 but let's make this 4. - if npattern>0 then + -- Check if pattern is already full. + if npattern>self.Nmaxpattern then -- Patern is full! text=string.format("Negative ghostrider, pattern is full! There are %d aircraft currently in pattern.", npattern) else @@ -5503,6 +5858,14 @@ function AIRBOSS:_DisplayPlayerStatus(_unitName) local playerData=self.players[_playername] --#AIRBOSS.PlayerData if playerData then + + -- Stack and stack altitude. + local stack=playerData.flag:Get() + local stackalt=UTILS.MetersToFeet(self:_GetMarshalAltitude(stack)) + + -- Fuel and fuel state. + local fuel=playerData.unit:GetFuel()*100 + local fuelstate=self:_GetFuelState(playerData.unit) -- Player data. local text=string.format("Status of player %s (%s)\n", playerData.name, playerData.callsign) @@ -5511,15 +5874,11 @@ function AIRBOSS:_DisplayPlayerStatus(_unitName) text=text..string.format("Skil level: %s\n", playerData.difficulty) text=text..string.format("Aircraft: %s\n", playerData.actype) text=text..string.format("Board number: %s\n", playerData.onboard) - text=text..string.format("Fuel: %.1f %%\n", playerData.unit:GetFuel()*100) - local stack=playerData.flag:Get() - local stackalt=UTILS.MetersToFeet(self:_GetMarshalAltitude(stack)) - text=text..string.format("Flag/stack: %d\n", stack) - text=text..string.format("Stack alt: %d ft\n", stackalt) + text=text..string.format("Fuel state: %.1f lbs/1000 (%.1f %%)\n", fuelstate/1000, fuel) + text=text..string.format("Stack: %d alt=%d ft\n", stack, stackalt) text=text..string.format("Group: %s\n", playerData.group:GetName()) - text=text..string.format("# units: %d\n", #playerData.group:GetUnits()) - text=text..string.format("n units: %d\n", playerData.nunits) - text=text..string.format("Section Lead: %s\n", tostring(playerData.seclead)) + text=text..string.format("# units: %d (n=%d)\n", #playerData.group:GetUnits(), playerData.nunits) + text=text..string.format("Section Lead: %s\n", tostring(playerData.seclead)) text=text..string.format("# section: %d", #playerData.section) for _,_sec in pairs(playerData.section) do local sec=_sec --#AIRBOSS.PlayerData @@ -5539,7 +5898,7 @@ function AIRBOSS:_DisplayPlayerStatus(_unitName) elseif playerData.step==AIRBOSS.PatternStep.PLATFORM then -- Heading and distance to platform zone. - local flyhdg=playerData.unit:GetCoordinate():HeadingTo(self.zonePlatform:GetCoordinate()) + local flyhdg=playerData.unit:GetCoordinate():HeadingTo(self:_GetZonePlatform(playerData.case):GetCoordinate()) local flydist=UTILS.MetersToNM(playerData.unit:GetCoordinate():Get2DDistance(self.zoneInitial:GetCoordinate())) local fb=self:GetFinalBearing(true) @@ -5610,32 +5969,51 @@ function AIRBOSS:_MarkCase23Zones(_unitName, flare) local playerData=self.players[_playername] --#AIRBOSS.PlayerData if playerData then + + local case=playerData.case + + if case<2 then + case=3 + end + - local text="CASE II/III: Marking:\n" + -- Initial + local text=string.format("Marking CASE %d zone:\n", case) --TODO: Add height! if flare then - text=text.."* Valid zone with GREEN flares\n" - local zp=self:_GetCase23ValidZone() - zp:FlareZone(FLARECOLOR.Green, 45) - text=text.."* Platform zone with RED flares\n" - self.zonePlatform:FlareZone(FLARECOLOR.Red, 45) - text=text.."* Dirty up zone with YELLOW flares\n" - self.zoneDirtyup:FlareZone(FLARECOLOR.Yellow, 45) - text=text.."* Bullseye zone with WHITE flares\n" - self.zoneBullseye:FlareZone(FLARECOLOR.White, 45) + text=text.."* approach corridor with GREEN flares\n" + self:_GetZoneCorridor(case):FlareZone(FLARECOLOR.Green, 45) + text=text.."* platform with RED flares\n" + self:_GetZonePlatform(case):FlareZone(FLARECOLOR.Red, 45) + text=text.."* dirty up with YELLOW flares\n" + self:_GetZoneDirtyUp(case):FlareZone(FLARECOLOR.Yellow, 45) + if math.abs(self.holdingoffset)>0 then + self:_GetZoneArcIn(case):FlareZone(FLARECOLOR.Yellow, 45) + text=text.."* arc turn in with YELLOW flares\n" + self:_GetZoneArcOut(case):FlareZone(FLARECOLOR.White, 45) + text=text.."* arc trun out with WHITE flares\n" + end + text=text.."* bullseye with WHITE flares\n" + self:_GetZoneBullseye(case):FlareZone(FLARECOLOR.White, 45) else - text=text.."* Valid zone with GREEN smoke\n" - local zp=self:_GetCase23ValidZone() - zp:SmokeZone(SMOKECOLOR.Green, 45) - text=text.."* Platform zone with RED smoke\n" - self.zonePlatform:SmokeZone(SMOKECOLOR.Red, 45) - text=text.."* Dirty up zone with ORANGE flares\n" - self.zoneDirtyup:SmokeZone(SMOKECOLOR.Orange, 45) - text=text.."* Bullseye zone with BLUE smoke\n" - self.zoneBullseye:SmokeZone(SMOKECOLOR.Blue, 45) + text=text.."* approach corridor with GREEN smoke\n" + self:_GetZoneCorridor(case):SmokeZone(SMOKECOLOR.Green, 45) + text=text.."* platform with RED smoke\n" + self:_GetZonePlatform(case):SmokeZone(SMOKECOLOR.Red, 45) + text=text.."* dirty up with ORANGE flares\n" + self:_GetZoneDirtyUp(case):SmokeZone(SMOKECOLOR.Orange, 45) + if math.abs(self.holdingoffset)>0 then + self:_GetZoneArcIn(case):SmokeZone(SMOKECOLOR.Red, 45) + text=text.."* arc turn in with YELLOW flares\n" + self:_GetZoneArcOut(case):SmokeZone(SMOKECOLOR.Orange, 45) + text=text.."* arc trun out with WHITE flares\n" + end + text=text.."* bullseye with BLUE smoke\n" + self:_GetZoneBullseye(case):SmokeZone(SMOKECOLOR.Blue, 45) end + -- Send message to player. self:MessageToPlayer(playerData, text, "AIRBOSS", "", 10) end diff --git a/Moose Development/Moose/Utilities/Utils.lua b/Moose Development/Moose/Utilities/Utils.lua index be2754d19..9eee42af4 100644 --- a/Moose Development/Moose/Utilities/Utils.lua +++ b/Moose Development/Moose/Utilities/Utils.lua @@ -308,7 +308,12 @@ UTILS.hPa2mmHg = function( hPa ) return hPa * 0.7500615613030 end - +--- Convert kilo gramms (kg) to pounds (lbs). +-- @param #number kg Mass in kg. +-- @return #number Mass in lbs. +UTILS.kg2lbs = function( kg ) + return kg * 2.20462 +end --[[acc: in DM: decimal point of minutes.