From 087ac992a2920409c7b1a910d7e275facd827a72 Mon Sep 17 00:00:00 2001 From: Frank Date: Sun, 4 Nov 2018 01:14:47 +0100 Subject: [PATCH] CT v0.1.6 --- .../Moose/Functional/CarrierTrainer.lua | 469 +++++++++++------- .../Moose/Wrapper/Controllable.lua | 20 +- .../Moose/Wrapper/Positionable.lua | 15 +- 3 files changed, 311 insertions(+), 193 deletions(-) diff --git a/Moose Development/Moose/Functional/CarrierTrainer.lua b/Moose Development/Moose/Functional/CarrierTrainer.lua index f0518f16f..341a24cec 100644 --- a/Moose Development/Moose/Functional/CarrierTrainer.lua +++ b/Moose Development/Moose/Functional/CarrierTrainer.lua @@ -40,7 +40,9 @@ -- @field #CARRIERTRAINER.Checkpoint Wake Right behind the carrier. -- @field #CARRIERTRAINER.Checkpoint Groove In the groove 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 --- Practice Carrier Landings @@ -55,7 +57,7 @@ -- -- @field #CARRIERTRAINER CARRIERTRAINER = { - ClassName = "CARRIERTRAINER", + ClassName = "CARRIERTRAINER", lid = nil, Debug = true, carrier = nil, @@ -76,6 +78,9 @@ CARRIERTRAINER = { Trap = {}, TACAN = nil, ICLS = nil, + rwyangle = -10, + sterndist =-100, + deckheight = 22, } --- Aircraft types. @@ -160,8 +165,7 @@ CARRIERTRAINER.Difficulty={ HARD="TOPGUN Graduate", } - ---- Groove data. +--- Groove position. -- @type CARRIERTRAINER.GroovePos -- @field #string X At the start. -- @field #string RB Roger ball. @@ -197,8 +201,10 @@ CARRIERTRAINER.GroovePos={ -- @field #string difficulty Difficulty level. -- @field #boolean inbigzone If true, player is in the big zone. -- @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 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 #CARRIERTRAINER.GroovePos Groove data table with elemets of type @{#CARRIERTRAINER.GrooveData}. @@ -225,19 +231,23 @@ CARRIERTRAINER.MenuF10={} --- Carrier trainer class version. -- @field #string version -CARRIERTRAINER.version="0.1.5w" +CARRIERTRAINER.version="0.1.6" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- --- TODO: Fix radio menu. +-- TODO: Add scoring to radio menu. -- TODO: Optimized debrief. -- TODO: Add automatic grading. -- TODO: Get board numbers. +-- TODO: Get fuel state in pounds. -- 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. +-- DONE: Fix radio menu. ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Constructor @@ -331,13 +341,6 @@ end -- 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 @@ -371,8 +374,8 @@ function CARRIERTRAINER:onafterStatus(From, Event, To) -- Check player status. self:_CheckPlayerStatus() - -- Call status again in one second. - self:__Status(-0.5) + -- Call status again in 0.25 seconds. + self:__Status(-0.25) end --- 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:_DetailedPlayerStatus(playerData) - --self:_DetailedPlayerStatus(playerData) if unit:IsInZone(self.giantZone) then -- Check if player was previously not inside the zone. @@ -412,15 +414,15 @@ function CARRIERTRAINER:_CheckPlayerStatus() 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 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) end - + if playerData.step==0 and unit:InAir() then self:_NewRound(playerData) -- Jump to Groove for testing. - --playerData.step=8 + playerData.step=8 elseif playerData.step == 1 then self:_Start(playerData) elseif playerData.step == 2 then @@ -453,7 +455,7 @@ function CARRIERTRAINER:_CheckPlayerStatus() else -- Unit not alive. - --playerDatas[i] = nil + self:E(self.lid.."WARNING: Player unit is not alive!") end end end @@ -504,8 +506,8 @@ function CARRIERTRAINER:OnEventBirth(EventData) -- Add Menu commands. self:_AddF10Commands(_unitName) - -- Init player. - self.players[_playername]=self:_InitNewPlayer(_unitName) + -- Init player data. + self.players[_playername]=self:_InitPlayer(_unitName) -- Test --CARRIERTRAINER.LSOcall.HIGHL:ToGroup(_group) @@ -539,9 +541,10 @@ function CARRIERTRAINER:OnEventLand(EventData) self:T(self.lid..text) MESSAGE:New(text, 5):ToAllIf(self.Debug) - -- Check if we caught a wire after one second. - -- TODO: test this! + -- Player data. local playerData=self.players[_playername] --#CARRIERTRAINER.PlayerData + + -- Coordinate at landing event local coord=playerData.unit:GetCoordinate() -- We did land. @@ -555,11 +558,17 @@ function CARRIERTRAINER:OnEventLand(EventData) end end + + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- CARRIER TRAINING functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + --- Initialize player data. -- @param #CARRIERTRAINER self -- @param #string unitname Name of the player unit. -- @return #CARRIERTRAINER.PlayerData Player data. -function CARRIERTRAINER:_InitNewPlayer(unitname) +function CARRIERTRAINER:_InitPlayer(unitname) local playerData={} --#CARRIERTRAINER.PlayerData @@ -601,14 +610,11 @@ function CARRIERTRAINER:_InitNewRound(playerData) playerData.boltered=false playerData.landed=false playerData.waveoff=false + playerData.patternwo=false playerData.Tlso=timer.getTime() return playerData end -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- --- CARRIER TRAINING functions -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- - --- Initialize player data. -- @param #CARRIERTRAINER self -- @param #CARRIERTRAINER.PlayerData playerData Player data. @@ -738,13 +744,13 @@ end function CARRIERTRAINER:_CheckForLongDownwind(playerData) -- Get distances between carrier and player unit (parallel and perpendicular to direction of movement of carrier) - local X, Z = self:_GetDistances(playerData.unit) + local X, Z=self:_GetDistances(playerData.unit) -- Get relative heading. local relhead=self:_GetRelativeHeading(playerData.unit) - -- One NM from carrier is way too far. - local limit = -UTILS.NMToMeters(1) + -- One NM from carrier is too far. + local limit=-UTILS.NMToMeters(1) local text=string.format("Long groove check: X=%d, relhead=%.1f", X, relhead) self:T(text) @@ -793,11 +799,6 @@ function CARRIERTRAINER:_Abeam(playerData) -- Check nest step threshold. if self:_CheckLimits(X, Z, self.Abeam) then - - -- Checks: - -- AoA - -- Altitude - -- Distance to carrier. -- Get AoA and altitude. local aoa = playerData.unit:GetAoA() @@ -821,7 +822,7 @@ function CARRIERTRAINER:_Abeam(playerData) -- Add to debrief. self:_AddToSummary(playerData, "Abeam Position", hintFull) - -- Proceed to next step. + -- Next step: ninety. playerData.step = 6 end end @@ -894,6 +895,7 @@ function CARRIERTRAINER:_Wake(playerData) -- Right behind the wake of the carrier dZ>0. if self:_CheckLimits(X, Z, self.Wake) then + -- Get player altitude and AoA. local alt=playerData.unit:GetAltitude() local aoa=playerData.unit:GetAoA() @@ -931,16 +933,16 @@ function CARRIERTRAINER:_Groove(playerData) return 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) if rho<=calltheball then + -- Get player altitude and AoA. local alt = playerData.unit:GetAltitude() local aoa = playerData.unit:GetAoA() + self:_SendMessageToPlayer("Call the ball.", 8, playerData) CARRIERTRAINER.LSOcall.CALLTHEBALL:ToGroup(playerData.unit:GetGroup()) @@ -966,12 +968,14 @@ function CARRIERTRAINER:_Groove(playerData) groovedata.LUE=self:_Lineup(playerData)-10 groovedata.Step=playerData.step + -- Init groove table. playerData.Groove={} - playerData.Groove.X=grovedata - --table.insert(playerData.Groove, groovedata) - -- Next step. - playerData.step = 90 + + playerData.Groove.X=groovedata + + -- Next step: roger ball. + playerData.step=90 end end @@ -997,20 +1001,17 @@ function CARRIERTRAINER:_CallTheBall(playerData) return 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. local lineup=self:_Lineup(playerData) - local lineupError=lineup-rwyangle + local lineupError=lineup-self.rwyangle -- Glide slope. local glideslope=self:_Glideslope(playerData) local glideslopeError=glideslope-3.5 --TODO: maybe 3.0? + -- Get AoA. + local AoA=playerData.unit:GetAoA() + -- Ranges in the groove. local RRB=UTILS.NMToMeters(0.500) -- Roger Ball! call. local RIM=UTILS.NMToMeters(0.375) -- In the Middle 0.75/2. @@ -1020,11 +1021,10 @@ function CARRIERTRAINER:_CallTheBall(playerData) -- Data local groovedata={} --#CARRIERTRAINER.GrooveData groovedata.Alt=alt - groovedata.AoA=playerData.unit:GetAoA() + groovedata.AoA=AoA groovedata.GSE=glideslopeError groovedata.LUE=lineupError groovedata.Step=playerData.step - --table.insert(playerData.Groove, groovedata) if rho<=RRB and playerData.step==90 then @@ -1057,10 +1057,25 @@ function CARRIERTRAINER:_CallTheBall(playerData) env.info(string.format("FF IC=%d", rho)) -- 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. + playerData.step=93 + end - -- Next step: at the ramp. - playerData.step=93 elseif rho<=RAR and playerData.step==93 then @@ -1069,7 +1084,7 @@ function CARRIERTRAINER:_CallTheBall(playerData) env.info(string.format("FF AR=%d", rho)) -- Store data. - playerData.Groove.AR=groovedata + playerData.Groove.AR=groovedata -- Next step: at the ramp. playerData.step=94 @@ -1084,11 +1099,13 @@ function CARRIERTRAINER:_CallTheBall(playerData) self:_LSOcall(playerData, glideslopeError, lineupError) - elseif X > 150 then + elseif X>0 then local wire = 0 local hint = "" local score = 0 + + if playerData.landed then hint = "You boltered." else @@ -1108,6 +1125,31 @@ function CARRIERTRAINER:_CallTheBall(playerData) 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. -- @param #CARRIERTRAINER self -- @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) 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 -- Seems we have successfully landed. @@ -1151,14 +1189,14 @@ function CARRIERTRAINER:_Trapped(playerData, pos) local score = -10 -- Which wire - if X < -14 then - wire = 1 + if X<-14 then + wire = 1 score = -15 - elseif X < -3 then - wire = 2 + elseif X<-3 then + wire = 2 score = 10 - elseif X < 10 then - wire = 3 + elseif X<10 then + wire = 3 score = 20 else wire = 4 @@ -1177,6 +1215,7 @@ function CARRIERTRAINER:_Trapped(playerData, pos) else --Boltered! + playerData.boltered=true end end @@ -1194,22 +1233,22 @@ function CARRIERTRAINER:_LSOcall(playerData, glideslopeError, lineupError) -- Glideslope high/low calls. if glideslopeError>1 then - text="You're too high! Throttles back!" + text="You're high!" CARRIERTRAINER.LSOcall.HIGHL:ToGroup(player) elseif glideslopeError>0.5 then - text="You're slightly high. Decrease power." + text="You're a little high." CARRIERTRAINER.LSOcall.HIGHS:ToGroup(player) elseif glideslopeError<-1.0 then - text="Power! You're way too low." + text="Power!" CARRIERTRAINER.LSOcall.POWERL:ToGroup(player) elseif glideslopeError<-0.5 then - text="You're slightly low. Increase power." + text="You're a little low." CARRIERTRAINER.LSOcall.POWERS:ToGroup(player) else text="Good altitude." end - text=text..string.format(" Glideslope Error = %.2f %%", glideslopeError) + text=text..string.format(" Glideslope Error = %.2f°", glideslopeError) text=text.."\n" local delay=0 @@ -1235,7 +1274,7 @@ function CARRIERTRAINER:_LSOcall(playerData, glideslopeError, lineupError) text=text.."Good lineup." end - text=text..string.format(" Lineup Error = %.1f %%\n", lineupError) + text=text..string.format(" Lineup Error = %.1f°\n", lineupError) -- Get AoA. local aoa=playerData.unit:GetAoA() @@ -1243,23 +1282,24 @@ function CARRIERTRAINER:_LSOcall(playerData, glideslopeError, lineupError) if aoa>=9.3 then text=text.."Your're slow!" 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 text=text.."You're on speed." 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 text=text.."You're fast!" else text=text.."Unknown AoA state." end + + text=text..string.format(" AoA = %.1f", aoa) -- LSO Message to player. - self:_SendMessageToPlayer(text, 8, playerData, true) + self:_SendMessageToPlayer(text, 5, playerData, false) -- Set last time. - playerData.Tlso=timer.getTime() - + playerData.Tlso=timer.getTime() end --- Get glide slope of aircraft. @@ -1268,16 +1308,12 @@ end -- @return #number Glide slope angle in degrees measured from the 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) 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. - local h=playerData.unit:GetAltitude()-deckheight - local x=math.abs(X-tailpos) + local h=playerData.unit:GetAltitude()-self.deckheight + local x=math.abs(X-self.sterndist) local glideslope=math.atan(h/x) return math.deg(glideslope) @@ -1290,18 +1326,11 @@ end -- @return #number Distance from carrier tail to player aircraft in meters. 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) local X, Z, rho, phi = self:_GetDistances(playerData.unit) -- Position at the end of the deck. From there we calculate the angle. - -- TODO: Check exact number and make carrier dependent. - local b={x=tailpos, z=0} + local b={x=self.sterndist, z=0} -- Position of the aircraft wrt carrier coordinates. local a={x=X, z=Z} @@ -1456,11 +1485,10 @@ end -- @param #CARRIERTRAINER self -- @param #number X X 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. -function CARRIERTRAINER:_CheckAbort(X, Z, check) +function CARRIERTRAINER:_CheckAbort(X, Z, pos) - --[[ local abort=false if pos.Xmin and Xpos.Zmax then abort=true end - ]] + --[[ -- Abort conditions. 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) @@ -1481,7 +1509,8 @@ function CARRIERTRAINER:_CheckAbort(X, Z, check) -- Check if any of the conditions are met. local abort=abortXmin or abortXmax or abortZmin or abortZmax - + ]] + return abort end @@ -1543,8 +1572,8 @@ function CARRIERTRAINER:_AbortPattern(playerData, X, Z, posData) -- Add to debrief. self:_AddToSummary(playerData, "Abort", "Approach aborted.") - -- - playerData.waveoff=true + -- Pattern wave off! + playerData.patternwo=true --TODO: set score and grade. @@ -1567,13 +1596,6 @@ function CARRIERTRAINER:_DetailedPlayerStatus(playerData) local dist=playerData.unit:GetCoordinate():Get2DDistance(self.carrier:GetCoordinate()) 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 wind=unit:GetCoordinate():GetWindWithTurbulenceVec3() @@ -1581,16 +1603,13 @@ function CARRIERTRAINER:_DetailedPlayerStatus(playerData) local relhead=self:_GetRelativeHeading(playerData.unit) - local text=string.format("%s, current AoA=%.1f\n", playerData.callsign, aoa) - text=text..string.format("velo x=%.1f y=%.1f z=%.1f\n", 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("pitch=%.1f | roll=%.1f | yaw=%.1f | climb=%.1f\n", pitch, roll, yaw, unit:GetClimbAnge()) + local text=string.format("%s, current step: %.1f\n", playerData.callsign, self:_StepName(playerData.step)) + 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 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:GetClimbAngle()) 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("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) end @@ -1599,6 +1618,7 @@ end -- @param #CARRIERTRAINER self function CARRIERTRAINER:_InitStennis() + -- Upwind leg self.Upwind.name="Upwind" self.Upwind.Xmin=-4000 -- TODO Should be withing 4 km behind carrier. Why? @@ -1616,7 +1636,7 @@ function CARRIERTRAINER:_InitStennis() -- Early break self.BreakEarly.name="Early Break" self.BreakEarly.Xmin=-500 - self.BreakEarly.Xmax=nil + self.BreakEarly.Xmax=4000 self.BreakEarly.Zmin=-3700 self.BreakEarly.Zmax=1500 self.BreakEarly.LimitXmin=0 @@ -1630,7 +1650,7 @@ function CARRIERTRAINER:_InitStennis() -- Late break self.BreakLate.name="Late Break" self.BreakLate.Xmin=-500 - self.BreakLate.Xmax=nil + self.BreakLate.Xmax=4000 self.BreakLate.Zmin=-3700 self.BreakLate.Zmax=1500 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.") else score = 0 - hint = string.format("perfect distance to the boat.") + hint = string.format("Perfect distance to the boat.") end 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") end + -- Player Data. local playerData=self.players[playername] -- F10/Carrier Trainer/ local _trainPath = missionCommands.addSubMenuForGroup(_gid, self.alias, CARRIERTRAINER.MenuF10[_gid]) + -- F10/Carrier Trainer//Results - --local _statsPath = missionCommands.addSubMenuForGroup(_gid, "Results", _trainPath) - -- F10/Carrier Trainer//My Settings - local _settingsPath = missionCommands.addSubMenuForGroup(_gid, "My Settings", _trainPath) + local _statsPath = missionCommands.addSubMenuForGroup(_gid, "Results", _trainPath) + -- F10/Carrier Trainer//My Settings/Difficulty - local _difficulPath = missionCommands.addSubMenuForGroup(_gid, "Difficulty", _settingsPath) - -- F10/Carrier Trainer//Carrier Info - local _infoPath = missionCommands.addSubMenuForGroup(_gid, "Carrier Info", _trainPath) + local _difficulPath = missionCommands.addSubMenuForGroup(_gid, "Difficulty", _trainPath) - -- F10/Carrier Trainer//Stats/ - --missionCommands.addCommandForGroup(_gid, "All Results", _statsPath, self._DisplayStrafePitResults, self, _unitName) - --missionCommands.addCommandForGroup(_gid, "My Results", _statsPath, self._DisplayBombingResults, self, _unitName) - --missionCommands.addCommandForGroup(_gid, "Reset All Results", _statsPath, self._ResetRangeStats, self, _unitName) - -- F10/Carrier Trainer//My Settings/ - --missionCommands.addCommandForGroup(_gid, "Smoke Delay On/Off", _settingsPath, self._SmokeBombDelayOnOff, self, _unitName) - --missionCommands.addCommandForGroup(_gid, "Smoke Impact On/Off", _settingsPath, self._SmokeBombImpactOnOff, self, _unitName) - --missionCommands.addCommandForGroup(_gid, "Flare Hits On/Off", _settingsPath, self._FlareDirectHitsOnOff, self, _unitName) - -- F10/Carrier Trainer//My Settings/Difficulty - 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) - missionCommands.addCommandForGroup(_gid, "TOPGUN Graduate", _difficulPath, self.SetDifficulty, self, playerData, CARRIERTRAINER.Difficulty.HARD) - -- F10/Carrier Trainer//Carrier Info/ - missionCommands.addCommandForGroup(_gid, "Carrier Info", _infoPath, self._DisplayCarrierInfo, self, _unitName) - missionCommands.addCommandForGroup(_gid, "Weather Report", _infoPath, self._DisplayCarrierWeather, self, _unitName) + -- F10/Carrier Trainer//Results/ + -- TODO: Add result functions. + --missionCommands.addCommandForGroup(_gid, "All Results", _statsPath, self._DisplayStrafePitResults, self, _unitName) + --missionCommands.addCommandForGroup(_gid, "My Results", _statsPath, self._DisplayBombingResults, self, _unitName) + --missionCommands.addCommandForGroup(_gid, "(Clear ALL Results)", _statsPath, self._ResetRangeStats, self, _unitName) + + -- F10/Carrier Trainer//Difficulty + missionCommands.addCommandForGroup(_gid, "Flight Student", _difficulPath, self._SetDifficulty, self, playername, CARRIERTRAINER.Difficulty.EASY) + missionCommands.addCommandForGroup(_gid, "Naval Aviator", _difficulPath, self._SetDifficulty, self, playername, CARRIERTRAINER.Difficulty.NORMAL) + missionCommands.addCommandForGroup(_gid, "TOPGUN Graduate", _difficulPath, self._SetDifficulty, self, playername, CARRIERTRAINER.Difficulty.HARD) + + -- F10/Carrier Trainer// + missionCommands.addCommandForGroup(_gid, "Carrier Info", _trainPath, self._DisplayCarrierInfo, self, _unitName) + missionCommands.addCommandForGroup(_gid, "Weather Report", _trainPath, self._DisplayCarrierWeather, self, _unitName) + --TODO: Flare carrier. end else 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 + +--- 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. -- @param #CARRIERTRAINER self -- @param #string _unitname Name of the player unit. function CARRIERTRAINER:_DisplayCarrierInfo(_unitname) - self:F(_unitname) + self:E(_unitname) -- Get player unit and player name. local unit, playername = self:_GetPlayerUnitAndName(_unitname) @@ -2101,35 +2196,43 @@ function CARRIERTRAINER:_DisplayCarrierInfo(_unitname) -- Check if we have a player. if unit and playername then - -- Message text. - local text=string.format("%s info:\n", self.alias) - - -- Current coordinates. - local coord=self.carrier:GetCoordinate() - + -- Player data. local playerData=self.players[playername] --#CARRIERTRAINER.PlayerData - local carrierheading=self.carrier:GetHeading() - local carrierspeed=UTILS.MpsToKnots(self.carrier:GetVelocity()) - - text=text..string.format("BRC %d\n", carrierheading) - text=text..string.format("Speed %d kts\n", carrierspeed) + if playerData then - - local tacan="unknown" - local icls="unknown" - if self.TACAN~=nil then - tacan=tostring(self.TACAN) - end - if self.ICLS~=nil then - icls=tostring(self.ICLS) - end - - text=text..string.format("TACAN Channel %s", tacan) - text=text..string.format("ICLS Channel %s", icls) - - self:_SendMessageToPlayer(text, 20, playerData) + -- Message text. + local text=string.format("%s info:\n", self.alias) + -- Current coordinates. + local coord=self.carrier:GetCoordinate() + + -- Carrier speed and heading. + local carrierheading=self.carrier:GetHeading() + local carrierspeed=UTILS.MpsToKnots(self.carrier:GetVelocityMPS()) + + -- Tacan/ICLS. + local tacan="unknown" + local icls="unknown" + if self.TACAN~=nil then + tacan=tostring(self.TACAN) + end + if self.ICLS~=nil then + icls=tostring(self.ICLS) + end + + -- 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) + + -- Send message. + 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 @@ -2139,10 +2242,11 @@ end -- @param #CARRIERTRAINER self -- @param #string _unitname Name of the player unit. function CARRIERTRAINER:_DisplayCarrierWeather(_unitname) - self:F(_unitname) + self:E(_unitname) -- Get player unit and player name. local unit, playername = self:_GetPlayerUnitAndName(_unitname) + self:E({playername=playername}) -- Check if we have a player. if unit and playername then @@ -2153,11 +2257,10 @@ function CARRIERTRAINER:_DisplayCarrierWeather(_unitname) -- Current coordinates. local coord=self.carrier:GetCoordinate() - -- Get atmospheric data at range location. - local position=self.location --Core.Point#COORDINATE - local T=position:GetTemperature() - local P=position:GetPressure() - local Wd,Ws=position:GetWind() + -- Get atmospheric data at carrier location. + local T=coord:GetTemperature() + local P=coord:GetPressure() + local Wd,Ws=coord:GetWind() -- Get Beaufort wind scale. local Bn,Bd=UTILS.BeaufortScale(Ws) @@ -2177,24 +2280,22 @@ function CARRIERTRAINER:_DisplayCarrierWeather(_unitname) tW=string.format("%.1f knots", UTILS.MpsToKnots(Ws)) tP=string.format("%.2f inHg", P*hPa2inHg) end - - - -- Message text. - text=text..string.format("Weather Report at %s:\n", self.rangename) + + -- Report text. + text=text..string.format("Weather Report at Carrier %s:\n", self.alias) text=text..string.format("--------------------------------------------------\n") 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("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. self:T2(self.lid..text) + + -- Send message to player group. + self:_SendMessageToPlayer(text, 30, self.players[playername]) + 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 diff --git a/Moose Development/Moose/Wrapper/Controllable.lua b/Moose Development/Moose/Wrapper/Controllable.lua index d57f7fac4..d349aa88d 100644 --- a/Moose Development/Moose/Wrapper/Controllable.lua +++ b/Moose Development/Moose/Wrapper/Controllable.lua @@ -342,16 +342,26 @@ function CONTROLLABLE:PushTask( DCSTask, WaitTime ) local DCSControllable = self:GetDCSObject() if DCSControllable then - local Controller = self:_GetController() + + local DCSControllableName = self:GetName() -- When a controllable SPAWNs, it takes about a second to get the controllable in the simulator. Setting tasks to unspawned controllables provides unexpected results. -- Therefore we schedule the functions to set the mission and options for the Controllable. - -- Controller:pushTask( DCSTask ) + -- Controller:pushTask( DCSTask ) + + local function PushTask( Controller, DCSTask ) + if self and self:IsAlive() then + local Controller = self:_GetController() + Controller:pushTask( DCSTask ) + else + BASE:E( { DCSControllableName .. " is not alive anymore.", DCSTask = DCSTask } ) + end + end - if WaitTime then - self.TaskScheduler:Schedule( Controller, Controller.pushTask, { DCSTask }, WaitTime ) + if not WaitTime or WaitTime == 0 then + PushTask( self, DCSTask ) else - Controller:pushTask( DCSTask ) + self.TaskScheduler:Schedule( self, PushTask, { DCSTask }, WaitTime ) end return self diff --git a/Moose Development/Moose/Wrapper/Positionable.lua b/Moose Development/Moose/Wrapper/Positionable.lua index fef546f38..137d4ab4c 100644 --- a/Moose Development/Moose/Wrapper/Positionable.lua +++ b/Moose Development/Moose/Wrapper/Positionable.lua @@ -706,8 +706,8 @@ end --- Returns the unit's climb or descent angle. -- @param Wrapper.Positionable#POSITIONABLE self --- @return #number Climb or descent angle in degrees. -function POSITIONABLE:GetClimbAnge() +-- @return #number Climb or descent angle in degrees. Or 0 if velocity vector norm is zero (or nil). Or nil, if the position of the POSITIONABLE returns nil. +function POSITIONABLE:GetClimbAngle() -- Get position of the unit. local unitpos = self:GetPosition() @@ -719,10 +719,17 @@ function POSITIONABLE:GetClimbAnge() if unitvel and UTILS.VecNorm(unitvel)~=0 then - return math.asin(unitvel.y/UTILS.VecNorm(unitvel)) - + -- Calculate climb angle. + local angle=math.asin(unitvel.y/UTILS.VecNorm(unitvel)) + + -- Return angle in degrees. + return math.deg(angle) + else + return 0 end end + + return nil end --- Returns the pitch angle of a unit.