diff --git a/Moose Development/Moose/Ops/Airboss.lua b/Moose Development/Moose/Ops/Airboss.lua index eb14efcc6..7de0646ec 100644 --- a/Moose Development/Moose/Ops/Airboss.lua +++ b/Moose Development/Moose/Ops/Airboss.lua @@ -842,7 +842,10 @@ -- a naval unit (i.e. the carrier). -- -- As a workaround, you can put an aircraft, e.g. a Helicopter on the deck of the carrier or another ship of the strike group. The aircraft should be set to --- uncontrolled and maybe even to immortal. With the @{#AIRBOSS.SetRadioUnit}(*unitname*) function you can use this unit as "radio repeater". +-- uncontrolled and maybe even to immortal. With the @{#AIRBOSS.SetRadioUnitName}(*unitname*) function you can use this unit as "radio repeater" for both Marshal and LSO +-- radio channels. However, this might lead to interruptions in the transmission if both channels transmit simultaniously. Therefore, it is better to assign a unit for +-- each radio via the @{#AIRBOSS.SetRadioRelayLSO}(unitname) and @{#AIRBOSS.SetRadioRelayMarshal}(unitname) functions. +-- -- Of course you can also use any other aircraft in the vicinity of the carrier, e.g. a rescue helo or a recovery tanker. It is just important that this -- unit is and stays close the the boat as the distance from the sender to the receiver is modeled in DCS. So messages from too far away might not reach the players. -- @@ -880,6 +883,27 @@ -- -- If no AI handling is desired, this can be turned off via the @{#AIRBOSS.SetHandleAIOFF} function. -- +-- In case only specifc AI groups shall be excluded, it can be done by adding the groups to a set, e.g. +-- +-- -- AI groups explicitly excluded from handling by the Airboss +-- local CarrierExcludeSet=SET_GROUP:New():FilterPrefixes("E-2D Wizard Group"):FilterStart() +-- AirbossStennis:SetExcludeAI(CarrierExcludeSet) +-- +-- Similarly, to the @{#AIRBOSS.SetExcludeAI} function, AI groups can be explicitly *included* via the @{#AIRBOSS.SetSquadronAI} function. If this is used, only the *included* groups are handled +-- by the AIRBOSS. +-- +-- ## Refueling +-- +-- AI groups in the marshal pattern can be send to refuel at the recovery tanker or if none is defined to the nearest divert airfield. This can be enabled by the @{AIRBOSS.SetRefuelAI}(*lowfuelthreshold*). +-- The parameter *lowfuelthreshold* is the threshold of fuel in percent. If the fuel drops below this value, the group will go for refueling. If refueling is performed at the recovery tanker, +-- the group will return to the marshal stack when done. The aircraft will not return from the divert airfield however. +-- +-- ## Respawning - DCS Landing Bug +-- +-- AI groups that enter the CCA are usually guided to Marshal stack. However, due to DCS limitations they might not obey the landing task if they have another airfield as departure and/or destination in +-- their mission task. Therefore, AI groups can be respawned when detected in the CCA. This should clear all other airfields and allow the aircraft to land on the carrier. +-- This is achieved by the @{AIRBOSS.SetRespawnAI}() function. +-- -- ## Known Issues -- -- Dealing with the DCS AI is a big challenge and there is only so much one can do. Please bear this in mind! @@ -1362,7 +1386,6 @@ AIRBOSS.Difficulty={ -- @field #number LUE Lineup error in degrees. -- @field #number Roll Roll angle. -- @field #number Rhdg Relative heading player to carrier. 0=parallel, +-90=perpendicular. --- @field #number TGroove Time stamp when pilot entered the groove. -- @field #string FlyThrough Fly through up "/" or fly through down "\\". --- LSO grade data. @@ -1416,6 +1439,7 @@ AIRBOSS.Difficulty={ -- @field #table elements Flight group elements. -- @field #number Tcharlie Charlie (abs) time in seconds. -- @field #string name Player name or name of first AI unit. +-- @field #boolean refueling Flight is refueling. --- Parameters of an element in a flight group. -- @type AIRBOSS.FlightElement @@ -1707,25 +1731,28 @@ function AIRBOSS:New(carriername, alias) ------------------- -- Debug trace. - if true then + if false then --self.Debug=true BASE:TraceOnOff(true) BASE:TraceClass(self.ClassName) BASE:TraceLevel(1) self.dTstatus=0.1 end - - --self:_GetZoneGroove():SmokeZone(SMOKECOLOR.Red, 5) -- Smoke zones. if self.Debug and false then local case=2 + self.holdingoffset=30 + self:_GetZoneGroove():SmokeZone(SMOKECOLOR.Red, 5) 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) + self:_GetZoneHolding(case, 1):SmokeZone(SMOKECOLOR.White, 45) + self:_GetZoneInitial(case):SmokeZone(SMOKECOLOR.Orange, 45) + self:_GetZoneCommence(1):SmokeZone(SMOKECOLOR.Red, 45) end -- Carrier parameter debug tests. @@ -2280,6 +2307,15 @@ function AIRBOSS:SetRespawnAI(switch) return self end +--- Give AI aircraft the refueling task if a recovery tanker is present or send them to the nearest divert airfield. +-- @param #AIRBOSS self +-- @param #number lowfuelthreshold Low fuel threshold in percent. AI will go refueling if their fuel level drops below this value. Default 10 %. +-- @return #AIRBOSS self +function AIRBOSS:SetRefuelAI(lowfuelthreshold) + self.lowfuelAI=lowfuelthreshold or 10 + return self +end + --- Set folder where the airboss sound files are located **within you mission (miz) file**. -- The default path is "l10n/DEFAULT/" but sound files simply copied there will be removed by DCS the next time you save the mission. @@ -3002,11 +3038,16 @@ function AIRBOSS:_CheckAIStatus() -- Only AI! if flight.ai then - -- TODO: Check that aircraft can be refueled. + -- Get fuel amount in %. local fuel=flight.group:GetFuelMin()*100 - if self.lowfuelAI and fuel5. This would mean the player has not turned in correctly! @@ -9177,8 +9234,6 @@ function AIRBOSS:_GetZoneGroove(l, w) -- Stern coordinate. local st=self:_GetSternCoord() - -- TODO: optimize for Tarawa. shift port. - -- Zone points. local c1=st:Translate(self.carrierparam.totwidthstarboard, fbi-90) local c2=st:Translate(UTILS.NMToMeters(0.10), fbi-90):Translate(UTILS.NMToMeters(0.3), fbi) @@ -9252,7 +9307,7 @@ end -- @return Core.Zone#ZONE_RADIUS Arc in zone. function AIRBOSS:_GetZoneArcOut(case) - -- Radius = 1 NM. + -- Radius = 1.25 NM. local radius=UTILS.NMToMeters(1.25) -- Distance = 12 NM @@ -9276,7 +9331,7 @@ end -- @return Core.Zone#ZONE_RADIUS Arc in zone. function AIRBOSS:_GetZoneArcIn(case) - -- Radius = 1 NM. + -- Radius = 1.25 NM. local radius=UTILS.NMToMeters(1.25) -- Zone depends on Case recovery. @@ -9286,8 +9341,7 @@ function AIRBOSS:_GetZoneArcIn(case) local alpha=math.rad(self.holdingoffset) -- 14+x NM from carrier - -- TODO - local x=14/math.cos(alpha) + local x=14 --/math.cos(alpha) -- Distance = 14 NM local distance=UTILS.NMToMeters(x) @@ -9342,8 +9396,8 @@ function AIRBOSS:_GetZoneCorridor(case) -- Angle between radial and offset in rad. local alpha=math.rad(self.holdingoffset) - -- Distance shift ahead of carrier to allow for some space to bolter - local dx=2 + -- Distance shift ahead of carrier to allow for some space to bolter. + local dx=5 -- Width of the box in NM. local w=2 @@ -9382,9 +9436,12 @@ function AIRBOSS:_GetZoneCorridor(case) self:T3(string.format("FF Q = %.1f NM", Q)) local c={} - c[1]=self:GetCoordinate():Translate(-UTILS.NMToMeters(dx), radial) --Carrier coordinate translated 2 NM in direction of travel to allow for bolter space. + local cv=self:GetCoordinate() + c[1]=cv:Translate(-UTILS.NMToMeters(dx), radial) --Carrier coordinate translated 2 NM in direction of travel to allow for bolter space. - if math.abs(self.holdingoffset)>1 then + if math.abs(self.holdingoffset)>=5 then + + --[[ -- Complicated case with an angle. c[2]=c[1]:Translate( UTILS.NMToMeters(w2), radial-90) -- 1 Right of carrier. c[3]=c[2]:Translate( UTILS.NMToMeters(d+dx+w2), radial) -- 13 "south" @ 1 right @@ -9394,10 +9451,22 @@ function AIRBOSS:_GetZoneCorridor(case) c[9]=c[1]:Translate( UTILS.NMToMeters(w2), radial+90) -- 1 left of carrier. c[8]=c[9]:Translate( UTILS.NMToMeters(d+dx-w2), radial) -- 1 left and 11 behind of carrier. c[7]=c[8]:Translate( UTILS.NMToMeters(P), radial+90) + ]] + + c[2]=c[1]:Translate( UTILS.NMToMeters(w2), radial-90) -- 1 Right of carrier, dx ahead. + c[3]=c[2]:Translate( UTILS.NMToMeters(d+dx+w2), radial) -- 13 "south" @ 1 right + + c[4]=cv:Translate(UTILS.NMToMeters(15), offset):Translate(UTILS.NMToMeters(1), offset-90) + c[5]=cv:Translate(UTILS.NMToMeters(31), offset):Translate(UTILS.NMToMeters(1), offset-90) + c[6]=cv:Translate(UTILS.NMToMeters(31), offset):Translate(UTILS.NMToMeters(1), offset+90) + c[7]=cv:Translate(UTILS.NMToMeters(13), offset):Translate(UTILS.NMToMeters(1), offset+90) + c[8]=cv:Translate(UTILS.NMToMeters(11), radial):Translate(UTILS.NMToMeters(1), radial+90) + c[9]=c[1]:Translate(UTILS.NMToMeters(w2), radial+90) + -- Translate these points a bit for a smoother turn. - c[4]=c[4]:Translate(UTILS.NMToMeters(2), offset) - c[7]=c[7]:Translate(UTILS.NMToMeters(2), offset) + --c[4]=c[4]:Translate(UTILS.NMToMeters(2), offset) + --c[7]=c[7]:Translate(UTILS.NMToMeters(2), offset) else -- Easy case of a long box. c[2]=c[1]:Translate( UTILS.NMToMeters(w2), radial-90) @@ -9958,15 +10027,25 @@ end --- Get wind direction and speed at carrier position. -- @param #AIRBOSS self -- @param #number alt Altitude ASL in meters. Default 50 m. +-- @param #boolean magnetic Direction including magnetic declination. -- @return #number Direction the wind is blowing **from** in degrees. -- @return #number Wind speed in m/s. -function AIRBOSS:GetWind(alt) +function AIRBOSS:GetWind(alt, magnetic) -- Current position of the carrier local cv=self:GetCoordinate() -- Wind direction and speed. By default at 50 meters ASL. local Wdir, Wspeed=cv:GetWind(alt or 50) + + -- Include magnetic declination. + if magnetic then + Wdir=Wdir-self.magvar + -- Adjust negative values. + if Wdir<0 then + Wdir=Wdir+360 + end + end return Wdir, Wspeed end @@ -10279,7 +10358,6 @@ function AIRBOSS:_LSOadvice(playerData, glideslopeError, lineupError) local advice=0 -- Glideslope high/low calls. - --TODO: introduce GSE enumerator values. if glideslopeError>self.gle.HIGH then --1.5 then -- "You're high!" self:RadioTransmission(self.LSORadio, self.LSOCall.HIGH, true, nil, nil, true) @@ -10293,7 +10371,6 @@ function AIRBOSS:_LSOadvice(playerData, glideslopeError, lineupError) self:RadioTransmission(self.LSORadio, self.LSOCall.POWER, true, nil, nil, true) advice=advice+self.LSOCall.POWER.duration elseif glideslopeError/F1 Help/F2 Skill Level local _skillPath=missionCommands.addSubMenuForGroup(gid, "Skill Level", _helpPath) -- F10/Airboss//F1 Help/F2 Skill Level/ - missionCommands.addCommandForGroup(gid, "Flight Student", _skillPath, self._SetDifficulty, self, playername, AIRBOSS.Difficulty.EASY) -- F1 - missionCommands.addCommandForGroup(gid, "Naval Aviator", _skillPath, self._SetDifficulty, self, playername, AIRBOSS.Difficulty.NORMAL) -- F2 - missionCommands.addCommandForGroup(gid, "TOPGUN Graduate", _skillPath, self._SetDifficulty, self, playername, AIRBOSS.Difficulty.HARD) -- F3 - missionCommands.addCommandForGroup(gid, "Hints On/Off", _skillPath, self._SetHintsOnOff, self, playername) -- F4 + missionCommands.addCommandForGroup(gid, "Flight Student", _skillPath, self._SetDifficulty, self, _unitName, AIRBOSS.Difficulty.EASY) -- F1 + missionCommands.addCommandForGroup(gid, "Naval Aviator", _skillPath, self._SetDifficulty, self, _unitName, AIRBOSS.Difficulty.NORMAL) -- F2 + missionCommands.addCommandForGroup(gid, "TOPGUN Graduate", _skillPath, self._SetDifficulty, self, _unitName, AIRBOSS.Difficulty.HARD) -- F3 + missionCommands.addCommandForGroup(gid, "Hints On/Off", _skillPath, self._SetHintsOnOff, self, _unitName) -- F4 -- F10/Airboss//F1 Help/ missionCommands.addCommandForGroup(gid, "My Status", _helpPath, self._DisplayPlayerStatus, self, _unitName) -- F3 missionCommands.addCommandForGroup(gid, "Attitude Monitor", _helpPath, self._DisplayAttitude, self, _unitName) -- F4 @@ -14187,7 +14288,7 @@ function AIRBOSS:_RequestRefueling(_unitName) elseif self.tanker:IsReturning() then -- Tanker is RTB. - text="Tanker is RTB. Request denied!\nWait for the tanker to be back on station if you can." + text="negative, tanker is RTB. Request denied!\nWait for the tanker to be back on station if you can." end else @@ -14847,28 +14948,36 @@ end --- Set difficulty level. -- @param #AIRBOSS self --- @param #string playername Player name. +-- @param #string _unitname Name of the player unit. -- @param #AIRBOSS.Difficulty difficulty Difficulty level. -function AIRBOSS:_SetDifficulty(playername, difficulty) - self:T2({difficulty=difficulty, playername=playername}) +function AIRBOSS:_SetDifficulty(_unitname, difficulty) + self:T2({difficulty=difficulty, unitname=_unitname}) - local playerData=self.players[playername] --#AIRBOSS.PlayerData + -- Get player unit and player name. + local unit, playername = self:_GetPlayerUnitAndName(_unitname) - if playerData then - playerData.difficulty=difficulty - local text=string.format("your skill level is now: %s.", difficulty) - self:MessageToPlayer(playerData, text, nil, playerData.name, 5) - else - self:E(self.lid..string.format("ERROR: Could not get player data for player %s.", playername)) - end - - -- Set hints as well. - if playerData.difficulty==AIRBOSS.Difficulty.HARD then - playerData.showhints=false - else - playerData.showhints=true - end + -- Check if we have a player. + if unit and playername then + -- Player data. + local playerData=self.players[playername] --#AIRBOSS.PlayerData + + if playerData then + playerData.difficulty=difficulty + local text=string.format("your skill level is now: %s.", difficulty) + self:MessageToPlayer(playerData, text, nil, playerData.name, 5) + else + self:E(self.lid..string.format("ERROR: Could not get player data for player %s.", playername)) + end + + -- Set hints as well. + if playerData.difficulty==AIRBOSS.Difficulty.HARD then + playerData.showhints=false + else + playerData.showhints=true + end + + end end --- Turn player's aircraft attitude display on or off. @@ -14887,13 +14996,15 @@ function AIRBOSS:_SetHintsOnOff(_unitname) local playerData=self.players[playername] --#AIRBOSS.PlayerData if playerData then + + -- Invert hints. playerData.showhints=not playerData.showhints -- Inform player. local text="" if playerData.showhints==true then text=string.format("hints are now ON.") - elseif playerData.showhints==false then + else text=string.format("hints are now OFF.") end self:MessageToPlayer(playerData, text, nil, playerData.name, 5)