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