From 3a9be7a890ace8206cf9967fd2199d78cb117156 Mon Sep 17 00:00:00 2001 From: Frank Date: Wed, 17 Oct 2018 22:07:11 +0200 Subject: [PATCH 01/15] CTA 0.0.6 --- .../Moose/Functional/CarrierTrainer.lua | 1630 +++++++++++++++++ 1 file changed, 1630 insertions(+) create mode 100644 Moose Development/Moose/Functional/CarrierTrainer.lua diff --git a/Moose Development/Moose/Functional/CarrierTrainer.lua b/Moose Development/Moose/Functional/CarrierTrainer.lua new file mode 100644 index 000000000..c24e7fbd5 --- /dev/null +++ b/Moose Development/Moose/Functional/CarrierTrainer.lua @@ -0,0 +1,1630 @@ +--- **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 #CARRIERTRAINER.Checkpoint Upwind Upwind checkpoint. +-- @field #CARRIERTRAINER.Checkpoint BreakEarly Early break checkpoint. +-- @field #CARRIERTRAINER.Checkpoint BreakLate Late brak checkpoint. +-- @field #CARRIERTRAINER.Checkpoint Abeam Abeam checkpoint. +-- @field #CARRIERTRAINER.Checkpoint Ninety At the ninety checkpoint. +-- @field #CARRIERTRAINER.Checkpoint Wake Right behind the carrier. +-- @field #CARRIERTRAINER.Checkpoint Groove In the groove checkpoint. +-- @field #CARRIERTRAINER.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 = {}, +} + +--- Aircraft types. +-- @type CARRIERTRAINER.AircraftType +-- @field #string AV8B AV-8B Night Harrier. +-- @field #string HORNET F/A-18C Lot 20 Hornet. +CARRIERTRAINER.AircraftType={ + AV8B="AV8BNA", + HORNET="FA-18C_hornet", +} + +--- Carrier types. +-- @type CARRIERTRAINER.CarrierType +-- @field #string STENNIS USS John C. Stennis (CVN-74) +-- @field #string VINSON USS Carl Vinson (CVN-70) +-- @field #string TARAWA USS Tarawa (LHA-1) +-- @field #string KUZNETSOV Admiral Kuznetsov (CV 1143.5) +CARRIERTRAINER.CarrierType={ + STENNIS="Stennis", + VINSON="Vinson", + TARAWA="LHA_Tarawa", + KUZNETSOV="KUZNECOW" +} + +--- Difficulty level. +-- @type CARRIERTRAINER.Difficulty +-- @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. +CARRIERTRAINER.Difficulty={ + EASY="Easy", + NORMAL="Normal", + HARD="Hard", +} + +--- 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. + +--- Checkpoint parameters triggering the next step in the pattern. +-- @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 LimitXmin Latitudal threshold for triggering the next step if XXmax. +-- @field #number LimitZmin Latitudal threshold for triggering the next step if ZZmax. +-- @field #number Altitude Optimal altitude at this point. +-- @field #number AoA Optimal AoA at this point. +-- @field #number Distance Optimal distance at this point. +-- @field #number Speed Optimal speed at this point. +-- @field #table Checklist Table of checklist text items to display at this point. + +--- Main radio menu. +-- @field #table MenuF10 +CARRIERTRAINER.MenuF10={} + +--- Carrier trainer class version. +-- @field #string version +CARRIERTRAINER.version="0.0.6" + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- 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 + + --CARRIERTRAINER.Difficulty.EASY + + -- 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 + + self:_DetailedPlayerStatus(playerData) + 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 + + + +--- Check limits for reaching next step. +-- @param #CARRIERTRAINER self +-- @param #number X X position of player unit. +-- @param #number Z Z position of player unit. +-- @param #CARRIERTRAINER.Checkpoint check Checkpoint. +-- @return #boolean If true, checkpoint condition for next step was reached. +function CARRIERTRAINER:_CheckLimits(X, Z, check) + + local next=false + if check.LimitXmin and Xcheck.LimitXmax then + next=true + elseif check.LimitZmin and Zcheck.LimitZmax then + next=true + end + + return next +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="when entering pattern" + elseif step==2 then + name="on upwind leg" + elseif step==3 then + name="at the early break" + elseif step==4 then + name="at the late break" + elseif step==5 then + name="in the abeam position" + 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 + +--- 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 + + +--- 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 wind=unit:GetCoordinate():GetWindWithTurbulenceVec3() + local velo=unit:GetVelocityVec3() + + local text=string.format("%s, current AoA=%.1f\n", playerData.callsign, aoa) + text=text..string.format("velo x=%.1f y=%.1f z=%.1f\n", velo.x, velo.y, velo.z) + text=text..string.format("wind x=%.1f y=%.1f z=%.1f\n", wind.x, wind.y, wind.z) + text=text..string.format("pitch=%.1f | roll=%.1f | yaw=%.1f | climb=%.1f\n", pitch, roll, yaw, unit:GetClimbAnge()) + --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 + +--- Init parameters for USS Stennis carrier. +-- @param #CARRIERTRAINER self +function CARRIERTRAINER:_InitStennis() + + -- Upwind leg + 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.LimitXmin=nil + self.Upwind.LimitXmax=nil + self.Upwind.LimitZmin=nil + self.Upwind.LimitZmax=nil + self.upwind.Alitude=UTILS.FeetToMeters(800) + self.Upwind.AoA=8.1 + self.Upwind.Distance=nil + + -- Early break + self.BreakEarly.Xmin=-500 + self.BreakEarly.Xmax=nil + self.BreakEarly.Zmin=-3700 + self.BreakEarly.Zmax=1500 + self.BreakEarly.LimitXmin=nil + self.BreakEarly.LimitXmax=nil + self.BreakEarly.LimitZmin=-370 --0.2 NM + self.BreakEarly.LimitZmax=nil + self.BreakEarly.Alitude=UTILS.FeetToMeters(800) + self.BreakEarly.AoA=8.1 + self.BreakEarly.Distance=nil + + -- Late break + self.BreakLate.Xmin=-500 + self.BreakLate.Xmax=nil + self.BreakLate.Zmin=-3700 + self.BreakLate.Zmax=1500 + self.BreakLate.LimitXmin=nil + self.BreakLate.LimitXmax=nil + self.BreakLate.LimitZmin=-1470 --0.8 NM + self.BreakLate.LimitZmax=nil + self.BreakLate.Alitude=UTILS.FeetToMeters(800) + self.BreakLate.AoA=8.1 + self.BreakLate.Distance=nil + + -- Abeam position + self.Abeam.Xmin=nil + self.Abeam.Xmax=nil + self.Abeam.Zmin=-3700 + self.Abeam.Zmax=-1000 + self.Abeam.LimitXmin=-200 + self.Abeam.LimitXmax=nil + self.Abeam.LimitZmin=nil + self.Abeam.LimitZmax=nil + self.Abeam.Alitude=UTILS.FeetToMeters(600) + self.Abeam.AoA=8.1 + self.Abeam.Distance=nil + + -- At the ninety + self.Ninety.Xmin=-3700 + self.Ninety.Xmax=0 + self.Ninety.Zmin=-3700 + self.Ninety.Zmax=nil + self.Ninety.LimitXmin=nil + self.Ninety.LimitXmax=nil + self.Ninety.LimitZmin=nil + self.Ninety.LimitZmax=-1111 + self.Ninety.Altitude=UTILS.FeetToMeters(500) + self.Abeam.AoA=8.1 + self.Abeam.Distance=nil + + -- Wake position + self.Wake.Xmin=-4000 + self.Wake.Xmax=0 + self.Wake.Zmin=-2000 + self.Wake.Zmax=nil + self.Wake.LimitXmin=nil + self.Wake.LimitXmax=nil + self.Wake.LimitZmin=nil + self.Wake.LimitZmax=0 + self.Wake.Alitude=UTILS.FeetToMeters(370) + self.Wake.AoA=8.1 + self.Wake.Distance=nil + + -- In the groove + self.Groove.Xmin=-4000 + self.Groove.Xmax=100 + + -- Landing 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 + +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) + + -- Get distances between carrier and player unit (parallel and perpendicular to direction of movement of carrier) + local diffX, diffZ = self:_GetDistances(playerData.unit) + + -- Abort condition check. + if self:_CheckAbort(diffX,diffZ, self.Upwind) then + self:_AbortPattern(playerData, diffX, diffZ, self.Upwind) + return + end + + -- Check if we are in front of the boat (diffX > 0). + if self:_CheckLimits(diffX, diffZ, self.Upwind) then + + -- Get + local score, hint=self:_AltitudeCheck(playerData, self.Upwind) + + self:_SendMessageToPlayer(hint, 8, playerData) + self:_AddToSummary(playerData, hint) + + -- Next step. + playerData.step = 3 + end +end + + +--- Break. +-- @param #CARRIERTRAINER self +-- @param #CARRIERTRAINER.PlayerData playerData Player data table. +-- @param #string part Part of the break. +function CARRIERTRAINER:_Break(playerData, part) + + -- Get distances between carrier and player unit (parallel and perpendicular to direction of movement of carrier) + local diffX, diffZ = self:_GetDistances(playerData.unit) + + -- Check abort conditions. + if self:_CheckAbort(diffX, diffZ, self.Break) then + self:_AbortPattern(playerData, diffX, diffZ, self.Break) + return + end + + -- Early or late break. + local limit = self.BreakEarly + if part == "late" then + limit = self.BreakLate + end + + -- Check if too far left + --if diffZ < limit then + if self:_CheckLimits(diffX, diffZ, limit) then + + -- Check altitude. + local score, hint=self:_AltitudeCheck(playerData, self.Upwind) + + + self:_SendMessageToPlayer(hint, 8, playerData) + 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) + + -- Get distances between carrier and player unit (parallel and perpendicular to direction of movement of carrier) + local diffX, diffZ = self:_GetDistances(playerData.unit) + + -- Check abort conditions. + if self:_CheckAbort(diffX, diffZ, self.Abeam) then + self:_AbortPattern(playerData, diffX, diffZ, self.Abeam) + return + end + + -- Check nest step threshold. + if self:_CheckLimits(diffX, diffZ, self.Abeam) 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 score, hint=self:_AltitudeCheck(playerData, self.Abeam) + + local distanceHint = "" + local distanceScore + local diffEast = carrierPosition.z - playerPosition.z + + local idealDistance = UTILS.NMToMeters(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 .. ")") + + -- Proceed to next step. + 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) + + -- Get distances between carrier and player unit (parallel and perpendicular to direction of movement of carrier) + local diffX, diffZ = self:_GetDistances(playerData.unit) + + --if(diffZ < -3700 or diffX < -3700 or diffX > 0) then + if self:_CheckAbort(diffX, diffZ, self.Ninety) then + self:_AbortPattern(playerData, diffX, diffZ, self.Ninety) + return + end + + local limitEast = -1111 --0.6nm + + --if diffZ > limitEast then + if self:_CheckLimits(diffX, diffZ, self.Ninety) then + + local idealAltitude = 500 + local score, hint=self:_AltitudeCheck(playerData, self.Ninety) + + 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) + + -- Get distances between carrier and player unit (parallel and perpendicular to direction of movement of carrier) + local diffX, diffZ = self:_GetDistances(playerData.unit) + + --if (diffZ < -2000 or diffX < -4000 or diffX > 0) then + if self:_CheckAbort(diffX, diffZ, self.Wake) then + self:_AbortPattern(playerData, diffX, diffZ, self.Wake) + return + end + + --if diffZ > 0 then + if self:_CheckLimits(diffX, diffZ, self.Wake) then + + local idealAltitude = 370 + local score, hint=self:_AltitudeCheck(playerData, self.Wake) + + self:_SendMessageToPlayer( hint, 8, playerData ) + self:_PrintAltitudeFeedback(altitude, idealAltitude, playerData) + + 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 + + -- 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 + 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 score, hint=self:_AltitudeCheck(playerData, self.Groove) + + 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) + + + --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:E(_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 +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Evaluate player's altitude at checkpoint. +-- @param #CARRIERTRAINER self +-- @param #CARRIERTRAINER.PlayerData playerData Player data table. +-- @param #CARRIERTRAINER.Checkpoint checkpoint Checkpoint. +-- @return #number Score. +-- @return #string Message text. +function CARRIERTRAINER:_AltitudeCheck(playerData, checkpoint) + + -- Player altitude. + local altitude=playerData.unit:GetAltitude() + + local lowscore + local badscore + if playerData.difficulty==CARRIERTRAINER.Difficulty.EASY then + lowscore=10 + badscore=20 + elseif playerData.difficulty==CARRIERTRAINER.Difficulty.NORMAL then + lowscore=5 + badscore=10 + elseif playerData.difficulty==CARRIERTRAINER.Difficulty.HARD then + lowscore=2.5 + badscore=5 + end + + -- Altitude error +-X% + local _error=(altitude-checkpoint.Altitude)/checkpoint.Altitude*100 + + local score + local hint + local steptext=self:_StepName(playerData.step) + if _error>badscore then + score = 5 + hint = string.format("You're high %s.", steptext) + elseif _error>lowscore then + score = 7 + hint = string.format("You're slightly high %s.", steptext) + elseif _errorbadscore then + score = 0 + hint = string.format("too far from the boat (%.1f NM)", dnm) + elseif _error>lowscore then + score = 5 + hint = string.format("slightly too far from the boat (%.1f NM)", dnm) + elseif _errorbadscore then --Slow + score = 0 + elseif _error>lowscore then --Slightly slow + score = 5 + elseif _error 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. +-- @param #boolean printtotal Also print total score. +function CARRIERTRAINER:_PrintScore(score, playerData, printtotal) + + if printtotal 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 7d7c521bce6b315c1f4c19f22c83ed9381eab561 Mon Sep 17 00:00:00 2001 From: funkyfranky Date: Thu, 18 Oct 2018 16:50:08 +0200 Subject: [PATCH 02/15] CT007 --- .../Moose/Functional/CarrierTrainer.lua | 75 ++++++------------- 1 file changed, 21 insertions(+), 54 deletions(-) diff --git a/Moose Development/Moose/Functional/CarrierTrainer.lua b/Moose Development/Moose/Functional/CarrierTrainer.lua index c24e7fbd5..f6bcbd419 100644 --- a/Moose Development/Moose/Functional/CarrierTrainer.lua +++ b/Moose Development/Moose/Functional/CarrierTrainer.lua @@ -144,7 +144,7 @@ CARRIERTRAINER.MenuF10={} --- Carrier trainer class version. -- @field #string version -CARRIERTRAINER.version="0.0.6" +CARRIERTRAINER.version="0.0.7" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Constructor @@ -864,41 +864,17 @@ function CARRIERTRAINER:_Abeam(playerData) local onSpeedScore = self:_GetOnSpeedScore(aoa) - local idealAltitude = 600 local score, hint=self:_AltitudeCheck(playerData, self.Abeam) - local distanceHint = "" - local distanceScore - local diffEast = carrierPosition.z - playerPosition.z + local dscore, dhint=self:_DistanceCheck(diffZ, playerData, self.Abeam) - local idealDistance = UTILS.NMToMeters(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 + local fullHint = hint .. ", " .. dhint 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:_IncreaseScore(playerData, score + dscore + onSpeedScore) + self:_PrintScore(score + dscore + onSpeedScore, playerData, true) self:_AddToSummary(playerData, fullHint .. " (" .. aoaFeedback .. ")") @@ -1024,15 +1000,9 @@ end -- @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) + local diffX, diffZ = self:_GetDistances(playerData.unit) --diffX=diffX+100 @@ -1054,21 +1024,19 @@ function CARRIERTRAINER:_Groove(playerData) local limitDeg = 8.0 + -- TODO: what is this angle? Does not make sense! local fraction = diffZ / (-diffX) local asinValue = math.asin(fraction) local angle = math.deg(asinValue) if diffZ > -1300 and angle > limitDeg then - local idealAltitude = 300 + + -- Altitude check. local score, hint=self:_AltitudeCheck(playerData, self.Groove) - self:_SendMessageToPlayer(hint, 8, playerData) - self:_PrintAltitudeFeedback(altitude, idealAltitude, playerData) - - --local aoa = math.deg(mist.getAoA(playerData.mistUnit)) + -- AoA feed back local aoa = playerData.unit:GetAoA() - local aoaFeedback = self:_PrintAoAFeedback(aoa, 8.1, playerData) - + local aoaFeedback = self:_PrintAoAFeedback(aoa, 8.1, playerData) local onSpeedScore = self:_GetOnSpeedScore(aoa) self:_IncreaseScore(playerData, score + onSpeedScore) @@ -1087,19 +1055,17 @@ end -- @param #CARRIERTRAINER self -- @param #CARRIERTRAINER.PlayerData playerData Player data table. function CARRIERTRAINER:_Trap(playerData) - local playerPosition = playerData.unit:GetVec3() - local carrierPosition = self.carrier:GetVec3() + -- Get distances between carrier and player unit (parallel and perpendicular to direction of movement of carrier) + local diffZ, diffX = self:_GetDistances(playerData.unit) + + -- Player altitude + local alt=playerData.unit:GetAltitude() + + -- Get velocities. 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) - - --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) @@ -1110,11 +1076,12 @@ function CARRIERTRAINER:_Trap(playerData) playerData.highestCarrierXDiff = diffX end - if (playerPosition.y < playerData.lowestAltitude) then - playerData.lowestAltitude = playerPosition.y + if (alt < playerData.lowestAltitude) then + playerData.lowestAltitude = alt end if math.abs(playerVelocity-carrierVelocity) < 0.01 then + playerData.secondsStandingStill = playerData.secondsStandingStill + 1 if diffX < playerData.highestCarrierXDiff or playerData.secondsStandingStill > 5 then From 223de9d1aa5c84fc967d4ab027303cc9d723420e Mon Sep 17 00:00:00 2001 From: Frank Date: Thu, 18 Oct 2018 22:51:51 +0200 Subject: [PATCH 03/15] Range --- Moose Development/Moose/Functional/CarrierTrainer.lua | 8 -------- Moose Development/Moose/Functional/Range.lua | 10 +++++++--- 2 files changed, 7 insertions(+), 11 deletions(-) diff --git a/Moose Development/Moose/Functional/CarrierTrainer.lua b/Moose Development/Moose/Functional/CarrierTrainer.lua index f6bcbd419..4d043221e 100644 --- a/Moose Development/Moose/Functional/CarrierTrainer.lua +++ b/Moose Development/Moose/Functional/CarrierTrainer.lua @@ -395,8 +395,6 @@ function CARRIERTRAINER:_CheckPlayerStatus() if playerData then - self:I("player "..playerData.callsign) - -- Player unit. local unit = playerData.unit @@ -940,9 +938,6 @@ function CARRIERTRAINER:_Ninety(playerData) local idealAltitude = 500 local score, hint=self:_AltitudeCheck(playerData, self.Ninety) - 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) @@ -979,9 +974,6 @@ function CARRIERTRAINER:_Wake(playerData) local idealAltitude = 370 local score, hint=self:_AltitudeCheck(playerData, self.Wake) - self:_SendMessageToPlayer( hint, 8, playerData ) - self:_PrintAltitudeFeedback(altitude, idealAltitude, playerData) - local aoa = playerData.unit:GetAoA() local aoaFeedback = self:_PrintAoAFeedback(aoa, 8.1, playerData) diff --git a/Moose Development/Moose/Functional/Range.lua b/Moose Development/Moose/Functional/Range.lua index 95b800521..1825e262a 100644 --- a/Moose Development/Moose/Functional/Range.lua +++ b/Moose Development/Moose/Functional/Range.lua @@ -276,7 +276,7 @@ RANGE.id="RANGE | " --- Range script version. -- @field #string version -RANGE.version="1.2.1" +RANGE.version="1.2.2" --TODO list: --TODO: Add custom weapons, which can be specified by the user. @@ -1161,11 +1161,13 @@ function RANGE:OnEventShot(EventData) local _callsign=self:_myname(_unitName) -- Coordinate of impact point. - local impactcoord=COORDINATE:NewFromVec3(_lastBombPos) + local impactcoord=COORDINATE:NewFromVec3(_lastBombPos) -- Distance from range. We dont want to smoke targets outside of the range. local impactdist=impactcoord:Get2DDistance(self.location) + --impactcoord:MarkToAll("Bomb impact point") + -- Smoke impact point of bomb. if self.PlayerSettings[_playername].smokebombimpact and impactdist Date: Fri, 19 Oct 2018 15:09:26 +0200 Subject: [PATCH 04/15] CT008 --- .../Moose/Functional/CarrierTrainer.lua | 286 ++++++++---------- 1 file changed, 127 insertions(+), 159 deletions(-) diff --git a/Moose Development/Moose/Functional/CarrierTrainer.lua b/Moose Development/Moose/Functional/CarrierTrainer.lua index 4d043221e..14f1c40a7 100644 --- a/Moose Development/Moose/Functional/CarrierTrainer.lua +++ b/Moose Development/Moose/Functional/CarrierTrainer.lua @@ -144,7 +144,7 @@ CARRIERTRAINER.MenuF10={} --- Carrier trainer class version. -- @field #string version -CARRIERTRAINER.version="0.0.7" +CARRIERTRAINER.version="0.0.8" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Constructor @@ -236,6 +236,7 @@ function CARRIERTRAINER:onafterStart(From, Event, To) -- Handle events. self:HandleEvent(EVENTS.Birth) + --self:HandleEvent(EVENTS.Lan) -- Init status check self:__Status(5) @@ -340,14 +341,6 @@ function CARRIERTRAINER:_InitNewRound(playerData) 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. @@ -429,7 +422,7 @@ function CARRIERTRAINER:_CheckPlayerStatus() self:_Groove(playerData) elseif playerData.step == 9 and unit:IsInZone(self.giantZone) then self:_Trap(playerData) - end + end else -- Unit not alive. --playerDatas[i] = nil @@ -758,22 +751,25 @@ function CARRIERTRAINER:_NewRound(playerData) playerData.step = 1 end ---- Start landing pattern. +--- Start landing pattern, when player enters the start zone. -- @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 + local hint = string.format("Entering the pattern, %s! Aim for 800 feet and 350-400 kts in the break entry.", playerData.callsign) + self:_SendMessageToPlayer(hint, 8, playerData) + + -- TODO: Check for correct player heading! + playerData.score = 0 + playerData.step = 2 +end + --- Upwind leg. -- @param #CARRIERTRAINER self -- @param #CARRIERTRAINER.PlayerData playerData Player data table. -function CARRIERTRAINER:_Upwind(playerData) +function CARRIERTRAINER:_Upwind(playerData) -- Get distances between carrier and player unit (parallel and perpendicular to direction of movement of carrier) local diffX, diffZ = self:_GetDistances(playerData.unit) @@ -787,10 +783,10 @@ function CARRIERTRAINER:_Upwind(playerData) -- Check if we are in front of the boat (diffX > 0). if self:_CheckLimits(diffX, diffZ, self.Upwind) then - -- Get + -- Get altutide. local score, hint=self:_AltitudeCheck(playerData, self.Upwind) - self:_SendMessageToPlayer(hint, 8, playerData) + self:_AddToSummary(playerData, hint) -- Next step. @@ -827,8 +823,7 @@ function CARRIERTRAINER:_Break(playerData, part) -- Check altitude. local score, hint=self:_AltitudeCheck(playerData, self.Upwind) - - self:_SendMessageToPlayer(hint, 8, playerData) + -- Add hint to summary. self:_AddToSummary(playerData, hint) if (part == "early") then @@ -839,7 +834,7 @@ function CARRIERTRAINER:_Break(playerData, part) end end ---- Break. +--- Abeam. -- @param #CARRIERTRAINER self -- @param #CARRIERTRAINER.PlayerData playerData Player data table. function CARRIERTRAINER:_Abeam(playerData) @@ -855,17 +850,23 @@ function CARRIERTRAINER:_Abeam(playerData) -- Check nest step threshold. if self:_CheckLimits(diffX, diffZ, self.Abeam) then + + -- Checks: + -- AoA + -- Altitude + -- Distance to carrier. -- Get AoA. local aoa = playerData.unit:GetAoA() - local aoaFeedback = self:_PrintAoAFeedback(aoa, 8.1, playerData) + self:_AoACheck(aoa, self.Abeam, playerData) - local onSpeedScore = self:_GetOnSpeedScore(aoa) - + -- Check Alitude local score, hint=self:_AltitudeCheck(playerData, self.Abeam) + -- Check distance. local dscore, dhint=self:_DistanceCheck(diffZ, playerData, self.Abeam) + --[[ local fullHint = hint .. ", " .. dhint self:_SendMessageToPlayer( fullHint, 8, playerData ) @@ -873,8 +874,10 @@ function CARRIERTRAINER:_Abeam(playerData) self:_IncreaseScore(playerData, score + dscore + onSpeedScore) self:_PrintScore(score + dscore + onSpeedScore, playerData, true) + ]] - self:_AddToSummary(playerData, fullHint .. " (" .. aoaFeedback .. ")") + + --self:_AddToSummary(playerData, fullHint .. " (" .. aoaFeedback .. ")") -- Proceed to next step. playerData.step = 6 @@ -885,31 +888,34 @@ 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 -- Get distances between carrier and player unit (parallel and perpendicular to direction of movement of carrier) local diffX, diffZ = self:_GetDistances(playerData.unit) + local limit = -1500 + -- 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 cx = self.carrier:GetOrientationX() + local px = playerData.unit:GetOrientationX() + local dp = UTILS.VecDot(cx, px) -- dot > 0 means that we are 90 degrees and about to align + + if dp<-0.98 then local hint = "Too long downwind. Turn final earlier next time." - self:_SendMessageToPlayer( hint, 8, playerData ) + self:_SendMessageToPlayer(hint, 8, playerData) + + self:_AddToSummary(playerData, hint) + local score = -40 self:_IncreaseScore(playerData, score) self:_PrintScore(score, playerData, true) - self:_AddToSummary(playerData, hint) + + -- Long downwind done! playerData.longDownwindDone = true end @@ -975,13 +981,10 @@ function CARRIERTRAINER:_Wake(playerData) local score, hint=self:_AltitudeCheck(playerData, self.Wake) local aoa = playerData.unit:GetAoA() + local score, hint=self:_AoACheck(aoa, self.Wake, playerData) - 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 .. ")") + -- TODO make summay + --self:_AddToSummary(playerData, hint .. " (" .. aoaFeedback .. ")") playerData.step = 8 end @@ -1007,11 +1010,17 @@ function CARRIERTRAINER:_Groove(playerData) --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 ) + + -- zero score self:_PrintScore(0, playerData, true) self:_AddToSummary(playerData, hint) + + -- Next step. playerData.step = 9 + else local limitDeg = 8.0 @@ -1028,16 +1037,13 @@ function CARRIERTRAINER:_Groove(playerData) -- AoA feed back 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 .. ")" + local score, hint=self:_AoACheck(aoa, self.Groove,playerData) - self:_AddToSummary(playerData, fullHint) + -- TODO + --local fullHint = hint .. " (" .. aoaFeedback .. ")" + --self:_AddToSummary(playerData, fullHint) + -- Next step. playerData.step = 9 end end @@ -1083,6 +1089,7 @@ function CARRIERTRAINER:_Trap(playerData) local wire = 1 local score = -10 + -- Which wire if(diffX < -14) then wire = 1 score = -15 @@ -1115,14 +1122,14 @@ function CARRIERTRAINER:_Trap(playerData) elseif (diffX > 150) then - local wire = 0 - local hint = "" + local wire = 0 + local hint = "" local score = 0 if (playerData.lowestAltitude < 23) then - hint = "You boltered." + hint = "You boltered." else - hint = "You were waved off." - wire = -1 + hint = "You were waved off." + wire = -1 score = -10 end @@ -1267,14 +1274,10 @@ end --- Evaluate player's altitude at checkpoint. -- @param #CARRIERTRAINER self -- @param #CARRIERTRAINER.PlayerData playerData Player data table. --- @param #CARRIERTRAINER.Checkpoint checkpoint Checkpoint. --- @return #number Score. --- @return #string Message text. -function CARRIERTRAINER:_AltitudeCheck(playerData, checkpoint) +-- @return #number Low score. +-- @return #number Bad score. +function CARRIERTRAINER:_GetGoodBadScore(playerData) - -- Player altitude. - local altitude=playerData.unit:GetAltitude() - local lowscore local badscore if playerData.difficulty==CARRIERTRAINER.Difficulty.EASY then @@ -1288,6 +1291,23 @@ function CARRIERTRAINER:_AltitudeCheck(playerData, checkpoint) badscore=5 end + return lowscore, badscore +end + +--- Evaluate player's altitude at checkpoint. +-- @param #CARRIERTRAINER self +-- @param #CARRIERTRAINER.PlayerData playerData Player data table. +-- @param #CARRIERTRAINER.Checkpoint checkpoint Checkpoint. +-- @return #number Score. +-- @return #string Message text. +function CARRIERTRAINER:_AltitudeCheck(playerData, checkpoint) + + -- Player altitude. + local altitude=playerData.unit:GetAltitude() + + -- Get relative score. + local lowscore, badscore = self:_GetGoodBadScore(playerData) + -- Altitude error +-X% local _error=(altitude-checkpoint.Altitude)/checkpoint.Altitude*100 @@ -1295,31 +1315,30 @@ function CARRIERTRAINER:_AltitudeCheck(playerData, checkpoint) local hint local steptext=self:_StepName(playerData.step) if _error>badscore then - score = 5 - hint = string.format("You're high %s.", steptext) + score = -10 + hint = string.format("You're high %s. ", steptext) elseif _error>lowscore then - score = 7 - hint = string.format("You're slightly high %s.", steptext) + score = -5 + hint = string.format("You're slightly high %s. ", steptext) elseif _errorbadscore then - score = 0 - hint = string.format("too far from the boat (%.1f NM)", dnm) + score = -10 + hint = string.format("too far from the boat (%.1f NM)", dnm) elseif _error>lowscore then - score = 5 - hint = string.format("slightly too far from the boat (%.1f NM)", dnm) + score = -5 + hint = string.format("slightly too far from the boat (%.1f NM)", dnm) elseif _errorbadscore then --Slow - score = 0 + score = -10 + hint = "You're slow." elseif _error>lowscore then --Slightly slow - score = 5 + score = -5 + hint = "You're slightly slow." elseif _error 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. From 645ca570a8096559f8b89a1c230eef56a74195f4 Mon Sep 17 00:00:00 2001 From: Frank Date: Sat, 20 Oct 2018 12:20:35 +0200 Subject: [PATCH 05/15] CT 0.0.9 many fixes --- .../Moose/Functional/CarrierTrainer.lua | 264 ++++++++++-------- 1 file changed, 140 insertions(+), 124 deletions(-) diff --git a/Moose Development/Moose/Functional/CarrierTrainer.lua b/Moose Development/Moose/Functional/CarrierTrainer.lua index 14f1c40a7..d9607ebcb 100644 --- a/Moose Development/Moose/Functional/CarrierTrainer.lua +++ b/Moose Development/Moose/Functional/CarrierTrainer.lua @@ -54,23 +54,24 @@ -- @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 = {}, + lid = nil, + Debug = true, + carrier = nil, + carriertype = nil, + alias = nil, + registerZone = nil, + startZone = nil, + giantZone = nil, + players = {}, + menuadded = {}, + Upwind = {}, + Abeam = {}, + BreakEarly = {}, + BreakLate = {}, + Ninety = {}, + Wake = {}, + Groove = {}, + Trap = {}, } --- Aircraft types. @@ -101,9 +102,9 @@ CARRIERTRAINER.CarrierType={ -- @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. CARRIERTRAINER.Difficulty={ - EASY="Easy", - NORMAL="Normal", - HARD="Hard", + EASY="Rookey", + NORMAL="Naval Aviator", + HARD="TOPGUN Graduate", } --- Player data table holding all important parameters for each player. @@ -119,7 +120,7 @@ CARRIERTRAINER.Difficulty={ -- @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 Wrapper.Client#CLIENT client object of player. -- @field #string difficulty Difficulty level. --- Checkpoint parameters triggering the next step in the pattern. @@ -144,7 +145,7 @@ CARRIERTRAINER.MenuF10={} --- Carrier trainer class version. -- @field #string version -CARRIERTRAINER.version="0.0.8" +CARRIERTRAINER.version="0.0.9" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Constructor @@ -164,16 +165,15 @@ function CARRIERTRAINER:New(carriername, alias) 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 }) + self.registerZone = ZONE_UNIT:New("registerZone", self.carrier, 2500, { dx = -5000, dy = 100, relative_to_unit = true }) + 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 - - --CARRIERTRAINER.Difficulty.EASY -- Set some string id for output to DCS.log file. self.lid=string.format("CARRIERTRAINER %s | ", carriername) @@ -323,6 +323,8 @@ function CARRIERTRAINER:_InitNewPlayer(unitname) playerData=self:_InitNewRound(playerData) + playerData.difficulty=CARRIERTRAINER.Difficulty.NORMAL + return playerData end @@ -392,14 +394,17 @@ function CARRIERTRAINER:_CheckPlayerStatus() local unit = playerData.unit if unit:IsAlive() then + + self:_SendMessageToPlayer("current step "..self:_StepName(playerData.step),1,playerData) - self:_DetailedPlayerStatus(playerData) + --self:_DetailedPlayerStatus(playerData) 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) + self:_InitStennis() elseif playerData.step == 1 and unit:IsInZone(self.startZone) then self:_Start(playerData) elseif playerData.step == 2 and unit:IsInZone(self.giantZone) then @@ -442,16 +447,22 @@ end -- @return #boolean If true, checkpoint condition for next step was reached. function CARRIERTRAINER:_CheckLimits(X, Z, check) - local next=false + local next=true if check.LimitXmin and Xcheck.LimitXmax then - next=true + next=false elseif check.LimitZmin and Zcheck.LimitZmax then - next=true + next=false end + + self:E({X=X, Z=Z, check=check}) + + local text=string.format("next=%s : X=%d Xmin=%s Xmax=%s ||| Z=%d Zmin=%s Zmax=%s", + tostring(next), X, tostring(check.LimitXmin), tostring(check.LimitXmax), Z, tostring(check.LimitZmin), tostring(check.LimitZmax)) + MESSAGE:New(text,1):ToAllIf(self.Debug) return next end @@ -626,11 +637,14 @@ function CARRIERTRAINER:_DetailedPlayerStatus(playerData) local wind=unit:GetCoordinate():GetWindWithTurbulenceVec3() local velo=unit:GetVelocityVec3() + + local relhead=self:_GetRelativeHeading(playerData.unit) local text=string.format("%s, current AoA=%.1f\n", playerData.callsign, aoa) text=text..string.format("velo x=%.1f y=%.1f z=%.1f\n", velo.x, velo.y, velo.z) text=text..string.format("wind x=%.1f y=%.1f z=%.1f\n", wind.x, wind.y, wind.z) text=text..string.format("pitch=%.1f | roll=%.1f | yaw=%.1f | climb=%.1f\n", pitch, roll, yaw, unit:GetClimbAnge()) + text=text..string.format("relheading %.1f degrees", relhead) --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)) @@ -646,13 +660,13 @@ function CARRIERTRAINER:_InitStennis() -- Upwind leg 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.LimitXmin=nil + self.Upwind.Zmin=-1200 + self.Upwind.Zmax=1200 + self.Upwind.LimitXmin=0 self.Upwind.LimitXmax=nil - self.Upwind.LimitZmin=nil + self.Upwind.LimitZmin=0 self.Upwind.LimitZmax=nil - self.upwind.Alitude=UTILS.FeetToMeters(800) + self.Upwind.Altitude=UTILS.FeetToMeters(800) self.Upwind.AoA=8.1 self.Upwind.Distance=nil @@ -665,7 +679,7 @@ function CARRIERTRAINER:_InitStennis() self.BreakEarly.LimitXmax=nil self.BreakEarly.LimitZmin=-370 --0.2 NM self.BreakEarly.LimitZmax=nil - self.BreakEarly.Alitude=UTILS.FeetToMeters(800) + self.BreakEarly.Altitude=UTILS.FeetToMeters(800) self.BreakEarly.AoA=8.1 self.BreakEarly.Distance=nil @@ -678,7 +692,7 @@ function CARRIERTRAINER:_InitStennis() self.BreakLate.LimitXmax=nil self.BreakLate.LimitZmin=-1470 --0.8 NM self.BreakLate.LimitZmax=nil - self.BreakLate.Alitude=UTILS.FeetToMeters(800) + self.BreakLate.Altitude=UTILS.FeetToMeters(800) self.BreakLate.AoA=8.1 self.BreakLate.Distance=nil @@ -691,7 +705,7 @@ function CARRIERTRAINER:_InitStennis() self.Abeam.LimitXmax=nil self.Abeam.LimitZmin=nil self.Abeam.LimitZmax=nil - self.Abeam.Alitude=UTILS.FeetToMeters(600) + self.Abeam.Altitude=UTILS.FeetToMeters(600) self.Abeam.AoA=8.1 self.Abeam.Distance=nil @@ -717,7 +731,7 @@ function CARRIERTRAINER:_InitStennis() self.Wake.LimitXmax=nil self.Wake.LimitZmin=nil self.Wake.LimitZmax=0 - self.Wake.Alitude=UTILS.FeetToMeters(370) + self.Wake.Altitude=UTILS.FeetToMeters(370) self.Wake.AoA=8.1 self.Wake.Distance=nil @@ -730,8 +744,8 @@ function CARRIERTRAINER:_InitStennis() self.Trap.Xmax=nil self.Trap.Zmin=-2000 self.Trap.Zmax=2000 - self.Trap.Limit=nil - self.Trap.Alitude=nil + --self.Trap.Limit=nil + self.Trap.Altitude=nil end @@ -783,9 +797,13 @@ function CARRIERTRAINER:_Upwind(playerData) -- Check if we are in front of the boat (diffX > 0). if self:_CheckLimits(diffX, diffZ, self.Upwind) then + local altitude=playerData.unit:GetAltitude() + -- Get altutide. - local score, hint=self:_AltitudeCheck(playerData, self.Upwind) - + local hint=self:_AltitudeCheck(playerData, self.Upwind, altitude) + + + self:_SendMessageToPlayer(hint, 8, playerData) self:_AddToSummary(playerData, hint) @@ -804,24 +822,29 @@ function CARRIERTRAINER:_Break(playerData, part) -- Get distances between carrier and player unit (parallel and perpendicular to direction of movement of carrier) local diffX, diffZ = self:_GetDistances(playerData.unit) - -- Check abort conditions. - if self:_CheckAbort(diffX, diffZ, self.Break) then - self:_AbortPattern(playerData, diffX, diffZ, self.Break) - return - end - -- Early or late break. - local limit = self.BreakEarly + local breakpoint = self.BreakEarly if part == "late" then - limit = self.BreakLate + breakpoint = self.BreakLate + end + + + -- Check abort conditions. + if self:_CheckAbort(diffX, diffZ, breakpoint) then + self:_AbortPattern(playerData, diffX, diffZ, breakpoint) + return end -- Check if too far left --if diffZ < limit then - if self:_CheckLimits(diffX, diffZ, limit) then + if self:_CheckLimits(diffX, diffZ, breakpoint) then + + local altitude=playerData.unit:GetAltitude() -- Check altitude. - local score, hint=self:_AltitudeCheck(playerData, self.Upwind) + local hint=self:_AltitudeCheck(playerData, breakpoint, altitude) + + self:_SendMessageToPlayer(hint, 8, playerData) -- Add hint to summary. self:_AddToSummary(playerData, hint) @@ -858,26 +881,21 @@ function CARRIERTRAINER:_Abeam(playerData) -- Get AoA. local aoa = playerData.unit:GetAoA() - self:_AoACheck(aoa, self.Abeam, playerData) + local alt = playerData.unit:GetAltitude() - -- Check Alitude - local score, hint=self:_AltitudeCheck(playerData, self.Abeam) + local hintAoA=self:_AoACheck(playerData, self.Abeam, aoa) + + -- Check Altitude + local hintAlt=self:_AltitudeCheck(playerData, self.Abeam, alt) -- Check distance. - local dscore, dhint=self:_DistanceCheck(diffZ, playerData, self.Abeam) + local hintDist=self:_DistanceCheck(playerData, self.Abeam, diffZ) - --[[ - local fullHint = hint .. ", " .. dhint + local hintFull=string.format("%s.\n%s.\n%s.", hintAoA, hintAlt, hintDist) - self:_SendMessageToPlayer( fullHint, 8, playerData ) - self:_SendMessageToPlayer( "(Target: 600 ft and 1.2 nm).", 8, playerData ) - - self:_IncreaseScore(playerData, score + dscore + onSpeedScore) - self:_PrintScore(score + dscore + onSpeedScore, playerData, true) - ]] + self:_SendMessageToPlayer(hintFull, 8, playerData ) - - --self:_AddToSummary(playerData, fullHint .. " (" .. aoaFeedback .. ")") + self:_AddToSummary(playerData, hintFull) -- Proceed to next step. playerData.step = 6 @@ -897,23 +915,16 @@ function CARRIERTRAINER:_CheckForLongDownwind(playerData) -- 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() + local relhead=self:_GetRelativeHeading(playerData.unit) - local cx = self.carrier:GetOrientationX() - local px = playerData.unit:GetOrientationX() - local dp = UTILS.VecDot(cx, px) -- dot > 0 means that we are 90 degrees and about to align - - if dp<-0.98 then + if relhead<45 then local hint = "Too long downwind. Turn final earlier next time." self:_SendMessageToPlayer(hint, 8, playerData) self:_AddToSummary(playerData, hint) - local score = -40 - self:_IncreaseScore(playerData, score) - self:_PrintScore(score, playerData, true) + playerData.score=playerData.score-40 -- Long downwind done! playerData.longDownwindDone = true @@ -941,21 +952,19 @@ function CARRIERTRAINER:_Ninety(playerData) --if diffZ > limitEast then if self:_CheckLimits(diffX, diffZ, self.Ninety) then - local idealAltitude = 500 - local score, hint=self:_AltitudeCheck(playerData, self.Ninety) - - --local aoa = math.deg(mist.getAoA(playerData.mistUnit)) - local aoa = playerData.unit:GetAoA() - local aoaFeedback = self:_PrintAoAFeedback(aoa, 8.1, playerData) + local alt=playerData.unit:GetAltitude() + local aoa=playerData.unit:GetAoA() - local onSpeedScore = self:_GetOnSpeedScore(aoa) - - self:_IncreaseScore(playerData, score + onSpeedScore) - self:_PrintScore(score + onSpeedScore, playerData, true) + local hintAlt=self:_AltitudeCheck(playerData, self.Ninety, alt) + local hintAoA=self:_AoACheck(playerData, self.Ninety, aoa) - self:_AddToSummary(playerData, hint .. " (" .. aoaFeedback .. ")") + + local hintFull=string.format("%s.\n%s.", hintAoA, hintAlt) + self:_AddToSummary(playerData, hintFull) playerData.longDownwindDone = true + + -- Next step. playerData.step = 7 end end @@ -977,15 +986,17 @@ function CARRIERTRAINER:_Wake(playerData) --if diffZ > 0 then if self:_CheckLimits(diffX, diffZ, self.Wake) then - local idealAltitude = 370 - local score, hint=self:_AltitudeCheck(playerData, self.Wake) + local alt=playerData.unit:GetAltitude() + local aoa=playerData.unit:GetAoA() + + local hintAlt=self:_AltitudeCheck(playerData, self.Wake, alt) + local hintAoA=self:_AoACheck(playerData, self.Wake, aoa) - local aoa = playerData.unit:GetAoA() - local score, hint=self:_AoACheck(aoa, self.Wake, playerData) - - -- TODO make summay - --self:_AddToSummary(playerData, hint .. " (" .. aoaFeedback .. ")") - + + local hintFull=string.format("%s.\n%s.", hintAoA, hintAlt) + self:_AddToSummary(playerData, hintFull) + + -- Next step. playerData.step = 8 end end @@ -1298,9 +1309,9 @@ end -- @param #CARRIERTRAINER self -- @param #CARRIERTRAINER.PlayerData playerData Player data table. -- @param #CARRIERTRAINER.Checkpoint checkpoint Checkpoint. --- @return #number Score. --- @return #string Message text. -function CARRIERTRAINER:_AltitudeCheck(playerData, checkpoint) +-- @param #number altitude Player's current altitude in meters. +-- @return #string Feedback text. +function CARRIERTRAINER:_AltitudeCheck(playerData, checkpoint, altitude) -- Player altitude. local altitude=playerData.unit:GetAltitude() @@ -1331,25 +1342,21 @@ function CARRIERTRAINER:_AltitudeCheck(playerData, checkpoint) hint = string.format("Good altitude %s. ", steptext) end - hint=hint..string.format(" %d\% deviation from %d ft target alt.", _error, checkpoint.Altitude) + hint=hint..string.format(" %d deviation from %d ft target alt.", _error, UTILS.MetersToFeet(checkpoint.Altitude)) -- Set score. playerData.score=playerData.score+score - - -- Display feedback. - self:_SendMessageToPlayer(hint, 8,playerData) - - return score, hint + + return hint end --- Evaluate player's altitude at checkpoint. -- @param #CARRIERTRAINER self --- @param #number distance Distance player to boat. -- @param #CARRIERTRAINER.PlayerData playerData Player data table. -- @param #CARRIERTRAINER.Checkpoint checkpoint Checkpoint. --- @return #number Score. --- @return #string Message text. -function CARRIERTRAINER:_DistanceCheck(distance, playerData, checkpoint) +-- @param #number distance Player's current distance to the boat in meters. +-- @return #string Feedback message text. +function CARRIERTRAINER:_DistanceCheck(playerData, checkpoint, distance) -- Get relative score. local lowscore, badscore = self:_GetGoodBadScore(playerData) @@ -1363,36 +1370,42 @@ function CARRIERTRAINER:_DistanceCheck(distance, playerData, checkpoint) local steptext=self:_StepName(playerData.step) if _error>badscore then score = -10 - hint = string.format("too far from the boat (%.1f NM)", dnm) + hint = string.format("You're too far from the boat!") elseif _error>lowscore then score = -5 - hint = string.format("slightly too far from the boat (%.1f NM)", dnm) + hint = string.format("You're slightly too far from the boat.") elseif _error Date: Mon, 22 Oct 2018 00:08:38 +0200 Subject: [PATCH 06/15] ARTY v1.0.7 Fixed bug when targets get removed because ARTY group is immobile and not declared as cargo. CT fixes --- .../Moose/Functional/Artillery.lua | 180 ++++++++++-------- .../Moose/Functional/CarrierTrainer.lua | 126 +++++++----- 2 files changed, 173 insertions(+), 133 deletions(-) diff --git a/Moose Development/Moose/Functional/Artillery.lua b/Moose Development/Moose/Functional/Artillery.lua index 8509928fb..3c370915f 100644 --- a/Moose Development/Moose/Functional/Artillery.lua +++ b/Moose Development/Moose/Functional/Artillery.lua @@ -216,7 +216,7 @@ -- One way to determin which types of ammo the unit carries, one can use the debug mode of the arty class via @{#ARTY.SetDebugON}(). -- In debug mode, the all ammo types of the group are printed to the monitor as message and can be found in the DCS.log file. -- --- ## Empoying Selected Weapons +-- ## Employing Selected Weapons -- -- If an ARTY group carries multiple weapons, which can be used for artillery task, a certain weapon type can be selected to attack the target. -- This is done via the *weapontype* parameter of the @{#ARTY.AssignTargetCoord}(..., *weapontype*, ...) function. @@ -674,11 +674,13 @@ ARTY.id="ARTY | " --- Arty script version. -- @field #string version -ARTY.version="1.0.6" +ARTY.version="1.0.7" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list: +-- TODO: Add hit event and make the arty group relocate. +-- TODO: Handle rearming for ships. How? -- DONE: Delete targets from queue user function. -- DONE: Delete entire target queue user function. -- DONE: Add weapon types. Done but needs improvements. @@ -697,11 +699,9 @@ ARTY.version="1.0.6" -- DONE: Add command move to make arty group move. -- DONE: remove schedulers for status event. -- DONE: Improve handling of special weapons. When winchester if using selected weapons? --- TODO: Handle rearming for ships. How? -- DONE: Make coordinate after rearming general, i.e. also work after the group has moved to anonther location. -- DONE: Add set commands via markers. E.g. set rearming place. -- DONE: Test stationary types like mortas ==> rearming etc. --- TODO: Add hit event and make the arty group relocate. -- DONE: Add illumination and smoke. --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -4253,101 +4253,116 @@ end -- @param #ARTY self function ARTY:_CheckTargetsInRange() + local targets2delete={} + for i=1,#self.targets do local _target=self.targets[i] self:T3(ARTY.id..string.format("Before: Target %s - in range = %s", _target.name, tostring(_target.inrange))) -- Check if target is in range. - local _inrange,_toofar,_tooclose=self:_TargetInRange(_target) + local _inrange,_toofar,_tooclose,_remove=self:_TargetInRange(_target) self:T3(ARTY.id..string.format("Inbetw: Target %s - in range = %s, toofar = %s, tooclose = %s", _target.name, tostring(_target.inrange), tostring(_toofar), tostring(_tooclose))) - -- Init default for assigning moves into range. - local _movetowards=false - local _moveaway=false + if _remove then - if _target.inrange==nil then - - -- First time the check is performed. We call the function again and send a message. - _target.inrange,_toofar,_tooclose=self:_TargetInRange(_target, self.report or self.Debug) + -- The ARTY group is immobile and not cargo but the target is not in range! + table.insert(targets2delete, _target.name) - -- Send group towards/away from target. - if _toofar then - _movetowards=true - elseif _tooclose then - _moveaway=true - end + else - elseif _target.inrange==true then - - -- Target was in range at previous check... - - if _toofar then --...but is now too far away. - _movetowards=true - elseif _tooclose then --...but is now too close. - _moveaway=true - end - - elseif _target.inrange==false then - - -- Target was out of range at previous check. + -- Init default for assigning moves into range. + local _movetowards=false + local _moveaway=false - if _inrange then - -- Inform coalition that target is now in range. - local text=string.format("%s, target %s is now in range.", self.alias, _target.name) - self:T(ARTY.id..text) - MESSAGE:New(text,10):ToCoalitionIf(self.Controllable:GetCoalition(), self.report or self.Debug) - end - - end - - -- Assign a relocation command so that the unit will be in range of the requested target. - if self.autorelocate and (_movetowards or _moveaway) then - - -- Get current position. - local _from=self.Controllable:GetCoordinate() - local _dist=_from:Get2DDistance(_target.coord) + if _target.inrange==nil then - if _dist<=self.autorelocatemaxdist then - - local _tocoord --Core.Point#COORDINATE - local _name="" - local _safetymargin=500 - - if _movetowards then + -- First time the check is performed. We call the function again and send a message. + _target.inrange,_toofar,_tooclose=self:_TargetInRange(_target, self.report or self.Debug) - -- Target was in range on previous check but now we are too far away. - local _waytogo=_dist-self.maxrange+_safetymargin - local _heading=self:_GetHeading(_from,_target.coord) - _tocoord=_from:Translate(_waytogo, _heading) - _name=string.format("%s, relocation to within max firing range of target %s", self.alias, _target.name) - - elseif _moveaway then - - -- Target was in range on previous check but now we are too far away. - local _waytogo=_dist-self.minrange+_safetymargin - local _heading=self:_GetHeading(_target.coord,_from) - _tocoord=_from:Translate(_waytogo, _heading) - _name=string.format("%s, relocation to within min firing range of target %s", self.alias, _target.name) - + -- Send group towards/away from target. + if _toofar then + _movetowards=true + elseif _tooclose then + _moveaway=true end - - -- Send info message. - MESSAGE:New(_name.." assigned.", 10):ToCoalitionIf(self.Controllable:GetCoalition(), self.report or self.Debug) - - -- Assign relocation move. - self:AssignMoveCoord(_tocoord, nil, nil, self.autorelocateonroad, false, _name, true) + + elseif _target.inrange==true then + + -- Target was in range at previous check... + + if _toofar then --...but is now too far away. + _movetowards=true + elseif _tooclose then --...but is now too close. + _moveaway=true + end + + elseif _target.inrange==false then + + -- Target was out of range at previous check. + if _inrange then + -- Inform coalition that target is now in range. + local text=string.format("%s, target %s is now in range.", self.alias, _target.name) + self:T(ARTY.id..text) + MESSAGE:New(text,10):ToCoalitionIf(self.Controllable:GetCoalition(), self.report or self.Debug) + end + end + + -- Assign a relocation command so that the unit will be in range of the requested target. + if self.autorelocate and (_movetowards or _moveaway) then + + -- Get current position. + local _from=self.Controllable:GetCoordinate() + local _dist=_from:Get2DDistance(_target.coord) + + if _dist<=self.autorelocatemaxdist then + + local _tocoord --Core.Point#COORDINATE + local _name="" + local _safetymargin=500 + + if _movetowards then + + -- Target was in range on previous check but now we are too far away. + local _waytogo=_dist-self.maxrange+_safetymargin + local _heading=self:_GetHeading(_from,_target.coord) + _tocoord=_from:Translate(_waytogo, _heading) + _name=string.format("%s, relocation to within max firing range of target %s", self.alias, _target.name) + elseif _moveaway then + + -- Target was in range on previous check but now we are too far away. + local _waytogo=_dist-self.minrange+_safetymargin + local _heading=self:_GetHeading(_target.coord,_from) + _tocoord=_from:Translate(_waytogo, _heading) + _name=string.format("%s, relocation to within min firing range of target %s", self.alias, _target.name) + + end + + -- Send info message. + MESSAGE:New(_name.." assigned.", 10):ToCoalitionIf(self.Controllable:GetCoalition(), self.report or self.Debug) + + -- Assign relocation move. + self:AssignMoveCoord(_tocoord, nil, nil, self.autorelocateonroad, false, _name, true) + + end + + end + + -- Update value. + _target.inrange=_inrange + + self:T3(ARTY.id..string.format("After: Target %s - in range = %s", _target.name, tostring(_target.inrange))) end - - -- Update value. - _target.inrange=_inrange - - self:T3(ARTY.id..string.format("After: Target %s - in range = %s", _target.name, tostring(_target.inrange))) - end + + -- Remove targets not in range. + for _,targetname in pairs(targets2delete) do + self:RemoveTarget(targetname) + end + end --- Check all normal (untimed) targets and return the target with the highest priority which has been engaged the fewest times. @@ -4728,6 +4743,7 @@ end -- @return #boolean True if target is in range, false otherwise. -- @return #boolean True if ARTY group is too far away from the target, i.e. distance > max firing range. -- @return #boolean True if ARTY group is too close to the target, i.e. distance < min finring range. +-- @return #boolean True if target should be removed since ARTY group is immobile and not cargo. function ARTY:_TargetInRange(target, message) self:F3(target) @@ -4763,11 +4779,13 @@ function ARTY:_TargetInRange(target, message) end -- Remove target if ARTY group cannot move, e.g. Mortas. No chance to be ever in range - unless they are cargo. + local _remove=false if not (self.ismobile or self.iscargo) and _inrange==false then - self:RemoveTarget(target.name) + --self:RemoveTarget(target.name) + _remove=true end - return _inrange,_toofar,_tooclose + return _inrange,_toofar,_tooclose,_remove end --- Get the weapon type name, which should be used to attack the target. diff --git a/Moose Development/Moose/Functional/CarrierTrainer.lua b/Moose Development/Moose/Functional/CarrierTrainer.lua index d9607ebcb..f44b7d2c7 100644 --- a/Moose Development/Moose/Functional/CarrierTrainer.lua +++ b/Moose Development/Moose/Functional/CarrierTrainer.lua @@ -125,6 +125,7 @@ CARRIERTRAINER.Difficulty={ --- Checkpoint parameters triggering the next step in the pattern. -- @type CARRIERTRAINER.Checkpoint +-- @field #string name Name of 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. @@ -317,7 +318,7 @@ function CARRIERTRAINER:_InitNewPlayer(unitname) playerData.unit = UNIT:FindByName(unitname) playerData.client = CLIENT:FindByName(playerData.unit.UnitName, nil, true) playerData.callsign = playerData.unit:GetCallsign() - playerData.totalScore = 0 + playerData.totalscore = 0 playerData.passes = 0 playerData.collectedResultString = "" @@ -364,11 +365,6 @@ end -- @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() @@ -395,7 +391,7 @@ function CARRIERTRAINER:_CheckPlayerStatus() if unit:IsAlive() then - self:_SendMessageToPlayer("current step "..self:_StepName(playerData.step),1,playerData) + --self:_SendMessageToPlayer("current step "..self:_StepName(playerData.step),1,playerData) --self:_DetailedPlayerStatus(playerData) if unit:IsInZone(self.giantZone) then @@ -437,36 +433,6 @@ function CARRIERTRAINER:_CheckPlayerStatus() end - - ---- Check limits for reaching next step. --- @param #CARRIERTRAINER self --- @param #number X X position of player unit. --- @param #number Z Z position of player unit. --- @param #CARRIERTRAINER.Checkpoint check Checkpoint. --- @return #boolean If true, checkpoint condition for next step was reached. -function CARRIERTRAINER:_CheckLimits(X, Z, check) - - local next=true - if check.LimitXmin and Xcheck.LimitXmax then - next=false - elseif check.LimitZmin and Zcheck.LimitZmax then - next=false - end - - self:E({X=X, Z=Z, check=check}) - - local text=string.format("next=%s : X=%d Xmin=%s Xmax=%s ||| Z=%d Zmin=%s Zmax=%s", - tostring(next), X, tostring(check.LimitXmin), tostring(check.LimitXmax), Z, tostring(check.LimitZmin), tostring(check.LimitZmax)) - MESSAGE:New(text,1):ToAllIf(self.Debug) - - return next -end - --- Get name of the current pattern step. -- @param #CARRIERTRAINER self -- @param #number step Step @@ -600,7 +566,9 @@ function CARRIERTRAINER:_AbortPattern(playerData, 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) + local text=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)) + self:E(self.lid..text) + MESSAGE:New(text, 60):ToAllIf(self.Debug) self:_AddToSummary(playerData, "Approach aborted.") @@ -658,9 +626,10 @@ end function CARRIERTRAINER:_InitStennis() -- Upwind leg + self.Upwind.name="Upwind" self.Upwind.Xmin=-4000 -- TODO Should be withing 4 km behind carrier. Why? self.Upwind.Xmax=nil - self.Upwind.Zmin=-1200 + self.Upwind.Zmin=0 self.Upwind.Zmax=1200 self.Upwind.LimitXmin=0 self.Upwind.LimitXmax=nil @@ -671,11 +640,12 @@ function CARRIERTRAINER:_InitStennis() self.Upwind.Distance=nil -- Early break + self.BreakEarly.name="Early Break" self.BreakEarly.Xmin=-500 self.BreakEarly.Xmax=nil self.BreakEarly.Zmin=-3700 self.BreakEarly.Zmax=1500 - self.BreakEarly.LimitXmin=nil + self.BreakEarly.LimitXmin=0 self.BreakEarly.LimitXmax=nil self.BreakEarly.LimitZmin=-370 --0.2 NM self.BreakEarly.LimitZmax=nil @@ -684,32 +654,35 @@ function CARRIERTRAINER:_InitStennis() self.BreakEarly.Distance=nil -- Late break + self.BreakLate.name="Late Break" self.BreakLate.Xmin=-500 self.BreakLate.Xmax=nil self.BreakLate.Zmin=-3700 self.BreakLate.Zmax=1500 - self.BreakLate.LimitXmin=nil - self.BreakLate.LimitXmax=nil + self.BreakLate.LimitXmin=0 + self.BreakLate.LimitXmax=10000 self.BreakLate.LimitZmin=-1470 --0.8 NM - self.BreakLate.LimitZmax=nil + self.BreakLate.LimitZmax=10000 self.BreakLate.Altitude=UTILS.FeetToMeters(800) self.BreakLate.AoA=8.1 self.BreakLate.Distance=nil -- Abeam position + self.Abeam.name="Abeam Position" self.Abeam.Xmin=nil self.Abeam.Xmax=nil self.Abeam.Zmin=-3700 self.Abeam.Zmax=-1000 self.Abeam.LimitXmin=-200 - self.Abeam.LimitXmax=nil - self.Abeam.LimitZmin=nil - self.Abeam.LimitZmax=nil + self.Abeam.LimitXmax=10000 + self.Abeam.LimitZmin=0 + self.Abeam.LimitZmax=10000 self.Abeam.Altitude=UTILS.FeetToMeters(600) self.Abeam.AoA=8.1 self.Abeam.Distance=nil -- At the ninety + self.Ninety.name="Ninety" self.Ninety.Xmin=-3700 self.Ninety.Xmax=0 self.Ninety.Zmin=-3700 @@ -723,6 +696,7 @@ function CARRIERTRAINER:_InitStennis() self.Abeam.Distance=nil -- Wake position + self.Wake.name="Wake" self.Wake.Xmin=-4000 self.Wake.Xmax=0 self.Wake.Zmin=-2000 @@ -736,10 +710,12 @@ function CARRIERTRAINER:_InitStennis() self.Wake.Distance=nil -- In the groove + self.Groove.name="Groove" self.Groove.Xmin=-4000 self.Groove.Xmax=100 -- Landing trap + self.Trap.name="Trap" self.Trap.Xmin=-3000 self.Trap.Xmax=nil self.Trap.Zmin=-2000 @@ -749,6 +725,50 @@ function CARRIERTRAINER:_InitStennis() end +--- Check limits for reaching next step. +-- @param #CARRIERTRAINER self +-- @param #number X X position of player unit. +-- @param #number Z Z position of player unit. +-- @param #CARRIERTRAINER.Checkpoint check Checkpoint. +-- @return #boolean If true, checkpoint condition for next step was reached. +function CARRIERTRAINER:_CheckLimits(X, Z, check) + + --[[ + local next=true + if check.LimitXmin and Xcheck.LimitXmax then + next=false + elseif check.LimitZmin and Zcheck.LimitZmax then + next=false + end + + + local next = ((check.LimitXmin and math.abs(X)>=math.abs(check.LimitXmin)) or check.LimitXmin==nil) and + ((check.LimitXmax and math.abs(X)<=math.abs(check.LimitXmax)) or check.LimitXmax==nil) and + ((check.LimitZmin and math.abs(Z)>=math.abs(check.LimitZmin)) or check.LimitZmin==nil) and + ((check.LimitZmax and math.abs(Z)<=math.abs(check.LimitZmax)) or check.LimitZmax==nil) + ]] + + local nextXmin=check.LimitXmin==nil or (check.LimitXmin and (check.LimitXmin<0 and X<=check.LimitXmin or check.LimitXmin>=0 and X>=check.LimitXmin)) + local nextXmax=check.LimitXmax==nil or (check.LimitXmax and (check.LimitXmax<0 and X>=check.LimitXmax or check.LimitXmax>=0 and X<=check.LimitXmax)) + local nextZmin=check.LimitZmin==nil or (check.LimitZmin and (check.LimitZmin<0 and Z<=check.LimitZmin or check.LimitZmin>=0 and Z>=check.LimitZmin)) + local nextZmax=check.LimitZmax==nil or (check.LimitZmax and (check.LimitZmax<0 and Z>=check.LimitZmax or check.LimitZmax>=0 and Z<=check.LimitZmax)) + + local next=nextXmin and nextXmax and nextZmin and nextZmax + + --self:E({next=next, X=X, Z=Z, check=check}) + + local text=string.format("step=%s: next=%s: X=%d Xmin=%s Xmax=%s ||| Z=%d Zmin=%s Zmax=%s", + check.name, tostring(next), X, tostring(check.LimitXmin), tostring(check.LimitXmax), Z, tostring(check.LimitZmin), tostring(check.LimitZmax)) + self:E(self.lid..text) + MESSAGE:New(text,1):ToAllIf(self.Debug) + + return next +end + ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- CARRIER TRAINING functions ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -1325,16 +1345,18 @@ function CARRIERTRAINER:_AltitudeCheck(playerData, checkpoint, altitude) local score local hint local steptext=self:_StepName(playerData.step) + + if _error>badscore then score = -10 hint = string.format("You're high %s. ", steptext) elseif _error>lowscore then score = -5 hint = string.format("You're slightly high %s. ", steptext) - elseif _errorlowscore then score = -5 hint = string.format("You're slightly too far from the boat.") - elseif _errorlowscore then --Slightly slow score = -5 hint = "You're slightly slow." - elseif _error Date: Mon, 22 Oct 2018 23:41:48 +0200 Subject: [PATCH 07/15] CT v0.1.0 --- .../Moose/Functional/CarrierTrainer.lua | 604 +++++++++++------- 1 file changed, 377 insertions(+), 227 deletions(-) diff --git a/Moose Development/Moose/Functional/CarrierTrainer.lua b/Moose Development/Moose/Functional/CarrierTrainer.lua index f44b7d2c7..fd3546e7c 100644 --- a/Moose Development/Moose/Functional/CarrierTrainer.lua +++ b/Moose Development/Moose/Functional/CarrierTrainer.lua @@ -27,7 +27,8 @@ -- @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 Core.Zone#ZONE_UNIT giantZone Large zone around the carrier to welcome players. +-- @field Core.Zone#ZONE_UNIT registerZone Zone behind the carrier to register for a new approach. -- @field #table players Table of players. -- @field #table menuadded Table of units where the F10 radio menu was added. -- @field #CARRIERTRAINER.Checkpoint Upwind Upwind checkpoint. @@ -122,6 +123,7 @@ CARRIERTRAINER.Difficulty={ -- @field #string summary Result summary text. -- @field Wrapper.Client#CLIENT client object of player. -- @field #string difficulty Difficulty level. +-- @field #boolean inbigzone If true, player is in the big zone. --- Checkpoint parameters triggering the next step in the pattern. -- @type CARRIERTRAINER.Checkpoint @@ -146,7 +148,7 @@ CARRIERTRAINER.MenuF10={} --- Carrier trainer class version. -- @field #string version -CARRIERTRAINER.version="0.0.9" +CARRIERTRAINER.version="0.1.0" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Constructor @@ -185,6 +187,22 @@ function CARRIERTRAINER:New(carriername, alias) -- Set alias. self.alias=alias or carriername + if self.carriertype==CARRIERTRAINER.CarrierType.STENNIS then + self:_InitStennis() + elseif self.carriertype==CARRIERTRAINER.CarrierType.VINSON then + -- TODO: Carl Vinson parameters. + self:_InitStennis() + elseif self.carriertype==CARRIERTRAINER.CarrierType.TARAWA then + -- TODO: Tarawa parameters. + self:_InitStennis() + elseif self.carriertype==CARRIERTRAINER.CarrierType.KUZNETSOV then + -- TODO: Kusnetsov parameters - maybe... + self:_InitStennis() + else + self:E(self.lid.."ERROR: Unknown carrier type!") + return nil + end + ----------------------- --- FSM Transitions --- ----------------------- @@ -237,7 +255,7 @@ function CARRIERTRAINER:onafterStart(From, Event, To) -- Handle events. self:HandleEvent(EVENTS.Birth) - --self:HandleEvent(EVENTS.Lan) + self:HandleEvent(EVENTS.Land) -- Init status check self:__Status(5) @@ -264,6 +282,7 @@ end -- @param #string To To state. function CARRIERTRAINER:onafterStop(From, Event, To) self:UnHandleEvent(EVENTS.Birth) + self:UnHandleEvent(EVENTS.Land) end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -307,6 +326,39 @@ function CARRIERTRAINER:OnEventBirth(EventData) end end + +--- Carrier trainer event handler for event land. +-- @param #CARRIERTRAINER self +-- @param Core.Event#EVENTDATA EventData +function CARRIERTRAINER:OnEventLand(EventData) + self:F3({eventland = EventData}) + + local _unitName=EventData.IniUnitName + local _unit, _playername=self:_GetPlayerUnitAndName(_unitName) + + self:T3(self.lid.."LAND: unit = "..tostring(EventData.IniUnitName)) + self:T3(self.lid.."LAND: group = "..tostring(EventData.IniGroupName)) + self:T3(self.lid.."LAND: 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 unit %s (ID=%d) of group %s landed.", _playername, _callsign, _unitName, _uid, _group:GetName()) + self:T(self.lid..text) + MESSAGE:New(text, 5):ToAllIf(self.Debug) + + -- Check if we caught a wire after one second. + -- TODO: test this! + local playerData=self.players[_playername] + SCHEDULER:New(nil, self._Trapped,{self, playerData}, 1) + + end +end + --- Initialize player data. -- @param #CARRIERTRAINER self -- @param #string unitname Name of the player unit. @@ -326,6 +378,8 @@ function CARRIERTRAINER:_InitNewPlayer(unitname) playerData.difficulty=CARRIERTRAINER.Difficulty.NORMAL + playerData.inbigzone=playerData.unit:IsInZone(self.giantZone) + return playerData end @@ -334,9 +388,9 @@ end -- @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.score = 0 + playerData.summary = "Debriefing:\n" playerData.longDownwindDone = false playerData.highestCarrierXDiff = -9999999 playerData.secondsStandingStill = 0 @@ -392,38 +446,52 @@ function CARRIERTRAINER:_CheckPlayerStatus() if unit:IsAlive() then --self:_SendMessageToPlayer("current step "..self:_StepName(playerData.step),1,playerData) + --self:_DetailedPlayerStatus(playerData) --self:_DetailedPlayerStatus(playerData) if unit:IsInZone(self.giantZone) then - --self:_DetailedPlayerStatus(playerData) + + -- Check if player was previously not inside the zone. + if playerData.inbigzone==false then + + local text=string.format("Welcome back, %s! TCN 1X, BRC 354 (MAG HDG).\n", playerData.callsign) + local heading=playerData.unit:GetCoordinate():HeadingTo(self.registerZone:GetCoordinate()) + local distance=playerData.unit:GetCoordinate():Get2DDistance(self.registerZone:GetCoordinate()) + text=text..string.format("Fly heading %d for %.1f NM to begin your approach.", heading, distance) + MESSAGE:New(text, 5):ToClient(playerData.client) + + end + + if playerData.step==0 and unit:InAir() then + self:_NewRound(playerData) + elseif playerData.step == 1 then + self:_Start(playerData) + elseif playerData.step == 2 then + self:_Upwind(playerData) + elseif playerData.step == 3 then + self:_Break(playerData, "early") + elseif playerData.step == 4 then + self:_Break(playerData, "late") + elseif playerData.step == 5 then + self:_Abeam(playerData) + elseif playerData.step == 6 then + -- Check long down wind leg. + if not playerData.longDownwindDone then + self:_CheckForLongDownwind(playerData) + end + self:_Ninety(playerData) + elseif playerData.step == 7 then + self:_Wake(playerData) + elseif playerData.step == 8 then + self:_Groove(playerData) + elseif playerData.step == 9 then + self:_Trap(playerData) + end + + else + playerData.inbigzone=false end - if playerData.step==0 and unit:IsInZone(self.giantZone) and unit:InAir() then - self:_NewRound(playerData) - self:_InitStennis() - 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 @@ -445,7 +513,7 @@ function CARRIERTRAINER:_StepName(step) elseif step==1 then name="when entering pattern" elseif step==2 then - name="on upwind leg" + name="in the break entry" elseif step==3 then name="at the early break" elseif step==4 then @@ -453,9 +521,9 @@ function CARRIERTRAINER:_StepName(step) elseif step==5 then name="in the abeam position" elseif step==6 then - name="at the wake" - elseif step==7 then name="at the ninety" + elseif step==7 then + name="at the wake" elseif step==8 then name="in the groove" elseif step==9 then @@ -468,8 +536,10 @@ 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. +-- @return #number Distance [m] in the direction of the orientation of the carrier. +-- @return #number Distance [m] perpendicular to the orientation of the carrier. +-- @return #number Distance [m] to the carrier. +-- @return #number Angle [Deg] from carrier to plane. Phi=0 if the plane is directly behind the carrier, phi=90 if the plane is starboard, phi=180 if the plane is in front of the carrier. function CARRIERTRAINER:_GetDistances(unit) -- Vector to carrier @@ -493,7 +563,16 @@ function CARRIERTRAINER:_GetDistances(unit) -- Projection of player pos on z component. local dz=UTILS.VecDot(z,c) - return dx,dz + -- Polar coordinates + local rho=math.sqrt(dx*dx+dz*dz) + local phi=math.deg(math.atan2(dz,dx)) + if phi<0 then + phi=phi+360 + end + -- phi=0 if the plane is directly behind the carrier, phi=180 if the plane is in front of the carrier + phi=phi-180 + + return dx,dz,rho,phi end --- Check if a player is within the right area. @@ -568,7 +647,7 @@ function CARRIERTRAINER:_AbortPattern(playerData, X, Z, posData) local text=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)) self:E(self.lid..text) - MESSAGE:New(text, 60):ToAllIf(self.Debug) + --MESSAGE:New(text, 60):ToAllIf(self.Debug) self:_AddToSummary(playerData, "Approach aborted.") @@ -592,7 +671,7 @@ function CARRIERTRAINER:_DetailedPlayerStatus(playerData) local roll=unit:GetRoll() local pitch=unit:GetPitch() local dist=playerData.unit:GetCoordinate():Get2DDistance(self.carrier:GetCoordinate()) - local dx,dz=self:_GetDistances(unit) + local dx,dz,rho,phi=self:_GetDistances(unit) -- Player and carrier position vector. local playerPosition = playerData.unit:GetVec3() @@ -612,7 +691,8 @@ function CARRIERTRAINER:_DetailedPlayerStatus(playerData) text=text..string.format("velo x=%.1f y=%.1f z=%.1f\n", velo.x, velo.y, velo.z) text=text..string.format("wind x=%.1f y=%.1f z=%.1f\n", wind.x, wind.y, wind.z) text=text..string.format("pitch=%.1f | roll=%.1f | yaw=%.1f | climb=%.1f\n", pitch, roll, yaw, unit:GetClimbAnge()) - text=text..string.format("relheading %.1f degrees", relhead) + text=text..string.format("relheading=%.1f degrees\n", relhead) + text=text..string.format("rho=%.1f m phi=%.1f degrees\n", rho,phi) --text=text..string.format("current step = %d %s\n", playerData.step, self:_StepName(playerData.step)) --text=text..string.format("Carrier distance: d=%d m\n", dist) --text=text..string.format("Carrier distance: x=%d m z=%d m sum=%d (old)\n", diffX, diffZ, math.abs(diffX)+math.abs(diffZ)) @@ -647,7 +727,7 @@ function CARRIERTRAINER:_InitStennis() self.BreakEarly.Zmax=1500 self.BreakEarly.LimitXmin=0 self.BreakEarly.LimitXmax=nil - self.BreakEarly.LimitZmin=-370 --0.2 NM + self.BreakEarly.LimitZmin=-370 -- 0.2 NM port of carrier self.BreakEarly.LimitZmax=nil self.BreakEarly.Altitude=UTILS.FeetToMeters(800) self.BreakEarly.AoA=8.1 @@ -660,9 +740,9 @@ function CARRIERTRAINER:_InitStennis() self.BreakLate.Zmin=-3700 self.BreakLate.Zmax=1500 self.BreakLate.LimitXmin=0 - self.BreakLate.LimitXmax=10000 + self.BreakLate.LimitXmax=nil self.BreakLate.LimitZmin=-1470 --0.8 NM - self.BreakLate.LimitZmax=10000 + self.BreakLate.LimitZmax=nil self.BreakLate.Altitude=UTILS.FeetToMeters(800) self.BreakLate.AoA=8.1 self.BreakLate.Distance=nil @@ -671,19 +751,19 @@ function CARRIERTRAINER:_InitStennis() self.Abeam.name="Abeam Position" self.Abeam.Xmin=nil self.Abeam.Xmax=nil - self.Abeam.Zmin=-3700 + self.Abeam.Zmin=-4000 self.Abeam.Zmax=-1000 self.Abeam.LimitXmin=-200 - self.Abeam.LimitXmax=10000 - self.Abeam.LimitZmin=0 - self.Abeam.LimitZmax=10000 + self.Abeam.LimitXmax=nil + self.Abeam.LimitZmin=nil + self.Abeam.LimitZmax=nil self.Abeam.Altitude=UTILS.FeetToMeters(600) self.Abeam.AoA=8.1 - self.Abeam.Distance=nil + self.Abeam.Distance=UTILS.NMToMeters(1.2) -- At the ninety self.Ninety.name="Ninety" - self.Ninety.Xmin=-3700 + self.Ninety.Xmin=-4000 self.Ninety.Xmax=0 self.Ninety.Zmin=-3700 self.Ninety.Zmax=nil @@ -692,8 +772,8 @@ function CARRIERTRAINER:_InitStennis() self.Ninety.LimitZmin=nil self.Ninety.LimitZmax=-1111 self.Ninety.Altitude=UTILS.FeetToMeters(500) - self.Abeam.AoA=8.1 - self.Abeam.Distance=nil + self.Ninety.AoA=8.1 + self.Ninety.Distance=nil -- Wake position self.Wake.name="Wake" @@ -713,6 +793,15 @@ function CARRIERTRAINER:_InitStennis() self.Groove.name="Groove" self.Groove.Xmin=-4000 self.Groove.Xmax=100 + self.Groove.Zmin=-2000 + self.Groove.Zmax=nil + self.Groove.LimitXmin=nil + self.Groove.LimitXmax=nil + self.Groove.LimitZmin=nil + self.Groove.LimitZmax=nil + self.Groove.Altitude=UTILS.FeetToMeters(300) + self.Groove.AoA=8.1 + self.Groove.Distance=nil -- Landing trap self.Trap.name="Trap" @@ -720,8 +809,13 @@ function CARRIERTRAINER:_InitStennis() self.Trap.Xmax=nil self.Trap.Zmin=-2000 self.Trap.Zmax=2000 - --self.Trap.Limit=nil - self.Trap.Altitude=nil + self.Trap.LimitXmin=nil + self.Trap.LimitXmax=nil + self.Trap.LimitZmin=nil + self.Trap.LimitZmax=nil + self.Trap.Altitude=nil + self.Trap.AoA=nil + self.Trap.Distance=nil end @@ -733,25 +827,6 @@ end -- @return #boolean If true, checkpoint condition for next step was reached. function CARRIERTRAINER:_CheckLimits(X, Z, check) - --[[ - local next=true - if check.LimitXmin and Xcheck.LimitXmax then - next=false - elseif check.LimitZmin and Zcheck.LimitZmax then - next=false - end - - - local next = ((check.LimitXmin and math.abs(X)>=math.abs(check.LimitXmin)) or check.LimitXmin==nil) and - ((check.LimitXmax and math.abs(X)<=math.abs(check.LimitXmax)) or check.LimitXmax==nil) and - ((check.LimitZmin and math.abs(Z)>=math.abs(check.LimitZmin)) or check.LimitZmin==nil) and - ((check.LimitZmax and math.abs(Z)<=math.abs(check.LimitZmax)) or check.LimitZmax==nil) - ]] - local nextXmin=check.LimitXmin==nil or (check.LimitXmin and (check.LimitXmin<0 and X<=check.LimitXmin or check.LimitXmin>=0 and X>=check.LimitXmin)) local nextXmax=check.LimitXmax==nil or (check.LimitXmax and (check.LimitXmax<0 and X>=check.LimitXmax or check.LimitXmax>=0 and X<=check.LimitXmax)) local nextZmin=check.LimitZmin==nil or (check.LimitZmin and (check.LimitZmin<0 and Z<=check.LimitZmin or check.LimitZmin>=0 and Z>=check.LimitZmin)) @@ -759,12 +834,11 @@ function CARRIERTRAINER:_CheckLimits(X, Z, check) local next=nextXmin and nextXmax and nextZmin and nextZmax - --self:E({next=next, X=X, Z=Z, check=check}) - local text=string.format("step=%s: next=%s: X=%d Xmin=%s Xmax=%s ||| Z=%d Zmin=%s Zmax=%s", + local text=string.format("step=%s: next=%s: X=%d Xmin=%s Xmax=%s | Z=%d Zmin=%s Zmax=%s", check.name, tostring(next), X, tostring(check.LimitXmin), tostring(check.LimitXmax), Z, tostring(check.LimitZmin), tostring(check.LimitZmax)) - self:E(self.lid..text) - MESSAGE:New(text,1):ToAllIf(self.Debug) + self:T(self.lid..text) + --MESSAGE:New(text, 1):ToAllIf(self.Debug) return next end @@ -778,11 +852,15 @@ end -- @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) + if playerData.unit:IsInZone(self.registerZone) then + local text="Cleared for approach." + self:_SendMessageToPlayer(text, 10,playerData) - self:_InitNewRound(playerData) - playerData.step = 1 + self:_InitNewRound(playerData) + + -- Next step: start of pattern. + playerData.step = 1 + end end --- Start landing pattern, when player enters the start zone. @@ -790,16 +868,18 @@ end -- @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 in the break entry.", playerData.callsign) - self:_SendMessageToPlayer(hint, 8, playerData) + if playerData.unit:IsInZone(self.startZone) then + + local hint = string.format("Entering the pattern, %s! Aim for 800 feet and 350 kts in the break entry.", playerData.callsign) + self:_SendMessageToPlayer(hint, 8, playerData) + + -- Next step: upwind. + playerData.step = 2 + + end - -- TODO: Check for correct player heading! - playerData.score = 0 - playerData.step = 2 end - --- Upwind leg. -- @param #CARRIERTRAINER self -- @param #CARRIERTRAINER.PlayerData playerData Player data table. @@ -819,16 +899,15 @@ function CARRIERTRAINER:_Upwind(playerData) local altitude=playerData.unit:GetAltitude() - -- Get altutide. + -- Get altitude. local hint=self:_AltitudeCheck(playerData, self.Upwind, altitude) - - + self:_SendMessageToPlayer(hint, 8, playerData) self:_AddToSummary(playerData, hint) -- Next step. - playerData.step = 3 + playerData.step = 3 end end @@ -847,28 +926,29 @@ function CARRIERTRAINER:_Break(playerData, part) if part == "late" then breakpoint = self.BreakLate end - - + -- Check abort conditions. if self:_CheckAbort(diffX, diffZ, breakpoint) then self:_AbortPattern(playerData, diffX, diffZ, breakpoint) return end - -- Check if too far left - --if diffZ < limit then + -- Check limits. if self:_CheckLimits(diffX, diffZ, breakpoint) then + -- Get current altitude. local altitude=playerData.unit:GetAltitude() - -- Check altitude. + -- Grade altitude. local hint=self:_AltitudeCheck(playerData, breakpoint, altitude) - self:_SendMessageToPlayer(hint, 8, playerData) + -- Send message to player. + self:_SendMessageToPlayer(hint, 10, playerData) -- Add hint to summary. self:_AddToSummary(playerData, hint) + -- Nest step: late break or abeam. if (part == "early") then playerData.step = 4 else @@ -877,10 +957,46 @@ function CARRIERTRAINER:_Break(playerData, part) end end +--- Long downwind leg check. +-- @param #CARRIERTRAINER self +-- @param #CARRIERTRAINER.PlayerData playerData Player data table. +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) + + local limit = -1500 + + -- Check we are not too far out w.r.t back of the boat. + if diffX < limit then + + -- Get relative heading. + local relhead=self:_GetRelativeHeading(playerData.unit) + + if relhead<45 then + + -- Message to player. + local hint = "Your downwind leg is too long. Turn to final earlier next time." + self:_SendMessageToPlayer(hint, 10, playerData) + + -- Add to debrief. + self:_AddToSummary(playerData, hint) + + -- Decrease score. + playerData.score=playerData.score-40 + + -- Long downwind done! + playerData.longDownwindDone = true + end + + end +end + + --- Abeam. -- @param #CARRIERTRAINER self -- @param #CARRIERTRAINER.PlayerData playerData Player data table. -function CARRIERTRAINER:_Abeam(playerData) +function CARRIERTRAINER:_Abeam(playerData) -- Get distances between carrier and player unit (parallel and perpendicular to direction of movement of carrier) local diffX, diffZ = self:_GetDistances(playerData.unit) @@ -903,18 +1019,22 @@ function CARRIERTRAINER:_Abeam(playerData) local aoa = playerData.unit:GetAoA() local alt = playerData.unit:GetAltitude() + -- Grade AoA. local hintAoA=self:_AoACheck(playerData, self.Abeam, aoa) - -- Check Altitude + -- Grade Altitude. local hintAlt=self:_AltitudeCheck(playerData, self.Abeam, alt) - -- Check distance. - local hintDist=self:_DistanceCheck(playerData, self.Abeam, diffZ) + -- Grade distance to carrier. + local hintDist=self:_DistanceCheck(playerData, self.Abeam, math.abs(diffZ)) - local hintFull=string.format("%s.\n%s.\n%s.", hintAoA, hintAlt, hintDist) + -- Compile full hint. + local hintFull=string.format("%s\n%s\n%s", hintAlt, hintAoA, hintDist) - self:_SendMessageToPlayer(hintFull, 8, playerData ) + -- Send message to playerr. + self:_SendMessageToPlayer(hintFull, 10, playerData) + -- Add to debrief. self:_AddToSummary(playerData, hintFull) -- Proceed to next step. @@ -922,37 +1042,6 @@ function CARRIERTRAINER:_Abeam(playerData) end end ---- Down wind long check. --- @param #CARRIERTRAINER self --- @param #CARRIERTRAINER.PlayerData playerData Player data table. -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) - - local limit = -1500 - - -- Check we are not too far out w.r.t back of the boat. - if diffX < limit then - - local relhead=self:_GetRelativeHeading(playerData.unit) - - if relhead<45 then - - local hint = "Too long downwind. Turn final earlier next time." - self:_SendMessageToPlayer(hint, 8, playerData) - - self:_AddToSummary(playerData, hint) - - playerData.score=playerData.score-40 - - -- Long downwind done! - playerData.longDownwindDone = true - end - - end -end - --- Ninety. -- @param #CARRIERTRAINER self -- @param #CARRIERTRAINER.PlayerData playerData Player data table. @@ -967,24 +1056,34 @@ function CARRIERTRAINER:_Ninety(playerData) return end - local limitEast = -1111 --0.6nm + -- Get Realtive heading player to carrier. + local relheading=self:_GetRelativeHeading(playerData.unit) - --if diffZ > limitEast then - if self:_CheckLimits(diffX, diffZ, self.Ninety) then + -- At the 90, i.e. 90 degrees between player heading and BRC of carrier. + if relheading<=90 then local alt=playerData.unit:GetAltitude() local aoa=playerData.unit:GetAoA() + -- Grade altitude. local hintAlt=self:_AltitudeCheck(playerData, self.Ninety, alt) + + -- Grade AoA. local hintAoA=self:_AoACheck(playerData, self.Ninety, aoa) + -- Compile full hint. + local hintFull=string.format("%s\n%s", hintAlt, hintAoA) - local hintFull=string.format("%s.\n%s.", hintAoA, hintAlt) + -- Message to player. + self:_SendMessageToPlayer(hintFull, 10, playerData) + + -- Add to debrief. self:_AddToSummary(playerData, hintFull) + -- Long downwind not an issue any more playerData.longDownwindDone = true - -- Next step. + -- Next step: wake. playerData.step = 7 end end @@ -997,26 +1096,34 @@ function CARRIERTRAINER:_Wake(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 (diffZ < -2000 or diffX < -4000 or diffX > 0) then + -- Check abort conditions. if self:_CheckAbort(diffX, diffZ, self.Wake) then self:_AbortPattern(playerData, diffX, diffZ, self.Wake) return end - --if diffZ > 0 then + -- Right behind the wake of the carrier dZ>0. if self:_CheckLimits(diffX, diffZ, self.Wake) then local alt=playerData.unit:GetAltitude() local aoa=playerData.unit:GetAoA() + -- Grade altitude. local hintAlt=self:_AltitudeCheck(playerData, self.Wake, alt) + + -- Grade AoA. local hintAoA=self:_AoACheck(playerData, self.Wake, aoa) + -- Compile full hint. + local hintFull=string.format("%s\n%s", hintAlt, hintAoA) - local hintFull=string.format("%s.\n%s.", hintAoA, hintAlt) + -- Message to player. + self:_SendMessageToPlayer(hintFull, 10, playerData) + + -- Add to debrief. self:_AddToSummary(playerData, hintFull) - -- Next step. + -- Next step: Groove. playerData.step = 8 end end @@ -1026,58 +1133,51 @@ end -- @param #CARRIERTRAINER.PlayerData playerData Player data table. function CARRIERTRAINER:_Groove(playerData) - --TODO -100?! -- Get distances between carrier and player unit (parallel and perpendicular to direction of movement of carrier) local diffX, diffZ = self:_GetDistances(playerData.unit) - - --diffX=diffX+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 self:_AbortPattern(playerData, diffX, diffZ, self.Groove) return end + + -- Get heading of runway. + local brc=self.carrier:GetHeading() + local rwy=brc-10 --runway heading is -10 degree from carrier BRC. + if rwy<0 then + rwy=rwy+360 + end + -- Radial (inverse heading). + rwy=rwy-180 - --TODO: - if (diffX > -500) then --Reached in close before groove + -- 0 means player is on BRC course but runway heading is -10 degrees. + local heading=self:_GetRelativeHeading(playerData.unit)-10 - local hint = "You're too far left and never reached the groove." - self:_SendMessageToPlayer( hint, 8, playerData ) + if diffZ>-1300 and heading<10 then + + local alt = playerData.unit:GetAltitude() + local aoa = playerData.unit:GetAoA() + + -- Grade altitude. + local hintAlt=self:_AltitudeCheck(playerData, self.Groove, alt) + + -- AoA feed back + local hintAoA=self:_AoACheck(playerData, self.Groove, aoa) - -- zero score - self:_PrintScore(0, playerData, true) - self:_AddToSummary(playerData, hint) + -- Compile full hint. + local hintFull=string.format("%s\n%s", hintAlt, hintAoA) + + -- Message to player. + self:_SendMessageToPlayer(hintFull, 10, playerData) + + -- Add to debrief. + self:_AddToSummary(playerData, hintFull) -- Next step. playerData.step = 9 - - else - - local limitDeg = 8.0 - - -- TODO: what is this angle? Does not make sense! - local fraction = diffZ / (-diffX) - local asinValue = math.asin(fraction) - local angle = math.deg(asinValue) - - if diffZ > -1300 and angle > limitDeg then - - -- Altitude check. - local score, hint=self:_AltitudeCheck(playerData, self.Groove) - - -- AoA feed back - local aoa = playerData.unit:GetAoA() - local score, hint=self:_AoACheck(aoa, self.Groove,playerData) - - -- TODO - --local fullHint = hint .. " (" .. aoaFeedback .. ")" - --self:_AddToSummary(playerData, fullHint) - - -- Next step. - playerData.step = 9 - end end + end --- Trap. @@ -1086,16 +1186,16 @@ end function CARRIERTRAINER:_Trap(playerData) -- 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, rho, phi = self:_GetDistances(playerData.unit) -- Player altitude local alt=playerData.unit:GetAltitude() -- Get velocities. - local playerVelocity = playerData.unit:GetVelocityKMH() + local playerVelocity = playerData.unit:GetVelocityKMH() local carrierVelocity = self.carrier:GetVelocityKMH() - --if(diffZ < -2000 or diffZ > 2000 or diffX < -3000) then + -- Check abort conditions. if self:_CheckAbort(diffX, diffZ, self.Trap) then self:_AbortPattern(playerData, diffX, diffZ, self.Trap) return @@ -1109,48 +1209,56 @@ function CARRIERTRAINER:_Trap(playerData) playerData.lowestAltitude = alt end - if math.abs(playerVelocity-carrierVelocity) < 0.01 then + -- Lineup. + local lineup = math.asin(diffZ/(-(diffX-100))) + local lineuperror = math.deg(lineup)-10 - playerData.secondsStandingStill = playerData.secondsStandingStill + 1 + -- Glideslope. + local glideslope = math.atan((playerData.unit:GetAltitude()-22)/(-diffX)) + local glideslopeError = math.deg(glideslope) - 3.5 - 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 - - -- Which wire - 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 + if diffX<100 then + + local text="Good height." + if glideslopeError>1 then + text="You're too high! Throttles back!" + elseif glideslopeError>0.5 then + text="You're slightly high. Decrease power." + elseif glideslopeError<1.0 then + text="Power! You're way too low." + elseif glideslopeError<0.5 then + text="You're slightly low. Increase power." + else end + local aoa=playerData.unit:GetAoA() + if aoa>=9.0 then + text=text.." You're way too slow!" + elseif aoa>=8.5 then + text=text.." You're slow." + elseif aoa<6.9 then + text=text.." You're too fast!" + elseif aoa<7.7 then + text=text.." You're slightly fast." + else + text=text.." Looking good on speed." + end + text=text.."\n" + + if lineuperror>3 then + text=text.."Come left!" + elseif lineuperror >1 then + text=text.."Come left..." + elseif lineuperror <3 then + text=text.."Right for lineup!" + elseif lineuperror <1 then + text=text.."Right for lineup.." + else + text=text.."Good on lineup." + end + + self:_SendMessageToPlayer(text, 8,playerData) + elseif (diffX > 150) then local wire = 0 @@ -1176,6 +1284,49 @@ function CARRIERTRAINER:_Trap(playerData) end end +--- Trapped? +-- @param #CARRIERTRAINER self +-- @param #CARRIERTRAINER.PlayerData playerData Player data table. +function CARRIERTRAINER:_Trapped(playerData) + + -- Get distances between carrier and player unit (parallel and perpendicular to direction of movement of carrier) + local diffX, diffZ, rho, phi = self:_GetDistances(playerData.unit) + + -- Get velocities. + local playerVelocity = playerData.unit:GetVelocityKMH() + local carrierVelocity = self.carrier:GetVelocityKMH() + + if math.abs(playerVelocity-carrierVelocity) < 0.01 then + env.info("Trap identified! diff " .. diffX .. ", highestCarrierXDiff" .. playerData.highestCarrierXDiff .. ", secondsStandingStill: " .. playerData.secondsStandingStill) + + local wire = 1 + local score = -10 + + -- Which wire + 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:_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." + + end +end + ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Menu Functions ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -1364,7 +1515,7 @@ function CARRIERTRAINER:_AltitudeCheck(playerData, checkpoint, altitude) hint = string.format("Good altitude %s. ", steptext) end - hint=hint..string.format(" %d deviation from %d ft target alt.", _error, UTILS.MetersToFeet(checkpoint.Altitude)) + hint=hint..string.format(" Altitude %d ft = %d%% deviation from %d ft target alt.", UTILS.MetersToFeet(altitude), _error, UTILS.MetersToFeet(checkpoint.Altitude)) -- Set score. playerData.score=playerData.score+score @@ -1388,7 +1539,6 @@ function CARRIERTRAINER:_DistanceCheck(playerData, checkpoint, distance) local score local hint - local dnm=UTILS.MetersToNM(distance) local steptext=self:_StepName(playerData.step) if _error>badscore then score = -10 @@ -1407,7 +1557,7 @@ function CARRIERTRAINER:_DistanceCheck(playerData, checkpoint, distance) hint = string.format("with perfect distance to the boat.") end - hint=hint..string.format(" %d\% deviation from %d target distance %.1f NM.", _error, UTILS.MetersToNM(checkpoint.Distance)) + hint=hint..string.format(" Distance %.1f NM = %d%% deviation from %.1f NM optimal distance.",UTILS.MetersToNM(distance), _error, UTILS.MetersToNM(checkpoint.Distance)) -- Set score. playerData.score=playerData.score+score @@ -1448,7 +1598,7 @@ function CARRIERTRAINER:_AoACheck(playerData, checkpoint, aoa) hint = "You're on speed!" end - hint=hint..string.format(" %d\% deviation from %d target AoA.", _error, checkpoint.AoA) + hint=hint..string.format(" AoA %.1f = %d %% deviation from %.1f target AoA.", aoa, _error, checkpoint.AoA) -- Set score. playerData.score=playerData.score+score From d9374f038922dad2a07519d4a22c223c8cec5c5c Mon Sep 17 00:00:00 2001 From: Frank Date: Tue, 23 Oct 2018 23:35:25 +0200 Subject: [PATCH 08/15] Controllable Added more general orbit task. --- .../Moose/Wrapper/Controllable.lua | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/Moose Development/Moose/Wrapper/Controllable.lua b/Moose Development/Moose/Wrapper/Controllable.lua index 4ab481021..bc05a0db8 100644 --- a/Moose Development/Moose/Wrapper/Controllable.lua +++ b/Moose Development/Moose/Wrapper/Controllable.lua @@ -870,6 +870,34 @@ function CONTROLLABLE:TaskOrbitCircleAtVec2( Point, Altitude, Speed ) return DCSTask end +--- (AIR) Orbit at a position with at a given altitude and speed. Optionally, a race track pattern can be specified. +-- @param #CONTROLLABLE self +-- @param Core.Point#COORDINATE Coord Coordinate at which the CONTROLLABLE orbits. +-- @param #number Altitude Altitude in meters of the orbit pattern. +-- @param #number Speed Speed [m/s] flying the orbit pattern +-- @param Core.Point#COORDINATE CoordRaceTrack (Optional) If this coordinate is specified, the CONTROLLABLE will fly a race-track pattern using this and the initial coordinate. +-- @return #CONTROLLABLE self +function CONTROLLABLE:TaskOrbit(Coord, Altitude, Speed, CoordRaceTrack) + + local Pattern=AI.Task.OrbitPattern.CIRCLE + if CoordRaceTrack then + Pattern=AI.Task.OrbitPattern.RACE_TRACK + end + + local Task = { + id = 'Orbit', + params = { + pattern = Pattern, + point = Coord:GetVec2(), + point2 = CoordRaceTrack:GetVec2(), + speed = Speed, + altitude = Altitude, + } + } + + return Task +end + --- (AIR) Orbit at the current position of the first unit of the controllable at a specified alititude. -- @param #CONTROLLABLE self -- @param #number Altitude The altitude [m] to hold the position. From 4434d1da2122e9a57bc8be4ea986dcd1fb7c8fa9 Mon Sep 17 00:00:00 2001 From: Frank Date: Wed, 24 Oct 2018 21:32:07 +0200 Subject: [PATCH 09/15] Fixes AI_FORMATION: * added stop possibility ad interval input WAREHOUSE * added autodefence assignment UNIT: * improved InAir() check function --- Moose Development/Moose/AI/AI_Formation.lua | 33 ++++- .../Moose/Functional/CarrierTrainer.lua | 123 ++++++++++++++---- .../Moose/Functional/Warehouse.lua | 2 +- .../Moose/Wrapper/Controllable.lua | 3 - Moose Development/Moose/Wrapper/Group.lua | 2 +- Moose Development/Moose/Wrapper/Unit.lua | 27 ++-- 6 files changed, 143 insertions(+), 47 deletions(-) diff --git a/Moose Development/Moose/AI/AI_Formation.lua b/Moose Development/Moose/AI/AI_Formation.lua index 489e69cf3..cab3c528a 100644 --- a/Moose Development/Moose/AI/AI_Formation.lua +++ b/Moose Development/Moose/AI/AI_Formation.lua @@ -36,6 +36,7 @@ -- @field #boolean ReportTargets If true, nearby targets are reported. -- @Field DCSTypes#AI.Option.Air.val.ROE OptionROE Which ROE is set to the FollowGroup. -- @field DCSTypes#AI.Option.Air.val.REACTION_ON_THREAT OptionReactionOnThreat Which REACTION_ON_THREAT is set to the FollowGroup. +-- @field #number dtFollow Time step between position updates. --- Build large formations, make AI follow a @{Wrapper.Client#CLIENT} (player) leader or a @{Wrapper.Unit#UNIT} (AI) leader. @@ -106,6 +107,7 @@ AI_FORMATION = { FollowScheduler = nil, OptionROE = AI.Option.Air.val.ROE.OPEN_FIRE, OptionReactionOnThreat = AI.Option.Air.val.REACTION_ON_THREAT.ALLOW_ABORT_MISSION, + dtFollow = 0.5, } --- AI_FORMATION.Mode class @@ -139,7 +141,7 @@ function AI_FORMATION:New( FollowUnit, FollowGroupSet, FollowName, FollowBriefin self:AddTransition( "*", "Stop", "Stopped" ) - self:AddTransition( "None", "Start", "Following" ) + self:AddTransition( {"None", "Stopped"}, "Start", "Following" ) self:AddTransition( "*", "FormationLine", "*" ) --- FormationLine Handler OnBefore for AI_FORMATION @@ -620,6 +622,16 @@ function AI_FORMATION:New( FollowUnit, FollowGroupSet, FollowName, FollowBriefin return self end + +--- Set time interval between updates of the formation. +-- @param #AI_FORMATION self +-- @param #number dt Time step in seconds between formation updates. Default is every 0.5 seconds. +-- @return #AI_FORMATION +function AI_FORMATION:SetFollowTimeInterval(dt) --R2.1 + self.dtFollow=dt or 0.5 + return self +end + --- This function is for test, it will put on the frequency of the FollowScheduler a red smoke at the direction vector calculated for the escort to fly to. -- This allows to visualize where the escort is flying to. -- @param #AI_FORMATION self @@ -893,7 +905,20 @@ function AI_FORMATION:SetFlightRandomization( FlightRandomization ) --R2.1 end ---- @param Follow#AI_FORMATION self +--- Follow event fuction. Check if coming from state "stopped". If so the transition is rejected. +-- @param #AI_FORMATION self +-- @param Core.Set#SET_GROUP FollowGroupSet The following set of groups. +-- @param #string From From state. +-- @param #string Event Event. +-- @pram #string To The to state. +function AI_FORMATION:onbeforeFollowing( FollowGroupSet, From, Event, To ) --R2.1 + if From=="Stopped" then + return false -- Deny transition. + end + return true +end + +--- @param #AI_FORMATION self function AI_FORMATION:onenterFollowing( FollowGroupSet ) --R2.1 self:F( ) @@ -1032,8 +1057,8 @@ function AI_FORMATION:onenterFollowing( FollowGroupSet ) --R2.1 end, self, ClientUnit, CT1, CV1, CT2, CV2 ) - - self:__Follow( -0.5 ) + + self:__Follow( -self.dtFollow ) end end diff --git a/Moose Development/Moose/Functional/CarrierTrainer.lua b/Moose Development/Moose/Functional/CarrierTrainer.lua index fd3546e7c..c0dcad809 100644 --- a/Moose Development/Moose/Functional/CarrierTrainer.lua +++ b/Moose Development/Moose/Functional/CarrierTrainer.lua @@ -26,6 +26,7 @@ -- @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 #string alias Alias of the carrier trainer. -- @field Core.Zone#ZONE_UNIT startZone Zone in which the pattern approach starts. -- @field Core.Zone#ZONE_UNIT giantZone Large zone around the carrier to welcome players. -- @field Core.Zone#ZONE_UNIT registerZone Zone behind the carrier to register for a new approach. @@ -39,6 +40,7 @@ -- @field #CARRIERTRAINER.Checkpoint Wake Right behind the carrier. -- @field #CARRIERTRAINER.Checkpoint Groove In the groove checkpoint. -- @field #CARRIERTRAINER.Checkpoint Trap Landing checkpoint. +-- @field -- @extends Core.Fsm#FSM --- Practice Carrier Landings @@ -73,6 +75,8 @@ CARRIERTRAINER = { Wake = {}, Groove = {}, Trap = {}, + TACAN = nil, + ICLS = nil, } --- Aircraft types. @@ -148,7 +152,7 @@ CARRIERTRAINER.MenuF10={} --- Carrier trainer class version. -- @field #string version -CARRIERTRAINER.version="0.1.0" +CARRIERTRAINER.version="0.1.0w" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Constructor @@ -353,8 +357,11 @@ function CARRIERTRAINER:OnEventLand(EventData) -- Check if we caught a wire after one second. -- TODO: test this! - local playerData=self.players[_playername] - SCHEDULER:New(nil, self._Trapped,{self, playerData}, 1) + local playerData=self.players[_playername] --#CARRIERTRAINER.PlayerData + local coord=playerData.unit:GetCoordinate() + + -- Call trapped function in 5 seconds to make sure we did not bolter. + SCHEDULER:New(nil, self._Trapped,{self, playerData, coord}, 5) end end @@ -1287,18 +1294,19 @@ end --- Trapped? -- @param #CARRIERTRAINER self -- @param #CARRIERTRAINER.PlayerData playerData Player data table. -function CARRIERTRAINER:_Trapped(playerData) +-- @param Core.Point#COORDINATE pos Position of aircraft on landing event. +function CARRIERTRAINER:_Trapped(playerData, pos) -- Get distances between carrier and player unit (parallel and perpendicular to direction of movement of carrier) - local diffX, diffZ, rho, phi = self:_GetDistances(playerData.unit) + local diffX, diffZ, rho, phi = self:_GetDistances(pos) -- Get velocities. local playerVelocity = playerData.unit:GetVelocityKMH() - local carrierVelocity = self.carrier:GetVelocityKMH() + local carrierVelocity = self.carrier:GetVelocityKMH() - if math.abs(playerVelocity-carrierVelocity) < 0.01 then - env.info("Trap identified! diff " .. diffX .. ", highestCarrierXDiff" .. playerData.highestCarrierXDiff .. ", secondsStandingStill: " .. playerData.secondsStandingStill) - + if playerData.unit:InAir()==false then + -- Seems we have successfully landed. + local wire = 1 local score = -10 @@ -1317,13 +1325,19 @@ function CARRIERTRAINER:_Trapped(playerData) score = 7 end - self:_SendMessageToPlayer( "TRAPPED! " .. wire .. "-wire!", 30, playerData ) - self:_PrintScore(score, playerData, false) + local text=string.format("TRAPPED! %d-wire.", wire) + self:_SendMessageToPlayer(text, 30, playerData) - env.info("Distance! " .. diffX .. " meters resulted in a " .. wire .. "-wire estimation."); + local text2=string.format("Distance %.1f meters resulted in a %d-wire estimate.", diffX, wire) + MESSAGE:New(text,30):ToAllIf(self.Debug) + env.info(text2) - local fullHint = "Trapped catching the " .. wire .. "-wire." + local fullHint = string.format("Trapped catching the %d-wire.", wire) + self:_AddToSummary(playerData, fullHint) + + else + --Boltered! end end @@ -1358,24 +1372,35 @@ function CARRIERTRAINER:_AddF10Commands(_unitName) 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) + + local playerData=self.players[playername] + + -- F10/Carrier Trainer/ + local _trainPath = missionCommands.addSubMenuForGroup(_gid, self.alias, CARRIERTRAINER.MenuF10[_gid]) + -- F10/Carrier Trainer//Results + --local _statsPath = missionCommands.addSubMenuForGroup(_gid, "Results", _trainPath) + -- F10/Carrier Trainer//My Settings + local _settingsPath = missionCommands.addSubMenuForGroup(_gid, "My Settings", _trainPath) + -- F10/Carrier Trainer//My Settings/Difficulty + local _difficulPath = missionCommands.addSubMenuForGroup(_gid, "Difficulty", _settingsPath) + -- F10/Carrier Trainer//Carrier Info + local _infoPath = missionCommands.addSubMenuForGroup(_gid, "Carrier Info", _trainPath) - -- F10/On the Range//Stats/ + -- F10/Carrier Trainer//Stats/ --missionCommands.addCommandForGroup(_gid, "All Results", _statsPath, self._DisplayStrafePitResults, self, _unitName) --missionCommands.addCommandForGroup(_gid, "My Results", _statsPath, self._DisplayBombingResults, self, _unitName) --missionCommands.addCommandForGroup(_gid, "Reset All Results", _statsPath, self._ResetRangeStats, self, _unitName) - -- F10/On the Range//My Settings/ + -- F10/Carrier Trainer//My Settings/ --missionCommands.addCommandForGroup(_gid, "Smoke Delay On/Off", _settingsPath, self._SmokeBombDelayOnOff, self, _unitName) --missionCommands.addCommandForGroup(_gid, "Smoke Impact On/Off", _settingsPath, self._SmokeBombImpactOnOff, self, _unitName) - --missionCommands.addCommandForGroup(_gid, "Flare Hits On/Off", _settingsPath, self._FlareDirectHitsOnOff, self, _unitName) - -- F10/On the Range//Range Information - --missionCommands.addCommandForGroup(_gid, "General Info", _infoPath, self._DisplayRangeInfo, self, _unitName) + --missionCommands.addCommandForGroup(_gid, "Flare Hits On/Off", _settingsPath, self._FlareDirectHitsOnOff, self, _unitName) + -- F10/Carrier Trainer//My Settings/Difficulty + missionCommands.addCommandForGroup(_gid, "Flight Student", _difficulPath, self.SetDifficulty, self, playerData, CARRIERTRAINER.Difficulty.EASY) + missionCommands.addCommandForGroup(_gid, "Naval Aviator", _difficulPath, self.SetDifficulty, self, playerData, CARRIERTRAINER.Difficulty.NORMAL) + missionCommands.addCommandForGroup(_gid, "TOPGUN Graduate", _difficulPath, self.SetDifficulty, self, playerData, CARRIERTRAINER.Difficulty.HARD) + -- F10/Carrier Trainer//Carrier Info/ + missionCommands.addCommandForGroup(_gid, "Carrier Info", _infoPath, self._DisplayCarrierInfo, self, _unitName) missionCommands.addCommandForGroup(_gid, "Weather Report", _infoPath, self._DisplayCarrierWeather, self, _unitName) - --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) @@ -1386,11 +1411,55 @@ function CARRIERTRAINER:_AddF10Commands(_unitName) end ---- Report weather conditions at range. Temperature, QFE pressure and wind data. +--- Report information about carrier. +-- @param #CARRIERTRAINER self +-- @param #string _unitname Name of the player unit. +function CARRIERTRAINER:_DisplayCarrierInfo(_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=string.format("%s info:\n", self.alias) + + -- Current coordinates. + local coord=self.carrier:GetCoordinate() + + local playerData=self.players[playername] --#CARRIERTRAINER.PlayerData + + local carrierheading=self.carrier:GetHeading() + local carrierspeed=UTILS.MpsToKnots(self.carrier:GetVelocity()) + + text=text..string.format("BRC %d\n", carrierheading) + text=text..string.format("Speed %d kts\n", carrierspeed) + + + local tacan="unknown" + local icls="unknown" + if self.TACAN~=nil then + tacan=tostring(self.TACAN) + end + if self.ICLS~=nil then + icls=tostring(self.ICLS) + end + + text=text..string.format("TACAN Channel %s", tacan) + text=text..string.format("ICLS Channel %s", icls) + + end + +end + + +--- Report weather conditions at the carrier location. Temperature, QFE pressure and wind data. -- @param #CARRIERTRAINER self -- @param #string _unitname Name of the player unit. function CARRIERTRAINER:_DisplayCarrierWeather(_unitname) - self:E(_unitname) + self:F(_unitname) -- Get player unit and player name. local unit, playername = self:_GetPlayerUnitAndName(_unitname) @@ -1677,7 +1746,7 @@ function CARRIERTRAINER:_HandleCollectedResult(playerData, wire) playerData.collectedResultString = newString else playerData.collectedResultString = playerData.collectedResultString .. ", " .. newString - MessageToAll( playerData.callsign .. "'s " .. playerData.passes .. " passes: " .. playerData.collectedResultString .. " (TOTAL: " .. playerData.totalScore .. ")" , 30, "CollectedResult" ) + MessageToAll( playerData.callsign .. "'s " .. playerData.passes .. " passes: " .. playerData.collectedResultString .. " (TOTAL: " .. playerData.totalscore .. ")" , 30, "CollectedResult" ) end local heading=playerData.unit:GetCoordinate():HeadingTo(self.startZone:GetCoordinate()) diff --git a/Moose Development/Moose/Functional/Warehouse.lua b/Moose Development/Moose/Functional/Warehouse.lua index 499e0683e..1241f077d 100644 --- a/Moose Development/Moose/Functional/Warehouse.lua +++ b/Moose Development/Moose/Functional/Warehouse.lua @@ -4620,7 +4620,7 @@ function WAREHOUSE:onafterAttacked(From, Event, To, Coalition, Country) text=text..string.format("Deploying all %d ground assets.", nground) -- Add self request. - self:AddRequest(self, WAREHOUSE.Descriptor.CATEGORY, Group.Category.GROUND, WAREHOUSE.Quantity.ALL, nil, nil , 0) + self:AddRequest(self, WAREHOUSE.Descriptor.CATEGORY, Group.Category.GROUND, WAREHOUSE.Quantity.ALL, nil, nil , 0, "AutoDefence") else text=text..string.format("No ground assets currently available.") end diff --git a/Moose Development/Moose/Wrapper/Controllable.lua b/Moose Development/Moose/Wrapper/Controllable.lua index bc05a0db8..cdd6a39dd 100644 --- a/Moose Development/Moose/Wrapper/Controllable.lua +++ b/Moose Development/Moose/Wrapper/Controllable.lua @@ -3034,6 +3034,3 @@ function CONTROLLABLE:IsAirPlane() return nil end - - --- Message APIs \ No newline at end of file diff --git a/Moose Development/Moose/Wrapper/Group.lua b/Moose Development/Moose/Wrapper/Group.lua index e6c6648fd..6c6061e7d 100644 --- a/Moose Development/Moose/Wrapper/Group.lua +++ b/Moose Development/Moose/Wrapper/Group.lua @@ -325,7 +325,7 @@ end -- So all event listeners will catch the destroy event of this group for each unit in the group. -- To raise these events, provide the `GenerateEvent` parameter. -- @param #GROUP self --- @param #boolean GenerateEvent true if you want to generate a crash or dead event for each unit. +-- @param #boolean GenerateEvent If true, a crash or dead event for each unit is generated. If false, if no event is triggered. If nil, a RemoveUnit event is triggered. -- @usage -- -- Air unit example: destroy the Helicopter and generate a S_EVENT_CRASH for each unit in the Helicopter group. -- Helicopter = GROUP:FindByName( "Helicopter" ) diff --git a/Moose Development/Moose/Wrapper/Unit.lua b/Moose Development/Moose/Wrapper/Unit.lua index 5487ef3b6..b50a908f4 100644 --- a/Moose Development/Moose/Wrapper/Unit.lua +++ b/Moose Development/Moose/Wrapper/Unit.lua @@ -902,22 +902,27 @@ end function UNIT:InAir() self:F2( self.UnitName ) + -- Get DCS unit object. local DCSUnit = self:GetDCSObject() --DCS#Unit if DCSUnit then --- Implementation of workaround. The original code is below. --- This to simulate the landing on buildings. --- local UnitInAir = DCSUnit:inAir() - local UnitInAir = true - local VelocityVec3 = DCSUnit:getVelocity() - local Velocity = ( VelocityVec3.x ^ 2 + VelocityVec3.y ^ 2 + VelocityVec3.z ^ 2 ) ^ 0.5 -- in meters / sec - local Coordinate = DCSUnit:getPoint() - local LandHeight = land.getHeight( { x = Coordinate.x, y = Coordinate.z } ) - local Height = Coordinate.y - LandHeight - if Velocity < 1 and Height <= 60 then - UnitInAir = false + -- Get DCS result of whether unit is in air or not. + local UnitInAir = DCSUnit:inAir() + + -- If DCS says that it is in air, check if this is really the case, since we might have landed on a building where inAir()=true but actually is not. + if UnitInAir==true then + local VelocityVec3 = DCSUnit:getVelocity() + --local Velocity = ( VelocityVec3.x ^ 2 + VelocityVec3.y ^ 2 + VelocityVec3.z ^ 2 ) ^ 0.5 -- in meters / sec + local Velocity = UTILS.VecNorm(VelocityVec3) + local Coordinate = DCSUnit:getPoint() + local LandHeight = land.getHeight( { x = Coordinate.x, y = Coordinate.z } ) + local Height = Coordinate.y - LandHeight + if Velocity < 1 and Height <= 60 then + UnitInAir = false + end end + self:T3( UnitInAir ) return UnitInAir end From 238fcf1176b1e39d3ff38f45a920596b60432472 Mon Sep 17 00:00:00 2001 From: Frank Date: Wed, 24 Oct 2018 23:13:56 +0200 Subject: [PATCH 10/15] WAREHOUSE v0.6.5 * fixed bug for assignments --- .../Moose/Functional/Warehouse.lua | 21 ++++++++++--------- .../Moose/Wrapper/Controllable.lua | 4 +++- 2 files changed, 14 insertions(+), 11 deletions(-) diff --git a/Moose Development/Moose/Functional/Warehouse.lua b/Moose Development/Moose/Functional/Warehouse.lua index 1241f077d..1536fca49 100644 --- a/Moose Development/Moose/Functional/Warehouse.lua +++ b/Moose Development/Moose/Functional/Warehouse.lua @@ -624,7 +624,8 @@ -- The @{#WAREHOUSE.OnAfterAttacked} function can be used by the mission designer to react to the enemy attack. For example by deploying some or all ground troops -- currently in stock to defend the warehouse. Note that the warehouse also has a self defence option which can be enabled by the @{#WAREHOUSE.SetAutoDefenceOn}() -- function. In this case, the warehouse will automatically spawn all ground troops. If the spawn zone is further away from the warehouse zone, all mobile troops --- are routed to the warehouse zone. +-- are routed to the warehouse zone. The self request which is triggered on an automatic defence has the assignment "AutoDefence". So you can use this to +-- give orders to the groups that were spawned using the @{#WAREHOUSE.OnAfterSelfRequest} function. -- -- If only ground troops of the enemy coalition are present in the warehouse zone, the warehouse and all its assets falls into the hands of the enemy. -- In this case the event **Captured** is triggered which can be captured by the @{#WAREHOUSE.OnAfterCaptured} function. @@ -1726,7 +1727,7 @@ WAREHOUSE.db = { --- Warehouse class version. -- @field #string version -WAREHOUSE.version="0.6.4" +WAREHOUSE.version="0.6.5" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO: Warehouse todo list. @@ -1735,12 +1736,12 @@ WAREHOUSE.version="0.6.4" -- TODO: Add check if assets "on the move" are stationary. Can happen if ground units get stuck in buildings. If stationary auto complete transport by adding assets to request warehouse? Time? -- TODO: Optimize findpathonroad. Do it only once (first time) and safe paths between warehouses similar to off-road paths. -- TODO: Spawn assets only virtually, i.e. remove requested assets from stock but do NOT spawn them ==> Interface to A2A dispatcher! Maybe do a negative sign on asset number? --- TODO: Test capturing a neutral warehouse. -- TODO: Make more examples: ARTY, CAP, ... -- TODO: Check also general requests like all ground. Is this a problem for self propelled if immobile units are among the assets? Check if transport. -- TODO: Handle the case when units of a group die during the transfer. -- TODO: Added habours as interface for transport to from warehouses? Could make a rudimentary shipping dispatcher. --- TODO: Add save/load capability of warehouse <==> percistance after mission restart. Difficult in lua! +-- DONE: Test capturing a neutral warehouse. +-- DONE: Add save/load capability of warehouse <==> percistance after mission restart. Difficult in lua! -- DONE: Get cargo bay and weight from CARGO_GROUP and GROUP. No necessary any more! -- DONE: Add possibility to set weight and cargo bay manually in AddAsset function as optional parameters. -- DONE: Check overlapping aircraft sometimes. @@ -1862,7 +1863,7 @@ function WAREHOUSE:New(warehouse, alias) self:AddTransition("*", "Stop", "Stopped") -- Stop the warehouse. self:AddTransition("Stopped", "Restart", "Running") -- Restart the warehouse when it was stopped before. self:AddTransition("Loaded", "Restart", "Running") -- Restart the warehouse when assets were loaded from file before. - self:AddTransition("*", "Save", "*") -- TODO Save the warehouse state to disk. + self:AddTransition("*", "Save", "*") -- Save the warehouse state to disk. self:AddTransition("*", "Attacked", "Attacked") -- Warehouse is under attack by enemy coalition. self:AddTransition("Attacked", "Defeated", "Running") -- Attack by other coalition was defeated! self:AddTransition("*", "ChangeCountry", "*") -- Change country (and coalition) of the warehouse. Warehouse is respawned! @@ -3530,12 +3531,12 @@ function WAREHOUSE:onafterAddAsset(From, Event, To, group, ngroups, forceattribu else self:T(warehouse.wid..string.format("WARNING: Group %s is neither cargo nor transport!", group:GetName())) end - - end - -- If no assignment was given we take the assignment of the request if there is any. - if assignment==nil and request.assignment~=nil then - assignment=request.assignment + -- If no assignment was given we take the assignment of the request if there is any. + if assignment==nil and request.assignment~=nil then + assignment=request.assignment + end + end end diff --git a/Moose Development/Moose/Wrapper/Controllable.lua b/Moose Development/Moose/Wrapper/Controllable.lua index 337d133af..e38cf9d33 100644 --- a/Moose Development/Moose/Wrapper/Controllable.lua +++ b/Moose Development/Moose/Wrapper/Controllable.lua @@ -880,8 +880,10 @@ end function CONTROLLABLE:TaskOrbit(Coord, Altitude, Speed, CoordRaceTrack) local Pattern=AI.Task.OrbitPattern.CIRCLE + local P2=nil if CoordRaceTrack then Pattern=AI.Task.OrbitPattern.RACE_TRACK + P2=CoordRaceTrack:GetVec2() end local Task = { @@ -889,7 +891,7 @@ function CONTROLLABLE:TaskOrbit(Coord, Altitude, Speed, CoordRaceTrack) params = { pattern = Pattern, point = Coord:GetVec2(), - point2 = CoordRaceTrack:GetVec2(), + point2 = P2, speed = Speed, altitude = Altitude, } From b72ea2d02c6057765becbcb3b23aad484bf81d2e Mon Sep 17 00:00:00 2001 From: Frank Date: Fri, 26 Oct 2018 00:12:21 +0200 Subject: [PATCH 11/15] minor --- Moose Development/Moose/Functional/Warehouse.lua | 1 + Moose Development/Moose/Wrapper/Controllable.lua | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/Moose Development/Moose/Functional/Warehouse.lua b/Moose Development/Moose/Functional/Warehouse.lua index 1536fca49..dc3834638 100644 --- a/Moose Development/Moose/Functional/Warehouse.lua +++ b/Moose Development/Moose/Functional/Warehouse.lua @@ -3589,6 +3589,7 @@ function WAREHOUSE:onafterAddAsset(From, Event, To, group, ngroups, forceattribu else self:E(self.wid.."ERROR: Unknown group added as asset!") + self:E({unknowngroup=group}) end -- Update status. diff --git a/Moose Development/Moose/Wrapper/Controllable.lua b/Moose Development/Moose/Wrapper/Controllable.lua index e38cf9d33..ede63f9e5 100644 --- a/Moose Development/Moose/Wrapper/Controllable.lua +++ b/Moose Development/Moose/Wrapper/Controllable.lua @@ -880,6 +880,8 @@ end function CONTROLLABLE:TaskOrbit(Coord, Altitude, Speed, CoordRaceTrack) local Pattern=AI.Task.OrbitPattern.CIRCLE + + local P1=Coord:GetVec2() local P2=nil if CoordRaceTrack then Pattern=AI.Task.OrbitPattern.RACE_TRACK @@ -890,7 +892,7 @@ function CONTROLLABLE:TaskOrbit(Coord, Altitude, Speed, CoordRaceTrack) id = 'Orbit', params = { pattern = Pattern, - point = Coord:GetVec2(), + point = P1, point2 = P2, speed = Speed, altitude = Altitude, From 1f97495fdde5abb658cf876e0a4ad40e0731e359 Mon Sep 17 00:00:00 2001 From: funkyfranky Date: Fri, 26 Oct 2018 16:04:02 +0200 Subject: [PATCH 12/15] CT,RAT,WH work --- .../Moose/Functional/CarrierTrainer.lua | 116 ++++++++++-------- Moose Development/Moose/Functional/RAT.lua | 23 ++-- .../Moose/Functional/Warehouse.lua | 45 ++++--- 3 files changed, 103 insertions(+), 81 deletions(-) diff --git a/Moose Development/Moose/Functional/CarrierTrainer.lua b/Moose Development/Moose/Functional/CarrierTrainer.lua index c0dcad809..f75fc9b7f 100644 --- a/Moose Development/Moose/Functional/CarrierTrainer.lua +++ b/Moose Development/Moose/Functional/CarrierTrainer.lua @@ -115,19 +115,20 @@ CARRIERTRAINER.Difficulty={ --- 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 callsign Callsign of player. +-- @field #number score Player score of the current pass. +-- @field #number passes Number of passes. +-- @field #table debrief Debrief analysis of the current step of this pass. +-- @field #table results Results of all passes. -- @field #string summary Result summary text. -- @field Wrapper.Client#CLIENT client object of player. -- @field #string difficulty Difficulty level. -- @field #boolean inbigzone If true, player is in the big zone. +-- @field #boolean landed If true, player landed or attempted to land. +-- @field #boolean boltered If true, player boltered. +-- @field #boolean calledball If true, player called the ball. +-- @field #number Tlso Last time the LSO gave an advice. --- Checkpoint parameters triggering the next step in the pattern. -- @type CARRIERTRAINER.Checkpoint @@ -152,7 +153,7 @@ CARRIERTRAINER.MenuF10={} --- Carrier trainer class version. -- @field #string version -CARRIERTRAINER.version="0.1.0w" +CARRIERTRAINER.version="0.1.1w" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Constructor @@ -374,18 +375,26 @@ function CARRIERTRAINER:_InitNewPlayer(unitname) local playerData={} --#CARRIERTRAINER.PlayerData + -- Player unit, client and callsign. 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) + playerData.totalscore = 0 + + -- Number of passes done by player. + playerData.passes=0 + + playerData.results={} + + -- Set difficulty level. playerData.difficulty=CARRIERTRAINER.Difficulty.NORMAL + -- Player is in the big zone around the carrier. playerData.inbigzone=playerData.unit:IsInZone(self.giantZone) + + -- Init stuff for this round. + playerData=self:_InitNewRound(playerData) return playerData end @@ -395,22 +404,26 @@ end -- @param #CARRIERTRAINER.PlayerData playerData Player data. -- @return #CARRIERTRAINER.PlayerData Initialized player data. function CARRIERTRAINER:_InitNewRound(playerData) - playerData.step = 0 - playerData.score = 0 + playerData.step=0 + playerData.score=100 + playerData.grade={} playerData.summary = "Debriefing:\n" playerData.longDownwindDone = false - playerData.highestCarrierXDiff = -9999999 - playerData.secondsStandingStill = 0 - playerData.lowestAltitude = 999999 + playerData.boltered=false + playerData.landed=false + playerData.calledball=false + playerData.Tlso=timer.getTime() return playerData end ---- Append text to summary text. +--- Append text to debrief 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" +-- @param #string step Current step in the pattern. +-- @param #string item Text item appeded to the debrief. +function CARRIERTRAINER:_AddToSummary(playerData, step, item) + --playerData.summary = playerData.summary .. item .. "\n" + table.inser(playerData.debrief, {step=step, hint=item}) end --- Append text to result text. @@ -492,7 +505,9 @@ function CARRIERTRAINER:_CheckPlayerStatus() elseif playerData.step == 8 then self:_Groove(playerData) elseif playerData.step == 9 then - self:_Trap(playerData) + self:_CallTheBall(playerData) + elseif playerData.step == 99 then + self:_Debrief(playerData) end else @@ -866,7 +881,7 @@ function CARRIERTRAINER:_NewRound(playerData) self:_InitNewRound(playerData) -- Next step: start of pattern. - playerData.step = 1 + playerData.step=1 end end @@ -881,8 +896,7 @@ function CARRIERTRAINER:_Start(playerData) self:_SendMessageToPlayer(hint, 8, playerData) -- Next step: upwind. - playerData.step = 2 - + playerData.step=2 end end @@ -909,12 +923,14 @@ function CARRIERTRAINER:_Upwind(playerData) -- Get altitude. local hint=self:_AltitudeCheck(playerData, self.Upwind, altitude) + -- Message to player self:_SendMessageToPlayer(hint, 8, playerData) + -- Debrief. self:_AddToSummary(playerData, hint) -- Next step. - playerData.step = 3 + playerData.step=3 end end @@ -952,7 +968,7 @@ function CARRIERTRAINER:_Break(playerData, part) -- Send message to player. self:_SendMessageToPlayer(hint, 10, playerData) - -- Add hint to summary. + -- Debrif self:_AddToSummary(playerData, hint) -- Nest step: late break or abeam. @@ -986,7 +1002,7 @@ function CARRIERTRAINER:_CheckForLongDownwind(playerData) local hint = "Your downwind leg is too long. Turn to final earlier next time." self:_SendMessageToPlayer(hint, 10, playerData) - -- Add to debrief. + -- Debrief. self:_AddToSummary(playerData, hint) -- Decrease score. @@ -1042,7 +1058,7 @@ function CARRIERTRAINER:_Abeam(playerData) self:_SendMessageToPlayer(hintFull, 10, playerData) -- Add to debrief. - self:_AddToSummary(playerData, hintFull) + self:_AddToSummary(playerData, "Abeam", hintFull) -- Proceed to next step. playerData.step = 6 @@ -1085,7 +1101,7 @@ function CARRIERTRAINER:_Ninety(playerData) self:_SendMessageToPlayer(hintFull, 10, playerData) -- Add to debrief. - self:_AddToSummary(playerData, hintFull) + self:_AddToSummary(playerData, "At the 90:", hintFull) -- Long downwind not an issue any more playerData.longDownwindDone = true @@ -1128,14 +1144,14 @@ function CARRIERTRAINER:_Wake(playerData) self:_SendMessageToPlayer(hintFull, 10, playerData) -- Add to debrief. - self:_AddToSummary(playerData, hintFull) + self:_AddToSummary(playerData, "At the wake:", hintFull) -- Next step: Groove. playerData.step = 8 end end ---- Groove. +--- Entering the Groove. -- @param #CARRIERTRAINER self -- @param #CARRIERTRAINER.PlayerData playerData Player data table. function CARRIERTRAINER:_Groove(playerData) @@ -1179,7 +1195,7 @@ function CARRIERTRAINER:_Groove(playerData) self:_SendMessageToPlayer(hintFull, 10, playerData) -- Add to debrief. - self:_AddToSummary(playerData, hintFull) + self:_AddToSummary(playerData, "Entering the Groove:", hintFull) -- Next step. playerData.step = 9 @@ -1187,10 +1203,10 @@ function CARRIERTRAINER:_Groove(playerData) end ---- Trap. +--- Call the ball, i.e. 3/4 NM distance between aircraft and carrier. -- @param #CARRIERTRAINER self -- @param #CARRIERTRAINER.PlayerData playerData Player data table. -function CARRIERTRAINER:_Trap(playerData) +function CARRIERTRAINER:_CallTheBall(playerData) -- Get distances between carrier and player unit (parallel and perpendicular to direction of movement of carrier) local diffX, diffZ, rho, phi = self:_GetDistances(playerData.unit) @@ -1208,23 +1224,21 @@ function CARRIERTRAINER:_Trap(playerData) return end - if (diffX > playerData.highestCarrierXDiff) then - playerData.highestCarrierXDiff = diffX - end - - if (alt < playerData.lowestAltitude) then - playerData.lowestAltitude = alt - end - - -- Lineup. + -- Lineup. We need to correct for the end of the carrier deck and the tilted angle of the runway. + -- TODO: make this parameter of the carrier. local lineup = math.asin(diffZ/(-(diffX-100))) local lineuperror = math.deg(lineup)-10 - -- Glideslope. + -- Glideslope. Wee need to correct for the height of the deck. The ideal glide slope is 3.5 degrees. + -- TODO: make this parameter of the carrier. local glideslope = math.atan((playerData.unit:GetAltitude()-22)/(-diffX)) - local glideslopeError = math.deg(glideslope) - 3.5 + local glideslopeError = math.deg(glideslope) - 3.5 - if diffX<100 then + if diffX>-UTILS.NMToMeters(0.75) and diffX<-100 and playerData.calledball==false then + + + -- Check if we are beween 3/4 NM and end of ship. + if diffX>-UTILS.NMToMeters(0.75) and diffX<-100 then local text="Good height." if glideslopeError>1 then @@ -1259,12 +1273,12 @@ function CARRIERTRAINER:_Trap(playerData) elseif lineuperror <3 then text=text.."Right for lineup!" elseif lineuperror <1 then - text=text.."Right for lineup.." + text=text.."Right for lineup..." else text=text.."Good on lineup." end - self:_SendMessageToPlayer(text, 8,playerData) + self:_SendMessageToPlayer(text, 8, playerData) elseif (diffX > 150) then diff --git a/Moose Development/Moose/Functional/RAT.lua b/Moose Development/Moose/Functional/RAT.lua index 5b1616c81..9a1c9306b 100644 --- a/Moose Development/Moose/Functional/RAT.lua +++ b/Moose Development/Moose/Functional/RAT.lua @@ -5435,7 +5435,7 @@ function RAT:_ATCInit(airports_map) if not RAT.ATC.init then local text text="Starting RAT ATC.\nSimultanious = "..RAT.ATC.Nclearance.."\n".."Delay = "..RAT.ATC.delay - self:T(RAT.id..text) + BASE:T(RAT.id..text) RAT.ATC.init=true for _,ap in pairs(airports_map) do local name=ap:GetName() @@ -5458,7 +5458,7 @@ end -- @param #string name Group name of the flight. -- @param #string dest Name of the destination airport. function RAT:_ATCAddFlight(name, dest) - self:T(string.format("%sATC %s: Adding flight %s with destination %s.", RAT.id, dest, name, dest)) + BASE:T(string.format("%sATC %s: Adding flight %s with destination %s.", RAT.id, dest, name, dest)) RAT.ATC.flight[name]={} RAT.ATC.flight[name].destination=dest RAT.ATC.flight[name].Tarrive=-1 @@ -5483,7 +5483,7 @@ end -- @param #string name Group name of the flight. -- @param #number time Time the fight first registered. function RAT:_ATCRegisterFlight(name, time) - self:T(RAT.id.."Flight ".. name.." registered at ATC for landing clearance.") + BASE:T(RAT.id.."Flight ".. name.." registered at ATC for landing clearance.") RAT.ATC.flight[name].Tarrive=time RAT.ATC.flight[name].holding=0 end @@ -5514,7 +5514,7 @@ function RAT:_ATCStatus() -- Aircraft is holding. local text=string.format("ATC %s: Flight %s is holding for %i:%02d. %s.", dest, name, hold/60, hold%60, busy) - self:T(RAT.id..text) + BASE:T(RAT.id..text) elseif hold==RAT.ATC.onfinal then @@ -5522,7 +5522,7 @@ function RAT:_ATCStatus() local Tfinal=Tnow-RAT.ATC.flight[name].Tonfinal local text=string.format("ATC %s: Flight %s is on final. Waiting %i:%02d for landing event.", dest, name, Tfinal/60, Tfinal%60) - self:T(RAT.id..text) + BASE:T(RAT.id..text) elseif hold==RAT.ATC.unregistered then @@ -5530,7 +5530,7 @@ function RAT:_ATCStatus() --self:T(string.format("ATC %s: Flight %s is not registered yet (hold %d).", dest, name, hold)) else - self:E(RAT.id.."ERROR: Unknown holding time in RAT:_ATCStatus().") + BASE:E(RAT.id.."ERROR: Unknown holding time in RAT:_ATCStatus().") end end @@ -5572,12 +5572,12 @@ function RAT:_ATCCheck() -- Debug message. local text=string.format("ATC %s: Flight %s runway is busy. You are #%d of %d in landing queue. Your holding time is %i:%02d.", name, flight,qID, nqueue, RAT.ATC.flight[flight].holding/60, RAT.ATC.flight[flight].holding%60) - self:T(RAT.id..text) + BASE:T(RAT.id..text) else local text=string.format("ATC %s: Flight %s was cleared for landing. Your holding time was %i:%02d.", name, flight, RAT.ATC.flight[flight].holding/60, RAT.ATC.flight[flight].holding%60) - self:T(RAT.id..text) + BASE:T(RAT.id..text) -- Clear flight for landing. RAT:_ATCClearForLanding(name, flight) @@ -5705,12 +5705,7 @@ function RAT:_ATCQueue() for k,v in ipairs(_queue) do table.insert(RAT.ATC.airport[airport].queue, v[1]) end - - --fvh - --for k,v in ipairs(RAT.ATC.airport[airport].queue) do - --print(string.format("queue #%02i flight \"%s\" holding %d seconds",k, v, RAT.ATC.flight[v].holding)) - --end - + end end diff --git a/Moose Development/Moose/Functional/Warehouse.lua b/Moose Development/Moose/Functional/Warehouse.lua index dc3834638..bc1e840ae 100644 --- a/Moose Development/Moose/Functional/Warehouse.lua +++ b/Moose Development/Moose/Functional/Warehouse.lua @@ -69,6 +69,7 @@ -- @field #boolean autosave Automatically save assets to file when mission ends. -- @field #string autosavepath Path where the asset file is saved on auto save. -- @field #string autosavefilename File name of the auto asset save file. Default is auto generated from warehouse id and name. +-- @field #boolean safeparking If true, parking spots for aircraft are considered as occupied if e.g. a client aircraft is parked there. Default false. -- @extends Core.Fsm#FSM --- Have your assets at the right place at the right time - or not! @@ -1556,6 +1557,7 @@ WAREHOUSE = { autosave = false, autosavepath = nil, autosavefile = nil, + saveparking = false, } --- Item of the warehouse stock table. @@ -2364,6 +2366,24 @@ function WAREHOUSE:SetReportOff() return self end +--- Enable safe parking option, i.e. parking spots at an airbase will be considered as occupied when a client aircraft is parked there (even if the client slot is not taken by a player yet). +-- Note that also incoming aircraft can reserve/occupie parking spaces. +-- @param #WAREHOUSE self +-- @return #WAREHOUSE self +function WAREHOUSE:SetSafeParkingOn() + self.safeparking=true + return self +end + +--- Disable safe parking option. Note that is the default setting. +-- @param #WAREHOUSE self +-- @return #WAREHOUSE self +function WAREHOUSE:SetSafeParkingOff() + self.safeparking=false + return self +end + + --- Set interval of status updates. Note that normally only one request can be processed per time interval. -- @param #WAREHOUSE self -- @param #number timeinterval Time interval in seconds. @@ -7004,20 +7024,6 @@ function WAREHOUSE:_FindParkingForAssets(airbase, assets) table.insert(obstacles,{coord=_coord, size=_size, name=_name, type="scenery"}) end - --[[ - -- TODO Clients? Unoccupied client aircraft are also important! Are they already included in scanned units maybe? - local clients=_DATABASE.CLIENTS - for _,_client in pairs(clients) do - local client=_client --Wrapper.Client#CLIENT - env.info(string.format("FF Client name %s", client:GetName())) - local unit=UNIT:FindByName(client:GetName()) - --local unit=client:GetClientGroupUnit() - local _coord=unit:GetCoordinate() - local _name=unit:GetName() - local _size=self:_GetObjectSize(client:GetClientGroupDCSUnit()) - table.insert(obstacles,{coord=_coord, size=_size, name=_name, type="client"}) - end - ]] end -- Parking data for all assets. @@ -7050,10 +7056,17 @@ function WAREHOUSE:_FindParkingForAssets(airbase, assets) local _toac=parkingspot.TOAC --env.info(string.format("FF asset=%s (id=%d): needs terminal type=%d, id=%d, #obstacles=%d", _asset.templatename, _asset.uid, terminaltype, _termid, #obstacles)) - - -- Loop over all obstacles. + local free=true local problem=nil + + -- Safe parking using TO_AC from DCS result. + if self.safeparking and _toac then + free=false + self:T("Parking spot %d is occupied by other aircraft taking off or landing.", _termid) + end + + -- Loop over all obstacles. for _,obstacle in pairs(obstacles) do -- Check if aircraft overlaps with any obstacle. From 0c36e4e40d12bab6f0c5f80d61759ddf5b631a17 Mon Sep 17 00:00:00 2001 From: Frank Date: Fri, 26 Oct 2018 18:55:36 +0200 Subject: [PATCH 13/15] CT --- Moose Development/Moose/Functional/CarrierTrainer.lua | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/Moose Development/Moose/Functional/CarrierTrainer.lua b/Moose Development/Moose/Functional/CarrierTrainer.lua index c0dcad809..dbc23fad9 100644 --- a/Moose Development/Moose/Functional/CarrierTrainer.lua +++ b/Moose Development/Moose/Functional/CarrierTrainer.lua @@ -1411,6 +1411,14 @@ function CARRIERTRAINER:_AddF10Commands(_unitName) end +--- Set difficulty level. +-- @param #CARRIERTRAINER self +-- @param #CARRIERTRAINER.PlayerData playerData Player data. +-- @param #CARRIERTRAINER.Difficulty difficulty Difficulty level. +function CARRIERTRAINER:SetDifficulty(playerData, difficulty) + playerData.difficulty=difficulty +end + --- Report information about carrier. -- @param #CARRIERTRAINER self -- @param #string _unitname Name of the player unit. @@ -1449,6 +1457,8 @@ function CARRIERTRAINER:_DisplayCarrierInfo(_unitname) text=text..string.format("TACAN Channel %s", tacan) text=text..string.format("ICLS Channel %s", icls) + + self:_SendMessageToPlayer(text, 20, playerData) end From 1a4baeafb691ed36f90356ac46da8108b70ce74c Mon Sep 17 00:00:00 2001 From: Frank Date: Sun, 28 Oct 2018 13:15:38 +0100 Subject: [PATCH 14/15] Warehose v0.6.6 WAREHOUSE: - Improved parking spot check for airbase ship. AI_FORMATION: - Added Stop option CT: - fixes COORDINATE: - Improved landing waypoint. --- Moose Development/Moose/AI/AI_Formation.lua | 12 ++++- Moose Development/Moose/Core/Point.lua | 7 ++- .../Moose/Functional/CarrierTrainer.lua | 51 +++++++++++-------- .../Moose/Functional/Warehouse.lua | 35 ++++++++----- .../Moose/Wrapper/Controllable.lua | 6 +-- 5 files changed, 71 insertions(+), 40 deletions(-) diff --git a/Moose Development/Moose/AI/AI_Formation.lua b/Moose Development/Moose/AI/AI_Formation.lua index cab3c528a..b086e7b70 100644 --- a/Moose Development/Moose/AI/AI_Formation.lua +++ b/Moose Development/Moose/AI/AI_Formation.lua @@ -911,7 +911,17 @@ end -- @param #string From From state. -- @param #string Event Event. -- @pram #string To The to state. -function AI_FORMATION:onbeforeFollowing( FollowGroupSet, From, Event, To ) --R2.1 +function AI_FORMATION:onafterStop(FollowGroupSet, From, Event, To) --R2.1 + self:E("Stopping formation.") +end + +--- Follow event fuction. Check if coming from state "stopped". If so the transition is rejected. +-- @param #AI_FORMATION self +-- @param Core.Set#SET_GROUP FollowGroupSet The following set of groups. +-- @param #string From From state. +-- @param #string Event Event. +-- @pram #string To The to state. +function AI_FORMATION:onbeforeFollow( FollowGroupSet, From, Event, To ) --R2.1 if From=="Stopped" then return false -- Deny transition. end diff --git a/Moose Development/Moose/Core/Point.lua b/Moose Development/Moose/Core/Point.lua index 528ac9a84..862dc3839 100644 --- a/Moose Development/Moose/Core/Point.lua +++ b/Moose Development/Moose/Core/Point.lua @@ -1121,6 +1121,9 @@ do -- COORDINATE --- Build a Waypoint Air "Landing". -- @param #COORDINATE self -- @param DCS#Speed Speed Airspeed in km/h. + -- @param Wrapper.Airbase#AIRBASE airbase The airbase for takeoff and landing points. + -- @param #table DCSTasks A table of @{DCS#Task} items which are executed at the waypoint. + -- @param #string description A text description of the waypoint, which will be shown on the F10 map. -- @return #table The route point. -- @usage -- @@ -1129,8 +1132,8 @@ do -- COORDINATE -- LandingWaypoint = LandingCoord:WaypointAirLanding( 60 ) -- HeliGroup:Route( { LandWaypoint }, 1 ) -- Start landing the helicopter in one second. -- - function COORDINATE:WaypointAirLanding( Speed ) - return self:WaypointAir( nil, COORDINATE.WaypointType.Land, COORDINATE.WaypointAction.Landing, Speed ) + function COORDINATE:WaypointAirLanding( Speed, airbase, DCSTasks, description ) + return self:WaypointAir( nil, COORDINATE.WaypointType.Land, COORDINATE.WaypointAction.Landing, Speed, airbase, DCSTasks, description ) end diff --git a/Moose Development/Moose/Functional/CarrierTrainer.lua b/Moose Development/Moose/Functional/CarrierTrainer.lua index 4e00ea75d..c3f4f68e0 100644 --- a/Moose Development/Moose/Functional/CarrierTrainer.lua +++ b/Moose Development/Moose/Functional/CarrierTrainer.lua @@ -153,7 +153,7 @@ CARRIERTRAINER.MenuF10={} --- Carrier trainer class version. -- @field #string version -CARRIERTRAINER.version="0.1.1w" +CARRIERTRAINER.version="0.1.1" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Constructor @@ -407,6 +407,7 @@ function CARRIERTRAINER:_InitNewRound(playerData) playerData.step=0 playerData.score=100 playerData.grade={} + playerData.debrief={} playerData.summary = "Debriefing:\n" playerData.longDownwindDone = false playerData.boltered=false @@ -423,7 +424,7 @@ end -- @param #string item Text item appeded to the debrief. function CARRIERTRAINER:_AddToSummary(playerData, step, item) --playerData.summary = playerData.summary .. item .. "\n" - table.inser(playerData.debrief, {step=step, hint=item}) + table.insert(playerData.debrief, {step=step, hint=item}) end --- Append text to result text. @@ -671,7 +672,7 @@ function CARRIERTRAINER:_AbortPattern(playerData, X, Z, posData) self:E(self.lid..text) --MESSAGE:New(text, 60):ToAllIf(self.Debug) - self:_AddToSummary(playerData, "Approach aborted.") + self:_AddToSummary(playerData, "Abort", "Approach aborted.") self:_PrintFinalScore(playerData, 30, -2) @@ -805,8 +806,8 @@ function CARRIERTRAINER:_InitStennis() self.Wake.Zmax=nil self.Wake.LimitXmin=nil self.Wake.LimitXmax=nil - self.Wake.LimitZmin=nil - self.Wake.LimitZmax=0 + self.Wake.LimitZmin=0 + self.Wake.LimitZmax=nil self.Wake.Altitude=UTILS.FeetToMeters(370) self.Wake.AoA=8.1 self.Wake.Distance=nil @@ -927,7 +928,7 @@ function CARRIERTRAINER:_Upwind(playerData) self:_SendMessageToPlayer(hint, 8, playerData) -- Debrief. - self:_AddToSummary(playerData, hint) + self:_AddToSummary(playerData, "Entering the Break", hint) -- Next step. playerData.step=3 @@ -968,8 +969,12 @@ function CARRIERTRAINER:_Break(playerData, part) -- Send message to player. self:_SendMessageToPlayer(hint, 10, playerData) - -- Debrif - self:_AddToSummary(playerData, hint) + -- Debrief + if part =="late" then + self:_AddToSummary(playerData, "Late Break", hint) + else + self:_AddToSummary(playerData, "Early Entry", hint) + end -- Nest step: late break or abeam. if (part == "early") then @@ -1003,7 +1008,7 @@ function CARRIERTRAINER:_CheckForLongDownwind(playerData) self:_SendMessageToPlayer(hint, 10, playerData) -- Debrief. - self:_AddToSummary(playerData, hint) + self:_AddToSummary(playerData, "Long Downwind Leg", hint) -- Decrease score. playerData.score=playerData.score-40 @@ -1058,7 +1063,7 @@ function CARRIERTRAINER:_Abeam(playerData) self:_SendMessageToPlayer(hintFull, 10, playerData) -- Add to debrief. - self:_AddToSummary(playerData, "Abeam", hintFull) + self:_AddToSummary(playerData, "Abeam Position", hintFull) -- Proceed to next step. playerData.step = 6 @@ -1101,7 +1106,7 @@ function CARRIERTRAINER:_Ninety(playerData) self:_SendMessageToPlayer(hintFull, 10, playerData) -- Add to debrief. - self:_AddToSummary(playerData, "At the 90:", hintFull) + self:_AddToSummary(playerData, "At the 90", hintFull) -- Long downwind not an issue any more playerData.longDownwindDone = true @@ -1144,7 +1149,7 @@ function CARRIERTRAINER:_Wake(playerData) self:_SendMessageToPlayer(hintFull, 10, playerData) -- Add to debrief. - self:_AddToSummary(playerData, "At the wake:", hintFull) + self:_AddToSummary(playerData, "At the Wake", hintFull) -- Next step: Groove. playerData.step = 8 @@ -1195,7 +1200,7 @@ function CARRIERTRAINER:_Groove(playerData) self:_SendMessageToPlayer(hintFull, 10, playerData) -- Add to debrief. - self:_AddToSummary(playerData, "Entering the Groove:", hintFull) + self:_AddToSummary(playerData, "Entering the Groove", hintFull) -- Next step. playerData.step = 9 @@ -1235,7 +1240,10 @@ function CARRIERTRAINER:_CallTheBall(playerData) local glideslopeError = math.deg(glideslope) - 3.5 if diffX>-UTILS.NMToMeters(0.75) and diffX<-100 and playerData.calledball==false then - + self:_SendMessageToPlayer("Call the ball.", 8, playerData) + playerData.calledball=true + return + end -- Check if we are beween 3/4 NM and end of ship. if diffX>-UTILS.NMToMeters(0.75) and diffX<-100 then @@ -1296,7 +1304,7 @@ function CARRIERTRAINER:_CallTheBall(playerData) self:_SendMessageToPlayer( hint, 8, playerData ) self:_PrintScore(score, playerData, true) - self:_AddToSummary(playerData, hint) + self:_AddToSummary(playerData, "Calling the Ball", hint) self:_PrintFinalScore(playerData, 60, wire) self:_HandleCollectedResult(playerData, wire) @@ -1348,7 +1356,7 @@ function CARRIERTRAINER:_Trapped(playerData, pos) local fullHint = string.format("Trapped catching the %d-wire.", wire) - self:_AddToSummary(playerData, fullHint) + self:_AddToSummary(playerData, "Trapped", fullHint) else --Boltered! @@ -1766,16 +1774,19 @@ function CARRIERTRAINER:_HandleCollectedResult(playerData, wire) playerData.totalscore = playerData.totalscore + playerData.score playerData.passes = playerData.passes + 1 + --TODO: collect results + --[[ 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)) + ]] + + local heading=playerData.unit:GetCoordinate():HeadingTo(self.registerZone:GetCoordinate()) + local distance=playerData.unit:GetCoordinate():Get2DDistance(self.registerZone: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 diff --git a/Moose Development/Moose/Functional/Warehouse.lua b/Moose Development/Moose/Functional/Warehouse.lua index bc1e840ae..2802ecc52 100644 --- a/Moose Development/Moose/Functional/Warehouse.lua +++ b/Moose Development/Moose/Functional/Warehouse.lua @@ -1729,7 +1729,7 @@ WAREHOUSE.db = { --- Warehouse class version. -- @field #string version -WAREHOUSE.version="0.6.5" +WAREHOUSE.version="0.6.6" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO: Warehouse todo list. @@ -6318,25 +6318,26 @@ function WAREHOUSE:_CheckRequestValid(request) -- TODO: maybe only check if spots > 0 for the necessary terminal type? At least for FARPS. -- Get necessary terminal type. - local termtype=self:_GetTerminal(asset.attribute) + local termtype_dep=self:_GetTerminal(asset.attribute, self:GetAirbaseCategory()) + local termtype_des=self:_GetTerminal(asset.attribute, request.warehouse:GetAirbaseCategory()) -- Get number of parking spots. - local np_departure=self.airbase:GetParkingSpotsNumber(termtype) - local np_destination=request.airbase:GetParkingSpotsNumber(termtype) + local np_departure=self.airbase:GetParkingSpotsNumber(termtype_dep) + local np_destination=request.airbase:GetParkingSpotsNumber(termtype_des) -- Debug info. - self:T(string.format("Asset attribute = %s, terminal type = %d, spots at departure = %d, destination = %d", asset.attribute, termtype, np_departure, np_destination)) + self:T(string.format("Asset attribute = %s, DEPARTURE: terminal type = %d, spots = %d, DESTINATION: terminal type = %d, spots = %d", asset.attribute, termtype_dep, np_departure, termtype_des, np_destination)) -- Not enough parking at sending warehouse. --if (np_departure < request.nasset) and not (self.category==Airbase.Category.SHIP or self.category==Airbase.Category.HELIPAD) then if np_departure < nasset then - self:E(string.format("ERROR: Incorrect request. Not enough parking spots of terminal type %d at warehouse. Available spots %d < %d necessary.", termtype, np_departure, nasset)) + self:E(string.format("ERROR: Incorrect request. Not enough parking spots of terminal type %d at warehouse. Available spots %d < %d necessary.", termtype_dep, np_departure, nasset)) valid=false end -- No parking at requesting warehouse. if np_destination == 0 then - self:E(string.format("ERROR: Incorrect request. No parking spots of terminal type %d at requesting warehouse. Available spots = %d!", termtype, np_destination)) + self:E(string.format("ERROR: Incorrect request. No parking spots of terminal type %d at requesting warehouse. Available spots = %d!", termtype_des, np_destination)) valid=false end @@ -6474,7 +6475,7 @@ function WAREHOUSE:_CheckRequestValid(request) self:T(text) -- Get necessary terminal type for helos or transport aircraft. - local termtype=self:_GetTerminal(request.transporttype) + local termtype=self:_GetTerminal(request.transporttype, self:GetAirbaseCategory()) -- Get number of parking spots. local np_departure=self.airbase:GetParkingSpotsNumber(termtype) @@ -6493,6 +6494,7 @@ function WAREHOUSE:_CheckRequestValid(request) if request.transporttype==WAREHOUSE.TransportType.AIRPLANE then -- Total number of parking spots for transport planes at destination. + termtype=self:_GetTerminal(request.transporttype, request.warehouse:GetAirbaseCategory()) local np_destination=request.airbase:GetParkingSpotsNumber(termtype) -- Debug info. @@ -6934,13 +6936,13 @@ end --- Get the proper terminal type based on generalized attribute of the group. --@param #WAREHOUSE self --@param #WAREHOUSE.Attribute _attribute Generlized attibute of unit. +--@param #number _category Airbase category. --@return Wrapper.Airbase#AIRBASE.TerminalType Terminal type for this group. -function WAREHOUSE:_GetTerminal(_attribute) +function WAREHOUSE:_GetTerminal(_attribute, _category) -- Default terminal is "large". local _terminal=AIRBASE.TerminalType.OpenBig - - + if _attribute==WAREHOUSE.Attribute.AIR_FIGHTER then -- Fighter ==> small. _terminal=AIRBASE.TerminalType.FighterAircraft @@ -6950,6 +6952,15 @@ function WAREHOUSE:_GetTerminal(_attribute) elseif _attribute==WAREHOUSE.Attribute.AIR_TRANSPORTHELO or _attribute==WAREHOUSE.Attribute.AIR_ATTACKHELO then -- Helicopter. _terminal=AIRBASE.TerminalType.HelicopterUsable + else + --_terminal=AIRBASE.TerminalType.OpenMedOrBig + end + + -- For ships, we allow medium spots for all fixed wing aircraft. There are smaller tankers and AWACS aircraft that can use a carrier. + if _category==Airbase.Category.SHIP then + if not (_attribute==WAREHOUSE.Attribute.AIR_TRANSPORTHELO or _attribute==WAREHOUSE.Attribute.AIR_ATTACKHELO) then + _terminal=AIRBASE.TerminalType.OpenMedOrBig + end end return _terminal @@ -7034,7 +7045,7 @@ function WAREHOUSE:_FindParkingForAssets(airbase, assets) local _asset=asset --#WAREHOUSE.Assetitem -- Get terminal type of this asset - local terminaltype=self:_GetTerminal(asset.attribute) + local terminaltype=self:_GetTerminal(asset.attribute, self:GetAirbaseCategory()) -- Asset specific parking. parking[_asset.uid]={} diff --git a/Moose Development/Moose/Wrapper/Controllable.lua b/Moose Development/Moose/Wrapper/Controllable.lua index ede63f9e5..d57f7fac4 100644 --- a/Moose Development/Moose/Wrapper/Controllable.lua +++ b/Moose Development/Moose/Wrapper/Controllable.lua @@ -990,11 +990,7 @@ function CONTROLLABLE:TaskRefueling() -- params = {} -- } - local DCSTask - DCSTask = { id = 'Refueling', - params = { - }, - }, + local DCSTask={id='Refueling', params={}} self:T3( { DCSTask } ) return DCSTask From 023eae825df11e9b96024ffd6f308d647b17ead3 Mon Sep 17 00:00:00 2001 From: Frank Date: Sun, 28 Oct 2018 13:16:32 +0100 Subject: [PATCH 15/15] RCT --- .../Moose/Functional/CarrierTrainer.lua | 1849 ----------------- 1 file changed, 1849 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 c3f4f68e0..000000000 --- a/Moose Development/Moose/Functional/CarrierTrainer.lua +++ /dev/null @@ -1,1849 +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 #string alias Alias of the carrier trainer. --- @field Core.Zone#ZONE_UNIT startZone Zone in which the pattern approach starts. --- @field Core.Zone#ZONE_UNIT giantZone Large zone around the carrier to welcome players. --- @field Core.Zone#ZONE_UNIT registerZone Zone behind the carrier to register for a new approach. --- @field #table players Table of players. --- @field #table menuadded Table of units where the F10 radio menu was added. --- @field #CARRIERTRAINER.Checkpoint Upwind Upwind checkpoint. --- @field #CARRIERTRAINER.Checkpoint BreakEarly Early break checkpoint. --- @field #CARRIERTRAINER.Checkpoint BreakLate Late brak checkpoint. --- @field #CARRIERTRAINER.Checkpoint Abeam Abeam checkpoint. --- @field #CARRIERTRAINER.Checkpoint Ninety At the ninety checkpoint. --- @field #CARRIERTRAINER.Checkpoint Wake Right behind the carrier. --- @field #CARRIERTRAINER.Checkpoint Groove In the groove checkpoint. --- @field #CARRIERTRAINER.Checkpoint Trap Landing checkpoint. --- @field --- @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, - registerZone = nil, - startZone = nil, - giantZone = nil, - players = {}, - menuadded = {}, - Upwind = {}, - Abeam = {}, - BreakEarly = {}, - BreakLate = {}, - Ninety = {}, - Wake = {}, - Groove = {}, - Trap = {}, - TACAN = nil, - ICLS = nil, -} - ---- Aircraft types. --- @type CARRIERTRAINER.AircraftType --- @field #string AV8B AV-8B Night Harrier. --- @field #string HORNET F/A-18C Lot 20 Hornet. -CARRIERTRAINER.AircraftType={ - AV8B="AV8BNA", - HORNET="FA-18C_hornet", -} - ---- Carrier types. --- @type CARRIERTRAINER.CarrierType --- @field #string STENNIS USS John C. Stennis (CVN-74) --- @field #string VINSON USS Carl Vinson (CVN-70) --- @field #string TARAWA USS Tarawa (LHA-1) --- @field #string KUZNETSOV Admiral Kuznetsov (CV 1143.5) -CARRIERTRAINER.CarrierType={ - STENNIS="Stennis", - VINSON="Vinson", - TARAWA="LHA_Tarawa", - KUZNETSOV="KUZNECOW" -} - ---- Difficulty level. --- @type CARRIERTRAINER.Difficulty --- @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. -CARRIERTRAINER.Difficulty={ - EASY="Rookey", - NORMAL="Naval Aviator", - HARD="TOPGUN Graduate", -} - ---- Player data table holding all important parameters for each player. --- @type CARRIERTRAINER.PlayerData --- @field #number id Player ID. --- @field Wrapper.Unit#UNIT unit Aircraft unit of the player. --- @field #string callsign Callsign of player. --- @field #number score Player score of the current pass. --- @field #number passes Number of passes. --- @field #table debrief Debrief analysis of the current step of this pass. --- @field #table results Results of all passes. --- @field #string summary Result summary text. --- @field Wrapper.Client#CLIENT client object of player. --- @field #string difficulty Difficulty level. --- @field #boolean inbigzone If true, player is in the big zone. --- @field #boolean landed If true, player landed or attempted to land. --- @field #boolean boltered If true, player boltered. --- @field #boolean calledball If true, player called the ball. --- @field #number Tlso Last time the LSO gave an advice. - ---- Checkpoint parameters triggering the next step in the pattern. --- @type CARRIERTRAINER.Checkpoint --- @field #string name Name of 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 LimitXmin Latitudal threshold for triggering the next step if XXmax. --- @field #number LimitZmin Latitudal threshold for triggering the next step if ZZmax. --- @field #number Altitude Optimal altitude at this point. --- @field #number AoA Optimal AoA at this point. --- @field #number Distance Optimal distance at this point. --- @field #number Speed Optimal speed at this point. --- @field #table Checklist Table of checklist text items to display at this point. - ---- Main radio menu. --- @field #table MenuF10 -CARRIERTRAINER.MenuF10={} - ---- Carrier trainer class version. --- @field #string version -CARRIERTRAINER.version="0.1.1" - -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- --- 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.registerZone = ZONE_UNIT:New("registerZone", self.carrier, 2500, { dx = -5000, dy = 100, relative_to_unit = true }) - 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 - - if self.carriertype==CARRIERTRAINER.CarrierType.STENNIS then - self:_InitStennis() - elseif self.carriertype==CARRIERTRAINER.CarrierType.VINSON then - -- TODO: Carl Vinson parameters. - self:_InitStennis() - elseif self.carriertype==CARRIERTRAINER.CarrierType.TARAWA then - -- TODO: Tarawa parameters. - self:_InitStennis() - elseif self.carriertype==CARRIERTRAINER.CarrierType.KUZNETSOV then - -- TODO: Kusnetsov parameters - maybe... - self:_InitStennis() - else - self:E(self.lid.."ERROR: Unknown carrier type!") - return nil - end - - ----------------------- - --- 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) - self:HandleEvent(EVENTS.Land) - - -- 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) - self:UnHandleEvent(EVENTS.Land) -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 - - ---- Carrier trainer event handler for event land. --- @param #CARRIERTRAINER self --- @param Core.Event#EVENTDATA EventData -function CARRIERTRAINER:OnEventLand(EventData) - self:F3({eventland = EventData}) - - local _unitName=EventData.IniUnitName - local _unit, _playername=self:_GetPlayerUnitAndName(_unitName) - - self:T3(self.lid.."LAND: unit = "..tostring(EventData.IniUnitName)) - self:T3(self.lid.."LAND: group = "..tostring(EventData.IniGroupName)) - self:T3(self.lid.."LAND: 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 unit %s (ID=%d) of group %s landed.", _playername, _callsign, _unitName, _uid, _group:GetName()) - self:T(self.lid..text) - MESSAGE:New(text, 5):ToAllIf(self.Debug) - - -- Check if we caught a wire after one second. - -- TODO: test this! - local playerData=self.players[_playername] --#CARRIERTRAINER.PlayerData - local coord=playerData.unit:GetCoordinate() - - -- Call trapped function in 5 seconds to make sure we did not bolter. - SCHEDULER:New(nil, self._Trapped,{self, playerData, coord}, 5) - - 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 - - -- Player unit, client and callsign. - playerData.unit = UNIT:FindByName(unitname) - playerData.client = CLIENT:FindByName(playerData.unit.UnitName, nil, true) - playerData.callsign = playerData.unit:GetCallsign() - - playerData.totalscore = 0 - - -- Number of passes done by player. - playerData.passes=0 - - playerData.results={} - - -- Set difficulty level. - playerData.difficulty=CARRIERTRAINER.Difficulty.NORMAL - - -- Player is in the big zone around the carrier. - playerData.inbigzone=playerData.unit:IsInZone(self.giantZone) - - -- Init stuff for this round. - 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.step=0 - playerData.score=100 - playerData.grade={} - playerData.debrief={} - playerData.summary = "Debriefing:\n" - playerData.longDownwindDone = false - playerData.boltered=false - playerData.landed=false - playerData.calledball=false - playerData.Tlso=timer.getTime() - return playerData -end - ---- Append text to debrief text. --- @param #CARRIERTRAINER self --- @param #CARRIERTRAINER.PlayerData playerData Player data. --- @param #string step Current step in the pattern. --- @param #string item Text item appeded to the debrief. -function CARRIERTRAINER:_AddToSummary(playerData, step, item) - --playerData.summary = playerData.summary .. item .. "\n" - table.insert(playerData.debrief, {step=step, hint=item}) -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 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 - - -- Player unit. - local unit = playerData.unit - - if unit:IsAlive() then - - --self:_SendMessageToPlayer("current step "..self:_StepName(playerData.step),1,playerData) - --self:_DetailedPlayerStatus(playerData) - - --self:_DetailedPlayerStatus(playerData) - if unit:IsInZone(self.giantZone) then - - -- Check if player was previously not inside the zone. - if playerData.inbigzone==false then - - local text=string.format("Welcome back, %s! TCN 1X, BRC 354 (MAG HDG).\n", playerData.callsign) - local heading=playerData.unit:GetCoordinate():HeadingTo(self.registerZone:GetCoordinate()) - local distance=playerData.unit:GetCoordinate():Get2DDistance(self.registerZone:GetCoordinate()) - text=text..string.format("Fly heading %d for %.1f NM to begin your approach.", heading, distance) - MESSAGE:New(text, 5):ToClient(playerData.client) - - end - - if playerData.step==0 and unit:InAir() then - self:_NewRound(playerData) - elseif playerData.step == 1 then - self:_Start(playerData) - elseif playerData.step == 2 then - self:_Upwind(playerData) - elseif playerData.step == 3 then - self:_Break(playerData, "early") - elseif playerData.step == 4 then - self:_Break(playerData, "late") - elseif playerData.step == 5 then - self:_Abeam(playerData) - elseif playerData.step == 6 then - -- Check long down wind leg. - if not playerData.longDownwindDone then - self:_CheckForLongDownwind(playerData) - end - self:_Ninety(playerData) - elseif playerData.step == 7 then - self:_Wake(playerData) - elseif playerData.step == 8 then - self:_Groove(playerData) - elseif playerData.step == 9 then - self:_CallTheBall(playerData) - elseif playerData.step == 99 then - self:_Debrief(playerData) - end - - else - playerData.inbigzone=false - end - - else - -- Unit not alive. - --playerDatas[i] = nil - end - end - end - -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="when entering pattern" - elseif step==2 then - name="in the break entry" - elseif step==3 then - name="at the early break" - elseif step==4 then - name="at the late break" - elseif step==5 then - name="in the abeam position" - elseif step==6 then - name="at the ninety" - elseif step==7 then - name="at the wake" - elseif step==8 then - name="in the groove" - elseif step==9 then - name="trapped" - end - - return name -end - ---- Calculate distances between carrier and player unit. --- @param #CARRIERTRAINER self --- @param Wrapper.Unit#UNIT unit Player unit --- @return #number Distance [m] in the direction of the orientation of the carrier. --- @return #number Distance [m] perpendicular to the orientation of the carrier. --- @return #number Distance [m] to the carrier. --- @return #number Angle [Deg] from carrier to plane. Phi=0 if the plane is directly behind the carrier, phi=90 if the plane is starboard, phi=180 if the plane is in front 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) - - -- Polar coordinates - local rho=math.sqrt(dx*dx+dz*dz) - local phi=math.deg(math.atan2(dz,dx)) - if phi<0 then - phi=phi+360 - end - -- phi=0 if the plane is directly behind the carrier, phi=180 if the plane is in front of the carrier - phi=phi-180 - - return dx,dz,rho,phi -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 ) - - local text=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)) - self:E(self.lid..text) - --MESSAGE:New(text, 60):ToAllIf(self.Debug) - - self:_AddToSummary(playerData, "Abort", "Approach aborted.") - - self:_PrintFinalScore(playerData, 30, -2) - - self:_HandleCollectedResult(playerData, -2) - - playerData.step = 0 -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,rho,phi=self:_GetDistances(unit) - - -- Player and carrier position vector. - local playerPosition = playerData.unit:GetVec3() - local carrierPosition = self.carrier:GetVec3() - - local diffZ = playerPosition.z - carrierPosition.z - local diffX = playerPosition.x - carrierPosition.x - - local heading=unit:GetCoordinate():HeadingTo(self.startZone:GetCoordinate()) - - local wind=unit:GetCoordinate():GetWindWithTurbulenceVec3() - local velo=unit:GetVelocityVec3() - - local relhead=self:_GetRelativeHeading(playerData.unit) - - local text=string.format("%s, current AoA=%.1f\n", playerData.callsign, aoa) - text=text..string.format("velo x=%.1f y=%.1f z=%.1f\n", velo.x, velo.y, velo.z) - text=text..string.format("wind x=%.1f y=%.1f z=%.1f\n", wind.x, wind.y, wind.z) - text=text..string.format("pitch=%.1f | roll=%.1f | yaw=%.1f | climb=%.1f\n", pitch, roll, yaw, unit:GetClimbAnge()) - text=text..string.format("relheading=%.1f degrees\n", relhead) - text=text..string.format("rho=%.1f m phi=%.1f degrees\n", rho,phi) - --text=text..string.format("current step = %d %s\n", playerData.step, self:_StepName(playerData.step)) - --text=text..string.format("Carrier distance: d=%d m\n", dist) - --text=text..string.format("Carrier distance: x=%d m z=%d m sum=%d (old)\n", diffX, diffZ, math.abs(diffX)+math.abs(diffZ)) - --text=text..string.format("Carrier distance: x=%d m z=%d m sum=%d (new)", dx, dz, math.abs(dz)+math.abs(dx)) - - MESSAGE:New(text, 1, nil , true):ToClient(playerData.client) -end - ---- Init parameters for USS Stennis carrier. --- @param #CARRIERTRAINER self -function CARRIERTRAINER:_InitStennis() - - -- Upwind leg - self.Upwind.name="Upwind" - self.Upwind.Xmin=-4000 -- TODO Should be withing 4 km behind carrier. Why? - self.Upwind.Xmax=nil - self.Upwind.Zmin=0 - self.Upwind.Zmax=1200 - self.Upwind.LimitXmin=0 - self.Upwind.LimitXmax=nil - self.Upwind.LimitZmin=0 - self.Upwind.LimitZmax=nil - self.Upwind.Altitude=UTILS.FeetToMeters(800) - self.Upwind.AoA=8.1 - self.Upwind.Distance=nil - - -- Early break - self.BreakEarly.name="Early Break" - self.BreakEarly.Xmin=-500 - self.BreakEarly.Xmax=nil - self.BreakEarly.Zmin=-3700 - self.BreakEarly.Zmax=1500 - self.BreakEarly.LimitXmin=0 - self.BreakEarly.LimitXmax=nil - self.BreakEarly.LimitZmin=-370 -- 0.2 NM port of carrier - self.BreakEarly.LimitZmax=nil - self.BreakEarly.Altitude=UTILS.FeetToMeters(800) - self.BreakEarly.AoA=8.1 - self.BreakEarly.Distance=nil - - -- Late break - self.BreakLate.name="Late Break" - self.BreakLate.Xmin=-500 - self.BreakLate.Xmax=nil - self.BreakLate.Zmin=-3700 - self.BreakLate.Zmax=1500 - self.BreakLate.LimitXmin=0 - self.BreakLate.LimitXmax=nil - self.BreakLate.LimitZmin=-1470 --0.8 NM - self.BreakLate.LimitZmax=nil - self.BreakLate.Altitude=UTILS.FeetToMeters(800) - self.BreakLate.AoA=8.1 - self.BreakLate.Distance=nil - - -- Abeam position - self.Abeam.name="Abeam Position" - self.Abeam.Xmin=nil - self.Abeam.Xmax=nil - self.Abeam.Zmin=-4000 - self.Abeam.Zmax=-1000 - self.Abeam.LimitXmin=-200 - self.Abeam.LimitXmax=nil - self.Abeam.LimitZmin=nil - self.Abeam.LimitZmax=nil - self.Abeam.Altitude=UTILS.FeetToMeters(600) - self.Abeam.AoA=8.1 - self.Abeam.Distance=UTILS.NMToMeters(1.2) - - -- At the ninety - self.Ninety.name="Ninety" - self.Ninety.Xmin=-4000 - self.Ninety.Xmax=0 - self.Ninety.Zmin=-3700 - self.Ninety.Zmax=nil - self.Ninety.LimitXmin=nil - self.Ninety.LimitXmax=nil - self.Ninety.LimitZmin=nil - self.Ninety.LimitZmax=-1111 - self.Ninety.Altitude=UTILS.FeetToMeters(500) - self.Ninety.AoA=8.1 - self.Ninety.Distance=nil - - -- Wake position - self.Wake.name="Wake" - self.Wake.Xmin=-4000 - self.Wake.Xmax=0 - self.Wake.Zmin=-2000 - self.Wake.Zmax=nil - self.Wake.LimitXmin=nil - self.Wake.LimitXmax=nil - self.Wake.LimitZmin=0 - self.Wake.LimitZmax=nil - self.Wake.Altitude=UTILS.FeetToMeters(370) - self.Wake.AoA=8.1 - self.Wake.Distance=nil - - -- In the groove - self.Groove.name="Groove" - self.Groove.Xmin=-4000 - self.Groove.Xmax=100 - self.Groove.Zmin=-2000 - self.Groove.Zmax=nil - self.Groove.LimitXmin=nil - self.Groove.LimitXmax=nil - self.Groove.LimitZmin=nil - self.Groove.LimitZmax=nil - self.Groove.Altitude=UTILS.FeetToMeters(300) - self.Groove.AoA=8.1 - self.Groove.Distance=nil - - -- Landing trap - self.Trap.name="Trap" - self.Trap.Xmin=-3000 - self.Trap.Xmax=nil - self.Trap.Zmin=-2000 - self.Trap.Zmax=2000 - self.Trap.LimitXmin=nil - self.Trap.LimitXmax=nil - self.Trap.LimitZmin=nil - self.Trap.LimitZmax=nil - self.Trap.Altitude=nil - self.Trap.AoA=nil - self.Trap.Distance=nil - -end - ---- Check limits for reaching next step. --- @param #CARRIERTRAINER self --- @param #number X X position of player unit. --- @param #number Z Z position of player unit. --- @param #CARRIERTRAINER.Checkpoint check Checkpoint. --- @return #boolean If true, checkpoint condition for next step was reached. -function CARRIERTRAINER:_CheckLimits(X, Z, check) - - local nextXmin=check.LimitXmin==nil or (check.LimitXmin and (check.LimitXmin<0 and X<=check.LimitXmin or check.LimitXmin>=0 and X>=check.LimitXmin)) - local nextXmax=check.LimitXmax==nil or (check.LimitXmax and (check.LimitXmax<0 and X>=check.LimitXmax or check.LimitXmax>=0 and X<=check.LimitXmax)) - local nextZmin=check.LimitZmin==nil or (check.LimitZmin and (check.LimitZmin<0 and Z<=check.LimitZmin or check.LimitZmin>=0 and Z>=check.LimitZmin)) - local nextZmax=check.LimitZmax==nil or (check.LimitZmax and (check.LimitZmax<0 and Z>=check.LimitZmax or check.LimitZmax>=0 and Z<=check.LimitZmax)) - - local next=nextXmin and nextXmax and nextZmin and nextZmax - - - local text=string.format("step=%s: next=%s: X=%d Xmin=%s Xmax=%s | Z=%d Zmin=%s Zmax=%s", - check.name, tostring(next), X, tostring(check.LimitXmin), tostring(check.LimitXmax), Z, tostring(check.LimitZmin), tostring(check.LimitZmax)) - self:T(self.lid..text) - --MESSAGE:New(text, 1):ToAllIf(self.Debug) - - return next -end - -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- --- CARRIER TRAINING functions -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- - ---- Initialize player data. --- @param #CARRIERTRAINER self --- @param #CARRIERTRAINER.PlayerData playerData Player data. -function CARRIERTRAINER:_NewRound(playerData) - - if playerData.unit:IsInZone(self.registerZone) then - local text="Cleared for approach." - self:_SendMessageToPlayer(text, 10,playerData) - - self:_InitNewRound(playerData) - - -- Next step: start of pattern. - playerData.step=1 - end -end - ---- Start landing pattern, when player enters the start zone. --- @param #CARRIERTRAINER self --- @param #CARRIERTRAINER.PlayerData playerData Player data table. -function CARRIERTRAINER:_Start(playerData) - - if playerData.unit:IsInZone(self.startZone) then - - local hint = string.format("Entering the pattern, %s! Aim for 800 feet and 350 kts in the break entry.", playerData.callsign) - self:_SendMessageToPlayer(hint, 8, playerData) - - -- Next step: upwind. - playerData.step=2 - end - -end - ---- Upwind leg. --- @param #CARRIERTRAINER self --- @param #CARRIERTRAINER.PlayerData playerData Player data table. -function CARRIERTRAINER:_Upwind(playerData) - - -- Get distances between carrier and player unit (parallel and perpendicular to direction of movement of carrier) - local diffX, diffZ = self:_GetDistances(playerData.unit) - - -- Abort condition check. - if self:_CheckAbort(diffX,diffZ, self.Upwind) then - self:_AbortPattern(playerData, diffX, diffZ, self.Upwind) - return - end - - -- Check if we are in front of the boat (diffX > 0). - if self:_CheckLimits(diffX, diffZ, self.Upwind) then - - local altitude=playerData.unit:GetAltitude() - - -- Get altitude. - local hint=self:_AltitudeCheck(playerData, self.Upwind, altitude) - - -- Message to player - self:_SendMessageToPlayer(hint, 8, playerData) - - -- Debrief. - self:_AddToSummary(playerData, "Entering the Break", hint) - - -- Next step. - playerData.step=3 - end -end - - ---- Break. --- @param #CARRIERTRAINER self --- @param #CARRIERTRAINER.PlayerData playerData Player data table. --- @param #string part Part of the break. -function CARRIERTRAINER:_Break(playerData, part) - - -- Get distances between carrier and player unit (parallel and perpendicular to direction of movement of carrier) - local diffX, diffZ = self:_GetDistances(playerData.unit) - - -- Early or late break. - local breakpoint = self.BreakEarly - if part == "late" then - breakpoint = self.BreakLate - end - - -- Check abort conditions. - if self:_CheckAbort(diffX, diffZ, breakpoint) then - self:_AbortPattern(playerData, diffX, diffZ, breakpoint) - return - end - - -- Check limits. - if self:_CheckLimits(diffX, diffZ, breakpoint) then - - -- Get current altitude. - local altitude=playerData.unit:GetAltitude() - - -- Grade altitude. - local hint=self:_AltitudeCheck(playerData, breakpoint, altitude) - - -- Send message to player. - self:_SendMessageToPlayer(hint, 10, playerData) - - -- Debrief - if part =="late" then - self:_AddToSummary(playerData, "Late Break", hint) - else - self:_AddToSummary(playerData, "Early Entry", hint) - end - - -- Nest step: late break or abeam. - if (part == "early") then - playerData.step = 4 - else - playerData.step = 5 - end - end -end - ---- Long downwind leg check. --- @param #CARRIERTRAINER self --- @param #CARRIERTRAINER.PlayerData playerData Player data table. -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) - - local limit = -1500 - - -- Check we are not too far out w.r.t back of the boat. - if diffX < limit then - - -- Get relative heading. - local relhead=self:_GetRelativeHeading(playerData.unit) - - if relhead<45 then - - -- Message to player. - local hint = "Your downwind leg is too long. Turn to final earlier next time." - self:_SendMessageToPlayer(hint, 10, playerData) - - -- Debrief. - self:_AddToSummary(playerData, "Long Downwind Leg", hint) - - -- Decrease score. - playerData.score=playerData.score-40 - - -- Long downwind done! - playerData.longDownwindDone = true - end - - end -end - - ---- Abeam. --- @param #CARRIERTRAINER self --- @param #CARRIERTRAINER.PlayerData playerData Player data table. -function CARRIERTRAINER:_Abeam(playerData) - - -- Get distances between carrier and player unit (parallel and perpendicular to direction of movement of carrier) - local diffX, diffZ = self:_GetDistances(playerData.unit) - - -- Check abort conditions. - if self:_CheckAbort(diffX, diffZ, self.Abeam) then - self:_AbortPattern(playerData, diffX, diffZ, self.Abeam) - return - end - - -- Check nest step threshold. - if self:_CheckLimits(diffX, diffZ, self.Abeam) then - - -- Checks: - -- AoA - -- Altitude - -- Distance to carrier. - - -- Get AoA. - local aoa = playerData.unit:GetAoA() - local alt = playerData.unit:GetAltitude() - - -- Grade AoA. - local hintAoA=self:_AoACheck(playerData, self.Abeam, aoa) - - -- Grade Altitude. - local hintAlt=self:_AltitudeCheck(playerData, self.Abeam, alt) - - -- Grade distance to carrier. - local hintDist=self:_DistanceCheck(playerData, self.Abeam, math.abs(diffZ)) - - -- Compile full hint. - local hintFull=string.format("%s\n%s\n%s", hintAlt, hintAoA, hintDist) - - -- Send message to playerr. - self:_SendMessageToPlayer(hintFull, 10, playerData) - - -- Add to debrief. - self:_AddToSummary(playerData, "Abeam Position", hintFull) - - -- Proceed to next step. - playerData.step = 6 - end -end - ---- Ninety. --- @param #CARRIERTRAINER self --- @param #CARRIERTRAINER.PlayerData playerData Player data table. -function CARRIERTRAINER:_Ninety(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(diffZ < -3700 or diffX < -3700 or diffX > 0) then - if self:_CheckAbort(diffX, diffZ, self.Ninety) then - self:_AbortPattern(playerData, diffX, diffZ, self.Ninety) - return - end - - -- Get Realtive heading player to carrier. - local relheading=self:_GetRelativeHeading(playerData.unit) - - -- At the 90, i.e. 90 degrees between player heading and BRC of carrier. - if relheading<=90 then - - local alt=playerData.unit:GetAltitude() - local aoa=playerData.unit:GetAoA() - - -- Grade altitude. - local hintAlt=self:_AltitudeCheck(playerData, self.Ninety, alt) - - -- Grade AoA. - local hintAoA=self:_AoACheck(playerData, self.Ninety, aoa) - - -- Compile full hint. - local hintFull=string.format("%s\n%s", hintAlt, hintAoA) - - -- Message to player. - self:_SendMessageToPlayer(hintFull, 10, playerData) - - -- Add to debrief. - self:_AddToSummary(playerData, "At the 90", hintFull) - - -- Long downwind not an issue any more - playerData.longDownwindDone = true - - -- Next step: wake. - playerData.step = 7 - end -end - ---- Wake. --- @param #CARRIERTRAINER self --- @param #CARRIERTRAINER.PlayerData playerData Player data table. -function CARRIERTRAINER:_Wake(playerData) - - -- Get distances between carrier and player unit (parallel and perpendicular to direction of movement of carrier) - local diffX, diffZ = self:_GetDistances(playerData.unit) - - -- Check abort conditions. - if self:_CheckAbort(diffX, diffZ, self.Wake) then - self:_AbortPattern(playerData, diffX, diffZ, self.Wake) - return - end - - -- Right behind the wake of the carrier dZ>0. - if self:_CheckLimits(diffX, diffZ, self.Wake) then - - local alt=playerData.unit:GetAltitude() - local aoa=playerData.unit:GetAoA() - - -- Grade altitude. - local hintAlt=self:_AltitudeCheck(playerData, self.Wake, alt) - - -- Grade AoA. - local hintAoA=self:_AoACheck(playerData, self.Wake, aoa) - - -- Compile full hint. - local hintFull=string.format("%s\n%s", hintAlt, hintAoA) - - -- Message to player. - self:_SendMessageToPlayer(hintFull, 10, playerData) - - -- Add to debrief. - self:_AddToSummary(playerData, "At the Wake", hintFull) - - -- Next step: Groove. - playerData.step = 8 - end -end - ---- Entering the Groove. --- @param #CARRIERTRAINER self --- @param #CARRIERTRAINER.PlayerData playerData Player data table. -function CARRIERTRAINER:_Groove(playerData) - - -- Get distances between carrier and player unit (parallel and perpendicular to direction of movement of carrier) - local diffX, diffZ = self:_GetDistances(playerData.unit) - - -- In front of carrier or more than 4 km behind carrier. - if self:_CheckAbort(diffX, diffZ, self.Groove) then - self:_AbortPattern(playerData, diffX, diffZ, self.Groove) - return - end - - -- Get heading of runway. - local brc=self.carrier:GetHeading() - local rwy=brc-10 --runway heading is -10 degree from carrier BRC. - if rwy<0 then - rwy=rwy+360 - end - -- Radial (inverse heading). - rwy=rwy-180 - - -- 0 means player is on BRC course but runway heading is -10 degrees. - local heading=self:_GetRelativeHeading(playerData.unit)-10 - - if diffZ>-1300 and heading<10 then - - local alt = playerData.unit:GetAltitude() - local aoa = playerData.unit:GetAoA() - - -- Grade altitude. - local hintAlt=self:_AltitudeCheck(playerData, self.Groove, alt) - - -- AoA feed back - local hintAoA=self:_AoACheck(playerData, self.Groove, aoa) - - -- Compile full hint. - local hintFull=string.format("%s\n%s", hintAlt, hintAoA) - - -- Message to player. - self:_SendMessageToPlayer(hintFull, 10, playerData) - - -- Add to debrief. - self:_AddToSummary(playerData, "Entering the Groove", hintFull) - - -- Next step. - playerData.step = 9 - end - -end - ---- Call the ball, i.e. 3/4 NM distance between aircraft and carrier. --- @param #CARRIERTRAINER self --- @param #CARRIERTRAINER.PlayerData playerData Player data table. -function CARRIERTRAINER:_CallTheBall(playerData) - - -- Get distances between carrier and player unit (parallel and perpendicular to direction of movement of carrier) - local diffX, diffZ, rho, phi = self:_GetDistances(playerData.unit) - - -- Player altitude - local alt=playerData.unit:GetAltitude() - - -- Get velocities. - local playerVelocity = playerData.unit:GetVelocityKMH() - local carrierVelocity = self.carrier:GetVelocityKMH() - - -- Check abort conditions. - if self:_CheckAbort(diffX, diffZ, self.Trap) then - self:_AbortPattern(playerData, diffX, diffZ, self.Trap) - return - end - - -- Lineup. We need to correct for the end of the carrier deck and the tilted angle of the runway. - -- TODO: make this parameter of the carrier. - local lineup = math.asin(diffZ/(-(diffX-100))) - local lineuperror = math.deg(lineup)-10 - - -- Glideslope. Wee need to correct for the height of the deck. The ideal glide slope is 3.5 degrees. - -- TODO: make this parameter of the carrier. - local glideslope = math.atan((playerData.unit:GetAltitude()-22)/(-diffX)) - local glideslopeError = math.deg(glideslope) - 3.5 - - if diffX>-UTILS.NMToMeters(0.75) and diffX<-100 and playerData.calledball==false then - self:_SendMessageToPlayer("Call the ball.", 8, playerData) - playerData.calledball=true - return - end - - -- Check if we are beween 3/4 NM and end of ship. - if diffX>-UTILS.NMToMeters(0.75) and diffX<-100 then - - local text="Good height." - if glideslopeError>1 then - text="You're too high! Throttles back!" - elseif glideslopeError>0.5 then - text="You're slightly high. Decrease power." - elseif glideslopeError<1.0 then - text="Power! You're way too low." - elseif glideslopeError<0.5 then - text="You're slightly low. Increase power." - else - end - - local aoa=playerData.unit:GetAoA() - if aoa>=9.0 then - text=text.." You're way too slow!" - elseif aoa>=8.5 then - text=text.." You're slow." - elseif aoa<6.9 then - text=text.." You're too fast!" - elseif aoa<7.7 then - text=text.." You're slightly fast." - else - text=text.." Looking good on speed." - end - text=text.."\n" - - if lineuperror>3 then - text=text.."Come left!" - elseif lineuperror >1 then - text=text.."Come left..." - elseif lineuperror <3 then - text=text.."Right for lineup!" - elseif lineuperror <1 then - text=text.."Right for lineup..." - else - text=text.."Good on lineup." - end - - self:_SendMessageToPlayer(text, 8, playerData) - - 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, "Calling the Ball", hint) - - self:_PrintFinalScore(playerData, 60, wire) - self:_HandleCollectedResult(playerData, wire) - - playerData.step = 0 - end -end - ---- Trapped? --- @param #CARRIERTRAINER self --- @param #CARRIERTRAINER.PlayerData playerData Player data table. --- @param Core.Point#COORDINATE pos Position of aircraft on landing event. -function CARRIERTRAINER:_Trapped(playerData, pos) - - -- Get distances between carrier and player unit (parallel and perpendicular to direction of movement of carrier) - local diffX, diffZ, rho, phi = self:_GetDistances(pos) - - -- Get velocities. - local playerVelocity = playerData.unit:GetVelocityKMH() - local carrierVelocity = self.carrier:GetVelocityKMH() - - if playerData.unit:InAir()==false then - -- Seems we have successfully landed. - - local wire = 1 - local score = -10 - - -- Which wire - 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 - - local text=string.format("TRAPPED! %d-wire.", wire) - self:_SendMessageToPlayer(text, 30, playerData) - - local text2=string.format("Distance %.1f meters resulted in a %d-wire estimate.", diffX, wire) - MESSAGE:New(text,30):ToAllIf(self.Debug) - env.info(text2) - - - local fullHint = string.format("Trapped catching the %d-wire.", wire) - self:_AddToSummary(playerData, "Trapped", fullHint) - - else - --Boltered! - 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 playerData=self.players[playername] - - -- F10/Carrier Trainer/ - local _trainPath = missionCommands.addSubMenuForGroup(_gid, self.alias, CARRIERTRAINER.MenuF10[_gid]) - -- F10/Carrier Trainer//Results - --local _statsPath = missionCommands.addSubMenuForGroup(_gid, "Results", _trainPath) - -- F10/Carrier Trainer//My Settings - local _settingsPath = missionCommands.addSubMenuForGroup(_gid, "My Settings", _trainPath) - -- F10/Carrier Trainer//My Settings/Difficulty - local _difficulPath = missionCommands.addSubMenuForGroup(_gid, "Difficulty", _settingsPath) - -- F10/Carrier Trainer//Carrier Info - local _infoPath = missionCommands.addSubMenuForGroup(_gid, "Carrier Info", _trainPath) - - -- F10/Carrier Trainer//Stats/ - --missionCommands.addCommandForGroup(_gid, "All Results", _statsPath, self._DisplayStrafePitResults, self, _unitName) - --missionCommands.addCommandForGroup(_gid, "My Results", _statsPath, self._DisplayBombingResults, self, _unitName) - --missionCommands.addCommandForGroup(_gid, "Reset All Results", _statsPath, self._ResetRangeStats, self, _unitName) - -- F10/Carrier Trainer//My Settings/ - --missionCommands.addCommandForGroup(_gid, "Smoke Delay On/Off", _settingsPath, self._SmokeBombDelayOnOff, self, _unitName) - --missionCommands.addCommandForGroup(_gid, "Smoke Impact On/Off", _settingsPath, self._SmokeBombImpactOnOff, self, _unitName) - --missionCommands.addCommandForGroup(_gid, "Flare Hits On/Off", _settingsPath, self._FlareDirectHitsOnOff, self, _unitName) - -- F10/Carrier Trainer//My Settings/Difficulty - missionCommands.addCommandForGroup(_gid, "Flight Student", _difficulPath, self.SetDifficulty, self, playerData, CARRIERTRAINER.Difficulty.EASY) - missionCommands.addCommandForGroup(_gid, "Naval Aviator", _difficulPath, self.SetDifficulty, self, playerData, CARRIERTRAINER.Difficulty.NORMAL) - missionCommands.addCommandForGroup(_gid, "TOPGUN Graduate", _difficulPath, self.SetDifficulty, self, playerData, CARRIERTRAINER.Difficulty.HARD) - -- F10/Carrier Trainer//Carrier Info/ - missionCommands.addCommandForGroup(_gid, "Carrier Info", _infoPath, self._DisplayCarrierInfo, self, _unitName) - missionCommands.addCommandForGroup(_gid, "Weather Report", _infoPath, self._DisplayCarrierWeather, self, _unitName) - 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 - ---- Set difficulty level. --- @param #CARRIERTRAINER self --- @param #CARRIERTRAINER.PlayerData playerData Player data. --- @param #CARRIERTRAINER.Difficulty difficulty Difficulty level. -function CARRIERTRAINER:SetDifficulty(playerData, difficulty) - playerData.difficulty=difficulty -end - ---- Report information about carrier. --- @param #CARRIERTRAINER self --- @param #string _unitname Name of the player unit. -function CARRIERTRAINER:_DisplayCarrierInfo(_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=string.format("%s info:\n", self.alias) - - -- Current coordinates. - local coord=self.carrier:GetCoordinate() - - local playerData=self.players[playername] --#CARRIERTRAINER.PlayerData - - local carrierheading=self.carrier:GetHeading() - local carrierspeed=UTILS.MpsToKnots(self.carrier:GetVelocity()) - - text=text..string.format("BRC %d\n", carrierheading) - text=text..string.format("Speed %d kts\n", carrierspeed) - - - local tacan="unknown" - local icls="unknown" - if self.TACAN~=nil then - tacan=tostring(self.TACAN) - end - if self.ICLS~=nil then - icls=tostring(self.ICLS) - end - - text=text..string.format("TACAN Channel %s", tacan) - text=text..string.format("ICLS Channel %s", icls) - - self:_SendMessageToPlayer(text, 20, playerData) - - end - -end - - ---- Report weather conditions at the carrier location. 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 -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- - ---- Evaluate player's altitude at checkpoint. --- @param #CARRIERTRAINER self --- @param #CARRIERTRAINER.PlayerData playerData Player data table. --- @return #number Low score. --- @return #number Bad score. -function CARRIERTRAINER:_GetGoodBadScore(playerData) - - local lowscore - local badscore - if playerData.difficulty==CARRIERTRAINER.Difficulty.EASY then - lowscore=10 - badscore=20 - elseif playerData.difficulty==CARRIERTRAINER.Difficulty.NORMAL then - lowscore=5 - badscore=10 - elseif playerData.difficulty==CARRIERTRAINER.Difficulty.HARD then - lowscore=2.5 - badscore=5 - end - - return lowscore, badscore -end - ---- Evaluate player's altitude at checkpoint. --- @param #CARRIERTRAINER self --- @param #CARRIERTRAINER.PlayerData playerData Player data table. --- @param #CARRIERTRAINER.Checkpoint checkpoint Checkpoint. --- @param #number altitude Player's current altitude in meters. --- @return #string Feedback text. -function CARRIERTRAINER:_AltitudeCheck(playerData, checkpoint, altitude) - - -- Player altitude. - local altitude=playerData.unit:GetAltitude() - - -- Get relative score. - local lowscore, badscore = self:_GetGoodBadScore(playerData) - - -- Altitude error +-X% - local _error=(altitude-checkpoint.Altitude)/checkpoint.Altitude*100 - - local score - local hint - local steptext=self:_StepName(playerData.step) - - - if _error>badscore then - score = -10 - hint = string.format("You're high %s. ", steptext) - elseif _error>lowscore then - score = -5 - hint = string.format("You're slightly high %s. ", steptext) - elseif _error<-badscore then - score = -10 - hint = string.format("You're low %s.", steptext) - elseif _error<-lowscore then - score = -5 - hint = string.format("You're slightly low %s. ", steptext) - else - score = 0 - hint = string.format("Good altitude %s. ", steptext) - end - - hint=hint..string.format(" Altitude %d ft = %d%% deviation from %d ft target alt.", UTILS.MetersToFeet(altitude), _error, UTILS.MetersToFeet(checkpoint.Altitude)) - - -- Set score. - playerData.score=playerData.score+score - - return hint -end - ---- Evaluate player's altitude at checkpoint. --- @param #CARRIERTRAINER self --- @param #CARRIERTRAINER.PlayerData playerData Player data table. --- @param #CARRIERTRAINER.Checkpoint checkpoint Checkpoint. --- @param #number distance Player's current distance to the boat in meters. --- @return #string Feedback message text. -function CARRIERTRAINER:_DistanceCheck(playerData, checkpoint, distance) - - -- Get relative score. - local lowscore, badscore = self:_GetGoodBadScore(playerData) - - -- Altitude error +-X% - local _error=(distance-checkpoint.Distance)/checkpoint.Distance*100 - - local score - local hint - local steptext=self:_StepName(playerData.step) - if _error>badscore then - score = -10 - hint = string.format("You're too far from the boat!") - elseif _error>lowscore then - score = -5 - hint = string.format("You're slightly too far from the boat.") - elseif _error<-badscore then - score = -10 - hint = string.format( "You're too close to the boat!") - elseif _error<-lowscore then - score = -5 - hint = string.format("slightly too far from the boat.") - else - score = 0 - hint = string.format("with perfect distance to the boat.") - end - - hint=hint..string.format(" Distance %.1f NM = %d%% deviation from %.1f NM optimal distance.",UTILS.MetersToNM(distance), _error, UTILS.MetersToNM(checkpoint.Distance)) - - -- Set score. - playerData.score=playerData.score+score - - return hint -end - ---- Score for correct AoA. --- @param #CARRIERTRAINER self --- @param #CARRIERTRAINER.PlayerData playerData Player data. --- @param #CARRIERTRAINER.Checkpoint checkpoint Checkpoint. --- @param #number aoa Player's current Angle of attack. --- @return #string hint Feedback message text. -function CARRIERTRAINER:_AoACheck(playerData, checkpoint, aoa) - - -- Get relative score. - local lowscore, badscore = self:_GetGoodBadScore(playerData) - - -- Altitude error +-X% - local _error=(aoa-checkpoint.AoA)/checkpoint.AoA*100 - - local score = 0 - local hint="" - if _error>badscore then --Slow - score = -10 - hint = "You're slow." - elseif _error>lowscore then --Slightly slow - score = -5 - hint = "You're slightly slow." - elseif _error<-badscore then --Fast - score = -10 - hint = "You're fast." - elseif _error<-lowscore then --Slightly fast - score = -5 - hint = "You're slightly fast." - else --On speed - score = 0 - hint = "You're on speed!" - end - - hint=hint..string.format(" AoA %.1f = %d %% deviation from %.1f target AoA.", aoa, _error, checkpoint.AoA) - - -- Set score. - playerData.score=playerData.score+score - - 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. --- @param #boolean printtotal Also print total score. -function CARRIERTRAINER:_PrintScore(score, playerData, printtotal) - - if printtotal 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 - - --TODO: collect results - --[[ - 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.registerZone:GetCoordinate()) - local distance=playerData.unit:GetCoordinate():Get2DDistance(self.registerZone: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 - -