From fa4abed02856186437e85f237c06421465964744 Mon Sep 17 00:00:00 2001 From: funkyfranky Date: Wed, 10 Oct 2018 15:56:34 +0200 Subject: [PATCH 01/10] Added AoA function and carrier training WIP --- .../Moose/Functional/CarrierTraining.lua | 931 ++++++++++++++++++ Moose Development/Moose/Utilities/Utils.lua | 21 + .../Moose/Wrapper/Positionable.lua | 59 ++ 3 files changed, 1011 insertions(+) create mode 100644 Moose Development/Moose/Functional/CarrierTraining.lua diff --git a/Moose Development/Moose/Functional/CarrierTraining.lua b/Moose Development/Moose/Functional/CarrierTraining.lua new file mode 100644 index 000000000..acdf751ef --- /dev/null +++ b/Moose Development/Moose/Functional/CarrierTraining.lua @@ -0,0 +1,931 @@ +--- **Functional** - (R2.4) - Carrier CASE I Recovery Practice +-- +-- Practice carrier landings. +-- +-- Features: +-- +-- * CASE I recovery. +-- * Performance evaluation. +-- * Feedback about performance during flight. +-- +-- Please not that his class is work in progress and in an **alpha** stage. +-- At the moment training parameters are optimized for F/A-18C Hornet as aircraft and USS Stennis as carrier. +-- +-- === +-- +-- ### Author: **Bankler** (original idea and script) +-- ### Co-author: **funkyfranky** (implementation as MOOSE class) +-- +-- @module Functional.CarrierTrainer +-- @image MOOSE.JPG + +--- CARRIERTRAINER class. +-- @type CARRIERTRAINER +-- @field #string ClassName Name of the class. +-- @field Wrapper.Unit#UNIT carrier Aircraft carrier unit on which we want to practice. +-- @field Core.Zone#ZONE_UNIT startZone Zone in which the pattern approach starts. +-- @field Core.Zone#ZONE_UNIT giantZone Zone around the carrier to register a new player. +-- @extends Core.Fsm#FSM + +--- Practice Carrier Landings +-- +-- === +-- +-- ![Banner Image](..\Presentations\WAREHOUSE\Warehouse_Main.png) +-- +-- # The Trainer Concept +-- +-- +-- bla bla +-- +-- @field #CARRIERTRAINER +CARRIERTRAINER = { + ClassName = "CARRIERTRAINER", + carrier = nil, +} + +--- Player data. +-- @type CARRIERTRAINER.PlayerData +-- @field #number id Player ID. +-- @field #string callsign Callsign of player. +-- @field #number score Player score. +-- @field Wrapper.Unit#UNIT unit Aircraft unit of the player. +-- @field #number lowestAltitude Lowest altitude. +-- @field #number highestCarrierXDiff +-- @field #number secondsStandingStill Time player does not move after a landing attempt. + + +--- Create new carrier trainer. +-- @param carriername Name of the aircraft carrier unit. +function CARRIERTRAINER:New(carriername) + + -- Set carrier unit. + self.carrier=UNIT:FindByName(carriername) + + if self.carrier then + self.startZone = ZONE_UNIT:New("startZone", self.carrier, 1000, { dx = -2000, dy = 100, relative_to_unit = true }) + self.giantZone = ZONE_UNIT:New("giantZone", self.carrier, 30000, { dx = 0, dy = 0, relative_to_unit = true }) + else + self:E("ERROR: Carrier unit could not be found!") + end + +end + + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- FSM states +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- On after Start event. Starts the warehouse. Addes event handlers and schedules status updates of reqests and queue. +-- @param #CARRIERTRAINER self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function CARRIERTRAINER:onafterStart(From, Event, To) + + -- Events are handled my MOOSE. + self:T(CARRIERTRAINER.id.."Events are handled by MOOSE.") + self:HandleEvent(EVENTS.Birth) + +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- EVENT functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Carrier trainer event handler for event birth. +-- @param #CARRIERTRAINER self +-- @param Core.Event#EVENTDATA EventData +function CARRIERTRAINER:OnEventBirth(EventData) + self:F3({eventbirth = EventData}) + + local _unitName=EventData.IniUnitName + local _unit, _playername=self:_GetPlayerUnitAndName(_unitName) + + self:T3(CARRIERTRAINER.id.."BIRTH: unit = "..tostring(EventData.IniUnitName)) + self:T3(CARRIERTRAINER.id.."BIRTH: group = "..tostring(EventData.IniGroupName)) + self:T3(CARRIERTRAINER.id.."BIRTH: player = "..tostring(_playername)) + + if _unit and _playername then + + local _uid=_unit:GetID() + local _group=_unit:GetGroup() + local _gid=_group:GetID() + local _callsign=_unit:GetCallsign() + + -- Debug output. + local text=string.format("Player %s, callsign %s entered unit %s (UID %d) of group %s (GID %d)", _playername, _callsign, _unitName, _uid, _group:GetName(), _gid) + self:T(CARRIERTRAINER.id..text) + MESSAGE:New(text, 5):ToAllIf(self.Debug) + + local playerdata={} --#CARRIERTRAINER.PlayerData + + playerdata.callsign=_callsign + + -- By default, some bomb impact points and do not flare each hit on target. + self.Player[_playername]=playerdata + + -- Start check in zone timer. + if self.planes[_uid] ~= true then + SCHEDULER:New(nil, self._CheckInZone, {self, EventData.IniUnitName}, 1, 1) + self.planes[_uid] = true + end + + end +end + +--- Carrier trainer event handler for event birth. +-- @param #CARRIERTRAINER self +function CARRIERTRAINER:_CheckPlayerStatus() + + -- Loop over all players. + for _playerName,_playerData in pairs(self.Player) do + local playerData = _playerData --#CARRIERTRAINER.PlayerData + + if playerData then + + -- Player unit. + local unit = playerData.unit + + if unit:IsAlive() then + + if unit:IsInZone(self.giantZone) then + --Tick(playerData) + end + + -- Check long down wind leg. + if playerData.step == 6 and not playerData.longDownwindDone and unit:IsInZone(self.giantZone) then + self:_CheckForLongDownwind(playerData) + end + + if playerData.step == 1 and unit:IsInZone(self.startZone) then + self:_Start(playerData) + elseif playerData.step == 2 and unit:IsInZone(self.giantZone) then + self:_Upwind(playerData) + elseif playerData.step == 3 and unit:IsInZone(self.giantZone) then + self:_Break(playerData, "early") + elseif playerData.step == 4 and unit:IsInZone(self.giantZone) then + self:_Break(playerData, "late") + elseif playerData.step == 5 and unit:IsInZone(self.giantZone) then + self:_Abeam(playerData) + elseif playerData.step == 6 and unit:IsInZone(self.giantZone) then + self:_Ninety(playerData) + elseif playerData.step == 7 and unit:IsInZone(self.giantZone) then + self:_Wake(playerData) + elseif playerData.step == 8 and unit:IsInZone(self.giantZone) then + self:_Groove(playerData) + elseif playerData.step == 9 and unit:IsInZone(self.giantZone) then + self:_Trap(playerData) + end + else + --playerDatas[i] = nil + end + end + end + +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- CARRIER TRAINING functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Initialize player data. +-- @param #CARRIERTRAINER self +function CARRIERTRAINER:_Init(id) + local playerData = nil + + local existingData = playerDatas[id] + if(existingData and existingData.unit:IsAlive()) then + playerData = playerDatas[id] + else + playerData = PlayerData:New(id) + end + + playerData:InitNewRound() + + playerDatas[id] = playerData + env.info("Created playerData object for " .. playerData.unit.UnitName) + + MessageToAll( "Pilot ID: " .. id .. ". Welcome back, " .. playerData.callsign .. "! Cleared for approach! TCN 1X, BRC 354 (MAG HDG).", 5, "InitZoneMessage" ) + + playerData.step = 1 -- 1 !! + playerData.highestCarrierXDiff = -9999999 + playerData.secondsStandingStill = 0 + playerData.summary = "SUMMARY:\n" +end + +--- Start landing pattern. +-- @param #CARRIERTRAINER self +-- @param #CARRIERTRAINER.PlayerData playerData Player data table. +function CARRIERTRAINER:_Start(playerData) + local hint = "Entering the pattern, " .. playerData.callsign .. "! Aim for 800 feet and 350-400 kts on the upwind." + self:_SendMessageToPlayer(hint, 8, playerData) + playerData.score = 0 + playerData.step = 2 +end + +--- Upwind leg. +-- @param #CARRIERTRAINER self +-- @param #CARRIERTRAINER.PlayerData playerData Player data table. +function CARRIERTRAINER:_Upwind(playerData) + + -- Player and carrier position vector. + local position = playerData.unit:GetVec3() + local carrierPosition = self.carrier:GetVec3() + + local diffZ = position.z - carrierPosition.z + local diffX = position.x - carrierPosition.x + if(diffZ > 500 or diffZ < 0 or diffX < -4000) then + self:_AbortPattern(playerData) + return + end + + if (diffX < 0) then + return + end + + local idealAltitude = 800 + local altitude = UTILS.Round( UTILS.MetersToFeet( position.y ) ) + + local hint = "" + local score = 0 + + if(altitude > 850) then + score = 5 + hint = "You're high on the upwind." + elseif(altitude > 830) then + score = 7 + hint = "You're slightly high on the upwind." + elseif (altitude < 750) then + score = 5 + hint = "You're low on the upwind." + elseif (altitude < 770) then + score = 7 + hint = "You're slightly low on the upwind." + else + score = 10 + hint = "Good altitude on the upwind." + end + + playerData:IncreaseScore(score) + + self:_SendMessageToPlayer(hint, 8, playerData) + + self:_PrintAltitudeFeedback(altitude, idealAltitude, playerData) + self:_PrintScore(score, playerData, true) + + playerData:AddToSummary(hint) + playerData.step = 3 +end + +--- Break. +-- @param #CARRIERTRAINER self +-- @param #CARRIERTRAINER.PlayerData playerData Player data table. +-- @param #string part Part of the break. +function CARRIERTRAINER:_Break(playerData, part) + + local playerPosition = playerData.unit:GetVec3() + local carrierPosition = self.carrier:GetVec3() + + local diffZ = playerPosition.z - carrierPosition.z + local diffX = playerPosition.x - carrierPosition.x + + if(diffZ > 1500 or diffZ < -3700 or diffX < -500) then + self:_AbortPattern(playerData) + return + end + + local limit = -370 + + if (part == "late") then + limit = -1470 + end + + if diffZ < limit then + local idealAltitude = 800 + local altitude = UTILS.Round( UTILS.MetersToFeet( playerPosition.y ) ) + + local hint = "" + local score = 0 + + if(altitude > 880) then + score = 5 + hint = "You're high in the " .. part .. " break." + elseif(altitude > 850) then + score = 7 + hint = "You're slightly high in the " .. part .. " break." + elseif (altitude < 720) then + score = 5 + hint = "You're low in the " .. part .. " break." + elseif (altitude < 750) then + score = 7 + hint = "You're slightly low in the " .. part .. " break." + else + score = 10 + hint = "Good altitude in the " .. part .. " break!" + end + + playerData:IncreaseScore(score) + + self:_SendMessageToPlayer( hint, 8, playerData ) + self:_PrintAltitudeFeedback(altitude, idealAltitude, playerData) + self:_PrintScore(score, playerData, true) + + playerData:AddToSummary(hint) + + if (part == "early") then + playerData.step = 4 + else + playerData.step = 5 + end + end +end + +--- Break. +-- @param #CARRIERTRAINER self +-- @param #CARRIERTRAINER.PlayerData playerData Player data table. +function CARRIERTRAINER:_Abeam(playerData) + local playerPosition = playerData.unit:GetVec3() + local carrierPosition = self.carrier:GetVec3() + + local diffZ = playerPosition.z - carrierPosition.z + local diffX = playerPosition.x - carrierPosition.x + if(diffZ > -1000 or diffZ < -3700) then + self:_AbortPattern(playerData) + return + end + + local limit = -200 + + if diffX < limit then + --local aoa = math.deg(mist.getAoA(playerData.mistUnit)) + local aoa = playerData.unit:GetAoA() + local aoaFeedback = self:_PrintAoAFeedback(aoa, 8.1, playerData) + + local onSpeedScore = self:_GetOnSpeedScore(aoa) + + local idealAltitude = 600 + local altitude = UTILS.Round( UTILS.MetersToFeet( playerPosition.y ) ) + + local hint = "" + local score = 0 + + if(altitude > 700) then + score = 5 + hint = "You're high (" .. altitude .. " ft) abeam" + elseif(altitude > 650) then + score = 7 + hint = "You're slightly high (" .. altitude .. " ft) abeam" + elseif (altitude < 540) then + score = 5 + hint = "You're low (" .. altitude .. " ft) abeam" + elseif (altitude < 570) then + score = 7 + hint = "You're slightly low (" .. altitude .. " ft) abeam" + else + score = 10 + hint = "Good altitude (" .. altitude .. " ft) abeam" + end + + local distanceHint = "" + local distanceScore + local diffEast = carrierPosition.z - playerPosition.z + + local nm = diffEast / 1852 --nm conversion + local idealDistance = 1.2 + + local roundedNm = UTILS.Round(nm, 2) + + if(nm < 1.0) then + distanceScore = 0 + distanceHint = "too close to the boat (" .. roundedNm .. " nm)" + elseif(nm < 1.1) then + distanceScore = 5 + distanceHint = "slightly too close to the boat (" .. roundedNm .. " nm)" + elseif(nm < 1.3) then + distanceScore = 10 + distanceHint = "with perfect distance to the boat (" .. roundedNm .. " nm)" + elseif(nm < 1.4) then + distanceScore = 5 + distanceHint = "slightly too far from the boat (" .. roundedNm .. " nm)" + else + distanceScore = 0 + distanceHint = "too far from the boat (" .. roundedNm .. " nm)" + end + + local fullHint = hint .. ", " .. distanceHint + + self:_SendMessageToPlayer( fullHint, 8, playerData ) + self:_SendMessageToPlayer( "(Target: 600 ft and 1.2 nm).", 8, playerData ) + + playerData:IncreaseScore(score + distanceScore + onSpeedScore) + self:_PrintScore(score + distanceScore + onSpeedScore, playerData, true) + + playerData:AddToSummary(fullHint .. " (" .. aoaFeedback .. ")") + playerData.step = 6 + end +end + +--- Down wind long check. +-- @param #CARRIERTRAINER self +-- @param #CARRIERTRAINER.PlayerData playerData Player data table. +function CARRIERTRAINER:_CheckForLongDownwind(playerData) + local playerPosition = playerData.unit:GetVec3() + local carrierPosition = self.carrier:GetVec3() + + local limit = 1500 + + if carrierPosition.x - playerPosition.x > limit then + --local heading = math.deg(mist.getHeading(playerData.mistUnit)) + local heading = playerData.unit:GetHeading() + + if(heading > 170) then + local hint = "Too long downwind. Turn final earlier next time." + self:_SendMessageToPlayer( hint, 8, playerData ) + local score = -40 + playerData:IncreaseScore(score) + self:_PrintScore(score, playerData, true) + playerData:AddToSummary(hint) + playerData.longDownwindDone = true + end + end +end + +--- Ninety. +-- @param #CARRIERTRAINER self +-- @param #CARRIERTRAINER.PlayerData playerData Player data table. +function CARRIERTRAINER:_Ninety(playerData) + local playerPosition = playerData.unit:GetVec3() + local carrierPosition = self.carrier:GetVec3() + + local diffZ = playerPosition.z - carrierPosition.z + local diffX = playerPosition.x - carrierPosition.x + if(diffZ < -3700 or diffX < -3700 or diffX > 0) then + self:_AbortPattern(playerData) + return + end + + local limitEast = -1111 --0.6nm + + if diffZ > limitEast then + local idealAltitude = 500 + local altitude = UTILS.Round( UTILS.MetersToFeet( playerPosition.y ) ) + + local hint = "" + local score = 0 + + if(altitude > 600) then + score = 5 + hint = "You're high at the 90." + elseif(altitude > 550) then + score = 7 + hint = "You're slightly high at the 90." + elseif (altitude < 380) then + score = 5 + hint = "You're low at the 90." + elseif (altitude < 420) then + score = 7 + hint = "You're slightly low at the 90." + else + score = 10 + hint = "Good altitude at the 90!" + end + + self:_SendMessageToPlayer( hint, 8, playerData ) + self:_PrintAltitudeFeedback(altitude, idealAltitude, playerData) + + --local aoa = math.deg(mist.getAoA(playerData.mistUnit)) + local aoa = playerData.unit:GetAoA() + local aoaFeedback = self:_PrintAoAFeedback(aoa, 8.1, playerData) + + local onSpeedScore = self:_GetOnSpeedScore(aoa) + + playerData:IncreaseScore(score + onSpeedScore) + self:_PrintScore(score + onSpeedScore, playerData, true) + + playerData:AddToSummary(hint .. " (" .. aoaFeedback .. ")") + + playerData.longDownwindDone = true + playerData.step = 7 + end +end + +--- Wake. +-- @param #CARRIERTRAINER self +-- @param #CARRIERTRAINER.PlayerData playerData Player data table. +function CARRIERTRAINER:_Wake(playerData) + local playerPosition = playerData.unit:GetVec3() + local carrierPosition = carrier:GetVec3() + + local diffZ = playerPosition.z - carrierPosition.z + local diffX = playerPosition.x - carrierPosition.x + if(diffZ < -2000 or diffX < -4000 or diffX > 0) then + self:_AbortPattern(playerData) + return + end + + if diffZ > 0 then + local idealAltitude = 370 + local altitude = UTILS.Round( UTILS.MetersToFeet( playerPosition.y ) ) + + local hint = "" + local score = 0 + + if(altitude > 500) then + score = 5 + hint = "You're high at the wake." + elseif(altitude > 450) then + score = 7 + hint = "You're slightly high at the wake." + elseif (altitude < 300) then + score = 5 + hint = "You're low at the wake." + elseif (altitude < 340) then + score = 7 + hint = "You're slightly low at the wake." + else + score = 10 + hint = "Good altitude at the wake!" + end + + self:_SendMessageToPlayer( hint, 8, playerData ) + self:_PrintAltitudeFeedback(altitude, idealAltitude, playerData) + + --local aoa = math.deg(mist.getAoA(playerData.mistUnit)) + local aoa = playerData.unit:GetAoA() + local aoaFeedback = self:_PrintAoAFeedback(aoa, 8.1, playerData) + + local onSpeedScore = self:_GetOnSpeedScore(aoa) + + playerData:IncreaseScore(score + onSpeedScore) + self:_PrintScore(score + onSpeedScore, playerData, true) + + playerData:AddToSummary(hint .. " (" .. aoaFeedback .. ")") + playerData.step = 8 + end +end + +--- Groove. +-- @param #CARRIERTRAINER self +-- @param #CARRIERTRAINER.PlayerData playerData Player data table. +function CARRIERTRAINER:_Groove(playerData) + local playerPosition = playerData.unit:GetVec3() + local carrierPosition = self.carrier:GetVec3() + + local diffX = playerPosition.x - (carrierPosition.x - 100) + local diffZ = playerPosition.z - carrierPosition.z + + if(diffX > 0 or diffX < -4000) then + self:_AbortPattern(playerData) + return + end + + if(diffX > -500) then --Reached in close before groove + local hint = "You're too far left and never reached the groove." + self:_SendMessageToPlayer( hint, 8, playerData ) + self:_PrintScore(0, playerData, true) + playerData:AddToSummary(hint) + playerData.step = 9 + else + local limitDeg = 8.0 + + local fraction = diffZ / (-diffX) + local asinValue = math.asin(fraction) + local angle = math.deg(asinValue) + + if diffZ > -1300 and angle > limitDeg then + local idealAltitude = 300 + local altitude = UTILS.Round( UTILS.MetersToFeet( playerPosition.y ) ) + + local hint = "" + local score = 0 + + if (altitude > 450) then + score = 5 + hint = "You're high in the groove." + elseif (altitude > 350) then + score = 7 + hint = "You're slightly high in the groove." + elseif (altitude < 240) then + score = 5 + hint = "You're low in the groove." + elseif (altitude < 270) then + score = 7 + hint = "You're slightly low in the groove." + else + score = 10 + hint = "Good altitude in the groove!" + end + + self:_SendMessageToPlayer(hint, 8, playerData) + self:_PrintAltitudeFeedback(altitude, idealAltitude, playerData) + + --local aoa = math.deg(mist.getAoA(playerData.mistUnit)) + local aoa = playerData.unit:GetAoA() + local aoaFeedback = self:_PrintAoAFeedback(aoa, 8.1, playerData) + + local onSpeedScore = self:_GetOnSpeedScore(aoa) + + playerData:IncreaseScore(score + onSpeedScore) + self:_PrintScore(score + onSpeedScore, playerData, true) + + local fullHint = hint .. " (" .. aoaFeedback .. ")" + + playerData:AddToSummary(fullHint) + + playerData.step = 9 + end + end +end + +--- Trap. +-- @param #CARRIERTRAINER self +-- @param #CARRIERTRAINER.PlayerData playerData Player data table. +function CARRIERTRAINER:_Trap(playerData) + local playerPosition = playerData.unit:GetVec3() + local carrierPosition = self.carrier:GetVec3() + + local playerVelocity = playerData.unit:GetVelocityKMH() + local carrierVelocity = self.carrier:GetVelocityKMH() + + local diffZ = playerPosition.z - carrierPosition.z + local diffX = playerPosition.x - carrierPosition.x + + if(diffZ < -2000 or diffZ > 2000 or diffX < -3000) then + self:_AbortPattern(playerData) + return + end + + if(diffX > playerData.highestCarrierXDiff) then + playerData.highestCarrierXDiff = diffX + end + + if(playerPosition.y < playerData.lowestAltitude) then + playerData.lowestAltitude = playerPosition.y + end + + if math.abs(playerVelocity - carrierVelocity) < 0.01 then + playerData.secondsStandingStill = playerData.secondsStandingStill + 1 + + if diffX < playerData.highestCarrierXDiff or playerData.secondsStandingStill > 5 then + + env.info("Trap identified! diff " .. diffX .. ", highestCarrierXDiff" .. playerData.highestCarrierXDiff .. ", secondsStandingStill: " .. playerData.secondsStandingStill); + + local wire = 1 + local score = -10 + + if(diffX < -14) then + wire = 1 + score = -15 + elseif(diffX < -3) then + wire = 2 + score = 10 + elseif (diffX < 10) then + wire = 3 + score = 20 + else + wire = 4 + score = 7 + end + + playerData:IncreaseScore(score) + + self:_SendMessageToPlayer( "TRAPPED! " .. wire .. "-wire!", 30, playerData ) + self:_PrintScore(score, playerData, false) + + env.info("Distance! " .. diffX .. " meters resulted in a " .. wire .. "-wire estimation."); + + local fullHint = "Trapped catching the " .. wire .. "-wire." + + playerData:AddToSummary(fullHint) + + self:_PrintFinalScore(playerData, 60, wire) + self:_HandleCollectedResult(playerData, wire) + playerData.step = 0 + end + + elseif (diffX > 150) then + + local wire = 0 + local hint = "" + local score = 0 + if(playerData.lowestAltitude < 23) then + hint = "You boltered." + else + hint = "You were waved off." + wire = -1 + score = -10 + end + + self:_SendMessageToPlayer( hint, 8, playerData ) + self:_PrintScore(score, playerData, true) + + playerData:AddToSummary(hint) + + self:_PrintFinalScore(playerData, 60, wire) + self:_HandleCollectedResult(playerData, wire) + + playerData.step = 0 + end +end + + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- MISC functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Send message about altitude feedback. +-- @param #CARRIERTRAINER self +-- @param #number altitude Current altitude of the player. +-- @param #number idealAltitude Ideal altitude. +-- @param #CARRIERTRAINER.PlayerData playerData Player data. +function CARRIERTRAINER:_PrintAltitudeFeedback(altitude, idealAltitude, playerData) + self:_SendMessageToPlayer( "Alt: " .. altitude .. " (Target: " .. idealAltitude .. ")", 8, playerData ) +end + +--- Score for correct AoA. +-- @param #CARRIERTRAINER self +-- @param #number AoA Angle of attack. +function CARRIERTRAINER:_GetOnSpeedScore(AoA) + local score = 0 + if(AoA > 9.5) then --Slow + score = 0 + elseif(AoA > 9) then --Slightly slow + score = 5 + elseif(AoA > 7.25) then --On speed + score = 10 + elseif(AoA > 6.7) then --Slightly fast + score = 5 + else --Fast + score = 0 + end + + return score +end + +--- Print AoA feedback. +-- @param #CARRIERTRAINER self +-- @param #number AoA Angle of attack. +-- @param #number idealAoA Ideal AoA. +-- @param #CARRIERTRAINER.PlayerData playerData Player data. +-- @return #string Feedback hint. +function CARRIERTRAINER:_PrintAoAFeedback(AoA, idealAoA, playerData) + + local hint = "" + if(AoA > 9.5) then + hint = "You're slow." + elseif(AoA > 9) then + hint = "You're slightly slow." + elseif(AoA > 7.25) then + hint = "You're on speed!" + elseif(AoA > 6.7) then + hint = "You're slightly fast." + else + hint = "You're fast." + end + + local roundedAoA = UTILS.Round(AoA, 2) + + self:_SendMessageToPlayer(hint .. " AOA: " .. roundedAoA .. " (Target: " .. idealAoA .. ")", 8, playerData) + + return hint +end + +--- Send message to playe client. +-- @param #CARRIERTRAINER self +-- @param #string message The message to send. +-- @param #number duration Display message duration. +-- @param #CARRIERTRAINER.PlayerData playerData Player data. +function CARRIERTRAINER:_SendMessageToPlayer(message, duration, playerData) + if playerData.client then + MESSAGE:New( message, duration, ""):ToClient(playerData.client) + end +end + +--- Send message to playe client. +-- @param #CARRIERTRAINER self +-- @param #number score Score. +-- @param #CARRIERTRAINER.PlayerData playerData Player data. +function CARRIERTRAINING:_PrintScore(score, playerData, alsoPrintTotalScore) + if(alsoPrintTotalScore) then + self:_SendMessageToPlayer( "Score: " .. score .. " (Total: " .. playerData.score .. ")", 8, playerData ) + else + self:_SendMessageToPlayer( "Score: " .. score, 8, playerData ) + end +end + +--- Display final score. +-- @param #CARRIERTRAINER self +-- @param #CARRIERTRAINER.PlayerData playerData Player data. +-- @param #number duration Duration for message display. +function CARRIERTRAINER:_PrintFinalScore(playerData, duration, wire) + local wireText = "" + if(wire == -2) then + wireText = "Aborted approach" + elseif(wire == -1) then + wireText = "Wave-off" + elseif(wire == 0) then + wireText = "Bolter" + else + wireText = wire .. "-wire" + end + + MessageToAll( playerData.callsign .. " - Final score: " .. playerData.score .. " / 140 (" .. wireText .. ")", duration, "FinalScore" ) + self:_SendMessageToPlayer( playerData.summary, duration, playerData ) +end + +--- Collect result. +-- @param #CARRIERTRAINER self +-- @param #CARRIERTRAINER.PlayerData playerData Player data. +-- @param #number wire Trapped wire. +function CARRIERTRAINER:_HandleCollectedResult(playerData, wire) + local newString = "" + if(wire == -2) then + newString = playerData.score .. " (Aborted)" + elseif(wire == -1) then + newString = playerData.score .. " (Wave-off)" + elseif(wire == 0) then + newString = playerData.score .. " (Bolter)" + else + newString = playerData.score .. " (" .. wire .."W)" + end + + playerData.totalScore = playerData.totalScore + playerData.score + playerData.passes = playerData.passes + 1 + + if(playerData.collectedResultString == "") then + playerData.collectedResultString = newString + else + playerData.collectedResultString = playerData.collectedResultString .. ", " .. newString + MessageToAll( playerData.callsign .. "'s " .. playerData.passes .. " passes: " .. playerData.collectedResultString .. " (TOTAL: " .. playerData.totalScore .. ")" , 30, "CollectedResult" ) + end + + self:_SendMessageToPlayer( "Return south 4 nm (over the trailing ship), towards WP 1, to restart the pattern.", 20, playerData ) +end + +--- Pattern aborted. +-- @param #CARRIERTRAINER self +-- @param #CARRIERTRAINER.PlayerData playerData Player data. +function CARRIERTRAINER:_AbortPattern(playerData) + self:_SendMessageToPlayer( "You're too far from where you should be. Abort approach!", 15, playerData ) + playerData:AddToSummary("Approach aborted.") + self:_PrintFinalScore(playerData, 30, -2) + self:_HandleCollectedResult(playerData, -2) + playerData.step = 0 +end + +--- Get the formatted score. +-- @param #CARRIERTRAINER self +-- @param #number score Score of player. +-- @param #number maxScore Max score possible. +-- @return #string Formatted score text. +function CARRIERTRAINER:_GetFormattedScore(score, maxScore) + if(score < maxScore) then + return " (" .. score .. " points)." + else + return " (" .. score .. " points)!" + end +end + +--- Get distance feedback. +-- @param #CARRIERTRAINER self +-- @param #number distance Distance to boat. +-- @param #number idealDistance Ideal distance. +-- @return #string Feedback text. +function CARRIERTRAINER:_GetDistanceFeedback(distance, idealDistance) + return distance .. " nm (Target: " .. idealDistance .. " nm)" +end + + +--- Returns the unit of a player and the player name. If the unit does not belong to a player, nil is returned. +-- @param #CARRIERTRAINER self +-- @param #string _unitName Name of the player unit. +-- @return Wrapper.Unit#UNIT Unit of player or nil. +-- @return #string Name of the player or nil. +function CARRIERTRAINER:_GetPlayerUnitAndName(_unitName) + self:F2(_unitName) + + if _unitName ~= nil then + + -- Get DCS unit from its name. + local DCSunit=Unit.getByName(_unitName) + + if DCSunit then + + local playername=DCSunit:getPlayerName() + local unit=UNIT:Find(DCSunit) + + self:T2({DCSunit=DCSunit, unit=unit, playername=playername}) + if DCSunit and unit and playername then + return unit, playername + end + + end + + end + + -- Return nil if we could not find a player. + return nil,nil +end + + diff --git a/Moose Development/Moose/Utilities/Utils.lua b/Moose Development/Moose/Utilities/Utils.lua index 1ccee1d39..5c7cce48a 100644 --- a/Moose Development/Moose/Utilities/Utils.lua +++ b/Moose Development/Moose/Utilities/Utils.lua @@ -657,5 +657,26 @@ function UTILS.Randomize(value, fac, lower, upper) return r end +--- Calculate the dot (scalar) product of two vectors. +-- @param DCS#Vec3 a Vector in 3D with x,y,z components. +-- @param DCS#Vec3 b Vector in 3D with x,y,z components. +-- @return #number Scalar product of the two vectors a*b. +function UTILS.VecDot(a, b) + return a.x*b.x + a.y*b.y + a.z*b.z +end +--- Calculate the eucledian norm (length) of a 3D vector. +-- @param DCS#Vec3 a Vector in 3D with x,y,z components. +-- @return #number Norm of the vector. +function UTILS.VecNorm(a) + return math.sqrt(UTILS.DotProduct(a, a)) +end + +--- Calculate the cross product of two 3D vectors. +-- @param DCS#Vec3 a Vector in 3D with x,y,z components. +-- @param DCS#Vec3 b Vector in 3D with x,y,z components. +-- @return DCS#Vec3 Vector +function UTILS.VecCross(a, b) + return {x=a.y*b.z - a.z*b.y, y=a.z*b.x - a.x*b.z, z=a.x*b.y - a.y*b.x} +end diff --git a/Moose Development/Moose/Wrapper/Positionable.lua b/Moose Development/Moose/Wrapper/Positionable.lua index ac01535fe..f97f33dfd 100644 --- a/Moose Development/Moose/Wrapper/Positionable.lua +++ b/Moose Development/Moose/Wrapper/Positionable.lua @@ -121,6 +121,25 @@ function POSITIONABLE:Destroy( GenerateEvent ) return nil end +--- Returns a pos3 table of the objects current position and orientation in 3D space. X, Y, Z values are unit vectors defining the objects orientation. +-- Coordinates are dependent on the position of the maps origin. +-- @param Wrapper.Positionable#POSITIONABLE self +-- @return DCS#Position Table consisting of the point and orientation tables. +function POSITIONABLE:GetPosition() + self:F2( self.PositionableName ) + + local DCSPositionable = self:GetDCSObject() + + if DCSPositionable then + local PositionablePosition = DCSPositionable:getPosition() + self:T3( PositionablePosition ) + return PositionablePosition + end + + BASE:E( { "Cannot GetPositionVec3", Positionable = self, Alive = self:IsAlive() } ) + return nil +end + --- Returns the @{DCS#Position3} position vectors indicating the point and direction vectors in 3D of the POSITIONABLE within the mission. -- @param Wrapper.Positionable#POSITIONABLE self -- @return DCS#Position The 3D position vectors of the POSITIONABLE. @@ -582,6 +601,46 @@ function POSITIONABLE:GetVelocityMPS() return 0 end +--- Returns the Angle of Attack of a positionable. +-- @param Wrapper.Positionable#POSITIONABLE self +-- @return #number Angle of attack in degrees. +function POSITIONABLE:GetAoA() + + -- Get position of the unit. + local unitpos = self:GetPosition() + + if unitpos then + + -- Get velocity vector of the unit. + local unitvel = self:GetVelocity() + + if unitvel and UTILS.VecNorm(unitvel)~=0 then + + -- Unit velocity transformed into aircraft axes directions. + local AxialVel = {} + + -- Transform velocity components in direction of aircraft axes. + AxialVel.x = UTILS.VecDot(unitpos.x, unitvel) + AxialVel.y = UTILS.VecDot(unitpos.y, unitvel) + AxialVel.z = UTILS.VecDot(unitpos.z, unitvel) + + -- AoA is angle between unitpos.x and the x and y velocities. + local AoA = math.acos(UTILS.VecDot({x = 1, y = 0, z = 0}, {x = AxialVel.x, y = AxialVel.y, z = 0})/UTILS.VecNorm({x = AxialVel.x, y = AxialVel.y, z = 0})) + + --Set correct direction: + if AxialVel.y > 0 then + AoA = -AoA + end + + -- Return AoA value in degrees. + return math.deg(AoA) + end + + end + + return nil +end + --- Returns the message text with the callsign embedded (if there is one). -- @param #POSITIONABLE self From ccf2f6006894b6b741c91b30f9757d7cb80414c9 Mon Sep 17 00:00:00 2001 From: Frank Date: Thu, 11 Oct 2018 00:34:14 +0200 Subject: [PATCH 02/10] CT --- .../Moose/Functional/CarrierTraining.lua | 234 ++++++++++++++---- 1 file changed, 181 insertions(+), 53 deletions(-) diff --git a/Moose Development/Moose/Functional/CarrierTraining.lua b/Moose Development/Moose/Functional/CarrierTraining.lua index acdf751ef..8a218c789 100644 --- a/Moose Development/Moose/Functional/CarrierTraining.lua +++ b/Moose Development/Moose/Functional/CarrierTraining.lua @@ -10,6 +10,7 @@ -- -- Please not that his class is work in progress and in an **alpha** stage. -- At the moment training parameters are optimized for F/A-18C Hornet as aircraft and USS Stennis as carrier. +-- Other aircraft and carriers **might** be possible in future but would need a different set of parameters. -- -- === -- @@ -22,16 +23,19 @@ --- CARRIERTRAINER class. -- @type CARRIERTRAINER -- @field #string ClassName Name of the class. +-- @field #string lid Class id string for output to DCS log file. +-- @field #boolean Debug Debug mode. Messages to all about status. -- @field Wrapper.Unit#UNIT carrier Aircraft carrier unit on which we want to practice. -- @field Core.Zone#ZONE_UNIT startZone Zone in which the pattern approach starts. --- @field Core.Zone#ZONE_UNIT giantZone Zone around the carrier to register a new player. +-- @field Core.Zone#ZONE_UNIT giantZone Zone around the carrier to register a new player. +-- @field #table plyers Table of players. -- @extends Core.Fsm#FSM --- Practice Carrier Landings -- -- === -- --- ![Banner Image](..\Presentations\WAREHOUSE\Warehouse_Main.png) +-- ![Banner Image](..\Presentations\CARRIERTRAINER\CarrierTrainer_Main.png) -- -- # The Trainer Concept -- @@ -41,9 +45,18 @@ -- @field #CARRIERTRAINER CARRIERTRAINER = { ClassName = "CARRIERTRAINER", + lid = nil, + Debug = true, carrier = nil, + startZone = nil, + giantZone = nil, + players = nil, } +--- Carrier trainer class version. +-- @field #string version +CARRIERTRAINER.version="0.0.1" + --- Player data. -- @type CARRIERTRAINER.PlayerData -- @field #number id Player ID. @@ -54,11 +67,19 @@ CARRIERTRAINER = { -- @field #number highestCarrierXDiff -- @field #number secondsStandingStill Time player does not move after a landing attempt. +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Constructor +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- --- Create new carrier trainer. +-- @param #CARRIERTRAINER self -- @param carriername Name of the aircraft carrier unit. +-- @return #CARRIERTRAINER self function CARRIERTRAINER:New(carriername) + -- Inherit everthing from FSM class. + local self = BASE:Inherit(self, FSM:New()) -- #WAREHOUSE + -- Set carrier unit. self.carrier=UNIT:FindByName(carriername) @@ -67,8 +88,44 @@ function CARRIERTRAINER:New(carriername) self.giantZone = ZONE_UNIT:New("giantZone", self.carrier, 30000, { dx = 0, dy = 0, relative_to_unit = true }) else self:E("ERROR: Carrier unit could not be found!") - end + end + -- Set some string id for output to DCS.log file. + self.lid=string.format("CARRIERTRAINER %s | ", carriername) + + ----------------------- + --- FSM Transitions --- + ----------------------- + + -- Start State. + self:SetStartState("Stopped") + + -- Add FSM transitions. + -- From State --> Event --> To State + self:AddTransition("Stopped", "Start", "Running") + self:AddTransition("Running", "Status", "Running") + self:AddTransition("Running", "Stop", "Stopped") + + + --- Triggers the FSM event "Start" that starts the carrier trainer. Initializes parameters and starts event handlers. + -- @function [parent=#CARRIERTRAINER] Start + -- @param #CARRIERTRAINER self + + --- Triggers the FSM event "Start" after a delay that starts the carrier trainer. Initializes parameters and starts event handlers. + -- @function [parent=#CARRIERTRAINER] __Start + -- @param #CARRIERTRAINER self + -- @param #number delay Delay in seconds. + + --- Triggers the FSM event "Stop" that stops the carrier trainer. Event handlers are stopped. + -- @function [parent=#CARRIERTRAINER] Stop + -- @param #CARRIERTRAINER self + + --- Triggers the FSM event "Stop" that stops the carrier trainer after a delay. Event handlers are stopped. + -- @function [parent=#CARRIERTRAINER] __Stop + -- @param #CARRIERTRAINER self + -- @param #number delay Delay in seconds. + + return self end @@ -84,9 +141,37 @@ end function CARRIERTRAINER:onafterStart(From, Event, To) -- Events are handled my MOOSE. - self:T(CARRIERTRAINER.id.."Events are handled by MOOSE.") + self:I(self.lid..string.format"Starting Carrier Training %s for carrier unit %s.", CARRIERTRAINER.version, self.carrier:GetName()) + + -- Handle events. self:HandleEvent(EVENTS.Birth) + + -- Init status checkss + self:__Status(-1) +end + +--- On after Status event. Checks player status. +-- @param #CARRIERTRAINER self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function CARRIERTRAINER:onafterStatus(From, Event, To) + + -- Check player status. + self:_CheckPlayerStatus() + + -- Call status again in one second. + self:_Status(-1) +end + +--- On after Stop event. Unhandle events and stop status updates. +-- @param #CARRIERTRAINER self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function CARRIERTRAINER:onafterStop(From, Event, To) + self:UnHandleEvent(EVENTS.Birth) end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -102,9 +187,9 @@ function CARRIERTRAINER:OnEventBirth(EventData) local _unitName=EventData.IniUnitName local _unit, _playername=self:_GetPlayerUnitAndName(_unitName) - self:T3(CARRIERTRAINER.id.."BIRTH: unit = "..tostring(EventData.IniUnitName)) - self:T3(CARRIERTRAINER.id.."BIRTH: group = "..tostring(EventData.IniGroupName)) - self:T3(CARRIERTRAINER.id.."BIRTH: player = "..tostring(_playername)) + self:T3(self.lid.."BIRTH: unit = "..tostring(EventData.IniUnitName)) + self:T3(self.lid.."BIRTH: group = "..tostring(EventData.IniGroupName)) + self:T3(self.lid.."BIRTH: player = "..tostring(_playername)) if _unit and _playername then @@ -114,8 +199,8 @@ function CARRIERTRAINER:OnEventBirth(EventData) local _callsign=_unit:GetCallsign() -- Debug output. - local text=string.format("Player %s, callsign %s entered unit %s (UID %d) of group %s (GID %d)", _playername, _callsign, _unitName, _uid, _group:GetName(), _gid) - self:T(CARRIERTRAINER.id..text) + local text=string.format("Player %s, callsign %s entered unit %s (ID=%d) of group %s", _playername, _callsign, _unitName, _uid, _group:GetName()) + self:T(self.lid..text) MESSAGE:New(text, 5):ToAllIf(self.Debug) local playerdata={} --#CARRIERTRAINER.PlayerData @@ -123,7 +208,7 @@ function CARRIERTRAINER:OnEventBirth(EventData) playerdata.callsign=_callsign -- By default, some bomb impact points and do not flare each hit on target. - self.Player[_playername]=playerdata + self.players[_playername]=playerdata -- Start check in zone timer. if self.planes[_uid] ~= true then @@ -134,6 +219,70 @@ function CARRIERTRAINER:OnEventBirth(EventData) end end +--- Initialize player data. +-- @param #CARRIERTRAINER self +-- @param #string unitname Name of the player unit. +-- @return #CARRIERTRAINER.PlayerData Player data. +function CARRIERTRAINER:_NewPlayer(unitname) + local playerData = nil + + local existingData = playerDatas[id] + if(existingData and existingData.unit:IsAlive()) then + playerData = playerDatas[id] + else + playerData = PlayerData:New(id) + end + + playerData:InitNewRound() + + playerDatas[id] = playerData + env.info("Created playerData object for " .. playerData.unit.UnitName) + + MessageToAll( "Pilot ID: " .. id .. ". Welcome back, " .. playerData.callsign .. "! Cleared for approach! TCN 1X, BRC 354 (MAG HDG).", 5, "InitZoneMessage" ) + + playerData.step = 1 -- 1 !! + playerData.highestCarrierXDiff = -9999999 + playerData.secondsStandingStill = 0 + playerData.summary = "SUMMARY:\n" +end + +--- Initialize new approach for player +-- @param #CARRIERTRAINER self +function CARRIERTRAINER:_InitNewRound(playerData) + playerData.score = 0 + playerData.summary = "SUMMARY:\n" + playerData.step = 0 + playerData.longDownwindDone = false + playerData.highestCarrierXDiff = -9999999 + playerData.secondsStandingStill = 0 + playerData.lowestAltitude = 999999 +end + +--- Increase score for this approach. +-- @param #CARRIERTRAINER self +-- @param #CARRIERTRAINER.PlayerData playerData Player data. +-- @param #number amount Amount by which the score is increased. +function CARRIERTRAINER:_IncreaseScore(playerData, amount) + playerData.score = playerData.score + amount +end + +--- Append text to summary text. +-- @param #CARRIERTRAINER self +-- @param #CARRIERTRAINER.PlayerData playerData Player data. +-- @param #string item Text item appeded to the summary. +function CARRIERTRAINER:_AddToSummary(playerData, item) + playerData.summary = playerData.summary .. item .. "\n" +end + +--- Append text to result text. +-- @param #CARRIERTRAINER self +-- @param #CARRIERTRAINER.PlayerData playerData Player data. +-- @param #string item Text item appeded to the result. +function CARRIERTRAINER:_AddToCollectedResult(playerData, item) + playerData.collectedResultString = playerData.collectedResultString .. item .. "\n" +end + + --- Carrier trainer event handler for event birth. -- @param #CARRIERTRAINER self function CARRIERTRAINER:_CheckPlayerStatus() @@ -189,31 +338,6 @@ end -- CARRIER TRAINING functions ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ---- Initialize player data. --- @param #CARRIERTRAINER self -function CARRIERTRAINER:_Init(id) - local playerData = nil - - local existingData = playerDatas[id] - if(existingData and existingData.unit:IsAlive()) then - playerData = playerDatas[id] - else - playerData = PlayerData:New(id) - end - - playerData:InitNewRound() - - playerDatas[id] = playerData - env.info("Created playerData object for " .. playerData.unit.UnitName) - - MessageToAll( "Pilot ID: " .. id .. ". Welcome back, " .. playerData.callsign .. "! Cleared for approach! TCN 1X, BRC 354 (MAG HDG).", 5, "InitZoneMessage" ) - - playerData.step = 1 -- 1 !! - playerData.highestCarrierXDiff = -9999999 - playerData.secondsStandingStill = 0 - playerData.summary = "SUMMARY:\n" -end - --- Start landing pattern. -- @param #CARRIERTRAINER self -- @param #CARRIERTRAINER.PlayerData playerData Player data table. @@ -267,14 +391,18 @@ function CARRIERTRAINER:_Upwind(playerData) hint = "Good altitude on the upwind." end - playerData:IncreaseScore(score) + -- Increase score. + self:_IncreaseScore(playerData, score) self:_SendMessageToPlayer(hint, 8, playerData) self:_PrintAltitudeFeedback(altitude, idealAltitude, playerData) self:_PrintScore(score, playerData, true) - playerData:AddToSummary(hint) + + self:_AddToSummary(playerData, hint) + + -- Set step. playerData.step = 3 end @@ -325,13 +453,13 @@ function CARRIERTRAINER:_Break(playerData, part) hint = "Good altitude in the " .. part .. " break!" end - playerData:IncreaseScore(score) + self:_IncreaseScore(playerData, score) self:_SendMessageToPlayer( hint, 8, playerData ) self:_PrintAltitudeFeedback(altitude, idealAltitude, playerData) self:_PrintScore(score, playerData, true) - playerData:AddToSummary(hint) + self:_AddToSummary(playerData, hint) if (part == "early") then playerData.step = 4 @@ -418,10 +546,10 @@ function CARRIERTRAINER:_Abeam(playerData) self:_SendMessageToPlayer( fullHint, 8, playerData ) self:_SendMessageToPlayer( "(Target: 600 ft and 1.2 nm).", 8, playerData ) - playerData:IncreaseScore(score + distanceScore + onSpeedScore) + self:_IncreaseScore(playerData, score + distanceScore + onSpeedScore) self:_PrintScore(score + distanceScore + onSpeedScore, playerData, true) - playerData:AddToSummary(fullHint .. " (" .. aoaFeedback .. ")") + self:_AddToSummary(playerData, fullHint .. " (" .. aoaFeedback .. ")") playerData.step = 6 end end @@ -443,9 +571,9 @@ function CARRIERTRAINER:_CheckForLongDownwind(playerData) local hint = "Too long downwind. Turn final earlier next time." self:_SendMessageToPlayer( hint, 8, playerData ) local score = -40 - playerData:IncreaseScore(score) + self:_IncreaseScore(playerData, score) self:_PrintScore(score, playerData, true) - playerData:AddToSummary(hint) + self:_AddToSummary(playerData, hint) playerData.longDownwindDone = true end end @@ -500,10 +628,10 @@ function CARRIERTRAINER:_Ninety(playerData) local onSpeedScore = self:_GetOnSpeedScore(aoa) - playerData:IncreaseScore(score + onSpeedScore) + self:_IncreaseScore(playerData, score + onSpeedScore) self:_PrintScore(score + onSpeedScore, playerData, true) - playerData:AddToSummary(hint .. " (" .. aoaFeedback .. ")") + self:_AddToSummary(playerData, hint .. " (" .. aoaFeedback .. ")") playerData.longDownwindDone = true playerData.step = 7 @@ -557,10 +685,10 @@ function CARRIERTRAINER:_Wake(playerData) local onSpeedScore = self:_GetOnSpeedScore(aoa) - playerData:IncreaseScore(score + onSpeedScore) + self:_IncreaseScore(playerData, score + onSpeedScore) self:_PrintScore(score + onSpeedScore, playerData, true) - - playerData:AddToSummary(hint .. " (" .. aoaFeedback .. ")") + self:_AddToSummary(playerData, hint .. " (" .. aoaFeedback .. ")") + playerData.step = 8 end end @@ -626,12 +754,12 @@ function CARRIERTRAINER:_Groove(playerData) local onSpeedScore = self:_GetOnSpeedScore(aoa) - playerData:IncreaseScore(score + onSpeedScore) + self:_IncreaseScore(playerData, score + onSpeedScore) self:_PrintScore(score + onSpeedScore, playerData, true) local fullHint = hint .. " (" .. aoaFeedback .. ")" - playerData:AddToSummary(fullHint) + self:_AddToSummary(playerData, fullHint) playerData.step = 9 end @@ -688,7 +816,7 @@ function CARRIERTRAINER:_Trap(playerData) score = 7 end - playerData:IncreaseScore(score) + self:_IncreaseScore(playerData, score) self:_SendMessageToPlayer( "TRAPPED! " .. wire .. "-wire!", 30, playerData ) self:_PrintScore(score, playerData, false) @@ -798,7 +926,7 @@ end -- @param #CARRIERTRAINER.PlayerData playerData Player data. function CARRIERTRAINER:_SendMessageToPlayer(message, duration, playerData) if playerData.client then - MESSAGE:New( message, duration, ""):ToClient(playerData.client) + MESSAGE:New(message, duration):ToClient(playerData.client) end end @@ -806,7 +934,7 @@ end -- @param #CARRIERTRAINER self -- @param #number score Score. -- @param #CARRIERTRAINER.PlayerData playerData Player data. -function CARRIERTRAINING:_PrintScore(score, playerData, alsoPrintTotalScore) +function CARRIERTRAINER:_PrintScore(score, playerData, alsoPrintTotalScore) if(alsoPrintTotalScore) then self:_SendMessageToPlayer( "Score: " .. score .. " (Total: " .. playerData.score .. ")", 8, playerData ) else From bbd5766688a05018b7a88a15c8962148b154052a Mon Sep 17 00:00:00 2001 From: funkyfranky Date: Thu, 11 Oct 2018 16:17:58 +0200 Subject: [PATCH 03/10] CT general dist --- .../Moose/Functional/CarrierTraining.lua | 124 ++++++++++++++---- .../Moose/Wrapper/Positionable.lua | 55 ++++++++ 2 files changed, 151 insertions(+), 28 deletions(-) diff --git a/Moose Development/Moose/Functional/CarrierTraining.lua b/Moose Development/Moose/Functional/CarrierTraining.lua index 8a218c789..a45b46c24 100644 --- a/Moose Development/Moose/Functional/CarrierTraining.lua +++ b/Moose Development/Moose/Functional/CarrierTraining.lua @@ -28,7 +28,7 @@ -- @field Wrapper.Unit#UNIT carrier Aircraft carrier unit on which we want to practice. -- @field Core.Zone#ZONE_UNIT startZone Zone in which the pattern approach starts. -- @field Core.Zone#ZONE_UNIT giantZone Zone around the carrier to register a new player. --- @field #table plyers Table of players. +-- @field #table players Table of players. -- @extends Core.Fsm#FSM --- Practice Carrier Landings @@ -57,15 +57,20 @@ CARRIERTRAINER = { -- @field #string version CARRIERTRAINER.version="0.0.1" ---- Player data. +--- Player data table holding all important parameters for each player. -- @type CARRIERTRAINER.PlayerData -- @field #number id Player ID. -- @field #string callsign Callsign of player. -- @field #number score Player score. +-- @field #number totalscore Score of all landing attempts. +-- @field #number passes Number of passes. +-- @field #string collectedResultString Results text of all passes. -- @field Wrapper.Unit#UNIT unit Aircraft unit of the player. -- @field #number lowestAltitude Lowest altitude. -- @field #number highestCarrierXDiff -- @field #number secondsStandingStill Time player does not move after a landing attempt. +-- @field #string summary Result summary text. +-- @field Wrapper.Client#CLIENT Client object of player. ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Constructor @@ -78,7 +83,7 @@ CARRIERTRAINER.version="0.0.1" function CARRIERTRAINER:New(carriername) -- Inherit everthing from FSM class. - local self = BASE:Inherit(self, FSM:New()) -- #WAREHOUSE + local self = BASE:Inherit(self, FSM:New()) -- #CARRIERTRAINER -- Set carrier unit. self.carrier=UNIT:FindByName(carriername) @@ -207,6 +212,12 @@ function CARRIERTRAINER:OnEventBirth(EventData) playerdata.callsign=_callsign + if self.player[_playername]==nil then + self:_InitNewPlayer(_unitName) + else + self:_InitNewRound(self.player[_playername]) + end + -- By default, some bomb impact points and do not flare each hit on target. self.players[_playername]=playerdata @@ -223,31 +234,26 @@ end -- @param #CARRIERTRAINER self -- @param #string unitname Name of the player unit. -- @return #CARRIERTRAINER.PlayerData Player data. -function CARRIERTRAINER:_NewPlayer(unitname) - local playerData = nil - - local existingData = playerDatas[id] - if(existingData and existingData.unit:IsAlive()) then - playerData = playerDatas[id] - else - playerData = PlayerData:New(id) - end +function CARRIERTRAINER:_InitNewPlayer(unitname) - playerData:InitNewRound() - - playerDatas[id] = playerData - env.info("Created playerData object for " .. playerData.unit.UnitName) + local playerData={} --#CARRIERTRAINER.PlayerData - MessageToAll( "Pilot ID: " .. id .. ". Welcome back, " .. playerData.callsign .. "! Cleared for approach! TCN 1X, BRC 354 (MAG HDG).", 5, "InitZoneMessage" ) + playerData.unit = UNIT:FindByName(unitname) + playerData.client = CLIENT:FindByName(self.unit.UnitName, nil, true) + playerData.callsign = self.unit:GetCallsign() + playerData.totalScore = 0 + playerData.passes = 0 + playerData.collectedResultString = "" + + playerData=self:_InitNewRound(playerData) - playerData.step = 1 -- 1 !! - playerData.highestCarrierXDiff = -9999999 - playerData.secondsStandingStill = 0 - playerData.summary = "SUMMARY:\n" + return playerData end ---- Initialize new approach for player +--- Initialize new approach for player by resetting parmeters to initial values. -- @param #CARRIERTRAINER self +-- @param #CARRIERTRAINER.PlayerData playerData Player data. +-- @return #CARRIERTRAINER.PlayerData Initialized player data. function CARRIERTRAINER:_InitNewRound(playerData) playerData.score = 0 playerData.summary = "SUMMARY:\n" @@ -256,6 +262,7 @@ function CARRIERTRAINER:_InitNewRound(playerData) playerData.highestCarrierXDiff = -9999999 playerData.secondsStandingStill = 0 playerData.lowestAltitude = 999999 + return playerData end --- Increase score for this approach. @@ -307,7 +314,9 @@ function CARRIERTRAINER:_CheckPlayerStatus() self:_CheckForLongDownwind(playerData) end - if playerData.step == 1 and unit:IsInZone(self.startZone) then + if playerData.step==0 and unit:IsInZone(self.giantZone and unit:InAir()) then + self:_NewRound(unitname) + elseif playerData.step == 1 and unit:IsInZone(self.startZone) then self:_Start(playerData) elseif playerData.step == 2 and unit:IsInZone(self.giantZone) then self:_Upwind(playerData) @@ -338,6 +347,18 @@ end -- CARRIER TRAINING functions ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +--- Initialize player data. +-- @param #CARRIERTRAINER self +-- @param #CARRIERTRAINER.PlayerData playerData Player data. +function CARRIERTRAINER:_NewRound(playerData) + local playerData = nil + + MessageToAll( "Welcome back, " .. playerData.callsign .. "! Cleared for approach! TCN 1X, BRC 354 (MAG HDG).", 5, "InitZoneMessage" ) + + self:_InitNewRound(playerData) + playerData.step = 1 +end + --- Start landing pattern. -- @param #CARRIERTRAINER self -- @param #CARRIERTRAINER.PlayerData playerData Player data table. @@ -359,6 +380,10 @@ function CARRIERTRAINER:_Upwind(playerData) local diffZ = position.z - carrierPosition.z local diffX = position.x - carrierPosition.x + + -- Get distances between carrier and player unit (parallel and perpendicular to direction of movement of carrier) + diffZ, diffX = self:_GetDistances(playerData.unit) + if(diffZ > 500 or diffZ < 0 or diffX < -4000) then self:_AbortPattern(playerData) return @@ -374,10 +399,10 @@ function CARRIERTRAINER:_Upwind(playerData) local hint = "" local score = 0 - if(altitude > 850) then + if (altitude > 850) then score = 5 hint = "You're high on the upwind." - elseif(altitude > 830) then + elseif (altitude > 830) then score = 7 hint = "You're slightly high on the upwind." elseif (altitude < 750) then @@ -406,6 +431,22 @@ function CARRIERTRAINER:_Upwind(playerData) playerData.step = 3 end +--- Calculate distances between carrier and player unit. +-- @param #CARRIERTRAINER self +-- @param Wrapper.Unit#UNIT unit Player unit +-- @return #number Distance in the direction of the orientation of the carrier. +-- @return #number Distance perpendicular to the orientation of the carrier. +function CARRIERTRAINER:_GetDistances(unit) + local a=self.carrier:GetVec3() + local b=unit:GetVec3() + local c={x=b.x-a.x, y=0, z=b.z-a.z} + local x=self.carrier:GetOrientationX() + local dz=UTILS.VecDot(x,c) + local alpha=math.acos(UTILS.VecDot(c,x)/UTILS.VecNorm(c)) + local dx=c*math.sin(alpha) + return dz,dx +end + --- Break. -- @param #CARRIERTRAINER self -- @param #CARRIERTRAINER.PlayerData playerData Player data table. @@ -418,6 +459,9 @@ function CARRIERTRAINER:_Break(playerData, part) local diffZ = playerPosition.z - carrierPosition.z local diffX = playerPosition.x - carrierPosition.x + -- Get distances between carrier and player unit (parallel and perpendicular to direction of movement of carrier) + diffZ, diffX = self:_GetDistances(playerData.unit) + if(diffZ > 1500 or diffZ < -3700 or diffX < -500) then self:_AbortPattern(playerData) return @@ -425,7 +469,7 @@ function CARRIERTRAINER:_Break(playerData, part) local limit = -370 - if (part == "late") then + if part == "late" then limit = -1470 end @@ -478,6 +522,10 @@ function CARRIERTRAINER:_Abeam(playerData) local diffZ = playerPosition.z - carrierPosition.z local diffX = playerPosition.x - carrierPosition.x + + -- Get distances between carrier and player unit (parallel and perpendicular to direction of movement of carrier) + diffZ, diffX = self:_GetDistances(playerData.unit) + if(diffZ > -1000 or diffZ < -3700) then self:_AbortPattern(playerData) return @@ -563,6 +611,11 @@ function CARRIERTRAINER:_CheckForLongDownwind(playerData) local limit = 1500 + -- Get distances between carrier and player unit (parallel and perpendicular to direction of movement of carrier) + local diffZ, diffX = self:_GetDistances(playerData.unit) + + --TODO if diffX + if carrierPosition.x - playerPosition.x > limit then --local heading = math.deg(mist.getHeading(playerData.mistUnit)) local heading = playerData.unit:GetHeading() @@ -588,6 +641,10 @@ function CARRIERTRAINER:_Ninety(playerData) local diffZ = playerPosition.z - carrierPosition.z local diffX = playerPosition.x - carrierPosition.x + + -- Get distances between carrier and player unit (parallel and perpendicular to direction of movement of carrier) + diffZ, diffX = self:_GetDistances(playerData.unit) + if(diffZ < -3700 or diffX < -3700 or diffX > 0) then self:_AbortPattern(playerData) return @@ -643,10 +700,14 @@ end -- @param #CARRIERTRAINER.PlayerData playerData Player data table. function CARRIERTRAINER:_Wake(playerData) local playerPosition = playerData.unit:GetVec3() - local carrierPosition = carrier:GetVec3() + local carrierPosition = self.carrier:GetVec3() local diffZ = playerPosition.z - carrierPosition.z local diffX = playerPosition.x - carrierPosition.x + + -- Get distances between carrier and player unit (parallel and perpendicular to direction of movement of carrier) + diffZ, diffX = self:_GetDistances(playerData.unit) + if(diffZ < -2000 or diffX < -4000 or diffX > 0) then self:_AbortPattern(playerData) return @@ -702,6 +763,10 @@ function CARRIERTRAINER:_Groove(playerData) local diffX = playerPosition.x - (carrierPosition.x - 100) local diffZ = playerPosition.z - carrierPosition.z + + --TODO -100?! + -- Get distances between carrier and player unit (parallel and perpendicular to direction of movement of carrier) + diffZ, diffX = self:_GetDistances(playerData.unit) if(diffX > 0 or diffX < -4000) then self:_AbortPattern(playerData) @@ -779,6 +844,9 @@ function CARRIERTRAINER:_Trap(playerData) local diffZ = playerPosition.z - carrierPosition.z local diffX = playerPosition.x - carrierPosition.x + -- Get distances between carrier and player unit (parallel and perpendicular to direction of movement of carrier) + diffZ, diffX = self:_GetDistances(playerData.unit) + if(diffZ < -2000 or diffZ > 2000 or diffX < -3000) then self:_AbortPattern(playerData) return @@ -837,7 +905,7 @@ function CARRIERTRAINER:_Trap(playerData) local wire = 0 local hint = "" local score = 0 - if(playerData.lowestAltitude < 23) then + if (playerData.lowestAltitude < 23) then hint = "You boltered." else hint = "You were waved off." diff --git a/Moose Development/Moose/Wrapper/Positionable.lua b/Moose Development/Moose/Wrapper/Positionable.lua index f97f33dfd..78104237d 100644 --- a/Moose Development/Moose/Wrapper/Positionable.lua +++ b/Moose Development/Moose/Wrapper/Positionable.lua @@ -140,6 +140,61 @@ function POSITIONABLE:GetPosition() return nil end +--- Returns a {@DCS#Vec3} table of the objects current orientation in 3D space. X, Y, Z values are unit vectors defining the objects orientation. +-- X is the orientation parallel to the movement of the object, Z perpendicular and Y vertical orientation. +-- @param Wrapper.Positionable#POSITIONABLE self +-- @return DCS#Vec3 X orientation, i.e. parallel to the direction of movement. +-- @return DCS#Vec3 Y orientation, i.e. vertical. +-- @return DCS#Vec3 Z orientation, i.e. perpendicular to the direction of movement. +function POSITIONABLE:GetOrientation() + local position=self:GetPosition() + if position then + return position.x, position.y, position.z + else + BASE:E( { "Cannot GetOrientation", Positionable = self, Alive = self:IsAlive() } ) + return nil, nil, nil + end +end + +--- Returns a {@DCS#Vec3} table of the objects current X orientation in 3D space, i.e. along the direction of movement. +-- @param Wrapper.Positionable#POSITIONABLE self +-- @return DCS#Vec3 X orientation, i.e. parallel to the direction of movement. +function POSITIONABLE:GetOrientationX() + local position=self:GetPosition() + if position then + return position.x + else + BASE:E( { "Cannot GetOrientationX", Positionable = self, Alive = self:IsAlive() } ) + return nil + end +end + +--- Returns a {@DCS#Vec3} table of the objects current Y orientation in 3D space, i.e. vertical orientation. +-- @param Wrapper.Positionable#POSITIONABLE self +-- @return DCS#Vec3 Y orientation, i.e. vertical. +function POSITIONABLE:GetOrientationY() + local position=self:GetPosition() + if position then + return position.y + else + BASE:E( { "Cannot GetOrientationY", Positionable = self, Alive = self:IsAlive() } ) + return nil + end +end + +--- Returns a {@DCS#Vec3} table of the objects current Z orientation in 3D space, i.e. perpendicular to direction of movement. +-- @param Wrapper.Positionable#POSITIONABLE self +-- @return DCS#Vec3 Z orientation, i.e. perpendicular to movement. +function POSITIONABLE:GetOrientationZ() + local position=self:GetPosition() + if position then + return position.z + else + BASE:E( { "Cannot GetOrientationZ", Positionable = self, Alive = self:IsAlive() } ) + return nil + end +end + --- Returns the @{DCS#Position3} position vectors indicating the point and direction vectors in 3D of the POSITIONABLE within the mission. -- @param Wrapper.Positionable#POSITIONABLE self -- @return DCS#Position The 3D position vectors of the POSITIONABLE. From eda359e20fd32152239d792ce07cebb040be1c30 Mon Sep 17 00:00:00 2001 From: Frank Date: Fri, 12 Oct 2018 00:11:39 +0200 Subject: [PATCH 04/10] CT v0.0.2 --- ...CarrierTraining.lua => CarrierTrainer.lua} | 104 +++++++++++------- Moose Development/Moose/Utilities/Utils.lua | 18 +-- .../Moose/Wrapper/Positionable.lua | 2 +- 3 files changed, 73 insertions(+), 51 deletions(-) rename Moose Development/Moose/Functional/{CarrierTraining.lua => CarrierTrainer.lua} (93%) diff --git a/Moose Development/Moose/Functional/CarrierTraining.lua b/Moose Development/Moose/Functional/CarrierTrainer.lua similarity index 93% rename from Moose Development/Moose/Functional/CarrierTraining.lua rename to Moose Development/Moose/Functional/CarrierTrainer.lua index a45b46c24..274dcebe4 100644 --- a/Moose Development/Moose/Functional/CarrierTraining.lua +++ b/Moose Development/Moose/Functional/CarrierTrainer.lua @@ -50,12 +50,12 @@ CARRIERTRAINER = { carrier = nil, startZone = nil, giantZone = nil, - players = nil, + players = {}, } --- Carrier trainer class version. -- @field #string version -CARRIERTRAINER.version="0.0.1" +CARRIERTRAINER.version="0.0.2" --- Player data table holding all important parameters for each player. -- @type CARRIERTRAINER.PlayerData @@ -146,14 +146,13 @@ end function CARRIERTRAINER:onafterStart(From, Event, To) -- Events are handled my MOOSE. - self:I(self.lid..string.format"Starting Carrier Training %s for carrier unit %s.", CARRIERTRAINER.version, self.carrier:GetName()) + self:I(self.lid..string.format("Starting Carrier Training %s for carrier unit %s.", CARRIERTRAINER.version, self.carrier:GetName())) -- Handle events. self:HandleEvent(EVENTS.Birth) - - -- Init status checkss - self:__Status(-1) + -- Init status check + self:__Status(5) end --- On after Status event. Checks player status. @@ -167,7 +166,7 @@ function CARRIERTRAINER:onafterStatus(From, Event, To) self:_CheckPlayerStatus() -- Call status again in one second. - self:_Status(-1) + self:__Status(-1) end --- On after Stop event. Unhandle events and stop status updates. @@ -208,25 +207,13 @@ function CARRIERTRAINER:OnEventBirth(EventData) self:T(self.lid..text) MESSAGE:New(text, 5):ToAllIf(self.Debug) - local playerdata={} --#CARRIERTRAINER.PlayerData - - playerdata.callsign=_callsign - - if self.player[_playername]==nil then - self:_InitNewPlayer(_unitName) + -- + if self.players[_playername]==nil then + self.players[_playername]=self:_InitNewPlayer(_unitName) else - self:_InitNewRound(self.player[_playername]) + self:_InitNewRound(self.players[_playername]) end - - -- By default, some bomb impact points and do not flare each hit on target. - self.players[_playername]=playerdata - -- Start check in zone timer. - if self.planes[_uid] ~= true then - SCHEDULER:New(nil, self._CheckInZone, {self, EventData.IniUnitName}, 1, 1) - self.planes[_uid] = true - end - end end @@ -239,8 +226,8 @@ function CARRIERTRAINER:_InitNewPlayer(unitname) local playerData={} --#CARRIERTRAINER.PlayerData playerData.unit = UNIT:FindByName(unitname) - playerData.client = CLIENT:FindByName(self.unit.UnitName, nil, true) - playerData.callsign = self.unit:GetCallsign() + playerData.client = CLIENT:FindByName(playerData.unit.UnitName, nil, true) + playerData.callsign = playerData.unit:GetCallsign() playerData.totalScore = 0 playerData.passes = 0 playerData.collectedResultString = "" @@ -294,19 +281,26 @@ end -- @param #CARRIERTRAINER self function CARRIERTRAINER:_CheckPlayerStatus() + if self.players==nil then + self:I(self.lid.."No players yet.") + return + end + -- Loop over all players. - for _playerName,_playerData in pairs(self.Player) do + for _playerName,_playerData in pairs(self.players) do local playerData = _playerData --#CARRIERTRAINER.PlayerData if playerData then + self:I("player "..playerData.callsign) + -- Player unit. local unit = playerData.unit if unit:IsAlive() then if unit:IsInZone(self.giantZone) then - --Tick(playerData) + self:_DetailedPlayerStatus(playerData) end -- Check long down wind leg. @@ -314,8 +308,8 @@ function CARRIERTRAINER:_CheckPlayerStatus() self:_CheckForLongDownwind(playerData) end - if playerData.step==0 and unit:IsInZone(self.giantZone and unit:InAir()) then - self:_NewRound(unitname) + if playerData.step==0 and unit:IsInZone(self.giantZone) and unit:InAir() then + self:_NewRound(playerData) elseif playerData.step == 1 and unit:IsInZone(self.startZone) then self:_Start(playerData) elseif playerData.step == 2 and unit:IsInZone(self.giantZone) then @@ -343,6 +337,34 @@ function CARRIERTRAINER:_CheckPlayerStatus() end +--- Provide info about player status on the fly. +-- @param #CARRIERTRAINER self +-- @param #CARRIERTRAINER.PlayerData playerData Player data. +function CARRIERTRAINER:_DetailedPlayerStatus(playerData) + + local unit=playerData.unit + + local aoa=unit:GetAoA() + local dist=playerData.unit:GetCoordinate():Get2DDistance(self.carrier:GetCoordinate()) + local dz,dx=self:_GetDistances(unit) + + local text=string.format("%s, current AoA=%.1f\n", playerData.callsign, aoa) + text=text..string.format("Carrier distance: d=%d m\n", dist) + text=text..string.format("Carrier distance: z=%d m x=%d m sum=%d", dz, dx, math.abs(dz)+math.abs(dx)) + + -- 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 + + -- Get distances between carrier and player unit (parallel and perpendicular to direction of movement of carrier) + diffZ, diffX = self:_GetDistances(playerData.unit) + + MESSAGE:New(text, 5, nil ,true):ToClient(playerData.client) +end + ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- CARRIER TRAINING functions ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -351,9 +373,10 @@ end -- @param #CARRIERTRAINER self -- @param #CARRIERTRAINER.PlayerData playerData Player data. function CARRIERTRAINER:_NewRound(playerData) - local playerData = nil - MessageToAll( "Welcome back, " .. playerData.callsign .. "! Cleared for approach! TCN 1X, BRC 354 (MAG HDG).", 5, "InitZoneMessage" ) + local text=string.format("Welcome back, %s! Cleared for approach. TCN 1X, BRC 354 (MAG HDG).", playerData.callsign) + MESSAGE:New(text, 5):ToClient(playerData.client) + --MessageToAll( "Welcome back, " .. playerData.callsign .. "! Cleared for approach! TCN 1X, BRC 354 (MAG HDG).", 5, "InitZoneMessage" ) self:_InitNewRound(playerData) playerData.step = 1 @@ -423,8 +446,7 @@ function CARRIERTRAINER:_Upwind(playerData) self:_PrintAltitudeFeedback(altitude, idealAltitude, playerData) self:_PrintScore(score, playerData, true) - - + self:_AddToSummary(playerData, hint) -- Set step. @@ -443,7 +465,7 @@ function CARRIERTRAINER:_GetDistances(unit) local x=self.carrier:GetOrientationX() local dz=UTILS.VecDot(x,c) local alpha=math.acos(UTILS.VecDot(c,x)/UTILS.VecNorm(c)) - local dx=c*math.sin(alpha) + local dx=UTILS.VecNorm(c)*math.sin(alpha) return dz,dx end @@ -526,7 +548,7 @@ function CARRIERTRAINER:_Abeam(playerData) -- Get distances between carrier and player unit (parallel and perpendicular to direction of movement of carrier) diffZ, diffX = self:_GetDistances(playerData.unit) - if(diffZ > -1000 or diffZ < -3700) then + if (diffZ > -1000 or diffZ < -3700) then self:_AbortPattern(playerData) return end @@ -617,10 +639,10 @@ function CARRIERTRAINER:_CheckForLongDownwind(playerData) --TODO if diffX if carrierPosition.x - playerPosition.x > limit then - --local heading = math.deg(mist.getHeading(playerData.mistUnit)) + local heading = playerData.unit:GetHeading() - if(heading > 170) then + if (heading > 170) then local hint = "Too long downwind. Turn final earlier next time." self:_SendMessageToPlayer( hint, 8, playerData ) local score = -40 @@ -777,7 +799,7 @@ function CARRIERTRAINER:_Groove(playerData) local hint = "You're too far left and never reached the groove." self:_SendMessageToPlayer( hint, 8, playerData ) self:_PrintScore(0, playerData, true) - playerData:AddToSummary(hint) + self:_AddToSummary(playerData, hint) playerData.step = 9 else local limitDeg = 8.0 @@ -893,7 +915,7 @@ function CARRIERTRAINER:_Trap(playerData) local fullHint = "Trapped catching the " .. wire .. "-wire." - playerData:AddToSummary(fullHint) + self:_AddToSummary(playerData, fullHint) self:_PrintFinalScore(playerData, 60, wire) self:_HandleCollectedResult(playerData, wire) @@ -916,7 +938,7 @@ function CARRIERTRAINER:_Trap(playerData) self:_SendMessageToPlayer( hint, 8, playerData ) self:_PrintScore(score, playerData, true) - playerData:AddToSummary(hint) + self:_AddToSummary(playerData, hint) self:_PrintFinalScore(playerData, 60, wire) self:_HandleCollectedResult(playerData, wire) @@ -1064,7 +1086,7 @@ end -- @param #CARRIERTRAINER.PlayerData playerData Player data. function CARRIERTRAINER:_AbortPattern(playerData) self:_SendMessageToPlayer( "You're too far from where you should be. Abort approach!", 15, playerData ) - playerData:AddToSummary("Approach aborted.") + self:_AddToSummary(playerData, "Approach aborted.") self:_PrintFinalScore(playerData, 30, -2) self:_HandleCollectedResult(playerData, -2) playerData.step = 0 diff --git a/Moose Development/Moose/Utilities/Utils.lua b/Moose Development/Moose/Utilities/Utils.lua index 5c7cce48a..42dce2b58 100644 --- a/Moose Development/Moose/Utilities/Utils.lua +++ b/Moose Development/Moose/Utilities/Utils.lua @@ -657,24 +657,24 @@ function UTILS.Randomize(value, fac, lower, upper) return r end ---- Calculate the dot (scalar) product of two vectors. --- @param DCS#Vec3 a Vector in 3D with x,y,z components. --- @param DCS#Vec3 b Vector in 3D with x,y,z components. +--- Calculate the [dot product](https://en.wikipedia.org/wiki/Dot_product) of two vectors. The result is a number. +-- @param DCS#Vec3 a Vector in 3D with x, y, z components. +-- @param DCS#Vec3 b Vector in 3D with x, y, z components. -- @return #number Scalar product of the two vectors a*b. function UTILS.VecDot(a, b) return a.x*b.x + a.y*b.y + a.z*b.z end ---- Calculate the eucledian norm (length) of a 3D vector. --- @param DCS#Vec3 a Vector in 3D with x,y,z components. +--- Calculate the [euclidean norm](https://en.wikipedia.org/wiki/Euclidean_distance) (length) of a 3D vector. +-- @param DCS#Vec3 a Vector in 3D with x, y, z components. -- @return #number Norm of the vector. function UTILS.VecNorm(a) - return math.sqrt(UTILS.DotProduct(a, a)) + return math.sqrt(UTILS.VecDot(a, a)) end ---- Calculate the cross product of two 3D vectors. --- @param DCS#Vec3 a Vector in 3D with x,y,z components. --- @param DCS#Vec3 b Vector in 3D with x,y,z components. +--- Calculate the [cross product](https://en.wikipedia.org/wiki/Cross_product) of two 3D vectors. The result is a 3D vector. +-- @param DCS#Vec3 a Vector in 3D with x, y, z components. +-- @param DCS#Vec3 b Vector in 3D with x, y, z components. -- @return DCS#Vec3 Vector function UTILS.VecCross(a, b) return {x=a.y*b.z - a.z*b.y, y=a.z*b.x - a.x*b.z, z=a.x*b.y - a.y*b.x} diff --git a/Moose Development/Moose/Wrapper/Positionable.lua b/Moose Development/Moose/Wrapper/Positionable.lua index 78104237d..2b8170ba9 100644 --- a/Moose Development/Moose/Wrapper/Positionable.lua +++ b/Moose Development/Moose/Wrapper/Positionable.lua @@ -667,7 +667,7 @@ function POSITIONABLE:GetAoA() if unitpos then -- Get velocity vector of the unit. - local unitvel = self:GetVelocity() + local unitvel = self:GetVelocityVec3() if unitvel and UTILS.VecNorm(unitvel)~=0 then From 33509a49e0c6e190b9e1883ee5f6cffd1d2132a4 Mon Sep 17 00:00:00 2001 From: funkyfranky Date: Fri, 12 Oct 2018 16:16:39 +0200 Subject: [PATCH 05/10] CT v0.0.3 --- .../Moose/Functional/CarrierTrainer.lua | 227 +++++++++++------- .../Moose/Wrapper/Positionable.lua | 99 ++++++++ 2 files changed, 245 insertions(+), 81 deletions(-) diff --git a/Moose Development/Moose/Functional/CarrierTrainer.lua b/Moose Development/Moose/Functional/CarrierTrainer.lua index 274dcebe4..1f9467890 100644 --- a/Moose Development/Moose/Functional/CarrierTrainer.lua +++ b/Moose Development/Moose/Functional/CarrierTrainer.lua @@ -55,7 +55,7 @@ CARRIERTRAINER = { --- Carrier trainer class version. -- @field #string version -CARRIERTRAINER.version="0.0.2" +CARRIERTRAINER.version="0.0.3" --- Player data table holding all important parameters for each player. -- @type CARRIERTRAINER.PlayerData @@ -72,6 +72,7 @@ CARRIERTRAINER.version="0.0.2" -- @field #string summary Result summary text. -- @field Wrapper.Client#CLIENT Client object of player. + ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Constructor ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -281,11 +282,6 @@ end -- @param #CARRIERTRAINER self function CARRIERTRAINER:_CheckPlayerStatus() - if self.players==nil then - self:I(self.lid.."No players yet.") - return - end - -- Loop over all players. for _playerName,_playerData in pairs(self.players) do local playerData = _playerData --#CARRIERTRAINER.PlayerData @@ -303,11 +299,6 @@ function CARRIERTRAINER:_CheckPlayerStatus() self:_DetailedPlayerStatus(playerData) end - -- Check long down wind leg. - if playerData.step == 6 and not playerData.longDownwindDone and unit:IsInZone(self.giantZone) then - self:_CheckForLongDownwind(playerData) - end - if playerData.step==0 and unit:IsInZone(self.giantZone) and unit:InAir() then self:_NewRound(playerData) elseif playerData.step == 1 and unit:IsInZone(self.startZone) then @@ -321,6 +312,10 @@ function CARRIERTRAINER:_CheckPlayerStatus() elseif playerData.step == 5 and unit:IsInZone(self.giantZone) then self:_Abeam(playerData) elseif playerData.step == 6 and unit:IsInZone(self.giantZone) then + -- Check long down wind leg. + if not playerData.longDownwindDone then + self:_CheckForLongDownwind(playerData) + end self:_Ninety(playerData) elseif playerData.step == 7 and unit:IsInZone(self.giantZone) then self:_Wake(playerData) @@ -337,6 +332,37 @@ function CARRIERTRAINER:_CheckPlayerStatus() end +--- Get name of the current pattern step. +-- @param #CARRIERTRAINER self +-- @param #number step Step +-- @return #string Name of the step +function CARRIERTRAINER:_StepName(step) + + local name="unknown" + if step==0 then + name="Unregistered" + elseif step==1 then + name="entering pattern" + elseif step==2 then + name="on upwind leg" + elseif step==3 then + name="early break" + elseif step==4 then + name="late break" + elseif step==5 then + name="abeam" + elseif step==6 then + name="at the wake" + elseif step==7 then + name="at the ninety" + elseif step==8 then + name="in the groove" + elseif step==9 then + name="trapped" + end + +end + --- Provide info about player status on the fly. -- @param #CARRIERTRAINER self -- @param #CARRIERTRAINER.PlayerData playerData Player data. @@ -345,12 +371,11 @@ function CARRIERTRAINER:_DetailedPlayerStatus(playerData) local unit=playerData.unit local aoa=unit:GetAoA() + local yaw=unit:GetYaw() + local roll=unit:GetRoll() + local pitch=unit:GetPitch() local dist=playerData.unit:GetCoordinate():Get2DDistance(self.carrier:GetCoordinate()) - local dz,dx=self:_GetDistances(unit) - - local text=string.format("%s, current AoA=%.1f\n", playerData.callsign, aoa) - text=text..string.format("Carrier distance: d=%d m\n", dist) - text=text..string.format("Carrier distance: z=%d m x=%d m sum=%d", dz, dx, math.abs(dz)+math.abs(dx)) + local dx,dz=self:_GetDistances(unit) -- Player and carrier position vector. local playerPosition = playerData.unit:GetVec3() @@ -358,11 +383,17 @@ function CARRIERTRAINER:_DetailedPlayerStatus(playerData) local diffZ = playerPosition.z - carrierPosition.z local diffX = playerPosition.x - carrierPosition.x - - -- Get distances between carrier and player unit (parallel and perpendicular to direction of movement of carrier) - diffZ, diffX = self:_GetDistances(playerData.unit) - MESSAGE:New(text, 5, nil ,true):ToClient(playerData.client) + local heading=unit:GetCoordinate():HeadingTo(self.startZone:GetCoordinate()) + + local text=string.format("%s, current AoA=%.1f\n", playerData.callsign, aoa) + text=text..string.format("roll=%.1f yaw=%.1f pitch=%.1f\n", roll, yaw, pitch) + 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)", 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 ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -386,7 +417,7 @@ end -- @param #CARRIERTRAINER self -- @param #CARRIERTRAINER.PlayerData playerData Player data table. function CARRIERTRAINER:_Start(playerData) - local hint = "Entering the pattern, " .. playerData.callsign .. "! Aim for 800 feet and 350-400 kts on the upwind." + local hint = string.format("Entering the pattern, %s! Aim for 800 feet and 350-400 kts on the upwind.", playerData.callsign) self:_SendMessageToPlayer(hint, 8, playerData) playerData.score = 0 playerData.step = 2 @@ -405,52 +436,56 @@ function CARRIERTRAINER:_Upwind(playerData) local diffX = position.x - carrierPosition.x -- Get distances between carrier and player unit (parallel and perpendicular to direction of movement of carrier) - diffZ, diffX = self:_GetDistances(playerData.unit) + diffX, diffZ = self:_GetDistances(playerData.unit) - if(diffZ > 500 or diffZ < 0 or diffX < -4000) then + -- Too far away. + -- Should be between 0-500 meters right of carrier. + -- TODO Should be withing 4 km behind carrier. Why? + if (diffZ > 500 or diffZ < 0 or diffX < -4000) then self:_AbortPattern(playerData) return end - if (diffX < 0) then - return - end + -- Now before the boat. + if diffX > 0 then - local idealAltitude = 800 - local altitude = UTILS.Round( UTILS.MetersToFeet( position.y ) ) - - local hint = "" - local score = 0 - - if (altitude > 850) then - score = 5 - hint = "You're high on the upwind." - elseif (altitude > 830) then - score = 7 - hint = "You're slightly high on the upwind." - elseif (altitude < 750) then - score = 5 - hint = "You're low on the upwind." - elseif (altitude < 770) then - score = 7 - hint = "You're slightly low on the upwind." - else - score = 10 - hint = "Good altitude on the upwind." - end - - -- Increase score. - self:_IncreaseScore(playerData, score) - - self:_SendMessageToPlayer(hint, 8, playerData) - - self:_PrintAltitudeFeedback(altitude, idealAltitude, playerData) - self:_PrintScore(score, playerData, true) + local idealAltitude = 800 + local altitude = UTILS.MetersToFeet(position.y) - self:_AddToSummary(playerData, hint) - - -- Set step. - playerData.step = 3 + local hint = "" + local score = 0 + + if (altitude > 850) then + score = 5 + hint = "You're high on the upwind." + elseif (altitude > 830) then + score = 7 + hint = "You're slightly high on the upwind." + elseif (altitude < 750) then + score = 5 + hint = "You're low on the upwind." + elseif (altitude < 770) then + score = 7 + hint = "You're slightly low on the upwind." + else + score = 10 + hint = "Good altitude on the upwind." + end + + -- Increase score. + self:_IncreaseScore(playerData, score) + + self:_SendMessageToPlayer(hint, 8, playerData) + + self:_PrintAltitudeFeedback(altitude, idealAltitude, playerData) + self:_PrintScore(score, playerData, true) + + self:_AddToSummary(playerData, hint) + + -- Set step. + playerData.step = 3 + + end end --- Calculate distances between carrier and player unit. @@ -482,20 +517,31 @@ function CARRIERTRAINER:_Break(playerData, part) local diffX = playerPosition.x - carrierPosition.x -- Get distances between carrier and player unit (parallel and perpendicular to direction of movement of carrier) - diffZ, diffX = self:_GetDistances(playerData.unit) + diffX, diffZ = self:_GetDistances(playerData.unit) - if(diffZ > 1500 or diffZ < -3700 or diffX < -500) then + -- Abort when + -- > 1.5 km right of carrier + -- > 3.7 km left of carrier + -- > 0.5 km behind carrier + if (diffZ > 1500 or diffZ < -3700 or diffX < -500) then self:_AbortPattern(playerData) return end + + -- Break + -- z= -370 + -- y= 800 + -- x> -500 - local limit = -370 + local limit = -370 --0.2 NM if part == "late" then - limit = -1470 + limit = -1470 -- 0.8 NM end + -- if diffZ < limit then + local idealAltitude = 800 local altitude = UTILS.Round( UTILS.MetersToFeet( playerPosition.y ) ) @@ -546,17 +592,27 @@ function CARRIERTRAINER:_Abeam(playerData) local diffX = playerPosition.x - carrierPosition.x -- Get distances between carrier and player unit (parallel and perpendicular to direction of movement of carrier) - diffZ, diffX = self:_GetDistances(playerData.unit) + diffX, diffZ = self:_GetDistances(playerData.unit) + -- Abort if + -- less than 1.0 km left of boat (no closer than 1 km to boat + -- more than 3.7 km left of boat if (diffZ > -1000 or diffZ < -3700) then self:_AbortPattern(playerData) return end + -- Abeam pos: + -- x= -200 + -- z=-2160 + -- y= 600 + + -- Abeam pos 200 meters behind ship local limit = -200 if diffX < limit then - --local aoa = math.deg(mist.getAoA(playerData.mistUnit)) + + -- Get AoA. local aoa = playerData.unit:GetAoA() local aoaFeedback = self:_PrintAoAFeedback(aoa, 8.1, playerData) @@ -594,7 +650,7 @@ function CARRIERTRAINER:_Abeam(playerData) local roundedNm = UTILS.Round(nm, 2) - if(nm < 1.0) then + if (nm < 1.0) then distanceScore = 0 distanceHint = "too close to the boat (" .. roundedNm .. " nm)" elseif(nm < 1.1) then @@ -628,21 +684,23 @@ end -- @param #CARRIERTRAINER self -- @param #CARRIERTRAINER.PlayerData playerData Player data table. function CARRIERTRAINER:_CheckForLongDownwind(playerData) + local playerPosition = playerData.unit:GetVec3() local carrierPosition = self.carrier:GetVec3() - local limit = 1500 + local limit = -1500 -- Get distances between carrier and player unit (parallel and perpendicular to direction of movement of carrier) - local diffZ, diffX = self:_GetDistances(playerData.unit) + local diffX, diffZ = self:_GetDistances(playerData.unit) - --TODO if diffX + --if carrierPosition.x - playerPosition.x > limit then + if diffX > limit then - if carrierPosition.x - playerPosition.x > limit then - - local heading = playerData.unit:GetHeading() + local headingPlayer = playerData.unit:GetHeading() + local headingCarrier = self.carrier:GetHeading() + --TODO: Take carrier heading != 0 into account! + if (headingPlayer > 170) then - if (heading > 170) then local hint = "Too long downwind. Turn final earlier next time." self:_SendMessageToPlayer( hint, 8, playerData ) local score = -40 @@ -651,6 +709,7 @@ function CARRIERTRAINER:_CheckForLongDownwind(playerData) self:_AddToSummary(playerData, hint) playerData.longDownwindDone = true end + end end @@ -665,7 +724,7 @@ function CARRIERTRAINER:_Ninety(playerData) local diffX = playerPosition.x - carrierPosition.x -- Get distances between carrier and player unit (parallel and perpendicular to direction of movement of carrier) - diffZ, diffX = self:_GetDistances(playerData.unit) + diffX, diffZ = self:_GetDistances(playerData.unit) if(diffZ < -3700 or diffX < -3700 or diffX > 0) then self:_AbortPattern(playerData) @@ -728,7 +787,7 @@ function CARRIERTRAINER:_Wake(playerData) local diffX = playerPosition.x - carrierPosition.x -- Get distances between carrier and player unit (parallel and perpendicular to direction of movement of carrier) - diffZ, diffX = self:_GetDistances(playerData.unit) + diffX, diffZ = self:_GetDistances(playerData.unit) if(diffZ < -2000 or diffX < -4000 or diffX > 0) then self:_AbortPattern(playerData) @@ -788,20 +847,25 @@ function CARRIERTRAINER:_Groove(playerData) --TODO -100?! -- Get distances between carrier and player unit (parallel and perpendicular to direction of movement of carrier) - diffZ, diffX = self:_GetDistances(playerData.unit) + diffX, diffZ = self:_GetDistances(playerData.unit) - if(diffX > 0 or diffX < -4000) then + diffX=diffX+100 + + -- In front of carrier or more than 4 km behind carrier. + if (diffX > 0 or diffX < -4000) then self:_AbortPattern(playerData) return end - if(diffX > -500) then --Reached in close before groove + --TODO: + if (diffX > -500) then --Reached in close before groove local hint = "You're too far left and never reached the groove." self:_SendMessageToPlayer( hint, 8, playerData ) self:_PrintScore(0, playerData, true) self:_AddToSummary(playerData, hint) playerData.step = 9 - else + else + local limitDeg = 8.0 local fraction = diffZ / (-diffX) @@ -958,7 +1022,8 @@ end -- @param #number idealAltitude Ideal altitude. -- @param #CARRIERTRAINER.PlayerData playerData Player data. function CARRIERTRAINER:_PrintAltitudeFeedback(altitude, idealAltitude, playerData) - self:_SendMessageToPlayer( "Alt: " .. altitude .. " (Target: " .. idealAltitude .. ")", 8, playerData ) + local text=string.format("Alt: %d feet (Target: %d feet)", altitude, idealAltitude) + self:_SendMessageToPlayer(text, 8, playerData) end --- Score for correct AoA. diff --git a/Moose Development/Moose/Wrapper/Positionable.lua b/Moose Development/Moose/Wrapper/Positionable.lua index 2b8170ba9..a9e8ee026 100644 --- a/Moose Development/Moose/Wrapper/Positionable.lua +++ b/Moose Development/Moose/Wrapper/Positionable.lua @@ -696,6 +696,105 @@ function POSITIONABLE:GetAoA() return nil end +--- Returns the unit's climb or descent angle. +-- @param Wrapper.Positionable#POSITIONABLE self +-- @return #number Climb or descent angle in degrees. +function POSITIONABLE:GetAoA() + + -- Get position of the unit. + local unitpos = self:GetPosition() + + if unitpos then + + -- Get velocity vector of the unit. + local unitvel = self:GetVelocityVec3() + + if unitvel and UTILS.VecNorm(unitvel)~=0 then + + return math.asin(unitvel.y/UTILS.VecNorm(unitvel)) + + end + end +end + +--- Returns the pitch angle of a unit. +-- @param Wrapper.Positionable#POSITIONABLE self +-- @return #number Pitch ange in degrees. +function POSITIONABLE:GetPitch() + + -- Get position of the unit. + local unitpos = self:GetPosition() + + if unitpos then + return math.deg(math.asin(unitpos.x.y)) + end + + return nil +end + +--- Returns the roll angle of a unit. +-- @param Wrapper.Positionable#POSITIONABLE self +-- @return #number Pitch ange in degrees. +function POSITIONABLE:GetRoll() + + -- Get position of the unit. + local unitpos = self:GetPosition() + + if unitpos then + + --first, make a vector that is perpendicular to y and unitpos.x with cross product + local cp = UTILS.VecCross(unitpos.x, {x = 0, y = 1, z = 0}) + + --now, get dot product of of this cross product with unitpos.z + local dp = UTILS.VecDot(cp, unitpos.z) + + --now get the magnitude of the roll (magnitude of the angle between two vectors is acos(vec1.vec2/|vec1||vec2|) + local Roll = math.acos(dp/(UTILS.VecNorm(cp)*UTILS.VecNorm(unitpos.z))) + + --now, have to get sign of roll. + -- by convention, making right roll positive + -- to get sign of roll, use the y component of unitpos.z. For right roll, y component is negative. + + if unitpos.z.y > 0 then -- left roll, flip the sign of the roll + Roll = -Roll + end + + return math.deg(Roll) + end +end + +--- Returns the yaw angle of a unit. +-- @param Wrapper.Positionable#POSITIONABLE self +-- @return #number Yaw ange in degrees. +function POSITIONABLE:GetYaw() + + local unitpos = self:GetPosition() + if unitpos then + -- get unit velocity + local unitvel = self:GetVelocityVec3() + + if unitvel and UTILS.VecNorm(unitvel) ~= 0 then --must have non-zero velocity! + local AxialVel = {} --unit velocity transformed into aircraft axes directions + + --transform velocity components in direction of aircraft axes. + AxialVel.x = UTILS.VecDot(unitpos.x, unitvel) + AxialVel.y = UTILS.VecDot(unitpos.y, unitvel) + AxialVel.z = UTILS.VecDot(unitpos.z, unitvel) + + --Yaw is the angle between unitpos.x and the x and z velocities + --define right yaw as positive + local Yaw = math.acos(UTILS.VecDot({x = 1, y = 0, z = 0}, {x = AxialVel.x, y = 0, z = AxialVel.z})/UTILS.VecNorm({x = AxialVel.x, y = 0, z = AxialVel.z})) + + --now set correct direction: + if AxialVel.z > 0 then + Yaw = -Yaw + end + return Yaw + end + end + +end + --- Returns the message text with the callsign embedded (if there is one). -- @param #POSITIONABLE self From f9cb5d596648930c3888880df291a9b39a6f96dc Mon Sep 17 00:00:00 2001 From: Frank Date: Sat, 13 Oct 2018 00:37:15 +0200 Subject: [PATCH 06/10] CT v0.0.4 Fixed wrong z-position. Generalized abort parameters. Added preliminary F10 menu. --- .../Moose/Functional/CarrierTrainer.lua | 404 +++++++++++++++--- .../Moose/Wrapper/Positionable.lua | 2 +- 2 files changed, 344 insertions(+), 62 deletions(-) diff --git a/Moose Development/Moose/Functional/CarrierTrainer.lua b/Moose Development/Moose/Functional/CarrierTrainer.lua index 1f9467890..b15e0f715 100644 --- a/Moose Development/Moose/Functional/CarrierTrainer.lua +++ b/Moose Development/Moose/Functional/CarrierTrainer.lua @@ -29,6 +29,7 @@ -- @field Core.Zone#ZONE_UNIT startZone Zone in which the pattern approach starts. -- @field Core.Zone#ZONE_UNIT giantZone Zone around the carrier to register a new player. -- @field #table players Table of players. +-- @field #table menuadded Table of units where the F10 radio menu was added. -- @extends Core.Fsm#FSM --- Practice Carrier Landings @@ -48,14 +49,20 @@ CARRIERTRAINER = { lid = nil, Debug = true, carrier = nil, + alias = nil, startZone = nil, giantZone = nil, players = {}, + menuadded = {}, } +--- Main radio menu. +-- @field #table MenuF10 +CARRIERTRAINER.MenuF10={} + --- Carrier trainer class version. -- @field #string version -CARRIERTRAINER.version="0.0.3" +CARRIERTRAINER.version="0.0.4" --- Player data table holding all important parameters for each player. -- @type CARRIERTRAINER.PlayerData @@ -79,9 +86,10 @@ CARRIERTRAINER.version="0.0.3" --- Create new carrier trainer. -- @param #CARRIERTRAINER self --- @param carriername Name of the aircraft carrier unit. +-- @param carriername Name of the aircraft carrier unit as defined in the mission editor. +-- @param alias (Optional) Alias for the carrier. This will be used for radio messages and the F10 radius menu. Default is the carrier name as defined in the mission editor. -- @return #CARRIERTRAINER self -function CARRIERTRAINER:New(carriername) +function CARRIERTRAINER:New(carriername, alias) -- Inherit everthing from FSM class. local self = BASE:Inherit(self, FSM:New()) -- #CARRIERTRAINER @@ -97,7 +105,10 @@ function CARRIERTRAINER:New(carriername) end -- Set some string id for output to DCS.log file. - self.lid=string.format("CARRIERTRAINER %s | ", carriername) + self.lid=string.format("CARRIERTRAINER %s | ", carriername) + + -- Set alias. + self.alias=alias or carriername ----------------------- --- FSM Transitions --- @@ -200,7 +211,6 @@ function CARRIERTRAINER:OnEventBirth(EventData) local _uid=_unit:GetID() local _group=_unit:GetGroup() - local _gid=_group:GetID() local _callsign=_unit:GetCallsign() -- Debug output. @@ -208,7 +218,10 @@ function CARRIERTRAINER:OnEventBirth(EventData) self:T(self.lid..text) MESSAGE:New(text, 5):ToAllIf(self.Debug) - -- + -- Add Menu commands. + self:_AddF10Commands(_unitName) + + -- Init player. if self.players[_playername]==nil then self.players[_playername]=self:_InitNewPlayer(_unitName) else @@ -296,7 +309,7 @@ function CARRIERTRAINER:_CheckPlayerStatus() if unit:IsAlive() then if unit:IsInZone(self.giantZone) then - self:_DetailedPlayerStatus(playerData) + --self:_DetailedPlayerStatus(playerData) end if playerData.step==0 and unit:IsInZone(self.giantZone) and unit:InAir() then @@ -325,6 +338,7 @@ function CARRIERTRAINER:_CheckPlayerStatus() self:_Trap(playerData) end else + -- Unit not alive. --playerDatas[i] = nil end end @@ -361,6 +375,7 @@ function CARRIERTRAINER:_StepName(step) name="trapped" end + return name end --- Provide info about player status on the fly. @@ -390,10 +405,10 @@ function CARRIERTRAINER:_DetailedPlayerStatus(playerData) text=text..string.format("roll=%.1f yaw=%.1f pitch=%.1f\n", roll, yaw, pitch) 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)", diffX, diffZ, math.abs(diffX)+math.abs(diffZ)) + 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 ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -407,7 +422,6 @@ function CARRIERTRAINER:_NewRound(playerData) local text=string.format("Welcome back, %s! Cleared for approach. TCN 1X, BRC 354 (MAG HDG).", playerData.callsign) MESSAGE:New(text, 5):ToClient(playerData.client) - --MessageToAll( "Welcome back, " .. playerData.callsign .. "! Cleared for approach! TCN 1X, BRC 354 (MAG HDG).", 5, "InitZoneMessage" ) self:_InitNewRound(playerData) playerData.step = 1 @@ -436,13 +450,22 @@ function CARRIERTRAINER:_Upwind(playerData) local diffX = position.x - carrierPosition.x -- Get distances between carrier and player unit (parallel and perpendicular to direction of movement of carrier) - diffX, diffZ = self:_GetDistances(playerData.unit) + diffX, diffZ = self:_GetDistances(playerData.unit) + + self.Upwind={} + self.Upwind.Xmin=-4000 -- TODO Should be withing 4 km behind carrier. Why? + self.Upwind.Xmax=nil + self.Upwind.Zmin=0 + self.Upwind.Zmax=500 + self.Upwind.Limit=0 + self.Upwind.Alitude=UTILS.FeetToMeters(800) -- Too far away. - -- Should be between 0-500 meters right of carrier. - -- TODO Should be withing 4 km behind carrier. Why? - if (diffZ > 500 or diffZ < 0 or diffX < -4000) then - self:_AbortPattern(playerData) + -- Should be between 0-500 meters right of carrier. + --if (diffZ > 500 or diffZ < 0 or diffX < -4000) then + if self:_CheckAbort(diffX,diffZ, self.Upwind) then + --MESSAGE:New(string.format("Abort: diffX=%d (min=-4000, max=nil), diffZ=%d (min=0, max=500)", diffX, diffZ)):ToAllIf(self.Debug) + self:_AbortPattern(playerData, diffX, diffZ, self.Upwind) return end @@ -455,16 +478,16 @@ function CARRIERTRAINER:_Upwind(playerData) local hint = "" local score = 0 - if (altitude > 850) then + if altitude > 850 then score = 5 hint = "You're high on the upwind." - elseif (altitude > 830) then + elseif altitude > 830 then score = 7 hint = "You're slightly high on the upwind." - elseif (altitude < 750) then + elseif altitude < 750 then score = 5 hint = "You're low on the upwind." - elseif (altitude < 770) then + elseif altitude < 770 then score = 7 hint = "You're slightly low on the upwind." else @@ -494,21 +517,110 @@ end -- @return #number Distance in the direction of the orientation of the carrier. -- @return #number Distance perpendicular to the orientation of the carrier. function CARRIERTRAINER:_GetDistances(unit) + + -- Vector to carrier local a=self.carrier:GetVec3() + + -- Vector to player local b=unit:GetVec3() + + -- Vector from carrier to player. local c={x=b.x-a.x, y=0, z=b.z-a.z} + + -- Orientation of carrier. local x=self.carrier:GetOrientationX() - local dz=UTILS.VecDot(x,c) - local alpha=math.acos(UTILS.VecDot(c,x)/UTILS.VecNorm(c)) - local dx=UTILS.VecNorm(c)*math.sin(alpha) - return dz,dx -end + + -- Projection of player pos on x component. + local dx=UTILS.VecDot(x,c) + + -- Orientation of carrier. + local z=self.carrier:GetOrientationZ() + + -- Projection of player pos on z component. + local dz=UTILS.VecDot(z,c) + + return dx,dz +end + +--- Check if a player is within the right area. +-- @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. +-- @return #boolean If true, approach should be aborted. +function CARRIERTRAINER:_CheckAbort(X, Z, pos) + + local abort=false + if pos.Xmin and Xpos.Xmax then + abort=true + elseif pos.Zmin and Zpos.Zmax then + abort=true + end + + return abort +end + +--- Generate a text if a player is too far from where he should be. +-- @param #CARRIERTRAINER self +-- @param #number X X distance player to carrier. +-- @param #number Z Z distance player to carrier. +-- @param #table posData Position data limits. +function CARRIERTRAINER:_TooFarOutText(X, Z, posData) + + local text="You are too far" + + local xtext=nil + if posData.Xmin and XposData.Xmax then + xtext=" behind" + end + + local ztext=nil + if posData.Zmin and ZposData.Zmax then + ztext=" starboard (right)" + end + + if xtext and ztext then + text=text..xtext.." and"..ztext + elseif xtext then + text=text..xtext + elseif ztext then + text=text..ztext + end + + text=text.." of the carrier!" + + return text +end + +--- Pattern aborted. +-- @param #CARRIERTRAINER self +-- @param #CARRIERTRAINER.PlayerData playerData Player data. +-- @param #number X X distance player to carrier. +-- @param #number Z Z distance player to carrier. +-- @param #table posData Position data. +function CARRIERTRAINER:_AbortPattern(playerData, X, Z, posData) + local toofartext=self:_TooFarOutText(X, Z, posData) + self:_SendMessageToPlayer(toofartext.." Abort approach!", 15, playerData ) + MESSAGE:New(string.format("Abort: X=%d Xmin=%s, Xmax=%s | Z=%d Zmin=%s Zmax=%s", X, tostring(posData.Xmin), tostring(posData.Xmax), Z, tostring(posData.Zmin), tostring(posData.Zmax)), 60):ToAllIf(self.Debug) + self:_AddToSummary(playerData, "Approach aborted.") + self:_PrintFinalScore(playerData, 30, -2) + self:_HandleCollectedResult(playerData, -2) + playerData.step = 0 +end --- Break. -- @param #CARRIERTRAINER self -- @param #CARRIERTRAINER.PlayerData playerData Player data table. -- @param #string part Part of the break. -function CARRIERTRAINER:_Break(playerData, part) +function CARRIERTRAINER:_Break(playerData, part) local playerPosition = playerData.unit:GetVec3() local carrierPosition = self.carrier:GetVec3() @@ -520,11 +632,18 @@ function CARRIERTRAINER:_Break(playerData, part) diffX, diffZ = self:_GetDistances(playerData.unit) -- Abort when - -- > 1.5 km right of carrier - -- > 3.7 km left of carrier - -- > 0.5 km behind carrier - if (diffZ > 1500 or diffZ < -3700 or diffX < -500) then - self:_AbortPattern(playerData) + self.Break={} + self.Break.Xmin=-500 + self.Break.Xmax=nil + self.Break.Zmin=-3700 + self.Break.Zmax=1500 + self.Break.LimitEarly=-370 --0.2 NM + self.Break.LimitLate=-1470 --0.8 NM + self.Break.Alitude=UTILS.FeetToMeters(800) + + --if (diffZ > 1500 or diffZ < -3700 or diffX < -500) then + if self:_CheckAbort(diffX, diffZ, self.Break) then + self:_AbortPattern(playerData, diffX, diffZ, self.Break) return end @@ -539,7 +658,7 @@ function CARRIERTRAINER:_Break(playerData, part) limit = -1470 -- 0.8 NM end - -- + -- Check if too far left if diffZ < limit then local idealAltitude = 800 @@ -594,11 +713,21 @@ function CARRIERTRAINER:_Abeam(playerData) -- Get distances between carrier and player unit (parallel and perpendicular to direction of movement of carrier) diffX, diffZ = self:_GetDistances(playerData.unit) + self.Abeam={} + self.Abeam.Xmin=nil + self.Abeam.Xmax=nil + self.Abeam.Zmin=-3700 + self.Abeam.Zmax=-1000 + self.Abeam.Limit=-200 + self.Abeam.Alitude=UTILS.FeetToMeters(600) + -- Abort if -- less than 1.0 km left of boat (no closer than 1 km to boat -- more than 3.7 km left of boat - if (diffZ > -1000 or diffZ < -3700) then - self:_AbortPattern(playerData) + --if (diffZ > -1000 or diffZ < -3700) then + if self:_CheckAbort(diffX, diffZ, self.Abeam) then + --MESSAGE:New(string.format("Abort: diffX=%d (min=nil, max=nil), diffZ=%d (min=-3700, max=-1000)", diffX, diffZ)):ToAllIf(self.Debug) + self:_AbortPattern(playerData, diffX, diffZ, self.Abeam) return end @@ -693,12 +822,14 @@ function CARRIERTRAINER:_CheckForLongDownwind(playerData) -- Get distances between carrier and player unit (parallel and perpendicular to direction of movement of carrier) local diffX, diffZ = self:_GetDistances(playerData.unit) - --if carrierPosition.x - playerPosition.x > limit then - if diffX > limit then + -- Check we are not too far out w.r.t back of the boat. + if diffX < limit then local headingPlayer = playerData.unit:GetHeading() local headingCarrier = self.carrier:GetHeading() + --TODO: Take carrier heading != 0 into account! + if (headingPlayer > 170) then local hint = "Too long downwind. Turn final earlier next time." @@ -724,10 +855,20 @@ function CARRIERTRAINER:_Ninety(playerData) local diffX = playerPosition.x - carrierPosition.x -- Get distances between carrier and player unit (parallel and perpendicular to direction of movement of carrier) - diffX, diffZ = self:_GetDistances(playerData.unit) + diffX, diffZ = self:_GetDistances(playerData.unit) - if(diffZ < -3700 or diffX < -3700 or diffX > 0) then - self:_AbortPattern(playerData) + self.Ninety={} + self.Ninety.Xmin=-3700 + self.Ninety.Xmax=0 + self.Ninety.Zmin=-3700 + self.Ninety.Zmax=nil + self.Ninety.Limit=-1111 + self.Ninety.Altitude=UTILS.FeetToMeters(500) + + --if(diffZ < -3700 or diffX < -3700 or diffX > 0) then + if self:_CheckAbort(diffX, diffZ, self.Ninety) then + --MESSAGE:New(string.format("Abort: diffX=%d (min=-3700, max=0), diffZ=%d (min=-3700, max=nil)", diffX, diffZ)):ToAllIf(self.Debug) + self:_AbortPattern(playerData, diffX, diffZ, self.Ninety) return end @@ -788,9 +929,19 @@ function CARRIERTRAINER:_Wake(playerData) -- Get distances between carrier and player unit (parallel and perpendicular to direction of movement of carrier) diffX, diffZ = self:_GetDistances(playerData.unit) + + self.Wake={} + self.Wake.Xmin=-4000 + self.Wake.Xmax=0 + self.Wake.Zmin=-2000 + self.Wake.Zmax=nil + self.Wake.Limit=0 + self.Wake.Alitude=UTILS.FeetToMeters(370) - if(diffZ < -2000 or diffX < -4000 or diffX > 0) then - self:_AbortPattern(playerData) + --if (diffZ < -2000 or diffX < -4000 or diffX > 0) then + if self:_CheckAbort(diffX, diffZ, self.Wake) then + MESSAGE:New(string.format("Abort: diffX=%d (min=-4000, max=0), diffZ=%d (min=-2000, max=nil)", diffX, diffZ)):ToAllIf(self.Debug) + self:_AbortPattern(playerData, diffX, diffZ, self.Wake) return end @@ -849,11 +1000,17 @@ function CARRIERTRAINER:_Groove(playerData) -- Get distances between carrier and player unit (parallel and perpendicular to direction of movement of carrier) diffX, diffZ = self:_GetDistances(playerData.unit) - diffX=diffX+100 + --diffX=diffX+100 + + self.Groove={} + self.Groove.Xmin=-4000 + self.Groove.Xmax=100 -- In front of carrier or more than 4 km behind carrier. - if (diffX > 0 or diffX < -4000) then - self:_AbortPattern(playerData) + --if (diffX > 0 or diffX < -4000) then + if self:_CheckAbort(diffX, diffZ, self.Groove) then + --MESSAGE:New(string.format("Abort: diffX=%d (min=-4000, max=0), diffZ=%d (min=nil, max=nil)", diffX, diffZ)):ToAllIf(self.Debug) + self:_AbortPattern(playerData, diffX, diffZ, self.Groove) return end @@ -931,18 +1088,27 @@ function CARRIERTRAINER:_Trap(playerData) local diffX = playerPosition.x - carrierPosition.x -- Get distances between carrier and player unit (parallel and perpendicular to direction of movement of carrier) - diffZ, diffX = self:_GetDistances(playerData.unit) + diffZ, diffX = self:_GetDistances(playerData.unit) - if(diffZ < -2000 or diffZ > 2000 or diffX < -3000) then - self:_AbortPattern(playerData) + self.Trap={} + self.Trap.Xmin=-3000 + self.Trap.Xmax=nil + self.Trap.Zmin=-2000 + self.Trap.Zmax=2000 + self.Trap.Limit=nil + self.Trap.Alitude=nil + + --if(diffZ < -2000 or diffZ > 2000 or diffX < -3000) then + if self:_CheckAbort(diffX, diffZ, self.Trap) then + self:_AbortPattern(playerData, diffX, diffZ, self.Trap) return end - if(diffX > playerData.highestCarrierXDiff) then + if (diffX > playerData.highestCarrierXDiff) then playerData.highestCarrierXDiff = diffX end - if(playerPosition.y < playerData.lowestAltitude) then + if (playerPosition.y < playerData.lowestAltitude) then playerData.lowestAltitude = playerPosition.y end @@ -1011,6 +1177,127 @@ function CARRIERTRAINER:_Trap(playerData) end end +----------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Menu Functions +----------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Add menu commands for player. +-- @param #CARRIERTRAINER self +-- @param #string _unitName Name of player unit. +function CARRIERTRAINER:_AddF10Commands(_unitName) + self:F(_unitName) + + -- Get player unit and name. + local _unit, playername = self:_GetPlayerUnitAndName(_unitName) + + -- Check for player unit. + if _unit and playername then + + -- Get group and ID. + local group=_unit:GetGroup() + local _gid=group:GetID() + + if group and _gid then + + if not self.menuadded[_gid] then + + -- Enable switch so we don't do this twice. + self.menuadded[_gid] = true + + -- Main F10 menu: F10/Carrier Trainer// + if CARRIERTRAINER.MenuF10[_gid] == nil then + CARRIERTRAINER.MenuF10[_gid]=missionCommands.addSubMenuForGroup(_gid, "Carrier Trainer") + end + local _rangePath = missionCommands.addSubMenuForGroup(_gid, self.alias, CARRIERTRAINER.MenuF10[_gid]) + local _statsPath = missionCommands.addSubMenuForGroup(_gid, "Results", _rangePath) + local _settingsPath = missionCommands.addSubMenuForGroup(_gid, "My Settings", _rangePath) + local _infoPath = missionCommands.addSubMenuForGroup(_gid, "Carrier Info", _rangePath) + + -- F10/On the Range//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/On the Range//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/On the Range//Range Information + --missionCommands.addCommandForGroup(_gid, "General Info", _infoPath, self._DisplayRangeInfo, self, _unitName) + missionCommands.addCommandForGroup(_gid, "Weather Report", _infoPath, self._DisplayCarrierWeather, self, _unitName) + --missionCommands.addCommandForGroup(_gid, "Bombing Targets", _infoPath, self._DisplayBombTargets, self, _unitName) + --missionCommands.addCommandForGroup(_gid, "Strafe Pits", _infoPath, self._DisplayStrafePits, self, _unitName) + end + else + self:T(self.lid.."Could not find group or group ID in AddF10Menu() function. Unit name: ".._unitName) + end + else + self:T(self.lid.."Player unit does not exist in AddF10Menu() function. Unit name: ".._unitName) + end + +end + +--- Report weather conditions at range. Temperature, QFE pressure and wind data. +-- @param #CARRIERTRAINER self +-- @param #string _unitname Name of the player unit. +function CARRIERTRAINER:_DisplayCarrierWeather(_unitname) + self:F(_unitname) + + -- Get player unit and player name. + local unit, playername = self:_GetPlayerUnitAndName(_unitname) + + -- Check if we have a player. + if unit and playername then + + -- Message text. + local text="" + + -- 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 Beaufort wind scale. + local Bn,Bd=UTILS.BeaufortScale(Ws) + + local WD=string.format('%03d°', Wd) + local Ts=string.format("%d°C",T) + + local hPa2inHg=0.0295299830714 + local hPa2mmHg=0.7500615613030 + + local settings=_DATABASE:GetPlayerSettings(playername) or _SETTINGS --Core.Settings#SETTINGS + local tT=string.format("%d°C",T) + local tW=string.format("%.1f m/s", Ws) + local tP=string.format("%.1f mmHg", P*hPa2mmHg) + if settings:IsImperial() then + tT=string.format("%d°F", UTILS.CelciusToFarenheit(T)) + 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) + 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) + else + self:T(self.lid..string.format("ERROR! Could not find player unit in RangeInfo! Name = %s", _unitname)) + end +end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- MISC functions @@ -1081,7 +1368,7 @@ end -- @param #CARRIERTRAINER.PlayerData playerData Player data. function CARRIERTRAINER:_SendMessageToPlayer(message, duration, playerData) if playerData.client then - MESSAGE:New(message, duration):ToClient(playerData.client) + MESSAGE:New(string.format("%s, %s, ", self.alias, playerData.callsign)..message, duration):ToClient(playerData.client) end end @@ -1122,6 +1409,7 @@ end -- @param #CARRIERTRAINER.PlayerData playerData Player data. -- @param #number wire Trapped wire. function CARRIERTRAINER:_HandleCollectedResult(playerData, wire) + local newString = "" if(wire == -2) then newString = playerData.score .. " (Aborted)" @@ -1136,26 +1424,20 @@ function CARRIERTRAINER:_HandleCollectedResult(playerData, wire) playerData.totalScore = playerData.totalScore + playerData.score playerData.passes = playerData.passes + 1 - if(playerData.collectedResultString == "") then + if playerData.collectedResultString == "" then playerData.collectedResultString = newString else playerData.collectedResultString = playerData.collectedResultString .. ", " .. newString MessageToAll( playerData.callsign .. "'s " .. playerData.passes .. " passes: " .. playerData.collectedResultString .. " (TOTAL: " .. playerData.totalScore .. ")" , 30, "CollectedResult" ) end - self:_SendMessageToPlayer( "Return south 4 nm (over the trailing ship), towards WP 1, to restart the pattern.", 20, playerData ) + local heading=playerData.unit:GetCoordinate():HeadingTo(self.startZone:GetCoordinate()) + local distance=playerData.unit:GetCoordinate():Get2DDistance(self.startZone:GetCoordinate()) + local text=string.format("%s, fly heading %d for %d nm to restart the pattern.", playerData.callsign, heading, UTILS.MetersToNM(distance)) + --"Return south 4 nm (over the trailing ship), towards WP 1, to restart the pattern." + self:_SendMessageToPlayer(text, 30, playerData) end ---- Pattern aborted. --- @param #CARRIERTRAINER self --- @param #CARRIERTRAINER.PlayerData playerData Player data. -function CARRIERTRAINER:_AbortPattern(playerData) - self:_SendMessageToPlayer( "You're too far from where you should be. Abort approach!", 15, playerData ) - self:_AddToSummary(playerData, "Approach aborted.") - self:_PrintFinalScore(playerData, 30, -2) - self:_HandleCollectedResult(playerData, -2) - playerData.step = 0 -end --- Get the formatted score. -- @param #CARRIERTRAINER self diff --git a/Moose Development/Moose/Wrapper/Positionable.lua b/Moose Development/Moose/Wrapper/Positionable.lua index a9e8ee026..ac64a90d6 100644 --- a/Moose Development/Moose/Wrapper/Positionable.lua +++ b/Moose Development/Moose/Wrapper/Positionable.lua @@ -699,7 +699,7 @@ end --- Returns the unit's climb or descent angle. -- @param Wrapper.Positionable#POSITIONABLE self -- @return #number Climb or descent angle in degrees. -function POSITIONABLE:GetAoA() +function POSITIONABLE:GetClimbAnge() -- Get position of the unit. local unitpos = self:GetPosition() From 8b78090a577c9419e496f4a43a56e6864b1d78b9 Mon Sep 17 00:00:00 2001 From: Frank Date: Sat, 13 Oct 2018 19:42:17 +0200 Subject: [PATCH 07/10] CT v0.0.5 --- .../Moose/Functional/CarrierTrainer.lua | 137 +++++++++++++++--- 1 file changed, 118 insertions(+), 19 deletions(-) diff --git a/Moose Development/Moose/Functional/CarrierTrainer.lua b/Moose Development/Moose/Functional/CarrierTrainer.lua index b15e0f715..189f8c58c 100644 --- a/Moose Development/Moose/Functional/CarrierTrainer.lua +++ b/Moose Development/Moose/Functional/CarrierTrainer.lua @@ -15,7 +15,6 @@ -- === -- -- ### Author: **Bankler** (original idea and script) --- ### Co-author: **funkyfranky** (implementation as MOOSE class) -- -- @module Functional.CarrierTrainer -- @image MOOSE.JPG @@ -26,10 +25,18 @@ -- @field #string lid Class id string for output to DCS log file. -- @field #boolean Debug Debug mode. Messages to all about status. -- @field Wrapper.Unit#UNIT carrier Aircraft carrier unit on which we want to practice. +-- @field #string carriertype Type name of aircraft carrier. -- @field Core.Zone#ZONE_UNIT startZone Zone in which the pattern approach starts. -- @field Core.Zone#ZONE_UNIT giantZone Zone around the carrier to register a new player. -- @field #table players Table of players. -- @field #table menuadded Table of units where the F10 radio menu was added. +-- @field #CARRIER.Checkpoint upwind Upwind checkpoint. +-- @field #CARRIER.Checkpoint breakearly Early break checkpoint. +-- @field #CARRIER.Checkpoint breaklate Late brak checkpoint. +-- @field #CARRIER.Checkpoint abeam Abeam checkpoint. +-- @field #CARRIER.Checkpoint ninety At the ninety checkpoint. +-- @field #CARRIER.Checkpoint groove In the groove checkpoint. +-- @field #CARRIER.Checkpoint trap Landing checkpoint. -- @extends Core.Fsm#FSM --- Practice Carrier Landings @@ -46,14 +53,23 @@ -- @field #CARRIERTRAINER CARRIERTRAINER = { ClassName = "CARRIERTRAINER", - lid = nil, - Debug = true, - carrier = nil, - alias = nil, - startZone = nil, - giantZone = nil, - players = {}, - menuadded = {}, + lid = nil, + Debug = true, + carrier = nil, + carriertype = nil, + alias = nil, + startZone = nil, + giantZone = nil, + players = {}, + menuadded = {}, + upwind = {}, + abeam = {}, + breakearly = {}, + breaklate = {}, + ninety = {}, + wake = {}, + groove = {}, + trap = {}, } --- Main radio menu. @@ -62,7 +78,25 @@ CARRIERTRAINER.MenuF10={} --- Carrier trainer class version. -- @field #string version -CARRIERTRAINER.version="0.0.4" +CARRIERTRAINER.version="0.0.5" + +--- Carrier types. +-- @type CARRIERTRAINER.CarrierType +-- @field #string Stennis CVN-74 John C. Stennis +-- @field #string CarlVinson CVN-70 Carl Vinson +-- @field #string Tarawa LHA-1 Tarawa +-- @field #string Kuznetsov CV 1143.5 Admiral Kuznetsov +CARRIERTRAINER.CarrierType={ + STENNIS="Stennis", + VINSON="Vinson", + TARAWA="LHA_Tarawa", + KUZNETSOV="KUZNECOW", +} + +CARRIERTRAINER.AircraftType={ + AV8B="AV8BNA", + HORNET="FA-18C_hornet", +} --- Player data table holding all important parameters for each player. -- @type CARRIERTRAINER.PlayerData @@ -78,7 +112,25 @@ CARRIERTRAINER.version="0.0.4" -- @field #number secondsStandingStill Time player does not move after a landing attempt. -- @field #string summary Result summary text. -- @field Wrapper.Client#CLIENT Client object of player. +-- @field #string difficulty Difficulty level. +--- Difficulty level. +-- @field #string easy Easy difficulty: error margin 10% for high score and 20% for low score. No score for deviation >20%. +-- @field #string normal Normal difficulty: error margin 5% deviation from ideal for high score and 10% for low score. No score for deviation >10%. +-- @field #string hard Hard difficulty: error margin 2.5% deviation from ideal value for high score and 5% for low score. No score for deviation >5%. + +--- Checkpoint +-- @type CARRIERTRAINER.Checkpoint +-- @field #number Xmin Minimum allowed longitual distance to carrier. +-- @field #number Xmax Maximum allowed longitual distance to carrier. +-- @field #number Zmin Minimum allowed latitudal distance to carrier. +-- @field #number Zmax Maximum allowed latitudal distance to carrier. +-- @field #number Xlimit Latitudal threshold for triggering the next step. +-- @field #number Zlimit Latitudal threshold for triggering the next step. +-- @field #number Altitude Optimal altitude at this point. +-- @field #number AoA Optimal AoA at this point. +-- @field #number Speed Optimal speed at this point. +-- @field #table Checklist Table of checklist text items to display at this point. ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Constructor @@ -101,12 +153,18 @@ function CARRIERTRAINER:New(carriername, alias) self.startZone = ZONE_UNIT:New("startZone", self.carrier, 1000, { dx = -2000, dy = 100, relative_to_unit = true }) self.giantZone = ZONE_UNIT:New("giantZone", self.carrier, 30000, { dx = 0, dy = 0, relative_to_unit = true }) else - self:E("ERROR: Carrier unit could not be found!") + local text=string.format("ERROR: Carrier unit %s could not be found! Make sure this UNIT is defined in the mission editor and check the spelling of the unit name carefully.", carriername) + MESSAGE:New(text, 120):ToAll() + self:E(self.lid..text) + return nil end -- Set some string id for output to DCS.log file. self.lid=string.format("CARRIERTRAINER %s | ", carriername) + -- Get carrier type. + self.carriertype=self.carrier:GetTypeName() + -- Set alias. self.alias=alias or carriername @@ -158,7 +216,7 @@ end function CARRIERTRAINER:onafterStart(From, Event, To) -- Events are handled my MOOSE. - self:I(self.lid..string.format("Starting Carrier Training %s for carrier unit %s.", CARRIERTRAINER.version, self.carrier:GetName())) + self:I(self.lid..string.format("Starting Carrier Training %s for carrier unit %s of type %s.", CARRIERTRAINER.version, self.carrier:GetName(), self.carriertype)) -- Handle events. self:HandleEvent(EVENTS.Birth) @@ -290,6 +348,26 @@ function CARRIERTRAINER:_AddToCollectedResult(playerData, item) playerData.collectedResultString = playerData.collectedResultString .. item .. "\n" end +--- Get relative heading of player wrt carrier. +-- @param #CARRIERTRAINER self +-- @param Wrapper.Unit#UNIT unit Player unit. +-- @return #number Relative heading in degrees. +function CARRIERTRAINER:_GetRelativeHeading(unit) + --local a=self.carrier:GetVec3() + --local b=unit:GetVec3() + --local c={x=b.x-a.x, y=0, z=b.z-a.z} + --local headingCarrier=self.carrier:GetHeading() + --local headingPlayer=unit:GetHeading() + local vC=self.carrier:GetOrientationX() + local vP=unit:GetOrientationX() + + -- Get angle between the two orientation vectors in rad. + local relHead=math.acos(UTILS.VecDot(vC,vP)/UTILS.VecNorm(vC)/UTILS.VecNorm(vP)) + + -- Return heading in degrees. + return math.deg(relHead) +end + --- Carrier trainer event handler for event birth. -- @param #CARRIERTRAINER self @@ -346,6 +424,33 @@ function CARRIERTRAINER:_CheckPlayerStatus() end +--- Init parameters for USS Stennis carrier. +-- @param #CARRIERTRAINER self +function CARRIERTRAINMER:_InitStennis() + + self.Upwind.Xmin=1 + + self.upwind={} + self.upwind.Xmin=-4000 -- TODO Should be withing 4 km behind carrier. Why? + self.upwind.Xmax=nil + self.upwind.Zmin=0 + self.upwind.Zmax=500 + self.upwind. + self.upwind.Alitude=UTILS.FeetToMeters(800) + + + -- Early break checkpoint parameters. + self.breakearly={} + self.Breakearly.Xmin=-500 + self.Breakearly.Xmax=nil + self.Breakearly.Zmin=-3700 + self.Breakearly.Zmax=1500 + self.Breakearly.LimitEarly=-370 --0.2 NM + self.Breakearly.LimitLate=-1470 --0.8 NM + self.Breakearly.Alitude=UTILS.FeetToMeters(800) + +end + --- Get name of the current pattern step. -- @param #CARRIERTRAINER self -- @param #number step Step @@ -452,13 +557,7 @@ function CARRIERTRAINER:_Upwind(playerData) -- Get distances between carrier and player unit (parallel and perpendicular to direction of movement of carrier) diffX, diffZ = self:_GetDistances(playerData.unit) - self.Upwind={} - self.Upwind.Xmin=-4000 -- TODO Should be withing 4 km behind carrier. Why? - self.Upwind.Xmax=nil - self.Upwind.Zmin=0 - self.Upwind.Zmax=500 - self.Upwind.Limit=0 - self.Upwind.Alitude=UTILS.FeetToMeters(800) + -- Too far away. -- Should be between 0-500 meters right of carrier. From f47bf4771f51c8f9881bf630436b3c7e0e18d8b7 Mon Sep 17 00:00:00 2001 From: Frank Date: Sat, 13 Oct 2018 19:45:26 +0200 Subject: [PATCH 08/10] Remove CT --- .../Moose/Functional/CarrierTrainer.lua | 1595 ----------------- 1 file changed, 1595 deletions(-) delete mode 100644 Moose Development/Moose/Functional/CarrierTrainer.lua diff --git a/Moose Development/Moose/Functional/CarrierTrainer.lua b/Moose Development/Moose/Functional/CarrierTrainer.lua deleted file mode 100644 index 189f8c58c..000000000 --- a/Moose Development/Moose/Functional/CarrierTrainer.lua +++ /dev/null @@ -1,1595 +0,0 @@ ---- **Functional** - (R2.4) - Carrier CASE I Recovery Practice --- --- Practice carrier landings. --- --- Features: --- --- * CASE I recovery. --- * Performance evaluation. --- * Feedback about performance during flight. --- --- Please not that his class is work in progress and in an **alpha** stage. --- At the moment training parameters are optimized for F/A-18C Hornet as aircraft and USS Stennis as carrier. --- Other aircraft and carriers **might** be possible in future but would need a different set of parameters. --- --- === --- --- ### Author: **Bankler** (original idea and script) --- --- @module Functional.CarrierTrainer --- @image MOOSE.JPG - ---- CARRIERTRAINER class. --- @type CARRIERTRAINER --- @field #string ClassName Name of the class. --- @field #string lid Class id string for output to DCS log file. --- @field #boolean Debug Debug mode. Messages to all about status. --- @field Wrapper.Unit#UNIT carrier Aircraft carrier unit on which we want to practice. --- @field #string carriertype Type name of aircraft carrier. --- @field Core.Zone#ZONE_UNIT startZone Zone in which the pattern approach starts. --- @field Core.Zone#ZONE_UNIT giantZone Zone around the carrier to register a new player. --- @field #table players Table of players. --- @field #table menuadded Table of units where the F10 radio menu was added. --- @field #CARRIER.Checkpoint upwind Upwind checkpoint. --- @field #CARRIER.Checkpoint breakearly Early break checkpoint. --- @field #CARRIER.Checkpoint breaklate Late brak checkpoint. --- @field #CARRIER.Checkpoint abeam Abeam checkpoint. --- @field #CARRIER.Checkpoint ninety At the ninety checkpoint. --- @field #CARRIER.Checkpoint groove In the groove checkpoint. --- @field #CARRIER.Checkpoint trap Landing checkpoint. --- @extends Core.Fsm#FSM - ---- Practice Carrier Landings --- --- === --- --- ![Banner Image](..\Presentations\CARRIERTRAINER\CarrierTrainer_Main.png) --- --- # The Trainer Concept --- --- --- bla bla --- --- @field #CARRIERTRAINER -CARRIERTRAINER = { - ClassName = "CARRIERTRAINER", - lid = nil, - Debug = true, - carrier = nil, - carriertype = nil, - alias = nil, - startZone = nil, - giantZone = nil, - players = {}, - menuadded = {}, - upwind = {}, - abeam = {}, - breakearly = {}, - breaklate = {}, - ninety = {}, - wake = {}, - groove = {}, - trap = {}, -} - ---- Main radio menu. --- @field #table MenuF10 -CARRIERTRAINER.MenuF10={} - ---- Carrier trainer class version. --- @field #string version -CARRIERTRAINER.version="0.0.5" - ---- Carrier types. --- @type CARRIERTRAINER.CarrierType --- @field #string Stennis CVN-74 John C. Stennis --- @field #string CarlVinson CVN-70 Carl Vinson --- @field #string Tarawa LHA-1 Tarawa --- @field #string Kuznetsov CV 1143.5 Admiral Kuznetsov -CARRIERTRAINER.CarrierType={ - STENNIS="Stennis", - VINSON="Vinson", - TARAWA="LHA_Tarawa", - KUZNETSOV="KUZNECOW", -} - -CARRIERTRAINER.AircraftType={ - AV8B="AV8BNA", - HORNET="FA-18C_hornet", -} - ---- Player data table holding all important parameters for each player. --- @type CARRIERTRAINER.PlayerData --- @field #number id Player ID. --- @field #string callsign Callsign of player. --- @field #number score Player score. --- @field #number totalscore Score of all landing attempts. --- @field #number passes Number of passes. --- @field #string collectedResultString Results text of all passes. --- @field Wrapper.Unit#UNIT unit Aircraft unit of the player. --- @field #number lowestAltitude Lowest altitude. --- @field #number highestCarrierXDiff --- @field #number secondsStandingStill Time player does not move after a landing attempt. --- @field #string summary Result summary text. --- @field Wrapper.Client#CLIENT Client object of player. --- @field #string difficulty Difficulty level. - ---- Difficulty level. --- @field #string easy Easy difficulty: error margin 10% for high score and 20% for low score. No score for deviation >20%. --- @field #string normal Normal difficulty: error margin 5% deviation from ideal for high score and 10% for low score. No score for deviation >10%. --- @field #string hard Hard difficulty: error margin 2.5% deviation from ideal value for high score and 5% for low score. No score for deviation >5%. - ---- Checkpoint --- @type CARRIERTRAINER.Checkpoint --- @field #number Xmin Minimum allowed longitual distance to carrier. --- @field #number Xmax Maximum allowed longitual distance to carrier. --- @field #number Zmin Minimum allowed latitudal distance to carrier. --- @field #number Zmax Maximum allowed latitudal distance to carrier. --- @field #number Xlimit Latitudal threshold for triggering the next step. --- @field #number Zlimit Latitudal threshold for triggering the next step. --- @field #number Altitude Optimal altitude at this point. --- @field #number AoA Optimal AoA at this point. --- @field #number Speed Optimal speed at this point. --- @field #table Checklist Table of checklist text items to display at this point. - -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- --- Constructor -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- - ---- Create new carrier trainer. --- @param #CARRIERTRAINER self --- @param carriername Name of the aircraft carrier unit as defined in the mission editor. --- @param alias (Optional) Alias for the carrier. This will be used for radio messages and the F10 radius menu. Default is the carrier name as defined in the mission editor. --- @return #CARRIERTRAINER self -function CARRIERTRAINER:New(carriername, alias) - - -- Inherit everthing from FSM class. - local self = BASE:Inherit(self, FSM:New()) -- #CARRIERTRAINER - - -- Set carrier unit. - self.carrier=UNIT:FindByName(carriername) - - if self.carrier then - self.startZone = ZONE_UNIT:New("startZone", self.carrier, 1000, { dx = -2000, dy = 100, relative_to_unit = true }) - self.giantZone = ZONE_UNIT:New("giantZone", self.carrier, 30000, { dx = 0, dy = 0, relative_to_unit = true }) - else - local text=string.format("ERROR: Carrier unit %s could not be found! Make sure this UNIT is defined in the mission editor and check the spelling of the unit name carefully.", carriername) - MESSAGE:New(text, 120):ToAll() - self:E(self.lid..text) - return nil - end - - -- Set some string id for output to DCS.log file. - self.lid=string.format("CARRIERTRAINER %s | ", carriername) - - -- Get carrier type. - self.carriertype=self.carrier:GetTypeName() - - -- Set alias. - self.alias=alias or carriername - - ----------------------- - --- FSM Transitions --- - ----------------------- - - -- Start State. - self:SetStartState("Stopped") - - -- Add FSM transitions. - -- From State --> Event --> To State - self:AddTransition("Stopped", "Start", "Running") - self:AddTransition("Running", "Status", "Running") - self:AddTransition("Running", "Stop", "Stopped") - - - --- Triggers the FSM event "Start" that starts the carrier trainer. Initializes parameters and starts event handlers. - -- @function [parent=#CARRIERTRAINER] Start - -- @param #CARRIERTRAINER self - - --- Triggers the FSM event "Start" after a delay that starts the carrier trainer. Initializes parameters and starts event handlers. - -- @function [parent=#CARRIERTRAINER] __Start - -- @param #CARRIERTRAINER self - -- @param #number delay Delay in seconds. - - --- Triggers the FSM event "Stop" that stops the carrier trainer. Event handlers are stopped. - -- @function [parent=#CARRIERTRAINER] Stop - -- @param #CARRIERTRAINER self - - --- Triggers the FSM event "Stop" that stops the carrier trainer after a delay. Event handlers are stopped. - -- @function [parent=#CARRIERTRAINER] __Stop - -- @param #CARRIERTRAINER self - -- @param #number delay Delay in seconds. - - return self -end - - -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- --- FSM states -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- - ---- On after Start event. Starts the warehouse. Addes event handlers and schedules status updates of reqests and queue. --- @param #CARRIERTRAINER self --- @param #string From From state. --- @param #string Event Event. --- @param #string To To state. -function CARRIERTRAINER:onafterStart(From, Event, To) - - -- Events are handled my MOOSE. - self:I(self.lid..string.format("Starting Carrier Training %s for carrier unit %s of type %s.", CARRIERTRAINER.version, self.carrier:GetName(), self.carriertype)) - - -- Handle events. - self:HandleEvent(EVENTS.Birth) - - -- Init status check - self:__Status(5) -end - ---- On after Status event. Checks player status. --- @param #CARRIERTRAINER self --- @param #string From From state. --- @param #string Event Event. --- @param #string To To state. -function CARRIERTRAINER:onafterStatus(From, Event, To) - - -- Check player status. - self:_CheckPlayerStatus() - - -- Call status again in one second. - self:__Status(-1) -end - ---- On after Stop event. Unhandle events and stop status updates. --- @param #CARRIERTRAINER self --- @param #string From From state. --- @param #string Event Event. --- @param #string To To state. -function CARRIERTRAINER:onafterStop(From, Event, To) - self:UnHandleEvent(EVENTS.Birth) -end - -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- --- EVENT functions -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- - ---- Carrier trainer event handler for event birth. --- @param #CARRIERTRAINER self --- @param Core.Event#EVENTDATA EventData -function CARRIERTRAINER:OnEventBirth(EventData) - self:F3({eventbirth = EventData}) - - local _unitName=EventData.IniUnitName - local _unit, _playername=self:_GetPlayerUnitAndName(_unitName) - - self:T3(self.lid.."BIRTH: unit = "..tostring(EventData.IniUnitName)) - self:T3(self.lid.."BIRTH: group = "..tostring(EventData.IniGroupName)) - self:T3(self.lid.."BIRTH: player = "..tostring(_playername)) - - if _unit and _playername then - - local _uid=_unit:GetID() - local _group=_unit:GetGroup() - local _callsign=_unit:GetCallsign() - - -- Debug output. - local text=string.format("Player %s, callsign %s entered unit %s (ID=%d) of group %s", _playername, _callsign, _unitName, _uid, _group:GetName()) - self:T(self.lid..text) - MESSAGE:New(text, 5):ToAllIf(self.Debug) - - -- Add Menu commands. - self:_AddF10Commands(_unitName) - - -- Init player. - if self.players[_playername]==nil then - self.players[_playername]=self:_InitNewPlayer(_unitName) - else - self:_InitNewRound(self.players[_playername]) - end - - end -end - ---- Initialize player data. --- @param #CARRIERTRAINER self --- @param #string unitname Name of the player unit. --- @return #CARRIERTRAINER.PlayerData Player data. -function CARRIERTRAINER:_InitNewPlayer(unitname) - - local playerData={} --#CARRIERTRAINER.PlayerData - - playerData.unit = UNIT:FindByName(unitname) - playerData.client = CLIENT:FindByName(playerData.unit.UnitName, nil, true) - playerData.callsign = playerData.unit:GetCallsign() - playerData.totalScore = 0 - playerData.passes = 0 - playerData.collectedResultString = "" - - playerData=self:_InitNewRound(playerData) - - return playerData -end - ---- Initialize new approach for player by resetting parmeters to initial values. --- @param #CARRIERTRAINER self --- @param #CARRIERTRAINER.PlayerData playerData Player data. --- @return #CARRIERTRAINER.PlayerData Initialized player data. -function CARRIERTRAINER:_InitNewRound(playerData) - playerData.score = 0 - playerData.summary = "SUMMARY:\n" - playerData.step = 0 - playerData.longDownwindDone = false - playerData.highestCarrierXDiff = -9999999 - playerData.secondsStandingStill = 0 - playerData.lowestAltitude = 999999 - return playerData -end - ---- Increase score for this approach. --- @param #CARRIERTRAINER self --- @param #CARRIERTRAINER.PlayerData playerData Player data. --- @param #number amount Amount by which the score is increased. -function CARRIERTRAINER:_IncreaseScore(playerData, amount) - playerData.score = playerData.score + amount -end - ---- Append text to summary text. --- @param #CARRIERTRAINER self --- @param #CARRIERTRAINER.PlayerData playerData Player data. --- @param #string item Text item appeded to the summary. -function CARRIERTRAINER:_AddToSummary(playerData, item) - playerData.summary = playerData.summary .. item .. "\n" -end - ---- Append text to result text. --- @param #CARRIERTRAINER self --- @param #CARRIERTRAINER.PlayerData playerData Player data. --- @param #string item Text item appeded to the result. -function CARRIERTRAINER:_AddToCollectedResult(playerData, item) - playerData.collectedResultString = playerData.collectedResultString .. item .. "\n" -end - ---- Get relative heading of player wrt carrier. --- @param #CARRIERTRAINER self --- @param Wrapper.Unit#UNIT unit Player unit. --- @return #number Relative heading in degrees. -function CARRIERTRAINER:_GetRelativeHeading(unit) - --local a=self.carrier:GetVec3() - --local b=unit:GetVec3() - --local c={x=b.x-a.x, y=0, z=b.z-a.z} - --local headingCarrier=self.carrier:GetHeading() - --local headingPlayer=unit:GetHeading() - local vC=self.carrier:GetOrientationX() - local vP=unit:GetOrientationX() - - -- Get angle between the two orientation vectors in rad. - local relHead=math.acos(UTILS.VecDot(vC,vP)/UTILS.VecNorm(vC)/UTILS.VecNorm(vP)) - - -- Return heading in degrees. - return math.deg(relHead) -end - - ---- Carrier trainer event handler for event birth. --- @param #CARRIERTRAINER self -function CARRIERTRAINER:_CheckPlayerStatus() - - -- Loop over all players. - for _playerName,_playerData in pairs(self.players) do - local playerData = _playerData --#CARRIERTRAINER.PlayerData - - if playerData then - - self:I("player "..playerData.callsign) - - -- Player unit. - local unit = playerData.unit - - if unit:IsAlive() then - - if unit:IsInZone(self.giantZone) then - --self:_DetailedPlayerStatus(playerData) - end - - if playerData.step==0 and unit:IsInZone(self.giantZone) and unit:InAir() then - self:_NewRound(playerData) - elseif playerData.step == 1 and unit:IsInZone(self.startZone) then - self:_Start(playerData) - elseif playerData.step == 2 and unit:IsInZone(self.giantZone) then - self:_Upwind(playerData) - elseif playerData.step == 3 and unit:IsInZone(self.giantZone) then - self:_Break(playerData, "early") - elseif playerData.step == 4 and unit:IsInZone(self.giantZone) then - self:_Break(playerData, "late") - elseif playerData.step == 5 and unit:IsInZone(self.giantZone) then - self:_Abeam(playerData) - elseif playerData.step == 6 and unit:IsInZone(self.giantZone) then - -- Check long down wind leg. - if not playerData.longDownwindDone then - self:_CheckForLongDownwind(playerData) - end - self:_Ninety(playerData) - elseif playerData.step == 7 and unit:IsInZone(self.giantZone) then - self:_Wake(playerData) - elseif playerData.step == 8 and unit:IsInZone(self.giantZone) then - self:_Groove(playerData) - elseif playerData.step == 9 and unit:IsInZone(self.giantZone) then - self:_Trap(playerData) - end - else - -- Unit not alive. - --playerDatas[i] = nil - end - end - end - -end - ---- Init parameters for USS Stennis carrier. --- @param #CARRIERTRAINER self -function CARRIERTRAINMER:_InitStennis() - - self.Upwind.Xmin=1 - - self.upwind={} - self.upwind.Xmin=-4000 -- TODO Should be withing 4 km behind carrier. Why? - self.upwind.Xmax=nil - self.upwind.Zmin=0 - self.upwind.Zmax=500 - self.upwind. - self.upwind.Alitude=UTILS.FeetToMeters(800) - - - -- Early break checkpoint parameters. - self.breakearly={} - self.Breakearly.Xmin=-500 - self.Breakearly.Xmax=nil - self.Breakearly.Zmin=-3700 - self.Breakearly.Zmax=1500 - self.Breakearly.LimitEarly=-370 --0.2 NM - self.Breakearly.LimitLate=-1470 --0.8 NM - self.Breakearly.Alitude=UTILS.FeetToMeters(800) - -end - ---- Get name of the current pattern step. --- @param #CARRIERTRAINER self --- @param #number step Step --- @return #string Name of the step -function CARRIERTRAINER:_StepName(step) - - local name="unknown" - if step==0 then - name="Unregistered" - elseif step==1 then - name="entering pattern" - elseif step==2 then - name="on upwind leg" - elseif step==3 then - name="early break" - elseif step==4 then - name="late break" - elseif step==5 then - name="abeam" - elseif step==6 then - name="at the wake" - elseif step==7 then - name="at the ninety" - elseif step==8 then - name="in the groove" - elseif step==9 then - name="trapped" - end - - return name -end - ---- Provide info about player status on the fly. --- @param #CARRIERTRAINER self --- @param #CARRIERTRAINER.PlayerData playerData Player data. -function CARRIERTRAINER:_DetailedPlayerStatus(playerData) - - local unit=playerData.unit - - local aoa=unit:GetAoA() - local yaw=unit:GetYaw() - local roll=unit:GetRoll() - local pitch=unit:GetPitch() - local dist=playerData.unit:GetCoordinate():Get2DDistance(self.carrier:GetCoordinate()) - local dx,dz=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 text=string.format("%s, current AoA=%.1f\n", playerData.callsign, aoa) - text=text..string.format("roll=%.1f yaw=%.1f pitch=%.1f\n", roll, yaw, pitch) - 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 - -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- --- CARRIER TRAINING functions -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- - ---- Initialize player data. --- @param #CARRIERTRAINER self --- @param #CARRIERTRAINER.PlayerData playerData Player data. -function CARRIERTRAINER:_NewRound(playerData) - - local text=string.format("Welcome back, %s! Cleared for approach. TCN 1X, BRC 354 (MAG HDG).", playerData.callsign) - MESSAGE:New(text, 5):ToClient(playerData.client) - - self:_InitNewRound(playerData) - playerData.step = 1 -end - ---- Start landing pattern. --- @param #CARRIERTRAINER self --- @param #CARRIERTRAINER.PlayerData playerData Player data table. -function CARRIERTRAINER:_Start(playerData) - local hint = string.format("Entering the pattern, %s! Aim for 800 feet and 350-400 kts on the upwind.", playerData.callsign) - self:_SendMessageToPlayer(hint, 8, playerData) - playerData.score = 0 - playerData.step = 2 -end - ---- Upwind leg. --- @param #CARRIERTRAINER self --- @param #CARRIERTRAINER.PlayerData playerData Player data table. -function CARRIERTRAINER:_Upwind(playerData) - - -- Player and carrier position vector. - local position = playerData.unit:GetVec3() - local carrierPosition = self.carrier:GetVec3() - - local diffZ = position.z - carrierPosition.z - local diffX = position.x - carrierPosition.x - - -- Get distances between carrier and player unit (parallel and perpendicular to direction of movement of carrier) - diffX, diffZ = self:_GetDistances(playerData.unit) - - - - -- Too far away. - -- Should be between 0-500 meters right of carrier. - --if (diffZ > 500 or diffZ < 0 or diffX < -4000) then - if self:_CheckAbort(diffX,diffZ, self.Upwind) then - --MESSAGE:New(string.format("Abort: diffX=%d (min=-4000, max=nil), diffZ=%d (min=0, max=500)", diffX, diffZ)):ToAllIf(self.Debug) - self:_AbortPattern(playerData, diffX, diffZ, self.Upwind) - return - end - - -- Now before the boat. - if diffX > 0 then - - local idealAltitude = 800 - local altitude = UTILS.MetersToFeet(position.y) - - local hint = "" - local score = 0 - - if altitude > 850 then - score = 5 - hint = "You're high on the upwind." - elseif altitude > 830 then - score = 7 - hint = "You're slightly high on the upwind." - elseif altitude < 750 then - score = 5 - hint = "You're low on the upwind." - elseif altitude < 770 then - score = 7 - hint = "You're slightly low on the upwind." - else - score = 10 - hint = "Good altitude on the upwind." - end - - -- Increase score. - self:_IncreaseScore(playerData, score) - - self:_SendMessageToPlayer(hint, 8, playerData) - - self:_PrintAltitudeFeedback(altitude, idealAltitude, playerData) - self:_PrintScore(score, playerData, true) - - self:_AddToSummary(playerData, hint) - - -- Set step. - playerData.step = 3 - - end -end - ---- Calculate distances between carrier and player unit. --- @param #CARRIERTRAINER self --- @param Wrapper.Unit#UNIT unit Player unit --- @return #number Distance in the direction of the orientation of the carrier. --- @return #number Distance perpendicular to the orientation of the carrier. -function CARRIERTRAINER:_GetDistances(unit) - - -- Vector to carrier - local a=self.carrier:GetVec3() - - -- Vector to player - local b=unit:GetVec3() - - -- Vector from carrier to player. - local c={x=b.x-a.x, y=0, z=b.z-a.z} - - -- Orientation of carrier. - local x=self.carrier:GetOrientationX() - - -- Projection of player pos on x component. - local dx=UTILS.VecDot(x,c) - - -- Orientation of carrier. - local z=self.carrier:GetOrientationZ() - - -- Projection of player pos on z component. - local dz=UTILS.VecDot(z,c) - - return dx,dz -end - ---- Check if a player is within the right area. --- @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. --- @return #boolean If true, approach should be aborted. -function CARRIERTRAINER:_CheckAbort(X, Z, pos) - - local abort=false - if pos.Xmin and Xpos.Xmax then - abort=true - elseif pos.Zmin and Zpos.Zmax then - abort=true - end - - return abort -end - ---- Generate a text if a player is too far from where he should be. --- @param #CARRIERTRAINER self --- @param #number X X distance player to carrier. --- @param #number Z Z distance player to carrier. --- @param #table posData Position data limits. -function CARRIERTRAINER:_TooFarOutText(X, Z, posData) - - local text="You are too far" - - local xtext=nil - if posData.Xmin and XposData.Xmax then - xtext=" behind" - end - - local ztext=nil - if posData.Zmin and ZposData.Zmax then - ztext=" starboard (right)" - end - - if xtext and ztext then - text=text..xtext.." and"..ztext - elseif xtext then - text=text..xtext - elseif ztext then - text=text..ztext - end - - text=text.." of the carrier!" - - return text -end - ---- Pattern aborted. --- @param #CARRIERTRAINER self --- @param #CARRIERTRAINER.PlayerData playerData Player data. --- @param #number X X distance player to carrier. --- @param #number Z Z distance player to carrier. --- @param #table posData Position data. -function CARRIERTRAINER:_AbortPattern(playerData, X, Z, posData) - local toofartext=self:_TooFarOutText(X, Z, posData) - self:_SendMessageToPlayer(toofartext.." Abort approach!", 15, playerData ) - MESSAGE:New(string.format("Abort: X=%d Xmin=%s, Xmax=%s | Z=%d Zmin=%s Zmax=%s", X, tostring(posData.Xmin), tostring(posData.Xmax), Z, tostring(posData.Zmin), tostring(posData.Zmax)), 60):ToAllIf(self.Debug) - self:_AddToSummary(playerData, "Approach aborted.") - self:_PrintFinalScore(playerData, 30, -2) - self:_HandleCollectedResult(playerData, -2) - playerData.step = 0 -end - ---- Break. --- @param #CARRIERTRAINER self --- @param #CARRIERTRAINER.PlayerData playerData Player data table. --- @param #string part Part of the break. -function CARRIERTRAINER:_Break(playerData, part) - - local playerPosition = playerData.unit:GetVec3() - local carrierPosition = self.carrier:GetVec3() - - local diffZ = playerPosition.z - carrierPosition.z - local diffX = playerPosition.x - carrierPosition.x - - -- Get distances between carrier and player unit (parallel and perpendicular to direction of movement of carrier) - diffX, diffZ = self:_GetDistances(playerData.unit) - - -- Abort when - self.Break={} - self.Break.Xmin=-500 - self.Break.Xmax=nil - self.Break.Zmin=-3700 - self.Break.Zmax=1500 - self.Break.LimitEarly=-370 --0.2 NM - self.Break.LimitLate=-1470 --0.8 NM - self.Break.Alitude=UTILS.FeetToMeters(800) - - --if (diffZ > 1500 or diffZ < -3700 or diffX < -500) then - if self:_CheckAbort(diffX, diffZ, self.Break) then - self:_AbortPattern(playerData, diffX, diffZ, self.Break) - return - end - - -- Break - -- z= -370 - -- y= 800 - -- x> -500 - - local limit = -370 --0.2 NM - - if part == "late" then - limit = -1470 -- 0.8 NM - end - - -- Check if too far left - if diffZ < limit then - - local idealAltitude = 800 - local altitude = UTILS.Round( UTILS.MetersToFeet( playerPosition.y ) ) - - local hint = "" - local score = 0 - - if(altitude > 880) then - score = 5 - hint = "You're high in the " .. part .. " break." - elseif(altitude > 850) then - score = 7 - hint = "You're slightly high in the " .. part .. " break." - elseif (altitude < 720) then - score = 5 - hint = "You're low in the " .. part .. " break." - elseif (altitude < 750) then - score = 7 - hint = "You're slightly low in the " .. part .. " break." - else - score = 10 - hint = "Good altitude in the " .. part .. " break!" - end - - self:_IncreaseScore(playerData, score) - - self:_SendMessageToPlayer( hint, 8, playerData ) - self:_PrintAltitudeFeedback(altitude, idealAltitude, playerData) - self:_PrintScore(score, playerData, true) - - self:_AddToSummary(playerData, hint) - - if (part == "early") then - playerData.step = 4 - else - playerData.step = 5 - end - end -end - ---- Break. --- @param #CARRIERTRAINER self --- @param #CARRIERTRAINER.PlayerData playerData Player data table. -function CARRIERTRAINER:_Abeam(playerData) - local playerPosition = playerData.unit:GetVec3() - local carrierPosition = self.carrier:GetVec3() - - local diffZ = playerPosition.z - carrierPosition.z - local diffX = playerPosition.x - carrierPosition.x - - -- Get distances between carrier and player unit (parallel and perpendicular to direction of movement of carrier) - diffX, diffZ = self:_GetDistances(playerData.unit) - - self.Abeam={} - self.Abeam.Xmin=nil - self.Abeam.Xmax=nil - self.Abeam.Zmin=-3700 - self.Abeam.Zmax=-1000 - self.Abeam.Limit=-200 - self.Abeam.Alitude=UTILS.FeetToMeters(600) - - -- Abort if - -- less than 1.0 km left of boat (no closer than 1 km to boat - -- more than 3.7 km left of boat - --if (diffZ > -1000 or diffZ < -3700) then - if self:_CheckAbort(diffX, diffZ, self.Abeam) then - --MESSAGE:New(string.format("Abort: diffX=%d (min=nil, max=nil), diffZ=%d (min=-3700, max=-1000)", diffX, diffZ)):ToAllIf(self.Debug) - self:_AbortPattern(playerData, diffX, diffZ, self.Abeam) - return - end - - -- Abeam pos: - -- x= -200 - -- z=-2160 - -- y= 600 - - -- Abeam pos 200 meters behind ship - local limit = -200 - - if diffX < limit then - - -- Get AoA. - local aoa = playerData.unit:GetAoA() - local aoaFeedback = self:_PrintAoAFeedback(aoa, 8.1, playerData) - - local onSpeedScore = self:_GetOnSpeedScore(aoa) - - local idealAltitude = 600 - local altitude = UTILS.Round( UTILS.MetersToFeet( playerPosition.y ) ) - - local hint = "" - local score = 0 - - if(altitude > 700) then - score = 5 - hint = "You're high (" .. altitude .. " ft) abeam" - elseif(altitude > 650) then - score = 7 - hint = "You're slightly high (" .. altitude .. " ft) abeam" - elseif (altitude < 540) then - score = 5 - hint = "You're low (" .. altitude .. " ft) abeam" - elseif (altitude < 570) then - score = 7 - hint = "You're slightly low (" .. altitude .. " ft) abeam" - else - score = 10 - hint = "Good altitude (" .. altitude .. " ft) abeam" - end - - local distanceHint = "" - local distanceScore - local diffEast = carrierPosition.z - playerPosition.z - - local nm = diffEast / 1852 --nm conversion - local idealDistance = 1.2 - - local roundedNm = UTILS.Round(nm, 2) - - if (nm < 1.0) then - distanceScore = 0 - distanceHint = "too close to the boat (" .. roundedNm .. " nm)" - elseif(nm < 1.1) then - distanceScore = 5 - distanceHint = "slightly too close to the boat (" .. roundedNm .. " nm)" - elseif(nm < 1.3) then - distanceScore = 10 - distanceHint = "with perfect distance to the boat (" .. roundedNm .. " nm)" - elseif(nm < 1.4) then - distanceScore = 5 - distanceHint = "slightly too far from the boat (" .. roundedNm .. " nm)" - else - distanceScore = 0 - distanceHint = "too far from the boat (" .. roundedNm .. " nm)" - end - - local fullHint = hint .. ", " .. distanceHint - - self:_SendMessageToPlayer( fullHint, 8, playerData ) - self:_SendMessageToPlayer( "(Target: 600 ft and 1.2 nm).", 8, playerData ) - - self:_IncreaseScore(playerData, score + distanceScore + onSpeedScore) - self:_PrintScore(score + distanceScore + onSpeedScore, playerData, true) - - self:_AddToSummary(playerData, fullHint .. " (" .. aoaFeedback .. ")") - playerData.step = 6 - end -end - ---- Down wind long check. --- @param #CARRIERTRAINER self --- @param #CARRIERTRAINER.PlayerData playerData Player data table. -function CARRIERTRAINER:_CheckForLongDownwind(playerData) - - local playerPosition = playerData.unit:GetVec3() - local carrierPosition = self.carrier:GetVec3() - - local limit = -1500 - - -- Get distances between carrier and player unit (parallel and perpendicular to direction of movement of carrier) - local diffX, diffZ = self:_GetDistances(playerData.unit) - - -- Check we are not too far out w.r.t back of the boat. - if diffX < limit then - - local headingPlayer = playerData.unit:GetHeading() - local headingCarrier = self.carrier:GetHeading() - - --TODO: Take carrier heading != 0 into account! - - if (headingPlayer > 170) then - - local hint = "Too long downwind. Turn final earlier next time." - self:_SendMessageToPlayer( hint, 8, playerData ) - local score = -40 - self:_IncreaseScore(playerData, score) - self:_PrintScore(score, playerData, true) - self:_AddToSummary(playerData, hint) - playerData.longDownwindDone = true - end - - end -end - ---- Ninety. --- @param #CARRIERTRAINER self --- @param #CARRIERTRAINER.PlayerData playerData Player data table. -function CARRIERTRAINER:_Ninety(playerData) - local playerPosition = playerData.unit:GetVec3() - local carrierPosition = self.carrier:GetVec3() - - local diffZ = playerPosition.z - carrierPosition.z - local diffX = playerPosition.x - carrierPosition.x - - -- Get distances between carrier and player unit (parallel and perpendicular to direction of movement of carrier) - diffX, diffZ = self:_GetDistances(playerData.unit) - - self.Ninety={} - self.Ninety.Xmin=-3700 - self.Ninety.Xmax=0 - self.Ninety.Zmin=-3700 - self.Ninety.Zmax=nil - self.Ninety.Limit=-1111 - self.Ninety.Altitude=UTILS.FeetToMeters(500) - - --if(diffZ < -3700 or diffX < -3700 or diffX > 0) then - if self:_CheckAbort(diffX, diffZ, self.Ninety) then - --MESSAGE:New(string.format("Abort: diffX=%d (min=-3700, max=0), diffZ=%d (min=-3700, max=nil)", diffX, diffZ)):ToAllIf(self.Debug) - self:_AbortPattern(playerData, diffX, diffZ, self.Ninety) - return - end - - local limitEast = -1111 --0.6nm - - if diffZ > limitEast then - local idealAltitude = 500 - local altitude = UTILS.Round( UTILS.MetersToFeet( playerPosition.y ) ) - - local hint = "" - local score = 0 - - if(altitude > 600) then - score = 5 - hint = "You're high at the 90." - elseif(altitude > 550) then - score = 7 - hint = "You're slightly high at the 90." - elseif (altitude < 380) then - score = 5 - hint = "You're low at the 90." - elseif (altitude < 420) then - score = 7 - hint = "You're slightly low at the 90." - else - score = 10 - hint = "Good altitude at the 90!" - end - - self:_SendMessageToPlayer( hint, 8, playerData ) - self:_PrintAltitudeFeedback(altitude, idealAltitude, playerData) - - --local aoa = math.deg(mist.getAoA(playerData.mistUnit)) - local aoa = playerData.unit:GetAoA() - local aoaFeedback = self:_PrintAoAFeedback(aoa, 8.1, playerData) - - local onSpeedScore = self:_GetOnSpeedScore(aoa) - - self:_IncreaseScore(playerData, score + onSpeedScore) - self:_PrintScore(score + onSpeedScore, playerData, true) - - self:_AddToSummary(playerData, hint .. " (" .. aoaFeedback .. ")") - - playerData.longDownwindDone = true - playerData.step = 7 - end -end - ---- Wake. --- @param #CARRIERTRAINER self --- @param #CARRIERTRAINER.PlayerData playerData Player data table. -function CARRIERTRAINER:_Wake(playerData) - local playerPosition = playerData.unit:GetVec3() - local carrierPosition = self.carrier:GetVec3() - - local diffZ = playerPosition.z - carrierPosition.z - local diffX = playerPosition.x - carrierPosition.x - - -- Get distances between carrier and player unit (parallel and perpendicular to direction of movement of carrier) - diffX, diffZ = self:_GetDistances(playerData.unit) - - self.Wake={} - self.Wake.Xmin=-4000 - self.Wake.Xmax=0 - self.Wake.Zmin=-2000 - self.Wake.Zmax=nil - self.Wake.Limit=0 - self.Wake.Alitude=UTILS.FeetToMeters(370) - - --if (diffZ < -2000 or diffX < -4000 or diffX > 0) then - if self:_CheckAbort(diffX, diffZ, self.Wake) then - MESSAGE:New(string.format("Abort: diffX=%d (min=-4000, max=0), diffZ=%d (min=-2000, max=nil)", diffX, diffZ)):ToAllIf(self.Debug) - self:_AbortPattern(playerData, diffX, diffZ, self.Wake) - return - end - - if diffZ > 0 then - local idealAltitude = 370 - local altitude = UTILS.Round( UTILS.MetersToFeet( playerPosition.y ) ) - - local hint = "" - local score = 0 - - if(altitude > 500) then - score = 5 - hint = "You're high at the wake." - elseif(altitude > 450) then - score = 7 - hint = "You're slightly high at the wake." - elseif (altitude < 300) then - score = 5 - hint = "You're low at the wake." - elseif (altitude < 340) then - score = 7 - hint = "You're slightly low at the wake." - else - score = 10 - hint = "Good altitude at the wake!" - end - - self:_SendMessageToPlayer( hint, 8, playerData ) - self:_PrintAltitudeFeedback(altitude, idealAltitude, playerData) - - --local aoa = math.deg(mist.getAoA(playerData.mistUnit)) - local aoa = playerData.unit:GetAoA() - local aoaFeedback = self:_PrintAoAFeedback(aoa, 8.1, playerData) - - local onSpeedScore = self:_GetOnSpeedScore(aoa) - - self:_IncreaseScore(playerData, score + onSpeedScore) - self:_PrintScore(score + onSpeedScore, playerData, true) - self:_AddToSummary(playerData, hint .. " (" .. aoaFeedback .. ")") - - playerData.step = 8 - end -end - ---- Groove. --- @param #CARRIERTRAINER self --- @param #CARRIERTRAINER.PlayerData playerData Player data table. -function CARRIERTRAINER:_Groove(playerData) - local playerPosition = playerData.unit:GetVec3() - local carrierPosition = self.carrier:GetVec3() - - local diffX = playerPosition.x - (carrierPosition.x - 100) - local diffZ = playerPosition.z - carrierPosition.z - - --TODO -100?! - -- Get distances between carrier and player unit (parallel and perpendicular to direction of movement of carrier) - diffX, diffZ = self:_GetDistances(playerData.unit) - - --diffX=diffX+100 - - self.Groove={} - self.Groove.Xmin=-4000 - self.Groove.Xmax=100 - - -- In front of carrier or more than 4 km behind carrier. - --if (diffX > 0 or diffX < -4000) then - if self:_CheckAbort(diffX, diffZ, self.Groove) then - --MESSAGE:New(string.format("Abort: diffX=%d (min=-4000, max=0), diffZ=%d (min=nil, max=nil)", diffX, diffZ)):ToAllIf(self.Debug) - self:_AbortPattern(playerData, diffX, diffZ, self.Groove) - return - end - - --TODO: - if (diffX > -500) then --Reached in close before groove - local hint = "You're too far left and never reached the groove." - self:_SendMessageToPlayer( hint, 8, playerData ) - self:_PrintScore(0, playerData, true) - self:_AddToSummary(playerData, hint) - playerData.step = 9 - else - - local limitDeg = 8.0 - - local fraction = diffZ / (-diffX) - local asinValue = math.asin(fraction) - local angle = math.deg(asinValue) - - if diffZ > -1300 and angle > limitDeg then - local idealAltitude = 300 - local altitude = UTILS.Round( UTILS.MetersToFeet( playerPosition.y ) ) - - local hint = "" - local score = 0 - - if (altitude > 450) then - score = 5 - hint = "You're high in the groove." - elseif (altitude > 350) then - score = 7 - hint = "You're slightly high in the groove." - elseif (altitude < 240) then - score = 5 - hint = "You're low in the groove." - elseif (altitude < 270) then - score = 7 - hint = "You're slightly low in the groove." - else - score = 10 - hint = "Good altitude in the groove!" - end - - self:_SendMessageToPlayer(hint, 8, playerData) - self:_PrintAltitudeFeedback(altitude, idealAltitude, playerData) - - --local aoa = math.deg(mist.getAoA(playerData.mistUnit)) - local aoa = playerData.unit:GetAoA() - local aoaFeedback = self:_PrintAoAFeedback(aoa, 8.1, playerData) - - local onSpeedScore = self:_GetOnSpeedScore(aoa) - - self:_IncreaseScore(playerData, score + onSpeedScore) - self:_PrintScore(score + onSpeedScore, playerData, true) - - local fullHint = hint .. " (" .. aoaFeedback .. ")" - - self:_AddToSummary(playerData, fullHint) - - playerData.step = 9 - end - end -end - ---- Trap. --- @param #CARRIERTRAINER self --- @param #CARRIERTRAINER.PlayerData playerData Player data table. -function CARRIERTRAINER:_Trap(playerData) - local playerPosition = playerData.unit:GetVec3() - local carrierPosition = self.carrier:GetVec3() - - local playerVelocity = playerData.unit:GetVelocityKMH() - local carrierVelocity = self.carrier:GetVelocityKMH() - - local diffZ = playerPosition.z - carrierPosition.z - local diffX = playerPosition.x - carrierPosition.x - - -- Get distances between carrier and player unit (parallel and perpendicular to direction of movement of carrier) - diffZ, diffX = self:_GetDistances(playerData.unit) - - self.Trap={} - self.Trap.Xmin=-3000 - self.Trap.Xmax=nil - self.Trap.Zmin=-2000 - self.Trap.Zmax=2000 - self.Trap.Limit=nil - self.Trap.Alitude=nil - - --if(diffZ < -2000 or diffZ > 2000 or diffX < -3000) then - if self:_CheckAbort(diffX, diffZ, self.Trap) then - self:_AbortPattern(playerData, diffX, diffZ, self.Trap) - return - end - - if (diffX > playerData.highestCarrierXDiff) then - playerData.highestCarrierXDiff = diffX - end - - if (playerPosition.y < playerData.lowestAltitude) then - playerData.lowestAltitude = playerPosition.y - end - - if math.abs(playerVelocity - carrierVelocity) < 0.01 then - playerData.secondsStandingStill = playerData.secondsStandingStill + 1 - - if diffX < playerData.highestCarrierXDiff or playerData.secondsStandingStill > 5 then - - env.info("Trap identified! diff " .. diffX .. ", highestCarrierXDiff" .. playerData.highestCarrierXDiff .. ", secondsStandingStill: " .. playerData.secondsStandingStill); - - local wire = 1 - local score = -10 - - if(diffX < -14) then - wire = 1 - score = -15 - elseif(diffX < -3) then - wire = 2 - score = 10 - elseif (diffX < 10) then - wire = 3 - score = 20 - else - wire = 4 - score = 7 - end - - self:_IncreaseScore(playerData, score) - - self:_SendMessageToPlayer( "TRAPPED! " .. wire .. "-wire!", 30, playerData ) - self:_PrintScore(score, playerData, false) - - env.info("Distance! " .. diffX .. " meters resulted in a " .. wire .. "-wire estimation."); - - local fullHint = "Trapped catching the " .. wire .. "-wire." - - self:_AddToSummary(playerData, fullHint) - - self:_PrintFinalScore(playerData, 60, wire) - self:_HandleCollectedResult(playerData, wire) - playerData.step = 0 - end - - elseif (diffX > 150) then - - local wire = 0 - local hint = "" - local score = 0 - if (playerData.lowestAltitude < 23) then - hint = "You boltered." - else - hint = "You were waved off." - wire = -1 - score = -10 - end - - self:_SendMessageToPlayer( hint, 8, playerData ) - self:_PrintScore(score, playerData, true) - - self:_AddToSummary(playerData, hint) - - self:_PrintFinalScore(playerData, 60, wire) - self:_HandleCollectedResult(playerData, wire) - - playerData.step = 0 - end -end - ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ --- Menu Functions ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ - ---- Add menu commands for player. --- @param #CARRIERTRAINER self --- @param #string _unitName Name of player unit. -function CARRIERTRAINER:_AddF10Commands(_unitName) - self:F(_unitName) - - -- Get player unit and name. - local _unit, playername = self:_GetPlayerUnitAndName(_unitName) - - -- Check for player unit. - if _unit and playername then - - -- Get group and ID. - local group=_unit:GetGroup() - local _gid=group:GetID() - - if group and _gid then - - if not self.menuadded[_gid] then - - -- Enable switch so we don't do this twice. - self.menuadded[_gid] = true - - -- Main F10 menu: F10/Carrier Trainer// - if CARRIERTRAINER.MenuF10[_gid] == nil then - CARRIERTRAINER.MenuF10[_gid]=missionCommands.addSubMenuForGroup(_gid, "Carrier Trainer") - end - local _rangePath = missionCommands.addSubMenuForGroup(_gid, self.alias, CARRIERTRAINER.MenuF10[_gid]) - local _statsPath = missionCommands.addSubMenuForGroup(_gid, "Results", _rangePath) - local _settingsPath = missionCommands.addSubMenuForGroup(_gid, "My Settings", _rangePath) - local _infoPath = missionCommands.addSubMenuForGroup(_gid, "Carrier Info", _rangePath) - - -- F10/On the Range//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/On the Range//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/On the Range//Range Information - --missionCommands.addCommandForGroup(_gid, "General Info", _infoPath, self._DisplayRangeInfo, self, _unitName) - missionCommands.addCommandForGroup(_gid, "Weather Report", _infoPath, self._DisplayCarrierWeather, self, _unitName) - --missionCommands.addCommandForGroup(_gid, "Bombing Targets", _infoPath, self._DisplayBombTargets, self, _unitName) - --missionCommands.addCommandForGroup(_gid, "Strafe Pits", _infoPath, self._DisplayStrafePits, self, _unitName) - end - else - self:T(self.lid.."Could not find group or group ID in AddF10Menu() function. Unit name: ".._unitName) - end - else - self:T(self.lid.."Player unit does not exist in AddF10Menu() function. Unit name: ".._unitName) - end - -end - ---- Report weather conditions at range. Temperature, QFE pressure and wind data. --- @param #CARRIERTRAINER self --- @param #string _unitname Name of the player unit. -function CARRIERTRAINER:_DisplayCarrierWeather(_unitname) - self:F(_unitname) - - -- Get player unit and player name. - local unit, playername = self:_GetPlayerUnitAndName(_unitname) - - -- Check if we have a player. - if unit and playername then - - -- Message text. - local text="" - - -- 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 Beaufort wind scale. - local Bn,Bd=UTILS.BeaufortScale(Ws) - - local WD=string.format('%03d°', Wd) - local Ts=string.format("%d°C",T) - - local hPa2inHg=0.0295299830714 - local hPa2mmHg=0.7500615613030 - - local settings=_DATABASE:GetPlayerSettings(playername) or _SETTINGS --Core.Settings#SETTINGS - local tT=string.format("%d°C",T) - local tW=string.format("%.1f m/s", Ws) - local tP=string.format("%.1f mmHg", P*hPa2mmHg) - if settings:IsImperial() then - tT=string.format("%d°F", UTILS.CelciusToFarenheit(T)) - 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) - 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) - else - self:T(self.lid..string.format("ERROR! Could not find player unit in RangeInfo! Name = %s", _unitname)) - end -end - -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- --- MISC functions -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- - ---- Send message about altitude feedback. --- @param #CARRIERTRAINER self --- @param #number altitude Current altitude of the player. --- @param #number idealAltitude Ideal altitude. --- @param #CARRIERTRAINER.PlayerData playerData Player data. -function CARRIERTRAINER:_PrintAltitudeFeedback(altitude, idealAltitude, playerData) - local text=string.format("Alt: %d feet (Target: %d feet)", altitude, idealAltitude) - self:_SendMessageToPlayer(text, 8, playerData) -end - ---- Score for correct AoA. --- @param #CARRIERTRAINER self --- @param #number AoA Angle of attack. -function CARRIERTRAINER:_GetOnSpeedScore(AoA) - local score = 0 - if(AoA > 9.5) then --Slow - score = 0 - elseif(AoA > 9) then --Slightly slow - score = 5 - elseif(AoA > 7.25) then --On speed - score = 10 - elseif(AoA > 6.7) then --Slightly fast - score = 5 - else --Fast - score = 0 - end - - return score -end - ---- Print AoA feedback. --- @param #CARRIERTRAINER self --- @param #number AoA Angle of attack. --- @param #number idealAoA Ideal AoA. --- @param #CARRIERTRAINER.PlayerData playerData Player data. --- @return #string Feedback hint. -function CARRIERTRAINER:_PrintAoAFeedback(AoA, idealAoA, playerData) - - local hint = "" - if(AoA > 9.5) then - hint = "You're slow." - elseif(AoA > 9) then - hint = "You're slightly slow." - elseif(AoA > 7.25) then - hint = "You're on speed!" - elseif(AoA > 6.7) then - hint = "You're slightly fast." - else - hint = "You're fast." - end - - local roundedAoA = UTILS.Round(AoA, 2) - - self:_SendMessageToPlayer(hint .. " AOA: " .. roundedAoA .. " (Target: " .. idealAoA .. ")", 8, playerData) - - return hint -end - ---- Send message to playe client. --- @param #CARRIERTRAINER self --- @param #string message The message to send. --- @param #number duration Display message duration. --- @param #CARRIERTRAINER.PlayerData playerData Player data. -function CARRIERTRAINER:_SendMessageToPlayer(message, duration, playerData) - if playerData.client then - MESSAGE:New(string.format("%s, %s, ", self.alias, playerData.callsign)..message, duration):ToClient(playerData.client) - end -end - ---- Send message to playe client. --- @param #CARRIERTRAINER self --- @param #number score Score. --- @param #CARRIERTRAINER.PlayerData playerData Player data. -function CARRIERTRAINER:_PrintScore(score, playerData, alsoPrintTotalScore) - if(alsoPrintTotalScore) then - self:_SendMessageToPlayer( "Score: " .. score .. " (Total: " .. playerData.score .. ")", 8, playerData ) - else - self:_SendMessageToPlayer( "Score: " .. score, 8, playerData ) - end -end - ---- Display final score. --- @param #CARRIERTRAINER self --- @param #CARRIERTRAINER.PlayerData playerData Player data. --- @param #number duration Duration for message display. -function CARRIERTRAINER:_PrintFinalScore(playerData, duration, wire) - local wireText = "" - if(wire == -2) then - wireText = "Aborted approach" - elseif(wire == -1) then - wireText = "Wave-off" - elseif(wire == 0) then - wireText = "Bolter" - else - wireText = wire .. "-wire" - end - - MessageToAll( playerData.callsign .. " - Final score: " .. playerData.score .. " / 140 (" .. wireText .. ")", duration, "FinalScore" ) - self:_SendMessageToPlayer( playerData.summary, duration, playerData ) -end - ---- Collect result. --- @param #CARRIERTRAINER self --- @param #CARRIERTRAINER.PlayerData playerData Player data. --- @param #number wire Trapped wire. -function CARRIERTRAINER:_HandleCollectedResult(playerData, wire) - - local newString = "" - if(wire == -2) then - newString = playerData.score .. " (Aborted)" - elseif(wire == -1) then - newString = playerData.score .. " (Wave-off)" - elseif(wire == 0) then - newString = playerData.score .. " (Bolter)" - else - newString = playerData.score .. " (" .. wire .."W)" - end - - playerData.totalScore = playerData.totalScore + playerData.score - playerData.passes = playerData.passes + 1 - - if playerData.collectedResultString == "" then - playerData.collectedResultString = newString - else - playerData.collectedResultString = playerData.collectedResultString .. ", " .. newString - MessageToAll( playerData.callsign .. "'s " .. playerData.passes .. " passes: " .. playerData.collectedResultString .. " (TOTAL: " .. playerData.totalScore .. ")" , 30, "CollectedResult" ) - end - - local heading=playerData.unit:GetCoordinate():HeadingTo(self.startZone:GetCoordinate()) - local distance=playerData.unit:GetCoordinate():Get2DDistance(self.startZone:GetCoordinate()) - local text=string.format("%s, fly heading %d for %d nm to restart the pattern.", playerData.callsign, heading, UTILS.MetersToNM(distance)) - --"Return south 4 nm (over the trailing ship), towards WP 1, to restart the pattern." - self:_SendMessageToPlayer(text, 30, playerData) -end - - ---- Get the formatted score. --- @param #CARRIERTRAINER self --- @param #number score Score of player. --- @param #number maxScore Max score possible. --- @return #string Formatted score text. -function CARRIERTRAINER:_GetFormattedScore(score, maxScore) - if(score < maxScore) then - return " (" .. score .. " points)." - else - return " (" .. score .. " points)!" - end -end - ---- Get distance feedback. --- @param #CARRIERTRAINER self --- @param #number distance Distance to boat. --- @param #number idealDistance Ideal distance. --- @return #string Feedback text. -function CARRIERTRAINER:_GetDistanceFeedback(distance, idealDistance) - return distance .. " nm (Target: " .. idealDistance .. " nm)" -end - - ---- Returns the unit of a player and the player name. If the unit does not belong to a player, nil is returned. --- @param #CARRIERTRAINER self --- @param #string _unitName Name of the player unit. --- @return Wrapper.Unit#UNIT Unit of player or nil. --- @return #string Name of the player or nil. -function CARRIERTRAINER:_GetPlayerUnitAndName(_unitName) - self:F2(_unitName) - - if _unitName ~= nil then - - -- Get DCS unit from its name. - local DCSunit=Unit.getByName(_unitName) - - if DCSunit then - - local playername=DCSunit:getPlayerName() - local unit=UNIT:Find(DCSunit) - - self:T2({DCSunit=DCSunit, unit=unit, playername=playername}) - if DCSunit and unit and playername then - return unit, playername - end - - end - - end - - -- Return nil if we could not find a player. - return nil,nil -end - - From 003b0c875e9a1721679676361f6f44d7085e21a9 Mon Sep 17 00:00:00 2001 From: Frank Date: Sat, 13 Oct 2018 23:25:45 +0200 Subject: [PATCH 09/10] Added wind ot AoA COORDINATE * added GetWindWithTurbulences function POSITIONABLE * Included wind in AoA calculation --- Moose Development/Moose/Core/Point.lua | 18 ++++++++++++++++++ .../Moose/Wrapper/Positionable.lua | 10 +++++++++- 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/Moose Development/Moose/Core/Point.lua b/Moose Development/Moose/Core/Point.lua index 4b2c592a1..528ac9a84 100644 --- a/Moose Development/Moose/Core/Point.lua +++ b/Moose Development/Moose/Core/Point.lua @@ -767,6 +767,24 @@ do -- COORDINATE -- Return wind direction and strength km/h. return direction, strength end + + --- Returns the wind direction (from) and strength. + -- @param #COORDINATE self + -- @param height (Optional) parameter specifying the height ASL. The minimum height will be always be the land height since the wind is zero below the ground. + -- @return Direction the wind is blowing from in degrees. + function COORDINATE:GetWindWithTurbulenceVec3(height) + + -- AGL height if + local landheight=self:GetLandHeight()+0.1 -- we at 0.1 meters to be sure to be above ground since wind is zero below ground level. + + -- Point at which the wind is evaluated. + local point={x=self.x, y=math.max(height or self.y, landheight), z=self.z} + + -- Get wind velocity vector including turbulences. + local vec3 = atmosphere.getWindWithTurbulence(point) + + return vec3 + end --- Returns a text documenting the wind direction (from) and strength according the measurement system @{Settings}. diff --git a/Moose Development/Moose/Wrapper/Positionable.lua b/Moose Development/Moose/Wrapper/Positionable.lua index ac64a90d6..fef546f38 100644 --- a/Moose Development/Moose/Wrapper/Positionable.lua +++ b/Moose Development/Moose/Wrapper/Positionable.lua @@ -671,8 +671,16 @@ function POSITIONABLE:GetAoA() if unitvel and UTILS.VecNorm(unitvel)~=0 then + -- Get wind vector including turbulences. + local wind=self:GetCoordinate():GetWindWithTurbulenceVec3() + + -- Include wind vector. + unitvel.x=unitvel.x-wind.x + unitvel.y=unitvel.y-wind.y + unitvel.z=unitvel.z-wind.z + -- Unit velocity transformed into aircraft axes directions. - local AxialVel = {} + local AxialVel = {} -- Transform velocity components in direction of aircraft axes. AxialVel.x = UTILS.VecDot(unitpos.x, unitvel) From 112945b3f63e7e2608d40b902f1abd7b11ee0a5d Mon Sep 17 00:00:00 2001 From: FlightControl Date: Sun, 14 Oct 2018 14:05:27 +0200 Subject: [PATCH 10/10] Fixing a stupid typo with the dead event handler in SET_UNIT. --- Moose Development/Moose/Core/Set.lua | 2 +- Moose Development/Moose/Tasking/CommandCenter.lua | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Moose Development/Moose/Core/Set.lua b/Moose Development/Moose/Core/Set.lua index 0722dcc8a..199688c19 100644 --- a/Moose Development/Moose/Core/Set.lua +++ b/Moose Development/Moose/Core/Set.lua @@ -1908,7 +1908,7 @@ do -- SET_UNIT if _DATABASE then self:_FilterStart() self:HandleEvent( EVENTS.Birth, self._EventOnBirth ) - self:HandleEvent( EVENTS.Dead, self._EventOnDeadOrCrashOr ) + self:HandleEvent( EVENTS.Dead, self._EventOnDeadOrCrash ) self:HandleEvent( EVENTS.Crash, self._EventOnDeadOrCrash ) self:HandleEvent( EVENTS.RemoveUnit, self._EventOnDeadOrCrash ) end diff --git a/Moose Development/Moose/Tasking/CommandCenter.lua b/Moose Development/Moose/Tasking/CommandCenter.lua index 11bf5b0b2..125762730 100644 --- a/Moose Development/Moose/Tasking/CommandCenter.lua +++ b/Moose Development/Moose/Tasking/CommandCenter.lua @@ -687,9 +687,9 @@ function COMMANDCENTER:ReportMissionsPlayers( ReportGroup ) Report:Add( "Players active in all missions." ) - for MissionID, Mission in pairs( self.Missions ) do - local Mission = Mission -- Tasking.Mission#MISSION - Report:Add( " - " .. Mission:ReportPlayers() ) + for MissionID, MissionData in pairs( self.Missions ) do + local Mission = MissionData -- Tasking.Mission#MISSION + Report:Add( " - " .. Mission:ReportPlayersPerTask(ReportGroup) ) end self:MessageToGroup( Report:Text(), ReportGroup )