CT v0.1.6

This commit is contained in:
Frank 2018-11-04 01:14:47 +01:00
parent 27bf6069d7
commit 087ac992a2
3 changed files with 311 additions and 193 deletions

View File

@ -40,7 +40,9 @@
-- @field #CARRIERTRAINER.Checkpoint Wake Right behind the carrier. -- @field #CARRIERTRAINER.Checkpoint Wake Right behind the carrier.
-- @field #CARRIERTRAINER.Checkpoint Groove In the groove checkpoint. -- @field #CARRIERTRAINER.Checkpoint Groove In the groove checkpoint.
-- @field #CARRIERTRAINER.Checkpoint Trap Landing checkpoint. -- @field #CARRIERTRAINER.Checkpoint Trap Landing checkpoint.
-- @field -- @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.
-- @extends Core.Fsm#FSM -- @extends Core.Fsm#FSM
--- Practice Carrier Landings --- Practice Carrier Landings
@ -76,6 +78,9 @@ CARRIERTRAINER = {
Trap = {}, Trap = {},
TACAN = nil, TACAN = nil,
ICLS = nil, ICLS = nil,
rwyangle = -10,
sterndist =-100,
deckheight = 22,
} }
--- Aircraft types. --- Aircraft types.
@ -160,8 +165,7 @@ CARRIERTRAINER.Difficulty={
HARD="TOPGUN Graduate", HARD="TOPGUN Graduate",
} }
--- Groove position.
--- Groove data.
-- @type CARRIERTRAINER.GroovePos -- @type CARRIERTRAINER.GroovePos
-- @field #string X At the start. -- @field #string X At the start.
-- @field #string RB Roger ball. -- @field #string RB Roger ball.
@ -197,8 +201,10 @@ CARRIERTRAINER.GroovePos={
-- @field #string difficulty Difficulty level. -- @field #string difficulty Difficulty level.
-- @field #boolean inbigzone If true, player is in the big zone. -- @field #boolean inbigzone If true, player is in the big zone.
-- @field #boolean landed If true, player landed or attempted to land. -- @field #boolean landed If true, player landed or attempted to land.
-- @field #boolean bolter If true, LSO told player to bolter.
-- @field #boolean boltered If true, player boltered. -- @field #boolean boltered If true, player boltered.
-- @field #boolean waveoff If true, player was waved off. -- @field #boolean waveoff If true, player was waved off during final approach.
-- @field #boolean patternwo If true, playe was waved of during the pattern.
-- @field #number Tlso Last time the LSO gave an advice. -- @field #number Tlso Last time the LSO gave an advice.
-- @field #CARRIERTRAINER.GroovePos Groove data table with elemets of type @{#CARRIERTRAINER.GrooveData}. -- @field #CARRIERTRAINER.GroovePos Groove data table with elemets of type @{#CARRIERTRAINER.GrooveData}.
@ -225,19 +231,23 @@ CARRIERTRAINER.MenuF10={}
--- Carrier trainer class version. --- Carrier trainer class version.
-- @field #string version -- @field #string version
CARRIERTRAINER.version="0.1.5w" CARRIERTRAINER.version="0.1.6"
------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- TODO list -- TODO list
------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- TODO: Fix radio menu. -- TODO: Add scoring to radio menu.
-- TODO: Optimized debrief. -- TODO: Optimized debrief.
-- TODO: Add automatic grading. -- TODO: Add automatic grading.
-- TODO: Get board numbers. -- TODO: Get board numbers.
-- TODO: Get fuel state in pounds.
-- TODO: Add user functions. -- TODO: Add user functions.
-- TODO: Generalize parameters for other carriers and aircraft. -- TODO: Generalize parameters for other carriers.
-- TODO: Generalize parameters for other aircraft.
-- TODO: CASE II.
-- TODO: CASE III. -- TODO: CASE III.
-- DONE: Fix radio menu.
------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Constructor -- Constructor
@ -331,13 +341,6 @@ end
-- User functions -- User functions
------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
--- Set difficulty level.
-- @param #CARRIERTRAINER self
-- @param #CARRIERTRAINER.PlayerData playerData Player data.
-- @param #CARRIERTRAINER.Difficulty difficulty Difficulty level.
function CARRIERTRAINER:SetDifficulty(playerData, difficulty)
playerData.difficulty=difficulty
end
------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- FSM states -- FSM states
@ -371,8 +374,8 @@ function CARRIERTRAINER:onafterStatus(From, Event, To)
-- Check player status. -- Check player status.
self:_CheckPlayerStatus() self:_CheckPlayerStatus()
-- Call status again in one second. -- Call status again in 0.25 seconds.
self:__Status(-0.5) self:__Status(-0.25)
end end
--- On after Stop event. Unhandle events and stop status updates. --- On after Stop event. Unhandle events and stop status updates.
@ -403,7 +406,6 @@ function CARRIERTRAINER:_CheckPlayerStatus()
--self:_SendMessageToPlayer("current step "..self:_StepName(playerData.step),1,playerData) --self:_SendMessageToPlayer("current step "..self:_StepName(playerData.step),1,playerData)
--self:_DetailedPlayerStatus(playerData) --self:_DetailedPlayerStatus(playerData)
--self:_DetailedPlayerStatus(playerData)
if unit:IsInZone(self.giantZone) then if unit:IsInZone(self.giantZone) then
-- Check if player was previously not inside the zone. -- Check if player was previously not inside the zone.
@ -412,7 +414,7 @@ function CARRIERTRAINER:_CheckPlayerStatus()
local text=string.format("Welcome back, %s! TCN 74X, ICLS 1, BRC 354 (MAG HDG).\n", playerData.callsign) local text=string.format("Welcome back, %s! TCN 74X, ICLS 1, BRC 354 (MAG HDG).\n", playerData.callsign)
local heading=playerData.unit:GetCoordinate():HeadingTo(self.registerZone:GetCoordinate()) local heading=playerData.unit:GetCoordinate():HeadingTo(self.registerZone:GetCoordinate())
local distance=playerData.unit:GetCoordinate():Get2DDistance(self.registerZone:GetCoordinate()) local distance=playerData.unit:GetCoordinate():Get2DDistance(self.registerZone:GetCoordinate())
text=text..string.format("Fly heading %d for %.1f NM to begin your approach.", heading, distance) text=text..string.format("Fly heading %d for %.1f NM and turn to BRC.", heading, distance)
MESSAGE:New(text, 5):ToClient(playerData.client) MESSAGE:New(text, 5):ToClient(playerData.client)
end end
@ -420,7 +422,7 @@ function CARRIERTRAINER:_CheckPlayerStatus()
if playerData.step==0 and unit:InAir() then if playerData.step==0 and unit:InAir() then
self:_NewRound(playerData) self:_NewRound(playerData)
-- Jump to Groove for testing. -- Jump to Groove for testing.
--playerData.step=8 playerData.step=8
elseif playerData.step == 1 then elseif playerData.step == 1 then
self:_Start(playerData) self:_Start(playerData)
elseif playerData.step == 2 then elseif playerData.step == 2 then
@ -453,7 +455,7 @@ function CARRIERTRAINER:_CheckPlayerStatus()
else else
-- Unit not alive. -- Unit not alive.
--playerDatas[i] = nil self:E(self.lid.."WARNING: Player unit is not alive!")
end end
end end
end end
@ -504,8 +506,8 @@ function CARRIERTRAINER:OnEventBirth(EventData)
-- Add Menu commands. -- Add Menu commands.
self:_AddF10Commands(_unitName) self:_AddF10Commands(_unitName)
-- Init player. -- Init player data.
self.players[_playername]=self:_InitNewPlayer(_unitName) self.players[_playername]=self:_InitPlayer(_unitName)
-- Test -- Test
--CARRIERTRAINER.LSOcall.HIGHL:ToGroup(_group) --CARRIERTRAINER.LSOcall.HIGHL:ToGroup(_group)
@ -539,9 +541,10 @@ function CARRIERTRAINER:OnEventLand(EventData)
self:T(self.lid..text) self:T(self.lid..text)
MESSAGE:New(text, 5):ToAllIf(self.Debug) MESSAGE:New(text, 5):ToAllIf(self.Debug)
-- Check if we caught a wire after one second. -- Player data.
-- TODO: test this!
local playerData=self.players[_playername] --#CARRIERTRAINER.PlayerData local playerData=self.players[_playername] --#CARRIERTRAINER.PlayerData
-- Coordinate at landing event
local coord=playerData.unit:GetCoordinate() local coord=playerData.unit:GetCoordinate()
-- We did land. -- We did land.
@ -555,11 +558,17 @@ function CARRIERTRAINER:OnEventLand(EventData)
end end
end end
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- CARRIER TRAINING functions
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
--- Initialize player data. --- Initialize player data.
-- @param #CARRIERTRAINER self -- @param #CARRIERTRAINER self
-- @param #string unitname Name of the player unit. -- @param #string unitname Name of the player unit.
-- @return #CARRIERTRAINER.PlayerData Player data. -- @return #CARRIERTRAINER.PlayerData Player data.
function CARRIERTRAINER:_InitNewPlayer(unitname) function CARRIERTRAINER:_InitPlayer(unitname)
local playerData={} --#CARRIERTRAINER.PlayerData local playerData={} --#CARRIERTRAINER.PlayerData
@ -601,14 +610,11 @@ function CARRIERTRAINER:_InitNewRound(playerData)
playerData.boltered=false playerData.boltered=false
playerData.landed=false playerData.landed=false
playerData.waveoff=false playerData.waveoff=false
playerData.patternwo=false
playerData.Tlso=timer.getTime() playerData.Tlso=timer.getTime()
return playerData return playerData
end end
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- CARRIER TRAINING functions
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
--- Initialize player data. --- Initialize player data.
-- @param #CARRIERTRAINER self -- @param #CARRIERTRAINER self
-- @param #CARRIERTRAINER.PlayerData playerData Player data. -- @param #CARRIERTRAINER.PlayerData playerData Player data.
@ -743,7 +749,7 @@ function CARRIERTRAINER:_CheckForLongDownwind(playerData)
-- Get relative heading. -- Get relative heading.
local relhead=self:_GetRelativeHeading(playerData.unit) local relhead=self:_GetRelativeHeading(playerData.unit)
-- One NM from carrier is way too far. -- One NM from carrier is too far.
local limit=-UTILS.NMToMeters(1) local limit=-UTILS.NMToMeters(1)
local text=string.format("Long groove check: X=%d, relhead=%.1f", X, relhead) local text=string.format("Long groove check: X=%d, relhead=%.1f", X, relhead)
@ -794,11 +800,6 @@ function CARRIERTRAINER:_Abeam(playerData)
-- Check nest step threshold. -- Check nest step threshold.
if self:_CheckLimits(X, Z, self.Abeam) then if self:_CheckLimits(X, Z, self.Abeam) then
-- Checks:
-- AoA
-- Altitude
-- Distance to carrier.
-- Get AoA and altitude. -- Get AoA and altitude.
local aoa = playerData.unit:GetAoA() local aoa = playerData.unit:GetAoA()
local alt = playerData.unit:GetAltitude() local alt = playerData.unit:GetAltitude()
@ -821,7 +822,7 @@ function CARRIERTRAINER:_Abeam(playerData)
-- Add to debrief. -- Add to debrief.
self:_AddToSummary(playerData, "Abeam Position", hintFull) self:_AddToSummary(playerData, "Abeam Position", hintFull)
-- Proceed to next step. -- Next step: ninety.
playerData.step = 6 playerData.step = 6
end end
end end
@ -894,6 +895,7 @@ function CARRIERTRAINER:_Wake(playerData)
-- Right behind the wake of the carrier dZ>0. -- Right behind the wake of the carrier dZ>0.
if self:_CheckLimits(X, Z, self.Wake) then if self:_CheckLimits(X, Z, self.Wake) then
-- Get player altitude and AoA.
local alt=playerData.unit:GetAltitude() local alt=playerData.unit:GetAltitude()
local aoa=playerData.unit:GetAoA() local aoa=playerData.unit:GetAoA()
@ -931,16 +933,16 @@ function CARRIERTRAINER:_Groove(playerData)
return return
end end
-- 0 means player is on BRC course but runway heading is -10 degrees.
local heading=self:_GetRelativeHeading(playerData.unit)-10
local calltheball=UTILS.NMToMeters(0.75) local calltheball=UTILS.NMToMeters(0.75)
if rho<=calltheball then if rho<=calltheball then
-- Get player altitude and AoA.
local alt = playerData.unit:GetAltitude() local alt = playerData.unit:GetAltitude()
local aoa = playerData.unit:GetAoA() local aoa = playerData.unit:GetAoA()
self:_SendMessageToPlayer("Call the ball.", 8, playerData) self:_SendMessageToPlayer("Call the ball.", 8, playerData)
CARRIERTRAINER.LSOcall.CALLTHEBALL:ToGroup(playerData.unit:GetGroup()) CARRIERTRAINER.LSOcall.CALLTHEBALL:ToGroup(playerData.unit:GetGroup())
@ -966,11 +968,13 @@ function CARRIERTRAINER:_Groove(playerData)
groovedata.LUE=self:_Lineup(playerData)-10 groovedata.LUE=self:_Lineup(playerData)-10
groovedata.Step=playerData.step groovedata.Step=playerData.step
-- Init groove table.
playerData.Groove={} playerData.Groove={}
playerData.Groove.X=grovedata
--table.insert(playerData.Groove, groovedata)
-- Next step.
playerData.Groove.X=groovedata
-- Next step: roger ball.
playerData.step=90 playerData.step=90
end end
@ -997,20 +1001,17 @@ function CARRIERTRAINER:_CallTheBall(playerData)
return return
end end
-- Runway is at an angle of -10 degrees wrt to carrier X direction.
-- TODO: make this carrier dependent
local rwyangle=-10
local deckheight=22
local tailpos=-100
-- Lineup with runway centerline. -- Lineup with runway centerline.
local lineup=self:_Lineup(playerData) local lineup=self:_Lineup(playerData)
local lineupError=lineup-rwyangle local lineupError=lineup-self.rwyangle
-- Glide slope. -- Glide slope.
local glideslope=self:_Glideslope(playerData) local glideslope=self:_Glideslope(playerData)
local glideslopeError=glideslope-3.5 --TODO: maybe 3.0? local glideslopeError=glideslope-3.5 --TODO: maybe 3.0?
-- Get AoA.
local AoA=playerData.unit:GetAoA()
-- Ranges in the groove. -- Ranges in the groove.
local RRB=UTILS.NMToMeters(0.500) -- Roger Ball! call. local RRB=UTILS.NMToMeters(0.500) -- Roger Ball! call.
local RIM=UTILS.NMToMeters(0.375) -- In the Middle 0.75/2. local RIM=UTILS.NMToMeters(0.375) -- In the Middle 0.75/2.
@ -1020,11 +1021,10 @@ function CARRIERTRAINER:_CallTheBall(playerData)
-- Data -- Data
local groovedata={} --#CARRIERTRAINER.GrooveData local groovedata={} --#CARRIERTRAINER.GrooveData
groovedata.Alt=alt groovedata.Alt=alt
groovedata.AoA=playerData.unit:GetAoA() groovedata.AoA=AoA
groovedata.GSE=glideslopeError groovedata.GSE=glideslopeError
groovedata.LUE=lineupError groovedata.LUE=lineupError
groovedata.Step=playerData.step groovedata.Step=playerData.step
--table.insert(playerData.Groove, groovedata)
if rho<=RRB and playerData.step==90 then if rho<=RRB and playerData.step==90 then
@ -1059,8 +1059,23 @@ function CARRIERTRAINER:_CallTheBall(playerData)
-- Store data. -- Store data.
playerData.Groove.IC=groovedata playerData.Groove.IC=groovedata
-- Check if player should wave off.
local waveoff=self:_CheckWaveOff(glideslopeError, lineupError, AoA)
-- Let's see..
if waveoff then
-- Wave off player.
self:_SendMessageToPlayer(CARRIERTRAINER.LSOcall.WAVEOFFT, 10, playerData)
CARRIERTRAINER.LSOcall.WAVEOFF:ToGroup(playerData.unit:GetGroup())
-- Next step: debrief.
playerData.step=999
else
-- Next step: at the ramp. -- Next step: at the ramp.
playerData.step=93 playerData.step=93
end
elseif rho<=RAR and playerData.step==93 then elseif rho<=RAR and playerData.step==93 then
@ -1084,11 +1099,13 @@ function CARRIERTRAINER:_CallTheBall(playerData)
self:_LSOcall(playerData, glideslopeError, lineupError) self:_LSOcall(playerData, glideslopeError, lineupError)
elseif X > 150 then elseif X>0 then
local wire = 0 local wire = 0
local hint = "" local hint = ""
local score = 0 local score = 0
if playerData.landed then if playerData.landed then
hint = "You boltered." hint = "You boltered."
else else
@ -1108,6 +1125,31 @@ function CARRIERTRAINER:_CallTheBall(playerData)
end end
end end
--- LSO check if player needs to wave off.
-- @param #CARRIERTRAINER self
-- @param #number glideslopeError Glide slope error in degrees.
-- @param #number lineupError Line up error in degrees.
-- @param #number AoA Angle of attack of player aircraft.
-- @return #boolean If true, player should wave off!
function CARRIERTRAINER:_CheckWaveOff(glideslopeError, lineupError, AoA)
local waveoff=false
if math.abs(glideslopeError)>3 then
waveoff=true
end
if math.abs(lineupError)>3 then
waveoff=true
end
if AoA<6.9 or AoA>9.3 then
waveoff=true
end
return waveoff
end
--- Get name of the current pattern step. --- Get name of the current pattern step.
-- @param #CARRIERTRAINER self -- @param #CARRIERTRAINER self
-- @param #number step Step -- @param #number step Step
@ -1140,10 +1182,6 @@ function CARRIERTRAINER:_Trapped(playerData, pos)
-- Get distances between carrier and player unit (parallel and perpendicular to direction of movement of carrier) -- Get distances between carrier and player unit (parallel and perpendicular to direction of movement of carrier)
local X, Z, rho, phi = self:_GetDistances(pos) local X, Z, rho, phi = self:_GetDistances(pos)
-- Get velocities.
local playerVelocity = playerData.unit:GetVelocityKMH()
local carrierVelocity = self.carrier:GetVelocityKMH()
if playerData.unit:InAir()==false then if playerData.unit:InAir()==false then
-- Seems we have successfully landed. -- Seems we have successfully landed.
@ -1177,6 +1215,7 @@ function CARRIERTRAINER:_Trapped(playerData, pos)
else else
--Boltered! --Boltered!
playerData.boltered=true
end end
end end
@ -1194,22 +1233,22 @@ function CARRIERTRAINER:_LSOcall(playerData, glideslopeError, lineupError)
-- Glideslope high/low calls. -- Glideslope high/low calls.
if glideslopeError>1 then if glideslopeError>1 then
text="You're too high! Throttles back!" text="You're high!"
CARRIERTRAINER.LSOcall.HIGHL:ToGroup(player) CARRIERTRAINER.LSOcall.HIGHL:ToGroup(player)
elseif glideslopeError>0.5 then elseif glideslopeError>0.5 then
text="You're slightly high. Decrease power." text="You're a little high."
CARRIERTRAINER.LSOcall.HIGHS:ToGroup(player) CARRIERTRAINER.LSOcall.HIGHS:ToGroup(player)
elseif glideslopeError<-1.0 then elseif glideslopeError<-1.0 then
text="Power! You're way too low." text="Power!"
CARRIERTRAINER.LSOcall.POWERL:ToGroup(player) CARRIERTRAINER.LSOcall.POWERL:ToGroup(player)
elseif glideslopeError<-0.5 then elseif glideslopeError<-0.5 then
text="You're slightly low. Increase power." text="You're a little low."
CARRIERTRAINER.LSOcall.POWERS:ToGroup(player) CARRIERTRAINER.LSOcall.POWERS:ToGroup(player)
else else
text="Good altitude." text="Good altitude."
end end
text=text..string.format(" Glideslope Error = %.2f %%", glideslopeError) text=text..string.format(" Glideslope Error = %.2f°", glideslopeError)
text=text.."\n" text=text.."\n"
local delay=0 local delay=0
@ -1235,7 +1274,7 @@ function CARRIERTRAINER:_LSOcall(playerData, glideslopeError, lineupError)
text=text.."Good lineup." text=text.."Good lineup."
end end
text=text..string.format(" Lineup Error = %.1f %%\n", lineupError) text=text..string.format(" Lineup Error = %.1f°\n", lineupError)
-- Get AoA. -- Get AoA.
local aoa=playerData.unit:GetAoA() local aoa=playerData.unit:GetAoA()
@ -1243,23 +1282,24 @@ function CARRIERTRAINER:_LSOcall(playerData, glideslopeError, lineupError)
if aoa>=9.3 then if aoa>=9.3 then
text=text.."Your're slow!" text=text.."Your're slow!"
elseif aoa>=8.8 and aoa<9.3 then elseif aoa>=8.8 and aoa<9.3 then
text=text.."Your're slightly slow." text=text.."Your're a little slow."
elseif aoa>=7.4 and aoa<8.8 then elseif aoa>=7.4 and aoa<8.8 then
text=text.."You're on speed." text=text.."You're on speed."
elseif aoa>=6.9 and aoa<7.4 then elseif aoa>=6.9 and aoa<7.4 then
text=text.."You're slightly fast." text=text.."You're a little fast."
elseif aoa>=0 and aoa<6.9 then elseif aoa>=0 and aoa<6.9 then
text=text.."You're fast!" text=text.."You're fast!"
else else
text=text.."Unknown AoA state." text=text.."Unknown AoA state."
end end
text=text..string.format(" AoA = %.1f", aoa)
-- LSO Message to player. -- LSO Message to player.
self:_SendMessageToPlayer(text, 8, playerData, true) self:_SendMessageToPlayer(text, 5, playerData, false)
-- Set last time. -- Set last time.
playerData.Tlso=timer.getTime() playerData.Tlso=timer.getTime()
end end
--- Get glide slope of aircraft. --- Get glide slope of aircraft.
@ -1268,16 +1308,12 @@ end
-- @return #number Glide slope angle in degrees measured from the -- @return #number Glide slope angle in degrees measured from the
function CARRIERTRAINER:_Glideslope(playerData) function CARRIERTRAINER:_Glideslope(playerData)
-- Carrier parameters.
local deckheight=22
local tailpos=-100
-- Get distances between carrier and player unit (parallel and perpendicular to direction of movement of carrier) -- Get distances between carrier and player unit (parallel and perpendicular to direction of movement of carrier)
local X, Z, rho, phi = self:_GetDistances(playerData.unit) local X, Z, rho, phi = self:_GetDistances(playerData.unit)
-- Glideslope. Wee need to correct for the height of the deck. The ideal glide slope is 3.5 degrees. -- Glideslope. Wee need to correct for the height of the deck. The ideal glide slope is 3.5 degrees.
local h=playerData.unit:GetAltitude()-deckheight local h=playerData.unit:GetAltitude()-self.deckheight
local x=math.abs(X-tailpos) local x=math.abs(X-self.sterndist)
local glideslope=math.atan(h/x) local glideslope=math.atan(h/x)
return math.deg(glideslope) return math.deg(glideslope)
@ -1290,18 +1326,11 @@ end
-- @return #number Distance from carrier tail to player aircraft in meters. -- @return #number Distance from carrier tail to player aircraft in meters.
function CARRIERTRAINER:_Lineup(playerData) function CARRIERTRAINER:_Lineup(playerData)
-- Runway is at an angle of -10 degrees wrt to carrier X direction.
-- TODO: make this carrier dependent
local rwyangle=-10
local deckheight=22
local tailpos=-100
-- Get distances between carrier and player unit (parallel and perpendicular to direction of movement of carrier) -- Get distances between carrier and player unit (parallel and perpendicular to direction of movement of carrier)
local X, Z, rho, phi = self:_GetDistances(playerData.unit) local X, Z, rho, phi = self:_GetDistances(playerData.unit)
-- Position at the end of the deck. From there we calculate the angle. -- Position at the end of the deck. From there we calculate the angle.
-- TODO: Check exact number and make carrier dependent. local b={x=self.sterndist, z=0}
local b={x=tailpos, z=0}
-- Position of the aircraft wrt carrier coordinates. -- Position of the aircraft wrt carrier coordinates.
local a={x=X, z=Z} local a={x=X, z=Z}
@ -1456,11 +1485,10 @@ end
-- @param #CARRIERTRAINER self -- @param #CARRIERTRAINER self
-- @param #number X X distance player to carrier. -- @param #number X X distance player to carrier.
-- @param #number Z Z distance player to carrier. -- @param #number Z Z distance player to carrier.
-- @param #table pos Position data limits. -- @param #CARRIERTRAINER.Checkpoint pos Position data limits.
-- @return #boolean If true, approach should be aborted. -- @return #boolean If true, approach should be aborted.
function CARRIERTRAINER:_CheckAbort(X, Z, check) function CARRIERTRAINER:_CheckAbort(X, Z, pos)
--[[
local abort=false local abort=false
if pos.Xmin and X<pos.Xmin then if pos.Xmin and X<pos.Xmin then
abort=true abort=true
@ -1471,8 +1499,8 @@ function CARRIERTRAINER:_CheckAbort(X, Z, check)
elseif pos.Zmax and Z>pos.Zmax then elseif pos.Zmax and Z>pos.Zmax then
abort=true abort=true
end end
]]
--[[
-- Abort conditions. -- Abort conditions.
local abortXmin=check.Xmin and (check.Xmin<0 and X<=check.Xmin or check.Xmin>=0 and X>=check.Xmin) local abortXmin=check.Xmin and (check.Xmin<0 and X<=check.Xmin or check.Xmin>=0 and X>=check.Xmin)
local abortXmax=check.Xmax and (check.Xmax<0 and X>=check.Xmax or check.Xmax>=0 and X<=check.Xmax) local abortXmax=check.Xmax and (check.Xmax<0 and X>=check.Xmax or check.Xmax>=0 and X<=check.Xmax)
@ -1481,6 +1509,7 @@ function CARRIERTRAINER:_CheckAbort(X, Z, check)
-- Check if any of the conditions are met. -- Check if any of the conditions are met.
local abort=abortXmin or abortXmax or abortZmin or abortZmax local abort=abortXmin or abortXmax or abortZmin or abortZmax
]]
return abort return abort
end end
@ -1543,8 +1572,8 @@ function CARRIERTRAINER:_AbortPattern(playerData, X, Z, posData)
-- Add to debrief. -- Add to debrief.
self:_AddToSummary(playerData, "Abort", "Approach aborted.") self:_AddToSummary(playerData, "Abort", "Approach aborted.")
-- -- Pattern wave off!
playerData.waveoff=true playerData.patternwo=true
--TODO: set score and grade. --TODO: set score and grade.
@ -1567,13 +1596,6 @@ function CARRIERTRAINER:_DetailedPlayerStatus(playerData)
local dist=playerData.unit:GetCoordinate():Get2DDistance(self.carrier:GetCoordinate()) local dist=playerData.unit:GetCoordinate():Get2DDistance(self.carrier:GetCoordinate())
local dx,dz,rho,phi=self:_GetDistances(unit) local dx,dz,rho,phi=self:_GetDistances(unit)
-- Player and carrier position vector.
local playerPosition = playerData.unit:GetVec3()
local carrierPosition = self.carrier:GetVec3()
local diffZ = playerPosition.z - carrierPosition.z
local diffX = playerPosition.x - carrierPosition.x
local heading=unit:GetCoordinate():HeadingTo(self.startZone:GetCoordinate()) local heading=unit:GetCoordinate():HeadingTo(self.startZone:GetCoordinate())
local wind=unit:GetCoordinate():GetWindWithTurbulenceVec3() local wind=unit:GetCoordinate():GetWindWithTurbulenceVec3()
@ -1581,16 +1603,13 @@ function CARRIERTRAINER:_DetailedPlayerStatus(playerData)
local relhead=self:_GetRelativeHeading(playerData.unit) local relhead=self:_GetRelativeHeading(playerData.unit)
local text=string.format("%s, current AoA=%.1f\n", playerData.callsign, aoa) local text=string.format("%s, current step: %.1f\n", playerData.callsign, self:_StepName(playerData.step))
text=text..string.format("velo x=%.1f y=%.1f z=%.1f\n", velo.x, velo.y, velo.z) text=text..string.format("AoA=%.1f | Vx=%.1f Vy=%.1f Vz=%.1f\n", aoa, velo.x, velo.y, velo.z)
text=text..string.format("wind x=%.1f y=%.1f z=%.1f\n", wind.x, wind.y, wind.z) text=text..string.format("Wind Vx=%.1f Vy=%.1f Vz=%.1f\n", wind.x, wind.y, wind.z)
text=text..string.format("pitch=%.1f | roll=%.1f | yaw=%.1f | climb=%.1f\n", pitch, roll, yaw, unit:GetClimbAnge()) text=text..string.format("pitch=%.1f | roll=%.1f | yaw=%.1f | climb=%.1f\n", pitch, roll, yaw, unit:GetClimbAngle())
text=text..string.format("relheading=%.1f degrees\n", relhead) text=text..string.format("relheading=%.1f degrees\n", relhead)
text=text..string.format("Distance: X=%d m Z=%d m", dx, dz)
text=text..string.format("rho=%.1f m phi=%.1f degrees\n", rho,phi) text=text..string.format("rho=%.1f m phi=%.1f degrees\n", rho,phi)
--text=text..string.format("current step = %d %s\n", playerData.step, self:_StepName(playerData.step))
--text=text..string.format("Carrier distance: d=%d m\n", dist)
--text=text..string.format("Carrier distance: x=%d m z=%d m sum=%d (old)\n", diffX, diffZ, math.abs(diffX)+math.abs(diffZ))
--text=text..string.format("Carrier distance: x=%d m z=%d m sum=%d (new)", dx, dz, math.abs(dz)+math.abs(dx))
MESSAGE:New(text, 1, nil , true):ToClient(playerData.client) MESSAGE:New(text, 1, nil , true):ToClient(playerData.client)
end end
@ -1599,6 +1618,7 @@ end
-- @param #CARRIERTRAINER self -- @param #CARRIERTRAINER self
function CARRIERTRAINER:_InitStennis() function CARRIERTRAINER:_InitStennis()
-- Upwind leg -- Upwind leg
self.Upwind.name="Upwind" self.Upwind.name="Upwind"
self.Upwind.Xmin=-4000 -- TODO Should be withing 4 km behind carrier. Why? self.Upwind.Xmin=-4000 -- TODO Should be withing 4 km behind carrier. Why?
@ -1616,7 +1636,7 @@ function CARRIERTRAINER:_InitStennis()
-- Early break -- Early break
self.BreakEarly.name="Early Break" self.BreakEarly.name="Early Break"
self.BreakEarly.Xmin=-500 self.BreakEarly.Xmin=-500
self.BreakEarly.Xmax=nil self.BreakEarly.Xmax=4000
self.BreakEarly.Zmin=-3700 self.BreakEarly.Zmin=-3700
self.BreakEarly.Zmax=1500 self.BreakEarly.Zmax=1500
self.BreakEarly.LimitXmin=0 self.BreakEarly.LimitXmin=0
@ -1630,7 +1650,7 @@ function CARRIERTRAINER:_InitStennis()
-- Late break -- Late break
self.BreakLate.name="Late Break" self.BreakLate.name="Late Break"
self.BreakLate.Xmin=-500 self.BreakLate.Xmin=-500
self.BreakLate.Xmax=nil self.BreakLate.Xmax=4000
self.BreakLate.Zmin=-3700 self.BreakLate.Zmin=-3700
self.BreakLate.Zmax=1500 self.BreakLate.Zmax=1500
self.BreakLate.LimitXmin=0 self.BreakLate.LimitXmin=0
@ -1864,7 +1884,7 @@ function CARRIERTRAINER:_DistanceCheck(playerData, checkpoint, distance)
hint = string.format("You're slightly too far from the boat.") hint = string.format("You're slightly too far from the boat.")
else else
score = 0 score = 0
hint = string.format("perfect distance to the boat.") hint = string.format("Perfect distance to the boat.")
end end
hint=hint..string.format(" Distance %.1f NM = %d%% deviation from %.1f NM optimal distance.",UTILS.MetersToNM(distance), _error, UTILS.MetersToNM(checkpoint.Distance)) hint=hint..string.format(" Distance %.1f NM = %d%% deviation from %.1f NM optimal distance.",UTILS.MetersToNM(distance), _error, UTILS.MetersToNM(checkpoint.Distance))
@ -2051,34 +2071,33 @@ function CARRIERTRAINER:_AddF10Commands(_unitName)
CARRIERTRAINER.MenuF10[_gid]=missionCommands.addSubMenuForGroup(_gid, "Carrier Trainer") CARRIERTRAINER.MenuF10[_gid]=missionCommands.addSubMenuForGroup(_gid, "Carrier Trainer")
end end
-- Player Data.
local playerData=self.players[playername] local playerData=self.players[playername]
-- F10/Carrier Trainer/<Carrier Name> -- F10/Carrier Trainer/<Carrier Name>
local _trainPath = missionCommands.addSubMenuForGroup(_gid, self.alias, CARRIERTRAINER.MenuF10[_gid]) local _trainPath = missionCommands.addSubMenuForGroup(_gid, self.alias, CARRIERTRAINER.MenuF10[_gid])
-- F10/Carrier Trainer/<Carrier Name>/Results
--local _statsPath = missionCommands.addSubMenuForGroup(_gid, "Results", _trainPath)
-- F10/Carrier Trainer/<Carrier Name>/My Settings
local _settingsPath = missionCommands.addSubMenuForGroup(_gid, "My Settings", _trainPath)
-- F10/Carrier Trainer/<Carrier Name>/My Settings/Difficulty
local _difficulPath = missionCommands.addSubMenuForGroup(_gid, "Difficulty", _settingsPath)
-- F10/Carrier Trainer/<Carrier Name>/Carrier Info
local _infoPath = missionCommands.addSubMenuForGroup(_gid, "Carrier Info", _trainPath)
-- F10/Carrier Trainer/<Carrier Name>/Stats/ -- F10/Carrier Trainer/<Carrier Name>/Results
local _statsPath = missionCommands.addSubMenuForGroup(_gid, "Results", _trainPath)
-- F10/Carrier Trainer/<Carrier Name>/My Settings/Difficulty
local _difficulPath = missionCommands.addSubMenuForGroup(_gid, "Difficulty", _trainPath)
-- F10/Carrier Trainer/<Carrier Name>/Results/
-- TODO: Add result functions.
--missionCommands.addCommandForGroup(_gid, "All Results", _statsPath, self._DisplayStrafePitResults, self, _unitName) --missionCommands.addCommandForGroup(_gid, "All Results", _statsPath, self._DisplayStrafePitResults, self, _unitName)
--missionCommands.addCommandForGroup(_gid, "My Results", _statsPath, self._DisplayBombingResults, self, _unitName) --missionCommands.addCommandForGroup(_gid, "My Results", _statsPath, self._DisplayBombingResults, self, _unitName)
--missionCommands.addCommandForGroup(_gid, "Reset All Results", _statsPath, self._ResetRangeStats, self, _unitName) --missionCommands.addCommandForGroup(_gid, "(Clear ALL Results)", _statsPath, self._ResetRangeStats, self, _unitName)
-- F10/Carrier Trainer/<Carrier Name>/My Settings/
--missionCommands.addCommandForGroup(_gid, "Smoke Delay On/Off", _settingsPath, self._SmokeBombDelayOnOff, self, _unitName) -- F10/Carrier Trainer/<Carrier Name>/Difficulty
--missionCommands.addCommandForGroup(_gid, "Smoke Impact On/Off", _settingsPath, self._SmokeBombImpactOnOff, self, _unitName) missionCommands.addCommandForGroup(_gid, "Flight Student", _difficulPath, self._SetDifficulty, self, playername, CARRIERTRAINER.Difficulty.EASY)
--missionCommands.addCommandForGroup(_gid, "Flare Hits On/Off", _settingsPath, self._FlareDirectHitsOnOff, self, _unitName) missionCommands.addCommandForGroup(_gid, "Naval Aviator", _difficulPath, self._SetDifficulty, self, playername, CARRIERTRAINER.Difficulty.NORMAL)
-- F10/Carrier Trainer/<Carrier Name>/My Settings/Difficulty missionCommands.addCommandForGroup(_gid, "TOPGUN Graduate", _difficulPath, self._SetDifficulty, self, playername, CARRIERTRAINER.Difficulty.HARD)
missionCommands.addCommandForGroup(_gid, "Flight Student", _difficulPath, self.SetDifficulty, self, playerData, CARRIERTRAINER.Difficulty.EASY)
missionCommands.addCommandForGroup(_gid, "Naval Aviator", _difficulPath, self.SetDifficulty, self, playerData, CARRIERTRAINER.Difficulty.NORMAL) -- F10/Carrier Trainer/<Carrier Name>/
missionCommands.addCommandForGroup(_gid, "TOPGUN Graduate", _difficulPath, self.SetDifficulty, self, playerData, CARRIERTRAINER.Difficulty.HARD) missionCommands.addCommandForGroup(_gid, "Carrier Info", _trainPath, self._DisplayCarrierInfo, self, _unitName)
-- F10/Carrier Trainer/<Carrier Name>/Carrier Info/ missionCommands.addCommandForGroup(_gid, "Weather Report", _trainPath, self._DisplayCarrierWeather, self, _unitName)
missionCommands.addCommandForGroup(_gid, "Carrier Info", _infoPath, self._DisplayCarrierInfo, self, _unitName) --TODO: Flare carrier.
missionCommands.addCommandForGroup(_gid, "Weather Report", _infoPath, self._DisplayCarrierWeather, self, _unitName)
end end
else else
self:T(self.lid.."Could not find group or group ID in AddF10Menu() function. Unit name: ".._unitName) self:T(self.lid.."Could not find group or group ID in AddF10Menu() function. Unit name: ".._unitName)
@ -2089,11 +2108,87 @@ function CARRIERTRAINER:_AddF10Commands(_unitName)
end end
--- Display top 10 player scores.
-- @param #CARRIERTRAINER self
-- @param #string _unitName Name fo the player unit.
function CARRIERTRAINER:_DisplayScoreBoard(_unitName)
self:F(_unitName)
-- Get player unit and name.
local _unit, _playername = self:_GetPlayerUnitAndName(_unitName)
-- Check if we have a unit which is a player.
if _unit and _playername then
-- Results table.
local _playerResults={}
-- Message text.
local _message = string.format("Greenie Board:\n")
-- Loop over player results.
for _playerName,_results in pairs(self.strafePlayerResults) do
-- Get the best result of the player.
local _best=nil
for _,_result in pairs(_results) do
if _best==nil or _result.hits > _best.hits then
_best = _result
end
end
-- Add best result to table.
if _best ~= nil then
local text=string.format("%s: Hits %i - %s - %s", _playerName, _best.hits, _best.zone.name, _best.text)
table.insert(_playerResults,{msg = text, hits = _best.hits})
end
end
--Sort list!
local _sort = function( a,b ) return a.hits > b.hits end
table.sort(_playerResults,_sort)
-- Add top 10 results.
for _i = 1, math.min(#_playerResults, self.ndisplayresult) do
_message = _message..string.format("\n[%d] %s", _i, _playerResults[_i].msg)
end
-- In case there are no scores yet.
if #_playerResults<1 then
_message = _message.."No player scored yet."
end
-- Send message.
self:_DisplayMessageToGroup(_unit, _message, nil, true)
end
end
--- Set difficulty level.
-- @param #CARRIERTRAINER self
-- @param #string playernaame Player name.
-- @param #CARRIERTRAINER.Difficulty difficulty Difficulty level.
function CARRIERTRAINER:_SetDifficulty(playername, difficulty)
self:E({difficulty=difficulty, playername=playername})
local playerData=self.players[playername] --CARRIERTRAINER.PlayerData
if playerData then
playerData.difficulty=difficulty
local text=string.format("Your difficulty level is now: %s.", difficulty)
self:_SendMessageToPlayer(text, 5, playerData)
else
self:E(self.lid..string.format("ERROR: Could not get player data for player %s.", playername))
end
end
--- Report information about carrier. --- Report information about carrier.
-- @param #CARRIERTRAINER self -- @param #CARRIERTRAINER self
-- @param #string _unitname Name of the player unit. -- @param #string _unitname Name of the player unit.
function CARRIERTRAINER:_DisplayCarrierInfo(_unitname) function CARRIERTRAINER:_DisplayCarrierInfo(_unitname)
self:F(_unitname) self:E(_unitname)
-- Get player unit and player name. -- Get player unit and player name.
local unit, playername = self:_GetPlayerUnitAndName(_unitname) local unit, playername = self:_GetPlayerUnitAndName(_unitname)
@ -2101,21 +2196,22 @@ function CARRIERTRAINER:_DisplayCarrierInfo(_unitname)
-- Check if we have a player. -- Check if we have a player.
if unit and playername then if unit and playername then
-- Player data.
local playerData=self.players[playername] --#CARRIERTRAINER.PlayerData
if playerData then
-- Message text. -- Message text.
local text=string.format("%s info:\n", self.alias) local text=string.format("%s info:\n", self.alias)
-- Current coordinates. -- Current coordinates.
local coord=self.carrier:GetCoordinate() local coord=self.carrier:GetCoordinate()
local playerData=self.players[playername] --#CARRIERTRAINER.PlayerData -- Carrier speed and heading.
local carrierheading=self.carrier:GetHeading() local carrierheading=self.carrier:GetHeading()
local carrierspeed=UTILS.MpsToKnots(self.carrier:GetVelocity()) local carrierspeed=UTILS.MpsToKnots(self.carrier:GetVelocityMPS())
text=text..string.format("BRC %d\n", carrierheading)
text=text..string.format("Speed %d kts\n", carrierspeed)
-- Tacan/ICLS.
local tacan="unknown" local tacan="unknown"
local icls="unknown" local icls="unknown"
if self.TACAN~=nil then if self.TACAN~=nil then
@ -2125,11 +2221,18 @@ function CARRIERTRAINER:_DisplayCarrierInfo(_unitname)
icls=tostring(self.ICLS) icls=tostring(self.ICLS)
end end
text=text..string.format("TACAN Channel %s", tacan) -- Message text
text=text..string.format("BRC %d°\n", carrierheading)
text=text..string.format("Speed %d kts\n", carrierspeed)
text=text..string.format("TACAN Channel %s\n", tacan)
text=text..string.format("ICLS Channel %s", icls) text=text..string.format("ICLS Channel %s", icls)
-- Send message.
self:_SendMessageToPlayer(text, 20, playerData) self:_SendMessageToPlayer(text, 20, playerData)
else
self:E(self.lid..string.format("ERROR: Could not get player data for player %s.", playername))
end
end end
end end
@ -2139,10 +2242,11 @@ end
-- @param #CARRIERTRAINER self -- @param #CARRIERTRAINER self
-- @param #string _unitname Name of the player unit. -- @param #string _unitname Name of the player unit.
function CARRIERTRAINER:_DisplayCarrierWeather(_unitname) function CARRIERTRAINER:_DisplayCarrierWeather(_unitname)
self:F(_unitname) self:E(_unitname)
-- Get player unit and player name. -- Get player unit and player name.
local unit, playername = self:_GetPlayerUnitAndName(_unitname) local unit, playername = self:_GetPlayerUnitAndName(_unitname)
self:E({playername=playername})
-- Check if we have a player. -- Check if we have a player.
if unit and playername then if unit and playername then
@ -2153,11 +2257,10 @@ function CARRIERTRAINER:_DisplayCarrierWeather(_unitname)
-- Current coordinates. -- Current coordinates.
local coord=self.carrier:GetCoordinate() local coord=self.carrier:GetCoordinate()
-- Get atmospheric data at range location. -- Get atmospheric data at carrier location.
local position=self.location --Core.Point#COORDINATE local T=coord:GetTemperature()
local T=position:GetTemperature() local P=coord:GetPressure()
local P=position:GetPressure() local Wd,Ws=coord:GetWind()
local Wd,Ws=position:GetWind()
-- Get Beaufort wind scale. -- Get Beaufort wind scale.
local Bn,Bd=UTILS.BeaufortScale(Ws) local Bn,Bd=UTILS.BeaufortScale(Ws)
@ -2178,23 +2281,21 @@ function CARRIERTRAINER:_DisplayCarrierWeather(_unitname)
tP=string.format("%.2f inHg", P*hPa2inHg) tP=string.format("%.2f inHg", P*hPa2inHg)
end end
-- Report text.
-- Message text. text=text..string.format("Weather Report at Carrier %s:\n", self.alias)
text=text..string.format("Weather Report at %s:\n", self.rangename)
text=text..string.format("--------------------------------------------------\n") text=text..string.format("--------------------------------------------------\n")
text=text..string.format("Temperature %s\n", tT) text=text..string.format("Temperature %s\n", tT)
text=text..string.format("Wind from %s at %s (%s)\n", WD, tW, Bd) text=text..string.format("Wind from %s at %s (%s)\n", WD, tW, Bd)
text=text..string.format("QFE %.1f hPa = %s", P, tP) text=text..string.format("QFE %.1f hPa = %s", P, tP)
-- Send message to player group.
--self:_DisplayMessageToGroup(unit, text, nil, true)
self:_SendMessageToPlayer(text, 30, self.players[playername])
-- Debug output. -- Debug output.
self:T2(self.lid..text) self:T2(self.lid..text)
-- Send message to player group.
self:_SendMessageToPlayer(text, 30, self.players[playername])
else else
self:T(self.lid..string.format("ERROR! Could not find player unit in RangeInfo! Name = %s", _unitname)) self:E(self.lid..string.format("ERROR! Could not find player unit in CarrierWeather! Unit name = %s", _unitname))
end end
end end

View File

@ -342,16 +342,26 @@ function CONTROLLABLE:PushTask( DCSTask, WaitTime )
local DCSControllable = self:GetDCSObject() local DCSControllable = self:GetDCSObject()
if DCSControllable then 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. -- 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. -- Therefore we schedule the functions to set the mission and options for the Controllable.
-- Controller:pushTask( DCSTask ) -- Controller:pushTask( DCSTask )
if WaitTime then local function PushTask( Controller, DCSTask )
self.TaskScheduler:Schedule( Controller, Controller.pushTask, { DCSTask }, WaitTime ) if self and self:IsAlive() then
else local Controller = self:_GetController()
Controller:pushTask( DCSTask ) Controller:pushTask( DCSTask )
else
BASE:E( { DCSControllableName .. " is not alive anymore.", DCSTask = DCSTask } )
end
end
if not WaitTime or WaitTime == 0 then
PushTask( self, DCSTask )
else
self.TaskScheduler:Schedule( self, PushTask, { DCSTask }, WaitTime )
end end
return self return self

View File

@ -706,8 +706,8 @@ end
--- Returns the unit's climb or descent angle. --- Returns the unit's climb or descent angle.
-- @param Wrapper.Positionable#POSITIONABLE self -- @param Wrapper.Positionable#POSITIONABLE self
-- @return #number Climb or descent angle in degrees. -- @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:GetClimbAnge() function POSITIONABLE:GetClimbAngle()
-- Get position of the unit. -- Get position of the unit.
local unitpos = self:GetPosition() local unitpos = self:GetPosition()
@ -719,10 +719,17 @@ function POSITIONABLE:GetClimbAnge()
if unitvel and UTILS.VecNorm(unitvel)~=0 then 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
end end
return nil
end end
--- Returns the pitch angle of a unit. --- Returns the pitch angle of a unit.